一次调用能否成功,由四道闸按顺序决定。它们彼此独立:过了一道,不说明下一道也能过。
最常见的误解是以为有凭证就够了。凭证只过第 2 道。第 3、4 道依然生效 —— 在一个没有声明任何网络规则的 blocklet 上,即使是 owner 身份的调用方,写入照样被拒。
| # | 闸 | 由谁决定 | 失败长什么样 |
|---|---|---|---|
| 1 | 方法白名单 | 这个 JSON-RPC 方法是否匿名安全 | 401 + WWW-Authenticate |
| 2 | 凭证 | 你发的 bearer token 或 session cookie | 401 + WWW-Authenticate |
| 3 | 路径策略 | blocklet 为该路径声明了什么 | 200,带 isError 与 AFS_FORBIDDEN |
| 4 | provider 能力 | 该路径背后的 provider 实现了什么 | 200,带 isError 与「不支持」错误 |
第 1、2 道在传输层:直接以 HTTP 状态码答复,没有 JSON-RPC 结果。第 3、4 道发生在一次成功的 MCP 调用内部 —— 调用本身通了,操作没通。见错误。
为什么是四道
因为它们由三个不同的方决定,谁也不能替谁回答。
| 闸 | 由谁决定 | 它真正在问什么 |
|---|---|---|
| 1、2 | 运行时 | 这个方法在没有调用方时也安全吗?这个调用方是他自称的那个人吗? |
| 3 | blocklet | 我把这条路径向网络开放了吗? |
| 4 | provider | 这个操作我到底实现了没有? |
这就是凭证不够用的原因,也是「换一个更高权限的凭证」同样没用的原因:凭证是给运行时的答案,而运行时并不拥有第 3、4 道闸。没有任何 token 能代替 blocklet 发言,也没有任何 token 能凭空补上 provider 从未实现的操作。
失败形态也由此而来。第 1、2 道在触达工具之前就判完了,所以只能用 HTTP 答复;第 3、4 道是已经跑起来的代码判的,所以答在一个 200 里面。
第 1 道 —— 方法白名单
不带凭证时,只有这些被允许:
| 匿名允许 | 匿名不允许 |
|---|---|
initialize、notifications/initialized | tools/call 调 afs_write、afs_delete、afs_exec |
tools/list、resources/list、prompts/list | 其他任何方法,包括未知方法 |
tools/call 调 afs_read、afs_list、afs_search、afs_stat、afs_explain、search_content、list_content、get_content |
规则是 fail-closed:不在清单上的一律拒绝;批量请求中每一条都必须被允许,否则整批拒绝。
第 2 道 —— 凭证
三种过法:
| 档 | 凭证 | 从哪来 |
|---|---|---|
| 匿名 | 无 | — |
| 认证 | Authorization: Bearer blocklet-… 或 DID-Connect session cookie | 授权客户端或 device grant |
| loopback operator | 经校验的同机 socket peer,仅 Node 运行时 | 无需获取。不通过网络提供;Cloudflare 运行时从不授予 |
Role
人在批准一个客户端时,同意界面会要求选一个访问级别。提供四种:
| Role | |
|---|---|
owner | 同意界面上的默认值 |
admin | |
member | |
guest |
凭证携带这个人所选的 role,作用域限于该 blocklet 实例。它不是全局 role:同一个人可以在一个 blocklet 上是 owner,在另一个上毫无权限。
role 是上界,不是授予。 它打不开第 3 道闸关掉的路径。
第 3 道 —— 路径策略
这是大多数调用方真正撞上的一道,也是任何凭证都绕不过的一道。
blocklet 逐路径决定网络客户端可以做什么。没有被声明的路径对网络关闭 —— 对 owner 也一样。拒绝信息会直接给出修法:
{
"result": {
"content": [{
"type": "text",
"text": "AFS_FORBIDDEN: Forbidden at /instance/notes.txt: network clients cannot write base paths; use a resolver overlay or internal code"
}],
"isError": true
},
"jsonrpc": "2.0",
"id": 3
}读和列出是同一种形状,文案换成 cannot read base paths / cannot list base paths 与 declare a networkRead rule or use an overlay。
声明带来的差别,用两个 blocklet 对照:
| 调用 | 什么都没声明的 blocklet | 声明了集合且 readRole: guest 的 blocklet |
|---|---|---|
匿名 afs_list / | 拒绝 | 200 |
匿名 afs_list /packages | 拒绝 | 200 |
匿名 afs_list /.knowledge | 拒绝 | 拒绝,因为这条路径同样没被声明 |
匿名调已声明集合的 list_content | 工具未注册 | 200 |
owner 凭证 afs_write 任一 base path | 拒绝 | 拒绝 |
最后一行是要记住的那条。从网络写入不是凭证能打开的东西,它需要一个 resolver overlay 或运行在 blocklet 内部的代码。如果你要做的 agent 必须写入,那是 blocklet 拥有者的决定 —— 见声明 agent 能看到什么。
第 4 道 —— provider 能力
每条路径由一个 provider 提供服务,而它只实现其中一部分操作。afs_stat 会同时报告 provider 能做什么、以及你被允许做什么:
{
"capabilities": ["list", "read", "stat", "search", "write", "delete", "exec", "explain"],
"accessMode": "readonly"
}capabilities 是 provider 的实现。accessMode 是你过完第 3 道之后的有效访问。一条路径可以在 capabilities 里列着 write,而对你仍然是 readonly。
把某个操作写进集成之前先查 capabilities。这些操作所属的合同见 AFS。
实际含义
- 读取已发布内容不需要任何凭证 —— 前提是 blocklet 声明了它。这是大多数 agent 的预期路径,也是内容工具存在的意义。
- 取凭证值得做 的场景是:你需要某个特定的人所看到的数据,或者该 blocklet 已经向网络开放了写入路径。
- 凭证不是写入开关。 如果认证之后写入仍被拒,原因是第 3 道闸,而只有 blocklet 拥有者能改。