---
name: dev-corp-runtime-deploy
version: 1.0.12
description: dev-corp 배포, public digitalplatform Edge와 내부 DGX/Mac Studio provider pool 배포 및 OpenAI-compatible capacity smoke 절차
---

# dev-corp-runtime-deploy

## 목적

dev-corp provider pool을 현재 dev-runtime과 같은 구조로 배포한다. 배포는 runner checkout 준비, source clean sync, 테스트, dev-corp runtime rebuild, public Edge/Node 재시작, provider snapshot 기반 capacity 검증까지 포함한다. 현재 dev-corp Edge runtime은 `iop.ai.kr`이며, mac-mini는 source/build/provider SSH runner로만 사용한다.

## 언제 호출할지

- 사용자가 `dev-corp 배포`, `dev-corp runtime 배포`, `회사 dev 환경 배포`처럼 dev-corp 환경 배포를 요청할 때
- public `iop.ai.kr` Edge와 DGX Spark 01/02 vLLM node, Mac Studio vLLM-MLX node로 이루어진 provider pool을 갱신할 때
- dev-corp model alias, provider endpoint, provider capacity, node 접속 정보를 배포 절차에 반영할 때
- dev-corp OpenAI-compatible `/v1/chat/completions` 경로가 provider capacity만큼 채워지는지 검증할 때

## 입력

- `env`: 배포 대상 환경. 기본값은 `dev-corp`이다.
- `model`: OpenAI-compatible model alias. 지정하지 않으면 `agent-test/inventory-dev-corp.yaml`의 `model.aliases`에 기록된 model-specific alias를 사용한다.
- `capacity_targets`: provider별 기대 capacity. 지정하지 않으면 `agent-test/inventory-dev-corp.yaml`의 target 후보 값을 사용한다.
- `source_ref`: 배포할 git ref. 지정하지 않으면 mac-mini checkout의 기본 배포 branch 기준을 따른다.

## 먼저 확인할 것

- [ ] `agent-ops/rules/project/domain/testing/rules.md`를 읽고 사용자 실행 파이프라인 검증 기준을 확인한다.
- [ ] `go build -o /tmp/iop-inventory-query ./scripts/inventory-query`로 current checkout query binary를 만든다.
- [ ] `/tmp/iop-inventory-query --env dev-corp`로 env projection을 먼저 확인한다.
- [ ] 필요한 selector별 bounded 조회는 아래 명령으로 실행한다:
  - node: `/tmp/iop-inventory-query --env dev-corp --node <node-id-or-alias>`
  - provider: `/tmp/iop-inventory-query --env dev-corp --provider <provider-id>`
  - model: `/tmp/iop-inventory-query --env dev-corp --model <model-alias>`
