diff --git a/docs/architecture/cli-product-line-design.md b/docs/architecture/cli-product-line-design.md index 1191ee01db..64a7b9e9b2 100644 --- a/docs/architecture/cli-product-line-design.md +++ b/docs/architecture/cli-product-line-design.md @@ -171,6 +171,9 @@ CLI-local 配置只保存终端形态偏好与调用入口设置。共享权限 CLI 通过 `DeliveryProfile::Cli` 消费经过校验的产品 Runtime parts。产品定义、Delivery Profile、Runtime Configuration 和 Capability Availability 是不同概念: +- 编译期由 CLI 显式选择 `agent-runtime`、`canvas-runtime`、`external-sources`、 + `plugin-runtime` 与 `ssh-remote` owner feature;这保持现有 CLI capability plan, + 但不继承 Desktop 后续加入 `product-full` 的能力。 - 隐藏入口不证明后端依赖被移除。 - CLI 不读取 authoring product definition 作为运行时业务配置。 - 品牌、资源、数据 namespace、更新渠道和内置扩展由产品定制 owner 生成,CLI 只消费结果。 diff --git a/docs/architecture/platform-portability-design.md b/docs/architecture/platform-portability-design.md index dd1a06b283..44761b9964 100644 --- a/docs/architecture/platform-portability-design.md +++ b/docs/architecture/platform-portability-design.md @@ -93,7 +93,7 @@ Cargo package `bitfun-cli` 的 `aarch64-unknown-linux-ohos` 目标依赖解析 | 问题域 | 当前识别结果 | 主要风险 | 后续专题需要回答 | |---|---|---|---| -| 产品依赖闭包 | CLI 仍通过 `bitfun-core/product-full` 拉入 remote、browser、canvas、plugin、watch、Git、SQLite、PTY 等能力 | 无关平台依赖阻塞构建;为过编译而破坏共享 owner | CLI 真正需要哪些能力,哪些依赖应保留、隔离或移出目标闭包 | +| 产品依赖闭包 | CLI 已显式选择 `agent-runtime`、`canvas-runtime`、`external-sources`、`plugin-runtime` 与 `ssh-remote` Core owner feature,不再继承 `product-full`;当前闭包仍主动保留 remote、browser、canvas、plugin、watch、Git、SQLite、PTY 等现有能力 | 无关平台依赖阻塞构建;为过编译而破坏共享 owner | 在不改变现有 CLI 规格的前提下,哪些 owner 还应继续拆分或针对目标平台隔离 | | Rust 与依赖解析 | 仓库无根 `Cargo.lock`;Rust 1.94.1 探针先被要求 Rust 1.95 的 `oxc-browserslist`、`oxc_sourcemap` 阻塞 | 把通用 MSRV/解析问题误判为 OHOS 问题;构建不可复现 | 仓库认可的 Rust、依赖解析和构建基线 | | TUI/TTY | `ratatui/crossterm` 依赖 `mio`、rustix、signal-hook 和终端系统调用 | 能编译但 raw mode、输入、resize、信号或恢复不可用 | 真实系统终端支持范围与 TUI 退化边界 | | 剪贴板与语法高亮 | `arboard -> x11rb` 带入 X11;`syntect-tui` 重新带入 `onig_sys` | 桌面 Linux/C 原生依赖进入 OHOS 产物 | 这些能力是否必需,以及各自可维护的鸿蒙化路线 | diff --git a/docs/architecture/product-architecture.md b/docs/architecture/product-architecture.md index 75d9e13b72..82d3690fcb 100644 --- a/docs/architecture/product-architecture.md +++ b/docs/architecture/product-architecture.md @@ -724,12 +724,13 @@ flowchart LR ```mermaid flowchart TB Desktop["Desktop"] --> Full["product-full"] - CLI["CLI / TUI"] --> Full + CLI["CLI / TUI"] --> CliClosure["Core owner feature closure"] ACP["ACP"] --> Parts["Runtime Parts"] SDKHost["SDK Host"] --> Parts ServerBootstrap["Server agent bootstrap · dormant"] --> Full Full --> Coordinator["ConversationCoordinator"] + CliClosure --> Coordinator Parts --> Coordinator Ownership["CoreRuntimeOwnership"] -. "first-party composition injects once" .-> Coordinator ``` @@ -753,8 +754,8 @@ flowchart LR | 当前入口 | 已有能力 | 明确边界 | |---|---|---| | Desktop | 使用 `product-full`;显示外部来源、审批、冲突、诊断和 Host 能力 | 可执行能力在事实所在 Host 运行;Safe Mode 只阻止新调用,不改来源、不取消正在运行的调用 | -| CLI / TUI | 使用 `product-full`;提供 `/extensions`、统一 `/hooks`(旧 `/hooks_external` 为别名)、`/tools` 和 `/agents`;Claude Code/Codex 命令 Hook 可经显式审阅复制为原生层 | 生态解析仍在适配器,不启动第二套 Agent Runtime;OpenCode Hook 仍只静态发现;远程能力未接入时不回退本机 | -| ACP | 使用 `DeliveryProfile::Acp` 和 Runtime Parts | load 成功后才发布活动状态;close 排空后再卸载;完整历史和配置仍由 Core/ACP 管理 | +| CLI / TUI | 使用显式 Core owner feature closure(`agent-runtime`、`canvas-runtime`、`external-sources`、`plugin-runtime`、`ssh-remote`);提供 `/extensions`、统一 `/hooks`(旧 `/hooks_external` 为别名)、`/tools` 和 `/agents`;Claude Code/Codex 命令 Hook 可经显式审阅复制为原生层 | 保持现有 CLI capability plan,但不自动继承 Desktop 后续加入 `product-full` 的能力;生态解析仍在适配器,不启动第二套 Agent Runtime;OpenCode Hook 仍只静态发现;远程能力未接入时不回退本机 | +| ACP | 使用 `DeliveryProfile::Acp`、Runtime Parts,以及 `agent-runtime`/`canvas-runtime`/`external-sources`/`ssh-remote` Core owner feature | load 成功后才发布活动状态;close 排空后再卸载;完整历史、Canvas 工具物化、兼容指令来源和配置仍由 Core/ACP 管理 | | Peer / Server | Server 提供 control/catalog;Peer Host 执行真实工作区操作;当前 HTTP Server 不装配 Agent Runtime | 控制端不替远端发现或执行;旧 Host 明确降级,SSH Remote 未接入时返回不支持;只读 Server 不声明 Runtime ownership | | Web / Mobile Web | 依赖现有后端入口 | 不持有插件执行单元,也不能据空 profile 宣称独立能力 | | HarmonyOS 手机 Remote | phone-only ArkTS 远程入口 | 不等于 HarmonyOS PC 本地 Runtime、CLI/TUI 或 GUI | diff --git a/docs/plans/core-decomposition-plan.md b/docs/plans/core-decomposition-plan.md index fbc9f1b9bd..9a0f0af315 100644 --- a/docs/plans/core-decomposition-plan.md +++ b/docs/plans/core-decomposition-plan.md @@ -22,7 +22,7 @@ | 事实 | 当前状态 | 结论 | |---|---|---| | 产品能力组装 | `DeliveryProfile`、`ProductAssembler`、能力计划、服务可用性和测试已存在 | 这些是可测试的 assembly facts,不代表产品入口已接入 | -| CLI / Desktop / ACP | 三者仍按需启用 `bitfun-core/product-full`;CLI 与 ACP 已分别提交对应 `DeliveryProfile` 并消费 Runtime Parts/SDK,Desktop 主交互已消费由现有 owner 构造的窄口径 SDK 接口 | 三个入口均复用单一 Core owner;完整 Desktop profile 和剩余兼容操作仍需逐项迁移 | +| CLI / Desktop / ACP | CLI 与 ACP 已分别提交对应 `DeliveryProfile`、消费 Runtime Parts/SDK,并选择经过评审的 Core owner feature closure;Desktop 仍启用 `bitfun-core/product-full`,主交互已消费由现有 owner 构造的窄口径 SDK 接口 | 三个入口仍复用单一 Core runtime owner;完整 Desktop profile 和剩余兼容操作仍需逐项迁移 | | Server | 当前生产路由只形成 health/info/ping 基线 | 没有插件状态或独立产品组装完整流程 | | Server / Remote / Web / Mobile Web / SDK profile | 当前为空计划、未接入入口或仅有 preview 测试 | 不得据枚举值宣称产品能力已交付 | | Agent Runtime SDK | 已有无 `bitfun-core` 依赖的 v3 preview 接口和 smoke test | 发布边界仍需真实嵌入方证明 | diff --git a/docs/plans/product-architecture-evolution-plan.md b/docs/plans/product-architecture-evolution-plan.md index 06ec1f374d..ccacccfefd 100644 --- a/docs/plans/product-architecture-evolution-plan.md +++ b/docs/plans/product-architecture-evolution-plan.md @@ -32,7 +32,7 @@ | CLI/TUI | 宿主 `ACTION_SPECS` 已统一 Slash、Palette、Help、Keymap 与 dispatch;启动页及活动 turn 的 Linux PTY / Windows ConPTY 行为由进程级契约保护 | 保持现有 renderer 与交互规格,只按真实故障样例补可靠性契约;macOS 活动 PTY 另行验收 | | OpenCode | Prompt Command、受支持的单文件 JavaScript Tool 和 Subagent 安全子集已分别通过能力专属 provider 接入;受管 package plugin 仍只有静态预览 | 先收敛三条已交付路径的诊断、运行时提示和配置失败语义,再按真实阻塞样例评估下一能力切片 | | HarmonyOS PC | 未来平台目标,当前未实现 | 目标、问题、风险和旧设计闭环见平台规约;具体工作后续分别立项 | -| 入口迁移 | CLI 已消费 Runtime Parts;Desktop 主交互消费由现有 owner 构造的窄口径 Runtime SDK 门面,完整 Desktop Runtime Parts 尚未组装;CLI/ACP/Desktop 仍按需保留 `bitfun-core/product-full` 兼容 owner | 保持单一 owner,按真实端口逐项迁移,不批量删除兼容门面或用桩服务提前声明能力 | +| 入口迁移 | CLI 已消费 Runtime Parts;Desktop 主交互消费由现有 owner 构造的窄口径 Runtime SDK 门面,完整 Desktop Runtime Parts 尚未组装;CLI 与 ACP 选择经过评审的 Core owner feature closure,Desktop 仍保留 `bitfun-core/product-full` 兼容 owner | 保持单一运行时 owner,按真实端口逐项迁移,不批量删除兼容门面或用桩服务提前声明能力 | ## 3. 工作流一:边界与依赖可信 diff --git a/scripts/check-core-boundaries.test.mjs b/scripts/check-core-boundaries.test.mjs index 8d242c6a9d..f22dfae6bc 100644 --- a/scripts/check-core-boundaries.test.mjs +++ b/scripts/check-core-boundaries.test.mjs @@ -80,6 +80,7 @@ function packageAt(name, repoManifestPath, dependencies = []) { function pathDependency(repoCratePath, options = {}) { return { name: options.name ?? repoCratePath.split('/').at(-1), + rename: options.rename ?? null, path: join(TEST_ROOT, ...repoCratePath.split('/')), kind: options.kind ?? null, optional: options.optional ?? false, @@ -415,6 +416,782 @@ test('explicit product entrypoint bitfun-core feature selections pass', () => { ); }); +test('ACP Core capability closure must retain its Canvas owner', () => { + const core = packageAt('bitfun-core', 'src/crates/assembly/core/Cargo.toml'); + const acp = packageAt('bitfun-acp', 'src/crates/interfaces/acp/Cargo.toml', [ + pathDependency('src/crates/assembly/core', { + name: 'bitfun-core', + usesDefaultFeatures: false, + features: ['agent-runtime', 'external-sources', 'ssh-remote'], + }), + ]); + + const violations = findProductEntrypointCoreFeatureViolations( + [acp, core], + { root: TEST_ROOT, crateLayoutRules }, + ); + + assert.equal(violations.length, 1); + assert.match(violations[0].message, /must include canvas-runtime/); +}); + +test('ACP Core capability closure validation cannot be disabled by removing an owner', () => { + const core = packageAt('bitfun-core', 'src/crates/assembly/core/Cargo.toml'); + const acp = packageAt('bitfun-acp', 'src/crates/interfaces/acp/Cargo.toml', [ + pathDependency('src/crates/assembly/core', { + name: 'bitfun-core', + usesDefaultFeatures: false, + features: ['ssh-remote'], + }), + ]); + + const violations = findProductEntrypointCoreFeatureViolations( + [acp, core], + { root: TEST_ROOT, crateLayoutRules }, + ); + + assert.deepEqual( + violations.map((violation) => violation.message).sort(), + [ + 'bitfun-acp Core capability closure must include agent-runtime', + 'bitfun-acp Core capability closure must include canvas-runtime', + 'bitfun-acp Core capability closure must include external-sources', + ], + ); +}); + +test('CLI Core capability closure requires every reviewed owner', () => { + const core = packageAt('bitfun-core', 'src/crates/assembly/core/Cargo.toml'); + const cli = packageAt('bitfun-cli', 'src/apps/cli/Cargo.toml', [ + pathDependency('src/crates/assembly/core', { + name: 'bitfun-core', + usesDefaultFeatures: false, + features: [ + 'agent-runtime', + 'canvas-runtime', + 'external-sources', + 'ssh-remote', + ], + }), + ]); + + const violations = findProductEntrypointCoreFeatureViolations( + [cli, core], + { root: TEST_ROOT, crateLayoutRules }, + ); + + assert.equal(violations.length, 1); + assert.match(violations[0].message, /must include plugin-runtime/); +}); + +test('CLI entrypoint must not select the product-full Core feature', () => { + const core = packageAt('bitfun-core', 'src/crates/assembly/core/Cargo.toml'); + const cli = packageAt('bitfun-cli', 'src/apps/cli/Cargo.toml', [ + pathDependency('src/crates/assembly/core', { + name: 'bitfun-core', + usesDefaultFeatures: false, + features: ['product-full'], + }), + ]); + + const violations = findProductEntrypointCoreFeatureViolations( + [cli, core], + { root: TEST_ROOT, crateLayoutRules }, + ); + + assert.ok(violations.some((violation) => + /bitfun-cli -> bitfun-core\/product-full/.test(violation.message))); +}); + +test('CLI entrypoint must not reach product-full through a Core owner feature', () => { + const core = { + ...packageAt('bitfun-core', 'src/crates/assembly/core/Cargo.toml'), + features: { + 'agent-runtime': ['runtime-services', 'product-full'], + 'runtime-services': ['dep:bitfun-runtime-services'], + 'product-full': ['dep:bitfun-agent-runtime'], + }, + }; + const cli = packageAt('bitfun-cli', 'src/apps/cli/Cargo.toml', [ + pathDependency('src/crates/assembly/core', { + name: 'bitfun-core', + usesDefaultFeatures: false, + features: ['agent-runtime'], + }), + ]); + + const violations = findProductEntrypointCoreFeatureViolations( + [cli, core], + { root: TEST_ROOT, crateLayoutRules }, + ); + + assert.ok(violations.some((violation) => + /bitfun-cli -> bitfun-core\/product-full/.test(violation.message))); +}); + +test('CLI dependency closure must not re-enable product-full through an interface crate', () => { + const core = packageAt('bitfun-core', 'src/crates/assembly/core/Cargo.toml'); + const acp = packageAt('bitfun-acp', 'src/crates/interfaces/acp/Cargo.toml', [ + pathDependency('src/crates/assembly/core', { + name: 'bitfun-core', + usesDefaultFeatures: false, + features: ['product-full'], + }), + ]); + const cli = packageAt('bitfun-cli', 'src/apps/cli/Cargo.toml', [ + pathDependency('src/crates/interfaces/acp', { name: 'bitfun-acp' }), + ]); + + const violations = findProductEntrypointCoreFeatureViolations( + [cli, acp, core], + { root: TEST_ROOT, crateLayoutRules }, + ); + + assert.ok(violations.some((violation) => + /bitfun-cli -> bitfun-acp -> bitfun-core\/product-full/.test(violation.message))); +}); + +test('CLI dependency closure rejects indirect Core default features', () => { + const core = { + ...packageAt('bitfun-core', 'src/crates/assembly/core/Cargo.toml'), + features: { default: ['product-full'], 'product-full': [] }, + }; + const bridge = packageAt('bridge', 'src/crates/assembly/bridge/Cargo.toml', [ + pathDependency('src/crates/assembly/core', { name: 'bitfun-core' }), + ]); + const cli = packageAt('bitfun-cli', 'src/apps/cli/Cargo.toml', [ + pathDependency('src/crates/assembly/bridge', { name: 'bridge' }), + ]); + + const violations = findProductEntrypointCoreFeatureViolations( + [cli, bridge, core], + { root: TEST_ROOT, crateLayoutRules }, + ); + + assert.equal(violations.length, 1); + assert.match(violations[0].message, /bitfun-cli -> bridge -> bitfun-core\/product-full/); +}); + +test('CLI dependency closure resolves active intermediate feature forwarding', () => { + const core = { + ...packageAt('bitfun-core', 'src/crates/assembly/core/Cargo.toml'), + features: { default: ['product-full'], 'product-full': [] }, + }; + const bridge = { + ...packageAt('bridge', 'src/crates/assembly/bridge/Cargo.toml', [ + pathDependency('src/crates/assembly/core', { + name: 'bitfun-core', + optional: true, + usesDefaultFeatures: false, + }), + ]), + features: { + default: ['full'], + full: ['dep:bitfun-core', 'bitfun-core/product-full'], + }, + }; + const cli = packageAt('bitfun-cli', 'src/apps/cli/Cargo.toml', [ + pathDependency('src/crates/assembly/bridge', { name: 'bridge' }), + ]); + + const violations = findProductEntrypointCoreFeatureViolations( + [cli, bridge, core], + { root: TEST_ROOT, crateLayoutRules }, + ); + + assert.equal(violations.length, 1); + assert.match(violations[0].message, /bitfun-cli -> bridge -> bitfun-core\/product-full/); +}); + +test('CLI dependency closure resolves renamed optional dependency forwarding', () => { + const core = { + ...packageAt('bitfun-core', 'src/crates/assembly/core/Cargo.toml'), + features: { 'product-full': [] }, + }; + const bridge = { + ...packageAt('bridge', 'src/crates/assembly/bridge/Cargo.toml', [ + pathDependency('src/crates/assembly/core', { + name: 'bitfun-core', + rename: 'core-alias', + optional: true, + usesDefaultFeatures: false, + }), + ]), + features: { + full: ['dep:core-alias', 'core-alias/product-full'], + }, + }; + const cli = packageAt('bitfun-cli', 'src/apps/cli/Cargo.toml', [ + pathDependency('src/crates/assembly/bridge', { + name: 'bridge', + usesDefaultFeatures: false, + features: ['full'], + }), + ]); + + const violations = findProductEntrypointCoreFeatureViolations( + [cli, bridge, core], + { root: TEST_ROOT, crateLayoutRules }, + ); + + assert.equal(violations.length, 1); + assert.match(violations[0].message, /bitfun-core\/product-full/); +}); + +test('CLI dependency closure unions weak forwarding and optional activation per package', () => { + const core = { + ...packageAt('bitfun-core', 'src/crates/assembly/core/Cargo.toml'), + features: { 'product-full': [] }, + }; + const bridge = { + ...packageAt('bridge', 'src/crates/assembly/bridge/Cargo.toml', [ + pathDependency('src/crates/assembly/core', { + name: 'bitfun-core', + optional: true, + usesDefaultFeatures: false, + }), + ]), + features: { + forward: ['bitfun-core?/product-full'], + activate: ['dep:bitfun-core'], + }, + }; + const left = packageAt('left', 'src/crates/assembly/left/Cargo.toml', [ + pathDependency('src/crates/assembly/bridge', { + name: 'bridge', + usesDefaultFeatures: false, + features: ['forward'], + }), + ]); + const right = packageAt('right', 'src/crates/assembly/right/Cargo.toml', [ + pathDependency('src/crates/assembly/bridge', { + name: 'bridge', + usesDefaultFeatures: false, + features: ['activate'], + }), + ]); + const cli = packageAt('bitfun-cli', 'src/apps/cli/Cargo.toml', [ + pathDependency('src/crates/assembly/left', { name: 'left' }), + pathDependency('src/crates/assembly/right', { name: 'right' }), + ]); + + const violations = findProductEntrypointCoreFeatureViolations( + [cli, left, right, bridge, core], + { root: TEST_ROOT, crateLayoutRules }, + ); + + assert.equal(violations.length, 1); + assert.match(violations[0].message, /bitfun-core\/product-full/); +}); + +test('CLI dependency closure keeps normal and build feature unions separate', () => { + const core = { + ...packageAt('bitfun-core', 'src/crates/assembly/core/Cargo.toml'), + features: { 'product-full': [] }, + }; + const bridge = { + ...packageAt('bridge', 'src/crates/assembly/bridge/Cargo.toml', [ + pathDependency('src/crates/assembly/core', { + name: 'bitfun-core', + optional: true, + usesDefaultFeatures: false, + }), + ]), + features: { + forward: ['bitfun-core?/product-full'], + activate: ['dep:bitfun-core'], + }, + }; + const normalParent = packageAt('normal-parent', 'src/crates/assembly/normal-parent/Cargo.toml', [ + pathDependency('src/crates/assembly/bridge', { + name: 'bridge', + usesDefaultFeatures: false, + features: ['forward'], + }), + ]); + const buildParent = packageAt('build-parent', 'src/crates/assembly/build-parent/Cargo.toml', [ + pathDependency('src/crates/assembly/bridge', { + name: 'bridge', + usesDefaultFeatures: false, + features: ['activate'], + }), + ]); + const cli = packageAt('bitfun-cli', 'src/apps/cli/Cargo.toml', [ + pathDependency('src/crates/assembly/normal-parent', { name: 'normal-parent' }), + pathDependency('src/crates/assembly/build-parent', { + name: 'build-parent', + kind: 'build', + }), + ]); + + assert.deepEqual( + findProductEntrypointCoreFeatureViolations( + [cli, normalParent, buildParent, bridge, core], + { root: TEST_ROOT, crateLayoutRules }, + ), + [], + ); +}); + +test('CLI dependency closure keeps proc-macro and normal feature unions separate', () => { + const core = { + ...packageAt('bitfun-core', 'src/crates/assembly/core/Cargo.toml'), + features: { 'product-full': [] }, + }; + const shared = { + ...packageAt('shared', 'src/crates/assembly/shared/Cargo.toml', [ + pathDependency('src/crates/assembly/core', { + name: 'bitfun-core', + optional: true, + usesDefaultFeatures: false, + }), + ]), + features: { + forward: ['bitfun-core?/product-full'], + activate: ['dep:bitfun-core'], + }, + }; + const normalParent = packageAt('normal-parent', 'src/crates/assembly/normal-parent/Cargo.toml', [ + pathDependency('src/crates/assembly/shared', { + name: 'shared', + usesDefaultFeatures: false, + features: ['forward'], + }), + ]); + const macroParent = { + ...packageAt('macro-parent', 'src/crates/assembly/macro-parent/Cargo.toml', [ + pathDependency('src/crates/assembly/shared', { + name: 'shared', + usesDefaultFeatures: false, + features: ['activate'], + }), + ]), + targets: [{ kind: ['proc-macro'] }], + }; + const cli = packageAt('bitfun-cli', 'src/apps/cli/Cargo.toml', [ + pathDependency('src/crates/assembly/normal-parent', { name: 'normal-parent' }), + pathDependency('src/crates/assembly/macro-parent', { name: 'macro-parent' }), + ]); + + assert.deepEqual( + findProductEntrypointCoreFeatureViolations( + [cli, normalParent, macroParent, shared, core], + { root: TEST_ROOT, crateLayoutRules }, + ), + [], + ); +}); + +test('CLI dependency architecture closure cannot hide features behind target cfgs', () => { + const core = { + ...packageAt('bitfun-core', 'src/crates/assembly/core/Cargo.toml'), + features: { 'product-full': [] }, + }; + const bridge = { + ...packageAt('bridge', 'src/crates/assembly/bridge/Cargo.toml', [ + pathDependency('src/crates/assembly/core', { + name: 'bitfun-core', + optional: true, + usesDefaultFeatures: false, + }), + ]), + features: { + forward: ['bitfun-core?/product-full'], + activate: ['dep:bitfun-core'], + }, + }; + const cli = packageAt('bitfun-cli', 'src/apps/cli/Cargo.toml', [ + pathDependency('src/crates/assembly/bridge', { + name: 'bridge', + target: 'cfg(windows)', + usesDefaultFeatures: false, + features: ['forward'], + }), + pathDependency('src/crates/assembly/bridge', { + name: 'bridge', + target: 'cfg(unix)', + usesDefaultFeatures: false, + features: ['activate'], + }), + ]); + + const violations = findProductEntrypointCoreFeatureViolations( + [cli, bridge, core], + { root: TEST_ROOT, crateLayoutRules }, + ); + + assert.equal(violations.length, 1); + assert.match(violations[0].message, /bitfun-core\/product-full/); +}); + +test('CLI dependency architecture closure unions unconditional and target-specific declarations', () => { + const core = { + ...packageAt('bitfun-core', 'src/crates/assembly/core/Cargo.toml'), + features: { 'product-full': [] }, + }; + const bridge = { + ...packageAt('bridge', 'src/crates/assembly/bridge/Cargo.toml', [ + pathDependency('src/crates/assembly/core', { + name: 'bitfun-core', + optional: true, + usesDefaultFeatures: false, + }), + ]), + features: { + forward: ['bitfun-core?/product-full'], + activate: ['dep:bitfun-core'], + }, + }; + const cli = packageAt('bitfun-cli', 'src/apps/cli/Cargo.toml', [ + pathDependency('src/crates/assembly/bridge', { + name: 'bridge', + usesDefaultFeatures: false, + features: ['forward'], + }), + pathDependency('src/crates/assembly/bridge', { + name: 'bridge', + target: 'cfg(windows)', + usesDefaultFeatures: false, + features: ['activate'], + }), + ]); + + const violations = findProductEntrypointCoreFeatureViolations( + [cli, bridge, core], + { root: TEST_ROOT, crateLayoutRules }, + ); + + assert.equal(violations.length, 1); + assert.match(violations[0].message, /bitfun-core\/product-full/); +}); + +function reviewedCoreFeaturesFor(rootName) { + return rootName === 'bitfun-cli' + ? ['agent-runtime', 'canvas-runtime', 'external-sources', 'plugin-runtime', 'ssh-remote'] + : ['agent-runtime', 'canvas-runtime', 'external-sources', 'ssh-remote']; +} + +function targetedWeakForwardingGraph(rootName, forwardTarget, activateTarget, reverse = false) { + const reviewedFeatures = reviewedCoreFeaturesFor(rootName); + const core = { + ...packageAt('bitfun-core', 'src/crates/assembly/core/Cargo.toml'), + features: Object.fromEntries([ + ...reviewedFeatures.map((feature) => [feature, []]), + ['product-full', []], + ]), + }; + const bridge = { + ...packageAt('bridge', 'src/crates/assembly/bridge/Cargo.toml', [ + pathDependency('src/crates/assembly/core', { + name: 'bitfun-core', + optional: true, + usesDefaultFeatures: false, + }), + ]), + features: { + forward: ['bitfun-core?/product-full'], + activate: ['dep:bitfun-core'], + }, + }; + const root = packageAt(rootName, rootName === 'bitfun-cli' + ? 'src/apps/cli/Cargo.toml' + : 'src/crates/interfaces/acp/Cargo.toml', [ + pathDependency('src/crates/assembly/core', { + name: 'bitfun-core', + usesDefaultFeatures: false, + features: reviewedFeatures, + }), + pathDependency('src/crates/assembly/bridge', { + name: 'bridge', + target: forwardTarget, + usesDefaultFeatures: false, + features: [reverse ? 'activate' : 'forward'], + }), + pathDependency('src/crates/assembly/bridge', { + name: 'bridge', + target: activateTarget, + usesDefaultFeatures: false, + features: [reverse ? 'forward' : 'activate'], + }), + ]); + + return { root, bridge, core }; +} + +test('CLI dependency architecture closure ignores Windows target spelling differences', () => { + for (const reverse of [false, true]) { + const { root, bridge, core } = targetedWeakForwardingGraph( + 'bitfun-cli', + 'cfg(windows)', + 'cfg(target_os = "windows")', + reverse, + ); + const violations = findProductEntrypointCoreFeatureViolations( + [root, bridge, core], + { root: TEST_ROOT, crateLayoutRules }, + ); + + assert.equal(violations.length, 1); + assert.match(violations[0].message, /bitfun-core\/product-full/); + } +}); + +test('CLI dependency architecture closure includes Unix and not-Windows declarations', () => { + for (const reverse of [false, true]) { + const { root, bridge, core } = targetedWeakForwardingGraph( + 'bitfun-cli', + 'cfg(not(windows))', + 'cfg(unix)', + reverse, + ); + const violations = findProductEntrypointCoreFeatureViolations( + [root, bridge, core], + { root: TEST_ROOT, crateLayoutRules }, + ); + + assert.equal(violations.length, 1); + assert.match(violations[0].message, /bitfun-core\/product-full/); + } +}); + +test('CLI dependency architecture closure includes nested target-specific declarations', () => { + const reviewedFeatures = reviewedCoreFeaturesFor('bitfun-cli'); + const core = { + ...packageAt('bitfun-core', 'src/crates/assembly/core/Cargo.toml'), + features: Object.fromEntries([ + ...reviewedFeatures.map((feature) => [feature, []]), + ['product-full', []], + ]), + }; + const bridge = packageAt('bridge', 'src/crates/assembly/bridge/Cargo.toml', [ + pathDependency('src/crates/assembly/core', { + name: 'bitfun-core', + target: 'cfg(unix)', + usesDefaultFeatures: false, + features: ['product-full'], + }), + ]); + const cli = packageAt('bitfun-cli', 'src/apps/cli/Cargo.toml', [ + pathDependency('src/crates/assembly/core', { + name: 'bitfun-core', + usesDefaultFeatures: false, + features: reviewedFeatures, + }), + pathDependency('src/crates/assembly/bridge', { + name: 'bridge', + target: 'cfg(windows)', + }), + ]); + + const violations = findProductEntrypointCoreFeatureViolations( + [cli, bridge, core], + { root: TEST_ROOT, crateLayoutRules }, + ); + + assert.equal(violations.length, 1); + assert.match(violations[0].message, /bitfun-core\/product-full/); +}); + +test('CLI dependency architecture closure includes target-specific build dependencies', () => { + const reviewedFeatures = reviewedCoreFeaturesFor('bitfun-cli'); + const core = { + ...packageAt('bitfun-core', 'src/crates/assembly/core/Cargo.toml'), + features: Object.fromEntries([ + ...reviewedFeatures.map((feature) => [feature, []]), + ['product-full', []], + ]), + }; + const bridge = packageAt('bridge', 'src/crates/assembly/bridge/Cargo.toml', [ + pathDependency('src/crates/assembly/core', { + name: 'bitfun-core', + kind: 'build', + target: 'cfg(unix)', + usesDefaultFeatures: false, + features: ['product-full'], + }), + ]); + const cli = packageAt('bitfun-cli', 'src/apps/cli/Cargo.toml', [ + pathDependency('src/crates/assembly/core', { + name: 'bitfun-core', + usesDefaultFeatures: false, + features: reviewedFeatures, + }), + pathDependency('src/crates/assembly/bridge', { + name: 'bridge', + target: 'cfg(windows)', + }), + ]); + + const violations = findProductEntrypointCoreFeatureViolations( + [cli, bridge, core], + { root: TEST_ROOT, crateLayoutRules }, + ); + + assert.equal(violations.length, 1); + assert.match(violations[0].message, /bitfun-core\/product-full/); +}); + +test('CLI dependency closure inspects non-default root features', () => { + const reviewedFeatures = reviewedCoreFeaturesFor('bitfun-cli'); + const core = { + ...packageAt('bitfun-core', 'src/crates/assembly/core/Cargo.toml'), + features: Object.fromEntries([ + ...reviewedFeatures.map((feature) => [feature, []]), + ['product-full', []], + ]), + }; + const bridge = packageAt('bridge', 'src/crates/assembly/bridge/Cargo.toml', [ + pathDependency('src/crates/assembly/core', { + name: 'bitfun-core', + usesDefaultFeatures: false, + features: ['product-full'], + }), + ]); + const cli = { + ...packageAt('bitfun-cli', 'src/apps/cli/Cargo.toml', [ + pathDependency('src/crates/assembly/core', { + name: 'bitfun-core', + usesDefaultFeatures: false, + features: reviewedFeatures, + }), + pathDependency('src/crates/assembly/bridge', { + name: 'bridge', + optional: true, + }), + ]), + features: { + default: [], + bad: ['dep:bridge'], + }, + }; + + const violations = findProductEntrypointCoreFeatureViolations( + [cli, bridge, core], + { root: TEST_ROOT, crateLayoutRules }, + ); + + assert.equal(violations.length, 1); + assert.match(violations[0].message, /bitfun-core\/product-full/); +}); + +test('ACP dependency closure rejects indirect unreviewed Core features', () => { + const reviewedFeatures = reviewedCoreFeaturesFor('bitfun-acp'); + const core = { + ...packageAt('bitfun-core', 'src/crates/assembly/core/Cargo.toml'), + features: Object.fromEntries([ + ...reviewedFeatures.map((feature) => [feature, []]), + ['product-full', []], + ]), + }; + const bridge = packageAt('bridge', 'src/crates/assembly/bridge/Cargo.toml', [ + pathDependency('src/crates/assembly/core', { + name: 'bitfun-core', + usesDefaultFeatures: false, + features: ['product-full'], + }), + ]); + const acp = packageAt('bitfun-acp', 'src/crates/interfaces/acp/Cargo.toml', [ + pathDependency('src/crates/assembly/core', { + name: 'bitfun-core', + usesDefaultFeatures: false, + features: reviewedFeatures, + }), + pathDependency('src/crates/assembly/bridge', { name: 'bridge' }), + ]); + + const violations = findProductEntrypointCoreFeatureViolations( + [acp, bridge, core], + { root: TEST_ROOT, crateLayoutRules }, + ); + + assert.equal(violations.length, 1); + assert.match(violations[0].message, /bitfun-core\/product-full/); +}); + +test('ACP active closure cannot be expanded by a reviewed owner definition', () => { + const reviewedFeatures = reviewedCoreFeaturesFor('bitfun-acp'); + const core = { + ...packageAt('bitfun-core', 'src/crates/assembly/core/Cargo.toml'), + features: { + 'agent-runtime': [], + 'canvas-runtime': ['plugin-runtime'], + 'external-sources': [], + 'plugin-runtime': [], + 'ssh-remote': [], + }, + }; + const acp = packageAt('bitfun-acp', 'src/crates/interfaces/acp/Cargo.toml', [ + pathDependency('src/crates/assembly/core', { + name: 'bitfun-core', + usesDefaultFeatures: false, + features: reviewedFeatures, + }), + ]); + + const violations = findProductEntrypointCoreFeatureViolations( + [acp, core], + { root: TEST_ROOT, crateLayoutRules }, + ); + + assert.equal(violations.length, 1); + assert.match(violations[0].message, /bitfun-core\/plugin-runtime/); +}); + +test('CLI dependency closure includes build dependencies and excluded capabilities', () => { + const core = { + ...packageAt('bitfun-core', 'src/crates/assembly/core/Cargo.toml'), + features: { + 'cli-everything': ['announcement', 'debug-log'], + announcement: [], + 'debug-log': [], + }, + }; + const bridge = packageAt('bridge', 'src/crates/assembly/bridge/Cargo.toml', [ + pathDependency('src/crates/assembly/core', { + name: 'bitfun-core', + kind: 'build', + usesDefaultFeatures: false, + features: ['cli-everything'], + }), + ]); + const cli = packageAt('bitfun-cli', 'src/apps/cli/Cargo.toml', [ + pathDependency('src/crates/assembly/bridge', { name: 'bridge' }), + ]); + + const violations = findProductEntrypointCoreFeatureViolations( + [cli, bridge, core], + { root: TEST_ROOT, crateLayoutRules }, + ); + + assert.equal(violations.length, 1); + assert.match(violations[0].message, /bitfun-core\/announcement/); +}); + +test('CLI dependency closure excludes the Core dispatch store', () => { + const core = { + ...packageAt('bitfun-core', 'src/crates/assembly/core/Cargo.toml'), + features: { 'dispatch-store': [] }, + }; + const bridge = packageAt('bridge', 'src/crates/assembly/bridge/Cargo.toml', [ + pathDependency('src/crates/assembly/core', { + name: 'bitfun-core', + usesDefaultFeatures: false, + features: ['dispatch-store'], + }), + ]); + const cli = packageAt('bitfun-cli', 'src/apps/cli/Cargo.toml', [ + pathDependency('src/crates/assembly/bridge', { name: 'bridge' }), + ]); + + const violations = findProductEntrypointCoreFeatureViolations( + [cli, bridge, core], + { root: TEST_ROOT, crateLayoutRules }, + ); + + assert.equal(violations.length, 1); + assert.match(violations[0].message, /bitfun-core\/dispatch-store/); +}); + test('cargo layer checker rejects reverse edges across dependency kinds', () => { const packages = [ packageAt('entry', 'src/apps/example/Cargo.toml'), diff --git a/scripts/core-boundaries/cargo-dependency-boundaries.mjs b/scripts/core-boundaries/cargo-dependency-boundaries.mjs index d253744f97..2287d11cd2 100644 --- a/scripts/core-boundaries/cargo-dependency-boundaries.mjs +++ b/scripts/core-boundaries/cargo-dependency-boundaries.mjs @@ -59,6 +59,68 @@ function dependencyDescription(dependency) { return `${kind}${optional} dependency${target}`; } +function expandedLocalFeatures(featureGraph, selectedFeatures, useDefaultFeatures) { + const pending = [...selectedFeatures]; + if (useDefaultFeatures && Object.hasOwn(featureGraph, 'default')) { + pending.push('default'); + } + const active = new Set(); + const references = new Set(); + + while (pending.length > 0) { + const feature = pending.pop(); + if (active.has(feature)) { + continue; + } + active.add(feature); + for (const reference of featureGraph[feature] ?? []) { + references.add(reference); + if (Object.hasOwn(featureGraph, reference)) { + pending.push(reference); + } + } + } + + return { active, references }; +} + +function dependencyAlias(dependency) { + return dependency.rename ?? dependency.name; +} + +function dependencyActivation(dependency, sourceFeatureState) { + const alias = dependencyAlias(dependency); + const forwarded = []; + let explicitlyActivated = false; + for (const reference of sourceFeatureState.references) { + if (reference === `dep:${alias}`) { + explicitlyActivated = true; + continue; + } + const match = reference.match(/^([^/?]+)(\?)?\/(.+)$/); + if (match?.[1] !== alias) { + continue; + } + if (!match[2]) { + explicitlyActivated = true; + } + forwarded.push(match[3]); + } + + if (dependency.optional && !explicitlyActivated) { + return null; + } + return { + features: [...new Set([...(dependency.features ?? []), ...forwarded])], + useDefaultFeatures: dependency.uses_default_features !== false, + }; +} + +function isProcMacroPackage(pkg) { + return (pkg.targets ?? []).some((target) => + (target.kind ?? []).includes('proc-macro')); +} + const SERVICES_INTEGRATIONS_TOKIO_FEATURES = new Map([ ['announcement', ['fs', 'sync']], ['browser-control', ['time']], @@ -708,6 +770,48 @@ export function findProductEntrypointCoreFeatureViolations( packages, { root, crateLayoutRules }, ) { + const reviewedCoreFeatureClosures = new Map([ + ['bitfun-cli', [ + 'agent-runtime', + 'canvas-runtime', + 'external-sources', + 'plugin-runtime', + 'ssh-remote', + ]], + ['bitfun-acp', [ + 'agent-runtime', + 'canvas-runtime', + 'external-sources', + 'ssh-remote', + ]], + ]); + const acpActiveCoreFeatures = [ + 'agent-runtime', + 'ai-adapter-runtime', + 'canvas-runtime', + 'external-sources', + 'file-watch', + 'filesystem', + 'git', + 'lsp', + 'local-storage', + 'plugin-source', + 'process-runtime', + 'product-capabilities', + 'product-domains', + 'remote-workspace', + 'review-platform', + 'runtime-services', + 'ssh-remote', + 'terminal', + 'tool-packs', + 'workspace-runtime', + 'workspace-watch', + ]; + const reviewedActiveCoreFeatureClosures = new Map([ + ['bitfun-cli', [...acpActiveCoreFeatures, 'plugin-runtime']], + ['bitfun-acp', acpActiveCoreFeatures], + ]); const packageByManifest = new Map( packages.map((pkg) => [normalizedPath(pkg.manifest_path), pkg]), ); @@ -746,6 +850,192 @@ export function findProductEntrypointCoreFeatureViolations( message: `product entrypoint ${sourcePackage.name} must select at least one explicit feature for its bitfun-core ${dependencyDescription(dependency)}`, }); } + const reviewedClosure = reviewedCoreFeatureClosures.get(sourcePackage.name); + if (reviewedClosure) { + const selectedFeatures = new Set(dependency.features ?? []); + for (const requiredFeature of reviewedClosure) { + if (!selectedFeatures.has(requiredFeature)) { + violations.push({ + path: sourcePackage.manifest_path, + line: 1, + message: `${sourcePackage.name} Core capability closure must include ${requiredFeature}`, + }); + } + } + for (const selectedFeature of selectedFeatures) { + if (!reviewedClosure.includes(selectedFeature)) { + violations.push({ + path: sourcePackage.manifest_path, + line: 1, + message: `${sourcePackage.name} Core capability closure must not include unreviewed feature ${selectedFeature}`, + }); + } + } + } + } + } + + const corePackage = packages.find((pkg) => pkg.name === 'bitfun-core'); + if (corePackage) { + const forbiddenCoreFeatures = [ + 'product-full', + 'announcement', + 'debug-log', + 'dispatch-store', + ]; + const reportedUnexpectedFeatures = new Set(); + + for (const [rootName, reviewedClosure] of reviewedCoreFeatureClosures) { + const rootPackage = packages.find((pkg) => pkg.name === rootName); + if (!rootPackage) { + continue; + } + const allowedCoreFeatures = new Set( + reviewedActiveCoreFeatureClosures.get(rootName) ?? [], + ); + const rootSelectedFeatures = Object.keys(rootPackage.features ?? {}) + .filter((feature) => feature !== 'default'); + const rootLabel = rootName === 'bitfun-cli' ? 'CLI' : 'ACP'; + + const packageStates = new Map(); + const pending = []; + const queued = new Set(); + + const mergePackageState = ( + pkg, + dependencyKindContext, + selectedFeatures, + useDefaultFeatures, + packagePath, + ) => { + const key = [ + normalizedPath(pkg.manifest_path), + dependencyKindContext, + ].join('|'); + let state = packageStates.get(key); + if (!state) { + state = { + pkg, + dependencyKindContext, + selectedFeatures: new Set(), + useDefaultFeatures: false, + featureState: { active: new Set(), references: new Set() }, + packagePath, + initialized: false, + }; + packageStates.set(key, state); + } + + let changed = false; + for (const feature of selectedFeatures) { + if (!state.selectedFeatures.has(feature)) { + state.selectedFeatures.add(feature); + changed = true; + } + } + if (useDefaultFeatures && !state.useDefaultFeatures) { + state.useDefaultFeatures = true; + changed = true; + } + if (!changed && state.initialized) { + return state; + } + + state.featureState = expandedLocalFeatures( + pkg.features ?? {}, + state.selectedFeatures, + state.useDefaultFeatures, + ); + state.initialized = true; + if (!queued.has(key)) { + pending.push(key); + queued.add(key); + } + return state; + }; + + // This is an architecture declaration check, not a target simulator. + // Cargo target cfg facts are multi-valued and evolve with rustc. Treating + // every declared target edge as reachable prevents a platform-only path + // from hiding an unreviewed Core owner. Products that genuinely need + // different owners must express that difference through package/module + // boundaries. Cargo features are additive, so all root features form the + // strongest buildable profile. + mergePackageState( + rootPackage, + 'normal', + rootSelectedFeatures, + true, + [rootPackage.name], + ); + + while (pending.length > 0) { + const stateKey = pending.shift(); + queued.delete(stateKey); + const { + pkg: sourcePackage, + dependencyKindContext, + featureState, + packagePath, + } = packageStates.get(stateKey); + + for (const dependency of sourcePackage.dependencies ?? []) { + const kind = dependency.kind ?? 'normal'; + if ( + !dependency.path + || (kind !== 'normal' && kind !== 'build') + || repositoryPath(root, dependency.path) === null + ) { + continue; + } + const activation = dependencyActivation(dependency, featureState); + if (!activation) { + continue; + } + const targetPackage = packageByManifest.get( + normalizedPath(join(dependency.path, 'Cargo.toml')), + ); + if (!targetPackage) { + continue; + } + const targetDependencyKindContext = + dependencyKindContext === 'build' + || kind === 'build' + || isProcMacroPackage(targetPackage) + ? 'build' + : 'normal'; + const targetPath = [...packagePath, targetPackage.name]; + const targetState = mergePackageState( + targetPackage, + targetDependencyKindContext, + activation.features, + activation.useDefaultFeatures, + targetPath, + ); + + if (targetPackage.name === 'bitfun-core') { + const activeCoreFeatures = targetState.featureState.active; + const unexpected = forbiddenCoreFeatures.find((feature) => + activeCoreFeatures.has(feature)) + ?? [...activeCoreFeatures] + .filter((feature) => !allowedCoreFeatures.has(feature)) + .sort()[0]; + const reportKey = [rootName, targetDependencyKindContext, unexpected].join('|'); + if (unexpected && !reportedUnexpectedFeatures.has(reportKey)) { + reportedUnexpectedFeatures.add(reportKey); + violations.push({ + path: sourcePackage.manifest_path, + line: 1, + message: `${rootLabel} dependency closure must not enable ${unexpected}: ${[ + ...packagePath, + `${targetPackage.name}/${unexpected}`, + ].join(' -> ')}`, + }); + } + continue; + } + } + } } } diff --git a/scripts/core-boundaries/rules/feature-rules.mjs b/scripts/core-boundaries/rules/feature-rules.mjs index 8ccdb84d0d..3a1af0e3a7 100644 --- a/scripts/core-boundaries/rules/feature-rules.mjs +++ b/scripts/core-boundaries/rules/feature-rules.mjs @@ -65,11 +65,16 @@ export const optionalDependencyFeatureOwnerRules = [ reason: 'bitfun-core product/runtime optional dependencies must stay owned by explicit feature gates', dependencies: [ - { depName: 'axum', ownerFeatures: ['product-full'] }, + { depName: 'axum', ownerFeatures: ['agent-runtime', 'debug-log'] }, { depName: 'bitfun-ai-adapters', ownerFeatures: ['ai-adapter-runtime'] }, - { depName: 'bitfun-agent-runtime', ownerFeatures: ['product-full'] }, - { depName: 'bitfun-agent-stream', ownerFeatures: ['product-full'] }, - { depName: 'bitfun-harness', ownerFeatures: ['product-full'] }, + { depName: 'bitfun-agent-runtime', ownerFeatures: ['agent-runtime'] }, + { depName: 'bitfun-agent-stream', ownerFeatures: ['agent-runtime'] }, + { depName: 'bitfun-claude-code-adapter', ownerFeatures: ['external-sources'] }, + { depName: 'bitfun-codex-adapter', ownerFeatures: ['external-sources'] }, + { depName: 'bitfun-external-sources', ownerFeatures: ['external-sources'] }, + { depName: 'bitfun-harness', ownerFeatures: ['agent-runtime'] }, + { depName: 'bitfun-opencode-adapter', ownerFeatures: ['external-sources'] }, + { depName: 'bitfun-plugin-runtime-client', ownerFeatures: ['plugin-runtime'] }, { depName: 'bitfun-product-capabilities', ownerFeatures: ['product-capabilities'] }, { depName: 'bitfun-product-domains', ownerFeatures: ['product-domains'] }, { depName: 'bitfun-runtime-services', ownerFeatures: ['runtime-services'] }, @@ -77,38 +82,40 @@ export const optionalDependencyFeatureOwnerRules = [ depName: 'bitfun-services-integrations', ownerFeatures: [ 'announcement', + 'agent-runtime', 'canvas-runtime', + 'debug-log', + 'external-sources', 'file-watch', 'git', 'plugin-source', 'product-domains', - 'product-full', 'remote-workspace', 'review-platform', 'ssh-remote', ], }, { depName: 'bitfun-tool-packs', ownerFeatures: ['tool-packs'] }, - { depName: 'chrono-tz', ownerFeatures: ['product-full'] }, - { depName: 'cron', ownerFeatures: ['product-full'] }, - { depName: 'dashmap', ownerFeatures: ['product-full'] }, - { depName: 'filetime', ownerFeatures: ['product-full'] }, - { depName: 'flate2', ownerFeatures: ['product-full'] }, - { depName: 'fs2', ownerFeatures: ['product-full'] }, - { depName: 'image', ownerFeatures: ['product-full', 'tool-packs'] }, - { depName: 'include_dir', ownerFeatures: ['product-full'] }, - { depName: 'indexmap', ownerFeatures: ['product-full'] }, - { depName: 'md5', ownerFeatures: ['product-full'] }, - { depName: 'reqwest', ownerFeatures: ['ai-adapter-runtime', 'product-full'] }, - { depName: 'rusqlite', ownerFeatures: ['product-full'] }, - { depName: 'semver', ownerFeatures: ['product-full'] }, + { depName: 'chrono-tz', ownerFeatures: ['agent-runtime'] }, + { depName: 'cron', ownerFeatures: ['agent-runtime'] }, + { depName: 'dashmap', ownerFeatures: ['agent-runtime'] }, + { depName: 'filetime', ownerFeatures: ['agent-runtime'] }, + { depName: 'flate2', ownerFeatures: ['agent-runtime'] }, + { depName: 'fs2', ownerFeatures: ['agent-runtime'] }, + { depName: 'image', ownerFeatures: ['agent-runtime', 'tool-packs'] }, + { depName: 'include_dir', ownerFeatures: ['agent-runtime'] }, + { depName: 'indexmap', ownerFeatures: ['agent-runtime'] }, + { depName: 'md5', ownerFeatures: ['agent-runtime'] }, + { depName: 'reqwest', ownerFeatures: ['ai-adapter-runtime', 'agent-runtime'] }, + { depName: 'rusqlite', ownerFeatures: ['agent-runtime'] }, + { depName: 'semver', ownerFeatures: ['agent-runtime'] }, { depName: 'serde_yaml', ownerFeatures: ['workspace-runtime'] }, - { depName: 'similar', ownerFeatures: ['product-full'] }, + { depName: 'similar', ownerFeatures: ['agent-runtime'] }, { depName: 'terminal-core', ownerFeatures: ['terminal'] }, { depName: 'notify', ownerFeatures: ['lsp', 'workspace-watch'] }, - { depName: 'tokio-tungstenite', ownerFeatures: ['product-full'] }, - { depName: 'tower-http', ownerFeatures: ['product-full'] }, - { depName: 'tool-runtime', ownerFeatures: ['product-full'] }, + { depName: 'tokio-tungstenite', ownerFeatures: ['agent-runtime'] }, + { depName: 'tower-http', ownerFeatures: ['debug-log'] }, + { depName: 'tool-runtime', ownerFeatures: ['agent-runtime'] }, ], }, { @@ -200,7 +207,10 @@ export const coreProductFullFeatureAssemblyRule = { manifestPath: 'src/crates/assembly/core/Cargo.toml', featureName: 'product-full', requiredFeatureRefs: [ + 'agent-runtime', 'announcement', + 'canvas-runtime', + 'debug-log', 'dispatch-store', 'file-watch', 'filesystem', @@ -208,6 +218,8 @@ export const coreProductFullFeatureAssemblyRule = { 'lsp', 'local-storage', 'process-runtime', + 'external-sources', + 'plugin-runtime', 'remote-workspace', 'review-platform', 'ssh-remote', @@ -222,6 +234,152 @@ export const coreProductFullFeatureAssemblyRule = { }; export const coreClosedFeatureProfileRules = [ + { + manifestPath: 'src/crates/assembly/core/Cargo.toml', + featureName: 'agent-runtime', + requiredFeatureRefs: [ + 'ai-adapter-runtime', + 'dep:bitfun-agent-runtime', + 'dep:bitfun-agent-content', + 'dep:bitfun-agent-stream', + 'dep:bitfun-harness', + 'dep:chrono-tz', + 'dep:cron', + 'dep:dashmap', + 'dep:filetime', + 'dep:flate2', + 'dep:fs2', + 'dep:include_dir', + 'dep:indexmap', + 'dep:image', + 'dep:md5', + 'dep:reqwest', + 'dep:semver', + 'dep:rusqlite', + 'dep:similar', + 'dep:tokio-tungstenite', + 'dep:tool-runtime', + 'dep:axum', + 'bitfun-services-integrations/browser-control', + 'bitfun-services-integrations/deep-research', + 'bitfun-services-integrations/mcp', + 'bitfun-services-integrations/remote-connect', + 'bitfun-services-integrations/script-tool-runtime', + 'bitfun-services-integrations/web-tools', + 'bitfun-services-integrations/workspace-search', + 'tokio/rt-multi-thread', + 'bitfun-services-core/dispatch-workspace', + 'bitfun-services-core/permission', + 'bitfun-services-core/runtime-ownership', + 'bitfun-services-core/session-git', + 'filesystem', + 'lsp', + 'local-storage', + 'process-runtime', + 'remote-workspace', + 'terminal', + 'workspace-runtime', + 'product-capabilities', + 'product-domains', + 'runtime-services', + 'git', + 'review-platform', + 'tool-packs', + ], + allowedTransitiveFeatureRefs: ['plugin-source'], + exact: true, + reason: + 'bitfun-core agent-runtime is the reviewed Core Agent Runtime owner closure, not a product-full alias', + }, + { + manifestPath: 'src/crates/assembly/core/Cargo.toml', + featureName: 'external-sources', + requiredFeatureRefs: [ + 'agent-runtime', + 'dep:bitfun-opencode-adapter', + 'dep:bitfun-claude-code-adapter', + 'dep:bitfun-codex-adapter', + 'dep:bitfun-external-sources', + 'bitfun-services-integrations/hook-import', + 'file-watch', + 'workspace-watch', + ], + allowedTransitiveFeatureRefs: [ + 'ai-adapter-runtime', + 'filesystem', + 'lsp', + 'local-storage', + 'process-runtime', + 'remote-workspace', + 'terminal', + 'workspace-runtime', + 'product-capabilities', + 'product-domains', + 'runtime-services', + 'git', + 'review-platform', + 'tool-packs', + 'plugin-source', + ], + exact: true, + reason: + 'bitfun-core external-sources must compose only the reviewed source adapters, watchers, and Core runtime owner', + }, + { + manifestPath: 'src/crates/assembly/core/Cargo.toml', + featureName: 'plugin-runtime', + requiredFeatureRefs: ['external-sources', 'dep:bitfun-plugin-runtime-client'], + allowedTransitiveFeatureRefs: [ + 'agent-runtime', + 'file-watch', + 'workspace-watch', + 'ai-adapter-runtime', + 'filesystem', + 'lsp', + 'local-storage', + 'process-runtime', + 'remote-workspace', + 'terminal', + 'workspace-runtime', + 'product-capabilities', + 'product-domains', + 'runtime-services', + 'git', + 'review-platform', + 'tool-packs', + 'plugin-source', + ], + exact: true, + reason: + 'bitfun-core plugin-runtime must add only the executable client boundary to external source composition', + }, + { + manifestPath: 'src/crates/assembly/core/Cargo.toml', + featureName: 'canvas-runtime', + requiredFeatureRefs: [ + 'product-domains', + 'bitfun-services-integrations/canvas-runtime', + ], + allowedTransitiveFeatureRefs: [ + 'ai-adapter-runtime', + 'plugin-source', + 'runtime-services', + ], + exact: true, + reason: + 'bitfun-core canvas-runtime must extend only the product domain surface with Canvas runtime IO', + }, + { + manifestPath: 'src/crates/assembly/core/Cargo.toml', + featureName: 'debug-log', + requiredFeatureRefs: [ + 'dep:axum', + 'dep:tower-http', + 'bitfun-services-integrations/debug-log', + ], + exact: true, + reason: 'bitfun-core debug-log must own only the debug ingest HTTP capability', + }, { manifestPath: 'src/crates/services/services-core/Cargo.toml', featureName: 'default', diff --git a/scripts/core-boundaries/rules/source/required-rules.mjs b/scripts/core-boundaries/rules/source/required-rules.mjs index 77502e31ac..3f044aa894 100644 --- a/scripts/core-boundaries/rules/source/required-rules.mjs +++ b/scripts/core-boundaries/rules/source/required-rules.mjs @@ -3926,8 +3926,8 @@ export const requiredContentRules = [ message: 'core ai-adapter-runtime feature must explicitly enable the optional dependency', }, { - regex: /product-full = \[[^\]]*"ai-adapter-runtime"[^\]]*\]/, - message: 'core product-full assembly must explicitly opt into AI adapter runtime', + regex: /agent-runtime = \[[^\]]*"ai-adapter-runtime"[^\]]*\]/, + message: 'core agent-runtime assembly must explicitly opt into AI adapter runtime', }, { regex: /product-domains = \[[^\]]*"ai-adapter-runtime"[^\]]*\]/, @@ -3973,8 +3973,10 @@ export const requiredContentRules = [ message: 'core product-full must explicitly enable tool pack product features', }, { - regex: /"bitfun-services-integrations\/product-full"/, - message: 'core product-full must explicitly enable integration product features', + regex: + /agent-runtime = \[[\s\S]*"bitfun-services-integrations\/mcp"[\s\S]*"bitfun-services-integrations\/remote-connect"[\s\S]*"bitfun-services-integrations\/workspace-search"[\s\S]*\]/, + message: + 'core agent-runtime must directly assemble the MCP, Remote Connect, and workspace-search services it exposes', }, { regex: /"dep:bitfun-product-domains"/, @@ -3997,12 +3999,12 @@ export const requiredContentRules = [ 'no-default bitfun-core must keep product runtime surfaces behind explicit features', patterns: [ { - regex: /#\[cfg\(feature = "product-full"\)\]\s*pub mod agentic\b/s, - message: 'agentic runtime must stay behind product-full for no-default builds', + regex: /#\[cfg\(feature = "agent-runtime"\)\]\s*pub mod agentic\b/s, + message: 'agentic runtime must stay behind agent-runtime for no-default builds', }, { - regex: /#\[cfg\(feature = "product-full"\)\]\s*mod external_subagents\b/s, - message: 'external subagent product assembly must stay behind product-full', + regex: /#\[cfg\(feature = "external-sources"\)\]\s*mod external_subagents\b/s, + message: 'external subagent product assembly must stay behind external-sources', }, { regex: /#\[cfg\(feature = "product-domains"\)\]\s*pub mod function_agents\b/s, @@ -4013,8 +4015,16 @@ export const requiredContentRules = [ message: 'MiniApp product domain facade must stay behind product-domains', }, { - regex: /#\[cfg\(feature = "product-full"\)\]\s*pub\(crate\) mod service_agent_runtime\b/s, - message: 'service agent runtime owner assembly must stay behind product-full', + regex: /#\[cfg\(feature = "agent-runtime"\)\]\s*pub\(crate\) mod service_agent_runtime\b/s, + message: 'service agent runtime owner assembly must stay behind agent-runtime', + }, + { + regex: /#\[cfg\(feature = "agent-runtime"\)\]\s*pub mod native_hooks\b/s, + message: 'native Agent hook dispatch must stay owned by agent-runtime', + }, + { + regex: /#\[cfg\(feature = "external-sources"\)\]\s*mod instruction_sources\b/s, + message: 'third-party instruction discovery must stay behind external-sources', }, ], }, @@ -4025,17 +4035,17 @@ export const requiredContentRules = [ patterns: [ { regex: - /#\[cfg\(feature = "product-full"\)\]\s*mod baseline;[\s\S]*?#\[cfg\(feature = "product-full"\)\]\s*mod controller;[\s\S]*?#\[cfg\(feature = "product-full"\)\]\s*mod device_controller;[\s\S]*?#\[cfg\(feature = "product-full"\)\]\s*mod preparation;/s, - message: 'Dispatch product controllers must stay behind product-full', + /#\[cfg\(feature = "agent-runtime"\)\]\s*mod baseline;[\s\S]*?#\[cfg\(all\(feature = "agent-runtime", feature = "ssh-remote"\)\)\]\s*mod controller;[\s\S]*?#\[cfg\(all\(feature = "agent-runtime", feature = "ssh-remote"\)\)\]\s*mod device_controller;[\s\S]*?#\[cfg\(feature = "agent-runtime"\)\]\s*mod preparation;/s, + message: 'Dispatch SSH/device controllers must require both agent-runtime and ssh-remote', }, { regex: - /#\[cfg\(feature = "product-full"\)\]\s*async fn release_baseline_claim\b/s, - message: 'worktree-backed dispatch claim release must stay behind product-full', + /#\[cfg\(feature = "agent-runtime"\)\]\s*async fn release_baseline_claim\b/s, + message: 'worktree-backed dispatch claim release must stay behind agent-runtime', }, { regex: - /#\[cfg\(not\(feature = "product-full"\)\)\]\s*async fn release_baseline_claim\([^)]*\)\s*->\s*Result<\(\), DispatchStoreError>\s*\{\s*Err\(\s*DispatchStoreError::ClaimRelease\([\s\S]*?\)\s*\)\s*\}/s, + /#\[cfg\(not\(feature = "agent-runtime"\)\)\]\s*async fn release_baseline_claim\([^)]*\)\s*->\s*Result<\(\), DispatchStoreError>\s*\{\s*Err\(\s*DispatchStoreError::ClaimRelease\([\s\S]*?\)\s*\)\s*\}/s, message: 'no-default dispatch claim release must fail closed', }, ], @@ -4053,8 +4063,8 @@ export const requiredContentRules = [ message: 'AI subscription auth runtime must stay behind ai-adapter-runtime', }, { - regex: /#\[cfg\(feature = "product-full"\)\]\s*pub mod debug_log\b/s, - message: 'debug ingest HTTP server must stay behind product-full', + regex: /#\[cfg\(feature = "debug-log"\)\]\s*pub mod debug_log\b/s, + message: 'debug ingest HTTP server must stay behind debug-log', }, ], }, @@ -4090,28 +4100,28 @@ export const requiredContentRules = [ message: 'git service facade must stay behind its exact feature', }, { - regex: /#\[cfg\(feature = "product-full"\)\]\s*pub mod mcp\b/s, - message: 'Core MCP product bridge must stay behind product-full', + regex: /#\[cfg\(feature = "agent-runtime"\)\]\s*pub mod mcp\b/s, + message: 'Core MCP product bridge must stay behind agent-runtime', }, { - regex: /#\[cfg\(feature = "product-full"\)\]\s*pub mod remote_connect\b/s, - message: 'Core Remote Connect product bridge must stay behind product-full', + regex: /#\[cfg\(feature = "agent-runtime"\)\]\s*pub mod remote_connect\b/s, + message: 'Core Remote Connect product bridge must stay behind agent-runtime', }, { regex: /#\[cfg\(feature = "review-platform"\)\]\s*pub mod review_platform\b/s, message: 'review platform facade must stay behind its exact feature', }, { - regex: /#\[cfg\(feature = "product-full"\)\]\s*pub mod search\b/s, - message: 'workspace search facade must stay behind product-full', + regex: /#\[cfg\(feature = "agent-runtime"\)\]\s*pub mod search\b/s, + message: 'workspace search facade must stay behind agent-runtime', }, { - regex: /#\[cfg\(feature = "product-full"\)\]\s*pub use search::/s, - message: 'workspace search exports must stay behind product-full', + regex: /#\[cfg\(feature = "agent-runtime"\)\]\s*pub use search::/s, + message: 'workspace search exports must stay behind agent-runtime', }, { - regex: /#\[cfg\(feature = "product-full"\)\]\s*pub mod snapshot\b/s, - message: 'snapshot service must stay behind product-full until tool-runtime ownership is split', + regex: /#\[cfg\(feature = "agent-runtime"\)\]\s*pub mod snapshot\b/s, + message: 'snapshot service must stay behind agent-runtime until tool-runtime ownership is split', }, ], }, @@ -4121,8 +4131,8 @@ export const requiredContentRules = [ 'mode config canonicalization depends on product agent/tool registries and must stay out of no-default builds', patterns: [ { - regex: /#\[cfg\(feature = "product-full"\)\]\s*pub mod mode_config_canonicalizer\b/s, - message: 'mode config canonicalizer must stay behind product-full', + regex: /#\[cfg\(feature = "agent-runtime"\)\]\s*pub mod mode_config_canonicalizer\b/s, + message: 'mode config canonicalizer must stay behind agent-runtime', }, ], }, @@ -4148,12 +4158,12 @@ export const requiredContentRules = [ 'workspace runtime binding helpers may depend on agentic runtime only in full product builds and must delegate legacy session-store migration to services-core', patterns: [ { - regex: /#\[cfg\(feature = "product-full"\)\]\s*use crate::agentic::WorkspaceBinding\b/s, + regex: /#\[cfg\(feature = "agent-runtime"\)\]\s*use crate::agentic::WorkspaceBinding\b/s, message: 'WorkspaceBinding import must stay gated for no-default builds', }, { - regex: /#\[cfg\(feature = "product-full"\)\]\s*pub async fn ensure_runtime_for_workspace_binding\b/s, - message: 'WorkspaceBinding runtime helper must stay behind product-full', + regex: /#\[cfg\(feature = "agent-runtime"\)\]\s*pub async fn ensure_runtime_for_workspace_binding\b/s, + message: 'WorkspaceBinding runtime helper must stay behind agent-runtime', }, { regex: /\bmerge_legacy_session_store\b/, diff --git a/scripts/core-boundaries/self-test.mjs b/scripts/core-boundaries/self-test.mjs index 39c39aa99d..a29845408f 100644 --- a/scripts/core-boundaries/self-test.mjs +++ b/scripts/core-boundaries/self-test.mjs @@ -229,7 +229,10 @@ export function runManifestParserSelfTest({ } for (const featureName of [ + 'agent-runtime', 'announcement', + 'canvas-runtime', + 'debug-log', 'dispatch-store', 'file-watch', 'filesystem', @@ -237,6 +240,8 @@ export function runManifestParserSelfTest({ 'lsp', 'local-storage', 'process-runtime', + 'external-sources', + 'plugin-runtime', 'remote-workspace', 'review-platform', 'ssh-remote', @@ -364,6 +369,11 @@ export function runManifestParserSelfTest({ 'bitfun-services-integrations/remote-ssh', ], ], + [ + coreManifest, + 'canvas-runtime', + ['product-domains', 'bitfun-services-integrations/canvas-runtime'], + ], [coreManifest, 'announcement', ['bitfun-services-integrations/announcement']], [coreManifest, 'file-watch', ['bitfun-services-integrations/file-watch']], [coreManifest, 'git', ['bitfun-services-integrations/git']], @@ -4162,7 +4172,10 @@ export function runManifestParserSelfTest({ 'dep:bitfun-product-capabilities', 'dep:bitfun-tool-packs', 'bitfun-tool-packs\\/product-full', - 'bitfun-services-integrations\\/product-full', + 'agent-runtime', + 'bitfun-services-integrations\\/mcp', + 'bitfun-services-integrations\\/remote-connect', + 'bitfun-services-integrations\\/workspace-search', 'dep:bitfun-product-domains', 'bitfun-product-domains\\/product-full', ], @@ -4170,13 +4183,14 @@ export function runManifestParserSelfTest({ { path: 'src/crates/assembly/core/src/lib.rs', contracts: [ - 'feature = "product-full"', + 'feature = "agent-runtime"', 'pub mod agentic', + 'feature = "external-sources"', 'mod external_subagents', 'feature = "product-domains"', 'pub mod function_agents', 'pub mod miniapp', - 'feature = "product-full"', + 'feature = "agent-runtime"', 'service_agent_runtime', ], }, @@ -4186,7 +4200,7 @@ export function runManifestParserSelfTest({ 'feature = "ai-adapter-runtime"', 'pub mod ai', 'pub mod subscription_auth', - 'feature = "product-full"', + 'feature = "debug-log"', 'pub mod debug_log', ], }, @@ -4208,18 +4222,21 @@ export function runManifestParserSelfTest({ 'file_watch', 'feature = "git"', 'pub mod git', - 'feature = "product-full"', + 'feature = "agent-runtime"', 'pub mod mcp', + 'feature = "agent-runtime"', 'pub mod remote_connect', 'feature = "review-platform"', 'pub mod review_platform', + 'feature = "agent-runtime"', 'pub mod search', + 'feature = "agent-runtime"', 'pub mod snapshot', ], }, { path: 'src/crates/assembly/core/src/service/config/mod.rs', - contracts: ['feature = "product-full"', 'mode_config_canonicalizer'], + contracts: ['feature = "agent-runtime"', 'mode_config_canonicalizer'], }, { path: 'src/crates/assembly/core/src/service/workspace/manager.rs', @@ -4232,7 +4249,7 @@ export function runManifestParserSelfTest({ { path: 'src/crates/assembly/core/src/service/workspace_runtime/service.rs', contracts: [ - 'feature = "product-full"', + 'feature = "agent-runtime"', 'WorkspaceBinding', 'ensure_runtime_for_workspace_binding', 'merge_legacy_session_store', @@ -4243,8 +4260,8 @@ export function runManifestParserSelfTest({ { path: 'src/crates/assembly/core/src/service/dispatch/mod.rs', contracts: [ - 'feature = "product-full"', - 'not\\(feature = "product-full"\\)', + 'feature = "agent-runtime"', + 'not\\(feature = "agent-runtime"\\)', 'release_baseline_claim', 'DispatchStoreError::ClaimRelease', ], @@ -4828,12 +4845,12 @@ export function runManifestParserSelfTest({ throw new Error('missing no-default dispatch claim release boundary rule'); } const failClosedDispatchRelease = ` -#[cfg(not(feature = "product-full"))] +#[cfg(not(feature = "agent-runtime"))] async fn release_baseline_claim(release: BaselineClaimRelease) -> Result<(), DispatchStoreError> { Err(DispatchStoreError::ClaimRelease(format!("job_id={}", release.job_id))) }`; const unsafeDispatchRelease = ` -#[cfg(not(feature = "product-full"))] +#[cfg(not(feature = "agent-runtime"))] async fn release_baseline_claim(release: BaselineClaimRelease) -> Result<(), DispatchStoreError> { let _ignored = DispatchStoreError::ClaimRelease(format!("job_id={}", release.job_id)); Ok(()) diff --git a/src/apps/cli/AGENTS.md b/src/apps/cli/AGENTS.md index 9054a6bbf4..800436ba5c 100644 --- a/src/apps/cli/AGENTS.md +++ b/src/apps/cli/AGENTS.md @@ -88,6 +88,10 @@ restrictions remain enforced. - Assemble CLI through `DeliveryProfile::Cli` and validated product Runtime parts. Hiding a command is not a backend capability restriction. +- The CLI selects the reviewed `bitfun-core` owner-feature closure + (`agent-runtime`, `canvas-runtime`, `external-sources`, `plugin-runtime`, and + `ssh-remote`). Do not replace it with `product-full` or a CLI-named umbrella; + add a Core feature only when a production CLI path consumes that owner. - CLI consumes typed external-source summaries and actions. It does not parse source files, import executable modules, start plugin workers, duplicate approval state, or treat static discovery as runtime availability. diff --git a/src/apps/cli/Cargo.toml b/src/apps/cli/Cargo.toml index 5e76d7b063..f7e9c3129e 100644 --- a/src/apps/cli/Cargo.toml +++ b/src/apps/cli/Cargo.toml @@ -30,7 +30,13 @@ path = "tests/terminal_process_contracts.rs" [dependencies] # Internal crates -bitfun-core = { path = "../../crates/assembly/core", default-features = false, features = ["product-full"] } +bitfun-core = { path = "../../crates/assembly/core", default-features = false, features = [ + "agent-runtime", + "canvas-runtime", + "external-sources", + "plugin-runtime", + "ssh-remote", +] } bitfun-events = { path = "../../crates/contracts/events" } bitfun-acp = { path = "../../crates/interfaces/acp" } bitfun-agent-runtime = { path = "../../crates/execution/agent-runtime" } diff --git a/src/crates/assembly/core/AGENTS-CN.md b/src/crates/assembly/core/AGENTS-CN.md index a4eac10737..b9b1b3ef99 100644 --- a/src/crates/assembly/core/AGENTS-CN.md +++ b/src/crates/assembly/core/AGENTS-CN.md @@ -17,7 +17,7 @@ - `src/agentic/`:agents、prompts、tools、sessions、execution、persistence - `src/service/`:config、filesystem、terminal、git、LSP、MCP、remote connect、AI memory - `src/infrastructure/`:AI clients、app paths、event system、storage、debug log server -- `src/product_runtime/`:product-full 兼容 adapter 与 runtime service provider wiring +- `src/product_runtime/`:Core Agent Runtime 兼容 adapter 与 runtime service provider wiring Agent 运行时心智模型: @@ -45,11 +45,17 @@ SessionManager -> Session -> DialogTurn -> ModelRound - Product-domain 改动可以在有等价保护时迁移纯产品领域计划;filesystem writes、worker/host side effect、 Git/AI concrete calls、marker IO 和 path-manager integration 仍留在 core,除非有经过评审的 owner 设计。 - `plugin_source` 只注入产品目录并保留兼容接口;受管插件包发现与信任持久化归 `services-integrations`,生态适配解析与 `PluginRuntimeClient` 行为分别归对应的适配器层和执行层。 -- `plugin_runtime` 与 `external_sources` 是经过评审、可分别为对应能力契约选择生态适配器的 `product-full` 组装文件。 - 产品入口只消费产品级视图,不得导入适配器或原始插件运行时 client 类型。 +- `plugin_runtime`、`external_sources` 与 `instruction_sources` 是经过评审、可分别为对应能力契约选择生态适配器的 + owner-feature 组装文件。产品入口只消费产品级视图,不得导入适配器或原始插件运行时 client 类型。 - Remote/service 改动必须保持 external protocol lifecycle、workspace projection、scheduler/session restore、 terminal pre-warm 和 product execution 边界清晰。 - Feature 改动必须保持 `product-full` 作为兼容产品组装边界;默认能力选择只有在单独的 product matrix review 后才能变化。 +- `agent-runtime` 负责现有 Core Agent Runtime 兼容 facade,包括 MCP、Remote Connect、 + workspace-search 与原生 Hook runtime service;`external-sources` 增加第三方发现/导入 adapter, + `plugin-runtime` 增加可执行 plugin client wiring,`debug-log` 单独控制调试日志服务。它们都不得启用 `product-full`。 +- CLI/ACP 的闭包检查遵循 Cargo resolver-v2,保持 normal 与 host(build/proc-macro)feature context 相互隔离; + 但同一 context 内的所有 target-specific 声明都属于同一个已评审架构边界。平台确实需要不同 owner 时,应拆分清晰的 + package/module 归属;不得用互斥 Cargo `cfg` 隐藏未评审的 Core 能力。 - 保持轻量兼容 feature 可独立编译。本地服务 profile 为 `dispatch-store`、`lsp`、`terminal`、 `workspace-runtime` 和 `workspace-watch`;`remote-workspace` 只增加远程工作区 facade, `ssh-remote` 才增加具体 SSH transport。`announcement`、`file-watch`、`git`、 diff --git a/src/crates/assembly/core/AGENTS.md b/src/crates/assembly/core/AGENTS.md index 225e154b4a..380a4d7b30 100644 --- a/src/crates/assembly/core/AGENTS.md +++ b/src/crates/assembly/core/AGENTS.md @@ -19,7 +19,7 @@ Main areas: - `src/agentic/`: agents, prompts, tools, sessions, execution, persistence - `src/service/`: config, filesystem, terminal, git, LSP, MCP, remote connect, AI memory - `src/infrastructure/`: AI clients, app paths, event system, storage, debug log server -- `src/product_runtime/`: product-full compatibility adapters and runtime service provider wiring +- `src/product_runtime/`: Core Agent Runtime compatibility adapters and runtime service provider wiring Agent runtime mental model: @@ -65,10 +65,11 @@ SessionManager -> Session -> DialogTurn -> ModelRound concrete managed-package discovery and trust persistence stay in `services-integrations`, while ecosystem parsing and PluginRuntimeClient behavior remain in their adapter and execution owners. -- `plugin_runtime`, `external_sources`, and `instruction_sources` are the reviewed product-full - composition files allowed to select ecosystem adapters for their respective - capability contracts. Product surfaces consume product-level views and must - not import adapter or raw plugin runtime client types. +- `plugin_runtime`, `external_sources`, and `instruction_sources` are the + reviewed owner-feature composition files allowed to select ecosystem adapters + for their respective capability contracts. Product surfaces consume + product-level views and must not import adapter or raw plugin runtime client + types. - External-source Desktop, TUI, Peer, and Server surfaces share the versioned product-domain control DTO and closed generic actions. Capability-specific approvals and conflict choices remain typed owner operations; do not add a @@ -79,6 +80,17 @@ SessionManager -> Session -> DialogTurn -> ModelRound - Feature work must keep `product-full` as the compatibility product assembly boundary unless a separate product matrix review changes default capability selection. +- `agent-runtime` owns the existing Core Agent Runtime compatibility facade, + including its MCP, Remote Connect, workspace-search, and native Hook runtime + services. `external-sources` adds third-party discovery/import adapters, + `plugin-runtime` adds executable plugin-client wiring, and `debug-log` keeps + the debug ingest server separate. None may enable `product-full`. +- CLI/ACP closure checks keep Cargo resolver-v2 normal and host + (build/proc-macro) feature contexts separate, while treating all + target-specific declarations within each context as one reviewed architecture + boundary. Split a package/module owner when platforms genuinely differ; do + not hide an unreviewed Core capability behind mutually exclusive Cargo `cfg` + branches. - Keep the light compatibility features independently compilable. Local service profiles are `dispatch-store`, `lsp`, `terminal`, `workspace-runtime`, and `workspace-watch`; `remote-workspace` adds only the remote workspace facade, diff --git a/src/crates/assembly/core/Cargo.toml b/src/crates/assembly/core/Cargo.toml index 23551b975d..06cfd8d38d 100644 --- a/src/crates/assembly/core/Cargo.toml +++ b/src/crates/assembly/core/Cargo.toml @@ -129,13 +129,40 @@ bitfun-plugin-runtime-client = { path = "../../execution/plugin-runtime-client", bitfun-transport = { path = "../../adapters/transport" } [features] -# Full product runtime feature set. Product crates should depend on this -# explicitly before `bitfun-core` default features are made lighter. +# Full product runtime compatibility set. Product entrypoints with a narrower +# construction path select stable owner features instead of inheriting this +# union. default = ["product-full"] product-full = [ - "ai-adapter-runtime", + "agent-runtime", "canvas-runtime", - "dep:axum", + "external-sources", + "plugin-runtime", + "debug-log", + "announcement", + "file-watch", + "workspace-watch", + "ssh-remote", + "dispatch-store", + "filesystem", + "git", + "lsp", + "local-storage", + "process-runtime", + "remote-workspace", + "review-platform", + "terminal", + "workspace-runtime", + "product-capabilities", + "product-domains", + "runtime-services", + "tool-packs", +] +# Core compatibility facade and concrete product Agent Runtime assembly. This +# owns the existing agent/session/tool lifecycle, not app presentation or +# external ecosystem discovery. +agent-runtime = [ + "ai-adapter-runtime", "dep:bitfun-agent-runtime", "dep:bitfun-agent-content", "dep:bitfun-agent-stream", @@ -147,11 +174,6 @@ product-full = [ "dep:flate2", "dep:fs2", "dep:include_dir", - "dep:bitfun-opencode-adapter", - "dep:bitfun-claude-code-adapter", - "dep:bitfun-codex-adapter", - "dep:bitfun-external-sources", - "dep:bitfun-plugin-runtime-client", "dep:indexmap", "dep:image", "dep:md5", @@ -160,14 +182,20 @@ product-full = [ "dep:rusqlite", "dep:similar", "dep:tokio-tungstenite", - "dep:tower-http", "dep:tool-runtime", - "bitfun-services-integrations/product-full", + "dep:axum", + "bitfun-services-integrations/browser-control", + "bitfun-services-integrations/deep-research", + "bitfun-services-integrations/mcp", + "bitfun-services-integrations/remote-connect", + "bitfun-services-integrations/script-tool-runtime", + "bitfun-services-integrations/web-tools", + "bitfun-services-integrations/workspace-search", + "tokio/rt-multi-thread", "bitfun-services-core/dispatch-workspace", "bitfun-services-core/permission", "bitfun-services-core/runtime-ownership", "bitfun-services-core/session-git", - "dispatch-store", "filesystem", "lsp", "local-storage", @@ -175,17 +203,32 @@ product-full = [ "remote-workspace", "terminal", "workspace-runtime", - "workspace-watch", - "ssh-remote", "product-capabilities", "product-domains", "runtime-services", - "announcement", - "file-watch", "git", "review-platform", "tool-packs", ] +external-sources = [ + "agent-runtime", + "dep:bitfun-opencode-adapter", + "dep:bitfun-claude-code-adapter", + "dep:bitfun-codex-adapter", + "dep:bitfun-external-sources", + "bitfun-services-integrations/hook-import", + "file-watch", + "workspace-watch", +] +plugin-runtime = [ + "external-sources", + "dep:bitfun-plugin-runtime-client", +] +debug-log = [ + "dep:axum", + "dep:tower-http", + "bitfun-services-integrations/debug-log", +] ai-adapter-runtime = [ "dep:bitfun-ai-adapters", "bitfun-ai-adapters/subscription-auth", diff --git a/src/crates/assembly/core/build.rs b/src/crates/assembly/core/build.rs index 89ec0717e9..9f7e551c4c 100644 --- a/src/crates/assembly/core/build.rs +++ b/src/crates/assembly/core/build.rs @@ -1,5 +1,5 @@ fn main() { - if std::env::var_os("CARGO_FEATURE_PRODUCT_FULL").is_some() { + if std::env::var_os("CARGO_FEATURE_AGENT_RUNTIME").is_some() { emit_rerun_if_changed(std::path::Path::new("builtin_skills")); if let Err(e) = build_embedded_builtin_skills_metadata() { eprintln!("Warning: Failed to embed built-in skills metadata: {}", e); diff --git a/src/crates/assembly/core/src/agentic/execution/conditional_instructions.rs b/src/crates/assembly/core/src/agentic/execution/conditional_instructions.rs index d020f279b4..fc957251f5 100644 --- a/src/crates/assembly/core/src/agentic/execution/conditional_instructions.rs +++ b/src/crates/assembly/core/src/agentic/execution/conditional_instructions.rs @@ -217,12 +217,12 @@ fn normalize_relative_read_path(path: &str) -> Option { #[cfg(test)] mod tests { - use super::{ - build_conditional_instruction_reminder, successful_workspace_read_paths, - ConditionalInstructionCatalog, - }; + #[cfg(feature = "external-sources")] + use super::build_conditional_instruction_reminder; + use super::{successful_workspace_read_paths, ConditionalInstructionCatalog}; use crate::agentic::core::{Message, MessageContent}; use crate::agentic::WorkspaceBinding; + #[cfg(feature = "external-sources")] use crate::instruction_sources::test_support::{lock_environment, EnvironmentGuard}; use bitfun_services_core::workspace_instructions::WorkspaceInstructionFile; use serde_json::json; @@ -320,6 +320,7 @@ mod tests { ); } + #[cfg(feature = "external-sources")] #[tokio::test] async fn an_unmatched_read_does_not_freeze_rule_content_before_activation() { let _environment = lock_environment(); diff --git a/src/crates/assembly/core/src/agentic/execution/execution_engine.rs b/src/crates/assembly/core/src/agentic/execution/execution_engine.rs index c0524919ba..9e60a91eaa 100644 --- a/src/crates/assembly/core/src/agentic/execution/execution_engine.rs +++ b/src/crates/assembly/core/src/agentic/execution/execution_engine.rs @@ -18,7 +18,7 @@ use crate::agentic::core::{ MessageRole, MessageSemanticKind, RequestReasoningTokenPolicy, Session, }; use crate::agentic::events::{AgenticEvent, EventPriority, EventQueue}; -#[cfg(feature = "product-full")] +#[cfg(feature = "agent-runtime")] use crate::agentic::execution::conditional_instructions::{ build_conditional_instruction_reminder, successful_workspace_read_paths, }; @@ -152,7 +152,7 @@ fn manual_compaction_terminal_error(error: BitFunError) -> BitFunError { } } -#[cfg(feature = "product-full")] +#[cfg(feature = "agent-runtime")] async fn activate_conditional_instructions_after_round( session_manager: &SessionManager, context: &ExecutionContext, @@ -3856,7 +3856,7 @@ impl ExecutionEngine { } } - #[cfg(feature = "product-full")] + #[cfg(feature = "agent-runtime")] activate_conditional_instructions_after_round( self.session_manager.as_ref(), &context, @@ -4453,7 +4453,7 @@ impl ExecutionEngine { // successfully, renumber `cit_XXX` references in the final report // into consecutive `[N]` display IDs. Two gates apply (agent type + // dialog success) so other agents and failed turns are unaffected. - #[cfg(feature = "product-full")] + #[cfg(feature = "agent-runtime")] { if bitfun_agent_runtime::deep_research::should_post_process_research_report( &agent_type, @@ -4602,6 +4602,7 @@ mod tests { use crate::agentic::tools::ToolRuntimeRestrictions; use crate::agentic::workspace::{local_workspace_services, WorkspaceBinding}; use crate::infrastructure::PathManager; + #[cfg(feature = "external-sources")] use crate::instruction_sources::test_support::{lock_environment, EnvironmentGuard}; use crate::service::config::types::AIConfig; use crate::service::config::types::AIModelConfig; @@ -4819,6 +4820,7 @@ mod tests { .contains("Recovered workspace instructions.")); } + #[cfg(feature = "external-sources")] #[tokio::test] async fn local_workspace_services_still_include_local_user_instruction_sources() { let _environment = lock_environment(); @@ -4863,6 +4865,7 @@ mod tests { assert!(context.contains("Local engine project")); } + #[cfg(feature = "external-sources")] #[tokio::test] async fn local_workspace_services_remain_the_project_instruction_io_owner() { let _environment = lock_environment(); @@ -4909,6 +4912,7 @@ mod tests { assert!(!context.contains("Disk project source")); } + #[cfg(feature = "external-sources")] #[tokio::test] async fn conditional_rules_persist_once_and_reload_after_compaction() { let _environment = lock_environment(); diff --git a/src/crates/assembly/core/src/agentic/execution/mod.rs b/src/crates/assembly/core/src/agentic/execution/mod.rs index 3987dc2672..a381fccad0 100644 --- a/src/crates/assembly/core/src/agentic/execution/mod.rs +++ b/src/crates/assembly/core/src/agentic/execution/mod.rs @@ -2,7 +2,7 @@ //! //! Responsible for AI interaction and model round control -#[cfg(feature = "product-full")] +#[cfg(feature = "agent-runtime")] pub(crate) mod conditional_instructions; pub mod edit_constraint_guard; pub mod execution_engine; diff --git a/src/crates/assembly/core/src/agentic/tools/implementations/skills/registry.rs b/src/crates/assembly/core/src/agentic/tools/implementations/skills/registry.rs index 7897a562cf..f4563e5adb 100644 --- a/src/crates/assembly/core/src/agentic/tools/implementations/skills/registry.rs +++ b/src/crates/assembly/core/src/agentic/tools/implementations/skills/registry.rs @@ -11,7 +11,7 @@ use super::mode_overrides::{ use super::source_cache::{LocalSkillWatchMonitor, LocalSkillWatchRoot, VersionedSnapshotCache}; use super::types::{ModeSkillInfo, SkillData, SkillInfo, SkillLocation}; use crate::agentic::workspace::WorkspaceFileSystem; -#[cfg(feature = "product-full")] +#[cfg(feature = "external-sources")] use crate::external_sources::{ opencode_configured_skill_roots, LocalConfiguredSkillRootContribution, }; @@ -29,29 +29,29 @@ use bitfun_agent_runtime::skills::{ USER_HOME_SKILL_ROOTS, USER_SKILL_KEY_PREFIX, }; use bitfun_services_core::bounded_fs::is_symlink_or_reparse; -#[cfg(feature = "product-full")] +#[cfg(feature = "external-sources")] use bitfun_services_core::bounded_fs::{collect_bounded_regular_files, BoundedDirectoryWalkLimits}; -#[cfg(feature = "product-full")] +#[cfg(feature = "external-sources")] use bitfun_services_core::bounded_fs::{read_bounded_text, BoundedTextRead}; -#[cfg(feature = "product-full")] +#[cfg(feature = "external-sources")] use bitfun_services_core::workspace_text::read_workspace_relative_text_bounded; use log::{debug, error, warn}; -#[cfg(feature = "product-full")] +#[cfg(feature = "external-sources")] use sha2::{Digest, Sha256}; use std::collections::HashSet; use std::path::{Path, PathBuf}; use std::sync::OnceLock; use tokio::fs; -#[cfg(feature = "product-full")] +#[cfg(feature = "external-sources")] const MAX_OPENCODE_CONFIGURED_SKILL_ROOTS: usize = 64; -#[cfg(feature = "product-full")] +#[cfg(feature = "external-sources")] const MAX_OPENCODE_CONFIGURED_SKILLS_PER_ROOT: usize = 512; -#[cfg(feature = "product-full")] +#[cfg(feature = "external-sources")] const MAX_OPENCODE_CONFIGURED_SKILL_BYTES: usize = 256 * 1024; -#[cfg(feature = "product-full")] +#[cfg(feature = "external-sources")] const MAX_OPENCODE_CONFIGURED_POLICY_BYTES: usize = 64 * 1024; -#[cfg(feature = "product-full")] +#[cfg(feature = "external-sources")] const OPENCODE_CONFIGURED_PRIORITY_BAND: usize = MAX_OPENCODE_CONFIGURED_SKILL_ROOTS * MAX_OPENCODE_CONFIGURED_SKILLS_PER_ROOT; @@ -269,7 +269,7 @@ fn sort_remote_dir_entries(entries: &mut [crate::agentic::workspace::WorkspaceDi }); } -#[cfg(feature = "product-full")] +#[cfg(feature = "external-sources")] fn configured_opencode_source_slot(skill_dir: &Path) -> String { let mut hasher = Sha256::new(); hasher.update(skill_dir.to_string_lossy().as_bytes()); @@ -277,18 +277,18 @@ fn configured_opencode_source_slot(skill_dir: &Path) -> String { format!("config.opencode.{}", &digest[..16]) } -#[cfg(feature = "product-full")] +#[cfg(feature = "external-sources")] fn canonical_candidate_path(candidate: &SkillCandidate) -> PathBuf { dunce::canonicalize(&candidate.info.path) .unwrap_or_else(|_| PathBuf::from(&candidate.info.path)) } -#[cfg(feature = "product-full")] +#[cfg(feature = "external-sources")] fn is_configured_opencode_source_slot(source_slot: &str) -> bool { source_slot.starts_with("config.opencode.") } -#[cfg(feature = "product-full")] +#[cfg(feature = "external-sources")] fn validate_configured_opencode_skill_root( skill_dir: &Path, expected_source_slot: &str, @@ -399,7 +399,7 @@ impl SkillRegistry { cacheable } - #[cfg(feature = "product-full")] + #[cfg(feature = "external-sources")] async fn apply_configured_opencode_policy( skill_data: &mut SkillData, skill_dir: &Path, @@ -445,7 +445,7 @@ impl SkillRegistry { } async fn read_local_skill_markdown(info: &SkillInfo) -> BitFunResult { - #[cfg(feature = "product-full")] + #[cfg(feature = "external-sources")] if is_configured_opencode_source_slot(&info.source_slot) { let skill_dir = validate_configured_opencode_skill_root(Path::new(&info.path), &info.source_slot) @@ -870,7 +870,7 @@ impl SkillRegistry { standard.append(&mut user_sources.standard); } - #[cfg(feature = "product-full")] + #[cfg(feature = "external-sources")] { // OpenCode configured roots are workspace-sensitive: an absolute path // from user config may become project-scoped for the current workspace. @@ -894,11 +894,11 @@ impl SkillRegistry { ); } - #[cfg(not(feature = "product-full"))] + #[cfg(not(feature = "external-sources"))] standard } - #[cfg(feature = "product-full")] + #[cfg(feature = "external-sources")] fn merge_configured_opencode_candidates( mut standard: Vec, mut configured: Vec, @@ -955,7 +955,7 @@ impl SkillRegistry { standard } - #[cfg(feature = "product-full")] + #[cfg(feature = "external-sources")] async fn scan_configured_opencode_candidates( roots: Vec, ) -> Vec { @@ -1742,7 +1742,7 @@ impl SkillRegistry { } } -#[cfg(all(test, feature = "product-full"))] +#[cfg(all(test, feature = "external-sources"))] mod opencode_configured_skill_tests { use super::{SkillRegistry, SkillRootEntry}; use crate::external_sources::LocalConfiguredSkillRootContribution; diff --git a/src/crates/assembly/core/src/agentic/tools/pipeline/tool_pipeline.rs b/src/crates/assembly/core/src/agentic/tools/pipeline/tool_pipeline.rs index 4719a5a7aa..4c9a073742 100644 --- a/src/crates/assembly/core/src/agentic/tools/pipeline/tool_pipeline.rs +++ b/src/crates/assembly/core/src/agentic/tools/pipeline/tool_pipeline.rs @@ -39,6 +39,7 @@ use bitfun_runtime_ports::{ use futures::future::join_all; use log::{debug, error, info, warn}; use std::collections::{HashMap, HashSet}; +use std::path::Path; use std::sync::Arc; use std::time::{Instant, SystemTime}; use tokio::sync::{Mutex as TokioMutex, RwLock as TokioRwLock}; @@ -50,6 +51,25 @@ use tool_runtime::pipeline::{ ToolCancellationTokenStore, ToolExecutionErrorClass, ToolRetryAttemptFacts, }; +fn resolve_contextual_tool( + tool: Arc, + workspace_root: Option<&Path>, + remote: bool, +) -> Option> { + #[cfg(feature = "external-sources")] + { + return crate::external_tools::resolve_external_tool_for_workspace( + tool, + crate::external_tools::external_tool_route_root(workspace_root, remote), + ); + } + #[cfg(not(feature = "external-sources"))] + { + let _ = (workspace_root, remote); + Some(tool) + } +} + fn persisted_effective_tool_name( wire_tool_name: &str, effective_tool_name: &str, @@ -1419,21 +1439,19 @@ impl ToolPipeline { if resolution_error.is_some() { return false; } - let route_root = crate::external_tools::external_tool_route_root( - context - .workspace - .as_ref() - .map(|workspace| workspace.root_path()), - context - .workspace - .as_ref() - .is_some_and(|workspace| workspace.is_remote()), - ); let tool_is_concurrency_safe = registry .get_tool(&invocation.effective_tool_name) .and_then(|tool| { - crate::external_tools::resolve_external_tool_for_workspace( - tool, route_root, + resolve_contextual_tool( + tool, + context + .workspace + .as_ref() + .map(|workspace| workspace.root_path()), + context + .workspace + .as_ref() + .is_some_and(|workspace| workspace.is_remote()), ) }) .map(|tool| tool.is_concurrency_safe(Some(&invocation.effective_arguments))) @@ -2209,12 +2227,10 @@ impl ToolPipeline { let execution_future = tool.call(task.effective_arguments(), &tool_context); - let timeout_owner = crate::external_tools::resolve_external_tool_for_workspace( + let timeout_owner = resolve_contextual_tool( Arc::clone(&tool), - crate::external_tools::external_tool_route_root( - tool_context.workspace_root(), - tool_context.is_remote(), - ), + tool_context.workspace_root(), + tool_context.is_remote(), ); let pipeline_timeout_secs = if timeout_owner .as_ref() @@ -2465,6 +2481,7 @@ mod tests { }; use serde_json::json; use std::collections::HashMap; + #[cfg(feature = "external-sources")] use std::path::PathBuf; use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::{Arc, Mutex}; @@ -2998,6 +3015,7 @@ mod tests { ) } + #[cfg(feature = "external-sources")] #[test] fn remote_workspace_route_root_isolated_from_same_local_path() { let pipeline = test_tool_pipeline(); diff --git a/src/crates/assembly/core/src/agentic/tools/product_runtime.rs b/src/crates/assembly/core/src/agentic/tools/product_runtime.rs index cb0c0ad498..b6e3a22057 100644 --- a/src/crates/assembly/core/src/agentic/tools/product_runtime.rs +++ b/src/crates/assembly/core/src/agentic/tools/product_runtime.rs @@ -140,6 +140,22 @@ mod tests { ); } + #[test] + fn product_tool_runtime_can_consume_acp_product_assembly_plan() { + let runtime = ProductToolRuntime::for_profile(DeliveryProfile::Acp); + let owner_registry = runtime.create_registry(); + let compatibility_registry = create_tool_registry(); + + assert_eq!( + owner_registry.get_tool_names(), + compatibility_registry.get_tool_names() + ); + assert_eq!( + owner_registry.get_deferred_tool_names(), + compatibility_registry.get_deferred_tool_names() + ); + } + #[test] fn sdk_and_cli_profiles_current_tool_plan_ceilings_match_without_sharing_identity() { let sdk = ProductToolRuntime::for_profile(DeliveryProfile::Sdk).create_registry(); diff --git a/src/crates/assembly/core/src/agentic/tools/product_runtime/catalog.rs b/src/crates/assembly/core/src/agentic/tools/product_runtime/catalog.rs index 5b3e615fe3..c68e55a5f9 100644 --- a/src/crates/assembly/core/src/agentic/tools/product_runtime/catalog.rs +++ b/src/crates/assembly/core/src/agentic/tools/product_runtime/catalog.rs @@ -283,22 +283,32 @@ pub(crate) async fn resolve_product_tool_manifest( } async fn contextual_tool_snapshot(context: &ToolUseContext) -> Vec { - if !context.is_remote() { - crate::external_sources::ensure_external_source_workspace_runtime(context.workspace_root()) + #[cfg(feature = "external-sources")] + { + if !context.is_remote() { + crate::external_sources::ensure_external_source_workspace_runtime( + context.workspace_root(), + ) .await; + } + let registry = get_global_tool_registry(); + let tools = registry.read().await.get_all_tools(); + let route_root = crate::external_tools::external_tool_route_root( + context.workspace_root(), + context.is_remote(), + ); + return tools + .into_iter() + .filter_map(|tool| { + crate::external_tools::resolve_external_tool_for_workspace(tool, route_root) + }) + .collect(); + } + #[cfg(not(feature = "external-sources"))] + { + let _ = context; + get_global_tool_registry().read().await.get_all_tools() } - let route_root = crate::external_tools::external_tool_route_root( - context.workspace_root(), - context.is_remote(), - ); - let registry = get_global_tool_registry(); - let tools = registry.read().await.get_all_tools(); - tools - .into_iter() - .filter_map(|tool| { - crate::external_tools::resolve_external_tool_for_workspace(tool, route_root) - }) - .collect() } pub(crate) async fn resolve_product_resolved_visible_tools( @@ -358,6 +368,7 @@ mod tests { use crate::agentic::tools::registry::create_tool_registry; use crate::agentic::tools::tool_context_runtime::ToolUseContext; use crate::agentic::tools::ToolRuntimeRestrictions; + #[cfg(feature = "external-sources")] use crate::agentic::WorkspaceBinding; use bitfun_agent_tools::{ GetToolSpecCatalogProvider, ToolCatalogSnapshotProvider, CALL_DEFERRED_TOOL_NAME, @@ -365,6 +376,7 @@ mod tests { }; use serde_json::{json, Value}; use std::collections::HashMap; + #[cfg(feature = "external-sources")] use std::path::PathBuf; use std::sync::Arc; @@ -446,6 +458,7 @@ mod tests { tool_context(None) } + #[cfg(feature = "external-sources")] #[test] fn remote_workspace_route_root_isolated_from_same_local_path() { let root = std::env::current_dir().expect("absolute test workspace root"); diff --git a/src/crates/assembly/core/src/agentic/tools/registry.rs b/src/crates/assembly/core/src/agentic/tools/registry.rs index 4bc1e3a696..a8ddd76ffd 100644 --- a/src/crates/assembly/core/src/agentic/tools/registry.rs +++ b/src/crates/assembly/core/src/agentic/tools/registry.rs @@ -16,6 +16,46 @@ use std::sync::Arc; pub(in crate::agentic::tools) type ToolRef = Arc; pub(in crate::agentic::tools) type ProductToolDecoratorRef = ToolDecoratorRef; +fn route_external_tool_registration(tool: ToolRef) -> ToolRef { + #[cfg(feature = "external-sources")] + { + return crate::external_tools::intercept_external_tool_registry_registration(tool); + } + #[cfg(not(feature = "external-sources"))] + { + tool + } +} + +fn detach_external_mcp_routes(server_id: &str) -> Vec { + #[cfg(feature = "external-sources")] + { + return crate::external_tools::detach_external_tool_mcp_server(server_id); + } + #[cfg(not(feature = "external-sources"))] + { + let _ = server_id; + Vec::new() + } +} + +fn retain_external_tool_muxes(prefix: &str) -> Vec { + #[cfg(feature = "external-sources")] + { + return crate::external_tools::retain_external_tool_muxes_for_prefix(prefix); + } + #[cfg(not(feature = "external-sources"))] + { + let _ = prefix; + Vec::new() + } +} + +fn notify_external_tool_registry_changed() { + #[cfg(feature = "external-sources")] + crate::external_sources::notify_external_tool_registry_changed(); +} + pub use bitfun_agent_tools::GET_TOOL_SPEC_TOOL_NAME; /// Tool registry - manages all available tools (using IndexMap to maintain registration order) @@ -77,13 +117,13 @@ impl ToolRegistry { ); } - let routed = crate::external_tools::intercept_external_tool_registry_registration(tool); + let routed = route_external_tool_registration(tool); self.inner.register_tool(routed); debug!("MCP tool registered: tool_name={}", name); } if tool_count > 0 { - crate::external_sources::notify_external_tool_registry_changed(); + notify_external_tool_registry_changed(); } let after_count = self.get_tool_names().len(); @@ -97,8 +137,7 @@ impl ToolRegistry { /// Remove all tools from the MCP server pub fn unregister_mcp_server_tools(&mut self, server_id: &str) { - let retained_external_routes = - crate::external_tools::detach_external_tool_mcp_server(server_id); + let retained_external_routes = detach_external_mcp_routes(server_id); let removed_tool_names = self .get_tool_names() .into_iter() @@ -116,7 +155,7 @@ impl ToolRegistry { } if !retained_external_routes.is_empty() { - crate::external_sources::notify_external_tool_registry_changed(); + notify_external_tool_registry_changed(); } for key in removed_tool_names { @@ -126,7 +165,7 @@ impl ToolRegistry { /// Remove all tools whose registry name starts with the given prefix. pub fn unregister_tools_by_prefix(&mut self, prefix: &str) -> usize { - let retained_muxes = crate::external_tools::retain_external_tool_muxes_for_prefix(prefix); + let retained_muxes = retain_external_tool_muxes(prefix); let retained_names = retained_muxes .iter() .map(|tool| tool.name().to_string()) @@ -149,7 +188,7 @@ impl ToolRegistry { } if !retained_muxes.is_empty() { - crate::external_sources::notify_external_tool_registry_changed(); + notify_external_tool_registry_changed(); } for key in removed_tool_names { @@ -168,10 +207,10 @@ impl ToolRegistry { /// Register a single tool pub fn register_tool(&mut self, tool: ToolRef) { let is_router = tool.dynamic_provider_id() == Some("external-source-router"); - let routed = crate::external_tools::intercept_external_tool_registry_registration(tool); + let routed = route_external_tool_registration(tool); self.inner.register_tool(routed); if !is_router { - crate::external_sources::notify_external_tool_registry_changed(); + notify_external_tool_registry_changed(); } } diff --git a/src/crates/assembly/core/src/agentic/workspace.rs b/src/crates/assembly/core/src/agentic/workspace.rs index 7bb17bbae9..7136addedd 100644 --- a/src/crates/assembly/core/src/agentic/workspace.rs +++ b/src/crates/assembly/core/src/agentic/workspace.rs @@ -13,6 +13,18 @@ pub use bitfun_services_integrations::remote_ssh::{ }; use std::path::{Path, PathBuf}; +/// Stable local workspace identity shared by per-workspace runtime routers. +pub(crate) fn workspace_route_key(workspace_root: Option<&Path>) -> String { + workspace_root + .map(|path| { + dunce::canonicalize(path) + .unwrap_or_else(|_| path.to_path_buf()) + .to_string_lossy() + .into_owned() + }) + .unwrap_or_else(|| "".to_string()) +} + /// Describes whether the workspace is local or remote via SSH. #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum WorkspaceBackend { diff --git a/src/crates/assembly/core/src/external_sources.rs b/src/crates/assembly/core/src/external_sources.rs index 47f3cae499..74d5ba6f43 100644 --- a/src/crates/assembly/core/src/external_sources.rs +++ b/src/crates/assembly/core/src/external_sources.rs @@ -40,6 +40,7 @@ pub use bitfun_product_domains::external_subagents::{ ExternalSubagentModelRequest, ExternalSubagentSummary, }; +use crate::agentic::workspace::workspace_route_key; use crate::external_mcp::{ reconcile_external_mcp_catalog, BitFunExternalMcpRuntime, ExternalMcpDecision, ExternalMcpDecisions, ExternalMcpProductState, ExternalMcpRuntimePort, @@ -53,8 +54,8 @@ use crate::external_tools::{ begin_external_tool_workspace_recovery, external_tool_workspace_requires_recovery, invalidate_external_tool_runtime_availability, merge_tool_state, project_external_tools_read_only, reconcile_external_tools, release_external_tool_workspace, - reset_external_tool_workspace_recovery_budget, workspace_route_key, ExternalToolDecisions, - ExternalToolProductState, TOOL_CONFLICT_RESELECTION_REQUIRED, UNRESOLVED_TOOL_CONFLICT_CHOICE, + reset_external_tool_workspace_recovery_budget, ExternalToolDecisions, ExternalToolProductState, + TOOL_CONFLICT_RESELECTION_REQUIRED, UNRESOLVED_TOOL_CONFLICT_CHOICE, }; use crate::service::config::{subscribe_config_updates, ConfigUpdateEvent}; use bitfun_claude_code_adapter::{ diff --git a/src/crates/assembly/core/src/external_subagents.rs b/src/crates/assembly/core/src/external_subagents.rs index 448ffdfa9b..5a0c882bf4 100644 --- a/src/crates/assembly/core/src/external_subagents.rs +++ b/src/crates/assembly/core/src/external_subagents.rs @@ -11,8 +11,9 @@ use crate::agentic::agents::{ ExternalSubagentRoute, }; use crate::agentic::tools::registry::get_all_registered_tools; +use crate::agentic::workspace::workspace_route_key; use crate::external_sources::safe_external_source_location; -use crate::external_tools::{resolve_external_tool_for_workspace, workspace_route_key}; +use crate::external_tools::resolve_external_tool_for_workspace; use crate::service::config::global::GlobalConfigManager; use crate::service::config::types::{model_runtime_binding_fingerprint, AIConfig, AIModelConfig}; use crate::service::config::SubagentModelSelection; diff --git a/src/crates/assembly/core/src/external_tools.rs b/src/crates/assembly/core/src/external_tools.rs index df8b25ed6a..97a60cedc8 100644 --- a/src/crates/assembly/core/src/external_tools.rs +++ b/src/crates/assembly/core/src/external_tools.rs @@ -5,6 +5,7 @@ use crate::agentic::tools::framework::{ ValidationResult, }; use crate::agentic::tools::registry::get_global_tool_registry; +use crate::agentic::workspace::workspace_route_key; use crate::util::errors::{BitFunError, BitFunResult}; use async_trait::async_trait; use bitfun_external_sources::{ExternalSourceControlPlane, ExternalToolCoordinatorSnapshot}; @@ -1004,7 +1005,8 @@ impl ExternalToolRuntimeManager { ) -> bool { let mut loaded = self.loaded.lock().await; if loaded - .get(runtime_target_id).is_none_or(|target| target.load_generation != load_generation) + .get(runtime_target_id) + .is_none_or(|target| target.load_generation != load_generation) { return false; } @@ -2036,17 +2038,6 @@ async fn local_candidate(tool: &Arc) -> ExternalToolConflictCandidate } } -pub(crate) fn workspace_route_key(workspace_root: Option<&Path>) -> String { - workspace_root - .map(|path| { - dunce::canonicalize(path) - .unwrap_or_else(|_| path.to_path_buf()) - .to_string_lossy() - .into_owned() - }) - .unwrap_or_else(|| "".to_string()) -} - fn workspace_conflict_domain(execution_domain_id: &str, workspace_key: &str) -> String { let mut hasher = Sha256::new(); hasher.update(workspace_key.as_bytes()); diff --git a/src/crates/assembly/core/src/infrastructure/mod.rs b/src/crates/assembly/core/src/infrastructure/mod.rs index 55abf59458..d3ca11ed52 100644 --- a/src/crates/assembly/core/src/infrastructure/mod.rs +++ b/src/crates/assembly/core/src/infrastructure/mod.rs @@ -5,7 +5,7 @@ #[cfg(feature = "ai-adapter-runtime")] pub mod ai; pub mod app_paths; -#[cfg(feature = "product-full")] +#[cfg(feature = "debug-log")] pub mod debug_log; pub mod events; #[cfg(feature = "filesystem")] diff --git a/src/crates/assembly/core/src/instruction_sources.rs b/src/crates/assembly/core/src/instruction_sources.rs index 9055ad93b0..ffbf6650c4 100644 --- a/src/crates/assembly/core/src/instruction_sources.rs +++ b/src/crates/assembly/core/src/instruction_sources.rs @@ -1,4 +1,4 @@ -//! Product-full composition for local user instruction source adapters. +//! External-source composition for local user instruction adapters. use bitfun_claude_code_adapter::{ load_claude_code_user_instructions, ClaudeCodeInstructionSourceOptions, diff --git a/src/crates/assembly/core/src/lib.rs b/src/crates/assembly/core/src/lib.rs index d16aa81dd2..201c12d7bc 100644 --- a/src/crates/assembly/core/src/lib.rs +++ b/src/crates/assembly/core/src/lib.rs @@ -5,60 +5,60 @@ //! New implementation code should live in owner crates under `src/crates/*`. //! This crate re-exports legacy paths and wires the full BitFun product runtime. -#[cfg(feature = "product-full")] +#[cfg(feature = "agent-runtime")] pub mod agentic; // Agent system, tool system, and product runtime orchestration -#[cfg(feature = "product-full")] +#[cfg(feature = "external-sources")] pub mod external_hook_import; -#[cfg(feature = "product-full")] +#[cfg(feature = "external-sources")] pub mod external_hooks; -#[cfg(all(test, feature = "product-full"))] +#[cfg(all(test, feature = "external-sources"))] mod external_hooks_tests; -#[cfg(feature = "product-full")] +#[cfg(feature = "external-sources")] mod external_mcp; -#[cfg(feature = "product-full")] +#[cfg(feature = "external-sources")] pub mod external_mcp_import; -#[cfg(all(test, feature = "product-full"))] +#[cfg(all(test, feature = "external-sources"))] mod external_mcp_tests; -#[cfg(feature = "product-full")] +#[cfg(feature = "external-sources")] pub mod external_sources; -#[cfg(feature = "product-full")] +#[cfg(feature = "external-sources")] mod external_subagents; -#[cfg(feature = "product-full")] +#[cfg(feature = "external-sources")] mod external_tools; #[cfg(feature = "product-domains")] pub mod function_agents; // Function-based agents pub mod infrastructure; // AI clients, storage, logging, events -#[cfg(feature = "product-full")] +#[cfg(feature = "external-sources")] mod instruction_sources; #[cfg(feature = "product-domains")] pub mod miniapp; // AI-generated instant apps (Zero-Dialect Runtime) -#[cfg(feature = "product-full")] +#[cfg(feature = "agent-runtime")] pub mod native_hooks; -#[cfg(all(test, feature = "product-full"))] +#[cfg(all(test, feature = "agent-runtime"))] mod native_hooks_tests; -#[cfg(feature = "product-full")] +#[cfg(feature = "plugin-runtime")] pub mod plugin_runtime; #[cfg(any(feature = "plugin-source", feature = "product-domains"))] pub mod plugin_source; -#[cfg(feature = "product-full")] +#[cfg(feature = "agent-runtime")] pub mod product_assembly; -#[cfg(all(test, feature = "product-full"))] +#[cfg(all(test, feature = "agent-runtime"))] mod product_assembly_tests; #[cfg(feature = "product-domains")] pub(crate) mod product_domain_runtime; -#[cfg(feature = "product-full")] +#[cfg(feature = "agent-runtime")] pub mod product_runtime; -#[cfg(feature = "product-full")] +#[cfg(feature = "agent-runtime")] pub mod runtime_ownership; -#[cfg(all(test, feature = "product-full"))] +#[cfg(all(test, feature = "agent-runtime"))] mod runtime_ownership_tests; pub mod service; // Workspace, Config, FileSystem, Terminal, Git -#[cfg(feature = "product-full")] +#[cfg(feature = "agent-runtime")] pub(crate) mod service_agent_runtime; pub mod util; // General types, errors, helper functions // Re-export debug_log from infrastructure for backward compatibility. -#[cfg(feature = "product-full")] +#[cfg(feature = "debug-log")] pub use infrastructure::debug_log as debug; // Export main types @@ -78,7 +78,7 @@ pub use infrastructure::ai::AIClient; pub use infrastructure::events::BackendEventManager; // Export Agentic service core types -#[cfg(feature = "product-full")] +#[cfg(feature = "agent-runtime")] pub use agentic::{ core::{Message, Session}, // NOTE: agentic::core::DialogTurn / ModelRound used to be re-exported here @@ -91,7 +91,7 @@ pub use agentic::{ }; // Export ToolRegistry separately. -#[cfg(feature = "product-full")] +#[cfg(feature = "agent-runtime")] pub use agentic::tools::registry::ToolRegistry; // Version information diff --git a/src/crates/assembly/core/src/miniapp/mod.rs b/src/crates/assembly/core/src/miniapp/mod.rs index 1a5eed7c0c..fc2a7bdc74 100644 --- a/src/crates/assembly/core/src/miniapp/mod.rs +++ b/src/crates/assembly/core/src/miniapp/mod.rs @@ -3,7 +3,7 @@ pub mod builtin; pub mod compiler; pub mod exporter; -#[cfg(feature = "product-full")] +#[cfg(feature = "agent-runtime")] pub mod host_dispatch; pub mod js_worker; pub mod js_worker_pool; @@ -21,7 +21,7 @@ pub use bitfun_product_domains::miniapp::{ pub use builtin::{seed_builtin_miniapps, BuiltinApp, BUILTIN_APPS}; pub use exporter::{ExportCheckResult, ExportOptions, ExportResult, ExportTarget, MiniAppExporter}; -#[cfg(feature = "product-full")] +#[cfg(feature = "agent-runtime")] pub use host_dispatch::{dispatch_host, is_host_primitive}; pub use js_worker_pool::{InstallResult, JsWorkerPool}; pub use manager::{ diff --git a/src/crates/assembly/core/src/native_hooks.rs b/src/crates/assembly/core/src/native_hooks.rs index 435356b93a..692e706d61 100644 --- a/src/crates/assembly/core/src/native_hooks.rs +++ b/src/crates/assembly/core/src/native_hooks.rs @@ -549,14 +549,22 @@ async fn engine_for( .iter() .map(|(_, path)| fingerprint(path.clone())) .collect::>(); - let imported_generation = - match crate::external_hook_import::imported_hook_generation(workspace_root).await { - Ok(generation) => generation, - Err(error) => { - warn!("Imported Hook state is unavailable: {error}"); - 0 + let imported_generation = { + #[cfg(feature = "external-sources")] + { + match crate::external_hook_import::imported_hook_generation(workspace_root).await { + Ok(generation) => generation, + Err(error) => { + warn!("Imported Hook state is unavailable: {error}"); + 0 + } } - }; + } + #[cfg(not(feature = "external-sources"))] + { + 0 + } + }; { let cache = engine_cache().lock().await; if let Some(cached) = cache.get(&key) { @@ -571,14 +579,22 @@ async fn engine_for( } } - let imported_layers = - match crate::external_hook_import::enabled_imported_hook_layers(workspace_root).await { - Ok(layers) => layers, - Err(error) => { - warn!("Imported Hook layers are unavailable: {error}"); - Vec::new() + let imported_layers = { + #[cfg(feature = "external-sources")] + { + match crate::external_hook_import::enabled_imported_hook_layers(workspace_root).await { + Ok(layers) => layers, + Err(error) => { + warn!("Imported Hook layers are unavailable: {error}"); + Vec::new() + } } - }; + } + #[cfg(not(feature = "external-sources"))] + { + Vec::new() + } + }; let (manual_layers, skipped) = read_layers(&paths); for message in &skipped { warn!("{message}"); @@ -699,9 +715,16 @@ pub async fn overview(workspace_root: Option<&Path>) -> NativeHookOverview { // actually load, so the view can show a gated-off project file. let config = hooks_config().await; let imported_layers = if config.enabled { - crate::external_hook_import::enabled_imported_hook_layers(workspace_root) - .await - .unwrap_or_default() + #[cfg(feature = "external-sources")] + { + crate::external_hook_import::enabled_imported_hook_layers(workspace_root) + .await + .unwrap_or_default() + } + #[cfg(not(feature = "external-sources"))] + { + Vec::new() + } } else { Vec::new() }; diff --git a/src/crates/assembly/core/src/plugin_runtime.rs b/src/crates/assembly/core/src/plugin_runtime.rs index b06c203f2a..a366d9d394 100644 --- a/src/crates/assembly/core/src/plugin_runtime.rs +++ b/src/crates/assembly/core/src/plugin_runtime.rs @@ -1,6 +1,6 @@ //! Managed plugin composition. //! -//! This is the only product-full root that selects the OpenCode-compatible +//! This is the only plugin-runtime root that selects the OpenCode-compatible //! adapter and PluginRuntimeClient. Workspace identity here scopes source, //! approval, and logical client state; it is not a process ownership boundary. //! This module projects candidates for product surfaces; it does not register diff --git a/src/crates/assembly/core/src/product_runtime.rs b/src/crates/assembly/core/src/product_runtime.rs index 3b01ef44da..b37242b993 100644 --- a/src/crates/assembly/core/src/product_runtime.rs +++ b/src/crates/assembly/core/src/product_runtime.rs @@ -1,4 +1,4 @@ -//! Core product-full runtime adapter boundary. +//! Core Agent Runtime compatibility adapter boundary. //! //! Product runtime assembly facts live in `bitfun-product-capabilities`. Core //! keeps only compatibility exports and adapter wiring that still depends on diff --git a/src/crates/assembly/core/src/product_runtime/runtime_services.rs b/src/crates/assembly/core/src/product_runtime/runtime_services.rs index 7e4b66fdd2..5eb7f33db3 100644 --- a/src/crates/assembly/core/src/product_runtime/runtime_services.rs +++ b/src/crates/assembly/core/src/product_runtime/runtime_services.rs @@ -1,4 +1,4 @@ -//! Core product-full runtime service adapters. +//! Core Agent Runtime service adapters. //! //! This file registers existing core concrete adapters into typed runtime //! service builders. It does not create new runtime behavior. diff --git a/src/crates/assembly/core/src/runtime_ownership_tests.rs b/src/crates/assembly/core/src/runtime_ownership_tests.rs index b50a47eb4a..586a4fef0d 100644 --- a/src/crates/assembly/core/src/runtime_ownership_tests.rs +++ b/src/crates/assembly/core/src/runtime_ownership_tests.rs @@ -6,6 +6,7 @@ use bitfun_services_core::runtime_ownership::{ use tempfile::tempdir; use crate::runtime_ownership::CoreRuntimeOwnership; +#[cfg(feature = "dispatch-store")] use crate::service::dispatch::{DispatchTarget, OutboundDispatchRecord, OutboundDispatchStore}; #[test] @@ -167,6 +168,7 @@ fn ssh_host_without_connection_id_cannot_bypass_local_ownership() { drop(shared); } +#[cfg(feature = "dispatch-store")] #[tokio::test] async fn dispatch_observer_record_never_acquires_local_workspace_ownership() { let ownership_root = tempdir().expect("ownership root"); diff --git a/src/crates/assembly/core/src/service/bootstrap/bootstrap_impl.rs b/src/crates/assembly/core/src/service/bootstrap/bootstrap_impl.rs index 9bc83a49bc..b6ebf91ba3 100644 --- a/src/crates/assembly/core/src/service/bootstrap/bootstrap_impl.rs +++ b/src/crates/assembly/core/src/service/bootstrap/bootstrap_impl.rs @@ -11,7 +11,7 @@ const BOOTSTRAP_TEMPLATE: &str = include_str!("templates/BOOTSTRAP.md"); const SOUL_TEMPLATE: &str = include_str!("templates/SOUL.md"); const USER_TEMPLATE: &str = include_str!("templates/USER.md"); const IDENTITY_TEMPLATE: &str = include_str!("templates/IDENTITY.md"); -#[cfg(feature = "product-full")] +#[cfg(feature = "agent-runtime")] const PERSONA_FILE_NAMES: [&str; 4] = [ BOOTSTRAP_FILE_NAME, SOUL_FILE_NAME, @@ -141,12 +141,12 @@ pub(crate) async fn initialize_workspace_persona_files(workspace_root: &Path) -> Ok(()) } -#[cfg(feature = "product-full")] +#[cfg(feature = "agent-runtime")] pub(crate) fn is_workspace_bootstrap_pending(workspace_root: &Path) -> bool { workspace_root.join(BOOTSTRAP_FILE_NAME).exists() } -#[cfg(feature = "product-full")] +#[cfg(feature = "agent-runtime")] pub(crate) async fn ensure_workspace_persona_files_for_prompt( workspace_root: &Path, ) -> BitFunResult<()> { @@ -236,7 +236,7 @@ pub async fn reset_workspace_persona_files_to_default(workspace_root: &Path) -> Ok(()) } -#[cfg(feature = "product-full")] +#[cfg(feature = "agent-runtime")] pub(crate) async fn build_workspace_persona_prompt( workspace_root: &Path, ) -> BitFunResult> { @@ -312,7 +312,7 @@ The following files are located in the workspace root directory and define your ))) } -#[cfg(feature = "product-full")] +#[cfg(feature = "agent-runtime")] fn persona_file_description(file_name: &str) -> &'static str { match file_name { BOOTSTRAP_FILE_NAME => "Bootstrap guidance and initialization instructions", @@ -323,7 +323,7 @@ fn persona_file_description(file_name: &str) -> &'static str { } } -#[cfg(all(test, feature = "product-full"))] +#[cfg(all(test, feature = "agent-runtime"))] mod tests { use super::{ ensure_workspace_gitignore_ignores_bitfun, ensure_workspace_persona_files_for_prompt, diff --git a/src/crates/assembly/core/src/service/bootstrap/mod.rs b/src/crates/assembly/core/src/service/bootstrap/mod.rs index d26eaf719b..a0e710c6b3 100644 --- a/src/crates/assembly/core/src/service/bootstrap/mod.rs +++ b/src/crates/assembly/core/src/service/bootstrap/mod.rs @@ -1,7 +1,7 @@ mod bootstrap_impl; pub use bootstrap_impl::reset_workspace_persona_files_to_default; -#[cfg(feature = "product-full")] +#[cfg(feature = "agent-runtime")] pub(crate) use bootstrap_impl::{ build_workspace_persona_prompt, ensure_workspace_persona_files_for_prompt, is_workspace_bootstrap_pending, diff --git a/src/crates/assembly/core/src/service/config/global.rs b/src/crates/assembly/core/src/service/config/global.rs index 9f92a8352c..bee55ef66e 100644 --- a/src/crates/assembly/core/src/service/config/global.rs +++ b/src/crates/assembly/core/src/service/config/global.rs @@ -4,7 +4,7 @@ use super::service::ConfigService; use crate::util::errors::*; -#[cfg(feature = "product-full")] +#[cfg(feature = "agent-runtime")] use log::warn; use log::{debug, info}; use std::sync::Arc; @@ -108,7 +108,7 @@ impl GlobalConfigManager { info!("Global config service initialized"); - #[cfg(feature = "product-full")] + #[cfg(feature = "agent-runtime")] { match super::mode_config_canonicalizer::canonicalize_agent_profile_configs().await { Ok(report) => { @@ -168,7 +168,7 @@ impl GlobalConfigManager { pub async fn reload() -> BitFunResult<()> { let service = Self::get_service().await?; service.reload().await?; - #[cfg(feature = "product-full")] + #[cfg(feature = "agent-runtime")] if let Err(error) = super::mode_config_canonicalizer::canonicalize_agent_profile_configs().await { diff --git a/src/crates/assembly/core/src/service/config/mod.rs b/src/crates/assembly/core/src/service/config/mod.rs index cfae48b8d7..94d2f02940 100644 --- a/src/crates/assembly/core/src/service/config/mod.rs +++ b/src/crates/assembly/core/src/service/config/mod.rs @@ -2,13 +2,13 @@ //! //! A complete configuration management system based on the Provider mechanism. -#[cfg(feature = "product-full")] +#[cfg(feature = "agent-runtime")] pub mod agent_profile_project_store; pub mod app_language; pub mod factory; pub mod global; pub mod manager; -#[cfg(feature = "product-full")] +#[cfg(feature = "agent-runtime")] pub mod mode_config_canonicalizer; pub mod project_permission_store; pub mod providers; @@ -24,7 +24,7 @@ pub use global::{ subscribe_config_updates, ConfigUpdateEvent, GlobalConfigManager, }; pub use manager::{ConfigManager, ConfigManagerSettings, ConfigStatistics}; -#[cfg(feature = "product-full")] +#[cfg(feature = "agent-runtime")] pub use mode_config_canonicalizer::{ canonicalize_agent_profile_configs, AgentProfileConfigCanonicalizationReport, AgentProfileConfigUpdateInfo, diff --git a/src/crates/assembly/core/src/service/config/service.rs b/src/crates/assembly/core/src/service/config/service.rs index bd8c2227f7..3b1462bd95 100644 --- a/src/crates/assembly/core/src/service/config/service.rs +++ b/src/crates/assembly/core/src/service/config/service.rs @@ -120,7 +120,7 @@ impl ConfigService { /// Atomically replaces one JSON configuration value when its current value /// still matches the caller's snapshot. The read, comparison, and persisted /// write share the existing manager write lock. - #[cfg(any(test, feature = "product-full"))] + #[cfg(any(test, feature = "agent-runtime"))] pub(crate) async fn compare_and_set_json_config( &self, path: &str, diff --git a/src/crates/assembly/core/src/service/dispatch/mod.rs b/src/crates/assembly/core/src/service/dispatch/mod.rs index 43062911e2..9960f12a44 100644 --- a/src/crates/assembly/core/src/service/dispatch/mod.rs +++ b/src/crates/assembly/core/src/service/dispatch/mod.rs @@ -1,10 +1,10 @@ -#[cfg(feature = "product-full")] +#[cfg(feature = "agent-runtime")] mod baseline; -#[cfg(feature = "product-full")] +#[cfg(all(feature = "agent-runtime", feature = "ssh-remote"))] mod controller; -#[cfg(feature = "product-full")] +#[cfg(all(feature = "agent-runtime", feature = "ssh-remote"))] mod device_controller; -#[cfg(feature = "product-full")] +#[cfg(feature = "agent-runtime")] mod preparation; mod target; @@ -20,14 +20,14 @@ use tokio::fs; use crate::infrastructure::PathManager; -#[cfg(feature = "product-full")] +#[cfg(all(feature = "agent-runtime", feature = "ssh-remote"))] pub use controller::{ answer as answer_dispatch, append as append_dispatch, cancel as cancel_dispatch, continue_job as continue_dispatch_job, install_cli_cancel as cancel_dispatch_cli_install, - install_cli_poll as poll_dispatch_cli_install, - install_cli_start as start_dispatch_cli_install, list_jobs as list_dispatch_jobs, - list_targets as list_dispatch_targets, probe_target as probe_dispatch_target, - query_job as query_dispatch_job, status as get_dispatch_status, submit as submit_dispatch, + install_cli_poll as poll_dispatch_cli_install, install_cli_start as start_dispatch_cli_install, + list_jobs as list_dispatch_jobs, list_targets as list_dispatch_targets, + probe_target as probe_dispatch_target, query_job as query_dispatch_job, + status as get_dispatch_status, submit as submit_dispatch, sync_model_config as sync_dispatch_model_config, sync_result as sync_dispatch_result, DispatchAnswerRequest, DispatchAppendRequest, DispatchConnectionRequest, DispatchContinueRequest, DispatchInstallPollRequest, DispatchInstallStartRequest, @@ -35,7 +35,7 @@ pub use controller::{ DispatchPermissionReplyKind, DispatchProbeTargetRequest, DispatchQueryJobRequest, DispatchStatusRequest, DispatchSubmitRequest, DispatchSyncResultRequest, DispatchTargetOption, }; -#[cfg(feature = "product-full")] +#[cfg(all(feature = "agent-runtime", feature = "ssh-remote"))] pub use device_controller::{ answer_device as answer_device_dispatch, append_device as append_device_dispatch, cancel_device as cancel_device_dispatch, continue_device_job as continue_device_dispatch_job, @@ -51,7 +51,7 @@ const PROMPT_PREVIEW_CHARS: usize = 160; /// controller's baseline worktree. pub(super) const OUTBOUND_RESULTS_DIR: &str = ".results"; /// Where base bundles are built before being uploaded to a target. -#[cfg(feature = "product-full")] +#[cfg(feature = "agent-runtime")] const OUTBOUND_BUNDLES_DIR: &str = ".bundles"; /// Where the renderer's observer transcript cache lives. const OUTBOUND_TRANSCRIPTS_DIR: &str = ".transcripts"; @@ -65,7 +65,7 @@ const MAX_OUTBOUND_TRANSCRIPT_BYTES: usize = 8 * 1024 * 1024; #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] -#[cfg(feature = "product-full")] +#[cfg(feature = "agent-runtime")] struct DispatchTargetJobEntry { job_id: String, session_id: String, @@ -382,7 +382,7 @@ impl OutboundDispatchStore { } pub async fn list(&self) -> Result, DispatchStoreError> { - #[cfg(feature = "product-full")] + #[cfg(feature = "agent-runtime")] if let Err(error) = self.reconcile_expired_preparations().await { log::warn!("Failed to reconcile expired dispatch preparations: {error}"); } @@ -521,7 +521,7 @@ impl OutboundDispatchStore { /// /// Bundles hold repository contents, so they get the same private treatment /// as everything else the controller writes here. - #[cfg(feature = "product-full")] + #[cfg(feature = "agent-runtime")] pub(crate) async fn bundles_dir(&self) -> anyhow::Result { let bundles = self.root.join(OUTBOUND_BUNDLES_DIR); fs::create_dir_all(&bundles).await?; @@ -530,7 +530,7 @@ impl OutboundDispatchStore { } /// Owner-only staging directory for bundles fetched back from a target. - #[cfg(feature = "product-full")] + #[cfg(feature = "agent-runtime")] pub(crate) async fn results_dir(&self) -> anyhow::Result { let results = self.root.join(OUTBOUND_RESULTS_DIR); fs::create_dir_all(&results).await?; @@ -665,7 +665,7 @@ impl BaselineClaimRelease { /// The caller intentionally keeps the durable outbound record until this /// succeeds, so a moved repository or temporary registry error remains /// observable and retryable instead of silently stranding a claim. -#[cfg(feature = "product-full")] +#[cfg(feature = "agent-runtime")] async fn release_baseline_claim(release: BaselineClaimRelease) -> Result<(), DispatchStoreError> { crate::service::worktree::WorktreeService::release_claim_for_worktree( &release.project_workspace_path, @@ -679,10 +679,10 @@ async fn release_baseline_claim(release: BaselineClaimRelease) -> Result<(), Dis }) } -#[cfg(not(feature = "product-full"))] +#[cfg(not(feature = "agent-runtime"))] async fn release_baseline_claim(release: BaselineClaimRelease) -> Result<(), DispatchStoreError> { Err(DispatchStoreError::ClaimRelease(format!( - "job_id={} error=product-full is required to release the baseline worktree claim", + "job_id={} error=agent-runtime is required to release the baseline worktree claim", release.job_id ))) } @@ -700,7 +700,7 @@ async fn remove_file_if_present(path: &Path) -> anyhow::Result<()> { } } -#[cfg(feature = "product-full")] +#[cfg(feature = "agent-runtime")] async fn adopt_target_jobs( store: &OutboundDispatchStore, target: &DispatchTarget, @@ -795,7 +795,7 @@ async fn adopt_target_jobs( /// Validate a path returned by the target without applying the controller /// process's host path semantics. The target may run POSIX while the controller /// runs Windows, or vice versa. -#[cfg(feature = "product-full")] +#[cfg(feature = "agent-runtime")] fn target_workspace_path_is_absolute(path: &str) -> bool { let path = path.trim(); if path.starts_with('/') { @@ -820,7 +820,7 @@ fn target_workspace_path_is_absolute(path: &str) -> bool { components.next().is_some() && components.next().is_some() } -#[cfg(feature = "product-full")] +#[cfg(feature = "agent-runtime")] fn same_target_identity_for_store(left: &DispatchTarget, right: &DispatchTarget) -> bool { match (left, right) { ( @@ -1094,13 +1094,13 @@ mod tests { .is_none()); } - #[cfg(not(feature = "product-full"))] + #[cfg(not(feature = "agent-runtime"))] #[tokio::test] async fn removing_a_claimed_record_without_product_full_fails_closed() { let temp = tempfile::tempdir().expect("tempdir"); let store = OutboundDispatchStore::new_in_root_for_tests(temp.path().to_path_buf()); let mut record = OutboundDispatchRecord::new( - "job-no-product-full".to_string(), + "job-no-agent-runtime".to_string(), target(), "session-1".to_string(), "/srv/app".to_string(), @@ -1114,20 +1114,20 @@ mod tests { store.bind_if_absent(&record).await.expect("persist"); let error = store - .remove("job-no-product-full") + .remove("job-no-agent-runtime") .await .expect_err("claim cleanup without the product owner must fail closed"); let DispatchStoreError::ClaimRelease(message) = error else { panic!("unexpected dispatch cleanup error: {error}"); }; - assert!(message.contains("product-full")); + assert!(message.contains("agent-runtime")); assert!( store - .get("job-no-product-full") + .get("job-no-agent-runtime") .await .expect("read retained record") .is_some(), - "the durable record must remain available for a product-full retry" + "the durable record must remain available for an agent-runtime retry" ); } @@ -1402,7 +1402,7 @@ mod tests { assert_eq!(record.prompt_preview.chars().count(), PROMPT_PREVIEW_CHARS); } - #[cfg(feature = "product-full")] + #[cfg(feature = "agent-runtime")] #[test] fn target_workspace_paths_use_target_platform_semantics() { assert!(target_workspace_path_is_absolute("/srv/app")); @@ -1417,7 +1417,7 @@ mod tests { assert!(!target_workspace_path_is_absolute(r"\\server")); } - #[cfg(feature = "product-full")] + #[cfg(feature = "agent-runtime")] #[tokio::test] async fn listing_a_target_adopts_observer_records_without_runtime_ownership() { let temp = tempfile::tempdir().expect("tempdir"); diff --git a/src/crates/assembly/core/src/service/instruction_context.rs b/src/crates/assembly/core/src/service/instruction_context.rs index ad093c50be..4483e606b7 100644 --- a/src/crates/assembly/core/src/service/instruction_context.rs +++ b/src/crates/assembly/core/src/service/instruction_context.rs @@ -1,5 +1,6 @@ use crate::util::errors::*; use bitfun_runtime_ports::WorkspaceFileSystem; +use bitfun_services_core::local_instructions::LocalInstructionFile; use bitfun_services_core::workspace_instructions::WorkspaceInstructionFile; use std::path::Path; @@ -11,6 +12,31 @@ pub(crate) struct InstructionContextBuild { pub(crate) cacheable: bool, } +async fn load_user_instruction_files(workspace_root: &Path) -> (Vec, bool) { + #[cfg(feature = "external-sources")] + { + let files = + crate::instruction_sources::load_local_user_instruction_files(workspace_root).await; + return (files.files, files.cacheable); + } + #[cfg(not(feature = "external-sources"))] + { + let _ = workspace_root; + (Vec::new(), true) + } +} + +async fn load_user_conditional_instruction_files() -> Vec { + #[cfg(feature = "external-sources")] + { + return crate::instruction_sources::load_local_user_conditional_instruction_sources().await; + } + #[cfg(not(feature = "external-sources"))] + { + Vec::new() + } +} + pub(crate) async fn build_workspace_instruction_files_context( workspace_root: &Path, ) -> BitFunResult> { @@ -24,8 +50,8 @@ pub(crate) async fn build_workspace_instruction_files_context( pub(crate) async fn build_workspace_instruction_files_context_detailed( workspace_root: &Path, ) -> BitFunResult { - let user_instruction_files = - crate::instruction_sources::load_local_user_instruction_files(workspace_root).await; + let (user_instruction_files, user_instruction_files_cacheable) = + load_user_instruction_files(workspace_root).await; let workspace_instruction_files = bitfun_services_core::workspace_instructions::read_workspace_instruction_files( workspace_root, @@ -35,6 +61,7 @@ pub(crate) async fn build_workspace_instruction_files_context_detailed( Ok(compose_local_instruction_sources( workspace_root, user_instruction_files, + user_instruction_files_cacheable, workspace_instruction_files, )) } @@ -44,8 +71,8 @@ pub(crate) async fn build_local_workspace_instruction_files_context_with_fs_deta fs: &dyn WorkspaceFileSystem, workspace_root_path: &str, ) -> BitFunResult { - let user_instruction_files = - crate::instruction_sources::load_local_user_instruction_files(workspace_root).await; + let (user_instruction_files, user_instruction_files_cacheable) = + load_user_instruction_files(workspace_root).await; let workspace_instruction_files = bitfun_services_core::workspace_instructions::read_workspace_instruction_files_with_fs( fs, @@ -56,30 +83,31 @@ pub(crate) async fn build_local_workspace_instruction_files_context_with_fs_deta Ok(compose_local_instruction_sources( workspace_root, user_instruction_files, + user_instruction_files_cacheable, workspace_instruction_files, )) } fn compose_local_instruction_sources( workspace_root: &Path, - user_instruction_files: crate::instruction_sources::LocalUserInstructionFiles, + user_instruction_files: Vec, + user_instruction_files_cacheable: bool, workspace_instruction_files: Vec, ) -> InstructionContextBuild { - let cacheable = user_instruction_files.cacheable; let instruction_files = merge_local_instruction_sources( workspace_root, - user_instruction_files.files, + user_instruction_files, workspace_instruction_files, ); InstructionContextBuild { content: render_workspace_instruction_files_section(&instruction_files), - cacheable, + cacheable: user_instruction_files_cacheable, } } fn merge_local_instruction_sources( workspace_root: &Path, - user_instruction_files: Vec, + user_instruction_files: Vec, mut workspace_instruction_files: Vec, ) -> Vec { bitfun_services_core::workspace_instructions::retain_distinct_local_workspace_instruction_files( @@ -104,8 +132,7 @@ fn merge_local_instruction_sources( pub(crate) async fn load_local_conditional_instruction_files( workspace_root: &Path, ) -> BitFunResult> { - let user_instruction_files = - crate::instruction_sources::load_local_user_conditional_instruction_sources().await; + let user_instruction_files = load_user_conditional_instruction_files().await; let workspace_instruction_files = bitfun_services_core::workspace_instructions::read_workspace_conditional_instruction_sources( workspace_root, @@ -124,8 +151,7 @@ pub(crate) async fn load_local_conditional_instruction_files_with_fs( fs: &dyn WorkspaceFileSystem, workspace_root_path: &str, ) -> BitFunResult> { - let user_instruction_files = - crate::instruction_sources::load_local_user_conditional_instruction_sources().await; + let user_instruction_files = load_user_conditional_instruction_files().await; let workspace_instruction_files = bitfun_services_core::workspace_instructions::read_workspace_conditional_instruction_sources_with_fs( fs, @@ -214,16 +240,20 @@ fn escape_document_name(name: &str) -> String { #[cfg(test)] mod tests { + #[cfg(feature = "external-sources")] use super::{ build_workspace_instruction_files_context, build_workspace_instruction_files_context_detailed, build_workspace_instruction_files_context_with_fs, - load_local_conditional_instruction_files, render_workspace_instruction_files_section, - WorkspaceInstructionFile, + load_local_conditional_instruction_files, }; + use super::{render_workspace_instruction_files_section, WorkspaceInstructionFile}; + #[cfg(feature = "external-sources")] use crate::instruction_sources::test_support::{lock_environment, EnvironmentGuard}; + #[cfg(feature = "external-sources")] use bitfun_services_core::workspace::LocalWorkspaceFs; + #[cfg(feature = "external-sources")] #[tokio::test] async fn local_user_instructions_precede_workspace_instructions_by_ecosystem_priority() { let _environment = lock_environment(); @@ -266,6 +296,7 @@ mod tests { assert!(positions.windows(2).all(|pair| pair[0] < pair[1])); } + #[cfg(feature = "external-sources")] #[tokio::test] async fn conditional_instructions_keep_user_then_workspace_precedence() { let _environment = lock_environment(); @@ -311,6 +342,7 @@ mod tests { ); } + #[cfg(feature = "external-sources")] #[tokio::test] async fn invalid_user_rule_does_not_hide_project_conditional_instructions() { let _environment = lock_environment(); @@ -351,6 +383,7 @@ mod tests { assert_eq!(files[0].name, ".claude/rules/project.md"); } + #[cfg(feature = "external-sources")] #[tokio::test] async fn opencode_global_config_resolves_relative_instructions_in_the_local_workspace() { let _environment = lock_environment(); @@ -390,6 +423,7 @@ mod tests { assert!(rendered.contains("")); } + #[cfg(feature = "external-sources")] #[tokio::test] async fn invalid_user_source_does_not_hide_workspace_instructions() { let _environment = lock_environment(); @@ -423,6 +457,7 @@ mod tests { assert!(!build.cacheable); } + #[cfg(feature = "external-sources")] #[tokio::test] async fn a_user_configured_workspace_file_is_not_rendered_again_as_a_project_source() { let _environment = lock_environment(); @@ -456,6 +491,7 @@ mod tests { assert_eq!(rendered.matches("One physical source").count(), 1); } + #[cfg(feature = "external-sources")] #[tokio::test] async fn port_backed_workspace_never_falls_back_to_local_user_sources() { let _environment = lock_environment(); diff --git a/src/crates/assembly/core/src/service/mcp/adapter/tool.rs b/src/crates/assembly/core/src/service/mcp/adapter/tool.rs index 4af536b058..11692f6fbc 100644 --- a/src/crates/assembly/core/src/service/mcp/adapter/tool.rs +++ b/src/crates/assembly/core/src/service/mcp/adapter/tool.rs @@ -119,7 +119,7 @@ impl MCPToolWrapper { } fn is_blocked_in_context(&self, context: Option<&ToolUseContext>) -> bool { - let workspace_key = crate::external_tools::workspace_route_key( + let workspace_key = crate::agentic::workspace::workspace_route_key( context.and_then(ToolUseContext::workspace_root), ); !self.context_policy.server_available_for_route( diff --git a/src/crates/assembly/core/src/service/mcp/server/manager/external_lifecycle.rs b/src/crates/assembly/core/src/service/mcp/server/manager/external_lifecycle.rs index b9584919cb..bba62016fc 100644 --- a/src/crates/assembly/core/src/service/mcp/server/manager/external_lifecycle.rs +++ b/src/crates/assembly/core/src/service/mcp/server/manager/external_lifecycle.rs @@ -2,6 +2,11 @@ use super::*; const EXTERNAL_START_GUARD_ALLOWANCE: Duration = Duration::from_secs(30); +fn notify_external_tool_registry_changed() { + #[cfg(feature = "external-sources")] + crate::external_sources::notify_external_tool_registry_changed(); +} + fn external_start_timeout(timeouts: super::super::MCPServerTimeouts) -> Duration { // The outer guard also covers bounded orchestration that sits outside the // per-request initialize and Tool catalog deadlines. @@ -174,7 +179,7 @@ impl MCPServerManager { .external_start_token_matches(&server_id, &start_token) .await { - crate::external_sources::notify_external_tool_registry_changed(); + notify_external_tool_registry_changed(); } } Ok(Err(error)) => { @@ -186,7 +191,7 @@ impl MCPServerManager { .remove_ephemeral_server_for_start(&server_id, &start_token) .await { - crate::external_sources::notify_external_tool_registry_changed(); + notify_external_tool_registry_changed(); } } Err(_) => { @@ -198,7 +203,7 @@ impl MCPServerManager { .remove_ephemeral_server_for_start(&server_id, &start_token) .await { - crate::external_sources::notify_external_tool_registry_changed(); + notify_external_tool_registry_changed(); } } } diff --git a/src/crates/assembly/core/src/service/mcp/server/manager/mod.rs b/src/crates/assembly/core/src/service/mcp/server/manager/mod.rs index 4d7c4eb2b1..0127e2112a 100644 --- a/src/crates/assembly/core/src/service/mcp/server/manager/mod.rs +++ b/src/crates/assembly/core/src/service/mcp/server/manager/mod.rs @@ -128,7 +128,7 @@ impl MCPServerManager { .await .get(server_id) .cloned(); - let workspace_key = crate::external_tools::workspace_route_key(workspace_root); + let workspace_key = crate::agentic::workspace::workspace_route_key(workspace_root); self.tool_context_policy.server_available_for_route( server_id, external_workspace_scope.as_deref(), diff --git a/src/crates/assembly/core/src/service/mod.rs b/src/crates/assembly/core/src/service/mod.rs index 25909783e7..d2894eabf6 100644 --- a/src/crates/assembly/core/src/service/mod.rs +++ b/src/crates/assembly/core/src/service/mod.rs @@ -11,7 +11,7 @@ pub(crate) mod bootstrap; // Workspace persona bootstrap helpers #[cfg(feature = "canvas-runtime")] pub mod canvas; // Canvas service compatibility facade pub mod config; // Config management -#[cfg(feature = "product-full")] +#[cfg(feature = "agent-runtime")] pub mod cron; // Scheduled jobs #[cfg(feature = "dispatch-store")] pub mod dispatch; // Outbound dispatch observer index and target contracts @@ -20,13 +20,13 @@ pub mod filesystem; // FileSystem management #[cfg(feature = "git")] pub mod git; // Git service pub mod i18n; // I18n service -#[cfg(feature = "product-full")] +#[cfg(feature = "agent-runtime")] pub(crate) mod instruction_context; // Workspace instruction file prompt helpers #[cfg(feature = "lsp")] pub mod lsp; // LSP (Language Server Protocol) system -#[cfg(feature = "product-full")] +#[cfg(feature = "agent-runtime")] pub mod mcp; // MCP (Model Context Protocol) system -#[cfg(feature = "product-full")] +#[cfg(feature = "agent-runtime")] pub mod remote_connect; // Remote Connect (phone → desktop) #[cfg(feature = "remote-workspace")] pub mod remote_ssh; // Remote SSH (desktop → server) @@ -34,21 +34,21 @@ pub mod remote_ssh; // Remote SSH (desktop → server) pub mod review_platform; // Pull request review platform adapters #[cfg(feature = "process-runtime")] pub mod runtime; // Managed runtime and capability management -#[cfg(feature = "product-full")] +#[cfg(feature = "agent-runtime")] pub mod search; // Workspace search via managed flashgrep daemon #[cfg(feature = "local-storage")] pub mod session; // Session persistence -#[cfg(feature = "product-full")] +#[cfg(feature = "agent-runtime")] pub mod session_usage; // Session runtime usage reports -#[cfg(feature = "product-full")] +#[cfg(feature = "agent-runtime")] pub mod snapshot; // Snapshot-based change tracking -#[cfg(feature = "product-full")] +#[cfg(feature = "agent-runtime")] pub mod token_usage; // Token usage tracking #[cfg(feature = "workspace-runtime")] pub mod workspace; // Workspace management // Diff calculation and merge service #[cfg(feature = "workspace-runtime")] pub mod workspace_runtime; // Workspace runtime layout / migration / initialization -#[cfg(feature = "product-full")] +#[cfg(feature = "agent-runtime")] pub mod worktree; // Managed Git worktree lifecycle and session bindings // Terminal is implemented in the workspace-level `terminal-core` crate. @@ -69,7 +69,7 @@ pub use bootstrap::reset_workspace_persona_files_to_default; #[cfg(feature = "canvas-runtime")] pub use canvas::{CanvasMemoryStore, CanvasService}; pub use config::{ConfigManager, ConfigProvider, ConfigService}; -#[cfg(feature = "product-full")] +#[cfg(feature = "agent-runtime")] pub use cron::{ get_global_cron_service, set_global_cron_service, CronEventSubscriber, CronService, }; @@ -89,7 +89,7 @@ pub use git::GitService; pub use i18n::{get_global_i18n_service, I18nConfig, I18nService, LocaleId, LocaleMetadata}; #[cfg(feature = "lsp")] pub use lsp::LspManager; -#[cfg(feature = "product-full")] +#[cfg(feature = "agent-runtime")] pub use mcp::MCPService; #[cfg(feature = "review-platform")] pub use review_platform::{ @@ -104,7 +104,7 @@ pub use review_platform::{ }; #[cfg(feature = "process-runtime")] pub use runtime::{ResolvedCommand, RuntimeCommandCapability, RuntimeManager, RuntimeSource}; -#[cfg(feature = "product-full")] +#[cfg(feature = "agent-runtime")] pub use search::{ get_global_workspace_search_service, set_global_workspace_search_service, ContentSearchRequest, ContentSearchResult, GlobSearchRequest, GlobSearchResult, IndexTaskHandle, @@ -115,14 +115,14 @@ pub use search::{ WorkspaceSearchTaskKind, WorkspaceSearchTaskPhase, WorkspaceSearchTaskState, WorkspaceSearchTaskStatus, }; -#[cfg(feature = "product-full")] +#[cfg(feature = "agent-runtime")] pub use snapshot::SnapshotService; #[cfg(feature = "process-runtime")] pub use system::{ check_command, check_commands, run_command, run_command_simple, CheckCommandResult, CommandOutput, SystemError, }; -#[cfg(feature = "product-full")] +#[cfg(feature = "agent-runtime")] pub use token_usage::{ ModelTokenStats, SessionTokenStats, TimeRange, TokenUsageQuery, TokenUsageRecord, TokenUsageService, TokenUsageSummary, @@ -135,5 +135,5 @@ pub use workspace_runtime::{ RuntimeMigrationRecord, WorkspaceRuntimeContext, WorkspaceRuntimeEnsureResult, WorkspaceRuntimeService, WorkspaceRuntimeTarget, }; -#[cfg(feature = "product-full")] +#[cfg(feature = "agent-runtime")] pub use worktree::WorktreeService; diff --git a/src/crates/assembly/core/src/service/workspace/service.rs b/src/crates/assembly/core/src/service/workspace/service.rs index 4c46616f4a..b37de9e080 100644 --- a/src/crates/assembly/core/src/service/workspace/service.rs +++ b/src/crates/assembly/core/src/service/workspace/service.rs @@ -288,7 +288,7 @@ impl WorkspaceService { Ok(service) } - #[cfg(all(test, feature = "product-full"))] + #[cfg(all(test, feature = "agent-runtime"))] pub(crate) async fn new_for_test_path_manager(path_manager: Arc) -> Self { path_manager .initialize_user_directories() @@ -2395,7 +2395,7 @@ pub fn get_global_workspace_service() -> Option> { GLOBAL_WORKSPACE_SERVICE.get().cloned() } -#[cfg(all(test, feature = "product-full"))] +#[cfg(all(test, feature = "agent-runtime"))] mod tests { use super::*; use crate::agentic::persistence::PersistenceManager; diff --git a/src/crates/assembly/core/src/service/workspace_runtime/service.rs b/src/crates/assembly/core/src/service/workspace_runtime/service.rs index d27df10847..93c4b4a6ba 100644 --- a/src/crates/assembly/core/src/service/workspace_runtime/service.rs +++ b/src/crates/assembly/core/src/service/workspace_runtime/service.rs @@ -2,7 +2,7 @@ use super::types::{ RuntimeMigrationRecord, WorkspaceRuntimeContext, WorkspaceRuntimeEnsureResult, WorkspaceRuntimeTarget, WORKSPACE_RUNTIME_LAYOUT_VERSION, }; -#[cfg(feature = "product-full")] +#[cfg(feature = "agent-runtime")] use crate::agentic::WorkspaceBinding; use crate::infrastructure::{get_path_manager_arc, PathManager}; use crate::util::errors::{BitFunError, BitFunResult}; @@ -135,7 +135,7 @@ impl WorkspaceRuntimeService { .await } - #[cfg(feature = "product-full")] + #[cfg(feature = "agent-runtime")] pub async fn ensure_runtime_for_workspace_binding( &self, workspace: &WorkspaceBinding, diff --git a/src/crates/assembly/core/src/util/errors.rs b/src/crates/assembly/core/src/util/errors.rs index 2828799b9e..35de43aedd 100644 --- a/src/crates/assembly/core/src/util/errors.rs +++ b/src/crates/assembly/core/src/util/errors.rs @@ -206,7 +206,7 @@ impl BitFunError { } } -#[cfg(feature = "product-full")] +#[cfg(feature = "agent-runtime")] impl From for BitFunError { fn from(error: bitfun_agent_stream::StreamProcessorError) -> Self { match error { @@ -217,7 +217,7 @@ impl From for BitFunError { } } -#[cfg(feature = "product-full")] +#[cfg(feature = "agent-runtime")] impl From for BitFunError { fn from(error: bitfun_agent_runtime::event_bus::EventBusError) -> Self { Self::Agent(error.to_string()) @@ -230,7 +230,7 @@ impl From for BitFun } } -#[cfg(feature = "product-full")] +#[cfg(feature = "agent-runtime")] impl From for BitFunError { fn from(error: bitfun_services_integrations::mcp::MCPRuntimeError) -> Self { use bitfun_services_integrations::mcp::MCPRuntimeErrorKind; diff --git a/src/crates/assembly/product-capabilities/AGENTS.md b/src/crates/assembly/product-capabilities/AGENTS.md index bb506b9648..de71a3fe1f 100644 --- a/src/crates/assembly/product-capabilities/AGENTS.md +++ b/src/crates/assembly/product-capabilities/AGENTS.md @@ -19,6 +19,10 @@ concrete runtime execution. selection. - `ProductAssembler` may validate explicit profile input and return immutable runtime parts; it must not create concrete services or product state. +- `ProductCoreDependencyMode::ExplicitCoreCapabilityClosure` records that an + entrypoint selects reviewed Cargo owner capabilities; it is not a feature + list, runtime availability result, or permission to introduce a profile-named + umbrella feature. - Do not encode product UI behavior, permission decisions, session lifecycle, filesystem/process IO, Git/AI provider acquisition, or feature defaults here. - Preserve default product tool provider order and legacy harness provider ids diff --git a/src/crates/assembly/product-capabilities/src/lib.rs b/src/crates/assembly/product-capabilities/src/lib.rs index 887df76a39..2f263d40ea 100644 --- a/src/crates/assembly/product-capabilities/src/lib.rs +++ b/src/crates/assembly/product-capabilities/src/lib.rs @@ -195,6 +195,7 @@ impl fmt::Display for DeliveryProfile { #[non_exhaustive] pub enum ProductCoreDependencyMode { ProductFullCompatibility, + ExplicitCoreCapabilityClosure, NoDirectCoreDependency, } @@ -235,7 +236,7 @@ const PRODUCT_DELIVERY_PROFILE_ENTRIES: &[ProductDeliveryProfileEntry] = &[ ), ProductDeliveryProfileEntry::new( DeliveryProfile::Cli, - ProductCoreDependencyMode::ProductFullCompatibility, + ProductCoreDependencyMode::ExplicitCoreCapabilityClosure, ), ProductDeliveryProfileEntry::new( DeliveryProfile::Server, @@ -247,7 +248,7 @@ const PRODUCT_DELIVERY_PROFILE_ENTRIES: &[ProductDeliveryProfileEntry] = &[ ), ProductDeliveryProfileEntry::new( DeliveryProfile::Acp, - ProductCoreDependencyMode::ProductFullCompatibility, + ProductCoreDependencyMode::ExplicitCoreCapabilityClosure, ), ProductDeliveryProfileEntry::new( DeliveryProfile::Web, diff --git a/src/crates/assembly/product-capabilities/tests/product_capabilities.rs b/src/crates/assembly/product-capabilities/tests/product_capabilities.rs index 19c2d3adba..7abe4bfb5c 100644 --- a/src/crates/assembly/product-capabilities/tests/product_capabilities.rs +++ b/src/crates/assembly/product-capabilities/tests/product_capabilities.rs @@ -359,7 +359,7 @@ fn product_delivery_profile_matrix_documents_current_core_dependency_shape() { ), ( DeliveryProfile::Cli, - ProductCoreDependencyMode::ProductFullCompatibility, + ProductCoreDependencyMode::ExplicitCoreCapabilityClosure, ), ( DeliveryProfile::Server, @@ -371,7 +371,7 @@ fn product_delivery_profile_matrix_documents_current_core_dependency_shape() { ), ( DeliveryProfile::Acp, - ProductCoreDependencyMode::ProductFullCompatibility, + ProductCoreDependencyMode::ExplicitCoreCapabilityClosure, ), ( DeliveryProfile::Web, @@ -400,10 +400,11 @@ fn product_assembly_plan_follows_core_dependency_matrix() { let plan = product_assembly_plan_for_profile(entry.profile()); match entry.core_dependency_mode() { - ProductCoreDependencyMode::ProductFullCompatibility => { + ProductCoreDependencyMode::ProductFullCompatibility + | ProductCoreDependencyMode::ExplicitCoreCapabilityClosure => { assert!( !plan.capability_set().ids().is_empty(), - "{} must retain product-full capabilities", + "{} must retain runtime capabilities", entry.profile() ); assert!( @@ -411,12 +412,12 @@ fn product_assembly_plan_follows_core_dependency_matrix() { .capability_assembly() .tool_provider_group_plan() .is_empty(), - "{} must retain product-full tool groups", + "{} must retain runtime tool groups", entry.profile() ); assert!( !plan.feature_groups().is_empty(), - "{} must retain product-full feature groups", + "{} must retain runtime feature groups", entry.profile() ); } diff --git a/src/crates/interfaces/acp/AGENTS-CN.md b/src/crates/interfaces/acp/AGENTS-CN.md index 811c49b3cb..f71972f39d 100644 --- a/src/crates/interfaces/acp/AGENTS-CN.md +++ b/src/crates/interfaces/acp/AGENTS-CN.md @@ -8,9 +8,10 @@ CLI 托管的 ACP 服务端已通过 `ProductAssembler` 消费 `DeliveryProfile::Acp`,并使用 Agent Runtime SDK 完成会话创建/列举、活动会话模型/模式更新、轮次提交/取消、交互响应和 Agent 事件订阅。`bitfun-acp` 仍直接依赖 -`bitfun-core/product-full`,用于一次性恢复完整持久化历史、模型/模式目录与提供方配置读取、MCP 配置, -以及本 crate 的 ACP 客户端路径。在这些生产路径分别获得可移植替代并证明等价前,不得宣称整个 crate -已与 Core 解耦。 +`bitfun-core` 的 `agent-runtime`、`canvas-runtime`、`external-sources` 与 `ssh-remote` owner feature, +用于一次性恢复完整持久化历史、模型/模式目录、Canvas 工具物化、提供方与兼容指令来源读取、MCP 配置、 +远程工作区,以及本 crate 的 ACP 客户端路径。 +在这些生产路径分别获得可移植替代并证明等价前,不得宣称整个 crate 已与 Core 解耦。 ## 护栏 diff --git a/src/crates/interfaces/acp/AGENTS.md b/src/crates/interfaces/acp/AGENTS.md index 77ba2bb99e..fd6febe6d0 100644 --- a/src/crates/interfaces/acp/AGENTS.md +++ b/src/crates/interfaces/acp/AGENTS.md @@ -12,8 +12,10 @@ The CLI-hosted ACP server consumes `DeliveryProfile::Acp` through `ProductAssembler` and uses the Agent Runtime SDK for session creation/listing, active session model/mode updates, dialog submission/cancellation, interaction responses, and agent event subscription. `bitfun-acp` still depends directly on -`bitfun-core` with `product-full` for single-pass full persisted-history restore, -model/mode catalog and provider configuration reads, MCP provisioning, and the +the `bitfun-core` `agent-runtime`, `canvas-runtime`, `external-sources`, and +`ssh-remote` owner features for single-pass full persisted-history restore, +model/mode catalog, Canvas tool materialization, provider and compatible +instruction-source reads, MCP provisioning, remote workspace support, and the ACP client half of this crate. Do not describe the crate as Core-independent until those production paths have separately proven portable replacements. diff --git a/src/crates/interfaces/acp/Cargo.toml b/src/crates/interfaces/acp/Cargo.toml index 62d389c768..6563705fbf 100644 --- a/src/crates/interfaces/acp/Cargo.toml +++ b/src/crates/interfaces/acp/Cargo.toml @@ -9,7 +9,12 @@ description = "BitFun Agent Client Protocol surface" name = "bitfun_acp" [dependencies] -bitfun-core = { path = "../../assembly/core", default-features = false, features = ["product-full"] } +bitfun-core = { path = "../../assembly/core", default-features = false, features = [ + "agent-runtime", + "canvas-runtime", + "external-sources", + "ssh-remote", +] } bitfun-agent-runtime = { path = "../../execution/agent-runtime" } bitfun-agent-tools = { path = "../../execution/tool-contracts" } bitfun-events = { path = "../../contracts/events" }