<?xml version="1.0" encoding="utf-8"?>
<!-- name="GENERATOR" content="github.com/mmarkdown/mmark Mmark Markdown Processor - mmark.nl" -->
<rfc version="3" docName="draft-shovan-gap-00" ipr="trust200902" submissionType="independent" category="info" xml:lang="en" xmlns:xi="http://www.w3.org/2001/XInclude">

<front>
<title abbrev="GAP">Governed Action Protocol (GAP)</title><seriesInfo value="draft-shovan-gap-00" stream="independent" status="informational" name="Internet-Draft"></seriesInfo>
<author initials="J." surname="Shovan" fullname="Joshua Shovan"><organization>SynOI Inc</organization><address><postal><street></street>
<country>US</country>
</postal><email>joshua@foundationx.com</email>
<uri>https://github.com/synoi/synoi-gap/issues</uri>
</address></author>
<date year="2026" month="June" day="30"></date>
<area>Internet</area>
<workgroup></workgroup>

<abstract>
<t>The Governed Action Protocol (GAP) is an open wire protocol for a Universal
Action Coordination Fabric. GAP defines a four-phase lifecycle (Declare,
Grant, Invoke, Receipt) that governs every action an AI agent, smart device,
industrial controller, or automated pipeline may take. Every gate decision
produces a content-addressed, immutable receipt; receipts are signed at L2+
(Ed25519) and L4 (hybrid ML-DSA-65). The protocol is
designed to be language- and platform-neutral, applicable across enterprise
AI agent pipelines, consumer smart home devices, medical equipment,
industrial control systems, and game engines.</t>
<t>This document specifies the wire format, object model, grant evaluation rules,
HTTP API surface, workflow semantics, revocation mechanisms, and conformance
tiers for GAP version 1.0.</t>
</abstract>

</front>

<middle>

<section anchor="introduction"><name>Introduction</name>
<t>As AI agents and automated systems proliferate, the absence of a common
authorization and auditability layer creates compounding risk. Each environment
(AI agent frameworks, smart home hubs, industrial SCADA systems, game engines,
medical devices) builds its own integration layer with its own permission
model, its own audit log (if any), and its own revocation mechanism. None share
an audit trail. None speak to each other. None produce verifiable, portable
evidence of what was authorized.</t>
<t>GAP is the fabric underneath all of them. An actor declares what it can do. An
operator grants what it may do, under what conditions. Every invocation is
evaluated against an active grant. Every gate decision (allow, deny, defer,
timeout) produces a content-addressed, immutable receipt (signed at L2+). The same protocol that governs
an AI agent's tool calls also governs an industrial valve controller, a door
lock, and a medication infusion pump.</t>

<section anchor="motivation"><name>Motivation</name>
<t>Several properties motivate a unified open protocol:</t>

<dl>
<dt>Portability:</dt>
<dd><t>Authorization evidence must be independently verifiable by any party with
access to the gateway's public key, without contacting the gateway.</t>
</dd>
<dt>Non-repudiation:</dt>
<dd><t>A signed, content-addressed receipt cannot be silently modified after issuance.</t>
</dd>
<dt>Composability:</dt>
<dd><t>Delegation chains, multi-party approvals, and workflow orchestration must
compose from the same primitive objects.</t>
</dd>
<dt>Physical-safety applicability:</dt>
<dd><t>The protocol must carry safety classification metadata and define fail-closed
semantics for capabilities that affect physical systems.</t>
</dd>
</dl>
</section>

<section anchor="scope"><name>Scope</name>
<t>This document defines:</t>

<ul>
<li><t>The CDRO (Content-addressed, Deterministic, Replayable Object) wire format</t>
</li>
<li><t>OID computation (canonical JSON + SHA-256)</t>
</li>
<li><t>The four lifecycle phases: Declare, Grant, Invoke, Receipt</t>
</li>
<li><t>Grant evaluation algorithm including scope narrowing and delegation</t>
</li>
<li><t>Workflow orchestration and Human-in-the-Loop (HITL) signaling</t>
</li>
<li><t>Revocation (immediate, scheduled, provisional block, and L3 quorum)</t>
</li>
<li><t>The HTTP API surface for GAP-conformant gateways</t>
</li>
<li><t>Conformance tiers L1 through L4</t>
</li>
</ul>
<t>Security considerations for implementing a GAP gateway are specified in
{{security-considerations}}. The protocol specification is dedicated to the
public domain under CC0 1.0 Universal. Implementations of this specification
may be licensed independently.</t>
</section>

<section anchor="conventions-and-definitions"><name>Conventions and Definitions</name>
<t>The key words &quot;MUST&quot;, &quot;MUST NOT&quot;, &quot;REQUIRED&quot;, &quot;SHALL&quot;, &quot;SHALL NOT&quot;, &quot;SHOULD&quot;,
&quot;SHOULD NOT&quot;, &quot;RECOMMENDED&quot;, &quot;NOT RECOMMENDED&quot;, &quot;MAY&quot;, and &quot;OPTIONAL&quot; in this
document are to be interpreted as described in BCP 14 <xref target="RFC2119"></xref> <xref target="RFC8174"></xref>
when, and only when, they appear in all capitals, as shown here.</t>
<t>The following terms are used throughout this document:</t>

<dl>
<dt>Actor:</dt>
<dd><t>An entity (AI agent, service, device, or human user) that can declare
capabilities and invoke them subject to grants.</t>
</dd>
<dt>Operator:</dt>
<dd><t>The human or organizational entity that issues grants. An operator holds the
root of trust for a tenant.</t>
</dd>
<dt>Tenant:</dt>
<dd><t>An isolated authorization domain. CDROs never implicitly cross tenant
boundaries.</t>
</dd>
<dt>CDRO:</dt>
<dd><t>Content-addressed, Deterministic, Replayable Object. The base unit of the
GAP object model.</t>
</dd>
<dt>OID:</dt>
<dd><t>Object Identifier. A content-addressed string of the form <tt>sha256:&lt;hex&gt;</tt>
computed over the canonical JSON serialization of a CDRO's body fields.</t>
</dd>
<dt>Gateway:</dt>
<dd><t>A GAP-conformant server that evaluates invocations against active grants and
produces signed decision receipts.</t>
</dd>
<dt>Physical-safety capability:</dt>
<dd><t>A capability whose invocation can cause irreversible physical consequences.
Identified by <tt>physical_safety: true</tt> in the capability declaration.</t>
</dd>
</dl>
</section>
</section>

<section anchor="object-model"><name>Object Model</name>

<section anchor="cdro-envelope"><name>CDRO Envelope</name>
<t>Every GAP record is a CDRO. A CDRO is a JSON object with the following fields:</t>
<table>
<thead>
<tr>
<th>Field</th>
<th>Required</th>
<th>Description</th>
</tr>
</thead>

<tbody>
<tr>
<td><tt>oid</tt></td>
<td>yes</td>
<td><tt>sha256:&lt;64 hex chars&gt;</tt>, content-addressed identifier</td>
</tr>

<tr>
<td><tt>type</tt></td>
<td>yes</td>
<td>One of the <tt>gap:*</tt> type strings defined in this document</td>
</tr>

<tr>
<td><tt>gap_version</tt></td>
<td>yes</td>
<td>Always <tt>&quot;1.0&quot;</tt> for this specification</td>
</tr>

<tr>
<td><tt>tenant_id</tt></td>
<td>yes</td>
<td>The owning tenant identifier</td>
</tr>

<tr>
<td><tt>created_at_ms</tt></td>
<td>yes</td>
<td>Unix epoch milliseconds</td>
</tr>

<tr>
<td><tt>created_by</tt></td>
<td>yes</td>
<td>OID of the actor creating this record</td>
</tr>

<tr>
<td><tt>body</tt></td>
<td>yes</td>
<td>Type-specific payload</td>
</tr>

<tr>
<td><tt>signature</tt></td>
<td>no</td>
<td>Ed25519 signature over the canonical envelope, base64url</td>
</tr>

<tr>
<td><tt>signature_key_id</tt></td>
<td>no</td>
<td>Identifies the signing key</td>
</tr>

<tr>
<td><tt>signature_algorithm</tt></td>
<td>no</td>
<td>Signature algorithm: <tt>Ed25519</tt>, <tt>ML-DSA-65</tt>, or <tt>Ed25519+ML-DSA-65</tt>. Verifiers MUST NOT guess the algorithm from key length.</td>
</tr>

<tr>
<td><tt>supersedes</tt></td>
<td>no</td>
<td>OID of the CDRO this record replaces</td>
</tr>
</tbody>
</table><t>The defined <tt>type</tt> values are:</t>
<table>
<thead>
<tr>
<th>Type string</th>
<th>Body type</th>
</tr>
</thead>

<tbody>
<tr>
<td><tt>gap:capability_declaration</tt></td>
<td>CapabilityDeclarationBody</td>
</tr>

<tr>
<td><tt>gap:capability_grant</tt></td>
<td>CapabilityGrantBody</td>
</tr>

<tr>
<td><tt>gap:capability_invocation</tt></td>
<td>CapabilityInvocationBody</td>
</tr>

<tr>
<td><tt>gap:decision_receipt</tt></td>
<td>GapDecisionReceiptBody</td>
</tr>

<tr>
<td><tt>gap:revocation_event</tt></td>
<td>RevocationEventBody</td>
</tr>

<tr>
<td><tt>gap:workflow_definition</tt></td>
<td>WorkflowDefinitionBody</td>
</tr>

<tr>
<td><tt>gap:workflow_instance</tt></td>
<td>WorkflowInstanceBody</td>
</tr>

<tr>
<td><tt>gap:stage_transition</tt></td>
<td>StageTransitionBody</td>
</tr>

<tr>
<td><tt>gap:channel_event</tt></td>
<td>ChannelEventBody</td>
</tr>

<tr>
<td><tt>gap:break_glass_token</tt></td>
<td>BreakGlassTokenBody</td>
</tr>

<tr>
<td><tt>gap:local_override_credential</tt></td>
<td>LocalOverrideCredentialBody</td>
</tr>

<tr>
<td><tt>gap:lca_root</tt></td>
<td>LcaRootBody (<tt>{ root_public_key_base64: string, algorithm: string, tenant_id: string, valid_from_ms: number, expires_at_ms: number }</tt>)</td>
</tr>

<tr>
<td><tt>gap:erasure_event</tt></td>
<td>ErasureEventBody</td>
</tr>

<tr>
<td><tt>gap:orchestration_chain</tt></td>
<td>OrchestrationChainBody [DESIGN]</td>
</tr>

<tr>
<td><tt>gap:consent_record</tt></td>
<td>ConsentRecordBody [DESIGN]</td>
</tr>

<tr>
<td><tt>gap:pip_response</tt></td>
<td>PipResponseBody [DESIGN]</td>
</tr>
</tbody>
</table></section>

<section anchor="oid-computation"><name>OID Computation</name>
<t>An OID is computed as follows:</t>

<ol>
<li><t>Take the CDRO envelope object.</t>
</li>
<li><t>Remove the fields <tt>oid</tt>, <tt>gap_version</tt>, <tt>signature</tt>, <tt>signature_key_id</tt>,
and <tt>supersedes</tt>.</t>
</li>
<li><t>Canonicalize the resulting object per {{canonical-json}}.</t>
</li>
<li><t>Compute SHA-256 over the UTF-8 encoding of the canonical string.</t>
</li>
<li><t>Hex-encode the digest (lowercase).</t>
</li>
<li><t>Prepend <tt>sha256:</tt>.</t>
</li>
</ol>
<t>The result is the OID for this CDRO. The same input MUST always produce the
same OID. Implementations MUST compute and store the OID before signing so
that the signature covers the canonical content.</t>
</section>

<section anchor="canonical-json"><name>Canonical JSON</name>
<t>The canonical JSON form of a value is defined recursively:</t>

<ul>
<li><t><strong>string</strong>: standard JSON string encoding</t>
</li>
<li><t><strong>number</strong>: standard JSON number encoding (no trailing zeros beyond the
decimal point)</t>
</li>
<li><t><strong>boolean</strong>: <tt>true</tt> or <tt>false</tt></t>
</li>
<li><t><strong>null</strong>: excluded; <tt>null</tt> values MUST be omitted from canonical form</t>
</li>
<li><t><strong>array</strong>: <tt>[</tt> + elements joined by <tt>,</tt> (order preserved, <tt>null</tt> elements
omitted) + <tt>]</tt></t>
</li>
<li><t><strong>object</strong>: <tt>{</tt> + key-value pairs joined by <tt>,</tt>, keys sorted
lexicographically (Unicode code-point order), pairs with <tt>null</tt> values
omitted, keys encoded as JSON strings + <tt>}</tt></t>
</li>
</ul>
<t>Note: GAP canonical JSON is NOT identical to JSON Canonicalization Scheme
(JCS) <xref target="RFC8785"></xref>. The primary difference is that GAP omits <tt>null</tt> values while
JCS preserves them. Implementors MUST NOT use a JCS library for OID computation
without first stripping null values.</t>
</section>
</section>

<section anchor="phase-1-declare"><name>Phase 1: Declare</name>

<section anchor="purpose"><name>Purpose</name>
<t>Before any capability can be granted or invoked, the actor offering it MUST
publish a CapabilityDeclaration. The declaration is an immutable record of
what the actor can do, the safety classification of each capability, and
optional scope constraints.</t>
</section>

<section anchor="capabilitydeclarationbody"><name>CapabilityDeclarationBody</name>
<table>
<thead>
<tr>
<th>Field</th>
<th>Required</th>
<th>Description</th>
</tr>
</thead>

<tbody>
<tr>
<td><tt>actor_type</tt></td>
<td>yes</td>
<td>One of: <tt>service</tt>, <tt>device</tt>, <tt>agent</tt>, <tt>human_user</tt>, <tt>mcp_server</tt>, <tt>gateway_subsystem</tt>, <tt>skill</tt></td>
</tr>

<tr>
<td><tt>actor_id</tt></td>
<td>yes</td>
<td>Stable identifier for the declaring actor</td>
</tr>

