<!-- task=m-principal-provider-credential-slot-routing/01_principal_store plan=0 tag=API -->

# Principal Store

## For the Implementing Agent

Filling the implementation-owned sections of `CODE_REVIEW-cloud-G06.md` is mandatory. Run every verification command, paste actual notes/output, keep both active files in place, and report ready for review; only the code-review skill may finalize, rename logs, write `complete.log`, or archive the task. If blocked, record only the exact blocker, attempted commands/output, and resume condition in the implementation-owned evidence fields; do not ask the user, call user-input tools, create control-plane stop files, or classify the next state.

## Background

Control Plane currently accepts `database.url` but only logs that it is configured; there is no durable credential-plane repository. Principal tokens are Edge config entries, so issuance, revocation, revision fencing, restart persistence, and one-time raw-token return cannot be enforced by Control Plane.

## 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:
  - `principal-store`: principal 및 IOP token hash 원장
- Completion mode: check-on-pass

## Analysis

### Files Read

- `go.mod`
- `go.sum`
- `docker-compose.yml`
- `configs/control-plane.yaml`
- `apps/control-plane/cmd/control-plane/main.go`
- `apps/control-plane/cmd/control-plane/server.go`
- `apps/control-plane/cmd/control-plane/config_test.go`
- `apps/edge/internal/openai/principal.go`
- `apps/node/internal/store/store.go`
- `apps/node/internal/store/store_test.go`

### SDD Criteria

- SDD: `agent-roadmap/sdd/operational-observability-provider-management/principal-provider-credential-slot-routing/SDD.md` (`승인`, 구현 잠금 해제).
- Target: S01 / `principal-store`.
- Evidence Map row: S01-S03의 CP repository lifecycle 근거 중 S01 persistence/auth 결과.
- 따라서 checklist는 원문 token의 1회 반환, SHA-256 digest만 영속화, revision/status 전이, revoke 불가역성, process reopen persistence, DB/API/log/metric 비노출 검증을 한 저장소 경계에 묶는다.

### Verification Context

- 별도 handoff는 없었다. 저장소 근거는 `go.mod`, Control Plane config/server, `docker-compose.yml`, project/domain/test rules와 `agent-test/local`의 control-plane smoke profile에서 도출했다.
- Local runner: `/config/workspace/iop-s0`, branch `feature/multi-provider-protocol-profile-native-messages`, HEAD `f2306f4dc8cabd7ff3c2085c016897f0bb337cc6`, Linux aarch64.
- `go version go1.26.2 linux/arm64`; PostgreSQL URL은 compose의 `IOP_DATABASE_URL`, 테스트는 기존 `modernc.org/sqlite`로 process reopen을 재현한다. PostgreSQL runtime driver는 현재 manifest에 없어 `pgx/v5/stdlib` 추가가 필요하다.
- 현재 셸의 기본 `GOCACHE`는 권한이 없는 임시 경로이고 `/tmp`는 `noexec`이다. 저장소의 ignored `.cache/go-cache`와 `.cache/go-build`를 명시하면 관련 package compile preflight가 PASS했다.
- 외부 provider, 원격 runner, secret, port가 필요하지 않다. Confidence: high.

### Test Coverage Gaps

- 기존 Control Plane 테스트는 config redaction만 확인하고 DB open/migration을 다루지 않는다: 신규 startup wiring test 필요.
- 기존 Node SQLite store는 reopen 패턴만 제공하며 principal token의 one-time return/revoke/revision을 다루지 않는다: 신규 repository tests 필요.
- raw token의 log/metric 비노출 검사는 없다: logger observer를 사용하는 신규 test 필요.

### Symbol References

- Rename/remove 없음. `run` 호출은 `apps/control-plane/cmd/control-plane/main.go:82` 한 곳이며 signature를 유지한다.

### Split Judgment

- `01_principal_store`: DB 연결·migration·principal/token transaction을 제공하고 restart persistence tests로 독립 PASS한다.
- `02+01_credential_catalog`: 01의 FK/source-of-truth를 사용해 slot/route catalog를 독립 검증한다.
- `03+01,02_projection_auth`: 01/02 snapshot을 wire로 보내 Edge에서 인증한다.
- `04+03_principal_routes`: 적용된 projection만 읽어 discovery/selection을 검증한다.
- `05+01,02_management_core`: 01/02 service를 principal-scoped in-process operations로 노출한다.
- `06+01,02_host_local_bootstrap`: 01의 first-principal issuance를 host-local CLI로 노출한다.
- 본 packet은 predecessor가 없으며, principal과 최초 token 발급을 한 transaction으로 유지해야 raw-token one-time invariant가 독립 PASS하므로 더 나누지 않는다.

### Scope Rationale

- Edge projection, HTTP ingress, slot/route schema, management wire/CLI, provider secret delivery는 후속 packet 소유이므로 제외한다.
- `agent-ops/rules/common/**`, `agent-ops/skills/common/**`, roadmap/SDD/spec 문서는 수정하지 않는다.
- SQLite는 deterministic repository test dialect이고 production URL은 PostgreSQL이다; 별도 migration framework 도입은 하지 않는다.

### Final Routing

- `status=routed`, `evaluation_mode=first-pass`, finalizer=`finalize-task-policy.sh`, mode=`pair`.
- Build closures: scope/context/verification/evidence/ownership/decision 모두 true. Scores `2+1+1+1+1=G06`; base/route=`local-fit`, lane=`local`, canonical=`PLAN-local-G06.md`.
- Review closures 모두 true. Scores `2+1+1+1+1=G06`; route=`official-review`, lane=`cloud`, model=`gpt-5.6-sol` xhigh, canonical=`CODE_REVIEW-cloud-G06.md`.
- `large_indivisible_context=false`; matched loop risk=`temporal_state` (1); rework=0; evidence integrity failure=false; capability gap 없음.

## Implementation Checklist

- [ ] Add the Control Plane credential-store connection, schema migration, and clean shutdown boundary.
- [ ] Implement atomic principal/token issuance, lookup, disable, and irreversible revoke lifecycle with digest-only persistence.
- [ ] Wire the optional database-backed store into Control Plane startup while preserving database-unconfigured legacy startup.
- [ ] Run fresh repository, startup, vet, and diff verification with the declared executable Go cache paths.
- [ ] Fill implementation-owned sections in CODE_REVIEW-*-G??.md with actual implementation notes and verification output.

### [API-1] Open and migrate the credential store

**Problem:** `apps/control-plane/cmd/control-plane/server.go:29-31` only logs `database.url`; no connection, migration, or close path exists.

**Solution:** Add a `database/sql` store that selects PostgreSQL for `postgres://`/`postgresql://` and SQLite only for tests, pings before use, runs idempotent schema migrations, and closes with server lifetime. Add `github.com/jackc/pgx/v5/stdlib`; keep secrets out of connection logs.

Before (`apps/control-plane/cmd/control-plane/server.go:29-31`):

```go
if cfg.Database.URL != "" {
    logger.Info("control-plane database configured", databaseLogFields(cfg.Database.URL)...)
}
```

After:

```go
store, err := credentialstore.Open(ctx, cfg.Database.URL)
if err != nil {
    return fmt.Errorf("open credential store: %w", err)
}
if store != nil {
    defer store.Close()
}
```

**Modified Files and Checklist:**

- [ ] `go.mod`: add the PostgreSQL stdlib driver.
- [ ] `go.sum`: record resolved driver checksums.
- [ ] `apps/control-plane/internal/credentialstore/store.go`: implement dialect selection, ping, transaction helper, and close.
- [ ] `apps/control-plane/internal/credentialstore/schema.go`: implement idempotent principal/token schema and constraints.
- [ ] `apps/control-plane/cmd/control-plane/server.go`: open/close the optional store.
- [ ] `apps/control-plane/cmd/control-plane/credential_store_test.go`: prove configured failure and unconfigured compatibility.

**Test Strategy:** Write `TestOpenMigratesAndReopensCredentialStore`, `TestRunRejectsInvalidDatabaseURL`, and `TestRunAllowsUnconfiguredDatabase`. Use a temp SQLite file and ephemeral listener addresses; assert schema persists and invalid configured DB fails before serving.

**Verification:** `GOTMPDIR=/config/workspace/iop-s0/.cache/go-build GOCACHE=/config/workspace/iop-s0/.cache/go-cache go test -count=1 ./apps/control-plane/internal/credentialstore ./apps/control-plane/cmd/control-plane` must pass.

### [API-2] Enforce principal token lifecycle and secret hygiene

**Problem:** `apps/edge/internal/openai/principal.go:86-137` matches static config hashes, but Control Plane has no issuer, revision, durable status, or revocation source of truth.

**Solution:** Generate 32 random bytes, encode only the one-time response, persist SHA-256 digest plus stable token reference, principal reference, status, revision, and timestamps in one transaction. List/get return metadata only. Disable/revoke use compare-and-swap revision; revoke is irreversible and no method reconstructs raw token.

Before (`apps/control-plane/cmd/control-plane/server.go:15-18`):

```go
func run(ctx context.Context, cfg controlPlaneConfig, logger *zap.Logger) error {
    mux := newHTTPMux()

    wireEndpoint := wire.Endpoint{Listen: cfg.Server.WireListen}
```

After domain boundary:

