Recover svc.sh database config across restarts and refuse on invalid binding

Why: the exec= reuse path stripped --dsn into the process environment (G1-7)
and record_state then persisted a DSN-less exec=, while the dbref fallback only
existed in the explicit-rebuild branch - the second bare restart after a
rebind silently dropped the database (SQLite fallback on loopback). Auth
preflight also matched --dsn on the command line only, so env-delivered DSN
with auth=auto was misjudged as unauthenticated.
Evidence: bash -n OK; make test-governance 115 tests OK (new tests/test_svc.py
11 cases; 10/11 fail against the pre-fix script); make test green; make build
OK; isolated real PostgreSQL 18.6 + real binary validation 27/27 (consecutive
restarts/deploy keep binding, psql-verified database identity, DB-down restart
fails without SQLite fallback or config loss, no credential leakage) - all
recorded via make evidence in the task card.
Impact: svc.env gains db=/dbref=/dsn= recovery keys (atomic 0600 writes);
start/restart/deploy/update/db share one resolver; invalid bindings, config
conflicts, mock coexistence and env-DSN-incapable binaries now refuse to
start instead of silently downgrading; legacy exec= DSN migrates idempotently;
docs/svc.md and script help updated.
Refs: task card .task/feature-svc-db-config-recover.md; user-approved plan
(2026-10-11 session message); AGENTS.md evidence gates.
This commit is contained in:
2026-10-11 09:16:54 +00:00
parent 6332767c39
commit 3b8fef7fad
4 changed files with 1315 additions and 155 deletions
+433 -110
View File
@@ -53,10 +53,15 @@
# db copy:--db X --force set:--port N --host H --apply --tag T
#
# ════════════════════ 配置优先级 ════════════════════
# 命令行 > --config/--env 文件 > svc.env 持久化(exec=/addr=)> SVC_<NAME>_* 环境变量
# (.svc.env 自动加载,同层)> 默认值。裸 start/restart 复用上次启动配置;显式参数
# 出现即覆盖。任意环境 X 的变量名:SVC_<X 大写,- → _>_ADDR/DATA/DSN/AUTH/MOCK,
# 以及 INSECURE_LISTEN / ALLOWED_HOSTS(传给进程的 AGENT_SYSTEM_*,旧二进制会忽略)。
# 命令行 > --config/--env 文件 > svc.env 持久化(exec=/addr=/dbref=/dsn=/db=)
# > SVC_<NAME>_* 环境变量(.svc.env 自动加载,同层)> 默认值。裸 start/restart 复用
# 上次启动配置;显式参数出现即覆盖。任意环境 X 的变量名:SVC_<X 大写,- → _>_
# ADDR/DATA/DSN/AUTH/MOCK,以及 INSECURE_LISTEN / ALLOWED_HOSTS(传给进程的
# AGENT_SYSTEM_*,旧二进制会忽略)。
# 数据库配置统一解析:托管绑定(dbref=)从实例登记重建 DSN,外部直连存受保护
# dsn= 字段;连接串不进 exec=/命令行,经 AGENT_SYSTEM_DSN 环境变量交付。绑定失效
# 或配置冲突(直接 DSN 与 dbref 不同库、mock 与绑定并存、db=pg 无可恢复 DSN)时
# 拒绝启动,绝不静默回退内嵌 SQLite;显式解绑(db=sqlite)后环境残留不再重新选库。
#
# ════════════════════ 服务模型与登记 ════════════════════
# - 服务名 ^[a-z][a-z0-9-]{0,15}$(保留字不可用);dev=8788 / prod=8787 默认端口,
@@ -202,7 +207,11 @@ svc.env 字段:
version 产物版本串(agent-system version 输出)
started 最近启动时间
pidcheck 进程判定标记(agent=二进制路径、postgres=pgdata 路径、static=--directory)
exec agent 上次完整启动参数(裸 start/restart 复用;显式参数出现即重建)
exec agent 上次完整启动参数(裸 start/restart 复用;显式参数出现即重建)。
只存非敏感参数——DSN 不在其中,数据库配置走 db=/dbref=/dsn= 专键
db agent 数据库模式:pg | sqlite(显式解绑)| mock;从未配置则无此键
dbref agent 托管数据库绑定 <pg名>/<库名>(启动时由实例登记重建 DSN)
dsn agent 外部直连 DSN(受保护,600;未命中本机实例时使用)
user/password/dbname/authmode/pgdata postgres 专属(密码 600 保存)
htdocs static 专属
@@ -248,7 +257,9 @@ EOF
行为细节:
- deploy 前自动停止旧实例(只停自己 pidfile+cmdline 校验的进程)
- agent 启动后等健康检查(20s),随后按 --model-*/--tavily-keys 播种设置(幂等)
- 旧版本产物不认识 --dsn/--auth 时自动省略(探测 serve 用法)并提示
- 旧版本产物不认识 --auth/--config 时自动省略(探测 serve 用法)并提示;
不消费环境变量 DSN(AGENT_SYSTEM_DSN)的旧产物在需要交付数据库配置时
拒绝部署/启动(不静默按内嵌 SQLite 运行)
- static 的 release 源不支持(制品内嵌前端);--dir PATH 可直接登记并服务
既有目录(不构建、不复制,source=dir@<路径>)
- deploy 全程事件入 history.jsonl(begin/ok/fail)
@@ -268,9 +279,10 @@ EOF
scripts/svc.sh <name> stop 停止(agent/static: TERM→KILL;postgres: pg_ctl -m fast)
scripts/svc.sh <name> restart 停止 + 启动(裸 restart 复用上次配置)
scripts/svc.sh <name> db [pg名|--clear] [--db X] agent 数据库绑定:缺省查看当前
绑定与可用实例;给 pg名=绑定/切换(DSN 写入 exec=,dbref= 登记,
实例运行中缺库自动建,服务运行中自动重启生效);--clear 解绑回内嵌
SQLite 档;--no-restart 只改登记
绑定与可用实例;给 pg名=绑定/切换(dbref= 登记,启动时由实例登记
重建 DSN 并经环境变量交付,实例运行中缺库自动建,服务运行中自动
重启生效);--clear 显式解绑(db=sqlite,清除直接 DSN,环境残留
SVC_*_DSN 不再重新选库);--no-restart 只改登记
scripts/svc.sh <name> config [file|--clear] [--no-restart] agent 配置文件:缺省查看当前
文件与生效值;给 file=更换(agent 二进制 --config,JSON schema 校验
通过才切换,运行中自动重启即时生效);--clear 移除回命令行参数形态。
@@ -335,9 +347,9 @@ EOF
scripts/svc.sh prod db pg1 --db prod # 绑定 pg1/prod 库(缺库自动建)
scripts/svc.sh prod db pg2 --no-restart # 切换实例但暂不重启(下次 start 生效)
scripts/svc.sh prod db # 查看当前绑定 + 可用实例
scripts/svc.sh prod db --clear # 解绑回内嵌 SQLite 档
绑定写入 svc.env(exec= 的 --dsn + dbref=,600);info / list 的 DB 列与数据库节
"绑定服务"行展示服务↔实例对应关系。
scripts/svc.sh prod db --clear # 显式解绑(db=sqlite,回内嵌档)
绑定登记于 svc.env(dbref= + db=pg,600;DSN 启动时由实例登记重建,经环境变量
交付);info / list 的 DB 列与数据库节 "绑定服务" 行展示服务↔实例对应关系。
密码管理:密码明文存 runtime/<name>/svc.env(600);--password 未给时自动生成
(head -c 18 /dev/urandom)。auth-mode 默认 scram-sha-256(pg_hba 全线)。
@@ -383,7 +395,7 @@ EOF
优先级(高 → 低):
1. 命令行参数(--port/--dsn/--tag/...)
2. --config FILE(或 --env FILE,兼容别名;=FILE 写法亦可)
3. svc.env 持久化(addr=、exec=——上次启动的实际参数)
3. svc.env 持久化(addr=、exec=、dbref=/dsn=/db=——数据库配置专键)
4. SVC_<NAME>_* 环境变量与 .svc.env(自动加载,同层)
5. 默认值(dev=8788 / prod=8787 / 其余派生端口)
@@ -401,11 +413,18 @@ dotenv 格式:KEY=VALUE,支持 # 注释、成对引号、export 前缀;只
播种幂等:设置持久化在 agent 数据目录,播过一次后续启动无需再传。
持久化语义:
- deploy/start 成功(健康探测通过)后把当前态写入 svc.env(600);启动失败不覆盖
上次配置——裸 start 仍按上次可用参数重试
- deploy/start 成功(健康探测通过)后把当前态写入 svc.env(600,临时文件原子替换);
启动失败不覆盖上次配置——裸 start 仍按上次可用参数重试
- 裸 start/restart 复用 exec=(上次完整参数);任何显式参数(或 --config 文件、
SVC_* 变量)出现即整体重建参数——重建时 dbref 绑定的 --dsn 自动从实例登记
重建保留(mock 档或显式 --dsn 除外)
SVC_* 变量)出现即整体重建参数;两种路径共用同一数据库配置解析——改端口/地址/
版本不改变数据库选择,conffile 引用随重建保留
- 数据库配置可恢复:托管绑定 dbref= 在启动时由实例登记重建 DSN(实例端口/密码
变更自动跟随);外部直连存受保护 dsn= 字段;显式解绑记 db=sqlite(环境残留
SVC_*_DSN 不再重新选库)。DSN 经 AGENT_SYSTEM_DSN 环境变量交付,不进 exec=
与命令行;存量 exec= 内的 --dsn 启动时自动迁移(幂等)
- 绑定失效或配置冲突时拒绝启动(不静默回退 SQLite):dbref 实例缺失/登记不全、
直接 DSN 与 dbref 指向不同库、mock 与绑定并存、db=pg 无可恢复 DSN、产物不
消费环境 DSN;restart/deploy 在停止旧进程前先做这些静态检查
- tags 随每次登记合并去重,跨重启保留
服务级配置文件(与上述 svc.sh dotenv 层不同——这是 agent 二进制自己的 JSON 配置):
@@ -661,21 +680,43 @@ svc_merge_tags() {
printf '%s' "$out"
}
# record_state EXTRA_KEY=VALUE...:把当前态+持久化配置写入 svc.env(600)。
# 基本键:kind/env/pid/addr/tags/source/version/started/pidcheck/exec + dbref/
# conffile(跨重启保留的数据库绑定与配置文件);kind 特有键(user/password/dbname/
# authmode/pgdata/htdocs...)经参数追加。
# record_state EXTRA_KEY=VALUE...:把当前态+持久化配置写入 svc.env(600,临时文件
# 原子替换,避免截断/半写)。基本键:kind/env/pid/addr/tags/source/version/started/
# pidcheck/exec + db/dbref/dsn/conffile(跨重启保留的数据库配置与配置文件引用);
# kind 特有键(user/password/dbname/authmode/pgdata/htdocs...)经参数追加。
# agent 服务经统一解析启动后按生效态写 db=(pg/mock;显式 sqlite)与 dbref=/dsn=
# (托管绑定不重复存 DSN,外部直连存受保护 dsn=);未走解析的调用保持旧值。
record_state() {
# 先读完旧文件再开重定向(块内 {} > svc.env 会先截断文件,读旧值必须在其之前)。
local pid prev_src prev_dbref prev_conffile merged_tags
local pid prev_src prev_dbref prev_conffile prev_db prev_dsn merged_tags
pid="$(read_pid)"
prev_src=""
[ -f "$ENV_DIR/svc.env" ] && prev_src="$(svc_get source)"
prev_dbref="$(svc_get dbref)"
prev_conffile="$(svc_get conffile)"
prev_db="$(svc_get db)"
prev_dsn="$(svc_get dsn)"
SVC_SOURCE="${SRC_LABEL:-${prev_src:-}}"
merged_tags="$(svc_merge_tags)"
local db_out="" dbref_out="" dsn_out=""
if [ "${SVC_KIND:-agent}" = agent ]; then
if [ -n "${SVC_DB_MODE:-}" ]; then
case "$SVC_DB_MODE" in
pg)
# 只在有可恢复来源(dbref=/dsn=/conffile)时记 pg 标记——纯环境层来源
# 不落标记,环境撤除后回到原语义,不产生"db=pg 却无可恢复 DSN"的死锁。
[ -n "$SVC_DB_REF" ] || [ -n "$SVC_DB_DSN_SAVE" ] || [ "$SVC_DB_SOURCE" = conffile ] && db_out=pg
;;
mock) db_out=mock ;;
sqlite) [ "$SVC_DB_SOURCE" != none ] && db_out=sqlite ;; # 显式解绑才记;从未配置不写
esac
dbref_out="${SVC_DB_REF:-}"
dsn_out="${SVC_DB_DSN_SAVE:-}"
else
db_out="$prev_db"; dbref_out="$prev_dbref"; dsn_out="$prev_dsn"
fi
fi
mkdir -p "$ENV_DIR"
local tmp="$ENV_DIR/svc.env.tmp.$$"
{
printf 'kind=%s\n' "${SVC_KIND:-agent}"
printf 'env=%s\n' "$ENV_NAME"
@@ -687,11 +728,14 @@ record_state() {
printf 'started=%s\n' "$(date -Is)"
printf 'pidcheck=%s\n' "${SVC_PIDCHECK:-}"
printf 'exec=%s\n' "${SVC_EXEC:-}"
[ -n "$prev_dbref" ] && printf 'dbref=%s\n' "$prev_dbref"
[ -n "$db_out" ] && printf 'db=%s\n' "$db_out"
[ -n "$dbref_out" ] && printf 'dbref=%s\n' "$dbref_out"
[ -n "$dsn_out" ] && printf 'dsn=%s\n' "$dsn_out"
[ -n "$prev_conffile" ] && printf 'conffile=%s\n' "$prev_conffile"
[ $# -gt 0 ] && printf '%s\n' "$@"
} > "$ENV_DIR/svc.env"
chmod 600 "$ENV_DIR/svc.env"
} > "$tmp"
chmod 600 "$tmp"
mv -f "$tmp" "$ENV_DIR/svc.env"
}
# ---- pid 管理(泛化:pidfile + cmdline 标记双校验)----
@@ -756,6 +800,8 @@ env_config() {
ENV_LOG="$ENV_DIR/svc.log"
ENV_BIN_DIR="$ENV_DIR/bin"
ENV_BIN="$ENV_BIN_DIR/agent-system"
# 数据库配置解析态按实例重置——批量 update/restart 逐服务解析时不串用上一服务的 DB_*。
SVC_DB_MODE=""; SVC_DB_SOURCE=""; SVC_DB_REF=""; SVC_DB_DSN_SAVE=""
local registered=""
[ -f "$ENV_DIR/svc.env" ] && registered="$(svc_kind "$name")"
SVC_KIND="${registered:-agent}"
@@ -810,7 +856,7 @@ agent_has_explicit_cfg() {
return 1
}
# adjust_args_for_bin BIN ARGS:按二进制 serve 用法过滤旧产物不认识的 --dsn/--auth。
# adjust_args_for_bin BIN ARGS:按二进制 serve 用法过滤旧产物不认识的 --auth/--config。
# 匹配按"去横线的旗标名 + 1-2 个横线"(Go flag 帮助打印单横线 -dsn,2026-10-04 实测
# 旧实现 grep -- '--dsn' 永不命中,把现役二进制的 --dsn/--auth 误当不支持而丢弃)。
adjust_args_for_bin() {
@@ -819,7 +865,7 @@ adjust_args_for_bin() {
local filtered="" w drop_next=0 flag
for w in $args; do
case "$w" in
--dsn|--auth)
--auth|--config)
flag="${w#--}"
if printf '%s\n' "$out" | grep -Eq -- "(^|[[:space:]])-{1,2}${flag}([[:space:]]|=|$)"; then
filtered="$filtered $w"
@@ -1087,6 +1133,12 @@ materialize_release() {
agent_do_deploy() {
resolve_source
if [ "$CLI_DRYRUN" = 1 ]; then print_agent_plan; return 0; fi
# 停旧之前的静态配置验证:数据库配置可解析(绑定有效/无冲突)、conffile 存在、
# 现产物承接能力——配置性问题不制造停机窗口(登记保持可恢复,失败直接返回)。
if ! agent_static_preflight; then
hist_event deploy fail "$(read_pid)" "预检拒绝:数据库配置/配置文件无效(登记未变更)"
return 1
fi
hist_event deploy begin "$(read_pid)" "from=$SRC_LABEL"
do_stop_quiet
local wt
@@ -1108,6 +1160,13 @@ agent_do_deploy() {
materialize_release || { hist_event deploy fail "$(read_pid)" "物化/校验失败($SRC_LABEL)"; return 1; }
;;
esac
# 新产物承接能力复核(环境 DSN 交付):不支持即失败——绑定保持可恢复,不降级启动。
if [ "$DB_MODE" = pg ] && [ "$DB_SOURCE" != conffile ] && ! bin_env_dsn_supported "$ENV_BIN"; then
c_err "[$ENV_NAME] 新产物不消费环境变量 DSN(AGENT_SYSTEM_DSN)——部署中止;旧绑定保留在 svc.env"
c_err " 恢复:换部署版本,或改用携带 dsn 的配置文件(scripts/svc.sh $ENV_NAME config <file.json>)"
hist_event deploy fail "$(read_pid)" "新产物不支持环境 DSN($SRC_LABEL)"
return 1
fi
agent_do_start || { hist_event deploy fail "$(read_pid)" "启动失败($SRC_LABEL)"; return 1; }
hist_event deploy ok "$(read_pid)" "from=$SRC_LABEL"
return 0
@@ -1198,15 +1257,253 @@ addr_is_loopback() {
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 ;;
# dsn_target DSN → host:port/db(配置一致性比较用;解析失败回空串)。
dsn_target() {
local t="${1%%\?*}"
case "$t" in *://*) t="${t#*://}" ;; esac
case "$t" in *@*) t="${t#*@}" ;; esac
local hostport="${t%%/*}" db="${t#*/}"
[ "$db" = "$t" ] && db=""
printf '%s/%s' "$hostport" "$db"
}
# dsn_ref_match DSN:DSN 的 host:port/db 命中已登记 postgres 实例时回显 <inst>/<db>(无匹配回空)。
dsn_ref_match() {
local tgt pg paddr
tgt="$(dsn_target "$1")"
[ -n "$tgt" ] || return 0
while IFS= read -r pg; do
[ "$(svc_kind "$pg")" = postgres ] || continue
paddr="$(env_kv_of "$pg" addr)"
[ -n "$paddr" ] || continue
if [ "$tgt" = "$paddr/${tgt#*/}" ]; then
printf '%s/%s' "$pg" "${tgt#*/}"
return 0
fi
done < <(registered_names)
return 0
}
# auth_pick CLI ENV PERSISTED:生效认证方式(缺省 auto)。
auth_pick() {
case "${1:-}" in ?*) printf '%s' "$1"; return ;; esac
case "${2:-}" in ?*) printf '%s' "$2"; return ;; esac
case "${3:-}" in ?*) printf '%s' "$3"; return ;; esac
printf auto
}
# migrate_legacy_db_config:存量 exec= 的 --dsn 迁移为可恢复配置(dbref=/dsn=)并从 exec
# 剥离——此后 exec= 只存非敏感启动参数,DSN 经环境变量传递(G1-7)。幂等:再次运行时
# exec= 已无 --dsn,直接返回。命中已登记实例 → dbref(密码经实例登记存放,不重复保存);
# 否则 → 受保护 dsn= 字段。已有 dbref= 时 exec 的 DSN 只作历史残留处理,直接剥离。
migrate_legacy_db_config() {
local exec_old dsn new_exec ref
[ -f "$ENV_DIR/svc.env" ] || return 0
exec_old="$(svc_get exec)"
case "$exec_old" in *--dsn\ *) ;; *) return 0 ;; esac
dsn="$(printf '%s' "$exec_old" | sed -n 's/.*--dsn \([^ ]*\).*/\1/p')"
new_exec="$(printf '%s' "$exec_old" | sed -E 's/(^| )--dsn [^ ]+//; s/ +/ /g; s/^ //; s/ $//')"
svc_set_key exec "$new_exec"
if [ -n "$(svc_get dbref)" ]; then
c_info "[$ENV_NAME] 存量 exec DSN 与 dbref=$(svc_get dbref) 并存,已剥离 exec 残留(绑定不受影响)"
return 0
fi
ref="$(dsn_ref_match "$dsn")"
if [ -n "$ref" ]; then
svc_set_key dbref "$ref"
svc_set_key db pg
c_info "[$ENV_NAME] 存量 exec DSN 已迁移为托管绑定 dbref=$ref(DSN 改由实例登记重建)"
else
svc_set_key dsn "$dsn"
svc_set_key db pg
c_info "[$ENV_NAME] 存量 exec DSN 已迁移至受保护 dsn= 字段(未命中本机实例登记)"
fi
return 0
}
# ---- 数据库配置统一解析(start/restart/deploy/update/db 共用单点)----
# 核心约束:修改端口、监听地址或部署版本不改变数据库选择;绑定失效或配置冲突时
# 拒绝启动(返回 1),绝不静默回退内嵌 SQLite。
# 结果全局变量(入口全部重置——批量 update 逐服务重解析,不串用):
# DB_MODE pg | sqlite | mock
# DB_SOURCE cli | env | dbref | dsn | conffile | db | mock | none
# DB_REF <inst>/<db>(命中/保持托管绑定时;否则空)
# DB_DSN 有效连接串(sqlite/mock 为空;conffile 来源=文件内值,仅用于判定不外传)
# DB_AUTH on | off | auto(生效值,对齐二进制 AuthEnabled 口径)
# DB_DSN_SAVE 需持久化到 dsn= 的外部直连 DSN(仅 cli/dsn 来源且未命中实例)
resolve_db_config() {
DB_MODE="sqlite"; DB_SOURCE="none"; DB_REF=""; DB_DSN=""; DB_AUTH="auto"; DB_DSN_SAVE=""
migrate_legacy_db_config
local env_auth env_mock env_dsn
env_auth="$(env_var "$ENV_NAME" AUTH)"
env_mock="$(env_var "$ENV_NAME" MOCK)"
env_dsn="$(env_var "$ENV_NAME" DSN)"
local p_dbref p_dsn p_db p_conf p_exec p_auth
p_dbref="$(svc_get dbref)"
p_dsn="$(svc_get dsn)"
p_db="$(svc_get db)"
p_conf="$(svc_get conffile)"
p_exec="$(svc_get exec)"
p_auth="$(printf '%s' "$p_exec" | sed -n 's/.*--auth \([^ ]*\).*/\1/p')"
[ -n "$p_auth" ] || p_auth="$(svc_get auth)"
DB_AUTH="$(auth_pick "$CLI_AUTH" "$env_auth" "$p_auth")"
case "$DB_AUTH" in on|off|auto) ;; *)
c_err "[$ENV_NAME] 拒绝启动:无效 auth 值「$DB_AUTH」(可用 on/off/auto)"
return 1 ;;
esac
local mock=0
{ [ "$CLI_MOCK" = 1 ] || [ "$env_mock" = "1" ]; } && mock=1
[ "$mock" = 0 ] && case "$p_exec" in *--mock*) mock=1 ;; esac
# conffile 的 dsn 键(二进制自行读文件,连接串不经环境重复传递)。
local cf_dsn=""
if [ -n "$p_conf" ] && [ -f "$p_conf" ]; then
cf_dsn="$(grep -oE '"dsn"[[:space:]]*:[[:space:]]*"[^"]*"' "$p_conf" 2>/dev/null | head -1 | sed -E 's/.*"([^"]*)"$/\1/')"
fi
# mock 档与数据库配置共存 = 冲突,拒绝(行为要明确,不隐式降级)。
if [ "$mock" = 1 ]; then
if [ -n "$p_dbref" ] || [ -n "$p_dsn" ] || [ -n "$CLI_DSN" ] || { [ -n "$env_dsn" ] && [ "$p_db" != "sqlite" ]; }; then
c_err "[$ENV_NAME] 拒绝启动:mock 档与数据库配置冲突(dbref=${p_dbref:-无}、dsn=${p_dsn:+已设、}CLI/环境 DSN=${CLI_DSN:+已设}${env_dsn:+已设})"
c_err " 先解绑(scripts/svc.sh $ENV_NAME db --clear)再用 mock,或去掉 --mock / SVC_$(printf '%s' "$ENV_NAME" | tr 'a-z-' 'A-Z_')_MOCK"
return 1
fi
DB_MODE=mock; DB_SOURCE=mock
return 0
fi
# 显式解绑(db=sqlite):残留 SVC_*_DSN 不再悄悄重新选库;CLI 显式 --dsn 仍可覆盖。
if [ -n "$env_dsn" ] && [ "$p_db" = "sqlite" ] && [ -z "$CLI_DSN" ]; then
c_info "[$ENV_NAME] 已显式解绑(db=sqlite),忽略环境层 SVC_*_DSN 残留(显式 --dsn 或重新绑定可覆盖)"
env_dsn=""
fi
# 有效 DSN 选择:CLI > 环境 > 持久化(dbref / dsn) > conffile > 无(SQLite)。
local eff_dsn="" eff_src="" bt
if [ -n "$CLI_DSN" ]; then
eff_dsn="$CLI_DSN"; eff_src=cli
elif [ -n "$env_dsn" ]; then
eff_dsn="$env_dsn"; eff_src=env
elif [ -n "$p_dbref" ] && [ -n "$p_dsn" ]; then
bt="$(dsn_from_dbref "$p_dbref" 2>/dev/null || true)"
if [ -n "$bt" ] && [ "$(dsn_target "$bt")" = "$(dsn_target "$p_dsn")" ]; then
eff_dsn="$p_dsn"; eff_src=dsn
else
c_err "[$ENV_NAME] 拒绝启动:配置冲突——dbref=$p_dbref 与 dsn= 指向不同数据库,不能静默取其一"
c_err " 保留其一:重新绑定(scripts/svc.sh $ENV_NAME db <pg实例>)或 db --clear 后重新直连"
return 1
fi
elif [ -n "$p_dbref" ]; then
if ! eff_dsn="$(dsn_from_dbref "$p_dbref")"; then
c_err "[$ENV_NAME] 拒绝启动:dbref=$p_dbref 无法重建 DSN(实例未登记 / 类型错误 / addr·user·password 登记不全)"
c_err " 恢复实例登记或重新绑定(scripts/svc.sh $ENV_NAME db <pg实例>);不回退内嵌 SQLite"
return 1
fi
eff_src=dbref
elif [ -n "$p_dsn" ]; then
eff_dsn="$p_dsn"; eff_src=dsn
elif [ -n "$cf_dsn" ]; then
eff_dsn="$cf_dsn"; eff_src=conffile
fi
# 直接 DSN(CLI/环境/持久化)与 dbref 并存:目标库必须一致,否则配置冲突。
if [ -n "$eff_dsn" ] && [ -n "$p_dbref" ] && [ "$eff_src" != dbref ]; then
if ! bt="$(dsn_from_dbref "$p_dbref")"; then
c_err "[$ENV_NAME] 拒绝启动:dbref=$p_dbref 无效(无法重建 DSN)且存在直接 DSN——先修复或解除绑定"
return 1
fi
if [ "$(dsn_target "$bt")" != "$(dsn_target "$eff_dsn")" ]; then
c_err "[$ENV_NAME] 拒绝启动:直接 DSN($(mask_dsn "$eff_dsn"))与 dbref=$p_dbref 指向不同数据库,不能静默取其一"
return 1
fi
fi
# conffile 一致性:文件内 dsn 与有效来源并存时必须同库。
if [ -n "$cf_dsn" ] && [ "$eff_src" != conffile ] && [ -n "$eff_dsn" ] \
&& [ "$(dsn_target "$cf_dsn")" != "$(dsn_target "$eff_dsn")" ]; then
c_err "[$ENV_NAME] 拒绝启动:配置文件 dsn($(mask_dsn "$cf_dsn"))与数据库绑定指向不同库——先对齐(改文件或换绑定)"
return 1
fi
# 已选 PostgreSQL 但拿不到任何有效 DSN → 拒绝(不降级 SQLite)。
if [ -z "$eff_dsn" ] && [ "$p_db" = "pg" ]; then
c_err "[$ENV_NAME] 拒绝启动:已登记 db=pg 但无可恢复 DSN(dbref/dsn/conffile 均缺失)"
c_err " 重新绑定(scripts/svc.sh $ENV_NAME db <pg实例>)或 db --clear 显式回内嵌 SQLite"
return 1
fi
if [ -n "$eff_dsn" ]; then
DB_MODE=pg; DB_DSN="$eff_dsn"; DB_SOURCE="$eff_src"
local ref=""
case "$eff_src" in
cli|dsn)
if [ -n "$p_dbref" ]; then
DB_REF="$p_dbref" # 一致性已验证:保持既有托管绑定
else
ref="$(dsn_ref_match "$eff_dsn")"
if [ -n "$ref" ]; then DB_REF="$ref" # 直连命中本机实例 → 升级托管绑定(密码不重复保存)
else DB_DSN_SAVE="$eff_dsn"
fi
fi
;;
env)
[ -n "$p_dbref" ] && DB_REF="$p_dbref" # 环境层本身不持久化;已有绑定/直连保持
[ -n "$p_dsn" ] && DB_DSN_SAVE="$p_dsn" # 环境覆盖不丢已持久化的直连配置
;;
dbref)
DB_REF="$p_dbref"
;;
esac
else
DB_MODE=sqlite
[ "$p_db" = "sqlite" ] && DB_SOURCE=db # 显式解绑的选择跨重启保留
fi
return 0
}
# db_auth_enabled:启动预检的认证判定,对齐二进制 AuthEnabled:显式 on → 开;
# 显式 off → 关;auto → 存在有效 DSN(含环境与配置文件来源)即开。
db_auth_enabled() {
case "$DB_AUTH" in
on) return 0 ;;
off) return 1 ;;
*) [ -n "$DB_DSN" ] ;;
esac
}
# bin_env_dsn_supported BIN:二进制是否消费环境层 AGENT_SYSTEM_DSN(经 config explain
# 探测:explain 对 DSN 值脱敏但用户名保留,探测标记用户即可)。旧产物不支持环境层时
# 返回 1——调用方拒绝以环境传递 DSN(防静默按内嵌 SQLite 运行)。
bin_env_dsn_supported() {
local out
out="$(AGENT_SYSTEM_DSN='postgresql://svcprobe:svcprobe@127.0.0.1:9/svcprobe' "$1" config explain 2>/dev/null || true)"
case "$out" in *svcprobe*) return 0 ;; esac
return 1
}
# agent_static_preflight:停止旧进程 / 更换产物之前可静态检查的配置验证——数据库配置
# 可解析(含绑定有效性与冲突)、conffile 存在、现产物能承接环境 DSN。失败返回 1(输出
# 脱敏原因;除存量迁移外不写登记)。
agent_static_preflight() {
resolve_db_config || return 1
local cf
cf="$(svc_get conffile)"
if [ -n "$cf" ] && [ ! -f "$cf" ]; then
c_err "[$ENV_NAME] 拒绝启动:配置文件不存在:$cf(恢复文件或 config --clear)"
return 1
fi
if [ "$DB_MODE" = pg ] && [ "$DB_SOURCE" != conffile ] && [ -x "$ENV_BIN" ] \
&& ! bin_env_dsn_supported "$ENV_BIN"; then
c_err "[$ENV_NAME] 拒绝启动:当前产物不消费环境变量 DSN(AGENT_SYSTEM_DSN),无法按绑定非降级交付数据库配置"
c_err " 更新产物(deploy 新版本),或改用携带 dsn 的配置文件(scripts/svc.sh $ENV_NAME config <file.json>)"
return 1
fi
return 0
}
agent_do_start() {
@@ -1218,52 +1515,34 @@ agent_do_start() {
return 1
fi
SVC_VERSION="$("$ENV_BIN" version 2>/dev/null || true)"
# 组装启动参数:显式配置 → 重建;否则复用上次持久化 exec=。
# 统一解析数据库配置(复用与重建两路共用;失败拒绝启动,绝不静默回退 SQLite)。
# 2026-10-11 修复:复用 exec= 分支此前不查 dbref——G1-7 迁移把 --dsn 剥入环境后
# record_state 写回不含 DSN 的 exec=,重新绑定后的第二次裸 restart 即丢库。
if ! resolve_db_config; then
hist_event start fail "" "数据库配置解析拒绝(绑定失效/配置冲突)"
return 1
fi
SVC_DB_MODE="$DB_MODE"; SVC_DB_SOURCE="$DB_SOURCE"; SVC_DB_REF="$DB_REF"; SVC_DB_DSN_SAVE="$DB_DSN_SAVE"
# 组装启动参数:显式配置 → 重建;否则复用上次持久化 exec=(DSN 已由解析迁移出 exec)。
local exec_args
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")"
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
local args="" explicit_auth="${CLI_AUTH:-$(env_var "$ENV_NAME" AUTH)}"
if [ "$DB_MODE" = mock ]; then
args="--mock"
else
local dsn="${CLI_DSN:-$(env_var "$ENV_NAME" DSN)}"
local auth="${CLI_AUTH:-$(env_var "$ENV_NAME" 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
# G1-7:DSN 经环境变量传入(AGENT_SYSTEM_DSN),不再进命令行——命令行对
# 同机任何进程可见(含智能体生成脚本的 /proc/<pid>/cmdline),2026-10-09
# 方案 §3.4 实测。基座 config 优先级 CLI>env,旧 exec= 里的 --dsn 仍生效
# (向后兼容),但新建/重建的启动配置从此不带 DSN 明文。
DSN_ENV="$dsn"
if [ -n "$dsn" ] || [ -n "$auth" ] || [ "$ENV_NAME" != dev ]; then
[ -z "$auth" ] && auth=auto
args="$args --auth $auth"
fi
elif [ "$DB_MODE" = pg ] || [ -n "$explicit_auth" ] || [ "$ENV_NAME" != dev ]; then
args="--auth $DB_AUTH"
fi
# 保留既有配置文件引用:显式参数重建时不丢 --config(conffile= 持久化跨重启)。
local conf; conf="$(svc_get conffile)"
[ -n "$conf" ] && [ -f "$conf" ] && args="$args --config $conf"
local adjusted; adjusted="$(adjust_args_for_bin "$ENV_BIN" "$args")"
# adjust_args_for_bin 返回值已去首空格——比较前对 args 同样归一,否则每次
# 带 --dsn/--auth 的重建都会误报"旧版本二进制已自动省略"(参数实际全保留)。
# adjust_args_for_bin 返回值已去首空格——比较前对 args 同样归一,否则每次带
# --auth/--config 的重建都会误报"旧版本二进制已自动省略"(参数实际全保留)。
if [ "$adjusted" != "${args# }" ]; then
c_warn "[$ENV_NAME] 该产物不支持 --dsn/--auth(旧版本二进制),已自动省略:$(mask_dsn "${args:-<无>}") → $(mask_dsn "${adjusted:-<无>}")"
c_warn "[$ENV_NAME] 该产物不支持部分启动参数(旧版本二进制),已自动省略:${args:-<无>} → ${adjusted:-<无>}"
fi
args="$adjusted"
exec_args="--addr $ENV_ADDR --data $ENV_DATA --plugins ${CLI_PLUGINS:-$ENV_DIR/plugins} $args $CLI_EXTRA"
@@ -1278,9 +1557,12 @@ agent_do_start() {
[ -n "$hosts" ] && guard_env+=("AGENT_SYSTEM_ALLOWED_HOSTS=$hosts")
# 预检(快速拦截):非回环地址且认证判定为关时提前拒绝,报错可操作——
# 不等二进制 listen guard 以"进程即退"暴露;二进制守卫仍是权威。
# 认证判定依据统一解析的最终有效 DSN 与 auth(含环境变量与配置文件来源),
# 与二进制 AuthEnabled 口径一致(2026-10-11 修复:此前只看命令行 --dsn,
# DSN 迁入环境变量后 auto 被误判为未开认证)。
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 ! addr_is_loopback "${pf_addr%:*}" && [ -z "$insecure" ] && ! db_auth_enabled; 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 显式放行。"
@@ -1294,8 +1576,19 @@ agent_do_start() {
fi
# 直接 nohup 简单命令:$! 即服务进程本身,pidfile 语义才成立。
# 守卫变量走环境,不写进 exec=;旧二进制不认识这两个变量,会忽略。
# G1-7:DSN 同样走环境(AGENT_SYSTEM_DSN;基座 config 环境档已支持)。
[ -n "${DSN_ENV:-}" ] && guard_env+=("AGENT_SYSTEM_DSN=$DSN_ENV")
# G1-7:DSN 经环境变量传入(AGENT_SYSTEM_DSN),不进命令行——命令行对同机任何
# 进程可见(含智能体生成脚本的 /proc/<pid>/cmdline)。conffile 来源由二进制自行
# 读文件,不重复传递。环境层交付前再核一次产物能力(start 可能紧跟 deploy 换产物),
# 不支持即拒绝——绝不静默按内嵌 SQLite 运行。
if [ "$DB_MODE" = pg ] && [ "$DB_SOURCE" != conffile ]; then
if ! bin_env_dsn_supported "$ENV_BIN"; then
c_err "[$ENV_NAME] 拒绝启动:产物不消费环境变量 DSN(AGENT_SYSTEM_DSN),无法按绑定交付数据库配置(不回退 SQLite)"
c_err " 更新产物(deploy 新版本),或改用携带 dsn 的配置文件(scripts/svc.sh $ENV_NAME config <file.json>)"
hist_event start fail "" "产物不支持环境 DSN,拒绝启动"
return 1
fi
guard_env+=("AGENT_SYSTEM_DSN=$DB_DSN")
fi
if [ "${#guard_env[@]}" -gt 0 ];then
nohup env "${guard_env[@]}" "$ENV_BIN" serve $exec_args >> "$ENV_DIR/svc.log" 2>&1 &
else
@@ -1671,13 +1964,16 @@ check_port_available() {
return 0
}
# svc_set_key KEY VALUE:改写当前服务 svc.env 的单键(先读后写,避开重定向截断陷阱)。
# svc_set_key KEY VALUE:改写当前服务 svc.env 的单键(先读后写 + 临时文件原子替换,
# 避免重定向截断与半写)。
svc_set_key() {
local key="$1" val="$2" rest
local key="$1" val="$2" rest tmp
[ -f "$ENV_DIR/svc.env" ] || { c_err "svc.env 不存在:$ENV_DIR"; return 1; }
rest="$(grep -v "^$key=" "$ENV_DIR/svc.env" || true)"
{ [ -n "$rest" ] && printf '%s\n' "$rest"; printf '%s=%s\n' "$key" "$val"; } > "$ENV_DIR/svc.env"
chmod 600 "$ENV_DIR/svc.env"
tmp="$ENV_DIR/svc.env.tmp.$$"
{ [ -n "$rest" ] && printf '%s\n' "$rest"; printf '%s=%s\n' "$key" "$val"; } > "$tmp"
chmod 600 "$tmp"
mv -f "$tmp" "$ENV_DIR/svc.env"
return 0
}
@@ -2134,9 +2430,10 @@ EOF
# ---- agent 服务的数据库绑定(<name> db ...)----
# svc_db_bind [pg名] [--db X] [--clear] [--no-restart]:查看 / 绑定切换 / 解除
# agent 服务的数据库。绑定 = 目标实例的 DSN 写入持久化 exec=(--dsn,密码 600 存放)
# 并登记 dbref=<pg>/<库>;实例运行中时确保目标库存在(缺则建);服务运行中则自动
# 重启生效(--no-restart 只改登记)。--clear 解除绑定(回内嵌 SQLite 档)。
# agent 服务的数据库。绑定 = dbref=<pg>/<库> + db=pg 登记(600);DSN 不落 exec=,
# 启动时由实例登记重建并经 AGENT_SYSTEM_DSN 环境变量交付(实例运行中缺库自动建;
# 服务运行中自动重启生效,--no-restart 只改登记)。--clear 显式解绑:db=sqlite、
# 清除 dbref 与直接 dsn=,残留 SVC_*_DSN 不再悄悄重新选库。
svc_db_bind() {
[ "$SVC_KIND" = agent ] || { c_err "[$ENV_NAME] kind=$SVC_KIND 无数据库绑定(仅 agent 服务)"; return 2; }
[ -f "$ENV_DIR/svc.env" ] || { c_err "[$ENV_NAME] 无登记(先 deploy)"; return 2; }
@@ -2151,7 +2448,7 @@ svc_db_bind() {
[$ENV_NAME] 数据库绑定:
scripts/svc.sh $ENV_NAME db 查看当前绑定与可用实例
scripts/svc.sh $ENV_NAME db <pg名> [--db X] [--no-restart] 绑定/切换(运行中自动重启生效)
scripts/svc.sh $ENV_NAME db --clear [--no-restart] 解除绑定(回内嵌 SQLite 档)
scripts/svc.sh $ENV_NAME db --clear [--no-restart] 解除绑定(显式回内嵌 SQLite 档)
EOF
return 0 ;;
-*) c_err "[$ENV_NAME] db 未知参数:$1(可用:<pg名> | --clear,--db X,--no-restart)"; return 2 ;;
@@ -2162,12 +2459,17 @@ EOF
# 无参:查看当前绑定 + 可用实例。
if [ -z "$target" ] && [ "$clear" = 0 ]; then
local ref dsn pg st
local ref dsn pg st mode
mode="$(svc_get db)"
ref="$(db_ref_of "$ENV_NAME")"
if [ -n "$ref" ]; then
printf ' [%s] 数据库绑定:%s\n' "$ENV_NAME" "$ref"
dsn="$(printf '%s' "$(svc_get exec)" | sed -n 's/.*--dsn \([^ ]*\).*/\1/p')"
dsn="$(svc_get dsn)"
[ -z "$dsn" ] && dsn="$(printf '%s' "$(svc_get exec)" | sed -n 's/.*--dsn \([^ ]*\).*/\1/p')"
[ -z "$dsn" ] && [ -n "$(svc_get dbref)" ] && dsn="$(dsn_from_dbref "$(svc_get dbref)" 2>/dev/null || true)"
[ -n "$dsn" ] && printf ' DSN %s\n' "$(mask_dsn "$dsn")"
elif [ "$mode" = "sqlite" ]; then
printf ' [%s] 数据库绑定:已显式解绑(db=sqlite,data=%s;残留 SVC_*_DSN 不再自动选库)\n' "$ENV_NAME" "$ENV_DATA"
else
printf ' [%s] 数据库绑定:无(内嵌 SQLite 档,data=%s)\n' "$ENV_NAME" "$ENV_DATA"
fi
@@ -2185,7 +2487,7 @@ EOF
return 0
fi
# 解除绑定。
# 解除绑定(显式选择 SQLite:清除 dbref 与直接 dsn,环境残留不再重新选库)。
if [ "$clear" = 1 ]; then
[ -n "$target" ] && { c_err "--clear 不接受服务名参数"; return 2; }
local exec_old new_exec
@@ -2195,8 +2497,10 @@ EOF
svc_set_key exec "$new_exec"
fi
svc_del_key dbref
svc_del_key dsn
svc_set_key db sqlite
hist_event set ok "$(read_pid)" "db unbind"
c_ok "[$ENV_NAME] 已解除数据库绑定(内嵌 SQLite 档;数据目录 $ENV_DATA)"
c_ok "[$ENV_NAME] 已解除数据库绑定(显式 db=sqlite;数据目录 $ENV_DATA)"
if [ "$restart" = 1 ] && [ -n "$(read_pid)" ] && pid_alive "$(read_pid)"; then
do_stop; do_start || { c_err "[$ENV_NAME] 重启失败——解绑已登记,排查后 start"; return 1; }
c_ok "[$ENV_NAME] 已重启(内嵌 SQLite 档)"
@@ -2217,7 +2521,21 @@ EOF
tpass="$(env_kv_of "$target" password)"
regdb="$(env_kv_of "$target" dbname)"
tdb="${CLI_DB_NAME:-$regdb}"
{ [ -n "$tuser" ] && [ -n "$tpass" ]; } || { c_err "[$target] 登记缺 user/password(重新 db create)"; return 2; }
{ [ -n "$tuser" ] && [ -n "$tpass" ] && [ -n "$taddr" ]; } || { c_err "[$target] 登记缺 addr/user/password(重新 db create)"; return 2; }
# mock 档与绑定冲突:拒绝——绑定后按 mock 运行等于静默丢库。
local exec_now mock_now=0
exec_now="$(svc_get exec)"
{ [ "$CLI_MOCK" = 1 ] || [ "$(env_var "$ENV_NAME" MOCK)" = "1" ]; } && mock_now=1
case "$exec_now" in *--mock*) mock_now=1 ;; esac
if [ "$mock_now" = 1 ]; then
c_err "[$ENV_NAME] 拒绝绑定:当前为 mock 档(--mock)与数据库配置冲突;先去 mock(重新 deploy 不带 --mock)或保持 mock 不绑定"
return 2
fi
# 产物承接能力预检:绑定后 DSN 经环境变量交付,产物须消费 AGENT_SYSTEM_DSN。
if [ -x "$ENV_BIN" ] && ! bin_env_dsn_supported "$ENV_BIN"; then
c_err "[$ENV_NAME] 拒绝绑定:当前产物不消费环境变量 DSN(AGENT_SYSTEM_DSN),绑定后无法生效;先更新产物再绑定"
return 2
fi
# 实例运行中:确保目标库存在(缺则建,幂等)。
st="$(svc_pid_state "$target")"
if [ "${st%% *}" = alive ]; then
@@ -2232,26 +2550,19 @@ EOF
c_warn "[$target] 未运行——库 $tdb 暂未创建(先 start $target 再重跑,或绑定默认库 $regdb)"
fi
dsn="postgresql://$tuser:$tpass@$thost:$tport/$tdb"
# 产物支持预检(旧二进制不识别 --dsn 时提示;重启复用 exec 不会自动过滤)。
if [ -x "$ENV_BIN" ]; then
local adj
adj="$(adjust_args_for_bin "$ENV_BIN" "--dsn $dsn")"
[ "$adj" != "--dsn $dsn" ] && \
c_warn "[$ENV_NAME] 当前产物不识别 --dsn(旧版本二进制)——绑定已登记,但重启后将按内嵌档运行"
# 写持久化配置(托管绑定形态):dbref= + db=pg;清除直接 dsn=;剥离 exec 残留 --dsn。
# DSN 不落 exec=——启动时由实例登记重建(实例端口/密码变更自动跟随,密码不重复保存)。
if printf '%s' "$exec_now" | grep -q -- '--dsn '; then
local new_exec
new_exec="$(printf '%s' "$exec_now" | sed -E 's/(^| )--dsn [^ ]+//; s/ +/ /g; s/^ //; s/ $//')"
svc_set_key exec "$new_exec"
fi
# 写持久化配置:exec= 的 --dsn 替换或追加;dbref= 登记。
local exec_old new_exec
exec_old="$(svc_get exec)"
if printf '%s' "$exec_old" | grep -q -- '--dsn '; then
new_exec="$(printf '%s' "$exec_old" | sed -E "s|--dsn [^ ]+|--dsn $dsn|")"
else
new_exec="${exec_old:+$exec_old }--dsn $dsn"
fi
svc_set_key exec "$new_exec"
svc_set_key dbref "$target/$tdb"
svc_set_key db pg
svc_del_key dsn
hist_event set ok "$(read_pid)" "db bind → $target/$tdb"
c_ok "[$ENV_NAME] 数据库绑定:$target/$tdb(DSN $(mask_dsn "$dsn");svc.env 600 保存,info 可查)"
# 运行中:重启生效(start 复用新 exec=)。
c_ok "[$ENV_NAME] 数据库绑定:$target/$tdb(启动时由实例登记重建 DSN:$(mask_dsn "$dsn");svc.env 600 保存,info 可查)"
# 运行中:重启生效(start 按新登记重建 DSN 交付)。
if [ "$restart" = 1 ] && [ -n "$(read_pid)" ] && pid_alive "$(read_pid)"; then
c_info "[$ENV_NAME] 重启以切换数据库……"
do_stop
@@ -2370,6 +2681,17 @@ do_stop_quiet() {
return 0
}
# do_restart:agent 先完成可静态检查的配置验证(数据库绑定可解析、conffile 存在、
# 产物承接能力)再停旧进程——配置性拒绝不制造停机窗口,登记保持可恢复;其余类型
# 直接停止后启动。
do_restart() {
if [ "$SVC_KIND" = agent ]; then
agent_static_preflight || { hist_event start fail "" "restart 预检拒绝(数据库配置/配置文件无效)"; return 1; }
fi
do_stop
do_start
}
cmd_rm() {
local name="${1:-}"
[ -n "$name" ] || { c_err "用法:scripts/svc.sh rm <name> [--purge]"; exit 2; }
@@ -2427,6 +2749,7 @@ db_ref_of() {
ref="$(env_kv_of "$name" dbref)"
if [ -n "$ref" ]; then printf '%s' "$ref"; return 0; fi
dsn="$(printf '%s' "$(env_kv_of "$name" exec)" | sed -n 's/.*--dsn \([^ ]*\).*/\1/p')"
[ -z "$dsn" ] && dsn="$(env_kv_of "$name" dsn)"
if [ -z "$dsn" ]; then
cf="$(env_kv_of "$name" conffile)"
[ -z "$cf" ] && cf="$(printf '%s' "$(env_kv_of "$name" exec)" | sed -n 's/.*--config \([^ ]*\).*/\1/p')"
@@ -2564,13 +2887,13 @@ mask_dsn() {
printf '%s' "$1" | sed -E 's#(postgres(ql)?://[^:/@[:space:]]+:)[^@[:space:]]+@#\1***@#g'
}
# mask_kv LINE:KEY=VALUE 行的敏感值掩码——password 键与键名含 KEY/PASS/SECRET/
# mask_kv LINE:KEY=VALUE 行的敏感值掩码——password/dsn 键与键名含 KEY/PASS/SECRET/
# TOKEN/DSN 的值整段掩为 ***;其余行只掩值内 DSN 密码(exec= 里可能带完整连接串)。
mask_kv() {
local line="$1" k="${1%%=*}"
if [ "$k" != "$1" ]; then
case "$k" in
password|*KEY*|*PASS*|*SECRET*|*TOKEN*|*DSN*)
password|dsn|*KEY*|*PASS*|*SECRET*|*TOKEN*|*DSN*)
printf '%s=***' "$k"; return 0 ;;
esac
fi
@@ -3066,7 +3389,7 @@ main() {
esac ;;
stop) do_stop ;;
start) do_start ;;
restart) do_stop; do_start ;;
restart) do_restart ;;
status) env_status_detail ;;
logs) [ -f "$ENV_LOG" ] && tail -n "${CLI_LOGS_N:-50}" -f "$ENV_LOG" || c_warn "[$ENV_NAME] 暂无日志" ;;
esac