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
+63
-15
@@ -12,11 +12,13 @@
|
||||
- cancelled / 未知终态 -> 只记日志(未知终态 fail-closed,
|
||||
不通知)。
|
||||
|
||||
账本:state_dir/notification-ledger.json(原子写 + 损坏
|
||||
quarantine,orphans 同款口径);同一任务同一原因在节流窗口
|
||||
(3600 秒,本项目自定)内只通知一次。P7 状态存储落地后迁移,
|
||||
本模块 docstring 与差距表已标注;账本丢了只影响去重(可能
|
||||
重复通知一次),不影响任务执行本身。
|
||||
账本:宿主级 AppStateStore(round-12 已迁移,见
|
||||
docs/chv-muse-p8-gap.md §6):state_dir/app-state.sqlite 的
|
||||
``p8-ledger`` 命名空间,一行存整份账本;旧
|
||||
notification-ledger.json 首次加载时自动导入并改名
|
||||
``.migrated``;行损坏/库打不开时回空账本(去重信息丢了只会
|
||||
导致一次重复通知,不影响任务本身),下次成功保存即覆盖自愈。
|
||||
同一任务同一原因在节流窗口(3600 秒,本项目自定)内只通知一次。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -26,13 +28,15 @@ import logging
|
||||
import os
|
||||
import time
|
||||
|
||||
from cloud.chv.atomic_file import atomic_write_text
|
||||
from cloud.chv.app_state import AppStateStore, host_state_db_path
|
||||
|
||||
_LOGGER = logging.getLogger("cloud.cloud_hypervisor")
|
||||
|
||||
# 同一任务同一原因的通知节流窗口(秒,本项目自定)。
|
||||
NOTIFY_THROTTLE_SECONDS = 3600
|
||||
_LEDGER_FILENAME = "notification-ledger.json"
|
||||
_KV_NAMESPACE = "p8-ledger"
|
||||
_KV_KEY = "registry"
|
||||
_MAX_LEDGER_ENTRIES = 500
|
||||
|
||||
|
||||
@@ -71,8 +75,15 @@ def _evaluate_completed(result: dict) -> dict:
|
||||
return {"present": False, "reason": "routine_completed"}
|
||||
|
||||
|
||||
def _open_store(state_dir: str) -> AppStateStore:
|
||||
"""打开宿主级状态库(每次调用新建连接,用完即关)。"""
|
||||
store = AppStateStore(host_state_db_path(state_dir))
|
||||
store.open()
|
||||
return store
|
||||
|
||||
|
||||
def _ledger_path(state_dir: str) -> str:
|
||||
"""通知账本文件路径。"""
|
||||
"""旧版通知账本文件路径(只用于一次性迁移导入)。"""
|
||||
return os.path.join(state_dir, _LEDGER_FILENAME)
|
||||
|
||||
|
||||
@@ -85,8 +96,34 @@ def _quarantine(path: str) -> None:
|
||||
|
||||
|
||||
def _load_ledger(state_dir: str) -> dict:
|
||||
"""读账本:无文件回空结构;损坏则 quarantine 后回空结构
|
||||
(去重信息丢了只会导致一次重复通知,不影响任务本身)。"""
|
||||
"""读账本:KV 行缺失时走一次性旧文件导入;行损坏/库打不
|
||||
开时回空结构(去重信息丢了只会导致一次重复通知,不影响
|
||||
任务本身;下次成功保存即覆盖自愈)。"""
|
||||
try:
|
||||
with _open_store(state_dir) as store:
|
||||
raw = store.kv_get(_KV_NAMESPACE, _KV_KEY)
|
||||
except Exception:
|
||||
_LOGGER.warning("chv_ledger_store_unreadable state_dir=%s",
|
||||
state_dir)
|
||||
return {"notified": {}}
|
||||
if raw is None:
|
||||
return _import_legacy(state_dir)
|
||||
try:
|
||||
data = json.loads(raw)
|
||||
except ValueError:
|
||||
_LOGGER.warning("chv_ledger_corrupt state_dir=%s", state_dir)
|
||||
return {"notified": {}}
|
||||
if not isinstance(data, dict) \
|
||||
or not isinstance(data.get("notified"), dict):
|
||||
return {"notified": {}}
|
||||
return data
|
||||
|
||||
|
||||
def _import_legacy(state_dir: str) -> dict:
|
||||
"""一次性导入旧 notification-ledger.json:合法即进 KV 并
|
||||
把文件改名 ``.migrated``;无文件回空;损坏则 quarantine
|
||||
后回空(旧口径)。导入失败时返回读到的数据、文件保留,
|
||||
下次加载重试。"""
|
||||
path = _ledger_path(state_dir)
|
||||
try:
|
||||
with open(path, "r") as fh:
|
||||
@@ -100,19 +137,30 @@ def _load_ledger(state_dir: str) -> dict:
|
||||
or not isinstance(data.get("notified"), dict):
|
||||
_quarantine(path)
|
||||
return {"notified": {}}
|
||||
try:
|
||||
with _open_store(state_dir) as store:
|
||||
store.kv_put(_KV_NAMESPACE, _KV_KEY, json.dumps(data))
|
||||
except Exception:
|
||||
_LOGGER.warning("chv_ledger_import_failed state_dir=%s",
|
||||
state_dir)
|
||||
return data
|
||||
try:
|
||||
os.replace(path, path + ".migrated")
|
||||
except OSError:
|
||||
pass
|
||||
return data
|
||||
|
||||
|
||||
def _save_ledger(state_dir: str, data: dict) -> None:
|
||||
"""原子落盘账本(截断到上限);写失败只记日志。"""
|
||||
"""落盘账本到 KV 行(截断到上限);写失败只记日志。"""
|
||||
notified = data.get("notified", {})
|
||||
if len(notified) > _MAX_LEDGER_ENTRIES:
|
||||
items = sorted(notified.items(), key=lambda kv: kv[1])
|
||||
data["notified"] = dict(items[-_MAX_LEDGER_ENTRIES:])
|
||||
try:
|
||||
os.makedirs(state_dir, exist_ok=True)
|
||||
atomic_write_text(_ledger_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_ledger_save_failed state_dir=%s", state_dir)
|
||||
|
||||
|
||||
@@ -120,8 +168,8 @@ class NotificationLedger:
|
||||
"""通知去重节流账本(每任务每原因节流)。
|
||||
|
||||
should_notify(task_id, reason) 在窗口内未通知过时回 True
|
||||
并记下这次通知;窗口内已通知过回 False。账本先走原子
|
||||
JSON 文件,待迁移到 P7 状态存储(见模块 docstring)。
|
||||
并记下这次通知;窗口内已通知过回 False。账本落在宿主级
|
||||
AppStateStore(round-12 已迁移,见模块 docstring)。
|
||||
"""
|
||||
|
||||
def __init__(self, state_dir: str) -> None:
|
||||
|
||||
Reference in new issue
Block a user