<!-- task=m-iop-owned-single-request-agent-execution/07+04_workspace_catalog plan=0 tag=API milestone-task=workspace-binding -->

# Operator-owned Mac Workspace Catalog

## For the Implementing Agent

Do not start until packet 04 has `complete.log`. Implement only the files in `Modified Files Summary`, run every verification command, fill the paired review stub with actual evidence, and leave finalization to the code-review skill. If blocked, record the exact evidence and resume condition in the review stub; do not ask the user or create control-plane artifacts.

## Background

The fixed-light preset carries only an opaque `workspace_ref`, but Edge has no operator-owned catalog that maps that reference to one Mac Node root and bounded file/command capabilities. This packet creates that source of truth without sending tool requests yet.

## 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-ops/rules/project/domain/platform-common/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`
- `packages/go/config/edge_types.go`
- `packages/go/config/load.go`
- `packages/go/config/edge_runtime_config_test.go`
- `apps/edge/internal/node/store.go`
- `apps/edge/internal/node/store_test.go`
- `apps/edge/internal/configrefresh/classify.go`
- `apps/edge/internal/configrefresh/node_runtime_classify_test.go`
- `configs/edge.yaml`
- `agent-contract/inner/edge-config-runtime-refresh.md`
- `agent-spec/runtime/provider-pool-config-refresh.md`

### SDD Criteria

- The approved SDD maps `workspace-binding` to S04 and requires approved/denied workspace, foreign Node/path, and symlink-escape candidates to fail before execution.
- This foundation supplies S04's operator catalog and refresh boundary. Runtime admission and filesystem containment remain in packets 08 and 10.
- D03, D06, and D08 require a Mac Node-owned bounded executor and forbid caller-selected raw roots and reuse of provider wire.

### Verification Context

- Starting HEAD is `a94002a19c774b90160f87a99887b531f7d84015`; the worktree was clean before plan creation.
- `go version go1.26.2 linux/arm64`, `go`, `make`, and `protoc` are available.
- Fresh baseline tests passed for config, Edge node/config-refresh/service/transport, and Node transport/node/bootstrap packages.
- Repository-native fallback is `LoadEdge`, `NodeStore`, restart classification, their unit tests, and the approved config contract/spec. No external runner is required.

### Test Coverage Gaps

- No config validates globally unique workspace refs, absolute non-root paths, the fixed `darwin` platform, closed operations, bounded sizes/timeouts, exact command templates, or environment-name allowlists.
- `NodeStore` and refresh classification discard workspace ownership facts.

### Symbol References

- `NodeDefinition` is decoded by `LoadEdge` and compiled by `LoadFromConfig`.
- `nodeKey` in config refresh must retain the new catalog so changes cannot be silently live-applied.
- No existing symbol is renamed or removed.

### Split Judgment

- Stable contract: validated config plus `NodeStore.ResolveWorkspace` can independently PASS before wire/executor work.
- Packet 04 owns preset refresh files first; this packet therefore depends on 04 and may edit them only after its completion.

### Scope Rationale

- Include catalog schema, validation, store lookup, restart classification, example comments, contract, and living spec.
- Exclude protobuf, Node filesystem access, admission generation fencing, process execution, and coordinator integration.

### Final Routing

- `evaluation_mode=first-pass`; finalizer `finalize-task-policy.sh` in `pair` mode.
- Build closures are true; scores 2/0/2/1/1 = G06; route `local-fit`, lane `local`, filename `PLAN-local-G06.md`.
- Build signals: `large_indivisible_context=false`; positive risks `boundary_contract`, `structured_interpretation`, `variant_product` (3); no rework or evidence-integrity failure.
- Review closures are true; scores 2/0/2/1/2 = G07; official review filename `CODE_REVIEW-cloud-G07.md`.

## Dependencies and Execution Order

1. Require `04+02_preset_refresh` completion.
2. Add and validate the config DTOs.
3. Preserve the catalog in `NodeStore`, classify any catalog mutation as restart-required, then synchronize docs.

