<!-- task=m-iop-owned-single-request-agent-execution/01_preset_config plan=3 tag=REVIEW_API milestone-task=preset-binding -->

# Restore Fail-closed Single-request Preset Binding

## For the Implementing Agent

Implement this plan exactly within the listed write boundary. Run every verification command, fill all implementation-owned sections of `CODE_REVIEW-cloud-G04.md` with actual notes and stdout/stderr, keep both active files in place, and report ready for review. Finalization belongs only to the code-review skill. If blocked, record the exact blocker, attempted commands/output, and resume condition in implementation-owned evidence fields; do not ask the user, call user-input tools, create control-plane stop files, classify the next state, archive logs, or write `complete.log`.

## Background

The failed review proved that a marked preset can expose different models and options through `routes.light` and `single_request.stages`, so the declared fixed binding is not immutable. The implementation also introduced second-based limit keys that conflict with the approved SDD millisecond contract and omitted the planned nested unknown-field tests. This follow-up restores one fail-closed config contract without entering the refresh/publication or runtime packets.

## Archive Evidence Snapshot

- Failed pair: `plan_local_G04_2.log` and `code_review_cloud_G04_2.log`; verdict `FAIL` with Required R1, R2, and R3, zero Suggested findings, and no residual Nit.
- Fresh build, planned config/package tests, vet, and `git diff --check` passed, but the reviewer reproducer showed `LoadEdge` accepting divergent route/policy work models.
- Roadmap carryover: `milestone-task=preset-binding`, approved SDD S02, immutable fixed-light plan/work/review binding, and millisecond request/stage limit keys.

## Finding Resolution Map

| Finding | Mode | Exact resolution | Changed precondition |
|---------|------|------------------|----------------------|
| Required R1 | `direct-fix` | In `packages/go/config/execution_preset_types.go`, validate every effective marked route stage against the catalog, require route model/options to equal the matching typed policy stage, and reject any work `reasoning_effort` key. Add divergent/dangling/option-leakage cases in `packages/go/config/single_request_execution_preset_config_test.go`. | The prior suite checked route roles only; new rows exercise the previously accepted malformed bindings. |
| Required R2 | `direct-fix` | Rename the request/stage time fields, tags, caps, diagnostics, and fixtures to approved millisecond semantics: `wall_clock_ms` and per-stage `timeout_ms`. | The follow-up uses the approved SDD interface instead of publishing the unapproved second-based schema. |
| Required R3 | `direct-fix` | Add nested unknown-field decode rows for the policy root, limits, stages, and individual stage binding. | The strict decoder is now exercised at every new schema nesting boundary rather than only at the legacy preset root. |

## Analysis

### Files Read

- `packages/go/config/execution_preset_types.go`
- `packages/go/config/load.go`
- `packages/go/config/execution_preset_config_test.go`
- `packages/go/config/single_request_execution_preset_config_test.go`
- `apps/edge/internal/openai/principal_routes.go`
- `apps/edge/internal/openai/route_resolution.go`
- `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-contract/inner/edge-config-runtime-refresh.md`
- `agent-contract/outer/anthropic-compatible-api.md`
- `agent-spec/runtime/provider-pool-config-refresh.md`
- `agent-test/local/rules.md`
- `agent-test/local/platform-common-smoke.md`

### SDD Criteria

- SDD: `agent-roadmap/sdd/knowledge-tool-optimization-extension/iop-owned-single-request-agent-execution/SDD.md`; status `[승인됨]`, lock released, and no user review.
- First-line Milestone task: `preset-binding`; targeted Acceptance Scenario: S02.
- S02 and its Evidence Map require immutable fixed-light decode/binding evidence. The Interface Contract requires `wall_clock_ms`, per-stage `timeout_ms`, positive bounded limits, plan/review high reasoning, and work high-option absence.
- R1 drives binding parity and option-leakage regression tests. R2 drives the typed millisecond schema and cap boundaries. R3 supplies the planned fail-closed nested decode evidence.

