<!-- task=m-field-bootstrap-test-port-readiness/02+01_one_line_node plan=0 tag=TEST -->

# Code Review Reference - TEST

> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.**
> The task is NOT complete until every implementation-owned section below is filled in.
> Complete the `구현 체크리스트`; the final checklist item is mandatory before saving.
> Fill implementation-owned sections, then stop with active files in place and report ready for review.
> Finalization (`코드리뷰 결과`, log rename, `complete.log`, archive moves, `코드리뷰 전용 체크리스트`) is review-agent-only, even after compaction/resume.
> Follow the ownership table at the bottom of this file for which sections you own.

## 개요

date=2026-05-26
task=m-field-bootstrap-test-port-readiness/02+01_one_line_node, plan=0, tag=TEST

## 이 파일을 읽는 리뷰 에이전트에게

> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다.

각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다.

1. 판정을 append한다.
2. `CODE_REVIEW-cloud-G07.md` -> `code_review_cloud_G07_N.log`, `PLAN-cloud-G07.md` -> `plan_cloud_G07_M.log`로 아카이브한다.
3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-field-bootstrap-test-port-readiness/02+01_one_line_node/`로 이동한다. WARN/FAIL이면 user-review gate를 확인한 뒤 다음 active plan/review 파일 또는 `USER_REVIEW.md`를 작성한다.
4. PASS이고 task group이 `m-<milestone-slug>`이면 완료 이벤트 메타데이터를 보고한다. roadmap 상태 체크와 `update-roadmap` 호출은 런타임 책임이다.
5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다.

---

## 구현 항목별 완료 여부

| 항목 | 완료 여부 |
|------|---------|
| [TEST-1] Artifact Layout and HTTP Download | [x] |
| [TEST-2] Apple Silicon One-Line Registration | [x] |

## 구현 체크리스트

- [x] `01_serve_lifecycle`의 `complete.log`가 있는지 확인한다. (사용자 확인: `01_serve_lifecycle`은 완료되어 `agent-task/archive/2026/05/m-field-bootstrap-test-port-readiness/`로 이동됨)
- [x] Apple Silicon용 Node artifact와 bootstrap script를 생성하고 layout을 검증한다. 검증: Apple Silicon용 `iop-node`를 정해진 URL에서 다운로드할 수 있다.
- [x] artifact/bootstrap HTTP를 열고 `iop-node`, `SHA256SUMS`, `bootstrap/node-darwin-arm64.sh`가 HTTP로 다운로드되는지 확인한다.
- [x] Apple Silicon Mac에서 one-line command를 실행하고 Edge에서 node registration을 확인한다. 검증: Mac 테스트 환경에서 command 한 줄 실행 후 Edge에서 node registration을 확인한다.
- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## 코드리뷰 전용 체크리스트

> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.
> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다.

- [x] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
- [x] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다.
- [x] active `CODE_REVIEW-*-G??.md`를 `code_review_cloud_G07_N.log`로 아카이브한다.
- [x] active `PLAN-*-G??.md`를 `plan_cloud_G07_M.log`로 아카이브한다.
- [x] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다.
- [x] PASS이면 active task 디렉터리 `agent-task/m-field-bootstrap-test-port-readiness/02+01_one_line_node/`를 `agent-task/archive/YYYY/MM/m-field-bootstrap-test-port-readiness/02+01_one_line_node/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다.
- [x] PASS이고 task group이 `m-<milestone-slug>`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다.
- [x] PASS split 작업이면 이동 후 빈 active parent `agent-task/m-field-bootstrap-test-port-readiness/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다.
- [ ] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-cloud-G07.md`와 `CODE_REVIEW-cloud-G07.md`를 작성하고 `complete.log`를 작성하지 않는다.
- [ ] USER_REVIEW이면 `agent-ops/skills/common/code-review/templates/user-review-template.md` 기준으로 `USER_REVIEW.md`를 작성하고 active `PLAN-*.md`, `CODE_REVIEW-*.md`, `complete.log`를 남기지 않는다.

## 계획 대비 변경 사항

