feat: update control-plane-client roadmap, add edge Dockerfile & config, update docker-compose & dev scripts

This commit is contained in:
toki 2026-06-02 15:37:34 +09:00
parent 7ccefd7f9f
commit 68f6e08da8
7 changed files with 127 additions and 113 deletions

View file

@ -16,17 +16,20 @@ Client는 NomadCode 전용 화면이 아니라 IOP가 독립적으로 운영되
## 구현 잠금
- 상태: 잠금
- 결정 필요: 아래 체크리스트
- [ ] Client의 첫 운영 화면 범위를 Edge/Node 상태 조회 중심으로 둘지, 명령/설정 변경까지 포함할지 결정한다.
- [ ] IOP client가 범용 운영 UI로 남기 위해 제외할 NomadCode 전용 UX 경계를 결정한다.
- 상태: 해제
- 결정 필요: 없음
- 결정 결과:
- Client의 첫 운영 화면은 Edge/Node 운영 lifecycle을 얇게 파악하는 데 필요한 설정 조회, Node registry, 연결 상태, 로그/이벤트 중심으로 둔다.
- 설정 변경, 명령 실행, 권한이 필요한 조작 UX는 첫 화면에 무겁게 넣지 않고 조회/상태/근거가 안정화된 뒤 후속으로 덧붙인다.
- IOP client는 범용 운영 UI로 유지하며 NomadCode 전용 project/workspace/webview/diff/PR/branch/commit UX를 포함하지 않는다.
## 범위
- Control Plane Go server의 Edge connection 관리
- Client-Control Plane wire surface와 Control Plane-Edge Wire Baseline 위의 운영 표면 확장
- Flutter client의 Runtime/Automation/Serving 운영 화면 기초
- Edge 상태 조회, 설정 변경, 명령 전달, 이벤트 수신
- Edge/Node 설정 조회, Node registry, 연결 상태, 로그/이벤트 수신을 중심으로 한 얇은 lifecycle 운영 화면
- 설정 변경과 명령 전달은 lifecycle 조회 표면 위에 붙일 후속 확장 후보로 정리
- Edge agent 생성과 bootstrap command 발급 상태 조회/관리
- remote terminal bridge session 생성/조회/권한 확인 표면
- IOP 책임 범위 안의 model serving, automation, session, routing, policy, history, audit 운영 UX
@ -36,14 +39,16 @@ Client는 NomadCode 전용 화면이 아니라 IOP가 독립적으로 운영되
### Epic: [edge-ops-boundary] Edge Operations Boundary
- [ ] [edge-mediated] Control Plane은 Node를 직접 연결하거나 직접 스케줄링하지 않는다.
- [ ] [edge-status] Control Plane은 선행 Wire Baseline으로 연결된 하나 이상의 Edge 상태를 조회할 수 있다. 검증: Edge 연결 상태 조회 경로가 동작하는지 확인한다.
- [ ] [edge-command-events] Edge 명령 전달과 이벤트 수신 흐름이 Control Plane-Edge Wire Baseline 위의 IOP Wire Protocol 기준으로 문서화된다.
- [ ] [client-via-cp] Client는 Control Plane을 통해 Edge 운영 상태를 조회한다.
- [ ] [edge-status] Control Plane은 선행 Wire Baseline으로 연결된 하나 이상의 Edge 연결 상태를 조회할 수 있다. 검증: Edge 연결 상태 조회 경로가 동작하는지 확인한다.
- [ ] [node-registry] Control Plane은 Edge가 보고하는 Node registry와 Node 연결 상태를 운영 표면에 전달할 수 있다. 검증: Node 목록과 연결 상태가 Control Plane을 통해 조회된다.
- [ ] [edge-node-config-view] Control Plane/Client는 Edge/Node의 현재 운영 설정 요약을 조회 중심으로 보여준다. 검증: 설정 원본이나 effective config 요약이 조작 없이 표시된다.
- [ ] [edge-event-logs] Edge lifecycle event와 기본 로그/상태 이벤트 수신 흐름이 Control Plane-Edge Wire Baseline 위의 IOP Wire Protocol 기준으로 정리된다.
- [ ] [client-via-cp] Client는 Control Plane을 통해 Edge/Node 운영 lifecycle 상태를 조회한다.
### Epic: [client-wire-surface] Client Wire Surface
- [x] [client-boundary] Flutter client는 IOP 자체 운영 UI/UX이며 NomadCode에 맞춘 전용 shell로 만들지 않는다.
- [ ] [client-status] Client는 Edge/Node/agent 상태를 사용자에게 보여줄 수 있다. 검증: Client 상태 화면 또는 상태 표시 경로를 확인한다.
- [ ] [client-status] Client는 Edge/Node 설정 요약, registry, 연결 상태, 로그/이벤트를 lifecycle 중심으로 얇게 보여줄 수 있다. 검증: Client 상태 화면 또는 상태 표시 경로를 확인한다.
- [x] [server-bridge] 브라우저 직접 TCP가 어려운 구간은 Control Plane server-side bridge를 둔다.
- [x] [client-wire-hello] Client-Control Plane proto-socket WebSocket hello handshake scaffold가 구현되고 테스트되어 있다. 검증: Control Plane과 client 테스트에서 hello handshake가 통과한다.
- [x] [http-boundary] `net/http`는 health/readiness/bootstrap 같은 보조 endpoint 용도로 유지한다.
@ -64,6 +69,7 @@ Client는 NomadCode 전용 화면이 아니라 IOP가 독립적으로 운영되
- Control Plane을 Kubernetes식 중앙 스케줄러로 만드는 작업
- Edge 내부 DB를 Control Plane DB로 대체
- 모든 런타임 상태를 Control Plane 단일 원본으로 만드는 작업
- 첫 운영 화면에서 설정 변경, 명령 실행, 권한 조작 UX를 무겁게 구현하는 작업
- NomadCode Agent Shell 자체 구현
- NomadCode 전용 Project Workspace, diff/PR/branch/commit UX
@ -73,6 +79,7 @@ Client는 NomadCode 전용 화면이 아니라 IOP가 독립적으로 운영되
- 표준선(선택): Control Plane은 Node를 직접 스케줄링하지 않고 Edge를 통해 관찰/제어한다.
- 표준선(선택): Control Plane-Edge 통신 레일은 `Control Plane-Edge Wire Baseline` 선행 Milestone에서 먼저 구축한다.
- 표준선(선택): Client-Control Plane의 첫 wire surface는 `/client` WebSocket hello handshake를 기준으로 둔다.
- 표준선(선택): 첫 운영 화면은 설정 조회, Node registry, 연결 상태, 로그/이벤트처럼 한 lifecycle을 이해하는 데 필요한 얇은 조회 표면을 우선한다.
- 선행 작업: Control Plane-Edge Wire Baseline, Flutter-first Client 마이그레이션, Edge Command와 Control Plane 운영 경계, Agent Bootstrap과 OTO 등록, 정책/이력/감사 일부 기준
- 후속 작업: Multi-Edge 운영
- 확인 필요: Client 첫 화면 범위, NomadCode 전용 UX 제외 기준
- 확인 필요: 없음

