Align the svc.sh startup precheck with the binary auth semantics and keep the reused DSN

Why: G1-7 moved the DSN into AGENT_SYSTEM_DSN, but the precheck only read
the arg string, so a non-loopback restart with auth auto was wrongly
rejected (2026-10-10 dev incident); the reuse path no longer restored the
dbref-bound DSN and would silently fall back to SQLite; and DSN_ENV leaked
across same-process starts (update --all) into other services' env.
Evidence: tests/test_svc_precheck.py 19 cases green (auth matrix vs
config.go AuthEnabled, reuse migration/dbref restore, scope isolation);
full governance suite 123 green; incident-scenario replay allowed + DSN
carried. Recorded via make evidence.
Impact: scripts/svc.sh precheck and reuse-start DSN handling only; no
binary change, no persistence-format change.
Refs: .task/fix-svc-dsn-reuse-dbref.md; config.go AuthEnabled (on || (not
off && DSN non-empty), env DSN counts).
This commit is contained in:
2026-10-11 02:10:21 +00:00
parent 675b740ce4
commit 7acf3186ca
2 changed files with 279 additions and 17 deletions
+63 -17
View File
@@ -1198,15 +1198,61 @@ addr_is_loopback() {
esac
}
# agent_args_authed ARGS:按二进制 AuthEnabled 口径判断启动参数是否开启认证
# (--auth on;或未显式 off 且带 --dsn——auto+PG 即开)。--mock 只影响存储档不影响认证。
# agent_args_authed ARGS [DSN_ENV]:按二进制 AuthEnabled 口径(config.go:
# 显式 on;或非显式 off 且 StorageDSN 非空)判断认证是否开启。DSN 两个来源等价:
# 命令行 --dsn(历史兼容)与 AGENT_SYSTEM_DSN 环境变量(G1-7 起的标准通道,
# 第二参数传其值)——只看参数串会把「非回环 + auto + env DSN」误判为认证关
#(2026-10-10 dev 实测误拒)。--mock 只影响存储档不影响认证。
agent_args_authed() {
case "$1" in
*--auth[=\ ]on*) return 0 ;;
*--auth[=\ ]off*) return 1 ;;
*--dsn\ *) return 0 ;;
*) return 1 ;;
esac
[ -n "${2:-}" ] && return 0
case "$1" in *--dsn\ *) return 0 ;; esac
return 1
}
# agent_listen_rejected EXEC_ARGS DEFAULT_ADDR INSECURE DSN_ENV:启动预检判定(0=拒绝)。
# 与二进制 listen guard 同口径:回环或显式放行(insecure)不拦;认证开不拦;
# 仅非回环 + 未放行 + 认证关才拦。
agent_listen_rejected() {
local addr; addr="$(printf '%s' "$1" | sed -n 's/.*--addr \([^ ]*\).*/\1/p')"
addr="${addr:-$2}"
addr_is_loopback "${addr%:*}" && return 1
[ -n "$3" ] && return 1
agent_args_authed "$1" "$4" && return 1
return 0
}
# agent_reuse_exec EXEC_ARGS OUT_VAR:复用启动(无显式参数,复用 svc.env exec=)
# 时的 exec 参数与 DSN 解析。处理后的 exec 参数经 nameref OUT_VAR 回写;DSN 经
# 全局 DSN_ENV 传递(agent_do_start 内已 local 化)。必须直接调用而非放进命令
# 替换——$() 子壳里的 DSN_ENV 赋值回不到调用方。两步:先做 G1-7 迁移(存量
# --dsn 剥到环境变量);exec= 不含 DSN 且 dbref= 登记在案时从实例登记重建——
# 否则 G1-7 后 exec= 永不含 DSN,纯 start/restart 复用会静默回落 SQLite,数据库
# 绑定丢失。
agent_reuse_exec() {
local -n out_var="$2"
# 内部变量不得叫 exec_args:与 nameref 目标同名会在函数作用域自我遮蔽,
# 回写落到局部、调用方拿不到值。
local cur="$1" bound_dsn
case "$cur" in
*--dsn\ *)
DSN_ENV="$(printf '%s' "$cur" | sed -n 's/.*--dsn \([^ ]*\).*/\1/p')"
cur="$(printf '%s' "$cur" | sed 's/--dsn [^ ]*//' | sed 's/[[:space:]]\{2,\}/ /g; s/^ //; s/ $//')"
c_info "[$ENV_NAME] DSN 已从命令行迁移到环境变量(G1-7)"
;;
esac
if [ -z "${DSN_ENV:-}" ] && [ -n "$(svc_get dbref)" ]; then
if bound_dsn="$(dsn_from_dbref "$(svc_get dbref)")"; then
DSN_ENV="$bound_dsn"
c_info "[$ENV_NAME] 数据库绑定 $(svc_get dbref) 随复用启动保留(AGENT_SYSTEM_DSN)"
else
c_warn "[$ENV_NAME] dbref=$(svc_get dbref) 无法重建 DSN(实例缺失或登记不全),按无数据库绑定启动"
fi
fi
out_var="$cur"
}
agent_do_start() {
@@ -1219,18 +1265,12 @@ agent_do_start() {
fi
SVC_VERSION="$("$ENV_BIN" version 2>/dev/null || true)"
# 组装启动参数:显式配置 → 重建;否则复用上次持久化 exec=。
local exec_args
# DSN_ENV 必须 local 并清零:G1-7 起 DSN 经它传给启动守卫环境,全局残留会让
# update --all 同进程连续启动时前一个服务的 DSN 泄漏进后一个的 AGENT_SYSTEM_DSN。
local exec_args DSN_ENV=""
if ! agent_has_explicit_cfg && [ -n "$(svc_get exec)" ]; then
exec_args="$(svc_get exec)"
# G1-7:存量 exec= 里的 --dsn 剥离到环境变量(迁移;命令行不再可见明文)。
case "$exec_args" in
*--dsn\ *)
DSN_ENV="$(printf '%s' "$exec_args" | sed -n 's/.*--dsn \([^ ]*\).*/\1/p')"
exec_args="$(printf '%s' "$exec_args" | sed 's/--dsn [^ ]*//' | sed 's/[[:space:]]\{2,\}/ /g; s/^ //; s/ $//')"
c_info "[$ENV_NAME] DSN 已从命令行迁移到环境变量(G1-7)"
;;
esac
c_info "[$ENV_NAME] 复用上次配置(svc.env exec=;显式参数可覆盖):$(mask_dsn "$exec_args")"
agent_reuse_exec "$(svc_get exec)" exec_args
c_info "[$ENV_NAME] 复用上次配置(svc.env exec=;显式参数可覆盖):$(mask_dsn "$exec_args")"
else
local args=""
if [ "$CLI_MOCK" = 1 ] || [ "$(env_var "$ENV_NAME" MOCK)" = "1" ]; then
@@ -1278,9 +1318,12 @@ agent_do_start() {
[ -n "$hosts" ] && guard_env+=("AGENT_SYSTEM_ALLOWED_HOSTS=$hosts")
# 预检(快速拦截):非回环地址且认证判定为关时提前拒绝,报错可操作——
# 不等二进制 listen guard 以"进程即退"暴露;二进制守卫仍是权威。
# 认证判定与二进制同口径(agent_listen_rejected → agent_args_authed):
# env DSN(AGENT_SYSTEM_DSN)同样算「已配置 DSN」,不再因命令行不见 --dsn
# 而把非回环+auto 误判为认证关(2026-10-10 dev 实测误拒)。
local pf_addr; pf_addr="$(printf '%s' "$exec_args" | sed -n 's/.*--addr \([^ ]*\).*/\1/p')"
pf_addr="${pf_addr:-$ENV_ADDR}"
if ! addr_is_loopback "${pf_addr%:*}" && [ -z "$insecure" ] && ! agent_args_authed "$exec_args"; then
if agent_listen_rejected "$exec_args" "$ENV_ADDR" "$insecure" "${DSN_ENV:-}"; then
c_err "[$ENV_NAME] 拒绝启动:非回环地址 $pf_addr 且认证判定为关(auto 无 DSN 即关)。"
c_err " 任选其一:--addr 127.0.0.1:${pf_addr##*:};数据库绑定 scripts/svc.sh $ENV_NAME db <pg实例>;"
c_err " 显式 --auth on;或环境 SVC_$(printf '%s' "$ENV_NAME" | tr 'a-z-' 'A-Z_')_INSECURE_LISTEN=1 显式放行。"
@@ -3072,4 +3115,7 @@ main() {
esac
}
main "$@"
# 仅作为脚本直接执行时进入主流程;被 source(治理单测 fixtures)时只定义函数。
if [ "${BASH_SOURCE[0]}" = "$0" ]; then
main "$@"
fi