SpecificationChangelogGovernanceFormal modelv1.0v1.1v1.2v1.3Raw markdown

Frozen snapshot — Zanii Protocol 1.1 (2026-09-29). Kept verbatim so any receipt can be checked against the exact rules it was written under. The current specification is at /protocol; what changed between versions is in CHANGELOG.md.

Zanii Protocol Spec — v1 (Phase 0)

Verifiable identity and proof-of-action for AI agents. This document is normative for @zanii/core. Anything not specified here is implementation detail.

1. Conventions

2. Delegation certificate

Proves an identity authorized another identity to act, within scopes, until expiry.

{
  "v": 1,
  "issuer": "did:key:z6Mk...",
  "subject": "did:key:z6Mk...",
  "scopes": ["stripe.*", "gmail.send"],
  "exp": "2026-08-01T00:00:00.000Z",
  "sig": "ed25519:..."
}

Scopes

A scope is either an exact string (gmail.send) or a wildcard prefix (stripe.*). a.* covers a, a.b, a.b.c, and a.*. * covers everything.

Delegation chain

An ordered array of certs, root first: org → team → agent.

A chain is valid for agent A at time t against expected root owner O iff:

  1. Every cert's sig verifies against its issuer DID.
  2. chain[0].issuer === O (when the verifier pins an expected owner).
  3. chain[i+1].issuer === chain[i].subject for all links.
  4. chain[last].subject === A.
  5. Every cert satisfies t < exp.
  6. No cert's hash appears in the verifier's revocation set.

Effective scopes = the last cert's scopes, filtered to those covered by every ancestor cert's scopes (a delegate can only narrow, never widen).

3. Action receipt

One signed record per agent action. The payload itself is never logged — only its hash — so proofs leak no data.

{
  "v": 1,
  "agent_id": "did:key:z6Mk...",
  "delegation": [ /* cert chain, root first */ ],
  "action": "tool_call",
  "target": "stripe.charges.create",
  "payload_hash": "sha256:...",
  "ts": "2026-07-02T16:00:00.000Z",
  "prev": "sha256:... | null",
  "sig": "ed25519:..."
}

4. Receipt verification

A receipt R verifies at time t iff:

  1. R.sig verifies against the public key in R.agent_id.
  2. R.delegation is a valid chain for R.agent_id (§2), at R.ts.
  3. R.target is covered by the chain's effective scopes.
  4. When verifying a sequence: R.prev equals the hash of the preceding receipt.

Verification is pure — no network, no trusted party. Phase 1 adds a fifth check: Merkle inclusion proof against a signed (and later chain-anchored) tree head.

5. Transparency log (Phase 1)

An append-only Merkle tree over receipts, per RFC 6962 / RFC 9162:

Signed Tree Head (STH)

{
  "v": 1,
  "log_id": "did:key:z6Mk...",
  "size": 12345,
  "root": "sha256:...",
  "ts": "2026-07-02T16:00:00.000Z",
  "sig": "ed25519:..."
}

sig = Ed25519 by the log's key over JCS of the STH without sig. The log key's DID is the log's identity; Phase 2 anchors root to a public chain.

Proofs

A receipt is fully proven when: it verifies per §4, its leaf has a valid inclusion proof against an STH, and the STH signature verifies against the log's DID.

Log API (v1)

6. Revocation (Phase 2)

A revocation record kills a delegation certificate, in-log so the revocation time itself is provable:

{
  "v": 1,
  "type": "revocation",
  "cert": { /* the full delegation cert being revoked */ },
  "ts": "2026-07-02T16:00:00.000Z",
  "sig": "ed25519:..."
}

7. Chain anchoring (Phase 2)

Periodically, the log publishes its current STH outside its own control so that not even the log operator can rewrite history:

8. Audit bundles (Phase 3)

GET /v1/export/:agentDid produces a self-contained, offline-verifiable export of one agent's complete history:

{
  "v": 1,
  "bundle_type": "zanii-audit-bundle",
  "generated_at": "...",
  "log_id": "did:key:...",
  "agent_id": "did:key:...",
  "sth": { /* STH at export time */ },
  "receipts":    [{ "receipt": {}, "hash": "", "index": 0, "proof": [] }],
  "revocations": [{ "revocation": {}, "cert_hash": "", "index": 0, "proof": [] }],
  "anchors":     [{ /* anchor record */ , "consistency_proof": [] }]
}

anchors holds the anchors the bundle needs, not every anchor the log ever made: for each receipt, the earliest anchor covering it (sth.size > index — the evidence that the receipt existed by that anchored time), plus the latest anchor (the bundle tree is an append-only extension of it). Every anchor record at a kept size is included, so dual-backend anchors of one STH travel together. Verifiers check the anchors present; they do not require completeness of the log's anchor history.

A bundle verifies iff, offline: the STH signature is valid; every receipt verifies per §4, hashes to its hash, belongs to agent_id, and has a valid inclusion proof against the STH; the prev chain is complete from null with no gaps; every revocation record verifies per §6 with a valid inclusion proof; and every anchor's STH verifies with a valid consistency proof to the bundle STH (so the bundle tree is an append-only extension of everything anchored). Reference implementation: verifyAuditBundle() in @zanii/core (pure).

9. Cross-org co-signed receipts (Phase 4)

When agent A (org X) interacts with agent B (org Y), neither org's word should be the record. An A2A receipt is one statement signed by both:

{
  "v": 1,
  "type": "a2a",
  "action": "tool_call",
  "target": "quotes.request",
  "payload_hash": "sha256:...",
  "ts": "2026-07-02T18:00:00.000Z",
  "initiator": { "agent_id": "did:key:...", "delegation": [], "prev": null, "sig": "ed25519:..." },
  "responder": { "agent_id": "did:key:...", "delegation": [], "prev": null, "sig": "ed25519:..." }
}

