<!-- task=m-principal-provider-credential-slot-routing/04+03_principal_routes plan=2 tag=REVIEW_API -->

# Managed Principal Route Binding Rework

## For the Implementing Agent

Filling implementation-owned sections of `CODE_REVIEW-cloud-G09.md` is mandatory. Run every verification command, paste actual output, leave active files in place, and report ready for review; only code-review may finalize, rename logs, write `complete.log`, or archive. If blocked, record exact blocker evidence and resume conditions only in implementation-owned fields; do not ask the user, call user-input tools, create stop files, or classify the next state.

## Background

The preceding foundation review failed because it treated a provider credential slot as a provider resource ID, passed the public route ID where the provider pool requires an internal catalog model-group key, discarded route/credential revisions, and authenticated and selected routes from separate projection loads. Its passing tests exercised helper predicates and rejection responses but never proved a successful managed provider-pool dispatch or recovery re-entry. This follow-up fixes those identity, snapshot, metadata, and evidence gaps while keeping projection transport dormant.

## Archive Evidence Snapshot

- Failed pair after review archive: `agent-task/m-principal-provider-credential-slot-routing/04+03_principal_routes/plan_local_G07_1.log` and `agent-task/m-principal-provider-credential-slot-routing/04+03_principal_routes/code_review_cloud_G08_1.log`.
- Verdict: FAIL; Required=4, Suggested=0, Nit=0; `review_rework_count=1`, `evidence_integrity_failure=false`.
- Required findings: separate public route, internal model group, provider resource, and credential-slot identities; retain trusted route/credential revisions; authenticate and route from one immutable projection view; replace helper-only tests with successful adverse-order and recovery integration fixtures on every managed surface.
- Affected implementation areas: `apps/edge/internal/authprojection`, OpenAI-compatible ingress/dispatch/recovery, managed route tests, and the two compatible API contracts.
- Verification evidence: fresh vet, focused tests, race tests, affected-package tests, full `./apps/edge/...`, dormant-activation, formatting, and diff checks passed, but those commands did not cover the failed success/recovery invariants.
- Roadmap carryover: this remains a dormant foundation packet with no Roadmap Targets; PASS must not check `model-discovery` or `explicit-selection` until authenticated projection transport is activated elsewhere.

## Analysis

### Files Read

- `apps/edge/internal/authprojection/cache.go`
- `apps/edge/internal/authprojection/cache_test.go`
- `apps/edge/internal/openai/principal.go`
- `apps/edge/internal/openai/routes.go`
- `apps/edge/internal/openai/principal_routes.go`
- `apps/edge/internal/openai/route_resolution.go`
- `apps/edge/internal/openai/dispatch_context.go`
- `apps/edge/internal/openai/chat_handler.go`
- `apps/edge/internal/openai/chat_completion.go`
- `apps/edge/internal/openai/responses_handler.go`
- `apps/edge/internal/openai/anthropic_handler.go`
- `apps/edge/internal/openai/provider_tunnel.go`
- `apps/edge/internal/openai/stream_gate_runtime.go`
- `apps/edge/internal/openai/principal_routes_test.go`
- `apps/edge/internal/openai/provider_test_support_test.go`
- `apps/edge/internal/service/provider_resolution.go`
- `apps/edge/internal/service/provider_pool.go`
- `packages/go/config/provider_types.go`
- `agent-contract/outer/openai-compatible-api.md`
- `agent-contract/outer/anthropic-compatible-api.md`

### SDD Criteria

- SDD: `agent-roadmap/sdd/operational-observability-provider-management/principal-provider-credential-slot-routing/SDD.md` (`승인`, implementation lock released).
- The foundation maps to S06 / `model-discovery` and S07 / `explicit-selection`, Evidence Map S04-S08, but authenticated transport remains absent, so this plan intentionally has no Roadmap Targets.
- SDD lines 83-96 separate the public route, credential slot, provider resource selector, profile, upstream model, and safe attribution revisions. A provider resource may change for capacity while the credential slot remains immutable; therefore slot identity cannot be a provider candidate ID.
- The required oracle is one request-local projection view, one exact catalog model group, a selector-limited provider candidate set, immutable safe slot/revision metadata, and zero fallback to another public route, catalog group, profile, upstream model, or legacy route.

