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.
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.
| Socket | Admits | Mode | For |
|---|---|---|---|
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. |
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.
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:
op | What it does |
|---|---|
coord_whoami | This 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_show | One 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. |
stage | Inert, 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_show | A staged payload's status and digests, plus its consent once an operator has minted one. |
dispatch | The 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_status | Whether 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_read | The durable cursored coordination feed. Cursors are opaque: one that did not come from a reply is refused 400 bad-cursor. |
capability_evidence | A 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.
Consent stays on admin
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.
$ 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 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.
| Argument | Its job |
|---|---|
stage_ref | Which bytes. |
consent_receipt | The operator's authority for exactly those bytes. Spendable once. |
execution_key | Names 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 does | What 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_receipt
— nine of them, all
required, and every one either something the operator consented to or something the
receiver needs in order to verify that fact:
| Member | Why it is there |
|---|---|
schema_version | So a receiver refuses rather than guesses. |
protocol | The coordination protocol this envelope belongs to; a receiver that does not speak it refuses. |
task_type | Consented to — the byom-owned type, carried uninterpreted. |
recipient_label | Consented 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_root | So a misrouted disclosure is refused even over a good channel. |
sender_root | So a claimed sender can never differ from the pinned one. |
payload_sha256 | The payload, by the digest the receiver recomputes over what it actually read. |
staged_digest | The exact value the consent receipt binds. |
consent_receipt | The 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 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:
sender_rootequals the root the mutual-TLS handshake actually authenticated, so a claimed sender can never differ from the pinned one.recipient_rootequals its own root, so a misrouted disclosure is refused even over a good channel.- SHA-256 over the payload bytes it actually read equals
payload_sha256. - The staged-digest derivation over
{payload_sha256, recipient_label, task_type}reproducesstaged_digest— which is whyrecipient_labelis 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 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.state | What it means |
|---|---|
pending | Committed, 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. |
sent | The pinned recipient echoed this exact staged digest. Terminal — and the only state that claims delivery. |
failed | Attempted and refused, or timed out. Retryable: re-presenting the same execution_key re-attempts carriage and spends nothing. |
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.
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.