Comma Agents
@comma-agents/core

Sandbox

Declare a working directory and access policies for strategy execution.

Sandbox

A sandbox declares the working directory, jail boundary, and read/write policies for a strategy's execution environment. Use inSandbox() to apply enforcement — it creates per-tool guards internally and wires them into every agent. Tools call ctx.guard.authorize() before file I/O to resolve paths and check policies.

import { inSandbox, loadStrategyFromString } from "@comma-agents/core";

const strategy = await loadStrategyFromString(content, "yaml");
inSandbox(strategy, {
  cwd: "/projects/my-app",
  jail: true,
  write: { default: "ask" },
}, {
  onAsk: async (request) => {
    const answer = prompt(`Allow ${request.operation} on ${request.resource}?`);
    return answer === "y" ? "allow" : "deny";
  },
});

When no sandbox is configured, all tools run with a permissive sandbox (no restrictions, cwd: process.cwd()).

Pre-built Configs

Three configs cover common scenarios. Pass them to inSandbox() or createSandbox().

import {
  inSandbox,
  DEFAULT_SANDBOX_CONFIG,
  PERMISSIVE_SANDBOX_CONFIG,
  DEFAULT_DAEMON_SANDBOX_CONFIG,
} from "@comma-agents/core";

DEFAULT_SANDBOX_CONFIG — jailed to process.cwd(), absolute paths rejected, standard forbidden globs (.git/**, .env*, keys, certs), reads and writes allowed within the boundary. .comma/** and . are explicitly allowlisted for reading so skills and configuration are always accessible. Recommended for most projects.

PERMISSIVE_SANDBOX_CONFIG — no jail, absolute paths allowed, no forbidden globs, all reads and writes allowed. Used as the fallback when no sandbox is configured.

DEFAULT_DAEMON_SANDBOX_CONFIG — jailed to cwd, absolute paths allowed (daemon passes absolute paths), in-cwd paths auto-allowed via allow: ["**"], anything outside cwd triggers "ask" via the permission bridge. .comma/** and . are explicitly allowlisted for reading so skills are always accessible without prompting. Leaves cwd unset so the daemon supplies it at runtime.

inSandbox()

Apply sandbox enforcement to a strategy or agent. Creates the sandbox internally and mutates the entity in-place so all agents receive per-tool guards.

import { inSandbox } from "@comma-agents/core";

// Apply to a strategy (mutates all agents)
inSandbox(strategy, { cwd: "/workspace", jail: true });

// Apply to a single agent
inSandbox(agent, { write: { default: "ask" } });

// With callbacks for interactive "ask" prompts
inSandbox(strategy, config, {
  onAsk: async (request) => { /* prompt user */ return "allow"; },
  onPolicyChange: (snapshot) => { /* broadcast policy changes */ },
});

Returns the same entity. Use getSandbox(entity) to retrieve the Sandbox instance.

getSandbox()

Retrieve the Sandbox from a previously sandboxed entity. Returns undefined if inSandbox() was never called.

import { getSandbox } from "@comma-agents/core";

const sandbox = getSandbox(strategy);
if (sandbox) {
  // Access a tool's guard for policy management
  const guard = sandbox.guardFor("read_file");
  guard.addPolicy(myCustomPolicy);
}

createSandbox()

Low-level factory — creates a standalone Sandbox instance without attaching it to agents. Prefer inSandbox() for normal usage.

import { createSandbox } from "@comma-agents/core";

const sandbox = createSandbox({
  cwd: "/workspace",
  jail: true,
});

SandboxConfig

Configuration passed to inSandbox() or createSandbox(). All fields default to permissive values when omitted.

