<!-- task=m-iop-owned-single-request-agent-execution/08+03,07_workspace_admission plan=0 tag=API milestone-task=workspace-binding -->

# Request-stable Workspace Admission

## For the Implementing Agent

Do not start until packets 03 and 07 each have `complete.log`. Implement exactly within `Modified Files Summary`, run all verification, fill `CODE_REVIEW-cloud-G09.md`, and leave loop finalization to the official reviewer. A blocker belongs only in implementation evidence with its resume condition.

## Background

Packet 07 maps an opaque ref to one configured Node; packet 03 starts a request-local coordinator. The remaining admission gap is to freeze that ref to the exact dispatch-ready Node connection generation and effective capabilities before any provider or tool work begins.

## Analysis

### Files Read

- `AGENTS.md`
- `agent-ops/rules/project/rules.md`
- `agent-ops/rules/common/rules-roadmap.md`
- `agent-ops/rules/common/rules-agent-spec.md`
- `agent-ops/rules/project/domain/edge/rules.md`
- `agent-test/local/rules.md`
- `agent-test/local/edge-smoke.md`
- `agent-roadmap/priority-queue.md`
- `agent-roadmap/phase/knowledge-tool-optimization-extension/PHASE.md`
- `agent-roadmap/phase/knowledge-tool-optimization-extension/milestones/iop-owned-single-request-agent-execution.md`
- `agent-roadmap/sdd/knowledge-tool-optimization-extension/iop-owned-single-request-agent-execution/SDD.md`
- `agent-task/m-iop-owned-single-request-agent-execution/02+01_preset_binding/PLAN-local-G06.md`
- `agent-task/m-iop-owned-single-request-agent-execution/03+02_single_request_coordinator/PLAN-local-G07.md`
- `apps/edge/internal/service/service.go`
- `apps/edge/internal/node/store.go`
- `apps/edge/internal/node/store_test.go`
- `apps/edge/internal/node/registry.go`
- `apps/edge/internal/node/registry_test.go`
- `agent-contract/inner/edge-config-runtime-refresh.md`
- `agent-spec/runtime/edge-node-execution.md`
- `agent-spec/runtime/provider-pool-config-refresh.md`

### SDD Criteria

- S04 requires approved workspace admission and pre-execution rejection of unapproved ref, foreign Node/path, and escape candidates.
- The request's workspace generation is immutable alongside planner/worker/reviewer bindings; refresh or reconnect cannot silently retarget it.
- This packet supplies admission and generation fencing. Packet 10 supplies filesystem/symlink enforcement.

### Verification Context

- Baseline targeted packages passed fresh at the starting HEAD.
- Both predecessor APIs are intentionally unavailable in current source; their active plans define `SingleRequestBinding`, `StartSingleRequest`, and the catalog contract. Implementation must first verify their exact `complete.log` evidence and then use the implemented symbols without changing predecessor ownership.
- Deterministic evidence is race-tested service/node unit tests; no Mac runner is needed for admission-only behavior.

### State and Concurrency Findings

- A ready Node connection owns `(node_id, connection_generation)`; reconnect creates a strictly larger generation.
- Admission must copy capability ids/limits, never retain mutable config slices, and must not store the raw root or command executable/args in the coordinator-facing binding.
- Unavailable, pending, stale, foreign, or ambiguous ownership fails before the executor starts; no implicit single-node fallback is allowed for `workspace_ref`.

### Test Coverage Gaps

- No test binds a workspace ref to a ready generation or proves reconnect/refresh isolation and executor non-invocation on rejection.

### Symbol References

- Packet 02 plans `SingleRequestBinding.WorkspaceRef`; extend rather than replace its public model/stage/limit fields.
- Packet 03 plans `Service.StartSingleRequest`; workspace binding must wrap its admission before the executor call.
- Additive registry snapshot helpers must not change existing `ResolveReady` behavior.

### Split Judgment

- Stable contract: a request-stable `SingleRequestWorkspaceBinding` can be independently tested with a fake executor and no wire.
- Wire dispatch remains packet 09 because mixing transport would prevent admission-only PASS evidence.

