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
This commit is contained in:
toki 2026-06-15 17:12:35 +09:00
parent 5cecc55905
commit 590e3219da
24 changed files with 1504 additions and 98 deletions

View file

@ -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)
각 앱/서비스의 패키지 및 모듈 의존성을 설치합니다.

View file

@ -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 경계

View file

@ -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 통합
- 확인 필요: 없음

View file

@ -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 데이터 바인딩, 인증/권한, 운영 대시보드 고도화
- 확인 필요: 없음

View file

@ -0,0 +1,153 @@
<!-- task=m-cross-os-runner-bootstrap/01_core_command plan=1 tag=REVIEW_API -->
# 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-<milestone-slug>`이면 완료 이벤트 메타데이터를 보고한다. 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-<milestone-slug>`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, 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 <script-url>))) -- --server-url <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를 실제 내용으로 교체 |
| 사용자 리뷰 요청 | 구현 에이전트 | 필요 없으면 `상태: 없음` 유지 |
| 검증 결과 | 구현 에이전트 | 실행 출력 기록 |
| 코드리뷰 결과 | 리뷰 에이전트 | 스텁에는 포함하지 않음 |

View file

@ -0,0 +1,66 @@
<!-- task=m-cross-os-runner-bootstrap/01_core_command plan=1 tag=REVIEW_API -->
# 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 <script-url>))) -- --server-url <server-url> --agent-id <runner-id> --enrollment-token <token> --release-base-url <release-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 계약이 변하지 않았는지 확인한다.

View file

@ -0,0 +1,105 @@
<!-- task=m-cross-os-runner-bootstrap/01_core_command plan=0 tag=API -->
# 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 <url>))) -- --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` 직접 호출을 하지 않았다.

View file

@ -0,0 +1,117 @@
<!-- task=m-cross-os-runner-bootstrap/01_core_command plan=0 tag=API -->
# 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`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.

View file

@ -0,0 +1,59 @@
<!-- task=m-cross-os-runner-bootstrap/02_unix_bootstrap plan=0 tag=BOOTSTRAP_UNIX -->
# 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 | 미작성 |

View file

@ -0,0 +1,110 @@
<!-- task=m-cross-os-runner-bootstrap/02_unix_bootstrap plan=0 tag=BOOTSTRAP_UNIX -->
# 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`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.

View file

@ -0,0 +1,59 @@
<!-- task=m-cross-os-runner-bootstrap/03+01_windows_bootstrap plan=0 tag=BOOTSTRAP_WINDOWS -->
# 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 | 미작성 |

View file

@ -0,0 +1,122 @@
<!-- task=m-cross-os-runner-bootstrap/03+01_windows_bootstrap plan=0 tag=BOOTSTRAP_WINDOWS -->
# 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`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.

View file

@ -0,0 +1,59 @@
<!-- task=m-cross-os-runner-bootstrap/04+02,03_binary_contract plan=0 tag=BINARY -->
# 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 | 미작성 |

View file

@ -0,0 +1,114 @@
<!-- task=m-cross-os-runner-bootstrap/04+02,03_binary_contract plan=0 tag=BINARY -->
# 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`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.

View file

@ -0,0 +1,59 @@
<!-- task=m-cross-os-runner-bootstrap/05+01,02,03_node_connect plan=0 tag=CONNECT -->
# 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 | 미작성 |

View file

@ -0,0 +1,124 @@
<!-- task=m-cross-os-runner-bootstrap/05+01,02,03_node_connect plan=0 tag=CONNECT -->
# 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`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.

View file

@ -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 이름이 함께 갱신되었는지 확인한다.
## 보조 검증

View file

@ -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 {

View file

@ -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 = {

View file

@ -1,14 +1,31 @@
import 'dart:io';
import 'package:test/test.dart';
const bootstrapAssetMatrix = <String, List<String>>{
'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<String> 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<String, String>.from(Platform.environment);
env['PATH'] = '${fakeBinDir.path}:${env['PATH']}';
env['HOME'] = tempHome.path;
// PATH fakeBinDir , HOME을 tempHome으로
final env = Map<String, String>.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());

View file

@ -50,6 +50,7 @@ message HeartbeatResponse {
message BootstrapCommandRequest {
string runner_id = 1;
string enrollment_token = 2;
string target = 3;
}
message BootstrapCommandResponse {

View file

@ -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)

View file

@ -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.

View file

@ -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" +