Secure Sandbox

Documentation for @agentsh/secure-sandboxagentsh-secure-sandbox — drop-in runtime security for hosted AI agent sandboxes. Wraps Vercel Sandbox, E2B Sandbox, Daytona Sandbox, Cloudflare Containers, Blaxel Sandbox, Sprites (Fly.io), Modal (gVisor), Runloop Devboxes, exe.dev VMs, and Freestyle VMs with kernel-level policy enforcement.

Quick Start#

Install the package:

$ npm install @agentsh/secure-sandbox
$ pip install agentsh-secure-sandbox

Wrap any supported sandbox in three lines:

import { Sandbox } from '@vercel/sandbox';
import { secureSandbox, adapters } from '@agentsh/secure-sandbox';

const raw     = await Sandbox.create({ runtime: 'node24' });
const sandbox = await secureSandbox(adapters.vercel(raw));

await sandbox.exec('npm install express');  // allowed
await sandbox.exec('cat ~/.ssh/id_rsa');   // blocked by policy
from vercel_sandbox import Sandbox
from agentsh_secure_sandbox import secure_sandbox
from agentsh_secure_sandbox.adapters import vercel

raw     = await Sandbox.create(runtime="node24")
sandbox = await secure_sandbox(vercel(raw))

await sandbox.exec("npm install express")  # allowed
await sandbox.exec("cat ~/.ssh/id_rsa")   # blocked by policy

secureSandbox() is built on agentsh (agent shell), the open-source execution-layer security engine. It installs the agentsh binary into the sandbox, replaces /bin/bash with a policy-enforcing shell shim, and routes every operation through multiple kernel-level enforcement layers.

Unlike prompt-level guardrails that depend on the AI model complying with instructions, agentsh operates below the model layer — intercepting actual system calls (file access, network connections, process execution) and evaluating them against deterministic policy rules. No matter what prompt injection is attempted, policy enforcement is applied to the real operations the agent tries to perform. Learn more about this approach in the execution-layer security overview.

The enforcement layers:

Landing page

For a quick overview and interactive examples, see the secure-sandbox landing page.

Vercel AI SDKPydantic AI#

@agentsh/secure-sandbox pairs naturally with the Vercel AI SDK. Use createSandbox from the AI SDK to spin up a sandbox, then wrap it with secureSandbox so every tool call the model makes — file writes, shell commands, network requests — is policy-enforced at the kernel level.

import { generateText } from 'ai';
import { openai } from '@ai-sdk/openai';
import { Sandbox } from '@vercel/sandbox';
import { secureSandbox, adapters } from '@agentsh/secure-sandbox';
import { agentDefault } from '@agentsh/secure-sandbox/policies';

// 1. Create the sandbox and secure it
const raw = await Sandbox.create();
const sandbox = await secureSandbox(adapters.vercel(raw), {
  policy: agentDefault({
    network: [
      { allow: ['api.openai.com'], ports: [443] },
    ],
  }),
});

// 2. Give the model tools that run inside the secured sandbox
const result = await generateText({
  model: openai('gpt-5.4'),
  prompt: 'Set up a Node.js project with Express and create a hello-world server.',
  tools: {
    exec: {
      description: 'Run a shell command in the sandbox',
      parameters: { command: { type: 'string' } },
      async execute({ command }) {
        const { stdout, stderr } = await sandbox.exec(command);
        return { stdout, stderr };
      },
    },
    writeFile: {
      description: 'Write a file in the sandbox',
      parameters: { path: { type: 'string' }, content: { type: 'string' } },
      async execute({ path, content }) {
        return await sandbox.writeFile(path, content);
      },
    },
  },
});

// The model can npm install, write files, run code — but can't
// read ~/.ssh, curl evil domains, or escalate privileges.

await sandbox.stop();

Every tool call the model makes goes through the agentsh policy engine. If a prompt injection tricks the model into running cat ~/.ssh/id_rsa or curl https://evil.com, it gets blocked at the kernel level — regardless of what the model intended.

agentsh-secure-sandbox integrates with Pydantic AI via the SecureSandboxToolset. It provides three tools — run_command, write_file, and read_file — that run inside the secured sandbox with full policy enforcement.

from pydantic_ai import Agent
from daytona_sdk import Daytona
from agentsh_secure_sandbox import SecureSandboxToolset
from agentsh_secure_sandbox.adapters import daytona
from agentsh_secure_sandbox.policies import agent_default

# 1. Create the sandbox and secure it
raw = await Daytona().create()
toolset = SecureSandboxToolset(
    daytona(raw),
    policy=agent_default(
        network=[{"allow": ["api.openai.com"], "ports": [443]}],
    ),
)

# 2. Give the model tools that run inside the secured sandbox
agent = Agent("claude-sonnet-4-6", toolsets=[toolset])
result = await agent.run(
    "Set up a Python project with FastAPI and create a hello-world server."
)

# The model can pip install, write files, run code — but can't
# read ~/.ssh, curl evil domains, or escalate privileges.

await toolset.cleanup()

Every tool call the model makes goes through the agentsh policy engine. If a prompt injection tricks the model into running cat ~/.ssh/id_rsa or curl https://evil.com, it gets blocked at the kernel level — regardless of what the model intended.

Supported Platforms#

Built-in adapters for the major hosted AI sandbox providers. Each adapter maps the platform’s SDK to the SandboxAdapter interface with zero configuration.

Platform Matrix#

Provider seccomp Landlock FUSE Network Proxy DLP Security Mode
Vercel Sandbox Yes Yes No * Yes Yes landlock
E2B Sandbox Yes Yes Yes Yes Yes full
Daytona Sandbox Yes Yes Yes Yes Yes full
Cloudflare Containers Yes Yes No * Yes Yes landlock
Blaxel Sandbox Yes Yes Yes Yes Yes full
Sprites (Fly.io) Yes Yes Yes Yes Yes full
Modal (gVisor) No No No Yes Yes ptrace
Runloop Yes Yes Yes * Yes Yes full
exe.dev Yes Yes Yes * Yes Yes full (hybrid)
Freestyle Yes No ** Yes * Yes Yes minimal
* FUSE availability

Some platforms (Vercel, Cloudflare) do not expose /dev/fuse, so the FUSE virtual filesystem layer cannot be mounted. Without FUSE, soft-delete quarantine and real-paths virtualization are not available. Other platforms (Runloop, exe.dev, Freestyle) support FUSE via deferred activation — the binary configures FUSE at runtime after enabling /dev/fuse access. All other enforcement layers — seccomp, Landlock, Network Proxy, and DLP — operate normally.

** Landlock on Freestyle

The Freestyle kernel (6.1.0-6-freestyle) is currently compiled without CONFIG_SECURITY_LANDLOCK=y, so Landlock-based file enforcement is not available. freestyleDefaults() sets allowDegraded: true and the runtime settles into minimal security mode (seccomp wrapper + network proxy + FUSE workspace + cgroups). Command blocking, FUSE-protected workspaces, network policy, and DLP redaction all enforce normally.

Adapters#

Each adapter wraps a provider’s SDK object. Import from adapters:

import { secureSandbox, adapters } from '@agentsh/secure-sandbox';

// Vercel
const sb = await secureSandbox(adapters.vercel(raw));

// E2B
const sb = await secureSandbox(adapters.e2b(raw));

// Daytona
const sb = await secureSandbox(adapters.daytona(raw));

// Cloudflare Containers
const sb = await secureSandbox(adapters.cloudflare(raw));

// Blaxel
const sb = await secureSandbox(adapters.blaxel(raw));

// Sprites (Fly.io Firecracker microVMs)
const sb = await secureSandbox(adapters.sprites(raw));

// Modal (gVisor — uses ptrace enforcement)
import { modal, modalDefaults } from '@agentsh/secure-sandbox/adapters/modal';
const sb = await secureSandbox(modal(raw), { ...modalDefaults() });

// Runloop (seccomp + FUSE + Landlock + cgroups)
const sb = await secureSandbox(adapters.runloop({ client, id: devbox.id }), adapters.runloopDefaults());

// exe.dev (hybrid ptrace+seccomp, persistent VMs via SSH)
const sb = await secureSandbox(adapters.exe('my-vm'), adapters.exeDefaults());

// Freestyle (Firecracker VMs — minimal mode, no Landlock)
import { freestyle, freestyleDefaults } from '@agentsh/secure-sandbox/adapters/freestyle';
const sb = await secureSandbox(freestyle(vm), freestyleDefaults());
from agentsh_secure_sandbox import secure_sandbox
from agentsh_secure_sandbox.adapters import vercel, e2b, daytona, cloudflare, blaxel, sprites
from agentsh_secure_sandbox.adapters.modal import modal, modal_defaults
from agentsh_secure_sandbox.adapters import runloop, runloop_defaults
from agentsh_secure_sandbox.adapters import exe, exe_defaults
from agentsh_secure_sandbox.adapters.freestyle import FreestyleClient, freestyle, freestyle_defaults

# Vercel
sb = await secure_sandbox(vercel(raw))

# E2B
sb = await secure_sandbox(e2b(raw))

# Daytona
sb = await secure_sandbox(daytona(raw))

# Cloudflare Containers
sb = await secure_sandbox(cloudflare(raw))

# Blaxel
sb = await secure_sandbox(blaxel(raw))

# Sprites (Fly.io)
sb = await secure_sandbox(sprites(raw))

# Modal (gVisor) — uses ptrace enforcement
sb = await secure_sandbox(modal(raw), **modal_defaults())

# Runloop
sb = await secure_sandbox(runloop(client=client, id=devbox.id), **runloop_defaults())

# exe.dev — hybrid ptrace+seccomp
sb = await secure_sandbox(exe("my-vm"), **exe_defaults())

# Freestyle — minimal mode (no Landlock)
async with FreestyleClient(api_key=FREESTYLE_API_KEY) as client:
    vm = await client.create_sandbox_vm()
    sb = await secure_sandbox(freestyle(vm), **freestyle_defaults())

You can also implement a custom adapter by providing an object with exec, writeFile, and readFile methods. See API Reference below.

Policy System#

Policies define what operations are allowed, denied, redirected, or audited inside a secured sandbox. Policies are TypeScript objects validated with Zod and serialized to YAML for the agentsh engine.

Presets#

Four built-in presets cover common use cases:

Preset Use Case Network Filesystem Commands
agentDefault Production AI agents Allowlisted registries (npm, PyPI, Cargo, Go, GitHub) on port 443 Workspace read/write/create, deny .ssh, .aws, .env, shell configs Safe utilities + dev tools, deny sudo/env/shutdown
devSafe Local development Permissive (no deny-all) Workspace-focused, fewer restrictions Fewer command restrictions
ciStrict CI/CD runners Strict registries only Deny all files outside workspace Restricted command set
agentSandbox Untrusted code No network access Read-only workspace Heavily restricted

Usage:

import { agentDefault, devSafe, ciStrict, agentSandbox }
  from '@agentsh/secure-sandbox/policies';

// Use a preset directly
const sandbox = await secureSandbox(adapter, { policy: ciStrict() });
from agentsh_secure_sandbox.policies import agent_default, dev_safe, ci_strict, agent_sandbox

# Use a preset directly
sandbox = await secure_sandbox(adapter, policy=ci_strict())

Extending Policies#

Every preset accepts an override object to add rules. Override rules are appended — the preset’s base rules take priority (first-match-wins).

import { agentDefault } from '@agentsh/secure-sandbox/policies';

const policy = agentDefault({
  network: [
    { allow: ['api.stripe.com', 'api.openai.com'], ports: [443] }
  ],
  file: [
    { allow: '/data/**', ops: ['read', 'write'] }
  ],
  command: [
    { allow: ['ffmpeg'] }
  ],
});

const sandbox = await secureSandbox(adapter, { policy });
from agentsh_secure_sandbox.policies import agent_default

policy = agent_default(
    network=[
        {"allow": ["api.stripe.com", "api.openai.com"], "ports": [443]}
    ],
    file=[
        {"allow": "/data/**", "ops": ["read", "write"]}
    ],
)

Merging Policies#

For more control, use merge() or mergePrepend() to combine multiple policy objects:

import { merge, mergePrepend } from '@agentsh/secure-sandbox/policies';

// Append: base rules take priority (first-match-wins)
const combined = merge(basePolicy, overridePolicy);

// Prepend: override rules take priority
const combined = mergePrepend(basePolicy, overridePolicy);
from agentsh_secure_sandbox.policies import merge, merge_prepend

# Append: base rules take priority (first-match-wins)
combined = merge(base_policy, override_policy)

# Prepend: overrides take priority
combined = merge_prepend(base_policy, override_policy)

Policy Rules Reference#

A policy is a TypeScript object with five optional rule arrays: file, network, command, env, and packageRules. Here is the overall structure:

import { agentDefault } from '@agentsh/secure-sandbox/policies';

const policy = agentDefault({
  file: [
    // file rules go here (see below)
  ],
  network: [
    // network rules go here (see below)
  ],
  command: [
    // command rules go here (see below)
  ],
  env: [
    // environment rules go here (see below)
  ],
  packageRules: [
    // package install rules go here (see below)
  ],
});

const sandbox = await secureSandbox(adapter, { policy });
from agentsh_secure_sandbox.policies import agent_default

policy = agent_default(
    file=[
        # file rules go here (see below)
    ],
    network=[
        # network rules go here
    ],
    command=[
        # command rules go here
    ],
    env=[
        # environment variable rules go here
    ],
    package_rules=[
        # package install rules go here
    ],
)

Each rule type is an array of rule objects. Rules are evaluated top-to-bottom; the first matching rule wins. Package rules use a match/action pattern instead of allow/deny. The sections below show the available fields and examples for each rule type.

File Rules#

FieldTypeDescription
allow or denystring | string[]Glob pattern(s) for file paths
ops('read' | 'write' | 'create' | 'delete')[]Which file operations this rule applies to. Omit for all operations.
decision'allow' | 'deny' | 'redirect' | 'audit' | 'softDelete'Override the decision (alternative to using allow/deny keys)
redirectstringRedirect path (when decision is redirect)
const policy = agentDefault({
  file: [
    // Deny access to credential files
    { deny: ['/home/*/.ssh/**', '/home/*/.aws/**'] },

    // Allow read-only access to /data
    { allow: '/data/**', ops: ['read'] },

    // Soft-delete instead of real delete (files go to trash)
    { decision: 'softDelete', allow: '/workspace/**', ops: ['delete'] },
  ],
});
policy = agent_default(
    file=[
        # Deny access to credential files
        {"deny": ["/home/*/.ssh/**", "/home/*/.aws/**"]},
        # Allow read-only access to workspace
        {"allow": "/workspace/**", "ops": ["read"]},
        # Enable soft-delete (quarantine instead of hard delete)
        {"allow": "/workspace/**", "ops": ["write"], "softDelete": True},
    ],
)

Network Rules#

FieldTypeDescription
allow or denystring[]Domain patterns to match
portsnumber[]Restrict to specific ports. Omit for all ports.
decision'allow' | 'deny' | 'redirect'Override the decision
redirectstringRedirect destination (when decision is redirect)
const policy = agentDefault({
  network: [
    // Allow only HTTPS to specific APIs
    { allow: ['api.stripe.com', 'api.openai.com'], ports: [443] },

    // Block cloud metadata endpoints
    { deny: ['169.254.169.254', 'metadata.google.internal'] },

    // Redirect npm to internal registry
    { decision: 'redirect', allow: ['registry.npmjs.org'], redirect: 'npm.internal.company.com' },
  ],
});
policy = agent_default(
    network=[
        # Allow only HTTPS to specific APIs
        {"allow": ["api.stripe.com", "api.openai.com"], "ports": [443]},
        # Block cloud metadata endpoints
        {"deny": ["169.254.169.254", "metadata.google.internal"]},
        # Redirect registry traffic
        {"allow": ["registry.npmjs.org", "pypi.org"]},
    ],
)

Command Rules#

FieldTypeDescription
allow or denystring[]Command names or patterns
decision'allow' | 'deny' | 'redirect'Override the decision
redirectstringRedirect to alternative command
const policy = agentDefault({
  command: [
    // Block privilege escalation
    { deny: ['sudo', 'su', 'env', 'shutdown', 'reboot'] },

    // Redirect curl to agentsh-fetch (prevents raw exfiltration)
    { decision: 'redirect', allow: ['curl', 'wget'], redirect: 'agentsh-fetch' },
  ],
});
policy = agent_default(
    command=[
        # Block privilege escalation
        {"deny": ["sudo", "su", "env", "shutdown", "reboot"]},
        # Redirect curl/wget to safe wrapper
        {"redirect": {"from": "curl", "to": "safe-curl"}},
    ],
)

Environment Rules#

Control which environment variables are visible to commands, or inject new ones:

const policy = agentDefault({
  env: [
    // Hide specific environment variables
    { deny: ['AWS_SECRET_ACCESS_KEY', 'GITHUB_TOKEN'] },

    // Inject environment variables
    { inject: { 'NODE_ENV': 'production', 'AGENTSH': '1' } },
  ],
});
policy = agent_default(
    env=[
        # Hide specific environment variables
        {"deny": ["AWS_SECRET_ACCESS_KEY", "GITHUB_TOKEN"]},
        # Inject safe defaults
        {"set": {"NODE_ENV": "sandbox", "HOME": "/workspace"}},
    ],
)

Package Rules#

Control what packages can be installed based on vulnerability data, malware detection, typosquatting analysis, and license checks. Package rules are evaluated when an agent runs npm install, pip install, or similar commands.

FieldTypeDescription
matchPackageMatchConditions to match: vulnerability, malware, typosquat, license, newPackage
action'block' | 'warn' | 'approve'What to do when the rule matches

Match conditions:

ConditionTypeDescription
vulnerability{ severity: 'critical' | 'high' | 'medium' | 'low' }Match packages with known vulnerabilities at or above the given severity
malwaretrueMatch packages flagged as malware
typosquattrueMatch packages detected as typosquats of popular packages
license{ category: string }Match packages by license category (e.g. 'copyleft', 'unknown')
newPackagetrueMatch packages not already in the lockfile
const policy = agentDefault({
  packageRules: [
    // Block critical vulnerabilities and malware
    { match: { vulnerability: { severity: 'critical' } }, action: 'block' },
    { match: { malware: true }, action: 'block' },
    { match: { typosquat: true }, action: 'block' },

    // Block copyleft licenses
    { match: { license: { category: 'copyleft' } }, action: 'block' },

    // Warn on medium vulnerabilities
    { match: { vulnerability: { severity: 'medium' } }, action: 'warn' },

    // Approve new packages (require review)
    { match: { newPackage: true }, action: 'approve' },
  ],
});
policy = agent_default(
    package_rules=[
        # Block critical vulnerabilities and malware
        {"match": {"vulnerability": {"severity": "critical"}}, "action": "block"},
        {"match": {"malware": True}, "action": "block"},
        # Block known typosquats
        {"match": {"typosquat": True}, "action": "block"},
        # Audit copyleft licenses
        {"match": {"license": {"spdx": ["GPL-3.0", "AGPL-3.0"]}}, "action": "audit"},
    ],
)
Default package rules

The agentDefault() preset includes sensible package rules: block critical vulnerabilities, malware, and typosquats; block copyleft licenses; warn on medium vulnerabilities; and approve new packages for review.

Package Install Security Checks#

When an agent runs a package install command (npm install, pip install, cargo add, etc.), secure-sandbox can intercept the install and check each package against multiple vulnerability and quality databases before allowing it.

Providers#

Three providers supply package intelligence:

ProviderDescriptionData Source
localFast local checks — typosquat detection, license analysis, lockfile diffingLocal heuristics
osvKnown vulnerability lookup via the Open Source Vulnerabilities databaseosv.dev
depsdevPackage metadata, scorecard, and dependency graph analysisdeps.dev

Configuration#

Enable package checks via the packageChecks config option:

const sandbox = await secureSandbox(adapter, {
  packageChecks: {
    scope: 'all',           // 'all' | 'new-only' | 'none'
    providers: ['local', 'osv', 'depsdev'],
  },
});
sandbox = await secure_sandbox(adapter,
    package_checks={
        "scope": "all",           # "all" | "new-only" | "none"
        "providers": ["local", "osv", "depsdev"],
    },
)
FieldTypeDefaultDescription
scope'all' | 'new-only' | 'none''all''all' checks every package; 'new-only' checks only packages not in the lockfile; 'none' disables checks
providersstring[]['local', 'osv', 'depsdev']Which providers to query
Enabled by default

The agentDefault() preset enables package checks with all three providers. Set packageChecks: { scope: 'none' } to disable.

Security Modes#

The system automatically negotiates the strongest available security mode based on kernel capabilities. Ptrace mode is opt-in. Modes from strongest to weakest:

ModeEnforcement LayersPlatforms
full seccomp + Landlock + FUSE + Network Proxy + DLP E2B, Daytona, Blaxel, Sprites, Runloop
full (hybrid) ptrace (execve) + seccomp (file monitor) + Landlock + FUSE + Network Proxy + DLP exe.dev
ptrace ptrace syscall interception + Network Proxy + DLP Modal (gVisor), AWS Fargate, restricted Kubernetes (opt-in)
landlock seccomp + Landlock + Network Proxy + DLP (no FUSE) Vercel, Cloudflare
landlock-only Landlock filesystem restrictions only Older kernels
minimal Policy evaluation only, no kernel enforcement Fallback

Each layer provides specific protections:

Minimum Security#

Use minimumSecurityMode to fail provisioning if the kernel can’t meet your requirements:

const sandbox = await secureSandbox(adapter, {
  minimumSecurityMode: 'landlock',  // fail if weaker than landlock
});
sandbox = await secure_sandbox(adapter,
    minimum_security_mode="landlock",  # fail if weaker than landlock
)
Production recommendation

Always set minimumSecurityMode in production to ensure kernel-level enforcement. Without it, the system may silently fall back to minimal mode.

Threat Intelligence#

By default, secure-sandbox blocks connections to known-malicious domains using two threat feeds:

Package registries (npm, PyPI, Cargo, Go, GitHub) are always allowlisted and never blocked by threat feeds.

Disable threat feeds entirely:

const sandbox = await secureSandbox(adapter, {
  threatFeeds: false,
});
sandbox = await secure_sandbox(adapter,
    threat_feeds=False,
)

Custom Feeds#

Add your own threat intelligence sources or replace the defaults:

const sandbox = await secureSandbox(adapter, {
  threatFeeds: {
    action: 'deny',        // or 'audit' to log without blocking
    feeds: [
      {
        name: 'internal-blocklist',
        url: 'https://security.internal/blocked-domains.txt',
        format: 'domain-list',   // or 'hostfile'
        refreshInterval: '1h',
      },
    ],
    allowlist: ['trusted.internal.com'],
  },
});
sandbox = await secure_sandbox(adapter,
    threat_feeds={
        "action": "deny",        # or "audit" to log without blocking
        "feeds": [
            {
                "name": "internal-blocklist",
                "url": "https://security.internal/feeds/domains.txt",
                "type": "domain",
                "refresh_hours": 6,
            },
            {
                "name": "urlhaus",
                "url": "https://urlhaus.abuse.ch/downloads/text_online/",
                "type": "domain",
            },
        ],
    },
)
FieldTypeDescription
action'deny' | 'audit'Block or just log matches. Default: 'deny'
feeds[].namestringDisplay name for the feed
feeds[].urlstringURL to the feed file
feeds[].format'hostfile' | 'domain-list'Feed format
feeds[].refreshIntervalstringHow often to refresh. Default: '6h'
allowliststring[]Domains to never block

API Reference#

secureSandbox(adapter, config?)#

Secures any sandbox via its adapter. Returns a SecuredSandbox that mediates every command, file read, and file write through the agentsh policy engine.

async function secureSandbox(
  adapter: SandboxAdapter,
  config?: SecureConfig
): Promise<SecuredSandbox>
async def secure_sandbox(
    adapter: SandboxAdapter,
    *,
    policy: Policy | None = None,
    package_checks: PackageChecksConfig | None = None,
    minimum_security_mode: str | None = None,
    threat_feeds: bool | ThreatFeedsConfig | None = None,
    server_config: ServerConfig | None = None,
) -> SecuredSandbox: ...

SecuredSandbox#

The secured wrapper returned by secureSandbox():

interface SecuredSandbox {
  exec(command: string, opts?: {
    cwd?: string;
    timeout?: number;
  }): Promise<ExecResult>;

  writeFile(path: string, content: string): Promise<WriteFileResult>;
  readFile(path: string): Promise<ReadFileResult>;
  stop(): Promise<void>;

  readonly sessionId: string;
  readonly securityMode: SecurityMode;
}
class SecuredSandbox:
    async def exec(self, command: str, *, cwd: str | None = None, timeout: int | None = None) -> ExecResult: ...
    async def write_file(self, path: str, content: str | bytes) -> None: ...
    async def read_file(self, path: str) -> str: ...
    async def stop(self) -> None: ...
    @property
    def security_mode(self) -> str: ...
    @property
    def audit_log(self) -> list[AuditEntry]: ...
MethodDescription
exec(command)Run a shell command through the policy engine. Returns { stdout, stderr, exitCode }.
writeFile(path, content)Write a file after policy check. Returns { success, path, error? }.
readFile(path)Read a file after policy check. Returns { success, path, content?, error? }.
stop()Terminate the sandbox and release resources.
sessionIdUnique agentsh session identifier.
securityModeNegotiated security mode (full, ptrace, landlock, landlock-only, minimal).

Config Options#

OptionTypeDefaultDescription
policyPolicyDefinitionagentDefault()Policy rules for file, network, command, env, and package access
workspacestring'/workspace'Root path for workspace-relative policy rules
installStrategystring'download''download', 'upload', 'preinstalled', or 'running'
agentshVersionstring'0.15.0'Pin to a specific agentsh release
minimumSecurityModeSecurityModeFail if kernel can’t enforce this level
threatFeedsfalse | ThreatFeedsConfigURLhaus + Phishing.DatabaseThreat intelligence configuration
packageChecksPackageChecksConfigAll providers enabledPackage install security check configuration (see Package Install Security Checks)
realPathsbooleanfalseUse real host paths instead of virtualizing
enforceRedirectsbooleanfalseMake redirects enforced instead of shadowed
serverConfigServerConfigAdvanced server options: gRPC settings, logging, session limits, audit config, sandbox resource limits, and ptrace configuration
watchtowerstringEvent sink URL for audit events
traceParentstringW3C traceparent header for OpenTelemetry tracing

Error Classes#

All errors extend AgentSHError:

ErrorWhen
PolicyValidationErrorPolicy object fails Zod schema validation
ProvisioningErrorBinary installation, config writing, or server startup fails
IntegrityErrorSHA256 checksum mismatch on downloaded binary
RuntimeErrorCommand execution failure (includes sessionId for debugging)
MissingPeerDependencyErrorSandbox provider SDK not installed
IncompatibleProviderVersionErrorProvider SDK version mismatch
import { ProvisioningError, RuntimeError } from '@agentsh/secure-sandbox';

try {
  const sandbox = await secureSandbox(adapter);
} catch (e) {
  if (e instanceof ProvisioningError) {
    console.error('Setup failed:', e.phase, e.stderr);
  }
}
from agentsh_secure_sandbox import ProvisioningError, RuntimeError

try:
    sandbox = await secure_sandbox(adapter)
except ProvisioningError as e:
    if e.reason == "unsupported_platform":
        print(f"Platform not supported: {e.message}")
    elif e.reason == "binary_install_failed":
        print(f"Failed to install agentsh: {e.message}")
except RuntimeError as e:
    print(f"Runtime error: {e.message}")
Custom adapters

To integrate a sandbox not listed above, implement the SandboxAdapter interface with exec, writeFile, and readFile methods, then pass it directly to secureSandbox().

const myAdapter: SandboxAdapter = {
  async exec(cmd, args, opts) { /* ... */ },
  async writeFile(path, content) { /* ... */ },
  async readFile(path) { /* ... */ },
};

const sandbox = await secureSandbox(myAdapter);
class MyAdapter:
    async def exec(self, cmd: str, args: list[str], opts: dict | None = None) -> ExecResult: ...
    async def write_file(self, path: str, content: str | bytes) -> None: ...
    async def read_file(self, path: str) -> str: ...

Ptrace Configuration#

For platforms that require ptrace-based enforcement (Modal, AWS Fargate, restricted Kubernetes), configure ptrace via serverConfig.ptrace:

