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.
| Field | Type | Default | Description |
|---|---|---|---|
cwd | string | process.cwd() | Workspace root for path resolution and jail boundary |
jail | boolean | false | When true, paths that resolve outside cwd are rejected |
allowAbsolutePaths | boolean | true | When false, absolute path arguments are rejected |
forbiddenGlobs | readonly string[] | [] | Always-deny glob patterns (.git/**, .env*, keys, certs). Cannot be overridden by allow patterns. |
read | PathPolicy | { default: "allow" } | Read-access policy: deny patterns → allow patterns → default |
write | PathPolicy | { 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-approvedNon-throwing inspection for filtering:
const ok = ctx.guard.canAccess({ type: "fs.read", resource: "src/config.ts" });
if (!ok) continue; // skip blocked entries during traversalPolicy
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 patternsreadPathPolicy(config, cwd)— deny > allow > default forfs.readwritePathPolicy(config, cwd)— deny > allow > default forfs.writedenyCommandsPolicy(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/appWith 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