104 lines
5.5 KiB
Python
104 lines
5.5 KiB
Python
# ============================================================
|
||
# 审计 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)
|