<!-- task=m-iop-owned-single-request-agent-execution/18+17_plan_stage plan=0 tag=API milestone-task=plan-stage -->

# Authorized Plan provider stage

## For the Implementing Agent

Filling the implementation-owned sections of `CODE_REVIEW-cloud-G06.md` is the mandatory last implementation step. Run every verification command, paste actual output, keep both active files in place, and report ready for review; only the official code-review skill may append a verdict, archive files, write `complete.log`, or classify the next state. If blocked, record the exact blocker, attempted commands/output, and resume condition only in the review evidence fields; do not ask the user, call a user-input tool, create a control-plane stop file, or change the owner or scope.

## Background

Marked single-request admission freezes canonical Plan/Work/Review model names and options, but it discards the authorized managed route facts required to dispatch a provider stage. No provider-specific runner currently converts the immutable Anthropic task into a bounded Gemini Plan request or persists its result. This packet adds a reusable non-streaming managed stage codec and the S08 Plan runner, but deliberately leaves production executor installation to the final Review packet.

## Analysis

### Files Read

- `AGENTS.md`
- `agent-roadmap/phase/knowledge-tool-optimization-extension/PHASE.md`
- `agent-roadmap/phase/knowledge-tool-optimization-extension/milestones/iop-owned-single-request-agent-execution.md`
- `agent-roadmap/sdd/knowledge-tool-optimization-extension/iop-owned-single-request-agent-execution/SDD.md`
- `agent-spec/runtime/edge-node-execution.md`
- `agent-contract/outer/anthropic-compatible-api.md`
- `agent-contract/inner/edge-config-runtime-refresh.md`
- `agent-contract/inner/execution-runtime.md`
- `apps/edge/internal/service/single_request_types.go`
- `apps/edge/internal/service/single_request_types_test.go`
- `apps/edge/internal/service/single_request.go`
- `apps/edge/internal/service/provider_pool.go`
- `apps/edge/internal/service/provider_tunnel.go`
- `apps/edge/internal/service/run_types.go`
- `apps/edge/internal/openai/single_request_preset_binding.go`
- `apps/edge/internal/openai/single_request_preset_binding_test.go`
- `apps/edge/internal/openai/principal_routes.go`
- `apps/edge/internal/openai/principal_routes_test.go`
- `apps/edge/internal/openai/route_resolution.go`
- `apps/edge/internal/openai/server.go`
- `apps/edge/internal/openai/chat_types.go`
- `apps/edge/internal/openai/anthropic_handler.go`
- `apps/edge/internal/openai/anthropic_types.go`

### SDD Criteria

- SDD: `agent-roadmap/sdd/knowledge-tool-optimization-extension/iop-owned-single-request-agent-execution/SDD.md`, `[승인됨]`, `SDD 잠금: 해제`.
- First-line contribution id: `plan-stage`.
- Targeted scenario: S08 — immutable user task plus empty request job produces a small plan and verification criteria using Gemini 3.6 Flash with `reasoning_effort=high`, then writes internal `plan.md`.
- Evidence Map driver: “Gemini plan request/options/artifact fixture.” The checklist therefore verifies the exact authorized candidate/credential fence, request options/body, bounded provider response, planning envelope, and PLAN artifact write rather than merely checking a mocked state transition.

### Verification Context

- No verification handoff was supplied. Repository-native sources were `agent-test/local/rules.md`, `edge-smoke.md`, and the related service/OpenAI tests.
- Current checkout preflight: branch `feature/iop-owned-single-request-agent-execution`, HEAD `3bb4a24ad750a9ce5b7754a70db24545554ba238`, clean worktree; Go `1.26.2 linux/arm64` is available.
- Baseline relevant packages passed with `-count=1`; no external provider, credential, or remote runner is needed because provider frames and candidate selection are deterministic fixtures.
- Constraint: the Plan runner must not be installed as the service executor while Work/Review are absent. Actual Claude/Mac qualification remains SDD S12 `claude-smoke`.
- Confidence: high after predecessor completion. The current provider-pool surface already supports request-local candidate predicates, protocol-operation filtering, credential leases, final-target body building, and bounded tunnel handles.

### Test Coverage Gaps

- Binding tests cover stage model/options and refresh isolation, but not dispatch/credential route facts.
- Provider-pool tests cover candidate filtering and tunnel dispatch generally, but no single-request stage request body or private result codec.
- Existing single-request handler tests use injected fake executors; they do not establish S08 Gemini request/options/artifact behavior. This packet adds focused normal and failure fixtures.

