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