rha.toml
唯一必须写内容的一份:[api].token 没有默认值,少了这一段整份配置连加载都加载不了。
其余三段([storage] / [llm] / [notify])全部可省,省掉就是走默认:开 sqlite、
不接 LLM、通知拒私网。
「必须写内容」不等于「唯一必须存在」——devices.toml 也必须存在,哪怕是个空文件。
它和 rha.toml 一样被无条件读取,缺了直接报 cannot read .../devices.toml;而
bridges.toml 与 automations/ 是真可选,不存在就当没配。新建配置目录时先把这两个
文件建出来:
mkdir -p /etc/rhaprintf '[api]\ntoken = "${RHA_TOKEN}"\n' > /etc/rha/rha.tomltouch /etc/rha/devices.toml[api]listen = "127.0.0.1:8420"token = "${RHA_TOKEN}"
[storage]backend = "sqlite"path = "/var/lib/rha/rha.db"retention_days = 30record = ["*"]exclude = ["*.indicator_light_*"]
[llm]api_key = "${ANTHROPIC_API_KEY}"model = "claude-sonnet-5"
[notify]allowed_hosts = ["ntfy.sh"]${VAR} 是配置加载期的环境变量插值,rha.toml / devices.toml / bridges.toml /
automations/*.toml 四份都支持(spec.d/ 不走这条路,它由型号表加载器单独读)。变量
没定义会直接报 MissingEnv 而不是静默留空——所以凭据写成 ${...} 既不用把明文提交进
仓库,也不会因为忘了设环境变量而无声降级。
[api] —— 这台 daemon 的全部门禁
Section titled “[api] —— 这台 daemon 的全部门禁”这一段是控制面入口:rha 命令行、REST 调用、以及挂在 /mcp 上的 agent 都从这里进来,
而这个面既能读全屋状态、也能下发控制命令。所以下面三个字段合起来就是全部门禁——
listen 决定谁在网络上够得着,token 是唯一一道鉴权,mcp_allowed_hosts 是 /mcp 额外
的一道 Host 白名单。跨机访问要动其中两个,只动一个是最常见的卡点。
整段不参与热重载,改了必须重启进程。
| 字段 | 类型 | 默认 | 说明 |
|---|---|---|---|
listen | string | 127.0.0.1:8420 | HTTP API(含 缺省绑 loopback 而不是 改成
|
mcp_allowed_hosts | array | [] | 允许直连 填 |
token* | string | — | HTTP API 的鉴权令牌 —— 请求携带的 token 与它不一致就一律 401。
建议用 |
[storage] —— 历史与快照
Section titled “[storage] —— 历史与快照”这一段管三样东西:实体的历史时间序列、规则的评估轨迹、以及重启后用来恢复当前值
的快照。三者共用一个后端,但只有第一样受 record / exclude / retain 的范围策略
影响——所以「我把这个实体排除了,为什么重启后它还有值」不是 bug。
范围策略与轮询分层(hot / cold)是两件独立的事:「该多勤地问设备」和「要不要留曲线」 不是同一个问题。
| 字段 | 类型 | 默认 | 说明 |
|---|---|---|---|
backend | string | sqlite | 选后端, 合法取值只有 两种后端建表语句等价( |
enabled | boolean | true | 总开关, 缺省 它与「整段不写 关掉之后: 历史接口返回 503 另外关掉时这一段连校验都不跑 —— 写了非法 |
exclude | array | [] | 在 语法与 |
path | string | 可省略 | sqlite 数据库文件的路径, 只有 相对路径相对的是配置目录( 文件不存在会自动创建(WAL 模式), 但不自动建父目录 —— 父目录不存在或不可写会
导致启动失败, 错误信息带出路提示「set [storage].path to a writable location, or set
enabled = false」。 |
record | array | [] | 记录哪些实体的历史(极简 glob, 只支持 这是存储侧的用户策略, 与轮询分层无关 —— "该多勤地问设备"和"要不要留曲线" 是两个问题。绑在一起会让 BLE 温湿度计这类广播设备因为没人写规则而丢掉温度曲线。 空表被当作 这条策略只管历史时间序列, 不管快照: 被排除的实体照样写 snapshot, 所以重启后 它仍能恢复出当前值。 |
retention_days | integer | 30 | 历史保留天数, 缺省 写 三个容易误解的边界:
|
url | string | 可省略 | postgres DSN。装的是 只有 建议写成 敏感值。建议写成 ${ENV} 由环境变量插值, 不要把明文提交进配置文件。该值绝不进日志与 trace。 |
[llm] —— 规则里 llm 动作的插槽
Section titled “[llm] —— 规则里 llm 动作的插槽”规则里的 llm 动作用这一段去访问模型。它是纯可选的,而**「没
配」与「配错了」的表现完全一样**:两种情况下 rha check 都全绿,运行时都静默走
fallback 分支。所以这一段配完,第一件事是看一条 trace 确认 source=llm。
| 字段 | 类型 | 默认 | 说明 |
|---|---|---|---|
api_key | string | 可省略 | 访问 LLM 端点的密钥, 作为 它是 不配则每次 敏感值。建议写成 ${ENV} 由环境变量插值, 不要把明文提交进配置文件。该值绝不进日志与 trace。 |
base_url | string | https://api.anthropic.com | LLM 端点根地址, 缺省 结尾的 |
max_tokens | integer | 1024 | 请求的 缺省够用: 这里的调用只需要模型返回一次 |
model | string | claude-sonnet-5 | 请求里的模型名, 原样传给上游。缺省 写错不会在 check 期发现: 上游返回非 2xx, 引擎降级 fallback, 而且响应体原文不进
trace(只记字节数与 sha256 前 4 字节指纹, 防上游注入)。所以你看不到
「model not found」这种原始报错, 只能看到 |
[notify] —— 通知的 SSRF 闸
Section titled “[notify] —— 通知的 SSRF 闸”这一段约束的是规则里 notify 动作与 llm 动作的 snapshot
能发到哪儿。
下面两个字段不是叠加,是互斥的两条路,这一点两行说明各说了一半,合起来是:
allowed_hosts 留空时走 allow_private_networks(缺省拒私网、放行公网);
allowed_hosts 一旦非空就切换成纯白名单,allow_private_networks 根本不再参与判断。
所以内网自建的接收端有两种配法,而选了白名单那条路就等于把公网目标(包括 ntfy.sh)
一起挡掉了。
| 字段 | 类型 | 默认 | 说明 |
|---|---|---|---|
allow_private_networks | boolean | false | 是否允许把通知(以及 llm 动作的 缺省拒绝是 SSRF 防护: 规则可以由 HTTP API / MCP 写入, 一个能任意 POST 到内网地址
的动作等于把 daemon 变成内网探针。check 期拒绝字面量私网 IP 与
私网判定比直觉严: 环回、RFC1918、链路本地( 已知残留缺口(明说了没修): 运行时的 DNS 校验与实际连接是两次独立查询, 存在 DNS rebinding 的绕过空间。 |
allowed_hosts | array | [] | 通知目标主机白名单。缺省空表 = 不启用白名单, 改由 非空时策略变成纯白名单: URL 的 host 与列表做精确字符串比对, 命中即放行(跳过
私网判定和 DNS 复核), 未命中一律拒绝。所以 「精确比对」的范围很窄: 不支持通配符、不匹配子域、不带端口(只比 host)。
尾点( |