# fs proxy _(no submodules)_ ## Purpose `installFsProxy` returns the sandbox-side filesystem surface that plugins consume. Every method gates the call through the supplied `PermissionChecker` before delegating to the opaque host adapter, so a denied call never reaches the host's `fs/promises` layer. The proxy is closure-bound: the adapter reference is reachable only through the returned methods, so the sandbox cannot retrieve the adapter from the proxy receiver and bypass the permission gate. This module owns the sandbox-side half of the `fs` capability surface; the adapter on the other side of the closure is supplied by `api-host` and performs the real `node:fs/promises` calls after the policy decision has been confirmed. ## Features - Permission-checked `readFile` and `writeFile` with policy decisions computed synchronously and the host call deferred until allowance is confirmed. - Denials raise `SAND_VM.PERMISSION_DENIED` immediately (`Promise.reject`) and increment the `plugin.permission_denied_total` counter with `{ plugin, capability: "fs", mode }` attributes — identical labels to the metric emitted by `api-sandbox-permissions` so the host's dashboard aggregates across implementations. - Frozen receiver: the returned object is `Object.freeze`d and the adapter reference is not exposed. ## Usage: Basic ```ts import { installFsProxy } from "@suaveplan/api-sandbox-vm/proxies/fs-proxy/fs-proxy"; import { createPermissionChecker } from "@suaveplan/api-sandbox-vm/permissions/checker"; const checker = createPermissionChecker({ policy: { fs: ["/srv/data/**"] }, }); const fs = installFsProxy({ checker, pluginId: "demo", adapter: { readFile: (p) => realReadFile(p), writeFile: (p, d) => realWriteFile(p, d), }, }); await fs.readFile("/srv/data/users.json"); ``` ## Usage: Advanced Wrap the adapter to record host-side latency without polluting the proxy contract: ```ts import { installFsProxy } from "@suaveplan/api-sandbox-vm/proxies/fs-proxy/fs-proxy"; import { hrtime } from "node:process"; const adapter = { readFile: async (p: string) => { const start = hrtime.bigint(); try { return await realReadFile(p); } finally { const elapsed = Number(hrtime.bigint() - start) / 1e6; telemetry.recordFsLatency("read", elapsed); } }, writeFile: (p, d) => realWriteFile(p, d), }; const fs = installFsProxy({ checker, adapter, pluginId: "demo" }); ``` ## API Reference - `installFsProxy(options): FsProxy` — build the proxy. `options.checker`, `options.adapter`, `options.pluginId` are all required. - `interface FsProxy` — frozen receiver with `readFile`, `writeFile`. - `interface FsHostAdapter` — the host-supplied counterpart that actually performs the IO. ## Implementation Notes The proxy is the only place where sandbox-side `fs` calls cross the permission boundary. The adapter MUST never be exposed back to the sandbox; the closure-binding guarantees that property at the language level. Denials are accounted for with the same metric name as the sister `api-sandbox-permissions` package emits, so dashboards see one denial-rate series rather than two. The `mode` attribute on the counter is `"read"` or `"write"` — keeping it on the metric (rather than in a separate counter) means cardinality remains bounded to two values regardless of the path being requested.