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

# Repair Credential Slot and Route Catalog Invariants

## For the Implementing Agent

Filling implementation-owned sections of `CODE_REVIEW-cloud-G08.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

The first implementation persists opaque envelopes and route tuples, but it does not implement the approved S04/S05 slot shape or fail-closed activation rules. A fresh reviewer reproducer proved that a slot can be re-enabled after its envelope key disappears and that an active route can be created for a revoked slot. This follow-up repairs those durable invariants and replaces the same-profile evidence with real compatible multi-profile coverage.

## Archive Evidence Snapshot

- Closing pair: `agent-task/m-principal-provider-credential-slot-routing/02+01_credential_catalog/plan_local_G07_1.log`, `agent-task/m-principal-provider-credential-slot-routing/02+01_credential_catalog/code_review_cloud_G07_1.log`.
- Verdict: FAIL with five Required findings, zero Suggested findings, and zero Nits.
- Required repair: persist credential kind and optional normalized aliases; reject unknown vendor/auth tuples; preserve key-backed slot activation and inactive-slot route rejection; add revision-CAS route update; replace incomplete S04/S05 tests.
- Fresh evidence: planned package tests, race tests, Control Plane regression, vet, and diff checks passed, while a task-local reviewer reproducer failed because `EnableSlot` and `CreateRoute` returned nil for the invalid activation cases. The temporary reproducer 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`
- `apps/control-plane/cmd/control-plane/server.go`
- `apps/control-plane/cmd/control-plane/credential_store_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`

### 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 multi-slot, alias collision, multi-model/profile binding, and principal isolation evidence.
- The checklist therefore restores the SDD slot fields and draft/active lifecycle, validates key/vendor/credential-kind/profile compatibility at every active transition, and proves one credential-compatible slot across distinct protocol profiles plus strict route update/lookup behavior.

### Verification Context

- No external handoff was supplied. Repository-native sources were the local Control Plane rules, `agent-test/local/rules.md`, `agent-test/local/control-plane-smoke.md`, the approved SDD, the protocol-profile catalog, and the current package tests.
- 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 baseline results: both focused package commands, package vet, race tests, `./apps/control-plane/...`, Control Plane vet, and `git diff --check` passed. `go mod tidy -diff` reports pre-existing manifest drift outside this packet's file claims and is not used as this follow-up's completion oracle.
- Focused reviewer evidence: key removal followed by `EnableSlot`, and revoked-slot `CreateRoute`, both returned nil errors. The temporary reproducer was removed after capture.
- The predecessor has exactly one archived `complete.log`; no external provider, credential, Docker service, or remote runner is required. Confidence: high.

### Test Coverage Gaps

- Optional/normalized slot alias and credential kind: absent.
- Draft-to-active activation with a currently registered envelope key: absent; current code fails the reviewer reproducer.
- Disabled/revoked slot route rejection and route re-enable fencing: absent; current code fails the reviewer reproducer.
- Unknown vendor and vendor/credential-kind/profile-auth rejection: absent; the compatibility function currently permits unknown vendors.
- Actual one-slot/two-profile binding: absent; `TestSlotSupportsMultipleCompatibleRoutes` selects `openai` twice.
- Route update CAS, route-id/alias namespace collision, and reopen persistence: absent.

### Symbol References

- No symbol is removed. `CreateSlotInput`, `CredentialSlotRecord`, `CreateRoute`, `EnableSlot`, and route status methods are referenced only inside `apps/control-plane/internal/credentialstore` tests in the current worktree.
- Add `UpdateRoute` as a new typed store operation; no existing call site requires migration.

### Split Judgment

- Keep one packet. Slot schema, key-backed activation, compatibility validation, and route update share one SQL transaction/state invariant; splitting would allow an active route or slot to exist against an incomplete durable contract.
- Runtime predecessor `01_principal_store` is satisfied by `agent-task/archive/2026/08/m-principal-provider-credential-slot-routing/01_principal_store/complete.log`.

### Scope Rationale

- Exclude encryption/decryption and deployment key loading, management transports, Edge projections, route discovery, credential leases, and adapter injection; later milestone packets own those surfaces.
- Do not modify `packages/go/config` built-in profiles. The credential store consumes the immutable catalog and owns a strict compatibility mapping for slot vendor/credential kind to approved profile ids/auth declarations.
- Do not modify predecessor principal/token code or unrelated task-loop files present in the shared worktree.

### 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 `2+2+1+2+1=G08`; base=`local-fit`, route basis=`recovery-boundary`, lane=`cloud`, filename=`PLAN-cloud-G08.md`.
- Review closures: scope/context/verification/evidence/ownership/decision all true. Scores `2+2+1+2+1=G08`; route=`official-review`, lane=`cloud`, adapter=`codex`, model=`gpt-5.6-sol`, reasoning=`xhigh`, filename=`CODE_REVIEW-cloud-G08.md`.
- `large_indivisible_context=false`; positive loop risks=`temporal_state`, `variant_product` (2); `review_rework_count=1`; `evidence_integrity_failure=true`; 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. Migrate the slot shape and lifecycle before changing route compatibility/update behavior.
3. Keep every change within `02+01_credential_catalog`; no new split dependency is introduced.

## Implementation Checklist

- [ ] Restore the S04 slot schema/API with credential kind, optional normalized aliases, draft-to-active semantics, and key validation on every active transition, with deterministic migration and lifecycle tests.
- [ ] Restore the S05 route contract with strict vendor/credential-kind/profile-auth compatibility, usable-slot fencing, collision-free route identity/alias resolution, revision-CAS update, and real multi-profile/reopen tests.
- [ ] Run fresh focused, race, Control Plane regression, predecessor, and diff verification without repository-local temporary reviewer files.
- [ ] Fill implementation-owned sections in CODE_REVIEW-*-G??.md with actual implementation notes and verification output.

