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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions