Coordination · ADR-0016

A third socket, for a different principal.

A sibling system needs to stage outbound payloads, dispatch them once a human has consented, and read coordination state — without ever holding akson's admin authority. That is a separate Unix identity on a separate socket with its own small registry of operations, and it does not exist on your machine unless you create it.

akson_byom_exchange_v1 eight ops, deny-by-absence absent by default consent is admin-only v0.0.1-alpha.1
Read this first

The assurance this surface rests on is a developer one. Admission is a Unix UID via SO_PEERCRED — which user connected, never which program — so there is no attested process identity and no asymmetric endpoint identity here; anything running as the configured UID reaches the surface. The default profile has no UID separation at all: it runs everything under one user, and this socket is simply absent. Separate identities per role are an opt-in fleet profile in deploy/, not what a laptop gets. Each of these limits is repeated below where the capability it bounds is described.

This page describes the surface the way the rest of this site describes akson: what the running code does, with the residuals named next to the thing they qualify. The decision and its threat cases are ADR-0016; the op-by-op reference — every argument, every reply field, every refusal — is spec/control-protocol.md, which is normative where this page is descriptive.

A third socket, with its own Unix identity

Akson had two control sockets, both admitting only the daemon's own UID. A different local principal needs a third — and it is created only when you name the identity that may use it.

SocketAdmitsModeFor
admin.sock the daemon's own UID 0600 Authority-bearing operator ops — pairing, approval, run, deliver, send, configuration, and minting a coordination consent receipt.
worker.sock the daemon's own UID 0600 The narrow surface the sandboxed worker uses: two ops, submit_result and request_processor_call.
coord.sock the UID named by AKSON_COORD_UID, or the daemon's own (so an operator can diagnose the surface without a second account) 0660 The coordination surface: a different local principal stages outbound payloads, spends a consent an operator already granted, and reads coordination state.
aksond one process, one daemon UID admin.sock 26 ops · dominates both worker.sock 2 ops · dominates nothing coord.sock 8 ops · dominates nothing the operator uid = the daemon's the confined worker uid = the daemon's the driver uid = AKSON_COORD_UID admin may invoke a coord op — diagnostics a coord connection reaches no admin op, ever with AKSON_COORD_UID unset, the third box does not exist — nothing to connect to, nothing to probe
Dominance is deliberately asymmetric, and stated case by case in Surface::satisfies rather than inferred. Admin dominates both narrow surfaces; worker and coordination dominate nothing — including each other.

Absent rather than guarded. With AKSON_COORD_UID unset, coord.sock is not created at all — the same posture that removed the pairing bootstrap listener. An endpoint that was never mounted cannot be probed, fingerprinted, or misconfigured open. A value that is not a numeric UID is fatal at startup, so a typo cannot quietly delete the surface instead of creating it.

The 0660 mode is not the boundary — SO_PEERCRED is. What the mode and the runtime directory decide is only whether a separate identity can reach the socket file, and that is a deliberate act with two halves. A plain single-user run keeps its runtime directory at 0700, where no other identity can traverse to the socket in the first place. The fleet profile opens exactly that path and no more: RuntimeDirectoryMode=0710 on the daemon unit, plus SupplementaryGroups=akson on the driver's, so the driver's identity may traverse the directory. Traversal is not authorization. Having reached the socket, an unadmitted UID is refused before the request line is even read, with a generic unauthorized problem carrying no detail — so a UID mismatch and an unreadable credential are indistinguishable to the caller.

Because a different principal is on the other end, the coordination surface bounds its request line at 1 MiB (413 request-too-large); admin and worker are the daemon's own UID and stay unbounded. A staged payload is separately capped at 512 KiB (413 payload-too-large). Connections on each socket are served concurrently, up to 16 at a time — and beyond that ceiling the next connection is served on the accept thread, so the socket serialises rather than refusing anyone. Within the ceiling, one slow dispatch carriage delays only itself; past it, a backlog of slow carriages does hold up the accept loop, which is the bound worth knowing rather than the one worth omitting.

The limit here

SO_PEERCRED authenticates a UID, not an attested process identity and not a key: anything running as that UID reaches this surface. This is the developer assurance profile, and the default profile has no UID separation in it at all. Separately, no hardening score is claimed for the service units that would give you that separation: systemd-analyze security only inspects units systemd has loaded, so it needs them installed as root — it has never produced a number for akson-daemon.service or akson-coord.service, and none is quoted anywhere. What has been verified is narrower and should be read as exactly that: both units parse under systemd-analyze verify, and no sandbox-hostile directive is active in the daemon unit.

What is on it — a registry that denies by absence

Not "an op the driver is not authorized for". An op that does not exist on this surface at all, and therefore cannot be reached, mistyped into, or gradually widened.

These eight operations are the whole surface:

opWhat it does
coord_whoamiThis endpoint's identity and the protocol/feature versions — the driver's own handshake. Narrower than admin's who_am_i on purpose: no data_dir, no receive_addr.
peer_showOne named peer's verified identity tuple and card claims. It answers about the peer asked for and never enumerates; an unknown or malformed label gets the same 404 unknown-peer.
stageInert, idempotent staging of outbound bytes. Nothing starts, no authority is minted, no socket opens. The same bytes return the same reference and write no second record.
stage_showA staged payload's status and digests, plus its consent once an operator has minted one.
dispatchThe one op with an effect. One-shot: it spends a consent receipt it cannot create, commits the dispatch, and carries the bytes to the pinned recipient.
task_statusWhether a dispatch this surface committed was acknowledged. Scoped to those dispatches — an inbound task in the operator's inbox is the same 404 unknown-task.
events_readThe durable cursored coordination feed. Cursors are opaque: one that did not come from a reply is refused 400 bad-cursor.
capability_evidenceA DSSE-signed in-toto Statement of what this endpoint can federate with a peer, signed with the same evidence key result evidence uses.

Everything else returns 403 forbidden-surface, with a detail that names only the surface and never which ops exist elsewhere. Worth naming explicitly, because what is absent is the actual security property: there is no approve or deny of an inbound task, no pairing, no peer import, no processor or credential operation, no task send / fulfill / deliver, and no configuration. A compromised driver can stage inert bytes and burn consent receipts it was already given. It cannot mint consent, reach a credential, or touch inbound authority — those operations are not merely unauthorized here, they are unaddressable.

For contrast, the admin socket carries 26 operations. A coordination connection reaches none of them, and reaches neither of the worker's: Coord dominates nothing.

A driver does not have to guess which of these it is talking to. coord_whoami reports the registry as its features list — coord_whoami, peer_show, stage, stage_show, dispatch, task_status, events_read and capability_evidence — beside unimplemented and partial, which are both empty now that dispatch has its outbound carrier. Both fields stay present rather than disappearing when they empty: a driver parses them, and "the list is empty" is an answer while "the field is gone" is a parse change.

dispatch requires a receipt it cannot create. Minting one is an operator act on the admin socket, after the operator has read the risk card for that exact staged digest.

the operator, on admin
$ akson stage show stage-2deacff5c7b26955f15c16c8e05cf978
stage-2deacff5c7b26955f15c16c8e05cf978
  status:     staged
  task type:  https://byom.example/task/exchange/v1
  recipient:  partner
  bytes:      16
  payload:    sha256:2c7777f9dd4529ce821d7e5450aaf7972642011fecdabbecaf49b86dacc269eb
  staged as:  2deacff5c7b26955f15c16c8e05cf97816bd3d105e5e3dd80db9cf91a0fc6896
  consent:    none — `akson stage consent stage-2deacff5c7b26955f15c16c8e05cf978`

$ akson stage consent stage-2deacff5c7b26955f15c16c8e05cf978
You are about to allow ONE outbound disclosure to your peer "partner": 16 bytes of https://byom.example/task/exchange/v1.

What leaves this machine
  task type:   https://byom.example/task/exchange/v1
  bytes:       16
  payload:     sha256:2c7777f9dd4529ce821d7e5450aaf7972642011fecdabbecaf49b86dacc269eb
  staged as:   2deacff5c7b26955f15c16c8e05cf97816bd3d105e5e3dd80db9cf91a0fc6896
Who receives it
  recipient label: partner
  root thumbprint: fpekQ8923WNzVDB-gyv-ZRFyLFGPbHSJcsqY0sTKRcQ
  relationship: imported, not yet introduced
  endpoint hint: 127.0.0.1:18444
