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

# Principal Route Resolution Foundation

## For the Implementing Agent

Filling implementation-owned sections of `CODE_REVIEW-cloud-G08.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 next state.

## Background

Managed model discovery and explicit route selection should consume one immutable principal projection view. Because packet 03 intentionally leaves projection transport dormant until authenticated CP–Edge transport exists, this packet builds and tests the managed route behavior without enabling it in production or claiming S06/S07 completion.

## Archive Evidence Snapshot

- Prior pair: `agent-task/m-principal-provider-credential-slot-routing/04+03_principal_routes/plan_local_G08_0.log`, `agent-task/m-principal-provider-credential-slot-routing/04+03_principal_routes/code_review_cloud_G09_0.log`.
- Verdict/evidence: implementation had not started; no verdict or verification output.
- Replan reason: prior plan claimed principal discovery/selection rollout while its authorization projection would traverse an unauthenticated transport.
- Roadmap carryover: PASS is foundation-only and must not check `model-discovery` or `explicit-selection`.

## Analysis

### Files Read

- `apps/edge/internal/openai/routes.go`
- `apps/edge/internal/openai/route_resolution.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/server.go`
- `apps/edge/internal/openai/openai_auth_routes_models_test.go`
- `apps/edge/internal/openai/provider_selection_test.go`
- `apps/edge/internal/openai/anthropic_surface_test.go`
- `apps/edge/internal/service/provider_pool.go`
- `apps/edge/internal/service/protocol_profile_test.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` (`승인`, 잠금 해제).
- Foundation maps to S06 / `model-discovery` and S07 / `explicit-selection`, Evidence Map S04-S08, but lacks authenticated projection rollout evidence and therefore has no Roadmap Targets.
- Checklist covers principal list isolation, inactive omission, exact provider/profile/model candidate predicate, stable slot metadata, recovery predicate retention, and no legacy/alternate-slot fallback in a directly installed verified-cache fixture.

### Verification Context

- handoff 없음. Existing provider-pool `AcceptCandidate` is reapplied during queued/recovery resolution and is the deterministic no-fallback oracle.
- Local Go-only tests use a counting fake service; no provider credential or network runner is required. Workspace-local Go temp/cache paths are mandatory.
- Predecessor 03 is incomplete and will be archived on PASS; verification checks the narrow archived candidate.
- Confidence: high for dormant foundation, not for production rollout.

### Test Coverage Gaps

- Existing model tests use global catalogs and do not isolate principals.
- Existing selection tests allow provider-pool fallback and do not retain slot/route binding metadata.
- No test proves managed lookup refuses legacy fallback while unmanaged behavior remains unchanged.

### Symbol References

- Rename/remove 없음.
- Production `resolveRouteDispatch` calls: `chat_handler.go:40`, `responses_handler.go:40`, `anthropic_handler.go:49,98`; preserve legacy function and add a context-aware wrapper.
- `advertisedModels` is called at `routes.go:60`; only internal managed branch changes.
- Existing handler `AcceptCandidate` assignments must compose, not overwrite, the route predicate.

### Split Judgment

- Discovery and dispatch resolution share the same route-view invariant and stay atomic.
- Predecessor 03 `03+01,02_projection_auth`: active/archived completion missing, ambiguity 없음.
- Authenticated transport activation remains a separate secure-transport rollout; no Roadmap Targets section is present.

### Scope Rationale

- Do not modify CP store/wire, Connector/Runtime, provider queue accounting, secret delivery, or provider auth header injection.
- Managed behavior is reachable only through packet-03 verified-cache state; current production stays unmanaged/legacy.
- Contracts describe gated semantics, not an active CP-managed deployment mode.

### Final Routing

- `status=routed`, `evaluation_mode=isolated-reassessment`, finalizer=`finalize-task-policy.sh`, pair mode.
- Build closures all true for dormant route foundation; scores `2+1+1+1+2=G07`; base/route=`local-fit`, local, `PLAN-local-G07.md`.
- Review closures all true; scores `2+1+1+2+2=G08`; `official-review`, cloud, `gpt-5.6-sol` xhigh, `CODE_REVIEW-cloud-G08.md`.
- `large_indivisible_context=false`; risks=`boundary_contract`, `structured_interpretation`, `variant_product` (3); rework=0; integrity failure=false; no capability gap.

## Dependencies and Execution Order

1. `03+01,02_projection_auth` must complete; runtime accepts its archived `complete.log`.
2. Implement principal route list/lookup, then compose its fixed predicate in all three handlers.
3. No dependency absent from `04+03_principal_routes` may be introduced.

## Implementation Checklist

- [ ] Return only a verified-cache principal's active projected route IDs from OpenAI and Anthropic model-list variants, covering the S06 isolation fixtures without production activation.
- [ ] Resolve request model to one projected route and compose fixed provider/profile/upstream-model predicates plus stable slot metadata across initial and recovery dispatch, covering S07 no-fallback fixtures.
- [ ] Preserve unmanaged global/legacy behavior and prove managed unknown/inactive/cross-principal routes never fall back or dispatch.
- [ ] Keep contracts explicitly gated and run fresh focused/full Edge, dormant-activation, vet, and diff verification.
- [ ] Fill implementation-owned sections in CODE_REVIEW-*-G??.md with actual implementation notes and verification output.

### [API-1] Add principal-scoped dormant discovery

**Problem:** `apps/edge/internal/openai/routes.go:60` always lists the global catalog and ignores authenticated principal context.

**Solution:** When the packet-03 cache is managed, list only active routes for the request principal from one snapshot, sort by canonical route id, and use the same list for OpenAI and Anthropic shapes. Unmanaged mode retains current behavior.

Before (`apps/edge/internal/openai/routes.go:60-66`):

```go
models := s.advertisedModels()
if anthropic {
    if err := validateAnthropicHeaders(r, false); err != nil {
        writeAnthropicError(w, http.StatusBadRequest, "invalid_request_error", err.Error())
        return
    }
    writeAnthropicModels(w, models)
```

After:

```go
models, err := s.advertisedModelsForPrincipal(r.Context())
if err != nil { s.writeManagedRouteError(w, r, err); return }
```

**Modified Files and Checklist:**

- [ ] `apps/edge/internal/authprojection/cache.go`: expose active route snapshots for exactly one principal.
- [ ] `apps/edge/internal/openai/principal_routes.go`: implement managed list/normalization/sorting/errors.
- [ ] `apps/edge/internal/openai/routes.go`: use managed list for both variants.
- [ ] `apps/edge/internal/openai/principal_routes_test.go`: cover two-principal isolation, inactive/ambiguous omission, ordering, aliases, and unmanaged compatibility.

**Test Strategy:** Write `TestManagedModelsListIsPrincipalScoped`, `TestManagedAnthropicModelsListUsesRouteIDs`, `TestManagedModelsOmitInactiveRoutes`, and `TestUnmanagedModelsRetainGlobalCatalog`.

**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 'Managed.*Models|PrincipalRoute|UnmanagedModels'` passes.

### [API-2] Fix managed requests to one projected route

**Problem:** `apps/edge/internal/openai/route_resolution.go:103-115` resolves global model groups, and handlers may overwrite request-local candidate predicates.

**Solution:** Add a context-aware wrapper. Managed mode resolves one principal route and returns route id, slot id, provider id, profile id, and upstream model; unmanaged mode calls the legacy resolver. Compose its predicate with stream/profile predicates so initial and recovery candidate resolution cannot change the binding.

Required standard-library import:

```go
import "context"
```

Before (`apps/edge/internal/openai/route_resolution.go:103-108`):

```go
// resolveRouteDispatch returns fully-resolved dispatch params for model.
// Priority: provider-pool catalog → legacy model_routes → single-target fallback.
// Returns (dispatch, true) on success; (zero, false) when no target can be resolved.
func (s *Server) resolveRouteDispatch(model string) (routeDispatch, bool) {
    // Provider-pool catalog takes highest priority.
    if catalogEntry := s.findProviderPoolEntry(model); catalogEntry != nil {
```

After:

```go
func (s *Server) resolveRouteDispatchForPrincipal(ctx context.Context, model string) (routeDispatch, error) {
    if s.principalProjection.Managed() { return s.resolveProjectedRoute(ctx, model) }
    dispatch, ok := s.resolveRouteDispatch(model)
    if !ok { return routeDispatch{}, ErrRouteNotFound }
    return dispatch, nil
}
```

**Modified Files and Checklist:**

- [ ] `apps/edge/internal/openai/route_resolution.go`: add managed wrapper and route/slot facts while preserving legacy resolver.
- [ ] `apps/edge/internal/openai/principal_routes.go`: resolve one route, compose predicates, and attach trusted metadata.
- [ ] `apps/edge/internal/openai/chat_handler.go`: use wrapper and composed route/stream predicate.
- [ ] `apps/edge/internal/openai/responses_handler.go`: use wrapper and composed route/stream predicate.
- [ ] `apps/edge/internal/openai/anthropic_handler.go`: use wrapper and composed route/capability predicate.
- [ ] `apps/edge/internal/openai/principal_routes_test.go`: test exact candidate filtering, recovery retention, zero fallback, and all surfaces.

**Test Strategy:** Write `TestManagedRouteSelectsOnlyBoundSlot`, `TestManagedRoutePredicateSurvivesRecovery`, `TestManagedRouteFailureDoesNotFallback`, and table cases for Chat/Responses/Messages/count_tokens.

**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 'ManagedRoute|ExplicitSlot|NoFallback'` passes.

### [API-3] Keep gated contracts honest

**Problem:** contracts cannot describe principal-managed routing as available while no authenticated projection writer is wired.

**Solution:** Document route-id selection and no-fallback semantics under an explicit “verified managed projection only” gate, and state that current deployments remain legacy until secure transport activation.

Before (`apps/edge/internal/openai/anthropic_handler.go:49-54`):

```go
dispatch, ok := s.resolveRouteDispatch(envelope.Model)
if !ok || !dispatch.ProviderPool {
    writeAnthropicError(w, http.StatusBadRequest, "not_supported_error", "model does not resolve to a protocol profile")
    return
}
```

After:

```go
dispatch, err := s.resolveRouteDispatchForPrincipal(r.Context(), envelope.Model)
if err != nil || !dispatch.ProviderPool { s.writeAnthropicRouteError(w, err); return }
```

**Modified Files and Checklist:**

- [ ] `agent-contract/outer/openai-compatible-api.md`: document verified-cache gate, route ids, and no fallback.
- [ ] `agent-contract/outer/anthropic-compatible-api.md`: document the same gated model-list/selection semantics.

**Test Strategy:** No docs-only test; API-1/API-2 tests are executable contract evidence.

**Verification:** `rg --sort path -n 'verified|managed|route_id|slot_id|fallback' agent-contract/outer/openai-compatible-api.md agent-contract/outer/anthropic-compatible-api.md` shows gated clauses.

## Modified Files Summary

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

## Final Verification

Run from `/config/workspace/iop-s0`; fresh Go results are required.

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` — executable Go paths exist.
3. `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.
4. `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 'Managed|PrincipalRoute|ExplicitSlot|NoFallback|Anthropic'` — focused fixtures pass.
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 ./apps/edge/internal/service` — affected suites pass.
6. `GOTMPDIR=/config/workspace/iop-s0/.cache/go-build GOCACHE=/config/workspace/iop-s0/.cache/go-cache go test -count=1 ./apps/edge/...` — Edge regression suite passes.
7. `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 activation.
8. `git diff --check` — no whitespace errors.

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