<!-- task=m-principal-provider-credential-slot-routing/09+08_migration_attribution plan=0 tag=API -->

# Credential-plane migration mode와 slot attribution 계획

## For the Implementing Agent

`CODE_REVIEW-*-G??.md` 구현 담당 섹션을 반드시 채운다. 모든 검증을 실행해 실제 notes/stdout/stderr를 기록하고 active 파일을 유지한 채 review 준비 완료를 보고한다. 막히면 blocker, 시도한 명령/출력, 재개 조건만 evidence에 남긴다. 사용자 질문, user-input, control-plane stop 파일, 상태 분류, archive, `complete.log`는 구현자가 수행하지 않는다.

## Background

08의 secure delivery는 default-off contract로 남아 legacy deployment를 깨지 않는다. 이 작업은 명시적인 `credential_plane.enabled` 전환 규칙을 production composition에 적용하여 enabled mode에서 Control Plane projection/lease만 허용하고, 동시에 선택 slot/ref revision을 inbound IOP `token_ref`와 분리된 immutable dispatch/usage dimension으로 만든다.

## Archive Evidence Snapshot

- `agent-task/m-principal-provider-credential-slot-routing/08+03,04,07_secure_delivery/complete.log`: 현재 missing이다. 구현 전 이 exact path가 있어야 TLS/lease/injection/revocation invariant가 완료된 것으로 본다.
- 과거 03/04 archive는 08을 통해 transitively 소비한다. 구현자는 archive를 재탐색하지 않는다.

## 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:
  - `compat-migration`: legacy-only와 CP-only credential source 전환/rollback
  - `slot-attribution`: safe slot ref/revision dispatch와 usage 귀속
- Completion mode: check-on-pass

## Analysis

### Files Read

- `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-contract/inner/edge-config-runtime-refresh.md`
- `agent-contract/outer/openai-compatible-api.md`
- `agent-contract/outer/anthropic-compatible-api.md`
- `agent-spec/runtime/provider-pool-config-refresh.md`
- `agent-spec/input/openai-compatible-surface.md`
- `packages/go/config/edge_types.go`
- `packages/go/config/load.go`
- `packages/go/config/validate.go`
- `apps/edge/internal/bootstrap/runtime.go`
- `apps/edge/internal/input/manager.go`
- `apps/edge/internal/openai/principal.go`
- `apps/edge/internal/openai/principal_routes.go`
- `apps/edge/internal/openai/dispatch_context.go`
- `apps/edge/internal/openai/provider_tunnel.go`
- `apps/edge/internal/openai/usage_metrics.go`
- `apps/edge/internal/openai/usage_metrics_test.go`
- `apps/edge/internal/openai/identity_metering_test.go`
- `apps/edge/internal/openai/principal_routes_test.go`
- `apps/edge/internal/service/run_types.go`
- `apps/edge/internal/service/provider_tunnel.go`
- `apps/edge/internal/service/usage_attribution_dispatch_test.go`
- `docs/openai-usage-grafana.md`
- `configs/edge.yaml`
- `agent-test/local/rules.md`
- `agent-test/local/profiles/edge-smoke.md`
- `agent-test/local/profiles/platform-common-smoke.md`

### SDD Criteria

- 승인 SDD S14/`compat-migration`, S15/`slot-attribution`과 Evidence Map S14-S15를 대상으로 한다.
- S14는 disabled mode의 기존 `principal_tokens[]`/`provider_auth` 완전 호환, enabled mode의 CP-only source, dual-source startup/apply rejection, IOP token/provider token 비재사용, Responses schema 보존을 요구한다.
- S15는 같은 principal/model의 서로 다른 slot이 `credential_slot_ref`와 revision으로 provider usage에 분리되고 inbound `token_ref`, alias, raw secret과 혼합되지 않아야 한다.
- 이 매핑에서 migration matrix, config refresh classification, all three HTTP surfaces, two-slot attempt metrics와 label allowlist를 checklist와 verification으로 역산했다.

### Verification Context

- handoff는 없었다. config load/validation, bootstrap, managed principal route와 usage recorder의 현재 구현 및 local profiles를 repository-native evidence로 사용했다.
- current host는 Linux/aarch64, Go `1.26.2`. 외부 provider나 credential 없이 deterministic unit/integration tests로 완료할 수 있다.
- 08 predecessor가 PASS한 checkout이 precondition이다. cached test output은 허용하지 않고 `-count=1`; attribution state/race는 `-race`를 사용한다.
- gap: 현재 branch가 dirty이므로 검증은 unrelated diff가 없다는 주장 대신 exact target package 결과와 allowlist search를 evidence로 남긴다.
- confidence: high after 08 completion.

