Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions docs/architecture/deep-review.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,13 @@ catalog behind a deferred tool specification, so a direct primary review pays
no catalog input cost. Full selected guidance enters model context only after
admission.

The Skill catalog follows the runtime registry's invocation eligibility. External
discovery alone does not admit guidance: the user must import a native copy first.
Imported copies retain their source parser semantics through the import record,
including Claude skills without explicit names. Resolution rechecks the native
key and implicit-invocation policy, so undoing an import or disabling implicit
invocation also invalidates an earlier catalog selection.

The existing manifest and backend admission path gain one small typed
`focused_assignment` projection: question id, target fingerprint, allowed
changed paths or packet id, expected evidence, capability key, and capability
Expand Down
84 changes: 66 additions & 18 deletions docs/architecture/extensions/external-ai-work-sources-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,15 @@ Codex/Claude Code 运行时适配和外部 Subagent 续接仍属于后续阶段
Codex role 仍仅作为 Subagent,不能因来源被识别就宣称宿主运行时兼容。OpenCode、Claude Code 与 Codex 的本地 Hook 脱敏目录
已作为独立只读切片接入;在此之上,Claude Code 与 Codex 的同步 command 子集可经精确命令审阅复制为 OpenBitFun 管理的
原生 Hook 层,仍由唯一 `AgentHookEngine` 执行。OpenCode handler、非 command/异步 handler 和未审阅声明仍不可执行。
独立的 MCP C0a 快照导入复用上述来源与现有 MCP 配置 owner:Desktop 和根 CLI 可预览 OpenCode、Claude Code
与 Codex 中语义等价的安全声明,并在用户显式确认后原子写入 disabled 原生条目。凭据/header/env/cwd 迁移、
通用导入记录、undo、Peer/Remote 写入均未实现;这不改变外部 MCP 持续兼容来源的运行路径。
独立的 MCP C0a 快照导入复用上述来源与现有 MCP 配置 owner:Desktop 和根 CLI 可预览 OpenCode、Claude Code、
Codex 与 DeepSeek Harness 中受支持的安全声明,并在用户显式确认后原子写入 disabled 原生条目。静态 env/header
值随私有投影复制,来源信息保存在原生配置中;GUI 可批量导入并撤销本机副本。动态凭据引用解析和 Peer/Remote
写入仍不支持;这不改变外部 MCP 持续兼容来源的运行路径。

## 0. 当前 MCP 快照导入契约(C0a)

快照导入是显式复制,不是持续同步,也不改变现有外部 MCP 兼容来源。Desktop 与根 CLI 只负责展示脱敏预览并发送
typed intent;OpenCode、Claude Code 与 Codex sibling adapter 复用各自已合并的解析结果生成私有安全投影,外部来源
typed intent;OpenCode、Claude Code 与 Codex sibling adapter 复用各自已合并的解析结果,DeepSeek Harness adapter 读取独立的完整声明,生成私有安全投影。外部来源
协调器固定当前 candidate 与行为版本,core 负责重新规划,最终仍由唯一 MCP 配置 service 校验并写入
`mcp_servers`。Codex 的投影与其运行准备共用同一当前 candidate/version fencing,不建立第二套解析或缓存。

Expand All @@ -43,36 +44,83 @@ native ID、disposition 和稳定 reason code,不包含 command arguments、UR
`MCPServerConfig`。provider 私有投影不可序列化且使用 redacted `Debug`;plan/request 最多包含 256 个 candidate,未知请求
字段与重复选择直接拒绝。

发现任务在进程共享并发预算下排队,并保留原有扫描及延迟完成期限。一次刷新中的提供方超过 worker 数量时不能被
直接丢弃为过载;导入预览也不能把待完成或调度失败的扫描当作完整目录。

当前只复制能够与原生配置保持等价语义的声明:

- 无显式 environment/cwd 的 local stdio command 与 adapter 已解析 arguments;
- 无 userinfo、query、fragment、header、bearer token 或 provider OAuth 变化的 HTTPS streamable HTTP URL。
- local stdio command、adapter 已解析 arguments、静态 environment 和绝对 working directory;
- 无 userinfo、query、fragment 的 HTTPS streamable HTTP URL、静态 headers,保留显式 OAuth discovery 开关;
- adapter 已校验的 startup/catalog/execution 毫秒超时。原生 JSON 的读写与再次保存均保留这些可选字段,旧配置缺省语义不变。

environment 值或引用、header/authorization、cwd、未知字段和其他 transport 不猜测、不复制、不记录。导入条目始终为
environment/header 的静态值仅经不可序列化、Debug 脱敏的私有投影复制到原生配置,并纳入计划摘要;目录和确认界面只展示字段名。
未解析的变量或凭据引用、未知字段和其他 transport 不猜测、不复制。导入条目始终为
`enabled: false` 与 `autoStart: false`;local 条目不继承完整父进程环境,只保留 MCP runtime owner 提供的安全环境。
Codex 的 legacy `name` 是上游忽略的展示字段,不进入导入结果或行为版本;`startup_timeout_sec`(含旧
`startup_timeout_ms`)和 `tool_timeout_sec` 可进入受审批保护的兼容运行投影,但当前原生快照格式不能无损保留它们,
因此仍会阻断 C0a 导入。`enabled_tools`、`disabled_tools`、approval、environment/scopes/OAuth 与并行调用等运行敏感字段
`startup_timeout_ms`)和 `tool_timeout_sec` 同时进入受审批保护的兼容运行投影和原生快照导入。
`enabled_tools`、`disabled_tools`、approval、scopes/OAuth 凭据与并行调用等运行敏感字段
仍按不支持处理,不能因静态发现成功而丢弃语义后导入。
Codex 未显式声明 cwd 时,其兼容运行投影仍会把当前 workspace 作为 effective cwd;现有原生快照格式不会保留这项隐式
语义,因此 workspace 场景的 local 声明返回“需要设置”,不能以“没有 cwd 字段”为由导入后继承 OpenBitFun 进程目录。
Codex 和 OpenCode 未显式声明 cwd 时,当前 workspace 产生的 effective cwd 也会随导入保留;不能因上游未写 cwd
就改为继承 OpenBitFun 进程目录。工作目录仅通过私有投影传到配置 owner,不加入公开预览。

DeepSeek Harness 读取 `DSH_HOME`(缺省 `~/.dsh`)及其 `profiles/*`、选中 workspace 下的 `cordis.yml` 和
`cordis.patch.yml` 中显式 MCP 声明;可识别普通 group 和无目标 insert,但不合成原生 profile 或加载 bundle。
每个文件独立复用完整 `@deepseek-ai/dsh-mcp-client` 配置,目录中其他 profile 不会被视为当前启用 profile。
相对 cwd 按选中 workspace 解析;缺少该上下文时不猜测。保留默认 60 秒 tool timeout,并关闭此来源未提供的 OAuth
discovery。动态 YAML tag、需合成的 partial patch、重复声明、作用域/生命周期字段、显式 reconnect policy 和
`failOnStartupError: true` 显示不支持,不执行插件。OpenBitFun MCP owner 负责导入后的连接、重连与启停。
PI 本轮未接入 MCP provider。

native ID 优先使用外部 logical name,再使用稳定生态后缀和最小可用数字后缀;超长名称使用 bounded digest,已有条目
永不覆盖。plan fingerprint 同时绑定脱敏 plan、私有投影和当前原生 MCP 配置摘要。apply 会重新发现并重建 plan;来源或
目标内容变化时返回刷新后的脱敏 plan,且不写入;fingerprint 不绑定 coordinator refresh generation,因此内容未变的刷新
不会让 plan stale。配置 service 通过同一 JSON key 的 compare-and-set mutation lane
一次提交全部选中条目或全部不提交,并在 `_openbitfunImport` 中只保留 source-qualified candidate IDbehavior version。
普通 MCP 编辑保留这段 provenance,删除条目时随条目一并移除。
一次提交全部选中条目或全部不提交,并在 `_openbitfunImport` 中保留 source-qualified candidate IDbehavior version 和可选 sourceId
普通 MCP 编辑保留这段 provenance,删除条目时随条目一并移除。旧记录缺少 sourceId 时,Desktop 从 candidate 的完整稳定 ID 与已注册 provider identity 恢复来源显示,不依赖外部文件仍在线。

根 CLI 的 `openbitfun mcp import` 默认只预览,`--apply` 导入全部 eligible 项;重复 `--candidate` 可缩小集合,单一选择可用
`--native-id` 指定目标 ID,`--format json` 输出 versioned plan/result。当前没有 TUI/Mobile/Server/Peer/Remote/ACP/SDK
写入口、导入 journal、tombstone、undo、外部应用回写或插件安装/激活策略;导入后仍由既有 MCP manager 完成复核、编辑、
写入口、导入 journal、tombstone、外部应用回写或插件安装/激活策略;Desktop 可审阅并撤销选定原生副本,仍由既有 MCP manager 完成复核、编辑、
启用和删除。