### Scope Rationale

- Include exact ref lookup, ready-generation snapshot, effective capability copy, start-time rejection, and living-spec sync.
- Exclude protobuf, Node root validation, actual tool execution, tool loop, and cleanup.

### Final Routing

- `evaluation_mode=first-pass`; build closures true, scores 2/2/1/1/2 = G08.
- Finalizer route is `risk-boundary`, lane `cloud`, filename `PLAN-cloud-G08.md`; risks are `temporal_state`, `concurrent_consistency`, `boundary_contract`, and `variant_product` (4).
- Review scores 2/2/1/2/2 = G09; official review filename `CODE_REVIEW-cloud-G09.md`. No rework/evidence-integrity signal and no capability gap.

## Dependencies and Execution Order

1. `03+02_single_request_coordinator` must complete.
2. `07+04_workspace_catalog` must complete.
3. Add a cloned ready-owner snapshot, compile the binding, then insert it before executor startup.

## Implementation Checklist

- [ ] Freeze each approved workspace ref to one configured Node id, ready connection generation, closed operation/command ids, and effective limits without raw root/template leakage.
- [ ] Fail before executor startup on missing, foreign, pending, stale, malformed, or unsupported workspace ownership and prohibit fallback/reselection.
- [ ] Prove admission immutability and reconnect/refresh races, then synchronize the runtime living spec.
- [ ] Run exact dependency, focused race, package, vet, documentation, and whitespace verification.
- [ ] Fill implementation-owned sections in CODE_REVIEW-*-G??.md with actual implementation notes and verification output.

## Implementation Plan

### [API-1] Snapshot exact ready workspace ownership

**Problem**

- `apps/edge/internal/node/registry.go:325` returns a shared ready entry and callers can otherwise re-resolve a different connection after admission.
- Packet 07's catalog lookup identifies the configured Node but not its live generation.

**Solution**

Before (`apps/edge/internal/node/registry.go:325`):

```go
func (r *Registry) GetReady(nodeID string) (*NodeEntry, bool) {
	r.mu.RLock()
	defer r.mu.RUnlock()
	e, ok := r.byID[nodeID]
	if !ok || !e.DispatchReady {
		return nil, false
	}
	return e, true
}
```

After, preserve existing behavior and add a cloned API:

```go
func (r *Registry) ReadyOwnerSnapshot(nodeID string) (*NodeEntry, bool) {
	// Return Clone() only when DispatchReady under the same read lock.
}
```

Use packet 07's exact ref lookup to derive the configured Node id. Never accept caller Node/path input and never use `ResolveReady("")` fallback.

**Modified Files and Checklist**

- [ ] `apps/edge/internal/node/registry.go` — add an atomic cloned ready-owner snapshot.
- [ ] `apps/edge/internal/node/registry_test.go` — cover pending, ready, disconnect/reconnect generation, and copy isolation.

**Test Strategy**

- Add `TestRegistryReadyOwnerSnapshot` and a reconnect race case under `-race`.

**Verification**

- `go test -race ./apps/edge/internal/node -run 'TestRegistryReadyOwnerSnapshot' -count=1`
- Expected: snapshots are immutable and never represent a pending or superseded connection.

### [API-2] Bind workspace before single-request execution

**Problem**

- Packet 02's planned `SingleRequestBinding` carries only opaque `WorkspaceRef` (`02+01_preset_binding/PLAN-local-G06.md:120`).
- Packet 03's planned `StartSingleRequest` snapshots the executor and immediately calls it (`03+02_single_request_coordinator/PLAN-local-G07.md:140`), leaving no concrete workspace admission.

**Solution**

Before (predecessor contract, `03+02_single_request_coordinator/PLAN-local-G07.md:140`):

```go
func (s *Service) StartSingleRequest(ctx context.Context, req SingleRequestRequest) (SingleRequestExecution, error) {
	s.mu.RLock()
	executor := s.singleRequestExecutor
	s.mu.RUnlock()
	return startSingleRequest(ctx, executor, req)
}
```