## Implementation Checklist

- [ ] Define and fail-closed validate the globally unique operator workspace catalog, closed operations, fixed command templates, Mac platform, and numeric/environment boundaries.
- [ ] Preserve immutable workspace capabilities in `NodeStore`, expose exact-ref lookup, and classify workspace changes as restart-required.
- [ ] Synchronize the config example, inner config contract, and provider/config-refresh living spec without claiming runtime execution.
- [ ] Run 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] Add the approved workspace catalog schema

**Problem**

- `packages/go/config/edge_types.go:134` gives a Node only adapters, providers, and runtime metadata.
- `packages/go/config/load.go:123` validates providers but has no workspace uniqueness or capability validation.

**Solution**

Before (`packages/go/config/edge_types.go:134`):

```go
type NodeDefinition struct {
	ID        string
	Alias     string
	Token     string
	Adapters  AdaptersConf
	Providers []NodeProviderConf
	Runtime   RuntimeConf
}
```

After, with complete `mapstructure`/`yaml` tags:

```go
type NodeDefinition struct {
	// Existing fields remain.
	Workspaces []WorkspaceDefinition
}

type WorkspaceDefinition struct {
	Ref, Platform, Root string
	Operations []WorkspaceOperation
	Commands []WorkspaceCommandDefinition
	EnvironmentAllowlist []string
	MaxReadBytes, MaxWriteBytes, MaxOutputBytes, MaxCommandTimeoutMS int
}
```

Define the closed operation constants `read`, `list`, `write`, `delete`, and `command`. A command definition is an operator-owned template (`id`, absolute clean `executable`, fixed `args`); the caller/model selects only its id. Validate trimmed globally unique refs, `platform == "darwin"`, absolute clean roots other than `/`, non-empty unique operations/command ids, command presence iff command is enabled, positive bounded byte/time limits, and unique portable environment variable names. Do not stat Mac paths on Edge and do not put roots or command details in execution presets.

**Modified Files and Checklist**

- [ ] `packages/go/config/edge_types.go` — add typed workspace and command capability DTOs/constants.
- [ ] `packages/go/config/load.go` — normalize and validate all workspaces before presets become observable.
- [ ] `packages/go/config/workspace_config_test.go` — table-test valid decode plus duplicates, platform/root, operation, command, env, and numeric rejection.

**Test Strategy**

- Add `TestLoadEdgeWorkspaceCatalog` and `TestLoadEdgeWorkspaceCatalogRejectsInvalid` with temporary YAML fixtures.
- Assert unknown/duplicate operations and globally duplicated refs fail closed; empty catalogs remain backward-compatible.

**Verification**

- `go test -race ./packages/go/config -run 'TestLoadEdgeWorkspaceCatalog' -count=1`
- Expected: valid Mac catalogs normalize exactly and every invalid boundary is rejected.

### [API-2] Compile catalog ownership and restart semantics

**Problem**

- `apps/edge/internal/node/store.go:12` drops workspace definitions while compiling nodes.
- `apps/edge/internal/configrefresh/classify.go:223` compares alias/token/adapters/runtime but not workspace roots or capabilities.

**Solution**

Before (`apps/edge/internal/node/store.go:12`):

```go
type NodeRecord struct {
	ID, Alias, Token string
	Adapters config.AdaptersConf
	Providers []config.NodeProviderConf
	Runtime config.RuntimeConf
}
```

After:

```go
type NodeRecord struct {
	// Existing fields remain.
	Workspaces []config.WorkspaceDefinition
}

func (s *NodeStore) ResolveWorkspace(ref string) (*NodeRecord, config.WorkspaceDefinition, error)
```

Deep-copy slices/maps on store construction and lookup. Reject duplicate refs even when `LoadFromConfig` is called directly. Add workspaces to the config-refresh node key and report `nodes["<id>"].workspaces` as `restart_required`; active requests must never observe a root/capability mutation. Add only a commented, non-host-specific example in `configs/edge.yaml`.