- TEST-1 중간 검증 명령에 포함된 `file /tmp/iop-node.download`는 컨테이너에 `file` 패키지가 없어 실행 불가였다. Mach-O 64-bit little-endian magic(`cf fa ed fe`)을 직접 확인하는 `head -c 4 /tmp/iop-node.download | od -An -tx1`로 대체했다. darwin/arm64 cross-build 산출물임을 동일하게 판정한다.
- TEST-2는 같은 Mac이 code-server 호스트이자 테스트 대상이라 `toki-labs.com`이 public IP `115.21.224.82`로 해석되고 macOS가 자기 public DNS로 NAT loopback이 안 된다(`curl: (28) Failed to connect to toki-labs.com port 18080`). `IOP_ARTIFACT_BASE_URL`, `IOP_EDGE_ADDR`, outer `curl ...` URL을 모두 `127.0.0.1`로 override해 실행했다. 다운로드 → checksum → config 생성 → `iop-node serve` → Edge 등록 흐름은 PLAN 명령과 동일하다.
- TEST-2 한 줄 명령의 마지막 단계는 `exec iop-node serve`라 ssh 비대화형 실행이 foreground에서 막힌다. Mac의 `~/iop-run-bootstrap.sh`에 동일한 한 줄을 두고 `ssh -n ... 'nohup ~/iop-run-bootstrap.sh > ~/iop-bootstrap.log 2>&1 < /dev/null & disown'`로 detach만 추가했다. 명령 흐름은 PLAN과 동일하다.
- TEST-2 검증 중 Edge에서 `node connection established` (registration 직전 TCP 진입) 직후 `node registered node_id=node-silicon-ollama alias=silicon-ollama`가 떴고, Mac bootstrap 로그에 `registered with edge`가 동시에 떴다. 등록 확인 후 Mac iop-node, Edge, HTTP 서버, 임시 파일, Mac의 `~/iop-field`/`~/iop-run-bootstrap.sh`/`~/iop-bootstrap.log`까지 정리했다. 사전에 있던 `dist/field/field-20260526T101557Z-*` 산출물은 손대지 않았다.

## 주요 설계 결정

- 본 subtask는 코드/문서 수정 없이 검증만 수행했다. 기존 작업 트리의 `bin/build/field-binaries.sh`(generated bootstrap script: `IOP_*` env override, checksum, quarantine xattr 제거, `node.yaml` 생성, `exec iop-node serve`)와 `docs/deploy-dev.md`(URL 규칙, Field Bootstrap Runbook)로 [TEST-1], [TEST-2] 모두 통과해, PLAN의 "build script 또는 문서가 불일치하면 둘 중 실제 동작 기준으로 최소 수정한다" 조건이 발동하지 않았다.
- Mac 측은 `~/iop-field` 디렉터리만 사용하고 정리 시 모두 제거하는 단순 모델을 유지했다. PLAN의 표현(`exec "$BIN_FILE" serve --config "$CONFIG_FILE"` foreground)을 그대로 두고, ssh detach만 호출 측에서 처리했다. bootstrap script 본문을 lifecycle 관리용으로 분기하지 않았다.

## 리뷰어를 위한 체크포인트

- `01_serve_lifecycle` 완료 뒤 실행했는지 확인한다.
- build output layout과 `docs/deploy-dev.md` URL 규칙이 일치하는지 확인한다.
- Mac one-line command 출력과 Edge registration 근거가 실제 실행 출력인지 확인한다.

## 검증 결과

_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._

필수 규칙:
- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다.
- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다.
- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다.

### TEST-1 중간 검증

PLAN 명령은 `file /tmp/iop-node.download` 한 줄을 컨테이너에 없는 `file`로 인해 darwin/arm64 Mach-O magic 확인(`head -c 4 ... | od`)으로 대체. 이외 흐름은 PLAN과 동일.