### [REVIEW_API-1] Restore the key-backed slot contract

**Problem:** `apps/control-plane/internal/credentialstore/schema.go:71-90` has no credential kind, requires every alias, and has no draft state. `apps/control-plane/internal/credentialstore/slot.go:68-79` validates a non-empty but unnormalized alias, while `EnableSlot` at lines 320-322 bypasses envelope-key validation and binding prerequisites.

**Solution:** Persist a typed credential kind and nullable canonical alias in both dialect schemas. Add an idempotent migration for databases created with the current intermediate schema. Create slots as draft; normalize an optional alias before transaction checks. When the first compatible route activates a draft slot, or when a disabled slot is enabled, validate the stored envelope key and require a non-revoked compatible binding before the status CAS.

Before (`apps/control-plane/internal/credentialstore/slot.go:28-34`):

```go
type CreateSlotInput struct {
    PrincipalID string
    Vendor      string
    Alias       string
    Envelope    SecretEnvelope
}
```

After contract:

```go
type CreateSlotInput struct {
    PrincipalID   string
    Vendor        string
    CredentialKind string
    Alias         string
    Envelope      SecretEnvelope
}
```

**Modified Files and Checklist:**

- [ ] `apps/control-plane/internal/credentialstore/schema.go`: add dialect-safe credential-kind, nullable alias, draft status, partial alias uniqueness, and idempotent intermediate-schema migration.
- [ ] `apps/control-plane/internal/credentialstore/slot.go`: normalize vendor/kind/alias, create draft slots, and key/binding-gate active transitions with revision CAS.
- [ ] `apps/control-plane/internal/credentialstore/slot_test.go`: add optional alias, whitespace normalization, credential-kind validation, draft activation, missing-key reactivation, lifecycle CAS, ciphertext-only inspection, and reopen fixtures.

**Test Strategy:** Add `TestCredentialSlotOptionalAliasDoesNotCollide`, `TestCredentialSlotAliasNormalizationFailsClosed`, `TestCredentialSlotRequiresKnownCredentialKind`, `TestCredentialSlotActivationRequiresRegisteredKey`, and a draft/active/reopen table test. Use the deterministic fake key registry and opaque ciphertext only.

**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 'CredentialSlot|EnvelopeKey|SlotActivation'` must pass.

### [REVIEW_API-2] Make route bindings strict and mutable by CAS

**Problem:** `apps/control-plane/internal/credentialstore/route.go:62-75` permits every unknown vendor and ignores profile auth/credential kind. `CreateRoute` at lines 95-118 does not reject disabled/revoked slots, and the file has no planned revision-CAS update for alias/profile/model/resource selector. `route_test.go:117-144` proves two models on the same `openai` profile, not two compatible profiles.

**Solution:** Replace the permissive driver switch with an explicit normalized vendor + credential-kind + profile-id/auth compatibility table that rejects unknown tuples. In one transaction, allow the first valid route to promote a draft slot to active after key validation; reject disabled/revoked slots for route creation and reject unusable slots when enabling a route. Add `UpdateRoute` with expected revision, ownership, compatibility, alias/route-id namespace, and stale-CAS checks.

Before (`apps/control-plane/internal/credentialstore/route.go:62-75`):

```go
func checkVendorProfileCompatibility(vendor string, profile config.ConcreteProtocolProfile) error {
    switch strings.ToLower(strings.TrimSpace(vendor)) {
    // known driver groups
    }
    return nil
}
```

After contract:

```go
func checkSlotProfileCompatibility(slot CredentialSlotRecord, profile config.ConcreteProtocolProfile) error {
    rule, ok := credentialProfileRules[compatibilityKey(slot.Vendor, slot.CredentialKind)]
    if !ok || !rule.allows(profile.ID, profile.Auth) {
        return ErrIncompatibleProfile
    }
    return nil
}
```

**Modified Files and Checklist:**

- [ ] `apps/control-plane/internal/credentialstore/route.go`: add strict compatibility, active-slot fencing, draft activation transaction, alias/id collision checks, and `UpdateRoute` revision CAS.
- [ ] `apps/control-plane/internal/credentialstore/route_test.go`: add a Minimax-style bearer slot bound to distinct Chat and Messages profiles, unknown/mismatched tuple rejection, disabled/revoked slot rejection, route-enable fencing, alias/id collision, update CAS, and reopen persistence.

**Test Strategy:** Add `TestSlotSupportsCompatibleChatAndMessagesProfiles`, `TestRouteRejectsUnknownVendorCredentialTuple`, `TestRouteRejectsInactiveSlot`, `TestEnableRouteRejectsInactiveSlot`, `TestUpdateRouteUsesRevisionCAS`, `TestRouteAliasCannotShadowRouteID`, and `TestRouteLifecyclePersistsAcrossReopen`. Keep existing same-model/two-slot and cross-principal fixtures.

**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 'Route|Binding|Compatibility'` must pass.

## Modified Files Summary

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

## 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 -race ./apps/control-plane/internal/credentialstore` — S04/S05 migration, lifecycle, CAS, compatibility, isolation, and reopen fixtures pass.
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 'SlotActivation|InactiveSlot|UpdateRouteUsesRevisionCAS|AliasCannotShadow'` — fail-closed and CAS regressions are stable across repetition.
6. `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.
7. `git diff --check` — no whitespace errors.

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