<tr>
<td><tt>actor_name</tt></td>
<td>yes</td>
<td>Human-readable name</td>
</tr>

<tr>
<td><tt>actor_version</tt></td>
<td>yes</td>
<td>Semantic version string</td>
</tr>

<tr>
<td><tt>capabilities</tt></td>
<td>yes</td>
<td>Array of CapabilitySpec</td>
</tr>
</tbody>
</table><t>Each CapabilitySpec:</t>
<table>
<thead>
<tr>
<th>Field</th>
<th>Required</th>
<th>Description</th>
</tr>
</thead>

<tbody>
<tr>
<td><tt>capability</tt></td>
<td>yes</td>
<td>Dotted-taxonomy capability name (e.g. <tt>home.lock.engage</tt>)</td>
</tr>

<tr>
<td><tt>safety_class</tt></td>
<td>yes</td>
<td><tt>A</tt> (reversible), <tt>B</tt> (state-changing), or <tt>C</tt> (irreversible/critical)</td>
</tr>

<tr>
<td><tt>physical_safety</tt></td>
<td>no</td>
<td>Boolean. <tt>true</tt> if invocation can cause physical harm</td>
</tr>

<tr>
<td><tt>description</tt></td>
<td>no</td>
<td>Human-readable description</td>
</tr>

<tr>
<td><tt>scope_narrowing_schema</tt></td>
<td>no</td>
<td>JSON Schema describing allowed <tt>scope_narrowing</tt> keys</td>
</tr>

<tr>
<td><tt>privacy_classification</tt></td>
<td>no</td>
<td><tt>none</tt>, <tt>pii</tt>, <tt>phi</tt>, <tt>financial</tt>, or <tt>privileged</tt> (legally privileged content: attorney-client, work product, common-interest; the gateway routes receipts to a privilege-isolated store and suppresses them from standard list endpoints)</td>
</tr>

<tr>
<td><tt>require_signed_receipt</tt></td>
<td>no</td>
<td>Boolean. When <tt>true</tt>, the gateway MUST attach a cryptographic signature to every decision receipt for this capability. When <tt>false</tt>, the gateway SHOULD omit signing even on a server that signs by default (useful for high-frequency low-risk capabilities where signing cost outweighs benefit). When absent, the gateway applies its configured default signing policy. The operator MAY override this per-grant via <tt>GrantedCapabilityScope.require_signed_receipt</tt>.</td>
</tr>

<tr>
<td><tt>pii_args</tt></td>
<td>no</td>
<td>Array of arg key strings whose values contain PII, PHI, or NPI requiring tokenization before storage. The gateway MUST replace each listed key's value with a keyed HMAC token (one-way, using a per-tenant key) before constructing the invocation CDRO and receipt body. The original value is used for capability execution by the adapter but MUST NOT be stored in any CDRO.</td>
</tr>

<tr>
<td><tt>privilege_protected</tt></td>
<td>no</td>
<td>Boolean. When <tt>true</tt>, the gateway: routes receipts to a privilege-isolated store; suppresses the receipt body from <tt>GET /receipts</tt> list endpoint; requires an explicit attorney-assertion header on fetch by OID; excludes the receipt from automated compliance exports. Controls access routing, not deletion. Add <tt>privilege_asserted</tt> to the compliance_tags vocabulary when this field is true.</td>
</tr>
</tbody>
</table><t>When <tt>privilege_protected: true</tt> is set on a CapabilitySpec, the gateway: routes receipts to a privilege-isolated store; suppresses the receipt body from <tt>GET /receipts</tt> list endpoint; requires an explicit attorney-assertion header on fetch by OID; excludes the receipt from automated compliance exports. <tt>privilege_protected</tt> controls access routing, not deletion.</t>
<t>For capabilities with <tt>privacy_classification: phi</tt> or matching <tt>medical.*</tt> or <tt>financial.*</tt>, any arg key whose name is listed in <tt>pii_args</tt> MUST be tokenized before CDRO construction. The canonical JSON used for OID computation is taken from the unencrypted args before tokenization so OID integrity is maintained. A <tt>GET /v1/gap/receipts/:oid?include_pii=true</tt> path with elevated authorization serves authorized reviewers.</t>
</section>

<section anchor="supersession"><name>Supersession</name>
<t>An actor MAY publish a new declaration superseding an existing one by setting
<tt>supersedes</tt> to the OID of the prior declaration. A gateway MUST treat the
prior declaration as inactive once the superseding declaration is accepted.
A gateway MUST NOT accept a new declaration with the same <tt>actor_id</tt> unless it
includes a valid <tt>supersedes</tt> pointer to the currently active declaration for
that <tt>actor_id</tt> within the tenant.</t>
</section>

<section anchor="ephemeral-actors"><name>Ephemeral Actors</name>
<t>Some actors have a lifecycle tied to a session, job, or deployment instance rather than a persistent identity.</t>
<t>Add <tt>actor_lifecycle</tt> to CapabilityDeclarationBody:</t>
<table>
<thead>
<tr>
<th>Field</th>
<th>Required</th>
<th>Description</th>
</tr>
</thead>

<tbody>
<tr>
<td><tt>actor_lifecycle</tt></td>
<td>no</td>
<td><tt>persistent</tt> (default) or <tt>ephemeral</tt>. Ephemeral declarations are exempt from the supersession uniqueness rule; each declaration gets a fresh OID and is valid for its session only.</td>
</tr>

<tr>
<td><tt>actor_instance_id</tt></td>
<td>no</td>
<td>UUID or job ID distinguishing this instance from others with the same <tt>actor_id</tt>. When present, two declarations with the same <tt>actor_id</tt> but different <tt>actor_instance_id</tt> MUST NOT be treated as superseding each other.</td>
</tr>

<tr>
<td><tt>session_expires_at_ms</tt></td>
<td>no</td>
<td>For ephemeral actors: when this session ends. The gateway MUST auto-revoke all grants scoped to this actor<em>instance</em>id at this time.</td>
</tr>
</tbody>
</table><t>Grants issued to an <tt>actor_id</tt> without <tt>actor_instance_id</tt> apply to all instances of that actor_id. Grants scoped to a specific <tt>actor_instance_id</tt> expire with that instance.</t>
</section>

<section anchor="mcp-tool-governance"><name>MCP Tool-Call Governance [DESIGN]</name>
<t>Actors of type <tt>mcp_server</tt> expose tools via the Model Context Protocol (MCP) tools/list response. A GAP gateway auto-generates CapabilityDeclarations from those responses.</t>
<t>Capability names for MCP tools follow the pattern: <tt>mcp.&lt;server_id&gt;.&lt;tool_name&gt;</tt>.</t>
<t>MUST: a gateway MUST reject any auto-generated capability name that starts with <tt>gap:</tt> or matches any normative capability name defined in this specification. This prevents namespace pollution from attacker-controlled MCP servers that return malicious tools/list responses.</t>
<t>An invocation for an MCP-originated capability MAY carry <tt>mcp_tool_call</tt> on the CapabilityInvocationBody:</t>
<table>
<thead>
<tr>
<th>Field</th>
<th>Required</th>
<th>Description</th>
</tr>
</thead>

<tbody>
<tr>
<td><tt>server_id</tt></td>
<td>yes</td>
<td>Stable identifier for the MCP server</td>
</tr>

<tr>
<td><tt>tool_name</tt></td>
<td>yes</td>
<td>Name of the tool as returned by tools/list</td>
</tr>

<tr>
<td><tt>tool_schema_hash</tt></td>
<td>no</td>
<td>SHA-256 hash of the tool's JSON schema, for drift detection</td>
</tr>
</tbody>
</table></section>

<section anchor="identity-binding"><name>Identity Binding [DESIGN]</name>
<t>A CapabilityDeclarationBody MAY carry an <tt>identity_binding</tt> object that ties the <tt>actor_oid</tt> to a real-world credential using a hardware-backed signature.</t>
<t>The canonical binding payload is domain-separated:</t>

<artwork>&quot;gap-identity-binding-v1&quot; + &quot;:&quot; + actor_oid + &quot;:&quot; + tenant_id + &quot;:&quot; + credential_identifier
</artwork>
<t>The <tt>binding_signature</tt> field carries the credential holder's signature over the canonical binding payload.</t>
<t>IdentityBinding fields:</t>
<table>
<thead>
<tr>
<th>Field</th>
<th>Required</th>
<th>Description</th>
</tr>
</thead>

<tbody>
<tr>
<td><tt>credential_kind</tt></td>
<td>yes</td>
<td>One of the normative values below</td>
</tr>

<tr>
<td><tt>credential_identifier</tt></td>
<td>yes</td>
<td>Stable identifier within the credential_kind namespace</td>
</tr>

<tr>
<td><tt>binding_signature</tt></td>
<td>yes</td>
<td>Signature over canonical binding payload, base64url</td>
</tr>

<tr>
<td><tt>binding_alg</tt></td>
<td>yes</td>
<td>Signature algorithm (e.g. <tt>Ed25519</tt>, <tt>ES256</tt>, <tt>RS256</tt>)</td>
</tr>

<tr>
<td><tt>bound_at_ms</tt></td>
<td>yes</td>
<td>Unix epoch ms when binding was established</td>
</tr>

<tr>
<td><tt>issuer</tt></td>
<td>no</td>
<td>Issuer identifier (CA DN, OIDC issuer URL, etc.)</td>
</tr>

<tr>
<td><tt>expires_at_ms</tt></td>
<td>no</td>
<td>Expiry of the binding; absent means no expiry</td>
</tr>
</tbody>
</table><t>Normative <tt>credential_kind</tt> values:</t>
<table>
<thead>
<tr>
<th>Value</th>
<th>Credential type</th>
</tr>
</thead>

<tbody>
<tr>
<td><tt>piv_cac</tt></td>
<td>US Federal PIV or Common Access Card</td>
</tr>

<tr>
<td><tt>x509</tt></td>
<td>X.509 certificate</td>
</tr>

<tr>
<td><tt>fido2</tt></td>
<td>FIDO2 / WebAuthn credential</td>
</tr>

<tr>
<td><tt>tpm_attestation</tt></td>
<td>TPM 2.0 attestation key</td>
</tr>

<tr>
<td><tt>oidc_sub</tt></td>
<td>OIDC subject claim from a trusted IdP</td>
</tr>

<tr>
<td><tt>spiffe_svid</tt></td>
<td>SPIFFE SVID (X.509 or JWT)</td>
</tr>

<tr>
<td><tt>wallet_address</tt></td>
<td>Cryptographic wallet address</td>
</tr>

<tr>
<td><tt>professional_license</tt></td>
<td>Licensed professional credential (e.g. medical, legal)</td>
</tr>
</tbody>
</table></section>

<section anchor="compartment-scoping"><name>Compartment-Based Access Scoping [DESIGN]</name>
<t>A <tt>compartment</tt> field MAY be added to CapabilityDeclarationBody, CapabilityGrantBody, and CapabilityInvocationBody.</t>
<t>Accepted values: <tt>UNCLASS</tt>, <tt>CUI</tt>, or any reverse-domain operator-defined label (e.g. <tt>com.acme.project-alpha</tt>).</t>
<t>MUST: at invocation time, if the grant carries a <tt>compartment</tt>, the invocation <tt>compartment</tt> MUST exactly match. A compartment mismatch MUST be treated as a denial.</t>
<t>MUST: a gateway MUST return HTTP 404 (not HTTP 403) when the invoking actor's compartment level is insufficient to know that a resource exists. OID existence MUST NOT leak across compartment boundaries.</t>
<t>Cross-compartment access requires a bridge grant issued through a TPI-gated HITL workflow. Bridge grants MUST be issued by an operator whose declaration carries both compartment labels and MUST produce a compliance_tag of <tt>cross_compartment_bridge</tt> on the receipt.</t>
</section>
</section>

<section anchor="phase-2-grant"><name>Phase 2: Grant</name>

<section anchor="purpose-1"><name>Purpose</name>
<t>A CapabilityGrant authorizes a specific actor (the grantee) to invoke one or
more capabilities declared by a specific actor (the declarer), subject to
optional scope constraints, time limits, and preconditions.</t>
</section>

<section anchor="capabilitygrantbody"><name>CapabilityGrantBody</name>
<table>
<thead>
<tr>
<th>Field</th>
<th>Required</th>
<th>Description</th>
</tr>
</thead>

<tbody>
<tr>
<td><tt>grantee</tt></td>
<td>yes</td>
<td>ActorRef identifying the authorized actor</td>
</tr>

<tr>
<td><tt>capability_scopes</tt></td>
<td>yes</td>
<td>Array of GrantedCapabilityScope</td>
</tr>

<tr>
<td><tt>granted_at_ms</tt></td>
<td>yes</td>
<td>Unix epoch milliseconds</td>
</tr>

<tr>
<td><tt>granted_by</tt></td>
<td>yes</td>
<td>OID of the operator issuing the grant</td>
</tr>

<tr>
<td><tt>expires_at_ms</tt></td>
<td>no</td>
<td>Expiry timestamp in milliseconds</td>
</tr>

<tr>
<td><tt>parent_grant_oid</tt></td>
<td>no</td>
<td>OID of the parent grant (delegation chains)</td>
</tr>

<tr>
<td><tt>limits</tt></td>
<td>no</td>
<td>InvocationLimits</td>
</tr>

<tr>
<td><tt>additional_preconditions</tt></td>
<td>no</td>
<td>Array of Precondition</td>
</tr>

<tr>
<td><tt>timestamp_window_seconds</tt></td>
<td>no</td>
<td>For safety<em>class C without physical</em>safety: override for the default timestamp validation window in seconds. Gateway applies its default if absent.</td>
</tr>

<tr>
<td><tt>offline_grace_seconds</tt></td>
<td>no</td>
<td>Additional seconds beyond grant expiry during which offline provisional receipts are accepted at reconciliation. Defaults to 0.</td>
</tr>

<tr>
<td><tt>max_grant_offline_ttl_ms</tt></td>
<td>no</td>
<td>Maximum duration any device may use this grant without syncing to the gateway. After this window, further invocations MUST be denied until the device reconnects.</td>
</tr>

