<!-- task=m-iop-agent-chronos-extraction-decoupling/04+03_bundle_exporter plan=3 tag=REVIEW_REFACTOR milestone-task=transfer -->

# Repair owner-state provenance and workspace confinement

## For the Implementing Agent

Filling the implementation-owned sections of `CODE_REVIEW-cloud-G10.md` is mandatory. Implement only this follow-up scope, run every verification command, paste actual notes and stdout/stderr, keep the active pair in place, and report ready for review. If blocked, record the exact blocker, attempted commands/output, and resume condition only 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 belongs to the code-review agent.

## Background

The exporter passes its current unit, vet, race, and baseline checks, but the review found two contract violations. Owner mode aliases the same aggregate state-root bytes across resumable and quarantine records, and output confinement recognizes a shared workspace only through a basename or sibling-directory heuristic. Both defects are repository-fixable within the exporter and its tests.

## Archive Evidence Snapshot

- `agent-task/m-iop-agent-chronos-extraction-decoupling/04+03_bundle_exporter/plan_cloud_G10_2.log` and `code_review_cloud_G10_2.log` are the predicted archives for the failed plan-2 loop. The review has two Required findings: disjoint owner-state provenance/quarantine is missing, and shared-workspace output confinement is heuristic.
- Fresh reviewer verification passed the predecessor PASS check, manifest SHA check, pinned commit check, `go test -count=1`, `go vet`, `go test -count=1 -race`, ten repeated package runs, and `git diff --check`. `evidence_integrity_failure=false`; the failure is behavioral rather than a false verification claim.
- The `transfer` Milestone contribution remains S02 exporter evidence only. Actual current-input reconciliation, private owner export, isolated staging build, and final bundle closure remain downstream Task 06 responsibilities.

## Analysis

### Files Read

- `agent-roadmap/phase/automation-runtime-bridge/PHASE.md`
- `agent-roadmap/phase/automation-runtime-bridge/milestones/iop-agent-chronos-extraction-decoupling.md`
- `agent-roadmap/sdd/automation-runtime-bridge/iop-agent-chronos-extraction-decoupling/SDD.md`
- `agent-task/archive/2026/08/m-iop-agent-chronos-extraction-decoupling/03+02_inventory_closure/complete.log`
- `agent-task/m-iop-agent-chronos-extraction-decoupling/04+03_bundle_exporter/PLAN-cloud-G10.md`
- `agent-task/m-iop-agent-chronos-extraction-decoupling/04+03_bundle_exporter/CODE_REVIEW-cloud-G10.md`
- `agent-task/m-iop-agent-chronos-extraction-decoupling/04+03_bundle_exporter/plan_cloud_G10_0.log`
- `agent-task/m-iop-agent-chronos-extraction-decoupling/04+03_bundle_exporter/code_review_cloud_G10_0.log`
- `agent-task/m-iop-agent-chronos-extraction-decoupling/04+03_bundle_exporter/plan_cloud_G09_1.log`
- `agent-task/m-iop-agent-chronos-extraction-decoupling/04+03_bundle_exporter/code_review_cloud_G09_1.log`
- `cmd/iop-chronos-transfer/main.go`
- `cmd/iop-chronos-transfer/main_test.go`
- `packages/go/agentstate/store.go`
- `scripts/fixtures/iop-agent-chronos-ownership-manifest.tsv`
- `scripts/fixtures/iop-agent-chronos-transfer-state-v1.json`
- `scripts/fixtures/iop-agent-chronos-transfer-state-v1.schema.json`
- `agent-spec/runtime/iop-agent-cli-runtime.md`
- `agent-contract/inner/iop-agent-cli-runtime.md`

### SDD Criteria

