# ============================================================ # 审计 v1(moduleId: core-audit, 可重生 ✅) # plan.md §3.6 的最小实现:5W1H 事件 + 链式哈希(prev_hash → hash) # M1 简化:锚定/WORM/保留策略后置;事件存于世界状态 auditEvents 表 # ============================================================ from __future__ import annotations # 前向类型引用 import hashlib # SHA-256 哈希链 import json # 规范化序列化 from datetime import datetime # 时间戳 from typing import Any # 类型标注 from server.timeutil import fmt_dt # 时间格式化 # 世界状态类型别名 World = dict[str, Any] def write_audit(world: World, next_id, *, actor: str, category: str, action: str, target: dict[str, Any], power: str, rationale: dict[str, Any], result: str = "SUCCESS", before_snapshot: str | None = None, evidence_refs: list[str] | tuple[str, ...] = ()) -> dict[str, Any]: """写入一条审计事件(append-only;调用方负责随后 save)。 权力等级:P0(审计本身只追加,不改业务数据)。 Args: world: 世界状态(事件追加到 auditEvents) next_id: 发号函数 actor: 谁(用户/agent 标识) category: 事件大类(§3.6.2:SESSION/PLAN/MODEL/TOOL/GATE/WORLD_WRITE/...) action: 具体动作名(如 schedule.publish) target: 作用对象 {type, id, ...} power: 权力等级 P0-P3 rationale: 依据 {intent/evidence/confirmId/approver...} result: 结果 SUCCESS/DENIED/FAILED Returns: 写入的事件对象(含哈希) """ actor_id: str | None = None try: from server.auth.context import get_identity identity = get_identity() if identity.user_id: authenticated_actor = identity.username or str(identity.user_id) if actor and actor != authenticated_actor: rationale = {**rationale, "requestedActor": actor} actor = authenticated_actor actor_id = str(identity.user_id) except (ImportError, RuntimeError): pass events = world.setdefault("auditEvents", []) # 审计表(缺失自动补) prev_hash = events[-1]["hash"] if events else "GENESIS" # 链头:上一事件哈希或创世标记 at = fmt_dt(datetime.now()) # 单事件统一时间戳 event: dict[str, Any] = { # 5W1H 事件体(§3.6.1 的 M1 子集) "id": next_id("audit"), # 事件 ID "at": at, # When(前端契约字段) "ts": at, # 兼容旧数据字段 "actor": actor, # Who "actorId": actor_id, # Stable authenticated user id "category": category, # What(大类) "action": action, # What(动作) "target": target, # Where(对象) "power": power, # 权力等级 "rationale": rationale, # Why(依据) "evidenceRefs": list(evidence_refs), # 证据引用(数据/算法/知识版本) "beforeSnapshot": before_snapshot, # 写前成对快照(回滚锚点) "result": result, # 结果 "prevHash": prev_hash, # 链式哈希:前驱 } # 本事件哈希 = SHA256(前驱哈希 ‖ 事件体规范化 JSON)——任何中间篡改都会断链 payload = prev_hash + json.dumps(event, ensure_ascii=False, sort_keys=True) event["hash"] = hashlib.sha256(payload.encode("utf-8")).hexdigest() # 计算并挂载 events.append(event) # 追加入链(append-only) _mirror_event(event) # 独立介质镜像(开关启用时;尽力而为) return event # 返回事件(供响应引用) def _mirror_event(event: dict[str, Any]) -> None: """APS_AUDIT_MIRROR=1 时,把事件同步镜像到独立 append-only JSONL。 尽力而为:整个镜像过程任何异常都不影响业务写(主证据仍在 world + 锚定根)。 """ try: from server.agent_core.audit_mirror import AuditMirror, mirror_enabled if not mirror_enabled(): return try: from server.auth.context import get_identity identity = get_identity() tenant = getattr(identity, "tenant_uuid", "platform") or "platform" except (ImportError, RuntimeError): tenant = "platform" # world_key:有 project store 则取活动项目,否则默认 try: from server.state.projects import get_project_store world_key = get_project_store().active_world_key() except Exception: world_key = "default" AuditMirror(tenant_uuid=tenant, world_key=world_key).append_event(event) except Exception: # 严格尽力而为:绝不阻断业务写 import logging logging.getLogger("audit").warning("audit mirror failed", exc_info=True)