### Verification Context

- No separate verification handoff was supplied. Repository-native fallback came from `agent-test/local/rules.md`, `agent-test/local/platform-common-smoke.md`, the existing config tests, the approved SDD, and the active review evidence.
- Reviewer preflight confirmed `/config/workspace/iop-s0`, Go `go1.26.2 linux/arm64`, and the shared dirty worktree. No credential or external service is required.
- Fresh `go build ./packages/go/...`, focused config tests, full config tests, `go vet ./packages/go/...`, `go test ./packages/go/... -count=1`, and `git diff --check` passed. A focused fresh reproducer failed because divergent work models were accepted.
- Final checks use `-count=1`; cached test output is not acceptable. Actual Claude/provider smoke remains a later Milestone-wide task because this packet changes only config decode/validation/cloning.
- Preconditions: none. Constraint: preserve ordinary direct/light preset behavior and keep refresh/publication work in packet 04. Confidence: high after the deterministic reproducer.

### Test Coverage Gaps

- Route/policy model and option parity: not covered; add plan/work/review mismatch rows plus a dangling route model row.
- Work reasoning absence: only exact `high` is covered; add non-high string and non-string key-presence rows and route-side leakage.
- Millisecond limits: current tests cover second-based fields; rename fixtures and retain zero/exact-cap/cap+1/cross-limit boundaries in milliseconds.
- Nested unknown fields: not covered; add root/limits/stages/stage-binding rows.
- Clone isolation and ordinary preset compatibility are covered and must remain unchanged apart from field renames.

### Symbol References

- Rename `MaxSingleRequestWallClockSec`, `MaxSingleRequestStageTimeoutSec`, `ExecutionSingleRequestLimits.WallClockSec`, and `ExecutionSingleRequestLimits.StageTimeoutSec` to millisecond equivalents.
- Deterministic `rg --sort path` found production references only in `packages/go/config/execution_preset_types.go` and test references only in `packages/go/config/single_request_execution_preset_config_test.go`; the archived review log is evidence, not a call site to edit.
- No dependency or import is added. Existing `reflect` support can compare normalized option maps.

### Split Judgment

- Keep one compact follow-up. Binding parity, reasoning leakage, limit tags, and their strict decode tests form one config admission invariant and share the same validator and fixture; splitting would allow a schema that still admits an ambiguous fixed binding.

### Scope Rationale

- Modify only the typed preset validator and its dedicated tests. `load.go` already provides strict nested `mapstructure` decoding and needs no change.
- Exclude refresh classification, `configs/edge.yaml`, config contract/spec publication, authorization/model echo, handlers, provider execution, Node workspace execution, protobuf, and SSE. Packet 04 publishes the schema after this child passes; runtime packets consume the validated snapshot.
- Do not edit the approved SDD or roadmap. The code aligns to the existing SDD decision and completion remains runtime-aggregated.

### Final Routing

- `evaluation_mode=isolated-reassessment`; finalizer `finalize-task-policy.sh` in `pair` mode.
- Build closures scope/context/verification/evidence/ownership/decision are true; scores 1/0/1/1/1 = G04; base `local-fit`, final route `recovery-boundary`, lane `cloud`, canonical filename `PLAN-cloud-G04.md`.
- Build signals: `large_indivisible_context=false`; positive loop risks `boundary_contract`, `structured_interpretation`, `variant_product` (3); `review_rework_count=1`; `evidence_integrity_failure=true`; no capability gap.
- Review closures are true; scores 1/0/1/1/1 = G04; route `official-review`, lane `cloud`, canonical filename `CODE_REVIEW-cloud-G04.md`.

## Implementation Checklist

