<!-- task=m-seulgivibe-openai-compatible-provider/01_config_auth_catalog plan=0 tag=SEULGI_CONFIG -->

# Implementation Plan - SEULGI_CONFIG

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

`CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채우는 것이 필수 완료 조건이다. 구현 후 검증을 실행하고, 실제 stdout/stderr를 붙이고, active 파일은 그대로 둔 뒤 review ready로 보고한다. selected Milestone `구현 잠금 > 결정 필요` 항목이 실구현을 막는 경우에만 review stub의 `사용자 리뷰 요청`에 정확한 근거를 기록하고 멈춘다. 구현 중 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 외부 secret/service 준비, 검증 증거 공백, 일반 범위 조정은 사용자 리뷰 요청이 아니라 후속 plan 또는 검증 기록으로 처리한다. finalization은 code-review-skill 전용이다.

## 배경

Seulgivibe Claude/OpenAI 경로는 OpenAI-compatible 요청 형식으로 동작하지만, IOP config에는 이를 별도 provider 축으로 관리할 명시적 타입과 사용자별 provider token 설정 지점이 없다. 이 plan은 구현 전 foundation만 만든다: provider type alias, Edge-level provider auth config schema, 정적 model catalog 예시/계약을 추가한다. 실제 요청 헤더 주입과 `/v1/responses` passthrough는 후속 split task에서 처리한다.

## 사용자 리뷰 요청 흐름

선택된 Milestone lock decision이 실구현을 차단할 때만 active review stub의 `사용자 리뷰 요청` 섹션에 기록한다. 구현 에이전트는 직접 사용자에게 질문하지 않고, code-review가 그 요청을 검증해 `USER_REVIEW.md` 작성 여부를 결정한다.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/routing-policy-model-orchestration/milestones/seulgivibe-openai-compatible-provider.md`
- Milestone link: [Milestone 문서](agent-roadmap/phase/routing-policy-model-orchestration/milestones/seulgivibe-openai-compatible-provider.md)
- Task ids:
  - `config-auth-catalog`: Seulgivibe Claude/OpenAI provider aliases, `openai.provider_auth` schema, 정적 model catalog/계약 예시
- Completion mode: check-on-pass

## 분석 결과

### 읽은 파일

- `agent-ops/rules/project/rules.md`
- `agent-ops/rules/common/rules-roadmap.md`
- `agent-ops/skills/common/router.md`
- `agent-ops/skills/common/plan/SKILL.md`
- `agent-ops/skills/common/_templates/implementation-user-review-request-section.md`
- `agent-test/local/rules.md`
- `agent-test/local/platform-common-smoke.md`
- `agent-test/local/edge-smoke.md`
- `agent-test/local/node-smoke.md`
- `agent-ops/rules/project/domain/platform-common/rules.md`
- `agent-ops/rules/project/domain/edge/rules.md`
- `agent-ops/rules/project/domain/node/rules.md`
- `agent-contract/index.md`
- `agent-contract/inner/edge-config-runtime-refresh.md`
- `agent-contract/outer/openai-compatible-api.md`
- `agent-spec/index.md`
- `agent-spec/runtime/provider-pool-config-refresh.md`
- `agent-spec/input/openai-compatible-surface.md`
- `packages/go/config/config.go`
- `packages/go/config/config_test.go`
- `apps/edge/internal/node/mapper.go`
- `apps/edge/internal/node/mapper_test.go`
- `proto/iop/runtime.proto`
- `apps/node/internal/runtime/types.go`
- `apps/node/internal/adapters/config_set.go`
- `apps/node/internal/adapters/openai_compat/openai_compat.go`
- `apps/node/internal/adapters/openai_compat/openai_compat_test.go`
- `apps/edge/internal/openai/routes.go`
- `apps/edge/internal/openai/chat_handler.go`
- `apps/edge/internal/openai/stream.go`
- `apps/edge/internal/openai/responses_handler.go`
- `apps/edge/internal/openai/types.go`
- `apps/edge/internal/openai/server_test.go`
- `apps/edge/internal/service/run_dispatch.go`

### SDD 기준

- SDD: [SDD.md](agent-roadmap/sdd/routing-policy-model-orchestration/seulgivibe-openai-compatible-provider/SDD.md)
- 상태: `[승인됨]`
- SDD 잠금: 해제
- 대상 Acceptance Scenario:
  - S01 -> `config-auth-catalog`: Seulgivibe provider alias가 `openai_compat` runtime으로 정규화되고 provider label을 보존한다.
  - S02 -> `config-auth-catalog`: static `models[]` catalog가 Seulgivibe model ids를 노출하고 served model membership을 검증한다.
- Evidence Map 반영: 이 plan의 config/mapper tests와 secret scan이 `config-auth-catalog` Roadmap Completion evidence가 된다.

### 테스트 환경 규칙

- `test_env=local`.
- `agent-test/local/rules.md`를 읽었다.
- 매칭 profile: `agent-test/local/platform-common-smoke.md`, `agent-test/local/edge-smoke.md`, `agent-test/local/node-smoke.md`.
- 이 subtask는 `packages/go/config/**`, `apps/edge/internal/node/**`, 계약 문서만 변경한다. 적용 명령은 `go test ./packages/go/config -count=1` 및 `go test ./apps/edge/internal/node -count=1`이다.
- local checkout 밖으로 나가는 필수 검증은 없다. live Seulgivibe 호출은 secret과 사내망에 의존하므로 이 subtask의 필수 검증이 아니다.

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

- `seulgivibe_claude`, `seulgivibe_openai` provider type alias: 기존 테스트 없음. 새 config/mapper 테스트 필요.
- `openai.provider_auth` schema/default/validation: 기존 테스트 없음. 새 config 테스트 필요.
- 정적 model catalog로 Seulgivibe 모델을 노출하는 예시/계약: 기존 문서 없음. 계약 또는 예시 업데이트 필요.

### 심볼 참조

renamed/removed symbol 없음. 새 symbol 후보: `EdgeOpenAIProviderAuthConf`, `ProviderAuth`.

### 분할 판단

split decision policy를 먼저 평가했다. shared task group은 `m-seulgivibe-openai-compatible-provider`다.

- `01_config_auth_catalog`: independent. config/schema/catalog foundation.
- `02+01_provider_tunnel_auth`: depends on 01. Edge request header를 provider tunnel header로 주입.
- `03+01,02_responses_passthrough`: depends on 01 and 02. `/v1/responses` raw passthrough parity.

이 plan은 01만 수행한다. 후속 predecessor는 아직 missing이며, 02/03 구현은 01 PASS 후 시작해야 한다.

### 범위 결정 근거

- `~/.claude/anthropic_key.sh`, `~/.codex/config.toml`, host-local Claude/Codex/Pi 설정은 수정하지 않는다.
- 실제 token 값을 tracked file, log, test fixture에 쓰지 않는다.
- Edge handler의 provider auth 주입은 02에서 처리한다.
- `/v1/responses` provider tunnel 지원은 03에서 처리한다.
- Node adapter/proto 변경은 이 subtask 범위가 아니다. 현재 `ProviderTunnelRequest.Headers`가 존재하므로 foundation 단계에서 proto 확장은 필요 없다.

### 빌드 등급

`cloud-G06`. config 계약, provider routing label, secret-handling schema가 맞물리고 후속 Edge/API behavior를 결정하므로 local 단독 판단보다 cross-domain context가 필요하다.

## 구현 체크리스트

- [ ] [SEULGI_CONFIG-1] `seulgivibe_claude`, `seulgivibe_openai` provider type을 `openai_compat` runtime으로 정규화하고 mapper provider label에 보존한다.
- [ ] [SEULGI_CONFIG-2] `openai.provider_auth` config schema/default/validation을 추가하되 실제 token 값이나 local helper path를 참조하지 않는다.
- [ ] [SEULGI_CONFIG-3] Seulgivibe 정적 model catalog/계약 예시를 추가하고 실제 endpoint secret/token은 문서에 쓰지 않는다.
- [ ] `go test ./packages/go/config -count=1`와 `go test ./apps/edge/internal/node -count=1`를 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [SEULGI_CONFIG-1] Provider Type Alias

#### 문제

`packages/go/config/config.go:26-37`의 `NormalizeProviderType`은 `openai_api`, `vllm`, `lemonade`, `sglang`만 `openai_compat`로 정규화한다. `apps/edge/internal/node/mapper.go:239-245`의 provider label 기본값도 Seulgivibe 계열 type을 보존하지 않는다.

Before:

```go
// packages/go/config/config.go:28
switch strings.ToLower(strings.TrimSpace(t)) {
case "openai_compat", "openai_api", "vllm", "lemonade", "sglang":
    return "openai_compat"
```

```go
// apps/edge/internal/node/mapper.go:243
switch strings.TrimSpace(p.Type) {
case "vllm", "lemonade", "sglang", "openai_api":
    return strings.TrimSpace(p.Type)
```

#### 해결 방법

`seulgivibe_claude`, `seulgivibe_openai`는 runtime adapter type은 `openai_compat`로 정규화하되, provider label은 명시 `provider:`가 없을 때 원래 type을 유지한다.

