Skip to content

RFC: Runtime Project Decision Path (R1.1)

Status: Living implementation plan — GAT-228 baseline implemented in PR #221; GAT-221 bootstrap architecture implemented as stacked follow-up — updated 2026-07-10 Scope: GAT-247, GAT-241, GAT-227, GAT-228, GAT-242, GAT-243, GAT-221, GAT-225, GAT-244, GAT-246, GAT-261 (+ GAT-248/249/250 validation) Companion: docs/rfcs/rfc-governance-studio-adapter-integration.md (GAT-208 contract), llm-gateway/docs/verification.md Source repos surveyed: this repo + platform/integrity-monorepo, platform/governance-backend, platform/governance-studio

0. Reconciliation with implementation reality

Section titled “0. Reconciliation with implementation reality”

What the issues assume vs. what the repo already has:

ConceptReality in repo
ProjectMembershipV1 contractFully specified (adapter RFC §9) and implemented in shared/servicekit/projectmembership (ValidateCredential, ValidationStatus enum, error codes, EvidenceIndexItem, postgres store over project_membership_evidence, migration 0030). Consumed by control-plane ingestion, the GAT-221 binder input, and builtin.project_membership policy evaluation.
Project referencesshared/servicekit/projectrefs + gateway_project_references (migration 0027). Used by control-plane administration and by the GAT-221 binder to reject candidates for unknown projects before policy selection.
Project-scoped snapshotsRuntime binds a non-authoritative project candidate, resolves the exact project activation with fallback disabled, compiles through a bounded concurrency-safe cache, and pins one identity across bootstrap/pre/post/audit/request-end. Project snapshot creation and activation require builtin.project_membership.
External registry evidenceextregistry.HTTPRegistryAdapter is built and configured but explicitly parked: _ = extRegistryAdapter // to be wired into the Visa TAP verifier path in the next sprint (cmd/guardian-gateway/main.go). Decision codes `ext_registry_trusted
Observe/enforce/disabledproject_context.mode remains the rollout/kill switch. Both enabled values enforce core protocol/binding failures. Membership policy observe/enforce and required-membership now live in builtin.project_membership config.
ATOThe literal term appears nowhere in any of the four repos. It resolves to the policy compliance certificate issued by the integrity service: VC type ["VerifiableCredential", "ComponentAttestation"] with credentialSubject.component.type = "ProjectComplianceV1", subject id = urn:uuid:{project_uuid}, component.policy = urn:cid:{policyCid}, component.authorizedBy = <approver session DID>, evidence[].type = ["ProjectComplianceEvidenceV1"], revocable via BitstringStatusList. Issued at POST {integrity}/gov/v1/policy/compliance/{project_uuid} (integrity-service/src/api/governance/v1/compliance.rs), persisted in governance-backend as ProjectPolicyCredential rows. This is a project-level credential, distinct from agent-level ProjectMembershipV1 — see §1.5.
Agent↔project record of truthgovernance-backend dropped its agent/project-agent tables (migration 106_remove_legacy_tables); auth-service RBAC covers human users only. The gateway’s project_membership_evidence index is therefore the only operational record of agent↔project association anywhere in the platform — it is a validated view over integrity-issued VCs, not a cache of an upstream table.
Shared signing primitivesRFC 9421 verify code is trapped in llm-gateway/service/internal/{sig9421,verify} (unimportable by control-plane). Viper’s signing core (viper/src/proxy/signing.rs) is clean but its header constants and sign-and-attach glue are duplicated across 3 call sites. servicekit has no HTTP-signing package.
Audit redactionGAT-228 redacts inbound X-Viper-* values from raw audit header snapshots and strips Viper/signature headers before provider forwarding. Structured audit fields contain resolved identifiers/status only.

1. The end-to-end decision path (concrete names)