View file

@ -1,94 +0,0 @@
# Milestone: Embeddable IOP Console Workbench
## 위치
- Roadmap: `agent-roadmap/ROADMAP.md`
- Phase: `agent-roadmap/phase/control-plane-portal-ops/PHASE.md`
## 목표
IOP standalone 앱은 application shell로 유지하되, 실제 IOP 관리 기능과 UI 위젯 전체는 Flutter package 형태로 분리한다. 이 package는 IOP 앱에서는 좌측 icon rail + center content 구조로 보이고, NomadCode 같은 host 앱에서는 같은 native Flutter 위젯이 별도 제품 shell 안에 그대로 들어갈 수 있어야 한다.
## 상태
[진행중]
## 구현 잠금
- 상태: 해제
- 결정 필요: 없음
## 범위
- `iop/apps/client`를 standalone application shell로 유지하는 구조
- IOP 관리 화면, route/controller, rail item, agent capability를 embeddable Flutter package로 분리하는 구조
- IOP standalone에서는 좌측 icon rail만 두고 중앙 content를 전환하는 workbench layout
- host 앱이 IOP package를 선택적으로 import해 native Flutter widget으로 삽입할 수 있는 public API
- 공통 agent shell package와 IOP agent capability pack의 연결 경계
- IOP package가 NomadCode 제품 shell, WebView 조작 UX, workflow/task UX를 알지 못하게 하는 의존성 경계
## 기능
### Epic: [package-boundary] IOP console package boundary
IOP 운영 경험을 application shell에서 분리해, standalone 앱과 외부 host가 같은 관리 UI를 재사용할 수 있는 package 경계를 만든다.
- [x] [console-package] IOP 관리 UI를 `MaterialApp`/global bootstrap 없이 import 가능한 `packages/flutter/iop_console` Flutter package로 분리하고, `IopConsoleShell`, `IopConsoleSection`, `IopAgentPanel`을 public export로 제공한다. 검증: package public export가 app shell 없이 widget/section 단위로 소비 가능하다.
- [x] [standalone-shell] `iop/apps/client`는 Firebase/SystemChrome/app bootstrap과 standalone theme를 담당하고, 실제 workbench shell은 IOP console package를 감싸서 실행한다. 검증: standalone 앱에서 IOP console이 좌측 rail + center content 구조로 표시되고 agent panel 진입 widget test가 있다.
- [x] [host-agnostic] IOP console package는 NomadCode, Project/Workspace, WebView 조작, product workflow 개념을 import하거나 전제하지 않는다. 검증: package dependency graph에 NomadCode 전용 package가 없다.
- [ ] [overview-boundary] Control Plane wire status overview는 현재 `apps/client/lib/main.dart`의 host-injected content로 남아 있다. 재사용 가능한 package widget으로 옮길지, standalone host content로 유지할지 경계를 확정한다.
### Epic: [workbench-layout] IOP left-rail workbench
IOP standalone 화면은 좌측 icon rail을 기준으로 중앙 content를 전환하는 운영 workbench로 정리한다.
- [x] [left-rail] IOP standalone layout은 좌측 icon rail과 center content host를 기본 구조로 둔다.
- [x] [content-switch] Overview, Edges, Nodes, Runtime, Maintenance, Agent, Settings 영역은 rail item 선택에 따라 중앙 content를 전환한다.
- [ ] [execution-logs] Execution/Logs 영역은 별도 rail item, Runtime 하위 화면, Maintenance 하위 화면 중 어디에 둘지 정리하고 표시 경로를 추가한다.
- [ ] [stable-widgets] rail item, tooltip, selected state, content host는 host layout 크기에 따라 안정적으로 배치된다. 검증: desktop/mobile 또는 narrow/wide viewport에서 rail과 content가 겹치지 않는다.
### Epic: [agent-capability] IOP agent capability pack
공통 agent shell은 외부 package로 두고, IOP는 자신의 운영 capability만 제공한다.
- [x] [agent-shell-link] IOP console package는 공통 `agent_shell` 계열 package를 통해 채팅형 agent surface를 열 수 있다.
- [ ] [iop-capabilities] IOP agent capability는 Edge/Node 상태, runtime/adapter/target, execution, logs, maintenance command 같은 IOP 책임 범위만 포함한다.
- [ ] [capability-api] IOP capability pack은 host가 agent shell registry에 꽂을 수 있는 public API를 제공한다. 검증: IOP standalone과 외부 host가 같은 IOP capability pack을 등록할 수 있다.
### Epic: [embedding-contract] External host embedding contract
NomadCode 같은 외부 host가 IOP UI를 optional feature로 넣거나 빼도 빌드 경계가 깨지지 않도록 한다.
- [x] [public-widget] IOP package는 `IopConsoleShell`, `IopConsoleSection`, `IopAgentPanel` public entry를 제공한다.
- [ ] [config-injection] host는 Control Plane endpoint, auth/token reference, theme adapter, navigation callback을 주입하고 IOP package는 host 전역 상태를 직접 소유하지 않는다.
- [ ] [optional-consumer] IOP package는 optional consumer인 NomadCode 없이도 단독으로 테스트되고 빌드된다. 검증: IOP repository 안에서 package test/analyze 경로가 독립적으로 통과한다.
## 완료 리뷰
- 상태: 없음
- 요청일: 없음
- 완료 근거: `packages/flutter/iop_console` package boundary, left rail shell, standalone app mount, agent shell link는 구현되었으나 모든 기능 Task와 Task 안에 명시된 검증은 아직 충족되지 않았다.
- 리뷰 필요:
- [ ] 사용자가 완료 결과를 확인했다
- [ ] archive 이동을 승인했다
- 리뷰 코멘트: 없음
## 범위 제외
- NomadCode의 우측 rail, titlebar, workflow/project/webview UX 구현
- NomadCode default build와 IOP 포함 build의 조립 전략 구현
- IOP 내부 모델 라우팅, RAG, MCP/tool policy, output validation 세부 구현
- 런타임 동적 plugin framework 도입
- IOP package가 NomadCode 전용 화면을 직접 import하거나 소유하는 구조
## 작업 컨텍스트
- 관련 경로: `apps/client/`, `packages/flutter/iop_console/`, `proto/iop/`, `apps/control-plane/`
- 표준선(선택): IOP는 범용 추론/자동화 운영 엔진이며 NomadCode 전용 Agent Shell이 아니다.
- 표준선(선택): shared UI는 application shell이 아니라 package/widget/controller/capability 단위로 재사용한다.
- 표준선(선택): IOP standalone은 좌측 icon rail + center content 구조를 기본 workbench로 둔다.
- 구현 근거: `49872ae feat(client): iop_console 패키지로 클라이언트 UI를 분리한다`에서 `packages/flutter/iop_console` package, `IopConsoleShell`, `IopAgentPanel`, `apps/client` mount와 widget test가 추가되었다.
- 현재 구현 상태: package 이름은 `iop_console`으로 확정되었고 최초 rail item은 Overview, Edges, Nodes, Runtime, Maintenance, Agent, Settings다. Execution/Logs, controller/config injection, 독립 package test/analyze는 남은 작업이다.
- 선행 작업: Control Plane과 Client, Flutter-first Client 마이그레이션, Control Plane-Edge Wire Baseline
- 후속 작업: NomadCode Workbench Shell and Optional IOP Composition, Multi-Edge 운영
- 확인 필요: Execution/Logs의 화면 배치, Control Plane endpoint/auth/theme/navigation injection public API, package 독립 검증 경로를 확정한다.

