Phase 5 — store_file operation (the scoped write path)
The core "AI stores a file" library function for the fula-mcp crate, building on P1 (encrypt/upload/download), P2 (proven inverse-share model), P3 (capability bundle + canonicalized scope), and P4 (categorization + bucket routing). Security-critical: this is the scoped write path. MCP-protocol wiring comes later (P9); here we build + test the operation as a library function.
The model (from the P2 finding)
The AI writes each file into its OWN workspace (separate buckets, encrypted with the dedicated workspace secret — NOT the user's master KEK), then mints a per-file ShareToken wrapping that file's content DEK to the owner's public key so the owner can read it. The AI never writes the owner's forest.
Build
pub async fn store_file(cap: &CapabilityBundle, content: Bytes, filename: &str, mime: Option<&str>, text: Option<&str>, category_override: Option<Category>) -> Result<StoreOutcome>:
category = category_override.unwrap_or_else(|| classify(mime, filename, text)).
- Compute workspace target: key =
ai/<category>/<uuid>-<safe-filename> in a single workspace bucket. Keys MUST be canonical (canon from P3 rejects ../empty segments).
assert_in_scope(key, "ai", Write)? BEFORE any I/O.
- Upload via
workspace_client().put_object_flat(bucket, key, content, content_type).
- Recover the per-file content DEK:
list_files_from_forest -> storage_key; get_object_encryption_metadata_with_fallback -> unwrap wrapped_key with the workspace keypair -> DekKey; read top-level nonce (single-block) and/or chunked (chunked) independently.
mint_owner_share(dek, storage_key, nonce?, chunked?, expiry) -> owner-read ShareToken.
- Return
StoreOutcome { key, bucket, storage_key, etag, category, native_bucket, owner_share }.
Tests
- Offline (green, no network): classify routing into the right key/native_bucket;
assert_in_scope is actually called (out-of-scope write rejected); owner-share round-trips (owner accept_share -> DEK -> decrypts bytes), small + chunked sizes.
- Gated e2e (
#[ignore] + FULA_E2E=1): store_file a unique blob into a disposable workspace bucket, then as the owner use the returned share to read it back byte-identically; clean up. Includes a >768 KB chunked payload to exercise the chunked recover->share->owner-read loop.
Out of scope (later phases)
Tagging (P8), quota (P10), MCP protocol wiring (P9).
PR will target feat/fula-mcp (not main).
Phase 5 —
store_fileoperation (the scoped write path)The core "AI stores a file" library function for the
fula-mcpcrate, building on P1 (encrypt/upload/download), P2 (proven inverse-share model), P3 (capability bundle + canonicalized scope), and P4 (categorization + bucket routing). Security-critical: this is the scoped write path. MCP-protocol wiring comes later (P9); here we build + test the operation as a library function.The model (from the P2 finding)
The AI writes each file into its OWN workspace (separate buckets, encrypted with the dedicated workspace secret — NOT the user's master KEK), then mints a per-file
ShareTokenwrapping that file's content DEK to the owner's public key so the owner can read it. The AI never writes the owner's forest.Build
pub async fn store_file(cap: &CapabilityBundle, content: Bytes, filename: &str, mime: Option<&str>, text: Option<&str>, category_override: Option<Category>) -> Result<StoreOutcome>:category = category_override.unwrap_or_else(|| classify(mime, filename, text)).ai/<category>/<uuid>-<safe-filename>in a single workspace bucket. Keys MUST be canonical (canon from P3 rejects../empty segments).assert_in_scope(key, "ai", Write)?BEFORE any I/O.workspace_client().put_object_flat(bucket, key, content, content_type).list_files_from_forest->storage_key;get_object_encryption_metadata_with_fallback-> unwrapwrapped_keywith the workspace keypair ->DekKey; read top-levelnonce(single-block) and/orchunked(chunked) independently.mint_owner_share(dek, storage_key, nonce?, chunked?, expiry)-> owner-readShareToken.StoreOutcome { key, bucket, storage_key, etag, category, native_bucket, owner_share }.Tests
assert_in_scopeis actually called (out-of-scope write rejected); owner-share round-trips (owneraccept_share-> DEK -> decrypts bytes), small + chunked sizes.#[ignore]+FULA_E2E=1):store_filea unique blob into a disposable workspace bucket, then as the owner use the returned share to read it back byte-identically; clean up. Includes a >768 KB chunked payload to exercise the chunked recover->share->owner-read loop.Out of scope (later phases)
Tagging (P8), quota (P10), MCP protocol wiring (P9).
PR will target
feat/fula-mcp(notmain).