diff --git a/docs/architecture/extensions/plugin-runtime-design.md b/docs/architecture/extensions/plugin-runtime-design.md index 18f07e242..599263449 100644 --- a/docs/architecture/extensions/plugin-runtime-design.md +++ b/docs/architecture/extensions/plugin-runtime-design.md @@ -285,7 +285,7 @@ plugin、Hook、完整 Client 或 TUI 插件入口。与其独立的 standalone - `cargo test -p bitfun-runtime-ports --test runtime_port_contracts plugin_runtime_diagnostics_contracts` - `cargo test -p bitfun-plugin-runtime-client` - `cargo test -p bitfun-opencode-adapter --test opencode_source_adapter` -- `cargo test -p bitfun-core plugin_runtime::tests --lib` +- `cargo test -p bitfun-core --no-default-features --features plugin-runtime --lib plugin_runtime::tests` - `node scripts/check-core-boundaries.mjs` 目标 Plugin Host 还必须使用固定版本真实 fixture 验证: diff --git a/docs/architecture/rust-build-dependency-boundaries.md b/docs/architecture/rust-build-dependency-boundaries.md index fb608230e..d1012a291 100644 --- a/docs/architecture/rust-build-dependency-boundaries.md +++ b/docs/architecture/rust-build-dependency-boundaries.md @@ -54,6 +54,12 @@ Cargo 会统一同一 package 在依赖图中的 feature;workspace dependency 入口应选择真实需要的 owner feature;`product-full` 只能描述确实需要完整产品装配的兼容入口,不能作为尚未完成 feature/owner 分解时的占位解法。缩小某个产品的 capability 集合时必须从实际 construction/command path 反推,并保留行为等价或明确 unsupported-state 测试。 +Core library 的默认 feature 集合为空;完整产品必须显式选择 `product-full`。若 interface crate 以多个 +公开角色复用一条 optional Core dependency,则 dependency 声明本身保持无 feature,每个角色 feature +分别激活 Core 并选择自己的非空 owner 闭包;兼容默认值只能组合这些已评审角色,不能重新引入 +`product-full`。每个角色必须独立编译,产品消费者还要显式关闭该 interface crate 的默认 feature 并选择 +真实使用的角色,避免 workspace feature union 掩盖边界缺口。 + Core 的 `agent-runtime` 只承载 Agent 生命周期基线和明确的基线工具,不得再次把 MCP、Remote Connect、模型目录、Browser/Web、Git/LSP 或产品工具组藏成 capability union。具体 service 由同名 owner feature 选择,内置工具由 `tools-*` 选择;`product-full` 显式相加全部 owner,CLI/ACP 等窄入口则按真实命令与构造路径列出自己的闭包。 Owner feature 不等于“无前置依赖”。当实现确实调用较低层基线时,依赖必须按 `owner → baseline` 显式组合,禁止反向把 owner 藏回基线:例如 Core MCP 工具桥和 Remote Connect 依赖 Agent 生命周期,Workspace Search 依赖本地 Workspace Runtime。每个新增或调整后的 owner 闭包都必须单独 `cargo check`,避免被 Desktop/CLI 的 feature union 偶然补齐。 diff --git a/docs/development/i18n.md b/docs/development/i18n.md index 5e254900a..367efab26 100644 --- a/docs/development/i18n.md +++ b/docs/development/i18n.md @@ -41,7 +41,7 @@ pnpm run i18n:audit | Web UI i18n runtime or namespace loading | `pnpm run i18n:contract:test && pnpm run type-check:web && pnpm --dir src/web-ui run test:run src/infrastructure/i18n/core/I18nService.test.ts` | | Mobile Web i18n runtime | `pnpm --dir src/mobile-web run type-check` | | Installer frontend i18n runtime | `pnpm --dir BitFun-Installer run type-check` | -| Backend i18n runtime | `cargo test -p bitfun-core i18n -- --nocapture` | +| Backend i18n runtime | `cargo test -p bitfun-core --no-default-features --lib i18n -- --nocapture` | Do not add process-only execution plans to version control. Keep durable rules in this file and `docs/architecture/i18n.md`; keep temporary rollout notes out diff --git a/docs/features/session-runtime-usage-report-design.md b/docs/features/session-runtime-usage-report-design.md index 08d32d3cd..2c423febd 100644 --- a/docs/features/session-runtime-usage-report-design.md +++ b/docs/features/session-runtime-usage-report-design.md @@ -689,8 +689,8 @@ Risk and drift controls: Required verification before merging P0: -- `cargo check -p bitfun-core` -- `cargo test -p bitfun-core session_usage -- --nocapture` +- `cargo check -p bitfun-core --no-default-features --features agent-runtime` +- `cargo test -p bitfun-core --no-default-features --features agent-runtime --lib session_usage -- --nocapture` - Focused CLI command tests or manual CLI smoke if no existing helper test harness exists. - `pnpm run lint:web` - `pnpm run type-check:web` @@ -1038,8 +1038,8 @@ Risks and mitigations: Verification: -- `cargo test -p bitfun-core session_usage -- --nocapture` once tests exist. -- `cargo check -p bitfun-core`. +- `cargo test -p bitfun-core --no-default-features --features agent-runtime --lib session_usage -- --nocapture` once tests exist. +- `cargo check -p bitfun-core --no-default-features --features agent-runtime`. - DTO tests for workspace identity, report scope, in-progress reports, cache-unavailable coverage, and redaction metadata. ### Task 2: Non-model-visible local report item diff --git a/docs/performance/01-compile-performance.md b/docs/performance/01-compile-performance.md index 13a506fc0..dd886aa34 100644 --- a/docs/performance/01-compile-performance.md +++ b/docs/performance/01-compile-performance.md @@ -2,7 +2,7 @@ > 最近核实:2026-08-11 > -> 实现复核基线:`gcwing/main@3d8ee4bc0` +> 实现复核基线:`gcwing/main@7345619ac` > > 性能 A/B 基线:`gcwing/main@1f538b96d` > @@ -20,7 +20,9 @@ | Agent Runtime 基线不再隐藏重型 capability | `bitfun-core/agent-runtime` 只保留生命周期和基础工具 owner;文档转换与订阅认证也改为产品显式 modifier。在最新主线 A/B 中,三平台 normal/build 闭包进一步减少 69/64/110 个版本化 package instance | | App Server 不继承未消费能力 | App Server 保持现有 Agent/Git/外部来源 handler 边界,不再因 Core 基线携带文档转换和本地订阅凭据,三平台闭包减少 61/56/78 | | SDK Host 使用显式能力闭包 | SDK Host 保留当前本机协议和工具能力,但不再通过 `product-full` 携带协议未暴露的 Remote Connect、SSH、Function Agent 等能力;Windows/macOS/Linux normal/build 闭包减少 66/68/76 | -| 完整产品行为保持 | `product-full` 显式组合全部 owner且三平台闭包不变;CLI 删除未调用适配层时显式保留原先实际生效的 Oniguruma 高亮后端,三平台闭包进一步减少 6/7/7。ACP 只退出未选择或未使用的隐含能力 | +| Core 默认值不再代表完整产品 | Core library 的默认 feature 集合为空,四条能力内部工具依赖回到实际 owner;`product-full` 仍由真实产品入口显式选择且三平台闭包不变 | +| ACP 按实际宿主拆分角色 | 兼容默认值仍为 client + server;Desktop 只选择 client,CLI 选择两者。Desktop 独立构建不再编译 ACP 的 4,211 行 server/runtime 源码,产品协议与远程行为不变 | +| 完整产品行为保持 | `product-full` 显式组合全部 owner且三平台闭包不变;CLI 删除未调用适配层时显式保留原先实际生效的 Oniguruma 高亮后端,三平台闭包进一步减少 6/7/7。ACP 默认组合保持原能力 | | Installer 删除未使用的直接能力 | 独立 manifest 的直接 dependency 从 18 降到 10,Windows normal/build 闭包减少 6;不把 Installer 并入根 workspace,本 PR 按要求不提交其生成 lockfile | | focused test 仍保持精确 | 同 owner、feature、平台和进程语义的源文件进入分组 target;使用 `--test ::` 运行单模块 | @@ -167,6 +169,31 @@ SSH、密钥和连接子图退出。完整产品 package 集合不变, | MiniApp Market | 205 → 204 | 208 → 207 | 206 → 205 | 删除服务从未消费的 `urlencoding` 直接边 | | Page Function tests | 38 → 35 | 38 → 35 | 38 → 35 | 删除同步 Rust 测试未使用的 dev-only Tokio 闭包 | +以下继续以 `gcwing/main@7345619ac` 为变更前基线,记录 Core 默认值与 ACP 角色边界收敛。三平台、 +依赖类型与去重口径与上表一致: + +| 本轮闭包 | Windows | macOS | Linux | 行为边界 | +|---|---:|---:|---:|---| +| Core 隐式默认 | 570 → 93 | 557 → 82 | 601 → 81 | library 默认不再冒充完整产品;只保留 feature-free facade 与 build dependency | +| Core `--no-default-features` | 102 → 93 | 91 → 82 | 90 → 81 | 四条 Core 直接边退出并回到实际 owner;package 集合净减 9 | +| Core `product-full` | 570 → 570 | 557 → 557 | 601 → 601 | Desktop/Server 等完整产品入口仍显式恢复全部能力 | +| ACP 默认兼容组合 | 587 → 587 | 572 → 572 | 592 → 592 | 默认仍精确组合 client + server,独立 ACP 测试与外部兼容行为不缩小 | +| Desktop | 790 → 790 | 805 → 805 | 887 → 887 | package 集合不变;ACP 仅编译 client 模块,server/runtime 4,211 行退出该 package build | +| CLI | 643 → 643 | 642 → 642 | 665 → 665 | 显式选择 ACP client + server,既有 CLI-hosted server 行为不变 | + +ACP 两个新角色的当前独立闭包为 client 397/390/391、server 533/518/539(Windows/macOS/Linux)。 +它们不能直接相加:Cargo 会对共同依赖去重。该拆分的确定收益是 Desktop 独立构建不再编译 ACP server +模块,而不是 Desktop package 数下降;因此不宣称完整 Desktop wall-clock 提速。 + +Core 空闭包减少的 9 个 package instance 主要来自 `base64` 与 `futures` 的独有子图;`regex` 和 +`tokio-util` 的 Core 直接边虽然已经移除,但 package 仍由 feature-free contracts/services 路径传递保留。 +因此本轮证明的是 direct owner 边界收敛,不能把四条 direct edge 都描述成 package 完全退出。 + +Core 的 bare/default 编译契约本轮发生了有意变化:仓内产品消费者此前已经全部关闭默认 feature 并显式 +选择 owner,因此运行行为不变;仓外若有 path/git consumer 依赖旧的隐式完整表面,需要显式选择 +`product-full`,或改为列出实际使用的 owner。该迁移属于编译期契约变化,不能描述成对未知外部 consumer +完全无影响。 + Syntect 不能机械地只删适配层:旧 feature union 同时启用 `regex-fancy` 与 `regex-onig` 时,实际由 Oniguruma 后端处理。当前 manifest 直接选择 `regex-onig`,因此运行后端、默认 syntax/theme 和 Syntect→Ratatui 样式转换保持不变,同时让未生效的 fancy 后端与未消费的 YAML loader 退出。 @@ -177,14 +204,15 @@ Package instance 会低估“同一个大 crate 少编译了多少 feature 代 只保留 Agent Runtime 实际使用的 external-subagent contract slice。Function Agent、MiniApp、 Plugin Source 由各自 owner 选择,完整产品仍经 `product-full` 显式恢复。 -根 `Cargo.lock` 从 1176 降到 1169,精确删除 `syntect-tui`、`custom_error`、`fancy-regex`、 +上一轮根 `Cargo.lock` 从 1176 降到 1169,精确删除 `syntect-tui`、`custom_error`、`fancy-regex`、 `yaml-rust`、`linked-hash-map`、`tauri-plugin-global-shortcut` 和 `global-hotkey`;没有新增、升级或 -降级 package。Installer 自己生成的 `BitFun-Installer/src-tauri/Cargo.lock` 本 PR 不提交。 +降级 package。本轮 feature/角色边界调整保持该 lockfile 字节不变,也没有新增第三方 package。 +Installer 自己生成的 `BitFun-Installer/src-tauri/Cargo.lock` 不提交。 | 状态 | 范围 | 处理结论 | |---|---|---| | 已稳定 | 根 `Cargo.lock`、Reqwest Rustls 单栈、workspace Tokio 最小基线 | 不重复治理 | -| 本轮完成 | Core Agent Runtime capability、文档转换与订阅认证 modifier、SDK Host 显式 owner closure、Installer/CLI/Desktop/Core/MiniApp Market/Page Function 未使用直接依赖 | 以真实入口 closure 收敛,不建立新的产品 umbrella;根 lock 只减少 package | +| 本轮完成 | Core 空默认与 capability-local 工具依赖、ACP client/server 角色、Core Agent Runtime capability、文档转换与订阅认证 modifier、SDK Host 显式 owner closure、Installer/CLI/Desktop/Core/MiniApp Market/Page Function 未使用直接依赖 | 以真实入口 closure 收敛,不建立新的产品 umbrella;根 lock 不增加 package | | 当前不动 | App Server / Server | 只为保持现有 handler 编译显式声明其已消费的 Core owner;不在改造稳定前继续拆其生产路径 | | 明确保留 | Desktop screenshots backend | 替换方案必须同时保持三平台坐标/权限/区域捕获语义且不增加根 lock package;当前候选不满足 | | 明确保留 | `portable-pty 0.8/0.9` | 非 OHOS 与 OHOS 的平台兼容选择,不为去重破坏 | @@ -209,6 +237,7 @@ Plugin Source 由各自 owner 选择,完整产品仍经 `product-full` 显式 | CI 拓扑 | Rust job 不再等待完整前端构建,自建 Tauri 检查所需资源目录 | | 依赖收敛 | Desktop 直接 image 版本和 Reqwest TLS 双栈已治理 | | Agent Runtime 闭包 | Core 基线不再暗带具体 capability;完整产品和 CLI 显式保持原能力,ACP 退出未选择闭包 | +| Core/ACP 默认与角色 | Core 默认 feature 为空;ACP 默认精确保持 client + server,Desktop client-only、CLI 双角色均由现有边界检查锁定 | | 重型可选能力 | 文档转换和本地订阅凭据由弱 modifier 细化已有 runtime owner;Core 基线和 App Server 退出未消费闭包 | | Installer 闭包 | 删除 8 个未使用直接 dependency;独立 workspace 和发布生命周期不变,本 PR 不提交其生成 lockfile | | SDK Host 闭包 | 从 `product-full` 改为与当前协议/构造路径一致的显式 Core owner closure;保留 ring TLS 初始化,本机 SDK 行为不变,未交付的远程执行能力不再进入构建图 | diff --git a/scripts/check-core-boundaries.test.mjs b/scripts/check-core-boundaries.test.mjs index af9eb725b..481a524b4 100644 --- a/scripts/check-core-boundaries.test.mjs +++ b/scripts/check-core-boundaries.test.mjs @@ -34,6 +34,7 @@ import { crateLayoutRules } from './core-boundaries/rules/crate-layout.mjs'; import { coreClosedFeatureProfileRules, coreProductFullFeatureAssemblyRule, + optionalDependencyFeatureOwnerRules, } from './core-boundaries/rules/feature-rules.mjs'; const ENTRYPOINT = new URL('./check-core-boundaries.mjs', import.meta.url); @@ -55,6 +56,39 @@ const MODULES = [ const TEST_ROOT = join('C:', 'repo'); +test('Core and ACP defaults preserve their explicit assembly contracts', async () => { + const [coreManifest, acpManifest] = await Promise.all([ + readFile(new URL('../src/crates/assembly/core/Cargo.toml', import.meta.url), 'utf8'), + readFile(new URL('../src/crates/interfaces/acp/Cargo.toml', import.meta.url), 'utf8'), + ]); + + assert.deepEqual(parseManifestFeatures(coreManifest).default, []); + assert.deepEqual( + new Set(parseManifestFeatures(acpManifest).default), + new Set(['client', 'server']), + ); +}); + +test('Core feature-free dependencies stay attached to their exact runtime owners', () => { + const coreOwnerRule = optionalDependencyFeatureOwnerRules.find( + (rule) => rule.crateName === 'core', + ); + const ownersByDependency = new Map( + coreOwnerRule.dependencies.map((dependency) => [ + dependency.depName, + new Set(dependency.ownerFeatures), + ]), + ); + + assert.deepEqual(ownersByDependency.get('base64'), new Set(['agent-runtime', 'dispatch-store'])); + assert.deepEqual(ownersByDependency.get('futures'), new Set(['agent-runtime'])); + assert.deepEqual(ownersByDependency.get('regex'), new Set(['agent-runtime'])); + assert.deepEqual( + ownersByDependency.get('tokio-util'), + new Set(['agent-runtime', 'debug-log']), + ); +}); + function parseManifestFeatures(manifest) { const section = manifest.match(/^\[features\]\s*$([\s\S]*?)(?=^\[|(?![\s\S]))/m)?.[1] ?? ''; const features = {}; @@ -709,6 +743,271 @@ test('explicit product entrypoint bitfun-core feature selections pass', () => { ); }); +test('Desktop must select only the ACP client role', () => { + const acp = { + ...packageAt('bitfun-acp', 'src/crates/interfaces/acp/Cargo.toml'), + features: { + default: ['client', 'server'], + client: [], + server: [], + }, + }; + const desktop = packageAt('bitfun-desktop', 'src/apps/desktop/Cargo.toml', [ + pathDependency('src/crates/interfaces/acp', { + name: 'bitfun-acp', + usesDefaultFeatures: false, + features: ['client', 'server'], + }), + ]); + + const violations = findProductEntrypointCoreFeatureViolations( + [desktop, acp], + { root: TEST_ROOT, crateLayoutRules }, + ); + + assert.equal(violations.length, 1); + assert.match(violations[0].message, /Desktop ACP role selection must not include server/); +}); + +test('ACP consumers must disable compatibility default roles', () => { + const acp = { + ...packageAt('bitfun-acp', 'src/crates/interfaces/acp/Cargo.toml'), + features: { default: ['client', 'server'], client: [], server: [] }, + }; + const desktop = packageAt('bitfun-desktop', 'src/apps/desktop/Cargo.toml', [ + pathDependency('src/crates/interfaces/acp', { + name: 'bitfun-acp', + usesDefaultFeatures: true, + features: ['client'], + }), + ]); + + const violations = findProductEntrypointCoreFeatureViolations( + [desktop, acp], + { root: TEST_ROOT, crateLayoutRules }, + ); + + assert.equal(violations.length, 1); + assert.match(violations[0].message, /must set default-features = false on every dependency/); +}); + +test('CLI must select both ACP roles explicitly', () => { + const acp = { + ...packageAt('bitfun-acp', 'src/crates/interfaces/acp/Cargo.toml'), + features: { + default: ['client', 'server'], + client: [], + server: [], + }, + }; + const cli = packageAt('bitfun-cli', 'src/apps/cli/Cargo.toml', [ + pathDependency('src/crates/interfaces/acp', { + name: 'bitfun-acp', + usesDefaultFeatures: false, + features: ['client'], + }), + ]); + + const violations = findProductEntrypointCoreFeatureViolations( + [cli, acp], + { root: TEST_ROOT, crateLayoutRules }, + ); + + assert.equal(violations.length, 1); + assert.match(violations[0].message, /CLI ACP role selection must include server/); +}); + +test('new product entrypoints must register an explicit ACP role selection', () => { + const acp = { + ...packageAt('bitfun-acp', 'src/crates/interfaces/acp/Cargo.toml'), + features: { + default: ['client', 'server'], + client: [], + server: [], + }, + }; + const newHost = packageAt('bitfun-new-host', 'src/apps/new-host/Cargo.toml', [ + pathDependency('src/crates/interfaces/acp', { + name: 'bitfun-acp', + usesDefaultFeatures: false, + features: ['client'], + }), + ]); + + const violations = findProductEntrypointCoreFeatureViolations( + [newHost, acp], + { root: TEST_ROOT, crateLayoutRules }, + ); + + assert.equal(violations.length, 1); + assert.match(violations[0].message, /must register an explicit role selection/); +}); + +test('ACP roles must be selected by an unconditional normal dependency', () => { + const acp = { + ...packageAt('bitfun-acp', 'src/crates/interfaces/acp/Cargo.toml'), + features: { + default: ['client', 'server'], + client: [], + server: [], + }, + }; + const desktop = packageAt('bitfun-desktop', 'src/apps/desktop/Cargo.toml', [ + pathDependency('src/crates/interfaces/acp', { + name: 'bitfun-acp', + kind: 'dev', + usesDefaultFeatures: false, + features: ['client'], + }), + ]); + + const violations = findProductEntrypointCoreFeatureViolations( + [desktop, acp], + { root: TEST_ROOT, crateLayoutRules }, + ); + + assert.equal(violations.length, 1); + assert.match( + violations[0].message, + /Desktop ACP role selection must keep an unconditional normal bitfun-acp dependency/, + ); +}); + +test('reviewed ACP roles require an unconditional normal dependency', () => { + const acp = { + ...packageAt('bitfun-acp', 'src/crates/interfaces/acp/Cargo.toml'), + features: { default: ['client', 'server'], client: [], server: [] }, + }; + const desktop = packageAt('bitfun-desktop', 'src/apps/desktop/Cargo.toml', [ + pathDependency('src/crates/interfaces/acp', { + name: 'bitfun-acp', + target: 'cfg(windows)', + usesDefaultFeatures: false, + features: ['client'], + }), + ]); + + const violations = findProductEntrypointCoreFeatureViolations( + [desktop, acp], + { root: TEST_ROOT, crateLayoutRules }, + ); + + assert.equal(violations.length, 1); + assert.match(violations[0].message, /must keep an unconditional normal bitfun-acp dependency/); +}); + +test('target-specific ACP edges cannot expand a reviewed product role', () => { + const acp = { + ...packageAt('bitfun-acp', 'src/crates/interfaces/acp/Cargo.toml'), + features: { default: ['client', 'server'], client: [], server: [] }, + }; + const desktop = packageAt('bitfun-desktop', 'src/apps/desktop/Cargo.toml', [ + pathDependency('src/crates/interfaces/acp', { + name: 'bitfun-acp', + usesDefaultFeatures: false, + features: ['client'], + }), + pathDependency('src/crates/interfaces/acp', { + name: 'bitfun-acp', + target: 'cfg(windows)', + usesDefaultFeatures: false, + features: ['server'], + }), + ]); + + const violations = findProductEntrypointCoreFeatureViolations( + [desktop, acp], + { root: TEST_ROOT, crateLayoutRules }, + ); + + assert.equal(violations.length, 1); + assert.match(violations[0].message, /Desktop ACP role selection must not include server/); +}); + +test('dev and build ACP edges cannot expand a reviewed product role', () => { + const acp = { + ...packageAt('bitfun-acp', 'src/crates/interfaces/acp/Cargo.toml'), + features: { default: ['client', 'server'], client: [], server: [] }, + }; + + for (const kind of ['dev', 'build']) { + const desktop = packageAt('bitfun-desktop', 'src/apps/desktop/Cargo.toml', [ + pathDependency('src/crates/interfaces/acp', { + name: 'bitfun-acp', + usesDefaultFeatures: false, + features: ['client'], + }), + pathDependency('src/crates/interfaces/acp', { + name: 'bitfun-acp', + kind, + usesDefaultFeatures: false, + features: ['server'], + }), + ]); + + const violations = findProductEntrypointCoreFeatureViolations( + [desktop, acp], + { root: TEST_ROOT, crateLayoutRules }, + ); + + assert.equal(violations.length, 1, `${kind} dependency must not widen Desktop ACP roles`); + assert.match(violations[0].message, /Desktop ACP role selection must not include server/); + } +}); + +test('reviewed ACP product dependencies must not become optional', () => { + const acp = { + ...packageAt('bitfun-acp', 'src/crates/interfaces/acp/Cargo.toml'), + features: { default: ['client', 'server'], client: [], server: [] }, + }; + const desktop = packageAt('bitfun-desktop', 'src/apps/desktop/Cargo.toml', [ + pathDependency('src/crates/interfaces/acp', { + name: 'bitfun-acp', + optional: true, + usesDefaultFeatures: false, + features: ['client'], + }), + ]); + + const violations = findProductEntrypointCoreFeatureViolations( + [desktop, acp], + { root: TEST_ROOT, crateLayoutRules }, + ); + + assert.equal(violations.length, 2); + assert.match(violations[0].message, /must keep an unconditional normal bitfun-acp dependency/); + assert.match(violations[1].message, /must not make a bitfun-acp dependency optional/); +}); + +test('target, dev, and build ACP consumers must still register their role selection', () => { + const acp = { + ...packageAt('bitfun-acp', 'src/crates/interfaces/acp/Cargo.toml'), + features: { default: ['client', 'server'], client: [], server: [] }, + }; + for (const dependency of [ + { target: 'cfg(windows)' }, + { kind: 'dev' }, + { kind: 'build' }, + ]) { + const newHost = packageAt('bitfun-new-host', 'src/apps/new-host/Cargo.toml', [ + pathDependency('src/crates/interfaces/acp', { + name: 'bitfun-acp', + ...dependency, + usesDefaultFeatures: false, + features: ['client'], + }), + ]); + + const violations = findProductEntrypointCoreFeatureViolations( + [newHost, acp], + { root: TEST_ROOT, crateLayoutRules }, + ); + + assert.equal(violations.length, 1); + assert.match(violations[0].message, /must register an explicit role selection/); + } +}); + const SDK_HOST_REVIEWED_CORE_FEATURES = [ 'agent-runtime', 'document-read', diff --git a/scripts/core-boundaries/cargo-dependency-boundaries.mjs b/scripts/core-boundaries/cargo-dependency-boundaries.mjs index 7da696362..aa59a907a 100644 --- a/scripts/core-boundaries/cargo-dependency-boundaries.mjs +++ b/scripts/core-boundaries/cargo-dependency-boundaries.mjs @@ -2,7 +2,11 @@ import { readFileSync, readdirSync } from 'node:fs'; import { isAbsolute, join, relative, resolve } from 'node:path'; import { spawnSync } from 'node:child_process'; -import { servicesReqwestOwnerFeatures } from './rules/feature-rules.mjs'; +import { + acpClientCoreFeatures, + acpServerCoreFeatures, + servicesReqwestOwnerFeatures, +} from './rules/feature-rules.mjs'; const SKIPPED_DIRECTORIES = new Set([ '.git', @@ -833,7 +837,7 @@ export function findProductEntrypointCoreFeatureViolations( 'plugin-runtime', 'ssh-remote', ]], - ['bitfun-acp', [...coreCompatibilityReviewedFeatures, 'ssh-remote']], + ['bitfun-acp', [...new Set([...acpClientCoreFeatures, ...acpServerCoreFeatures])]], ['bitfun-app-server', [ 'external-sources', 'git', @@ -936,6 +940,109 @@ export function findProductEntrypointCoreFeatureViolations( ); const violations = []; + const reviewedAcpRoleSelections = new Map([ + ['bitfun-cli', { + label: 'CLI', + requiredFeatures: ['client', 'server'], + }], + ['bitfun-desktop', { + label: 'Desktop', + requiredFeatures: ['client'], + }], + ]); + const acpPackage = packages.find((pkg) => pkg.name === 'bitfun-acp'); + if (acpPackage) { + const reviewedConsumersFound = new Set(); + for (const sourcePackage of packages) { + const declaredDependencies = (sourcePackage.dependencies ?? []).filter((candidate) => { + if (!candidate.path) { + return false; + } + return packageByManifest.get( + normalizedPath(join(candidate.path, 'Cargo.toml')), + )?.name === 'bitfun-acp'; + }); + const normalDependencies = declaredDependencies.filter( + (dependency) => dependency.kind === null, + ); + const rule = reviewedAcpRoleSelections.get(sourcePackage.name); + if (!rule) { + if (declaredDependencies.length > 0) { + violations.push({ + path: sourcePackage.manifest_path, + line: 1, + message: `bitfun-acp consumer ${sourcePackage.name} must register an explicit role selection`, + }); + } + continue; + } + if (declaredDependencies.length === 0) { + continue; + } + reviewedConsumersFound.add(sourcePackage.name); + const unconditionalDependencies = normalDependencies.filter( + (dependency) => dependency.target === null && dependency.optional !== true, + ); + if (unconditionalDependencies.length === 0) { + violations.push({ + path: sourcePackage.manifest_path, + line: 1, + message: `${rule.label} ACP role selection must keep an unconditional normal bitfun-acp dependency`, + }); + } + if (declaredDependencies.some((dependency) => dependency.optional === true)) { + violations.push({ + path: sourcePackage.manifest_path, + line: 1, + message: `${rule.label} ACP role selection must not make a bitfun-acp dependency optional`, + }); + } + if (declaredDependencies.some((dependency) => dependency.uses_default_features !== false)) { + violations.push({ + path: sourcePackage.manifest_path, + line: 1, + message: `${rule.label} ACP role selection must set default-features = false on every dependency`, + }); + } + const unconditionalFeatures = new Set( + unconditionalDependencies.flatMap((dependency) => dependency.features ?? []), + ); + const selectedFeatures = new Set( + declaredDependencies.flatMap((dependency) => dependency.features ?? []), + ); + if (unconditionalDependencies.length > 0) { + for (const requiredFeature of rule.requiredFeatures) { + if (!unconditionalFeatures.has(requiredFeature)) { + violations.push({ + path: sourcePackage.manifest_path, + line: 1, + message: `${rule.label} ACP role selection must include ${requiredFeature}`, + }); + } + } + } + for (const selectedFeature of selectedFeatures) { + if (!rule.requiredFeatures.includes(selectedFeature)) { + violations.push({ + path: sourcePackage.manifest_path, + line: 1, + message: `${rule.label} ACP role selection must not include ${selectedFeature}`, + }); + } + } + } + for (const [sourceName, rule] of reviewedAcpRoleSelections) { + const sourcePackage = packages.find((pkg) => pkg.name === sourceName); + if (sourcePackage && !reviewedConsumersFound.has(sourceName)) { + violations.push({ + path: sourcePackage.manifest_path, + line: 1, + message: `${rule.label} ACP role selection must keep the bitfun-acp dependency`, + }); + } + } + } + for (const sourcePackage of packages) { const sourceLayer = layerForManifest(sourcePackage.manifest_path, { root, @@ -962,14 +1069,21 @@ export function findProductEntrypointCoreFeatureViolations( message: `product entrypoint ${sourcePackage.name} must set default-features = false for its bitfun-core ${dependencyDescription(dependency)}`, }); } - if (!Array.isArray(dependency.features) || dependency.features.length === 0) { + const roleOwnedAcpDependency = + sourcePackage.name === 'bitfun-acp' && dependency.optional === true; + if ( + !roleOwnedAcpDependency + && (!Array.isArray(dependency.features) || dependency.features.length === 0) + ) { violations.push({ path: sourcePackage.manifest_path, line: 1, message: `product entrypoint ${sourcePackage.name} must select at least one explicit feature for its bitfun-core ${dependencyDescription(dependency)}`, }); } - const reviewedClosure = reviewedCoreFeatureClosures.get(sourcePackage.name); + const reviewedClosure = roleOwnedAcpDependency + ? undefined + : reviewedCoreFeatureClosures.get(sourcePackage.name); if (reviewedClosure) { const selectedFeatures = new Set(dependency.features ?? []); for (const requiredFeature of reviewedClosure) { diff --git a/scripts/core-boundaries/checker.mjs b/scripts/core-boundaries/checker.mjs index d42ed7b39..7c8206fa1 100644 --- a/scripts/core-boundaries/checker.mjs +++ b/scripts/core-boundaries/checker.mjs @@ -15,6 +15,7 @@ import { } from './rules/crate-layout.mjs'; import { checkTuiLegacyBackendRatchet } from './tui-boundary-ratchet.mjs'; import { + acpClosedFeatureProfileRules, coreClosedFeatureProfileRules, coreProductFullFeatureAssemblyRule, optionalDependencyFeatureOwnerRules, @@ -585,19 +586,6 @@ function checkOptionalDependencyFeatureOwners(crateDir, rule) { } } -function checkCoreDefaultProductFullFeature() { - const manifestPath = join(crateDirForName('core'), 'Cargo.toml'); - const features = parseManifestFeatures(readText(manifestPath).split(/\r?\n/)); - if (!featureReferencesFeature(features.get('default'), 'product-full')) { - failures.push({ - path: manifestPath, - line: features.get('default')?.line ?? 1, - message: - 'bitfun-core default feature must remain product-full until a separate product matrix review changes it', - }); - } -} - function checkCoreProductFullFeatureAssembly(rule) { const manifestPath = repoPathToFsPath(rule.manifestPath); const features = parseManifestFeatures(readText(manifestPath).split(/\r?\n/)); @@ -1092,6 +1080,7 @@ export function runCoreBoundaryCheck() { manifestDependencyMatches, matchingForbiddenDependency, coreClosedFeatureProfileRules, + acpClosedFeatureProfileRules, coreProductFullFeatureAssemblyRule, ownerCrateFeatureAssemblyRules, parseManifestFeatures, @@ -1158,11 +1147,13 @@ export function runCoreBoundaryCheck() { checkOptionalDependencyFeatureOwners(crateDir, rule); } - checkCoreDefaultProductFullFeature(); checkCoreProductFullFeatureAssembly(coreProductFullFeatureAssemblyRule); for (const rule of coreClosedFeatureProfileRules) { checkClosedFeatureProfile(rule); } + for (const rule of acpClosedFeatureProfileRules) { + checkClosedFeatureProfile(rule); + } for (const rule of ownerCrateFeatureAssemblyRules) { checkOwnerCrateFeatureAssembly(rule); } diff --git a/scripts/core-boundaries/rules/feature-rules.mjs b/scripts/core-boundaries/rules/feature-rules.mjs index d21a30da3..b27d020ee 100644 --- a/scripts/core-boundaries/rules/feature-rules.mjs +++ b/scripts/core-boundaries/rules/feature-rules.mjs @@ -67,6 +67,7 @@ export const optionalDependencyFeatureOwnerRules = [ 'bitfun-core product/runtime optional dependencies must stay owned by explicit feature gates', dependencies: [ { depName: 'axum', ownerFeatures: ['debug-log', 'mcp-runtime'] }, + { depName: 'base64', ownerFeatures: ['agent-runtime', 'dispatch-store'] }, { depName: 'bitfun-ai-adapters', ownerFeatures: ['ai-adapter-runtime', 'subscription-auth'], @@ -139,11 +140,13 @@ export const optionalDependencyFeatureOwnerRules = [ { depName: 'filetime', ownerFeatures: ['agent-runtime'] }, { depName: 'flate2', ownerFeatures: ['agent-runtime'] }, { depName: 'fs2', ownerFeatures: ['agent-runtime'] }, + { depName: 'futures', ownerFeatures: ['agent-runtime'] }, { depName: 'image', ownerFeatures: ['agent-runtime'] }, { depName: 'include_dir', ownerFeatures: ['agent-runtime'] }, { depName: 'indexmap', ownerFeatures: ['agent-runtime'] }, { depName: 'md5', ownerFeatures: ['agent-runtime'] }, { depName: 'reqwest', ownerFeatures: ['mcp-runtime', 'tools-miniapp'] }, + { depName: 'regex', ownerFeatures: ['agent-runtime'] }, { depName: 'rusqlite', ownerFeatures: ['agent-runtime'] }, { depName: 'semver', ownerFeatures: ['tools-miniapp'] }, { depName: 'serde_yaml', ownerFeatures: ['workspace-runtime'] }, @@ -151,6 +154,7 @@ export const optionalDependencyFeatureOwnerRules = [ { depName: 'terminal-core', ownerFeatures: ['terminal'] }, { depName: 'notify', ownerFeatures: ['lsp', 'workspace-watch'] }, { depName: 'tokio-tungstenite', ownerFeatures: ['browser-control'] }, + { depName: 'tokio-util', ownerFeatures: ['agent-runtime', 'debug-log'] }, { depName: 'tower-http', ownerFeatures: ['debug-log'] }, { depName: 'tool-runtime', @@ -292,7 +296,72 @@ export const coreProductFullFeatureAssemblyRule = { reason: 'bitfun-core product-full must explicitly assemble current owner feature groups', }; +export const acpClientCoreFeatures = [ + 'agent-runtime', + 'ssh-remote', +]; + +export const acpServerCoreFeatures = [ + 'agent-runtime', + 'document-read', + 'subscription-auth', + 'deep-research', + 'lsp', + 'external-sources', + 'tools-basic', + 'tools-git', + 'tools-mcp', + 'tools-browser-web', + 'tools-computer-use', + 'tools-image-analysis', + 'tools-miniapp', + 'tools-canvas', + 'tools-agent-control', +]; + +export const acpClosedFeatureProfileRules = [ + { + manifestPath: 'src/crates/interfaces/acp/Cargo.toml', + featureName: 'default', + requiredFeatureRefs: ['client', 'server'], + exact: true, + reason: 'bitfun-acp default must preserve its complete client and server compatibility surface', + }, + { + manifestPath: 'src/crates/interfaces/acp/Cargo.toml', + featureName: 'client', + requiredFeatureRefs: [ + 'dep:futures', + 'dep:serde', + 'dep:bitfun-core', + ...acpClientCoreFeatures.map((feature) => `bitfun-core/${feature}`), + ], + exact: true, + reason: 'bitfun-acp client must own only external ACP agent and SSH transport capabilities', + }, + { + manifestPath: 'src/crates/interfaces/acp/Cargo.toml', + featureName: 'server', + requiredFeatureRefs: [ + 'dep:bitfun-agent-runtime', + 'dep:bitfun-core-types', + 'dep:bitfun-core', + 'dep:sha2', + ...acpServerCoreFeatures.map((feature) => `bitfun-core/${feature}`), + ], + exact: true, + reason: 'bitfun-acp server must preserve the reviewed Agent Runtime capability surface without SSH transport', + }, +]; + export const coreClosedFeatureProfileRules = [ + { + manifestPath: 'src/crates/assembly/core/Cargo.toml', + featureName: 'default', + requiredFeatureRefs: [], + exact: true, + reason: 'bitfun-core default must stay empty so product entrypoints select capabilities explicitly', + }, { manifestPath: 'src/crates/assembly/core/Cargo.toml', featureName: 'agent-runtime', @@ -301,17 +370,21 @@ export const coreClosedFeatureProfileRules = [ 'dep:bitfun-agent-runtime', 'dep:bitfun-agent-content', 'dep:bitfun-agent-stream', + 'dep:base64', 'dep:bitfun-harness', 'dep:dashmap', 'dep:filetime', 'dep:flate2', 'dep:fs2', + 'dep:futures', 'dep:include_dir', 'dep:indexmap', 'dep:image', 'dep:md5', 'dep:rusqlite', + 'dep:regex', 'dep:similar', + 'dep:tokio-util', 'dep:tool-runtime', 'bitfun-services-core/permission', 'bitfun-services-core/runtime-ownership', @@ -697,6 +770,7 @@ export const coreClosedFeatureProfileRules = [ featureName: 'debug-log', requiredFeatureRefs: [ 'dep:axum', + 'dep:tokio-util', 'dep:tower-http', 'bitfun-services-integrations/debug-log', ], @@ -823,7 +897,11 @@ export const coreClosedFeatureProfileRules = [ { manifestPath: 'src/crates/assembly/core/Cargo.toml', featureName: 'dispatch-store', - requiredFeatureRefs: ['local-storage', 'bitfun-services-core/dispatch-workspace'], + requiredFeatureRefs: [ + 'dep:base64', + 'local-storage', + 'bitfun-services-core/dispatch-workspace', + ], exact: true, reason: 'bitfun-core dispatch-store must expose only the durable dispatch index facade', }, diff --git a/scripts/core-boundaries/self-test.mjs b/scripts/core-boundaries/self-test.mjs index 1f93445ef..51a4b9416 100644 --- a/scripts/core-boundaries/self-test.mjs +++ b/scripts/core-boundaries/self-test.mjs @@ -7,6 +7,7 @@ export function runManifestParserSelfTest({ parseManifestDependencies, manifestDependencyMatches, matchingForbiddenDependency, + acpClosedFeatureProfileRules, coreClosedFeatureProfileRules, coreProductFullFeatureAssemblyRule, ownerCrateFeatureAssemblyRules, @@ -399,6 +400,7 @@ export function runManifestParserSelfTest({ const coreManifest = 'src/crates/assembly/core/Cargo.toml'; const servicesCoreManifest = 'src/crates/services/services-core/Cargo.toml'; const expectedClosedCoreProfiles = [ + [coreManifest, 'default', []], [servicesCoreManifest, 'default', []], [ servicesCoreManifest, @@ -472,7 +474,11 @@ export function runManifestParserSelfTest({ ], [servicesCoreManifest, 'session-git', ['local-storage', 'dep:git2']], [servicesCoreManifest, 'workspace-identity', ['dep:dunce', 'dep:sha2']], - [coreManifest, 'dispatch-store', ['local-storage', 'bitfun-services-core/dispatch-workspace']], + [ + coreManifest, + 'dispatch-store', + ['dep:base64', 'local-storage', 'bitfun-services-core/dispatch-workspace'], + ], [coreManifest, 'filesystem', ['bitfun-services-core/filesystem']], [coreManifest, 'local-storage', ['bitfun-services-core/local-storage']], [coreManifest, 'process-runtime', ['bitfun-services-core/process-runtime']], @@ -539,6 +545,61 @@ export function runManifestParserSelfTest({ throw new Error(`core closed feature profile must not reach product-full in ${featureName}`); } } + const acpProfiles = new Map( + acpClosedFeatureProfileRules.map((rule) => [rule.featureName, rule]), + ); + const expectedAcpProfiles = new Map([ + ['default', ['client', 'server']], + [ + 'client', + [ + 'dep:futures', + 'dep:serde', + 'dep:bitfun-core', + 'bitfun-core/agent-runtime', + 'bitfun-core/ssh-remote', + ], + ], + [ + 'server', + [ + 'dep:bitfun-agent-runtime', + 'dep:bitfun-core-types', + 'dep:bitfun-core', + 'dep:sha2', + 'bitfun-core/agent-runtime', + 'bitfun-core/document-read', + 'bitfun-core/subscription-auth', + 'bitfun-core/deep-research', + 'bitfun-core/lsp', + 'bitfun-core/external-sources', + 'bitfun-core/tools-basic', + 'bitfun-core/tools-git', + 'bitfun-core/tools-mcp', + 'bitfun-core/tools-browser-web', + 'bitfun-core/tools-computer-use', + 'bitfun-core/tools-image-analysis', + 'bitfun-core/tools-miniapp', + 'bitfun-core/tools-canvas', + 'bitfun-core/tools-agent-control', + ], + ], + ]); + for (const [featureName, expectedReferences] of expectedAcpProfiles) { + const rule = acpProfiles.get(featureName); + if (!rule?.exact) { + throw new Error(`ACP closed feature profile must cover ${featureName} exactly`); + } + if ( + rule.requiredFeatureRefs.length !== expectedReferences.length + || expectedReferences.some((reference) => !rule.requiredFeatureRefs.includes(reference)) + ) { + throw new Error(`ACP closed feature profile has stale references for ${featureName}`); + } + if (rule.requiredFeatureRefs.some((reference) => reference.includes('product-full'))) { + throw new Error(`ACP closed feature profile must not reach product-full in ${featureName}`); + } + } const ownerFeatureRulePaths = new Set( ownerCrateFeatureAssemblyRules.map((rule) => rule.manifestPath), ); diff --git a/src/apps/cli/Cargo.toml b/src/apps/cli/Cargo.toml index 8726604c0..3c56682b4 100644 --- a/src/apps/cli/Cargo.toml +++ b/src/apps/cli/Cargo.toml @@ -52,7 +52,7 @@ bitfun-core = { path = "../../crates/assembly/core", default-features = false, f ] } bitfun-events = { path = "../../crates/contracts/events" } bitfun-core-types = { path = "../../crates/contracts/core-types" } -bitfun-acp = { path = "../../crates/interfaces/acp" } +bitfun-acp = { path = "../../crates/interfaces/acp", default-features = false, features = ["client", "server"] } bitfun-agent-runtime = { path = "../../crates/execution/agent-runtime" } bitfun-agent-runtime-ipc = { path = "../../crates/adapters/agent-runtime-ipc" } bitfun-runtime-ports = { path = "../../crates/contracts/runtime-ports" } diff --git a/src/apps/desktop/Cargo.toml b/src/apps/desktop/Cargo.toml index c7077c423..506564459 100644 --- a/src/apps/desktop/Cargo.toml +++ b/src/apps/desktop/Cargo.toml @@ -30,7 +30,7 @@ bitfun-agent-tools = { path = "../../crates/execution/tool-contracts" } bitfun-transport = { path = "../../crates/adapters/transport", features = ["tauri-adapter"] } bitfun-events = { path = "../../crates/contracts/events" } bitfun-webdriver = { path = "../../crates/adapters/webdriver" } -bitfun-acp = { path = "../../crates/interfaces/acp" } +bitfun-acp = { path = "../../crates/interfaces/acp", default-features = false, features = ["client"] } # Tauri tauri = { workspace = true } diff --git a/src/crates/assembly/core/AGENTS-CN.md b/src/crates/assembly/core/AGENTS-CN.md index d9c9eab93..2948ea664 100644 --- a/src/crates/assembly/core/AGENTS-CN.md +++ b/src/crates/assembly/core/AGENTS-CN.md @@ -50,9 +50,11 @@ SessionManager -> Session -> DialogTurn -> ModelRound - 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`。 +- `agent-runtime` 只负责 Core Agent 生命周期基线、原生 Hook runtime、基础文件/进程工具和 + Agent-control 工具。`mcp-runtime`、`remote-connect`、`workspace-search` 等具体网络或产品能力 + 保持独立 owner;`external-sources` 增加第三方发现/导入 adapter,`plugin-runtime` 增加可执行 + plugin client wiring,`debug-log` 单独控制调试日志服务。不得把这些能力藏回 `agent-runtime`, + 它们也都不得启用 `product-full`。 - CLI/ACP 的闭包检查遵循 Cargo resolver-v2,保持 normal 与 host(build/proc-macro)feature context 相互隔离; 但同一 context 内的所有 target-specific 声明都属于同一个已评审架构边界。平台确实需要不同 owner 时,应拆分清晰的 package/module 归属;不得用互斥 Cargo `cfg` 隐藏未评审的 Core 能力。 @@ -63,6 +65,10 @@ SessionManager -> Session -> DialogTurn -> ModelRound - `product-full` 必须显式组合自身消费的每个能力,包括 `permission`、`session-git`、 `runtime-ownership` 等产品专属 `services-core` feature。不得把这些 feature 写在依赖声明上, 否则 Cargo feature union 会迫使所有 core consumer 编译它们。 +- Core 的默认 feature 集合为空。`product-full` 是由真实产品入口显式选择的兼容组装,不能再作为 + library 的隐式默认值。能力内部使用的工具依赖必须保持 optional 并由 owner feature 激活; + `base64`、`futures`、`regex` 与 `tokio-util` 分别归实际使用它们的 Agent Runtime、 + dispatch-store 或 debug-log 闭包。 - 保持 `cargo check -p bitfun-core --no-default-features` 可用。产品专属模块必须由 owner feature 控制;轻量 facade 操作在缺少产品 owner 时若无法安全完成,应明确 fail-closed 并保留持久化恢复状态,不得隐式启用 `product-full`。 diff --git a/src/crates/assembly/core/AGENTS.md b/src/crates/assembly/core/AGENTS.md index 8651359ce..a409f75b0 100644 --- a/src/crates/assembly/core/AGENTS.md +++ b/src/crates/assembly/core/AGENTS.md @@ -124,6 +124,12 @@ SessionManager -> Session -> DialogTurn -> ModelRound `runtime-ownership`, every concrete service owner, and every `tools-*` group. Do not put those features on the dependency declaration, because Cargo feature union would force them into every core consumer. +- Core's default feature set is empty. `product-full` is an explicit + compatibility assembly selected by real product entrypoints, never the + library's implicit default. Capability-local utility dependencies remain + optional and are activated by their owner features; in particular, + `base64`, `futures`, `regex`, and `tokio-util` belong to the Agent Runtime, + dispatch-store, or debug-log closures that use them. - Keep `cargo check -p bitfun-core --no-default-features` viable. Gate product-only modules at their owner feature; if a light facade operation cannot safely complete without a product owner, fail closed and preserve any diff --git a/src/crates/assembly/core/Cargo.toml b/src/crates/assembly/core/Cargo.toml index 6e2f70a06..aed7f6748 100644 --- a/src/crates/assembly/core/Cargo.toml +++ b/src/crates/assembly/core/Cargo.toml @@ -12,9 +12,9 @@ crate-type = ["rlib"] [dependencies] # Inherit shared dependencies from workspace tokio = { workspace = true, features = ["fs", "io-util", "macros", "net", "rt", "sync", "time"] } -tokio-util = { workspace = true } +tokio-util = { workspace = true, optional = true } async-trait = { workspace = true } -futures = { workspace = true } +futures = { workspace = true, optional = true } serde = { workspace = true } serde_json = { workspace = true } @@ -30,8 +30,8 @@ uuid = { workspace = true } chrono = { workspace = true } chrono-tz = { workspace = true, optional = true } cron = { workspace = true, optional = true } -regex = { workspace = true } -base64 = { workspace = true } +regex = { workspace = true, optional = true } +base64 = { workspace = true, optional = true } image = { workspace = true, optional = true } md5 = { workspace = true, optional = true } hex = { workspace = true } @@ -129,7 +129,7 @@ bitfun-transport = { path = "../../adapters/transport" } # Full product runtime compatibility set. Product entrypoints with a narrower # construction path select stable owner features instead of inheriting this # union. -default = ["product-full"] +default = [] # Derive `ts_rs::TS` on selected wire types (e.g. `AgentProfileView`) for # downstream TypeScript binding export. Propagates `ts` to the upstream crates # whose types core re-exports (git types live in `services-integrations`, @@ -197,17 +197,21 @@ agent-runtime = [ "dep:bitfun-agent-runtime", "dep:bitfun-agent-content", "dep:bitfun-agent-stream", + "dep:base64", "dep:bitfun-harness", "dep:dashmap", "dep:filetime", "dep:flate2", "dep:fs2", + "dep:futures", "dep:include_dir", "dep:indexmap", "dep:image", "dep:md5", "dep:rusqlite", + "dep:regex", "dep:similar", + "dep:tokio-util", "dep:tool-runtime", "bitfun-services-core/permission", "bitfun-services-core/runtime-ownership", @@ -334,6 +338,7 @@ plugin-runtime = [ ] debug-log = [ "dep:axum", + "dep:tokio-util", "dep:tower-http", "bitfun-services-integrations/debug-log", ] @@ -363,7 +368,7 @@ file-watch = ["bitfun-services-integrations/file-watch"] git = ["bitfun-services-integrations/git"] review-platform = ["bitfun-services-integrations/review-platform"] service-integrations = ["announcement", "file-watch", "git", "review-platform"] -dispatch-store = ["local-storage", "bitfun-services-core/dispatch-workspace"] +dispatch-store = ["dep:base64", "local-storage", "bitfun-services-core/dispatch-workspace"] filesystem = ["bitfun-services-core/filesystem"] local-storage = ["bitfun-services-core/local-storage"] process-runtime = ["bitfun-services-core/process-runtime"] diff --git a/src/crates/assembly/core/src/agentic/deep_review/AGENTS.md b/src/crates/assembly/core/src/agentic/deep_review/AGENTS.md index 449fdf91a..d0e33b581 100644 --- a/src/crates/assembly/core/src/agentic/deep_review/AGENTS.md +++ b/src/crates/assembly/core/src/agentic/deep_review/AGENTS.md @@ -33,7 +33,7 @@ Use the nearest Web UI check for frontend-only behavior. For shared runtime behavior, run: ```bash -cargo test -p bitfun-core deep_review -- --nocapture +cargo test -p bitfun-core --no-default-features --features agent-runtime --lib deep_review -- --nocapture ``` Also run the relevant Rust or desktop check when the change touches backend diff --git a/src/crates/assembly/core/src/infrastructure/debug_log/types.rs b/src/crates/assembly/core/src/infrastructure/debug_log/types.rs index 222981182..250f15f27 100644 --- a/src/crates/assembly/core/src/infrastructure/debug_log/types.rs +++ b/src/crates/assembly/core/src/infrastructure/debug_log/types.rs @@ -4,6 +4,7 @@ //! and writes them to the local NDJSON log file. use super::{DebugLogConfig, DebugLogEntry}; +#[cfg(feature = "workspace-runtime")] use crate::service::workspace::get_global_workspace_service; use anyhow::Result; use log::debug; @@ -108,14 +109,16 @@ pub async fn handle_ingest( request: IngestLogRequest, config: &DebugLogConfig, ) -> Result { + let log_config = config.clone(); + #[cfg(feature = "workspace-runtime")] let log_config = if let Some(workspace_path) = get_global_workspace_service().and_then(|service| service.try_get_current_workspace_path()) { - let mut cfg = config.clone(); + let mut cfg = log_config; cfg.log_path = workspace_path.join(".bitfun").join("debug.log"); cfg } else { - config.clone() + log_config }; let entry: DebugLogEntry = request.into(); @@ -128,3 +131,38 @@ pub async fn handle_ingest( error: None, }) } + +#[cfg(all(test, not(feature = "workspace-runtime")))] +mod tests { + use super::*; + + #[tokio::test] + async fn standalone_debug_log_uses_the_configured_path() { + let temp = tempfile::tempdir().expect("temp directory"); + let log_path = temp.path().join("standalone").join("debug.log"); + let config = DebugLogConfig { + log_path: log_path.clone(), + ingest_url: None, + session_id: "standalone-session".to_string(), + }; + + handle_ingest( + IngestLogRequest { + location: "standalone-test".to_string(), + message: "uses configured path".to_string(), + data: serde_json::Value::Null, + session_id: None, + run_id: None, + hypothesis_id: None, + timestamp: Some(1), + }, + &config, + ) + .await + .expect("ingest succeeds without the workspace runtime"); + + let contents = std::fs::read_to_string(log_path).expect("configured log is written"); + assert!(contents.contains("\"location\":\"standalone-test\"")); + assert!(contents.contains("\"sessionId\":\"standalone-session\"")); + } +} diff --git a/src/crates/interfaces/acp/AGENTS-CN.md b/src/crates/interfaces/acp/AGENTS-CN.md index f71972f39..51392ff76 100644 --- a/src/crates/interfaces/acp/AGENTS-CN.md +++ b/src/crates/interfaces/acp/AGENTS-CN.md @@ -6,12 +6,11 @@ `bitfun-acp` 负责基于已组装产品 runtime 的 Agent Client Protocol 入口与 ACP client 行为。ACP protocol / client 细节留在这里或应用入口 adapter 中;跨层只共享稳定 capability facts。 -CLI 托管的 ACP 服务端已通过 `ProductAssembler` 消费 `DeliveryProfile::Acp`,并使用 Agent Runtime SDK -完成会话创建/列举、活动会话模型/模式更新、轮次提交/取消、交互响应和 Agent 事件订阅。`bitfun-acp` 仍直接依赖 -`bitfun-core` 的 `agent-runtime`、`canvas-runtime`、`external-sources` 与 `ssh-remote` owner feature, -用于一次性恢复完整持久化历史、模型/模式目录、Canvas 工具物化、提供方与兼容指令来源读取、MCP 配置、 -远程工作区,以及本 crate 的 ACP 客户端路径。 -在这些生产路径分别获得可移植替代并证明等价前,不得宣称整个 crate 已与 Core 解耦。 +本 crate 暴露两个可加性的角色 feature。`client` 负责 ACP 进程发现、配置、远程探测、会话传输和 +tool-card 投影,选择 Core Agent Runtime 与具体 SSH 能力;`server` 负责 CLI 托管的 ACP 服务端, +通过 `DeliveryProfile::Acp` 投影 runtime,并精确选择该路径使用的工具、文档、订阅、LSP 与外部来源 owner, +但不选择 SSH。兼容默认值必须精确等于 `client + server`;Desktop 只选择 `client`,CLI 选择两者。 +两个角色保持加法语义,不得用 `product-full` 替代任一闭包。 ## 护栏 @@ -24,6 +23,7 @@ CLI 托管的 ACP 服务端已通过 `ProductAssembler` 消费 `DeliveryProfile: ## 验证 ```bash -cargo check -p bitfun-acp +cargo check -p bitfun-acp --no-default-features --features client +cargo check -p bitfun-acp --no-default-features --features server cargo test -p bitfun-acp ``` diff --git a/src/crates/interfaces/acp/AGENTS.md b/src/crates/interfaces/acp/AGENTS.md index fd6febe6d..a695abeb4 100644 --- a/src/crates/interfaces/acp/AGENTS.md +++ b/src/crates/interfaces/acp/AGENTS.md @@ -8,16 +8,15 @@ Scope: this guide applies to `src/crates/interfaces/acp`. runtime. Keep ACP protocol/client details here or in app-surface adapters; share only stable capability facts through contract crates. -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 -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. +The crate exposes two additive roles. `client` owns ACP process discovery, +configuration, remote probing, session transport, and tool-card projection; it +selects the Core Agent Runtime plus concrete SSH support. `server` owns the +CLI-hosted ACP server and runtime projection through `DeliveryProfile::Acp`; it +selects the exact Core tool, document, subscription, LSP, and external-source +owners used by that path, but does not select SSH. The compatibility default is +exactly `client + server`. Desktop selects only `client`; CLI selects both. +Keep these role features additive and do not replace either closure with +`product-full`. ## Guardrails @@ -39,6 +38,7 @@ until those production paths have separately proven portable replacements. ## Verification ```bash -cargo check -p bitfun-acp +cargo check -p bitfun-acp --no-default-features --features client +cargo check -p bitfun-acp --no-default-features --features server cargo test -p bitfun-acp ``` diff --git a/src/crates/interfaces/acp/Cargo.toml b/src/crates/interfaces/acp/Cargo.toml index 9cae80839..567324329 100644 --- a/src/crates/interfaces/acp/Cargo.toml +++ b/src/crates/interfaces/acp/Cargo.toml @@ -5,45 +5,59 @@ authors.workspace = true edition.workspace = true description = "BitFun Agent Client Protocol surface" +[features] +default = ["client", "server"] +client = [ + "dep:futures", + "dep:serde", + "dep:bitfun-core", + "bitfun-core/agent-runtime", + "bitfun-core/ssh-remote", +] +server = [ + "dep:bitfun-agent-runtime", + "dep:bitfun-core-types", + "dep:bitfun-core", + "dep:sha2", + "bitfun-core/agent-runtime", + "bitfun-core/document-read", + "bitfun-core/subscription-auth", + "bitfun-core/deep-research", + "bitfun-core/lsp", + "bitfun-core/external-sources", + "bitfun-core/tools-basic", + "bitfun-core/tools-git", + "bitfun-core/tools-mcp", + "bitfun-core/tools-browser-web", + "bitfun-core/tools-computer-use", + "bitfun-core/tools-image-analysis", + "bitfun-core/tools-miniapp", + "bitfun-core/tools-canvas", + "bitfun-core/tools-agent-control", +] + [lib] name = "bitfun_acp" [dependencies] -bitfun-core = { path = "../../assembly/core", default-features = false, features = [ - "agent-runtime", - "document-read", - "subscription-auth", - "deep-research", - "lsp", - "external-sources", - "ssh-remote", - "tools-basic", - "tools-git", - "tools-mcp", - "tools-browser-web", - "tools-computer-use", - "tools-image-analysis", - "tools-miniapp", - "tools-canvas", - "tools-agent-control", -] } -bitfun-agent-runtime = { path = "../../execution/agent-runtime" } +bitfun-core = { path = "../../assembly/core", default-features = false, optional = true } +bitfun-agent-runtime = { path = "../../execution/agent-runtime", optional = true } bitfun-agent-tools = { path = "../../execution/tool-contracts" } bitfun-events = { path = "../../contracts/events" } -bitfun-core-types = { path = "../../contracts/core-types" } +bitfun-core-types = { path = "../../contracts/core-types", optional = true } agent-client-protocol = { workspace = true } tokio = { workspace = true, features = ["fs", "io-std", "io-util", "macros", "process", "rt", "sync", "time"] } tokio-util = { workspace = true, features = ["compat"] } -futures = { workspace = true } +futures = { workspace = true, optional = true } async-trait = { workspace = true } -serde = { workspace = true } +serde = { workspace = true, optional = true } serde_json = { workspace = true } chrono = { workspace = true } dashmap = { workspace = true } log = { workspace = true } uuid = { workspace = true } -sha2 = { workspace = true } +sha2 = { workspace = true, optional = true } [dev-dependencies] tokio = { workspace = true, features = ["rt-multi-thread"] } diff --git a/src/crates/interfaces/acp/src/lib.rs b/src/crates/interfaces/acp/src/lib.rs index 5930b0a52..e00dd7567 100644 --- a/src/crates/interfaces/acp/src/lib.rs +++ b/src/crates/interfaces/acp/src/lib.rs @@ -1,13 +1,20 @@ //! BitFun Agent Client Protocol integration. //! -//! This crate owns the external ACP server surface and maps it onto BitFun's -//! core agentic runtime. CLI and other hosts should only start this crate. +//! This crate owns the ACP client and server surfaces over BitFun's assembled +//! runtime. Product hosts select the additive `client` and `server` features +//! explicitly; the compatibility default enables both roles. +#[cfg(feature = "client")] pub mod client; +#[cfg(feature = "server")] mod runtime; +#[cfg(feature = "server")] mod server; pub use agent_client_protocol as protocol; +#[cfg(feature = "client")] pub use client::AcpClientService; +#[cfg(feature = "server")] pub use runtime::BitfunAcpRuntime; +#[cfg(feature = "server")] pub use server::AcpServer;