初始化提交

This commit is contained in:
chenxin committed 2026-08-21 16:00:13 +08:00
commit 887c1f449e
963 files changed
+166869

No files matched your search

@@ -0,0 +1,99 @@
"""审计事件 sink:把沙箱 tool.completed / tool.failed 事件写入 AuditEvent 表(起居注)。
与 EventStoreSink 并列——EventStoreSink 落一炁流程事件(events 表,run 内 sequence),
AuditEventSink 落治理审计(audit_events 表,含 decision / risk_level / argv 脱敏摘要)。
两者经 CompositeEventSink 同时注入 SandboxClient,互不感知。
审计只记终态(completed/failed);started 不记(审计关注决策结果,非中间态)。
argv 经 record_audit_event 的 ``_summary`` → ``redact_text`` + 截断 1000 脱敏,作为
input_summary(审计核心价值:谁执行了什么命令;脱敏打码密钥/token 等敏感模式后保留可读性)。
per-call/per-run 绑定 run_id + character_id(与 EventStoreSink 构造一致);risk_level +
argv 由 started 事件 payload 携带,completed/failed 时消费。
"""
from __future__ import annotations
from typing import Any
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
from app.integrations.sandbox.events import SandboxEvent
from app.services.audit import record_audit_event
class AuditEventSink:
"""把沙箱事件转写为 AuditEvent(审计表),与 EventStoreSink 并列注入。"""
def __init__(
self,
session_factory: async_sessionmaker[AsyncSession],
*,
run_id: str | None = None,
character_id: str | None = None,
) -> None:
self._session_factory = session_factory
self._run_id = run_id
self._character_id = character_id
# 缓存最近一次 started 的上下文(risk_level + argv),completed/failed 写审计时消费。
# sink 为 per-call 实例(随一次 exec 生命周期),状态隔离安全。
self._pending_risk: str | None = None
self._pending_argv: list[str] = []
async def emit(self, event: SandboxEvent) -> None:
etype = event.type
payload = event.payload
if etype == "tool.started":
# 缓存 risk_level + argv(started 携带),completed/failed 写审计时消费
self._pending_risk = str(payload.get("risk_level") or "L1")
self._pending_argv = list(payload.get("argv") or [])
return
if etype not in ("tool.completed", "tool.failed"):
return
tool = str(payload.get("tool") or "")
risk_level = self._pending_risk or "L1"
argv = self._pending_argv
if etype == "tool.completed":
action = "tool.executed"
decision = "allow"
reason: str | None = None
output_summary = self._completed_summary(payload)
else: # tool.failed
action = "tool.failed"
decision = "deny"
reason = str(payload.get("error_code") or "") or None
output_summary = str(payload.get("error_message") or "") or None
# input_summary = argv 空格拼接(record_audit_event 内部 _summary 会 redact + 截断 1000)
input_summary = " ".join(argv) if argv else None
async with self._session_factory() as session:
await record_audit_event(
session,
action=action,
pivot="tool_runtime",
character_id=self._character_id,
run_id=self._run_id,
risk_level=risk_level,
decision=decision,
reason=reason,
input_summary=input_summary,
output_summary=output_summary,
resource_type="tool",
resource_id=tool or None,
trace_id=event.trace_id,
)
await session.commit()
@staticmethod
def _completed_summary(payload: dict[str, Any]) -> str:
parts = [f"exit={payload.get('exit_code')}", f"dur={payload.get('duration_ms')}ms"]
if payload.get("timeout"):
parts.append("timeout")
if payload.get("truncated"):
parts.append("truncated")
return " ".join(parts)