<tr>
<td><tt>max_revocation_bundle_age_ms</tt></td>
<td>no</td>
<td>Maximum acceptable age of a revocation bundle for this grant. Devices MUST deny physical_safety/class C invocations if the bundle is older than this value.</td>
</tr>
</tbody>
</table><t>Each GrantedCapabilityScope:</t>
<table>
<thead>
<tr>
<th>Field</th>
<th>Required</th>
<th>Description</th>
</tr>
</thead>

<tbody>
<tr>
<td><tt>capability</tt></td>
<td>yes</td>
<td>Capability name or wildcard pattern</td>
</tr>

<tr>
<td><tt>capability_declaration_oid</tt></td>
<td>yes*</td>
<td>OID of the CapabilityDeclaration. REQUIRED when <tt>safety_class</tt> is <tt>C</tt> or <tt>physical_safety</tt> is <tt>true</tt></td>
</tr>

<tr>
<td><tt>scope_narrowing</tt></td>
<td>no</td>
<td>Object constraining invocation arguments (see Scope Narrowing Evaluation)</td>
</tr>

<tr>
<td><tt>additional_preconditions</tt></td>
<td>no</td>
<td>Array of Precondition evaluated at invocation time for this scope (in addition to grant-level preconditions)</td>
</tr>

<tr>
<td><tt>require_signed_receipt</tt></td>
<td>no</td>
<td>Operator override for receipt signing. When set, takes precedence over the capability declaration's <tt>require_signed_receipt</tt>. Allows an operator to require signing for a capability the actor declared unsigned, or suppress signing for a high-frequency capability the actor flagged as requiring it.</td>
</tr>
</tbody>
</table><t>InvocationLimits:</t>
<table>
<thead>
<tr>
<th>Field</th>
<th>Description</th>
</tr>
</thead>

<tbody>
<tr>
<td><tt>max_invocations</tt></td>
<td>Maximum total invocation count</td>
</tr>

<tr>
<td><tt>max_per_window</tt></td>
<td>Maximum invocations within a rolling time window</td>
</tr>

<tr>
<td><tt>window_seconds</tt></td>
<td>Window duration in seconds</td>
</tr>

<tr>
<td><tt>aggregate_limits</tt></td>
<td>Array of AggregateLimitEntry</td>
</tr>
</tbody>
</table><t>AggregateLimitEntry:</t>
<table>
<thead>
<tr>
<th>Field</th>
<th>Description</th>
</tr>
</thead>

<tbody>
<tr>
<td><tt>key</tt></td>
<td>The invocation argument key to sum</td>
</tr>

<tr>
<td><tt>max</tt></td>
<td>Maximum rolling sum (non-negative)</td>
</tr>

<tr>
<td><tt>window_seconds</tt></td>
<td>Rolling window duration in seconds</td>
</tr>
</tbody>
</table>
<section anchor="cross-grant-aggregate-limit-groups"><name>Cross-Grant Aggregate Limit Groups</name>
<t>An <tt>aggregate_limit_group</tt> field on <tt>InvocationLimits</tt> references a named shared limit pool at the tenant level. Multiple grants referencing the same pool share a single rolling counter enforced atomically by the gateway.</t>
<table>
<thead>
<tr>
<th>Field</th>
<th>Required</th>
<th>Description</th>
</tr>
</thead>

<tbody>
<tr>
<td><tt>aggregate_limit_group</tt></td>
<td>no</td>
<td>Named pool identifier. All grants with the same <tt>aggregate_limit_group</tt> share rolling aggregate counters defined in the tenant's pool configuration.</td>
</tr>
</tbody>
</table><t>The gateway MUST maintain atomic counters per pool and MUST deny any invocation from any grant in the pool that would exceed the pool ceiling. Pool configuration is out of scope for this specification and is implementation-defined.</t>
</section>
</section>

<section anchor="precondition-kind-registry"><name>Precondition Kind Registry</name>
<t>A normative precondition_kind registry defines semantics for standard kinds. Custom kinds use reverse-domain prefixes (e.g. <tt>com.example.custom_check</tt>).</t>
<table>
<thead>
<tr>
<th>Kind</th>
<th>Required args</th>
<th>Description</th>
</tr>
</thead>

<tbody>
<tr>
<td><tt>time_window</tt></td>
<td><tt>days_of_week</tt>, <tt>start_hour_utc</tt>, <tt>end_hour_utc</tt></td>
<td>Invocation only permitted in the specified UTC time window</td>
</tr>

<tr>
<td><tt>rate_limit</tt></td>
<td><tt>max_count</tt>, <tt>window_seconds</tt></td>
<td>Maximum invocations per rolling window (cross-invocation, tracked by gateway)</td>
</tr>

<tr>
<td><tt>sanctions_screening</tt></td>
<td><tt>list_version</tt>, <tt>screening_provider</tt>, <tt>subject_fields</tt></td>
<td>Screens named arg keys against a sanctions list. Gateway MUST record screening result OID in the receipt. Gateway MUST NOT proceed to execution if result is <tt>denied</tt>.</td>
</tr>

<tr>
<td><tt>external_pip</tt></td>
<td><tt>endpoint_url</tt>, <tt>cache_ttl_seconds</tt>, <tt>subject_fields</tt>, <tt>pip_response_oid</tt> (optional)</td>
<td>POSTs invocation args to an external Policy Information Point and evaluates the boolean <tt>allowed</tt> response. Result is cached per (tenant, capability, args-hash) for <tt>cache_ttl_seconds</tt>. When <tt>pip_response_oid</tt> is present the response is ENFORCING (see Signed PIP Response below).</td>
</tr>

<tr>
<td><tt>inventory_check</tt></td>
<td><tt>resource_key</tt>, <tt>min_available</tt></td>
<td>Verifies the named resource has sufficient availability before execution</td>
</tr>

<tr>
<td><tt>token_budget</tt></td>
<td><tt>model_scope</tt>, <tt>max_input_tokens</tt>, <tt>max_output_tokens</tt>, <tt>max_cost_usd</tt>, <tt>window_seconds</tt></td>
<td>[DESIGN] Rolling token-budget cap evaluated post_invoke. See Token Budget Governance below.</td>
</tr>

<tr>
<td><tt>consent_current</tt></td>
<td>(none)</td>
<td>[DESIGN] Evaluates at invocation time whether the actor's most recent gap:consent_record for the capability's context has consented: true. The gateway MUST NOT use the idempotency cache for this evaluation. See Consent Version Chain below.</td>
</tr>
</tbody>
</table>
<section anchor="token-budget"><name>Token Budget Governance [DESIGN]</name>
<t>The <tt>token_budget</tt> precondition kind governs token consumption across invocations that call an LLM. Evaluation timing: post_invoke (settled after execution). The gateway writes the actual consumption to <tt>token_consumption</tt> on the decision receipt.</t>
<t>TokenBudgetArgs:</t>
<table>
<thead>
<tr>
<th>Field</th>
<th>Required</th>
<th>Description</th>
</tr>
</thead>

<tbody>
<tr>
<td><tt>model_scope</tt></td>
<td>yes</td>
<td>Shell-glob pattern matching model IDs (e.g. <tt>anthropic/claude-*</tt>)</td>
</tr>

<tr>
<td><tt>max_input_tokens</tt></td>
<td>no</td>
<td>Maximum input tokens within <tt>window_seconds</tt></td>
</tr>

<tr>
<td><tt>max_output_tokens</tt></td>
<td>no</td>
<td>Maximum output tokens within <tt>window_seconds</tt></td>
</tr>

<tr>
<td><tt>max_cost_usd</tt></td>
<td>no</td>
<td>Maximum cost in USD within <tt>window_seconds</tt>. [MODELED]; not authoritative until a conformance vector exists.</td>
</tr>

<tr>
<td><tt>window_seconds</tt></td>
<td>yes</td>
<td>Rolling window length in seconds</td>
</tr>
</tbody>
</table><t>The gateway settles actual consumption onto the receipt via the <tt>token_consumption</tt> field (see Phase 4: Receipt). Uses the <tt>aggregate_limit_group</tt> counter mechanism for cross-grant budgeting.</t>
</section>

<section anchor="signed-pip-response"><name>Signed PIP Response [DESIGN]</name>
<t>When an <tt>external_pip</tt> precondition carries <tt>pip_response_oid</tt>, the referenced CDRO is a <tt>gap:pip_response</tt> that the external PIP emitted and the gateway re-signed.</t>
<t>Distinction:</t>

<ul>
<li><t>Unsigned external reads (no <tt>pip_response_oid</tt>): ADVISORY. The response influences the gate decision but MUST NOT be the sole basis for an <tt>allow</tt> outcome.</t>
</li>
<li><t>Signed <tt>gap:pip_response</tt> (with <tt>pip_response_oid</tt>): ENFORCING. The gateway MAY use it as the sole basis for allow or deny.</t>
</li>
</ul>
<t>MUST: if <tt>pip_response_oid</tt> is present, the gateway MUST verify the CDRO signature before treating the response as enforcing.</t>
<t>PipResponseBody:</t>
<table>
<thead>
<tr>
<th>Field</th>
<th>Required</th>
<th>Description</th>
</tr>
</thead>

<tbody>
<tr>
<td><tt>pip_endpoint</tt></td>
<td>yes</td>
<td>URL of the external PIP endpoint</td>
</tr>

<tr>
<td><tt>request_args_hash</tt></td>
<td>yes</td>
<td>SHA-256 hash of the canonical request args sent to the PIP</td>
</tr>

<tr>
<td><tt>response_body_hash</tt></td>
<td>yes</td>
<td>SHA-256 hash of the raw response body from the PIP</td>
</tr>

<tr>
<td><tt>response_summary</tt></td>
<td>no</td>
<td>Human-readable summary (not authoritative)</td>
</tr>

<tr>
<td><tt>evaluated_at_ms</tt></td>
<td>yes</td>
<td>Unix epoch ms when the PIP was queried</td>
</tr>

<tr>
<td><tt>cache_ttl_ms</tt></td>
<td>yes</td>
<td>How long (ms) this response may be cached by the gateway</td>
</tr>

<tr>
<td><tt>pip_signature</tt></td>
<td>no</td>
<td>Optional signature from the PIP itself, base64url</td>
</tr>

<tr>
<td><tt>pip_signature_alg</tt></td>
<td>no</td>
<td>Algorithm used for <tt>pip_signature</tt></td>
</tr>
</tbody>
</table><t>For any invocation of <tt>financial.wire.initiate</tt> or <tt>financial.payment.initiate</tt>, a gateway MUST enforce <tt>sanctions_screening</tt> precondition evaluation even when absent from the grant's <tt>additional_preconditions</tt>. The gateway MUST reject the invocation and produce a denial receipt if the screening result is <tt>denied</tt>.</t>
</section>
</section>

<section anchor="scope-narrowing"><name>Scope Narrowing Evaluation</name>
<t>When a grant contains <tt>scope_narrowing</tt>, a gateway MUST enforce the following
rules at invocation time:</t>

<ol>
<li><t>For each key <tt>K</tt> in <tt>scope_narrowing</tt>:</t>

<ul>
<li><t>The invocation arguments MUST contain key <tt>K</tt>. If absent, the invocation
MUST be denied.</t>
</li>
<li><t>If the scope value is a string: <tt>args[K]</tt> MUST equal the scope value
(exact match, case-sensitive). Key names are exact and singular/plural
variants are distinct keys.</t>
</li>
<li><t>If the scope value is a boolean: <tt>args[K]</tt> MUST equal the scope value.</t>
</li>
<li><t>If the scope value is a number and the key name has the prefix <tt>min_</tt>:
<tt>args[K]</tt> MUST be greater than or equal to the scope value (lower bound).</t>
</li>
<li><t>If the scope value is a number and the key name does not have the prefix
<tt>min_</tt>: <tt>args[K]</tt> MUST be less than or equal to the scope value (upper
bound). For <tt>physical_safety: true</tt> capabilities, negative values for
<tt>args[K]</tt> MUST be rejected even when they would satisfy the upper-bound
check.</t>
</li>
<li><t>If the scope value is a string array: <tt>args[K]</tt> MUST be a member of the
array.</t>
</li>
</ul></li>
</ol>
<t>Dot-path expansion: A <tt>scope_narrowing</tt> key containing a dot (e.g. <tt>position.x</tt>) evaluates against the nested arg path <tt>args.position.x</tt>. This enables constraint of nested arg structures without requiring flat arg schemas.</t>
<t>Envelope constraint (optional): when a <tt>scope_narrowing</tt> value is an object with <tt>$constraint_oid</tt>, the gateway evaluates the named constraint CDRO (a pre-registered function) against the full args object, returning boolean. This enables correlated multi-axis constraints (e.g. motor speed AND torque within a 2D operating envelope).</t>
</section>

<section anchor="granted-by-verification"><name>Granted-By Verification</name>
<t>A gateway MUST verify at grant acceptance time that <tt>body.granted_by</tt> OID matches the actor OID associated with the authenticated Bearer token or SSC credential. A grant where these do not match MUST be rejected with HTTP 403.</t>
</section>

<section anchor="delegation"><name>Delegation</name>
<t>A grant MAY delegate authority to a sub-grantee by setting <tt>parent_grant_oid</tt>.
A gateway MUST enforce all of the following when evaluating a delegated grant:</t>

<ol>
<li><t>The <tt>granted_by</tt> field of the child grant MUST equal the <tt>grantee.actor_oid</tt>
of the parent grant.</t>
</li>
<li><t>For every capability scope in the child grant, a matching scope MUST exist
in the parent grant (using capability pattern matching per {{patterns}}).</t>
</li>
<li><t>For every key in the child grant's <tt>scope_narrowing</tt>, the child value MUST
satisfy the subset rule against the parent value for the same key:</t>

