diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 7324baa1d3..b41b47c2a2 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -204,6 +204,50 @@ jobs:
save-if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
cache-on-failure: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
+ # sherpa-onnx-sys downloads its native static libraries from GitHub
+ # Releases at build time. Hosted runners intermittently fail that
+ # download (network/rate limits), so pre-fetch and extract the archive
+ # here, then point SHERPA_ONNX_LIB_DIR at the extracted lib directory.
+ # The build script uses that variable directly without any cache/rerun
+ # logic, so both `cargo check` and `cargo test` link against the same
+ # pre-fetched libraries. Windows is skipped: its archive is already
+ # cached in the rust-cache path and the previous failures were Linux and
+ # macOS only.
+ - name: Pre-download sherpa-onnx native libraries
+ if: runner.os != 'Windows'
+ shell: bash
+ env:
+ SHERPA_VERSION: "1.13.4"
+ run: |
+ set -euo pipefail
+ case "${{ runner.os }}" in
+ Linux)
+ archive="sherpa-onnx-v${SHERPA_VERSION}-linux-x64-static-lib.tar.bz2"
+ ;;
+ macOS)
+ archive="sherpa-onnx-v${SHERPA_VERSION}-osx-arm64-static-lib.tar.bz2"
+ ;;
+ *)
+ exit 0
+ ;;
+ esac
+ mkdir -p "$RUNNER_TEMP/sherpa-onnx-libs"
+ archive_path="$RUNNER_TEMP/sherpa-onnx-libs/$archive"
+ if [ ! -f "$archive_path" ]; then
+ curl -fL --retry 5 --retry-all-errors \
+ "https://github.com/k2-fsa/sherpa-onnx/releases/download/v${SHERPA_VERSION}/${archive}" \
+ -o "$archive_path"
+ fi
+ lib_dir="$RUNNER_TEMP/sherpa-onnx-libs/lib"
+ if [ ! -d "$lib_dir" ]; then
+ tar -xjf "$archive_path" -C "$RUNNER_TEMP/sherpa-onnx-libs"
+ # The archive extracts to a versioned directory; its lib/ is the
+ # native library directory the build script expects.
+ lib_dir="$(find "$RUNNER_TEMP/sherpa-onnx-libs" -maxdepth 2 -type d -name lib | head -n 1)"
+ fi
+ test -n "$lib_dir" && test -f "$lib_dir/libsherpa-onnx-c-api.a"
+ echo "SHERPA_ONNX_LIB_DIR=$lib_dir" >> "$GITHUB_ENV"
+
- name: Check compilation
run: cargo check --locked --workspace
diff --git a/.github/workflows/desktop-package.yml b/.github/workflows/desktop-package.yml
index 32a861682b..37a9b4b75e 100644
--- a/.github/workflows/desktop-package.yml
+++ b/.github/workflows/desktop-package.yml
@@ -98,7 +98,7 @@ jobs:
env:
NODE_OPTIONS: --max-old-space-size=6144
BITFUN_ENABLE_UPDATER_ARTIFACTS: ${{ needs.prepare.outputs.upload_to_release }}
- TAURI_UPDATER_ENDPOINT: https://github.com/GCWing/BitFun/releases/latest/download/latest.json
+ TAURI_UPDATER_ENDPOINT: https://github.com/${{ github.repository }}/releases/latest/download/latest.json
TAURI_UPDATER_PUBKEY: ${{ secrets.TAURI_UPDATER_PUBKEY }}
# Same trust root, compiled into the Desktop binary so one-click relay
# deploy can verify the signed checksum locally and hand the remote host
@@ -285,7 +285,7 @@ jobs:
contents: write
packages: write
env:
- IMAGE: ghcr.io/gcwing/bitfun-relay-server
+ IMAGE: ghcr.io/${{ github.repository_owner }}/bitfun-relay-server
steps:
- name: Checkout
@@ -311,7 +311,7 @@ jobs:
set -euo pipefail
mkdir -p linux-release-assets
gh release download "${RELEASE_TAG}" \
- --repo GCWing/BitFun \
+ --repo "${{ github.repository }}" \
--dir linux-release-assets \
--pattern 'bitfun-relay-server-*.tar.gz' \
--pattern 'bitfun-relay-server-*.tar.gz.sha256'
@@ -359,7 +359,7 @@ jobs:
if [[ "${IMAGE_ONLY}" == "true" ]]; then
# Backfilling an older release must not roll the floating tag
# backwards. GitHub's latest endpoint excludes prereleases.
- latest_release="$(gh api repos/GCWing/BitFun/releases/latest --jq .tag_name)"
+ latest_release="$(gh api repos/${{ github.repository }}/releases/latest --jq .tag_name)"
if [[ "${RELEASE_TAG}" == "${latest_release}" ]]; then
echo "${IMAGE}:latest"
fi
@@ -537,7 +537,7 @@ jobs:
--manual-assets-dir release-manual-assets \
--version "${{ needs.prepare.outputs.version }}" \
--tag "${{ needs.prepare.outputs.release_tag }}" \
- --repo "GCWing/BitFun" \
+ --repo "${{ github.repository }}" \
--out release-updater-assets/latest.json \
--required-platforms "${REQUIRED_UPDATER_PLATFORMS}"
@@ -555,7 +555,7 @@ jobs:
--assets-dir linux-release-assets \
--version "${{ needs.prepare.outputs.version }}" \
--tag "${{ needs.prepare.outputs.release_tag }}" \
- --repo "GCWing/BitFun" \
+ --repo "${{ github.repository }}" \
--out linux-release-assets/linux-binaries.json
# The Tauri bundler signs the five updater artifacts during `tauri build`,
@@ -616,7 +616,7 @@ jobs:
- name: Verify published updater manifest
run: |
curl -fsSL --retry 5 --retry-delay 3 \
- "https://github.com/GCWing/BitFun/releases/download/${{ needs.prepare.outputs.release_tag }}/latest.json" \
+ "https://github.com/${{ github.repository }}/releases/download/${{ needs.prepare.outputs.release_tag }}/latest.json" \
-o latest.published.json
node scripts/verify-tauri-latest-json.mjs \
--manifest latest.published.json \
@@ -628,7 +628,7 @@ jobs:
- name: Verify published Linux binaries manifest
run: |
curl -fsSL --retry 5 --retry-delay 3 \
- "https://github.com/GCWing/BitFun/releases/download/${{ needs.prepare.outputs.release_tag }}/linux-binaries.json" \
+ "https://github.com/${{ github.repository }}/releases/download/${{ needs.prepare.outputs.release_tag }}/linux-binaries.json" \
-o linux-binaries.published.json
test "$(jq -r '.version' linux-binaries.published.json)" = "${{ needs.prepare.outputs.version }}"
while IFS= read -r cli_url; do
@@ -639,13 +639,13 @@ jobs:
- name: Verify published Relay image descriptor
run: |
curl -fsSL --retry 5 --retry-delay 3 \
- "https://github.com/GCWing/BitFun/releases/download/${{ needs.prepare.outputs.release_tag }}/relay-image.json" \
+ "https://github.com/${{ github.repository }}/releases/download/${{ needs.prepare.outputs.release_tag }}/relay-image.json" \
-o relay-image.published.json
test "$(jq -r '.tag' relay-image.published.json)" = "${{ needs.prepare.outputs.release_tag }}"
- test "$(jq -r '.image' relay-image.published.json)" = "ghcr.io/gcwing/bitfun-relay-server"
+ test "$(jq -r '.image' relay-image.published.json)" = "ghcr.io/${{ github.repository_owner }}/bitfun-relay-server"
jq -e '.digest | test("^sha256:[0-9a-f]{64}$")' relay-image.published.json >/dev/null
curl -fsSL --retry 5 --retry-delay 3 \
- "https://github.com/GCWing/BitFun/releases/download/${{ needs.prepare.outputs.release_tag }}/relay-image.json.sig" \
+ "https://github.com/${{ github.repository }}/releases/download/${{ needs.prepare.outputs.release_tag }}/relay-image.json.sig" \
-o /dev/null
# Nudge the openbitfun.com mirror to sync now instead of on its next
diff --git a/.gitignore b/.gitignore
index 95806da18c..329b666a45 100644
--- a/.gitignore
+++ b/.gitignore
@@ -23,11 +23,44 @@ dist-ssr
# Build outputs - Rust/Tauri
target/
**/target/
+.target/
/.targets/
+# Local evidence working dir (fully ignored; intermediate artifacts go to
+# outside the repo)
+target2/
# The deployable Rust services use the workspace lockfile for reproducible
# container builds.
!Cargo.lock
+# Work artifacts that must never enter the repo (S-37/S-56 hygiene):
+# recon/fix/verify/report/sediment intermediates, sync records, ws-check data.
+/docs/plans/RECON-*
+/docs/plans/FIX-*
+/docs/plans/VERIFY-*
+/docs/plans/REPORT-*
+/docs/plans/SEDIMENT-*
+/docs/plans/sync-record-*
+/docs/plans/recon-*
+/docs/plans/fix-*
+/docs/plans/doc-governance-report-*
+/docs/plans/pr-final-*
+/docs/plans/ws-check-*
+/docs/plans/del-*.json
+/docs/plans/侦查-*
+/docs/plans/核对-*
+/docs/plans/核查-*
+/docs/plans/*.log
+/docs/plans/*.json
+/docs/plans/*.cjs
+
+# Local customization docs stay out of the repo (S-56 sanitize)
+
+/docs/功能文档/
+/交接文档-现状与决议.md
+/fix-dualfeed-停止回报.md
+/docs/plans/review-upstream-sync-*
+/docs/features/agent-hot-reload.md
+
# Monaco Editor - copied from node_modules
public/monaco-editor/
src/web-ui/public/monaco-editor/
@@ -91,5 +124,9 @@ external/
/.bitfun/search/flashgrep-index/
.agents/
/.flashgrep-index-engine/
+/src/apps/desktop/.bitfun/search/flashgrep-index/
+/target/debug/.bitfun/search/flashgrep-index/
.design/
+__pycache__/
+*.pyc
diff --git a/Cargo.lock b/Cargo.lock
index a2e1ab33ec..1b1162eab3 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -778,6 +778,7 @@ dependencies = [
"tokio",
"tokio-util",
"uuid",
+ "which 8.0.5",
]
[[package]]
@@ -1092,6 +1093,7 @@ dependencies = [
"log",
"md5",
"notify",
+ "rand 0.8.7",
"regex",
"reqwest",
"rusqlite",
@@ -1514,9 +1516,11 @@ dependencies = [
"bitfun-events",
"bitfun-runtime-ports",
"chrono",
+ "dashmap",
"dunce",
"filetime",
"fs2",
+ "futures",
"git2",
"globset",
"ignore",
@@ -1565,8 +1569,12 @@ dependencies = [
"futures",
"futures-util",
"git2",
+ "globset",
+ "grep-regex",
+ "grep-searcher",
"hex",
"hostname",
+ "ignore",
"image 0.25.10",
"keyring-core",
"libc",
@@ -2156,9 +2164,9 @@ dependencies = [
[[package]]
name = "clang-sys"
-version = "1.8.1"
+version = "1.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4"
+checksum = "157a8ba7b480713b56f4c09fd13fc3e0a22a5dfab8097ba61cbc5feef950788a"
dependencies = [
"glob",
"libc",
diff --git a/Cargo.toml b/Cargo.toml
index 6d57caa7e8..1447cc2763 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -57,6 +57,7 @@ resolver = "2"
version = "0.2.16" # x-release-please-version
authors = ["BitFun Team"]
edition = "2021"
+license = "MIT"
[workspace.lints.rust]
unsafe_op_in_unsafe_fn = "warn"
diff --git a/deny.toml b/deny.toml
new file mode 100644
index 0000000000..47941bac0b
--- /dev/null
+++ b/deny.toml
@@ -0,0 +1,83 @@
+# =============================================================================
+# cargo-deny configuration
+# License compliance + dependency review + vulnerability gate
+# =============================================================================
+# Reference: https://embarkstudios.github.io/cargo-deny/
+
+[advisories]
+# Ignore the following advisories (each needs an explicit reason)
+# ignore = [
+# # Example: { id = "RUSTSEC-2024-0001", reason = "explain why this is ignored" },
+# ]
+vulnerability = "deny"
+unmaintained = "warn"
+notice = "warn"
+severity-threshold = "high"
+# Ignore yanked crate warnings (decide per-case when upstream is unmaintained)
+ignore-yanked = false
+
+[bans]
+# Ban specific crates
+multiple-versions = "deny" # multiple versions of the same crate are not allowed
+wildcard-predicates = "deny" # "*" version requirements are not allowed
+deny = []
+# skip list - allow multiple versions for some crates (usually unavoidable via transitive deps)
+skip = []
+# skip-tree - allow multiple versions for an entire subtree rooted at a crate
+skip-tree = [
+ # tokio-util multiple versions are common via transitive dependencies
+ { name = "tokio-util", version = "0.6" },
+ { name = "tokio-util", version = "0.7" },
+ # Some crates depend on different versions of the object-storage SDK
+ { name = "aws-sdk-s3", version = "0.39" },
+ { name = "aws-sdk-s3", version = "1.0" },
+]
+
+[licenses]
+# Allowed licenses
+allow = [
+ "MIT",
+ "Apache-2.0",
+ "Apache-2.0 WITH LLVM-exception",
+ "BSD-2-Clause",
+ "BSD-3-Clause",
+ "ISC",
+ "Unicode-3.0",
+ "Unlicense",
+ "CC0-1.0",
+ "Zlib",
+ "MPL-2.0",
+]
+# Licenses requiring explicit approval
+deny = [
+ "AGPL-3.0",
+ "GPL-3.0",
+ "GPL-2.0",
+ "LGPL-3.0",
+ "CC-BY-4.0", # some CC licenses may restrict commercial use
+]
+# Licenses requiring manual confirmation
+copyleft = "deny"
+allow-osi-fsf-free = "both"
+confidence-threshold = 0.8
+default = "deny"
+# Exceptions - crates with explicit license approval
+exceptions = [
+ # Allow specific licenses for specific crates
+ { allow = ["MPL-2.0"], name = "ring" },
+ { allow = ["MPL-2.0"], name = "webpki" },
+ { allow = ["MPL-2.0"], name = "untrusted" },
+ { allow = ["ISC"], name = "ipnetwork" },
+]
+
+[sources]
+# Allowed crate sources
+allow-git-registry = true
+allow-registry = true
+# Unknown sources (e.g. git deps not published to crates.io) need explicit approval
+unknown-registry = "deny"
+unknown-git = "deny"
+# Git dependency allowlist
+allow-git = [
+ # List git dependencies not published to crates.io here
+]
diff --git a/docs/architecture/cli-product-line-design.md b/docs/architecture/cli-product-line-design.md
index f08fd86217..f3c800abe3 100644
--- a/docs/architecture/cli-product-line-design.md
+++ b/docs/architecture/cli-product-line-design.md
@@ -10,8 +10,6 @@
- 公开 Agent SDK:[`agent-sdk-product-architecture.md`](agent-sdk-product-architecture.md)
- 产品定制:[`product-customization-blueprint.md`](product-customization-blueprint.md)
- 外部 AI 工作来源:[`extensions/external-ai-work-sources-design.md`](extensions/external-ai-work-sources-design.md)
-- 外部 AI 应用连接体验:[`extensions/external-ai-app-connection-experience-design.md`](extensions/external-ai-app-connection-experience-design.md)
-- 外部 AI 应用连接执行计划:[`../plans/external-ai-app-connection-experience-plan.md`](../plans/external-ai-app-connection-experience-plan.md)
- OpenCode 兼容矩阵:[`extensions/opencode-extension-compatibility.md`](extensions/opencode-extension-compatibility.md)
- 插件 Runtime:[`extensions/plugin-runtime-design.md`](extensions/plugin-runtime-design.md)
- Detached Dispatch:[`detached-task-dispatch.md`](detached-task-dispatch.md)
@@ -146,7 +144,7 @@ SHELL composer
- `stream-json` stdout 每行是一个完整 Agent event。
- 日志与诊断进入 stderr 或日志文件。
- 默认拒绝需要人工确认的操作;只有显式调用级策略可以自动批准。
-- 目标连接体验交付后,只有 Agent Runtime 沿现有事件流返回与当前执行域、工作区作用域、根会话和根轮次完全匹配的依赖结果时,CLI 才投影类型化 `action-required`;当前实现尚未提供该结果。子代理必须通过现有父子关系事件证明仍属于根依赖链,无关待办或后台子代理不得改变退出结果。
+- 非交互入口不等待人工确认,也不从全局外部来源状态推断特殊任务结果。能力不可用时返回普通失败;能够可靠归属到 Tool、Agent 或 MCP owner 时,错误只给出对应管理入口。
- 取消、事件失步、失败完成和 Patch 失败不能报告成功。
## 5. TUI 内部边界
@@ -185,10 +183,8 @@ CLI 通过 `DeliveryProfile::Cli` 消费经过校验的产品 Runtime parts。
CLI 只消费 typed summary 与 typed action:
-> **实现状态:部分交付。** 交互式 TUI 已通过 Host 返回的 V2 快照提供应用级状态、连接、断开、暂不使用和分页批量确认;Embedded 连接旧 Host 时回退既有 V1 只读状态,未接线的 Shared Runtime 明确不支持且绝不回退到控制进程本地执行。任务相关 `action-required` 与非交互 CLI 结果仍未交付。
-
-- `/extensions` 是应用级摘要、首次连接和状态恢复入口;`/extensions review` 提供与 GUI 等价的单页批量确认。
-- `/tools`、`/agent`、`/mcp` 和 `/hooks` 保留能力专项或高级管理职责,不复制应用级连接流程。
+- `/extensions` 只提供外部应用/来源的简短状态、启停和刷新,不拥有审批、冲突或批量决策。
+- `/tools`、`/agent`、`/mcp` 和 `/hooks` 是对应能力的直接管理入口;需要用户允许时由真实 owner 在该入口处理,不再增加跨能力复审流程。
- 静态发现不等于代码执行或服务健康。
- 配置导入不授予插件执行权限。
- ACP、MCP import、Hook import、可执行插件和 TUI contribution 使用独立状态与生命周期。
diff --git a/docs/architecture/extensions/capability-runtime-integration-design.md b/docs/architecture/extensions/capability-runtime-integration-design.md
index 5b5bde6229..f02b9484bc 100644
--- a/docs/architecture/extensions/capability-runtime-integration-design.md
+++ b/docs/architecture/extensions/capability-runtime-integration-design.md
@@ -435,8 +435,8 @@ OpenCode,多语言协议与发布一致性参考 Copilot SDK。最终结构和
## 10. 产品体验要求
-1. **不阻塞正常工作**:发现、准备、兼容检查和无关待确认项在后台进行;只有当前操作真正依赖待确认能力时返回
- 类型化 `action-required`。
+1. **不阻塞正常工作**:发现、准备、兼容检查和无关待确认项在后台进行;当前操作真正依赖不可用能力时,由该能力 owner
+ 返回普通失败并指向对应的权限或配置入口,不增加跨能力任务结果类型。
2. **能力状态可解释**:设置页、CLI 和 SDK 能看到来源范围、执行位置、外部宿主、native/degraded 状态、最终 Provider、权限
上限、最近错误和恢复动作;默认界面只显示需处理项和聚合摘要。
3. **不重复打扰**:同一来源/能力/候选内容摘要只询问一次;内部 `prepare/ready/activate` 阶段不逐层重复审批。
diff --git a/docs/architecture/extensions/external-ai-app-connection-experience-design.md b/docs/architecture/extensions/external-ai-app-connection-experience-design.md
deleted file mode 100644
index 3d55dfb884..0000000000
--- a/docs/architecture/extensions/external-ai-app-connection-experience-design.md
+++ /dev/null
@@ -1,551 +0,0 @@
-# 外部 AI 应用连接与管理详细设计
-
-本文定义“外部 AI 应用”在 Desktop Settings、交互式 TUI 和非交互 CLI 中的应用级连接与管理体验。稳定架构、归属模块和运行视图见[外部 AI 工作内容架构](external-ai-work-sources-design.md),实施顺序见[外部 AI 应用连接体验执行计划](../../plans/external-ai-app-connection-experience-plan.md)。
-
-本文只描述交互、应用级读模型、动作语义和宿主投影,不重定义生态解析、能力归属、执行权限或插件运行时。
-
-> **实现状态:部分交付。** 当前分支保留严格 V1 兼容路径,并已交付独立 V2 应用快照、作用域化连接偏好与迁移、分页批量确认、Desktop/Peer 投影,以及 Desktop Settings 和交互式 TUI 消费。App Server 只在真实注入 management owner 的宿主中暴露这些方法;Shared Runtime 与通用 Server 不伪装支持。任务相关 `action-required`、非交互 CLI 结果和 `HookManagementSnapshot` 仍是后续工作。Hook 管理继续沿用独立 owner 和现有安全审核契约,其产品入口与展示规则见第 5.5、7.1 和 8.3 节。
-
-## 1. 问题与设计目标
-
-当前 Settings 页面把接入策略、物理来源、Tool、Subagent、MCP、冲突、诊断和 Safe Mode 平铺在同一页面。用户必须理解内部能力分类,才能完成“使用另一个 AI 应用中的能力”这一主任务。
-
-目标是:
-
-1. 以外部应用而不是能力类型作为首次连接和日常管理入口。
-2. 明确区分发现、连接和加载,避免“发现即运行”。
-3. 对低风险声明式内容采用低摩擦默认路径,对可执行或权限扩大的内容集中确认。
-4. 给连接动作明确完成反馈,说明已启用、待确认和受限内容。
-5. 适配 Settings 约 600px 的正文宽度,采用纵向单列和渐进披露。
-6. 提示低侵入、一次性、状态驱动;用户已决定后不重复打扰。
-7. GUI 与 TUI 共享产品语义、状态、默认策略和决策结果,不共享布局与渲染实现。
-
-## 2. 范围与非目标
-
-本设计覆盖:
-
-- Desktop Web UI 的应用首页、详情、批量确认和高级设置;
-- TUI `/extensions` 的应用摘要、连接和批量确认;
-- 非交互 CLI 的任务相关 `action-required`;
-- Peer Host / Server 对共享应用级读模型和类型化动作的投影;
-- 默认连接产品事实、提示去重和跨宿主决策一致性。
-
-本设计不包含:
-
-- 外部聊天历史或项目迁移;
-- 将持续来源复制成 BitFun 原生配置;
-- 自动连接或加载所有检测到的应用;
-- 自动运行所有 Tool、Subagent、MCP、Hook、进程或网络能力;
-- 改变生态配置解析、能力归属、权限归属或安全上限;
-- GUI/TUI 共享布局、组件、主题 key、快捷键或渲染 schema;
-- 无法可靠实现的全局撤销;
-- 扩展 OpenCode legacy managed-package 路径为目标运行时模型。
-
-“导入”只用于真正复制或迁移数据的独立能力。持续兼容来源统一使用“发现、连接、加载、断开连接”。
-
-### 2.1 核心术语
-
-正文优先使用中文,协议字段保留代码名:
-
-| 术语 | 含义 |
-|---|---|
-| 执行域(`execution_domain_id`) | 外部事实被读取、能力被加载的真实宿主边界 |
-| 工作区作用域(`workspace_scope_id`) | 宿主为当前工作区计算的不透明策略键,只在所属执行域内有效 |
-| 用户默认(`user_default`) | 同一执行域内,没有工作区覆盖时使用的缺省决定 |
-| 工作区覆盖(`workspace_override`) | 只影响当前工作区、且优先于用户默认的决定 |
-| 发现代次(`generation`) | 一次不可变发现结果的版本,用于拒绝过期操作 |
-| 偏好版本(`preference_revision`) | 用户决定文档的版本,用于并发保护 |
-
-## 3. 产品状态模型
-
-### 3.1 发现
-
-发现是只读扫描:识别外部应用及其用户级、项目级或工作区级候选,生成脱敏摘要、支持范围和风险事实。
-
-发现不得注册运行时能力、启动外部进程、建立网络连接、读取凭据值、改写配置,或把候选加入模型可调用集合。
-
-### 3.2 连接
-
-连接表示用户或产品默认策略允许 BitFun 在明确的执行域和策略作用域内持续读取并同步某个生态。连接是应用级、作用域相关的状态,不等同于允许其全部内容运行,也不能从一个工作区或宿主外溢到另一个执行域。
-
-连接结果必须包含:
-
-- 已连接的应用;
-- 已自动启用的低风险内容;
-- 等待确认的类别和数量;
-- 被安全上限阻止或暂不可用的内容;
-- 唯一下一步主操作。
-
-### 3.3 加载
-
-加载表示将策略允许或用户确认的具体能力注册到真实归属模块。只有同时满足以下条件的内容可以加载:
-
-- 低风险声明式内容已被共享策略允许自动应用,或用户已确认该能力;
-- 未超过产品、组织、宿主能力、Safe Mode 和安全上限;
-- 发现代次、偏好版本、决策键与行为版本仍有效;
-- 对应能力归属模块已完成自身校验、准备和注册。
-
-下图是目标产品流,不代表当前 V1 已具备这些能力:
-
-```mermaid
-flowchart LR
- A["只读发现
生成应用摘要"] --> B["作用域连接决定
默认仅当前工作区"]
- B --> C["加载低风险内容
归属模块最终校验"]
- B --> D["待确认摘要"]
- D --> E["有界分页读取
每页最多 128 项"]
- E --> F["用户确认"]
- F --> C
- C --> G["更新应用结果摘要"]
- C -. "当前任务实际受阻" .-> H["只提示当前会话与轮次"]
-```
-
-### 3.4 面向用户的应用级状态
-
-首页只展示五种应用级摘要:
-
-| 状态 | 含义 | 默认主操作 |
-|---|---|---|
-| 已连接 | 连接有效,当前没有必须处理的应用级事项 | 查看 |
-| 发现可用配置 | 已发现候选,但尚未连接 | 连接 |
-| 未发现配置 | 支持该应用,但当前执行域没有配置 | 无强调操作 |
-| 需要处理 | 存在待确认、权限扩大、阻断性冲突或应用级恢复事项 | 检查 |
-| 暂时不可用 | 连接、同步或宿主状态失败,且存在恢复路径 | 重试或查看原因 |
-
-这些是从底层发现、期望连接、确认、运行、支持、健康和冲突事实派生的持久产品摘要,不替代架构文档定义的正交生命周期。优先级为:`需要处理 > 暂时不可用 > 已连接 > 发现可用配置 > 未发现配置`;Safe Mode 作为全局显著状态单独展示,不被该优先级隐藏。当前轮次的任务依赖作为短期、作用域化导航上下文单独呈现,不写回应用状态。
-
-“已启用”只描述能力结果,不替代“已连接”。应用可以已连接,同时仍有部分能力等待确认或被限制。
-
-## 4. 默认连接与推荐集合
-
-### 4.1 默认连接产品事实
-
-默认连接由 Product Assembly 提供的生态能力事实决定,不能在 React、TUI 或协议 adapter 中按 `ecosystemId` 硬编码。
-
-首期策略:
-
-- OpenCode:允许默认连接;低风险声明式能力按策略自动加载;Tool、Subagent、MCP、进程、网络、环境变量或权限扩大仍进入确认。
-- Codex、Claude Code:默认只发现,不连接、不加载;用户可主动连接。
-
-读模型同时给出默认值和原因,例如适配成熟度、支持范围、产品策略或当前宿主限制。明确的“断开连接”或“暂不使用”优先于后续默认连接,不能被自动发现覆盖。
-
-### 4.2 推荐集合
-
-批量确认默认选中共享控制面计算的推荐集合,高风险项默认不选。推荐计算至少考虑:
-
-- 能力类别和行为风险;
-- 本地进程、网络、环境变量、文件范围和权限扩大;
-- 来源、作用域与适配支持范围;
-- 宿主能力、Safe Mode、产品/组织安全上限;
-- 冲突、诊断和兼容状态;
-- 用户既有决策及其绑定的行为版本。
-
-宿主只能展示推荐、允许用户在安全上限内调整并提交选择,不能自行提高推荐等级或放宽上限。
-
-### 4.3 作用域与旧偏好迁移
-
-连接决定沿用现有集成策略的两级语义,而不是建立一个跨工作区的全局布尔值:
-
-- `user_default` 绑定 `execution_domain_id + application_id`,不带工作区作用域,只作为同一执行域内工作区的缺省值;
-- `workspace_override` 绑定 `execution_domain_id + workspace_scope_id + application_id`,优先于 user default;
-- `workspace_scope_id` 直接复用 `assembly/core` 现有 `workspace_policy_key` 生成的不透明键:`workspace:` 加规范化工作区 SHA-256 的前 16 字节十六进制。它由事实所在宿主计算并随快照返回,控制端只原样回传;它不是路径、没有反查索引,也不建立新的全局工作区注册表。Peer/Remote 宿主必须在自身执行域计算,控制端不得用本机目录代算;
-- 现有 `workspace_overrides` 已以同一不透明键为键,迁移可以原样枚举,不需要也不得反查绝对路径。宿主身份或执行域改变后旧键不能跨域复用;显式无工作区使用 `none`,不是任意工作区的通配符;
-- 偏好版本、提示键和确认计划都在同一作用域内解释,不能跨作用域去重或重放。
-
-现有 `ExternalSourcesConfig` 已保存 integration policy、来源抑制、Tool/Subagent/MCP 审批和冲突决定,但没有应用连接字段。`integration_policy.enabled=false` 同时表示结构体默认值和用户显式关闭,而且现有 MCP revision-key 初始化可能把默认对象自动写成文件;因此不能再用“有文件/无文件”或 `false` 单独还原用户意图。升级必须先读取原始存储状态,再进入会物化默认文件的 helper,并在现有原子读改写路径中执行可重入迁移:
-
-1. `WorkspaceExternalSourceService` 的启动迁移关口必须成为偏好存储的第一次访问:它先读取原始文件存在性和 `schema`,完成或保留迁移后,才允许发现、MCP 版本键初始化或 V2 接口继续。只有确认从未存在过偏好文件的新安装才写入 `config_origin=fresh_v2`,保持“无用户决定”并应用新的产品默认。已有旧文件或不兼容策略重置都不能重新归类为 fresh V2。
-2. 迁移关口在内存中一次计算所有旧用户默认和 `workspace_overrides` 的连接决定;每项都按 `(execution_domain_id, application_id, workspace_scope_id?)` 写入真实连接状态与 `decision_origin`,无法归属的项写为 `needs_review`。`connection_schema_migration_version` 只表示整份文档已完成一次原子转换,不引入逐作用域的迁移生命周期。
-3. 任何旧文件中的 `integration_policy.enabled=false` 都保守迁移为该作用域的显式未连接,`decision_origin=legacy_safety`;这包括由旧版自动生成、无法与用户显式关闭区分的默认文件。该规则优先于“已有有效使用”判断,保证升级不意外启用能力;可能要求从未手动关闭的旧用户重新连接一次,并应在迁移说明中明确,而不能用 OpenCode 新默认覆盖。
-4. 仅当旧策略的 `integration_policy.enabled=true`,且该作用域已有效使用某生态——至少一项能力的实际访问级别为 `ask_before_use`/`auto`,或存在可归属到该生态的有效审批、冲突决定或活动路由——才迁移为已连接,避免升级静默撤下现有 Claude Code/Codex/OpenCode 能力。现有 `workspace_overrides` 直接按不透明 `workspace_scope_id` 逐项迁移。
-5. 审批、拒绝和冲突记录不因连接迁移而删除;重新连接时仍需决策键与行为版本匹配,权限扩大继续重新确认。无法可靠归属到应用、执行域或某一作用域的旧记录写为连接状态 `needs_review`,该作用域继续使用 V1 路径,不得猜测连接、静默停用或用新默认接管。
-6. 若读取到未知未来 `schemaMajor`,必须沿用现有不兼容策略的安全拒绝语义:不迁移、不应用默认、不写任何 V2 决定,也不触发偏好文件重写,逐字节保留包含不透明策略的原文件。用户执行既有“备份并重置”时,在同一原子更新中保存原策略、写入 `config_origin=incompatible_reset` 和显式未连接决定;该来源永不应用默认连接,只有用户随后显式连接才能启用能力。
-7. 全部作用域决定、`connection_schema_migration_version` 和既有审批/冲突事实必须在同一次锁内原子替换中提交。成功时不存在“部分迁移”;失败则保持原文件和完整 V1 运行路径,重启后重新计算并重试整次转换。
-
-Instruction、Skill、Hook 和显式复制成 BitFun 原生配置的内容继续由各自归属模块决定。只有归属模块已提供来源限定的激活/撤下端口时,应用连接才能协调其持续外部来源;否则应用摘要必须标记 `managed_separately` 或部分支持,断开连接不得虚假宣称已卸载。已经复制的原生 Hook/MCP 等快照不随外部应用断开而删除。
-
-## 5. Desktop Settings 信息架构
-
-### 5.1 首页
-
-首页沿用现有 `ConfigPageLayout` 的 760px 正文最大宽度。接入设置不能藏在详情或“高级设置”深层,默认页面只保留:
-
-1. 一个应用级总开关,控制是否使用外部 AI 应用能力;
-2. 一个推荐模式入口,默认由产品安全策略自动完成发现、连接和低风险能力加载;
-3. “需要确认”入口,仅在确有少量不能安全自动决定的事项时显示数量;
-4. 一个可展开的应用/能力树,供用户查看结果或覆盖单项决定;
-5. Safe Mode,仅在生效或当前宿主可操作时显著展示。
-
-首页不平铺 Tool、Subagent、MCP、Hook、来源路径、冲突、完整诊断、scope 和兼容参数。应用树默认折叠,只在用户希望检查单项状态时展开。
-
-默认路径的目标不是让用户逐项配置,而是由产品完成大多数决定:只读发现自动执行;成熟适配中安全上限内的低风险声明式内容按推荐策略自动连接和加载;已有等价决定静默复用;普通更新静默同步。只有可执行内容首次信任、实质权限扩大、无法自动选择的真实冲突,或宿主安全策略要求时才进入“需要确认”。
-
-用户处理全部待确认事项应在一个页面或弹层内完成:默认应用共享推荐集合,提供“使用推荐设置”和“暂不启用”少量主操作;可展开树只用于查看和调整例外项。正常流程不要求用户理解能力分类、来源文件、作用域或诊断码。
-
-应用行与能力节点用于解释自动化结果,而不是要求用户逐项决策。顶层只显示应用名、整体状态和简短结果;展开后才显示能力类别与单项状态。没有问题的应用不展示操作按钮,连接、断开或单项覆盖通过同一树内的上下文操作完成。
-
-### 5.2 应用树与逐级披露
-
-默认页面不再要求用户进入独立应用详情才能接入或查看结果。应用树按外部应用分组;每个折叠行只包含展开箭头、应用名称、已启用能力图标和应用总开关:
-
-- 打开应用总开关等价于“使用推荐设置”,系统自动应用安全且无歧义的决定,不启动配置向导;
-- 关闭应用总开关撤下该应用贡献的运行能力,但不修改外部配置、不删除已保存决定,也不影响其他应用;
-- 只显示已启用能力类型的紧凑图标,未启用类型不占位;图标全名、数量和状态原因通过 tooltip 或无障碍标签提供;
-- 应用或能力下存在待确认项时只显示圆点/计数,不重复放置确认按钮或说明段落;
-- 展开应用后显示能力类型及类型开关,再展开能力类型才显示单项覆盖;单项明细不是正常使用的前置步骤。
-
-正常状态下不显示“管理连接”、恢复或确认文案。低频作用域覆盖、来源路径、完整诊断、兼容说明和恢复动作进入高级设置。Safe Mode 生效时仍必须显著显示,不能因折叠隐藏。
-
-### 5.3 单一确认入口
-
-Tool、Subagent、MCP、Hook 和无法自动决定的真实冲突进入同一个确认页面或弹层,不使用连续弹窗,也不要求按应用重复提交。顶部仅在存在真实待确认项时显示一个带数量的入口;应用树只负责定位相关应用和能力。
-
-确认页默认只显示总数、简短风险摘要和共享推荐集合,并提供“使用推荐设置”和“暂不启用”两个主要决定。用户展开分类或单项后,才展示名称、来源、路径、命令、环境变量名、网络目标、冲突和行为变化。敏感值、完整 prompt、完整 URL query 和未经脱敏的绝对路径不进入公共快照。
-
-系统应把确认项压缩到最少:只读发现、低风险声明式能力、行为等价更新、已有有效决定和无歧义优先级自动完成。首次可执行内容信任、实质权限扩大、无法安全自动消解的冲突及组织策略要求才需要确认。待确认项在决定前不运行,但同应用其他安全能力继续生效。
-
-提交不要求客户端读取全部分页:`review_id` 绑定同一不可变确认计划,`selection_baseline` 只能是共享推荐集合或空集合,`selection_overrides` 只携带与基线不同的稳定项目引用和选择结果。服务端从同代权威计划还原完整选择,依次应用基线和改动项,再校验作用域、偏好版本、发现代次、决策键、行为版本、安全上限和最大选择数。计划过期或引用不属于该计划时整批拒绝,不能把不同页面或不同代次拼接。
-
-批量语义:
-
-- stale revision、无效 generation 或宿主能力整体不兼容时,整个请求不应用;
-- owner 允许逐项业务拒绝时,响应返回逐项结果;宿主只把成功项标为已启用;
-- “整个请求不应用”只保证分派前的 identity、revision 和 generation 预检;开始分派后若 owner 状态并发变化,可以同时返回已应用项和类型化 stale/failed 项,不承诺跨 owner 回滚;
-- 未知结果不能假定成功;
-- 失败项保留可行动原因与恢复动作。
-
-### 5.4 高级设置
-
-以下内容后置到可展开树或高级设置:全局/项目 scope、生态与能力覆盖、物理来源、完整诊断、配置位置、Safe Mode 恢复和兼容说明。高级设置不是正常接入的前置入口,也不能重复承载应用总开关或待确认主操作。
-
-### 5.5 扩展能力与 Hook
-
-External AI Apps 是 GUI 中查看外部应用及其能力的唯一 Settings 一级入口。Hook 与 Command、Skill、Agent、Tool、MCP 等并列,是扩展能力类型之一;不能把 Hook 提升为与外部应用并列的产品域,也不能为了统一界面创建承载所有能力载荷的通用 Extension 对象。
-
-应用树中的能力节点把 Hook 与其他类型并列展示。折叠应用行只显示已启用的 Hook 图标;展开后显示已启用数量和待确认标记,再展开 Hook 节点才列出原生、已导入和可审核单项。Hook 摘要至少区分已启用、待确认、需要更新、部分兼容和异常;技术性的“已发现/可导入/已导入”只在单项详情中表达,不作为顶层用户状态。
-
-Hook 节点同时覆盖:
-
-1. BitFun 用户级和项目级原生 Hook;
-2. 已导入并由 BitFun 管理的外部 Hook;
-3. Claude Code、Codex 等外部应用中可审核导入的 Hook;
-4. Hook 总开关、项目 Hook 开关、配置位置和兼容说明,这些内容作为类型专属高级控制呈现。
-
-该节点优先用图标、开关和计数回答当前哪些 Hook 会运行、来自哪里以及是否需要确认;解释文字进入 tooltip、无障碍标签或按需详情。完整命令、依赖、matcher 和诊断只随单项展开。Settings 不再单列 Agent Hooks 一级入口;既有深链可兼容导航到 External AI Apps 并展开对应 Hook 节点,但不能继续形成第二套管理页面。
-
-Hook 数据仍由 `native_hooks`、`external_hooks` catalog 和 `external_hook_import` 各自拥有。External AI Apps 只消费应用与能力摘要,不复制可执行载荷、不修改外部来源文件,也不取代 Hook 的精确计划审核、revision fencing、行为版本和运行开关。
-
-加载遵循渐进披露:默认页只读取轻量摘要;用户展开应用、能力节点,或已有 Hook 相关真实待办时才读取对应详情。不能为了默认页精确计数而预加载命令和依赖;摘要不足时使用图标状态而不是触发无界扫描。
-
-### 5.6 当前 V1 修复切片
-
-在组合 `HookManagementSnapshot` 尚未交付时,Desktop 仍必须保证既有 Hook 管理能力可达,但不能为此恢复第二个 Settings 一级入口或复制一套 Hook 数据 owner。当前 V1 修复切片采用以下过渡边界:
-
-1. `External AI Apps` 保持唯一一级入口;页面内提供一个按需展开的 Hook 专属区域,复用现有 `app.hooks` 配置、`external_hooks` catalog 和 `external_hook_import` 操作。区域未展开时不读取 Hook 详情;旧 `hooks` 深链进入本页并自动展开、聚焦该区域。
-2. Hook 区域只做现有 owner 的组合呈现,不创建新的后端协议、不复制可执行载荷,也不把现有 Hook 操作改接到 external-source policy。后续组合快照交付时替换读模型,不改变现有写操作的 owner。
-3. 应用总开关从 `custom` 关闭时只把当前 mode 设为 `disabled`,不清除当前作用域的 capability overrides;重新打开时,存在保留 override 的应用恢复为 `custom`,否则进入 `recommended`。显式重置会清除 override,因此仍返回推荐模式。
-4. 应用树的能力状态展示生效权限,而不是连接计划中的推荐权限。`auto`、`ask_before_use`、`discover_only` 和 `disabled` 必须有不同且准确的用户文案;推荐值只用于产生默认决定,不能冒充当前状态。
-5. 首次加载且没有可展示快照时,页面显示可访问的错误提示和带文字的重试操作,并暂不展示依赖快照的应用树与高级设置。已有 last-valid 快照时继续展示该快照,同时标记降级状态,不能因刷新失败把现有内容替换为空白页。
-6. 新增或调整的 appearance part 必须在同一 DOM 节点声明所属 component,并与 appearance 注册表一一对应;不能通过放宽审计或保留不存在的 part 让检查通过。
-
-该切片必须用生产路径组件测试覆盖旧深链到 Hook 区域、Hook owner API 可达、`custom -> disabled -> custom` 权威快照往返、四种生效权限文案、无快照错误恢复和 last-valid 降级显示,并通过 appearance contract audit。它不实现完整 Hook 摘要计数、跨宿主通知决定或新的共享连接协议。
-
-## 6. 提示、去重和恢复
-
-### 6.1 首次发现
-
-不使用启动弹窗。允许的入口是:
-
-- 聊天区一次性非阻塞轻提示;
-- Settings 导航低侵入状态;
-- Settings 内应用摘要。
-
-文案只说明“发现了可连接的应用”,不能暗示能力已经加载。
-
-### 6.2 持久化去重
-
-提示与用户决定由共享持久化事实驱动,不能只保存在某个 GUI/TUI 进程。去重键至少包含:
-
-- execution domain ID;
-- `user_default` 或 `workspace_override`;workspace override 还包含 Host 返回的 `workspace_scope_id`;
-- application / ecosystem ID;
-- 内容或行为版本;
-- 风险摘要版本;
-- 用户决定状态。
-
-用户关闭、完成确认、断开连接或选择“暂不使用”后,同一作用域、同一有效版本不再主动提示。仅数量变化但行为和风险未扩大时,只更新 Settings 摘要。用户级决定可以作为同一执行域的缺省值,workspace override 只影响对应 `workspace_scope_id`;任何决定都不能跨执行域传播。
-
-### 6.3 再次主动提示
-
-仅允许:
-
-1. 当前任务真正依赖待确认能力并因此受阻或降级;
-2. 已确认内容发生实质权限扩大,需要重新确认。
-
-权限扩大包括新增进程执行、网络访问、环境变量读取、更宽文件范围、工具集合扩大、模型或 Subagent 行为变化。行为等价刷新、普通路径变化和未连接应用更新不构成主动提示理由。
-
-“当前任务受影响”不是持久化应用快照字段,也不参与应用级提示去重。能力归属模块在实际解析或调用依赖时,如果被连接策略或批量确认阻止,就返回类型化依赖事实;Agent Runtime 负责把它关联到根轮次并沿现有 Agent 事件流发布。现有 `session_id + turn_id` 已唯一标识根任务,不再新增一套任务身份。一个轮次的待确认能力不能改变另一个并发轮次的状态或退出结果。
-
-子代理结果不得只凭“来自当前会话树”就使根任务失败。Runtime 使用现有 `SubagentSessionLinked` 的父 session、父 turn 和父 tool-call 关系追溯来源:只有根 turn 仍在等待该子代理调用时,子代理的阻断事实才聚合到根任务;无关、后台或已经脱离等待链的子代理结果保留在其来源 turn。事件在对应根任务结束事件之前发出,CLI/Host 只消费与当前根 session、turn 完全匹配的结果。
-
-### 6.4 错误与恢复
-
-必须区分发现失败、连接失败、同步暂时失败但沿用上一版本、stale revision、Host/Remote 不支持、Safe Mode 或 safety ceiling 阻止。
-
-读模型提供类型化恢复动作,例如刷新、重试、重新连接、重新审阅、解决冲突、安装运行时、升级/重连 Host 或退出 Safe Mode。宿主不得解析错误文本决定控制流。
-
-## 7. TUI 与非交互 CLI
-
-### 7.1 TUI
-
-`/extensions` 是应用级摘要和首次连接主入口,展示与 Settings 首页等价的状态、默认策略、数量和主操作。
-
-`/extensions review` 提供与 GUI 等价的批量确认语义:共享推荐集合、高风险默认不选、可展开技术详情并调整。能力管理沿用竞品与 BitFun 已建立的直达命令:`/hooks`、`/tools`、`/agent` 和 `/mcp` 都是完整专项入口,不要求用户先进入 `/extensions`,也不新增 `/extensions hooks` 一类层级。GUI 的应用/能力导航不能被强加为 TUI 命令心智。
-
-`/hooks` 同时展示 BitFun 用户级和项目级 Hook、已导入的外部 Hook,以及 Claude Code、Codex 等应用中可审核的 Hook 来源,并提供现有审核、导入、更新、启停和移除操作。`/hooks_external` 与 `/hooks-external` 仅保留解析兼容,不进入推荐帮助和补全。TUI 与 GUI 使用同一后端派生状态和安全操作,但各自保留适合表面的布局。
-
-首次发现只显示一次非阻塞摘要;无关待办不阻塞聊天输入。
-
-### 7.2 非交互 CLI
-
-非交互命令不等待确认输入。只有当前操作真正依赖待确认能力时返回类型化 `action-required`,包含:
-
-- 受影响应用和能力摘要;
-- 风险原因;
-- 可执行的后续动作或交互入口;
-- 当前操作是否可降级继续。
-
-与当前操作无关的待确认能力不能导致命令失败。
-
-## 8. 应用级读模型
-
-产品级协调 owner 应通过独立 V2 协议提供宿主可直接投影的版本化应用级读模型:
-
-```text
-ExternalApplicationSnapshotV2
- schema_version = 2
- execution_domain_id
- workspace_scope_id? # 复用宿主的 workspace_policy_key;none 表示无工作区,不是通配符
- effective_connection_scope
- refresh_generation
- preference_revision
- safe_mode
- host_capabilities
- applications[]
- application_id / ecosystem_id / display_name
- discovery / connection / health
- effective_status / primary_action
- default_connection_policy + reason
- enabled / pending_review / blocked / conflict counts
- risk_summary
- notice_key / user_decision
- recovery_actions
- review_summary
- review_id / total_count / category_counts / max_selection_count
- risk_summary / recommendation_summary / safety_ceiling
-```
-
-应用级对象是对同一生态多个物理来源和能力事实的聚合。它不携带可执行载荷,不取代现有目录与能力专属 DTO。首页快照只携带批量确认摘要,不能内嵌完整项目列表;否则每次轮询都会重复序列化与首页无关的大量候选。
-
-用户进入批量确认页后,客户端再调用有界只读接口取得稳定引用:
-
-```text
-ExternalApplicationReviewPageV2
- schema_version = 2
- execution_domain_id / workspace_scope_id? / target_scope
- review_id / preference_revision / expected_generations
- cursor / next_cursor / total_count
- items[] # 每页最多 128,只含 item reference、显示摘要、推荐与安全上限
-```
-
-首次打开确认页时,请求不带 cursor 和 expected generations;若后台发现已在首页快照后完成,Host 可以返回当前只读确认计划,并以响应中的 `review_id` 和 generations 作为后续翻页与提交的唯一基准。偏好版本、执行域、工作区和目标作用域仍必须完全匹配。首次响应之后,分页游标严格绑定作用域、`review_id`、偏好版本和发现代次;任一事实变化都返回过期并重新读取,不能把旧页与新页拼接。详细页通过稳定项目引用关联现有 Tool、Subagent、MCP 和冲突投影;总量继续服从现有归属模块上限,完整提示词、命令正文、凭据和可执行载荷不进入分页响应。
-
-状态和主操作由共享归属模块派生;React、TUI、Peer 和 Server 不重复实现优先级规则。
-
-### 8.3 Hook 管理读模型与通知决定
-
-Hook owner 应提供一个面向产品表面的版本化组合读模型,供 External AI Apps 和 TUI `/hooks` 使用。它组合原生 Hook 概览、外部 Hook catalog 与导入摘要,但不成为新的数据 owner:
-
-```text
-HookManagementSnapshot
- schema_version / revision
- native
- enabled / project_hooks_enabled
- configured_count / active_count / issue_count
- applications[]
- ecosystem_id / display_name
- discovered_count / importable_count
- imported_count / enabled_count
- update_count / unsupported_count / issue_count
- attention_state
- imports[]
- existing import summaries
- notice?
- notice_key / attention_reason / acknowledged
-```
-
-摘要不得携带完整命令、环境变量值、依赖文件正文、matcher 正文或其他执行载荷。能力专属操作继续调用现有 Hook API;写操作成功后返回或重新读取权威快照,客户端不能长期维护乐观派生状态。现有 `ExternalHookImportSnapshotV1` 能直接表达的字段应复用,不能复制一套同构导入协议。
-
-首次发现圆点的已查看事实必须由事实所在 Host 持久化,不能只写 React `localStorage`。最小决定键包含 execution domain、可选 workspace scope、ecosystem、notice kind 和行为或摘要版本。`acknowledged` 只消除低侵入提示,不代表信任、导入或允许运行;安全决定仍由现有计划指纹、revision、行为版本和运行开关控制。相同行为版本跨重启不重复提示,只有新审核、实质权限扩大、已激活 Hook 失效或真实冲突才能产生新 notice。
-
-冲突由能力 owner 在默认状态确实不能共存、用户正在启用冲突项,或外部变化使既有决定失效时产生。仅发现多个候选或数量变化不构成冲突,也不触发打断式确认。
-
-任务依赖通过执行路径单独返回,不进入可轮询、可持久化的应用快照:
-
-```text
-AgenticEvent::ExternalDependencyActionRequired
- schema_version = 2
- execution_domain_id / workspace_scope_id?
- session_id / turn_id # 根任务身份
- origin_session_id / origin_turn_id / origin_tool_call_id?
- dependency_refs[] / risk_summary / can_degrade
- recovery_actions
-```
-
-该契约归 `bitfun-events` 所有,而不是应用快照归属模块或 CLI。`AgentSubmissionResult` 仍只表示轮次已被接收;Runtime 在真实能力解析路径产生事件,现有 App Server `agent/event` 与 Shared Runtime IPC `RuntimeIpcEvent::Agent` 承载 `AgenticEventEnvelope`。新增事件前必须补齐 App Server 协议/客户端、Shared IPC 协议版本兼容处理和 Embedded/Shared 等价测试。
-
-该事件与外部来源 V1/V2 接口是两个版本边界,不能因为应用快照是 V2,就假设旧 App Server 客户端能解析新的 `AgenticEvent` 类型。实现必须提升 App Server 协议版本,并按每条连接协商出的版本过滤新事件;旧协议连接不得收到未知类型。若无法可靠过滤,则提升最低协议版本并在初始化阶段安全拒绝旧客户端。Shared IPC 同步提升其严格 `PROTOCOL_VERSION`。新客户端连接旧宿主时必须明确返回“任务依赖结果不支持”,不能从结束文本推断。只有根会话和根轮次完全匹配的任务可以据此返回 `action-required`;Settings 可把它作为短期导航上下文读取,但不能合并成所有任务共享的应用状态。
-
-### 8.1 V1/V2 协议边界与协商
-
-现有 `ExternalSourceControlSnapshotV1`、`ExternalSourceControlActionV1`、`ExternalSourceRecoveryActionV1` 和 V1 `hostCapabilities` 保持字段与闭合枚举不变。应用级快照、连接动作、批量确认、`upgrade-host` 语义以及新增能力位不得追加到 V1 对象。
-
-`get_external_application_snapshot_v2` 不提交用户决定或运行能力写动作,直接承担版本探测,不再增加单独的版本信息接口。首次激活对应 owner 时允许执行可重入偏好迁移并启动既有后台发现;当前 V2 偏好读取不得重复写回。确认分页只读取已激活 owner 的不可变结果,不能冷启动服务或发现:
-
-- 新宿主返回严格的 V2 快照和 `host_capabilities`;客户端校验成功后,才可读取分页确认项或发送 V2 写操作;
-- 旧宿主对 V2 快照返回传输层 method-not-found 时,客户端回退显示 V1 来源/能力管理并禁用 V2 写操作;
-- “升级宿主”由新客户端根据 method-not-found 本地投影,不能向旧宿主发送未知 V2 动作,也不能要求旧宿主返回 V1 不认识的恢复类型;
-- 旧客户端只调用原 V1 接口,因此新宿主必须继续生成严格 V1 响应;V1/V2 快照不得拼接成混合数据结构;
-- 数据结构不匹配、宿主身份变化或重连后,所有未完成 V2 写操作和分页游标失效并重新读取快照。
-
-兼容测试必须覆盖旧客户端 → 新宿主、新客户端 → 旧宿主、V2 同代成功、未知数据结构/枚举安全拒绝,以及重连后旧响应不能覆盖新执行域或工作区作用域。
-
-### 8.2 性能与演进约束
-
-- 应用快照和确认分页必须从当前不可变发现结果派生;首次快照可触发既有 owner 的迁移和后台发现,确认分页不得重新扫描文件、冷启动 owner、启动外部进程或持有偏好写锁。
-- 首页只返回摘要,确认页每页最多 128 项。完整候选总量继续服从各归属模块已有上限,不建立第二套无界缓存。
-- 共享缓存只允许按执行域、工作区作用域、发现代次和偏好版本精确失效;React、TUI、Peer 与 Server 不得各自维护产品状态机。
-- 归属模块的加载与卸载在锁外执行;迁移关口只阻塞外部来源读写,不阻塞项目打开或无关 Agent 任务。
-- 实现 PR 必须记录 V1/V2 快照大小和聚焦读取延迟的前后对比。没有基线时不宣称性能提升;出现明显回退时先减少返回数据或重复计算,再考虑新增缓存。
-- 后续只有出现真实消费者和独立兼容要求时,才增加新的版本化接口;不提前扩展 V1,也不为单一 V2 接口建立通用协议目录。
-
-## 9. 类型化动作
-
-V2 控制协议应提供闭合动作:
-
-- `ConnectApplication`;
-- `DisconnectApplication`;
-- `SetApplicationDeferred`(暂不使用);
-- `SubmitApplicationReview`;
-- `Refresh`;
-- V2 投影需要的来源开关、策略更新和 `SetSafeMode`;既有 V1 action 保持原样,不扩充枚举。
-
-每个 V2 写操作信封必须携带 `execution_domain_id`、`target_scope`、`operation_id` 和该作用域的 `expected_preference_revision`;`workspace_override` 必须携带 `workspace_scope_id`,`user_default` 必须省略它。无工作区的读取使用显式 `none`,不能当作通配符。宿主必须确认这些身份与当前连接绑定一致,不能使用控制端当前目录推断目标。宿主默认动作只能提交当前工作区范围;全执行域默认必须来自用户明确选择。
-
-`operation_id` 只用于请求/响应关联和界面中的待处理操作排序,不提供业务幂等、结果缓存或跨重启重放。客户端不得在同一活动连接内为并发请求复用它;服务端也不会因 ID 相同而重放旧结果。偏好版本是唯一写并发保护:响应丢失后,客户端必须重新读取权威快照,再决定是否发起新操作;不能用相同 `operation_id` 绕过过期版本。`SubmitApplicationReview` 还必须携带 `review_id`、选择基线和有界改动项,服务端从该计划取得各归属模块的发现代次、决策键和行为版本。
-
-断开连接必须停止继续同步、卸载由该连接注册的运行能力、保留必要审计与用户决定、不改写外部配置、不影响其他生态,并返回不再可用的能力摘要。重新连接只复用仍与 decision key / behavior version 匹配且策略允许的决定;权限扩大重新确认。
-
-## 10. Web UI 组件边界
-
-现有 `ExternalSourcesConfig` 收敛为页面 controller,并拆分为:
-
-- `ExternalAppsOverview`:应用首页;
-- `ExternalAttentionSummary`:真实待办;
-- `ExternalAppDetail`:单应用结果与管理;
-- `ExternalAppReview`:批量确认;
-- `ExternalAdvancedSettings`:scope、来源、冲突、诊断和 Safe Mode;
-- controller/hook:读取、轮询、mutation sequencing 和恢复;
-- presentation helpers:格式化展示,不做策略判断。
-
-拆分必须保留现有请求序列、accepted sequence、pending mutation、scope mutation 栅栏、stale read/mutation 防护和失败恢复。UI 继续通过 infrastructure API,不直接调用 Tauri。
-
-## 11. 关键场景
-
-### 11.1 首次发现 OpenCode
-
-1. 只读发现;
-2. 产品事实允许默认连接;
-3. 建立持续连接;
-4. 加载策略允许的低风险内容;
-5. 生成高风险推荐集合;
-6. 一次性显示连接结果和待办;
-7. 用户提交批量 review 后加载成功项;同一行为版本不重复提示。
-
-### 11.2 首次发现 Codex 或 Claude Code
-
-1. 只读发现;
-2. 显示“发现可用配置”;
-3. 不连接、不加载;
-4. 一次性轻提示或 Settings 状态;
-5. 用户主动连接后进入相同风险确认流程。
-
-### 11.3 多应用并存
-
-- 发现多个应用只增加候选;
-- 只有产品事实允许且未被用户拒绝的生态可默认连接;
-- 未连接应用不注册运行能力,也不参与运行时冲突;
-- 一个应用的连接、审批或断开不隐式改变另一个应用;
-- 已连接应用之间的真实冲突由共享归属模块生成待办。
-
-### 11.4 内容更新
-
-- 行为等价且风险不扩大:保持决定,静默更新摘要;
-- 是否可复用旧决定由共享策略判定,宿主不猜测;
-- 权限扩大:扩大部分安全拒绝,生成重新确认;
-- 偏好版本过期:刷新权威状态后重新确认。
-
-## 12. 可访问性、文案与 i18n
-
-- 保持 600px 单列阅读轴,不依赖宽屏左右主从布局;
-- 每行只有一个强调主操作;
-- 状态不能只靠颜色,必须有文本或图标标签;
-- 批量选择、展开和恢复动作支持键盘与清晰焦点;
-- 使用现有主题令牌,不新增无归属色值;
-- 统一文案:“发现、连接、等待确认、已启用、需要处理、断开连接”;
-- 用户可见文案进入对应 i18n namespace;日志保持英文且无 emoji。
-
-## 13. 验收标准
-
-### 13.1 共享契约与运行时
-
-- OpenCode 默认连接,其他生态默认只发现;
-- 默认策略来自共享产品事实,而不是宿主生态 ID 分支;
-- 发现不注册运行能力;
-- 连接只自动加载允许的低风险内容;
-- 推荐集合、高风险默认不选和 safety ceiling 可验证;
-- 批量确认的偏好版本/发现代次、整体失效与逐项结果可验证;
-- 断开或暂不使用后不被默认策略覆盖;
-- 权限扩大重新确认;
-- 未连接应用不参与运行时冲突;
-- 断开卸载对应能力且不改写外部配置;
-- Safe Mode、旧宿主、Remote/只读场景继续安全拒绝。
-- 旧偏好迁移保留显式 disabled/discover-only、已有效使用的能力、审批与冲突决定,并以升级/重启 fixture 证明不会静默改变行为;
-- user default、workspace override、本机/Peer/Remote 在 execution domain 与 workspace scope 上相互隔离;
-- V1 枚举和字段保持不变,V2 只在独立协商成功后使用,双向新旧组合测试通过;
-- 任务相关 `action-required` 绑定 session/turn outcome,不从全局应用快照推断。
-
-### 13.2 GUI
-
-- 默认页只呈现总状态、单一确认入口、刷新和按应用分组的可展开树;
-- 应用折叠行只显示名称、已启用能力图标和应用总开关,解释文字进入 tooltip 或按需详情;
-- 打开应用自动应用推荐设置,不启动逐项配置流程;关闭应用撤下其运行能力且不修改外部文件;
-- 应用树把 Hook 与其他扩展能力并列,逐级展开后同时覆盖 BitFun 原生和外部来源;
-- Settings 不再单列 Agent Hooks 一级入口,旧深链导航到 External AI Apps 并展开 Hook 节点;
-- 所有例外通过一个确认入口一次处理,只有首次可执行信任、权限扩大和真实冲突进入确认;
-- 首次发现圆点由 Host 持久化去重,同一行为版本跨重启不重复;
-- 批量默认选择与共享推荐一致,待确认单项不阻塞其他安全能力;
-- 技术详情默认折叠;
-- 过期读取或写操作不覆盖新状态;
-- 现有 Safe Mode、审批、冲突、诊断和脱敏测试保持通过;
-- type-check、i18n 和主题治理通过。
-
-### 13.3 TUI 与非交互 CLI
-
-- GUI/TUI 对同一 fixture 的应用状态、默认策略和数量一致;
-- `/extensions review` 提交同一批量决定;
-- `/hooks` 完整显示并管理 BitFun 原生、已导入和可审核的外部 Hook,且不新增 `/extensions hooks`;
-- `/hooks_external` 与 `/hooks-external` 仅保留解析兼容,非交互 `bitfun hooks` 契约保持稳定;
-- 提示去重跨进程和宿主生效;
-- 无关待办不阻塞交互;
-- 非交互仅在当前任务受影响时返回 `action-required`;
-- Host/Remote 差异通过共享能力与恢复动作表达。
diff --git a/docs/architecture/extensions/external-ai-work-sources-design.md b/docs/architecture/extensions/external-ai-work-sources-design.md
index f222f4d54a..ecf60b044b 100644
--- a/docs/architecture/extensions/external-ai-work-sources-design.md
+++ b/docs/architecture/extensions/external-ai-work-sources-design.md
@@ -5,14 +5,13 @@
适配器负责,本文不建立跨生态通用配置格式或脚本 SDK。BitFun 自身能力如何通过 MCP、Skill、Plugin、Hook、
SDK 或 Server 输出到外部宿主,以及内部能力组合、状态、事件和并发边界,见
[`capability-runtime-integration-design.md`](capability-runtime-integration-design.md);两条方向共用适用的身份事实和能力归属模块,
-但不共用一个大一统 adapter 或状态模型。外部应用的 Settings/TUI 信息架构、默认连接、批量确认和提示去重见
-[`external-ai-app-connection-experience-design.md`](external-ai-app-connection-experience-design.md),对应实施顺序见
-[`../../plans/external-ai-app-connection-experience-plan.md`](../../plans/external-ai-app-connection-experience-plan.md)。
+但不共用一个大一统 adapter 或状态模型。Settings 和 TUI 只能把本文的来源与 integration policy 事实压缩为简短概览;
+审批、冲突和可执行能力状态继续由 Tool、Agent、MCP、Hook 等真实 owner 负责。
本文同时记录当前可用端到端能力与目标架构。当前 BitFun 已具备通用外部来源目录、四条能力专属发现通道,并由
`ExternalSourceControlPlane` 负责 provider-neutral 调度、generation fencing 和故障隔离;`assembly/core` 的
`WorkspaceExternalSourceService` 负责产品级策略、偏好、聚合和运行装配,`contracts/product-domains` 提供版本化控制事实、固定动作与错误语义,
-Desktop、交互式 TUI 和 Peer Host 只显示宿主所需状态,不再各自派生另一套状态机。Server 仓库中保留了只读 external-source dispatch helper,但当前 `/ws` 已直连 in-process App Server,external-source 方法尚未进入 App Server schema,生产请求会得到 `method_not_found`;因此不能把 Server 只读投影列为已交付。OpenCode Prompt Command
+Desktop、交互式 TUI 和 Peer Host 只显示宿主所需状态,不再各自派生另一套状态机。App Server 已注册 external-source schema 与 handler;Embedded TUI 注入 management owner 后可以调用,通用 Server `/ws` 当前没有注入绑定可信工作区的 management owner,因此请求会得到类型化 `unsupported`,不能把通用 Server 只读投影列为已交付。OpenCode Prompt Command
适配器已接入本地用户全局/项目来源;Desktop 可查看、刷新、抑制和处理跨来源冲突,交互式 TUI(ChatMode)可列出并执行
Prompt Command;静态文件和经审阅的本地 shell 输出由共享归属模块完成装配。第二条端到端能力已让受支持的单文件 OpenCode `.js` standalone Tool 经静态
预览、来源/能力确认和同名冲突选择后进入现有 Tool Runtime;Desktop 与交互式 TUI(ChatMode)使用同一决策状态。第三条纵向
@@ -120,7 +119,7 @@ stale,界面替换为服务端返回的新 plan,并只保留“旧选择与
### 3.1 首次发现
发现始终在后台进行。Desktop、交互式 TUI(ChatMode)和 Peer 控制界面消费事实所在 Host 的同一来源状态,但按
-宿主展示;Peer 控制界面只代理 Peer Host,不读取控制端同名来源。当前 Server `/ws` 尚未接入 external-source App Server 方法;未来只读 Web 入口必须先通过版本化 App Server schema 接入 Host 能力,不能由浏览器扫描来源:
+宿主展示;Peer 控制界面只代理 Peer Host,不读取控制端同名来源。当前 Server `/ws` 已注册 external-source App Server 方法但未注入可信工作区 owner;未来只读 Web 入口必须先绑定 Host 持有的工作区范围,不能由浏览器提供任意路径或扫描来源:
```text
已发现 OpenCode 工作内容
@@ -162,8 +161,8 @@ stale,界面替换为服务端返回的新 plan,并只保留“旧选择与
Safe Mode 是执行域/工作区实例内的易失控制状态,不写入来源偏好,也不把来源伪装成 `disabled`。进入后继续发现和
展示 Command、Tool、Subagent 与 MCP,但立即撤下外部 Tool、Subagent 和 MCP 的新调用路由;Prompt Command 作为
静态模板继续可见。退出后基于当前来源版本重新协调,不能恢复已删除、已撤销或已过期审批的旧路由。
-GUI 和 TUI 都通过同一个 `SetSafeMode` 动作请求该变化,Peer Host 在事实所在 Host 执行;目标只读 Server 在完成 App Server 接线后通过
-`hostCapabilities` 明确拒绝变更,当前 Server external-source 方法仍是 `method_not_found`。所有偏好写操作携带 `expectedPreferenceRevision`,旧视图必须得到 `stale_revision`
+GUI 和 TUI 都通过同一个 `SetSafeMode` 动作请求该变化,Peer Host 在事实所在 Host 执行;目标只读 Server 在注入绑定可信工作区的只读 owner 后通过
+`hostCapabilities` 明确拒绝变更,当前通用 Server 因没有 management owner 而返回类型化 `unsupported`。所有偏好写操作携带 `expectedPreferenceRevision`,旧视图必须得到 `stale_revision`
并重新读取,不能用界面本地状态覆盖并发进程的新决定。
### 3.3 兼容来源与显式导入
@@ -299,7 +298,7 @@ generation lease 的模型绑定形态,不建立第二套 Agent Runtime。
Desktop、交互式 TUI 以及未来通过 Host 能力访问该状态的界面必须同时展示:来源请求、实际绑定、绑定方式和受影响候选数。
例如“来源请求 `sonnet`;当前工作区由用户绑定到 Primary(实际为已配置模型 X);影响 71 个 Agent”。用户可以选择其他
已配置模型、`primary`、`fast` 或保持相关候选禁用。界面不得把用户选择的替代模型描述成来源原始要求,也不得逐项重复确认
-同一绑定。目标只读 Server 在完成 App Server V1 前置切片后只投影脱敏状态,不获得写入能力;当前 Server 尚不能消费该投影。
+同一绑定。目标只读 Server 在注入绑定可信工作区的只读 management owner 后只投影脱敏状态,不获得写入能力;当前 App Server 方法已经注册,但通用 Server 尚未注入该 owner。
绑定目标的配置 ID 与 `model_runtime_binding_fingerprint` 进入既有激活审批 envelope。来源引用改变、绑定目标被删除或停用,
或者同一配置 ID 下的 provider、模型名、endpoint、认证来源及其他运行身份发生变化时,旧激活决定失效;进行中的调用继续
@@ -452,9 +451,9 @@ Theme、Keybind、完整插件清单,以及各生态新增的 managed/session/
## 6. 架构与职责
-当前 V1 生产路径是 `Desktop/TUI/Peer Host adapter → WorkspaceExternalSourceService → ExternalSourceControlPlane → 能力专属 provider`,宿主消费 `ExternalSourceControlSnapshotV1`、目录和既有能力级动作。它没有应用级连接状态、统一批量确认计划或任务依赖结果;当前 Server App Server 也尚未暴露这条只读路径。以下 6.1-6.3 全部是连接体验交付后的目标视图,不能作为现状证据。
+当前生产路径是 `Desktop/TUI/Peer Host adapter → WorkspaceExternalSourceService → ExternalSourceControlPlane → 能力专属 provider`。宿主消费现有的 `ExternalSourceControlSnapshotV1`、公共目录、integration policy 和能力级动作。这里不再建立第二套应用连接状态、跨能力批量确认或任务依赖事件。
-### 6.1 目标逻辑视图
+### 6.1 逻辑视图
```mermaid
flowchart TB
@@ -463,12 +462,11 @@ flowchart TB
Ports["能力专属 provider 契约"]
Discovery["ExternalSourceControlPlane\nprovider-neutral discovery"]
ProductCoordinator["WorkspaceExternalSourceService\n产品级协调"]
- Policy["产品能力事实 / 接入策略 / 安全上限"]
+ Policy["Integration policy / Safe Mode"]
Store["现有原子偏好存储"]
- AppView["版本化应用级读模型 + 批量确认计划"]
- Catalog["公共 catalog + 能力专属详情"]
+ Catalog["来源控制 + 公共 catalog"]
Owners["Command / Tool / Subagent / MCP / Config owner"]
- Surfaces["Desktop / TUI / Peer / future read-only Server"]
+ Surfaces["Desktop / TUI / Peer"]
Sources --- Adapters
Adapters --- Ports
@@ -476,35 +474,31 @@ flowchart TB
Discovery --- ProductCoordinator
Policy --- ProductCoordinator
Store --- ProductCoordinator
- ProductCoordinator --- AppView
ProductCoordinator --- Catalog
ProductCoordinator ---|窄 typed owner boundary| Owners
Owners --- Catalog
- AppView --- Surfaces
Catalog --- Surfaces
```
-图中连线只表示目标稳定逻辑关系,不表示调用顺序或用户动作;连接、断开和批量确认时序只在 6.3 描述。目标中,发现、连接和加载是三个独立阶段:适配层只产生候选;现有 `ExternalSourceControlPlane` 继续只协调能力专属提供方的发现、期限、代次和故障隔离;现有 `WorkspaceExternalSourceService` 增加产品级协调职责,结合产品事实、作用域化用户决定和安全上限派生应用级连接状态与确认计划,并通过窄类型化端口请求真实能力归属模块加载或撤下。应用级读模型不携带可执行载荷,也不取代公共目录或能力专属 DTO。这里不新增第二个公开控制面类型,也不声称这些新增职责已经接线。
+图中连线表示稳定逻辑关系,不表示调用顺序。适配层只产生候选;`ExternalSourceControlPlane` 负责 provider discovery、期限、代次和故障隔离;`WorkspaceExternalSourceService` 组合 integration policy 与目录,并把真正的批准、冲突选择、加载和撤下交给能力 owner。Desktop 可以按生态对这些事实分组,但分组只属于展示,不是第二个业务状态或协议对象。
-### 6.2 目标开发视图
+### 6.2 开发视图
```mermaid
flowchart TB
- ProductDomains["contracts/product-domains\n应用级状态、确认、类型化动作"]
+ ProductDomains["contracts/product-domains\nV1 来源、policy 与能力契约"]
AssemblyExternal["assembly/external-sources\nprovider-neutral 协调器"]
AssemblyCore["assembly/core\nWorkspaceExternalSourceService / 产品装配"]
EcosystemAdapters["adapters/*\n生态解析与原生覆盖"]
Services["services/*\n文件观察、原子存储、进程/网络"]
CapabilityOwners["execution / services / core owners\nCommand、Tool、Subagent、MCP"]
DesktopAdapter["apps/desktop\nTauri / Peer Host adapter"]
- WebUi["web-ui\nOverview / Detail / Review / Advanced"]
- Cli["apps/cli\n/extensions 与 action-required"]
- Server["server / remote adapters\n目标能力约束与只读投影"]
+ WebUi["web-ui\n简短概览 / 能力专项设置"]
+ Cli["apps/cli\n/extensions /tools /agent /mcp /hooks"]
WebUi --> DesktopAdapter
DesktopAdapter --> AssemblyCore
Cli --> AssemblyCore
- Server --> AssemblyCore
AssemblyCore --> AssemblyExternal
AssemblyCore --> EcosystemAdapters
AssemblyCore --> Services
@@ -515,9 +509,9 @@ flowchart TB
CapabilityOwners --> ProductDomains
```
-箭头表示目标编译期/模块依赖方指向被依赖方,不是运行时数据流。依赖方向继续遵守 interfaces/apps → assembly → adapters/services/execution → contracts;`assembly/external-sources` 只依赖 product-domain 契约,不反向依赖 Core、app 或具体生态 adapter。产品默认连接事实由 assembly 选择并通过稳定 contract 投影;React、TUI 和远端 adapter 不按生态 ID 重算默认值、应用状态或推荐集合。Server 节点只有在先把 V1 external-source 只读方法接入 App Server schema、handler/client translation 并通过 WebSocket round-trip 后才能进入这张目标图。
+箭头表示编译期依赖方指向被依赖方,不是运行时数据流。依赖方向继续遵守 interfaces/apps → assembly → adapters/services/execution → contracts;`assembly/external-sources` 只依赖 product-domain 契约,不反向依赖 Core、app 或具体生态 adapter。React 和 TUI 不复制审批、冲突或 capability owner 状态机。
-### 6.3 目标运行视图
+### 6.3 运行视图
```mermaid
sequenceDiagram
@@ -525,34 +519,27 @@ sequenceDiagram
participant Product as WorkspaceExternalSourceService
participant Discovery as ExternalSourceControlPlane
participant Adapter as Ecosystem Adapter
- participant Policy as Product Policy
+ participant Policy as Integration Policy
participant Owner as Capability Owner
participant Store as Preference Store
- Surface->>Product: 读取作用域化应用级 snapshot
+ Surface->>Product: 读取来源 control/catalog
Product->>Discovery: 按 execution domain / workspace scope 刷新
Discovery->>Adapter: 只读发现候选
Adapter-->>Discovery: 来源、版本、风险摘要
Discovery-->>Product: 同代能力专属发现结果
- Product->>Policy: 计算默认连接、推荐集合与安全上限
- Policy-->>Product: OpenCode 可默认连接;其他生态只发现
- Product-->>Surface: 应用状态、主操作、review plan
- Surface->>Product: ConnectApplication(scope, expected revision)
- Product->>Store: 原子保存连接决定并推进权威 preference revision
- Product->>Owner: 仅请求允许自动应用的低风险内容
- Owner-->>Product: 已启用 / 受限 / 失败结果
- Product-->>Surface: 连接完成摘要
- Surface->>Product: SubmitApplicationReview(scope, generations, decision keys)
- Product->>Product: 重验身份、revision、generation 与 safety ceiling
- Product->>Owner: 按能力类型提交批准项
- Owner-->>Product: 逐项权威结果
- Product->>Store: 原子保存有效决定
- Product-->>Surface: 同代 snapshot 与逐项结果
+ Product->>Policy: 读取作用域化启停和 capability access
+ Product-->>Surface: 来源/应用简短概览
+ Surface->>Product: 更新 integration policy 或来源启停
+ Product->>Store: 校验 preference revision 后原子保存
+ Surface->>Owner: 通过 /tools、/agent、/mcp 或 /hooks 处理精确对象
+ Owner->>Owner: 重验 identity、version、scope 与 generation
+ Owner-->>Surface: 权威批准、拒绝、冲突或恢复结果
```
-目标运行语义中,发现不会产生执行副作用。连接先在目标执行域和工作区作用域中持久化应用级决定,再只协调共享策略允许的低风险内容;批量确认仍分派到各能力归属模块,并在提交前重新校验身份、作用域、偏好版本、发现代次、决策键、行为版本、宿主能力、Safe Mode 和安全上限。
+发现不会产生执行副作用。启停只改变 integration policy 或来源状态;可执行内容在真正的能力 owner 中按精确对象确认。跨能力页面不能代替 owner,也不能批量扩大权限。
-### 6.4 现有能力与连接体验的边界
+### 6.4 现有能力边界
| 部分 | 负责 | 不能承担 |
|---|---|---|
@@ -563,8 +550,8 @@ sequenceDiagram
| 文件观察服务 | 提供可订阅、去抖的文件变化事实 | 解释生态路径、决定优先级、提交业务状态。 |
| 本地 JSON 存储服务 | 提供跨进程锁、锁内读改写和同卷原子替换;替换失败时保留旧文件 | 定义外部来源偏好 schema、冲突策略或生态语义。 |
| `ExternalSourceControlPlane` | 四类来源分别刷新;同一 provider 同一时间只扫描一次;超时只影响该 provider;旧结果不能覆盖新刷新;确认最新结果后,再通知对应能力模块切换 | 按生态 ID 分支业务行为、把四类数据合并为通用资产、解析生态文件、直接提交配置、工具、权限或界面状态。 |
-| `WorkspaceExternalSourceService` / 产品级协调 | 绑定执行域与工作区路由;组合产品事实、现有偏好和控制面发现结果;派生应用级投影;通过窄类型化端口分派连接、撤下和批量确认,并汇总归属模块的权威结果 | 复制提供方调度器、能力审批/冲突存储或 Runtime 归属;把无法撤下的能力宣称为已断开;成为新的公共跨生态执行 API。 |
-| 版本化控制状态视图 | 根据 discovery/desired/review/runtime/support 事实生成一级状态;向宿主提供同一版本的 control/catalog、`hostCapabilities`、恢复动作和固定通用操作 | 保存第二份权威状态、携带 Prompt/凭据/可执行数据、替代能力专属审批和冲突 DTO,或让 GUI/TUI 自行推导生命周期。 |
+| `WorkspaceExternalSourceService` / 产品级协调 | 绑定执行域与工作区路由;组合 integration policy、现有偏好和控制面发现结果;向能力 owner 提交窄类型化请求 | 复制提供方调度器、能力审批/冲突存储或 Runtime 归属;派生第二套应用连接状态;成为新的公共跨生态执行 API。 |
+| 来源控制状态视图 | 根据 discovery、desired、owner decision、runtime 和 support 事实提供 control/catalog、`hostCapabilities`、恢复动作和固定通用操作 | 保存第二份权威状态、携带 Prompt/凭据/可执行数据、替代能力专属审批和冲突 DTO。 |
| 界面状态 | 按使用范围、工作区或用户目录关系统一生成安全来源位置,清理诊断文本中的已知绝对路径,并按 `Source / Command / Tool / Subagent` 资源类型路由诊断 | 让 GUI/TUI 解析 provider 诊断码前缀、识别 `.opencode`、`.claude` 等私有目录结构,或接收原始用户/工作区路径。 |
| 冲突解析 | 对独立 provider 或产品本地可执行能力的同名候选建立版本敏感内容摘要;未选择时不激活,选择后只在内容摘要不变时复用。现有 Skill 固定根顺序由 Skill 归属模块独立维护 | 用 adapter 优先级静默覆盖另一生态或本地可执行能力,或把选择写回外部文件。 |
| 激活策略与能力归属模块 | 根据风险、用户选择、组织上限和执行位置决定自动应用、等待确认或限制 | 修改生态加载顺序或把策略拒绝伪装成解析失败。 |
@@ -589,10 +576,7 @@ provider discovery 必须是可独立调度的 request/result,不在协调器
未来网络 provider 仍应实现协作式超时和取消,
但不改变目录、冲突或产品入口契约。
-控制请求保持闭合且类型化:严格 V1 只保留已有 `Refresh`、`SetSourceEnabled` 和 `SetSafeMode`;应用级连接体验在独立协商后的 V2 增加
-`ConnectApplication`、`DisconnectApplication`、`SetApplicationDeferred` 和 `SubmitApplicationReview`。批量 review 只封装
-一组带执行域、workspace route、能力类型、generation、decision key 和 behavior version 的选择,并由产品级协调 owner 分派给现有能力归属模块;它不能成为携带
-任意数据的通用执行 API。能力专属执行参数和调用时权限继续由各归属模块的类型明确契约承担。错误以 `code + stage + retryable +
+控制请求保持闭合且类型化:现有来源 control 保留 `Refresh`、`SetSourceEnabled` 和 `SetSafeMode`,应用/生态启停复用 integration policy mutation。能力审批、冲突和执行参数继续由各归属模块的类型明确契约承担,不增加跨能力批量动作。错误以 `code + stage + retryable +
correlationId/causationId + recoveryActions` 表达;`detail` 只用于有界诊断,界面和远端协议不得解析文本
决定控制流。日志只记录动作、阶段、关联 ID、错误类别和脱敏对象身份;产品打点可在同一结果上叠加,但不得反向改变状态。
@@ -602,9 +586,7 @@ Command;明确缺失且未被标记失败的 Command 是稳定删除。产品
## 7. 状态与提示规则
-本节定义底层来源/能力的正交生命周期状态;面向 Settings 首页和 TUI `/extensions` 的五种应用级摘要、优先级和主操作,统一见
-[外部 AI 应用连接与管理详细设计](external-ai-app-connection-experience-design.md#3-产品状态模型)。宿主不得把底层状态直接拼成第二套
-应用级规则,也不能用应用摘要替代底层事实。
+本节定义底层来源/能力的正交生命周期状态。Settings 首页和 TUI `/extensions` 可以隐藏不必要的技术细节并生成简短摘要,但不得建立第二套应用级状态规则,也不能用摘要替代底层事实。
| 用户状态 | 含义 |
|---|---|
@@ -625,8 +607,7 @@ Command;明确缺失且未被标记失败的 Command 是稳定删除。产品
- 用户关闭、确认、断开连接或选择暂不使用后,同一内容/行为与风险摘要版本不再主动提示;普通数量变化只更新应用摘要。
- 再次主动提示仅限当前任务确实因待确认能力受阻或降级,或者已确认内容发生实质权限扩大。与当前任务无关的更新失败、来源删除和未连接应用变化只更新状态与恢复动作。
- 普通文件变化、多个同源错误和多项目全局更新按应用/来源聚合;详情进入设置页或 CLI 状态,每次重载最多产生一条摘要,不用 Toast 展示字段级错误。
-- 非交互入口只有在当前操作实际依赖待确认资产时才返回类型化 `action-required`;无关待办只进入结构化状态或
- `stderr` 摘要,不阻塞当前操作,也不自动批准。
+- 非交互入口不等待人工确认,也不从全局待办推断特殊任务结果;能力不可用时返回普通失败且不自动批准。
## 8. 分阶段落地与验收
diff --git a/docs/architecture/product-architecture.md b/docs/architecture/product-architecture.md
index ed904873d6..2e9e5d15ec 100644
--- a/docs/architecture/product-architecture.md
+++ b/docs/architecture/product-architecture.md
@@ -7,10 +7,8 @@
内置扩展边界见 [`product-customization-blueprint.md`](product-customization-blueprint.md);CLI 产品入口和配置
兼容见 [`cli-product-line-design.md`](cli-product-line-design.md);HarmonyOS PC 原生 CLI/TUI 平台规约见
[`platform-portability-design.md`](platform-portability-design.md)。跨专题实施顺序见
-[`../plans/product-architecture-evolution-plan.md`](../plans/product-architecture-evolution-plan.md)。外部 AI 工作内容架构、应用级连接详细设计与对应执行计划分别见
-[`external-ai-work-sources-design.md`](extensions/external-ai-work-sources-design.md)、
-[`external-ai-app-connection-experience-design.md`](extensions/external-ai-app-connection-experience-design.md)和
-[`external-ai-app-connection-experience-plan.md`](../plans/external-ai-app-connection-experience-plan.md);OpenCode 扩展总矩阵、配置资产、插件执行、
+[`../plans/product-architecture-evolution-plan.md`](../plans/product-architecture-evolution-plan.md)。外部 AI 工作内容架构见
+[`external-ai-work-sources-design.md`](extensions/external-ai-work-sources-design.md);OpenCode 扩展总矩阵、配置资产、插件执行、
终端插件和外部集成适配分别见
[`opencode-extension-compatibility.md`](extensions/opencode-extension-compatibility.md)、
[`opencode-config-assets-adapter-design.md`](extensions/opencode-config-assets-adapter-design.md)、
@@ -638,8 +636,8 @@ flowchart LR
Node/Bun 和第三方 JS/TS 的子进程;插件启停与贡献生命周期仍由既有来源和能力归属模块管理。
- 外部来源的 Command、Tool、Subagent、MCP 仍保留能力专属 DTO 和 owner,但它们的发现调度统一由
`ExternalSourceControlPlane` 持有;当前 Desktop/TUI/Peer 的控制事实只通过版本化的 product-domain 只读视图共享,
- 不复制生态 payload、界面状态机或远端专用 DTO。Server 的 external-source helper 当前未接入 App Server schema,生产 `/ws`
- 返回 `method_not_found`;只有完成 V1 read-only schema、handler/client translation 和 WebSocket round-trip 后,Server 才进入该共享边界。
+ 不复制生态 payload、界面状态机或远端专用 DTO。App Server 已注册 external-source schema、handler 和 client translation;Embedded Host
+ 注入 management owner 后可以调用。通用 Server `/ws` 当前没有绑定可信工作区的 management owner,因此返回类型化 `unsupported`;只有注入 Host 持有的作用域化 owner 并通过 WebSocket round-trip 后,Server 才交付该共享边界。
- 每个生态适配层独立保留该生态的外部格式、来源顺序和调用语义,并映射到 BitFun 归属模块;它本身不成为新的
业务归属模块,也不能依赖或修改兄弟生态 adapter。通用目录、`ExternalSourceControlPlane` 和能力归属模块只依赖开放生态 ID、
来源限定身份与能力专属 provider 契约,不按 OpenCode、Codex 或 Claude Code 分支行为。
@@ -817,10 +815,10 @@ flowchart LR
| 当前入口 | 已有能力 | 明确边界 |
|---|---|---|
-| Desktop | 使用 `product-full`;显示外部来源、审批、冲突、诊断和 Host 能力;目标增加应用级读模型、默认连接事实和批量确认 DTO | 可执行能力在事实所在 Host 运行;Safe Mode 只阻止新调用,不改来源、不取消正在运行的调用 |
-| CLI / TUI | 使用显式 Core owner feature closure(`agent-runtime`、`canvas-runtime`、`external-sources`、`plugin-runtime`、`ssh-remote`);现有 `/extensions` 支持来源状态、刷新、Safe Mode 和来源开关,统一 `/hooks`(旧 `/hooks_external` 为别名)、`/tools` 和 `/agents` 保留各自专项职责 | 应用级摘要、首次连接、`/extensions review` 批量确认和任务相关 `action-required` 仍是目标能力,完成条件以对应详细设计和 P6 端到端证据为准;生态解析仍在适配器,不启动第二套 Agent Runtime;远程能力未接入时不回退本机 |
+| Desktop | 使用 `product-full`;Settings 从现有来源目录和 integration policy 生成简短应用概览,具体审批与冲突仍进入 Tool、Agent、MCP 或 Hook owner | 可执行能力在事实所在 Host 运行;Safe Mode 只阻止新调用,不改来源、不取消正在运行的调用 |
+| CLI / TUI | 使用显式 Core owner feature closure(`agent-runtime`、`canvas-runtime`、`external-sources`、`plugin-runtime`、`ssh-remote`);`/extensions` 只提供状态、启停和刷新,`/hooks`、`/tools`、`/agent` 和 `/mcp` 处理各自能力 | 非交互不等待权限输入,也不从全局状态或错误文本推断特殊任务结果;生态解析仍在适配器;远程能力未接入时不回退本机 |
| ACP | 使用 `DeliveryProfile::Acp`、Runtime Parts,以及 `agent-runtime`/`canvas-runtime`/`external-sources`/`ssh-remote` Core owner feature | load 成功后才发布活动状态;close 排空后再卸载;完整历史、Canvas 工具物化、兼容指令来源和配置仍由 Core/ACP 管理 |
-| Peer / Server | Peer Host 执行真实工作区操作;当前 HTTP Server 使用 `product-full` 组装 Embedded Runtime,并通过 `/ws` 暴露 App Server,但 external-source 方法尚未进入 schema、当前返回 `method_not_found` | 控制端不替远端发现或执行;Server external-source 只读投影须先完成真实 App Server 接线,且 loopback 单用户边界不扩展到远程/多用户;SSH Remote 未接入时返回不支持 |
+| Peer / Server | Peer Host 执行真实工作区操作;通用 HTTP Server 未绑定可信 workspace owner 时明确返回不支持 | 控制端不替远端发现或执行;loopback 单用户边界不扩展到远程/多用户;SSH Remote 未接入时返回不支持 |
| Web / Mobile Web | 依赖现有后端入口 | 不持有插件执行单元,也不能据空 profile 宣称独立能力 |
| HarmonyOS 手机 Remote | phone-only ArkTS 远程入口 | 不等于 HarmonyOS PC 本地 Runtime、CLI/TUI 或 GUI |
@@ -837,7 +835,7 @@ Shared Agent Runtime 是第一方多实例的目标部署,不是上表新增
底层来源与能力继续使用[外部 AI 工作内容设计](extensions/external-ai-work-sources-design.md#7-状态与提示规则)定义的
已发现、已应用、可用、需确认、更新中、沿用上一版本、部分受限、暂时过期、已移除/已停用和不可用,并附带
-原因与恢复建议。目标状态下,Settings 首页和 TUI `/extensions` 将消费[应用级连接详细设计](extensions/external-ai-app-connection-experience-design.md#34-面向用户的应用级状态)派生的五种摘要;当前生产入口仍消费 V1 来源/能力控制事实,不能据目标文档宣称应用级连接已经交付。目标摘要不能替代底层事实,宿主也不能自行重算优先级。Host 的准备完成、重启、暂停、不支持或失败只作为详情映射。现有代码中的过渡状态只能展示为“静态预览、未执行”,不能因为进入来源清单就误报为已应用、已连接或可用。
+原因与恢复建议。Settings 首页和 TUI 可以把这些事实压缩为简短应用/来源概览,但不能建立第二套连接、审批或任务结果状态机,也不能因为进入来源清单就误报为已应用或可用。
## 7. 完成判定
diff --git a/docs/plans/external-ai-app-connection-experience-plan.md b/docs/plans/external-ai-app-connection-experience-plan.md
deleted file mode 100644
index 9fc57e62bd..0000000000
--- a/docs/plans/external-ai-app-connection-experience-plan.md
+++ /dev/null
@@ -1,637 +0,0 @@
-# 外部 AI 应用连接体验执行计划
-
-> 本计划把[外部 AI 工作内容总体架构](../architecture/extensions/external-ai-work-sources-design.md)和[外部 AI 应用连接与管理详细设计](../architecture/extensions/external-ai-app-connection-experience-design.md)拆成可独立评审、验证和回退的实施阶段。本文不扩大任何生态的能力兼容范围;OpenCode 具体能力路线仍以[OpenCode 扩展兼容计划](opencode-extension-compatibility-plan.md)为准。
-
-> **实现状态:分阶段交付。** 当前分支已完成共享 V2 应用契约、产品默认、旧偏好迁移、分页批量确认、Desktop/Peer/App Server 薄适配,以及 Desktop Settings 和交互式 TUI 的纵向切片。Web 在旧 Host 上保持严格 V1 只读回退;交互式 TUI 只在 Embedded 旧 Host 上回退 V1,未接线的 Shared Runtime 明确不支持且不会改在控制进程本地执行。通用 Server 尚未绑定可信 workspace owner,任务相关 `action-required`、非交互 CLI 结果、组合 Hook 摘要和完整跨宿主回归仍按本计划后续阶段推进,不能据此宣称支持。
-
-## 1. 目标与执行原则
-
-目标是在保留现有 Command、Tool、Subagent、MCP、Safe Mode、冲突和远端保护语义的前提下,把“外部 AI 应用”从能力平铺页调整为应用级连接与管理体验:
-
-1. 后台发现、应用连接和能力加载明确分离;
-2. OpenCode 可由产品事实默认连接,Codex 与 Claude Code 默认只发现;
-3. 低风险声明式内容按共享策略自动应用,可执行或权限扩大的内容进入单页批量确认;
-4. Desktop、TUI、Peer 和 Server 消费同一应用级读模型、默认策略和决策结果;
-5. 提示一次性、持久化去重,只在任务受阻/降级或实质权限扩大时再次主动出现;
-6. 不把应用级聚合对象变成新的配置、权限或执行归属模块。
-
-执行遵守以下原则:
-
-- 每个阶段形成可独立评审的纵向结果,不能用仅有 DTO、固定假数据或未接线组件宣称完成;
-- 先以测试冻结共享契约和策略,再接宿主,再替换信息架构;
-- 当前 `ExternalSourceControlSnapshotV1`、V1 动作/恢复闭合枚举、V1 宿主能力和能力专属 DTO 保持字段与行为不变;应用级读写使用独立版本化 V2 接口,V2 快照不提交用户决定或运行能力写动作并直接用于能力探测;首次 owner 激活仍可执行可重入迁移和既有后台发现;
-- 所有 V2 写操作携带 `execution_domain_id`、`target_scope`、`operation_id` 和与该作用域绑定的 `expected_preference_revision`;`workspace_override` 必须携带宿主快照返回的 `workspace_scope_id`,`user_default` 必须省略。`operation_id` 只做请求/响应关联,不承诺幂等重放;偏好版本是唯一写并发保护;
-- 宿主能力、Safe Mode、组织/产品安全上限和 Remote/只读限制只能收紧结果;
-- React、TUI、Desktop 适配层和 Server 适配层不按生态 ID 重算默认连接、推荐集合或应用级状态;
-- 不建立第二套审批存储、冲突存储、监听系统、调度器或运行时注册表;
-- 先完成版本化旧偏好迁移,再启用新的默认连接;升级不能静默撤下已有效使用的能力或覆盖显式 disabled/discover-only;
-- GUI 与 TUI 共享语义和契约样例,不共享布局、组件、主题键、快捷键或渲染数据结构。
-
-## 2. 变更地图
-
-| 责任 | 主要文件 | 计划内变更 |
-|---|---|---|
-| 共享应用级契约 | `src/crates/contracts/product-domains/src/external_source_control.rs` | 保持 V1 不变,独立定义 `ExternalApplicationSnapshotV2`、五种摘要状态、主操作、默认连接事实、确认计划、逐项结果和 V2 类型化动作;任务依赖结果归 Agent 事件契约,不塞入可轮询应用快照。 |
-| 产品默认与能力上限 | `src/crates/assembly/core/src/external_sources.rs` 及 assembly 中现有产品能力事实归属模块 | 提供 OpenCode 默认连接、Codex/Claude Code 默认只发现的产品事实;派生推荐集合、安全上限与应用状态。 |
-| 偏好、迁移与提示去重 | `src/crates/assembly/core/src/external_sources.rs` | 在现有原子偏好存储中加入作用域化连接、暂不使用、提示决定和一次性 `connection_schema_migration_version`;每个旧作用域直接生成真实连接决定,不新增第二个迁移状态机或存储。 |
-| 批量确认编排 | `src/crates/assembly/core/src/external_sources.rs` | 预检整批偏好版本、发现代次和宿主条件,按能力类型分派现有归属模块,汇总逐项权威结果。 |
-| Desktop/Peer/App Server 投影 | `src/apps/desktop/src/api/external_sources_api.rs`、`src/apps/desktop/src/api/remote_workspace_policy.rs`、Peer 适配层、`src/crates/interfaces/app-server{,-protocol,-client}` 与 `src/apps/server` | 保持薄适配层;先把当前缺失的 V1 Server 只读投影接入 App Server 协议、客户端和处理器,再增加独立 V2 协商和接口;声明远端策略;旧宿主保持 V1 并拒绝 V2 写操作。 |
-| Runtime 任务依赖结果 | `src/crates/contracts/events/src/agentic.rs`、`src/crates/assembly/core/src/agentic`、`src/crates/interfaces/app-server{,-protocol,-client}`、`src/crates/adapters/agent-runtime-ipc`、CLI 执行生命周期 | 能力归属模块产生依赖事实,Agent Runtime 关联根/来源轮次并发布 `ExternalDependencyActionRequired`;App Server 与 Shared IPC 传输同一事件,CLI 只投影匹配当前根轮次的结果。 |
-| TypeScript 基础设施 | `src/web-ui/src/infrastructure/api/service-api/ExternalSourcesAPI.ts`、`ExternalSourcesAPI.test.ts` | 保持 V1 转换不变,新增独立 V2 转换,并对作用域、发现代次、偏好版本和协议协商安全拒绝。 |
-| Web UI | `src/web-ui/src/infrastructure/config/components/ExternalSourcesConfig.tsx` 及同目录拆分组件、样式和测试 | 收敛页面控制器,增加首页、待办、详情、批量确认和高级设置的纵向单列体验。 |
-| TUI/CLI | `src/apps/cli/src/modes/chat/external_review.rs`、`external_hooks.rs`、`external_sources.rs`、`src/apps/cli/src/actions.rs` | `/extensions` 应用级入口、`/extensions review`、共享提示去重和任务相关 `action-required`。 |
-| i18n 与主题 | 外部来源设置页现有命名空间、CLI 自有本地化资源、现有 SCSS/主题令牌 | 新文案进入归属模块的命名空间,复用 600px 布局和主题令牌,不提高治理基线。 |
-
-具体文件可在实施阶段按仓库当时结构做最小调整,但责任归属和依赖方向不得改变。
-
-## 3. 阶段依赖
-
-```mermaid
-flowchart LR
- P1["P1 应用级契约与产品事实"] --> P2["P2 连接偏好与提示去重"]
- P2 --> P3["P3 批量确认编排"]
- P1 --> P4["P4 宿主与协议投影"]
- P3 --> P4
- P4 --> P5["P5 Desktop Web UI"]
- P4 --> P6["P6 TUI 与非交互 CLI"]
- P5 --> P7["P7 跨宿主回归与迁移清理"]
- P6 --> P7
-```
-
-P1-P4 是共享语义和协议前置;P5 与 P6 可以在 P4 稳定后并行,但必须以同一契约样例验证。P7 只在 Desktop 与 TUI 都消费共享读模型后执行,不能提前删除旧投影。
-
-## 4. P1:应用级契约、产品事实与状态派生
-
-### 归属与范围
-
-- 归属:`contracts/product-domains` 与 Product Assembly;
-- 主要文件:
- - `src/crates/contracts/product-domains/src/external_source_control.rs`
- - `src/crates/assembly/core/src/external_sources.rs`
- - 对应 crate 内已存在的 focused tests。
-
-### 实施内容
-
-1. 冻结 `ExternalSourceControlSnapshotV1`、V1 动作/恢复枚举和 V1 `hostCapabilities`,另行增加 `ExternalApplicationSnapshotV2`:
- - `application_id` 与 `ecosystem_id`;
- - `execution_domain_id`、可选但非通配的 `workspace_scope_id` 和实际连接作用域;`workspace_scope_id` 复用当前宿主的 `workspace_policy_key`,不新增路径注册或反查;
- - 发现、连接、健康等正交事实;
- - `已连接 / 发现可用配置 / 未发现配置 / 需要处理 / 暂时不可用`;
- - 唯一 `primary_action`;
- - `enabled`、`pending_review`、`blocked`、`conflict` 数量;
- - 风险摘要和恢复动作;
- - 确认摘要、稳定 `review_id`、推荐数量/风险、`max_selection_count` 和总数,不内嵌项目列表或可执行载荷。
-2. 另行定义 `ExternalApplicationReviewPageV2`:首次无 cursor/no-generation 打开允许 Host 在后台发现刚完成时返回当前只读计划,客户端从该响应接续;其余分页游标严格绑定执行域、工作区作用域、`review_id`、偏好版本和发现代次。每页最多 128 项,只携带稳定项目引用、显示摘要、推荐和安全上限。读取分页不能触发重新发现或能力加载,提交仍必须绑定首次响应的权威计划。
-3. 将应用级状态优先级固定在共享归属模块:
- `需要处理 > 暂时不可用 > 已连接 > 发现可用配置 > 未发现配置`;Safe Mode 独立投影。
-4. 在 Product Assembly 中定义默认连接事实及原因:
- - OpenCode:允许默认连接;
- - Codex、Claude Code:默认只发现;
- - 未注册生态、旧宿主或受限产品形态:明确不支持或只读,不猜测默认值。
-5. 从现有目录、能力控制事实和归属模块状态派生应用聚合;未连接应用不参与运行时冲突和能力注册。
-6. 推荐集合由共享策略生成,高风险项默认不推荐;宿主只展示,并只允许在安全上限内调整。
-7. 应用快照不持久化或全局聚合任务影响;任务相关结果在 P6 由 Agent Runtime 事件契约单独实现,P1 只定义供其引用的稳定应用/依赖引用。
-8. 应用级纯状态、动作和作用域规则归 `contracts/product-domains`;具体聚合、持久化和归属模块分派留在 `WorkspaceExternalSourceService`,`ExternalSourceControlPlane` 不接收产品状态职责。
-
-### 测试优先顺序
-
-先增加失败测试,再实现最小派生逻辑:
-
-- OpenCode、Codex、Claude Code 默认连接事实;
-- 五种状态的优先级和 Safe Mode 独立性;
-- “已连接但有能力待确认”不会错误显示为全部已启用;
-- 未连接应用不进入运行时冲突;
-- 未知枚举、不同发现代次或不同偏好版本均安全拒绝;
-- 用户默认、工作区覆盖和不同执行域的状态互不污染;
-- V1 序列化固定样例完全不变,V2 未协商时不可调用;
-- 首页快照不含确认项目;分页单页不超过 128,过期游标不能与新代次拼接;
-- 一个轮次的任务依赖结果不能改变另一个轮次的应用状态或退出结果;
-- 高风险项默认不进入推荐集合;
-- 产品、组织和宿主上限不能被宿主推荐放宽。
-
-### 验证
-
-```bash
-cargo test -p bitfun-product-domains external_source_control
-cargo test -p bitfun-core external_source
-cargo check --workspace
-```
-
-实际 package 名以对应 `Cargo.toml` 为准;若 focused test 过滤器不能覆盖新增测试,运行受影响 crate 的完整测试,不用全 workspace 测试代替静态检查。
-
-### 用户可见结果
-
-无独立用户界面变化;后端能够稳定返回应用级状态、默认策略、主操作和确认计划。
-
-### 退出条件
-
-- Desktop/TUI 无需生态分支即可渲染同一 fixture;
-- V1 消费方保持可编译、golden wire shape 和原有行为;
-- 应用级状态完全由共享归属模块派生;
-- V2 应用状态按执行域和工作区作用域求值,任务依赖只存在于根会话和根轮次绑定的 Agent Runtime 事件;
-- 默认连接事实有产品组装测试,不存在 `ecosystem_id == "opencode"` 的宿主业务分支。
-
-### 暂停条件
-
-若应用级聚合需要读取能力 owner 尚未公开且无第二个真实消费方的内部状态,先设计最窄只读事实并完成 owner 评审;不得通过公开任意 payload 或复制 owner 状态绕过。
-
-## 5. P2:连接、断开、暂不使用与提示去重
-
-### 归属与范围
-
-- 归属:`assembly/core` 的 `WorkspaceExternalSourceService`(或实施时同一现有产品级服务的私有协调单元)和现有偏好存储;`assembly/external-sources` 的 `ExternalSourceControlPlane` 只提供与提供方无关的发现结果;
-- 主要文件:
- - `src/crates/assembly/core/src/external_sources.rs`
- - `src/crates/contracts/product-domains/src/external_source_control.rs`
- - 对应持久化和并发测试。
-
-### 实施内容
-
-1. 在 V2 endpoint 增加闭合类型化动作:
- - `ConnectApplication`;
- - `DisconnectApplication`;
- - `SetApplicationDeferred`;
- - 保持已有 `Refresh`、`SetSourceEnabled` 和 `SetSafeMode`。
-2. 在现有偏好文件和跨进程原子更新路径中持久化:
- - execution domain ID;
- - `user_default` 或 `workspace_override`;workspace override 携带当前 `workspace_policy_key` 产生的 Host-local `workspace_scope_id`;
- - application/ecosystem ID;
- - desired connection 状态;
- - 明确断开或暂不使用决定;
- - notice key、内容/行为版本、风险摘要版本和用户决策状态;
- - 一次性 `connection_schema_migration_version`;它只与整份文档的原子转换一起写入;
- - 按 `(execution_domain_id, application_id, workspace_scope_id?)` 保存的真实连接决定与 `decision_origin`,无法归属的项直接使用 `needs_review`,不保存逐 scope 迁移进度。
-3. 在启用新默认连接前执行锁内、可重入的旧偏好迁移。`WorkspaceExternalSourceService` 启动时先建立全局迁移 gate;所有 discovery、MCP revision-key helper 和 V2 endpoint 必须等待它完成或返回明确 incompatible/needs-review 状态。该 gate 先读取原始存储存在性和 schema,再调用会通过 MCP secret/revision-key 初始化自动物化默认文件的 `external_sources_config_with_mcp_revision_key`;不得根据已经默认化的对象猜测旧文件来源:
- - 新决定已存在时保持不变;
- - 只有确认从未存在偏好文件的 V2 新安装写入 `config_origin=fresh_v2`,允许保持“无决定”并应用新产品默认;已有文件、legacy 默认文件和 incompatible-policy reset 都不能获得该 origin;
- - 任一 legacy 文件中的 `integration_policy.enabled=false` 都保守迁移为显式未连接,并记录 `decision_origin=legacy_safety`;这包括旧版本自动写出的默认文件。它与用户显式 `SetEnabled(false)` 无法区分,因此不能让 OpenCode 默认连接覆盖。代价是部分从未主动关闭的旧用户需重新连接一次,迁移说明必须明确该安全取舍;
- - 目标 user default/workspace override 下该生态明确求得 disabled/discover-only 时,同样迁移为显式未连接;
- - 只有旧作用域的 `integration_policy.enabled=true` 且已有效使用某生态时才迁移为已连接;该判断晚于上一条保守未连接规则。“有效使用”要求至少一项实际访问级别为 `ask_before_use`/`auto`,或存在可按来源归属的审批、冲突决定或活动路由;
- - 当前 `workspace_overrides` 的键已是 `workspace:` 加规范化工作区 SHA-256 的前 16 字节十六进制;直接把每个键作为 `workspace_scope_id` 逐项迁移,不建立路径反查。无法可靠归属应用、执行域或作用域的旧记录写为 `needs_review`,对应作用域继续由 V1 路径管理;
- - 读到未知未来 `schemaMajor` 时沿用当前 incompatible-policy fail-closed:不迁移、不应用默认、不写 V2 决定、不进入偏好 update/atomic replace,byte-for-byte 保留包含 opaque policy 的原文件;用户执行既有“备份并重置”时,在同一原子更新中备份 raw policy、写入 `config_origin=incompatible_reset` 和显式未连接决定,继续保持外部执行关闭,不能转成 fresh V2;
- - 先在内存中计算全部旧作用域决定,再把决定、schema migration version 和现有审批/冲突数据一次原子替换。成功时不存在部分迁移;失败保持旧文件和完整 legacy 路径,重启后重试整次转换。
-4. 默认连接只对 `fresh_v2` 或已完成迁移且确实没有显式决定的作用域生效;工作区覆盖优先于同一执行域的用户默认;明确断开、暂不使用、不兼容策略或 `decision_origin=legacy_safety` 不得被监听器、重启或重新发现覆盖。
-5. 连接先在现有权威偏好文档中提交作用域化决定并推进 preference revision,再协调允许自动应用的低风险内容;返回已启用、待确认、受限和失败摘要。
-6. 断开先撤下该 execution domain/workspace scope 上的新调用路由和由该连接注册的能力,再停止持续同步;不改写外部配置,不影响其他作用域或生态。
-7. Instruction、Skill、Hook 和复制后的原生配置仍服从各自 owner。没有来源限定撤下端口的能力必须报告 `managed_separately`/部分支持,并暂停“完整断开”交付,不能由 UI 隐藏冒充卸载。
-8. 提示规则:
- - 首次发现只允许一次性非阻塞轻提示;
- - 用户关闭、决定或完成处理后,同一版本不再主动提示;
- - 仅当前任务受阻/降级或已确认内容权限实质扩大时再次主动提示;
- - 普通数量变化、无关更新失败和来源删除只更新状态。
-
-### 测试优先顺序
-
-- 默认连接与显式断开/暂不使用的优先级;
-- fresh V2 无文件时 OpenCode 可应用产品默认;旧版自动物化的默认文件与用户显式 `enabled=false` 都保守保持未连接,且不会被默认连接覆盖;
-- legacy 配置中 disabled/discover-only、已有效使用的 Claude Code/Codex/OpenCode、无决定生态分别迁移到预期状态;
-- 多个 workspace scope 的迁移要么一次全部提交,要么一个都不提交;写入失败和崩溃重启不会留下部分新状态;
-- discovery、MCP revision-key 初始化与 V2 endpoint 并发首次访问时都等待同一 migration gate,不能先物化默认文件或观察半迁移状态;
-- 未知未来 `schemaMajor` 保持原始 JSON、拒绝迁移和 V2 mutation;备份并重置后记录 `incompatible_reset` 且仍显式未连接,不应用 OpenCode 默认;
-- future-major → backup/reset → restart fixture 证明 raw backup 保留、外部执行仍关闭,只有后续显式 ConnectApplication 才改变状态;
-- stale preference revision 整个 mutation 不应用;
-- 响应丢失后使用旧偏好版本重试会返回过期;客户端重读权威快照后再决定是否发送新操作,相同 `operation_id` 不能绕过版本检查或重放旧结果;同一活动连接中的并发请求不复用 ID;
-- 跨进程并发更新不丢失另一个应用的决定;
-- 两个工作区作用域和两个执行域的连接、提示与偏好版本相互隔离;
-- watcher 更新不会重新连接用户已断开的应用;
-- 断开仅卸载目标生态能力;
-- notice key 在 GUI/TUI/重启之间去重;
-- 权限扩大产生新风险版本,普通数量变化不产生主动提示。
-
-### 验证
-
-```bash
-cargo test -p bitfun-core external_source
-cargo check --workspace
-```
-
-### 用户可见结果
-
-连接、断开和暂不使用具有明确完成结果;同一发现不会在多个项目、进程或宿主反复提示。
-
-### 退出条件
-
-- 所有连接决定和 `connection_schema_migration_version` 使用现有原子偏好存储,且没有第二套逐 scope 迁移状态机;
-- 默认连接与用户显式决定的优先级可由重启测试证明;
-- 断开后目标 execution domain/workspace scope 的相关新调用不可达,其他作用域和生态不受影响;
-- 旧审批、拒绝、冲突和来源抑制记录在迁移后保持,只有 fingerprint 失效或权限扩大才重新确认;
-- 提示去重不依赖 React local storage 或 TUI 进程内集合。
-
-### 暂停条件
-
-若某能力 owner 无法按来源/生态撤下路由,先补 owner 的类型化撤下能力和行为测试;不得把“UI 显示已断开”作为运行时已卸载的替代证据。
-
-## 6. P3:单页批量确认与归属模块分派
-
-### 归属与范围
-
-- 归属:`assembly/core` 的产品级 `WorkspaceExternalSourceService` 负责预检与分派,各能力归属模块负责最终业务决定;`ExternalSourceControlPlane` 不参与审批、偏好写入或产品状态派生;
-- 主要文件:
- - `src/crates/contracts/product-domains/src/external_source_control.rs`
- - `src/crates/assembly/core/src/external_sources.rs`
- - 现有 Tool、Subagent、MCP 审批与冲突测试。
-
-### 实施内容
-
-1. 定义 `GetApplicationReviewPage` 只读请求:
- - `execution_domain_id`、`target_scope` 与可选 `workspace_scope_id`;
- - `review_id`、cursor 和页面大小;服务端将页面大小限制为 128;
- - 响应只含同一偏好版本/发现代次的稳定 item reference 和脱敏显示摘要;stale cursor 要求从第一页重读;
- - 从当前不可变发现结果派生,不重新扫描文件、不启动能力,也不持有偏好写锁。
-2. 定义 `SubmitApplicationReview` 请求:
- - `execution_domain_id`、`target_scope`;仅 workspace override 携带 Host 快照返回的 `workspace_scope_id`;
- - `review_id`;
- - `operation_id`,仅用于请求/响应关联;
- - `expected_preference_revision`;
- - 相关 provider/owner generations;
- - `selection_baseline = recommended | none`;
- - 有界 `selection_overrides[]`,每项只含稳定项目引用和与基线不同的选择结果。
-3. 请求不携带命令正文、提示词、凭据值、任意执行载荷或整份确认项目。服务端用 `review_id` 查找同代不可变计划,先应用共享推荐或空集合基线,再应用改动项,并从计划取得能力类型、决策键、行为版本和归属模块代次。最终选择数量服从现有归属模块/协议上限,并由确认摘要返回 `max_selection_count`;改动项也不得超过该上限。
-4. 整批预检以下条件:
- - V2 schema/协议协商、Host identity 和 capability;
- - execution domain、workspace scope 与当前 Host 连接绑定;
- - preference revision;
- - review plan/generation;
- - application connection 状态;
- - Safe Mode 和 safety ceiling。
-5. 整批预检失败时不应用任何项;通过后按能力类型分派现有单项审批/冲突 owner。
-6. owner 可以逐项拒绝业务请求;响应必须返回每项 `applied / rejected / blocked / stale / failed` 等闭合结果及恢复动作,未知结果不得视为成功。
-7. 只持久化实际成功且仍与 decision key/behavior version 匹配的决定;返回与最终 preference revision 同代的新快照。
-
-这里的零应用保证止于分派前预检。分派开始后若某个 owner 的事实并发变化,响应可以同时包含已应用项与类型化 stale/failed 项;本阶段不增加跨 owner 事务或回滚管理器,也不宣称批量业务执行原子化。
-
-### 测试优先顺序
-
-- stale revision、generation 或 Host capability 导致整批零应用;
-- snapshot 只含 review summary;分页大小、总量上限、cursor 绑定和 stale 重读均按契约执行,翻页不触发重新发现;
-- 推荐项跨越多页且用户未读取后续页面时,`recommended` 基线仍选择同代完整推荐集合;已查看页面的改动项准确覆盖基线,不为提交强制拉取全部页面;
-- `none` 基线加选择改动项可以表达从空集合开始的选择;改动项越界、未知引用或来自另一 `review_id` 时整批拒绝;
-- 作用域身份不匹配或从另一 workspace scope/Host 重放导致整批零应用;
-- 两个不同 owner 的成功项共同提交;
-- 一个 owner 业务拒绝时另一个成功项的逐项结果准确;
-- 未知 item reference 和未知能力类型 fail closed;
-- safety ceiling 阻止宿主选择高于上限的项;
-- 高风险默认未选,但用户可在上限允许时显式选择;
-- 重放旧 review plan 不恢复旧权限;
-- 逐项结果与最终快照状态一致。
-
-### 验证
-
-```bash
-cargo test -p bitfun-core external_source
-cargo test -p bitfun-core external_tool
-cargo test -p bitfun-core external_subagent
-cargo test -p bitfun-core external_mcp
-cargo check --workspace
-```
-
-过滤器以实际测试模块为准,至少覆盖本次触及的所有 owner。
-
-### 用户可见结果
-
-用户可以在一个 review 页面确认推荐集合;无需连续处理 Tool、Subagent、MCP 和冲突弹窗,并能看到逐项真实结果。
-
-### 退出条件
-
-- 整批并发保护与逐项业务结果边界清楚;
-- 没有通用任意 payload API;
-- owner 仍是最终批准、注册和失败事实的权威;
-- 旧单项入口在迁移期间仍可工作,并与批量入口共享决定。
-
-### 暂停条件
-
-如果无法定义跨 owner 的原子回滚,不得宣称批量业务执行原子化;保留“整批预检原子、owner 逐项结果”的明确语义,并确保响应与快照可解释。
-
-## 7. P4:Desktop、Peer、App Server 与 Server 协议投影
-
-### 归属与范围
-
-- 归属:各应用/传输适配层与 `interfaces/app-server` 线协议适配层;
-- 主要文件:
- - `src/apps/desktop/src/api/external_sources_api.rs`
- - `src/apps/desktop/src/api/remote_workspace_policy.rs`
- - `src/apps/cli/src/peer_host/commands/external_sources.rs`
- - `src/crates/interfaces/app-server-protocol/src/external_sources.rs` 及 `method.rs`/`lib.rs` 注册
- - `src/crates/interfaces/app-server-client/src/lib.rs`
- - `src/crates/interfaces/app-server/src/server/handlers/external_sources.rs` 及 Runtime/domain-to-wire conversion
- - `src/apps/server/src/app_server.rs`、`src/apps/server/src/routes/external_sources.rs` 与 WebSocket round-trip tests
- - `src/web-ui/src/infrastructure/api/service-api/ExternalSourcesAPI.ts`
- - `src/web-ui/src/infrastructure/api/service-api/ExternalSourcesAPI.test.ts`。
-
-### 实施内容
-
-1. 保持现有 V1 DTO、Desktop/Peer endpoint、TypeScript union/allowlist 和 wire fixtures 不变;不得向 V1 action、recovery action 或 `hostCapabilities` 追加应用级字段。当前 Server `/ws` 经 `BitfunAppServer` 处理,仓库中的旧 `routes/external_sources.rs::dispatch` 已脱离生产路径并返回 `method_not_found`,不能把它当作“现有 Server adapter”。
-2. 先完成 P4a Server V1 只读前置切片:
- - 在 `app-server-protocol` 定义独立的 V1 snapshot/control-snapshot method、wire DTO 和错误;`AppServer`/`AppClient` role 保持 schema-free,不登记领域方法;
- - `app-server-client` 增加 typed request/response,`app-server` 只注册 handler、校验 wire contract 并转换 Runtime/domain 类型;handler 注入 `WorkspaceExternalSourceService` 的最窄只读 owner port,不持有第二份状态;
- - `interfaces/app-server-client` 与 TypeScript translation 保持 V1 wire shape,Server Host 绑定其真实 workspace,不读取浏览器或控制端路径;
- - Server 不注册 write handler;未知/写方法在反序列化 mutation payload 前以 method-not-found/host-capability-unavailable 拒绝;
- - 用真实 `/ws` transport 做 Server bootstrap → `BitfunAppServer::serve` → handler → owner → client 的端到端 round-trip。该切片通过前,Server 不进入 V2 共享 fixture,也不得标记为只读 external-source Host。
-3. P4a 后新增不提交用户决定或运行能力写动作的 `get_external_application_snapshot_v2`,直接作为版本探测:成功响应必须是严格 V2 数据结构,并携带宿主读写能力;首次 owner 激活可执行可重入迁移和既有后台发现,确认分页不得冷启动 owner。旧宿主的传输层 method-not-found 等价于“仅 V1”。不增加独立版本信息接口,也不引入“声明支持但接口不可用”的第二种状态。
-4. 客户端只有在 V2 snapshot 校验成功后,才调用 `get_external_application_review_page_v2` 或 `apply_external_application_action_v2`。V2 snapshot/action 不与 V1 对象混合序列化;read-only Server 只登记 snapshot/review read endpoint,不登记 mutation endpoint。
-5. Desktop Tauri command 只映射结构化 request/response,不派生状态、默认策略或推荐集合。
-6. 每个新增 Desktop command 在 remote workspace policy 中声明明确策略;Remote 未支持时返回 V2 类型化 unsupported,不回退本机。
-7. Peer Host 在事实所在 Host 执行相同 V2 typed action;Host 始终校验 `execution_domain_id`,并在 workspace override/上下文存在时校验快照返回的 `workspace_scope_id` 与连接绑定;控制端只原样回传 scope id,再用 Host identity、generation 和 accepted sequence 隔离响应。
-8. 旧 Peer/Host:
- - 新客户端回退显示 legacy V1 control/catalog,不把候选误报为应用级已连接;
- - V2 mutation 在客户端禁用;“升级 Host”是协商失败后的本地 UI 恢复建议,不发送给旧 Host;
- - 不由控制端模拟 mutation。
-9. TypeScript 为 V1/V2 使用独立 normalization;V2 严格检查 schema、作用域身份、generation、preference revision、Host capability 和 item reference,未知字段组合 fail closed。
-
-### 测试优先顺序
-
-- V1 Rust/TypeScript golden fixtures 在新 Host/客户端中保持完全一致;
-- old client → new Host 继续只使用 V1;new client → old Host 经 method-not-found 明确回退 V1 且没有 V2 mutation;
-- V2 Rust/TypeScript 序列化字段一致;V2 snapshot 成功、method-not-found 回退和未知 schema 拒绝均有契约测试;
-- App Server V1 read-only 方法在真实 Server `/ws` 往返成功,且 wire fixture 与 Desktop/Peer V1 一致;
-- control、catalog 和 application snapshot 同代;
-- read-only Host 未注册 mutation endpoint,并在 mutation payload 解析前拒绝;
-- Remote 不回退本机;
-- 旧 Host 降级不会把候选误报为已连接或已启用,也不会收到未知 V2 action/recovery enum;
-- Host identity、execution domain 或 workspace scope 不匹配时拒绝响应/结果;
-- accepted sequence 防止旧响应覆盖新连接决定;
-- 未知状态、动作、逐项结果和恢复动作安全失败。
-
-### 验证
-
-```bash
-cargo check -p bitfun-desktop
-cargo test -p bitfun-app-server
-cargo test -p bitfun-server external_source
-pnpm --dir src/web-ui run test:run src/infrastructure/api/service-api/ExternalSourcesAPI.test.ts
-pnpm run type-check:web
-```
-
-同时运行 Desktop/Peer/Server 中与 external source command 直接对应的 focused tests。
-
-### 用户可见结果
-
-本机 Desktop、Peer 控制界面和只读 Host 对相同应用事实给出一致状态;不支持的宿主明确说明升级、重连或切换 Host。
-
-### 退出条件
-
-- adapter 无生态业务分支;
-- 新 Desktop commands 全部具备 remote workspace policy;
-- TypeScript 对未知、未协商或不同作用域/代快照 fail closed;
-- V1 wire contract 冻结,V2 只在独立 endpoint 协商后启用;
-- Server V1 read-only App Server 前置切片有真实 WebSocket round-trip,不能由 dead dispatch 单元测试替代;
-- 双向新旧 Host/客户端组合有契约测试,旧 Host 降级不产生执行位置 fallback。
-
-## 8. P5:Desktop Web UI 信息架构
-
-### 归属与范围
-
-- 归属:Web UI Settings;
-- 主要文件:
- - `src/web-ui/src/infrastructure/config/components/ExternalSourcesConfig.tsx`
- - `src/web-ui/src/infrastructure/config/components/ExternalSourcesConfig.scss`
- - `src/web-ui/src/infrastructure/config/components/ExternalSourcesConfig.test.tsx`
- - 同目录新增的聚焦组件与测试
- - `src/web-ui/src/infrastructure/config/components/common/config-page-layout.tokens.scss`
- - 外部来源设置页现有 i18n namespace。
-
-### 实施内容
-
-1. 保留 `ExternalSourcesConfig` 作为页面 controller,继续负责读取、轮询、mutation sequencing、accepted sequence、pending mutation、scope mutation 栅栏和错误恢复。
-2. 按责任拆分:
- - `ExternalAppsOverview`;
- - `ExternalAttentionSummary`;
- - `ExternalAppDetail`;
- - `ExternalAppReview`;
- - `ExternalAdvancedSettings`;
- - 无策略判断的 presentation helpers。
-3. 首页使用现有 `ConfigPageLayout` 的 760px 单列阅读轴:标题、应用列表、高级设置。真实的任务相关待办通过就地提示或状态变化处理,不把无法归属的系统诊断聚合成首页数量。
-4. 每个应用行只显示应用名、一个状态、一句结果摘要和唯一主操作;有工作区时主操作明确标注“仅当前工作区”,没有工作区时先进入详情选择范围。来源路径、能力清单、冲突和诊断进入详情。
-5. 详情按“结果优先、控制后置”排列;连接完成显示生效范围、已启用、待确认和受限摘要。`user_default` 只在详情/高级设置中提供,并在提交前再次展示会影响同一执行域的所有工作区。
-6. 批量确认页面先使用快照摘要,再按需分页读取项目引用;默认只显示确认数量和“使用推荐/暂不启用”两个决定,单项名称、风险和安全上限放在折叠的调整区,不展示内部错误码、处理阶段或任意载荷。高风险默认未选。提交使用同代推荐/空集合基线和用户改动项,不为提交强制读取全部页面;首页轮询不读取项目页面。
-7. Safe Mode 在首页和详情显著展示,高级设置保留现有 source、scope、冲突、诊断和能力级管理。
-8. 首次发现只使用一次性轻提示和 Settings 导航状态;不增加启动 Modal 或常驻 banner。
-9. 所有文案进入现有 i18n namespace,颜色与状态复用主题 token,不提高主题治理基线。
-
-### 测试优先顺序
-
-- 五种应用状态和唯一主操作;
-- “需要处理”仅在真实待办时出现;
-- OpenCode 默认连接结果与 Codex/Claude 主动连接路径;
-- 连接完成摘要;
-- 当前工作区主操作不会改写 `user_default`;无工作区时不会直接执行全局连接;全局连接必须明确选择并二次确认范围;
-- 批量默认选择严格等于共享推荐;跨页未读取项由同代推荐基线表达,已修改项只作为覆盖提交;
-- 首页请求不携带 review items;打开/翻页才读取 bounded page,stale page 会整体刷新而不是混合显示;
-- stale response/mutation 不覆盖新状态;
-- review 整体失败和逐项失败;
-- 断开与暂不使用;
-- Safe Mode 显著状态;
-- 旧 Host/read-only/Remote 降级;
-- 键盘焦点、展开、批量选择和状态非颜色表达;
-- 现有审批、冲突、诊断、scope 和脱敏回归保持通过。
-
-### 验证
-
-```bash
-pnpm --dir src/web-ui run test:run src/infrastructure/config/components/ExternalSourcesConfig.test.tsx src/infrastructure/api/service-api/ExternalSourcesAPI.test.ts
-pnpm run type-check:web
-pnpm run i18n:audit
-pnpm run theme:color-audit:all
-```
-
-若组件拆分出独立测试文件,将这些文件加入同一次 focused test 命令。
-
-### 用户可见结果
-
-Settings 以应用为主入口,采用纵向单列;用户先看到连接结果和唯一下一步,高级能力管理仍可访问但不占据首页。
-
-### 退出条件
-
-- 首页不再平铺 Tool、Subagent、MCP、来源和诊断;
-- controller 的竞态保护有回归测试;
-- UI 不包含 OpenCode/Codex/Claude 默认策略分支;
-- 现有高级操作没有被隐藏为不可达;
-- type-check、focused tests、i18n 和主题治理通过。
-
-### 暂停条件
-
-若拆分组件需要重写现有 controller 并改变 mutation 顺序,先保留 controller,仅提取纯展示组件;不得以视觉改版为由同时重构请求状态机。
-
-## 9. P6:TUI `/extensions` 与非交互 CLI
-
-### 归属与范围
-
-- 归属:能力归属模块产生阻塞事实,Agent Runtime 拥有根任务结果与父子关系;App Server/Shared IPC 只传输,`src/apps/cli` 只投影交互和退出结果;
-- 主要文件:
- - `src/crates/contracts/events/src/agentic.rs`
- - `src/crates/contracts/runtime-ports/src/lib.rs`
- - `src/crates/assembly/core/src/agentic/coordination/coordinator.rs` 及真实外部能力解析/调用 owner
- - `src/crates/interfaces/app-server-protocol/src/tui.rs`、`src/crates/interfaces/app-server-protocol/src/event.rs`、`src/crates/interfaces/app-server-client/src/lib.rs` 与 event round-trip tests
- - `src/crates/interfaces/app-server/src/server/event_forwarder.rs` 及 handler/conversion tests
- - `src/crates/adapters/agent-runtime-ipc/src/protocol.rs` 及 Shared Runtime client/server tests
- - `src/apps/cli/src/modes/chat/external_review.rs`
- - `src/apps/cli/src/modes/chat/external_hooks.rs`
- - `src/apps/cli/src/modes/chat/external_sources.rs`
- - `src/apps/cli/src/actions.rs`
- - `src/apps/cli/src/peer_host/commands/external_sources.rs`
- - `src/apps/cli/src/modes/exec/lifecycle.rs`
- - 对应 parser、action registry、snapshot、事件和输出测试。
-
-### 实施内容
-
-1. `/extensions` 使用共享应用级快照展示应用、状态、数量、默认策略、主操作和 Safe Mode。
-2. 增加连接、断开、暂不使用和详情动作;默认命令作用于当前工作区并在输出中显示范围,全执行域默认必须使用明确参数/确认路径。parser、help、palette/action registry 与 dispatch 从同一 action 定义保持一致。
-3. `/extensions review` 使用共享 review summary,并按需读取有界 item page:
- - 默认采用推荐集合;
- - 高风险默认不选;
- - 支持查看技术详情和调整;
- - 用同代推荐/空集合基线和有界改动项提交同一类型化批量动作,不强制读取全部页面;
- - 逐项展示权威结果。
-4. `/tools`、`/agent`、`/mcp` 和 `/hooks` 保留专项/高级管理,不复制首次连接向导。
-5. 删除仅进程内有效的重复提示判断,改为读取共享 notice/user decision facts;首次发现不阻塞聊天。
-6. 复用现有提交身份,不新增任务 ID:`AgentSubmissionResult` 仍只返回 accepted/turn ID;根 `session_id + turn_id` 唯一标识本次任务,子代理来源由现有 `SubagentSessionLinked` 追溯。
-7. 能力 owner 在真实解析或调用路径因未连接、待批量确认或权限扩大而阻止一个被请求的外部依赖时,返回类型化依赖事实。Agent Runtime 用 turn-local collector 聚合并发布新的 `AgenticEvent::ExternalDependencyActionRequired`,事件至少包含:
- - `execution_domain_id` 与可选、非通配的 `workspace_scope_id`;
- - 根 `session_id + turn_id`;
- - `origin_session_id + origin_turn_id + origin_tool_call_id?`;
- - 依赖引用、风险摘要、`can_degrade` 与闭合恢复动作。
-8. Runtime 使用现有 `SubagentSessionLinked(parent_session_id, parent_dialog_turn_id, parent_tool_call_id)` 递归追溯子代理来源。只有根 turn 仍在等待来源 tool call 时,子代理阻断事实才聚合给根;无关、后台或已脱离等待链的子代理结果不改变根任务。聚合事件必须在对应根任务结束事件前发出;并发根 turn 之间不共享 collector。
-9. 通过已有 Agent 事件路径端到端传输,而不是新增 CLI 私有旁路:
- - `bitfun-events` 拥有事件 wire contract;应用级 product-domain DTO 只提供稳定 dependency reference,不拥有任务结果;
- - App Server 继续通过 `agent/event` 的 `AgenticEventEnvelope` 转发,但新闭合事件是 wire 扩展:提升 `app-server-protocol::PROTOCOL_VERSION`,按每连接协商版本过滤 `ExternalDependencyActionRequired`。旧协议连接继续接收其已知事件但绝不能收到新 variant;若实现无法可靠逐连接过滤,就必须同步提升 `MIN_PROTOCOL_VERSION` 并在 initialize 时拒绝旧客户端,不能让其在事件流中反序列化失败;
- - `app-server-client` 只有在 Host 协商到新增版本后才解释该事件;新客户端连接旧 App Server Host 时明确报告“任务依赖结果不支持”,不从结束文本猜测;
- - Shared Runtime 继续通过 `RuntimeIpcEvent::Agent` 转发。由于 IPC 是严格版本协议,新增事件时同步提升 `PROTOCOL_VERSION`,旧 client/server 在握手失败后明确降级,不能混读;
- - Peer/Remote fanout 必须保留根任务和来源身份,不得重写为控制端 workspace。
-10. 非交互 CLI 只缓存与当前 `execution_domain_id + workspace_scope_id? + root session + root turn` 全部匹配的事件;不可降级的事件在根任务结束后投影为类型化 `action-required`,可降级事件保留为结构化警告并沿用真实结束结果。不得从轮询应用快照、任意子代理事件或错误文本推断退出状态。
-11. stdout/stderr 与现有结构化输出契约保持不变;不得把交互式选择提示写入非交互 stdout。
-
-### 测试优先顺序
-
-- `/extensions` parser、help、palette 和 dispatch 一致;
-- GUI/TUI 对同一 fixture 的状态、默认策略、数量和主操作一致;
-- GUI/TUI 默认连接或断开只改变当前 workspace scope;全执行域操作必须明确选择,结果摘要显示最终生效范围;
-- `/extensions review` 默认选择与共享推荐一致,跨页未访问项与用户改动项的结果和 GUI 相同;
-- stale review 重新读取,不重放旧决定;
-- 首次提示跨进程去重;
-- 无关待办不阻塞聊天或非交互任务;
-- 根 turn 直接命中 pending capability 时,在 terminal event 前收到匹配的 `ExternalDependencyActionRequired` 并返回 `action-required`;
-- 通过 `SubagentSessionLinked` 证明依赖的 child blocking fact 聚合到根;无关/后台 child、错误 parent tool-call 或已断开的依赖边不影响根;
-- 两个并发根 turn 的事件不串扰,来自另一 execution domain、workspace scope、session 或 turn 的事件被拒绝;
-- App Server Embedded 与 Shared Runtime IPC 对同一事件 fixture 的字段、顺序和 terminal 结果等价;Shared 新旧协议版本在握手处 fail closed;
-- new App Server Host → protocol v2/v3 client 不发送未知 outcome variant;新版本 client → old Host 不提交/不期待该能力;协商新版本时完整 round-trip;
-- Peer/Remote 转发保留 root/origin identity 且不回退控制端 workspace;
-- read-only/Remote/旧 Host 输出明确恢复动作;
-- `/tools`、`/agent`、`/mcp`、`/hooks` 原有职责和兼容别名保持通过。
-
-### 验证
-
-```bash
-cargo test -p bitfun-cli external
-cargo test -p bitfun-cli action
-cargo test -p bitfun-events external_dependency
-cargo test -p bitfun-app-server agent_event
-cargo test -p bitfun-agent-runtime-ipc agent_event
-cargo check -p bitfun-cli
-```
-
-同时运行 action registry 和相关 slash command 的现有 focused tests。
-
-### 用户可见结果
-
-TUI 与 Desktop 共享“发现—连接—加载”的心智和决定;CLI 用户通过 `/extensions` 完成首次连接和批量确认,能力专项入口继续可用。
-
-### 退出条件
-
-- GUI/TUI golden fixture 一致;
-- 交互提示不阻塞普通输入;
-- 非交互只对当前 execution domain、workspace scope、根 session/turn 的不可降级任务相关待办返回 `action-required`;
-- direct root、linked subagent、unrelated/background subagent、并发 roots、Embedded/Shared 和 Peer/Remote 路径都有端到端事件证据;
-- TUI 无生态默认策略分支,且不共享 GUI 布局或组件 schema。
-
-## 10. P7:跨宿主回归、迁移与清理
-
-### 归属与范围
-
-- 归属:Product Assembly、Desktop、Web UI、CLI 共同完成;
-- 范围:共享 fixtures、i18n、主题、旧投影退场和文档同步。
-
-### 实施内容
-
-1. 建立同一组跨宿主 fixture,至少覆盖:
- - 首次发现并默认连接 OpenCode;
- - 首次发现但不连接 Codex/Claude Code;
- - 多应用并存;
- - 已连接且部分待确认;
- - 权限扩大;
- - 连接失败、沿用上一版本和 Host 不支持;
- - Safe Mode;
- - stale revision/generation;
- - 断开后重新发现;
- - 当前任务相关与无关待办;
- - user default 与 workspace override;
- - 本机、Peer、Remote execution domain 隔离;
- - old client/new Host、new client/old Host;
- - fresh V2 无文件、legacy 自动物化默认文件/显式 false、disabled/discover-only、已有效使用、无法归属、多个 workspace scope 原子转换和 future-major incompatible policy。
-2. 对比 Rust read model、TypeScript normalization、Desktop 展示和 TUI 文本中的状态、默认策略、数量、主操作及恢复动作。
-3. 验证 P2 的原位旧偏好迁移和切换:
- - 全部 scope 决定与 `connection_schema_migration_version` 一次原子提交,崩溃/写失败保持完整旧文件,不存在部分完成状态;
- - legacy 自动物化默认文件、显式 false 和 disabled/discover-only 均不被新默认覆盖;只有明确 `fresh_v2` 无文件初始化可应用 OpenCode 默认;已有效使用的 Claude Code/Codex/OpenCode 能力、审批和冲突决定不因升级静默撤下;
- - future-major incompatible policy byte-for-byte 保留包含 opaque policy 的原文件,拒绝迁移、默认连接、MCP secret 自动写入和 V2 mutation;备份并重置后写入 `incompatible_reset` 并保持显式未连接,直到用户主动连接;
- - 无法归属的旧状态继续留在 legacy V1 路径并要求审阅,直到有明确迁移决定;
- - Instruction、Skill、Hook 和复制后的原生配置按各自 owner 验证,不被应用连接误删。
-4. 迁移旧入口:
- - 保留能力专项操作;
- - 删除 React/TUI 中重复的状态优先级、默认连接和提示去重逻辑;
- - 只有所有生产宿主切换且回归通过后,才删除不再消费的 legacy 聚合字段或 action;
- - 只要仍有旧 Host/客户端,V1 wire contract 和 endpoint 就继续保留;V2 不复用或扩展 V1 闭合枚举;Server 只有在 App Server V1 read-only round-trip 交付后才计入生产宿主。
-5. 更新架构、详细设计、CLI 架构和实现状态;不能把目标能力写成已交付。
-6. 记录首页快照和确认分页的序列化大小、聚焦读取延迟及前后对比;读取不得重新扫描、启动外部能力或持有偏好写锁。明显回退必须先减少返回数据或重复计算,不能无基线地增加缓存。
-7. 复核远端策略、日志脱敏、i18n、主题、仓库卫生和未跟踪生成文件。
-
-### 综合验证
-
-```bash
-pnpm run fmt:rs
-cargo check --workspace
-cargo test -p bitfun-core external_source
-cargo test -p bitfun-cli external
-cargo check -p bitfun-desktop
-pnpm --dir src/web-ui run test:run src/infrastructure/config/components/ExternalSourcesConfig.test.tsx src/infrastructure/api/service-api/ExternalSourcesAPI.test.ts
-pnpm run type-check:web
-pnpm run i18n:contract:test
-pnpm run i18n:audit
-pnpm run theme:color-audit:all
-pnpm run check:repo-hygiene
-```
-
-仅在实际触及对应范围时运行 i18n contract 或全主题审计;Rust 和 Web UI 的最小必需检查仍按仓库根 `AGENTS.md` 执行。
-
-### 退出条件
-
-- Desktop、TUI、Peer 与已完成 App Server 前置切片的 Server 对共享 fixture 的应用事实一致;
-- 连接、批量确认、断开、提示去重和任务相关 `action-required` 均有端到端证据;
-- 现有 Safe Mode、能力审批、冲突、诊断、脱敏和竞态测试保持通过;
-- 无宿主按生态 ID 重算产品事实;
-- 未连接应用不加载能力、不参与运行时冲突;
-- Remote/read-only 不回退本机;
-- legacy 升级不改变显式策略、已有效使用的能力、审批或冲突决定,失败可重试且不产生半迁移;
-- 连接、提示和确认按执行域与工作区作用域隔离;任务结果按执行域、工作区作用域、根会话和根轮次隔离,并沿用子代理来源关系;
-- 首页快照与确认分页保持有界,且性能对比没有未解释的明显回退;
-- V1 wire fixtures 不变,V2 协商及双向新旧组合通过;
-- 旧字段和逻辑只在确认无生产消费方后删除;
-- 文档明确区分当前能力与目标状态。
-
-## 11. 提交与评审边界
-
-建议按 P1-P7 分为独立提交或 PR;P5 与 P6 可以在 P4 后并行。每个提交必须:
-
-1. 包含自己的失败测试、实现和最小验证;
-2. 说明修改了哪个稳定 contract/owner,是否影响旧 Host;
-3. 不混入新的生态能力解析、OpenCode package runtime、聊天历史迁移或显式配置导入;
-4. 不提高 i18n/theme 治理基线来掩盖新增债务;
-5. 不删除与旧消费方仍有关联的公共 V1 符号;
-6. 在评审描述中列出实际执行的 focused commands 和剩余由 CI 覆盖的范围。
-
-出现以下任一情况应停止当前阶段并回到架构评审:
-
-- 需要让 UI/TUI 解析生态原始 payload;
-- 需要新增跨能力任意执行 DTO;
-- 需要通过本地 fallback 掩盖 Remote/Host 不支持;
-- 需要绕过 owner 才能批量批准或卸载;
-- 需要为连接体验建立第二套偏好、权限、冲突或 watcher 系统;
-- 无法在不改变现有能力运行语义的情况下实现应用聚合。
diff --git a/examples/example-pipeline.yaml b/examples/example-pipeline.yaml
new file mode 100644
index 0000000000..ae99276e3b
--- /dev/null
+++ b/examples/example-pipeline.yaml
@@ -0,0 +1,19 @@
+name: "example-ma-cross"
+version: "1.0"
+bar_gen:
+ modes:
+ - "time"
+ time_freqs:
+ - "1m"
+data_source:
+ type: "csv_replay"
+ config:
+ csv_path: "test_data/golden_tick/20260721/a2609/a2609_golden_20260721.csv"
+nodes:
+ - id: "ma_cross"
+ type: "ma_cross"
+ config:
+ fast_period: 5
+ slow_period: 20
+ input_keys: []
+ output_keys: ["out1"]
diff --git a/package.json b/package.json
index d37c05c7e5..336766b291 100644
--- a/package.json
+++ b/package.json
@@ -90,6 +90,8 @@
"installer:build:only": "pnpm --dir BitFun-Installer run installer:build:only",
"installer:build:only:fast": "pnpm --dir BitFun-Installer run installer:build:only:fast",
"installer:dev": "pnpm --dir BitFun-Installer run installer:dev",
+ "package:windows:assets": "node scripts/package-windows-assets.mjs",
+ "package:windows:test": "node --test scripts/package-windows-assets.test.mjs",
"cli:dev": "node scripts/cli-product.mjs dev",
"cli:build": "node scripts/cli-product.mjs build",
"cli:install": "node scripts/install-cli.mjs",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 769d91a40a..8d4bf8f1e0 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -960,89 +960,105 @@ packages:
resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==}
cpu: [arm64]
os: [linux]
+ libc: [glibc]
'@img/sharp-libvips-linux-arm@1.2.4':
resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==}
cpu: [arm]
os: [linux]
+ libc: [glibc]
'@img/sharp-libvips-linux-ppc64@1.2.4':
resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==}
cpu: [ppc64]
os: [linux]
+ libc: [glibc]
'@img/sharp-libvips-linux-riscv64@1.2.4':
resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==}
cpu: [riscv64]
os: [linux]
+ libc: [glibc]
'@img/sharp-libvips-linux-s390x@1.2.4':
resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==}
cpu: [s390x]
os: [linux]
+ libc: [glibc]
'@img/sharp-libvips-linux-x64@1.2.4':
resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==}
cpu: [x64]
os: [linux]
+ libc: [glibc]
'@img/sharp-libvips-linuxmusl-arm64@1.2.4':
resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==}
cpu: [arm64]
os: [linux]
+ libc: [musl]
'@img/sharp-libvips-linuxmusl-x64@1.2.4':
resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==}
cpu: [x64]
os: [linux]
+ libc: [musl]
'@img/sharp-linux-arm64@0.34.5':
resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [linux]
+ libc: [glibc]
'@img/sharp-linux-arm@0.34.5':
resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm]
os: [linux]
+ libc: [glibc]
'@img/sharp-linux-ppc64@0.34.5':
resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [ppc64]
os: [linux]
+ libc: [glibc]
'@img/sharp-linux-riscv64@0.34.5':
resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [riscv64]
os: [linux]
+ libc: [glibc]
'@img/sharp-linux-s390x@0.34.5':
resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [s390x]
os: [linux]
+ libc: [glibc]
'@img/sharp-linux-x64@0.34.5':
resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [linux]
+ libc: [glibc]
'@img/sharp-linuxmusl-arm64@0.34.5':
resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [linux]
+ libc: [musl]
'@img/sharp-linuxmusl-x64@0.34.5':
resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [linux]
+ libc: [musl]
'@img/sharp-wasm32@0.34.5':
resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==}
@@ -1333,36 +1349,42 @@ packages:
engines: {node: '>= 10.0.0'}
cpu: [arm]
os: [linux]
+ libc: [glibc]
'@parcel/watcher-linux-arm-musl@2.5.6':
resolution: {integrity: sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==}
engines: {node: '>= 10.0.0'}
cpu: [arm]
os: [linux]
+ libc: [musl]
'@parcel/watcher-linux-arm64-glibc@2.5.6':
resolution: {integrity: sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==}
engines: {node: '>= 10.0.0'}
cpu: [arm64]
os: [linux]
+ libc: [glibc]
'@parcel/watcher-linux-arm64-musl@2.5.6':
resolution: {integrity: sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==}
engines: {node: '>= 10.0.0'}
cpu: [arm64]
os: [linux]
+ libc: [musl]
'@parcel/watcher-linux-x64-glibc@2.5.6':
resolution: {integrity: sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==}
engines: {node: '>= 10.0.0'}
cpu: [x64]
os: [linux]
+ libc: [glibc]
'@parcel/watcher-linux-x64-musl@2.5.6':
resolution: {integrity: sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==}
engines: {node: '>= 10.0.0'}
cpu: [x64]
os: [linux]
+ libc: [musl]
'@parcel/watcher-win32-arm64@2.5.6':
resolution: {integrity: sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==}
@@ -1459,66 +1481,79 @@ packages:
resolution: {integrity: sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==}
cpu: [arm]
os: [linux]
+ libc: [glibc]
'@rollup/rollup-linux-arm-musleabihf@4.57.1':
resolution: {integrity: sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==}
cpu: [arm]
os: [linux]
+ libc: [musl]
'@rollup/rollup-linux-arm64-gnu@4.57.1':
resolution: {integrity: sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==}
cpu: [arm64]
os: [linux]
+ libc: [glibc]
'@rollup/rollup-linux-arm64-musl@4.57.1':
resolution: {integrity: sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==}
cpu: [arm64]
os: [linux]
+ libc: [musl]
'@rollup/rollup-linux-loong64-gnu@4.57.1':
resolution: {integrity: sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==}
cpu: [loong64]
os: [linux]
+ libc: [glibc]
'@rollup/rollup-linux-loong64-musl@4.57.1':
resolution: {integrity: sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==}
cpu: [loong64]
os: [linux]
+ libc: [musl]
'@rollup/rollup-linux-ppc64-gnu@4.57.1':
resolution: {integrity: sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==}
cpu: [ppc64]
os: [linux]
+ libc: [glibc]
'@rollup/rollup-linux-ppc64-musl@4.57.1':
resolution: {integrity: sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==}
cpu: [ppc64]
os: [linux]
+ libc: [musl]
'@rollup/rollup-linux-riscv64-gnu@4.57.1':
resolution: {integrity: sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==}
cpu: [riscv64]
os: [linux]
+ libc: [glibc]
'@rollup/rollup-linux-riscv64-musl@4.57.1':
resolution: {integrity: sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==}
cpu: [riscv64]
os: [linux]
+ libc: [musl]
'@rollup/rollup-linux-s390x-gnu@4.57.1':
resolution: {integrity: sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==}
cpu: [s390x]
os: [linux]
+ libc: [glibc]
'@rollup/rollup-linux-x64-gnu@4.57.1':
resolution: {integrity: sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==}
cpu: [x64]
os: [linux]
+ libc: [glibc]
'@rollup/rollup-linux-x64-musl@4.57.1':
resolution: {integrity: sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==}
cpu: [x64]
os: [linux]
+ libc: [musl]
'@rollup/rollup-openbsd-x64@4.57.1':
resolution: {integrity: sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==}
@@ -1598,30 +1633,35 @@ packages:
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
+ libc: [glibc]
'@tauri-apps/cli-linux-arm64-musl@2.10.0':
resolution: {integrity: sha512-GUoPdVJmrJRIXFfW3Rkt+eGK9ygOdyISACZfC/bCSfOnGt8kNdQIQr5WRH9QUaTVFIwxMlQyV3m+yXYP+xhSVA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
+ libc: [musl]
'@tauri-apps/cli-linux-riscv64-gnu@2.10.0':
resolution: {integrity: sha512-JO7s3TlSxshwsoKNCDkyvsx5gw2QAs/Y2GbR5UE2d5kkU138ATKoPOtxn8G1fFT1aDW4LH0rYAAfBpGkDyJJnw==}
engines: {node: '>= 10'}
cpu: [riscv64]
os: [linux]
+ libc: [glibc]
'@tauri-apps/cli-linux-x64-gnu@2.10.0':
resolution: {integrity: sha512-Uvh4SUUp4A6DVRSMWjelww0GnZI3PlVy7VS+DRF5napKuIehVjGl9XD0uKoCoxwAQBLctvipyEK+pDXpJeoHng==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
+ libc: [glibc]
'@tauri-apps/cli-linux-x64-musl@2.10.0':
resolution: {integrity: sha512-AP0KRK6bJuTpQ8kMNWvhIpKUkQJfcPFeba7QshOQZjJ8wOS6emwTN4K5g/d3AbCMo0RRdnZWwu67MlmtJyxC1Q==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
+ libc: [musl]
'@tauri-apps/cli-win32-arm64-msvc@2.10.0':
resolution: {integrity: sha512-97DXVU3dJystrq7W41IX+82JEorLNY+3+ECYxvXWqkq7DBN6FsA08x/EFGE8N/b0LTOui9X2dvpGGoeZKKV08g==}
@@ -3453,12 +3493,12 @@ packages:
glob@7.2.3:
resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
- deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
+ deprecated: Glob versions prior to v9 are no longer supported
glob@8.1.0:
resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==}
engines: {node: '>=12'}
- deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
+ deprecated: Glob versions prior to v9 are no longer supported
globals@14.0.0:
resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==}
@@ -5290,7 +5330,6 @@ packages:
uuid@10.0.0:
resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==}
- deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).
hasBin: true
uuid@11.1.0:
diff --git a/scripts/cargo-target-gc.mjs b/scripts/cargo-target-gc.mjs
index 8494cf597b..e39e4813a3 100644
--- a/scripts/cargo-target-gc.mjs
+++ b/scripts/cargo-target-gc.mjs
@@ -314,12 +314,20 @@ function sleepMs(ms) {
export function isCompilerBusy({ exec = execFileSync, platform = process.platform } = {}) {
try {
if (platform === 'win32') {
- const out = exec(
- 'cmd.exe',
- ['/d', '/s', '/c', 'tasklist /FI "IMAGENAME eq cargo.exe" & tasklist /FI "IMAGENAME eq rustc.exe"'],
+ // Pass each /FI filter as a single argument. Routing the whole command
+ // through cmd.exe /c re-splits the quoted filter, so tasklist receives
+ // `eq` as a standalone option and fails with `无效参数/选项 - 'eq'`.
+ const cargo = exec(
+ 'tasklist',
+ ['/FI', 'IMAGENAME eq cargo.exe', '/NH'],
{ encoding: 'utf8' }
);
- return /\bcargo\.exe\b/i.test(out) || /\brustc\.exe\b/i.test(out);
+ const rustc = exec(
+ 'tasklist',
+ ['/FI', 'IMAGENAME eq rustc.exe', '/NH'],
+ { encoding: 'utf8' }
+ );
+ return /\bcargo\.exe\b/i.test(cargo) || /\brustc\.exe\b/i.test(rustc);
}
const cargo = exec('pgrep', ['-x', 'cargo'], { encoding: 'utf8' }).trim();
if (cargo) {
diff --git a/scripts/core-boundaries/rules/feature-rules.mjs b/scripts/core-boundaries/rules/feature-rules.mjs
index 1e0cdc078d..401314a815 100644
--- a/scripts/core-boundaries/rules/feature-rules.mjs
+++ b/scripts/core-boundaries/rules/feature-rules.mjs
@@ -107,6 +107,7 @@ export const optionalDependencyFeatureOwnerRules = [
{ depName: 'include_dir', ownerFeatures: ['agent-runtime'] },
{ depName: 'indexmap', ownerFeatures: ['agent-runtime'] },
{ depName: 'md5', ownerFeatures: ['agent-runtime'] },
+ { depName: 'rand', ownerFeatures: ['agent-runtime'] },
{ depName: 'reqwest', ownerFeatures: ['ai-adapter-runtime', 'agent-runtime'] },
{ depName: 'rusqlite', ownerFeatures: ['agent-runtime'] },
{ depName: 'semver', ownerFeatures: ['agent-runtime'] },
@@ -254,6 +255,7 @@ export const coreClosedFeatureProfileRules = [
'dep:indexmap',
'dep:image',
'dep:md5',
+ 'dep:rand',
'dep:reqwest',
'dep:semver',
'dep:rusqlite',
diff --git a/scripts/core-boundaries/rules/source/public-api-rules.mjs b/scripts/core-boundaries/rules/source/public-api-rules.mjs
index ad4378da94..1fe145be9a 100644
--- a/scripts/core-boundaries/rules/source/public-api-rules.mjs
+++ b/scripts/core-boundaries/rules/source/public-api-rules.mjs
@@ -829,8 +829,6 @@ export const externalSourceContractPublicApiEntries = [
export const externalSourceControlPublicApiEntries = [
'EXTERNAL_SOURCE_CONTROL_SCHEMA_V1',
- 'EXTERNAL_APPLICATION_SCHEMA_V2',
- 'EXTERNAL_APPLICATION_REVIEW_PAGE_MAX_ITEMS',
'ExternalSourceOperationStage',
'ExternalSourceRecoveryActionV1',
'ExternalSourceDiscoveryState',
@@ -846,39 +844,6 @@ export const externalSourceControlPublicApiEntries = [
'ExternalSourceSurfaceSnapshotV1',
'ExternalSourceControlActionV1',
'ExternalSourceControlRequestV1',
- 'ExternalApplicationTargetScopeV2',
- 'ExternalApplicationDesiredConnectionV2',
- 'ExternalApplicationUserDecisionV2',
- 'ExternalApplicationDiscoveryStateV2',
- 'ExternalApplicationConnectionStateV2',
- 'ExternalApplicationHealthV2',
- 'ExternalApplicationEffectiveStatusV2',
- 'ExternalApplicationPrimaryActionV2',
- 'derive_external_application_status_v2',
- 'ExternalApplicationDefaultConnectionPolicyV2',
- 'ExternalApplicationRiskLevelV2',
- 'ExternalApplicationSafetyCeilingV2',
- 'ExternalApplicationRecoveryActionV2',
- 'ExternalApplicationHostCapabilitiesV2',
- 'ExternalApplicationRiskSummaryV2',
- 'ExternalApplicationReviewItemKindV2',
- 'ExternalApplicationReviewItemRefV2',
- 'ExternalApplicationOwnerGenerationV2',
- 'ExternalApplicationReviewCategoryCountV2',
- 'ExternalApplicationReviewRecommendationSummaryV2',
- 'ExternalApplicationReviewSummaryV2',
- 'ExternalApplicationSummaryV2',
- 'ExternalApplicationSnapshotV2',
- 'ExternalApplicationReviewItemV2',
- 'ExternalApplicationReviewPageRequestV2',
- 'ExternalApplicationReviewPageV2',
- 'ExternalApplicationReviewSelectionBaselineV2',
- 'ExternalApplicationReviewSelectionOverrideV2',
- 'ExternalApplicationControlActionV2',
- 'ExternalApplicationControlRequestV2',
- 'ExternalApplicationOperationOutcomeV2',
- 'ExternalApplicationReviewItemResultV2',
- 'ExternalApplicationControlResultV2',
].map((symbol) =>
externalSourceControlEntry(
symbol,
@@ -1020,9 +985,6 @@ export const externalSourceCorePublicApiEntries = [
'EXTERNAL_SOURCE_CONTROL_SCHEMA_V1',
'get_external_source_control_snapshot',
'apply_external_source_control_action',
- 'get_external_application_snapshot_v2',
- 'get_external_application_review_page_v2',
- 'apply_external_application_action_v2',
].map((symbol) =>
externalSourceControlEntry(
symbol,
@@ -1120,16 +1082,16 @@ export const externalSourceCorePublicApiEntries = [
].map((symbol) => ({
symbol,
owner: 'bitfun-core external source composition facade',
- consumer: 'Desktop settings navigation and CLI/TUI external application entry points',
+ consumer: 'Desktop external-source host adapter and Web settings navigation',
verification:
- 'core acknowledgement persistence and execution-domain scoping tests, plus Desktop and TUI first-discovery hint tests',
- p0: 'first-discovery hint for external applications shared by GUI and TUI',
+ 'core acknowledgement persistence and execution-domain scoping tests, Desktop command contract tests, and Web settings awareness tests',
+ p0: 'first-discovery notification for external-source settings',
contractSlice: contractSlices.externalSourceCommandContract,
wireImpact: true,
rationale:
- 'both surfaces must derive "an external application the user has not seen" from one owner, otherwise GUI and TUI drift; awareness stays outside the preference-revision contract because it grants nothing and only suppresses a hint',
+ 'the Web settings notification must remain stable across refreshes and workspace changes without treating acknowledgement as permission or policy',
exit:
- 'remove once the versioned application-level read model owns notice state, together with its cross-surface deduplication tests',
+ 'remove only if Web settings no longer persists first-discovery awareness or a reviewed owner-scoped replacement preserves the same workspace isolation',
})),
...[
'ExternalToolActivationState',
diff --git a/scripts/core-boundaries/self-test.mjs b/scripts/core-boundaries/self-test.mjs
index ff6465a514..24a30dfac2 100644
--- a/scripts/core-boundaries/self-test.mjs
+++ b/scripts/core-boundaries/self-test.mjs
@@ -1527,12 +1527,6 @@ export function runManifestParserSelfTest({
'ExternalSourceControlRequestV1',
'ExternalSourceOperationStage',
'ExternalSourceRecoveryActionV1',
- 'EXTERNAL_APPLICATION_SCHEMA_V2',
- 'ExternalApplicationSnapshotV2',
- 'ExternalApplicationReviewPageV2',
- 'ExternalApplicationControlActionV2',
- 'ExternalApplicationControlRequestV2',
- 'ExternalApplicationControlResultV2',
]) {
if (!externalSourceControlPublicApiRule?.allowedSymbolEntries.some(
(entry) => entry.symbol === requiredSymbol
@@ -1583,9 +1577,6 @@ export function runManifestParserSelfTest({
'ExternalSourceControlRequestV1',
'get_external_source_control_snapshot',
'apply_external_source_control_action',
- 'get_external_application_snapshot_v2',
- 'get_external_application_review_page_v2',
- 'apply_external_application_action_v2',
]) {
if (!externalSourceCorePublicApiRule?.allowedSymbolEntries.some(
(entry) => entry.symbol === requiredSymbol
diff --git a/scripts/embed-server.py b/scripts/embed-server.py
new file mode 100644
index 0000000000..3d2aadb02a
--- /dev/null
+++ b/scripts/embed-server.py
@@ -0,0 +1,153 @@
+#!/usr/bin/env python3
+"""Local OpenAI-compatible embedding server for gbrain (port 8890).
+
+Model: Qdrant/bge-small-zh-v1.5 (ONNX, Dim=512) via onnxruntime + transformers
+tokenizer. No optimum dependency (optimum-onnxruntime has no py3.14 wheel).
+
+History (see .workbuddy/HANDBOOK.md):
+ uvicorn/FastAPI -> wedges after ~58 requests (async + ONNX blocking)
+ single-thread http.server -> queue timeouts under gbrain 20-way concurrency
+ ThreadingHTTPServer -> stable (200/200 concurrent test passed)
+"""
+
+import json
+import logging
+import os
+import sys
+import time
+from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
+
+import numpy as np
+import onnxruntime as ort
+from transformers import AutoTokenizer
+
+MODEL_DIR = os.path.expanduser(
+ r"~/.cache/huggingface/hub/models--Qdrant--bge-small-zh-v1.5/snapshots/v1.5"
+)
+HOST = "127.0.0.1"
+PORT = 8890
+MAX_TOKENS = 512
+
+logging.basicConfig(
+ level=logging.INFO,
+ format="[embed-server] %(message)s",
+ stream=sys.stdout,
+)
+
+
+class EmbedServer:
+ def __init__(self):
+ logging.info("Loading BAAI/bge-small-zh-v1.5...")
+ t0 = time.time()
+ self.tokenizer = AutoTokenizer.from_pretrained(MODEL_DIR, local_files_only=True)
+ self.sess = ort.InferenceSession(
+ os.path.join(MODEL_DIR, "model_optimized.onnx"),
+ providers=["CPUExecutionProvider"],
+ )
+ self.input_names = [i.name for i in self.sess.get_inputs()]
+ self.dim = 512
+ logging.info(f"Model loaded. Dim={self.dim} ({(time.time() - t0):.1f}s)")
+
+ def _mean_pool(self, last_hidden, mask):
+ # mask must stay 2D for count; expanded copy only for weighting
+ m = mask.astype("float32")[..., np.newaxis] # (B, S, 1)
+ summed = (last_hidden * m).sum(1) # (B, D)
+ count = mask.astype("float32").sum(1).clip(min=1e-9)[..., np.newaxis] # (B, 1)
+ return summed / count
+
+ def embed(self, texts):
+ enc = self.tokenizer(
+ list(texts),
+ padding=True,
+ truncation=True,
+ max_length=MAX_TOKENS,
+ return_tensors="np",
+ )
+ feed = {}
+ for name in self.input_names:
+ if name in enc:
+ feed[name] = enc[name]
+ out = self.sess.run(None, feed)
+ # last_hidden_state is the first output
+ last_hidden = out[0]
+ mask = enc["attention_mask"]
+ pooled = self._mean_pool(last_hidden, mask).astype("float32")
+ pooled = pooled / np.linalg.norm(pooled, axis=1, keepdims=True).clip(min=1e-9)
+ # OpenAI format: each item's embedding is a flat list (no batch axis).
+ return pooled.tolist()
+
+
+class Handler(BaseHTTPRequestHandler):
+ server: "EmbedServerWrapper" # type: ignore
+
+ def log_message(self, fmt, *args):
+ try:
+ msg = fmt % args
+ except Exception: # noqa: BLE001
+ msg = fmt
+ logging.info(f"{self.command} {self.path} HTTP/1.1 {msg}")
+
+ def do_GET(self):
+ if self.path == "/health":
+ body = b'{"status":"ok"}'
+ self.send_response(200)
+ self.send_header("Content-Type", "application/json")
+ self.send_header("Content-Length", str(len(body)))
+ self.end_headers()
+ self.wfile.write(body)
+ return
+ self.send_error(404)
+
+ def do_POST(self):
+ if self.path != "/v1/embeddings":
+ self.send_error(404)
+ return
+ length = int(self.headers.get("Content-Length", 0))
+ raw = self.rfile.read(length)
+ try:
+ req = json.loads(raw)
+ except json.JSONDecodeError:
+ self._json(400, {"error": {"message": "invalid JSON", "type": "invalid_request_error"}})
+ return
+ inp = req.get("input", "")
+ if isinstance(inp, str):
+ texts = [inp]
+ elif isinstance(inp, list):
+ texts = [t if isinstance(t, str) else str(t) for t in inp]
+ else:
+ self._json(400, {"error": {"message": "input must be string or list", "type": "invalid_request_error"}})
+ return
+ try:
+ vectors = self.server.embedder.embed(texts)
+ except Exception as e: # noqa: BLE001
+ logging.error(f"embed failed: {e}")
+ self._json(500, {"error": {"message": str(e), "type": "server_error"}})
+ return
+ data = [{"object": "embedding", "index": i, "embedding": v} for i, v in enumerate(vectors)]
+ self._json(200, {"object": "list", "data": data, "model": req.get("model", "bge-small-zh-v1.5"),
+ "usage": {"prompt_tokens": 0, "total_tokens": 0}})
+
+ def _json(self, code, payload):
+ body = json.dumps(payload).encode("utf-8")
+ self.send_response(code)
+ self.send_header("Content-Type", "application/json")
+ self.send_header("Content-Length", str(len(body)))
+ self.end_headers()
+ self.wfile.write(body)
+
+
+class EmbedServerWrapper(ThreadingHTTPServer):
+ daemon_threads = True
+
+ def __init__(self, server_address, handler_class):
+ self.embedder = EmbedServer()
+ super().__init__(server_address, handler_class)
+
+
+if __name__ == "__main__":
+ srv = EmbedServerWrapper((HOST, PORT), Handler)
+ logging.info(f"Embedding server running on http://{HOST}:{PORT}")
+ try:
+ srv.serve_forever()
+ except KeyboardInterrupt:
+ pass
diff --git a/scripts/package-windows-assets.mjs b/scripts/package-windows-assets.mjs
new file mode 100644
index 0000000000..17ba578a27
--- /dev/null
+++ b/scripts/package-windows-assets.mjs
@@ -0,0 +1,184 @@
+#!/usr/bin/env node
+/**
+ * Windows release asset packager (三件套: installer exe + zip 便携版 + SHA256SUMS).
+ *
+ * Usage:
+ * node scripts/package-windows-assets.mjs \
+ * --installer \
+ * --app-release-dir \
+ * --version 0.2.16 \
+ * --out-dir release-assets
+ *
+ * Produces under --out-dir:
+ * BitFun__windows-x86_64-installer.exe (copied installer)
+ * BitFun__windows-x86_64-portable.zip (portable app: exe + runtime dirs)
+ * SHA256SUMS (sha256 of every asset)
+ *
+ * The portable zip mirrors the installer payload layout: the main app exe plus
+ * the runtime siblings the app needs at startup (mobile-web, resources,
+ * third-party, THIRD_PARTY_NOTICES.md). It is a no-install distribution.
+ *
+ * Windows-native: uses tar.exe bsdtar to create the zip (available on Windows
+ * 10+); falls back to PowerShell Compress-Archive if bsdtar is unavailable.
+ */
+import { createHash } from 'crypto';
+import {
+ copyFileSync,
+ existsSync,
+ mkdirSync,
+ readFileSync,
+ readdirSync,
+ rmSync,
+ writeFileSync,
+} from 'fs';
+import { basename, join } from 'path';
+import { spawnSync } from 'child_process';
+
+const args = parseArgs(process.argv.slice(2));
+
+if (import.meta.url === new URL(`file://${process.argv[1]}`).href) {
+ await main(args);
+}
+
+export async function main(argv = []) {
+ const installerPath = requireArg(argv, 'installer');
+ const appReleaseDir = requireArg(argv, 'app-release-dir');
+ const version = requireArg(argv, 'version');
+ const outDir = requireArg(argv, 'out-dir');
+
+ if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(version)) {
+ fail(`Version is not safe for a release asset name: ${version}`);
+ }
+ if (!existsSync(installerPath)) {
+ fail(`Installer does not exist: ${installerPath}`);
+ }
+ if (!existsSync(appReleaseDir)) {
+ fail(`App release dir does not exist: ${appReleaseDir}`);
+ }
+
+ const exeName = 'bitfun-desktop.exe';
+ const exePath = join(appReleaseDir, exeName);
+ if (!existsSync(exePath)) {
+ fail(`Main app exe not found in release dir: ${exePath}`);
+ }
+
+ rmSync(outDir, { recursive: true, force: true });
+ mkdirSync(outDir, { recursive: true });
+
+ const baseName = `BitFun_${version}_windows-x86_64`;
+ const installerOut = join(outDir, `${baseName}-installer.exe`);
+ const zipOut = join(outDir, `${baseName}-portable.zip`);
+ const sumsOut = join(outDir, 'SHA256SUMS');
+
+ // 1. Copy installer exe.
+ copyFile(installerPath, installerOut);
+ log(`Copied installer: ${installerPath} -> ${installerOut}`);
+
+ // 2. Create portable zip from the app release dir.
+ // Only copy the runtime-relevant entries (mirrors build-installer.cjs payload
+ // selection, plus the notice file); exclude build metadata and debug symbols.
+ const portableEntries = collectPortableEntries(appReleaseDir, exeName);
+ log(`Portable zip will contain ${portableEntries.length} file(s) from ${appReleaseDir}`);
+ createZip(portableEntries, zipOut);
+
+ // 3. Write SHA256SUMS over every produced asset.
+ const assets = [installerOut, zipOut].sort();
+ const lines = assets
+ .map((file) => `${sha256File(file)} ${basename(file)}`)
+ .join('\n');
+ writeFileSync(sumsOut, `${lines}\n`);
+ log(`Wrote ${sumsOut}:`);
+ for (const line of lines.split('\n')) log(` ${line}`);
+
+ console.log(`\n[package-windows-assets] Done. Output in ${outDir}`);
+ console.log(` ${installerOut}`);
+ console.log(` ${zipOut}`);
+ console.log(` ${sumsOut}`);
+}
+
+export function collectPortableEntries(releaseDir, exeName) {
+ const entries = [];
+ const runtimeDirs = ['mobile-web', 'resources', 'third-party'];
+ for (const entry of readdirSync(releaseDir, { withFileTypes: true })) {
+ const src = join(releaseDir, entry.name);
+ if (entry.isFile()) {
+ if (entry.name === exeName) entries.push(src);
+ else if (entry.name === 'THIRD_PARTY_NOTICES.md') entries.push(src);
+ // .pdb / .d / .cargo-lock are build metadata, not runtime files.
+ } else if (entry.isDirectory() && runtimeDirs.includes(entry.name)) {
+ entries.push(src);
+ }
+ }
+ return entries;
+}
+
+function createZip(entries, zipPath) {
+ // Build a bsdtar include list of the source paths. tar.exe on Windows
+ // (C:\Windows\System32\tar.exe) uses libarchive and can write zip archives.
+ const cwd = process.cwd();
+ let tar = spawnSync('tar', ['--version'], { encoding: 'utf8' });
+ if (tar.status === 0) {
+ const args = ['-a', '-c', '-f', zipPath];
+ for (const entry of entries) args.push('-C', cwd, entry);
+ const result = spawnSync('tar', args, { stdio: 'inherit', encoding: 'utf8' });
+ if (result.status === 0) {
+ log(`Created zip via bsdtar: ${zipPath}`);
+ return;
+ }
+ log('bsdtar zip creation failed, falling back to Compress-Archive');
+ }
+ // Fallback: PowerShell Compress-Archive (slower, but always present).
+ const psScript = [
+ '$ErrorActionPreference = "Stop"',
+ `$dest = '${zipPath.replace(/'/g, "''")}'`,
+ 'if (Test-Path $dest) { Remove-Item $dest -Force }',
+ `$items = @(${entries
+ .map((entry) => `'${entry.replace(/'/g, "''")}'`)
+ .join(', ')})`,
+ 'Compress-Archive -Path $items -DestinationPath $dest -CompressionLevel Optimal',
+ ].join('; ');
+ const result = spawnSync('powershell.exe', ['-NoProfile', '-Command', psScript], {
+ stdio: 'inherit',
+ encoding: 'utf8',
+ });
+ if (result.status !== 0) fail(`Failed to create zip: ${zipPath}`);
+ log(`Created zip via Compress-Archive: ${zipPath}`);
+}
+
+function sha256File(filePath) {
+ return createHash('sha256').update(readFileSync(filePath)).digest('hex');
+}
+
+function copyFile(src, dest) {
+ mkdirSync(join(dest, '..'), { recursive: true });
+ copyFileSync(src, dest);
+}
+
+function parseArgs(rawArgs) {
+ const parsed = {};
+ for (let i = 0; i < rawArgs.length; i += 1) {
+ const arg = rawArgs[i];
+ if (!arg.startsWith('--')) continue;
+ const key = arg.slice(2);
+ const value = rawArgs[i + 1];
+ if (!value || value.startsWith('--')) fail(`Missing value for --${key}`);
+ parsed[key] = value;
+ i += 1;
+ }
+ return parsed;
+}
+
+function requireArg(parsed, key) {
+ const value = parsed[key];
+ if (!value) fail(`Missing required argument --${key}`);
+ return value;
+}
+
+function log(message) {
+ console.log(`\x1b[36m[package-windows-assets]\x1b[0m ${message}`);
+}
+
+function fail(message) {
+ console.error(`\x1b[31m[package-windows-assets]\x1b[0m ${message}`);
+ process.exit(1);
+}
diff --git a/scripts/package-windows-assets.test.mjs b/scripts/package-windows-assets.test.mjs
new file mode 100644
index 0000000000..ae3ce1dc31
--- /dev/null
+++ b/scripts/package-windows-assets.test.mjs
@@ -0,0 +1,70 @@
+import { test } from 'node:test';
+import assert from 'node:assert/strict';
+import { mkdtempSync, mkdirSync, writeFileSync, readdirSync, rmSync, statSync } from 'node:fs';
+import { tmpdir } from 'node:os';
+import { join } from 'node:path';
+
+import { collectPortableEntries } from './package-windows-assets.mjs';
+
+function makeFakeReleaseDir() {
+ const dir = mkdtempSync(join(tmpdir(), 'pwa-assets-'));
+ writeFileSync(join(dir, 'bitfun-desktop.exe'), 'fake exe bytes');
+ writeFileSync(join(dir, 'THIRD_PARTY_NOTICES.md'), '# notices');
+ writeFileSync(join(dir, 'bitfun_desktop.pdb'), 'debug symbols');
+ writeFileSync(join(dir, 'bitfun-desktop.d'), 'dep file');
+ writeFileSync(join(dir, '.cargo-lock'), '');
+ mkdirSync(join(dir, 'mobile-web', 'dist'), { recursive: true });
+ writeFileSync(join(dir, 'mobile-web', 'dist', 'index.html'), '');
+ mkdirSync(join(dir, 'resources'), { recursive: true });
+ writeFileSync(join(dir, 'resources', 'worker_host.js'), 'worker');
+ mkdirSync(join(dir, 'third-party', 'models.dev'), { recursive: true });
+ writeFileSync(join(dir, 'third-party', 'models.dev', 'LICENSE.txt'), 'license');
+ // Non-runtime build dirs that must be excluded.
+ mkdirSync(join(dir, 'deps'));
+ mkdirSync(join(dir, 'build'));
+ mkdirSync(join(dir, 'incremental'));
+ mkdirSync(join(dir, '.fingerprint'));
+ return dir;
+}
+
+test('collectPortableEntries includes exe, notice, and runtime dirs only', () => {
+ const dir = makeFakeReleaseDir();
+ try {
+ const entries = collectPortableEntries(dir, 'bitfun-desktop.exe');
+ const names = entries
+ .map((entry) => entry.replace(dir, '').replace(/\\/g, '/'))
+ .sort();
+ assert.deepEqual(names, [
+ '/THIRD_PARTY_NOTICES.md',
+ '/bitfun-desktop.exe',
+ '/mobile-web',
+ '/resources',
+ '/third-party',
+ ]);
+ } finally {
+ rmSync(dir, { recursive: true, force: true });
+ }
+});
+
+test('collectPortableEntries excludes pdb/d/cargo-lock/build dirs', () => {
+ const dir = makeFakeReleaseDir();
+ try {
+ const entries = collectPortableEntries(dir, 'bitfun-desktop.exe');
+ const flat = JSON.stringify(entries);
+ for (const excluded of ['bitfun_desktop.pdb', 'bitfun-desktop.d', '.cargo-lock', 'deps', 'build', 'incremental', '.fingerprint']) {
+ assert.ok(!flat.includes(excluded), `must exclude ${excluded}`);
+ }
+ } finally {
+ rmSync(dir, { recursive: true, force: true });
+ }
+});
+
+test('collectPortableEntries returns empty for empty release dir', () => {
+ const dir = mkdtempSync(join(tmpdir(), 'pwa-empty-'));
+ try {
+ const entries = collectPortableEntries(dir, 'bitfun-desktop.exe');
+ assert.deepEqual(entries, []);
+ } finally {
+ rmSync(dir, { recursive: true, force: true });
+ }
+});
diff --git a/src/apps/cli/src/account_sync.rs b/src/apps/cli/src/account_sync.rs
index 576c129218..444a8b2677 100644
--- a/src/apps/cli/src/account_sync.rs
+++ b/src/apps/cli/src/account_sync.rs
@@ -400,11 +400,10 @@ pub(crate) async fn run_auto_sync(
Ok((session_id, hash, Ok(Ok(version)))) => {
uploaded.push((session_id.clone(), hash, version));
let done = uploaded.len();
- let percent = if upload_total == 0 {
- 95u8
- } else {
- 20 + ((75 * done) / upload_total) as u8
- };
+ let percent = (75 * done)
+ .checked_div(upload_total)
+ .map(|part| 20 + part as u8)
+ .unwrap_or(95u8);
emit_progress(
"exporting_sessions",
percent.min(95),
diff --git a/src/apps/cli/src/acp_cli.rs b/src/apps/cli/src/acp_cli.rs
index 2e0d19b405..ab5eacfa42 100644
--- a/src/apps/cli/src/acp_cli.rs
+++ b/src/apps/cli/src/acp_cli.rs
@@ -1,6 +1,7 @@
use anyhow::{anyhow, bail, Context, Result};
use bitfun_acp::client::{
AcpClientConfig, AcpClientInfo, AcpClientPermissionMode, AcpClientRequirementProbe,
+ TryConnectResult,
};
use bitfun_acp::AcpClientService;
use clap::ValueEnum;
@@ -27,6 +28,7 @@ pub(crate) enum ExternalAcpClient {
pub(crate) enum CliAcpPermissionMode {
Ask,
AllowOnce,
+ AllowAlways,
RejectOnce,
}
@@ -67,6 +69,8 @@ impl ExternalAcpClient {
enabled: true,
readonly: false,
permission_mode: AcpClientPermissionMode::Ask,
+ category: None,
+ description: None,
}
}
}
@@ -76,6 +80,7 @@ impl CliAcpPermissionMode {
match self {
Self::Ask => AcpClientPermissionMode::Ask,
Self::AllowOnce => AcpClientPermissionMode::AllowOnce,
+ Self::AllowAlways => AcpClientPermissionMode::AllowAlways,
Self::RejectOnce => AcpClientPermissionMode::RejectOnce,
}
}
@@ -284,6 +289,9 @@ pub(crate) async fn doctor_external_clients() -> Result {
has_runnable = true;
}
print_requirement_probe(&probe);
+ if probe.runnable {
+ print_client_connect_check(&service, &probe).await?;
+ }
}
println!();
@@ -349,7 +357,7 @@ pub(crate) async fn run_external_client(
) -> Result<()> {
if matches!(permission, CliAcpPermissionMode::Ask) {
bail!(
- "`--permission ask` is not available for non-interactive `acp run`; use allow-once or reject-once."
+ "`--permission ask` is not available for non-interactive `acp run`; use allow-always, allow-once or reject-once."
);
}
@@ -549,6 +557,41 @@ fn print_requirement_probe(probe: &AcpClientRequirementProbe) {
}
}
+/// Runs the ACP handshake for a runnable client and surfaces login guidance
+/// when the client requires authentication.
+async fn print_client_connect_check(
+ service: &Arc,
+ probe: &AcpClientRequirementProbe,
+) -> Result<()> {
+ match service.try_connect_client(&probe.id).await {
+ Ok(TryConnectResult::Success) => {
+ println!(" connect: ok");
+ }
+ Ok(TryConnectResult::FailAuth { error, login_hint }) => {
+ println!(" connect: auth required ({})", error);
+ match login_hint {
+ Some(hint) => println!(" hint: {}", hint),
+ None => println!(
+ " hint: no login command is known for this client; authenticate the CLI manually"
+ ),
+ }
+ }
+ Ok(TryConnectResult::FailCli { error }) => {
+ println!(" connect: CLI not found ({})", error);
+ }
+ Ok(TryConnectResult::FailAcp { error }) => {
+ println!(" connect: handshake failed ({})", error);
+ }
+ Err(error) if error.to_string().contains("not found") => {
+ // Client is not configured; requirement probe already covers it.
+ }
+ Err(error) => {
+ println!(" connect: check failed ({})", error);
+ }
+ }
+ Ok(())
+}
+
fn print_requirement_item(label: &str, item: &bitfun_acp::client::AcpRequirementProbeItem) {
let installed = if item.installed {
"installed"
diff --git a/src/apps/cli/src/actions.rs b/src/apps/cli/src/actions.rs
index 5b8b35a86e..c85e359182 100644
--- a/src/apps/cli/src/actions.rs
+++ b/src/apps/cli/src/actions.rs
@@ -586,9 +586,9 @@ static ACTION_SPECS: &[ActionSpec] = &[
},
ActionSpec {
id: "extensions",
- name: "External integrations",
+ name: "Extensions",
aliases: &["/extensions"],
- description: "View external source status and Safe Mode",
+ description: "View and manage extensions",
contexts: CHAT,
availability: ActionAvailability::Always,
handler: ActionHandler::Extensions,
@@ -603,7 +603,7 @@ static ACTION_SPECS: &[ActionSpec] = &[
id: "hooks",
name: "Hooks",
aliases: &["/hooks"],
- description: "Review and manage native and imported Hooks",
+ description: "View and manage Hooks",
contexts: CHAT,
availability: ActionAvailability::Always,
handler: ActionHandler::NativeHooks,
@@ -625,7 +625,7 @@ static ACTION_SPECS: &[ActionSpec] = &[
default_bindings: &[],
fallback_bindings: &[],
shortcut_field: None,
- palette: palette("Tools", false),
+ palette: None,
shortcut_label: None,
slash_on_startup: false,
},
@@ -1327,6 +1327,7 @@ pub(crate) fn slash_actions(state: ActionState) -> Vec {
.filter(|spec| {
spec.available(state)
&& !spec.aliases.is_empty()
+ && spec.id != "hooks_external"
&& (state.context != ActionContext::Startup || spec.slash_on_startup)
})
.flat_map(|spec| {
@@ -2484,7 +2485,22 @@ mod tests {
assert_eq!(tools.handler, ActionHandler::Tools);
let extensions = action_for_alias("/extensions", ActionContext::Chat).unwrap();
assert_eq!(extensions.handler, ActionHandler::Extensions);
- assert!(extensions.description.contains("Safe Mode"));
+ assert_eq!(extensions.name, "Extensions");
+ assert_eq!(extensions.description, "View and manage extensions");
+ assert_eq!(
+ action_for_alias("/hooks_external", ActionContext::Chat)
+ .expect("legacy Hook alias remains parseable")
+ .handler,
+ ActionHandler::ExternalHooks
+ );
+ assert!(!slash_actions(ActionState::chat(false, false))
+ .iter()
+ .any(|action| action.id == "hooks_external"));
+ assert!(!palette_actions(ActionState::chat(false, false))
+ .iter()
+ .any(|action| action.id == "hooks_external"));
+ let hooks = action_for_alias("/hooks", ActionContext::Chat).unwrap();
+ assert_eq!(hooks.description, "View and manage Hooks");
let agents = action_for_alias("/agent", ActionContext::Chat).unwrap();
assert_eq!(agents.handler, ActionHandler::OpenAgentSelector);
assert_eq!(agents.description, "Switch modes and manage agents");
diff --git a/src/apps/cli/src/agent/runtime_client.rs b/src/apps/cli/src/agent/runtime_client.rs
index 369677501a..fc5f4e8c13 100644
--- a/src/apps/cli/src/agent/runtime_client.rs
+++ b/src/apps/cli/src/agent/runtime_client.rs
@@ -87,6 +87,9 @@ pub(crate) enum SessionMigrationNotice {
}
impl SessionMigrationNotice {
+ /// Local CLI migration notice rendering, retained for the shared-runtime
+ /// path after the upstream app-server CLI refactor dropped its call sites.
+ #[allow(dead_code)]
pub(crate) fn user_message(&self) -> String {
let (setting, previous_id, restored_id) = match self {
Self::Mode {
@@ -131,6 +134,9 @@ fn session_migration_notices(
#[derive(Debug)]
pub(crate) struct SessionOperationError {
message: String,
+ /// Whether the remote outcome was unknown after the operation returned.
+ /// Retained for the shared-runtime path after the upstream CLI refactor.
+ #[allow(dead_code)]
outcome_unknown: bool,
}
@@ -142,6 +148,9 @@ impl fmt::Display for SessionOperationError {
impl std::error::Error for SessionOperationError {}
+/// Local error-shaping helpers retained for the shared-runtime path after the
+/// upstream app-server CLI refactor dropped their call sites.
+#[allow(dead_code)]
impl SessionOperationError {
fn runtime(error: RuntimeError) -> Self {
let outcome_unknown = matches!(
@@ -252,6 +261,7 @@ impl CliWorkspacePaths {
self.remote = binding.remote_connection_id.is_some() || binding.remote_ssh_host.is_some();
}
+ #[allow(dead_code)]
fn reset_execution_to_project(&mut self) -> PathBuf {
let project = self.project();
self.execution = Some(project.clone());
@@ -262,6 +272,7 @@ impl CliWorkspacePaths {
project
}
+ #[allow(dead_code)]
fn workspace_diff_unavailable_reason(&self) -> Option<&'static str> {
if self.remote {
return Some("Workspace diff is unavailable for remote Sessions");
@@ -277,6 +288,7 @@ impl CliWorkspacePaths {
}
}
+#[allow(dead_code)]
fn same_workspace_location(left: &Path, right: &Path) -> bool {
left == right
|| dunce::canonicalize(left)
@@ -296,17 +308,21 @@ pub(crate) struct ExecAgentRuntimeClient {
/// Current turn ID (for cancellation)
current_turn_id: Arc>>,
shared_agent_events: Option>,
+ #[allow(dead_code)]
shared_permission_events:
Option>,
shared_pending_permissions: Arc>>,
}
+#[allow(clippy::large_enum_variant)] // embedded runtime holds the full agent stack; boxing would churn every dispatch site
enum CliAgentRuntimeBackend {
Embedded(AgentRuntime),
+ #[allow(dead_code)]
Shared(RuntimeIpcClient),
}
#[derive(Debug, Clone, PartialEq, Eq)]
+#[allow(dead_code)]
pub(crate) struct CliAgentMode {
pub(crate) id: String,
pub(crate) description: String,
@@ -316,6 +332,10 @@ pub(crate) struct CliAgentMode {
type SharedBroadcast = Arc>>>;
+/// Local shared-runtime construction surface. The upstream app-server CLI
+/// refactor dropped the call sites of `new_shared` and friends; they are
+/// retained as the local shared-runtime capability surface.
+#[allow(dead_code)]
impl ExecAgentRuntimeClient {
pub(crate) fn new(runtime: &CliRuntimeContext, workspace_path: Option) -> Self {
Self {
@@ -568,6 +588,7 @@ impl ExecAgentRuntimeClient {
workspace_path: workspace_path.to_string_lossy().to_string(),
remote_connection_id: None,
remote_ssh_host: None,
+ include_hidden: false,
};
match &self.backend {
CliAgentRuntimeBackend::Embedded(runtime) => runtime
@@ -1166,6 +1187,7 @@ impl ExecAgentRuntimeClient {
workspace_path: project_workspace.to_string_lossy().to_string(),
remote_connection_id: None,
remote_ssh_host: None,
+ include_hidden: false,
})
.await
{
@@ -1271,6 +1293,10 @@ impl ExecAgentRuntimeClient {
}
}
+/// Local shared-runtime client methods. The upstream app-server CLI refactor
+/// dropped the call sites of several of these; they are retained as the
+/// local shared-runtime capability surface until the local CLI wires them in.
+#[allow(dead_code)]
impl ExecAgentRuntimeClient {
pub(crate) async fn ensure_session(&self, agent_type: &str) -> Result {
self.ensure_session_with_model(agent_type, None).await
@@ -1357,7 +1383,7 @@ impl ExecAgentRuntimeClient {
} if accepted_session == session_id && accepted_turn == turn_id => {
Ok(accepted_turn)
}
- _ => return Err(unexpected_shared_result("compact_session")),
+ _ => Err(unexpected_shared_result("compact_session")),
},
}
}
@@ -1543,6 +1569,7 @@ impl ExecAgentRuntimeClient {
turn_id: turn_id.clone(),
content,
display_content,
+ prepended_reminders: Vec::new(),
};
match &self.backend {
@@ -1852,6 +1879,7 @@ fn shared_receiver(
.ok_or_else(|| RuntimeError::Port(PortError::new(PortErrorKind::NotAvailable, message)))
}
+#[allow(dead_code)]
fn spawn_shared_event_bridge(
mut source: broadcast::Receiver,
agent_sender: broadcast::Sender,
@@ -1933,6 +1961,7 @@ fn spawn_shared_event_bridge(
});
}
+#[allow(dead_code)]
fn shared_disconnect_message(reason: Option) -> String {
if reason == Some(RuntimeIpcStreamInvalidationReason::FrameTooLarge) {
format!(
@@ -1943,6 +1972,7 @@ fn shared_disconnect_message(reason: Option)
}
}
+#[allow(dead_code)]
fn project_routed_permission_event(
event: &mut bitfun_agent_runtime::sdk::PermissionRequestEvent,
routed_session_id: &str,
@@ -2441,6 +2471,9 @@ mod tests {
turn_count: 1,
created_at_ms: 1,
last_active_at_ms: 2,
+ is_daemon: false,
+ parent_session_id: None,
+ status: None,
}
}
diff --git a/src/apps/cli/src/agent/tui_client.rs b/src/apps/cli/src/agent/tui_client.rs
index bec380e236..eedd9822e1 100644
--- a/src/apps/cli/src/agent/tui_client.rs
+++ b/src/apps/cli/src/agent/tui_client.rs
@@ -22,11 +22,7 @@ use bitfun_app_server_protocol::workspace::*;
use bitfun_app_server_protocol::worktree::*;
use bitfun_core_types::SessionUsageReport;
use bitfun_events::{AgenticEvent, AgenticEventEnvelope, AgenticEventPriority};
-use bitfun_product_domains::external_source_control::{
- ExternalApplicationControlRequestV2, ExternalApplicationControlResultV2,
- ExternalApplicationReviewPageRequestV2, ExternalApplicationReviewPageV2,
- ExternalApplicationSnapshotV2, ExternalSourceControlRequestV1,
-};
+use bitfun_product_domains::external_source_control::ExternalSourceControlRequestV1;
use bitfun_product_domains::external_sources::{
ExternalSourceOperationError, ExternalSourceOperationErrorCode, ExternalSourcePublicSnapshot,
NativePromptCommandDescriptor, PromptCommandShellReviewDecision,
@@ -309,13 +305,6 @@ impl TuiAgentClient {
.map_err(|error| anyhow::anyhow!(error))
}
- pub(crate) async fn delete_model(&self, model_id: String) -> Result {
- self.backend
- .delete_model(DeleteModelRequest { model_id })
- .await
- .map_err(|error| anyhow::anyhow!(error))
- }
-
pub(crate) async fn set_model_default(
&self,
request: SetModelDefaultRequest,
@@ -467,49 +456,6 @@ impl TuiAgentClient {
.map_err(external_source_backend_error)
}
- pub(crate) async fn external_application_snapshot_v2(
- &self,
- force_refresh: bool,
- ) -> std::result::Result {
- self.backend
- .external_application_snapshot_v2(ExternalApplicationSnapshotRequestV2 {
- workspace_path: Some(self.workspace_path_string()),
- force_refresh,
- })
- .await
- .map(|response| response.0)
- .map_err(external_source_backend_error)
- }
-
- pub(crate) async fn external_application_review_page_v2(
- &self,
- request: ExternalApplicationReviewPageRequestV2,
- ) -> std::result::Result {
- self.backend
- .external_application_review_page_v2(ExternalApplicationReviewPageRequest {
- workspace_path: Some(self.workspace_path_string()),
- request,
- })
- .await
- .map(|response| response.0)
- .map_err(external_source_backend_error)
- }
-
- pub(crate) async fn apply_external_application_action_v2(
- &self,
- request: ExternalApplicationControlRequestV2,
- ) -> std::result::Result {
- let operation_id = request.operation_id.clone();
- self.backend
- .apply_external_application_action_v2(ExternalApplicationActionRequest {
- workspace_path: Some(self.workspace_path_string()),
- request,
- })
- .await
- .map(|response| response.0)
- .map_err(|error| external_source_backend_error_with_id(error, Some(&operation_id)))
- }
-
pub(crate) fn subscribe_external_source_updates(
&self,
) -> Result> {
@@ -939,6 +885,7 @@ impl TuiAgentClient {
workspace_path: self.project_workspace_path_string(),
remote_connection_id: None,
remote_ssh_host: None,
+ include_hidden: false,
}))
.await?
.sessions)
@@ -1243,6 +1190,9 @@ impl TuiAgentClient {
Ok(())
}
+ /// Local TUI turn-settlement waiter, retained for the shared-runtime path
+ /// after the upstream app-server CLI refactor dropped its call sites.
+ #[allow(dead_code)]
pub(crate) async fn wait_for_turn_settlement(
&self,
session_id: &str,
@@ -1476,6 +1426,7 @@ impl TuiAgentClient {
turn_id,
content,
display_content,
+ prepended_reminders: Vec::new(),
}))
.await?
.steering_id)
diff --git a/src/apps/cli/src/bin/bitfun_cli_compat.rs b/src/apps/cli/src/bin/bitfun_cli_compat.rs
index 0c24fb35a4..e9e014d5b5 100644
--- a/src/apps/cli/src/bin/bitfun_cli_compat.rs
+++ b/src/apps/cli/src/bin/bitfun_cli_compat.rs
@@ -33,6 +33,7 @@ unsafe extern "system" fn keep_wrapper_alive(ctrl_type: u32) -> windows::core::B
fn hand_off(primary: &Path) -> i32 {
use windows::Win32::System::Console::SetConsoleCtrlHandler;
+ // SAFETY: the handler is a static Rust fn; the pointer is valid for the process lifetime.
if let Err(error) = unsafe { SetConsoleCtrlHandler(Some(keep_wrapper_alive), true) } {
eprintln!("Error: failed to initialize deprecated launcher: {error}");
return 1;
diff --git a/src/apps/cli/src/chat_state.rs b/src/apps/cli/src/chat_state.rs
index 54c5c19da8..ea57f32e46 100644
--- a/src/apps/cli/src/chat_state.rs
+++ b/src/apps/cli/src/chat_state.rs
@@ -150,6 +150,7 @@ pub(crate) struct ToolDisplayState {
/// A single content block in a message (text, thinking, or tool call)
#[derive(Debug, Clone)]
+#[allow(clippy::large_enum_variant)] // tool display state is inherently the largest content block
pub(crate) enum FlowItem {
/// Text content block
Text { content: String, is_streaming: bool },
diff --git a/src/apps/cli/src/daemon/service.rs b/src/apps/cli/src/daemon/service.rs
index 67591c330c..8d7ec98310 100644
--- a/src/apps/cli/src/daemon/service.rs
+++ b/src/apps/cli/src/daemon/service.rs
@@ -85,6 +85,7 @@ fn render_launch_agent(executable: &Path) -> String {
)
}
+#[cfg_attr(windows, allow(dead_code))]
fn run_command(program: &str, args: &[&str]) -> Result {
std::process::Command::new(program)
.args(args)
@@ -110,6 +111,7 @@ fn run_systemctl_user(args: &[&str]) -> Result {
.with_context(|| format!("run `systemctl --user {}`", args.join(" ")))
}
+#[cfg_attr(windows, allow(dead_code))]
#[cfg(target_os = "macos")]
fn ensure_success(program: &str, args: &[&str]) -> Result<()> {
let output = run_command(program, args)?;
diff --git a/src/apps/cli/src/dispatch/runner.rs b/src/apps/cli/src/dispatch/runner.rs
index 0f4eec6aca..f580f9c1b3 100644
--- a/src/apps/cli/src/dispatch/runner.rs
+++ b/src/apps/cli/src/dispatch/runner.rs
@@ -1,4 +1,5 @@
use std::process::{Command, Stdio};
+#[cfg_attr(windows, allow(unused_imports))]
use std::time::Duration;
use anyhow::{anyhow, bail, Context, Result};
@@ -286,6 +287,7 @@ fn process_matches_action(_pid: u32, _action: &str, _job_id: &str) -> bool {
false
}
+#[cfg_attr(windows, allow(dead_code))]
fn arguments_match_action(args: &[String], action: &str, job_id: &str) -> bool {
args.windows(4).any(|window| {
window[0] == "dispatch"
diff --git a/src/apps/cli/src/dispatch/worker.rs b/src/apps/cli/src/dispatch/worker.rs
index 2a6679bce0..42a6a6d56a 100644
--- a/src/apps/cli/src/dispatch/worker.rs
+++ b/src/apps/cli/src/dispatch/worker.rs
@@ -505,6 +505,7 @@ async fn process_mailboxes(
turn_id: turn_id.to_string(),
content: request.content.clone(),
display_content: request.display_content.clone(),
+ prepended_reminders: Vec::new(),
})
.await
.map_err(|error| anyhow!(error.into_message()))
diff --git a/src/apps/cli/src/dispatch/workspace.rs b/src/apps/cli/src/dispatch/workspace.rs
index 465dccf592..c8716d250d 100644
--- a/src/apps/cli/src/dispatch/workspace.rs
+++ b/src/apps/cli/src/dispatch/workspace.rs
@@ -735,14 +735,14 @@ fn bundle_commit_in_store(
// `git bundle verify` checks the bundle's own integrity and that every
// prerequisite commit is already present, so a bundle that would leave
// a broken history is rejected before it touches the object store.
- git(&repo, &["bundle", "verify", path_arg(&bundle_path)?])
+ git(&repo, &["bundle", "verify", path_arg(&bundle_path)?.as_str()])
.context("verify dispatch bundle")?;
git(
&repo,
&[
"fetch",
"--no-tags",
- path_arg(&bundle_path)?,
+ path_arg(&bundle_path)?.as_str(),
&format!("+refs/heads/{0}:refs/heads/{0}", provision.branch),
],
)
@@ -1142,7 +1142,7 @@ fn sync_in_store(
let bundle_range = format!("{sync_base}..{}", provision.branch);
git(
&worktree,
- &["bundle", "create", path_arg(&bundle_path)?, &bundle_range],
+ &["bundle", "create", path_arg(&bundle_path)?.as_str(), &bundle_range],
)
.context("package dispatch result bundle")?;
set_private_file_permissions(&bundle_path)?;
@@ -1491,7 +1491,7 @@ fn create_worktree(
git(repo, &["update-ref", &branch_ref, base_commit])
.context("point the dispatch branch at the requested base commit")?;
}
- git(repo, &["worktree", "add", path_arg(worktree_path)?, branch])
+ git(repo, &["worktree", "add", path_arg(worktree_path)?.as_str(), branch])
.context("create the dispatch worktree")?;
canonical_utf8(worktree_path)
}
@@ -1700,9 +1700,13 @@ fn git_succeeds(dir: &Path, args: &[&str]) -> Result {
Ok(status.success())
}
-fn path_arg(path: &Path) -> Result<&str> {
- path.to_str()
- .ok_or_else(|| anyhow::anyhow!("dispatch path is not valid UTF-8: {}", path.display()))
+fn path_arg(path: &Path) -> Result {
+ let text = path
+ .to_str()
+ .ok_or_else(|| anyhow::anyhow!("dispatch path is not valid UTF-8: {}", path.display()))?;
+ #[cfg(windows)]
+ let text = strip_verbatim_prefix(text);
+ Ok(text.to_string())
}
fn canonical_utf8(path: &Path) -> Result {
@@ -1713,6 +1717,24 @@ fn canonical_utf8(path: &Path) -> Result {
.ok_or_else(|| anyhow::anyhow!("dispatch path is not valid UTF-8"))
}
+/// Strip the `\\?\` verbatim prefix that `fs::canonicalize` emits on Windows.
+///
+/// Git for Windows cannot create worktrees under a verbatim path (it sees
+/// `//?/C:/...` and fails to create leading directories), and persisted
+/// dispatch records must stay in the normal path form. The helper also covers
+/// records that were already persisted with the prefix before this fix.
+#[cfg(windows)]
+fn strip_verbatim_prefix(path: &str) -> String {
+ match path.strip_prefix(r"\\?\") {
+ Some(rest) => match rest.strip_prefix("UNC\\") {
+ // `\\?\UNC\server\share\...` is the verbatim form of `\\server\share\...`.
+ Some(unc_rest) => format!(r"\\{unc_rest}"),
+ None => rest.to_string(),
+ },
+ None => path.to_string(),
+ }
+}
+
fn is_real_directory(path: &Path) -> bool {
fs::symlink_metadata(path)
.ok()
@@ -1911,7 +1933,7 @@ mod tests {
fn bundle_everything(source: &Path, bundle: &Path) {
git(
source,
- &["bundle", "create", path_arg(bundle).expect("path"), "main"],
+ &["bundle", "create", path_arg(bundle).expect("path").as_str(), "main"],
)
.expect("bundle");
}
@@ -2200,7 +2222,7 @@ mod tests {
"worktree",
"remove",
"--force",
- path_arg(&worktree).unwrap(),
+ path_arg(&worktree).unwrap().as_str(),
],
)
.expect("remove checkout only");
@@ -2296,7 +2318,7 @@ mod tests {
assert!(bundle.is_file());
let prerequisites = git(
&worktree,
- &["bundle", "list-heads", path_arg(&bundle).unwrap()],
+ &["bundle", "list-heads", path_arg(&bundle).unwrap().as_str()],
)
.expect("list heads");
assert!(prerequisites.contains("refs/heads/main"));
@@ -2441,6 +2463,9 @@ mod tests {
);
}
+ // Detached dispatch workers exist only on Linux and macOS
+ // (runner::is_supported), so these retry flows cannot run on Windows.
+ #[cfg(any(target_os = "linux", target_os = "macos"))]
#[test]
fn reported_sync_failure_allows_a_new_operation_to_take_over() {
let temp = tempfile::tempdir().expect("tempdir");
@@ -2519,6 +2544,7 @@ mod tests {
assert!(!replacement.failure_reported);
}
+ #[cfg(any(target_os = "linux", target_os = "macos"))]
#[test]
fn legacy_sync_failure_without_operation_id_is_reported_then_retryable() {
let temp = tempfile::tempdir().expect("tempdir");
@@ -2719,7 +2745,7 @@ mod tests {
&[
"bundle",
"create",
- path_arg(&bundle).expect("path"),
+ path_arg(&bundle).expect("path").as_str(),
&format!("bitfun/dispatch/{first}"),
],
)
diff --git a/src/apps/cli/src/management.rs b/src/apps/cli/src/management.rs
index 26ea57db5f..c9cf3f50ce 100644
--- a/src/apps/cli/src/management.rs
+++ b/src/apps/cli/src/management.rs
@@ -548,6 +548,7 @@ pub(crate) async fn print_usage_report(session_id: Option<&str>) -> Result<()> {
workspace_path: workspace_path.to_string_lossy().to_string(),
remote_connection_id: None,
remote_ssh_host: None,
+ include_hidden: false,
})
.await?
.first()
diff --git a/src/apps/cli/src/modes/chat.rs b/src/apps/cli/src/modes/chat.rs
index c5f41ffa3e..82aa7d6513 100644
--- a/src/apps/cli/src/modes/chat.rs
+++ b/src/apps/cli/src/modes/chat.rs
@@ -557,8 +557,9 @@ pub(crate) struct ChatMode {
external_tool_notice_key: Option,
external_tool_review_snapshot: Option,
external_tool_mutation_rx: Option>,
+ external_control_snapshot:
+ Option,
external_control_mutation_rx: Option>,
- external_application_ui: ExternalApplicationUiState,
external_agent_notice_key: Option,
external_agent_review_snapshot: Option,
external_agent_mutation_rx: Option>,
@@ -623,8 +624,8 @@ impl ChatMode {
external_tool_notice_key: None,
external_tool_review_snapshot: None,
external_tool_mutation_rx: None,
+ external_control_snapshot: None,
external_control_mutation_rx: None,
- external_application_ui: ExternalApplicationUiState::default(),
external_agent_notice_key: None,
external_agent_review_snapshot: None,
external_agent_mutation_rx: None,
diff --git a/src/apps/cli/src/modes/chat/commands.rs b/src/apps/cli/src/modes/chat/commands.rs
index e684ddd954..a81068dcc9 100644
--- a/src/apps/cli/src/modes/chat/commands.rs
+++ b/src/apps/cli/src/modes/chat/commands.rs
@@ -180,7 +180,6 @@ fn consume_selected_native_command_once(
fn retain_selected_native_command_for_input(selected_command: &mut Option, input: &str) {
let still_selected = selected_command.as_deref().is_some_and(|selected| {
input
- .trim_start()
.split_whitespace()
.next()
.map(|token| token.trim_start_matches('/'))
diff --git a/src/apps/cli/src/modes/chat/external_editor.rs b/src/apps/cli/src/modes/chat/external_editor.rs
index 68127af24d..ec8e59af21 100644
--- a/src/apps/cli/src/modes/chat/external_editor.rs
+++ b/src/apps/cli/src/modes/chat/external_editor.rs
@@ -1,4 +1,6 @@
-use std::ffi::{OsStr, OsString};
+#[cfg(windows)]
+use std::ffi::OsStr;
+use std::ffi::OsString;
use std::io::Write;
use std::path::PathBuf;
use std::process::{Command, Stdio};
@@ -103,7 +105,7 @@ fn has_unclosed_windows_quote(value: &str) -> bool {
backslashes += 1;
continue;
}
- if character == '"' && backslashes % 2 == 0 {
+ if character == '"' && backslashes.is_multiple_of(2) {
quoted = !quoted;
}
backslashes = 0;
diff --git a/src/apps/cli/src/modes/chat/external_hooks.rs b/src/apps/cli/src/modes/chat/external_hooks.rs
index 51bc0cbe7e..b946a18cca 100644
--- a/src/apps/cli/src/modes/chat/external_hooks.rs
+++ b/src/apps/cli/src/modes/chat/external_hooks.rs
@@ -644,6 +644,7 @@ impl ChatMode {
item
}
+ #[allow(clippy::too_many_arguments)] // hook mutation entry carrying view, state and runtime handles
fn start_hook_mutation(
&mut self,
import_number: usize,
diff --git a/src/apps/cli/src/modes/chat/external_review.rs b/src/apps/cli/src/modes/chat/external_review.rs
index 9e680b9a70..68fd650db3 100644
--- a/src/apps/cli/src/modes/chat/external_review.rs
+++ b/src/apps/cli/src/modes/chat/external_review.rs
@@ -1,16 +1,6 @@
// Pure projections and review text derived from the external-source catalog.
use bitfun_product_domains::external_source_control::{
- ExternalApplicationControlActionV2, ExternalApplicationControlRequestV2,
- ExternalApplicationControlResultV2, ExternalApplicationEffectiveStatusV2,
- ExternalApplicationHealthV2, ExternalApplicationOperationOutcomeV2,
- ExternalApplicationPrimaryActionV2, ExternalApplicationRecoveryActionV2,
- ExternalApplicationReviewItemRefV2, ExternalApplicationReviewPageRequestV2,
- ExternalApplicationReviewPageV2, ExternalApplicationReviewSelectionBaselineV2,
- ExternalApplicationReviewSelectionOverrideV2, ExternalApplicationRiskLevelV2,
- ExternalApplicationSafetyCeilingV2, ExternalApplicationSnapshotV2,
- ExternalApplicationTargetScopeV2, ExternalSourceDesiredState, ExternalSourceEffectiveStatus,
- ExternalSourceRecoveryActionV1, ExternalSourceSupportState,
- EXTERNAL_APPLICATION_REVIEW_PAGE_MAX_ITEMS, EXTERNAL_APPLICATION_SCHEMA_V2,
+ ExternalSourceDesiredState, ExternalSourceEffectiveStatus, ExternalSourceRecoveryActionV1,
};
fn external_command_projections(
@@ -21,7 +11,7 @@ fn external_command_projections(
let mut projections = snapshot
.commands
.iter()
- .filter_map(|entry| {
+ .map(|entry| {
let ecosystem = snapshot
.sources
.iter()
@@ -68,7 +58,7 @@ fn external_command_projections(
conflict_key,
})
});
- Some(ExternalCommandProjection {
+ ExternalCommandProjection {
action_id: format!("external-command:{}", entry.definition.name),
command_name: entry.definition.name.clone(),
invocation_alias: format!("/{}", entry.definition.name),
@@ -78,7 +68,7 @@ fn external_command_projections(
restricted,
provider_conflict_key: None,
native_collision,
- })
+ }
})
.collect::>();
@@ -271,7 +261,7 @@ enum ExternalControlUiAction {
Show,
Refresh,
SetSafeMode(bool),
- SetSourceEnabled { source_key: String, enabled: bool },
+ SetSourceEnabled { source_index: usize, enabled: bool },
}
fn parse_external_control_action(arguments: &str) -> Result {
@@ -280,863 +270,105 @@ fn parse_external_control_action(arguments: &str) -> Result Ok(ExternalControlUiAction::Refresh),
["safe-mode", "on"] => Ok(ExternalControlUiAction::SetSafeMode(true)),
["safe-mode", "off"] => Ok(ExternalControlUiAction::SetSafeMode(false)),
- ["source", "enable", source_key] => Ok(ExternalControlUiAction::SetSourceEnabled {
- source_key: (*source_key).to_string(),
+ ["enable", source_number] => Ok(ExternalControlUiAction::SetSourceEnabled {
+ source_index: parse_positive_index(Some(source_number), "extension number")?,
enabled: true,
}),
- ["source", "disable", source_key] => Ok(ExternalControlUiAction::SetSourceEnabled {
- source_key: (*source_key).to_string(),
+ ["disable", source_number] => Ok(ExternalControlUiAction::SetSourceEnabled {
+ source_index: parse_positive_index(Some(source_number), "extension number")?,
enabled: false,
}),
- _ => Err("usage: /extensions [status | refresh | safe-mode on | safe-mode off | source enable | source disable ]".to_string()),
- }
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-enum ExternalReviewDirection {
- Next,
- Previous,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-enum ExternalReviewNavigation {
- Open,
- Move {
- expected_cursor: Option,
- previous_cursors: Vec