From 590e3219dab9434352fd16be3a32abbe9c50d3d8 Mon Sep 17 00:00:00 2001 From: toki Date: Mon, 15 Jun 2026 17:12:35 +0900 Subject: [PATCH] feat: cross-os runner bootstrap task and related updates - Add agent-task/m-cross-os-runner-bootstrap with subtasks (G07, G06) - Update roadmap phases and milestones for control-plane-product-surface - Add runner.proto definitions and regenerate code - Update HTTP server handler for runner management - Add smoke tests and update local test configurations - Update README with latest project status --- README.md | 17 ++ .../control-plane-product-surface/PHASE.md | 10 +- .../milestones/cross-os-runner-bootstrap.md | 21 +- .../milestones/flutter-console-skeleton.md | 5 +- .../01_core_command/CODE_REVIEW-cloud-G07.md | 153 ++++++++++++ .../01_core_command/PLAN-cloud-G07.md | 66 +++++ .../code_review_cloud_G06_0.log | 105 ++++++++ .../01_core_command/plan_cloud_G06_0.log | 117 +++++++++ .../CODE_REVIEW-cloud-G07.md | 59 +++++ .../02_unix_bootstrap/PLAN-cloud-G07.md | 110 +++++++++ .../CODE_REVIEW-cloud-G07.md | 59 +++++ .../03+01_windows_bootstrap/PLAN-cloud-G07.md | 122 ++++++++++ .../CODE_REVIEW-local-G06.md | 59 +++++ .../PLAN-local-G06.md | 114 +++++++++ .../CODE_REVIEW-cloud-G07.md | 59 +++++ .../PLAN-cloud-G07.md | 124 ++++++++++ agent-test/local/agent-smoke.md | 5 + apps/runner/lib/oto/agent/oto/runner.pb.dart | 12 + .../lib/oto/agent/oto/runner.pbjson.dart | 9 +- .../test/oto_agent_bootstrap_script_test.dart | 229 +++++++++++++----- proto/oto/runner.proto | 1 + .../internal/httpserver/runner_handlers.go | 42 +++- .../core/internal/httpserver/server_test.go | 91 +++++++ services/core/oto/runner.pb.go | 13 +- 24 files changed, 1504 insertions(+), 98 deletions(-) create mode 100644 agent-task/m-cross-os-runner-bootstrap/01_core_command/CODE_REVIEW-cloud-G07.md create mode 100644 agent-task/m-cross-os-runner-bootstrap/01_core_command/PLAN-cloud-G07.md create mode 100644 agent-task/m-cross-os-runner-bootstrap/01_core_command/code_review_cloud_G06_0.log create mode 100644 agent-task/m-cross-os-runner-bootstrap/01_core_command/plan_cloud_G06_0.log create mode 100644 agent-task/m-cross-os-runner-bootstrap/02_unix_bootstrap/CODE_REVIEW-cloud-G07.md create mode 100644 agent-task/m-cross-os-runner-bootstrap/02_unix_bootstrap/PLAN-cloud-G07.md create mode 100644 agent-task/m-cross-os-runner-bootstrap/03+01_windows_bootstrap/CODE_REVIEW-cloud-G07.md create mode 100644 agent-task/m-cross-os-runner-bootstrap/03+01_windows_bootstrap/PLAN-cloud-G07.md create mode 100644 agent-task/m-cross-os-runner-bootstrap/04+02,03_binary_contract/CODE_REVIEW-local-G06.md create mode 100644 agent-task/m-cross-os-runner-bootstrap/04+02,03_binary_contract/PLAN-local-G06.md create mode 100644 agent-task/m-cross-os-runner-bootstrap/05+01,02,03_node_connect/CODE_REVIEW-cloud-G07.md create mode 100644 agent-task/m-cross-os-runner-bootstrap/05+01,02,03_node_connect/PLAN-cloud-G07.md diff --git a/README.md b/README.md index f686eca..b367427 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,23 @@ Flutter web preview는 `cd apps/client && flutter run -d chrome --web-port=13020 Makefile 기준 실행은 `make core-run`과 `make client-run-web OTO_SERVER_HTTP_URL=http://toki-labs.com:18020`을 사용합니다. bootstrap command와 artifact URL 문서는 public validation 기준과 source of truth만 남기고, enrollment token이나 private release host는 런타임 환경에서만 주입합니다. +### Runner bootstrap asset matrix + +Control Plane bootstrap은 `OTO_RUNNER_RELEASE_BASE_URL` 아래의 OS/arch별 `oto` binary asset을 내려받는 것을 기준으로 합니다. +Linux와 macOS는 `tar.gz`, Windows는 `zip` archive를 사용합니다. + +| OS | Arch | Release asset | +|----|------|---------------| +| Linux | x64 | `oto-linux-x64.tar.gz` | +| Linux | arm64 | `oto-linux-arm64.tar.gz` | +| macOS | x64 | `oto-macos-x64.tar.gz` | +| macOS | arm64 | `oto-macos-arm64.tar.gz` | +| Windows | x64 | `oto-windows-x64.zip` | +| Windows | arm64 | `oto-windows-arm64.zip` | + +Bootstrap command 예시는 `--server-url`, `--agent-id`, `--enrollment-token`, `--release-base-url` 인자를 기준으로 하며, `--release-base-url`은 `https://` URL만 허용합니다. +Tracked 문서와 테스트 규칙에는 enrollment token, credential, private release host 원문을 기록하지 않습니다. + ### 의존성 설치 (Setup) 각 앱/서비스의 패키지 및 모듈 의존성을 설치합니다. diff --git a/agent-roadmap/phase/control-plane-product-surface/PHASE.md b/agent-roadmap/phase/control-plane-product-surface/PHASE.md index e8a7bbf..e6bd05a 100644 --- a/agent-roadmap/phase/control-plane-product-surface/PHASE.md +++ b/agent-roadmap/phase/control-plane-product-surface/PHASE.md @@ -7,7 +7,7 @@ ## 목표 현재 분리된 OTO Core API 서버, Flutter client, runner bootstrap 경계를 실제 사용자가 조작할 수 있는 Control Plane 제품 표면으로 끌어올린다. -먼저 Flutter UI 스켈레톤을 만들어 Control Plane의 주요 화면과 연결 지점을 고정하고, 이어서 OTO Core bootstrap 흐름이 Linux, macOS, Windows 대상 runner 설치를 지원하도록 확장한다. +먼저 OTO Core bootstrap 흐름이 Linux, macOS, Windows 대상 `oto` binary 설치와 runner/node 연결까지 이어지도록 고정하고, 그 실제 연결 상태를 바탕으로 Flutter UI 스켈레톤을 만든다. ## Milestone 흐름 @@ -15,12 +15,12 @@ 완료, 검토중, 진행중, 계획, 스케치 순서로 두어 아래로 갈수록 미래 작업에 가까워지게 정렬한다. 스케치 Milestone은 아직 구현 가능한 계획이 아니므로 계획 Milestone보다 아래에 둔다. +- [진행중] Runner bootstrap, OTO binary, 노드 연결 + - 경로: `agent-roadmap/phase/control-plane-product-surface/milestones/cross-os-runner-bootstrap.md` + - 요약: OTO Core bootstrap command와 bootstrap asset이 Linux, macOS, Windows에서 `oto` binary를 내려받고 runner/node로 Core에 연결할 수 있도록 확장한다. - [계획] Flutter 콘솔 스켈레톤 - 경로: `agent-roadmap/phase/control-plane-product-surface/milestones/flutter-console-skeleton.md` - - 요약: `apps/client`와 `packages/flutter/oto_console`에 Control Plane 주요 화면, 상태 표시, 빈 상태, Core 연결 기준을 갖춘 UI 스켈레톤을 만든다. -- [계획] OS별 runner bootstrap 설치 지원 - - 경로: `agent-roadmap/phase/control-plane-product-surface/milestones/cross-os-runner-bootstrap.md` - - 요약: OTO Core bootstrap command와 bootstrap asset이 Linux, macOS, Windows에서 OTO runner를 내려받고 설정/실행할 수 있도록 확장한다. + - 요약: bootstrap과 runner/node 연결 기준을 바탕으로 `apps/client`와 `packages/flutter/oto_console`에 Control Plane 주요 화면, 상태 표시, 빈 상태를 갖춘 UI 스켈레톤을 만든다. ## Phase 경계 diff --git a/agent-roadmap/phase/control-plane-product-surface/milestones/cross-os-runner-bootstrap.md b/agent-roadmap/phase/control-plane-product-surface/milestones/cross-os-runner-bootstrap.md index e716b16..9d0dc5c 100644 --- a/agent-roadmap/phase/control-plane-product-surface/milestones/cross-os-runner-bootstrap.md +++ b/agent-roadmap/phase/control-plane-product-surface/milestones/cross-os-runner-bootstrap.md @@ -1,4 +1,4 @@ -# Milestone: OS별 runner bootstrap 설치 지원 +# Milestone: Runner bootstrap, OTO binary, 노드 연결 ## 위치 @@ -7,12 +7,12 @@ ## 목표 -OTO Core가 발급하는 bootstrap 흐름을 Linux 단일 shell script 기준에서 확장해 Linux, macOS, Windows 대상 runner 설치를 지원한다. -완료 후 대상 머신은 OTO가 미리 설치되어 있지 않아도 OS별 bootstrap command로 release asset을 내려받고, agent 설정을 만들고, `oto agent run` 실행 준비 또는 시작까지 진행할 수 있어야 한다. +OTO Core가 발급하는 bootstrap 흐름을 Linux 단일 shell script 기준에서 확장해 Linux, macOS, Windows 대상 `oto` binary 설치와 runner/node 연결을 지원한다. +완료 후 대상 머신은 OTO가 미리 설치되어 있지 않아도 OS별 bootstrap command로 release asset을 내려받고, agent 설정을 만들고, `oto agent run`으로 OTO Core에 runner/node로 등록 또는 연결할 준비까지 진행할 수 있어야 한다. ## 상태 -[계획] +[진행중] ## 승격 조건 @@ -27,7 +27,8 @@ OTO Core가 발급하는 bootstrap 흐름을 Linux 단일 shell script 기준에 - OTO Core bootstrap command/API와 embedded bootstrap asset 제공 경로를 OS별로 확장한다. - Linux와 macOS는 POSIX shell 기반 설치 흐름을 우선 재사용하고, Windows는 PowerShell 기반 설치 흐름을 둔다. -- release asset 이름, OS/arch 감지, 설치 경로, config 경로, log/workspace 경로를 OS별 표준선으로 정리한다. +- release asset 이름, OS/arch 감지, `oto` binary 설치 경로, config 경로, log/workspace 경로를 OS별 표준선으로 정리한다. +- 설치된 `oto` binary가 생성된 agent 설정을 사용해 OTO Core runner registry에 runner/node로 연결되는 최소 계약을 고정한다. - bootstrap script와 server handler 테스트를 OS별 케이스로 확장한다. ## 기능 @@ -36,11 +37,13 @@ OTO Core가 발급하는 bootstrap 흐름을 Linux 단일 shell script 기준에 OS별 bootstrap command와 release asset 선택 기준을 OTO Core와 runner asset이 같은 의미로 해석하도록 만든다. -- [ ] [platform-matrix] Linux, macOS, Windows의 우선 지원 OS/arch와 release asset naming 규칙을 정리한다. 검증: 문서와 테스트 fixture가 동일한 asset 이름을 사용한다. +- [x] [platform-matrix] Linux, macOS, Windows의 우선 지원 OS/arch와 release asset naming 규칙을 정리한다. 검증: 문서와 테스트 fixture가 동일한 asset 이름을 사용한다. - [ ] [core-command] OTO Core bootstrap command 요청/응답이 대상 OS 또는 script 종류를 표현하고 올바른 bootstrap URL/command를 반환한다. 검증: `cd services/core && go test ./...`가 OS별 command 케이스를 포함해 통과한다. - [ ] [unix-bootstrap] Linux/macOS shell bootstrap이 OS/arch를 감지해 올바른 release asset을 내려받고 config/runtime 경로를 생성한다. 검증: `cd apps/runner && dart test test/oto_agent_bootstrap_script_test.dart` 또는 대응 smoke가 통과한다. - [ ] [windows-bootstrap] Windows PowerShell bootstrap이 release asset 다운로드, 압축 해제, config 생성, agent 실행 안내 또는 시작 흐름을 제공한다. 검증: script parsing/fixture 테스트가 Windows 경로와 quoting을 검증한다. -- [ ] [docs-smoke] README와 `agent-test/local/agent-smoke.md`가 OS별 bootstrap command, secret 기록 금지, release URL 검증 기준을 최신화한다. +- [ ] [oto-binary] bootstrap으로 설치된 `oto` binary의 실행 경로와 버전 확인 방법을 OS별로 고정한다. 검증: fixture 또는 smoke가 설치 후 `oto --version` 또는 동등한 binary 확인 절차를 검증한다. +- [ ] [node-connect] bootstrap으로 생성된 agent 설정이 `oto agent run`에서 OTO Core runner registry에 runner/node로 등록 또는 연결되는 최소 흐름을 보장한다. 검증: `cd services/core && go test ./...`와 runner agent 연결 smoke 또는 fixture 테스트가 통과한다. +- [x] [docs-smoke] README와 `agent-test/local/agent-smoke.md`가 OS별 bootstrap command, secret 기록 금지, release URL 검증 기준을 최신화한다. ## 완료 리뷰 @@ -60,9 +63,9 @@ OS별 bootstrap command와 release asset 선택 기준을 OTO Core와 runner ass ## 작업 컨텍스트 -- 관련 경로: `services/core/internal/httpserver/**`, `apps/runner/assets/script/**`, `apps/runner/test/**`, `README.md`, `agent-test/local/**` +- 관련 경로: `services/core/internal/httpserver/**`, `services/core/internal/runnerregistry/**`, `apps/runner/lib/oto/agent/**`, `apps/runner/assets/script/**`, `apps/runner/test/**`, `README.md`, `agent-test/local/**` - 표준선(선택): 우선 지원 OS는 Linux, macOS, Windows로 보고, 각 OS의 x64/arm64 지원 여부는 release asset 존재와 테스트 fixture로 명시한다. - 표준선(선택): Linux/macOS는 shell, Windows는 PowerShell을 사용하며, 공통 설정 의미는 `agent.id`, `agent.alias`, `agent.enrollment_token`, `server.url`, `runtime.install_dir`, `runtime.workspace_root`, `runtime.log_dir`를 유지한다. - 선행 작업: `독립 Control Plane 분리 마이그레이션`의 bootstrap command, `워크스페이스 포트/환경 표준화`의 release URL 기준, 폐기된 `Edge bootstrap 계약`의 Linux 설치 근거 -- 후속 작업: release packaging 자동화, checksum/signature 검증, OS별 service manager 통합 +- 후속 작업: Flutter 콘솔 스켈레톤, release packaging 자동화, checksum/signature 검증, OS별 service manager 통합 - 확인 필요: 없음 diff --git a/agent-roadmap/phase/control-plane-product-surface/milestones/flutter-console-skeleton.md b/agent-roadmap/phase/control-plane-product-surface/milestones/flutter-console-skeleton.md index c52b7ce..d930ed4 100644 --- a/agent-roadmap/phase/control-plane-product-surface/milestones/flutter-console-skeleton.md +++ b/agent-roadmap/phase/control-plane-product-surface/milestones/flutter-console-skeleton.md @@ -8,7 +8,7 @@ ## 목표 현재 endpoint 표시와 placeholder 중심인 Flutter client를 Control Plane 작업 표면의 스켈레톤으로 확장한다. -완료 후 사용자는 OTO Core 연결 상태, runner, pipeline/job, execution, log/artifact, settings 영역의 기본 화면 구조와 빈 상태를 확인할 수 있고, 후속 API 연동을 넣을 위치가 명확해야 한다. +완료 후 사용자는 OTO Core 연결 상태와 선행 bootstrap 흐름에서 정의된 runner/node 연결 상태를 바탕으로 runner, pipeline/job, execution, log/artifact, settings 영역의 기본 화면 구조와 빈 상태를 확인할 수 있고, 후속 API 연동을 넣을 위치가 명확해야 한다. ## 상태 @@ -28,6 +28,7 @@ - `apps/client` host app과 `packages/flutter/oto_console` 패키지의 현재 shell/contract 경계를 유지한다. - Overview, Runners, Pipelines/Jobs, Executions, Artifacts, Settings 화면의 스켈레톤과 상태 표현을 만든다. - OTO Core HTTP URL 설정값을 기준으로 health/readiness 연결 상태를 표시할 수 있는 최소 adapter 또는 port를 둔다. +- Runners 화면의 상태 모델은 선행 `Runner bootstrap, OTO binary, 노드 연결` Milestone에서 고정한 runner/node registration과 connectivity 기준을 따른다. - 실제 API 데이터가 없거나 비어 있을 때의 empty/loading/error 상태를 제품 표면에 맞게 정리한다. ## 기능 @@ -62,6 +63,6 @@ OTO Console이 placeholder가 아니라 후속 기능을 얹을 수 있는 제 - 관련 경로: `apps/client/**`, `packages/flutter/oto_console/**`, `services/core/internal/httpserver/**`, `agent-test/local/**` - 표준선(선택): 현재 `OtoConsoleShell`과 `OtoConsoleConfig`를 유지하고, 화면별 구현은 `oto_console` 패키지 안에 둔다. - 표준선(선택): 현재 Core는 CORS 헤더를 제공하지 않으므로 브라우저 직접 API 호출을 붙일 때 CORS middleware 또는 same-origin proxy 정책을 함께 판단한다. -- 선행 작업: `oto_console 계약 경계 정리`, `워크스페이스 포트/환경 표준화` +- 선행 작업: `Runner bootstrap, OTO binary, 노드 연결`, `oto_console 계약 경계 정리`, `워크스페이스 포트/환경 표준화` - 후속 작업: 실제 runner/job/execution API 데이터 바인딩, 인증/권한, 운영 대시보드 고도화 - 확인 필요: 없음 diff --git a/agent-task/m-cross-os-runner-bootstrap/01_core_command/CODE_REVIEW-cloud-G07.md b/agent-task/m-cross-os-runner-bootstrap/01_core_command/CODE_REVIEW-cloud-G07.md new file mode 100644 index 0000000..c7ad441 --- /dev/null +++ b/agent-task/m-cross-os-runner-bootstrap/01_core_command/CODE_REVIEW-cloud-G07.md @@ -0,0 +1,153 @@ + + +# Code Review Reference - REVIEW_API + +> **[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. +> If implementation is blocked by a user-only decision, user-owned external environment prerequisite, or scope conflict, fill `사용자 리뷰 요청` with evidence and stop with active files in place; code-review decides whether to write `USER_REVIEW.md`. Evidence gaps that a follow-up agent can close by rerunning commands or collecting artifacts are normal follow-up issues, not user-review blockers by themselves. +> Do not ask the user directly, present choices in chat, or call `request_user_input` during implementation; record the needed decision in `사용자 리뷰 요청` and stop for code-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-06-15 +task=m-cross-os-runner-bootstrap/01_core_command, plan=1, tag=REVIEW_API + +## Roadmap Targets + +- Milestone: `agent-roadmap/phase/control-plane-product-surface/milestones/cross-os-runner-bootstrap.md` +- Epic: `[bootstrap-platforms] Cross-OS bootstrap contract` +- Task id: `[core-command]` +- Completion mode: check-on-pass + +## 이 파일을 읽는 리뷰 에이전트에게 + +> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다. + +각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요. +리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다. + +1. 판정을 append한다. +2. `CODE_REVIEW-{review_lane}-GNN.md` -> `code_review_{review_lane}_GNN_N.log`, `PLAN-{build_lane}-GNN.md` -> `plan_{build_lane}_GNN_M.log`로 아카이브한다. +3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/{task_name}/`로 이동한다. WARN/FAIL이면 user-review gate를 확인한 뒤 다음 active plan/review 파일 또는 `USER_REVIEW.md`를 작성한다. +4. PASS이고 task group이 `m-`이면 완료 이벤트 메타데이터를 보고한다. roadmap 수정이나 `update-roadmap` 직접 호출은 하지 않는다. +5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다. + +--- + +## 구현 항목별 완료 여부 + +| 항목 | 완료 여부 | +|------|---------| +| [REVIEW_API-1] Windows command가 script 인자를 실제 invocation에 전달하도록 수정 | [x] | + +## 구현 체크리스트 + +- [x] Windows bootstrap command가 다운로드한 PowerShell script를 scriptblock으로 실행하고 `--server-url`, `--agent-id`, `--enrollment-token`, `--release-base-url`을 같은 `-Command` 표현식 안에서 전달하도록 수정한다. +- [x] PowerShell escaping helper/command template이 single quote, semicolon, `$()`가 포함된 runner_id/token을 실행하지 않고 인자로 보존하도록 테스트를 갱신한다. +- [x] 기존 no-target/Linux/macOS shell command와 invalid target 400/token 비노출 계약이 유지되는지 확인한다. +- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. + +## 코드리뷰 전용 체크리스트 + +> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다. +> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다. + +- [ ] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다. +- [ ] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다. +- [ ] active `CODE_REVIEW-*-G??.md`를 `code_review_{review_lane}_GNN_N.log`로 아카이브한다. +- [ ] active `PLAN-*-G??.md`를 `plan_{build_lane}_GNN_M.log`로 아카이브한다. +- [ ] `.gitignore`의 Agent-Ops 관리 block이 `agent-task/**/*.md`와 `agent-task/**/*.log`를 unignore하고 `agent-roadmap/current.md`를 ignore하는지 확인한다. +- [ ] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다. +- [ ] PASS이면 active task 디렉터리 `agent-task/{task_name}/`를 `agent-task/archive/YYYY/MM/{task_name}/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다. +- [ ] PASS이고 task group이 `m-`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다. +- [ ] PASS split 작업이면 이동 후 빈 active parent `agent-task/{task_group}/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다. +- [ ] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-{build_lane}-GNN.md`와 `CODE_REVIEW-{review_lane}-GNN.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`를 남기지 않는다. +- [ ] USER_REVIEW가 사용자 결정으로 완료/PASS 해소되면 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log`를 작성한 뒤 task directory를 archive로 이동한다. + +## 계획 대비 변경 사항 + +- 없음. 계획에 제시된 해결 방식에 따라 Windows PowerShell bootstrap command 템플릿의 인자 전달 방식을 scriptblock 표현식 내부(`-Command` 큰따옴표 안)로 수정하여 성공적으로 구현하였습니다. + +## 주요 설계 결정 + +- Windows PowerShell target에 대해 다운로드된 script가 필수 bootstrap 인자를 parameter binding 또는 `$args`로 즉시 전달받을 수 있도록 `& ([scriptblock]::Create((irm ))) -- --server-url ...` 템플릿 구조를 설계하였습니다. +- `powershellEscape` 헬퍼가 single quote를 중복(`''`) 처리함으로써 `$()` 변수 치환이나 세미콜론 `;` 등의 PowerShell 메타 캐릭터가 단일 인용부호 내부에서 실행되지 않고 literal argument로 스크립트에 고스란히 전달되도록 이스케이프 안전성을 보장하였습니다. + +## 사용자 리뷰 요청 + +_기본값은 `없음`이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 변경 없이는 안전하게 진행할 수 없으면 아래 항목을 실제 내용으로 교체하고, 구현을 중단한 뒤 active 파일을 그대로 둔 채 리뷰를 요청한다. 구현 에이전트는 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 증거 공백만으로는 사용자 리뷰 요청을 작성하지 않는다._ + +- 상태: 없음 +- 사유 유형: 없음 +- 결정 필요: 없음 +- 차단 근거: 없음 +- 실행한 검증/명령: 없음 +- 자동 후속 불가 이유: 없음 +- 재개 조건: 없음 + +## 리뷰어를 위한 체크포인트 + +- Windows command가 다운로드 script 실행과 bootstrap 인자 전달을 같은 PowerShell `-Command` 표현식 안에서 수행하는지 확인한다. +- runner id/token escaping이 PowerShell code injection으로 바뀌지 않는지 확인한다. +- 기존 Linux/macOS/default target 계약이 변하지 않았는지 확인한다. + +## 검증 결과 + +_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._ + +필수 규칙: +- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다. +- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다. +- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다. +- 사용자 리뷰 요청으로 명령을 끝까지 실행하지 못했다면 `사용자 리뷰 요청`에 실행한 명령, 실제 출력, 미실행 명령의 사유를 기록한다. + +### REVIEW_API-1 중간 검증 + +```text +$ cd services/core && go test ./... +? github.com/toki/oto/services/core/cmd/oto-core [no test files] +ok github.com/toki/oto/services/core/internal/cicdstate (cached) +ok github.com/toki/oto/services/core/internal/httpserver 0.061s +ok github.com/toki/oto/services/core/internal/runnerregistry (cached) +? github.com/toki/oto/services/core/oto [no test files] +``` + +### 최종 검증 + +```text +$ cd services/core && go test ./... +? github.com/toki/oto/services/core/cmd/oto-core [no test files] +ok github.com/toki/oto/services/core/internal/cicdstate (cached) +ok github.com/toki/oto/services/core/internal/httpserver 0.061s +ok github.com/toki/oto/services/core/internal/runnerregistry (cached) +? github.com/toki/oto/services/core/oto [no test files] + +$ cd apps/runner && dart analyze +Analyzing runner... +No issues found! +``` + +--- + +> **[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. + +## 섹션 소유권 + +| 섹션 | 소유자 | 설명 | +|------|--------|------| +| 헤더 주석, 개요(date/task/plan/tag), 리뷰 에이전트 지시 | 스텁 생성 시 고정 | 구현 에이전트가 수정하거나 실행하지 않음 | +| Roadmap Targets | follow-up plan에서 복사해 스텁 생성 시 고정 | 구현 에이전트가 수정하지 않음; PASS 시 code-review가 `complete.log`의 `Roadmap Completion`으로 복사 | +| 구현 항목별 완료 여부 | 구현 에이전트 | `[ ]` -> `[x]` 체크 | +| 구현 체크리스트 | 구현 에이전트 | `[ ]` -> `[x]` 체크 | +| 코드리뷰 전용 체크리스트 | 리뷰 에이전트 | 구현 에이전트가 수정하지 않음 | +| 계획 대비 변경 사항, 주요 설계 결정 | 구현 에이전트 | placeholder를 실제 내용으로 교체 | +| 사용자 리뷰 요청 | 구현 에이전트 | 필요 없으면 `상태: 없음` 유지 | +| 검증 결과 | 구현 에이전트 | 실행 출력 기록 | +| 코드리뷰 결과 | 리뷰 에이전트 | 스텁에는 포함하지 않음 | diff --git a/agent-task/m-cross-os-runner-bootstrap/01_core_command/PLAN-cloud-G07.md b/agent-task/m-cross-os-runner-bootstrap/01_core_command/PLAN-cloud-G07.md new file mode 100644 index 0000000..31a8a46 --- /dev/null +++ b/agent-task/m-cross-os-runner-bootstrap/01_core_command/PLAN-cloud-G07.md @@ -0,0 +1,66 @@ + + +# PLAN: Windows PowerShell bootstrap command 인자 전달 수정 + +## 이 파일을 읽는 구현 에이전트에게 + +이 계획은 직전 코드리뷰 `code_review_cloud_G06_0.log`의 Required 이슈만 해결한다. 구현 중 사용자 결정이 필요하면 채팅으로 질문하지 말고 동반 `CODE_REVIEW-cloud-G07.md`의 `사용자 리뷰 요청` 섹션을 실제 내용으로 채운 뒤 중단한다. + +## Roadmap Targets + +- Milestone: `agent-roadmap/phase/control-plane-product-surface/milestones/cross-os-runner-bootstrap.md` +- Epic: `[bootstrap-platforms] Cross-OS bootstrap contract` +- Task id: `[core-command]` +- Completion mode: check-on-pass + +## 실패 원인 + +`services/core/internal/httpserver/runner_handlers.go`의 Windows command는 `powershell -ExecutionPolicy Bypass -Command "irm ... | iex" -- --server-url ...` 형태다. 이 형태는 다운로드한 script를 실행하더라도 `--server-url`, `--agent-id`, `--enrollment-token`, `--release-base-url` 인자를 script invocation에 넘기는 구조가 아니다. Windows bootstrap command 계약은 script URL과 필수 bootstrap 인자를 함께 전달해야 한다. + +## 범위 결정 근거 + +이번 후속은 Core command template과 해당 Go 테스트만 수정한다. PowerShell bootstrap script asset 자체, `/bootstrap/oto-agent.ps1` serving route, OS별 binary install 구현은 후속 split 작업 범위로 둔다. + +## 구현 체크리스트 + +- [ ] Windows bootstrap command가 다운로드한 PowerShell script를 scriptblock으로 실행하고 `--server-url`, `--agent-id`, `--enrollment-token`, `--release-base-url`을 같은 `-Command` 표현식 안에서 전달하도록 수정한다. +- [ ] PowerShell escaping helper/command template이 single quote, semicolon, `$()`가 포함된 runner_id/token을 실행하지 않고 인자로 보존하도록 테스트를 갱신한다. +- [ ] 기존 no-target/Linux/macOS shell command와 invalid target 400/token 비노출 계약이 유지되는지 확인한다. +- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. + +### [REVIEW_API-1] Windows command가 script 인자를 실제 invocation에 전달하도록 수정 + +문제: PowerShell command의 인자가 `-Command` 문자열 밖에 붙어 있어 다운로드된 script의 `$args` 또는 parameter binding으로 전달된다고 볼 수 없다. + +해결 방법: + +- `runner_handlers.go`의 Windows branch를 다운로드 script를 scriptblock으로 만든 뒤 그 scriptblock invocation에 bootstrap 인자를 붙이는 형태로 바꾼다. +- 예시는 `powershell -ExecutionPolicy Bypass -Command "& ([scriptblock]::Create((irm ))) -- --server-url --agent-id --enrollment-token --release-base-url "` 같은 구조다. 실제 문법은 테스트 가능하고 quoting이 안전한 형태로 고정한다. +- `powershellEscape`가 이 command template에서 안전한지 다시 점검하고, single quote를 포함한 token이 PowerShell 코드로 실행되지 않고 literal argument로 남도록 한다. +- `server_test.go`의 Windows expected command와 malicious field 케이스를 새 구조로 갱신한다. 테스트는 인자들이 `-Command` 표현식 내부의 scriptblock invocation 뒤에 위치하는지 드러나야 한다. + +수정 파일 및 체크리스트: + +- `services/core/internal/httpserver/runner_handlers.go` +- `services/core/internal/httpserver/server_test.go` + +테스트 작성: + +- Windows target expected command를 새 scriptblock invocation 구조로 갱신한다. +- malicious runner_id/token 케이스가 literal quoting을 보존하는지 유지한다. +- no-target, linux, macos, invalid target 케이스는 회귀로 유지한다. + +중간 검증: + +- `cd services/core && go test ./...` + +## 최종 검증 + +- `cd services/core && go test ./...` +- `cd apps/runner && dart analyze` + +## 리뷰 포인트 + +- Windows command가 다운로드 script 실행과 bootstrap 인자 전달을 같은 PowerShell `-Command` 표현식 안에서 수행하는지 확인한다. +- runner id/token escaping이 PowerShell code injection으로 바뀌지 않는지 확인한다. +- 기존 Linux/macOS/default target 계약이 변하지 않았는지 확인한다. diff --git a/agent-task/m-cross-os-runner-bootstrap/01_core_command/code_review_cloud_G06_0.log b/agent-task/m-cross-os-runner-bootstrap/01_core_command/code_review_cloud_G06_0.log new file mode 100644 index 0000000..379d171 --- /dev/null +++ b/agent-task/m-cross-os-runner-bootstrap/01_core_command/code_review_cloud_G06_0.log @@ -0,0 +1,105 @@ + + +# CODE REVIEW: bootstrap command contract 확장 + +## Roadmap Targets + +- Milestone: `agent-roadmap/phase/control-plane-product-surface/milestones/cross-os-runner-bootstrap.md` +- Epic: `[bootstrap-platforms] Cross-OS bootstrap contract` +- Task id: `[core-command]` +- Completion mode: check-on-pass + +## 사용자 리뷰 요청 + +_기본값은 `없음`이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 변경 없이는 안전하게 진행할 수 없으면 아래 항목을 실제 내용으로 교체하고, 구현을 중단한 뒤 active 파일을 그대로 둔 채 리뷰를 요청한다. 구현 에이전트는 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 증거 공백만으로는 사용자 리뷰 요청을 작성하지 않는다._ + +- 상태: 없음 +- 사유 유형: 없음 +- 결정 필요: 없음 +- 차단 근거: 없음 +- 실행한 검증/명령: 없음 +- 자동 후속 불가 이유: 없음 +- 재개 조건: 없음 + +## 구현 에이전트 소유 섹션 + +### 계획 대비 변경 사항 + +- 없음. 계획에 따라 충실히 구현 완료하였습니다. + +### 수정 파일 + +- `proto/oto/runner.proto` : `BootstrapCommandRequest`에 `target` string 필드 추가 +- `services/core/internal/httpserver/runner_handlers.go` : `powershellEscape` 추가, `handleRunnerBootstrapCommand`에서 target 파싱하여 linux/macos/windows(powershell) 및 invalid target 분기 처리 +- `services/core/internal/httpserver/server_test.go` : 9~13번 테스트 케이스 추가 (OS/script 분기 및 invalid target 400 에러 검증) +- `services/core/oto/runner.pb.go` : Generated Go protobuf 파일 갱신 +- `apps/runner/lib/oto/agent/oto/runner.pb.dart` : Generated Dart protobuf 파일 갱신 +- `apps/runner/lib/oto/agent/oto/runner.pbjson.dart` : Generated Dart protobuf JSON mapping 파일 갱신 + +### 검증 결과 + +1. **Protobuf 갱신 검증 (`make proto-go && make proto-dart`)**: + 성공적으로 Makefile을 통해 Go 및 Dart용 파일들을 자동 생성하였습니다. +2. **Go handler 및 테스트 실행 (`cd services/core && go test ./...`)**: + `TestHandleRunnerBootstrapCommand`를 포함한 모든 Go 테스트가 성공적으로 통과하였습니다. + ```text + ? github.com/toki/oto/services/core/cmd/oto-core [no test files] + ok github.com/toki/oto/services/core/internal/cicdstate (cached) + ok github.com/toki/oto/services/core/internal/httpserver 0.067s + ok github.com/toki/oto/services/core/internal/runnerregistry 0.003s + ? github.com/toki/oto/services/core/oto [no test files] + ``` +3. **Dart 정적 분석 검증 (`cd apps/runner && dart analyze`)**: + 아무런 경고나 분석 오류 없이 정상 컴파일 및 통과하였습니다. + ```text + Analyzing runner... + No issues found! + ``` + +### 구현 체크리스트 + +- [x] `BootstrapCommandRequest`에 대상 OS 또는 script 종류를 표현하는 필드를 추가하고 기존 요청의 기본 Linux shell 동작을 보존한다. +- [x] `handleRunnerBootstrapCommand`가 대상별 bootstrap URL과 command template을 반환하도록 테스트 주도 변경한다. +- [x] 지원하지 않는 OS/script 값은 명확한 400 응답으로 실패시키고 enrollment token을 로그/응답 에러에 노출하지 않는다. +- [x] generated proto는 `make proto-go`, `make proto-dart`로 갱신하고 수동 편집하지 않는다. +- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. + +## 코드 리뷰 전용 섹션 + +- [ ] Roadmap Targets와 구현 결과가 일치한다. +- [ ] 기존 Linux shell 기본 동작이 깨지지 않았다. +- [ ] invalid target과 secret 비노출 실패가 테스트로 확인된다. +- [ ] 구현 검증 명령 출력이 충분히 기록됐다. + +## 완료 판정 메모 + +| 항목 | 값 | +|------|----| +| Verdict | 미작성 | +| Roadmap Completion | `[core-command]` only | +| Archive ready | 미작성 | + +## 코드리뷰 결과 + +- 종합 판정: FAIL +- 차원별 평가: + - Correctness: Fail + - Completeness: Fail + - Test coverage: Fail + - API contract: Fail + - Code quality: Pass + - Plan deviation: Pass + - Verification trust: Pass +- 발견된 문제: + - Required: `services/core/internal/httpserver/runner_handlers.go:254` Windows command가 `powershell -ExecutionPolicy Bypass -Command "irm ... | iex" -- --server-url ...` 형태로 생성됩니다. `Invoke-Expression`이 실행하는 내용은 `-Command` 문자열인 `irm ... | iex`뿐이라, 뒤에 붙은 `--server-url`, `--agent-id`, `--enrollment-token`, `--release-base-url` 인자가 다운로드된 bootstrap script에 전달되지 않습니다. Windows PowerShell bootstrap command 계약은 script URL뿐 아니라 runner id/token/server URL을 script 호출에 넘겨야 하므로, scriptblock invocation 안에서 다운로드 script를 실행하고 인자도 같은 PowerShell command 표현식 안에 포함하도록 수정하세요. 예: `& ([scriptblock]::Create((irm ))) -- --server-url <...> ...` 같은 형태를 기준으로 escaping을 재검토하고, 테스트도 단순 문자열 일치가 아니라 인자 전달 구조가 `-Command` 내부에 들어가는지 검증해야 합니다. +- 다음 단계: FAIL 후속 plan/review를 작성한다. + +## 코드리뷰 전용 종결 체크리스트 + +- [x] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append했다. +- [x] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다. +- [x] active `CODE_REVIEW-cloud-G06.md`를 `code_review_cloud_G06_0.log`로 아카이브했다. +- [x] active `PLAN-cloud-G06.md`를 `plan_cloud_G06_0.log`로 아카이브했다. +- [x] `.gitignore`의 Agent-Ops 관리 block이 `agent-task/**/*.md`와 `agent-task/**/*.log`를 unignore하고 `agent-roadmap/current.md`를 ignore하는지 확인했다. +- [x] FAIL이고 user-review gate가 트리거되지 않았으므로 다음 active `PLAN-cloud-G07.md`와 `CODE_REVIEW-cloud-G07.md`를 작성하고 `complete.log`를 작성하지 않았다. +- [x] task group이 `m-cross-os-runner-bootstrap`이지만 PASS가 아니므로 roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않았다. diff --git a/agent-task/m-cross-os-runner-bootstrap/01_core_command/plan_cloud_G06_0.log b/agent-task/m-cross-os-runner-bootstrap/01_core_command/plan_cloud_G06_0.log new file mode 100644 index 0000000..b697c0e --- /dev/null +++ b/agent-task/m-cross-os-runner-bootstrap/01_core_command/plan_cloud_G06_0.log @@ -0,0 +1,117 @@ + + +# PLAN: bootstrap command contract 확장 + +## 이 파일을 읽는 구현 에이전트에게 + +이 계획은 현 마일스톤의 `bootstrap-platforms` 에픽 중 `[core-command]`만 완료 대상으로 삼는다. 구현 중 사용자 결정이 필요하면 채팅으로 질문하지 말고 동반 `CODE_REVIEW-cloud-G06.md`의 `사용자 리뷰 요청` 섹션을 실제 내용으로 채운 뒤 중단한다. + +## 배경 + +현재 Core는 `/api/v1/runners/bootstrap-command`에서 Linux shell command만 반환한다. 요청 protobuf도 `runner_id`, `enrollment_token`만 있어 대상 OS 또는 script 종류를 표현하지 못한다. 이 작업은 OS/script 선택 계약을 먼저 고정해 후속 Unix/Windows bootstrap 구현이 같은 API 위에서 진행되게 한다. + +## Roadmap Targets + +- Milestone: `agent-roadmap/phase/control-plane-product-surface/milestones/cross-os-runner-bootstrap.md` +- Epic: `[bootstrap-platforms] Cross-OS bootstrap contract` +- Task id: `[core-command]` +- Completion mode: check-on-pass + +## 분석 결과 + +### 읽은 파일 + +- `proto/oto/runner.proto` +- `services/core/internal/httpserver/runner_handlers.go` +- `services/core/internal/httpserver/routes.go` +- `services/core/internal/httpserver/bootstrap_provider.go` +- `services/core/internal/httpserver/server.go` +- `services/core/internal/httpserver/server_test.go` +- `services/core/oto/runner.pb.go` +- `apps/runner/lib/oto/agent/oto/runner.pb.dart` +- `apps/runner/lib/oto/agent/oto/runner.pbjson.dart` +- `apps/runner/test/oto_server_connection_smoke_test.dart` +- `agent-test/local/rules.md` +- `agent-test/local/agent-smoke.md` + +### 테스트 환경 규칙 + +기본 환경은 local이다. 원격 runner checkout은 `/Users/toki/agent-work/oto`이고 SSH는 확인됐지만, 이 계획 작성 시점의 변경분은 현재 workspace에 있으므로 구현 검증은 변경분이 있는 checkout에서 실행해야 한다. + +### 테스트 커버리지 공백 + +현재 Go 테스트는 기본 Linux shell command와 release URL validation은 다루지만 OS/script 선택 요청을 다루지 않는다. Dart smoke는 기존 필드 호환성 위주라 신규 proto 필드 기본값 호환 검증이 필요하다. + +### 심볼 참조 + +`BootstrapCommandRequest`, `BootstrapCommandResponse`, `handleRunnerBootstrapCommand`, `/api/v1/runners/bootstrap-command`가 직접 참조점이다. generated proto 파일은 수동 편집하지 말고 `make proto-go`, `make proto-dart`로 갱신한다. + +### 분할 판단 + +공유 task group은 `agent-task/m-cross-os-runner-bootstrap/`이다. 이 작업은 API/foundation이므로 `01_core_command`로 독립 실행한다. `03+01_windows_bootstrap`과 `05+01,02,03_node_connect`는 이 작업의 완료를 전제로 삼는다. + +### 범위 결정 근거 + +이 계획은 API 요청/응답과 Core handler/test까지만 다룬다. shell/PowerShell script 구현, binary version check, agent 연결 smoke는 후속 split에서 다룬다. + +### 빌드 등급 + +`cloud-G06`: protobuf 계약과 Go/Dart generated code를 포함하지만 변경 범위가 endpoint와 테스트에 한정되어 있다. + +## 구현 체크리스트 + +- [ ] `BootstrapCommandRequest`에 대상 OS 또는 script 종류를 표현하는 필드를 추가하고 기존 요청의 기본 Linux shell 동작을 보존한다. +- [ ] `handleRunnerBootstrapCommand`가 대상별 bootstrap URL과 command template을 반환하도록 테스트 주도 변경한다. +- [ ] 지원하지 않는 OS/script 값은 명확한 400 응답으로 실패시키고 enrollment token을 로그/응답 에러에 노출하지 않는다. +- [ ] generated proto는 `make proto-go`, `make proto-dart`로 갱신하고 수동 편집하지 않는다. +- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. + +### [API-1] bootstrap command 요청 계약 확장 + +문제: 현재 요청에는 OS/script 선택 필드가 없다. + +해결 방법: proto에 enum 또는 string 기반 target 필드를 추가하되 기존 필드 번호를 보존한다. 기본값은 현재 Linux shell command와 동일해야 한다. + +수정 파일 및 체크리스트: + +- `proto/oto/runner.proto` +- `services/core/oto/runner.pb.go` +- `apps/runner/lib/oto/agent/oto/runner.pb.dart` +- `apps/runner/lib/oto/agent/oto/runner.pbjson.dart` + +테스트 작성: generated code 갱신 후 기존 Dart smoke가 컴파일되는지 확인한다. + +중간 검증: `make proto-go`, `make proto-dart` + +### [API-2] Core handler OS/script 분기 + +문제: handler가 항상 `/bootstrap/oto-agent.sh`를 반환한다. + +해결 방법: request target에 따라 shell 또는 powershell URL과 command wrapper를 선택한다. Linux/macOS는 shell 경로를 공유할 수 있고 Windows는 PowerShell script URL을 반환할 계약만 먼저 고정한다. + +수정 파일 및 체크리스트: + +- `services/core/internal/httpserver/runner_handlers.go` +- `services/core/internal/httpserver/server_test.go` + +테스트 작성: Linux 기본값, macOS shell, Windows PowerShell, invalid target, HTTPS release URL validation 케이스를 추가한다. + +중간 검증: `cd services/core && go test ./...` + +## 수정 파일 요약 + +| 경로 | 목적 | +|------|------| +| `proto/oto/runner.proto` | bootstrap command 요청 계약 확장 | +| `services/core/internal/httpserver/runner_handlers.go` | OS/script별 command 반환 | +| `services/core/internal/httpserver/server_test.go` | command contract 회귀 테스트 | +| generated proto files | Go/Dart 컴파일 계약 갱신 | + +## 최종 검증 + +- `make proto-go` +- `make proto-dart` +- `cd services/core && go test ./...` +- `cd apps/runner && dart analyze` + +모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다. diff --git a/agent-task/m-cross-os-runner-bootstrap/02_unix_bootstrap/CODE_REVIEW-cloud-G07.md b/agent-task/m-cross-os-runner-bootstrap/02_unix_bootstrap/CODE_REVIEW-cloud-G07.md new file mode 100644 index 0000000..65b3ef8 --- /dev/null +++ b/agent-task/m-cross-os-runner-bootstrap/02_unix_bootstrap/CODE_REVIEW-cloud-G07.md @@ -0,0 +1,59 @@ + + +# CODE REVIEW: Linux/macOS shell bootstrap 확장 + +## Roadmap Targets + +- Milestone: `agent-roadmap/phase/control-plane-product-surface/milestones/cross-os-runner-bootstrap.md` +- Epic: `[bootstrap-platforms] Cross-OS bootstrap contract` +- Task id: `[unix-bootstrap]` +- Completion mode: check-on-pass + +## 사용자 리뷰 요청 + +_기본값은 `없음`이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 변경 없이는 안전하게 진행할 수 없으면 아래 항목을 실제 내용으로 교체하고, 구현을 중단한 뒤 active 파일을 그대로 둔 채 리뷰를 요청한다. 구현 에이전트는 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 증거 공백만으로는 사용자 리뷰 요청을 작성하지 않는다._ + +- 상태: 없음 +- 사유 유형: 없음 +- 결정 필요: 없음 +- 차단 근거: 없음 +- 실행한 검증/명령: 없음 +- 자동 후속 불가 이유: 없음 +- 재개 조건: 없음 + +## 구현 에이전트 소유 섹션 + +### 계획 대비 변경 사항 + +- 미작성 + +### 수정 파일 + +- 미작성 + +### 검증 결과 + +- 미작성 + +### 구현 체크리스트 + +- [ ] shell script가 `uname -s`와 `uname -m` 조합으로 Linux/macOS x64/arm64 asset을 선택한다. +- [ ] macOS 기본 install/config/workspace/log 경로가 기존 설정 의미를 보존한다. +- [ ] runner asset과 Core embedded script copy를 동기화한다. +- [ ] script fixture 테스트가 Linux/macOS asset matrix와 unsupported OS/arch 실패를 검증한다. +- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. + +## 코드 리뷰 전용 섹션 + +- [ ] Linux 기존 실행 fixture가 계속 통과한다. +- [ ] macOS asset 선택이 테스트로 고정됐다. +- [ ] embedded script copy drift가 없다. +- [ ] secret/token이 실패 출력에 노출되지 않는다. + +## 완료 판정 메모 + +| 항목 | 값 | +|------|----| +| Verdict | 미작성 | +| Roadmap Completion | `[unix-bootstrap]` only | +| Archive ready | 미작성 | diff --git a/agent-task/m-cross-os-runner-bootstrap/02_unix_bootstrap/PLAN-cloud-G07.md b/agent-task/m-cross-os-runner-bootstrap/02_unix_bootstrap/PLAN-cloud-G07.md new file mode 100644 index 0000000..c2737ad --- /dev/null +++ b/agent-task/m-cross-os-runner-bootstrap/02_unix_bootstrap/PLAN-cloud-G07.md @@ -0,0 +1,110 @@ + + +# PLAN: Linux/macOS shell bootstrap 확장 + +## 이 파일을 읽는 구현 에이전트에게 + +이 계획은 `[unix-bootstrap]` 완료만 목표로 한다. Windows PowerShell, Core API schema 변경, runner registry 연결 smoke는 다른 split의 책임이다. + +## 배경 + +현재 shell bootstrap은 Linux arch만 감지해 `oto-linux-x64.tar.gz` 또는 `oto-linux-arm64.tar.gz`를 내려받는다. README와 테스트 fixture에는 macOS asset 이름까지 고정됐으므로 shell script가 OS/arch를 함께 감지해야 한다. + +## Roadmap Targets + +- Milestone: `agent-roadmap/phase/control-plane-product-surface/milestones/cross-os-runner-bootstrap.md` +- Epic: `[bootstrap-platforms] Cross-OS bootstrap contract` +- Task id: `[unix-bootstrap]` +- Completion mode: check-on-pass + +## 분석 결과 + +### 읽은 파일 + +- `apps/runner/assets/script/shell/oto_agent_bootstrap.sh` +- `services/core/internal/httpserver/oto_agent_bootstrap.sh` +- `services/core/internal/httpserver/bootstrap_provider.go` +- `services/core/internal/httpserver/server_test.go` +- `apps/runner/test/oto_agent_bootstrap_script_test.dart` +- `README.md` +- `agent-test/local/rules.md` +- `agent-test/local/agent-smoke.md` + +### 테스트 환경 규칙 + +기본 환경은 local이며 agent smoke는 runner bootstrap script 변경 후 `cd apps/runner && dart test test/oto_agent_bootstrap_script_test.dart`와 Core embedded copy 검증을 요구한다. + +### 테스트 커버리지 공백 + +현재 script 실행 테스트는 Linux archive fixture만 실제 실행한다. macOS는 이 workspace에서 실행할 수 없으므로 script text/fixture 기반 검증으로 asset 선택 분기와 경로 생성 계약을 확인해야 한다. + +### 심볼 참조 + +`oto_agent_bootstrap.sh`는 runner asset과 Core embedded copy가 byte-identical이어야 한다. `TestEmbeddedBootstrapScriptMatchesRunnerAsset`가 이 계약을 검증한다. + +### 분할 판단 + +이 작업은 `01_core_command` 없이도 shell asset 자체를 확장할 수 있다. `04+02,03_binary_contract`는 이 작업 완료 후 Unix binary check를 보강한다. + +### 범위 결정 근거 + +Linux/macOS POSIX shell 흐름만 수정한다. Windows zip 압축 해제와 PowerShell quoting은 `03+01_windows_bootstrap`에서 다룬다. + +### 빌드 등급 + +`cloud-G07`: shell process 흐름, OS command output, archive extraction 계약이 핵심이므로 terminal-agent 판단과 꼼꼼한 테스트가 필요하다. + +## 구현 체크리스트 + +- [ ] shell script가 `uname -s`와 `uname -m` 조합으로 Linux/macOS x64/arm64 asset을 선택한다. +- [ ] macOS 기본 install/config/workspace/log 경로가 기존 설정 의미를 보존한다. +- [ ] runner asset과 Core embedded script copy를 동기화한다. +- [ ] script fixture 테스트가 Linux/macOS asset matrix와 unsupported OS/arch 실패를 검증한다. +- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. + +### [BOOTSTRAP_UNIX-1] OS/arch asset 선택 + +문제: script가 Linux만 지원하고 macOS asset을 선택하지 않는다. + +해결 방법: `uname -s` 결과를 정규화해 `linux` 또는 `macos`로 매핑하고, arch는 x64/arm64로 매핑한다. unsupported 조합은 설치 전 명확히 실패한다. + +수정 파일 및 체크리스트: + +- `apps/runner/assets/script/shell/oto_agent_bootstrap.sh` +- `services/core/internal/httpserver/oto_agent_bootstrap.sh` +- `apps/runner/test/oto_agent_bootstrap_script_test.dart` + +테스트 작성: script text에 네 asset이 모두 포함되고 unsupported OS/arch 메시지가 token 없이 실패하는지 검증한다. + +중간 검증: `cd apps/runner && dart test test/oto_agent_bootstrap_script_test.dart` + +### [BOOTSTRAP_UNIX-2] embedded copy 동기화 + +문제: Core는 embedded shell copy를 따로 들고 있어 drift가 생길 수 있다. + +해결 방법: runner asset 변경 후 Core embedded copy를 동일 내용으로 갱신한다. + +수정 파일 및 체크리스트: + +- `services/core/internal/httpserver/oto_agent_bootstrap.sh` +- `services/core/internal/httpserver/server_test.go` + +테스트 작성: 기존 embedded copy 일치 테스트가 계속 통과해야 한다. + +중간 검증: `cd services/core && go test ./...` + +## 수정 파일 요약 + +| 경로 | 목적 | +|------|------| +| `apps/runner/assets/script/shell/oto_agent_bootstrap.sh` | Linux/macOS bootstrap 구현 | +| `services/core/internal/httpserver/oto_agent_bootstrap.sh` | Core embedded script 동기화 | +| `apps/runner/test/oto_agent_bootstrap_script_test.dart` | OS/arch fixture 검증 | +| `services/core/internal/httpserver/server_test.go` | embedded copy 회귀 검증 | + +## 최종 검증 + +- `cd apps/runner && dart test test/oto_agent_bootstrap_script_test.dart` +- `cd services/core && go test ./...` + +모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다. diff --git a/agent-task/m-cross-os-runner-bootstrap/03+01_windows_bootstrap/CODE_REVIEW-cloud-G07.md b/agent-task/m-cross-os-runner-bootstrap/03+01_windows_bootstrap/CODE_REVIEW-cloud-G07.md new file mode 100644 index 0000000..7e974b6 --- /dev/null +++ b/agent-task/m-cross-os-runner-bootstrap/03+01_windows_bootstrap/CODE_REVIEW-cloud-G07.md @@ -0,0 +1,59 @@ + + +# CODE REVIEW: Windows PowerShell bootstrap 추가 + +## Roadmap Targets + +- Milestone: `agent-roadmap/phase/control-plane-product-surface/milestones/cross-os-runner-bootstrap.md` +- Epic: `[bootstrap-platforms] Cross-OS bootstrap contract` +- Task id: `[windows-bootstrap]` +- Completion mode: check-on-pass + +## 사용자 리뷰 요청 + +_기본값은 `없음`이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 변경 없이는 안전하게 진행할 수 없으면 아래 항목을 실제 내용으로 교체하고, 구현을 중단한 뒤 active 파일을 그대로 둔 채 리뷰를 요청한다. 구현 에이전트는 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 증거 공백만으로는 사용자 리뷰 요청을 작성하지 않는다._ + +- 상태: 없음 +- 사유 유형: 없음 +- 결정 필요: 없음 +- 차단 근거: 없음 +- 실행한 검증/명령: 없음 +- 자동 후속 불가 이유: 없음 +- 재개 조건: 없음 + +## 구현 에이전트 소유 섹션 + +### 계획 대비 변경 사항 + +- 미작성 + +### 수정 파일 + +- 미작성 + +### 검증 결과 + +- 미작성 + +### 구현 체크리스트 + +- [ ] PowerShell bootstrap asset을 추가하고 Windows x64/arm64 zip asset 선택을 고정한다. +- [ ] Core가 `.ps1` bootstrap asset을 serve하고 Windows command에서 해당 URL을 사용한다. +- [ ] script/config 생성 시 기존 agent 설정 의미를 유지하고 enrollment token 출력 노출을 피한다. +- [ ] Windows route, content, command, quoting, zip asset 이름을 테스트한다. +- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. + +## 코드 리뷰 전용 섹션 + +- [ ] `01_core_command` 완료 근거가 있다. +- [ ] PowerShell quoting과 Windows path가 fixture로 검증된다. +- [ ] `.ps1` serving route와 command URL이 일치한다. +- [ ] token/credential 출력 노출이 없다. + +## 완료 판정 메모 + +| 항목 | 값 | +|------|----| +| Verdict | 미작성 | +| Roadmap Completion | `[windows-bootstrap]` only | +| Archive ready | 미작성 | diff --git a/agent-task/m-cross-os-runner-bootstrap/03+01_windows_bootstrap/PLAN-cloud-G07.md b/agent-task/m-cross-os-runner-bootstrap/03+01_windows_bootstrap/PLAN-cloud-G07.md new file mode 100644 index 0000000..a668a18 --- /dev/null +++ b/agent-task/m-cross-os-runner-bootstrap/03+01_windows_bootstrap/PLAN-cloud-G07.md @@ -0,0 +1,122 @@ + + +# PLAN: Windows PowerShell bootstrap 추가 + +## 이 파일을 읽는 구현 에이전트에게 + +이 계획은 `[windows-bootstrap]` 완료만 목표로 하며 `01_core_command` 완료 후 실행해야 한다. predecessor `01_core_command`의 `complete.log`가 없으면 구현을 시작하지 말고 대기한다. + +## 배경 + +마일스톤은 Windows PowerShell bootstrap을 요구하지만 현재 repository에는 shell bootstrap만 있다. Core가 Windows script URL과 command를 반환할 수 있어야 하고, PowerShell script는 zip asset 다운로드, 압축 해제, config 생성, 실행 안내 또는 시작 흐름을 제공해야 한다. + +## Roadmap Targets + +- Milestone: `agent-roadmap/phase/control-plane-product-surface/milestones/cross-os-runner-bootstrap.md` +- Epic: `[bootstrap-platforms] Cross-OS bootstrap contract` +- Task id: `[windows-bootstrap]` +- Completion mode: check-on-pass + +## 분석 결과 + +### 읽은 파일 + +- `services/core/internal/httpserver/routes.go` +- `services/core/internal/httpserver/bootstrap_provider.go` +- `services/core/internal/httpserver/runner_handlers.go` +- `services/core/internal/httpserver/server_test.go` +- `apps/runner/assets/script/shell/oto_agent_bootstrap.sh` +- `apps/runner/test/oto_agent_bootstrap_script_test.dart` +- `README.md` +- `agent-test/local/rules.md` +- `agent-test/local/agent-smoke.md` + +### 테스트 환경 규칙 + +기본 환경은 local이다. Windows 실제 실행 환경은 보장되지 않으므로 parser/text fixture와 Core handler 테스트를 우선 검증으로 둔다. + +### 테스트 커버리지 공백 + +PowerShell script는 아직 파일도 route도 없다. 실제 Windows에서 실행하는 integration smoke는 후속 환경 준비가 필요할 수 있으므로 이 작업은 quoting, zip asset, config key, command generation을 deterministic test로 고정한다. + +### 심볼 참조 + +`/bootstrap/oto-agent.sh`와 embedded shell provider가 현재 유일한 bootstrap asset serving 경로다. Windows에는 별도 `.ps1` route와 provider entry가 필요하다. + +### 분할 판단 + +이 작업은 `03+01_windows_bootstrap`이며 `01_core_command`의 대상 script 계약에 의존한다. Unix script 확장과는 병렬 가능하지만 `04+02,03_binary_contract`, `05+01,02,03_node_connect`의 전제다. + +### 범위 결정 근거 + +PowerShell bootstrap과 Core serving/command 연결만 구현한다. release packaging, checksum/signature, Windows service manager 등록은 범위 제외다. + +### 빌드 등급 + +`cloud-G07`: PowerShell quoting, process invocation, Windows path semantics가 중심이라 높은 주의가 필요하다. + +## 의존 관계 및 구현 순서 + +- 선행: `agent-task/m-cross-os-runner-bootstrap/01_core_command/complete.log` +- 병렬 가능: `02_unix_bootstrap` +- 후속: `04+02,03_binary_contract`, `05+01,02,03_node_connect` + +## 구현 체크리스트 + +- [ ] PowerShell bootstrap asset을 추가하고 Windows x64/arm64 zip asset 선택을 고정한다. +- [ ] Core가 `.ps1` bootstrap asset을 serve하고 Windows command에서 해당 URL을 사용한다. +- [ ] script/config 생성 시 기존 agent 설정 의미를 유지하고 enrollment token 출력 노출을 피한다. +- [ ] Windows route, content, command, quoting, zip asset 이름을 테스트한다. +- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. + +### [BOOTSTRAP_WINDOWS-1] PowerShell asset 추가 + +문제: Windows bootstrap script가 없다. + +해결 방법: `apps/runner/assets/script/powershell/oto_agent_bootstrap.ps1` 같은 명시적 asset을 추가한다. 새 파일은 기존 파일로 대체할 수 없으므로 생성이 정당하다. + +수정 파일 및 체크리스트: + +- `apps/runner/assets/script/powershell/oto_agent_bootstrap.ps1` +- `apps/runner/test/oto_agent_bootstrap_script_test.dart` 또는 별도 runner bootstrap test + +테스트 작성: x64/arm64 zip asset, HTTPS release URL, config key, token 비노출, quoting을 fixture로 검증한다. + +중간 검증: `cd apps/runner && dart test test/oto_agent_bootstrap_script_test.dart` + +### [BOOTSTRAP_WINDOWS-2] Core route와 command 연결 + +문제: Core는 shell script만 embed/serve한다. + +해결 방법: PowerShell embedded asset과 route를 추가하고 `01_core_command`의 Windows target이 해당 URL을 사용하도록 한다. + +수정 파일 및 체크리스트: + +- `services/core/internal/httpserver/bootstrap_provider.go` +- `services/core/internal/httpserver/routes.go` +- `services/core/internal/httpserver/runner_handlers.go` +- `services/core/internal/httpserver/server_test.go` +- `services/core/internal/httpserver/oto_agent_bootstrap.ps1` + +테스트 작성: `.ps1` route 응답, Windows command, invalid target regression을 검증한다. + +중간 검증: `cd services/core && go test ./...` + +## 수정 파일 요약 + +| 경로 | 목적 | +|------|------| +| `apps/runner/assets/script/powershell/oto_agent_bootstrap.ps1` | Windows bootstrap 구현 | +| `services/core/internal/httpserver/oto_agent_bootstrap.ps1` | Core embedded Windows asset | +| `services/core/internal/httpserver/bootstrap_provider.go` | bootstrap provider 확장 | +| `services/core/internal/httpserver/routes.go` | `.ps1` route 추가 | +| `services/core/internal/httpserver/runner_handlers.go` | Windows command 반환 | +| tests | PowerShell/route/command fixture 검증 | + +## 최종 검증 + +- `cd apps/runner && dart test test/oto_agent_bootstrap_script_test.dart` +- `cd services/core && go test ./...` +- `cd apps/runner && dart analyze` + +모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다. diff --git a/agent-task/m-cross-os-runner-bootstrap/04+02,03_binary_contract/CODE_REVIEW-local-G06.md b/agent-task/m-cross-os-runner-bootstrap/04+02,03_binary_contract/CODE_REVIEW-local-G06.md new file mode 100644 index 0000000..3165138 --- /dev/null +++ b/agent-task/m-cross-os-runner-bootstrap/04+02,03_binary_contract/CODE_REVIEW-local-G06.md @@ -0,0 +1,59 @@ + + +# CODE REVIEW: bootstrap 설치 binary 확인 계약 + +## Roadmap Targets + +- Milestone: `agent-roadmap/phase/control-plane-product-surface/milestones/cross-os-runner-bootstrap.md` +- Epic: `[bootstrap-platforms] Cross-OS bootstrap contract` +- Task id: `[oto-binary]` +- Completion mode: check-on-pass + +## 사용자 리뷰 요청 + +_기본값은 `없음`이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 변경 없이는 안전하게 진행할 수 없으면 아래 항목을 실제 내용으로 교체하고, 구현을 중단한 뒤 active 파일을 그대로 둔 채 리뷰를 요청한다. 구현 에이전트는 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 증거 공백만으로는 사용자 리뷰 요청을 작성하지 않는다._ + +- 상태: 없음 +- 사유 유형: 없음 +- 결정 필요: 없음 +- 차단 근거: 없음 +- 실행한 검증/명령: 없음 +- 자동 후속 불가 이유: 없음 +- 재개 조건: 없음 + +## 구현 에이전트 소유 섹션 + +### 계획 대비 변경 사항 + +- 미작성 + +### 수정 파일 + +- 미작성 + +### 검증 결과 + +- 미작성 + +### 구현 체크리스트 + +- [ ] Unix bootstrap이 설치된 `oto` binary 실행 경로를 확인하고 버전 또는 동등한 health command를 실행한다. +- [ ] Windows bootstrap이 설치된 `oto.exe` 실행 경로를 확인하고 버전 또는 동등한 health command를 실행한다. +- [ ] fake binary fixture가 성공/실패 케이스를 검증하고 실패 출력에 token을 노출하지 않는다. +- [ ] README 또는 agent-smoke의 binary 확인 기준이 바뀌면 함께 갱신한다. +- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. + +## 코드 리뷰 전용 섹션 + +- [ ] `02_unix_bootstrap`와 `03+01_windows_bootstrap` 완료 근거가 있다. +- [ ] version/health command 실패가 설치 성공으로 처리되지 않는다. +- [ ] fake binary test가 성공과 실패를 모두 검증한다. +- [ ] embedded script copy drift가 없다. + +## 완료 판정 메모 + +| 항목 | 값 | +|------|----| +| Verdict | 미작성 | +| Roadmap Completion | `[oto-binary]` only | +| Archive ready | 미작성 | diff --git a/agent-task/m-cross-os-runner-bootstrap/04+02,03_binary_contract/PLAN-local-G06.md b/agent-task/m-cross-os-runner-bootstrap/04+02,03_binary_contract/PLAN-local-G06.md new file mode 100644 index 0000000..56eeb99 --- /dev/null +++ b/agent-task/m-cross-os-runner-bootstrap/04+02,03_binary_contract/PLAN-local-G06.md @@ -0,0 +1,114 @@ + + +# PLAN: bootstrap 설치 binary 확인 계약 + +## 이 파일을 읽는 구현 에이전트에게 + +이 계획은 `[oto-binary]` 완료만 목표로 하며 `02_unix_bootstrap`, `03+01_windows_bootstrap` 완료 후 실행해야 한다. 두 predecessor의 `complete.log`가 없으면 구현을 시작하지 않는다. + +## 배경 + +마일스톤은 bootstrap으로 설치된 `oto` binary 실행 경로와 버전 확인 방법을 OS별로 고정해야 한다. Unix/Windows script가 완성된 뒤 각 script에 binary 확인 절차와 테스트 fixture를 추가하는 후속 작업이다. + +## Roadmap Targets + +- Milestone: `agent-roadmap/phase/control-plane-product-surface/milestones/cross-os-runner-bootstrap.md` +- Epic: `[bootstrap-platforms] Cross-OS bootstrap contract` +- Task id: `[oto-binary]` +- Completion mode: check-on-pass + +## 분석 결과 + +### 읽은 파일 + +- `apps/runner/assets/script/shell/oto_agent_bootstrap.sh` +- `apps/runner/test/oto_agent_bootstrap_script_test.dart` +- `services/core/internal/httpserver/oto_agent_bootstrap.sh` +- `services/core/internal/httpserver/server_test.go` +- `agent-test/local/rules.md` +- `agent-test/local/agent-smoke.md` + +### 테스트 환경 규칙 + +기본 환경은 local이다. binary 확인은 script fixture에서 fake `oto` 실행 파일로 deterministic하게 검증할 수 있다. + +### 테스트 커버리지 공백 + +현재 테스트는 archive에서 `oto`가 설치되고 background 실행 PID가 생기는지만 본다. 설치 직후 `oto --version` 또는 동등한 확인 절차는 없다. + +### 심볼 참조 + +Unix script의 fake `oto` fixture와 Windows PowerShell fixture가 직접 검증 대상이다. Core embedded copy는 script 변경 시 일치해야 한다. + +### 분할 판단 + +이 작업은 `04+02,03_binary_contract`로 Unix와 Windows bootstrap 구현에 의존한다. API나 registry 연결을 바꾸지 않는 bounded contract라 `local-G06`으로 충분하다. + +### 범위 결정 근거 + +release asset 생성 자동화나 실제 release upload는 하지 않는다. 설치된 binary 실행 가능성/버전 확인 계약만 고정한다. + +### 빌드 등급 + +`local-G06`: 선행 script가 완료되면 fake binary fixture와 deterministic tests 중심의 제한된 변경이다. + +## 의존 관계 및 구현 순서 + +- 선행: `agent-task/m-cross-os-runner-bootstrap/02_unix_bootstrap/complete.log` +- 선행: `agent-task/m-cross-os-runner-bootstrap/03+01_windows_bootstrap/complete.log` +- 후속: `05+01,02,03_node_connect` + +## 구현 체크리스트 + +- [ ] Unix bootstrap이 설치된 `oto` binary 실행 경로를 확인하고 버전 또는 동등한 health command를 실행한다. +- [ ] Windows bootstrap이 설치된 `oto.exe` 실행 경로를 확인하고 버전 또는 동등한 health command를 실행한다. +- [ ] fake binary fixture가 성공/실패 케이스를 검증하고 실패 출력에 token을 노출하지 않는다. +- [ ] README 또는 agent-smoke의 binary 확인 기준이 바뀌면 함께 갱신한다. +- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. + +### [BINARY-1] Unix binary 확인 + +문제: archive extraction 이후 binary 자체 검증이 없다. + +해결 방법: 설치된 `oto` 경로에서 `--version` 또는 안정적인 health command를 실행하고 실패 시 설치를 중단한다. + +수정 파일 및 체크리스트: + +- `apps/runner/assets/script/shell/oto_agent_bootstrap.sh` +- `services/core/internal/httpserver/oto_agent_bootstrap.sh` +- `apps/runner/test/oto_agent_bootstrap_script_test.dart` + +테스트 작성: fake `oto`가 version 성공/실패를 반환하는 케이스를 추가한다. + +중간 검증: `cd apps/runner && dart test test/oto_agent_bootstrap_script_test.dart` + +### [BINARY-2] Windows binary 확인 + +문제: Windows zip extraction 후 `oto.exe` 검증 계약이 필요하다. + +해결 방법: PowerShell script fixture에서 설치 경로의 `oto.exe` 존재와 version command를 검증한다. + +수정 파일 및 체크리스트: + +- `apps/runner/assets/script/powershell/oto_agent_bootstrap.ps1` +- Windows bootstrap test file +- `services/core/internal/httpserver/oto_agent_bootstrap.ps1` + +테스트 작성: text/parser fixture로 `oto.exe` 경로와 version invocation을 확인한다. + +중간 검증: `cd services/core && go test ./...` + +## 수정 파일 요약 + +| 경로 | 목적 | +|------|------| +| Unix/Windows bootstrap scripts | 설치 binary 확인 계약 | +| runner bootstrap tests | fake binary 성공/실패 fixture | +| Core embedded assets | script copy 동기화 | + +## 최종 검증 + +- `cd apps/runner && dart test test/oto_agent_bootstrap_script_test.dart` +- `cd services/core && go test ./...` + +모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다. diff --git a/agent-task/m-cross-os-runner-bootstrap/05+01,02,03_node_connect/CODE_REVIEW-cloud-G07.md b/agent-task/m-cross-os-runner-bootstrap/05+01,02,03_node_connect/CODE_REVIEW-cloud-G07.md new file mode 100644 index 0000000..ecb4e33 --- /dev/null +++ b/agent-task/m-cross-os-runner-bootstrap/05+01,02,03_node_connect/CODE_REVIEW-cloud-G07.md @@ -0,0 +1,59 @@ + + +# CODE REVIEW: bootstrap 생성 설정의 runner/node 연결 smoke + +## Roadmap Targets + +- Milestone: `agent-roadmap/phase/control-plane-product-surface/milestones/cross-os-runner-bootstrap.md` +- Epic: `[bootstrap-platforms] Cross-OS bootstrap contract` +- Task id: `[node-connect]` +- Completion mode: check-on-pass + +## 사용자 리뷰 요청 + +_기본값은 `없음`이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 변경 없이는 안전하게 진행할 수 없으면 아래 항목을 실제 내용으로 교체하고, 구현을 중단한 뒤 active 파일을 그대로 둔 채 리뷰를 요청한다. 구현 에이전트는 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 증거 공백만으로는 사용자 리뷰 요청을 작성하지 않는다._ + +- 상태: 없음 +- 사유 유형: 없음 +- 결정 필요: 없음 +- 차단 근거: 없음 +- 실행한 검증/명령: 없음 +- 자동 후속 불가 이유: 없음 +- 재개 조건: 없음 + +## 구현 에이전트 소유 섹션 + +### 계획 대비 변경 사항 + +- 미작성 + +### 수정 파일 + +- 미작성 + +### 검증 결과 + +- 미작성 + +### 구현 체크리스트 + +- [ ] bootstrap-generated YAML key가 `AgentConfig` parser와 정확히 일치하는지 fixture로 고정한다. +- [ ] `oto agent run` 또는 대응 runner flow가 Core runner registry에 등록/연결되는 최소 smoke를 추가한다. +- [ ] enrollment token validation 실패와 성공 경로를 구분하고 token 출력 노출을 피한다. +- [ ] Go Core registry tests와 Dart agent smoke를 모두 통과시킨다. +- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. + +## 코드 리뷰 전용 섹션 + +- [ ] `01_core_command`, `02_unix_bootstrap`, `03+01_windows_bootstrap` 완료 근거가 있다. +- [ ] bootstrap config fixture와 `AgentConfig` parser가 같은 key를 쓴다. +- [ ] Core registry 연결 성공/실패가 테스트로 구분된다. +- [ ] token/credential 출력 노출이 없다. + +## 완료 판정 메모 + +| 항목 | 값 | +|------|----| +| Verdict | 미작성 | +| Roadmap Completion | `[node-connect]` only | +| Archive ready | 미작성 | diff --git a/agent-task/m-cross-os-runner-bootstrap/05+01,02,03_node_connect/PLAN-cloud-G07.md b/agent-task/m-cross-os-runner-bootstrap/05+01,02,03_node_connect/PLAN-cloud-G07.md new file mode 100644 index 0000000..7924a69 --- /dev/null +++ b/agent-task/m-cross-os-runner-bootstrap/05+01,02,03_node_connect/PLAN-cloud-G07.md @@ -0,0 +1,124 @@ + + +# PLAN: bootstrap 생성 설정의 runner/node 연결 smoke + +## 이 파일을 읽는 구현 에이전트에게 + +이 계획은 `[node-connect]` 완료만 목표로 하며 `01_core_command`, `02_unix_bootstrap`, `03+01_windows_bootstrap` 완료 후 실행해야 한다. 연결 smoke는 기존 agent 설정 의미를 보존하면서 최소 등록/heartbeat 계약을 검증해야 한다. + +## 배경 + +bootstrap script는 agent config를 생성하고 `oto agent run`을 시작하거나 안내한다. 마일스톤 완료를 위해 이 설정이 OTO Core runner registry에 runner/node로 등록 또는 연결되는 최소 흐름을 보장해야 한다. + +## Roadmap Targets + +- Milestone: `agent-roadmap/phase/control-plane-product-surface/milestones/cross-os-runner-bootstrap.md` +- Epic: `[bootstrap-platforms] Cross-OS bootstrap contract` +- Task id: `[node-connect]` +- Completion mode: check-on-pass + +## 분석 결과 + +### 읽은 파일 + +- `apps/runner/lib/oto/agent/agent_config.dart` +- `apps/runner/lib/oto/agent/agent_runner.dart` +- `apps/runner/lib/oto/agent/registration_client.dart` +- `apps/runner/lib/oto/agent/oto_server_job_client.dart` +- `apps/runner/lib/cli/commands/command_agent.dart` +- `apps/runner/test/oto_agent_config_test.dart` +- `apps/runner/test/oto_agent_registration_test.dart` +- `apps/runner/test/oto_server_connection_smoke_test.dart` +- `services/core/internal/runnerregistry/registry.go` +- `services/core/internal/runnerregistry/registry_test.go` +- `services/core/internal/httpserver/runner_handlers.go` +- `services/core/internal/httpserver/server_test.go` +- `agent-test/local/rules.md` +- `agent-test/local/agent-smoke.md` + +### 테스트 환경 규칙 + +기본 환경은 local이다. agent smoke는 runner analyze와 agent 관련 Dart tests, Core Go tests를 요구한다. + +### 테스트 커버리지 공백 + +기존 테스트는 agent config parsing, registration client, server connection smoke를 다루지만 bootstrap script가 생성한 config와 `oto agent run` 사이의 end-to-end 최소 계약은 명시적으로 묶여 있지 않다. + +### 심볼 참조 + +`AgentConfig`, `RegistrationClient`, `AgentRunner`, `handleRunnerRegistration`, runner registry store가 연결 지점이다. bootstrap script가 쓰는 YAML key와 Dart parser의 기대 key가 일치해야 한다. + +### 분할 판단 + +이 작업은 `05+01,02,03_node_connect`로 API contract와 양쪽 platform bootstrap이 모두 준비된 뒤 수행한다. binary version check와는 병렬 가능하지만 실제 연결 흐름의 안정성을 위해 script 생성 config가 선행되어야 한다. + +### 범위 결정 근거 + +실제 장기 실행 daemon/service manager는 범위 제외다. local fake Core 또는 in-process test server에서 등록/연결 최소 경로를 검증한다. + +### 빌드 등급 + +`cloud-G07`: Dart runner와 Go Core의 cross-domain smoke이며 process/HTTP 상태 전이를 함께 검증한다. + +## 의존 관계 및 구현 순서 + +- 선행: `agent-task/m-cross-os-runner-bootstrap/01_core_command/complete.log` +- 선행: `agent-task/m-cross-os-runner-bootstrap/02_unix_bootstrap/complete.log` +- 선행: `agent-task/m-cross-os-runner-bootstrap/03+01_windows_bootstrap/complete.log` + +## 구현 체크리스트 + +- [ ] bootstrap-generated YAML key가 `AgentConfig` parser와 정확히 일치하는지 fixture로 고정한다. +- [ ] `oto agent run` 또는 대응 runner flow가 Core runner registry에 등록/연결되는 최소 smoke를 추가한다. +- [ ] enrollment token validation 실패와 성공 경로를 구분하고 token 출력 노출을 피한다. +- [ ] Go Core registry tests와 Dart agent smoke를 모두 통과시킨다. +- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. + +### [CONNECT-1] bootstrap config fixture 연결 + +문제: script가 쓰는 YAML과 Dart parser가 같은 의미인지 별도 fixture로 묶여 있지 않다. + +해결 방법: bootstrap script가 생성하는 config shape를 테스트 fixture에 반영하고 `AgentConfig`가 해당 값을 그대로 읽는지 검증한다. + +수정 파일 및 체크리스트: + +- `apps/runner/test/oto_agent_config_test.dart` +- `apps/runner/test/oto_agent_bootstrap_script_test.dart` +- 필요 시 `apps/runner/lib/oto/agent/agent_config.dart` + +테스트 작성: `agent.id`, `agent.enrollment_token`, `server.url`, `runtime.install_dir`, `runtime.workspace_root`, `runtime.log_dir` key를 확인한다. + +중간 검증: `cd apps/runner && dart test test/oto_agent_config_test.dart test/oto_agent_bootstrap_script_test.dart` + +### [CONNECT-2] runner registry 연결 smoke + +문제: bootstrap 이후 `oto agent run`이 Core registry에 연결되는 최소 흐름이 마일스톤 target으로 고정되어 있지 않다. + +해결 방법: existing smoke를 확장하거나 새 fixture를 추가해 registration endpoint, status/heartbeat, registry 상태를 함께 검증한다. + +수정 파일 및 체크리스트: + +- `apps/runner/test/oto_agent_registration_test.dart` +- `apps/runner/test/oto_server_connection_smoke_test.dart` +- `services/core/internal/runnerregistry/registry_test.go` +- `services/core/internal/httpserver/server_test.go` + +테스트 작성: 성공 등록, invalid token, status visibility를 검증한다. + +중간 검증: `cd apps/runner && dart test test/oto_agent_registration_test.dart test/oto_server_connection_smoke_test.dart` + +## 수정 파일 요약 + +| 경로 | 목적 | +|------|------| +| runner agent config/tests | bootstrap config와 parser 연결 | +| runner registration/smoke tests | `oto agent run` 최소 연결 검증 | +| Core registry/httpserver tests | registry 상태와 endpoint 회귀 검증 | + +## 최종 검증 + +- `cd apps/runner && dart analyze` +- `cd apps/runner && dart test test/oto_agent_config_test.dart test/oto_agent_registration_test.dart test/oto_server_connection_smoke_test.dart` +- `cd services/core && go test ./...` + +모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다. diff --git a/agent-test/local/agent-smoke.md b/agent-test/local/agent-smoke.md index 537d510..004c92d 100644 --- a/agent-test/local/agent-smoke.md +++ b/agent-test/local/agent-smoke.md @@ -41,6 +41,10 @@ last_rule_updated_at: 2026-06-12 - 외부 브라우저/클라이언트가 원격 Core에 접근해야 하면 `http://toki-labs.com:18020`을 서버 URL로 사용한다. - bootstrap command는 `--server-url`을 기준 인자로 문서화한다. `--edge-url`은 script 호환 alias로만 본다. - `OTO_RUNNER_RELEASE_BASE_URL`은 `https://` scheme만 허용한다. token, credential, private release host 원문은 테스트 규칙과 tracked 문서에 쓰지 않는다. +- OS별 bootstrap asset 이름은 README의 `Runner bootstrap asset matrix`와 테스트 fixture가 같은 값을 사용한다. + - Linux: `oto-linux-x64.tar.gz`, `oto-linux-arm64.tar.gz` + - macOS: `oto-macos-x64.tar.gz`, `oto-macos-arm64.tar.gz` + - Windows: `oto-windows-x64.zip`, `oto-windows-arm64.zip` - iop Edge/wire endpoint는 독립 OTO Control Plane smoke의 필수 검증 대상이 아니다. ## 명령 @@ -64,6 +68,7 @@ last_rule_updated_at: 2026-06-12 - runner agent 도메인 변경 후 `cd apps/runner && dart analyze`를 실행한다. - runner agent 도메인 변경 후 agent 관련 `cd apps/runner && dart test ...` 명령을 실행한다. - services/core registration, bootstrap command, release URL validation 변경 후 `cd services/core && go test ./...`를 실행한다. +- OS별 bootstrap matrix 또는 release asset 이름을 바꾸면 `README.md`, `apps/runner/test/oto_agent_bootstrap_script_test.dart`, 이 문서의 asset 이름이 함께 갱신되었는지 확인한다. ## 보조 검증 diff --git a/apps/runner/lib/oto/agent/oto/runner.pb.dart b/apps/runner/lib/oto/agent/oto/runner.pb.dart index 8904606..44882e9 100644 --- a/apps/runner/lib/oto/agent/oto/runner.pb.dart +++ b/apps/runner/lib/oto/agent/oto/runner.pb.dart @@ -514,10 +514,12 @@ class BootstrapCommandRequest extends $pb.GeneratedMessage { factory BootstrapCommandRequest({ $core.String? runnerId, $core.String? enrollmentToken, + $core.String? target, }) { final result = create(); if (runnerId != null) result.runnerId = runnerId; if (enrollmentToken != null) result.enrollmentToken = enrollmentToken; + if (target != null) result.target = target; return result; } @@ -536,6 +538,7 @@ class BootstrapCommandRequest extends $pb.GeneratedMessage { createEmptyInstance: create) ..aOS(1, _omitFieldNames ? '' : 'runnerId') ..aOS(2, _omitFieldNames ? '' : 'enrollmentToken') + ..aOS(3, _omitFieldNames ? '' : 'target') ..hasRequiredFields = false; @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') @@ -575,6 +578,15 @@ class BootstrapCommandRequest extends $pb.GeneratedMessage { $core.bool hasEnrollmentToken() => $_has(1); @$pb.TagNumber(2) void clearEnrollmentToken() => $_clearField(2); + + @$pb.TagNumber(3) + $core.String get target => $_getSZ(2); + @$pb.TagNumber(3) + set target($core.String value) => $_setString(2, value); + @$pb.TagNumber(3) + $core.bool hasTarget() => $_has(2); + @$pb.TagNumber(3) + void clearTarget() => $_clearField(3); } class BootstrapCommandResponse extends $pb.GeneratedMessage { diff --git a/apps/runner/lib/oto/agent/oto/runner.pbjson.dart b/apps/runner/lib/oto/agent/oto/runner.pbjson.dart index 374a394..d37777b 100644 --- a/apps/runner/lib/oto/agent/oto/runner.pbjson.dart +++ b/apps/runner/lib/oto/agent/oto/runner.pbjson.dart @@ -170,14 +170,15 @@ const BootstrapCommandRequest$json = { '2': [ {'1': 'runner_id', '3': 1, '4': 1, '5': 9, '10': 'runnerId'}, {'1': 'enrollment_token', '3': 2, '4': 1, '5': 9, '10': 'enrollmentToken'}, + {'1': 'target', '3': 3, '4': 1, '5': 9, '10': 'target'}, ], }; /// Descriptor for `BootstrapCommandRequest`. Decode as a `google.protobuf.DescriptorProto`. -final $typed_data.Uint8List bootstrapCommandRequestDescriptor = - $convert.base64Decode( - 'ChdCb290c3RyYXBDb21tYW5kUmVxdWVzdBIbCglydW5uZXJfaWQYASABKAlSCHJ1bm5lcklkEi' - 'kKEGVucm9sbG1lbnRfdG9rZW4YAiABKAlSD2Vucm9sbG1lbnRUb2tlbg=='); +final $typed_data.Uint8List bootstrapCommandRequestDescriptor = $convert.base64Decode( + 'ChdCb290c3RyYXBDb21tYW5kUmVxdWVzdBIbCglydW5uZXJfaWQYASABKAlSCHJ1bm5lcklkEi' + 'kKEGVucm9sbG1lbnRfdG9rZW4YAiABKAlSD2Vucm9sbG1lbnRUb2tlbhIWCgZ0YXJnZXQYAyAB' + 'KAlSBnRhcmdldA=='); @$core.Deprecated('Use bootstrapCommandResponseDescriptor instead') const BootstrapCommandResponse$json = { diff --git a/apps/runner/test/oto_agent_bootstrap_script_test.dart b/apps/runner/test/oto_agent_bootstrap_script_test.dart index 01740ee..de06ae4 100644 --- a/apps/runner/test/oto_agent_bootstrap_script_test.dart +++ b/apps/runner/test/oto_agent_bootstrap_script_test.dart @@ -1,14 +1,31 @@ import 'dart:io'; import 'package:test/test.dart'; +const bootstrapAssetMatrix = >{ + 'linux': ['oto-linux-x64.tar.gz', 'oto-linux-arm64.tar.gz'], + 'macos': ['oto-macos-x64.tar.gz', 'oto-macos-arm64.tar.gz'], + 'windows': ['oto-windows-x64.zip', 'oto-windows-arm64.zip'], +}; + +Iterable allSupportedBootstrapAssets() sync* { + for (final assets in bootstrapAssetMatrix.values) { + yield* assets; + } +} + void main() { final scriptFile = File('assets/script/shell/oto_agent_bootstrap.sh'); + final rootReadmeFile = File('../../README.md'); group('Linux Bootstrap Script Contract Tests', () { late String content; setUpAll(() async { - expect(await scriptFile.exists(), isTrue, reason: 'Bootstrap script file must exist'); + expect( + await scriptFile.exists(), + isTrue, + reason: 'Bootstrap script file must exist', + ); content = await scriptFile.readAsString(); }); @@ -31,10 +48,27 @@ void main() { }); test('should support correct architecture assets', () { - expect(content, contains('oto-linux-x64.tar.gz')); - expect(content, contains('oto-linux-arm64.tar.gz')); + for (final asset in bootstrapAssetMatrix['linux']!) { + expect(content, contains(asset)); + } }); + test( + 'should keep README bootstrap asset matrix aligned with test fixture', + () async { + expect( + await rootReadmeFile.exists(), + isTrue, + reason: 'Root README must document bootstrap asset names', + ); + final readmeContent = await rootReadmeFile.readAsString(); + + for (final asset in allSupportedBootstrapAssets()) { + expect(readmeContent, contains(asset)); + } + }, + ); + test('should write expected keys to configuration yaml', () { expect(content, contains('agent:')); expect(content, contains('id:')); @@ -67,35 +101,53 @@ void main() { ]); expect(result.exitCode, isNot(0)); - expect(result.stderr.toString(), contains('--release-base-url must use https://')); - expect(result.stdout.toString(), isNot(contains('Downloading OTO agent'))); + expect( + result.stderr.toString(), + contains('--release-base-url must use https://'), + ); + expect( + result.stdout.toString(), + isNot(contains('Downloading OTO agent')), + ); // enrollment token이 출력에 노출되지 않는지 확인 expect(result.stdout.toString(), isNot(contains('secret-token-123'))); expect(result.stderr.toString(), isNot(contains('secret-token-123'))); }); - test('should write PID to standard agent state path when custom config-path is specified', () async { - final tempDir = await Directory.systemTemp.createTemp('oto_bootstrap_test_'); - try { - final tempHome = Directory('${tempDir.path}/home'); - await tempHome.create(recursive: true); + test( + 'should write PID to standard agent state path when custom config-path is specified', + () async { + final tempDir = await Directory.systemTemp.createTemp( + 'oto_bootstrap_test_', + ); + try { + final tempHome = Directory('${tempDir.path}/home'); + await tempHome.create(recursive: true); - // 1. 가짜 OTO 실행 파일 생성 - final fakeOtoSource = Directory('${tempDir.path}/fake_oto_src'); - await fakeOtoSource.create(recursive: true); - final fakeOtoFile = File('${fakeOtoSource.path}/oto'); - await fakeOtoFile.writeAsString('#!/bin/sh\nwhile true; do sleep 1; done\n'); - await Process.run('chmod', ['+x', fakeOtoFile.path]); + // 1. 가짜 OTO 실행 파일 생성 + final fakeOtoSource = Directory('${tempDir.path}/fake_oto_src'); + await fakeOtoSource.create(recursive: true); + final fakeOtoFile = File('${fakeOtoSource.path}/oto'); + await fakeOtoFile.writeAsString( + '#!/bin/sh\nwhile true; do sleep 1; done\n', + ); + await Process.run('chmod', ['+x', fakeOtoFile.path]); - // 2. 가짜 tar.gz 아카이브 생성 - final fakeTarGz = File('${tempDir.path}/oto-linux-x64.tar.gz'); - await Process.run('tar', ['-czf', fakeTarGz.path, '-C', fakeOtoSource.path, 'oto']); + // 2. 가짜 tar.gz 아카이브 생성 + final fakeTarGz = File('${tempDir.path}/oto-linux-x64.tar.gz'); + await Process.run('tar', [ + '-czf', + fakeTarGz.path, + '-C', + fakeOtoSource.path, + 'oto', + ]); - // 3. 가짜 curl 스크립트 생성 - final fakeBinDir = Directory('${tempDir.path}/bin'); - await fakeBinDir.create(recursive: true); - final fakeCurl = File('${fakeBinDir.path}/curl'); - await fakeCurl.writeAsString('''#!/bin/sh + // 3. 가짜 curl 스크립트 생성 + final fakeBinDir = Directory('${tempDir.path}/bin'); + await fakeBinDir.create(recursive: true); + final fakeCurl = File('${fakeBinDir.path}/curl'); + await fakeCurl.writeAsString('''#!/bin/sh out_file="" while [ \$# -gt 0 ]; do if [ "\$1" = "-o" ]; then @@ -108,51 +160,75 @@ if [ -n "\$out_file" ]; then cp "${fakeTarGz.path}" "\$out_file" fi '''); - await Process.run('chmod', ['+x', fakeCurl.path]); + await Process.run('chmod', ['+x', fakeCurl.path]); - // 4. custom config path 지정 - final customConfigDir = Directory('${tempDir.path}/custom_config'); - await customConfigDir.create(recursive: true); - final customConfigPath = '${customConfigDir.path}/my-config.yaml'; + // 4. custom config path 지정 + final customConfigDir = Directory('${tempDir.path}/custom_config'); + await customConfigDir.create(recursive: true); + final customConfigPath = '${customConfigDir.path}/my-config.yaml'; - // PATH 환경변수에 fakeBinDir 등록, HOME을 tempHome으로 설정 - final env = Map.from(Platform.environment); - env['PATH'] = '${fakeBinDir.path}:${env['PATH']}'; - env['HOME'] = tempHome.path; + // PATH 환경변수에 fakeBinDir 등록, HOME을 tempHome으로 설정 + final env = Map.from(Platform.environment); + env['PATH'] = '${fakeBinDir.path}:${env['PATH']}'; + env['HOME'] = tempHome.path; - final result = await Process.run('bash', [ - scriptFile.absolute.path, - '--server-url', 'https://server.example.com', - '--agent-id', 'test-agent', - '--enrollment-token', 'secret-token-123', - '--release-base-url', 'https://example.com/release', - '--config-path', customConfigPath, - ], environment: env); + final result = await Process.run('bash', [ + scriptFile.absolute.path, + '--server-url', + 'https://server.example.com', + '--agent-id', + 'test-agent', + '--enrollment-token', + 'secret-token-123', + '--release-base-url', + 'https://example.com/release', + '--config-path', + customConfigPath, + ], environment: env); - expect(result.exitCode, 0, reason: 'Script failed with: ${result.stderr}'); + expect( + result.exitCode, + 0, + reason: 'Script failed with: ${result.stderr}', + ); - // 5. 검증 - final configFile = File(customConfigPath); - expect(await configFile.exists(), isTrue); + // 5. 검증 + final configFile = File(customConfigPath); + expect(await configFile.exists(), isTrue); - final expectedPidFile = File('${tempHome.path}/.oto/agent/oto-agent.pid'); - expect(await expectedPidFile.exists(), isTrue, reason: 'PID file should exist in standard agent state path'); + final expectedPidFile = File( + '${tempHome.path}/.oto/agent/oto-agent.pid', + ); + expect( + await expectedPidFile.exists(), + isTrue, + reason: 'PID file should exist in standard agent state path', + ); - final unexpectedPidFile = File('${customConfigDir.path}/oto-agent.pid'); - expect(await unexpectedPidFile.exists(), isFalse, reason: 'PID file should not exist in custom config path directory'); + final unexpectedPidFile = File( + '${customConfigDir.path}/oto-agent.pid', + ); + expect( + await unexpectedPidFile.exists(), + isFalse, + reason: 'PID file should not exist in custom config path directory', + ); - final pidStr = await expectedPidFile.readAsString(); - final pid = int.tryParse(pidStr.trim()); - if (pid != null) { - Process.killPid(pid); + final pidStr = await expectedPidFile.readAsString(); + final pid = int.tryParse(pidStr.trim()); + if (pid != null) { + Process.killPid(pid); + } + } finally { + await tempDir.delete(recursive: true); } - } finally { - await tempDir.delete(recursive: true); - } - }); + }, + ); test('should support --edge-url as alias compatibility', () async { - final tempDir = await Directory.systemTemp.createTemp('oto_bootstrap_alias_test_'); + final tempDir = await Directory.systemTemp.createTemp( + 'oto_bootstrap_alias_test_', + ); try { final tempHome = Directory('${tempDir.path}/home'); await tempHome.create(recursive: true); @@ -161,12 +237,20 @@ fi final fakeOtoSource = Directory('${tempDir.path}/fake_oto_src'); await fakeOtoSource.create(recursive: true); final fakeOtoFile = File('${fakeOtoSource.path}/oto'); - await fakeOtoFile.writeAsString('#!/bin/sh\nwhile true; do sleep 1; done\n'); + await fakeOtoFile.writeAsString( + '#!/bin/sh\nwhile true; do sleep 1; done\n', + ); await Process.run('chmod', ['+x', fakeOtoFile.path]); // 2. 가짜 tar.gz 아카이브 생성 final fakeTarGz = File('${tempDir.path}/oto-linux-x64.tar.gz'); - await Process.run('tar', ['-czf', fakeTarGz.path, '-C', fakeOtoSource.path, 'oto']); + await Process.run('tar', [ + '-czf', + fakeTarGz.path, + '-C', + fakeOtoSource.path, + 'oto', + ]); // 3. 가짜 curl 스크립트 생성 final fakeBinDir = Directory('${tempDir.path}/bin'); @@ -195,14 +279,23 @@ fi final result = await Process.run('bash', [ scriptFile.absolute.path, - '--edge-url', 'https://edge.example.com', - '--agent-id', 'test-agent-alias', - '--enrollment-token', 'secret-token-123', - '--release-base-url', 'https://example.com/release', - '--config-path', customConfigPath, + '--edge-url', + 'https://edge.example.com', + '--agent-id', + 'test-agent-alias', + '--enrollment-token', + 'secret-token-123', + '--release-base-url', + 'https://example.com/release', + '--config-path', + customConfigPath, ], environment: env); - expect(result.exitCode, 0, reason: 'Script failed with: \${result.stderr}'); + expect( + result.exitCode, + 0, + reason: 'Script failed with: \${result.stderr}', + ); final configFile = File(customConfigPath); expect(await configFile.exists(), isTrue); @@ -210,7 +303,9 @@ fi expect(configContent, contains('server:')); expect(configContent, contains('url: "https://edge.example.com"')); - final expectedPidFile = File('${tempHome.path}/.oto/agent/oto-agent.pid'); + final expectedPidFile = File( + '${tempHome.path}/.oto/agent/oto-agent.pid', + ); expect(await expectedPidFile.exists(), isTrue); final pidStr = await expectedPidFile.readAsString(); final pid = int.tryParse(pidStr.trim()); diff --git a/proto/oto/runner.proto b/proto/oto/runner.proto index cfa7493..e3c921a 100644 --- a/proto/oto/runner.proto +++ b/proto/oto/runner.proto @@ -50,6 +50,7 @@ message HeartbeatResponse { message BootstrapCommandRequest { string runner_id = 1; string enrollment_token = 2; + string target = 3; } message BootstrapCommandResponse { diff --git a/services/core/internal/httpserver/runner_handlers.go b/services/core/internal/httpserver/runner_handlers.go index 63e5527..19133c0 100644 --- a/services/core/internal/httpserver/runner_handlers.go +++ b/services/core/internal/httpserver/runner_handlers.go @@ -177,6 +177,10 @@ func shellEscape(s string) string { return "'" + strings.ReplaceAll(s, "'", "'\\''") + "'" } +func powershellEscape(s string) string { + return "'" + strings.ReplaceAll(s, "'", "''") + "'" +} + func handleRunnerBootstrapCommand(registry *runnerregistry.Registry) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { @@ -197,6 +201,12 @@ func handleRunnerBootstrapCommand(registry *runnerregistry.Registry) http.Handle return } + target := strings.ToLower(strings.TrimSpace(request.GetTarget())) + if target != "" && target != "linux" && target != "macos" && target != "windows" { + writeResponse(w, http.StatusBadRequest, errorToJSON(fmt.Sprintf("unsupported bootstrap target: %s", target))) + return + } + scheme := "http" if r.TLS != nil { scheme = "https" @@ -233,16 +243,30 @@ func handleRunnerBootstrapCommand(registry *runnerregistry.Registry) http.Handle } } - escapedScriptURL := shellEscape(serverURL + "/bootstrap/oto-agent.sh") - escapedServerURL := shellEscape(serverURL) - escapedRunnerID := shellEscape(runnerID) - escapedToken := shellEscape(token) - escapedReleaseURL := shellEscape(releaseBaseURL) + var bootstrapCmd string + if target == "windows" { + escapedScriptURL := powershellEscape(serverURL + "/bootstrap/oto-agent.ps1") + escapedServerURL := powershellEscape(serverURL) + escapedRunnerID := powershellEscape(runnerID) + escapedToken := powershellEscape(token) + escapedReleaseURL := powershellEscape(releaseBaseURL) - bootstrapCmd := fmt.Sprintf( - "curl -fsSL %s | bash -s -- --server-url %s --agent-id %s --enrollment-token %s --release-base-url %s", - escapedScriptURL, escapedServerURL, escapedRunnerID, escapedToken, escapedReleaseURL, - ) + bootstrapCmd = fmt.Sprintf( + "powershell -ExecutionPolicy Bypass -Command \"& ([scriptblock]::Create((irm %s))) -- --server-url %s --agent-id %s --enrollment-token %s --release-base-url %s\"", + escapedScriptURL, escapedServerURL, escapedRunnerID, escapedToken, escapedReleaseURL, + ) + } else { + escapedScriptURL := shellEscape(serverURL + "/bootstrap/oto-agent.sh") + escapedServerURL := shellEscape(serverURL) + escapedRunnerID := shellEscape(runnerID) + escapedToken := shellEscape(token) + escapedReleaseURL := shellEscape(releaseBaseURL) + + bootstrapCmd = fmt.Sprintf( + "curl -fsSL %s | bash -s -- --server-url %s --agent-id %s --enrollment-token %s --release-base-url %s", + escapedScriptURL, escapedServerURL, escapedRunnerID, escapedToken, escapedReleaseURL, + ) + } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) diff --git a/services/core/internal/httpserver/server_test.go b/services/core/internal/httpserver/server_test.go index c435d25..81fdbb2 100644 --- a/services/core/internal/httpserver/server_test.go +++ b/services/core/internal/httpserver/server_test.go @@ -486,6 +486,97 @@ func TestHandleRunnerBootstrapCommand(t *testing.T) { if rr.Code != http.StatusBadRequest { t.Fatalf("expected 400 Bad Request for Host with shell metacharacter (no space), got %v", rr.Code) } + + // 9. target="linux" should work and return Unix shell command + bodyLinux := bytes.NewBufferString(`{"runner_id":"runner-123","enrollment_token":"token-123","target":"linux"}`) + req = httptest.NewRequest(http.MethodPost, "/api/v1/runners/bootstrap-command", bodyLinux) + req.Host = "localhost:8080" + rr = httptest.NewRecorder() + + handleRunnerBootstrapCommand(registry)(rr, req) + if rr.Code != http.StatusOK { + t.Fatalf("status = %v, want %v for target=linux", rr.Code, http.StatusOK) + } + if err := json.Unmarshal(rr.Body.Bytes(), &response); err != nil { + t.Fatalf("decode bootstrap command response: %v", err) + } + expectedLinuxCmd := "curl -fsSL 'http://localhost:8080/bootstrap/oto-agent.sh' | bash -s -- --server-url 'http://localhost:8080' --agent-id 'runner-123' --enrollment-token 'token-123' --release-base-url 'https://example.com/releases'" + if response.GetBootstrapCommand() != expectedLinuxCmd { + t.Fatalf("bootstrap_command for linux = %q, want %q", response.GetBootstrapCommand(), expectedLinuxCmd) + } + + // 10. target="macos" should work and return Unix shell command + bodyMacos := bytes.NewBufferString(`{"runner_id":"runner-123","enrollment_token":"token-123","target":"macos"}`) + req = httptest.NewRequest(http.MethodPost, "/api/v1/runners/bootstrap-command", bodyMacos) + req.Host = "localhost:8080" + rr = httptest.NewRecorder() + + handleRunnerBootstrapCommand(registry)(rr, req) + if rr.Code != http.StatusOK { + t.Fatalf("status = %v, want %v for target=macos", rr.Code, http.StatusOK) + } + if err := json.Unmarshal(rr.Body.Bytes(), &response); err != nil { + t.Fatalf("decode bootstrap command response: %v", err) + } + expectedMacosCmd := "curl -fsSL 'http://localhost:8080/bootstrap/oto-agent.sh' | bash -s -- --server-url 'http://localhost:8080' --agent-id 'runner-123' --enrollment-token 'token-123' --release-base-url 'https://example.com/releases'" + if response.GetBootstrapCommand() != expectedMacosCmd { + t.Fatalf("bootstrap_command for macos = %q, want %q", response.GetBootstrapCommand(), expectedMacosCmd) + } + + // 11. target="windows" should work and return PowerShell command + bodyWindows := bytes.NewBufferString(`{"runner_id":"runner-123","enrollment_token":"token-123","target":"windows"}`) + req = httptest.NewRequest(http.MethodPost, "/api/v1/runners/bootstrap-command", bodyWindows) + req.Host = "localhost:8080" + rr = httptest.NewRecorder() + + handleRunnerBootstrapCommand(registry)(rr, req) + if rr.Code != http.StatusOK { + t.Fatalf("status = %v, want %v for target=windows", rr.Code, http.StatusOK) + } + if err := json.Unmarshal(rr.Body.Bytes(), &response); err != nil { + t.Fatalf("decode bootstrap command response: %v", err) + } + expectedWindowsCmd := "powershell -ExecutionPolicy Bypass -Command \"& ([scriptblock]::Create((irm 'http://localhost:8080/bootstrap/oto-agent.ps1'))) -- --server-url 'http://localhost:8080' --agent-id 'runner-123' --enrollment-token 'token-123' --release-base-url 'https://example.com/releases'\"" + if response.GetBootstrapCommand() != expectedWindowsCmd { + t.Fatalf("bootstrap_command for windows = %q, want %q", response.GetBootstrapCommand(), expectedWindowsCmd) + } + + // 12. target="windows" with malicious runner_id/token should escape properly for PowerShell + bodyWindowsMalicious := bytes.NewBufferString(`{"runner_id":"runner; rm -rf /","enrollment_token":"token'$(say hello)'","target":"windows"}`) + req = httptest.NewRequest(http.MethodPost, "/api/v1/runners/bootstrap-command", bodyWindowsMalicious) + req.Host = "localhost:8080" + rr = httptest.NewRecorder() + + handleRunnerBootstrapCommand(registry)(rr, req) + if rr.Code != http.StatusOK { + t.Fatalf("status = %v, want %v for target=windows with malicious fields", rr.Code, http.StatusOK) + } + if err := json.Unmarshal(rr.Body.Bytes(), &response); err != nil { + t.Fatalf("decode bootstrap command response: %v", err) + } + expectedWindowsEscapedCmd := "powershell -ExecutionPolicy Bypass -Command \"& ([scriptblock]::Create((irm 'http://localhost:8080/bootstrap/oto-agent.ps1'))) -- --server-url 'http://localhost:8080' --agent-id 'runner; rm -rf /' --enrollment-token 'token''$(say hello)''' --release-base-url 'https://example.com/releases'\"" + if response.GetBootstrapCommand() != expectedWindowsEscapedCmd { + t.Fatalf("bootstrap_command for windows malicious = %q, want %q", response.GetBootstrapCommand(), expectedWindowsEscapedCmd) + } + + // 13. unsupported target should be rejected with 400 Bad Request + bodyUnsupported := bytes.NewBufferString(`{"runner_id":"runner-123","enrollment_token":"token-123","target":"freebsd"}`) + req = httptest.NewRequest(http.MethodPost, "/api/v1/runners/bootstrap-command", bodyUnsupported) + req.Host = "localhost:8080" + rr = httptest.NewRecorder() + + handleRunnerBootstrapCommand(registry)(rr, req) + if rr.Code != http.StatusBadRequest { + t.Fatalf("status = %v, want %v for unsupported target", rr.Code, http.StatusBadRequest) + } + var errResponse map[string]interface{} + if err := json.Unmarshal(rr.Body.Bytes(), &errResponse); err != nil { + t.Fatalf("decode error response: %v", err) + } + expectedErr := "unsupported bootstrap target: freebsd" + if errResponse["error"] != expectedErr { + t.Fatalf("error = %v, want %q", errResponse["error"], expectedErr) + } } // staticProvider is a test-only bootstrapScriptProvider with fixed content. diff --git a/services/core/oto/runner.pb.go b/services/core/oto/runner.pb.go index 42ca4ef..4ef7f55 100644 --- a/services/core/oto/runner.pb.go +++ b/services/core/oto/runner.pb.go @@ -442,6 +442,7 @@ type BootstrapCommandRequest struct { state protoimpl.MessageState `protogen:"open.v1"` RunnerId string `protobuf:"bytes,1,opt,name=runner_id,json=runnerId,proto3" json:"runner_id,omitempty"` EnrollmentToken string `protobuf:"bytes,2,opt,name=enrollment_token,json=enrollmentToken,proto3" json:"enrollment_token,omitempty"` + Target string `protobuf:"bytes,3,opt,name=target,proto3" json:"target,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -490,6 +491,13 @@ func (x *BootstrapCommandRequest) GetEnrollmentToken() string { return "" } +func (x *BootstrapCommandRequest) GetTarget() string { + if x != nil { + return x.Target + } + return "" +} + type BootstrapCommandResponse struct { state protoimpl.MessageState `protogen:"open.v1"` BootstrapCommand string `protobuf:"bytes,1,opt,name=bootstrap_command,json=bootstrapCommand,proto3" json:"bootstrap_command,omitempty"` @@ -1761,10 +1769,11 @@ const file_oto_runner_proto_rawDesc = "" + "\x11HeartbeatResponse\x12\x18\n" + "\asuccess\x18\x01 \x01(\bR\asuccess\x12#\n" + "\rerror_message\x18\x02 \x01(\tR\ferrorMessage\x122\n" + - "\x05error\x18\x03 \x01(\v2\x1c.oto.runner.v1.ProtocolErrorR\x05error\"a\n" + + "\x05error\x18\x03 \x01(\v2\x1c.oto.runner.v1.ProtocolErrorR\x05error\"y\n" + "\x17BootstrapCommandRequest\x12\x1b\n" + "\trunner_id\x18\x01 \x01(\tR\brunnerId\x12)\n" + - "\x10enrollment_token\x18\x02 \x01(\tR\x0fenrollmentToken\"G\n" + + "\x10enrollment_token\x18\x02 \x01(\tR\x0fenrollmentToken\x12\x16\n" + + "\x06target\x18\x03 \x01(\tR\x06target\"G\n" + "\x18BootstrapCommandResponse\x12+\n" + "\x11bootstrap_command\x18\x01 \x01(\tR\x10bootstrapCommand\"h\n" + "\x0fJobClaimRequest\x12\x1b\n" +