OpenWOP openwop.dev
FieldValue
RFC0010
TitleAuth-Profile Conformance
StatusAccepted
Author(s)David Tufts (@davidscotttufts)
Created2026-05-11
Updated2026-05-12 (Active → Accepted: all acceptance-criteria items satisfied — capabilities.auth block landed in schemas/capabilities.schema.json, four conformance scenarios shipped (auth-api-key-rotation + auth-oauth2-client-credentials + auth-oidc-user-bearer capability-gated, auth-mtls opt-in via OPENWOP_TEST_MTLS=1), conformance/src/lib/oidc-issuer.ts synthetic harness landed (RS256 + ES256 via node:crypto stdlib), conformance/coverage.md §"Capability-gated scenarios" enumerates the four scenarios under per-profile gating, spec/v1/auth-profiles.md cross-references the new capabilities.auth. discovery path while keeping extensions.auth. valid for compatibility, SECURITY/threat-model-auth-profiles.md §3 adds A3 (scope confusion) + A4 (mTLS downgrade) adversaries with scenario bindings and §4 STRIDE rows per profile, SQLite reference host implements openwop-auth-api-key-rotation end-to-end with constant-time dual-candidate checkAuth per INTEROP-MATRIX.md, and CHANGELOG records both the Draft drop and Active landing. The four unresolved questions remain open as documented and may be revisited as additive sub-RFCs without breaking v1 wire compatibility — OAuth2-CC + OIDC + mTLS profile bodies are capability-shaped on the reference host and ready for adopter wiring, which is the documented stance, not a blocker.)
Affectsspec/v1/auth-profiles.md, schemas/capabilities.schema.json, conformance/src/scenarios/ (3 new + 1 opt-in), conformance/src/lib/oidc-issuer.ts (new harness), conformance/coverage.md, INTEROP-MATRIX.md, SECURITY/threat-model-auth-profiles.md, optionally examples/hosts/sqlite/ or postgres/ (reference implementation)
Compatibilityadditive
Supersedes
Superseded by

Summary

Convert the four production-auth profiles in spec/v1/auth-profiles.md (openwop-auth-api-key-rotation, openwop-auth-oauth2-client-credentials, openwop-auth-oidc-user-bearer, openwop-auth-mtls) from prose-only claims into mechanically verified capability advertisements + conformance scenarios. Formalize capabilities.auth as a top-level schema block with profiles[] and per-profile metadata sub-blocks. Add three new conformance scenarios for the routinely-tested profiles plus one opt-in scenario for mTLS where cert provisioning is operator-environmental. Ship a synthetic OIDC issuer harness under conformance/src/lib/oidc-issuer.ts so OIDC validation can be exercised without depending on a real IdP.

Motivation

auth-profiles.md defines four profiles for hosts that need stronger authentication than the baseline API-key bearer contract in auth.md. As of 2026-05-11:

  • The profile prose is FINAL v1 and every profile has a "Conformance gaps to close" line. None of those gaps are closed.
  • conformance/src/scenarios/auth.test.ts exercises only the baseline (missing credential → 401; invalid credential → 401). It does not cover rotation grace windows, OAuth2 token shapes, OIDC issuer validation, or mTLS.
  • The Postgres reference host advertises openwop-audit-log-integrity (which lives under the same capabilities.auth namespace these new profiles will occupy) but no host advertises any of the four production-auth profiles, because there is no published conformance evidence to point at.
  • The external security review tracked in SECURITY/external-audit-engagement.md (per docs/PROTOCOL-GAP-CLOSURE-PLAN.md Track 9) needs mechanically-verified auth surface before vendor outreach is credible — "we have prose for auth profiles" is weaker than "the conformance suite asserts rotation grace, OAuth shapes, OIDC issuer validation, and mTLS handling."

This RFC ships four artifacts as one coherent additive landing because the profiles share infrastructure (the capabilities.auth namespace, the canonical-envelope assertion pattern, threat-model overlap, and INTEROP-MATRIX advertisement). One consolidated RFC keeps the comment window shared; splitting into four parallel RFCs would multiply maintainer overhead without adding review fidelity. If a specific profile turns out to need a follow-up wire-shape decision, additive sub-RFCs remain possible.

Proposal

§A Capability shape

Add a top-level capabilities.auth block to schemas/capabilities.schema.json. The block is optional in v1; absence preserves the baseline auth contract. additionalProperties: true is preserved so the existing informal auth.profiles[] + auth.auditLogIntegrity shape (used by audit-log-integrity.test.ts) continues to work and is not blocked by this RFC.

