<!-- task=m-principal-provider-credential-slot-routing/03+01,02_projection_auth plan=0 tag=API -->

# Principal Projection and Surface Authentication

## For the Implementing Agent

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

## Background

Edge authenticates static config hashes and already normalizes Anthropic bearer/`x-api-key`, but it has no Control Plane generation, expiry, revocation, or bounded cache. This packet establishes the CP→Edge projection contract and makes both compatible HTTP surfaces use the same fresh principal snapshot before model lookup or provider dispatch.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/operational-observability-provider-management/milestones/principal-provider-credential-slot-routing.md`
- Milestone link: [Milestone 문서](agent-roadmap/phase/operational-observability-provider-management/milestones/principal-provider-credential-slot-routing.md)
- Task ids:
  - `auth-projection`: generation/expiry principal projection과 bounded Edge cache
  - `surface-auth`: OpenAI/Anthropic 동일 principal 인증
- Completion mode: check-on-pass

## Analysis

### Files Read

- `proto/iop/control.proto`
- `proto/gen/iop/control.pb.go`
- `apps/client/lib/gen/proto/iop/control.pb.dart`
- `apps/client/lib/gen/proto/iop/control.pbenum.dart`
- `apps/client/lib/gen/proto/iop/control.pbjson.dart`
- `apps/client/lib/gen/proto/iop/control.pbserver.dart`
- `apps/control-plane/cmd/control-plane/server.go`
- `apps/control-plane/internal/wire/edge.go`
- `apps/control-plane/internal/wire/edge_server.go`
- `apps/control-plane/internal/wire/edge_test.go`
- `apps/control-plane/internal/wire/edge_server_test.go`
- `apps/edge/internal/controlplane/connector.go`
- `apps/edge/internal/controlplane/connector_test.go`
- `apps/edge/internal/bootstrap/runtime.go`
- `apps/edge/internal/bootstrap/runtime_connector_test.go`
- `apps/edge/internal/input/manager.go`
- `apps/edge/internal/input/manager_test.go`
- `apps/edge/internal/openai/server.go`
- `apps/edge/internal/openai/principal.go`
- `apps/edge/internal/openai/routes.go`
- `apps/edge/internal/openai/identity_metering_test.go`
- `apps/edge/internal/openai/anthropic_surface_test.go`
- `apps/client/test/iop_wire/generated_proto_import_test.dart`
- `agent-contract/inner/control-plane-edge-wire.md`
- `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` (`승인`, 잠금 해제).
- Targets: S02 / `auth-projection`, S03 / `surface-auth`.
- Evidence Map row: S01-S03의 projection generation/expiry 및 dual-header ingress tests 중 S02-S03.
- Checklist는 durable monotonic generation, atomic higher-generation apply, fresh/stale/expired/revoked/out-of-order fixtures, bounded cache, same-token dual header success, mismatched/unregistered/revoked 401-before-handler를 직접 연결한다.

### Verification Context

- handoff 없음. wire/proto generators, CP/Edge tests, outer/inner contracts, domain rules와 local control-plane/edge/client smoke profiles에서 검증을 구성했다.
- Tools: Go 1.26.2, `protoc` 29.3, `protoc-gen-go` v1.36.11, `protoc-gen-dart` present, Flutter 3.41.5/Dart 3.11.3. `make -n proto proto-dart`가 canonical outputs를 확인했다.
- Local preflight compile and focused legacy principal/Anthropic tests pass only with `GOTMPDIR`/`GOCACHE` under ignored workspace `.cache`; `/tmp` is noexec and default cache is inaccessible.
- Preconditions: `01_principal_store` and `02+01_credential_catalog` complete logs. 현재 둘 다 missing이며 ambiguity 없음.
- Full cycle is local CP→Edge TCP plus Edge HTTP; no real provider credential or external host is required because rejection must occur before fake dispatch. Confidence: high.

### Test Coverage Gaps

- Existing Edge auth tests cover static hashes and dual-header mismatch, but not projection generation/expiry/revocation or fail-closed managed mode.
- CP-Edge tests cover hello/status/command only; no projection parser/request/apply acknowledgment exists.
- Runtime/input tests do not prove a shared cache is wired from Connector into OpenAI server.
- Generated Dart import test does not reference projection messages yet.

### Symbol References

- No rename/remove.
- `wire.NewEdgeServer` call sites: `apps/control-plane/cmd/control-plane/server.go:51`, `apps/control-plane/internal/wire/edge_server_test.go:22`; preserve constructor compatibility through an option.
- `controlplane.NewConnector` call site: `apps/edge/internal/bootstrap/runtime.go:83`; existing tests call it directly and remain source-compatible through an option.
- `openai.NewServer` has many tests; do not change its signature. Add setter/sink methods.

### Split Judgment

- Projection and ingress authentication remain one packet because “higher fresh generation atomically replaces token lookup, then middleware authenticates from that exact snapshot” is one correctness invariant. Splitting would allow an invalid state where wire reports success while HTTP still reads static config.
- Predecessor 01 `01_principal_store`: missing active `complete.log`, no archived candidate.
- Predecessor 02 `02+01_credential_catalog`: missing active `complete.log`, no archived candidate.
- Consumer 04 depends only on this packet's complete projection snapshot contract.

### Scope Rationale

- Principal-specific model listing/dispatch is packet 04. Management mutations are packet 05.
- Provider ciphertext delivery/decrypt/header injection is Epic 3 and excluded; projection contains IDs, hashes, route metadata, never raw provider or IOP secrets.
- Legacy config auth remains available only when Edge has never entered CP-managed projection mode; once managed, expiry is fail-closed rather than fallback.

### Final Routing

- `status=routed`, `evaluation_mode=first-pass`, finalizer=`finalize-task-policy.sh`, pair mode.
- Build closures all true; scores `2+2+2+2+2=G10`; base/route=`grade-boundary`, cloud, canonical=`PLAN-cloud-G10.md`.
- Review closures all true; scores `2+2+2+2+2=G10`; `official-review`, cloud, `gpt-5.6-sol` xhigh, canonical=`CODE_REVIEW-cloud-G10.md`.
- `large_indivisible_context=false`; risks=`temporal_state`, `concurrent_consistency`, `boundary_contract`, `variant_product` (4), risk boundary matched but grade basis retained; rework=0; integrity failure=false; no capability gap.

## Dependencies and Execution Order

1. `01_principal_store` must provide `agent-task/m-principal-provider-credential-slot-routing/01_principal_store/complete.log`.
2. `02+01_credential_catalog` must provide `agent-task/m-principal-provider-credential-slot-routing/02+01_credential_catalog/complete.log`.
3. Add durable projection generation/snapshot, then wire apply, then switch ingress authentication to the shared cache.
4. No dependency beyond indices `01,02` may be introduced.

## Implementation Checklist

- [ ] Add durable monotonic principal projection snapshots and the CP-to-Edge protobuf request/acknowledgment contract.
- [ ] Apply only higher, unexpired generations to a bounded atomic Edge cache and retain managed fail-closed state after expiry or revocation.
- [ ] Authenticate OpenAI bearer and Anthropic bearer/x-api-key through the shared cache, rejecting mismatch or invalid tokens before model/dispatch handlers.
- [ ] Update generated Go/Dart code and inner/outer contracts without exposing raw secrets.
- [ ] Run fresh race, wire, client-generation, full-cycle ingress, vet, and diff verification.
- [ ] Fill implementation-owned sections in CODE_REVIEW-*-G??.md with actual implementation notes and verification output.

### [API-1] Produce and synchronize durable projections

**Problem:** `apps/control-plane/internal/wire/edge.go:12-46` knows only hello/status/events/commands, and store mutations have no durable projection generation.

**Solution:** Add a singleton projection generation incremented in the same DB transaction as principal/token/slot/route mutations. Build immutable snapshots of active token digests and route metadata with `generation`, `issued_at`, and `expires_at`. Add CP→Edge `PrincipalProjectionApplyRequest/Response`; push after accepted hello and on newer store generation, with retry safe through generation fencing.

Before (`apps/control-plane/internal/wire/edge.go:12-18`):

```go
return proto_socket.ParserMap{
    proto_socket.TypeNameOf(&iop.EdgeHelloRequest{}): func(b []byte) (proto.Message, error) {
        req := &iop.EdgeHelloRequest{}
        return req, proto.Unmarshal(b, req)
    },
```

After:

```go
proto_socket.TypeNameOf(&iop.PrincipalProjectionApplyRequest{}): func(b []byte) (proto.Message, error) {
    req := &iop.PrincipalProjectionApplyRequest{}
    return req, proto.Unmarshal(b, req)
},
```

**Modified Files and Checklist:**

- [ ] `proto/iop/control.proto`: add projection token/route/snapshot/apply/ack messages with additive field numbers.
- [ ] `proto/gen/iop/control.pb.go`: regenerate Go messages.
- [ ] `apps/client/lib/gen/proto/iop/control.pb.dart`: regenerate Dart messages.
- [ ] `apps/client/lib/gen/proto/iop/control.pbenum.dart`: regenerate Dart enum output.
- [ ] `apps/client/lib/gen/proto/iop/control.pbjson.dart`: regenerate Dart JSON metadata.
- [ ] `apps/client/lib/gen/proto/iop/control.pbserver.dart`: regenerate Dart server helpers.
- [ ] `apps/control-plane/internal/credentialstore/schema.go`: add durable projection generation state.
- [ ] `apps/control-plane/internal/credentialstore/principal.go`: bump generation transactionally on token lifecycle mutations.
- [ ] `apps/control-plane/internal/credentialstore/slot.go`: bump generation transactionally on slot lifecycle mutations.
- [ ] `apps/control-plane/internal/credentialstore/route.go`: bump generation transactionally on route lifecycle mutations.
- [ ] `apps/control-plane/internal/credentialstore/projection.go`: build bounded immutable active snapshots.
- [ ] `apps/control-plane/internal/credentialstore/projection_test.go`: test generation durability and filtered snapshots.
- [ ] `apps/control-plane/internal/wire/edge.go`: register projection parsers.
- [ ] `apps/control-plane/internal/wire/edge_server.go`: accept projection source option and push fresh snapshot to live Edge connections.
- [ ] `apps/control-plane/internal/wire/projection_sync.go`: implement generation-aware publish/retry without blocking hello.
- [ ] `apps/control-plane/internal/wire/edge_test.go`: assert parser round trips.
- [ ] `apps/control-plane/internal/wire/edge_server_test.go`: assert connect/push/ack and reconnect behavior.
- [ ] `apps/control-plane/cmd/control-plane/server.go`: supply store projection source to EdgeServer.

**Test Strategy:** Write `TestProjectionGenerationCommitsWithMutation`, `TestProjectionSnapshotOmitsDisabledRevokedExpiredRecords`, `TestEdgeServerPushesProjectionAfterHello`, and `TestProjectionRetryKeepsGeneration`. Use temp SQLite and loopback TCP; timestamps use injected clocks.

**Verification:** `make proto && make proto-dart && GOTMPDIR=/config/workspace/iop-s0/.cache/go-build GOCACHE=/config/workspace/iop-s0/.cache/go-cache go test -count=1 ./apps/control-plane/internal/credentialstore ./apps/control-plane/internal/wire` passes.

### [API-2] Apply a fenced bounded Edge cache

**Problem:** `apps/edge/internal/controlplane/connector.go:229-238` registers only status/command listeners, and OpenAI server state has no CP projection cache.

**Solution:** Define a `PrincipalProjectionSink` connector option. Register the apply listener before hello, validate sizes/digests/times, deep-copy and atomically swap only when generation is higher and snapshot is fresh. Once the first managed generation is accepted, expiry/revocation remains managed fail-closed and never falls back to static config.

Before (`apps/edge/internal/controlplane/connector.go:229-238`):

```go
toki.AddRequestListenerTyped(&cl.Communicator, func(req *iop.EdgeStatusRequest) (*iop.EdgeStatusResponse, error) {
    return c.buildStatusResponse(req), nil
})

toki.AddRequestListenerTyped(&cl.Communicator, func(req *iop.EdgeCommandRequest) (*iop.EdgeCommandResponse, error) {
    return c.handleEdgeCommand(ctx, req)
})
```

After:

```go
toki.AddRequestListenerTyped(&cl.Communicator, func(req *iop.PrincipalProjectionApplyRequest) (*iop.PrincipalProjectionApplyResponse, error) {
    return c.applyPrincipalProjection(req), nil
})
```

**Modified Files and Checklist:**

- [ ] `apps/edge/internal/controlplane/connector.go`: add sink option/listener and stable apply responses.
- [ ] `apps/edge/internal/controlplane/connector_test.go`: test listener ordering, invalid/out-of-order application, reconnect.
- [ ] `apps/edge/internal/openai/principal_projection.go`: implement limits, injected clock, atomic immutable snapshot, lookup, expiry state.
- [ ] `apps/edge/internal/openai/principal_projection_test.go`: cover fresh/stale/expired/revoked/out-of-order/concurrent reads.
- [ ] `apps/edge/internal/openai/server.go`: own the projection cache and expose sink/setter without changing `NewServer` signature.
- [ ] `apps/edge/internal/input/manager.go`: expose the OpenAI projection sink.
- [ ] `apps/edge/internal/input/manager_test.go`: assert the shared cache exists.
- [ ] `apps/edge/internal/bootstrap/runtime.go`: wire connector sink to `Input.OpenAI`.
- [ ] `apps/edge/internal/bootstrap/runtime_connector_test.go`: assert projection sink wiring.

**Test Strategy:** Write `TestProjectionAppliesOnlyHigherFreshGeneration`, `TestProjectionExpiryKeepsManagedFailClosed`, `TestProjectionBoundsRejectOversizedSnapshot`, and `TestProjectionConcurrentLookupAndApply`. Run `-race` with a deterministic fake clock.

**Verification:** `GOTMPDIR=/config/workspace/iop-s0/.cache/go-build GOCACHE=/config/workspace/iop-s0/.cache/go-cache go test -count=1 -race ./apps/edge/internal/controlplane ./apps/edge/internal/openai ./apps/edge/internal/input ./apps/edge/internal/bootstrap` passes.

### [API-3] Authenticate both HTTP surfaces from the same snapshot

**Problem:** `apps/edge/internal/openai/routes.go:30-43` always calls static `principalFromRequest(r, s.cfg)`. Static dual-header logic cannot observe Control Plane revocation or expiry.

**Solution:** Make middleware resolve from the cache when managed; use constant-time digest lookup, require equal bearer and `x-api-key` values when both exist, and return surface-correct 401 before invoking the handler. Keep current static behavior only in never-managed compatibility mode.

Before (`apps/edge/internal/openai/routes.go:30-33`):

```go
func (s *Server) withAuth(next http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        principal, ok := principalFromRequest(r, s.cfg)
        if !ok {
```

After:

```go
principal, authState := s.authenticatePrincipal(r)
if authState != principalAuthenticated {
    s.writeAuthenticationFailure(w, r)
    return
}
```

**Modified Files and Checklist:**

- [ ] `apps/edge/internal/openai/principal.go`: share header normalization and digest matching between static and projected sources.
- [ ] `apps/edge/internal/openai/routes.go`: fail before handler invocation with OpenAI/Anthropic 401 envelopes.
- [ ] `apps/edge/internal/openai/identity_metering_test.go`: retain static compatibility and projected principal metadata assertions.
- [ ] `apps/edge/internal/openai/anthropic_surface_test.go`: add bearer/x-api-key same/mismatch/unregistered/revoked/expired cases and zero-dispatch assertions.
- [ ] `agent-contract/inner/control-plane-edge-wire.md`: document projection fields, generation, expiry, size and ack rules.
- [ ] `agent-contract/outer/openai-compatible-api.md`: document CP-managed bearer behavior.
- [ ] `agent-contract/outer/anthropic-compatible-api.md`: document bearer/x-api-key equivalence and 401 precedence.

**Test Strategy:** Add `TestProjectedOpenAIAuthRejectsBeforeHandler`, `TestAnthropicProjectedDualHeadersSameToken`, `TestAnthropicProjectedDualHeadersMismatch`, and `TestRevokedOrExpiredProjectionRejectsBeforeDispatch`. Use a counting handler/fake service to assert zero calls.

**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 'Principal|Anthropic.*Auth|Projection'` passes.

## Modified Files Summary

| File | Items |
|------|-------|
| `proto/iop/control.proto` | API-1 |
| `proto/gen/iop/control.pb.go` | API-1 |
| `apps/client/lib/gen/proto/iop/control.pb.dart` | API-1 |
| `apps/client/lib/gen/proto/iop/control.pbenum.dart` | API-1 |
| `apps/client/lib/gen/proto/iop/control.pbjson.dart` | API-1 |
| `apps/client/lib/gen/proto/iop/control.pbserver.dart` | API-1 |
| `apps/control-plane/internal/credentialstore/schema.go` | API-1 |
| `apps/control-plane/internal/credentialstore/principal.go` | API-1 |
| `apps/control-plane/internal/credentialstore/slot.go` | API-1 |
| `apps/control-plane/internal/credentialstore/route.go` | API-1 |
| `apps/control-plane/internal/credentialstore/projection.go` | API-1 |
| `apps/control-plane/internal/credentialstore/projection_test.go` | API-1 |
| `apps/control-plane/internal/wire/edge.go` | API-1 |
| `apps/control-plane/internal/wire/edge_server.go` | API-1 |
| `apps/control-plane/internal/wire/projection_sync.go` | API-1 |
| `apps/control-plane/internal/wire/edge_test.go` | API-1 |
| `apps/control-plane/internal/wire/edge_server_test.go` | API-1 |
| `apps/control-plane/cmd/control-plane/server.go` | API-1 |
| `apps/edge/internal/controlplane/connector.go` | API-2 |
| `apps/edge/internal/controlplane/connector_test.go` | API-2 |
| `apps/edge/internal/openai/principal_projection.go` | API-2 |
| `apps/edge/internal/openai/principal_projection_test.go` | API-2 |
| `apps/edge/internal/openai/server.go` | API-2 |
| `apps/edge/internal/input/manager.go` | API-2 |
| `apps/edge/internal/input/manager_test.go` | API-2 |
| `apps/edge/internal/bootstrap/runtime.go` | API-2 |
| `apps/edge/internal/bootstrap/runtime_connector_test.go` | API-2 |
| `apps/edge/internal/openai/principal.go` | API-3 |
| `apps/edge/internal/openai/routes.go` | API-3 |
| `apps/edge/internal/openai/identity_metering_test.go` | API-3 |
| `apps/edge/internal/openai/anthropic_surface_test.go` | API-3 |
| `agent-contract/inner/control-plane-edge-wire.md` | API-3 |
| `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/03+01,02_projection_auth/CODE_REVIEW-cloud-G10.md` | API-1, API-2, API-3 |

## Final Verification

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

1. `test -f agent-task/m-principal-provider-credential-slot-routing/01_principal_store/complete.log && test -f agent-task/m-principal-provider-credential-slot-routing/02+01_credential_catalog/complete.log` — both predecessors complete.
2. `command -v go && go version && command -v protoc && protoc --version && command -v protoc-gen-go && command -v protoc-gen-dart && command -v flutter && flutter --version` — required generators/toolchains resolve.
3. `mkdir -p .cache/go-build .cache/go-cache` — executable Go temp/cache paths exist.
4. `make proto && make proto-dart` — generated Go/Dart artifacts are current.
5. `GOTMPDIR=/config/workspace/iop-s0/.cache/go-build GOCACHE=/config/workspace/iop-s0/.cache/go-cache go vet ./apps/control-plane/... ./apps/edge/internal/controlplane ./apps/edge/internal/openai ./apps/edge/internal/input ./apps/edge/internal/bootstrap` — no vet findings.
6. `GOTMPDIR=/config/workspace/iop-s0/.cache/go-build GOCACHE=/config/workspace/iop-s0/.cache/go-cache go test -count=1 -race ./apps/control-plane/internal/credentialstore ./apps/control-plane/internal/wire ./apps/edge/internal/controlplane ./apps/edge/internal/openai ./apps/edge/internal/input ./apps/edge/internal/bootstrap` — projection/auth lifecycle and races pass.
7. `GOTMPDIR=/config/workspace/iop-s0/.cache/go-build GOCACHE=/config/workspace/iop-s0/.cache/go-cache make test-control-plane-edge-wire` — CP↔Edge wire compatibility passes.
8. `make client-test` — generated Dart messages/imports remain valid.
9. `cd apps/client && flutter analyze` — client analysis has no findings.
10. `git diff --check` — no whitespace errors.

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