Internals · Architecture
How Akson is built.
One daemon per user, a handful of trusted components, and a rule that shapes everything: the path that receives a peer's bytes and the path that executes them never meet without a local decision in between — yours, or a standing policy you configured.
This page is the technical map. The normative source of truth is the design document (§ references below point into it) and the threat model; the state machines drawn here are also model-checked against the code.
The system, drawn
Everything lives in aksond. Peers reach it over pinned mTLS; the operator
reaches it over a Unix socket; a model is reachable only through the broker.
Three structural choices do most of the security work:
- The receive path cannot act. The function that accepts a proposal is handed a database handle and nothing else — no transport, no processor client, no filesystem, no worker. "Arrival is not execution" is not a policy check that could be forgotten; the capability to do anything else simply is not in scope. (§6.3)
- Authority is constructed, not inherited. Approving a task issues a one-shot work order carrying an explicit capability vector. The worker is built from nothing to match exactly that grant — rather than built powerful and then restricted. (§12.3)
- Every effect is durable first. The store commits
running/dispatchingbefore the first byte of any effect leaves. Crash recovery marks anything mid-flightambiguous— reported as uncertain, never retried automatically, never claimed done. (§13.1, §15.5)
One task's life
Twelve steps, in order, with the commit points where they belong. (§7.2)
- The network endpoint authenticates the peer's pinned certificate before reading a request body.
- Connection, content-length, rate, and parse limits are enforced.
- The standard A2A object and the recognized Akson extension are validated.
- The encrypted object and its stable digest are stored transactionally.
- The A2A response is returned only after the durability commit — a receipt you can hold the sender to.
- A human (or a standing policy, for non-disclosing work) evaluates the exact immutable contract revision — the risk card.
- Contract acceptance is signed separately from local authorization.
- The local issuer creates a one-shot work order for a clean executor.
- The executor receives only the selected input manifest and typed capabilities.
- Results pass through size, media-type, recipient, and schema gates.
- The evidence engine signs statements over the exact inputs, attempt, outputs, and verifier results.
- The requester validates the bundle independently and signs its own outcome.
Execution is tracked as an attempt with a small, closed state machine — the same one the TLA+ model checks against the code:
akson-authority. There is no
edge out of ambiguous: after a crash, honesty beats optimism. One honest
footnote: task fulfill — the trusted, operator-only path — completes a
claimed attempt without entering running, an edge the
model does not yet cover. (§10.4)The two permission domains
Your authority and a peer's grant never share a mechanism — different sockets, different keys, different processes, different rules.
Akson's core principle is that a peer's commands run in a separate, enforced, reduced-authority domain, and your own user-granted authority is never touched. Four mechanisms realize it:
| Mechanism | How it separates the domains |
|---|---|
| Grant-derived sandbox spec | The sandbox is not a generic jail with holes poked in it — it is derived from the approved grant. The named inputs, the single output directory, and (only if granted) one broker channel are constructed in; nothing else exists. |
| One-shot work order | Approval mints a single-use, nonce-bound order (§12.3). Replaying it is refused
by the store's claim transaction; a crash between claim and run resolves to
ambiguous, not to a second execution. |
| Two control sockets, one broker channel | The admin socket carries operator authority; the worker socket carries only "submit a result". Brokered model calls travel a third, even narrower channel — the pre-opened socketpair the confined worker inherits as a file descriptor. A compromised worker holding both still cannot approve, send, or pair — those ops are not on any surface it can reach. |
| Explicit outward grants | The two grants that disclose content — processor_use (reach a model)
and artifact_export (return files) — must be requested by the task
and granted at approval. Auto-approval can never confer them. |
The sandbox, layer by layer
Defense in depth, each layer independent, all of it probed by
akson doctor and refused rather than degraded. (§13.1)
| Layer | What it removes |
|---|---|
| User / mount / PID / net / IPC / UTS namespaces | The host's users, filesystem, processes, network interfaces, and IPC. The worker sees a private world with only its task directory in it. |
| Default-deny seccomp | Every syscall not on the reviewed allowlist — including
socket() and connect(), so there is no network path
even inside the net namespace. no_new_privs is set. |
| Strict adapter profile | For a production adapter (AKSON_WORKER_EXEC), the
process-creation family too: no fork, clone, or
vfork. A shell reached via execve cannot spawn a
single command. |
| cgroups v2 | Unbounded memory and process count (hard ceilings per run). A delegated
subtree is required; without one, task run refuses
(503 no-confinement). Wall-clock is a separate daemon-enforced
timer; there is no CPU quota yet. |
| Landlock | Filesystem reach beyond the constructed paths — applied by the worker entrypoint where the kernel ABI supports the rules. Best-effort depth behind the mount namespace, not part of the fail-closed required set. |
Private /proc, fd allowlist, empty environment |
Sight of host processes; inherited descriptors beyond the explicit allowlist
(the broker channel is the notable member); ambient credentials in
env. No home, no D-Bus, no SSH agent, no keyring, no Docker
socket, no cloud metadata. |
If the host cannot provide the required set — unprivileged user namespaces, the
delegated cgroup subtree, seccomp — execution refuses rather than running a peer's
task unconfined. akson doctor names the missing capability. The worker
does get a read-only runtime (/usr, /lib*) and a private,
task-scoped output directory — what it never gets is your workspace, home,
credentials, or a socket. (§13.1)
The broker: processor calls are effects
Sending plaintext to a model discloses data and costs money — so every call is a durable sub-attempt with the same crash honesty as execution itself.
The confined worker inherits exactly one already-connected file descriptor. It writes a typed request; the daemon makes the real HTTPS call. On the daemon's side of that descriptor, the broker enforces:
- The approved processor and budgets — bound into the one-shot work order at approval. The broker snapshots that processor's currently configured origin when dispatching, so a configuration change between approval and dispatch is an acknowledged gap rather than a bound value;
- An egress allowlist and address re-check —
httpsonly, the resolved address re-validated before dialing (global unicast unless a local processor opted in), redirects and ambient proxies disabled — so neither a task nor a DNS rebind can choose a different destination; - Credential injection — the API key is added daemon-side and never enters the worker's domain;
- A budget — a per-work-order operation count, response-size cap, and wall-clock limit, counted in the same transaction that admits the call.
prepared ──> dispatching ──> completed | failed | ambiguous | cancelled
durable-before-wire: "dispatching" commits before the first byte leaves.
a crash mid-call resolves to ambiguous — possible disclosure and cost are
reported to the operator, and never silently retried.
The broker bounds operations, bytes, destination, and time — not
money. max_cost_microusd is recorded, not enforced: pricing is
per-provider and depends on token usage the broker does not parse. The operation count
and size caps bound the blast radius; a true cost ceiling needs per-provider usage
accounting and is tracked as follow-up work in the threat model.
Evidence and outcomes
A result is not "some bytes arrived" — it is a signed claim the requester re-verifies from scratch before recording anything.
When the performer delivers, two things travel: the output bytes, and a canonical result manifest naming each output with its role, media type, size, and SHA-256 — signed as a DSSE envelope (§14.1–14.2). Findings use plain SARIF, validated against the profile before they are emitted, so a model answering with malformed or oversized findings fails closed on the performer's side.
The requester's daemon then re-verifies the claim independently: the delivery must match an outstanding request, verify under the performer's pinned task-result key, and every output the manifest names must re-hash to the digest it signs. One missing output, one altered byte, one extra part — the whole delivery is refused before anything is recorded (T8). Only then does the requester sign its own outcome, a durable disposition grounded in the verified bundle (§14.5). Two honest limits, tracked in the threat model: the manifest is not yet cross-checked against the contract's declared deliverables, and referenced in-toto evidence bytes are not delivered or independently verified in v1.
Before any renderable artifact leaves the performer, it is checked inert: SVG, HTML, Markdown, Mermaid, and Graphviz outputs are refused if they carry scripts, event handlers, script-bearing data URIs, external fetches, or DOCTYPE/ENTITY declarations (§11, T4). That is defense in depth for the supported renderable types — not a warranty for arbitrary media or a vulnerable viewer.
Verification establishes who produced the bytes and that nobody altered them. It does not establish that the content is right, honest, or safe. Treat a verified result as attributable input — the guide says how.
The wire
Standard A2A 1.0 where a standard exists; a small, versioned extension surface where Akson needs stronger promises. (§3, §10)
| Surface | Profile |
|---|---|
| Protocol | A2A 1.0 objects
over HTTP+JSON — today the SendMessage surface; the wider profiled
operation set (GetTask, ListTasks,
CancelTask) is specified but not yet implemented. Akson extensions
ride the declared extension mechanism: signed task contracts, durable passive
delivery, portable evidence. |
| Transport | TLS 1.3 only, mutual authentication, peer certificates pinned by digest at pairing — no CA chain for peers, no DNS trust. No session resumption, no tickets, no 0-RTT. (§9.1) |
| Delivery | Durable-before-response: the A2A reply is returned only after the store commit,
and every request carries an idempotency commitment over its covered values — an
exact replay returns the original response byte-identically; a changed value is a
Conflict. (§9.2) |
| Encoding | Strict I-JSON with hard byte, depth, and node caps; duplicate-key and unsafe-integer rejection; digests computed over original bytes; JCS (RFC 8785) where canonical bytes are needed. Every parser gate fails closed, backed by fuzz targets and hostile-input suites. (§11.1) |
| Signing | Ed25519 keys, purpose-bound (a proposal key cannot sign a result), exchanged and pinned at pairing; DSSE envelopes for contracts and results; JWS for the agent card. |
The control plane
The CLI, the MCP server, and any client you write all speak one tiny protocol to the daemon: one JSON line in, one JSON line out, over a Unix socket.
printf '{"op":"who_am_i"}\n' | socat - UNIX-CONNECT:"$XDG_RUNTIME_DIR/akson/admin.sock"
# {"outcome":"ok","result":{"issuer":"orgB","agent":"bob", …}}
Every exchange has that shape; akson whoami is exactly this request. The
daemon checks two things before running any op: the connecting process's UID (via
SO_PEERCRED) must equal its own, and the op must be allowed on the socket
it arrived at:
| Socket | Surface |
|---|---|
admin.sock |
Authority-bearing operator ops: pair, approve, run, fulfil, deliver, send, configure processors. |
worker.sock |
The narrow surface the worker uses to submit its result. Nothing else — a compromised worker cannot approve a contract or send a task, because those ops do not exist on its socket. (Brokered processor calls do not travel here at all: the confined worker uses the pre-opened broker fd it inherited.) |
The framing, problem shapes, and operation tables: spec/control-protocol.md.
The crate map
A workspace of small crates, each owning one concern — the boundaries in the code mirror the boundaries in the design.
| Crate | Owns |
|---|---|
aksond | The daemon: receive pipeline, decision surface, issuing, delivery, outcomes. |
akson-cli | The operator CLI — a thin client of the control socket. |
akson-mcp | The MCP server exposing the daemon to an agent harness as tools. |
akson-transport | A2A HTTP+JSON server/client on the pinned mTLS 1.3 profile with the delivery extension. |
akson-proto | Pinned A2A types, JSON mapping, profile validation. |
akson-ext | Akson extension schemas, I-JSON, JCS canonicalization, DSSE envelopes, golden vectors. |
akson-pairing | Invitations, the consume-once bootstrap ledger, pending-to-active pairing, re-pair and removal. |
akson-contract | Contract validation, the revision chain, decisions, risk-card projection. |
akson-authority | Local policy, the capability vector, one-shot work orders, the attempt machine. |
akson-sandbox | The Linux isolation launcher: namespaces, seccomp, cgroups v2, Landlock; fail-closed probing. |
akson-worker | The clean-worker protocol and the output gate, including inertness checks. |
akson-broker | The processor broker: durable sub-attempts; the only egress path for approved plaintext. |
akson-evidence | Result manifests, in-toto statements, the SARIF profile, evidence validation. |
akson-crypto | Key lifecycle, purpose binding, thumbprints, JWS — thin wrappers over reviewed libraries. |
akson-store | Encrypted SQLite state, outbox/inbox, replay tombstones, the audit chain, state-generation checkpoint. |
akson-adapter-sdk | The adapter contract, fixtures, and the §16.3 conformance harness. |
adapters/* | OpenAI-compatible, Anthropic, and Gemini worker adapters, built on the SDK, run under the strict profile. |
Beyond the runtime crates: proof/ holds the TLA+ models, and its
conformance harness is itself a workspace member so plain
cargo test --workspace runs it (see Proofs);
xcheck/ is the independent Python cross-checker (deliberately
not sharing the workspace); fuzz/ carries the contract,
I-JSON, and SARIF fuzz targets; spec/ the golden vectors, control
protocol, and ADRs.