<ul>
<li><t>String: child value MUST equal parent value.</t>
</li>
<li><t>String array: every element of the child array MUST appear in the parent
array.</t>
</li>
<li><t>Number (upper bound, no <tt>min_</tt> prefix): child value MUST be less than or
equal to the parent value.</t>
</li>
<li><t>Number (lower bound, <tt>min_</tt> prefix): child value MUST be greater than or
equal to the parent value.</t>
</li>
<li><t>Key present in parent but absent in child: MUST be denied (a child cannot
drop a parent constraint).</t>
</li>
</ul></li>
<li><t>The child grant MUST NOT increase <tt>max_delegation_depth</tt> beyond the parent's
value. For <tt>physical_safety: true</tt> grants, <tt>max_delegation_depth</tt> MUST
default to <tt>0</tt> (no further delegation) when absent.</t>
</li>
<li><t>For <tt>physical_safety: true</tt> grants, the child MUST inherit all
<tt>additional_preconditions</tt> from all ancestors in the chain.</t>
</li>
<li><t>At invocation time, the gateway MUST re-validate that all grants in the
delegation chain are non-expired and non-revoked.</t>
</li>
</ol>
</section>

<section anchor="patterns"><name>Capability Pattern Matching</name>
<t>A capability pattern matches a capability name as follows:</t>

<ul>
<li><t><tt>*</tt> matches any capability name (match-all).</t>
</li>
<li><t><tt>prefix.*</tt> matches direct children only (single path segment). <tt>game.*</tt> matches <tt>game.session</tt> but NOT <tt>game.admin.delete.users</tt>.</t>
</li>
<li><t><tt>prefix.**</tt> matches all descendants recursively. <tt>game.**</tt> matches all nested paths under <tt>game</tt>, including <tt>game.session</tt>, <tt>game.admin.delete.users</tt>, etc. The prefix itself also matches (<tt>game.**</tt> matches <tt>game</tt>).</t>
</li>
<li><t>An exact string matches only that exact name (no wildcard).</t>
</li>
</ul>
<t>Two wildcard levels are defined: <tt>prefix.*</tt> matches direct children only (single path segment); <tt>prefix.**</tt> matches all descendants recursively. Implementations MUST support both levels.</t>
</section>
</section>

<section anchor="phase-3-invoke"><name>Phase 3: Invoke</name>

<section anchor="purpose-2"><name>Purpose</name>
<t>A CapabilityInvocation is the actor's request to exercise a granted capability.
The gateway evaluates the invocation against active grants and produces a
GapDecisionReceipt.</t>
</section>

<section anchor="capabilityinvocationbody"><name>CapabilityInvocationBody</name>
<table>
<thead>
<tr>
<th>Field</th>
<th>Required</th>
<th>Description</th>
</tr>
</thead>

<tbody>
<tr>
<td><tt>caller</tt></td>
<td>yes</td>
<td>CallerRef (actor<em>type, actor</em>oid, grant_oid)</td>
</tr>

<tr>
<td><tt>capability</tt></td>
<td>yes</td>
<td>The capability being invoked</td>
</tr>

<tr>
<td><tt>args</tt></td>
<td>yes</td>
<td>Invocation arguments (flat JSON object)</td>
</tr>

<tr>
<td><tt>invoked_at_ms</tt></td>
<td>yes</td>
<td>Client-supplied or server-stamped timestamp</td>
</tr>

<tr>
<td><tt>idempotency_key</tt></td>
<td>no</td>
<td>Client-provided deduplication key</td>
</tr>

<tr>
<td><tt>client_event_ms</tt></td>
<td>no</td>
<td>Unix epoch ms when the action originally occurred in the caller's reference frame (game-world time, clinical queue time, SCADA scan cycle). Populated by the client; not used for replay prevention. Stored in receipt for audit.</td>
</tr>

<tr>
<td><tt>queued_at_ms</tt></td>
<td>no</td>
<td>Unix epoch ms when the invocation was enqueued for submission (e.g. at reconnect after offline period). Optional; aids debugging of delivery latency.</td>
</tr>
</tbody>
</table></section>

<section anchor="timestamp-validation"><name>Timestamp Validation</name>
<t>Timestamp validation is per safety_class:</t>
<table>
<thead>
<tr>
<th>safety_class</th>
<th>physical_safety</th>
<th>Max client_timestamp age</th>
<th>Gateway behavior</th>
</tr>
</thead>

<tbody>
<tr>
<td>A</td>
<td>any</td>
<td>5 minutes</td>
<td>Reject if invoked<em>at</em>ms (client-supplied) is more than 5 minutes in the past</td>
</tr>

<tr>
<td>B</td>
<td>any</td>
<td>120 seconds</td>
<td>Reject if more than 120 seconds in the past</td>
</tr>

<tr>
<td>C</td>
<td>false</td>
<td>operator-configurable</td>
<td>Grant field <tt>timestamp_window_seconds</tt> sets the window; gateway default if absent</td>
</tr>

<tr>
<td>C</td>
<td>true</td>
<td>server-stamp only</td>
<td>Gateway MUST ignore client-supplied invoked<em>at</em>ms; stamps receipt time authoritatively</td>
</tr>
</tbody>
</table><t>When rejecting due to timestamp, the denial receipt MUST include:
- <tt>detail: 'timestamp_rejected'</tt>
- <tt>server_time_ms</tt> field containing the gateway's current Unix epoch ms so clients can resync clocks</t>
<t>For <tt>physical_safety: true</tt> capabilities:</t>

<ul>
<li><t>The gateway MUST server-stamp <tt>invoked_at_ms</tt> and ignore any client-supplied
value. The client-supplied value, if present, MUST be stored in
<tt>client_claimed_at_ms</tt> on the receipt for audit purposes.</t>
</li>
</ul>
</section>

<section anchor="idempotency"><name>Idempotency</name>
<t>A gateway SHOULD implement idempotency keyed by <tt>(tenant_id, capability,
idempotency_key)</tt>. On a cache hit:</t>

<ul>
<li><t>The gateway MUST re-validate that the grant is still non-revoked and
non-expired. If the grant has been revoked since the original execution, the
gateway MUST return HTTP 410 with a new denial receipt.</t>
</li>
<li><t>The gateway MUST verify that the arguments in the new request exactly match
the stored arguments. If they differ, the gateway MUST return HTTP 409.</t>
</li>
<li><t>The receipt MUST have <tt>is_idempotency_replay: true</tt>.</t>
</li>
<li><t>For <tt>physical_safety: true</tt> capabilities, the maximum idempotency window
MUST NOT exceed 60 seconds.</t>
</li>
</ul>
</section>

<section anchor="grant-selection"><name>Grant Selection</name>
<t>When multiple active grants match an invocation, a gateway MUST use the
following deterministic selection order:</t>

<ol>
<li><t>Prefer the grant with more <tt>scope_narrowing</tt> keys (more specific).</t>
</li>
<li><t>Among grants with equal key count, prefer lower numeric upper-bound values.</t>
</li>
<li><t>Among grants with equal numeric bounds, prefer smaller string-array
cardinality.</t>
</li>
</ol>
</section>
</section>

<section anchor="phase-4-receipt"><name>Phase 4: Receipt</name>

<section anchor="gapdecisionreceiptbody"><name>GapDecisionReceiptBody</name>
<t>A GapDecisionReceipt is produced for every gate decision. It is immutable.</t>
</section>

<section anchor="gdpr-erasure"><name>GDPR Erasure</name>
<t>GDPR Article 17 (right to erasure) requires a mechanism to handle erasure requests for receipts containing personal data. The GAP erasure mechanism preserves OID integrity while satisfying erasure obligations.</t>
<t>A <tt>gap:erasure_event</tt> CDRO replaces the body of a targeted receipt with a fixed erasure sentinel while preserving envelope metadata.</t>
<t>ErasureEventBody:</t>
<table>
<thead>
<tr>
<th>Field</th>
<th>Required</th>
<th>Description</th>
</tr>
</thead>

<tbody>
<tr>
<td><tt>target_oid</tt></td>
<td>yes</td>
<td>OID of the CDRO being erased</td>
</tr>

<tr>
<td><tt>erasure_reason</tt></td>
<td>yes</td>
<td><tt>gdpr_article_17</tt>, <tt>ccpa</tt>, or <tt>operator_policy</tt></td>
</tr>

<tr>
<td><tt>erased_at_ms</tt></td>
<td>yes</td>
<td>Unix epoch ms of erasure</td>
</tr>

<tr>
<td><tt>erased_by</tt></td>
<td>yes</td>
<td>Actor OID issuing the erasure</td>
</tr>

<tr>
<td><tt>fields_erased</tt></td>
<td>yes</td>
<td>Array of field paths erased from the target CDRO body</td>
</tr>
</tbody>
</table><t>The erasure event's OID anchors to the original CDRO OID and is itself a signed CDRO, making it non-repudiable. Verifiers MUST treat an erasure event as authoritative over the prior OID body. Add <tt>gdpr_erasure</tt> to the compliance_tags vocabulary.</t>
<t>For <tt>privacy_classification: pii</tt> capabilities, gateways operating under GDPR SHOULD use <tt>pii_args</tt> tokenization (see Phase 1: Declare) to minimize PII stored in receipts before erasure is required.</t>
<table>
<thead>
<tr>
<th>Field</th>
<th>Required</th>
<th>Description</th>
</tr>
</thead>

<tbody>
<tr>
<td><tt>subject_kind</tt></td>
<td>yes</td>
<td><tt>capability_invocation</tt>, <tt>workflow_stage</tt>, <tt>provisional_block</tt>, or <tt>grant_evaluation</tt></td>
</tr>

<tr>
<td><tt>subject_oid</tt></td>
<td>yes</td>
<td>OID of the evaluated object</td>
</tr>

<tr>
<td><tt>status</tt></td>
<td>yes</td>
<td>DecisionStatus (see below)</td>
</tr>

<tr>
<td><tt>capability_grant_oids</tt></td>
<td>yes</td>
<td>Array of grant OIDs that were evaluated</td>
</tr>

<tr>
<td><tt>decided_at_ms</tt></td>
<td>yes</td>
<td>Gateway-stamped decision timestamp</td>
</tr>

<tr>
<td><tt>detail</tt></td>
<td>no</td>
<td>Error code string (from the error code registry)</td>
</tr>

<tr>
<td><tt>compliance_tags</tt></td>
<td>no</td>
<td>Array of compliance tag strings (gateway-populated, not in OID hash)</td>
</tr>

<tr>
<td><tt>is_idempotency_replay</tt></td>
<td>no</td>
<td>Boolean. <tt>true</tt> if this receipt is a cached replay</td>
</tr>

<tr>
<td><tt>client_claimed_at_ms</tt></td>
<td>no</td>
<td>Client-supplied <tt>invoked_at_ms</tt> for physical_safety invocations</td>
</tr>

<tr>
<td><tt>max_offline_ttl_ms</tt></td>
<td>no</td>
<td>Maximum duration a verifier MAY cache this receipt offline</td>
</tr>

<tr>
<td><tt>signer_identity</tt></td>
<td>no</td>
<td>For 21 CFR Part 11 contexts: display name, role, and credential identifier of the authorizing human. The <tt>granted_by</tt> actor OID SHOULD resolve to this identity.</td>
</tr>

<tr>
<td><tt>sequence_number</tt></td>
<td>no</td>
<td>Monotonically increasing integer within the tenant, incremented per receipt, gapless. Gaps in the sequence indicate dropped receipts. Provides determinable ordering within a millisecond for high-frequency deployments (MiFID II RTS 25).</td>
</tr>

<tr>
<td><tt>decided_at_ns</tt></td>
<td>no</td>
<td>Optional nanoseconds since Unix epoch for sub-millisecond precision. RECOMMENDED for <tt>financial.*</tt> capabilities.</td>
</tr>
</tbody>
</table><t>A gateway MUST guarantee strict monotonicity of <tt>sequence_number</tt> within a tenant. For <tt>financial.*</tt> capabilities, the gateway SHOULD populate <tt>decided_at_ns</tt>.</t>
<t>DecisionStatus values: <tt>ok</tt>, <tt>denied</tt>, <tt>failed</tt>, <tt>deferred</tt>, <tt>timed_out</tt>,
<tt>pending</tt>, <tt>rate_limited</tt>.</t>
</section>

<section anchor="token-consumption-on-receipt-design"><name>Token Consumption on Receipt [DESIGN]</name>
<t>When a <tt>token_budget</tt> precondition is active, the gateway settles actual consumption onto the receipt after execution via the <tt>token_consumption</tt> field.</t>
<t>TokenConsumption:</t>
<table>
<thead>
<tr>
<th>Field</th>
<th>Required</th>
<th>Description</th>
</tr>
</thead>

<tbody>
<tr>
<td><tt>input_tokens</tt></td>
<td>yes</td>
<td>Input (prompt) tokens consumed; non-negative integer</td>
</tr>

<tr>
<td><tt>output_tokens</tt></td>
<td>yes</td>
<td>Output (completion) tokens consumed; non-negative integer</td>
</tr>

<tr>
<td><tt>model</tt></td>
<td>yes</td>
<td>Model identifier</td>
</tr>

<tr>
<td><tt>cost_usd</tt></td>
<td>no</td>
<td>Estimated cost in USD. [MODELED]</td>
</tr>

<tr>
<td><tt>settled_at_ms</tt></td>
<td>yes</td>
<td>Unix epoch ms when consumption was settled</td>
</tr>
</tbody>
</table></section>

<section anchor="compliance-tags"><name>Compliance Tags</name>
<t>A gateway MUST populate <tt>compliance_tags</tt> on every receipt. The minimum tag set:</t>
<table>
<thead>
<tr>
<th>Tag</th>
<th>When set</th>
</tr>
</thead>

<tbody>
<tr>
<td><tt>safety_class:A</tt>, <tt>safety_class:B</tt>, <tt>safety_class:C</tt></td>
<td>Capability safety class</td>
</tr>

<tr>
<td><tt>physical_safety</tt></td>
<td><tt>physical_safety: true</tt> capability</td>
</tr>

<tr>
<td><tt>hitl_approved</tt></td>
<td>Workflow reached <tt>terminal_outcome: approved</tt></td>
</tr>

