diff --git a/.codex b/.codex new file mode 100644 index 0000000..e69de29 diff --git a/README.md b/README.md index ca4a749..fff5e8c 100644 --- a/README.md +++ b/README.md @@ -1,54 +1,257 @@ -# IOP — Inference Operations Platform +# IOP -IOP는 단일 서비스가 아닌 플랫폼이다. 여러 앱이 협력하여 AI 모델 추론 워크로드를 처리한다. +IOP(Inference Operations Platform)는 단순한 모델 라우터나 OpenAI API proxy가 아니다. -## 앱 구성 +IOP는 **Control Plane - Edge - Node** 계층 구조를 기반으로, 여러 로컬 모델 런타임과 CLI Agent 실행 환경을 통합 관리하는 실행 오케스트레이션 플랫폼을 지향한다. 모델 서빙, CLI Agent 실행, shell/git/docker/code workspace 작업, node maintenance 작업을 같은 실행 파이프라인에서 다룰 수 있도록 만드는 것이 핵심 방향이다. -| 앱 | 책임 | +현재 프로젝트는 완성된 운영 시스템이 아니라 스켈레톤 단계다. 이 README는 현재 구현의 세부 사용법보다, 프로젝트가 향하는 구조와 경계를 명확히 설명한다. + +## Overview + +IOP의 실행 대상은 크게 두 가지다. + +- **Model Serving** + - Ollama + - vLLM + - MLX + - 그 외 로컬/원격 모델 런타임 +- **Agent / Automation Execution** + - CLI Agent + - Shell + - Git + - Docker + - Code workspace 작업 + - NomadCode 계열 자동화 작업 + +IOP는 model serving만 담당하는 시스템이 아니다. CLI Agent 실행과 node maintenance도 adapter 기반 실행으로 보고, Edge와 Node를 통해 실행 요청, 스트림, 상태, 결과를 관리하는 방향으로 설계한다. + +## Core Concept + +IOP의 중심 개념은 `adapter + target` 기반 실행이다. + +- `adapter`는 실행 방식을 나타낸다. +- `target`은 해당 adapter 안에서 실행할 구체 대상을 나타낸다. +- `execution`은 adapter와 target을 해석해 실제 Node에서 수행되는 단위다. + +예시는 다음과 같다. + +```text +adapter = ollama +target = qwen3.6 + +adapter = vllm +target = gemma4 + +adapter = cli +target = cline-dgx + +adapter = cli +target = codex-local +``` + +외부 OpenAI API 호환 계층에서는 호환성을 위해 `model` 필드가 남을 수 있다. 그러나 내부 실행 개념에서는 모델 이름만으로 전체 실행을 설명하지 않고, `adapter`, `target`, `execution`, `node adapter`, `adapter execution` 같은 용어를 우선한다. + +## Architecture + +IOP는 Control Plane이 Edge를 통해 시스템을 제어하고, Edge가 자신의 로컬 실행 그룹을 운영하는 구조를 지향한다. + +```text +Control Plane +├─ Edge Group A +│ ├─ Node 1 +│ ├─ Node 2 +│ └─ Node 3 +└─ Edge Group B + ├─ Node 4 + └─ Node 5 +``` + +Control Plane은 Node에 직접 연결하지 않는다. 전체 시스템 제어는 Edge를 통해 이뤄지고, Edge는 자신이 관리하는 Node와 로컬 런타임 상태의 원본을 가진다. + +핵심 문장은 다음과 같다. + +> Control Plane은 Edge를 통해 시스템을 제어하고, Edge는 자신의 로컬 런타임 상태를 소유하고 운영한다. + +### Control Plane + +Control Plane은 자체 서버와 프론트 페이지를 가진 중앙 관리 계층이다. + +주요 책임은 다음과 같다. + +- 여러 Edge 연결 관리 +- Edge 상태 조회 +- Edge 설정 변경 +- Edge에 명령 전달 +- Edge 이벤트 수신 +- 전체 시스템 관찰 +- Runtime 영역과 Automation 영역을 나눠 보여주는 운영 화면 제공 + +Control Plane과 Edge는 소켓 기반 연결을 사용하고, 이벤트, 상태, 명령 결과를 실시간으로 주고받는 구조를 지향한다. + +Control Plane은 전통적인 Kubernetes식 중앙 스케줄러가 아니다. 다음 책임은 Control Plane에 두지 않는다. + +- Node 직접 연결 +- Node 직접 스케줄링 +- Edge 내부 DB 대체 +- 모든 런타임 상태의 단일 원본화 +- 매 요청마다 Node 할당 판단 + +### Edge + +Edge는 단순 API gateway가 아니라 백엔드 전용 실행 그룹 컨트롤러다. + +하나의 Edge는 여러 Node를 관리하며, 특정 디바이스 그룹, 로컬 모델 그룹, 자동화 실행 그룹을 하나로 묶는 단위가 된다. Edge는 모델 서빙과 CLI Agent 실행을 모두 처리할 수 있어야 한다. + +Edge의 핵심 역할은 다음과 같다. + +- Node registry +- Node configuration +- Adapter/Profile configuration +- Runtime routing +- Job assignment +- Stream relay +- Session handling +- Local runtime state +- Execution history aggregation +- Event aggregation + +Edge는 자신의 데이터를 자체적으로 가진다. Control Plane은 Edge의 데이터를 조회하고 조정하지만, 런타임 데이터의 원본은 Edge다. + +### Node + +Node는 실제 실행자다. + +Node는 모델 런타임, CLI Agent, 도구 실행을 담당한다. Node는 Control Plane이 아니라 Edge에 연결되며, Edge가 전달한 실행 요청을 adapter execution으로 수행하고 이벤트와 결과를 되돌려준다. + +Node는 가능한 한 단순한 실행 단위로 유지한다. 정책, 전체 시스템 조정, 다중 Edge 운영 판단을 Node에 밀어 넣지 않고, 전달받은 실행을 안정적으로 수행하는 데 집중한다. + +## Execution Model + +### Adapter and Target + +IOP 내부 실행은 model 중심이 아니라 adapter 중심으로 정리한다. + +- `adapter`: `mock`, `ollama`, `vllm`, `cli` 같은 실행 구현 +- `target`: adapter 안에서 선택되는 모델, profile, agent, toolchain +- `execution`: 특정 adapter와 target으로 수행되는 단일 실행 +- `node adapter`: Node 안에 등록되어 실제 실행을 담당하는 adapter +- `adapter execution`: Node adapter가 수행하는 실행 단위 + +현재 코드에는 `RunRequest`, `ExecutionSpec`, `RuntimeEvent`, `NodeCommandRequest`처럼 이 방향을 담기 위한 타입들이 있다. 세부 계약과 schema는 작업을 진행하면서 점진적으로 정리한다. + +### Runtime Domain + +Runtime Domain은 모델 서빙 중심 실행 영역이다. + +- OpenAI API 호환 요청 +- Ollama, vLLM, MLX 같은 모델 런타임 +- 모델 라우팅 +- 추론 요청 처리 +- 모델 런타임 adapter 확장 + +이 영역에서도 내부적으로는 `adapter + target` 개념을 사용한다. 예를 들어 OpenAI 호환 요청의 `model` 값은 내부에서 특정 adapter와 target으로 해석될 수 있다. + +### Automation Domain + +Automation Domain은 CLI Agent와 도구 실행 중심 영역이다. + +- CLI Agent 실행 +- Shell, Git, Docker 작업 +- code workspace 작업 +- Plane 작업과 유지보수 작업 +- Claude CLI, Gemini CLI, Codex CLI, OpenCode, Cline 같은 실행 대상 + +NomadCode는 IOP 안에 완전히 흡수된 제품이 아니라, IOP Automation Domain 위에서 동작할 수 있는 대표 사용처로 본다. + +```text +IOP Core +└─ 공통 실행 오케스트레이션 계층 + +NomadCode +└─ IOP Automation Domain을 활용하는 개발 업무 자동화 도메인 +``` + +## Current Status + +현재 go-iop는 스켈레톤 단계다. + +- Edge-Node 소켓 기반 구조를 우선 검증 중이다. +- Node 등록, 설정 전달, 실행 요청, 스트리밍 이벤트 흐름이 점진적으로 정리되고 있다. +- CLI adapter 쪽 구현이 먼저 진행되고 있다. +- 현재 실행 이력은 Node local SQLite store에서 검증 중이다. Edge 단위 이력 집계와 로컬 실행 그룹 상태 소유권은 로드맵에 따라 정리한다. +- `mock` adapter와 dummy/TODO 구현은 개발 단계에서 정상적인 구성이다. +- Ollama/vLLM 등 모델 runtime adapter는 단계적으로 확장한다. +- Control Plane은 향후 여러 Edge 관리와 프론트 페이지 제공을 위해 추가된다. + +현재 앱 구성은 다음과 같다. + +| 경로 | 현재 의미 | |---|---| -| `apps/node` | 디바이스별 node agent. 런타임 라우팅, 추론 어댑터 실행, edge 연결을 담당한다. | -| `apps/edge` | node 등록, 레지스트리, transport, 콘솔 기반 요청 전달을 담당하는 gateway이다. | -| `apps/control-plane` | 노드 등록, 정책, 스케줄링 제어 영역이다. | -| `apps/worker` | 비동기 작업 처리 영역이다. | +| `apps/node` | Edge에 연결되어 adapter execution을 수행하는 Node agent | +| `apps/edge` | Node registry, 설정 전달, routing, stream relay를 담당하는 Edge skeleton | +| `apps/control-plane` | 향후 여러 Edge를 연결하고 운영 화면을 제공할 중앙 관리 계층 | +| `apps/worker` | 향후 비동기 작업 처리 가능성을 위한 placeholder | +| `packages` | 설정, 인증, 정책, 작업, 관측성, 버전 등 공통 패키지 | +| `proto` | 앱 간 메시지 계약 원본과 생성물 | +| `configs` | 현재 개발용 설정 예시 | -## 내부 통신 +## Roadmap -- **프로토콜**: TCP + Protobuf + mTLS -- **메시지 계약**: `proto/iop/*.proto` -- **생성물**: `proto/gen/iop/*.pb.go` -- gRPC **사용 금지** -- WebSocket transport 기본화 **금지** +### Phase 1. Edge-Node execution skeleton -## 외부 API +- Edge-Node 소켓 연결 +- Node adapter execution +- CLI adapter 검증 +- `adapter + target` 개념 정리 -외부 OpenAI-compatible API는 `apps/edge`의 책임 영역으로 둔다. +### Phase 2. Runtime and Automation unification -## 기본 명령 예시 +- 모델 서빙 adapter 확장 +- CLI Agent 실행 안정화 +- Runtime / Automation 실행 흐름 공통화 +- Node local execution history와 Edge event aggregation 경계 정리 -```bash -# 의존성 설치 -go mod tidy +### Phase 3. Control Plane -# node CLI 빌드 예시 -go build -o bin/node ./apps/node/cmd/node +- 자체 서버와 프론트 페이지 제공 +- 여러 Edge 연결 +- Edge 상태 조회 +- Edge 설정 변경 +- Edge 명령 전달 +- 이벤트 수신 -# node CLI 실행 예시 -./bin/node version -./bin/node config print -./bin/node serve -``` +### Phase 4. Multi-Edge operations -## 모노레포 구조 +- 여러 Edge group 관리 +- Edge별 runtime/automation 상태 표시 +- Edge 단위 실행 이력과 상태 집계 +- Edge 단위 작업 실행과 제어 -``` -iop/ - apps/ - node/ - edge/ - control-plane/ - worker/ - packages/ # 공통 패키지 - proto/ # Protobuf 스펙과 생성물 - configs/ # 앱별 설정 파일 - docs/ # 아키텍처와 운영 문서 -``` +### Phase 5. Policy, History, Audit + +- 권한, 정책, 이력, 감사 기능 확장 +- 상세 설계는 후속 작업에서 결정 + +## Development Notes + +- 기존 구조를 우선하며, 세부 구현은 각 작업의 domain rule과 현재 코드 경계를 먼저 확인한 뒤 진행한다. +- 내부 통신은 TCP/protobuf 기반 소켓 흐름을 우선한다. +- gRPC 도입, WebSocket 기본 transport 전환, actor/FSM/plugin framework 도입은 현재 단계의 기본 방향이 아니다. +- OpenAI API 호환 계층은 외부 호환을 위한 표면이며, 내부 실행 모델 전체를 대표하지 않는다. +- 앱별 README에는 현재 수동 테스트나 구현 세부가 더 많이 남아 있을 수 있다. 루트 README는 전체 방향과 경계를 설명하는 문서로 유지한다. + +## Non-Goals for Current Stage + +이번 단계에서는 다음 내용을 루트 README에서 상세 설계로 확정하지 않는다. + +- 상세 DB schema +- 상세 protobuf 설계 +- 상세 event schema +- 상세 permission model +- 상세 policy engine 설계 +- 상세 audit log 구조 +- Edge federation 세부 설계 +- mTLS 세부 구현 계획 +- Control Plane UI 화면별 상세 기획 +- Plane 연동 상세 workflow +- NomadCode 상세 제품 설계 diff --git a/agent-ops/rules/project/domain/edge/rules.md b/agent-ops/rules/project/domain/edge/rules.md index 7e6b053..ac253bd 100644 --- a/agent-ops/rules/project/domain/edge/rules.md +++ b/agent-ops/rules/project/domain/edge/rules.md @@ -2,7 +2,7 @@ ## 목적 / 책임 -외부 API gateway로 발전할 앱 영역이며, 현재는 node 연결을 수락하고 token 기반 등록을 검증한 뒤 중앙 관리 설정을 내려주는 TCP/protobuf 서버 책임을 가진다. +여러 Node를 하나의 로컬 실행 그룹으로 묶는 백엔드 실행 그룹 컨트롤러 영역이다. 현재는 node 연결을 수락하고 token 기반 등록을 검증한 뒤 adapter/runtime 설정을 내려주며, console 기반 실행 요청과 run event relay를 검증한다. ## 포함 경로 @@ -15,7 +15,7 @@ ## 제외 경로 - `apps/node/` — 실제 모델 실행과 adapter 관리 -- `apps/control-plane/` — 노드 등록/정책/스케줄링 예정 영역 +- `apps/control-plane/` — 여러 Edge 연결 관리와 운영 화면 제공 예정 영역 - `apps/worker/` — 비동기 작업 처리 예정 영역 - `packages/` — 공통 설정/관측성/인증 패키지 - `proto/` — 메시지 계약 원본과 생성물 @@ -34,21 +34,24 @@ ## 유지할 패턴 - edge는 사전 등록된 node 정의를 검증하고 연결 상태를 registry에 반영하는 역할을 먼저 안정화한다. -- 외부 OpenAI-compatible HTTP API는 node/transport 안정화 이후 확장한다. +- edge는 단순 gateway가 아니라 Node registry, adapter/profile configuration, routing, stream relay의 소유자다. +- 외부 OpenAI-compatible HTTP API는 node/transport 안정화 이후 확장하며, 내부 실행 모델 전체를 대표하지 않는다. - node가 연결 직후 RegisterRequest를 보내고 edge가 RegisterResponse로 중앙 설정을 응답하는 흐름을 유지한다. - Registry는 동시성 안전성을 유지하고 외부로 내부 map을 노출하지 않는다. - NodeStore는 설정 파일에서 한 번 seed되며 런타임 중 변경하지 않는다; token 중복·빈 token은 LoadFromConfig에서 즉시 거부한다. - adapter config 변환(mapper)은 `node.BuildConfigPayload()`에서만 수행하고 transport 레이어에 변환 로직을 두지 않는다. +- 내부 실행 요청은 `adapter + target`으로 표현한다. 외부 API 호환 경계의 `model` 표현을 edge 내부 책임 전체로 확장하지 않는다. ## 다른 도메인과의 경계 -- **node**: edge는 node를 실행하지 않는다. edge는 사전 등록 정보와 연결 registry를 기반으로 요청을 보낼 대상만 선택한다. +- **node**: edge는 node 내부 adapter를 직접 실행하지 않는다. edge는 사전 등록 정보와 연결 registry를 기반으로 요청을 보낼 대상과 실행 설정을 관리한다. - **platform-common**: edge 설정, metrics, protobuf 타입은 platform-common 계약을 따른다. -- **control-plane 후보**: 장기적으로 노드 등록/정책/스케줄링이 control-plane으로 분리될 수 있으므로 edge에 해당 책임을 굳히기 전에 경계를 재검토한다. +- **control-plane 후보**: control-plane은 Edge를 통해 시스템을 제어한다. Node 직접 연결/직접 스케줄링을 control-plane 책임으로 굳히지 않는다. ## 금지 사항 - edge를 구현하면서 node adapter 실행 로직을 복제하지 않는다. - OpenAI-compatible HTTP API를 추가할 때 내부 TCP/protobuf 경계를 우회하지 않는다. +- Control Plane 구현 전이라도 Node를 Control Plane에 직접 연결하는 경로를 edge 도메인에 추가하지 않는다. - gRPC 또는 WebSocket을 기본 내부 transport로 바꾸지 않는다. - `proto/gen/iop/*.pb.go` 생성 파일을 직접 수정하지 않는다. diff --git a/agent-ops/rules/project/domain/node/rules.md b/agent-ops/rules/project/domain/node/rules.md index 034d62a..ababd7a 100644 --- a/agent-ops/rules/project/domain/node/rules.md +++ b/agent-ops/rules/project/domain/node/rules.md @@ -2,7 +2,7 @@ ## 목적 / 책임 -디바이스당 1개 실행되는 IOP 노드 에이전트 영역이다. edge에서 들어온 실행 요청을 runtime 요청으로 변환하고, 라우팅된 어댑터를 실행하며, 실행 이벤트와 이력을 관리한다. +Edge에 연결되어 실제 adapter execution을 수행하는 IOP 노드 에이전트 영역이다. edge에서 들어온 실행 요청을 runtime 요청으로 변환하고, 라우팅된 어댑터를 실행하며, 실행 이벤트와 현재 단계의 로컬 실행 이력을 관리한다. ## 포함 경로 @@ -17,15 +17,15 @@ ## 제외 경로 -- `apps/edge/` — node를 받아들이는 gateway 서버 영역 -- `apps/control-plane/` — 노드 등록/정책/스케줄링 예정 영역 +- `apps/edge/` — Node를 관리하는 실행 그룹 컨트롤러 영역 +- `apps/control-plane/` — 여러 Edge 연결 관리와 운영 화면 제공 예정 영역 - `apps/worker/` — 비동기 작업 처리 예정 영역 - `packages/` — 여러 앱이 공유하는 공통 패키지 - `proto/` — 앱 간 메시지 계약 ## 주요 구성 요소 -- `runtime.Adapter` — 모델/CLI 실행 어댑터 계약 +- `runtime.Adapter` — adapter target 실행 계약 - `runtime.Router` — 실행 요청을 구체적인 `ExecutionSpec`으로 변환하는 계약 - `node.Node` — `transport.Handler` 구현체이자 실행 파이프라인 조정자 - `node.runManager` — run ID 기준 `runHandle`(cancel, done) 등록/해제/취소 관리; `node.Node` 내부에서만 사용 @@ -41,6 +41,7 @@ - `runtime` 패키지에는 도메인 타입과 인터페이스를 두고 구체 구현 의존성을 넣지 않는다. - transport/proto 타입은 `node.Node` 경계에서 runtime 타입으로 변환한다. +- 내부 실행 식별자는 `adapter + target`을 사용한다. 외부 OpenAI-compatible API나 legacy placeholder를 제외하고 `model`을 내부 실행 대표 용어로 되돌리지 않는다. - 어댑터 추가 시 `runtime.Adapter`를 구현하고 `adapters.BuildFromPayload()` 또는 bootstrap registry에 등록한다. - 실행 취소는 run ID 기준으로 `runManager`에 등록하고 실행 종료 시 반드시 `deregister`로 해제한다. - `adapters.Registry`의 start/stop은 bootstrap lifecycle에서만 호출하고 개별 adapter에서 직접 호출하지 않는다. @@ -49,9 +50,9 @@ ## 다른 도메인과의 경계 -- **edge**: edge는 node 연결 등록과 중앙 설정 전달, 라우팅 진입을 담당한다. node는 edge가 보낸 실행 요청을 처리하고 이벤트를 돌려준다. +- **edge**: edge는 node 연결 등록, adapter/runtime 설정 전달, 라우팅 진입, stream relay를 담당한다. node는 edge가 보낸 실행 요청을 처리하고 이벤트를 돌려준다. - **platform-common**: node는 `packages/config`, `packages/observability`, `proto/gen/iop` 등을 사용하지만 공통 타입/설정 자체의 소유자는 platform-common이다. -- **control-plane 후보**: 정책/스케줄링의 시스템 단위 결정은 control-plane에서 다루고, node는 전달받은 실행 단위 수행에 집중한다. +- **control-plane 후보**: control-plane은 Node가 아니라 Edge를 통해 시스템을 제어한다. node는 control-plane 직접 연결/직접 스케줄링을 전제로 하지 않는다. ## 금지 사항 diff --git a/agent-ops/rules/project/domain/platform-common/rules.md b/agent-ops/rules/project/domain/platform-common/rules.md index 49d18ac..e23d8a3 100644 --- a/agent-ops/rules/project/domain/platform-common/rules.md +++ b/agent-ops/rules/project/domain/platform-common/rules.md @@ -2,7 +2,7 @@ ## 목적 / 책임 -여러 앱이 공유하는 설정, 인증, 정책, 메타데이터, 작업 상태, 관측성, 버전, protobuf 계약을 관리한다. 앱별 구현보다 안정적인 공통 계약과 작은 유틸리티를 제공하는 영역이다. +여러 앱이 공유하는 설정, 인증, 정책, 메타데이터, 작업 상태, 관측성, 버전, protobuf 계약을 관리한다. 앱별 구현보다 안정적인 공통 계약과 작은 유틸리티를 제공하며, 내부 실행 계약은 `adapter + target` 방향을 우선한다. ## 포함 경로 @@ -20,7 +20,7 @@ ## 제외 경로 - `apps/node/` — node 실행 파이프라인과 adapter 관리 -- `apps/edge/` — gateway 서버와 node registry +- `apps/edge/` — 실행 그룹 컨트롤러와 node registry - `apps/control-plane/` — control-plane 앱 구현 예정 영역 - `apps/worker/` — worker 앱 구현 예정 영역 @@ -30,7 +30,7 @@ - `auth.LoadServerTLS` / `auth.LoadClientTLS` — mTLS TLS config 생성 - `observability.NewLogger` / `observability.ServeMetrics` — 공통 로깅/메트릭 - `policy.Engine` — 정책 적용/검증 계약 -- `jobs.Job` — 비동기 작업 상태 모델 +- `jobs.Job` — 비동기 작업 상태 placeholder; 내부 실행 대상은 `target`으로 표현 - `proto/iop/*.proto` — 앱 간 메시지 원본 계약 ## 유지할 패턴 @@ -40,16 +40,18 @@ - protobuf 계약 변경은 `proto/iop/*.proto`에서 시작하고 `make proto`로 생성물을 갱신한다. - 생성 파일(`proto/gen/iop/*.pb.go`)은 사람이 직접 편집하지 않는다. - 공통 패키지는 작고 명확한 계약을 유지하고 앱별 정책을 과도하게 끌어올리지 않는다. +- `RunRequest`, `ExecutionSpec`, `NodeCommandRequest`, job/history 계열 계약을 변경할 때 내부 실행 용어는 `target`을 우선하고, `model`은 외부 호환 경계인지 확인한다. ## 다른 도메인과의 경계 - **node**: node가 필요로 하는 설정/타입/계약을 제공하지만 실행 파이프라인의 소유자는 node이다. -- **edge**: edge가 필요로 하는 설정/관측성/protobuf 계약을 제공하지만 gateway 동작의 소유자는 edge이다. +- **edge**: edge가 필요로 하는 설정/관측성/protobuf 계약을 제공하지만 실행 그룹 제어와 node registry 동작의 소유자는 edge이다. - **control-plane/worker 후보**: 두 앱이 구현되면 필요한 공통 타입만 이 영역으로 승격하고 앱 내부 책임은 각 도메인에 둔다. ## 금지 사항 - `packages`에서 `apps/*/internal` 패키지를 import하지 않는다. - 앱 하나만을 위한 임시 타입을 충분한 근거 없이 공통 패키지로 승격하지 않는다. +- 내부 실행 계약을 확장하면서 `model` 중심 명명을 되살리지 않는다. 외부 API 호환이 필요한 경우 경계와 변환 위치를 명시한다. - protobuf 생성물을 직접 수정하지 않는다. - 설정 파일만 바꾸고 `packages/config`의 로딩/default와 불일치하게 두지 않는다. diff --git a/agent-ops/rules/project/rules.md b/agent-ops/rules/project/rules.md index d839823..6399aaa 100644 --- a/agent-ops/rules/project/rules.md +++ b/agent-ops/rules/project/rules.md @@ -7,15 +7,16 @@ ## 프로젝트 개요 -- IOP(Inference Operations Platform)는 분산 AI 모델 추론 워크로드를 처리하는 Go 모노레포이다. -- 현재 1차 구현 중심은 `apps/node`이며, `apps/edge`는 node 등록/레지스트리/transport가 일부 구현되어 있다. +- IOP(Inference Operations Platform)는 Control Plane - Edge - Node 계층 구조를 기반으로 모델 서빙과 CLI Agent/Automation 실행을 함께 다루는 Go 실행 오케스트레이션 모노레포이다. +- 내부 실행 개념은 model 중심이 아니라 `adapter + target` 중심으로 정리한다. 외부 OpenAI-compatible 경계나 외부 CLI 인자에서는 호환성을 위해 `model` 표현이 남을 수 있다. +- 현재 1차 구현 중심은 `apps/node`와 `apps/edge`의 Edge-Node 실행 스켈레톤이며, CLI adapter와 node 등록/레지스트리/transport가 우선 검증되고 있다. - `apps/control-plane`과 `apps/worker`는 README와 CLI placeholder 수준이므로, 본격 구현 전 별도 domain rule을 만들거나 갱신한다. ## 주요 구조 -- `apps/node/` — 디바이스별 노드 에이전트. 런타임 라우팅, 어댑터 실행, edge 연결, 실행 이력 저장을 담당한다. -- `apps/edge/` — node 연결을 받아 token 기반 등록을 처리하고 중앙 설정을 내려주는 gateway 서버 영역이다. -- `apps/control-plane/` — 노드 등록, 정책, 스케줄링 예정 영역이다. 현재 placeholder이다. +- `apps/node/` — Edge에 연결되는 실행자. 런타임 라우팅, adapter execution, CLI/model runtime 실행, 현재 단계의 로컬 실행 이력 저장을 담당한다. +- `apps/edge/` — 여러 Node를 묶는 백엔드 실행 그룹 컨트롤러. token 기반 등록, node registry, node 설정 전달, routing, stream relay를 담당한다. +- `apps/control-plane/` — 향후 여러 Edge를 연결하고 상태 조회/설정 변경/명령 전달/이벤트 수신/프론트 페이지 제공을 담당할 중앙 관리 계층이다. 현재 placeholder이다. - `apps/worker/` — 비동기 작업 처리 예정 영역이다. 현재 placeholder이다. - `packages/` — 설정, 인증, 정책, 메타데이터, 작업, 관측성, 버전 등 공통 패키지이다. - `proto/iop/` — IOP 메시지 계약 원본이다. @@ -39,11 +40,13 @@ - 기존 hexagonal 구조를 유지한다. 특히 `apps/node/internal/runtime` 인터페이스를 중심에 두고 transport/adapters/store는 바깥쪽 구현으로 둔다. - 새 node 어댑터는 `runtime.Adapter`를 구현하고 `apps/node/internal/bootstrap/module.go`에서 registry에 등록한다. +- 내부 실행 요청과 상태 저장에서는 `adapter`, `target`, `execution` 용어를 우선한다. `model`은 외부 API 호환이나 legacy placeholder일 때만 허용한다. +- Control Plane은 Node를 직접 연결/스케줄링하지 않고 Edge를 통해 시스템을 제어한다. Edge는 자신의 로컬 런타임 상태와 Node registry를 소유한다. - 내부 통신은 TCP 기반 protobuf 메시지 흐름을 우선한다. gRPC 도입, WebSocket 기본 transport 전환, actor/FSM/plugin framework 도입은 금지한다. - protobuf 계약 변경 시 `proto/iop/*.proto`를 먼저 수정하고 `make proto`로 `proto/gen/iop/*.pb.go`를 갱신한다. 생성 파일은 직접 수정하지 않는다. - 앱 설정 구조 변경 시 `packages/config`의 struct/default와 `configs/*.yaml` 예시를 함께 확인한다. - 테스트는 변경 범위에 맞춰 `go test ./...` 또는 대상 패키지 테스트를 실행한다. -- `packages/protocol`은 README에 언급되어 있지만 현재 디렉터리가 없으므로, 생성이 필요하면 실제 책임 경계를 먼저 확정한다. +- 상세 DB schema, event schema, permission/policy/audit model, federation, mTLS 구현 세부, Control Plane UI 세부 기획은 각 작업에서 별도로 결정한다. ## 도메인 매핑 @@ -57,7 +60,7 @@ ## 도메인 후보 -- `control-plane`: `apps/control-plane/**`가 placeholder를 넘어 노드 등록, 정책, 스케줄링을 구현하기 시작할 때 생성한다. +- `control-plane`: `apps/control-plane/**`가 placeholder를 넘어 여러 Edge 연결 관리, Edge 상태 조회, Edge 설정 변경, Edge 명령 전달, 이벤트 수신, 프론트 페이지 제공을 구현하기 시작할 때 생성한다. - `worker`: `apps/worker/**`가 placeholder를 넘어 작업 큐 소비/재시도/결과 저장을 구현하기 시작할 때 생성한다. ## 스킬 라우팅 diff --git a/apps/control-plane/README.md b/apps/control-plane/README.md index d865ed6..0a4116d 100644 --- a/apps/control-plane/README.md +++ b/apps/control-plane/README.md @@ -1,12 +1,17 @@ # control-plane — Control Plane -노드 등록, 정책 적용, 작업 스케줄링을 담당한다. +여러 Edge를 연결하고 관찰하며, Edge 설정 변경과 명령 전달을 담당할 중앙 관리 계층이다. **현재 상태: Placeholder** +현재 Control Plane은 구현 전 단계다. Node를 직접 등록하거나 직접 스케줄링하는 계층으로 확정하지 않는다. Control Plane은 Edge를 통해 시스템을 제어하고, Edge가 자신의 로컬 런타임 상태와 Node registry를 운영하는 방향을 따른다. + ## 계획된 기능 -- 노드 등록 및 헬스체크 -- 정책 DSL 평가 -- 작업 스케줄링 (라운드로빈 / 최소 부하) -- 노드 드레이닝 / 업그레이드 조율 +- 자체 서버와 프론트 페이지 제공 +- 여러 Edge 연결 관리 +- Edge 상태 조회 +- Edge 설정 변경 +- Edge 명령 전달 +- Edge 이벤트 수신 +- Runtime / Automation 영역을 나눠 보여주는 운영 화면 diff --git a/apps/control-plane/cmd/control-plane/main.go b/apps/control-plane/cmd/control-plane/main.go index ca51ada..17676d5 100644 --- a/apps/control-plane/cmd/control-plane/main.go +++ b/apps/control-plane/cmd/control-plane/main.go @@ -17,11 +17,12 @@ func main() { func rootCmd() *cobra.Command { root := &cobra.Command{ Use: "control-plane", - Short: "IOP Control Plane — node registry, policy, scheduling (placeholder)", - Long: `control-plane manages node registration, enforces policy, -and schedules jobs to nodes. + Short: "IOP Control Plane — multi-edge management placeholder", + Long: `control-plane will manage Edge connections, observe Edge state, +adjust Edge configuration, deliver commands to Edge, and receive Edge events. -This component is a placeholder. Implementation is planned after the node is stable.`, +This component is a placeholder. It should control the system through Edge, +not by connecting to or scheduling Node directly.`, } root.AddCommand(&cobra.Command{ Use: "serve", diff --git a/docs/architecture.md b/docs/architecture.md index 88b26db..c23ba0d 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -2,92 +2,81 @@ ## 개요 -IOP(Inference Operations Platform)는 분산 AI 모델 추론을 위한 플랫폼이다. -단일 서비스가 아니라 협력하는 여러 앱으로 구성된다. +IOP(Inference Operations Platform)는 모델 서빙과 CLI Agent/Automation 실행을 함께 다루는 실행 오케스트레이션 플랫폼을 지향한다. 단순한 OpenAI-compatible proxy나 모델 라우터가 아니라, Control Plane - Edge - Node 계층으로 로컬 실행 그룹을 관리하는 구조를 목표로 한다. -``` - Client (OpenAI SDK) - │ - ▼ - ┌─────────┐ - │ edge │ OpenAI-compatible HTTP API - └────┬────┘ - │ IOP TCP + Protobuf + mTLS - ▼ - ┌──────────────────┐ - │ control-plane │ 노드 등록 / 정책 / 스케줄링 - └────────┬─────────┘ - │ - ┌─────┴──────┐ - ▼ ▼ - ┌──────┐ ┌──────┐ (N개의 node) - │ node │ │ node │ - └──┬───┘ └──┬───┘ - │ │ - ┌──┴──┐ ┌──┴──┐ - │mock │ │ollama│ 어댑터 - │cli │ │vllm │ - └─────┘ └──────┘ +현재 구현은 Edge-Node 실행 스켈레톤을 검증하는 단계다. Control Plane과 worker는 placeholder이며, 세부 계약은 후속 작업에서 정리한다. - ┌────────┐ - │ worker │ 비동기 작업 처리 - └────────┘ +## 계층 구조 + +```text +Control Plane +├─ Edge Group A +│ ├─ Node 1 +│ │ ├─ adapter: cli +│ │ └─ adapter: ollama +│ └─ Node 2 +│ └─ adapter: vllm +└─ Edge Group B + └─ Node 3 + └─ adapter: cli ``` -## 내부 통신 프로토콜 +Control Plane은 Node에 직접 연결하지 않는다. Control Plane은 Edge 연결을 관리하고, Edge 상태를 조회하고, Edge 설정을 조정하고, Edge에 명령을 전달하는 중앙 관리 계층이다. -### 와이어 포맷 +Edge는 여러 Node를 묶는 실행 그룹 컨트롤러다. Edge는 Node registry, adapter/profile configuration, routing, stream relay, session handling, event aggregation을 담당한다. -``` -Frame: [4 bytes big-endian uint32: length] [payload] -Payload: JSON-encoded Envelope -Envelope.Payload: JSON-encoded inner message +Node는 실제 실행자다. Node는 Edge에 연결되어 adapter execution을 수행하고, RuntimeEvent를 Edge로 돌려준다. + +## 현재 Edge-Node 흐름 + +현재 검증 중인 기본 흐름은 다음과 같다. + +```text +Edge +└─ accepts Node connection + └─ validates token + └─ sends NodeConfigPayload + └─ relays RunRequest / NodeCommandRequest + +Node +└─ receives adapter/runtime config + └─ builds adapter registry + └─ resolves adapter + target + └─ executes adapter + └─ emits RuntimeEvent ``` -**TODO**: Payload 인코딩을 protobuf로 교체 (proto/iop/runtime.proto 참고) +현재 실행 이력은 Node의 local SQLite store에서 검증 중이다. Edge 단위 이력 집계와 로컬 실행 그룹 상태 소유권은 로드맵에 따라 정리한다. -### 메시지 타입 +## 내부 실행 모델 -| Type | 방향 | 설명 | -|------|------|------| -| `run.request` | client → node | 추론 실행 요청 | -| `run.event` | node → client | 스트리밍 이벤트 (start/delta/complete/error) | -| `heartbeat` | 양방향 | 연결 유지 (30초 간격) | -| `cancel.request` | client → node | 실행 중단 요청 | -| `register.request` | node → edge | 사전 발급 token 기반 등록 요청 | -| `register.response` | edge → node | 등록 수락 여부와 중앙 관리 node 설정 응답 | -| `error` | node → client | 에러 응답 | +내부 실행은 `adapter + target`으로 표현한다. -### mTLS +- `adapter`: 실행 방식. 예: `mock`, `ollama`, `vllm`, `cli` +- `target`: adapter 안의 실행 대상. 예: 모델 이름, CLI profile, agent/toolchain +- `execution`: 특정 adapter와 target으로 수행되는 단일 실행 -```yaml -tls: - enabled: true - cert: certs/node.crt - key: certs/node.key - ca: certs/ca.crt -``` - -`tls.enabled: true`로 설정하면 RequireAndVerifyClientCert (mTLS) 모드로 동작. +외부 OpenAI-compatible API에서는 호환성을 위해 `model` 필드가 남을 수 있다. 그러나 내부 실행 계약에서는 `target`을 우선한다. ## apps/node 내부 구조 -``` +```text Hexagonal Architecture ┌───────────────────────────────┐ - │ node.Node │ (application core) + │ node.Node │ application core │ │ TCP │ transport.Handler interface │ ─────────► OnRunRequest() │ - client │ OnCancel() │ + edge │ OnCancel() │ + │ OnCommandRequest() │ └───────┬───────────────────────┘ │ - router.Resolve() → ExecutionSpec + router.Resolve() -> ExecutionSpec │ - adapter.Execute() → RuntimeEvent stream + adapter.Execute() -> RuntimeEvent stream │ - store.CompleteRun() → SQLite + store.CompleteRun() -> SQLite ``` ### 어댑터 패턴 @@ -100,10 +89,14 @@ type Adapter interface { } ``` -새 어댑터를 추가하려면: -1. `apps/node/internal/adapters//` 패키지 생성 -2. `runtime.Adapter` 인터페이스 구현 -3. `bootstrap/module.go`에서 Registry에 등록 +새 어댑터를 추가할 때는 `runtime.Adapter`를 구현하고 registry에 등록한다. CLI Agent, 모델 런타임, 도구 실행은 모두 같은 adapter execution 경로로 다루는 방향을 유지한다. + +## 통신 방향 + +- Edge-Node 내부 통신은 TCP + protobuf 기반 소켓 흐름을 우선한다. +- Node는 Edge에 연결한다. +- Control Plane은 향후 Edge와 소켓 기반 연결을 맺는다. +- Control Plane이 Node에 직접 연결하거나 매 요청마다 Node를 직접 스케줄링하는 구조는 목표가 아니다. ## 라이브러리 선택 @@ -113,9 +106,9 @@ type Adapter interface { | `go.uber.org/zap` | 구조화 로깅 | | `github.com/spf13/cobra` | CLI | | `github.com/spf13/viper` | 설정 파일 | -| `google.golang.org/protobuf` | Protobuf (spec용) | +| `google.golang.org/protobuf` | Protobuf | | `github.com/prometheus/client_golang` | 메트릭 | -| `modernc.org/sqlite` | 실행 이력 저장 | +| `modernc.org/sqlite` | 현재 Node local 실행 이력 저장 | ## 금지 사항 diff --git a/packages/jobs/jobs.go b/packages/jobs/jobs.go index 2313275..8f8fea0 100644 --- a/packages/jobs/jobs.go +++ b/packages/jobs/jobs.go @@ -18,7 +18,7 @@ type Job struct { RunID string `json:"run_id"` NodeID string `json:"node_id"` Adapter string `json:"adapter"` - Model string `json:"model"` + Target string `json:"target"` Status Status `json:"status"` CreatedAt time.Time `json:"created_at"` StartedAt *time.Time `json:"started_at,omitempty"` diff --git a/packages/policy/engine.go b/packages/policy/engine.go index 1c08c3a..0bf6740 100644 --- a/packages/policy/engine.go +++ b/packages/policy/engine.go @@ -8,9 +8,9 @@ type Spec map[string]any // Engine applies and validates policies on execution specs. type Engine interface { // Apply enriches or modifies spec fields based on policy rules. - Apply(ctx context.Context, adapter, model string, policy Spec) (Spec, error) + Apply(ctx context.Context, adapter, target string, policy Spec) (Spec, error) // Validate returns an error if the policy rejects the request. - Validate(ctx context.Context, adapter, model string, policy Spec) error + Validate(ctx context.Context, adapter, target string, policy Spec) error } // Passthrough is a no-op policy engine used when no policy is configured. diff --git a/proto/gen/iop/control.pb.go b/proto/gen/iop/control.pb.go index 45f1351..6094706 100644 --- a/proto/gen/iop/control.pb.go +++ b/proto/gen/iop/control.pb.go @@ -21,7 +21,7 @@ const ( _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) -// PolicyRule defines a routing/resource constraint. +// PolicyRule is a placeholder for future edge-level routing/resource constraints. type PolicyRule struct { state protoimpl.MessageState `protogen:"open.v1"` Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` @@ -82,7 +82,8 @@ func (x *PolicyRule) GetParams() map[string]string { return nil } -// ScheduleRequest asks the control-plane to route a job to a node. +// ScheduleRequest is a legacy placeholder for future Control Plane orchestration. +// It must not be treated as an active contract for direct Node scheduling. type ScheduleRequest struct { state protoimpl.MessageState `protogen:"open.v1"` JobId string `protobuf:"bytes,1,opt,name=job_id,json=jobId,proto3" json:"job_id,omitempty"` @@ -151,7 +152,8 @@ func (x *ScheduleRequest) GetMetadata() map[string]string { return nil } -// ScheduleResponse contains the selected node address. +// ScheduleResponse is a legacy placeholder. Future scheduling should be +// reworked around Edge-owned runtime state instead of bypassing Edge. type ScheduleResponse struct { state protoimpl.MessageState `protogen:"open.v1"` NodeId string `protobuf:"bytes,1,opt,name=node_id,json=nodeId,proto3" json:"node_id,omitempty"` diff --git a/proto/gen/iop/job.pb.go b/proto/gen/iop/job.pb.go index db4cb32..1fd64ad 100644 --- a/proto/gen/iop/job.pb.go +++ b/proto/gen/iop/job.pb.go @@ -85,7 +85,7 @@ type Job struct { RunId string `protobuf:"bytes,2,opt,name=run_id,json=runId,proto3" json:"run_id,omitempty"` NodeId string `protobuf:"bytes,3,opt,name=node_id,json=nodeId,proto3" json:"node_id,omitempty"` Adapter string `protobuf:"bytes,4,opt,name=adapter,proto3" json:"adapter,omitempty"` - Model string `protobuf:"bytes,5,opt,name=model,proto3" json:"model,omitempty"` + Target string `protobuf:"bytes,5,opt,name=target,proto3" json:"target,omitempty"` Status JobStatus `protobuf:"varint,6,opt,name=status,proto3,enum=iop.JobStatus" json:"status,omitempty"` CreatedAt int64 `protobuf:"varint,7,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` StartedAt int64 `protobuf:"varint,8,opt,name=started_at,json=startedAt,proto3" json:"started_at,omitempty"` @@ -154,9 +154,9 @@ func (x *Job) GetAdapter() string { return "" } -func (x *Job) GetModel() string { +func (x *Job) GetTarget() string { if x != nil { - return x.Model + return x.Target } return "" } @@ -319,13 +319,13 @@ var File_proto_iop_job_proto protoreflect.FileDescriptor const file_proto_iop_job_proto_rawDesc = "" + "\n" + - "\x13proto/iop/job.proto\x12\x03iop\"\x8a\x03\n" + + "\x13proto/iop/job.proto\x12\x03iop\"\x8c\x03\n" + "\x03Job\x12\x15\n" + "\x06job_id\x18\x01 \x01(\tR\x05jobId\x12\x15\n" + "\x06run_id\x18\x02 \x01(\tR\x05runId\x12\x17\n" + "\anode_id\x18\x03 \x01(\tR\x06nodeId\x12\x18\n" + - "\aadapter\x18\x04 \x01(\tR\aadapter\x12\x14\n" + - "\x05model\x18\x05 \x01(\tR\x05model\x12&\n" + + "\aadapter\x18\x04 \x01(\tR\aadapter\x12\x16\n" + + "\x06target\x18\x05 \x01(\tR\x06target\x12&\n" + "\x06status\x18\x06 \x01(\x0e2\x0e.iop.JobStatusR\x06status\x12\x1d\n" + "\n" + "created_at\x18\a \x01(\x03R\tcreatedAt\x12\x1d\n" + diff --git a/proto/gen/iop/node.pb.go b/proto/gen/iop/node.pb.go index c61b023..e275054 100644 --- a/proto/gen/iop/node.pb.go +++ b/proto/gen/iop/node.pb.go @@ -73,7 +73,9 @@ func (NodeStatus) EnumDescriptor() ([]byte, []int) { return file_proto_iop_node_proto_rawDescGZIP(), []int{0} } -// NodeInfo describes a registered node. +// NodeInfo is a legacy placeholder for node snapshots. +// Current Edge-Node registration uses RegisterRequest/RegisterResponse in +// runtime.proto, and Control Plane should observe nodes through Edge. type NodeInfo struct { state protoimpl.MessageState `protogen:"open.v1"` NodeId string `protobuf:"bytes,1,opt,name=node_id,json=nodeId,proto3" json:"node_id,omitempty"` @@ -158,7 +160,8 @@ func (x *NodeInfo) GetLabels() map[string]string { return nil } -// NodeRegisterRequest is sent to the control-plane on startup. +// NodeRegisterRequest is a legacy placeholder and is not the active startup +// contract. Nodes connect to Edge, not directly to Control Plane. type NodeRegisterRequest struct { state protoimpl.MessageState `protogen:"open.v1"` Info *NodeInfo `protobuf:"bytes,1,opt,name=info,proto3" json:"info,omitempty"` @@ -203,7 +206,8 @@ func (x *NodeRegisterRequest) GetInfo() *NodeInfo { return nil } -// NodeRegisterResponse is returned by the control-plane. +// NodeRegisterResponse is a legacy placeholder retained until the +// Control Plane contracts are redesigned around Edge. type NodeRegisterResponse struct { state protoimpl.MessageState `protogen:"open.v1"` Accepted bool `protobuf:"varint,1,opt,name=accepted,proto3" json:"accepted,omitempty"` diff --git a/proto/iop/control.proto b/proto/iop/control.proto index 10980d0..b474637 100644 --- a/proto/iop/control.proto +++ b/proto/iop/control.proto @@ -4,14 +4,15 @@ package iop; option go_package = "iop/proto/gen/iop"; -// PolicyRule defines a routing/resource constraint. +// PolicyRule is a placeholder for future edge-level routing/resource constraints. message PolicyRule { string name = 1; string expression = 2; map params = 3; } -// ScheduleRequest asks the control-plane to route a job to a node. +// ScheduleRequest is a legacy placeholder for future Control Plane orchestration. +// It must not be treated as an active contract for direct Node scheduling. message ScheduleRequest { string job_id = 1; string target = 2; @@ -19,7 +20,8 @@ message ScheduleRequest { map metadata = 4; } -// ScheduleResponse contains the selected node address. +// ScheduleResponse is a legacy placeholder. Future scheduling should be +// reworked around Edge-owned runtime state instead of bypassing Edge. message ScheduleResponse { string node_id = 1; string address = 2; diff --git a/proto/iop/job.proto b/proto/iop/job.proto index 794ff4c..8e8944e 100644 --- a/proto/iop/job.proto +++ b/proto/iop/job.proto @@ -18,7 +18,7 @@ message Job { string run_id = 2; string node_id = 3; string adapter = 4; - string model = 5; + string target = 5; JobStatus status = 6; int64 created_at = 7; int64 started_at = 8; diff --git a/proto/iop/node.proto b/proto/iop/node.proto index 0ef7fbd..9fa23b7 100644 --- a/proto/iop/node.proto +++ b/proto/iop/node.proto @@ -4,7 +4,9 @@ package iop; option go_package = "iop/proto/gen/iop"; -// NodeInfo describes a registered node. +// NodeInfo is a legacy placeholder for node snapshots. +// Current Edge-Node registration uses RegisterRequest/RegisterResponse in +// runtime.proto, and Control Plane should observe nodes through Edge. message NodeInfo { string node_id = 1; string name = 2; @@ -21,12 +23,14 @@ enum NodeStatus { NODE_STATUS_DRAINING = 3; } -// NodeRegisterRequest is sent to the control-plane on startup. +// NodeRegisterRequest is a legacy placeholder and is not the active startup +// contract. Nodes connect to Edge, not directly to Control Plane. message NodeRegisterRequest { NodeInfo info = 1; } -// NodeRegisterResponse is returned by the control-plane. +// NodeRegisterResponse is a legacy placeholder retained until the +// Control Plane contracts are redesigned around Edge. message NodeRegisterResponse { bool accepted = 1; string token = 2;