### Symbol References

No symbol is renamed or removed. `SingleRequestStageBinding` construction occurs in `resolveStageBinding` and service tests; all composite literals found by repository search must be updated or remain valid through a zero-value route only in test helpers that never dispatch.

### Split Judgment

- Predecessor 17 contract: controller-owned PLAN/REVIEW artifact read/write with exact workspace lifecycle cleanup; PASS evidence is `agent-task/m-iop-owned-single-request-agent-execution/17_internal_artifact_wire/complete.log`.
- At plan creation the predecessor `complete.log` is missing, so implementation must wait. The directory name `18+17_plan_stage` is the runtime source of truth.
- This packet's stable contract is an immutable managed stage dispatch snapshot plus a directly testable Plan runner that is not production-installed. PASS evidence is the S08 provider request/options/artifact fixture.
- Work and Review stay separate because their tool continuation and repair state machines have independent invariants and deterministic PASS tests.

### Scope Rationale

This packet excludes Work tool calls, provider continuation, Review verdict/repair, manager installation, public response changes, generic error/cancel quality work, config examples, and actual provider execution. It consumes the frozen model references already approved by the preset and does not introduce fallback, dynamic model selection, caller credentials, caller tools, or a new public API.

### Final Routing

- `evaluation_mode=first-pass`; `finalizer=finalize-task-policy.sh`; `finalizer_mode=pair`.
- Build closures: scope/context/verification/evidence/ownership/decision all true. Scores `2/1/1/1/1` => G06, base/route `local-fit`, `worker/local/G06`, `PLAN-local-G06.md`.
- Review closures: all true. Scores `2/1/1/1/1` => G06, `official-review`, `review/cloud/G06`, `CODE_REVIEW-cloud-G06.md`.
- `large_indivisible_context=false`; positive loop risks: `boundary_contract`, `structured_interpretation` (2); `review_rework_count=0`; `evidence_integrity_failure=false`; no capability gap.

## Dependencies and Execution Order

1. Do not implement until `agent-task/m-iop-owned-single-request-agent-execution/17_internal_artifact_wire/complete.log` exists. No archive candidate exists yet at plan creation.
2. Implement this packet only after that evidence is present; do not add dependencies absent from `18+17_plan_stage`.
3. Do not install the resulting runner into `Service.SetSingleRequestExecutor`; `20+19_review_stage` owns production activation after all stages exist.

## Implementation Checklist

- [ ] Extend immutable stage admission with the exact managed provider-pool, candidate, and credential facts required for stage dispatch, with validation and defensive clone coverage.
- [ ] Add a bounded non-streaming single-request provider-stage request/response codec that reuses provider-pool admission and rejects normalized, mismatched, malformed, oversized, or provider-error outcomes.
- [ ] Implement the Gemini Plan runner: emit planning, send the immutable task with `reasoning_effort=high`, require a small plan plus verification criteria, and persist PLAN through the controller artifact API.
- [ ] Update the current implementation spec and run dependency, focused, broader Edge, vet, deterministic search, and diff checks without production activation.
- [ ] Fill implementation-owned sections in CODE_REVIEW-*-G??.md with actual implementation notes and verification output.

### [API-1] Preserve authorized managed route facts in stage admission

**Problem**

`apps/edge/internal/openai/single_request_preset_binding.go:163` returns only `Model` and `Options`, even though its source `routeDispatch` contains the principal, credential slot, route/profile revisions, model group, provider, upstream model, timeouts, and candidate predicate. A runner using only the canonical model would have to re-resolve mutable config or bypass the lease fence.

**Solution**

Add a service-owned `SingleRequestStageDispatchBinding` to each stage. Copy the exact frozen managed route facts at compilation, validate required identity/revision fields in `NewSingleRequestBinding`, clone all mutable values, and expose a method that reconstructs only the request-local candidate predicate and `CredentialBinding` needed by provider-pool dispatch. Do not retain a refreshable map, closure over mutable projection state, secret, endpoint, or Node selection.

Before (`apps/edge/internal/service/single_request_types.go:94`):

```go
type SingleRequestStageBinding struct {
    Model string
    Options map[string]any
}
```

After:

