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

# Auth Projection and Surface-Auth Foundation

## For the Implementing Agent

Filling implementation-owned sections of `CODE_REVIEW-cloud-G09.md` is mandatory. Run every verification command, paste actual output, keep 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

The Edge needs a bounded immutable principal projection cache and common OpenAI/Anthropic authentication logic. The current CP–Edge TCP connection has no authenticated peer or transport integrity, so wiring an authorization projection over it would allow an untrusted source to replace token verifiers and routes; this packet deliberately builds but does not activate that transport path.

## Archive Evidence Snapshot

- Prior pair: `agent-task/m-principal-provider-credential-slot-routing/03+01,02_projection_auth/plan_cloud_G10_0.log`, `agent-task/m-principal-provider-credential-slot-routing/03+01,02_projection_auth/code_review_cloud_G10_0.log`.
- Verdict/evidence: implementation had not started; no verdict or verification output.
- Replan reason: the previous plan pushed authorization state over the current unauthenticated plain TCP connector and incorrectly claimed S02/S03 completion before S10 `secure-transport`.
- Roadmap carryover: this is foundation-only. PASS must not check `auth-projection` or `surface-auth`; authenticated CP–Edge activation remains required.

## 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/internal/wire/edge.go`
- `apps/control-plane/internal/wire/edge_server.go`
- `apps/edge/internal/controlplane/connector.go`
- `apps/edge/internal/controlplane/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/anthropic_types.go`
- `apps/edge/internal/openai/routes.go`
- `apps/edge/internal/openai/identity_metering_test.go`
- `apps/edge/internal/openai/anthropic_surface_test.go`
- `apps/edge/internal/openai/openai_auth_routes_models_test.go`
- `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` (`승인`, 잠금 해제).
- Foundation maps to S02 / `auth-projection` and S03 / `surface-auth`, Evidence Map S01-S03, but intentionally does not satisfy their production synchronization/authorization evidence.
- Checklist covers durable generation, bounded atomic cache, fresh/stale/revoked/out-of-order fixtures, and dual-header zero-handler-call behavior. Final verification also proves no current Connector/EdgeServer call site activates projection messages.

### Verification Context

- handoff 없음. SDD secure-delivery clause explicitly states current plain TCP/WS cannot carry credential-bearing operations; current CP–Edge contract has hello but no peer authentication.
- Go 1.26.2, protoc 29.3, Go/Dart generators present, Flutter 3.41.5. Workspace ignored Go temp/cache paths are required because `/tmp` is noexec.
- Preconditions 01 and 02 are currently incomplete; runtime resolves their archived completions before implementation.
- No external provider or TLS runner is required because this packet is dormant foundation. Confidence: high.
- The current dirty baseline already contains user-owned authentication fixes in `principal.go`, `anthropic_types.go`, `anthropic_surface_test.go`, and `openai_auth_routes_models_test.go`; preserve and integrate those semantics instead of replacing or reverting them.

### Test Coverage Gaps

- Static Edge auth tests cover header normalization but not generation/expiry/revocation or concurrent apply/read.
- No durable projection snapshot builder or bounded cache exists.
- No test currently proves projection activation is absent on the unauthenticated connector.

### Symbol References

- Rename/remove 없음. `openai.NewServer` signature remains unchanged; many tests rely on it.
- `input.NewManager` is called by `bootstrap.NewRuntime`; preserve its signature and expose the shared cache on Manager.
- No `NewConnector`/`NewEdgeServer` call site is changed in this packet.

### Split Judgment

- Durable snapshot format and Edge cache/header semantics form a stable dormant contract with deterministic tests.
- Predecessors 01 and 02: active/archived completion missing, ambiguity 없음.
- Authenticated transport activation is excluded and must be planned with S10; therefore no Roadmap Targets section is present.
- Packet 04 may consume only the dormant cache contract and likewise cannot claim public rollout.

### Scope Rationale

- Do not register projection listeners, push from `EdgeServer`, or wire `bootstrap.Runtime.ControlPlane` to the cache on the current plain transport.
- Do not carry ciphertext/plaintext, add TLS, or implement credential leases.
- Preserve the existing user changes that scope Anthropic `x-api-key`/version handling away from ordinary OpenAI routes; managed-mode additions must extend that baseline without reverting it.
- Contracts must mark messages/cache as reserved/gated foundation, not active production behavior.

### Final Routing

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