const sandbox = await secureSandbox(adapter, {
  serverConfig: {
    ptrace: {
      enabled: true,
      attachMode: 'children',        // 'children' or 'pid'
      maskTracerPid: 'off',          // hide tracer from /proc/*/status
      trace: {
        execve: true,
        file: true,
        network: true,
        signal: true,
      },
      performance: {
        seccompPrefilter: false,     // disable on gVisor
        maxTracees: 500,
        maxHoldMs: 5000,
      },
      onAttachFailure: 'fail_open', // or 'fail_closed'
    },
  },
});
sandbox = await secure_sandbox(adapter,
    server_config={
        "ptrace": {
            "enabled": True,
            "attach_mode": "children",        # "children" or "pid"
            "trace": {
                "file_ops": True,
                "network_ops": True,
                "process_ops": True,
            },
            "performance": {
                "cache_decisions": True,
                "batch_signals": True,
            },
            "behavior": {
                "follow_forks": True,
                "exit_symlink_verify": True,
            },
        },
    },
)
OptionTypeDefaultDescription
enabledbooleanfalseEnable ptrace-based syscall tracing
attachMode'children' | 'pid''children''children' traces all child processes; 'pid' attaches to a specific PID
maskTracerPidstring'off'Hide the tracer PID from /proc/*/status reads
trace.execvebooleantrueIntercept execve/execveat syscalls
trace.filebooleantrueIntercept file I/O syscalls (openat, unlinkat, renameat2, etc.)
trace.networkbooleantrueIntercept connect/bind + DNS proxy
trace.signalbooleantrueIntercept kill/tgkill/tkill
performance.seccompPrefilterbooleantrueInject BPF pre-filter into tracees. Disable on gVisor.
performance.maxTraceesnumber500Maximum concurrent traced threads
performance.maxHoldMsnumber5000Timeout for held syscalls (ms)
onAttachFailure'fail_open' | 'fail_closed''fail_open'Behavior when ptrace attach fails

Modal sandboxes run on gVisor, which does not support seccomp user-notify, Landlock, or FUSE. The Modal adapter uses ptrace-based enforcement for full policy coverage.

import { secureSandbox } from '@agentsh/secure-sandbox';
import { modal, modalDefaults } from '@agentsh/secure-sandbox/adapters/modal';

const sandbox = await secureSandbox(modal(modalSandbox), {
  ...modalDefaults(),   // pre-configured for gVisor/ptrace
  // your policy overrides here
});
from agentsh_secure_sandbox import secure_sandbox
from agentsh_secure_sandbox.adapters.modal import modal, modal_defaults

sandbox = await secure_sandbox(modal(modal_sandbox), **modal_defaults())
    # pre-configured for gVisor/ptrace

modalDefaults() returns a Partial<SecureConfig> pre-configured for gVisor:

Runloop Adapter#

Runloop Devboxes run as a non-root user (runloop) with passwordless sudo. The Runloop adapter uses the Runloop API client for command execution and file operations.

import Runloop from '@runloop/api-client';
import { secureSandbox, adapters } from '@agentsh/secure-sandbox';

const client = new Runloop();
const devbox = await client.devboxes.createAndAwaitRunning({ blueprint_id: 'my-bp' });

const sandbox = await secureSandbox(
  adapters.runloop({ client, id: devbox.id }),
  adapters.runloopDefaults(),   // pre-configured for Runloop
);
from runloop import Runloop
from agentsh_secure_sandbox import secure_sandbox
from agentsh_secure_sandbox.adapters import runloop, runloop_defaults

client = Runloop()
devbox = await client.devboxes.create_and_await_running(blueprint_id="my-bp")
sandbox = await secure_sandbox(
    runloop(client=client, id=devbox.id),
    **runloop_defaults(),
)

runloopDefaults() returns a Partial<SecureConfig> pre-configured for Runloop Devboxes:

exe.dev Adapter#

exe.dev VMs are persistent, SSH-accessible VMs accessed through the exe.dev gateway. The adapter routes commands as ssh exe.dev ssh <vmName> <command>. VMs must be created externally via ssh exe.dev new.

import { secureSandbox, adapters } from '@agentsh/secure-sandbox';

// VM already created: ssh exe.dev new --name=my-vm --image=ubuntu:22.04
const sandbox = await secureSandbox(
  adapters.exe('my-vm'),
  adapters.exeDefaults(),   // pre-configured hybrid ptrace+seccomp mode
);
from agentsh_secure_sandbox import secure_sandbox
from agentsh_secure_sandbox.adapters import exe, exe_defaults

# VM already created: ssh exe.dev new --name=my-vm --image=ubuntu:22.04
sandbox = await secure_sandbox(
    exe("my-vm"),
    **exe_defaults(),
)

exeDefaults() returns a Partial<SecureConfig> pre-configured for exe.dev’s hybrid enforcement mode:

Freestyle Adapter#

Freestyle provides Firecracker-backed Linux VMs accessed via the freestyle-sandboxes SDK. The adapter uses Freestyle’s typed filesystem API (vm.fs.*) for file operations and the VM exec API for commands. Use configureFreestyleSpec to bake agentsh into a VmSpec snapshot, or boot a stock VM and let freestyleDefaults() install agentsh on first start.

import { freestyle as freestyleClient, VmSpec } from 'freestyle-sandboxes';
import { secureSandbox } from '@agentsh/secure-sandbox';
import { freestyle, freestyleDefaults, configureFreestyleSpec } from '@agentsh/secure-sandbox/adapters/freestyle';

const fs = freestyleClient({ apiKey: process.env.FREESTYLE_API_KEY });

// Option A: bake agentsh into a snapshot (one-time, faster cold-start)
const spec = configureFreestyleSpec(new VmSpec().snapshot());
const { vm } = await fs.vms.create({ spec });

// Option B: stock VM — agentsh installs on first start
// const { vm } = await fs.vms.create({ spec: new VmSpec() });

const sandbox = await secureSandbox(freestyle(vm), freestyleDefaults());
await sandbox.exec('echo hello');
await sandbox.stop();
import os
from agentsh_secure_sandbox import secure_sandbox
from agentsh_secure_sandbox.adapters.freestyle import (
    FreestyleClient, freestyle, freestyle_defaults,
)

async with FreestyleClient(api_key=os.environ["FREESTYLE_API_KEY"]) as client:
    vm = await client.create_sandbox_vm()
    try:
        sandbox = await secure_sandbox(freestyle(vm), **freestyle_defaults())
        await sandbox.exec("echo hello")
    finally:
        await vm.stop()

freestyleDefaults() returns a Partial<SecureConfig> tuned for the Freestyle kernel: