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:
host committed 2026-11-07 00:36:25 +00:00
1 parent 466cf7eae7
commit d4cef29cdb
11 files changed
+1147 -63

No files matched your search

+61 -9
View File
@@ -12,8 +12,12 @@ transport 接口:真实 transport 走 P2 的 broker 协议
复制 P2 的文件)。本层只管生命周期、单实例单任务与状态隔离,
transport 注入(测试用 fake)。
存储:state_dir/browser-tasks.json(原子写 + 损坏 quarantine,
orphans 同款口径);P7 状态存储落地后迁移。
存储:宿主级 AppStateStore(round-12 已迁移,见
docs/chv-muse-p8-gap.md §6):state_dir/app-state.sqlite 的
``p8-browser-tasks`` 命名空间,一行存整份登记;旧
browser-tasks.json 首次加载时自动导入并改名 ``.migrated``;
行损坏/库打不开时回空结构(fail-safe,orphans 同款口径),
下次成功保存即覆盖自愈。
"""
from __future__ import annotations
@@ -24,12 +28,14 @@ import os
import time
import uuid
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")
_REGISTRY_FILENAME = "browser-tasks.json"
_KV_NAMESPACE = "p8-browser-tasks"
_KV_KEY = "registry"
class BrowserTransport:
@@ -51,8 +57,15 @@ class BrowserTransport:
raise NotImplementedError
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)
@@ -66,7 +79,35 @@ def _quarantine(path: str) -> None:
def _load_registry(state_dir: str) -> dict:
"""读登记:无文件回空结构;损坏则 quarantine 后回空结构。"""
"""读登记:KV 行缺失时走一次性旧文件导入;行损坏/库打不
开时回空结构(fail-safe,orphans 同款口径;下次成功保存
即覆盖自愈)。"""
try:
with _open_store(state_dir) as store:
raw = store.kv_get(_KV_NAMESPACE, _KV_KEY)
except Exception:
_LOGGER.warning("chv_browser_tasks_store_unreadable "
"state_dir=%s", state_dir)
return {"instances": {}}
if raw is None:
return _import_legacy(state_dir)
try:
data = json.loads(raw)
except ValueError:
_LOGGER.warning("chv_browser_tasks_corrupt state_dir=%s",
state_dir)
return {"instances": {}}
if not isinstance(data, dict) \
or not isinstance(data.get("instances"), dict):
return {"instances": {}}
return data
def _import_legacy(state_dir: str) -> dict:
"""一次性导入旧 browser-tasks.json:合法即进 KV 并把文件
改名 ``.migrated``;无文件回空;损坏则 quarantine 后回空
(旧口径)。导入失败时返回读到的数据、文件保留,下次加
载重试。"""
path = _registry_path(state_dir)
try:
with open(path, "r") as fh:
@@ -80,15 +121,26 @@ def _load_registry(state_dir: str) -> dict:
or not isinstance(data.get("instances"), dict):
_quarantine(path)
return {"instances": {}}
try:
with _open_store(state_dir) as store:
store.kv_put(_KV_NAMESPACE, _KV_KEY, json.dumps(data))
except Exception:
_LOGGER.warning("chv_browser_tasks_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 行;写失败只记日志。"""
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_browser_tasks_save_failed "
"state_dir=%s", state_dir)