From 21fd7dbcdfb559248a1cb02224427be9bfe017e4 Mon Sep 17 00:00:00 2001 From: toki Date: Sun, 14 Jun 2026 05:32:20 +0900 Subject: [PATCH] update: automation-runtime-bridge phase and edge openai server test --- ...enai-workspace-agent-execution-contract.md | 24 +++++---- .../phase/automation-runtime-bridge/PHASE.md | 4 +- apps/edge/internal/openai/server_test.go | 52 +++++++++++++++++++ 3 files changed, 67 insertions(+), 13 deletions(-) rename agent-roadmap/{ => archive}/phase/automation-runtime-bridge/milestones/openai-workspace-agent-execution-contract.md (85%) diff --git a/agent-roadmap/phase/automation-runtime-bridge/milestones/openai-workspace-agent-execution-contract.md b/agent-roadmap/archive/phase/automation-runtime-bridge/milestones/openai-workspace-agent-execution-contract.md similarity index 85% rename from agent-roadmap/phase/automation-runtime-bridge/milestones/openai-workspace-agent-execution-contract.md rename to agent-roadmap/archive/phase/automation-runtime-bridge/milestones/openai-workspace-agent-execution-contract.md index d9e9e4d..b96ff68 100644 --- a/agent-roadmap/phase/automation-runtime-bridge/milestones/openai-workspace-agent-execution-contract.md +++ b/agent-roadmap/archive/phase/automation-runtime-bridge/milestones/openai-workspace-agent-execution-contract.md @@ -14,7 +14,7 @@ ## 상태 -[진행중] +[완료] ## 구현 잠금 @@ -37,7 +37,7 @@ OpenAI-compatible 입력 표면에서 workspace agent 실행 문맥을 별도 wrapper 없이 `metadata.workspace`로 받는 계약을 구현한다. -- [ ] [metadata-schema] `/v1/responses`와 `/v1/chat/completions`가 flat `metadata.workspace`를 파싱해 run workspace로 전달한다. 검증: 기존 `metadata.request_id`, `metadata.nomadcode.*`, `metadata.inference.target`은 유지되고 `metadata.cli`는 계속 거부된다. +- [x] [metadata-schema] `/v1/responses`와 `/v1/chat/completions`가 flat `metadata.workspace`를 파싱해 run workspace로 전달한다. 검증: 기존 `metadata.request_id`, `metadata.nomadcode.*`, `metadata.inference.target`은 유지되고 `metadata.cli`는 계속 거부된다. - [x] [workspace-required] 내부 실행 route가 workspace-bound agent target이면 workspace가 비어 있거나 상대 경로일 때 OpenAI-compatible error로 거부한다. 검증: workspace가 필요 없는 inference route는 기존 동작을 유지하고, workspace-bound route만 필수 조건을 적용한다. - [x] [route-catalog] 외부 `model` route가 내부 `adapter + target`으로 해석되는 기준을 config와 smoke fixture에 남긴다. 검증: `model: "codex"` 같은 route가 명시적으로 workspace-bound agent target으로 수렴한다. @@ -54,20 +54,22 @@ Edge service와 Node runtime 사이에서 workspace가 metadata 문자열로만 NomadCode가 Plane `Todo` projection 전 단계에서 IOP Edge HTTP 호출로 workspace agent 산출물을 만들 수 있는 최소 실행 증거를 확보한다. -- [ ] [workspace-authoring-smoke] `metadata.workspace`가 가리키는 임시 checkout에서 workspace-bound agent target이 파일을 생성/수정할 수 있음을 검증한다. 검증: OpenAI-compatible `/v1/responses` smoke가 workspace 안 marker 또는 git diff를 남기고, 프로세스 cwd가 workspace 밖으로 벗어나지 않는다. -- [ ] [nomad-metadata-shape] NomadCode Core가 workspace slot path를 flat `metadata.workspace`로, task/source context를 `metadata.task_id`/`metadata.source` 또는 `metadata.nomadcode.*`로 전달하는 호출 shape를 맞춘다. 검증: `../nomadcode`의 OpenAI Responses client/scheduler fixture와 IOP Edge smoke fixture가 같은 metadata contract를 사용한다. +- [x] [workspace-authoring-smoke] `metadata.workspace`가 가리키는 임시 checkout에서 workspace-bound agent target이 파일을 생성/수정할 수 있음을 검증한다. 검증: OpenAI-compatible `/v1/responses` smoke가 workspace 안 marker 또는 git diff를 남기고, 프로세스 cwd가 workspace 밖으로 벗어나지 않는다. +- [x] [nomad-metadata-shape] NomadCode Core가 workspace slot path를 flat `metadata.workspace`로, task/source context를 `metadata.task_id`/`metadata.source` 또는 `metadata.nomadcode.*`로 전달하는 호출 shape를 맞춘다. 검증: `../nomadcode`의 OpenAI Responses client/scheduler fixture와 IOP Edge smoke fixture가 같은 metadata contract를 사용한다. - [x] [nomad-handoff-contract] NomadCode authoring 호출 shape를 계약 문서와 운영 문서에 연결한다. 검증: 외부 호출은 `model`, `input`, `metadata.workspace`, task/source metadata만으로 충분하며 `metadata.cli`, root-level `iop` wrapper, IOP CLI 직접 실행을 요구하지 않는다. -- [ ] [failure-surface] workspace 누락, 존재하지 않는 경로, 권한 오류, agent process exit failure가 호출자에게 구분 가능한 실패로 드러난다. 검증: 잘못된 workspace 요청이 조용히 기본 cwd에서 실행되지 않는다. +- [x] [failure-surface] workspace 누락, 존재하지 않는 경로, 권한 오류, agent process exit failure가 호출자에게 구분 가능한 실패로 드러난다. 검증: 잘못된 workspace 요청이 조용히 기본 cwd에서 실행되지 않는다. ## 완료 리뷰 -- 상태: 없음 -- 요청일: 없음 -- 완료 근거: 모든 기능 Task와 Task 안에 명시된 검증이 아직 충족되지 않았다. +- 상태: 승인됨 +- 요청일: 2026-06-14 +- 완료 근거: OpenAI metadata schema와 workspace-required/failure-surface unit coverage가 `go test ./apps/edge/...`와 관련 Node/Service 패키지 테스트로 통과했다. +- 완료 근거: `./scripts/e2e-openai-cli-workspace.sh`가 임시 checkout 안 marker 생성, repo root/temp parent 누수 방지, `iop-edge smoke openai --workspace --expect-file` 검증까지 통과했다. +- 완료 근거: 코드 레벨 완료 리뷰에서 blocker를 찾지 못했고, NomadCode Core의 OpenAI Responses client/scheduler fixture도 같은 flat metadata contract를 사용함을 확인했다. - 리뷰 필요: - - [ ] 사용자가 완료 결과를 확인했다 - - [ ] archive 이동을 승인했다 -- 리뷰 코멘트: 없음 + - [x] 사용자가 완료 결과를 확인했다 + - [x] archive 이동을 승인했다 +- 리뷰 코멘트: 2026-06-14 코드 레벨 리뷰와 재검증 결과 완료로 전환하고 archive 이동을 진행했다. ## 범위 제외 diff --git a/agent-roadmap/phase/automation-runtime-bridge/PHASE.md b/agent-roadmap/phase/automation-runtime-bridge/PHASE.md index b8e6b86..8cdb803 100644 --- a/agent-roadmap/phase/automation-runtime-bridge/PHASE.md +++ b/agent-roadmap/phase/automation-runtime-bridge/PHASE.md @@ -71,8 +71,8 @@ CLI 실행, specialized agent 등록, bootstrap/enrollment, 원격 터미널 브 - 경로: `agent-roadmap/archive/phase/automation-runtime-bridge/milestones/codex-app-server-streaming-migration.md` - 요약: Codex CLI target의 기본 `codex` profile을 app-server 기반으로 전환하고 실제 `codex` foreground/background smoke와 app-server session lifecycle 검증을 완료했다. -- [진행중] OpenAI Workspace Agent Execution Contract - - 경로: `agent-roadmap/phase/automation-runtime-bridge/milestones/openai-workspace-agent-execution-contract.md` +- [완료] OpenAI Workspace Agent Execution Contract + - 경로: `agent-roadmap/archive/phase/automation-runtime-bridge/milestones/openai-workspace-agent-execution-contract.md` - 요약: NomadCode가 IOP CLI를 직접 실행하지 않고 IOP Edge OpenAI-compatible HTTP 호출의 `metadata.workspace`와 task/source metadata만으로 내부 workspace-bound agent target이 해당 checkout에서 산출물을 만들 수 있게 하는 최우선 contract/serving hardening 작업이다. - [계획] 원격 터미널 브리지 POC diff --git a/apps/edge/internal/openai/server_test.go b/apps/edge/internal/openai/server_test.go index e640d33..f58bb48 100644 --- a/apps/edge/internal/openai/server_test.go +++ b/apps/edge/internal/openai/server_test.go @@ -1286,6 +1286,58 @@ func TestResponsesNonRequiredRouteNoWorkspaceOK(t *testing.T) { } } +func TestResponsesSurfacesDistinctRunFailures(t *testing.T) { + cases := []struct { + name string + runError string + want []string + forbidAny []string + }{ + { + name: "missing workspace path from node", + runError: "cli adapter: workspace not found: /abs/missing", + want: []string{`"type":"run_error"`, "cli adapter: workspace not found: /abs/missing"}, + }, + { + name: "inaccessible workspace path from node", + runError: "cli adapter: workspace inaccessible: /abs/private: permission denied", + want: []string{`"type":"run_error"`, "cli adapter: workspace inaccessible: /abs/private"}, + }, + { + name: "agent process exit failure", + runError: "command failed: exit status 7", + want: []string{`"type":"run_error"`, "command failed: exit status 7"}, + forbidAny: []string{"workspace not found", "workspace inaccessible", "workspace is not a directory"}, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + fake := &fakeRunService{events: make(chan *iop.RunEvent, 1)} + fake.events <- &iop.RunEvent{Type: "error", Error: tc.runError} + srv := NewServer(workspaceBoundCfg(), fake, nil) + body := `{"model":"codex","input":"hello","metadata":{"workspace":"/abs/workspace"}}` + req := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(body)) + w := httptest.NewRecorder() + srv.handleResponses(w, req) + + if w.Code != http.StatusBadGateway { + t.Fatalf("run failure: want 502, got %d body=%s", w.Code, w.Body.String()) + } + for _, want := range tc.want { + if !strings.Contains(w.Body.String(), want) { + t.Fatalf("expected body to contain %q, got %s", want, w.Body.String()) + } + } + for _, forbidden := range tc.forbidAny { + if strings.Contains(w.Body.String(), forbidden) { + t.Fatalf("body should not contain %q, got %s", forbidden, w.Body.String()) + } + } + }) + } +} + func TestChatCompletionsWorkspaceRequiredRouteMissingWorkspace400(t *testing.T) { fake := &fakeRunService{events: make(chan *iop.RunEvent)} srv := NewServer(workspaceBoundCfg(), fake, nil)