Valid iff: both signatures verify against their agent_id keys; both delegation chains are valid at ts and terminate at their respective agents; target is within both parties' effective scopes; the parties are distinct.

Chains: the A2A receipt hash enters both agents' hash chains — each party's prev is that agent's prior receipt hash, and each agent's next receipt points back to the A2A hash. The log verifies both prevs against its heads on ingest (POST /v1/interactions) and advances both heads atomically.

Exchange flow: initiator sends a draft (its half of the body); responder completes the body with its identity/delegation/prev and returns its signature; initiator verifies its half is unchanged, signs, and submits. The log is the settlement point — both parties re-sync their chain heads from it.

10. Reputation & payments (Phase 4)

11. Selective disclosure — payload field commitments (client convention)

An optional, client-side convention (@zanii/redact / zanii.redact) for revealing individual payload fields without disclosing the rest. The log is unaffected — it only ever sees the receipt payload, which here is a commitment envelope. Format id: sd-v1.

Commit. Given fields {k: v}: - Sort keys. For each key k, draw a random 16-byte salt_k (hex) and compute a leaf L_k = LeafHash( JCS(["zr-field-v1", k, v, salt_k]) ) — the RFC 6962 leaf hash (§5) of the RFC 8785 canonical bytes (§1) of that 4-element array. - root = MerkleRoot([L_k for k in sorted keys]) (RFC 6962, §5). - The receipt payload is the envelope {"_zr":"sd-v1", "root":"sha256:<hex>", "keys":[sorted keys]}. It is hashed and recorded like any payload (§3), so the ledger binds root immutably. keys discloses field names only, never values.

Disclose. To reveal field k at index i: publish {"_zr":"sd-disclosure-v1", "key":k, "value":v, "salt":salt_k, "index":i, "size":n, "proof":[audit path], "root"}, where proof is the RFC 6962 inclusion proof for leaf i (§5).

Verify. Recompute L_k from (k, v, salt_k); check keys[i] == k and size == len(keys); verify the inclusion proof of L_k at (i, size) against root; and confirm root is the payload root of a receipt whose payload hash is in the log (§4). Undisclosed fields reveal nothing — each leaf is independently salted.

This is a client convention, not a change to the log or receipt format (an envelope is an ordinary payload). @zanii/retention uses the same salted-commitment idea, SHA256(salt ‖ subject), for a single subject value.

12. Owner confirmation of irreversible actions (Article VI)

An action_confirmation is cryptographic evidence a human owner approved a specific irreversible action — replacing an in-memory "yes" that proves nothing.

{
  "v": 1,
  "type": "action_confirmation",
  "owner_id": "did:key:z... (the owner)",
  "action_hash": "sha256:... (JCS hash of {tool, args})",
  "confirmed_at": "2026-07-12T00:00:00Z",
  "sig": "ed25519:... (owner over JCS without sig)"
}

13. Infrastructure co-signatures — segregation of duties (Connect)

The agent that performs a task must not be the sole party reporting it. Connect — the deterministic rails that are the only door to the outside world — holds its own did:key and co-signs every receipt.

{
  "v": 1,
  "type": "receipt_cosign",
  "cosigner_id": "did:key:z... (Connect / the rails)",
  "receipt_hash": "sha256:... (the agent receipt)",
  "agent_id": "did:key:z... (the doer)",
  "cosigned_at": "2026-07-12T00:00:00Z",
  "sig": "ed25519:... (Connect over JCS without sig)"
}

14. N-party (swarm) receipts — agent teams (Phase 4)

A task performed by a team of 3+ agents is recorded as one swarm receipt: an M-of-N threshold of the declared participants co-sign a shared body. This is genuine threshold crypto — every counted signer signs the same canonical body — not a chain of 2-party co-signatures.

Receipt shape.

{ "v": 1, "type": "swarm",
  "body": { "v":1, "type":"swarm", "task", "target", "payload_hash"?, "ts",
            "participants": [did …],   // distinct, sorted; N ≥ 3
            "threshold": M },          // 1 ≤ M ≤ N
  "signers": [ { "agent_id", "delegation": [cert …], "prev", "sig" } … ] }

The signed bytes are JCS(body) — signers attest the shared statement and bring their own authority proof in the envelope (delegation, prev), which is not part of the signed bytes. sig is ed25519:<hex> over JCS(body).

Verification (a signature is not authority). For each listed signer, distinct and ∈ participants: (1) sig verifies against agent_id; (2) the delegation chain is valid, owner-rooted, unexpired, and unrevoked at ts; (3) body.target is within the chain's effective scopes; (4) prev equals the signer's current log head. Then the count of valid signers MUST be ≥ threshold. By default the counted signers MUST root at ≥ threshold distinct owners (segregation of duties — one owner's N agents cannot satisfy an M-of-N); this may be relaxed per receipt.

Attribution. Only actual signers are attributed the action (indexed, chain-advanced, counted for reputation) — a declared but non-signing participant is bound by nothing.

Ingest. POST /v1/swarm { receipt }. Merkle inclusion, the STH, and on-chain anchoring cover a swarm receipt exactly as any other leaf. The client primitive ships in @zanii/swarm / zanii.swarm (offline sign/verify); server verification is verify_swarm_receipt.

15. Explicitly out of scope so far

A pending-state protocol for interleaved A2A/swarm and solo traffic (currently: settle via the log, then re-sync heads). In-dashboard revocation (revocation requires the issuer's key, which the server must never hold).