<!-- task=m-principal-provider-credential-slot-routing/08+03,04,07_secure_delivery plan=4 tag=REVIEW_REVIEW_REVIEW_API milestone-task=secure-transport,credential-lease,adapter-injection,revocation -->

# Secure Enrollment Identity and Projection Renewal Follow-up

## For the Implementing Agent

Filling the implementation-owned sections of `CODE_REVIEW-cloud-G10.md` is mandatory. Implement the two items below, run every verification command exactly, paste actual notes and stdout/stderr into the review artifact, keep both active files in place, and report ready for review. Final verdicts, log archives, `complete.log`, task moves, user-review classification, and control-plane stop files belong only to the code-review workflow. If blocked, record the exact blocker, attempted commands/output, and resume condition only in implementation-owned evidence; do not ask the user or classify the next state.

## Background

The secure-delivery implementation passes its current suites, but the review found two production gaps. A same-role CA-issued certificate can claim another logical Edge or Node identity because the authenticated URI SAN name is not bound to hello/register state, and a managed Edge cannot renew or reconnect with an unchanged durable projection generation. This follow-up closes those enrollment and freshness invariants without changing lease cryptography, adapter injection, public protocol schemas, or migration ownership.

## Archive Evidence Snapshot

- `plan_cloud_G10_3.log` and `code_review_cloud_G10_3.log`: current-pair predicted archives; verdict `FAIL` with 2 Required, 0 Suggested, and 0 Nit findings. Fresh reviewer tests passed, but production inspection proved missing same-role enrollment-name binding and a five-minute/no-mutation plus same-generation reconnect projection outage.
- `plan_cloud_G10_2.log` and `code_review_cloud_G10_2.log`: prior recovery loop that found all secure-delivery production paths absent; the next implementation added TLS 1.3, signed/sealed leases, candidate-bound injection, revocation fences, and full-cycle evidence.
- `agent-task/archive/2026/08/m-principal-provider-credential-slot-routing/03+01,02_projection_auth/complete.log`: secret-free projection/cache and managed ingress foundation passed; this follow-up preserves its bounded atomic cache and fail-closed expired state.
- `agent-task/archive/2026/08/m-principal-provider-credential-slot-routing/07+01,02,05_secret_material/complete.log`: secret-at-rest and fail-closed startup prerequisite passed.

## Analysis

### Files Read

- `packages/go/auth/auth.go`
- `packages/go/auth/auth_test.go`
- `packages/go/config/tls_types.go`
- `configs/control-plane.yaml`
- `configs/edge.yaml`
- `configs/node.yaml`
- `apps/control-plane/cmd/control-plane/main.go`
- `apps/control-plane/cmd/control-plane/server.go`
- `apps/control-plane/internal/wire/edge_server.go`
- `apps/control-plane/internal/wire/edge_server_test.go`
- `apps/edge/internal/transport/server.go`
- `apps/edge/internal/transport/connection_handlers.go`
- `apps/edge/internal/transport/integration_test.go`
- `apps/edge/internal/controlplane/connector.go`
- `apps/edge/internal/controlplane/connector_test.go`
- `apps/edge/internal/authprojection/cache.go`
- `apps/edge/internal/authprojection/cache_test.go`
- `agent-roadmap/phase/operational-observability-provider-management/milestones/principal-provider-credential-slot-routing.md`
- `agent-roadmap/sdd/operational-observability-provider-management/principal-provider-credential-slot-routing/SDD.md`
- `agent-task/m-principal-provider-credential-slot-routing/08+03,04,07_secure_delivery/PLAN-cloud-G10.md`
- `agent-task/m-principal-provider-credential-slot-routing/08+03,04,07_secure_delivery/CODE_REVIEW-cloud-G10.md`

### SDD Criteria

- SDD: `agent-roadmap/sdd/operational-observability-provider-management/principal-provider-credential-slot-routing/SDD.md`, status `[승인됨]`, lock released.
- Milestone tasks remain exactly `secure-transport,credential-lease,adapter-injection,revocation` because enrollment authentication and projection freshness gate the complete S10-S13 dispatch path.
- Acceptance S10 requires authenticated confidential operations and enrollment-bound system peer identity. S02 requires fresh/stale/expired projection behavior and successful refresh before expiry; S11-S13 require the refreshed generation/revision fence to remain authoritative through lease acquisition, adapter admission, and revocation.
- Evidence Map S09-S13 requires a TLS peer matrix, lease scope/tamper evidence, exact upstream auth/redaction, and revocation ordering. The checklist therefore adds same-role wrong-name TLS cases and managed projection reconnect/renewal cases, then reruns the existing lease/full-cycle/race evidence.

