round-12: migrate P8 registries + P12 audit to AppStateStore
- app_state.py: host_state_db_path() (host-level DB, distinct from per-VM state_db_path); kv_get/kv_put/kv_delete on meta (namespaced keys; kv_put skips the no-secrets guard by design — registry docs hold user prompts, documented); audit_trail table (P12 record shape + hash chain, seq explicit); counts()/backup() gain audit_trail. - swarm.py / presentation.py / browser_tasks.py: JSON-file backends replaced by KV rows (p8-swarm / p8-ledger / p8-browser-tasks); legacy JSON imported once on first load (renamed .migrated); corrupt row / unreadable store returns empty (fail-safe, same semantics as before; next successful save self-heals). - audit_trail.py: StoreAuditTrail (same record shape, hash chain, filters, export as JSONL version, backed by audit_trail table); migrate_jsonl_to_store() moves a chain verbatim (broken chain refused, divergent seq refused, re-run idempotent). JSONL class kept for reading old files. - P12 dict contracts now run against real P3/P5 code (ApprovalQueue.record -> build_approval_card -> card_matches_channel; CapabilityIssuer.issue -> describe -> PermissionRegistry.record_grant). - P6->P3 local protocol test: real PrivsepClient + real worker server + real git status; worker git argv carries the real P3 helper + useHttpPath; no token in argv/env/audit. runuser + HTTP pull stay real-machine-only (see gap docs). - Gap docs updated: P8 migration done, guest-side browser follow-up explicitly left for the real-machine track (no task_id in the browser protocol yet; lifecycle needs a real VM to verify); P6 wiring + P12 migration marked done; production-readiness.md gains §20 (round-12 integration). - Tests: new cloud/tests/test_chv_muse_round12.py, 24/24 OK; test_chv_muse_p8.py ledger corrupt test rewritten for the KV backend (+ legacy-import test). P7+P8: 95+1 tests OK.
This commit is contained in:
1 parent
466cf7eae7
commit
d4cef29cdb
11 files changed
+1147
-63
No files matched your search
@@ -547,15 +547,37 @@ class PresentationTestCase(unittest.TestCase):
|
||||
with self.assertRaises(ValueError):
|
||||
self.ledger.should_notify("t1", "", now=T0)
|
||||
|
||||
def test_ledger_corrupt_file_recovers(self):
|
||||
def test_ledger_corrupt_row_recovers(self):
|
||||
# round-12 迁移后:损坏的 KV 行回空账本(旧版是损坏的
|
||||
# JSON 文件 quarantine 后回空;语义同款,fail-safe)。
|
||||
self.assertTrue(self.ledger.should_notify("t1", "failed",
|
||||
now=T0))
|
||||
from cloud.chv.app_state import AppStateStore, \
|
||||
host_state_db_path
|
||||
with AppStateStore(host_state_db_path(self.state_dir)) as store:
|
||||
store.kv_put("p8-ledger", "registry", "{not json")
|
||||
self.assertTrue(self.ledger.should_notify("t2", "failed",
|
||||
now=T0))
|
||||
|
||||
def test_ledger_legacy_json_imported_once(self):
|
||||
# 旧版 JSON 文件首次加载时导入 KV 并改名 .migrated;
|
||||
# 之后再写坏 JSON 也不影响 KV 里的账本。
|
||||
with open(os.path.join(self.state_dir,
|
||||
"notification-ledger.json"),
|
||||
"w") as fh:
|
||||
fh.write('{"notified": {"t0:failed": 1000}}')
|
||||
self.assertFalse(self.ledger.should_notify(
|
||||
"t0", "failed", now=1000 + 10))
|
||||
self.assertTrue(os.path.exists(os.path.join(
|
||||
self.state_dir, "notification-ledger.json.migrated")))
|
||||
self.assertFalse(os.path.exists(os.path.join(
|
||||
self.state_dir, "notification-ledger.json")))
|
||||
with open(os.path.join(self.state_dir,
|
||||
"notification-ledger.json"),
|
||||
"w") as fh:
|
||||
fh.write("{not json")
|
||||
self.assertTrue(self.ledger.should_notify("t2", "failed",
|
||||
now=T0))
|
||||
self.assertFalse(self.ledger.should_notify(
|
||||
"t0", "failed", now=1000 + 20))
|
||||
|
||||
def test_present_or_log_combined(self):
|
||||
out = present_or_log({"status": "failed"}, self.ledger,
|
||||
|
||||
@@ -0,0 +1,528 @@
|
||||
"""对齐 Muse round-12 集成测试(P4~P12 合入后的跨轨接线)。
|
||||
|
||||
只测本轨新增的接线,不复述各轨自有测试:
|
||||
- AppStateStore KV API + audit_trail 表(迁移地基)。
|
||||
- P8 三处登记 + P12 审计 JSONL 到 AppStateStore 的迁移
|
||||
(语义不变:去重、哈希链、project_id 隔离;旧文件一次
|
||||
性导入;损坏回空 fail-safe)。
|
||||
- P6 worker → P3 helper/authd → git 的本地协议链(真
|
||||
PrivsepClient + 真 worker server + 真 git status;runuser
|
||||
与 HTTP 拉取需真机,见测试 docstring)。
|
||||
- P12 审批卡 / 权限登记对真实 P3/P5 代码的 dict 契约
|
||||
(不再用替身)。
|
||||
"""
|
||||
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
import socket
|
||||
import sqlite3
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import threading
|
||||
import time
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(REPO_ROOT))
|
||||
|
||||
from cloud.chv.app_state import AppStateStore, host_state_db_path
|
||||
from cloud.chv.audit_trail import (
|
||||
AuditTrail,
|
||||
StoreAuditTrail,
|
||||
migrate_jsonl_to_store,
|
||||
)
|
||||
from cloud.chv.swarm import (
|
||||
create_swarm,
|
||||
get_swarm,
|
||||
)
|
||||
from cloud.chv.presentation import NotificationLedger
|
||||
from cloud.chv.browser_tasks import (
|
||||
BrowserTransport,
|
||||
create_instance,
|
||||
get_instance,
|
||||
)
|
||||
|
||||
TOKEN = "fake-token-123"
|
||||
T0 = 1700000000.0
|
||||
|
||||
|
||||
def _load_guest_module(filename, module_name):
|
||||
"""按文件加载 chv-guest 下的模块(不走包名,与 P3 测试同款)。"""
|
||||
path = REPO_ROOT / "chv-guest" / filename
|
||||
spec = importlib.util.spec_from_file_location(module_name, path)
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(mod)
|
||||
return mod
|
||||
|
||||
|
||||
class KvApiTest(unittest.TestCase):
|
||||
"""AppStateStore KV API(P8 迁移的后端)。"""
|
||||
|
||||
def setUp(self):
|
||||
self.tmp = tempfile.TemporaryDirectory()
|
||||
self.dir = self.tmp.name
|
||||
self.store = AppStateStore(
|
||||
os.path.join(self.dir, "app-state.sqlite"))
|
||||
self.store.open()
|
||||
|
||||
def tearDown(self):
|
||||
self.store.close()
|
||||
self.tmp.cleanup()
|
||||
|
||||
def test_put_get_delete_roundtrip(self):
|
||||
self.assertIsNone(self.store.kv_get("p8-swarm", "registry"))
|
||||
self.store.kv_put("p8-swarm", "registry", '{"a": 1}')
|
||||
self.assertEqual(self.store.kv_get("p8-swarm", "registry"),
|
||||
'{"a": 1}')
|
||||
self.store.kv_put("p8-swarm", "registry", '{"a": 2}')
|
||||
self.assertEqual(self.store.kv_get("p8-swarm", "registry"),
|
||||
'{"a": 2}')
|
||||
self.store.kv_delete("p8-swarm", "registry")
|
||||
self.assertIsNone(self.store.kv_get("p8-swarm", "registry"))
|
||||
self.store.kv_delete("p8-swarm", "registry")
|
||||
|
||||
def test_namespaces_isolated(self):
|
||||
self.store.kv_put("p8-swarm", "registry", "swarm-doc")
|
||||
self.store.kv_put("p8-ledger", "registry", "ledger-doc")
|
||||
self.assertEqual(self.store.kv_get("p8-swarm", "registry"),
|
||||
"swarm-doc")
|
||||
self.assertEqual(self.store.kv_get("p8-ledger", "registry"),
|
||||
"ledger-doc")
|
||||
|
||||
def test_bad_namespace_or_key_rejected(self):
|
||||
for ns, key in (("", "k"), ("a:b", "k"), ("ns", ""),
|
||||
("ns", "a:b")):
|
||||
with self.assertRaises(ValueError):
|
||||
self.store.kv_put(ns, key, "v")
|
||||
with self.assertRaises(ValueError):
|
||||
self.store.kv_put("ns", "k", 123)
|
||||
|
||||
def test_host_path_differs_from_vm_path(self):
|
||||
from cloud.chv.app_state import state_db_path
|
||||
host = host_state_db_path(self.dir)
|
||||
vm = state_db_path(self.dir, "chv-abc123")
|
||||
self.assertNotEqual(host, vm)
|
||||
self.assertTrue(host.endswith("app-state.sqlite"))
|
||||
self.assertEqual(os.path.dirname(host), self.dir)
|
||||
|
||||
def test_counts_includes_audit_trail(self):
|
||||
counts = self.store.counts()
|
||||
self.assertIn("audit_trail", counts)
|
||||
self.assertEqual(counts["audit_trail"], 0)
|
||||
|
||||
|
||||
class StoreAuditTrailTest(unittest.TestCase):
|
||||
"""StoreAuditTrail:与 JSONL 版同款链与隔离,走 SQLite。"""
|
||||
|
||||
def setUp(self):
|
||||
self.tmp = tempfile.TemporaryDirectory()
|
||||
self.dir = self.tmp.name
|
||||
|
||||
def tearDown(self):
|
||||
self.tmp.cleanup()
|
||||
|
||||
def test_append_chain_resumes_across_instances(self):
|
||||
first = StoreAuditTrail(self.dir)
|
||||
r1 = first.append("p1", "worker", "task_dispatch", "run",
|
||||
object_id="t1", ts=T0)
|
||||
r2 = first.append("p1", "worker", "vm_lifecycle", "wake",
|
||||
object_id="vm1", ts=T0 + 1)
|
||||
self.assertEqual(r1["seq"], 1)
|
||||
self.assertEqual(r2["seq"], 2)
|
||||
self.assertEqual(r2["prev_hash"], r1["hash"])
|
||||
second = StoreAuditTrail(self.dir)
|
||||
r3 = second.append("p1", "worker", "task_dispatch", "done",
|
||||
object_id="t1", ts=T0 + 2)
|
||||
self.assertEqual(r3["seq"], 3)
|
||||
self.assertEqual(r3["prev_hash"], r2["hash"])
|
||||
ok, bad = second.verify()
|
||||
self.assertTrue(ok)
|
||||
self.assertEqual(bad, -1)
|
||||
|
||||
def test_project_isolation_and_filters(self):
|
||||
trail = StoreAuditTrail(self.dir)
|
||||
trail.append("p1", "worker", "task_dispatch", "run",
|
||||
object_id="t1", ts=T0)
|
||||
trail.append("p2", "worker", "task_dispatch", "run",
|
||||
object_id="t2", ts=T0)
|
||||
trail.append("p1", "worker", "quota", "denied",
|
||||
object_id="t1", ts=T0 + 1)
|
||||
self.assertEqual(len(trail.list_records("p1", limit=0)), 2)
|
||||
self.assertEqual(len(trail.list_records("p2", limit=0)), 1)
|
||||
self.assertEqual(trail.list_records("p3", limit=0), [])
|
||||
self.assertEqual(trail.list_records("", limit=0), [])
|
||||
self.assertEqual(
|
||||
len(trail.list_records("p1", category="quota",
|
||||
limit=0)), 1)
|
||||
rows = trail.list_records("p1", limit=0)
|
||||
self.assertGreater(rows[0]["ts"], rows[1]["ts"])
|
||||
|
||||
def test_tamper_detected_with_row_index(self):
|
||||
trail = StoreAuditTrail(self.dir)
|
||||
trail.append("p1", "worker", "task_dispatch", "run",
|
||||
object_id="t1", ts=T0)
|
||||
trail.append("p1", "worker", "task_dispatch", "done",
|
||||
object_id="t1", ts=T0 + 1)
|
||||
db = host_state_db_path(self.dir)
|
||||
conn = sqlite3.connect(db)
|
||||
conn.execute("UPDATE audit_trail SET action = ? WHERE seq = 2",
|
||||
("forged",))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
ok, bad = StoreAuditTrail(self.dir).verify()
|
||||
self.assertFalse(ok)
|
||||
self.assertEqual(bad, 1)
|
||||
|
||||
def test_export_json(self):
|
||||
trail = StoreAuditTrail(self.dir)
|
||||
trail.append("p1", "worker", "task_dispatch", "run",
|
||||
object_id="t1", ts=T0)
|
||||
trail.append("p1", "worker", "task_dispatch", "done",
|
||||
object_id="t1", ts=T0 + 1)
|
||||
dest = os.path.join(self.dir, "export.json")
|
||||
self.assertEqual(trail.export_json("p1", dest), 2)
|
||||
self.assertEqual(os.stat(dest).st_mode & 0o777, 0o600)
|
||||
with open(dest) as fh:
|
||||
rows = json.load(fh)
|
||||
self.assertEqual([r["seq"] for r in rows], [1, 2])
|
||||
|
||||
def test_secret_detail_scrubbed(self):
|
||||
trail = StoreAuditTrail(self.dir)
|
||||
rec = trail.append("p1", "worker", "task_dispatch", "run",
|
||||
detail={"token": TOKEN, "note": "ok"},
|
||||
ts=T0)
|
||||
self.assertNotIn("token", rec["detail"])
|
||||
self.assertEqual(rec["detail"]["note"], "ok")
|
||||
|
||||
|
||||
class MigrateJsonlTest(unittest.TestCase):
|
||||
"""migrate_jsonl_to_store:原样搬链。"""
|
||||
|
||||
def setUp(self):
|
||||
self.tmp = tempfile.TemporaryDirectory()
|
||||
self.dir = self.tmp.name
|
||||
self.jsonl = os.path.join(self.dir, "audit.jsonl")
|
||||
|
||||
def tearDown(self):
|
||||
self.tmp.cleanup()
|
||||
|
||||
def _seed_jsonl(self):
|
||||
trail = AuditTrail(self.jsonl)
|
||||
trail.append("p1", "worker", "task_dispatch", "run",
|
||||
object_id="t1", ts=T0)
|
||||
trail.append("p2", "worker", "quota", "denied",
|
||||
object_id="t2", ts=T0 + 1)
|
||||
trail.append("p1", "worker", "task_dispatch", "done",
|
||||
object_id="t1", ts=T0 + 2)
|
||||
return trail
|
||||
|
||||
def test_migrate_then_verify_and_list(self):
|
||||
self._seed_jsonl()
|
||||
self.assertEqual(migrate_jsonl_to_store(self.jsonl, self.dir),
|
||||
3)
|
||||
store_trail = StoreAuditTrail(self.dir)
|
||||
ok, bad = store_trail.verify()
|
||||
self.assertTrue(ok)
|
||||
self.assertEqual(bad, -1)
|
||||
self.assertEqual(len(store_trail.list_records("p1",
|
||||
limit=0)), 2)
|
||||
self.assertEqual(len(store_trail.list_records("p2",
|
||||
limit=0)), 1)
|
||||
rows = store_trail.list_records("p1", limit=0)
|
||||
self.assertEqual([r["seq"] for r in rows], [3, 1])
|
||||
|
||||
def test_migrate_is_idempotent(self):
|
||||
self._seed_jsonl()
|
||||
self.assertEqual(migrate_jsonl_to_store(self.jsonl, self.dir),
|
||||
3)
|
||||
self.assertEqual(migrate_jsonl_to_store(self.jsonl, self.dir),
|
||||
0)
|
||||
ok, _ = StoreAuditTrail(self.dir).verify()
|
||||
self.assertTrue(ok)
|
||||
|
||||
def test_missing_file_returns_zero(self):
|
||||
self.assertEqual(
|
||||
migrate_jsonl_to_store(os.path.join(self.dir, "nope"),
|
||||
self.dir), 0)
|
||||
|
||||
def test_broken_chain_refused(self):
|
||||
self._seed_jsonl()
|
||||
with open(self.jsonl, "a") as fh:
|
||||
fh.write("{broken json\n")
|
||||
with self.assertRaises(ValueError):
|
||||
migrate_jsonl_to_store(self.jsonl, self.dir)
|
||||
self.assertEqual(
|
||||
len(StoreAuditTrail(self.dir).list_records("p1",
|
||||
limit=0)), 0)
|
||||
|
||||
def test_divergent_seq_refused(self):
|
||||
self._seed_jsonl()
|
||||
migrate_jsonl_to_store(self.jsonl, self.dir)
|
||||
other = os.path.join(self.dir, "other.jsonl")
|
||||
trail = AuditTrail(other)
|
||||
trail.append("p1", "worker", "task_dispatch", "run",
|
||||
object_id="tX", ts=T0)
|
||||
with self.assertRaises(ValueError):
|
||||
migrate_jsonl_to_store(other, self.dir)
|
||||
|
||||
|
||||
class P8MigrationTest(unittest.TestCase):
|
||||
"""P8 三处登记走 KV:无 JSON 文件、旧文件导入、损坏恢复。"""
|
||||
|
||||
def setUp(self):
|
||||
self.tmp = tempfile.TemporaryDirectory()
|
||||
self.dir = self.tmp.name
|
||||
|
||||
def tearDown(self):
|
||||
self.tmp.cleanup()
|
||||
|
||||
def test_swarm_uses_kv_no_json_file(self):
|
||||
swarm = create_swarm(self.dir, "p1", "parent1", now=T0)
|
||||
self.assertFalse(os.path.exists(
|
||||
os.path.join(self.dir, "swarms.json")))
|
||||
self.assertTrue(os.path.exists(host_state_db_path(self.dir)))
|
||||
with AppStateStore(host_state_db_path(self.dir)) as store:
|
||||
raw = store.kv_get("p8-swarm", "registry")
|
||||
self.assertIsNotNone(raw)
|
||||
self.assertIn(swarm["id"], json.loads(raw)["swarms"])
|
||||
self.assertEqual(get_swarm(self.dir, swarm["id"])["id"],
|
||||
swarm["id"])
|
||||
|
||||
def test_swarm_legacy_json_imported_once(self):
|
||||
legacy = {"swarms": {"s1": {"id": "s1", "project_id": "p1",
|
||||
"parent_task_id": "parent1",
|
||||
"agents": {}, "status": "active",
|
||||
"created_at": T0}},
|
||||
"tools": {}, "invocations": [], "audit": []}
|
||||
with open(os.path.join(self.dir, "swarms.json"), "w") as fh:
|
||||
json.dump(legacy, fh)
|
||||
self.assertEqual(get_swarm(self.dir, "s1")["id"], "s1")
|
||||
self.assertTrue(os.path.exists(
|
||||
os.path.join(self.dir, "swarms.json.migrated")))
|
||||
self.assertFalse(os.path.exists(
|
||||
os.path.join(self.dir, "swarms.json")))
|
||||
|
||||
def test_swarm_corrupt_row_recovers(self):
|
||||
create_swarm(self.dir, "p1", "parent1", now=T0)
|
||||
with AppStateStore(host_state_db_path(self.dir)) as store:
|
||||
store.kv_put("p8-swarm", "registry", "{not json")
|
||||
swarm = create_swarm(self.dir, "p1", "parent2", now=T0)
|
||||
self.assertEqual(swarm["parent_task_id"], "parent2")
|
||||
|
||||
def test_ledger_uses_kv_no_json_file(self):
|
||||
ledger = NotificationLedger(self.dir)
|
||||
self.assertTrue(ledger.should_notify("t1", "failed", now=T0))
|
||||
self.assertFalse(os.path.exists(os.path.join(
|
||||
self.dir, "notification-ledger.json")))
|
||||
with AppStateStore(host_state_db_path(self.dir)) as store:
|
||||
raw = store.kv_get("p8-ledger", "registry")
|
||||
self.assertIn("t1:failed", json.loads(raw)["notified"])
|
||||
|
||||
def test_browser_tasks_legacy_import_and_corrupt_row(self):
|
||||
legacy = {"instances": {
|
||||
"old-task": {"instance_id": "i1", "task_id": "old-task",
|
||||
"status": "destroyed",
|
||||
"shots_dir": "browser-tasks/i1/shots",
|
||||
"created_at": T0}}}
|
||||
with open(os.path.join(self.dir, "browser-tasks.json"),
|
||||
"w") as fh:
|
||||
json.dump(legacy, fh)
|
||||
self.assertEqual(
|
||||
get_instance(self.dir, "old-task")["instance_id"], "i1")
|
||||
self.assertTrue(os.path.exists(os.path.join(
|
||||
self.dir, "browser-tasks.json.migrated")))
|
||||
|
||||
class _FakeTransport(BrowserTransport):
|
||||
def spawn(self, instance):
|
||||
return None
|
||||
|
||||
def close(self, instance):
|
||||
return None
|
||||
|
||||
def ping(self, instance):
|
||||
return True
|
||||
|
||||
with AppStateStore(host_state_db_path(self.dir)) as store:
|
||||
store.kv_put("p8-browser-tasks", "registry", "{not json")
|
||||
inst = create_instance(self.dir, "new-task",
|
||||
_FakeTransport(), now=T0)
|
||||
self.assertEqual(inst["status"], "active")
|
||||
self.assertFalse(os.path.exists(os.path.join(
|
||||
self.dir, "browser-tasks.json")))
|
||||
|
||||
|
||||
class WorkerProtocolTest(unittest.TestCase):
|
||||
"""P6 worker → P3 helper/authd → git 本地协议链。
|
||||
|
||||
真 PrivsepClient + 真 chv-gitea-worker server(Unix socket
|
||||
+ SO_PEERCRED)+ 真 git status;run_git 探针调 worker 自身
|
||||
的 ``_git_base_argv()``(含 P3 helper 与 useHttpPath)组
|
||||
argv,只去掉本机不存在的 ``runuser -u chv`` 包装——runuser
|
||||
与 HTTP 拉取(需真实 gitea.tose.top + Sentinel 放行)留
|
||||
给真机轨,本测试不假装已覆盖。
|
||||
"""
|
||||
|
||||
HELPER = REPO_ROOT / "chv-guest" / "git-credential-chv-authd"
|
||||
|
||||
def setUp(self):
|
||||
self.tmp = tempfile.TemporaryDirectory()
|
||||
self.dir = self.tmp.name
|
||||
self.workspace = os.path.join(self.dir, "workspace")
|
||||
os.makedirs(self.workspace)
|
||||
self.repo = os.path.join(self.workspace, "testrepo")
|
||||
subprocess.run(["git", "init", "-q", "-b", "main", self.repo],
|
||||
check=True)
|
||||
subprocess.run(["git", "-C", self.repo, "config",
|
||||
"user.email", "t@example.invalid"], check=True)
|
||||
subprocess.run(["git", "-C", self.repo, "config",
|
||||
"user.name", "Test"], check=True)
|
||||
with open(os.path.join(self.repo, "README.md"), "w") as fh:
|
||||
fh.write("hello\n")
|
||||
subprocess.run(["git", "-C", self.repo, "add", "."],
|
||||
check=True)
|
||||
subprocess.run(["git", "-C", self.repo, "commit", "-q",
|
||||
"-m", "init"], check=True)
|
||||
self.socket_path = os.path.join(self.dir, "worker.sock")
|
||||
self.log_path = os.path.join(self.dir, "worker.log")
|
||||
self.mod = _load_guest_module("chv-gitea-worker.py",
|
||||
"chv_gitea_worker_round12")
|
||||
self.mod.HELPER_PATH = str(self.HELPER)
|
||||
self.argv_seen = []
|
||||
self.env_seen = []
|
||||
|
||||
def run_git(action, repo):
|
||||
argv = ["runuser", "-u", self.mod.SYNC_USER, "--",
|
||||
"env", "HOME=/home/chv"] + \
|
||||
self.mod._git_base_argv()
|
||||
if action == "status":
|
||||
argv += ["-C", repo, "status", "-sb"]
|
||||
elif action == "pull":
|
||||
argv += ["-C", repo, "pull", "--ff-only"]
|
||||
elif action == "push":
|
||||
argv += ["-C", repo, "push"]
|
||||
else:
|
||||
return 2, "", "未知 git 操作:%s" % action
|
||||
self.argv_seen.append(list(argv))
|
||||
env = {"PATH": os.environ.get("PATH", ""),
|
||||
"HOME": self.dir,
|
||||
"CHV_AUTHD_SOCKET": ""}
|
||||
self.env_seen.append(dict(env))
|
||||
runnable = [a for a in argv
|
||||
if a not in ("runuser", "-u", "chv", "--")]
|
||||
runnable[1] = "HOME=%s" % self.dir
|
||||
proc = subprocess.run(runnable, capture_output=True,
|
||||
text=True, timeout=60,
|
||||
env={**env, "HOME": self.dir})
|
||||
return proc.returncode, proc.stdout, proc.stderr
|
||||
|
||||
self.server = self.mod.WorkerServer(
|
||||
self.socket_path, self.workspace, self.log_path,
|
||||
allowed_uids=(os.getuid(),), run_git=run_git,
|
||||
helper_path=str(self.HELPER))
|
||||
self.thread = threading.Thread(
|
||||
target=self.server.serve_forever, daemon=True)
|
||||
self.thread.start()
|
||||
|
||||
def tearDown(self):
|
||||
self.server.shutdown()
|
||||
self.server.server_close()
|
||||
self.tmp.cleanup()
|
||||
|
||||
def test_status_through_real_client_and_server(self):
|
||||
from cloud.chv.privsep import PrivsepClient, WorkerRequest
|
||||
client = PrivsepClient(socket_path=self.socket_path,
|
||||
connector="gitea",
|
||||
workspace=self.workspace)
|
||||
ping = client.ping()
|
||||
self.assertTrue(ping.ok)
|
||||
resp = client.call(WorkerRequest(
|
||||
"status", "gitea", self.repo, "req-1"))
|
||||
self.assertTrue(resp.ok, resp.stderr)
|
||||
self.assertEqual(resp.returncode, 0)
|
||||
self.assertIn("## main", resp.stdout)
|
||||
self.assertEqual(resp.request_id, "req-1")
|
||||
# worker 的 git argv 走 P3 helper + useHttpPath,不含 token:
|
||||
self.assertEqual(len(self.argv_seen), 1)
|
||||
argv = self.argv_seen[0]
|
||||
self.assertIn("credential.helper=%s" % self.HELPER, argv)
|
||||
self.assertIn("credential.useHttpPath=true", argv)
|
||||
self.assertNotIn(TOKEN, " ".join(argv))
|
||||
self.assertNotIn(TOKEN, json.dumps(self.env_seen))
|
||||
# worker 用的 helper 就是 P3 的 helper 文件本身:
|
||||
self.assertTrue(os.path.samefile(
|
||||
self.server.helper_path, str(self.HELPER)))
|
||||
# 审计日志不含 token(本测试的 token 只存在于断言字面量,
|
||||
# 不进任何被测输入——worker 从未见过它):
|
||||
with open(self.log_path) as fh:
|
||||
self.assertNotIn(TOKEN, fh.read())
|
||||
|
||||
def test_helper_file_is_the_p3_helper(self):
|
||||
# worker 硬编码的 helper 路径与 P3 测试用的 helper 是同一
|
||||
# 个文件(接线不是替身)。
|
||||
from cloud.tests.test_chv_muse_p3 import HelperE2ETest
|
||||
self.assertTrue(os.path.samefile(
|
||||
str(self.HELPER), str(HelperE2ETest.HELPER)))
|
||||
|
||||
|
||||
class P12DictContractTest(unittest.TestCase):
|
||||
"""P12 接线用真实 P3/P5 代码,不用替身。"""
|
||||
|
||||
def test_approval_card_from_real_p3_queue(self):
|
||||
from cloud.chv.sentinel import ApprovalQueue, EgressRequest
|
||||
from cloud.chv.approval_card import (
|
||||
build_approval_card,
|
||||
card_matches_channel,
|
||||
)
|
||||
from cloud.chv.approval_channel import request_approval
|
||||
queue = ApprovalQueue(clock=lambda: T0)
|
||||
req = EgressRequest("gitea", "POST", "https",
|
||||
"gitea.tose.top", 443,
|
||||
"/api/v1/repos/admin/x/issues")
|
||||
pending_id = queue.record(req, ["needs_approval"])
|
||||
record = queue.list_pending()[0]
|
||||
self.assertEqual(record["pending_id"], pending_id)
|
||||
card = build_approval_card(record)
|
||||
self.assertEqual(card["pending_id"], pending_id)
|
||||
self.assertEqual(card["target"]["host"], "gitea.tose.top")
|
||||
self.assertEqual(card["target"]["method"], "POST")
|
||||
self.assertEqual(card["risk"], "medium")
|
||||
self.assertIn("needs_approval", card["reasons"])
|
||||
channel = request_approval(record, secret="test-secret",
|
||||
now=T0)
|
||||
self.assertTrue(
|
||||
card_matches_channel(card, channel.payload()))
|
||||
|
||||
def test_permission_registry_from_real_p5_describe(self):
|
||||
from cloud.chv.capabilities import CapabilityIssuer
|
||||
from cloud.chv.activity import PermissionRegistry
|
||||
issuer = CapabilityIssuer(clock=lambda: T0)
|
||||
cap = issuer.issue(
|
||||
"one-shot", "gitea", "gitea.tose.top", ["GET"],
|
||||
[("GET", "/api/v1/*", "allow")], "sync", now=T0)
|
||||
described = cap.describe()
|
||||
reg_path = os.path.join(
|
||||
tempfile.mkdtemp(), "permissions.json")
|
||||
reg = PermissionRegistry(reg_path)
|
||||
grant = reg.record_grant("p1", described, ts=T0)
|
||||
self.assertEqual(grant["capability_id"],
|
||||
described["capability_id"])
|
||||
self.assertEqual(grant["connector"], "gitea")
|
||||
self.assertEqual(grant["host"], "gitea.tose.top")
|
||||
self.assertEqual(grant["use_case"], "sync")
|
||||
self.assertEqual(grant["tier"], "one-shot")
|
||||
active = reg.list_active("p1", now=T0 + 10)
|
||||
self.assertEqual(len(active), 1)
|
||||
self.assertEqual(active[0]["capability_id"],
|
||||
described["capability_id"])
|
||||
self.assertTrue(reg.note_use("p1",
|
||||
described["capability_id"]))
|
||||
self.assertTrue(reg.revoke("p1",
|
||||
described["capability_id"]))
|
||||
self.assertEqual(reg.list_active("p1", now=T0 + 10), [])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user