Wrap an agent with firma run
firma run launches an agent through a runtime backend and routes outbound calls through the Sidecar. On structural backends, such as Linux bwrap, the backend removes the agent’s ability to bypass that route. On current macOS vz and Windows/WSL2 wsl2, enforcement is proxy-only compatibility mode: useful for cooperative agents, but not a hard network boundary. This guide shows you how to use firma run, and when you should reach for it instead of plain proxy env vars.
flowchart LR
Run["firma run"]
Sandbox["Sandbox backend"]
Seccomp["Managed seccomp filter"]
Agent["Agent process"]
LocalExec["Sidecar local-exec gate"]
Sidecar["Firma Sidecar pipeline"]
Authority["Firma Authority"]
External["External services"]
Audit["Audit event"]
Run -->|"starts with profile"| Sandbox
Sandbox -->|"loads"| Seccomp
Sandbox -->|"launches"| Agent
Agent -->|"pre-exec command check (optional)"| LocalExec
LocalExec -->|"allow only"| Agent
Agent -->|"structural: forced; proxy-only: cooperative"| Sidecar
Authority -->|"tokens, policies, revocations"| Sidecar
Sidecar -->|"allowed traffic"| External
Sidecar -->|"policy decision"| Audit
You should already have a Sidecar running with a capability for some agent identity (see Run the sidecar standalone and Issue capability tokens).
When firma run is the right tool
Section titled “When firma run is the right tool”Reach for firma run when one or more of these is true:
- The agent is third-party code or runs prompts you don’t fully control (most LLM agents).
- You want a hard guarantee that nothing escapes the policy boundary, and you are using a structural backend.
- The agent might spawn child processes that don’t inherit env vars, and you are using a structural backend.
- You’re shipping a managed runtime to others and want enforcement to be part of the product.
For development work, a Sidecar you wrote, or a CI script you trust, plain proxy env vars are fine. For everything else, firma run is the right wrapper, but the backend determines whether the routing is mandatory or proxy-only.
For the conceptual background, read The sandbox boundary.
Step 1: Pick a backend
Section titled “Step 1: Pick a backend”firma run uses a different sandbox backend per platform. The defaults are usually right:
| Platform | Default backend | Notes |
|---|---|---|
| Linux | bwrap | Structural mode; requires unprivileged user namespaces + AppArmor allowance for bwrap. |
| macOS | vz | Default compatibility mode: host process, sandbox-exec, proxy bridge, explicit --allow-non-structural opt-in. Experimental structural modes are available via sandbox-exec network-deny or the VZ guest runner contract. |
| Windows | wsl2 | Current compatibility mode; explicit --allow-non-structural opt-in. |
Verify the platform default works on your host. On Linux, the bwrap backend
needs two things: unprivileged user namespaces enabled, and — on AppArmor
distros — permission for bwrap to keep CAP_NET_ADMIN inside the namespace
so it can bring up loopback.
unshare --user --pid echo ok # user namespacesbwrap --unshare-net --bind / / true # bwrap + net namespaceIf the first command prints ok but the second exits with bwrap: loopback: Failed RTM_NEWADDR: Operation not permitted, you’re on a distro with
AppArmor restricting unprivileged user namespaces (Ubuntu 24.04 ships this
on by default). See Common gotchas below for the fix.
If unshare --user --pid echo ok itself fails with a permission error,
enable unprivileged user namespaces (sysctl -w kernel.unprivileged_userns_clone=1
on some distros) or pick a different backend.
On macOS and WSL, firma run defaults to compatibility mode and fails closed unless you acknowledge the weaker boundary with --allow-non-structural or run.allow_non_structural = true. On WSL, firma run does not implicitly select bwrap; it automatically selects the wsl2 compatibility backend.
macOS structural modes are explicit opt-ins:
# Intermediate: block external IP egress with sandbox-exec, loopback remains reachable.FIRMA_RUN_VZ_STRUCTURAL_NETWORK=1 firma run --profile generic -- curl https://example.com
# Under development: build, ad-hoc sign and launch a local Virtualization.framework runner.just macos-vz-runner-devFIRMA_RUN_VZ_GUEST=1 \FIRMA_RUN_VZ_GUEST_RUNNER=target/macos-vz-runner-dev/firma-vz-runner \FIRMA_RUN_VZ_GUEST_KERNEL=/var/lib/firma/vz/vmlinuz \FIRMA_RUN_VZ_GUEST_INITRD=/var/lib/firma/vz/initrd.img \FIRMA_RUN_VZ_GUEST_ROOTFS=/var/lib/firma/vz/rootfs.img \firma run --profile generic -- curl https://example.comGuest mode is still under active development. Today it validates those artifact paths, creates an owner-only run/contract directory, writes vz-guest-launch.json and spawns the runner with --launch-contract scheme. The runner is responsible for the Apple Virtualization.framework lifecycle and for enforcing the contract inside the guest.
Step 2: Scaffold a config directory with firma config
Section titled “Step 2: Scaffold a config directory with firma config”firma config creates a complete working configuration directory — sidecar + authority config, Cedar policy, mapping rules, and an authority keypair — in a single step:
firma config --name my-agent --posture dev --mapping anthropicThis writes to the current directory by default. To write to a specific directory, pass --output-dir:
firma config --name my-agent --posture dev --mapping anthropic --output-dir .localGenerated layout (default --output-dir .firma):
.firma/ firma.toml — unified config (authority + sidecar + run profiles) mapping-rules.toml — base mapping rules mappings/anthropic.toml — Anthropic endpoint mapping policies/dev.cedar — Cedar enforcement policy issuance-policies/issuance.cedar
$XDG_DATA_HOME/firma/ — platform state dir (keys, revocations, CA) authority.key — authority signing keypair (preserved on re-run) audit.key — demo audit signing key revocations.txtfirma config is idempotent — re-running preserves existing files including the authority keypair; pass --force to overwrite everything. Skip all interactive prompts with --yes. Preview without writing:
firma config --dry-runInspect the generated config:
cat firma.tomlStep 3: Run with Per-Run Sidecar (Default)
Section titled “Step 3: Run with Per-Run Sidecar (Default)”With --sidecar local (also the default when no sidecar endpoint is configured), firma run always starts a per-run
Sidecar alongside the agent process, waits for readiness, and tears it down
on exit.
The simplest invocation:
cargo run --release -p firma -- run --profile generic -- curl https://example.comIf you use a coding-agent profile:
cargo run --release -p firma -- run --profile codex -- codexOptional: run a Sidecar manually (--sidecar <url>)
Section titled “Optional: run a Sidecar manually (--sidecar <url>)”Manual sidecar startup is only needed when you explicitly choose external mode
or operate with a pre-managed sidecar (systemd / firma sidecar start).
In a dedicated terminal:
firma sidecar -c .firma/firma.tomlWait for the sidecar ready line.
Opt out for production or CI:
--no-autostart— fail with a typedMissingSidecar/SidecarUnreachableerror instead of spawning a child Sidecar.--sidecar tcp://host:port/--sidecar unix:///path— point at an external sidecar (systemd-managed orfirma sidecar start); never autostarts.
Autostart currently requires Unix. On Windows, use --sidecar <url> with a pre-started Sidecar.
Authority bootstrap
Section titled “Authority bootstrap”Before the Sidecar starts, firma run resolves which Authority to use.
Precedence:
--authority local/--authority <url>— CLI override.[authority]section present in the discoveredfirma.toml(walk-up from the current directory to<dir>/.firma/firma.toml, or$FIRMA_CONFIG) — autostart a local Mini Authority.[sidecar.authority].urlset — connect to that remote Authority.- Nothing configured —
firma runfalls back to local autostart so zero-config works. The spawned Authority uses an ephemeral signing key and a per-run loopback listen address; it is killed onfirma runexit.--no-autostartoverrides this to fail withMissingAuthority.
Flags:
--authority local— autostart a local Mini Authority on a per-run loopback ephemeral port; overrides config.--authority <url>— point at a remote Authority; overrides config; fails withAuthorityUnreachableif the URL does not answer.--authority-profile <name>— profile materialised by the autostarted Mini Authority. Today onlydeveloperships. Ignored when the Authority is remote or already reachable.
--no-autostart --authority local is a typed argument-conflict error.
Autostart startup notices
Section titled “Autostart startup notices”firma run logs a structured INFO line to stderr for every autostart and reuse event so you can see exactly what was started, at what address, and under which run ID. With the default log filter (info), you will see lines like:
2026-05-27T10:00:01Z INFO firma_run::authority::supervisor: authority started sandbox_id="01970def" pid=84231 listen_addr="[::1]:54321"
2026-05-27T10:00:02Z INFO firma_run::sidecar::supervisor: sidecar started sandbox_id="01970def" pid=84235 endpoint="127.0.0.1:18080"If a local authority was already reachable before the run, you see an INFO instead:
2026-05-27T10:00:01Z INFO firma_run::routing: authority reused: existing local authority on plaintext loopback sandbox_id="01970def" url="http://[::1]:50051"On exit, each autostarted component logs a stopping notice:
2026-05-27T10:00:10Z INFO firma_run::sidecar::supervisor: sidecar stopped pid=842352026-05-27T10:00:10Z INFO firma_run::authority::supervisor: authority stopped pid=84231Key fields:
| Field | Meaning |
|---|---|
sandbox_id | UUIDv7 run identifier — unique per firma run invocation |
pid | OS process ID of the autostarted component |
listen_addr / endpoint | Address the component is reachable at |
url | Full URL (authority reuse path) |
Controlling log visibility:
Log output goes to stderr by default. Use --log-filter (or FIRMA_LOG_FILTER) to adjust verbosity. Use --log-file (or FIRMA_LOG_FILE) to redirect logs to a file:
# Quiet: suppress info, keep warningsfirma run --log-filter warn -- my-agent
# Redirect to file (startup notices go to the file, not the terminal)firma run --log-file /tmp/firma.log -- my-agentStep 4: What firma run does
Section titled “Step 4: What firma run does”Everything after -- is the command and its arguments.
On structural backends, firma run:
- Resolves the
genericprofile. - Builds a sandbox using the platform default backend.
- Starts the in-sandbox proxy bridge listening on
127.0.0.1:18080. - Sets
HTTP_PROXY=http://127.0.0.1:18080(and the HTTPS variant). - Launches
curl https://example.cominside the sandbox under a sandbox identity.
The Sidecar receives the curl’s request, runs it through the pipeline, and either dispatches or denies. With sidecar-seeded capabilities, the curl invocation never sees a token; it just talks to the proxy. If you use --capability-file, current firma run exports capability material into the wrapped process environment for compatibility, so do not use that mode when token non-exposure is a hard requirement.
On current macOS vz and Windows/WSL2 wsl2, firma run instead starts a host-side proxy bridge, injects proxy environment variables, clears NO_PROXY, and refuses to launch unless non-structural mode is explicitly allowed. A cooperative HTTP client is mediated and audited; a non-cooperative client can still bypass by ignoring proxy variables or opening direct sockets. The runtime logs this as a backend compatibility proof, not a structural proof.
firma run execution flow (Linux: bwrap + seccomp + Sidecar governance)
Section titled “firma run execution flow (Linux: bwrap + seccomp + Sidecar governance)”flowchart TD
A["Operator runs: firma run --profile generic -- <command>"]
B["Resolve profile/config/backend (Linux default: bwrap)"]
C{"Authority available?"}
D["Autostart Mini Authority (optional)"]
E["Resolve Sidecar endpoint"]
F{"Sidecar reachable?"}
G["Autostart per-run Sidecar (optional)"]
H["Resolve seccomp source + artifact path"]
I["Verify seccomp metadata + checksum + trust-path"]
J{"Verification/load OK?"}
K["Fail closed: block launch"]
L["Build bwrap sandbox + load --seccomp filter"]
M{"sidecar_local_exec configured?"}
N["Send pre-exec local.exec request (sandbox_id + session_id)"]
O{"Decision = allow?"}
P["Deny/timeout/invalid/pending -> fail closed"]
Q["Launch wrapped command in sandbox"]
R["Command egress forced to local proxy bridge"]
S["Sidecar pipeline: normalize -> capability/policy -> allow/deny + audit"]
A --> B --> C
C -- "no, and autostart allowed" --> D --> E
C -- "yes" --> E
E --> F
F -- "no, and autostart allowed" --> G --> H
F -- "yes" --> H
H --> I --> J
J -- "no" --> K
J -- "yes" --> L --> M
M -- "no" --> Q
M -- "yes" --> N --> O
O -- "no" --> P
O -- "yes" --> Q --> R --> S
Flow references:
- Linux local-command architecture:
docs/architecture/linux-local-command-enforcement.md - Local-exec request/response contract:
docs/architecture/command-governance-local-exec-contract.md firma runautostart + flags + typed errors:docs/cli.md(## firma run)- Runtime launcher implementation:
crates/firma/src/services/run.rs firma-runruntime orchestration:crates/firma-run/src/runtime.rs- Linux backend (
bwrap):crates/firma-run/src/backend/linux_bwrap.rs - Seccomp artifact verify/load path:
crates/firma-run/src/seccomp.rs - Local-exec mediator client:
crates/firma-run/src/mediator.rs - Sidecar local-exec endpoint:
crates/firma-sidecar/src/local_exec/endpoint.rs
Step 5: Use the right capability
Section titled “Step 5: Use the right capability”For Stage 1 to allow the call, the Sidecar must have a capability matching (session_id, action_class, resource). Two options:
Pre-staged capability seed. Issue a capability once with firma authority issue --output .local/capability-<agent>.toml and reference it in [sidecar.capability_seed].paths in firma.toml. Right for a long-lived dev workflow.
Per-run capability. Pass --capability-file to firma run. The wrapper writes the file to a host-side path the Sidecar reads. Right for one-off invocations.
firma authority -c .firma/firma.toml issue \ --agent-id local-dev \ --session-id $(uuidgen) \ --action communication.external.send \ --output .firma/capability-local-dev.toml
firma run \ --config .firma/firma.toml \ --profile generic \ --capability-file .firma/capability-local-dev.toml \ -- curl https://example.comStep 6: Inspect the effective config
Section titled “Step 6: Inspect the effective config”Before you trust a firma run invocation in production, see what it’s actually going to do:
firma run --profile generic --print-effective-config -- echo hiThis prints the resolved profile as JSON: which backend, which env vars are injected, which mounts are visible inside the sandbox, which identity remap applies. No agent is launched. Use this to audit your wrapper config the same way you’d terraform plan infrastructure.
Useful flags
Section titled “Useful flags”firma run --help is the full reference. The flags that come up most often:
| Flag | Effect |
|---|---|
--profile <name> | Pick a runtime profile. generic is the default; codex adds workspace mounts for coding agents. |
--config <file> | Override profile defaults from a TOML/YAML file. |
--backend <bwrap|vz|wsl2|firecracker> | Override the platform default backend. |
--sidecar <local|url> | local autostarts a per-run Sidecar; a tcp://host:port / unix:///path value targets an external one and never autostarts. Omitted: persisted sidecar_endpoint else local autostart. |
--no-autostart | Disable autostart for any missing component and fail loudly. Incompatible with --sidecar local and --authority local. CI / production safety net. |
--sidecar-config <path> | Sidecar TOML template for autostart. Falls back to FIRMA_SIDECAR_CONFIG_FILE, then the discovered firma.toml. |
--sidecar-startup-timeout-secs <n> | Maximum wait for the autostarted Sidecar’s ready line (default 10). |
--capability-file <path> | Pre-staged capability seed for this run. |
--identity-mode <sandbox-user|host-user> | Choose whether the sandboxed process runs as the host user or a remapped sandbox user. |
--print-effective-config | Print resolved config and exit. No agent launched. |
--monitor | Run in observe-only mode for this invocation. Every call is allowed through; firma monitor shows the original deny reason prefixed with monitor_mode:. This flag is the explicit opt-in: it sets FIRMA_ALLOW_MONITOR_MODE=1 on the autostarted sidecar. A config that sets mode = "monitor" without that env var downgrades to enforce at startup. Never use in production. |
What does and does not pass through
Section titled “What does and does not pass through”On structural backends, the agent sees:
- A loopback interface where only
127.0.0.1:18080is reachable. - A DNS stub that answers only the hostnames the Sidecar is configured to route.
HTTP_PROXYandHTTPS_PROXYset to the proxy bridge.- Whatever filesystem the profile mounts (the
genericprofile mounts very little;codexmounts a workspace).
It does not see:
- The capability token when capabilities are pre-seeded into the host-side Sidecar. Current
--capability-filemode is an exception and exports capability material into the wrapped process environment. - Host environment variables (the sandbox starts with a stripped env).
- Host filesystem outside profile-mounted paths.
This means an agent under structural firma run cannot:
- Open a raw TCP socket to anything but the proxy bridge.
- Resolve and connect to
8.8.8.8:53to do its own DNS. - Spawn a child that bypasses
HTTP_PROXY(the network namespace forecloses on this regardless). - Read host files containing secrets.
What it can still do is whatever its capability + policy allow it to do via the Sidecar. The sandbox is plumbing; the policy is what decides.
On proxy-only compatibility backends, the agent sees proxy variables and NO_PROXY clearing, but it does not get a mandatory network namespace or deterministic DNS confinement. That mode is for compatibility and ergonomics; it is not the answer for an adversarial or non-cooperative agent.
Common gotchas
Section titled “Common gotchas”bwrap: setting up uid map: Permission denied. Unprivileged user namespaces are disabled on your kernel. Enable them or use another supported backend for your environment.
bwrap: loopback: Failed RTM_NEWADDR: Operation not permitted. Hits on Ubuntu 24.04 and other distros that ship kernel.apparmor_restrict_unprivileged_userns=1. When bwrap enters its unprivileged user namespace, AppArmor transitions it to the unprivileged_userns profile, which audit deny capability — stripping CAP_NET_ADMIN. bwrap then can’t add 127.0.0.1/8 to lo inside the new netns, and --unshare-net fails. Confirm with sysctl kernel.apparmor_restrict_unprivileged_userns (expect 1) and bwrap --unshare-net --bind / / true (reproduces the error in isolation). Pick one of:
-
Targeted (recommended): install an AppArmor profile that lets bwrap keep its caps inside the userns. Create
/etc/apparmor.d/bwrap:abi <abi/4.0>,include <tunables/global>profile bwrap /usr/bin/bwrap flags=(unconfined) {userns,include if exists <local/bwrap>}Then
sudo apparmor_parser -r /etc/apparmor.d/bwrap. This carves out bwrap specifically; other unprivileged-userns users on the host remain restricted. -
Dev-host shortcut: turn the restriction off globally.
sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0(persist with a drop-in under/etc/sysctl.d/). Fine for a single-user dev VM; do not do this on shared or production hosts — any user can then create unprivileged user namespaces with full caps. -
Sidestep bwrap: use a VM-backed mode if one is available in your setup. VM backends don’t depend on host AppArmor for namespace setup.
firma run exits immediately with a typed backend/config error. This is expected when the selected backend is incompatible with the host. On WSL, implicit backend selection uses wsl2 compatibility mode. On explicit bwrap selection with unsupported host conditions (for example WSL or restricted user namespaces), you’ll get an UnsupportedBackend error with remediation guidance.
The agent sees HTTP_PROXY but its calls still fail with DNS errors on a structural backend. The DNS stub only answers hosts the Sidecar will route. If your mapping rules don’t cover the host, the stub returns NXDOMAIN. Add the host to the mapping (and a permitting rule to the policy).
Tight loops produce PolicyDenied. A coding agent doing one task per second can blow through action_count faster than expected. If your policy gates on action_count, raise the threshold or scope the rule more narrowly.
What’s next
Section titled “What’s next”- Secure a local coding agent — putting
firma runto work for Claude Code / Codex / Cursor. - Concepts: The sandbox boundary — for the architectural reasoning.
- Read & verify the audit log — observe what the wrapped agent actually does.