### Verification Context

- No external handoff was supplied. Repository-native evidence came from the active implementation/review artifacts, approved SDD, milestone, source and test files listed above, and fresh reviewer execution.
- Fresh command passed with exit 0: `env GOCACHE=/config/tmp/gocache-secure-delivery GOTMPDIR=/config/workspace/.gotmp-iop-secure-delivery TMPDIR=/config/workspace/.gotmp-iop-secure-delivery go test -count=1 ./packages/go/auth ./packages/go/config ./apps/control-plane/internal/wire ./apps/edge/internal/controlplane ./apps/edge/internal/authprojection ./apps/edge/internal/transport`.
- The pass is not closure evidence for the new defects: `auth_test.go` covers wrong role, wrong CA, missing cert, and wrong server DNS name, while connector reconnect tests run without `WithPrincipalProjection`.
- The host requires the recorded `GOCACHE`, `GOTMPDIR`, and `TMPDIR` overrides because inherited cache entries are not uniformly accessible and global temporary mounts are non-executable. Fresh execution is required (`-count=1`); cached test output is not accepted.
- Required verification stays in the current checkout. The three-process target builds temporary binaries and identities under its repository-local executable fixture workspace; it needs no user-owned runner, device, credential, or external authorization.
- Confidence: high. The identity gap follows directly from accepting any role-prefix SAN and then trusting request IDs; the refresh gap follows directly from a five-minute snapshot, mutation-only broadcast, and `candidate.generation <= installed.generation` rejection.

### Test Coverage Gaps

- CP-Edge enrollment: no test presents a valid CA-issued `edge/edge-a` certificate and attempts `EdgeHello(edge-b)`; add a TLS regression that requires rejection before registry mutation.
- Edge-Node enrollment: no test presents a valid CA-issued `node/node-a` certificate with `node-b`'s valid registration token; add a TLS regression that requires rejection before recipient-key or registry admission.
- SPIFFE parsing: no test rejects missing or multiple ambiguous IOP workload URI SAN identities; extend auth tests for exact unique parsing and configured role/name verification.
- Projection renewal: same-generation candidates are uniformly rejected; add exact-content later-expiry renewal, idempotent duplicate/older renewal, changed-content rejection, lower-generation rejection, expiry recovery, and concurrent apply/read tests.
- Managed reconnect: existing reconnect tests omit a projection cache; add a fake Control Plane that closes the first accepted connection and returns the same generation with a renewed expiry on reconnect, then require `StateConnected` and a fresh cache.
- Periodic refresh: no test proves the Control Plane pushes before expiry or stops its loop; add a short deterministic interval test and verify one failed client does not prevent refresh attempts to other active clients.

### Symbol References

- `EdgeServer.SetCredentialPlane` will gain the explicit projection refresh interval; its only production call site is `apps/control-plane/cmd/control-plane/server.go` and all test call sites are in `apps/control-plane/internal/wire/edge_server_test.go`.
- No symbol is renamed or removed. New peer-identity helpers must be used by both `installPeerIdentityVerifier` and the CP-Edge/Edge-Node enrollment handlers so parsing rules cannot diverge.

### Split Judgment

Keep one plan. Authenticated enrollment identity and a continuously fresh authenticated projection are the two gates of one managed secure-activation invariant: partial completion still permits either peer impersonation or permanent loss of lease-bearing dispatch. The production changes are compact, share the CP-Edge lifecycle, and close independently testable subcases inside one final S10-S13 regression packet.

### Scope Rationale

- Do not change lease canonicalization, X25519/Ed25519 material, Node replay fencing, adapter injection, public protobuf schemas, or public OpenAI/Anthropic response shapes; the review found no defect there.
- Do not synchronize contracts/specs or tracked README text; sibling task `10+03,04,08_contract_operations` owns contract/spec operations.
- Do not change managed/legacy migration or attribution semantics; sibling task `09+08_migration_attribution` owns those boundaries.
- Preserve plaintext constructors for credential-plane-disabled compatibility. Identity-to-enrollment binding is mandatory only when a TLS server connection is present; managed configuration already requires TLS.

### Final Routing

