Skip to content

feat(fula-js): expose putFlat, getFlat, listFilesFromForest (forest-tracked I/O)#82

Merged
ehsan6sha merged 1 commit into
mainfrom
feat/fula-js-forest-bindings
Jun 24, 2026
Merged

feat(fula-js): expose putFlat, getFlat, listFilesFromForest (forest-tracked I/O)#82
ehsan6sha merged 1 commit into
mainfrom
feat/fula-js-forest-bindings

Conversation

@ehsan6sha

Copy link
Copy Markdown
Member

Problem

The hosted MCP Worker stores AI files into fula-ai-workspace, but they never appear in FxFiles. Root cause (proven empirically with a native probe and a real-wasm Node harness against the prod gateway): the Worker's only upload binding, putEncryptedWithType, is a RAW encrypted-object PUT — it writes the blob but never registers it in the bucket's private-forest index, so the file is readable only by its exact obfuscated key and invisible to every list path. FxFiles' own uploads don't have this problem because the Flutter FFI uses the forest-tracked put_object_flat.

Change

Three additive #[wasm_bindgen] exports in fula-js, each wrapping an existing, unchanged fula-client method (the same ones the Flutter FFI already uses):

binding wraps purpose
putFlat put_object_flat upload and index (upsert + flush_forest)
getFlat get_object_flat forest + CID-aware read (pairs with putFlat)
listFilesFromForest list_files_from_forest enumerate the forest (decrypted keys)

No fula-client logic changes — purely new exports. Normal files (FxFiles via the FFI) are unaffected; the format is identical.

Test

Node harness on the freshly-built wasm, against the prod gateway with a real workspace secret:

  • putFlat a file → listFilesFromForest enumerates it → getFlat round-trips byte-exact. ✅
  • Return shape confirmed {storageKey, originalKey, …} (camelCase), matching the existing listDecrypted contract.

Follow-up

The hosted Worker (separate PR) switches storeFileputFlat, fula_read_filegetFlat, listFiles/searchlistFilesFromForest. After merge, @functionland/fula-client must be republished with the new exports.

🤖 Generated with Claude Code

…racked I/O)

The WASM bindings only exposed `putEncryptedWithType` — a RAW encrypted-object
PUT that writes the blob but never registers it in the bucket's private-forest
index. Objects written that way are readable only by their exact obfuscated key
and are INVISIBLE to listDecrypted / listFilesFromForest / listDirectory. That is
why the hosted MCP Worker's AI-workspace writes never appeared in FxFiles.

Add three additive bindings wrapping EXISTING, unchanged fula-client methods that
FxFiles' own Flutter FFI already uses for every file it writes/reads:
  - putFlat             -> EncryptedClient::put_object_flat        (upsert + flush_forest)
  - getFlat             -> EncryptedClient::get_object_flat        (forest + CID-aware read)
  - listFilesFromForest -> EncryptedClient::list_files_from_forest (forest enumeration)