## Dependencies and Execution Order

1. `01_principal_store` and `02+01_credential_catalog` must complete; runtime accepts their archived `complete.log` files.
2. Add durable snapshot generation and additive message schema, then the shared Edge cache, then dormant HTTP managed-mode logic.
3. Do not add dependencies absent from `03+01,02_projection_auth`.

## Implementation Checklist

- [ ] Build durable, secret-free projection snapshots with monotonic generation and additive generated message types, covering the S02 data/state fixtures without transport activation.
- [ ] Build a bounded immutable Edge cache that applies only higher fresh generations and remains managed fail-closed after expiry/revocation under race tests.
- [ ] Make OpenAI bearer and Anthropic bearer/x-api-key authentication consume the shared cache only when a verified snapshot is installed, with S03 mismatch and zero-handler-call fixtures.
- [ ] Document and test that the current unauthenticated CP–Edge connector cannot install or receive the projection; leave S02/S03 Roadmap completion for secure-transport rollout.
- [ ] Run fresh generation, race, Edge/client regression, dormant-activation search, and diff verification.
- [ ] Fill implementation-owned sections in CODE_REVIEW-*-G??.md with actual implementation notes and verification output.

### [API-1] Define durable secret-free projection snapshots

**Problem:** `proto/iop/control.proto` and the Control Plane store have no generation/expiry token+route snapshot contract.

**Solution:** Add additive projection value messages, but no live request listener. Add a DB singleton generation bumped transactionally by token/slot/route mutations and a bounded snapshot builder containing active digests and route metadata only.

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 schema boundary:

```protobuf
message PrincipalProjection {
  uint64 generation = 1;
  int64 issued_at_unix_nano = 2;
  int64 expires_at_unix_nano = 3;
  repeated ProjectedPrincipalToken tokens = 4;
  repeated ProjectedPrincipalRoute routes = 5;
}
```

**Modified Files and Checklist:**

- [ ] `proto/iop/control.proto`: add value/apply types additively; do not register an active wire operation.
- [ ] `proto/gen/iop/control.pb.go`: regenerate Go.
- [ ] `apps/client/lib/gen/proto/iop/control.pb.dart`: regenerate Dart.
- [ ] `apps/client/lib/gen/proto/iop/control.pbenum.dart`: regenerate Dart enums.
- [ ] `apps/client/lib/gen/proto/iop/control.pbjson.dart`: regenerate Dart metadata.
- [ ] `apps/client/lib/gen/proto/iop/control.pbserver.dart`: regenerate Dart helpers.
- [ ] `apps/control-plane/internal/credentialstore/schema.go`: add durable projection generation.
- [ ] `apps/control-plane/internal/credentialstore/principal.go`: bump generation with token mutations.
- [ ] `apps/control-plane/internal/credentialstore/slot.go`: bump generation with slot mutations.
- [ ] `apps/control-plane/internal/credentialstore/route.go`: bump generation with route mutations.
- [ ] `apps/control-plane/internal/credentialstore/projection.go`: build bounded immutable snapshots.
- [ ] `apps/control-plane/internal/credentialstore/projection_test.go`: verify durability/filtering/bounds.