### Test Coverage Gaps

- 현재 `resolvePrincipal`은 projection unmanaged일 때 static token/legacy fallback을 자동 사용한다. explicit mode가 없고 dual-source validation이 없다.
- `providerTunnelAuthHeaders`는 caller raw provider token을 계속 generic headers에 전달한다. enabled mode의 explicit rejection test가 없다.
- config refresh는 credential-plane toggle/source conflicts를 분류하지 않는다.
- usage labels에는 `principal_ref`, `token_ref`, provider/model은 있지만 `credential_slot_ref`/revision이 없으므로 two-slot 분리 oracle이 없다.
- Responses/Anthropic managed route tests는 route binding을 검증하지만 migration rollback과 public schema non-regression을 함께 검증하지 않는다.

### Symbol References

- rename/remove는 없다.
- `RunDispatch` additive fields are consumed only by `newUsageDispatchBinding` and structured logs; existing composite literals remain source-compatible.
- label-vector changes require every `WithLabelValues` call in `apps/edge/internal/openai/usage_metrics.go` and expectations in `usage_metrics_test.go`; repository search found no other direct construction of these private label slices.

### Split Judgment

- stable child contract는 “disabled=legacy only, enabled=CP only, ambiguous/dual source=reject”라는 config/runtime state machine과 이에 종속된 immutable slot attribution이다.
- independent PASS evidence는 config migration table, all-surface dispatch fixtures, Responses schema regression, two-slot Prometheus metric gathering과 label allowlist다.
- predecessor `08`의 `agent-task/m-principal-provider-credential-slot-routing/08+03,04,07_secure_delivery/complete.log`는 missing이다. 이 path가 생기기 전 구현하지 않는다.
- 10은 이 mode와 label 계약 위에서 deterministic/live smoke 및 canonical docs를 완성한다.

### Scope Rationale

- TLS, lease cryptography, dedicated secret payload, revocation race 구현은 08 소유이며 재구현하지 않는다.
- 새로운 public request field, slot alias selector, Responses response field는 추가하지 않는다. request `model`이 이미 route/slot 선택의 유일한 외부 selector다.
- alias, lease id, target URL, raw credential은 metric label에서 제외한다. request-level durable ledger와 billing/chargeback은 범위 밖이다.
- migration은 silent auto-detect가 아니라 explicit bool과 fail-closed validation만 사용한다.

### Final Routing

- evaluation_mode `first-pass`; finalizer `finalize-task-policy.sh` pair.
- build closures 모두 true; scores `2,2,2,1,2` = G09, base/route `grade-boundary`; `large_indivisible_context=false`; positive risks `temporal_state`, `concurrent_consistency`, `boundary_contract`, `variant_product` (4); recovery 0/false; `PLAN-cloud-G09.md`.
- review closures 모두 true; scores `2,2,2,2,2` = G10; route `official-review`, `CODE_REVIEW-cloud-G10.md`, Codex `gpt-5.6-sol` xhigh.
- capability gap은 없다.

## Implementation Checklist

- [ ] Add an explicit credential-plane mode with strict disabled-legacy/enabled-Control-Plane-only configuration and restart/apply classification.
- [ ] Reject dual sources and caller-supplied provider auth before route/provider dispatch in enabled mode while preserving disabled-mode compatibility and Responses schemas.
- [ ] Carry immutable credential slot ref/revision through dispatch and provider-attempt usage without conflating inbound token ref or exposing alias/secret/lease ids.
- [ ] Add fresh migration/rollback/all-surface/two-slot attribution tests, race coverage, and deterministic label/secret audits.
- [ ] Fill implementation-owned sections in CODE_REVIEW-*-G??.md with actual implementation notes and verification output.

### [API-1] Explicit migration state and fail-closed source selection

**Problem:** `EdgeConfig` at `packages/go/config/edge_types.go:30-62` has no credential-plane state, while `authenticatePrincipal` at `apps/edge/internal/openai/principal.go:194-219` selects managed vs legacy solely from whether a cache has ever received a snapshot.

**Solution:** Add `credential_plane.enabled` plus bounded lease knobs to `EdgeConfig`. Disabled mode constructs the old unmanaged behavior exactly. Enabled mode requires secure CP and Node transport from 08, installs the managed cache/lease provider, requires a fresh projection before cloud dispatch, and rejects `openai.bearer_token`, `principal_tokens[]`, `provider_auth.enabled`, or incomplete TLS as config errors. Toggle/source changes are restart-required, never live-merged.

Before (`apps/edge/internal/openai/principal.go:194-219`):

```go
projection := s.PrincipalProjection()
if projection != nil {
	view, matched := projection.AuthenticatedView(digest)
	if view.State != authprojection.StateUnmanaged { /* managed */ }
}
/* static fallback */
```

After:

```go
switch s.credentialMode {
case credentialModeManaged:
	return authenticateManagedOnly(projection, token)
case credentialModeLegacy:
	return authenticateLegacyOnly(s.cfg, token)
default:
	return openAIPrincipal{}, authprojection.AuthenticatedView{}, false
}
```

**Modified Files and Checklist:**

- [ ] Extend `packages/go/config/edge_types.go`, `packages/go/config/load.go`, and `packages/go/config/validate.go` with explicit mode/default/bounds and dual-source/TLS validation.
- [ ] Update `apps/edge/internal/bootstrap/runtime.go` and `apps/edge/internal/input/manager.go` to choose exactly one source and fail startup before listeners.
- [ ] Update `apps/edge/internal/openai/principal.go` for explicit mode rather than cache-state inference.
- [ ] Update `configs/edge.yaml` with disabled default and secret-free migration examples.
- [ ] Add `packages/go/config/credential_plane_config_test.go` and `apps/edge/internal/bootstrap/credential_plane_mode_test.go` for legacy, enabled, conflict, missing TLS and rollback matrices.

**Test Strategy:** Table-test omitted/false/true, each legacy source independently and combined, CP unavailable, stale projection, rollback restart, and reload. Compare disabled fixtures to current auth/dispatch results byte-for-byte where applicable.

**Verification:** `go test -count=1 ./packages/go/config ./apps/edge/internal/bootstrap ./apps/edge/internal/input` must pass.

### [API-2] Enabled-mode caller credential rejection and surface compatibility

**Problem:** `providerTunnelAuthHeaders` at `apps/edge/internal/openai/provider_tunnel.go:158-182` reads `X-IOP-Provider-Authorization` and builds a generic provider header. Enabled mode must not ignore or forward this competing secret source.

**Solution:** Gate provider authorization by explicit mode. In managed mode, detect the legacy provider-auth header before route resolution and return the surface-native 400 error without echoing the value; the IOP `Authorization`/`x-api-key` remains only principal auth. Disabled mode retains current provider-auth behavior exactly. Keep Chat, Responses and Anthropic body/stream response schemas unchanged.

Before (`apps/edge/internal/openai/provider_tunnel.go:164-181`):

```go
raw := strings.TrimSpace(r.Header.Get(auth.FromHeader))
if raw == "" { /* required handling */ }
return map[string]string{auth.TargetHeader: value}, nil
```

After:

```go
if s.credentialMode == credentialModeManaged {
	if hasLegacyProviderCredential(r) { return nil, errCallerProviderCredentialRejected }
	return nil, nil
}
return s.legacyProviderTunnelAuthHeaders(r)
```

**Modified Files and Checklist:**

- [ ] Update `apps/edge/internal/openai/provider_tunnel.go` and `apps/edge/internal/openai/dispatch_context.go` for early explicit rejection and secret-free typed error mapping.
- [ ] Add `apps/edge/internal/openai/credential_migration_test.go` covering OpenAI Chat/Responses and Anthropic Messages in both modes, including dual headers and no IOP-token reuse.
- [ ] Extend `apps/edge/internal/openai/identity_metering_test.go` and `apps/edge/internal/openai/principal_routes_test.go` with mode-expiry/no-fallback/Responses schema assertions.

**Test Strategy:** Use sentinel raw provider and IOP tokens; assert enabled cases make zero fake service/upstream calls and sentinel never appears in response/log. Disabled fixtures must still deliver the configured legacy header. Compare Responses JSON/SSE shapes before/after.

**Verification:** `go test -count=1 ./apps/edge/internal/openai -run 'TestCredentialPlane|TestManaged|TestResponses'` must pass.

### [API-3] Immutable slot usage attribution

**Problem:** `RunDispatch` at `apps/edge/internal/service/run_types.go:48-69` lacks slot fields and `usageAttemptLabels` at `apps/edge/internal/openai/usage_metrics.go:199-205` can only separate principal token/provider/model, not two credentials for the same provider/model.

**Solution:** Carry `CredentialSlotRef` and `CredentialRevision` from trusted route scope through `SubmitProviderTunnelRequest`, selected-candidate binding, `RunDispatch`, structured dispatch log and attempt metric labels. Freeze them before queue/recovery and never recompute from caller metadata. Keep `token_ref` as the inbound IOP token. Add only safe `credential_slot_ref` and decimal `credential_revision` labels; exclude slot alias, lease id and credential value.

