Wrap an agent with firma run
firma run launches an agent inside an OS-level sandbox where every outbound call is forced through the Sidecar. Setting HTTP_PROXY is a hint; firma run is a constraint. This guide shows you how to use it, and when you should reach for it instead of plain proxy env vars.
flowchart LR
Run["firma run"]
Sandbox["Sandbox backend (Linux: bwrap)"]
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 -->|"forced outbound traffic"| 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.
- The agent might spawn child processes that don’t inherit env vars.
- 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 answer.
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 | Requires unprivileged user namespaces + AppArmor allowance for bwrap |
| macOS | vz | Native Apple Virtualization framework |
| Windows | wsl2 | Linux guest under WSL2 |
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.
Step 2: Use the bundled local example as a starting point
Section titled “Step 2: Use the bundled local example as a starting point”The repo ships a complete local-dev setup under examples/firma-run/local/. From the repo root:
examples/firma-run/local/setup.shThis creates a .local/ directory with:
.local/firma.toml— a working sectioned Sidecar config..local/mapping-rules.toml— a starter mapping (one stub rule)..local/audit-key.pem— a freshly generated audit signing key.
The setup script is idempotent — re-running it leaves existing files alone. Inspect the generated config:
cat .local/firma.tomlYou’ll see [sidecar.mapping].default_protected = false and a file audit sink. For a real workload, you’d switch to default_protected = true and tighten the mapping. For first-touch, leave it as is.
Step 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:
cargo run --release -p firma -- sidecar -c .local/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> > persisted
[authority] table in the discovered firma.toml
(~/.config/firma/firma.toml on Linux/macOS,
%USERPROFILE%\.firma\firma.toml on Windows) > a one-time y/N prompt when
both are empty and stdin is a TTY:
No Authority is configured for this project.firma run can start a local Mini Authority for development on [::1]:<ephemeral-port>.This is suitable for a single developer on a trusted workstation.
Start a local Mini Authority? [y/N]:On y the choice is persisted and a per-run Mini Authority is spawned
with an ephemeral signing key and loopback listen address. On n, no-TTY, or --no-autostart,
firma run exits with a typed error (AuthorityDeclined,
AuthorityPromptNoTty, or MissingAuthority). The spawned Authority is
killed on firma run exit.
Flags:
--authority local— autostart a local Mini Authority on a per-run loopback ephemeral port; bypasses the prompt.--authority <url>— point at a remote Authority; bypasses the prompt; 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.
Step 4: What firma run does
Section titled “Step 4: What firma run does”Everything after -- is the command and its arguments. 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. The curl invocation never sees a token; it just talks to the proxy.
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 .local/firma.toml issue \ --agent-id local-dev \ --session-id $(uuidgen) \ --action communication.external.send \ --output .local/capability-local-dev.toml
# Note: the setup.sh-generated .local/firma.toml is sidecar-only (no# [authority] section). To run `firma authority`, add an [authority]# section to .local/firma.toml first, or point -c at a file that has one.
cargo run --release -p firma -- run \ --profile generic \ --capability-file .local/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. |
What does and does not pass through
Section titled “What does and does not pass through”Inside the sandbox, 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 (handled host-side; the agent never holds it).
- Host environment variables (the sandbox starts with a stripped env).
- Host filesystem outside profile-mounted paths.
This means an agent under 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.
Common gotchas
Section titled “Common gotchas”bwrap: setting up uid map: Permission denied. Unprivileged user namespaces are disabled on your kernel. Either enable them or use --backend firecracker if available.
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
--backend firecrackerif it’s available in your setup. The VM backends don’t depend on host AppArmor for namespace setup.
firma run exits immediately with no output. Almost always a startup failure in the bridge. Run with --print-effective-config to verify the config first, then RUST_LOG=debug firma run … to see the bridge logs.
The agent sees HTTP_PROXY but its calls still fail with DNS errors. 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 CapabilityScopeMismatch. 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.