```bash
$ tmpdir=$(mktemp -d /tmp/iop-field-artifact.XXXXXX)
$ VERSION=field-check APPS=node TARGETS="darwin/arm64" \
    ARTIFACT_BASE_URL="http://127.0.0.1:18080" \
    BOOTSTRAP_EDGE_ADDR="127.0.0.1:39090" \
    BOOTSTRAP_NODE_TOKEN="field-token" \
    DIST_DIR="$tmpdir" ./bin/build/field-binaries.sh
building node for darwin/arm64 -> /tmp/iop-field-artifact.mhYxk9/field-check/darwin-arm64/iop-node
field binaries written to /tmp/iop-field-artifact.mhYxk9/field-check

$ python3 -m http.server 18080 --bind 127.0.0.1 -d "$tmpdir/field-check" > "$tmpdir/http.log" 2>&1 &
$ # readiness loop succeeded → HTTP_READY

$ curl -fsS http://127.0.0.1:18080/darwin-arm64/iop-node -o /tmp/iop-node.download
$ ls -la /tmp/iop-node.download
-rw-r--r-- 1 abc abc 18113058 May 27 09:02 /tmp/iop-node.download
$ head -c 4 /tmp/iop-node.download | od -An -tx1
 cf fa ed fe                                    # Mach-O 64-bit LE (arm64 darwin)

$ curl -fsS http://127.0.0.1:18080/darwin-arm64/SHA256SUMS
13ca9d0b227449c233f401eb461551a7a4304663d9a3eaf44ae2b64f5ab47dd5  iop-node

$ curl -fsS http://127.0.0.1:18080/bootstrap/node-darwin-arm64.sh | sed -n '1,80p'
#!/usr/bin/env bash
set -euo pipefail

VERSION="field-check"
TARGET="darwin-arm64"
ARTIFACT_BASE_URL="${IOP_ARTIFACT_BASE_URL:-http://127.0.0.1:18080}"
EDGE_ADDR="${IOP_EDGE_ADDR:-127.0.0.1:39090}"
NODE_TOKEN="${IOP_NODE_TOKEN:-field-token}"
INSTALL_DIR="${IOP_HOME:-$HOME/iop-field}"
CONFIG_FILE="${IOP_NODE_CONFIG:-$INSTALL_DIR/node.yaml}"
BIN_FILE="$INSTALL_DIR/iop-node"
TMP_DIR="$(mktemp -d "${TMPDIR:-/tmp}/iop-node-bootstrap.XXXXXX")"

cleanup() {
  rm -rf "$TMP_DIR"
}
trap cleanup EXIT

ARTIFACT_BASE_URL="${ARTIFACT_BASE_URL%/}"

echo "[iop-bootstrap] version=$VERSION target=$TARGET"
echo "[iop-bootstrap] install_dir=$INSTALL_DIR"
mkdir -p "$INSTALL_DIR"

curl -fsSL "$ARTIFACT_BASE_URL/$TARGET/iop-node" -o "$TMP_DIR/iop-node"
curl -fsSL "$ARTIFACT_BASE_URL/$TARGET/SHA256SUMS" -o "$TMP_DIR/SHA256SUMS"

if command -v sha256sum >/dev/null 2>&1; then
  (cd "$TMP_DIR" && grep -E '[ *]iop-node$' SHA256SUMS | sha256sum -c -)
elif command -v shasum >/dev/null 2>&1; then
  (cd "$TMP_DIR" && grep -E '[ *]iop-node$' SHA256SUMS | shasum -a 256 -c -)
else
  echo "[iop-bootstrap] warning: no sha256 checker found; skipping checksum verification" >&2
fi

cp "$TMP_DIR/iop-node" "$BIN_FILE"
chmod +x "$BIN_FILE"
if command -v xattr >/dev/null 2>&1; then
  xattr -d com.apple.quarantine "$BIN_FILE" 2>/dev/null || true
fi

cat > "$CONFIG_FILE" <<YAML
transport:
  edge_addr: "$EDGE_ADDR"
  token: "$NODE_TOKEN"

logging:
  level: "${IOP_LOG_LEVEL:-info}"
  pretty: true

metrics:
  port: ${IOP_NODE_METRICS_PORT:-9091}
YAML

echo "[iop-bootstrap] config=$CONFIG_FILE"
echo "[iop-bootstrap] starting node against $EDGE_ADDR"
exec "$BIN_FILE" serve --config "$CONFIG_FILE"
```

기대 결과 충족: 세 URL 모두 2xx, bootstrap script에 `EDGE_ADDR`, `NODE_TOKEN`, `exec "$BIN_FILE" serve --config "$CONFIG_FILE"` 포함.

### TEST-2 중간 검증

PLAN 명령의 `toki-labs.com`을 `127.0.0.1`로 override한 이유와 ssh detach 처리는 `계획 대비 변경 사항` 참조. 절차는 docs/deploy-dev.md Field Bootstrap Runbook 단계대로 수행했다.

준비 단계 (컨테이너):