Schema diff (relative to schemas/capabilities.schema.json):

   "properties": {
     ...
     "production": { ... },          // RFC 0009
+    "auth": {
+      "type": "object",
+      "description": "Auth-profile advertisement (auth-profiles.md). Optional in v1; absence preserves the baseline bearer-token contract from auth.md. RFC 0010 formalized this block; `additionalProperties: true` permits existing informal usages (auth.profiles[], auth.auditLogIntegrity from the audit-log-integrity profile) to continue alongside the formal sub-blocks.",
+      "properties": {
+        "profiles": {
+          "type": "array",
+          "items": { "type": "string", "minLength": 1 },
+          "uniqueItems": true,
+          "description": "Auth profiles the host claims. Canonical ids: `openwop-audit-log-integrity` (auth-profiles.md §Audit-log integrity), `openwop-auth-api-key-rotation`, `openwop-auth-oauth2-client-credentials`, `openwop-auth-oidc-user-bearer`, `openwop-auth-mtls`. Clients SHOULD tolerate unknown profile ids."
+        },
+        "rotation": {
+          "type": "object",
+          "description": "API-key rotation advertisement (auth-profiles.md §`openwop-auth-api-key-rotation`).",
+          "properties": {
+            "supported": { "type": "boolean" },
+            "minGraceSeconds": {
+              "type": "integer",
+              "minimum": 0,
+              "description": "Minimum rotation grace window in seconds. auth-profiles.md SHOULDs production-profile hosts to ≥ 86400 (24h). The scenario tolerates any non-negative value but flags < 86400 in behavior mode."
+            }
+          },
+          "additionalProperties": false
+        },
+        "oauth2": {
+          "type": "object",
+          "description": "OAuth2 client-credentials advertisement (auth-profiles.md §`openwop-auth-oauth2-client-credentials`).",
+          "properties": {
+            "supported": { "type": "boolean" },
+            "issuer": {
+              "type": "string",
+              "format": "uri",
+              "description": "Token issuer URL the host trusts. Tokens MUST carry a matching `iss` claim."
+            },
+            "audience": {
+              "type": "string",
+              "description": "Audience the host requires. Tokens MUST carry a matching `aud` claim."
+            },
+            "supportedAlgorithms": {
+              "type": "array",
+              "items": { "type": "string", "minLength": 1 },
+              "uniqueItems": true,
+              "description": "JWS signing algorithms the host accepts (canonical: RS256, ES256). Hosts MUST reject tokens signed with algorithms outside this list."
+            }
+          },
+          "additionalProperties": false
+        },
+        "oidc": {
+          "type": "object",
+          "description": "OIDC user-bearer advertisement (auth-profiles.md §`openwop-auth-oidc-user-bearer`).",
+          "properties": {
+            "supported": { "type": "boolean" },
+            "issuers": {
+              "type": "array",
+              "items": { "type": "string", "format": "uri" },
+              "minItems": 1,
+              "uniqueItems": true,
+              "description": "Trusted OIDC issuer URLs. The host accepts tokens whose `iss` claim matches any entry."
+            },
+            "audience": {
+              "type": "string",
+              "description": "Audience identifier the host requires in OIDC tokens."
+            },
+            "supportedScopeMapping": {
+              "type": "string",
+              "enum": ["group-claim", "scope-claim", "host-acl"],
+              "description": "How the host derives openwop scopes from the OIDC token. `group-claim`: from `groups` claim via host config. `scope-claim`: from `scope` claim directly. `host-acl`: from a host-side mapping table (sub → scope)."
+            },
+            "introspectionIntervalSeconds": {
+              "type": "integer",
+              "minimum": 0,
+              "description": "Maximum interval at which the host SHOULD re-introspect a cached token to detect IdP revocation. auth-profiles.md recommends `min(exp - now, 300)`."
+            }
+          },
+          "additionalProperties": false
+        },
+        "mtls": {
+          "type": "object",
+          "description": "mTLS advertisement (auth-profiles.md §`openwop-auth-mtls`).",
+          "properties": {
+            "supported": { "type": "boolean" },
+            "required": {
+              "type": "boolean",
+              "description": "When `true`, the host rejects bearer-only requests. When `false`, mTLS is optional and complements bearer auth."
+            },
+            "subjectMapping": {
+              "type": "string",
+              "enum": ["cn", "san-dns", "san-uri"],
+              "description": "How the host derives the transport principal from the client certificate. `cn`: subject CN. `san-dns`: subjectAltName DNS entry. `san-uri`: subjectAltName URI entry."
+            }
+          },
+          "additionalProperties": false
+        }
+      },
+      "additionalProperties": true
+    }
   }