What this consent allows
  exactly one dispatch of the staged digest above, once (max_uses 1)
  nothing else: it grants no model call, no credential, and no inbound authority
  akson does not interpret these bytes — it discloses them as staged
Who staged it
  the coordination surface (akson_byom_exchange_v1), at unix time 1753574400
  staging was inert: nothing has left this machine yet

consent receipt: consent-6f1c2d5b9a084e37b1c0d2e4f5a6b7c8
  binds:      2deacff5c7b26955f15c16c8e05cf97816bd3d105e5e3dd80db9cf91a0fc6896
  uses:       0/1

That session is not a mock-up: it is rendered from the golden vectors coordination/stage-show-staged and coordination/stage-consent, in the field order akson-cli prints, by the same script that checks this site. Be precise about what that pins, though. The vectors are asserted against the running daemon for the reply's key set, the risk card's opening sentence, and its section headings — so those cannot drift here unnoticed. The card's individual lines are reproduced from the vector but are not separately asserted against the daemon, so they are accurate rather than enforced.

The card and the receipt come from a single call over the row the receipt is minted against, so what the operator read and what the receipt authorizes cannot drift apart. A second consent is refused while an unconsumed receipt exists (409 already-consented), and refused permanently once the dispatch ledger holds a dispatch for that staging (409 already-dispatched) — a second consent could only authorize a second disclosure of the same digest. The card's closing claim about what has already left this machine is read off that same ledger rather than asserted, so it can never say "nothing has left yet" over bytes that have.

Staging is inert

stage writes bytes and returns a reference. It starts no model, mints no authority, touches no workspace, invokes no tool, and opens no socket — "arrival is not execution", applied to the outbound direction.

The reference is a function of the content, so a client cannot choose it and two stagings of the same content cannot disagree about it. The staged digest is SHA-256 over the RFC 8785 canonical JSON of exactly three fields — the payload by its own digest, the recipient, and the task type:

the staged-digest preimage — spec/vectors/coordination/dispatch-envelope.json
the three fields, canonicalized — RFC 8785 emits one line, no whitespace
  {"payload_sha256":"cb039a35e2bcb613d0feca64b1ec06e10bae0bf83be9d9f539c3455ee69dc325","performer":"partner","task_type":"https://byom.example/task/exchange/v1"}

sha256 of those bytes   253d90e8a70eb7462395571eb37d0c486de8ee77ce1b0434274bce8e9a3c62e8   the staged digest — what the consent binds
its first 128 bits      stage-253d90e8a70eb7462395571eb37d0c48                             the stage_ref the driver is handed

So "the same bytes" is a precise claim: same payload, same recipient, same type ⇒ same reference, already_staged: true, and no second record. The preimage names the recipient performer; the same value travels the wire as recipient_label, which is why the receiver can re-derive this digest from the envelope alone.

What may be staged is decided by the dispatch envelope's own schema rather than by a rule written out a second time. "Acceptable to stage" and "acceptable to send" have one definition and cannot drift into a gap, so no consent receipt can ever be minted against bytes that turn out to be unsendable.

One-shot dispatch

The only operation on this surface with an effect. Read its three arguments as three separate jobs.

ArgumentIts job
stage_refWhich bytes.
consent_receiptThe operator's authority for exactly those bytes. Spendable once.
execution_keyNames one attempt, which is what makes a retry distinguishable from a replay.

The spend and its record commit in one store transaction, guarded by a uses < max_uses compare-and-set and a UNIQUE constraint on the receipt in the dispatch ledger — so a second dispatch cannot commit even if that compare-and-set were wrong, and the refusal survives a daemon restart.

What the driver doesWhat it gets
Re-sends the same execution_key A retry: the same dispatch_receipt with replayed: true, and nothing spent again. Carriage is re-attempted only if the row is not already sent — a terminal row has nothing left to carry, so the retry just returns what happened.
Sends a different key against a spent receipt A replay: 409 consent-spent.
Dispatches with no live receipt for that staging 409 consent-required.
Reuses a key already committed to other arguments 409 execution-key-conflict.