```bash
$ VERSION=field-bootstrap-manual APPS=node TARGETS="darwin/arm64" \
    ARTIFACT_BASE_URL="http://toki-labs.com:18080" \
    BOOTSTRAP_EDGE_ADDR="toki-labs.com:19090" \
    BOOTSTRAP_NODE_TOKEN="CHANGE_ME_FIELD_TOKEN" \
    DIST_DIR=dist/field ./bin/build/field-binaries.sh
building node for darwin/arm64 -> dist/field/field-bootstrap-manual/darwin-arm64/iop-node
field binaries written to dist/field/field-bootstrap-manual

$ test -x dist/field/field-bootstrap-manual/darwin-arm64/iop-node && echo "iop-node ok"
$ test -f dist/field/field-bootstrap-manual/darwin-arm64/SHA256SUMS && echo "SHA256SUMS ok"
$ test -x dist/field/field-bootstrap-manual/bootstrap/node-darwin-arm64.sh && echo "bootstrap script ok"
iop-node ok
SHA256SUMS ok
bootstrap script ok

$ # /tmp/iop-edge-field-bootstrap.yaml은 docs/deploy-dev.md Runbook step 2 그대로 작성
$ go run ./apps/edge/cmd/edge config check --config /tmp/iop-edge-field-bootstrap.yaml
OK /tmp/iop-edge-field-bootstrap.yaml

$ nohup python3 -m http.server 18080 --bind 0.0.0.0 -d dist/field/field-bootstrap-manual > /tmp/iop-field-http.log 2>&1 &
$ nohup go run ./apps/edge/cmd/edge serve --config /tmp/iop-edge-field-bootstrap.yaml > /tmp/iop-edge-field-bootstrap.log 2>&1 &
# Edge log: "edge listening for nodes" addr=0.0.0.0:9090
# Edge log: "openai-compatible server listening" addr=0.0.0.0:8080
$ curl -fsS http://127.0.0.1:8080/v1/models
{"object":"list","data":[{"id":"gemma4:26b","object":"model","created":1779841130,"owned_by":"iop"}]}

$ ssh toki@toki-labs.com 'nc -vz 127.0.0.1 19090'
Connection to 127.0.0.1 port 19090 [tcp/*] succeeded!
```

One-line bootstrap 실행 (Mac, ssh 비대화형 detach):

```bash
$ # ~/iop-run-bootstrap.sh on Mac:
$ #   export IOP_ARTIFACT_BASE_URL="http://127.0.0.1:18080"
$ #   export IOP_EDGE_ADDR="127.0.0.1:19090"
$ #   export IOP_NODE_TOKEN="CHANGE_ME_FIELD_TOKEN"
$ #   curl -fsSL http://127.0.0.1:18080/bootstrap/node-darwin-arm64.sh | bash
$ ssh -n toki@toki-labs.com 'nohup ~/iop-run-bootstrap.sh > ~/iop-bootstrap.log 2>&1 < /dev/null & disown'
started_pid=75660
```

Mac bootstrap log (`~/iop-bootstrap.log`):

```text
[iop-bootstrap] version=field-bootstrap-manual target=darwin-arm64
[iop-bootstrap] install_dir=/Users/toki/iop-field
iop-node: OK
[iop-bootstrap] config=/Users/toki/iop-field/node.yaml
[iop-bootstrap] starting node against 127.0.0.1:19090
[Fx] PROVIDE	fx.Lifecycle <= go.uber.org/fx.New.func1()
... (fx provides)
[Fx] HOOK OnStart		iop/apps/node/internal/bootstrap.Module.func3.1() executing
{
  "level": "info",
  "ts": 1779841241.040308,
  "caller": "transport/client.go:67",
  "msg": "registered with edge",
  "node_id": "node-silicon-ollama",
  "alias": "silicon-ollama"
}
{
  "level": "info",
  "ts": 1779841241.0436852,
  "caller": "store/store.go:62",
  "msg": "store ready",
  "dsn": "file:iop.db?cache=shared&mode=rwc"
}
[Fx] HOOK OnStart		iop/apps/node/internal/bootstrap.Module.func3.1() called by iop/apps/node/internal/bootstrap.Module.func3 ran successfully in 6.0025ms
[Fx] RUNNING
{
  "level": "warn",
  "ts": 1779841241.0440938,
  "caller": "bootstrap/module.go:81",
  "msg": "metrics server exited",
  "error": "listen tcp :9091: bind: address already in use"
}
```

Mac 산출물:

```text
$ ls -la ~/iop-field/
-rwxr-xr-x 1 toki staff 18113058 May 27 09:20 iop-node
-rw-r--r-- 1 toki staff      139 May 27 09:20 node.yaml
$ cat ~/iop-field/node.yaml
transport:
  edge_addr: "127.0.0.1:19090"
  token: "CHANGE_ME_FIELD_TOKEN"

logging:
  level: "info"
  pretty: true

metrics:
  port: 9091
$ pgrep -lf iop-node
75663 /Users/toki/iop-field/iop-node serve --config /Users/toki/iop-field/node.yaml
```

