iop/apps/edge
toki f836c91dd7 chore(roadmap): OTO 제거 마일스톤을 마무리한다
IOP 활성 코드와 문서에서 OTO 전용 잔여 fixture를 제거했고, 해당 마일스톤을 완료 후보 상태로 동기화한다.
2026-06-06 06:23:56 +09:00
..
cmd/edge chore(roadmap): OTO 제거 마일스톤을 마무리한다 2026-06-06 06:23:56 +09:00
internal refactor(edge)!: OTO 전용 경로를 제거한다 2026-06-05 21:06:22 +09:00
Dockerfile feat(edge): envsubst 템플릿 기반으로 설정을 전환한다 2026-06-02 18:06:44 +09:00
README.md Plan multi-edge operations milestone 2026-06-03 19:31:46 +09:00

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)을 제공하며, 이 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 store
  • internal/service: SubmitRun, TerminateSession, UsageStatus, ListNodes, ResolveNode
  • internal/openai: OpenAI-compatible HTTP serving 표면. 외부 model은 이 경계에서 내부 adapter + target으로 변환된다.
  • internal/events: RunEvent, EdgeNodeEvent in-process publish/subscribe fanout
  • cmd/edge: serve/console CLI 진입점과 console 출력 포맷

실행 요청과 이벤트는 같은 TCP 연결을 공유하지만, 메시지 타입과 내부 파이프라인은 분리한다. RunRequest, CancelRequest, NodeCommandRequest는 edge가 node에 보내는 command/request 계열이고, RunEvent, EdgeNodeEvent는 node와 edge 내부에서 발생한 event 계열이다.

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.shscripts/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.yamlconsole.target이 가리키는 CLI profile command가 node 호스트에서 실행 가능해야 한다. 기본 예시는 opencode이며, claude, claude-tui, antigravity, codex, cline-dgx 등으로 바꿀 수 있다.

같은 cli 어댑터 안에 claude, claude-tui, antigravity, codex, opencode, cline profile을 포함할 수 있다. 예를 들어 claudeclaude -p 기반 one-shot profile로 두고, claude-tuipersistent: true, terminal: true, mode: "persistent-lazy"로 첫 요청 시 일반 Claude Code TUI 세션을 띄운다. antigravitymode: "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 조합도 설정할 수 있다. opencodemode: "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 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 initnode register를 수행해 현재 host 기준 bootstrap URL을 확정한다. Node host는 <artifact-base-url>/bootstrap/node.sh 하나를 실행하고, bootstrap script가 OS/architecture를 감지해 맞는 Node artifact를 받는다. 특정 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를 지정해야 덮어쓴다.

운영 표면 경계

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, 단일 Edge serve/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/transportNodeCommandResponse.result 맵을 키 정렬 순서로 출력한다. /sessionssession.N.* structured key가 있으면 mode/target/session 을 행별로 묶은 grouped output으로 표시하고, 없으면 키 정렬 출력으로 fallback한다. /statusclaude, 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.yamlopenai 섹션을 켜면 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) 계열에서 다룬다.

openai:
  enabled: true
  listen: "0.0.0.0:8080"
  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://192.168.0.91:11434"
        context_size: 262144

openai.target이 비어 있으면 HTTP 요청의 model 값을 내부 target으로 사용한다. 값이 있으면 Cline 같은 외부 agent가 보낸 model과 무관하게 YAML의 target으로 고정 라우팅한다.

기본 non-streaming과 streaming SSE 응답을 모두 지원한다.

curl -s http://127.0.0.1:8080/v1/chat/completions \
  -H 'Content-Type: application/json' \
  -d '{"model":"qwen3.6:35b-a3b-bf16","messages":[{"role":"user","content":"hello"}]}'

vLLM은 같은 adapter + target 경계를 쓰도록 설정과 /v1/models capability 조회 skeleton만 먼저 열어두었다. 실제 completion 실행은 다음 단계에서 붙인다.

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 작업 위임과 상태 공유에 맞는 경로다.

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를 제공한다.