<!-- task=m-principal-provider-credential-slot-routing/02+01_credential_catalog plan=4 tag=REVIEW_API -->

# Reconcile Slot Lifecycle with Startup Binding Validation

## For the Implementing Agent

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

## Background

Startup validation now rejects incompatible persisted bindings, but it also treats every active route row attached to a disabled or revoked slot as catalog corruption. Supported `DisableSlot` and `RevokeSlot` operations leave route rows intact, so either transition makes the next Control Plane startup fail. This follow-up preserves deliberate slot lifecycle state while continuing to reject incompatible bindings before serving.

## Archive Evidence Snapshot

- Closing pair: `agent-task/m-principal-provider-credential-slot-routing/02+01_credential_catalog/plan_cloud_G08_3.log`, `agent-task/m-principal-provider-credential-slot-routing/02+01_credential_catalog/code_review_cloud_G08_3.log`.
- Verdict: FAIL with one Required finding, zero Suggested findings, and zero Nits.
- Required repair: reconcile startup binding validation with supported slot disable/revoke persistence and add restart regressions without weakening compatibility fencing.
- Fresh evidence: predecessor, focused, repeated, race, Control Plane regression, vet, reviewer-artifact absence, and diff commands passed. A temporary reviewer regression failed both disabled-slot and revoked-slot reopen attempts with `ErrIncompatibleProfile: active migrated route has an unavailable slot`; the temporary file was removed.
- Roadmap carryover: S04 `slot-store` and S05 `model-binding` remain incomplete until this follow-up passes.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/operational-observability-provider-management/milestones/principal-provider-credential-slot-routing.md`
- Milestone link: [Milestone document](agent-roadmap/phase/operational-observability-provider-management/milestones/principal-provider-credential-slot-routing.md)
- Task ids:
  - `slot-store`: principal-scoped provider credential slot ledger
  - `model-binding`: one route to one slot/profile/upstream-model tuple
- Completion mode: check-on-pass

## Analysis

### Files Read

- `apps/control-plane/internal/credentialstore/schema.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/credentialstore/route.go`
- `apps/control-plane/internal/credentialstore/route_test.go`
- `packages/go/config/protocol_profile.go`
- `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-task/m-principal-provider-credential-slot-routing/02+01_credential_catalog/plan_cloud_G08_2.log`
- `agent-task/m-principal-provider-credential-slot-routing/02+01_credential_catalog/code_review_cloud_G08_2.log`

### SDD Criteria

- Approved and unlocked SDD: `agent-roadmap/sdd/operational-observability-provider-management/principal-provider-credential-slot-routing/SDD.md`.
- Targeted scenarios: S04 / `slot-store` and S05 / `model-binding`; the S04-S08 Evidence Map row requires slot lifecycle and binding isolation evidence.
- S04 explicitly includes disable/revoke, and S05 requires each binding to remain a deterministic slot/profile/model tuple. The checklist therefore makes startup validation distinguish deliberate slot inactivity from incompatible binding data, while the final verification proves both restart persistence and fail-closed compatibility.

### Verification Context

- The code-review handoff supplied exact failing lifecycle evidence; repository-native sources were `agent-test/local/rules.md`, `agent-test/local/control-plane-smoke.md`, the Control Plane/testing domain rules, the approved SDD, the current package tests, and the exact predecessor completion count.
- Local preflight: `/config/.local/bin/go`, Go 1.26.2 linux/arm64, `GOROOT=/config/opt/go`, repo root `/config/workspace/iop-s0`. Use ignored `.cache/go-build` and `.cache/go-cache`; fresh `-count=1` results are required.
- Fresh reviewer verification: package vet, focused migration/catalog tests, 25-repeat regressions, package race, `./apps/control-plane/...`, predecessor count, reviewer-artifact absence, and `git diff --check` passed. The focused lifecycle probe failed disabled and revoked reopen with the same startup validation error and was removed.
- No external service, provider credential, Docker runner, remote host, user-visible wire path, or user authorization is required. Control Plane wire smoke and full-cycle execution are not applicable because this packet changes only repository startup validation over local durable catalog state. Confidence: high.

### Test Coverage Gaps

- `TestCredentialSlotLifecyclePersists` reopens an active slot before revocation but never closes and reopens after `DisableSlot` or `RevokeSlot`.
- Migration tests cover compatible active catalogs and incompatible active-slot bindings, but not compatible routes retained beneath deliberately inactive slots or incompatible routes beneath a disabled slot.
- Existing focused/race/Control Plane suites therefore pass while the supported lifecycle cannot restart.

### Symbol References

- No symbol is renamed, removed, or added. The change refines `validateMigratedCredentialBindings`; existing `DisableSlot`, `EnableSlot`, and `RevokeSlot` call sites remain unchanged.

### Split Judgment

- Keep one packet. Startup validation and slot lifecycle persistence are one durable-state invariant: inactive slots must remain reopenable, while every retained active route row must still satisfy ownership and profile compatibility before it can become effective again.
- Runtime predecessor `01_principal_store` remains satisfied by `agent-task/archive/2026/08/m-principal-provider-credential-slot-routing/01_principal_store/complete.log`.

### Scope Rationale

- Change only the startup validator and its slot migration/lifecycle tests. Do not add management APIs, route discovery, projection, leases, adapter injection, or contract/spec updates; later roadmap tasks own those surfaces.
- Preserve route rows across temporary slot disable so re-enable can restore the same explicit bindings. Keep incompatible binding rejection for active, disabled, and revoked slot rows; relax only the incorrect requirement that every retained active route row's slot status itself be `active` during startup.
- Do not modify unrelated shared-worktree files, predecessor principal/token code, protocol-profile definitions, or task-loop artifacts.

### Final Routing

- `status=routed`, `evaluation_mode=isolated-reassessment`, finalizer=`finalize-task-policy.sh`, mode=`pair`.
- Build closures: scope/context/verification/evidence/ownership/decision all true. Scores `1+2+1+2+1=G07`; base=`local-fit`, route basis=`recovery-boundary`, lane=`cloud`, filename=`PLAN-cloud-G07.md`.
- Review closures: scope/context/verification/evidence/ownership/decision all true. Scores `1+2+1+2+1=G07`; route=`official-review`, lane=`cloud`, adapter=`codex`, model=`gpt-5.6-sol`, reasoning=`xhigh`, filename=`CODE_REVIEW-cloud-G07.md`.
- `large_indivisible_context=false`; positive loop risks=`temporal_state`, `boundary_contract`, `variant_product` (3); `review_rework_count=3`; `evidence_integrity_failure=false`; recovery boundary matched; capability gap none.

## Dependencies and Execution Order

1. `01_principal_store` is satisfied by the exact archived completion path recorded above.
2. Refine startup validation before adding lifecycle reopen assertions so the tests exercise the final durable invariant.
3. Keep every change within `02+01_credential_catalog`; no new split dependency is introduced.

## Implementation Checklist

- [ ] Preserve disabled/revoked slot catalogs across reopen while retaining principal ownership, draft-state, and profile-compatibility fail-closed validation, with deterministic lifecycle and migration regressions.
- [ ] Run fresh focused, repeated, race, Control Plane regression, predecessor, reviewer-artifact absence, and diff verification.
- [ ] Fill implementation-owned sections in CODE_REVIEW-*-G??.md with actual implementation notes and verification output.

### [REVIEW_API-1] Align startup validation with durable slot lifecycle

**Problem:** `apps/control-plane/internal/credentialstore/schema.go:384-389` rejects an active route whenever its joined slot is not `active`. `apps/control-plane/internal/credentialstore/slot.go:365-379` intentionally persists slot disable/revoke without rewriting route rows, so a valid catalog produced by either public lifecycle operation cannot reopen.

**Solution:** Keep route/slot ownership and profile compatibility validation for every active route row, but treat `disabled` and `revoked` as deliberate non-serving slot states rather than migration corruption. Continue rejecting an active route attached to a `draft` or unknown slot state, and keep the active-slot compatible-binding check unchanged. This preserves temporary disable/re-enable binding identity, permanent revoke state, and fail-closed compatibility without fabricating route lifecycle transitions.

Before (`apps/control-plane/internal/credentialstore/schema.go:384-389`):

```go
if routePrincipalID != slot.PrincipalID || slot.Status != StatusActive {
    return fmt.Errorf("%w: active migrated route has an unavailable slot", ErrIncompatibleProfile)
}
if _, err := resolveSlotProfile(slot, profileID); err != nil {
    return fmt.Errorf("active migrated route compatibility: %w", err)
}
```

After contract:

```go
if routePrincipalID != slot.PrincipalID {
    return fmt.Errorf("%w: active migrated route has mismatched ownership", ErrIncompatibleProfile)
}
switch slot.Status {
case StatusActive, StatusDisabled, StatusRevoked:
    // Compatibility is still validated below. Disabled/revoked slots are not serving-ready.
default:
    return fmt.Errorf("%w: active migrated route has an unavailable slot", ErrIncompatibleProfile)
}
if _, err := resolveSlotProfile(slot, profileID); err != nil {
    return fmt.Errorf("active migrated route compatibility: %w", err)
}
```

**Modified Files and Checklist:**

- [ ] `apps/control-plane/internal/credentialstore/schema.go`: distinguish deliberate inactive slot lifecycle states from corrupt draft/missing ownership while preserving compatibility validation.
- [ ] `apps/control-plane/internal/credentialstore/slot_test.go`: add `TestCredentialSlotLifecycleReopensWithInactiveBindings` for disabled/revoked state and `TestCredentialSlotMigrationRejectsIncompatibleInactiveBinding` so the relaxation cannot bypass the closed profile catalog.

**Test Strategy:** Add table-driven SQLite restart coverage using the existing fake key registry and opaque ciphertext. For `disabled`, reopen, verify the slot state, re-enable it, and confirm the retained route remains compatible; for `revoked`, reopen, verify permanent revocation, and confirm re-enable is rejected. Add an incompatible disabled-slot intermediate-catalog fixture and require `Open` to fail with `ErrIncompatibleProfile` without exposing ciphertext.

**Verification:** `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 -run 'CredentialSlotMigration|SlotLifecycleReopens'` must pass.

## Modified Files Summary

| File | Items |
|------|-------|
| `apps/control-plane/internal/credentialstore/schema.go` | REVIEW_API-1 |
| `apps/control-plane/internal/credentialstore/slot_test.go` | REVIEW_API-1 |
| `agent-task/m-principal-provider-credential-slot-routing/02+01_credential_catalog/CODE_REVIEW-cloud-G07.md` | REVIEW_API-1 |

## Final Verification

Run from `/config/workspace/iop-s0`; cached Go test results are not acceptable.

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` — exactly one archived predecessor completion exists.
2. `mkdir -p .cache/go-build .cache/go-cache` — executable Go temp/cache paths exist.
3. `GOTMPDIR=/config/workspace/iop-s0/.cache/go-build GOCACHE=/config/workspace/iop-s0/.cache/go-cache go vet ./apps/control-plane/internal/credentialstore` — 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/control-plane/internal/credentialstore -run 'CredentialSlotMigration|SlotLifecycleReopens'` — valid inactive lifecycle catalogs reopen and incompatible bindings still fail closed.
5. `GOTMPDIR=/config/workspace/iop-s0/.cache/go-build GOCACHE=/config/workspace/iop-s0/.cache/go-cache go test -count=25 ./apps/control-plane/internal/credentialstore -run 'LifecycleReopens|RejectsIncompatibleInactive'` — lifecycle and compatibility regressions remain stable across repetition.
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` — S04/S05 migration, lifecycle, compatibility, CAS, isolation, and reopen fixtures pass under the race detector.
7. `GOTMPDIR=/config/workspace/iop-s0/.cache/go-build GOCACHE=/config/workspace/iop-s0/.cache/go-cache go test -count=1 ./apps/control-plane/...` — Control Plane regression suite passes.
8. `test ! -e apps/control-plane/internal/credentialstore/reviewer_probe_test.go` — no repository-local reviewer probe remains.
9. `git diff --check` — no whitespace errors.

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