# Edge Config And Runtime Refresh Contract ## 계약 메타 - id: `iop.edge-config-runtime-refresh` - boundary: `inner` - status: active - 원본 경로: - `packages/go/config/config.go` - `configs/edge.yaml` - `apps/edge/internal/configrefresh/request.go` - `apps/edge/internal/configrefresh/result.go` - `apps/edge/internal/configrefresh/classify.go` - `proto/iop/runtime.proto` - `apps/edge/internal/node/mapper.go` - `apps/node/internal/adapters/config_set.go` - human docs: `apps/edge/README.md` ## 읽는 조건 - `configs/edge.yaml`, `packages/go/config`, provider pool, `openai.model_routes`, `models[]`, `nodes[].providers[]`, adapter instance 설정을 바꿀 때 - `iop-edge config refresh`의 dry-run/apply 결과 schema나 restart/applied 분류를 바꿀 때 - Edge가 Node에 전달하는 `NodeConfigPayload` 또는 `NodeConfigRefresh*` payload를 바꿀 때 ## 범위 이 계약은 Edge 설정 YAML, Go config struct, config refresh 결과, Edge-to-Node runtime config payload의 연결 규칙이다. tracked config에는 public 예시와 기본 구조만 두고, 실제 endpoint/credential/private host 값은 환경별 private 설정으로 주입한다. ## 핵심 규칙 - `openai.principal_tokens[]`는 raw token을 저장하지 않고 hash/reference로 principal 매핑을 관리한다. 각 entry는 `token_ref` (non-empty, unique), `token_hash_sha256` (64-char hex, duplicate hash rejection), `principal_ref` (non-empty), optional `principal_alias` 필드를 갖는다. 여러 entry가 같은 `principal_ref`와 `principal_alias`를 공유할 수 있으며, 이때 `token_ref`가 앱/통합/용도별 사용량 분해 기준이 된다. tracked config에는 raw token을 저장하지 않고 hash/reference만 둔다. - `openai.provider_auth`는 request-time raw provider token forwarding rule이다. `enabled=false`가 기본이며 raw token 값은 저장하지 않는다. `enabled=true`이고 header fields가 생략되면 `from_header=X-IOP-Provider-Authorization`, `target_header=Authorization`, `scheme=Bearer`, `required=true`로 해석한다. - `openai` deep diff는 restart-required로 분류한다. `openai.principal_tokens[]` 변경은 credential/hash 변경으로 restart-required classifier에 포함된다. - `openai.model_routes[]`는 외부 OpenAI-compatible `model` id를 내부 `adapter + target` route로 매핑하는 compatibility catalog다. - `long_context_threshold_tokens`는 Edge root의 입력 토큰 추정 기준 long-context 분류 threshold다. 기본값은 `100000`이며 0 이하 값은 config load에서 거부한다. - `models[]`는 provider pool 방향의 canonical routing key이며 `nodes[].providers[].id`를 참조한다. `context_window_tokens`는 해당 model group의 provider 공통 단일 요청 최대 context 계약이다. `default_max_tokens`, `min_max_tokens`, `default_thinking_token_budget`은 OpenAI-compatible 요청을 내부 실행으로 넘기기 전에 적용하는 모델 단위 generation policy다. - `nodes[].providers[]`는 Node 아래 resource/provider catalog다. `category`는 `api`, `cli`, `local_inference` resource kind를 나타낸다. - `nodes[].providers[].type`의 `seulgivibe_claude`와 `seulgivibe_openai`는 runtime type을 `openai_compat`로 정규화한다. Edge가 Node adapter payload를 만들 때 명시 provider label이 없으면 원래 Seulgivibe type alias를 `OpenAICompatAdapterConfig.provider`로 보존한다. - `nodes[].providers[].id`는 전체 Edge config 안에서 중복되면 안 된다. - `nodes[].providers[].adapter`는 같은 Node 안의 enabled adapter instance key를 참조해야 한다. Exact instance key를 우선하고, legacy type-name route는 같은 type의 enabled instance가 정확히 하나일 때만 허용한다. `category: cli` resource는 enabled CLI adapter가 필요하다. - `nodes[].providers[].enabled`: 생략 또는 `true` → provider pool dispatch 후보에 포함. `false` → dispatch pool에서 제외. 비활성화된 provider는 status snapshot에 `status=disabled`, `health=disabled`, `capacity=0`으로 표시된다. adapter process lifecycle 변경 없음. config refresh 시 `enabled` 토글은 live-apply(restart 불필요)로 분류된다. disabled provider의 adapter reference check는 skip되지만 structural validation(type, category, models, numeric bounds)은 수행된다. - `nodes[].providers[].priority`: provider-pool dispatch tie-breaker다. 기본값은 `0`이고 음수는 validation error다. dispatch는 `in_flight < capacity` 후보 중 가장 낮은 `in_flight`를 먼저 선택하며, `in_flight`가 같은 후보에서만 낮은 숫자의 `priority`를 우선한다. `in_flight`와 `priority`가 모두 같으면 기존 순환을 유지한다. priority 변경은 live-apply(restart 불필요)로 분류된다. - `nodes[].providers[].queue_timeout_ms`와 adapter instance `queue_timeout_ms`는 provider-pool 대기열 timeout이다. queue policy가 구성된 상태에서 값이 `0`이면 queue timeout을 두지 않고, caller context cancellation 또는 연결 종료로만 대기 요청을 중단한다. provider가 queue policy를 전혀 제공하지 않으면 runtime 기본 queue timeout을 사용한다. - legacy single-instance adapter 설정은 load 시 named instance slice로 normalize된다. - `NodeConfigPayload`는 Edge가 Node에 내려주는 실행 adapter/runtime payload다. - refresh 결과는 `applied`, `restart_required`, `rejected`를 구분하고, changed node/provider/model/report slice는 안정적으로 non-nil이어야 한다. ## refresh 분류 기준 - live apply 가능: Edge root `long_context_threshold_tokens`, provider capacity, provider priority, provider max queue, provider queue timeout, provider `enabled` toggle, `models[]` display/context window/provider/generation policy mapping, legacy node runtime concurrency metadata. - restart required: Edge identity/listen/bootstrap/logging/metrics/console/control-plane/openai/a2a listener config, node 추가/삭제, node token/alias/agent kind, adapter 설정, provider type/category/adapter/models/health/lifecycle capability, provider-first execution fields(`provider`, `endpoint`, `base_url`, `headers`, `command`, `args`, `env`, `mode`, `resume_args`, `output_format`, `context_size`, `request_timeout_ms`) 변경. - rejected: candidate config load/validate 실패, invalid refresh mode, apply failure. ## 금지 사항 - 설정 파일만 바꾸고 `packages/go/config` loading/default/validation과 불일치하게 두지 않는다. - provider id, model id, route id의 의미를 섞지 않는다. 외부 `model`은 OpenAI-compatible 경계의 route key이고, 내부 실행은 `adapter + target`이다. - credential, bearer token, private endpoint 원문을 tracked docs/roadmap/contract에 기록하지 않는다. - provider auth raw token 원문을 config, docs, task artifact, metric label에 기록하지 않는다. - Node bootstrap 기본 경로에서 사용자가 직접 node config를 작성하거나 adapter/provider 값을 명령줄로 넣어야 하는 계약을 만들지 않는다. ## 변경 시 확인할 코드/테스트 - `packages/go/config/config_test.go` - `apps/edge/internal/configrefresh/classify_test.go` - `apps/edge/internal/edgecmd/edgecmd_test.go` - `apps/edge/internal/node/mapper_test.go` - `apps/node/internal/adapters/config_set_test.go` - `agent-test/local/platform-common-smoke.md` - `agent-test/local/edge-smoke.md`