- Evaluation mode: `isolated-reassessment`.
- Finalizer: `finalize-task-policy.sh`, mode `pair`; both target closures are true (`scope`, `context`, `verification`, `evidence`, `ownership`, `decision`), with no capability gap.
- Build scores: scope coupling 2, state/concurrency 2, blast/irreversibility 2, evidence/diagnosis 2, verification complexity 2; grade G10, base/final route `grade-boundary`, lane `cloud`, filename `PLAN-cloud-G10.md`.
- Review scores: 2/2/2/2/2; grade G10, route `official-review`, lane `cloud`, filename `CODE_REVIEW-cloud-G10.md`.
- `large_indivisible_context=false`; positive loop-risk signatures are `temporal_state`, `concurrent_consistency`, and `boundary_contract` (`loop_risk_count=3`).
- Recovery signals: `review_rework_count=3`, `evidence_integrity_failure=true`; recovery boundary is matched but does not replace the G10 `grade-boundary` basis.

## Implementation Checklist

- [ ] Bind one unambiguous authenticated SPIFFE workload name to CP-Edge hello and Edge-Node token-resolved registration identity before any registry, lease, or recipient-key state mutation, and pass same-role wrong-name TLS regressions.
- [ ] Renew immutable same-generation projections safely before expiry, keep lower/changed same-generation snapshots fenced, retain periodic refresh across active Edges, and pass managed reconnect/expiry/concurrency regressions.
- [ ] Run the focused, race, full secure-delivery, vet, diff, and secret-structure verification commands with fresh output.
- [ ] Fill implementation-owned sections in CODE_REVIEW-*-G??.md with actual implementation notes and verification output.

### [REVIEW_REVIEW_REVIEW_API-1] Bind authenticated workload identity to enrollment

**Problem:** `packages/go/auth/auth.go:31-34` explicitly permits any non-empty workload name when `peerName` is empty, and `packages/go/auth/auth.go:92-116` returns only a verifier result rather than the authenticated identity. `apps/control-plane/internal/wire/edge_server.go:109-124` registers the request's `edge_id` without comparing the TLS SAN, while `apps/edge/internal/transport/connection_handlers.go:70-110` accepts the token-resolved Node and recipient key without comparing the Node certificate name.

**Before (`packages/go/auth/auth.go:92-116`):**

```go
func installPeerIdentityVerifier(cfg *tls.Config, role, name string) {
    // ...
    for _, uri := range state.PeerCertificates[0].URIs {
        // ...
        if name == "" || peerName == name {
            return nil
        }
    }
    return fmt.Errorf("peer workload identity mismatch")
}
```

**Solution:** Add one shared parser that accepts exactly one canonical `spiffe://iop/<role>/<name>` URI SAN, rejects missing, malformed, slash-containing, or ambiguous multiple IOP identities, and returns a typed role/name. Add a TLS-connection helper that reads the completed peer certificate. Reuse the parser in `installPeerIdentityVerifier`; when an exact configured name exists it must still match. Capture the accepted TLS `net.Conn` for each proto-socket client and compare `edge/<name>` to `EdgeHelloRequest.edge_id`, and `node/<name>` to the token-resolved `NodeRecord.ID`, before calling registry methods, installing active clients, accepting a recipient key, or enabling lease requests. Plain constructors must retain current behavior.

**Modified Files and Checklist:**

- [ ] `packages/go/auth/auth.go` — add unique workload identity parsing/connection extraction and share it with TLS verification.
- [ ] `packages/go/auth/auth_test.go` — cover exact identity, same-role different name, malformed/missing identity, and ambiguous multiple identity SANs.
- [ ] `apps/control-plane/internal/wire/edge_server.go` — bind the authenticated Edge name before registry/active-client/lease state.
- [ ] `apps/control-plane/internal/wire/edge_server_test.go` — add valid-name and same-role wrong-name mTLS hello tests and assert no rejected identity reaches the registry.
- [ ] `apps/edge/internal/transport/server.go` — retain each TLS connection's authenticated identity source for its proto-socket client and clean it up on disconnect.
- [ ] `apps/edge/internal/transport/connection_handlers.go` — reject certificate/record mismatch before config build, recipient-key capture, listener ownership, or registry admission.
- [ ] `apps/edge/internal/transport/integration_test.go` — add valid-name and same-role wrong-name mTLS token registration tests, including an unchanged registry assertion.