29
apps/edge/Dockerfile Normal file
View file

@ -0,0 +1,29 @@
# syntax=docker/dockerfile:1
FROM golang:1.24-alpine AS builder
WORKDIR /workspace/iop
RUN apk add --no-cache git
# docker-compose builds this Dockerfile with /config/workspace as context so
# the local ../proto-socket/go replace in go.mod is available.
COPY iop/go.mod iop/go.sum ./
COPY proto-socket/go /workspace/proto-socket/go
RUN go mod download
COPY iop/ ./
RUN CGO_ENABLED=0 GOFLAGS=-trimpath go build -o /out/edge ./apps/edge/cmd/edge
FROM alpine:3.22
RUN addgroup -S iop && adduser -S iop -G iop
WORKDIR /app
COPY --from=builder /out/edge /usr/local/bin/edge
COPY --from=builder /workspace/iop/configs/edge-compose.yaml /app/configs/edge-compose.yaml
EXPOSE 9090
USER iop
ENTRYPOINT ["edge"]
CMD ["serve", "--config", "/app/configs/edge-compose.yaml"]

61
configs/edge-compose.yaml Normal file
View file

@ -0,0 +1,61 @@
edge:
id: "edge-compose"
name: "Compose Edge"
server:
listen: "0.0.0.0:9090"
advertise_host: ""
bootstrap:
artifact_base_url: ""
listen: "0.0.0.0:18080"
artifact_dir: "/tmp/iop-artifacts"
tls:
enabled: false
logging:
level: "info"
pretty: true
path: "/tmp/iop-edge/edge.log"
metrics:
port: 0
control_plane:
enabled: true
wire_addr: "control-plane:19081"
reconnect_interval_sec: 5
a2a:
enabled: false
openai:
enabled: false
console:
adapter: "cli"
target: "codex"
session_id: "default"
background: false
timeout_sec: 300
nodes:
- id: "external-node"
alias: "external-node"
token: "changeme"
adapters:
cli:
enabled: true
profiles:
codex:
command: "codex"
args: []
persistent: false
terminal: false
response_idle_timeout_ms: 1000
startup_idle_timeout_ms: 1000
output_format: "plain"
mode: "exec"
runtime:
concurrency: 1

View file

@ -9,7 +9,7 @@ services:
POSTGRES_PASSWORD: ${IOP_POSTGRES_PASSWORD:-iop_dev_password}
POSTGRES_DB: ${IOP_POSTGRES_DB:-iop-control-plane-dev}
ports:
- "${IOP_POSTGRES_PORT:-5432}:5432"
- "${IOP_POSTGRES_BIND:-127.0.0.1}:${IOP_POSTGRES_PORT:-5432}:5432"
volumes:
- control-plane-postgres:/var/lib/postgresql/data
healthcheck:
@ -23,7 +23,7 @@ services:
restart: unless-stopped
command: ["redis-server", "--appendonly", "yes"]
ports:
- "${IOP_REDIS_PORT:-6379}:6379"
- "${IOP_REDIS_BIND:-127.0.0.1}:${IOP_REDIS_PORT:-6379}:6379"
volumes:
- control-plane-redis:/data
healthcheck:
@ -42,9 +42,9 @@ services:
IOP_REDIS_URL: "redis://redis:6379/${IOP_REDIS_DB:-2}"
IOP_REDIS_KEY_PREFIX: "${IOP_REDIS_KEY_PREFIX:-iop:control-plane:dev:}"
ports:
- "${IOP_CONTROL_PLANE_HTTP_PORT:-9080}:9080"
- "${IOP_CONTROL_PLANE_WIRE_PORT:-19080}:19080"
- "${IOP_CONTROL_PLANE_EDGE_WIRE_PORT:-19081}:19081"
- "${IOP_CONTROL_PLANE_HTTP_BIND:-127.0.0.1}:${IOP_CONTROL_PLANE_HTTP_PORT:-9080}:9080"
- "${IOP_CONTROL_PLANE_WIRE_BIND:-0.0.0.0}:${IOP_CONTROL_PLANE_WIRE_PORT:-19080}:19080"
- "${IOP_CONTROL_PLANE_EDGE_WIRE_BIND:-127.0.0.1}:${IOP_CONTROL_PLANE_EDGE_WIRE_PORT:-19081}:19081"
depends_on:
postgres:
condition: service_healthy
@ -56,16 +56,27 @@ services:
timeout: 3s
retries: 5
edge:
build:
context: ..
dockerfile: iop/apps/edge/Dockerfile
restart: unless-stopped
ports:
- "${IOP_EDGE_NODE_BIND:-0.0.0.0}:${IOP_EDGE_NODE_PORT:-19090}:9090"
depends_on:
control-plane:
condition: service_healthy
web:
build:
context: ..
dockerfile: iop/apps/client/Dockerfile
args:
IOP_CONTROL_PLANE_HTTP_URL: "http://localhost:${IOP_CONTROL_PLANE_HTTP_PORT:-9080}"
IOP_CONTROL_PLANE_WIRE_URL: "ws://localhost:${IOP_CONTROL_PLANE_WIRE_PORT:-19080}/client"
IOP_CONTROL_PLANE_HTTP_URL: "${IOP_CONTROL_PLANE_HTTP_URL:-http://localhost:9080}"
IOP_CONTROL_PLANE_WIRE_URL: "${IOP_CONTROL_PLANE_WIRE_URL:-ws://localhost:19080/client}"
restart: unless-stopped
ports:
- "${IOP_WEB_PORT:-3000}:3000"
- "${IOP_WEB_BIND:-0.0.0.0}:${IOP_WEB_PORT:-13000}:3000"
depends_on:
control-plane:
condition: service_healthy

View file

@ -6,7 +6,7 @@ REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
CLIENT_DIR="$REPO_ROOT/apps/client"
WEB_HOST="${IOP_WEB_HOST:-0.0.0.0}"
WEB_PORT="${IOP_WEB_PORT:-3000}"
WEB_PORT="${IOP_WEB_PORT:-13000}"
IOP_CONTROL_PLANE_HTTP_URL="${IOP_CONTROL_PLANE_HTTP_URL:-http://localhost:9080}"
IOP_CONTROL_PLANE_WIRE_URL="${IOP_CONTROL_PLANE_WIRE_URL:-ws://localhost:19080/client}"