diff --git a/docs/explorations/0270_[_]_DESKTOP_FILESYSTEM_AS_A_GOVERNED_CAPABILITY.md b/docs/explorations/0270_[_]_DESKTOP_FILESYSTEM_AS_A_GOVERNED_CAPABILITY.md
new file mode 100644
index 000000000..55954acf5
--- /dev/null
+++ b/docs/explorations/0270_[_]_DESKTOP_FILESYSTEM_AS_A_GOVERNED_CAPABILITY.md
@@ -0,0 +1,710 @@
+# 0270 - Desktop Filesystem As A Governed Capability
+
+## Problem Statement
+
+What becomes possible if the xNet desktop app can see and touch the user's
+file system — and how do we do that without betraying the platform's core
+promise ("your data is yours, we can't read it") or turning the workspace
+into a prompt-injection-powered exfiltration machine?
+
+Two distinct opportunities hide in the question:
+
+1. **Loading the brain.** The user's real life lives in folders: contracts,
+ tax PDFs, screenshots, research papers, repos, exports from every silo.
+ If xNet can index granted folders, the "digital brain" stops being only
+ what the user manually pastes in and starts being an ambient map of what
+ they actually have.
+2. **Doing the work.** xNet is growing hands — plugins (0189/0192),
+ connectors (0196), outbound actions (0213), in-app AI (0192/0208), and
+ the agent bridge that drives the user's own Claude Code/Codex (0194).
+ Today those hands can only touch nodes. With a filesystem capability
+ they could compose real documents, synthesize analysis across local
+ sources, materialize databases as CSVs, maintain a folder of living
+ reports — actual artifacts in the user's world, not just in ours.
+
+The user's framing is right about the platform split: this is a
+**desktop (Electron) capability**. The browser can approximate a slice of
+it on Chromium only; Safari/Firefox get none of it beyond OPFS.
+
+Exploration [0127](0127_[_]_XNET_FILESYSTEM_INTEGRATION_AND_GLOBAL_FILE_NAMESPACE.md)
+already designed the *ingestion* half in depth (integration modes A–E,
+IndexedFolder/IndexedFile schemas, scanner/watcher pipelines, managed
+folder, availability states). It remains unimplemented and remains the
+reference design for indexing. What 0127 could **not** design — because the
+machinery didn't exist yet — is the *actor* half: how plugins, connectors,
+actions, and agents get filesystem powers safely. Since 0127 we shipped the
+trust fabric (provenance-derived tiers, `packages/trust`), the FeatureModule
+capability manifest + hub secret broker (0189), `guardStore`/`guardedFetch`
+(0192), `defineConnector`/`defineAction` (0196/0213), and the MCP server on
+`:31416` (0194). This exploration slots the filesystem into that fabric as
+one more brokered capability — and works through the security model that
+makes it survivable.
+
+## Executive Summary
+
+- **Treat the filesystem exactly like secrets and network: a declared,
+ brokered, never-ambient capability.** Plugins declare
+ `capabilities.filesystem`; the user maps *specific granted roots* to
+ *specific plugins*; enforcement lives in the Electron main/data processes
+ behind a `guardFs` facade that mirrors `guardStore`/`guardedFetch`.
+ Renderer and plugin code never see absolute paths or file descriptors —
+ only opaque root/file IDs and per-call-validated operations.
+- **Read and write are different products.** Read = indexing granted roots
+ (0127's design, unchanged). Write lands in three progressively-trusted
+ forms: (1) a **managed workbench folder** (`~/xNet/…`) that xNet owns and
+ plugins write into freely within their own subdirectory; (2) **save-as
+ dialogs**, where the OS picker gesture *is* the grant (the macOS powerbox
+ model); (3) in-place edits of user files — deferred, requires per-file
+ approval with diff preview, and only via the agent plan/apply pattern.
+- **Trust tiers map cleanly:** `builtin` modules may implement the scanner
+ itself; `user`-tier (authored/AI-generated) plugins may *request* fs
+ capability and get per-root consent; `marketplace`-tier plugins get **no
+ direct fs access at all** in v1 — they consume indexed nodes through the
+ existing `guardStore` surface, which is already scoped and audited.
+- **Agents get filesystem tools through the existing `:31416` MCP server**
+ (`xnet_fs_search`, `xnet_fs_read`, `xnet_fs_plan_write` →
+ `xnet_fs_apply_write`), reusing the plan→approve→apply pattern that
+ `xnet_plan_page_patch` established. The **lethal-trifecta rule** is the
+ load-bearing safety property: a session that has read local file content
+ is *tainted*, and tainted sessions cannot trigger outbound network
+ actions (webhooks, `defineAction` dispatches, share links) without
+ explicit human approval.
+- **Web is not zero, but it is not this.** Chromium's File System Access
+ API can power a "lite" folder grant (picker + persisted permission), but
+ there are no background watchers, no access without an open tab, and no
+ Safari/Firefox support. Design the capability schema so a Chromium-lite
+ adapter can implement a subset later; build the real thing on Electron.
+- Recommended path: land 0127's Phase 1 (read-only indexing, local-only
+ tables) **plus** the capability plumbing (`ModuleCapabilities.filesystem`,
+ `guardFs`, consent UI) **plus** the managed workbench write target and
+ the four MCP tools. That combination is the smallest thing that delivers
+ both halves of the vision — brain-loading *and* work-doing — behind one
+ security model.
+
+## Current State In The Repository
+
+What exists today, verified against the tree (2026-07-05):
+
+### Electron shell — hardened, and that's the point
+
+- [apps/electron/src/main/index.ts](../../apps/electron/src/main/index.ts)
+ creates the window with `sandbox: true`, `contextIsolation: true`,
+ `nodeIntegration: false`. The renderer is a proper sandbox; **all fs
+ work must live in main/data processes behind typed IPC.** This matches
+ current Electron guidance and is the baseline we must not weaken.
+- [apps/electron/src/preload/index.ts](../../apps/electron/src/preload/index.ts)
+ exposes a narrow `window.xnet` bridge via `contextBridge` (profile, seed,
+ menu events, share payloads, deep links). No fs APIs today — but the
+ one-method-per-message pattern is the correct vector for `xnet:fs:*`.
+- [apps/electron/src/data-process/data-service.ts](../../apps/electron/src/data-process/data-service.ts)
+ is a Node utility process that already owns SQLite
+ (`better-sqlite3` via `@xnetjs/sqlite/electron`), the Yjs pool, and blob
+ sync — the natural home for a scanner/watcher/extractor, off the main
+ process.
+- [apps/electron/src/main/cloudflare-tunnel-manager.ts](../../apps/electron/src/main/cloudflare-tunnel-manager.ts)
+ spawns and supervises `cloudflared` — precedent for subprocess lifecycle.
+ [apps/electron/src/main/agent-bridge-manager.ts](../../apps/electron/src/main/agent-bridge-manager.ts)
+ exists but is not fully wired.
+- The local API already serves the workspace over loopback (`:31415`), and
+ the MCP HTTP transport defaults to `:31416` with pairing token + Origin
+ allowlist ([packages/plugins/src/services/mcp-http.ts](../../packages/plugins/src/services/mcp-http.ts)).
+
+### Trust and capability fabric — the slot the filesystem fits into
+
+- [packages/trust/src/index.ts](../../packages/trust/src/index.ts):
+ `deriveTrustTier`, `requiresCapabilityReprompt`, `sandboxForTier`. Tiers
+ are **provenance-derived, never self-declared**, and re-derived on sync.
+- [packages/plugins/src/feature-module.ts](../../packages/plugins/src/feature-module.ts):
+ `FeatureModule.capabilities` today declares `secrets`, `schemaWrite`,
+ `schemaRead`, `network`, `endowments`. A `filesystem` entry is the
+ obvious sixth member.
+- [packages/hub/src/features/broker.ts](../../packages/hub/src/features/broker.ts):
+ `scopedEnv(moduleId, declaredKeys)` — the enforcement pattern to copy: a
+ module physically cannot read what it didn't declare.
+- `guardedFetch` (network allowlist + SSRF guard via `assertPublicUrl`) and
+ `guardStore` (schema-scoped writes, space-stamped) already wrap every
+ connector sync in
+ [packages/plugins/src/connectors/sync-runner.ts](../../packages/plugins/src/connectors/sync-runner.ts).
+- Widget sandbox tiers are live in production
+ ([packages/dashboard/src/sandbox/](../../packages/dashboard/src/sandbox/)):
+ first-party in host realm, user tier in SES Compartment (console/JSON/Math
+ endowments only), marketplace tier in an opaque-origin iframe.
+
+### Actors that would consume the capability
+
+- Connectors: [packages/plugins/src/connectors/define-connector.ts](../../packages/plugins/src/connectors/define-connector.ts)
+ (GitHub/Slack/Unreal in production) — the template for a `local-files`
+ connector that pulls an indexed root into nodes.
+- Outbound actions: [packages/plugins/src/actions/define-action.ts](../../packages/plugins/src/actions/define-action.ts)
+ — trigger-on-schema-change + declared network/secrets; the template for
+ "materialize this page to a file on change".
+- Agent surface: [packages/plugins/src/services/mcp-server.ts](../../packages/plugins/src/services/mcp-server.ts)
+ already exposes `xnet_search`, `xnet_read_page_markdown`,
+ `xnet_plan_page_patch` → `xnet_apply_page_markdown` (plan/approve/apply),
+ plus [packages/devkit/src/agent.ts](../../packages/devkit/src/agent.ts)
+ which spawns the user's own `claude`/`codex` CLI (0194 — never proxies
+ their subscription token).
+
+### Data model — close but not sufficient
+
+- `FileRef` ([packages/data/src/schema/properties/file.ts](../../packages/data/src/schema/properties/file.ts))
+ is content-addressed (`cid`, `name`, `mimeType`, `size`) and assumes
+ bytes were *captured into* the blob store
+ ([packages/storage/src/blob-store.ts](../../packages/storage/src/blob-store.ts)).
+ There is no path-backed reference type; 0127's
+ IndexedFolder/IndexedFile/FileIdentity/FileSnapshot schemas fill that gap.
+- Import today = social archive importers
+ ([packages/social/src/importers/registry.ts](../../packages/social/src/importers/registry.ts)),
+ drag-drop onto canvas (capture, no indexing), JSON/CSV into databases.
+ No folder import, no watcher, no export-to-folder.
+
+### Guardrails that constrain the design
+
+- Humane charter (0234) and consent spine (0210): consent-first,
+ progressive tiers, failure visible, right to leave. Local paths are
+ PII-grade; 0127 already concluded **absolute paths never sync** by
+ default.
+- Authorization coverage: every new schema must declare authorization
+ (usually `spaceCascadeAuthorization()`) or the coverage test fails; the
+ devtools seed coverage test will also demand seeded nodes for any new
+ registered schema (see `packages/devtools/src/seed/README.md`).
+
+## External Research
+
+### Electron security baseline
+
+The current [Electron security guidance](https://www.electronjs.org/docs/latest/tutorial/security)
+is unambiguous: `sandbox: true` + `contextIsolation: true` +
+`nodeIntegration: false`, one IPC method per message, validate
+`event.senderFrame` on every handler, serve app content from a custom
+protocol rather than `file://`. xNet already complies. The consequence for
+this design: **the renderer must never gain a general fs API** — not even a
+"safe" one — because a compromised renderer is the attack we're budgeting
+for. Everything routes through main/data-process handlers that validate
+per call. See also [Bishop Fox's "Reasonably Secure Electron"](https://bishopfox.com/blog/reasonably-secure-electron)
+on treating the renderer as hostile.
+
+### Tauri's three-tier ACL — the design benchmark
+
+[Tauri v2's security model](https://v2.tauri.app/security/permissions/) is
+the strongest prior art for *shape*: **permissions** (on/off per command) ×
+**scopes** (parameter validators — glob path allowlists/denylists) ×
+**capabilities** (permission+scope sets attached to specific windows). Its
+[fs plugin](https://v2.tauri.app/plugin/file-system/) ships glob-scoped
+path allow/deny lists per capability. We are not migrating to Tauri, but
+`ModuleCapabilities.filesystem` should reproduce this triple: *which
+operations* (read/write/watch) × *which scope* (granted root IDs, not
+globs — coarser and more legible to users) × *which consumer* (module ID,
+enforced by the broker).
+
+### Browser reality — the user's instinct, quantified
+
+The [File System Access API](https://developer.chrome.com/docs/capabilities/web-apis/file-system-access)
+(`showDirectoryPicker` + persistable permissions) works in **Chromium
+only** (Chrome/Edge 86+). [Firefox and Safari ship only OPFS](https://caniuse.com/native-filesystem-api)
+— the private origin filesystem xNet already uses for SQLite. Even on
+Chromium: no background access (a tab must be open), permission prompts
+re-arm aggressively, no watcher API (polling only), and directory handles
+live in IndexedDB per origin. So: a **Chromium-lite adapter** (manual
+"re-sync this folder" against a persisted handle) is feasible later; the
+ambient, watched, agent-accessible filesystem is desktop-only. Mobile
+(0238 Capacitor shell) is a further-degraded tier: document-picker imports
+only.
+
+### macOS sandbox & security-scoped bookmarks
+
+If xNet ever ships via the Mac App Store, folder grants must persist as
+[security-scoped bookmarks](https://developer.apple.com/documentation/security/accessing-files-from-the-macos-app-sandbox)
+(Electron: `app.startAccessingSecurityScopedResource`). The **powerbox
+pattern** — the OS file dialog *is* the permission grant — is worth adopting
+as UX philosophy even outside the MAS sandbox: a user gesture through a
+native picker is the least-ambiguous consent primitive we have. Note
+[CVE-2025-31191](https://www.microsoft.com/en-us/security/blog/2025/05/01/analyzing-cve-2025-31191-a-macos-security-scoped-bookmarks-based-sandbox-escape/):
+bookmark stores themselves are attack surface — treat the grant ledger as
+security-critical state (integrity-protect it; it already must be
+device-local).
+
+### The lethal trifecta — the defining threat
+
+[Simon Willison's "lethal trifecta"](https://simonwillison.net/2025/Jun/16/the-lethal-trifecta/):
+an agent with (1) access to private data, (2) exposure to untrusted
+content, and (3) the ability to communicate externally is structurally
+vulnerable to prompt-injection exfiltration — real incidents exist (GitLab
+Duo, Writer.com). **A filesystem capability hands xNet's agents legs (1)
+and (2) at once**: local files are both maximally private *and* untrusted
+content (a downloaded PDF can carry adversarial instructions). xNet already
+has leg (3): `defineAction` dispatches, webhooks, share links, managed AI
+egress. The mitigation with industry consensus is **taint tracking + policy
+gating**: once a session ingests untrusted/private content, block or
+human-gate every externally-communicating action. This must be a first-class
+invariant of the design, not a docs footnote.
+
+### The vault/sidecar pattern
+
+The Obsidian ecosystem converged on the shape we want for "loading the
+brain": plaintext files stay where they are; a **local sidecar index**
+(SQLite + FTS + local embeddings) provides retrieval; agents reach it
+through [MCP servers over the vault](https://www.rodneydyer.com/your-vault-your-vectors-building-a-local-first-mcp-server-for-obsidian/),
+and Claude Desktop's filesystem extension prompts the user to select which
+folders are reachable. xNet's version is strictly stronger: the index lives
+beside a real node graph, and access is brokered per-plugin rather than
+app-global.
+
+## Key Findings
+
+1. **Every piece of enforcement machinery already exists; only the
+ capability itself is missing.** The broker pattern (`scopedEnv`), the
+ guard pattern (`guardStore`/`guardedFetch`), provenance-derived tiers,
+ consent UX (`requiresCapabilityReprompt`), the sandboxed renderer, the
+ Node-side data process, and the agent plan/apply loop are all shipped.
+ Adding `filesystem` is additive, not architectural.
+2. **The renderer-blindness rule from 0127 generalizes to plugins.** 0127
+ said the renderer sees opaque root/file IDs, never paths. The same must
+ hold for plugin code *wherever it runs* — even hub-side. A plugin gets a
+ `ctx.fs` facade whose every call is validated against its granted roots
+ in the main/data process; it never receives a path string it could
+ smuggle into another channel. Paths are display-time data, resolved by
+ trusted code only.
+3. **Write access is where the products live and where the risk lives.**
+ Reading powers search and context. Writing powers the actual "do work
+ for me" promise: compose a report, maintain an exported folder, build an
+ analysis artifact. The managed workbench folder makes writing *safe by
+ construction* (xNet owns the directory; nothing pre-existing can be
+ destroyed) and covers ~80% of the value. In-place edits to arbitrary
+ user files are the last 20% and can wait indefinitely.
+4. **Marketplace-tier plugins never need raw fs.** Their legitimate use
+ cases (visualize my files, summarize a folder) are served by reading
+ IndexedFile *nodes* through `guardStore` — already scoped, already
+ audited, no new surface. This single decision removes the scariest cell
+ of the threat matrix (untrusted third-party code × user filesystem).
+5. **The trifecta taint gate is cheap to build here** because all three
+ legs already flow through choke points we own: fs reads through
+ `guardFs`, untrusted content through extraction, and egress through
+ `guardedFetch`/`defineAction`/share creation. A session-scoped taint bit
+ checked at the egress choke points is a small patch with an outsized
+ safety payoff.
+6. **Web support is a scoping decision, not an engineering decision.**
+ Define `FsBackend` as an interface; Electron implements all of it,
+ Chromium-web implements picker+enumerate+read (no watch, no background),
+ Safari/Firefox/mobile implement none of it. Features detect the backend
+ capability level exactly the way `detectOpfsCapability` (0238) does.
+
+## Options And Tradeoffs
+
+### Option A — Capture-only (status quo plus)
+
+Drag files in; bytes copy into the blob store; no paths, no index, no
+watcher.
+
+- ✅ Zero new attack surface; works in every browser.
+- ❌ The brain only knows what's manually fed; no "do work in my world"
+ story at all; files go stale the moment they're copied.
+- Verdict: this is today. Insufficient for the vision.
+
+### Option B — App-global folder access (the Claude Desktop / Obsidian model)
+
+User grants folders to *the app*; everything inside the app (plugins,
+agents, search) can read them.
+
+- ✅ Simple mental model; one consent moment; fastest to ship.
+- ❌ Violates xNet's own plugin-trust architecture: a marketplace widget
+ and the built-in search would have identical reach. One over-permissive
+ grant exposes everything to everything. Doesn't compose with tiers.
+- Verdict: right for single-vendor apps, wrong for a platform whose whole
+ pitch is governed extensibility.
+
+### Option C — Per-plugin brokered capability (Tauri-shaped)
+
+Grants attach to the *workspace* (roots ledger), but each consumer
+(plugin/connector/action/agent tool) must declare `filesystem` and be
+mapped to a subset of roots + operations by the user. Enforcement in
+main/data process via `guardFs`.
+
+- ✅ Composes with the existing trust fabric; least-privilege by default;
+ legible consent ("Let *Tax Helper* read *~/Documents/Taxes*"); audit log
+ per module; marketplace tier excludable wholesale.
+- ❌ More plumbing (capability type, consent UI, per-call validation);
+ consent fatigue risk if scopes are too granular (mitigate: root-level
+ grants, not per-file).
+- Verdict: **recommended.** The extra plumbing is one new guard in an
+ established pattern.
+
+### Option D — Agent-only filesystem (everything through the bridge)
+
+No plugin capability at all; only the `:31416` MCP tools touch files, with
+per-action approval.
+
+- ✅ Smallest surface; human-in-the-loop on everything.
+- ❌ No ambient indexing (search over your files needs a first-party
+ scanner anyway), no connector/action automation ("export on change"
+ requires a human click every time). Approval fatigue guarantees users
+ rubber-stamp.
+- Verdict: right as the *agent* slice of Option C, wrong as the whole.
+
+### Where should filesystem code run?
+
+| Host | Role | Rationale |
+| --- | --- | --- |
+| Electron **main** | Dialogs, grant ledger, `shell.openPath`/reveal, IPC validation | Only place with native dialog + shell access; keep it thin |
+| Electron **data process** | Scanner, watcher, hashing, extraction, FTS writes, `guardFs` execution | Already owns SQLite; crashes don't take the UI; malicious-file parsing stays out of main |
+| **Renderer** | UI only; opaque IDs | Sandboxed; must stay blind to paths |
+| **Hub (local API / :31416)** | `xnet_fs_*` tools, connector `ctx.fs` | Reuses pairing-token + Origin allowlist; agents already live here |
+
+## Recommendation
+
+Adopt **Option C** with the agent slice of Option D, in this shape:
+
+```mermaid
+flowchart TD
+ subgraph consent [Grant ledger — device-local, never synced]
+ Picker[Native folder picker
powerbox gesture] --> Ledger[(filesystem_roots
+ per-module grants)]
+ end
+
+ subgraph consumers [Declared consumers]
+ Search[First-party indexer
tier: builtin]
+ Conn[local-files connector
defineConnector]
+ Act[export actions
defineAction]
+ Agent[MCP tools :31416
xnet_fs_*]
+ MP[marketplace plugins]
+ end
+
+ subgraph enforcement [Electron main + data process]
+ GuardFs[guardFs facade
per-call root containment,
denylist, op check, audit log]
+ Scanner[scanner / watcher / extractor
data process]
+ Workbench[managed workbench
~/xNet/Workbench/<module>/]
+ end
+
+ Search --> GuardFs
+ Conn --> GuardFs
+ Act --> GuardFs
+ Agent -->|plan → approve → apply| GuardFs
+ MP -.->|no direct fs —
IndexedFile nodes via guardStore| Nodes[(NodeStore)]
+ GuardFs --> Ledger
+ GuardFs --> Scanner
+ GuardFs --> Workbench
+ Scanner --> Nodes
+ GuardFs --> Taint[session taint bit]
+ Taint -->|tainted ⇒ human gate| Egress[guardedFetch / defineAction /
share creation]
+```
+
+### The five pillars
+
+1. **One grant ledger, workspace-level, device-local.** Roots are granted
+ through the native picker into `filesystem_roots` (local SQLite table
+ per 0127 — absolute paths never become node properties, never sync).
+ On MAS builds the ledger stores security-scoped bookmarks.
+2. **`capabilities.filesystem` on `FeatureModule`,** brokered like
+ secrets: declaring it is necessary but not sufficient; the user maps
+ granted roots (and `read` | `write` | `watch` ops) to the module at
+ install/consent time. `deriveTrustTier` gates eligibility —
+ `marketplace` tier is ineligible in v1.
+3. **`guardFs`,** the third guard beside `guardStore`/`guardedFetch`:
+ resolves opaque IDs to paths, enforces root containment (after
+ `realpath` — symlinks are the classic escape), applies the default
+ denylist (`.env`, key material, browser profiles, `.git` internals),
+ enforces size/time budgets, and appends to a per-module audit log the
+ user can inspect.
+4. **Write targets in order of trust:** managed workbench subdirectory per
+ module (free within it, atomic writes, trash-don't-delete) → save-as
+ picker (gesture = grant) → in-place edits (deferred; plan/apply with
+ diff preview only).
+5. **Trifecta taint gate:** `guardFs.read*` sets a session taint;
+ `guardedFetch`, `defineAction.dispatch`, and share-link creation check
+ it and require explicit approval when tainted. Approval clears per
+ destination, not globally.
+
+### Grant and use, end to end
+
+```mermaid
+sequenceDiagram
+ participant U as User
+ participant R as Renderer (sandboxed)
+ participant M as Main process
+ participant D as Data process
+ participant P as Plugin (tier: user)
+
+ P->>R: manifest declares filesystem: {ops: [read]}
+ R->>U: consent sheet — "Tax Helper wants to read a folder you choose"
+ U->>M: xnet:fs:chooseFolder (native picker)
+ M->>M: ledger.addRoot(path) → rootId; grant(moduleId, rootId, read)
+ M-->>R: rootId (opaque)
+ P->>D: ctx.fs.list(rootId) / ctx.fs.readText(fileId)
+ D->>D: guardFs: module grant? root containment? denylist? budget?
+ D-->>P: entries / bounded text
+ D->>D: audit log += (module, op, fileId, ts)
+ Note over D: taint(session) = true on content read
+```
+
+### Root lifecycle
+
+```mermaid
+stateDiagram-v2
+ [*] --> Granted: user picks folder
+ Granted --> Active: first scan complete
+ Active --> Suspended: OS permission lost /
volume unmounted
+ Suspended --> Active: revalidated / relinked
+ Active --> Revoked: user revokes
+ Suspended --> Revoked: user revokes
+ Revoked --> [*]: watcher stopped,
index rows purged,
module grants dropped
+```
+
+### What this unlocks (the possibility half)
+
+- **Search that knows your life:** FTS over extracted text of granted
+ folders beside pages/tasks/messages; file cards on canvas with live
+ availability (0127 UX, unchanged).
+- **A workspace that produces artifacts:** "summarize these 40 PDFs into a
+ memo" writes `memo.md` into the workbench where Finder, Word, and email
+ can reach it; a database view exports itself to `~/xNet/Workbench/crm/contacts.csv`
+ on every change via `defineAction`.
+- **Agents with context and hands:** the bridge agent (0194) can already
+ edit pages with approval; with `xnet_fs_*` it can *ground* its work in
+ local sources and *deliver* results as files — the home-cooked-software
+ story from blog #10 with real I/O.
+- **Local-first AI grounding:** granted folders + local index + (optional)
+ local models (0252) = ask questions over your documents with zero cloud
+ upload — a differentiator the hosted competitors structurally cannot copy.
+
+## Example Code
+
+### Capability declaration and guard
+
+```typescript
+// packages/plugins/src/feature-module.ts (extension)
+export interface FilesystemCapability {
+ /** Operations the module may perform on user-granted roots. */
+ ops: Array<'read' | 'write' | 'watch'>
+ /** Module always gets its own workbench dir; set false to opt out. */
+ workbench?: boolean
+}
+
+export interface ModuleCapabilities {
+ secrets?: string[]
+ schemaWrite?: SchemaIRI[]
+ schemaRead?: SchemaIRI[]
+ network?: string[]
+ endowments?: string[]
+ filesystem?: FilesystemCapability // NEW — consent maps roots at install
+}
+```
+
+```typescript
+// packages/plugins/src/fs/guard-fs.ts (new, mirrors guardStore/guardedFetch)
+export interface GuardedFs {
+ list(rootId: RootId, opts?: { under?: FileId }): Promise
+ stat(fileId: FileId): Promise
+ readText(fileId: FileId, opts?: { maxBytes?: number }): Promise
+ readBlob(fileId: FileId): Promise // budget-capped
+ workbench(): Promise // module-scoped dir
+ writeWorkbench(rel: string, data: Uint8Array | string): Promise
+ saveAs(suggested: string, data: Uint8Array): Promise // native picker
+ watch(rootId: RootId, cb: (ev: FsEvent) => void): Unsubscribe
+}
+
+export function guardFs(moduleId: string, deps: GuardFsDeps): GuardedFs {
+ const assertGrant = (rootId: RootId, op: FsOp) => {
+ if (!deps.ledger.hasGrant(moduleId, rootId, op)) {
+ throw new FsCapabilityError(moduleId, rootId, op)
+ }
+ }
+ const resolveContained = async (fileId: FileId) => {
+ const { rootId, absPath } = await deps.ledger.resolve(fileId)
+ const real = await fs.realpath(absPath) // symlink escape check
+ if (!real.startsWith(deps.ledger.rootRealPath(rootId) + sep)) {
+ throw new FsContainmentError(fileId)
+ }
+ if (deps.denylist.matches(real)) throw new FsDenylistError(fileId)
+ return { rootId, real }
+ }
+ return {
+ async readText(fileId, opts) {
+ const { rootId, real } = await resolveContained(fileId)
+ assertGrant(rootId, 'read')
+ deps.audit.log(moduleId, 'readText', fileId)
+ deps.taint.mark(deps.session) // trifecta leg 1+2
+ return deps.io.readTextBounded(real, opts?.maxBytes ?? MAX_EXTRACT_BYTES)
+ },
+ // …list/stat/readBlob analogous; write ops route to workbench/saveAs only
+ }
+}
+```
+
+### Egress taint gate
+
+```typescript
+// packages/plugins/src/actions/define-action.ts (dispatch wrapper, sketch)
+async function dispatchWithTrifectaGate(action, event, ctx) {
+ if (ctx.taint.isTainted(ctx.session) && !ctx.approvals.has(action.id, event.destination)) {
+ await ctx.approvals.request({
+ kind: 'tainted-egress',
+ module: action.id,
+ destination: event.destination,
+ summary: 'This session read local files; approve sending data externally.',
+ }) // throws if declined
+ }
+ return action.dispatch(event, ctx)
+}
+```
+
+### Agent tools (MCP, `:31416`)
+
+```typescript
+// packages/plugins/src/services/mcp-server.ts (additions)
+xnet_fs_search // FTS over indexed roots → fileIds + snippets (no paths)
+xnet_fs_read // fileId → bounded text; marks session tainted
+xnet_fs_plan_write // proposed path-in-workbench|saveAs + content → planId + diff
+xnet_fs_apply_write // planId → executes after user approval in panel
+```
+
+## Risks And Open Questions
+
+- **Prompt-injection residue.** The taint gate covers egress choke points
+ we control, but page content synced to collaborators is itself an egress
+ path (agent reads hostile PDF → writes "summary" containing exfiltrated
+ secrets into a shared page). Mitigation candidates: taint-labelled node
+ writes, share-time warnings when tainted-session writes are included.
+ Needs its own design pass before agents get `xnet_fs_read` + shared
+ spaces simultaneously.
+- **Consent fatigue vs. legibility.** Root-level grants keep dialogs rare,
+ but "read everything under ~/Documents" is a big grant. The privacy
+ preview (0127's settings design) and the per-module audit log are the
+ honesty mechanisms — both must ship in v1, not later.
+- **Denylist is heuristic.** Secret scanners miss things; users will grant
+ folders containing credentials. Extraction-time entropy/secret scanning
+ (gitleaks-style patterns) should quarantine hits from FTS and from
+ `readText`, with a visible "3 files excluded" indicator.
+- **Watcher scale and battery.** 0127's budgets (depth/file caps, backoff,
+ pause) are necessary; Chokidar vs native FSEvents/RDCW needs a spike on
+ a 500k-file root. The cold-open lesson (0249: `requestIdleCallback` ≠
+ worker-idle) applies to scan scheduling in the data process.
+- **MAS distribution.** Security-scoped bookmarks change grant-ledger
+ persistence and add entitlements; current non-MAS distribution avoids
+ this, but the ledger API should abstract it now
+ (`ledger.persistGrant` / `ledger.reacquire`).
+- **Multi-device semantics.** IndexedFile nodes syncing (metadata-only) to
+ a device that lacks the root creates "ghost" files. 0127's availability
+ states cover display; grants themselves must remain per-device. Do we
+ sync *that a root exists* (name only) for continuity, or nothing? Leaning
+ nothing until a concrete need appears.
+- **Do marketplace plugins ever get fs?** Possible future: a
+ `filesystem:workbench-only` tier (own sandbox dir, no user roots) — safe
+ enough in principle, but defer until demand exists.
+- **Web-lite adapter demand.** Chromium-only folder grants may be more
+ confusion than value ("why does it work on my laptop Chrome but not
+ Safari?"). Ship desktop first; decide with usage data.
+
+## Implementation Checklist
+
+Phase 1 — grant ledger + read capability + first-party indexing
+(supersedes 0127 Phase 1 by adding the capability plumbing):
+
+- [ ] `filesystem_roots` / `filesystem_entries` / `filesystem_jobs` local
+ SQLite tables in the data process (local-only, never synced)
+- [ ] Grant ledger in main process: add/list/revoke roots, per-module
+ grants, opaque `rootId`/`fileId` minting; `xnet:fs:chooseFolder`,
+ `xnet:fs:listRoots`, `xnet:fs:revokeRoot` IPC with sender validation
+- [ ] Scanner + extractor in data process (ignore rules, bounded text for
+ md/txt/json/csv/pdf-text, BLAKE3 fingerprints, FTS table); no watcher
+ yet — manual rescan
+- [ ] Secret-scanning quarantine at extraction (pattern pass; quarantined
+ files visibly counted, excluded from FTS and `readText`)
+- [ ] `capabilities.filesystem` on `FeatureModule` + trust-tier
+ eligibility check (`marketplace` ineligible) + consent sheet mapping
+ roots→module
+- [ ] `guardFs` facade (containment via realpath, denylist, budgets,
+ audit log) wired into connector/action contexts beside
+ `guardedFetch`/`guardStore`
+- [ ] Settings → Files UI: roots list, per-module grants, audit log
+ viewer, revoke-and-purge
+- [ ] Search integration: indexed-file results + file card (open/reveal
+ via `xnet:fs:openFile(fileId)` validated in main)
+
+Phase 2 — write targets + agent tools:
+
+- [ ] Managed workbench `~/xNet/Workbench//` (atomic writes,
+ trash-don't-delete, reveal action)
+- [ ] `saveAs` powerbox flow (native save dialog = grant)
+- [ ] `defineAction` export template: page→md / database→csv
+ materialization on change
+- [ ] MCP tools `xnet_fs_search` / `xnet_fs_read` /
+ `xnet_fs_plan_write` / `xnet_fs_apply_write` with panel approval UI
+- [ ] Session taint bit + egress gate in `guardedFetch`, action dispatch,
+ and share creation
+- [ ] IndexedFolder/IndexedFile schemas (0127 shapes) for user-promoted
+ files only, with `spaceCascadeAuthorization()` + seed coverage
+ (Tier-1 seeder or exclusion per `seed-manifest.ts`)
+
+Phase 3 — ambient + polish (demand-gated):
+
+- [ ] Watcher (Chokidar spike vs native; budgets, pause, overflow
+ recovery)
+- [ ] Local-files `defineConnector` (cadence-based pull of a root into
+ nodes)
+- [ ] Chromium web-lite `FsBackend` (picker + persisted handle + manual
+ resync); capability detection mirrors `detectOpfsCapability`
+- [ ] Security-scoped-bookmark ledger backend behind `ledger.persistGrant`
+- [ ] In-place edit design pass (diff-preview plan/apply on user files)
+
+## Validation Checklist
+
+- [ ] Renderer cannot obtain an absolute path via any `xnet:fs:*` IPC
+ (fuzz the surface; grep IPC payloads for `/Users/`)
+- [ ] Symlink inside a granted root pointing outside is refused
+ (containment test with `realpath`)
+- [ ] A module without `filesystem` capability calling `ctx.fs.*` throws
+ `FsCapabilityError`; a module granted root A cannot read root B
+- [ ] Marketplace-tier plugin cannot reach `guardFs` at all (tier test)
+- [ ] `.env`/key-material fixtures inside a granted root are quarantined:
+ absent from FTS, `readText` refuses, UI shows exclusion count
+- [ ] Tainted session: `defineAction` dispatch and share creation block
+ pending approval; untainted session unaffected
+- [ ] `xnet_fs_plan_write` → apply without approval is impossible via
+ direct HTTP to `:31416` (pairing token + plan state checked
+ server-side)
+- [ ] Revoke purges `filesystem_entries` rows and FTS entries for the
+ root and drops module grants (verify by row count and search miss)
+- [ ] 100k-file root scans within budget without blocking interactive
+ queries (read-p95 guard from 0266 stays green during scan)
+- [ ] Workbench writes are atomic (kill the data process mid-write; no
+ torn files) and deletions land in OS trash
+- [ ] Authorization + seed coverage tests pass with the new schemas
+- [ ] Web build: all fs features hidden when `FsBackend` reports
+ `none` (Safari/Firefox smoke)
+
+## References
+
+### Repository
+
+- [0127 — xNet Filesystem Integration and Global File Namespace](0127_[_]_XNET_FILESYSTEM_INTEGRATION_AND_GLOBAL_FILE_NAMESPACE.md)
+ (ingestion design this doc builds on)
+- 0189 feature modules / capability broker · 0192 plugin trust substrate ·
+ 0194 agent bridge · 0196 connectors · 0213 actions · 0210 consent ·
+ 0234 humane charter (see `docs/explorations/`)
+- Key seams: [apps/electron/src/main/index.ts](../../apps/electron/src/main/index.ts),
+ [apps/electron/src/data-process/data-service.ts](../../apps/electron/src/data-process/data-service.ts),
+ [packages/plugins/src/feature-module.ts](../../packages/plugins/src/feature-module.ts),
+ [packages/hub/src/features/broker.ts](../../packages/hub/src/features/broker.ts),
+ [packages/plugins/src/connectors/define-connector.ts](../../packages/plugins/src/connectors/define-connector.ts),
+ [packages/plugins/src/actions/define-action.ts](../../packages/plugins/src/actions/define-action.ts),
+ [packages/plugins/src/services/mcp-server.ts](../../packages/plugins/src/services/mcp-server.ts),
+ [packages/trust/src/index.ts](../../packages/trust/src/index.ts),
+ [packages/storage/src/blob-store.ts](../../packages/storage/src/blob-store.ts)
+
+### External
+
+- [Electron security tutorial](https://www.electronjs.org/docs/latest/tutorial/security) ·
+ [Context isolation](https://www.electronjs.org/docs/latest/tutorial/context-isolation) ·
+ [Process sandboxing](https://www.electronjs.org/docs/latest/tutorial/sandbox) ·
+ [Bishop Fox — Reasonably Secure Electron](https://bishopfox.com/blog/reasonably-secure-electron)
+- [Tauri v2 permissions](https://v2.tauri.app/security/permissions/) ·
+ [Tauri capabilities](https://v2.tauri.app/security/capabilities/) ·
+ [Tauri fs plugin scopes](https://v2.tauri.app/plugin/file-system/)
+- [File System Access API (Chrome)](https://developer.chrome.com/docs/capabilities/web-apis/file-system-access) ·
+ [MDN File System API](https://developer.mozilla.org/en-US/docs/Web/API/File_System_API) ·
+ [caniuse — native filesystem](https://caniuse.com/native-filesystem-api) ·
+ [Cloud Four — the many confusing file system APIs](https://cloudfour.com/thinks/the-many-confusing-file-system-apis/)
+- [Apple — accessing files from the macOS App Sandbox](https://developer.apple.com/documentation/security/accessing-files-from-the-macos-app-sandbox) ·
+ [Microsoft — CVE-2025-31191 security-scoped bookmarks sandbox escape](https://www.microsoft.com/en-us/security/blog/2025/05/01/analyzing-cve-2025-31191-a-macos-security-scoped-bookmarks-based-sandbox-escape/)
+- [Simon Willison — the lethal trifecta](https://simonwillison.net/2025/Jun/16/the-lethal-trifecta/) ·
+ [HiddenLayer — defending against the trifecta](https://www.hiddenlayer.com/research/the-lethal-trifecta-and-how-to-defend-against-it)
+- [Local-first MCP server over a PKM vault](https://www.rodneydyer.com/your-vault-your-vectors-building-a-local-first-mcp-server-for-obsidian/)