【免费下载链接】aos-ce
AOS Community Edition: the open agent operating system.
aos-hook-adapter-oracle是 AOS(AOS Community Edition)中负责前端钩子协议翻译的胶囊(capsule):它将经过认证校验的 Codex、Claude、Grok 前端 hook 信封转换为 AOS 标准的hook.v1.event.*协议事件。本文基于 README、实现源码 与 钩子架构文档 完整梳理该适配器的职责边界、事件映射表、响应模式、信封校验规则与发布/订阅配置,帮助你理解 AOS 中"前端协议翻译与标准钩子策略解耦"这一设计的落地方式。
一、定位与职责边界:只做翻译,不做裁决
README 对适配器职责的界定非常明确:该胶囊只拥有协议翻译职责(owns protocol translation only),不拥有下游钩子策略,也不把观察事件(observation events)转化为授权决策。两条具体边界值得强调:
user_prompt_submit只收集有界的additional_context回复,且只针对当前精确的宿主回合(exact host turn)收集;pre_tool_use与permission_request在本中继上是纯观察事件,原生工具(native tool)的拒绝(deny)决策仍留在astrid-gate/broker 的决策路径上——只有那条路径的响应 schema 才能表达 deny。
这一点在源码中有直接印证。[src/lib.rs](https://link.gitcode.com/i/5b6c4ce80874e0a99f05972323f58a7f)中定义了两个响应模式:
#[derive(Debug, Clone, Copy, PartialEq, Eq)] enum ResponseMode { /// Publish the canonical event but do not attach a reply solicitation. Observe, /// Collect bounded `additional_context` for the exact host turn. AdditionalContext, }docs/hooks.md中给出的整体管道也说明了适配器的位置:
frontend plugin -> authenticated ingress in capsule-mcp -> exact oracle.v1.hook.validated.<frontend> topic -> one frontend adapter // 即本胶囊 -> canonical hook.v1.event.<semantic-hook> -> zero or more independent subscriber capsules -> optional correlation-scoped replies -> adapter-shaped frontend response即:认证入口(ingress)在capsule-mcp中完成,本胶囊订阅"已验证"主题(oracle.v1.hook.validated.<frontend>),翻译后发布到标准钩子总线,最终由capsule-mcp把响应按前端传输支持的形状回传。AOS 将标准钩子总线与任何特定 agent 产品解耦,因此新增前端只需新增一个适配胶囊,而不必改动路由器或内核。
二、入口:三个"每个认证主题恰好一个所有者"的处理器
本胶囊在 Capsule.toml 中声明了它订阅的三个已验证主题与三个处理函数:
[subscribe] # Exactly one adapter owns each authenticated frontend topic. No priority is # set: these are protocol ingress points, not a middleware chain. "oracle.v1.hook.validated.codex" = { wit = "opaque", handler = "on_codex_hook" } "oracle.v1.hook.validated.claude" = { wit = "opaque", handler = "on_claude_hook" } "oracle.v1.hook.validated.grok" = { wit = "opaque", handler = "on_grok_hook" } # Dynamic, correlation-scoped response collection for prompt-context hooks. "hook.v1.response.message_received.*" = { wit = "opaque" }[publish]部分声明两条出口:
[publish] "hook.v1.event.*" = { wit = "@unicity-astrid/wit/hook/hook-event-request" } "oracle.v1.hook.response.*" = { wit = "opaque" }源码中三个处理器通过#[astrid::interceptor(...)]宏注册,全部汇入同一个入口函数handle_oracle_hook(expected: Frontend, payload),以Frontend::Codex / Claude / Grok枚举区分前端:
#[capsule] impl OracleHookAdapter { #[astrid::interceptor("on_codex_hook")] pub fn on_codex_hook(&self, payload: serde_json::Value) -> Result<(), SysError> { handle_oracle_hook(Frontend::Codex, payload) } // on_claude_hook / on_grok_hook 同构 }注释明确写道:每个精确的已验证主题恰好由一个适配器拥有(exactly one adapter owns each authenticated frontend topic)。docs/hooks.md进一步指出:同一 raw 主题上安装第二个适配器属于部署错误——"raw 协议入口只有一个所有者,扩展发生在下游的标准钩子主题上"。这也是配置中刻意不设置priority的原因:它们是协议入口点,不是中间件链。
三、事件映射表:三个前端各自的翻译规则
适配器为每个前端维护独立的事件映射表(源码注释说明这是有意为之:"上游插件当前已归一化到通用名称,但每个前端可以独立演化,而不会削弱其他适配器的接受面")。映射函数结构为:
common_mapping(event):三个前端共享的通用映射;codex_mapping / claude_mapping / grok_mapping:各前端对stop等特殊事件的差异化处理,其余事件回落到common_mapping。
3.1 通用映射(common_mapping)
| 前端事件 | 标准钩子canonical_hook | 响应模式 |
|---|---|---|
session_start | session_start | Observe |
session_end | session_end | Observe |
user_prompt_submit | message_received | AdditionalContext |
pre_tool_use | before_tool_call | Observe |
permission_request | before_tool_call | Observe |
post_tool_use | after_tool_call | Observe |
pre_compact | on_compaction_started | Observe |
post_compact | on_compaction_completed | Observe |
subagent_start | subagent_start | Observe |
subagent_stop | subagent_stop | Observe |
表中除user_prompt_submit外全部为Observe模式。源码对pre_tool_use | permission_request的注释点明了原因:本中继外层响应 schema 只能携带 context,因此这些事件在此只是观察;绑定原生工具决策走astrid-gate。
3.2 三个前端的差异:stop与message_display
这是 README 着墨最多、也是适配器中最容易被误解的部分:
| 前端 | 事件 | 映射结果 | 原因 |
|---|---|---|---|
| Codex | stop | message_sent | per-turn 事件,携带last_assistant_message |
| Claude | stop | message_sent | 同上 |
| Claude | message_display | message_displayed | 渲染期间以delta携带响应文本 |
| Grok | stop | session_end | 兼容性例外 |
- Codex 与 Claude 的
stop是回合级响应事件而非会话终止:它们携带已完成回合的last_assistant_message,因此发布为标准message_sent;只有显式的session_end事件才映射为标准session_end。 - Grok 是显式的兼容性例外:当前安装的 Grok hook 契约没有暴露一个独立的、已验证的终止事件,因此其
stop被映射为标准session_end。源码注释写道:"在 Grok 上游 hook 契约提供独立的、已验证的 per-turn 响应事件之前,保留现有解释"。 - Claude
message_display把渲染中的delta批次发布为标准message_displayed,Codex 侧对message_display返回None(不支持)。
单元测试对这四条规则做了精确断言(见 src/lib.rs 测试模块):
#[test] fn codex_and_claude_stop_are_response_events_not_session_termination() { assert_eq!(Frontend::Codex.mapping("stop"), Some(observe("message_sent"))); assert_eq!(Frontend::Claude.mapping("stop"), Some(observe("message_sent"))); assert_eq!(Frontend::Codex.mapping("session_end"), Some(observe("session_end"))); assert_eq!(Frontend::Claude.mapping("session_end"), Some(observe("session_end"))); } #[test] fn grok_stop_remains_an_explicit_session_termination_exception() { assert_eq!(Frontend::Grok.mapping("stop"), Some(observe("session_end"))); }docs/hooks.md补充了一条关键结论:这些响应事件在本中继上是观察性质的——下游策略可以检查和报告它们,但"不能声称收回前端已经产出的文本"(cannot claim to retract text that the frontend has already produced)。
四、信封格式与校验:OracleHookEvent的完整字段约束
上游capsule-mcp完成 bearer token 认证并剥离 token 后,本胶囊收到的是无 token 的"已验证信封"。源码中的反序列化结构如下:
#[derive(Debug, Deserialize)] struct OracleHookEvent { schema_version: u8, principal_id: String, host: String, session_id: String, event: String, correlation_id: String, route_id: String, delivery_id: String, #[serde(default)] turn_id: Option<String>, #[serde(default)] workspace_id: Option<String>, payload: serde_json::Value, }validate_oracle_hook对该信封执行一组严格校验,任一失败都会记 warn 日志并丢弃该事件(仍返回 Ok,不让上游重试风暴):
| 校验项 | 规则 | 失败原因字符串 |
|---|---|---|
schema_version | 必须等于 1 | unsupported schema version |
host绑定 | 必须与已验证主题对应的前端名一致(codex/claude/grok) | host does not match validated topic |
| 事件支持性 | 前端映射表中必须存在该事件 | unsupported host event |
session_id/event/delivery_id | 干净分段:非空、≤128 字节、仅 ASCII 字母数字加_- | invalid routed segment |
route_id | 小写十六进制,恰好 64 字符 | invalid route identifier |
correlation_id | 小写十六进制,恰好 32 字符 | invalid route identifier |
delivery_id绑定 | 必须等于"{route_id}-{correlation_id}" | delivery identifier does not bind route and correlation |
turn_id | 若存在,非空且 ≤256 字节 | invalid optional routing metadata |
workspace_id | 若存在,干净分段 ≤128 字节 | invalid optional routing metadata |
payload | 序列化后 ≤ 1 MiB(MAX_HOST_PAYLOAD_BYTES = 1024 * 1024) | host payload exceeds limit |
"干净分段"校验同时承担了主题走私防护:session_id中不允许出现.(is_clean_segment只接受 ASCII 字母数字、_、-),因此类似"../session"或"codex.session"的值都会被拒绝,无法通过段名注入改变主题路由。测试用例直接验证了这一点:
#[test] fn event_and_session_cannot_add_topic_segments() { let mut event = host_event("codex"); event.session_id = "codex.other".to_owned(); assert_eq!( validate_oracle_hook(Frontend::Codex, &event), Err("invalid routed segment") ); }此外还有一个身份一致性检查:handle_oracle_hook会用runtime::caller()取内核打戳(kernel-stamped)的调用者主体,与信封中的principal_id比对,不一致即丢弃。这是"前端进程不可信命名主体"原则的落点——认证由内核打戳的调用方身份背书,而非信封自述。
五、发布路径:标准事件如何带上"双重命名"
翻译完成后,dispatch_oracle_hook按响应模式分两条路径:
- Observe 模式:直接
ipc::publish_json("hook.v1.event.{hook}", &request),不订阅任何回复主题; - AdditionalContext 模式:先订阅
hook.v1.response.message_received.{correlation_id},再发布事件,然后进入有界的上下文收集循环。
发布到标准总线的请求体是HookEventRequest(来自astrid_sdk::contracts::hook),其payload字段是一个CanonicalOraclePayload,结构为:
#[derive(Debug, Serialize)] struct CanonicalOraclePayload<'a> { principal_id: &'a str, host: &'a str, session_id: &'a str, source_event: &'a str, // 原始前端事件名(如 "stop"、"user_prompt_submit") #[serde(skip_serializing_if = "Option::is_none")] turn_id: Option<&'a str>, #[serde(skip_serializing_if = "Option::is_none")] workspace_id: Option<&'a str>, payload: &'a serde_json::Value, // 原始前端 payload 嵌套保留 }注意两个设计要点:
- 原始 payload 嵌套保留(nested under canonical provenance),而不是把不可信的键扁平化进信封——这是 docs/hooks.md "Adding a frontend" 清单中的第 5 条;
correlation_id只在 AdditionalContext 模式下透传到标准请求:观察类事件保持"无关联"(测试canonical_request_keeps_observation_uncorrelated验证了 observe 请求的correlation_id为None),避免下游订阅者误以为可以对该事件做出响应;- 整个标准事件序列化后不得超过
MAX_CANONICAL_EVENT_BYTES(1 MiB),超限直接以HostError失败。
测试canonical_events_retain_user_prompt_and_assistant_response_text验证了文本保留语义:user_prompt_submit的prompt文本与 Codexstop的last_assistant_message都原样出现在标准事件的嵌套payload中,source_event字段分别保留为user_prompt_submit与stop。
六、user_prompt_submit的有界上下文收集
message_received是唯一走 AdditionalContext 模式的事件。collect_additional_context的实现约束如下(均为 src/lib.rs 中的常量):
| 常量 | 值 | 语义 |
|---|---|---|
HOST_HOOK_COLLECT_DEADLINE_MS | 1000 ms | 收集总截止期 |
HOOK_QUIESCENCE_MS | 25 ms | 收到至少一条回复后的静默窗口 |
MAX_HOST_CONTEXT_BYTES | 64 KiB | 合并后上下文的总字节上限 |
收集循环的行为:
- 动态等待窗口:尚无回复时等待剩余总时限;已有回复后只等待 25 ms 静默窗口,收到一批回复后即尽快收敛;
- 回复身份过滤:只接受"回复主题精确匹配 且 内核验证主体与
principal_id相同"的消息,其余记 warn 并丢弃; - 格式要求:每条回复必须是含非空字符串
additional_context字段的 JSON,否则丢弃并记 warn; - 整体作废语义:若 IPC 轮询报告
dropped != 0或lagged != 0(扇出丢失/滞后),则丢弃全部已收集的 partial context并返回None——宁可无上下文,也不给出不完整上下文; - 字节上限:
push_context以 checked 算术累计(含 2 字节分隔符),超过 64 KiB 的后续片段被丢弃。测试combined_context_stays_inside_relay_limit精确验证了上限边界:两条 context 恰好填满 64 KiB(含\n\n分隔符)后,第三条被拒。 - 合并输出:所有片段以
"\n\n"连接成单个字符串,作为响应信封中的context字段。
这与docs/hooks.md中 "Additional context" 一节完全一致:"适配只接受同主体回复,在 64 KiB 内合并,若响应订阅报告 lag 或 loss 则丢弃整个部分结果"。
七、响应回传与路由生命周期:canonical_hook的双字段设计
处理结束后,适配器把响应发布到oracle.v1.hook.response.{delivery_id},响应结构为:
#[derive(Debug, Serialize)] struct OracleHookResponse<'a> { schema_version: u8, principal_id: &'a str, host: &'a str, session_id: &'a str, /// 由本前端适配器选定的标准生命周期/观察分类。 /// 源前端事件仍保留在 `event` 字段中。 canonical_hook: &'a str, event: &'a str, correlation_id: &'a str, route_id: &'a str, delivery_id: &'a str, #[serde(skip_serializing_if = "Option::is_none")] context: Option<String>, }README 对canonical_hook与event分开的解释是:适配器响应把源event与canonical_hook独立保留,这样认证路由的清理(cleanup)可以跟随标准生命周期语义,同时不抹掉前端溯源信息(frontend provenance)。
这条设计如何被消费?看 capsule-mcp 的 host_hooks 模块:
relay_response订阅oracle.v1.hook.response.*,重新校验主体、会话 token 与route_id(由host + session_id + token经 blake3 派生,derive_route_id),然后中继到精确的astrid.v1.response.{delivery_id}主题——即前端 uplink 等待的响应;retires_session_route决定何时清理已认证路由(删除 KV 中的 session token):- 响应带
canonical_hook:仅当canonical_hook == "session_end"时才退役路由。因此 Codex/Claude 的stop(canonical 为message_sent)不会退役路由;Grok 的stop(canonical 为session_end)则会; - 分阶段升级兼容:若响应没有
canonical_hook(旧版适配器),只有源event显式为session_end时才退役。
- 响应带
测试only_real_session_end_retires_the_authenticated_route覆盖了这条兼容矩阵的全部四个分支。
同时注意入口侧的对称约束:capsule-mcp只在session_start或user_prompt_submit(can_register)时向 KV 注册 session token(32–128 字节、纯 ASCII 字母数字),并用恒定时间比较(tokens_match逐字节异或)校验 token;验证通过后才发布无 token 的ValidatedHostHook到oracle.v1.hook.validated.{host}。测试validated_shape_does_not_contain_token断言了已验证信封中不存在token字段。
八、下游订阅的约定:省略 priority,保持独立扇出
README 的最后一句给出了对下游标准钩子订阅者的规范约定:通常应省略priority,以保持独立扇出(independent fan-out)。docs/hooks.md 对此有完整契约:
- 观察类订阅者不要设置 priority。相等的默认优先级保持独立并发扇出:每个订阅者都看到原始事件,且一个订阅者无法抑制另一个;
- 不要用 priority 给前端适配器排序——每个已认证 raw 主题只有一个适配器所有者;
- 只有对显式声明为绑定(binding)的中间件主题才设置优先级,且此时同一主题上的所有匹配处理器会从扇出转为单一有序链。文档为绑定中间件保留了指导性优先级带:
| 优先级带 | 用途 | 要求行为 |
|---|---|---|
| 10-19 | 校验与归一化 | 当安全依赖拒绝时,畸形输入返回Deny而非Err |
| 20-39 | 安全与策略收窄 | deny 可中断链;任何层不得扩大先前授权 |
| 40-79 | 确定性转换 | 转换后的 payload 必须显式且经过测试 |
| 80-99 | 富化(enrichment) | 不得把 deny 重新解释为 allow |
| 100+ | provider/应用执行 | 只接收前一阶段接受的 payload |
九、构建形态与依赖
从 Cargo.toml 可见该胶囊以cdylib形式编译为 WASM(Capsule.toml中组件文件为aos_hook_adapter_oracle.wasm,类型为executable),运行于astrid-version >= 0.10.1,依赖仅astrid-sdk、serde、serde_json。源码头部开启#![deny(unsafe_code)]、#![deny(clippy::all)]与#![warn(missing_docs)],是一个完全无 unsafe 的纯协议转换实现。
十、如何扩展:新增前端时的检查清单
docs/hooks.md 的 "Adding a frontend" 一节(该适配器的映射表与校验代码正是这份清单的实践)给出了 8 步清单,可作为参照实现核对:
- 添加认证入口与前端专属的已验证主题;
- 添加一个适配胶囊(或一张隔离的 handler 表),做精确的主题/host 绑定;
- 只映射语义真正等价的事件;
- 把每条映射分类为 observation / context / binding;
- 把原始 payload 嵌套保留在标准溯源之下,不扁平化不可信键;
- 对输入、标准输出、关联标识、回复与等待时间全部设定边界;
- 添加负面测试:跨主题 host 声明、主体不一致、主题走私、过期路由、超尺寸 payload、回复丢失、不支持的事件;
- 不改动内核——翻译与策略都属于胶囊层。
本胶囊自带的负面测试恰好覆盖了清单中的多数项:跨主题 host 声明(each_validated_topic_binds_its_exact_host)、delivery 绑定(delivery_binds_route_and_correlation)、主题走私(event_and_session_cannot_add_topic_segments)、上下文边界(combined_context_stays_inside_relay_limit)与观察/上下文模式区分(canonical_request_keeps_observation_uncorrelated)。
小结
aos-hook-adapter-oracle展示了 AOS 前端钩子体系的一个关键设计:把"翻译"从"策略"中剥离。它用独立映射表消化三个前端在stop语义上的差异,用严格分段/十六进制/字节边界校验抵御主题走私与超尺寸输入,用event+canonical_hook双字段让路由生命周期清理既不破坏前端溯源、又与标准事件语义对齐,并用"部分丢失即整体作废"的上下文收集策略保证additional_context要么完整、要么不存在。对于要在此体系上添加新前端、或编写下游hook.v1.event.*订阅胶囊的开发者,上述映射表、信封约束与 priority 约定就是可以直接对照实施的契约。
【免费下载链接】aos-ce
AOS Community Edition: the open agent operating system.
相关推荐
Impeccable Design Hook 实战指南:在 Claude Code、Cursor、Codex、Grok 与 Copilot 中为 UI 文件安装自动设计检测钩子
Impeccable Design Hook 实战指南:在 Claude Code、Cursor、Codex、Grok 与 Copilot 中为 UI 文件安装
AI 技能前端CLIdsh-pluginUnicity AOS(aos-ce)Capsule 构建全生命周期实战:从脚手架、编译、安装到诊断与升级
Unicity AOS(aos ce)Capsule 构建全生命周期实战:从脚手架、编译、安装到诊断与升级 本文基于 aos ce 仓库中 capsule fo
Unicity AOS 元框架实战:aos-ce 中 meta-harness Skill 如何驱动智能体主动扩展自身世界
Unicity AOS 元框架实战:aos ce 中 meta harness Skill 如何驱动智能体主动扩展自身世界 本文以 aos ce 仓库中 met
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考