示例

本页的示例各自独立。每个示例说明其前置条件,给出完整的命令或配置,并展示预期结果。除非另有说明,示例均使用 教程 中创建的两个身份 provider 与 requester,在同一台机器上运行,并注册到同一个 Hub。

带速率限制的免费能力

公开能力对 Hub 上的所有调用方开放。inbound.public_capabilities 中的对应条目限定单个调用方以及全部调用方合计可施加于服务的负载。

前置条件:教程中的服务监听 ~/.anet-tutorial/digest.sock。

替换提供方 config.json 中的条目,使该能力免费并受到限制:

{
  "inbound": {
    "policy": "closed",
    "public_capabilities": [
      {
        "id": "text.digest",
        "per_caller_per_min": 6,
        "per_caller_per_day": 200,
        "global_per_min": 60,
        "max_inflight": 4,
        "per_caller_inflight": 1,
        "max_args_bytes": 65536
      }
    ]
  },
  "modules": {
    "service": {
      "timeout_ms": 5000,
      "capabilities": [
        {
          "id": "text.digest",
          "url": "unix:///home/user/.anet-tutorial/digest.sock:/digest",
          "description": "SHA-256 digest of a text"
        }
      ]
    }
  }
}

url 的值必须是绝对路径。配置仅显示发生变化的键,文件中的其他键保持不变。各项限制如下:

键

限制

per_caller_per_min、per_caller_per_day

单个调用方每分钟和每天的调用次数。

global_per_min

全部调用方合计每分钟的调用次数。

max_inflight、per_caller_inflight

同时运行的调用数量,分别为总数和单个调用方的数量。调用方限制默认为 1,且不能超过 max_inflight。

max_args_bytes

调用参数的大小。

timeout_ms(service 模块)

对服务的单次调用的时间限制。

省略的键采用默认值。未设置 price 的能力为免费,请求方不会收到报价。

重启提供方,并从请求方调用该能力:

$ anet stop provider && anet up provider
$ anet --id requester delegate "$PROVIDER" --capability text.digest \
    --args '{"text":"hello"}' > /dev/null
$ anet --id requester results
{
  "results": [
    {
      "goal": "invoke capability text.digest",
      "state": "completed",
      ...
    }
  ]
}

该调用免费,因此不输出支付行,也不消耗任何限额。

识别调用方的服务与仅限名单的能力

标记为 allow_only 的能力只向允许列表或信任列表中的对端提供。对其他所有调用方,该能力不存在:应答内容与节点不具备该能力时相同,到达时间也相同,且它不出现在 Hub 可见的任何卡片中。

守护进程通过请求头而非正文,将已认证调用方的身份传递给服务,因此为纯 JSON 编写的服务仍可正常工作。令牌文件使服务能够验证请求来自守护进程,而不是能够打开该套接字的其他进程。

本例中的服务会返回其收到的请求头。将其保存为 whoami_service.py:

import json
import os
import socketserver
from http.server import BaseHTTPRequestHandler

BASE = os.path.expanduser("~/.anet-example")
SOCKET = os.path.join(BASE, "whoami.sock")
TOKEN = open(os.path.join(BASE, "token")).readline().strip()


class UnixServer(socketserver.UnixStreamServer):
    def get_request(self):
        request, _ = super().get_request()
        return request, ("local", 0)


class Handler(BaseHTTPRequestHandler):
    def do_POST(self):
        if self.headers.get("Authorization") != "Bearer " + TOKEN:
            self.send_response(401)
            self.send_header("Content-Length", "0")
            self.end_headers()
            return
        self.rfile.read(int(self.headers.get("Content-Length", "0")))
        body = json.dumps({
            "caller": self.headers.get("X-ANet-Caller", ""),
            "call": self.headers.get("X-ANet-Call", ""),
            "capability": self.headers.get("X-ANet-Capability", ""),
        }).encode()
        self.send_response(200)
        self.send_header("Content-Type", "application/json")
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        self.wfile.write(body)


if os.path.exists(SOCKET):
    os.unlink(SOCKET)
UnixServer(SOCKET, Handler).serve_forever()

创建目录和一个至少 16 个字符、仅所有者可读的令牌,然后启动服务:

$ mkdir -p -m 700 ~/.anet-example
$ python3 -c 'import secrets; print(secrets.token_urlsafe(32))' > ~/.anet-example/token
$ chmod 600 ~/.anet-example/token
$ python3 whoami_service.py

