update: automation-runtime-bridge phase and edge openai server test

This commit is contained in:
toki 2026-06-14 05:32:20 +09:00
parent af639537ec
commit 21fd7dbcdf
3 changed files with 67 additions and 13 deletions

View file

@ -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 이동을 진행했다.
## 범위 제외

View file

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

View file

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