<!-- task=m-openai-workspace-agent-execution-contract/02+01_workspace_required_enforcement plan=0 tag=API -->

# 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`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.