Everything that decides "this cannot leave" runs before the spend. A staging whose recipient is unnamed, un-introduced, suspended, or has no usable https endpoint is refused 409 unroutable-recipient; one whose members cannot form a conforming envelope is refused 500 bad-envelope. Both leave the consent receipt live. Burning a one-shot consent on a disclosure that provably cannot leave is the worst failure available to this surface, so it is closed by ordering rather than by care.

The envelope: a coordination dispatch is not a contract

The risk card the operator read named a payload, a recipient, and a task type. Akson's closed contract schema cannot carry that without contract terms the operator never saw — so the contract schema is not reused, and no terms are invented.

The bytes ride a dedicated object, spec/ext/coord-dispatch.v1.schema.json, carried as an A2A Part of media type application/vnd.akson-dev.coord-dispatch.v1+json beside the raw payload Part (application/octet-stream), over the same pinned mutual-TLS SendMessage POST every other peer-to-peer byte in akson already uses. Its members are schema_version, protocol, task_type, recipient_label, recipient_root, sender_root, payload_sha256, staged_digest and consent_receiptnine of them, all required, and every one either something the operator consented to or something the receiver needs in order to verify that fact:

MemberWhy it is there
schema_versionSo a receiver refuses rather than guesses.
protocolThe coordination protocol this envelope belongs to; a receiver that does not speak it refuses.
task_typeConsented to — the byom-owned type, carried uninterpreted.
recipient_labelConsented to, and one of the three preimages of the staged digest: without it the receiver could check the bytes but not the digest the consent binds.
recipient_rootSo a misrouted disclosure is refused even over a good channel.
sender_rootSo a claimed sender can never differ from the pinned one.
payload_sha256The payload, by the digest the receiver recomputes over what it actually read.
staged_digestThe exact value the consent receipt binds.
consent_receiptThe identity of the authorization — the id only, never the sealed body, and it confers nothing on the receiver.

The protocol member is the constant akson_byom_exchange_v1, and additionalProperties is false — a contract term cannot be added later without changing the ADR and the golden vectors. The vector proves that by presenting the exact terms a contract would have smuggled in and requiring the refusal: contract_id, deadline, deliverables, limits, objective and requested_capabilities.

The limit here

The envelope is not signed, and that is deliberate. Its authenticity is the channel's — mutual TLS pinned to the peer record on both sides — and its integrity is the digest chain above. Signing it would need a purpose key, and there is no key purpose for a coordination dispatch; borrowing the contract-proposal key to sign a non-contract is the one-key-one-role violation this codebase refuses everywhere else. The residual is named rather than hidden: a coordination dispatch is authenticated, not non-repudiable — a recipient cannot prove to a third party who sent it. A future decision that needs that adds the purpose; nothing here forecloses it.

The media type above is in the unregistered vnd.akson-dev tree. That is a development placeholder, and it is exactly why the release tooling refuses a stable tag and this is a prerelease: the wire types may still change.

The receiver checks four things

And answers one generic 422 coordination-refused otherwise. The reason is recorded locally — an operator has to be able to tell a tampered disclosure from a lost one — and never returned.

Before any of the four, the envelope is validated against its schema, so a malformed object is refused without a single member being interpreted. The four checks are what the specification enumerates, and they are what runs once the object is known to be an envelope at all:

  1. sender_root equals the root the mutual-TLS handshake actually authenticated, so a claimed sender can never differ from the pinned one.
  2. recipient_root equals its own root, so a misrouted disclosure is refused even over a good channel.
  3. SHA-256 over the payload bytes it actually read equals payload_sha256.
  4. The staged-digest derivation over {payload_sha256, recipient_label, task_type} reproduces staged_digest — which is why recipient_label is on the wire at all.

Swap the payload bytes under a consented digest and checks 3 and 4 both fail; aim a consented disclosure at a different peer and the sender's own pinned handshake means an impostor never receives the bytes, while a peer that is pinned refuses an envelope whose recipient_root is not its own.

Arrival is still not execution. An admitted disclosure creates no Task, no contract head, no work order, and nothing to approve. What it does create is an acknowledgement, one dispatch_received event, and the idempotency record that makes the acknowledgement replayable — nothing that can become work. A refusal is bounded the same way an admission is: it is committed through that same idempotency record under its own response class, so a peer re-sending a refused dispatch replays the identical 422 and appends no second event. Either way a peer pays one durable row per distinct request, which is exactly what an admitted dispatch costs.

