Skip to content

contracts

Envelope, payload models, signing, scope and schema validation.

coalitionkit_contracts

Contracts for the Coalition Protocol v0.1: models, JCS, signing, IDs and schema validation.

NotADeciderError

Bases: ValueError

The answering participant is not named for this subject (not_a_decider).

Envelope

Bases: WireModel

parse_payload
parse_payload() -> WireModel

Validate the payload against the model for this envelope's type.

StoredSubmission dataclass

What the host keeps per accepted event: who sent it and the digest of what they sent.

DecisionExpiredError

Bases: ValueError

The decision arrived after its subject's decision_deadline (deadline_exceeded).

LogIntegrityError

Bases: Exception

The log does not match its genesis, its own chain or a receipt the host signed.

LogStub dataclass

A replay entry for an event outside the reader's scope: only its place in the chain.

OwnerKeyConflictError

Bases: ValueError

The owner key is already pinned to another participant (wire error not_authorized).

StaleSubjectError

Bases: ValueError

The decision names a revision that is not the subject's current one (stale_subject).

Membership dataclass

Who the members are and who holds each role, at derivation time.

ScopeSource dataclass

One source event of a derived output: its scope and the owner of its sender.

projected says the output is built from the source's redacted projection only (its redact paths are already removed from the data the host read), not from the original.

SignatureError

Bases: Exception

The signature is malformed or does not verify.

SchemaValidationError

Bases: Exception

The data fails its JSON Schema. errors lists one line per problem.

canonicalize

canonicalize(value: Any) -> bytes

JCS bytes of a JSON value. Integers outside +-(2**53 - 1) are refused by rfc8785.

prev_hash

prev_hash(previous: Envelope | Mapping[str, Any]) -> str

SHA-256 hex of the JCS form of the previous accepted envelope (all fields, sig too).

consent_payload

consent_payload(
    coalition_id: str,
    decide: Mapping[str, Any],
    *,
    participant: str,
) -> bytes

The JCS bytes the owner signs. decide is a coalition.decide payload.

sign_consent(
    coalition_id: str,
    decide: Mapping[str, Any],
    owner_key: Ed25519PrivateKey,
    kid: str,
    *,
    participant: str,
) -> str

The consent_sig value for a decision (any consent_sig already in it is ignored).

verify_consent(
    coalition_id: str,
    decide: Mapping[str, Any],
    owner_public_key: Ed25519PublicKey,
    *,
    participant: str,
    expected_kid: str | None = None,
) -> str

Verify decide['consent_sig'] against the pinned OWNER key only. Returns the kid.

verify_decision

verify_decision(
    envelope: Envelope | Mapping[str, Any],
    agent_public_key: Ed25519PublicKey,
    owner_public_key: Ed25519PublicKey,
) -> None

A decision counts only when the agent signed the envelope AND the owner signed the consent.

Raises SignatureError otherwise.

record_answer

record_answer(
    answers: Mapping[str, T],
    named: Collection[str],
    decider: str,
    consent: T,
) -> tuple[dict[str, T], bool]

Record one decider's answer for one (subject, revision).

Returns the new answers and whether this answer was recorded. A second answer from the same decider is not recorded (the first one wins) and recorded is False.

tally

tally(
    named: Collection[str], answers: Mapping[str, bool]
) -> State

refused on any refusal, decided when every named decider consented, else pending.

tally_choice

tally_choice(
    named: Collection[str],
    answers: Mapping[str, tuple[bool, str]],
) -> tuple[ChoiceState, str | None]

Tally a subject whose consents each name a choice (a tension_option).

answers maps a decider to (consent, choice). Returns the state and the chosen option: refused on any refusal; split when two consents name different options (no option has every decider, so the tension is replanned); decided when every named decider consented to the same option; otherwise pending.

classify_submission

classify_submission(
    original: StoredSubmission | None,
    authenticated_sender: str,
    envelope: Envelope | Mapping[str, Any],
) -> Outcome

new (nothing stored), replay (return the original acknowledgement) or mismatch.

mismatch is answered with duplicate_mismatch and discloses nothing about the original: the same answer for a different sender and for a different envelope.

idempotency_key

idempotency_key(
    envelope: Envelope | Mapping[str, Any],
) -> tuple[str, str]

The lookup key: (coalition_id, event_id).

submission_digest

