first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,100 @@
# fs proxy
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## 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.