# SDD: 사용자별 Provider Credential Slot과 Alias Routing

## 위치

- Milestone: [Milestone 문서](../../../phase/operational-observability-provider-management/milestones/principal-provider-credential-slot-routing.md)
- Phase: [PHASE.md](../../../../phase/operational-observability-provider-management/PHASE.md)

## 상태

[승인됨]

## SDD 잠금

- 상태: 해제
- 사용자 리뷰: 없음
- 잠금 항목: 없음

## 문제 / 비목표

- 문제: 현재 IOP bearer identity는 Edge YAML의 `openai.principal_tokens[]` hash mapping이 원장이고, 외부 provider token은 caller가 `X-IOP-Provider-Authorization` header로 매번 전달한다. 이 구조는 Control Plane이 사용자와 provider credential을 소유하지 않으며, 한 사용자가 같은 vendor/model의 여러 token을 독립 slot과 alias로 관리하거나 Claude Code가 IOP token만으로 직접 호출할 수 없게 한다.
- 비목표:
  - 여러 token slot의 자동 rotation, round-robin, failover 또는 quota 최적화
  - 여러 vendor를 한 model alias 뒤에서 자동 대체하는 routing policy
  - 조직 역할 기반 IAM, billing/chargeback, provider credential marketplace
  - 모든 provider 계정의 상시 구독과 정기 live smoke

## Source of Truth

| 영역 | 기준 | 메모 |
|------|------|------|
| Roadmap | [Milestone 문서](../../../phase/operational-observability-provider-management/milestones/principal-provider-credential-slot-routing.md) | 목표, credential slot과 migration 완료 Task |
| Control Plane Store | `apps/control-plane`, `database.url` repository | principal, IOP token hash, slot/binding/ciphertext/revision의 canonical store |
| Client-Control Plane | `apps/control-plane/internal/wire`, `apps/client`, [Client-Control Plane Wire 계약](../../../../../agent-contract/inner/client-control-plane-wire.md) | principal/token/slot/binding/alias management와 authorization |
| Control Plane-Edge | `apps/control-plane/internal/wire`, `apps/edge/internal/controlplane`, [Control Plane-Edge Wire 계약](../../../../../agent-contract/inner/control-plane-edge-wire.md) | auth/routing projection과 bounded credential lease |
| Edge Ingress | `apps/edge/internal/openai` | principal authentication, route resolution와 model discovery |
| Edge-Node | `apps/edge/internal/service`, `apps/node/internal/node`, `proto/iop/runtime.proto`, [Edge-Node Runtime Wire 계약](../../../../../agent-contract/inner/edge-node-runtime-wire.md) | confidential sensitive payload 전달과 adapter injection |
| Current Compatibility | `packages/go/config`, `configs/edge.yaml`, [Edge Config/Refresh 계약](../../../../../agent-contract/inner/edge-config-runtime-refresh.md) | 현재 Edge-local principal hash와 caller provider-auth migration 입력 |
| User Decision | 2026-07-31 사용자 대화 | Control Plane ownership, 사용자/vendor별 multi-token slot, optional alias, IOP token 기반 호출, Seulgi 포함, implicit rotation 없음 |

## State Machine

| 상태 | 진입 조건 | 다음 상태 | 근거 |
|------|-----------|-----------|------|
| iop-token/issued | principal에 새 IOP token을 발급하고 hash만 commit함 | iop-token/active | one-time raw token 반환과 token revision |
| iop-token/active | active hash가 유효 projection에 포함됨 | iop-token/revoked / iop-token/expired | Control Plane token lifecycle |
| iop-token/revoked | revoke revision이 commit됨 | 없음 | 이후 projection/인증에서 제거, 재활성화 대신 재발급 |
| slot/draft | principal이 vendor/profile/secret/model binding을 등록함 | slot/active / slot/rejected | alias, key, profile, binding validation |
| slot/active | encrypted secret revision과 한 개 이상의 valid binding이 있음 | slot/disabled / slot/rotated / slot/revoked | slot lifecycle command |
| slot/rotated | 새 ciphertext revision이 commit됨 | slot/active | 이전 lease는 만료 또는 revision rejection으로 수렴 |
| slot/disabled | 운영자가 일시 중지함 | slot/active / slot/revoked | 새 route/lease 발급 차단, 명시 re-enable 가능 |
| slot/revoked | slot을 영구 폐기함 | 없음 | route와 새 lease 제거 |
| projection/fresh | Edge가 더 큰 generation의 signed/authorized projection을 적용함 | projection/fresh / projection/stale | expiry와 generation order |
| projection/stale | expiry 전 refresh에 실패하거나 expiry가 지남 | projection/fresh / request/rejected | expiry 뒤 CP-managed principal auth/route fail closed |
| request/authenticated | inbound IOP token이 fresh projection의 principal과 매칭됨 | request/route-resolved / request/rejected | request model과 principal-scoped route catalog |
| request/route-resolved | route id/alias가 정확히 한 slot, protocol profile과 upstream model을 가리킴 | request/lease-ready / request/rejected | active slot/binding/revision과 provider resource selector |
| request/lease-ready | authenticated confidential channel에서 받은 bounded credential lease가 valid함 | request/dispatched / request/rejected | principal/slot/route/target/revision/expiry validation |
| request/dispatched | adapter가 실행 직전에 secret을 주입함 | request/terminal | upstream response, cancel, timeout 또는 error |