- [ ] 직접 실행 exit 2가 schema/file 오류일 때만 canonical inventory 전체 읽기로 fallback하고, exit 1은 zero-match로 보고한다.
- [ ] dev-corp Edge 배포/검증 통로는 `iop.ai.kr`이다. mac-mini runner/provider 관리 경유지를 Edge 배포, OpenAI-compatible smoke, Node `edge_addr`, bootstrap 기본 URL로 잡지 않는다.
- [ ] dev-corp provider pool과 compose/local/dev-runtime profile을 섞지 않는다. dev-corp provider pool은 public `iop.ai.kr` Edge와 dev-corp config를 기준으로 한다.
- [ ] mac-mini에 `/Users/fe/agent-work/iop-dev-corp` checkout이 없으면 배포를 시작하지 말고 checkout 생성과 source sync를 setup blocker로 보고한다.
- [ ] mac-mini에서 Spark Ornith `192.168.2.2:8003`, `192.168.2.4:8005`와 Mac Studio Gemma4 `192.168.2.3:8004`의 `/health`와 `/v1/models`가 성공하는지 확인한다.
- [ ] Edge config의 OpenAI-compatible adapter endpoint는 각 Node process 기준으로 평가된다. direct preflight는 `192.168.2.x` 주소로 하되, Edge가 Node에 내려주는 provider endpoint는 node-local `127.0.0.1:<provider-port>`를 우선한다.
- [ ] DGX Spark 01/02와 Mac Studio Node의 Edge addr은 `iop.ai.kr:18087`이다. mac-mini local route 또는 reverse SSH tunnel은 현재 기준 잘못된 Edge addr이며 명시적인 rollback/장애 비교 외에는 사용하지 않는다.
- [ ] `gemma4:26b`와 `ornith:35b` alias, 그리고 현재 provider별 capacity는 `agent-test/inventory-dev-corp.yaml`의 최신 검증 상태를 기준으로 판단한다. 현재 방향은 Gemma4는 Mac Studio, Ornith는 Spark 01/02 provider pool이다. 새 배포에서 Edge config 반영과 capacity smoke가 통과하기 전에는 새 결과를 확정값으로 보고하지 않는다.
- [ ] DGX Spark vLLM provider의 catalog capacity `4`는 `262144` token request 4개가 항상 context 100%를 채우는 보장이 아니다. dev-corp 운영 기준은 호출당 prompt+generated KV가 최대 context의 `50-70%`라는 가정이다. capacity `4`, max context `262144`에서 KV `524,288` tokens / full-context concurrency `2.0x`는 lower bound이고, 운영 target은 KV `734,004-786,432` tokens / full-context concurrency `2.8x-3.0x`이다. 이 값이 `4.0x`가 아니라는 이유만으로 under-capacity로 판정하지 않되, `2.0x-2.8x`는 workload-specific smoke 없이는 운영 적합으로 보지 않는다. vLLM에는 vLLM-MLX식 `--max-kv-size`가 없으므로 실제 KV cache는 `gpu_memory_utilization` 적용 후 startup log의 `GPU KV cache size`와 `Maximum concurrency for 262,144 tokens per request`로 검증한다. 2026-07-13 기준 Ornith Spark01은 `8003`, `--gpu-memory-utilization 0.47`, KV `783,347`, full-context `2.99x`; Spark02는 `8005`, `--gpu-memory-utilization 0.50`, KV `1,074,276`, full-context `4.10x`이다.
- [ ] provider catalog admission queue timeout은 `queue_timeout_ms=0`으로 비활성화하고, Node `openai_compat_instances` adapter queue/request budget은 long reasoning/long-context 요청을 위해 `queue_timeout_ms=1800000`, `request_timeout_ms=1800000`으로 유지한다.
- [ ] DGX Spark 02 Ornith endpoint는 Docker publish 때문에 host `192.168.2.4:8005` -> container `8000`이다. `8000`, `8001`, `8002`, `8004`를 Spark02 Ornith 기본 endpoint로 쓰지 않는다.
- [ ] Mac Studio `192.168.2.3:8005` DiffusionGemma endpoint는 secondary provider 후보이며 기본 pool에 자동 포함하지 않는다.
- [ ] Mac Studio `192.168.2.3:8004` provider catalog type은 `openai_compat`이고 실제 runtime은 vLLM-MLX이다. provider catalog capacity는 `5`로 유지하고, vLLM-MLX runtime은 `vllm-mlx=0.4.0`, 호출당 최대 context window `262144`, requested KV `262144x3` 기준으로 runtime headroom `--max-num-seqs 6`, `--prefill-batch-size 6`, `--completion-batch-size 6`, `--max-request-tokens 262144`, `--max-kv-size 786432`을 사용한다. Continuous batching은 필수로 유지하고, Gemma4 MLLM/prefix-cache 불안정성을 피하기 위해 `--disable-prefix-cache`를 둔다. Gemma4 agent/tool-call profile은 현재 지원되는 `--enable-auto-tool-choice`, `--tool-call-parser gemma4`, `--reasoning-parser gemma4`, `--default-chat-template-kwargs '{"enable_thinking":true}'`를 함께 둔다.
- [ ] Mac Studio `192.168.2.3:8004` vLLM-MLX runtime은 2026-06-25 기준 detached `screen` session `vllm_mlx_8004`로 관리한다. 이 host는 Python `pyexpat` 로딩에 `DYLD_LIBRARY_PATH=/opt/homebrew/opt/expat/lib`가 필요하므로 재시작 명령에 반드시 포함한다.
- [ ] 현재 구현의 completion 검증 대상은 legacy `/v1/completions`가 아니라 `/v1/chat/completions`이다. `/v1/completions`는 route가 구현되어 있을 때만 별도 검증한다.
- [ ] dev-corp current runtime은 OpenAI-compatible bearer token이 켜져 있다. smoke는 token 원문을 출력하지 말고 `OPENAI_API_KEY=$(cat build/dev-corp-runtime/.secrets/openai_api_key)` 또는 `--api-key-file`로 실행한다.
- [ ] token, secret header, bootstrap token, private key 경로는 최종 보고에 원문으로 출력하지 않는다.
- [ ] 원격 host의 보안 정책 때문에 `scp`/`rsync` 같은 파일 전송 프로토콜이 `Operation not permitted`로 막히면 권한·ACL·보안 정책을 완화하거나 재시도하지 않는다. 산출물과 config는 SSH 표준 입력 스트림으로 canonical target과 같은 디렉터리에 새 remote candidate 파일을 생성하는 절차를 사용한다.