submission_digest(
    envelope: Envelope | Mapping[str, Any],
) -> str

SHA-256 hex of the JCS form of the envelope without the host fields (sig included).

is_ulid

is_ulid(value: object) -> bool

True when value matches the ULID pattern used by the JSON Schemas.

new_ulid

new_ulid(
    *,
    now_ms: int | None = None,
    entropy: bytes | None = None,
) -> str

Create a ULID. now_ms and entropy (10 bytes) exist for tests.

ulid_timestamp_ms

ulid_timestamp_ms(value: str) -> int

The millisecond timestamp encoded in a ULID.

accepts

accepts(status: TaskStatus, action: Action) -> bool

True when a message of this kind about a task in status passes the status check.

after_planning_start

after_planning_start(status: TaskStatus) -> TaskStatus

Status after the host's first query, proposal or tension names the task.

after_replan

after_replan(status: TaskStatus) -> TaskStatus

Status after a refusal, a withdrawn commitment, a ripple or a decision expiry.

check_decision_window

check_decision_window(
    decision_deadline: str, accepted_at: str
) -> None

Raise DecisionExpiredError when a decision is accepted after the deadline.

decision_expired

decision_expired(decision_deadline: str, at: str) -> bool

True when at (the host's acceptance time, or its clock) is after the deadline.

may_create_commitment

may_create_commitment(
    states: Collection[CommitmentState],
) -> bool

One active commitment per task: creation needs none of the task's commitments active.

supersede_active

supersede_active(
    states: Mapping[str, CommitmentState],
) -> dict[str, CommitmentState]

The commitments of a task after a replan: every active one becomes superseded.

check_receipts

check_receipts(
    entries: Sequence[Entry],
    receipts: Sequence[ReceiptInfo],
    host_public_key: Ed25519PublicKey,
    *,
    expected_kid: str | None = None,
) -> None

Compare a replayed log with the receipts a member kept.

truncated: the log ends before a receipt's seq. receipt_mismatch: the entry at a receipt's seq hashes to something else (an edited tail, even one with a re-linked chain). Projections are verified against the pinned host key first, as in verify_chain.

entry_hash

entry_hash(entry: Entry) -> str

The hash the next entry must carry as prev_hash.

A coalition.projection is refused here: its own envelope hash is not the chain hash. Turn it into a stub with verify_projection first.

genesis_hash

genesis_hash(
    coalition_id: str, host_card_url: str, created_at: str
) -> str

The prev_hash of seq 1.

projection_stub

projection_stub(
    projection: Envelope | Mapping[str, Any],
) -> LogStub

The stub {seq, prev_hash, hash} a coalition.projection carries for its source.

receipt_payload

receipt_payload(
    coalition_id: str,
    accepted: Envelope | Mapping[str, Any],
) -> dict[str, Any]

The payload of the receipt for an accepted envelope (which carries its host fields).

verify_chain

verify_chain(
    entries: Sequence[Entry],
    genesis: str,
    host_public_key: Ed25519PublicKey,
    *,
    first_seq: int = 1,
    expected_kid: str | None = None,
) -> int

Check seq is gap-free from first_seq and each prev_hash links to the one before.

genesis is the expected prev_hash of entries[0] (the genesis hash for a full replay). Each coalition.projection entry is first verified against the pinned host key (SignatureError if forged) and stands in as its source's stub. Returns the head seq (first_seq - 1 for an empty log).

verify_projection

verify_projection(
    projection: Envelope | Mapping[str, Any],
    host_public_key: Ed25519PublicKey,
    *,
    expected_kid: str | None = None,
) -> LogStub

Verify a projection against the pinned host key and return the stub it carries.

A replay holding projections checks the chain with these stubs, never with the projection's own envelope hash.

verify_receipt

verify_receipt(
    receipt: Envelope | Mapping[str, Any],
    host_public_key: Ed25519PublicKey,
    *,
    accepted: Envelope | Mapping[str, Any] | None = None,
    expected_kid: str | None = None,
) -> ReceiptInfo

Verify a receipt envelope against the pinned host key and, if given, the accepted envelope.

Raises SignatureError for a bad signature and LogIntegrityError for a receipt that does not describe accepted.

check_owner_key_unique

check_owner_key_unique(
    pinned: Mapping[str, str],
    participant: str,
    owner_key_thumbprint: str,
) -> None

Raise when this owner key is already pinned to a different participant.

pinned maps participant ID to the thumbprint of its pinned owner key, for one Coalition.

jwk_thumbprint

jwk_thumbprint(jwk: Mapping[str, Any]) -> str

RFC 7638 SHA-256 thumbprint (base64url, no padding) of an Ed25519 (OKP) JWK.

jwk_to_public_key

jwk_to_public_key(
    jwk: Mapping[str, Any],
) -> Ed25519PublicKey

The public key in an Ed25519 JWK. Raises SignatureError when it is not one.

public_key_to_jwk

public_key_to_jwk(
    key: Ed25519PublicKey, kid: str
) -> dict[str, str]

An Ed25519 public key as the JWK shape of common.json#/$defs/ed25519_jwk.

sign_owner_proof

sign_owner_proof(
    coalition_id: str,
    participant: str,
    card_url: str,
    owner_key: Mapping[str, Any],
    owner_private_key: Ed25519PrivateKey,
) -> str

The owner_proof for a join; the JWS kid is the JWK's kid.

verify_owner_proof

verify_owner_proof(
    join: Envelope | Mapping[str, Any],
    *,
    agent_public_key: Ed25519PublicKey | None = None,
) -> Ed25519PublicKey

Check a coalition.join envelope's owner proof and return the key to pin.

The proof must verify under the key it carries, bind this Coalition, participant (the envelope's on_behalf_of) and card, and the owner key must not be the agent's own key. Raises SignatureError on any failure.

delivery_form

delivery_form(
    *, in_scope: bool, redacted: bool, viewer_specific: bool
) -> Form

The form of an event for one recipient, also used for the sender's acknowledgement.

A recipient outside the event's scope gets nothing but the receipt. Inside the scope, an event with a field redacted for the recipient, or one that is viewer-specific, is a projection; any other event is delivered as the original.

check_revision

check_revision(
    decided_revision: int, current_revision: int
) -> None

Compare as the host does inside the recording transaction. Older or newer both fail.

is_superseded

is_superseded(
    request_revision: int, current_revision: int
) -> bool

A pending consent request for an older revision is superseded by a change.

next_revision

next_revision(current: int) -> int

The revision after any change to an object.

principals

principals(
    source: ScopeSource, membership: Membership
) -> frozenset[str]

The resolved principal set of one source: the members who may see it.

resolve_narrowest

resolve_narrowest(
    sources: Sequence[ScopeSource], membership: Membership
) -> Scope

The scope a derived output takes: the intersection of its sources' principal sets.

Roles resolve to the members holding them now, self to the source sender's owner. An empty intersection becomes host. A source with a non-empty redact list that is not projected makes the output host (no redaction path survives a renamed field), and a projected source contributes no redaction paths. When every source is coalition the result is coalition; when every source lists the same roles it stays roles; otherwise it is participants with the sorted intersection.

sign_detached

sign_detached(
    payload: bytes, private_key: Ed25519PrivateKey, kid: str
) -> str

Compact JWS with a detached payload over payload (already canonical bytes).

sign_envelope

sign_envelope(
    envelope: Envelope | Mapping[str, Any],
    private_key: Ed25519PrivateKey,
    kid: str,
) -> str

Sign an envelope and return the compact JWS to put in sig.

signing_payload

signing_payload(
    envelope: Envelope | Mapping[str, Any],
) -> bytes

The JCS bytes that are signed.

verify_detached

verify_detached(
    sig: object,
    payload: bytes,
    public_key: Ed25519PublicKey,
    *,
    expected_kid: str | None = None,
) -> str

Verify a detached-payload compact JWS against public_key only. Returns the kid.

Raises SignatureError on any failure.

verify_envelope

verify_envelope(
    envelope: Envelope | Mapping[str, Any],
    public_key: Ed25519PublicKey,
    *,
    expected_kid: str | None = None,
) -> str

Verify envelope['sig'] against public_key only. Returns the kid.

Raises SignatureError on any failure.

schema_dir

schema_dir() -> Path

An explicit schema override, or the schemas shipped in this distribution.

validate_envelope

validate_envelope(data: object) -> None

Validate an envelope, then its payload by type.

validate_payload

validate_payload(
    message_type: str, payload: object
) -> None

Validate a payload against the schema for its message type.