## Interface Contract

- 계약 원문: principal/slot management는 [Client-Control Plane Wire 계약](../../../../../agent-contract/inner/client-control-plane-wire.md), Control Plane projection/lease는 [Control Plane-Edge Wire 계약](../../../../../agent-contract/inner/control-plane-edge-wire.md), sensitive runtime payload는 [Edge-Node Runtime Wire 계약](../../../../../agent-contract/inner/edge-node-runtime-wire.md), 현재 principal/provider auth migration은 [OpenAI-compatible API 계약](../../../../../agent-contract/outer/openai-compatible-api.md)과 [Edge Config/Refresh 계약](../../../../../agent-contract/inner/edge-config-runtime-refresh.md)을 갱신한다. 구현 시 Anthropic public auth/model route는 protocol Milestone이 만드는 `agent-contract/outer/anthropic-compatible-api.md`에 연결한다.
- Control Plane persistence:
  - `principal`: stable principal ref, display alias와 status를 가진다.
  - `iop_token`: stable token ref, cryptographically random한 high-entropy raw token의 SHA-256 digest, principal ref, status, revision과 timestamps를 가진다. raw token은 발급 시 한 번만 반환하고 저장하지 않으며 Edge는 같은 digest를 계산해 projection과 매칭한다.
  - `provider_credential_slot`: stable slot id, principal ref, optional user alias, vendor, credential kind, encrypted typed secret material, secret/key revision, status와 timestamps를 가진다. slot은 protocol profile이나 model을 직접 소유하지 않는다.
  - `provider_model_binding`: stable route id, slot id, provider profile id, upstream model, optional route alias, provider resource selector, capability/allowlist와 status를 가진다. binding profile의 vendor/auth declaration이 slot의 vendor/credential kind와 호환되어야 하며, 한 slot은 Chat/Messages를 포함한 여러 compatible profile/model binding을 가질 수 있고 같은 upstream model은 서로 다른 slot의 route로 각각 존재할 수 있다.
  - alias uniqueness: slot alias는 principal 안에서 유일하다. client `model` 선택에 쓰는 route id/route alias도 principal 안에서 유일해야 하며 중복 또는 다의적 해석은 저장/활성화 단계에서 fail closed한다. single-model slot은 slot alias를 route alias로 직접 쓸 수 있고 multi-model slot은 각 binding의 고유 route id/alias를 사용한다.
- Control Plane management:
  - host-local Control Plane operator CLI가 principal과 최초 IOP token을 bootstrap한다. 이 CLI는 Control Plane host의 deployment admin 권한과 DB/service boundary 안에서만 실행하며 unauthenticated remote admin endpoint를 만들지 않는다.
  - 이후 principal은 IOP token으로 인증해 자신의 추가 token ref, credential slot, model binding과 alias를 create/list/update/rotate/disable/revoke한다. provider raw secret은 create/rotate 입력에서만 받고 이후 list/get 응답에는 secret, ciphertext와 token digest를 반환하지 않는다.
  - operator의 secret-blind disable/revoke/recovery는 host-local CLI로 제한하고 raw provider secret 조회/reveal operation은 제공하지 않는다. 마지막 active IOP token 폐기처럼 principal의 자기 접근을 끊는 operation은 명시 확인을 요구하되 조직 RBAC와 세부 remote operator role model은 이 Milestone 범위가 아니다.
  - mutation은 expected revision을 받아 stale update를 conflict로 거부하고, alias/binding validation과 encrypted secret commit이 모두 성공한 뒤 새 revision을 publish한다.
- Secret at rest:
  - provider secret은 authenticated encryption envelope의 ciphertext, nonce, key id/version과 AAD metadata만 DB에 저장한다.
  - master/envelope key material은 deployment secret manager 또는 동등한 외부 secret source가 제공하고 tracked YAML, DB와 API payload에 저장하지 않는다. key source가 없으면 slot secret 생성/rotation/활성화를 거부한다.
