unicity-astrid/sage

GitHub: unicity-astrid/sage

Sage 将 Claude Code 作为能力门控的智能体托管在 Astrid OS 上,通过 WASM capsule 实现 IPC 工具桥接、审计追踪和身份隔离。

Stars: 0 | Forks: 0

# sage [![License: MIT OR Apache-2.0](https://img.shields.io/badge/License-MIT%20OR%20Apache--2.0-blue.svg)](LICENSE-MIT) [![MSRV: 1.94](https://img.shields.io/badge/MSRV-1.94-blue)](https://www.rust-lang.org) **[Astrid OS](https://github.com/unicity-astrid/astrid) 的 Anthropic Claude 集成。** 由 Claude 提供支持。 Sage 是一个运行时桥接器,允许 Astrid 将 Claude 作为一等智能体托管——具备能力门控、审计跟踪,且身份与模型解耦。面向用户的智能体拥有自己的身份(通过 `capsule-identity` / `spark` 配置);Claude 是其底层的引擎。 ## 此仓库包含什么 这是一个多 capsule 捆绑包。四个 wasm 组件作为一个单独的 Astrid 发行版一起发布: | Crate | 角色 | |-------|------| | `sage` | 监督 `claude -p` 无头子进程(每个会话一个,每个主体 ≤4 个)。流式传输 stdin/stdout,解析 Claude 的 stream-json,将 tool-call 事件分发到 bus,并反馈结果。 | | `sage-completion` | 直接的 Anthropic Messages API LLM 提供程序。实现了 `astrid:llm@1.0.0`。用于 Astrid 驱动 agent loop 时的逐轮补全。 | | `sage-mcp` | MCP 工具桥。从 `sage` 获取 `sage.v1.tool.call.*` 事件,验证参数和能力,通过 `tool.v1.*` capsule IPC 进行分发,并回传发布结果。 | | `sage-install` | 配置器。`astrid sage install` 引导用户完成每个主体 `HOME` 隔离的 Claude 设置。 | ## 两种计费路径 - **Agent 模式**(`sage` crate):`claude -p` 运行 Claude 自己的 agent loop。消耗 Pro/Max 订阅上的 Anthropic Agent SDK 额度(截至 2026 年 6 月 15 日,分别为 $20 / $100 / $200/月)。 - **补全模式**(`sage-completion` crate):直接的 Anthropic API 逐轮调用。消耗 API 使用额度,与 Agent SDK 额度分开。在 Astrid 驱动循环并希望进行逐轮模型路由时使用。 `capsule-router` 逐轮选择——常规工作通过补全模式使用 Sonnet/Haiku,仅在需要推理深度时,通过任一模式使用 Opus。 ## 交互模式 Sage 暴露了两个正交的、针对每个主体的维度。第一个维度选择**谁来驱动 `claude`**: - **headless**(默认)—— Astrid 从 `sage` capsule 生成 `claude -p` 并拥有 agent loop。工具接口被锁定为 `mcp__sage__*`(sage 从 stream-json 中解析出 `tool_use` 块并通过 bus 路由)。所有原生 Claude Code 工具(`Bash`、`Read`、`Write`、`Edit`、`WebFetch`,……)均被拒绝。这是无人值守 / 多用户 / 生产环境部署的推荐模式——每次工具调度都可审计,并通过 Astrid 进行能力门控。 - **repl** —— 操作员直接在 principal 文件夹(`~/.astrid/home//`)内运行 `claude`。Sage 拒绝生成(在 `sage.v1.request.spawn` 上发布 `sage.v1.event.session_rejected{reason:"interaction_mode_is_repl"}`)。拒绝列表为空,因此用户可以完全访问原生 Claude Code 工具集;sage 的 MCP 接口未连接(进程内的 `mcp__sage__*` 桥需要 sage 拥有该子进程)。Hook 仍然在 `settings.local.json` 中声明,以便未来的原生 hook 桥可以在不重新发布该文件的情况下审计/监管会话(参见[已知缺陷 #6](#known-deficiencies))。 ### headless 用法 ``` astrid capsule install sage # operator picks interaction_mode=headless, auth_mode=api_key astrid sage install # provisions ~/.astrid/home//.claude/ astrid sage spawn # sage publishes sage.v1.request.spawn; supervisor returns a session id astrid sage send "hi" # writes a user turn into the running claude -p stdin ``` ### REPL 用法 ``` astrid capsule install sage # operator picks interaction_mode=repl astrid sage install # provisions ~/.astrid/home//.claude/ (hooks declared, deny list empty) cd ~/.astrid/home/ # then drive claude directly: HOME="$PWD" claude ``` ### 在运行时切换模式 发布 `sage.v1.request.settings.set` IPC envelope。Sage 拥有 `kv://sage.principal.config`(规范的真相来源)并重新发出 `sage.v1.install.relink`,以便 `sage-install` 使用新配置重写磁盘上的 `.claude/settings.local.json` 和 `.mcp.json`。成功重链后,sage 会为下游订阅者(仪表板、审计接收器)发布 `sage.v1.settings.changed`。审计跟踪为 `sage.v1.audit.settings_changed{previous_config, new_config}`。主题结构定义在下面的[主题契约](#v1-ipc-topic-contract)中。 ``` // publish on sage.v1.request.settings.set { "principal_id": "alice", "interaction_mode": "repl" } // headless -> repl { "principal_id": "alice", "auth_mode": "subscription" } // api_key -> subscription { "principal_id": "alice", "interaction_mode": "headless", "auth_mode": "api_key" } // full reset ``` 缺失的字段会被保留(部分补丁语义)。合并后的记录在持久化之前会进行验证;超出范围的 `schema_version` 将被拒绝。 ## 身份验证模式 第二个维度选择 **`claude` 如何对 Anthropic 进行身份验证**: - **api_key**(默认)—— sage 读取特定于主体的 `api_key` 密钥(在安装时由内核从 `Capsule.toml [env]` 获取,存储在主机 SecretStore 中,并作为 capsule 的运行时配置公开),并在每次生成时将其作为 `ANTHROPIC_API_KEY` 导出到 `claude` 子进程环境中。`apiKeyHelper` 在 `settings.local.json` 中被固定为 `/bin/false`,因此 Claude 无法回退到使用环境凭据。每个主体携带独立的密钥——完全的加密隔离。 - **subscription** —— sage 从不设置 `ANTHROPIC_API_KEY` 并忽略 `apiKeyHelper` 固定,以便 Claude 可以回退到其 keychain OAuth 凭据路径。操作员在**主体文件夹内手动**运行 `claude /login`(sage 明确从不调用 `/login`)。这将解锁 Agent SDK 所依赖的 Anthropic Pro / Max 订阅计费路径。 操作员在执行 `astrid capsule install sage` 时选择这两个维度。`api_key` 密钥是无条件提供的(内核会遍历每个 `[env]` 键——目前没有 `when=` 语义);subscription 模式的操作员**将密钥提示留空**,且空值不会被持久化。 ### macOS Keychain 注意事项 macOS 上的 `claude /login` 会将 OAuth token 写入一个由 **service+account** 作为键的 keychain 条目中,而不是以 `HOME` 作为键。同一 macOS 用户帐户上的两个主体文件夹共享该凭据——**subscription 模式在 macOS 上不会对主体进行加密隔离**。要在 macOS 上实现每个主体的完全隔离,请执行以下任一操作: - 使用 `api_key` 模式(每个主体获得其自己的 `SecretStore` 加密密钥),或者 - 在单独的 macOS 用户帐户下运行主体。 Linux 不受影响——libsecret 由用户会话命名空间控制。Sage 明确**从不在任何模式下调用 `claude /login`**;subscription 用户需在主体文件夹外带外手动运行它。 ## 编写扩展 Claude 的 capsule 当 headless 模式处于活动状态时,从 Claude 的角度来看,任何订阅了 `tool.v1.request.describe` 和 `tool.v1.execute.` 的 capsule 都将成为 `mcp__sage__` 工具。`#[capsule]` 和 `#[astrid::tool]` 宏会生成 IPC 管道,因此作者只需编写地道的 Rust 处理器: ``` # capsules//Capsule.toml [package] name = "my-cool-capsule" version = "0.1.0" astrid-version = ">=0.7.0" [[component]] id = "my-cool" file = "my_cool_capsule.wasm" type = "executable" [capabilities] fs_read = ["cwd://"] # whatever your tool actually needs [publish] "tool.v1.response.describe.*" = {} "tool.v1.execute.*.result" = {} [subscribe] "tool.v1.request.describe" = { handler = "tool_describe" } "tool.v1.execute.greet" = { handler = "tool_execute_greet" } ``` ``` // capsules//src/lib.rs use astrid_sdk::prelude::*; use astrid_sdk::schemars; use serde::Deserialize; #[derive(Default)] pub struct MyCool; #[derive(Debug, Default, Deserialize, schemars::JsonSchema)] pub struct GreetArgs { /// Who to greet. pub name: String, } #[capsule] impl MyCool { /// Greet a person by name. Returns the greeting string. #[astrid::tool("greet")] pub fn greet(&self, args: GreetArgs) -> Result { Ok(format!("Hello, {}!", args.name)) } } ``` 安装 capsule 后,运行 `astrid sage spawn `,Claude 就会在其工具列表中看到 `mcp__sage__greet`。端到端流程:sage-mcp 扇出 `tool.v1.request.describe`,将响应整理到 `sage.v1.tools.list` 中,并将 Claude 的 `tool_use` 块桥接到 `tool.v1.execute.`(具有 50 秒的截止时间)。工具描述符的文档注释成为 Claude 看到的描述;`inputSchema` 通过 `schemars` 从 `GreetArgs` 派生。 ## v1 IPC 主题契约 所有主题都遵循 `...[.]` 约定。尾部的 `*` 是单段通配符(correlation id 或 session id)。 捆绑包中存在两种订阅方式: - **Manifest-declared** —— 列在 crate 的 `Capsule.toml` `[subscribe]` 块中,由主机分发到指定的处理函数。 - **Runtime-subscribed** —— 通过 `ipc::subscribe(...)` 从管理器 `#[astrid::run]` 循环中打开,并按 tick 耗尽。这些在每个表格中都有内联文档,并标记为“(runtime)”。 相同的区别也适用于发布:任何未被 `[publish]` 块的通配符模式覆盖的内容在表格中都会被标记为“(runtime)”。有关清单协调的待处理工作,请参阅[已知缺陷 #5](#known-deficiencies)。 ### `sage`(agent runner) | 方向 | 主题 | Payload | |---|---|---| | 订阅 | `sage.v1.request.spawn` | `{principal_id: String, session_id?: String, initial_message?: String}` | | 订阅 | `sage.v1.request.send.` | `{session_id: String, text: String}`(单用户回合,≤1 MiB) | | 订阅 | `sage.v1.request.settings.set` | `{principal_id, interaction_mode?, auth_mode?}`(部分补丁;缺失字段被保留) | | 订阅 | `sage.v1.tool.result.` | `{content: String, isError: bool}`(来自 sage-mcp 的回写) | | 订阅 (runtime) | `sage.v1.request.stop.` | `{}` —— 优雅终止请求 | | 订阅 (runtime) | `tool.v1.execute.save_identity.result` | `{success: bool, principal_id?: String}` —— 身份刷新触发器 | | 订阅 (runtime) | `approval.v1.response.` | `{allowed: bool, reason?: String}` —— 能力批准结论 | | 订阅 (runtime) | `sage.v1.install.complete` | `{principal_id, success: bool, ...}`(从 `ensure_install` 等待) | | 发布 | `sage.v1.event..spawned` | `{principal_id, session_id, pid}` | | 发布 | `sage.v1.event..init` | `{session_id, model, cwd, tools: [String]}` | | 发布 | `sage.v1.event..text` | `{delta: String}` | | 发布 | `sage.v1.event..done` | `{usage: Usage, is_error: bool, permission_denials: [String]}` | | 发布 | `sage.v1.event..exited` | `{exit_code?: i32, signal?: i32, reason?: String, stdout_tail?: String, stderr_tail?: String}` | | 发布 | `sage.v1.event..respawned` | `{principal_id, reason: "identity_refresh", flags_hash}` | | 发布 | `sage.v1.event..error` | `{reason: String}`(例如 `stdin_quota`, `api_key_missing`) | | 发布 | `sage.v1.event.session_rejected` | `{reason: "principal_limit" \| "interaction_mode_is_repl" \| ...}` | | 发布 | `sage.v1.tool.call.` | `{session_id, principal_id, tool_name, arguments: JSON}` | | 发布 | `sage.v1.install.relink` | `{principal_id, config: PrincipalConfig}`(sage 在 settings.set 时发布;sage-install 重写磁盘上的 JSON) | | 发布 | `sage.v1.settings.changed` | `{principal_id, config: PrincipalConfig, schema_version}`(在 sage-install 确认重写后发出) | | 发布 (runtime) | `sage.v1.audit.spawn` | `{principal_id, session_id, pid, flags_hash, auth_mode, interaction_mode}` | | 发布 (runtime) | `sage.v1.audit.tool_call` | `{principal_id, session_id, call_id, tool_name, allowed: bool, decision_source}` | | 发布 (runtime) | `sage.v1.audit.identity_fallback` | `{principal_id, session_id, reason: String}` | | 发布 (runtime) | `sage.v1.audit.install_choices` | `{interaction_mode, auth_mode}`(从 `#[astrid::install]` 发出) | | 发布 (runtime) | `sage.v1.audit.settings_changed` | `{principal_id, previous_config, new_config}` | | 发布 (runtime) | `sage.v1.install.run` | `{principal_id, config?: PrincipalConfig}`(从 `ensure_install` 唤醒 `sage-install`) | ### `sage-mcp`(tool bridge) | 方向 | | Payload | |---|---|---| | 订阅 | `sage.v1.tool.call.` | sage 发布的镜像 | | 订阅 | `sage.v1.tools.describe` | `{}`(触发 describe-collect) | | 订阅 | `tool.v1.response.describe.*` | `{tools: [ToolDescriptor]}`(扇入缓存) | | 发布 | `sage.v1.tool.result.` | `{content: String, isError: bool}` | | 发布 | `sage.v1.tools.list` | `{tools: [McpToolDescriptor]}` | | 发布 | `tool.v1.request.describe` | `{}`(广播 describe-collect) | | 发布 | `tool.v1.execute.`(单段)和 `tool.v1.execute..`(点分隔) | `{type: "tool_execute_request", call_id, tool_name, arguments}` | ### `sage-completion`(LLM provider) | 方向 | 主题 | Payload | |---|---|---| | 订阅 | `llm.v1.request.describe` | `{}` | | 订阅 | `llm.v1.request.generate.sage` | `IpcPayload::LlmRequest { request_id, model, messages, tools, system, ... }` | | 发布 | `llm.v1.response.describe` | `{providers: [ProviderEntry]}` | | 发布 | `llm.v1.response.describe.*` | `{providers: [ProviderEntry]}`(相关路由) | | 发布 | `llm.v1.stream.sage` | `IpcPayload::LlmStreamEvent { request_id, event: StreamEvent }` | 发出的 `StreamEvent` 变体:`TextDelta`, `ToolCallStart{id,name}`, `ToolCallDelta{id,args_delta}`(不透明的部分 JSON,从不按块解析), `ToolCallEnd{id}`, `Usage{input_tokens,output_tokens}`(累计,在结束时一次发出), `Done`, `Error(msg)`。 ### `sage-install`(provisioner) | 方向 | 主题 | Payload | |---|---|---| | 订阅 | `sage.v1.install.run` | `{principal_id, force?: bool, config?: PrincipalConfig}` | | 订阅 | `sage.v1.install.relink` | `{principal_id, config?: PrincipalConfig}` | | 发布 | `sage.v1.install.status` | `{principal_id, step: String}` | | 发布 | `sage.v1.install.complete` | `{principal_id, success: bool, error?: String}` | | 发布 | `sage.v1.audit.settings_changed` | `{principal_id, previous_config, new_config}`(在每次确认重写后发出) | 还会为每个会话的身份注入发布一次性 `spark.v1.request.build` 并读取 `spark.v1.response.ready`(按 session-id 原子写入主体 `.claude/` 中的 `.sage-identity-` 文件)。 ## 已知缺陷 1. **`.mcp.json` 是一个存根,而不是真正的 MCP server。** Claude 期望将 MCP server 作为原生子进程进行 fork-exec;发布这样的二进制文件需要主机端内核工作(一个 `astrid-mcp-shim` 原生二进制文件),这超出了仅限 capsule 的 Sage 工作区范围。v1 附带包含 `{"mcpServers":{"sage":{"command":"/bin/false","args":[],"env":{}}}}` 的 `.claude/.mcp.json`——即文档化存根。Claude 的工具接口仍然有效,因为 (a) `--allowed-tools mcp__sage__*` 对接口进行了限制,(b) `sage` 直接从 `claude -p` 的 `--output-format stream-json` 中解析 `tool_use` 内容块,并通过 bus 路由它们,(c) `--append-system-prompt-file` 枚举可用工具,以便 Claude 知道要调用什么。当内核发布原生 `astrid-mcp-shim` 二进制文件时,`sage-install` 将重写 `.mcp.json` 以指向它(附加更改,无 IPC 契约变动)。**标记的差距**——在此工作区之外进行跟踪。 2. **没有原生的 MCP tools/list 响应。** 由于 (1),Claude 从未看到真正的 MCP `tools/list`——`--append-system-prompt-file` 枚举是替代品。工具描述通过该通道而不是协议级别的 `tools/list` 调用显现。 3. **身份验证是针对每个主体且双模式的;macOS subscription 模式未隔离。** Sage 支持两种身份验证模式,在 `astrid capsule install sage` 时通过 `Capsule.toml [env]` 针对每个主体进行配置: - **api_key** —— 内核获取 `api_key` 密钥,将其持久化在主机 `SecretStore` 中,并将其作为 capsule 的运行时配置注入。Sage 在生成时通过 `astrid_sdk::env::var("api_key")` 读回它,并将其作为 `ANTHROPIC_API_KEY` 导出到 `claude` 子进程环境中。`apiKeyHelper` 在 `settings.local.json` 中被固定为 `/bin/false`,因此 Claude 无法回退到环境凭据。每个主体携带独立的密钥——完全的加密隔离。 - **subscription** —— sage 从不设置 `ANTHROPIC_API_KEY` 并忽略 `apiKeyHelper` 固定。操作员在主体文件夹内手动运行 `claude /login`。**在 macOS 上**,`claude /login` 会将 OAuth token 写入由 service+account 作为键的 keychain 条目中,而不是以 HOME 作为键——同一 macOS 用户帐户上的两个主体文件夹共享该凭据,因此 subscription 模式在 macOS 上不会对主体进行加密隔离。使用 api_key 模式(或单独的 macOS 用户)进行完全隔离。Linux libsecret 由用户会话命名空间控制,不受影响。Sage 明确从不调用 `claude /login`——操作员需手动运行它。 有关完整的演练和 macOS 注意事项,请参阅上面的[身份验证模式](#authentication-modes)部分。 4. **#752 之前的仅注册 describe。** `sage-completion` 发布到 `llm.v1.response.describe`(无后缀;旧版注册表清理)和 `llm.v1.response.describe.*`(相关路由;#752 之后)。一旦每个消费者都使用 #752 之后的版本,就丢弃无后缀的发布。 5. **Manifest 发布/订阅接口比运行时窄。** `sage/Capsule.toml` 声明了 `sage.v1.event.*` 和 `sage.v1.tool.call.*` 发布,以及四个绑定处理器的订阅(`sage.v1.request.spawn`、`sage.v1.request.send.*`、`sage.v1.request.settings.set`、`sage.v1.tool.result.*`)。在运行时,管理器还会为 `sage.v1.request.stop.*`、`tool.v1.execute.save_identity.result`、`approval.v1.response.*` 和 `sage.v1.install.complete` 打开 `ipc::subscribe`,并发布到 `sage.v1.audit.*` 和 `sage.v1.install.run`。一旦主机对运行时调用执行 manifest `[publish]/[subscribe]` 强制检查落地,这些就需要在 `Capsule.toml` 中声明,或者转换为绑定处理器的订阅。在那之前,上面的主题契约表是事实来源——`(runtime)` 标记指出了每个差距。 6. **原生 `astrid-emit` 二进制文件尚不存在。** `sage-install` 为每个 Claude hook 事件在 `settings.local.json` 中编写了带有 `astrid-emit --topic sage.v1.hook.` 命令的 `settings.local.json`,并且 sage 的循环验证器已经完全连接并准备好消耗它们(参见上面的 [Hook 事件验证](#hook-event-validation)部分)。shim 二进制文件本身单独发布——在 [`unicity-astrid/astrid#814`](https://github.com/unicity-astrid/astrid/issues/814) 跟踪。在它落地之前,Claude 会在每次触发 hook 时执行不存在的命令,并且事件永远不会到达 bus;这会降低 sage 的可观测性,但不会破坏生成流程(Claude 根据其协议将缺失的命令视为非阻塞错误)。当 `astrid-emit` 落地时,hook 事件将开始流动,**无需进一步的 sage 更改**——sage 端的契约在设计上是前向兼容的。目前 Sage 也是**仅限 Unix**(`claude` 二进制文件、HOME 重定向、`/bin/false` `apiKeyHelper` 以及最终的 `astrid-emit` 调用都假定在 Unix 上)。**标记的差距**——二进制文件在此工作区之外进行跟踪;capability-token 加固在 [`rfcs#30`](https://github.com/unicity-astrid/rfcs/pull/30) 中跟踪。 7. **Sage 端的审计主题尚未镜像到共享的跨 capsule 审计主题。** Sage 发布 `sage.v1.audit.*`(spawn、tool_call、identity_fallback、install_choices、settings_changed、respawn_abandoned)。内核端的 `astrid.v1.audit.entry` 是管理操作形式的(method / required_capability / target_principal),不是 capsule 发出的归因事件的合适归宿。建立一个共享的跨 capsule 审计命名空间(`audit.v1.event` 或类似名称)属于 RFC 级别的工作,已推迟。该 TODO 存在于 `sage` 和 `sage-install` 源代码中的每个审计发布点。 ## Hook 事件验证 Claude Code hooks 在 `settings.local.json` 中使用一个 `command` 字符串进行配置,Claude fork-exec 该字符串,并通过管道将 hook payload 传输到 stdin。Sage 使用 **sage 作为验证器**模型,将这些子进程调用引入经过审计的 IPC bus,而无需授予 hook 子进程自己的内核主体。 ### 验证链 1. **原生发射 (`astrid-emit`)** —— 一个主机端二进制文件(通过 [astrid#814](https://github.com/unicity-astrid/astrid/issues/814) 在核心中单独发布),由 `sage-install` 配置为 hook `command`。在调用时,`astrid-emit` 读取 hook 的 stdin payload,进行 base64 编码,并在 `sage.v1.hook.` 上发布一个包含 `hook`、`payload`、`correlation_id: null` 以及从其环境中提取的三个传输字段:`principal_id`(来自 `ASTRID_PRINCIPAL_ID`)、`session_id`(来自 `ASTRID_SESSION_ID`)和 `token`(来自 `ASTRID_HOOK_TOKEN`)的 envelope。 2. **基于会话的 token 生成** —— 当 sage 在 `handle_spawn` 中生成 `claude -p` 时,它会通过主机 CSPRNG(`runtime::random_bytes`)生成一个 256 位随机 token,将其持久化到 `kv://sage.hook_token..`,并在 `claude` 子进程上设置所有三个环境变量(`ASTRID_PRINCIPAL_ID`、`ASTRID_SESSION_ID`、`ASTRID_HOOK_TOKEN`)。该 token 对于每个 `(principal, session)` 都是唯一的。 3. **循环验证** —— sage 在其 `#[astrid::run]` 管理器中订阅了 `sage.v1.hook.*`。对于每个事件,sage 使用来自 envelope 的**声明** `(principal, session)` 查找 `kv://sage.hook_token..`,并将存储的 token 与 envelope 的 `token` 字段进行比较。内核基于主体的 KV 范围将查找限制在 sage 自己的命名空间内,因此即使声明是伪造的,跨主体查找也无法返回另一个主体的 token。 4. **重新发布(sage 作为 CA)** —— 在 token 匹配时,sage 剥离传输字段(`principal_id`、`session_id`、`token`),并在 `hook.v1.event.` 上重新发布规范的 hook-event-request 结构(`hook`、`payload`、`correlation_id: null`)。主题映射:`PreToolUse` → `before_tool_call`,`PostToolUse` → `after_tool_call`,`UserPromptSubmit` → `message_received`,`Stop` → `session_end`,`SubagentStop` → `subagent_stop`。Claude 端的 `Notification` hook 没有规范的 Astrid 等价物,并在 sage 命名空间主题 `sage.v1.notification` 上重新发布。重新发布带有 sage 自己的 capsule 归因——下游订阅者信任 sage 的担保(“sage 作为 CA”);`principal_id` 位于 payload 内部,而不是内核归因的 envelope 元数据中。 5. **不匹配处理** —— 如果 token 不匹配,sage 将丢弃该事件,并发布带有声明字段的 `sage.v1.audit.hook_spoof_attempt`,以便能够观察到欺骗尝试。在 token 匹配之前,Sage **不会**将 envelope 的 `principal_id` 视为真实的;在验证之前,该声明仅仅是 KV 命名空间索引。 6. **会话清理** —— 在优雅停止、管理器驱动的逐出或 capsule 重新加载孤立扫描时,sage 会删除 `kv://sage.hook_token..`,以确保在会话结束后无法重放泄漏的 token。 ### 残留差距:Linux `/proc//environ` 环境窃取风险 `ASTRID_HOOK_TOKEN` 被导出到 `claude` 子进程环境中。在 **Linux** 上,任何在同一 uid 下运行的进程都可以读取 `/proc//environ` 并提取 token,然后伪造能够通过 sage 验证的 hook 事件。macOS 对非 root 调用者隐藏了 `environ`(`ps -E` 需要提权),但 Linux 默认情况下不提供这种保护。 威胁模型:在与 `claude` 相同的 uid 下运行的共租攻击者进程(例如另一个 shell 会话、受损的二进制文件)可以窃取基于会话的 token,并在该会话的生命周期内冒充 hook 源。 当前缓解措施:短生命周期会话、每次生成(包括身份刷新重生成)时都重新生成 token、基于会话的 token 轮换以及会话结束时的 KV 清理。完全缓解需要由内核颁发并针对每个 hook 限定范围的 capability token,完全替换环境共享的密钥——有关 capability token 的前向方向,请参见 [RFC#30](https://github.com/unicity-astrid/rfcs/pull/30)。 ### Sage 不信任的内容 - 传入 envelope 中的 `principal_id` 字段是一个**声明**——在 token 查找匹配之前,它不是身份验证。它是一个索引,而不是身份验证声明。 - `session_id` 字段同样是一个声明——只有当 token 匹配时,它才会解析为真实的 (principal, session)。 - `sage.v1.hook.*` 事件的内核归因发送者(无论是发布它的任何 capsule 还是原生二进制文件)均不受信任,不认为是 `astrid-emit`。任何具有主题能力的发布者都可以尝试该链;token 匹配步骤是唯一的门控。 - Sage 在 `hook.v1.event.` 上的重新发布是下游订阅者使用的规范归因——订阅者不应直接订阅 `sage.v1.hook.*`。 `astrid-emit` 二进制文件尚不存在(在 [astrid#814](https://github.com/unicity-astrid/astrid/issues/814) 跟踪)。Sage 今天会编写引用它的 `settings.local.json` 命令字符串,因此该链是前向兼容的:一旦主机二进制文件落地,hook 事件将开始流动,无需进一步的 sage 更改。 ## Claude 二进制文件版本固定 Sage 通过换行符分隔的 JSON 分发来解析 `claude -p --output-format stream-json` 输出。stream-json 语法(特别是 `system.init`、`assistant.message.content[].type ∈ {text, tool_use}`、`result.subtype`、`sdk_control_request`、强制性的 `control_response.response.mcp_response` 包装器)**不属于** Claude Code CLI 公开 API 文档的一部分。它是凭经验观察到的,并得到了第三方 SDK 的证实。 因此:**捆绑或预期的 `claude` 二进制文件版本是按 Sage 版本固定的。** 每个 Claude Code 版本都必须重新验证是否存在 stream-json 语法偏差,然后才能提升 Sage 的预期版本。`sage-install` 会在 KV(`sage.claude_version.`)中记录其配置时所依据的版本;如果在生成时不匹配,sage 将在继续执行之前发布一个类型化的审计事件,以便偏差可见。 当前预期:`claude-code` ≥ 2.1.x。 ## 状态 Pre-alpha。四个 crate 骨架和 Capsule.toml 契约已落地;`sage` 管理器(`#[astrid::run]`)、spawn、send、tool-result 回写、身份刷新和优雅停止路径已在 `crates/sage/src/` 中连接。`sage-mcp` 附带发现和 describe 以及执行桥:`handle_tool_call` 剥离 `mcp__sage__` 前缀,通过字符集白名单控制裸名称,在发布 `tool.v1.execute.` 请求之前订阅 `tool.v1.execute..result`,在 `EXECUTE_SLICE_MS` 步骤中排空 50 秒(通过 `call_id` 过滤),并在每个失败路径(未知前缀、无效名称、订阅/发布错误、超时)上合成 `isError:true` envelope,以便 sage 的 `pending_tool_calls` 插槽干净地停止。能力执行和桥接端审计发布仍然是后续工作——桥本身已发布。`sage-completion` 和 `sage-install` 已针对上述接口进行了端到端连接。下面已知缺陷 #1 中的 `.mcp.json` 原生 shim 注意事项仍然适用——Claude 实际上并没有 fork-exec sage MCP server,工具接口是通过 stream-json 解析观察到的。剩下的润色工作包括工作区锁清理、本 README、主题契约接口,以及针对已发布行为不变量的对抗性审查。 ## 商标 本项目是与 Claude 和 Claude Code 的非官方集成。它不隶属于、也未被 Anthropic, PBC 认可或赞助。“Claude”和“Anthropic”是 Anthropic, PBC 的商标。 ## 许可证 根据您的选择,在 [Apache License, Version 2.0](LICENSE-APACHE) 或 [MIT license](LICENSE-MIT) 下双重许可。
标签:AI代理, Claude, CVE检测, DLL 劫持, Rust, WebAssembly, 可视化界面, 大语言模型, 系统集成, 网络流量审计, 运行时, 通知系统