Section titled “1. The end-to-end decision path (concrete names)”
Viper proxy LLM Gateway (single enforcement locus) Postgres
─────────── ──────────────────────────────────── ────────
prepare_signed_proxy_request handleVerifyProxy (gatewayhttp/server.go)
X-Viper-State-CID ───► 1. route classify (routing.go)
X-Viper-Nonce / -Timestamp 2. request.start hook
Content-Digest 3. verifier.Verify ── ResolveAgentByKey ─► registry_agent_keys
X-Viper-Credential-CID* 4. extregistry.Lookup (wire-in, observe) registry_agents
X-Viper-Project-URN* 5. projectctx.Binder.Bind ───────────────────────► project_membership_evidence
Signature-Input / Signature 6. exact project snapshot select + pin ─────────► plugin_snapshot_activations
(all X-Viper-* covered) 7. typed project.bootstrap membership decision
8. authoritative ProjectContext on success only
9. pinned proxy.pre / proxy.post / request.end
10. forward (providerProxy / relay) + audit ──────► gateway_audit_exchanges
* new headers (GAT-241/227)

Normative spec: docs/rfcs/rfc-project-membership-presentation-contract.md (GAT-241). The summary below is informative.

Two Viper→Gateway project-context headers, never embedding credential bytes in headers:

  • X-Viper-Credential-CID — canonical sorted list of JCS VC blob CIDs. Viper automatically includes its registered agent VC CID and merges configured membership CIDs. The identity hook validates the agent VC; the binder may derive a non-authoritative project candidate from ProjectMembershipV1 fields, and only the selected snapshot’s membership policy can authorize it.
  • X-Viper-Project-URN — optional runtime project selector (urn:uuid:...). Required only when the credential list or stored evidence has more than one project candidate; it is the deterministic tie-breaker for multi-project agents, not project authority.

X-Viper-Verifiable-Credential-CID is deprecated. Viper emits only the unified singular header; the Gateway accepts the old signed single-CID form only when the unified header is absent. GAT-241 defines the ProjectMembershipV1 selection semantics needed for deterministic project context and crisp reason codes.

Both headers MUST be added to covered_components (viper/src/proxy/server.rs) so they are inside the RFC 9421 signature base — the gateway trusts them only because they are signed, exactly like X-Viper-Upstream-URL on the relay path.

Viper-side changes (GAT-227, implemented), all in existing extension points:

  • header constants next to STATE_CID_HEADER (server.rs:42–51)
  • app-scoped membership credential files or durable JSON_JCS CIDs loaded from project_presentations.<app> in local config (issuance stays with Governance Studio — Viper only presents)
  • credential files JCS-canonicalized and stored with blobs::put(..., JSON_JCS), merged with the registered agent VC CID and sorted/deduped into X-Viper-Credential-CID, synced in sync_attestation_blobs, attached + covered in prepare_signed_proxy_request

verify.Verifier.Verify (server.go:1499) keeps its current pipeline: header presence → 9421 parse → profile → freshness → signature → content-digest → attestation blob → registry.ResolveAgentByKey(did) → replay reserve (keyid, nonce, created). Reason codes stay as in verify/errors.go. Existing LLM mediation behavior is preserved — everything below is additive and gated.

1.3 External registry evidence (wire-in, observe-only)

Section titled “1.3 External registry evidence (wire-in, observe-only)”

Wire the parked extregistry.HTTPRegistryAdapter.Lookup immediately after verifier.Verify, record Evidence.Status (trusted|untrusted|unknown) and decision code (ext_registry_trusted|untrusted|unknown|error) into hook context + audit. FailClosed config exists; for R1.1 default observe (never deny on registry outcome). This satisfies “external registry evidence resolves” without adding a catalog product — Gateway remains an L7 relay.

1.4 Current candidate/bootstrap boundary (GAT-221)

Section titled “1.4 Current candidate/bootstrap boundary (GAT-221)”

The production request path uses projectctx.Binder, not hardwired membership authorization. Binder retains non-configurable signature coverage, canonical credential-list grammar/bounds, current/legacy mutual exclusion, CID/body binding, signer/credential-subject binding, deterministic project selection, ambiguity/conflict rejection, project-reference existence, and local-only storage access. Its ProjectCandidate is safe for policy selection but is explicitly non-authoritative.

Gateway selects and pins only the candidate project’s active snapshot; a presented/implicit candidate never falls back to the environment default. It runs the snapshot’s mandatory typed builtin.project_membership entry at project.bootstrap. Snapshot config supplies issuers, required/accepted status, expiration, revocation, roles, and policy observe/enforce. Gateway validates the typed result against candidate project + signer and only then constructs protected projectctx.Context. Generic plugin context_writes cannot establish it. Failed observe policy establishes no context and suppresses ordinary project hooks.

