aasm run
Launch an AI agent — a supported developer tool or a program you own — inside Agent Assembly’s governed launch path: identity registration, policy resolution, proxy wiring, and (optionally) an execution-isolation boundary, resolved once and applied consistently whether you preview it or run it.
Availability.
aasm runis one of three command groups gated behind thedevtoolregion inaa-cli/src/commands/mod.rs(the others areaasm toolsandaasm integrations)..ci/strip-for-publish.shremoves that region from the crates.io publish only —cargo install aasmdoes not haveaasm run. A source build (cargo build -p aa-cli), the GitHub Release tarballs, thecurlinstaller, and the Homebrew formula all ship the unstripped tree and do have it. See CLI Reference — Overview.
Prerequisite. A live (non-
--dry-run) launch first registers the session with the governance gateway. With no reachable gateway it refuses (refusing to launch unregistered: the governance gateway at … is unreachable) rather than running ungoverned: start one withaasm gateway start, or pointAA_GATEWAY_ENDPOINTat the gRPC endpoint. A proxied launch (the default) also needs theaa-proxybinary besideaasm, onPATHor in~/.cargo/bin, and refuses (dedicated proxy failed to start: aa-proxy binary not found) without it. An isolation request that no backend can meet is refused before the gateway step.
Synopsis
aasm run <TOOL> [TOOL_ARGS...] [OPTIONS]
aasm run exec [OPTIONS] -- <PROGRAM> [ARGS...]
Two forms, both entering the same identity/policy/lifecycle/isolation/evidence flow (ADR 0035 §1):
- Dev-tool form —
<TOOL>is one of the built-in adapters:claude,codex,copilot,windsurf. The longer idsaasm integrations listprints (claude-code,github-copilot,windsurf-cascade, …) are accepted too and resolve to the same adapter. Everything after<TOOL>is forwarded verbatim to the launched tool. - Generic command form —
aasm run exec [run-options] -- <program> [args...]launches a program you own.execis resolved only after every tool id has failed to match, so it can never shadow a real tool. There is no adapter for a generic command: no managed settings are generated or applied, andaasm runcannot detect whether the program is installed before attempting to launch it — the launch attempt itself is the test, andexec’s ownNo such file or directoryis the answer if it is not.
Options
| Flag | Type | Default | Description |
|---|---|---|---|
--agent-id <ID> | string | (derived) | Override the agent identity for this session. |
--team-id <ID> | string | (none) | Team identifier for this session. |
--root-agent <ID> | string | (none) | Root agent identifier for lineage tracking. Claiming a parent this way is refused (AncestryUnresolved): rc.8 has no cross-process parent-authority handoff (AAASM-6273), so a launch that declares a parent does not run. A launch that omits --root-agent runs as a root with only what its own policy and leases grant, with no comparison against the process that launched it, and nothing detects a child launched that way. |
--governance-level <LEVEL> | enum | (policy default) | Override the governance level for this session. |
--no-proxy | flag | off | Launch without routing the tool through the governed proxy — an explicit opt-out of transport mediation. Without it, aasm run refuses to launch unless it can establish a trusted local proxy endpoint; it never launches unproxied by accident. Refused outright on a host where another party has already required managed operation of the named tool. |
--policy <PATH> | path | (resolved from $AA_POLICY / default locations) | Policy YAML file this session runs under. A launch refuses when no effective policy resolves — an unconfigured policy is never an implicit allow-all. |
--workdir <DIR> | path | (inherited from this shell) | Directory the launched process starts in. Checked before anything is registered or started; a launch that cannot start where it was told to is refused, not silently redirected. |
--workspace-tx | flag | off | Run the child inside a materialized transaction over the base directory instead of the base itself. See Transactional workspace mode below. |
--workspace-tx-exclude <PATH> | path, repeatable | (none) | A path excluded from the declared surface, relative to the base directory. Requires --workspace-tx. |
--workspace-tx-protect <PATH> | path, repeatable | (none) | A protected selector; a change set touching it refuses to commit without --workspace-tx-approve. Requires --workspace-tx. |
--workspace-tx-approve | flag | off | Pre-authorize a commit that touches a protected selector. Requires --workspace-tx. |
--dry-run | flag | off | Show the resolved launch — identity, policy, proxy, managed settings, launch command, environment, and the execution-isolation report — without executing anything. |
--enforcement-mode <MODE> | enforce | observe | disabled | enforce | Enforcement posture for this session, overriding the policy default. observe records decisions but never applies them — the tool sees Allow for every action, and shadow events land in the audit log. |
--observe | flag | off | Shorthand for --enforcement-mode observe. Mutually exclusive with --enforcement-mode. |
--isolation <INTENT> | none | auto | process | none | How much execution isolation this launch requires. See Isolation intent below. |
--isolation-backend <ID> | string | (unset — auto selects by capability, process defaults to sandlock) | Pin the concrete isolation backend by id. Advanced and diagnostic only — see Backend pinning. |
--max-memory-bytes <BYTES> | integer | (none) | Resident-memory ceiling. A launch refuses if the selected backend cannot enforce it; it is never silently ignored. |
--max-pids <COUNT> | integer | (none) | Ceiling on processes in the confined tree. Refuses if the backend cannot enforce it. |
--max-open-files <COUNT> | integer | (none) | Ceiling on simultaneously open file descriptors. Refuses if the backend cannot enforce it. |
--max-file-size-bytes <BYTES> | integer | (none) | Ceiling on the size of any single file the agent creates. Refuses if the backend cannot enforce it. |
--max-wall-clock-seconds <SECONDS> | integer | (none) | Wall-clock lifetime ceiling, enforced by aasm run’s own supervisor rather than by a backend, on a launch that establishes an isolation boundary. Termination is best-effort, and orphan-freedom is not guaranteed (ADR 0041). |
--max-cpu-seconds <SECONDS> | integer | (none) | Accumulated CPU-time ceiling. Optional: no backend enforces it as prevention in this version (ADR 0041). |
The --max-* ceilings only take effect on a launch that establishes an
isolation boundary. With the default --isolation none they are accepted and
not enforced: no backend is consulted, so nothing is refused either, and
--dry-run reports an empty requested capability set (measured: aasm run exec --no-proxy --isolation none --max-wall-clock-seconds 2 -- /bin/sleep 12 ran
for the full 12 seconds and exited 0). Where a boundary is established, the
ceilings are enforced per backend, not uniformly: the
aasm-macos-vm backend has no resource-ceiling mechanism, so a required
ceiling is refused at planning time (AAASM-6294), and Linux enforcement is
described in ADR 0041.
Isolation intent (--isolation)
--isolation states a backend-neutral isolation class the launch
requires (ADR 0035 §3) — never a backend or mechanism name. Three values:
| Value | Meaning |
|---|---|
none (default) | No execution-isolation boundary is established. This is the pre-existing behaviour every aasm run had before this flag existed, stated explicitly rather than left as an absence — the report can then say why a run has no boundary: nobody asked for one, versus one was asked for and could not be built. |
auto | Agent Assembly selects a backend by capability: it walks the compiled-in backends in a fixed order and picks the first one that can meet this launch’s lowered policy requirements, using the same negotiation every real launch goes through. It is not “isolate if convenient”: when no compiled-in backend can meet the requirements, auto refuses — naming every backend it considered and why — rather than running unconfined or silently picking a default. |
process | Confine the launch and its descendants within one host’s process model. |
The default is deliberate, not neutral. Turning isolation on by default would sandbox every existing user’s tool in a release they did not read the notes for, and a governed launch that suddenly cannot write to the operator’s own repository is worse than one that states plainly what it is not doing. Changing the default is a separate product decision, not a side effect of this flag existing.
auto and process both refuse rather than degrade silently. Neither
ever falls back to none. When the selected backend cannot be used on this
host, the launch is refused with the reason and a pointer to --isolation none as the explicit, deliberate way to launch unconfined:
error: refusing to launch: an execution-isolation boundary was requested and the `sandlock`
backend cannot be selected on this host — <reason>.
There is no fallback. A launch that asked for a boundary and quietly ran without one would
report as governed while being unconfined, which is the failure this mode exists to prevent.
Install the backend, or re-run with `--isolation none` to launch unconfined deliberately.
Under --dry-run the same condition does not stop the preview — it prints a
warning: pair to stderr and reports what a live launch would do, because a
preview exists precisely to answer “what would happen” from a machine that is
not yet fully set up.
See Security Model → Execution isolation for what each isolation class actually achieves — pre-effect denial versus observation — and the platform/backend support matrix.
Backend pinning (--isolation-backend)
--isolation-backend <ID> is not product vocabulary and not a policy
dimension. Policy describes required isolation properties; naming a
mechanism is a diagnostic escape hatch for reproducing a result on a specific
backend and for telling two backends apart in a bug report. Ordinary
operation should use --isolation alone.
--isolation-backendcombined with--isolation noneis refused as a contradiction: naming a backend for a launch that asked for no boundary would leave the operator believing one half of the request took effect.--isolation-backendalways wins overauto’s capability-based selection:--isolation auto --isolation-backend <ID>names that backend outright and reports its own refusal if it cannot meet the launch’s requirements — it does not fall through to a different backend automatic selection would have picked.- Naming a backend this build does not have is refused, naming the backends it
does have (
sandlock,aasm-nativeandaasm-macos-vm). - The exact string it accepts is an implementation fact recorded in the platform/backend support matrix — it is not repeated here because this page documents the CLI contract, and the matrix is the place that fact is allowed to change without this page changing.
Transactional workspace mode (--workspace-tx)
--workspace-tx stages a materialized copy of the base directory (--workdir, or
this shell’s own working directory), points the launched process at the staged copy
instead, and folds the exact change set back onto the base only if the launch exits
0. Any other exit — or an unobservable exit — discards the staged copy; the base is
never touched in that case. There is no separate commit mode: the child’s own exit
code is the entire disposition rule.
--workspace-tx-exclude <PATH>excludes a selector from the declared surface. It is not a glob — a path matches when it equals the selector or starts with<selector>/. An excluded path is absent from the staged workspace, not shared with the base — a build inside a transaction withtarget/excluded rebuilds from scratch. Exclusion applies to the copy-in only: a path the launched process creates inside an excluded selector is still present in the change set and still applied on commit.--workspace-tx-protect <PATH>marks a selector as protected: a change set touching it refuses to commit unless--workspace-tx-approveis also passed. The base is left untouched either way until that decision is made.- Every
--workspace-txrun prints aworkspace_tx.*block to stderr — the only surface this mode’s outcome reaches when no execution-isolation boundary ran (--isolation none, the default, writes no execution receipt). The block always includes aworkspace_tx.not_transactionalline naming what this mode does not cover: network calls, database writes, processes started, or a path outside the declared surface. This is about durability of the declared surface, not about confinement. - When
--isolationalso establishes an execution boundary, the execution receipt’sworkspacebinding carries the same facts — base/result/diff digests, added/modified/deleted counts, and the same non-coverage list — andaasm receipt verifyrefuses a receipt whose binding drops any part of that list.
An interrupted apply (the process dies while the change set is being folded
back onto the base) leaves an on-disk apply journal, but nothing in aasm
reads or reports it today, and no aasm workspace tx command exists to
inspect or clear it (AAASM-6291); recovering from that state is a manual
operation.
See ADR 0040 for the full design — alternatives considered (a git worktree, overlayfs, a VM disk snapshot), the fail-closed commit order, and the crash/cancel disposition.
Dry-run output
aasm run --dry-run <tool-or-exec-invocation> [options] prints the full
resolved plan and executes nothing — the same resolution code path a live
launch uses (ResolvedRunPlan::bind), so what you see is what a live run
would do, not a separately-built preview that can drift from it. The output
has one section per stage of the plan:
--- aasm run dry-run ---
agent_id: ...
agent_did: ...
trace_id: ...
session_id: ...
--- preview fidelity ---
...
--- protection ---
state: ...
detail: ...
--- policy ---
state: ...
source: ...
detail: ...
--- execution isolation ---
... (human-readable isolation report — see below)
--- execution isolation (machine-readable) ---
... (key=value lines, for scripting)
--- managed settings ---
...
--- launch command ---
working_dir: ...
...
--- environment ---
...
The --- execution isolation --- and --- execution isolation (machine-readable) ---
sections are printed unconditionally, on every launch, including
--isolation none. A section that appeared only once a backend was selected
would make “no boundary was established” look like a formatting quirk rather
than a fact about the run — so the report always states, per capability
domain, whether policy left the domain unset, whether the policy schema has
no way to express it at all, or what was actually planned. See
Execution isolation → Requested vs. achieved
for what the report’s fields mean, including the states that read like a
restriction but are not one (PolicyCannotExpress is never “no restriction
required”).
The machine-readable block is an explicit, versioned contract for scripting: a
--- execution isolation (machine-readable) --- header followed by key=value
lines, so a consumer can anchor on the header and read records until the first
line that is not one, without parsing prose.
Lease plan
After the environment, --dry-run prints a --- lease plan (PLAN: point-in-time, not authorization) --- section: one verdict line per tool the policy declares under
tools.<name>.capabilities, a tool <undeclared> line, and one action <TYPE> line for
each typed action type.
--- lease plan (PLAN: point-in-time, not authorization) ---
status: PLAN ONLY. ...
revocation: not evaluated; ...
enforcement: runtime-only. ...
issuance: leases from `authority.leases` only (never from tool declarations), ...
lease: filesystem_read scope=whole ttl=3600s (default)
tool calc [none]: PERMIT
tool reader [filesystem_read]: PERMIT
tool writer [filesystem_write]: DENY (tool "writer": no lease for `filesystem_write`)
summary: 1 of 3 tools declaring capabilities would be denied
note: a tool declared without `capabilities` is not listed above; ...
tool <undeclared>: DENY (...)
action LLM_CALL: DENY (...)
action FILE_OPERATION: DENY (...)
The verdicts are what a governed runtime would decide: a runtime enforces leases only
when started with AA_LEASE_POLICY (and a record from aasm lease init in
AA_LEASE_STATE); without that no lease gate runs and none of this is in force. They come
from the same decision function the lease gate uses
(ADR 0038 section 38),
not from a copy of its rules. Read them with these limits:
- It is a plan: leases are taken as issued at the moment of the preview, so a lease that has lapsed in a real run cannot appear.
- Revocation is not evaluated. It exists only in a running runtime’s lease state; the preview reads no state record.
- A tool declared without
capabilitiesis not listed; thetool <undeclared>line covers it, and asummary:line counts denials among the listed tools. - It is not authorization and enforces nothing. A
PERMITis not a grant; the runtime decides each call, and still passes a permitted call on to the gateway and local policy. - A DENY line does not change the exit code; a preview that shows refusals is a successful
preview. A policy that is not an enforced load prints
plan: not availablewith the reason and no verdicts.
Examples
Launch Claude Code under governance, with no isolation boundary (unchanged pre-existing behaviour):
$ aasm run claude
Preview a generic command’s launch plan without running it:
$ aasm run exec --dry-run -- python agent.py --task "summarize repo"
Launch a generic command with the strongest isolation class this build can provide, refusing rather than running unconfined if it cannot be provided:
$ aasm run exec --isolation auto -- python agent.py
Require process-level isolation explicitly, and preview it first:
$ aasm run exec --isolation process --dry-run -- python agent.py
$ aasm run exec --isolation process -- python agent.py
For a full governed walkthrough with the isolation report explained line by line, see Execution isolation → Quickstart.
Last updated: 2026-11-01 by Chisanan232