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:
| Concept | Reality in repo |
|---|---|
| ProjectMembershipV1 contract | Fully 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 references | shared/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 snapshots | Runtime 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 evidence | extregistry.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/disabled | project_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. |
| ATO | The 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 truth | governance-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 primitives | RFC 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 redaction | GAT-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)1.1 Request contract (GAT-241 / GAT-227)
Section titled “1.1 Request contract (GAT-241 / GAT-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 intoX-Viper-Credential-CID, synced insync_attestation_blobs, attached + covered inprepare_signed_proxy_request
1.2 Verification (existing, unchanged)
Section titled “1.2 Verification (existing, unchanged)”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:
- Header credential list (
X-Viper-Credential-CIDpresent): parse the canonical bounded CID list (project_credentials_malformedon bad syntax). For each CID, first tryproject_membership_evidence.GetBySourceCID(subjectDID, sourceCID)usingsource_refs.source_cid. Otherwise fetch fromgateway_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, wherebuiltin.project_membershipperforms those configurable checks. - Stored evidence (no usable membership VC in the credential list): query
project_membership_evidencebysubject_did, optionally constrained by signedX-Viper-Project-URN, without filtering out non-valid rows up front.- 0 candidate rows →
Source: none(+project_membership_missingonly if enforcement requires membership) - 1 distinct project URN → choose the deterministic same-project credential (newest
last_verified_at, then latestvalid_until, then latestobserved_at, thencredential_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
- 0 candidate rows →
- 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” | |
|---|---|---|
| Credential | ProjectMembershipV1 (identity attestation) | ProjectComplianceV1 (ComponentAttestation) |
| Subject | agent did:key:... | urn:uuid:{project_uuid} |
| Asserts | agent is a member of project, with roles | project is approved against a policy (component.policy = urn:cid:..., authorizedBy = approver DID) |
| Issuance | POST {integrity}/gov/v1/identity/membership/{uuid} | POST {integrity}/gov/v1/policy/compliance/{uuid} |
| System of record for evidence | gateway project_membership_evidence (submitted by Studio via control-plane) | governance-backend ProjectPolicyCredential rows — no gateway-side store exists today |
| Revocation | BitstringStatusList | BitstringStatusList (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}/trustaccepting{credential_id, source_cid, refresh_status}refs (mirror of the membership submit), validated with aProjectComplianceV1-aware sibling ofprojectmembership.ValidateCredential, indexed into a newproject_trust_evidencetable (or trust columns ongateway_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.
1.6 Project snapshot selection (GAT-221)
Section titled “1.6 Project snapshot selection (GAT-221)”Project-scoped snapshots are the membership and ordinary-policy vehicle:
PinProjectSnapshotresolves withEnvironmentFallbackEnabled:false; missing project policy fails closed.- Project snapshots require exactly one enabled premade
builtin.project_membershipentry atproject.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.endpersists 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.
1.7 Policy context (GAT-243)
Section titled “1.7 Policy context (GAT-243)”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.statusSame keys on LLM and relay paths (handleLLMRoute,handleRelayRoute) so a single Rego policy can gate both. Per-pluginPluginMode ∈ {enforcement, observability}andenabledremain the policy-level switches.
1.8 Modes and reason codes (GAT-244)
Section titled “1.8 Modes and reason codes (GAT-244)”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.observeorenforce— 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):
| Code | Meaning |
|---|---|
project_presentation_unsigned | Credential/project header was not signature-covered |
project_credentials_malformed | Credential CID list was malformed or over limit |
project_membership_missing | No header or stored evidence for signer DID |
project_membership_unresolvable | Referenced membership blob/evidence could not resolve |
project_membership_cid_mismatch | Blob body recomputed to a different JCS CID |
project_membership_evidence_conflict | Source-CID/evidence lookup returned conflicting rows |
project_membership_invalid | Credential failed structural/proof validation |
project_membership_expired / _not_yet_valid | Validity window |
project_membership_untrusted_issuer | Issuer not in allowlist |
project_membership_subject_mismatch | credentialSubject.id ≠ request signer DID |
project_membership_project_mismatch | Credential project ≠ presented URN |
membership_revoked | Stored credential_status revoked (live check deferred) |
project_context_ambiguous | Multi-project agent, no signed URN hint |
project_unknown | URN not in gateway_project_references |
project_resolver_error | Local storage or resolver failure |
project_trust_missing | No ProjectComplianceV1 evidence stored for the project |
project_trust_invalid / _expired | Trust credential failed validation / validity window |
project_trust_revoked | Stored trust credential_status revoked |
project_snapshot_missing | Candidate project has no active snapshot |
project_snapshot_error | Scoped snapshot resolution/compilation failed |
project_membership_bootstrap_error | Mandatory typed bootstrap missing/invalid/failed |
project_membership_role_denied | Allowed/required role policy failed |
1.9 Audit (GAT-244)
Section titled “1.9 Audit (GAT-244)”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/httpsiglifted fromllm-gateway/service/internal/sig9421+verify/content_digest.go; promote header constants (X-Viper-*,X-Guardian-*) intoshared/servicekit/httpxand delete the ≥6 duplicated per-file definitions (gatewayserver.go/relay.go/verifier.go, plugins builtins). - Rust:
viper/src/proxy/signing.rsis already the reusable core; add asign_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 + uuid — no 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.
2. Dependency map
Section titled “2. Dependency map”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 gatewayext-registry wire-in — independent, tiny (adapter already built), observe-onlyGAT-247 (acceptance matrix/demo gates) — consumes all; define observe-mode gates now, enforce gates behind mode flagGAT-248/249/250 (fixtures/E2E) — one validation step per slice, seeded from slice-1 fixturesHistorical 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.
- Contract doc:
rfc-project-membership-presentation-contract.mdis the normative GAT-241 artifact;llm-gateway/docs/verification.mdsummarizes implemented gateway behavior. internal/projectctxpackage:Resolveras above; header path + stored-evidence path; usesservicekit/projectmembershipand narrow store interfaces overproject_membership_evidence+gateway_project_references. The evidence store provides indexedGetBySourceCID(subjectDID, sourceCID)and a count-freeListPagefor runtime pagination; conflicting duplicate source-CID rows map toproject_membership_evidence_conflict.- Config:
ProjectContextModeonconfig.Runtime(TOML/env/flag, mirroringPluginsEnabledplumbing) +TrustedMembershipIssuerDIDs. - Wire into
handleVerifyProxyafter identity enrichment (server.go:~1531): resolve, stash intohookStatecontext keys, populaterequestOutcome/audit; inenforce,writeLocalDenyon the deny-set codes; inobserve, log + record only. - Migration 0031 +
ExchangeRecordfields + sink columns, without a blocking audit-table index build; GAT-225 owns that query index. - 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 bysource_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; storedrevoked. Reuse credential fixtures fromshared/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 assertprojectmembership.ValidateCredentialaccepts itsDataIntegrityProof/eddsa-rdfc-2022proof — the existing fixture’s proof suite may not match production issuance. Same later for aProjectComplianceV1fixture (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 (reusegateway-dev-e2e/signing.gohelpers) against a server with seededproject_membership_evidence: (a) observe → 200, audit row hasproject_urn+membership_validation_status; (b) enforce + no evidence → 403project_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 rawX-Viper-*values in persisted header snapshots. - E2E (GAT-248/249/250 hook): extend
gateway-dev-e2ewith a membership-seeding step; demo gate = audit timeline in console shows project URN + reason code per request.
5. Constraint check
Section titled “5. Constraint check”- 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=disableddefault. Either enabled Gateway mode fails closed for non-configurable protocol/binding and bootstrap-infrastructure failures;builtin.project_membership.mode=observealone 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.