From 59350b29b1d9558a753beb7600a1491505c96372 Mon Sep 17 00:00:00 2001 From: servicememeai Date: Tue, 27 Oct 2026 13:26:40 +0900 Subject: [PATCH 1/2] feat: add provider-neutral ApplicationProfile V1 and nonbreaking Bridge contracts --- README.md | 17 +- crates/local-bridge-core/src/lib.rs | 8 +- crates/local-bridge-core/src/profile.rs | 262 ++++++++++++++++++++++++ crates/local-bridge/src/main.rs | 19 +- docs/APPLICATION_PROFILE_V1.md | 52 +++++ docs/ARCHITECTURE.md | 10 +- docs/BRIDGE_TASK_V1.md | 69 +++++++ docs/CAPABILITY_AND_MCP_V1.md | 73 +++++++ docs/CLOUDFILE_TO_BRIDGE_MIGRATION.md | 84 ++++++++ docs/MCP.md | 4 + docs/ROADMAP.md | 47 +++-- docs/SECURITY.md | 6 + examples/application-profile.json | 53 +++++ examples/enterprise-channels.json | 24 +++ examples/etech-channels.json | 22 -- 15 files changed, 699 insertions(+), 51 deletions(-) create mode 100644 crates/local-bridge-core/src/profile.rs create mode 100644 docs/APPLICATION_PROFILE_V1.md create mode 100644 docs/BRIDGE_TASK_V1.md create mode 100644 docs/CAPABILITY_AND_MCP_V1.md create mode 100644 docs/CLOUDFILE_TO_BRIDGE_MIGRATION.md create mode 100644 examples/application-profile.json create mode 100644 examples/enterprise-channels.json delete mode 100644 examples/etech-channels.json diff --git a/README.md b/README.md index 8979f05..b165be8 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,21 @@ Local Bridge 提供一个 **最小、受控的本机执行接口**: - 后续企业功能以 **Channel manifest + 本机能力白名单**扩展,不允许远程服务器下发 Shell/EXE 命令。 - 优先支持 Windows 普通用户与便携运行、macOS;Rust 核心预留 Linux。支持企业服务端 HTTP 和 HTTPS,敏感授权另行处理。 +## 通用 Profile 与长期演进合同 + +**本仓只认识通用 ApplicationProfile,不内建任何企业应用的业务规则。** 企业应用自行发布和维护其 Profile;CloudFile 与 Bridge 则可以双向适配文件能力。 + +- [ApplicationProfile V1](docs/APPLICATION_PROFILE_V1.md):应用配置、可信来源、Dev/Prod、更新及稳定身份 +- [BridgeTask V1](docs/BRIDGE_TASK_V1.md):统一文件/企业/AI 任务中心合同 +- [Capability / MCP / Pi V1](docs/CAPABILITY_AND_MCP_V1.md):通用工具、Pi RPC、MCP 权限 +- [CloudFile → Bridge 迁移](docs/CLOUDFILE_TO_BRIDGE_MIGRATION.md):并行开发不接管或破坏现有文件能力 + +```sh +cargo run -p local-bridge -- validate-profile examples/application-profile.json +``` + +`validate-profile` **仅验证 JSON 结构**,返回 `trusted=false` / `activated=false`;不会下载配置或注册 Host。现有 `validate-channel`、MCP 的 `bridge.status` 和 `cloudfile.status` 完全保持原样。 + ## 结构 ```text @@ -66,7 +81,7 @@ An MCP client can launch `local-bridge mcp` with **stdio**, without any listenin Example Channel configuration (not authenticated or automatically fetched): ```bash -cargo run -p local-bridge -- validate-channel examples/etech-channels.json +cargo run -p local-bridge -- validate-channel examples/enterprise-channels.json ``` Read [security boundaries](docs/SECURITY.md) before integrating Web or AI workloads. Schema validation alone never authenticates an enterprise server. diff --git a/crates/local-bridge-core/src/lib.rs b/crates/local-bridge-core/src/lib.rs index d89883f..1d2a1f1 100644 --- a/crates/local-bridge-core/src/lib.rs +++ b/crates/local-bridge-core/src/lib.rs @@ -1,5 +1,7 @@ //! The small trust boundary shared by the core, MCP adapter and enterprise //! integrations. A remote Channel manifest is presentation, NOT code. +pub mod profile; + use anyhow::{bail, Result}; use serde::{Deserialize, Serialize}; use std::collections::BTreeSet; @@ -130,8 +132,8 @@ mod tests { fn sample() -> ChannelManifest { ChannelManifest { schema: 1, - provider: "etech".into(), - origin: "http://10.9.8.162".into(), + provider: "sample".into(), + origin: "http://192.0.2.10".into(), expires_at: 5000, channels: vec![ Channel { @@ -153,7 +155,7 @@ mod tests { } #[test] fn accepts_enterprise_http_and_https_without_implicit_downgrade() { - for url in ["http://10.9.8.162", "https://files.example.test:8443"] { + for url in ["http://192.0.2.10", "https://files.example.test:8443"] { let mut x = sample(); x.origin = url.into(); validate_manifest(&x, 4000).unwrap(); diff --git a/crates/local-bridge-core/src/profile.rs b/crates/local-bridge-core/src/profile.rs new file mode 100644 index 0000000..dc0957e --- /dev/null +++ b/crates/local-bridge-core/src/profile.rs @@ -0,0 +1,262 @@ +//! Additive, provider-neutral ApplicationProfile V1 data contract. +//! Validation is structural only: no remote manifest, URL, or capability is +//! trusted merely because it parses. Existing ChannelManifest remains intact. +use crate::{identifier, valid_origin}; +use anyhow::{bail, Result}; +use serde::{Deserialize, Serialize}; +use std::collections::{BTreeMap, BTreeSet}; +use url::Url; + +pub const PROFILE_SCHEMA_VERSION: u32 = 1; + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct ApplicationProfile { + pub schema_version: u32, + /// Stable instance ID; never derived from a URL, file path, or client version. + pub profile_id: String, + pub provider: String, + pub environment: String, + pub revision: u64, + pub expires_at: u64, + pub application: ApplicationBrand, + /// Trusted enrollment must separately bind this origin and the user. + pub origin: String, + #[serde(default)] + pub services: BTreeMap, + #[serde(default)] + pub channels: Vec, + #[serde(default)] + pub update: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct ApplicationBrand { + pub id: String, + pub name: String, + #[serde(default)] + pub logo_url: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct ProfileChannel { + pub id: String, + pub title: String, + pub kind: ProfileChannelKind, + #[serde(default)] + pub module: Option, + #[serde(default)] + pub entry: Option, + /// Requested capability IDs, not granted permissions. + #[serde(default)] + pub capabilities: Vec, + #[serde(default = "default_enabled")] + pub enabled: bool, + #[serde(default)] + pub optional: bool, +} + +fn default_enabled() -> bool { + true +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum ProfileChannelKind { + Web, + Native, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct ProfileUpdate { + pub track: String, + /// Locally pinned update source name, never a URL or a signing key. + pub source_id: String, +} + +fn valid_endpoint(endpoint: &str) -> bool { + endpoint.len() <= 2048 + && Url::parse(endpoint).is_ok_and(|url| { + matches!(url.scheme(), "http" | "https") + && url.username().is_empty() + && url.password().is_none() + && url.fragment().is_none() + }) +} + +fn valid_logo(profile_origin: &str, value: &str) -> bool { + if !valid_endpoint(value) { + return false; + } + match (Url::parse(profile_origin), Url::parse(value)) { + (Ok(profile), Ok(logo)) => { + logo.scheme() == "https" + || (logo.scheme() == "http" && logo.origin() == profile.origin()) + } + _ => false, + } +} + +fn valid_web_entry(value: &str) -> bool { + value.starts_with('/') + && !value.starts_with("//") + && !value.contains("..") + && value.len() <= 256 + && value + .bytes() + .all(|b| b.is_ascii_alphanumeric() || matches!(b, b'/' | b'_' | b'-')) +} + +/// Offline shape validation only; it does NOT enroll, authenticate or activate. +pub fn validate_profile_shape(profile: &ApplicationProfile) -> Result<()> { + if profile.schema_version != PROFILE_SCHEMA_VERSION + || !identifier(&profile.profile_id) + || !identifier(&profile.provider) + || !identifier(&profile.environment) + || profile.revision == 0 + || profile.expires_at == 0 + || !valid_origin(&profile.origin) + { + bail!("invalid profile version, identity, revision, expiry or origin"); + } + if !identifier(&profile.application.id) + || profile.application.name.is_empty() + || profile.application.name.chars().count() > 100 + || profile.application.name.chars().any(char::is_control) + { + bail!("invalid application metadata"); + } + if profile + .application + .logo_url + .as_deref() + .is_some_and(|logo| !valid_logo(&profile.origin, logo)) + { + bail!("invalid logo URL"); + } + if profile.services.len() > 16 + || profile + .services + .iter() + .any(|(name, url)| !identifier(name) || !valid_endpoint(url)) + { + bail!("invalid service endpoint declaration"); + } + if profile.channels.len() > 32 { + bail!("too many channels"); + } + if let Some(update) = &profile.update { + if !identifier(&update.track) || !identifier(&update.source_id) { + bail!("invalid update track or pinned source ID"); + } + } + let mut seen = BTreeSet::new(); + for channel in &profile.channels { + if !identifier(&channel.id) + || !seen.insert(&channel.id) + || channel.title.is_empty() + || channel.title.chars().count() > 80 + || channel.title.chars().any(char::is_control) + || channel.capabilities.len() > 16 + || !channel.capabilities.iter().all(|cap| identifier(cap)) + { + bail!("invalid or duplicate channel"); + } + match channel.kind { + ProfileChannelKind::Web => { + if channel.module.is_some() + || !channel.entry.as_deref().is_some_and(valid_web_entry) + { + bail!("web channel requires an origin-relative entry only"); + } + } + ProfileChannelKind::Native => { + if channel.entry.is_some() + || !channel.module.as_deref().is_some_and(identifier) + { + bail!("native channel requires a known module ID only"); + } + } + } + } + Ok(()) +} + +/// Enrollment checks freshness separately from the shape and MUST also verify +/// the profile's trusted origin/signature; neither function grants capabilities. +pub fn validate_profile_fresh(profile: &ApplicationProfile, now: u64) -> Result<()> { + validate_profile_shape(profile)?; + if profile.expires_at <= now || profile.expires_at > now.saturating_add(7 * 86400) { + bail!("profile expired or lifetime too long"); + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + + fn example() -> ApplicationProfile { + serde_json::from_value(json!({ + "schema_version": 1, + "profile_id": "sample-prod", + "provider": "sample", + "environment": "prod", + "revision": 1, + "expires_at": 5000, + "application": {"id": "sample", "name": "Sample App", "logo_url": "https://assets.example.test/logo.png"}, + "origin": "http://192.0.2.10", + "services": {"files": "http://192.0.2.10/files"}, + "channels": [ + {"id": "files", "title": "Files", "kind": "native", + "module": "cloudfile", "capabilities": ["cloudfile.status"]}, + {"id": "ai", "title": "AI", "kind": "web", "entry": "/ai"} + ], + "update": {"track": "stable", "source_id": "trusted-stable"} + })) + .unwrap() + } + + #[test] + fn accepts_generic_profile_without_activating_it() { + let x = example(); + validate_profile_shape(&x).unwrap(); + validate_profile_fresh(&x, 4000).unwrap(); + assert_eq!(x.channels.len(), 2); + assert_eq!(x.profile_id, "sample-prod"); + } + + #[test] + fn rejects_untrusted_urls_and_remote_commands() { + let mut x = example(); + x.channels[0].module = Some("powershell -e".into()); + assert!(validate_profile_shape(&x).is_err()); + x.channels[0].module = Some("cloudfile".into()); + x.channels[1].entry = Some("//outside.example/run".into()); + assert!(validate_profile_shape(&x).is_err()); + x.channels[1].entry = Some("/ai".into()); + x.application.logo_url = Some("http://outside.example/logo.png".into()); + assert!(validate_profile_shape(&x).is_err()); + x.application.logo_url = None; + x.update.as_mut().unwrap().source_id = "https://remote/updates".into(); + assert!(validate_profile_shape(&x).is_err()); + } + + #[test] + fn rejects_duplicate_capabilities_unknown_fields_and_expired_profiles() { + let mut x = example(); + x.channels[1].id = "files".into(); + assert!(validate_profile_shape(&x).is_err()); + x.channels[1].id = "ai".into(); + assert!(validate_profile_fresh(&x, 5000).is_err()); + assert!(validate_profile_fresh(&x, 0).is_ok()); + assert!(serde_json::from_str::( + r#"{"schema_version":1,"profile_id":"x","unknown":"shell"}"# + ) + .is_err()); + } +} diff --git a/crates/local-bridge/src/main.rs b/crates/local-bridge/src/main.rs index 36f1685..1ec8cce 100644 --- a/crates/local-bridge/src/main.rs +++ b/crates/local-bridge/src/main.rs @@ -2,6 +2,7 @@ mod cloudfile; mod mcp; use anyhow::{bail, Context, Result}; +use local_bridge_core::profile::{validate_profile_shape, ApplicationProfile}; use local_bridge_core::{ validate_manifest, visible_channels, ChannelManifest, AVAILABLE_CAPABILITIES, }; @@ -45,9 +46,25 @@ fn run() -> Result<()> { "visible_channels":visible_channels(&value, AVAILABLE_CAPABILITIES)}) ); } + [command, filename] if command == "validate-profile" => { + // Shape-only: a local file is not proof of trusted origin or authorization. + let path = Path::new(filename); + let metadata = std::fs::metadata(path)?; + if !metadata.is_file() || metadata.len() > 64 * 1024 { + bail!("invalid application profile file size"); + } + let value: ApplicationProfile = serde_json::from_slice(&std::fs::read(path)?)?; + validate_profile_shape(&value)?; + println!( + "{}", + json!({"shape_valid":true, "trusted":false, "activated":false, + "profile_id":value.profile_id, + "channels":value.channels.len()}) + ); + } [] | [_] if args.first().is_none_or(|s| s == "--help" || s == "-h") => { println!( - "Local Bridge {}\nCommands: status | mcp | validate-channel | --version", + "Local Bridge {}\nCommands: status | mcp | validate-channel | validate-profile | --version", env!("CARGO_PKG_VERSION") ); } diff --git a/docs/APPLICATION_PROFILE_V1.md b/docs/APPLICATION_PROFILE_V1.md new file mode 100644 index 0000000..6d8b490 --- /dev/null +++ b/docs/APPLICATION_PROFILE_V1.md @@ -0,0 +1,52 @@ +# ApplicationProfile V1 — 通用应用配置合同 + +状态:**V1 合同与离线结构校验已提供;注册、下载、可信绑定和动态生效尚未实现**。这是跨 CloudFile Agent 与 Local Bridge 演进时必须保留的逻辑合同。 + +## 1. 角色边界 + +- **企业应用**拥有自己的业务身份、组织权限、项目对象、AI 服务和服务器配置,通过 Profile 向本地申请能力。Local Bridge 不需要认识任何具体企业应用名称或业务数据库。 +- **Local Bridge**负责 Profile 结构、受信应用注册、本机能力发现、任务聚合、用户确认、MCP 工具暴露和客户端生命周期。不实施企业项目权限,也不代发 CloudFile 文件版本。 +- **CloudFile**与 Local Bridge 可双向集成:Bridge 发现并路由 CloudFile 文件能力;CloudFile Agent 可声明 Bridge 所需的能力与任务摘要。CloudFile 的文件会话/Journal/Guard 是文件操作的唯一事实源。 + +应用定制信息位于**应用侧**;通用 Schema 与验证器只存在于 Local Bridge。禁止把特定应用的固定服务地址、账号规则、项目字段和业务路由写入 Bridge 核心。 + +## 2. 唯一配置合同及本地生效 + +1. `ApplicationProfile` 是服务端发布的声明,不含账号 Token、更新签名公钥、本机 EXE 路径或任何可执行脚本。 +2. `LocalBinding` 是本机安装/注册状态:`profile_id` → 受信 origin、当前用户和设备身份、已安装模块、开发/生产隔离目录、允许的发布源 ID。它不是另一套服务器 Profile。 +3. `EffectiveProfile` 是运行时的**派生视图**:经过认证的 ApplicationProfile ∩ 实际安装模块 ∩ 服务端当前对象授权 ∩ 本地用户许可。不要以它覆盖 ApplicationProfile,也不要持久化一份相互冲突的业务配置。 +4. `TaskContext` 随单个任务传递业务上下文。Bridge 只处理明确的通用字段;额外的 `context` 为应用模块拥有的不可执行、不可信数据,不能直接解释成文件路径或 Shell 参数。 + +## 3. V1 JSON 字段 + +参见 [通用示例](../examples/application-profile.json)。JSON Schema 对应 `crates/local-bridge-core/src/profile.rs`,解析器默认拒绝未定义字段。 + +| 字段 | 要求 | 说明 | +|---|---|---| +| `schema_version` | 固定 1 | V1 格式主版本;不符合则拒绝,不静默转型 | +| `profile_id` | 稳定应用**实例** ID | 同一部署跨客户端/架构升级**保持不变**;Dev、Prod 各有独立 ID | +| `provider` / `environment` | 通用标识 | 应用提供者与环境;Bridge 不识别特定厂商 | +| `revision` / `expires_at` | 正整数 / Unix 秒 | 版本递增;受信生效时检查有效期(最大七天) | +| `application.id/name/logo_url` | 应用 ID、名称、Logo | Logo 可使用 HTTPS,内网 HTTP 仅限同源;Logo 不提供执行权限 | +| `origin` | 规范 HTTP(S) origin | 唯一来源身份基准;仅 JSON 校验不构成可信绑定 | +| `services` | 服务名 → HTTP(S) 地址 | 表示拟使用的业务 API 地址;跨域服务仍需单独可信授权 | +| `channels` | 最多 32 个 | `web` 为同源相对路由;`native` 为受信模块 ID;`capabilities` 仅表示**请求** | +| `update.track/source_id` | 发布轨道及本地钉住源 ID | 不能以远端 Profile URL 或公钥改变本机更新信任根 | + +每个 Channel:`id`、`title`、`kind`(`web`/`native`)、`capabilities`、`enabled`(默认 true)、`optional`(默认 false)。`web` 必须给同源相对 `entry`;`native` 必须给 `module`,**不能**提供程序路径、参数或命令行。 + +当服务器未来需要新增字段或能力时,优先增加可选 Channel 与能力 ID;需要修改 V1 解析语义时显式引入新 `schema_version`,由服务端协商兼容版本。升级不能更换现有 `profile_id`。 + +## 4. 安全与升级规则 + +- **离线解析 ≠ 可信注册。** `local-bridge validate-profile examples/application-profile.json` 只返回 `shape_valid`,`trusted=false`、`activated=false`;示例失效时间不代表真实授权。 +- 正式启用前需由用户/企业部署显式建立受信 origin 与本机身份绑定,从可信会话取得配置;HTTP 内网配置须额外有受信签名或等价完整性保护。缓存旧 Profile 仅供必要显示,不授予新的写权限。 +- `capabilities` 不授予本地程序运行、读取文件、CloudFile 写回或 AI 工具调用权;每次操作均经过本机允许列表和拥有该业务数据的服务端授权。没有安装的模块必须显示不可用,不伪装成功。 +- Profile 修改名称、Logo、菜单无需升级二进制;程序更新复用本机钉住的签名清单、健康检查和回退机制。活动 Checkout、未确认发布时不得静默替换文件引擎。 +- 不同环境的 Profile、浏览器扩展、Native Host、工作区、凭据与更新源须隔离,不能凭网页请求自动切换正式/开发 Host。 + +## 5. V1 契约与现有运行代码 + +`ChannelManifest` 与旧 `validate-channel` 继续保留,属于已有 0.1.0 离线验证接口,**不自动迁移、不被悄悄重解释**。新增 ApplicationProfile 是目标统一合同;可信服务端获取、LocalBinding 存储、自动投影和任务执行为后续独立增量。 + +完成标准:通用 Profile 示例可离线校验;旧 CLI/MCP 调用与 CloudFile 现有网页入口行为不发生改变。 diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index f857e7a..d8e00dc 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -4,18 +4,24 @@ Local Bridge is **not** another WorkBuddy/Claude/Copilot, an MCP gateway fleet, RPA suite or a SeaDrive replacement. It supplies deterministic desktop capabilities to authorized enterprise Web and AI clients. +## Three-party boundary + +Any enterprise application publishes a **generic** ApplicationProfile and owns its business objects, authentication and business permissions. Local Bridge must not depend on the name, data model or private code of the application. CloudFile and Bridge may know each other as mutually integrated capabilities: Bridge routes a file task; CloudFile Agent owns its authorized file execution, Journal and checkout state. No existing application must migrate or change its production behavior to develop Bridge in parallel. + +Target contracts: [ApplicationProfile](APPLICATION_PROFILE_V1.md), [Tasks](BRIDGE_TASK_V1.md), [Capability/MCP](CAPABILITY_AND_MCP_V1.md), [nonbreaking migration](CLOUDFILE_TO_BRIDGE_MIGRATION.md). + ## Components - **Rust Runtime**: one local capability registry, task gate, trusted clients, structured results, audit interfaces. Current repo starts with a small CLI; task supervisor and authorization are planned. - **MCP adapter**: stdio, no open network port by default. The adapter owns no file authorization. It invokes a local trusted runtime service, not shell text from the model. - **CloudFile application**: first adapter targets the **existing** cloudfile-local-agent2. The file module retains native Seafile protocol, authorization, checkout guard, Journal, CDC, upload receipt and rollback behavior. We do not fork it. -- **Enterprise Channels**: the enterprise server supplies declarative labels, Web route, requested capability IDs and UI metadata. Runtime only shows entries backed by trusted modules. It does not download programs. +- **Application Profiles / Enterprise Channels**: the enterprise server supplies declarative labels, Web route, requested capability IDs and UI metadata. Runtime only shows entries backed by trusted modules. It does not download programs. - **AI**: external MCP clients are optional. Core must function with no installed AI. ## Integration boundaries ```text -enterprise CloudFile/Etech API: identity, roles, object permissions, versions +enterprise application API + CloudFile Hub/Server: existing authorization | native browser/short-lived authorization + eventual enterprise channel config | diff --git a/docs/BRIDGE_TASK_V1.md b/docs/BRIDGE_TASK_V1.md new file mode 100644 index 0000000..c98391c --- /dev/null +++ b/docs/BRIDGE_TASK_V1.md @@ -0,0 +1,69 @@ +# BridgeTask V1 — 跨模块任务中心合同 + +状态:**接口与显示语义固定,任务聚合运行时尚未实现**。文件、企业应用、AI 任务使用一个任务中心,但不创建第二套文件编辑/AI 执行账本。 + +## 1. 所有权 + +任务由`owner_module`产生并对专业状态负责。Local Bridge 只负责聚合、展示、用户发起的操作分派与本机通知。 + +- `cloudfile`:原有 File ID/Branch、Journal、Intent、Guard、Checkout/Commit/Checkin 始终是发布事实源。 +- `enterprise`:企业 API/桌面程序的完成与失败由相应业务模块判定。 +- `ai` / `pi`:会话、推理、工具事件由企业 AI 服务或 Pi 管理;Bridge 只显示明确的 run/task 状态。 + +任务中心可以汇总正在执行的 AI run;**不要**把每条聊天消息、每次工具 token 或浏览器导航都永久记录成 BridgeTask。 + +## 2. 最小结构 + +```json +{ + "schema_version": 1, + "profile_id": "sample-app-prod", + "task_id": "owner-assigned-id", + "owner_module": "cloudfile", + "kind": "file", + "capability": "cloudfile.file.exclusive", + "title": "Edit drawing", + "context": {"object_id": "object-001"}, + "status": "needs_action", + "progress": null, + "available_actions": ["open", "commit", "checkin"] +} +``` + +- 身份键为 (`profile_id`, `owner_module`, `task_id`)。不要仅以文件名或原始云端路径匹配任务。 +- `kind` 为视图分类(如 `file`、`enterprise`、`ai`、`local_tool`),不是执行权限。 +- `context` 是应用拥有的提示/引用信息;通用任务 UI 只能展示已允许的字段,不解释成命令、账号凭据、工作目录。 +- `available_actions` 由拥有该任务的模块实时计算,不由网页或 AI 自行拼装。 +- `progress` 允许 null,未知或无法量化时不捏造百分比。 + +## 3. 通用状态 + +| `status` | 统一语义 | +|---|---| +| `queued` | 已接收、等待执行 | +| `running` | 模块正在执行,结果尚未确认 | +| `needs_action` | 用户必须确认、提交、解决冲突或重新授权 | +| `completed` | 拥有模块确认其事实链已完成 | +| `failed` | 已确认失败,但保留可恢复数据 | +| `cancelled` | 拥有模块确认终止 | +| `unknown` | 结果未知;不得假定成功或失败,不得因展示层超时重复执行 | + +CloudFile 的“本地已保存”**不等于**`completed`(云端已发布);独占编辑的已 Checkout 也不等于文件已 Commit。CloudFile 仍保留自己的详细状态,映射到 Bridge 状态时需附带可读说明及必要操作。 + +## 4. 任务接口(规划) + +- `list_tasks(profile_id?)`:按当前可信应用和权限展示任务摘要; +- `get_task(profile_id, owner_module, task_id)`:获取详情,敏感路径和票据不返回通用 UI / MCP; +- `task_events`:事件订阅和重连后快照,仅状态变化,不创建业务提交; +- `request_action(task_ref, action_id, args)`:转发给 owner 模块再次鉴权。发送成功**不等于**动作完成。 + +Bridge 只维护低成本索引/展示缓存;工作副本、快照、Intent、服务端原生版本和 Pi 会话各归原模块。不存在任务“迁入 Bridge 新表”的默认步骤。 + +## 5. 失败恢复与迁移 + +- 已存在的 CloudFile 工作副本/Journal/Guard 不动,Bridge 通过只读 task adapter 取得摘要。**先读后写**,写入功能独立验收。 +- 桥接不可用时,CloudFile 现有文件任务入口仍可独立工作;UI 应说明功能降级,不改写原任务状态。 +- 不允许 Bridge、Pi、MCP 因网络错误自动再发一次新的文件提交。结果未知时查询原操作;需人工重试也必须遵守 CloudFile 原有幂等合同。 +- 结束、退出、升级、卸载均不能删除未确认工作副本或自动释放仍有效的 Checkout。 + +V1 验收从一个真实 CloudFile 只读任务摘要开始,再扩展企业任务和 Pi run;无需先建通用任务数据库。 diff --git a/docs/CAPABILITY_AND_MCP_V1.md b/docs/CAPABILITY_AND_MCP_V1.md new file mode 100644 index 0000000..ce96f53 --- /dev/null +++ b/docs/CAPABILITY_AND_MCP_V1.md @@ -0,0 +1,73 @@ +# Capability / MCP / Pi V1 — 通用能力合同 + +状态:**设计基线**。当前 MCP stdio 已实装且仅开放 `bridge.status`、`cloudfile.status`;其他下述能力 ID/接口属于待实现目标,不可标记为已可用。 + +## 1. 依赖方向 + +```text +Enterprise App / Browser / AI Client + | authenticated app profile + per-action grant +Generic Browser Extension / MCP Client + | +Local Bridge (trusted caller + capability registry + task UI) + | | +CloudFile file module Pi RPC runtime (optional) + | | +existing Hub/Server Bridge MCP stdio tools +``` + +- Bridge **不导入**企业应用代码,不识别固定业务系统、项目表、员工标识或企业私有 API;ApplicationProfile 来自企业应用。 +- CloudFile 和 Bridge 可以相互知道,并通过稳定模块 ID、最小 IPC 与任务摘要适配;CloudFile 引擎仍拥有文件权限和版本操作。 +- Pi 作为可选独立进程。运行时可使用 Pi RPC 接入界面;Pi 作为 MCP Client 访问 Bridge 的受限工具;其他 MCP Client 同理。不要在 Rust 内实现第二个 AI Agent 引擎。 +- MCP Server 默认 `stdio`,不开放任意网站可调用的本机 TCP 端口。 + +## 2. 能力注册、广告与真实授权 + +能力 ID 使用命名空间(例如 `cloudfile.status`、`cloudfile.file.view`、`cloudfile.file.edit`、`cloudfile.file.exclusive`、`bridge.task.list`、`enterprise.action.invoke`、`ai.local.chat`)。 + +严格区分: +1. **Declared**:ApplicationProfile 请求使用的能力; +2. **Installed**:本地 Bridge 已有并可调用的能力模块; +3. **Available**:模块/服务健康,当前用户/来源允许操作; +4. **Authorized**:对本次对象/任务持有可信业务授权,包括 CloudFile 的文件票据和 ACL。 + +只有 4 同时成立才能执行。Web 入口可显示不代表具有本机命令权限。未知模块/能力为不可用而非默认透传。 + +## 3. 本地能力的类型安全合同 + +本地执行只接受已编译/已安装的注册能力、固定工具名称及受限参数结构。不得从远端 Profile、网页或模型提示中读取任意 shell、EXE 路径或 argv 并执行。 + +示例目标工具(**尚未实现**): + +| 工具 | 归属模块 | 最低授权 | +|---|---|---| +| `bridge.task.list` | Bridge | 当前可信 Profile 下的可见任务 | +| `cloudfile.task.list` | CloudFile | 当前用户文件任务摘要,去敏 | +| `cloudfile.file.open` | CloudFile | 既有短期只读文件授权 | +| `cloudfile.file.edit` | CloudFile | 受控写票据;本机启动不代表发布成功 | +| `cloudfile.file.exclusive` | CloudFile | 有效 ACL + Guard,用户明确 Checkout | +| `cloudfile.file.commit` | CloudFile | 既有 Journal/原生条件提交和人工确认 | +| `enterprise.action.invoke` | 企业适配模块 | 已注册动作 ID + 企业对象权限 + 必要确认 | + +首次上线优先只读任务、能力发现,再逐步开放写操作。不要为了 MCP 重新写 CloudFile 发布或独占逻辑。 + +## 4. AI 两种接入方向 + +**A. Bridge 调用本地 Pi** + +Bridge Desktop → Pi RPC (JSONL 子进程) → Pi 的既有会话、事件与模型配置;仅用户/企业启用后启动。BridgeTask 映射 run 级状态,AI 会话仍由 Pi 管理,不能让 Pi 改写 BridgeTask 的文件执行事实。 + +**B. Pi 或其他 AI 客户端调用 Bridge** + +Pi MCP Client → `local-bridge mcp` (stdio) → 注册的 Bridge/CloudFile 工具 → 原模块。Pi 官方已支持 stdio/HTTP MCP Client;无需自己再实现一套 Pi MCP 解析器。对工具的授权需要在 Bridge/目标服务实际执行时完成,MCP 的 tool annotations 不是最终鉴权。 + +**企业 AI 通道**:企业 AI 应用可通过 Profile 暴露其已有 Web/API;若将来需要访问本机能力,需经过用户绑定的可信网页转发或另行认证的 MCP 通道,不能把来自远程 AI 服务的指令默认为本机用户授权。 + +## 5. Dev/Prod 与默认限制 + +- Dev/Prod 使用不同的 Profile/Native Host/本机凭据/工作区,禁止跨环境查询、写回或回退。 +- 不因为安装了 Pi 就自动开放本机 shell、整个文件系统、模型凭据或企业密钥。 +- 文件查看与写入始终由 CloudFile 决定;企业动作由企业业务服务判断;Bridge 负责本机能力范围与用户确认。 +- 变更能力定义只增加小范围合同测试,真实文件发布仍由现有 CloudFile E2E 验收负责。 + +当前受支持的 MCP 工具及边界以 [MCP.md](MCP.md) 为准,不能将本设计表格误认为已实现功能。 diff --git a/docs/CLOUDFILE_TO_BRIDGE_MIGRATION.md b/docs/CLOUDFILE_TO_BRIDGE_MIGRATION.md new file mode 100644 index 0000000..71f8bd1 --- /dev/null +++ b/docs/CLOUDFILE_TO_BRIDGE_MIGRATION.md @@ -0,0 +1,84 @@ +# CloudFile Agent → Generic Local Bridge — 并行演进及零破坏约束 + +状态:**分阶段接入方案,未启动生产切换**。第一原则:通用 Bridge 的平行开发不得改变任何已投入使用的 CloudFile 与上层应用的实际行为。 + +## 1. 三者关系与唯一依赖方向 + +```text +Enterprise Application --publishes--> Generic ApplicationProfile + | | + +-- existing business APIs v + +-- CloudFile Hub/Server Local Bridge + | | + +-- current file flow +-- CloudFile adapter + | <--> CloudFile Agent2 + +-- optional Pi / MCP +``` + +**应用 → 配置提供方**;**Bridge → 通用本机运行时**;**CloudFile ↔ Bridge → 允许双向适配的文件能力**。 + +Bridge 不可硬编码上层应用名、项目字段、业务账号、员工组织、业务 API 端点,也不直接管理企业服务端权限。应用侧部署 Profile 与业务 Context;Bridge 只认识标准合同。 + +## 2. 必须保留的已用入口 + +以下是现有 CloudFile Agent2 文档记录的兼容界面,切换前以实际已安装版本再确认: + +- Windows 生产 Native Messaging Host:`com.cloudfile.current_agent`; +- macOS 正式 Host:`com.cloudfile.agent2`; +- Rust 开发 Host:`com.cloudfile.agent22.dev`,开发与正式不可互相覆盖; +- Chrome 现有 `status`、`open_uri`、`task_summary`、`focus_task`、`query_local_file`、`query_local_folder`、`open_workspace` 与更新、配对消息; +- CloudFile 已有 SSO/登录 Cookie、Hub/Server URI、原生 Seafile 路由、普通/独占编辑权限及更新机制; +- 文件工作副本、Journal、Intent、Guard、Checkout/Checkin 和历史版本,特别是未完成及结果未知的任务。 + +新增 Profile 不修改上述协议,也不改变已有上层应用的配置键、登录逻辑、文件服务 URL、业务账号或文件权限。不得通过默认设置将生产 Host 切到新的 Bridge。 + +## 3. 演进步骤及回退 + +### A. 合同准备(现在) + +- 在 Bridge 主仓引入`ApplicationProfile V1`、`BridgeTask V1` 和 Capability/MCP 合同;只增加离线校验、文档与可运行样例。 +- 上层应用**自行**提供 Profile 示例和适配计划。部署尚未注册时,完全走旧 CloudFile 路径。 +- CloudFile Agent2 与 Chrome 仍以现有调用为准;不因引入 Profile 而改变任何请求路由。 + +### B. 只读旁路(优先 DEV) + +- 本机可信注册 Profile,并将其`profile_id` 与已确认的原 CloudFile 服务/用户绑定;不改变原配对与登录。 +- Bridge 只读读取 CloudFile 任务/版本状态,不持有 Cookie、任意工作文件路径或独占锁操作;不能让旧 Agent2 任务因 Bridge 不在线而失败。 +- Dev 和 Prod 安装身份及目录隔离。先在独立 Dev Extension/Host/测试账号上验证,不覆盖现有正式扩展的 Host Manifest。 + +### C. 受控转发(独立验收) + +- 逐项增加现有 V2 已有能力的 Bridge IPC 适配:查看→普通编辑→独占编辑;请求仍由 Agent2 的受信票据、Journal、Guard、原生提交机制执行。 +- 每个能力通过实际 Windows 文件/Chrome/Hub/Server 验证后,才能从旧入口切换到 Bridge 转发。切换失败必须能按任务回到**原既有入口**,不能在一个未知结果的写入操作中自动换 Host 重试。 +- AI/企业任务可以旁路接入统一任务中心,但不得更改文件模块的完成判据。 + +### D. 客户端归口(满足完整替代后) + +- 通用 Browser Extension / Local Bridge 统一安装与任务 UI;原正式 Host 名称可由兼容适配器继续服务,直至真实安装基数完成迁移。 +- 当旧 Agent2 有进行中的文件任务时,继续由旧模块处理直至完成;即使 UI 改由 Bridge 展示,也不转写 Journal。 +- 一次用户明确的受控升级,备份旧 Host Manifest、保留工作副本和凭据,健康检查失败立即恢复原入口。不能把“可启动”当成“文件版本已经安全发布”。 + +## 4. Dev/Prod 隔离与验收矩阵 + +| 维度 | DEV | PROD / 存量 | +|---|---|---| +| Profile | 独立稳定`profile_id` | 原位绑定稳定`profile_id` | +| Chrome Extension | 单独开发扩展 ID | 已安装正式扩展保持 | +| Native Host | `com.cloudfile.agent22.dev` | 当前生产 Host 不变 | +| 配置/Journal/工作副本/设备凭据 | 独立目录与身份 | 不修改原目录、任务和登录 | +| 程序升级 | Dev 发行信任根 | 既有正式发行签名根 | +| 回退 | 可以卸载 Bridge Dev 而无影响 | Bridge 不可用时仍可用旧文件入口 | + +最低验收: +1. 不安装 Bridge 时,当前应用的登录、目录、文件下载、三种本地操作仍按现有版本行为运行; +2. 安装 Bridge Dev 后,正式 Chrome/Host/工作文件/SSO/权限均不变; +3. Bridge 只读任务摘要与 CloudFile 自有任务中心一致,含冲突、未知回执、Checkout; +4. Dev 两个 Profile/两套 Host 共存,不混任务或凭据; +5. Windows 首先验收;macOS、Linux 共享合同逐步补齐,但不得拖慢正式 Windows 文件流; +6. 正式转发需另立验收,不凭 Mock/CI 通过直接切生产。 + +## 5. 明确禁止 + +本合同阶段不启动数据迁移、SSO 重构、CloudFile Hub/Server API 改造、Chrome 默认 Host 接管、文件引擎复制、生产部署或 MCP 写入工具开放。只在验证确有新业务需求时增加必要能力适配。 + +下一步工作以一个跨应用 Profile + 一个只读 CloudFile 任务摘要为最小垂直切片;不要先做复杂模块市场或通用任务数据库。 diff --git a/docs/MCP.md b/docs/MCP.md index 899deef..c89167a 100644 --- a/docs/MCP.md +++ b/docs/MCP.md @@ -36,3 +36,7 @@ printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocol ``` AI clients must be approved at the user or enterprise policy layer. Local stdio is not a substitute for file-specific authorization. All future sensitive operations must confirm that the user intended the action. + +## Unified Profile / task / Pi plan + +[Capability / MCP / Pi V1](CAPABILITY_AND_MCP_V1.md) defines the **planned** unified tool contract. This 0.1.0 MCP server still exposes **only** the two read-only tools listed above. Neither a declared ApplicationProfile channel nor a Pi process automatically enables future file or enterprise write tools. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index a3e540c..45d08c3 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -1,27 +1,30 @@ -# Roadmap (proposal, not completed work) +# Roadmap — 通用 Profile 优先,非破坏性并行演进 -**0.1.0 — repository baseline** -- Rust workspace + local MCP stdio read-only endpoint. -- Validated enterprise Channel schema (offline only). -- CloudFile Agent2 identity adapter, no duplicated file-edit engine. -- Cross-platform CI for Rust. +**现状:** `0.1.0` 是只读 MCP + 离线旧 Channel 校验,CloudFile Agent2 负责真实文件编辑;本仓尚未实现可信 Profile 获取、通用任务中心、Pi RPC 或可写 CloudFile IPC。 -**0.2 — CloudFile application** -- Reuse CloudFile 2.3 durable file engine via authenticated local IPC. -- Read task list and open user-authorized files; no raw paths to AI. -- Real Office/CAD on Windows standard user, macOS QA. -- Write/Checkout/Checkin remain user-confirmed and continue using Agent2. +**P0 · 通用合同(本 PR)** +- 固定 [ApplicationProfile V1](APPLICATION_PROFILE_V1.md)、[BridgeTask V1](BRIDGE_TASK_V1.md)、[Capability/MCP V1](CAPABILITY_AND_MCP_V1.md) 和 [CloudFile 非破坏迁移](CLOUDFILE_TO_BRIDGE_MIGRATION.md); +- 增量增加 `validate-profile` **离线结构校验**;旧 `validate-channel` / read-only MCP 保留; +- 不在通用仓存放任何具体上层企业应用配置、地址、账号或业务规则。 -**0.3 — Enterprise Channel MVP** -- Trusted provider enrollment; provider-bound signed/authenticated config fetch. -- Dynamic enterprise Web entries and built-in capability visibility. -- One Etech proof of concept for an approved tool with typed inputs/outputs. +**P1 · 可信 Profile + 只读任务旁路(DEV)** +- 建立受信应用注册、客户端 LocalBinding、Profile 缓存和 EffectiveProfile 计算;不得通过 Profile 启用任意 EXE、替换更新公钥; +- CloudFile Agent2 提供受信的只读任务摘要接口,Bridge 根据`profile_id`聚合任务; +- Dev 与 Prod Chrome/Host/Journal/凭据完全隔离;不接管任何生产文件入口。 -**Later only if needed** -- Safe remote AI relay with device/user/task binding. -- Optional signed native extension distribution. -- Linux desktop packaging and enterprise deployment policy. -- Device/printing/CAD automation based on real customer use cases. +**P2 · AI 与企业能力垂直切片** +- 一个通用企业 Web/AI 通道(应用自己提供 Profile,不让 Bridge 依赖其业务模型); +- 可选 Pi RPC 与 Bridge MCP Client/Server 接入;先只读 MCP 任务/能力查询,写入工具要逐项业务授权; +- 一个真实跨应用非文件任务映射到 BridgeTask,和文件任务共同展示。 -Out of scope: competing AI model/chat UI, generalized RPA, filesystem driver, -Seafile sync client, plugin marketplace, arbitrary remote scripts. +**P3 · CloudFile 受控文件转发** +- 经认证本机 IPC 顺序支持查看、普通编辑、独占编辑,复用 Agent2 Journal、Guard、原生条件发布; +- 每项在真实 Windows 普通用户+Chrome+CloudFile DEV 验收后再切流;macOS 同合同验收,Linux 先基础能力; +- 存量 Host、用户数据、SSO 和正在编辑的文件不迁移、不暗中修改。 + +**P4 · 通用浏览器扩展与 Bridge 统一入口** +- 确认所有旧真实调用均受支持后,可把产品入口归口为通用 Bridge; +- 保留旧正式 Host 的兼容转发和安全回退,直到已安装客户端经真实升级验证; +- CloudFile 长期作为文件能力模块,并不因此改变其业务权限或版本语义。 + +不做:重写 CloudFile 文件引擎、虚拟盘、独立 AI 模型网关、通用 RPA、远程 Shell/EXE、插件商店、大型任务数据库。未验收的目标不能写为已经实现。 diff --git a/docs/SECURITY.md b/docs/SECURITY.md index 2d88bfc..c941ca4 100644 --- a/docs/SECURITY.md +++ b/docs/SECURITY.md @@ -12,3 +12,9 @@ - Avoid default localhost web listeners, unrestricted filesystem access, remote code download, secrets in logs, silent Checkout release and auto-triggered write actions. Security reports: use the repository maintainers' private contact method; do not post credentials or exploit payloads in a public issue. + +## ApplicationProfile V1 / Dev 与 Prod + +Profile JSON 的语法校验不等于来源认证、设备绑定或业务授权;`validate-profile` 永远不注册来源、不触发下载或执行。有效期、修订号与真正的受信 origin 必须在上线接入时再次验证。`update.source_id` 只能指向已安装并钉住签名公钥的更新来源,不接受服务端直接替换公钥。不同环境不能共用 Native Host、缓存/Journal、会话或 MCP 授权上下文。 + +通用 Bridge 不硬编码任何企业应用的接口或账号模型。CloudFile 的文件写入只能通过当前有效的服务端票据和现有 Agent2 Journal/Guard 执行。参见 [统一 Profile 合同](APPLICATION_PROFILE_V1.md)。 diff --git a/examples/application-profile.json b/examples/application-profile.json new file mode 100644 index 0000000..1378c93 --- /dev/null +++ b/examples/application-profile.json @@ -0,0 +1,53 @@ +{ + "schema_version": 1, + "profile_id": "sample-app-prod", + "provider": "sample-app", + "environment": "prod", + "revision": 1, + "expires_at": 1999999999, + "application": { + "id": "sample-app", + "name": "Sample Enterprise App", + "logo_url": "https://assets.example.test/logo.png" + }, + "origin": "https://app.example.test", + "services": { + "files": "https://files.example.test", + "ai": "https://app.example.test/ai" + }, + "channels": [ + { + "id": "files", + "title": "Files", + "kind": "native", + "module": "cloudfile", + "capabilities": [ + "cloudfile.status" + ], + "enabled": true + }, + { + "id": "ai", + "title": "Enterprise AI", + "kind": "web", + "entry": "/ai", + "capabilities": [], + "enabled": true + }, + { + "id": "pi", + "title": "Local Pi", + "kind": "native", + "module": "pi", + "capabilities": [ + "ai.local.chat" + ], + "enabled": false, + "optional": true + } + ], + "update": { + "track": "stable", + "source_id": "trusted-stable" + } +} diff --git a/examples/enterprise-channels.json b/examples/enterprise-channels.json new file mode 100644 index 0000000..1920c32 --- /dev/null +++ b/examples/enterprise-channels.json @@ -0,0 +1,24 @@ +{ + "schema": 1, + "provider": "sample-app", + "origin": "https://app.example.test", + "expires_at": 1999999999, + "channels": [ + { + "id": "cloudfile-status", + "title": "File Agent Status", + "kind": "builtin", + "entry": "cloudfile.status", + "requires": [ + "cloudfile.status" + ] + }, + { + "id": "enterprise-ai", + "title": "Enterprise AI", + "kind": "web", + "entry": "/ai", + "requires": [] + } + ] +} diff --git a/examples/etech-channels.json b/examples/etech-channels.json deleted file mode 100644 index d2a7fe5..0000000 --- a/examples/etech-channels.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "schema": 1, - "provider": "etech", - "origin": "http://10.9.8.162", - "expires_at": 1792540800, - "channels": [ - { - "id": "cloudfile-status", - "title": "CloudFile 状态", - "kind": "builtin", - "entry": "cloudfile.status", - "requires": ["cloudfile.status"] - }, - { - "id": "enterprise-ai", - "title": "企业 AI", - "kind": "web", - "entry": "/agent/ai", - "requires": [] - } - ] -} From 983ddf65651a0432ed7468d90d05e603710f22c2 Mon Sep 17 00:00:00 2001 From: servicememeai Date: Tue, 27 Oct 2026 13:30:37 +0900 Subject: [PATCH 2/2] style: apply rustfmt-compatible native channel check --- crates/local-bridge-core/src/profile.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/crates/local-bridge-core/src/profile.rs b/crates/local-bridge-core/src/profile.rs index dc0957e..23a8123 100644 --- a/crates/local-bridge-core/src/profile.rs +++ b/crates/local-bridge-core/src/profile.rs @@ -174,9 +174,7 @@ pub fn validate_profile_shape(profile: &ApplicationProfile) -> Result<()> { } } ProfileChannelKind::Native => { - if channel.entry.is_some() - || !channel.module.as_deref().is_some_and(identifier) - { + if channel.entry.is_some() || !channel.module.as_deref().is_some_and(identifier) { bail!("native channel requires a known module ID only"); } }