From 6e8854eadddde705fd9fcb4ac056df614ab5657c Mon Sep 17 00:00:00 2001 From: toki Date: Wed, 10 Jun 2026 19:32:24 +0900 Subject: [PATCH] =?UTF-8?q?fix(core):=20=EB=B6=80=ED=8A=B8=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=EB=9E=A9=20=EB=B0=B0=ED=8F=AC=20=EA=B2=BD=EB=A1=9C?= =?UTF-8?q?=EC=99=80=20=ED=95=B8=EB=93=A4=EB=9F=AC=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=EB=A5=BC=20=EC=A0=95=EB=A6=AC=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 코어 서버의 부트스트랩 공급자 경로와 라우팅을 실운영 흐름에 맞춰 정합성 있게 갱신하고, 핸들러 테스트를 함께 갱신해 동작 회귀를 방지한다. --- .../code_review_local_G05_0.log | 41 +++++ .../code_review_local_G06_1.log | 120 ++++++++++++++ .../complete.log | 35 ++++ .../plan_local_G05_0.log} | 0 .../plan_local_G06_1.log | 120 ++++++++++++++ .../CODE_REVIEW-local-G05.md | 21 --- .../internal/httpserver/bootstrap_provider.go | 24 +++ .../httpserver/oto_agent_bootstrap.sh | 153 ++++++++++++++++++ services/core/internal/httpserver/routes.go | 2 +- .../internal/httpserver/runner_handlers.go | 18 +-- .../core/internal/httpserver/server_test.go | 65 +++++++- 11 files changed, 561 insertions(+), 38 deletions(-) create mode 100644 agent-task/archive/2026/06/control_plane_phase_exit_followups/03_bootstrap_script_distribution/code_review_local_G05_0.log create mode 100644 agent-task/archive/2026/06/control_plane_phase_exit_followups/03_bootstrap_script_distribution/code_review_local_G06_1.log create mode 100644 agent-task/archive/2026/06/control_plane_phase_exit_followups/03_bootstrap_script_distribution/complete.log rename agent-task/{control_plane_phase_exit_followups/03_bootstrap_script_distribution/PLAN-local-G05.md => archive/2026/06/control_plane_phase_exit_followups/03_bootstrap_script_distribution/plan_local_G05_0.log} (100%) create mode 100644 agent-task/archive/2026/06/control_plane_phase_exit_followups/03_bootstrap_script_distribution/plan_local_G06_1.log delete mode 100644 agent-task/control_plane_phase_exit_followups/03_bootstrap_script_distribution/CODE_REVIEW-local-G05.md create mode 100644 services/core/internal/httpserver/bootstrap_provider.go create mode 100644 services/core/internal/httpserver/oto_agent_bootstrap.sh diff --git a/agent-task/archive/2026/06/control_plane_phase_exit_followups/03_bootstrap_script_distribution/code_review_local_G05_0.log b/agent-task/archive/2026/06/control_plane_phase_exit_followups/03_bootstrap_script_distribution/code_review_local_G05_0.log new file mode 100644 index 0000000..b5304c8 --- /dev/null +++ b/agent-task/archive/2026/06/control_plane_phase_exit_followups/03_bootstrap_script_distribution/code_review_local_G05_0.log @@ -0,0 +1,41 @@ +# Code review stub: bootstrap script distribution boundary + +## Review target + +- Plan: `PLAN-local-G05.md` +- Status: pending implementation + +## Reviewer focus + +- Confirm the bootstrap endpoint no longer depends on source checkout-relative paths. +- Confirm the chosen script source cannot silently drift without a test or explicit sync command. +- Confirm response headers and error behavior remain suitable for shell bootstrap usage. +- Confirm packaging/runtime assumptions are documented in code or tests, not tribal knowledge. + +## Required checks + +- `cd services/core && go test -count=1 ./...` + +## Notes + +This item should stay focused on distribution mechanics, not bootstrap script feature changes. + +## 코드리뷰 결과 + +- 종합 판정: FAIL +- 차원별 평가: + - correctness: Pass + - completeness: Fail + - test coverage: Fail + - API contract: Pass + - code quality: Pass + - plan deviation: Fail + - verification trust: Fail +- 발견된 문제: + - Required: `services/core/internal/httpserver/server_test.go:536`의 `TestEmbeddedBootstrapScriptMatchesRunnerAsset`는 이름과 주석상 runner asset과 embedded copy의 drift check를 주장하지만, 실제로는 embedded content가 비어 있지 않은지만 검사한다. 따라서 `apps/runner/assets/script/shell/oto_agent_bootstrap.sh`와 `services/core/internal/httpserver/oto_agent_bootstrap.sh`가 달라져도 테스트가 통과하며, 계획의 "chosen script source cannot silently drift" 요구를 충족하지 못한다. 테스트에서 runner asset 파일을 읽어 embedded provider bytes 또는 core copy와 동일함을 직접 비교해야 한다. + - Required: `CODE_REVIEW-local-G05.md:1`이 구현 체크리스트, 구현 항목별 완료 여부, 검증 결과, 사용자 리뷰 요청을 채우지 않은 pending stub 상태다. 이번 리뷰는 소스와 로컬 재실행으로 판단했지만, 후속 구현은 active review stub에 실제 완료 항목과 `cd services/core && go test -count=1 ./...` 결과를 기록해야 한다. +- 검증: + - `cd services/core && go test -count=1 ./...` 통과 + - `cmp -s apps/runner/assets/script/shell/oto_agent_bootstrap.sh services/core/internal/httpserver/oto_agent_bootstrap.sh` 현재 통과, 하지만 Go 테스트에 동등 비교가 포함되어 있지 않음 +- 다음 단계: + - WARN/FAIL 후속: drift 비교 테스트와 구현 증거 기록을 보강하는 후속 `PLAN-local-G06.md` / `CODE_REVIEW-local-G06.md`를 작성한다. diff --git a/agent-task/archive/2026/06/control_plane_phase_exit_followups/03_bootstrap_script_distribution/code_review_local_G06_1.log b/agent-task/archive/2026/06/control_plane_phase_exit_followups/03_bootstrap_script_distribution/code_review_local_G06_1.log new file mode 100644 index 0000000..a24e89d --- /dev/null +++ b/agent-task/archive/2026/06/control_plane_phase_exit_followups/03_bootstrap_script_distribution/code_review_local_G06_1.log @@ -0,0 +1,120 @@ + + +# Code Review Reference - REVIEW_BOOTSTRAP_SCRIPT_DISTRIBUTION_BOUNDARY + +> **[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-10 +task=control_plane_phase_exit_followups/03_bootstrap_script_distribution, plan=1, tag=REVIEW_BOOTSTRAP_SCRIPT_DISTRIBUTION_BOUNDARY + +## 이 파일을 읽는 리뷰 에이전트에게 + +> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다. + +각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요. +리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다. + +1. 판정을 append한다. +2. `CODE_REVIEW-local-G06.md` -> `code_review_local_G06_N.log`, `PLAN-local-G06.md` -> `plan_local_G06_M.log`로 아카이브한다. +3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/control_plane_phase_exit_followups/03_bootstrap_script_distribution/`로 이동한다. WARN/FAIL이면 user-review gate를 확인한 뒤 다음 active plan/review 파일 또는 `USER_REVIEW.md`를 작성한다. +4. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다. + +--- + +## 구현 항목별 완료 여부 + +| 항목 | 완료 여부 | +|------|---------| +| [REVIEW_BOOTSTRAP_SCRIPT_DISTRIBUTION_BOUNDARY-1] 실제 drift 비교 테스트 추가 | [x] | + +## 구현 체크리스트 + +- [x] `services/core/internal/httpserver/server_test.go`의 `TestEmbeddedBootstrapScriptMatchesRunnerAsset`가 `apps/runner/assets/script/shell/oto_agent_bootstrap.sh`와 `embeddedBootstrapProvider{}.bootstrapScript()` 결과를 직접 비교하도록 보강한다. +- [x] `cd services/core && go test -count=1 ./...`를 실행하고 실제 출력 요약을 `CODE_REVIEW-local-G06.md`에 기록한다. +- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. + +## 구현 내용 + +- 변경 파일: `services/core/internal/httpserver/server_test.go` +- 핵심 변경: + - `os` import 추가 + - `TestEmbeddedBootstrapScriptMatchesRunnerAsset`에 runner asset 파일 읽기(`os.ReadFile`)와 `bytes.Equal` 비교 추가. 기존 non-empty 단순 확인을 실제 byte-identical 비교로 교체. 실패 시 동기화 명령(`cp ...`)을 메시지에 포함. +- 계획 대비 변경 사항: 없음. 계획 요구 사항 그대로 구현. `TestEmbeddedBootstrapScriptNonEmpty`는 drift 비교 통과 시 중복이지만 독립 보증용으로 유지. + +## 검증 결과 + +- 실행한 명령: `cd services/core && go test -count=1 ./...` +- 결과: + ``` + ? github.com/toki/oto/services/core/cmd/oto-core [no test files] + ok github.com/toki/oto/services/core/internal/cicdstate 0.002s + ok github.com/toki/oto/services/core/internal/httpserver 0.095s + ok github.com/toki/oto/services/core/internal/runnerregistry 0.003s + ? github.com/toki/oto/services/core/oto [no test files] + ``` +- 생략/차단: 없음 + +## 사용자 리뷰 요청 + +_기본값은 `없음`이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 변경 없이는 안전하게 진행할 수 없으면 아래 항목을 실제 내용으로 교체하고, 구현을 중단한 뒤 active 파일을 그대로 둔 채 리뷰를 요청한다. 구현 에이전트는 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 증거 공백만으로는 사용자 리뷰 요청을 작성하지 않는다._ + +- 상태: 없음 +- 사유 유형: 없음 +- 결정 필요: 없음 +- 차단 근거: 없음 +- 실행한 검증/명령: 없음 +- 자동 후속 불가 이유: 없음 +- 재개 조건: 없음 + +## 코드리뷰 전용 체크리스트 + +> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다. +> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다. + +- [x] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다. +- [x] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다. +- [x] active `CODE_REVIEW-*-G??.md`를 `code_review_local_G06_N.log`로 아카이브한다. +- [x] active `PLAN-*-G??.md`를 `plan_local_G06_M.log`로 아카이브한다. +- [x] `.gitignore`의 Agent-Ops 관리 block이 `agent-task/**/*.md`와 `agent-task/**/*.log`를 unignore하고 `agent-roadmap/current.md`를 ignore하는지 확인한다. +- [x] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다. +- [x] PASS이면 active task 디렉터리 `agent-task/control_plane_phase_exit_followups/03_bootstrap_script_distribution/`를 `agent-task/archive/YYYY/MM/control_plane_phase_exit_followups/03_bootstrap_script_distribution/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다. + +## 소유권 + +| 섹션 | 소유자 | +|------|--------| +| 구현 항목별 완료 여부 | 구현 에이전트 | +| 구현 체크리스트 | 구현 에이전트 | +| 구현 내용 | 구현 에이전트 | +| 검증 결과 | 구현 에이전트 | +| 사용자 리뷰 요청 | 구현 에이전트 | +| 코드리뷰 결과 | 리뷰 에이전트 | +| 코드리뷰 전용 체크리스트 | 리뷰 에이전트 | + +## 코드리뷰 결과 + +- 종합 판정: PASS +- 차원별 평가: + - correctness: Pass + - completeness: Pass + - test coverage: Pass + - API contract: Pass + - code quality: Pass + - plan deviation: Pass + - verification trust: Pass +- 발견된 문제: 없음 +- 리뷰어 정리: + - `services/core/internal/httpserver/server_test.go`의 drift mismatch 실패 메시지에서 비ASCII 대시를 ASCII 하이픈으로 교체했다. 동작/계약 변경은 없다. +- 검증: + - `cd services/core && go test -count=1 ./...` 통과 +- 다음 단계: + - PASS: active plan/review를 로그로 아카이브하고 `complete.log`를 작성한 뒤 task 디렉터리를 archive로 이동한다. diff --git a/agent-task/archive/2026/06/control_plane_phase_exit_followups/03_bootstrap_script_distribution/complete.log b/agent-task/archive/2026/06/control_plane_phase_exit_followups/03_bootstrap_script_distribution/complete.log new file mode 100644 index 0000000..2a7a779 --- /dev/null +++ b/agent-task/archive/2026/06/control_plane_phase_exit_followups/03_bootstrap_script_distribution/complete.log @@ -0,0 +1,35 @@ +# Complete - control_plane_phase_exit_followups/03_bootstrap_script_distribution + +## 완료 일시 + +2026-06-10 + +## 요약 + +Bootstrap script distribution boundary follow-up completed after 2 review loops; final verdict PASS. + +## 루프 이력 + +| Plan | Review | Verdict | 메모 | +|------|--------|---------|------| +| `plan_local_G05_0.log` | `code_review_local_G05_0.log` | FAIL | Embedded bootstrap script drift test was named but did not compare runner asset bytes. | +| `plan_local_G06_1.log` | `code_review_local_G06_1.log` | PASS | Drift comparison test now reads the runner asset and compares it with embedded provider bytes. | + +## 구현/정리 내용 + +- Bootstrap script serving now uses an injected provider boundary instead of source checkout-relative path discovery. +- Production route uses a Go embedded bootstrap script provider. +- Handler tests cover provider-backed success, provider failure, method rejection, embedded script non-empty, and runner asset drift comparison. +- Review cleanup changed one test failure message dash to ASCII without behavior impact. + +## 최종 검증 + +- `cd services/core && go test -count=1 ./...` - PASS; `cmd/oto-core` and `services/core/oto` had no test files, and `internal/cicdstate`, `internal/httpserver`, `internal/runnerregistry` passed. + +## 잔여 Nit + +- 없음 + +## 후속 작업 + +- 없음 diff --git a/agent-task/control_plane_phase_exit_followups/03_bootstrap_script_distribution/PLAN-local-G05.md b/agent-task/archive/2026/06/control_plane_phase_exit_followups/03_bootstrap_script_distribution/plan_local_G05_0.log similarity index 100% rename from agent-task/control_plane_phase_exit_followups/03_bootstrap_script_distribution/PLAN-local-G05.md rename to agent-task/archive/2026/06/control_plane_phase_exit_followups/03_bootstrap_script_distribution/plan_local_G05_0.log diff --git a/agent-task/archive/2026/06/control_plane_phase_exit_followups/03_bootstrap_script_distribution/plan_local_G06_1.log b/agent-task/archive/2026/06/control_plane_phase_exit_followups/03_bootstrap_script_distribution/plan_local_G06_1.log new file mode 100644 index 0000000..2f72bcb --- /dev/null +++ b/agent-task/archive/2026/06/control_plane_phase_exit_followups/03_bootstrap_script_distribution/plan_local_G06_1.log @@ -0,0 +1,120 @@ + + +# Follow-up Plan - Bootstrap Script Drift Check + +## 이 파일을 읽는 구현 에이전트에게 + +이 계획은 이전 리뷰의 Required 항목만 처리한다. 구현을 마치면 active `CODE_REVIEW-local-G06.md`의 구현 에이전트 소유 섹션을 실제 변경 내용과 검증 출력으로 채운 뒤 active 파일을 그대로 두고 리뷰를 요청한다. + +구현 중 사용자만 결정할 수 있는 범위 변경, 사용자 소유 외부 환경/secret/service 준비, 또는 계획 충돌이 없으면 사용자에게 직접 질문하지 않는다. 그런 blocker가 생기면 `CODE_REVIEW-local-G06.md`의 `사용자 리뷰 요청` 섹션에 근거와 재개 조건을 기록하고 중단한다. `USER_REVIEW.md`, log archive, `complete.log` 작성은 코드리뷰 에이전트 전용이다. + +## 배경 + +첫 리뷰에서 provider 주입과 Go embed 방향은 확인되었지만, `TestEmbeddedBootstrapScriptMatchesRunnerAsset`가 실제 runner asset과 embedded script를 비교하지 않아 drift를 막지 못했다. 현재 두 파일은 동일하지만, 테스트가 동등성을 강제하지 않으면 runner asset과 core embedded copy가 조용히 갈라질 수 있다. 이번 후속은 drift 비교 테스트와 구현 증거 기록만 보강한다. + +## 사용자 리뷰 요청 흐름 + +구현 중 blocker가 생기면 active review stub의 `사용자 리뷰 요청` 섹션을 사용한다. 직접 사용자 프롬프트, 채팅 선택지 제시, `request_user_input` 호출은 금지된다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 증거 공백은 사용자 리뷰 요청이 아니라 `검증 결과`에 기록한다. + +## 분석 결과 + +### 읽은 파일 + +- `agent-ops/rules/project/rules.md` +- `agent-ops/rules/common/rules-roadmap.md` +- `agent-roadmap/current.md` +- `agent-roadmap/phase/independent-control-plane/PHASE.md` +- `agent-ops/skills/common/router.md` +- `agent-ops/skills/common/code-review/SKILL.md` +- `agent-ops/skills/common/plan/SKILL.md` +- `agent-ops/skills/common/_templates/implementation-user-review-request-section.md` +- `agent-test/local/rules.md` +- `agent-test/local/agent-smoke.md` +- `agent-test/local/core-smoke.md` +- `agent-task/control_plane_phase_exit_followups/03_bootstrap_script_distribution/plan_local_G05_0.log` +- `agent-task/control_plane_phase_exit_followups/03_bootstrap_script_distribution/code_review_local_G05_0.log` +- `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.sh` +- `apps/runner/assets/script/shell/oto_agent_bootstrap.sh` + +### 테스트 환경 규칙 + +- test_env: `local` +- `agent-test/local/rules.md`를 읽었다. +- `services/core/internal/httpserver/**`와 bootstrap script 경계 변경이므로 `agent-test/local/agent-smoke.md`를 적용한다. +- 적용 명령: `cd services/core && go test ./...`; 이번 계획에서는 캐시 회피를 위해 `cd services/core && go test -count=1 ./...`를 사용한다. +- `core-smoke`는 이전 계획에 기록되어 있어 확인했지만, 이번 변경의 직접 라우팅은 `agent-smoke`다. + +### 테스트 커버리지 공백 + +- Provider-backed 성공 응답, method not allowed, provider error는 기존 구현 테스트가 다룬다. +- Embedded script non-empty는 다루지만, `TestEmbeddedBootstrapScriptMatchesRunnerAsset`가 runner asset과 embedded bytes를 직접 비교하지 않아 drift를 잡지 못한다. + +### 심볼 참조 + +- 이름 변경/삭제 없음. +- `handleServeBootstrapScript` 호출부는 `routes.go`의 embedded provider 주입과 `server_test.go`의 static provider 주입으로 확인했다. + +### 분할 판단 + +- 단일 plan으로 진행한다. +- 변경 대상은 `server_test.go`의 drift check 테스트 한 곳과 필요한 import 보강에 한정된다. +- API/foundation 변경, call-site rollout, 독립 검증 단위 분리, 상이한 위험 프로필이 없어서 split이 오히려 불필요한 coordination을 만든다. + +### 범위 결정 근거 + +- `oto_agent_bootstrap.sh`의 내용, bootstrap command semantics, route path, provider interface, embed source 구조는 변경하지 않는다. +- drift 비교 테스트가 필요로 하는 최소 import와 assertion만 수정한다. +- 새 Makefile target은 만들지 않는다. Go test 안에서 runner asset과 embedded bytes를 직접 비교하는 방식으로 이전 계획의 drift check 요구를 만족시킨다. + +### 빌드 등급 + +- `local-G06`: 실패 원인이 명확하고 deterministic한 테스트 보강이지만, monorepo 상대 경로와 verification trust를 바로잡아야 하므로 이전 `G05`보다 한 단계 올린다. + +## 구현 체크리스트 + +- [ ] `services/core/internal/httpserver/server_test.go`의 `TestEmbeddedBootstrapScriptMatchesRunnerAsset`가 `apps/runner/assets/script/shell/oto_agent_bootstrap.sh`와 `embeddedBootstrapProvider{}.bootstrapScript()` 결과를 직접 비교하도록 보강한다. +- [ ] `cd services/core && go test -count=1 ./...`를 실행하고 실제 출력 요약을 `CODE_REVIEW-local-G06.md`에 기록한다. +- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. + +### [REVIEW_BOOTSTRAP_SCRIPT_DISTRIBUTION_BOUNDARY-1] 실제 drift 비교 테스트 추가 + +#### 문제 + +`services/core/internal/httpserver/server_test.go:536`의 `TestEmbeddedBootstrapScriptMatchesRunnerAsset`는 runner asset과 embedded copy의 drift check라고 설명하지만, 실제 assertion은 embedded content non-empty뿐이다. 현재 `cmp -s apps/runner/assets/script/shell/oto_agent_bootstrap.sh services/core/internal/httpserver/oto_agent_bootstrap.sh`는 통과하지만, Go test가 이 동등성을 보장하지 않는다. + +#### 해결 + +`TestEmbeddedBootstrapScriptMatchesRunnerAsset`에서 runner asset 파일을 읽고 embedded provider bytes와 직접 비교한다. `go test` 실행 cwd가 `services/core/internal/httpserver` package 디렉터리임을 기준으로 runner asset 상대 경로는 `../../../../apps/runner/assets/script/shell/oto_agent_bootstrap.sh`를 사용한다. + +비교 실패 시에는 core embedded copy를 runner asset에서 다시 동기화하라는 메시지를 남긴다. 별도 non-empty 테스트는 유지해도 되지만, 동등 비교 테스트만으로 non-empty가 충분히 검증되면 중복을 줄여도 된다. + +#### 체크리스트 + +- [ ] 필요한 경우 `os` 또는 표준 라이브러리 import를 추가한다. +- [ ] runner asset read 실패를 `t.Fatalf`로 보고한다. +- [ ] `bytes.Equal` 또는 동등한 byte 비교로 embedded bytes와 runner asset bytes를 비교한다. +- [ ] 실패 메시지에 동기화 대상 파일 경로를 포함한다. + +#### 테스트 결정 + +`cd services/core && go test -count=1 ./...`를 필수로 실행한다. 이 명령이 drift check 테스트를 포함해야 한다. + +## 수정 파일 요약 + +| 파일 | 항목 | +|------|------| +| `services/core/internal/httpserver/server_test.go` | `REVIEW_BOOTSTRAP_SCRIPT_DISTRIBUTION_BOUNDARY-1` | + +## 최종 검증 + +```bash +cd services/core && go test -count=1 ./... +``` + +예상 결과: 모든 services/core package 테스트가 통과하고, `services/core/internal/httpserver` package에 drift 비교 테스트가 포함된다. + +모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다. diff --git a/agent-task/control_plane_phase_exit_followups/03_bootstrap_script_distribution/CODE_REVIEW-local-G05.md b/agent-task/control_plane_phase_exit_followups/03_bootstrap_script_distribution/CODE_REVIEW-local-G05.md deleted file mode 100644 index dbce1c9..0000000 --- a/agent-task/control_plane_phase_exit_followups/03_bootstrap_script_distribution/CODE_REVIEW-local-G05.md +++ /dev/null @@ -1,21 +0,0 @@ -# Code review stub: bootstrap script distribution boundary - -## Review target - -- Plan: `PLAN-local-G05.md` -- Status: pending implementation - -## Reviewer focus - -- Confirm the bootstrap endpoint no longer depends on source checkout-relative paths. -- Confirm the chosen script source cannot silently drift without a test or explicit sync command. -- Confirm response headers and error behavior remain suitable for shell bootstrap usage. -- Confirm packaging/runtime assumptions are documented in code or tests, not tribal knowledge. - -## Required checks - -- `cd services/core && go test -count=1 ./...` - -## Notes - -This item should stay focused on distribution mechanics, not bootstrap script feature changes. diff --git a/services/core/internal/httpserver/bootstrap_provider.go b/services/core/internal/httpserver/bootstrap_provider.go new file mode 100644 index 0000000..24c4dfe --- /dev/null +++ b/services/core/internal/httpserver/bootstrap_provider.go @@ -0,0 +1,24 @@ +package httpserver + +import ( + _ "embed" + "fmt" +) + +//go:embed oto_agent_bootstrap.sh +var embeddedBootstrapScript []byte + +// bootstrapScriptProvider returns the content of the OTO agent bootstrap script. +type bootstrapScriptProvider interface { + bootstrapScript() ([]byte, error) +} + +// embeddedBootstrapProvider serves the script compiled into the binary via go:embed. +type embeddedBootstrapProvider struct{} + +func (embeddedBootstrapProvider) bootstrapScript() ([]byte, error) { + if len(embeddedBootstrapScript) == 0 { + return nil, fmt.Errorf("embedded bootstrap script is empty") + } + return embeddedBootstrapScript, nil +} diff --git a/services/core/internal/httpserver/oto_agent_bootstrap.sh b/services/core/internal/httpserver/oto_agent_bootstrap.sh new file mode 100644 index 0000000..64a377a --- /dev/null +++ b/services/core/internal/httpserver/oto_agent_bootstrap.sh @@ -0,0 +1,153 @@ +#!/usr/bin/env bash +set -euo pipefail + +server_url="" +agent_id="" +enrollment_token="" +release_base_url="" +agent_alias="" +install_dir="${HOME}/.oto/bin" +config_path="${HOME}/.oto/agent/config.yaml" +workspace_root="${HOME}/.oto/workspace" +log_dir="${HOME}/.oto/agent/log" +background="true" + +usage() { + echo "Usage: $0 --server-url --agent-id --enrollment-token --release-base-url [options]" + echo "Options:" + echo " --agent-alias Alias for the agent" + echo " --install-dir Directory to install OTO agent (default: \$HOME/.oto/bin)" + echo " --config-path Path to config file (default: \$HOME/.oto/agent/config.yaml)" + echo " --workspace-root Workspace root directory (default: \$HOME/.oto/workspace)" + echo " --log-dir Log directory (default: \$HOME/.oto/agent/log)" + echo " --no-background Run in the foreground" +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --server-url|--edge-url) + server_url="$2" + shift 2 + ;; + --agent-id) + agent_id="$2" + shift 2 + ;; + --enrollment-token) + enrollment_token="$2" + shift 2 + ;; + --release-base-url) + release_base_url="$2" + shift 2 + ;; + --agent-alias) + agent_alias="$2" + shift 2 + ;; + --install-dir) + install_dir="$2" + shift 2 + ;; + --config-path) + config_path="$2" + shift 2 + ;; + --workspace-root) + workspace_root="$2" + shift 2 + ;; + --log-dir) + log_dir="$2" + shift 2 + ;; + --no-background) + background="false" + shift 1 + ;; + *) + echo "Unknown option: $1" >&2 + usage + exit 1 + ;; + esac +done + +if [[ -z "$server_url" || -z "$agent_id" || -z "$enrollment_token" || -z "$release_base_url" ]]; then + echo "Error: Missing required arguments." >&2 + usage + exit 1 +fi + +case "$release_base_url" in + https://*) ;; + *) + echo "Error: --release-base-url must use https://" >&2 + exit 1 + ;; +esac + +arch=$(uname -m) +case "$arch" in + x86_64|amd64) + asset_name="oto-linux-x64.tar.gz" + ;; + aarch64|arm64) + asset_name="oto-linux-arm64.tar.gz" + ;; + *) + echo "Error: Unsupported architecture: $arch" >&2 + exit 1 + ;; +esac + +download_url="${release_base_url}/${asset_name}" +tmp_dir=$(mktemp -d) +trap 'rm -rf "$tmp_dir"' EXIT + +echo "Downloading OTO agent..." +curl -fsSL "$download_url" -o "$tmp_dir/$asset_name" + +echo "Extracting OTO agent..." +tar -xzf "$tmp_dir/$asset_name" -C "$tmp_dir" + +mkdir -p "$install_dir" +cp "$tmp_dir/oto" "$install_dir/oto" +chmod +x "$install_dir/oto" + +config_dir=$(dirname "$config_path") +agent_state_dir="${HOME}/.oto/agent" +pid_path="${agent_state_dir}/oto-agent.pid" + +mkdir -p "$config_dir" +mkdir -p "$agent_state_dir" +mkdir -p "$workspace_root" +mkdir -p "$log_dir" + +cat < "$config_path" +agent: + id: "$agent_id" + alias: "$agent_alias" + enrollment_token: "$enrollment_token" +server: + url: "$server_url" +runtime: + install_dir: "$install_dir" + workspace_root: "$workspace_root" + log_dir: "$log_dir" +EOF + +chmod 600 "$config_path" + +if [ "$background" = "true" ]; then + echo "Starting OTO agent in the background..." + nohup "$install_dir/oto" agent run --config "$config_path" >> "$log_dir/oto-agent.log" 2>&1 & + pid=$! + echo "$pid" > "$pid_path" + echo "OTO agent started in the background. PID: $pid" + echo "Log path: $log_dir/oto-agent.log" +else + echo "OTO agent is configured to run in the foreground." + echo "To run it manually, use the following command:" + echo " $install_dir/oto agent run --config $config_path" +fi diff --git a/services/core/internal/httpserver/routes.go b/services/core/internal/httpserver/routes.go index af23e13..3cdf38a 100644 --- a/services/core/internal/httpserver/routes.go +++ b/services/core/internal/httpserver/routes.go @@ -19,7 +19,7 @@ func registerRoutes(mux *http.ServeMux, registry *runnerregistry.Registry, store mux.HandleFunc("/api/v1/runners/{id}/heartbeat", handleRunnerHeartbeat(registry)) mux.HandleFunc("/api/v1/runners/{id}/disconnect", handleRunnerDisconnect(registry)) mux.HandleFunc("/api/v1/runners/{id}", handleGetRunner(registry)) - mux.HandleFunc("/bootstrap/oto-agent.sh", handleServeBootstrapScript()) + mux.HandleFunc("/bootstrap/oto-agent.sh", handleServeBootstrapScript(embeddedBootstrapProvider{})) mux.HandleFunc("/api/v1/", handleRouter(store, registry)) } diff --git a/services/core/internal/httpserver/runner_handlers.go b/services/core/internal/httpserver/runner_handlers.go index 6e0d11f..63e5527 100644 --- a/services/core/internal/httpserver/runner_handlers.go +++ b/services/core/internal/httpserver/runner_handlers.go @@ -252,27 +252,15 @@ func handleRunnerBootstrapCommand(registry *runnerregistry.Registry) http.Handle } } -func handleServeBootstrapScript() http.HandlerFunc { +func handleServeBootstrapScript(provider bootstrapScriptProvider) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed) return } - paths := []string{ - "../../apps/runner/assets/script/shell/oto_agent_bootstrap.sh", - "../../../../apps/runner/assets/script/shell/oto_agent_bootstrap.sh", - "apps/runner/assets/script/shell/oto_agent_bootstrap.sh", - } - var content []byte - var err error - for _, p := range paths { - content, err = os.ReadFile(p) - if err == nil { - break - } - } + content, err := provider.bootstrapScript() if err != nil { - http.Error(w, "Bootstrap script not found: "+err.Error(), http.StatusNotFound) + http.Error(w, "Bootstrap script not available: "+err.Error(), http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/x-sh") diff --git a/services/core/internal/httpserver/server_test.go b/services/core/internal/httpserver/server_test.go index b308e2d..c435d25 100644 --- a/services/core/internal/httpserver/server_test.go +++ b/services/core/internal/httpserver/server_test.go @@ -4,9 +4,11 @@ import ( "bytes" "context" "encoding/json" + "fmt" "net" "net/http" "net/http/httptest" + "os" "strings" "testing" "time" @@ -486,11 +488,19 @@ func TestHandleRunnerBootstrapCommand(t *testing.T) { } } +// staticProvider is a test-only bootstrapScriptProvider with fixed content. +type staticProvider struct { + content []byte + err error +} + +func (p staticProvider) bootstrapScript() ([]byte, error) { return p.content, p.err } + func TestHandleServeBootstrapScript(t *testing.T) { req := httptest.NewRequest(http.MethodGet, "/bootstrap/oto-agent.sh", nil) rr := httptest.NewRecorder() - handleServeBootstrapScript()(rr, req) + handleServeBootstrapScript(staticProvider{content: []byte("#!/usr/bin/env bash\n")})(rr, req) if rr.Code != http.StatusOK { t.Fatalf("status = %v, want %v; body = %s", rr.Code, http.StatusOK, rr.Body.String()) } @@ -504,6 +514,59 @@ func TestHandleServeBootstrapScript(t *testing.T) { } } +func TestHandleServeBootstrapScript_MethodNotAllowed(t *testing.T) { + req := httptest.NewRequest(http.MethodPost, "/bootstrap/oto-agent.sh", nil) + rr := httptest.NewRecorder() + + handleServeBootstrapScript(staticProvider{content: []byte("#!/usr/bin/env bash\n")})(rr, req) + if rr.Code != http.StatusMethodNotAllowed { + t.Fatalf("status = %v, want %v", rr.Code, http.StatusMethodNotAllowed) + } +} + +func TestHandleServeBootstrapScript_ProviderError(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/bootstrap/oto-agent.sh", nil) + rr := httptest.NewRecorder() + + handleServeBootstrapScript(staticProvider{err: fmt.Errorf("script unavailable")})(rr, req) + if rr.Code != http.StatusInternalServerError { + t.Fatalf("status = %v, want %v", rr.Code, http.StatusInternalServerError) + } +} + +func TestEmbeddedBootstrapScriptMatchesRunnerAsset(t *testing.T) { + // Drift check: embedded copy must be byte-identical to the runner asset. + // On failure: cp apps/runner/assets/script/shell/oto_agent_bootstrap.sh \ + // services/core/internal/httpserver/oto_agent_bootstrap.sh + embedded, err := embeddedBootstrapProvider{}.bootstrapScript() + if err != nil { + t.Fatalf("embeddedBootstrapProvider.bootstrapScript() failed: %v", err) + } + + const runnerAsset = "../../../../apps/runner/assets/script/shell/oto_agent_bootstrap.sh" + asset, err := os.ReadFile(runnerAsset) + if err != nil { + t.Fatalf("failed to read runner asset %s: %v", runnerAsset, err) + } + + if !bytes.Equal(embedded, asset) { + t.Fatalf("embedded bootstrap script differs from runner asset %s - "+ + "run: cp %s services/core/internal/httpserver/oto_agent_bootstrap.sh", + runnerAsset, runnerAsset) + } +} + +func TestEmbeddedBootstrapScriptNonEmpty(t *testing.T) { + provider := embeddedBootstrapProvider{} + content, err := provider.bootstrapScript() + if err != nil { + t.Fatalf("bootstrapScript() error: %v", err) + } + if len(content) == 0 { + t.Fatal("embedded bootstrap script content is empty") + } +} + func TestHandleCreateJob(t *testing.T) { store := cicdstate.NewStore()