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
@@ -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)
|
||||
|
||||
|
||||
Reference in new issue
Block a user