## known/current runtime update notes

- DGX Spark 01/02의 requested KV `262144x2`는 vLLM에서 직접 `--max-kv-size`로 표현하지 않는다. `--max_num_batched_tokens`는 scheduler iteration budget이므로 KV cache 총량으로 취급하지 않는다. dev-corp 기준은 `--max-model-len 262144`와 `gpu_memory_utilization` 적용 후 startup log의 KV cache/concurrency 확인이다.
- capacity/KV 해석 기준은 `agent-test/inventory-dev-corp.yaml`의 `model.capacity_planning_policy`를 우선한다. catalog capacity는 일반 요청 admission/queue target이고, `Maximum concurrency for 262,144 tokens per request`는 full-context upper-bound 진단값이다. capacity `4` 기준 `2.0x`는 lower bound, `2.8x-3.0x`는 운영 target이다. `4.0x` 미만이라는 이유만으로 실패로 보고하지 않되, `2.8x` 미만은 workload-specific smoke 없이 운영 적합으로 보고하지 않는다.
- queue timeout 해석 기준은 `agent-test/inventory-dev-corp.yaml`의 `model.timeout_policy`를 우선한다. provider catalog `queue_timeout_ms=0`은 provider admission queue timeout을 두지 않는다는 뜻이고, adapter instance의 `queue_timeout_ms=1800000`은 backend request budget과 같은 30분 장기 요청 예산이다.
- Historical Spark Gemma4/FP4 profile note: explicit `--max-num-batched-tokens 524288`를 revive하는 경우 first profile 중 FP4 MoE token-per-expert 기본 한계에 걸릴 수 있으므로 `VLLM_MAX_TOKENS_PER_EXPERT_FP4_MOE=4194304`를 유지한다. 현재 Spark Ornith Docker profile의 active capacity source로 취급하지 않는다.
- Historical Spark Gemma4 venv note: DGX Spark 01 Gemma4 0.24.0 venv profile을 revive하는 경우에만 `/home/digitalcommerce_dgx_spark_01/vllm_env_0_24_0/bin`을 PATH 앞에 둔다. 현재 Spark Ornith provider는 Docker `vllm/vllm-openai:nightly-aarch64` 기준이다.
- DGX Spark 01/02 Ornith Docker image는 `vllm/vllm-openai:nightly-aarch64`이고 container command에는 `serve`를 중복으로 넣지 않는다. Spark01은 host `8003` -> container `8000`, Spark02는 host `8005` -> container `8000` 기준이다.
- Spark Ornith parser/tool-call profile은 capacity/KV 튜닝과 별개로 agent 동작 가능성을 결정한다. DGX Spark 01/02 Ornith는 `--enable-auto-tool-choice --tool-call-parser qwen3_xml --reasoning-parser qwen3 --trust-remote-code --runner generate`를 둔다. Gemma4는 Mac Studio vLLM-MLX `8004`만 기본 대상으로 하며 별도 chat template override 없이 `--continuous-batching --disable-prefix-cache --enable-auto-tool-choice --tool-call-parser gemma4 --reasoning-parser gemma4 --default-chat-template-kwargs '{"enable_thinking":true}'`를 둔다.
- 모델별 parser/template 값은 섞지 않는다. Gemma 계열은 `tool_call_parser=gemma4`, `reasoning_parser=gemma4`, Gemma4 tool chat template 또는 vLLM-MLX 기본 template kwargs를 사용한다. Qwen 계열은 dev-runtime 문서의 `tool_call_parser=qwen`, `reasoning_parser=qwen3`, Qwen 전용 chat template 검증값을 따르며 Gemma4 parser/template을 재사용하지 않는다.
- Mac Studio는 `DYLD_LIBRARY_PATH=/opt/homebrew/opt/expat/lib` 없이 Python `pyexpat` import가 실패할 수 있다. `screen` session `vllm_mlx_8004` 안에서 이 환경변수를 export한 뒤 시작한다.
- Mac Studio vLLM-MLX는 `/Users/dc_dev/vllm-env-0.4.0/bin/python` 환경의 `vllm-mlx=0.4.0`, `mlx=0.32.0`, `mlx-lm=0.31.3`, `mlx-vlm=0.6.4`, `transformers=5.12.1`, `huggingface-hub=1.22.0`로 동작한다. `--continuous-batching`은 유지하고 `--disable-prefix-cache`를 추가했으며, `/health`, `/v1/models`, 직접 API non-stream/stream auto tool-call, streaming multi-turn tool-result final answer를 확인했다.
- Mac Studio vLLM-MLX의 현 값은 "구동 가능한 최종 체크포인트"로 취급한다. 추가 미세 조정은 새 포트/새 venv 또는 명시적 rollback 지점을 둔 별도 실험으로만 수행하고, Spark 01/02 vLLM 설정과 함께 변경하지 않는다. 알려진 잔여 이슈는 Gemma4 thought channel delimiter가 최종 content에 누수될 수 있는 점이며, 이 경우 serving 옵션을 계속 흔들기보다 Edge/provider adapter sanitizer로 containment 하는 방향을 우선 검토한다.
- 2026-07-13 기준 Spark Gemma4 `8002`/`8004` runtime은 내린 상태로 보고, Spark 01/02는 Ornith `8003`/`8005`를 IOP provider로 둔다. Gemma4는 Mac Studio `192.168.2.3:8004` provider만 기본 대상으로 둔다.
- 2026-07-13 재확인 기준 DGX Spark 01 Ornith는 Docker `iop-vllm-ornith35b-fp8`, host `8003`, `--gpu-memory-utilization 0.47`로 동작하며 startup log에서 GPU KV cache `783,347` tokens, full-context concurrency `2.99x`를 확인했다. DGX Spark 02 Ornith는 host `8005`, `--gpu-memory-utilization 0.50`으로 동작하며 GPU KV cache `1,074,276` tokens, full-context concurrency `4.10x`를 확인했다.
- 2026-07-02 mac-mini native Control Plane `18002/19004/19005` 관측값은 historical evidence다. 2026-07-09 현재 dev-corp Edge source of truth는 public `iop.ai.kr`이고, current runner에서 public host SSH와 public Control Plane status 접근은 확인되지 않았다.
- 2026-07-13 기준 dev-corp provider-pool Edge OpenAI-compatible capacity smoke 표준은 public `https://digitalplatform.iop.ai.kr/v1/chat/completions`에서 `ornith:35b` 9/6, `gemma4:26b` 9/6 동시 요청이다. Direct Edge listener `http://digitalplatform.iop.ai.kr:18086/v1`도 같은 backend로 동작하지만 사용자-facing 기본값으로 쓰지 않는다. `/v1/responses`는 capacity 성공 기준이 아니라 selected provider 지원 여부와 IOP raw passthrough/relay 동작을 분리해 보고한다.
- 2026-07-13 live status 기준 DGX Spark 01/02와 Mac Studio node는 `iop.ai.kr:18087`로 연결되어 있으며, provider snapshot은 Spark01 Ornith capacity `4`, Spark02 Ornith capacity `4`, Mac Studio Gemma4 capacity `5`이다. 같은 날 model-specific smoke에서 `ornith:35b` 9/6, `gemma4:26b` 9/6 요청이 모두 성공했고 완료 후 `in_flight=0`, `queued=0`, `healthy`로 회복했다.

## 실행 절차

1. **환경 인벤토리 확정**
   - 배포 대상 runner, repo path, Edge id, Control Plane status URL, OpenAI base URL, Edge admin URL, Node SSH 정보를 `agent-test/inventory-dev-corp.yaml`에서 확정한다.
   - public OpenAI/base, bootstrap/artifact, Edge-Node TCP는 기본적으로 `iop.ai.kr` 경로를 사용한다. mac-mini runner/provider 관리 경로로 Edge를 배포하거나 Node를 붙이는 것은 기본 금지이며, 사용자가 mac-mini local 비교를 명시 요청한 경우에만 별도 비교로 진행한다.
   - provider pool 대상 model alias와 provider별 capacity target을 확정한다.
   - 필수 정보가 없거나 서로 충돌하면 배포를 시작하지 말고 누락/충돌 항목을 보고한다.

2. **provider direct preflight**
   - mac-mini에서 DGX Spark 01 Ornith `http://192.168.2.2:8003/v1/models`, DGX Spark 02 Ornith `http://192.168.2.4:8005/v1/models`, Mac Studio Gemma4 `http://192.168.2.3:8004/v1/models`를 확인한다.
   - 각 endpoint가 기대 `served_model`을 노출하는지 확인한다.
   - DGX Spark 02가 provider port down과 SSH banner exchange timeout을 동시에 보이면 runtime 추가 조작을 보류하고, SSH 회복 후 process/log부터 확인한다.
   - 실패한 provider는 Edge config에 넣지 않거나 배포 blocker로 보고한다.

