2026-07-21 11:05:57 +08:00
|
|
|
|
# ============================================================
|
|
|
|
|
|
# 审计 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],
|
2026-08-20 11:39:21 +08:00
|
|
|
|
result: str = "SUCCESS", before_snapshot: str | None = None,
|
|
|
|
|
|
evidence_refs: list[str] | tuple[str, ...] = ()) -> dict[str, Any]:
|
2026-07-21 11:05:57 +08:00
|
|
|
|
"""写入一条审计事件(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:
|
|
|
|
|
|
写入的事件对象(含哈希)
|
|
|
|
|
|
"""
|
2026-08-20 11:39:21 +08:00
|
|
|
|
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
|
2026-07-21 11:05:57 +08:00
|
|
|
|
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
|
2026-08-20 11:39:21 +08:00
|
|
|
|
"actorId": actor_id, # Stable authenticated user id
|
2026-07-21 11:05:57 +08:00
|
|
|
|
"category": category, # What(大类)
|
|
|
|
|
|
"action": action, # What(动作)
|
|
|
|
|
|
"target": target, # Where(对象)
|
|
|
|
|
|
"power": power, # 权力等级
|
|
|
|
|
|
"rationale": rationale, # Why(依据)
|
2026-08-20 11:39:21 +08:00
|
|
|
|
"evidenceRefs": list(evidence_refs), # 证据引用(数据/算法/知识版本)
|
|
|
|
|
|
"beforeSnapshot": before_snapshot, # 写前成对快照(回滚锚点)
|
2026-07-21 11:05:57 +08:00
|
|
|
|
"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)
|
2026-08-20 11:39:21 +08:00
|
|
|
|
_mirror_event(event) # 独立介质镜像(开关启用时;尽力而为)
|
2026-07-21 11:05:57 +08:00
|
|
|
|
return event # 返回事件(供响应引用)
|
2026-08-20 11:39:21 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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)
|