<!-- task=m-iop-owned-single-request-agent-execution/10+09_workspace_files plan=1 tag=API milestone-task=tool-executor -->

# Mac Workspace File Executor

## For the Implementing Agent

Do not start until packet 09 has `complete.log`. Implement only the exact files listed, preserve the typed wire contract, run every verification command, and fill `CODE_REVIEW-cloud-G09.md`. Official review owns verdict, logs, completion, and archive moves.

## Background

The dedicated wire is intentionally inert until Node can validate its private catalog, open a request context, and execute canonical read/list/write/delete operations beneath one root. This packet supplies that Mac-owned filesystem boundary and leaves command/process execution to packet 11.

## Archive Evidence Snapshot

- The first-pass pair is preserved at `agent-task/m-iop-owned-single-request-agent-execution/10+09_workspace_files/plan_cloud_G08_0.log` and `agent-task/m-iop-owned-single-request-agent-execution/10+09_workspace_files/code_review_cloud_G09_0.log`; it contains no implementation evidence or review verdict.
- Self-review found that the original reserved-root wording did not isolate sibling requests and used a second execution identity. Plan 1 binds the immutable coordinator `request_id`, reserves only `.iop/job/<request_id>` for internal runtime use, denies all caller access to `.iop`, and adds an independent Darwin compile gate.

## 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/node/rules.md`
- `agent-ops/rules/project/domain/platform-common/rules.md`
- `agent-test/local/rules.md`
- `agent-test/local/node-smoke.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`
- `apps/node/internal/bootstrap/module.go`
- `apps/node/internal/bootstrap/module_test.go`
- `apps/node/internal/node/node.go`
- `apps/node/internal/node/config_refresh_handler.go`
- `apps/node/internal/transport/session.go`
- `apps/edge/internal/node/mapper.go`
- `proto/iop/runtime.proto`
- `agent-contract/inner/edge-node-runtime-wire.md`
- `agent-contract/inner/execution-runtime.md`
- `agent-spec/runtime/edge-node-execution.md`

### SDD Criteria

- S04 requires approved root containment and rejection of absolute/foreign paths, caller access to `.iop`, sibling request namespaces, mount-boundary traversal, and symlink escape before effects.
- S05 requires typed read/list/write/delete success/failure and bounded output. D06 excludes interactive shell/desktop/scheduler.
- Cleanup and command process groups remain packets 13 and 11 respectively.

### Verification Context

- Host Go is 1.26.2; module baseline supports Go 1.24, whose `os.Root` provides root-relative APIs and rejects symlink traversal outside the root.
- `go doc os.Root` confirms methods are concurrent-safe and that special files/mounts still require explicit rejection.
- Baseline Node/bootstrap/transport packages passed fresh. Filesystem tests use `t.TempDir`; production Mac platform validation is injected with an explicit host-OS argument for deterministic Linux CI tests.

### State and Concurrency Findings

- One runtime catalog owns long-lived `os.Root` handles plus opened directory handles for stable process cwd; request contexts bind the immutable coordinator `request_id` to exactly one ref and immutable caps. No Node-local execution-id alias is introduced.
- Duplicate open is idempotent only for byte-identical binding; conflicting reuse fails. A request cannot operate before open or after close/cleanup.
- `os.Root` supplies lexical/symlink containment but explicitly does not prohibit mount traversal. The executor must separately reject empty/absolute/parent paths, root deletion, caller-visible `.iop`, sibling jobs, cross-device/mount targets, non-regular reads/writes, devices/FIFOs, recursive user delete, and unbounded reads/lists.

### Test Coverage Gaps

- Node has no workspace runtime field/handler or bootstrap catalog validation.
- No test covers in-root symlinks versus escaping symlinks, special files, bounded listing/read, atomic writes, or concurrent request isolation.

### Symbol References

- Packet 09's optional `transport.WorkspaceHandler` is implemented by `*node.Node`; existing `transport.Handler` remains unchanged.
- `runtimeOwner.close` must close workspace roots during reconnect/shutdown.
- No provider adapter/router/store API is reused.

