让 Agent 代表用户调用你的 API
本页说明自定义 API 的 on-behalf-of(OBO)链路,以及它与当前 Qoni SDK 产品接口的边界。
当前 SDK 边界
@qoniai/qoni@0.4.1 的具名产品接口覆盖 GUMem 和 Web Agent,并只接受 SDK 已知的产品 scope。它没有公开一个可用任意自定义 resource 与自定义 scope 的端到端具名方法。
因此,不要把 QoniScopes.WEB_SEARCH_*、DO_ANYTHING_* 等 Qoni 产品权限写进“你的 API”示例,也不要把 qoni.webSearch.run() 当成调用自定义 API。自定义 API OBO 需要先在 GenAuth 中配置资源与 scope,再按 HTTP API Reference 完成协议调用。
两条不同的调用路径
| 目标资源 | 推荐调用方式 | token 处理 |
|---|---|---|
| Qoni GUMem / Web Agent | 使用 qoni.gumem.*、qoni.webSearch.*、qoni.doAnything.*、qoni.deepResearch.* 或 qoni.track.* | 把 grant.token 传给具名方法;SDK 内部完成产品 token exchange |
| 你的自定义 API | 使用 GenAuth HTTP 契约和你已配置的 resource / scope | 兑换出 audience 指向你的 API 的 access token,再由你的 API 验证 |
这两条路径共享委托、审计和缩权概念,但不能互换代码或 scope。
自定义 API OBO 链路
1. 配置资源与 scope
先在 GenAuth 中登记你的 API 资源标识和可委托 scope。资源标识、scope 名称和 token audience 必须来自实际配置,不能从 Qoni Web Agent 常量推断。
2. 发起用户授权
你的服务端通过 GenAuth HTTP API 创建 interactive 委托。请求至少包括:
- 稳定的 Agent 标识。
- 你的 API 已登记的 scope。
- 已登记的
redirectUri。 - 服务端生成并保存的防重放
state。
interactive 模式由授权页解析当前登录用户,不要额外发送旧版 SDK 的 task、agentKey 或顶层 userId 模型。
3. 完成回调
用户同意后,回调带回一次性 code、你的业务 state 和 grant_state,不带 grantId。按 grant_state 找回创建时保存的记录并核对业务 state,再用保存的 grantId、回调的 code,以及作为 state 传入的 grant_state 完成兑换。委托 token 只能保存在可信服务端。
4. 兑换自定义 API access token
按 API Reference 调用 token exchange。请求中的 resource 必须是你的 API 已登记的资源标识,scopes 必须是委托实际授予 scope 的子集。
不要在文档或应用代码中使用 webagent 作为自定义 API 的 resource 占位符;那会把请求导向 Qoni Web Agent,而不是你的服务。
5. 在资源侧验证
你的 API 收到 Authorization: Bearer <access-token> 后至少完成:
- 使用可信 JWKS 验证签名。
- 校验
iss、aud、exp和nbf。 - 从
sub识别被代表的用户,并执行你的业务权限检查。 - 从
act识别执行的 Agent,并写入审计记录。 - 校验 scope 覆盖当前操作。
- 拒绝 audience、用户、Agent 或 scope 不匹配的请求。
完整资源侧清单见 保护你的 API,claim 结构见 Token 与 Claim 参考。
验收检查
- 用 read-only scope 调用写接口时,资源侧必须返回拒绝。
- access token 的
aud必须是你的 API,而不是webagent或其他产品。 sub、act、scope、grant ID 和 audit ID 能串成同一条审计链。- 用户撤销 grant 后,后续兑换或调用按 GenAuth 的吊销策略失败。
如果你只需要调用 Qoni 产品
不要使用本页的自定义 API 流程。完成 delegateToken() 后,直接把 grant.token 交给具名产品方法:
const search = await qoni.webSearch.run({
token: grant.token,
prompt: 'Qoni Agent Identity',
})
const result = await search.wait()