auth-profiles.md §"Discovery guidance" currently advertises this metadata under extensions.auth.. RFC 0010 promotes it to capabilities.auth. (matching the audit-log-integrity precedent). The extensions.auth. location remains valid for hosts that haven't migrated; clients SHOULD prefer capabilities.auth. when both are present. This is a non-breaking documentation update to auth-profiles.md.

§B Scenario: auth-api-key-rotation.test.ts

Asserts the rotation contract from auth-profiles.md §openwop-auth-api-key-rotation.

  • Gating: behaviorGate('openwop-auth-api-key-rotation', capabilities.auth.profiles.includes('openwop-auth-api-key-rotation') && capabilities.auth.rotation?.supported === true).
  • Setup: operator supplies a primary API key via OPENWOP_API_KEY (already required for the suite) and a secondary key via OPENWOP_TEST_SECONDARY_API_KEY. The two keys MUST map to the same principal + tenant (the rotation invariant).
  • Assertions:

1. Both keys authenticate POST /v1/runs with the same fixture (conformance-noop); both return 201; both run-snapshots show the same tenant and principal (when audit surface is present). 2. When capabilities.auth.rotation.minGraceSeconds is advertised, it is an integer ≥ 0; behavior-mode warns if < 86400 per auth-profiles.md SHOULD. 3. An invalid bearer (synthetic canary, not either real key) returns 401 invalid_token per the canonical envelope; the response body does NOT echo the canary in any field (redaction MUST hold even on rotation-profile hosts).

  • Soft-skip: when OPENWOP_TEST_SECONDARY_API_KEY is unset, the overlap assertion soft-skips; capability shape still asserts.

§C Scenario: auth-oauth2-client-credentials.test.ts

