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

# Credential Slot and Route Catalog

## 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 and the resume condition in implementation-owned fields; do not ask the user, call user-input tools, create stop files, or classify the next state.

## Background

Provider credentials need a principal-scoped durable slot independent of protocol profiles, and every client route must converge to one slot/profile/model tuple. The store boundary must accept only encrypted envelopes whose key id is recognized; accepting a merely non-empty key id would let an unusable or fabricated envelope become active.

## Archive Evidence Snapshot

- Prior pair: `agent-task/m-principal-provider-credential-slot-routing/02+01_credential_catalog/plan_local_G07_0.log`, `agent-task/m-principal-provider-credential-slot-routing/02+01_credential_catalog/code_review_cloud_G07_0.log`.
- Verdict/evidence: implementation had not started and no verdict or verification output existed.
- Replan reason: predecessor verification assumed an active `complete.log` even though PASS archives the predecessor, and envelope validation did not prove that the referenced key exists.
- Roadmap carryover: S04 `slot-store` and S05 `model-binding` remain the completion targets.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/operational-observability-provider-management/milestones/principal-provider-credential-slot-routing.md`
- Milestone link: [Milestone 문서](agent-roadmap/phase/operational-observability-provider-management/milestones/principal-provider-credential-slot-routing.md)
- Task ids:
  - `slot-store`: principal별 provider credential slot 원장
  - `model-binding`: route와 slot/profile/upstream model의 단일 결합
- Completion mode: check-on-pass

## Analysis

### Files Read

- `apps/control-plane/cmd/control-plane/server.go`
- `apps/node/internal/store/store.go`
- `apps/node/internal/store/store_test.go`
- `packages/go/config/config.go`
- `packages/go/config/protocol_profile.go`
- `packages/go/config/protocol_profile_test.go`

### SDD Criteria

- SDD: `agent-roadmap/sdd/operational-observability-provider-management/principal-provider-credential-slot-routing/SDD.md` (`승인`, 잠금 해제).
- Targets: S04 / `slot-store`, S05 / `model-binding`; Evidence Map S04-S08 중 multi-slot, alias collision, multi-model binding.
- Checklist와 verification은 동일 principal/vendor 복수 slot, registered key의 opaque ciphertext envelope, alias uniqueness, 한 slot의 compatible multi-profile/model route, cross-principal/incompatible/ambiguous binding 거부를 직접 증명한다.

### Verification Context

- handoff 없음. manifest, protocol-profile resolver/tests, local Control Plane test rules에서 검증을 도출했다.
- Go 1.26.2 linux/arm64. `/tmp`는 noexec이고 기본 Go cache는 접근 불가하므로 ignored `.cache/go-build`와 `.cache/go-cache`를 명시한다.
- Predecessor 01은 현재 미완료다. 구현 시 runtime scheduler가 active 또는 archived completion을 해결하며, verification은 허용된 동일 task-group archive 후보를 정확히 한 건 확인한다.
- External provider, raw secret, production key manager는 필요 없다. Tests use a deterministic fake key registry and opaque ciphertext. Confidence: high.

### Test Coverage Gaps

- 기존 protocol profile tests는 credential kind/vendor compatibility와 slot ownership을 다루지 않는다.
- multi-slot, registered/missing key, alias collision, route ownership, restart persistence tests가 없다.

### Symbol References

- Rename/remove 없음. Existing `config.ResolveProtocolProfile`은 validation dependency로만 추가 호출한다.

### Split Judgment

- Slot envelope/key registration, principal ownership, route FK/compatibility는 활성화 transaction의 한 invariant이므로 S04/S05를 같은 packet으로 유지한다.
- Predecessor 01 `01_principal_store`: active/archived `complete.log` missing, ambiguity 없음.
- 03, 05, 06은 이 packet의 immutable slot/route store API를 소비한다.

### Scope Rationale

- Encryption/decryption, deployment key loading/rotation, secure management transport는 Epic 3이다. 이 packet은 `EnvelopeKeyRegistry`로 key id 존재만 확인하고 plaintext/decrypt API를 만들지 않는다.
- Edge projection/route resolution과 management adapters는 후속 packets이다.

### Final Routing

- `status=routed`, `evaluation_mode=isolated-reassessment`, finalizer=`finalize-task-policy.sh`, pair mode.
- Build closures all true; scores `2+2+1+1+1=G07`; base/route=`local-fit`, local, `PLAN-local-G07.md`.
- Review closures all true; scores `2+2+1+1+1=G07`; `official-review`, cloud, `gpt-5.6-sol` xhigh, `CODE_REVIEW-cloud-G07.md`.
- `large_indivisible_context=false`; risks=`temporal_state`, `boundary_contract`, `variant_product` (3); rework=0; integrity failure=false; capability gap 없음.

## Dependencies and Execution Order

1. `01_principal_store` must complete first; runtime accepts its active or archived `complete.log`.
2. Implement envelope/key registry and slot lifecycle before route bindings.
3. No dependency absent from `02+01_credential_catalog` may be introduced.

## Implementation Checklist

- [ ] Persist opaque encrypted-secret revisions and slot lifecycle only when the envelope key id is registered, while enforcing principal-scoped aliases and S04 lifecycle fixtures.
- [ ] Persist route bindings that converge to exactly one principal, slot, compatible protocol profile, and upstream model, with S05 ambiguity and cross-principal fixtures.
- [ ] Run fresh repository race, restart, Control Plane regression, archive-predecessor, and diff verification.
- [ ] Fill implementation-owned sections in CODE_REVIEW-*-G??.md with actual implementation notes and verification output.

### [API-1] Persist key-backed opaque credential slots

**Problem:** no Control Plane slot store exists, and checking only a non-empty `key_id` cannot satisfy S04's missing-envelope-key rejection.

**Solution:** Add constrained slot and immutable secret-revision tables. Add a source-compatible store option for `EnvelopeKeyRegistry`; the existing `Open` path installs an unavailable registry so production slot secret mutations fail closed until Epic-3 key wiring exists. Creation/rotation/activation rejects unknown key ids before transaction commit. Persist only algorithm, key id/version, nonce, ciphertext, AAD metadata, lifecycle, and revision.

Before (`apps/control-plane/cmd/control-plane/server.go:29-31`):

```go
if cfg.Database.URL != "" {
    logger.Info("control-plane database configured", databaseLogFields(cfg.Database.URL)...)
}
```

After domain contract:

```go
import "context"