3. **mac-mini checkout clean sync**
   - mac-mini checkout에서 `git fetch` 후 배포 기준 ref로 `git reset --hard`를 수행한다.
   - dirty 파일은 보존 대상으로 보지 않는다. 배포 전 clean 상태를 만든다.
   - 기본 cleanup은 `git clean -fd`이다. `git clean -fdx`는 config, token, secret, runtime artifact까지 삭제할 수 있으므로 사용하지 않는다.
   - sync 후 `git status --short --branch`와 `git log --oneline -1`을 기록한다.

4. **빌드 전 테스트**
   - clean source 기준으로 `go test ./...`를 실행한다.
   - client/Flutter, proto, Makefile, script, config 변경이 배포 범위에 포함되면 해당 도메인 규칙의 테스트도 추가한다.
   - 테스트가 실패하면 build/deploy를 진행하지 않고 실패 패키지와 핵심 오류를 보고한다.
   - mac-mini 또는 로컬 PATH에 Go가 없으면 시스템 전역 설치 대신 `build/.tools` 같은 ignored runtime 경로에 임시 Go toolchain을 두고 사용한다.

5. **dev-corp runtime rebuild**
   - 같은 source ref에서 dev-corp Control Plane binary, Edge binary, mac node binary, Linux ARM64 node binary를 다시 빌드한다.
   - mac-mini에서 직접 빌드할 수 없으면 같은 source ref 기준으로 로컬에서 빌드한 뒤 SSH 표준 입력 스트림으로 mac-mini canonical target과 같은 디렉터리의 새 candidate 경로에 생성한다. `scp`/`rsync` 전송 재시도나 권한 완화는 하지 않는다.
   - Windows AMD64 node binary는 dev-corp 기본 provider pool 대상이 아니다.
   - stale binary가 의심되거나 `config refresh` subcommand, admin port, version 출력이 맞지 않으면 clean sync부터 다시 시작한다.
   - 빌드 산출물 경로, timestamp, 크기, 실행 가능 여부를 기록한다.

6. **빌드 후 config check**
   - 빌드된 Edge binary로 `config check`, `config refresh --help`, `config refresh --mode dry-run`을 실행한다.
   - dry-run이 `rejected` 또는 예상 밖 `restart_required`를 반환하면 배포를 멈추고 config diff를 보고한다.

