🧪 Beta — 能力已可用,接口契约可能微调。
第一次委托:30 分钟跑通
读完本指南,你将拥有:
- 一枚由用户亲自授权签发的委托令牌(Delegate Token)——不是密码,不是长期 API Key,是一份短时、缩权、可撤销的授权切片
- 一次完整的验证与兑换:确认令牌有效,并换出产品访问令牌
- 一条可追溯的审计链
auditId——谁授权、授权了什么、多长时间,全程在案
开始前:两件需要先对齐的事
① 域名和端点里为什么是 eak 而不是 genauth:GenAuth 是 Qoni 的身份层,SDK、控制台、访问密钥三者共用 Qoni 这套入口(域名与端点沿用历史标识 eak)——@eazo/anima、dashboard.qoni.ai、/api/v3/eak/... 都是同一套东西的门牌号(详见 GenAuth 是什么)。
② 本文用什么当"被访问的资源":为了让你 30 分钟内跑通完整链路,示例访问的是 Qoni 自带的 Web 搜索能力(scope 形如 webagent.web_search:run),不需要你先改造任何东西。要把委托用在你自己的 API 上,链路完全相同、只换目标资源——见 让 Agent 代表用户调用你的 API 与 保护你的 API。
Step 0 —— 拿到访问密钥(3 分钟)
- 在 dashboard.qoni.ai 进入你的工作空间
- Credentials → Create,配置两个白名单:
allowedScopes:这把密钥允许发起委托的 scope 上限。本教程要用webagent.web_search:run和webagent.web_search:read,至少要包含这两个——配得比后面请求的更小会直接被拒。allowedAgents:允许委托的 Agent 标识。本教程用report-agent,把它填进去。
- 复制 AccessKey / SecretKey。**Secret 只显示一次。**丢了只能轮换重建
- 在同一处登记回调地址(
redirectUri),本教程用http://localhost:3000/qoni/callback
export ANIMA_ACCESS_KEY=ak_demo_xxxxxxxxxxxxxxxx
export ANIMA_SECRET_KEY=sk_demo_xxxxxxxxxxxxxxxx密钥只属于服务端
AK/SK 绝不进浏览器、App 或任何客户端代码。它能替你的组织发起委托——保管要求见安全考量。
Agent 标识(agent)从哪来
本教程直接用字符串 report-agent 跑通,前提是它在上一步的 allowedAgents 里。生产环境里 Agent 应当先注册进台账(有说明、有技术负责人与业务归属人),标识与注册记录对应——见 Agent 的身份模型。
Step 1 —— 装 SDK(30 秒)
npm install @eazo/anima# 不用装。但 HTTP 直连需要自己构造 AK/SK 签名的 Authorization 头,
# 本教程的 cURL 片段用 <AK/SK 签名> 作占位。
# 想直连:用 SDK 导出的 buildStringToSign / buildSignature / buildAuthorization
# 三个函数生成,或先用 SDK 跑通再迁移。见 h2-sdk-reference 与 h1-api-reference。cURL 片段是契约示意,不是可直接粘贴运行的命令
签名算法未在本页展开。要在 30 分钟内跑通,请走 TypeScript 路径,cURL 片段用来对照 HTTP 契约。
Step 2 —— 发起委托,拿到授权链接(8 分钟)
interactive 模式:你替 Agent 提出请求,用户说了才算。
import { Qoni, AnimaScopes } from "@eazo/anima";
const anima = new Qoni({
host: "https://api.eak.eazo.ai",
accessKey: process.env.ANIMA_ACCESS_KEY!,
secretKey: process.env.ANIMA_SECRET_KEY!,
});
const { data } = await anima.delegateToken({
mode: "interactive",
agent: "report-agent",
scopes: [AnimaScopes.WEB_SEARCH_RUN, AnimaScopes.WEB_SEARCH_READ],
redirectUri: "http://localhost:3000/qoni/callback",
state: "demo-state-001",
user: { id: "usr_demo_0001" },
});
console.log(data.authorizationUrl); // 把用户带到这里
console.log(data.grantId); // 回调时要用curl -X POST https://api.eak.eazo.ai/api/v3/eak/delegations \
-H "Authorization: <AK/SK 签名>" \
-H "Content-Type: application/json" \
-d '{
"mode": "interactive",
"agent": "report-agent",
"scopes": ["webagent.web_search:run", "webagent.web_search:read"],
"redirectUri": "http://localhost:3000/qoni/callback",
"state": "demo-state-001",
"userId": "usr_demo_0001"
}'响应里的 authorizationUrl 就是授权确认页入口。scope 用 服务.能力:动作 格式——本例只要了 Web 搜索的执行与读取,没有别的。
两个容易卡住的地方
redirectUri 必须与 Step 0 登记的一致,否则请求会被拒绝。本地开发用 http://localhost:3000/... 即可,不需要公网 https。
user.id 填谁:生产环境里填当前登录用户的 ID(用 anima.currentUser({ accessToken }) 从用户的 access token 解析)。教程里想省事,可以用 await anima.resolveAnyBoundUser() 从绑定用户池取一个真实用户 ID——这个方法只适合 demo 与冒烟测试。
两种写法的对应关系
SDK 里写 user: { id },HTTP 契约里的字段名是 userId——同一件事的两种表达。SDK 也接受顶层 userId,但已标记弃用,新代码请用 user: { id }(见 SDK Reference)。
Step 3 —— 用户点同意,换取委托令牌(10 分钟)
用户打开 authorizationUrl,登录后看到授权确认页:哪个 Agent、哪些权限、多长时间。点同意后,浏览器带着一次性 code 回跳到你的 redirectUri。在回调处理里完成兑换:
// 你的回调路由(下例是 Express;其他框架取 query 的方式类似)
// GET /eak/callback?grantId=...&code=...&state=...
app.get("/eak/callback", async (req, res) => {
const { grantId, code, state } = req.query as Record<string, string>;
// 收到回调先验 state,与你发起时的值一致才继续
if (state !== "demo-state-001") return res.status(400).send("state mismatch");
const { data: grant } = await anima.completeDelegateToken({ grantId, code, state });
console.log(grant.token); // 委托令牌
console.log(grant.expiresIn); // 有效期(秒)
console.log(grant.auditId); // 审计链 ID,从此每一步都在案
console.log(grant.grantedScopes); // 实际授予的 scope
res.send("已授权");
});curl -X POST https://api.eak.eazo.ai/api/v3/eak/delegations/complete \
-H "Authorization: <AK/SK 签名>" \
-H "Content-Type: application/json" \
-d '{ "grantId": "grant_xxx", "code": "code_xxx", "state": "demo-state-001" }'code 是一次性的
授权回调 code 消费一次即失效,重放直接失败。拿到的 token 才是接下来要用的东西。
Step 4 —— 验证与兑换(8 分钟)
先确认这枚令牌真实有效——introspect 会回显全部授权事实:
const { data: info } = await anima.genauth.introspectDelegationToken({
token: grant.token,
});
// { active: true, sub: "usr_demo_0001", agent_id: "report-agent",
// scope: ["webagent.web_search:run", ...], grant_id, audit_id, ... }为什么 introspect 的返回是下划线命名
introspect 直接回显的是令牌里的 claims,claims 按 JWT 惯例用下划线(agent_id、audit_id)。而 SDK 方法的返回值是驼峰(auditId、grantedScopes)。看到两种拼写不是笔误:驼峰=SDK 返回值,下划线=令牌 claims(完整字段表见 Token 与 Claim 参考)。
委托令牌本身不直接访问资源——它的受众被钉死在令牌兑换端点上,拿去调资源会被拒。Agent 要先用它兑换出目标资源的访问令牌(RFC 8693 语义):这一跳的意义在于每次兑换都是一次实时裁决(令牌是否过期、密钥是否停用、scope 是否越界),并且换出的令牌只对一个资源有效,被偷也开不了别的门。
const { data: exchanged } = await anima.request<{
token: string;
tokenType: string;
expiresIn?: number;
}>({
method: "POST",
path: "/api/v3/eak/token-exchange",
body: {
subjectToken: grant.token,
resource: "webagent",
scopes: [AnimaScopes.WEB_SEARCH_RUN],
},
});
console.log(exchanged.token); // 交给 Agent 去调用资源curl -X POST https://api.eak.eazo.ai/api/v3/eak/token-exchange \
-H "Authorization: <AK/SK 签名>" \
-H "Content-Type: application/json" \
-d '{
"subjectToken": "<委托令牌>",
"resource": "webagent",
"scopes": ["webagent.web_search:run"]
}'
# → { "token": "...", "tokenType": "Bearer", "expiresIn": ... }换出的访问令牌里,sub 仍是用户、act 标识 Agent——"以谁的身份、由谁行动"在令牌里分得清清楚楚(字段详解见 Token 与 Claim 参考)。把它交给 Agent 去调用对应产品,任务就跑起来了。
然后呢
- 令牌会过期:
expiresIn到点即失效(60 秒到 24 小时,按你的请求)。任务级委托建议给短期限。 - 随时可撤:吊销的分层与时效见吊销与应急处置。
- 全程可查:拿着
auditId去看审计与追责链。
服务端受信集成(免用户逐次确认)
你的应用已有登录体系、想以组织名义直接为用户签发?走组织级授权确认的 silent 路径——先读接入你已有的认证体系与安全考量的硬约束清单。