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

# Credential Slot and Route Catalog

## For the Implementing Agent

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

## Background

Provider credentials are currently coupled to static Edge provider configuration. The milestone needs a Control Plane source of truth where multiple credential slots per principal are independent from protocol profiles and client-facing routes resolve unambiguously to one slot/profile/upstream-model tuple.

## 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

- `go.mod`
- `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 row: S04-S08의 multi-slot, alias collision, multi-model binding 근거 중 S04-S05.
- Checklist/verification은 동일 principal/vendor 복수 slot, ciphertext envelope 필수, principal-scoped alias uniqueness, 한 slot의 compatible multi-profile/model route, cross-principal/incompatible/ambiguous route fail-closed를 직접 증명한다.

### Verification Context

- handoff 없음. 저장소 manifest, protocol profile resolver/tests, Control Plane store plan contract, project/domain/local test rules로 명령을 도출했다.
- Local runner `/config/workspace/iop-s0`, Go 1.26.2 linux/arm64. 기본 Go temp/cache 권한 문제 때문에 ignored `.cache/go-build`와 `.cache/go-cache`를 명시한다.
- Precondition: sibling `01_principal_store/complete.log`가 필요하다. 현재 active/archived completion은 없어 implementation을 시작할 수 없지만 계획 작성과 routing에는 ambiguity가 없다.
- 외부 provider/DB/secret 없이 temp SQLite와 deterministic opaque envelopes로 검증 가능하다. Confidence: high.

### Test Coverage Gaps

- 기존 config protocol profile tests는 built-in profile resolution만 다루며 credential slot compatibility/lifecycle을 다루지 않는다.
- multi-slot, alias collision, route ownership, route ID uniqueness, encrypted envelope validation을 다루는 Control Plane test가 전무하므로 신규 repository tests가 필요하다.

### Symbol References

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

### Split Judgment

- Stable contract: slot row는 opaque encrypted revision을 보존하고 route row는 정확히 한 principal/slot/profile/model을 참조한다. 두 테이블의 FK와 활성화 transaction이 하나의 invariant이므로 S04/S05를 같은 packet으로 유지한다.
- Predecessor 01 = `01_principal_store`; active `complete.log` missing, archived candidate 없음. `agent-task/m-principal-provider-credential-slot-routing/01_principal_store/complete.log` 생성 전 구현 금지.
- Consumers: 03은 projection snapshot을, 05는 management mutation API를 사용한다.

### Scope Rationale

- raw provider credential 암호화 구현과 실행 시 secret delivery는 Epic 3 소유이다. 이 packet은 non-empty key id/nonce/ciphertext를 가진 opaque envelope만 수락하고 plaintext를 받지 않는다.
- Edge cache, discovery/dispatch, management transport는 후속 packet으로 제외한다.
- built-in protocol profile 정의는 변경하지 않고 resolver 결과를 compatibility oracle로 사용한다.

### Final Routing

- `status=routed`, `evaluation_mode=first-pass`, finalizer=`finalize-task-policy.sh`, pair mode.
- Build closures 모두 true; scores `2+2+1+1+1=G07`; base/route=`local-fit`, lane=`local`, canonical=`PLAN-local-G07.md`.
- Review closures 모두 true; scores `2+2+1+1+1=G07`; `official-review`, cloud, `gpt-5.6-sol` xhigh, canonical=`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 produce `agent-task/m-principal-provider-credential-slot-routing/01_principal_store/complete.log`.
2. Implement schema/envelope/slot lifecycle, then route bindings against that stable contract.
3. Do not add dependencies absent from directory name `02+01_credential_catalog`.

## Implementation Checklist

- [ ] Add opaque encrypted-secret revisions and principal-scoped credential-slot lifecycle with strict alias constraints.
- [ ] Add route bindings that converge to exactly one principal, slot, compatible protocol profile, and upstream model.
- [ ] Prove multi-slot, multi-profile/model, collision, cross-principal, lifecycle, and restart behavior with fresh repository tests.
- [ ] Fill implementation-owned sections in CODE_REVIEW-*-G??.md with actual implementation notes and verification output.

### [API-1] Persist opaque credential slots independently of profiles

**Problem:** the repository has no Control Plane credential tables; Edge provider auth config couples secret material and provider protocol selection.

**Solution:** Extend the credential-store migration with `credential_slots` and immutable `credential_secret_revisions`. Define stable UUID `slot_id`, principal FK, vendor, credential kind, optional normalized alias, lifecycle status/revision, and current secret revision. Accept only an `EncryptedSecretEnvelope` containing key id, nonce, ciphertext, and algorithm; never accept plaintext in this package.

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 shape:

```go
type EncryptedSecretEnvelope struct {
    KeyID, Algorithm string
    Nonce, Ciphertext []byte
}
type CredentialSlot struct {
    SlotID, PrincipalID, Vendor, CredentialKind string
    Alias, Status string
    Revision, SecretRevision uint64
}
```

**Modified Files and Checklist:**

- [ ] `apps/control-plane/internal/credentialstore/schema.go`: add constrained slot/secret revision tables and indexes.
- [ ] `apps/control-plane/internal/credentialstore/secret_envelope.go`: define opaque validated envelope without decrypt/plaintext API.
- [ ] `apps/control-plane/internal/credentialstore/slot.go`: create/list/rotate/disable/revoke with revision CAS and principal-scoped alias normalization.
- [ ] `apps/control-plane/internal/credentialstore/slot_test.go`: test two same-vendor slots, alias collision, envelope rejection, rotate, disable/revoke, reopen.

**Test Strategy:** Write `TestCredentialSlotsAllowSameVendorMultipleSlots`, `TestCredentialSlotRejectsMissingEnvelopeFields`, `TestCredentialSlotAliasUniqueWithinPrincipal`, `TestCredentialSlotRotationCreatesRevision`, and `TestCredentialSlotLifecyclePersists`. Fixtures use distinct opaque ciphertext bytes and never raw API tokens.

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

### [API-2] Bind each client route to one slot/profile/model

**Problem:** no durable `route_id` exists; config model catalogs can map a model group to several providers and cannot express principal-scoped explicit slot selection.

**Solution:** Add route records with principal FK, slot FK, client-facing stable `route_id`, optional display alias, protocol profile id, upstream model, status/revision. Resolve the profile via `config.ResolveProtocolProfile`; validate vendor/credential-kind compatibility; enforce route and alias uniqueness within a principal and transactionally reject cross-principal slot references.

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

```go
func ResolveProtocolProfile(profileID, providerType string, catalog map[string]ConcreteProtocolProfile) (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 route binding table, principal/slot composite FK, and uniqueness constraints.
- [ ] `apps/control-plane/internal/credentialstore/route.go`: create/list/update/disable/revoke and strict lookup by principal+route id.
- [ ] `apps/control-plane/internal/credentialstore/route_test.go`: cover same upstream model on two slots, one slot on multiple compatible routes, invalid profile, alias ambiguity, and cross-principal rejection.

**Test Strategy:** Write `TestRoutesDisambiguateSameUpstreamModelBySlot`, `TestSlotSupportsMultipleCompatibleProfileModelRoutes`, `TestRouteRejectsCrossPrincipalSlot`, `TestRouteRejectsIncompatibleProfile`, and `TestRouteAliasCollisionFailsClosed`. Assert each lookup returns one tuple and never guesses.

**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/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 test results are not acceptable.

1. `test -f agent-task/m-principal-provider-credential-slot-routing/01_principal_store/complete.log` — predecessor is complete.
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` — all lifecycle, compatibility, isolation, reopen, 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 suite passes.
6. `git diff --check` — no whitespace errors.

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