### Verification Context

- The predecessor `03+01,02_projection_auth` has exactly one archived `complete.log`; its cache is an in-process verified fixture and its production writer remains dormant.
- All required evidence is local and deterministic. A recording fake `SubmitProviderPool` service can evaluate candidate order twice to represent initial admission and recovery re-entry without credentials, network access, or an external runner.
- `go test ./apps/edge/...` is the relevant local regression boundary. The Ollama smoke pipeline cannot activate or observe this dormant managed cache and is not a substitute for the in-process provider-pool oracle.
- Confidence: high once distinct-identity success, metadata spoof resistance, generation swap, and repeated candidate admission are executable.

### Test Coverage Gaps

- The projection fixture discards each route's `ResourceSelector` and replaces it with `default`.
- Existing candidate tests use the credential-slot ID as `ProviderPoolCandidate.ProviderID`, encoding the production bug as the expected result.
- Existing recovery coverage calls a composed predicate once instead of re-entering provider-pool selection with an adverse candidate set.
- Surface coverage verifies unknown-route 400 responses only; no successful Chat, Responses, Messages, or count-tokens request records the internal model group or trusted route/slot metadata.
- No interleaving test swaps projection generation after authentication and before handler route resolution.

### Symbol References

- `authprojection.Reader` is installed through `Server.SetPrincipalProjection`; preserve the transport-neutral reader boundary and add one atomic authenticated-view operation rather than exposing raw snapshots.
- `withAuth` currently stores only `openAIPrincipal`; extend request context with a secret-free authenticated projection view while preserving unmanaged static authentication.
- Every provider-pool `ModelGroupKey` derived from caller `model` must instead use a managed internal key. Public request/model echo fields remain the caller's route ID or alias.
- `AcceptCandidate` is retained in `ProviderPoolDispatchRequest` during Core recovery. Preserve that copy path and prove the same managed predicate and metadata are present on the second admission.
- Rename/remove 없음.

### Split Judgment

- Snapshot identity, catalog mapping, credential binding metadata, and recovery evidence form one security invariant; splitting them would allow another apparently passing but undispatchable or cross-generation state.
- Predecessor index 03 is satisfied by `agent-task/archive/2026/08/m-principal-provider-credential-slot-routing/03+01,02_projection_auth/complete.log`; no predecessor ambiguity exists.
- Secure projection delivery, credential lease acquisition, secret injection, and production activation remain separate later packets and must not enter this task.

### Scope Rationale

- Modify only the immutable cache read surface, compatible API request context/route construction/recovery consumers, focused tests, and gated contract wording.
- Do not modify Control Plane stores or wire handlers, Edge Connector/Runtime activation, Node/provider secret delivery, provider queue accounting, or raw provider auth injection.
- Preserve unmanaged global catalog, legacy model routes, single-target fallback, public response schemas, and caller-facing model echo behavior.

### Final Routing

- `status=routed`, `evaluation_mode=isolated-reassessment`, finalizer=`finalize-task-policy.sh`, pair mode.
- Build closures are all true; scores `2+2+1+2+1=G08`; base=`local-fit`, route=`risk-boundary`, lane=`cloud`, canonical file=`PLAN-cloud-G08.md`.
- Review closures are all true; scores `2+2+2+2+1=G09`; route=`official-review`, lane=`cloud`, adapter=`codex`, model=`gpt-5.6-sol`, reasoning=`xhigh`, canonical file=`CODE_REVIEW-cloud-G09.md`.
- `large_indivisible_context=false`; positive loop risks=`concurrent_consistency`, `boundary_contract`, `structured_interpretation`, `variant_product` (4); `review_rework_count=1`; `evidence_integrity_failure=false`; no capability gap.

## Dependencies and Execution Order