Asserts the OAuth2-CC contract from auth-profiles.md §openwop-auth-oauth2-client-credentials.

  • Gating: behaviorGate(&#39;openwop-auth-oauth2-client-credentials&#39;, capabilities.auth.profiles.includes(&#39;openwop-auth-oauth2-client-credentials&#39;) &amp;&amp; capabilities.auth.oauth2?.supported === true).
  • Positive case: operator supplies a valid token via OPENWOP_TEST_OAUTH_TOKEN. The scenario submits Authorization: Bearer &lt;token&gt; to POST /v1/runs; expects 201 + canonical RunSnapshot.
  • Negative cases (no operator setup required):

1. Malformed JWT (e.g., Bearer not.a.real.jwt) → 401 invalid_token. 2. Token signed with HS256 (or any algorithm NOT in supportedAlgorithms) → 401. 3. Token with wrong aud claim (synthetic via the OIDC issuer harness from §E) → 401 with details.reason SHOULD indicate audience mismatch. 4. Token with valid signature but expired exp → 401.

  • Capability shape: when oauth2.issuer is advertised, it MUST be a valid URI; audience MUST be a non-empty string; supportedAlgorithms MUST be a non-empty array.
  • Soft-skip: when OPENWOP_TEST_OAUTH_TOKEN is unset, positive case soft-skips; negative cases still run.

§D Scenario: auth-oidc-user-bearer.test.ts

Asserts the OIDC contract from auth-profiles.md §openwop-auth-oidc-user-bearer. Uses the synthetic OIDC issuer harness from §E so the suite can mint and present tokens with controlled claims.

  • Gating: behaviorGate('openwop-auth-oidc-user-bearer', capabilities.auth.profiles.includes('openwop-auth-oidc-user-bearer') && capabilities.auth.oidc?.supported === true).
  • Harness boot: scenario boots a SyntheticOIDCIssuer in-process (per §E), publishes JWKS, mints tokens. The host MUST be pre-configured to trust the harness's issuer URL — operator wires this via OPENWOP_TEST_OIDC_ISSUER_URL and the host's config.
  • Assertions:

1. Token with valid signature + matching iss/aud/exp/sub → host returns 201 on POST /v1/runs; sub claim threads into the run's audit/principal surface (when discoverable). 2. Token with valid signature + wrong iss → 401. 3. Token with valid signature + wrong aud → 401 with details.reason indicating audience mismatch. 4. Expired token (exp < now) → 401. 5. Token signed with an unknown JWKS key (key id not published by the harness) → 401. 6. Token-valid-but-scope-insufficient (e.g., minted with empty groups claim against a group-claim mapping host) → 403 forbidden.

  • Soft-skip: when OPENWOP_TEST_OIDC_ISSUER_URL is unset, the scenario asserts only capability shape.

§E Synthetic OIDC issuer harness — conformance/src/lib/oidc-issuer.ts

A minimal in-process OIDC issuer the conformance suite mints tokens against. Server-free; no external dependencies beyond node:crypto.

Interface:

export interface SyntheticOIDCIssuerOptions {
  issuer: string;                      // base URL (caller supplies; not bound)
  audience: string;
  algorithm: &#39;RS256&#39; | &#39;ES256&#39;;        // default RS256
  keyId?: string;                      // default &#39;openwop-conformance-key-1&#39;
}

export interface MintedToken {
  token: string;                       // signed JWT
  claims: Record&lt;string, unknown&gt;;
}

export interface SyntheticOIDCIssuer {
  readonly issuer: string;
  readonly audience: string;
  readonly algorithm: 'RS256' | 'ES256';
  readonly jwksJson: string;           // serve at $issuer/.well-known/jwks.json
  readonly discoveryJson: string;      // serve at $issuer/.well-known/openid-configuration

  mint(claims: Record&lt;string, unknown&gt;, opts?: { expiresInSeconds?: number; keyId?: string }): MintedToken;
  rotateKey(): void;                   // generate a new signing key; jwksJson updates
}

export function createSyntheticOIDCIssuer(opts: SyntheticOIDCIssuerOptions): SyntheticOIDCIssuer;

The harness:

  • Generates an RSA or EC keypair on construction via node:crypto.generateKeyPairSync.
  • Serializes the public key as JWKS (kty, kid, alg, n+e for RSA, crv+x+y for EC).
  • Mints tokens by signing the JWT header + payload with node:crypto.sign.
  • The discovery + JWKS JSON strings are caller-served — the harness does NOT bind a port itself. Scenarios stand up a minimal HTTP server (via node:http) to publish them, or the host can be pre-configured with the harness's discovery document fed via env.

This is server-free: the harness runs inside the vitest worker process. Tokens are minted synchronously.

§F Scenario: auth-mtls.test.ts (opt-in)

mTLS is environmental (cert provisioning, server config); the conformance scenario is opt-in following the restart-during-run.test.ts precedent.

  • Gating: RUN_THIS_SCENARIO = process.env.OPENWOP_TEST_MTLS === '1'. When unset, scenario skips entirely (vitest describe.skipIf).
  • Operator setup:

- OPENWOP_TEST_MTLS_CLIENT_CERT_PATH — path to PEM-encoded client cert. - OPENWOP_TEST_MTLS_CLIENT_KEY_PATH — path to PEM-encoded client key. - OPENWOP_TEST_MTLS_CA_PATH — optional path to CA bundle for server-cert validation.

  • Assertions:

1. Request with valid client cert + valid bearer → 201 on POST /v1/runs. 2. Request with invalid client cert → either 401 invalid_token envelope OR a transport-layer TLS failure (both are conformant per auth-profiles.md §openwop-auth-mtls; the scenario MAY assert either). 3. When capabilities.auth.mtls.required === true, a bearer-only request (no client cert) MUST fail; the scenario submits a no-cert request and asserts non-2xx.

  • Capability shape: the shape check runs unconditionally (cheap discovery probe); the behavior portion runs only under OPENWOP_TEST_MTLS=1.

§G SECURITY/threat-model-auth-profiles.md updates

Add the rotation/OAuth/OIDC scenario-specific failure modes to the existing threat model:

  • A1 (Stolen old API key holder): scenario coverage = rotation revocation assertion in §B (revoked-old-key returns 401, not a partial pass).
  • A2 (Token substitution attacker): scenario coverage = wrong-aud + wrong-iss negative cases in §C + §D.
  • A3 (new): OIDC IdP impersonation via key spoofing. Adversary presents a token signed with a key whose kid is not in the published JWKS. Scenario coverage = §D #5.
  • A4 (new): mTLS downgrade via missing-cert. Adversary connects without a client cert to a host claiming mtls.required: true. Scenario coverage = §F #3.

The existing threat-model document already enumerates A1 + A2; this RFC adds the scenario-binding line for each and introduces A3 + A4.

§H INTEROP-MATRIX guidance

INTEROP-MATRIX.md "Compatibility profile claim" column gains entries for each auth profile a host advertises. A host claiming openwop-auth-oidc-user-bearer lists it alongside openwop-core (or wherever profile order makes sense). The conformance link column records which auth-profile scenarios passed under OPENWOP_REQUIRE_BEHAVIOR=true.

Hosts that don't claim any production-auth profile remain v1-conformant via baseline auth.md — no advertisement needed.

Compatibility

Additive per COMPATIBILITY.md §2.1:

  • New capabilities.auth block is optional; absence preserves v1.0 behavior.
  • Sub-blocks each have their own supported: boolean; absence on a specific sub-block means the host doesn't claim that profile.
  • additionalProperties: true on the auth block preserves the existing informal auditLogIntegrity advertisement (used by audit-log-integrity.test.ts).
  • New scenarios are capability-gated; v1.0 hosts that don't advertise any auth profile skip cleanly.
  • No new error codes; the canonical 401/403 envelopes are already-normated v1 wire shapes (auth.md §3).
  • mTLS scenario is opt-in via env var, matching the restart-during-run.test.ts precedent.

Conformance

Existing scenarios that cover this surface (de facto baseline): auth.test.ts (missing/invalid credential).

New scenarios required by this RFC:

1. conformance/src/scenarios/auth-api-key-rotation.test.ts — §B. 2. conformance/src/scenarios/auth-oauth2-client-credentials.test.ts — §C. 3. conformance/src/scenarios/auth-oidc-user-bearer.test.ts — §D. 4. conformance/src/scenarios/auth-mtls.test.ts — §F, opt-in. 5. conformance/src/lib/oidc-issuer.ts — §E (library, not a scenario).

All four scenarios use behaviorGate(profileName, advertised); OPENWOP_REQUIRE_BEHAVIOR=true converts capability-shape-only skips into hard failures, matching the audit-log-integrity precedent.

Alternatives considered

1. Four separate RFCs (0010 rotation, 0011 OAuth2, 0012 OIDC, 0013 mTLS). Rejected: the profiles share infrastructure (capability namespace, envelope assertion pattern, threat-model categories, INTEROP-MATRIX column) and one consolidated RFC keeps the comment window shared. Splitting multiplies maintainer overhead without adding review fidelity. If a specific profile turns out to need a wire-shape decision, an additive sub-RFC remains possible.

2. Skip the synthetic OIDC issuer harness; require operators to wire a real IdP. Rejected: the negative-case tokens (wrong aud, wrong iss, expired, unknown key) cannot be minted against a real IdP without IdP-admin tooling that varies per vendor. The synthetic harness keeps CI hermetic; real-IdP interop remains a separate (post-Active) operator step.

3. **Bake the auth advertisement into extensions.auth.* as auth-profiles.md currently shows.** Rejected: extensions is the catch-all for unschema'd metadata, but rotation/OAuth/OIDC/mTLS metadata is now stable enough to formalize. Promoting to capabilities.auth. matches the audit-log-integrity precedent and gives the conformance scenarios a typed schema to gate against. The extensions.auth. path remains valid (additionalProperties: true on the top-level capabilities object); clients SHOULD prefer capabilities.auth.* when both are present.

4. Do nothing. Rejected: auth-profiles.md has shipped four profiles for over six weeks (FINAL v1 since 2026-05-10) with zero conformance coverage and zero advertising hosts. Each day this gap stays open, the protocol's production-auth story is "trust the spec text" rather than "trust the suite."

Unresolved questions

1. Rotation overlap assertion granularity. §B asserts both keys authenticate. Should the suite also assert audit-log-event distinguishability (key.created / key.used / key.revoked events per auth-profiles.md)? That requires audit-log surface a host MAY or MAY NOT expose; it could land as a §B addendum if the audit-log-integrity profile is co-advertised. Out of scope by default.

2. OIDC harness key-rotation testing. §E exposes rotateKey() but §D doesn't assert host-side behavior on rotation. Adding a "key rotated mid-flight; old tokens continue working through grace; new tokens use new key" scenario is valuable but doubles the harness setup. Defer to a sub-RFC.

3. mTLS subject-mapping ambiguity. §A advertises subjectMapping: cn|san-dns|san-uri. Real-world deployments often combine these (e.g., fall back from SAN to CN). Should the schema support an array of fallback rules, or stay single-valued? Out of scope; the schema is single-valued for now and hosts with complex mapping use extensions.auth.* for documentation.

4. OAuth2-CC vs OIDC overlap. A host accepting OIDC user-bearer tokens with the client_credentials grant flow is technically conformant to both profiles. Should auth.profiles[] allow simultaneous advertisement, and should §C + §D handle overlap explicitly? Current draft permits both advertisements; scenarios run independently. Worth a sentence in auth-profiles.md clarification but not blocking.

5. Tighten capabilities.auth.additionalProperties to false. The schema landed in §A intentionally uses additionalProperties: true on the auth block so the existing informal auth.auditLogIntegrity advertisement (from the audit-log-integrity profile, predating RFC 0010) continues to validate alongside the four formal sub-blocks defined here. A follow-up additive RFC SHOULD formalize auditLogIntegrity as a fifth sub-block schema (with additionalProperties: false) and tighten the parent auth block to additionalProperties: false at the same time. Until that lands, hosts MAY add arbitrary keys under capabilities.auth.* — that's a documentation surface for unenumerated future-profile metadata, not an extension point clients should depend on.

Implementation notes (non-normative)

  • Suite version bump: @openwop/openwop-conformance 1.X.0 (next available minor after the RFC 0009 wave). Per ROADMAP.md line 24, conformance minors don't modify the wire contract.
  • No SDK changes required — none of the wire shapes are SDK-side; the schema diff is server-advertised.
  • The OIDC issuer harness (§E) uses node:crypto and stdlib JWT signing; no new npm dependencies. The existing audit-log-integrity scenario uses Ed25519 from node:crypto so the precedent is established.
  • Reference-host implementation: SQLite or Postgres can adopt one rotation profile end-to-end as the worked example (per docs/PROTOCOL-GAP-CLOSURE-PLAN.md Track 3 §"Conformance gaps to close"). Most realistic: SQLite advertises openwop-auth-api-key-rotation since it has a smaller surface than OIDC.

Acceptance criteria

  • [ ] RFCS/0010-auth-profile-conformance.md merged at Status: Active after the 7-day comment window (or waived per bootstrap-phase rule).
  • [ ] schemas/capabilities.schema.json includes the auth block per §A.
  • [ ] conformance/src/scenarios/auth-api-key-rotation.test.ts lands capability-gated.
  • [ ] conformance/src/scenarios/auth-oauth2-client-credentials.test.ts lands capability-gated.
  • [ ] conformance/src/scenarios/auth-oidc-user-bearer.test.ts lands capability-gated.
  • [ ] conformance/src/scenarios/auth-mtls.test.ts lands opt-in via OPENWOP_TEST_MTLS=1.
  • [ ] conformance/src/lib/oidc-issuer.ts lands as the synthetic harness per §E.
  • [ ] conformance/coverage.md §"Capability-gated scenarios" lists the four scenarios under per-profile gating.
  • [ ] auth-profiles.md updated to cross-reference the new capabilities.auth. discovery path (additive note; extensions.auth. remains valid).
  • [ ] SECURITY/threat-model-auth-profiles.md adds A3 + A4 plus scenario-binding lines for A1 + A2.
  • [ ] One reference host (SQLite or Postgres) implements at least openwop-auth-api-key-rotation and passes the scenario under OPENWOP_REQUIRE_BEHAVIOR=true.
  • [ ] INTEROP-MATRIX.md reflects the host's profile claim.
  • [ ] CHANGELOG.md entries for the Draft drop and the post-Active landing.

References

  • spec/v1/auth-profiles.md — the prose this RFC mechanizes.
  • spec/v1/auth.md §3 — canonical 401/403 error envelope.
  • RFCS/0009-production-profile-conformance.md — same-pattern precedent (profile advertisement + capability shape + scenarios + behavior-mode gate).
  • conformance/src/scenarios/audit-log-integrity.test.ts — the existing capabilities.auth.profiles[] consumer; informs the schema shape.
  • SECURITY/threat-model-auth-profiles.md — companion threat-model surface.
  • SECURITY/external-audit-engagement.md — downstream consumer of this RFC's evidence.
  • docs/PROTOCOL-GAP-CLOSURE-PLAN.md Track 3 — the planning context.