Docs menu
Extensions

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

1

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.

2

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.

3

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.

4

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.

The two capsules are joined by a shared 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:

DispositionMeans
delivery_unconfirmedThe 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_timeoutDelivery was confirmed, but the performing party didn't co-sign within the request's validity window.
countersign_refusedThe 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

LayerRelationship
Agent Action CapsuleCarries 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 receiptsA request attestation may reference a delegation receipt authorizing the requesting agent.
SCITT transparency logWitnessing 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: BilateralHandshake state machine, canonical payload functions, and seal_request / seal_action / seal_bilateral capsule 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
The demo uses HMAC-SHA256 with symmetric keys for simplicity. Production deployments should use asymmetric signatures (Ed25519 or equivalent) with keys bound to verifiable organizational identity.