Files
skills/docs/api-stack/api/api-sandbox-vm/src/proxies/fs-proxy/fs-proxy.md
2026-05-29 08:20:56 +01:00

3.4 KiB

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.freezed and the adapter reference is not exposed.

Usage: Basic

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:

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.