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. |
| Three control sockets, one broker channel | The admin socket carries operator authority; the worker socket carries only "submit a result"; the coordination socket carries a different local principal's staging and dispatch, and is absent unless configured. Brokered model calls travel a narrower channel still — the pre-opened socketpair the confined worker inherits as a file descriptor. A compromised worker holding both still cannot approve, send, or pair; neither can a compromised coordination driver — those ops are not on any surface either 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.
First contact
The pinned mTLS the rest of this page assumes has to be established somehow. It is established out of band, by a public token — never by a bearer secret on the wire. (§8.2, ADR-0013/0015)
Each endpoint has one identity token: a printable, QR-able line
(akson1…, bech32m) that commits to its identity root key — the key that
signs its Agent Card. The token is public: possession grants nothing, so it travels
over any channel whose integrity you trust, needing no confidentiality. Two
operators exchange tokens and each runs akson peer add <token> <label>
— the import is the whole trust decision, and the label is a local name you
choose, never one the peer declares.
A relationship is keyed by the root's thumbprint, not by the self-declared name, so two
peers who both call themselves claude simply coexist. On first connection
the two daemons run the introduction: a mutual, fail-closed
verification bound to that TLS session (RFC 9266 exporter), in a fixed order so a
hijacked endpoint learns nothing.
| Step | What holds |
|---|---|
| Hello | The dialer names the target and its own root thumbprint. An unimported root is refused after a constant-time lookup, before any signature work — the cheap admission gate, and a refused caller lands in a local knock log. |
| Responder proves first | Its key bindings, signed Agent Card, and proof of possession — the dialer checks the card's key is the imported root, then and only then discloses its own material. A wrong endpoint or MITM without the root key harvests nothing. |
| Commit | A compare-and-swap on (root, epoch): a removal that raced the
handshake fails it; changed material for a pinned peer suspends for review
(§8.4), never re-pins. Then the connection closes; work runs on a fresh, pinned
connection. |
From then on the identity is everything downstream binds to: the signed contract
carries each party's root (ADR-0014), and the receiver refuses a proposal
whose signed requester root does not match the channel it arrived on. There is no
invitation, no bearer secret, no pending-confirm dance — deleted, along with
AKSON_PAIR_ADDR and its listener. The public
/.well-known/agent-card.json is the one surface an unimported stranger may
read; it is display-only until an introduction verifies it against a root you imported.
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), verified and pinned at first contact against the peer's out-of-band identity-token root; 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 be one that socket admits — the rule is
per-socket, not one rule for all three — and the op must be allowed on the socket it
arrived at:
| Socket | Admits | Surface |
|---|---|---|
admin.sock |
the daemon's own UID | Authority-bearing operator ops: import a peer, approve, run, fulfil, deliver, send, configure processors, and mint a coordination consent receipt. |
worker.sock |
the daemon's own UID | The narrow surface the worker uses:
two ops,
submit_result and request_processor_call.
Nothing else — a compromised worker cannot approve a contract or send a task,
because those ops do not exist on its socket. (The brokered call's bytes
do not travel here at all: the confined worker uses the pre-opened broker fd it
inherited.) |
coord.sock |
the UID named by AKSON_COORD_UID, plus the daemon's own for
diagnostics |
The coordination surface (ADR-0016): a
different local principal stages outbound payloads, spends a consent an
operator already granted, and reads coordination state.
eight ops exist here and
everything else answers forbidden-surface — no approve, no pairing,
no credential op. Consent is not on it. With
AKSON_COORD_UID unset the socket is not created at all. |
Dominance is deliberately asymmetric: an admin connection may invoke the narrow
surfaces' ops (so an operator can diagnose them without a second account), while
worker and coord dominate nothing — including each other. And admission is a
UID, never an attested process identity: SO_PEERCRED says which
user connected, not which program. That is the developer assurance profile, and the
default profile has no UID separation in it at all — separate identities per role are an
opt-in fleet arrangement.
Because a different principal sits on the coordination socket, its request line is
bounded at 1 MiB; admin and worker are the daemon's own UID and stay unbounded.
Connections on each socket are served concurrently, up to 16 at a time — beyond that the
next is served on the accept thread, so the socket serialises rather than refusing
anyone. Within the ceiling, an op that blocks on the network — dispatch
carries bytes to a pinned peer — delays only itself rather than the whole surface.
The framing, problem shapes, and operation tables: spec/control-protocol.md. The coordination surface in full — the registry, the envelope, the receiver's checks, and its limits — is its own page.
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 | The introduction: key bindings, proof of possession over the session-bound transcript, peer-identity assembly, §8.4 change detection. |
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, and the bech32m identity-token codec — 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.