Projectless requests (no selector and no membership candidate) explicitly pin the environment-default snapshot and skip bootstrap. No external membership or status service is called on the request path.

1.4.1 Historical GAT-228 resolver baseline (superseded for production requests)

Section titled “1.4.1 Historical GAT-228 resolver baseline (superseded for production requests)”

The following shape documents the shipped GAT-228 baseline and its reason-code/evidence behavior reused by Binder/bootstrap tests. Its issuer/status/validity authorization placement is superseded by the boundary above.

type Context struct {
ProjectURN string // identified project; may be set on attributable failures
Established bool // true only for one currently valid membership/project
Source string // "header_evidence" | "stored_evidence" | "none"
ValidationStatus projectmembership.ValidationStatus
CredentialID string
IssuerDID string
Roles []string
ReasonCode string // "" when clean
}
type Resolver interface {
Resolve(ctx context.Context, in Input) Context
}
// Input: signerDID (keyid), raw credential-CID/project-URN headers, and signature-coverage facts.
// Storage and validation failures surface as stable reason codes so observe mode can continue.

Deterministic resolution order:

  1. Header credential list (X-Viper-Credential-CID present): parse the canonical bounded CID list (project_credentials_malformed on bad syntax). For each CID, first try project_membership_evidence.GetBySourceCID(subjectDID, sourceCID) using source_refs.source_cid. Otherwise fetch from gateway_blobs, recompute the JCS JSON CID, and compare it to the signed list CID before trusting its candidate fields. Core binds subject/project/selector and checks the project reference, but does not apply issuer, status, validity, revocation, or role policy. It carries raw CID-bound and/or stored evidence into the selected snapshot’s typed bootstrap, where builtin.project_membership performs those configurable checks.
  2. Stored evidence (no usable membership VC in the credential list): query project_membership_evidence by subject_did, optionally constrained by signed X-Viper-Project-URN, without filtering out non-valid rows up front.
    • 0 candidate rows → Source: none (+ project_membership_missing only if enforcement requires membership)
    • 1 distinct project URN → choose the deterministic same-project credential (newest last_verified_at, then latest valid_until, then latest observed_at, then credential_id) and map its stored/current status
    • 1 distinct project URNs and no signed URN hint → project_context_ambiguous, no project scope (env-default snapshot applies)

    • candidate rows that are expired, not-yet-valid, revoked, invalid, or untrusted map to their canonical reason codes instead of collapsing into project_membership_missing
  3. Never call out to Auth/Governance services in the request path — enforcement stays local to Gateway; stored evidence is refreshed asynchronously by control-plane, which remains the membership authority.

1.5 Project trust / “ATO” validation v0 (GAT-242)

Section titled “1.5 Project trust / “ATO” validation v0 (GAT-242)”

Two distinct credentials participate in the decision, issued by the same platform signer (integrity service, platform DID via auth-service; Ed25519 → DataIntegrityProof with eddsa-rdfc-2022 cryptosuite, secp256k1 → EcdsaSecp256k1Signature2019):

Agent membership (§1.4)Project trust / “ATO”
CredentialProjectMembershipV1 (identity attestation)ProjectComplianceV1 (ComponentAttestation)
Subjectagent did:key:...urn:uuid:{project_uuid}
Assertsagent is a member of project, with rolesproject is approved against a policy (component.policy = urn:cid:..., authorizedBy = approver DID)
IssuancePOST {integrity}/gov/v1/identity/membership/{uuid}POST {integrity}/gov/v1/policy/compliance/{uuid}
System of record for evidencegateway project_membership_evidence (submitted by Studio via control-plane)governance-backend ProjectPolicyCredential rows — no gateway-side store exists today
RevocationBitstringStatusListBitstringStatusList (Studio revoke → PUT {gov}/api/v1/projects/{id}/policies/{ppId}/credentials/{cid}/revoke → integrity POST /credentials/v1/revoke/{credentialId})