- `agent-roadmap/sdd/automation-runtime-bridge/iop-agent-chronos-extraction-decoupling/SDD.md` is `[승인됨]` with its lock released; the exact first-line scope remains `milestone-task=transfer`.
- This follow-up targets S02. D01 and the S02 Evidence Map require versioned full-state provenance, non-resumable handling for broken or unauthenticated residual state, deterministic item/bundle digests, and an archive safe for later isolated staging.
- The checklist therefore requires each owner-state record to bind disjoint exact payload evidence and requires output confinement independent of checkout naming. Final verification remains local exporter evidence; it does not claim the downstream real export or staging closure.

### Verification Context

- No external handoff was supplied. Repository-native evidence is the active pair, approved SDD, exact predecessor completion log, current source/tests, state-store envelope implementation, and transfer fixtures.
- Local preflight: `go version` reported `go1.26.2 linux/arm64`; `go env GOMOD` reported `/config/workspace/iop-s1/go.mod`. No external provider, credential, daemon, device state, or `iop-agent` execution is required or permitted.
- Fresh reviewer commands all passed: exact predecessor PASS, manifest SHA `7026fedc8b0eb24eab5c4124d3818400f66db5f1d116a71e7781f346fbd256a7`, pinned commit availability with a different HEAD, package test, vet, race test, repeated package test, and `git diff --check`.
- The current manifest still has three rows absent from the pinned tree; Task 06 owns that input-provenance reconciliation. This follow-up must preserve the existing fail-closed behavior and must not add a worktree fallback.
- Confidence is high: both review defects are directly visible in current branches and have deterministic unit-test oracles.

### Test Coverage Gaps

- Existing owner-mode coverage checks counts and redaction but does not prove that a resumable logical record owns exact distinct bytes or that corrupt/unmapped bytes occur only in non-resumable quarantine.
- Existing workspace coverage uses a parent literally named `workspace`, so it does not exercise an arbitrary shared-workspace basename without a `chronos` sibling.
- Pinned-tree, missing-blob, traversal/symlink, archive determinism, live-writer/socket, receipt, state-index, and self-contained behavior coverage already exists and must remain passing.

### Symbol References

- No public or cross-package symbol is renamed. Changes are confined to exporter-internal owner-state projection and output-boundary helpers plus their tests.

### Split Judgment

- Keep one compact follow-up. The two fixes share the same exporter entry point, output receipt/archive invariants, and test fixture setup; splitting would duplicate final contract verification without producing a useful independent acceptance bundle.
- Encoded predecessor 03 is satisfied by `agent-task/archive/2026/08/m-iop-agent-chronos-extraction-decoupling/03+02_inventory_closure/complete.log`, whose Summary contains exactly one final PASS.

### Scope Rationale

- Do not modify the ownership manifest, tracked state fixture/schema, active SDD/spec/contract, runtime state store, Chronos repository, or Task 06 artifacts.
- Do not perform the real owner-state export, reconcile the three missing pinned-tree rows, add a worktree fallback, remove IOP source, or change the exact 12-key receipt.
- Preserve the existing fixed-revision Git object reader, deterministic tar/gzip metadata, external two-file output contract, and synthetic fixture behavior.

### Final Routing

- `evaluation_mode=isolated-reassessment`; finalizer=`finalize-task-policy.sh`; mode=`pair`.
- Build closures are all true. Scores=`2,2,2,2,2`, grade=`G10`, base/final route=`grade-boundary`, lane=`cloud`.
- Review closures are all true. Scores=`2,2,2,2,2`, route=`official-review`, lane=`cloud`, grade=`G10`.
- `large_indivisible_context=false`; matched loop risks=`temporal_state, concurrent_consistency, boundary_contract, structured_interpretation` (4); `review_rework_count=1`; `evidence_integrity_failure=false`; no capability gap.
- Canonical files: `PLAN-cloud-G10.md`, `CODE_REVIEW-cloud-G10.md`.

## Implementation Checklist

- [ ] Replace aggregate owner-state aliases with exact, disjoint logical payload provenance; only invalid or unmapped residual bytes may enter non-resumable quarantine.
- [ ] Enforce output confinement against the canonical workspace boundary without basename or sibling-existence heuristics.
- [ ] Add and run focused owner-state and arbitrary-workspace regressions, then run the fresh full package, race, vet, baseline, and formatting verification.
- [ ] Fill implementation-owned sections in CODE_REVIEW-*-G??.md with actual implementation notes and verification output.

### [REVIEW_REFACTOR-1] Bind owner-state records to disjoint payload evidence

**Problem**

`cmd/iop-chronos-transfer/main.go:725-750` derives one digest per broad root and `cmd/iop-chronos-transfer/main.go:974-988` maps manager checkpoint, leases, ledger, recovery locators, registrations, and broken residuals to the same `state-root` digest. The same bytes are consequently attested both resumable and quarantined, and arbitrary state-root files pass the owner-mode success test without an ownership or validity classification.

```go
// before: cmd/iop-chronos-transfer/main.go:729
role := roleForLogicalID(id)
digest := digests[role]
record.Resumable = true
```

**Solution**

Build a canonical owner-state projection in memory after the two snapshots match. Each logical record must reference one exact regular archive payload member and derive its digest and size from that member's bytes. Partition known valid config, state-envelope/integration-record, log, overlay, cache, and temp inputs by stable runtime identity; unknown, invalid, or unowned residual bytes must be emitted only through the broken-residual non-resumable quarantine payload and must not be reused by a resumable record. Fail closed if a required logical record cannot be proven, if one payload is assigned to both sets, or if the state index path/digest/size does not match the emitted member. Keep host paths and raw bytes out of the canonical index and receipt.

```go
// after
payloads, err := projectOwnerState(snapshot)
if err != nil { return stateExport{}, nil, err }
records, entries, err := bindLogicalPayloads(payloads)
// every record digest/size is computed from its one emitted member;
// resumable and quarantine membership is disjoint.
```

**Modified Files and Checklist**

- [ ] `cmd/iop-chronos-transfer/main.go` — replace broad-root aliases with exact logical payload binding and disjoint invalid-residual quarantine.
- [ ] `cmd/iop-chronos-transfer/main_test.go` — seed valid and corrupt owner inputs and assert exact member/digest/size binding plus disjoint resumable/quarantine membership.

**Test Strategy**

- Add `TestOwnerStatePayloadsAreExactAndDisjoint` to verify that each owner record resolves to one emitted regular member whose bytes match its digest and size, with no member shared across logical IDs.
- Add `TestInvalidOwnerResidualIsOnlyNonResumableQuarantine` to mix valid and corrupt/unmapped owner bytes and prove the invalid bytes are absent from every resumable payload and present in quarantine with `resumable=false`.
- Preserve `TestExportOwnerStateSnapshotIsRedactedAndQuiesced`, live-writer/socket tests, canonical index facts, and synthetic fixture tests.

**Verification**

- `go test -count=1 ./cmd/iop-chronos-transfer -run 'TestOwnerStatePayloadsAreExactAndDisjoint|TestInvalidOwnerResidualIsOnlyNonResumableQuarantine|TestExportOwnerStateSnapshotIsRedactedAndQuiesced|TestRefusesConcurrentStateMutation|TestRefusesLiveStateSocket'` — fresh focused owner-state tests PASS.

### [REVIEW_REFACTOR-2] Remove workspace-name heuristics from output confinement

**Problem**

`cmd/iop-chronos-transfer/main.go:332-337` treats the repository parent as a shared workspace only when its basename is `workspace` or a `chronos` sibling already exists. Before that sibling exists, `/srv/product/iop` can export to `/srv/product/private-output`, even though the plan requires output outside the shared workspace.

```go
// before: cmd/iop-chronos-transfer/main.go:332
if filepath.Base(workspaceRoot) == "workspace" || directoryExists(filepath.Join(workspaceRoot, "chronos")) {
    if pathWithin(value, workspaceRoot) { return "", errUnsafeWorkspace }
}
```

**Solution**

