☰
aos-ce: aos-hook-adapter-oracle 如何将 Codex、Claude 与 Grok 前端钩子事件翻译为 AOS 标准钩子协议
2026/9/25 3:41:28 网站建设 项目流程

【免费下载链接】aos-ce

AOS Community Edition: the open agent operating system.

项目地址:https://gitcode.com/gh_mirrors/ao/aos-ce
点击查看免费下载

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)转化为授权决策。两条具体边界值得强调:

  1. user_prompt_submit只收集有界的additional_context回复,且只针对当前精确的宿主回合(exact host turn)收集;
  2. 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_startsession_startObserve
session_endsession_endObserve
user_prompt_submitmessage_receivedAdditionalContext
pre_tool_usebefore_tool_callObserve
permission_requestbefore_tool_callObserve
post_tool_useafter_tool_callObserve
pre_compacton_compaction_startedObserve
post_compacton_compaction_completedObserve
subagent_startsubagent_startObserve
subagent_stopsubagent_stopObserve

表中除user_prompt_submit外全部为Observe模式。源码对pre_tool_use | permission_request的注释点明了原因:本中继外层响应 schema 只能携带 context,因此这些事件在此只是观察;绑定原生工具决策走astrid-gate。

3.2 三个前端的差异:stop与message_display

这是 README 着墨最多、也是适配器中最容易被误解的部分:

前端事件映射结果原因
Codexstopmessage_sentper-turn 事件,携带last_assistant_message
Claudestopmessage_sent同上
Claudemessage_displaymessage_displayed渲染期间以delta携带响应文本
Grokstopsession_end兼容性例外
  • Codex 与 Claude 的stop是回合级响应事件而非会话终止:它们携带已完成回合的last_assistant_message,因此发布为标准message_sent;只有显式的session_end事件才映射为标准session_end。
  • Grok 是显式的兼容性例外:当前安装的 Grok hook 契约没有暴露一个独立的、已验证的终止事件,因此其stop被映射为标准session_end。源码注释写道:"在 Grok 上游 hook 契约提供独立的、已验证的 per-turn 响应事件之前,保留现有解释"。
  • Claudemessage_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必须等于 1unsupported 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按响应模式分两条路径:

  1. Observe 模式:直接ipc::publish_json("hook.v1.event.{hook}", &request),不订阅任何回复主题;
  2. 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_MS1000 ms收集总截止期
HOOK_QUIESCENCE_MS25 ms收到至少一条回复后的静默窗口
MAX_HOST_CONTEXT_BYTES64 KiB合并后上下文的总字节上限

收集循环的行为:

  1. 动态等待窗口:尚无回复时等待剩余总时限;已有回复后只等待 25 ms 静默窗口,收到一批回复后即尽快收敛;
  2. 回复身份过滤:只接受"回复主题精确匹配 且 内核验证主体与principal_id相同"的消息,其余记 warn 并丢弃;
  3. 格式要求:每条回复必须是含非空字符串additional_context字段的 JSON,否则丢弃并记 warn;
  4. 整体作废语义:若 IPC 轮询报告dropped != 0或lagged != 0(扇出丢失/滞后),则丢弃全部已收集的 partial context并返回None——宁可无上下文,也不给出不完整上下文;
  5. 字节上限:push_context以 checked 算术累计(含 2 字节分隔符),超过 64 KiB 的后续片段被丢弃。测试combined_context_stays_inside_relay_limit精确验证了上限边界:两条 context 恰好填满 64 KiB(含\n\n分隔符)后,第三条被拒。
  6. 合并输出:所有片段以"\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 模块:

  1. relay_response订阅oracle.v1.hook.response.*,重新校验主体、会话 token 与route_id(由host + session_id + token经 blake3 派生,derive_route_id),然后中继到精确的astrid.v1.response.{delivery_id}主题——即前端 uplink 等待的响应;
  2. 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 步清单,可作为参照实现核对:

  1. 添加认证入口与前端专属的已验证主题;
  2. 添加一个适配胶囊(或一张隔离的 handler 表),做精确的主题/host 绑定;
  3. 只映射语义真正等价的事件;
  4. 把每条映射分类为 observation / context / binding;
  5. 把原始 payload 嵌套保留在标准溯源之下,不扁平化不可信键;
  6. 对输入、标准输出、关联标识、回复与等待时间全部设定边界;
  7. 添加负面测试:跨主题 host 声明、主体不一致、主题走私、过期路由、超尺寸 payload、回复丢失、不支持的事件;
  8. 不改动内核——翻译与策略都属于胶囊层。

本胶囊自带的负面测试恰好覆盖了清单中的多数项:跨主题 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.

项目地址:https://gitcode.com/gh_mirrors/ao/aos-ce
点击查看免费下载
上一篇:Flipper Zero社区治理终极指南:如何建立高效的贡献者协作体系
下一篇:Java注解继承:toBeBetterJavaer元注解组合

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询