<!-- task=m-usage-token-log-ops-mvp/01_identity_alias_auth plan=0 tag=USAGE_ID -->

# Plan - USAGE_ID

## 이 파일을 읽는 구현 에이전트에게

`CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채우는 것이 마지막 단계다. 구현 후 active 파일을 유지하고 리뷰 준비 상태로 보고한다. 선택된 Milestone `구현 잠금 > 결정 필요` 항목이 실구현을 막을 때만 review stub의 `사용자 리뷰 요청`을 채우고 멈춘다. 직접 사용자에게 질문하거나 `USER_REVIEW.md`, `complete.log`, archive log를 만들지 않는다. 환경/secret/service 차단과 검증 증거 공백은 일반 후속 이슈로 기록한다.

## 배경

현재 OpenAI-compatible auth는 단일 `openai.bearer_token` 문자열 일치만 수행한다. 이번 Milestone은 `metadata.user`가 아니라 IOP bearer token 운영 매핑에서 `principal_ref`, 내부 alias, `token_ref`를 파생해야 한다. 이 plan은 raw token을 저장하지 않는 config schema와 request context propagation을 먼저 만든다.

## 사용자 리뷰 요청 흐름

구현 중 직접 사용자 프롬프트를 띄우지 않는다. 선택된 Milestone lock decision만 active review stub의 `사용자 리뷰 요청` 섹션에 기록하며, code-review가 검증 후 실제 `USER_REVIEW.md` 작성 여부를 결정한다.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/operational-observability-provider-management/milestones/usage-token-log-ops-mvp.md`
- Milestone link: [Milestone 문서](agent-roadmap/phase/operational-observability-provider-management/milestones/usage-token-log-ops-mvp.md)
- Task ids:
  - `identity-scope`: OpenAI-compatible 호출 주체는 IOP bearer token 운영 매핑으로 판별한다.
  - `token-alias`: `token_ref -> principal_ref/internal_alias` 매핑을 raw token 없이 관리한다.
- Completion mode: check-on-pass

## 분석 결과

### 읽은 파일

- `agent-roadmap/phase/operational-observability-provider-management/PHASE.md`
- `agent-roadmap/phase/operational-observability-provider-management/milestones/usage-token-log-ops-mvp.md`
- `agent-roadmap/sdd/operational-observability-provider-management/usage-token-log-ops-mvp/SDD.md`
- `agent-contract/outer/openai-compatible-api.md`
- `agent-contract/inner/edge-config-runtime-refresh.md`
- `agent-spec/input/openai-compatible-surface.md`
- `agent-spec/runtime/provider-pool-config-refresh.md`
- `agent-ops/rules/project/domain/edge/rules.md`
- `agent-ops/rules/project/domain/platform-common/rules.md`
- `agent-ops/rules/project/domain/testing/rules.md`
- `agent-test/local/rules.md`
- `agent-test/local/edge-smoke.md`
- `agent-test/local/platform-common-smoke.md`
- `packages/go/config/config.go`
- `packages/go/config/config_test.go`
- `configs/edge.yaml`
- `apps/edge/internal/openai/routes.go`
- `apps/edge/internal/openai/server.go`
- `apps/edge/internal/openai/chat_handler.go`
- `apps/edge/internal/openai/responses_handler.go`
- `apps/edge/internal/openai/server_test.go`

### SDD 기준

- SDD: `agent-roadmap/sdd/operational-observability-provider-management/usage-token-log-ops-mvp/SDD.md`, 상태 `[승인됨]`, SDD 잠금 `해제`.
- 대상 Acceptance Scenario: S01(`identity-scope`), S02(`token-alias`).
- Evidence Map: S01은 bearer token 기반 alias 판별 test/code evidence, S02는 raw token 미저장과 `principal_ref`/alias/`token_ref` 산출 test/config evidence를 요구한다.
- 구현 checklist는 S01/S02에서 역산해 config schema, constant-time hash match, request context propagation, no-`metadata.user` test로 제한한다.

### 테스트 환경 규칙

- test_env: `local`.
- 읽은 규칙: `agent-test/local/rules.md`, `agent-test/local/edge-smoke.md`, `agent-test/local/platform-common-smoke.md`.
- 적용 명령: `go test -count=1 ./packages/go/config ./apps/edge/internal/openai`, `go test -count=1 ./apps/edge/...`, `git diff --check`.
- 외부 provider/remote runner는 사용하지 않는다. full-cycle Edge/Node 구동은 이 plan의 auth/config 단위 PASS 조건이 아니라 02 usage metric plan의 runtime 검증에서 다룬다.

### 테스트 커버리지 공백

- config: `TestLoadEdge_OpenAIDefaults`와 `TestLoadEdge_OpenAIOverride`가 legacy bearer token만 검증한다. `principal_tokens` load/validation test가 없다.
- auth: `TestRoutesRequireBearerTokenWhenConfigured`는 raw `BearerToken`만 검증한다. hash 기반 token mapping과 principal context test가 없다.
- dispatch metadata: chat/responses/tunnel request에 principal context가 들어가는 test가 없다.

### 심볼 참조

- renamed/removed symbol: none.
- 새 helper 후보: `openAIPrincipal`, `principalFromRequest`, `principalContextFromRequest`, `principalMetadata`.

### 분할 판단

- split policy를 먼저 적용했다.
- shared task group: `m-usage-token-log-ops-mvp`.
- sibling subtasks:
  - `01_identity_alias_auth`: 독립 선행 작업. config/auth/principal context.
  - `02+01_usage_metrics`: 01의 principal labels가 필요하다. predecessor `01`은 아직 `complete.log` 없음.
  - `03+02_grafana_rollup_docs`: 02의 metric names/labels가 필요하다. predecessor `02`는 아직 `complete.log` 없음.
- 이 plan은 단일 ownership boundary가 config/auth/openai input surface에 한정되고, proto/metric emit은 후속 subtask로 분리되어 단일 plan이 적절하다.

### 범위 결정 근거

- 제외: Prometheus collectors, proto `Usage` 확장, Node adapter usage parsing, Grafana docs. 모두 02/03에서 처리한다.
- 제외: 사용자 CRUD/tenant/org/RBAC, token limit enforcement, `metadata.user` support. Milestone 범위 제외와 SDD 금지사항이다.
- 유지: legacy `openai.bearer_token`은 backward compatibility auth로 유지하되 principal context는 `unknown` 또는 empty로 둔다.

### 빌드 등급

- `cloud-G05`: config schema와 auth/security 경계를 함께 바꾸며 외부 API auth 동작에 영향이 있다. 범위는 작지만 잘못 구현하면 secret/raw token 노출이나 auth regression이 생긴다.

## 구현 체크리스트

- [ ] `packages/go/config`에 raw token 없는 OpenAI principal token mapping schema와 validation을 추가한다.
- [ ] `configs/edge.yaml`에 secret 없는 예시와 운영 주석을 추가한다.
- [ ] `apps/edge/internal/openai` auth를 hash 기반 principal mapping 우선, legacy bearer fallback으로 재구성하고 request context metadata를 만든다.
- [ ] Chat Completions, Responses, provider tunnel dispatch metadata에 `principal_ref`, `principal_alias`, `token_ref`, source를 추가하고 `metadata.user`를 사용하지 않는 test를 추가한다.
- [ ] `go test -count=1 ./packages/go/config ./apps/edge/internal/openai`와 `go test -count=1 ./apps/edge/...`를 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [USAGE_ID-1] Principal Token Config

문제: [config.go](/config/workspace/iop/packages/go/config/config.go:376)의 `EdgeOpenAIConf`는 raw `BearerToken`만 가진다. [LoadEdge](/config/workspace/iop/packages/go/config/config.go:609)는 OpenAI token alias mapping을 검증하지 않는다.

해결 방법:

```go
// before packages/go/config/config.go:376
type EdgeOpenAIConf struct {
    BearerToken string `mapstructure:"bearer_token" yaml:"bearer_token"`
}

// after
type OpenAIPrincipalTokenConf struct {
    TokenRef        string `mapstructure:"token_ref" yaml:"token_ref"`
    TokenHashSHA256 string `mapstructure:"token_hash_sha256" yaml:"token_hash_sha256"`
    PrincipalRef    string `mapstructure:"principal_ref" yaml:"principal_ref"`
    PrincipalAlias  string `mapstructure:"principal_alias" yaml:"principal_alias,omitempty"`
}
type EdgeOpenAIConf struct {
    BearerToken     string                   `mapstructure:"bearer_token" yaml:"bearer_token"`
    PrincipalTokens []OpenAIPrincipalTokenConf `mapstructure:"principal_tokens" yaml:"principal_tokens,omitempty"`
}
```

수정 파일 및 체크리스트:
- [ ] `packages/go/config/config.go`: struct 추가, `validateOpenAIPrincipalTokens` 추가, `LoadEdge`에서 호출.
- [ ] `packages/go/config/config_test.go`: valid mapping, duplicate `token_ref`, empty principal, non-hex/non-64 hash, raw-looking token field 없음 테스트.
- [ ] `configs/edge.yaml`: `principal_tokens` 예시 주석 추가. raw token 값은 넣지 않는다.

테스트 작성:
- `TestLoadEdge_OpenAIPrincipalTokens`와 `TestLoadEdge_OpenAIPrincipalTokensRejectInvalid` 추가.

중간 검증:

```bash
go test -count=1 ./packages/go/config
```

### [USAGE_ID-2] Auth Principal Context

문제: [routes.go](/config/workspace/iop/apps/edge/internal/openai/routes.go:20)는 `openai.bearer_token` 문자열만 비교하고, 주체 정보를 request context에 남기지 않는다.

해결 방법:
- `principal.go`를 추가해 Authorization bearer 추출, SHA-256 hash 계산, constant-time hash compare, request context 저장 helper를 둔다.
- `principal_tokens`가 있으면 hash mapping으로 인증한다. 없으면 legacy `BearerToken`을 유지한다.
- raw token, provider token, prompt/response를 logger/metadata에 넣지 않는다.

수정 파일 및 체크리스트:
- [ ] `apps/edge/internal/openai/principal.go`: principal type/context/hash matching.
- [ ] `apps/edge/internal/openai/routes.go`: `withAuth`가 `principalFromBearer`를 호출하고 context를 주입.
- [ ] `apps/edge/internal/openai/identity_metering_test.go`: valid hash, missing/wrong token, legacy fallback, healthz no-auth 테스트.

테스트 작성:
- `TestRoutesResolvePrincipalTokenMapping`.
- `TestRoutesDoNotUseMetadataUserForPrincipal`.

중간 검증:

```bash
go test -count=1 ./apps/edge/internal/openai -run 'TestRoutes'
```

### [USAGE_ID-3] Dispatch Metadata Propagation

문제: [chat_handler.go](/config/workspace/iop/apps/edge/internal/openai/chat_handler.go:42)와 [responses_handler.go](/config/workspace/iop/apps/edge/internal/openai/responses_handler.go:50)는 caller metadata만 파싱한다. principal context가 SubmitRun/SubmitProviderTunnel metadata에 없다.

해결 방법:
- `principalMetadata(r.Context())`를 `runMeta`에 merge한다.
- metadata keys는 `iop_principal_ref`, `iop_principal_alias`, `iop_token_ref`, `iop_principal_source`로 둔다.
- `metadata.user`는 무시하거나 caller metadata로만 보존하지 말고 사용자 식별 source로 쓰지 않는 test를 둔다.

수정 파일 및 체크리스트:
- [ ] `apps/edge/internal/openai/chat_handler.go`: normalized/tunnel metadata에 principal metadata 추가.
- [ ] `apps/edge/internal/openai/responses_handler.go`: SubmitRun metadata에 principal metadata 추가.
- [ ] `apps/edge/internal/openai/identity_metering_test.go`: chat, responses, provider tunnel metadata 검증.

테스트 작성:
- `TestChatCompletionsAddsPrincipalMetadataFromBearerToken`.
- `TestProviderTunnelAddsPrincipalMetadataFromBearerToken`.
- `TestResponsesAddsPrincipalMetadataFromBearerToken`.

중간 검증:

```bash
go test -count=1 ./apps/edge/internal/openai -run 'Principal|Metadata'
```

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `packages/go/config/config.go` | USAGE_ID-1 |
| `packages/go/config/config_test.go` | USAGE_ID-1 |
| `configs/edge.yaml` | USAGE_ID-1 |
| `apps/edge/internal/openai/principal.go` | USAGE_ID-2, USAGE_ID-3 |
| `apps/edge/internal/openai/routes.go` | USAGE_ID-2 |
| `apps/edge/internal/openai/chat_handler.go` | USAGE_ID-3 |
| `apps/edge/internal/openai/responses_handler.go` | USAGE_ID-3 |
| `apps/edge/internal/openai/identity_metering_test.go` | USAGE_ID-2, USAGE_ID-3 |

## 최종 검증

```bash
go test -count=1 ./packages/go/config ./apps/edge/internal/openai
go test -count=1 ./apps/edge/...
git diff --check
```

모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.
