Operations
Submit an operation, receive a signed decision — canonical CBOR on the wire, a nullifier that makes replay a refusal, and no policy anywhere in the transport.
scsa-operationd is the network face of the authority contract. It binds
127.0.0.1:8080 by default and speaks canonical CBOR, not JSON:
scsa-operationd [ADDR:PORT] [STATE_DIR]Give it a STATE_DIR in any deployment you care about — see the note on
nullifier durability below.
Early access. The wire format is specified and stable within a schema version, but schema versions may still advance. Run the service behind your own gateway and authentication.
Why CBOR and not JSON
The bytes on the wire are the same bytes the offline verifier reads and the transparency log commits to. That only works if there is exactly one valid encoding of every object — deterministic CBOR per RFC 8949 §4.2.1: definite lengths, shortest-form integers, sorted map keys, no duplicate keys, no trailing bytes. A decoder that accepted a second encoding of the same object would let an attacker present a signature over bytes the verifier never saw.
The decoder is strict in both directions. Round-trip tests assert that decoding and re-encoding reproduces the exact input bytes, and unknown fields are an error rather than something to skip — a verifier that silently ignores a field cannot claim it saw everything the signer signed.
JSON appears at exactly two read-only endpoints, is labelled an operator convenience in its own payload, and is never the signed representation of anything.
Endpoints
| Method | Path | In | Out |
|---|---|---|---|
GET | /healthz | — | JSON liveness |
GET | /readyz | — | JSON readiness; 503 if the governance gate did not clear at startup |
POST | /v1/sessions | — | JSON session offer |
POST | /v1/operations | canonical CBOR OperationIntent | canonical CBOR DecisionEnvelope |
GET | /v1/operations/{intent_digest} | — | canonical CBOR DecisionEnvelope |
GET | /v1/operations/{intent_digest}/explain | — | JSON, operator only |
GET | /v1/operations/{intent_digest}/bundle | — | canonical CBOR EvidenceBundle (audit scope) |
GET | /v1/operations/{intent_digest}/recipient-bundle | — | canonical CBOR EvidenceBundle, including seat contributions |
GET | /v1/nullifiers/{nullifier} | — | canonical CBOR DecisionEnvelope |
GET | /v1/governance | — | JSON, operator only |
A refusal is a 200
POST /v1/operations returns 200 for every decision, including a refusal.
A refusal is signed, journaled, published, and checkable offline years later —
it is the product, not an error.
HTTP status codes in the 4xx and 5xx ranges are reserved for failures of transport: a body that is not canonical CBOR, a route that does not exist, a service that is not ready. Mapping a refusal code onto a 4xx would invent a second, unsigned, unloggable refusal vocabulary shadowing the real one — and the two would drift.
The service enforces this structurally rather than by convention. The HTTP handlers cannot name a policy, a world view, a governance assessment, a refusal code, an outcome, a quorum or a constitution; they are handed bytes that are already a decision. A refusal and a completion leave the module through the same line of code.
The flow
POST /v1/sessionsReturns a session offer: the coordinator's identity, the constitution digest it is running under, and the freshness window your intent must fall inside.
POST /v1/operations
Content-Type: application/cborThe body is a canonical OperationIntent. Its digest is the operation's name for
the rest of its life — every later lookup uses it.
The response is a canonical DecisionEnvelope: signed, and already committed to
the transparency log if it completed. Read
Decisions and refusals for what is inside it, and
Offline verification for how to check it without
trusting the service that produced it.
Nullifiers: replay is a refusal, not a second effect
Every intent carries a nullifier — a value derived so that the same logical operation cannot be authorized twice. The coordinator reserves it before any contribution is gathered.
Submit the same operation again and you do not get a second effect. You get the
original decision envelope, or a refusal carrying NULLIFIER_SPENT (400).
GET /v1/nullifiers/{nullifier} looks the decision up directly.
This property depends on the nullifier registry being durable. A restart that freed every nullifier would reopen every replay window the nullifier exists to close, so a real deployment must configure directory-backed storage, not the in-memory default. The restart case is covered by a test that spends a nullifier, restarts the process, and asserts it is still spent.
Single-node durability is durability against process restart. It is not durability against loss of the node's disk, and the service does not pretend otherwise.
Readiness and the governance gate
/readyz is not a liveness check with extra steps. It reports whether the
governance gate cleared at startup — whether the compiled policy could be
shown to be no weaker than the constitution, and whether the deployment's
quorum, diversity and algorithm floors are actually met by the seats present.
If it did not clear, the endpoint returns 503 with the gate's own account of which axis was weakened. A deployment that cannot justify its own configuration does not serve traffic.
Overview
The Authority Fabric — an operation contract in which no permitted operation completes unilaterally, and every outcome is portable evidence.
Decisions and refusals
Every outcome is a signed envelope with a stable code — and "we could not tell" is a third state that must never be reported as a denial.