<tr>
<td><tt>hitl_denied</tt></td>
<td>Workflow reached <tt>terminal_outcome: denied</tt></td>
</tr>

<tr>
<td><tt>idempotency_replay</tt></td>
<td><tt>is_idempotency_replay: true</tt></td>
</tr>

<tr>
<td><tt>rate_limited</tt></td>
<td><tt>status: rate_limited</tt></td>
</tr>

<tr>
<td><tt>phi</tt></td>
<td>Declaration has <tt>privacy_classification: phi</tt></td>
</tr>
</tbody>
</table><t><tt>compliance_tags</tt> are gateway-populated. They MUST NOT be included in the OID
hash computation. A verifier MUST NOT trust <tt>compliance_tags</tt> as normative; they
are informational audit annotations.</t>
</section>
</section>

<section anchor="agent-delegation-chain"><name>Agent Delegation Chain [DESIGN]</name>
<t>A <tt>gap:orchestration_chain</tt> CDRO captures the ordered sequence of delegation hops that authorized a terminal capability invocation. It consolidates multi-agent pipelines into a single auditable object.</t>

<section anchor="structure"><name>Structure</name>
<t>An OrchestrationChainBody:</t>
<table>
<thead>
<tr>
<th>Field</th>
<th>Required</th>
<th>Description</th>
</tr>
</thead>

<tbody>
<tr>
<td><tt>root_actor_oid</tt></td>
<td>yes</td>
<td>Actor OID that initiated the chain</td>
</tr>

<tr>
<td><tt>steps</tt></td>
<td>yes</td>
<td>Ordered array of DelegationStep, maximum 10</td>
</tr>

<tr>
<td><tt>capability_name</tt></td>
<td>yes</td>
<td>The capability name being delegated through the chain</td>
</tr>

<tr>
<td><tt>final_invocation_oid</tt></td>
<td>yes</td>
<td>OID of the terminal CapabilityInvocation CDRO</td>
</tr>
</tbody>
</table><t>Each DelegationStep:</t>
<table>
<thead>
<tr>
<th>Field</th>
<th>Required</th>
<th>Description</th>
</tr>
</thead>

<tbody>
<tr>
<td><tt>step_index</tt></td>
<td>yes</td>
<td>Zero-based position of this hop in the chain</td>
</tr>

<tr>
<td><tt>delegator_actor_oid</tt></td>
<td>yes</td>
<td>Actor OID performing the delegation</td>
</tr>

<tr>
<td><tt>delegatee_actor_oid</tt></td>
<td>yes</td>
<td>Actor OID receiving authority</td>
</tr>

<tr>
<td><tt>grant_oid</tt></td>
<td>yes</td>
<td>OID of the grant authorizing this hop</td>
</tr>

<tr>
<td><tt>prior_receipt_oid</tt></td>
<td>no</td>
<td>OID of the receipt from the prior hop (absent for step_index 0)</td>
</tr>

<tr>
<td><tt>delegated_at_ms</tt></td>
<td>yes</td>
<td>Unix epoch ms of delegation</td>
</tr>

<tr>
<td><tt>step_signature</tt></td>
<td>yes</td>
<td>Signature over canonical(prior<em>receipt</em>oid + invocation_body), base64url</td>
</tr>

<tr>
<td><tt>step_signature_alg</tt></td>
<td>yes</td>
<td>Algorithm used for step_signature</td>
</tr>
</tbody>
</table></section>

<section anchor="constraints"><name>Constraints</name>
<t>MUST: the <tt>steps</tt> array MUST NOT exceed 10 entries. A gateway MUST return HTTP 400 with error code <tt>delegation_depth_exceeded</tt> when this limit is breached.</t>
<t>MUST: signing keys for each hop MUST be declared at grant issuance. The gateway MUST verify each step's signature before allowing the terminal invocation.</t>
<t>MUST: at invocation time, the gateway MUST re-validate that all grants referenced in the chain are non-expired and non-revoked. Partial chain validation is insufficient.</t>
<t>A CapabilityInvocationBody MAY carry <tt>delegation_chain</tt> (array of DelegationStep) as a shorthand when the full <tt>gap:orchestration_chain</tt> CDRO is not yet materialized. The same maximum-10-hop and per-step signature verification rules apply.</t>
</section>
</section>

<section anchor="consent-version-chain"><name>Consent Version Chain [DESIGN]</name>
<t>GAP consent records form an append-only chain. Each record references <tt>prior_consent_oid</tt>, making consent history non-repudiable and auditable.</t>

<section anchor="gap-consent-record"><name>gap:consent_record</name>
<t>ConsentRecordBody:</t>
<table>
<thead>
<tr>
<th>Field</th>
<th>Required</th>
<th>Description</th>
</tr>
</thead>

<tbody>
<tr>
<td><tt>actor_oid</tt></td>
<td>yes</td>
<td>Actor OID whose consent this record captures</td>
</tr>

<tr>
<td><tt>tenant_id</tt></td>
<td>yes</td>
<td>Tenant scope</td>
</tr>

<tr>
<td><tt>context</tt></td>
<td>yes</td>
<td>Free-form context string (e.g. <tt>hiring.background_check</tt>, <tt>clinical.data_sharing</tt>)</td>
</tr>

<tr>
<td><tt>consented</tt></td>
<td>yes</td>
<td>Boolean: true = consent granted; false = consent withdrawn</td>
</tr>

<tr>
<td><tt>prior_consent_oid</tt></td>
<td>no</td>
<td>OID of the prior record for this actor + context</td>
</tr>

<tr>
<td><tt>consented_at_ms</tt></td>
<td>yes</td>
<td>Unix epoch ms of consent event</td>
</tr>

<tr>
<td><tt>expires_at_ms</tt></td>
<td>no</td>
<td>Optional expiry; the gateway MUST treat expired records as consented: false</td>
</tr>

<tr>
<td><tt>consent_text_hash</tt></td>
<td>no</td>
<td>SHA-256 hash of the disclosure text shown to the actor</td>
</tr>
</tbody>
</table></section>

<section anchor="precondition-consent-current"><name>Precondition: consent_current</name>
<t>The <tt>consent_current</tt> precondition evaluates at invocation time whether the actor's most recent consent record for the capability's context has <tt>consented: true</tt>.</t>
<t>MUST: the gateway MUST NOT use the idempotency cache for <tt>consent_current</tt> evaluation. Every invocation MUST re-read the most recent record.</t>
<t>MUST: withdrawal of consent (a new record with <tt>consented: false</tt>) MUST take effect within 5 seconds across all gateway replicas.</t>
<t>This single primitive subsumes all sector-specific consent patterns: hiring consent, learner consent, and clinical consent all use <tt>gap:consent_record</tt> with context-specific values in the <tt>context</tt> field.</t>
</section>

<section anchor="relationship-to-gdpr"><name>Relationship to GDPR</name>
<t>The consent chain is a complement to the erasure mechanism (see GDPR Erasure). Erasure removes PII from receipts; the consent chain records the authority under which invocations were permitted. Both chains SHOULD be linked in receipt <tt>compliance_tags</tt> when applicable.</t>
</section>
</section>

<section anchor="offline-execution-profile"><name>Offline Execution Profile</name>
<t>An Offline Execution Profile (OEP) enables a device or agent to evaluate grants
and issue provisional receipts without network connectivity to the gateway.</t>

<section anchor="oep-bundle"><name>OEP Bundle</name>
<t>A <tt>gap:offline_bundle</tt> is a gateway-signed CDRO containing everything needed for
offline grant evaluation:</t>
<table>
<thead>
<tr>
<th>Field</th>
<th>Required</th>
<th>Description</th>
</tr>
</thead>

<tbody>
<tr>
<td><tt>grant</tt></td>
<td>yes</td>
<td>Full CapabilityGrant CDRO (inline, not by reference)</td>
</tr>

<tr>
<td><tt>declaration</tt></td>
<td>yes</td>
<td>Full CapabilityDeclaration CDRO for the granted actor</td>
</tr>

<tr>
<td><tt>keyring</tt></td>
<td>yes</td>
<td>KeyringExport CDRO (see Offline Key Distribution)</td>
</tr>

<tr>
<td><tt>revocation_snapshot</tt></td>
<td>yes</td>
<td>RevocationSnapshot CDRO (see Offline Revocation)</td>
</tr>

<tr>
<td><tt>offline_policy</tt></td>
<td>yes</td>
<td>OfflinePolicy block</td>
</tr>
</tbody>
</table><t>OfflinePolicy:</t>
<table>
<thead>
<tr>
<th>Field</th>
<th>Required</th>
<th>Description</th>
</tr>
</thead>

<tbody>
<tr>
<td><tt>max_offline_duration_ms</tt></td>
<td>yes</td>
<td>Maximum duration the bundle is valid for offline use</td>
</tr>

<tr>
<td><tt>max_offline_invocations</tt></td>
<td>yes</td>
<td>Maximum invocations permitted during the offline period</td>
</tr>

<tr>
<td><tt>offline_capability_filter</tt></td>
<td>no</td>
<td>Array of capability patterns permitted offline; absent = all granted capabilities permitted</td>
</tr>

<tr>
<td><tt>offline_allowed</tt></td>
<td>no</td>
<td>Boolean. For safety<em>class C with physical</em>safety: true, offline operation MUST NOT proceed unless this is explicitly true</td>
</tr>
</tbody>
</table><t>OEP bundles are fetched via <tt>GET /v1/gap/offline-bundle?grant_oid=&lt;oid&gt;</tt> and expire at <tt>expires_at_ms</tt>.</t>
</section>

<section anchor="offline-evaluation"><name>Offline Evaluation</name>
<t>When operating offline, the device:</t>

<ol>
<li><t>Verifies the OEP bundle signature against the locally-held root public key</t>
</li>
<li><t>Checks <tt>grant.expires_at_ms</tt> against local clock</t>
</li>
<li><t>Checks <tt>offline_policy.max_offline_duration_ms</tt> from bundle issuance</t>
</li>
<li><t>Evaluates <tt>scope_narrowing</tt> against invocation args</t>
</li>
<li><t>Increments local invocation counter; denies if <tt>max_offline_invocations</tt> is reached</t>
</li>
<li><t>Issues a provisional receipt signed with the device-local key pre-provisioned at enrollment</t>
</li>
</ol>
<t>Provisional receipts carry <tt>status: 'ok:offline'</tt> and include <tt>oep_bundle_oid</tt> referencing the OEP bundle.</t>
<t>For safety<em>class C capabilities with physical</em>safety: true, offline operation MUST NOT proceed
unless the OEP bundle's <tt>offline_policy.offline_allowed</tt> is explicitly true.</t>
</section>

<section anchor="reconciliation"><name>Reconciliation</name>
<t>On reconnection, the device submits accumulated provisional receipts via
<tt>POST /v1/gap/offline-receipts</tt> (array of provisional receipt CDROs).</t>
<t>The gateway:
1. Validates each provisional receipt against the referenced OEP bundle
2. Issues an authoritative receipt (status: ok) or rejection receipt (status: denied)
3. Rejection does not retroactively void the physical action but establishes the audit record</t>
</section>

<section anchor="sovereign-mode"><name>Sovereign Mode</name>
<t>For fully self-hosted deployments with no external connectivity, operators MAY run a
locally-operated gateway with a locally-generated root-of-trust keypair. In sovereign mode:
- All CDRO types are valid with locally-generated OIDs
- The root public key is distributed at device provisioning time
- No SynOI infrastructure is required at any point in the lifecycle</t>
</section>
</section>

<section anchor="workflows"><name>Workflows</name>

<section anchor="purpose-3"><name>Purpose</name>
<t>A WorkflowDefinition describes a multi-stage Human-in-the-Loop (HITL) process
triggered by a specific capability invocation. When a grant's <tt>pending_workflow</tt>
field references a workflow definition, the gateway instantiates a
WorkflowInstance instead of immediately producing a terminal receipt.</t>
</section>

<section anchor="workflow-lifecycle"><name>Workflow Lifecycle</name>

<ol>
<li><t>On invocation, the gateway emits a receipt with <tt>status: pending</tt>.</t>
</li>
<li><t>The workflow proceeds through stages. Each stage may listen for channel
events (SMS, push notification, webhook, overlay approval).</t>
</li>
<li><t>When the workflow reaches a terminal stage, the gateway emits a NEW receipt
with the terminal status. The pending receipt MUST NOT be modified in place.</t>
</li>
<li><t>The terminal receipt's <tt>capability_grant_oids</tt> MUST include the grant OIDs
from the triggering invocation.</t>
</li>
</ol>
</section>

<section anchor="signal-sender-validation"><name>Signal Sender Validation</name>
<t>A gateway MUST validate the sender of every channel event used as a workflow
signal:</t>

<ul>
<li><t>If a WorkflowStage's <tt>StageListen</tt> specifies <tt>required_from_binding</tt>, the
gateway MUST verify the event's <tt>from</tt> field matches before accepting the
event as a valid stage signal.</t>
</li>
<li><t>For every channel kind on <tt>physical_safety: true</tt> stages, the gateway MUST
verify the channel event's authenticity using the mechanism appropriate to
the channel adapter (e.g. webhook HMAC, push token binding, local credential
verification). No single channel is normatively required for this check; the
Channel Adapter interface defines the verification contract.</t>
</li>
<li><t>The same <tt>actor_oid</tt> MUST NOT be counted as two approvals for the same stage
(no self-counting).</t>
</li>
</ul>

<section anchor="channel-adapters"><name>Channel Adapters</name>
<t>GAP workflow stages deliver signals and receive responses through Channel Adapters.
A Channel Adapter is an implementation of the following interface:</t>

<ul>
<li><t><tt>kind</tt>: a <tt>ChannelKind</tt> string identifying the adapter</t>
</li>
<li><t><tt>performAction(stage, context)</tt>: executes the stage action (sends alert, invokes tool, etc.)</t>
</li>
<li><t><tt>armListen(stage, context)</tt>: arms the adapter to receive inbound signals (reply, button tap, etc.)</t>
</li>
<li><t><tt>health()</tt>: returns whether the adapter is currently operational</t>
</li>
</ul>
<t>The gateway MUST support at minimum the <tt>sms</tt> channel kind. All other channel kinds
are OPTIONAL but MUST conform to this interface when implemented.</t>

