diff --git a/.task/feature-svc-start-keep-db-binding.md b/.task/feature-svc-start-keep-db-binding.md new file mode 100644 index 00000000..324cbd68 --- /dev/null +++ b/.task/feature-svc-start-keep-db-binding.md @@ -0,0 +1,36 @@ +# feature/svc-start-keep-db-binding 任务卡 + +任务主记录:;状态:进行中;owner:weichen + +## 基线与授权 + +- 权威仓:ssh://git@git-ssh.u1680832.nyat.app:31411/weichen/Agent-System-Dev.git;目标分支:main;R0(initial_base_sha)= 4d59610fd17fb1a05cf55ff266f393cc007995e2 +- 工作区:/home/weichen/projects/Agent-System-Dev-wt-feature-svc-start-keep-db-binding(worktree);分支:feature/svc-start-keep-db-binding +- 授权与交付终点:用户明确要求修复 svc.sh 此类缺陷(2026-10-05 dev 服务事故后续)。终点 = PR 合入 main + 主检出回归验证。 + +## 目标与非目标 + +背景(2026-10-05 01:23 dev 事故):后端 8eda35f 引入 listen guard 后,`svc.sh dev restart --addr 0.0.0.0:8788` 走"显式参数重建"分支,dev 分支刻意不拼 `--dsn/--auth`,持久化的 pg1 绑定被静默丢弃 → 二进制 `--auth auto` 无 DSN 判认证关 → 守卫拒启("进程即退")。且 `record_state` 在健康探测前写盘,坏 `exec=` 持久化,裸 start 复用导致二次失败。 + +修复项: +1. 重建分支 dbref 兜底:`dsn_from_dbref` 由 dbref 从 pg 实例登记重建 `--dsn`(优先级 CLI_DSN > SVC_*_DSN > dbref;mock 除外);dev 同样尊重显式 `--dsn/--auth`(此前传入即被静默丢弃)。 +2. 失败不落坏配置:agent/static 启动的 `record_state` 后移至健康探测后——探测通过才写 svc.env;进程即退时保留上次配置并明示;超时(进程存活)按实际运行参数登记。pg 路径原本就在 pg_ctl 成功后落盘,不动。 +3. 预检拦截:非回环 + 认证判定关(按二进制 AuthEnabled 口径:--auth on;未显式 off 且带 --dsn)时 svc.sh 层提前拒绝并给可操作报错(回环/绑定/--auth on/INSECURE_LISTEN 四条出路);二进制 listen guard 仍是权威。 +4. 日志泄漏对齐:启动/复用/旧产物过滤三处日志对 DSN 掩码(此前裸打密码,违背脚本自述"DSN 默认掩码"原则)。 + +同类排查结论(不改):`set --addr` 对 agent 拒绝并指向 restart;`db --clear`/`config --clear` 均同步改写 exec=,无漂移;非 dev 环境重建分支原本就带 --dsn/--auth,行为保持。 + +非目标:不改后端 listen guard 语义;不改 deploy 物化/构建链路。 + +## 同步记录 + +| 节点 | sync_base_sha | tested_head_sha | checked_at | +|---|---|---|---| +| 启动 | 4d59610fd17fb1a05cf55ff266f393cc007995e2 | 4d59610fd17fb1a05cf55ff266f393cc007995e2 | 2026-10-05T01:51:21Z | + +## 结果与验证(完成后填写) + +- 结果: +- 验证: +- 影响与剩余项: +| 同步前验收 | 4d59610fd17fb1a05cf55ff266f393cc007995e2 | 42151071263aa133520fe346b6b1929e1ecda008 | 2026-10-05T02:16:28Z | diff --git a/scripts/svc.sh b/scripts/svc.sh index f46eaa43..23ecfeb7 100755 --- a/scripts/svc.sh +++ b/scripts/svc.sh @@ -401,9 +401,11 @@ dotenv 格式:KEY=VALUE,支持 # 注释、成对引号、export 前缀;只 播种幂等:设置持久化在 agent 数据目录,播过一次后续启动无需再传。 持久化语义: - - deploy/start 成功后把当前态写入 svc.env(600) + - deploy/start 成功(健康探测通过)后把当前态写入 svc.env(600);启动失败不覆盖 + 上次配置——裸 start 仍按上次可用参数重试 - 裸 start/restart 复用 exec=(上次完整参数);任何显式参数(或 --config 文件、 - SVC_* 变量)出现即整体重建参数 + SVC_* 变量)出现即整体重建参数——重建时 dbref 绑定的 --dsn 自动从实例登记 + 重建保留(mock 档或显式 --dsn 除外) - tags 随每次登记合并去重,跨重启保留 服务级配置文件(与上述 svc.sh dotenv 层不同——这是 agent 二进制自己的 JSON 配置): @@ -1172,6 +1174,41 @@ PY fi } +# dsn_from_dbref DBREF:由 dbref(pg实例/库名)从该实例登记重建连接串。 +# 实例未登记 / 非 postgres / 缺 addr·user·password 时返回非零且不输出。 +dsn_from_dbref() { + local dbref="$1" inst tdb taddr tuser tpass + inst="${dbref%%/*}"; tdb="${dbref#*/}" + { [ -n "$inst" ] && [ -n "$tdb" ] && [ "$inst" != "$dbref" ]; } || return 1 + [ -f "$RUNTIME/$inst/svc.env" ] || return 1 + [ "$(svc_kind "$inst")" = postgres ] || return 1 + taddr="$(env_kv_of "$inst" addr)" + tuser="$(env_kv_of "$inst" user)" + tpass="$(env_kv_of "$inst" password)" + { [ -n "$taddr" ] && [ -n "$tuser" ] && [ -n "$tpass" ]; } || return 1 + printf 'postgresql://%s:%s@%s:%s/%s' "$tuser" "$tpass" "${taddr%:*}" "${taddr##*:}" "$tdb" +} + +# addr_is_loopback HOST:回环写法白名单判定(127.x / ::1 / localhost)。 +# 只服务启动预检的快速拦截:其余形态一律按非回环处理,权威判定在二进制 listen guard。 +addr_is_loopback() { + case "$1" in + localhost|127.*|::1|[::1]) return 0 ;; + *) return 1 ;; + esac +} + +# agent_args_authed ARGS:按二进制 AuthEnabled 口径判断启动参数是否开启认证 +# (--auth on;或未显式 off 且带 --dsn——auto+PG 即开)。--mock 只影响存储档不影响认证。 +agent_args_authed() { + case "$1" in + *--auth[=\ ]on*) return 0 ;; + *--auth[=\ ]off*) return 1 ;; + *--dsn\ *) return 0 ;; + *) return 1 ;; + esac +} + agent_do_start() { mkdir -p "$ENV_DATA" "$ENV_DIR" check_port_free || return 1 @@ -1185,21 +1222,34 @@ agent_do_start() { local exec_args if ! agent_has_explicit_cfg && [ -n "$(svc_get exec)" ]; then exec_args="$(svc_get exec)" - c_info "[$ENV_NAME] 复用上次配置(svc.env 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 args="--mock" - elif [ "$ENV_NAME" != dev ]; then + else local dsn="${CLI_DSN:-$(env_var "$ENV_NAME" DSN)}" - [ -n "$dsn" ] && args="$args --dsn $dsn" local auth="${CLI_AUTH:-$(env_var "$ENV_NAME" AUTH)}" - [ -z "$auth" ] && auth=auto - args="$args --auth $auth" + # dbref 兜底:显式参数重建启动参数时,持久化的数据库绑定不再静默丢失 + #(2026-10-05 dev 事故:restart --addr 走重建分支丢 --dsn → auto 判认证关 → 守卫拒启)。 + if [ -z "$dsn" ] && [ -n "$(svc_get dbref)" ]; then + local bound_dsn + if bound_dsn="$(dsn_from_dbref "$(svc_get dbref)")"; then + dsn="$bound_dsn" + c_info "[$ENV_NAME] 数据库绑定 $(svc_get dbref) 随参数重建保留" + else + c_warn "[$ENV_NAME] dbref=$(svc_get dbref) 无法重建 DSN(实例缺失或登记不全),按无数据库绑定启动" + fi + fi + [ -n "$dsn" ] && args="$args --dsn $dsn" + if [ -n "$dsn" ] || [ -n "$auth" ] || [ "$ENV_NAME" != dev ]; then + [ -z "$auth" ] && auth=auto + args="$args --auth $auth" + fi fi local adjusted; adjusted="$(adjust_args_for_bin "$ENV_BIN" "$args")" if [ "$adjusted" != "$args" ]; then - c_warn "[$ENV_NAME] 该产物不支持 --dsn/--auth(旧版本二进制),已自动省略:${args:-<无>} → ${adjusted:-<无>}" + c_warn "[$ENV_NAME] 该产物不支持 --dsn/--auth(旧版本二进制),已自动省略:$(mask_dsn "${args:-<无>}") → $(mask_dsn "${adjusted:-<无>}")" fi args="$adjusted" exec_args="--addr $ENV_ADDR --data $ENV_DATA --plugins ${CLI_PLUGINS:-$ENV_DIR/plugins} $args $CLI_EXTRA" @@ -1212,7 +1262,19 @@ agent_do_start() { hosts="$(env_var "$ENV_NAME" ALLOWED_HOSTS)" [ -n "$insecure" ] && guard_env+=("AGENT_SYSTEM_INSECURE_LISTEN=$insecure") [ -n "$hosts" ] && guard_env+=("AGENT_SYSTEM_ALLOWED_HOSTS=$hosts") - c_info "[$ENV_NAME] 启动:$ENV_BIN serve $exec_args" + # 预检(快速拦截):非回环地址且认证判定为关时提前拒绝,报错可操作—— + # 不等二进制 listen guard 以"进程即退"暴露;二进制守卫仍是权威。 + 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 + c_err "[$ENV_NAME] 拒绝启动:非回环地址 $pf_addr 且认证判定为关(auto 无 DSN 即关)。" + c_err " 任选其一:--addr 127.0.0.1:${pf_addr##*:};数据库绑定 scripts/svc.sh $ENV_NAME db ;" + c_err " 显式 --auth on;或环境 SVC_$(printf '%s' "$ENV_NAME" | tr 'a-z-' 'A-Z_')_INSECURE_LISTEN=1 显式放行。" + c_err " 登记未变更(svc.env 保留上次配置);二进制侧 listen guard 同样会拒绝。" + hist_event start fail "" "预检拒绝:非回环且未开认证" + return 1 + fi + c_info "[$ENV_NAME] 启动:$ENV_BIN serve $(mask_dsn "$exec_args")" if [ -n "$insecure" ] || [ -n "$hosts" ]; then c_info "[$ENV_NAME] 监听守卫:insecureListen=${insecure:-0} allowedHosts=${hosts:-无}" fi @@ -1224,7 +1286,6 @@ agent_do_start() { nohup "$ENV_BIN" serve $exec_args >> "$ENV_DIR/svc.log" 2>&1 & fi echo $! > "$ENV_PID" - record_state # 健康探测:取 exec 里的 --addr(复用持久化配置时可能与当前派生值不同)。 local probe="${ENV_ADDR}" case "$exec_args" in *--addr\ *) @@ -1234,18 +1295,23 @@ agent_do_start() { [ "$phost" = "0.0.0.0" ] || [ "$phost" = "::" ] && phost="127.0.0.1" for _ in $(seq 1 100); do if curl -s -o /dev/null -m 2 "http://$phost:${probe##*:}/" 2>/dev/null; then + # 健康通过才写 svc.env——启动失败不再用新参数覆盖上次的可用配置 + #(2026-10-05 dev 事故:失败后坏 exec= 持久化,裸 start 复用导致二次失败)。 + record_state c_ok "[$ENV_NAME] 已启动 pid=$(read_pid) → http://$phost:${probe##*:}(来源 ${SVC_SOURCE:-?},日志:$ENV_DIR/svc.log)" hist_event start ok "$(read_pid)" "http://$phost:${probe##*:}" seed_settings "$phost" "${probe##*:}" return 0 fi pid_alive "$(read_pid)" || { - c_err "[$ENV_NAME] 进程启动后即退出,查看日志:tail -40 $ENV_DIR/svc.log" + c_err "[$ENV_NAME] 进程启动后即退出(登记未变更,svc.env 保留上次 exec=),查看日志:tail -40 $ENV_DIR/svc.log" hist_event start fail "$(read_pid)" "进程即退(日志 svc.log)" return 1 } sleep 0.2 done + # 进程存活但未响应:按实际运行参数登记(进程确以新参数在跑,登记与现实一致)。 + record_state c_err "[$ENV_NAME] 20s 健康检查未通过(进程存活但未响应),查看日志:tail -40 $ENV_DIR/svc.log" hist_event start fail "$(read_pid)" "健康检查超时" return 1 @@ -1356,22 +1422,23 @@ static_do_start() { c_info "[$ENV_NAME] 启动静态服务:python3 http.server $port --bind $host --directory $htdocs" nohup python3 -m http.server "$port" --bind "$host" --directory "$htdocs" >> "$ENV_DIR/svc.log" 2>&1 & echo $! > "$ENV_PID" - record_state "htdocs=$htdocs" local probe_host="$host" [ "$probe_host" = "0.0.0.0" ] || [ "$probe_host" = "::" ] && probe_host="127.0.0.1" for _ in $(seq 1 50); do if curl -s -o /dev/null -m 2 "http://$probe_host:$port/" 2>/dev/null; then + record_state "htdocs=$htdocs" # 健康通过才写 svc.env(与 agent 路径同规) c_ok "[$ENV_NAME] 已启动 pid=$(read_pid) → http://$probe_host:$port/(目录 $htdocs)" hist_event start ok "$(read_pid)" "http://$probe_host:$port/" return 0 fi pid_alive "$(read_pid)" || { - c_err "[$ENV_NAME] 进程启动后即退出,查看日志:tail -40 $ENV_DIR/svc.log" + c_err "[$ENV_NAME] 进程启动后即退出(登记未变更,svc.env 保留上次配置),查看日志:tail -40 $ENV_DIR/svc.log" hist_event start fail "$(read_pid)" "进程即退" return 1 } sleep 0.2 done + record_state "htdocs=$htdocs" c_err "[$ENV_NAME] 10s 健康检查未通过" hist_event start fail "$(read_pid)" "健康检查超时" return 1