docs: B-8 在 HomeAgent 上是 N/A(平台无审批环节),补齐平台矩阵第四列
B-8 的 homeagent 那格一直标着「❌ 要先摸清 homed approval API」。
查清了:**那个 API 不存在,而且不该存在。**
判据:SDK 与核心两处 grep `approval|consent|permission|confirm`,
命中数均为 0。
前置条件是「平台本来就要问人」。另三个平台各有一个现成的审批环节
(opencode `permission.ask` / DSH `approval/request` / pi `tool_call`),
桥做的只是把它从本地 TUI 改道到邮件通道 —— 没有发明审批协议。
HomeAgent 的核心是纯思维核:本身无对外交互能力(全部能力来自插件),
也没有会话这一层(单事件循环)。它不问人,工具调用直接执行。
它确实有 `StageBeforeToolcall` 可以拦下调用(`process.go:273`,插件给
`ctx.Response` 赋值即拒绝,核心把「工具 X 已被插件拒绝」喂回模型)。
但那是「插件可以否决」而非「平台在征求同意」:没有待批准的请求、
没有选项、也没有等人的语义。
所以 B-8 是 N/A 而不是待办。硬补等于给平台加它本来没有的能力 ——
要自己划高风险工具白名单、自己定义超时与 fail closed、自己决定人不在时
怎么办,那些是产品决策不是契约合规。
顺带记下一个需要知道的事实:homeagent 的工具全部无条件执行(含 cmd_run),
接入邮件之后任何能给它发信的人或 Agent 都能间接触发,中间没有人类确认。
另三条链至少有 409 兜底,这条没有 —— 因为它根本不发起询问。
这不是缺陷而是那个平台的信任模型(homed 跑在用户自己机器上,默认完整权限),
记下来是为了让「谁能给 homeagent 发信」被当作访问控制来对待。
平台差异对照表补齐 HomeAgent 一列(14 行)。它是四平台里唯一**不需要**
为每封邮件开平台侧会话的:没有会话概念,所有邮件注入同一事件循环,
靠 output_send__agentmail 输出通道送回复。
This commit is contained in:
@ -355,3 +355,63 @@ T 前的 `M`(月)与正号 trigger 一律忽略而不是乱换算)。
|
||||
会被截断。常见客户端导出的短字段不受影响
|
||||
- `RRULE` 只认 `FREQ=`,忽略 `INTERVAL`/`BYDAY`/`COUNT`/`UNTIL`
|
||||
- 周/日视图不按小时定位色块高度(事件都是等高行,不体现时长)
|
||||
|
||||
|
||||
---
|
||||
|
||||
## B-8(权限询问转邮件)在 HomeAgent 上不适用
|
||||
|
||||
四平台能力矩阵里 homeagent 的 B-8 一直标着「❌ 要先摸清 homed approval API」。
|
||||
本轮查清了:**那个 API 不存在,而且不该存在。**
|
||||
|
||||
判据:在 SDK(`third_party/homeagent-sdk/sdk/*.go`)与核心
|
||||
(`internal/**/*.go`)两处 grep `approval|consent|permission|confirm`,
|
||||
命中数均为 **0**。
|
||||
|
||||
原因是设计取向不同。另三个平台各有一个现成的审批环节,桥做的只是把它从
|
||||
本地 TUI **改道**到邮件通道:
|
||||
|
||||
| 平台 | 审批钩子 |
|
||||
|---|---|
|
||||
| opencode | `permission.ask` |
|
||||
| DSH | `approval/request` |
|
||||
| pi | `tool_call` |
|
||||
|
||||
HomeAgent 的核心是一个**纯思维核** —— 本身没有任何对外交互能力,
|
||||
全部能力来自插件,也没有会话这一层(单事件循环)。它不问人:
|
||||
工具调用直接执行。
|
||||
|
||||
它确实有一个可以拦下调用的位置:`StageBeforeToolcall`
|
||||
(`internal/agent/core/process.go:273`,插件给 `ctx.Response` 赋值即视为拒绝,
|
||||
核心会把「工具 X 已被插件拒绝」当作 tool 结果喂回模型并发 `status: "denied"`
|
||||
事件)。但那是「插件可以否决」而不是「平台在征求同意」:没有待批准的请求、
|
||||
没有选项、也没有等人的语义。
|
||||
|
||||
**因此 B-8 在这个平台上是 N/A,不是待实现项。** 硬要补等于给平台加一层
|
||||
它本来没有的能力:要自己划高风险工具白名单、自己定义超时与 fail closed
|
||||
语义、自己决定人不在时怎么办 —— 那些都是产品决策而不是契约合规。
|
||||
(技术上可行:`RegisterTool` 的 handler 是我们的代码,能在执行前发权限邮件
|
||||
并阻塞等待;`InjectInputSync` 证明这套 SDK 里「同步等外部答复」是既有形态。
|
||||
但没有需求驱动就不做。)
|
||||
|
||||
### 随之而来的一个事实,需要知道
|
||||
|
||||
homeagent 的工具**全部无条件执行**,其中包括 `cmd_run` 这类能力。
|
||||
它接入 AgentMail 之后,任何能给 `homeagent@…` 发信的人或 Agent 都能间接
|
||||
触发这些工具,中间没有人类确认环节。
|
||||
|
||||
另三条链上至少有一道兜底:桥收到 409(Gateway 判定整条会话树上没有人类
|
||||
可路由)时当场表态拒绝。homeagent 这条链没有这一环 —— 因为它根本不发起询问。
|
||||
|
||||
这不是缺陷,是那个平台的信任模型:homed 及其插件都跑在用户自己的机器上,
|
||||
默认完整权限。记在这里是为了让「谁能给 homeagent 发信」这个问题被当作
|
||||
访问控制来对待,而不是当作邮件权限。
|
||||
|
||||
### 契约文档的改动
|
||||
|
||||
- `B-8` 标题从「若平台支持,MUST」改为「若平台有**审批环节**,MUST;没有则 N/A」,
|
||||
并补一段说明前置条件与 `StageBeforeToolcall` 的语义差异
|
||||
- 平台差异对照表补齐 **HomeAgent 一列**(14 行)。它是四个平台里
|
||||
唯一不需要为每封邮件开平台侧会话的 —— 没有会话概念,所有邮件注入同一个
|
||||
事件循环,靠 `output_send__agentmail` 输出通道送回复
|
||||
- 检查清单里「未决权限询问 fail closed」标注适用条件
|
||||
|
||||
@ -313,11 +313,30 @@ SSE 只推连上之后的事件。插件重启前发来的邮件不会再推一
|
||||
> **B-7.5 为什么跳过 permission**:原来的工具调用早随进程一起没了,
|
||||
> 投过去模型没有可恢复的上下文。
|
||||
|
||||
### B-8 平台权限询问 → 邮件(若平台支持,MUST)
|
||||
### B-8 平台权限询问 → 邮件(若平台有审批环节,MUST;没有则 N/A)
|
||||
|
||||
挂平台的权限/审批钩子,把询问转成一封邮件问人。这是 `I-1` 最直接的体现:
|
||||
被平台真正拦下的那一次才是事实,不依赖模型「记得」问人。
|
||||
|
||||
> **前置条件是「平台本来就要问人」。**
|
||||
>
|
||||
> 三个已实现的平台各有一个现成的审批环节,桥做的只是把它从本地 TUI
|
||||
> **改道**到邮件通道:opencode 的 `permission.ask`、DSH 的 `approval/request`、
|
||||
> pi 的 `tool_call`。桥没有发明审批协议。
|
||||
>
|
||||
> HomeAgent 的设计不同:核心是一个纯思维核,本身没有任何对外交互能力
|
||||
> (能力全部来自插件),因此它**不问人** —— 工具调用直接执行。
|
||||
> 它确实有 `StageBeforeToolcall` 这个可以拦下调用的位置
|
||||
> (插件给 `ctx.Response` 赋值即视为拒绝),但那是「插件可以否决」而不是
|
||||
> 「平台在征求同意」:没有待批准的请求、没有选项、也没有等人的语义。
|
||||
>
|
||||
> 在那种平台上,B-8 整条**不适用**(N/A),不是待实现项。硬要补的话等于
|
||||
> 给平台加一层它本来没有的能力:需要自己划定高风险工具白名单、自己定义
|
||||
> 超时与 fail closed 语义 —— 那是产品决策,不是契约合规。
|
||||
>
|
||||
> 判据:在 SDK 与核心两处 grep `approval|consent|permission|confirm`,
|
||||
> 命中数均为 0。
|
||||
|
||||
```
|
||||
平台权限钩子被调用
|
||||
├─ 1. POST /permission/request(带平台的权限 id 作 relay_key)
|
||||
@ -582,7 +601,7 @@ SSE 只推连上之后的事件。插件重启前发来的邮件不会再推一
|
||||
|
||||
### D-5 别名来源阶梯(`C-11` 缺失或不可用时)
|
||||
|
||||
| 优先级 | 别名来源 | 何时用 |
|
||||
| 优先级 | 别名来源 | 何时用 | 邮件主题派生(无平台标题) |
|
||||
|---|---|---|
|
||||
| 1 | 平台自带的 slug(如 `witty-planet`) | 平台创建会话时就给 |
|
||||
| 2 | 由平台标题派生(`slugFromTitle`) | 有标题且标题可用 |
|
||||
@ -1018,7 +1037,7 @@ GET /api/v1/attachments/{id}
|
||||
[ ] sqlite3 <db> "SELECT status, last_seen FROM agents WHERE agent_name='<name>'"
|
||||
→ status=online,last_seen 每 30 秒推进(B-2.1)
|
||||
[ ] 断网 60 秒再恢复:SSE 自动重连,期间的邮件通过 Last-Event-ID 补回(D-7.2)
|
||||
[ ] kill 插件:未决权限询问全部 fail closed(B-8.2)
|
||||
[ ] kill 插件:未决权限询问全部 fail closed(B-8.2;平台无审批环节时跳过)
|
||||
```
|
||||
|
||||
### 7.3 主链路
|
||||
@ -1144,24 +1163,30 @@ GET /api/v1/attachments/{id}
|
||||
|
||||
三次真实适配的对照表。接新平台时逐行回答「我这边是什么」。
|
||||
|
||||
| 关注点 | opencode | DeepSeek Harness | pi |
|
||||
|---|---|---|---|
|
||||
| 插件形态 | `export default async function(input)` | Cordis:`export const inject` + `apply(ctx, config)` | **常驻守护进程**,不是插件(见下) |
|
||||
| 建会话 | `client.session.create({ query: { directory } })` | `ctx.agents.create({ sessionId, meta: { cwd }, agentOptions })` | `createAgentSession({ cwd, sessionManager, … })` |
|
||||
| 注入一轮 | `client.session.promptAsync({ parts })` | `agent.followup(UserMessage)` | `session.prompt(text)`;忙时 `prompt(text, {streamingBehavior:'followUp'})` |
|
||||
| 工具定义 | zod schema | `defineTool()` + spec 格式参数 | `defineTool()` + **TypeBox** schema |
|
||||
| 轮次结束 | `session.idle` 事件 | `agent/status` → `idle` | `prompt()` 的 promise resolve;事件是 `agent_end` |
|
||||
| 模型失败信号 | `session.error` 事件 | `turn/end` 的 `reason.kind === 'error'` | `prompt()` reject **或** 末条 assistant 的 `stopReason==='error'` |
|
||||
| 权限钩子 | `permission.ask`(**同步,不能等**) | `approval/request`(异步 waterfall,**能等**) | `tool_call` 扩展事件(**能 await**,实测) |
|
||||
| 会话列表 | `client.session.list()` | `ctx.sessionQuery.listSessions()` | `SessionManager.listAll()`(**不传参**,传字符串会被当自定义目录) |
|
||||
| 模型目录 | `client.config.providers()`(`models` 是**对象**) | `ctx.llm.listProviders()` + `listModels()` | `modelRuntime.getAvailable()`(**不是** `getModels()`:1221 条里只有 1 条能用) |
|
||||
| 别名来源 | `session.slug`(创建时就有) | 模型标题派生 | **邮件主题派生**(SDK 会话没有平台标题,见下) |
|
||||
| 日志可见性 | `console.error` | `console.error`(`ctx.logger` 不进 journalctl) | `console.error` |
|
||||
| 工作区分组 | `session.create({directory})` 自带 | `ctx.get('workspaceRegistry')` → `create(cwd)` + `attachSession()` | 按 cwd 自动分目录(`~/.pi/agent/sessions/--tmp-x--/`),无需注册 |
|
||||
| 工具参数命名 | `path` / `save_to` | `file_path` / `save_path` | `file_path` / `save_path` |
|
||||
| 权限三态 | 原生 `once`/`always`/`reject` | **只有** `allowed-once`/`rejected` | 只有 block/放行 |
|
||||
| 「一直同意」 | 给,平台自己记 | **不给**(表达不了,桥代劳会覆盖平台策略) | 给,桥用 `createGrantStore()` 记 |
|
||||
| 关注点 | opencode | DeepSeek Harness | pi | HomeAgent |
|
||||
|---|---|---|---|---|
|
||||
| 插件形态 | `export default async function(input)` | Cordis:`export const inject` + `apply(ctx, config)` | **常驻守护进程**,不是插件(见下) | 独立子进程 `plugin.bin`,SDK 走 C ABI |
|
||||
| 建会话 | `client.session.create({ query: { directory } })` | `ctx.agents.create({ sessionId, meta: { cwd }, agentOptions })` | `createAgentSession({ cwd, sessionManager, … })` | **没有会话概念** —— 单事件循环,不做会话映射 |
|
||||
| 注入一轮 | `client.session.promptAsync({ parts })` | `agent.followup(UserMessage)` | `session.prompt(text)`;忙时 `prompt(text, {streamingBehavior:'followUp'})` | `s.InjectText(source, channel, text)` / `InjectInputSync`(同步等回复) |
|
||||
| 工具定义 | zod schema | `defineTool()` + spec 格式参数 | `defineTool()` + **TypeBox** schema | `s.RegisterTool(name, ToolDef, handler)`,Parameters 是裸 JSON Schema map |
|
||||
| 轮次结束 | `session.idle` 事件 | `agent/status` → `idle` | `prompt()` 的 promise resolve;事件是 `agent_end` | 无「轮次」事件;靠 `RegisterOutputChannel` 的 handler 被调用 |
|
||||
| 模型失败信号 | `session.error` 事件 | `turn/end` 的 `reason.kind === 'error'` | `prompt()` reject **或** 末条 assistant 的 `stopReason==='error'` | 无(核心不把模型错误暴露给插件) |
|
||||
| 权限钩子 | `permission.ask`(**同步,不能等**) | `approval/request`(异步 waterfall,**能等**) | `tool_call` 扩展事件(**能 await**,实测) | **无审批环节**(核心不问人)。有 `StageBeforeToolcall` 可否决,但语义不同 —— 见 `B-8` |
|
||||
| 会话列表 | `client.session.list()` | `ctx.sessionQuery.listSessions()` | `SessionManager.listAll()`(**不传参**,传字符串会被当自定义目录) | N/A |
|
||||
| 模型目录 | `client.config.providers()`(`models` 是**对象**) | `ctx.llm.listProviders()` + `listModels()` | `modelRuntime.getAvailable()`(**不是** `getModels()`:1221 条里只有 1 条能用) | N/A(模型由核心配置,插件不选) |
|
||||
| 别名来源 | `session.slug`(创建时就有) | 模型标题派生 | **邮件主题派生**(SDK 会话没有平台标题,见下) | 邮件主题派生(无平台标题) |
|
||||
| 日志可见性 | `console.error` | `console.error`(`ctx.logger` 不进 journalctl) | `console.error` | `log.Printf` 进 homed 的 journalctl(带 `[plugin]` 前缀) |
|
||||
| 工作区分组 | `session.create({directory})` 自带 | `ctx.get('workspaceRegistry')` → `create(cwd)` + `attachSession()` | 按 cwd 自动分目录(`~/.pi/agent/sessions/--tmp-x--/`),无需注册 | N/A(无会话,无 cwd 概念) |
|
||||
| 工具参数命名 | `path` / `save_to` | `file_path` / `save_path` | `file_path` / `save_path` | 自定(本桥用 `file_path` / `save_path` 对齐其他平台) |
|
||||
| 权限三态 | 原生 `once`/`always`/`reject` | **只有** `allowed-once`/`rejected` | 只有 block/放行 | N/A |
|
||||
| 「一直同意」 | 给,平台自己记 | **不给**(表达不了,桥代劳会覆盖平台策略) | 给,桥用 `createGrantStore()` 记 | N/A |
|
||||
|
||||
> **HomeAgent 为什么整列都是 N/A**:它的核心是一个纯思维核 ——
|
||||
> 本身没有任何对外交互能力,全部能力来自插件,也没有「会话」这一层
|
||||
> (单事件循环)。因此桥在这个平台上不做会话映射:所有邮件注入同一个循环,
|
||||
> 靠 `output_send__agentmail` 输出通道把回复送出去。
|
||||
> 它是四个平台里唯一**不需要**为每封邮件开一条平台侧会话的。
|
||||
>
|
||||
> **pi 为什么是守护进程而不是扩展**:pi 扩展被加载进**一条已经存在的**会话,
|
||||
> 那条会话的 cwd 由启动 pi 的人决定。而 `B-3.1` 要求每封邮件的 `to_workspace`
|
||||
> 成为会话 cwd —— 扩展做不到「按邮件新开一条 cwd 不同的会话」。
|
||||
|
||||
Reference in New Issue
Block a user