Bilateral Attestation
When two organizations act together across a trust boundary, one record — owned by one side, deniable by the other — isn't evidence. Bilateral attestation gives each organization proof of the other's commitment, verifiable by a third party who trusts neither.
The problem it solves
Each side keeps its own log. A dispute reduces to my log versus your log. Neither log carries the other party's agreement; either side can later claim the terms were different. The record exists, but it gives a relying party nothing to check independently.
Bilateral attestation replaces that with a four-move exchange that produces a record both parties have signed, each party holds proof of the other's signature, and the combined record can be witnessed so a third party who trusts neither organization can verify it end-to-end.
The four moves
Request attestation
The requesting party signs a statement over the action and its material terms, naming the intended performer. The requester is now bound: it cannot later deny having asked, or having asked on these terms.
Constraint evaluation
The performing party verifies the requester's organizational identity and evaluates the request against deterministic constraints at the boundary where the action takes effect — not at the transport edge. No verified request attestation, no consequential action.
Action attestation
The performing party signs a statement referencing the request by digest, recording the constraint results and the disposition — a verdict_class such as executed, blocked, denied, escalated or timeout (the full list). Every outcome is a first-class record; refusals carry the same fidelity as performance.
Acknowledgment
Each party acknowledges the other's attestation. When both acknowledge, each is bound to its part and holds proof of the other's. The record is complete.
action_digest — SHA-256 of the canonicalized action — so a third party who receives both can confirm they describe the same action without either org having to reveal the action's content.Refusals are first-class
A declined request is a completed exchange, not a failed one. The action attestation records that the request was declined and on what constraint basis. The requester's acknowledgment is verifiable evidence contradicting any later claim that the request was never answered.
For the performing party, a bilaterally-acknowledged refusal is stronger evidence than a unilateral one — the counterparty that was refused has signed the record. For the requesting party, a pattern of acknowledged refusals is now checkable by its counterparties.
Graceful degradation
Counterparties will be of mixed capability. A performing party whose counterpart cannot produce request attestations may proceed under policy, producing its own action attestation unilaterally and recording a reduced-assurance indicator in place of the missing attestations. The record format is the same; the assurance marking differs. A fully-bilateral record and a degraded record are never confusable.
A refusal to act vs. a failure to co-sign
The draft distinguishes two things this page's "refusals are first-class" section could otherwise blur. A declined action (the section above) is the performing party evaluating the request and choosing not to act — a completed exchange, and strong evidence because the refused counterparty signed it. A failure to co-sign is different: the exchange itself didn't complete symmetrically. Three narrower asymmetry dispositions, kept distinct from the action-disposition vocabulary above, cover that case:
| Disposition | Means |
|---|---|
delivery_unconfirmed | The requesting party emitted its attestation, but delivery to the performing party couldn't be confirmed — the weakest outcome; the counterparty may never have received the request. |
counterparty_timeout | Delivery was confirmed, but the performing party didn't co-sign within the request's validity window. |
countersign_refused | The performing party was reached and explicitly refused to co-sign. |
These MUST NOT be conflated with a declined action: a decline is a performed boundary decision the requester acknowledges, while a failure to co-sign is a failure of the exchange, not a decision about the action — only the latter distinction matters when weighing an anchored-but-unacknowledged record. A half that can't be matched to its counterpart is an orphan — a defined state, not an error, and still admissible evidence that the attempt was made.
What it composes with
| Layer | Relationship |
|---|---|
| Agent Action Capsule | Carries the attestations. The action disposition is the capsule's own verdict_class registry, reused as is — the same disposition list the rest of this site uses. |
| Delegation receipts | A request attestation may reference a delegation receipt authorizing the requesting agent. |
| SCITT transparency log | Witnessing both capsules makes the record verifiable by parties who trust neither organization. |
| Remote attestation (RATS) | RATS attests platform state; bilateral attestation attests actions. Orthogonal layers that compose. |
The Internet-Draft
The bilateral attestation exchange is specified as a companion Internet-Draft to the Agent Action Capsule profile:
draft-mih-agent-bilateral-attestation ↗ — always resolves to the current revision on the Datatracker; as of this writing, -02.
The draft fixes the exchange, the binding obligations, and the disposition semantics. Wire encodings are left to a future revision. What the draft already fixes is how canonicalization works: each attested object must declare its canonicalization by identifier, inside the bytes its signature covers, and a verifier must apply the declared construction rather than infer one. The expected construction is JCS (RFC 8785). This is the same rule the Canonical Payload Binding defines, and the same JCS a capsule already declares (canonicalization_id: "jcs"). JCS is the construction; CPB is how a record names it.
Open-source implementation
A reference implementation is available in two packages:
- capsule-emit —
capsule_emit.bilateral:BilateralHandshakestate machine, canonical payload functions, andseal_request/seal_action/seal_bilateralcapsule helpers. - agent-action-capsule —
agent_action_capsule.bilateral:verify_pair(capsule_a, capsule_b)— checks individual Class-1 verify, shared action digest, and chain linkage.
# Run the four-move demo (HMAC-SHA256 demo signer)
pip install capsule-emit agent-action-capsule
git clone https://github.com/action-state-group/capsule-emit
cd capsule-emit
python examples/bilateral/demo.py --no-anchor
# Verify the capsule pair
from agent_action_capsule.bilateral import verify_pair
pvr = verify_pair(capsule_a, capsule_b)
assert pvr.ok
assert pvr.shared_digest == action_digest