- Edge projection:
  - projection은 generation, issued/expiry time, active IOP token hash→principal mapping, principal별 route/slot reference, profile/upstream model과 status revision만 포함한다. provider ciphertext/plaintext와 encryption key를 포함하지 않는다.
  - Edge는 더 큰 generation만 atomic apply하고 bounded memory cache를 사용한다. refresh 실패 중에도 expiry 전 projection은 사용할 수 있지만 expiry 뒤 CP-managed principal 인증/선택은 identity projection unavailable typed error로 fail closed한다. health와 별도 Edge-local admin auth 표면은 이 projection에 종속시키지 않는다.
- Inbound auth:
  - OpenAI Chat/Responses는 `Authorization: Bearer <IOP token>`을 사용한다. Anthropic Messages는 같은 bearer 또는 `x-api-key: <IOP token>`을 허용해 Claude Code의 API-key helper와 직접 연결한다.
  - bearer와 `x-api-key`가 동시에 있으면 두 값이 같은 active token이어야 한다. 불일치, unknown, disabled 또는 revoked token은 route/model lookup과 provider dispatch 전에 `401`로 종료한다.
  - caller metadata/user field와 caller-supplied provider token은 principal 또는 slot 선택 source가 아니다.
- Route and alias resolution:
  - public request의 `model`은 인증 principal의 canonical route id 또는 unique route alias만 해석한다. slot alias는 model binding의 route alias 생성/표시에 사용할 수 있지만 실행 시 항상 하나의 concrete route id, slot id, protocol profile id와 upstream model로 고정된다.
  - `/v1/models`의 OpenAI/Anthropic response variant는 그 principal의 active binding만 반환한다. 다른 principal, disabled/revoked slot과 expired projection의 route는 숨긴다.
  - binding의 provider resource selector는 기존 Edge provider pool 안에서 같은 profile/model을 실행할 Node/provider resource 후보를 제한한다. capacity/load 기반 실행 위치 선택은 동일 credential slot을 유지하며 token slot rotation으로 취급하지 않는다.
  - 같은 vendor/model의 `claude-1`, `claude-2` route가 있으면 요청된 route의 slot만 사용한다. quota/auth/provider failure가 발생해도 다른 slot 또는 vendor로 자동 전환하지 않는다.
- Secure credential delivery:
  - provider secret을 입력·운반하는 Client-Control Plane, Control Plane-Edge와 Edge-Node 연결은 server identity verification과 전송 기밀성을 제공해야 한다. system peer는 enrollment-bound identity로 상호 인증하고 principal client는 TLS 위에서 IOP token으로 인증한다. 현재 평문 TCP/WS 연결은 credential-bearing operation에 사용할 수 없다.
  - Edge는 route resolution 뒤 Control Plane에서 principal/slot/secret revision/route/target/expiry에 묶인 짧은 credential lease를 받고, 동일 scope와 revision 안에서만 bounded memory cache를 사용할 수 있다. Control Plane availability가 없고 유효 lease도 없으면 cloud dispatch를 거부한다.
  - credential lease는 wire의 dedicated sensitive payload로 구분하고 generic `headers`, metadata, config refresh, status snapshot과 event payload에 raw secret을 싣지 않는다. transport/debug interceptor도 sensitive payload body를 기록하지 않는다.
  - adapter는 request 실행 직전에 request-local lease secret을 provider profile의 auth header로 주입하고 terminal/cancel 뒤 보존하지 않는다. 관측에는 slot ref와 secret revision만 허용한다.
  - slot disable/revoke/rotation projection을 받은 뒤 새 lease와 새 dispatch를 차단한다. 이미 upstream에 dispatch된 요청은 강제 replay/cancel하지 않지만 그 lease는 새 attempt/retry에 재사용하지 않는다.
- Attribution:
  - route resolution 결과의 `credential_slot_ref`와 `credential_revision`은 request 동안 immutable dispatch binding으로 유지한다. 기존 `token_ref`는 inbound IOP token을 뜻하므로 provider credential slot 식별자로 재사용하지 않는다.
  - provider usage/실행 관측은 safe stable slot ref로 분리 집계할 수 있지만 user alias, raw secret, verifier/ciphertext와 lease id는 metric label에 넣지 않는다.
- Responses compatibility:
  - credential plane 활성 뒤 기존 Responses도 같은 principal route와 credential resolver를 사용하지만 Responses request/response/stream schema는 변경하지 않는다.