After:

```go
bound, err := s.bindSingleRequestWorkspace(req.Binding)
if err != nil {
	return nil, err
}
req.Binding = bound
return startSingleRequest(ctx, executor, req)
```

Add `SingleRequestWorkspaceBinding` with only `Ref`, `NodeID`, `ConnectionGeneration`, cloned operation/command ids, and workspace/preset effective maxima. Keep raw root, executable, fixed args, and environment values out. Validate the original binding and catalog agree; use the lower applicable preset/workspace bound. Recheck generation immediately before executor handoff and make the executor receive only the frozen value.

**Modified Files and Checklist**

- [ ] `apps/edge/internal/service/single_request_types.go` — extend the predecessor DTO with a frozen workspace binding and deep-copy validation.
- [ ] `apps/edge/internal/service/single_request.go` — bind before executor startup and fail closed without emitting a stage.
- [ ] `apps/edge/internal/service/single_request_workspace.go` — own exact catalog/registry admission and effective-limit calculation.
- [ ] `apps/edge/internal/service/single_request_workspace_test.go` — cover approval matrix, no fallback, pending/stale/reconnect, refresh mutation, copy isolation, and executor non-invocation.
- [ ] `agent-spec/runtime/edge-node-execution.md` — document request-generation workspace admission and defer executor/wire claims.

**Test Strategy**

- Use a recording executor and real `NodeStore`/`Registry`. Mutate source config after admission and reconnect the same Node id to prove the frozen generation does not retarget.

**Verification**

- `go test -race ./apps/edge/internal/service -run 'TestSingleRequestWorkspace' -count=1`
- `rg --sort path -n 'workspace_ref|connection generation|admission|reselect|defer' agent-spec/runtime/edge-node-execution.md`
- Expected: only one approved ready generation reaches the executor and rejected paths emit no provider/tool work.

## Modified Files Summary

| File | Item |
|------|------|
| `apps/edge/internal/node/registry.go` | API-1 |
| `apps/edge/internal/node/registry_test.go` | API-1 |
| `apps/edge/internal/service/single_request_types.go` | API-2 |
| `apps/edge/internal/service/single_request.go` | API-2 |
| `apps/edge/internal/service/single_request_workspace.go` | API-2 |
| `apps/edge/internal/service/single_request_workspace_test.go` | API-2 |
| `agent-spec/runtime/edge-node-execution.md` | API-2 |
| `agent-task/m-iop-owned-single-request-agent-execution/08+03,07_workspace_admission/CODE_REVIEW-cloud-G09.md` | API-1, API-2 |

## Final Verification

1. `test -f agent-task/m-iop-owned-single-request-agent-execution/03+02_single_request_coordinator/complete.log || test "$(compgen -G 'agent-task/archive/*/*/m-iop-owned-single-request-agent-execution/03+02_single_request_coordinator/complete.log' | wc -l)" -eq 1`
2. `test -f agent-task/m-iop-owned-single-request-agent-execution/07+04_workspace_catalog/complete.log || test "$(compgen -G 'agent-task/archive/*/*/m-iop-owned-single-request-agent-execution/07+04_workspace_catalog/complete.log' | wc -l)" -eq 1`
3. `go test -race ./apps/edge/internal/node -run 'TestRegistryReadyOwnerSnapshot' -count=1`
4. `go test -race ./apps/edge/internal/service -run 'TestSingleRequestWorkspace' -count=1`
5. `go test ./apps/edge/internal/node ./apps/edge/internal/service -count=1`
6. `go vet ./apps/edge/internal/node ./apps/edge/internal/service`
7. `rg --sort path -n 'workspace_ref|connection generation|admission|reselect|defer' agent-spec/runtime/edge-node-execution.md`
8. `git diff --check`

Expected: both predecessors resolve exactly once; ready-owner and request bindings remain immutable under races; rejection precedes executor activity; all package checks pass. Cached Go tests are not acceptable.

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