"""沙箱调用的一炁事件构造与回写接口。 事件结构对齐 docs/architecture/data-and-security-architecture.md:``type / source / payload / trace_id / sensitivity / created_at``。当前一炁事件存储尚未实现,``EventSink`` 用 Protocol 抽象, 默认实现写 loguru 日志;后续接入真实事件存储时实现 ``EventSink`` 注入即可,无需改动客户端。 payload 仅记录摘要(argv、输出长度等),不含密钥、Authorization、签名 URL 或完整输出, 遵循事件 payload 脱敏约定。 """ from __future__ import annotations from dataclasses import dataclass, field from datetime import UTC, datetime from typing import Any, Protocol from app.core.logging import logger @dataclass(frozen=True) class SandboxEvent: """沙箱领域事件(派生自一炁事件契约)。""" type: str # tool.started / tool.completed / tool.failed source: str = "tool" trace_id: str | None = None sensitivity: str = "low" # none / low / medium / high payload: dict[str, Any] = field(default_factory=dict) created_at: str = field(default_factory=lambda: datetime.now(UTC).isoformat()) class EventSink(Protocol): """事件回写接口。""" async def emit(self, event: SandboxEvent) -> None: ... class LoggingEventSink: """默认事件回写:输出到 loguru(结构化字段,不含敏感原文)。""" async def emit(self, event: SandboxEvent) -> None: logger.bind( event_type=event.type, source=event.source, trace_id=event.trace_id, sensitivity=event.sensitivity, payload=event.payload, ).info("sandbox event: {}", event.type) class CompositeEventSink: """扇出到多个 sink:一次 emit 顺序广播给所有子 sink。 供 ToolRuntime 同时落 EventStore(一炁流程事件)与 AuditEvent(治理审计),互不感知。 任一子 sink 抛异常会中断后续 sink——审计写入失败应显式暴露(宁可事件漏写也不静默吞 异常,便于及时发现 DB 问题)。 """ def __init__(self, sinks: list[EventSink]) -> None: self._sinks = sinks async def emit(self, event: SandboxEvent) -> None: for sink in self._sinks: await sink.emit(event) def started_event( *, tool: str, argv: list[str], risk_level: str, trace_id: str | None, run_id: str | None, ) -> SandboxEvent: return SandboxEvent( type="tool.started", trace_id=trace_id, payload={"tool": tool, "risk_level": risk_level, "run_id": run_id, "argv": argv}, ) def completed_event( *, tool: str, trace_id: str | None, run_id: str | None, exit_code: int | None, duration_ms: int, timeout: bool, truncated: bool, stdout_len: int, stderr_len: int, ) -> SandboxEvent: return SandboxEvent( type="tool.completed", trace_id=trace_id, payload={ "tool": tool, "run_id": run_id, "exit_code": exit_code, "duration_ms": duration_ms, "timeout": timeout, "truncated": truncated, "stdout_len": stdout_len, "stderr_len": stderr_len, }, ) def failed_event( *, tool: str, trace_id: str | None, run_id: str | None, error_code: str, error_message: str, ) -> SandboxEvent: return SandboxEvent( type="tool.failed", trace_id=trace_id, payload={ "tool": tool, "run_id": run_id, "error_code": error_code, "error_message": error_message, }, )