### Split Judgment

- Stable contract: file-only runtime independently passes every S04 containment case and returns typed unsupported for command.
- Command is separate because process-group cancellation and environment/output races are a distinct correctness boundary.

### Scope Rationale

- Include catalog startup validation, request contexts, file operations, Node handler, bootstrap lifecycle, tests, contract/spec sync.
- Exclude command execution, provider tool loop, cleanup artifact removal, and metrics.

### Final Routing

- `evaluation_mode=isolated-reassessment`; `review_rework_count=0`; `evidence_integrity_failure=false`; build closures true, scores 2/1/2/1/2 = G08.
- Finalizer route `risk-boundary`, lane `cloud`, filename `PLAN-cloud-G08.md`; risks `boundary_contract`, `structured_interpretation`, `variant_product`, `concurrent_consistency` (4).
- Review scores 2/1/2/2/2 = G09; official filename `CODE_REVIEW-cloud-G09.md`; no capability gap or recovery signal.

## Dependencies and Execution Order

1. Require packet 09 completion.
2. Implement the private catalog/root layer and file operations.
3. Adapt typed messages in Node, then wire runtime ownership in bootstrap.
4. Run race/containment tests before updating contract/spec claims.

## Implementation Checklist

- [ ] Build a Mac-only immutable workspace catalog using `os.Root`, canonical-root checks, immutable coordinator `request_id` binding, and explicit runtime lifecycle ownership.
- [ ] Implement bounded read/list plus atomic write and non-recursive delete with fail-closed relative path, symlink, mount, special-file, capability, and `.iop` namespace validation.
- [ ] Implement packet 09's optional Node workspace handler, bootstrap/close the runtime before ready, and keep command typed-unsupported.
- [ ] Prove containment, sibling-request isolation, bounds, concurrency, mapping, startup failure, and synchronize only implemented file-executor contract/spec claims.
- [ ] Run dependency, focused race, package, vet, Darwin compile, documentation, and whitespace verification.
- [ ] Fill implementation-owned sections in CODE_REVIEW-*-G??.md with actual implementation notes and verification output.

## Implementation Plan

### [API-1] Own immutable roots and request contexts

**Problem**

- `apps/node/internal/node/node.go:18` has only provider runtime fields.
- `apps/node/internal/bootstrap/module.go:100` builds adapters immediately from registration config and signals ready without validating workspaces.

**Solution**

Before (`apps/node/internal/node/node.go:18`):

```go
type Node struct {
	nodeID string
	router runtime.Router
	store  *store.Store
	// provider fields
}
```

After:

```go
type Node struct {
	// Existing fields remain.
	workspaceRuntime *workspace.Runtime
}

func (n *Node) SetWorkspaceRuntime(rt *workspace.Runtime) { n.workspaceRuntime = rt }
```

Create `apps/node/internal/workspace` with an immutable catalog. Constructor accepts packet 09 configs plus explicit host OS, requires `darwin`, validates the configured root exists, is an absolute directory and not a symlink/root, records its filesystem identity, and opens both `os.Root` and a directory handle for later descriptor-based command cwd. The opened handles, not a re-resolved path, are the admitted workspace authority. `Open` binds the validated immutable coordinator `request_id` to one workspace ref and copied lower limits; conflicting duplicate ids fail. The request context derives one internal prefix, `.iop/job/<request_id>`, from that validated identity and exposes no caller-controlled internal path. `Close` makes and closes all handles exactly once and is idempotent.

**Modified Files and Checklist**

- [ ] `apps/node/internal/workspace/runtime.go` — catalog, filesystem and stable directory handles/identity, request map, open/close, immutable request identity, internal-prefix derivation, and capability checks.
- [ ] `apps/node/internal/workspace/runtime_test.go` — platform/root/startup, duplicate/open/close, request-id validation, copy and concurrent request isolation.
- [ ] `apps/node/internal/node/node.go` — hold the optional runtime without changing constructor call sites.

