Files
Agent-System-Dev/tests/test_svc_superadmin_envfile.py
agent-exec-transportandCursor 1edf326448 Remember the super-admin credential file path in svc
Auth-on agent services need super-admin credentials at every launch, but
svc only forwarded them from the current environment, and recover/watchdog
relaunch with a cleared environment, so a bare restart or recovery failed
closed. svc now records only the absolute path of AGENT_SYSTEM_ENV_FILE as
superadmin_envfile= after checking it is a regular, owner-only (0600) file
before stopping anything, and forwards that path when no explicit
credentials are set. An empty AGENT_SYSTEM_ENV_FILE clears the record. The
file content is never read, copied or printed.

Why: prod restarts and recovery required re-exporting AGENT_SYSTEM_ENV_FILE.
Evidence: agent-exec unit dev-svc-superadmin-envfile-persist (cursor, attempt 1, terminal=success); unittest test_svc_superadmin_envfile + test_svc_recover + test_svc_identity: 51 OK (1 skipped).
Impact: svc.sh agent launch and svc.env gain superadmin_envfile; docs/svc.md 4.2 note.
Refs: Dev task fix/svc-superadmin-envfile-surface-conv.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-21 14:58:38 +00:00

195 lines
8.3 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""svc.sh 超级管理员凭据文件路径登记回归(superadmin_envfile=)。
deploy/start/restart 时传一次 AGENT_SYSTEM_ENV_FILE:svc 校验(普通文件、属当前用户、
无组/其他权限位)后只登记路径;裸 restart 与 recover(清空环境的生命周期子进程)
复用该路径转发给 serve;权限过宽在停旧服务前拒绝;空值清除登记;svc.env/svc.log
绝不出现文件内容。桩二进制与夹具复用 tests/test_svc_recover.py。
"""
import importlib.util
import os
import shutil
import unittest
from pathlib import Path
SOURCE = Path(__file__).resolve().parents[1]
_spec = importlib.util.spec_from_file_location("tsr", SOURCE / "tests" / "test_svc_recover.py")
tsr = importlib.util.module_from_spec(_spec)
_spec.loader.exec_module(tsr)
# 桩 serve 额外记录实际收到的 AGENT_SYSTEM_ENV_FILE(未设置记 <unset>)。
_ANCHOR = '> "$trace_dir/serve.txt"\n'
assert _ANCHOR in tsr.STUB
STUB = tsr.STUB.replace(
_ANCHOR,
_ANCHOR + r""" printf 'AGENT_SYSTEM_ENV_FILE=%s\n' "${AGENT_SYSTEM_ENV_FILE-<unset>}" >> "$trace_dir/serve.txt"
""", 1)
PASSWORD = "Dummy-SA-Pw-9f4c2e71-do-not-leak"
UNSET = "<unset>"
class SuperAdminEnvfileTests(tsr.SvcAutoRecoverTests):
def setUp(self):
super().setUp()
shutil.copy2(SOURCE / "scripts" / "svc_manifest.py",
self.root / "scripts" / "svc_manifest.py")
self.stub.write_text(STUB)
for key in ("AGENT_SYSTEM_ENV_FILE", "SUPER_ADMIN_OWNER", "SUPER_ADMIN_PASSWORD"):
self.env.pop(key, None)
self.secrets = self.root / "secrets"
self.secrets.mkdir(mode=0o700)
self.cred = self.make_cred("sa.env", 0o600)
# ---- 夹具 ----
def make_cred(self, name, mode):
p = self.secrets / name
p.write_text("SUPER_ADMIN_OWNER=op\nSUPER_ADMIN_PASSWORD=%s\n" % PASSWORD)
p.chmod(mode)
return p
def env_with(self, **extra):
env = dict(self.env)
env.update(extra)
return env
def served_envfile(self):
for line in (self.trace / "serve.txt").read_text().splitlines():
if line.startswith("AGENT_SYSTEM_ENV_FILE="):
return line.split("=", 1)[1]
return None
def fresh_trace(self):
f = self.trace / "serve.txt"
if f.exists():
f.unlink()
def assert_same_path(self, recorded, expected):
self.assertIsNotNone(recorded)
self.assertTrue(os.path.isabs(recorded), recorded)
self.assertEqual(os.path.realpath(recorded), os.path.realpath(expected))
def assert_no_secret(self, name, *outputs):
for fname in ("svc.env", "svc.log", "manifest.json", "serve.env", "history.jsonl"):
p = self.runtime(name) / fname
if p.exists():
self.assertNotIn(PASSWORD, p.read_text(errors="replace"), fname)
for out in outputs:
self.assertNotIn(PASSWORD, out)
def start_with_cred(self, name, cred=None):
result = self.svc(name, "start",
env=self.env_with(AGENT_SYSTEM_ENV_FILE=str(cred or self.cred)))
self.assertIn("已启动", result.stdout)
return result
# ---- 用例 ----
def test_sa_start_records_path_and_bare_restart_reuses_it(self):
self.seed_agent("dev", tsr.free_fixture_port())
r1 = self.start_with_cred("dev")
self.assert_same_path(self.kv("dev", "superadmin_envfile"), self.cred)
self.assert_same_path(self.served_envfile(), self.cred)
status = self.svc("dev", "status")
self.assertIn("超管凭据文件", status.stdout)
self.fresh_trace()
r2 = self.svc("dev", "restart") # 环境里不再有 AGENT_SYSTEM_ENV_FILE
self.assertIn("已启动", r2.stdout)
self.assert_same_path(self.served_envfile(), self.cred)
self.assert_same_path(self.kv("dev", "superadmin_envfile"), self.cred)
self.assert_no_secret("dev", r1.stdout, r2.stdout, status.stdout)
def test_sa_relative_path_recorded_absolute(self):
self.seed_agent("dev", tsr.free_fixture_port())
result = self.svc("dev", "start",
env=self.env_with(AGENT_SYSTEM_ENV_FILE="secrets/sa.env"))
self.assertIn("已启动", result.stdout)
self.assert_same_path(self.kv("dev", "superadmin_envfile"), self.cred)
def test_sa_recover_forwards_recorded_path(self):
self.seed_agent("dev", tsr.free_fixture_port())
self.start_with_cred("dev")
old_pid = self.kill9("dev")
self.fresh_trace()
result = self.svc("recover", "--quiet")
self.assertIn("recover 汇总:拉回 1", result.stdout)
new_pid = self.pid_of("dev")
self.assertTrue(self.pid_alive(new_pid))
self.assertNotEqual(new_pid, old_pid)
self.assert_same_path(self.served_envfile(), self.cred)
self.assert_same_path(self.kv("dev", "superadmin_envfile"), self.cred)
self.assert_no_secret("dev", result.stdout)
def test_sa_loose_permissions_refused_before_stop(self):
self.seed_agent("dev", tsr.free_fixture_port())
self.start_ok("dev")
pid = self.pid_of("dev")
before = (self.runtime("dev") / "svc.env").read_bytes()
loose = self.make_cred("loose.env", 0o644)
result = self.svc("dev", "restart", check=False,
env=self.env_with(AGENT_SYSTEM_ENV_FILE=str(loose)))
self.assertNotEqual(result.returncode, 0, result.stdout)
self.assertIn("权限过宽", result.stdout)
self.assertIn(str(loose), result.stdout)
self.assertEqual(self.pid_of("dev"), pid)
self.assertTrue(self.pid_alive(pid), "拒绝时不得停掉运行中的服务")
self.assertEqual((self.runtime("dev") / "svc.env").read_bytes(), before)
self.assert_no_secret("dev", result.stdout)
def test_sa_missing_file_refused_before_stop(self):
self.seed_agent("dev", tsr.free_fixture_port())
self.start_ok("dev")
pid = self.pid_of("dev")
result = self.svc("dev", "restart", check=False,
env=self.env_with(AGENT_SYSTEM_ENV_FILE=str(self.secrets / "nope.env")))
self.assertNotEqual(result.returncode, 0, result.stdout)
self.assertIn("nope.env", result.stdout)
self.assertTrue(self.pid_alive(pid))
self.assertEqual(self.pid_of("dev"), pid)
def test_sa_empty_value_clears_key(self):
self.seed_agent("dev", tsr.free_fixture_port())
self.start_with_cred("dev")
self.assertIsNotNone(self.kv("dev", "superadmin_envfile"))
self.fresh_trace()
self.svc("dev", "restart", env=self.env_with(AGENT_SYSTEM_ENV_FILE=""))
self.assertIsNone(self.kv("dev", "superadmin_envfile"))
self.assertEqual(self.served_envfile(), "")
self.fresh_trace()
self.svc("dev", "restart")
self.assertEqual(self.served_envfile(), UNSET)
def test_sa_explicit_environment_overrides_record(self):
self.seed_agent("dev", tsr.free_fixture_port())
self.start_with_cred("dev")
self.fresh_trace()
r = self.svc("dev", "restart", env=self.env_with(SUPER_ADMIN_OWNER="op"))
self.assertEqual(self.served_envfile(), UNSET, "显式凭据环境优先,不回落登记路径")
self.assert_same_path(self.kv("dev", "superadmin_envfile"), self.cred)
other = self.make_cred("other.env", 0o600)
self.fresh_trace()
self.svc("dev", "restart", env=self.env_with(AGENT_SYSTEM_ENV_FILE=str(other)))
self.assert_same_path(self.served_envfile(), other)
self.assert_same_path(self.kv("dev", "superadmin_envfile"), other)
self.assert_no_secret("dev", r.stdout)
def test_sa_recorded_file_gone_warns_and_skips(self):
self.seed_agent("dev", tsr.free_fixture_port())
self.start_with_cred("dev")
self.cred.unlink()
self.fresh_trace()
result = self.svc("dev", "restart")
self.assertIn("已不存在", result.stdout)
self.assertEqual(self.served_envfile(), UNSET)
# 只跑本文件用例;继承来的自恢复用例由 tests/test_svc_recover.py 负责。
for _name in dir(tsr.SvcAutoRecoverTests):
if _name.startswith("test_") and _name not in SuperAdminEnvfileTests.__dict__:
setattr(SuperAdminEnvfileTests, _name, None)
if __name__ == "__main__":
unittest.main()