Membership validation v0 (in builtin.project_membership): core supplies the bound signer/project; projectmembership.ValidateCredential verifies signature/proof and applies the selected snapshot’s trusted issuers, status, validity, and roles. Stored terminal status still overrides a freshly valid blob. secp256k1 is covered by gov-336; verify eddsa-rdfc-2022 DataIntegrityProof acceptance against a real integrity-issued VC — the adapter RFC’s Ed25519Signature2018 example does not match what the integrity service actually emits.

Project trust v0: the gateway needs local evidence that the project holds a valid, unrevoked ProjectComplianceV1 credential. Governance-backend has zero gateway integration (confirmed: no client, no adapter), and Studio already drives the analogous membership submission — so the same pattern extends:

  • New control-plane endpoint POST /v1/projects/{uuid}/trust accepting {credential_id, source_cid, refresh_status} refs (mirror of the membership submit), validated with a ProjectComplianceV1-aware sibling of projectmembership.ValidateCredential, indexed into a new project_trust_evidence table (or trust columns on gateway_project_references). Studio wires an “Attach compliance credential to Gateway” step next to its existing issuance flow.
  • A future project-trust bootstrap policy reads locally indexed trust evidence for the bound project candidate; it must not add runtime callouts or move trust policy back into candidate binding.

Revocation: GET {integrity}/credentials/v1/status?vcId= is unauthenticated and returns {"revoked": bool} (status lists are public, ETag’d, max-age=300), so live checking is cheaper than the adapter RFC implies — but it stays out of the request hot path. v0: control-plane refreshes credential_status on stored evidence (both types) on submit (refresh_status: true) and on a background cadence; a stored revoked maps to membership_revoked / project_trust_revoked and denies only in enforce mode. Live in-request status resolution stays behind a deferred flag.

Aug 1 posture, stated explicitly: membership validation is enforceable (offline checks + stored status). Project trust requires the new control-plane submission path + Studio step; if that slips, project_trust_* codes run observe-only — the future trust policy emits project_trust_missing for every project, audit shows it, nothing denies on trust. The mode split makes this a config statement, not a code change.

Project-scoped snapshots are the membership and ordinary-policy vehicle:

  • PinProjectSnapshot resolves with EnvironmentFallbackEnabled:false; missing project policy fails closed.
  • Project snapshots require exactly one enabled premade builtin.project_membership entry at project.bootstrap, in enforcement host mode and with no host capabilities.
  • A bounded LRU cache retains compiled immutable identities by (snapshot_id, version). Concurrent first loads compile once; in-flight pins may temporarily exceed the bound and are evicted after release.
  • Activation changes affect later resolutions but never an existing pin, preventing bootstrap/pre/post/request-end TOCTOU.
  • request.end persists the pinned ID/version and reloads that immutable snapshot when it is no longer current.
  • Projectless requests pin the environment-default active pointer. They do not run project.bootstrap.

Hook/OPA input context gains project keys alongside the existing relay keys (relay.destination, relay.host):

  • project.urn, project.membership_status, project.roles, project.resolution_source, ext_registry.status Same keys on LLM and relay paths (handleLLMRoute, handleRelayRoute) so a single Rego policy can gate both. Per-plugin PluginMode ∈ {enforcement, observability} and enabled remain the policy-level switches.

Gateway-level mode is a rollout/kill switch; membership policy mode belongs to the bootstrap plugin:

LLM_GATEWAY_PROJECT_CONTEXT_MODE = disabled | observe | enforce (default: disabled)
  • disabled — binder/bootstrap not invoked; legacy environment-only behavior.
  • observe or enforce — enable Binder and both fail closed on core protocol/binding, missing snapshot, invalid snapshot, and bootstrap infrastructure failures.
  • builtin.project_membership.mode=observe — records would-deny, establishes no context, runs no ordinary project plugins, and does not block transport.
  • builtin.project_membership.mode=enforce — policy denial returns local 403.

New reason codes (same registry style as verify/errors.go + relay.go):