**Test Strategy**

- Test valid injected `darwin`, wrong host OS, root symlink, missing/non-directory/root path, duplicate refs, invalid/conflicting request id, and concurrent isolation under `-race`.

**Verification**

- `go test -race ./apps/node/internal/workspace -run 'TestRuntime(Open|Catalog|Close|Concurrent)' -count=1`
- Expected: only a validated Mac catalog opens and request identities never cross roots.

### [API-2] Execute canonical bounded file operations

**Problem**

- No Node code executes relative workspace tools; using `os.ReadFile`/ordinary joins would allow unbounded allocation or TOCTOU escape.

**Solution**

Add path validation before every operation and use only the request's `*os.Root`. Caller tool paths must be canonical relative user paths and must reject `.iop` itself and every descendant before lookup. A separate unexported internal-path helper may accept only the exact derived `.iop/job/<request_id>` prefix for the current request; it rejects `.iop`, `.iop/job`, sibling ids, and caller-supplied variants. Opened targets/parents must remain on the admitted root filesystem and must not be symlinks or special files where the operation requires regular files/directories; fail closed when identity cannot be proven. Read through `io.LimitReader(max+1)`, stat the opened handle as regular, and return typed truncation/error. List a directory with stable lexical ordering, entry/type encoding, and shared byte/entry bound. Write rejects oversized input and non-regular existing targets, creates validated parents through `Root.MkdirAll`, writes/fsyncs a random same-directory temp, and atomically renames. Delete rejects `.` and removes only a regular file, symlink itself, or empty directory; never recursively deletes user paths. Command returns the packet 09 unsupported error until packet 11.

**Modified Files and Checklist**

- [ ] `apps/node/internal/workspace/path.go` — canonical relative user path, exact request-owned internal-prefix, sibling namespace, filesystem-identity, and reserved-root validation.
- [ ] `apps/node/internal/workspace/file_executor.go` — bounded read/list, atomic write, non-recursive delete, typed results.
- [ ] `apps/node/internal/workspace/file_executor_test.go` — operation/capability matrix, bounds, `.iop` and sibling-request denial, symlinks/mounts/special files, atomic replacement, and concurrent roots.

**Test Strategy**

- Use `t.TempDir`, inside/outside symlinks, a mount substitute/helper where the host permits it, FIFO where supported, large fixtures, and parallel operations. Verify caller operations cannot observe or mutate `.iop`, the current request's internal helper cannot enter a sibling job, no cross-filesystem/outside file changes occur, and no partial target remains after failed write. Skip only the privileged mount fixture when unavailable while retaining deterministic filesystem-identity unit coverage.

**Verification**

- `go test -race ./apps/node/internal/workspace -run 'TestFileExecutor' -count=1`
- Expected: all canonical user operations work inside root and every reserved-namespace/sibling/mount/escape/special/bound violation fails before outside or partial effects.

### [API-3] Wire Node handler and bootstrap lifecycle

**Problem**

- `apps/node/internal/bootstrap/module.go:124` creates Node and signals ready without workspace initialization.
- Packet 09's optional handler currently returns unsupported because `Node` does not implement it.

**Solution**

Before (`apps/node/internal/bootstrap/module.go:124`):

```go
rtr := router.New(set.Registry, logger)
n := node.New(result.NodeID, rtr, st, globalConcurrency, os.Stdout, logger, set)
```

After:

```go
workspaceRuntime, err := workspace.NewRuntime(result.Config.GetWorkspaces(), runtime.GOOS, logger)
if err != nil { /* close owner and fail before ready */ }
n := node.New(/* existing args */)
n.SetWorkspaceRuntime(workspaceRuntime)
owner.workspace = workspaceRuntime
```

Implement open/tool dispatch in `workspace_handler.go`, echo the immutable `request_id` plus stage/tool identities, map only stable error codes/messages, and never log request path/content/result. Cancel/cleanup remain typed unsupported. Extend `runtimeOwner.close` to close the workspace runtime before session/store teardown.

**Modified Files and Checklist**

