配置参考
config.json 的全部字段
本页逐字段对照 internal/daemon/config.go 与 module/*/ 各模块的 Config 结构写成,覆盖 anet 0.1.7。默认值取自源码里的常量或零值处理逻辑,不取自帮助文本;错误文本是在本机实际触发后抄下来的。
相邻的三份参考:命令怎么用见 CLI 参考,哪个标志去掉哪个模块见 构建标签参考,模块返回的状态和证据字段见 效果状态与证据字段。
文件位置与生成方式
配置文件是数据目录下的 config.json:
| 情况 | 路径 |
|---|---|
| 默认身份 | ~/.anet/config.json |
| 具名身份 | ~/.anet/ids/<name>/config.json |
设了 ANET_DATA_DIR | $ANET_DATA_DIR/config.json |
| 都取不到 home | ./.anet/config.json |
daemon 启动时读它。文件不存在就写一份默认配置再返回,所以首次 anet up 之后目录里一定有这个文件;文件存在但 JSON 不合法则是错误,daemon 拒绝启动,绝不会被静默覆盖:
刚生成的默认配置只有两个键:
数据目录以 0700 创建,config.json 以 0600 写入。写入走"先写同目录临时文件再 rename"的原子路径,崩在中途只会留下旧文件或新文件,不会留半个文件。
SaveConfig 把 daemon 内存里的整份配置原样写回,不做字段合并。daemon 在跑的时候手改 config.json,随后任何一条会持久化配置的命令(anet hub-register、anet hub-leave、anet accept、anet profile set、anet autoreply set)都会把你的手改冲掉。唯一的例外是 auto_reply:hub-register 在写回前会先把磁盘上的 auto_reply 块读进内存,免得覆盖掉一个 daemon 启动后才被手工加进去的块。
结论:手改 config.json 前先 anet stop。
顶层字段
| 字段 | 类型 | 默认 | 说明 |
|---|---|---|---|
control_addr | string | 127.0.0.1:39811 | 本机控制面的 HTTP 监听地址。空字符串会在加载时被替换成默认值 |
hub_url | string | 空 | 这个节点的 hub base URL。非空时 daemon 会起一个后台轮询循环收委派和结果 |
name | string | 空 | 注册到 hub 注册表的展示名 |
caps | string[] | 空 | 对外声明的能力列表,anet find 可以搜到 |
summary | string | 空 | 一句话自述 |
readme | string | 空 | 较长的 markdown 自述 |
pricing | string | 空 | 自由文本报价。v0.1 里纯展示,不参与结算 |
accept_delegations | bool | 键缺失视为 true | 是否接收并存储别人委派过来的任务 |
guest_messages | int | 键缺失视为 5 | 允许一个没有 daemon 的访客试用几条消息 |
auto_reply | object | 无 | 内置自动回复循环,见下 |
providers | object | 无 | C1 能力提供者接线,见下 |
modules | object | 无 | 各可插拔模块的配置块,见下 |
control_addr
控制面只有一层 bearer token,跑在明文 HTTP 上。token 在数据目录的 control_token.txt。除非你自己在前面加了 TLS + 鉴权,否则不要把 control_addr 从 127.0.0.1 挪开。
anet id new <name> 建新身份时会自动分配一个未占用端口(第二个身份通常是 39812),写进该身份自己的 config.json。
accept_delegations 与 guest_messages 的三态
两个字段在 Go 里都是指针,所以"键不存在"和"显式写了零值"是两回事:
| 写法 | 效果 |
|---|---|
没有 accept_delegations 这个键 | 接受(装完就能收任务,这是默认发行版的出厂行为) |
"accept_delegations": true | 接受 |
"accept_delegations": false | 不接受 |
没有 guest_messages 这个键 | 5 条 |
"guest_messages": 0 | 0 条,即完全关闭访客试用 |
"guest_messages": -1 | 负数被夹到 0,等同关闭 |
"接受"只意味着存下来(存之前先验 TaskDoc 签名)。anet 自己不跑模型,活是你的外部 agent 或 auto_reply 干的。
caps 不完全由你写
anet hub-register 在注册前会把你 --caps 声明的 id 与当前已注册的 provider 实际提供的能力 id 合并、去重、排序,然后把合并后的结果写回 caps。所以配了 cas 模块之后再注册一次,caps 里会多出 cas.get cas.has cas.put cas.stat,这不是被改坏了。
daemon 每次启动也做同一次合并并把结果推给 hub,但不写回 config.json,见下文《重启就会重新注册》。
summary / readme / pricing
由 anet profile set 写入并同时发布到 hub;anet hub-register 之后如果这三个字段非空,会重新发布一遍,免得重新注册把自述弄丢。
发布前要过一道 INV-1/INV-2 筛查:如果某个模块声明了机密字符串(目前只有 org 模块,它把 org id 声明为机密),自述里出现该字符串会被拒绝发布而不是发出去。
auto_reply
配了这个块,daemon 就从"只收不答"变成自驱动的 provider:后台循环盯着入站会话,调你自己的服务生成回复。anet 内核仍然不跑模型 —— 模型在你配的后端里。
这个块通常不该手写。anet autoreply set … 会校验、持久化并热重启循环,不需要重启 daemon。
后端选择
| 字段 | 类型 | 默认 | 说明 |
|---|---|---|---|
backend | string | openai | 只接受 openai 和 exec。空字符串按 openai 处理 |
写了别的值,newAutoReplier 会报 auto_reply: unknown backend "x" (supported: openai, exec)。
openai 后端
把会话 POST 给任何 OpenAI 兼容的 /chat/completions 端点(ollama、vLLM、llama.cpp server、云 API 都行)。
| 字段 | 类型 | 默认 | 说明 |
|---|---|---|---|
api_base | string | 无,必填 | 例如 http://127.0.0.1:11434/v1。请求发往 <api_base>/chat/completions |
model | string | 无,必填 | 模型名 |
api_key | string | 空 | 非空时作为 Authorization: Bearer <key> 发出 |
system_prompt | string | 见下 | 系统提示 |
api_base 或 model 缺一个,报 auto_reply: backend "openai" requires api_base and model。
system_prompt 的默认值是:
exec 后端
拉起本机已装的编码 agent 跑一轮,把它的最终回复发回去。
| 字段 | 类型 | 默认 | 说明 |
|---|---|---|---|
agent | string | 无,必填 | cursor / claude / codex / openclaw / hermes 之一 |
work_dir | string | 身份数据目录 | agent 的工作目录 |
command | string | 从 PATH 探测 | 覆盖 agent 二进制路径 |
extra_args | string[] | 空 | 追加给 agent 的命令行参数 |
openclaw_agent | string | main | 仅 openclaw:--agent 的取值 |
model | string | 按 agent 而定 | 覆盖模型 |
agent 缺失报 auto_reply: backend "exec" requires agent (supported: cursor, claude, codex, openclaw, hermes);填了不认识的名字报 unknown agent "x" (supported: …)。
model 不填时用各 agent 的默认档,选的是最便宜的一档 —— 无人值守的 provider 不该默认烧高价模型:
| agent | 不填 model 时 |
|---|---|
cursor | auto |
claude | haiku |
codex | 交给 agent CLI 自己的默认 |
openclaw | 交给 agent CLI 自己的默认 |
hermes | 交给 agent CLI 自己的默认 |
extra_args 只能手写进 config.json。--extra-args 在 cmd/anet/main.go 的 knownFlags 里被列为 autoreply 可接受的标志,但 runAutoReply 从来不读它 —— 给了不报错,也不会写进配置块。
输入契约
| 字段 | 类型 | 默认 | 说明 |
|---|---|---|---|
require_image | bool | false | 只做视觉的服务:输入不带图片就直接回 usage_hint,不调后端 |
usage_hint | string | 见下 | 输入不符合契约时的回复 |
error_reply | string | 见下 | 后端调用失败时的回复 |
usage_hint 默认值:
error_reply 默认值:
调参
| 字段 | 类型 | 默认 | 说明 |
|---|---|---|---|
poll_interval_seconds | int | 5 | 扫描入站会话的节奏。0 或负数用默认 |
max_history | int | 20 | 送给后端的最多会话轮数 |
api_timeout_seconds | int | 180 | 一次后端调用的超时 |
max_auto_replies | int | 30 | 失控保护:一次交互里最多自动发这么多条,之后改为提议 end |
这四个字段目前只能手写进 config.json。anet autoreply set 的用法串里写了 --poll-interval --max-history --api-timeout --max-auto-replies,runAutoReply 也确实会读,但 knownFlags 表没有收录它们,而 CLI 现在会在分发处拒绝未收录的标志:
--command(覆盖 agent 二进制路径)同样被拒。
max_auto_replies 存在的原因是两个自动驾驶的 agent 会互相回复到天荒地老。想让它更早收敛,后端可以在回复里带上完成标记 <<ANET_TASK_DONE>> —— 循环会把标记剥掉、把剩下的文本发出去,然后提议结束;对方的自动回复会自动接受这个提议。
样例
anet autoreply set 整块替换,不是逐字段合并:它只把这一次给到的标志拼成新块写下去。原来配过 --system-prompt 又执行了一次不带该标志的 set,那条系统提示就没了。改一个字段时把其余字段一并重新给出。
另有一条:--error-reply 能用,但没有出现在 anet autoreply set 的用法串里;反过来,用法串里列出的 --poll-interval --max-history --api-timeout --max-auto-replies 现在会被 CLI 拒绝。两处漂移都在 cmd/anet/main.go —— knownFlags 表与 runAutoReply 实际读的标志集合对不上。
auto_reply 块坏了不会让 daemon 起不来。启动时 startAutoReply 只是打一行 anet: auto-reply DISABLED: <原因> 到 daemon.log,daemon 照常跑,只是不自动回复。反过来,通过 anet autoreply set 提交同样的坏配置会被当场拒绝并把错误返回给你 —— 所以配完之后请用 anet autoreply show 或看日志确认它真的起来了,再用 anet autoreply test 打一发本地自检。
providers
C1 能力提供者的接线。目前只有一个子字段:
| 字段 | 类型 | 默认 | 说明 |
|---|---|---|---|
providers.anetlink.socket | string | 无 | 一个正在跑的 anetlinkd 的 C1 UDS 路径(即 anetlinkd --c1-socket 的取值) |
providers.anetlink 与 modules.anetlink 指的是同一个模块 —— daemon 在启动时把前者翻译成后者的配置块。翻译发生在 modules 拷贝之后,所以两边都写时 providers.anetlink 覆盖 modules.anetlink,而翻译只带 socket 一个键 —— 你在 modules.anetlink 里写的 timeout_s 会被静默丢掉。二选一;modules.anetlink 是能写全字段的那个。
modules 总览
modules 是一个"模块名 → 配置块"的映射。daemon 不解析块的内容,原样交给对应模块的工厂函数;所以升级 daemon 之后老配置里的模块块不会被改写。
默认构建里注册了 8 个模块。anet version 会把这个二进制实际含有的可选子系统列出来:
这一行有 9 个名字:8 个模块,加一个 mcp。mcp 不是模块(见下),它由 module.DeclareCompiled 单独登记,好让这一行说全二进制里到底有什么。
daemon 启动时也会往 daemon.log 里写一行,取自同一个来源:
started: 是实际起来的模块数,不是编进去的数。上面那行 started: 1 是一份没配任何模块块的配置 —— 起来的那一个是 x402,它是唯一一个"没有配置块也会加载"的模块。
| 模块 | 编译标志 | 没有配置块时 | 块必填字段 |
|---|---|---|---|
service | no_service(减) | 不加载 | capabilities |
x402 | no_x402(减) | 加载 | 无 |
p2p | no_p2p(减) | 不加载 | socket |
anetlink | no_anetlink(减) | 不加载 | socket |
cas | no_cas(减) | 不加载 | dir |
blackboard | no_blackboard(减) | 不加载 | 无 |
org | no_org(减) | 不加载 | genesis |
taskboard | no_taskboard(减) | 不加载 | 无(但需要 hub_url) |
shell | shell(加) | 不加载 | commands 或 allow_arbitrary |
mcp 出现在 anet version 的 modules: 行里,但它不是 module.Module —— MCP 北向面在 internal/mcpserv,经 anet mcp 子命令进入,由 no_mcp 这个编译标志控制,没有配置块。往 modules 里写 "mcp": {} 在完整构建里也会让 daemon 拒绝启动:
这条提示在这里是误导的 —— 完整构建并没有用 no_mcp,只是这个名字从来没有 module.Register 过,而 module.Build 判断"编进去了没有"用的是模块注册表,不含 DeclareCompiled 登记的名字。
配置块的错误分两个阶段,错误文本的形状不同:
- 工厂阶段(解析和字段校验)会被包一层模块名:
module "cas": cas: dir is required - 启动阶段(真正连 socket、开目录、要 hub)原样返回:
taskboard: this node has no hub, so there is no board
两者都是致命的:配了、编进去了、起不来,是运维错误,daemon 不会带着缺失的能力静默启动。
service
把一个普通 HTTP 服务变成网络上的能力。daemon 把调用参数以 JSON POST 给你的 URL,读回复;你的服务完全不需要知道 ANet、AID、回执或 CBOR 的存在。
| 字段 | 类型 | 默认 | 说明 |
|---|---|---|---|
capabilities | 数组 | 无,必填且非空 | 这个节点提供的能力 |
timeout_ms | int | 120000(2 分钟) | 单次调用的超时。0 用默认 |
capabilities 的每一项:
| 字段 | 类型 | 默认 | 说明 |
|---|---|---|---|
id | string | 无,必填 | 网络上调用它用的能力 id |
url | string | 无,必填 | 后面那个 HTTP 端点 |
price | uint | 0 | 在 hub 信用单位下的价格。0 表示免费,且是"根本不发 402",不是"价格为零" |
description | string | 空 | 对端在决定要不要调用时看到的说明 |
protocol | string | http | 写进证据记录的协议名,后面挂的东西更具体时可以改 |
校验:capabilities 为空报 service: no capabilities declared;某一项缺 id 或 url 报 service: capability <i> needs both id and url;id 重复报 service: capability "x" declared twice。
关于诚实效果状态(五种状态的完整定义见效果状态与证据字段):服务连不上是 UNAVAILABLE(远端什么都没干),服务返回非 2xx 是 FAILED。服务答了就是"确认收到"(V1);配置不能声明更高的信任等级 —— daemon 没有办法核实回答是不是对的。能说更多的服务可以在响应里返回一个 evidence 对象,其中的 observed_state、protocol、quirk、native_ack 会被采用;信任等级不在其中 —— 一次未鉴权的 HTTP 应答支撑不起更高的等级,声明也改不了它。
配了 price 但构建里没有支付模块时,调用被拒绝而不是白干:
x402
收款与付款。这是唯一一个没有配置块也会加载的模块 —— 一个有 hub 的节点不配任何东西就能付款,所以缺块仍然造出模块;只有块本身解析不了才是错误。
付费面在 anet hub-register 之后立即可用,不需要重启:模块在 Start 时拿到的是"以本节点身份签名"这项许可(module.Host.PaymentSeam),而"这个节点在哪家 hub 记账"是每次调用现读配置的。
| 字段 | 类型 | 默认 | 说明 |
|---|---|---|---|
voucher_addr | string | 空 | 一个公网监听地址,在 hub 付了款的买方来这里直接兑券。不设就不开 |
voucher_url | string | 空 | 外界访问 voucher_addr 的地址,写进本节点的签名卡片 |
witness_hub | bool | false | 周期性把自己 hub 的发放链头钉在自己的证据链上,并把签名证明回给 hub |
witness_every_seconds | int | 3600(1 小时) | 钉的间隔。0 或负数用默认。启动 30 秒后先钉一次,之后按间隔 |
voucher_addr 和 voucher_url 必须同时设或同时不设,只设一个报错:
两个字段分开的原因是:监听地址常常是 0.0.0.0 或容器内端口,把它签进卡片等于对外广告一个谁也到不了的地址。只有运维知道外界看到的是什么。
witness_hub 默认关。一个只跟自己 hub 说话的裁剪节点没有义务替网络干活;打开它的节点得到的直接好处是,自己余额所在的那本账有了独立证据。
只付款不收款的节点,modules 里可以完全不写 x402。
p2p
点对点投递:把委派直接送给另一个节点,而不是经 hub 中继。
对等栈跑在另一个进程里,这个模块只是它的客户端 —— 通过一个 Unix socket 说一套很小的行协议。这样做是因为 libp2p 那套依赖树会把内核的依赖面扩大一个数量级,而编译标志只能去掉代码、去不掉 go.mod 里的依赖。
| 字段 | 类型 | 默认 | 说明 |
|---|---|---|---|
socket | string | 无,必填 | 对等进程监听的 UDS 路径 |
dial_timeout_ms | int | 3000 | 单次投递尝试的超时。0 用默认 |
reach_timeout_ms | int | 300 | "对方可达吗"这一问的超时,每次发送前都要问,所以必须便宜。0 用默认 |
两个超时都刻意很短:这条通路是优化,后面还有 hub 兜着;一个慢对端不该把每一次委派都拖慢。
对等进程连不上不影响 daemon 启动,模块会带退避重连,日志里写:
退避从 250ms 起倍增,封顶 30 秒。
本仓带一个参考实现:tools/anetpeer。它一头用 Unix socket 对 daemon 说这套行协议,另一头用 socket 或 TCP 对别的 anetpeer 说话,靠会合点找对方 —— --rendezvous 接一个本地目录,或接一个 hub URL:
它不是 libp2p 对等栈:没有 NAT 穿透、没有中继、没有目录之外的发现。要那些就得换自己的实现。
直连地址不在 config.json 里 —— 它由 anet p2p-advertise <addr> 发布到 hub 的签名地址目录(会合点填 hub URL 时,anetpeer 查的就是这份目录),anet p2p-advertise 不带参数则是撤回。
anetlink
设备能力。注册 ANetLink 的 provider,让这个节点对外提供 anetlinkd 能够到达的那些能力。
| 字段 | 类型 | 默认 | 说明 |
|---|---|---|---|
socket | string | 无,必填 | 一个正在跑的 ANetLink 运行时提供的 C1 socket 路径 |
timeout_s | int | 5 | 初次注册握手的超时。0 或负数用默认 |
注册失败是致命的 —— daemon 不会带着"你要的设备能力其实没上线"这个状态起来。所以 anetlinkd 必须先于 daemon 就绪,systemd 上用 After= / Requires= 排好顺序。
daemon 在这里学到的东西不包含"设备"这个概念:模块注册的是一个提供若干能力 id 的 provider,daemon 解析和调用它的方式与任何别的 provider 完全一样。这是 C1 的红线。
cas
内容寻址存储。把 CID 映射到哈希等于该 CID 的那串字节,以及反向。刻意不带 schema —— 可搜索的元数据、逻辑路径、索引属于需要它们的那一层。
提供 cas.put cas.get cas.has cas.stat 四个能力。
| 字段 | 类型 | 默认 | 说明 |
|---|---|---|---|
dir | string | 无,必填 | 存储目录。没有默认值 —— 自己挑位置的存储会把节点的内容放到运维没选也找不到的地方 |
max_blob_bytes | int | 33554432(32 MiB) | 单个 blob 上限。0 用默认 |
max_blob_bytes 必须默认就有,而不是等运维想起来去设:一个接受网络任意字节的能力,没有上限时的失败形态是别人把你的磁盘填满。
blackboard
共享认知:签名的 CogUnit、CRDT 合并、按任务分阶段。提供 blackboard.add blackboard.snapshot blackboard.conclude 三个能力。
| 字段 | 类型 | 默认 | 说明 |
|---|---|---|---|
enabled | bool | 键缺失视为开 | 设成 false 可以在不删配置块的前提下关掉它 |
三种写法的区别:
| 写法 | 效果 |
|---|---|
modules 里没有 blackboard | 模块不加载 |
"blackboard": {} | 加载并注册 provider |
"blackboard": {"enabled": false} | 加载但不注册任何 provider,对外看不到那三个能力 |
黑板的 HLC 用本节点的 AID 打标,所以一个时间戳自带"哪个节点写的",不需要另一套 node-id 去同步。CogUnit 被标记为 org 作用域,INV-1 的守卫会在公开发布路径上拒绝它。
org
组织成员资格。回答一个问题:某份凭据此刻对某个组织是否有效。提供 org.verify 和 org.info。
| 字段 | 类型 | 默认 | 说明 |
|---|---|---|---|
genesis | string | 无,必填 | 该组织已签名的创世对象,base64 编码的 CBOR |
没有默认组织 —— 说不出自己在给谁做验证的节点没有可验之物:
genesis 会在启动时被解码、反序列化并校验,任一步失败都让 daemon 拒绝启动(org: genesis not base64 / org: genesis malformed / org: genesis invalid)。
配了这个模块之后,org id 成为机密字符串(INV-2)。daemon 在每次公开发布(hub 注册、自述发布)前会筛查发布体,里面出现 org id 就拒绝发布 —— 因为"我属于哪个组织"本身不是公开信息。所以别把 org id 写进 name、summary、readme 或能力 id。
治理纪元(governance epoch)未做:它需要 ANetCore 里尚未落地的 ascpevo.GovernanceCert。
taskboard
hub 共享任务板的客户端。提供 task.board(读板)、task.create(放活)、task.claim(接活)三个能力。移动、阻塞、驳回这些协调动作是人在 UI 上做的,这里没有,它们仍然可以由任何会签名的东西走 HTTP 调。
| 字段 | 类型 | 默认 | 说明 |
|---|---|---|---|
url | string | 空,即本节点自己的 hub | 覆盖用哪个板 |
块里所有字段都是可选的,但块本身必须存在,否则模块不加载:
板是 hub 的。没有 hub_url 的节点配了这个模块会让 daemon 起不来:
这是刻意的:带着"每次调用都返回错误"的能力注册上去,比起不起来更难排查。先 anet hub-register,再配 taskboard。
每次变更操作都用本节点的密钥对一个 task.* 命名空间的挑战串签名,hub 拿它保存的密钥历史来验;动作名分命名空间正是为了让一次签名不能被重放到别的用途上。
shell
在 daemon 所在的机器上执行运维批准过的命令。这是全套里唯一一个加法标志的模块 —— 默认构建里没有,只有 -tags shell 编出来的二进制才有。
这是唯一一个在宿主机上执行命令的模块。它不提权:命令以 daemon 所在的用户身份运行。daemon 用 root 起就能跑 root 命令 —— 那是安装时运维做的决定,写在 unit 文件里,不是这个模块能授予的。
能力 id 的形状:shell.list 报告这个节点提供什么;每条具名命令按 shell.run@<名字> 单独成一个能力 id,所以卡片上广告的是这台机器确实会做的事,而不是一句"它能跑东西";shell.exec 只在打开 allow_arbitrary 时出现。
| 字段 | 类型 | 默认 | 说明 |
|---|---|---|---|
commands | 对象 | 空 | 运维批准的命令,键是名字 |
allow | string[] | 空 | 允许调用的 AID 列表。空表示谁都不允许 |
allow_file | string | 空 | 一个每行一个 AID 的文件,每次调用前重读。设了它就取代 allow |
allow_local | bool | false | 是否接受不带调用方身份的调用 |
allow_arbitrary | bool | false | 打开 shell.exec,允许调用方送任意命令 |
timeout_s | int | 30 | 单条命令的超时。取值范围 0–1800,0 用默认 |
max_output_bytes | int | 65536(64 KiB) | 捕获输出的上限。取值范围 0–4194304,0 用默认 |
shell | string | /bin/sh | 解释器 |
commands 里每一项:
| 字段 | 类型 | 默认 | 说明 |
|---|---|---|---|
run | string | 无,必填 | 命令行,交给解释器执行,所以管道、重定向都能写 |
description | string | 用 run 的原文 | agent 读卡片时看到的说明 |
args | bool | false | 为真时把调用方 args.argv 里的元素逐个 shell 转义后追加到命令行末尾 |
dir | string | daemon 的工作目录 | 换个地方跑 |
timeout_s | int | 用模块级 timeout_s | 单独覆盖这一条的超时 |
调用参数的键是封闭集合:shell.run@<名字> 只接受 argv,shell.exec 只接受 command 和 dir。送来别的键会被拒绝并列出接受哪些,而不是丢掉那个键再把结果报成 OK。args 为假的命令收到 argv 也是拒绝。
校验规则:
commands为空且allow_arbitrary为假 →shell: no commands declared and allow_arbitrary is off — this module would offer nothing- 命令名不能含空格、制表符、
@或/(它要变成能力 id 的一部分) run是空白 →shell: command "x" has nothing to runtimeout_s超出 0–1800 →shell: timeout_s must be between 0 and 1800max_output_bytes超出 0–4194304 →shell: max_output_bytes must be between 0 and 4194304allow与allow_file不能同时设 →shell: set either allow or allow_file, not both — two sources of who may call is one more than can be right
allow_file 里空行和 # 注释会被忽略,可以在每行旁边写清是谁。文件不存在等同于空文件(即拒绝所有人),因为删掉文件是一种合法的"一次性吊销所有人"。
对应的 /etc/anet/shell-allow.txt:
关于安全默认值,三条都是"零值即拒绝":
allow为空拒绝所有人。启动时如果发现没有任何调用方被允许,会往 stderr 打一行anet shell: no caller is allowed — set allow or allow_file, or this module will refuse every call,因为这几乎总是一个没配完的安装。allow_local默认为假。空的CallerAID意思是"这次调用没有带经过验证的身份",不是"本地运维"。默认放行会让 daemon 里任何一处忘填该字段变成对整个白名单的静默绕过。allow_arbitrary与"配了这个模块"是两个独立的决定。打开时启动会打一行anet shell: allow_arbitrary is ON — an allowed caller can run any command as the user this daemon runs as。
每次调用 —— 包括被拒绝的 —— 都先落到本节点的证据链上再返回输出:anet.shell.command 记录谁问的、跑了什么、退出码是多少,anet.shell.refused 记录被拒的调用。后者更值得看:白名单外的 AID 来敲门,只有这里能看到。
shell.list 对远程调用方不返回白名单名单,只返回条数;只有不带调用方身份的本地调用才拿得到 allowed_caller_aids。把一个已获准的调用方变成"这台机器信任的所有其他运维"的通讯录不是它的职责。
配了但没编进去
配置了某个模块而二进制里没有它,daemon 拒绝启动并点名该用哪个标志。这不是宽容处理:一个读到 "p2p": {...} 却静默忽略、让运维琢磨为什么没有对等连接的 daemon,是更坏的结果。
减法模块(默认在,-tags no_<name> 去掉)。在一个 -tags no_p2p 的构建上配 p2p:
加法模块(默认不在,-tags <name> 才编进去)。在默认构建上配 shell:
两句话不同是有意的。减法模块缺席是因为有人把它去掉了,加法模块缺席是因为没人要它 —— 让运维去找 no_shell 这个从来不存在的标志,只会浪费时间。这也是 cmd/anet/optin_tags.go 存在的理由:它不带任何编译标志,所以一个没有 shell 代码的构建仍然能说出该用哪个标志才能把它带进来。
这两条错误由 daemon 进程写进 daemon.log。anet up 会保留子进程句柄:子进程先退出时,它把日志末尾几行原样报到终端,所以起不来的原因当场就能看到,不必再去翻日志。
每个标志各去掉什么,以及 CI 用 go tool nm 数符号的两向判据,见构建标签参考。
排查顺序:
输出第二行就是这个二进制实际含有的可选子系统清单(8 个模块加 mcp,一个都没有时打 (none))。想要的模块不在里面,要么换一个发行版,要么自己编:
配置改动如何生效
| 改动 | 生效方式 |
|---|---|
accept_delegations | 热生效。用 anet accept on / anet accept off,持久化后立即起作用(每次入站委派都重新读) |
auto_reply | 热生效。用 anet autoreply set … / anet autoreply off,校验、持久化、停旧循环、起新循环 |
summary readme pricing | 热生效并同时发布。用 anet profile set … |
hub_url name caps guest_messages | 热生效并同时注册。用 anet hub-register <url> …,持久化后重启中继轮询循环。反向操作是 anet hub-leave <url>:向该 hub 注销,并且当它正是当前 hub_url 时清掉本地 hub_url、停掉轮询循环 |
control_addr | 需重启。监听在 daemon 启动时建立 |
providers | 需重启。在 daemon 启动时翻译成模块块 |
modules 里任何一块 | 需重启。模块只在 New() 里构建和启动一次,没有热加载 |
需重启的做法:
编辑 config.json,然后:
具名身份加上名字:anet stop coder / anet up coder。
重启就会重新注册
模块注册 provider 之后,这个节点对外的能力 id 集合就变了。daemon 每次启动都会把新清单推给 hub:hub_url 非空时,New() 在后台跑一次 refreshRegistration,读当前配置,把 provider 实际提供的能力 id 与 caps 里声明的合并,重新注册一次。所以新加了 cas、service、shell 之类会提供能力的模块,anet stop 加 anet up 就够了。
这次刷新是后台的,失败不致命:hub 连不上时 daemon 照常起来,只往 daemon.log 写一行 anet: could not refresh this node's registration with <url>: …。清单真变了时写 anet: refreshed capabilities at <url>: [...]。
显式注册仍然可用,而且有一件事只有它做 —— 把合并后的能力清单写回 config.json 的 caps;后台刷新只推给 hub,不动磁盘。改 name、改自己声明的标签、换 hub,也都走这条命令:
shell 模块要注意的是另一件事:每加一条具名命令,能力 id 集合就变(多一个 shell.run@<名字>),而模块只在 New() 里构建一次 —— 所以改 commands 仍然要重启才生效,只是重启之后不必再手动注册。
不需要重启也不需要重新注册的
shell 的 allow_file:文件内容在每次调用前重读。改文件即刻生效,删文件即刻吊销所有人。这正是它与内联 allow 的区别 —— 内联列表是静态的,改它要重启,而"想吊销某人访问权"的那一刻通常不是"想重启被吊销的那个东西"的时刻。
一份完整的 config.json
一个有 hub、收费提供两个 HTTP 服务、带内容存储、用本地模型自动接单的节点:
已知的坑
按"位置、行为、影响、发现方式"记:
-
providers.anetlink覆盖modules.anetlink。位置internal/daemon/modules.go的moduleConfig。行为:先拷贝cfg.Modules,再用providers.anetlink.socket覆写out["anetlink"],而覆写只带socket一个键。影响:两边都写时modules.anetlink.timeout_s被静默丢弃,握手仍用 5 秒默认值。发现方式:读源码。 -
"mcp"在完整构建里也报"没编进去"。位置module.Build的未知名字分支。行为:mcp由module.DeclareCompiled登记,因此出现在anet version的modules:行里,却从来没有module.Register过;而Build判断"编进去了没有"只查模块注册表。任何构建下把mcp写进modules都是致命错误,提示还指向no_mcp。影响:一个version说有、modules说没有的名字,提示还把人引向一个没用过的标志。发现方式:在完整构建上实际配置"modules": {"mcp": {}}触发。 -
auto_reply配错在启动时只是打日志。位置startAutoReply。行为:newAutoReplier报错时打一行anet: auto-reply DISABLED: <原因>就返回,daemon 照常起。影响:一个自以为在自动接单的节点其实一直没在回,而anet status里auto_reply块仍然显示得好好的。发现方式:读源码;anet autoreply test能复现出同一条错误。 -
anet autoreply set整块替换而非合并。位置cmd/anet/main.go的runAutoReply与SetAutoReply。行为:set只把本次给到的标志拼成新块整体写下去。影响:改一个字段会丢掉其余字段。发现方式:读源码。 -
daemon 在跑时手改
config.json会被冲掉。位置SaveConfig。行为:它把内存里的整份配置原样写回,只有auto_reply在hub-register路径上做了"先读盘再写回"的保护。影响:改完modules之后随手跑一条anet accept off,modules就没了。发现方式:读源码注释。 -
anet autoreply set收不下自己用法串里的四个调参标志。位置cmd/anet/main.go的knownFlags["autoreply"]与runAutoReply。行为:runAutoReply会读--poll-interval--max-history--api-timeout--max-auto-replies--command,knownFlags没收录它们,而checkFlags在分发处就拒绝未收录的标志;反过来,knownFlags收录的--extra-args没有任何读取点。影响:这四个调参字段与--command只能手写进config.json;--extra-args给了也不生效。发现方式:在默认版二进制上跑anet autoreply set --backend openai --api-base http://x --model m --poll-interval 10。cmd/anet/knownflags_test.go的漂移检查只扫main.go顶层case块里的flags["…"],runAutoReply是独立函数,扫不到。