- Compatibility migration:
  - credential plane이 명시적으로 비활성인 배포만 기존 Edge `openai.principal_tokens[]`와 request-time `openai.provider_auth`를 사용할 수 있다.
  - credential plane 활성 시 Control Plane projection이 유일한 identity/slot 원장이다. legacy mapping과 충돌하면 병합하거나 우선순위를 추정하지 않고 startup/apply를 거부하며 caller provider-auth header는 무시가 아니라 명시적으로 거부한다.
  - IOP bearer token을 upstream provider credential로 재사용하지 않는다.
- 금지:
  - provider raw token, encryption key 또는 runtime auth header를 tracked config, generic protobuf map, status/error body, log, metric, trace와 qualification record에 남기지 않는다.
  - slot alias만 보고 vendor/model을 추정하거나 같은 model의 다른 slot으로 암묵적으로 회전하지 않는다.
  - stale/out-of-order projection이나 만료/변조/다른 target용 lease로 dispatch하지 않는다.

## Acceptance Scenarios

| ID | Milestone Task | Given | When | Then |
|----|----------------|-------|------|------|
| S01 | `principal-store` | operator bootstrap과 신규 principal의 IOP token 발급/폐기 요청이 있음 | Control Plane restart를 포함해 lifecycle을 실행함 | raw token은 한 번만 반환되고 SHA-256 digest/revision만 영속화되며 폐기 token은 복구되지 않는다. |
| S02 | `auth-projection` | fresh, stale, expired, revoked와 out-of-order projection이 있음 | Edge가 refresh와 인증을 수행함 | 더 큰 fresh generation만 atomic apply되고 expiry/revocation 뒤 CP-managed 외부 호출은 fail closed한다. |
| S03 | `surface-auth` | bearer, x-api-key, 동시 일치/불일치 token이 있음 | OpenAI/Anthropic ingress를 호출함 | 같은 principal 인증만 성공하고 실패는 provider/model 조회 전에 401로 끝난다. |
| S04 | `slot-store` | 한 principal의 같은 vendor에 두 provider token/alias와 한 token의 Chat/Messages profile 사용이 있음 | slot 생성/rotation/disable/revoke를 실행함 | credential은 protocol과 분리된 stable slot/revision으로 저장되고 compatible profile 재사용만 허용되며 secret은 ciphertext만 남고 충돌 alias는 거부된다. |
| S05 | `model-binding` | 같은 upstream Claude model을 쓰는 `claude-1`, `claude-2` slot과 multi-profile/model slot이 있음 | route/binding을 활성화하고 lookup함 | 각 route가 정확히 한 slot/profile/model로 수렴하고 multi-profile/model binding도 독립 route id로 선택된다. |
| S06 | `model-discovery` | 서로 다른 route 권한을 가진 두 principal이 있음 | OpenAI/Anthropic model list를 요청함 | 각 principal은 자신의 active route만 보고 Claude Code가 반환 alias/route를 선택할 수 있다. |
| S07 | `explicit-selection` | 같은 model에 두 active slot이 있고 하나가 quota/auth 실패함 | 첫 slot route로 요청함 | 요청 slot만 한 번 사용되고 다른 slot/vendor로 암묵적 fallback하지 않는다. |
| S08 | `credential-management` | host-local operator bootstrap과 principal create/list/update/rotate/disable/revoke 요청이 있음 | CLI와 Control Plane management operation을 실행함 | remote bootstrap은 없고 principal 범위/revision이 강제되며 secret은 입력 뒤 조회/로그에 반환되지 않는다. |
| S09 | `secret-at-rest` | provider token 저장, key rotation과 restart가 있음 | DB/API/log를 검사하고 decrypt fixture를 수행함 | ciphertext와 safe metadata만 저장되며 올바른 key revision만 복호화된다. |
| S10 | `secure-transport` | TLS identity가 올바르거나 없거나 다른 peer인 Client/Edge/Node 연결이 있음 | credential-bearing operation과 cloud dispatch를 시도함 | 인증·기밀 연결만 허용되고 평문/잘못된 peer에서는 secret을 보내기 전에 차단된다. |
| S11 | `credential-lease` | valid lease와 다른 principal/route/target, expired/revoked/tampered lease가 있음 | Edge/Node 실행 경계가 검증함 | valid scope/revision만 dedicated sensitive payload로 전달되고 나머지는 upstream 호출 전에 거부된다. |
| S12 | `adapter-injection` | valid Chat/Messages profile과 credential lease가 있음 | provider fixture endpoint를 호출함 | 기대 auth header만 upstream에 도달하고 wire debug/log/status/response에는 raw secret이 없다. |
| S13 | `revocation` | in-flight 요청과 아직 dispatch되지 않은 요청 중 slot revoke/rotation이 도착함 | 새 projection을 적용함 | in-flight는 기존 attempt만 terminal까지 가고 새 lease/dispatch/retry는 새 revision 없이는 차단된다. |
| S14 | `compat-migration` | legacy-only와 credential-plane-enabled config가 있음 | load/migrate/rollback을 실행함 | legacy-only는 유지되고 enabled mode는 CP만 원장으로 사용하며 dual-source 충돌은 거부되고 Responses schema는 유지된다. |
| S15 | `slot-attribution` | 같은 principal/model을 서로 다른 credential slot으로 호출함 | dispatch와 provider usage를 관측함 | 별도 `credential_slot_ref`로 분리 귀속되고 inbound `token_ref`와 alias/raw secret이 섞이지 않는다. |
| S16 | `slot-smoke` | deterministic Seulgi/vendor fixture와 opt-in 대표 provider credential 1개가 있음 | 일반 CI와 구현 시 one-shot E2E를 각각 수행함 | CI는 구독 없이 통과하고 live evidence에는 safe slot ref/revision/result만 남는다. |
| S17 | `contract-ops` | persistence/management/secure wire/auth 구현이 완료됨 | contract/spec/redaction runbook drift check를 수행함 | index와 계약이 구현 경계를 가리키고 raw secret 예시가 없다. |