**Modified Files and Checklist**

- [ ] `apps/edge/internal/node/store.go` — retain immutable capabilities and resolve exactly one ref.
- [ ] `apps/edge/internal/node/store_test.go` — cover lookup, copy isolation, missing/duplicate refs, and node ownership.
- [ ] `apps/edge/internal/configrefresh/classify.go` — make workspace mutations restart-required.
- [ ] `apps/edge/internal/configrefresh/workspace_classify_test.go` — assert root/capability changes cannot be applied live.
- [ ] `configs/edge.yaml` — add a commented operator workspace example with no real local path or secret.
- [ ] `agent-contract/inner/edge-config-runtime-refresh.md` — define schema ownership, validation, secrecy, and restart semantics.
- [ ] `agent-spec/runtime/provider-pool-config-refresh.md` — record current catalog compilation and explicit runtime deferral.

**Test Strategy**

- Extend store tests and add a focused refresh test. Documentation uses those executable tests as its oracle.

**Verification**

- `go test -race ./apps/edge/internal/node ./apps/edge/internal/configrefresh -run 'Test(LoadFromConfig.*Workspace|NodeStore.*Workspace|ClassifyWorkspace)' -count=1`
- `rg --sort path -n 'workspace_ref|workspaces|restart_required|darwin' configs/edge.yaml agent-contract/inner/edge-config-runtime-refresh.md agent-spec/runtime/provider-pool-config-refresh.md`
- Expected: lookup is immutable and exact, all workspace changes require restart, and docs do not claim executor completion.

## Modified Files Summary

| File | Item |
|------|------|
| `packages/go/config/edge_types.go` | API-1 |
| `packages/go/config/load.go` | API-1 |
| `packages/go/config/workspace_config_test.go` | API-1 |
| `apps/edge/internal/node/store.go` | API-2 |
| `apps/edge/internal/node/store_test.go` | API-2 |
| `apps/edge/internal/configrefresh/classify.go` | API-2 |
| `apps/edge/internal/configrefresh/workspace_classify_test.go` | API-2 |
| `configs/edge.yaml` | API-2 |
| `agent-contract/inner/edge-config-runtime-refresh.md` | API-2 |
| `agent-spec/runtime/provider-pool-config-refresh.md` | API-2 |
| `agent-task/m-iop-owned-single-request-agent-execution/07+04_workspace_catalog/CODE_REVIEW-cloud-G07.md` | API-1, API-2 |

## Final Verification

1. `test -f agent-task/m-iop-owned-single-request-agent-execution/04+02_preset_refresh/complete.log || test "$(compgen -G 'agent-task/archive/*/*/m-iop-owned-single-request-agent-execution/04+02_preset_refresh/complete.log' | wc -l)" -eq 1`
2. `go test -race ./packages/go/config -run 'TestLoadEdgeWorkspaceCatalog' -count=1`
3. `go test -race ./apps/edge/internal/node ./apps/edge/internal/configrefresh -run 'Test(LoadFromConfig.*Workspace|NodeStore.*Workspace|ClassifyWorkspace)' -count=1`
4. `go test ./packages/go/config ./apps/edge/internal/node ./apps/edge/internal/configrefresh -count=1`
5. `go vet ./packages/go/config ./apps/edge/internal/node ./apps/edge/internal/configrefresh`
6. `rg --sort path -n 'workspace_ref|workspaces|restart_required|darwin' configs/edge.yaml agent-contract/inner/edge-config-runtime-refresh.md agent-spec/runtime/provider-pool-config-refresh.md`
7. `git diff --check`

Expected: exactly one predecessor completion exists; the catalog is fail-closed and immutable; refresh requires restart; all focused/package checks pass. Go test cache output is not acceptable because every test command uses `-count=1`.

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