# Draft-3 free agreement wire contracts

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](../profiles/free_contracts.py) implements explicit `validate_*` functions. [free.schema.json](../profiles/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:

```text
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:

```text
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:

```sh
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.
