diff --git a/docs/architecture/core-decomposition.md b/docs/architecture/core-decomposition.md index e5680eaab..4c65276c1 100644 --- a/docs/architecture/core-decomposition.md +++ b/docs/architecture/core-decomposition.md @@ -66,7 +66,7 @@ Rust 编译和链接面。 | `bitfun-agent-stream` | Stream 聚合和 stream-focused 测试 | done:stream 聚合已独立 | | `bitfun-runtime-ports` | 面向 service/agent 边界的轻量跨层 DTO 和 trait | partial:DTO/trait-only 边界已建立,包含 agent submission/transcript/cancel、remote state、runtime event 与 remote image attachment 契约;不拥有 runtime 实现 | | `bitfun-agent-runtime` | Sessions、execution、coordination、agent system | target:crate 尚不存在,agent runtime 仍在 core | -| `bitfun-agent-tools` | 轻量 tool DTO / contract、portable tool context facts / provider、runtime restriction、host path normalization / runtime artifact URI / remote POSIX path pure contract、allowed-list / collapsed-tool execution gate policy、pure manifest/exposure and GetToolSpec presentation/schema/static metadata/detail/result assembly / execution-plan contract、provider-backed tool catalog / GetToolSpec runtime facade、provider-backed GetToolSpec execution result helper / Tool-result vector adapter、generic contextual manifest resolver、generic catalog snapshot provider / GetToolSpec catalog provider、generic registry / static-provider / dynamic-provider / decorator-ref / snapshot-decorator adapter / runtime assembly container、generic readonly/enabled snapshot filter | partial:product registry snapshot access、`ToolUseContext` adapter、`GetToolSpec` Tool impl 和 concrete tools 仍在 core,并由 core `tools/product_runtime.rs` 作为单一 product runtime owner 组装;core 当前从 `bitfun-tool-packs` provider plan 物化内置工具列表,static-provider 安装 assembly、decorator reference、generic snapshot decorator adapter、provider-backed catalog runtime facade 与 readonly/enabled 过滤规则已委托给 `bitfun-agent-tools` | +| `bitfun-agent-tools` | 轻量 tool DTO / contract、portable tool context facts / provider、runtime restriction、host path normalization / runtime artifact URI / remote POSIX path pure contract、provider-neutral tool path resolution / absolute-path check / runtime artifact reference assembly、allowed-list / collapsed-tool execution gate policy、provider-neutral path policy root matching / denial message、pure manifest/exposure and GetToolSpec presentation/schema/static metadata/detail/result assembly / execution-plan contract、provider-backed tool catalog / GetToolSpec runtime facade、provider-backed GetToolSpec execution result helper / Tool-result vector adapter、generic contextual manifest resolver、generic catalog snapshot provider / GetToolSpec catalog provider、generic registry / static-provider / dynamic-provider / decorator-ref / snapshot-decorator adapter / runtime assembly container、generic readonly/enabled snapshot filter | partial:product registry snapshot access、`ToolUseContext` adapter、`GetToolSpec` Tool impl 和 concrete tools 仍在 core,并由 core `tools/product_runtime.rs` 作为单一 product runtime owner 组装;core 当前从 `bitfun-tool-packs` provider plan 物化内置工具列表,static-provider 安装 assembly、decorator reference、generic snapshot decorator adapter、provider-backed catalog runtime facade、readonly/enabled 过滤规则、provider-neutral tool path resolution / runtime artifact reference assembly 与 path policy 判定已委托给 `bitfun-agent-tools` | | `bitfun-tool-packs` | 由 feature group 隔离的工具 provider plan | partial:提供 basic / git / mcp / browser-web / computer-use / image-analysis / miniapp / agent-control feature-group 元数据和 product provider group plan;不得声明 concrete tools 已迁移 | | `bitfun-services-core` | Config、session、workspace、storage、filesystem、system services | partial:部分 pure helper 已迁出;config/workspace/filesystem runtime 多数仍在 core | | `bitfun-services-integrations` | Git、MCP、remote SSH、remote connect、file watch integrations | partial:MCP runtime 已迁入;remote SSH 仍只迁移低风险 contracts/helpers;remote-connect 已拥有 wire DTO、request builder、tracker state / registry lifecycle、tracker event reduction、dialog submission orchestration port/provider、file IO/path resolution helper 与 image-context adapter contract;concrete scheduler/session restore/terminal adapter、workspace-root source、response wrapping 与 product execution 仍在 core | @@ -160,9 +160,12 @@ owner 边界,否则不要把一个 feature group 继续拆成更小的 crate 可执行 service handle;workspace root 使用 session identity 的 logical path (remote 为 normalized remote root)。`PortableToolContextProvider` 只是只读 facts provider 合约,当前由 core `ToolUseContext` 实现;`ToolUseContext` 本体仍归 core 拥有。 -- host path normalization、runtime artifact URI 与 remote POSIX path containment - 现在是 `bitfun-agent-tools` 的纯路径契约;core `workspace_paths` / `restrictions` - 只保留 `BitFunError` 映射、workspace runtime-root lookup 与 `ToolUseContext` 集成。 +- host path normalization、runtime artifact URI、provider-neutral tool path resolution / + effective absolute-path check、runtime artifact reference assembly 与 remote + POSIX path containment 现在是 + `bitfun-agent-tools` 的纯路径契约;core `workspace_paths` / `restrictions` + 只保留 `BitFunError` 映射、workspace runtime-root lookup、local canonicalize 回调 + 与 `ToolUseContext` 集成。 - tool allowed-list 与 collapsed tool 的直接执行 gate policy 现在由 `bitfun-agent-tools` 作为纯契约持有;core pipeline 仍保存 `ToolUseContext.unlocked_collapsed_tools`,负责失败状态更新与 `BitFunError` @@ -235,6 +238,9 @@ owner 边界,否则不要把一个 feature group 继续拆成更小的 crate 或任何产品 crate feature set 调整。 - 高风险项不能作为 P2/P3 普通收尾任务顺带执行,必须先有等价性测试、port/provider 设计、 旧路径兼容策略和用户确认。 +- 从 2026-05-22 起,后续 PR 不再按 helper / guard / facade 小块拆分;除当前 draft PR + 的文档与保护补强外,每次 PR 都必须围绕一个完整高风险 owner 主题推进,并在动代码前 + 先写清设计、预保护、验证矩阵和对抗性审核方式。 - 后续 runtime 迁移以 `docs/plans/core-decomposition-plan.md` 的里程碑表为准,不再按零散 “剩余 PR 数量”临时拆分。LR1 已闭环为文档/边界/基线校准,不包含 runtime owner 迁移;后续高风险队列只允许按 H1-H5 的单一 owner 主题推进: @@ -282,11 +288,10 @@ owner 边界,否则不要把一个 feature group 继续拆成更小的 crate runtime assembly;未完成等价评审的 runtime owner 继续显式 core-owned。 - H5:optional feature/build-benefit evaluation。`bitfun-core default = []`、per-product feature matrix、依赖版本收敛和构建收益评估只能在 H1-H4 后独立进行。 -- H4 之后的剩余工作口径必须区分“当前闭环必需项”和“后续深度 runtime 迁移”: - 当前 H1-H4 主线闭环后,不再把 deferred/core-owned runtime 当作当前 PR 漏项; - 若继续外移高风险 owner,最多按 tool runtime、product-domain runtime、 - service/agent runtime 这 3 个条件性大型 PR 重新评审;这些 HR 项不是 H5 - 之后的必做项。H5 仍是单独且可选的 feature/build-benefit evaluation, +- H4 之后的剩余工作口径必须区分“低风险准备已完成”和“后续深度 runtime 迁移”: + 不再把 deferred/core-owned runtime 当作零散漏项;若继续外移高风险 owner, + 只允许按 tool runtime、product-domain runtime、service/agent runtime 这 3 个 + 大型 PR 重新评审和实施。H5 仍是单独且可选的 feature/build-benefit evaluation, 只能在选择继续外移的 HR 项完成或明确 defer 后评估。 - HR1-HR3 的共同底线是功能影响范围可控、无性能劣化且不改变产品发布形态: 不修改 default feature、产品 crate feature set、CI/release 覆盖、desktop/installer @@ -309,6 +314,11 @@ owner 边界,否则不要把一个 feature group 继续拆成更小的 crate 本体、runtime service handles 或 concrete tool behavior,并通过 remote workspace containment、runtime URI scope、path policy、task/description/preflight context materialization 与 cancellation hook 回归测试保护现有工具语义。 +- 当前受保护 HR1 迁移只把 provider-neutral tool path resolution / effective + absolute-path check、runtime artifact reference assembly、path policy root matching 与拒绝消息移入 + `bitfun-agent-tools`;core 仍负责 workspace/runtime root 获取、allowed root + 解析、local canonicalize、remote POSIX containment 回调、`BitFunError` 映射和 + `ToolUseContext` runtime binding。 - 已完成的 MCP runtime/dynamic tools、remote-connect tracker/wire/pure policy、 semantic baseline、product-domain port/facade 与 tool contract/helper 外移不得重复规划; 如果后续发现这些已完成项存在实现错误,应在对应 H 阶段记录问题、风险和修复方案, @@ -391,15 +401,24 @@ BitFun 的重构目标不是把 Desktop、CLI、Remote、Server 和 ACP 强行 `ssh-remote`、`tool-packs` 或 `product-domains` 这些明确 feature owner 显式启用。 后续新增 optional dependency 时,必须同步更新 owner feature 规则,避免出现隐式或孤儿 feature 引用。 +- 当前 boundary check 已把该规则变成全量覆盖检查:凡列入 no-default product/runtime + forbidden list 且仍存在为 optional dependency 的条目,必须在 feature-owner 规则中声明; + owner feature 缺失或未显式启用对应依赖都会失败。 - Boundary check 也必须保护产品入口的完整能力装配:Desktop、CLI、ACP 对 `bitfun-core` 的依赖必须保持 `default-features = false` 且显式启用 `product-full`, 避免产品完整 runtime 退回到隐式默认 feature;脚本会扫描产品入口范围内新增的 `bitfun-core` 依赖,防止遗漏显式装配规则。 - 在单独完成产品矩阵评审前,Boundary check 必须继续锁定 `bitfun-core default = ["product-full"]`,不得把默认 feature 变轻作为依赖裁剪的副作用。 +- `bitfun-core/product-full` 必须继续显式聚合当前 owner feature group:`ssh-remote`、 + `product-domains`、`service-integrations` 和 `tool-packs`,防止完整产品 runtime + 在后续依赖裁剪中被隐式拆散。 - Boundary check 还必须锁定 owner crate 的 feature graph:`tool-packs`、 `services-integrations`、`product-domains` 的 `default` 保持空,`product-full` - 只显式聚合当前 owner crate 已声明的 feature group。 + 只显式聚合当前 owner crate 已声明的 feature group,且不得夹带未纳入规则的 feature + 或 dependency shortcut。`services-integrations` 与 `product-domains` 的 runtime/domain + optional dependency 也必须由对应 feature group 显式拥有,避免 owner crate default-light + 边界回退。 - Core 的 `service-integrations` feature 当前仍是完整 `product-full` runtime assembly 的一部分, 不是可单独发布或单独验证的产品形态;MCP/remote-connect/review-platform 仍引用 agentic、 snapshot 或 product execution owner。若未来要让该 feature 独立可编译,必须先做 diff --git a/docs/plans/core-decomposition-plan.md b/docs/plans/core-decomposition-plan.md index 791488d27..c126c5681 100644 --- a/docs/plans/core-decomposition-plan.md +++ b/docs/plans/core-decomposition-plan.md @@ -1,6 +1,6 @@ # BitFun Core 拆解与构建提速可执行计划 -> **执行约定:** 后续实施本计划时,建议按独立 PR 分步推进。每个阶段使用本文的 checkbox 跟踪,不要把多个高风险拆分混在一个 PR 中。 +> **执行约定:** 后续实施本计划时,按完整 owner 主题分步推进。低风险准备项已经收敛,后续 PR 不再提交零散 helper / guard 小块;每个高风险 PR 必须先补设计、预保护、等价验证和对抗性审核方案,再移动 runtime owner。 **目标:** 将当前职责过重的 `bitfun-core` 逐步拆成边界明确、依赖可控、可独立验证的 Rust crate 和能力 feature,同时不改变任何产品功能、CI/release 构建内容、关键构建脚本执行逻辑或各形态产品的依赖范围。 @@ -147,6 +147,7 @@ git diff -- package.json scripts/dev.cjs scripts/desktop-tauri-build.mjs scripts - 单个 feature group 迁移。 - facade/re-export 收敛。 - 低风险直接依赖版本收敛。 +- 单个高风险 owner 迁移,且必须先满足 `0A.7` 的设计和保护门禁。 禁止: @@ -193,6 +194,39 @@ git diff -- package.json scripts/dev.cjs scripts/desktop-tauri-build.mjs scripts 这些候选不阻塞里程碑推进,也不应优先于 feature 安全网和 `core-types` / `agent-stream` 拆分。 +### 0A.7 高风险 owner 迁移 PR 门禁 + +从 2026-05-22 起,除当前 draft PR 的文档/保护补强外,后续 core decomposition +PR 默认进入高风险 runtime owner 迁移队列。不得再把单个 helper、单条边界检查或 +小型 facade 移动包装成独立 PR;这些只能作为同一个 owner 迁移 PR 的预保护或收尾。 + +每个高风险 PR 开始写代码前,必须在本文或最近的模块文档中先记录: + +- **Owner 设计:** 当前 core-owned runtime 是什么,新 owner crate / core adapter + 分别负责什么,旧公开路径如何兼容。 +- **行为盘点:** 列出输入、输出、错误映射、日志、异步时序、feature gate、缓存 / + registry / manifest 副作用、产品表面差异。 +- **预保护:** 先补或复用迁移前 snapshot / focused regression / boundary check。 + 没有可执行保护时,不移动 runtime owner。 +- **实施边界:** 每个 PR 只迁移一个 owner 主题;不同时改产品 feature set、 + default feature、构建脚本、UI/命令语义或第三方依赖大版本。 +- **回滚边界:** 保留旧路径 facade 或 core adapter,保证可以回退到 core-owned + runtime 而不影响产品入口。 +- **验证矩阵:** 至少覆盖 owner crate tests、core focused tests、boundary check、 + `cargo check -p bitfun-core --features product-full`,并按影响面增加 desktop / + CLI / ACP / remote / web product checks。 +- **对抗性审核:** 提交前从第三方角度检查是否存在行为漂移、性能劣化、重复 + runtime materialization、锁/任务生命周期变化、产品发布形态变化、依赖方向回流。 + +暂停条件: + +- 需要改变用户可见行为、权限策略、产品命令或默认能力才能完成迁移。 +- owner crate 必须依赖回 `bitfun-core` 才能工作。 +- 等价测试无法表达关键行为,或者只能依赖人工观察确认。 +- 迁移会引入额外进程/网络启动、重复 registry/manifest 构建、无界缓存或更重的 + 默认编译面。 +- 最新 `main` 合入改变了相关 runtime 行为,但文档和保护测试尚未同步。 + --- ## 1. 当前问题与风险合集 @@ -1750,19 +1784,25 @@ P2 后产品表面契约轨道(contract-only): - product registry / manifest assembly 或 concrete tool implementation 迁移后工具清单、expanded/collapsed exposure、`GetToolSpec` unlock state 无法证明等价。 - 新 owner crate 反向依赖 core。 -**剩余工作压缩为 5 个 PR(2026-05-13):** +**历史 PR 拆分口径校正(2026-05-22):** + +2026-05-13 的“剩余工作压缩为 5 个 PR”是历史拆分口径,不再作为当前执行队列。 +其中 MCP runtime、remote-connect tracker/wire/pure policy、agent-tools/tool-packs 低风险 +contract、product-domain facade 与 H4 boundary closure 已分别闭环;后续不得再把这些已完成项 +拆成小 PR 重复提交。 -1. `services-integrations` runtime 收口:迁移 remote-SSH 中不直接持有 SSH channel / SFTP / terminal handle 的 workspace registry、session mirror 与轻量 runtime helper;继续保留 SSH manager / remote FS / remote terminal 的 core-owned assembly,直到 port/provider 合约明确。`file-watch` 已由 `services-integrations` 拥有,只做 contract 复核;announcement 只迁移不依赖 config service / embedded content / remote fetch 的 state 或 eligibility helper。验收重点是 owner crate contract test、旧路径 facade、boundary check、workspace check/test。 -2. 已完成:MCP runtime 与 dynamic tools:MCP config service orchestration、server process / transport lifecycle、adapter、dynamic tool/resource/prompt provider 已归属 `bitfun-services-integrations`;未混入 remote-connect 或 product tool runtime manifest / `GetToolSpec` 执行 owner 化。验收重点是 MCP wire shape、auth/config merge、dynamic manifest 快照和 core registry / manifest 集成等价。 - - 保留边界:`bitfun-core` 只保留 core `ConfigService` store adapter、OAuth data-dir 注入、`BitFunError` 映射、legacy facade 和与全局 tool registry / manifest 的组装调用;配置写入、OAuth、SSE/session 与 registry / manifest 行为不得在本 PR 中改变。 - - 后续切片:MCP concrete tool integration / product registry / manifest assembly 继续保留 dynamic provider metadata、工具清单顺序、expanded/collapsed exposure 和 snapshot wrapper 等价测试。 - - 文档校正:P2 后补充文档中的 MCP runtime step 已由本 PR2 闭环;后续 MCP 相关工作只保留 concrete tool implementation 迁移或 product registry / manifest assembly,不再重复迁移 config/process/transport lifecycle。 -3. 已完成:remote-connect tracker / wire / pure policy owner slice:产品表面 DTO、remote command/response wire DTO、remote model catalog DTO、poll response assembly / model catalog poll delta、remote chat/image/tool/session wire DTO、relay/bot session/submission request builder、remote image attachment/request DTO、`AgentTurnCancellationPort`、`RemoteControlStatePort`、`RuntimeEventSink`、`RemoteSessionStateTracker`、`RemoteSessionTrackerRegistry`、`TrackerEvent`、legacy image context fallback / preference、restore target decision、cancel decision、remote workspace file IO/path helper、image-context adapter contract 与 remote file transfer size/chunk/name policy 已具备 owner/port 契约;core 仍保留 tracker host adapter、dispatcher/product execution、workspace-root source、response/base64 wrapping 与 `ImageContextData` concrete impl。 - - 本轮收口:remote-connect 在当前批次以 tracker / wire / pure policy / registry lifecycle / dialog orchestration / file helper / image-context adapter contract 归 owner crate、dispatcher / product execution 显式保留 core-owned 闭环;若未来继续迁移 concrete scheduler/session restore/terminal adapter、remote-SSH runtime 或 agent registry/scheduler,必须另起 port/provider 设计与行为等价评审,不得混入 tool/provider owner 化。 -4. 已完成本轮可提交闭环:agent tools + `tool-packs` owner 化低风险部分。纯 tool contract/provider metadata、runtime restriction DTO、path resolution DTO、generic tool registry / static-provider / dynamic-provider container、generic catalog snapshot provider、generic GetToolSpec catalog provider、provider-backed `ToolCatalogRuntime`、`PortableToolContextProvider` 只读 facts provider、纯 manifest/exposure 契约、generic contextual manifest resolver,以及 GetToolSpec presentation/schema/detail/runtime facade helper 已迁入 `bitfun-agent-tools`,并为 dynamic provider contract 提供 `agent-tools` 兼容 re-export;`tool-packs` 现在提供 feature-group 元数据和 product provider group plan,但不注册或迁移具体工具;core tool runtime 保留 concrete tool materialization、product snapshot wrapper adapter、`dyn Tool` / `ToolUseContext` 适配、product registry snapshot access、agent policy 和 `GetToolSpec` 执行。`ToolUseContext`、`GetToolSpec` 执行与 concrete tool implementation 按 feature group 外移需要新的 service port/provider 设计,必须保持 builtin/readonly/dynamic manifest、expanded/collapsed exposure、prompt stub、unlock state、snapshot wrapping、runtime restrictions、cancellation 与 Deep Review tool flow 等价,作为后续高风险迁移单独审视。 -5. `product-domains` runtime + core facade finalization:迁移 miniapp runtime/compiler/builtin 与 function-agent 运行逻辑,最后把 `bitfun-core` 收敛为 facade + product runtime assembly;不在本 PR 中修改 `bitfun-core default = []` 或 per-product feature matrix。 +当前执行队列改为: -`bitfun-core default = []`、per-product feature set、构建矩阵和 release 能力调整仍作为重构完成后的独立评估,不计入上述 5 个 PR。 +| 序号 | PR 主题 | 必须完成的范围 | 不允许混入 | 合入门禁 | +|---|---|---|---|---| +| 当前 draft | HR1/H5 保护闭环扩展 | 把当前 path/runtime contract 迁移纳入完整边界说明、feature/dependency 保护和文档一致性;若继续加代码,只允许作为同一 owner 迁移的预保护或等价测试 | 新产品行为、default feature、产品 feature set、构建脚本、concrete tool IO | `0A.7` 审核、boundary check、focused Rust tests、PR 描述明确不是零散 helper PR | +| HR-A | Tool runtime owner migration | 以 tool runtime 为单一 owner 主题,处理 `ToolUseContext` / manifest / `GetToolSpec` / snapshot / concrete tool 边界中已设计且可保护的部分 | product-domain runtime、remote/service runtime、H5 feature matrix | tool registry / manifest / `GetToolSpec` / dynamic metadata / snapshot / Deep Review tool flow 等价 | +| HR-B | Product-domain runtime owner migration | 以 MiniApp 或 function-agent runtime 为单一 owner 主题,只迁移已有 port/provider 与 regression 保护的路径 | tool runtime、remote/service runtime、surface 行为变更 | MiniApp/function-agent focused regression、Git/AI/PathManager/worker 边界清晰 | +| HR-C | Service / agent runtime owner migration | 以 remote-connect/remote-SSH/scheduler/agent registry 中一个 owner 主题推进,先补端口与行为快照 | tool/product-domain runtime、feature matrix、产品逻辑变更 | remote/session/subagent/citation 行为等价,旧路径兼容,产品 checks 覆盖触碰面 | +| H5 | feature/build-benefit evaluation | 只评估 feature graph、dependency profile 和构建收益数据 | runtime owner 迁移、default feature 副作用、构建脚本变更 | feature graph baseline、cargo metadata/tree 证据、产品入口完整能力不变 | + +`bitfun-core default = []`、per-product feature set、构建矩阵和 release 能力调整仍作为 H5 的独立评估; +不得与 HR-A/HR-B/HR-C 的 runtime owner 迁移混合。 **低风险准备 PR 合并锁定(2026-05-19):** @@ -1986,16 +2026,20 @@ HR3:service / agent runtime deep owner migration 的主要风险和控制点 因此,当前文档口径下的剩余数量是: -- 必需主线:0 个新增 runtime 迁移 PR;H4 本身完成后即可进入提交审查。 -- 可选评估:1 个 H5 PR。 -- 若继续追求更深的 owner 化:最多 3 个条件性高风险 runtime PR + 1 个 H5 PR。 +- 低风险准备:0 个新增小 PR。不得再把 helper / guard / facade 小块单独提交。 +- 当前 draft PR:必须扩展成一个完整的 HR1/H5 边界闭环,或在提交前明确说明它只作为 + 高风险迁移前的保护网;不能以零散迁移 PR 的形式 ready。 +- 后续主线:最多 3 个高风险 runtime owner PR,分别对应 tool runtime、 + product-domain runtime、service/agent runtime。每个 PR 都必须满足 `0A.7`。 +- 可选评估:1 个 H5 feature/build-benefit PR。它只能在已选择继续外移的 HR 项完成或 + 明确 defer 后执行,且不得混入 runtime owner 迁移。 不计入上述数量:缺陷修复、行为变更、冗余清理和构建脚本调整。它们必须独立评估, 不能伪装成 core decomposition 的剩余里程碑。 本节解释“为什么统计总是 4-5 个”:之前把低风险准备事项拆成 R1-R4,导致每次都看起来还剩 4 个小 PR;这些准备事项已统一为 `LR1` 并在 2026-05-19 闭环。默认回答必须是: -低风险准备已完成,随后进入高风险 runtime 迁移队列;`H5` 仍是独立可选评估项。 +低风险准备已完成,随后只按完整高风险 owner 迁移 PR 推进;`H5` 仍是独立可选评估项。 **LR1 闭环结果(2026-05-19):** @@ -2142,7 +2186,7 @@ git diff -- package.json scripts/dev.cjs scripts/desktop-tauri-build.mjs scripts 29. 已完成:H1 tool catalog runtime facade slice。`bitfun-agent-tools::ToolCatalogRuntime` 接管 provider-backed visible-tools、prompt-visible manifest 与 readonly enabled catalog 查询入口;core 继续持有 product registry snapshot、agent policy、`dyn Tool` / `ToolUseContext` adapter 和 product facade,不迁移 `ToolUseContext`、`GetToolSpecTool` Tool impl、collapsed unlock state、snapshot wrapper implementation 或 concrete tools。 30. 已完成:H1 GetToolSpec Tool adapter facade slice。`bitfun-agent-tools::GetToolSpecRuntime::call_results` 接管单次执行结果到 `Vec` 的通用适配形状,core `product_runtime.rs` 暴露 product `resolve_product_get_tool_spec_results`,`GetToolSpecTool::call_impl` 只保留 product facade 委托和 `BitFunError` 映射;不迁移 `ToolUseContext`、runtime manifest assembly、unlock state owner、assistant rendering 语义或 concrete tools。 31. 已完成:H1 product provider plan closure。`bitfun-tool-packs::product_tool_provider_group_plan` 接管 product provider group id / feature group / tool-name order 计划,core `product_runtime.rs` 只按该计划物化 concrete tools 并继续注入 snapshot wrapper;不迁移 concrete tool implementation、`ToolUseContext`、runtime service handles 或 tool behavior。 -32. HR1 当前闭环状态:工具 runtime 的 provider-neutral contract、host path normalization / runtime artifact URI / remote POSIX path pure contract、allowed-list / collapsed-tool execution gate policy、manifest/catalog runtime facade、GetToolSpec facade、static-provider assembly、readonly filtering、provider plan 与 core product adapter 已收敛;core 内部 product runtime adapter 已统一到 `product_runtime.rs`。本轮进一步把 `ToolUseContext` 上的 workspace service accessor、runtime artifact lookup、path policy enforcement、tool pipeline/description/preflight context materialization、tool-call cancellation/post-call hook wrapper 和 Deep Review light checkpoint 绑定集中到 `tool_context_runtime.rs`,作为 core-owned runtime binding owner,并补齐 remote workspace containment、runtime URI scope、path policy、task/description/preflight context materialization 与 cancellation hook 回归测试;`framework.rs` 只保留 context shape、portable facts projection 和 `Tool` trait。`ToolUseContext` 本体和 concrete tools 仍显式 core-owned;继续外移会触碰 workspace services、cancellation、Deep Review hooks 或具体工具 IO,必须作为后续高风险 owner 迁移单独确认。 +32. HR1 当前闭环状态:工具 runtime 的 provider-neutral contract、host path normalization / runtime artifact URI / remote POSIX path pure contract、allowed-list / collapsed-tool execution gate policy、manifest/catalog runtime facade、GetToolSpec facade、static-provider assembly、readonly filtering、provider plan 与 core product adapter 已收敛;core 内部 product runtime adapter 已统一到 `product_runtime.rs`。本轮进一步把 `ToolUseContext` 上的 workspace service accessor、runtime artifact lookup、path policy enforcement、tool pipeline/description/preflight context materialization、tool-call cancellation/post-call hook wrapper 和 Deep Review light checkpoint 绑定集中到 `tool_context_runtime.rs`,作为 core-owned runtime binding owner,并补齐 remote workspace containment、runtime URI scope、path policy、task/description/preflight context materialization 与 cancellation hook 回归测试;`framework.rs` 只保留 context shape、portable facts projection 和 `Tool` trait。当前受保护 HR1 迁移把 provider-neutral tool path resolution / effective absolute-path check、runtime artifact reference assembly、path policy root matching 与拒绝消息移入 `bitfun-agent-tools`;core 继续负责 workspace/runtime root 获取、allowed root 解析、local canonicalize、remote POSIX containment 回调、`BitFunError` 映射和 `ToolUseContext` runtime binding。`ToolUseContext` 本体和 concrete tools 仍显式 core-owned;继续外移会触碰 workspace services、cancellation、Deep Review hooks 或具体工具 IO,必须作为后续高风险 owner 迁移单独确认。 33. H4 已完成:facade / boundary finalization。`scripts/check-core-boundaries.mjs` 的 regular check 和 self-test 已覆盖 remote-connect file/image/dialog owner anchor、core adapter/deferred owner anchor 与既有 duplicate-path required rule;root / core / services-integrations 文档与当前 H1-H3 代码状态一致,不声明 remote-SSH runtime、agent registry/scheduler、default feature 或构建收益已完成。 34. H5 已启动并完成当前闭环:第一步建立 `bitfun-core --no-default-features` 编译闭环, 证明 `ssh-remote` 关闭时不再编译 russh-backed runtime,并通过 disabled surface @@ -2154,10 +2198,136 @@ git diff -- package.json scripts/dev.cjs scripts/desktop-tauri-build.mjs scripts non-optional 回流、optional dependency feature-owner 映射、产品入口显式 `product-full` 装配、产品入口新增 core 依赖覆盖扫描和 `default = ["product-full"]` 保留,以及 owner crate default-light / `product-full` 显式 feature group 组装,同时保持产品 crate feature set、 - release/CI 脚本和用户可见能力不变。no-default core 当前只作为 + release/CI 脚本和用户可见能力不变。本轮进一步把 H5 feature-matrix guard 收紧为: + core 的 product/runtime optional 依赖必须全量声明 feature owner,owner feature 必须存在且 + 显式启用对应依赖,`bitfun-core/product-full` 必须显式聚合当前 owner feature group, + owner crate 的 `product-full` 不得包含未声明 feature group,`services-integrations` + 与 `product-domains` 的 optional runtime/domain dependency 也必须由显式 feature group + 拥有。 + no-default core 当前只作为 runtime-surface-light facade,已减少 direct dependency profile,但不声明 per-product feature matrix、构建收益或 runtime owner 深迁移已完成。 +### 11A. 后续高风险 PR 队列 + +后续不再新增低风险碎片 PR。每个 PR 必须按一个完整 owner 主题提交,先设计保护网, +再移动 runtime owner,最后用对抗性审核确认没有功能偏移。 + +#### HR-A:Tool Runtime Owner Migration + +目标:在不改变工具可见性、manifest、`GetToolSpec`、collapsed unlock、snapshot wrapper、 +Deep Review tool flow 或具体工具 IO 的前提下,继续收敛 tool runtime owner。 + +预保护: + +- 固化 product registry snapshot、expanded/collapsed exposure、prompt-visible manifest、 + `GetToolSpec` summary/detail/result、dynamic provider metadata、snapshot wrapper 覆盖顺序。 +- 覆盖 desktop/MCP/ACP catalog 等价、Deep Review 修改类工具 checkpoint hook、 + cancellation/post-call hook、runtime artifact URI 和 remote workspace path policy。 +- 边界脚本继续禁止 `agent-tools` / `tool-packs` 依赖 core 或 concrete service。 + +实施边界: + +- 可迁移 provider-neutral runtime contract、adapter facade、只依赖 portable facts 的 + registry/manifest assembly 规则。 +- `ToolUseContext` 本体、workspace services、cancellation token、Deep Review hook、 + concrete tools、dynamic MCP concrete execution 和 tool IO 只有在已有 port/provider + 设计和等价测试后才能移动。 +- 不改变 tool name、schema、prompt stub、readonly/enabled/filtering、unlock state 生命周期。 + +审核门: + +- 对比迁移前后 registry / manifest / `GetToolSpec` snapshot。 +- 检查是否新增重复 registry/materialization 或额外 async/runtime work。 +- 执行 `cargo test -p bitfun-agent-tools`、`cargo test -p bitfun-core tool_context_runtime -- --nocapture`、 + `node scripts/check-core-boundaries.mjs`、`cargo check -p bitfun-core --features product-full`; + 若触碰 dynamic MCP / Deep Review / desktop catalog,再补对应 focused tests。 + +#### HR-B:Product-Domain Runtime Owner Migration + +目标:在不改变 MiniApp filesystem IO、worker process、host dispatch、built-in asset seed / +marker IO、function-agent Git/AI 调用和 Startchat 时序的前提下,继续推进 +`bitfun-product-domains` owner 化。 + +预保护: + +- 复用并扩展 MiniApp import/sync/recompile/rollback/deps-state、built-in seed/update marker、 + customization metadata、function-agent staged diff、prompt/JSON extraction/domain error mapping + 等价测试。 +- 补齐 Git/AI port adapter 的输入输出、错误映射、fallback、`analyze_git=false`、非 Git + 目录和 no-HEAD diff 行为快照。 + +实施边界: + +- 可迁移纯决策、DTO、port-backed facade、domain parsing policy 和 core adapter 委托层。 +- MiniApp filesystem IO、worker process、asset include/seed、marker IO、host dispatch、 + function-agent Git service / AI client / provider acquisition 继续 core-owned,除非本 PR + 先补完整 port/provider 设计和回归。 +- 不改变 MiniApp permission policy、bundle/update semantics、Git commit-message 生成行为、 + Startchat work-state 输出或产品 surface。 + +审核门: + +- 对比 core adapter 与 owner facade 的快照输出。 +- 检查是否把 PathManager、Git/AI concrete service、worker runtime 或 host dispatch 下沉到 + `product-domains`。 +- 执行 `cargo test -p bitfun-product-domains`、相关 `bitfun-core` MiniApp/function-agent focused + tests、`node scripts/check-core-boundaries.mjs`、`cargo check -p bitfun-core --features product-full`。 + +#### HR-C:Service / Agent Runtime Owner Migration + +目标:在不改变 remote-connect、remote-SSH、terminal pre-warm、scheduler/registry、 +subagent visibility、background delivery、DeepResearch citation renumber hook 和 session restore +语义的前提下,继续收敛 service/agent runtime owner。 + +预保护: + +- 固化 remote command/response wire、poll/model catalog delta、queue/event fanout、restore -> + terminal pre-warm -> scheduler submit 顺序、file full/chunk/info、image context + fallback/preference、remote workspace startup guard。 +- 固化 mode-scoped subagent availability、`Multitask` / `GeneralPurpose` registration、 + background result delivery、running-turn injection、idle-session follow-up、DeepResearch + post-turn citation artifact 语义。 + +实施边界: + +- 可迁移只读 facts、queue/restore decision、remote workspace DTO、port/provider contract 和 + core adapter binding。 +- concrete scheduler/session restore、workspace-root source、response/base64 wrapping、 + `ImageContextData` concrete impl、remote-SSH runtime、terminal adapter、agent registry/scheduler + 继续 core-owned,除非本 PR 有端到端 regression 和明确回滚路径。 +- 不统一 Desktop / CLI / ACP / Remote / Server surface 命令或 presentation。 + +审核门: + +- 对比 remote/session/subagent/citation 行为快照。 +- 检查是否引入新全局 coordinator 访问、反向依赖 core、额外 network/process startup 或 + scheduler 生命周期变化。 +- 执行 owner crate tests、remote-connect / scheduler / agent runtime focused tests、 + `node scripts/check-core-boundaries.mjs`、`cargo check -p bitfun-core --features product-full`; + 按触碰范围补 desktop / CLI / ACP / server checks。 + +#### H5:Feature / Build-Benefit Evaluation + +目标:只评估 feature matrix、dependency profile 和构建收益,不迁移 runtime owner。 + +预保护: + +- 先记录 `bitfun-core`、owner crates、Desktop、CLI、ACP、Server、Relay 的 feature graph / + dependency profile。 +- 确认产品 crate 继续显式启用完整能力,release/CI/fast build 脚本无 diff。 + +实施边界: + +- 可补 boundary check、cargo metadata/cargo tree 证据和文档。 +- 不修改 default feature、产品 feature set、构建脚本或产品能力。 + +审核门: + +- 对比 no-default、product-full、产品入口依赖面。 +- 明确哪些 owner 已能绕开 heavy runtime,哪些仍因 core facade 阻塞;不得把局部收益写成 + 整体构建收益。 + 冗余清理 PR 不进入上述主线序号。只有在满足 `0A.6` 的绝对等价要求时,才可以插入到相邻里程碑之间,并且不得与主线拆分 PR 混合。 --- diff --git a/scripts/check-core-boundaries.mjs b/scripts/check-core-boundaries.mjs index 0b655a90c..4de162017 100644 --- a/scripts/check-core-boundaries.mjs +++ b/scripts/check-core-boundaries.mjs @@ -240,6 +240,8 @@ const dependencyProfileRules = [ reason: 'product-domains default profile must not compile runtime/platform helpers', forbiddenNonOptionalDeps: [ 'dirs', + 'log', + 'sha2', 'reqwest', 'git2', 'rmcp', @@ -277,6 +279,7 @@ const dependencyProfileRules = [ 'thiserror', 'tokio-util', 'tokio-tungstenite', + 'uuid', 'bitfun-relay-server', ], }, @@ -325,6 +328,42 @@ const optionalDependencyFeatureOwnerRules = [ { depName: 'x25519-dalek', ownerFeatures: ['service-integrations'] }, ], }, + { + crateName: 'services-integrations', + reason: + 'services-integrations optional runtime dependencies must stay owned by explicit integration features', + dependencies: [ + { depName: 'aes-gcm', ownerFeatures: ['mcp'] }, + { depName: 'anyhow', ownerFeatures: ['mcp'] }, + { depName: 'async-trait', ownerFeatures: ['mcp', 'remote-connect'] }, + { depName: 'base64', ownerFeatures: ['mcp'] }, + { depName: 'bitfun-runtime-ports', ownerFeatures: ['remote-connect'] }, + { depName: 'bitfun-services-core', ownerFeatures: ['git', 'mcp'] }, + { depName: 'chrono', ownerFeatures: ['git'] }, + { depName: 'dunce', ownerFeatures: ['remote-ssh'] }, + { depName: 'futures', ownerFeatures: ['mcp'] }, + { depName: 'git2', ownerFeatures: ['git'] }, + { depName: 'notify', ownerFeatures: ['file-watch'] }, + { depName: 'rand', ownerFeatures: ['mcp'] }, + { depName: 'reqwest', ownerFeatures: ['mcp'] }, + { depName: 'rmcp', ownerFeatures: ['mcp'] }, + { depName: 'sha2', ownerFeatures: ['remote-ssh'] }, + { depName: 'sse-stream', ownerFeatures: ['mcp'] }, + { depName: 'thiserror', ownerFeatures: ['git'] }, + { depName: 'tokio-util', ownerFeatures: ['remote-ssh'] }, + { depName: 'uuid', ownerFeatures: ['remote-connect'] }, + ], + }, + { + crateName: 'product-domains', + reason: + 'product-domains optional runtime dependencies must stay owned by explicit product-domain features', + dependencies: [ + { depName: 'dirs', ownerFeatures: ['miniapp'] }, + { depName: 'log', ownerFeatures: ['function-agents'] }, + { depName: 'sha2', ownerFeatures: ['miniapp'] }, + ], + }, ]; const productCoreFeatureAssemblyRules = [ @@ -350,6 +389,13 @@ const productCoreFeatureAssemblyRules = [ const productCoreFeatureAssemblyScanRoots = ['src/apps', 'src/crates/acp']; +const coreProductFullFeatureAssemblyRule = { + manifestPath: 'src/crates/core/Cargo.toml', + featureName: 'product-full', + requiredFeatureRefs: ['ssh-remote', 'product-domains', 'service-integrations', 'tool-packs'], + reason: 'bitfun-core product-full must explicitly assemble current owner feature groups', +}; + const ownerCrateFeatureAssemblyRules = [ { manifestPath: 'src/crates/tool-packs/Cargo.toml', @@ -1756,6 +1802,30 @@ const requiredContentRules = [ regex: /\bpub fn validate_collapsed_tool_usage\b/, message: 'missing collapsed-tool execution gate policy', }, + { + regex: /\bpub fn is_tool_path_allowed_by_resolved_roots\b/, + message: 'missing provider-neutral path policy root matcher', + }, + { + regex: /\bpub fn build_tool_path_policy_denial_message\b/, + message: 'missing provider-neutral path policy denial message', + }, + { + regex: /\bpub fn resolve_tool_path_with_context\b/, + message: 'missing provider-neutral tool path resolution owner', + }, + { + regex: /\bpub fn tool_path_is_effectively_absolute\b/, + message: 'missing provider-neutral tool path absolute check', + }, + { + regex: /\bpub fn build_tool_runtime_artifact_reference\b/, + message: 'missing provider-neutral runtime artifact reference builder', + }, + { + regex: /\bpub fn build_tool_session_runtime_artifact_reference\b/, + message: 'missing provider-neutral session runtime artifact reference builder', + }, { regex: /\bpub fn sort_tool_manifest_definitions\b/, message: 'missing prompt-visible manifest ordering helper', @@ -2638,6 +2708,30 @@ const requiredContentRules = [ regex: /\benforce_path_operation\b/, message: 'missing runtime path policy binding', }, + { + regex: /\bis_tool_path_allowed_by_resolved_roots\b/, + message: 'missing path policy owner delegation to agent-tools', + }, + { + regex: /\bbuild_tool_path_policy_denial_message\b/, + message: 'missing shared path policy denial contract', + }, + { + regex: /\bresolve_tool_path_with_context\b/, + message: 'missing shared tool path resolution owner delegation', + }, + { + regex: /\btool_path_is_effectively_absolute\b/, + message: 'missing shared tool path absolute owner delegation', + }, + { + regex: /\bbuild_tool_runtime_artifact_reference\b/, + message: 'missing runtime artifact reference owner delegation', + }, + { + regex: /\bbuild_tool_session_runtime_artifact_reference\b/, + message: 'missing session runtime artifact reference owner delegation', + }, { regex: /\bworkspace_path_resolution_rejects_absolute_paths_outside_remote_workspace\b/, message: 'missing remote workspace containment regression', @@ -4376,6 +4470,11 @@ function runManifestParserSelfTest() { throw new Error(`${rule.manifestPath} must require bitfun-core product-full`); } } + for (const featureName of ['ssh-remote', 'product-domains', 'service-integrations', 'tool-packs']) { + if (!coreProductFullFeatureAssemblyRule.requiredFeatureRefs.includes(featureName)) { + throw new Error(`core product-full assembly rule must require ${featureName}`); + } + } const discoveredProductCoreManifests = collectProductCoreDependencyManifestPaths([ { manifestPath: 'src/apps/desktop/Cargo.toml', @@ -4406,6 +4505,15 @@ function runManifestParserSelfTest() { throw new Error(`owner crate feature assembly rule must cover ${manifestPath}`); } } + for (const rule of ownerCrateFeatureAssemblyRules) { + const declaredFeatures = new Set(rule.requiredProductFullFeatures); + if (declaredFeatures.size !== rule.requiredProductFullFeatures.length) { + throw new Error(`${rule.manifestPath} product-full guard must not duplicate feature groups`); + } + if (rule.requiredProductFullFeatures.some((featureName) => featureName.startsWith('dep:'))) { + throw new Error(`${rule.manifestPath} product-full guard must track owner feature groups only`); + } + } const parsedFeatures = parseManifestFeatures([ '[package]', @@ -4528,6 +4636,28 @@ function runManifestParserSelfTest() { throw new Error(`core tool restrictions boundary rule must forbid contract: ${contract}`); } } + const agentToolsFrameworkRule = requiredContentRules.find( + (rule) => rule.path === 'src/crates/agent-tools/src/framework.rs', + ); + if (!agentToolsFrameworkRule) { + throw new Error('missing agent-tools framework boundary rule'); + } + const agentToolsFrameworkContracts = [ + 'is_tool_path_allowed_by_resolved_roots', + 'build_tool_path_policy_denial_message', + 'resolve_tool_path_with_context', + 'tool_path_is_effectively_absolute', + 'build_tool_runtime_artifact_reference', + 'build_tool_session_runtime_artifact_reference', + ]; + const agentToolsFrameworkRuleText = agentToolsFrameworkRule.patterns + .map((pattern) => pattern.regex.source) + .join('\n'); + for (const contract of agentToolsFrameworkContracts) { + if (!agentToolsFrameworkRuleText.includes(contract)) { + throw new Error(`agent-tools framework boundary rule must require contract: ${contract}`); + } + } const coreWorkspacePathRule = forbiddenContentRules.find( (rule) => rule.path === 'src/crates/core/src/agentic/tools/workspace_paths.rs', ); @@ -4571,8 +4701,16 @@ function runManifestParserSelfTest() { const productDomainProfile = dependencyProfileRules.find( (rule) => rule.crateName === 'product-domains', ); - if (!productDomainProfile?.forbiddenNonOptionalDeps.includes('dirs')) { - throw new Error('product-domains default profile must forbid non-optional dirs'); + for (const dep of ['dirs', 'log', 'sha2']) { + if (!productDomainProfile?.forbiddenNonOptionalDeps.includes(dep)) { + throw new Error(`product-domains default profile must forbid non-optional ${dep}`); + } + } + const servicesIntegrationsDefaultProfile = dependencyProfileRules.find( + (rule) => rule.crateName === 'services-integrations', + ); + if (!servicesIntegrationsDefaultProfile?.forbiddenNonOptionalDeps.includes('uuid')) { + throw new Error('services-integrations default profile must forbid non-optional uuid'); } const coreProfile = dependencyProfileRules.find((rule) => rule.crateName === 'core'); for (const dep of ['git2', 'rmcp', 'image', 'tool-runtime', 'bitfun-relay-server']) { @@ -4583,8 +4721,16 @@ function runManifestParserSelfTest() { const coreOptionalOwnerRule = optionalDependencyFeatureOwnerRules.find( (rule) => rule.crateName === 'core', ); + const coreOptionalOwnerDeps = new Set( + coreOptionalOwnerRule?.dependencies.map((dependency) => dependency.depName) ?? [], + ); + for (const dep of coreProfile?.forbiddenNonOptionalDeps ?? []) { + if (!coreOptionalOwnerDeps.has(dep)) { + throw new Error(`core optional dependency owner rule must cover forbidden dependency ${dep}`); + } + } for (const dep of ['git2', 'rmcp', 'image', 'tool-runtime', 'bitfun-relay-server']) { - if (!coreOptionalOwnerRule?.dependencies.some((dependency) => dependency.depName === dep)) { + if (!coreOptionalOwnerDeps.has(dep)) { throw new Error(`core optional dependency owner rule must cover ${dep}`); } } @@ -4594,6 +4740,22 @@ function runManifestParserSelfTest() { if (!coreGit2Owner?.ownerFeatures.includes('service-integrations')) { throw new Error('core optional dependency owner rule must keep git2 under service-integrations'); } + const servicesOptionalOwnerRule = optionalDependencyFeatureOwnerRules.find( + (rule) => rule.crateName === 'services-integrations', + ); + for (const dep of ['bitfun-runtime-ports', 'git2', 'notify', 'rmcp']) { + if (!servicesOptionalOwnerRule?.dependencies.some((dependency) => dependency.depName === dep)) { + throw new Error(`services-integrations optional dependency owner rule must cover ${dep}`); + } + } + const productDomainsOptionalOwnerRule = optionalDependencyFeatureOwnerRules.find( + (rule) => rule.crateName === 'product-domains', + ); + for (const dep of ['dirs', 'log', 'sha2']) { + if (!productDomainsOptionalOwnerRule?.dependencies.some((dependency) => dependency.depName === dep)) { + throw new Error(`product-domains optional dependency owner rule must cover ${dep}`); + } + } const productDomainRuntimeRule = forbiddenContentUnderRules.find( (rule) => rule.path === 'src/crates/product-domains/src', ); @@ -5883,6 +6045,7 @@ function checkOptionalDependencyFeatureOwners(crateDir, rule) { const deps = parseManifestDependencies(lines); const depsByName = new Map(deps.map((dep) => [dep.name, dep])); const features = parseManifestFeatures(lines); + const declaredOwnerDeps = new Set(rule.dependencies.map((dependency) => dependency.depName)); for (const dependency of rule.dependencies) { const dep = depsByName.get(dependency.depName); @@ -5903,15 +6066,42 @@ function checkOptionalDependencyFeatureOwners(crateDir, rule) { } for (const featureName of dependency.ownerFeatures) { const feature = features.get(featureName); + if (!feature) { + failures.push({ + path: manifestPath, + line: dep?.line ?? 1, + message: `${rule.reason}; missing owner feature ${featureName} for ${dependency.depName}`, + }); + continue; + } if (!featureReferencesDependency(feature, dependency.depName)) { failures.push({ path: manifestPath, - line: feature?.line ?? dep.line, + line: feature.line, message: `${rule.reason}; ${featureName} must explicitly enable ${dependency.depName}`, }); } } } + + const profileRule = dependencyProfileRules.find((profile) => profile.crateName === rule.crateName); + const depsRequiringOwner = new Set(profileRule?.forbiddenNonOptionalDeps ?? []); + const uncoveredDeps = new Map(); + for (const dep of deps) { + if (!dep.optional || !depsRequiringOwner.has(dep.name) || declaredOwnerDeps.has(dep.name)) { + continue; + } + if (!uncoveredDeps.has(dep.name)) { + uncoveredDeps.set(dep.name, dep); + } + } + for (const [depName, dep] of uncoveredDeps.entries()) { + failures.push({ + path: manifestPath, + line: dep.line, + message: `${rule.reason}; optional runtime dependency must declare owner feature coverage: ${depName}`, + }); + } } function checkProductCoreFeatureAssembly(rule) { @@ -5974,9 +6164,33 @@ function checkCoreDefaultProductFullFeature() { } } +function checkCoreProductFullFeatureAssembly(rule) { + const manifestPath = join(ROOT, ...rule.manifestPath.split('/')); + const features = parseManifestFeatures(readText(manifestPath).split(/\r?\n/)); + const productFull = features.get(rule.featureName); + if (!productFull) { + failures.push({ + path: manifestPath, + line: 1, + message: `${rule.reason}; missing ${rule.featureName} feature declaration`, + }); + return; + } + for (const featureName of rule.requiredFeatureRefs) { + if (!featureReferencesFeature(productFull, featureName)) { + failures.push({ + path: manifestPath, + line: productFull.line, + message: `${rule.reason}; ${rule.featureName} must explicitly enable ${featureName}`, + }); + } + } +} + function checkOwnerCrateFeatureAssembly(rule) { const manifestPath = join(ROOT, ...rule.manifestPath.split('/')); const features = parseManifestFeatures(readText(manifestPath).split(/\r?\n/)); + const allowedProductFullFeatures = new Set(rule.requiredProductFullFeatures); const defaultFeature = features.get('default'); if (!defaultFeature) { failures.push({ @@ -6011,6 +6225,15 @@ function checkOwnerCrateFeatureAssembly(rule) { }); } } + for (const featureName of productFull.refs) { + if (!allowedProductFullFeatures.has(featureName)) { + failures.push({ + path: manifestPath, + line: productFull.line, + message: `${rule.reason}; product-full must not include undeclared feature group ${featureName}`, + }); + } + } } function checkRustImports(crateDir) { @@ -6234,6 +6457,7 @@ for (const rule of productCoreFeatureAssemblyRules) { } checkProductCoreFeatureAssemblyCoverage(); checkCoreDefaultProductFullFeature(); +checkCoreProductFullFeatureAssembly(coreProductFullFeatureAssemblyRule); for (const rule of ownerCrateFeatureAssemblyRules) { checkOwnerCrateFeatureAssembly(rule); } diff --git a/src/crates/agent-tools/AGENTS.md b/src/crates/agent-tools/AGENTS.md index a29106b0d..1db680426 100644 --- a/src/crates/agent-tools/AGENTS.md +++ b/src/crates/agent-tools/AGENTS.md @@ -11,8 +11,9 @@ the product tool runtime. crates, Tauri, Git, MCP, network clients, or CLI UI dependencies. - This crate may own `ToolResult`, validation DTOs, runtime restriction DTOs, path-resolution DTOs, host path normalization, runtime artifact URI, - remote POSIX path pure contracts, allowed-list / collapsed-tool execution gate policy, - generic/static/dynamic provider contracts, pure + remote POSIX path pure contracts, provider-neutral path resolution / + absolute-path checks, runtime artifact reference assembly, allowed-list / + collapsed-tool execution gate policy, generic/static/dynamic provider contracts, pure manifest/exposure helpers, generic contextual prompt-manifest resolver contracts, generic catalog snapshot provider contracts, generic GetToolSpec catalog provider/detail/summary helpers, provider-backed GetToolSpec runtime diff --git a/src/crates/agent-tools/src/framework.rs b/src/crates/agent-tools/src/framework.rs index 721ba8df5..8bad2f95d 100644 --- a/src/crates/agent-tools/src/framework.rs +++ b/src/crates/agent-tools/src/framework.rs @@ -1442,6 +1442,8 @@ pub enum ToolPathContractError { MissingRuntimeUriWorkspaceScope, MissingRuntimeUriArtifactPath, EmptyRuntimeWorkspaceScope, + RuntimeUriScopeMismatch { workspace_scope: String }, + MissingRuntimeRoot, EmptyPath, MissingWorkspaceRoot { path: String }, } @@ -1467,6 +1469,18 @@ impl fmt::Display for ToolPathContractError { Self::EmptyRuntimeWorkspaceScope => { write!(formatter, "Runtime URI workspace scope cannot be empty") } + Self::RuntimeUriScopeMismatch { workspace_scope } => { + write!( + formatter, + "Runtime URI scope '{workspace_scope}' does not match the current workspace" + ) + } + Self::MissingRuntimeRoot => { + write!( + formatter, + "A workspace is required to resolve runtime artifacts" + ) + } Self::EmptyPath => write!(formatter, "path cannot be empty"), Self::MissingWorkspaceRoot { path } => { write!( @@ -1539,6 +1553,71 @@ pub fn resolve_workspace_tool_path( } } +pub fn resolve_tool_path_with_context( + path: &str, + workspace_root: Option<&str>, + workspace_is_remote: bool, + workspace_scope: Option<&str>, + runtime_root: Option, +) -> Result { + if is_bitfun_runtime_uri(path) { + let parsed = parse_bitfun_runtime_uri(path)?; + let scope_matches = parsed.workspace_scope == "current" + || workspace_scope == Some(parsed.workspace_scope.as_str()); + if !scope_matches { + return Err(ToolPathContractError::RuntimeUriScopeMismatch { + workspace_scope: parsed.workspace_scope, + }); + } + + let runtime_root = runtime_root.ok_or(ToolPathContractError::MissingRuntimeRoot)?; + let mut resolved_path = runtime_root.clone(); + for segment in parsed.relative_path.split('/') { + resolved_path.push(segment); + } + + let effective_scope = workspace_scope + .map(str::to_string) + .unwrap_or_else(|| parsed.workspace_scope.clone()); + let logical_path = build_bitfun_runtime_uri(&effective_scope, &parsed.relative_path)?; + + return Ok(ToolPathResolution { + requested_path: path.to_string(), + logical_path, + resolved_path: resolved_path.to_string_lossy().to_string(), + backend: ToolPathBackend::Local, + runtime_scope: Some(effective_scope), + runtime_root: Some(runtime_root), + }); + } + + let resolved_path = resolve_workspace_tool_path(path, workspace_root, workspace_is_remote)?; + Ok(ToolPathResolution { + requested_path: path.to_string(), + logical_path: resolved_path.clone(), + resolved_path, + backend: if workspace_is_remote { + ToolPathBackend::RemoteWorkspace + } else { + ToolPathBackend::Local + }, + runtime_scope: None, + runtime_root: None, + }) +} + +pub fn tool_path_is_effectively_absolute(path: &str, workspace_is_remote: bool) -> bool { + if is_bitfun_runtime_uri(path) { + return true; + } + + if workspace_is_remote { + posix_style_path_is_absolute(path) + } else { + Path::new(path).is_absolute() + } +} + pub fn normalize_runtime_relative_path(path: &str) -> Result { let normalized = path.trim().replace('\\', "/"); let trimmed = normalized.trim_matches('/'); @@ -1606,6 +1685,45 @@ pub fn build_bitfun_runtime_uri( )) } +pub fn build_tool_runtime_artifact_reference( + relative_path: &str, + runtime_root: Option<&Path>, + workspace_scope: Option<&str>, + emit_runtime_uri: bool, +) -> Result { + let normalized_relative_path = normalize_runtime_relative_path(relative_path)?; + if emit_runtime_uri { + return build_bitfun_runtime_uri( + workspace_scope.unwrap_or("current"), + &normalized_relative_path, + ); + } + + let runtime_root = runtime_root.ok_or(ToolPathContractError::MissingRuntimeRoot)?; + let mut resolved_path = runtime_root.to_path_buf(); + for segment in normalized_relative_path.split('/') { + resolved_path.push(segment); + } + + Ok(resolved_path.to_string_lossy().to_string()) +} + +pub fn build_tool_session_runtime_artifact_reference( + session_id: &str, + relative_path: &str, + runtime_root: Option<&Path>, + workspace_scope: Option<&str>, + emit_runtime_uri: bool, +) -> Result { + let normalized_relative_path = normalize_runtime_relative_path(relative_path)?; + build_tool_runtime_artifact_reference( + &format!("sessions/{}/{}", session_id, normalized_relative_path), + runtime_root, + workspace_scope, + emit_runtime_uri, + ) +} + pub fn posix_style_path_is_absolute(path: &str) -> bool { let path = path.trim().replace('\\', "/"); path.starts_with('/') @@ -1726,6 +1844,37 @@ impl ToolPathPolicy { } } +pub fn is_tool_path_allowed_by_resolved_roots( + resolution: &ToolPathResolution, + resolved_roots: &[ToolPathResolution], + mut root_contains_path: impl FnMut(&ToolPathResolution, &ToolPathResolution) -> Result, +) -> Result { + for root in resolved_roots { + if root.backend != resolution.backend { + continue; + } + + if root_contains_path(resolution, root)? { + return Ok(true); + } + } + + Ok(false) +} + +pub fn build_tool_path_policy_denial_message( + logical_path: &str, + operation: ToolPathOperation, + allowed_roots: &[String], +) -> String { + format!( + "Path '{}' is not allowed for {}. Allowed roots: {}", + logical_path, + operation.verb(), + allowed_roots.join(", ") + ) +} + #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub struct ToolRuntimeRestrictions { #[serde(default)] diff --git a/src/crates/agent-tools/src/lib.rs b/src/crates/agent-tools/src/lib.rs index b12d4cb0d..7e1ffeabb 100644 --- a/src/crates/agent-tools/src/lib.rs +++ b/src/crates/agent-tools/src/lib.rs @@ -29,10 +29,12 @@ pub use framework::{ build_get_tool_spec_collapsed_tool_entry, build_get_tool_spec_description, build_get_tool_spec_detail_result, build_get_tool_spec_duplicate_load_hint, build_get_tool_spec_duplicate_load_result, build_prompt_visible_tool_manifest_definitions, - build_tool_manifest_policy_tools, collect_loaded_collapsed_tool_names, - get_tool_spec_input_schema, get_tool_spec_is_concurrency_safe, get_tool_spec_is_readonly, - get_tool_spec_needs_permissions, get_tool_spec_short_description, is_bitfun_runtime_uri, - is_remote_posix_path_within_root, normalize_absolute_posix_path, normalize_host_path, + build_tool_manifest_policy_tools, build_tool_path_policy_denial_message, + build_tool_runtime_artifact_reference, build_tool_session_runtime_artifact_reference, + collect_loaded_collapsed_tool_names, get_tool_spec_input_schema, + get_tool_spec_is_concurrency_safe, get_tool_spec_is_readonly, get_tool_spec_needs_permissions, + get_tool_spec_short_description, is_bitfun_runtime_uri, is_remote_posix_path_within_root, + is_tool_path_allowed_by_resolved_roots, normalize_absolute_posix_path, normalize_host_path, normalize_runtime_relative_path, parse_bitfun_runtime_uri, posix_resolve_path_with_workspace, posix_style_path_is_absolute, render_get_tool_spec_tool_use_message, resolve_contextual_tool_manifest, resolve_contextual_tool_manifest_from_provider, @@ -40,8 +42,9 @@ pub use framework::{ resolve_get_tool_spec_detail, resolve_get_tool_spec_detail_from_provider, resolve_get_tool_spec_execution_plan, resolve_get_tool_spec_execution_result_from_provider, resolve_host_path, resolve_host_path_with_workspace, resolve_readonly_enabled_tools, - resolve_tool_manifest_policy, resolve_workspace_tool_path, sort_tool_manifest_definitions, - summarize_get_tool_spec_collapsed_tools, tool_manifest_sort_rank, - validate_collapsed_tool_usage, validate_get_tool_spec_input, validate_tool_allowed_by_list, + resolve_tool_manifest_policy, resolve_tool_path_with_context, resolve_workspace_tool_path, + sort_tool_manifest_definitions, summarize_get_tool_spec_collapsed_tools, + tool_manifest_sort_rank, tool_path_is_effectively_absolute, validate_collapsed_tool_usage, + validate_get_tool_spec_input, validate_tool_allowed_by_list, }; pub use input_validator::InputValidator; diff --git a/src/crates/agent-tools/tests/tool_contracts.rs b/src/crates/agent-tools/tests/tool_contracts.rs index 20a95bb56..95c566bc6 100644 --- a/src/crates/agent-tools/tests/tool_contracts.rs +++ b/src/crates/agent-tools/tests/tool_contracts.rs @@ -9,24 +9,26 @@ use bitfun_agent_tools::{ GetToolSpecExecutionError, GetToolSpecExecutionPlan, GetToolSpecLoadObservation, GetToolSpecRuntime, InputValidator, PromptVisibleToolManifestItem, ToolContextFacts, ToolExposure, ToolImageAttachment, ToolManifestDefinition, ToolManifestPolicyTool, - ToolPathBackend, ToolPathResolution, ToolRenderOptions, ToolResult, ToolRuntimeRestrictions, - ToolWorkspaceKind, ValidationResult, build_bitfun_runtime_uri, + ToolPathBackend, ToolPathOperation, ToolPathResolution, ToolRenderOptions, ToolResult, + ToolRuntimeRestrictions, ToolWorkspaceKind, ValidationResult, build_bitfun_runtime_uri, build_collapsed_tool_stub_definition, build_get_tool_spec_assistant_detail, build_get_tool_spec_catalog_description, build_get_tool_spec_catalog_description_from_provider, build_get_tool_spec_collapsed_tool_entry, build_get_tool_spec_description, build_get_tool_spec_detail_result, build_get_tool_spec_duplicate_load_hint, build_get_tool_spec_duplicate_load_result, build_prompt_visible_tool_manifest_definitions, - collect_loaded_collapsed_tool_names, get_tool_spec_input_schema, - get_tool_spec_is_concurrency_safe, get_tool_spec_is_readonly, get_tool_spec_needs_permissions, - get_tool_spec_short_description, is_bitfun_runtime_uri, is_remote_posix_path_within_root, - normalize_host_path, normalize_runtime_relative_path, parse_bitfun_runtime_uri, - posix_resolve_path_with_workspace, posix_style_path_is_absolute, - render_get_tool_spec_tool_use_message, resolve_contextual_tool_manifest, - resolve_contextual_tool_manifest_from_provider, resolve_get_tool_spec_detail, - resolve_get_tool_spec_detail_from_provider, + build_tool_path_policy_denial_message, build_tool_runtime_artifact_reference, + build_tool_session_runtime_artifact_reference, collect_loaded_collapsed_tool_names, + get_tool_spec_input_schema, get_tool_spec_is_concurrency_safe, get_tool_spec_is_readonly, + get_tool_spec_needs_permissions, get_tool_spec_short_description, is_bitfun_runtime_uri, + is_remote_posix_path_within_root, is_tool_path_allowed_by_resolved_roots, normalize_host_path, + normalize_runtime_relative_path, parse_bitfun_runtime_uri, posix_resolve_path_with_workspace, + posix_style_path_is_absolute, render_get_tool_spec_tool_use_message, + resolve_contextual_tool_manifest, resolve_contextual_tool_manifest_from_provider, + resolve_get_tool_spec_detail, resolve_get_tool_spec_detail_from_provider, resolve_get_tool_spec_execution_result_from_provider, resolve_host_path_with_workspace, - resolve_readonly_enabled_tools, resolve_tool_manifest_policy, resolve_workspace_tool_path, - sort_tool_manifest_definitions, summarize_get_tool_spec_collapsed_tools, + resolve_readonly_enabled_tools, resolve_tool_manifest_policy, resolve_tool_path_with_context, + resolve_workspace_tool_path, sort_tool_manifest_definitions, + summarize_get_tool_spec_collapsed_tools, tool_path_is_effectively_absolute, validate_collapsed_tool_usage, validate_get_tool_spec_input, validate_tool_allowed_by_list, }; use serde_json::json; @@ -286,6 +288,179 @@ fn path_resolution_contract_keeps_backend_and_runtime_helpers() { ); } +#[test] +fn tool_path_policy_owner_matches_resolved_roots_by_backend() { + let target = ToolPathResolution { + requested_path: "src/lib.rs".to_string(), + logical_path: "/workspace/src/lib.rs".to_string(), + resolved_path: "/workspace/src/lib.rs".to_string(), + backend: ToolPathBackend::RemoteWorkspace, + runtime_scope: None, + runtime_root: None, + }; + let local_root = ToolPathResolution { + requested_path: "src".to_string(), + logical_path: "/workspace/src".to_string(), + resolved_path: "/workspace/src".to_string(), + backend: ToolPathBackend::Local, + runtime_scope: None, + runtime_root: None, + }; + let remote_root = ToolPathResolution { + requested_path: "src".to_string(), + logical_path: "/workspace/src".to_string(), + resolved_path: "/workspace/src".to_string(), + backend: ToolPathBackend::RemoteWorkspace, + runtime_scope: None, + runtime_root: None, + }; + + let allowed = is_tool_path_allowed_by_resolved_roots( + &target, + &[local_root, remote_root], + |resolution, root| -> Result { + Ok(is_remote_posix_path_within_root( + &resolution.resolved_path, + &root.resolved_path, + )) + }, + ) + .expect("containment callback should succeed"); + + assert!(allowed); +} + +#[test] +fn tool_path_policy_owner_ignores_mismatched_backend_roots() { + let target = ToolPathResolution { + requested_path: "src/lib.rs".to_string(), + logical_path: "/workspace/src/lib.rs".to_string(), + resolved_path: "/workspace/src/lib.rs".to_string(), + backend: ToolPathBackend::RemoteWorkspace, + runtime_scope: None, + runtime_root: None, + }; + let local_root = ToolPathResolution { + requested_path: "src".to_string(), + logical_path: "/workspace/src".to_string(), + resolved_path: "/workspace/src".to_string(), + backend: ToolPathBackend::Local, + runtime_scope: None, + runtime_root: None, + }; + + let allowed = is_tool_path_allowed_by_resolved_roots( + &target, + &[local_root], + |_, _| -> Result { + panic!("mismatched backend roots must not call the containment callback"); + }, + ) + .expect("backend mismatch should not invoke containment"); + + assert!(!allowed); +} + +#[test] +fn tool_path_policy_owner_preserves_denial_message() { + let message = build_tool_path_policy_denial_message( + "/workspace/blocked/file.txt", + ToolPathOperation::Write, + &["/workspace/allowed".to_string()], + ); + + assert_eq!( + message, + "Path '/workspace/blocked/file.txt' is not allowed for write. Allowed roots: /workspace/allowed" + ); +} + +#[test] +fn tool_path_resolution_owner_preserves_runtime_uri_scope_and_backend() { + let runtime_root = PathBuf::from("/runtime/workspace"); + + let resolution = resolve_tool_path_with_context( + "bitfun://runtime/workspace-123/plans/demo.plan.md", + Some("/home/project"), + true, + Some("workspace-123"), + Some(runtime_root.clone()), + ) + .expect("runtime URI should resolve through the provider-neutral owner"); + + assert_eq!( + resolution.requested_path, + "bitfun://runtime/workspace-123/plans/demo.plan.md" + ); + assert_eq!( + resolution.logical_path, + "bitfun://runtime/workspace-123/plans/demo.plan.md" + ); + assert_eq!( + PathBuf::from(&resolution.resolved_path), + runtime_root.join("plans").join("demo.plan.md") + ); + assert_eq!(resolution.backend, ToolPathBackend::Local); + assert_eq!(resolution.runtime_scope.as_deref(), Some("workspace-123")); + assert_eq!( + resolution.runtime_root.as_deref(), + Some(runtime_root.as_path()) + ); +} + +#[test] +fn tool_path_resolution_owner_rejects_mismatched_runtime_scope() { + let err = resolve_tool_path_with_context( + "bitfun://runtime/workspace-456/plans/demo.plan.md", + Some("/home/project"), + true, + Some("workspace-123"), + Some(PathBuf::from("/runtime/workspace")), + ) + .expect_err("runtime artifact scopes must match the active workspace"); + + assert_eq!( + err.to_string(), + "Runtime URI scope 'workspace-456' does not match the current workspace" + ); +} + +#[test] +fn tool_path_resolution_owner_selects_workspace_backend_semantics() { + let local = + resolve_tool_path_with_context("src/lib.rs", Some("/repo/project"), false, None, None) + .expect("local path should resolve through host semantics"); + assert_eq!(local.backend, ToolPathBackend::Local); + assert_eq!( + PathBuf::from(local.resolved_path), + PathBuf::from("/repo/project").join("src").join("lib.rs") + ); + + let remote = + resolve_tool_path_with_context(r"src\lib.rs", Some("/home/project"), true, None, None) + .expect("remote path should resolve through POSIX workspace semantics"); + assert_eq!(remote.backend, ToolPathBackend::RemoteWorkspace); + assert_eq!(remote.resolved_path, "/home/project/src/lib.rs"); + assert_eq!(remote.logical_path, "/home/project/src/lib.rs"); +} + +#[test] +fn tool_path_absolute_contract_keeps_remote_posix_and_runtime_uri_semantics() { + assert!(tool_path_is_effectively_absolute( + "bitfun://runtime/current/logs/tool.txt", + false + )); + assert!(tool_path_is_effectively_absolute( + r"\home\workspace\src\lib.rs", + true + )); + assert!(!tool_path_is_effectively_absolute("src/lib.rs", true)); + assert_eq!( + tool_path_is_effectively_absolute("src/lib.rs", false), + PathBuf::from("src/lib.rs").is_absolute() + ); +} + #[test] fn runtime_uri_contract_is_provider_neutral_and_normalized() { let uri = build_bitfun_runtime_uri("workspace-123", r"plans\demo.plan.md") @@ -328,6 +503,75 @@ fn runtime_uri_contract_rejects_escape_and_invalid_scope() { ); } +#[test] +fn runtime_artifact_reference_owner_preserves_remote_uri_shape() { + let reference = build_tool_runtime_artifact_reference( + r"plans\demo.plan.md", + None, + Some("workspace-123"), + true, + ) + .expect("remote artifact reference should build as runtime URI"); + + assert_eq!( + reference, + "bitfun://runtime/workspace-123/plans/demo.plan.md" + ); +} + +#[test] +fn runtime_artifact_reference_owner_preserves_local_path_shape() { + let runtime_root = PathBuf::from("/runtime/workspace"); + + let reference = build_tool_runtime_artifact_reference( + r"sessions\session-1\tool-results\result.json", + Some(runtime_root.as_path()), + None, + false, + ) + .expect("local artifact reference should build as host path"); + + assert_eq!( + PathBuf::from(reference), + runtime_root + .join("sessions") + .join("session-1") + .join("tool-results") + .join("result.json") + ); +} + +#[test] +fn runtime_artifact_reference_owner_preserves_session_prefix_and_rejects_escape() { + let session_reference = build_tool_session_runtime_artifact_reference( + "session-1", + "tool-results/result.json", + None, + Some("workspace-123"), + true, + ) + .expect("session artifact reference should build"); + + assert_eq!( + session_reference, + "bitfun://runtime/workspace-123/sessions/session-1/tool-results/result.json" + ); + + let runtime_root = PathBuf::from("/runtime/workspace"); + let escape = build_tool_runtime_artifact_reference( + "../secret.txt", + Some(runtime_root.as_path()), + None, + false, + ) + .expect_err("artifact references must not escape the runtime root"); + + assert_eq!( + escape.to_string(), + "Runtime artifact path cannot escape its root" + ); +} + #[test] fn collapsed_tool_usage_gate_preserves_get_tool_spec_unlock_contract() { let collapsed_tools = vec!["WebFetch".to_string()]; diff --git a/src/crates/core/AGENTS.md b/src/crates/core/AGENTS.md index 9b97984c6..984e9daf1 100644 --- a/src/crates/core/AGENTS.md +++ b/src/crates/core/AGENTS.md @@ -97,6 +97,12 @@ SessionManager → Session → DialogTurn → ModelRound MiniApp/function-agent, Git/MCP, remote-connect, review-platform, snapshot, token usage, and mode canonicalization stay behind `product-full` or their owner feature group. +- Provider-neutral tool path resolution, effective absolute-path checks, + runtime artifact reference assembly, path policy root matching, and denial + text may live in `bitfun-agent-tools`; keep workspace/runtime root lookup, + allowed-root resolution, local canonicalization, remote POSIX containment + callbacks, `BitFunError` mapping, and `ToolUseContext` runtime/service + bindings in core unless a separate migration proves equivalence. - Product/runtime dependencies that are only used behind those feature gates should stay optional in `bitfun-core` and be enabled by `product-full`, `service-integrations`, or `ssh-remote`; do not treat that as permission to @@ -110,9 +116,16 @@ SessionManager → Session → DialogTurn → ModelRound dependencies and requires matching assembly rules. - Keep `default = ["product-full"]` until a separate product matrix review explicitly changes default capability selection. +- Keep `bitfun-core/product-full` explicitly wired to the current owner feature + groups: `ssh-remote`, `product-domains`, `service-integrations`, and + `tool-packs`. - Owner crate feature graph guards keep `tool-packs`, `services-integrations`, and `product-domains` default-light while allowing `product-full` to - explicitly aggregate current owner feature groups. + explicitly aggregate current owner feature groups. When adding an owner + feature group, update `scripts/check-core-boundaries.mjs`; `product-full` + must not include undeclared feature groups or dependency shortcuts. Optional + runtime/domain dependencies in owner crates must stay owned by explicit + feature groups. - `service-integrations` is not a standalone product shape in core yet; MCP, remote-connect, and review-platform still depend on agentic/product runtime owners through `product-full`. diff --git a/src/crates/core/src/agentic/tools/framework.rs b/src/crates/core/src/agentic/tools/framework.rs index d4611f1cd..3b0865846 100644 --- a/src/crates/core/src/agentic/tools/framework.rs +++ b/src/crates/core/src/agentic/tools/framework.rs @@ -7,7 +7,10 @@ use async_trait::async_trait; pub use bitfun_agent_tools::{ DynamicMcpToolInfo, DynamicToolInfo, PortableToolContextProvider, ToolContextFacts, ToolExposure, ToolPathBackend, ToolPathResolution, ToolRenderOptions, ToolResult, - ToolWorkspaceKind, ValidationResult, + ToolWorkspaceKind, ValidationResult, build_tool_path_policy_denial_message, + build_tool_runtime_artifact_reference, build_tool_session_runtime_artifact_reference, + is_tool_path_allowed_by_resolved_roots, resolve_tool_path_with_context, + tool_path_is_effectively_absolute, }; use serde_json::Value; use std::collections::HashMap; diff --git a/src/crates/core/src/agentic/tools/tool_context_runtime.rs b/src/crates/core/src/agentic/tools/tool_context_runtime.rs index 89512ef77..ab345fd03 100644 --- a/src/crates/core/src/agentic/tools/tool_context_runtime.rs +++ b/src/crates/core/src/agentic/tools/tool_context_runtime.rs @@ -13,6 +13,9 @@ use crate::agentic::tools::ToolRuntimeRestrictions; use crate::agentic::tools::computer_use_host::ComputerUseHostRef; use crate::agentic::tools::framework::{ ToolPathBackend, ToolPathResolution, ToolResult, ToolUseContext, + build_tool_path_policy_denial_message, build_tool_runtime_artifact_reference, + build_tool_session_runtime_artifact_reference, is_tool_path_allowed_by_resolved_roots, + resolve_tool_path_with_context, tool_path_is_effectively_absolute, }; use crate::agentic::tools::pipeline::{ToolExecutionContext, ToolTask}; use crate::agentic::tools::post_call_hooks; @@ -21,7 +24,6 @@ use crate::agentic::tools::restrictions::{ }; use crate::agentic::tools::workspace_paths::{ build_bitfun_runtime_uri, is_bitfun_runtime_uri, normalize_runtime_relative_path, - parse_bitfun_runtime_uri, }; use crate::agentic::workspace::WorkspaceServices; use crate::infrastructure::get_path_manager_arc; @@ -331,38 +333,34 @@ impl ToolUseContext { resolved_roots.push(self.resolve_tool_path(root)?); } - let mut is_allowed = false; - for root in &resolved_roots { - if root.backend != resolution.backend { - continue; - } - - let matches_root = match resolution.backend { - ToolPathBackend::Local => is_local_path_within_root( - Path::new(&resolution.resolved_path), - Path::new(&root.resolved_path), - )?, - ToolPathBackend::RemoteWorkspace => { - is_remote_posix_path_within_root(&resolution.resolved_path, &root.resolved_path) + let is_allowed = is_tool_path_allowed_by_resolved_roots( + resolution, + &resolved_roots, + |resolution, root| -> BitFunResult { + match resolution.backend { + ToolPathBackend::Local => is_local_path_within_root( + Path::new(&resolution.resolved_path), + Path::new(&root.resolved_path), + ), + ToolPathBackend::RemoteWorkspace => Ok(is_remote_posix_path_within_root( + &resolution.resolved_path, + &root.resolved_path, + )), } - }; - - if matches_root { - is_allowed = true; - break; - } - } + }, + )?; if is_allowed { return Ok(()); } - Err(BitFunError::validation(format!( - "Path '{}' is not allowed for {}. Allowed roots: {}", - resolution.logical_path, - operation.verb(), - allowed_roots.join(", ") - ))) + Err(BitFunError::validation( + build_tool_path_policy_denial_message( + &resolution.logical_path, + operation, + allowed_roots, + ), + )) } /// Resolve a user or model-supplied path for file/shell tools. Uses POSIX semantics when the @@ -445,17 +443,18 @@ impl ToolUseContext { } pub fn build_runtime_artifact_reference(&self, relative_path: &str) -> BitFunResult { - let normalized_relative_path = normalize_runtime_relative_path(relative_path)?; - if self.should_emit_runtime_uri() { - return self.build_runtime_uri(&normalized_relative_path); - } - - let mut resolved_path = self.current_workspace_runtime_root()?; - for segment in normalized_relative_path.split('/') { - resolved_path.push(segment); - } - - Ok(resolved_path.to_string_lossy().to_string()) + let runtime_root = if self.should_emit_runtime_uri() { + None + } else { + Some(self.current_workspace_runtime_root()?) + }; + build_tool_runtime_artifact_reference( + relative_path, + runtime_root.as_deref(), + self.current_workspace_scope().as_deref(), + self.should_emit_runtime_uri(), + ) + .map_err(|error| BitFunError::tool(error.to_string())) } pub fn build_session_runtime_artifact_reference( @@ -463,11 +462,19 @@ impl ToolUseContext { session_id: &str, relative_path: &str, ) -> BitFunResult { - let normalized_relative_path = normalize_runtime_relative_path(relative_path)?; - self.build_runtime_artifact_reference(&format!( - "sessions/{}/{}", - session_id, normalized_relative_path - )) + let runtime_root = if self.should_emit_runtime_uri() { + None + } else { + Some(self.current_workspace_runtime_root()?) + }; + build_tool_session_runtime_artifact_reference( + session_id, + relative_path, + runtime_root.as_deref(), + self.current_workspace_scope().as_deref(), + self.should_emit_runtime_uri(), + ) + .map_err(|error| BitFunError::tool(error.to_string())) } pub fn current_workspace_session_dir(&self, session_id: &str) -> BitFunResult { @@ -498,61 +505,46 @@ impl ToolUseContext { pub fn resolve_tool_path(&self, path: &str) -> BitFunResult { if is_bitfun_runtime_uri(path) { - let parsed = parse_bitfun_runtime_uri(path)?; let workspace_scope = self.current_workspace_scope(); - let scope_matches = parsed.workspace_scope == "current" - || workspace_scope.as_deref() == Some(parsed.workspace_scope.as_str()); - if !scope_matches { - return Err(BitFunError::tool(format!( - "Runtime URI scope '{}' does not match the current workspace", - parsed.workspace_scope - ))); - } - - let runtime_root = self.current_workspace_runtime_root()?; - let mut resolved_path = runtime_root.clone(); - for segment in parsed.relative_path.split('/') { - resolved_path.push(segment); - } - - let effective_scope = workspace_scope.unwrap_or_else(|| parsed.workspace_scope.clone()); - let logical_path = build_bitfun_runtime_uri(&effective_scope, &parsed.relative_path)?; - - return Ok(ToolPathResolution { - requested_path: path.to_string(), - logical_path, - resolved_path: resolved_path.to_string_lossy().to_string(), - backend: ToolPathBackend::Local, - runtime_scope: Some(effective_scope), - runtime_root: Some(runtime_root), - }); + let runtime_root = if self.workspace.is_some() { + Some(self.current_workspace_runtime_root()?) + } else { + None + }; + return resolve_tool_path_with_context( + path, + None, + self.is_remote(), + workspace_scope.as_deref(), + runtime_root, + ) + .map_err(|error| BitFunError::tool(error.to_string())); } - let resolved_path = self.resolve_workspace_tool_path(path)?; - Ok(ToolPathResolution { - requested_path: path.to_string(), - logical_path: resolved_path.clone(), - resolved_path, - backend: if self.is_remote() { - ToolPathBackend::RemoteWorkspace - } else { - ToolPathBackend::Local - }, - runtime_scope: None, - runtime_root: None, - }) + let workspace_root_owned = self + .workspace + .as_ref() + .map(|w| w.root_path_string()) + .ok_or_else(|| { + BitFunError::tool(format!( + "A workspace path is required to resolve tool path: {}", + path + )) + })?; + + resolve_tool_path_with_context( + path, + Some(workspace_root_owned.as_str()), + self.is_remote(), + self.current_workspace_scope().as_deref(), + None, + ) + .map_err(|error| BitFunError::tool(error.to_string())) } /// Whether `path` is absolute for the active workspace (POSIX `/` for remote SSH). pub fn workspace_path_is_effectively_absolute(&self, path: &str) -> bool { - if is_bitfun_runtime_uri(path) { - return true; - } - if self.is_remote() { - crate::agentic::tools::workspace_paths::posix_style_path_is_absolute(path) - } else { - Path::new(path).is_absolute() - } + tool_path_is_effectively_absolute(path, self.is_remote()) } } @@ -721,6 +713,20 @@ mod path_resolution_tests { ); } + #[test] + fn runtime_uri_scope_error_takes_precedence_without_workspace() { + let context = context_without_workspace(); + + let err = context + .resolve_tool_path("bitfun://runtime/workspace-456/plans/demo.plan.md") + .expect_err("runtime URI scope should be validated before runtime root lookup"); + + assert!( + err.to_string() + .contains("does not match the current workspace") + ); + } + #[test] fn workspace_absolute_detection_uses_remote_posix_semantics() { let context = remote_context("/home/wsp/projects/test", None);