aps-agent/server/agent_core/audit.py

104 lines
5.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# ============================================================
# 审计 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)