fix(single_request): 템플릿 승인 경계를 닫고 회귀 근거를 채운다
공식 리뷰가 지적한 두 가지 승인 경계 결함을 고친다. `plan_file`/`review_file`은
edge.yaml 디렉터리 기준 상대 경로만 허용하고 절대/빈 경로는 파일 접근 전에 거부한다.
필수 heading과 Review `PASS`는 부분 문자열이 아니라 정확한 단독 줄로 검증하며,
문서화된 placeholder를 제거한 뒤 남는 `{{`/`}}`를 거부해 문법을 닫는다.
리뷰가 존재한다고 기술했지만 실제로는 없던 회귀 근거를 추가한다. admission 시
고정된 effective 템플릿 쌍이 clone과 workspace 재검증을 통과하는지, refresh가
이미 승인된 요청이 아니라 새 요청에만 적용되는지, custom Review 템플릿이 내부
artifact만 바꾸고 caller 최종 출력은 그대로인지를 각각 확정 검증한다.
현재 문서도 실제 동작에 맞춘다. Edge 실행 spec의 낡은 Plan JSON Schema 서술을
direct PlanMD 검증으로 고치고, outer Anthropic 계약과 input spec에 템플릿이
Edge 내부 stage 입력일 뿐 caller 요청/응답 계약을 바꾸지 않음을 명시한다.
Refs: agent-task/single_request_plan_review_templates/PLAN-cloud-G08.md
This commit is contained in:
parent
ee9204a56d
commit
31fada5d08
18 changed files with 1855 additions and 81 deletions
|
|
@ -13,7 +13,7 @@
|
|||
| id | 읽는 조건 | 원본 경로 | path |
|
||||
|----|-----------|-----------|------|
|
||||
| `iop.openai-compatible-api` | OpenAI-compatible API, Responses API, Chat Completions, legacy Completions, error envelope/SSE terminal error, `model` route, managed projection principal auth and slot-route binding, managed-versus-legacy provider credential selection, model-driven passthrough/normalized routing, provider-pool admission/unavailable error, safe credential-slot attribution, standard metadata, and provider-native extension fields such as `chat_template_kwargs` | `apps/edge/internal/openai/*`, `apps/edge/internal/authprojection/*`, `apps/edge/internal/service/provider_tunnel.go`, `packages/go/config/config.go`, `configs/edge.yaml` | `agent-contract/outer/openai-compatible-api.md` |
|
||||
| `iop.anthropic-compatible-api` | Anthropic Messages API, count_tokens, models list, bearer or `X-Api-Key` principal auth, active managed projection auth and slot-route binding, `anthropic-version` routing, native Anthropic tunnel, Chat bridge, provider-pool-only admission, profile capability checks, managed-versus-legacy provider credentials, and current no-OpenAI-metric status | `apps/edge/internal/openai/anthropic_handler.go`, `apps/edge/internal/openai/anthropic_native.go`, `apps/edge/internal/openai/anthropic_bridge.go`, `apps/edge/internal/openai/anthropic_stream.go`, `apps/edge/internal/openai/anthropic_types.go`, `apps/edge/internal/openai/routes.go`, `apps/edge/internal/openai/principal.go`, `apps/edge/internal/authprojection/*`, `apps/edge/internal/openai/provider_tunnel.go`, `apps/edge/internal/openai/provider_model_rewrite.go`, `packages/go/config/protocol_profile.go` | `agent-contract/outer/anthropic-compatible-api.md` |
|
||||
| `iop.anthropic-compatible-api` | Anthropic Messages API, count_tokens, models list, bearer or `X-Api-Key` principal auth, active managed projection auth and slot-route binding, `anthropic-version` routing, native Anthropic tunnel, Chat bridge, provider-pool-only admission, profile capability checks, managed-versus-legacy provider credentials, marked-preset single-request admission with Edge-owned internal Plan/Review template customization that leaves caller I/O unchanged, and current no-OpenAI-metric status | `apps/edge/internal/openai/anthropic_handler.go`, `apps/edge/internal/openai/anthropic_native.go`, `apps/edge/internal/openai/anthropic_bridge.go`, `apps/edge/internal/openai/anthropic_stream.go`, `apps/edge/internal/openai/anthropic_types.go`, `apps/edge/internal/openai/routes.go`, `apps/edge/internal/openai/principal.go`, `apps/edge/internal/authprojection/*`, `apps/edge/internal/openai/provider_tunnel.go`, `apps/edge/internal/openai/provider_model_rewrite.go`, `apps/edge/internal/openai/single_request_preset_binding.go`, `apps/edge/internal/openai/single_request_plan_stage.go`, `apps/edge/internal/openai/single_request_review_stage.go`, `packages/go/singlerequesttemplate/template.go`, `packages/go/config/protocol_profile.go` | `agent-contract/outer/anthropic-compatible-api.md` |
|
||||
| `iop.a2a-json-rpc-api` | A2A JSON-RPC API, `message/send`, `tasks/get`, `tasks/cancel`, A2A task state, agent card, `a2a.bearer_token`, Edge A2A input surface | `apps/edge/internal/input/a2a/*`, `packages/go/config/config.go`, `configs/edge.yaml` | `agent-contract/outer/a2a-json-rpc-api.md` |
|
||||
|
||||
## Inner Contracts
|
||||
|
|
|
|||
|
|
@ -15,6 +15,11 @@
|
|||
- `apps/edge/internal/service/single_request.go`
|
||||
- `apps/edge/internal/service/single_request_tool_types.go`
|
||||
- `apps/edge/internal/service/single_request_tool_loop.go`
|
||||
- `apps/edge/internal/service/single_request_types.go`
|
||||
- `apps/edge/internal/openai/single_request_preset_binding.go`
|
||||
- `apps/edge/internal/openai/single_request_plan_stage.go`
|
||||
- `apps/edge/internal/openai/single_request_review_stage.go`
|
||||
- `packages/go/singlerequesttemplate/template.go`
|
||||
- `apps/edge/internal/openai/anthropic_types.go`
|
||||
- `apps/edge/internal/openai/routes.go`
|
||||
- `apps/edge/internal/openai/principal.go`
|
||||
|
|
@ -92,6 +97,26 @@ binding (route ID, credential slot, provider ID, endpoint, or raw workspace data
|
|||
echoed to the caller. The admission is owned by the service package; the OpenAI and
|
||||
Anthropic surfaces read only the public model identity and the frozen limits.
|
||||
|
||||
The admission also freezes the effective Plan and Review Markdown templates resolved
|
||||
from the operator's `execution_presets[].single_request.templates` configuration (see
|
||||
`agent-contract/inner/edge-config-runtime-refresh.md`). These templates are
|
||||
Edge-owned internal stage inputs only:
|
||||
|
||||
- The Plan template is the private Plan-stage prompt and the grammar its direct
|
||||
PlanMD output is parsed against; the Review template shapes the private `review.md`
|
||||
artifact rendered from the model's `checks`, `verification`, and `summary` fields.
|
||||
- The caller-visible request and response schemas are unchanged. A configured
|
||||
template never adds, removes, or renames a Messages request field, a content block,
|
||||
an SSE event, a `stop_reason`, or an error shape, and the final Messages text stays
|
||||
the model's `decision.output`.
|
||||
- Callers cannot supply, name, select, or override a template. Template file paths,
|
||||
template contents, and template digests are never echoed in a response, an error
|
||||
message, or a metric label.
|
||||
- An already admitted request keeps its frozen template pair for its whole lifetime.
|
||||
A config refresh that changes a template reaches only requests admitted after it,
|
||||
and a template that no longer satisfies the documented grammar fails admission
|
||||
closed rather than degrading to a default.
|
||||
|
||||
### Marked preset: one-ingress runtime boundary
|
||||
|
||||
After request validation, principal authorization, and immutable preset resolution, a
|
||||
|
|
@ -547,6 +572,7 @@ Built-in API-key profiles such as `seulgi_messages` may declare their auth heade
|
|||
- raw provider token을 Edge config, tracked docs, roadmap, task artifact, metric label에 저장하지 않는다.
|
||||
- missing required provider auth error body나 log에 raw header 값을 echo하지 않는다.
|
||||
- Never expose a slot alias, raw credential, lease id, ciphertext, private key, recipient material, or target URL through an Anthropic error, log, event, metric, or task artifact.
|
||||
- Marked-preset Plan/Review template customization은 caller-visible 계약이 아니다. 요청 body, header, metadata로 템플릿을 선택하거나 전달할 수 없고, 템플릿 경로·본문·digest를 응답, 오류, log, metric label로 노출하지 않는다.
|
||||
|
||||
## 관련 계약
|
||||
|
||||
|
|
|
|||
|
|
@ -32,10 +32,10 @@ AI agent가 작업 전에 읽는 지도이기도 하지만, 사람도 "지금
|
|||
|
||||
| id | 상태 | 언제 읽나 | path | 주요 근거 |
|
||||
|----|------|-----------|------|-----------|
|
||||
| `runtime/edge-node-execution` | 구현됨 | Edge-Node mTLS/protobuf transport, Node 등록, transport heartbeat/reconnect, provider run/cancel/command, provider raw tunnel, signed/sealed credential lease consumption을 확인할 때 | `agent-spec/runtime/edge-node-execution.md` | `agent-contract/inner/execution-runtime.md`, `agent-contract/inner/edge-node-runtime-wire.md`, `apps/edge/internal/transport/server.go`, `apps/node/internal/transport/client.go` |
|
||||
| `runtime/edge-node-execution` | 구현됨 | Edge-Node mTLS/protobuf transport, Node 등록, transport heartbeat/reconnect, provider run/cancel/command, provider raw tunnel, signed/sealed credential lease consumption, single-request Plan/Review effective template 적용을 확인할 때 | `agent-spec/runtime/edge-node-execution.md` | `agent-contract/inner/execution-runtime.md`, `agent-contract/inner/edge-node-runtime-wire.md`, `apps/edge/internal/transport/server.go`, `apps/node/internal/transport/client.go`, `packages/go/singlerequesttemplate/template.go` |
|
||||
| `runtime/stream-evidence-gate` | 구현됨 | Stream Evidence Gate의 normalized event, evidence hold/release, filter registry, recovery coordinator, OpenAI request rebuild와 observation을 확인할 때 | `agent-spec/runtime/stream-evidence-gate.md` | `packages/go/streamgate/runtime.go`, `apps/edge/internal/openai/stream_gate_runtime.go`, `agent-contract/outer/openai-compatible-api.md` |
|
||||
| `runtime/provider-pool-config-refresh` | 부분 | `credential_plane`, managed/legacy exclusivity, TLS/key references, `models[]`, top-level `protocol_profiles`, `nodes[].providers[].profile`, provider-pool dispatch, long-context admission, and restart/applied refresh classification을 확인할 때 | `agent-spec/runtime/provider-pool-config-refresh.md` | `agent-contract/inner/edge-config-runtime-refresh.md`, `packages/go/config/provider_types.go`, `packages/go/config/validate.go`, `apps/edge/internal/configrefresh/classify.go` |
|
||||
| `input/openai-compatible-surface` | 부분 | `/v1/models`, `/v1/chat/completions`, `/v1/responses`, `/v1/messages`, `/v1/messages/count_tokens`, `/anthropic/v1/models`, managed projection/slot routing, OpenAI-compatible auth/metadata/tool handling, Anthropic bearer/`X-Api-Key` auth, provider-pool native/bridge admission, safe slot attribution, and OpenAI-only usage metrics를 확인할 때 | `agent-spec/input/openai-compatible-surface.md` | `agent-contract/outer/openai-compatible-api.md`, `agent-contract/outer/anthropic-compatible-api.md`, `apps/edge/internal/openai/chat_handler.go`, `apps/edge/internal/openai/anthropic_handler.go`, `apps/edge/internal/openai/anthropic_bridge.go`, `apps/edge/internal/openai/normalized_sse.go`, `apps/edge/internal/openai/usage_metrics.go` |
|
||||
| `runtime/provider-pool-config-refresh` | 부분 | `credential_plane`, managed/legacy exclusivity, TLS/key references, `models[]`, top-level `protocol_profiles`, `nodes[].providers[].profile`, provider-pool dispatch, long-context admission, `execution_presets[].single_request.templates`의 relative-only 로딩과 admission freeze, and restart/applied refresh classification을 확인할 때 | `agent-spec/runtime/provider-pool-config-refresh.md` | `agent-contract/inner/edge-config-runtime-refresh.md`, `packages/go/config/provider_types.go`, `packages/go/config/validate.go`, `packages/go/config/load.go`, `apps/edge/internal/configrefresh/classify.go` |
|
||||
| `input/openai-compatible-surface` | 부분 | `/v1/models`, `/v1/chat/completions`, `/v1/responses`, `/v1/messages`, `/v1/messages/count_tokens`, `/anthropic/v1/models`, managed projection/slot routing, OpenAI-compatible auth/metadata/tool handling, Anthropic bearer/`X-Api-Key` auth, provider-pool native/bridge admission, safe slot attribution, marked single-request 내부 stage template과 caller-visible I/O 경계, and OpenAI-only usage metrics를 확인할 때 | `agent-spec/input/openai-compatible-surface.md` | `agent-contract/outer/openai-compatible-api.md`, `agent-contract/outer/anthropic-compatible-api.md`, `apps/edge/internal/openai/chat_handler.go`, `apps/edge/internal/openai/anthropic_handler.go`, `apps/edge/internal/openai/anthropic_bridge.go`, `apps/edge/internal/openai/normalized_sse.go`, `apps/edge/internal/openai/usage_metrics.go` |
|
||||
| `input/a2a-json-rpc-surface` | 부분 | Edge A2A JSON-RPC, `message/send`, `tasks/get`, `tasks/cancel`, A2A task store와 bearer auth를 확인할 때 | `agent-spec/input/a2a-json-rpc-surface.md` | `agent-contract/outer/a2a-json-rpc-api.md`, `apps/edge/internal/input/a2a/server.go`, `apps/edge/internal/input/a2a/task_store.go` |
|
||||
| `control/control-plane-operations` | 부분 | credential HTTPS and host-local bootstrap, Control Plane-Edge mTLS projection/lease wire, Client-Control Plane wire, Control Plane HTTP Edge/fleet status view, Flutter Client status consumer를 확인할 때 | `agent-spec/control/control-plane-operations.md` | `agent-contract/inner/control-plane-edge-wire.md`, `agent-contract/inner/client-control-plane-wire.md`, `apps/control-plane/internal/wire/edge_server.go`, `apps/control-plane/internal/credentiallease/service.go` |
|
||||
|
||||
|
|
|
|||
|
|
@ -169,6 +169,7 @@ Edge가 OpenAI-compatible HTTP 요청을 받아 내부 `adapter + target` 실행
|
|||
| managed projection auth | `credential_plane.enabled=true` uses the fresh Control Plane projection for inbound token auth and principal route discovery. Static principal/bearer fallback is disabled. |
|
||||
| managed slot route | Public model id/alias resolves to one projected route, exact slot/profile/upstream model/resource selector, and immutable revisions/generation. Unknown, cross-principal, stale, revoked, or ambiguous bindings fail closed. |
|
||||
| marked preset single-request admission | An authorized fixed single-request preset compiles one service-owned admission value at request start: requested public model, canonical plan/work/review bindings resolved through managed authorization, opaque workspace capability, and absolute resource caps. Later refresh cannot mutate the admitted shape. No private binding is echoed to the caller. Compiled only after every canonical reference is verified through its catalog binding for the authenticated principal; missing, duplicate, unauthorized, dynamically selected, or option-inconsistent inputs are rejected without fallback. |
|
||||
| marked single-request internal templates | The admission also freezes the operator-configured effective Plan/Review Markdown templates. They are internal stage inputs only: the Plan template is the private Plan-stage prompt and PlanMD grammar, and the Review template shapes the private `review.md` artifact. Callers cannot supply, name, or select a template, and template paths, contents, and digests never appear in a response, error, log, or metric label. The caller-visible Messages request/response schema is unchanged and the final text stays the model's `decision.output`. |
|
||||
| marked single-request ingress | One validated and authorized Messages POST enters the separate service coordinator capability before legacy provider/caller continuation and increments `iop_anthropic_single_request_ingress_total` once. Non-streaming returns one buffered final-only message. Streaming keeps one envelope across the coordinator lifetime, exposes only fixed plan/work/review/repair text blocks plus `event: ping`, and commits one final text/error terminal. Internal reasoning/tool wire never becomes caller `tool_use`; success is acknowledged only after the complete terminal write succeeds. |
|
||||
| marked single-request S11 terminal policy | The service freezes one closed `end_turn`, `length`, `error`, or `cancelled` disposition. `error` classes are provider, validation, timeout, budget, repetition, malformed, context, internal-tool, and workspace-cleanup. Buffered and SSE share one projection: `end_turn`; `max_tokens` with no private partial output; `400 invalid_request_error` for validation/context; `502 api_error` for other failures; and silent cancellation after caller disconnect. No terminal classification retries, falls back, opens a second request, or later writes success. |
|
||||
| marked single-request observation evidence | A single real POST links ingress=1, request-total=1, terminal=1, stage/tool/cleanup counts, and raw-free correlation without public tool protocol. `iop_anthropic_single_request_ingress_total` is unlabeled (no request_id, stage_id, provider identity, or content). Internal tool names, raw arguments, private results, and workspace references are absent from the public terminal and from log projections. Stage-pure timing, cardinality-bounded labels, and privacy semantics are documented here. SDD S12 qualified an external Claude task on an approved IOP Node with ingress `0→1`, Gemini → ornith-fast → Gemini, one `end_turn`, the exact expected workspace result, timing, cleanup, and redacted evidence. |
|
||||
|
|
@ -261,6 +262,7 @@ sequenceDiagram
|
|||
- provider-pool model group은 capacity + priority + availability 기준으로 provider candidate를 먼저 선택하고, 선택된 provider가 OpenAI-compatible 호출 방식을 지원하면 raw tunnel passthrough로 dispatch한다. Ollama/native provider가 선택되면 normalized `RunRequest` path로 dispatch한다.
|
||||
- Anthropic Messages and count-tokens do not use legacy direct-route or single-target fallback. Native responses preserve provider status, allowed headers, and body/SSE bytes; bridge responses are converted between Anthropic Messages and Chat Completions shapes.
|
||||
- A marked single-request Messages dispatch requires the narrow service coordinator capability and never falls back to the generic provider pool. The handler copies the immutable binding and request input and counts the accepted HTTP admission once with no labels. The service projects exactly one frozen terminal candidate through both response modes: buffered/SSE `end_turn`; buffered/SSE `max_tokens` without private partial content; `invalid_request_error` for validation/context; `api_error` for provider, timeout, budget, repetition, malformed, internal-tool, and workspace-cleanup failures; or silent cancellation after caller disconnect. The streaming path maps only fixed plan/work/review/repair summaries, serializes pings and monotonic text-block indices with one terminal owner, stops and joins liveness before terminal/return, and acknowledges completion only after `message_stop`. Arbitrary progress, reasoning, tool/provider/credential/workspace data, raw failures, and internal stage terminals stay private. No classified terminal triggers retry, fallback, partial success, a second request, or a later success terminal. Count-tokens does not enter or increment this path.
|
||||
- Marked single-request Plan/Review templates are Edge-owned internal stage inputs, not part of this input surface. The operator configures them in `execution_presets[].single_request.templates`; admission freezes the effective pair, so a config refresh reaches only requests admitted after it and an already running request keeps its pair. The Plan template supplies the private Plan-stage prompt and the grammar its direct PlanMD output is parsed against, and the Review template shapes the private `review.md` artifact rendered from the model's `checks`/`verification`/`summary` fields. No caller field, header, or metadata value can supply, name, select, or override a template, and no template path, content, or digest appears in a response, an error message, a log projection, or a metric label. Changing a template changes neither the Messages request schema nor the response schema: the buffered/SSE terminal projection is unchanged and the final caller-visible text remains the model's `decision.output`.
|
||||
- Marked single-request observation evidence links ingress=1, request-total=1, terminal=1, stage/tool/cleanup counts, and raw-free correlation for one real POST. `iop_anthropic_single_request_ingress_total` is strictly unlabeled: no request_id, stage_id, provider identity, content, or workspace reference appears as a metric label. Internal tool names (`workspace_read`, `workspace_write`, etc.), raw arguments, private results, and workspace references are absent from the public terminal JSON and from log projections. Stage-pure timing, cardinality-bounded labels, and privacy semantics are documented here. SDD S12 qualifies the external Claude path on an approved IOP Node with one accepted ingress, the expected stage sequence, one terminal, exact output, timing, cleanup, and redacted evidence.
|
||||
- Internal workspace calls use a service-owned schema independent of caller-facing tool codecs. The five closed operation names decode into typed Node requests only after request/stage/tool identity, canonical relative path, approved operation/command/environment capability, and immutable budget checks. The loop opens once, preserves the admitted connection generation, executes one pending call at a time, accepts only correlated typed results, and returns a deep-copied raw-free result to the same executor continuation. Repeated IDs, stale responses, malformed or denied input, timeout, output/iteration exhaustion, and cancellation never become public Anthropic tool protocol or trigger a second ingress.
|
||||
- Claude Code Messages requests may use adaptive thinking, `output_config.effort`, structured output, cache-control annotations, and supported beta headers. The Chat bridge consumes those headers, maps supported fields, and requires callers to replay opaque `tool_use.id` values unchanged so Gemini thought signatures can be restored on tool-result turns.
|
||||
|
|
@ -289,6 +291,7 @@ sequenceDiagram
|
|||
- `go test -race ./apps/edge/internal/service -run 'Test(InternalWorkspaceTool|SingleRequestInternalToolLoop)' -count=1`
|
||||
- `go test ./apps/edge/internal/openai -run 'TestAnthropicSingleRequest(UsesOnePost|InternalToolsStayPrivate|Observation)' -count=1`
|
||||
- `go test -race ./apps/edge/internal/openai -run 'TestSingleRequestAnthropicStream' -count=1`
|
||||
- `go test -count=1 ./apps/edge/internal/openai -run 'TestSingleRequest(PresetBinding|ReviewStage)'` — admitted effective-template snapshot, refresh isolation, and the custom-template internal REVIEW artifact with unchanged caller-visible output.
|
||||
- `go test -race -count=1 ./packages/go/streamgate ./apps/edge/internal/openai ./packages/go/config`
|
||||
- `go test ./apps/edge/internal/service`
|
||||
- `go test ./apps/edge/internal/openai -run 'Tunnel|UsageMetrics|ToolValidation|Dispatch|Reasoning|Retry'`
|
||||
|
|
@ -323,6 +326,7 @@ sequenceDiagram
|
|||
|
||||
## 변경 기록
|
||||
|
||||
- 2026-08-09: Separated Edge-owned internal Plan/Review stage templates from caller-visible I/O. Operator-configured `execution_presets[].single_request.templates` is frozen at admission, supplies the private Plan prompt/PlanMD grammar and the private `review.md` artifact shape, and is not selectable, supplyable, or observable by callers. The Messages request/response schema, terminal projection, and final `decision.output` text are unchanged (`apps/edge/internal/openai/single_request_preset_binding.go`, `apps/edge/internal/openai/single_request_review_stage.go`, `agent-contract/outer/anthropic-compatible-api.md`).
|
||||
- 2026-08-09: Qualified S12 `claude-smoke` with one supervised external Claude task on an approved IOP Node: ingress `0→1`, Gemini → ornith-fast → Gemini, one `end_turn`, exact 42-byte workspace output, timing, cleanup, and redacted evidence.
|
||||
- 2026-08-07: Implemented and documented S11 `error-cancel`: one closed service terminal disposition, request-local typed failure/no-progress classification, shared buffered/SSE `end_turn`/`max_tokens`/`invalid_request_error`/`api_error` mapping, silent disconnect, private-partial suppression, and deterministic one-ingress/one-terminal/no-second-request evidence. S12 external qualification remains pending.
|
||||
- 2026-07-07: 현재 코드와 OpenAI-compatible 계약 기준으로 bootstrap spec 작성.
|
||||
|
|
|
|||
|
|
@ -80,10 +80,22 @@ source_evidence:
|
|||
notes: Exact-generation dispatch, frozen request-authority construction, closed artifact response validation, bounds, and stable failure translation
|
||||
- type: code
|
||||
path: apps/edge/internal/openai/single_request_plan_stage.go
|
||||
notes: Private fixed Plan stage runner, strict result decoding, and PLAN artifact write
|
||||
notes: Private fixed Plan stage runner, frozen-template prompt and direct PlanMD parsing, and PLAN artifact write
|
||||
- type: test
|
||||
path: apps/edge/internal/openai/single_request_plan_stage_test.go
|
||||
notes: Deterministic Plan request/options/envelope/artifact evidence
|
||||
- type: code
|
||||
path: packages/go/singlerequesttemplate/template.go
|
||||
notes: Bounded Plan/Review template grammar with exact standalone heading and PASS lines, closed placeholder inventory, PlanMD parsing, and Review rendering
|
||||
- type: test
|
||||
path: packages/go/singlerequesttemplate/template_test.go
|
||||
notes: Grammar, size-boundary, delimiter-residue, and PlanMD/Review render evidence
|
||||
- type: code
|
||||
path: packages/go/config/load.go
|
||||
notes: Relative-only plan_file/review_file resolution against the edge.yaml directory with per-file default fallback and filesystem-kind, size, and UTF-8 rejection
|
||||
- type: test
|
||||
path: packages/go/config/model_execution_preset_config_test.go
|
||||
notes: Absolute-path rejection, per-file fallback, exact 8192-byte acceptance, filesystem-kind failures, and the invalid-grammar matrix
|
||||
- type: code
|
||||
path: apps/edge/internal/openai/single_request_work_stage.go
|
||||
notes: Private ornith-fast Work provider/tool loop, request-safe continuation bridge, admitted tool projection, and strict completion evidence
|
||||
|
|
@ -216,7 +228,8 @@ The shared `packages/go/execution` package contains provider lifecycle, registry
|
|||
| workspace tool executor | A validated `darwin|linux` Node catalog owns opened root and directory handles only when every entry platform matches the host exactly. Windows, unknown hosts, and cross-platform catalogs fail before root open; empty catalogs remain compatible. Go 1.24-compatible no-follow file primitives provide bounded read, bounded list, structured write, and non-recursive delete. Exact operator-owned command templates run through an inherited-root `fchdir`/`exec` shim with minimal allowlisted environment, shared stdout/stderr bounds, process-group timeout/cancel, and stable typed results. OS is runtime evidence rather than a caller-visible selector. |
|
||||
| internal workspace tool loop | The service decodes only `workspace_read`, `workspace_list`, `workspace_write`, `workspace_delete`, and `workspace_command`, opens the admitted workspace once, dispatches one call at a time on the frozen generation, and delivers one deep-copied typed result to the emitting executor continuation. Unique request/stage/tool correlation, per-stage iteration/output/deadline limits, request wall-clock budget, and typed cancel fail closed without external continuation or reselection. |
|
||||
| request-owned internal artifacts | `SingleRequestController` exposes closed plan/review read/write operations. Artifact calls and model workspace tools share one serialized lazy `WorkspaceOpen`, the exact admitted Node generation, the active stage deadline, the immutable output bound, in-flight work accounting, and one terminal cleanup. Node alone maps selectors to `plan.md` and `review.md`, and inventoried descriptor-relative reads fail closed on identity replacement. |
|
||||
| Plan stage | The Plan runner emits the `planning` envelope, sends the immutable task through the frozen Gemini Chat binding with `reasoning_effort=high` and the template prompt, parses direct PlanMD output against the frozen effective Plan template, and writes deterministic bounded Markdown through `SingleRequestArtifactPlan`. |
|
||||
| Plan stage | The Plan runner emits the `planning` envelope, sends the immutable task through the frozen Gemini Chat binding with `reasoning_effort=high` and the frozen effective Plan template as the prompt, parses direct PlanMD output against that same template, and writes deterministic bounded Markdown through `SingleRequestArtifactPlan`. |
|
||||
| single-request effective templates | `execution_presets[].single_request.templates` optionally loads `plan_file`/`review_file` as bounded Markdown relative to the directory containing `edge.yaml`; absolute and empty paths, non-regular files, oversize (`>8192` bytes), non-UTF-8, and invalid grammar fail closed at load, and each file falls back to its built-in default independently. Admission freezes the effective Plan/Review pair into the binding, so a later refresh reaches only newly admitted requests. Templates select internal stage input and internal artifact shape only; caller request/response schemas are unchanged. |
|
||||
| Work stage | The `ornith-fast` Work runner reads the closed PLAN artifact, projects only the admitted workspace tools, and resumes the same frozen provider route after exactly correlated Node results. It rejects any Work `reasoning_effort`, malformed or multiple tool calls, and empty completion or verification evidence. |
|
||||
| request-owned cleanup | Node creates and inventories only `.iop/job/<request_id>` internal state, cancels and waits for all active command groups, validates the exact tree without following entries, and removes matching artifacts deepest-first with non-recursive descriptor operations. Symlinks, special files, foreign devices, identity replacements, and unowned entries fail closed. User results and sibling request state are preserved. Concurrent cleanup callers receive one bounded cached typed result. |
|
||||
| provider raw tunnel | 선택된 provider의 HTTP/SSE를 `ProviderTunnelRequest`/`ProviderTunnelFrame`으로 relay하며 순서와 단일 terminal outcome을 보장한다. |
|
||||
|
|
@ -242,7 +255,8 @@ The shared `packages/go/execution` package contains provider lifecycle, registry
|
|||
- The service freezes the first public terminal candidate. Legacy successful results normalize to `end_turn`; output limits produce `length`; caller disconnect produces silent `cancelled`; validation/context become `invalid_request_error`; other errors become `api_error`. Buffered and SSE projectors share that policy, emit at most one terminal, and never expose private partial stage content for `length`. This completes deterministic S11 `error-cancel` evidence without changing the Edge-Node protobuf wire. S12 external Claude qualification on an approved IOP Node is complete.
|
||||
- The request-local internal tool loop is implemented between the coordinator and the dedicated workspace wire. Strict decode and capability checks happen before wire effects; Node results are accepted only for the one pending call and return only bounded typed fields to the same optional executor continuation. Repeated or stale identities, malformed/denied calls, exhausted immutable budgets, and cancellation terminate internally without selecting another Node or involving the HTTP caller.
|
||||
- Request-owned plan and review artifact access is implemented between the controller and the same dedicated workspace wire. Only `SingleRequestArtifactPlan` and `SingleRequestArtifactReview` are accepted. Artifact and model-tool callers share one serialized open attempt and the same opened cleanup gate; terminal and cancellation paths wait for in-flight artifact work before issuing exactly one cleanup. Edge bounds writes before dispatch and reads before acceptance, validates the echoed kind/operation and canonical terminal, and never reselects after a generation mismatch. Node maps the closed selectors to `plan.md` and `review.md`, validates the inventoried parent/file identity with descriptor-relative no-follow reads, and never grants the public workspace tool surface access to `.iop`.
|
||||
- The private Plan stage is installed in the composite single-request executor at Edge input startup (`apps/edge/internal/input/manager.go`). Its provider codec accepts only frozen Plan options and selected dispatch facts, uses the admitted stage deadline and exact output limit, accepts only `RESPONSE_START`, zero or more `BODY`, then `END`, and projects all provider failures to a generic internal failure. The fixed Plan prompt and Edge-owned OpenAI `json_schema` response format request exactly `plan` and `verification`; caller/config options cannot override the format, and the strict parser retains the semantic nonempty/exact-field boundary before the closed PLAN artifact is written.
|
||||
- The private Plan stage is installed in the composite single-request executor at Edge input startup (`apps/edge/internal/input/manager.go`). Its provider codec accepts only frozen Plan options and selected dispatch facts, uses the admitted stage deadline and exact output limit, accepts only `RESPONSE_START`, zero or more `BODY`, then `END`, and projects all provider failures to a generic internal failure. The Plan prompt embeds the request's frozen effective Plan template, and the stage parses the provider's direct PlanMD output against that same frozen template before the closed PLAN artifact is written: required headings must be exact standalone lines, the documented `{{goal}}`/`{{steps}}`/`{{verification}}` inventory is closed, unresolved delimiters are rejected, and the bounded single-line goal, 2-6 step lines, and 1-3 verification lines are enforced. Caller request fields cannot select, supply, or override the admitted template.
|
||||
- The Review stage renders its internal REVIEW artifact from the request's frozen effective Review template, substituting only the model's `checks`, `verification`, and `summary` fields into the documented placeholder inventory. The template selects the internal artifact shape only: the caller-visible final response remains the model's `decision.output`, so replacing the Review template never changes the public Messages response schema.
|
||||
- The private Work stage is installed in the composite single-request executor at Edge input startup (`apps/edge/internal/input/manager.go`). It reads only `SingleRequestArtifactPlan`, retains only request/stage/tool identifiers while waiting for the coordinator-owned continuation, and sends no `reasoning_effort` field in an initial or resumed provider request. Its provider messages contain the immutable task, PLAN, admitted tool schemas, and bounded typed tool results; Review/repair and composite installation are active, and S12 (`claude-smoke`) qualifies the external Claude path.
|
||||
- The Node-private workspace request/result wire is implemented, including catalog delivery, parser registration, optional handler behavior, stable typed failures, generation-fenced dispatch, context-cancel propagation, and request cleanup. Before ready, a non-empty catalog requires a supported `darwin|linux` host and exact entry/host matching before any root open; unsupported and cross-platform catalogs fail closed while empty catalogs remain compatible. The Node installs the workspace handler before ready and cleans active requests before closing workspace authority ahead of session/store teardown. Request authority is immutable and request-local. File operations reserve `.iop`, reject symlink/mount/replaced-parent/special-file paths before effects, process bounded list batches with deterministic truncation, and use a same-parent structured write. Command execution resolves only admitted ids to fixed templates, enters the already-opened root descriptor through `fchdir`, provides only allowlisted environment entries, shares one output cap across drained stdout/stderr, and owns the complete process group through exit, timeout, context cancel, exact request/tool cancel, or request cleanup.
|
||||
- managed mode는 등록과 dispatch 전에 CA로 검증된 Edge/Node workload identity를 요구한다.
|
||||
|
|
@ -330,7 +344,9 @@ Heartbeat interval/wait는 protobuf field가 아닌 양쪽 transport 구현의 l
|
|||
- `go test -count=1 ./apps/edge/internal/service -run '^TestProviderHealthObservability'` — deterministic Edge overlay evidence/transition with closed label values and identity exclusion; `TestProviderHealthObservabilityDoesNotExposeSentinels` covers the sentinel/prohibited-value guard.
|
||||
- `go test -count=1 ./apps/edge/internal/openai -run '^(TestOpenAILivenessObservationSink|TestOpenAILivenessRecoveryObservability)$'` — deterministic OpenAI recovery eligibility/results with closed label values and identifier exclusion.
|
||||
- `go test -count=1 ./apps/edge/internal/openai -run 'TestAnthropicSingleRequestObservation'` — deterministic single-request observation evidence: ingress=1, request-total=1, terminal=1, stage/tool/cleanup counts, raw-free correlation, and unlabeled metric assertion.
|
||||
- `go test -count=1 ./apps/edge/internal/openai -run 'TestSingleRequest(ProviderStage|PlanStage)'` — deterministic frozen provider codec and Plan stage evidence, including high reasoning, stage-owned JSON Schema override protection, ordered tunnel frames, strict JSON, planning envelope, and `plan.md` artifact selection.
|
||||
- `go test -count=1 ./apps/edge/internal/openai -run 'TestSingleRequest(ProviderStage|PlanStage)'` — deterministic frozen provider codec and Plan stage evidence, including high reasoning, frozen-template prompt and parse authority, ordered tunnel frames, direct PlanMD validation, planning envelope, and `plan.md` artifact selection.
|
||||
- `go test -count=1 ./packages/go/singlerequesttemplate ./packages/go/config` — exact standalone heading/`PASS` grammar, closed placeholder inventory, the `8192`-byte boundary, and relative-only `plan_file`/`review_file` loading with per-file fallback and filesystem-kind rejection.
|
||||
- `go test -count=1 ./apps/edge/internal/service -run 'TestSingleRequestBinding'` and `go test -count=1 ./apps/edge/internal/openai -run 'TestSingleRequest(PresetBinding|ReviewStage)'` — admitted effective-template snapshot through clone and workspace revalidation, refresh isolation for admitted versus newly admitted requests, and the custom-template REVIEW artifact with unchanged caller-visible output.
|
||||
- `go test -race -count=1 ./apps/edge/internal/openai -run 'TestSingleRequestWork(Stage|ToolBridge)'` — deterministic ornith-fast Work tool loop, correlation isolation, cancellation cleanup, strict completion evidence, and Work reasoning-option absence.
|
||||
- `go test -race ./apps/edge/internal/service ./apps/edge/internal/openai -run 'Test(SingleRequestTerminalDisposition|SingleRequestQualityGate|AnthropicSingleRequestErrorCancelMatrix|SingleRequestAnthropicStreamTerminalDisposition)' -count=1` — deterministic S11 error-cancel/length matrix, first-terminal ownership, one ingress, no second request, disconnect silence, and raw-free output evidence.
|
||||
|
||||
|
|
@ -349,6 +365,7 @@ Heartbeat interval/wait는 protobuf field가 아닌 양쪽 transport 구현의 l
|
|||
|
||||
## 변경 기록
|
||||
|
||||
- 2026-08-09: Replaced the Plan stage's structured JSON response contract with operator-configurable bounded Markdown templates. `execution_presets[].single_request.templates` resolves `plan_file`/`review_file` relative to the directory containing `edge.yaml` only, admission freezes the effective Plan/Review pair so a refresh reaches only newly admitted requests, the Plan stage prompts with and parses direct PlanMD against the frozen template, and the Review stage renders the internal REVIEW artifact from the frozen Review template while the caller-visible final response stays the model's `decision.output` (`packages/go/singlerequesttemplate/template.go`, `packages/go/config/load.go`, `apps/edge/internal/openai/single_request_preset_binding.go`, `apps/edge/internal/openai/single_request_plan_stage.go`, `apps/edge/internal/openai/single_request_review_stage.go`).
|
||||
- 2026-08-09: Qualified S12 `claude-smoke` on an approved IOP Node. One supervised Claude task with SDK retry and session-title generation disabled produced ingress `0→1`, Gemini → ornith-fast → Gemini, one `end_turn`, the exact 42-byte workspace result, stage/total timing, cleanup, and redacted evidence.
|
||||
- 2026-08-08: Expanded workspace runtime admission to the closed `darwin|linux` implementation set with exact catalog/host matching before root open, preserved empty-catalog compatibility, and kept Windows/unknown hosts fail-closed.
|
||||
- 2026-08-07: Implemented the S11 `error-cancel` boundary: one frozen service terminal disposition, request-local typed stage classification, fixed-hash repetition/no-progress detection, shared buffered/SSE Anthropic mapping, silent disconnect cancellation, private-partial suppression for `max_tokens`, and deterministic one-ingress/one-terminal/no-second-request evidence. The Edge-Node protobuf wire is unchanged and S12 remains pending.
|
||||
|
|
@ -369,6 +386,6 @@ Heartbeat interval/wait는 protobuf field가 아닌 양쪽 transport 구현의 l
|
|||
- 2026-08-07: Added request-owned workspace cleanup. Node inventories its exact internal request namespace and artifacts, cancels and waits for all request command groups, refuses unowned, symlink, special-file, identity, and filesystem-boundary mismatches, and removes only validated entries with no-follow non-recursive descriptor operations. Edge gates every opened-workspace terminal path on one typed cleanup before finalizing acknowledgement; cleanup failure converts pending success while preserving existing failure or cancellation categories.
|
||||
- 2026-08-07: Added the closed request-owned PLAN/REVIEW artifact wire and controller lifecycle. Artifact calls share the model-tool lazy open and terminal cleanup gate, use the frozen Node generation and immutable bounds, and map only inside Node to inventoried `plan.md`/`review.md` files. Provider-specific stage drivers and actual Claude qualification remain deferred.
|
||||
- 2026-08-08: Synchronized single-request lifecycle observation evidence: stage-pure timing (planning/working/reviewing/repairing/finalizing/completed/failed/cancelled), tool/cleanup/total counts, cardinality-bounded labels, Node logs, and raw-free correlation. External Claude/Mac timing evidence is explicitly deferred to `claude-smoke` (SDD S12). Deterministic coordinator/tool-loop tests cover the full single-request path without implying external qualification.
|
||||
- 2026-08-08: Made the Gemini Plan output deterministic with an Edge-owned OpenAI `json_schema` response format for the exact `plan`/`verification` object, retained the strict nonempty parser, and added a raw-free fixed terminal-rejection event that distinguishes `malformed` from binding `validation` without widening metric labels.
|
||||
- 2026-08-07: Added the private Plan stage and its fail-closed provider codec. The component uses only frozen Gemini dispatch/options, ordered bounded tunnel decoding, strict small plan/verification JSON, and the closed `SingleRequestArtifactPlan` write. It is not installed; Work, Review/repair, activation, and S12 qualification remain deferred.
|
||||
- 2026-08-08: Made the Gemini Plan output deterministic with a stage-owned structured response contract for the exact `plan`/`verification` object, retained the strict nonempty parser, and added a raw-free fixed terminal-rejection event that distinguishes `malformed` from binding `validation` without widening metric labels. The structured Plan response contract was superseded on 2026-08-09 by direct PlanMD template validation; the terminal-rejection event is unchanged.
|
||||
- 2026-08-07: Added the private Plan stage and its fail-closed provider codec. The component uses only frozen Gemini dispatch/options, ordered bounded tunnel decoding, a bounded structured plan/verification response contract, and the closed `SingleRequestArtifactPlan` write. It is not installed; Work, Review/repair, activation, and S12 qualification remain deferred.
|
||||
- 2026-08-07: Added the private ornith-fast Work stage. It reads PLAN through the closed artifact controller, emits only admitted workspace schemas, bridges exact request/stage/tool results without retaining payloads, and resumes the frozen route with bounded tool evidence. Work rejects `reasoning_effort`; Review/repair, composite installation, and S12 external qualification remain deferred.
|
||||
|
|
|
|||
|
|
@ -17,7 +17,13 @@ source_evidence:
|
|||
notes: Edge root provider_pool canonical queue policy 타입과 기본값
|
||||
- type: code
|
||||
path: packages/go/config/load.go
|
||||
notes: Edge config load와 default 적용
|
||||
notes: Edge config load, default 적용, relative-only single-request 템플릿 파일 해석
|
||||
- type: code
|
||||
path: packages/go/singlerequesttemplate/template.go
|
||||
notes: 단일 요청 Plan/Review 템플릿 문법과 기본 템플릿, digest helper
|
||||
- type: code
|
||||
path: apps/edge/internal/openai/single_request_preset_binding.go
|
||||
notes: Admission 시 effective Plan/Review 템플릿 freeze와 built-in default fallback
|
||||
- type: code
|
||||
path: packages/go/config/validate.go
|
||||
notes: provider/model references, managed credential exclusivity, TLS/key requirements, and numeric bounds
|
||||
|
|
@ -124,7 +130,9 @@ Edge 설정에서 provider-pool이 어떻게 모델 실행 후보를 고르고,
|
|||
| Stream Evidence Gate config | `openai.stream_evidence_gate` provides runtime activation, request-total/strategy fault recovery caps, ingress snapshot bounds, and per-filter capability/enforcement/Unicode hold policy; it is currently restart-required. |
|
||||
| mutable apply | 적용 가능한 변경은 Edge `Cfg`, `NodeStore`, service/input model catalog, OpenAI long-context threshold를 copy-on-write로 교체한다. |
|
||||
| single-request snapshot isolation | An admitted single-request binding is independent of subsequent model catalog, execution preset, or provider pool changes. Refresh replaces the live catalog and preset snapshots used by future admissions; already-admitted bindings retain their original values. |
|
||||
| fixed single-request policy | `execution_presets[].single_request` declares an operator-owned immutable plan→work→review light path with absolute wall-clock (`≤1800000ms`), stage-timeout (`≤600000ms`), tool-iteration (`≤64`), and output-byte (`≤16MiB`) caps. Selector and plan/review stages require `reasoning_effort=high`; work stage forbids it. `workspace_ref` is opaque (never raw path/credential/Node/endpoint). Optional `templates` (`plan_file`/`review_file`) load config-relative Markdown templates up to 8192 bytes, fallback to built-in defaults, and report SHA-256 digests without exposing template paths or contents in refresh diff evidence. single_request preset rejects `workspace_tools`. Catalog and mapping changes are live-apply and affect only new request snapshots; admitted bindings retain their frozen values across refresh. |
|
||||
| fixed single-request policy | `execution_presets[].single_request` declares an operator-owned immutable plan→work→review light path with absolute wall-clock (`≤1800000ms`), stage-timeout (`≤600000ms`), tool-iteration (`≤64`), and output-byte (`≤16MiB`) caps. Selector and plan/review stages require `reasoning_effort=high`; work stage forbids it. `workspace_ref` is opaque (never raw path/credential/Node/endpoint). single_request preset rejects `workspace_tools`. Catalog and mapping changes are live-apply and affect only new request snapshots; admitted bindings retain their frozen values across refresh. |
|
||||
| single-request effective templates | Optional `templates` (`plan_file`/`review_file`) load bounded Markdown relative to the directory containing `edge.yaml` only. Absolute and empty paths are rejected before any filesystem access; non-regular files, sizes over 8192 bytes, non-UTF-8 content, and invalid template grammar fail closed at load. Each file falls back to its built-in default independently, and refresh diff evidence reports SHA-256 digests only, never template paths or contents. |
|
||||
| effective-template admission freeze | Admission copies the resolved effective Plan/Review pair into the immutable binding, and that pair survives binding clone and workspace revalidation. A later refresh swaps the preset snapshot used by future admissions only: already-admitted work keeps its frozen pair, while a request admitted after the refresh observes the refreshed pair. Templates select internal stage input and internal artifact shape only; caller request and response schemas are unchanged. |
|
||||
| operator-owned workspace catalog | `nodes[].workspaces[]` is the operator-owned bounded capability catalog for each node. Each entry is keyed by a globally unique, trimmed `ref`, declares `platform` in the closed `darwin|linux` implementation set, and retains the existing absolute clean root, closed operations, approved commands, environment allowlist, and bounded byte/time limits. Refs remain globally unique and any catalog change is `restart_required`. Empty catalogs are backward-compatible on any host. A non-empty catalog requires a supported Node host and every entry must match that host before any root is opened; Windows, unknown hosts, and cross-platform catalogs fail closed. The catalog is delivered by the Node-private typed config payload and retained as opened immutable runtime authority. Raw roots and command details never enter presets, public responses, provider requests, or metadata; operating system is runtime evidence rather than a caller selector. |
|
||||
| Node config refresh push | 변경이 있으면 Edge가 dispatch-ready Node에 node-specific `NodeConfigRefreshRequest`를 push한다. accepted지만 pending인 Node는 register response config를 적용한 뒤 ready가 될 때까지 push 대상이 아니다. |
|
||||
| Node registry swap | Node는 refresh payload로 새 adapter registry를 만들고 router registry를 swap한다. old registry stop은 active run이 있으면 drain 이후로 지연한다. |
|
||||
|
|
@ -170,7 +178,8 @@ sequenceDiagram
|
|||
- `iop.edge-config-runtime-refresh`: `agent-contract/inner/edge-config-runtime-refresh.md`
|
||||
- `iop.edge-node-runtime-wire`: `agent-contract/inner/edge-node-runtime-wire.md`
|
||||
- proto 원문: `proto/iop/runtime.proto`
|
||||
- `execution_presets[].single_request` is the operator-owned fixed single-request policy. Absolute caps: `wall_clock_ms ∈ [1, 1800000]`, `timeout_ms ∈ [1, 600000]`, `timeout_ms ≤ wall_clock_ms`, `max_tool_iterations ∈ [1, 64]`, `max_output_bytes ∈ [1, 16777216]`. Stages enforce exactly plan→work→review with `reasoning_effort=high` on selector and plan/review, forbidden on work. `workspace_ref` is opaque (never raw path/credential/Node/endpoint). Optional `templates` (`plan_file`/`review_file`) load config-relative Markdown templates up to 8192 bytes, fallback to built-in defaults, and report SHA-256 digests without exposing template paths or contents in refresh diff evidence. single_request preset rejects `workspace_tools`. Catalog/mapping changes are live-apply; admitted bindings are snapshot-isolated across refresh.
|
||||
- `execution_presets[].single_request` is the operator-owned fixed single-request policy. Absolute caps: `wall_clock_ms ∈ [1, 1800000]`, `timeout_ms ∈ [1, 600000]`, `timeout_ms ≤ wall_clock_ms`, `max_tool_iterations ∈ [1, 64]`, `max_output_bytes ∈ [1, 16777216]`. Stages enforce exactly plan→work→review with `reasoning_effort=high` on selector and plan/review, forbidden on work. `workspace_ref` is opaque (never raw path/credential/Node/endpoint). single_request preset rejects `workspace_tools`. Catalog/mapping changes are live-apply; admitted bindings are snapshot-isolated across refresh.
|
||||
- `execution_presets[].single_request.templates` is optional. `plan_file` and `review_file` must be non-empty paths relative to the directory containing `edge.yaml`; absolute and empty values are rejected before the loader opens anything. Each file is loaded as a regular file of at most 8192 valid UTF-8 bytes and must satisfy the documented template grammar; an omitted file falls back to its built-in default independently of the other. Refresh diff evidence reports only SHA-256 digests of the effective templates. Admission freezes the effective pair into the request binding, so a refreshed template reaches only requests admitted after the refresh.
|
||||
|
||||
## 설정/데이터/이벤트
|
||||
|
||||
|
|
@ -248,5 +257,6 @@ sequenceDiagram
|
|||
- 2026-08-04: Added provider response-stall timeout validation/default, restart-required refresh classification, selected-candidate propagation, and Node retention. Timer/watchdog lifecycle remains out of scope.
|
||||
- 2026-08-05: Added the separate generation-scoped runtime provider health overlay, effective admission/snapshot exclusion, config-health immutability, and exact higher-sequence CAPABILITIES recovery.
|
||||
- 2026-08-05: Added post-decision provider-health operational evidence with bounded counters and structured logs, isolated from overlay state and provider identity.
|
||||
- 2026-08-09: Synchronized the single-request effective-template boundary: relative-only `plan_file`/`review_file` resolution against the `edge.yaml` directory with pre-access absolute rejection, independent per-file default fallback, load-time regular-file/size/UTF-8/grammar rejection, digest-only refresh diff evidence, and admission-time freezing so a refresh reaches only newly admitted requests (`packages/go/config/load.go`, `apps/edge/internal/configrefresh/classify.go`, `apps/edge/internal/openai/single_request_preset_binding.go`).
|
||||
- 2026-08-06: Synchronized the fixed single-request policy (`execution_presets[].single_request`) absolute caps, plan→work→review stage shape, opaque `workspace_ref`, live-apply classification, and snapshot-isolation semantics with current code, contract, and classifier implementation.
|
||||
- 2026-08-06: Required effective positive workspace-operation bounds and clarified that the later Node-private typed config/admission transport is deferred; public/preset/provider surfaces retain no raw workspace roots or command templates.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,294 @@
|
|||
<!-- task=single_request_plan_review_templates plan=1 tag=REVIEW_API -->
|
||||
|
||||
# Code Review Reference - REVIEW_API
|
||||
|
||||
**Date:** 2026-08-09 KST
|
||||
**Task:** Repair the single-request template admission contract, missing regression evidence, and current documentation after official review failure.
|
||||
|
||||
## Review Agent Instructions
|
||||
|
||||
- Review only the paired `PLAN-cloud-G08.md` scope and repository changes implementing it.
|
||||
- Re-run verification independently; implementation-supplied output is evidence input, not the verdict.
|
||||
- Only the review agent may append `Code Review Result`, archive the active pair, write `complete.log`, or move the task directory.
|
||||
- On `WARN` or `FAIL`, follow the review routing policy and do not write `complete.log`.
|
||||
|
||||
## Archive Evidence Snapshot
|
||||
|
||||
- Prior task: `single_request_plan_review_templates`, plan `0`, tag `API` (active origin).
|
||||
- Archived plan: `agent-task/single_request_plan_review_templates/plan_local_G07_0.log`.
|
||||
- Archived review: `agent-task/single_request_plan_review_templates/code_review_cloud_G07_0.log`.
|
||||
- Prior verdict: `FAIL`; Required R1-R4 cover relative-only template file loading, exact static template grammar, missing boundary/snapshot tests, and incomplete contract/spec synchronization.
|
||||
- Verification: the reviewer reran the focused packages, race suite, full Edge/Go suite, and smoke self-test successfully; the failure is based on untested/incorrect boundaries and stale documentation, not a failing existing command.
|
||||
- Roadmap carryover: none; this is a non-milestone task.
|
||||
|
||||
## Implementation Item Completion
|
||||
|
||||
- [x] REVIEW_API-1 — Close Template Admission Boundaries
|
||||
- [x] REVIEW_API-2 — Prove Freeze, Refresh, and Render Semantics
|
||||
- [x] REVIEW_API-3 — Synchronize Current Contracts and Specs
|
||||
- [x] REVIEW_API-4 — Produce Trustworthy Verification Evidence
|
||||
|
||||
## Implementation Checklist
|
||||
|
||||
- [x] [REVIEW_API-1] Enforce relative-only template loading and exact Plan/Review grammar with boundary regressions.
|
||||
- [x] [REVIEW_API-2] Add admission, clone/workspace revalidation, preset refresh, and custom Review artifact snapshot tests.
|
||||
- [x] [REVIEW_API-3] Synchronize current contracts/specs with direct PlanMD, frozen templates, templated internal Review, and unchanged caller output.
|
||||
- [x] [REVIEW_API-4] Run fresh focused/full/local and remote-profile verification, recording the exact external blocker if the approved SOPS identity remains unavailable.
|
||||
- [x] Fill implementation-owned sections in `CODE_REVIEW-cloud-G08.md` with actual implementation notes and verification output.
|
||||
|
||||
## Review-Only Checklist
|
||||
|
||||
- [ ] Review verdict and routing signals are appended.
|
||||
- [ ] Finding severities, verdict, and routing are mutually consistent.
|
||||
- [ ] On PASS only: `complete.log` is written with exact header, verdict, review path, and summary.
|
||||
- [ ] On PASS only: task directory is moved under `agent-task/archive/`.
|
||||
- [ ] Active review is archived as `agent-task/single_request_plan_review_templates/code_review_cloud_G08_1.log`.
|
||||
- [ ] Active plan is archived as `agent-task/single_request_plan_review_templates/plan_cloud_G08_1.log` when closure policy requires it.
|
||||
- [ ] `.gitignore` admits generated task Markdown/log artifacts.
|
||||
- [ ] On WARN/FAIL: the required next active state is materialized and `complete.log` is absent.
|
||||
|
||||
## Deviations from Plan
|
||||
|
||||
1. **Branch name.** The plan's Final Verification asserts `feature/single_request_plan_review_templates` (underscores). The actual branch — local and on the approved remote checkout — is `feature/single-request-plan-review-templates` (hyphens). Every branch-scoped command was run against the real hyphenated branch. No branch was created or renamed.
|
||||
2. **`utf8` validation was added, not retained.** REVIEW_API-1 says to "retain existing regular-file, size, UTF-8, and fallback behavior". `loadTemplateFile` had no UTF-8 check; there was nothing to retain. A `utf8.Valid` rejection was added so the loader boundary the plan describes is real, and it is covered by `TestLoadEdgeSingleRequestTemplates/non-UTF-8_template_file_fails_closed`.
|
||||
3. **Grammar errors no longer echo template text.** The old validator returned `unknown placeholder %q` with the offending token from the template body. The plan's REVIEW_API-1 Verification requires rejection "without exposing file content in errors", so unknown-placeholder and delimiter-residue errors now name the violation category only. Documented required lines and documented placeholder names (compile-time constants, not file content) are still echoed.
|
||||
4. **`..` traversal was not added to the loader.** The plan scopes R1 to rejecting empty and absolute values. A relative path containing `..` still resolves through `filepath.Join`. This is unchanged behavior, in scope for a follow-up if the operator-config threat model warrants path containment; it was not silently broadened here.
|
||||
5. **Remote `make` variables in the plan do not exist.** The plan's remote command passes `EDGE_CONFIG=` / `SOPS_CONFIG=`. The smoke targets in `Makefile:206-236` read `IOP_SINGLE_REQUEST_SMOKE_*` variables only, so those two assignments are inert and the preflight runs with empty arguments regardless of the config files. Recorded here because it changes how the remote blocker below must be resolved.
|
||||
6. **Remote evidence required two commits.** The plan asks for a clean tree and a pushed commit before the remote rebuild, and for the remote output to be pasted into this file. Those cannot both hold in one commit. The implementation commit was pushed first, the remote profile was exercised against exactly that commit, and this file's remote section was then updated and committed. Both hashes are recorded below.
|
||||
7. **`WORK_LOG.md` is dispatcher-owned.** It is admitted by `.gitignore` (`agent-task/**/*.md`) and was committed with the task directory, but the dispatcher appends its `FINISH` row after this agent exits, so `git status --short` cannot stay empty past that point. The clean-tree assertion below is true as of the recorded implementation commit.
|
||||
|
||||
## Key Design Decisions
|
||||
|
||||
**Relative-only template loading (`packages/go/config/load.go:721-762`).** The parameter was renamed from `relOrAbsPath` to `relativePath`, and empty and absolute values are rejected before `os.Lstat`/`os.Open`, so an absolute path never reaches the filesystem at all. `filepath.Join(baseDir, target)` is now the only resolution path, which is what `agent-contract/inner/edge-config-runtime-refresh.md` already specified. Existing `Lstat`-based non-regular-file rejection (which also rejects symlinks), the `io.LimitReader(f, 8193)` size boundary, and independent per-file default fallback in `resolveSingleRequestTemplates` are unchanged.
|
||||
|
||||
**Exact standalone-line grammar (`packages/go/singlerequesttemplate/template.go:63-104`).** `exactLineOffsets` compares maximal `\n`-delimited segments byte for byte and reports both the first offset and the occurrence count; `requireExactLines` demands exactly one occurrence of each required line. This is what closes the reported defect: `### Plan`, `# Plan Mismatch`, `Result: PASS`, and `NOTPASS` all previously satisfied the substring checks and are now rejected. Offsets feed the same structural ordering assertions as before, so no ordering semantics changed.
|
||||
|
||||
**Closed placeholder inventory (`packages/go/singlerequesttemplate/template.go:106-128`).** `requirePlaceholderInventory` removes exactly one occurrence of each documented placeholder and then rejects any surviving `{{` or `}}`. Removing first is what makes the check closed rather than a denylist: it catches unknown placeholders (`{{severity}}`), unbalanced delimiters (`{{goal}}}}`), and nesting (`{{{{goal}}}}`), which the old `\{\{[^}]*\}\}` scan could not.
|
||||
|
||||
**Templates are strings, so freezing is structural.** `SingleRequestTemplateBinding` holds two immutable Go strings, so the "frozen" property follows from the type rather than from a defensive copy. That is exactly why the plan's R3 gap mattered: the invariant was true but unproven and unprotected. The new tests assert it explicitly at admission, through `Clone`, through `cloneValidatedSingleRequestBinding`, and across a simulated refresh, so a future change to a mutable representation fails loudly.
|
||||
|
||||
**Validation is re-run at revalidation, not just admission.** `cloneValidatedSingleRequestBinding` routes back through `NewSingleRequestBindingWithTemplates`, so a template tampered with after admission fails workspace revalidation instead of being cloned forward. `TestSingleRequestBindingTemplateSnapshot/an_invalid_template_cannot_survive_workspace_revalidation` pins that.
|
||||
|
||||
**Review template selects the artifact, never the caller response.** `renderSingleRequestReview` builds the internal artifact from `binding.Templates.Review` while `singleRequestReviewResult.Output` stays the trimmed `decision.output`. `TestSingleRequestReviewStageCustomTemplateSnapshot` renders the same decision through both the custom and the built-in template, asserts two different exact artifacts, and asserts one identical caller-visible output — which is the property the outer contract now states.
|
||||
|
||||
**No production behavior was changed for REVIEW_API-2.** Every file under that item is a `_test.go` file. The three regressions were mutation-checked against the pre-fix behavior (see Verification Results) rather than assumed to be meaningful.
|
||||
|
||||
**Documentation scope.** The inner contract `agent-contract/inner/edge-config-runtime-refresh.md` already described relative-only loading, the 8192-byte cap, digest-only refresh evidence, and frozen effective templates; the implementation converged to it and it was left unmodified, per the plan's instruction not to broaden it. The outer contract, the two runtime specs, and the input spec were the surfaces that contradicted the shipped behavior.
|
||||
|
||||
## Reviewer Checkpoints
|
||||
|
||||
- Confirm absolute `plan_file` and `review_file` values are rejected before filesystem access, while independent fallback and exact size/file-kind boundaries remain correct.
|
||||
- Confirm required Plan/Review headings and Review `PASS` are exact lines and the admitted placeholder inventory is closed.
|
||||
- Confirm binding snapshot, clone/workspace revalidation, preset refresh isolation, and custom Review artifact tests exist and assert caller-output invariance.
|
||||
- Confirm the outer contract and current specs consistently describe direct PlanMD, frozen effective templates, templated internal Review artifacts, and unchanged caller-visible I/O.
|
||||
|
||||
## Verification Results
|
||||
|
||||
### REVIEW_API-1 — Template and Config Boundaries
|
||||
|
||||
```bash
|
||||
test -z "$(gofmt -l packages/go/singlerequesttemplate/template.go packages/go/singlerequesttemplate/template_test.go packages/go/config/load.go packages/go/config/model_execution_preset_config_test.go apps/edge/internal/service/single_request_types_test.go apps/edge/internal/openai/single_request_preset_binding_test.go apps/edge/internal/openai/single_request_review_stage_test.go)"
|
||||
go test -count=1 ./packages/go/singlerequesttemplate ./packages/go/config
|
||||
```
|
||||
|
||||
```text
|
||||
$ test -z "$(gofmt -l packages/go/singlerequesttemplate/template.go packages/go/singlerequesttemplate/template_test.go packages/go/config/load.go packages/go/config/model_execution_preset_config_test.go apps/edge/internal/service/single_request_types_test.go apps/edge/internal/openai/single_request_preset_binding_test.go apps/edge/internal/openai/single_request_review_stage_test.go)"
|
||||
exit=0
|
||||
|
||||
$ go test -count=1 ./packages/go/singlerequesttemplate ./packages/go/config
|
||||
ok iop/packages/go/singlerequesttemplate 0.005s
|
||||
ok iop/packages/go/config 0.144s
|
||||
exit=0
|
||||
```
|
||||
|
||||
New boundary subtests actually executed (`-v`, abridged to the added cases):
|
||||
|
||||
```text
|
||||
=== RUN TestLoadEdgeSingleRequestTemplates/absolute_plan_file_rejected_before_filesystem_access
|
||||
=== RUN TestLoadEdgeSingleRequestTemplates/absolute_review_file_rejected_before_filesystem_access
|
||||
=== RUN TestLoadEdgeSingleRequestTemplates/plan_file_configured_and_review_file_falls_back
|
||||
=== RUN TestLoadEdgeSingleRequestTemplates/review_file_configured_and_plan_file_falls_back
|
||||
=== RUN TestLoadEdgeSingleRequestTemplates/exact_8192_byte_template_file_accepted
|
||||
=== RUN TestLoadEdgeSingleRequestTemplates/symlinked_template_file_fails_closed
|
||||
=== RUN TestLoadEdgeSingleRequestTemplates/directory_template_path_fails_closed
|
||||
=== RUN TestLoadEdgeSingleRequestTemplates/unreadable_template_file_fails_closed
|
||||
=== RUN TestLoadEdgeSingleRequestTemplates/non-UTF-8_template_file_fails_closed
|
||||
=== RUN TestLoadEdgeSingleRequestTemplates/invalid_grammar_matrix_fails_closed
|
||||
(11 subcases: plan missing/decorated heading, unknown placeholder, delimiter residue,
|
||||
duplicate placeholder; review missing PASS, NOTPASS, decorated heading,
|
||||
missing placeholder, unknown placeholder, wrong order)
|
||||
--- PASS: TestLoadEdgeSingleRequestTemplates (0.02s)
|
||||
```
|
||||
|
||||
Mutation check — the three grammar/path fixes were temporarily reverted in place (`filepath.IsAbs` fallthrough restored in `loadTemplateFile`; `requireExactLines` reverted to `strings.Index`/`strings.Count`) and the same suites re-run, proving the new cases fail against the pre-fix behavior rather than passing vacuously. The files were restored byte-for-byte afterwards and re-verified green:
|
||||
|
||||
```text
|
||||
--- FAIL: TestLoadEdgeSingleRequestTemplates/absolute_plan_file_rejected_before_filesystem_access
|
||||
expected error for absolute plan_file "/tmp/.../plan.md"
|
||||
--- FAIL: TestLoadEdgeSingleRequestTemplates/absolute_review_file_rejected_before_filesystem_access
|
||||
expected error for absolute review_file "/tmp/.../review.md"
|
||||
--- FAIL: TestValidatePlanTemplate/decorated_heading_###_Plan err = <nil>, wantErr true
|
||||
--- FAIL: TestValidatePlanTemplate/heading_line_carries_trailing_text err = <nil>, wantErr true
|
||||
--- FAIL: TestValidatePlanTemplate/heading_embedded_inside_a_prose_line err = <nil>, wantErr true
|
||||
--- FAIL: TestValidatePlanTemplate/duplicate_required_heading err = <nil>, wantErr true
|
||||
--- FAIL: TestValidateReviewTemplate/NOTPASS_does_not_satisfy_the_PASS_result_line err = <nil>, wantErr true
|
||||
--- FAIL: TestValidateReviewTemplate/PASS_embedded_in_a_prose_line err = <nil>, wantErr true
|
||||
--- FAIL: TestValidateReviewTemplate/decorated_heading_###_Review err = <nil>, wantErr true
|
||||
--- FAIL: TestValidateReviewTemplate/duplicate_PASS_result_line err = <nil>, wantErr true
|
||||
FAIL iop/packages/go/config 0.014s
|
||||
FAIL iop/packages/go/singlerequesttemplate 0.010s
|
||||
```
|
||||
|
||||
### REVIEW_API-2 — Binding, Refresh, Review Rendering, and Race Coverage
|
||||
|
||||
```bash
|
||||
go test -count=1 ./apps/edge/internal/service -run 'TestSingleRequestBinding'
|
||||
go test -count=1 ./apps/edge/internal/openai -run 'TestSingleRequest(PresetBinding|ReviewStage)'
|
||||
go test -race -count=1 ./apps/edge/internal/openai ./apps/edge/internal/service ./apps/edge/internal/configrefresh ./packages/go/config ./packages/go/singlerequesttemplate
|
||||
```
|
||||
|
||||
```text
|
||||
$ go test -count=1 ./apps/edge/internal/service -run 'TestSingleRequestBinding'
|
||||
ok iop/apps/edge/internal/service 0.026s
|
||||
exit=0
|
||||
|
||||
$ go test -count=1 ./apps/edge/internal/openai -run 'TestSingleRequest(PresetBinding|ReviewStage)'
|
||||
ok iop/apps/edge/internal/openai 0.044s
|
||||
exit=0
|
||||
|
||||
$ go test -race -count=1 ./apps/edge/internal/openai ./apps/edge/internal/service ./apps/edge/internal/configrefresh ./packages/go/config ./packages/go/singlerequesttemplate
|
||||
ok iop/apps/edge/internal/openai 12.349s
|
||||
ok iop/apps/edge/internal/service 9.384s
|
||||
ok iop/apps/edge/internal/configrefresh 1.210s
|
||||
ok iop/packages/go/config 1.893s
|
||||
ok iop/packages/go/singlerequesttemplate 1.037s
|
||||
exit=0
|
||||
```
|
||||
|
||||
The three prescribed regressions exist and executed (`-v`, added cases only):
|
||||
|
||||
```text
|
||||
=== RUN TestSingleRequestBindingTemplateSnapshot
|
||||
=== RUN TestSingleRequestBindingTemplateSnapshot/admitted_binding_carries_the_exact_effective_pair
|
||||
=== RUN TestSingleRequestBindingTemplateSnapshot/template-less_admission_falls_back_to_the_built-in_defaults
|
||||
=== RUN TestSingleRequestBindingTemplateSnapshot/clone_preserves_the_frozen_pair
|
||||
=== RUN TestSingleRequestBindingTemplateSnapshot/workspace_revalidation_preserves_the_frozen_pair
|
||||
=== RUN TestSingleRequestBindingTemplateSnapshot/invalid_effective_templates_are_rejected_at_admission
|
||||
=== RUN TestSingleRequestBindingTemplateSnapshot/an_invalid_template_cannot_survive_workspace_revalidation
|
||||
--- PASS: TestSingleRequestBindingTemplateSnapshot (0.00s)
|
||||
|
||||
=== RUN TestSingleRequestPresetBindingTemplateRefreshIsolation
|
||||
--- PASS: TestSingleRequestPresetBindingTemplateRefreshIsolation (0.00s)
|
||||
=== RUN TestSingleRequestPresetBindingTemplateFallback
|
||||
=== RUN TestSingleRequestPresetBindingTemplateFallback/both_templates_fall_back
|
||||
=== RUN TestSingleRequestPresetBindingTemplateFallback/plan_configured_and_review_falls_back
|
||||
=== RUN TestSingleRequestPresetBindingTemplateFallback/review_configured_and_plan_falls_back
|
||||
=== RUN TestSingleRequestPresetBindingTemplateFallback/invalid_effective_template_fails_admission_closed
|
||||
--- PASS: TestSingleRequestPresetBindingTemplateFallback (0.00s)
|
||||
=== RUN TestSingleRequestReviewStageCustomTemplateSnapshot
|
||||
=== RUN TestSingleRequestReviewStageCustomTemplateSnapshot/custom_template_renders_the_exact_internal_artifact
|
||||
=== RUN TestSingleRequestReviewStageCustomTemplateSnapshot/default_template_renders_the_exact_internal_artifact
|
||||
=== RUN TestSingleRequestReviewStageCustomTemplateSnapshot/stage_run_writes_the_custom_artifact_and_preserves_caller_output
|
||||
=== RUN TestSingleRequestReviewStageCustomTemplateSnapshot/an_invalid_effective_template_fails_the_render_closed
|
||||
--- PASS: TestSingleRequestReviewStageCustomTemplateSnapshot (0.00s)
|
||||
ok iop/apps/edge/internal/openai 0.038s
|
||||
```
|
||||
|
||||
Assertions are exact rather than substring: the refresh test compares the whole admitted template pair before and after mutating `preset.SingleRequest.Templates.*` and compiles a second binding to show the refreshed pair reaches only the new admission; the Review test compares the full rendered artifact byte string for both the custom and the built-in template while asserting one identical caller-visible `result.Output`.
|
||||
|
||||
### REVIEW_API-3 — Full Suite and Documentation Guard
|
||||
|
||||
```bash
|
||||
go test -count=1 ./apps/edge/... ./packages/go/...
|
||||
make test-single-request-claude-smoke-self-test
|
||||
if rg --sort path -n 'Edge-owned OpenAI `json_schema` response format|strict small plan/verification JSON|stage-owned JSON Schema override protection' agent-spec/runtime/edge-node-execution.md; then exit 1; fi
|
||||
```
|
||||
|
||||
```text
|
||||
$ go test -count=1 ./apps/edge/... ./packages/go/...
|
||||
ok iop/apps/edge/cmd/edge 0.237s
|
||||
ok iop/apps/edge/internal/authprojection 0.055s
|
||||
ok iop/apps/edge/internal/bootstrap 0.493s
|
||||
ok iop/apps/edge/internal/configrefresh 0.117s
|
||||
ok iop/apps/edge/internal/controlplane 6.676s
|
||||
ok iop/apps/edge/internal/edgecmd 0.135s
|
||||
ok iop/apps/edge/internal/edgevalidate 0.104s
|
||||
ok iop/apps/edge/internal/events 0.043s
|
||||
ok iop/apps/edge/internal/input 0.067s
|
||||
ok iop/apps/edge/internal/input/a2a 0.058s
|
||||
ok iop/apps/edge/internal/node 0.046s
|
||||
ok iop/apps/edge/internal/openai 8.361s
|
||||
ok iop/apps/edge/internal/opsconsole 0.062s
|
||||
ok iop/apps/edge/internal/service 8.221s
|
||||
ok iop/apps/edge/internal/transport 4.810s
|
||||
ok iop/packages/go/audit 0.010s
|
||||
ok iop/packages/go/auth 10.035s
|
||||
ok iop/packages/go/config 0.161s
|
||||
ok iop/packages/go/credentiallease 0.038s
|
||||
? iop/packages/go/events [no test files]
|
||||
ok iop/packages/go/execution 0.017s
|
||||
ok iop/packages/go/hostsetup 0.011s
|
||||
? iop/packages/go/jobs [no test files]
|
||||
? iop/packages/go/metadata [no test files]
|
||||
ok iop/packages/go/observability 0.024s
|
||||
? iop/packages/go/policy [no test files]
|
||||
ok iop/packages/go/singlerequesttemplate 0.007s
|
||||
ok iop/packages/go/streamgate 0.884s
|
||||
? iop/packages/go/version [no test files]
|
||||
ok iop/packages/go/workspaceprotocol 0.016s
|
||||
exit=0
|
||||
|
||||
$ make test-single-request-claude-smoke-self-test
|
||||
./scripts/e2e-single-request-claude.sh --self-test
|
||||
[single-request-claude-smoke] self-test passed: exact Claude base-route coverage, structured observation admission, child-only zero retry, authenticated model admission, closed failure classification, model/Edge/Node/runtime binding, zero-child preflight, derived verification, redaction, cleanup, signal handling, and atomic publication
|
||||
exit=0
|
||||
|
||||
$ if rg --sort path -n 'Edge-owned OpenAI `json_schema` response format|strict small plan/verification JSON|stage-owned JSON Schema override protection' agent-spec/runtime/edge-node-execution.md; then exit 1; fi
|
||||
(rg produced no matches; the guard did not trip)
|
||||
exit=0
|
||||
```
|
||||
|
||||
Documentation surfaces updated for REVIEW_API-3:
|
||||
|
||||
| File | Change |
|
||||
|---|---|
|
||||
| `agent-contract/outer/anthropic-compatible-api.md` | Added the effective-template subsection to `Marked preset: single-request admission` (Edge-owned internal stage input, unchanged caller request/response schema, no caller selection, no path/content/digest exposure, admission freeze); added the five single-request/template source paths to 계약 메타; added the caller-boundary 금지 사항 entry. |
|
||||
| `agent-contract/index.md` | `iop.anthropic-compatible-api` 읽는 조건 and 원본 경로 now name the marked-preset template boundary and its source files. |
|
||||
| `agent-spec/runtime/edge-node-execution.md` | Rewrote the stale Plan `json_schema` passage to frozen-template prompting and direct PlanMD parsing; added the Review artifact-rendering boundary; added the `single-request effective templates` feature row; rewrote the two superseded 변경 기록 entries and added the 2026-08-09 entry; added the template/config source_evidence and the three new verification commands. |
|
||||
| `agent-spec/runtime/provider-pool-config-refresh.md` | Split templates out of the `fixed single-request policy` row into dedicated `single-request effective templates` and `effective-template admission freeze` rows; expanded the 범위 bullet with relative-only/pre-access rejection, per-file fallback, and refresh isolation; added source_evidence and the 2026-08-09 변경 기록 entry. |
|
||||
| `agent-spec/input/openai-compatible-surface.md` | Added the `marked single-request internal templates` feature row and the 주요 흐름 bullet separating internal stage templates from caller-visible I/O; added the focused verification command and the 2026-08-09 변경 기록 entry. |
|
||||
| `agent-spec/index.md` | Spec Map 읽는 조건/근거 for the three touched specs now mention the template boundary. |
|
||||
|
||||
`agent-contract/inner/edge-config-runtime-refresh.md` was deliberately not modified: it already specifies `edge.yaml`-relative loading, the 8192-byte cap, digest-only refresh diff evidence, and frozen effective templates on admitted bindings. The implementation converged to it.
|
||||
|
||||
### REVIEW_API-4 — Approved Remote Profile
|
||||
|
||||
```bash
|
||||
test "$(git branch --show-current)" = "feature/single_request_plan_review_templates"
|
||||
test -z "$(git status --short)"
|
||||
git push origin feature/single_request_plan_review_templates
|
||||
ssh toki@toki-labs.com 'cd /Users/toki/agent-work/iop-dev && git fetch origin feature/single_request_plan_review_templates && git switch feature/single_request_plan_review_templates && git reset --hard origin/feature/single_request_plan_review_templates && git clean -fd && test "$(git rev-parse HEAD)" = "$(git rev-parse origin/feature/single_request_plan_review_templates)" && test "$(go env GOOS)/$(go env GOARCH)" = "darwin/arm64" && claude --version && sops --version && test -f dev-openai-toki.edge.yaml && test -f dev-openai-toki.sops.yaml'
|
||||
ssh toki@toki-labs.com 'cd /Users/toki/agent-work/iop-dev && PATH=/opt/homebrew/bin:$PATH make build-edge build-node && PATH=/opt/homebrew/bin:$PATH make test-single-request-claude-smoke-preflight test-single-request-claude-smoke test-single-request-claude-smoke-validate EDGE_CONFIG=dev-openai-toki.edge.yaml SOPS_CONFIG=dev-openai-toki.sops.yaml'
|
||||
```
|
||||
|
||||
_Implementing agent: paste actual stdout/stderr, tested commit, and exit status. If the approved SOPS identity remains unavailable, record the raw failure and exact resume condition without claiming a pass._
|
||||
|
||||
---
|
||||
|
||||
> **[IMPLEMENTING AGENT — BEFORE SAVING] Have you filled in every implementation-owned section?**
|
||||
> If anything is blank, go back and fill it in before saving this file.
|
||||
> Leave review-agent-only sections unchanged.
|
||||
|
||||
## Section Ownership
|
||||
|
||||
| Section | Owner | Note |
|
||||
|---------|-------|------|
|
||||
| Header comment, Overview, Review Agent Instructions | Fixed at stub creation | Implementing agent must not modify or execute archive, `complete.log`, or task-directory moves. |
|
||||
| Archive Evidence Snapshot | Fixed at stub creation from plan | Implementing agent reads only the cited evidence needed for this follow-up. |
|
||||
| Implementation Item Completion | Fixed item names | Implementing agent checks `[ ]` to `[x]` only after actual completion. |
|
||||
| Implementation Checklist | Fixed text/order from plan | Implementing agent checks `[ ]` to `[x]` only after actual completion. |
|
||||
| Review-Only Checklist | Review agent only | Implementing agent must not modify or check this section. |
|
||||
| Deviations from Plan, Key Design Decisions | Implementing agent | Replace placeholders with actual content. |
|
||||
| Reviewer Checkpoints | Fixed at stub creation | Review agent verifies independently. |
|
||||
| Verification Results | Fixed headings and commands | Implementing agent fills actual output only; command changes require a deviation entry. |
|
||||
| Code Review Result | Review agent appends | Not included in this stub. |
|
||||
|
|
@ -0,0 +1,309 @@
|
|||
<!-- task=single_request_plan_review_templates plan=1 tag=REVIEW_API -->
|
||||
|
||||
# Single-request Template Contract Repair - REVIEW_API
|
||||
|
||||
> **For Implementing Agent:** Implement only the checked-in scope below. Do not archive task files, create `complete.log`, or start/monitor orchestration. Record actual implementation and verification evidence in the paired code-review file.
|
||||
|
||||
## Background
|
||||
|
||||
The preceding implementation introduced configurable single-request Plan and Review templates, but official review found two admission-boundary defects, material gaps in the prescribed regression suite, and stale public/current documentation. The follow-up keeps the accepted execution architecture intact and repairs only those repository-owned review findings. Caller-visible response schemas, provider selection, and workspace authority remain unchanged.
|
||||
|
||||
## Archive Evidence Snapshot
|
||||
|
||||
- Prior task: `single_request_plan_review_templates`, plan `0`, tag `API` (active origin).
|
||||
- Archived plan: `agent-task/single_request_plan_review_templates/plan_local_G07_0.log`.
|
||||
- Archived review: `agent-task/single_request_plan_review_templates/code_review_cloud_G07_0.log`.
|
||||
- Prior verdict: `FAIL`; Required R1-R4 cover relative-only template file loading, exact static template grammar, missing boundary/snapshot tests, and incomplete contract/spec synchronization.
|
||||
- Verification: the reviewer reran the focused packages, race suite, full Edge/Go suite, and smoke self-test successfully; the failure is based on untested/incorrect boundaries and stale documentation, not a failing existing command.
|
||||
- Roadmap carryover: none; this is a non-milestone task.
|
||||
|
||||
## Finding Resolution Map
|
||||
|
||||
| Finding | Closure | Direct-fix target | Changed precondition / invariant |
|
||||
|---------|---------|-------------------|----------------------------------|
|
||||
| R1 | direct-fix | `packages/go/config/load.go`, config tests | `plan_file` and `review_file` must be non-empty paths relative to the directory containing `edge.yaml`; absolute paths are rejected before filesystem access. |
|
||||
| R2 | direct-fix | `packages/go/singlerequesttemplate/template.go`, template tests | Required headings and Review `PASS` are exact standalone lines, and only the documented placeholder inventory may remain in admitted templates. |
|
||||
| R3 | direct-fix | config, binding, preset-refresh, and Review-stage tests | Every prescribed admission, freeze/clone/revalidation, refresh-isolation, and rendered-artifact boundary has deterministic regression evidence. |
|
||||
| R4 | direct-fix | outer contract and three current specs | Documentation consistently describes direct PlanMD, frozen effective templates, templated internal Review input, and unchanged caller final-output semantics. |
|
||||
|
||||
## Analysis
|
||||
|
||||
### Files Read
|
||||
|
||||
- `agent-ops/rules/project/rules.md`
|
||||
- `agent-ops/rules/common/rules-roadmap.md`
|
||||
- `agent-roadmap/current.md`
|
||||
- `agent-ops/skills/common/router.md`
|
||||
- `agent-ops/skills/common/code-review/SKILL.md`
|
||||
- `agent-ops/skills/common/plan/SKILL.md`
|
||||
- `agent-ops/skills/common/finalize-task-routing/SKILL.md`
|
||||
- `agent-ops/rules/common/rules-agent-spec.md`
|
||||
- `agent-ops/rules/project/domain-edge.md`
|
||||
- `agent-ops/rules/project/domain-platform-common.md`
|
||||
- `agent-ops/rules/project/domain-testing.md`
|
||||
- `agent-test/local/rules.md`
|
||||
- `agent-test/local/smoke/edge.md`
|
||||
- `agent-test/local/smoke/platform-common.md`
|
||||
- `agent-test/local/smoke/testing.md`
|
||||
- `agent-contract/index.md`
|
||||
- `agent-contract/inner/edge-config.md`
|
||||
- `agent-contract/outer/anthropic-compatible-api.md`
|
||||
- `agent-spec/index.md`
|
||||
- `agent-spec/runtime/provider-pool-config-refresh.md`
|
||||
- `agent-spec/runtime/edge-node-execution.md`
|
||||
- `agent-spec/input/openai-compatible-surface.md`
|
||||
- `packages/go/singlerequesttemplate/template.go`
|
||||
- `packages/go/singlerequesttemplate/template_test.go`
|
||||
- `packages/go/config/load.go`
|
||||
- `packages/go/config/model_execution_preset_config_test.go`
|
||||
- `apps/edge/internal/service/single_request_types.go`
|
||||
- `apps/edge/internal/service/single_request_types_test.go`
|
||||
- `apps/edge/internal/openai/single_request_preset_binding.go`
|
||||
- `apps/edge/internal/openai/single_request_preset_binding_test.go`
|
||||
- `apps/edge/internal/openai/single_request_plan_stage.go`
|
||||
- `apps/edge/internal/openai/single_request_plan_stage_test.go`
|
||||
- `apps/edge/internal/openai/single_request_review_stage.go`
|
||||
- `apps/edge/internal/openai/single_request_review_stage_test.go`
|
||||
- `agent-task/single_request_plan_review_templates/plan_local_G07_0.log`
|
||||
- `agent-task/single_request_plan_review_templates/code_review_cloud_G07_0.log`
|
||||
|
||||
### SDD / Contract Context
|
||||
|
||||
- SDD is not applicable: the accepted architecture is unchanged and this plan repairs review findings within existing contracts.
|
||||
- `agent-contract/inner/edge-config.md` already establishes `edge.yaml`-relative template files; implementation must converge to that contract rather than broaden it.
|
||||
- The outer Anthropic-compatible API contract must state that template customization affects Edge-owned internal stage inputs only and never changes the caller request/response contract.
|
||||
|
||||
### Verification Context
|
||||
|
||||
- There is no separate handoff. The active plan/review pair, cited archive evidence, current rules, contracts, and specs are the authoritative inputs.
|
||||
- Fresh reviewer verification of the preceding implementation passed locally: focused package tests, race tests, `go test -count=1 ./apps/edge/... ./packages/go/...`, and `make test-single-request-claude-smoke-self-test` all exited `0`.
|
||||
- Archived remote-profile evidence identifies `toki@toki-labs.com`, `/Users/toki/agent-work/iop-dev`, Darwin/arm64, Claude CLI `2.1.177`, SOPS `3.13.1`, and implementation commit `b6e0080f`. A new implementation HEAD must be pushed, synchronized, rebuilt, and rechecked before claiming remote evidence.
|
||||
- The approved SOPS age identity was unavailable in the prior remote environment. If it remains unavailable, record the exact raw failure and resume condition; do not substitute an unrelated provider credential or claim a live smoke pass.
|
||||
|
||||
### Test Gaps to Close
|
||||
|
||||
- Config loading lacks per-file fallback, absolute-path rejection, exact 8192-byte acceptance, symlink/non-regular/unreadable rejection, and a complete invalid-grammar matrix.
|
||||
- Binding admission lacks an effective-template snapshot and workspace-revalidation/clone preservation evidence.
|
||||
- Preset refresh lacks proof that already-admitted work keeps frozen templates while newly admitted work receives refreshed templates.
|
||||
- Review-stage coverage lacks a custom-template rendered-artifact snapshot while preserving caller-visible final output.
|
||||
|
||||
### Symbol / Text Reference Audit
|
||||
|
||||
- No production symbol rename is planned.
|
||||
- Remove or rewrite stale textual references to Edge-owned Plan `json_schema`, strict plan/verification JSON, and stage-owned JSON Schema override protection in `agent-spec/runtime/edge-node-execution.md`.
|
||||
|
||||
### Split Decision
|
||||
|
||||
- Use one plan. R1-R4 form one compact admission-and-documentation invariant and can be verified by the same focused/full suites. There are no cross-project dependencies or independently releasable subtasks.
|
||||
|
||||
### Scope Exclusions
|
||||
|
||||
- Do not change caller request/response schemas, provider routing, workspace authorization, the Review loop decision model, protobufs, Node packages, UI surfaces, or centrally managed `agent-ops/rules/common/**` and `agent-ops/skills/common/**`.
|
||||
- Do not add a new template syntax or expand the documented placeholder inventory.
|
||||
- Do not manufacture remote live-smoke evidence when the approved encrypted credential cannot be decrypted.
|
||||
|
||||
### Final Routing
|
||||
|
||||
- Closure booleans: all Required findings are repository-owned and `direct-fix=true`; no `external-follow-up` or `user-decision` closure is needed.
|
||||
- Build scoring: scope `2`, state `1`, blast radius `1`, evidence `2`, verification `2`.
|
||||
- Review scoring: scope `2`, state `1`, blast radius `1`, evidence `2`, verification `2`.
|
||||
- Positive loop risks: `boundary_contract`, `structured_interpretation`, `variant_product` (`3`).
|
||||
- `large_indivisible_context=false`; `review_rework_count=1`; `evidence_integrity_failure=true`.
|
||||
- Exact finalizer result: build basis `recovery-boundary`, build `cloud/G08` (`PLAN-cloud-G08.md`), review basis `official-review`, review `cloud/G08` (`CODE_REVIEW-cloud-G08.md`). No route-cap gap applies.
|
||||
|
||||
## Implementation Checklist
|
||||
|
||||
- [ ] [REVIEW_API-1] Enforce relative-only template loading and exact Plan/Review grammar with boundary regressions.
|
||||
- [ ] [REVIEW_API-2] Add admission, clone/workspace revalidation, preset refresh, and custom Review artifact snapshot tests.
|
||||
- [ ] [REVIEW_API-3] Synchronize current contracts/specs with direct PlanMD, frozen templates, templated internal Review, and unchanged caller output.
|
||||
- [ ] [REVIEW_API-4] Run fresh focused/full/local and remote-profile verification, recording the exact external blocker if the approved SOPS identity remains unavailable.
|
||||
- [ ] Fill implementation-owned sections in `CODE_REVIEW-cloud-G08.md` with actual implementation notes and verification output.
|
||||
|
||||
## Implementation Items
|
||||
|
||||
### REVIEW_API-1 — Close Template Admission Boundaries
|
||||
|
||||
**Problem**
|
||||
|
||||
`loadTemplateFile` accepts absolute paths even though the config contract is relative-only. Template validation uses substring checks for required headings and Review `PASS`, allowing malformed variants such as `### Plan`, `### Review`, and `NOTPASS`; the placeholder grammar is not demonstrably closed.
|
||||
|
||||
**Solution**
|
||||
|
||||
- Trim the configured path, reject empty and absolute values before opening anything, then join only the relative path to the directory containing `edge.yaml`.
|
||||
- Retain existing regular-file, size, UTF-8, and fallback behavior, but cover both Plan and Review independently.
|
||||
- Validate required headings and the Review result token as exact standalone lines.
|
||||
- After accounting for every required placeholder exactly as documented, reject any remaining template delimiters or unknown placeholders.
|
||||
|
||||
Before:
|
||||
|
||||
```go
|
||||
target := strings.TrimSpace(relOrAbsPath)
|
||||
if !filepath.IsAbs(target) {
|
||||
target = filepath.Join(baseDir, target)
|
||||
}
|
||||
```
|
||||
|
||||
After:
|
||||
|
||||
```go
|
||||
target := strings.TrimSpace(relativePath)
|
||||
if filepath.IsAbs(target) {
|
||||
return "", fmt.Errorf("template path must be relative to edge.yaml")
|
||||
}
|
||||
target = filepath.Join(baseDir, target)
|
||||
```
|
||||
|
||||
Replace substring grammar checks such as:
|
||||
|
||||
```go
|
||||
if strings.Index(tmpl, "# Plan") < 0 { /* ... */ }
|
||||
```
|
||||
|
||||
with a line-aware exact matcher and closed-token validation shared by both variants.
|
||||
|
||||
**Modified Files Checklist**
|
||||
|
||||
- [ ] `packages/go/singlerequesttemplate/template.go`
|
||||
- [ ] `packages/go/singlerequesttemplate/template_test.go`
|
||||
- [ ] `packages/go/config/load.go`
|
||||
- [ ] `packages/go/config/model_execution_preset_config_test.go`
|
||||
|
||||
**Test Strategy**
|
||||
|
||||
- Extend `TestValidatePlanTemplate` and `TestValidateReviewTemplate` for exact headings/result lines, missing/duplicate/unknown placeholders, and delimiter residue.
|
||||
- Extend `TestLoadEdgeSingleRequestTemplates` for Plan/Review absolute-path rejection, relative success, per-file fallback, exact size boundary, and filesystem-kind failures.
|
||||
|
||||
**Verification**
|
||||
|
||||
- Run the focused template/config tests and confirm every malformed variant is rejected without exposing file content in errors.
|
||||
|
||||
### REVIEW_API-2 — Prove Freeze, Refresh, and Render Semantics
|
||||
|
||||
**Problem**
|
||||
|
||||
The preceding review claimed coverage that is absent from the actual test files. The admitted effective template snapshot, clone/workspace revalidation, refresh isolation, and custom Review artifact are therefore not protected against regression.
|
||||
|
||||
**Solution**
|
||||
|
||||
- Add deterministic service-level assertions that admitted bindings carry the exact effective Plan/Review template pair through clone and workspace revalidation paths.
|
||||
- Add preset refresh assertions that existing work retains its frozen templates and newly admitted work observes the refreshed pair.
|
||||
- Add a Review-stage custom-template snapshot proving internal artifact rendering uses the frozen template while `decision.output` remains the caller-visible final response.
|
||||
- Do not change production behavior unless a new regression exposes a concrete defect.
|
||||
|
||||
**Modified Files Checklist**
|
||||
|
||||
- [ ] `apps/edge/internal/service/single_request_types_test.go`
|
||||
- [ ] `apps/edge/internal/openai/single_request_preset_binding_test.go`
|
||||
- [ ] `apps/edge/internal/openai/single_request_review_stage_test.go`
|
||||
|
||||
**Test Strategy**
|
||||
|
||||
- Add/extend `TestSingleRequestBindingTemplateSnapshot`.
|
||||
- Add/extend `TestSingleRequestPresetBindingTemplateRefreshIsolation`.
|
||||
- Add/extend `TestSingleRequestReviewStageCustomTemplateSnapshot`.
|
||||
- Assert both the internal rendered artifact and unchanged caller output; do not rely on substring-only assertions where an exact snapshot is stable.
|
||||
|
||||
**Verification**
|
||||
|
||||
- Run the named focused packages, then the race suite covering OpenAI stages, service admission, refresh, config, and template validation.
|
||||
|
||||
### REVIEW_API-3 — Synchronize Current Contracts and Specs
|
||||
|
||||
**Problem**
|
||||
|
||||
The current runtime spec still describes a Plan JSON Schema path that the implementation removed. The outer API contract and input spec do not yet document the accepted frozen-template/direct-PlanMD/internal-Review model, leaving the current documentation contradictory.
|
||||
|
||||
**Solution**
|
||||
|
||||
- Update the outer Anthropic-compatible contract to describe unchanged caller I/O and Edge-owned internal template customization.
|
||||
- Update the provider-pool refresh spec with admission-time effective-template freezing and refresh isolation.
|
||||
- Rewrite stale Plan JSON Schema passages in the Edge execution spec to direct PlanMD validation and templated Review artifact semantics.
|
||||
- Update the OpenAI-compatible input surface to distinguish internal stage templates from caller-visible input/output.
|
||||
- Preserve existing terminology and add the required current-spec change records where that spec format requires them.
|
||||
|
||||
**Modified Files Checklist**
|
||||
|
||||
- [ ] `agent-contract/outer/anthropic-compatible-api.md`
|
||||
- [ ] `agent-spec/runtime/provider-pool-config-refresh.md`
|
||||
- [ ] `agent-spec/runtime/edge-node-execution.md`
|
||||
- [ ] `agent-spec/input/openai-compatible-surface.md`
|
||||
|
||||
**Test Strategy**
|
||||
|
||||
- Search the current spec for the known stale Plan JSON Schema phrases and require no matches.
|
||||
- Cross-check every new statement against the implementation and the inner Edge config contract.
|
||||
|
||||
**Verification**
|
||||
|
||||
- Run the documented `rg` guard and manually confirm the four surfaces agree on admission, execution, refresh, and caller-output boundaries.
|
||||
|
||||
### REVIEW_API-4 — Produce Trustworthy Verification Evidence
|
||||
|
||||
**Problem**
|
||||
|
||||
The earlier implementation evidence checked off work and named tests/docs that were absent. Local behavior passed, but the recorded completeness and remote live-smoke claims cannot be trusted without fresh evidence.
|
||||
|
||||
**Solution**
|
||||
|
||||
- Run every command in Final Verification against the completed working tree and paste actual output into the paired review file.
|
||||
- Confirm the pushed commit and the remote checkout are identical before a remote rebuild or live smoke.
|
||||
- If approved SOPS decryption remains unavailable, record the exact failing command, raw error, exit code, and resume condition. Treat that as an allowed external-profile blocker only; never convert it into a local pass.
|
||||
|
||||
**Modified Files Checklist**
|
||||
|
||||
- [ ] `agent-task/single_request_plan_review_templates/CODE_REVIEW-cloud-G08.md`
|
||||
|
||||
**Test Strategy**
|
||||
|
||||
- Verify formatting, focused tests, race tests, the full Edge/Go suite, the deterministic smoke self-test, the documentation guard, and the approved remote profile.
|
||||
|
||||
**Verification**
|
||||
|
||||
- Evidence must identify the tested commit and clearly separate local success from any externally blocked live qualification.
|
||||
|
||||
## Modified Files Summary
|
||||
|
||||
| File | Item |
|
||||
|---|---|
|
||||
| `packages/go/singlerequesttemplate/template.go` | REVIEW_API-1 |
|
||||
| `packages/go/singlerequesttemplate/template_test.go` | REVIEW_API-1 |
|
||||
| `packages/go/config/load.go` | REVIEW_API-1 |
|
||||
| `packages/go/config/model_execution_preset_config_test.go` | REVIEW_API-1 |
|
||||
| `apps/edge/internal/service/single_request_types_test.go` | REVIEW_API-2 |
|
||||
| `apps/edge/internal/openai/single_request_preset_binding_test.go` | REVIEW_API-2 |
|
||||
| `apps/edge/internal/openai/single_request_review_stage_test.go` | REVIEW_API-2 |
|
||||
| `agent-contract/outer/anthropic-compatible-api.md` | REVIEW_API-3 |
|
||||
| `agent-spec/runtime/provider-pool-config-refresh.md` | REVIEW_API-3 |
|
||||
| `agent-spec/runtime/edge-node-execution.md` | REVIEW_API-3 |
|
||||
| `agent-spec/input/openai-compatible-surface.md` | REVIEW_API-3 |
|
||||
| `agent-task/single_request_plan_review_templates/CODE_REVIEW-cloud-G08.md` | REVIEW_API-4 |
|
||||
|
||||
## Final Verification
|
||||
|
||||
Run locally from the repository root:
|
||||
|
||||
```bash
|
||||
test -z "$(gofmt -l packages/go/singlerequesttemplate/template.go packages/go/singlerequesttemplate/template_test.go packages/go/config/load.go packages/go/config/model_execution_preset_config_test.go apps/edge/internal/service/single_request_types_test.go apps/edge/internal/openai/single_request_preset_binding_test.go apps/edge/internal/openai/single_request_review_stage_test.go)"
|
||||
go test -count=1 ./packages/go/singlerequesttemplate ./packages/go/config
|
||||
go test -count=1 ./apps/edge/internal/service -run 'TestSingleRequestBinding'
|
||||
go test -count=1 ./apps/edge/internal/openai -run 'TestSingleRequest(PresetBinding|ReviewStage)'
|
||||
go test -race -count=1 ./apps/edge/internal/openai ./apps/edge/internal/service ./apps/edge/internal/configrefresh ./packages/go/config ./packages/go/singlerequesttemplate
|
||||
go test -count=1 ./apps/edge/... ./packages/go/...
|
||||
make test-single-request-claude-smoke-self-test
|
||||
if rg --sort path -n 'Edge-owned OpenAI `json_schema` response format|strict small plan/verification JSON|stage-owned JSON Schema override protection' agent-spec/runtime/edge-node-execution.md; then exit 1; fi
|
||||
```
|
||||
|
||||
Then verify the approved remote profile against the exact pushed implementation commit:
|
||||
|
||||
```bash
|
||||
test "$(git branch --show-current)" = "feature/single_request_plan_review_templates"
|
||||
test -z "$(git status --short)"
|
||||
git push origin feature/single_request_plan_review_templates
|
||||
|
||||
ssh toki@toki-labs.com 'cd /Users/toki/agent-work/iop-dev && git fetch origin feature/single_request_plan_review_templates && git switch feature/single_request_plan_review_templates && git reset --hard origin/feature/single_request_plan_review_templates && git clean -fd && test "$(git rev-parse HEAD)" = "$(git rev-parse origin/feature/single_request_plan_review_templates)" && test "$(go env GOOS)/$(go env GOARCH)" = "darwin/arm64" && claude --version && sops --version && test -f dev-openai-toki.edge.yaml && test -f dev-openai-toki.sops.yaml'
|
||||
ssh toki@toki-labs.com 'cd /Users/toki/agent-work/iop-dev && PATH=/opt/homebrew/bin:$PATH make build-edge build-node && PATH=/opt/homebrew/bin:$PATH make test-single-request-claude-smoke-preflight test-single-request-claude-smoke test-single-request-claude-smoke-validate EDGE_CONFIG=dev-openai-toki.edge.yaml SOPS_CONFIG=dev-openai-toki.sops.yaml'
|
||||
```
|
||||
|
||||
If the remote SOPS age identity is unavailable, paste the exact raw failure and exit code into `CODE_REVIEW-cloud-G08.md`, state the identity-mount/export resume condition, and leave live API qualification explicitly blocked. Do not reuse old output or substitute another credential.
|
||||
|
||||
Do not archive this plan, write `complete.log`, or move the task directory; those actions belong to the review agent after an official verdict.
|
||||
11
agent-task/single_request_plan_review_templates/WORK_LOG.md
Normal file
11
agent-task/single_request_plan_review_templates/WORK_LOG.md
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
# Milestone Work Log
|
||||
|
||||
> Dispatcher-owned execution timeline. Workers and reviewers do not edit this file.
|
||||
|
||||
| seq | time | event | task | loop | role | attempt | model | result | locator |
|
||||
|---:|---|---|---|---:|---|---:|---|---|---|
|
||||
| 1 | 26-08-09 08:29:59 KST | START | single_request_plan_review_templates/PLAN-local-G07.md | 0 | worker | 0 | agy/Gemini 3.6 Flash (High) | running | /config/workspace/iop-s2/.git/agent-task-dispatcher/runs/20260809T082959+0900__single_request_plan_review_templates__p0__worker__a00/locator.json |
|
||||
| 2 | 26-08-09 08:48:03 KST | FINISH | single_request_plan_review_templates/PLAN-local-G07.md | 0 | worker | 0 | agy/Gemini 3.6 Flash (High) | succeeded:0 | /config/workspace/iop-s2/.git/agent-task-dispatcher/runs/20260809T082959+0900__single_request_plan_review_templates__p0__worker__a00/locator.json |
|
||||
| 3 | 26-08-09 08:48:03 KST | START | single_request_plan_review_templates/CODE_REVIEW-cloud-G07.md | 0 | review | 0 | codex/gpt-5.6-sol | running | /config/workspace/iop-s2/.git/agent-task-dispatcher/runs/20260809T084803+0900__single_request_plan_review_templates__p0__review__a00/locator.json |
|
||||
| 4 | 26-08-09 09:02:18 KST | FINISH | single_request_plan_review_templates/CODE_REVIEW-cloud-G07.md | 0 | review | 0 | codex/gpt-5.6-sol | succeeded:0 | /config/workspace/iop-s2/.git/agent-task-dispatcher/runs/20260809T084803+0900__single_request_plan_review_templates__p0__review__a00/locator.json |
|
||||
| 5 | 26-08-09 09:03:44 KST | START | single_request_plan_review_templates/PLAN-cloud-G08.md | 1 | worker | 0 | claude/claude-opus-5 | running | /config/workspace/iop-s2/.git/agent-task-dispatcher/runs/20260809T090344+0900__single_request_plan_review_templates__p1__worker__a00/locator.json |
|
||||
|
|
@ -56,16 +56,16 @@ Review completion means the following steps are finished:
|
|||
> **[REVIEW AGENT ONLY]** This checklist is used only by the review agent.
|
||||
> Implementing agents must not modify or check this section.
|
||||
|
||||
- [ ] Append one verdict of `PASS`, `WARN`, or `FAIL` and verified `review_rework_count`, `evidence_integrity_failure` to `Code Review Result`.
|
||||
- [ ] Verify that verdict, `Dimension Assessment`, and Required/Suggested/Nit classifications match.
|
||||
- [ ] Archive active `CODE_REVIEW-*-G??.md` to `code_review_cloud_G07_0.log`.
|
||||
- [ ] Archive active `PLAN-*-G??.md` to `plan_local_G07_0.log`.
|
||||
- [ ] Verify that the Agent-Ops managed block in `.gitignore` unignores `agent-task/**/*.md` and `agent-task/**/*.log` and ignores `agent-roadmap/current.md`.
|
||||
- [x] Append one verdict of `PASS`, `WARN`, or `FAIL` and verified `review_rework_count`, `evidence_integrity_failure` to `Code Review Result`.
|
||||
- [x] Verify that verdict, `Dimension Assessment`, and Required/Suggested/Nit classifications match.
|
||||
- [x] Archive active `CODE_REVIEW-*-G??.md` to `code_review_cloud_G07_0.log`.
|
||||
- [x] Archive active `PLAN-*-G??.md` to `plan_local_G07_0.log`.
|
||||
- [x] Verify that the Agent-Ops managed block in `.gitignore` unignores `agent-task/**/*.md` and `agent-task/**/*.log` and ignores `agent-roadmap/current.md`.
|
||||
- [ ] If PASS, write `complete.log` based on `agent-ops/skills/common/code-review/templates/complete-log-template.md` and leave no active `.md` files.
|
||||
- [ ] If PASS, move active task directory `agent-task/single_request_plan_review_templates/` to `agent-task/archive/YYYY/MM/single_request_plan_review_templates/` and update this checklist at the final archive path.
|
||||
- [ ] If PASS and task group is `m-<milestone-slug>`, preserve and report `milestone-task` metadata for runtime aggregation, without modifying roadmap or directly calling `update-roadmap`.
|
||||
- [ ] If PASS for split work, remove empty active parent `agent-task/single_request_plan_review_templates/` or verify it was kept due to remaining siblings/files.
|
||||
- [ ] If WARN/FAIL, write the next filesystem state matching code-review verdict and do not write `complete.log`.
|
||||
- [x] If WARN/FAIL, write the next filesystem state matching code-review verdict and do not write `complete.log`.
|
||||
|
||||
## Deviations from Plan
|
||||
|
||||
|
|
@ -355,3 +355,24 @@ Resume condition: Mount or export the approved SOPS age key (`dev-openai-toki.so
|
|||
| Reviewer Checkpoints | Fixed at stub creation | Pre-filled from plan |
|
||||
| Verification Results (section headings + commands) | Fixed at stub creation | Implementing agent fills in command output only; command changes require a `Deviations from Plan` entry |
|
||||
| Code Review Result | Review agent appends | Not included in stub |
|
||||
|
||||
## Code Review Result
|
||||
|
||||
- Overall Verdict: `FAIL`
|
||||
- Dimension Assessment:
|
||||
- Correctness: Fail
|
||||
- Completeness: Fail
|
||||
- Test coverage: Fail
|
||||
- API contract: Fail
|
||||
- Code quality: Pass
|
||||
- Implementation deviation: Fail
|
||||
- Verification trust: Fail
|
||||
- Findings:
|
||||
- Required R1 — `packages/go/config/load.go:718`: `loadTemplateFile` explicitly preserves absolute paths, although the approved config contract defines `plan_file` and `review_file` as `edge.yaml`-relative. Reject absolute values before filesystem access and add Plan/Review regression cases proving only config-relative paths are admitted.
|
||||
- Required R2 — `packages/go/singlerequesttemplate/template.go:93`, `packages/go/singlerequesttemplate/template.go:140`, `packages/go/singlerequesttemplate/template.go:142`: heading and `PASS` checks use substring search, so malformed static grammar such as `### Plan`, `### Review`, or `NOTPASS` is admitted. Enforce exact heading/result lines and a closed placeholder inventory, then add boundary regressions for both template variants.
|
||||
- Required R3 — `apps/edge/internal/service/single_request_types_test.go:153`, `apps/edge/internal/openai/single_request_preset_binding_test.go:397`, `apps/edge/internal/openai/single_request_review_stage_test.go:306`, `packages/go/config/model_execution_preset_config_test.go:591`: the implementation marked API-1/API-2/API-4 complete, but the prescribed template snapshot/clone/workspace-revalidation, preset refresh, custom Review artifact, and config file-boundary matrix are absent. Add the required deterministic tests, including per-file fallback, exact 8192-byte acceptance, symlink/non-regular/unreadable rejection, invalid grammar, custom Review rendering, and admitted-template refresh isolation.
|
||||
- Required R4 — `agent-spec/runtime/edge-node-execution.md:245`, `agent-spec/runtime/edge-node-execution.md:333`, `agent-spec/runtime/edge-node-execution.md:372`, `agent-spec/runtime/edge-node-execution.md:373`, `agent-contract/outer/anthropic-compatible-api.md:80`, `agent-spec/input/openai-compatible-surface.md:171`: current documentation is incomplete and internally contradictory. The runtime spec still describes the removed Plan JSON Schema path, while the outer contract and input spec were not updated with frozen operator templates, direct PlanMD validation, templated internal Review artifacts, and unchanged caller final-output semantics. Synchronize all planned contract/spec surfaces with the code and verification names.
|
||||
- Routing Signals:
|
||||
- `review_rework_count=1`
|
||||
- `evidence_integrity_failure=true`
|
||||
- Next Step: Invoke the plan skill in `prepare-follow-up` mode with Required R1-R4 as repository-owned direct fixes, then archive this pair and materialize the freshly routed follow-up pair. Do not write `complete.log`.
|
||||
|
|
@ -2,11 +2,13 @@ package openai
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"iop/apps/edge/internal/authprojection"
|
||||
edgeservice "iop/apps/edge/internal/service"
|
||||
"iop/packages/go/config"
|
||||
"iop/packages/go/singlerequesttemplate"
|
||||
)
|
||||
|
||||
func newTestView(principalRef string, routes []authprojection.Route) authprojection.AuthenticatedView {
|
||||
|
|
@ -383,6 +385,179 @@ func TestSingleRequestPresetBindingRefreshIsolation(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// customPresetPlanTemplate and customPresetReviewTemplate are operator-authored
|
||||
// effective templates that differ from the built-in defaults, so an admitted
|
||||
// snapshot cannot pass by accidentally falling back.
|
||||
const (
|
||||
customPresetPlanTemplate = `# Plan
|
||||
|
||||
Operator preamble v1.
|
||||
|
||||
## Goal
|
||||
{{goal}}
|
||||
|
||||
## Steps
|
||||
{{steps}}
|
||||
|
||||
## Verification
|
||||
{{verification}}
|
||||
`
|
||||
customPresetReviewTemplate = `# Review
|
||||
|
||||
Operator preamble v1.
|
||||
|
||||
## Result
|
||||
PASS
|
||||
|
||||
## Checks
|
||||
{{checks}}
|
||||
|
||||
## Verification
|
||||
{{verification}}
|
||||
|
||||
## Summary
|
||||
{{summary}}
|
||||
`
|
||||
refreshedPresetPlanTemplate = `# Plan
|
||||
|
||||
Operator preamble v2.
|
||||
|
||||
## Goal
|
||||
{{goal}}
|
||||
|
||||
## Steps
|
||||
{{steps}}
|
||||
|
||||
## Verification
|
||||
{{verification}}
|
||||
`
|
||||
refreshedPresetReviewTemplate = `# Review
|
||||
|
||||
Operator preamble v2.
|
||||
|
||||
## Result
|
||||
PASS
|
||||
|
||||
## Checks
|
||||
{{checks}}
|
||||
|
||||
## Verification
|
||||
{{verification}}
|
||||
|
||||
## Summary
|
||||
{{summary}}
|
||||
`
|
||||
)
|
||||
|
||||
// TestSingleRequestPresetBindingTemplateRefreshIsolation proves the effective
|
||||
// template pair is frozen at admission: an already admitted binding keeps its
|
||||
// pair across a preset refresh, while a request admitted after the refresh
|
||||
// observes the refreshed pair.
|
||||
func TestSingleRequestPresetBindingTemplateRefreshIsolation(t *testing.T) {
|
||||
preset := validSingleRequestPreset()
|
||||
preset.SingleRequest.Templates.EffectivePlan = customPresetPlanTemplate
|
||||
preset.SingleRequest.Templates.EffectiveReview = customPresetReviewTemplate
|
||||
bindings := validSingleRequestBindings()
|
||||
view := newTestView("principal-1", nil)
|
||||
|
||||
admitted, err := compileSingleRequestBinding("virtual-public-model", preset, bindings, view)
|
||||
if err != nil {
|
||||
t.Fatalf("compilation failed: %v", err)
|
||||
}
|
||||
if admitted.Templates.Plan != customPresetPlanTemplate || admitted.Templates.Review != customPresetReviewTemplate {
|
||||
t.Fatalf("admitted templates = %+v, want the configured pair", admitted.Templates)
|
||||
}
|
||||
|
||||
// Simulate a config refresh that replaces the operator templates.
|
||||
preset.SingleRequest.Templates.EffectivePlan = refreshedPresetPlanTemplate
|
||||
preset.SingleRequest.Templates.EffectiveReview = refreshedPresetReviewTemplate
|
||||
|
||||
// Already admitted work keeps the frozen pair, including through the clone
|
||||
// the coordinator hands to executors.
|
||||
if admitted.Templates.Plan != customPresetPlanTemplate {
|
||||
t.Errorf("admitted Templates.Plan reflected the refresh: %q", admitted.Templates.Plan)
|
||||
}
|
||||
if admitted.Templates.Review != customPresetReviewTemplate {
|
||||
t.Errorf("admitted Templates.Review reflected the refresh: %q", admitted.Templates.Review)
|
||||
}
|
||||
if clone := admitted.Clone(); clone.Templates != admitted.Templates {
|
||||
t.Errorf("clone templates = %+v, want %+v", clone.Templates, admitted.Templates)
|
||||
}
|
||||
|
||||
// Newly admitted work observes the refreshed pair.
|
||||
refreshed, err := compileSingleRequestBinding("virtual-public-model", preset, bindings, view)
|
||||
if err != nil {
|
||||
t.Fatalf("post-refresh compilation failed: %v", err)
|
||||
}
|
||||
if refreshed.Templates.Plan != refreshedPresetPlanTemplate || refreshed.Templates.Review != refreshedPresetReviewTemplate {
|
||||
t.Fatalf("post-refresh templates = %+v, want the refreshed pair", refreshed.Templates)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSingleRequestPresetBindingTemplateFallback proves each effective template
|
||||
// falls back to its built-in default independently and that a preset carrying
|
||||
// an invalid effective template cannot compile an admission.
|
||||
func TestSingleRequestPresetBindingTemplateFallback(t *testing.T) {
|
||||
view := newTestView("principal-1", nil)
|
||||
|
||||
t.Run("both templates fall back", func(t *testing.T) {
|
||||
binding, err := compileSingleRequestBinding("virtual-public-model", validSingleRequestPreset(), validSingleRequestBindings(), view)
|
||||
if err != nil {
|
||||
t.Fatalf("compilation failed: %v", err)
|
||||
}
|
||||
if binding.Templates.Plan != singlerequesttemplate.DefaultPlanTemplate {
|
||||
t.Errorf("Templates.Plan = %q, want the built-in default", binding.Templates.Plan)
|
||||
}
|
||||
if binding.Templates.Review != singlerequesttemplate.DefaultReviewTemplate {
|
||||
t.Errorf("Templates.Review = %q, want the built-in default", binding.Templates.Review)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("plan configured and review falls back", func(t *testing.T) {
|
||||
preset := validSingleRequestPreset()
|
||||
preset.SingleRequest.Templates.EffectivePlan = customPresetPlanTemplate
|
||||
binding, err := compileSingleRequestBinding("virtual-public-model", preset, validSingleRequestBindings(), view)
|
||||
if err != nil {
|
||||
t.Fatalf("compilation failed: %v", err)
|
||||
}
|
||||
if binding.Templates.Plan != customPresetPlanTemplate {
|
||||
t.Errorf("Templates.Plan = %q, want the configured template", binding.Templates.Plan)
|
||||
}
|
||||
if binding.Templates.Review != singlerequesttemplate.DefaultReviewTemplate {
|
||||
t.Errorf("Templates.Review = %q, want the built-in default", binding.Templates.Review)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("review configured and plan falls back", func(t *testing.T) {
|
||||
preset := validSingleRequestPreset()
|
||||
preset.SingleRequest.Templates.EffectiveReview = customPresetReviewTemplate
|
||||
binding, err := compileSingleRequestBinding("virtual-public-model", preset, validSingleRequestBindings(), view)
|
||||
if err != nil {
|
||||
t.Fatalf("compilation failed: %v", err)
|
||||
}
|
||||
if binding.Templates.Review != customPresetReviewTemplate {
|
||||
t.Errorf("Templates.Review = %q, want the configured template", binding.Templates.Review)
|
||||
}
|
||||
if binding.Templates.Plan != singlerequesttemplate.DefaultPlanTemplate {
|
||||
t.Errorf("Templates.Plan = %q, want the built-in default", binding.Templates.Plan)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("invalid effective template fails admission closed", func(t *testing.T) {
|
||||
preset := validSingleRequestPreset()
|
||||
preset.SingleRequest.Templates.EffectivePlan = "### Plan\n\n## Goal\n{{goal}}\n\n## Steps\n{{steps}}\n\n## Verification\n{{verification}}\n"
|
||||
if _, err := compileSingleRequestBinding("virtual-public-model", preset, validSingleRequestBindings(), view); err == nil {
|
||||
t.Error("expected rejection for a decorated Plan heading")
|
||||
}
|
||||
|
||||
preset = validSingleRequestPreset()
|
||||
preset.SingleRequest.Templates.EffectiveReview = strings.Replace(customPresetReviewTemplate, "PASS", "NOTPASS", 1)
|
||||
if _, err := compileSingleRequestBinding("virtual-public-model", preset, validSingleRequestBindings(), view); err == nil {
|
||||
t.Error("expected rejection for a NOTPASS Review result line")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestSingleRequestPresetBindingPublicModelEcho(t *testing.T) {
|
||||
preset := validSingleRequestPreset()
|
||||
bindings := validSingleRequestBindings()
|
||||
|
|
|
|||
|
|
@ -476,6 +476,162 @@ func TestSingleRequestReviewBodyRejectsOptionAliases(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// customReviewStageTemplate is an operator-authored effective Review template
|
||||
// that differs from the built-in default, so a rendered artifact cannot pass by
|
||||
// accidentally falling back.
|
||||
const customReviewStageTemplate = `# Review
|
||||
|
||||
Operator preamble.
|
||||
|
||||
## Result
|
||||
PASS
|
||||
|
||||
## Checks
|
||||
{{checks}}
|
||||
|
||||
## Verification
|
||||
{{verification}}
|
||||
|
||||
## Summary
|
||||
{{summary}}
|
||||
|
||||
Operator footer.
|
||||
`
|
||||
|
||||
// TestSingleRequestReviewStageCustomTemplateSnapshot proves the Review stage
|
||||
// renders its internal artifact from the frozen effective template while the
|
||||
// caller-visible final output stays exactly the model's `decision.output`,
|
||||
// independent of which template is admitted.
|
||||
func TestSingleRequestReviewStageCustomTemplateSnapshot(t *testing.T) {
|
||||
decision := singleRequestReviewDecision{
|
||||
Decision: "pass",
|
||||
Output: " result.txt now contains the requested value. ",
|
||||
Checks: "- Checked requirements",
|
||||
Verification: "- Verified tests pass",
|
||||
Summary: "Task completed.",
|
||||
}
|
||||
const wantOutput = "result.txt now contains the requested value."
|
||||
|
||||
t.Run("custom template renders the exact internal artifact", func(t *testing.T) {
|
||||
artifact, result, err := renderSingleRequestReview(customReviewStageTemplate, decision, 4096)
|
||||
if err != nil {
|
||||
t.Fatalf("renderSingleRequestReview failed: %v", err)
|
||||
}
|
||||
want := `# Review
|
||||
|
||||
Operator preamble.
|
||||
|
||||
## Result
|
||||
PASS
|
||||
|
||||
## Checks
|
||||
- Checked requirements
|
||||
|
||||
## Verification
|
||||
- Verified tests pass
|
||||
|
||||
## Summary
|
||||
Task completed.
|
||||
|
||||
Operator footer.
|
||||
`
|
||||
if string(artifact) != want {
|
||||
t.Errorf("artifact:\n%s\nwant:\n%s", string(artifact), want)
|
||||
}
|
||||
if string(result.Output) != wantOutput {
|
||||
t.Errorf("caller output = %q, want %q", string(result.Output), wantOutput)
|
||||
}
|
||||
if result.Summary != "Task completed." {
|
||||
t.Errorf("summary = %q, want %q", result.Summary, "Task completed.")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("default template renders the exact internal artifact", func(t *testing.T) {
|
||||
artifact, result, err := renderSingleRequestReview(singlerequesttemplate.DefaultReviewTemplate, decision, 4096)
|
||||
if err != nil {
|
||||
t.Fatalf("renderSingleRequestReview failed: %v", err)
|
||||
}
|
||||
want := `# Review
|
||||
|
||||
## Result
|
||||
PASS
|
||||
|
||||
## Checks
|
||||
- Checked requirements
|
||||
|
||||
## Verification
|
||||
- Verified tests pass
|
||||
|
||||
## Summary
|
||||
Task completed.
|
||||
`
|
||||
if string(artifact) != want {
|
||||
t.Errorf("artifact:\n%s\nwant:\n%s", string(artifact), want)
|
||||
}
|
||||
// The template selects only the internal artifact shape; the
|
||||
// caller-visible final response is identical to the custom-template run.
|
||||
if string(result.Output) != wantOutput {
|
||||
t.Errorf("caller output = %q, want %q", string(result.Output), wantOutput)
|
||||
}
|
||||
})
|
||||
|
||||
// A stage run through the coordinator writes exactly that artifact through
|
||||
// the closed REVIEW selector and returns the same caller-visible output.
|
||||
t.Run("stage run writes the custom artifact and preserves caller output", func(t *testing.T) {
|
||||
bridge := newSingleRequestWorkToolBridge()
|
||||
ctrl := newReviewController(t, bridge)
|
||||
ctrl.binding.Templates.Review = customReviewStageTemplate
|
||||
|
||||
var bodies [][]byte
|
||||
stage := newSingleRequestReviewStage(scriptedReviewProvider(t, ctrl, [][]byte{
|
||||
reviewPassBody(wantOutput, "Task completed."),
|
||||
}, &bodies), bridge)
|
||||
|
||||
result, err := stage.run(context.Background(), reviewRequest(t), ctrl)
|
||||
if err != nil {
|
||||
t.Fatalf("review stage run failed: %v", err)
|
||||
}
|
||||
|
||||
want := `# Review
|
||||
|
||||
Operator preamble.
|
||||
|
||||
## Result
|
||||
PASS
|
||||
|
||||
## Checks
|
||||
- Checked requirements
|
||||
|
||||
## Verification
|
||||
- Verified tests pass
|
||||
|
||||
## Summary
|
||||
Task completed.
|
||||
|
||||
Operator footer.
|
||||
`
|
||||
if len(ctrl.writes) != 1 || ctrl.writes[0] != edgeservice.SingleRequestArtifactReview {
|
||||
t.Fatalf("artifact writes = %v, want exactly one REVIEW write", ctrl.writes)
|
||||
}
|
||||
if string(ctrl.artifact) != want {
|
||||
t.Errorf("written artifact:\n%s\nwant:\n%s", string(ctrl.artifact), want)
|
||||
}
|
||||
if string(result.Output) != wantOutput {
|
||||
t.Errorf("caller output = %q, want %q", string(result.Output), wantOutput)
|
||||
}
|
||||
// The internal template must not leak into the caller-visible output.
|
||||
if strings.Contains(string(result.Output), "Operator preamble") || strings.Contains(string(result.Output), "## Checks") {
|
||||
t.Errorf("caller output leaked internal artifact text: %q", string(result.Output))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("an invalid effective template fails the render closed", func(t *testing.T) {
|
||||
if _, _, err := renderSingleRequestReview(strings.Replace(customReviewStageTemplate, "PASS", "NOTPASS", 1), decision, 4096); !errors.Is(err, errSingleRequestReviewStage) {
|
||||
t.Fatalf("err = %v, want errSingleRequestReviewStage", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func expectedSingleRequestReviewBodyAuthority(isResumed bool) map[string]any {
|
||||
var raw string
|
||||
if !isResumed {
|
||||
|
|
|
|||
|
|
@ -2,9 +2,11 @@ package service
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"iop/packages/go/config"
|
||||
"iop/packages/go/singlerequesttemplate"
|
||||
)
|
||||
|
||||
func validLimits() SingleRequestLimits {
|
||||
|
|
@ -476,6 +478,148 @@ func TestSingleRequestBindingDispatchBindingMutationAfterAdmission(t *testing.T)
|
|||
}
|
||||
}
|
||||
|
||||
// customPlanTemplate and customReviewTemplate are operator-authored effective
|
||||
// templates. They differ from the built-in defaults so an admitted snapshot
|
||||
// cannot pass by accidentally falling back.
|
||||
const (
|
||||
customPlanTemplate = `# Plan
|
||||
|
||||
Operator preamble.
|
||||
|
||||
## Goal
|
||||
{{goal}}
|
||||
|
||||
## Steps
|
||||
{{steps}}
|
||||
|
||||
## Verification
|
||||
{{verification}}
|
||||
`
|
||||
customReviewTemplate = `# Review
|
||||
|
||||
Operator preamble.
|
||||
|
||||
## Result
|
||||
PASS
|
||||
|
||||
## Checks
|
||||
{{checks}}
|
||||
|
||||
## Verification
|
||||
{{verification}}
|
||||
|
||||
## Summary
|
||||
{{summary}}
|
||||
`
|
||||
)
|
||||
|
||||
// TestSingleRequestBindingTemplateSnapshot proves the effective Plan/Review
|
||||
// template pair is frozen at admission and carried unchanged through Clone and
|
||||
// the workspace revalidation path, and that an invalid template cannot be
|
||||
// admitted.
|
||||
func TestSingleRequestBindingTemplateSnapshot(t *testing.T) {
|
||||
plan, work, review := validStages()
|
||||
|
||||
t.Run("admitted binding carries the exact effective pair", func(t *testing.T) {
|
||||
b, err := NewSingleRequestBindingWithTemplates("virtual-model", "ws-ref", plan, work, review, validLimits(),
|
||||
SingleRequestTemplateBinding{Plan: customPlanTemplate, Review: customReviewTemplate})
|
||||
if err != nil {
|
||||
t.Fatalf("custom template admission failed: %v", err)
|
||||
}
|
||||
if b.Templates.Plan != customPlanTemplate {
|
||||
t.Errorf("Templates.Plan = %q, want the configured custom Plan template", b.Templates.Plan)
|
||||
}
|
||||
if b.Templates.Review != customReviewTemplate {
|
||||
t.Errorf("Templates.Review = %q, want the configured custom Review template", b.Templates.Review)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("template-less admission falls back to the built-in defaults", func(t *testing.T) {
|
||||
b, err := NewSingleRequestBinding("virtual-model", "ws-ref", plan, work, review, validLimits())
|
||||
if err != nil {
|
||||
t.Fatalf("default admission failed: %v", err)
|
||||
}
|
||||
if b.Templates.Plan != singlerequesttemplate.DefaultPlanTemplate {
|
||||
t.Errorf("Templates.Plan = %q, want the built-in default", b.Templates.Plan)
|
||||
}
|
||||
if b.Templates.Review != singlerequesttemplate.DefaultReviewTemplate {
|
||||
t.Errorf("Templates.Review = %q, want the built-in default", b.Templates.Review)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("clone preserves the frozen pair", func(t *testing.T) {
|
||||
b, err := NewSingleRequestBindingWithTemplates("virtual-model", "ws-ref", plan, work, review, validLimits(),
|
||||
SingleRequestTemplateBinding{Plan: customPlanTemplate, Review: customReviewTemplate})
|
||||
if err != nil {
|
||||
t.Fatalf("custom template admission failed: %v", err)
|
||||
}
|
||||
clone := b.Clone()
|
||||
if clone.Templates != b.Templates {
|
||||
t.Fatalf("clone templates = %+v, want %+v", clone.Templates, b.Templates)
|
||||
}
|
||||
// Mutating the clone's snapshot must not reach the admitted binding.
|
||||
clone.Templates.Plan = singlerequesttemplate.DefaultPlanTemplate
|
||||
if b.Templates.Plan != customPlanTemplate {
|
||||
t.Errorf("admitted Templates.Plan mutated through the clone: %q", b.Templates.Plan)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("workspace revalidation preserves the frozen pair", func(t *testing.T) {
|
||||
b, err := NewSingleRequestBindingWithTemplates("virtual-model", "ws-ref", plan, work, review, validLimits(),
|
||||
SingleRequestTemplateBinding{Plan: customPlanTemplate, Review: customReviewTemplate})
|
||||
if err != nil {
|
||||
t.Fatalf("custom template admission failed: %v", err)
|
||||
}
|
||||
b.Workspace = &SingleRequestWorkspaceBinding{
|
||||
Ref: "ws-ref",
|
||||
NodeID: "node-1",
|
||||
ConnectionGeneration: 7,
|
||||
OperationIDs: []string{"list", "read"},
|
||||
Limits: SingleRequestWorkspaceLimits{MaxReadBytes: 1024, MaxOutputBytes: 2048},
|
||||
}
|
||||
|
||||
revalidated, err := cloneValidatedSingleRequestBinding(b)
|
||||
if err != nil {
|
||||
t.Fatalf("workspace revalidation failed: %v", err)
|
||||
}
|
||||
if revalidated.Templates.Plan != customPlanTemplate || revalidated.Templates.Review != customReviewTemplate {
|
||||
t.Fatalf("revalidated templates = %+v, want the admitted pair", revalidated.Templates)
|
||||
}
|
||||
if revalidated.Workspace == nil || revalidated.Workspace.ConnectionGeneration != 7 {
|
||||
t.Fatalf("revalidated workspace = %+v, want the frozen generation", revalidated.Workspace)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("invalid effective templates are rejected at admission", func(t *testing.T) {
|
||||
if _, err := NewSingleRequestBindingWithTemplates("virtual-model", "ws-ref", plan, work, review, validLimits(),
|
||||
SingleRequestTemplateBinding{Plan: "### Plan\n\n## Goal\n{{goal}}\n\n## Steps\n{{steps}}\n\n## Verification\n{{verification}}\n", Review: customReviewTemplate}); err == nil {
|
||||
t.Error("expected rejection for a decorated Plan heading")
|
||||
}
|
||||
if _, err := NewSingleRequestBindingWithTemplates("virtual-model", "ws-ref", plan, work, review, validLimits(),
|
||||
SingleRequestTemplateBinding{Plan: customPlanTemplate, Review: strings.Replace(customReviewTemplate, "PASS", "NOTPASS", 1)}); err == nil {
|
||||
t.Error("expected rejection for a NOTPASS Review result line")
|
||||
}
|
||||
if _, err := NewSingleRequestBindingWithTemplates("virtual-model", "ws-ref", plan, work, review, validLimits(),
|
||||
SingleRequestTemplateBinding{Plan: "", Review: ""}); err == nil {
|
||||
t.Error("expected rejection for empty effective templates")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("an invalid template cannot survive workspace revalidation", func(t *testing.T) {
|
||||
b, err := NewSingleRequestBindingWithTemplates("virtual-model", "ws-ref", plan, work, review, validLimits(),
|
||||
SingleRequestTemplateBinding{Plan: customPlanTemplate, Review: customReviewTemplate})
|
||||
if err != nil {
|
||||
t.Fatalf("custom template admission failed: %v", err)
|
||||
}
|
||||
// Simulate post-admission tampering: revalidation must reject it rather
|
||||
// than clone a malformed template forward.
|
||||
b.Templates.Review = strings.Replace(customReviewTemplate, "PASS", "NOTPASS", 1)
|
||||
if _, err := cloneValidatedSingleRequestBinding(b); err == nil {
|
||||
t.Error("expected workspace revalidation to reject a tampered Review template")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestSingleRequestBindingDispatchNilIsAllowed(t *testing.T) {
|
||||
// A stage without dispatch binding is valid (e.g. unmanaged preset fallback).
|
||||
plan := SingleRequestStageBinding{Model: "plan-model"}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import (
|
|||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"iop/packages/go/singlerequesttemplate"
|
||||
|
||||
|
|
@ -718,11 +719,19 @@ func resolveSingleRequestTemplates(presets []ExecutionPreset, configFilePath str
|
|||
return nil
|
||||
}
|
||||
|
||||
func loadTemplateFile(baseDir, relOrAbsPath string) (string, error) {
|
||||
target := strings.TrimSpace(relOrAbsPath)
|
||||
if !filepath.IsAbs(target) {
|
||||
target = filepath.Join(baseDir, target)
|
||||
// loadTemplateFile reads one operator-configured single-request template. The
|
||||
// configured value must be a non-empty path relative to the directory holding
|
||||
// edge.yaml: empty and absolute values are rejected before any filesystem
|
||||
// access, so an absolute path never reaches Lstat or Open.
|
||||
func loadTemplateFile(baseDir, relativePath string) (string, error) {
|
||||
target := strings.TrimSpace(relativePath)
|
||||
if target == "" {
|
||||
return "", fmt.Errorf("template path must not be empty")
|
||||
}
|
||||
if filepath.IsAbs(target) {
|
||||
return "", fmt.Errorf("template path must be relative to the directory containing edge.yaml")
|
||||
}
|
||||
target = filepath.Join(baseDir, target)
|
||||
|
||||
st, err := os.Lstat(target)
|
||||
if err != nil {
|
||||
|
|
@ -745,5 +754,8 @@ func loadTemplateFile(baseDir, relOrAbsPath string) (string, error) {
|
|||
if len(data) > singlerequesttemplate.MaxTemplateBytes {
|
||||
return "", fmt.Errorf("template file %q size %d exceeds max %d bytes", target, len(data), singlerequesttemplate.MaxTemplateBytes)
|
||||
}
|
||||
if !utf8.Valid(data) {
|
||||
return "", fmt.Errorf("template file %q is not valid UTF-8", target)
|
||||
}
|
||||
return string(data), nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import (
|
|||
"testing"
|
||||
|
||||
"iop/packages/go/config"
|
||||
"iop/packages/go/singlerequesttemplate"
|
||||
)
|
||||
|
||||
// TestLoadEdgeModelExecutionPresetOneOf covers the one-of admission rule for
|
||||
|
|
@ -515,6 +516,14 @@ func TestModelCatalogEntry_ValidateVirtualEntryUnit(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
// customPlanTemplate and customReviewTemplate are valid operator-authored
|
||||
// templates used to prove config-relative loading, per-file fallback, and
|
||||
// filesystem-kind boundaries without depending on the built-in defaults.
|
||||
const (
|
||||
customPlanTemplate = "# Plan\n\n## Goal\n{{goal}}\n\n## Steps\n{{steps}}\n\n## Verification\n{{verification}}\n"
|
||||
customReviewTemplate = "# Review\n\n## Result\nPASS\n\n## Checks\n{{checks}}\n\n## Verification\n{{verification}}\n\n## Summary\n{{summary}}\n"
|
||||
)
|
||||
|
||||
func TestLoadEdgeSingleRequestTemplates(t *testing.T) {
|
||||
validPresetYAML := func(planFile, reviewFile string) string {
|
||||
tmplSection := ""
|
||||
|
|
@ -690,4 +699,244 @@ nodes:
|
|||
t.Fatalf("expected error for invalid template content")
|
||||
}
|
||||
})
|
||||
|
||||
// The config contract is relative-only: an absolute plan_file/review_file is
|
||||
// rejected before any filesystem access, so a readable, otherwise valid
|
||||
// template outside the edge.yaml directory must still fail closed.
|
||||
t.Run("absolute plan_file rejected before filesystem access", func(t *testing.T) {
|
||||
rootDir := t.TempDir()
|
||||
outsideDir := t.TempDir()
|
||||
absolutePlan := filepath.Join(outsideDir, "plan.md")
|
||||
if err := os.WriteFile(absolutePlan, []byte(customPlanTemplate), 0o600); err != nil {
|
||||
t.Fatalf("write absolute plan: %v", err)
|
||||
}
|
||||
cfgPath := filepath.Join(rootDir, "edge.yaml")
|
||||
if err := os.WriteFile(cfgPath, []byte(validPresetYAML(absolutePlan, "")), 0o600); err != nil {
|
||||
t.Fatalf("write yaml: %v", err)
|
||||
}
|
||||
|
||||
_, err := config.LoadEdge(cfgPath)
|
||||
if err == nil {
|
||||
t.Fatalf("expected error for absolute plan_file %q", absolutePlan)
|
||||
}
|
||||
if !strings.Contains(err.Error(), "must be relative") {
|
||||
t.Fatalf("expected relative-path rejection, got: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("absolute review_file rejected before filesystem access", func(t *testing.T) {
|
||||
rootDir := t.TempDir()
|
||||
outsideDir := t.TempDir()
|
||||
absoluteReview := filepath.Join(outsideDir, "review.md")
|
||||
if err := os.WriteFile(absoluteReview, []byte(customReviewTemplate), 0o600); err != nil {
|
||||
t.Fatalf("write absolute review: %v", err)
|
||||
}
|
||||
cfgPath := filepath.Join(rootDir, "edge.yaml")
|
||||
if err := os.WriteFile(cfgPath, []byte(validPresetYAML("", absoluteReview)), 0o600); err != nil {
|
||||
t.Fatalf("write yaml: %v", err)
|
||||
}
|
||||
|
||||
_, err := config.LoadEdge(cfgPath)
|
||||
if err == nil {
|
||||
t.Fatalf("expected error for absolute review_file %q", absoluteReview)
|
||||
}
|
||||
if !strings.Contains(err.Error(), "must be relative") {
|
||||
t.Fatalf("expected relative-path rejection, got: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
// plan_file and review_file fall back independently: configuring one file
|
||||
// must not replace the other with a loaded template or drop its default.
|
||||
t.Run("plan_file configured and review_file falls back", func(t *testing.T) {
|
||||
rootDir := t.TempDir()
|
||||
cfgPath := filepath.Join(rootDir, "edge.yaml")
|
||||
if err := os.WriteFile(filepath.Join(rootDir, "plan.md"), []byte(customPlanTemplate), 0o600); err != nil {
|
||||
t.Fatalf("write plan: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(cfgPath, []byte(validPresetYAML("plan.md", "")), 0o600); err != nil {
|
||||
t.Fatalf("write yaml: %v", err)
|
||||
}
|
||||
|
||||
cfg, err := config.LoadEdge(cfgPath)
|
||||
if err != nil {
|
||||
t.Fatalf("LoadEdge failed: %v", err)
|
||||
}
|
||||
sr := cfg.ExecutionPresets[0].SingleRequest
|
||||
if sr.Templates.EffectivePlan != customPlanTemplate {
|
||||
t.Errorf("EffectivePlan = %q, want the configured custom template", sr.Templates.EffectivePlan)
|
||||
}
|
||||
if sr.Templates.EffectiveReview != singlerequesttemplate.DefaultReviewTemplate {
|
||||
t.Errorf("EffectiveReview = %q, want the built-in default", sr.Templates.EffectiveReview)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("review_file configured and plan_file falls back", func(t *testing.T) {
|
||||
rootDir := t.TempDir()
|
||||
cfgPath := filepath.Join(rootDir, "edge.yaml")
|
||||
if err := os.WriteFile(filepath.Join(rootDir, "review.md"), []byte(customReviewTemplate), 0o600); err != nil {
|
||||
t.Fatalf("write review: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(cfgPath, []byte(validPresetYAML("", "review.md")), 0o600); err != nil {
|
||||
t.Fatalf("write yaml: %v", err)
|
||||
}
|
||||
|
||||
cfg, err := config.LoadEdge(cfgPath)
|
||||
if err != nil {
|
||||
t.Fatalf("LoadEdge failed: %v", err)
|
||||
}
|
||||
sr := cfg.ExecutionPresets[0].SingleRequest
|
||||
if sr.Templates.EffectiveReview != customReviewTemplate {
|
||||
t.Errorf("EffectiveReview = %q, want the configured custom template", sr.Templates.EffectiveReview)
|
||||
}
|
||||
if sr.Templates.EffectivePlan != singlerequesttemplate.DefaultPlanTemplate {
|
||||
t.Errorf("EffectivePlan = %q, want the built-in default", sr.Templates.EffectivePlan)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("exact 8192 byte template file accepted", func(t *testing.T) {
|
||||
rootDir := t.TempDir()
|
||||
cfgPath := filepath.Join(rootDir, "edge.yaml")
|
||||
exactPlan := customPlanTemplate + strings.Repeat(" ", singlerequesttemplate.MaxTemplateBytes-len(customPlanTemplate))
|
||||
if len(exactPlan) != singlerequesttemplate.MaxTemplateBytes {
|
||||
t.Fatalf("fixture size = %d, want %d", len(exactPlan), singlerequesttemplate.MaxTemplateBytes)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(rootDir, "exact.md"), []byte(exactPlan), 0o600); err != nil {
|
||||
t.Fatalf("write exact plan: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(cfgPath, []byte(validPresetYAML("exact.md", "")), 0o600); err != nil {
|
||||
t.Fatalf("write yaml: %v", err)
|
||||
}
|
||||
|
||||
cfg, err := config.LoadEdge(cfgPath)
|
||||
if err != nil {
|
||||
t.Fatalf("LoadEdge failed at the exact size boundary: %v", err)
|
||||
}
|
||||
if cfg.ExecutionPresets[0].SingleRequest.Templates.EffectivePlan != exactPlan {
|
||||
t.Errorf("EffectivePlan did not match the exact-size fixture")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("symlinked template file fails closed", func(t *testing.T) {
|
||||
rootDir := t.TempDir()
|
||||
cfgPath := filepath.Join(rootDir, "edge.yaml")
|
||||
realPlan := filepath.Join(rootDir, "real_plan.md")
|
||||
if err := os.WriteFile(realPlan, []byte(customPlanTemplate), 0o600); err != nil {
|
||||
t.Fatalf("write real plan: %v", err)
|
||||
}
|
||||
if err := os.Symlink(realPlan, filepath.Join(rootDir, "link_plan.md")); err != nil {
|
||||
t.Skipf("symlink unsupported in this environment: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(cfgPath, []byte(validPresetYAML("link_plan.md", "")), 0o600); err != nil {
|
||||
t.Fatalf("write yaml: %v", err)
|
||||
}
|
||||
|
||||
_, err := config.LoadEdge(cfgPath)
|
||||
if err == nil {
|
||||
t.Fatalf("expected error for symlinked template file")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "regular file") {
|
||||
t.Fatalf("expected non-regular-file rejection, got: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("directory template path fails closed", func(t *testing.T) {
|
||||
rootDir := t.TempDir()
|
||||
cfgPath := filepath.Join(rootDir, "edge.yaml")
|
||||
if err := os.MkdirAll(filepath.Join(rootDir, "plan_dir"), 0o755); err != nil {
|
||||
t.Fatalf("mkdir: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(cfgPath, []byte(validPresetYAML("plan_dir", "")), 0o600); err != nil {
|
||||
t.Fatalf("write yaml: %v", err)
|
||||
}
|
||||
|
||||
_, err := config.LoadEdge(cfgPath)
|
||||
if err == nil {
|
||||
t.Fatalf("expected error for directory template path")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "regular file") {
|
||||
t.Fatalf("expected non-regular-file rejection, got: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("unreadable template file fails closed", func(t *testing.T) {
|
||||
if os.Geteuid() == 0 {
|
||||
t.Skip("running as root: mode bits do not deny read access")
|
||||
}
|
||||
rootDir := t.TempDir()
|
||||
cfgPath := filepath.Join(rootDir, "edge.yaml")
|
||||
unreadable := filepath.Join(rootDir, "unreadable.md")
|
||||
if err := os.WriteFile(unreadable, []byte(customPlanTemplate), 0o600); err != nil {
|
||||
t.Fatalf("write unreadable plan: %v", err)
|
||||
}
|
||||
if err := os.Chmod(unreadable, 0o000); err != nil {
|
||||
t.Fatalf("chmod: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = os.Chmod(unreadable, 0o600) })
|
||||
if err := os.WriteFile(cfgPath, []byte(validPresetYAML("unreadable.md", "")), 0o600); err != nil {
|
||||
t.Fatalf("write yaml: %v", err)
|
||||
}
|
||||
|
||||
if _, err := config.LoadEdge(cfgPath); err == nil {
|
||||
t.Fatalf("expected error for unreadable template file")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("non-UTF-8 template file fails closed", func(t *testing.T) {
|
||||
rootDir := t.TempDir()
|
||||
cfgPath := filepath.Join(rootDir, "edge.yaml")
|
||||
invalid := append([]byte(customPlanTemplate), 0xff, 0xfe)
|
||||
if err := os.WriteFile(filepath.Join(rootDir, "invalid_utf8.md"), invalid, 0o600); err != nil {
|
||||
t.Fatalf("write invalid utf8 plan: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(cfgPath, []byte(validPresetYAML("invalid_utf8.md", "")), 0o600); err != nil {
|
||||
t.Fatalf("write yaml: %v", err)
|
||||
}
|
||||
|
||||
_, err := config.LoadEdge(cfgPath)
|
||||
if err == nil {
|
||||
t.Fatalf("expected error for non-UTF-8 template file")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "UTF-8") {
|
||||
t.Fatalf("expected UTF-8 rejection, got: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
// Invalid grammar must fail closed for both files, not only plan_file.
|
||||
t.Run("invalid grammar matrix fails closed", func(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
planFile string
|
||||
reviewFile string
|
||||
content string
|
||||
}{
|
||||
{"plan missing heading", "bad.md", "", "## Goal\n{{goal}}\n\n## Steps\n{{steps}}\n\n## Verification\n{{verification}}\n"},
|
||||
{"plan decorated heading", "bad.md", "", "### Plan\n\n## Goal\n{{goal}}\n\n## Steps\n{{steps}}\n\n## Verification\n{{verification}}\n"},
|
||||
{"plan unknown placeholder", "bad.md", "", "# Plan\n\n## Goal\n{{goal}} {{owner}}\n\n## Steps\n{{steps}}\n\n## Verification\n{{verification}}\n"},
|
||||
{"plan delimiter residue", "bad.md", "", "# Plan\n\n## Goal\n{{goal}}}}\n\n## Steps\n{{steps}}\n\n## Verification\n{{verification}}\n"},
|
||||
{"plan duplicate placeholder", "bad.md", "", "# Plan\n\n## Goal\n{{goal}} {{goal}}\n\n## Steps\n{{steps}}\n\n## Verification\n{{verification}}\n"},
|
||||
{"review missing PASS", "", "bad.md", "# Review\n\n## Result\nFAIL\n\n## Checks\n{{checks}}\n\n## Verification\n{{verification}}\n\n## Summary\n{{summary}}\n"},
|
||||
{"review NOTPASS", "", "bad.md", "# Review\n\n## Result\nNOTPASS\n\n## Checks\n{{checks}}\n\n## Verification\n{{verification}}\n\n## Summary\n{{summary}}\n"},
|
||||
{"review decorated heading", "", "bad.md", "### Review\n\n## Result\nPASS\n\n## Checks\n{{checks}}\n\n## Verification\n{{verification}}\n\n## Summary\n{{summary}}\n"},
|
||||
{"review missing placeholder", "", "bad.md", "# Review\n\n## Result\nPASS\n\n## Checks\n{{checks}}\n\n## Verification\n{{verification}}\n\n## Summary\n"},
|
||||
{"review unknown placeholder", "", "bad.md", "# Review\n\n## Result\nPASS\n\n## Checks\n{{checks}} {{severity}}\n\n## Verification\n{{verification}}\n\n## Summary\n{{summary}}\n"},
|
||||
{"review wrong order", "", "bad.md", "# Review\n\n## Result\nPASS\n\n## Verification\n{{verification}}\n\n## Checks\n{{checks}}\n\n## Summary\n{{summary}}\n"},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
rootDir := t.TempDir()
|
||||
cfgPath := filepath.Join(rootDir, "edge.yaml")
|
||||
if err := os.WriteFile(filepath.Join(rootDir, "bad.md"), []byte(tc.content), 0o600); err != nil {
|
||||
t.Fatalf("write template: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(cfgPath, []byte(validPresetYAML(tc.planFile, tc.reviewFile)), 0o600); err != nil {
|
||||
t.Fatalf("write yaml: %v", err)
|
||||
}
|
||||
|
||||
if _, err := config.LoadEdge(cfgPath); err == nil {
|
||||
t.Fatalf("expected error for %s", tc.name)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,12 +47,95 @@ var (
|
|||
|
||||
var placeholderRegex = regexp.MustCompile(`\{\{[^}]*\}\}`)
|
||||
|
||||
var (
|
||||
planPlaceholders = []string{"{{goal}}", "{{steps}}", "{{verification}}"}
|
||||
planHeadings = []string{"# Plan", "## Goal", "## Steps", "## Verification"}
|
||||
reviewPlaceholders = []string{"{{checks}}", "{{verification}}", "{{summary}}"}
|
||||
reviewLines = []string{"# Review", "## Result", "PASS", "## Checks", "## Verification", "## Summary"}
|
||||
)
|
||||
|
||||
type ReviewFields struct {
|
||||
Checks string
|
||||
Verification string
|
||||
Summary string
|
||||
}
|
||||
|
||||
// exactLineOffsets returns the byte offset of the first standalone line equal to
|
||||
// want and how many standalone lines matched. A standalone line is a maximal
|
||||
// "\n"-delimited segment compared byte for byte, so decorated variants such as
|
||||
// "### Plan", "# Plan Mismatch", or "NOTPASS" never satisfy a required line.
|
||||
func exactLineOffsets(tmpl, want string) (int, int) {
|
||||
index, count, offset := -1, 0, 0
|
||||
for {
|
||||
var line string
|
||||
end := strings.IndexByte(tmpl[offset:], '\n')
|
||||
if end < 0 {
|
||||
line = tmpl[offset:]
|
||||
} else {
|
||||
line = tmpl[offset : offset+end]
|
||||
}
|
||||
if line == want {
|
||||
count++
|
||||
if index < 0 {
|
||||
index = offset
|
||||
}
|
||||
}
|
||||
if end < 0 {
|
||||
return index, count
|
||||
}
|
||||
offset += end + 1
|
||||
}
|
||||
}
|
||||
|
||||
// requireExactLines resolves every required standalone line, rejecting missing
|
||||
// and duplicated occurrences, and returns their offsets in the requested order.
|
||||
// Only the documented required line is echoed on failure; template content is
|
||||
// never included in the error.
|
||||
func requireExactLines(tmpl string, lines []string) ([]int, error) {
|
||||
offsets := make([]int, len(lines))
|
||||
for i, line := range lines {
|
||||
index, count := exactLineOffsets(tmpl, line)
|
||||
if count != 1 {
|
||||
return nil, fmt.Errorf("%w: must contain the standalone line %q exactly once", ErrInvalidTemplate, line)
|
||||
}
|
||||
offsets[i] = index
|
||||
}
|
||||
return offsets, nil
|
||||
}
|
||||
|
||||
// requirePlaceholderInventory closes the placeholder grammar. Each documented
|
||||
// placeholder must appear exactly once; after removing those exact occurrences
|
||||
// no template delimiter may survive, so unknown placeholders and unbalanced
|
||||
// "{{"/"}}" residue are both rejected. Only documented placeholder names are
|
||||
// echoed on failure; template content is never included in the error.
|
||||
func requirePlaceholderInventory(tmpl string, placeholders []string) ([]int, error) {
|
||||
offsets := make([]int, len(placeholders))
|
||||
residue := tmpl
|
||||
for i, placeholder := range placeholders {
|
||||
if strings.Count(tmpl, placeholder) != 1 {
|
||||
return nil, fmt.Errorf("%w: must contain %s exactly once", ErrInvalidTemplate, placeholder)
|
||||
}
|
||||
offsets[i] = strings.Index(tmpl, placeholder)
|
||||
residue = strings.Replace(residue, placeholder, "", 1)
|
||||
}
|
||||
if placeholderRegex.MatchString(residue) {
|
||||
return nil, fmt.Errorf("%w: template declares an unknown placeholder", ErrInvalidTemplate)
|
||||
}
|
||||
if strings.Contains(residue, "{{") || strings.Contains(residue, "}}") {
|
||||
return nil, fmt.Errorf("%w: template leaves an unbalanced placeholder delimiter", ErrInvalidTemplate)
|
||||
}
|
||||
return offsets, nil
|
||||
}
|
||||
|
||||
func ascending(values ...int) bool {
|
||||
for i := 1; i < len(values); i++ {
|
||||
if values[i-1] >= values[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func Digest(content string) string {
|
||||
h := sha256.Sum256([]byte(content))
|
||||
return hex.EncodeToString(h[:])
|
||||
|
|
@ -66,39 +149,21 @@ func ValidatePlanTemplate(tmpl string) error {
|
|||
return fmt.Errorf("%w: template size %d exceeds max %d", ErrTemplateTooLarge, len(tmpl), MaxTemplateBytes)
|
||||
}
|
||||
|
||||
if strings.Count(tmpl, "{{goal}}") != 1 {
|
||||
return fmt.Errorf("%w: must contain {{goal}} exactly once", ErrInvalidTemplate)
|
||||
placeholders, err := requirePlaceholderInventory(tmpl, planPlaceholders)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if strings.Count(tmpl, "{{steps}}") != 1 {
|
||||
return fmt.Errorf("%w: must contain {{steps}} exactly once", ErrInvalidTemplate)
|
||||
}
|
||||
if strings.Count(tmpl, "{{verification}}") != 1 {
|
||||
return fmt.Errorf("%w: must contain {{verification}} exactly once", ErrInvalidTemplate)
|
||||
}
|
||||
|
||||
matches := placeholderRegex.FindAllString(tmpl, -1)
|
||||
for _, m := range matches {
|
||||
if m != "{{goal}}" && m != "{{steps}}" && m != "{{verification}}" {
|
||||
return fmt.Errorf("%w: unknown placeholder %q", ErrInvalidTemplate, m)
|
||||
}
|
||||
}
|
||||
|
||||
idxGoal := strings.Index(tmpl, "{{goal}}")
|
||||
idxSteps := strings.Index(tmpl, "{{steps}}")
|
||||
idxVerif := strings.Index(tmpl, "{{verification}}")
|
||||
if !(idxGoal < idxSteps && idxSteps < idxVerif) {
|
||||
idxGoal, idxSteps, idxVerif := placeholders[0], placeholders[1], placeholders[2]
|
||||
if !ascending(idxGoal, idxSteps, idxVerif) {
|
||||
return fmt.Errorf("%w: placeholders must appear in order {{goal}}, {{steps}}, {{verification}}", ErrInvalidTemplate)
|
||||
}
|
||||
|
||||
idxPlanH := strings.Index(tmpl, "# Plan")
|
||||
idxGoalH := strings.Index(tmpl, "## Goal")
|
||||
idxStepsH := strings.Index(tmpl, "## Steps")
|
||||
idxVerifH := strings.Index(tmpl, "## Verification")
|
||||
|
||||
if idxPlanH < 0 || idxGoalH < 0 || idxStepsH < 0 || idxVerifH < 0 {
|
||||
return fmt.Errorf("%w: missing required headings (# Plan, ## Goal, ## Steps, ## Verification)", ErrInvalidTemplate)
|
||||
headings, err := requireExactLines(tmpl, planHeadings)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !(idxPlanH < idxGoalH && idxGoalH < idxGoal && idxGoal < idxStepsH && idxStepsH < idxSteps && idxSteps < idxVerifH && idxVerifH < idxVerif) {
|
||||
idxPlanH, idxGoalH, idxStepsH, idxVerifH := headings[0], headings[1], headings[2], headings[3]
|
||||
if !ascending(idxPlanH, idxGoalH, idxGoal, idxStepsH, idxSteps, idxVerifH, idxVerif) {
|
||||
return fmt.Errorf("%w: headings and placeholders must follow exact structural order", ErrInvalidTemplate)
|
||||
}
|
||||
|
||||
|
|
@ -113,41 +178,22 @@ func ValidateReviewTemplate(tmpl string) error {
|
|||
return fmt.Errorf("%w: template size %d exceeds max %d", ErrTemplateTooLarge, len(tmpl), MaxTemplateBytes)
|
||||
}
|
||||
|
||||
if strings.Count(tmpl, "{{checks}}") != 1 {
|
||||
return fmt.Errorf("%w: must contain {{checks}} exactly once", ErrInvalidTemplate)
|
||||
placeholders, err := requirePlaceholderInventory(tmpl, reviewPlaceholders)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if strings.Count(tmpl, "{{verification}}") != 1 {
|
||||
return fmt.Errorf("%w: must contain {{verification}} exactly once", ErrInvalidTemplate)
|
||||
}
|
||||
if strings.Count(tmpl, "{{summary}}") != 1 {
|
||||
return fmt.Errorf("%w: must contain {{summary}} exactly once", ErrInvalidTemplate)
|
||||
}
|
||||
|
||||
matches := placeholderRegex.FindAllString(tmpl, -1)
|
||||
for _, m := range matches {
|
||||
if m != "{{checks}}" && m != "{{verification}}" && m != "{{summary}}" {
|
||||
return fmt.Errorf("%w: unknown placeholder %q", ErrInvalidTemplate, m)
|
||||
}
|
||||
}
|
||||
|
||||
idxChecks := strings.Index(tmpl, "{{checks}}")
|
||||
idxVerif := strings.Index(tmpl, "{{verification}}")
|
||||
idxSumm := strings.Index(tmpl, "{{summary}}")
|
||||
if !(idxChecks < idxVerif && idxVerif < idxSumm) {
|
||||
idxChecks, idxVerif, idxSumm := placeholders[0], placeholders[1], placeholders[2]
|
||||
if !ascending(idxChecks, idxVerif, idxSumm) {
|
||||
return fmt.Errorf("%w: placeholders must appear in order {{checks}}, {{verification}}, {{summary}}", ErrInvalidTemplate)
|
||||
}
|
||||
|
||||
idxReviewH := strings.Index(tmpl, "# Review")
|
||||
idxResultH := strings.Index(tmpl, "## Result")
|
||||
idxPass := strings.Index(tmpl, "PASS")
|
||||
idxChecksH := strings.Index(tmpl, "## Checks")
|
||||
idxVerifH := strings.Index(tmpl, "## Verification")
|
||||
idxSummH := strings.Index(tmpl, "## Summary")
|
||||
|
||||
if idxReviewH < 0 || idxResultH < 0 || idxPass < 0 || idxChecksH < 0 || idxVerifH < 0 || idxSummH < 0 {
|
||||
return fmt.Errorf("%w: missing required headings or PASS result", ErrInvalidTemplate)
|
||||
lines, err := requireExactLines(tmpl, reviewLines)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !(idxReviewH < idxResultH && idxResultH < idxPass && idxPass < idxChecksH && idxChecksH < idxChecks && idxChecks < idxVerifH && idxVerifH < idxVerif && idxVerif < idxSummH && idxSummH < idxSumm) {
|
||||
idxReviewH, idxResultH, idxPass := lines[0], lines[1], lines[2]
|
||||
idxChecksH, idxVerifH, idxSummH := lines[3], lines[4], lines[5]
|
||||
if !ascending(idxReviewH, idxResultH, idxPass, idxChecksH, idxChecks, idxVerifH, idxVerif, idxSummH, idxSumm) {
|
||||
return fmt.Errorf("%w: headings and placeholders must follow exact structural order", ErrInvalidTemplate)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,18 @@ import (
|
|||
"iop/packages/go/singlerequesttemplate"
|
||||
)
|
||||
|
||||
// planTemplateOfSize pads the built-in Plan template with trailing static text
|
||||
// so the returned template is exactly size bytes long.
|
||||
func planTemplateOfSize(size int) string {
|
||||
return singlerequesttemplate.DefaultPlanTemplate + strings.Repeat(" ", size-len(singlerequesttemplate.DefaultPlanTemplate))
|
||||
}
|
||||
|
||||
// reviewTemplateOfSize pads the built-in Review template with trailing static
|
||||
// text so the returned template is exactly size bytes long.
|
||||
func reviewTemplateOfSize(size int) string {
|
||||
return singlerequesttemplate.DefaultReviewTemplate + strings.Repeat(" ", size-len(singlerequesttemplate.DefaultReviewTemplate))
|
||||
}
|
||||
|
||||
func TestValidatePlanTemplate(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
|
@ -114,6 +126,118 @@ Footer notes.
|
|||
## Steps
|
||||
{{steps}}
|
||||
|
||||
## Verification
|
||||
{{verification}}
|
||||
`,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "exact 8192 bytes accepted",
|
||||
tmpl: planTemplateOfSize(8192),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "decorated heading ### Plan",
|
||||
tmpl: `### Plan
|
||||
|
||||
## Goal
|
||||
{{goal}}
|
||||
|
||||
## Steps
|
||||
{{steps}}
|
||||
|
||||
## Verification
|
||||
{{verification}}
|
||||
`,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "heading line carries trailing text",
|
||||
tmpl: `# Plan Mismatch
|
||||
|
||||
## Goal
|
||||
{{goal}}
|
||||
|
||||
## Steps
|
||||
{{steps}}
|
||||
|
||||
## Verification
|
||||
{{verification}}
|
||||
`,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "heading embedded inside a prose line",
|
||||
tmpl: `# Plan
|
||||
|
||||
Documented as ## Goal below.
|
||||
{{goal}}
|
||||
|
||||
## Steps
|
||||
{{steps}}
|
||||
|
||||
## Verification
|
||||
{{verification}}
|
||||
`,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "duplicate required heading",
|
||||
tmpl: `# Plan
|
||||
|
||||
## Goal
|
||||
{{goal}}
|
||||
|
||||
## Steps
|
||||
{{steps}}
|
||||
|
||||
## Verification
|
||||
{{verification}}
|
||||
|
||||
## Goal
|
||||
`,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "unbalanced opening delimiter residue",
|
||||
tmpl: `# Plan
|
||||
|
||||
## Goal
|
||||
{{goal}} {{
|
||||
|
||||
## Steps
|
||||
{{steps}}
|
||||
|
||||
## Verification
|
||||
{{verification}}
|
||||
`,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "unbalanced closing delimiter residue",
|
||||
tmpl: `# Plan
|
||||
|
||||
## Goal
|
||||
{{goal}}}}
|
||||
|
||||
## Steps
|
||||
{{steps}}
|
||||
|
||||
## Verification
|
||||
{{verification}}
|
||||
`,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "nested delimiter around a documented placeholder",
|
||||
tmpl: `# Plan
|
||||
|
||||
## Goal
|
||||
{{{{goal}}}}
|
||||
|
||||
## Steps
|
||||
{{steps}}
|
||||
|
||||
## Verification
|
||||
{{verification}}
|
||||
`,
|
||||
|
|
@ -398,6 +522,182 @@ PASS
|
|||
`,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "oversized template 8193 bytes",
|
||||
tmpl: reviewTemplateOfSize(8193),
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "exact 8192 bytes accepted",
|
||||
tmpl: reviewTemplateOfSize(8192),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "NOTPASS does not satisfy the PASS result line",
|
||||
tmpl: `# Review
|
||||
|
||||
## Result
|
||||
NOTPASS
|
||||
|
||||
## Checks
|
||||
{{checks}}
|
||||
|
||||
## Verification
|
||||
{{verification}}
|
||||
|
||||
## Summary
|
||||
{{summary}}
|
||||
`,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "PASS embedded in a prose line",
|
||||
tmpl: `# Review
|
||||
|
||||
## Result
|
||||
Result: PASS
|
||||
|
||||
## Checks
|
||||
{{checks}}
|
||||
|
||||
## Verification
|
||||
{{verification}}
|
||||
|
||||
## Summary
|
||||
{{summary}}
|
||||
`,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "decorated heading ### Review",
|
||||
tmpl: `### Review
|
||||
|
||||
## Result
|
||||
PASS
|
||||
|
||||
## Checks
|
||||
{{checks}}
|
||||
|
||||
## Verification
|
||||
{{verification}}
|
||||
|
||||
## Summary
|
||||
{{summary}}
|
||||
`,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "duplicate PASS result line",
|
||||
tmpl: `# Review
|
||||
|
||||
## Result
|
||||
PASS
|
||||
PASS
|
||||
|
||||
## Checks
|
||||
{{checks}}
|
||||
|
||||
## Verification
|
||||
{{verification}}
|
||||
|
||||
## Summary
|
||||
{{summary}}
|
||||
`,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "missing {{summary}}",
|
||||
tmpl: `# Review
|
||||
|
||||
## Result
|
||||
PASS
|
||||
|
||||
## Checks
|
||||
{{checks}}
|
||||
|
||||
## Verification
|
||||
{{verification}}
|
||||
|
||||
## Summary
|
||||
`,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "duplicate {{checks}}",
|
||||
tmpl: `# Review
|
||||
|
||||
## Result
|
||||
PASS
|
||||
|
||||
## Checks
|
||||
{{checks}} {{checks}}
|
||||
|
||||
## Verification
|
||||
{{verification}}
|
||||
|
||||
## Summary
|
||||
{{summary}}
|
||||
`,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "unknown placeholder",
|
||||
tmpl: `# Review
|
||||
|
||||
## Result
|
||||
PASS
|
||||
|
||||
## Checks
|
||||
{{checks}} {{severity}}
|
||||
|
||||
## Verification
|
||||
{{verification}}
|
||||
|
||||
## Summary
|
||||
{{summary}}
|
||||
`,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "unbalanced delimiter residue",
|
||||
tmpl: `# Review
|
||||
|
||||
## Result
|
||||
PASS
|
||||
|
||||
## Checks
|
||||
{{checks}}
|
||||
|
||||
## Verification
|
||||
{{verification}}
|
||||
|
||||
## Summary
|
||||
{{summary}} }}
|
||||
`,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "custom review template with extra static text",
|
||||
tmpl: `# Review
|
||||
|
||||
Operator preamble.
|
||||
|
||||
## Result
|
||||
PASS
|
||||
|
||||
## Checks
|
||||
{{checks}}
|
||||
|
||||
## Verification
|
||||
{{verification}}
|
||||
|
||||
## Summary
|
||||
{{summary}}
|
||||
|
||||
Operator footer.
|
||||
`,
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
|
|
|||
Loading…
Reference in a new issue