Use the canonical parent of the verified Git top-level as the workspace boundary for this transfer command and reject every prospective output beneath it, regardless of directory names or current sibling existence. Keep the existing non-existing clean absolute path, symlink-free owned parent, source-repository, and other-Git-checkout checks. Remove the now-unused heuristic helper if no caller remains.

```go
// after
workspaceRoot := filepath.Dir(repoRoot)
if pathWithin(value, workspaceRoot) {
    return "", errors.New("output must be outside the shared workspace")
}
```

**Modified Files and Checklist**

- [ ] `cmd/iop-chronos-transfer/main.go` — enforce the canonical workspace boundary unconditionally and remove stale heuristic code.
- [ ] `cmd/iop-chronos-transfer/main_test.go` — cover an arbitrary workspace basename with no Chronos sibling and retain an allowed outside-workspace success case.

**Test Strategy**

- Add `TestRejectsOutputInsideArbitrarilyNamedWorkspace` using a shared root not named `workspace` and with no `chronos` child.
- Keep `TestRejectsOutputInsideWorkspace` and normal external-output success coverage.

**Verification**

- `go test -count=1 ./cmd/iop-chronos-transfer -run 'TestRejectsOutputInsideWorkspace|TestRejectsOutputInsideArbitrarilyNamedWorkspace|TestOutputContractAndReceiptSchema'` — fresh confinement/output tests PASS.

## Modified Files Summary

| File | Item |
|------|------|
| `cmd/iop-chronos-transfer/main.go` | REVIEW_REFACTOR-1, REVIEW_REFACTOR-2 |
| `cmd/iop-chronos-transfer/main_test.go` | REVIEW_REFACTOR-1, REVIEW_REFACTOR-2 |
| `agent-task/m-iop-agent-chronos-extraction-decoupling/04+03_bundle_exporter/CODE_REVIEW-cloud-G10.md` | REVIEW_REFACTOR-1, REVIEW_REFACTOR-2 |

## Final Verification

1. `bash -lc 'set -euo pipefail; mapfile -t predecessor_logs < <({ test ! -f agent-task/m-iop-agent-chronos-extraction-decoupling/03+02_inventory_closure/complete.log || printf "%s\n" agent-task/m-iop-agent-chronos-extraction-decoupling/03+02_inventory_closure/complete.log; find agent-task/archive -path "*/m-iop-agent-chronos-extraction-decoupling/03+02_inventory_closure/complete.log" -type f -print 2>/dev/null; } | sort -u); test "${#predecessor_logs[@]}" -eq 1; awk '\''BEGIN {in_summary=0; count=0} /^## Summary$/ {in_summary=1; next} in_summary && /^## / {in_summary=0} in_summary {line=$0; while (match(line, /final verdict: PASS/)) {count++; line=substr(line, RSTART+RLENGTH)}} END {exit count==1 ? 0 : 1}'\'' "${predecessor_logs[0]}"'` — exactly one predecessor completion has exactly one final PASS in Summary.
2. `test "$(sha256sum scripts/fixtures/iop-agent-chronos-ownership-manifest.tsv | awk '{print $1}')" = 7026fedc8b0eb24eab5c4124d3818400f66db5f1d116a71e7781f346fbd256a7` — the current-byte manifest baseline remains unchanged.
3. `git cat-file -e 3155be0e275437a8eedc1aa93497955a7d30465b^{commit} && test "$(git rev-parse HEAD)" != 3155be0e275437a8eedc1aa93497955a7d30465b` — the pinned commit remains available and distinct from HEAD.
4. `go test -count=1 ./cmd/iop-chronos-transfer` — the complete fresh exporter suite PASSes.
5. `go test -count=1 -race ./cmd/iop-chronos-transfer` — the fresh exporter race suite PASSes.
6. `go vet ./cmd/iop-chronos-transfer` — no findings.
7. `git diff --check` — no whitespace errors.

All Go tests require fresh output. Run commands from the repository root under Bash; no external provider, credential, device state, daemon, or `iop-agent` execution is used.

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