Provision AgenticOrg Authority Access
- Store
COMMERCE_C6Z_AUTHORITY_SERVICE_TOKENthrough the production secret path. - Optionally add bootstrap tenant ids to
COMMERCE_C6Z_AUTHORITY_SERVICE_TENANTS. Merchant tenants no longer need an entry here: AgenticOrg enrols each one itself (see Service-Tenant Enrolment). - Deploy or restart the Grantex auth service with both values available.
PUBLIC_BASE_URLmust be https (status list URIs are built from it). - Create the first OACP issuer key:
POST /v1/commerce/oacp/issuer-keys/rotatewith the platform admin key. Confirm itskid(oacp-...) appears inGET /.well-known/jwks.json. - Set
OACP_STATUS_LIST_PUBLIC_ENABLED=trueand restart when relying parties are ready to check status; until then they refuse artifacts withrevocation_status_unavailable. - Submit a fixture
POST /v1/commerce/oacp/c6z/authority-requestsfor that tenant. Public discovery stays disabled unlessOACP_PUBLIC_DISCOVERY_ISSUANCE_ENABLED=trueand the request opts in (public_discovery_requested: truewithnamespace_attestation_refandserving_host); turn the flag on only after the owner approves publishing UCP profiles. - Confirm
artifact_count = 11, or record the exact refusal/blocker. - To roll back, revoke the tenant’s enrolment (or remove a bootstrap tenant id from
COMMERCE_C6Z_AUTHORITY_SERVICE_TENANTS) and rotate the token if there is any suspicion of exposure.
Service-Tenant Enrolment
A tenant may use the AgenticOrg service token when it is onCOMMERCE_C6Z_AUTHORITY_SERVICE_TENANTS or has an active enrolment in
oacp_service_tenant_enrolments (migration 137_oacp_service_tenant_enrolments.sql). The env
list is now only a bootstrap list for operator and fixture tenants: no per-merchant env edit or
redeploy is needed. Env-listed tenants behave exactly as before (any merchant, any route).
Validation refusals are
422 validation_failed with details.fields: tenant_id must be a UUID,
merchant_id must match ^[A-Za-z0-9][A-Za-z0-9_.:-]{0,127}$ (the OACP id rule), source_system
must be shopify or catalog_upload, evidence_ref an opaque reference of at most 256
characters. Every enrolment, refusal, revocation and admin read is written to the append-only
oacp_service_tenant_enrolment_events table (UPDATE and DELETE are blocked by trigger, like
commerce_audit_events, which cannot hold these rows because it references commerce_tenants).
For an enrolled tenant the service token must also name one of the enrolled merchants:
- C6Z issuance (
request.merchant_id), arbiter decisions and settlement evidence (merchant_id), the UCP profile (merchant_id) and artifact replay (merchant_id, now required for an enrolled tenant) answer403 service_tenant_merchant_not_enrolledotherwise; - mandate issuance and the ACP delegate-payment allowance use the passport’s
merchant_id(or the body’s, when given) and refuse the same way; - reads, revocations and suspensions of a stored artifact, mandate or order of another merchant
answer
404, exactly like another tenant’s, and mandate verification answersmandate_scope_mismatch.
403 service_tenant_revoked (or 404 on the by-id routes); an unknown one
the route’s existing service_tenant_not_allowed / tenant_not_allowed. Operators and the
platform admin are unaffected.
Audit witness checkpoints (/v1/audit/witness-checkpoints) apply the same tenant check when
tenant_ref is a tenant UUID (what AgenticOrg sends): 403 SERVICE_TENANT_NOT_ALLOWED or
SERVICE_TENANT_REVOKED unless the tenant is env-listed or actively enrolled. A tenant_ref
that is not a UUID is an opaque reference with no tenant behind it and is accepted as before.
If AgenticOrg witnesses the audit chains of tenants that never enrolled (its
AGENTICORG_AUDIT_CHAIN_ENABLED covers every tenant), those posts are now refused and logged
as audit_chain_witness_refused; enrol or env-list such tenants first.
Catalogue upload source
connector_choice: "catalog_upload" (with connector evidence source_system: "catalog_upload")
is a catalogue the merchant uploaded itself. It is accepted for an env-listed tenant, or for an
enrolled tenant whose enrolment lists catalog_upload; otherwise it is refused
unsupported_or_enabling_authority_request, recorded like any refusal. It is never synthetic
data (synthetic_data: true is refused). Its artifacts say so honestly: source_system: "catalog_upload" at the top level, and in source_lineage source_system: "catalog_upload",
merchant_system_source_of_record: true, merchant_supplied_upload: true and
raw_catalog_upload_stored_by_grantex: false. The synthetic demo source keeps its rules: an
enrolment never grants it.
Public discovery issuance treats an actively enrolled tenant like an env-listed one; it still
needs OACP_PUBLIC_DISCOVERY_ISSUANCE_ENABLED=true and the request’s opt-in and attestation.
To offboard a merchant tenant: POST .../service-tenants/{tenant_id}/revoke with a reason code,
then revoke its live artifacts if they must stop verifying before they expire.
Enable a Synthetic Demo Tenant
A demonstration merchant with an invented catalogue gets real signed artifacts without claiming a merchant system of record. Issuance acceptsconnector_choice: "synthetic_demo" (and
connector evidence with source_system: "synthetic_demo") only when all of these hold:
- the tenant id is on
COMMERCE_C6Z_DEMO_SOURCE_TENANTS(comma-separated; empty by default, so no tenant can use the synthetic source); - the same tenant id is on
COMMERCE_C6Z_AUTHORITY_SERVICE_TENANTS, whoever calls (the platform admin included); - the request carries
synthetic_data: true, and so does the connector evidence.
422,
status: "rejected", refusal_code: "unsupported_or_enabling_authority_request") and the
refusal is recorded in oacp_artifacts with a refusal_record_id. Evidence that names another
source, or is not marked synthetic, is refused connector_evidence_scope_mismatch, also recorded.
Every artifact issued from the synthetic source says so in its signed payload:
source_system: "synthetic_demo" and synthetic_data: true at the top level, and in
source_lineage source_system: "synthetic_demo", synthetic_data: true and
merchant_system_source_of_record: false. Shopify issuance is unchanged, byte for byte.
Public discovery follows the same opt-in rules as for any tenant (OACP_PUBLIC_DISCOVERY_ISSUANCE_ENABLED,
the allowlist and the namespace / host attestation); the public_discovery_state artifact
then carries synthetic_data: true, and GET /v1/commerce/oacp/ucp-profile adds
synthetic_data: true to its render, so a relying party can see the profile is a demonstration.
To enable one demo tenant:
- Confirm with the owner that the tenant is a demonstration tenant whose catalogue is invented.
- Add its tenant id to
COMMERCE_C6Z_AUTHORITY_SERVICE_TENANTSand toCOMMERCE_C6Z_DEMO_SOURCE_TENANTS; redeploy. - Submit its authority request with
connector_choice: "synthetic_demo"andsynthetic_data: true. Confirm201,artifact_count = 11, and that every payload carriessynthetic_data: true. - Only if the demo is to publish a UCP profile: the public discovery opt-in, as above.
COMMERCE_C6Z_DEMO_SOURCE_TENANTS (new synthetic
requests are refused at once) and revoke its live artifacts if they must stop verifying before
they expire.
For a local stack (PUBLIC_BASE_URL=http://localhost:<port>), also set
OACP_ALLOW_LOOPBACK_HTTP=true; without it issuance refuses the http status list URI. It is
ignored when NODE_ENV is production. OACP_AUTO_GENERATE_ISSUER_KEY=true creates the first key
on first use in a local or test stack; it is also ignored in production. Then run the live
conformance suite from the in-tree build (the oacp suite is not yet in a published release of
@grantex/conformance):
GRANTEX_CONFORMANCE_INTERNAL_TOKEN=<service token> node packages/conformance/dist/index.js --base-url http://localhost:<port> --api-key <developer key> --include oacp --suite oacp --oacp-issuer http://localhost:<port> --oacp-tenant <tenant>.
Production Turn-On Order
A merge tomain deploys the auth service, so the OACP code is deployed before anything is
turned on. On 2026-10-20 production had no oacp- key in its JWKS and the status list route
was off. Turn OACP on in this order, and do not start a step before the previous one is
confirmed:
Rollback, in reverse: turning
OACP_STATUS_LIST_PUBLIC_ENABLED off makes every relying party
refuse (fail closed), it does not make anything valid; revoking the issuer key withdraws
everything it signed; revoking a tenant’s enrolment, or removing a bootstrap tenant from the
allowlist, stops new issuance for it.
Never put Shopify tokens, provider credentials, payment credentials, raw provider payloads, checkout links, raw POS callbacks, or webhook secrets in Grantex artifacts or logs.
Keys and Revocation
Smoke Tests
Checklist
Rollback
- Remove the AgenticOrg tenant from
COMMERCE_C6Z_AUTHORITY_SERVICE_TENANTS. - Rotate the service token if compromise is suspected.
- Ask AgenticOrg to stop refreshing artifacts and mark affected cache records stale.
- Leave existing historical artifacts reviewable for audit/export (
oacp_artifacts). - To invalidate artifacts already handed out, revoke them individually, or revoke the issuer key to invalidate everything it signed.
- Publish an operator note with affected tenants, merchant ids, artifact families, and timestamps.
Arbiter Decision Ledger
POST /v1/commerce/oacp/arbiter-decisions records every AgenticOrg arbiter decision in commerce_arbiter_decisions (migration 134_commerce_ledger.sql), append-only: a trigger refuses UPDATE, DELETE and TRUNCATE, and a check constraint refuses a refusal row that carries a grant. It needs no new configuration: the service token and COMMERCE_C6Z_AUTHORITY_SERVICE_TENANTS already provisioned for authority requests apply.
To stop countersigning for a tenant, remove it from
COMMERCE_C6Z_AUTHORITY_SERVICE_TENANTS; recorded decisions stay readable for audit.
Witness Checkpoints
POST /v1/audit/witness-checkpoints stores AgenticOrg audit chain heads in audit_witness_checkpoints and refused forks in audit_witness_forks (both append-only, migration 134_commerce_ledger.sql). It is authenticated with the AgenticOrg service token alone; no tenant allowlist applies, because tenant_ref names AgenticOrg’s own tenant.
A row in audit_witness_forks means AgenticOrg presented a second, different hash for a chain position Grantex had already witnessed: treat it as an irregularity, open an incident with AgenticOrg, and keep both the witnessed countersignature and the fork record for the review. Read them with GET /v1/audit/witness-checkpoints?source=agenticorg&tenant_ref=....
Wallet Ledger Chain
Migration134_commerce_ledger.sql adds chain_seq, prev_hash and entry_hash to wallet_ledger_entries, chains every new entry in a BEFORE INSERT trigger under a per-wallet advisory lock, and backfills existing entries per wallet in (created_at, id) order. The backfill disables the table’s append-only trigger only inside the migration transaction, which already holds an exclusive lock on the table; it is enabled again before commit. Plan the migration for a maintenance window on a large ledger. No flag: the columns and trigger are additive and no existing response changes.
A failed verification is an irregularity in financial evidence: block the wallet, keep a database snapshot, and reconcile from backups before any repair. Never repair a ledger by editing or deleting entries.