iop/agent-contract/inner/edge-node-runtime-wire.md

14 KiB

Edge-Node Runtime Wire Contract

계약 메타

  • id: iop.edge-node-runtime-wire
  • boundary: inner
  • status: active
  • 원본 경로:
    • proto/iop/runtime.proto
    • apps/edge/internal/transport/server.go
    • apps/node/internal/transport/client.go
    • apps/node/internal/transport/session.go
    • apps/node/internal/transport/parser.go
    • apps/node/internal/bootstrap/runtime_supervisor.go
    • apps/node/internal/node/tunnel_handler.go
    • packages/go/credentiallease/envelope.go
    • apps/edge/internal/transport/connection_handlers.go
    • apps/edge/internal/service/model_queue_release.go
    • apps/edge/internal/service/status_provider.go
    • apps/edge/internal/node/mapper.go
    • apps/node/internal/adapters/config_set.go
  • human docs:
    • apps/edge/README.md
    • apps/node/README.md

읽는 조건

  • Edge-Node TLS/protobuf transport, workload identity, initial/reconnect supervision, register/dispatch-ready handshake, connection generation fencing, run stream, provider raw tunnel, credential lease consumption, cancel, node command, node config refresh를 바꿀 때
  • NodeReadyRequest, NodeReadyResponse, RunRequest, RunEvent, ProviderTunnelRequest, ProviderTunnelFrame, CancelRequest, NodeCommandRequest, NodeCommandResponse, NodeConfigPayload, NodeConfigRefresh* 필드를 바꿀 때
  • node adapter 설정 payload나 runtime config가 Edge에서 Node로 전달되는 방식을 바꿀 때

범위

이 계약은 Edge와 Node 사이의 내부 TCP proto-socket 경계다. Edge는 Node 연결을 수락하고, Node는 연결 직후 등록 요청을 보낸다. 실행 요청과 이벤트 스트림, 취소, 조회성 명령, 설정 refresh는 같은 내부 wire 계열에서 처리한다.

주요 흐름

  • register와 readiness: Node가 RegisterRequest를 보내고 Edge가 RegisterResponse로 수락 여부와 NodeConfigPayload를 돌려준다. accepted registration은 Node ID의 현재 ownership을 pending으로 claim할 뿐 dispatch 가능 상태가 아니다. Node는 config 적용, adapter start, session handler 설치 뒤 NodeReadyRequest(node_id)를 보내고, Edge가 current owner를 dispatch-ready로 전환한 뒤 NodeReadyResponse로 ack한다. 이 ready ack 전에는 run, provider tunnel, command, config-refresh push와 connected availability/event가 열리지 않는다.
  • connectivity supervision: Node daemon은 Fx startup 전에 원격 연결 성공을 요구하지 않고 단일 supervisor goroutine이 initial dial과 established-session reconnect를 같은 policy로 직렬 처리한다. retryable 원격 실패는 재시도하고 local config/credential fatal error, 유한 retry exhaustion, local shutdown만 process terminal로 구분한다.
  • disconnect/reconnect: current dispatch-ready owner의 close/heartbeat timeout만 해당 connection generation을 fence한다. Edge는 같은 authoritative lifecycle에서 provider lease를 정확히 한 번 반환하고 resource를 offline/excluded로 만든 뒤 queue를 live candidate 기준으로 재평가한다. accepted Node의 ready transition은 새 generation resource를 활성화하고 기존 waiter를 즉시 pump한다. stale/rejected connection callback은 live state나 lifecycle event를 바꾸지 않는다.
  • execution: Edge가 RunRequest를 보내고 Node가 RunEvent stream으로 실행 상태를 보낸다.
  • provider raw tunnel: Edge가 기존 Edge-Node socket으로 ProviderTunnelRequest를 보내고 Node가 provider HTTP/SSE 요청을 연 뒤 ProviderTunnelFrame stream으로 provider status/header/body/end/error/usage 후보를 sequence와 함께 돌려준다. 이 경로는 OpenAI-compatible provider passthrough용이며 RunEvent 실행 stream과 분리된다.
  • managed credential delivery: after provider selection, Edge attaches an exact CredentialLeaseBinding and a short-lived signed lease sealed to the selected Node. The Node opens it only after adapter-capacity admission and immediately before provider execution, verifies signature, recipient, scope, expiry, and replay state, injects the declared auth header in memory, then zeroes plaintext material.
  • provider-pool mixed dispatch: Edge service는 model group provider candidate를 선택한 뒤, 같은 selected provider/queue lease로 OpenAI-compatible provider에는 ProviderTunnelRequest, Ollama/native provider에는 normalized RunRequest를 보낸다. Edge-Node wire는 client-provided response path selector를 받지 않고, provider type만으로 후보를 제외하지 않는다.
  • cancel: Edge가 provider run id를 가진 CancelRequest를 보내 현재 provider 실행을 취소한다.
  • command: Edge가 NodeCommandRequest를 보내고 Node가 NodeCommandResponse로 capabilities/transport/provider lifecycle 상태를 응답한다.
  • refresh: Edge가 NodeConfigRefreshRequest로 새 config payload를 보내고 Node가 NodeConfigRefreshResponse로 적용/재시작 필요/실패를 응답한다.

