diff --git a/agent-roadmap/phase/automation-runtime-bridge/milestones/openai-workspace-agent-execution-contract.md b/agent-roadmap/phase/automation-runtime-bridge/milestones/openai-workspace-agent-execution-contract.md index d83cd98..cd9ba54 100644 --- a/agent-roadmap/phase/automation-runtime-bridge/milestones/openai-workspace-agent-execution-contract.md +++ b/agent-roadmap/phase/automation-runtime-bridge/milestones/openai-workspace-agent-execution-contract.md @@ -1,4 +1,4 @@ -# Milestone: OpenAI Workspace Agent Execution Contract +현재# Milestone: OpenAI Workspace Agent Execution Contract ## 위치 diff --git a/agent-task/m-openai-workspace-agent-execution-contract/01_route_catalog_workspace_bound/CODE_REVIEW-local-G05.md b/agent-task/m-openai-workspace-agent-execution-contract/01_route_catalog_workspace_bound/CODE_REVIEW-local-G05.md new file mode 100644 index 0000000..b5b27f5 --- /dev/null +++ b/agent-task/m-openai-workspace-agent-execution-contract/01_route_catalog_workspace_bound/CODE_REVIEW-local-G05.md @@ -0,0 +1,143 @@ + + +# Code Review Reference - 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-13 +task=m-openai-workspace-agent-execution-contract/01_route_catalog_workspace_bound, plan=0, tag=API + +## Roadmap Targets + +- Milestone: `agent-roadmap/phase/automation-runtime-bridge/milestones/openai-workspace-agent-execution-contract.md` +- Task ids: + - `route-catalog`: 외부 `model` route가 내부 `adapter + target`으로 해석되는 기준을 config와 smoke fixture에 남긴다. +- Completion mode: check-on-pass + +## 이 파일을 읽는 리뷰 에이전트에게 + +> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다. + +각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요. +리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다. + +1. 판정을 append한다. +2. `CODE_REVIEW-local-G05.md` -> `code_review_local_G05_N.log`, `PLAN-local-G05.md` -> `plan_local_G05_M.log`로 아카이브한다. +3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-openai-workspace-agent-execution-contract/01_route_catalog_workspace_bound/`로 이동한다. WARN/FAIL이면 user-review gate를 확인한 뒤 다음 active plan/review 파일 또는 `USER_REVIEW.md`를 작성한다. `USER_REVIEW.md`가 사용자 결정으로 완료/PASS 해소되면 code-review가 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log` 작성 후 archive 이동한다. +4. PASS이고 task group이 `m-`이면 완료 이벤트 메타데이터를 보고한다. roadmap 상태 체크와 `update-roadmap` 호출은 런타임 책임이다. +5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다. + +--- + +## 구현 항목별 완료 여부 + +| 항목 | 완료 여부 | +|------|---------| +| [API-1] Route Catalog Workspace-Bound Flag | [ ] | +| [API-2] Config And Smoke Fixture Route Catalog Evidence | [ ] | + +## 구현 체크리스트 + +- [ ] `OpenAIRouteEntry`와 route dispatch에 workspace-bound agent route 표식을 추가하고 config load test를 갱신한다. +- [ ] `configs/edge.yaml`와 OpenAI smoke fixture가 route catalog 기준을 보여주도록 갱신한다. +- [ ] `go test ./packages/go/config ./apps/edge/internal/openai`와 `./scripts/e2e-openai-ollama.sh`를 실행한다. +- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. + +## 코드리뷰 전용 체크리스트 + +> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다. +> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다. + +- [ ] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다. +- [ ] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다. +- [ ] active `CODE_REVIEW-*-G??.md`를 `code_review_local_G05_N.log`로 아카이브한다. +- [ ] active `PLAN-*-G??.md`를 `plan_local_G05_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/m-openai-workspace-agent-execution-contract/01_route_catalog_workspace_bound/`를 `agent-task/archive/YYYY/MM/m-openai-workspace-agent-execution-contract/01_route_catalog_workspace_bound/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다. +- [ ] PASS이고 task group이 `m-`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다. +- [ ] PASS split 작업이면 이동 후 빈 active parent `agent-task/m-openai-workspace-agent-execution-contract/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다. +- [ ] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-local-G05.md`와 `CODE_REVIEW-local-G05.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로 이동한다. + +## 계획 대비 변경 사항 + +_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._ + +## 주요 설계 결정 + +_구현 에이전트가 주요 설계 결정 사항을 기록한다._ + +## 사용자 리뷰 요청 + +_기본값은 `없음`이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 변경 없이는 안전하게 진행할 수 없으면 아래 항목을 실제 내용으로 교체하고, 구현을 중단한 뒤 active 파일을 그대로 둔 채 리뷰를 요청한다. 구현 에이전트는 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 증거 공백만으로는 사용자 리뷰 요청을 작성하지 않는다._ + +- 상태: 없음 +- 사유 유형: 없음 +- 결정 필요: 없음 +- 차단 근거: 없음 +- 실행한 검증/명령: 없음 +- 자동 후속 불가 이유: 없음 +- 재개 조건: 없음 + +## 리뷰어를 위한 체크포인트 + +- `workspace_required`가 route catalog entry의 명시 필드이며 fallback route 동작을 바꾸지 않는지 확인한다. +- `scripts/e2e-openai-ollama.sh`가 route catalog를 실제로 사용하면서 workspace-required agent route로 오인되지 않는지 확인한다. +- Roadmap `route-catalog` Task 외 범위가 섞이지 않았는지 확인한다. + +## 검증 결과 + +_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._ + +필수 규칙: +- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다. +- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다. +- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다. +- 사용자 리뷰 요청으로 명령을 끝까지 실행하지 못했다면 `사용자 리뷰 요청`에 실행한 명령, 실제 출력, 미실행 명령의 사유를 기록한다. +- mobile/UI hang, timeout, 또는 2분 무진행은 blind retry를 중단하고 focused rerun 명령과 screenshot/window/UI-tree evidence path를 남기며, 불가능하면 정확한 사유를 남긴다. + +### API-1 중간 검증 +```bash +$ go test ./packages/go/config ./apps/edge/internal/openai +(output) +``` + +### API-2 중간 검증 +```bash +$ ./scripts/e2e-openai-ollama.sh +(output) +``` + +### 최종 검증 +```bash +$ go test ./packages/go/config ./apps/edge/internal/openai +$ ./scripts/e2e-openai-ollama.sh +(output) +``` + +--- + +## 섹션 소유권 + +| Section | Owner | Note | +|---------|-------|------| +| Header comment, 개요, 리뷰 에이전트 지시 | Fixed at stub creation | Implementing agent must not modify or execute these. | +| Roadmap Targets | Fixed at stub creation from plan | Implementing agent must not modify; code-review copies it into `complete.log` as `Roadmap Completion` only on PASS. | +| 구현 항목별 완료 여부 | Implementing agent | Check `[ ]` to `[x]` only. | +| 구현 체크리스트 | Implementing agent | Check `[ ]` to `[x]` only; final checkbox is mandatory before saving. | +| 코드리뷰 전용 체크리스트 | Review agent only | Implementing agent must not modify or check this section. | +| 계획 대비 변경 사항, 주요 설계 결정 | Implementing agent | Replace placeholder text with actual content. | +| 사용자 리뷰 요청 | Implementing agent | Keep `상태: 없음` unless user input is required to proceed. | +| 리뷰어를 위한 체크포인트 | Fixed at stub creation | Review focus areas from the plan. | +| 검증 결과 | Implementing agent | Paste actual stdout/stderr for the commands above. | diff --git a/agent-task/m-openai-workspace-agent-execution-contract/01_route_catalog_workspace_bound/PLAN-local-G05.md b/agent-task/m-openai-workspace-agent-execution-contract/01_route_catalog_workspace_bound/PLAN-local-G05.md new file mode 100644 index 0000000..b22da56 --- /dev/null +++ b/agent-task/m-openai-workspace-agent-execution-contract/01_route_catalog_workspace_bound/PLAN-local-G05.md @@ -0,0 +1,198 @@ + + +# Plan - API + +## 이 파일을 읽는 구현 에이전트에게 + +`CODE_REVIEW-local-G05.md`의 구현 에이전트 소유 섹션을 채우는 것까지가 구현이다. 코드를 수정하고 검증을 실행한 뒤 실제 변경 내용, 명령 출력, 계획 대비 변경 사항을 기록하고 active 파일을 그대로 둔 채 리뷰 준비를 보고한다. 사용자 결정, 사용자 소유 외부 환경, scope 충돌로 막히면 사용자에게 직접 묻지 말고 review stub의 `사용자 리뷰 요청` 섹션에 근거와 재개 조건을 채운다. 최종 판정, log rename, `complete.log`, archive 이동은 code-review 전용이다. + +## 배경 + +`metadata-schema` 선행 작업으로 `metadata.workspace`는 Edge service의 `RunRequest.Workspace`까지 전달된다. 남은 `route-catalog` Task는 외부 `model`이 내부 `adapter + target`으로 해석될 때 어떤 route가 workspace-bound agent target인지 config와 smoke fixture에 남겨야 한다. 이 표식이 있어야 후속 `workspace-required` Task가 inference route는 유지하면서 agent route에만 workspace 필수 조건을 적용할 수 있다. + +## 사용자 리뷰 요청 흐름 + +구현 중 사용자 결정이 필요하면 `agent-ops/skills/common/_templates/implementation-user-review-request-section.md` 형식으로 active `CODE_REVIEW-local-G05.md`의 `사용자 리뷰 요청` 섹션을 채운다. 구현 에이전트는 채팅으로 직접 질문하거나 `request_user_input`을 호출하지 않는다. code-review가 요청 타당성을 검증하고 실제 `USER_REVIEW.md` 작성 여부를 결정한다. + +## Roadmap Targets + +- Milestone: `agent-roadmap/phase/automation-runtime-bridge/milestones/openai-workspace-agent-execution-contract.md` +- Task ids: + - `route-catalog`: 외부 `model` route가 내부 `adapter + target`으로 해석되는 기준을 config와 smoke fixture에 남긴다. +- Completion mode: check-on-pass + +## 분석 결과 + +### 읽은 파일 + +- `agent-roadmap/current.md` +- `agent-roadmap/phase/automation-runtime-bridge/PHASE.md` +- `agent-roadmap/phase/automation-runtime-bridge/milestones/openai-workspace-agent-execution-contract.md` +- `agent-contract/index.md` +- `agent-contract/provided/openai-compatible-api.md` +- `agent-ops/rules/project/domain/edge/rules.md` +- `agent-ops/rules/project/domain/platform-common/rules.md` +- `agent-ops/rules/project/domain/testing/rules.md` +- `agent-test/local/rules.md` +- `agent-test/local/edge-smoke.md` +- `agent-test/local/platform-common-smoke.md` +- `packages/go/config/config.go` +- `packages/go/config/config_test.go` +- `configs/edge.yaml` +- `configs/edge-compose.yaml.tmpl` +- `scripts/e2e-openai-ollama.sh` +- `apps/edge/internal/openai/chat_handler.go` +- `apps/edge/internal/openai/responses_handler.go` +- `apps/edge/internal/openai/routes.go` +- `apps/edge/internal/openai/server_test.go` +- `apps/edge/internal/edgecmd/smoke_openai.go` + +### 테스트 환경 규칙 + +`test_env=local`을 선택했다. `agent-test/local/rules.md`를 읽었고, edge 변경 기준으로 `agent-test/local/edge-smoke.md`, config 변경 기준으로 `agent-test/local/platform-common-smoke.md`를 읽었다. 적용 명령은 `go test ./packages/go/config ./apps/edge/internal/openai`, 최종 smoke는 OpenAI 입력 표면 기준 `./scripts/e2e-openai-ollama.sh`다. 규칙 파일에 구조적 blank나 `<확인 필요>` 값은 없었다. + +### 테스트 커버리지 공백 + +route catalog dispatch는 `apps/edge/internal/openai/server_test.go`에 있지만 workspace-bound route 표식은 현재 없다. config load는 `packages/go/config/config_test.go`에 route catalog test가 있지만 `workspace_required` field coverage가 없다. `scripts/e2e-openai-ollama.sh`는 현재 top-level `target/models` fallback을 주로 쓰므로 route catalog smoke fixture coverage가 약하다. + +### 심볼 참조 + +renamed/removed symbols: none. + +### 분할 판단 + +split decision policy를 평가했다. 이 task group은 `agent-task/m-openai-workspace-agent-execution-contract/`이고 sibling은 `01_route_catalog_workspace_bound`와 `02+01_workspace_required_enforcement`다. `01_route_catalog_workspace_bound`는 선행 config/fixture 계약 작업이며 dependency가 없다. `02+01_workspace_required_enforcement`는 route catalog 표식을 소비하므로 predecessor `01`의 `complete.log`가 필요하다. + +### 범위 결정 근거 + +이 계획은 route catalog에 workspace-bound 표식을 추가하고 config/smoke fixture에 남기는 데 한정한다. `metadata.workspace` 파싱은 이미 작은 선행 변경으로 처리되었으므로 확장하지 않는다. workspace 누락/상대 경로 거부는 후속 `02+01_workspace_required_enforcement`가 맡는다. Node cwd 적용, session workspace isolation, NomadCode 호출 shape는 다른 Epic/Task 범위라 제외한다. + +### 빌드 등급 + +build lane은 `local-G05`다. config schema, YAML fixture, focused Go tests로 검증 가능하지만 public config 계약에 닿아 중간 등급으로 둔다. + +## 구현 체크리스트 + +- [ ] `OpenAIRouteEntry`와 route dispatch에 workspace-bound agent route 표식을 추가하고 config load test를 갱신한다. +- [ ] `configs/edge.yaml`와 OpenAI smoke fixture가 route catalog 기준을 보여주도록 갱신한다. +- [ ] `go test ./packages/go/config ./apps/edge/internal/openai`와 `./scripts/e2e-openai-ollama.sh`를 실행한다. +- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. + +### [API-1] Route Catalog Workspace-Bound Flag + +문제: `packages/go/config/config.go:90`의 `OpenAIRouteEntry`는 external model을 adapter/target으로 매핑하지만 workspace-bound agent target 여부를 표현하지 못한다. `apps/edge/internal/openai/chat_handler.go:156`의 `routeDispatch`도 이 정보를 후속 검증 로직에 전달할 필드가 없다. + +해결 방법: `workspace_required` YAML/mapstructure field를 `OpenAIRouteEntry`에 추가하고, route catalog match path에서 `routeDispatch.WorkspaceRequired`로 복사한다. fallback route는 `false`를 유지한다. + +Before: + +```go +// packages/go/config/config.go:90 +type OpenAIRouteEntry struct { + Model string `mapstructure:"model" yaml:"model"` + NodeRef string `mapstructure:"node" yaml:"node,omitempty"` + Adapter string `mapstructure:"adapter" yaml:"adapter,omitempty"` + Target string `mapstructure:"target" yaml:"target"` + SessionID string `mapstructure:"session_id" yaml:"session_id,omitempty"` + TimeoutSec int `mapstructure:"timeout_sec" yaml:"timeout_sec,omitempty"` +} +``` + +After: + +```go +type OpenAIRouteEntry struct { + Model string `mapstructure:"model" yaml:"model"` + NodeRef string `mapstructure:"node" yaml:"node,omitempty"` + Adapter string `mapstructure:"adapter" yaml:"adapter,omitempty"` + Target string `mapstructure:"target" yaml:"target"` + SessionID string `mapstructure:"session_id" yaml:"session_id,omitempty"` + TimeoutSec int `mapstructure:"timeout_sec" yaml:"timeout_sec,omitempty"` + WorkspaceRequired bool `mapstructure:"workspace_required" yaml:"workspace_required,omitempty"` +} +``` + +수정 파일 및 체크리스트: + +- [ ] `packages/go/config/config.go`: `OpenAIRouteEntry.WorkspaceRequired` 추가. +- [ ] `packages/go/config/config_test.go`: `TestLoadEdge_OpenAIRouteCatalog`에 `workspace_required: true` fixture와 assertion 추가. +- [ ] `apps/edge/internal/openai/chat_handler.go`: `routeDispatch.WorkspaceRequired` 추가, catalog match에서 복사, fallback false 유지. +- [ ] `apps/edge/internal/openai/server_test.go`: route dispatch가 `workspace_required`를 보존하는 package-level test 추가. + +테스트 작성: 작성한다. config load test는 YAML parsing을, openai package test는 dispatch DTO 보존을 검증한다. + +중간 검증: + +```bash +go test ./packages/go/config ./apps/edge/internal/openai +``` + +기대 결과: 두 패키지 모두 PASS. + +### [API-2] Config And Smoke Fixture Route Catalog Evidence + +문제: `configs/edge.yaml:56`의 `model_routes` 예시는 inference provider route만 보여주고 `model: "codex"` 같은 workspace-bound agent route 기준을 남기지 않는다. `scripts/e2e-openai-ollama.sh:123`의 smoke config는 `target/models` fallback 중심이라 route catalog smoke evidence가 약하다. + +해결 방법: `configs/edge.yaml` 주석 예시에 CLI agent route를 추가하고 `workspace_required: true`를 명시한다. `scripts/e2e-openai-ollama.sh`의 임시 config에 `model_routes`를 추가해 `/v1/models`, Chat Completions, Responses가 catalog route를 통과하게 하되 Ollama smoke route는 workspace-required로 만들지 않는다. + +Before: + +```yaml +# configs/edge.yaml:59 +# model_routes: +# - model: "llama3" +# adapter: "ollama" +# target: "llama3:8b" +``` + +After: + +```yaml +# model_routes: +# - model: "codex" +# adapter: "cli" +# target: "codex" +# workspace_required: true +# - model: "llama3" +# adapter: "ollama" +# target: "llama3:8b" +``` + +수정 파일 및 체크리스트: + +- [ ] `configs/edge.yaml`: codex workspace-bound route 예시 추가. +- [ ] `scripts/e2e-openai-ollama.sh`: temp `openai.model_routes` fixture 추가, model assertions/request model이 catalog route를 검증하도록 조정. +- [ ] `apps/edge/README.md`: route catalog 설명이 `workspace_required`를 언급하도록 필요한 최소 문장만 갱신. + +테스트 작성: 별도 단위 테스트보다 기존 smoke script를 route catalog fixture로 바꾸는 것이 검증이다. + +중간 검증: + +```bash +./scripts/e2e-openai-ollama.sh +``` + +기대 결과: `OpenAI-compatible Ollama serving test PASSED.` 출력. + +## 수정 파일 요약 + +| 파일 | 항목 | +|------|------| +| `packages/go/config/config.go` | API-1 | +| `packages/go/config/config_test.go` | API-1 | +| `apps/edge/internal/openai/chat_handler.go` | API-1 | +| `apps/edge/internal/openai/server_test.go` | API-1 | +| `configs/edge.yaml` | API-2 | +| `scripts/e2e-openai-ollama.sh` | API-2 | +| `apps/edge/README.md` | API-2 | + +## 최종 검증 + +```bash +go test ./packages/go/config ./apps/edge/internal/openai +./scripts/e2e-openai-ollama.sh +``` + +Go test cache output is acceptable for unchanged packages; if implementation changes test-sensitive logic and fresh execution is needed, use `go test -count=1 ./packages/go/config ./apps/edge/internal/openai` and record the substitution. + +모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다. diff --git a/agent-task/m-openai-workspace-agent-execution-contract/02+01_workspace_required_enforcement/CODE_REVIEW-local-G06.md b/agent-task/m-openai-workspace-agent-execution-contract/02+01_workspace_required_enforcement/CODE_REVIEW-local-G06.md new file mode 100644 index 0000000..33f5824 --- /dev/null +++ b/agent-task/m-openai-workspace-agent-execution-contract/02+01_workspace_required_enforcement/CODE_REVIEW-local-G06.md @@ -0,0 +1,144 @@ + + +# Code Review Reference - 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-13 +task=m-openai-workspace-agent-execution-contract/02+01_workspace_required_enforcement, plan=0, tag=API + +## Roadmap Targets + +- Milestone: `agent-roadmap/phase/automation-runtime-bridge/milestones/openai-workspace-agent-execution-contract.md` +- Task ids: + - `workspace-required`: 내부 실행 route가 workspace-bound agent target이면 workspace가 비어 있거나 상대 경로일 때 OpenAI-compatible error로 거부한다. +- Completion mode: check-on-pass + +## 이 파일을 읽는 리뷰 에이전트에게 + +> **[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/m-openai-workspace-agent-execution-contract/02+01_workspace_required_enforcement/`로 이동한다. WARN/FAIL이면 user-review gate를 확인한 뒤 다음 active plan/review 파일 또는 `USER_REVIEW.md`를 작성한다. `USER_REVIEW.md`가 사용자 결정으로 완료/PASS 해소되면 code-review가 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log` 작성 후 archive 이동한다. +4. PASS이고 task group이 `m-`이면 완료 이벤트 메타데이터를 보고한다. roadmap 상태 체크와 `update-roadmap` 호출은 런타임 책임이다. +5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다. + +--- + +## 구현 항목별 완료 여부 + +| 항목 | 완료 여부 | +|------|---------| +| [API-1] Workspace-Required Validation Helper | [ ] | +| [API-2] Endpoint Regression Tests For Required And Non-Required Routes | [ ] | + +## 구현 체크리스트 + +- [ ] predecessor `01_route_catalog_workspace_bound`의 `complete.log`가 active 또는 archive 위치에 있는지 확인한다. +- [ ] workspace-bound route에서 workspace가 빈 값 또는 상대 경로이면 `/v1/responses`와 `/v1/chat/completions` 모두 OpenAI-compatible 400 error를 반환하게 한다. +- [ ] workspace가 필요 없는 inference/fallback route는 기존 동작을 유지하는 회귀 test를 추가한다. +- [ ] `go test ./apps/edge/internal/openai ./apps/edge/internal/service`와 `./scripts/e2e-openai-ollama.sh`를 실행한다. +- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. + +## 코드리뷰 전용 체크리스트 + +> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다. +> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다. + +- [ ] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다. +- [ ] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다. +- [ ] active `CODE_REVIEW-*-G??.md`를 `code_review_local_G06_N.log`로 아카이브한다. +- [ ] active `PLAN-*-G??.md`를 `plan_local_G06_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/m-openai-workspace-agent-execution-contract/02+01_workspace_required_enforcement/`를 `agent-task/archive/YYYY/MM/m-openai-workspace-agent-execution-contract/02+01_workspace_required_enforcement/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다. +- [ ] PASS이고 task group이 `m-`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다. +- [ ] PASS split 작업이면 이동 후 빈 active parent `agent-task/m-openai-workspace-agent-execution-contract/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다. +- [ ] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-local-G06.md`와 `CODE_REVIEW-local-G06.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로 이동한다. + +## 계획 대비 변경 사항 + +_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._ + +## 주요 설계 결정 + +_구현 에이전트가 주요 설계 결정 사항을 기록한다._ + +## 사용자 리뷰 요청 + +_기본값은 `없음`이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 변경 없이는 안전하게 진행할 수 없으면 아래 항목을 실제 내용으로 교체하고, 구현을 중단한 뒤 active 파일을 그대로 둔 채 리뷰를 요청한다. 구현 에이전트는 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 증거 공백만으로는 사용자 리뷰 요청을 작성하지 않는다._ + +- 상태: 없음 +- 사유 유형: 없음 +- 결정 필요: 없음 +- 차단 근거: 없음 +- 실행한 검증/명령: 없음 +- 자동 후속 불가 이유: 없음 +- 재개 조건: 없음 + +## 리뷰어를 위한 체크포인트 + +- predecessor `01_route_catalog_workspace_bound`의 completion evidence 없이 구현되지 않았는지 확인한다. +- workspace-required route만 400을 반환하고 inference/fallback route는 기존 동작을 유지하는지 확인한다. +- workspace 검증이 existence/permission/cwd 적용 범위까지 확장되지 않았는지 확인한다. + +## 검증 결과 + +_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._ + +필수 규칙: +- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다. +- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다. +- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다. +- 사용자 리뷰 요청으로 명령을 끝까지 실행하지 못했다면 `사용자 리뷰 요청`에 실행한 명령, 실제 출력, 미실행 명령의 사유를 기록한다. +- mobile/UI hang, timeout, 또는 2분 무진행은 blind retry를 중단하고 focused rerun 명령과 screenshot/window/UI-tree evidence path를 남기며, 불가능하면 정확한 사유를 남긴다. + +### API-1 중간 검증 +```bash +$ go test ./apps/edge/internal/openai +(output) +``` + +### API-2 중간 검증 +```bash +$ go test ./apps/edge/internal/openai ./apps/edge/internal/service +(output) +``` + +### 최종 검증 +```bash +$ go test ./apps/edge/internal/openai ./apps/edge/internal/service +$ ./scripts/e2e-openai-ollama.sh +(output) +``` + +--- + +## 섹션 소유권 + +| Section | Owner | Note | +|---------|-------|------| +| Header comment, 개요, 리뷰 에이전트 지시 | Fixed at stub creation | Implementing agent must not modify or execute these. | +| Roadmap Targets | Fixed at stub creation from plan | Implementing agent must not modify; code-review copies it into `complete.log` as `Roadmap Completion` only on PASS. | +| 구현 항목별 완료 여부 | Implementing agent | Check `[ ]` to `[x]` only. | +| 구현 체크리스트 | Implementing agent | Check `[ ]` to `[x]` only; final checkbox is mandatory before saving. | +| 코드리뷰 전용 체크리스트 | Review agent only | Implementing agent must not modify or check this section. | +| 계획 대비 변경 사항, 주요 설계 결정 | Implementing agent | Replace placeholder text with actual content. | +| 사용자 리뷰 요청 | Implementing agent | Keep `상태: 없음` unless user input is required to proceed. | +| 리뷰어를 위한 체크포인트 | Fixed at stub creation | Review focus areas from the plan. | +| 검증 결과 | Implementing agent | Paste actual stdout/stderr for the commands above. | diff --git a/agent-task/m-openai-workspace-agent-execution-contract/02+01_workspace_required_enforcement/PLAN-local-G06.md b/agent-task/m-openai-workspace-agent-execution-contract/02+01_workspace_required_enforcement/PLAN-local-G06.md new file mode 100644 index 0000000..ac4b5b5 --- /dev/null +++ b/agent-task/m-openai-workspace-agent-execution-contract/02+01_workspace_required_enforcement/PLAN-local-G06.md @@ -0,0 +1,189 @@ + + +# Plan - API + +## 이 파일을 읽는 구현 에이전트에게 + +이 plan은 split dependency가 있다. 구현을 시작하기 전에 같은 task group의 `01_route_catalog_workspace_bound/complete.log`가 존재하는지 확인한다. `CODE_REVIEW-local-G06.md`의 구현 에이전트 소유 섹션을 채우는 것까지가 구현이며, 최종 판정, log rename, `complete.log`, archive 이동은 code-review 전용이다. 사용자 결정, 사용자 소유 외부 환경, scope 충돌로 막히면 사용자에게 직접 묻지 말고 review stub의 `사용자 리뷰 요청` 섹션에 근거와 재개 조건을 채운다. + +## 배경 + +`metadata.workspace`는 Edge service의 `RunRequest.Workspace`로 전달되도록 선행 작은 변경에서 처리되었다. 하지만 workspace-bound agent route와 일반 inference route를 구분하지 않으면 workspace 누락 검증을 안전하게 적용할 수 없다. 이 작업은 route catalog의 workspace-bound 표식을 소비해서 agent route에만 workspace 필수 조건을 적용한다. + +## 사용자 리뷰 요청 흐름 + +구현 중 사용자 결정이 필요하면 `agent-ops/skills/common/_templates/implementation-user-review-request-section.md` 형식으로 active `CODE_REVIEW-local-G06.md`의 `사용자 리뷰 요청` 섹션을 채운다. 구현 에이전트는 채팅으로 직접 질문하거나 `request_user_input`을 호출하지 않는다. code-review가 요청 타당성을 검증하고 실제 `USER_REVIEW.md` 작성 여부를 결정한다. + +## Roadmap Targets + +- Milestone: `agent-roadmap/phase/automation-runtime-bridge/milestones/openai-workspace-agent-execution-contract.md` +- Task ids: + - `workspace-required`: 내부 실행 route가 workspace-bound agent target이면 workspace가 비어 있거나 상대 경로일 때 OpenAI-compatible error로 거부한다. +- Completion mode: check-on-pass + +## 분석 결과 + +### 읽은 파일 + +- `agent-roadmap/current.md` +- `agent-roadmap/phase/automation-runtime-bridge/PHASE.md` +- `agent-roadmap/phase/automation-runtime-bridge/milestones/openai-workspace-agent-execution-contract.md` +- `agent-contract/index.md` +- `agent-contract/provided/openai-compatible-api.md` +- `agent-ops/rules/project/domain/edge/rules.md` +- `agent-ops/rules/project/domain/platform-common/rules.md` +- `agent-ops/rules/project/domain/testing/rules.md` +- `agent-test/local/rules.md` +- `agent-test/local/edge-smoke.md` +- `agent-test/local/platform-common-smoke.md` +- `apps/edge/internal/openai/chat_handler.go` +- `apps/edge/internal/openai/responses_handler.go` +- `apps/edge/internal/openai/server_test.go` +- `apps/edge/internal/service/run_dispatch.go` +- `apps/edge/internal/service/service_test.go` +- `packages/go/config/config.go` +- `configs/edge.yaml` +- `scripts/e2e-openai-ollama.sh` + +### 테스트 환경 규칙 + +`test_env=local`을 선택했다. `agent-test/local/rules.md`를 읽었고, edge 변경 기준 `agent-test/local/edge-smoke.md`, config 연동 기준 `agent-test/local/platform-common-smoke.md`를 읽었다. 적용 명령은 `go test ./apps/edge/internal/openai ./apps/edge/internal/service`, OpenAI boundary smoke는 `./scripts/e2e-openai-ollama.sh`다. 규칙 파일에 구조적 blank나 `<확인 필요>` 값은 없었다. + +### 테스트 커버리지 공백 + +OpenAI handler tests는 route dispatch와 metadata 전달을 검증하지만 workspace-bound route의 empty/relative workspace rejection은 아직 없다. 일반 inference route가 workspace 없이 계속 통과해야 하는 회귀 test도 명시적으로 추가해야 한다. 실제 Node cwd 적용은 이 Task 범위가 아니므로 이 계획에서는 검증하지 않는다. + +### 심볼 참조 + +renamed/removed symbols: none. + +### 분할 판단 + +split decision policy를 평가했다. 이 task group은 `agent-task/m-openai-workspace-agent-execution-contract/`이고 sibling은 `01_route_catalog_workspace_bound`와 `02+01_workspace_required_enforcement`다. 이 plan은 dependent subtask이며 predecessor index `01`은 현재 active plan 상태이고 `complete.log`가 아직 없다. 구현 시작 전 `agent-task/m-openai-workspace-agent-execution-contract/01_route_catalog_workspace_bound/complete.log` 또는 matching archive `complete.log`로 dependency 충족을 확인해야 한다. + +### 범위 결정 근거 + +이 계획은 OpenAI-compatible request validation에 한정한다. route catalog field 생성은 predecessor task 범위다. Node CLI adapter cwd 적용, session workspace isolation, nonexistent path/permission failure mapping은 `edge-node-workspace`와 `nomad-http-smoke` Epic 쪽 Task로 남긴다. `metadata.workspace`를 prompt 본문이나 `metadata` map으로 복제하지 않는다. + +### 빌드 등급 + +build lane은 `local-G06`이다. HTTP error behavior가 바뀌고 route catalog 선행 작업에 의존하지만, 변경 범위가 OpenAI handler와 focused tests에 묶인다. + +## 구현 체크리스트 + +- [ ] predecessor `01_route_catalog_workspace_bound`의 `complete.log`가 active 또는 archive 위치에 있는지 확인한다. +- [ ] workspace-bound route에서 workspace가 빈 값 또는 상대 경로이면 `/v1/responses`와 `/v1/chat/completions` 모두 OpenAI-compatible 400 error를 반환하게 한다. +- [ ] workspace가 필요 없는 inference/fallback route는 기존 동작을 유지하는 회귀 test를 추가한다. +- [ ] `go test ./apps/edge/internal/openai ./apps/edge/internal/service`와 `./scripts/e2e-openai-ollama.sh`를 실행한다. +- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. + +## 의존 관계 및 구현 순서 + +이 directory 이름 `02+01_workspace_required_enforcement`의 predecessor는 `01_route_catalog_workspace_bound`다. 구현 전 `01`의 `complete.log`를 확인한다. 없으면 구현하지 말고 `CODE_REVIEW-local-G06.md`의 `사용자 리뷰 요청`이 아니라 계획 미충족 상태로 보고한다. + +### [API-1] Workspace-Required Validation Helper + +문제: `apps/edge/internal/openai/responses_handler.go:45`는 workspace를 파싱하고 `apps/edge/internal/openai/responses_handler.go:82`에서 service로 전달하지만 workspace가 route 요구 조건을 만족하는지 검증하지 않는다. `apps/edge/internal/openai/chat_handler.go:30`과 submit path도 같은 상태다. + +해결 방법: predecessor가 추가한 `routeDispatch.WorkspaceRequired`를 기준으로 helper를 만든다. `WorkspaceRequired=false`면 no-op이다. `true`일 때 `strings.TrimSpace(workspace)==""`이면 `workspace is required for this model route`, `!filepath.IsAbs(workspace)`이면 `workspace must be an absolute path` 같은 OpenAI-compatible 400 error를 반환한다. + +Before: + +```go +// apps/edge/internal/openai/responses_handler.go:51 +dispatch, ok := s.resolveRouteDispatch(req.Model, inferenceTarget) +if !ok { + writeError(w, http.StatusBadRequest, "invalid_request_error", "model is required") + return +} +``` + +After: + +```go +dispatch, ok := s.resolveRouteDispatch(req.Model, inferenceTarget) +if !ok { + writeError(w, http.StatusBadRequest, "invalid_request_error", "model is required") + return +} +if err := validateWorkspaceForRoute(dispatch, workspace); err != nil { + writeError(w, http.StatusBadRequest, "invalid_request_error", err.Error()) + return +} +``` + +수정 파일 및 체크리스트: + +- [ ] `apps/edge/internal/openai/chat_handler.go`: dispatch resolve 뒤 workspace validation 호출. +- [ ] `apps/edge/internal/openai/responses_handler.go`: dispatch resolve 뒤 workspace validation 호출. +- [ ] `apps/edge/internal/openai/chat_handler.go` 또는 새 small file: `validateWorkspaceForRoute` helper 추가. `path/filepath`와 `strings` import를 정리한다. + +테스트 작성: 작성한다. helper 단독 test보다 handler tests로 HTTP status와 service dispatch 여부를 검증한다. + +중간 검증: + +```bash +go test ./apps/edge/internal/openai +``` + +기대 결과: openai package PASS. + +### [API-2] Endpoint Regression Tests For Required And Non-Required Routes + +문제: workspace-bound agent route만 workspace를 요구해야 한다. 기존 inference route, top-level `openai.target`, metadata target fallback은 workspace 없이도 동작해야 한다. + +해결 방법: `apps/edge/internal/openai/server_test.go`에 `/v1/responses`와 `/v1/chat/completions` test를 추가한다. route catalog entry `{Model:"codex", Adapter:"cli", Target:"codex", WorkspaceRequired:true}`에서 missing/relative workspace는 400이고 service `SubmitRun`은 호출되지 않아야 한다. absolute workspace는 OK이며 `fake.req.Workspace`에 보존되어야 한다. non-required Ollama route는 workspace 없이 OK임을 확인한다. + +Before: + +```go +// apps/edge/internal/openai/server_test.go:1029 +func TestResponsesRouteCatalogDispatchesRoute(t *testing.T) { + // route dispatch만 검증하고 workspace-required error는 검증하지 않는다. +} +``` + +After: + +```go +func TestResponsesWorkspaceRequiredRouteRequiresAbsoluteWorkspace(t *testing.T) { + // missing workspace -> 400 + // relative workspace -> 400 + // absolute workspace -> OK and fake.req.Workspace is preserved +} +``` + +수정 파일 및 체크리스트: + +- [ ] `apps/edge/internal/openai/server_test.go`: Responses missing/relative/absolute workspace cases 추가. +- [ ] `apps/edge/internal/openai/server_test.go`: Chat Completions missing/relative/absolute workspace cases 추가. +- [ ] `apps/edge/internal/openai/server_test.go`: non-required route 또는 fallback route가 workspace 없이 OK인 회귀 assertion 추가. + +테스트 작성: 작성한다. public HTTP behavior 변경이므로 regression tests가 필수다. + +중간 검증: + +```bash +go test ./apps/edge/internal/openai ./apps/edge/internal/service +``` + +기대 결과: 두 패키지 모두 PASS. + +## 수정 파일 요약 + +| 파일 | 항목 | +|------|------| +| `apps/edge/internal/openai/chat_handler.go` | API-1 | +| `apps/edge/internal/openai/responses_handler.go` | API-1 | +| `apps/edge/internal/openai/server_test.go` | API-2 | + +## 최종 검증 + +```bash +go test ./apps/edge/internal/openai ./apps/edge/internal/service +./scripts/e2e-openai-ollama.sh +``` + +Go test cache output is acceptable for unchanged packages; for the OpenAI handler package prefer fresh execution with `go test -count=1 ./apps/edge/internal/openai` if handler tests were edited. + +모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다. diff --git a/apps/edge/README.md b/apps/edge/README.md index 37321a4..0297a4f 100644 --- a/apps/edge/README.md +++ b/apps/edge/README.md @@ -193,7 +193,7 @@ nodes: `openai.target`이 비어 있으면 HTTP 요청의 `model` 값을 내부 target으로 사용한다. 값이 있으면 Cline 같은 외부 agent가 보낸 `model`과 무관하게 YAML의 target으로 고정 라우팅한다. -`/v1/chat/completions`는 기본 non-streaming과 streaming SSE 응답을 모두 지원한다. `/v1/responses`는 현재 non-streaming 요청만 지원하며, `metadata.request_id`, `metadata.inference.target`, `metadata.nomadcode.task_id`, `metadata.nomadcode.source`를 service boundary까지 전달한다. NomadCode compatibility alias로 root-level `metadata.task_id`와 `metadata.source`도 받아 각각 `nomadcode.task_id`, `nomadcode.source`로 매핑하며, structured `metadata.nomadcode.*` 값이 있으면 그 값을 우선한다. +`/v1/chat/completions`는 기본 non-streaming과 streaming SSE 응답을 모두 지원한다. `/v1/responses`는 현재 non-streaming 요청만 지원한다. 두 endpoint 모두 `metadata.workspace`를 run workspace로 전달하고, `metadata.request_id`, `metadata.inference.target`, `metadata.nomadcode.task_id`, `metadata.nomadcode.source`를 service boundary까지 전달한다. NomadCode compatibility alias로 root-level `metadata.task_id`와 `metadata.source`도 받아 각각 `nomadcode.task_id`, `nomadcode.source`로 매핑하며, structured `metadata.nomadcode.*` 값이 있으면 그 값을 우선한다. `metadata.cli`는 지원하지 않는다. ```bash curl -s http://127.0.0.1:18081/v1/chat/completions \ @@ -204,7 +204,7 @@ curl -s http://127.0.0.1:18081/v1/chat/completions \ ```bash curl -s http://127.0.0.1:18081/v1/responses \ -H 'Content-Type: application/json' \ - -d '{"model":"qwen3.6:35b-a3b-bf16","input":"hello","stream":false,"metadata":{"request_id":"example","nomadcode":{"task_id":"task-123","source":"manual"}}}' + -d '{"model":"qwen3.6:35b-a3b-bf16","input":"hello","stream":false,"metadata":{"request_id":"example","workspace":"/config/workspace/iop","nomadcode":{"task_id":"task-123","source":"manual"}}}' ``` vLLM은 같은 `adapter + target` 경계를 쓰도록 설정과 `/v1/models` capability 조회 skeleton만 먼저 열어두었다. 실제 completion 실행은 다음 단계에서 붙인다. diff --git a/apps/edge/internal/openai/chat_handler.go b/apps/edge/internal/openai/chat_handler.go index 171b313..09b1432 100644 --- a/apps/edge/internal/openai/chat_handler.go +++ b/apps/edge/internal/openai/chat_handler.go @@ -27,7 +27,13 @@ func (s *Server) handleChatCompletions(w http.ResponseWriter, r *http.Request) { writeError(w, http.StatusBadRequest, "invalid_request_error", "invalid JSON request") return } - dispatch, ok := s.resolveRouteDispatch(req.Model, "") + runMeta, inferenceTarget, workspace, err := parseOpenAIMetadata(req.Metadata) + if err != nil { + writeError(w, http.StatusBadRequest, "invalid_request_error", err.Error()) + return + } + + dispatch, ok := s.resolveRouteDispatch(req.Model, inferenceTarget) if !ok { writeError(w, http.StatusBadRequest, "invalid_request_error", "model is required") return @@ -64,15 +70,11 @@ func (s *Server) handleChatCompletions(w http.ResponseWriter, r *http.Request) { Adapter: dispatch.Adapter, Target: dispatch.Target, SessionID: dispatch.SessionID, + Workspace: workspace, Prompt: prompt, Input: input, TimeoutSec: dispatch.TimeoutSec, - Metadata: map[string]string{ - "source": "openai", - "openai_model": req.Model, - "openai_stream": fmt.Sprintf("%t", req.Stream), - "strict_output": fmt.Sprintf("%t", outputPolicy.Strict), - }, + Metadata: chatRunMetadata(runMeta, req, outputPolicy), }) if err != nil { writeError(w, http.StatusBadGateway, "node_dispatch_error", err.Error()) @@ -87,6 +89,17 @@ func (s *Server) handleChatCompletions(w http.ResponseWriter, r *http.Request) { s.completeChatCompletion(w, r, req, handle, outputPolicy) } +func chatRunMetadata(runMeta map[string]string, req chatCompletionRequest, outputPolicy strictOutputPolicy) map[string]string { + if runMeta == nil { + runMeta = make(map[string]string) + } + runMeta["source"] = "openai" + runMeta["openai_model"] = req.Model + runMeta["openai_stream"] = fmt.Sprintf("%t", req.Stream) + runMeta["strict_output"] = fmt.Sprintf("%t", outputPolicy.Strict) + return runMeta +} + func (s *Server) completeChatCompletion(w http.ResponseWriter, r *http.Request, req chatCompletionRequest, handle edgeservice.RunResult, outputPolicy strictOutputPolicy) { text, reasoning, usage, err := collectRunResult(r.Context(), handle.Stream(), handle.WaitTimeout()) if err != nil { diff --git a/apps/edge/internal/openai/responses_handler.go b/apps/edge/internal/openai/responses_handler.go index 33f2e8e..1002ef2 100644 --- a/apps/edge/internal/openai/responses_handler.go +++ b/apps/edge/internal/openai/responses_handler.go @@ -42,7 +42,7 @@ func (s *Server) handleResponses(w http.ResponseWriter, r *http.Request) { prompt = instruction + "\n" + prompt } - runMeta, inferenceTarget, err := parseResponsesMetadata(req.Metadata) + runMeta, inferenceTarget, workspace, err := parseOpenAIMetadata(req.Metadata) if err != nil { writeError(w, http.StatusBadRequest, "invalid_request_error", err.Error()) return @@ -79,6 +79,7 @@ func (s *Server) handleResponses(w http.ResponseWriter, r *http.Request) { Adapter: dispatch.Adapter, Target: dispatch.Target, SessionID: dispatch.SessionID, + Workspace: workspace, Prompt: prompt, Input: input, TimeoutSec: dispatch.TimeoutSec, @@ -156,23 +157,23 @@ func buildResponsesPrompt(instructions, input string) string { return instructions + "\n" + input } -func parseResponsesMetadata(raw json.RawMessage) (map[string]string, string, error) { +func parseOpenAIMetadata(raw json.RawMessage) (map[string]string, string, string, error) { if len(raw) == 0 || string(raw) == "null" { - return make(map[string]string), "", nil + return make(map[string]string), "", "", nil } var rawMap map[string]json.RawMessage if err := json.Unmarshal(raw, &rawMap); err != nil { - return nil, "", fmt.Errorf("metadata must be an object") + return nil, "", "", fmt.Errorf("metadata must be an object") } if _, hasCLI := rawMap["cli"]; hasCLI { - return nil, "", fmt.Errorf("metadata.cli is not supported") + return nil, "", "", fmt.Errorf("metadata.cli is not supported") } var meta responsesMetadata if err := json.Unmarshal(raw, &meta); err != nil { - return nil, "", fmt.Errorf("invalid metadata format") + return nil, "", "", fmt.Errorf("invalid metadata format") } flat := meta.metadataForRun() @@ -181,5 +182,5 @@ func parseResponsesMetadata(raw json.RawMessage) (map[string]string, string, err inferenceTarget = meta.Inference.Target } - return flat, inferenceTarget, nil + return flat, inferenceTarget, strings.TrimSpace(meta.Workspace), nil } diff --git a/apps/edge/internal/openai/server_test.go b/apps/edge/internal/openai/server_test.go index d897618..9b97936 100644 --- a/apps/edge/internal/openai/server_test.go +++ b/apps/edge/internal/openai/server_test.go @@ -526,6 +526,7 @@ func TestResponsesMetadataContractAndTargetOverride(t *testing.T) { "input":"test", "metadata":{ "request_id":"req-001", + "workspace":"/config/workspace/iop", "inference":{"target":"metadata-target"}, "nomadcode":{"task_id":"task-123","source":"nomadcode"} } @@ -539,9 +540,15 @@ func TestResponsesMetadataContractAndTargetOverride(t *testing.T) { if fake.req.Target != "metadata-target" { t.Fatalf("target: got %q, want metadata-target", fake.req.Target) } + if fake.req.Workspace != "/config/workspace/iop" { + t.Fatalf("workspace: got %q", fake.req.Workspace) + } if fake.req.Metadata["request_id"] != "req-001" { t.Fatalf("request_id: got %q", fake.req.Metadata["request_id"]) } + if _, ok := fake.req.Metadata["workspace"]; ok { + t.Fatal("workspace should not be copied into run metadata") + } if fake.req.Metadata["inference.target"] != "metadata-target" { t.Fatalf("inference.target: got %q", fake.req.Metadata["inference.target"]) } @@ -565,6 +572,68 @@ func TestResponsesMetadataContractAndTargetOverride(t *testing.T) { } } +func TestChatCompletionsMetadataContractAndWorkspace(t *testing.T) { + fake := &fakeRunService{events: make(chan *iop.RunEvent, 2)} + fake.events <- &iop.RunEvent{Type: "delta", Delta: "ok"} + fake.events <- &iop.RunEvent{Type: "complete"} + + srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama"}, fake, nil) + req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{ + "model":"client-model", + "messages":[{"role":"user","content":"hi"}], + "metadata":{ + "request_id":"req-chat-001", + "workspace":"/config/workspace/iop", + "inference":{"target":"metadata-target"}, + "nomadcode":{"task_id":"task-123","source":"nomadcode"} + } + }`)) + w := httptest.NewRecorder() + srv.routes().ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Fatalf("status: got %d body=%s", w.Code, w.Body.String()) + } + if fake.req.Target != "metadata-target" { + t.Fatalf("target: got %q, want metadata-target", fake.req.Target) + } + if fake.req.Workspace != "/config/workspace/iop" { + t.Fatalf("workspace: got %q", fake.req.Workspace) + } + if fake.req.Metadata["request_id"] != "req-chat-001" { + t.Fatalf("request_id: got %q", fake.req.Metadata["request_id"]) + } + if fake.req.Metadata["inference.target"] != "metadata-target" { + t.Fatalf("inference.target: got %q", fake.req.Metadata["inference.target"]) + } + if fake.req.Metadata["nomadcode.task_id"] != "task-123" { + t.Fatalf("nomadcode.task_id: got %q", fake.req.Metadata["nomadcode.task_id"]) + } + if fake.req.Metadata["nomadcode.source"] != "nomadcode" { + t.Fatalf("nomadcode.source: got %q", fake.req.Metadata["nomadcode.source"]) + } + if fake.req.Metadata["source"] != "openai" { + t.Fatalf("source: got %q", fake.req.Metadata["source"]) + } + if _, ok := fake.req.Metadata["workspace"]; ok { + t.Fatal("workspace should not be copied into run metadata") + } +} + +func TestChatCompletionsRejectsCLIMetadata(t *testing.T) { + srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama", Target: "llama"}, &fakeRunService{}, nil) + req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{ + "model":"m", + "messages":[{"role":"user","content":"hi"}], + "metadata":{"cli":{"flag":"x"}} + }`)) + w := httptest.NewRecorder() + srv.routes().ServeHTTP(w, req) + if w.Code != http.StatusBadRequest { + t.Fatalf("got %d want 400, body=%s", w.Code, w.Body.String()) + } +} + func TestResponsesAcceptsFlatNomadCodeMetadataAliases(t *testing.T) { fake := &fakeRunService{events: make(chan *iop.RunEvent, 2)} fake.events <- &iop.RunEvent{Type: "delta", Delta: "ok"} diff --git a/apps/edge/internal/openai/types.go b/apps/edge/internal/openai/types.go index 3294aab..494b00b 100644 --- a/apps/edge/internal/openai/types.go +++ b/apps/edge/internal/openai/types.go @@ -6,14 +6,15 @@ import ( ) type chatCompletionRequest struct { - Model string `json:"model"` - Messages []chatMessage `json:"messages"` - Stream bool `json:"stream"` - Options map[string]any `json:"options,omitempty"` - Format any `json:"format,omitempty"` - KeepAlive any `json:"keep_alive,omitempty"` - Think any `json:"think,omitempty"` - Tools []any `json:"tools,omitempty"` + Model string `json:"model"` + Messages []chatMessage `json:"messages"` + Stream bool `json:"stream"` + Metadata json.RawMessage `json:"metadata,omitempty"` + Options map[string]any `json:"options,omitempty"` + Format any `json:"format,omitempty"` + KeepAlive any `json:"keep_alive,omitempty"` + Think any `json:"think,omitempty"` + Tools []any `json:"tools,omitempty"` } type chatMessage struct { @@ -212,6 +213,7 @@ type responsesRequest struct { type responsesMetadata struct { RequestID string `json:"request_id,omitempty"` + Workspace string `json:"workspace,omitempty"` TaskID string `json:"task_id,omitempty"` Source string `json:"source,omitempty"` Inference *responsesInferenceMetadata `json:"inference,omitempty"` diff --git a/apps/edge/internal/service/run_dispatch.go b/apps/edge/internal/service/run_dispatch.go index 96e6ae4..a24f8a5 100644 --- a/apps/edge/internal/service/run_dispatch.go +++ b/apps/edge/internal/service/run_dispatch.go @@ -18,6 +18,7 @@ type SubmitRunRequest struct { Adapter string Target string SessionID string + Workspace string Prompt string Input map[string]any Background bool @@ -259,6 +260,7 @@ func BuildRunRequest(req SubmitRunRequest) (*iop.RunRequest, string, error) { RunId: runID, Adapter: req.Adapter, Target: req.Target, + Workspace: req.Workspace, SessionId: NormalizeSessionID(req.SessionID), SessionMode: iop.RunSessionMode_RUN_SESSION_MODE_CREATE_IF_MISSING, Background: req.Background, diff --git a/apps/edge/internal/service/service_test.go b/apps/edge/internal/service/service_test.go index cd0ddf5..859c7de 100644 --- a/apps/edge/internal/service/service_test.go +++ b/apps/edge/internal/service/service_test.go @@ -183,6 +183,25 @@ func TestBuildRunRequestPreservesResponsesMetadataKeys(t *testing.T) { } } +func TestBuildRunRequestPreservesWorkspace(t *testing.T) { + req, _, err := edgeservice.BuildRunRequest(edgeservice.SubmitRunRequest{ + Adapter: "cli", + Target: "codex", + Workspace: "/config/workspace/iop", + Prompt: "test", + Metadata: map[string]string{"request_id": "req-001"}, + }) + if err != nil { + t.Fatalf("BuildRunRequest: %v", err) + } + if req.GetWorkspace() != "/config/workspace/iop" { + t.Fatalf("Workspace: got %q", req.GetWorkspace()) + } + if _, ok := req.GetMetadata()["workspace"]; ok { + t.Fatal("workspace should be preserved as RunRequest.Workspace, not metadata") + } +} + func TestListNodesReturnsSnapshots(t *testing.T) { reg := edgenode.NewRegistry() reg.Register(&edgenode.NodeEntry{NodeID: "node-1", Alias: "alpha"})