<!-- task=m-iop-agent-cli-runtime/11+06_workspace_overlay plan=2 tag=REVIEW_API -->

# Close Metadata and Dispatch Confinement Gaps

## For the Implementing Agent

Implement the two review fixes, run every verification command, and fill the implementation-owned sections of `CODE_REVIEW-cloud-G10.md` with actual notes and stdout/stderr. Keep both active files in place and report ready for review. If blocked, record only the exact blocker, attempted commands/output, and resume condition in implementation-owned evidence fields. Do not ask the user, call user-input tools, create control-plane stop files, classify the next state, archive logs, or write `complete.log`; finalization is owned by the code-review skill.

## Background

The executable proof now denies ordinary opens outside the task roots and retained overlays are identity-bound. The second official review found two remaining S18 gaps: preferred Landlock does not deny protected metadata mutations, and `agenttask.Manager` delegates process start to an invoker that is not structurally required to call the proof. Both must close before the overlay task can claim canonical-unchanged evidence.

## Archive Evidence Snapshot

- Prior task: `agent-task/m-iop-agent-cli-runtime/11+06_workspace_overlay`
- Prior plan: `agent-task/m-iop-agent-cli-runtime/11+06_workspace_overlay/plan_cloud_G10_1.log`
- Prior review: `agent-task/m-iop-agent-cli-runtime/11+06_workspace_overlay/code_review_cloud_G10_1.log`
- Verdict: `FAIL`
- Required findings:
  - `packages/go/agentworkspace/confinement_linux.go:41` prefers Landlock although it does not restrict protected `chmod`, `chown`, `setxattr`, or `utime` operations.
  - `packages/go/agenttask/dispatch.go:147` passes the proof to arbitrary `ProviderInvoker.Start`; no production call site starts the provider through `InvocationConfinement.Start`.
