diff --git a/AGENTS-CN.md b/AGENTS-CN.md index 3a9fe7af85..093cce57d1 100644 --- a/AGENTS-CN.md +++ b/AGENTS-CN.md @@ -9,9 +9,9 @@ BitFun 是一个由 Rust workspace 与 React 前端组成的项目。 ## 快速开始 1. 在修改架构敏感代码前,先阅读 `README.md` 和 `CONTRIBUTING.md`。 -2. 桌面端开发优先使用 `pnpm run desktop:dev` — 提供完整热更新(Vite HMR + Rust 自动重编译并重启)。仅在需要更快冷启动且只迭代前端时使用 `pnpm run desktop:preview:debug`(Rust 改动不会自动重编译)。 +2. 日常开发使用下方主要产品循环;surface 专属的替代命令由最近的应用指南维护。 3. 修改 Rust 文件后,优先使用 `pnpm run fmt:rs`,只格式化已改动或已暂存的 `.rs` 文件。只有在你明确需要更大范围格式化时才使用 `cargo fmt`。 -4. 改完后按下方表格执行与改动范围匹配的最小验证。 +4. 改完后从离改动最近的 `AGENTS.md` 选择 focused 验证命令;下方仓库级验证章节只维护跨模块检查原则。 5. Rust workspace 依赖应在根清单中统一版本,而由消费 crate 按自身职责声明所需 feature;仅测试所需的 feature 应放入 `dev-dependencies`,受 crate feature 控制的服务能力应只在对应 feature 中启用。禁止使用 `tokio/full` 绕过依赖边界设计。 ## 分层模块索引 @@ -44,61 +44,23 @@ Stable Contracts and Security Control Plane 的边界以 ## 常用命令 -这些是命令参考,不是 PR 前置检查清单。预检请按下方“验证”表选择最小本地检查; -大范围测试和构建主要用于复现 CI 或验证构建相关改动。 +这里只保留稳定的仓库级入口。具体 surface/crate 的测试命令由最近的本地 `AGENTS.md` 维护, +不要在根文档重复抄写。 ```bash -# 安装 +# 安装与主要产品开发循环 pnpm install - -# 开发 pnpm run desktop:dev # 完整热更新:Vite HMR + Rust 自动重编译并重启 -pnpm run desktop:preview:debug # 复用预构建二进制 + Vite HMR;无 Rust 自动重编译 -pnpm run dev:web # 纯浏览器前端 -pnpm run cli:dev # CLI 运行时 -pnpm run cli:install # release 编译并安装 bitfun(Windows/macOS/Linux;含废弃兼容入口 bitfun-cli) - -# 检查 -pnpm run fmt:rs # 只格式化已改动 / 已暂存的 Rust 文件 -pnpm run lint:web -pnpm run type-check:web -pnpm --dir src/mobile-web run type-check -pnpm run i18n:contract:test # 仅 i18n contract / resources -pnpm run i18n:audit # 仅 i18n contract / resources -pnpm run product:check # 默认产品定义 -pnpm run check:repo-hygiene -pnpm run check:github-config -cargo check --workspace - -# 测试(本地优先用精确测试路径;大范围测试由 CI 兜底) -pnpm run product:test -pnpm --dir src/web-ui run test:run # 大范围测试;本地优先用精确测试路径 -cargo test --workspace # 大范围测试;CI 兜底 - -# 构建(仅构建相关改动或复现 CI 时运行) -cargo build -p bitfun-desktop # 构建相关改动 / 复现 CI -pnpm run build:web # 构建相关改动 / 复现 CI -pnpm run build:mobile-web # 构建相关改动 / 复现 CI - -# 快速构建(手动构建 / 调试流程) -pnpm run desktop:build:fast # debug 构建,不打包 -pnpm run desktop:build:release-fast # release 但降低 LTO -pnpm run desktop:build:nsis:fast # Windows 安装器,release-fast profile -``` - -完整脚本列表见 [`package.json`](package.json)。 - -### 构建逃生口 -开发/构建链路以一部分灵活性换取速度,必要时可覆盖: - -| 变量 / 参数 | 使用场景 | -| --- | --- | -| `CARGO_PROFILE_DEV_DEBUG=2` | 需要完整调试信息打断点。dev profile 默认 `line-tables-only`(panic 回溯仍带行号,PDB 体积大幅减小)。 | -| `BITFUN_MOBILE_WEB_FORCE_BUILD=1` 或 `node scripts/mobile-web-build.cjs --force` | 源码看起来没变但需要强制重建 mobile-web。当 `src/mobile-web/dist` 新于所有输入时构建会被跳过。 | -| `VITE_USE_POLLING=1` | Vite dev 监听不到文件变化——通常发生在网络盘或 WSL 挂载上。默认使用原生文件事件。 | +# 仓库级检查 +pnpm run fmt:rs # 只格式化已改动 / 已暂存的 Rust 文件 +pnpm run check:repo-hygiene # 仓库内容与文件名规则 +pnpm run check:github-config # GitHub workflow / 配置规则 +pnpm run check:core-boundaries # Cargo / 模块 owner 边界 +``` -`pnpm run build:web` 会并发执行类型检查与 Vite 构建,因此类型错误与打包错误出现的先后顺序不固定;两者的输出都带前缀(`[type-check]` / `[vite-build]`)。 +Web UI、mobile、CLI、Desktop、Installer、打包及 focused test 命令由最近的本地指南维护; +完整脚本注册表仍见 [`package.json`](package.json)。 ## 全局规则 @@ -248,24 +210,18 @@ OpenCode 兼容或目标项目治理的变更,先阅读 ## 验证 -按触及文件选择最小本地预检。完整构建和大范围测试默认由 CI 保护;只有改动直接影响构建、 -打包,或 CI 无法覆盖对应路径时,才在本地运行更重的命令。 - -| 改动类型 | 最低验证要求 | -|---|---| -| 不涉及 i18n 资源/契约的前端 UI、状态或适配层 | `pnpm run type-check:web`;行为变化时再加最近的 focused test | -| 仅 locale 资源改动 | `pnpm run i18n:audit` | -| Locale contract 或 shared terms | `pnpm run i18n:generate && pnpm run i18n:contract:test && pnpm run i18n:audit` | -| Web UI i18n runtime、namespace loading 或直接 `i18nService.t(...)` 调用 | `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 UI、状态、配对、断开或重连行为 | `pnpm --dir src/mobile-web run type-check`;行为变化还需要在 PR 中说明手动配对 / 重连验证 | -| 产品定义、schema、resolver 或 Desktop/CLI 产品构建 adapter | `pnpm run product:test`,并对默认定义运行 `pnpm run product:check` | -| `core`、`transport`、adapter 或共享服务中的 Rust 逻辑 | `cargo check --workspace`;行为变化时再加最近的 focused `cargo test` | -| 桌面端集成、Tauri API、browser/computer-use 或桌面专属行为 | `cargo check -p bitfun-desktop`;行为变化时再加 focused desktop tests | -| 被桌面端 smoke/functional 流覆盖的行为 | 优先运行最近的 focused E2E/smoke check;除非改动影响构建,否则 broad build/test 交给 CI | -| `src/crates/adapters/ai-adapters` | 运行上面相关 Rust 检查;只有 stream contract 改动时再加 `cargo test -p bitfun-agent-stream` | -| 不涉及打包的安装器前端或 i18n runtime | `pnpm --dir BitFun-Installer run type-check` | -| 安装器 Tauri/Rust 改动 | `cargo check --manifest-path BitFun-Installer/src-tauri/Cargo.toml` | -| 安装器打包、payload、安装/卸载流程或 native bundling | `pnpm run installer:build` | +验证范围由 owner 决定,不在根文档维护全仓测试矩阵: + +1. 阅读离改动最近的本地 `AGENTS.md`,运行能够覆盖该行为的最窄命令。 +2. 优先选择单 package、单 test target 或 module filter,并使用最小 feature;不要把 + `product-full`、`all-features` 或 workspace 全量测试当成捷径。 +3. 只有对应契约变化时才运行仓库级检查:布局/内容规则使用 repository hygiene, + workflow 变更使用 GitHub config,Cargo feature、依赖方向或 test-target 布局使用 core boundaries。 +4. 大范围 build、workspace suite、打包和平台矩阵默认交给现有 CI;只有改动影响这些路径或需要复现 + CI 故障时才在本地运行。 + +如果某个模块缺少有效的 focused 命令,应补充到该模块自己的指南,而不是继续扩张根文档。 +不要预先对齐所有模块的 test 清单;只有真实开发流程需要时才记录命令。 ## Agent 文档优先级 diff --git a/AGENTS.md b/AGENTS.md index 27794f7a00..3494a30943 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -9,9 +9,12 @@ Repository rule: **keep product logic platform-agnostic, then expose it through ## Quick start 1. Read `README.md` and `CONTRIBUTING.md` before architecture-sensitive changes. -2. For desktop development, prefer `pnpm run desktop:dev` — it provides full hot-reload (Vite HMR + Rust auto-rebuild & restart). Use `pnpm run desktop:preview:debug` only when you need a faster cold-start for frontend-only iteration (Rust changes are not auto-rebuilt). +2. Use the primary product loop below for normal development. Surface-specific + alternatives belong in the nearest app guide. 3. After Rust file changes, prefer `pnpm run fmt:rs` to format only changed or staged `.rs` files. Use `cargo fmt` only when you intentionally want broader formatting coverage. -4. After changes, run the smallest matching verification from the table below. +4. After changes, use the nearest local `AGENTS.md` for the focused verification + command. The repository-level verification section below only covers + cross-cutting checks. 5. Workspace Rust dependencies own compatible versions, not broad capability unions. Each crate must select the dependency features it actually uses; keep test-only features in dev-dependencies and attach feature-gated service @@ -50,64 +53,24 @@ Boundary rules: ## Common commands -These are command references, not a pre-PR checklist. Use the Verification table -to choose the smallest local precheck; broad suites and builds are mainly for CI -reproduction or build-impacting changes. +Keep this list to stable repository entry points. Surface- and crate-specific +test commands belong in the nearest local `AGENTS.md` and must not be copied here. ```bash -# Install +# Setup and primary product loop pnpm install - -# Dev pnpm run desktop:dev # full hot-reload: Vite HMR + Rust auto-rebuild & restart -pnpm run desktop:preview:debug # reuse pre-built binary + Vite HMR; no Rust auto-rebuild -pnpm run dev:web # browser-only frontend -pnpm run cli:dev # CLI runtime -pnpm run cli:install # build release + install bitfun (Windows/macOS/Linux; deprecated bitfun-cli included) - -# Check -pnpm run fmt:rs # format only changed / staged Rust files -pnpm run lint:web -pnpm run type-check:web -pnpm --dir src/mobile-web run type-check -pnpm run i18n:contract:test # i18n contract / resources only -pnpm run i18n:audit # i18n contract / resources only -pnpm run product:check # default product definition -pnpm run check:repo-hygiene -pnpm run check:github-config -cargo check --workspace - -# Test (prefer focused paths locally; broad suites are CI-backed) -pnpm run product:test -pnpm --dir src/web-ui run test:run # broad suite; prefer focused paths locally -cargo test --workspace # broad suite; CI-backed - -# Build (only for build-impacting changes or CI reproduction) -cargo build -p bitfun-desktop # build-impacting changes / CI reproduction -pnpm run build:web # build-impacting changes / CI reproduction -pnpm run build:mobile-web # build-impacting changes / CI reproduction - -# Fast builds (manual build/debug flows) -pnpm run desktop:build:fast # debug build, no bundling -pnpm run desktop:build:release-fast # release with reduced LTO -pnpm run desktop:build:nsis:fast # Windows installer, release-fast profile -``` - -For the full script list, see [`package.json`](package.json). -### Build escape hatches - -The dev/build pipeline trades some flexibility for speed. Override when needed: - -| Variable / flag | Use when | -| --- | --- | -| `CARGO_PROFILE_DEV_DEBUG=2` | You need full debug info for breakpoints. The dev profile ships `line-tables-only` (panic backtraces keep line numbers, PDBs stay small). | -| `BITFUN_MOBILE_WEB_FORCE_BUILD=1` or `node scripts/mobile-web-build.cjs --force` | mobile-web must rebuild even though its sources look unchanged. The build is skipped when `src/mobile-web/dist` is newer than every input. | -| `VITE_USE_POLLING=1` | The Vite dev watcher misses changes — typically on a network drive or a WSL mount. Native file events are the default. | +# Repository checks +pnpm run fmt:rs # format only changed / staged Rust files +pnpm run check:repo-hygiene # repository content and filename rules +pnpm run check:github-config # GitHub workflow/configuration rules +pnpm run check:core-boundaries # Cargo/module ownership boundaries +``` -`pnpm run build:web` runs the type-check and the Vite build concurrently, so a -type error and a bundling error can surface in either order; both are prefixed -(`[type-check]` / `[vite-build]`) in the output. +For Web UI, mobile, CLI, Desktop, Installer, packaging, and focused test +commands, use the nearest local guide. The full script registry remains in +[`package.json`](package.json). ## Global rules @@ -286,27 +249,22 @@ cost-aware, and auditable. ## Verification -Run the smallest local precheck that matches the touched files. CI is expected to -cover full builds and broad test suites; run heavier local commands only when the -change directly affects build, packaging, or CI cannot protect the path. - -| Change type | Minimum verification | -|---|---| -| Frontend UI, state, or adapters without i18n resource/contract changes | `pnpm run type-check:web`, plus the nearest focused test when behavior changed | -| Locale resource-only changes | `pnpm run i18n:audit` | -| Locale contract or shared terms | `pnpm run i18n:generate && pnpm run i18n:contract:test && pnpm run i18n:audit` | -| Web UI i18n runtime, namespace loading, or direct `i18nService.t(...)` usage | `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 UI, state, pairing, disconnect, or reconnect behavior | `pnpm --dir src/mobile-web run type-check`; include manual pairing / reconnect notes when behavior changes | -| Product definition, schema, resolver, or Desktop/CLI product build adapter | `pnpm run product:test`, plus `pnpm run product:check` for the default definition | -| Cargo manifests, features, test targets, or crate dependency boundaries | `pnpm run check:core-boundaries:test && pnpm run check:core-boundaries`; add the smallest affected `cargo check -p --no-default-features --features ` or focused target test when the compiled path changes | -| Shared Rust logic in `core`, `transport`, adapters, or services | `cargo check --workspace`, plus the nearest focused `cargo test` when behavior changed | -| Desktop integration, Tauri APIs, browser/computer-use, or desktop-only behavior | `cargo check -p bitfun-desktop`, plus focused desktop tests when behavior changed | -| Behavior covered by desktop smoke/functional flows | Prefer the nearest focused E2E/smoke check; rely on CI for broad build/test coverage unless build behavior changed | -| `src/crates/adapters/ai-adapters` | Relevant Rust checks above; add `cargo test -p bitfun-agent-stream` only when stream contracts changed | -| Installer frontend or i18n runtime without packaging changes | `pnpm --dir BitFun-Installer run type-check` | -| Installer Tauri/Rust changes | `cargo check --manifest-path BitFun-Installer/src-tauri/Cargo.toml` | -| Installer packaging, payload, install/uninstall flow, or native bundling | `pnpm run installer:build` | -| Build scripts or prerequisite changes | `pnpm run check:build-prereqs`, plus `node --test scripts/check-build-prereqs.test.mjs` when the check logic changed | +Choose verification at the owner, not from a repository-wide test matrix: + +1. Read the nearest local `AGENTS.md` and run its narrowest command that covers + the changed behavior. +2. Prefer one package, one test target or module filter, and the minimum feature + set. Do not use `product-full`, `all-features`, or a workspace-wide suite as a + shortcut. +3. Run a repository check only when its contract changed: repository hygiene for + layout/content rules, GitHub config for workflow changes, and core boundaries + for Cargo features, dependency direction, or test-target layout. +4. Leave broad builds, workspace suites, packaging, and platform matrices to + existing CI unless the change affects those paths or reproduces a CI failure. + +If a module lacks a useful focused command, add it to that module's guide rather +than expanding this file. Do not pre-emptively align every module's test list; +document a command only when a real workflow needs it. ## Agent-doc priority diff --git a/docs/architecture/agent-runtime-services-design.md b/docs/architecture/agent-runtime-services-design.md index e9a08a7495..e767ebfd01 100644 --- a/docs/architecture/agent-runtime-services-design.md +++ b/docs/architecture/agent-runtime-services-design.md @@ -441,6 +441,11 @@ model-round cancellation token、结构化 AgentInput 或更复杂的事件游 兼容边界: - `bitfun-agent-runtime` 只能依赖稳定接口、工具运行时、运行时服务接口和注入的提供方。 +- 权限规划按纯决策与产品编排分层: + - Agent Runtime 持有 `PermissionIntent` 的策略、约束层与记忆授权判定; + - Core 产品管线持有 workspace/remote scope 投影、平台大小写事实、grant store IO、native Hook 顺序、 + 交互请求投影、等待/取消和具体 Tool 执行; + - 该边界不建立第二套 Permission DTO、公开 SDK 接口或产品 feature。 - 具体调度器生命周期、会话元数据存储、token 订阅器、事件投递、产品 `Tool` handler、具体提示组装、workspace / remote / config IO、自定义子智能体文件 IO 和平台适配器 在行为等价未证明前不得下沉到运行时内核。 @@ -745,8 +750,11 @@ Rust Runtime SDK,不注册未实现的 `RuntimeServices` 能力,也不宣称 `doctor` 与 `health` 校验真实组装结果及必需注册完整性; Core 的 Network、Git 和 MCP Catalog 当前仍含兼容 marker,因此该诊断不等于对这些外部服务做实时探活。 -该切换仍是 `product-full` 兼容组装,不是 owner 迁移。协调器、调度器、持久化、工具管线和 Agentic Event Queue -仍由 Core 唯一持有;CLI 与 ACP 不复制这些状态。ACP 服务端通过 Rust Runtime SDK 处理会话创建/列举、轮次、取消、交互响应和事件订阅, +该切换仍是 `product-full` 兼容组装,不是完整 ToolPipeline owner 迁移。 +协调器、调度器、持久化、工具管线和 Agentic Event Queue 仍由 Core 唯一持有。 +唯一已迁移的部分是无 IO 的权限意图策略规划;scope、Hook、请求生命周期和实际执行继续归 Core。 + +CLI 与 ACP 不复制这些状态。ACP 服务端通过 Rust Runtime SDK 处理会话创建/列举、轮次、取消、交互响应和事件订阅, 但完整持久化历史回放、模型/模式目录与提供方配置和 MCP 仍走单一 Core 兼容接口;会话模型/模式写入通过 Agent Runtime API 回到同一 Core 归属模块。ACP stdio、连接和协议转换仍在 `interfaces/acp`。Desktop 复用同一 Core owner 构造一个窄口径 Rust Runtime SDK,主界面的轮次提交/取消、工具确认/拒绝和 用户问题回答与会话模型更新已通过 Rust Runtime SDK;会话 CRUD/恢复视图、MCP、MiniApp、Cron、远程连接、Tauri 窗口与平台资源 diff --git a/docs/architecture/product-customization-blueprint.md b/docs/architecture/product-customization-blueprint.md index 73b7cf829d..1664e6548d 100644 --- a/docs/architecture/product-customization-blueprint.md +++ b/docs/architecture/product-customization-blueprint.md @@ -62,6 +62,13 @@ manifest。相同输入必须产生相同摘要;非默认产品使用 digest-s 解释来源、localized 名称、技术 identity 与摘要。binary/bundle identity 不作为可见名称,产品 locale 必须符合共享 i18n locale 集合和 key parity。 +### 0.2 修改与验证 + +- 只修改默认产品定义或资源引用时运行 `pnpm run product:check`;非默认定义运行 + `pnpm run product:check -- --product-config `,确保校验实际改动的产品; +- 修改 schema、resolver 或 Desktop/CLI build adapter 行为时,再运行 `pnpm run product:test`; +- 打包和平台矩阵只在变更触及对应交付路径时运行,不作为产品定义的默认本地预检。 + ## 1. 设计结论 产品定制只需要四类对象: diff --git a/docs/performance/01-compile-performance.md b/docs/performance/01-compile-performance.md index c263f4af86..b5e036b186 100644 --- a/docs/performance/01-compile-performance.md +++ b/docs/performance/01-compile-performance.md @@ -1,303 +1,146 @@ -# BitFun 编译/构建性能审阅报告 +# BitFun 编译与依赖治理计划 + +> 最近核实:2026-08-04 +> +> 快照基线:`gcwing/main@061024fb2` 加权限规划 owner 迁移 +> +> 稳定规则:[Rust 构建与依赖边界](../architecture/rust-build-dependency-boundaries.md) -- 审阅日期:2026-07-26 -- 审阅范围:Rust workspace(36 个 crate + 5 个 app,约 57.8 万行 Rust)、前端 pnpm workspace(web-ui 1676 个 TS/TSX 文件、约 36.7 万行)、构建脚本(scripts/)、CI(.github/workflows) -- 审阅方式:只读静态分析 + 轻量命令(Cargo.lock 解析、LOC 统计、dist 产物检查),未运行完整编译 -- 环境:Windows 11,rustc/cargo 1.97.0,无 `.cargo/config.toml`、无 `rust-toolchain.toml`,PATH 中未发现 sccache/lld-link +这份文档只回答三个问题:当前主要成本在哪里、下一步先做什么、每轮治理如何证明有效。 +模块边界以架构文档为准,具体本地命令由最近的 `AGENTS.md` 维护,PR 只记录实际运行过的验证。 ---- +## 1. 当前结论 -## 一、发现总览表(按预期收益排序) +| 结论 | 说明 | +|---|---| +| 本轮收益是测试隔离,不是产品构建瘦身 | 权限纯策略测试从 Core 约 449 节点的闭包迁到 Agent Runtime 约 78 节点的闭包;产品依赖图不变 | +| 不再用 `product-full` 解决 focused test | Core 权限编排测试当前最小闭包是 `agent-runtime,canvas-runtime`;纯策略直接在 Agent Runtime 验证 | +| 不新增 CI 或测试入口 | 继续使用现有 test target 和 CI job;治理 PR 不复制同一闭包的验证 | +| 下一优先级是 App Server / Server | 先核实真实生产调用链,再收敛其 Core `product-full` 边界;收益不足则停止 | +| 依赖多版本不能按数量批量清理 | 只处理仓库能控制、行为等价且能缩小真实构建图的版本路径 | -| # | 发现 | 维度 | 预期收益 | 主要证据 | -|---|------|------|---------|---------| -| F1 | Monaco 被"双份打包":ESM 全量打进主 JS chunk,同时又以 AMD 形式复制到 public 运行时加载 | 前端 | 高 | `src/web-ui/src/tools/editor/core/MonacoEditorCore.tsx:11` 等 10+ 处 value import;`dist/assets/index-DwMxiWtW.js`(5.4MB)含 monaco 核心签名 | -| F2 | `build:web` 串行 type-check(366k 行、无 incremental),打包 CI 还重复执行一次 | 前端/CI | 高 | `package.json:49`、`src/web-ui/tsconfig.json`(无 incremental)、`.github/workflows/desktop-package.yml:225` | -| F3 | `bitfun-core` 巨石 crate(202k 行 / 469 文件),任何改动触发整 crate + 下游全量重编 | Rust | 高 | `src/crates/assembly/core`(LOC 统计) | -| F4 | release profile 全量 LTO + codegen-units=1 用于所有平台打包,仅 Linux aarch64 例外用了 thin | Rust/CI | 高 | `Cargo.toml:282-286`、`.github/workflows/desktop-package.yml:106` | -| F5 | 本地 dev 构建:全量 debuginfo + MSVC 默认链接器,`tauri dev` 路径未套用 dev.cjs 的加速环境变量 | Rust | 高 | `Cargo.toml:279-280`、`scripts/dev.cjs:346-351` vs `scripts/dev.cjs:749-767`、无 `.cargo/config.toml` | -| F6 | Cargo.lock 被 gitignore,CI 每次 `cargo generate-lockfile` → 依赖漂移 + rust-cache 频繁失效 | Rust/CI | 中-高 | `.gitignore:26`、`.github/workflows/ci.yml:47-48,136-137` | -| F7 | CI:`rust-build-check` 串行等待 `frontend-build`(含大量 lint/audit/test)完成才开始 | CI | 中 | `.github/workflows/ci.yml:72` | -| F8 | 每次 `desktop:dev` 无条件重建 mobile-web(先删 target 内产物 + pnpm install + vite build) | 脚本 | 中 | `scripts/dev.cjs:704-714`、`scripts/mobile-web-build.cjs:75-133` | -| F9 | 依赖树重复严重:1177 个包中 112 个名字存在多版本(image、thiserror、rand×3、getrandom×4、windows-sys×6、phf×6 等) | Rust | 中 | Cargo.lock 解析;`src/apps/desktop/Cargo.toml:85`(image 0.24 vs workspace 0.25) | -| F10 | reqwest TLS 单栈治理(已完成) | Rust | 已兑现 | workspace transport-only + 客户端 owner 显式 Rustls;见 F10 治理结果 | -| F11 | Vite dev watcher 强制 usePolling + 100ms 轮询,Windows 上 CPU 高、拖慢 HMR | 前端 | 中 | `src/web-ui/vite.config.ts:68-74` | -| F12 | Agent prompts 已抽到无依赖内容 owner;Cargo 指纹仍会让 Core 重检,公告仍由 Core 按 feature 生成 | Rust | 中(部分治理) | `src/crates/assembly/agent-content`、`src/crates/assembly/core/build.rs` | -| F13 | beforeBuildCommand 内 web 构建与 mobile-web 构建纯串行;dev.cjs 准备步骤也全串行 | 脚本 | 中 | `src/apps/desktop/tauri.conf.json`(build 块)、`scripts/dev.cjs:668-737` | -| F14 | tokio 全 workspace 开 `full` feature;tauri 开 `unstable`;个别重依赖(oxc、rquickjs、git2 vendored、sherpa-onnx)集中于少数 feature | Rust | 低-中 | `Cargo.toml:72,149,178-180,183,248` | -| F15 | tsconfig(web-ui/mobile-web)未启用 incremental;type-check 每次冷启动 | 前端 | 低-中 | `src/web-ui/tsconfig.json`(全文无 incremental) | -| F16 | copy-monaco 在 postinstall / prebuild / dev.cjs 三处重复执行(14MB / 103 文件,单次成本小) | 脚本 | 低 | `package.json:10,16,45`、`scripts/dev.cjs:669` | -| F17 | pnpm 结构小问题:tests/e2e 已在 workspace 内仍单独 install;installer 独立 Rust workspace 导致 Tauri 栈本地编译两份 | 结构 | 低 | `pnpm-workspace.yaml`、`package.json:92`、`Cargo.toml:40-42` | -| F18 | build.rs 生成代码遍历 HashMap,输出字节序不确定 → 不可复现构建,削弱 sccache/远端缓存效果 | Rust | 低 | `src/crates/assembly/core/build.rs:303,429`、`src/apps/cli/build.rs` | -| F19 | `[profile.dev] incremental = true` 为默认值,冗余;dev profile 无任何针对性调优 | Rust | 低 | `Cargo.toml:279-280` | -| F20 | `bitfun-agent-runtime` 的 28 个 integration targets 已收敛为 5 个显式目标,同时保留 Unix 进程测试隔离 | Rust/Test | 已兑现 | `src/crates/execution/agent-runtime/Cargo.toml`、`tests/agent_*_contracts.rs` | +权限 owner 的长期边界和功能不变量见 +[Agent Runtime 服务设计](../architecture/agent-runtime-services-design.md)。这里不重复维护行为规格。 ---- +## 2. 治理原则 -## 二、逐条详情 +目标是缩短常用开发、focused test、CI 和打包路径,同时保持产品行为与分层边界稳定。 +每个治理 PR 必须同时满足以下门槛: -### F1(高)Monaco 双份:ESM 全量进主 chunk + AMD 副本运行时再加载 +| 门槛 | 必须回答的问题 | +|---|---| +| Owner | 逻辑属于哪个现有 owner?是否存在真实生产消费者? | +| 行为 | 本地、远程和平台差异如何保持?哪些等价测试保护它? | +| 构建图 | 哪个产品或测试闭包实际退出了哪些依赖? | +| 耗时 | 若宣称性能收益,是否在同机器、同命令、同缓存状态下测量? | +| 增量成本 | 是否新增 dependency、feature、test target、CI job 或长期兼容层? | -**问题描述** -项目的 Monaco 策略是:`pnpm run copy-monaco` 把 `monaco-editor/min/vs`(AMD 版,14MB/103 文件)复制到 `src/web-ui/public/monaco-editor`,运行时由 `@monaco-editor/react` 的 `loader.init()` 从该路径加载(`src/web-ui/src/tools/editor/services/MonacoInitManager.ts:11,71-75`)。但同时有 10+ 个源文件对 `monaco-editor` 做了 **value import**(非 `import type`): +以下做法不属于优化: -- `src/web-ui/src/component-library/components/CodeEditor/CodeEditor.tsx:7` -- `src/web-ui/src/shared/helpers/MonacoHelper.ts:3` -- `src/web-ui/src/tools/editor/core/MonacoEditorCore.tsx:11`(`monaco.editor.create(...)` 实际调用,见 213 行) -- `src/web-ui/src/tools/editor/components/CodeEditor.tsx:10`(814 行 `monaco.editor.create`) -- `src/web-ui/src/tools/editor/services/MonacoModelManager.ts:12`(95 行 `monaco.editor.onWillDisposeModel`)等 +- 用 `product-full`、`all-features` 或 workspace 全量测试掩盖 feature 边界; +- 为减少重复版本数字强制 patch 平台依赖、宏生态或第三方兼容窗口; +- 新建第二套 Agent、Tool、Permission Runtime 或无消费者抽象; +- 未测量就引入 sccache、替换链接器、合并 Installer workspace 或增加 CI job; +- 删除跨平台行为保护来换取表面 CI 时长。 -vite.config.ts 没有为 `monaco-editor` 做 alias/external 处理,Rollup 会解析到 `monaco-editor` 的 ESM 入口(完整 editor),整包被打进**入口 chunk**。产物证据:`dist/assets/index-DwMxiWtW.js` 高达 **5.4MB**,其中含有 Monaco 内核独有的类名字符串 `monaco-mouse-cursor-text`(仅在 `index-DwMxiWtW.js` 与 monaco 的 CSS 中出现),且 `MonacoEnvironment` 也出现在该 chunk。即:**构建期要多打包压缩 ~3-4MB 的 Monaco ESM,运行期首屏要多下载解析这份代码,然后再从 /monaco-editor 加载第二份 AMD Monaco**。 +## 3. 当前基线 -**预期收益**:高。入口 chunk 缩小约 60-70%,vite build(压缩阶段)时间显著下降,应用启动时间同步受益。 +### 3.1 Rust 构建图 -**优化方案(二选一)** -- 方案 A(推荐,改动小):所有 `import * as monaco from 'monaco-editor'` 改为 `import type * as monaco from 'monaco-editor'`,运行时实例统一从 `MonacoInitManager.initialize()`/`loader.init()` 的返回值获取(现有单例已具备);同时把 `monaco-editor/min/vs/editor/editor.main.css` 的 import 保持不变(纯 CSS)。用 `scripts/report-web-bundle-size.cjs` 验证 index chunk 是否缩回。 -- 方案 B(彻底,改动大):放弃 AMD 副本与 copy-monaco,全面走 Vite ESM + `?worker` 方式打包 Monaco 与其 worker,由 Vite 做 code-split(`monaco` 单独 chunk,懒加载)。 +| 路径 | 当前快照 | 判断 | +|---|---:|---| +| `bitfun-core` | 约 493 个 Rust 文件、243,900 行 | 仍是最大的高频失效面;只按真实 owner 做纵向迁移 | +| Core 直接消费者 | ACP、App Server、CLI、Desktop、SDK Host、Server | 每次只迁移一个有真实调用方的服务切片 | +| Agent Runtime focused test | 约 78 个唯一 package/version 节点 | 适合无 IO 的 Agent Runtime 纯决策测试 | +| Core `agent-runtime` check | 约 391 个节点 | 窄 owner feature 可独立编译 | +| Core 权限编排测试 | `agent-runtime,canvas-runtime`,约 449 个节点 | 保留真实 scope、Hook、请求生命周期和 Tool 执行 | +| Core `product-full` test | 约 516 个节点 | 仅用于确实需要完整产品装配的兼容路径 | +| Agent Runtime integration target | 5 个显式 target | 已完成收敛;平台和进程边界继续独立 | -### F2(高)build:web 串行 type-check、无增量,CI 打包重复执行 +节点数来自同一 Windows 环境下的 `cargo tree --locked` 相对统计,不是实际耗时,也不是跨平台阈值。 +权限纯策略路径理论上少进入约 371 个节点;产品构建闭包没有变化。 -**问题描述** -`package.json:49`:`"build:web": "pnpm run type-check:web && pnpm --dir src/web-ui build && pnpm run verify:monaco-assets"`。`type-check:web` 即 `tsc --noEmit`(`src/web-ui/package.json:16`),对 1676 个文件/36.7 万行做全量检查,且 `src/web-ui/tsconfig.json` 未开启 `incremental`(仅 `tsconfig.node.json:3` 有 composite)。Vite 构建本身不依赖 tsc(esbuild 转译),两者完全可并行。更严重的是打包 CI 里重复执行:`.github/workflows/desktop-package.yml:225` 先跑 `pnpm run type-check:web`,随后 228 行的 build_command → `desktop:build` → `tauri build` 的 `beforeBuildCommand`(`src/apps/desktop/tauri.conf.json`)= `pnpm run build:web && ...` → **再次 type-check**。5 个平台矩阵各多花一次全量 tsc。 +### 3.2 依赖与 feature -**预期收益**:高。本地 `build:web` 墙钟时间约减 30-50%;打包 CI 每个平台省一次全量 tsc(约 1-3 分钟 × 5 平台)。 +| 状态 | 范围 | 处理结论 | +|---|---|---| +| 已稳定 | 根 `Cargo.lock`、Reqwest Rustls 单栈、Desktop 直接 `image 0.25`、workspace Tokio 最小基线 | 不重复治理 | +| 下一步核实 | App Server / Server 的 Core `product-full` | 按生产 construction path 收敛,不先写 feature 清单 | +| 可独立治理 | Installer 的 Reqwest 0.12、独立 lockfile、疑似无消费者的 `tokio/full` | 保持 Installer 独立 workspace,不顺手合并 | +| 等待上游 | `screenshots 0.8.10 -> image 0.24.9` | 只有受维护且行为等价的上游替代出现后再处理 | +| 明确保留 | `portable-pty 0.8/0.9` | 非 OHOS 与 OHOS 的平台兼容选择,不为去重破坏 | -**优化方案** -1. `build:web` 改为并行:`concurrently "pnpm run type-check:web" "pnpm --dir src/web-ui build"`(或用 `npm-run-all --parallel`),verify 放最后。 -2. `src/web-ui/tsconfig.json` 增加 `"incremental": true, "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo"`(noEmit + incremental 在 TS 5.x 合法)。 -3. desktop-package.yml 中二选一:删除 225 行独立 type-check,或给 desktop:build 提供跳过 type-check 的入口(例如 `BITFUN_SKIP_TYPECHECK=1` 时 build:web 只跑 vite build)。 +根 lockfile 约有 116 个名称存在多版本。这个数字只用于发现候选,不能直接转化为治理任务。 +`oxc`、`rquickjs`、vendored `git2`、`sherpa-onnx` 等重依赖都有真实 capability owner;只有某个产品入口 +不消费对应能力时,才允许让它退出该入口的构建图。 -### F3(高)bitfun-core 巨石 crate 是 Rust 增量编译瓶颈 +### 3.3 CI 与本地验证 -**问题描述** -LOC 统计(只算各 crate `src/`):`src/crates/assembly/core` **202,735 行 / 469 文件**,是第二名(services-integrations 72k)的近 3 倍;bitfun-desktop 依赖它(`src/apps/desktop/Cargo.toml:22`)。rustc 的编译单元是 crate:core 内任何一行改动都会重新编译整个 202k 行 crate(增量编译可缓解 codegen,但 MIR/借用检查/单体化与下游 `bitfun-desktop`(63k 行)的重编译+重链接不可避免)。相比之下 workspace 其他 crate 拆分粒度合理(contracts/adapters/execution 层多为 1-20k 行)。此外 core 的 `Cargo.toml` 显示它同时聚合了 sqlite(`rusqlite bundled`)、MCP 客户端、调试 HTTP 服务器、git2 等,大量子域仍在一个编译单元内。 +- 现有 CI 已覆盖 workspace check、Core/Desktop lib、平台敏感 owner 测试和独立 runtime/CLI 验证; + 不再为治理 PR 叠加同闭包 job。 +- 本地先运行 owner 文档维护的最小 package/target/feature 命令。广泛 build、workspace suite、打包和 + 平台矩阵由 CI 承担,除非改动直接影响这些路径或需要复现 CI 故障。 +- CI 收敛必须基于多次 job/step 耗时、缓存状态、平台事实和失败历史。测试名称相似不等于覆盖重复, + `SKIPPED`、未触发或只编译未运行也不等于通过。 -**预期收益**:高(日常增量编译;长期工程)。 +## 4. 已完成,不再重复实施 -**优化方案(渐进式)** -1. 先用 `cargo build --timings` 定位 core 的编译耗时占比,确认瓶颈(低风险、只读)。 -2. 按现有目录边界(`src/agentic`、`src/service/announcement`、sqlite 存储层、debug-log server 等)把相对独立、低耦合的子模块下沉为新 crate(workspace 已有清晰分层惯例)。优先拆"改动频繁"与"几乎不变"的两极模块。 -3. 拆分时保持 re-export(`pub use`)以最小化调用方改动。 +| 主题 | 当前结果 | +|---|---| +| 前端构建 | Monaco 运行时加载已统一;Web type-check/Vite 并行;Web/Mobile TS 已启用 incremental | +| 开发循环 | mobile-web 支持输入 mtime 短路;Vite 默认使用原生文件事件;前端准备步骤已并行 | +| Rust profile | release 使用 thin LTO;dev 使用 `line-tables-only` 和高 codegen-units,并保留调试逃生口 | +| 可复现解析 | 根 lockfile 已提交,普通 CI 使用 `--locked`;build.rs 输出已排序 | +| CI 拓扑 | Rust job 不再等待完整前端构建,自建 Tauri 检查所需资源目录 | +| 依赖收敛 | Desktop 直接 image 版本和 Reqwest TLS 双栈已治理 | +| Agent Runtime 测试 | 28 个 integration executable 已收敛为 5 个职责/平台 target | -### F4(高)release 全量 LTO + codegen-units=1 拖慢所有打包构建 +内置 Agent 内容已经移到无第三方依赖的 `bitfun-agent-content`,减少了 Core build-script 工作;但 Core +仍直接依赖该 crate。没有足够产品收益前,不为消除这一编译指纹引入动态 provider、运行时文件读取或资源协议。 -**问题描述** -`Cargo.toml:282-286`:`[profile.release] opt-level=3, lto=true(fat), codegen-units=1, strip=true`。fat LTO + CGU=1 使最终 crate 的 codegen/链接几乎完全单线程化,是 release 构建时长的最大放大器;desktop-package.yml 5 个平台打包全部使用该 profile,唯独 Linux aarch64 因慢而被显式覆盖为 `CARGO_PROFILE_RELEASE_LTO=thin`(`.github/workflows/desktop-package.yml:106`)——说明团队已验证 thin LTO 可行。thin LTO 通常仅比 fat LTO 损失 0-2% 运行性能,但构建时间可减 30-60%。 +## 5. 后续顺序 -**预期收益**:高(打包 CI 与本地 release 构建)。 +### R1:收敛 App Server / Server 的 `product-full` 边界 -**优化方案** -1. `[profile.release]` 改为 `lto = "thin"`;codegen-units 可保守维持 1,或放宽到 16 换更多并行(先 A/B 对比二进制体积与 e2e perf 基线,仓库已有 `e2e:test:perf:release-fast` 类基础设施)。 -2. 移除 desktop-package.yml:106 的特例环境变量(统一后不再需要)。 -3. 若担心性能回退,可只在 nightly/desktop-package 的非发布分支先切 thin,观察一个周期。 +这是下一 PR 的推荐范围,也是唯一需要优先设计的核心入口改造。 -### F5(高)本地 dev 循环:全量 debuginfo + 默认 MSVC 链接器 +先回答: -**问题描述** -`[profile.dev]` 只有 `incremental = true`(`Cargo.toml:279-280`,本身是默认值),debuginfo 为默认 full。Windows 上每次增量构建的大头是 link.exe 重链 bitfun-desktop(数百依赖 + 63k 行 app crate)并重写巨型 PDB。dev.cjs 已经意识到这一点——`rebuildDesktopDebugBinary()` 设置 `CARGO_PROFILE_DEV_DEBUG=0、CODEGEN_UNITS=256`(`scripts/dev.cjs:346-351`),**但只作用于 desktop-preview 路径**;最常用的 `desktop:dev`(`tauri dev`,`scripts/dev.cjs:749-767`)与 `desktop:dev:raw` 完全没有这些环境变量,仍是全量 debuginfo。仓库也没有 `.cargo/config.toml`,未启用任何链接器优化(rust-lld)或编译缓存(sccache)。 +- App Server 与 Server 的真实 construction、command 和 schema 路径分别消费哪些 Core owner? +- Server 对未实现能力应保持什么 typed unsupported 行为? +- 哪些能力由 Server 直接消费,哪些只是经 App Server 间接带入? -**预期收益**:高(日常 Rust 改动的"改一行到重启应用"时间,链接期通常可减 30-60%)。 +实现边界: -**优化方案** -1. 低风险:在 `Cargo.toml` `[profile.dev]` 显式设置 `debug = "line-tables-only"`(保留 panic 栈回溯行号,PDB 大幅缩小);需要完整调试时用 `CARGO_PROFILE_DEV_DEBUG=2` 临时覆盖。 -2. 或者把 dev.cjs 的三个 CARGO_PROFILE_DEV_* 环境变量同样注入 `desktop:dev` 的 `tauri dev` 进程(与 preview 路径一致,改动只在脚本层)。 -3. 中风险:新增 `.cargo/config.toml`,对 `x86_64-pc-windows-msvc` 设置 `linker = "rust-lld"`(随 rustup 分发,无需额外安装);先在本地验证 tauri/webview2 链接参数兼容后再提交。 -4. 可选:安装 sccache 并设 `RUSTC_WRAPPER`(注意 sccache 与 incremental 互斥,更适合 CI/冷构建场景,见 F18 的可复现性前提)。 +- 只替换一个端到端 capability slice 的依赖路径,不一次迁移全部 Core 调用; +- 优先显式选择已有 owner feature,或消费现有 Runtime SDK/service port; +- 不复制 Session、Tool、Permission、Hook、Event 状态,不建立第二 Runtime; +- 未迁移能力保留现有兼容路径或明确 unsupported,禁止静默本机回退。 -### F6(中-高)Cargo.lock 不入库,CI 每次重新解析依赖 +验收必须覆盖 Server WebSocket/App Server round-trip、权限、取消、事件与恢复语义,并对比 App Server、 +Server 的 normal/build/test closure。若构建图收益不足或行为等价无法证明,则不删除兼容边界。 -**问题描述** -`.gitignore:26` 忽略了 `Cargo.lock`;CI 各 Rust job 先 `cargo generate-lockfile` 再 `--locked`(`.github/workflows/ci.yml:47-48,136-137`),`src/apps/desktop/Cargo.toml:48-53` 的注释也明说"CI 有意忽略根 Cargo.lock"。后果:(a) 任何上游依赖发新版本都会改变解析结果,swatinem/rust-cache 以 lockfile 哈希为 key,缓存频繁整段失效,依赖需从零重编;(b) 构建不可复现,还被迫用 `=x.y.z` 硬钉住问题依赖(`Cargo.toml:98-107` 的 time/brotli/bitflags 补丁群);(c) 本地与 CI 依赖树可能不一致。对应用型(非库)workspace,Cargo 官方建议提交 lockfile。 +### 后续队列 -**预期收益**:中-高(CI 稳定性与缓存命中率;偶发的"上游发版导致全量重编/编译失败"归零)。 +| 顺序 | 范围 | 启动条件 | +|---|---|---| +| R2 | 从 ACP 迁移一个已有 Services owner 的 host-service 切片 | 明确真实调用方,并能保持 Windows 进程树、SSH、取消和远程身份语义 | +| R3 | Installer lockfile、Reqwest 0.13 与无消费者依赖治理 | 下载、SSE/进度、取消、代理、证书失败和三平台 packaging 可验证 | +| R4 | 消除 `screenshots -> image 0.24` | 有受维护、无需 fork/vendoring 且屏幕枚举/DPI/权限行为等价的上游路径 | -**优化方案** -1. 从 .gitignore 移除 Cargo.lock 并提交(根 workspace 与 BitFun-Installer/src-tauri 各一份);CI 删除 `cargo generate-lockfile` 步骤,直接 `--locked`。 -2. 依赖更新改为显式动作(Renovate/Dependabot 或定期 `cargo update` PR),届时可逐步解除 `=` 钉版。 -3. 风险:改变现行"自动吃最新补丁版本"策略,需团队确认;属流程变更而非代码变更。 +每一步都在前一 PR 合入后的最新 main 重新测量。无法证明边界或收益时停止,不为了完成清单继续重构。 -### F7(中)CI 拓扑:Rust 检查串行排在完整前端流水线之后 +## 6. 每轮 PR 的证据 -**问题描述** -`.github/workflows/ci.yml:72` `rust-build-check.needs: frontend-build`。frontend-build 包含 hygiene/boundaries/i18n/theme 审计、eslint、vitest、build:web、mobile-web 构建等十几个串行步骤,全部完成后三平台 Rust job 才开始。Rust 侧对前端的真实依赖只有 `dist/` 存在(tauri-build 校验 frontendDist)与 `src/mobile-web/dist` 目录(CI 已用 `mkdir -p` 打桩,ci.yml:99-101)。 +PR 描述只需维护一张简表,不新增全仓依赖台账: -**预期收益**:中(PR 反馈总时长,估计缩短 5-15 分钟视 frontend job 时长)。 +| 证据 | 变更前 | 变更后 | +|---|---:|---:| +| 真实产品 normal/build closure | | | +| owner focused-test closure | | | +| 目标重复版本或重型依赖路径 | | | +| 冷、热或增量耗时(同机器、命令、缓存状态) | | | +| 新增 dependency、feature、test target、CI job | | | -**优化方案** -- 将 `frontend-build` 拆成 `frontend-checks`(lint/audit/test)与 `frontend-dist`(仅 `pnpm install + build:web + build:mobile-web` 上传产物)两个 job:Rust job 只 `needs: frontend-dist`;或者更激进——Rust check/test 根本不需要真实 dist,给 `dist/` 也 mkdir 打桩即可完全并行(需验证 tauri-build 仅检查目录存在)。 - -### F8(中)desktop:dev 每次冷启动都全量重建 mobile-web - -**问题描述** -`scripts/dev.cjs:704-714` 在每次 `desktop:dev` 启动时调用 `buildMobileWeb({install:true})`;`scripts/mobile-web-build.cjs` 无任何变更检测:75-94 行先删除所有 `target/*/mobile-web` 副本,109 行无条件 `pnpm install`,122 行无条件 `vite build`。mobile-web 源码不变时这是纯浪费(install + vite build 通常 20-60s),且删除 target 副本会迫使桌面端重新拷贝。`lint:rs:desktop`(package.json:41)同样每次先跑 `prepare:mobile-web`。 - -**预期收益**:中(每次 desktop dev 启动省 20-60s)。 - -**优化方案** -- 在 buildMobileWeb 中加入 mtime/hash 短路:比较 `src/mobile-web/{src,public,package.json,vite.config.*}` 最新 mtime 与 `src/mobile-web/dist` 构建标记(dev.cjs 已有同型实现 `getDesktopPreviewRebuildPlan`,`scripts/dev.cjs:420-456`,可复用);dist 有效时跳过 clean/install/build。提供 `--force` 逃生口。 - -### F9(中)重复依赖:112 个包存在多版本 - -**问题描述** -Cargo.lock 共 1177 个包,其中 112 个名字存在 2 个以上版本(解析自本地 Cargo.lock)。重点: -- `image 0.24.9 + 0.25.10`:**根因在自家代码**——workspace 定义 image 0.25(`Cargo.toml:108`),但 `src/apps/desktop/Cargo.toml:85` 单独写死 `image = "0.24"`,两份 image(含 png/jpeg 解码器栈)都要编译。 -- `rand 0.7/0.8/0.9`、`getrandom ×4`、`thiserror 1+2`、`syn 1+2`、`windows-sys ×6`、`windows-targets ×4`、`phf ×6`、`toml/toml_edit ×3`、`zbus 4+5`、`which 4+8`、`portable-pty 0.8+0.9`、`nix ×4` 等,多数由第三方传递引入,但每个多版本都是一份额外编译时间与 target 体积。 - -**预期收益**:中(冷构建时间与 target 体积;image 一项立收)。 - -**优化方案** -1. 立即:desktop 的 image 改回 `image = { workspace = true }`(0.25 同样支持 png/jpeg feature 子集),验证调用点 API 兼容。 -2. 运行 `cargo tree -d -e normal --workspace`(建议加 `--target x86_64-pc-windows-msvc` 过滤无关平台)输出清单,针对 top 传递源头(如 pull 出 rand 0.7 的 crate)评估升级;`russh 0.45`、`screenshots 0.8`、`syntect-tui` 等旧版是主要嫌疑。 -3. 将该清单纳入 nightly CI 做趋势看护(非阻断)。 - -### F10(已治理)reqwest TLS 单栈 - -**治理结果(2026-08-03)** -- workspace 级 `reqwest` 只保留 HTTP/序列化能力,URL-only 消费者不再被动加载 TLS;CLI、Desktop、AI Adapter、MiniApp Market 与 Services 的各 Reqwest owner feature 显式选择 Rustls。 -- Reqwest 0.13 的 `rustls` feature 使用平台证书验证器,保留系统信任根行为;三处显式 Client Builder 统一使用 `tls_backend_rustls()`,不再混用默认后端或弃用 API。 -- `Cargo.lock` 已移除 `native-tls`、`hyper-tls`、`tokio-native-tls`、`openssl` 与 `openssl-macros`;`openssl-sys` 只剩非 Windows 的 Git/libgit2 目标路径。Windows 产品图、本地开发、常规构建和三个既有 workflow 不再下载或配置预编译 OpenSSL。 -- 边界检查约束 TLS 必须由真实客户端 owner 选择,避免未来把后端重新提升为 workspace 全局 feature;没有新增 CI job 或测试步骤。 - -**收益与风险**:Windows CI 每个受影响 job 省去约 6.6 秒 OpenSSL bootstrap(以 PR #1991 的 Windows job 步骤时间戳为基线),并减少一套 TLS 依赖闭包。显式代理配置仍由 Reqwest 处理;未受系统信任的自签证书仍会按安全默认值拒绝。仓库没有 Reqwest client identity 或自定义 native connector 用法,因此未保留第二后端兼容层。 - -### F11(中)Vite dev 强制 100ms 轮询 watch - -**问题描述** -`src/web-ui/vite.config.ts:68-74`:`watch: { usePolling: true, interval: 100 }`,注释称为 Windows 稳定性。对 1676 个源文件 + node_modules 的轮询每 100ms 扫一轮,持续占用 CPU(常见 5-15% 单核),还与 Vite 官方建议相悖(Windows 原生 fs 事件在本地盘上是可靠的,轮询主要用于网络盘/WSL 挂载)。 - -**预期收益**:中(dev 机器 CPU/风扇/电池,HMR 延迟)。 - -**优化方案** -- 默认关闭 `usePolling`(删除该配置),仅当 `process.env.VITE_USE_POLLING` 显式设置时启用轮询作为逃生口;若必须保留轮询,interval 提到 ≥1000ms。 - -### F12(中,部分治理)内置 Agent 内容已独立归属,Core 失效链仍存在 - -**2026-08-03 治理结果** - -- 35 个内置 Agent prompt 移到无第三方依赖、无 feature 的 `bitfun-agent-content`;Core 只在 - `product-full` 组装中依赖它,窄 `announcement` feature 不加载该 crate。 -- 26 个 catalog prompt 保留旧 build.rs 生成 Rust 源码时的换行归一化;Memory phase-1 的生产常量与 9 个 - Insights 常量继续保留旧 `include_str!` 字节行为。Core 继续持有选择、渲染、Memory/Insights 工作流与错误语义。 -- Core build script 不再扫描 Agent prompt;内置 Skill metadata 仅在 `product-full` 生成,announcement - 内容仅在 `announcement` feature 生成。 -- 未采用 debug 运行时读文件方案。它会使 debug/release 的内容来源、错误时机与自包含行为不同,不符合本轮 - 功能规格完全一致的约束。 - -**同机热缓存实测**(Windows,`cargo check -p bitfun-core --features product-full`): - -| 场景 | Cargo 失效路径 | 耗时 | -|---|---|---:| -| 无改动热检查 | 全部 fresh | 0.92s | -| 迁移前修改 `init_agents_md.md` | Core build script + `bitfun-core` | 11.38s | -| 迁移后修改同一 prompt | `bitfun-agent-content` + 依赖它的 `bitfun-core` | 10.02s | - -当前收益约 12%,主要来自移除 Core prompt codegen 工作;Rust/Cargo 仍会因直接依赖重建而检查 Core,因此这不是 -“下游完全隔离”。若后续要消除 Core 重检,必须先以独立设计评审 prompt provider 注入或资源打包路径,并证明 -Desktop、CLI、ACP、Server 与 SDK Host 的内容、错误和生命周期完全等价,不能用运行时 fallback 换取表面指标。 - -### F13(中)构建/启动编排中的串行步骤 - -**问题描述** -- `src/apps/desktop/tauri.conf.json` build 块:`beforeBuildCommand: "pnpm run build:web && pnpm run prepare:mobile-web"` —— web-ui 构建、mobile-web 构建互相独立却串行;且它们整体又发生在 cargo 编译之前(tauri CLI 约束,前端与 Rust 无法并行)。 -- `scripts/dev.cjs:668-737`:copy-monaco → generate-version → mobile-web → flashgrep 全串行,彼此无依赖。 - -**预期收益**:中(desktop:build 总时长减 1-3 分钟;dev 启动准备段减半)。 - -**优化方案** -1. beforeBuildCommand 改为并行封装脚本(Node 内 `Promise.all` 两个子进程,注意日志前缀区分)。 -2. dev.cjs 的四个准备步骤用 `Promise.all` 并行(copy/generate/flashgrep 都是纯本地 IO)。 -3. 进阶:绕开 tauri CLI 的串行约束——desktop-tauri-build.mjs 先并行启动"前端构建"与"cargo build(直接 cargo,不经 tauri)",最后再让 `tauri build` 复用增量结果;实现复杂度较高,建议放到后期。 - -### F14(低-中)feature 面偏大与重量级依赖 - -**问题描述** -- `tokio = { features = ["full"] }`(Cargo.toml:72)全 workspace 生效,包含 io-std/signal/process 等未必全用的模块(tokio 编译不算大头,收益有限)。 -- `tauri` 开 `unstable` + `tray-icon` + `macos-private-api`(183 行)——`unstable` 为多 webview 所需,合理但注意跟踪。 -- 重量级依赖:`oxc`(JS 编译器,178 行,canvas-runtime feature)、`rquickjs`(C 的 QuickJS,180 行)、`git2 vendored-libgit2`(149 行,冷构建编译整个 libgit2 C 库)、`sherpa-onnx`(248 行,speech feature,已用 prebuilt 缓解,target 下有 sherpa-onnx-prebuilt)。desktop 默认开 `canvas-runtime + speech`(`src/apps/desktop/Cargo.toml:27`)。 - -**预期收益**:低-中(主要影响冷构建)。 - -**优化方案** -- tokio 换成显式 feature 列表(rt-multi-thread、macros、fs、net、io-util、sync、time、process、signal 按需);一次性梳理,风险低但触碰面广,建议用 `cargo check --workspace` 验证。 -- 为 `canvas-runtime`/`speech` 评估"开发期默认关闭"的 dev feature 预设(例如 desktop `default = []` 已是如此,可提供 `desktop:dev:lite` 脚本传 `--no-default-features` 组合),按需取舍。 - -### F15(低-中)tsc 无增量缓存 - -见 F2 方案 2;`src/mobile-web/tsconfig.json` 同样处理。单独列出是因为即使不动 build:web 编排,本地反复 `type-check:web` 也能从 incremental 获益(二次检查通常快 3-10 倍)。 - -### F16(低)copy-monaco 重复执行 - -**问题描述**:`package.json:10,16,45` + `scripts/dev.cjs:669`,postinstall、prebuild:web、dev 启动三处都会全量复制。实测量级仅 14MB/103 文件,单次秒级,优先级低。 -**优化方案**:在 copy 脚本中比对 monaco-editor 版本号(package.json vs 目标目录内 marker 文件),相同则跳过。 - -### F17(低)pnpm/workspace 结构小问题 - -- `tests/e2e` 已列入 `pnpm-workspace.yaml`,根 `pnpm install` 已装依赖,但仍保留 `e2e:install`(package.json:92)单独 install 入口,易造成双份状态;website 不在 workspace(独立 install),属有意隔离可保留。 -- `BitFun-Installer/src-tauri` 是独立 Rust workspace(根 Cargo.toml:40-42 exclude),与主 workspace 各自维护 target,Tauri 全家桶在本地要编两份。可评估共享 `CARGO_TARGET_DIR`(风险:两个 workspace 依赖版本不同会互相踩缓存,需先对齐版本)或接受现状。 - -### F18(低)build.rs 生成代码不可复现 - -**问题描述**:`assembly/core/build.rs:303,429` 与 `src/apps/cli/build.rs` 遍历 `HashMap` 生成 `map.insert(...)` 行,顺序随机。同样输入两次构建产出字节不同的生成文件,破坏可复现构建,也让 sccache/远端缓存对相关 crate 失效。 -**优化方案**:改用 `BTreeMap` 或收集后 `sort`,一行级改动、零风险。 - -### F19(低)profile.dev 冗余配置 - -`Cargo.toml:279-280` 的 `incremental = true` 是 dev 默认值,可删;该段落是放置 F5 建议(`debug = "line-tables-only"`)的天然位置。 - -### F20(已治理)agent-runtime integration test 重复链接 - -`bitfun-agent-runtime` 没有可选 feature,原 28 个 integration test target 中的 27 个跨平台契约使用相同的依赖闭包, -却在每次 `cargo test -p bitfun-agent-runtime` 时分别编译和链接。当前通过 `autotests = false` 将它们按定义、会话、 -交互和 long-horizon 职责归入 4 个 target;另保留 1 个 Unix-only 原生进程 target,避免为了减少数量而跨平台或进程 -边界合并。246 个 Unix integration tests、224 个 Windows integration tests 及原有 lib tests 均保留。现有 CI 命令和 -覆盖范围不变,未新增测试、feature、依赖或 workflow;现有边界检查会拒绝未注册入口和未被引用的叶测试文件。 - -以下是本机观察值,不作为其他机器的固定收益承诺。测量日期 2026-08-03,基线 -`53c8c029a8b6245e810cbee0707c820bc74fb7b8`,Windows 10.0.19045、i7-10700、rustc/cargo 1.97.1;依赖预热后按 -原布局/现布局交错执行 A/B/A/B/A/B,每次运行 -`cargo clean -p bitfun-agent-runtime` 和 `cargo test -p bitfun-agent-runtime --no-run --locked --quiet`: - -| 布局 | 三次有效样本 | 中位数 | integration PDB | -|---|---|---:|---:| -| 原 28 targets | 12.48s / 15.01s / 13.63s | 13.63s | 312.3 MiB | -| 现 5 targets | 11.90s / 11.04s / 11.31s | 11.31s | 77.4 MiB | - -本机包级 test 编译/链接中位数降低约 17.0%,PDB 体积降低约 75.2%。这是测试可执行目标治理,不代表第三方依赖 -或 feature 数量减少;有独立 feature、平台、进程或外部系统边界的测试仍必须保持独立 target。 - ---- - -## 三、实施建议清单(可直接派发给实施 agent) - -按建议实施顺序排列;每条含验收标准与风险等级。 - -| 任务 | 内容 | 涉及文件 | 风险 | -|------|------|---------|------| -| T1 | Monaco 去重(F1 方案 A):将 `src/web-ui/src` 下所有 `import * as monaco from 'monaco-editor'` 改为 `import type`,运行时实例统一经 `MonacoInitManager.initialize()` 注入(必要处传参或增加 `getMonaco()` 访问器);保留 `editor.main.css` import 与 AMD loader 流程。验收:`pnpm run build:web` 后 `dist/assets/index-*.js` 体积从 ~5.4MB 降至 ~2MB 以下,`grep -L monaco-mouse-cursor-text dist/assets/index-*.js`,编辑器与 diff 功能手测正常。 | `src/web-ui/src/**`(约 10-14 个文件)、`vite.config.ts` | 中(触碰编辑器核心路径,需手测 CodeEditor/DiffEditor/主题/worker) | -| T2 | build:web 并行化 + tsc 增量(F2/F15):a) 根 package.json `build:web` 改为并行执行 type-check 与 vite build(推荐 `npm-run-all2 --parallel` 或自写 Node 封装,任一失败即整体失败);b) `src/web-ui/tsconfig.json`、`src/mobile-web/tsconfig.json` 加 `"incremental": true` 与 tsBuildInfoFile(放 node_modules/.cache),并确认 .gitignore 覆盖;c) `.github/workflows/desktop-package.yml` 删除独立的 `pnpm run type-check:web` 步骤(225 行)。验收:build:web 总时长下降;CI 打包日志中 tsc 只出现一次。 | `package.json:49`、`src/web-ui/tsconfig.json`、`src/mobile-web/tsconfig.json`、`.github/workflows/desktop-package.yml` | 低 | -| T3 | release 改 thin LTO(F4):`Cargo.toml` `[profile.release]` `lto = "thin"`;删除 desktop-package.yml:106 的 `CARGO_PROFILE_RELEASE_LTO=thin` 特例。验收:三平台打包成功,二进制体积增幅 <5%,`e2e:test:perf:release-fast` 基线无回退(注意 release-fast 继承 release 后 `lto=false` 覆盖不受影响)。 | `Cargo.toml:284`、`.github/workflows/desktop-package.yml:106` | 中(需一轮打包验证 + 性能基线对比) | -| T4 | dev 构建 debuginfo 裁剪(F5/F19):`Cargo.toml` `[profile.dev]` 增加 `debug = "line-tables-only"`,删除冗余 `incremental = true`;在 `scripts/dev.cjs` 的 `desktop:dev`(tauri dev)路径注入与 preview 相同的 `CARGO_PROFILE_DEV_CODEGEN_UNITS=256`(允许 env 覆盖)。文档注明"需要断点调试时 `set CARGO_PROFILE_DEV_DEBUG=2`"。验收:改动一个 desktop crate 文件后的增量重链时间下降;panic 栈仍含行号。 | `Cargo.toml:279-280`、`scripts/dev.cjs:749-767` | 低(调试体验有权衡,需在 README/AGENTS 说明) | -| T5 | mobile-web 构建短路(F8):在 `scripts/mobile-web-build.cjs` 增加输入 mtime 检测(参考 `dev.cjs:420-456` 的实现),dist 新于全部输入时跳过 clean/install/build;`--force`/env 逃生口;dev.cjs 与 lint:rs:desktop 路径自动受益。验收:连续两次 `desktop:dev` 第二次跳过 mobile-web 构建;修改 mobile-web 源码后正确重建。 | `scripts/mobile-web-build.cjs`、`scripts/dev.cjs` | 低 | -| T6 | 修复 image 双版本(F9):`src/apps/desktop/Cargo.toml:85` 改为 `image = { workspace = true }`,如 API 不兼容则升级调用点到 0.25。验收:Cargo.lock 中 image 仅剩 0.25.x;`cargo check -p bitfun-desktop` 通过。 | `src/apps/desktop/Cargo.toml:85` | 低 | -| T7 | CI 拓扑并行化(F7):拆分 ci.yml 的 frontend-build 为 `frontend-checks`(lint/audit/test,不阻塞他人)与 `frontend-dist`(install + build:web + build:mobile-web + upload);`rust-build-check` 改 `needs: frontend-dist`。进一步验证 `cargo check/test` 是否只需 dist 目录存在——若是,用 mkdir 打桩彻底解除 needs。验收:PR 上 Rust job 提前开始,总流水线时长下降。 | `.github/workflows/ci.yml` | 中(改 CI 拓扑,需观察 1-2 个 PR) | -| T8 | 提交 Cargo.lock(F6):从 `.gitignore:26` 移除并提交根与 installer 两份 lockfile;CI 删除 `cargo generate-lockfile` 步骤;建立定期依赖更新流程后逐步解除 `Cargo.toml:98-107` 的 `=` 钉版。验收:rust-cache 命中率上升,CI 不再因上游发版突然变慢/失败。 | `.gitignore`、`.github/workflows/*.yml`、`Cargo.toml` | 中(流程变更,需团队确认依赖更新策略) | -| T9 | Vite watch 去轮询(F11):删除 `src/web-ui/vite.config.ts:68-74` 的 `usePolling/interval`,保留 ignored 列表;以 `VITE_USE_POLLING=1` 环境变量作为网络盘用户逃生口。验收:dev server 空闲 CPU 占用明显下降,HMR 正常。 | `src/web-ui/vite.config.ts` | 低(个别特殊文件系统需逃生口) | -| T10 | build.rs 确定性输出(F18):`assembly/core/build.rs` 与 `src/apps/cli/build.rs` 生成代码前对 key 排序(HashMap→BTreeMap)。验收:连续两次 clean build 生成的 OUT_DIR 文件字节一致。 | `src/crates/assembly/core/build.rs`、`src/apps/cli/build.rs` | 低 | -| T11(已完成) | reqwest TLS 单栈(F10):workspace 保持 transport-only,由真实客户端 owner 显式选择 Rustls;删除 native-tls 与无消费者的 Windows OpenSSL bootstrap,保留平台证书验证。验收:Cargo.lock 无 native-tls;Windows 产品图无 openssl-sys,非 Windows 的 Git/libgit2 路径不变;相关最小 feature 与产品入口编译通过。 | 根与客户端 owner `Cargo.toml`、相关调用点、既有 workflow | 已完成;平台信任根行为保留,无第二后端兼容层 | -| T12 | beforeBuildCommand 并行(F13):新增 `scripts/frontend-build-all.mjs` 并行跑 build:web 与 prepare:mobile-web,tauri.conf.json / tauri.dev.conf.json 的 beforeBuildCommand 指向它;dev.cjs 准备步骤改 Promise.all。验收:desktop:build 前端阶段时长≈max(两者) 而非 sum。 | `src/apps/desktop/tauri.conf.json`、`tauri.dev.conf.json`、`scripts/dev.cjs`、新脚本 | 低 | -| T13 | bitfun-core 拆分启动(F3,长期):先跑 `cargo build --timings` 与 `cargo tree -d` 存档基线;选 1-2 个低耦合子域(如 announcement、debug-log server)试点拆出独立 crate 并保留 re-export;结合 F12 的"dev 运行时读取提示词"改造。验收:改动试点子域后 `cargo build -p bitfun-desktop` 的重编 crate 数与耗时下降。 | `src/crates/assembly/core/**`、根 `Cargo.toml` members | 中-高(架构改动,分多个 PR 渐进) | -| T14 | 可选工具链增强(F5):提交 `.cargo/config.toml` 模板(注释形式提供 rust-lld 与 sccache 配置,默认不启用),团队自选开启;CI 冷构建可评估 sccache-action。验收:提供文档,默认行为不变。 | 新增 `.cargo/config.toml`、文档 | 低(默认关闭) | -| T15(已完成) | agent-runtime integration target 收敛(F20):保持全部 contract test 源与现有 CI 命令不变,将 27 个跨平台契约按定义、会话、交互、long-horizon 职责归为 4 个 target,Unix 原生进程测试保持独立;focused test 使用 `--test ::`。 | `agent-runtime/Cargo.toml`、`agent-runtime/tests/**`、现有 boundary rule 路径 | 已完成;总体 28→5,只减少重复编译/链接,不改变 feature 或依赖闭包 | - -### 快速收益组合(建议第一批实施) -T2 + T4 + T5 + T6 + T9 + T10 + T12:全部低风险,合计可显著改善日常 dev 循环(启动省 20-60s、增量链接提速、dev CPU 下降)与 build:web 时长;随后再做 T1(最大单项前端收益)、T3(打包 CI)、T7/T8(CI 结构)。 - ---- - -## 附:数据快照 - -- Rust workspace:36 members + installer 独立 workspace;总计约 577,903 行 Rust。Top crate:assembly/core 202,735 行、services-integrations 72,251、desktop 63,887、cli 53,976。本地 `target/` 实测 **98GB**(cargo-target-gc 存在的原因;F5 的 debuginfo 裁剪与 F9 去重也能显著降低该体积)。 -- `BitFun-Installer/src-tauri/Cargo.toml:61-65` 的 release profile 同样是 `lto=true + codegen-units=1`(opt-level="z"),T3 的 thin LTO 评估可一并覆盖。 -- Cargo.lock:1177 个包,112 个存在多版本(windows-sys ×6、phf ×6、getrandom ×4、nix ×4、rand ×3、quick-xml ×4 等)。 -- 前端:web-ui 1676 个 TS/TSX、366,580 行;dist 共 872 个 asset,JS 总量 14.3MB,入口 chunk 5.4MB(含 Monaco 内核);public/monaco-editor 14MB/103 文件。 -- 现有良好实践(保持):release-fast profile(Cargo.toml:288-293)、cargo-target-gc 缓存清理、Reqwest Rustls 单栈与平台证书验证、sherpa-onnx prebuilt、CI CARGO_INCREMENTAL=0 + debug=0(ci.yml:74-77)、swatinem/rust-cache、pnpm store 缓存、workspace.dependencies 统一版本声明。 +同时记录功能不变量、远程/平台差异、实际运行的最小验证和未运行的 CI。若产品 closure 不变,只能说明 +focused-test 或 owner 边界收益,不能宣称产品构建已经变快。 diff --git a/src/apps/cli/AGENTS.md b/src/apps/cli/AGENTS.md index 800436ba5c..7584aaf127 100644 --- a/src/apps/cli/AGENTS.md +++ b/src/apps/cli/AGENTS.md @@ -104,6 +104,13 @@ restrictions remain enforced. Detailed compatibility rules belong in the dedicated architecture documents, not in this file. +## Commands + +```bash +pnpm run cli:dev +pnpm run cli:install +``` + ## Verification Run the smallest checks matching the changed path: @@ -113,14 +120,11 @@ cargo check -p bitfun-cli cargo test -p bitfun-cli ``` -Also run focused owner tests when a surface crosses a shared boundary: - -- Agent Runtime port/SDK changes: `cargo test -p bitfun-agent-runtime` -- Shared IPC/protocol changes: `cargo test -p bitfun-agent-runtime-ipc` -- Core turn/tool/persistence behavior: the focused `bitfun-core` tests, then - the repository shared-Rust verification row -- terminal lifecycle/input changes: the nearest PTY/ConPTY or input test -- product/packaging changes: product assembly and archive smoke paths +When a CLI change crosses a shared boundary, use the focused command maintained +by that owner: Agent Runtime for port/SDK behavior, the IPC adapter for shared +protocol behavior, Core for turn/tool/persistence behavior, Terminal for +PTY/ConPTY lifecycle, and Product Assembly for packaging. Do not copy those +owners' commands into this guide. Use [`README.md`](README.md) for user-facing behavior and installation. Keep developer internals here or in architecture docs instead of expanding the user diff --git a/src/apps/desktop/AGENTS-CN.md b/src/apps/desktop/AGENTS-CN.md index 22faabdbfe..476cca882f 100644 --- a/src/apps/desktop/AGENTS-CN.md +++ b/src/apps/desktop/AGENTS-CN.md @@ -32,17 +32,14 @@ crate;`src/crates/assembly/core` 只保留产品装配与兼容桥接。 - 桌面端专属集成留在这里,不要下沉到共享 core - 窗口 lifecycle 行为(包括 close/minimize-to-tray 默认值)属于桌面端 surface;修改时必须保留用户已保存偏好。 -- 涉及打包或 release 请求时,参见顶层 `AGENTS.md` ## 命令 +以下命令用于桌面开发循环;验证命令只在下方“验证”章节维护。 + ```bash pnpm run desktop:dev pnpm run desktop:preview:debug -cargo check -p bitfun-desktop -cargo test -p bitfun-desktop -cargo build -p bitfun-desktop -pnpm run desktop:build:fast ``` ## 快速构建 @@ -53,6 +50,9 @@ pnpm run desktop:build:fast | `pnpm run desktop:build:release-fast` | 类 Release 构建,降低 LTO;需要 release 行为但无法等待完整 LTO 时使用 | | `pnpm run desktop:build:nsis:fast` | Windows 安装器,使用 `release-fast` profile;快速验证安装器 | +需要完整断点调试信息时设置 `CARGO_PROFILE_DEV_DEBUG=2`。默认 dev profile 保留行号信息, +同时减少 PDB 体积。 + ## Target 缓存 GC `desktop:dev`(退出时)、`desktop:preview:debug`(关闭时)以及 `desktop:build*` 会裁剪过期的 `target//incremental`(每个 crate 只留最新根)以及已无对应 `.fingerprint` 的孤儿 `deps`。**不会**按 mtime 删除 fingerprint(否则下次 `desktop:dev` 会冷编译)。手动执行:`pnpm run target:gc -- --profile debug`。禁用:`BITFUN_TARGET_GC=0`;演练:`BITFUN_TARGET_GC_DRY_RUN=1`。 diff --git a/src/apps/desktop/AGENTS.md b/src/apps/desktop/AGENTS.md index 1f776832e5..8dd3eadede 100644 --- a/src/apps/desktop/AGENTS.md +++ b/src/apps/desktop/AGENTS.md @@ -40,17 +40,15 @@ product wiring and compatibility bridges in `src/crates/assembly/core`. - Keep desktop-only integrations here; do not move them into shared core - Window lifecycle behavior, including close/minimize-to-tray defaults, is a desktop surface concern. Preserve saved user preferences when changing it. -- For packaging or release asks, see the top-level `AGENTS.md` ## Commands +Use these for the desktop development loop. Verification commands are kept in +the Verification section below. + ```bash pnpm run desktop:dev pnpm run desktop:preview:debug -cargo check -p bitfun-desktop -cargo test -p bitfun-desktop -cargo build -p bitfun-desktop -pnpm run desktop:build:fast ``` ## Fast builds @@ -61,6 +59,9 @@ pnpm run desktop:build:fast | `pnpm run desktop:build:release-fast` | Release-like build with reduced LTO; use when you need release behavior but can't wait for full LTO | | `pnpm run desktop:build:nsis:fast` | Windows installer using `release-fast` profile; for quick installer validation | +Set `CARGO_PROFILE_DEV_DEBUG=2` when full breakpoint debug information is +required. The default dev profile keeps line tables while reducing PDB size. + ## Target cache GC `desktop:dev` (on exit), `desktop:preview:debug` (on shutdown), and `desktop:build*` prune stale `target//incremental` roots (keep latest per crate) and true-orphan `deps` hashes with no matching `.fingerprint` directory. Fingerprints are never mtime-pruned (that forced cold rebuilds). Manual: `pnpm run target:gc -- --profile debug`. Disable with `BITFUN_TARGET_GC=0`; dry-run with `BITFUN_TARGET_GC_DRY_RUN=1`. diff --git a/src/crates/assembly/core/AGENTS-CN.md b/src/crates/assembly/core/AGENTS-CN.md index b9b1b3ef99..d9c9eab93c 100644 --- a/src/crates/assembly/core/AGENTS-CN.md +++ b/src/crates/assembly/core/AGENTS-CN.md @@ -89,16 +89,14 @@ SessionManager -> Session -> DialogTurn -> ModelRound ## 验证 -按触及行为选择最小检查: +Core 验证由本指南维护。每次只选择与改动匹配的一种命令模式,不要依次运行所有 feature 变体: ```bash -cargo check --workspace cargo check -p bitfun-core --no-default-features -cargo check -p bitfun-core --no-default-features --features workspace-runtime -cargo check -p bitfun-core --no-default-features --features remote-workspace -cargo check -p bitfun-core --no-default-features --features ssh-remote -cargo test -p bitfun-core --lib -- --nocapture -node scripts/check-core-boundaries.mjs +cargo check -p bitfun-core --no-default-features --features +cargo test -p bitfun-core --no-default-features --features --lib :: ``` -仅改文档时运行 `git diff --check`。 +feature-free facade 改动使用第一种,单一 feature 边界改动使用第二种,行为改动使用第三种。 +只有 Cargo feature、依赖方向或 test-target 布局变化时才运行 `pnpm run check:core-boundaries`。 +workspace check 与产品全量测试由 CI 兜底,不是 Core 默认预检。仅改文档时运行 `git diff --check`。 diff --git a/src/crates/assembly/core/AGENTS.md b/src/crates/assembly/core/AGENTS.md index 380a4d7b30..c9bff5aeeb 100644 --- a/src/crates/assembly/core/AGENTS.md +++ b/src/crates/assembly/core/AGENTS.md @@ -130,16 +130,18 @@ Narrower local guides already exist for some subtrees: ## Verification -Use the smallest check that matches the touched behavior: +This guide owns Core verification. Select one command pattern that matches the +change; do not run every feature variant: ```bash -cargo check --workspace cargo check -p bitfun-core --no-default-features -cargo check -p bitfun-core --no-default-features --features workspace-runtime -cargo check -p bitfun-core --no-default-features --features remote-workspace -cargo check -p bitfun-core --no-default-features --features ssh-remote -cargo test -p bitfun-core --lib -- --nocapture -node scripts/check-core-boundaries.mjs +cargo check -p bitfun-core --no-default-features --features +cargo test -p bitfun-core --no-default-features --features --lib :: ``` -For documentation-only changes, run `git diff --check`. +Use the first command when the feature-free facade changed, the second when one +feature boundary changed, and the third for behavior. Run +`pnpm run check:core-boundaries` only for Cargo features, dependency direction, +or test-target layout. Workspace checks and product-wide tests are CI-backed and +are not the default Core precheck. For documentation-only changes, run +`git diff --check`. diff --git a/src/crates/assembly/core/src/agentic/coordination/coordinator.rs b/src/crates/assembly/core/src/agentic/coordination/coordinator.rs index 0254be7e3c..96ceb20ab5 100644 --- a/src/crates/assembly/core/src/agentic/coordination/coordinator.rs +++ b/src/crates/assembly/core/src/agentic/coordination/coordinator.rs @@ -1401,6 +1401,8 @@ impl ConversationCoordinator { external_sources_supported: bool, expected_owner: Option, ) -> BitFunResult { + let external_sources_supported = + cfg!(feature = "external-sources") && external_sources_supported; let registry = get_agent_registry(); registry.load_custom_agents(workspace_root).await; let local_binding = registry.resolve_primary_agent_for_turn( @@ -1416,6 +1418,7 @@ impl ConversationCoordinator { }); } + #[cfg(feature = "external-sources")] if let Err(error) = crate::external_sources::ensure_external_source_workspace_snapshot(workspace_root).await { diff --git a/src/crates/assembly/core/src/agentic/session/session_manager.rs b/src/crates/assembly/core/src/agentic/session/session_manager.rs index 5eb7dcba00..9a9eaf0f14 100644 --- a/src/crates/assembly/core/src/agentic/session/session_manager.rs +++ b/src/crates/assembly/core/src/agentic/session/session_manager.rs @@ -5085,9 +5085,11 @@ impl SessionManager { } None => false, }; - let external_sources_supported = session.config.remote_connection_id.is_none() + let external_sources_supported = cfg!(feature = "external-sources") + && session.config.remote_connection_id.is_none() && session.config.remote_ssh_host.is_none() && !workspace_path_is_remote; + #[cfg(feature = "external-sources")] if external_sources_supported { if let Err(error) = crate::external_sources::ensure_external_source_workspace_snapshot( 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 4c9a073742..4ab99d8cef 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 @@ -17,7 +17,8 @@ use crate::native_hooks::{self, NativeHookSessionFacts}; use crate::util::elapsed_ms_u64; use crate::util::errors::{BitFunError, BitFunResult}; use bitfun_agent_runtime::permission::{ - PendingPermissionReceiver, PermissionRequestManager, PermissionWaitOutcome, + plan_permission_intents, PendingPermissionReceiver, PermissionIntentPlan, + PermissionRequestManager, PermissionWaitOutcome, }; use bitfun_agent_runtime::sdk::PermissionReplySource; use bitfun_agent_stream::ToolArgumentRepairKind; @@ -32,9 +33,8 @@ use bitfun_agent_tools::{ ToolExecutionErrorPresentation, GET_TOOL_SPEC_TOOL_NAME, USER_STEERING_INTERRUPTED_MESSAGE, }; use bitfun_runtime_ports::{ - wildcard_matches, PermissionEffect, PermissionGrant, PermissionReply, PermissionRequest, - PermissionRequestSource, PermissionRequestSourceKind, PermissionResourceCaseSensitivity, - ResolvedPermissionPolicy, RoundInjectionToolPreemption, + PermissionReply, PermissionRequest, PermissionRequestSource, PermissionRequestSourceKind, + PermissionResourceCaseSensitivity, RoundInjectionToolPreemption, }; use futures::future::join_all; use log::{debug, error, info, warn}; @@ -566,93 +566,6 @@ fn permission_resource_case_sensitivity( } } -fn permission_intent_effect( - intent: &PermissionIntent, - policy: &ResolvedPermissionPolicy, - grants: &[PermissionGrant], - case_sensitivity: PermissionResourceCaseSensitivity, -) -> PermissionEffect { - let evaluator = bitfun_runtime_ports::PermissionEvaluator::new(case_sensitivity); - let mut aggregate = PermissionEffect::Allow; - - for resource in &intent.resources { - let configured_effect = if intent.action == "bash" { - policy - .rules() - .iter() - .rev() - .find(|rule| { - wildcard_matches( - &intent.action, - &rule.action, - PermissionResourceCaseSensitivity::Sensitive, - ) && match rule.effect { - PermissionEffect::Allow => { - rule.resource == *resource - || (rule.action == "*" && rule.resource == "*") - } - PermissionEffect::Ask | PermissionEffect::Deny => { - wildcard_matches(resource, &rule.resource, case_sensitivity) - } - } - }) - .map(|rule| rule.effect) - .unwrap_or(PermissionEffect::Ask) - } else { - evaluator.evaluate_resource(&intent.action, resource, policy.rules()) - }; - let configured_effect = - policy - .constraint_layers() - .iter() - .fold(configured_effect, |effect, layer| { - effect.most_restrictive(evaluator.evaluate_constraint_resource( - &intent.action, - resource, - layer, - )) - }); - - match configured_effect { - PermissionEffect::Deny => return PermissionEffect::Deny, - PermissionEffect::Allow => {} - PermissionEffect::Ask => { - let remembered = grants.iter().any(|grant| { - if intent.action == "bash" { - grant.action == intent.action && grant.resource == *resource - } else { - wildcard_matches( - &intent.action, - &grant.action, - PermissionResourceCaseSensitivity::Sensitive, - ) && wildcard_matches(resource, &grant.resource, case_sensitivity) - } - }); - if !remembered { - aggregate = PermissionEffect::Ask; - } - } - } - } - - let effect = if intent.resources.is_empty() { - PermissionEffect::Ask - } else { - aggregate - }; - if effect != PermissionEffect::Deny - && intent - .display_metadata - .get("requiresFreshApproval") - .and_then(serde_json::Value::as_bool) - .unwrap_or(false) - { - PermissionEffect::Ask - } else { - effect - } -} - const SUBAGENT_LAUNCH_TOOL_NAME: &str = "Task"; /// Native hook session facts derived from one tool task. @@ -751,13 +664,10 @@ impl ToolPipeline { .map_err(|error| BitFunError::service(error.to_string()))?, None => Vec::new(), }; - let mut asks = Vec::new(); - - for intent in intents { - match permission_intent_effect(&intent, &permission_policy, &grants, case_sensitivity) { - PermissionEffect::Allow => {} - PermissionEffect::Ask => asks.push(intent), - PermissionEffect::Deny => { + let asks = + match plan_permission_intents(intents, &permission_policy, &grants, case_sensitivity) { + PermissionIntentPlan::Allowed => return Ok(PermissionPlanDraft::Allowed), + PermissionIntentPlan::Denied(intent) => { return Ok(PermissionPlanDraft::Rejected { reason: format!( "Permission policy denied '{}' for {}", @@ -766,12 +676,8 @@ impl ToolPipeline { ), }); } - } - } - - if asks.is_empty() { - return Ok(PermissionPlanDraft::Allowed); - } + PermissionIntentPlan::RequiresApproval(intents) => intents, + }; // A PreToolUse hook already approved this call. The approval reaches // here — after policy evaluation — precisely so that it waives only @@ -2474,10 +2380,11 @@ mod tests { }; use bitfun_runtime_ports::{ ClockPort, PermissionAuditEvent, PermissionAuditRecord, PermissionAuditStorePort, - PermissionConstraintLayer, PermissionGrant, PermissionGrantKey, PermissionGrantStorePort, - PermissionPolicyPreset, PermissionReplyStorePort, PermissionRule, PortResult, - RoundInjection, RoundInjectionExecutionPolicy, RoundInjectionKind, RoundInjectionTarget, - RoundInjectionToolPreemption, RuntimeServiceCapability, RuntimeServicePort, + PermissionConstraintLayer, PermissionEffect, PermissionGrant, PermissionGrantKey, + PermissionGrantStorePort, PermissionReplyStorePort, PermissionRule, PortResult, + ResolvedPermissionPolicy, RoundInjection, RoundInjectionExecutionPolicy, + RoundInjectionKind, RoundInjectionTarget, RoundInjectionToolPreemption, + RuntimeServiceCapability, RuntimeServicePort, }; use serde_json::json; use std::collections::HashMap; @@ -2538,80 +2445,7 @@ mod tests { } #[test] - fn bash_permission_allows_only_exact_command_grants() { - let intent = PermissionIntent::new("bash", vec!["git status && rm -rf build".to_string()]); - let wildcard_allow = ResolvedPermissionPolicy::new( - vec![PermissionRule::new( - "bash", - "git *", - PermissionEffect::Allow, - )], - Vec::new(), - ); - assert_eq!( - permission_intent_effect( - &intent, - &wildcard_allow, - &[], - PermissionResourceCaseSensitivity::Sensitive, - ), - PermissionEffect::Ask - ); - - let exact_allow = ResolvedPermissionPolicy::new( - vec![PermissionRule::new( - "bash", - "git status && rm -rf build", - PermissionEffect::Allow, - )], - Vec::new(), - ); - assert_eq!( - permission_intent_effect( - &intent, - &exact_allow, - &[], - PermissionResourceCaseSensitivity::Sensitive, - ), - PermissionEffect::Allow - ); - - let wildcard_deny = ResolvedPermissionPolicy::new( - vec![PermissionRule::new("bash", "*", PermissionEffect::Deny)], - Vec::new(), - ); - assert_eq!( - permission_intent_effect( - &intent, - &wildcard_deny, - &[], - PermissionResourceCaseSensitivity::Sensitive, - ), - PermissionEffect::Deny - ); - } - - #[test] - fn full_access_baseline_allows_bash_commands() { - let intent = PermissionIntent::new("bash", vec!["git status && rm -rf build".to_string()]); - let full_access_rules = ResolvedPermissionPolicy::new( - PermissionPolicyPreset::FullAccess.baseline_rules(), - Vec::new(), - ); - - assert_eq!( - permission_intent_effect( - &intent, - &full_access_rules, - &[], - PermissionResourceCaseSensitivity::Sensitive, - ), - PermissionEffect::Allow - ); - } - - #[test] - fn account_scoped_fresh_approval_works_without_a_workspace_and_ignores_allow_rules() { + fn account_scoped_permission_works_without_a_workspace() { let mut intent = PermissionIntent::new( "page_publish", vec!["page:demo; visibility=private; deploy=saved-version-only".to_string()], @@ -2631,41 +2465,6 @@ mod tests { ACCOUNT_PERMISSION_PROJECT_PATH.to_string(), ) ); - - let allow = ResolvedPermissionPolicy::new( - vec![PermissionRule::new( - "page_publish", - "*", - PermissionEffect::Allow, - )], - Vec::new(), - ); - assert_eq!( - permission_intent_effect( - &intent, - &allow, - &[], - PermissionResourceCaseSensitivity::Sensitive, - ), - PermissionEffect::Ask - ); - let deny = ResolvedPermissionPolicy::new( - vec![PermissionRule::new( - "page_publish", - "*", - PermissionEffect::Deny, - )], - Vec::new(), - ); - assert_eq!( - permission_intent_effect( - &intent, - &deny, - &[], - PermissionResourceCaseSensitivity::Sensitive, - ), - PermissionEffect::Deny - ); } #[test] diff --git a/src/crates/execution/agent-runtime/AGENTS.md b/src/crates/execution/agent-runtime/AGENTS.md index 16ba17d8a0..de5ab03d3e 100644 --- a/src/crates/execution/agent-runtime/AGENTS.md +++ b/src/crates/execution/agent-runtime/AGENTS.md @@ -26,10 +26,11 @@ port-backed `sdk` / `AgentRuntime` facade that can be built and tested without through `bitfun_agent_runtime::runtime`; those surfaces must use `sdk` or projected Server/API DTOs. - Keep concrete scheduler/session lifecycle execution, session metadata IO, - event emitter wiring, permission UI presentation, and product `Tool` adapter - execution in `bitfun-core` until a reviewed owner migration proves behavior - equivalence. Provider-neutral confirmation gate/wait-channel and user-question state - may live here. + event emitter wiring, workspace/remote permission-scope projection, native + permission Hook ordering, permission UI presentation, and product `Tool` + adapter execution in `bitfun-core` until a reviewed owner migration proves + behavior equivalence. Provider-neutral permission policy/grant planning, + confirmation gate/wait-channel, and user-question state may live here. - Prefer pure facts and decisions first: queue policy, background delivery, dialog-turn queue state, active-turn facts, cancellation routing and suppression state, background running-turn injection construction, steering action @@ -89,9 +90,15 @@ so `autotests = false` cannot silently omit a new contract. ## Verification +Use the focused contract form by default. Run the package-wide form only when a +change crosses several runtime targets: + ```bash -cargo test -p bitfun-agent-runtime -cargo test -p bitfun-agent-runtime --test agent_definition_contracts prompt_contracts:: -node scripts/check-core-boundaries.mjs -cargo check -p bitfun-core --features product-full +cargo test --locked -p bitfun-agent-runtime --test :: +cargo test --locked -p bitfun-agent-runtime ``` + +Run `pnpm run check:core-boundaries` only when Cargo dependencies, explicit test +targets, or grouped-root layout changed. Core product assembly and +`product-full` verification belong to Core or the consuming product guide, not +to this module's default checklist. diff --git a/src/crates/execution/agent-runtime/src/permission.rs b/src/crates/execution/agent-runtime/src/permission.rs index 9549fa4d2a..b05171fa6e 100644 --- a/src/crates/execution/agent-runtime/src/permission.rs +++ b/src/crates/execution/agent-runtime/src/permission.rs @@ -1,13 +1,12 @@ -//! Process-local pending permission requests and reply coordination. -//! -//! This owner is intentionally not connected to the legacy tool confirmation -//! pipeline yet. It persists remembered grants and audit facts only when an -//! explicit permission reply is delivered through this standalone contract. +//! Provider-neutral permission planning, pending requests, and reply coordination. +use bitfun_agent_tools::PermissionIntent; use bitfun_runtime_ports::{ - ClockPort, PermissionAuditEvent, PermissionAuditRecord, PermissionAuditStorePort, - PermissionGrant, PermissionGrantStorePort, PermissionReply, PermissionReplySource, - PermissionReplyStorePort, PermissionRequest, PermissionRequestEvent, PortError, + wildcard_matches, ClockPort, PermissionAuditEvent, PermissionAuditRecord, + PermissionAuditStorePort, PermissionEffect, PermissionEvaluator, PermissionGrant, + PermissionGrantStorePort, PermissionReply, PermissionReplySource, PermissionReplyStorePort, + PermissionRequest, PermissionRequestEvent, PermissionResourceCaseSensitivity, PortError, + ResolvedPermissionPolicy, }; use dashmap::DashMap; use std::collections::{HashMap, HashSet}; @@ -23,6 +22,131 @@ const PERMISSION_EVENT_CAPACITY: usize = 128; /// policies (such as CLI `--auto`) separate from persisted user preferences. pub const AUTO_APPROVE_ASK_CONTEXT_KEY: &str = "auto_approve_ask"; +/// Provider-neutral result of applying resolved policy and remembered grants. +/// +/// Product orchestration remains responsible for scope derivation, native hooks, +/// interactive request construction, and tool execution. +#[derive(Debug, Clone, PartialEq)] +pub enum PermissionIntentPlan { + Allowed, + Denied(PermissionIntent), + RequiresApproval(Vec), +} + +/// Applies the resolved permission policy to one ordered set of tool intents. +/// +/// A denial short-circuits the whole set. Intents that still require approval +/// keep their input order so the product layer can create deterministic requests. +pub fn plan_permission_intents( + intents: Vec, + policy: &ResolvedPermissionPolicy, + grants: &[PermissionGrant], + case_sensitivity: PermissionResourceCaseSensitivity, +) -> PermissionIntentPlan { + let mut approvals = Vec::new(); + + for intent in intents { + match permission_intent_effect(&intent, policy, grants, case_sensitivity) { + PermissionEffect::Allow => {} + PermissionEffect::Ask => approvals.push(intent), + PermissionEffect::Deny => return PermissionIntentPlan::Denied(intent), + } + } + + if approvals.is_empty() { + PermissionIntentPlan::Allowed + } else { + PermissionIntentPlan::RequiresApproval(approvals) + } +} + +fn permission_intent_effect( + intent: &PermissionIntent, + policy: &ResolvedPermissionPolicy, + grants: &[PermissionGrant], + case_sensitivity: PermissionResourceCaseSensitivity, +) -> PermissionEffect { + let evaluator = PermissionEvaluator::new(case_sensitivity); + let mut aggregate = PermissionEffect::Allow; + + for resource in &intent.resources { + let configured_effect = if intent.action == "bash" { + policy + .rules() + .iter() + .rev() + .find(|rule| { + wildcard_matches( + &intent.action, + &rule.action, + PermissionResourceCaseSensitivity::Sensitive, + ) && match rule.effect { + PermissionEffect::Allow => { + rule.resource == *resource + || (rule.action == "*" && rule.resource == "*") + } + PermissionEffect::Ask | PermissionEffect::Deny => { + wildcard_matches(resource, &rule.resource, case_sensitivity) + } + } + }) + .map(|rule| rule.effect) + .unwrap_or(PermissionEffect::Ask) + } else { + evaluator.evaluate_resource(&intent.action, resource, policy.rules()) + }; + let configured_effect = + policy + .constraint_layers() + .iter() + .fold(configured_effect, |effect, layer| { + effect.most_restrictive(evaluator.evaluate_constraint_resource( + &intent.action, + resource, + layer, + )) + }); + + match configured_effect { + PermissionEffect::Deny => return PermissionEffect::Deny, + PermissionEffect::Allow => {} + PermissionEffect::Ask => { + let remembered = grants.iter().any(|grant| { + if intent.action == "bash" { + grant.action == intent.action && grant.resource == *resource + } else { + wildcard_matches( + &intent.action, + &grant.action, + PermissionResourceCaseSensitivity::Sensitive, + ) && wildcard_matches(resource, &grant.resource, case_sensitivity) + } + }); + if !remembered { + aggregate = PermissionEffect::Ask; + } + } + } + } + + let effect = if intent.resources.is_empty() { + PermissionEffect::Ask + } else { + aggregate + }; + if effect != PermissionEffect::Deny + && intent + .display_metadata + .get("requiresFreshApproval") + .and_then(serde_json::Value::as_bool) + .unwrap_or(false) + { + PermissionEffect::Ask + } else { + effect + } +} + pub type PermissionRequestEventReceiver = broadcast::Receiver; #[derive(Debug, Clone, PartialEq)] diff --git a/src/crates/execution/agent-runtime/tests/agent_interaction_contracts/permission_contracts.rs b/src/crates/execution/agent-runtime/tests/agent_interaction_contracts/permission_contracts.rs index 344d629726..deab9f59ff 100644 --- a/src/crates/execution/agent-runtime/tests/agent_interaction_contracts/permission_contracts.rs +++ b/src/crates/execution/agent-runtime/tests/agent_interaction_contracts/permission_contracts.rs @@ -1,15 +1,19 @@ use async_trait::async_trait; use bitfun_agent_runtime::permission::{ - PermissionRequestManager, PermissionRequestManagerError, PermissionWaitOutcome, + plan_permission_intents, PermissionIntentPlan, PermissionRequestManager, + PermissionRequestManagerError, PermissionWaitOutcome, }; +use bitfun_agent_tools::PermissionIntent; use bitfun_runtime_ports::{ - ClockPort, PermissionAuditRecord, PermissionAuditStorePort, PermissionGrant, - PermissionGrantKey, PermissionGrantStorePort, PermissionReply, PermissionReplySource, - PermissionReplyStorePort, PermissionRequest, PermissionRequestEvent, PermissionRequestSource, - PermissionRequestSourceKind, PortError, PortErrorKind, PortResult, RuntimeServiceCapability, + ClockPort, PermissionAuditRecord, PermissionAuditStorePort, PermissionConstraintLayer, + PermissionEffect, PermissionGrant, PermissionGrantKey, PermissionGrantStorePort, + PermissionPolicyPreset, PermissionReply, PermissionReplySource, PermissionReplyStorePort, + PermissionRequest, PermissionRequestEvent, PermissionRequestSource, + PermissionRequestSourceKind, PermissionResourceCaseSensitivity, PermissionRule, PortError, + PortErrorKind, PortResult, ResolvedPermissionPolicy, RuntimeServiceCapability, RuntimeServicePort, }; -use serde_json::Map; +use serde_json::{json, Map}; use std::sync::{Arc, Mutex}; #[derive(Debug, Default)] @@ -20,6 +24,320 @@ struct RecordingPermissionStore { fail_audit: Mutex, } +#[test] +fn bash_allow_rules_and_remembered_grants_require_exact_commands() { + let command = "git status && rm -rf build"; + let intent = PermissionIntent::new("bash", vec![command.to_string()]); + let wildcard_allow = ResolvedPermissionPolicy::new( + vec![PermissionRule::new( + "bash", + "git *", + PermissionEffect::Allow, + )], + Vec::new(), + ); + let wildcard_grant = PermissionGrant { + project_id: "project-a".to_string(), + action: "bash".to_string(), + resource: "git *".to_string(), + created_at_ms: 1, + }; + + assert_eq!( + plan_permission_intents( + vec![intent.clone()], + &wildcard_allow, + &[wildcard_grant], + PermissionResourceCaseSensitivity::Sensitive, + ), + PermissionIntentPlan::RequiresApproval(vec![intent.clone()]) + ); + + let exact_allow = ResolvedPermissionPolicy::new( + vec![PermissionRule::new( + "bash", + command, + PermissionEffect::Allow, + )], + Vec::new(), + ); + assert_eq!( + plan_permission_intents( + vec![intent.clone()], + &exact_allow, + &[], + PermissionResourceCaseSensitivity::Sensitive, + ), + PermissionIntentPlan::Allowed + ); + + let ask = ResolvedPermissionPolicy::new( + vec![PermissionRule::new("bash", "*", PermissionEffect::Ask)], + Vec::new(), + ); + let exact_grant = PermissionGrant { + project_id: "project-a".to_string(), + action: "bash".to_string(), + resource: command.to_string(), + created_at_ms: 2, + }; + assert_eq!( + plan_permission_intents( + vec![intent], + &ask, + &[exact_grant], + PermissionResourceCaseSensitivity::Sensitive, + ), + PermissionIntentPlan::Allowed + ); + + let denied_intent = PermissionIntent::new("bash", vec![command.to_string()]); + let wildcard_deny = ResolvedPermissionPolicy::new( + vec![PermissionRule::new("bash", "*", PermissionEffect::Deny)], + Vec::new(), + ); + assert_eq!( + plan_permission_intents( + vec![denied_intent.clone()], + &wildcard_deny, + &[], + PermissionResourceCaseSensitivity::Sensitive, + ), + PermissionIntentPlan::Denied(denied_intent) + ); +} + +#[test] +fn full_access_baseline_allows_bash_commands() { + let policy = ResolvedPermissionPolicy::new( + PermissionPolicyPreset::FullAccess.baseline_rules(), + Vec::new(), + ); + + assert_eq!( + plan_permission_intents( + vec![PermissionIntent::new( + "bash", + vec!["git status && rm -rf build".to_string()], + )], + &policy, + &[], + PermissionResourceCaseSensitivity::Sensitive, + ), + PermissionIntentPlan::Allowed + ); +} + +#[test] +fn fresh_approval_never_weakens_a_policy_denial() { + let mut intent = PermissionIntent::new("page_publish", vec!["page:demo".to_string()]); + intent + .display_metadata + .insert("requiresFreshApproval".to_string(), json!(true)); + let allow = ResolvedPermissionPolicy::new( + vec![PermissionRule::new( + "page_publish", + "*", + PermissionEffect::Allow, + )], + Vec::new(), + ); + assert_eq!( + plan_permission_intents( + vec![intent.clone()], + &allow, + &[PermissionGrant { + project_id: "project-a".to_string(), + action: "page_publish".to_string(), + resource: "page:demo".to_string(), + created_at_ms: 1, + }], + PermissionResourceCaseSensitivity::Sensitive, + ), + PermissionIntentPlan::RequiresApproval(vec![intent.clone()]) + ); + + let ask = ResolvedPermissionPolicy::new( + vec![PermissionRule::new( + "page_publish", + "*", + PermissionEffect::Ask, + )], + Vec::new(), + ); + assert_eq!( + plan_permission_intents( + vec![intent.clone()], + &ask, + &[PermissionGrant { + project_id: "project-a".to_string(), + action: "page_publish".to_string(), + resource: "page:demo".to_string(), + created_at_ms: 2, + }], + PermissionResourceCaseSensitivity::Sensitive, + ), + PermissionIntentPlan::RequiresApproval(vec![intent.clone()]) + ); + + let deny = ResolvedPermissionPolicy::new( + vec![PermissionRule::new( + "page_publish", + "*", + PermissionEffect::Deny, + )], + Vec::new(), + ); + let denied_intent = intent.clone(); + assert_eq!( + plan_permission_intents( + vec![intent], + &deny, + &[], + PermissionResourceCaseSensitivity::Sensitive, + ), + PermissionIntentPlan::Denied(denied_intent) + ); +} + +#[test] +fn empty_inputs_and_resources_keep_fail_closed_defaults() { + let policy = ResolvedPermissionPolicy::new(Vec::new(), Vec::new()); + assert_eq!( + plan_permission_intents( + Vec::new(), + &policy, + &[], + PermissionResourceCaseSensitivity::Sensitive, + ), + PermissionIntentPlan::Allowed + ); + + let resource_free = PermissionIntent::new("custom_action", Vec::new()); + assert_eq!( + plan_permission_intents( + vec![resource_free.clone()], + &policy, + &[], + PermissionResourceCaseSensitivity::Sensitive, + ), + PermissionIntentPlan::RequiresApproval(vec![resource_free]) + ); +} + +#[test] +fn approval_order_is_stable_and_a_later_denial_short_circuits_the_set() { + let first = PermissionIntent::new("edit", vec!["src/first.rs".to_string()]); + let allowed = PermissionIntent::new("read", vec!["README.md".to_string()]); + let second = PermissionIntent::new("edit", vec!["src/second.rs".to_string()]); + let ask_with_read_allow = ResolvedPermissionPolicy::new( + vec![ + PermissionRule::new("*", "*", PermissionEffect::Ask), + PermissionRule::new("read", "*", PermissionEffect::Allow), + ], + Vec::new(), + ); + assert_eq!( + plan_permission_intents( + vec![first.clone(), allowed, second.clone()], + &ask_with_read_allow, + &[], + PermissionResourceCaseSensitivity::Sensitive, + ), + PermissionIntentPlan::RequiresApproval(vec![first.clone(), second]) + ); + + let denied = PermissionIntent::new("edit", vec!["src/private/key.rs".to_string()]); + let deny_private = ResolvedPermissionPolicy::new( + vec![ + PermissionRule::new("edit", "*", PermissionEffect::Ask), + PermissionRule::new("edit", "src/private/*", PermissionEffect::Deny), + ], + Vec::new(), + ); + assert_eq!( + plan_permission_intents( + vec![first, denied.clone()], + &deny_private, + &[], + PermissionResourceCaseSensitivity::Sensitive, + ), + PermissionIntentPlan::Denied(denied) + ); +} + +#[test] +fn constraint_layers_can_tighten_but_never_widen_host_policy() { + let intent = PermissionIntent::new("edit", vec!["src/main.rs".to_string()]); + let tightened = ResolvedPermissionPolicy::new( + vec![PermissionRule::new("edit", "*", PermissionEffect::Allow)], + vec![PermissionConstraintLayer::new(vec![PermissionRule::new( + "edit", + "src/main.rs", + PermissionEffect::Deny, + )])], + ); + assert_eq!( + plan_permission_intents( + vec![intent.clone()], + &tightened, + &[], + PermissionResourceCaseSensitivity::Sensitive, + ), + PermissionIntentPlan::Denied(intent.clone()) + ); + + let cannot_widen = ResolvedPermissionPolicy::new( + vec![PermissionRule::new("edit", "*", PermissionEffect::Ask)], + vec![PermissionConstraintLayer::new(vec![PermissionRule::new( + "edit", + "src/main.rs", + PermissionEffect::Allow, + )])], + ); + assert_eq!( + plan_permission_intents( + vec![intent.clone()], + &cannot_widen, + &[], + PermissionResourceCaseSensitivity::Sensitive, + ), + PermissionIntentPlan::RequiresApproval(vec![intent]) + ); +} + +#[test] +fn resource_matching_uses_the_host_project_case_sensitivity() { + let intent = PermissionIntent::new("edit", vec!["SRC/Main.rs".to_string()]); + let policy = ResolvedPermissionPolicy::new( + vec![PermissionRule::new( + "edit", + "src/main.rs", + PermissionEffect::Allow, + )], + Vec::new(), + ); + + assert_eq!( + plan_permission_intents( + vec![intent.clone()], + &policy, + &[], + PermissionResourceCaseSensitivity::Insensitive, + ), + PermissionIntentPlan::Allowed + ); + assert_eq!( + plan_permission_intents( + vec![intent.clone()], + &policy, + &[], + PermissionResourceCaseSensitivity::Sensitive, + ), + PermissionIntentPlan::RequiresApproval(vec![intent]) + ); +} + impl RuntimeServicePort for RecordingPermissionStore { fn capability(&self) -> RuntimeServiceCapability { RuntimeServiceCapability::Permission diff --git a/src/mobile-web/AGENTS.md b/src/mobile-web/AGENTS.md index 600496da2b..468942be67 100644 --- a/src/mobile-web/AGENTS.md +++ b/src/mobile-web/AGENTS.md @@ -32,4 +32,8 @@ pnpm --dir src/mobile-web run type-check pnpm run build:mobile-web ``` +The build skips work when `src/mobile-web/dist` is newer than every input. Use +`BITFUN_MOBILE_WEB_FORCE_BUILD=1` or `node scripts/mobile-web-build.cjs --force` +only when a rebuild is required despite unchanged inputs. + For pairing, reconnect, disconnect, or chat behavior changes, also describe manual verification in the PR, including the browser/device used and the observed state transitions. diff --git a/src/web-ui/AGENTS-CN.md b/src/web-ui/AGENTS-CN.md index 529f863ace..75764b889e 100644 --- a/src/web-ui/AGENTS-CN.md +++ b/src/web-ui/AGENTS-CN.md @@ -45,18 +45,17 @@ Peer Device Mode(同账号远程完整客户端)的边界见 `docs/architect ## 命令 -以下命令仅供参考,不是默认预检清单;PR 应按下方“验证”选择范围。 +这里只维护开发/构建入口;验证命令统一放在下方“验证”章节。 ```bash pnpm --dir src/web-ui dev -pnpm --dir src/web-ui run lint -pnpm --dir src/web-ui run type-check -pnpm --dir src/web-ui run test:run # 大范围测试;本地优先用精确路径 -pnpm run i18n:contract:test -pnpm run i18n:audit pnpm run build:web # 构建相关改动或复现 CI ``` +`pnpm run build:web` 会并发执行类型检查与 Vite 构建,错误出现顺序不固定,输出分别带 +`[type-check]` / `[vite-build]` 前缀。只有网络盘或 WSL 挂载等原生文件事件漏报场景才设置 +`VITE_USE_POLLING=1`。 + ## 验证 按改动范围选择最小检查: diff --git a/src/web-ui/AGENTS.md b/src/web-ui/AGENTS.md index 164d8f2394..29521f1f98 100644 --- a/src/web-ui/AGENTS.md +++ b/src/web-ui/AGENTS.md @@ -54,19 +54,19 @@ Self-Hosted entries must open `RelayDeployWizard`, not an external README. ## Commands -These are command references, not the default precheck list. Use Verification -below for PR scope. +Keep development/build entry points here. Verification commands are maintained +only in the section below. ```bash pnpm --dir src/web-ui dev -pnpm --dir src/web-ui run lint -pnpm --dir src/web-ui run type-check -pnpm --dir src/web-ui run test:run # broad suite; prefer focused paths locally -pnpm run i18n:contract:test -pnpm run i18n:audit pnpm run build:web # build-impacting changes / CI reproduction ``` +`pnpm run build:web` runs type-check and Vite concurrently; either error may +appear first and their output uses `[type-check]` / `[vite-build]` prefixes. +Set `VITE_USE_POLLING=1` only when native file events miss changes, typically on +a network drive or WSL mount. + ## Verification Choose the smallest matching check: