iop/agent-spec/runtime/edge-node-execution.md

345 lines
48 KiB
Markdown

---
spec_doc_type: spec
spec_id: runtime/edge-node-execution
status: 구현됨
source_evidence:
- type: contract
path: agent-contract/inner/execution-runtime.md
notes: Host-neutral provider execution primitives
- type: contract
path: agent-contract/inner/edge-node-runtime-wire.md
notes: Edge-Node registration, execution, tunnel, request-owned workspace artifact, cancellation, command, and refresh wire
- type: code
path: packages/go/execution/types.go
notes: Provider execution and event types
- type: code
path: apps/node/internal/node/runtime_bridge.go
notes: Protobuf-to-execution translation and ordered runtime event projection
- type: code
path: apps/edge/internal/transport/server.go
notes: Edge-side tunnel-tolerant heartbeat and disconnect supervision
- type: code
path: apps/node/internal/transport/client.go
notes: Node-side tunnel-tolerant heartbeat and reconnect transport
- type: code
path: apps/edge/internal/service/provider_tunnel.go
notes: Provider selection, credential binding validation, lease acquisition, terminal handling, and pre-send fencing
- type: code
path: apps/edge/internal/service/model_queue_release.go
notes: Immutable lease validation and exactly-once release for provider and transport terminals
- type: code
path: apps/edge/internal/service/node_command.go
notes: Closed Node command dispatch and bounded result handling
- type: code
path: apps/node/internal/node/tunnel_handler.go
notes: Provider tunnel handling and recipient-sealed credential lease consumption
- type: code
path: apps/node/internal/transport/session.go
notes: Connection-scoped request handlers and cancellation ownership
- type: code
path: packages/go/credentiallease/envelope.go
notes: Signed scope validation, recipient sealing, expiry, replay, and exact binding verification
- type: test
path: apps/node/internal/node/command_test.go
notes: Closed provider command and bounded result regressions
- type: test
path: apps/edge/internal/transport/heartbeat_test.go
notes: Edge heartbeat liveness profile regression
- type: test
path: apps/node/internal/transport/heartbeat_test.go
notes: Node heartbeat liveness and idle-connection regressions
- type: test
path: apps/node/internal/node/provider_tunnel_test.go
notes: Credential preflight admission release regression
- type: test
path: apps/node/internal/transport/session_test.go
notes: Run and tunnel handler lifetime cancellation on disconnect
- type: code
path: apps/edge/internal/service/single_request_workspace.go
notes: Exact configured workspace owner and ready-generation admission projection
- type: code
path: apps/edge/internal/service/workspace_wire.go
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 Plan runner with bounded read/list-first workspace inspection, strict structured field response, deterministic frozen-template rendering, and PLAN artifact write
- type: test
path: apps/edge/internal/openai/single_request_plan_stage_test.go
notes: Deterministic read/list-first Plan analysis, 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, deterministic Plan rendering, legacy PlanMD parsing, and Review rendering
- type: test
path: packages/go/singlerequesttemplate/template_test.go
notes: Grammar, size-boundary, delimiter-residue, and deterministic Plan/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
- type: test
path: apps/edge/internal/openai/single_request_work_stage_test.go
notes: Deterministic Work tool continuation, correlation, high-option absence, and bounded completion evidence
- type: code
path: apps/edge/internal/service/single_request_tool_types.go
notes: Closed internal workspace schemas, strict decoding, defensive copies, and raw-free typed result projection
- type: code
path: apps/edge/internal/service/single_request_tool_loop.go
notes: Request-local ordered tool continuation, saved-stage correlation, immutable budgets, and cancellation ownership
- type: code
path: apps/edge/internal/service/single_request.go
notes: Validated closed terminal disposition, frozen terminal ownership, cleanup conversion, and acknowledgement stability
- type: code
path: apps/edge/internal/openai/single_request_quality_gate.go
notes: Request-local S11 stage classification and fixed-hash repetition/no-progress detection
- type: test
path: apps/edge/internal/openai/single_request_quality_gate_test.go
notes: S11 provider, timeout, budget, malformed, context, length, cancel, tool, and no-progress terminal evidence
- type: test
path: apps/edge/internal/openai/single_request_handler_test.go
notes: Buffered Anthropic error/cancel/length mapping, one-ingress evidence, and private-partial exclusion
- type: test
path: apps/edge/internal/openai/single_request_anthropic_stream_test.go
notes: Streaming terminal-disposition mapping, exactly-one terminal, disconnect silence, and private-partial exclusion
- type: code
path: apps/edge/internal/service/single_request_artifact.go
notes: Closed PLAN/REVIEW controller API, shared lazy workspace open, bounded artifact operations, and in-flight cleanup ownership
- type: test
path: apps/edge/internal/service/single_request_tool_loop_test.go
notes: Ordered multi-tool wire evidence plus identity, capability, stale result, budget, deadline, and cancel failures
- type: test
path: apps/edge/internal/service/single_request_artifact_test.go
notes: Artifact-first open sharing, tool-after-artifact reuse, terminal/cancel wait, exactly-once cleanup, and pre-dispatch bounds
- type: code
path: apps/node/internal/transport/session.go
notes: Optional workspace handler registration that preserves legacy provider Handler compatibility
- type: code
path: apps/node/internal/workspace/runtime.go
notes: Closed Darwin/Linux host-exact immutable catalog, opened root authority, operation-aware limits, immutable request-authority copy, and lifecycle ownership
- type: test
path: apps/node/internal/workspace/runtime_test.go
notes: Darwin/Linux positive admission, exact cross-platform mismatch, unsupported-host, empty-catalog compatibility, root identity, and redaction regressions
- type: code
path: apps/node/internal/workspace/file_executor.go
notes: Capability-gated bounded batch listing, descriptor-relative structured write, and non-recursive delete
- type: test
path: apps/node/internal/workspace/file_executor_test.go
notes: Reserved namespace, no-effect symlink/parent/device rejection, bounded listing, atomicity, special-file, and concurrency regressions
- type: code
path: apps/node/internal/workspace/command_executor.go
notes: Exact command-template lookup, minimal allowlisted environment, shared output cap, active-command identity, and terminal result ownership
- type: code
path: apps/node/internal/workspace/command_process_unix.go
notes: Darwin/Linux inherited-root fchdir/exec shim and process-group termination
- type: code
path: apps/node/internal/workspace/cleanup.go
notes: Exactly-once request cleanup ownership, process cancellation and wait, bounded result cache, and locked internal artifact inventory read/write
- type: code
path: apps/node/internal/workspace/cleanup_path_unix.go
notes: No-follow request namespace creation, descriptor enumeration, inventoried file reads, identity validation, and deepest-first non-recursive removal
- type: code
path: apps/node/internal/node/workspace_handler.go
notes: Closed artifact selector mapping plus stable typed open/tool/artifact/cancel/cleanup terminals
- type: test
path: apps/node/internal/workspace/cleanup_test.go
notes: Cleanup races, process groups, timeout, artifact read/write isolation, unsafe entry refusal, identity and device mismatch, user result preservation, and request isolation
- type: test
path: apps/node/internal/workspace/command_executor_test.go
notes: Success, non-zero exit, timeout, context/explicit cancel, child process group, shared output, environment, request isolation, and renamed-root identity evidence
- type: test
path: apps/node/internal/node/workspace_handler_test.go
notes: Typed command/cancel and plan/review artifact mapping, duplicate cancel, not-found, and raw-free stable error evidence
- type: test
path: apps/edge/internal/service/single_request_workspace_test.go
notes: Workspace admission rejection, effective-limit, refresh, and generation-fence regressions
- type: test
path: apps/edge/internal/service/workspace_wire_test.go
notes: Frozen open authority, typed workspace and artifact round trips, malformed response rejection, bounds, cancellation, and stale-generation no-reselection regressions
- type: test
path: apps/edge/internal/service/single_request_cleanup_test.go
notes: Cleanup-before-terminal ordering, success failure conversion, cancellation category preservation, write failure, unopened workspace, and exactly-once terminal races
- type: test
path: apps/node/internal/bootstrap/workspace_runtime_test.go
notes: Path-free startup failure, handler-before-ready composition, and registry/workspace/session/store close-order regressions
- type: complete-log
path: agent-task/archive/2026/08/m-iop-owned-single-request-agent-execution/25+23,24_claude_smoke_qualification/complete.log
notes: S12 external Claude qualification with one ingress, Gemini to ornith-fast to Gemini stage order, exact workspace result, one end_turn, timing, cleanup, and redacted evidence
---
# Edge-Node Provider Execution
## 목적
Edge owns provider selection, queue admission, leases, and connection-generation fencing. Node owns local provider adapters and executes normalized runs or provider HTTP tunnels after a ready handshake.
The shared `packages/go/execution` package contains provider lifecycle, registry, execution events, failures, cancellation, usage, and optional tunnel/command primitives. It does not manage host programs or durable conversation state.
## 기능 목록
| 기능 | 설명 |
|------|------|
| register/readiness | 등록된 Node의 현재 connection이 readiness를 완료한 뒤에만 dispatch한다. |
| normalized execution | `adapter + target`으로 provider 실행을 선택하고 ordered `RunEvent` stream을 반환한다. |
| single-request coordinator | Immutable admission과 closed stage envelope을 service-owned state graph (`accepted`, `planning`, `working`, `reviewing`, `repairing`, `internal_tool`, `finalizing`, `completed`, `failed`, `cancelled`)로 처리한다. An internal tool result can resume only its saved stage. After a successful workspace open, every terminal path waits for one cleanup before the finalizing candidate can reach surface acknowledgement. |
| single-request S11 terminal policy | One validated, copy-safe terminal disposition is frozen across envelope/result/progress with kinds `end_turn`, `length`, `error`, and `cancelled`. Error classes are `provider`, `validation`, `timeout`, `budget`, `repetition`, `malformed`, `context`, `internal_tool`, and `workspace_cleanup`. Cleanup can replace a pending success/length before publication; no acknowledgement race can publish a second terminal. |
| single-request observation evidence | Stage-pure timing, tool/cleanup/total counts, cardinality-bounded labels, Node logs, and raw-free correlation are documented for the single-request path. `iop_anthropic_single_request_ingress_total` is strictly unlabeled. A separate `edge_single_request_terminal_rejection` event projects only the fixed terminal kind/error class and HTTP status, so `malformed` and `validation` remain distinguishable without raw model output. The provider-normalization layer accepts and discards bounded standard/provider Chat Completions bookkeeping (`usage`, `service_tier`, `system_fingerprint`, provider `timings`, choice `logprobs`, message `annotations`, null `refusal`) plus optional string `message.reasoning_content` before emitting the canonical private stage envelope; none enters stage results or artifacts, while a non-null refusal, non-string reasoning, and unknown envelope members fail closed. Gemini Plan/Review also admit only exact non-empty `extra_content.google.thought_signature`; terminal signatures are discarded and a Review tool-call signature is replayed only in the matching request-local Gemini continuation. Work, artifacts, results, and observations never retain it. The Claude qualification child disables SDK retry and session-title generation so only the actual task can consume ingress. Internal tool names, raw arguments, and private results are absent from public output and log projections. SDD S12 qualified the external Claude path on an approved IOP Node with ingress `0→1`, Gemini → ornith-fast → Gemini stage order, one `end_turn`, the exact expected workspace result, timing, cleanup, and redacted evidence. |
| workspace admission | An opaque `workspace_ref` resolves only through the configured Node catalog. Edge freezes the exact configured owner, dispatch-ready connection generation, closed operation/command/environment-name capabilities, and effective limits before executor startup; unavailable, foreign, pending, malformed, and stale candidates fail closed without fallback or reselection. |
| workspace runtime wire | The dedicated `WorkspaceOpen`/`Tool`/`Artifact`/`Cancel`/`Cleanup` request-response families carry immutable coordinator identities and closed status/error codes. `WorkspaceArtifact` admits only enum-selected `PLAN`/`REVIEW` and `READ`/`WRITE`; it carries no relative path. Edge overwrites open capabilities with frozen request authority; Node copies only catalog-subset operations/command ids and equal-or-lower effective limits. |
| 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 validates the frozen effective template, emits the `planning` envelope, and sends the immutable task through the frozen Plan binding with `reasoning_effort=high`. Its first provider turn requires an admitted `workspace_read` or `workspace_list`; only a successful inspection result makes Plan completion eligible. The result stays in the request-local conversation and does not create another artifact. The runner then accepts the stage-owned strict JSON schema for one-line `goal` plus bounded one-line `steps`/`verification` arrays, validates the fields, adds Markdown bullets, renders the template deterministically inside Edge, and writes the resulting Markdown through `SingleRequestArtifactPlan`. |
| single-request provider normalization | Private Plan/Work/Review calls pass caller-neutral effort/tool/structured-output requirements to the selected protocol profile. The profile chooses Chat Completions or Responses and maps unsupported effort only downward. An explicit managed selector freezes the exact provider ID; `default` freezes no provider ID and accepts the provider pool's concrete choice while retaining exact model-group/profile/target/credential/tunnel fences. Chat and Responses provider results are both reduced to one canonical private Chat-shaped envelope before strict stage decoding. No new Edge-Node field is added: the selected operation continues through the existing provider tunnel operation field. |
| 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 and Review handoff | Work parses the stored PLAN with deterministic `P1..Pn` IDs, projects only admitted workspace tools, and writes exactly one validated REVIEW handoff containing item status, changes, verification, and deviations. Review rereads both artifacts before provider dispatch, has no memory work payload, performs any repair/re-verification in the request-local ledger, and writes no final REVIEW page. |
| 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을 보장한다. |
| terminal ownership | Node normalized run과 raw tunnel은 caller cancel, configured hard deadline, provider terminal/error, transport disconnect만으로 끝난다. 무출력은 상태 전이나 retry input이 아니다. |
| provider admission | Provider-pool admission selects candidates by configured capacity, priority, health, and route capability. A provider terminal releases its lease exactly once; it does not create an automatic replay or alternate-provider selection. |
| OpenAI terminal consumption | Supported Chat/Responses normalized and tunnel paths project provider and transport terminal outcomes through their existing endpoint adapters. No watchdog-specific handoff reaches StreamGate. |
| tunnel-tolerant liveness | Edge와 Node는 30초 heartbeat interval과 45초 response wait를 공통으로 사용해 긴 prompt prefill이나 streaming backpressure 중의 정상 connection을 조기에 끊지 않는다. |
| reconnect/generation fencing | 현재 connection이 종료되면 해당 generation만 fence하고 Node supervisor가 reconnect한다. Heartbeat wait를 넘긴 경우의 close reason은 `heartbeat_timeout`이다. |
| cancellation/command | `run_id`로 현재 run만 취소하며 command는 capabilities, transport status, Ollama API tunnel로 제한한다. |
| managed credential lease | Edge가 principal·route·slot·profile·target·Node·revision·generation을 binding한 sealed lease를 발급하고 Node가 capacity admission 후 provider 실행 직전에만 연다. |
## 범위
- `session_id`는 event와 command result의 opaque correlation일 뿐이며 같은 값을 재사용해도 모든 run은 독립적이다.
- provider usage, capacity, queue pressure, lifecycle, reconnect, tool calling은 Edge-Node 실행 경로에서 계속 지원한다.
- single-request coordinator owns the service-level workspace admission described above as well as executor envelope privacy and the service-owned state graph. It exposes no workspace root, command executable/template/arguments, or environment values to the coordinator-facing binding.
- The request-local single-request quality gate classifies provider/tool timeouts, exhausted stage/request budgets, first proven repeated action/result no-progress, malformed calls/results, context/output limits, cancellation, internal-tool failures, and workspace cleanup into the closed terminal vocabulary. It retains only fixed hashes for repetition evidence and never retries, reselects, falls back, exposes a partial success, or starts a second request after classification.
- 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, and projects provider failures to the closed stage terminal. Before structured completion, it exposes only frozen-authority `workspace_read`/`workspace_list`, forces the first turn to use one of them, validates the canonical call, and resumes `planning` through the shared correlated continuation bridge. A failed result does not unlock completion, and the admitted per-stage iteration limit bounds further inspection. No write/delete/command capability or separate analysis artifact exists in Plan. After successful inspection the stage owns a closed strict JSON response schema with exactly a string `goal`, a string-array `steps`, and a string-array `verification`; unknown, duplicate, missing, or mistyped fields fail malformed. It enforces a single-line goal, 2-6 non-empty one-line step items, and 1-3 non-empty one-line verification items. Edge, rather than the provider, adds Markdown bullet prefixes and newlines before substituting the values into the frozen effective Plan template. Required headings remain exact standalone lines, the documented placeholder inventory is closed, and unresolved delimiters are rejected. Provider output therefore cannot vary headings, bullet formatting, or static template text, and caller request fields cannot select, supply, or override the admitted template.
- The private Work stage is installed in the composite single-request executor at Edge input startup (`apps/edge/internal/input/manager.go`). It reads a non-empty bounded `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 response is rendered once as `SingleRequestArtifactReview`; write failure prevents Review, while partial report fields remain Reviewer-owned evidence.
- Review reads the stored non-empty bounded PLAN and REVIEW handoff before its provider call. Edge does not reparse the PLAN or pre-judge REVIEW semantic completeness. Review may inspect, repair, and re-verify with admitted tools, but it neither takes a memory worker result nor writes a final REVIEW artifact. A repair mutation requires later successful inspection evidence before PASS; caller output is byte-for-byte the reviewer strict `output` field and cleanup removes the temporary artifacts.
- 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를 요구한다.
- revoked, disabled, expired, stale, replayed, wrong-recipient, mismatched lease는 provider나 credential fallback 없이 fail closed한다.
IOP no longer provides persistent shell sessions, terminal emulation, process resume, local working-directory execution context, arbitrary host commands, or local quota/status probing.
Single-request lifecycle 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). Internal tool names, raw arguments, and private results are absent from public output and log projections. Stage-pure timing, cardinality-bounded labels, and privacy semantics are documented here. SDD S12 external Claude qualification on an approved IOP Node confirms the same boundary with ingress `0→1`, Gemini → ornith-fast → Gemini, one `end_turn`, the exact expected workspace result, timing, cleanup, and redacted evidence.
## 주요 흐름
```mermaid
sequenceDiagram
participant Edge
participant Node
participant Provider
Node->>Edge: RegisterRequest
Edge-->>Node: RegisterResponse + config
Node->>Edge: NodeReadyRequest
Edge-->>Node: NodeReadyResponse
opt admitted single-request internal workspace call
Edge->>Node: WorkspaceOpenRequest once (frozen generation)
Node-->>Edge: WorkspaceOpenResponse
opt coordinator-owned artifact access
Edge->>Node: WorkspaceArtifactRequest(PLAN or REVIEW, READ or WRITE)
Node->>Node: map selector to plan.md or review.md and validate inventory
Node-->>Edge: bounded typed WorkspaceArtifactResponse
end
loop one ordered pending call
Edge->>Node: WorkspaceToolRequest(request, stage, tool)
Node-->>Edge: bounded typed WorkspaceToolResponse
end
Edge->>Node: WorkspaceCleanupRequest once before terminal commit
Node->>Node: cancel/wait request process groups and validate inventory
Node-->>Edge: typed WorkspaceCleanupResponse
Note over Edge: expose finalizing only after successful cleanup
Note over Edge: observation: ingress=1, request-total=1, terminal=1, stage/tool/cleanup counts, raw-free correlation
end
Edge->>Node: ProviderTunnelRequest
Node->>Provider: HTTP/SSE request
Provider-->>Node: status/header/body stream
Node-->>Edge: ordered ProviderTunnelFrame stream
Note over Edge,Node: heartbeat 30s interval / 45s wait
alt heartbeat wait exceeded
Edge--xNode: current generation fenced
Node->>Edge: supervised reconnect
end
```
## 계약
- Edge-Node wire: `agent-contract/inner/edge-node-runtime-wire.md`
- provider execution primitives: `agent-contract/inner/execution-runtime.md`
Heartbeat interval/wait는 protobuf field가 아닌 양쪽 transport 구현의 connection profile이다. Provider execution request wire에는 watchdog timeout field가 없다.
## 설정/데이터/이벤트
- Edge와 Node의 현재 heartbeat interval은 30초, response wait는 45초다.
- 이 값은 runtime YAML model config나 `max_tokens`/context 설정이 아니라 transport 구현 상수다.
- 45초 동안 heartbeat response가 없으면 current connection을 `heartbeat_timeout`으로 닫고 provider resource를 offline 처리한 뒤 reconnect/queue 재평가를 수행한다.
- Provider catalog와 Edge-Node request wire에는 output-silence timeout이 없다. request hard timeout, queue timeout, provider HTTP timeout, transport heartbeat, client response-idle timeout은 각각 기존 owner가 처리한다.
## 검증
- `go test -count=1 ./packages/go/execution ./apps/node/... ./apps/edge/internal/service`
- `go test -race -count=1 ./packages/go/execution ./apps/node/internal/node ./apps/edge/internal/service`
- `go test -race -count=1 ./apps/edge/internal/node -run 'TestRegistryReadyOwnerSnapshot'`
- `go test -race -count=1 ./apps/edge/internal/service -run 'TestSingleRequestWorkspace'`
- `go test -race -count=1 ./apps/edge/internal/service -run 'Test(InternalWorkspaceTool|SingleRequestInternalToolLoop)'`
- `go test -race -count=1 ./apps/edge/internal/service ./apps/node/internal/transport -run 'Test.*(WorkspaceArtifact|SingleRequestArtifact)'`
- `go test -count=1 ./apps/node/internal/workspace ./apps/node/internal/node ./apps/node/internal/transport -run 'Test.*(InternalArtifact|WorkspaceArtifact)'`
- `go test -race -count=1 ./apps/edge/internal/node ./apps/edge/internal/service ./apps/edge/internal/transport ./apps/node/internal/transport -run 'Test(BuildConfigPayload.*Workspace|WorkspaceWire|NodeParserMapWorkspace|SessionWorkspace|EdgeParserMapWorkspace)'`
- `go test -race -count=1 ./apps/node/internal/workspace -run 'Test(CommandExecutor|WorkspaceCommandHelperProcess)'`
- `go test -race -count=1 ./apps/node/internal/node -run 'TestNodeWorkspace(Command|Cancel)'`
- `go test -race -count=1 ./apps/node/internal/workspace -run 'TestWorkspaceCleanup'`
- `go test -race -count=1 ./apps/node/internal/node ./apps/edge/internal/service -run 'Test(NodeWorkspaceCleanup|SingleRequestCleanup)'`
- `go test -count=1 ./apps/node/internal/transport ./apps/edge/internal/transport`
- `go test -race -count=1 ./apps/node/internal/transport ./apps/edge/internal/transport`
- 실제 provider tunnel 검증은 5초를 넘는 긴 prefill과 streaming 응답 동안 Node가 connected/healthy를 유지하고, 응답이 정상 terminal을 반환하며, `heartbeat_timeout`이 발생하지 않는지 확인한다.
- `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 required read/list-first inspection, mutation-tool exclusion, successful-result completion gate, high reasoning, the closed structured response schema, exact JSON field admission, ordered tunnel frames, deterministic frozen-template rendering, planning/internal-tool resume envelopes, and `plan.md` artifact selection.
- `go test -count=1 ./apps/edge/internal/openai -run 'TestSingleRequest(ProviderStage|ProviderNormalization|PlanStage|WorkStage|ReviewStage|Executor)'` — private stage operation selection, Chat/Responses conversion, nearest-lower effort (`max` → `xhigh`), default-selector pool-provider acceptance, explicit dispatch fences, and composite regression evidence.
- `go test -count=1 ./packages/go/singlerequesttemplate ./packages/go/config` — template rendering, 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 Work tool loop, correlation isolation, cancellation cleanup, REVIEW rendering, 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.
## 한계와 주의사항
- 30/45초 transport heartbeat profile은 provider 응답 token 상한이나 model context window를 늘리지 않는다. 요청 중단 원인 판정 시 model 설정과 transport disconnect를 별도로 확인한다.
- 45초를 넘겨 실제 heartbeat response가 없는 connection은 기존과 같이 오프라인 처리하고 reconnect한다.
- Edge owns reception-generation and immutable-lease validation, provider-pool admission, and exactly-once release on terminal events.
- Output silence alone does not cancel a Node attempt, alter provider health, or select another provider. Explicit provider/transport terminal, caller cancellation, and the request hard deadline remain the terminal inputs.
- Workspace admission and the private wire both fence the exact ready connection generation. The wire never exposes workspace fields through provider `RunRequest`, `NodeCommand`, or public API output. The executor exposes no caller access to `.iop`; only request-owned internal runtime code can derive and inventory `.iop/job/<request_id>`. Structured write input is required for WRITE, while legacy content-only input remains rejected. COMMAND is non-interactive and has no shell, PTY, arbitrary argv, ambient environment, path-based cwd lookup, or persistent process ownership. Cleanup never rolls back or deletes user-requested workspace results.
- The composite single-request executor is installed at Edge input startup (`apps/edge/internal/input/manager.go`), wiring the active Plan -> Work -> Review stage pipeline for single-request execution. Private stage outcomes use the implemented closed S11 terminal policy and stop without retry/fallback or a second request. Deterministic local activation and terminal evidence are proven, and S12 (`claude-smoke`) qualifies the external Claude path on an approved IOP Node.
- Single-request observation evidence (ingress=1, request-total=1, terminal=1, stage/tool/cleanup counts, raw-free correlation) is documented and tested. `iop_anthropic_single_request_ingress_total` is strictly unlabeled. Internal tool names, raw arguments, and private results are absent from public output and log projections. SDD S12 confirms the external path with one ingress, the expected stage sequence, one terminal, exact output, timing, cleanup, and redacted evidence.
## 변경 기록
- 2026-08-15: Removed duplicate PLAN reparsing and REVIEW semantic pre-validation from Work/Review. Non-empty bounded artifacts proceed to Reviewer, which owns completeness checks and repair.
- 2026-08-14: Added bounded read/list-first workspace analysis inside Plan. Structured Plan completion now requires a successful inspection result in the same request-local conversation; Plan exposes no mutation tool and creates no additional analysis artifact (`apps/edge/internal/openai/single_request_plan_stage.go`).
- 2026-08-14: Restored artifact-only model handoff: deterministic PLAN `P1..Pn` IDs, one Work-authored validated REVIEW handoff, Review artifact reread with request-local repair/re-verification evidence, reviewer zero-write, and strict terminal `output` provenance.
- 2026-08-14: Moved private Plan/Work/Review provider calls onto the shared provider-normalization boundary. Stage requirements now select Chat or Responses without caller identity, unsupported effort maps only downward, and default-selector provider-pool choices no longer fail the post-dispatch validation that still fences profile, target, credential revision, model group, and tunnel path.
- 2026-08-14: Added common Chat result normalization for private stages so standard OpenAI bookkeeping fields are removed before strict decoding, matching the existing Responses-to-common conversion while preserving fail-closed refusal and unknown-field handling.
- 2026-08-12: Replaced nondeterministic free-form PlanMD generation with a stage-owned strict `goal`/`steps`/`verification` JSON response. Edge rejects unknown, duplicate, missing, mistyped, or out-of-bound fields and deterministically renders the already-frozen operator Plan template, preserving template customization and every caller-visible schema (`apps/edge/internal/openai/single_request_plan_stage.go`, `packages/go/singlerequesttemplate/template.go`).
- 2026-08-12: Replaced Markdown-bearing Plan string fields with bounded one-line string arrays and moved all bullet/newline rendering into Edge, removing the remaining provider-specific formatting dependency without changing the public Messages surface (`apps/edge/internal/openai/single_request_plan_stage.go`, `packages/go/singlerequesttemplate/template.go`).
- 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.
- 2026-08-07: Installed the composite single-request executor at Edge input startup (`apps/edge/internal/input/manager.go`), activating the Plan -> Work -> Review stage pipeline. Production construction evidence is test-covered (`apps/edge/internal/input/manager_test.go`), while actual Claude/Mac external qualification remains explicitly deferred to S12 (`claude-smoke`).
- 2026-08-02: provider tunnel의 긴 prompt prefill과 streaming backpressure를 정상 traffic으로 허용하도록 Edge/Node heartbeat profile을 30초 interval/45초 wait로 복원한 현재 구현과 회귀 검증을 반영했다 (`apps/edge/internal/transport/server.go`, `apps/node/internal/transport/client.go`).
- 2026-08-15: Removed the provider output-silence timeout, Node cancellation/probe path, Edge health/replay handoff, and their observations. Existing explicit terminal owners and transport heartbeat remain separate.
- 2026-08-06: Added implemented Edge workspace admission for single-request execution: an opaque `workspace_ref` binds to one configured ready Node generation and a closed capability projection before executor startup. Admission rejects unavailable, foreign, pending, malformed, and stale candidates without fallback or reselection; Node-private wire, executor, filesystem path, and symlink enforcement remain deferred.
- 2026-08-06: Added the dedicated Edge-Node workspace wire. `NodeConfigPayload` now delivers the approved catalog; `WorkspaceOpen`/`Tool`/`Cancel`/`Cleanup` messages have closed typed outcomes, immutable coordinator identities, parser registration, and an optional Node handler. Edge dispatch is generation-fenced and context cancellation sends one typed cancel. Node filesystem and process execution are intentionally deferred.
- 2026-08-06: Completed the reviewed workspace file boundary repair. Edge now sends only frozen request authority, Node admits immutable catalog subsets/lower limits, and structured write reaches the file executor while legacy incomplete input remains rejected. The Go 1.24-compatible descriptor-relative no-follow write path validates before effects, bounded list processing retains fixed state, startup errors are path-free, and composition proves handler-before-ready plus workspace-before-session/store teardown. Command execution/cancellation and cleanup remain deferred.
- 2026-08-07: Implemented exact-template workspace COMMAND and typed cancellation. The Node uses an inherited-root `fchdir`/`exec` shim, minimal allowlisted environment, a shared draining stdout/stderr cap, and one process-group result owner across exit, timeout, context cancel, and exact request/tool cancel. Focused race tests cover non-zero exit, output overflow, descendant termination, cross-request isolation, and configured-root rename/replacement. Artifact cleanup remains deferred.
- 2026-08-07: Implemented the coordinator-owned internal workspace tool loop with closed strict schemas, one-time exact-generation open, ordered pending-call correlation, deep-copied raw-free continuation results, immutable iteration/output/deadline budgets, typed cancellation, and real one-POST multi-tool privacy evidence.
- 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 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.