CodeMeaning
project_presentation_unsignedCredential/project header was not signature-covered
project_credentials_malformedCredential CID list was malformed or over limit
project_membership_missingNo header or stored evidence for signer DID
project_membership_unresolvableReferenced membership blob/evidence could not resolve
project_membership_cid_mismatchBlob body recomputed to a different JCS CID
project_membership_evidence_conflictSource-CID/evidence lookup returned conflicting rows
project_membership_invalidCredential failed structural/proof validation
project_membership_expired / _not_yet_validValidity window
project_membership_untrusted_issuerIssuer not in allowlist
project_membership_subject_mismatchcredentialSubject.id ≠ request signer DID
project_membership_project_mismatchCredential project ≠ presented URN
membership_revokedStored credential_status revoked (live check deferred)
project_context_ambiguousMulti-project agent, no signed URN hint
project_unknownURN not in gateway_project_references
project_resolver_errorLocal storage or resolver failure
project_trust_missingNo ProjectComplianceV1 evidence stored for the project
project_trust_invalid / _expiredTrust credential failed validation / validity window
project_trust_revokedStored trust credential_status revoked
project_snapshot_missingCandidate project has no active snapshot
project_snapshot_errorScoped snapshot resolution/compilation failed
project_membership_bootstrap_errorMandatory typed bootstrap missing/invalid/failed
project_membership_role_deniedAllowed/required role policy failed

Migrations 0031_audit_project_context and 0035_audit_project_snapshot_bootstrap add matching fields on audit.ExchangeRecord, populated in recordAuditExchange and inserted by postgres_sink.go:

project_candidate_urn, project_urn, membership_established, project_resolution_source, membership_validation_status, membership_credential_id, membership_issuer_did, project_context_mode, project_reason_code, plugin_snapshot_id, plugin_snapshot_version, plugin_snapshot_scope, plugin_snapshot_fallback_used, bootstrap_decision_code, and bootstrap_outcome.

Candidate URN is recorded only when core can safely attribute it; authoritative project_urn is recorded only after typed establishment. External-registry fields belong to that adapter’s wire-in. GAT-225 owns the production-safe project audit query index.

Redaction rule: audit stores resolved identifiers and statuses only — project URN, credential ID, issuer DID, status, reason code — never raw credential JSON and never discarded unsigned project selectors. Stable project CIDs/URNs are internal metadata, so GAT-244 must redact or omit all inbound X-Viper-* header values from raw header snapshots before persistence. Hook context follows the same rule: project fields are populated only from the resolved, signature-covered context; unsigned or malformed credential/project headers produce reason codes with empty project fields.

1.10 Shared signing/header primitives (GAT-246)

Section titled “1.10 Shared signing/header primitives (GAT-246)”
  • Go: new shared/servicekit/httpsig lifted from llm-gateway/service/internal/sig9421 + verify/content_digest.go; promote header constants (X-Viper-*, X-Guardian-*) into shared/servicekit/httpx and delete the ≥6 duplicated per-file definitions (gateway server.go/relay.go/verifier.go, plugins builtins).
  • Rust: viper/src/proxy/signing.rs is already the reusable core; add a sign_and_attach(request, signer, keyid, covered) helper and a shared header-constants module to collapse the 3 duplicated call sites (prepare_signed_proxy_request, blob upload, VC renew) — the membership header lands in one place.
  • Not a blocker for Slice 1 (gateway-internal imports suffice); do it before GAT-227 so the new headers are defined once.

1.11 Platform issuance chain (cross-repo facts the contract depends on)

Section titled “1.11 Platform issuance chain (cross-repo facts the contract depends on)”

Membership already flows end-to-end. Governance Studio (AssociatedAgentsDashboard.tsx) runs a 3-stage flow: (1) ensure gateway project binding — PUT {gateway-cp}/v1/projects/{uuid} with {source: "governance_studio", governance_project_id, organization_id, display_name}; (2) issue at integrity — POST /gov/v1/identity/membership/{uuid} with {subjectDid, role[], expiryDate}{credential, credentialId, sourceCid}; (3) index into gateway — POST {gateway-cp}/v1/projects/{uuid}/memberships with {credential_id, source_cid, agent_did_hint, refresh_status: true} (durable refs, not the raw VC). Slice 1’s stored-evidence path therefore has a working producer today; the demo needs no new issuance tooling.

Identity/URN mapping. Governance-backend projects have only integer id + uuidno URN field exists upstream. project_urn = urn:uuid:{uuid} is derived (Studio client-side; gateway stores it in gateway_project_references). governance_project_id is the integer id, used only for auth-service RBAC paths (GET /api/v1/rbac/check/permission/{permission}/project/{projectId} — param is the integer id as string; response {hasPermission, permission, context, userId, projectId}). Relevant permission strings: view_project_data (all project roles), edit_project_settings (org/project owner only), create_credentials, revoke_credentials.

