Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 run is one of three command groups gated behind the devtool region in aa-cli/src/commands/mod.rs (the others are aasm tools and aasm integrations). .ci/strip-for-publish.sh removes that region from the crates.io publish only — cargo install aasm does not have aasm run. A source build (cargo build -p aa-cli), the GitHub Release tarballs, the curl installer, 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 with aasm gateway start, or point AA_GATEWAY_ENDPOINT at the gRPC endpoint. A proxied launch (the default) also needs the aa-proxy binary beside aasm, on PATH or 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 ids aasm integrations list prints (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. exec is 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, and aasm run cannot detect whether the program is installed before attempting to launch it — the launch attempt itself is the test, and exec’s own No such file or directory is the answer if it is not.

Options

FlagTypeDefaultDescription
--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-proxyflagoffLaunch 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-txflagoffRun 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-approveflagoffPre-authorize a commit that touches a protected selector. Requires --workspace-tx.
--dry-runflagoffShow the resolved launch — identity, policy, proxy, managed settings, launch command, environment, and the execution-isolation report — without executing anything.
--enforcement-mode <MODE>enforce | observe | disabledenforceEnforcement 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.
--observeflagoffShorthand for --enforcement-mode observe. Mutually exclusive with --enforcement-mode.
--isolation <INTENT>none | auto | processnoneHow 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:

ValueMeaning
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.
autoAgent 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.
processConfine 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-backend combined with --isolation none is 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-backend always wins over auto’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-native and aasm-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 with target/ 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-approve is also passed. The base is left untouched either way until that decision is made.
  • Every --workspace-tx run prints a workspace_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 a workspace_tx.not_transactional line 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 --isolation also establishes an execution boundary, the execution receipt’s workspace binding carries the same facts — base/result/diff digests, added/modified/deleted counts, and the same non-coverage list — and aasm receipt verify refuses 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 capabilities is not listed; the tool <undeclared> line covers it, and a summary: line counts denials among the listed tools.
  • It is not authorization and enforces nothing. A PERMIT is 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 available with 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