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-sandboxWrap 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 policyfrom 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 policysecureSandbox() 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:
- seccomp — blocks dangerous syscalls (
sudo,nc, privilege escalation) before execution. When FUSE and Landlock are unavailable, seccomp user-notify also provides file enforcement via AddFD emulation. - Landlock — kernel-level filesystem restrictions, immutable after activation
- FUSE — virtual filesystem layer for file operation interception and soft-delete quarantine. Supports automatic mount method detection including the new mount API for Firecracker microVMs.
- ptrace — syscall-level enforcement for restricted runtimes (gVisor, Fargate) where seccomp/eBPF/FUSE are unavailable. Includes exit-time symlink verification and soft-delete.
- Network Proxy — domain and port-based connection filtering, prevents data exfiltration
- DLP — detects and redacts secrets (API keys, tokens, credentials) in command output
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 |
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.
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#
| Field | Type | Description |
|---|---|---|
allow or deny | string | 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) |
redirect | string | Redirect 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#
| Field | Type | Description |
|---|---|---|
allow or deny | string[] | Domain patterns to match |
ports | number[] | Restrict to specific ports. Omit for all ports. |
decision | 'allow' | 'deny' | 'redirect' | Override the decision |
redirect | string | Redirect 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#
| Field | Type | Description |
|---|---|---|
allow or deny | string[] | Command names or patterns |
decision | 'allow' | 'deny' | 'redirect' | Override the decision |
redirect | string | Redirect 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.
| Field | Type | Description |
|---|---|---|
match | PackageMatch | Conditions to match: vulnerability, malware, typosquat, license, newPackage |
action | 'block' | 'warn' | 'approve' | What to do when the rule matches |
Match conditions:
| Condition | Type | Description |
|---|---|---|
vulnerability | { severity: 'critical' | 'high' | 'medium' | 'low' } | Match packages with known vulnerabilities at or above the given severity |
malware | true | Match packages flagged as malware |
typosquat | true | Match packages detected as typosquats of popular packages |
license | { category: string } | Match packages by license category (e.g. 'copyleft', 'unknown') |
newPackage | true | Match 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"},
],
)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:
| Provider | Description | Data Source |
|---|---|---|
local | Fast local checks — typosquat detection, license analysis, lockfile diffing | Local heuristics |
osv | Known vulnerability lookup via the Open Source Vulnerabilities database | osv.dev |
depsdev | Package metadata, scorecard, and dependency graph analysis | deps.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"],
},
)| Field | Type | Default | Description |
|---|---|---|---|
scope | 'all' | 'new-only' | 'none' | 'all' | 'all' checks every package; 'new-only' checks only packages not in the lockfile; 'none' disables checks |
providers | string[] | ['local', 'osv', 'depsdev'] | Which providers to query |
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:
| Mode | Enforcement Layers | Platforms |
|---|---|---|
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:
- seccomp — blocks dangerous syscalls (
sudo,nc,env) before execution - Landlock — kernel-level filesystem restrictions, immutable after activation
- FUSE — virtual filesystem layer for soft-delete quarantine and real-paths virtualization
- Network Proxy — domain and port-based connection filtering, prevents exfiltration
- DLP — detects and redacts secrets in command output
- ptrace — syscall-level interception via Linux ptrace for execve, file I/O, network, and signal enforcement; enables full policy enforcement on Modal (gVisor), Fargate, and restricted runtimes (opt-in)
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
)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:
- URLhaus (abuse.ch) — malware distribution domains, refreshed every 6 hours
- Phishing.Database (GitHub) — active phishing domains, refreshed every 12 hours
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",
},
],
},
)| Field | Type | Description |
|---|---|---|
action | 'deny' | 'audit' | Block or just log matches. Default: 'deny' |
feeds[].name | string | Display name for the feed |
feeds[].url | string | URL to the feed file |
feeds[].format | 'hostfile' | 'domain-list' | Feed format |
feeds[].refreshInterval | string | How often to refresh. Default: '6h' |
allowlist | string[] | 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]: ...| Method | Description |
|---|---|
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. |
sessionId | Unique agentsh session identifier. |
securityMode | Negotiated security mode (full, ptrace, landlock, landlock-only, minimal). |
Config Options#
| Option | Type | Default | Description |
|---|---|---|---|
policy | PolicyDefinition | agentDefault() | Policy rules for file, network, command, env, and package access |
workspace | string | '/workspace' | Root path for workspace-relative policy rules |
installStrategy | string | 'download' | 'download', 'upload', 'preinstalled', or 'running' |
agentshVersion | string | '0.15.0' | Pin to a specific agentsh release |
minimumSecurityMode | SecurityMode | — | Fail if kernel can’t enforce this level |
threatFeeds | false | ThreatFeedsConfig | URLhaus + Phishing.Database | Threat intelligence configuration |
packageChecks | PackageChecksConfig | All providers enabled | Package install security check configuration (see Package Install Security Checks) |
realPaths | boolean | false | Use real host paths instead of virtualizing |
enforceRedirects | boolean | false | Make redirects enforced instead of shadowed |
serverConfig | ServerConfig | — | Advanced server options: gRPC settings, logging, session limits, audit config, sandbox resource limits, and ptrace configuration |
watchtower | string | — | Event sink URL for audit events |
traceParent | string | — | W3C traceparent header for OpenTelemetry tracing |
Error Classes#
All errors extend AgentSHError:
| Error | When |
|---|---|
PolicyValidationError | Policy object fails Zod schema validation |
ProvisioningError | Binary installation, config writing, or server startup fails |
IntegrityError | SHA256 checksum mismatch on downloaded binary |
RuntimeError | Command execution failure (includes sessionId for debugging) |
MissingPeerDependencyError | Sandbox provider SDK not installed |
IncompatibleProviderVersionError | Provider 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}")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,
},
},
},
)| Option | Type | Default | Description |
|---|---|---|---|
enabled | boolean | false | Enable ptrace-based syscall tracing |
attachMode | 'children' | 'pid' | 'children' | 'children' traces all child processes; 'pid' attaches to a specific PID |
maskTracerPid | string | 'off' | Hide the tracer PID from /proc/*/status reads |
trace.execve | boolean | true | Intercept execve/execveat syscalls |
trace.file | boolean | true | Intercept file I/O syscalls (openat, unlinkat, renameat2, etc.) |
trace.network | boolean | true | Intercept connect/bind + DNS proxy |
trace.signal | boolean | true | Intercept kill/tgkill/tkill |
performance.seccompPrefilter | boolean | true | Inject BPF pre-filter into tracees. Disable on gVisor. |
performance.maxTracees | number | 500 | Maximum concurrent traced threads |
performance.maxHoldMs | number | 5000 | Timeout for held syscalls (ms) |
onAttachFailure | 'fail_open' | 'fail_closed' | 'fail_open' | Behavior when ptrace attach fails |
Modal Adapter#
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/ptracemodalDefaults() returns a Partial<SecureConfig> pre-configured for gVisor:
- Ptrace enabled with all four trace subsystems (execve, file, network, signal)
- seccomp pre-filter disabled (not supported on gVisor)
- FUSE deferred mode enabled
- Unix sockets and cgroups disabled
allowDegraded: true— starts even if FUSE/seccomp fail
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:
- seccomp file_monitor with
enforce_without_fuse: true - FUSE deferred mode (activated via
sudo chmod 666 /dev/fuse) - Landlock, cgroups, and unix sockets enabled
- Network proxy in
allintercept mode - LLM proxy with DLP redaction (including custom patterns for Runloop, OpenAI, Anthropic, AWS, GitHub keys)
allowDegraded: true— starts even if some enforcement layers fail
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:
- Hybrid ptrace+seccomp — ptrace handles execve interception while seccomp file_monitor handles file enforcement
- seccomp file_monitor with
enforce_without_fuse: true - FUSE deferred mode (activated via
chmod 666 /dev/fuse) - Landlock, cgroups, and unix sockets enabled
- Network proxy in
allintercept mode - LLM proxy with DLP redaction
allowDegraded: false— fails if enforcement layers cannot start
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:
- seccomp execve interception enabled (command blocking)
- seccomp file_monitor disabled — conflicts with FUSE without Yama
- FUSE in deferred mode (activated via
sudo /bin/chmod 666 /dev/fuseat first session) - Network proxy in
allintercept mode with cloud-metadata and RFC1918 blocked - LLM proxy with DLP redaction (OpenAI / Anthropic / AWS / GitHub / JWT / Slack patterns)
- cgroups + unix sockets enabled, workspace at
/home/user allowDegraded: true— the runtime settles intominimalsecurity mode (no Landlock on the current Freestyle kernel; see callout above)