ADR 0035: Agent Execution Isolation & Pluggable Enforcement Backends
Status: Proposed Date: 2026-08 Ticket: AAASM-5703 (Epic AAASM-5702)
This ADR defines how Agent Assembly turns a governed launch into an evidence-backed
execution boundary for an untrusted AI-agent process tree. It makes aasm run the
canonical managed-execution front door, separates whole-agent native isolation from the
existing WASM/WASI tool sandbox, and establishes a backend-neutral contract so one AASM
policy can be lowered into different enforcement substrates without making any substrate
the policy model.
It complements and does not supersede ADR 0033. ADR 0033 remains the canonical source for the six architectural elements and for truthful claims about decision timing and evidence. This ADR occupies the execution-specific part of that model:
- E2 · Managed Execution Checkpoints —
aasm runresolves the launch and its required capabilities before the untrusted process starts. - E4 · Platform-Specific Host-Level Interception Adapters — a concrete isolation backend realizes the OS/runtime-specific restrictions it actually supports.
- E5 · Credential / Capability Boundary — the launched process receives only the ambient authority and delegated capabilities its execution plan permits.
- E6 · Evidence & Protection-State Pipeline — the product reports requested, planned, achieved and unmeasured controls separately; backend availability is never evidence of enforcement by itself.
Cross-reference — AAASM-6160 (2026-09): ADR 0038 extends this ADR’s
ControlRequirement/negotiatecontract with a question this ADR does not ask — was a run ever explicitly authorized to touch a domain at all, independent of whether the selected backend can mechanically enforce it. It cross-references rather than amends: nothing decided here is reversed.
The existing aa-sandbox remains the WebAssembly/WASI
sandbox for individual WASM-marked tool executions. This ADR defines a different
boundary: confinement and supervision of the agent’s native process and descendants.
The two mechanisms may be composed but must never share a name or claim merely because
both use the word “sandbox”.
Amendment — AAASM-5751 (2026-08-14): filesystem path scope becomes policy-expressible
Scope of this amendment: one decision below gains a concrete policy node, and one recorded fact about the schema is corrected. No decision is reversed or superseded.
Why an amendment rather than a new ADR
The principle was already decided here. Backend capability examples states that the capability vocabulary must distinguish “Filesystem | read/write/create/delete scope”, and decision 3 states that policy describes required isolation properties portable across backends. Adding a filesystem path-scope node executes a decision this ADR already made; recording it as a new ADR would fork the record for a question that is already answered.
It is not “no ADR action” either. Until AAASM-5751, no policy node could name a path, so
aa_isolation::lowering reported both filesystem domains at
ScopeGranularity::WholeDomainOnly and its module documentation said the schema carried
“no path scope”. Shipping the node makes that recorded text false, and a decision record
that has gone stale is worse than one that never existed.
It amends this ADR and not ADR 0033: 0033 stays the canonical source for the §6 claim vocabulary, which this amendment does not touch, and it carries a separate open amendment under AAASM-5654. Two concurrent edits to 0033 would conflict for no benefit — nothing here needs a §6 term that does not already exist.
What is now expressible
A filesystem: section on the policy spec, with an operator-authored allow-list of
absolute path prefixes per verb:
spec:
filesystem:
read:
allow: ["/workspace", "/usr/share/dict"]
write:
allow: ["/workspace/build"]
Normative reading of the three states, which must not collapse into one:
| Authored form | Meaning | Lowers to |
|---|---|---|
| Section absent | The operator stated nothing about paths. Not a grant. | DomainCoverage::NotStated; no requirement from this node |
allow: present and non-empty | Only these subtrees may be reached; everything else must be prevented | Lowered { granularity: Enumerated }, RequirementScope::Selectors |
allow: present and empty | A restriction is in force and permits nothing — the most restrictive posture | Lowered { granularity: WholeDomainOnly }, RequirementScope::Whole |
An empty allow: deliberately reads as deny-all rather than as “no restriction”. That
matches aa_policy::check_network_egress’s live treatment of an empty egress allowlist
and refuses to repeat the canonical NetworkPolicy doc-comment’s opposite reading, which
is the fail-open shape AAASM-3728/AAASM-3730 already had to close once.
A whole-domain capability denial (capabilities.deny: [file_read]) outranks the path
allow-list, exactly as a network_outbound denial outranks network.allowlist: lowering
the narrower node under a broader denial would emit a requirement permitting paths the
same document forbids.
Across a policy cascade the merge is most-restrictive-wins: the effective permitted set is the intersection of every tier that declared the node, computed over path subtrees rather than over strings, so a narrower tier can only ever shrink a broader one. A cascade carrying no documents at all is a refusal, not an empty intersection — ADR 0024 §6(2) already settled that an absent cascade is Unconfigured, never permission.
What deliberately remains unrepresentable
NameResolution, Ipc, Credential and Resource keep reporting
DomainCoverage::PolicyCannotExpress. This is recorded as accepted, measured risk,
not as a set of domains nothing could use: measured against the Sandlock backend’s
capability.rs, Ipc and Credential are Mediation::Enforce with
SupportLevel::Partial and Resource is Mediation::Enforce, so for those three the
backend can enforce something an operator has no way to ask for. They stay unexpressed
because no acceptance criterion under Epic AAASM-5702 requires operator-authored control
of them, and ambient credential authority is handled procedurally by AAASM-5709 rather
than as a policy node. Speculative schema is a public contract that cannot be withdrawn;
a named gap can be closed at any time.
Two sources of paths that already exist in the codebase were examined and rejected as policy sources:
SENSITIVE_MUTATION_DENY_DEFAULTS(aa-security/src/policy/ebpf.rs) is the eBPF layer’s own implementation floor, not something an operator wrote. Consuming it would make one backend’s lowering the policy semantics, which decision 2 forbids.tools[].requires_approval_ifpath prefixes are operator-authored, but they mean approval, not denial. A confined process has no approval channel, so lowering them to a prevention requirement would silently convert “ask a human” into a hard block. Stronger than what the operator wrote is still wrong.
Consequence for existing text
Where this ADR and code derived from it describe filesystem policy as a whole-domain boolean, that description is superseded for path scope only. The remaining filesystem gaps this ADR names are unchanged and still reported per domain: create/rename/write and delete are not separable verbs, and the backend’s own sensitive-path defaults are still not re-derived as policy.
Amendment — AAASM-5801 (2026-08-20): the AASM-native backend becomes the second IsolationBackend implementor
Scope of this amendment: it names one concrete backend, records the Linux primitives and process shape it uses and why, and records what it deliberately does not attempt. It reverses no prior decision.
Why an amendment rather than a new ADR
Decision 8 already named Sandlock a candidate, not the only implementor, and said AASM “may replace or supplement it with a native Linux backend after compatibility, performance, security surface, release cadence and feature requirements are measured.” Reconsideration trigger 2 anticipates exactly this: “a production native-Linux backend replaces the initial third-party substrate and exposes a materially different trust or descendant model.” This amendment is that trigger firing — it exercises a choice the ADR already reserved rather than making a new one, so it belongs here rather than in a competing record.
It is not “no ADR action”: until this amendment nothing recorded which Linux kernel primitives the second implementor uses, why its launcher is shaped the way it is, or how it resolves the one thing Sandlock’s own mechanism could not express (below). Those are durable facts a later implementer needs and Jira comments do not preserve.
The backend: one IsolationBackend, composed, not several
The native backend is one implementor of IsolationBackend,
composed from Landlock (filesystem), seccomp-bpf (syscalls) and the existing
backend-neutral descendant/evidence machinery already built for Sandlock under
AAASM-5709/AAASM-5710 (ambient-descriptor sealing, environment scoping, evidence
kinds). It is not a family of narrower single-purpose backends, and it is not a
reimplementation of Landlock or seccomp-bpf themselves — both remain kernel
mechanisms this backend configures, not code this backend contains.
aa_isolation::backend::IsolationBackend (aa-isolation/src/backend.rs) needs no
change to accept it. The trait’s five execution stages plus the AAASM-5711
supervision pair (identity, capabilities, plan, prepare, spawn,
wait_for_exit, terminate, evidence) are already backend-neutral by construction:
PreparedExecution and ExecutionHandle are opaque tokens precisely so a backend
whose unit of confinement is a Linux process, rather than an external supervisor
binary, has nowhere it needs to leak that shape into the contract. This was verified
against the trait as it stands in this branch, not assumed from the trait’s stated
design intent.
Linux primitives: Landlock + seccomp-bpf, and why this pair
Landlock restricts filesystem access (open, execute, and — from the ABI version that supports it — network bind/connect) by attaching a ruleset the calling process cannot widen afterward. seccomp-bpf restricts which syscalls a process may issue at all. Together they cover the two domains decision 6 requires an implementor to answer for — filesystem and process/syscall behavior — using mechanisms that are already in the mainline kernel, require no new kernel module, and (per the commercial constraint) are addressable through permissively-licensed Rust binding crates rather than a redistributed third-party binary. Network-destination restriction is explicitly not attempted by this pair; see “Kernel-level network enforcement” below.
Launcher shape: a small auditable binary, not a pre_exec closure
The backend applies its Landlock ruleset and seccomp filter to itself, synchronously,
inside a small, dedicated, auditable Linux launcher binary — the same CLI-invocation
shape aa-isolation-sandlock already uses (sandlock-style: the supervisor execs a
purpose-built binary that installs the boundary and then execves the target program as
its own final act, rather than the supervisor process installing the boundary on itself).
This is a direct instance of decision 5:
“Where process-level sandbox initialization requires post-fork/pre-exec work,
implementation should prefer a deliberately small and auditable launcher/helper boundary.
Complex Landlock/seccomp/namespace setup must not be casually accumulated in an async
runtime’s pre_exec callback, where post-fork restrictions make ordinary allocation, locks
and library behavior unsafe or difficult to audit.” Landlock ruleset installation and
seccomp filter loading are exactly that complex post-fork setup, and aasm run’s
supervisor is a Tokio async runtime — the launcher binary is how this backend keeps that
setup out of a pre_exec closure rather than an alternative to the ADR’s existing rule.
Seccomp model: allowlist, and why it is not what Sandlock could express
The seccomp filter is a default-deny allowlist: SECCOMP_RET_KILL or
SECCOMP_RET_ERRNO for anything not named, SECCOMP_RET_ALLOW for each syscall the
resolved EnforcementPlan permits. This matches AASM’s own policy shape — a policy names
what is permitted, per decision 3 — where
Sandlock’s own mechanism could not follow it: Sandlock’s syscall-filtering surface takes a
denied-syscall list, and the complement of an arbitrary permitted set is unbounded, so
no finite denied list expresses “permit only these.” That mismatch, not an implementation
gap, is why AAASM-5753 (syscall-level enforcement in Sandlock) was deferred rather than
shipped as a partial feature — deferring a requirement the mechanism structurally cannot
satisfy is the correct call recorded here, not left implicit in a closed ticket.
Amendment — AAASM-5803 (2026-08-20): the installed filter is not exactly the policy
allowlist. It is policy names ∪ STARTUP_BASELINE, a fixed set of loader/exec-chain
syscalls (execve, clone/clone3/wait4, and a handful of glibc dynamic-linker calls
with no name in the policy vocabulary) permitted regardless of what policy stated. A seccomp
filter is inherited across execve, not reset by it — the launcher installs the filter on
itself before execve-ing the confined program, so the launcher’s own execve and the
dynamic loader that runs before the program’s first instruction both execute inside the
filter this backend just installed. aa_security::policy::syscall::Syscall’s 15-name
vocabulary has no execve in it, so a filter built from policy alone would kill the launcher
on every single launch. STARTUP_BASELINE is disjoint from the policy vocabulary by a
tested invariant (aa-isolation-native/src/seccomp.rs), so a policy author’s own allowlist
selection is never silently widened by it — the practical edge is the reverse: a policy that
omits a loader-critical call the vocabulary can name (read, openat, mmap, and ten
others) gets its confined program killed during dynamic linking, before its own code runs.
This is documented as a SupportLevel::Partial limitation on the syscall capability report,
not folded into “the filter enforces the policy allowlist” language anywhere in code or docs.
/proc scoping: how AAASM-5709’s environment grant becomes an enforced boundary
Landlock’s filesystem ruleset will scope /proc read access: another process’s
/proc/<pid>/environ (and equivalent per-PID files) is outside the confined process’s
allowed read set, while /proc/self and its own descendants remain reachable, because the
confined process still needs to read its own process state. Before this, AAASM-5709’s
environment-based credential scoping controlled what a child’s own environment contained
but nothing stopped that child from reading a sibling or ancestor process’s /proc/<pid>/environ
on a shared host. Landlock’s /proc scope closes that gap, which is how this Epic’s
filesystem work closes AAASM-5785 and AAASM-5786 — they are closed by this backend’s
filesystem primitive, not by separate work targeting those tickets directly.
Kernel/ABI floor: a measurement policy, not a number
This amendment does not assume a minimum kernel version or Landlock ABI number. It records
the policy: the floor will be measured against the actual Landlock ABI version the
implementation requires for the path-scope granularity the backend claims (Landlock’s ABI
versions add capability incrementally — network restriction, for instance, arrives only at
ABI v4), and reported truthfully once measured, including degraded or unsupported behavior
on a host below that floor — via CapabilitySet/EnforcementEvidence exactly as
decision 4 and
ADR 0033 §6
already require of every backend. This is the same discipline the existing “kernel floor”
(S2) dimension of the AAASM-5713 benchmark methodology already applies elsewhere in this
Epic — a measured floor per host, not a guessed one recorded now and corrected later.
Alternatives considered and rejected for this delivery
- Namespaces (mount/network/PID). No accepted requirement under this Epic’s acceptance criteria demands namespace isolation, and adopting them is a materially larger commitment — namespace setup, teardown and descendant lifecycle all change shape — than the measured problem (filesystem and syscall scoping) requires. Deferred, not ruled out for a later backend generation.
- cgroups v2 / resource ceilings. Nothing in the AAASM-5713 benchmark data or this Epic’s acceptance criteria requires CPU/memory/PID ceilings yet. Deferred until a measured requirement exists.
- Kernel-level network enforcement. The existing AASM proxy layer (
aa-proxy) remains the network-mediation mechanism for this delivery. Landlock’s own TCP bind/connect restriction (available from ABI v4) is port-based; it cannot express AASM’s IP/host allowlist semantics, so it would not replace the proxy layer even where available — it would only add a second, differently-shaped network control. Not attempted here.Cross-reference — AAASM-6163 (2026-09): ADR 0038’s AAASM-6163 amendment adds a contract/truthful-report pair over this same
aa-proxymechanism — a launch’s requirement of brokered egress, checked against what the proxy layer truthfully provides — not a second network control. The mechanism named here is unchanged. - A stronger per-action prevention evidence claim than Sandlock already makes.
EnforcementEvidence::supports_prevention_claimstaysfalsefor this backend’s first version, matching Sandlock’s own honest answer: the kernel delivers a seccomp/Landlock denial to the confined process, and nothing in this delivery gives the supervisor a per-decision record of it. A seccompSECCOMP_RET_USER_NOTIFdecision channel could earn a stronger claim later — the supervisor would then receive a synchronous notification per filtered syscall rather than only configuring a filter the kernel enforces unobserved — but that is explicitly deferred, not committed to here.
Compatibility with Sandlock
Sandlock is not deprecated, removed, or implied to be superseded by this amendment. It
remains a supported, available IsolationBackend. Which of the two a deployment uses by
default is an evidence-based decision to be made later, under AAASM-5805, once both
backends have comparable measured evidence — it is not pre-decided by naming the native
backend here.
Licensing
Unlike Sandlock, this backend redistributes no third-party binary — there is no external
sandlock-equivalent executable to carry provenance and SBOM data for. Its licensing
surface is the Rust dependency crates providing the Landlock and seccomp-bpf bindings,
each of which decision 11
already requires to be verified permissively licensed (Apache-2.0/MIT/BSD) and recorded
with source/version provenance before it is pinned. This amendment does not pin a
specific crate or version; it records that the same pre-pin verification decision 11
already requires applies here, with no third-party-binary case added on top of it.
No new backend-specific vocabulary in user-facing intent
This amendment introduces no exception to the rule decision 3
already applies to “sandlock”: “Landlock” and “seccomp” name mechanisms, not --isolation
CLI intent. They appear only where this ADR and future compatibility-matrix,
troubleshooting and licensing documentation state implementation facts, never as a
value a policy or CLI invocation authors directly.
Consequence for existing text
Where decision 8 described Sandlock as “the first candidate backend” for Linux process isolation, that remains true and is not superseded — this amendment adds a second implementor alongside it, per that same decision’s own anticipation, and changes no other recorded text in this ADR.
Amendment — AAASM-5808 (2026-08-21): --isolation auto selects by capability, not by a fixed default
Scope of this amendment: it names the algorithm --isolation auto uses now that more
than one IsolationBackend implementor exists, and records why eligibility is decided by
the same plan()/negotiate() machinery a real launch uses rather than a hand-written
comparison. It reverses no prior decision.
Update: at the time this amendment was written, exactly two
IsolationBackendimplementors existed (sandlock,aasm-native), and the candidate list below named only those two.aasm-macos-vm(AAASM-5811) was added afterward as a thirdIsolationBackendimplementor and a third entry inaa-cli/src/commands/run.rs’sCANDIDATESlist — the algorithm and its rationale below are unchanged, only the candidate list is longer. See the execution-isolation docs for the current three-entry list.
Why an amendment rather than a new ADR
Decision 8
already reserved that AASM “may replace or supplement [Sandlock] with a native Linux
backend,” and the AAASM-5801 amendment exercised that reservation by adding a second
IsolationBackend implementor. Neither recorded what --isolation auto does once two
implementors exist. Before this amendment, auto resolved to the same fixed default
--isolation process already used — whichever id --isolation-backend named, or
sandlock otherwise — because, per the pre-AAASM-5801 IsolationIntent::Auto doc
comment, “today exactly one class exists.” AAASM-5805’s benchmark data established that
Sandlock’s and the native backend’s capability gaps are complementary, not nested:
Sandlock covers every domain this ADR’s capability model defines except Syscall; the
native backend covers exactly FilesystemRead, FilesystemWrite and Syscall. A fixed
default cannot be capability-correct for every policy under that shape — a policy
requiring Syscall enforcement cannot be met by the fixed default, and a policy requiring
NetworkEgress enforcement cannot be met by the native backend — so this amendment
decides what auto now means, rather than leaving two backends behind one hardcoded
choice that is wrong for some policies by construction.
The selection algorithm: a fixed, ordered eligibility walk over plan() verdicts
--isolation auto walks a fixed, ordered candidate list — [sandlock, aasm-native, aasm-macos-vm], Sandlock first, then AASM-native, then the macOS VM backend — and selects
the first candidate for which backend.plan(probe_spec) returns Ok, where probe_spec
is an ExecutionSpec carrying nothing but the launch’s lowered requirements. The walk is
lazy: once a candidate is eligible, no later candidate is even discovered or probed. When
no candidate is eligible, the launch refuses, naming every candidate considered and why it
was rejected — there is no fallback to a default backend or to an unconfined launch,
matching
decision 4’s
existing “refuse before spawn, never degrade to unconfined” rule.
Eligibility is decided by the same narrow_for/negotiate machinery a real launch’s own
negotiation uses — not by a hand-written comparison of which domains each backend is known
to cover. That is a correctness decision, not a style preference: narrow_for and
negotiate read only ExecutionSpec::requirements — never the program, args, identity,
working directory or credentials — so a probe spec carrying only the lowered requirement
set reaches the identical verdict a real launch’s own spec would against the same backend.
A hand-written domain-subset comparison would have to be kept in step with both backends’
actual negotiation behavior by hand and could silently drift from it; reusing the
negotiation path the rest of this ADR already requires every backend to answer correctly
cannot drift from itself.
What --isolation auto does not do
Naming a backend directly — --isolation-backend, or --isolation process’s existing
hardcoded default — is unaffected and unchanged by this amendment: an operator who names a
backend always gets that backend, or its own refusal, never automatic selection’s
substitute. Automatic selection also does not run when no policy lowered to any
requirement at all; that remains the existing NoRequirementsLowered refusal this ADR
already records, not a new refusal this amendment introduces.
Consequence for existing text
Where the IsolationIntent::Auto documentation previously said this intent “resolves to
Process” because “today exactly one class exists,” that statement described the period
before AAASM-5801 added a second backend; this amendment supersedes it with the algorithm
above. No other recorded text in this ADR changes.
Amendment — AAASM-6167 (2026-09-25): selection generalizes from backend ids to a RuntimeRequirements property set
Scope of this amendment: it moves the AAASM-5808 eligibility walk from a CLI-specific,
name-branching implementation into a backend-neutral mechanism aa-isolation owns, and adds
an independent evidence-quality floor on top of it. It reverses no prior decision: the
walk’s algorithm — decide eligibility per candidate via plan()/negotiate(), select the
first eligible candidate in a fixed order, refuse naming every candidate and why — is
unchanged.
Why an amendment rather than a new ADR
The AAASM-5808 amendment recorded the selection algorithm while it lived in
aa-cli/src/commands/run.rs::auto_select, branching on each backend’s crate-level
BACKEND_ID to construct a concrete SelectedBackend. That was adequate while aasm auto
was the only caller and the only requirement vocabulary was “the launch’s lowered policy
requirements.” Runtime 2.0 (Epic AAASM-6159) needs the same decision reachable from a
policy or operator statement of runtime properties — not backend names — and needs new
backend candidates (AAASM-6168/6169/6170’s gVisor, Firecracker and Apple Containerization
spikes) addable without a new match arm in aa-cli. Both are the same generalization this
ADR already anticipated: decision 8
and the commercial/performance constraints above already frame backends as pluggable and
their properties as measured, not named.
What this amendment actually ships, and what remains a follow-up
aa-isolation::planner::select is a new, independent implementation of the walk this ADR
already names, living in the crate that owns negotiate() rather than in aa-cli: it
takes a aa_isolation::RuntimeRequirements (a versioned, backend-neutral property set —
confinement domains, resource ceilings, an evidence/attestation floor, an allowed
PlatformBoundary set) and a caller-built, declared-order list of aa_isolation::Candidate { identity, capabilities } — data, not dyn IsolationBackend objects, since aa-isolation
cannot depend on any backend crate. Eligibility for each candidate is decided by exactly one
call into negotiate() — the same “single decision point” this ADR already establishes —
so no per-domain refusal rule changes, proven by a mutation test and by a positive control
against aa-isolation-macos-vm’s own real capability-reporting logic (not a MockBackend).
aa-cli/src/commands/run.rs::auto_select now delegates its eligibility decision to
aa-isolation::planner::evaluate_candidate (AAASM-6167’s own follow-through, landed in the
same ticket this amendment records). It calls evaluate_candidate, not select: select
forces eager discovery of every candidate up front, which would break the lazy discovery
aa-cli/tests/run_isolation.rs and aa-isolation/tests/negotiation.rs already pin (the
second candidate must never be probed when the first is eligible), and select’s generic
PlanRefusal::to_string() detail would replace the CLI’s own, more actionable
describe_refusal wording. The loop, the availability pre-check and the ConsideredBackend
construction are otherwise unchanged — only the eligibility oracle and the capabilities fed
to it moved. The construction site still names concrete backend types, since Rust has no
plugin loader; see “Consequence for existing text” below for why that construction site must
narrow those capabilities before handing them to evaluate_candidate.
The evidence-minimum bar: a second, independent gate
negotiate()’s prevention check reads exactly three axes on a CapabilityReport —
Mediation, DecisionTiming, Synchrony — established by
decision 4. It has
never read FailurePosture or SupportLevel, and this amendment does not add either read
to it. Instead, RuntimeRequirements::EvidenceMinimum states an independent floor on those
two axes, and aa-isolation::planner checks it directly against a candidate’s
BackendCapabilities after negotiate() already accepted the candidate — never instead
of, and never in a way that can turn an unmet negotiate() requirement into an accepted
one. A candidate that is Enforce/Pre/Sync on a domain (and therefore
can_prevent()s it) can still be FailurePosture::FailOpenSilent, or
SupportLevel::Partial with stated gaps, on that same domain; this amendment’s floor is how
a caller excludes such a candidate anyway. An unreported domain — report_for returning
None — fails any stated evidence minimum for the same “unknown is not interpreted as
supported” reason a missing capability report already refuses a negotiate() prevention
requirement.
Tie-break remains declared order, not a performance score
The ticket’s design language (“performance among security-equivalent candidates”) does not
change the mechanism here: this workspace has no measured performance data for the new
candidate backends the AAASM-6168/6169/6170 spikes describe — those PRs are unmerged
capability-and-tradeoff writeups, not shipped IsolationBackend implementors with real
benchmark numbers — so encoding a numeric ranking now would be a policy input this ADR
cannot yet stand behind. aa-isolation::planner::select therefore keeps exactly the
AAASM-5808 tie-break: the first candidate in the caller’s declared order that is eligible
wins. aa-cli’s own CANDIDATES list, unchanged by this amendment, produces the identical
selection backend.plan() would for every requirement set expressible today — but only
because the construction site now supplies evaluate_candidate the same narrowed
capabilities plan() computes internally via each backend’s own capability::narrow_for
(see “Consequence for existing text” below). Feeding evaluate_candidate a candidate’s raw,
unnarrowed capabilities does not reach this parity — a domain narrow_for would have made
ineligible (e.g. a Resource ceiling lower::resource cannot express) can still look
eligible on the raw report, which is exactly the regression narrowing at the construction
site exists to prevent.
Named runtime classes remain out of scope
The ticket also raised whether a named, user-facing “runtime class” (a documented bundle of
properties) should exist alongside RuntimeRequirements. None is introduced by this
amendment. RuntimeRequirements is the property mechanism only; a class built from it,
were one added later, must satisfy this ADR’s existing “no new backend-specific vocabulary
in user-facing intent” constraint and must never document a stronger guarantee than the
explicit properties it bundles — deferred entirely rather than shipped provisionally.
Consequence for existing text
The AAASM-5808 amendment’s description of the algorithm as living in
aa-cli/src/commands/run.rs::auto_select still describes where the eligibility decision is
made today. This amendment does not record the two implementations as an independent
implementation of the identical algorithm: auto_select’s own backend.plan() narrows a
candidate’s capabilities via each backend’s capability::narrow_for before calling
negotiate(), while aa-isolation::planner::evaluate_candidate negotiates whatever
capabilities the caller’s Candidate carries, narrowed or not. The two paths agree only when
the construction site performs that narrowing itself before building the Candidate — they
were not, and are not by construction, the “identical algorithm” independently reached.
auto_select now delegates its own eligibility decision to evaluate_candidate directly
(never to select, which would force eager discovery of every candidate and replace the
CLI’s own describe_refusal wording with PlanRefusal::to_string()), feeding it capabilities
narrowed for the probe spec at the construction site via a new SelectedBackend::capabilities_for.
That is what makes auto_select’s selection and backend.plan()’s selection agree for every
requirement set expressible today — not an incidental property of the two algorithms being
written the same way twice. A debug_assert_eq! inside auto_select’s loop checks this per
candidate in debug builds, but only for a candidate that passes the availability pre-check
first — on a host where every candidate reports Unavailable (true of every macOS
development host today, since sandlock and the native backend’s Linux-only mechanisms and
the still-unavailable macOS VM backend are all unavailable there) the assertion never
executes for any candidate, so it pins this claim only on a provisioned Linux CI runner, not
on every host that builds this code. No other recorded text in this ADR changes.
Amendment — AAASM-6166 (2026-09-27): execution receipts bind policy, spec, backend and evidence into one durable, sealed artifact
Scope of this amendment: it adds a versioned, local, sealed record of one run’s isolation
posture — a durable projection of exactly the facts §10 already requires a report to carry,
written once at the end of a confined launch. It reverses no prior decision, adds no new
claim vocabulary, and changes no IsolationReport/EnforcementEvidence type. Parent Epic
AAASM-6159 (“Agent Execution Runtime 2.0”).
Why this belongs here rather than in a new ADR
§10 of this ADR (“Evidence is first-class output of execution”) already states the property
this amendment implements durably: a run’s evidence must be joinable back to the plan it
executed, and IsolationReport/EnforcementEvidence are the in-memory types that carry it.
report.rs’s own module documentation already cites this ADR as its authority. An execution
receipt is that same joined fact set, written to disk once, at the one place — the end of
run_confined — where the plan, the achieved boundary and the runtime evidence are all in
scope simultaneously. Nothing about the receipt’s content is new; what is new is that it
persists past process exit, and persisting it durably is what “signed execution receipts” in
the parent Epic’s own Goal line asks for.
The seal is a content digest, never a signature — and this is final for this ticket
The Epic’s Goal line says “cryptographically binds… signed execution receipts.” This
amendment implements the seal as sha256 over a canonical JSON rendering of the receipt’s
body — a content digest, not a signature and not a MAC — and the word “signed” appears
nowhere in the implementation’s types, CLI output, or documentation except in a sentence
stating what the seal is not. This mirrors aa_core::integration::store’s own receipts,
whose module documentation already states the identical position for a different artifact:
“Integrity is corruption detection, not tamper prevention… not a MAC.”
The reasoning is structural, not a matter of preference: a real signature needs a private key
the writer holds and a verifier that does not share the writer’s host UID — otherwise an
attacker capable of rewriting the receipt is, by construction, also capable of reading
whatever key would have signed it, and a signature would add nothing an unkeyed digest does
not already provide. aasm run writes its own receipts on the same host, under the same UID,
that would hold any such key. The only place a signature buys something real is a SaaS
ingestion path — a separate host holding a key the local writer never sees — which is
explicitly out of scope for this local-first design. An HMAC was considered for the same
reason and rejected on the same ground: the key would sit next to the receipt it protects.
ReceiptSealKind is #[non_exhaustive] with exactly one variant
(ContentDigestSha256) so that a future kind — were a SaaS verification path ever built — is
additive to the schema rather than a redesign, without this ticket deciding what that kind
would be.
What a holding seal establishes, and what it does not
A holding seal establishes that the receipt’s body has not changed since it was written, and
that the canonical form the digest was computed over is the one this build reads. It does
not establish origin (see above), the verified identity of the agent the run is
attributed to (asserted_identity is named that way deliberately —
aa_isolation::spec::IdentityRef already documents that a caller-supplied identity reference
is asserted, never authenticated, by anything in this crate, and nothing in this amendment
closes that residual), or that any property the receipt records as unmeasured actually held.
One boundary is worth stating precisely rather than leaving implicit: the seal digests the
receipt’s body only. The envelope’s schema identifier and the seal’s own metadata
(sealed_by, sealed_at_unix_secs, canonical_form) sit outside the digest and are freely
mutable without producing a mismatch. This is benign under the implementation shipped here —
mutating the schema string produces an independent UnknownSchema validation defect, and only
one canonical form exists today — but it is a real property of the design, not a theoretical
one, and aasm receipt verify closes the practical gap by always running both the seal check
and the validation rules below, reporting failure if either one fails. A pinned test
(mutating_the_envelope_schema_holds_the_seal_but_is_still_a_defect) asserts this boundary as
a fact rather than leaving it as a claim in this document alone.
Missing evidence cannot validate as a stronger state — a checked predicate, not a convention
The parent Epic’s Design Constraints already state the target property: “Receipt signature
proves integrity/origin, not properties that were never measured.” This amendment implements
it as eight checked rules (R0–R7, aa-cli/src/commands/execution_receipt/validate.rs) run at
construction and at verification time — the latter is what makes the property hold against
a hand-edited receipt, not merely against aasm run’s own honest construction path. In
summary: every one of the ten CapabilityDomains must be present (R0); a claim that asserts
coverage needs at least runtime evidence behind it, never setup-only (R1); the one prevention
term additionally needs a Decision-grade record, and the receipt’s own prevention_supported
flag must agree with that grade rather than being asserted independently (R2/R2b); a domain
carrying a degraded condition cannot also carry a prevention claim (R3); no domain may claim
coverage when no execution backend ran at all (R4); a state that itself asserts nothing
(unmeasured/unsupported) cannot carry a coverage-asserting claim (R5); every withheld field
must be recorded, never silently dropped (R6); and the recorded timeline must not be inverted
(R7). aasm receipt verify reports the seal and these rules as two independent results,
never collapsed into one pass/fail — a freshly, correctly resealed receipt that overclaims
still fails on the rules alone, which is the case the parent Epic’s Design Constraints are
actually worried about.
No new ClaimTerm, EvidenceKind, AttestationBasis, or CapabilityDomain variant was
added to implement this — the existing ADR 0033 §6 vocabulary and report.rs’s
EvidenceBasis/ControlState ladders already say everything R0–R7 need to check.
scripts/check_claim_vocabulary.py passes on the new module and documentation unchanged.
One narrow addition was necessary: host::FactBasis, for a host or build fact
(architecture, kernel release) that carries no protection claim at all — kernel release is not
a control’s state, so grading it with aa_core::attestation::AttestationBasis (which exists
specifically to grade protection components toward claim_ceiling) would misfit a
safety-critical vocabulary onto a fact that has nothing to do with enforcement. FactBasis’s
unmeasured arm reuses aa_isolation::UnmeasuredReason’s token vocabulary rather than
duplicating it a third time.
Redaction is structural, and the credential screen is the second line
No field of a receipt’s body holds an argument value, an environment variable’s value, a
lease’s basis reason, or a scope selector — those travel as digests (argv_digest,
working_dir_digest, a lease’s own redaction-safe projection digest) and counts
(arg_count, scope_selector_count). This is the primary guarantee, and it holds regardless
of the credential screen below: even if contains_credential_material matched nothing, the
value was never in the digest input or the stored field to begin with. The few fields that do
carry operator-visible free text (a policy source path, an Inconclusive detail sentence, a
degraded-condition detail) are screened through
aa_core::integration::fingerprint::contains_credential_material and withheld — recorded as
withheld in withheld_fields, never partially redacted, never silently dropped — on a match.
That screen is a floor, not a proof, exactly as aa-core’s own documentation for it already
states; this amendment does not claim otherwise.
Why aa-isolation gains no serde feature, and the resulting residual
aa-cli/Cargo.toml keeps aa-isolation dependency free of the serde feature. aa-isolation’s
own lib.rs documents enabling that feature as “a decision to expose a wire contract” the
crate is not ready to make while several sibling tickets are still reshaping these types, and
ExecutionSpec::working_dir is a PathBuf whose default Serialize impl fails on non-UTF-8
content — unlike the rest of a launch’s argv, which is already checked for UTF-8 at the CLI
boundary, nothing upstream of this amendment checks the working directory the same way. The
receipt module therefore projects each real aa-isolation type into a private, redaction-safe
mirror struct (SpecProjection, LeaseProjection) before serializing anything, computing the
working-directory digest from the path’s raw bytes rather than through Serialize at all.
The residual this does not close, stated plainly rather than hidden behind the mirror-struct
technique: a field added to ExecutionSpec or CapabilityLease later is not part of a
receipt’s digest until someone adds it to the corresponding projection here. The mirror
struct’s exhaustive destructure (the same BoundLaunchFields/E0027 technique
run.rs::IsolationPlan::base_spec already uses) only turns a field being dropped from this
module’s own local extraction into a compile error — it cannot, and does not claim to, catch a
field that was never extracted from the upstream type in the first place.
Measured versus asserted, by platform — naming the difference precisely
A receipt’s host facts distinguish three bases: Measured (probed on this specific run —
e.g. /proc/sys/kernel/osrelease on a Linux-native or sandlock-confined launch),
Asserted (a build-time constant, or a backend’s own documented claim about a component it
does not probe — e.g. aa-isolation-macos-vm’s guest architecture, stated in that backend’s
own source rather than measured at runtime), and Unmeasured (nothing established a value).
The field carrying the compile-time architecture constant is named build_target_arch, never
host_arch — conflating the two is exactly the class of overclaim the AAASM-5528 incident
this campaign exists to prevent already recorded, applied here to a new artifact rather than
relitigated. A macOS-hosted VM launch’s kernel-release fact is Unmeasured, honestly, because
the guest kernel — not the host’s — is what actually executed the confined process.
What this amendment does not decide
- No SaaS ingestion or verification service. Verification is local-only, via
aasm receipt verifyand the equivalent library call. - No signature, key, PKI, or MAC. See above;
ReceiptSealKindleaves room for a future kind without deciding what it is. - No replay or forensic reconstruction of a run from its receipt. Tracked separately under AAASM-6172.
runtime_imageisNoneon every backend today. No backend computes a digest of its guest/runtime image; this amendment does not hash one on every launch and does not touchIsolationBackend. The field exists in the schema so a future backend that does compute one has somewhere to put it.workspaceisNonetoday.aa-workspace-txhas zero consumers (grep -rln aa-workspace-tx --include=Cargo.toml .) — this amendment types the field against that crate’s existing vocabulary so wiring it later is a projection, not a redesign, but builds no workspace-diffing subsystem.- No receipt for a launch refused before it starts.
authority_gate/egress_gate/ negotiation refusals return beforerun_confinedand are already reported synchronously byisolation_machine_block— there is nothing an execution receipt could truthfully describe about a program that never started inside a boundary. What happened to a run that never started is AAASM-6172’s question, not this amendment’s. - No retention or garbage collection. One
0600file is written per confined run; growth is accepted and stated, not managed, for this ticket. - No change to
aa-isolation,aa-proxy,aa-core, or ADR 0033. ADR 0033 remains AAASM-5654’s territory, stillTo Doat the time of this amendment. - No new dependency.
Context
Agent Assembly already owns the governance semantics above execution: agent identity and
lineage, organization/team policy, approvals, budgets, MCP governance, sensitive-data
controls, developer-tool integration, transport mediation, audit and evidence-backed
protection state. aasm run already performs a managed launch for supported AI developer
tools: it resolves policy, registers an identity, applies managed settings and proxy
configuration, builds the child environment, starts the child and supervises lifecycle.
What it does not currently establish is a general synchronous OS-enforced boundary
around the whole native process tree. A child that is not mediated by an SDK or proxy can
still exercise capabilities exposed by its host identity unless another host mechanism
constrains them. Linux eBPF improves evidence and detection, but ADR 0033 records that its
current file/TLS/exec probes are observational and its opt-in syscall guard terminates
asynchronously. The source says so in-code: “bpf_send_signal is asynchronous; the
SIGKILL lands at the next signal-check point, so this syscall still runs once before
the task dies” (aa-ebpf-probes/src/syscall_guard.rs:189-192). Under ADR 0033 §6 that
mechanism reaches Detected, not Denied before execution, and the two are not
interchangeable.
The security requirement is therefore not “put the agent in another process”. A parent process is not a security boundary by itself. The requirement is that a trusted supervisor establishes kernel/runtime-enforced restrictions before executing the untrusted child, and that those restrictions cover descendants according to explicit inheritance semantics.
The product requirement is also not “build another sandbox”. Sandlock, seccomp/Landlock, gVisor, Firecracker and future platform mechanisms solve different portions of execution isolation. Re-implementing all of them would duplicate mature low-level work while still leaving Agent Assembly’s higher-level questions unanswered: which agent is acting, under which policy, which capabilities are required, which controls were actually applied, and what evidence justifies the UI or audit claim.
The commercial constraint
Agent Assembly is an open-core product with commercial extensions. A backend strategy must therefore preserve a clean product boundary between AASM-owned governance semantics and third-party isolation implementations. Built-in or redistributed backends should prefer permissive licenses (for example Apache-2.0/MIT/BSD) and carry explicit third-party notices, provenance and SBOM data. A backend with materially different copyleft or service terms requires a separate product/legal decision; it must not enter a proprietary distribution accidentally through an implementation shortcut.
The performance and compatibility constraint
No isolation mechanism is universally best. Coding agents are unusually syscall- and I/O-heavy: they traverse repositories, spawn compilers and test runners, use Git, package managers and language toolchains, open many small files and maintain network sessions. A stronger userspace-kernel or microVM boundary may impose a different compatibility or performance cost than a process-level Landlock/seccomp boundary. Conversely, a process sandbox shares more host-kernel attack surface than a userspace kernel or VM.
Consequently, performance and compatibility are measured backend properties, not architecture claims. The product must not encode one mechanism as “strong” merely by name, nor choose a universal backend without workload evidence.
Threat model
The default threat model for this ADR is:
- The agent and all code it causes to execute are untrusted. The agent may adaptively enumerate files, environment, network routes, metadata endpoints, IPC, sockets, credentials, process state and alternate execution paths.
- The host operator and AASM supervisor are trusted. Protecting a machine from its own root/administrator account is not provided merely by this local execution model.
- The process tree is the minimum native execution security unit. Threads share an address space and other process state and are not accepted as mutually hostile security domains.
- A descendant cannot gain broader authority by ordinary fork/exec. Child and sub-agent processes remain at the same or narrower effective capability scope unless an explicit delegated-capability mechanism is introduced and evidenced.
- The absence of an event is not proof that the agent did nothing. An unsupported, bypassed or uninstrumented path is reported according to ADR 0033/0030 evidence semantics, never inferred clean.
This ADR does not claim protection against a trusted host operator deliberately removing AASM, replacing its binaries, changing the kernel, modifying a policy they are authorized to modify, or otherwise exercising administrator ownership of the machine.
Decision
1. aasm run is the canonical managed-execution front door
Agent Assembly will evolve the existing aasm run surface rather than create a separate
product-shaped aasm sandbox run command.
Existing developer-tool forms remain backward compatible:
aasm run claude [args...]
aasm run codex [args...]
aasm run windsurf [args...]
A generic execution target will allow arbitrary agents/commands to enter the same identity, policy, lifecycle, isolation and evidence flow. The exact CLI grammar is an implementation/API review item; the intended shape is equivalent to:
aasm run exec -- python ai_agent_main.py
Isolation is a property of the run, not a separate lifecycle.
2. AASM owns the execution contract; backends own mechanism-specific realization
The durable contract consists of four conceptual objects (names may be refined without changing their semantics):
ExecutionSpec— the identity-bound, policy-derived statement of the process to run and the capabilities/resources it requires or forbids.CapabilitySet— a backend’s machine-readable statement of what it can enforce, observe or cannot represent on this host/version.EnforcementPlan— the resolved lowering fromExecutionSpecto a selected backend, including unsupported/degraded requirements before launch.EnforcementEvidence— the post-prepare/runtime facts that justify which ADR 0033 §6 claim term the product may attach to each control for this run.
A concrete backend implements an internal contract equivalent to:
#![allow(unused)]
fn main() {
trait IsolationBackend {
fn capabilities(&self) -> CapabilitySet;
fn plan(&self, spec: &ExecutionSpec) -> Result<EnforcementPlan>;
fn prepare(&self, plan: EnforcementPlan) -> Result<PreparedExecution>;
fn spawn(&self, prepared: PreparedExecution) -> Result<ExecutionHandle>;
fn evidence(&self, handle: &ExecutionHandle) -> EnforcementEvidence;
}
}
The trait shape above is illustrative; the semantic separation is normative.
A backend may use Sandlock, native Landlock/seccomp, gVisor, a microVM, an enterprise host mechanism or a future platform adapter. None of those names appear in the canonical AASM policy vocabulary merely because an implementation uses them.
3. Isolation class is not backend identity
User/product policy describes the required isolation properties, not the preferred vendor or mechanism. AASM may expose stable requirement classes such as process, userspace-kernel, microVM/hardware-backed or automatic selection, but a class is not a promise that one named backend will always implement it.
Concrete backend selection is an execution-plan fact. Advanced diagnostics may expose or pin a backend for reproducibility, but ordinary policy should survive a backend change.
Backend replaceability is an implementation property of this design, not the product value. What the product offers is portable policy semantics — one AASM policy that keeps its meaning across execution substrates — together with the evidence that records which controls that policy actually achieved on a given run. The backend interface exists to preserve those two things. Material derived from this ADR must not present pluggability itself as the benefit.
4. Capability negotiation happens before the untrusted process starts
The execution planner resolves policy requirements against the selected backend’s actual host capabilities before launch.
For every required control, planning resolves to exactly one outcome. This ADR does not invent a parallel vocabulary for those outcomes: each maps onto a term already defined by ADR 0033 §6, which stays authoritative for the terms themselves.
| Planning outcome | ADR 0033 §6 term the run may then claim |
|---|---|
| The backend has a mechanism that refuses the action before it takes effect | Denied before execution |
| The backend can report the action but cannot refuse it before effect | Observed / Detected |
| No applicable mechanism exists on this host, backend or backend version | Unsupported |
| A planned control is configured but unavailable, and posture permits proceeding | Degraded, carrying both the planned and the achieved level |
| Nothing inspected this action or payload | Unmeasured |
The following are normative:
- A required pre-effect control that the selected backend can only observe must not be promoted to Denied before execution.
- The default for an unmet required capability is refusal before launch.
- Degraded is reachable only where policy or operator posture explicitly permits it, and it must carry both levels into E6 evidence and audit rather than presenting the run as equivalently protected.
- A backend that is implemented but not validated for production use is Experimental; a control that is decided but not implemented is Planned and carries no capability claim. Neither may be reported as an achieved control.
5. The trusted supervisor stays outside the confined process tree
The AASM control/runtime process must not share the hostile process’s security boundary in a way that grants the child access to supervisor memory, privileged descriptors or credentials simply because both participate in one launch.
Where process-level sandbox initialization requires post-fork/pre-exec work, implementation
should prefer a deliberately small and auditable launcher/helper boundary. Complex
Landlock/seccomp/namespace setup must not be casually accumulated in an async runtime’s
pre_exec callback, where post-fork restrictions make ordinary allocation, locks and
library behavior unsafe or difficult to audit.
The target process and descendants execute inside the prepared boundary; the supervisor coordinates lifecycle, evidence and policy outside it.
6. Descendant confinement is part of correctness
An agent that can escape merely by spawning a child has no meaningful process boundary.
Therefore an execution backend is not considered capable of a process/filesystem/network
restriction unless its semantics cover the descendant processes relevant to that
restriction, or the limitation is explicitly surfaced in CapabilitySet and evidence.
Sub-agent identity may become narrower or more specific at the governance layer, but ordinary process creation must not widen OS capabilities.
7. aa-sandbox remains the tool sandbox
aa-sandbox continues to execute WASM-marked tools using Wasmtime/WASI controls such as
preopened filesystem paths, instruction fuel and memory limits. It is not renamed into or
silently reused as the whole-agent native process boundary.
A native agent may call a WASM tool inside aa-sandbox; that is composition of two
boundaries, not evidence that either one replaces the other.
8. Linux process isolation is the first implementation target; Sandlock is the first candidate backend
The first production experiment will target Linux process confinement using a permissively licensed existing substrate rather than immediately reimplementing every Landlock/seccomp edge case. Sandlock is the initial candidate because its process-based model maps closely to the required trusted-supervisor/restricted-child shape and provides useful Linux isolation primitives.
This is intentionally a candidate implementation decision, not policy architecture. AASM may replace or supplement it with a native Linux backend after compatibility, performance, security surface, release cadence and feature requirements are measured.
No Linux backend implies macOS or Windows support. Platform-specific mechanisms remain separate E4 decisions and capability reports.
9. Ambient credentials are capabilities, not convenient environment
The execution plan must treat inherited credentials, sensitive environment variables, open file descriptors, sockets and similar ambient authority as part of E5. The target state is least-authority inheritance and brokered/delegated credentials where practical.
This ADR does not claim that credential brokering is complete. Existing credential and secret-hardening work remains authoritative for its implementation. Until then, evidence must distinguish credentials intentionally delegated to the child from ambient authority that could not yet be removed.
Cross-reference — AAASM-6164 (2026-09): ADR 0038’s AAASM-6164 amendment adds a contract/truthful-report pair over
aa-proxy’s existing egress credential-injection mechanism (aa-proxy/src/credentials.rs, AAASM-3578/AAASM-5926) — a launch’s requirement of brokered credentials, checked against what that mechanism truthfully covers, and a default (no-escape-hatch) withholding of a brokered provider credential’s env name from the child ataasm runwhen the mechanism actually covers it. The injection mechanism named here is unchanged; this is the “credentials intentionally delegated … from ambient authority that could not yet be removed” distinction this paragraph already anticipated, applied to the one case a real brokerage mechanism now closes.
Cross-reference — AAASM-6171 (2026-09): ADR 0038’s AAASM-6171 amendment adds a typed host-capability broker (Xcode/Simulator native operations) as a supervisor-side mediation boundary, not a new backend implementation of this ADR’s
IsolationBackendtrait. It binds to this ADR’s ownCapabilityDomain::ProcessCreation(plusFilesystemRead/FilesystemWrite/Credential) rather than adding a domain, and states plainly — because it is true, not because this ticket introduces it — that no OS confinement backend exists on macOS today: the broker is the only mediation between a governed launch and a realxcodebuild/simctlinvocation on that platform.
10. Evidence is first-class output of execution
A successful process spawn is not evidence that every desired control was enforced.
aasm run --dry-run and runtime status will evolve to expose at least:
- requested isolation/capabilities;
- selected backend and backend version/provenance;
- planned controls, each with the ADR 0033 §6 term it is expected to reach;
- requirements resolved as Unsupported or Degraded, the latter naming the planned level alongside the achieved one;
- evidence sources available after prepare/start;
- the resulting launch posture: ready, refused or explicitly degraded.
The exact UI may evolve, but requested-versus-achieved separation is normative.
11. Backend provenance and license compatibility are release inputs
A distributed backend must have recorded license, source/version provenance, modification status and SBOM/third-party-notice handling. Permissive dependencies are preferred for built-in OSS/commercial distribution paths. A backend whose license or hosted-service terms materially change the commercial obligations cannot be enabled as an equivalent built-in implementation without review.
This requirement prevents implementation substitution from silently changing the legal or distribution boundary of the product.
Scope of this section. It records an engineering and release-process requirement — which facts a backend must carry, and which changes require review before it ships. It is not legal advice and does not interpret any licence. Whether a particular licence or hosted-service term is acceptable for a given distribution channel is a decision for the product and legal owners; this ADR records that the question must be asked, not what the answer is.
Initial execution flow
flowchart TB
CLI["aasm run"] --> RP["Run planner"]
RP --> ID["Agent identity / lineage"]
RP --> POL["Effective AASM policy"]
ID --> SPEC["ExecutionSpec"]
POL --> SPEC
SPEC --> SEL["Backend selection + capability negotiation"]
SEL -->|requirements satisfied| PLAN["EnforcementPlan"]
SEL -->|required capability missing| REFUSE["Refuse or explicit degraded posture"]
PLAN --> PREP["Prepare execution boundary"]
PREP --> SUP["Trusted AASM supervisor"]
PREP --> AGENT["Untrusted agent process tree"]
AGENT --> CHILD["Children / sub-agents: same or narrower OS authority"]
PREP --> EV["EnforcementEvidence"]
AGENT --> EV
EV --> E6["E6 audit / protection-state pipeline"]
Transport mediation, MCP decisions, DLP and WASM tool sandboxing remain composable controls around this execution flow. The diagram does not turn them into one ordered fallback pipeline; ADR 0033’s independent-element rule still applies.
Backend capability examples
The capability vocabulary must describe semantics rather than product names. Candidate axes include:
| Capability | Required distinction |
|---|---|
| Filesystem | read/write/create/delete scope; pre-effect deny vs observation |
| Network | destination/port/protocol scope; DNS behavior; pre-egress deny vs observation |
| Syscall | synchronous errno/kill/trap/observe semantics |
| Process tree | fork/exec inheritance, maximum descendants, signal/control semantics |
| Resources | CPU, memory, PIDs, wall time, file size / descriptor limits |
| IPC | Unix sockets, shared memory, namespaces and inherited descriptors |
| Credentials | removed, delegated, brokered, visible-to-child or unmeasured |
| Platform boundary | shared host kernel, userspace kernel, guest kernel/microVM |
| Evidence | which ADR 0033 §6 term the control reaches, and whether that rests on configuration, an exercised code path or independent verification |
A single boolean sandbox=true or supported=true is insufficient.
Alternatives considered
Reimplement native Landlock/seccomp/namespaces immediately
Rejected for the first delivery. It would make low-level correctness and kernel corner cases the critical path before AASM has validated its execution contract, CLI and policy semantics. A native backend remains a valid later optimization or independence strategy.
Hard-wire Sandlock into aasm run
Rejected. It would make an upstream implementation part of the product model, couple policy vocabulary to one Linux mechanism, complicate macOS/Windows/server evolution and increase exposure to upstream compatibility, release or license changes.
Treat backend pluggability itself as the product moat
Rejected as positioning. Replaceability is an architectural advantage, but customers buy the governance outcome: one policy, controlled capabilities and evidence across execution environments. The backend interface exists to preserve that product contract.
Treat current eBPF enforcement as equivalent to a process sandbox
Rejected. ADR 0033 §6 places the eBPF syscall guard at Detected plus asynchronous
termination, and explicitly not at Denied before execution; the guard’s own source
records that the offending syscall runs once before the SIGKILL lands
(aa-ebpf-probes/src/syscall_guard.rs:189-192). A pre-effect confinement requirement is
therefore not satisfied by enabling the syscall guard, and evidence from one term of the
§6 vocabulary cannot be relabelled as a stronger one.
Create aasm sandbox run
Rejected. It would split identity/policy/lifecycle semantics into competing launch paths
and make isolation appear optional or separate from governed execution. aasm run is
already the managed-launch surface and should be extended.
Isolate an agent thread inside a trusted process
Rejected as a hostile-code boundary. Threads share process address space and substantial state; the security unit here is a separately confined process tree.
Require a microVM for every launch
Rejected as the universal default. MicroVMs can offer a stronger kernel boundary for some workloads but impose deployment, startup, filesystem/toolchain and compatibility costs that are inappropriate to assume for all local developer-agent runs. They remain a future backend class for risk profiles that justify them.
Consequences
Positive
- AASM owns durable agent-execution semantics without reimplementing every isolation primitive.
- Backends can evolve or be replaced without changing organization/agent policy language.
- Local process isolation, server userspace-kernel isolation and future microVM execution can share one evidence and governance model.
- A failed or partial backend cannot truthfully appear equivalent to successful enforcement.
- The open-source/commercial boundary can be managed independently from a particular permissively licensed substrate.
Costs
- Capability negotiation is more complex than a boolean sandbox flag.
- Every backend needs conformance, adversarial, compatibility and performance evidence.
- AASM must maintain backend-version and platform capability matrices.
- Cross-platform support remains uneven until platform-specific backends are actually implemented.
- Process isolation adds new packaging and third-party supply-chain responsibilities.
Validation requirements
Before a backend can support a prevention claim:
- Each claimed capability has a precise decision-timing definition.
- Required-capability failure refuses launch or enters an explicitly permitted weaker posture.
- Descendant processes are covered or the limitation is surfaced before launch.
- Adversarial tests attempt direct filesystem, network, process, IPC and credential bypass paths.
- Negative controls prove the harness fails when an enforcement mechanism is removed/bypassed.
- Representative coding-agent performance/compatibility tests are recorded.
- Backend provenance, version, license and SBOM/notice requirements are recorded.
- E6 output never promotes Observed/Detected into Denied before execution, and never reports backend availability or configuration as an achieved control.
Reconsideration triggers
Re-open this ADR when any of the following changes the durable contract:
- A backend requires policy semantics that cannot be represented by
ExecutionSpecor the capability model without naming the backend. - A production native-Linux backend replaces the initial third-party substrate and exposes a materially different trust or descendant model.
- gVisor/userspace-kernel or microVM execution becomes a default rather than an optional risk-tier backend.
- macOS Endpoint Security/App Sandbox or a Windows host mechanism becomes a production whole-agent execution boundary.
- Root/admin-resistant managed-device protection becomes a product requirement.
- Credential brokering changes the meaning of ambient authority available to the child.
- Licensing or distribution constraints make a currently supported backend incompatible with an OSS or commercial channel.
Implementation ownership
Implementation is tracked by Epic AAASM-5702. This ADR defines the durable product/system decision; it does not prescribe branch/PR scheduling or CI procedure. Those remain contributor workflow concerns under the repository’s existing process documentation.
Last updated: 2026-10-02 by Chisanan232