Harden svc.sh identity isolation and deploy candidate gate (U1 slice) #474
File diff suppressed because one or more lines are too long
+106
-12
@@ -1055,6 +1055,12 @@ svc_manifest_migrate() {
|
||||
out="$(python3 "$MANIFEST_PY" "$ENV_DIR" migrate 2>&1)" || rc=$?
|
||||
if [ "$rc" -eq 0 ]; then
|
||||
c_ok "[$ENV_NAME] manifest:$(printf '%s' "$out" | tail -1)"
|
||||
# 领养即受控建身份:DB marker 只在领养(此处)与显式 init 建立;普通启动只
|
||||
# 验证不补行——缺行即事件(2026-10-12 裁定,rdmcand 实况:每次启动静默
|
||||
# ensure 会掩盖身份丢失)。领养时旧产物可能无 identity 能力而跳过补行,由
|
||||
# 部署候选门禁 svc_deploy_identity_gate 在下次换产物前用候选补齐。
|
||||
resolve_db_config 2>/dev/null || true
|
||||
svc_db_identity_ensure || return 1
|
||||
return 0
|
||||
fi
|
||||
if [ "$rc" -eq 4 ]; then
|
||||
@@ -1068,7 +1074,9 @@ svc_manifest_migrate() {
|
||||
}
|
||||
# svc_manifest_verify:启动前身份校验——数据 marker 存在且 instance_id 匹配;
|
||||
# 生效 --data / 数据库绑定与冻结锚点一致。失败即拒绝启动(KTD1/KTD2,I4)。
|
||||
# 顺序:文件 marker → DB ensure(空表幂等补)→ DB verify(缺/错即拦)。
|
||||
# 顺序:文件 marker → DB verify(缺/错即拦)。ensure 只在领养(svc_manifest_migrate)
|
||||
# 与显式 init——普通启动不建身份,缺行即事件(修复路径:svc init,或换具备
|
||||
# identity 能力的产物更新时由候选门禁补齐)。
|
||||
svc_manifest_verify() {
|
||||
manifest_ok || return 0
|
||||
local out
|
||||
@@ -1078,8 +1086,7 @@ svc_manifest_verify() {
|
||||
c_err " 数据根/数据库绑定是冻结身份;换根/重绑须显式迁移或登记命令(docs/recovery/u1-identity-manifest.md §5)"
|
||||
return 1
|
||||
fi
|
||||
# U1 I3:文件校验通过后再碰库——避免损坏 manifest 时 ensure 先 traceback。
|
||||
svc_db_identity_ensure || return 1
|
||||
# U1 I3:文件校验通过后再碰库——避免损坏 manifest 时身份 CLI 先 traceback。
|
||||
svc_db_identity_verify || return 1
|
||||
return 0
|
||||
}
|
||||
@@ -1105,20 +1112,33 @@ print("%s\t%s\t%s" % (m["instance_id"], dbref, data))
|
||||
PY
|
||||
}
|
||||
|
||||
# svc_bin_has_identity [BIN]:产物是否具备 identity 管理子命令(ensure/verify)。
|
||||
# 探测必须无副作用、有界:timeout 护栏(未知子命令直落服务态的产物会挂死或占口);
|
||||
# serve 痕迹与身份门禁报错文本不算能力证据——2026-10-12 rdm1/rdmcand 事故中 RDM
|
||||
# 产物对 identity 未知位置参数落入服务启动路径,报错恰好含「identity ensure」字样,
|
||||
# 旧实现 grep ensure 误报为有能力。仅 usage 形态(同时含 identity 与 ensure、
|
||||
# 无服务态痕迹、未超时)才认可。
|
||||
svc_bin_has_identity() {
|
||||
[ -x "${ENV_BIN:-}" ] || return 1
|
||||
local out
|
||||
out="$("$ENV_BIN" identity 2>&1 || true)"
|
||||
printf '%s' "$out" | grep -q ensure
|
||||
local bin="${1:-${ENV_BIN:-}}"
|
||||
[ -x "$bin" ] || return 1
|
||||
local out rc=0
|
||||
out="$(timeout 5 "$bin" identity 2>&1)" || rc=$?
|
||||
[ "$rc" -eq 124 ] && return 1
|
||||
case "$out" in
|
||||
*serving*|*"agent-system starting"*|*"identity missing"*|*"listen"*) return 1 ;;
|
||||
esac
|
||||
printf '%s' "$out" | grep -q identity && printf '%s' "$out" | grep -q ensure
|
||||
}
|
||||
|
||||
svc_db_identity_ensure() {
|
||||
# 显式建立 / I2→I3 引导:表空插入;一致幂等;不一致 fail-closed。
|
||||
# 显式建立 / 领养补行(I2→I3 引导):表空插入;一致幂等;不一致 fail-closed。
|
||||
# 非 agent 或无 identity 子命令的旧产物:跳过(不硬断 I2)。
|
||||
# 可选参数 BIN:用指定产物(如部署候选)执行——默认当前安装产物。
|
||||
local bin="${1:-${ENV_BIN:-}}"
|
||||
manifest_ok || return 0
|
||||
local kind; kind="$(svc_get kind)"
|
||||
[ "$kind" = agent ] || return 0
|
||||
svc_bin_has_identity || return 0
|
||||
svc_bin_has_identity "$bin" || return 0
|
||||
local line iid dbref data
|
||||
line="$(svc_db_identity_args)" || return 1
|
||||
IFS=$'\t' read -r iid dbref data <<<"$line"
|
||||
@@ -1127,7 +1147,7 @@ svc_db_identity_ensure() {
|
||||
args+=(--dsn "$DB_DSN")
|
||||
fi
|
||||
local out rc=0
|
||||
out="$("$ENV_BIN" "${args[@]}" 2>&1)" || rc=$?
|
||||
out="$("$bin" "${args[@]}" 2>&1)" || rc=$?
|
||||
if [ "$rc" -ne 0 ]; then
|
||||
c_err "[$ENV_NAME] DB 身份 ensure 失败:$out"
|
||||
return 1
|
||||
@@ -1161,6 +1181,35 @@ svc_db_identity_verify() {
|
||||
return 0
|
||||
}
|
||||
|
||||
# svc_deploy_identity_gate SRCBIN:部署候选的身份门禁——在停旧服务、覆盖制品之前
|
||||
# 执行(2026-10-12 rdm1/rdmcand 事故:停服→覆盖→启动即被身份门禁拒绝,只剩
|
||||
# 「登记版本≠磁盘制品」的混合状态且无恢复路径)。规则:
|
||||
# 1) 未入 manifest 治理(全新实例,无 manifest.json):放行——首次部署无旧状态可保;
|
||||
# 2) 非 agent 类型:放行;
|
||||
# 3) 候选产物无 identity 管理能力:拒绝——装上即启动被基座身份门禁拒绝,
|
||||
# 登记与旧制品/旧进程保持不变(下游组合仓接齐 identity 接口前不可更新);
|
||||
# 4) 候选有能力:先以候选幂等补齐 DB 身份行(领养时旧产物可能无能力而未补;
|
||||
# 外宾行/不一致 fail-closed),成功才放行——把「缺行」在停服前治好,
|
||||
# 避免换完制品才发现 start 起不来。
|
||||
svc_deploy_identity_gate() {
|
||||
local srcbin="$1"
|
||||
manifest_ok || return 0
|
||||
local kind; kind="$(svc_get kind)"
|
||||
[ "$kind" = agent ] || return 0
|
||||
if ! svc_bin_has_identity "$srcbin"; then
|
||||
c_err "[$ENV_NAME] 候选产物不支持 identity 管理:$srcbin"
|
||||
c_err " manifest 治理实例拒绝安装(装上即被启动身份校验拒绝);旧服务/旧制品/登记保持不变"
|
||||
c_err " 处理:换具备 identity ensure/verify 的版本部署,或 scripts/svc.sh rm $ENV_NAME --purge 后放弃治理重新部署"
|
||||
return 1
|
||||
fi
|
||||
resolve_db_config 2>/dev/null || true
|
||||
svc_db_identity_ensure "$srcbin" || {
|
||||
c_err "[$ENV_NAME] 候选补齐 DB 身份失败(外宾行/不一致 fail-closed);旧服务/旧制品/登记保持不变"
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
# svc_mark_autostart / svc_clear_autostart:期望运行态登记(成功 start 置 running,
|
||||
# 显式 stop 置 stopped;进程自行死亡不动——recover 据 svc.env 派生的 autostart 判断)。
|
||||
svc_mark_autostart() { manifest_ok && python3 "$MANIFEST_PY" "$ENV_DIR" set-desired running >/dev/null 2>&1 && return 0; svc_set_key autostart 1 >/dev/null 2>&1 || true; }
|
||||
@@ -1212,6 +1261,12 @@ env_config() {
|
||||
# update --all:dev 成功后 genui/rdm1/rdmcand 端口仍被孤儿占用)。
|
||||
SVC_DB_MODE=""; SVC_DB_SOURCE=""; SVC_DB_REF=""; SVC_DB_DSN_SAVE=""
|
||||
SVC_PIDCHECK=""; SVC_EXEC=""; SVC_VERSION=""; SVC_SOURCE=""
|
||||
# U1:身份环境同样按实例重置——上一服务 verify 成功后导出的
|
||||
# AGENT_SYSTEM_INSTANCE_ID/DBREF 不得串给下一服务(批量 update 单进程逐服务解析;
|
||||
# 2026-10-12 实况:dev 的身份漏给 rdm1/rdmcand,叠加 RDM 产物无 identity 子命令
|
||||
# 跳过校验,serve 带外宾身份即被基座门禁拒绝)。serve 只允许拿到本服务 verify
|
||||
# 写入的身份。
|
||||
unset AGENT_SYSTEM_INSTANCE_ID AGENT_SYSTEM_DBREF
|
||||
local registered=""
|
||||
[ -f "$ENV_DIR/svc.env" ] && registered="$(svc_kind "$name")"
|
||||
SVC_KIND="${registered:-agent}"
|
||||
@@ -1813,23 +1868,44 @@ agent_do_deploy() {
|
||||
return 1
|
||||
fi
|
||||
hist_event deploy begin "$(read_pid)" "from=$SRC_LABEL"
|
||||
do_stop_quiet
|
||||
# U1/U2(构建前置):先构建/物化候选并过身份门禁,再停旧服务——构建失败或候选
|
||||
# 不合格时旧服务继续运行、旧制品与登记不动(2026-10-12 rdm1/rdmcand 事故:停服
|
||||
# →覆盖→启动即拒,只留「登记版本≠磁盘制品」的混合状态且无恢复路径)。
|
||||
local wt RDM_UI_OK=0
|
||||
case "$SRC_KINDSRC" in
|
||||
local)
|
||||
c_info "[$ENV_NAME] 构建当前工作区(make build)——唯一写主检出构建产物的路径"
|
||||
make -C "$ROOT" build || { hist_event deploy fail "$(read_pid)" "构建失败($SRC_LABEL)"; return 1; }
|
||||
svc_deploy_identity_gate "$BIN" \
|
||||
|| { hist_event deploy fail "$(read_pid)" "候选身份门禁拒绝($SRC_LABEL)"; return 1; }
|
||||
do_stop_quiet
|
||||
install_artifacts "$BIN" "$ROOT/plugins" \
|
||||
|| { hist_event deploy fail "$(read_pid)" "产物复制失败"; return 1; }
|
||||
;;
|
||||
dev)
|
||||
wt="$(materialize_dev_commit "$SRC_FULL" "$SRC_SHA" build)" \
|
||||
|| { hist_event deploy fail "$(read_pid)" "物化/构建失败($SRC_LABEL)"; return 1; }
|
||||
if ! svc_deploy_identity_gate "$wt/bin/agent-system"; then
|
||||
hist_event deploy fail "$(read_pid)" "候选身份门禁拒绝($SRC_LABEL)"
|
||||
recycle_worktree "$SRC_SHA"
|
||||
return 1
|
||||
fi
|
||||
do_stop_quiet
|
||||
install_artifacts "$wt/bin/agent-system" "$wt/plugins" \
|
||||
|| { hist_event deploy fail "$(read_pid)" "产物复制失败"; return 1; }
|
||||
recycle_worktree "$SRC_SHA"
|
||||
;;
|
||||
release)
|
||||
local relbin="$REL_ROOT/build/$REL_VERSION/agent-system"
|
||||
if [ ! -f "$relbin" ]; then
|
||||
c_err "[$ENV_NAME] Releases 仓无制品:$relbin"
|
||||
c_err "先在 Releases 仓构建:scripts/build-release.sh $REL_VERSION(或换版本:scripts/svc.sh commits release)"
|
||||
hist_event deploy fail "$(read_pid)" "制品缺失($SRC_LABEL)"
|
||||
return 1
|
||||
fi
|
||||
svc_deploy_identity_gate "$relbin" \
|
||||
|| { hist_event deploy fail "$(read_pid)" "候选身份门禁拒绝($SRC_LABEL)"; return 1; }
|
||||
do_stop_quiet
|
||||
materialize_release || { hist_event deploy fail "$(read_pid)" "物化/校验失败($SRC_LABEL)"; return 1; }
|
||||
;;
|
||||
rdm)
|
||||
@@ -1837,6 +1913,12 @@ agent_do_deploy() {
|
||||
|| { hist_event deploy fail "$(read_pid)" "物化/构建失败($SRC_LABEL)"; return 1; }
|
||||
# RDM 组合插件编译进二进制(provides 无声明式资产)——插件目录按空集安装。
|
||||
mkdir -p "$wt/.svc-empty-plugins"
|
||||
if ! svc_deploy_identity_gate "$wt/bin/agent-system-rdm"; then
|
||||
hist_event deploy fail "$(read_pid)" "候选身份门禁拒绝($SRC_LABEL)"
|
||||
recycle_worktree "rdm-$SRC_SHA" "$RDM_SRC_DIR"
|
||||
return 1
|
||||
fi
|
||||
do_stop_quiet
|
||||
install_artifacts "$wt/bin/agent-system-rdm" "$wt/.svc-empty-plugins" \
|
||||
|| { hist_event deploy fail "$(read_pid)" "产物复制失败"; return 1; }
|
||||
# RDM 自带零构建工作台(web/)落 htdocs/;收尾自动部署伴生 ${name}web。
|
||||
@@ -2326,7 +2408,8 @@ 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")
|
||||
# U1 身份校验:文件 marker + DB marker(ensure 幂等补空表;不一致 fail-closed)。
|
||||
# U1 身份校验:文件 marker + DB verify(不一致 fail-closed;ensure 只在领养与
|
||||
# 显式 init,普通启动不补行——缺行即事件,见 svc_manifest_verify)。
|
||||
svc_manifest_verify || { hist_event start fail "" "manifest 身份校验失败"; return 1; }
|
||||
# 预检(快速拦截):非回环地址且认证判定为关时提前拒绝,报错可操作——
|
||||
# 不等二进制 listen guard 以"进程即退"暴露;二进制守卫仍是权威。
|
||||
@@ -4311,6 +4394,17 @@ cmd_update() {
|
||||
hist_event update begin "$(read_pid)" "from=$from to=$tov"
|
||||
CLI_FROM="$deploy_from"
|
||||
svc_lock wait
|
||||
# U1:批量更新与单实例生命周期同入口——锁内 manifest 迁移(领养/身份治理;
|
||||
# 2026-10-12 rdm1 实况:update 直入 agent_do_deploy 绕过迁移,多次更新后仍无
|
||||
# manifest)。归属冲突按该服务更新失败计,其余服务继续。
|
||||
if ! svc_manifest_migrate; then
|
||||
hist_event update fail "$(read_pid)" "from=$from to=$tov(manifest 迁移中止:身份治理冲突)"
|
||||
c_err "[$name] 更新失败(manifest 迁移中止),其余服务继续"
|
||||
svc_unlock
|
||||
CLI_FROM=""
|
||||
failed=$((failed + 1))
|
||||
continue
|
||||
fi
|
||||
if agent_do_deploy; then
|
||||
hist_event update ok "$(read_pid)" "from=$from to=$tov"
|
||||
c_ok "[$name] 已更新:$from → $tov"
|
||||
|
||||
@@ -210,6 +210,213 @@ class IdentityTests(tsr.SvcAutoRecoverTests):
|
||||
con.close()
|
||||
return rows
|
||||
|
||||
# ---- U1 批量隔离、候选门禁与启动语义(2026-10-12 rdm1/rdmcand 事故回归) ----
|
||||
|
||||
def noid_stub(self):
|
||||
"""自包含「无 identity」桩:复用 recover STUB,把 identity 分支换成 2026-10-12
|
||||
RDM 组合产物形态——identity 子命令落入服务路径、被身份门禁拒绝,报错文本
|
||||
恰含「identity ensure」字样(旧探测 grep ensure 的误报源)。不 exec 转发:
|
||||
serve 进程 cmdline 必须保留实例 bin 路径(pidcheck 契约与真实产物一致)。"""
|
||||
p = self.root / "stub-agent-system-noid"
|
||||
if not p.exists():
|
||||
stub = tsr.STUB
|
||||
head, sep, tail = stub.partition(" identity)")
|
||||
assert sep, "STUB 缺 identity 分支锚点"
|
||||
_, sep2, tail2 = tail.partition(" exit $? ;;")
|
||||
assert sep2, "STUB 缺 identity 分支结尾锚点"
|
||||
noid_case = (
|
||||
" identity)\n"
|
||||
" echo \"system identity: system identity missing: agent_system_identity 无行"
|
||||
"(先 svc init / identity ensure,禁止静默建空身份)\" >&2\n"
|
||||
" exit 1 ;;")
|
||||
p.write_text(head + noid_case + tail2)
|
||||
p.chmod(0o755)
|
||||
return p
|
||||
|
||||
def install_noid_bin(self, name):
|
||||
"""把实例磁盘产物换成无 identity 形态(rdm1/rdmcand 事故现场的制品状态)。"""
|
||||
shutil.copy2(self.noid_stub(), self.runtime(name) / "bin" / "agent-system")
|
||||
|
||||
def set_kv(self, name, key, value):
|
||||
f = self.runtime(name) / "svc.env"
|
||||
lines = f.read_text().splitlines()
|
||||
out, hit = [], False
|
||||
for l in lines:
|
||||
if l.startswith(key + "="):
|
||||
out.append("%s=%s" % (key, value)); hit = True
|
||||
else:
|
||||
out.append(l)
|
||||
if not hit:
|
||||
out.append("%s=%s" % (key, value))
|
||||
f.write_text("\n".join(out) + "\n")
|
||||
|
||||
def serve_record(self):
|
||||
f = self.trace / "serve.txt"
|
||||
return f.read_text() if f.exists() else ""
|
||||
|
||||
def clear_serve_record(self):
|
||||
(self.trace / "serve.txt").unlink(missing_ok=True)
|
||||
|
||||
def git_ok(self, cwd, *args):
|
||||
proc = subprocess.run(["git", *args], cwd=cwd, env=self.env, text=True,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=60)
|
||||
self.assertEqual(proc.returncode, 0, proc.stdout)
|
||||
|
||||
def git_sha(self, cwd, rev):
|
||||
proc = subprocess.run(["git", "rev-parse", "--short", rev], cwd=cwd,
|
||||
env=self.env, text=True,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, timeout=30)
|
||||
return proc.stdout.strip()
|
||||
|
||||
def _make_track_repo(self, artifact, target, with_web=False):
|
||||
"""双提交源仓夹具(旧/新两版):Makefile 产物 = artifact 复制为 target。"""
|
||||
remote, work = self.root / "trackremote.git", self.root / "trackwork"
|
||||
for p in (remote, work):
|
||||
if p.exists():
|
||||
shutil.rmtree(p)
|
||||
self.git_ok(self.root, "init", "--bare", "--initial-branch=main", str(remote))
|
||||
self.git_ok(self.root, "init", "--initial-branch=main", str(work))
|
||||
(work / "Makefile").write_text(
|
||||
"build:\n\tmkdir -p bin\n\tcp %s bin/%s\n" % (artifact, target))
|
||||
if with_web:
|
||||
(work / "web").mkdir()
|
||||
(work / "web" / "index.html").write_text("<html>rdm</html>\n")
|
||||
self.git_ok(work, "add", ".")
|
||||
self.git_ok(work, "commit", "-m", "track v1")
|
||||
self.git_ok(work, "remote", "add", "origin", str(remote))
|
||||
self.git_ok(work, "push", "-u", "origin", "main")
|
||||
(work / "v2.marker").write_text("v2\n")
|
||||
self.git_ok(work, "add", ".")
|
||||
self.git_ok(work, "commit", "-m", "track v2")
|
||||
self.git_ok(work, "push")
|
||||
return remote, self.git_sha(work, "HEAD~1"), self.git_sha(work, "HEAD")
|
||||
|
||||
def make_src_repo(self):
|
||||
"""dev 轨道:产物 = 可用 identity 桩;预克隆进 SVC_SRC_DIR(srccache)。"""
|
||||
remote, old, new = self._make_track_repo(self.stub, "agent-system")
|
||||
cache = self.root / "srccache"
|
||||
if cache.exists():
|
||||
shutil.rmtree(cache)
|
||||
self.git_ok(self.root, "clone", str(remote), str(cache))
|
||||
return old, new
|
||||
|
||||
def make_rdm_repo(self, artifact):
|
||||
"""rdm 轨道:产物 = 指定桩(可注入无 identity 形态)。"""
|
||||
return self._make_track_repo(artifact, "agent-system-rdm", with_web=True)
|
||||
|
||||
def rdm_env(self, remote):
|
||||
env = dict(self.env)
|
||||
env.update({"SVC_RDM_SRC_DIR": str(self.root / "rdmcache"),
|
||||
"SVC_RDM_ORIGIN": str(remote)})
|
||||
return env
|
||||
|
||||
def test_U1_update_refuses_identityless_candidate_keeps_old_state(self):
|
||||
"""manifest 治理实例 × 无 identity 候选(RDM 旧形态):停服前拒绝——
|
||||
旧服务继续运行、旧制品与登记不变(2026-10-12 rdm1/rdmcand 反例:停服→
|
||||
覆盖→启动即拒,只剩混合状态)。"""
|
||||
remote, old_sha, new_sha = self.make_rdm_repo(self.noid_stub())
|
||||
self.seed_agent("rdx", 9451)
|
||||
self.start_ok("rdx") # 领养:manifest + DB 行
|
||||
self.svc("rdx", "stop")
|
||||
self.install_noid_bin("rdx") # 磁盘产物退化为无 identity(事故现场)
|
||||
self.set_kv("rdx", "source", "rdm@%s" % old_sha)
|
||||
self.start_ok("rdx") # 校验跳过,照常运行
|
||||
pid = self.pid_of("rdx")
|
||||
bin_before = (self.runtime("rdx") / "bin" / "agent-system").read_bytes()
|
||||
|
||||
result = self.svc("update", "rdx", check=False, env=self.rdm_env(remote))
|
||||
self.assertNotEqual(result.returncode, 0, result.stdout)
|
||||
self.assertIn("候选产物不支持 identity 管理", result.stdout)
|
||||
# 拒绝发生在停旧服务之前:进程、制品、登记全部保持
|
||||
self.assertTrue(self.running("rdx"), "拒绝不得停掉可用旧服务")
|
||||
self.assertEqual(self.pid_of("rdx"), pid)
|
||||
self.assertEqual((self.runtime("rdx") / "bin" / "agent-system").read_bytes(),
|
||||
bin_before, "旧制品不得被覆盖")
|
||||
self.assertEqual(self.kv("rdx", "source"), "rdm@%s" % old_sha)
|
||||
hist = (self.runtime("rdx") / "history.jsonl").read_text()
|
||||
self.assertIn('"event":"update"', hist)
|
||||
self.assertIn('"result":"fail"', hist)
|
||||
self.svc("rdx", "stop")
|
||||
|
||||
def test_U1_update_ensures_missing_row_before_stop(self):
|
||||
"""领养时未能补行的实例(旧产物无 identity):候选有能力时,update 在
|
||||
停服前用候选补齐 DB 身份行——完整更新成功;serve 拿到的是本服务自己的
|
||||
身份(非批内残留)。"""
|
||||
old_sha, new_sha = self.make_src_repo()
|
||||
self.seed_agent("rowx", 9452)
|
||||
self.start_ok("rowx")
|
||||
self.svc("rowx", "stop")
|
||||
self.install_noid_bin("rowx")
|
||||
self.set_kv("rowx", "source", "dev@%s" % old_sha)
|
||||
self.start_ok("rowx")
|
||||
# 模拟领养时未补行(rdm1 现状):清空 DB 身份行
|
||||
db = Path(self.manifest("rowx")["identity"]["data_root"]) / "agent-system.db"
|
||||
con = sqlite3.connect(str(db))
|
||||
con.execute("DELETE FROM agent_system_identity")
|
||||
con.commit()
|
||||
con.close()
|
||||
|
||||
self.clear_serve_record()
|
||||
result = self.svc("update", "rowx")
|
||||
self.assertIn("已更新", result.stdout)
|
||||
self.assertEqual(self.kv("rowx", "source"), "dev@%s" % new_sha)
|
||||
iid = self.manifest("rowx")["instance_id"]
|
||||
self.assertEqual(self.db_identity_row("rowx"), [(iid, "sqlite")],
|
||||
"候选应在停服前补齐身份行")
|
||||
self.assertTrue(self.running("rowx"))
|
||||
self.assertIn("AGENT_SYSTEM_INSTANCE_ID=%s" % iid, self.serve_record(),
|
||||
"serve 只允许拿到本服务自己的身份")
|
||||
self.svc("rowx", "stop")
|
||||
|
||||
def test_U1_bare_start_missing_row_is_event_init_recovers(self):
|
||||
"""普通启动只验证不补行:DB 行缺失 → 拒绝且不建空行(缺行即事件);
|
||||
显式 init 补齐后恢复。"""
|
||||
self.seed_agent("rowm", 9453)
|
||||
self.start_ok("rowm")
|
||||
iid = self.manifest("rowm")["instance_id"]
|
||||
self.svc("rowm", "stop")
|
||||
db = Path(self.manifest("rowm")["identity"]["data_root"]) / "agent-system.db"
|
||||
con = sqlite3.connect(str(db))
|
||||
con.execute("DELETE FROM agent_system_identity")
|
||||
con.commit()
|
||||
con.close()
|
||||
|
||||
result = self.svc("rowm", "start", check=False)
|
||||
self.assertNotEqual(result.returncode, 0, result.stdout)
|
||||
self.assertIn("DB 身份校验失败", result.stdout)
|
||||
self.assertFalse(self.running("rowm"))
|
||||
self.assertEqual(self.db_identity_row("rowm"), [], "缺行不得被 start 静默补建")
|
||||
|
||||
self.svc("rowm", "init") # 修复路径:显式 ensure
|
||||
self.assertEqual(self.db_identity_row("rowm"), [(iid, "sqlite")])
|
||||
self.start_ok("rowm")
|
||||
self.svc("rowm", "stop")
|
||||
|
||||
def test_U1_capability_probe_no_false_positive_no_hang(self):
|
||||
"""能力探测无副作用:含 ensure 字样的服务态门禁报错不算能力(旧实现
|
||||
误报点);identity 即挂死的产物探测有界返回无能力。"""
|
||||
def probe(binpath):
|
||||
# svc.sh 顶层启用 errexit:source 后裸调用失败函数会杀掉探针 shell,
|
||||
# 用 if 条件上下文(errexit 豁免)取回真实返回码。
|
||||
return subprocess.run(
|
||||
["bash", "-c",
|
||||
'source "%s" >/dev/null 2>&1; ENV_BIN="%s"; '
|
||||
'if svc_bin_has_identity; then echo rc=0; else echo rc=$?; fi'
|
||||
% (self.root / "scripts" / "svc.sh", binpath)],
|
||||
cwd=self.root, env=self.env, text=True,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
||||
timeout=30).stdout.strip()
|
||||
|
||||
self.assertIn("rc=0", probe(self.stub), "identity 桩应判有能力")
|
||||
self.assertIn("rc=1", probe(self.noid_stub()),
|
||||
"报错文本含 ensure 不得误报为有能力")
|
||||
hang = self.root / "stub-hang"
|
||||
hang.write_text(
|
||||
'#!/usr/bin/env bash\ncase "${1:-}" in identity) sleep 30 ;; '
|
||||
'*) exec "%s" "$@" ;; esac\n' % self.stub)
|
||||
hang.chmod(0o755)
|
||||
self.assertIn("rc=1", probe(hang), "探测挂死须有界返回无能力")
|
||||
|
||||
def test_I3_start_ensures_sqlite_identity(self):
|
||||
self.seed_agent("idb1", 9441)
|
||||
self.start_ok("idb1")
|
||||
|
||||
@@ -236,6 +236,16 @@ class ResolverStateIsolation(unittest.TestCase):
|
||||
)
|
||||
self.assertEqual(out.strip(), "1")
|
||||
|
||||
def test_env_config_resets_identity_env(self):
|
||||
# 棘轮:env_config 必须重置 AGENT_SYSTEM_INSTANCE_ID/DBREF(删除即回归
|
||||
# 「批量 update 前一服务 verify 导出的身份串给下一服务」——2026-10-12
|
||||
# rdm1/rdmcand:dev 身份漏给 RDM,serve 带外宾身份被门禁拒绝)。
|
||||
out = _bash(
|
||||
'source "$1"; declare -f env_config | '
|
||||
'grep -c \'unset AGENT_SYSTEM_INSTANCE_ID AGENT_SYSTEM_DBREF\''
|
||||
)
|
||||
self.assertEqual(out.strip(), "1")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -111,6 +111,11 @@ PYID
|
||||
exit 0 ;;
|
||||
esac
|
||||
printf 'args: %s\n' "$*" > "$trace_dir/serve.txt"
|
||||
# 身份环境记录(U1 批量隔离回归观测点):serve 实际拿到谁的实例身份。
|
||||
if [ -n "${AGENT_SYSTEM_INSTANCE_ID:-}" ]; then
|
||||
printf 'AGENT_SYSTEM_INSTANCE_ID=%s\nAGENT_SYSTEM_DBREF=%s\n' \
|
||||
"$AGENT_SYSTEM_INSTANCE_ID" "${AGENT_SYSTEM_DBREF:-sqlite}" >> "$trace_dir/serve.txt"
|
||||
fi
|
||||
addr=""; data="./data"
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in --addr) addr="$2"; shift 2 ;; --data) data="$2"; shift 2 ;; *) shift ;; esac
|
||||
|
||||
Reference in New Issue
Block a user