1.2.0-rc.1 / Agreements and authority

Wire contracts

Closed draft-3 messages and cross-field requirements for a free exchange.

17 September 2026. Unpublished 1.2.0-draft.3 candidate contracts for direct and delegated free agreements. These contracts do not rename the published draft-2 service or claim an integrated service has passed interoperability tests. The accompanying service, gateway and independent client must supply their own execution evidence.

free_contracts.py implements explicit validate_* functions. free.schema.json contains closed Draft 2020-12 structural definitions; its root accepts the twelve public message families. The binding and other helper definitions are available separately under $defs. Published policy remains 1.2.0-draft.1. No global version variable or default crypto type registry is modified. Unversioned URL, integer, HTTP classification, asset-reference and policy-obligation semantics are reused explicitly; draft-2 envelope validators are not used to relabel new messages.

Common rules and binding

Every top-level message contains the following exact common fields, called B below:

protocol_version = "1.2.0-draft.3"
origin           = canonical publisher HTTPS origin
service_id       = canonical HTTPS identifier on that publisher origin
issuer           = canonical HTTPS service-operator identity
delegation_id    = explicit publisher HTTPS delegation identifier, or null

issuer identifies the service in the exchange, including messages an agent sends to that service; the JWS signer is established separately from its registered key and message role. An external provider needs an explicit current delegation. A same-origin deployment may use null. Binding equality compares all five fields. This profile keeps the binding fixed for one agreement; it does not silently transfer an agreement to a new issuer or delegation. Migration is a separate operation and conformance gate.

All fields are required unless a definition expressly uses null. Unknown fields are rejected. JSON is bounded UTF-8 with no duplicate members, non-finite numbers, fractional/exponent integer encodings, negative zero or surrogate strings. Normal messages and signed decoded report payloads are at most 128 KiB; intake envelopes are at most 1 MiB because they retain report bytes and JWS together. Semantic integer values are nonnegative safe integers and timestamps are exact whole-second UTC calendar values. IDs use canonical lowercase UUIDv4 where specified. Service/client/idempotency tokens are bounded ASCII; idempotency keys are 16–128 characters. Nonces and issued bearer tokens are canonical unpadded base64url encodings of 32 bytes.

The helpers validate data, not signatures, HTTPS provenance, client/principal authority, current delegation or completed duties. A public handler must authenticate before supplying role/reporter or other trusted parameters. Structural validation alone cannot establish a permission. An accepted receipt is evidence and explicitly is not an access credential.

Message fields

The following lists are exhaustive additions to B. Field spellings are the wire contract; request, context, admission and report components are defined below.

Message / validator Fields in addition to B
offer_request / validate_offer_request principal_id, request, access_seconds, use_seconds
offer / validate_offer offer_id, client_id, principal_id, policy_id, policy_revision, policy_digest, authority_revision, authority_digest, terms_digest, request, obligations, context, nonce, issued_at, expires_at, access_seconds, use_seconds, revocation, payment
acceptance / validate_acceptance offer_id, offer_digest, client_id, principal_id, intent, nonce, accepted_at, idempotency_key
receipt / validate_receipt record_type, agreement_id, client_id, principal_id, offer_id, offer_digest, acceptance_digest, recorded_at, access_expires_at, use_expires_at, state_at_issue, payment_required, access_credential
status / validate_status record_type, agreement_id, state, effective_at, reason, status_version, issued_at, authority_revision, authority_digest, authority_checked_at
revoke_request / validate_revoke_request agreement_id, reason, idempotency_key
token_request / validate_token_request agreement_id
token_response / validate_token_response token_type, access_token, expires_at, agreement_id
introspection_request / validate_introspection_request token, resource_url, method, actions, purposes, request_id
introspection_response / validate_introspection_response request_id, active, permitted, reason, admission
report / validate_report event_id, event_type, occurred_at, reporter_id, source, resource_url, policy_id, policy_revision, actions, purposes, agreement_id, asset_ref, operation, http, quantity, unit, related_events, derived_from
intake / validate_intake record_id, collector_id, received_at, assurance, authenticated_reporter_id, payload_digest, payload_b64url, reporter_jws, authority

request is exactly {url, actions, purposes, supported_obligations}. The last array is explicit even when empty. Action/purpose arrays are nonempty, known and unique; the policy’s Cartesian evaluation remains in force. URL belongs to the publisher origin, using canonical path/query semantics. Principals and issuer identities are canonical HTTPS identifiers. Access/use durations are positive seconds, at most 31,536,000; access cannot exceed use duration.

Offer, contexts and exact assent