Auth boundaries at integrity. GET /store/v1/blob/{cid} and GET /statements/v1/id/{id} require a Bearer JWT; GET /credentials/v1/status?vcId= and GET /credentials/v1/status-lists/{id} are public. Consequence: any fetch of source_cid blobs or statements from integrity must stay in control-plane (which holds Studio-delegated tokens) — the gateway request path never needs an integrity token, only its own blob store and DB. This confirms the “enforcement local to Gateway” boundary is workable.

Proof suites in reality. Platform-signed VCs use DataIntegrityProof + eddsa-rdfc-2022 (Ed25519) or EcdsaSecp256k1Signature2019 (secp256k1) — not the Ed25519Signature2018 shown in the adapter RFC example. CID scheme matches Viper’s CIDv1 BLAKE3 implementation. Membership credentials use the JSON_JCS multicodec at both Integrity issuance and Viper presentation, while state/identity attestations continue to use RAW_BINARY.

Membership authority location. Human project membership = auth-service RBAC rows. Agent project membership = integrity-issued VCs only (governance-backend’s agent tables were dropped in migration 106). The gateway evidence index is the single operational agent↔project record — treat its correctness as production-critical, not as a convenience cache.

The diagram below preserves the delivery history. The current runtime is GAT-221 on top of the GAT-228 binder/evidence baseline: GAT-227 presents the unchanged wire contract, GAT-221 owns candidate selection, exact project snapshot pinning, typed bootstrap authorization, and the audit extension, and GAT-243/GAT-244 consume that established context and decision metadata.

GAT-241 (presentation contract: credential list, URN tie-break, reason-code names)
├─► GAT-228 (gateway projectctx resolver) ── includes GAT-244 mode/reason/audit skeleton
│ ├─► GAT-221 (scope-keyed snapshot selection in hooks.System)
│ │ └─► GAT-243 (project keys in policy context; relay parity)
│ └─► GAT-242 (trust validation v0 = ProjectComplianceV1 evidence:
│ new control-plane submit endpoint + project_trust_evidence store
│ + Studio "attach to gateway" step; observe-only if the Studio
│ step slips; live revocation deferred)
└─► GAT-227 (viper attaches credential CID list + optional URN) [needs GAT-246 header consts ideally]
GAT-261 batches the bounded credential-list storage fan-out before high-volume GAT-227 traffic.
GAT-225 owns project-filtered audit APIs and the production-safe project audit index.
GAT-246 (shared httpsig/header primitives) — parallel; soft prerequisite of GAT-227, refactor-only for gateway
ext-registry wire-in — independent, tiny (adapter already built), observe-only
GAT-247 (acceptance matrix/demo gates) — consumes all; define observe-mode gates now, enforce gates behind mode flag
GAT-248/249/250 (fixtures/E2E) — one validation step per slice, seeded from slice-1 fixtures

Historical landing order: GAT-241 → GAT-228(+244 skeleton) → GAT-227 → GAT-221 → GAT-242 v0 → GAT-243, with GAT-246 and the extregistry wire-in in parallel. GAT-228 worked before GAT-227 because stored-evidence resolution (Path B) needed no Viper change.

3. Historical delivered safe slice — GAT-228 resolver baseline

Section titled “3. Historical delivered safe slice — GAT-228 resolver baseline”

This section records what GAT-228 delivered before GAT-221 moved configurable authorization into the selected snapshot. It is not the current production request sequence; §1.4 and §1.6 are normative now.

  1. Contract doc: rfc-project-membership-presentation-contract.md is the normative GAT-241 artifact; llm-gateway/docs/verification.md summarizes implemented gateway behavior.
  2. internal/projectctx package: Resolver as above; header path + stored-evidence path; uses servicekit/projectmembership and narrow store interfaces over project_membership_evidence + gateway_project_references. The evidence store provides indexed GetBySourceCID(subjectDID, sourceCID) and a count-free ListPage for runtime pagination; conflicting duplicate source-CID rows map to project_membership_evidence_conflict.
  3. Config: ProjectContextMode on config.Runtime (TOML/env/flag, mirroring PluginsEnabled plumbing) + TrustedMembershipIssuerDIDs.
  4. Wire into handleVerifyProxy after identity enrichment (server.go:~1531): resolve, stash into hookState context keys, populate requestOutcome/audit; in enforce, writeLocalDeny on the deny-set codes; in observe, log + record only.
  5. Migration 0031 + ExchangeRecord fields + sink columns, without a blocking audit-table index build; GAT-225 owns that query index.
  6. Gateway parses the new headers if present (forward-compatible with GAT-227), but slice-1 demo path is stored evidence submitted via existing control-plane POST /v1/projects/{uuid}/memberships.