7. **배포와 재시작**
   - Control Plane이 public host에서 운용되는 경우 `iop.ai.kr:18002/19004/19005` dev-corp port를 기준으로 확인한다. mac-mini local Control Plane은 dev-corp Edge source of truth가 아니다.
   - Edge는 public `iop.ai.kr` host에서 `18085/18086/18087`을 기준으로 재시작/확인한다. mac-mini runner runtime은 dev-corp Edge source of truth가 아니다.
   - **보안 전송 차단 시 SSH stdin candidate 표준**: `scp`/`rsync` 같은 파일 전송 프로토콜이 `Operation not permitted`로 차단되면 권한을 올리거나 전송을 재시도하지 않는다. canonical target과 같은 디렉터리에 timestamp 또는 고유 suffix를 붙인, 아직 존재하지 않는 candidate 경로를 먼저 정한다. 같은 filesystem에 두어야 이후 `mv` 전환이 가능하다.
   - source artifact는 `ssh <target> 'umask 077; tee /absolute/path/to/target.candidate >/dev/null' < /absolute/path/to/local-artifact`처럼 SSH stdin으로 candidate를 생성한다. config는 `<config-generator> | ssh <target> 'umask 077; tee /absolute/path/to/config.candidate >/dev/null'`처럼 생성 결과만 stream으로 보낸다. candidate path는 canonical target과 같은 디렉터리의 새 경로로 치환하며, token·config 원문을 command argument, 터미널 출력, command history, 배포 보고에 남기지 않는다.
   - cutover 전에 source와 remote candidate의 SHA-256과 byte length를 비교한다. macOS에서는 `shasum -a 256`, Linux에서는 `sha256sum`을 사용한다. candidate의 소유자와 mode도 확인하고, 실행 binary는 검증 후에만 `chmod 755`로 전환하며 secret-bearing config는 `0600`을 유지한다. 불일치 또는 preflight 실패 시 cutover하지 않는다.
   - 현재 실행 파일은 보존한 채 candidate의 `--help`와 필요한 config check를 먼저 통과시킨다. 전환은 같은 filesystem에서 canonical 파일을 timestamp backup으로 `mv`한 뒤 candidate를 canonical path로 `mv`한다. restart 또는 readiness 실패 시 backup을 canonical path로 다시 `mv`하여 즉시 rollback한다.
   - DGX Spark 01/02는 Linux ARM64 node binary를 mac-mini 경유로 배포하고 기존 node process를 재시작한다.
   - Mac Studio는 macOS node binary를 mac-mini 경유로 배포하고 기존 node process를 재시작한다.
   - provider runtime 옵션을 갱신하는 배포라면 DGX Spark 01은 `/home/digitalcommerce_dgx_spark_01/start_vllm_ornith35b_docker_8003.sh` 기준으로 Docker container `iop-vllm-ornith35b-fp8`를 재생성한다. 핵심 옵션은 `--max-model-len 262144`, `--gpu-memory-utilization 0.47`, `--max-num-seqs 4`, `--port 8000`, `--enable-prefix-caching`, `--enable-auto-tool-choice`, `--tool-call-parser qwen3_xml`, `--reasoning-parser qwen3`, `--trust-remote-code`, `--runner generate`이다.
   - provider runtime 옵션을 갱신하는 배포라면 DGX Spark 02는 `/home/dplab/start_vllm_ornith35b_docker_8005.sh` 기준으로 Docker container `iop-vllm-ornith35b-fp8`를 재생성한다. 핵심 옵션은 `--max-model-len 262144`, `--gpu-memory-utilization 0.50`, `--max-num-seqs 4`, `--port 8000`, `--enable-prefix-caching`, `--enable-auto-tool-choice`, `--tool-call-parser qwen3_xml`, `--reasoning-parser qwen3`, `--trust-remote-code`, `--runner generate`이다.
   - provider runtime 옵션을 갱신하는 배포라면 Mac Studio는 `screen -dmS vllm_mlx_8004` 안에서 `DYLD_LIBRARY_PATH=/opt/homebrew/opt/expat/lib`를 export한 뒤 `/Users/dc_dev/vllm-env-0.4.0/bin/python -m vllm_mlx.cli serve`를 `--continuous-batching`, `--max-num-seqs 6`, `--prefill-batch-size 6`, `--completion-batch-size 6`, `--chunked-prefill-tokens 1024`, `--disable-prefix-cache`, `--max-kv-size 786432`, `--max-request-tokens 262144`, `--enable-auto-tool-choice`, `--tool-call-parser gemma4`, `--reasoning-parser gemma4`, `--default-chat-template-kwargs '{"enable_thinking":true}'`로 재기동한다.
   - Node bootstrap은 Edge의 `node register`가 출력한 OS별 완성 명령을 우선한다. Node `edge_addr`는 `iop.ai.kr:18087`이어야 한다. node-local provider endpoint 또는 별도 `IOP_HOME`이 필요한 경우에는 `~/iop-dev-corp-field/node.yaml`을 수동 배치할 수 있고, token 원문은 로그/보고에 남기지 않는다.