**Test Strategy:** Write regressions. `TestEdgeServerRejectsSameRoleWrongEnrollmentName` presents an authenticated `edge/edge-a` certificate with `edge_id=edge-b`; `TestEdgeNodeServerRejectsSameRoleWrongEnrollmentName` presents `node/node-a` with `node-b`'s valid token. Both must reject before state mutation, while exact-name cases pass and plaintext legacy tests remain unchanged. Auth unit cases pin unique canonical parsing.

**Verification:**

```bash
env GOCACHE=/config/tmp/gocache-secure-delivery GOTMPDIR=/config/workspace/.gotmp-iop-secure-delivery TMPDIR=/config/workspace/.gotmp-iop-secure-delivery go test -count=1 ./packages/go/auth ./apps/control-plane/internal/wire ./apps/edge/internal/transport
```

Expected: exit 0; exact-name TLS enrollment succeeds, same-role wrong-name and ambiguous/missing identity cases fail before registration, and plaintext compatibility tests pass.

### [REVIEW_REVIEW_REVIEW_API-2] Renew and reconnect immutable managed projections

**Problem:** `apps/control-plane/cmd/control-plane/server.go:95-99` builds a five-minute snapshot, and `apps/control-plane/cmd/control-plane/server.go:109-112` broadcasts only after credential mutations. `apps/edge/internal/authprojection/cache.go:316-320` rejects equal generations, so a no-mutation deployment expires permanently and `apps/edge/internal/controlplane/connector.go:333-343` closes every reconnect whose hello carries the unchanged durable generation.

**Before (`apps/edge/internal/authprojection/cache.go:316-320`):**

```go
installed := c.current.Load()
if installed != nil && candidate.generation <= installed.generation {
    return fmt.Errorf("%w: candidate=%d installed=%d", ErrGenerationNotHigher, candidate.generation, installed.generation)
}
```

**Solution:** Keep strict lower-generation rejection. For an equal generation, compare normalized immutable token and route content and reject any difference. Treat an identical duplicate/older-expiry snapshot as successful without swapping, and atomically swap only an identical snapshot whose issued/expiry window advances; this renews freshness without authorizing same-generation content changes. Extend `SetCredentialPlane` with an explicit refresh interval of half the five-minute TTL, start one cancellable refresh loop with the Edge server, stop it with context/server shutdown, and make `BroadcastProjection` attempt all active authenticated Edges while aggregating failures. The normal higher-generation mutation/revocation path remains unchanged and wins over stale concurrent refreshes.

**Modified Files and Checklist:**

- [ ] `apps/edge/internal/authprojection/cache.go` — add exact immutable-content comparison and safe equal-generation renewal/idempotence under CAS.
- [ ] `apps/edge/internal/authprojection/cache_test.go` — cover renewal, duplicate/older expiry, changed equal generation, lower generation, post-expiry recovery, and concurrent readers/writers.
- [ ] `apps/control-plane/internal/wire/edge_server.go` — own the bounded refresh ticker lifecycle and broadcast to every active Edge with aggregated errors.
- [ ] `apps/control-plane/internal/wire/edge_server_test.go` — prove periodic pre-expiry push, shutdown cancellation, all-client attempts, and coexistence with higher-generation mutation pushes.
- [ ] `apps/control-plane/cmd/control-plane/server.go` — pass half-TTL refresh cadence into managed Edge-server composition.
- [ ] `apps/edge/internal/controlplane/connector_test.go` — add a projection-enabled disconnect/reconnect test that accepts identical same-generation renewed hello state and stays fail-closed for changed content.

**Test Strategy:** Write deterministic clock and bounded-duration regressions. `TestCacheRenewsOnlyIdenticalGeneration`, `TestConnectorReconnectsWithRenewedSameGenerationProjection`, and `TestEdgeServerPeriodicallyRefreshesEveryActiveEdge` must prove renewal before expiry, reconnect without a mutation, rejection of changed same-generation authorization, idempotence during broadcast races, and ticker cancellation. Run cache and wire/connector races.

**Verification:**

```bash
env GOCACHE=/config/tmp/gocache-secure-delivery GOTMPDIR=/config/workspace/.gotmp-iop-secure-delivery TMPDIR=/config/workspace/.gotmp-iop-secure-delivery go test -count=1 ./apps/edge/internal/authprojection ./apps/edge/internal/controlplane ./apps/control-plane/internal/wire ./apps/control-plane/cmd/control-plane
env GOCACHE=/config/tmp/gocache-secure-delivery GOTMPDIR=/config/workspace/.gotmp-iop-secure-delivery TMPDIR=/config/workspace/.gotmp-iop-secure-delivery go test -count=1 -race ./apps/edge/internal/authprojection ./apps/edge/internal/controlplane ./apps/control-plane/internal/wire
```