- [ ] `apps/node/internal/node/workspace_handler.go` — implement open/file tool responses and typed unsupported command/cancel/cleanup.
- [ ] `apps/node/internal/node/workspace_handler_test.go` — handler mapping, missing runtime, identity, raw-free error cases.
- [ ] `apps/node/internal/bootstrap/module.go` — construct/own/close runtime before `SignalReady`.
- [ ] `apps/node/internal/bootstrap/workspace_runtime_test.go` — startup success/failure and close ownership with injected payload/host OS helper.
- [ ] `agent-contract/inner/edge-node-runtime-wire.md` — mark file semantics implemented and command/cleanup deferred.
- [ ] `agent-spec/runtime/edge-node-execution.md` — synchronize current runtime entry points and tests.

**Test Strategy**

- Use direct handler tests and a bootstrap composition helper; no real Edge or Mac host is required.

**Verification**

- `go test -race ./apps/node/internal/node ./apps/node/internal/bootstrap -run 'Test(NodeWorkspace|WorkspaceRuntime)' -count=1`
- `rg --sort path -n 'os.Root|read|list|write|delete|symlink|command.*defer|cleanup.*defer' agent-contract/inner/edge-node-runtime-wire.md agent-spec/runtime/edge-node-execution.md`
- Expected: Node becomes ready only after valid root ownership and docs claim file operations only.

## Modified Files Summary

| File | Item |
|------|------|
| `apps/node/internal/workspace/runtime.go` | API-1 |
| `apps/node/internal/workspace/runtime_test.go` | API-1 |
| `apps/node/internal/node/node.go` | API-1 |
| `apps/node/internal/workspace/path.go` | API-2 |
| `apps/node/internal/workspace/file_executor.go` | API-2 |
| `apps/node/internal/workspace/file_executor_test.go` | API-2 |
| `apps/node/internal/node/workspace_handler.go` | API-3 |
| `apps/node/internal/node/workspace_handler_test.go` | API-3 |
| `apps/node/internal/bootstrap/module.go` | API-3 |
| `apps/node/internal/bootstrap/workspace_runtime_test.go` | API-3 |
| `agent-contract/inner/edge-node-runtime-wire.md` | API-3 |
| `agent-spec/runtime/edge-node-execution.md` | API-3 |
| `agent-task/m-iop-owned-single-request-agent-execution/10+09_workspace_files/CODE_REVIEW-cloud-G09.md` | API-1, API-2, API-3 |

## Final Verification

1. `test -f agent-task/m-iop-owned-single-request-agent-execution/09+08_workspace_wire/complete.log || test "$(compgen -G 'agent-task/archive/*/*/m-iop-owned-single-request-agent-execution/09+08_workspace_wire/complete.log' | wc -l)" -eq 1`
2. `go test -race ./apps/node/internal/workspace -run 'Test(Runtime|FileExecutor)' -count=1`
3. `go test -race ./apps/node/internal/node ./apps/node/internal/bootstrap -run 'Test(NodeWorkspace|WorkspaceRuntime)' -count=1`
4. `go test ./apps/node/internal/workspace ./apps/node/internal/node ./apps/node/internal/bootstrap ./apps/node/internal/transport -count=1`
5. `go vet ./apps/node/internal/workspace ./apps/node/internal/node ./apps/node/internal/bootstrap`
6. `GOOS=darwin GOARCH=arm64 go test -c -o /tmp/iop-workspace-files-darwin.test ./apps/node/internal/workspace`
7. `rg --sort path -n 'os.Root|request_id|\.iop/job|read|list|write|delete|symlink|mount|command.*defer|cleanup.*defer' agent-contract/inner/edge-node-runtime-wire.md agent-spec/runtime/edge-node-execution.md`
8. `git diff --check`

Expected: the wire predecessor is uniquely complete; immutable request identity, exact internal namespace isolation, containment, and lifecycle tests pass under race; Darwin compilation and Node regressions pass; docs defer command/cleanup. Cached tests are not acceptable.

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