8. **배포 후 연결 검증**
   - Edge, OpenAI-compatible listener, Node TCP, admin port, Control Plane status port가 열려 있는지 확인한다.
   - public Control Plane status가 접근 가능하면 Edge가 connected이고 dev-corp 기준 3개 provider node가 connected인지 확인한다.
   - public Control Plane status가 접근 가능하면 각 node의 `provider_snapshots`에서 provider `id`, `capacity`, `in_flight`, `queued`, `health`, `served_models`를 확인한다. 접근할 수 없으면 Node log의 `registered with edge`/`connected to edge`와 public OpenAI-compatible smoke 결과를 연결 증거로 분리 보고한다.
   - `/v1/models`가 대상 model alias를 노출하는지 확인한다.

9. **OpenAI-compatible capacity smoke**
   - `/v1/chat/completions`를 검증한다. Provider-pool passthrough 경계를 바꾼 배포라면 selected provider가 지원하는 OpenAI-compatible extension field 예: `chat_template_kwargs`가 IOP에서 거부되지 않고 provider로 전달되는지도 확인한다. `/v1/responses`는 capacity 성공 기준이 아니라 provider-dependent passthrough/relay 검증으로 분리한다. legacy `/v1/completions`는 route가 구현되어 있지 않으면 실패로 보지 않는다.
   - 표준 부하 프롬프트는 700~1200 token 수준의 구조화된 답변을 유도해 요청이 동시에 관측될 시간을 만든다.
   - model-specific smoke를 실행한다. `ornith:35b`는 Spark01/02 합산 capacity `8` 기준 9개와 6개 동시 요청을 보내고, `gemma4:26b`는 Mac Studio capacity `5` 기준 9개와 6개 동시 요청을 보낸다.
   - Control Plane status가 접근 가능하면 요청 실행 중 반복 polling하여 대상 provider들의 `in_flight`가 각 model capacity에 도달하고 초과 요청이 queue에 들어가는지 증거로 남긴다. 짧은 요청에서는 polling이 queued 순간을 놓칠 수 있으므로 HTTP 성공, max in_flight, 최종 회복을 함께 판정한다.
   - public Control Plane status가 접근 가능하면 각 provider의 `in_flight`가 자기 capacity를 넘지 않고, 적어도 한 번은 기대 capacity까지 차는지 확인한다.
   - public Control Plane status가 접근 가능하면 모든 요청 완료 후 같은 status에서 대상 provider들의 `in_flight=0`, `queued=0`으로 돌아오는지 확인한다. 접근할 수 없으면 model-specific HTTP 성공 결과와 Node 연결 로그만으로 smoke 통과와 capacity snapshot 미관측을 구분한다.
   - Gemma 계열 reasoning/tool-parser 텍스트는 정상 응답으로 허용한다. exact-output match를 smoke 성공 기준으로 삼지 않는다.
   - agent/tool-call 경계를 검증할 때는 forced tool call, auto tool call, streaming `delta.tool_calls`, multi-turn tool result 후 최종 답변을 provider direct와 Edge OpenAI-compatible 경로에서 나눠 확인한다. provider direct가 통과하고 Edge 경유만 실패하면 provider runtime option 문제가 아니라 Edge/Node relay 또는 validation 경계 문제로 분리한다.

10. **결과 보고**
   - source ref, clean sync 결과, 테스트 결과, 빌드 산출물, process/port 상태, connected node 목록, provider capacity snapshot, capacity smoke 관측값을 보고한다.
   - 실패한 단계가 있으면 다음 단계를 진행했는지 여부를 명확히 구분한다.
   - capacity smoke가 타이밍 문제로 관측 실패했으면 요청 성공과 별도로 `capacity 관측 미충족`으로 보고하고, 프롬프트 길이 또는 status polling 간격 조정을 제안한다.

## 실행 결과 검증