<section anchor="defined-channel-kinds"><name>Defined Channel Kinds</name>
<table>
<thead>
<tr>
<th>Kind</th>
<th>Description</th>
<th>Connectivity</th>
</tr>
</thead>

<tbody>
<tr>
<td><tt>sms</tt></td>
<td>SMS message via any provider (Twilio, AWS SNS, etc.)</td>
<td>Internet</td>
</tr>

<tr>
<td><tt>voice</tt></td>
<td>Outbound voice call with IVR response</td>
<td>Internet</td>
</tr>

<tr>
<td><tt>email</tt></td>
<td>Email with approve/deny link</td>
<td>Internet</td>
</tr>

<tr>
<td><tt>slack</tt></td>
<td>Slack message with Block Kit interactive components</td>
<td>Internet</td>
</tr>

<tr>
<td><tt>mobile_push</tt></td>
<td>APNs / FCM push notification</td>
<td>Internet</td>
</tr>

<tr>
<td><tt>sse</tt></td>
<td>Server-Sent Events to a connected dashboard</td>
<td>LAN/Internet</td>
</tr>

<tr>
<td><tt>webhook</tt></td>
<td>HTTP POST to a configured endpoint</td>
<td>LAN/Internet</td>
</tr>

<tr>
<td><tt>in_app</tt></td>
<td>In-app overlay or notification</td>
<td>Local</td>
</tr>

<tr>
<td><tt>game_engine</tt></td>
<td>Game engine event (Unity, Unreal, Godot hook)</td>
<td>Local</td>
</tr>

<tr>
<td><tt>local_terminal</tt></td>
<td>Operator console at the local enforcement point; requires hardware token or biometric</td>
<td>Local/Air-gapped</td>
</tr>

<tr>
<td><tt>hmi_panel</tt></td>
<td>HMI touchscreen or physical operator panel at the device</td>
<td>Local/Air-gapped</td>
</tr>

<tr>
<td><tt>opc_ua_ack</tt></td>
<td>OPC-UA operator acknowledgement signal from a process historian or SCADA terminal</td>
<td>Local/Air-gapped</td>
</tr>

<tr>
<td><tt>local_signed_token</tt></td>
<td>Physical signed token (QR code, smart card, NFC) scanned at the device</td>
<td>Local/Air-gapped</td>
</tr>
</tbody>
</table><t>Custom channel kinds MAY be used by implementing the Channel Adapter interface.
The <tt>kind</tt> field accepts any string value; non-standard kinds SHOULD use a
reverse-domain prefix (e.g. <tt>com.example.pager</tt>).</t>
</section>

<section anchor="air-gapped-hitl"><name>Air-Gapped HITL</name>
<t>For deployments without external connectivity, air-gapped channel types
(<tt>local_terminal</tt>, <tt>hmi_panel</tt>, <tt>opc_ua_ack</tt>, <tt>local_signed_token</tt>) produce
stage transition CDROs signed by the local gateway instance. These are
synchronized to the cloud gateway at next connectivity to establish the
complete receipt chain.</t>
<t>Local channel adapters authenticate the operator using locally-held credentials:
- <tt>local_terminal</tt>: smart card, hardware token, or biometric; verified against locally-registered public key
- <tt>hmi_panel</tt>: operator badge scan or PIN verified against local roster CDRO
- <tt>opc_ua_ack</tt>: OPC-UA NodeId-scoped acknowledgement from an authorized operator session
- <tt>local_signed_token</tt>: scanned CDRO (QR or NFC) verified against locally-held root public key</t>
<t>The signal sender validation rule (<tt>required_from_binding</tt>) applies to all channel kinds.
For local channels, <tt>required_from_binding</tt> references the operator's local actor OID
rather than a phone number or Slack user ID.</t>
</section>
</section>
</section>

<section anchor="two-person-integrity"><name>Two-Person Integrity</name>
<t>A stage MAY specify <tt>authorized_approvers</tt> (array of actor OIDs). When set:</t>

<ul>
<li><t>The gateway MUST verify the signal sender resolves to one of the listed OIDs.</t>
</li>
<li><t>The workflow actor (the actor who triggered the invocation) MUST NOT serve as
an approver (no self-approval).</t>
</li>
</ul>
</section>

<section anchor="safety-constraints-on-workflow-definitions"><name>Safety Constraints on Workflow Definitions</name>
<t>For any stage triggered by a <tt>physical_safety: true</tt> or <tt>safety_class: C</tt>
capability, a gateway MUST reject the workflow definition registration if any
<tt>on_timeout</tt> path leads to a terminal stage with <tt>terminal_outcome: approved</tt>.
Timeout MUST NOT produce approval for physical-safety capabilities.</t>
<t>The minimum <tt>duration_seconds</tt> for any stage triggered by a <tt>safety_class: C</tt>
capability is 30 seconds.</t>
</section>
</section>

<section anchor="revocation"><name>Revocation</name>

<section anchor="revocation-kinds"><name>Revocation Kinds</name>
<table>
<thead>
<tr>
<th>Kind</th>
<th>Effect</th>
<th>Use case</th>
</tr>
</thead>

<tbody>
<tr>
<td>Immediate (<tt>L1</tt>)</td>
<td>Grant denied from <tt>effective_at_ms</tt> forward</td>
<td>Contractor access removal</td>
</tr>

<tr>
<td>Scheduled (<tt>L2</tt>)</td>
<td>Grant denied after <tt>effective_at_ms</tt></td>
<td>Expiry enforcement</td>
</tr>

<tr>
<td>Provisional block</td>
<td>Capability temporarily blocked pending L3 quorum</td>
<td>Anomaly detection</td>
</tr>

<tr>
<td>L3 quorum</td>
<td>Block becomes permanent on quorum; may auto-renew</td>
<td>High-stakes revocation</td>
</tr>
</tbody>
</table></section>

<section anchor="provisional-block-policy"><name>Provisional Block Policy</name>
<t>A RevocationEvent with <tt>revocation_kind: provisional_block</tt> MAY carry
<tt>provisional_block_policy</tt>:</t>
<table>
<thead>
<tr>
<th>Field</th>
<th>Description</th>
</tr>
</thead>

<tbody>
<tr>
<td><tt>on_expiry_without_quorum</tt></td>
<td><tt>renew</tt> or <tt>revert</tt></td>
</tr>

<tr>
<td><tt>min_approvers</tt></td>
<td>Minimum approver count for quorum</td>
</tr>

<tr>
<td><tt>provisional_block_ttl_ms</tt></td>
<td>Operator override for the block TTL. Defaults to 72 hours. Minimum: 1 hour.</td>
</tr>
</tbody>
</table><t>The provisional block TTL defaults to 72 hours. Operators MAY set <tt>provisional_block_ttl_ms</tt> on the RevocationEventBody to override. Minimum: 1 hour. For safety_class C capabilities with <tt>on_expiry_without_quorum: renew</tt>, the renewal cycle period equals <tt>provisional_block_ttl_ms</tt>.</t>
<t>When the provisional block TTL expires and quorum has not been reached:</t>

<ul>
<li><t>If <tt>on_expiry_without_quorum</tt> is <tt>renew</tt> (or the field is absent on a
<tt>physical_safety: true</tt> grant): the block MUST auto-renew. The renewal MUST
produce a receipt with <tt>subject_kind: provisional_block</tt> and <tt>status: pending</tt>.</t>
</li>
<li><t>If <tt>on_expiry_without_quorum</tt> is <tt>revert</tt>: the block is lifted (capability
re-enabled). The <tt>revert</tt> value MUST NOT be used for <tt>physical_safety: true</tt>
capabilities regardless of the field value.</t>
</li>
</ul>
</section>

<section anchor="offline-revocation-bundle"><name>Offline Revocation Bundle</name>
<t>A <tt>gap:revocation_bundle</tt> enables offline devices to check grant revocation status
without contacting the gateway.</t>
<t>RevocationBundleBody:</t>
<table>
<thead>
<tr>
<th>Field</th>
<th>Required</th>
<th>Description</th>
</tr>
</thead>

<tbody>
<tr>
<td><tt>revocations</tt></td>
<td>yes</td>
<td>Array of RevocationEntry</td>
</tr>

<tr>
<td><tt>snapshot_at_ms</tt></td>
<td>yes</td>
<td>Unix epoch ms when the snapshot was taken</td>
</tr>

<tr>
<td><tt>expires_at_ms</tt></td>
<td>yes</td>
<td>Maximum age of this bundle; devices MUST NOT use it after this time</td>
</tr>

<tr>
<td><tt>tenant_id</tt></td>
<td>yes</td>
<td>Tenant this bundle applies to</td>
</tr>
</tbody>
</table><t>Each RevocationEntry:</t>
<table>
<thead>
<tr>
<th>Field</th>
<th>Required</th>
<th>Description</th>
</tr>
</thead>

<tbody>
<tr>
<td><tt>grant_oid</tt></td>
<td>yes</td>
<td>OID of the revoked grant</td>
</tr>

<tr>
<td><tt>effective_at_ms</tt></td>
<td>yes</td>
<td>When the revocation became effective</td>
</tr>

<tr>
<td><tt>kind</tt></td>
<td>yes</td>
<td><tt>immediate</tt>, <tt>scheduled</tt>, or <tt>provisional_block</tt></td>
</tr>
</tbody>
</table><t>Export via <tt>GET /v1/gap/revocations/bundle?since_ms=&lt;ms&gt;</tt>. The bundle is gateway-signed.</t>
<t>Fail-safe rules for offline devices:</t>

<ul>
<li><t>Devices operating with <tt>physical_safety: true</tt> capabilities MUST require a valid (not-expired)
revocation bundle as a precondition for any invocation</t>
</li>
<li><t>If the device cannot obtain a fresh-enough bundle before <tt>expires_at_ms</tt>, the fail-safe action
for <tt>physical_safety: true</tt> and safety_class C capabilities MUST be DENY</t>
</li>
<li><t>The maximum acceptable bundle age is set by <tt>max_revocation_bundle_age_ms</tt> on the grant;
if absent, the gateway default applies (RECOMMENDED: 24 hours for class C, 7 days for class A/B)</t>
</li>
</ul>
</section>

<section anchor="break-glass-grants"><name>Break-Glass Grants</name>
<t>A break-glass grant pre-authorizes a defined set of emergency capabilities with
an offline-verifiable signed token, for use when the gateway is unreachable and
immediate action is required for safety or clinical reasons.</t>

<section anchor="break-glass-grant-fields"><name>Break-Glass Grant Fields</name>
<t>A grant with <tt>break_glass: true</tt> additionally carries:</t>
<table>
<thead>
<tr>
<th>Field</th>
<th>Required</th>
<th>Description</th>
</tr>
</thead>

<tbody>
<tr>
<td><tt>break_glass</tt></td>
<td>yes</td>
<td>Boolean true. Marks this grant as a break-glass grant.</td>
</tr>

<tr>
<td><tt>break_glass_token</tt></td>
<td>yes</td>
<td>A signed CDRO of type <tt>gap:break_glass_token</tt> pre-issued by the gateway and stored securely on the device.</td>
</tr>

<tr>
<td><tt>break_glass_ttl_ms</tt></td>
<td>yes</td>
<td>TTL of the break-glass token in milliseconds from issuance. RECOMMENDED: 4 hours.</td>
</tr>

<tr>
<td><tt>break_glass_max_invocations</tt></td>
<td>no</td>
<td>Maximum invocations allowed under this token before it is exhausted. Defaults to 1 for safety_class C.</td>
</tr>

<tr>
<td><tt>break_glass_requires_reason</tt></td>
<td>no</td>
<td>Boolean. When true, the invoker MUST supply a <tt>break_glass_reason</tt> string in invocation args.</td>
</tr>
</tbody>
</table></section>

<section anchor="break-glass-token"><name>Break-Glass Token</name>
<t>A <tt>gap:break_glass_token</tt> CDRO body:</t>
<table>
<thead>
<tr>
<th>Field</th>
<th>Required</th>
<th>Description</th>
</tr>
</thead>

<tbody>
<tr>
<td><tt>grant_oid</tt></td>
<td>yes</td>
<td>OID of the break-glass grant this token activates</td>
</tr>

<tr>
<td><tt>actor_oid</tt></td>
<td>yes</td>
<td>OID of the authorized invoker</td>
</tr>

<tr>
<td><tt>valid_from_ms</tt></td>
<td>yes</td>
<td>Token validity start (Unix epoch ms)</td>
</tr>

<tr>
<td><tt>expires_at_ms</tt></td>
<td>yes</td>
<td>Token validity end (Unix epoch ms)</td>
</tr>

<tr>
<td><tt>permitted_capabilities</tt></td>
<td>yes</td>
<td>Array of capability patterns this token authorizes (subset of the grant)</td>
</tr>

<tr>
<td><tt>max_invocations</tt></td>
<td>yes</td>
<td>Maximum invocations before token is exhausted</td>
</tr>
</tbody>
</table><t>The token is gateway-signed at provisioning time. The device verifies the signature using
the locally-held public key (from the key bundle, per Offline Key Distribution) before
activating break-glass operation.</t>
</section>

<section anchor="break-glass-invocation"><name>Break-Glass Invocation</name>
<t>A break-glass invocation MUST carry <tt>break_glass_token_oid</tt> in the invocation args.
The enforcement point verifies:</t>

<ol>
<li><t>The token OID matches a locally-held break-glass token CDRO</t>
</li>
<li><t>The token signature is valid</t>
</li>
<li><t>The token has not expired (<tt>expires_at_ms</tt> &gt; current local clock)</t>
</li>
<li><t>The invoked capability matches <tt>permitted_capabilities</tt></t>
</li>
<li><t>The token invocation counter has not reached <tt>max_invocations</tt></t>
</li>
<li><t>If <tt>break_glass_requires_reason: true</tt>, <tt>break_glass_reason</tt> is non-empty</t>
</li>
</ol>
<t>Break-glass invocations produce a mandatory provisional receipt with
<tt>compliance_tags: ['break_glass']</tt>. The device MUST submit these receipts to
<tt>POST /v1/gap/offline-receipts</tt> at next connectivity.</t>
</section>