필드 의미

  • RunRequest.adapter, RunRequest.target: 내부 실행 식별자다. 외부 OpenAI-compatible model은 Edge 입력 표면에서 이 둘로 변환되어야 한다.
  • RunRequest.input: adapter가 해석할 normalized provider 실행 입력이다.
  • RunRequest.metadata: caller-defined 실행 metadata다. 실행 디렉터리나 session 소유권을 전달하는 제어 표면이 아니다.
  • RunEvent.type: start, delta, complete, error, cancelled 같은 실행 이벤트 종류다.
  • ProviderTunnelRequest is the protobuf request for opening a provider HTTP request over the existing Edge-Node socket. It carries adapter, target, method, path, headers, final serialized body, stream, timeout_sec, metadata, session_id, and operation, separately from normalized RunRequest execution.
  • ProviderTunnelRequest.operation is protobuf field 13. It identifies a named profile operation (models, chat_completions, messages, count_tokens, or responses); when it is empty, path remains the mixed-version fallback.
  • ProviderTunnelRequest.credential_lease and .credential_binding are an inseparable managed pair. A configured managed Node rejects a tunnel when either is absent, invalid, expired, replayed, or scope-mismatched. Legacy requests omit both only when the credential plane is disabled.
  • CredentialLeaseBinding independently names the principal, slot, route, profile, upstream target, selected Node and recipient, credential/route revisions, and projection generation. It is not caller metadata and cannot be rewritten by the Node.
  • Edge and Node validate each other with CA-signed workload identities. Managed mode requires Edge server identity plus expected Node role/name, and Node client identity plus expected Edge role/name; no-cert, wrong-CA, wrong-role, and wrong-name peers fail before registration or lease delivery.
  • SubmitProviderTunnelRequest.BuildBody is Edge-local only. After provider-pool selection determines the served target, Edge invokes it and serializes its returned bytes into protobuf ProviderTunnelRequest.body. It is not a protobuf field.
  • The resolved ConcreteProtocolProfile travels in nested OpenAICompatAdapterConfig.protocol_profile inside the Node configuration payload. Tunnel requests carry the selected operation and bytes, not profile configuration.
  • ProviderTunnelFrame is the ordered response frame. body is the passthrough source of truth and is not sent through RunEvent.delta or the Edge event bus; usage and metadata are observation candidates and are never merged into the body. RESPONSE_START occurs at most once, BODY occurs zero or more times, and exactly one terminal END or ERROR occurs. USAGE is observation-only.
  • tunnel cancellation: HTTP caller disconnect, response wait timeout, 또는 Edge write failure가 발생하면 Edge는 같은 run id에 대한 CancelRequest를 보내 upstream provider request 중단을 요청한다. Node adapter는 provider request context cancellation을 관측하고 ordered error/end semantics를 유지해야 한다.
  • RunEvent.metadata["openai_tool_calls"]: OpenAI-compatible provider adapter가 native tool_calls를 반환했을 때 완료 이벤트에 싣는 JSON 배열이다. Edge OpenAI-compatible 표면은 이 값을 message.tool_calls 또는 stream delta.tool_calls로 복원한다. provider assistant content 텍스트를 이 값으로 파싱/합성하지 않는다.
  • RunEvent.metadata["openai_text_tool_fallback"]: OpenAI-compatible provider adapter가 backend native tool API 거부 후 tools/tool_choice를 제거하고 text tool-call instruction으로 재시도했을 때 "true"를 싣는다. 이 instruction은 backend가 system role 위치를 거부하지 않도록 leading system message에 병합한다. Edge는 이 표시가 있는 실행에서만 assistant content의 text tool-call을 OpenAI-compatible tool_calls로 복원할 수 있다.
  • NodeCommandRequest.type: 실행이 아닌 조회/제어성 명령이다. adapter execution 요청과 섞지 않는다.
  • NodeConfigPayload.adapters: Edge가 Node에 내려주는 adapter instance 설정이다.
  • NodeReadyRequest.node_id: RegisterResponse가 돌려준 Node identity다. Edge registry의 internal connection generation은 이 wire/config field로 노출하지 않으며, Edge는 (node_id, current client) ownership 비교로 stale ready를 거부한다.
  • NodeReadyResponse.ready: current pending owner의 첫 ready transition과 이미 ready인 같은 owner의 duplicate ready에서 true다. 첫 transition만 provider resource activation, stranded provider-pool waiter pump, node.connected event를 만든다. stale/superseded/rejected connection은 false와 reason을 받고 session을 닫아 reconnect해야 한다.
  • AdapterConfig.name: node 내부 stable adapter instance identity다. 비어 있으면 legacy single-instance type 이름과 동등하다.
  • NodeRuntimeConfig.concurrency: legacy compatibility runtime metadata다. 실행 admission은 이 값을 node-wide global gate로 사용하지 않고 provider/resource capacity를 기준으로 한다. Node store 위치나 실행 작업 디렉터리는 이 runtime payload에 싣지 않는다.
  • reconnect.interval_sec, reconnect.max_attempts: initial connect와 established-session reconnect에 공통 적용된다. 명시적 max_attempts=0은 local shutdown까지 unlimited, 생략은 기본값 10, 양수는 정확한 유한 attempt limit, 음수는 validation error다. unlimited mode의 interval_sec는 양수여야 하며 생략은 기본값 10을 사용한다. 유한 exhaustion과 non-retryable 오류는 exit code 1, local shutdown은 정상 종료다.
  • ProviderSnapshot: legacy wire name을 유지하지만 Node 아래 resource/provider 상태 snapshot으로 해석한다. categoryapi, local_inference resource kind를 나타내며, provider-pool dispatch 대상은 Edge config models[].providers가 참조한 resource뿐이다. in_flightlong_in_flightnode_id + provider_id lease state의 현재 점유다. queued는 Edge queue에서 해당 provider를 live candidate로 포함하는 고유 pending request 수이고 long_queued는 그중 long request 수이므로 여러 provider snapshot에 같은 request가 candidate pressure로 나타날 수 있다.
  • configured Node가 disconnected/pending이면 Node snapshot은 connected=false를 유지하고 provider catalog entry도 남는다. enabled provider의 effective snapshot은 status=unavailable, health=offline, capacity/in-flight/queued/long-context 관련 수치가 모두 0이다. reconnect ready 뒤에는 같은 resource identity의 새 generation으로 configured capacity와 admission eligibility가 복구된다.
  • Node adapter instance는 normalized RunRequestProviderTunnelRequest가 공유하는 local capacity gate를 사용한다. 이 gate는 Edge provider lease를 복제하는 분산 admission이 아니라 Edge queue를 우회한 실행으로부터 같은 backend를 보호하는 defense-in-depth다.