Snapshot scope selection was explicitly out of the GAT-228 slice and is now implemented by GAT-221. Viper presentation is implemented by GAT-227. Live revocation and future project-trust work remain separate.

4. Historical tests / fixtures for the GAT-228 slice

Section titled “4. Historical tests / fixtures for the GAT-228 slice”
  • Unit — projectctx (table-driven): no evidence; single stored valid; stored expired/not-yet-valid/revoked/invalid/untrusted rows that do not collapse to missing; multiple same-project valid rows choose the deterministic newest credential; multi-project stored + URN hint; multi-project no hint → project_context_ambiguous; malformed credential list; credential-list membership VC fast path by source_refs.source_cid; duplicate source-CID conflict; blob CID/body mismatch; credential-list membership VC valid; list with non-membership VC ignored; multiple membership VCs with and without URN selector; expired; untrusted issuer; subject mismatch; URN/credential project mismatch; unknown URN; stored revoked. Reuse credential fixtures from shared/servicekit/projectmembership/testdata/gateway_studio_adapter_contract.json (already mirrored to console) — extend that fixture, don’t fork it (GAT-248 seed).
  • Fixture realism check: capture one actual integrity-issued ProjectMembershipV1 (dev integrity-service, POST /gov/v1/identity/membership/{uuid}) and assert projectmembership.ValidateCredential accepts its DataIntegrityProof/eddsa-rdfc-2022 proof — the existing fixture’s proof suite may not match production issuance. Same later for a ProjectComplianceV1 fixture (GAT-242).
  • Unit — mode matrix: each reason code × {disabled, observe, enforce} → expected (status, denied?, audit fields). This table doubles as the GAT-247 acceptance-matrix rows.
  • Integration — gatewayhttp: signed request (reuse gateway-dev-e2e/signing.go helpers) against a server with seeded project_membership_evidence: (a) observe → 200, audit row has project_urn + membership_validation_status; (b) enforce + no evidence → 403 project_membership_missing, route_type=local_deny; (c) credential-list variant with blob pre-uploaded.
  • Audit sink: migration 0031 round-trip — ExchangeRecord → INSERT → row assert; assert no raw credential bytes anywhere in the row and no raw X-Viper-* values in persisted header snapshots.
  • E2E (GAT-248/249/250 hook): extend gateway-dev-e2e with a membership-seeding step; demo gate = audit timeline in console shows project URN + reason code per request.
  • L7 relay, not a catalog: no new resource-catalog endpoints; gateway only reads evidence/references it already stores.
  • Snapshots are the policy vehicle: enforcement expressed via project-scoped snapshot entries + plugin modes; the project gate only establishes which scope applies.
  • Existing mediation preserved: everything remains behind the ProjectContextMode=disabled default. Either enabled Gateway mode fails closed for non-configurable protocol/binding and bootstrap-infrastructure failures; builtin.project_membership.mode=observe alone converts membership-policy denials into would-deny audit outcomes without establishing context or running ordinary project hooks.
  • Enforcement local to Gateway: no synchronous Auth/Governance/Integrity calls in the request path.
  • Authority stays outside Gateway: issuance = Governance Studio; membership submission/refresh = control-plane; Viper presents; Gateway validates + decides.
  • No credential leakage: credential travels as blob CID; downstream requests and raw audit header snapshots strip/redact gateway-internal X-Viper-* values; structured audit stores resolved IDs/statuses only.
  • Aug 1 honesty: project trust v0 is observe-first; live revocation checking is explicitly deferred with stored-status fallback.