Expected: both commands exit 0; unchanged authorization renews/reconnects, changed or lower generations remain fenced, periodic pushes reach all live Edges, and race detection reports no issue.

## Modified Files Summary

| File | Item |
|------|------|
| `packages/go/auth/auth.go` | REVIEW_REVIEW_REVIEW_API-1 |
| `packages/go/auth/auth_test.go` | REVIEW_REVIEW_REVIEW_API-1 |
| `apps/control-plane/internal/wire/edge_server.go` | REVIEW_REVIEW_REVIEW_API-1, REVIEW_REVIEW_REVIEW_API-2 |
| `apps/control-plane/internal/wire/edge_server_test.go` | REVIEW_REVIEW_REVIEW_API-1, REVIEW_REVIEW_REVIEW_API-2 |
| `apps/edge/internal/transport/server.go` | REVIEW_REVIEW_REVIEW_API-1 |
| `apps/edge/internal/transport/connection_handlers.go` | REVIEW_REVIEW_REVIEW_API-1 |
| `apps/edge/internal/transport/integration_test.go` | REVIEW_REVIEW_REVIEW_API-1 |
| `apps/edge/internal/authprojection/cache.go` | REVIEW_REVIEW_REVIEW_API-2 |
| `apps/edge/internal/authprojection/cache_test.go` | REVIEW_REVIEW_REVIEW_API-2 |
| `apps/control-plane/cmd/control-plane/server.go` | REVIEW_REVIEW_REVIEW_API-2 |
| `apps/edge/internal/controlplane/connector_test.go` | REVIEW_REVIEW_REVIEW_API-2 |
| `agent-task/m-principal-provider-credential-slot-routing/08+03,04,07_secure_delivery/CODE_REVIEW-cloud-G10.md` | REVIEW_REVIEW_REVIEW_API-1, REVIEW_REVIEW_REVIEW_API-2 |

## Final Verification

Run fresh tests; cached `go test` output is not acceptable.

```bash
command -v go && go version && go env GOROOT
test -f agent-task/archive/2026/08/m-principal-provider-credential-slot-routing/07+01,02,05_secret_material/complete.log
env GOCACHE=/config/tmp/gocache-secure-delivery GOTMPDIR=/config/workspace/.gotmp-iop-secure-delivery TMPDIR=/config/workspace/.gotmp-iop-secure-delivery go test -count=1 ./packages/go/auth ./packages/go/config ./packages/go/credentiallease
env GOCACHE=/config/tmp/gocache-secure-delivery GOTMPDIR=/config/workspace/.gotmp-iop-secure-delivery TMPDIR=/config/workspace/.gotmp-iop-secure-delivery go test -count=1 ./apps/control-plane/... ./apps/edge/... ./apps/node/...
env GOCACHE=/config/tmp/gocache-secure-delivery GOTMPDIR=/config/workspace/.gotmp-iop-secure-delivery TMPDIR=/config/workspace/.gotmp-iop-secure-delivery go test -count=1 -race ./packages/go/auth ./packages/go/credentiallease ./apps/control-plane/internal/credentiallease ./apps/control-plane/internal/wire ./apps/edge/internal/authprojection ./apps/edge/internal/controlplane ./apps/edge/internal/service ./apps/edge/internal/transport ./apps/node/internal/node
env GOCACHE=/config/tmp/gocache-secure-delivery GOTMPDIR=/config/workspace/.gotmp-iop-secure-delivery TMPDIR=/config/workspace/.gotmp-iop-secure-delivery make test-control-plane-edge-wire
env GOCACHE=/config/tmp/gocache-secure-delivery GOTMPDIR=/config/workspace/.gotmp-iop-secure-delivery TMPDIR=/config/workspace/.gotmp-iop-secure-delivery go vet ./apps/control-plane/... ./apps/edge/... ./apps/node/... ./packages/go/...
git diff --check
if rg --sort path -n 'bytes[[:space:]]+(secret|provider_secret)|map<string, string>[[:space:]]+(credential|provider_auth)' proto/iop/runtime.proto; then exit 1; fi
```

Expected: every command exits 0; the full three-process secure-delivery fixture still proves HTTPS/mTLS, Node-sealed exact upstream auth, in-flight completion, post-revoke fencing, and sentinel absence, while the new tests prove enrollment-name binding and projection continuity.

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