An offer has exactly payment: {required:false} and revocation:"future_access_only". Its lifetime is 1–600 seconds, also bounded by the publisher policy, authority, service and selected delegation. obligations are the deterministic cumulative policy duties, including the strictest retention/report deadline; unknown, duplicate/uncombined or unsupported duties cannot be silently dropped. Free operation includes no payment-provider call, quote, mandate or service fee in this wire profile.

context contains exactly three objects {role,url,digest,media_type}, one each for policy/application/json, origin_authority/application/json and human_terms/text/plain. Their digests equal the corresponding top-level digest fields. URLs may refer to publisher documents or the explicitly appointed provider’s retained context endpoint. Clients must constrain retrieval to those approved hosts/routes, avoid forwarding credentials to a different origin/tenant and verify exact bytes before use. A URL or old valid signature alone does not establish current authority.

validate_context_bundle(offer, *, policy_bytes, authority_bytes, terms_bytes, now=None) checks exact hashes, UTF-8 terms, decoded publisher/policy/revision identities, service/issuer/delegation binding, context hosts, offered lifetime/scope/duration ceilings and unchanged policy duties. It explicitly returns origin_authenticated:false: authenticated publication acquisition, history and current-authority decisions remain separate. The service must verify its selected receipt signing key’s authority and offer-lifetime bound through the authority engine.

Acceptance is an odexa-acceptance+jws payload with intent:"accept". It must match the exact offer bytes, selected client/principal, nonce and service binding, and be dated inside the offer interval. The server authenticates the client and its authority to act for that principal. It atomically consumes one offer and stores acceptance, original receipt and durable transition evidence. A changed payload under the same service/client/idempotency key conflicts; an exact decoded-payload retry returns the original receipt. Re-signing the same exact payload with another valid ES256 signature does not change acceptance identity. A retry cannot extend rights or issue a new access grant.

The signed odexa-receipt+jws payload uses record_type:"agreement.accepted", state_at_issue:"active", payment_required:false and access_credential:false. acceptance_digest covers retained decoded JWS payload bytes, not the randomized signature or a reserialized object. offer_digest covers exact retained offer bytes. Access/use expiry equals recorded time plus the accepted durations. The service must commit before the offer expires and after the supplied assent time.

Use validate_acceptance(value, offer_bytes, now) and validate_receipt(value, offer_bytes, acceptance_bytes) to enforce these cross-message invariants. Calling either validator without retained context only validates the standalone document; it is insufficient to accept an exchange. Original signed history must remain intact when later status changes.

Status, revocation and delegated authority

Status uses record_type:"agreement.status", monotonic positive status_version and state active, revoked or expired. effective_at cannot precede acceptance or follow issuance. An expired status takes effect at the accepted access deadline. Status must retain a current authority revision/digest/check time with no more than five seconds between check and issuance. The signed type is odexa-status+jws.

The authority matrix now includes:

Operation Capability / key use Exact endpoint
status issue_agreements / odexa-status+jws base_url + "agreements/" + agreement_id + "/status"
revoke issue_agreements / odexa-status+jws base_url + "agreements/" + agreement_id + "/revoke"

Both operation objects require a canonical UUIDv4 agreement_id; other kinds retain their previous exact field set and reject this additional field. Status/revoke have zero offer/access/use durations, payment_mode:"none", and no asset identifiers. Their decisions bind the agreement ID and full operation digest and remain operational authority with resource_permission_granted:false. A status query or revocation does not create or renew a licence.

The revoke body includes its agreement ID so path/body mismatches fail. It carries an idempotency key and bounded reason. The service authenticates an authorized client/operator, atomically transitions state and keeps the original signed result for exact command retries. Revocation stops future access; previously accepted use-window terms and historical facts remain independently represented. Authority outage, withdrawn delegation or an inactive signing key blocks a new authorized status/revoke result; authenticated retrieval of immutable old receipt evidence is not a fresh status assertion.

Tokens and introspection

Tokens are service-issued opaque Bearer values. validate_token_response(value, receipt, now) checks a positive lifetime no greater than 300 seconds and no greater than the accepted access deadline. Authentication, active agreement status, hash-only token persistence and token-to-agreement binding are service responsibilities.

Introspection is an authenticated gateway operation for GET/HEAD and must include retrieval in its action set. Response B and request_id must match the pending request. Both active:true and permitted:true are required before admission. A denial has admission:null: inactive token uses invalid_token or inactive_agreement; active-but-unpermitted uses scope_mismatch. An inability to obtain current authority is an unavailable-service HTTP error, not a fabricated negative claim about a token.