The limit here

The receiver verifies and acknowledges, but does not retain the payload. Reading one back would require an inbound coordination operation that the registry deliberately does not have, and storage without a reader is an unbounded liability. So the inbound half of this surface waits for the decision that gives it a shape, and until then a disclosure that arrives here is checked, acknowledged, counted — and not kept.

Where the bytes are is a durable column

Not an inference from what happened, and not a guess after a crash. One column, whose value is pending, sent or failed, defaulted so that the honest answer is the one you get for free.

egress.stateWhat it means
pendingCommitted, nothing acknowledged. The schema default, so a crash between the commit and the send is honest by construction rather than by remembering to write something.
sentThe pinned recipient echoed this exact staged digest. Terminal — and the only state that claims delivery.
failedAttempted and refused, or timed out. Retryable: re-presenting the same execution_key re-attempts carriage and spends nothing.
pending schema default sent terminal — digest echoed failed retryable the pinned recipient acknowledged this staged digest refused · timed out retry: same execution_key, nothing spent no fourth state for "attempted, outcome unknown": sent is the only state that claims delivery, and everything short of it is re-attemptable — precisely what the driver needs to know
A timed-out attempt is failed, never sent: nothing echoed the staged digest, and this endpoint does not report bytes as having left when that is unknown.

Carriage is bounded at every stage, and a bounded carriage does not deny the surface. A recipient that accepts the connection and then stays silent would otherwise hold the control-socket thread dispatch runs on. So each stage of the outbound POST has its own ceiling — name resolution 5s, the TCP connect 10s, the TLS handshake 10s, and the request/response exchange 30s, bounding one attempt at 55s. A timed-out attempt lands in failed with egress.detail naming the stage (urn:akson:error:peer-timeout), which is a state the ordinary retry resumes. The timeout bounds how long an attempt lasts; the socket's concurrency bounds who waits for it.

Recovery is the driver's retry, not a background sweeper — and a re-send is byte-identical. The recipient's idempotency covers the body digest as well as the message id, so the A2A body is emitted as RFC 8785 canonical JSON; serialising the request struct directly would not have that property, and the retry would arrive as a different request under the same message id and be refused. One consent therefore yields exactly one dispatch record and at-least-once carriage of it, deduplicated at the recipient by the dispatch receipt.

What this surface does not give you

Each of these is a real limit today, which is why it is stated beside the capability it bounds above as well as collected here.

Named, not rounded away

The assurance profile is developer. There is no UID separation in the default profile, no attested process identity, and no asymmetric endpoint identity on this surface. Admission is SO_PEERCRED — which user connected, never which program. The separate-identity fleet profile is opt-in, and no systemd-analyze security score is claimed for its units because that tool has never produced a number for them.

The envelope is authenticated but not non-repudiable. It is unsigned, deliberately: authenticity is the pinned channel's and integrity is the digest chain, but a recipient cannot prove to a third party who sent it.

The receiver does not retain the payload. It verifies, acknowledges, records one event, and keeps nothing — because the registry has no inbound op to read one back with, and storage without a reader is an unbounded liability.

The payload media types are unregistered (vnd.akson-dev). That is why a stable release is refused and this is a prerelease.

Key custody is interim. The master secret and the data-encryption key live in a file, and store rollback is undetectable today. That bounds every durable claim on this page — the consent ledger and the dispatch ledger included.

An unrouted staging is still stageable and consentable. It simply cannot be dispatched, and that is caught before the spend rather than after it.

And the plain ceilings, since a driver hits them. A staged payload is at most 512 KiB (413 payload-too-large) and a request line at most 1 MiB (413 request-too-large); events_read returns 64 events by default and 256 at most. This surface moves coordination bytes, not bulk data.

The two-process interop scenario harness/interop/scenario-coord-dispatch.sh runs this whole chain across real sockets between two daemons: stage, refuse consent on coord, consent on admin, dispatch, and refuse the replay. Each step is a request on the socket that owns it, so the surface gate is something the scenario can fail on rather than something it asserts. Golden vectors for every op — request wire, reply field set, the staged digest and its idempotency, the envelope's wire shape, the cursor encoding, and every refusal body — live in spec/vectors/coordination/ and are re-derived independently.