<!-- task=m-principal-provider-credential-slot-routing/07+01,02,05_secret_material plan=2 tag=REVIEW_API milestone-task=secret-at-rest -->

# Provider credential durable-context and enabled-runtime follow-up plan

## For the Implementing Agent

Filling every implementation-owned section in `CODE_REVIEW-*-G??.md` is mandatory. Run the verification commands, paste actual notes and stdout/stderr, keep the active files in place, and report ready for review; finalization belongs only to the code-review skill. If blocked, record only the exact blocker, attempted commands/output, and resume condition in implementation-owned evidence fields. Do not ask the user, call user-input tools, create control-plane stop files, classify the next state, archive logs, or write `complete.log`.

## Background

The production keyring and durable envelope path pass the submitted suites, but review found two uncovered boundaries. Create-time AAD can diverge from the normalized credential kind stored in the slot row, and a complete encryption configuration can silently start without a database, store, or credential service. This follow-up repairs both boundaries without changing the AES-GCM format or activating credential-bearing listeners.

## Archive Evidence Snapshot

- `agent-task/m-principal-provider-credential-slot-routing/07+01,02,05_secret_material/plan_cloud_G08_1.log` and `agent-task/m-principal-provider-credential-slot-routing/07+01,02,05_secret_material/code_review_cloud_G09_1.log`: current pair closed with `FAIL`; Required findings are canonical credential-kind AAD binding and rejection of enabled encryption without a database.
- Reviewer reproduction: creating with credential kind `" BEARER "` persisted `"bearer"`, then opening the initial envelope with the exact persisted principal/slot/kind context failed with `credentialseal: open failed`; the temporary reviewer probe was removed.
- Fresh reviewer evidence passed the submitted targeted/full Control Plane tests, `go vet`, `git diff --check`, leak guard, import-layer check, and compiled encrypted startup/shutdown cycle. `review_rework_count=1`; `evidence_integrity_failure=false`.
- `plan_cloud_G08_0.log` and `code_review_cloud_G09_0.log` remain the earlier unimplemented scaffold. The exact predecessor completion logs for indices 01, 02, and 05 remain satisfied.

## Analysis

### Files Read

- `agent-roadmap/phase/operational-observability-provider-management/PHASE.md`
- `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-spec/control/control-plane-operations.md`
- `agent-contract/index.md`
- `agent-task/m-principal-provider-credential-slot-routing/07+01,02,05_secret_material/PLAN-cloud-G08.md`
- `agent-task/m-principal-provider-credential-slot-routing/07+01,02,05_secret_material/CODE_REVIEW-cloud-G09.md`
- `apps/control-plane/internal/credentialseal/aead.go`
- `apps/control-plane/internal/credentialseal/aead_test.go`
- `apps/control-plane/internal/credentialstore/store.go`
- `apps/control-plane/internal/credentialstore/secret_envelope.go`
- `apps/control-plane/internal/credentialstore/slot.go`
- `apps/control-plane/internal/credentialstore/slot_test.go`
- `apps/control-plane/internal/credentialops/service.go`
- `apps/control-plane/internal/credentialops/service_test.go`
- `apps/control-plane/cmd/control-plane/main.go`
- `apps/control-plane/cmd/control-plane/server.go`
- `apps/control-plane/cmd/control-plane/config_test.go`
- `apps/control-plane/cmd/control-plane/credential_store_test.go`
- `apps/control-plane/cmd/control-plane/credential_commands.go`
- `configs/control-plane.yaml`
- `agent-test/local/rules.md`
- `agent-test/local/control-plane-smoke.md`

### SDD Criteria

- SDD: `agent-roadmap/sdd/operational-observability-provider-management/principal-provider-credential-slot-routing/SDD.md`; status `[승인됨]`, lock released, no user review.
- First-line contribution scope remains `milestone-task=secret-at-rest`.
- Targeted acceptance is S09: stored ciphertext must open only with the correct durable key revision and exact principal/slot/kind context across rotation and restart.
- Evidence Map row S09-S13 requires encrypted-store inspection and a secret scan. It drives the canonical-kind regression, enabled-runtime fail-closed test, fresh package/full Control Plane suites, and final leak guard below.

