<!-- task=m-principal-provider-credential-slot-routing/09+08_migration_attribution plan=1 tag=API milestone-task=compat-migration,slot-attribution -->

# Credential-plane migration qualification과 slot attribution 계획

## For the Implementing Agent

`CODE_REVIEW-*-G??.md` 구현 담당 섹션을 반드시 채운다. fresh verification의 actual stdout/stderr를 기록하고 active 파일을 유지한 채 review 준비 완료를 보고한다. 구현자는 사용자 질문, next-state 분류, archive, `complete.log`를 수행하지 않는다.

## Background

08은 네 보안 작업을 완료할 수 있도록 enabled mode의 최소 안전 활성화 원칙을 구현한다. 이 작업은 그 상태 전환을 전체 legacy/managed source와 reload/rollback 조합에서 검증·보강하고, 선택된 credential slot ref/revision을 inbound IOP `token_ref`와 분리된 immutable provider-attempt attribution으로 완성한다.

## Replan Evidence

- `plan_cloud_G09_0.log`와 `code_review_cloud_G10_0.log`는 최초 계획/미구현 stub이다.
- 최초 계획은 08을 dormant 상태로 두고 09에서 처음 production activation하려 해 08 Roadmap completion과 모순됐다. 수정본은 08의 안전 활성화 contract를 선행조건으로 삼고, 09가 migration/rollback qualification과 attribution을 독립 PASS oracle로 마감한다.
- expected-no-match `rg`를 guarded zero-exit 명령으로 수정했다.

## Archive Evidence Snapshot

- `agent-task/m-principal-provider-credential-slot-routing/08+03,04,07_secure_delivery/complete.log`: 현재 missing; 구현 전 exact path가 필요하다.
- 03/04/07 evidence는 08을 통해 transitively 소비하며 별도 archive 탐색을 하지 않는다.
- prior unimplemented pair: `plan_cloud_G09_0.log`, `code_review_cloud_G10_0.log`.

## 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/chat_handler.go`
- `apps/edge/internal/openai/responses_handler.go`
- `apps/edge/internal/openai/anthropic_handler.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/edge-smoke.md`
- `agent-test/local/platform-common-smoke.md`

### SDD Criteria

- SDD path: `agent-roadmap/sdd/operational-observability-provider-management/principal-provider-credential-slot-routing/SDD.md`; status: 승인.
- S14: disabled/omitted mode is current legacy-only behavior; enabled mode is Control Plane projection/lease-only; dual source and IOP-token-as-provider-token are rejected; Responses schema is unchanged; rollback is explicit and restart-safe.
- S15: same principal/model with two slots produces distinct safe slot attribution while inbound `token_ref`, alias, lease id and raw secret remain separate/absent.
- Evidence Map S14-S15 rows가 migration table, all-surface schema fixtures, two-slot metric gathering과 label allowlist를 checklist/final verification에 고정한다.

### Verification Context

- 별도 handoff는 없었다. repository-native fallback evidence는 위 config/surface/metric tests와 local edge/platform profiles다.
- Current host is Linux/aarch64, Go `1.26.2`; no external provider/credential is needed.
- Fresh config/process fixtures use `-count=1`; attribution concurrency uses `-race`. Current dirty worktree is preserved and review scopes claims to exact affected files/packages.
- confidence: high after 08 PASS.

### Test Coverage Gaps

- Existing auth inference is tied to projection presence, not an exhaustively tested explicit mode state machine.
- Source-conflict and reload/restart-required classification does not cover every legacy field and secure prerequisite.
- Chat/Responses/Anthropic lack one matrix proving managed rejection, disabled compatibility, no token reuse and response-schema stability.
- metrics carry principal/token/provider/model but not trusted slot ref/revision, and retry/terminal semantics lack two-slot oracle.

### Symbol References

