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.

Rust workspace A2A 1.0 + versioned extensions SQLite · envelope-encrypted fields Linux namespaces · seccomp Pre-release

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.

remote peer — untrusted another aksond, anywhere operator — trusted akson CLI · MCP · harness aksond HTTPS · A2A 1.0 mutual TLS, pinned unix socket UID admitted per socket receive authorize · execute egress · evidence network endpoint mTLS 1.3 · pinned peer cert bounded A2A parser I-JSON caps · fail closed inbox / dedupe durable · inert · replay-safe contract engine CAS head · risk card policy decision risk card → explicit grant one-shot work order single-use · capabilities clean worker sandboxed, from zero output gate recipient · type · size processor broker allowlist · budget · key evidence engine DSSE manifest · SARIF one inherited fd the receive path holds only a &Store — it cannot dial, run, or read files encrypted store tasks · contracts · outbox · audit — durable before effect the only egress for approved plaintext model / processor plaintext, never the key signed result back to the requester
The trusted computing base is the daemon's own components — endpoint, parser, contract engine, issuer, gate, broker, evidence validator, store. The peer, the task content, the worker while running peer work, and the model are all untrusted. (§7.1)

Three structural choices do most of the security work:

One task's life

Twelve steps, in order, with the commit points where they belong. (§7.2)

  1. The network endpoint authenticates the peer's pinned certificate before reading a request body.
  2. Connection, content-length, rate, and parse limits are enforced.
  3. The standard A2A object and the recognized Akson extension are validated.
  4. The encrypted object and its stable digest are stored transactionally.
  5. The A2A response is returned only after the durability commit — a receipt you can hold the sender to.
  6. A human (or a standing policy, for non-disclosing work) evaluates the exact immutable contract revision — the risk card.
  7. Contract acceptance is signed separately from local authorization.
  8. The local issuer creates a one-shot work order for a clean executor.
  9. The executor receives only the selected input manifest and typed capabilities.
  10. Results pass through size, media-type, recipient, and schema gates.
  11. The evidence engine signs statements over the exact inputs, attempt, outputs, and verifier results.
  12. 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:

pending claimed running completed claim spawn gate ok failed ambiguous cancelled crash · cancel crash while claimed cancel recovery at boot: claimed | running → ambiguous · pending is untouched
The attempt machine from 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:

MechanismHow 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)

LayerWhat 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.
Fails closed

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 sub-attempt machine, per 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.
Honest limits

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.

Authentic ≠ correct

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.

StepWhat 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)

SurfaceProfile
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.

any host
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:

SocketAdmitsSurface
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.

CrateOwns
aksondThe daemon: receive pipeline, decision surface, issuing, delivery, outcomes.
akson-cliThe operator CLI — a thin client of the control socket.
akson-mcpThe MCP server exposing the daemon to an agent harness as tools.
akson-transportA2A HTTP+JSON server/client on the pinned mTLS 1.3 profile with the delivery extension.
akson-protoPinned A2A types, JSON mapping, profile validation.
akson-extAkson extension schemas, I-JSON, JCS canonicalization, DSSE envelopes, golden vectors.
akson-pairingThe introduction: key bindings, proof of possession over the session-bound transcript, peer-identity assembly, §8.4 change detection.
akson-contractContract validation, the revision chain, decisions, risk-card projection.
akson-authorityLocal policy, the capability vector, one-shot work orders, the attempt machine.
akson-sandboxThe Linux isolation launcher: namespaces, seccomp, cgroups v2, Landlock; fail-closed probing.
akson-workerThe clean-worker protocol and the output gate, including inertness checks.
akson-brokerThe processor broker: durable sub-attempts; the only egress path for approved plaintext.
akson-evidenceResult manifests, in-toto statements, the SARIF profile, evidence validation.
akson-cryptoKey lifecycle, purpose binding, thumbprints, JWS, and the bech32m identity-token codec — thin wrappers over reviewed libraries.
akson-storeEncrypted SQLite state, outbox/inbox, replay tombstones, the audit chain, state-generation checkpoint.
akson-adapter-sdkThe 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.