Files
2026-08-21 16:00:13 +08:00

128 lines
3.6 KiB
Python
Raw Permalink 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.
"""沙箱调用的一炁事件构造与回写接口。
事件结构对齐 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,
},
)