- [ ] Make marked fixed-light presets admit one effective plan/work/review model-and-option binding, reject dangling/divergent route bindings and every work `reasoning_effort` key, and add regression variants for R1.
- [ ] Align request/stage time fields, tags, caps, diagnostics, fixtures, and boundaries to `wall_clock_ms` / `timeout_ms`, and add all nested unknown-field cases for R2 and R3.
- [ ] Run fresh formatting, focused config, build, full config, vet, full shared-package regression, and `git diff --check` verification.
- [ ] Fill implementation-owned sections in CODE_REVIEW-*-G??.md with actual implementation notes and verification output.

## Implementation Plan

### [REVIEW_API-1] Validate one effective fixed-light stage binding

**Problem**

- `packages/go/config/execution_preset_types.go:450` skips the normal light-route validator whenever `SingleRequest` is non-nil.
- `packages/go/config/execution_preset_types.go:724` checks only route count and role order. Route models/options can diverge from the typed stage map or reference a missing catalog model, while `CanonicalModelReferences` and current consumers read the route copy.
- `packages/go/config/execution_preset_types.go:712` rejects only the exact string `high`; a present `reasoning_effort: medium` or non-string value survives even though SDD S09 requires high-option absence on work.

**Solution**

Before (`packages/go/config/execution_preset_types.go:724`):

```go
route, hasRoute := p.Routes[ModeLight]
if !hasRoute {
    return fmt.Errorf("...")
}
if len(route.Stages) != singleRequestRequiredStagesCount {
    return fmt.Errorf("...")
}
for i, expectedRole := range singleRequestRequiredStageRoles {
    if route.Stages[i].Role != expectedRole {
        return fmt.Errorf("...")
    }
}
```

After:

```go
expectedStages := []ExecutionSingleRequestStageConfig{
    stages.Plan,
    stages.Work,
    stages.Review,
}
for i, expectedRole := range singleRequestRequiredStageRoles {
    routeStage := route.Stages[i]
    if routeStage.Role != expectedRole {
        return fmt.Errorf("...")
    }
    if _, ok := canonicalModelIDs[routeStage.Model]; !ok {
        return fmt.Errorf("...")
    }
    if routeStage.Model != expectedStages[i].Model || !reflect.DeepEqual(routeStage.Options, expectedStages[i].Options) {
        return fmt.Errorf("... route binding must match single_request stage ...")
    }
}
if _, present := stages.Work.Options["reasoning_effort"]; present {
    return fmt.Errorf("... work.options must not declare reasoning_effort")
}
```

Keep the existing typed stage map as the canonical policy source and require the compatibility route representation to match it exactly after decoding. Preserve selector high validation and require the selector binding to match the plan stage if both continue to represent the same fixed planning binding; do not silently normalize contradictory input.

**Modified Files and Checklist**

- [ ] `packages/go/config/execution_preset_types.go` — validate marked route model catalog membership, route/policy model-and-option parity, selector/plan parity where applicable, and work reasoning-key absence.
- [ ] `packages/go/config/single_request_execution_preset_config_test.go` — add table rows for valid-catalog mismatch, dangling route model, plan/work/review option mismatch, route-side high leakage, and non-high/non-string work reasoning keys.

**Test Strategy**

- Extend `TestLoadEdgeSingleRequestExecutionPresetRejectsInvalidShape`; every malformed binding must return an error naming the role and mismatch.
- Keep `TestLoadEdgeSingleRequestExecutionPreset` as the valid parity oracle and assert route models/options match the decoded typed stages.

**Verification**

- `go test ./packages/go/config -run 'Test(LoadEdgeSingleRequestExecutionPreset|LoadEdgeSingleRequestExecutionPresetRejectsInvalidShape)$' -count=1`
- Expected: all mismatch/leakage rows fail closed and the valid fixed binding loads.

### [REVIEW_API-2] Align limit units and strict nested schema coverage

**Problem**

- `packages/go/config/execution_preset_types.go:257` defines second-based caps and lines 287-289 publish `wall_clock_sec` / `stage_timeout_sec`, conflicting with the approved SDD at `agent-roadmap/sdd/knowledge-tool-optimization-extension/iop-owned-single-request-agent-execution/SDD.md:87`.
- `packages/go/config/single_request_execution_preset_config_test.go:306` has no nested unknown-field case despite the original plan requirement.

**Solution**

Before (`packages/go/config/execution_preset_types.go:256`):

```go
const (
    MaxSingleRequestWallClockSec    = 30 * 60
    MaxSingleRequestStageTimeoutSec = 10 * 60
)

type ExecutionSingleRequestLimits struct {
    WallClockSec   int `mapstructure:"wall_clock_sec" yaml:"wall_clock_sec"`
    StageTimeoutSec int `mapstructure:"stage_timeout_sec" yaml:"stage_timeout_sec"`
    // unchanged per-stage count/byte limits
}
```

After:

```go
const (
    MaxSingleRequestWallClockMS    = 30 * 60 * 1000
    MaxSingleRequestStageTimeoutMS = 10 * 60 * 1000
)

type ExecutionSingleRequestLimits struct {
    WallClockMS    int `mapstructure:"wall_clock_ms" yaml:"wall_clock_ms"`
    StageTimeoutMS int `mapstructure:"timeout_ms" yaml:"timeout_ms"`
    // unchanged per-stage count/byte limits
}
```

Treat `timeout_ms` as the cap independently applied to each plan/work/review stage. Update range/cross-limit diagnostics and every fixture/assertion atomically. Add table-driven mutations that insert one unknown key at each new nested boundary and assert strict decode failure names the key.

**Modified Files and Checklist**

- [ ] `packages/go/config/execution_preset_types.go` — rename millisecond fields/constants/tags and preserve 30-minute/10-minute absolute durations.
- [ ] `packages/go/config/single_request_execution_preset_config_test.go` — convert all fixtures/assertions/boundaries to milliseconds and add nested unknown-field rows.

**Test Strategy**

- Retain minimum, exact-cap, cap+1, and timeout-greater-than-wall-clock rows using `1`, `1800000`, and `600000` millisecond boundaries.
- Add strict decode rows for unknown fields under `single_request`, `single_request.limits`, `single_request.stages`, and `single_request.stages.work`.
- Rerun the legacy execution-preset catalog/rejection tests to prove unmarked compatibility.

**Verification**

- `go test ./packages/go/config -run 'Test(LoadEdgeSingleRequestExecutionPreset|CloneExecutionPresetSingleRequestIsolation|LoadEdgeExecutionPresetCatalog|LoadEdgeExecutionPresetRejectsInvalidShape)$' -count=1`
- Expected: the millisecond schema loads at valid boundaries, legacy second keys and nested unknown keys fail strict decode, and clones remain isolated.

## Modified Files Summary

| File | Item |
|------|------|
| `packages/go/config/execution_preset_types.go` | REVIEW_API-1, REVIEW_API-2 |
| `packages/go/config/single_request_execution_preset_config_test.go` | REVIEW_API-1, REVIEW_API-2 |
| `agent-task/m-iop-owned-single-request-agent-execution/01_preset_config/CODE_REVIEW-cloud-G04.md` | REVIEW_API-1, REVIEW_API-2 |

## Final Verification

1. `gofmt -d packages/go/config/execution_preset_types.go packages/go/config/single_request_execution_preset_config_test.go`
2. `go build ./packages/go/...`
3. `go test ./packages/go/config -run 'Test(LoadEdgeSingleRequestExecutionPreset|CloneExecutionPresetSingleRequestIsolation|LoadEdgeExecutionPresetCatalog|LoadEdgeExecutionPresetRejectsInvalidShape)$' -count=1`
4. `go test ./packages/go/config -count=1`
5. `go vet ./packages/go/...`
6. `go test ./packages/go/... -count=1`
7. `git diff --check`

Expected: formatting emits no diff; all commands exit 0; divergent/dangling bindings, work reasoning leakage, legacy second keys, and nested unknown fields fail closed; valid millisecond bounds, ordinary presets, and clone isolation pass.

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