### Verification Context

- No separate verification handoff was supplied. Repository-native sources were `agent-test/local/rules.md`, `agent-test/local/control-plane-smoke.md`, the source/tests listed above, and the active plan/review evidence.
- Local preflight resolved `/config/.local/bin/go`, Go `1.26.2 linux/arm64`, and `GOROOT=/config/opt/go`. The default cache is not usable in this environment, so fresh commands use `GOCACHE=/tmp/gocache-secret-material-followup`, `GOTMPDIR=/config/tmp`, and `-count=1`.
- Reviewer commands proved the submitted targeted/full Control Plane tests, vet, diff check, leak guard, import direction, and a compiled encrypted startup/shutdown cycle. The focused reviewer probe proved the normalized-kind failure and was removed afterward.
- No external runner, provider credential, network service, or user-controlled authorization is required. Tests use temporary SQLite databases and fake key material only; no raw secret may enter tracked config or evidence.
- Remaining gaps are the two missing boundary regressions named below. Confidence: high.

### Test Coverage Gaps

- Accepted credential-kind case/whitespace variants are normalized by `credentialstore` but are not tested through the real keyring; the current tests use already-canonical values and miss initial-envelope AAD divergence.
- Complete `credential_encryption` with an empty `database.url` is not tested; current coverage checks all-omitted mode, database-only mode, configured database+encryption, and partial triples.
- Existing nonce, tamper, key-version, restart, DB inspection, CAS/no-mutation, redaction, listener-failure, and secret-leak coverage remains valid and should not be rewritten.

### Symbol References

- No symbol is renamed or removed.
- `credentialops.Service.CreateSlot` is the only production create-time caller that constructs `credentialseal.Context`; rotation already derives kind from the persisted slot.
- `composeCredentialRuntime` is called only by `run`; its direct callers are command tests.

### Split Judgment

- Keep one compact follow-up in the existing subtask directory. Both fixes close the same `secret-at-rest` PASS oracle, and splitting an already reviewed child would add task-state coordination without creating a safer intermediate product state.
- The directory dependency indices remain satisfied by:
  - `agent-task/archive/2026/08/m-principal-provider-credential-slot-routing/01_principal_store/complete.log`
  - `agent-task/archive/2026/08/m-principal-provider-credential-slot-routing/02+01_credential_catalog/complete.log`
  - `agent-task/archive/2026/08/m-principal-provider-credential-slot-routing/05+01,02_management_core/complete.log`

### Scope Rationale

- Do not change the AES-GCM envelope format, key manifest, file permissions, schema, route lifecycle, or key-version behavior; fresh evidence already covers those paths.
- Do not add management wire handlers, TLS, credential leases, adapter injection, migration mode, attribution, or roadmap mutations; later sibling packets own those features.
- Preserve principal bootstrap as store-only and preserve all-fields-omitted/database-only compatibility.

### Final Routing

- `evaluation_mode=isolated-reassessment`; finalizer `finalize-task-policy.sh pair` executed once after the packet was complete.
- Build closures are all true. Scores `2,1,1,1,1` produce `G06`; base/final basis `local-fit`, route `PLAN-local-G06.md`.
- Review closures are all true. Scores `2,1,1,1,1` produce `G06`; route `official-review`, `CODE_REVIEW-cloud-G06.md` using Codex `gpt-5.6-sol` with `xhigh` reasoning.
- `large_indivisible_context=false`; positive loop risks are `temporal_state`, `boundary_contract`, and `variant_product` (`loop_risk_count=3`). `review_rework_count=1`, `evidence_integrity_failure=false`; neither risk nor recovery boundary matches. No capability gap exists.

## Implementation Checklist

