Merge authoritative main into the task branch
Why: include the current integration baseline before verification. Evidence: fetched origin/main=b3b48f5efa2a6a25f16f6413d8b9c89715f22960. Impact: integrate upstream changes without rewriting task history. Refs: task-sync
This commit is contained in:
File diff suppressed because one or more lines are too long
+63
-17
@@ -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
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
"""svc.sh 启动预检与复用 DSN 解析的治理回归。
|
||||
|
||||
背景(2026-10-10 dev 实测,记忆 svc-g17-precheck-dsn-bug):G1-7 把 DSN 从
|
||||
命令行迁到 AGENT_SYSTEM_DSN 环境变量后——
|
||||
|
||||
1. 预检 agent_args_authed 只看参数串,「非回环 + auth auto + env DSN」被误判
|
||||
认证关而拒启;二进制 AuthEnabled(modules/backend/internal/config/config.go:
|
||||
显式 on,或非显式 off 且 DSN 非空——env 为合法 DSN 来源)会放行。
|
||||
2. 复用启动路径(无显式参数、复用 exec=)不再有 DSN:dbref 兜底只在重建分支,
|
||||
纯 start/restart 会静默回落 SQLite。
|
||||
3. DSN_ENV 为全局且每次启动不重置:update --all 同进程连续启动会跨服务泄漏 DSN。
|
||||
|
||||
测试通过 source scripts/svc.sh 调用纯函数/fixture 函数(文件尾有 source 守卫),
|
||||
不启动真实服务、不触碰真实 runtime/。
|
||||
"""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parents[1]
|
||||
SVC = REPO / "scripts" / "svc.sh"
|
||||
|
||||
PG_ADDR = "10.1.2.3:8899"
|
||||
PG_USER = "svc_test"
|
||||
PG_PASS = "fixture-only-pass"
|
||||
PG_DSN = f"postgresql://{PG_USER}:{PG_PASS}@{PG_ADDR}/agentdb"
|
||||
NONLOOP_EXEC = "--addr 0.0.0.0:8788 --data /tmp/x --auth auto"
|
||||
|
||||
|
||||
def _bash(script: str, *args: str) -> str:
|
||||
"""source svc.sh 后在 bash 内执行脚本,返回 stdout(stderr 丢弃)。"""
|
||||
proc = subprocess.run(
|
||||
["bash", "-c", script, "svc-test", str(SVC), *args],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
cwd=str(REPO),
|
||||
env={**os.environ, "LC_ALL": "C"},
|
||||
timeout=30,
|
||||
)
|
||||
if proc.returncode != 0:
|
||||
raise AssertionError(
|
||||
f"bash 脚本失败 rc={proc.returncode}\nstdout:\n{proc.stdout}\nstderr:\n{proc.stderr}"
|
||||
)
|
||||
return proc.stdout
|
||||
|
||||
|
||||
def _authed(args: str, dsn_env: str = "") -> bool:
|
||||
out = _bash(
|
||||
'source "$1"; shift; if agent_args_authed "$1" "${2:-}"; then echo authed; else echo denied; fi',
|
||||
args,
|
||||
dsn_env,
|
||||
)
|
||||
return out.strip() == "authed"
|
||||
|
||||
|
||||
def _rejected(exec_args: str, default_addr: str = "0.0.0.0:8788",
|
||||
insecure: str = "", dsn_env: str = "") -> bool:
|
||||
out = _bash(
|
||||
"source \"$1\"; shift; if agent_listen_rejected \"$1\" \"$2\" \"$3\" \"$4\"; "
|
||||
"then echo rejected; else echo allowed; fi",
|
||||
exec_args,
|
||||
default_addr,
|
||||
insecure,
|
||||
dsn_env,
|
||||
)
|
||||
return out.strip() == "rejected"
|
||||
|
||||
|
||||
class AgentArgsAuthedMatrix(unittest.TestCase):
|
||||
"""认证判定与二进制 AuthEnabled 口径一致(config.go:301,默认 auth=auto)。"""
|
||||
|
||||
def test_auth_on_explicit(self):
|
||||
self.assertTrue(_authed("--addr 1.2.3.4:80 --auth on"))
|
||||
self.assertTrue(_authed("--addr 1.2.3.4:80 --auth=on"))
|
||||
|
||||
def test_auth_off_beats_env_dsn(self):
|
||||
# 显式 off 优先:与二进制 AuthMode=="off" → AuthEnabled=false 一致。
|
||||
self.assertFalse(_authed("--auth off", PG_DSN))
|
||||
self.assertFalse(_authed("--auth=off", PG_DSN))
|
||||
|
||||
def test_auto_with_env_dsn_is_authed(self):
|
||||
# 2026-10-10 误拒主场景:dbref 重建/G1-7 迁移后 DSN 只在环境变量里。
|
||||
self.assertTrue(_authed("--auth auto", PG_DSN))
|
||||
|
||||
def test_default_auto_with_env_dsn_is_authed(self):
|
||||
# exec= 里连 --auth 都没有(config 默认 auto):env DSN 即认证开。
|
||||
self.assertTrue(_authed(NONLOOP_EXEC, PG_DSN))
|
||||
|
||||
def test_auto_without_dsn_is_denied(self):
|
||||
self.assertFalse(_authed("--auth auto"))
|
||||
self.assertFalse(_authed(NONLOOP_EXEC))
|
||||
|
||||
def test_legacy_cli_dsn_still_authed(self):
|
||||
# 向后兼容:历史 exec= 的命令行 --dsn 仍是合法 DSN 来源。
|
||||
self.assertTrue(_authed(f"--addr 1.2.3.4:80 --dsn {PG_DSN}"))
|
||||
|
||||
def test_no_auth_no_dsn_denied(self):
|
||||
self.assertFalse(_authed("--addr 127.0.0.1:80 --mock"))
|
||||
|
||||
|
||||
class AgentListenRejectedPrecheck(unittest.TestCase):
|
||||
"""预检整体判定:非回环 + 未放行 + 认证关才拒绝。"""
|
||||
|
||||
def test_incident_combo_not_rejected(self):
|
||||
# 原事故组合:非回环 + auto + DSN 仅在环境变量 → 必须放行(曾误拒)。
|
||||
self.assertFalse(_rejected(NONLOOP_EXEC, dsn_env=PG_DSN))
|
||||
|
||||
def test_nonloop_no_dsn_rejected(self):
|
||||
self.assertTrue(_rejected(NONLOOP_EXEC))
|
||||
|
||||
def test_nonloop_auth_off_with_env_dsn_rejected(self):
|
||||
self.assertTrue(_rejected("--addr 0.0.0.0:8788 --auth off", dsn_env=PG_DSN))
|
||||
|
||||
def test_loopback_not_rejected(self):
|
||||
self.assertFalse(_rejected("--addr 127.0.0.1:8788"))
|
||||
|
||||
def test_insecure_not_rejected(self):
|
||||
self.assertFalse(_rejected(NONLOOP_EXEC, insecure="1"))
|
||||
|
||||
def test_addr_defaults_to_default_addr(self):
|
||||
# exec 参数里没有 --addr 时按 DEFAULT_ADDR 判。
|
||||
self.assertTrue(_rejected("--data /tmp/x", default_addr="0.0.0.0:8788"))
|
||||
self.assertFalse(_rejected("--data /tmp/x", default_addr="127.0.0.1:8788"))
|
||||
|
||||
|
||||
class AgentReuseExec(unittest.TestCase):
|
||||
"""复用启动的 DSN 解析:G1-7 迁移 + dbref 重建(防静默回落 SQLite)。"""
|
||||
|
||||
def setUp(self):
|
||||
self._tmp = tempfile.TemporaryDirectory()
|
||||
rt = Path(self._tmp.name) / "runtime"
|
||||
(rt / "pg1").mkdir(parents=True)
|
||||
(rt / "pg1" / "svc.env").write_text(
|
||||
f"kind=postgres\naddr={PG_ADDR}\nuser={PG_USER}\npassword={PG_PASS}\n"
|
||||
)
|
||||
(rt / "dev").mkdir()
|
||||
self.rt = rt
|
||||
self.dev_dir = rt / "dev"
|
||||
|
||||
def tearDown(self):
|
||||
self._tmp.cleanup()
|
||||
|
||||
def _reuse(self, exec_line: str) -> tuple:
|
||||
# 直接调用(非 $()):agent_reuse_exec 经 nameref 回写 exec 参数、
|
||||
# 全局 DSN_ENV 传 DSN——命令替换会把 DSN_ENV 赋值留在子壳。
|
||||
out = _bash(
|
||||
'source "$1"; shift; RUNTIME="$1"; ENV_NAME=dev; ENV_DIR="$2"; DSN_ENV=""; '
|
||||
'agent_reuse_exec "$3" exec_args; '
|
||||
'printf "EXEC=%s\\n" "$exec_args"; printf "DSN=%s\\n" "${DSN_ENV:-}"',
|
||||
str(self.rt),
|
||||
str(self.dev_dir),
|
||||
exec_line,
|
||||
)
|
||||
lines = dict(part.split("=", 1) for part in out.strip().splitlines())
|
||||
return lines["EXEC"], lines["DSN"]
|
||||
|
||||
def _write_dev_env(self, exec_line: str, dbref: str = "") -> None:
|
||||
content = f"kind=agent\nenv=dev\nexec={exec_line}\n"
|
||||
if dbref:
|
||||
content += f"dbref={dbref}\n"
|
||||
(self.dev_dir / "svc.env").write_text(content)
|
||||
|
||||
def test_post_g17_exec_restores_dsn_from_dbref(self):
|
||||
# G1-7 后 exec= 不含 DSN:复用启动必须从 dbref= 重建(否则回落 SQLite)。
|
||||
self._write_dev_env("--addr 0.0.0.0:8788 --auth auto", dbref="pg1/agentdb")
|
||||
exec_args, dsn = self._reuse("--addr 0.0.0.0:8788 --auth auto")
|
||||
self.assertEqual(dsn, PG_DSN)
|
||||
self.assertNotIn("--dsn", exec_args)
|
||||
|
||||
def test_legacy_exec_dsn_migrates_to_env(self):
|
||||
# 存量 exec= 的明文 --dsn:剥到环境变量,命令行不再可见。
|
||||
legacy = f"--addr 0.0.0.0:8788 --dsn {PG_DSN}"
|
||||
self._write_dev_env(legacy, dbref="pg1/agentdb")
|
||||
exec_args, dsn = self._reuse(legacy)
|
||||
self.assertEqual(dsn, PG_DSN)
|
||||
self.assertNotIn("--dsn", exec_args)
|
||||
self.assertEqual(exec_args, "--addr 0.0.0.0:8788")
|
||||
|
||||
def test_migration_dsn_wins_over_dbref(self):
|
||||
# 命令行 --dsn 与 dbref 并存:迁移值优先,dbref 不覆盖。
|
||||
other = "postgresql://other:pw@10.9.9.9:5432/otherdb"
|
||||
legacy = f"--addr 0.0.0.0:8788 --dsn {other}"
|
||||
self._write_dev_env(legacy, dbref="pg1/agentdb")
|
||||
_, dsn = self._reuse(legacy)
|
||||
self.assertEqual(dsn, other)
|
||||
|
||||
def test_no_dsn_no_dbref_stays_empty(self):
|
||||
self._write_dev_env("--addr 127.0.0.1:8788")
|
||||
exec_args, dsn = self._reuse("--addr 127.0.0.1:8788")
|
||||
self.assertEqual(dsn, "")
|
||||
self.assertEqual(exec_args, "--addr 127.0.0.1:8788")
|
||||
|
||||
def test_broken_dbref_warns_and_starts_unbound(self):
|
||||
# 实例登记不全:不伪造 DSN,保持为空(启动侧另有告警)。
|
||||
self._write_dev_env("--addr 127.0.0.1:8788", dbref="missing/agentdb")
|
||||
_, dsn = self._reuse("--addr 127.0.0.1:8788")
|
||||
self.assertEqual(dsn, "")
|
||||
|
||||
|
||||
class DsnEnvScopeIsolation(unittest.TestCase):
|
||||
"""DSN_ENV 每次启动隔离(agent_do_start 内 local 化,防 update --all 串库)。"""
|
||||
|
||||
def test_agent_do_start_localizes_dsn_env(self):
|
||||
# 棘轮:agent_do_start 必须以 local DSN_ENV="" 起始,删除即回归
|
||||
# 「同进程连续启动跨服务泄漏 AGENT_SYSTEM_DSN」。
|
||||
out = _bash(
|
||||
'source "$1"; declare -f agent_do_start | grep -c \'local exec_args DSN_ENV=""\''
|
||||
)
|
||||
self.assertEqual(out.strip(), "1")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user