FieldTypeDefaultDescription
cwdstringprocess.cwd()Workspace root for path resolution and jail boundary
jailbooleanfalseWhen true, paths that resolve outside cwd are rejected
allowAbsolutePathsbooleantrueWhen false, absolute path arguments are rejected
forbiddenGlobsreadonly string[][]Always-deny glob patterns (.git/**, .env*, keys, certs). Cannot be overridden by allow patterns.
readPathPolicy{ default: "allow" }Read-access policy: deny patterns → allow patterns → default
writePathPolicy{ default: "allow" }Write-access policy: deny patterns → allow patterns → default

PathPolicy

Policy applied to a class of file-system operations. Patterns use Bun.Glob syntax relative to cwd. Evaluation order: deny patterns → allow patterns → default.

{
  default: "ask",                    // "allow" | "deny" | "ask"
  allow: ["out/**", "tmp/**"],       // globs that are explicitly allowed
  deny: ["secrets/**", "*.pem"],     // deny wins over allow
}

Guard

Every tool in a sandboxed strategy gets its own Guard. The guard resolves paths against cwd, enforces the jail boundary, evaluates the policy chain, and dispatches "ask" prompts via the onAsk callback. Session-memory decisions ("allow-session", "deny-session") are scoped per-tool.

Prop

Type

Tools call ctx.guard.authorize() before file I/O:

const abs = await ctx.guard.authorize(
  { type: "fs.read", resource: "src/index.ts" },
  { agentName: ctx.agentName, toolName: "read_file", signal: ctx.abort },
);
// abs is the resolved absolute path — jail-checked, policy-approved

Non-throwing inspection for filtering:

const ok = ctx.guard.canAccess({ type: "fs.read", resource: "src/config.ts" });
if (!ok) continue;  // skip blocked entries during traversal

Policy

Each guard maintains an ordered chain of Policy objects. When a tool requests access, the guard evaluates the chain — the first policy that returns non-"pass" decides the outcome. Built-in policies ship by default; tool-level and runtime policies extend the chain.

Prop

Type

Built-in policy factories:

  • forbiddenGlobsPolicy(globs, cwd) — always-deny for secret/sensitive patterns
  • readPathPolicy(config, cwd) — deny > allow > default for fs.read
  • writePathPolicy(config, cwd) — deny > allow > default for fs.write
  • denyCommandsPolicy(patterns) — always-deny for matching commands (regex)
  • approveCommandsPolicy(patterns) — return "ask" for matching commands

AccessRequest

Tools declare what access they need by constructing AccessRequest objects.

Prop

Type

// File read
{ type: "fs.read", resource: "src/config.ts" }

// File write
{ type: "fs.write", resource: "out/report.md" }

// Command execution (used by run_command)
{ type: "command.execute", resource: "git push --force" }

GuardCallbacks

Callbacks injected into every guard at creation time. Passed to inSandbox().

Prop

Type

inSandbox(strategy, config, {
  onAsk: async (request) => {
    // request.agentName, request.toolName, request.operation, request.resource
    const decision = await showPermissionPrompt(request);
    return decision; // "allow" | "deny" | "allow-session" | "deny-session"
  },

  onPolicyChange: (snapshot) => {
    // snapshot.toolName, snapshot.policies
    console.log(`${snapshot.toolName} policy chain changed`);
  },
});

Tool-Level Policies

Tools define additional policies that get added to their guard's chain. Use this for tool-specific rules like command deny/approval patterns.

import { approveCommandsPolicy, denyCommandsPolicy, defineTool } from "@comma-agents/core";

const myTool = defineTool({
  description: "...",
  parameters: z.object({ /* ... */ }),
  policies: [
    denyCommandsPolicy(["rm -rf /", "sudo shutdown"]),
    approveCommandsPolicy(["git push --force", "npm publish"]),
  ],
  execute: async (args, ctx) => {
    // ctx.guard handles deny/approval automatically
    await ctx.guard.authorize(
      { type: "command.execute", resource: args.command },
      { agentName: ctx.agentName, toolName: "my_tool", signal: ctx.abort },
    );
  },
});

Writing a Tool That Uses the Guard

Tools receive their guard via ToolContext.guard. Call guard.authorize() before any file I/O. Throw SandboxViolationError on deny — the centralized catch in the agent framework converts it to a structured ToolError for the LLM.

import { defineTool } from "@comma-agents/core";
import { z } from "zod";

export const readFileTool = defineTool({
  description: "Read a file from disk.",
  parameters: z.object({ path: z.string() }),
  execute: async (args, ctx) => {
    const abs = await ctx.guard.authorize(
      { type: "fs.read", resource: args.path },
      { agentName: ctx.agentName, toolName: "read_file", signal: ctx.abort },
    );
    return okResult(await Bun.file(abs).text());
  },
});

Use guard.canAccess() for non-throwing filtering during traversal:

// Inside search_files / list_directory — skip blocked entries silently
if (!ctx.guard.canAccess({ type: "fs.read", resource: relativePath })) continue;

Jail Mode

When jail: true, paths that resolve outside cwd (including via symlinks) throw SandboxViolationError with reason: "jail". The jail check runs inside the guard before the policy chain.

import { inSandbox } from "@comma-agents/core";

inSandbox(strategy, { cwd: "/projects/app", jail: true });
// Tools cannot read or write anything outside /projects/app

With jail: false, paths resolve freely and policies still apply normally.

Absolute Paths

Set allowAbsolutePaths: false to force tools to use cwd-relative paths. Absolute path arguments throw SandboxViolationError with reason: "absolute-path". When allowAbsolutePaths: true (the default for backward compatibility), tools can pass absolute paths directly.

inSandbox(strategy, {
  cwd: "/projects/app",
  jail: true,
  allowAbsolutePaths: false,   // reject absolute path arguments
});

Forbidden Globs

forbiddenGlobs is an always-deny layer — paths matching any pattern return permission_denied regardless of read/write policy or user approval. Use this for secrets, build caches, or anything that must never be touched.

inSandbox(strategy, {
  cwd: "/projects/app",
  jail: true,
  forbiddenGlobs: ["**/.env", "**/.env.*", "**/node_modules/**"],
});

Forbidden globs apply to both reads and writes and cannot be overridden by allow patterns or session decisions.

SandboxViolationError

Thrown when a path is denied, escapes the jail, or matches a forbidden glob.

import { SandboxViolationError } from "@comma-agents/core";

try {
  await guard.authorize(
    { type: "fs.read", resource: "../../etc/passwd" },
    { agentName: "reader", toolName: "read_file", signal: new AbortController().signal },
  );
} catch (err) {
  if (err instanceof SandboxViolationError) {
    console.log(err.path);   // absolute path that was denied
    console.log(err.reason); // "jail" | "read-denied" | "write-denied" | "forbidden-glob" | "absolute-path" | "ask-no-handler" | "ask-aborted"
  }
}

PermissionDecision

Returned by the onAsk callback. One-shot decisions don't persist; session decisions are remembered in the guard's chain for the lifetime of the run.

Prop

Type

PermissionRequest

The data dispatched to the onAsk callback when a policy returns "ask".

Prop

Type

On this page