| .. | ||
| cmd/edge | ||
| internal | ||
| Dockerfile | ||
| README.md | ||
edge — Edge Execution Group Controller
여러 Node를 하나의 로컬 실행 그룹으로 묶고, IOP 내부 TCP/protobuf 프로토콜을 통해 adapter execution 요청과 이벤트 스트림을 중계한다.
Edge는 Node registry, node configuration, runtime routing, stream relay를 담당하는 백엔드 실행 그룹 컨트롤러다. 현재는 진단용 ops console과 함께 최소 OpenAI-compatible HTTP API 표면(/v1/models, /v1/chat/completions, /v1/responses)을 제공하며, 이 HTTP 표면은 edge service를 통해 기존 edge-node transport를 사용한다.
현재 상태: 초기 구현 node 등록/레지스트리/transport와 edge 콘솔 기반 수동 통신 테스트가 구현되어 있다.
내부 경계
edge-local ops console은 최종 API가 아니라 diagnostic surface다. ops console의 / 명령은 직접 transport를 다루지 않고 apps/edge/internal/service를 호출한다. 이후 HTTP/API handler가 추가되면 같은 service를 호출하고, ops console은 필요하면 수동 테스트 도구로만 남긴다.
ops console은 edge-local diagnostic surface이고, HTTP/API는 central/remote management surface다.
현재 edge 내부 흐름은 다음처럼 나뉜다.
internal/transport: node TCP/protobuf 연결, register handshake, protobuf message 수신internal/node: node registry와 사전 등록 node storeinternal/service:SubmitRun,TerminateSession,UsageStatus,ListNodes,ResolveNodeinternal/openai: OpenAI-compatible HTTP serving 표면. 외부model은 이 경계에서 내부adapter + target으로 변환된다.internal/events:RunEvent,EdgeNodeEventin-process publish/subscribe fanoutcmd/edge: serve/console CLI 진입점과 console 출력 포맷
실행 요청과 이벤트는 같은 TCP 연결을 공유하지만, 메시지 타입과 내부 파이프라인은 분리한다. RunRequest, CancelRequest, NodeCommandRequest는 edge가 node에 보내는 command/request 계열이고, RunEvent, EdgeNodeEvent는 node와 edge 내부에서 발생한 event 계열이다.
계약 원문은 Edge-Node runtime wire는 repo root의 agent-contract/inner/edge-node-runtime-wire.md, Edge config/runtime refresh는 agent-contract/inner/edge-config-runtime-refresh.md를 기준으로 한다.
internal/events.Bus는 Edge 안의 in-process fanout과 bounded replay를 담당한다. 현재 replay는 늦게 붙은 console/API handler를 위한 최근 이벤트 버퍼이지 durable history가 아니며, Node local SQLite 실행 이력과 다른 책임이다. 전역 이력 저장, audit, query API는 후속 Control Plane/Client 경계에서 별도 설계한다.
원격 CLI adapter 개발 진단 흐름
field 사용자 기본 경로는 iop-edge가 제시한 bootstrap 명령으로 Node를 연결하는 흐름이다. scripts/dev/edge.sh와 scripts/dev/node.sh는 repo 내부 개발 진단 helper이며 공식 사용자 UX로 안내하지 않는다.
개발 진단의 기준은 make test-e2e나 smoke 통과만이 아니라, edge console에서 메시지와 command를 직접 보내 결과가 edge 화면에 표시되는지 확인하는 것이다.
구체적인 내부 진단 절차는 agent-ops/skills/project/e2e-smoke/SKILL.md로 모은다.
make test-e2e 명령은 scripts/e2e-smoke.sh를 실행하는 보조 smoke이며, 최소 생존 확인에는 쓸 수 있지만 완료 기준을 대체하지 않는다.
실제 외부 프로필(예: antigravity)을 보조 smoke로 테스트하려면 IOP_E2E_PROFILE=antigravity make test-e2e와 같이 환경 변수를 지정한다.
전제 조건: configs/edge.yaml의 console.target이 가리키는 CLI profile command가 node 호스트에서 실행 가능해야 한다. 기본 예시는 opencode이며, claude, claude-tui, antigravity, codex, cline-dgx 등으로 바꿀 수 있다.
같은 cli 어댑터 안에 claude, claude-tui, antigravity, codex, opencode, cline profile을 포함할 수 있다. 예를 들어 claude는 claude -p 기반 one-shot profile로 두고, claude-tui는 persistent: true, terminal: true, mode: "persistent-lazy"로 첫 요청 시 일반 Claude Code TUI 세션을 띄운다. antigravity는 mode: "antigravity-print"로 agy --dangerously-skip-permissions --print <prompt>를 실행하고, 같은 IOP session의 다음 요청은 agy --conversation <conversation_id> --print <prompt> 형태로 이어간다. codex exec --dangerously-bypass-approvals-and-sandbox, cline -y --json --config /config/.cline/profiles/ollama-dgx <prompt>처럼 다른 headless 조합도 설정할 수 있다. opencode는 mode: "opencode-sse" profile로 opencode serve의 HTTP/SSE 인터페이스를 사용하며, args의 --model, --dangerously-skip-permissions, --title 등은 그대로 유지하면 된다.
기준 출력 예시:
edge> /nodes
test-node (test-node)
edge> Convert token iop_manual_one and reply only with converted token
[edge] sent run_id=manual-... node=test-node adapter=cli target=fake-cli session=default background=false
[node0-evt] start run_id=manual-...
[node0-msg] IOP_MANUAL_ONE_OK
[node0-msg] IOP_MANUAL_ONE_TAIL
[node0-evt] complete run_id=manual-... detail="idle-timeout"
edge> Convert token iop_manual_two and reply only with converted token
[edge] sent run_id=manual-... node=test-node adapter=cli target=fake-cli session=default background=false
[node0-evt] start run_id=manual-...
[node0-msg] IOP_MANUAL_TWO_OK
[node0-msg] IOP_MANUAL_TWO_TAIL
[node0-evt] complete run_id=manual-... detail="idle-timeout"
edge> /capabilities
[node0-capabilities] target=fake-cli session=default
adapter = cli
max_concurrency = 4
targets = fake-cli
edge> /transport
[node0-transport] target=fake-cli session=default
adapter = cli
connected = true
node_id = test-node
session_id = default
state = connected
target = fake-cli
edge> /sessions
[node0-sessions] target=fake-cli session=default
sessions: 1
[0] mode=persistent target=fake-cli session=default
edge> /terminate-session
terminated session default node=test-node
Edge CLI
운영 호스트에서 iop-edge 바이너리는 다음 명령을 제공한다.
iop-edge serve --config /etc/iop/edge.yaml
iop-edge console --config /etc/iop/edge.yaml
iop-edge version
iop-edge env
iop-edge config init
iop-edge config print --config /etc/iop/edge.yaml
iop-edge config check --config /etc/iop/edge.yaml
iop-edge config refresh --mode dry-run --config /etc/iop/edge.yaml
iop-edge config refresh --mode apply --config /etc/iop/edge.yaml
iop-edge bootstrap pack
iop-edge node register <node-id>
iop-edge nodes list
iop-edge smoke openai --model <model>
# host setup (systemd unit, user/group, data dir, config 템플릿 준비)
sudo iop-edge setup --dry-run --binary /usr/local/bin/iop-edge
sudo iop-edge setup --enable --start --binary /usr/local/bin/iop-edge
Edge 배포 archive는 repo root에서 make build로 만든다.
archive에는 iop-edge와 Linux/macOS/Windows arm64/amd64 Node bootstrap용 artifacts/가 들어가며, Edge host에서 config init과 node register를 수행해 현재 host 기준 bootstrap URL을 확정한다.
Node host는 node register 출력에 포함된 OS별 bootstrap 명령 원문을 그대로 실행한다. Linux/macOS는 generated curl | bash 명령을 사용하고, Windows native 환경은 generated PowerShell .ps1 bootstrap과 Start-IopNode 함수를 사용한다. Windows에서 bash를 요구하지 않는다.
기본 bootstrap은 token, artifact URL, Edge address처럼 Node 연결에 필요한 최소값만 전달한다. provider endpoint, capacity, model mapping 같은 추가 설정은 Edge config와 register response의 기본값을 따른다.
사용자 안내용 bootstrap 명령은 실제 URL과 실제 token이 들어간 완성된 한 줄 명령으로 제공한다. README나 tracked 문서에는 token 원문을 남기지 않는다.
특정 Node target만 담긴 개발용 package는 make build NODE_TARGETS="<goos>-<goarch>"로 만들 수 있다. Edge만 갱신할 때는 make build-edge && make pack-edge, Node artifact만 갱신할 때는 make pack-node-target NODE_TARGET=<goos>-<goarch> && make pack-edge를 사용한다.
setup의 --config 기본값은 /etc/iop/edge.yaml이다. dev 단계 명령(serve, console, config init/print/check, env, node register)의 root persistent --config 기본값은 bundle-local edge.yaml (없을 시 executable-adjacent edge.yaml, 그 다음 repo dev fallback configs/edge.yaml)로 확인된다. 기존 설정 파일은 그대로 두며 --overwrite-config를 지정해야 덮어쓴다.
Config Refresh와 Node Reconnect
실행 중 Edge config를 바꿀 때는 먼저 후보 edge.yaml을 저장하고 config check와 refresh dry-run을 통과시킨 뒤 apply한다.
refresh:
enabled: true
listen: "127.0.0.1:19093"
iop-edge --config edge.yaml config check
iop-edge --config edge.yaml config refresh --mode dry-run --addr 127.0.0.1:19093
iop-edge --config edge.yaml config refresh --mode apply --addr 127.0.0.1:19093
refresh admin API는 operator-local 표면이다. 별도 접근 제어 계층 없이 public interface에 열지 않는다.
provider capacity, provider mapping, model catalog, Node provider 설정, Node runtime concurrency처럼 runtime mutable로 분류된 변경은 refresh apply 결과가 applied로 반환되고 연결된 Node에 새 config payload가 전달된다. listener address, Edge identity, bootstrap artifact dir처럼 live apply가 위험한 변경은 restart_required로 보고되며 조용히 적용하지 않는다. invalid YAML이나 검증 실패는 rejected로 끝나고 기존 routing/runtime 상태를 유지한다.
Node는 최초 연결 성공 이후 Edge transport가 끊기면 기본 10s 간격으로 최대 10회 재접속을 시도한다. Edge process 재시작이나 일시 단절이 retry window 안에서 회복되면 사용자가 bootstrap 명령을 다시 실행하지 않아도 Node가 재등록된다. retry 한계를 넘으면 Node는 session/runtime을 정리하고 종료 경로를 탄다.
운영 표면 경계
iop-edge CLI는 Control Plane이 없거나 아직 연결되지 않은 host에서도 Edge를 설치, 진단, 복구할 수 있게 하는 local/field/fallback interface다. 상시 multi-edge 운영 UI/API는 Control Plane으로 확장하고, CLI를 fleet 운영 화면으로 키우지 않는다.
- Edge-local 필수:
config init/check/print,env,setup,bootstrap pack,node register,nodes list,smoke, 단일 Edgeserve/console - Control Plane 기본: 여러 Edge 연결/health 확인, 팀 운영 UI, 정책/권한/audit, 반복 운영/리포트, fleet-wide command
- Shared operation: Edge 상태 조회, node 및 후속 agent 등록 상태 조회, command 전달, event 수신, run/cancel/status, bootstrap 발급 상태
새 command나 운영 기능은 먼저 위 category 중 하나로 분류한다. CLI와 Control Plane에 모두 필요한 작업은 표면별로 registry 변경, run dispatch, command request 로직을 복제하지 않고 apps/edge/internal/service 같은 표면 중립 Edge operation boundary로 수렴시킨다. 후속 specialized agent enrollment, remote terminal bridge, Cline 확장도 새 command가 필요하면 이 분류를 먼저 따른다.
Console 명령
| 명령 | 설명 |
|---|---|
/nodes |
연결된 node 목록 확인. 선택된 node는 *로 표시됨. |
/node <id|alias> |
요청을 보낼 명시적 node 선택 |
/session <id> |
현재 console이 사용할 logical session 변경 |
/background on|off |
background 실행 모드 토글 (on: 응답 기다리지 않음) |
/terminate-session |
현재 adapter/target/session_id의 worker process 종료 |
/status |
현재 선택된 target/profile의 사용량(Usage Status) 조회. claude, claude-tui, codex, antigravity target만 지원. 미지원 target은 에러를 반환한다. |
/capabilities |
현재 선택된 adapter의 지원 target 등 capability 조회 |
/sessions |
현재 선택된 adapter의 logical session 목록 조회 |
/transport |
현재 선택된 node의 transport/runtime 상태 조회 |
/exit |
콘솔 종료 |
/capabilities와 /transport는 NodeCommandResponse.result 맵을 키 정렬 순서로 출력한다. /sessions는 session.N.* structured key가 있으면 mode/target/session 을 행별로 묶은 grouped output으로 표시하고, 없으면 키 정렬 출력으로 fallback한다. /status는 claude, claude-tui, codex, antigravity target에서만 지원된다. usage limit이 파싱되면 human-readable 형태로, 파싱에 실패하면 parse_status 주석과 함께 raw output을 표시한다. 미지원 target(fake-cli 등)은 error: node reported error: status check not supported for target ...을 반환한다. adapter가 명령을 지원하지 않거나 응답을 비워둔 경우 명시적인 unsupported / empty-payload 메시지가 출력된다.
멀티포인트 라우팅 (Multi-Point Routing)
edge는 여러 대의 node가 동시에 연결된 환경을 지원한다.
- 명시적 선택:
/node <alias>또는/node <node_id>명령으로 특정 node를 고정할 수 있다. - Single-node Fallback: 연결된 node가 정확히 1개일 때는 선택 없이도 해당 node가 자동 지정된다.
- Ambiguous Error: node가 2개 이상일 때 node 선택 없이 메시지를 보내면 에러가 발생하며 선택을 요구한다.
- Node-aware Events: 모든 이벤트와 메시지 출력에
[node-{alias}-...]접두어가 붙어 출처를 식별할 수 있다. - Lifecycle Events: node 등록/연결 해제는 실행 스트림과 분리된
EdgeNodeEvent로 인식하며, 콘솔에서는connected/disconnected이벤트와 reason을 출력한다.
Transport 1개 · Logical Session 여러 개
edge-node transport 연결은 node id당 1개 TCP 연결만 유지한다. 그 연결 위에서 edge는 session_id를 지정해 node의 cli adapter가 관리하는 개별 worker process에 접근한다. 같은 codex profile이라도 session_id가 다르면 독립적인 장수 process다.
- cancel run: 현재 run 중단, session process 유지
- terminate session: session process 명시 종료 (
/terminate-session또는CancelAction_TERMINATE_SESSION)
프롬프트 없이 TCP/protobuf edge 서버만 실행하려면 기존처럼 go run ./apps/edge/cmd/edge serve -c configs/edge.yaml를 사용한다.
Edge Input Surfaces
Edge 외부 입력은 OpenAI-compatible HTTP API와 A2A JSON-RPC HTTP API 두 방식으로 정리한다. 둘 다 Edge에서 받은 외부 요청을 내부 adapter + target 실행으로 변환하고, 실제 실행은 apps/edge/internal/service와 edge-node transport를 통해 처리한다.
OpenAI-Compatible Serving
configs/edge.yaml의 openai 섹션을 켜면 edge가 별도 HTTP listener를 열고 외부 agent가 OpenAI API 형태로 접속할 수 있다. 1차 구현은 Ollama를 주 테스트 경로로 삼는다.
이 표면은 외부 모델 클라이언트 호환을 위한 표준 경로다. Edge/Node 운영 제어, CLI logical session, background run, cancel/terminate-session, capabilities/status/session/transport command, node lifecycle event 같은 IOP 고유 기능은 OpenAI-compatible 요청에 억지로 싣지 않고 IOP native protocol(protobuf-socket) 계열에서 다룬다.
외부 프로젝트가 참조할 요청 계약 원문은 repo root의 agent-contract/outer/openai-compatible-api.md에 둔다. CLI agent route에서 사용할 workspace는 OpenAI request의 metadata.workspace에 둔다. Workspace authoring handoff의 요청 shape도 같은 계약 원문을 기준으로 한다.
openai:
enabled: true
listen: "0.0.0.0:18081"
adapter: "ollama"
target: "qwen3.6:35b-a3b-bf16"
models:
- "qwen3.6:35b-a3b-bf16"
session_id: "cline"
timeout_sec: 300
strict_output: true
strict_stream_buffer: false
nodes:
- id: "node-dgx-01"
adapters:
ollama:
enabled: true
base_url: "http://ollama-host.local:11434"
context_size: 262144
openai.target이 비어 있으면 HTTP 요청의 model 값을 내부 target으로 사용한다. 값이 있으면 Cline 같은 외부 agent가 보낸 model과 무관하게 YAML의 target으로 고정 라우팅한다.
openai.model_routes를 설정하면 외부 model id별로 adapter/target을 달리 지정할 수 있다. /v1/models는 catalog에 등록된 model id만 노출하고, 매칭된 route는 top-level adapter/target보다 우선한다. workspace_required: true는 CLI agent target처럼 호출자가 metadata.workspace를 반드시 제공해야 하는 route를 표시한다. fallback route(catalog 미매칭)는 항상 workspace_required: false다.
openai:
model_routes:
- model: "codex"
adapter: "cli"
target: "codex"
workspace_required: true
- model: "llama3"
adapter: "ollama"
target: "llama3:8b"
/v1/chat/completions는 기본 non-streaming과 streaming SSE 응답을 모두 지원한다. /v1/responses는 현재 non-streaming 요청만 지원한다. 두 endpoint 모두 metadata.workspace를 run workspace로 전달한다. metadata는 OpenAI 표준의 caller-defined string metadata container로 보고, IOP가 특별히 해석하는 key는 workspace뿐이다. /v1/responses의 max_output_tokens, temperature, top_p, instructions, background와 /v1/chat/completions의 max_tokens, max_completion_tokens, temperature, top_p, think, reasoning_effort, thinking_token_budget, include_reasoning 등은 OpenAI API처럼 top-level field에 둔다. metadata.source, metadata.cli, metadata.inference, 소비자 전용 metadata wrapper, options, chat_template_kwargs, format, keep_alive 같은 provider/Ollama 전용 request field는 지원하지 않는다.
curl -s http://127.0.0.1:18081/v1/chat/completions \
-H 'Content-Type: application/json' \
-d '{"model":"qwen3.6:35b-a3b-bf16","messages":[{"role":"user","content":"hello"}]}'
curl -s http://127.0.0.1:18081/v1/responses \
-H 'Content-Type: application/json' \
-d '{"model":"qwen3.6:35b-a3b-bf16","instructions":"reply briefly","input":"hello","stream":false,"background":false,"max_output_tokens":256,"temperature":0,"top_p":1,"metadata":{"request_id":"example","workspace":"/config/workspace/iop","task_id":"task-123"}}'
vLLM 같은 OpenAI-compatible inference server는 openai_compat adapter와 provider: "vllm" instance로 연결한다. 이 경로는 /v1/models, non-streaming/streaming /v1/chat/completions, provider header, 표준 top-level request field 전달을 지원하며, Edge의 model route alias를 실제 served model로 매핑한다.
Model Provider Pool 호환 방향
현재 openai.model_routes는 외부 model id를 단일 adapter + target으로 연결하는 호환 catalog다. 새 provider pool 계약에서는 models[].id를 OpenAI-compatible model key이자 queue/routing canonical key로 유지하고, models[].providers가 provider id별 실제 served model name을 가리킨다.
config/provider pool 변경 계약은 repo root의 agent-contract/inner/edge-config-runtime-refresh.md를 기준으로 한다.
마이그레이션 기준은 1:1 route를 단일 provider를 가진 models[] 항목으로 표현하는 것이다. 기존 Node provider instance 설정(openai_compat_instances, legacy vllm instance 등)은 provider id, provider type/category, served models, capacity/queue policy를 채우는 입력으로 사용하고, Edge는 선택된 provider의 served model name으로 target을 rewrite한 뒤 Node에는 기존처럼 adapter + target 실행 요청을 전달한다. provider pool이 구현되기 전까지 openai.model_routes는 fallback/compat 경로로 유지한다.
최신 provider pool schema 예시:
# models[] 는 top-level Edge config field로, canonical routing key와
# provider pool mapping을 정의한다.
models:
- id: "qwen3.6:35b"
display_name: "Qwen 3.6 35B"
providers:
vllm-gpu: "nvidia/Qwen3.6-35B-A3B-NVFP4"
ollama-local: "qwen3.6:35b"
# nodes[].providers[] 는 provider candidate schema다.
# models[].providers 의 key 와 매칭되어야 한다.
nodes:
- id: "node-gpu-01"
alias: "gpu-node"
providers:
- id: "vllm-gpu"
type: "vllm"
category: "api"
adapter: "openai_compat"
models:
- "nvidia/Qwen3.6-35B-A3B-NVFP4"
capacity: 4
max_queue: 16
queue_timeout_ms: 30000
lifecycle_capabilities:
- "list_models"
- id: "ollama-local"
type: "ollama"
category: "local_inference"
adapter: "ollama"
models:
- "qwen3.6:35b"
capacity: 2
max_queue: 8
queue_timeout_ms: 30000
lifecycle_capabilities:
- "list_models"
- "load_model"
- "unload_model"
- "pull_model"
migration 예시 — 기존 route를 provider pool로 이동:
# 기존 (legacy)
openai:
model_routes:
- model: "codex"
adapter: "cli"
target: "codex"
# 위를 provider pool로 표현하면 아래 와 같다.
models:
- id: "codex"
providers:
cli-codex: "codex"
nodes:
- id: "node-cli-01"
providers:
- id: "cli-codex"
type: "cli"
category: "cli"
adapter: "cli"
models:
- "codex"
A2A Agent Input
A2A JSON-RPC HTTP API는 NomadCode Core나 외부 agent가 Edge 실행 그룹에 작업을 위임하고 Task 상태, artifact, cancel/polling을 공유해야 할 때 사용하는 입력 표면이다. OpenAI-compatible API가 단순 모델/chat completion 호환에 맞는 경로라면, A2A는 agent-to-agent 작업 위임과 상태 공유에 맞는 경로다.
외부 caller가 참조할 A2A 요청 계약 원문은 repo root의 agent-contract/outer/a2a-json-rpc-api.md를 기준으로 한다.
1차 지원 범위는 message/send, tasks/get, tasks/cancel이다. message/stream, push notification, tasks/list는 후속 단계에서 다룬다. 구현은 OpenAI-compatible serving과 같은 Edge input 관리 계층에서 lifecycle/config를 묶고, 내부 실행은 동일하게 adapter + target으로 변환한다.
설정 예시
a2a:
enabled: true
listen: "0.0.0.0:8081"
path: "/a2a"
node: "node0" # 빈 값이면 단일 연결 node 자동 선택
adapter: "cli"
target: "claude"
session_id: "a2a"
timeout_sec: 120
bearer_token: "" # 빈 값이면 auth 비활성화
지원 메서드
| 메서드 | 설명 |
|---|---|
message/send |
실행 요청. configuration.blocking=true(기본)이면 완료까지 대기 후 최종 Task 반환. false이면 working Task를 즉시 반환하고 background 수집. |
tasks/get |
task ID로 현재 Task 상태/artifact/history 조회. |
tasks/cancel |
실행 중인 Task에 cancel 요청. CANCEL_ACTION_CANCEL_RUN을 node에 전송. |
Agent card
GET /.well-known/agent.json으로 최소 agent metadata를 제공한다.