An allowed admission is exactly:

decision_id, agreement_id, client_id, principal_id
resource_url, method, actions, purposes
expires_at, checked_at, status_version
authority_revision, authority_digest, authority_checked_at
policy_id, policy_revision, policy_digest

It contains no bearer token. Resource/method/action/purpose fields match the exact request. Principal/client/agreement binding matches the accepted receipt. Its expiry cannot exceed either token or agreement access expiry, and its authority observation is at most five seconds old when checked. validate_introspection_response(value, request, receipt, token_response) enforces these correlations. A gateway must separately authenticate the service response, require current service authority, record admission durably before bytes and enforce the matching request even for a cached body. An admission is not proof of delivery.

The reference service may stop future token issuance/delivery when current policy bytes differ from the pinned accepted policy; it must state that rule explicitly. It preserves the original agreement/evidence and does not silently rewrite its use window or assert that prior lawful use never occurred.

Report and intake semantics

The draft-3 report retains the established event semantics with explicit B. Client-reported use, origin-observed delivery and service-recorded transitions remain distinct. Authenticated roles are agent, gateway and service; callers cannot choose a stronger source. operation is exactly {id,started_at,ended_at,state}. A use report has one action and an operation; a completed/failed operation ends at occurred_at, while an in-progress checkpoint has a null end. This contract does not introduce a new continuous-storage session discriminator or claim more coverage than the executor observes.

HTTP classifications, partial/full/no-body handling, asset references and qualified related-event references reuse their existing unversioned semantic checks. A resource URL/policy scope does not itself identify immutable asset bytes. Nullable digests or absent asset references cannot be counted as independently verified version traceability.

For a gateway report bound to an agreement, the receiving service must load the actual durable admission, authenticate the gateway namespace and call validate_report(..., admission=full_introspection_response) or enforce identical constraints. The event’s binding, agreement, policy, URL, method, actions, purposes and delivery/request ID must match that admission, and the event cannot precede it. The service enforces one terminal observation per admitted delivery and distinguishes failed/unresolved delivery from completed bytes. A client-reported event can remain a claim, including a claim requiring later compliance evaluation; accepting a report does not establish that a use was authorized or occurred.

Intake is an odexa-event-record+jws payload. It retains canonical base64url of the exact report payload, its digest and, when verified, the original odexa-event+jws. collector_id equals B.service_id and the retained authority reference’s service ID. authority is exactly {origin,service_id,revision,url,digest}. Its URL may point to the appointed provider’s retained exact context; it is not forced into the draft-2 publisher-history path. A verifier must authenticate the collector against the digest-verified authority context, constrain retrieval and expose missing historical provenance.

assurance is unverified, client_key_verified, origin_key_verified or service_key_verified. Unverified intake allows only client claims and has null authenticated reporter/JWS. Verified intake binds the authenticated reporter identity, source and exact JWS payload; the semantic validator does not perform the cryptographic signature check. Reporting capability, keys, role provisioning, exact-payload deduplication and collector-signed persistence remain runtime requirements.

Verification and integration boundary

The focused tests passed 15 free-contract methods and 26 delegation methods, including exact offer/acceptance/receipt bindings, re-signed identical payloads, scope/duty constraints, denied admissions, principal/request/expiry mismatches, role spoofing, delivery linkage and the new status/revoke matrix. Run:

python3 -m unittest discover -s tests -p 'test_free_contracts.py' -v
python3 -m unittest discover -s tests -p 'test_delegation_profile.py' -v
PYTHONPATH=tests:. python3 tests/test_free_contracts.py --cases

The last command emits 31 independently usable schema cases: 13 positives and 18 negatives with separate structural/semantic validity labels. The integrator runs independent AJV and wire/client tests and records those results separately. Unit tests do not prove client/principal authentication, remote authority freshness, durable acceptance, receipt signatures, gateway enforcement, actual use reports, export completeness or full protocol release readiness. Frozen draft-2 artifacts remain unchanged.

Odexa / Protocol explorer

This page. Your terms.

Inspect this website’s published policy and see how a proposed use is evaluated.

Current pagehttps://odexa.io/guides/free-contracts/
Loading policy…

Published JSON
Open JSON

This is a local policy check, not a signed agreement or proof of agent compliance. Other published licences and applicable rights still apply. How policy evaluation works →

Odexa / Get in touch

Start a conversation.

Tell us what you have in mind. We’ll respond where we can.

We use these details to review and respond to your enquiry. Please leave out confidential information. Submitting does not subscribe you to marketing. Privacy policy.