type EnvelopeKeyRegistry interface {
    HasEnvelopeKey(context.Context, string, uint64) (bool, error)
}
if !known { return CredentialSlot{}, ErrEnvelopeKeyUnavailable }
```

**Modified Files and Checklist:**

- [ ] `apps/control-plane/internal/credentialstore/schema.go`: add slot/secret revision tables and constraints.
- [ ] `apps/control-plane/internal/credentialstore/store.go`: add source-compatible key-registry option and unavailable-by-default implementation.
- [ ] `apps/control-plane/internal/credentialstore/secret_envelope.go`: define opaque envelope and key-registry boundary.
- [ ] `apps/control-plane/internal/credentialstore/slot.go`: implement create/list/rotate/disable/enable/revoke with revision CAS and registered-key validation.
- [ ] `apps/control-plane/internal/credentialstore/slot_test.go`: cover same-vendor slots, registered/missing key, alias collision, lifecycle, ciphertext inspection, and reopen.

**Test Strategy:** Write `TestCredentialSlotsAllowSameVendorMultipleSlots`, `TestCredentialSlotRejectsUnknownEnvelopeKey`, `TestCredentialSlotRotationRequiresRegisteredKey`, `TestCredentialSlotAliasUniqueWithinPrincipal`, and `TestCredentialSlotLifecyclePersists` using fake key registry and opaque bytes.

**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'` passes.

### [API-2] Bind every route to one compatible slot/profile/model

**Problem:** the global model catalog has no durable principal route id and cannot fail closed on cross-principal or ambiguous alias selection.

**Solution:** Add route records with principal+slot composite ownership, canonical route id, optional normalized alias, profile id, upstream model, resource selector, status, and revision. Resolve profiles through `config.ResolveProtocolProfile`, validate vendor/auth compatibility, and enforce uniqueness transactionally.

Before (`packages/go/config/protocol_profile.go`, resolver contract):

```go
func ResolveProtocolProfile(selector, legacyType string, catalog map[string]ProtocolProfileConf) (ConcreteProtocolProfile, error)
```

After usage:

```go
profile, err := config.ResolveProtocolProfile(input.ProfileID, input.Vendor, config.BuiltInProtocolProfiles)
if err != nil { return RouteBinding{}, ErrIncompatibleProfile }
```

**Modified Files and Checklist:**

- [ ] `apps/control-plane/internal/credentialstore/schema.go`: add composite FKs and principal-scoped uniqueness constraints.
- [ ] `apps/control-plane/internal/credentialstore/route.go`: implement create/list/update/disable/revoke and strict lookup.
- [ ] `apps/control-plane/internal/credentialstore/route_test.go`: cover same upstream model/two slots, one slot/multiple routes, incompatible profile, ambiguous alias, and cross-principal rejection.

**Test Strategy:** Write `TestRoutesDisambiguateSameUpstreamModelBySlot`, `TestSlotSupportsMultipleCompatibleRoutes`, `TestRouteRejectsCrossPrincipalSlot`, `TestRouteRejectsIncompatibleProfile`, and `TestRouteAliasCollisionFailsClosed`.

**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'` passes.

## Modified Files Summary

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

## Final Verification

Run from `/config/workspace/iop-s0`; cached Go 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 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 lifecycle, isolation, restart, and race tests pass.
5. `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.
6. `git diff --check` — no whitespace errors.

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