- Suggested findings: None.
- Nit findings: None.
- Affected files: Linux confinement selection/probes, Manager/provider launch ports and fixtures, S18 tests, and the two runtime contracts.
- Fresh verification: focused overlay, retained-identity, guard/task target, overlay race, vet, formatting, Darwin cross-compile, and diff checks passed. A package-wide run later failed only in concurrently added `agentpolicy` tests outside this task; the target packages remained green.
- Roadmap carryover: S18 / `overlay-workspace` remains incomplete until all protected mutations are denied through the actual Manager launch path.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/automation-runtime-bridge/milestones/iop-agent-cli-runtime.md`
- Milestone link: [Milestone document](agent-roadmap/phase/automation-runtime-bridge/milestones/iop-agent-cli-runtime.md)
- Task ids:
  - `overlay-workspace`: pinned base and task writable layer
- Completion mode: check-on-pass

## Analysis

### Files Read

- `go.mod`
- `packages/go/agentworkspace/confinement.go`
- `packages/go/agentworkspace/confinement_linux.go`
- `packages/go/agentworkspace/confinement_darwin.go`
- `packages/go/agentworkspace/confinement_unsupported.go`
- `packages/go/agentworkspace/confinement_test.go`
- `packages/go/agentworkspace/overlay.go`
- `packages/go/agentworkspace/overlay_test.go`
- `packages/go/agentworkspace/snapshot.go`
- `packages/go/agentguard/types.go`
- `packages/go/agentguard/canonical.go`
- `packages/go/agentguard/permit.go`
- `packages/go/agentguard/admission_integration_test.go`
- `packages/go/agenttask/ports.go`
- `packages/go/agenttask/dispatch.go`
- `packages/go/agenttask/confinement_dispatch_test.go`
- `packages/go/agenttask/test_support_test.go`
- `packages/go/agenttask/manager_test.go`
- `agent-contract/index.md`
- `agent-contract/inner/agent-runtime.md`
- `agent-contract/inner/iop-agent-cli-runtime.md`
- `agent-roadmap/phase/automation-runtime-bridge/milestones/iop-agent-cli-runtime.md`
- `agent-roadmap/sdd/automation-runtime-bridge/iop-agent-cli-runtime/SDD.md`
- `agent-task/m-iop-agent-cli-runtime/11+06_workspace_overlay/plan_cloud_G10_1.log`
- `agent-task/m-iop-agent-cli-runtime/11+06_workspace_overlay/code_review_cloud_G10_1.log`
- `agent-test/local/rules.md`
- `agent-test/local/platform-common-smoke.md`

### SDD Criteria

- SDD: `agent-roadmap/sdd/automation-runtime-bridge/iop-agent-cli-runtime/SDD.md`
- Status: approved; SDD lock: released.
- Target: S18 / `overlay-workspace`.
- Acceptance: concurrent unattended tasks use one pinned dirty base and independent view/temp/cache roots while canonical files, shared Git metadata, snapshots, and sibling layers remain unchanged.
- Evidence Map: dirty/untracked/mode/symlink fingerprint, identical concurrent bases, real absolute-path denial, Git/temp/cache isolation, and canonical-unchanged traces.
- Plan consequence: the child regression must include metadata mutation, and the Manager path must own the exact proof-backed start rather than trusting an invoker convention.

### Test Environment Rules

- `test_env=local`.
- `agent-test/local/rules.md` was present and read.
- Matched profile: `agent-test/local/platform-common-smoke.md`.
- Applied commands: host Go identity, fresh target tests, repetition/race, package-wide regression, vet, formatting, Darwin cross-compile, deterministic symbol search, and `git diff --check`.
- `make proto` is not required because no protobuf source changes.
- The checkout is shared with active sibling work. Unrelated package failures must be reported with exact output and must not be fixed in this task, but the final target packages must pass from the final source state.

### Test Coverage Gaps

- The Linux probe and child test cover file creation/open denial but not mode, timestamp, ownership, or xattr mutation. Add protected metadata attempts and unchanged evidence.
- The real child test calls `Confinement.Start` directly. Add a Manager-level launch-port regression proving the Manager invokes the exact proof before binding a `ProviderInvocation`.
- Retained root/config/grant rebinding coverage is complete and must remain unchanged.

### Symbol References

- `ProviderInvoker.Start`: implemented only by `fakeInvoker.Start` and called by `Manager.runWork`.
- `InvocationConfinement.Start`: implemented by platform proof/fakes; current call sites are tests only.
- Update every `ProviderInvoker` implementation and `m.invoker.Start` reference found by `rg --sort path 'ProviderInvoker|\\.invoker\\.Start|Confinement\\.Start' packages/go`.

### Split Judgment

Keep one plan. Metadata-complete OS enforcement and a mandatory proof-owned dispatch start are two halves of the same S18 invariant; either can still mutate the canonical base if shipped alone.

### Scope Rationale

- Include only confinement backend selection/probes, provider launch ownership, direct call-site fixtures, and matching contracts/tests.
- Exclude `agentpolicy`, target selection, quota/failover, recovery, integration/change-set behavior, local control, client processes, and CLI surface.
- Preserve concurrent sibling changes in shared `agenttask` files and edit only the exact launch seam and affected fixtures.
- Add no dependency and no protobuf change.

### Final Routing

- `evaluation_mode=isolated-reassessment`
- `finalizer=finalize-task-policy.sh`, `finalizer_mode=pair`
- Build closures: `scope_closed=true`, `context_closed=true`, `verification_closed=true`, `evidence_trusted=true`, `ownership_closed=true`, `decision_closed=true`.
- Review closures: `scope_closed=true`, `context_closed=true`, `verification_closed=true`, `evidence_trusted=true`, `ownership_closed=true`, `decision_closed=true`.
- Build grade scores: scope=2, state=2, blast=2, evidence=2, verification=2; grade `G10`.
- Review grade scores: scope=2, state=2, blast=2, evidence=2, verification=2; grade `G10`.
- Build base/route basis: `grade-boundary`; lane `cloud`; filename `PLAN-cloud-G10.md`.
- Review route basis: `official-review`; lane `cloud`; filename `CODE_REVIEW-cloud-G10.md`; adapter `codex`, model `gpt-5.6-sol`, reasoning `xhigh`.
- `large_indivisible_context=false`.
- Positive loop risks: `concurrent_consistency`, `boundary_contract`, `variant_product`; count=3.
- Recovery signals: `review_rework_count=2`, `evidence_integrity_failure=false`; recovery boundary matched but does not replace the grade-boundary basis.
- Capability gap: none.

## Implementation Checklist

- [ ] Replace incomplete Landlock preference with a metadata-complete fail-closed Linux policy and add protected metadata mutation probes/tests.
- [ ] Move provider process start under a mandatory Manager-owned `InvocationConfinement.Start` boundary and add launch-order/identity regressions.
- [ ] Synchronize runtime contracts and run fresh target, repetition, race, package-wide, cross-platform compile, vet, formatting, search, and diff verification.
- [ ] Fill implementation-owned sections in CODE_REVIEW-*-G??.md with actual implementation notes and verification output.

### [REVIEW_API-1] Make Linux confinement metadata-complete

**Problem:** `packages/go/agentworkspace/confinement_linux.go:41-60` selects Landlock first after a `touch` probe. Landlock ABI 3 restricts write-open/truncate/remove/create/refer operations, but it does not restrict `chmod`, `chown`, `setxattr`, or `utime`; S18 fingerprints mode/symlink identity and requires protected trees to remain unchanged.

**Solution:** admit only a Linux backend that makes the filesystem recursively read-only outside the exact view/temp/cache bind mounts. Remove Landlock as an admissible fallback until it can enforce the complete mutation set; if the unprivileged mount namespace cannot be installed, return `ErrConfinementUnavailable`. Extend the runtime probe and helper regression beyond `touch`:

```go
// Before: an open/write-only probe can select Landlock.
if abi >= 3 && probeLinuxConfinement("landlock") == nil {
    linuxConfinementBackend = "landlock"
}