<section anchor="local-override-credential"><name>Local Override Credential</name>
<t>For lifting a provisional block when the gateway is unreachable, an operator MAY
provision a Local Override Credential (LOC) at grant issuance time.</t>
<t>A <tt>gap:local_override_credential</tt> CDRO body:</t>
<table>
<thead>
<tr>
<th>Field</th>
<th>Required</th>
<th>Description</th>
</tr>
</thead>

<tbody>
<tr>
<td><tt>grant_oid</tt></td>
<td>yes</td>
<td>OID of the provisionally-blocked grant</td>
</tr>

<tr>
<td><tt>actor_oid</tt></td>
<td>yes</td>
<td>OID of the authorized override operator</td>
</tr>

<tr>
<td><tt>expires_at_ms</tt></td>
<td>yes</td>
<td>Credential expiry</td>
</tr>

<tr>
<td><tt>single_use</tt></td>
<td>yes</td>
<td>Always true. An LOC MUST be invalidated after first use.</td>
</tr>

<tr>
<td><tt>override_reason_required</tt></td>
<td>yes</td>
<td>Boolean. When true, operator MUST supply a reason string.</td>
</tr>
</tbody>
</table><t>The LOC is signed by the operator's key at grant issuance time and stored physically
at the installation site (QR code, USB token, or printed secure storage).
When presented at the local enforcement point, it lifts the provisional block exactly once
and produces a local override receipt synchronized at next uplink.</t>
<t>Neither break-glass nor LOC mechanism allows indefinite ungoverned operation.
Both produce mandatory audit trails submitted to the gateway at next connectivity.</t>
</section>
</section>

<section anchor="l3-quorum-approval"><name>L3 Quorum Approval</name>
<t>A revocation event may collect approvals via <tt>POST /v1/gap/revoke/approve</tt>.
Each approval MUST:</t>

<ul>
<li><t>Identify a unique <tt>approver_actor_oid</tt> (no duplicate approvers).</t>
</li>
<li><t>Not be made by the same actor who created the revocation event (no
self-approval).</t>
</li>
</ul>
<t>When <tt>min_approvers</tt> is reached, the gateway MUST set <tt>effective_at_ms</tt> on the
revocation event and enforce the revocation.</t>
</section>
</section>

<section anchor="http-api-surface"><name>HTTP API Surface</name>
<t>A GAP-conformant gateway MUST expose the following endpoints under a base path
(conventionally <tt>/v1/gap</tt>). All endpoints require Bearer token authentication.
Tenant isolation MUST be enforced: every CDRO fetch endpoint MUST verify
<tt>stored_object.tenant_id === authenticated_tenant_id</tt>. On mismatch, the gateway
MUST return HTTP 404 (never HTTP 403, which would confirm cross-tenant OID
existence).</t>

<section anchor="core-endpoints"><name>Core Endpoints</name>
<table>
<thead>
<tr>
<th>Method</th>
<th>Path</th>
<th>Description</th>
</tr>
</thead>

<tbody>
<tr>
<td>POST</td>
<td><tt>/declarations</tt></td>
<td>Register a CapabilityDeclaration</td>
</tr>

<tr>
<td>GET</td>
<td><tt>/declarations/:oid</tt></td>
<td>Fetch a declaration by OID</td>
</tr>

<tr>
<td>POST</td>
<td><tt>/grants</tt></td>
<td>Issue a CapabilityGrant</td>
</tr>

<tr>
<td>GET</td>
<td><tt>/grants</tt></td>
<td>List grants (query: <tt>actor_oid</tt>, <tt>capability</tt>, <tt>status</tt>)</td>
</tr>

<tr>
<td>GET</td>
<td><tt>/grants/:oid</tt></td>
<td>Fetch a grant by OID</td>
</tr>

<tr>
<td>POST</td>
<td><tt>/invoke</tt></td>
<td>Invoke a capability</td>
</tr>

<tr>
<td>POST</td>
<td><tt>/invoke/batch</tt></td>
<td>Submit up to 1000 invocations sharing a tenant in a single request (L2 OPTIONAL). Returns array of receipts in submission order. Grant evaluation is independent per invocation.</td>
</tr>

<tr>
<td>GET</td>
<td><tt>/receipts</tt></td>
<td>List receipts (query: <tt>actor_oid</tt>, <tt>grant_oid</tt>, <tt>capability</tt>, <tt>from_ms</tt>, <tt>to_ms</tt>, <tt>status</tt>, <tt>limit</tt>, <tt>cursor</tt>)</td>
</tr>

<tr>
<td>GET</td>
<td><tt>/receipts/:oid</tt></td>
<td>Fetch a receipt by OID</td>
</tr>

<tr>
<td>GET</td>
<td><tt>/limits/:pool_id</tt></td>
<td>Returns current rolling sum and remaining headroom for a named aggregate limit pool.</td>
</tr>
</tbody>
</table></section>

<section anchor="revocation-endpoints"><name>Revocation Endpoints</name>
<table>
<thead>
<tr>
<th>Method</th>
<th>Path</th>
<th>Description</th>
</tr>
</thead>

<tbody>
<tr>
<td>POST</td>
<td><tt>/revoke</tt></td>
<td>Immediate or scheduled revocation</td>
</tr>

<tr>
<td>POST</td>
<td><tt>/revoke/provisional-block</tt></td>
<td>Provisional block (anomaly response)</td>
</tr>

<tr>
<td>POST</td>
<td><tt>/revoke/approve</tt></td>
<td>Submit L3 quorum approval</td>
</tr>

<tr>
<td>GET</td>
<td><tt>/revocations/:oid</tt></td>
<td>Fetch a revocation event by OID</td>
</tr>

<tr>
<td>GET</td>
<td><tt>/revocations</tt></td>
<td>List revocation events (query: <tt>grant_oid</tt>, <tt>target_kind</tt>, <tt>since_ms</tt>)</td>
</tr>
</tbody>
</table></section>

<section anchor="workflow-endpoints"><name>Workflow Endpoints</name>
<table>
<thead>
<tr>
<th>Method</th>
<th>Path</th>
<th>Description</th>
</tr>
</thead>

<tbody>
<tr>
<td>POST</td>
<td><tt>/workflows/definitions</tt></td>
<td>Register a WorkflowDefinition</td>
</tr>

<tr>
<td>GET</td>
<td><tt>/workflows/definitions/:oid</tt></td>
<td>Fetch a definition by OID</td>
</tr>

<tr>
<td>GET</td>
<td><tt>/workflows/instances/:oid</tt></td>
<td>Fetch a workflow instance</td>
</tr>

<tr>
<td>GET</td>
<td><tt>/workflows/instances/:oid/transitions</tt></td>
<td>List stage transitions</td>
</tr>

<tr>
<td>POST</td>
<td><tt>/workflows/signal</tt></td>
<td>Deliver a channel event signal</td>
</tr>
</tbody>
</table></section>

<section anchor="key-endpoints"><name>Key Endpoints</name>
<table>
<thead>
<tr>
<th>Method</th>
<th>Path</th>
<th>Description</th>
</tr>
</thead>

<tbody>
<tr>
<td>GET</td>
<td><tt>/keys/current</tt></td>
<td>Fetch the current signing key (public)</td>
</tr>

<tr>
<td>GET</td>
<td><tt>/keys/:key_id</tt></td>
<td>Fetch a signing key by ID (public)</td>
</tr>

<tr>
<td>GET</td>
<td><tt>/keys/bundle</tt></td>
<td>Fetch a signed keyring export bundle for offline verifiers</td>
</tr>
</tbody>
</table></section>

<section anchor="offline-endpoints"><name>Offline Endpoints</name>
<table>
<thead>
<tr>
<th>Method</th>
<th>Path</th>
<th>Description</th>
</tr>
</thead>

<tbody>
<tr>
<td>GET</td>
<td><tt>/offline-bundle</tt></td>
<td>Fetch an OEP bundle for offline grant evaluation (query: <tt>grant_oid</tt>)</td>
</tr>

<tr>
<td>POST</td>
<td><tt>/offline-receipts</tt></td>
<td>Submit accumulated provisional receipts for reconciliation</td>
</tr>

<tr>
<td>GET</td>
<td><tt>/revocations/bundle</tt></td>
<td>Fetch a signed revocation snapshot bundle (query: <tt>since_ms</tt>)</td>
</tr>
</tbody>
</table></section>
</section>

<section anchor="conformance"><name>Conformance</name>
<t>GAP implementations declare a conformance tier. A higher tier is a superset of
all lower tiers.</t>

<section anchor="tier-l1-object-layer"><name>Tier L1: Object Layer</name>
<t>An L1 implementation:</t>

<ul>
<li><t>MUST validate CDRO envelopes per {{object-model}}.</t>
</li>
<li><t>MUST compute and verify content-addressed OIDs per {{oid-computation}}.</t>
</li>
<li><t>MUST produce GapDecisionReceipts for every gate decision.</t>
</li>
<li><t>MUST enforce tenant isolation on all CDRO fetch operations.</t>
</li>
<li><t>Does NOT enforce scope_narrowing evaluation (steps 1-3 of grant evaluation
only: expiry, revocation, grantee match).</t>
</li>
<li><t>A grant containing any GrantedCapabilityScope with non-empty <tt>scope_narrowing</tt> MUST be rejected by an L1-conformant gateway at grant issuance time with HTTP 400 and error type <tt>tier_insufficient</tt>.</t>
</li>
</ul>
</section>

<section anchor="tier-l2-grant-evaluation-layer"><name>Tier L2: Grant Evaluation Layer</name>
<t>An L2 implementation satisfies L1 and additionally:</t>

<ul>
<li><t>MUST enforce scope_narrowing per {{scope-narrowing}}.</t>
</li>
<li><t>MUST enforce delegation subset rules per {{delegation}}.</t>
</li>
<li><t>MUST enforce invocation timestamp validation.</t>
</li>
<li><t>MUST implement idempotency per {{idempotency}}.</t>
</li>
<li><t>MUST enforce aggregate_limits rolling windows.</t>
</li>
</ul>
</section>

<section anchor="tier-l3-workflow-layer"><name>Tier L3: Workflow Layer</name>
<t>An L3 implementation satisfies L2 and additionally:</t>

<ul>
<li><t>MUST implement workflow instantiation, stage transitions, and HITL channel
signaling.</t>
</li>
<li><t>MUST implement optional_effects evaluation.</t>
</li>
<li><t>MUST implement provisional block with quorum semantics.</t>
</li>
<li><t>MUST emit the complete pending + terminal receipt chain for every workflow.</t>
</li>
</ul>
</section>

<section anchor="tier-l4-federation-layer"><name>Tier L4: Federation Layer</name>
<t>An L4 implementation satisfies L3 and additionally:</t>

<ul>
<li><t>MUST implement L3 quorum revocation with multi-party approval.</t>
</li>
<li><t>MUST implement cross-tenant receipt verification.</t>
</li>
<li><t>MUST implement federation handshakes.</t>
</li>
</ul>

<section anchor="l3-pq-and-l4-pq-post-quantum-only"><name>L3-PQ and L4-PQ (Post-Quantum Only)</name>
<t>For deployments under CNSA 2.0 (NSS policy) or equivalent post-quantum mandates where Ed25519 is not an approved algorithm:</t>
<table>
<thead>
<tr>
<th>Tier</th>
<th>Signing</th>
<th>Use case</th>
</tr>
</thead>

<tbody>
<tr>
<td>L3-PQ</td>
<td>ML-DSA-65 only (no Ed25519)</td>
<td>Classified / NSS deployments requiring CNSA 2.0 compliance</td>
</tr>

<tr>
<td>L4-PQ</td>
<td>ML-DSA-65 only + authorized axis</td>
<td>Classified / regulated deployments requiring PQ + full audit chain</td>
</tr>
</tbody>
</table><t>The existing L4 hybrid mode (Ed25519 + ML-DSA-65) remains for interoperability with non-classified systems. Pure ML-DSA-65 operation without Ed25519 is the conformant path for CNSA 2.0 deployments. The <tt>signature_algorithm</tt> field in the CDRO envelope (value: <tt>ML-DSA-65</tt>) identifies PQ-only receipts.</t>
<t>Security Considerations: Ed25519 is not approved for National Security Systems under CNSSP-15 and CNSA 2.0. New NSS deployments MUST use L3-PQ or L4-PQ minimum.</t>
</section>
</section>

<section anchor="conformance-by-sector"><name>Conformance by Sector</name>
<table>
<thead>
<tr>
<th>Sector</th>
<th>Minimum Tier</th>
<th>Rationale</th>
</tr>
</thead>

<tbody>
<tr>
<td>MCP tool governance</td>
<td>L2</td>
<td>Scope enforcement mandatory</td>
</tr>

<tr>
<td>AI agent pipelines</td>
<td>L2</td>
<td>Delegation chains required</td>
</tr>

<tr>
<td>Smart home (consumer)</td>
<td>L2</td>
<td>User-issued grants with scope</td>
</tr>

<tr>
<td>Industrial / OT</td>
<td>L3</td>
<td>HITL + provisional block required</td>
</tr>

<tr>
<td>Medical device / clinical (21 CFR Part 11)</td>
<td>L4 MINIMUM</td>
<td>HITL + signed receipts + authorized axis required. L3 without signed receipts does not satisfy 21 CFR Part 11 Section 11.50 electronic signature requirements. An L3 deployment for medical devices is non-conformant to Part 11.</td>
</tr>

<tr>
<td>Physical security</td>
<td>L3</td>
<td>HITL + two-person integrity</td>
</tr>

<tr>
<td>Cross-tenant / federated</td>
<td>L4</td>
<td>Federation semantics</td>
</tr>
</tbody>
</table></section>
</section>

<section anchor="security-considerations"><name>Security Considerations</name>

<section anchor="oid-integrity"><name>OID Integrity</name>
<t>The security of GAP receipts depends on the integrity of OID computation. An
implementation MUST compute OIDs using the canonical JSON form defined in
{{canonical-json}}. Divergence in canonical form between the issuing gateway
and a verifier produces OID mismatch, which is detectable but MUST be
investigated; it indicates either a bug or an active tampering attempt.</t>
<t>Implementations across different languages MUST independently verify that their
canonical JSON output matches the test vectors in the <tt>@synoi/gap</tt> TypeScript
package and the Python <tt>synoi-gap</tt> package. The pinned test vectors in
<tt>test/conformance.test.ts</tt> serve as cross-implementation reference points.</t>
</section>

<section anchor="signature-verification"><name>Signature Verification</name>

<section anchor="when-signing-is-required"><name>When signing is required</name>
<t>A gateway MUST sign a receipt with Ed25519 <xref target="RFC8032"></xref> when any of the
following are true:</t>

<ol>
<li><t>The effective <tt>require_signed_receipt</tt> for the capability is <tt>true</tt>.
The effective value is determined by evaluating in precedence order:
(a) <tt>GrantedCapabilityScope.require_signed_receipt</tt> if present;
(b) <tt>CapabilitySpec.require_signed_receipt</tt> if present;
(c) the gateway's configured default signing policy.</t>
</li>
<li><t>The gateway is configured to sign all receipts by default and neither
the grant nor the declaration has suppressed signing with
<tt>require_signed_receipt: false</tt>.</t>
</li>
<li><t>The capability's effective <tt>privacy_classification</tt> is <tt>financial</tt> or matches
<tt>financial.*</tt>, OR the deployment asserts 21 CFR Part 11 compliance. For
capabilities in these categories, <tt>require_signed_receipt</tt> MUST default to
<tt>true</tt> and MUST NOT be overridable to <tt>false</tt> by operator grant override.</t>
</li>
</ol>
<t>A gateway MAY be configured with a <tt>safety_class_signing_floor</tt> that sets the
minimum safety_class for which signatures are mandatory regardless of
<tt>require_signed_receipt</tt> settings (e.g. <tt>B</tt> means all class B and C receipts
are signed). Gateway configuration SHOULD document whether
<tt>safety_class_signing_floor</tt> is set, as it affects the signing behavior for
every tenant on that deployment.</t>
<t>A gateway MUST NOT sign a receipt when the effective <tt>require_signed_receipt</tt>
is <tt>false</tt>, regardless of the server's default policy. This allows actors to
opt specific high-frequency capabilities out of signing cost.</t>
<t>When signing is not required, the <tt>signature</tt> and <tt>signature_key_id</tt> fields
MUST be omitted from the receipt envelope.</t>
</section>

<section anchor="verifier-obligations"><name>Verifier obligations</name>
<t>Verifiers MUST:</t>

<ul>
<li><t>Fetch the signing key from the gateway's <tt>/keys/:key_id</tt> endpoint using the
<tt>signature_key_id</tt> from the receipt.</t>
</li>
<li><t>Verify that the key's <tt>expires_at_ms</tt> has not passed.</t>
</li>
<li><t>Verify the signature over the canonical envelope (with the same field
exclusions used for OID computation).</t>
</li>
</ul>
<t>A verifier that receives a receipt without a <tt>signature</tt> field for a
capability where <tt>require_signed_receipt</tt> is <tt>true</tt> MUST treat the receipt
as invalid.</t>
<t>Key rotation: a gateway MUST publish a new key before retiring an old one. Old
keys remain valid for verifying receipts signed under them. The current key is
always the one returned by <tt>GET /keys/current</tt>.</t>
</section>

<section anchor="offline-key-distribution"><name>Offline Key Distribution</name>
<t>A <tt>gap:keyring_export</tt> CDRO enables verifiers to operate without a live <tt>/keys</tt> endpoint.</t>
<t>KeyringExportBody:</t>
<table>
<thead>
<tr>
<th>Field</th>
<th>Required</th>
<th>Description</th>
</tr>
</thead>

<tbody>
<tr>
<td><tt>keys</tt></td>
<td>yes</td>
<td>Array of KeyEntry</td>
</tr>

<tr>
<td><tt>exported_at_ms</tt></td>
<td>yes</td>
<td>Unix epoch ms when the bundle was generated</td>
</tr>

<tr>
<td><tt>expires_at_ms</tt></td>
<td>yes</td>
<td>Bundle validity window</td>
</tr>
</tbody>
</table><t>Each KeyEntry:</t>
<table>
<thead>
<tr>
<th>Field</th>
<th>Required</th>
<th>Description</th>
</tr>
</thead>

<tbody>
<tr>
<td><tt>key_id</tt></td>
<td>yes</td>
<td>Key identifier matching <tt>signature_key_id</tt> on receipts</td>
</tr>

<tr>
<td><tt>public_key_base64</tt></td>
<td>yes</td>
<td>Base64url-encoded public key bytes</td>
</tr>

<tr>
<td><tt>algorithm</tt></td>
<td>yes</td>
<td><tt>Ed25519</tt>, <tt>ML-DSA-65</tt>, or <tt>Ed25519+ML-DSA-65</tt></td>
</tr>

<tr>
<td><tt>valid_from_ms</tt></td>
<td>yes</td>
<td>Key validity start</td>
</tr>

<tr>
<td><tt>expires_at_ms</tt></td>
<td>yes</td>
<td>Key validity end</td>
</tr>
</tbody>
</table><t>The bundle is signed by the gateway's current root key so verifiers can authenticate it.
Export via <tt>GET /v1/gap/keys/bundle</tt>. The bundle is distributable as a file, QR code, or USB
for air-gapped deployments.</t>
<t>Offline verifier rules:
1. Load the bundle at provisioning time; verify bundle signature against locally-installed root public key
2. On receipt verification, look up <tt>key_id</tt> in local bundle only
3. If <tt>key_id</tt> is not in the bundle, treat receipt as UNVERIFIABLE (not INVALID); the key may exist but has not been delivered yet
4. If the bundle is expired, treat all receipts issued after <tt>expires_at_ms</tt> as UNVERIFIABLE until a fresh bundle is obtained</t>
<t>A <tt>signature_algorithm</tt> field MUST be added to the CDRO envelope (alongside <tt>signature</tt> and <tt>signature_key_id</tt>):
values: <tt>Ed25519</tt>, <tt>ML-DSA-65</tt>, <tt>Ed25519+ML-DSA-65</tt>. Verifiers MUST NOT guess the algorithm from key length.</t>
</section>
</section>

<section anchor="tenant-isolation"><name>Tenant Isolation</name>
<t>A gateway MUST NOT return a CDRO belonging to one tenant in response to a
request authenticated as another tenant. The response for cross-tenant or
not-found OIDs MUST be HTTP 404 to avoid confirming cross-tenant OID existence.</t>
</section>

<section anchor="delegation-chain-security"><name>Delegation Chain Security</name>
<t>A gateway MUST evaluate the full delegation chain on every invocation. A child
grant whose parent has been revoked or expired MUST be denied, even if the
child grant itself has not expired. Partial chain validation is insufficient.</t>
</section>

<section anchor="provisional-block-fail-closed"><name>Provisional Block Fail-Closed</name>
<t>For <tt>physical_safety: true</tt> grants, a gateway MUST treat the absence of
<tt>on_expiry_without_quorum</tt> as <tt>renew</tt>. A gateway MUST NOT allow <tt>revert</tt>
semantics for physical-safety capabilities regardless of the field value. This
ensures that a gateway with misconfigured or absent provisional block policy
defaults to the safer state (block maintained).</t>
</section>

<section anchor="workflow-signal-injection"><name>Workflow Signal Injection</name>
<t>A gateway MUST validate the sender identity of every channel event before
advancing a workflow stage. Failure to validate enables unauthorized parties to
advance or terminate HITL workflows by injecting signals. The
<tt>required_from_binding</tt> field provides the expected sender identity; its
enforcement is REQUIRED for <tt>physical_safety: true</tt> stages.</t>
</section>

<section anchor="idempotency-cache-staleness"><name>Idempotency Cache Staleness</name>
<t>An idempotency cache hit MUST NOT return a cached approval receipt without
re-validating that the underlying grant is still active. A grant may be revoked
after the original approval. Returning a cached receipt for a revoked grant
enables unauthorized capability exercise after revocation.</t>
</section>

<section anchor="negative-numeric-scope-values"><name>Negative Numeric Scope Values</name>
<t>For <tt>physical_safety: true</tt> capabilities, a gateway MUST reject invocation
arguments that contain negative numeric values for scope-constrained keys, even
when those values would satisfy an upper-bound check. A negative value for a
physical parameter (e.g. <tt>max_delta_units: -5.0</tt>) is almost always a sign of
argument manipulation rather than a legitimate invocation.</t>
</section>

<section anchor="bearer-token-security"><name>Bearer Token Security</name>
<t>GAP does not define the authentication mechanism for the Bearer tokens used to
authenticate gateway API calls. Implementors MUST use short-lived tokens,
rotate signing keys, and follow <xref target="RFC6750"></xref> for Bearer token handling. Bearer
token theft is not mitigated by this protocol; it is a deployment concern.</t>

<section anchor="self-sovereign-credential-mode"><name>Self-Sovereign Credential Mode</name>
<t>For deployments without external connectivity or without a SynOI-operated token issuer, a Self-Sovereign Credential (SSC) mode provides an alternative authentication path.</t>
<t>In SSC mode:
1. The operator bootstraps a Local Credential Authority (LCA) by generating a local root signing keypair and publishing a <tt>gap:lca_root</tt> CDRO signed by the root key.
2. Actor credentials are short-lived signed tokens bound to <tt>actor_oid</tt> and <tt>tenant_id</tt>, issued by the LCA and signed with ML-DSA-65 (or Ed25519 for non-NSS deployments).
3. The gateway verifies actor credentials using the locally-held LCA public key only.
4. Credential rotation requires only local key material.</t>
<t>The existing <tt>synoi-sk-</tt> Bearer token remains valid for cloud-connected deployments. SSC mode is a normative alternative, not a deployment footnote.</t>
<t>The <tt>gap:lca_root</tt> CDRO type is defined with body: <tt>{ root_public_key_base64: string, algorithm: string, tenant_id: string, valid_from_ms: number, expires_at_ms: number }</tt>.</t>
</section>
</section>
</section>

<section anchor="iana-considerations"><name>IANA Considerations</name>
<t>This document makes no requests to the Internet Assigned Numbers Authority
(IANA) at this time.</t>
<t>Future revisions of this specification MAY request registration of:</t>

<ul>
<li><t>A media type <tt>application/gap+json</tt> for GAP CDRO objects.</t>
</li>
<li><t>A URI scheme <tt>gap:</tt> for portable CDRO references.</t>
</li>
</ul>
</section>

</middle>

<back>
<references><name>Normative References</name>
<xi:include href="https://xml2rfc.ietf.org/public/rfc/bibxml/reference.RFC.2119.xml"/>
<xi:include href="https://xml2rfc.ietf.org/public/rfc/bibxml/reference.RFC.8174.xml"/>
</references>
<references><name>Informative References</name>
<xi:include href="https://xml2rfc.ietf.org/public/rfc/bibxml/reference.RFC.6750.xml"/>
<xi:include href="https://xml2rfc.ietf.org/public/rfc/bibxml/reference.RFC.8259.xml"/>
<xi:include href="https://xml2rfc.ietf.org/public/rfc/bibxml/reference.RFC.7517.xml"/>
<xi:include href="https://xml2rfc.ietf.org/public/rfc/bibxml/reference.RFC.8785.xml"/>
<xi:include href="https://xml2rfc.ietf.org/public/rfc/bibxml/reference.RFC.8032.xml"/>
</references>

<section anchor="normative-references"><name>Normative References</name>

<dl>
<dt><xref target="RFC2119"></xref>:</dt>
<dd><t>Bradner, S., &quot;Key words for use in RFCs to Indicate Requirement Levels&quot;,
BCP 14, RFC 2119, March 1997.</t>
</dd>
<dt><xref target="RFC8174"></xref>:</dt>
<dd><t>Leiba, B., &quot;Ambiguity of Uppercase vs Lowercase in RFC 2119 Key Words&quot;,
BCP 14, RFC 8174, May 2017.</t>
</dd>
<dt><xref target="RFC8032"></xref>:</dt>
<dd><t>Josefsson, S. and I. Liusvaara, &quot;Edwards-Curve Digital Signature Algorithm
(EdDSA)&quot;, RFC 8032, January 2017.</t>
</dd>
<dt><xref target="RFC6750"></xref>:</dt>
<dd><t>Jones, M. and D. Hardt, &quot;The OAuth 2.0 Authorization Framework: Bearer Token
Usage&quot;, RFC 6750, October 2012.</t>
</dd>
</dl>
</section>

<section anchor="informative-references"><name>Informative References</name>

<dl>
<dt><xref target="RFC8259"></xref>:</dt>
<dd><t>Bray, T., Ed., &quot;The JavaScript Object Notation (JSON) Data Interchange
Format&quot;, STD 90, RFC 8259, December 2017.</t>
</dd>
<dt><xref target="RFC8785"></xref>:</dt>
<dd><t>Rundgren, A., Jordan, B., and S. Erdtman, &quot;JSON Canonicalization Scheme
(JCS)&quot;, RFC 8785, June 2020.</t>
</dd>
<dt><xref target="RFC7517"></xref>:</dt>
<dd><t>Jones, M., &quot;JSON Web Key (JWK)&quot;, RFC 7517, May 2015.</t>
</dd>
</dl>
</section>

<section anchor="acknowledgments"><name>Acknowledgments</name>
<t>The editors thank the SynOI protocol team and the early implementors who
provided feedback on the wire format and grant evaluation algorithms during the
cross-sector safety review that produced the ADR_006 safety protocol.</t>
</section>

</back>

</rfc>