Desktop 的导入卡默认选中当前 plan 中全部 eligible 项,用户可在原卡片内取消个别条目;每项同时显示来源生态和
用户/项目使用范围,不增加新的向导或主选择器。apply 只发送当前选中 candidate。若并发来源或目标配置变化导致 plan
stale,界面替换为服务端返回的新 plan,并只保留“旧选择与新 eligible candidate 的交集”;新出现的 candidate 不自动
勾选,避免一次旧确认扩大到用户未见过的内容。取消、完成或切换作用域会清空这份易失选择。
Desktop 的生态兼容页按所选 Agent 隔离外部内容。查看不会创建原生条目;用户可单项导入、导入当前类别或一键导入该 Agent 的全部可用项,审阅来源、目标 ID
和默认禁用状态后确认。MCP 选择集合一次提交;Skill/Hook 保留逐项成功、跳过和失败结果。Hook 在批量执行期间仅刷新目标 revision,源行为和精确命令必须与审阅内容相同。
其他 Agent 的 candidate 不进入此次选择。来源或目标变化导致 plan
stale 时,保留同一 candidate 的更新预览并要求重新确认;不自动加入新条目,也不隐式重试写入。切换 Agent、工作区、
Peer 主机或取消操作会清空详情与确认状态,迟到的旧请求不能更新新作用域。

### 0.1 外部内容与原生管理的界面边界

生态兼容页独立展示所选 Agent 的 Skill、MCP、Hook 和其他已发现目录项,不嵌入原生 Skill/MCP/Hook 管理页。
来源设置仅列出所选生态;范围级总开关明确标注其影响,保留既有兼容策略,不把发现动作当作导入。
ACP 配置嵌入时也只展示所选产品的 clients,并隐藏包含全部产品内容的 JSON 视图。

- Skill 使用现有扫描报告按稳定 sourceId 归属筛选,展示该生态的诊断。目录包复制完整依赖,PI/DSH 支持的 Markdown 单文件转换为独立 SKILL.md 包;包内链接依赖明确拒绝。
当前 Host 通过扫描报告的可选 importOperationsVersion 协商新导入能力;旧 Host 保留原有目录导入路径。
版本 2 支持可选 targetName:确认页可为同名项指定独立目录及调用名称,仅修改副本的 frontmatter name;旧 Host 不接收改名请求。
add_skill 的 sourceKey 对应已发现来源;复制先在临时目录校验、写入 `.openbitfun-import.json`,再发布到用户/项目原生目录。
来源 ID、位置、解析方言、内容摘要和导入 ID 随副本保存;sourceId 仍为 OpenBitFun,原始来源单独用于标签和筛选。
发现目录与原生运行时目录分离:未导入的外部 Skill 不进入对话候选、模式技能列表、提示词目录或名称/key 调用入口;旧来源 key 不会自动重定向到副本。
本地与远程工作区执行相同过滤,撤销后重新解析即失去调用资格。已批准插件通过独立发布 owner 提供的 Skill 贡献保持原有执行契约。
原生用户副本优先于外部用户发现,项目优先于用户的规则保持不变。PI 单文件的缺省名称来自文件名,扫描与实际加载使用相同规则。
同源重复导入保持现有副本及用户修改;不同内容或其他来源的同名目标拒绝覆盖。明确重新导入可为内容完全相同的旧副本补齐来源。
撤销核对导入 ID 并与发布共用跨进程锁,仅删除审阅的原生副本,外部原文件保留;来源标记损坏时保留文件并显示诊断。
- 批量操作支持当前 Agent 全部、分类以及勾选项,确认前列出目标和改名结果,完成后保留逐项成功/失败及具体错误。
一键导入/撤销弹窗按 Skill、MCP、Hook 分组,限制窗口高度并只滚动清单;进度以实际返回结果数推进(失败也计入已处理),列表刷新期间保留进度与操作区。
技能套件的列表、计数、分组编辑及保存后刷新均仅消费原生技能与已导入副本,外部发现项不自动进入套件。
批量 MCP 撤销把相同配置指纹下的删除合并为一次 CAS;Hook 撤销仅沿本批成功操作返回的版本推进,其他修改仍触发冲突。
弹窗关闭时保留最后一次完整内容直到退场结束,避免标题、正文和页脚先被清空。
- Hook 按 ecosystemId 和 source key 展示只读目录。Claude Code/Codex 支持的来源经既有 plan/apply 精确命令审阅导入;
其他形态只展示信息。原生 Hook 页仅管理已导入来源及原生启用设置,后续更新仍需明确确认。
- 原生 Skill 页只展示 OpenBitFun 自有目录和内置内容(含导入后的副本),按导入来源提供筛选;原生 MCP 页展示来源标签。
外部 Command/Tool/Subagent 等尚无快照导入能力的类型展示限制,不用原生管理入口冒充导入实现。
- 导入前端在不支持写入的 Server/Peer/Remote 环境明确禁用,继续通过现有宿主 API 展示可取得的目录;不回退到控制端文件。
已保存的兼容运行策略和用户数据不因发现失败或不支持而重置。