// After: exact read-only metadata semantics are required.
if err := probeLinuxConfinement("mountns"); err != nil {
    return ErrConfinementUnavailable
}
linuxConfinementBackend = "mountns"
```

**Modified Files and Checklist:**

- [ ] `packages/go/agentworkspace/confinement_linux.go`: remove Landlock admission/dead policy code, require the probed mount-namespace policy, and probe both ordinary writes and protected metadata changes.
- [ ] `packages/go/agentworkspace/confinement_test.go`: assert the selected Linux identity represents the metadata-complete policy and fail-closed behavior.
- [ ] `packages/go/agentworkspace/overlay_test.go`: attempt protected `chmod` and timestamp/xattr mutation where supported, assert denial, and verify modes/content/snapshot/Git evidence remain unchanged.
- [ ] `agent-contract/inner/iop-agent-cli-runtime.md`: describe only the backend that enforces the complete S18 guarantee.
- [ ] `agent-contract/inner/agent-runtime.md`: state that executable confinement includes protected metadata mutations, not only write-open denial.

**Test Strategy:** extend `TestOverlayBackendConfinementDeniesActualAbsoluteWrites` and the platform probe. View/temp/cache metadata changes must succeed; canonical, sibling, snapshot, overlay-record, and shared-Git metadata changes must fail without altering captured evidence.

**Verification:**

```bash
go test -count=1 -run '^(TestConfinement|TestOverlayBackendConfinement)' -v ./packages/go/agentworkspace
```

Expected: the selected Linux policy denies both content and metadata mutations outside the three writable roots.

### [REVIEW_API-2] Make the Manager own the confined child start

**Problem:** `packages/go/agenttask/dispatch.go:147-166` validates the proof and then calls arbitrary `ProviderInvoker.Start`. The invoker can ignore `DispatchRequest.Confinement`; no production call site invokes `Confinement.Start`, and the existing manager fake simulates completion without starting a confined child.

**Solution:** split provider launch construction from process start. Replace `ProviderInvoker.Start` with a side-effect-free preparation port that returns an identity-bound launch object containing the `ConfinementCommand` and a method that binds the already-started `*exec.Cmd` into `ProviderInvocation`. Inside the permit callback, the Manager must prepare the launch, call the exact validated proof's `Start`, then bind the returned process. On bind failure it must terminate/reap the child and return a typed invocation failure.

```go
type ProviderLaunch interface {
    Command() ConfinementCommand
    BindStarted(*exec.Cmd) (ProviderInvocation, error)
}