```go
type SingleRequestStageBinding struct {
    Model string
    Options map[string]any
    Dispatch SingleRequestStageDispatchBinding
}
```

**Modified Files and Checklist**

- [ ] Add validated dispatch DTOs and deep-clone behavior in `apps/edge/internal/service/single_request_types.go`.
- [ ] Extend normal, missing-field, clone, refresh-isolation, and malformed-route tests in `apps/edge/internal/service/single_request_types_test.go`.
- [ ] Copy route facts in `apps/edge/internal/openai/single_request_preset_binding.go` without changing public model echo or workspace admission.
- [ ] Expand `apps/edge/internal/openai/single_request_preset_binding_test.go` to assert exact route/credential facts, candidate rejection, and post-refresh isolation for all three stages.

**Test Strategy**

Write tests for exact managed values, wrong/missing principal/slot/route/profile/revision/model-group rejection, clone isolation, and refresh mutation. Existing model/options/shape tests remain unchanged and must continue to pass.

**Verification**

Run `go test ./apps/edge/internal/service ./apps/edge/internal/openai -run 'TestSingleRequest(Binding|PresetBinding)' -count=1`; all admission tests must pass freshly.

### [API-2] Add the private managed provider-stage codec

**Problem**

`apps/edge/internal/service/provider_pool.go:107-203` accepts a one-shot pool dispatch, but no single-request component builds its managed Chat request or consumes a non-streaming tunnel terminal. Caller-facing codecs cannot be reused because they forward caller tool/schema choices and public error semantics.

**Solution**

Create `single_request_provider_stage.go` with a narrow `SubmitProviderPool` dependency. Build a non-streaming `chat_completions` operation from server-owned messages/options after candidate selection supplies the final target; attach only the frozen credential binding and predicate. Collect ordered response-start/body/end frames under the stage deadline and max-output bound, require HTTP 2xx and the frozen OpenAI Chat profile, decode exactly one assistant message, and return typed content/tool-call data without reasoning or raw provider errors.

Before (`apps/edge/internal/openai/server.go:23`):

```go
type runService interface {
    SubmitProviderPool(context.Context, edgeservice.ProviderPoolDispatchRequest) (*edgeservice.ProviderPoolDispatchResult, error)
}
```

After:

```go
type singleRequestProviderStageService interface {
    SubmitProviderPool(context.Context, edgeservice.ProviderPoolDispatchRequest) (*edgeservice.ProviderPoolDispatchResult, error)
}
```

**Modified Files and Checklist**

- [ ] Add `apps/edge/internal/openai/single_request_provider_stage.go` with request assembly, frozen candidate predicate, tunnel collection, typed decode, bounds, cancellation, and generic errors.
- [ ] Keep stage options server-owned: reserved `model`, `messages`, `tools`, `stream`, and credential fields cannot be overridden by option maps.
- [ ] Cover normal content, wrong candidate/profile/path, normalized result, non-2xx, bad frame order, duplicate terminal, oversized body, malformed JSON, missing choice, and unexpected Plan tool call in `apps/edge/internal/openai/single_request_plan_stage_test.go`.

**Test Strategy**

Write a narrow fake that implements only `SubmitProviderPool`, invokes `PrepareProtocolTunnel` with a selected candidate, captures the final body, and returns deterministic frame sequences. Assertions must inspect the effective target, immutable route predicate, credential binding, option precedence, close call, and generic error behavior.

**Verification**

Run `go test ./apps/edge/internal/openai -run 'TestSingleRequestProviderStage' -count=1`; all codec fixtures must pass.

### [API-3] Implement S08 Plan and persist `plan.md`

**Problem**

`apps/edge/internal/openai/anthropic_handler.go:223-227` passes the immutable admitted body to a service executor, but no runner emits `planning`, creates the required small plan/verification criteria, or writes the PLAN artifact.

**Solution**

Create `single_request_plan_stage.go`. Treat the admitted Anthropic JSON as immutable task data inside a fixed system/user prompt, explicitly ignore caller-selected models/tools as execution authority, merge only the frozen Plan options, and require `reasoning_effort=high`. Decode a strict server-owned JSON result containing non-empty `plan` and `verification` fields, render bounded Markdown, write `SingleRequestArtifactPlan` through the controller, and return the artifact bytes to the later Work runner. Submit only the `planning` envelope; do not install an incomplete outer executor.

Before (`apps/edge/internal/openai/anthropic_handler.go:223`):