“支持发现”不表示支持快照导入或已经可执行。当前五种生态均有 Skill 发现;MCP provider 覆盖 OpenCode、Claude Code、Codex、DSH,PI 暂不支持。
五种生态的 Hook 目录与同步 command 导入范围分开呈现,后者仅覆盖 Claude Code/Codex 的受支持子集。
Command/Tool/Subagent 的持续兼容能力继续由原有归属模块控制,此页不提供其快照导入。
MCP/Hook 的“已导入”只表示已保存,界面提示到原生管理页启用与连接;实际执行仍需满足原生运行时的状态与策略。

## 1. 产品判断与竞品启示

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,7 @@
"visibility": "documented",
"rustPath": "add_skill",
"sourceFile": "src/apps/desktop/src/api/skill_api.rs",
"signature": "fn add_skill( _state: State<'_, AppState>, source_path: String, level: String, workspace_path: Option<String>, ) -> Result<String, String>",
"signature": "fn add_skill( _state: State<'_, AppState>, source_path: String, level: String, workspace_path: Option<String>, source_key: Option<String>, target_name: Option<String>, ) -> Result<String, String>",
"remoteWorkspacePolicy": "LegacyUnaudited"
},
{
Expand Down Expand Up @@ -1777,7 +1777,7 @@
"visibility": "documented",
"rustPath": "delete_skill",
"sourceFile": "src/apps/desktop/src/api/skill_api.rs",
"signature": "fn delete_skill( state: State<'_, AppState>, skill_key: String, workspace_path: Option<String>, ) -> Result<String, String>",
"signature": "fn delete_skill( state: State<'_, AppState>, skill_key: String, workspace_path: Option<String>, expected_import_id: Option<String>, ) -> Result<String, String>",
"remoteWorkspacePolicy": "LegacyUnaudited"
},
{
Expand Down
1 change: 1 addition & 0 deletions scripts/core-boundaries/rules/source/forbidden-rules.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4230,6 +4230,7 @@ export const forbiddenContentUnderRules = [
allowPaths: [
'src/crates/adapters/static-hook-support/tests/parser.rs',
'src/crates/adapters/dsh-adapter/src/hook_source.rs',
'src/crates/adapters/dsh-adapter/src/mcp_source.rs',
'src/crates/adapters/pi-adapter/src/hook_source.rs',
'src/crates/adapters/opencode-adapter/src/hook_source.rs',
'src/crates/adapters/opencode-adapter/src/command_source.rs',
Expand Down
13 changes: 13 additions & 0 deletions scripts/core-boundaries/rules/source/public-api-rules.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1195,6 +1195,19 @@ export const externalSourceCorePublicApiEntries = [
'Desktop external-source configuration host adapter',
true,
),
{
symbol: 'ecosystem_for_imported_mcp_candidate',
owner: 'openbitfun-core external MCP provider registration composition',
consumer: 'Desktop MCP list projection for legacy native import receipts',
verification: 'core imported_mcp_legacy_receipt_keeps_registered_origin_without_discovery test',
p0: 'preserve source identity for existing native MCP imports',
contractSlice: contractSlices.externalSourceMcpContract,
wireImpact: false,
rationale:
'resolve an optional ecosystem id from registered provider identity without discovery, runtime activation, or adapter types in the host API',
exit:
'remove when legacy import receipts no longer need source identity resolution or an owner replacement preserves offline provenance',
},
...[
'unacknowledged_external_ecosystems',
'acknowledge_external_ecosystems',
Expand Down
13 changes: 13 additions & 0 deletions src/apps/desktop/src/api/mcp_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ use tauri::State;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MCPServerInfo {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub import_origin: Option<openbitfun_core::service::mcp::server::MCPImportOrigin>,
pub id: String,
pub name: String,
pub status: String,
Expand Down Expand Up @@ -280,6 +282,17 @@ pub async fn get_mcp_servers(state: State<'_, AppState>) -> Result<Vec<MCPServer
};

infos.push(MCPServerInfo {
import_origin: config
.settings
.get("_openbitfunImport")
.cloned()
.and_then(|value| serde_json::from_value::<openbitfun_core::service::mcp::server::MCPImportOrigin>(value).ok())
.map(|mut origin| {
if origin.source_id.is_none() {
origin.source_id = openbitfun_core::external_sources::ecosystem_for_imported_mcp_candidate(&origin.source_candidate_id);
}
origin
}),
id: config.id.clone(),
name: config.name.clone(),
status,
Expand Down
Loading