금지 사항

  • Control Plane이나 Client가 Node에 직접 연결하거나 직접 스케줄링하는 계약을 만들지 않는다.
  • 외부 API의 model 용어를 이 내부 경계의 대표 실행 식별자로 되살리지 않는다.
  • proto/gen/iop/*.pb.go 생성물을 직접 수정하지 않는다.
  • transport handler에서 console/HTTP/A2A 표면 응답을 직접 만들지 않는다. 표면별 변환은 Edge service/input 계층에 둔다.
  • Node address, token, transport internals를 Control Plane status 계약으로 노출하지 않는다.
  • accepted registration만으로 Node를 dispatch candidate, connected snapshot/event 또는 config refresh recipient로 취급하지 않는다.
  • provider lease 반환, generation fencing, queue settlement 같은 correctness 전이를 drop 가능한 node event fanout의 성공에 의존시키지 않는다.
  • OS service/Task Scheduler restart를 retryable initial connect 또는 장기 outage 복구의 correctness owner로 사용하지 않는다.
  • Do not send provider plaintext, at-rest ciphertext, the recipient private key, or the issuer private key in NodeConfigPayload, logs, metrics, events, or tunnel metadata.
  • Do not open a lease before adapter capacity admission, cache plaintext across requests, accept a lease for another Node/target/revision/generation, or fall back to a different same-model credential slot after a bound route fails.

변경 시 확인할 코드/테스트

  • proto/iop/runtime.proto
  • apps/edge/internal/transport/*_test.go
  • apps/node/internal/transport/*_test.go
  • apps/edge/internal/bootstrap/reconnect_readiness_integration_test.go
  • apps/edge/internal/openai/provider_dispatch_test.go
  • apps/edge/internal/openai/provider_selection_test.go
  • apps/edge/internal/openai/provider_tunnel_test.go
  • apps/edge/internal/openai/cancellation_routes_test.go
  • apps/node/internal/adapters/openai_compat/*_test.go
  • apps/node/internal/adapters/vllm/*_test.go
  • apps/edge/internal/node/mapper_test.go
  • apps/node/internal/adapters/config_set_test.go
  • apps/node/internal/adapters/adapters_blackbox_test.go
  • apps/node/internal/node/provider_tunnel_credential_test.go
  • packages/go/credentiallease/envelope_test.go
  • proto 변경 시 make proto, Client가 소비하면 make proto-dart