# 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 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 계열이다.

계약 원문은 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` 등은 그대로 유지하면 된다.

기준 출력 예시:

```text
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` 바이너리는 다음 명령을 제공한다.

```bash
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한다.

```yaml
refresh:
  enabled: true
  listen: "127.0.0.1:19093"
```

```bash
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`, 단일 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`와 `/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도 같은 계약 원문을 기준으로 한다.

```yaml
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`다.

```yaml
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` 등은 OpenAI API처럼 top-level field에 둔다. `metadata.source`, `metadata.cli`, `metadata.inference`, 소비자 전용 metadata wrapper, `options`, `think`, `format`, `keep_alive` 같은 provider/Ollama 전용 request field는 지원하지 않는다.

```bash
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"}]}'
```

```bash
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 예시:

```yaml
# 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로 이동:

```yaml
# 기존 (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`으로 변환한다.

#### 설정 예시

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