No fula-client logic changes; purely additive exports. Verified end-to-end against
the prod gateway via a Node harness on the freshly-built wasm: putFlat ->
listFilesFromForest enumerates the write -> getFlat round-trips byte-exact.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0161UGEJmTpM6DM2cVZyV6Ev
@ehsan6sha ehsan6sha merged commit b11a698 into main Jun 24, 2026
8 of 9 checks passed
ehsan6sha added a commit to functionland/pinning-service that referenced this pull request Jun 24, 2026
…stFilesFromForest) (#68)

The hosted MCP Worker stored AI files with putEncryptedWithType — a raw encrypted-
object PUT that never registers the object in the bucket's private-forest index.
Result: AI files were readable by exact key but NOT enumerable, so they never appeared
in FxFiles (or the AI's own fula_list_files). See functionland/fula-api#82.

Switch every workspace I/O path to the forest-tracked bindings (the same ones the
FxFiles app already uses):
  - storeFile + applyTaggingToDoc  putEncryptedWithType -> putFlat (upsert + flush)
  - readFile + tag-doc read         getDecrypted        -> getFlat (forest + CID read)
  - listFiles + search              listDecrypted       -> listFilesFromForest
    (listFiles' category narrowing moves to the post-filter; the raw listDecrypted
     prefix-filtered the OBFUSCATED storage keys and returned nothing for this bucket.)

Concurrency: put_object_flat flushes the forest with a conditional PUT; a concurrent
writer makes it 412 (ConcurrentModification). The wasm flush is single-attempt, so
withWorkspaceClient now retries on 412 — re-creating the client drops the stale forest
cache so the next attempt reloads the winner's forest and re-applies the write
(bounded + jittered; read-only bodies never 412).

Requires @functionland/fula-client ^0.6.17 (the build that exports putFlat/getFlat/
listFilesFromForest). Verified: tsc clean + a workerd (vitest-pool-workers) integration
test driving putFlat -> listFilesFromForest -> getFlat against the prod gateway, in the
Worker runtime, round-trips byte-exact.


Claude-Session: https://claude.ai/code/session_0161UGEJmTpM6DM2cVZyV6Ev

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
ehsan6sha added a commit to functionland/FxFiles that referenced this pull request Jun 25, 2026
…rol (1.i/1.ii/1.iii) (#93)

After the Worker writes AI files forest-indexed (functionland/pinning-service#68 +
functionland/fula-api#82), FxFiles can see + move them. Three parts:

P1 (1.i) category map: the MCP writes SINGULAR category segments (ai/document/,
ai/note/, ai/image/ …); FxFiles' category views are PLURAL. _mergeAiWorkspaceInto now
maps them (images<-image+screenshot, documents<-document+note+link, other<-file+other)
and folds the AI categories with no FileCategory of their own into the closest view. A
view with no AI mapping (downloads/archives/starred) short-circuits.

P2 (1.i) cloud-files: the bucket view routes fula-ai-workspace through the workspace
client (workspace secret) instead of the master-KEK listObjects, so the AI files
decrypt + list, tagged sourceBucket for correct open/move routing.

P3 (1.ii/1.iii) move-as-access-control: moving a file INTO the AI bucket re-encrypts it
under the workspace secret + forest-indexes it (grant AI read) under an
ai/<category>/<name> key; moving OUT re-encrypts under the master KEK and removes the
workspace copy (revoke). The revoke is VERIFIED: the re-encrypted master copy must
decrypt BEFORE the AI copy is deleted, then the AI copy is verified gone — "revoke
incomplete" is surfaced loudly otherwise. All re-keying is client-side (the KEK never
leaves FxFiles). New primitives: uploadWorkspaceObject (putFlat), deleteWorkspaceObject
(deleteFlat, which removes the forest entry too).

The move orchestration is factored into a UI-free aiAwareMove() so the security-
critical revoke is unit-tested (grant / verified-revoke / abort-without-delete-on-
failed-verify / normal-move). flutter analyze clean (0 errors); 25 AI unit tests green.


Claude-Session: https://claude.ai/code/session_0161UGEJmTpM6DM2cVZyV6Ev

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
ehsan6sha added a commit that referenced this pull request Jun 25, 2026
#83)

* test(gateway): update stale auth.rs MCP-scope test to match a12ed4d

a12ed4d ("scope MCP tokens to dedicated bucket, not ai/ key prefix") deliberately
stopped enforcing the `ai/` key prefix in McpScope::assert — AI-workspace keys are
obfuscated (flat-namespace/metadata-privacy for FxFiles compat) so real S3 keys never
carry `ai/`, and the prefix check was 403-ing every legitimate AI write. That commit
updated the mcp_scope.rs test but missed a DUPLICATE assertion in auth.rs
(mcp_token_aimage_boundary_key_is_access_denied), which kept asserting the removed
behavior and failed CI — surfaced only when #82's fula-js change triggered Flutter CI
(the test-rust job had not run on a12ed4d).

Update the stale auth.rs test to match the intended design: within the scoped DEDICATED
bucket the key prefix is NOT enforced (isolation is bucket + (hashed_user_id,bucket) +
sub); a DIFFERENT bucket is still denied. No production code change. fula-cli --lib now
227/227 green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0161UGEJmTpM6DM2cVZyV6Ev

* test(gateway): also assert an obfuscated storage key is allowed (codex review)

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant