Skip to content

P5: store_file operation (scoped write + owner-share) #57

Description

@ehsan6sha

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>:

  1. category = category_override.unwrap_or_else(|| classify(mime, filename, text)).
  2. Compute workspace target: key = ai/<category>/<uuid>-<safe-filename> in a single workspace bucket. Keys MUST be canonical (canon from P3 rejects ../empty segments).
  3. assert_in_scope(key, "ai", Write)? BEFORE any I/O.
  4. Upload via workspace_client().put_object_flat(bucket, key, content, content_type).
  5. 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.
  6. mint_owner_share(dek, storage_key, nonce?, chunked?, expiry) -> owner-read ShareToken.
  7. 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).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions