--- spec_doc_type: spec spec_id: runtime/edge-node-execution status: 부분 source_evidence: - type: contract path: agent-contract/inner/agent-runtime.md notes: Node와 독립 host가 공유하는 provider lifecycle, event, session, failure 계약 - type: contract path: agent-contract/inner/edge-node-runtime-wire.md notes: Edge-Node register, run stream, cancel, node command, config refresh wire 계약 - type: code path: proto/iop/runtime.proto notes: RunRequest, RunEvent, CancelRequest, NodeCommandRequest, RegisterRequest, NodeConfigPayload 원문 - type: code path: apps/edge/internal/transport/server.go notes: Edge TCP proto-socket server, node register handshake, event relay - type: code path: apps/edge/internal/service/run_submit.go notes: surface-neutral SubmitRun과 direct/queued dispatch - type: code path: apps/edge/internal/service/provider_tunnel.go notes: provider tunnel dispatch와 request-bound frame relay - type: code path: apps/edge/internal/openai/provider_tunnel.go notes: protocol tunnel preparer, native/bridge operation flow, terminal ownership - type: code path: apps/edge/internal/service/run_types.go notes: Edge-local actual provider/model/node와 attribution policy dispatch result - type: code path: apps/edge/internal/service/model_queue_release.go notes: connection generation fencing, lease 반환, disconnect/reconnect queue 재평가 - type: code path: apps/edge/internal/service/status_provider.go notes: configured offline Node/provider snapshot과 dispatch-ready connectivity join - type: code path: packages/go/agentruntime/types.go notes: 공통 Provider, ExecutionSpec, RuntimeEvent와 optional lifecycle interface - type: code path: packages/go/agentprovider/cli/cli.go notes: Node와 독립 host가 공유하는 CLI provider 구현 - type: code path: apps/node/internal/node/runtime_bridge.go notes: Edge-Node protobuf와 공통 runtime request/event 변환 경계 - type: code path: apps/node/internal/bootstrap/runtime_supervisor.go notes: initial connect와 established-session reconnect를 공유하는 connectivity supervisor - type: code path: apps/node/internal/node/run_handler.go notes: Node RunRequest 처리와 adapter 실행 - type: code path: apps/node/internal/node/tunnel_handler.go notes: Node provider tunnel request, sealed lease consumption, in-memory credential injection, and frame relay - type: code path: packages/go/credentiallease/envelope.go notes: Signed scope validation, recipient sealing/opening, expiry, and exact binding verification - type: code path: apps/node/internal/adapters/openai_compat/execute.go notes: OpenAI-compatible provider 실행 stream과 RuntimeEvent usage 변환 - type: code path: apps/node/internal/adapters/openai_compat/provider_tunnel.go notes: OpenAI-compatible provider raw HTTP/SSE tunnel 처리 - type: code path: apps/node/internal/adapters/vllm/vllm.go notes: vLLM usage payload의 reasoning/cached token 변환 - type: test path: apps/edge/internal/transport/server_test.go notes: Edge transport server 단위 검증 - type: test path: apps/node/internal/node/run_cancel_test.go notes: Node run 실행과 cancel 처리 검증 - type: test path: apps/node/internal/node/provider_tunnel_test.go notes: Node provider tunnel lifecycle 검증 - type: test path: apps/node/internal/adapters/openai_compat/execute_test.go notes: OpenAI-compatible provider stream과 usage breakdown 검증 - type: test path: apps/node/internal/adapters/openai_compat/provider_tunnel_test.go notes: OpenAI-compatible provider raw tunnel 검증 - type: test path: apps/node/internal/adapters/vllm/vllm_test.go notes: vLLM usage breakdown 검증 - type: test path: apps/edge/internal/bootstrap/reconnect_readiness_integration_test.go notes: 실제 iop-node reconnect 뒤 queued waiter의 ready-gated dispatch와 terminal/counter 수렴 검증 - type: test path: apps/edge/internal/service/queue_reservation_test.go notes: provider lease exactly-once 반환과 connection generation race 검증 - type: test path: apps/node/internal/bootstrap/module_test.go notes: delayed initial connect, unlimited/finite retry, fatal, shutdown과 disabled metrics listener 검증 - type: test path: scripts/dev/edge-node-reconnect-diagnostic.sh notes: 별도 Edge·Node 프로세스의 메시지 relay 순서, terminal ordering, reconnect user-flow 검증 --- # 스펙: Edge-Node 실행 경로 ## 목적 Edge와 Node 사이에 현재 구현된 실행 기능을 기능 단위로 정리한다. 코드 배치 규칙이나 도메인별 작업 지침은 domain rule을 따른다. ## 기능 목록 | 기능 | 설명 | |------|------| | Node token 등록과 dispatch-ready | Node가 `RegisterRequest.token`으로 ownership/config를 받고, config 적용·adapter start·handler 설치 뒤 `NodeReadyRequest`/ack로 dispatch-ready가 된다. | | Node config payload 전달 | Edge가 token에 매칭되는 node record를 찾아 `NodeConfigPayload`를 `RegisterResponse`에 담아 내려준다. | | 등록 실패 처리 | unknown token, duplicate connection, config payload build failure를 register response와 node lifecycle event로 표현한다. | | 실행 요청 전달 | Edge service가 `SubmitRun` 요청을 `RunRequest`로 만들어 선택된 Node에 보낸다. 명시 node가 없고 연결 node가 1개면 single-node fallback을 사용한다. | | adapter 실행 | Node가 `RunRequest.adapter`로 공통 runtime registry의 provider instance를 찾고 `Provider.Execute`를 호출한다. admission은 `Capabilities().MaxConcurrency` 기준이다. CLI process/session/emitter/status 구현은 공통 package를 사용한다. | | 실행 이벤트 스트림 | Node adapter가 낸 start, delta, reasoning_delta, complete, error, cancelled 이벤트를 `RunEvent`로 Edge에 relay한다. | | provider raw tunnel | Edge가 `ProviderTunnelRequest`를 보내면 Node가 provider HTTP/SSE response를 열고 ordered `ProviderTunnelFrame`으로 status/header/body/end/error/usage 후보를 relay한다. protocol profile driver(`anthropic_messages`, `openai_chat`, `openai_responses`)에 따라 tunnel body preparation이 결정된다. | | Edge-Node mTLS identity | Managed mode requires CA-validated TLS and exact Edge/Node workload role/name checks before registration or dispatch. | | managed credential lease | Edge attaches an exact binding plus a short-lived signed lease sealed to the selected Node. Node opens it after adapter admission, immediately before provider execution, injects the profile auth header only in memory, and zeroes plaintext after the request. | | revision/generation fence | Edge validates the projected route binding before lease acquisition and immediately before send; Node independently verifies lease scope, recipient, expiry, signature, replay, and binding. | | mixed provider dispatch wire | provider-pool model group은 Edge service에서 provider를 먼저 선택한 뒤 OpenAI-compatible provider에는 `ProviderTunnelRequest`, Ollama/CLI/native provider에는 normalized `RunRequest`를 보낸다. | | Edge-local attribution binding | direct와 provider-pool normalized/tunnel dispatch result는 actual `provider_id`, served target, resolved node id, effective `usage_attribution` policy를 보존한다. 이 정보는 Edge-local이며 protobuf wire field를 추가하지 않는다. | | provider resource lease | 여러 model key가 같은 provider를 참조해도 Edge가 `node_id + provider_id` lease에서 일반·long capacity를 합산하고 terminal/send 실패/disconnect가 lease를 정확히 한 번 반환한다. | | Node connectivity supervision | 단일 supervisor가 retryable initial connect 실패와 established-session disconnect를 같은 reconnect policy로 처리하고 local shutdown, fatal 오류, 유한 exhaustion만 terminal로 구분한다. | | disconnect/reconnect fencing | current dispatch-ready owner의 generation만 provider를 offline/excluded로 만들고 queue를 재평가하며, reconnect ready는 새 generation candidate와 기존 waiter를 즉시 복구한다. | | adapter-local capacity guard | Node의 normalized 실행과 provider tunnel 실행은 같은 stable adapter instance capacity gate를 공유해 Edge admission 우회 실행도 backend 한도를 넘지 않는다. | | usage breakdown relay | `RunEvent.usage`와 `ProviderTunnelFrame.usage`는 provider가 보고한 input/output/reasoning/cached input token count를 Edge 관측 계층으로 전달한다. | | terminal event 합성 | adapter가 terminal event 없이 종료하면 Node가 terminal event를 합성한다. | | run cancel | Edge가 `CancelRequest`를 보내면 Node run manager가 active run context를 cancel한다. | | logical session 종료 | adapter가 `SessionTerminator`를 구현한 경우 `TERMINATE_SESSION`으로 session을 종료한다. 모든 adapter 공통 기능은 아니다. | | Node command | capabilities, transport status, usage status, session list, ollama API 계열 조회/제어성 command를 실행 요청과 분리해 처리한다. | | Node local run store | Node가 run id, adapter, target, session id, background, status, timestamps, error를 SQLite에 기록한다. | | Edge event fanout | Edge event bus가 run event와 node lifecycle event를 in-process subscriber에게 fanout한다. | ## 범위 - 포함: Edge-Node TCP/protobuf transport, register handshake, run/cancel/command, provider raw tunnel, Node adapter execution, Edge event bus fanout, Node local run store. - 제외: OpenAI-compatible/A2A HTTP request shape, Control Plane 운영 wire, provider-pool config refresh 상세, durable global history/audit. ## 주요 흐름 ### Node 등록 ```mermaid sequenceDiagram participant Node participant EdgeTransport as Edge transport participant NodeStore as Edge NodeStore Node->>EdgeTransport: TCP connect Node->>EdgeTransport: RegisterRequest(token) EdgeTransport->>NodeStore: token으로 NodeRecord 조회 alt token valid EdgeTransport->>EdgeTransport: NodeConfigPayload 생성, pending ownership claim EdgeTransport-->>Node: RegisterResponse(accepted=true, config) Node->>Node: config 적용, adapter start, session handler 설치 Node->>EdgeTransport: NodeReadyRequest(node_id) EdgeTransport->>EdgeTransport: current owner를 dispatch-ready로 전환 EdgeTransport->>EdgeTransport: provider availability 활성화, queued waiter pump, connected event EdgeTransport-->>Node: NodeReadyResponse(ready=true) else token invalid or duplicate EdgeTransport-->>Node: RegisterResponse(accepted=false, reason) end ``` Node process는 이 handshake 바깥에서 단일 connectivity supervisor를 실행한다. retryable initial dial/register 실패와 session disconnect는 같은 bounded cadence로 재시도하고, 명시적 `reconnect.max_attempts=0`은 local shutdown까지 unlimited로 동작한다. ### 실행 요청과 이벤트 ```mermaid sequenceDiagram participant Caller participant EdgeService as Edge service participant EdgeTransport as Edge transport participant Node participant Adapter Caller->>EdgeService: SubmitRun(adapter, target, input) EdgeService->>EdgeService: Node 선택 EdgeService->>EdgeTransport: RunRequest EdgeTransport->>Node: RunRequest Node->>Node: adapter instance resolve Node->>Adapter: Execute(spec) Adapter-->>Node: RuntimeEvent(delta/start/complete) Node-->>EdgeTransport: RunEvent EdgeTransport-->>Caller: run stream ``` ### Provider raw tunnel ```mermaid sequenceDiagram participant OpenAI as Edge OpenAI surface participant Anthropic as Edge Anthropic surface participant EdgeService as Edge service participant Node participant Provider OpenAI->>EdgeService: SubmitProviderTunnel (Chat/Responses) Anthropic->>EdgeService: SubmitProviderTunnel (Messages/CountTokens) EdgeService->>EdgeService: BuildBody(selected served target) EdgeService->>EdgeService: validate binding, acquire Node-targeted lease EdgeService->>Node: ProviderTunnelRequest(operation, body, binding, sealed lease) Node->>Node: capacity admission, verify/open lease, inject auth in memory Node->>Provider: HTTP/SSE request Provider-->>Node: status/header/body Node-->>EdgeService: ProviderTunnelFrame sequence EdgeService-->>OpenAI: request-bound frame stream (OpenAI response) EdgeService-->>Anthropic: request-bound frame stream (Anthropic response) ``` ### 취소와 session 종료 ```mermaid sequenceDiagram participant EdgeService as Edge service participant Node participant Adapter alt cancel run EdgeService->>Node: CancelRequest(CANCEL_RUN, run_id) Node->>Node: active run context cancel else terminate session EdgeService->>Node: CancelRequest(TERMINATE_SESSION, adapter, target, session_id) Node->>Adapter: TerminateSession(target, session_id) end ``` ## 계약 - `iop.edge-node-runtime-wire`: `agent-contract/inner/edge-node-runtime-wire.md` - `iop.agent-runtime`: `agent-contract/inner/agent-runtime.md` - proto 원문: `proto/iop/runtime.proto` ## 설정/데이터/이벤트 - Edge의 node source of truth는 `configs/edge.yaml`과 `packages/go/config`의 `nodes[]` 구조다. - The top-level `protocol_profiles` catalog and `nodes[].providers[].profile` selector resolve into a runtime-only `RuntimeProfile`. The resolved profile is nested in the OpenAI-compatible adapter configuration sent during Node config delivery. - `ProviderTunnelRequest.operation` is protobuf field 13 and identifies the named operation. `path` is retained as a mixed-version fallback. - `ProviderTunnelRequest.credential_lease` and `.credential_binding` are required together in managed mode and absent together in legacy mode. The scope binds principal, slot, route, profile, target, Node recipient, credential/route revisions, and projection generation. - Managed Node credential material is never part of adapter config. Recipient and issuer key references are loaded at startup; only the selected Node can open the lease, and plaintext exists only for the request immediately before adapter execution. - `SubmitProviderTunnelRequest.BuildBody` is Edge-local: it receives the selected served target, then Edge serializes its bytes into protobuf `ProviderTunnelRequest.body`. It is not part of the wire schema. - `ProviderTunnelFrame`은 ordered frame으로, `RESPONSE_START`은 최초 한 번만, `BODY`는 0회 이상, `END`는 정확히 한 번, `ERROR`는 `END` 대신 한 번만 온다. `USAGE` frame은 body에 합쳐지지 않고 관측 전용이다. - Native Anthropic Messages require `messages` capability and operation; the Chat bridge requires `chat` capability and `chat_completions` operation. Streaming and tools additionally require their respective capabilities. - A configured model-catalog TokenCounter returns a deterministic local count for Anthropic count_tokens without provider selection. Only the native upstream fallback requires an `anthropic_messages` candidate with `count_tokens` capability and operation; Chat profiles remain unsupported for that fallback. - Chat bridge는 provider profile의 `extensions.thinking` 또는 `extensions.reasoning`이 `true`일 때만 thinking block을 지원한다. - OpenAI와 Anthropic ingress는 같은 model catalog와 provider-pool dispatch를 공유한다. 같은 `model` key는 두 표면 모두에서 같은 provider-pool candidate set에서 선택된다. - accepted registration은 duplicate ownership claim과 config 전달만 담당한다. ready ack 전 Node는 direct/provider-pool dispatch, provider tunnel/command, config refresh push, connected snapshot/event에서 제외된다. - Edge registry의 connection generation은 internal fence이며 wire/config로 노출하지 않는다. current client의 첫 ready만 provider resource activation과 queue pump를 수행하고, duplicate ready는 idempotent ack, stale/rejected ready는 reject로 처리한다. - current owner disconnect는 event bus와 분리된 authoritative service 경로에서 해당 generation의 provider lease를 exactly-once 반환하고 resource를 offline으로 fence한 뒤 모든 model group waiter를 live candidate로 재평가한다. 후보가 없어진 waiter는 queue timeout을 기다리지 않고 unavailable로 끝난다. - configured Node/provider는 연결이 끊겨도 snapshot catalog에서 사라지지 않는다. Node는 `connected=false`, enabled provider는 `status=unavailable`, `health=offline`, effective capacity/counter 0으로 보이며 ready reconnect 뒤 새 generation의 configured capacity가 복구된다. - `reconnect.max_attempts` 생략은 `10`, 명시적 `0`은 unlimited, 양수는 유한 limit이다. unlimited mode는 양수 `interval_sec`가 필요하고 생략값은 `10`이다. fatal config/credential 오류와 유한 exhaustion은 non-zero terminal, local shutdown은 정상 종료다. - `RunEvent`는 adapter execution stream이고, `EdgeNodeEvent`는 node lifecycle/control event다. - `ProviderTunnelFrame.body`는 OpenAI-compatible provider passthrough의 source of truth이며 `RunEvent.delta`나 Edge event bus payload로 보내지 않는다. - `ProviderTunnelFrame.usage`와 `metadata`는 관측 후보이며 pure passthrough body에 합쳐지지 않는다. - provider-pool mixed dispatch에서 `ProviderTunnelRequest`와 `RunRequest` 중 어느 wire를 사용할지는 selected provider capability에서 파생되며, client request metadata selector로 결정하지 않는다. - direct dispatch result는 검증된 configured `provider_id`를 사용하고, provider-pool result는 선택된 candidate의 actual `provider_id`를 사용한다. 두 경로 모두 served target, resolved node id, effective `usage_attribution` policy를 Edge-local `RunDispatch`에 보존하며 adapter 또는 node text를 provider identity로 추론하지 않는다. - attribution binding은 기존 `RunRequest`/`ProviderTunnelRequest` protobuf message를 확장하지 않고 Node 실행 또는 Edge-Node wire schema를 변경하지 않는다. - `Usage.reasoning_tokens`와 `Usage.cached_input_tokens`는 provider가 별도 보고한 경우에만 채워지는 optional breakdown이다. - Node local DB는 기본 `file:iop.db?cache=shared&mode=rwc`로 열린다. - heartbeat는 Edge와 Node transport 양쪽에서 2초 interval, 5초 wait 기준을 사용한다. 정상적인 프로세스·OS 종료는 transport close로 즉시 감지하고, heartbeat timeout은 종료 신호가 오지 않는 전원 차단·네트워크 단절의 fallback으로 사용한다. ## 검증 - `go test ./apps/edge/internal/transport ./apps/edge/internal/service ./apps/edge/internal/node` - `go test ./apps/node/internal/transport ./apps/node/internal/node ./apps/node/internal/router ./apps/node/internal/adapters ./apps/node/internal/store` - `go test ./apps/node/internal/adapters/openai_compat ./apps/node/internal/adapters/vllm` - `go test ./apps/edge/internal/bootstrap -run '^TestActualNodeReconnectReadyPumpsQueuedWaiterExactlyOnce$'` - 실제 `iop-node` 재연결 뒤 기존 provider-pool waiter의 dispatch 1회, terminal 1회, counter 0 수렴을 확인한다. - `./scripts/e2e-provider-capacity-smoke.sh` - loopback provider에서 두 model alias가 capacity 1 resource를 공유하고 final normal/long counter가 0으로 회복하는지 확인한다. - `make test-e2e` - Edge-Node와 OpenAI 보조 smoke를 함께 실행한다. runtime path 변경 시 사용자 흐름 검증을 대체하지 않는다. ## 한계와 주의사항 - Legacy mode can run without the managed credential lease path. Managed mode cannot start without Edge-Node TLS, Control Plane connector TLS, and the configured issuer/recipient key material. - `TERMINATE_SESSION`은 모든 adapter에 공통으로 보장되는 기능이 아니다. - Node store는 전역 query/audit API가 아니다. 상위 운영 이력은 별도 설계가 필요하다. - provider raw tunnel은 기존 socket 위 request-bound stream이다. 별도 Node stream server나 Edge의 provider direct access 경로가 아니다. - usage breakdown은 provider-reported 값만 전달한다. provider가 보고하지 않은 reasoning token을 Node나 Edge가 추정하지 않는다. - Revoked, disabled, expired, stale, replayed, wrong-recipient, or mismatched leases fail closed. No route/provider/credential fallback is permitted after an authenticated managed route is bound. ## 변경 기록 - 2026-07-07: 현재 코드, 계약, README 기준으로 bootstrap spec 작성. - 2026-07-07: 기능 목록 중심으로 축소하고 주요 흐름을 Mermaid sequence diagram으로 정리. - 2026-07-08: Provider raw tunnel 실행 흐름과 passthrough event/data 경계를 현재 계약 기준으로 반영. - 2026-07-10: `RunEvent.usage`/`ProviderTunnelFrame.usage`의 input/output/reasoning/cached input breakdown 전달 기준을 반영. - 2026-07-12: Model Group Mixed Provider Dispatch 종료 검토 기준으로 selected provider capability에서 파생되는 `ProviderTunnelRequest`/`RunRequest` 분기 경계를 반영. - 2026-07-18: 저장소 구조 분해 뒤 Edge run/tunnel, Node handler, adapter split test의 `source_evidence`를 현재 경로로 동기화. - 2026-07-22: accepted registration을 pending ownership/config 단계로 제한하고, handler 설치 뒤 `NodeReadyRequest`/ack로 dispatch eligibility와 reconnect waiter pump를 여는 순서를 반영. - 2026-07-22: provider resource lease, connection generation fencing, initial/장기 reconnect supervision, offline snapshot과 adapter-local capacity guard를 현재 구현·계약·회귀 테스트 기준으로 동기화. - 2026-07-28: Node의 공통 Agent Runtime registry/CLI provider 소비와 protobuf translation bridge를 현재 코드·계약 기준으로 반영. - 2026-07-31: direct/provider-pool normalized·tunnel의 actual provider/model/node 및 attribution policy를 Edge-local dispatch result에 보존하는 경계를 반영했다. - 2026-08-01: protobuf operation, Edge-local body construction, nested adapter profile delivery, and native/bridge capability boundaries were synchronized with source. - 2026-08-02: Synchronized Edge-Node mTLS identity, exact credential binding, recipient-sealed lease consumption, in-memory injection/zeroization, and fail-closed revision/revocation behavior with current source.