```go
issued, err := store.CreatePrincipalWithToken(ctx, credentialstore.CreatePrincipalInput{Alias: alias})
// issued.RawToken is returned once; stored TokenRecord contains Digest only.
```

**Modified Files and Checklist:**

- [ ] `apps/control-plane/internal/credentialstore/principal.go`: define records, status validation, issuance, lookup/list, disable, and revoke transactions.
- [ ] `apps/control-plane/internal/credentialstore/principal_test.go`: cover issuance, duplicate alias, CAS revision, disable/revoke, reopen, and raw-token absence.

**Test Strategy:** Write `TestCreatePrincipalReturnsRawTokenOnce`, `TestPrincipalTokenLifecycleUsesRevisionCAS`, `TestRevokedTokenCannotBeReenabled`, and `TestPrincipalTokenPersistsAcrossReopen`. Inspect DB columns and captured zap entries to assert neither raw token nor a reversible form appears.

**Verification:** `GOTMPDIR=/config/workspace/iop-s0/.cache/go-build GOCACHE=/config/workspace/iop-s0/.cache/go-cache go test -count=1 -race ./apps/control-plane/internal/credentialstore` must pass with fresh execution.

### [API-3] Preserve startup compatibility and prove package integrity

**Problem:** `apps/control-plane/cmd/control-plane/main.go:87-97` defaults `database.url` to empty, so unconditional store construction would regress existing health/status use.

**Solution:** Treat empty URL as credential plane unavailable while leaving legacy server startup unchanged; treat any non-empty invalid/unreachable URL as fatal. Do not silently fall back from configured PostgreSQL to SQLite.

Before (`apps/control-plane/cmd/control-plane/main.go:92`):

```go
cfg.Database.URL = ""
```

After:

```go
// Empty URL preserves legacy startup; a configured URL must open successfully.
store, err := credentialstore.Open(ctx, cfg.Database.URL)
```

**Modified Files and Checklist:**

- [ ] `apps/control-plane/cmd/control-plane/server.go`: preserve nil-store compatibility and fail configured startup closed.
- [ ] `apps/control-plane/cmd/control-plane/credential_store_test.go`: assert both branches and redacted errors/logs.

**Test Strategy:** Reuse the explicit startup tests from API-1; no external PostgreSQL test is required because SQL behavior is covered through SQLite and driver selection/config failure is unit-tested.

**Verification:** `GOTMPDIR=/config/workspace/iop-s0/.cache/go-build GOCACHE=/config/workspace/iop-s0/.cache/go-cache go test -run '^$' ./apps/control-plane/...` must compile all Control Plane packages.

## Modified Files Summary

| File | Items |
|------|-------|
| `go.mod` | API-1 |
| `go.sum` | API-1 |
| `apps/control-plane/internal/credentialstore/store.go` | API-1 |
| `apps/control-plane/internal/credentialstore/schema.go` | API-1 |
| `apps/control-plane/internal/credentialstore/principal.go` | API-2 |
| `apps/control-plane/internal/credentialstore/principal_test.go` | API-2 |
| `apps/control-plane/cmd/control-plane/server.go` | API-1, API-3 |
| `apps/control-plane/cmd/control-plane/credential_store_test.go` | API-1, API-3 |
| `agent-task/m-principal-provider-credential-slot-routing/01_principal_store/CODE_REVIEW-cloud-G06.md` | API-1, API-2, API-3 |

## Final Verification

Run from `/config/workspace/iop-s0`; fresh results are required and Go test cache output is not acceptable.

1. `command -v go && go version && go env GOROOT` — Go resolves and reports the preflighted toolchain.
2. `mkdir -p .cache/go-build .cache/go-cache` — executable workspace-local temp/cache paths exist.
3. `GOTMPDIR=/config/workspace/iop-s0/.cache/go-build GOCACHE=/config/workspace/iop-s0/.cache/go-cache go mod tidy` — dependency files are canonical with no unrelated dependency drift.
4. `GOTMPDIR=/config/workspace/iop-s0/.cache/go-build GOCACHE=/config/workspace/iop-s0/.cache/go-cache go vet ./apps/control-plane/...` — no vet findings.
5. `GOTMPDIR=/config/workspace/iop-s0/.cache/go-build GOCACHE=/config/workspace/iop-s0/.cache/go-cache go test -count=1 -race ./apps/control-plane/internal/credentialstore` — lifecycle, restart, redaction, and race tests pass.
6. `GOTMPDIR=/config/workspace/iop-s0/.cache/go-build GOCACHE=/config/workspace/iop-s0/.cache/go-cache go test -count=1 ./apps/control-plane/...` — all Control Plane tests pass.
7. `git diff --check` — no whitespace errors.

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