From c8cc92b701eebaf7bb2b400ee3178bbaf0e91977 Mon Sep 17 00:00:00 2001 From: weichen Date: Wed, 14 Oct 2026 10:16:26 +0000 Subject: [PATCH] Plan the nested sandbox dual-gate remediation Why: The approved written amendment needs executable, reviewable steps for explicit test accounting and independent native sandbox replay. Evidence: Current Dev and RDM source inspection, failed Dev make test receipt, local KVM/tool availability, official Go, Ubuntu and QEMU documentation, plan link and diff checks. Impact: Documents only. No candidate execution, runner configuration, source lock or gate behavior changes. Refs: Documents design 1790cf8; Dev PR #537; Documents PR #100 --- ...ependency-nested-sandbox-amendment-plan.md | 221 ++++++++++++++++++ ...ned-dependency-nested-sandbox-amendment.md | 2 +- ...oned-dependency-review-remediation-plan.md | 2 + docs/proposals/INDEX.md | 2 +- 4 files changed, 225 insertions(+), 2 deletions(-) create mode 100644 docs/proposals/2026-10-14-versioned-dependency-nested-sandbox-amendment-plan.md diff --git a/docs/proposals/2026-10-14-versioned-dependency-nested-sandbox-amendment-plan.md b/docs/proposals/2026-10-14-versioned-dependency-nested-sandbox-amendment-plan.md new file mode 100644 index 0000000..bd394af --- /dev/null +++ b/docs/proposals/2026-10-14-versioned-dependency-nested-sandbox-amendment-plan.md @@ -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 和失败证据。 diff --git a/docs/proposals/2026-10-14-versioned-dependency-nested-sandbox-amendment.md b/docs/proposals/2026-10-14-versioned-dependency-nested-sandbox-amendment.md index e279854..d87ecec 100644 --- a/docs/proposals/2026-10-14-versioned-dependency-nested-sandbox-amendment.md +++ b/docs/proposals/2026-10-14-versioned-dependency-nested-sandbox-amendment.md @@ -1,6 +1,6 @@ # 版本依赖迁移:嵌套沙箱测试的双必需门禁修订设计 r1 -状态:**书面设计待用户评审;尚未批准实施**(2026-10-14)。用户已同意先把“双必需门禁”写成正式修订设计;该回复不批准本文件、实施计划、独立环境配置或代码改动。本文件拟补充已批准的[独立 Review 整改设计](2026-10-14-versioned-dependency-review-remediation.md)及其[实施计划](2026-10-14-versioned-dependency-review-remediation-plan.md)。在书面修订获批前,原执行边界继续有效,Dev PR #537 等迁移 PR 保持 WIP。 +状态:**书面设计 `1790cf8` 已获用户批准,实施计划待评审;尚未批准实施**(2026-10-14)。用户批准以“双必需门禁”编制[修订实施计划](2026-10-14-versioned-dependency-nested-sandbox-amendment-plan.md),该回复不批准独立环境配置或代码改动。本文件补充已批准的[独立 Review 整改设计](2026-10-14-versioned-dependency-review-remediation.md)及其[实施计划](2026-10-14-versioned-dependency-review-remediation-plan.md)。Dev PR #537 等迁移 PR 保持 WIP。 ## 触发事实与目标 diff --git a/docs/proposals/2026-10-14-versioned-dependency-review-remediation-plan.md b/docs/proposals/2026-10-14-versioned-dependency-review-remediation-plan.md index 2c13dcc..9d8ab6b 100644 --- a/docs/proposals/2026-10-14-versioned-dependency-review-remediation-plan.md +++ b/docs/proposals/2026-10-14-versioned-dependency-review-remediation-plan.md @@ -12,6 +12,8 @@ **Authority and current state:** 用户已批准书面设计和本计划 `20ada84`,选择本会话逐项实施。Task 1–4 的本地代码、回归与 Dev 提交已完成;Task 5 的文档/来源锁对齐进行中,Task 6 的新 HEAD 平台 GATE、阶段 7 人工验收、Stage 8 独立复审与合入尚未完成。Dev 旧候选 `6a2f7cd3e610a263b6348efe9084e386dc88f30c` 的 run3076、RDM run3077 和阶段 7 人工结论不放行新 HEAD。14 个迁移 PR 保持 WIP,#518 无活动 Dev 窗口;本计划不授权发布、部署、业务数据、历史改写、删除来源引用、改变共享 runner/分支保护或任务工作区清理。 +**执行修订(2026-10-14):** 新候选 `1ea93a5` 的正式 `make test` 遇到嵌套 Bubblewrap 阻塞,另有 `python-docx` 缺件。用户已批准[双必需门禁书面设计 `1790cf8`](2026-10-14-versioned-dependency-nested-sandbox-amendment.md),其[实施计划](2026-10-14-versioned-dependency-nested-sandbox-amendment-plan.md)待评审。Task 5/6 的完成条件现须同时满足修订计划;在计划未获批准、A/B 缺证据时,不按旧步骤推进阶段 7 或合入。 + ## Global Constraints - 正式 Go 构建仍是 `GOWORK=off`、`-mod=readonly`,Go 实际版本以消费方图为准;npm 锁、应用/规范/资料来源锁分别保留唯一权威,不用本地 replace 或 latest 兜底。 diff --git a/docs/proposals/INDEX.md b/docs/proposals/INDEX.md index 6dbd89b..41e4dd3 100644 --- a/docs/proposals/INDEX.md +++ b/docs/proposals/INDEX.md @@ -14,7 +14,7 @@ 正式验收、共享部署和发布保留原审批边界。 [独立 Review 整改设计 r1](2026-10-14-versioned-dependency-review-remediation.md)记录三项 Stage 8 阻塞的本地/CI 沙箱、规范缓存重试与生成工作区安全回收边界;书面设计 `68c658a` 已获用户批准,旧候选不得据旧 CI 合入。 [整改实施计划](2026-10-14-versioned-dependency-review-remediation-plan.md)细化每单元红绿回归、CI 沙箱、重新验收与合入停止条件;计划 `20ada84` 已获用户批准并由本会话逐项实施。Dev Task 1–4 本地候选已提交,Task 5 文档/来源锁对齐进行中;新 HEAD 的平台 CI、阶段 7 人工验收与 Stage 8 独立复审尚未完成。 - [嵌套沙箱双必需门禁修订设计 r1](2026-10-14-versioned-dependency-nested-sandbox-amendment.md)针对新候选 `make test` 的嵌套 `bwrap` 阻塞;**书面设计待用户评审,尚未批准实施**,独立重放环境及最终门禁均未就位。 + [嵌套沙箱双必需门禁修订设计 r1](2026-10-14-versioned-dependency-nested-sandbox-amendment.md)针对新候选 `make test` 的嵌套 `bwrap` 阻塞;书面设计 `1790cf8` 已获用户批准。[修订实施计划](2026-10-14-versioned-dependency-nested-sandbox-amendment-plan.md)待评审,独立重放环境及最终门禁均未就位。 - [IAM I1 权限边界与公开扩展合同 r1](2026-10-13-iam-i1-permission-contract.md): 依据已批准的[用户、职位角色与分层权限统一方案 r1](2026-10-13-identity-role-permission-plan.md)