Key workflows
This page traces the four workflows that define agent-assembly’s runtime
behaviour, each grounded in the real code path:
For component-level detail behind each box, see Component deep-dives; for the bird’s-eye map, see System architecture.
Policy evaluation
When aa-gateway receives a PolicyService.CheckAction RPC, the policy engine
under aa-gateway/src/policy/
walks parse → compile → scope cascade → budget → decision, then audits the
result. The decision type (engine/decision.rs) is one of Allow, Deny,
or RequireApproval.
flowchart TD
Req["CheckActionRequest<br/>(action, target, labels)"] --> Cache{Decision<br/>cache hit?<br/>engine/cache.rs}
Cache -->|hit| Resp
Cache -->|miss| Parse["policy/raw.rs<br/>deserialise bundle"]
Parse --> Validate["policy/validator.rs<br/>structural validation"]
Validate --> Compile["policy/expr.rs<br/>compile predicates"]
Compile --> Cascade["policy/document.rs + scope.rs<br/>org → team → agent → tool<br/>most-restrictive-wins"]
Cascade --> Budget["budget/tracker.rs<br/>check team budget"]
Budget --> Decide{PolicyDecision}
Decide -->|Allow| Audit
Decide -->|Deny| Audit
Decide -->|RequireApproval| Approval["approval queue<br/>(timeout ⇒ Pending)"]
Approval --> Audit
Audit["audit.rs<br/>append hash-chained entry"] --> Resp["CheckActionResponse"]
- Decision cache —
engine/cache.rsshort-circuits repeat lookups for the same(scope, action)key. - Parse + validate —
policy/raw.rsdeserialises the active bundle;policy/validator.rsenforces structural invariants (well-formed scopes, unique rule names). - Compile —
policy/expr.rsturns rule predicates into a typed expression tree evaluated against the request’sActionType, target, and labels. - Scope cascade —
policy/document.rs+scope.rswalkorg → team → agent → tooland merge most-restrictive-wins, with cycle detection on delegation. - Budget check —
budget/tracker.rs(priced viabudget/pricing.rs) downgrades an otherwise-allowed request to Deny if it would breach a budget. - Decision —
engine/decision.rsyieldsAllow,Deny { reason }, orRequireApproval { timeout_secs }. - Audit — every decision is appended to the hash-chained audit log via
audit.rsbefore the response is returned.
Latency targets and current p99 measurements live in Benchmarks — Policy Check p99.
Agent registration
Registration flows through AgentLifecycleService.Register
(aa-gateway/src/service/lifecycle_service.rs),
which validates delegation depth and writes into the DashMap-backed
AgentRegistry. Agents then keep their record live with periodic Heartbeats.
sequenceDiagram
autonumber
participant Agent
participant RT as aa-runtime
participant LS as AgentLifecycleService<br/>(aa-gateway)
participant Reg as AgentRegistry<br/>(registry/store.rs)
participant Store as Storage<br/>(storage_bridge.rs)
Agent->>RT: start with agent identity + parent
RT->>LS: gRPC Register(RegisterRequest)
LS->>LS: validate delegation depth<br/>(≤ DEFAULT_MAX_AGENT_DEPTH = 10)
alt depth OK and not already registered
LS->>Reg: insert AgentRecord (status Active)
Reg->>Store: persist via storage bridge
LS-->>RT: RegisterResponse (token)
else already registered / depth exceeded
LS-->>RT: AlreadyExists / FailedPrecondition
end
loop heartbeat interval
RT->>LS: Heartbeat(HeartbeatRequest)
LS->>Reg: refresh last-seen, recent events
LS-->>RT: HeartbeatResponse (control commands?)
end
- Delegation depth — a sub-agent’s depth must not exceed
DEFAULT_MAX_AGENT_DEPTH(10); over-deep registrations are rejected. - Lineage — the registry records parent/child links (
registry/lineage.rs) so the topology tree and orphan handling (registry/orphan.rs) work. - Control stream —
ControlStreamlets the gateway push commands (e.g.SuspendCommand) back to a live agent. - Deregister — on shutdown the agent calls
Deregister; orphaned children are handled per the configuredOrphanMode.
Budget tracking & rollup
Every priced action updates the in-memory BudgetTracker; the dashboard, SDK,
and CLI read a composed BudgetRollup across agent / team / org / subtree
scopes.
flowchart LR
subgraph track["Tracking (write path)"]
Action["priced action<br/>(model + tokens)"] --> Price["budget/pricing.rs<br/>PricingTable"]
Price --> Tracker["budget/tracker.rs<br/>BudgetTracker"]
Tracker --> Windows["daily + monthly windows<br/>per agent / team / global"]
Windows --> Alert{"≥ 80% / 95%?"}
Alert -->|yes| Broadcast["BudgetAlert<br/>(broadcast channel)"]
end
subgraph roll["Rollup (read path)"]
Req["GET /agents/{id}/budget<br/>or aasm policy show --show-budget"] --> Rollup["budget/rollup.rs<br/>BudgetRollup"]
Rollup --> Rows["BudgetRow[]<br/>agent · team · org · subtree"]
end
Tracker -. read-only accessors .-> Rollup
- Pricing —
budget/pricing.rsconverts model + token counts into a USD cost. - Windows —
BudgetTrackerkeeps daily and monthly windows for each agent, each team, and the global total. - Alerts — crossing 80 % or 95 % of a limit emits a
BudgetAlerton a broadcast channel (capacity 64) for live dashboards. - Rollup —
budget/rollup.rscomposes aBudgetRowper scope (agent,team:<id>,org,subtree) using the tracker’s read-only accessors — narrowest scope first. The same rollup drives both the HTTP endpoint andaasm policy show <agent_id> --show-budget.
Interception & enforcement
An agent action is observed by one of the three layers, normalised into the
aa-proto wire format, re-scanned by aa-runtime, then sent to the gateway for
a decision. The runtime is the mandatory chokepoint: it never trusts the
SDK’s assertions.
sequenceDiagram
autonumber
participant Agent
participant SDK as L1 SDK shim<br/>(aa-sdk-client)
participant Proxy as L2 proxy<br/>(aa-proxy)
participant eBPF as L3 eBPF<br/>(aa-ebpf)
participant RT as aa-runtime<br/>pipeline + enforcement
participant GW as aa-gateway<br/>PolicyService
alt L1 — in-process
Agent->>SDK: tool / LLM / network call
SDK->>RT: UDS IpcFrame (event)
else L2 — sidecar
Agent->>Proxy: outbound HTTPS (MitM)
Proxy->>RT: forwarded event
else L3 — kernel
Agent-->>eBPF: SSL_write / exec / file syscall
eBPF->>RT: ring-buffer event
end
RT->>RT: enrich (pipeline/event.rs)
RT->>RT: scan + redact (pipeline/enforcement.rs)<br/>fail-closed, oversized ⇒ redact whole
RT->>GW: CheckAction(CheckActionRequest)
GW-->>RT: Allow / Deny / RequireApproval
alt Allow
RT-->>Agent: pass-through
else Deny
RT-->>Agent: error / blocked
else RequireApproval
RT->>RT: approval_sink.wait_for_approval<br/>(timeout ⇒ Decision::Pending)
RT-->>Agent: allow or block on resolution
end
Key invariants from aa-runtime/src/pipeline/enforcement.rs:
- The runtime re-scans every event unconditionally — there is no
already_scanned/cleanwire marker, and none is honoured. - Enforcement is fail-closed: a field larger than
max_field_bytes(default 64 KiB) cannot be fully scanned, so it is redacted whole ([REDACTED:OVERSIZED]) rather than partially forwarded. - The credential scanner / redaction primitives come from the
aa-securityleaf crate.
The eBPF layer is observe-and-forward for bypass-detection: it cannot block in-kernel, so it streams audit events while the SDK and proxy layers carry the synchronous allow/deny. For the trust rationale, see three-layer defense.
Where each event goes next
Once a decision is made, the event flows into the audit and storage pipeline — covered in detail on the Data flows page.
Last updated: 2026-06-11 by Chisanan232