在第二个终端中声明该能力,并将其列为 allow_only。该命令将条目添加到提供方的配置中:

$ python3 - <<'EOF'
import json, os
home = os.path.expanduser("~")
path = os.path.join(home, ".anet/ids/provider/config.json")
config = json.load(open(path))
config["inbound"]["public_capabilities"].append(
    {"id": "text.whoami", "allow_only": True})
config["modules"]["service"]["capabilities"].append({
    "id": "text.whoami",
    "url": "unix://" + os.path.join(home, ".anet-example/whoami.sock") + ":/whoami",
    "token_file": os.path.join(home, ".anet-example/token"),
    "description": "Report the caller seen by the service",
})
json.dump(config, open(path, "w"), indent=2)
EOF
$ anet stop provider && anet up provider

本例假定教程中的配置(含能力 text.digest)已就绪。

在请求方不在任何名单中时,调用以原因 not_accepting 被拒绝,其应答与节点不具备该能力时相同,anet find 也不会显示该能力:

$ anet --id requester delegate "$PROVIDER" --capability text.whoami --args '{}' > /dev/null
$ anet --id requester task list | grep -E 'anet.reason|TASK_STATE'
        "anet.reason": "not_accepting",
        "state": "TASK_STATE_REJECTED",

提供方允许请求方之后,调用成功,服务可以看到调用方:

$ anet --id provider peers allow "$REQUESTER"
Add unnamed (<requester-aid>) to the allow list: it can send tasks to this node.
Type yes to confirm: yes
$ TASK=$(anet --id requester delegate "$PROVIDER" --capability text.whoami \
    --args '{}' | sed -n 's/^  "id": "\(.*\)",$/\1/p')
$ anet --id requester task get "$TASK" | grep observed_state
              "observed_state": "{\"caller\": \"<requester-aid>\", \"call\": \"ix_0573e9718990b5e765f96c5354c68785\", \"capability\": \"text.whoami\"}",

仅当守护进程通过中继对调用方完成认证时,才会出现 X-ANet-Caller 请求头。源自本机的调用不带调用方,因此服务可以区分这两种情形。令牌向服务证明守护进程的身份;服务在未先检查令牌之前,绝不能接受该请求头。

守护进程会拒绝其他用户可读的令牌文件,以及短于 16 个字符的令牌。

名单内对端之间的任务交换

不属于能力调用的任务是自由格式的文本,节点只接受来自允许列表或信任列表中的对端的此类任务。本例展示拒绝、名单条目,以及请求方发送、提供方手动应答的完整交换。前置条件是请求方尚不在提供方的允许列表中;如果上一个示例已将其加入,请先移除:

$ anet --id provider peers remove "$REQUESTER"

来自名单之外的对端的任务被拒绝,且不会运行:

$ anet --id requester delegate "$PROVIDER" "Summarise: ANet relays sealed envelopes." > /dev/null
$ anet --id requester task list --role requester | grep -E 'TASK_STATE|anet.reason'
      "anet.reason": "not_accepting",
        "state": "TASK_STATE_REJECTED",

任务中的语句指明了另一方运维者需要运行的命令:“The other side does not take tasks from you and did not run it; ask them to run: anet peers allow <requester-aid>.”运维者在终端上确认:

$ anet --id provider peers allow "$REQUESTER"
Add unnamed (<requester-aid>) to the allow list: it can send tasks to this node.
Type yes to confirm: yes
{
  "aid": "<requester-aid>",
  "list": "allow",
  "changed": true,
  "pinned": "allow"
}

以 trust 代替 allow 时,还允许该对端驱动节点的本地 Agent,并以同样方式请求确认。此时请求方再次发送任务,提供方读取其收件箱、应答并结束任务:

$ TASK=$(anet --id requester delegate "$PROVIDER" "Summarise: ANet relays sealed envelopes." \
    | sed -n 's/^  "id": "\(.*\)",$/\1/p')
$ anet --id provider inbox --pending
{
  "inbox": [
    {
      "interaction_id": "ix_2fe179cbe113e56d93453424abb0a4ba",
      "requester": "<requester-aid>",
      "goal": "Summarise: ANet relays sealed envelopes.",
      "state": "submitted",
      "trust": "peer",
      ...
    }
  ]
}
$ anet --id provider message "$TASK" \
    "A relay passes sealed envelopes without reading them."
$ anet --id provider end "$TASK"
{
  "interaction_id": "ix_2fe179cbe113e56d93453424abb0a4ba",
  "status": "completed"
}