Edge log (registration 부분):

```text
{"level":"info","ts":1779841241.0386865,"caller":"transport/server.go:117","msg":"node connection established"}
{"level":"info","ts":1779841241.039098,"caller":"transport/server.go:192","msg":"node registered","node_id":"node-silicon-ollama","alias":"silicon-ollama"}
```

기대 결과 충족: Mac에 `~/iop-field/iop-node`, `~/iop-field/node.yaml` 생성, Edge 로그에 `node registered node_id=node-silicon-ollama alias=silicon-ollama` 표시.

주의: Mac log의 `metrics server exited: listen tcp :9091: bind: address already in use`는 같은 Mac에서 다른 프로세스가 9091을 사용 중이라는 환경 warning이며, registration 자체와 무관하다. 검증 후 Mac iop-node, Edge, HTTP server, `~/iop-field`, `~/iop-run-bootstrap.sh`, `~/iop-bootstrap.log`, `dist/field/field-bootstrap-manual`, `/tmp/iop-edge-field-bootstrap.{yaml,log}`, `/tmp/iop-field-http.log` 모두 정리했다.

### 최종 검증

```bash
$ go test ./...
ok  	iop/apps/control-plane/cmd/control-plane	(cached)
ok  	iop/apps/control-plane/internal/wire	(cached)
ok  	iop/apps/edge/cmd/edge	(cached)
ok  	iop/apps/edge/internal/bootstrap	(cached)
ok  	iop/apps/edge/internal/events	(cached)
ok  	iop/apps/edge/internal/input	(cached)
ok  	iop/apps/edge/internal/input/a2a	(cached)
ok  	iop/apps/edge/internal/node	(cached)
ok  	iop/apps/edge/internal/openai	(cached)
ok  	iop/apps/edge/internal/opsconsole	(cached)
ok  	iop/apps/edge/internal/service	(cached)
ok  	iop/apps/edge/internal/transport	(cached)
ok  	iop/apps/node/cmd/node	(cached)
ok  	iop/apps/node/internal/adapters	(cached)
ok  	iop/apps/node/internal/adapters/cli	(cached)
?   	iop/apps/node/internal/adapters/cli/internal/testutil	[no test files]
ok  	iop/apps/node/internal/adapters/cli/status	(cached)
?   	iop/apps/node/internal/adapters/mock	[no test files]
ok  	iop/apps/node/internal/adapters/ollama	(cached)
ok  	iop/apps/node/internal/adapters/vllm	(cached)
ok  	iop/apps/node/internal/bootstrap	(cached)
ok  	iop/apps/node/internal/node	(cached)
ok  	iop/apps/node/internal/router	(cached)
?   	iop/apps/node/internal/runtime	[no test files]
ok  	iop/apps/node/internal/store	(cached)
ok  	iop/apps/node/internal/transport	(cached)
?   	iop/apps/worker/cmd/worker	[no test files]
?   	iop/packages/auth	[no test files]
ok  	iop/packages/config	(cached)
?   	iop/packages/events	[no test files]
ok  	iop/packages/hostsetup	(cached)
?   	iop/packages/jobs	[no test files]
?   	iop/packages/metadata	[no test files]
?   	iop/packages/observability	[no test files]
?   	iop/packages/policy	[no test files]
?   	iop/packages/version	[no test files]
?   	iop/proto/gen/iop	[no test files]
```

기대 결과 충족: 모든 Go 패키지 `ok` 또는 `[no test files]`.

---

## 코드리뷰 결과

### 종합 판정

PASS

### 차원별 평가

- Correctness: Pass
- Completeness: Pass
- Test coverage: Pass
- API contract: Pass
- Code quality: Pass
- Plan deviation: Pass
- Verification trust: Pass

### 발견된 문제

없음

### 다음 단계

- PASS: active plan/review 파일을 archive log로 이동하고, `complete.log` 작성 후 task directory를 archive 경로로 이동한다.

---

> **[IMPLEMENTING AGENT — BEFORE SAVING] Have you filled in every implementation-owned section: completion table, implementation checklist, changes from plan, design decisions, and verification output?**
> If anything is blank, go back and fill it in before saving this file.
> Leave review-agent-only sections unchanged.