- [ ] Canonicalize and validate credential kind before create-time sealing, use the same value for AAD and persistence, and add real-keyring variant/rotation/restart regression coverage.
- [ ] Reject configured credential encryption when no database can host the store/service, preserve omitted/database-only compatibility, and add composition/run listener-boundary regressions.
- [ ] Run fresh focused and full Control Plane verification, vet, diff/leak guards, and confirm no reviewer probe remains.
- [ ] Fill implementation-owned sections in CODE_REVIEW-*-G??.md with actual implementation notes and verification output.

### [REVIEW_API-1] Canonical durable credential-kind context

**Problem:** `apps/control-plane/internal/credentialops/service.go:239-256` puts raw `input.CredentialKind` into `credentialseal.Context`, while `credentialstore.CreateSlot` normalizes the persisted kind. Inputs such as `" BEARER "` are accepted but produce an initial envelope that cannot open from the durable row context.

**Solution:** Normalize and validate the service input before invoking the sealer. Use the resulting canonical `bearer` or `api_key` value in both `credentialseal.Context.Kind` and `credentialstore.CreateSlotInput.CredentialKind`; reject unknown kinds before any sealer call.

Before (`apps/control-plane/internal/credentialops/service.go:239-256`):

```go
sealCtx := credentialseal.Context{
	PrincipalID: principalID,
	SlotID:      slotID,
	Kind:        input.CredentialKind,
}
// ...
CredentialKind: input.CredentialKind,
```

After:

```go
kind, err := canonicalCredentialKind(input.CredentialKind)
if err != nil {
	return SlotRecord{}, err
}
sealCtx := credentialseal.Context{
	PrincipalID: principalID,
	SlotID:      slotID,
	Kind:        kind,
}
// ...
CredentialKind: kind,
```

**Modified Files and Checklist:**

- [ ] Update `apps/control-plane/internal/credentialops/service.go` with one canonical kind validator that reuses the `credentialstore` kind constants and runs before sealing.
- [ ] Add `TestServiceCreateCanonicalizesCredentialKindForAAD` in `apps/control-plane/internal/credentialops/service_test.go`, covering bearer/API-key case and whitespace variants, initial+rotated revisions, durable kind equality, and reopen with the exact persisted context.
- [ ] Assert an unknown kind returns before the sealer is called and leaves slot/revision/projection state unchanged.

**Test Strategy:** Write the named table-driven regression with a production `credentialseal.Keyring`, temporary SQLite DBs, and deterministic fake key material. The test must open every persisted revision after restart using the row's principal ID, slot ID, and normalized kind.

**Verification:** `GOCACHE=/tmp/gocache-secret-material-followup GOTMPDIR=/config/tmp go test -count=1 ./apps/control-plane/internal/credentialops -run 'TestServiceCreateCanonicalizesCredentialKindForAAD|TestServiceSecretHandlingFailureIsBlindAndDoesNotMutate'` exits 0.

### [REVIEW_API-2] Reject enabled encryption without a credential store

**Problem:** `apps/control-plane/cmd/control-plane/server.go:101-113` accepts a non-nil keyring while `credentialstore.Open` returns `(nil, nil)` for empty `database.url`; composition then returns success without the required store/service and `run` proceeds to listeners.

**Solution:** Treat a complete encryption configuration without a database as an invalid enabled-runtime combination. Return a constant, secret-free composition error before listeners, while leaving all-fields-omitted/no-database and database-only/no-encryption modes unchanged.

Before (`apps/control-plane/cmd/control-plane/server.go:105-113`):

```go
store, err := credentialstore.Open(ctx, cfg.Database.URL, opts...)
// ...
if store != nil && keyring != nil {
	rt.service = credentialops.NewService(store, logger, keyring, nil)
}
```

After:

```go
if keyring != nil && strings.TrimSpace(cfg.Database.URL) == "" {
	return nil, fmt.Errorf("credential encryption requires database.url")
}
store, err := credentialstore.Open(ctx, cfg.Database.URL, opts...)
// configured encryption now guarantees non-nil store and service
```

**Modified Files and Checklist:**

