Phase 7 of the fula-mcp line — scoped enumeration
Build the AI-workspace enumeration operations as library functions (MCP-protocol wiring is later, in P9):
list_files(cap, filter) -> Vec<FileEntry> — list the AI''s own workspace forest, scope-confined to ai/, with optional category and sub-prefix narrowing.
search(cap, query) -> Vec<FileEntry> — list_files over the whole ai/ scope, then filter where the filename contains query case-insensitively.
FileEntry { key, size, content_type, category, modified_at } (category derived from the ai/<category>/ key segment, falling back to classify).
Security: scope confinement (the crux)
Results MUST be limited to the AI''s ai/ workspace scope. The AI fully owns its workspace but must never enumerate the user''s real buckets. Enumeration is confined by the P3 segment-boundary geometry (reusing assert_in_scope(key, ai, Read) per entry), NOT a substring match — so ai-evil/... and any non-ai/ key can never leak. The scope gate runs BEFORE any client/I/O.
Builds on
- P3
capability.rs (assert_in_scope, workspace_client, Permission)
- P4
category.rs (Category, classify)
- P5
store.rs (WORKSPACE_BUCKET, WORKSPACE_KEY_PREFIX, key convention)
- P6
read.rs (gate-before-I/O pattern, error style)
- SDK
EncryptedClient::list_files_from_forest(bucket) -> Vec<FileMetadata>
Tests
- Offline (scope confinement): construct forest entries directly (both
ai/... and non-ai/ keys, no network); assert list_files returns ONLY ai/ ones; category filter narrows; the gate is provably called before I/O (drive the real async fn against an unreachable endpoint and assert a Capability error, not a network error); search matches case-insensitively on the filename and never leaks non-ai/ keys.
- Gated e2e (
#[ignore] + FULA_E2E=1): store a few files across categories into a disposable workspace bucket, then list_files (all + by category) and search by a filename fragment; assert the stored keys appear with the right category and nothing else; cleanup.
Out of scope
Tag-based search/filter is P8; MCP tool wiring is P9. No new crypto.
Phase 7 of the
fula-mcpline — scoped enumerationBuild the AI-workspace enumeration operations as library functions (MCP-protocol wiring is later, in P9):
list_files(cap, filter) -> Vec<FileEntry>— list the AI''s own workspace forest, scope-confined toai/, with optionalcategoryand sub-prefixnarrowing.search(cap, query) -> Vec<FileEntry>—list_filesover the wholeai/scope, then filter where the filename containsquerycase-insensitively.FileEntry { key, size, content_type, category, modified_at }(category derived from theai/<category>/key segment, falling back toclassify).Security: scope confinement (the crux)
Results MUST be limited to the AI''s
ai/workspace scope. The AI fully owns its workspace but must never enumerate the user''s real buckets. Enumeration is confined by the P3 segment-boundary geometry (reusingassert_in_scope(key, ai, Read)per entry), NOT a substring match — soai-evil/...and any non-ai/key can never leak. The scope gate runs BEFORE any client/I/O.Builds on
capability.rs(assert_in_scope,workspace_client,Permission)category.rs(Category,classify)store.rs(WORKSPACE_BUCKET,WORKSPACE_KEY_PREFIX, key convention)read.rs(gate-before-I/O pattern, error style)EncryptedClient::list_files_from_forest(bucket) -> Vec<FileMetadata>Tests
ai/...and non-ai/keys, no network); assertlist_filesreturns ONLYai/ones; category filter narrows; the gate is provably called before I/O (drive the real async fn against an unreachable endpoint and assert a Capability error, not a network error);searchmatches case-insensitively on the filename and never leaks non-ai/keys.#[ignore]+FULA_E2E=1): store a few files across categories into a disposable workspace bucket, thenlist_files(all + by category) andsearchby a filename fragment; assert the stored keys appear with the right category and nothing else; cleanup.Out of scope
Tag-based search/filter is P8; MCP tool wiring is P9. No new crypto.