@@ -0,0 +1,221 @@
# Versioned Dependency Nested-Sandbox Amendment Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal: ** 让版本化依赖迁移的最终候选同时具备隔离组合门禁 A 与无凭据原生沙箱重放门禁 B,覆盖 Dev、两个 Backend 来源及 RDM,并把两门禁的缺项作为验收与合入阻塞。
**Architecture: ** A 保留本地 Bubblewrap 和 Dev CI 无 socket 子容器,对确需再建 namespace 的用例按精确名称排除、核对并公开 `NOT_RUN_IN_A` ;RDM CI 候选步骤也进入无 socket 子容器。B 由受信获取器导出精确源码及 vendor 包,交给无凭据、无 socket 的短生命周期 QEMU/KVM VM,在原生 Bubblewrap 下离线逐用例重放;协调器核对两份证据的源码与用例映射。
**Tech Stack: ** Python 3 标准库、Go 1.27.1 的 `go test -json` /`-skip` 、Git、Bubblewrap、Docker、QEMU/KVM、Ubuntu 24.04 cloud image、Gitea Actions、现有 `make evidence` 。
**Spec: ** [用户批准的书面修订设计 r1 ](2026-10-14-versioned-dependency-nested-sandbox-amendment.md ), Documents `1790cf8f28cd2309dc542b63960379fd6a0405c6` ; [原整改设计 ](2026-10-14-versioned-dependency-review-remediation.md )与[原实施计划 ](2026-10-14-versioned-dependency-review-remediation-plan.md )继续约束 Task 1– 4 和最终 Task 6。
**Authority and current state: ** 用户 2026-10-14 仅批准书面修订设计并要求编制本计划,尚未批准本计划或实施。Dev `1ea93a5` 的 `make test` 为 FAIL, `make build` 在直接 npm 注册表环境通过;RDM `d8a7098` 的产品构建和旧 run3130 成功,但无 RDM CI 候选 socket 负例。B 为 `NOT_RUN` 。Dev、Docs、RDM 现有迁移 PR 保持 WIP;本计划不得把旧运行升级为最终来源图的证据。
## Global Constraints
- 不关闭 Dev 本地 Bubblewrap、不让 Dev/RDM 候选命令在有 Docker socket/凭据的 CI 父步骤执行;无 `--privileged` 、host AppArmor/共享 runner/保护规则修改或凭据扩权。
- A 的精确排除只适用于实际会再创建 namespace 的用例;在报告中写完整来源 SHA、包、命令和 `NOT_RUN_IN_A` 清单。未知/新增用例、非预期 Skip、其他失败均不换绿。
- B 的 VM 不继承宿主 HOME、SSH agent、token、Docker socket 或父 CI 文件系统;正向证明 `bwrap` 启动、负向证明假秘密不可见;命名用例必须全部实际 PASS、零 Skip。
- Dev 应用 Backend 与 RDM Go 图 Backend 分别按最终精确 SHA 重放,即使两者测试名相同也不互借结果。新提供方 SHA 使旧 A/B 证据失效。
- 源码获取、模块预取、镜像创建属受信控制面;候选代码只在 A 或 B 的执行面运行。控制面仍须 Review,本计划不声称平台阻止恶意 PR 修改工作流。
- 继续使用已有登记 worktree 和任务卡;Dev 任务中途不重复 precheck。新仓/新登记任务首次写操作前按该仓门禁启动。证据经 `CMD="..." make evidence` 写入,固定后用源码外 `RECEIPT` ,来源等级不自升。
- Backend 的 `python-docx` 失败须单独修复并重跑;A/B 不能遮盖它。阶段 7 人工验收、独立 Stage 8 Review、当前 HEAD GATE、#518 队列窗口和原计划 Task 6 仍为后续必经步骤。
## Review Focus
| 最容易漏掉的输入或故障 | 所属任务的钉住测试 |
| --- | --- |
| `bwrap` 可找到但立即退出,秘密不可读测试却误判成功 | Task 1 用假 `bwrap` 非零退出重放负例;Task 5 VM 正向探针 |
| A 排除正则误伤普通 `TestSandbox*` 或新用例自然 Skip | Task 2 的精确清单、`go test -list/-json` 与新增/缺失/Skip 负例 |
| Python 模块只在宿主 `~/.local` ,本地外层沙箱无法导入 | Task 3 的空 HOME、隔离 wheelhouse 及 Backend `TestLintErrorContinues` |
| RDM 获取步骤结束后 `make test` 仍在有 socket 的父容器执行 | Task 4 的工作流扫描、真实父子卷/socket 探针与失败关闭 |
| B 的源码包夹带 token、Go 图漂移或某个用例被跳过 | Task 5 的 bundle 污染/篡改/错 SHA/Skip 负例;Task 6 一一映射 |
## File map and interfaces
| 单元 | 文件 | 责任 |
| --- | --- | --- |
| 提供方真实断言 | Backend `internal/tools/exec_backend_test.go` ; RDM `internal/rdmapp/rdm_exec_isolation_test.go` | 对必须启动沙箱的负例加入启动成功信号;不改产品执行后端。 |
| A 清单及汇总 | Dev `scripts/nested_sandbox_gate.py` 、`policy/nested-sandbox-tests.json` 、`scripts/build-composition.py` 、`scripts/component-check.py` 、`tests/test_nested_sandbox_gate.py` 、`tests/test_composition.py` 、`tests/test_component_checks.py` | `run_a(source: Path, component: str, commit: str, mode: str, executor) -> dict` 发现精确测试、运行其余 Go 测试并返回可核验报告;`compose` 与 `test-backend-pg` 均公开报告。 |
| 本地 Python 测试工具 | Dev `scripts/python-test-tools.lock` 、`scripts/python_test_tools.py` 、`scripts/build-composition.py` 、`tests/test_python_test_tools.py` ; Backend `internal/runtime/llm_verify_test.go` | `prepare_wheels(root: Path, env: dict) -> Path` 在受信阶段下载固定 wheel; `install_for_candidate(executor, wheel_dir: Path, work_root: Path, env: dict) -> dict` 在已隔离环境离线安装,Backend 测试保留已有 `PYTHONPATH` 。 |
| RDM CI 子容器 | RDM `scripts/ci_candidate_executor.py` 、`scripts/nested_sandbox_gate.py` 、`.gitea/workflows/ci.yml` 、`Makefile` 、`tests/test_ci_candidate_executor.py` | `run_ci_job("build-and-test")` 验证固定 HEAD/卷与子容器负例后运行全部候选命令;RDM A 包装器执行原 `make test` 的 build/web/go/governance 实质检查并公开排除。 |
| B 来源包与 VM | Dev `scripts/prepare_sandbox_replay.py` 、`scripts/replay_vm.py` 、`scripts/replay-vm.lock.json` 、`tests/test_sandbox_replay.py` 、`tests/test_replay_vm.py` | `prepare_bundle(dev: Path, rdm: Path, output: Path) -> Path` 输出无凭据固定来源包;`probe_vm(lock: Path) -> dict` 验证 VM; `run_vm(bundle: Path, lock: Path, output: Path) -> dict` 输出逐用例报告。 |
| 双门禁核对和说明 | Dev `scripts/reconcile_sandbox_gates.py` 、`tests/test_reconcile_sandbox_gates.py` 、`PROJECT.md` 、`policy/dependencies.md` 、任务卡;Docs 本计划及索引 | `reconcile(a_reports: list[dict], b_report: dict, sources: dict, gates: list[dict]) -> dict` 拒绝缺口;只在全部门禁通过后整理阶段 7 材料。 |
接口冻结:A/B 报告 schema 1 均含 `lane` 、`source_sha` 、`package` 、`command` 、`tests` (每项 `name/status` )、`exit_code` 和输出摘要;A 的排除项状态为 `NOT_RUN_IN_A` , B 只接受 `PASS` 。环境探针另含 `vm_image_sha256` 、`helper_image_id` 、`no_socket` 、`no_credentials` 、`bwrap_started` 。字段缺失/重复/未知值即失败。计划执行时先核对最终来源图;这里的旧 SHA 只作为诊断基线。
---
### Task 1: Make false-pass sandbox tests fail when the inner sandbox never starts
**Files: ** Backend 修改 `internal/tools/exec_backend_test.go` ; RDM 修改 `internal/rdmapp/rdm_exec_isolation_test.go` 。分别在现有迁移 feature 分支的登记 worktree 实施,不编辑 Dev `.workspace/` 的 detached 固定检出。
**Interfaces: ** 消费现有 `sandboxRun` 、`sandboxBackend.Start` 和 `requireBwrap` ;不改公开产品 API。产出可在 B 上可靠判定的测试,Backend 新 SHA 由后续 Dev 来源锁采纳;RDM 新 SHA 由后续组合采纳。
- [ ] **Step 1 — 红灯:假 `bwrap` 失败不能使负例通过。 ** 受信获取阶段把固定 Backend/RDM 源码和 Go 模块缓存放进本次 `CandidateExecutor` 工作根;在该根的 `fake/bin/bwrap` 写入 `#!/bin/sh` 、`exit 1` 。候选命令仍经外层 Bubblewrap,进入后才把 `fake/bin` 前置 PATH,分别运行 Backend `go test -run '^(TestSandboxHomeHidden|TestSandboxSiblingSessionUnreadable)$' -count=1 ./internal/tools` 和 RDM `go test -run '^(TestRDMExecIsolation_HomeSecretHidden|TestRDMExecIsolation_SiblingSessionUnreadable|TestRDMExecIsolation_CrossProjectUnreadable|TestRDMExecIsolation_CancelKillsSandbox)$' -count=1 ./internal/rdmapp` 。先保存真实 FAIL/PASS 输出;现有可能误通过的项须在修改后变为失败。不得在未隔离的宿主上直接运行提供方 `go test` ;清理只删除本次临时假二进制。
- [ ] **Step 2 — 最小修正。 ** 在隔离脚本尾端输出不可由 `bwrap` 启动失败产生的固定 `SANDBOX_SCRIPT_RAN` ;对读不到秘密的测试同时断言该信号和预期脚本退出码。Backend sibling 测试须确认内层脚本运行后才接受 `cat` 拒绝;RDM cancel 测试在 `cancel()` 前确认进程仍存活并已写启动信号,立即退出 1 不能冒充成功。示例断言:
``` go
if state != "exit=7" || ! strings . Contains ( out , "SANDBOX_SCRIPT_RAN" ) {
t . Fatalf ( "sandbox script did not run: state=%s out=%q" , state , out )
}
```
- [ ] **Step 3 — 重跑真假两组。 ** 假 `bwrap` 必须使上述测试非零;能运行原生 `bwrap` 的环境中,相同用例必须真实 PASS。若本机环境无法满足正例,记 `NOT_RUN` 并留给 Task 5 的独立 VM;不能把仅通过假二进制负例当完成。分别提交 Backend、RDM 变更,任务卡写 SELF 收据和新的完整 SHA。
### Task 2: Account for exact nested tests in gate A
**Files: ** Dev 新建 `scripts/nested_sandbox_gate.py` 、`policy/nested-sandbox-tests.json` 、`tests/test_nested_sandbox_gate.py` ;修改 `scripts/build-composition.py` 、`scripts/component-check.py` 、`tests/test_composition.py` 、`tests/test_component_checks.py` 。RDM 新建 `scripts/nested_sandbox_gate.py` ,在 Task 4 的 CI 子容器中调用。
**Interfaces: ** Dev `run_a(source: Path, component: str, commit: str, mode: str, executor) -> dict` ;RDM 包装器复用同一报告字段,但只处理 RDM 自身 `internal/rdmapp` 。`policy/nested-sandbox-tests.json` 按完整来源 SHA 锁定 Backend `internal/tools` 的十项和 RDM `internal/rdmapp` 的五项;完整 SHA 由最终组合现场填入并核验。
- [ ] **Step 1 — 红灯单测。 ** 用假 Go `-list` /`-json` 输出覆盖“十项恰好存在”“`TestSandboxOrphanProbeScopesScript` 仍运行”“缺一项”“多一个运行时 `SKIP` ”“返回码非零”“来源 SHA 改变”和“空 JSON 流”。预期前两项 PASS,后五项拒绝;RDM 五项也做同样边界。`tests/test_component_checks.py` 增加 `test-backend-pg` 从普通 `go test` 改走 A 报告的断言,确保 CI `test-pg` 不再依赖自然 Skip。先经 `CMD='PYTHONDONTWRITEBYTECODE=1 python3 -m unittest tests.test_nested_sandbox_gate tests.test_component_checks -v' make evidence` 留 SELF 红灯。
- [ ] **Step 2 — 实现固定清单解析与精确 `-skip`。 ** 先用 `go test -list '^Test'` 对受影响包发现用例,再用下方构造的 `command` 运行。`go test` 与发现命令都通过 `CandidateExecutor.run` 执行;解析 JSON 时拒绝命名嵌套项被当 PASS/SKIP、清单项缺失、未清点的运行时 Skip、非零退出。正则和命令由固定清单生成,不接受调用者传入的宽泛前缀:
``` python
pattern = " ^(?: " + " | " . join ( re . escape ( name ) for name in sorted ( expected_names ) ) + " )$ "
command = [ " go " , " test " , " -json " , " -count=1 " , " -skip " , pattern , " -mod= " + mode , " ./... " ]
```
- [ ] **Step 3 — 接入组合器和 PG 门禁。 ** `run_provider_tests` 对 Backend 和可选 RDM 来源调用 `run_a` ,其他 Go 模块仍执行原完整 `go test` /`go vet` 。将 A 报告写入最终 `resolved-manifest.json` 并在标准输出逐项打印;`--rdm-source` 存在时 RDM Backend 行和 RDM 行均须绑定实际 Go 图版本。`component-check.py` 的**固定来源** `test-backend-pg` 路径也通过同一 `run_a` 执行真实 PG/Redis 环境下的 Backend 全套其余测试:受信获取用原 `process_environment` ,候选执行改用与 `execution_environment` 等价的无凭据环境;本地在 `check_sources` 的临时快照根进入 `CandidateExecutor` , CI 子容器核验只读 `ci_child` 身份。可编辑/`LOCAL_OVERLAY=1` 的开发者检查不冒充正式 A,原工作区语义不改。`svc-realcombo` 只构建/跑生命周期,没有嵌套 Go 测试,仍保留原 job 及 socket 负例。缺报告时对应命令非零,不发布成功清单。
- [ ] **Step 4 — 回归、提交。 ** 跑目标 Python 单测、`make test-governance` 、组合 fixture,检查固定 Backend 真源的 `-list` 输出与清单一致;`make test` 可在 Task 3 完成后再作为完整门禁。Dev 提交后仍不推送为阶段 7 候选;RDM 包装器与其独立单测随 Task 4 的 RDM 提交。
### Task 3: Provide Python test tools inside the local sandbox
**Files: ** Dev 新建 `scripts/python-test-tools.lock` 、`scripts/python_test_tools.py` 、`tests/test_python_test_tools.py` ;修改 `scripts/build-composition.py` 、`tests/test_composition.py` 、`PROJECT.md` ; Backend 修改 `internal/runtime/llm_verify_test.go` 。
**Interfaces: ** `prepare_wheels(root: Path, env: dict[str,str]) -> Path` 仅在受信获取阶段下载 wheel; `install_for_candidate(executor: CandidateExecutor, wheel_dir: Path, work_root: Path, env: dict[str,str]) -> dict[str,str]` 在外层沙箱内用 `--no-index --no-deps --only-binary=:all:` 安装到本次临时目录并返回只指向该目录的 `PYTHONPATH` 。不得读取宿主 `~/.local` 。
- [ ] **Step 1 — 写红灯。 ** 在现有 `CandidateExecutor` 外层隔离中使用空 `HOME` 、遮蔽宿主 `~/.local` ,验证 `python3 -c 'import docx,pptx,openpyxl'` 初始失败;伪造 wheel 缺失/散列变动时 `prepare_wheels` 必须非零。再通过隔离组合器运行固定 Backend `TestLintErrorContinues` ,记录当前缺 `docx` 的失败 SELF。该 Go 测试现以 `t.Setenv("PYTHONPATH", abs)` 覆盖执行环境,所以仅为组合器加 `PYTHONPATH` 不够;测试修复也在本任务中。
- [ ] **Step 2 — 固定工具图。 ** 锁文件写入 `python-docx==1.2.0` 、`python-pptx==1.0.2` 、`openpyxl==3.1.5` 、`lxml==6.1.3` 、`Pillow==12.3.0` 、`et-xmlfile==2.0.0` 、`typing-extensions==4.16.0` 、`XlsxWriter==3.2.9` ;受信阶段以 `["python3","-m","pip","download","--only-binary=:all:","--dest",str(wheel_dir),"-r",str(root/"scripts/python-test-tools.lock")]` 获取,记录每个 wheel SHA256 与 Python ABI,并拒绝源码发行包或额外分发物。若任一目标 ABI 无这些版本的 wheel,停下修订锁,不从宿主 site-packages 偷渡。
- [ ] **Step 3 — 隔离安装与测试环境。 ** 候选阶段按以下 argv 安装固定 wheel,`executor` 保证实际 pip 在外层沙箱中执行;仅把 `pydeps` 加进候选 `PYTHONPATH` 。Backend `TestLintErrorContinues` 设置 house-style 路径时改为拼接已有 `PYTHONPATH` ,保持两个路径可见:
``` python
fixed_root = work_root / " Dev-test-root "
argv = [ " python3 " , " -m " , " pip " , " install " , " --no-index " , " --no-deps " ,
" --only-binary=:all: " , " --target " , str ( work_root / " pydeps " ) ,
" --find-links " , str ( wheel_dir ) , " -r " , str ( fixed_root / " scripts/python-test-tools.lock " ) ]
executor . run ( argv , work_root , env )
```
``` go
original := os . Getenv ( "PYTHONPATH" )
if original != "" {
abs += string ( os . PathListSeparator ) + original
}
t . Setenv ( "PYTHONPATH" , abs )
```
在外层 Bubblewrap 内探测 `import docx,pptx,openpyxl` 和 `TestLintErrorContinues` ;CI 子容器也用相同锁和摘要,不能因为镜像偶然预装就跳过固定工具图。
- [ ] **Step 4 — 记录通过或失败。 ** 跑目标测试、组合 fixture 和完整 `make test` ;后者除 Task 2 明确排除外仍失败则按真实原因修复,不把 Python 工具正例扩大成全部门禁通过。提交 Dev 变更。
### Task 4: Isolate RDM's own CI candidate step
**Files: ** RDM 新建 `scripts/ci_candidate_executor.py` 、`scripts/nested_sandbox_gate.py` 、`tests/test_ci_candidate_executor.py` ;修改 `.gitea/workflows/ci.yml` 、`Makefile` 。Dev `scripts/ci_candidate_executor.py` 和 `tests/test_ci_credentials.py` 作为已验证模式的参考,不从 RDM 的工作流直接执行 Dev PR 的未固定脚本。
**Interfaces: ** `run_ci_job("build-and-test") -> int` 为 RDM CI 获取步骤后唯一候选入口;`inspect_workspace_volume(parent_id: str) -> str` 、`build_executor_image() -> str` 、`create_identity(volume: str) -> Path` 分别验证命名卷、固定镜像及只读身份标记;`nested_sandbox_gate.run_rdm_a(source: Path, commit: str, executor) -> dict` 返回 Task 2 schema。子容器的 `COMPOSITION_ROOT` 指向受信预取的固定 Dev 源;原 `make test` 的产品构建、`test-web` 、`test-governance` 、Go 包测试均在子容器执行。
- [ ] **Step 1 — 红灯负例。 ** 测试扫描 RDM 工作流:最后候选步骤若含直接 `GOWORK=off make test` 即失败;假 Docker inspect 返回非命名卷、父子 SHA 不一致、子挂载 socket/HOME、只读标记可改写、缺 Go/Node 工具时均失败。先跑 `python3 -B -m unittest tests.test_ci_candidate_executor -v` 记录红灯。
- [ ] **Step 2 — 受信启动器。 ** 验证 `GITHUB_REPOSITORY=weichen/Agent-System-RDM` 、平台 SHA=检出 HEAD、父容器卷和固定工具镜像 ID;以 `docker run --rm --cap-drop ALL --security-opt no-new-privileges --read-only --tmpfs /tmp:rw,exec,nosuid,nodev` 启动只挂工作卷与只读身份标记的子容器,不挂 socket/`/var/run/act` /父 HOME。启动前用假私有文件、socket、工作卷写入和网络探针做正反例;探针失败不得回落父进程执行。镜像在空上下文安装固定工具,不执行候选源码。
``` python
repo = os . environ . get ( " GITHUB_REPOSITORY " , " " )
platform_sha = os . environ . get ( " GITHUB_SHA " , " " )
actual_head = subprocess . check_output ( [ " git " , " rev-parse " , " HEAD " ] , text = True ) . strip ( )
parent_id = os . environ . get ( " HOSTNAME " , " " )
if actual_head != platform_sha or repo != " weichen/Agent-System-RDM " :
raise CIExecutorError ( " RDM CI source identity differs from platform " )
volume = inspect_workspace_volume ( parent_id )
image_id = build_executor_image ( )
identity_path = create_identity ( volume )
workspace_volume_mount = (
f " type=volume,source= { volume } ,target=/workspace/weichen/Agent-System-RDM " )
readonly_identity_mount = (
f " type=bind,source= { identity_path } ,target=/run/agent-system-candidate/identity,readonly " )
candidate_job_script = JOB_SCRIPTS [ " build-and-test " ]
argv = [ " docker " , " run " , " --rm " , " --cap-drop " , " ALL " ,
" --security-opt " , " no-new-privileges " , " --read-only " ,
" --tmpfs " , " /tmp:rw,exec,nosuid,nodev " ,
" --mount " , workspace_volume_mount ,
" --mount " , readonly_identity_mount , image_id ,
" bash " , " -lc " , candidate_job_script ]
probe_child ( argv )
subprocess . run ( argv , check = True )
```
- [ ] **Step 3 — A 的 RDM 用例报告。 ** 子容器先运行产品构建、web 和治理检查,再由 `run_rdm_a` 对五项精确 `NOT_RUN_IN_A` ,其余 Go 测试和 `go vet` 完整执行;报告绑定 RDM 最终 SHA 并输出。若 Dev 产品组合器需要 `ci_child` 身份,同一子容器只读标记须通过其内核/挂载校验。加入 RDM `make test-governance` 对本启动器单测的调用。
- [ ] **Step 4 — 真实机制和平台验证。 ** 用一次性命名 Docker 卷重放 RDM 父子挂载/socket 正反例,随后在 RDM PR 的**新 HEAD** 运行 `build-and-test` GATE;保存工作流 run URL、HEAD、作业身份与完整输出。CI 未在真实 runner 通过则 RDM 仍 WIP,不能引用 run3130 放行。提交并推送现有 RDM 迁移 PR,按该仓首次/恢复门禁操作。
### Task 5: Export credential-free fixed sources and replay them in a VM
**Files: ** Dev 新建 `scripts/prepare_sandbox_replay.py` 、`scripts/replay_vm.py` 、`scripts/replay-vm.lock.json` 、`tests/test_sandbox_replay.py` 、`tests/test_replay_vm.py` ;修改 `PROJECT.md` 。Task 1 的原生正例最终由此任务证明。
**Interfaces: ** `prepare_bundle(dev: Path, rdm: Path, output: Path) -> Path` 读取 Dev 应用锁与 RDM Go 图,输出每个来源的完整 SHA、`go.mod/go.sum` 摘要、vendor 树摘要和无 `.git` 的只读 tar; `probe_vm(lock: Path) -> dict` 输出实际 VM/`bwrap` 正例;`run_vm(bundle: Path, lock: Path, output: Path) -> dict` 产生 B 报告。
- [ ] **Step 1 — 红灯安全测试。 ** 假 bundle 含 `.git/config` 、形如 `ACCESS_TOKEN=` 后接 40 位十六进制的真实值、SSH 私钥头、符号链接越界、错来源 SHA、缺 vendor 或篡改 SHA256 时拒绝;普通文档中的文字 `ACCESS_TOKEN=PLACEHOLDER` 不应误报。假 VM 控制台报告缺 `bwrap_started` 、含 socket、命名用例 `SKIP` 、缺一个用例或把两个 Backend SHA 混成一个时拒绝。先跑目标 Python 单测保存红灯。
- [ ] **Step 2 — 固定且无凭据的来源包。 ** 复用已审阅的 Dev `transport.environment` /`export_source` /`go_dependencies` /`provider_revision` 获取 Dev Backend 与 RDM Go 图 Backend、RDM 自身;每一来源分别 `go mod vendor` ,在目录外核验 `go list -m -json all` 与清单。归档仅白名单源码、vendor、`go.mod` /`go.sum` 、manifest;逐 tar entry 拒绝绝对路径、`..` 、符号链接和 Git/config/凭据文件;最终受信步骤关闭 token 环境后再交 VM。两个 Backend SHA 即使一样也保留两个逻辑角色记录。
- [ ] **Step 3 — 建立任务专用 VM 基础镜像。 ** 在没有候选代码和凭据的受信 helper 中,以固定 `ubuntu@sha256:534baea6a22c03a63003dbc8dbe78fe34bc0d7e595d9a9dc9834884ff530eb55` 构建含 QEMU、`cloud-localds` 、`gpgv` 的工具镜像;下载 Ubuntu Noble 官方 cloud image、`SHA256SUMS` 与签名,按 [Ubuntu 官方镜像校验方法 ](https://documentation.ubuntu.com/security/software-integrity/image-verification/ ) 验签与逐文件 SHA256,将最终镜像散列、helper image ID 和 Go 1.27.1 工具散列写入 `replay-vm.lock.json` 。首次联网预置 VM 仅装公开工具 `bwrap` 、Go、bash、Git; Go tar 包须匹配 [Go 官方发布 SHA256 ](https://go.dev/dl/ ) `63d339f0da5ab53635a56f2490a7984dfe12dfcff22ad749f63edaf590168445` 。受信 helper 用 `cloud-localds` 生成 NoCloud seed,预置 user-data 只建无密码 `replay` 用户、安装公开工具、验证 `go version go1.27.1 linux/amd64` 、输出 `REPLAY_BASE_READY` 并关机;准备好后锁定只读基础磁盘,每次测试从独立 qcow2 overlay 启动。预置阶段没有仓库源码或 token。若签名、版本或工具链散列不符,失败并保留诊断,不改宿主策略。
``` bash
curl -fsSLo go1.27.1.linux-amd64.tar.gz https://go.dev/dl/go1.27.1.linux-amd64.tar.gz
echo '63d339f0da5ab53635a56f2490a7984dfe12dfcff22ad749f63edaf590168445 go1.27.1.linux-amd64.tar.gz' | sha256sum -c -
cloud-localds prep-seed.img prep-user-data prep-meta-data
```
- [ ] **Step 4 — 实际 VM 正负探针。 ** 用受信 Docker CLI 启动**不挂 socket/HOME**的 QEMU helper,仅传 `--device=/dev/kvm` 、限定内存/CPU 和任务临时磁盘;B 测试启动时 `--network=none` 。[Docker 官方文档 ](https://docs.docker.com/engine/containers/run/ )允许用 `--device` 单独传设备,无须 `--privileged` 。QEMU 仅以 `-virtfs local,path=/replay/source,mount_tag=source,security_model=none,readonly=on` 把来源包只读给 guest,按 [QEMU 9p 只读参数 ](https://www.qemu.org/docs/master/system/invocation.html )核对。guest 以非 root `/home/replay` 用户运行,先验证 socket/凭据不存在、假秘密在内层 `bwrap --unshare-user --unshare-pid` 中不可见、正向脚本确实输出 `BWRAP_OK` ;任何设备、namespace、安装或启动失败都停在 B=`NOT_RUN/FAIL` ,不试 `--privileged` 、改 AppArmor 或把测试移回主机。
``` python
source_bundle_readonly_mount = (
f " type=bind,source= { bundle . parent . resolve ( ) } ,target=/replay/source,readonly " )
task_scratch_mount = (
f " type=bind,source= { scratch . resolve ( ) } ,target=/replay/scratch " )
readonly_9p_arg = " local,path=/replay/source,mount_tag=source,security_model=none,readonly=on "
ephemeral_guest_disk_arg = " file=/replay/scratch/overlay.qcow2,if=virtio,format=qcow2 "
serial_output_arg = " file:/replay/scratch/serial.log "
helper_image_id = lock [ " helper_image_id " ]
docker_argv = [ " docker " , " run " , " --rm " , " --device=/dev/kvm " ,
" --cap-drop=ALL " , " --security-opt=no-new-privileges " ,
" --network=none " , " --memory=6g " , " --cpus=4 " ,
" --mount " , source_bundle_readonly_mount ,
" --mount " , task_scratch_mount , helper_image_id ,
" qemu-system-x86_64 " , " -machine " , " q35,accel=kvm " ,
" -m " , " 4G " , " -smp " , " 2 " , " -display " , " none " ,
" -nic " , " none " , " -virtfs " , readonly_9p_arg ,
" -drive " , ephemeral_guest_disk_arg ,
" -drive " , " file=/replay/scratch/replay-seed.img,if=virtio,format=raw,readonly=on " ,
" -serial " , serial_output_arg ]
subprocess . run ( docker_argv , check = True )
```
- [ ] **Step 5 — 离线逐用例重放。 ** guest 从只读 9p 包复制到自己的临时磁盘,以 `GOWORK=off GOPROXY=off GOSUMDB=off` 对两份 Backend 角色分别运行 `go test -mod=vendor -json -count=1 -run` 加十项名字组成的锚定正则、包路径 `./internal/tools` ;再以五项名字组成的锚定正则对 RDM `./internal/rdmapp` 运行。正则由 Task 2 的固定清单使用 `re.escape` 、`join` 生成,不接受自由文本。输出完整 JSONL、每项 PASS/FAIL/SKIP、包退出码和三个来源完整 SHA。受信宿主解析控制台结果形成 B schema,零 Skip/零缺项才返回 0;VM 退出即销毁本次临时磁盘,原始脱敏输出留在源码外收据。
- [ ] **Step 6 — 独立重放。 ** 作者可先以 SELF 调试,随后交由独立审阅者在其受控环境按同一工具和固定 bundle/lock 重跑,记 `REPLAY` (身份、环境摘要、命令、完整 SHA、输出);若将来有可验证身份的独立 CI,可记 `GATE` 。没有独立结果时 B 仍 `NOT_RUN` ,此任务不能标交付完成。提交 Dev 工具与测试,但不得在作者提交中伪填 REPLAY。
### Task 6: Reconcile both gates and resume the original final-delivery plan
**Files: ** Dev 新建 `scripts/reconcile_sandbox_gates.py` 、`tests/test_reconcile_sandbox_gates.py` ;修改 `PROJECT.md` 、`policy/dependencies.md` 、`.task/feature-dependency-workspace.md` 、`components.lock.json` ;Docs 更新本计划/索引状态。RDM 任务卡与 PR 引用最终证据。
**Interfaces: ** `reconcile(a_reports: list[dict], b_report: dict, sources: dict, gates: list[dict]) -> dict` 只在每个 A 的 `NOT_RUN_IN_A` 对应同来源 B `PASS` 、三个 Dev CI context 与 RDM CI 均为最终 SHA GATE、B 为独立 REPLAY/GATE、其余正式检查成功时返回 `{"result":"PASS"}` ;否则返回明确非零和差异表。`gates` 的运行身份、HEAD、状态和链接须从 Gitea API 的受信读取得到,不能从候选报告自填。
- [ ] **Step 1 — 红灯矩阵。 ** 输入旧 run3130、旧 Dev run3076、B=`NOT_RUN` 、错 Backend SHA、同名用例跨 SHA 借用、RDM CI 无 socket 证据、任何多余 Skip、缺 `python-docx` 修复或 A 失败,分别断言拒绝;两份精确对象、环境探针和逐用例均正确时才 PASS。先留 SELF 红灯。
- [ ] **Step 2 — 实现核对与任务卡。 ** 从 Dev `resolved-manifest.json` 、Gitea API 的最终 Dev/RDM CI 运行及 B 独立收据读取固定对象,不接受人工手抄通过值;按 `(source_sha, package, test_name)` 做一一映射并打印可读差异。受信 API 读取步骤只输出作业名、SHA、状态、runner 身份和 URL,不把 token 传入候选或报告。`make evidence` 写完整脱敏输出;文档说明普通 clone 的 A 门禁、VM 的 B 门禁、现有限制和故障恢复,不把测试原生 Skip 写成通过。
``` python
missing = { ( r [ " source_sha " ] , r [ " package " ] , t [ " name " ] )
for r in a_reports for t in r [ " tests " ]
if t [ " status " ] == " NOT_RUN_IN_A " }
passed = { ( r [ " source_sha " ] , r [ " package " ] , t [ " name " ] )
for r in b_report [ " runs " ] for t in r [ " tests " ]
if t [ " status " ] == " PASS " }
if missing - passed or any ( r [ " exit_code " ] != 0 for r in a_reports ) :
raise GateMismatch ( " A exclusions lack exact-source B passes " )
required_jobs = { " test " , " test-pg " , " svc-realcombo " , " rdm/build-and-test " }
verified_jobs = { g [ " job " ] for g in gates
if g [ " status " ] == " success " and
g [ " source_sha " ] == sources [ g [ " repository " ] ] [ " commit " ] and
g [ " runner_identity_verified " ] }
if verified_jobs != required_jobs :
raise GateMismatch ( " Final-source CI gates are incomplete " )
```
- [ ] **Step 3 — 完成原计划 Task 5/6 的新候选。 ** Docs 书面设计与本计划批准版本锁进 Dev;Guidelines 仅在通用规则确实有缺口时变更。按 `policy/merge-queue.md` 核对 #518 窗口后,逐仓同步主干、处理在途分支冲突并更新来源图;以最终 HEAD 跑 Dev `make test` /`make build` 、RDM 产品与 CI、三个 Dev 必需 CI、普通 clone/Native 按需编辑、受影响浏览器和 B 重放。每个失败保持 FAIL/NOT_RUN,绝不靠旧 SHA 结果补位。
- [ ] **Step 4 — 重新走评审和合入。 ** A/B 与所有原有门禁都通过后才提交新的阶段 7 材料给用户评审;通过后由独立审阅者执行 Stage 8 重放/Review,修复问题再以新 HEAD 重验。依既有队列和 PR 规则逐仓合入、闭卡;实际工作区回收、发布/部署及宿主/共享 runner 改动仍受原授权边界限制,不因本计划自动实施。
## Execution handoff
本计划只提供实施步骤和停止条件。先由用户审阅本计划;用户已为原整改计划选择“本会话逐项实施”,若本修订计划获批且未改选方法,继续以当前会话逐项实施。Task 5 的独立 VM 可用性须先有真实正例;若无法实现,停止并回到设计评审,保留全部 WIP 和失败证据。