Before (`apps/edge/internal/openai/usage_metrics.go:199-205`):

```go
type usageAttemptLabels struct {
	usageRequestLabels
	usageAttribution string
	providerID       string
	servedModel      string
	responseMode     string
}
```

After:

```go
type usageAttemptLabels struct {
	usageRequestLabels
	credentialSlotRef, credentialRevision string
	usageAttribution, providerID, servedModel, responseMode string
}
```

**Modified Files and Checklist:**

- [ ] Extend `apps/edge/internal/service/run_types.go` and `apps/edge/internal/service/provider_tunnel.go` with immutable slot ref/revision copied from trusted lease scope.
- [ ] Update `apps/edge/internal/openai/usage_metrics.go` for safe label allowlists and per-attempt binding.
- [ ] Extend `apps/edge/internal/service/usage_attribution_dispatch_test.go` and `apps/edge/internal/openai/usage_metrics_test.go` with two-slot same-model/provider cases, retries and request-terminal deduplication.
- [ ] Update `docs/openai-usage-grafana.md` label tables/queries to distinguish inbound `token_ref` from provider credential slot and prohibit alias/secret/lease id.

**Test Strategy:** Gather Prometheus families after two managed route calls. Assert two series differ only by slot/ref revision as expected, totals are correct, retries stay with frozen slot, request terminal remains once, and every label name is in the allowlist.

**Verification:** `go test -count=1 -race ./apps/edge/internal/service ./apps/edge/internal/openai` must pass.

## Dependencies and Execution Order

1. Do not begin until `agent-task/m-principal-provider-credential-slot-routing/08+03,04,07_secure_delivery/complete.log` exists.
2. Implement API-1 first, API-2 against the explicit mode, then API-3 against trusted lease dispatch.
3. 10 waits for this directory's `complete.log` before qualification/docs finalization.

## Modified Files Summary

| File | Items |
|------|-------|
| `packages/go/config/edge_types.go` | API-1 |
| `packages/go/config/load.go` | API-1 |
| `packages/go/config/validate.go` | API-1 |
| `packages/go/config/credential_plane_config_test.go` | API-1 |
| `apps/edge/internal/bootstrap/runtime.go` | API-1 |
| `apps/edge/internal/bootstrap/credential_plane_mode_test.go` | API-1 |
| `apps/edge/internal/input/manager.go` | API-1 |
| `apps/edge/internal/openai/principal.go` | API-1 |
| `apps/edge/internal/openai/provider_tunnel.go` | API-2 |
| `apps/edge/internal/openai/dispatch_context.go` | API-2 |
| `apps/edge/internal/openai/credential_migration_test.go` | API-2 |
| `apps/edge/internal/openai/identity_metering_test.go` | API-2 |
| `apps/edge/internal/openai/principal_routes_test.go` | API-2 |
| `apps/edge/internal/service/run_types.go` | API-3 |
| `apps/edge/internal/service/provider_tunnel.go` | API-3 |
| `apps/edge/internal/service/usage_attribution_dispatch_test.go` | API-3 |
| `apps/edge/internal/openai/usage_metrics.go` | API-3 |
| `apps/edge/internal/openai/usage_metrics_test.go` | API-3 |
| `docs/openai-usage-grafana.md` | API-3 |
| `configs/edge.yaml` | API-1 |
| `agent-task/m-principal-provider-credential-slot-routing/09+08_migration_attribution/CODE_REVIEW-cloud-G10.md` | API-1, API-2, API-3 |

## Final Verification

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

```bash
command -v go && go version && go env GOROOT
git status --short --branch
go test -count=1 ./packages/go/config ./apps/edge/internal/bootstrap ./apps/edge/internal/input
go test -count=1 ./apps/edge/internal/openai ./apps/edge/internal/service
go test -count=1 -race ./apps/edge/internal/openai ./apps/edge/internal/service
go vet ./apps/edge/... ./packages/go/...
rg --sort path -n 'credential_slot_ref|credential_revision|token_ref' apps/edge/internal/openai apps/edge/internal/service docs/openai-usage-grafana.md
rg --sort path -n '"(slot_alias|lease_id|provider_secret)"' apps/edge/internal/openai/usage_metrics.go
```

Expected: tests/vet/race pass; first audit shows three distinct identities with intended use; second audit exits 1 with no forbidden metric-label match. After completing all code changes, fill implementation-owned sections in `CODE_REVIEW-*-G??.md`.