- [ ] mac-mini checkout이 배포 기준 ref로 clean sync되었는가
- [ ] provider direct endpoint 3개가 mac-mini에서 `/health`와 `/v1/models`에 성공했는가
- [ ] 빌드 전 `go test ./...`와 필요한 추가 테스트가 통과했는가
- [ ] dev-corp Control Plane/Edge/mac/Linux ARM64 binary가 같은 source ref에서 rebuild되었는가
- [ ] 빌드 후 config check, refresh help, refresh dry-run이 통과했는가
- [ ] Edge와 3개 provider node가 재시작되고 connected 상태인가
- [ ] `/v1/chat/completions` model-specific capacity smoke에서 각 model/provider capacity까지 `in_flight`가 차고 초과 요청이 queue에 잡혔는가
- [ ] 완료 후 provider `in_flight=0`, `queued=0`으로 회복되었는가
- 검증 실패 시: 실패 단계, 실패한 host/provider/endpoint, 관측된 snapshot, 진행 중단 여부를 보고한다.

## 출력 형식

```text
dev-corp runtime 배포 결과
- Source: <branch/ref/commit>, clean=<yes|no>
- Provider preflight: dgx01=<pass|fail>, dgx02=<pass|fail>, mac-studio=<pass|fail>
- Pre-build tests: <command> - <pass|fail|not-run>
- Build: control-plane=<path>, edge=<path>, mac-node=<path>, linux-arm64-node=<path>
- Post-build checks: config-check=<pass|fail>, refresh-help=<pass|fail>, refresh-dry-run=<status>
- Deployment: control-plane=<pid/status>, edge=<pid/status>, dgx01=<pid/status>, dgx02=<pid/status>, mac-studio=<pid/status>
- Ports: <port summary>
- Nodes: <node_id connected summary>
- Providers: <provider_id capacity/in_flight/queued/health summary>
- OpenAI-compatible: models=<pass|fail>, chat-completions-capacity=<pass|fail>, provider-native-field-passthrough=<pass|fail|not-run>, responses-provider-pool=<pass|provider-unsupported|fail|not-run>
- Capacity evidence: <endpoint별 max in_flight/queued snapshot>
- Blockers/Risk: <없음 또는 내용>
```

## 금지 사항

- mac-mini checkout이 없거나 dirty/divergent 상태인데 배포를 계속하지 않는다.
- 사용자가 명시적으로 요청하지 않았는데 mac-mini runner/provider 관리 경로를 dev-corp Edge runtime, Node `edge_addr`, bootstrap 기본 URL, OpenAI-compatible base URL로 사용하지 않는다.
- `git clean -fdx`를 기본 cleanup으로 사용하지 않는다.
- 빌드 전 테스트 실패 후 배포를 계속하지 않는다.
- `scp`/`rsync`가 보안 정책으로 차단됐을 때 권한·ACL·보안 도구를 변경하거나 `sudo`로 우회하지 않는다. 같은 filesystem의 새 SSH stdin candidate 생성, checksum 검증, `mv` cutover/rollback 절차를 사용한다.
- `umask 077`으로 생성된 실행 binary를 `0600` 상태로 기동하지 않는다. checksum 검증 후 필요한 실행 권한만 부여한다.
- DGX Spark 02 Ornith provider endpoint를 `192.168.2.4:8000`, `8001`, `8002`, `8004`로 잘못 사용하지 않는다. 현재 기본은 `192.168.2.4:8005`이다.
- Mac Studio secondary `8005` endpoint를 별도 alias/capacity 결정 없이 기본 provider pool에 포함하지 않는다.
- Mac Studio `8004` vLLM-MLX runtime 재시작 방식을 확인하지 않고 provider process를 임의 종료하지 않는다.
- node02처럼 tunnel 예외가 필요한 경우를 제외하고 Node bootstrap 기본 경로에서 수동 token 치환, named env parameter, 수동 `node.yaml` 작성을 요구하지 않는다.
- Gemma reasoning/tool-parser 출력을 실패로 판정하거나 exact-output smoke를 기본 성공 기준으로 삼지 않는다.
- OpenAI-compatible 요청 성공만으로 provider snapshot capacity 관측 성공을 선언하지 않는다. public Control Plane status가 접근 가능할 때만 `in_flight/queued` 관측 성공을 함께 남기고, 접근할 수 없으면 capacity snapshot 미관측으로 분리 보고한다.
- 이 프로젝트 전용 배포 절차를 `agent-ops/rules/common/_templates` 또는 common skill에 넣지 않는다.