1. Preserve the archived packet-03 verified-cache contract and keep production projection activation dormant.
2. Add the single-load authenticated view and request-context propagation before changing discovery or route selection.
3. Resolve the projected selector/model tuple to an internal catalog group, then carry that key and trusted route/slot revisions through all initial and recovery request builders.
4. Add distinct-identity, spoofing, generation-swap, adverse-order, and repeated-admission tests before updating gated contract wording and running final verification.

## Implementation Checklist

- [ ] Authenticate, discover, and select routes from one secret-free immutable projection view, with a deterministic generation-swap test.
- [ ] Resolve each managed route to exactly one internal catalog model group and selector-limited provider set without treating the credential slot as a provider ID or falling back.
- [ ] Overwrite caller metadata with trusted route/slot revisions and preserve the same internal model-group key, predicate, and metadata across Chat, Responses, Anthropic, and Core recovery builders.
- [ ] Replace helper-only fixtures with distinct-identity successful and fail-closed integration tests for Chat Completions, Responses, Anthropic Messages, and count-tokens.
- [ ] Keep both compatible API contracts explicitly gated and run fresh formatting, vet, race, focused, affected-package, full Edge, dormant-activation, and diff verification.
- [ ] Fill implementation-owned sections in CODE_REVIEW-*-G??.md with actual implementation notes and verification output.

### [REVIEW_API-1] Bind authentication and route reads to one snapshot

**Problem:** `authenticatePrincipal` calls `LookupDigest`, while discovery and route resolution later call `State` and `RoutesForPrincipal` independently. A generation swap between those calls can authenticate against generation N and select generation N+1 routes.

**Solution:** Add a bounded, secret-free authenticated-view value containing the matched principal, copied routes for that principal, generation, and state, all produced from one `atomic.Pointer` load and one constant-time digest scan. Return it through the narrow reader API, store it beside the principal in request context, and make managed discovery/selection read only that view. Unmanaged authentication remains unchanged; managed requests with no fresh authenticated view fail closed.

**Modified Files and Checklist:**

- [ ] `apps/edge/internal/authprojection/cache.go`: add the atomic authenticated-view read without exposing raw tokens or mutable snapshot slices.
- [ ] `apps/edge/internal/authprojection/cache_test.go`: cover view copying, principal isolation, expiry, and one-load generation identity.
- [ ] `apps/edge/internal/openai/principal.go`: carry the safe view in request context and retain current principal metadata behavior.
- [ ] `apps/edge/internal/openai/routes.go`: install both authenticated principal and view before entering a handler.
- [ ] `apps/edge/internal/openai/principal_routes.go`: consume only the request view for managed discovery and selection.
- [ ] `apps/edge/internal/openai/principal_routes_test.go`: swap to generation N+1 inside the authenticated handler and prove the request still sees only generation N routes.

**Test Strategy:** Add cache-level view immutability cases and an HTTP interleaving test whose wrapped handler applies a new projection after `withAuth` succeeds but before it calls models/route resolution.

**Verification:** `GOTMPDIR=/config/workspace/iop-s0/.cache/go-build GOCACHE=/config/workspace/iop-s0/.cache/go-cache go test -count=1 ./apps/edge/internal/authprojection ./apps/edge/internal/openai -run 'AuthenticatedView|ProjectionView|GenerationSwap|ManagedModels'` passes.

### [REVIEW_API-2] Separate public route, catalog, provider, and slot identities

**Problem:** `resolveProjectedRoute` assigns `CredentialSlotRef` to `ProviderID`, ignores `ResourceSelector`, and handlers submit the public route ID as `ModelGroupKey`. The service accepts only a catalog entry ID and represents provider resources separately.

**Solution:** Add a pure catalog resolver over the current immutable catalog copy. A `default` selector imposes no provider-ID restriction; a non-default selector must equal a provider ID key. Match served model to `UpstreamModel`, require exactly one catalog entry containing at least one selector-compatible provider mapping, and fail closed on zero or multiple entries. Carry the selected catalog entry ID as `routeDispatch.ModelGroupKey`. The managed candidate predicate restricts an explicit provider selector plus `ProfileID` and `UpstreamModel`; it never compares a provider ID with `CredentialSlotRef`.

Before (`apps/edge/internal/openai/principal_routes.go:118-130`):

```go
ProviderID:        matchedRoute.CredentialSlotRef,
RouteID:           matchedRoute.RouteID,
CredentialSlotRef: matchedRoute.CredentialSlotRef,
ManagedPredicate:  managedRouteCandidatePredicate(*matchedRoute),
```

After:

```go
binding, err := resolveManagedCatalogBinding(*matchedRoute, s.modelCatalogSnapshot())
if err != nil { return routeDispatch{}, err }
ModelGroupKey:     binding.ModelGroupKey,
ProviderID:        binding.ProviderID,
CredentialSlotRef: matchedRoute.CredentialSlotRef,
ManagedPredicate:  managedRouteCandidatePredicate(*matchedRoute, binding.ProviderID),
```

**Modified Files and Checklist:**

- [ ] `apps/edge/internal/openai/route_resolution.go`: add separate managed model-group, resource-selector, and revision fields plus an effective key helper.
- [ ] `apps/edge/internal/openai/principal_routes.go`: implement exact catalog resolution and corrected candidate filtering.
- [ ] `apps/edge/internal/openai/chat_handler.go`: use the managed internal key for catalog policy and Run/Tunnel provider-pool requests.
- [ ] `apps/edge/internal/openai/responses_handler.go`: use the managed internal key for catalog policy, local preparation, and Run/Tunnel requests.
- [ ] `apps/edge/internal/openai/anthropic_handler.go`: use the managed internal key for Messages, count-tokens lookup, Run, and Tunnel requests.
- [ ] `apps/edge/internal/openai/provider_tunnel.go`: keep direct legacy behavior but use the route's effective internal key whenever a provider-pool route is rebuilt.
- [ ] `apps/edge/internal/openai/stream_gate_runtime.go`: use the frozen internal key for recovery catalog policy and context-window lookup.

**Test Strategy:** Use `route-public`, `catalog-internal`, `provider-resource`, and `credential-slot` as four distinct values. Cover `default`, explicit provider selector, missing selector target, ambiguous catalog groups, wrong profile/model, and no legacy/global fallback.

**Verification:** `GOTMPDIR=/config/workspace/iop-s0/.cache/go-build GOCACHE=/config/workspace/iop-s0/.cache/go-cache go test -count=1 ./apps/edge/internal/openai -run 'ManagedCatalogBinding|ManagedRoute|ExplicitSelector|NoFallback'` passes.

### [REVIEW_API-3] Preserve trusted binding metadata through recovery

**Problem:** `routeDispatch` discards route and credential revisions, and only authenticated principal metadata overwrites caller values. Recovery request builders may reconstruct requests from the public model rather than the internal group and have no executable proof of immutable slot attribution.

**Solution:** Retain `RouteRevision`, `CredentialRevision`, and `ResourceSelector` in `routeDispatch`. Define safe internal metadata constants `iop_route_id`, `iop_route_revision`, `iop_credential_slot_ref`, and `iop_credential_revision`; a single helper must clone and overwrite caller values from the resolved managed route. Apply it before Chat/Responses metadata is frozen and in Anthropic pool construction. Preserve the public route in `openai_model`/`anthropic_model`, but use the internal model group for admission. Ensure every Core recovery path copies the original `AcceptCandidate`, internal key, and trusted metadata unchanged.

**Modified Files and Checklist:**

- [ ] `apps/edge/internal/openai/route_resolution.go`: retain immutable managed route, slot, selector, and revision facts.
- [ ] `apps/edge/internal/openai/dispatch_context.go`: centralize caller-metadata overwrite with trusted managed binding metadata.
- [ ] `apps/edge/internal/openai/principal.go`: define safe binding metadata keys alongside principal keys.
- [ ] `apps/edge/internal/openai/chat_handler.go`: freeze trusted metadata into normalized and tunnel pool templates.
- [ ] `apps/edge/internal/openai/responses_handler.go`: freeze identical trusted metadata into both execution branches.
- [ ] `apps/edge/internal/openai/anthropic_handler.go`: add trusted metadata to Messages and native count-tokens fallback dispatch.
- [ ] `apps/edge/internal/openai/stream_gate_runtime.go`: preserve the same binding metadata and predicate on repeated admission.
- [ ] `apps/edge/internal/openai/principal_routes_test.go`: attempt metadata spoofing and compare the first and recovery request records byte-for-byte for all managed binding keys.

