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
+70
-14
@@ -10,9 +10,12 @@ VM 需要足够资源处理并发子代理与 crons。资源上限是本项目
|
||||
理自己的 VM/单元里(P1 常驻池 + P2 单元是底座),本层只做登
|
||||
记、派发与审计,不执行工具代码。
|
||||
|
||||
存储:state_dir/swarms.json(原子写 + 损坏 quarantine,orphans
|
||||
同款口径);P7 状态存储落地后迁移,本模块 docstring 与差距表
|
||||
已标注。
|
||||
存储:宿主级 AppStateStore(round-12 已迁移,见
|
||||
docs/chv-muse-p8-gap.md §6):state_dir/app-state.sqlite 的
|
||||
``p8-swarm`` 命名空间,一行存整份登记文档(原子写由 SQLite
|
||||
事务保证);旧 swarms.json 首次加载时自动导入并改名
|
||||
``.migrated``;KV 行损坏/库打不开时回空结构(fail-safe,
|
||||
orphans 同款口径),下次成功保存即覆盖自愈。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -24,7 +27,7 @@ import time
|
||||
import uuid
|
||||
from collections.abc import Callable
|
||||
|
||||
from cloud.chv.atomic_file import atomic_write_text
|
||||
from cloud.chv.app_state import AppStateStore, host_state_db_path
|
||||
from cloud.chv.compat215 import resolve_inside
|
||||
|
||||
_LOGGER = logging.getLogger("cloud.cloud_hypervisor")
|
||||
@@ -38,10 +41,19 @@ MAX_AUDIT_ENTRIES = 200
|
||||
MAX_INVOCATION_ENTRIES = 200
|
||||
AGENT_STATUSES = ("pending", "running", "done", "failed")
|
||||
_REGISTRY_FILENAME = "swarms.json"
|
||||
_KV_NAMESPACE = "p8-swarm"
|
||||
_KV_KEY = "registry"
|
||||
|
||||
|
||||
def _open_store(state_dir: str) -> AppStateStore:
|
||||
"""打开宿主级状态库(每次调用新建连接,用完即关)。"""
|
||||
store = AppStateStore(host_state_db_path(state_dir))
|
||||
store.open()
|
||||
return store
|
||||
|
||||
|
||||
def _registry_path(state_dir: str) -> str:
|
||||
"""群登记文件路径。"""
|
||||
"""旧版群登记文件路径(只用于一次性迁移导入)。"""
|
||||
return os.path.join(state_dir, _REGISTRY_FILENAME)
|
||||
|
||||
|
||||
@@ -61,8 +73,38 @@ def _empty_registry() -> dict:
|
||||
|
||||
|
||||
def _load_registry(state_dir: str) -> dict:
|
||||
"""读登记:无文件回空结构;损坏则 quarantine 后回空结构
|
||||
(损坏丢状态是 orphans 同款 fail-safe 口径,见差距表)。"""
|
||||
"""读登记:KV 行缺失时走一次性旧文件导入;行损坏/库打不
|
||||
开时回空结构(损坏丢状态是 orphans 同款 fail-safe 口径,
|
||||
见差距表;下次成功保存即覆盖自愈)。"""
|
||||
try:
|
||||
with _open_store(state_dir) as store:
|
||||
raw = store.kv_get(_KV_NAMESPACE, _KV_KEY)
|
||||
except Exception:
|
||||
_LOGGER.warning("chv_swarm_registry_store_unreadable "
|
||||
"state_dir=%s", state_dir)
|
||||
return _empty_registry()
|
||||
if raw is None:
|
||||
return _import_legacy(state_dir)
|
||||
try:
|
||||
data = json.loads(raw)
|
||||
except ValueError:
|
||||
_LOGGER.warning("chv_swarm_registry_corrupt state_dir=%s",
|
||||
state_dir)
|
||||
return _empty_registry()
|
||||
if not isinstance(data, dict):
|
||||
return _empty_registry()
|
||||
data.setdefault("swarms", {})
|
||||
data.setdefault("tools", {})
|
||||
data.setdefault("invocations", [])
|
||||
data.setdefault("audit", [])
|
||||
return data
|
||||
|
||||
|
||||
def _import_legacy(state_dir: str) -> dict:
|
||||
"""一次性导入旧 swarms.json:合法即进 KV 并把文件改名
|
||||
``.migrated``;无文件回空;损坏则 quarantine 后回空(旧
|
||||
口径)。导入失败(库写不进)时返回读到的数据、文件保留,
|
||||
下次加载重试。"""
|
||||
path = _registry_path(state_dir)
|
||||
try:
|
||||
with open(path, "r") as fh:
|
||||
@@ -72,27 +114,41 @@ def _load_registry(state_dir: str) -> dict:
|
||||
except (OSError, ValueError):
|
||||
_quarantine(path)
|
||||
return _empty_registry()
|
||||
if not isinstance(data, dict):
|
||||
if not isinstance(data, dict) \
|
||||
or not isinstance(data.get("swarms", {}), dict) \
|
||||
or not isinstance(data.get("tools", {}), dict) \
|
||||
or not isinstance(data.get("invocations", []), list) \
|
||||
or not isinstance(data.get("audit", []), list):
|
||||
_quarantine(path)
|
||||
return _empty_registry()
|
||||
data.setdefault("swarms", {})
|
||||
data.setdefault("tools", {})
|
||||
data.setdefault("invocations", [])
|
||||
data.setdefault("audit", [])
|
||||
try:
|
||||
with _open_store(state_dir) as store:
|
||||
store.kv_put(_KV_NAMESPACE, _KV_KEY, json.dumps(data))
|
||||
except Exception:
|
||||
_LOGGER.warning("chv_swarm_registry_import_failed "
|
||||
"state_dir=%s", state_dir)
|
||||
return data
|
||||
try:
|
||||
os.replace(path, path + ".migrated")
|
||||
except OSError:
|
||||
pass
|
||||
return data
|
||||
|
||||
|
||||
def _save_registry(state_dir: str, data: dict) -> None:
|
||||
"""原子落盘登记(审计与调用记录各截断到上限);写失败
|
||||
只记日志,不炸调用方。"""
|
||||
"""落盘登记到 KV 行(审计与调用记录各截断到上限);写失
|
||||
败只记日志,不炸调用方。"""
|
||||
data["audit"] = data.get("audit", [])[-MAX_AUDIT_ENTRIES:]
|
||||
data["invocations"] = data.get("invocations", [])[
|
||||
-MAX_INVOCATION_ENTRIES:]
|
||||
try:
|
||||
os.makedirs(state_dir, exist_ok=True)
|
||||
atomic_write_text(_registry_path(state_dir),
|
||||
json.dumps(data))
|
||||
except OSError:
|
||||
with _open_store(state_dir) as store:
|
||||
store.kv_put(_KV_NAMESPACE, _KV_KEY, json.dumps(data))
|
||||
except Exception:
|
||||
_LOGGER.warning("chv_swarm_registry_save_failed "
|
||||
"state_dir=%s", state_dir)
|
||||
|
||||
|
||||
Reference in new issue
Block a user