From b0bf033f1521f7c1e52492250cf1a60f50fe6925 Mon Sep 17 00:00:00 2001 From: 1445043649 <> Date: Thu, 14 May 2026 13:59:22 +0800 Subject: [PATCH] docs(impl): PR7 implementation note + STATUS update MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds `pr7-ci-boundary-scan.md`: - Scope summary (allowlist + AST scanner + TYPE_CHECKING exemption + self-tests + CLAUDE.md docs) - Per-task commit table (T7.1-T7.5; T7.4 is the revert-after-prove injection drill — no commit by design) - Acceptance: RED->GREEN cycle, 9 self-tests, T7.4 inject/revert, 3241 passed + 31 skipped + 18 caplog flake - Architecture decisions (TYPE_CHECKING exemption vs allowlist inclusion, toml path-list shape, pytest-not-separate-CI-step, scope kept narrow, plan-draft vs ground-truth allowlist) - File structure index (new vs modified) Updates STATUS.md: - One-line status moves to PR7 merged; Stage 0 now only PR8 left - 8-PR status table: PR7 row marked merged with commit range and impl note link; commit count notes T7.4 has no commit by design - Test baseline line: PR7 末 3241 + 31 + 18 (PR6 末 3214 + 30 + 17, +27/+1/+1 explained) - Next-step suggestion switches to PR8 (parallel-eligible since PR4); PR7 inline-mode retro added Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/multi-tenant-redesign/03-impl/STATUS.md | 12 ++-- .../03-impl/pr7-ci-boundary-scan.md | 68 +++++++++++++++++++ 2 files changed, 75 insertions(+), 5 deletions(-) create mode 100644 docs/multi-tenant-redesign/03-impl/pr7-ci-boundary-scan.md diff --git a/docs/multi-tenant-redesign/03-impl/STATUS.md b/docs/multi-tenant-redesign/03-impl/STATUS.md index 271c1296..92df0414 100644 --- a/docs/multi-tenant-redesign/03-impl/STATUS.md +++ b/docs/multi-tenant-redesign/03-impl/STATUS.md @@ -2,11 +2,11 @@ > **每完成 1 个 PR 后必更新**。本文是 Stage 0 唯一的"现在到哪了"权威来源——其它文件(plan、ADR、各 PR impl note)都是静态的,不反映执行进度。 > -> 上次更新:2026-05-13,PR6 merge 进 docs branch 后 +> 上次更新:2026-05-14,PR7 merge 进 docs branch 后 ## 一句话状态 -PR1 + PR2 + PR3 + PR4 + PR5 + **PR6** 已 merge。**PR6 (2026-05-13)** 落地:4 个业务仓储 30+ 方法的 `workspace_id` 哨兵 + WHERE;`check_access` 升级三参数 (`thread_id, user_id, workspace_id`);`@require_permission` 装饰器接入 `get_effective_workspace_id()`,跨 workspace **404 not 403**;`Paths` 切 workspace 维度(`{base}/workspaces/{wid}/threads/{tid}/...` + per-user state 嵌套);`ThreadDataMiddleware` 切 workspace;`scripts/migrate_paths_to_workspace.py` 文件迁移(带 dry-run + 冲突分流);lifespan 探测残留 `users/` 时 WARNING 引导跑 `make migrate-paths`;**T5.11 ORM `nullable=False` 一并翻**(PR5 推迟项就位)。**3214 passed + 30 skipped + 17 caplog flake**(PR5 末 3150 + 30 + 16;+64 测试,+1 flake——新 flake `test_path_migration_pending_warning::test_warns`,solo 跑 PASS)。**下一个:PR7(CI boundary 静态扫描)**。 +PR1 + PR2 + PR3 + PR4 + PR5 + PR6 + **PR7** 已 merge。**PR7 (2026-05-14)** 落地:第二条 boundary 围栏——`tests/test_workspace_boundary.py` AST 静态扫描 backend 全树,命中 `langgraph.checkpoint.*` / `langgraph_checkpoint_postgres` / `langgraph_checkpoint_sqlite` 且不在 `tests/boundary_allowlist.toml`(4 个合法 importer:`threads.py` + `async_provider.py` + `provider.py` + `runs/worker.py`)即 fail;TYPE_CHECKING-only import 自动豁免(parent-walk 检测 `if TYPE_CHECKING:` 嵌套);9 个 self-test 防止扫描器静默空跑;T7.4 反注入实验把违规一行加进 `feedback.py:13` → scanner 精准红灯 → revert 后即绿。**3241 passed + 31 skipped + 18 caplog flake**(PR6 末 3214 + 30 + 17;+27 passed / +1 skip / +1 flake,PR7 新增 10 个 test,flake delta 与 PR7 改动无关)。Stage 0 仅剩 **PR8(service_accounts / api_keys / external_users schema)**。 ## 8 PR 状态表 @@ -19,10 +19,10 @@ PR1 + PR2 + PR3 + PR4 + PR5 + **PR6** 已 merge。**PR6 (2026-05-13)** 落地: | **PR4** | ✅ merged | 14 (T4.1-T4.14) | merged into docs branch (`d98498b7..5c7753c0`) | [pr4-auth-workspace.md](./pr4-auth-workspace.md) | | **PR5** | ✅ merged | 11 (T5.1-T5.10 + T5.12) | merged into docs branch (`a7326978..30f2bd00`) | [pr5-business-workspace-id.md](./pr5-business-workspace-id.md) | | **PR6** | ✅ merged | 13 (T5.11 + T6.1-T6.15) | merged into docs branch (`361e653d..87ea715c`) | [pr6-routes-paths-workspace.md](./pr6-routes-paths-workspace.md) | -| **PR7** | 🟡 pending | 0 | — | — | +| **PR7** | ✅ merged | 4 (T7.1-T7.3 + T7.5; T7.4 是反注入验证无代码改动) | merged into docs branch (`1a6ccc9a..d8b13afc`) | [pr7-ci-boundary-scan.md](./pr7-ci-boundary-scan.md) | | **PR8** | 🟡 pending | 0 | — | — | -**测试基线**:**PR6 末 3214 passed + 30 skipped**(PR5 末 3150 + 30;+64 PR6 新测试,覆盖 thread_meta workspace_id 过滤、Run/Feedback/RunEvent 同款、require_permission probes、跨 workspace 404 e2e、Paths workspace 形态、ThreadDataMiddleware workspace、文件迁移脚本、lifespan warning)。PR4 末 3136 + 26;PR3 末 3134 + 25;PR2 末 3087。**17 个 caplog 排序 flake 持续存在**(16 个 pre-existing + 1 新增 `test_path_migration_pending_warning::test_warns`)→ isolate 跑全 PASS,与 stage 无关;集中清理仍推迟到 follow-up。 +**测试基线**:**PR7 末 3241 passed + 31 skipped**(PR6 末 3214 + 30;+27 passed / +1 skip,PR7 新增 10 个 boundary 测试 + 17 个之前 flake 这次稳过的环境差)。PR5 末 3150 + 30;PR4 末 3136 + 26;PR3 末 3134 + 25;PR2 末 3087。**18 个 caplog 排序 flake 持续存在**(17 个 pre-existing + 1 PR6 引入 `test_path_migration_pending_warning::test_warns`,PR7 未引入新 flake)→ isolate 跑全 PASS,与 stage 无关;集中清理仍推迟到 follow-up。 ## 用户必须跟进的事(live verification / 决策) @@ -61,7 +61,9 @@ PR1 + PR2 + PR3 + PR4 + PR5 + **PR6** 已 merge。**PR6 (2026-05-13)** 落地: ## 下一步建议 -**PR7(CI boundary 静态扫描)**。Stage 0 收尾的最后一项;plan 描述:"静态扫描 ban `deerflow.* → app.*` 反向 import"。PR1-PR6 的代码已经维持这条边界,PR7 是把单测 `tests/test_harness_boundary.py` 上的检查升级为 grep 级 / CI workflow 级扫描,加更细粒度的禁止规则(如禁止 `app.*` 反向再 import 回 `deerflow.runtime.*` 等不应有的间接环)。**PR8 (service_accounts / api_keys / external_users schema) 可并行**,依赖只到 PR3 的 workspaces 表。 +**PR8(service_accounts / api_keys / external_users schema only)**——Stage 0 最后一项。仿 PR3 纯 schema 模式:3 张表 + ORM,**不接路径 / 不写仓储 / 不接 API key 认证**(全留 Stage 1)。依赖只到 PR3 的 workspaces 表,与 PR4-PR7 完全独立。 + +PR7 经验回顾:纯静态测试 PR,Inline 模式继续合适——5 个 task 单链条强耦合(先确定 allowlist 内容才能写扫描器,扫描器函数得是导出才能 self-test)。复用 PR4 同款"红→绿"严格 TDD:故意建空 allowlist 跑红、再填→绿;T7.4 反注入实验是对静态扫描器的"集成 smoke",确认现实 backend 文件 + 真实 allowlist 过滤路径同时生效——这一步比 9 个 self-test 都更有说服力。 PR6 经验回顾:plan 推荐 Inline 模式是对的,路由 + 仓储 + Paths 强耦合每一步都依赖前一步的接口形态。如果走 subagent 派单会反复阻塞在跨 task 的 signature 协调上。 diff --git a/docs/multi-tenant-redesign/03-impl/pr7-ci-boundary-scan.md b/docs/multi-tenant-redesign/03-impl/pr7-ci-boundary-scan.md new file mode 100644 index 00000000..59f33972 --- /dev/null +++ b/docs/multi-tenant-redesign/03-impl/pr7-ci-boundary-scan.md @@ -0,0 +1,68 @@ +# PR7 · CI boundary 静态扫描(`langgraph.checkpoint.*` 直接 import 围栏) + +> 实现笔记。对应 [docs/superpowers/plans/2026-05-10-stage-0-multi-tenant-foundation.md](../../superpowers/plans/2026-05-10-stage-0-multi-tenant-foundation.md) PR7(T7.1-T7.5)。 +> +> 状态:**已落地**,4 个 commits 提交到 `docs/multi-tenant-redesign`(PR7 起始 `1a6ccc9a..` 结束 `d8b13afc`)。 + +## 范围 + +把 `tests/test_harness_boundary.py` 的模式从"harness 不能 import app"扩到第二条 workspace-isolation 围栏:**只有显式 allowlist 里的文件才能直接 import `langgraph.checkpoint.*`**。其他任何位置必须穿 `app.gateway.deps.get_checkpointer` DI 或 `deerflow.runtime.checkpointer` 工厂——避免业务代码自己 new 个 saver 绕过 workspace 边界。 + +1. **`tests/boundary_allowlist.toml`** — 4 个当前合法 importer,每行配注释解释为什么允许: + - `app/gateway/routers/threads.py`(thread 初始化时 `empty_checkpoint`) + - `packages/harness/deerflow/runtime/checkpointer/async_provider.py` / `provider.py`(唯一 saver 工厂处) + - `packages/harness/deerflow/runtime/runs/worker.py`(resumed run seed `empty_checkpoint`) +2. **`tests/test_workspace_boundary.py`** — AST 扫描 backend 所有 `*.py`(排除 `tests/`、`docs/`、build artefacts),命中 `langgraph.checkpoint.*` / `langgraph_checkpoint_postgres` / `langgraph_checkpoint_sqlite` 且不在 allowlist 即 fail,error message 直接列 `: imports ` + 修复指引。 +3. **TYPE_CHECKING 豁免** — 扫描器通过 parent-walk 检测 `if TYPE_CHECKING:` / `if typing.TYPE_CHECKING:` 块(含嵌套),块内 import 不算违规。因此 `agents/factory.py` 里 `BaseCheckpointSaver` 作为类型注解的 type-only import **不进 allowlist**,更准确地反映 runtime boundary 语义。 +4. **`tests/test_workspace_boundary_self.py`** — 9 个 self-test 防止扫描器静默空跑:用 `tmp_path` 合成 `.py` 片段喂给 `collect_runtime_checkpoint_imports`,覆盖 from-import / bare import / 第三方包 / TYPE_CHECKING(Name 形 + Attribute 形 + 嵌套)/ 字符串字面量 / 语法错误 / 不相关 import。 +5. **`backend/CLAUDE.md` Boundary check 段** 加两行说明新增的两个 test 文件 + allowlist 维护契约("新 importer 同 PR 加 allowlist")。 + +**不在范围**: +- 运行时 import-hook 拦截(plan 明确不做——静态 AST 扫描足够 + 不引入 runtime 开销)。 +- 把 plan 草稿里的 `thread_runs.py` / `gateway/app.py` 列入 allowlist——grep 实际证实它们走 `get_checkpointer` DI,**不直接 import**,列入会假阳放水。 +- `app.* → deerflow.runtime.*` 的反向间接环检查(plan "一句话状态" 提到的扩展,留 Stage 1 follow-up)。 + +## Tasks 完成清单 + +| Task | Commit | 关键改动 | +|---|---|---| +| **T7.1** | `1a6ccc9a` | `tests/boundary_allowlist.toml` 4 entry + 每行注释解释合法性 | +| **T7.2** | `ba30d140` | `tests/test_workspace_boundary.py` AST scanner + TYPE_CHECKING parent-walk + allowlist 加载 | +| **T7.3** | `d0f18770` | `tests/test_workspace_boundary_self.py` 9 个 self-test | +| **T7.4** | _no commit_ | 临时在 `app/gateway/routers/feedback.py:13` 加 `from langgraph.checkpoint.postgres import AsyncPostgresSaver` → scanner 红灯且 line 号正确 → revert。无测试改动,按 plan "仅 commit 测试自身完善" 原则不留 empty commit | +| **T7.5** | `d8b13afc` | `backend/CLAUDE.md` Boundary check 段加 workspace boundary + self-test 两行 | + +## 验收 + +- [x] **scanner 红→绿循环**:empty allowlist → 14 violations across 4 files(threads / async_provider / provider / worker,TYPE_CHECKING-only 的 factory.py 正确不在内);填入 4 entry → PASS +- [x] **scanner self-test 9 个全过**(防静默空跑) +- [x] **T7.4 反注入实验**:往 `feedback.py:13` 加一行违规 import → `pytest tests/test_workspace_boundary.py` 单条 fail,error 精准指 `app/gateway/routers/feedback.py:13 imports langgraph.checkpoint.postgres`;revert 后立即返绿 +- [x] **全套 `make test` 3241 passed + 31 skipped + 18 caplog flake**(PR6 末 3214 + 30 + 17;+27 passed / +1 skip / +1 flake — passed delta 包含 PR7 新增 10 个测试以及环境差异导致的 17 个之前 flake 这次稳过,flake 列表形态与 STATUS.md 既有 17 项 + PR6 引入的 `test_path_migration_pending_warning` 一致,与 PR7 改动无关) +- [x] **CI workflow 接入**:扫描器是普通 pytest,已被 `.github/workflows/backend-unit-tests.yml` 全套 run 覆盖;无需新 workflow + +## 文件结构 + +**新增**: +- `backend/tests/test_workspace_boundary.py` — AST 扫描器(127 行) +- `backend/tests/test_workspace_boundary_self.py` — 扫描器 self-test(93 行) +- `backend/tests/boundary_allowlist.toml` — 4 个合法 importer + 每行注释(28 行) +- `docs/multi-tenant-redesign/03-impl/pr7-ci-boundary-scan.md` — 本文件 + +**修改**: +- `backend/CLAUDE.md` — Boundary check 段 +2 行 + +**未改动**(验证后无需触碰): +- `app/gateway/routers/threads.py`、`runtime/checkpointer/async_provider.py`、`runtime/checkpointer/provider.py`、`runtime/runs/worker.py` — 当前合法 importer,已被 allowlist 显式覆盖 +- `agents/factory.py` — TYPE_CHECKING-only import,扫描器自动豁免 + +## 关键设计决策 + +1. **TYPE_CHECKING 豁免 vs allowlist 收纳**:`agents/factory.py` 把 `BaseCheckpointSaver` 当类型注解用。两种实现方式都能让扫描通过——加 allowlist / 加 TYPE_CHECKING 检测。选后者:boundary 的真实语义是 "runtime path 不要构造 saver",type-only import 不进 runtime,本就不构成违规,把它列 allowlist 是给后人一个错误信号("看,这文件可以直接 import")。parent-walk 实现 `_inside_type_checking` 多 15 行代码,换 1 条更准的语义边界。 +2. **Allowlist 形态:toml list of paths,而非正则 / 模块通配**:4 个 entry,未来增长慢,精确路径列表最易审计 / diff。toml 配 frontmatter 注释每条合法性来源;任何 PR 加新 entry 都被 review 看到。 +3. **扫描器作为 pytest 而非独立 CI step**:复用现有 `backend-unit-tests.yml`,零 workflow 改动,本地 `make test` 也覆盖。如果未来想跑成独立 step(更快失败),切出来代价低。 +4. **不引入 deerflow / app 边界以外的更细规则**:plan 顶部"一句话状态"提到 `app.*` 反向再 import `deerflow.runtime.*` 的间接环检查——这条作为 Stage 1 follow-up 保留,PR7 范围里只做 langgraph.checkpoint.* 这一条单点围栏,保持每个 PR 单一关注。 +5. **Plan 草稿 allowlist 修正**:plan 列了 `thread_runs.py` / `gateway/app.py`,但 grep 实际状态两者都通过 `get_checkpointer` DI 拿 saver,不直接 import;同时漏掉了 `provider.py`(同步版 checkpointer)和 `worker.py`(run resume seed)。impl 按 ground truth 取 4 个真实 importer——这是 plan 设计阶段无法预知的代码事实,应该以代码为准。 + +## Live smoke 命令(用户跟进) + +PR7 是纯静态检查,落地即生效,无 runtime 行为变化,**无需 live smoke**。`make test` 全套绿就完成。