## Evidence Map

| Scenario | Required Evidence | `agent-task` 연결 | 완료 Evidence 기대 |
|----------|-------------------|------------------|---------------------------|
| S01-S03 | CP repository lifecycle, projection generation/expiry와 dual-header ingress tests | `agent-task/m-principal-provider-credential-slot-routing/...` | `principal-store`, `auth-projection`, `surface-auth` Task id별 persistence/auth 결과 |
| S04-S08 | multi-slot, alias collision, multi-model binding, principal discovery/no-fallback와 management authorization tests | `agent-task/m-principal-provider-credential-slot-routing/...` | `slot-store`, `model-binding`, `model-discovery`, `explicit-selection`, `credential-management` Task id별 isolation 근거 |
| S09-S13 | encrypted-store inspection, TLS peer matrix, credential lease scope, upstream header와 revocation race tests | `agent-task/m-principal-provider-credential-slot-routing/...` | `secret-at-rest`, `secure-transport`, `credential-lease`, `adapter-injection`, `revocation` Task id와 secret scan 결과 |
| S14 | legacy/CP mode config and migration table tests | `agent-task/m-principal-provider-credential-slot-routing/...` | `compat-migration` Task id와 dual-source rejection/Responses regression 근거 |
| S15 | two-slot dispatch/usage attribution test와 metric label allowlist check | `agent-task/m-principal-provider-credential-slot-routing/...` | `slot-attribution` Task id, slot별 usage와 inbound token ref 분리 근거 |
| S16 | credential-free provider fixtures와 opt-in representative one-shot record | `agent-task/m-principal-provider-credential-slot-routing/...` | `slot-smoke` Task id, provider/model/safe slot ref/date/revision/result |
| S17 | contract/spec index check와 repository-wide credential leak scan | `agent-task/m-principal-provider-credential-slot-routing/...` | `contract-ops` Task id, drift 없음과 raw secret 미검출 근거 |

## Cross-repo Dependencies

- 없음

## Drift Check

- [x] Milestone 기능 Task와 Acceptance Scenario가 일치한다.
- [x] Evidence Map이 code-review/complete.log에서 검증 가능하다.
- [x] agent-contract를 쓰는 경우 SDD에 계약 원문을 복제하지 않았다.
- [x] 사용자 리뷰가 필요한 항목은 `USER_REVIEW.md`에만 남겼다.

## 사용자 리뷰 이력

- 2026-07-31: 사용자가 Control Plane credential ownership, 사용자/vendor별 multi-token slot, optional alias, IOP token 기반 호출, Seulgi 포함과 implicit rotation 제외를 확정했다.

## 작업 컨텍스트

- 표준선: Control Plane DB가 identity/credential metadata의 원장이고 Edge는 secret-free projection을 소비한다. credential-bearing management와 runtime 경로는 authenticated confidential transport를 전제로 하며 실행 target은 scope/revision/expiry가 제한된 request-local lease만 사용한다. Edge-local provider health/capacity/runtime state의 원장은 계속 Edge다.
- 후속 SDD: 없음