type ProviderInvoker interface {
    Prepare(context.Context, DispatchRequest) (ProviderLaunch, error)
}
```

**Modified Files and Checklist:**

- [ ] `packages/go/agenttask/ports.go`: replace the raw-start port with the prepared-launch/bind contract and document that preparation has no process side effects.
- [ ] `packages/go/agenttask/dispatch.go`: validate the exact proof, obtain the launch, start it through `InvocationConfinement.Start`, bind the returned process, and clean up every partial-start error path.
- [ ] `packages/go/agenttask/test_support_test.go`: adapt the strict fake launch/invocation while preserving concurrent sibling fixture behavior.
- [ ] `packages/go/agenttask/confinement_dispatch_test.go`: prove one exact proof start occurs before invocation binding; cover missing, rebound, prepare failure, proof-start failure, and bind failure with zero leaked child/accepted submission.
- [ ] `packages/go/agentworkspace/overlay_test.go`: keep the real OS child denial evidence compatible with the Manager-owned launch contract.
- [ ] `agent-contract/inner/agent-runtime.md`: synchronize the strict launch ordering and cleanup contract.

**Test Strategy:** use a spy `InvocationConfinement` and prepared launch in `confinement_dispatch_test.go` to assert ordering and exact command identity. Keep the real `agentworkspace` helper as the OS-enforcement oracle; together the tests prove the Manager cannot bypass the proof and the proof enforces the roots.

**Verification:**

```bash
go test -count=1 -run '^(TestValidatePreparedIsolation|TestManager.*Confinement|TestDispatch.*Confinement)' -v ./packages/go/agenttask
go test -count=1 ./packages/go/agentguard ./packages/go/agenttask
```

Expected: every intended provider child is started exactly once through the validated proof, and all partial failures remain invocation-free or are terminated/reaped.

## Modified Files Summary

| File | Item |
|---|---|
| `packages/go/agentworkspace/confinement_linux.go` | REVIEW_API-1 |
| `packages/go/agentworkspace/confinement_test.go` | REVIEW_API-1 |
| `packages/go/agentworkspace/overlay_test.go` | REVIEW_API-1, REVIEW_API-2 |
| `packages/go/agenttask/ports.go` | REVIEW_API-2 |
| `packages/go/agenttask/dispatch.go` | REVIEW_API-2 |
| `packages/go/agenttask/test_support_test.go` | REVIEW_API-2 |
| `packages/go/agenttask/confinement_dispatch_test.go` | REVIEW_API-2 |
| `agent-contract/inner/agent-runtime.md` | REVIEW_API-1, REVIEW_API-2 |
| `agent-contract/inner/iop-agent-cli-runtime.md` | REVIEW_API-1 |

## Dependencies and Execution Order

Implement the launch-port change and Linux policy change together before final verification. Preserve the already completed retained-identity fix.

## Final Verification

```bash
command -v go && readlink -f "$(command -v go)" && go version && go env GOROOT
go test -count=1 -run '^(TestConfinement|TestOverlayBackendConfinement)' -v ./packages/go/agentworkspace
go test -count=1 -run '^(TestValidatePreparedIsolation|TestManager.*Confinement|TestDispatch.*Confinement)' -v ./packages/go/agenttask
go test -count=1 ./packages/go/agentworkspace ./packages/go/agentguard ./packages/go/agenttask
go test -count=20 ./packages/go/agentworkspace
go test -count=1 -race ./packages/go/agentworkspace ./packages/go/agenttask
go test -count=1 ./packages/go/...
tmp_build_dir=$(mktemp -d)
CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go test -c -o "$tmp_build_dir/agentworkspace.test" ./packages/go/agentworkspace
go vet ./packages/go/agentworkspace ./packages/go/agentguard ./packages/go/agenttask
gofmt -d packages/go/agentworkspace packages/go/agentguard packages/go/agenttask
rg --sort path 'ProviderInvoker|\.invoker\.(Start|Prepare)|Confinement\.Start' packages/go
git diff --check
```

Expected: all commands pass; the deterministic search shows the Manager-owned production `Confinement.Start` call and no raw invoker start path. Cached test output is not acceptable for the listed `-count=1` commands.

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