Docs/guidesv1.0.0-rc.1

WorkspaceFS Sandbox Abstraction

Package: @tagna/udiot/sandbox Added: v0.16.0

WorkspaceFS is the common interface for code generators, local CLI workflows, and remote sandbox workers. It separates "what the generator wants to do" from "where the files and commands actually run."

Why It Exists

Before v0.16, generators wrote directly to the local filesystem. That works for the CLI, but hosted products need to redirect file writes and commands into an isolated workspace. WorkspaceFS gives both paths the same contract:

  • Read and write files
  • List files with globs
  • Delete files safely
  • Apply diffs
  • Run bounded commands
  • Start and stop preview servers
  • Snapshot the workspace for deploy artifacts

Local Workspace

import { LocalWorkspaceFS } from '@tagna/udiot/sandbox';

const ws = new LocalWorkspaceFS({
  rootDir: process.cwd(),
});

await ws.writeFile('src/generated.ts', 'export const ok = true;\n');

const result = await ws.runCommand({
  command: 'npm',
  args: ['test', '--', '--runInBand'],
  timeoutMs: 120000,
});

console.log(result.exitCode, result.stdout);
await ws.dispose();

LocalWorkspaceFS rejects path traversal attempts, creates intermediate directories on writes, and disables shell execution unless explicitly allowed.

Remote Workspace

Use RemoteWorkspaceFS when a sandbox worker owns the actual filesystem and processes. The remote implementation sends JSON-over-HTTP RPC requests to your worker.

import { RemoteWorkspaceFS } from '@tagna/udiot/sandbox';

const ws = new RemoteWorkspaceFS({
  baseUrl: 'https://sandbox.example.com',
  authToken: process.env.SANDBOX_TOKEN,
});

The remote worker decides how to isolate the workspace. The framework contract only requires that each operation returns the expected shape. The client refuses to send authToken over plain http:// to a non-local host unless you set allowInsecureHttp: true. Calls that carry their own deadline (runCommand's timeoutMs, startServer's readyTimeoutMs) wait that long plus a margin instead of the default timeoutMs. See the sandbox RPC protocol for the wire contract.

Factory

Use createWorkspaceFS() when product code chooses local or remote execution from configuration.

import { createWorkspaceFS } from '@tagna/udiot/sandbox';

const ws = process.env.SANDBOX_URL
  ? createWorkspaceFS({
      kind: 'remote',
      baseUrl: process.env.SANDBOX_URL,
      authToken: process.env.SANDBOX_TOKEN,
    })
  : createWorkspaceFS({
      kind: 'local',
      rootDir: process.cwd(),
    });

Command Safety

runCommand() takes an argv-style command by default:

await ws.runCommand({
  command: 'node',
  args: ['scripts/check.js'],
  timeoutMs: 30000,
});

Shell evaluation is opt-in:

const ws = new LocalWorkspaceFS({
  rootDir: process.cwd(),
  allowShell: true,
});

Keep allowShell disabled for user-supplied inputs. Prefer command plus args so implementations can avoid shell interpolation.

Commands and preview servers do not inherit the host environment. A child sees a small allow-list of OS variables (PATH, HOME, temp directories, locale and their Windows equivalents) plus the env you pass, so model-driven commands cannot read API keys or database credentials from the host process.

const ws = new LocalWorkspaceFS({
  rootDir: process.cwd(),
  envAllowList: ['NODE_OPTIONS'],
});

await ws.runCommand({
  command: 'npm',
  args: ['test'],
  env: { DATABASE_URL: 'postgres://localhost/test' },
});

Set inheritEnv: true only when every command is trusted. A command that cannot start, such as a missing executable, rejects with an error whose code is the spawn error code (for example ENOENT).

Each result keeps at most maxOutputBytes (default 1 MiB) of stdout and of stderr and sets truncated when output was cut; onChunk still receives every chunk. A timeout or abort sends SIGTERM to the whole process tree and SIGKILL after killGraceMs (default 2000 ms). On Windows the tree is ended with taskkill /T /F. server.stop() and dispose() wait for processes to exit.

applyDiff() accepts unified and git-style diffs, including created, deleted and renamed files. Every file is checked and patched in memory before anything is written, so a rejected hunk leaves the workspace unchanged.

Preview Servers

const server = await ws.startServer({
  command: 'npm',
  args: ['run', 'dev'],
  port: 3000,
  readyPattern: /ready/i,
});

console.log(await ws.getPreviewUrl(server));
await server.stop();

Local workspaces return localhost URLs. Remote sandboxes can return tunneled or proxy URLs from the sandbox provider.

Generator Integration

Generator code should accept a WorkspaceFS instead of importing node:fs/promises directly:

import type { WorkspaceFS } from '@tagna/udiot/sandbox';

export async function writeFeature(ws: WorkspaceFS, name: string) {
  await ws.writeFile(`src/features/${name}.ts`, 'export {}\n');
}

This keeps the same generator usable from the local CLI, CI jobs, and hosted sandbox execution.

From the canonical UDIOT public documentation.Try it in the playground ↗