**Test Strategy:** The recording fake service evaluates candidates in adverse order for initial admission, repeats evaluation with another adverse order for recovery, records Run/Tunnel templates, and asserts one internal group, one allowed provider/profile/model tuple, zero alternate admission, and identical trusted route/slot revisions.

**Verification:** `GOTMPDIR=/config/workspace/iop-s0/.cache/go-build GOCACHE=/config/workspace/iop-s0/.cache/go-cache go test -race -count=1 ./apps/edge/internal/authprojection ./apps/edge/internal/openai -run 'Managed|RouteBinding|Recovery|MetadataSpoof|GenerationSwap'` passes.

### [REVIEW_API-4] Prove every gated compatible surface

**Problem:** The current surface table proves only rejection. It does not demonstrate that any projected route can reach the provider pool, select the correct resource, or preserve slot attribution.

**Solution:** Fix the projection fixture to copy `ResourceSelector`, route and credential revisions. Add table-driven successful requests for OpenAI Chat Completions, Responses, Anthropic Messages, and count-tokens using a recording service that applies `AcceptCandidate` on initial and simulated recovery entry. Retain unmanaged compatibility and managed unknown/inactive/cross-principal rejection. Clarify in both outer contracts that credential slot identity is trusted attribution/lease scope, not provider resource identity, and that the public route may map to a distinct internal model group only inside the verified managed gate.

**Modified Files and Checklist:**

- [ ] `apps/edge/internal/openai/principal_routes_test.go`: replace bug-shaped helpers with successful distinct-identity surface/recovery fixtures and retain fail-closed cases.
- [ ] `agent-contract/outer/openai-compatible-api.md`: distinguish public route, internal model group/provider resource, and credential-slot attribution under the dormant gate.
- [ ] `agent-contract/outer/anthropic-compatible-api.md`: document the same managed identity separation and no-fallback semantics.

**Test Strategy:** Require success records from every surface, including both local TokenCounter lookup by internal group and native count-tokens provider fallback where applicable; assert response model echo remains public while dispatch facts remain internal and trusted.

**Verification:** `rg --sort path -n 'verified managed|route_id|model group|provider resource|credential slot|no fallback' agent-contract/outer/openai-compatible-api.md agent-contract/outer/anthropic-compatible-api.md` shows the gated identity clauses.

## Modified Files Summary

| File | Items |
|------|-------|
| `apps/edge/internal/authprojection/cache.go` | REVIEW_API-1 |
| `apps/edge/internal/authprojection/cache_test.go` | REVIEW_API-1 |
| `apps/edge/internal/openai/principal.go` | REVIEW_API-1, REVIEW_API-3 |
| `apps/edge/internal/openai/routes.go` | REVIEW_API-1 |
| `apps/edge/internal/openai/principal_routes.go` | REVIEW_API-1, REVIEW_API-2 |
| `apps/edge/internal/openai/route_resolution.go` | REVIEW_API-2, REVIEW_API-3 |
| `apps/edge/internal/openai/dispatch_context.go` | REVIEW_API-3 |
| `apps/edge/internal/openai/chat_handler.go` | REVIEW_API-2, REVIEW_API-3 |
| `apps/edge/internal/openai/responses_handler.go` | REVIEW_API-2, REVIEW_API-3 |
| `apps/edge/internal/openai/anthropic_handler.go` | REVIEW_API-2, REVIEW_API-3 |
| `apps/edge/internal/openai/provider_tunnel.go` | REVIEW_API-2 |
| `apps/edge/internal/openai/stream_gate_runtime.go` | REVIEW_API-2, REVIEW_API-3 |
| `apps/edge/internal/openai/principal_routes_test.go` | REVIEW_API-1, REVIEW_API-3, REVIEW_API-4 |
| `agent-contract/outer/openai-compatible-api.md` | REVIEW_API-4 |
| `agent-contract/outer/anthropic-compatible-api.md` | REVIEW_API-4 |
| `agent-task/m-principal-provider-credential-slot-routing/04+03_principal_routes/CODE_REVIEW-cloud-G09.md` | REVIEW_API-1, REVIEW_API-2, REVIEW_API-3, REVIEW_API-4 |