```go
Prompt: string(append([]byte(nil), body...)),
```

After (new private runner contract):

```go
plan, err := runner.Run(ctx, req.Prompt, req.Binding.Plan, ctrl)
// writes the exact bounded rendering through SingleRequestArtifactPlan
```

**Modified Files and Checklist**

- [ ] Add fixed prompt/result schema and Plan stage runner in `apps/edge/internal/openai/single_request_plan_stage.go`.
- [ ] Add S08 fixtures in `apps/edge/internal/openai/single_request_plan_stage_test.go` for high option, immutable task inclusion, no caller-tool authority, small plan/verification rendering, artifact kind/content, malformed output, and write failure.

**Test Strategy**

Write `TestSingleRequestPlanStageWritesArtifact` and table-driven fail-closed cases. The success fixture must inspect the final provider JSON and controller write, and verify no public stream/output receives provider reasoning or stage terminal data.

**Verification**

Run `go test ./apps/edge/internal/openai -run 'TestSingleRequest(ProviderStage|PlanStage)' -count=1`; the S08 fixture and all boundaries must pass.

### [API-4] Record the partial implementation state

**Problem**

`agent-spec/runtime/edge-node-execution.md:286` says no provider-specific Plan/Work/Review driver exists. After this packet, Plan exists as a tested component but the composite execution remains intentionally inactive.

**Solution**

Update the current spec to describe the authorized route snapshot and tested Plan artifact runner, while stating that Work, Review/repair, composite installation, and actual Claude qualification are still deferred. Do not update the public Anthropic contract as though the endpoint were active.

**Modified Files and Checklist**

- [ ] Update `agent-spec/runtime/edge-node-execution.md` implementation/limitation/history sections with the partial state and S08 tests.

**Test Strategy**

No document-only test; deterministic searches and the executable fixtures validate the statement.

**Verification**

Run `rg --sort path -n 'Plan stage|plan\.md|Work|Review|not installed|deferred' agent-spec/runtime/edge-node-execution.md`; the spec must distinguish implemented Plan from inactive composite stages.

## Modified Files Summary

| File | Items |
|------|-------|
| `apps/edge/internal/service/single_request_types.go` | API-1 |
| `apps/edge/internal/service/single_request_types_test.go` | API-1 |
| `apps/edge/internal/openai/single_request_preset_binding.go` | API-1 |
| `apps/edge/internal/openai/single_request_preset_binding_test.go` | API-1 |
| `apps/edge/internal/openai/single_request_provider_stage.go` | API-2 |
| `apps/edge/internal/openai/single_request_plan_stage.go` | API-3 |
| `apps/edge/internal/openai/single_request_plan_stage_test.go` | API-2, API-3 |
| `agent-spec/runtime/edge-node-execution.md` | API-4 |
| `agent-task/m-iop-owned-single-request-agent-execution/18+17_plan_stage/CODE_REVIEW-cloud-G06.md` | API-1, API-2, API-3, API-4 |

## Final Verification

Fresh output is required; Go tests use `-count=1` and cached output is not acceptable.

1. `test -f agent-task/m-iop-owned-single-request-agent-execution/17_internal_artifact_wire/complete.log` — predecessor completion evidence exists before implementation or review.
2. `go test ./apps/edge/internal/service ./apps/edge/internal/openai -run 'TestSingleRequest(Binding|PresetBinding|ProviderStage|PlanStage)' -count=1` — admission, codec, and S08 fixtures pass.
3. `go vet ./apps/edge/internal/service ./apps/edge/internal/openai` — changed Edge packages vet cleanly.
4. `go test ./apps/edge/... -count=1` — broader Edge regression passes.
5. `rg --sort path -n 'SetSingleRequestExecutor|NewSingleRequestExecutor' apps/edge --glob '*.go'` — no production installation of an incomplete composite exists; only the existing setter/tests or explicitly deferred references appear.
6. `rg --sort path -n 'Plan stage|plan\.md|not installed|deferred' agent-spec/runtime/edge-node-execution.md` — spec records Plan as implemented and later stages/activation as deferred.
7. `git diff --check` — no whitespace errors.

Actual provider/Claude full-cycle evidence is intentionally excluded and remains owned by SDD S12 `claude-smoke` after the composite runner is implemented.

After completing all code changes, fill implementation-owned sections in `CODE_REVIEW-*-G??.md`.