- No rename/remove. Additive `RunDispatch` fields must be populated only from trusted lease scope.
- Private Prometheus label vectors require every constructor/`WithLabelValues` call and test expectation to change together.
- `credential_slot_ref` is stable safe UUID-like reference required by S15; alias/lease/secret/target URL never become labels.

### Split Judgment

- stable contract is explicit two-state source selection plus immutable provider-attempt attribution. Config matrix and Prometheus gathering are independent deterministic PASS evidence after 08.
- 08 `complete.log` is missing and blocks implementation. 10 waits for this directory.

### Scope Rationale

- TLS, lease crypto, Node sealing/injection and revocation mechanics are 08-owned and not reimplemented.
- 09 may harden the 08 activation gate where matrix tests reveal incomplete source/reload handling, but must not introduce a second mode switch.
- no public request selector/response field or durable billing ledger is added.

### Final Routing

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

## Implementation Checklist

- [ ] Complete one explicit disabled-legacy/enabled-Control-Plane-only state machine and restart/apply classification without a second activation mechanism.
- [ ] Prove dual-source and caller provider-auth rejection in enabled mode while preserving disabled behavior and Chat/Responses/Anthropic public schemas.
- [ ] Carry immutable trusted credential slot ref/revision through dispatch, retry and provider-attempt usage separately from inbound token ref.
- [ ] Add fresh migration/rollback/all-surface/two-slot/race and label/secret audits.
- [ ] Fill implementation-owned sections in CODE_REVIEW-*-G??.md with actual implementation notes and verification output.

### [API-1] Exhaustive migration and rollback state machine

**Problem:** 08 supplies the minimum secure activation gate, but S14 requires complete omitted/false/true, source-conflict, reload, restart and rollback behavior across existing legacy fields.

**Solution:** Keep a single `credential_plane.enabled` setting. Disabled/omitted constructs exactly the legacy principal/provider-auth path and never uses CP projection/lease. Enabled requires all 08 secure dependencies, fresh projection and managed service composition; rejects `openai.bearer_token`, `principal_tokens[]`, `provider_auth.enabled` and any other raw provider source before listeners. Mode/source/TLS/key changes are restart-required on refresh and never partially merged. Rollback is a deliberate restart into a validated legacy-only config, not runtime fallback after CP failure.

Before (`packages/go/config/edge_types.go:82-89`):

```go
type EdgeControlPlaneConf struct {
	Enabled bool `mapstructure:"enabled" yaml:"enabled"`
	WireAddr string `mapstructure:"wire_addr" yaml:"wire_addr"`
	ReconnectIntervalSec int `mapstructure:"reconnect_interval_sec" yaml:"reconnect_interval_sec"`
}
```

After:

```go
type CredentialPlaneConf struct {
	Enabled bool `mapstructure:"enabled" yaml:"enabled"`
}
```

**Modified Files and Checklist:**

- [ ] Complete the state table in `packages/go/config/edge_types.go`, `packages/go/config/load.go`, `packages/go/config/validate.go`, and `packages/go/config/credential_plane_config_test.go` without duplicating 08 switches.
- [ ] Complete source selection/refresh classification in `apps/edge/internal/bootstrap/runtime.go`, `apps/edge/internal/bootstrap/credential_plane_mode_test.go`, and `apps/edge/internal/input/manager.go`.
- [ ] Keep `configs/edge.yaml` disabled by default with separate secret-free legacy and managed examples.
- [ ] Add config/bootstrap/input tables for omitted/false/true, each legacy source, combinations, missing secure dependency, CP unavailable/stale projection, restart reload and rollback.

**Test Strategy:** compare disabled fixtures to the pre-managed auth/dispatch result; enabled invalid combinations fail before listeners; CP outage never falls back; rollback works only after valid restart.

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

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

**Problem:** generic caller provider authorization can compete with the managed source and public surface regressions could be hidden by unit-only config tests.

**Solution:** In enabled mode reject legacy provider-auth headers before route/provider/lease work with a surface-native non-echoing 400. IOP `Authorization`/`x-api-key` remains principal authentication only. Disabled mode retains current provider header behavior. Keep model discovery, Chat, Responses JSON/SSE, and Anthropic Messages response shapes unchanged.

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

```go
raw := strings.TrimSpace(r.Header.Get(auth.FromHeader))
if raw == "" {
	if auth.Required {
		return nil, errProviderAuthRequired
	}
	return nil, nil
}
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/principal.go`, `apps/edge/internal/openai/provider_tunnel.go`, and `apps/edge/internal/openai/dispatch_context.go` only where the 08 gate needs complete surface mapping.
- [ ] Add `credential_migration_test.go` across Chat/Responses/Anthropic in both modes, dual headers, no token reuse and zero downstream calls on rejection.
- [ ] Extend `apps/edge/internal/openai/identity_metering_test.go` and `apps/edge/internal/openai/principal_routes_test.go` with expiry/no-fallback and exact Responses schema fixtures.

**Test Strategy:** sentinel IOP/provider tokens must never appear in output/log. Enabled cases make zero fake service/upstream calls; disabled cases deliver the configured legacy header exactly once. Compare JSON/SSE fields to approved fixtures.

**Verification:** targeted named tests followed by full `./apps/edge/internal/openai`.

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

**Problem:** `RunDispatch` and attempt metrics cannot distinguish two provider credentials for one provider/model.

**Solution:** Copy `CredentialSlotRef` and `CredentialRevision` from the verified lease scope into selected-candidate binding and `RunDispatch` before queue/recovery. Add safe `credential_slot_ref` and decimal `credential_revision` only to provider-attempt metrics/structured dispatch observation. Keep request-terminal cardinality/once semantics and inbound `token_ref` unchanged. Retry retains the frozen slot unless a new independently admitted dispatch is created; there is no implicit slot failover.

Before (`apps/edge/internal/service/run_types.go:47-67`):

```go
type RunDispatch struct {
	ProviderID string
	UsageAttribution string
	ProfileID string
}
```

After:

```go
type RunDispatch struct {
	ProviderID string
	UsageAttribution string
	ProfileID string
	CredentialSlotRef string
	CredentialRevision uint64
}
```

**Modified Files and Checklist:**

- [ ] Extend `apps/edge/internal/service/run_types.go`, `apps/edge/internal/service/provider_tunnel.go`, and `apps/edge/internal/service/usage_attribution_dispatch_test.go`.
- [ ] Update `apps/edge/internal/openai/usage_metrics.go` and `apps/edge/internal/openai/usage_metrics_test.go` atomically.
- [ ] Put two-slot same-principal/model/provider, retry, cancellation and terminal-dedup cases in the two exact attribution test files above.
- [ ] Update `docs/openai-usage-grafana.md` label tables/queries and prohibited-label guidance.

**Test Strategy:** gather metric families after two distinct managed routes. Assert series split only on safe slot dimensions, totals/retries/terminal counts are correct, and label-name allowlist excludes alias, lease id, secret and target URL.

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

## 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/qualify API-1, API-2, then API-3.
3. 10 waits for this directory's `complete.log`.

## 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, API-2 |
| `apps/edge/internal/openai/provider_tunnel.go` | API-2 |
| `apps/edge/internal/openai/dispatch_context.go` | API-2, API-3 |
| `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` | all |

## Final Verification

```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/...
git diff --check
rg --sort path -n 'credential_slot_ref|credential_revision|token_ref' apps/edge/internal/openai apps/edge/internal/service docs/openai-usage-grafana.md
if rg --sort path -n '"(slot_alias|lease_id|provider_secret|target_url)"' apps/edge/internal/openai/usage_metrics.go; then exit 1; fi
```

Expected: tests/race/vet/diff pass; identity audit shows distinct intended fields; guarded forbidden-label scan has no match. Record every intentional label occurrence and actual output.

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