- [ ] Update `apps/control-plane/cmd/control-plane/server.go` to reject enabled encryption with an empty/whitespace database URL before listener construction.
- [ ] Add `TestComposeCredentialRuntimeEncryptionRequiresDatabase` in `apps/control-plane/cmd/control-plane/credential_store_test.go`.
- [ ] Add `TestRunWithEncryptionAndNoDatabaseDoesNotStartListener` and assert no HTTP/client-wire/edge-wire listening log is emitted.
- [ ] Reassert unconfigured database and database-only secret-disabled modes remain compatible.

**Test Strategy:** Write direct composition and `run` regressions using a valid temporary 0600 manifest, observer logger, empty/whitespace database variants, and temporary listener addresses. No external service is required.

**Verification:** `GOCACHE=/tmp/gocache-secret-material-followup GOTMPDIR=/config/tmp go test -count=1 ./apps/control-plane/cmd/control-plane -run 'TestComposeCredentialRuntimeEncryptionRequiresDatabase|TestRunWithEncryptionAndNoDatabaseDoesNotStartListener|TestRunAllowsUnconfiguredDatabase|TestComposeCredentialRuntimeSecretDisabledMode'` exits 0.

## Dependencies and Execution Order

1. Implement `REVIEW_API-1` and its regression.
2. Implement `REVIEW_API-2` and its composition/run regressions.
3. Run the full verification block and fill the review evidence file.

## Modified Files Summary

| File | Items |
|------|-------|
| `apps/control-plane/internal/credentialops/service.go` | REVIEW_API-1 |
| `apps/control-plane/internal/credentialops/service_test.go` | REVIEW_API-1 |
| `apps/control-plane/cmd/control-plane/server.go` | REVIEW_API-2 |
| `apps/control-plane/cmd/control-plane/credential_store_test.go` | REVIEW_API-2 |
| `agent-task/m-principal-provider-credential-slot-routing/07+01,02,05_secret_material/CODE_REVIEW-cloud-G06.md` | all |

## Final Verification

Run from `/config/workspace/iop-s0`; cached test output is not accepted.

```bash
mkdir -p /config/tmp /tmp/gocache-secret-material-followup
command -v go && go version && go env GOROOT
git status --short --branch
GOCACHE=/tmp/gocache-secret-material-followup GOTMPDIR=/config/tmp go test -count=1 ./apps/control-plane/internal/credentialops -run 'TestServiceCreateCanonicalizesCredentialKindForAAD|TestServiceSecretHandlingFailureIsBlindAndDoesNotMutate'
GOCACHE=/tmp/gocache-secret-material-followup GOTMPDIR=/config/tmp go test -count=1 ./apps/control-plane/cmd/control-plane -run 'TestComposeCredentialRuntimeEncryptionRequiresDatabase|TestRunWithEncryptionAndNoDatabaseDoesNotStartListener|TestRunAllowsUnconfiguredDatabase|TestComposeCredentialRuntimeSecretDisabledMode'
GOCACHE=/tmp/gocache-secret-material-followup GOTMPDIR=/config/tmp go test -count=1 ./apps/control-plane/internal/credentialseal ./apps/control-plane/internal/credentialstore ./apps/control-plane/internal/credentialops
GOCACHE=/tmp/gocache-secret-material-followup GOTMPDIR=/config/tmp go test -count=1 ./apps/control-plane/...
GOCACHE=/tmp/gocache-secret-material-followup GOTMPDIR=/config/tmp go vet ./apps/control-plane/...
git diff --check
test ! -e apps/control-plane/internal/credentialops/reviewer_probe_test.go
credential_leak_pattern='BEGIN'' AGE PRIVATE KEY|active_key_value[[:space:]]*:[[:space:]]*[^<$]'
if rg --sort path -n --hidden --glob '!agent-task/archive/**' --glob '!*.sops.yaml' --glob '!PLAN-*.md' --glob '!*.log' "$credential_leak_pattern" apps/control-plane configs agent-task/m-principal-provider-credential-slot-routing/07+01,02,05_secret_material; then exit 1; fi
```

Expected: every command exits 0, the guarded scan emits no match, the exact persisted context opens every tested revision after restart, enabled encryption without a database fails before listener logs, and no raw secret/key appears in output.

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