双方使用同一个任务标识符,inbox 也会输出该标识符。请求方读取包含完整历史的交换记录:

$ anet --id requester task get "$TASK" --history 5 | grep -E 'ROLE_|"text"|TASK_STATE'
      "role": "ROLE_USER",
            "text": "Summarise: ANet relays sealed envelopes."
      "role": "ROLE_AGENT",
            "text": "A relay passes sealed envelopes without reading them."
    "state": "TASK_STATE_COMPLETED",

手动应答的提供方是最简单的情形。也可以由本地编程 Agent 应答,参见 委派与自动回复。

无人值守请求方的支出策略

没有运维者在终端前的请求方,需要预先设置限额。所有限额和收款方列表都在终端上更改,因为确认无法通过 MCP 或本地 A2A 接口给出。

本例的策略为:每次调用自动支付最多 3 额度,每天总支出最多 20 额度,只向教程中的提供方付款,不允许任何 Agent 自行支付。

$ anet --id requester payees add "$PROVIDER"
$ anet --id requester payments set auto_max=3 agent_max=0 agent_daily_max=0 daily_max=20
Change this node's payment limits:
  auto_max  5 → 3
  daily_max  50 → 20
Type yes to confirm: yes
{
  "auto_max": 3,
  "agent_max": 0,
  "agent_daily_max": 0,
  "explicit_max": 10,
  "daily_max": 20,
  "spent_today": 0,
  "agent_spent_today": 0,
  "left_today": 20,
  "agent_left_today": 0,
  "payees": 1
}

各项限额的作用如下:

  • 提供方的报价不超过 3 额度时,报价到达即支付。

  • 报价超过 3 额度,或报价来自不在收款方列表中的 AID 时,任务停留在 input-required 状态,原因为 needs_operator_approval。支付行会指明下一条命令,例如受 explicit_max 限制的 anet pay TASK。

  • 通过 MCP 调用 submit_payment 的 Agent 不会获得付款。任务停留在 input-required 状态,并说明运维者需要在终端上执行的操作。

  • 在一个 UTC 日内授权 20 额度之后,直到当日结束不再支付,在终端上确认的支付同样会被拒绝。每日合计按已签名的授权计算。

限额和收款方列表的每次更改,都连同旧值和新值写入证据链,anet audit 可显示这些记录:

$ anet --id requester audit --type anet.policy.changed

anet doctor 会列出偏离默认值的限额。

在其他位置验证证据链

节点的证据链是只追加、带签名的记录序列,包括已发送的委派、已授权和已结算的支付、已接受的结果以及策略变更,不含任务内容。导出证据链并在另一台机器上验证,可在无需访问该节点的情况下,向第三方证明该节点的所作所为。

$ anet --id requester audit --export ./requester-evidence
...
exported the whole chain (17 records) to ./requester-evidence — check it with: anet verify --chain ./requester-evidence
$ ls requester-evidence
audit.json  evidence.ael.jsonl  kel.b64  manifest.json
$ anet verify --chain ./requester-evidence
...
  state  ACTIVE
  keys   the key history in the export itself (kel.b64)

  this shows every record was signed under that key history and none was altered,
  removed or reordered before the head. A chain cut short after some record is still
  a valid chain: pass --head <id> with a head you learned elsewhere to rule that out.
  the key history came with the export: compare the AID above with the one you expect,
  or pass --hub <url> to fetch it from a hub.

检查既不需要守护进程,也不需要网络。导出内容自带密钥历史,因此须将命令输出的 AID 与验证者期望的 AID 比对,或使用 --hub 从 Hub 获取密钥历史。在某条记录之后被截断的链仍然有效;--head 接受从其他来源获知的最新记录的标识符,并拒绝更短的链。

缩短保留期

任务内容在节点上加密存储,并在保留期之后删除:任务结束 30 天后删除,对公开能力的调用为 7 天。anet retention 输出当前期限,anet retention set 更改期限,在终端上要求确认,并在删除新期限已覆盖的内容之前再次确认。

$ anet --id requester retention
Task content retention is 30 days, counted from when a task ends (public calls: 7 days).
$ anet --id requester retention set 7d
Task content retention is now 7 days, counted from when a task ends.

期限可用 Nd 设为 1 至 3650 天,也可设为 forever。证据链不受影响:它保留每个事件的对方、时间、内容标识符和金额,不含内容。