After:

```go
// packages/go/config/config.go:28
switch strings.ToLower(strings.TrimSpace(t)) {
case "openai_compat", "openai_api", "vllm", "lemonade", "sglang", "seulgivibe_claude", "seulgivibe_openai":
    return "openai_compat"
```

```go
// apps/edge/internal/node/mapper.go:243
switch strings.TrimSpace(p.Type) {
case "vllm", "lemonade", "sglang", "openai_api", "seulgivibe_claude", "seulgivibe_openai":
    return strings.TrimSpace(p.Type)
```

#### 수정 파일 및 체크리스트

- [ ] `packages/go/config/config.go`: alias 추가.
- [ ] `apps/edge/internal/node/mapper.go`: provider label default 추가.
- [ ] `packages/go/config/config_test.go`: provider-first Seulgivibe alias가 load/validate되는 테스트 추가.
- [ ] `apps/edge/internal/node/mapper_test.go`: alias provider가 `OpenAICompatAdapterConfig.Provider`로 보존되는 테스트 추가.

#### 테스트 작성

- `TestLoadEdge_SeulgivibeProviderFirstAliases`: `nodes[].providers[].type` alias 두 개와 `models[]` membership validation을 검증.
- `TestBuildConfigPayload_ProviderFirstSeulgivibeDefaultsProviderFromType`: proto config의 `Provider`가 alias 문자열로 들어가는지 검증.

#### 중간 검증

```bash
go test ./packages/go/config -run 'TestLoadEdge_SeulgivibeProviderFirstAliases' -count=1
go test ./apps/edge/internal/node -run 'TestBuildConfigPayload_ProviderFirstSeulgivibeDefaultsProviderFromType' -count=1
```

### [SEULGI_CONFIG-2] Provider Auth Config Schema

#### 문제

`packages/go/config/config.go:376-389`의 `EdgeOpenAIConf`에는 inbound IOP auth인 `bearer_token`만 있고, 사용자별 provider token을 별도 header에서 받아 tunnel 호출에 전달한다는 설정이 없다. `apps/edge/internal/openai/routes.go:20-29`는 inbound `Authorization`을 IOP auth로 이미 사용하므로 provider token을 같은 header로 재사용하면 충돌한다.

Before:

```go
// packages/go/config/config.go:376
type EdgeOpenAIConf struct {
    Enabled            bool               `mapstructure:"enabled"              yaml:"enabled"`
    Listen             string             `mapstructure:"listen"               yaml:"listen"`
    BearerToken        string             `mapstructure:"bearer_token"         yaml:"bearer_token"`
```

#### 해결 방법

Edge OpenAI surface에 optional provider auth config를 추가한다. 기본은 disabled라 기존 동작이 바뀌지 않는다. enabled일 때 구현 task 02가 `from_header`의 raw token을 읽어 `target_header`로 전달한다.

After:

```go
type EdgeOpenAIProviderAuthConf struct {
    Enabled      bool   `mapstructure:"enabled"       yaml:"enabled,omitempty"`
    FromHeader   string `mapstructure:"from_header"   yaml:"from_header,omitempty"`
    TargetHeader string `mapstructure:"target_header" yaml:"target_header,omitempty"`
    Scheme       string `mapstructure:"scheme"        yaml:"scheme,omitempty"`
    Required     bool   `mapstructure:"required"      yaml:"required,omitempty"`
}

type EdgeOpenAIConf struct {
    Enabled      bool                       `mapstructure:"enabled"       yaml:"enabled"`
    Listen       string                     `mapstructure:"listen"        yaml:"listen"`
    BearerToken  string                     `mapstructure:"bearer_token"  yaml:"bearer_token"`
    ProviderAuth EdgeOpenAIProviderAuthConf `mapstructure:"provider_auth" yaml:"provider_auth,omitempty"`
```

Default/validation rule:

- disabled by default.
- if enabled and empty: `from_header=X-IOP-Provider-Authorization`, `target_header=Authorization`, `scheme=Bearer`, `required=true`.
- reject empty `from_header` or `target_header` after default normalization only when enabled.
- never load token value from local files, env files, or helper scripts in this schema.

#### 수정 파일 및 체크리스트

- [ ] `packages/go/config/config.go`: struct, default normalization, validation 추가.
- [ ] `packages/go/config/config_test.go`: defaults, override, invalid enabled config 테스트 추가.
- [ ] 기존 `TestLoadEdge_OpenAIDefaults`와 `TestLoadEdge_OpenAIOverride`가 깨지지 않게 기대값 갱신.

#### 테스트 작성

- `TestLoadEdge_OpenAIProviderAuthDefaultsDisabled`: 기본 disabled 확인.
- `TestLoadEdge_OpenAIProviderAuthEnabledDefaults`: enabled일 때 default header/scheme/required 확인.
- `TestLoadEdge_OpenAIProviderAuthRejectsBlankHeaders`: enabled 상태에서 blank override reject 확인.

#### 중간 검증

```bash
go test ./packages/go/config -run 'TestLoadEdge_OpenAIProviderAuth|TestLoadEdge_OpenAIDefaults|TestLoadEdge_OpenAIOverride' -count=1
```

### [SEULGI_CONFIG-3] Static Catalog And Contract Notes

#### 문제

`packages/go/config/config.go:279-303`의 `ModelCatalogEntry`는 정적 model catalog를 지원하지만 Seulgivibe 모델 3개와 Codex/OpenAI 모델 축을 설명하는 계약/예시가 없다. `/anthropic/v1/models`와 `/openai/v1/models`가 안정적 catalog source가 아니라면 runtime은 config catalog를 source of truth로 삼아야 한다.

#### 해결 방법

계약/예시에 provider-first 구성을 추가하되 실제 secret/token은 쓰지 않는다. endpoint는 필요 시 placeholder 또는 사내 문서 포인터로 두고, token은 반드시 요청 시 `openai.provider_auth.from_header`로 들어온 raw user token을 task 02가 전달한다고 명시한다.

Example shape:

```yaml
openai:
  provider_auth:
    enabled: true
    from_header: X-IOP-Provider-Authorization
    target_header: Authorization
    scheme: Bearer
    required: true

nodes:
  - providers:
      - id: seulgivibe-claude
        type: seulgivibe_claude
        category: api
        endpoint: https://<seulgivibe-host>/anthropic/v1
        models: [claude-sonnet-4-5, claude-opus-4-8, claude-fable-5]
      - id: seulgivibe-openai
        type: seulgivibe_openai
        category: api
        endpoint: https://<seulgivibe-host>/openai/v1
        models: [gpt-5.1, gpt-5.5]

models:
  - id: claude-sonnet-4-5
    providers: {seulgivibe-claude: claude-sonnet-4-5}
  - id: claude-opus-4-8
    providers: {seulgivibe-claude: claude-opus-4-8}
  - id: claude-fable-5
    providers: {seulgivibe-claude: claude-fable-5}
  - id: gpt-5.1
    providers: {seulgivibe-openai: gpt-5.1}
  - id: gpt-5.5
    providers: {seulgivibe-openai: gpt-5.5}
```

#### 수정 파일 및 체크리스트

- [ ] `agent-contract/inner/edge-config-runtime-refresh.md`: config schema/refresh impact를 기록.
- [ ] `agent-contract/outer/openai-compatible-api.md`: provider auth request header와 token non-persistence rule 기록.
- [ ] 필요 시 existing sample config에 placeholder-only example 추가. 실제 Seulgivibe token/secret은 금지.

#### 테스트 작성

문서/예시 변경은 unit test가 직접 커버하지 않는다. `SEULGI_CONFIG-1/2`의 config tests가 schema와 catalog validation을 검증한다.

#### 중간 검증

```bash
rg --sort path -n 'e[y]J|secret -|OPENAI_API_KEY=.*e[y]J' agent-contract configs
test $? -eq 1
```

Expected: `rg` prints no matches, then `test $? -eq 1` passes.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `packages/go/config/config.go` | SEULGI_CONFIG-1, SEULGI_CONFIG-2 |
| `packages/go/config/config_test.go` | SEULGI_CONFIG-1, SEULGI_CONFIG-2 |
| `apps/edge/internal/node/mapper.go` | SEULGI_CONFIG-1 |
| `apps/edge/internal/node/mapper_test.go` | SEULGI_CONFIG-1 |
| `agent-contract/inner/edge-config-runtime-refresh.md` | SEULGI_CONFIG-3 |
| `agent-contract/outer/openai-compatible-api.md` | SEULGI_CONFIG-3 |
| `configs/**` | SEULGI_CONFIG-3, only if an existing sample is the correct place |

## 최종 검증

```bash
go test ./packages/go/config -count=1
go test ./apps/edge/internal/node -count=1
rg --sort path -n 'e[y]J|secret -|OPENAI_API_KEY=.*e[y]J' agent-contract configs
test $? -eq 1
```

Expected: Go tests pass. `rg` prints no real token references and `test $? -eq 1` passes; placeholder strings are acceptable only when they are not token-shaped.

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