**Test Strategy:** Write `TestProjectionGenerationCommitsWithMutation`, `TestProjectionGenerationPersistsAcrossReopen`, and `TestProjectionSnapshotOmitsInactiveRecords` with temp SQLite and 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` passes.

### [API-2] Add a shared bounded Edge cache

**Problem:** `apps/edge/internal/openai/server.go:50-68` owns mutable config/catalog state but has no fenced principal projection snapshot.

**Solution:** Add `apps/edge/internal/authprojection` with injected clock, hard entry/byte limits, deep-copy validation, atomic snapshot swap, digest lookup, and explicit unmanaged/fresh/expired states. `input.Manager` constructs one cache and gives it to OpenAI; it remains unmanaged because Runtime/Connector receives no apply handle.

Required imports for the new cache boundary:

```go
import (
    "crypto/subtle"
    "errors"
    "sync/atomic"
    "time"

    iop "iop/proto/gen/iop"
)
```

Before (`apps/edge/internal/input/manager.go:20-29`):

```go
func NewManager(cfg config.EdgeConfig, svc *edgeservice.Service, logger *zap.Logger) *Manager {
    openaiServer := edgeopenai.NewServer(cfg.OpenAI, svc, logger.Named("openai"))
    openaiServer.SetEdgeID(cfg.Edge.ID)
```

After:

```go
projection := authprojection.NewCache(authprojection.DefaultLimits(), time.Now)
openaiServer := edgeopenai.NewServer(cfg.OpenAI, svc, logger.Named("openai"))
openaiServer.SetPrincipalProjection(projection)
```

**Modified Files and Checklist:**

- [ ] `apps/edge/internal/authprojection/cache.go`: implement validated atomic cache and states.
- [ ] `apps/edge/internal/authprojection/cache_test.go`: cover fresh/stale/expired/revoked/out-of-order/bounds/concurrency.
- [ ] `apps/edge/internal/openai/server.go`: read the cache through a narrow interface without changing constructor.
- [ ] `apps/edge/internal/input/manager.go`: create and expose the dormant shared cache.
- [ ] `apps/edge/internal/input/manager_test.go`: prove shared instance and unmanaged default.

**Test Strategy:** Write `TestCacheAppliesOnlyHigherFreshGeneration`, `TestCacheExpiryKeepsManagedFailClosed`, `TestCacheRejectsOversizedSnapshot`, and `TestCacheConcurrentLookupAndApply`; run with `-race`.

**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/authprojection ./apps/edge/internal/input` passes.

### [API-3] Add dormant managed-mode surface authentication

**Problem:** `apps/edge/internal/openai/routes.go:30-43` reads only static config, so no shared cache behavior can be tested; directly wiring the cache to current connector would be unsafe.

**Solution:** When the cache is explicitly in managed state, authenticate constant-time token digests and surface-specific headers from it; unmanaged state preserves existing config behavior. Expired/revoked managed state returns 401 before handler. Do not add any production cache writer.

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)
```

After:

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

**Modified Files and Checklist:**

- [ ] `apps/edge/internal/openai/principal.go`: share header normalization and projected digest lookup.
- [ ] `apps/edge/internal/openai/routes.go`: enforce managed error precedence before handlers.
- [ ] `apps/edge/internal/openai/identity_metering_test.go`: cover managed principal metadata and static compatibility.
- [ ] `apps/edge/internal/openai/anthropic_surface_test.go`: cover same/mismatch/unregistered/revoked/expired dual headers and zero dispatch.
- [ ] `agent-contract/inner/control-plane-edge-wire.md`: mark projection types reserved until authenticated transport, with no current listener.
- [ ] `agent-contract/outer/openai-compatible-api.md`: document dormant managed-mode gate and unchanged legacy production mode.
- [ ] `agent-contract/outer/anthropic-compatible-api.md`: document the same gated header semantics.

**Test Strategy:** Write `TestManagedOpenAIAuthRejectsBeforeHandler`, `TestManagedAnthropicDualHeadersSameToken`, `TestManagedAnthropicDualHeadersMismatch`, and `TestManagedProjectionExpiryRejectsBeforeDispatch` using direct cache fixture installation inside tests.

**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|Principal|Anthropic.*Auth'` 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/edge/internal/authprojection/cache.go` | API-2 |
| `apps/edge/internal/authprojection/cache_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/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-G09.md` | API-1, API-2, API-3 |

## Final Verification

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

1. `test "$(find agent-task/archive -type f -path '*/m-principal-provider-credential-slot-routing/01_principal_store/complete.log' -print | wc -l)" -eq 1 && test "$(find agent-task/archive -type f -path '*/m-principal-provider-credential-slot-routing/02+01_credential_catalog/complete.log' -print | wc -l)" -eq 1` — both archived predecessors exist exactly once.
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` — toolchains resolve.
3. `mkdir -p .cache/go-build .cache/go-cache` — executable Go paths exist.
4. `make proto && make proto-dart` — generated outputs are current.
5. `GOTMPDIR=/config/workspace/iop-s0/.cache/go-build GOCACHE=/config/workspace/iop-s0/.cache/go-cache go vet ./apps/control-plane/internal/credentialstore ./apps/edge/internal/authprojection ./apps/edge/internal/input ./apps/edge/internal/openai` — 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/edge/internal/authprojection ./apps/edge/internal/input ./apps/edge/internal/openai` — foundation lifecycle and race tests pass.
7. `make client-test` — generated Dart compatibility passes.
8. `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` — current unauthenticated runtime has no projection activation.
9. `git diff --check` — no whitespace errors.

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