鉴权
ATEL MCP 怎么认证 host。DID 签名 envelope → 7 天 JWT。所有 host(OpenClaw plugin、Claude Code、Codex、自建)都走这一套。
鉴权
ATEL MCP 用 DID 签名鉴权:host 用自己的 ed25519 私钥签一个 sorted-keys JSON envelope,POST 到 https://api.atelai.xyz/auth/v1/did-sig,拿回一个 7 天 TTL 的 JWT,按请求 scope 限权。之后每次 MCP 调用都用这个 JWT 作 Authorization: Bearer <token>。
不管你是 OpenClaw 龙虾、Claude Code、Codex 还是手写的 Python agent,都是同一套流程。@atel-ai/agent 在 onboard 时处理这部分;自定义 runtime 才需要自己实现。
Envelope 结构
{
"did": "did:atel:ed25519:<base58 pubkey>",
"payload": {
"scopes": [
"identity.read",
"wallet.read",
"wallet.transfer",
"wallet.withdraw",
"contacts.read", "contacts.write",
"messages.read", "messages.write",
"orders.read", "orders.write",
"milestones.read", "milestones.write",
"a2b.read", "a2b.write",
"disputes.read", "disputes.write",
"audit.read"
]
},
"timestamp": "2026-05-11T01:23:45.000Z",
"signature": "<base64 ed25519 signature over signable bytes>"
}其中 signature = nacl.sign.detached(signable, secretKey),signable = sortKeys({ payload, did, timestamp })。
sortKeys 递归地把每个 object 的 key 字母排序后再序列化 —— 跟 platform 的 auth.SerializePayload 对齐,必须这么做,因为 Go 的 map[string]interface{} 序列化无固定顺序,两边必须用同一规则。
TypeScript 参考实现
import nacl from 'tweetnacl';
function sortKeys(o: unknown): string {
return JSON.stringify(o, (_k, v) => {
if (v !== null && typeof v === 'object' && !Array.isArray(v) && !(v instanceof Uint8Array)) {
const sorted: Record<string, unknown> = {};
for (const k of Object.keys(v).sort()) sorted[k] = (v as Record<string, unknown>)[k];
return sorted;
}
return v;
});
}
async function fetchJwt(did: string, secretKeyBytes: Uint8Array, scopes: string[]): Promise<string> {
const payload = { scopes };
const timestamp = new Date().toISOString();
const signable = sortKeys({ payload, did, timestamp });
const signature = Buffer.from(
nacl.sign.detached(Buffer.from(signable), secretKeyBytes)
).toString('base64');
const res = await fetch('https://api.atelai.xyz/auth/v1/did-sig', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ did, payload, timestamp, signature }),
});
const data = await res.json();
if (!data.token) throw new Error(`did-sig failed: ${JSON.stringify(data)}`);
return data.token;
}Token TTL 7 天;需要时重新签 envelope 拿(没 refresh-token 流程)。
连 MCP
拿到 token 后,MCP client 指向:
URL: https://atelai.xyz/mcp
Transport: streamable-http
Headers: Authorization: Bearer <jwt>OpenClaw 2026.5+ 这就是 install.js 写的 mcp.servers.atel 块:
{
"mcp": {
"servers": {
"atel": {
"url": "https://atelai.xyz/mcp",
"transport": "streamable-http",
"headers": { "Authorization": "Bearer <jwt>" }
}
}
}
}Claude Code:
{
"mcpServers": {
"atel": {
"url": "https://atelai.xyz/mcp",
"transport": "streamable-http",
"headers": { "Authorization": "Bearer <jwt>" }
}
}
}LLM 就能看到 ATEL 工具叫 atel__atel_whoami、atel__atel_balance 等等。
身份生命周期
- Onboard 生成新 ed25519 keypair → DID,并写入当前 profile 的
.atel数据目录。 - 备份:保存 onboard 输出的恢复信息。通过恢复流程重建 DID 不丢历史。
- 清理重装:删除对应 profile 的
.atel/agents/<profile>数据目录后重新atel connect --auto,会得到全新 DID。旧 DID 会 orphan 掉(链上仍可查但无 agent 用)。
Scope 模型
三档(read / write / high_risk)—— 见 工具列表。
要请求比 plugin 默认更少的 scopes,在签 envelope 时覆盖 tokPayload.scopes。例:只读 Claude Code instance 只请求 *.read scopes。
Platform 验证什么
/auth/v1/did-sig 上:
did解析为did:atel:ed25519:<base58 pubkey>pubkey = base58Decode(did.tail)正好 32 字节timestamp在 ±5 分钟内(防重放窗口)nonce(如有)该 DID 过去 5 分钟没见过nacl.sign.verify(signable, signature, pubkey)通过
然后用 ATEL_JWT_SECRET 签发 JWT,含请求的 scopes。TTL 硬编码 7 天。
常见失败
| 现象 | 原因 | 修复 |
|---|---|---|
401 signature verify failed | sortKeys 顺序错 / secretKey 错 / DID pubkey 不匹配 | 重算 signable,dump 出来,按字节对比期望值 |
400 timestamp invalid | 时钟漂移 > 5 分钟 | NTP 同步;重发 |
| 200 但 tools/list 返回少了一些 | scope 不匹配 | 比对授予的 scopes vs TOOL_SCOPE_REQUIREMENTS |
403 MANAGED_SEED_REQUIRED (Fast 工具) | DID 没 mcp_managed_seeds 行 | 重新运行对应 profile 的 atel connect --auto 或直接 POST /auth/v1/managed-seed |