## Final Verification

Run from `/config/workspace/iop-s0`; paste actual stdout/stderr under the matching review-stub command.

1. `test "$(find agent-task/archive -type f -path '*/m-principal-provider-credential-slot-routing/03+01,02_projection_auth/complete.log' -print | wc -l)" -eq 1` — exactly one archived predecessor completion exists.
2. `mkdir -p .cache/go-build .cache/go-cache` — workspace-local Go paths exist.
3. `test -z "$(gofmt -d apps/edge/internal/authprojection/cache.go apps/edge/internal/authprojection/cache_test.go apps/edge/internal/openai/principal.go apps/edge/internal/openai/routes.go apps/edge/internal/openai/principal_routes.go apps/edge/internal/openai/route_resolution.go apps/edge/internal/openai/dispatch_context.go apps/edge/internal/openai/chat_handler.go apps/edge/internal/openai/responses_handler.go apps/edge/internal/openai/anthropic_handler.go apps/edge/internal/openai/provider_tunnel.go apps/edge/internal/openai/stream_gate_runtime.go apps/edge/internal/openai/principal_routes_test.go)"` — planned Go files are formatted.
4. `GOTMPDIR=/config/workspace/iop-s0/.cache/go-build GOCACHE=/config/workspace/iop-s0/.cache/go-cache go vet ./apps/edge/internal/authprojection ./apps/edge/internal/openai ./apps/edge/internal/service` — no vet findings.
5. `GOTMPDIR=/config/workspace/iop-s0/.cache/go-build GOCACHE=/config/workspace/iop-s0/.cache/go-cache go test -count=1 ./apps/edge/internal/authprojection ./apps/edge/internal/openai -run 'AuthenticatedView|ProjectionView|GenerationSwap|ManagedCatalogBinding|ManagedRoute|RouteBinding|ExplicitSelector|NoFallback|MetadataSpoof|Anthropic'` — focused snapshot, identity, surface, and recovery fixtures pass.
6. `GOTMPDIR=/config/workspace/iop-s0/.cache/go-build GOCACHE=/config/workspace/iop-s0/.cache/go-cache go test -race -count=1 ./apps/edge/internal/authprojection ./apps/edge/internal/openai -run 'AuthenticatedView|GenerationSwap|Managed|RouteBinding|Recovery|MetadataSpoof'` — focused race fixtures pass.
7. `GOTMPDIR=/config/workspace/iop-s0/.cache/go-build GOCACHE=/config/workspace/iop-s0/.cache/go-cache go test -count=1 ./apps/edge/internal/authprojection ./apps/edge/internal/openai ./apps/edge/internal/service` — affected package suites pass.
8. `GOTMPDIR=/config/workspace/iop-s0/.cache/go-build GOCACHE=/config/workspace/iop-s0/.cache/go-cache go test -count=1 ./apps/edge/...` — full Edge regression suite passes.
9. `rg --sort path -n 'verified managed|route_id|model group|provider resource|credential slot|no fallback' agent-contract/outer/openai-compatible-api.md agent-contract/outer/anthropic-compatible-api.md` — gated identity clauses are present.
10. `if rg --sort path -n 'PrincipalProjection' apps/control-plane/internal/wire/edge_server.go apps/edge/internal/controlplane/connector.go apps/edge/internal/bootstrap/runtime.go; then exit 1; else exit 0; fi` — unauthenticated runtime still has no projection activation.
11. `git diff --check` — no whitespace errors.

After completing all code changes, fill implementation-owned sections in `CODE_REVIEW-*-G??.md`.
