Skip to content

Commit e737426

Browse files
committed
feat(tools): add safety audit logger with JSONL output
1 parent cf2349a commit e737426

2 files changed

Lines changed: 158 additions & 0 deletions

File tree

tests/tools/safety/test_audit.py

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
# Tencent is pleased to support the open source community by making tRPC-Agent-Python available.
2+
#
3+
# Copyright (C) 2026 Tencent. All rights reserved.
4+
#
5+
# tRPC-Agent-Python is licensed under Apache-2.0.
6+
"""Tests for audit logging."""
7+
8+
import json
9+
import tempfile
10+
from pathlib import Path
11+
12+
from trpc_agent_sdk.tools.safety._audit import SafetyAuditLogger
13+
from trpc_agent_sdk.tools.safety._types import AuditEvent
14+
from trpc_agent_sdk.tools.safety._types import Decision
15+
from trpc_agent_sdk.tools.safety._types import ScanReport
16+
17+
18+
class TestSafetyAuditLogger:
19+
def test_log_writes_valid_jsonl(self):
20+
with tempfile.NamedTemporaryFile(mode="w", suffix=".jsonl", delete=False) as f:
21+
log_path = f.name
22+
try:
23+
logger = SafetyAuditLogger(output_path=log_path)
24+
event = AuditEvent(
25+
timestamp="2026-07-10T12:00:00Z",
26+
tool_name="bash_tool",
27+
decision="deny",
28+
risk_level="critical",
29+
rule_ids=["DANGEROUS_DELETE_001"],
30+
scan_duration_ms=12.5,
31+
sanitized=False,
32+
intercepted=True,
33+
script_hash="abc123",
34+
)
35+
logger.log_event(event)
36+
37+
with open(log_path) as f:
38+
lines = f.readlines()
39+
assert len(lines) == 1
40+
parsed = json.loads(lines[0])
41+
assert parsed["tool_name"] == "bash_tool"
42+
assert parsed["decision"] == "deny"
43+
finally:
44+
Path(log_path).unlink()
45+
46+
def test_log_from_scan_report(self):
47+
with tempfile.NamedTemporaryFile(mode="w", suffix=".jsonl", delete=False) as f:
48+
log_path = f.name
49+
try:
50+
logger = SafetyAuditLogger(output_path=log_path)
51+
report = ScanReport(
52+
decision=Decision.ALLOW,
53+
findings=[],
54+
scan_duration_ms=3.1,
55+
)
56+
logger.log(report, tool_name="python_tool", script_hash="def456")
57+
58+
with open(log_path) as f:
59+
lines = f.readlines()
60+
assert len(lines) == 1
61+
parsed = json.loads(lines[0])
62+
assert parsed["decision"] == "allow"
63+
assert parsed["intercepted"] is False
64+
assert parsed["risk_level"] is None
65+
assert parsed["tool_name"] == "python_tool"
66+
finally:
67+
Path(log_path).unlink()
68+
69+
def test_multiple_events_written(self):
70+
with tempfile.NamedTemporaryFile(mode="w", suffix=".jsonl", delete=False) as f:
71+
log_path = f.name
72+
try:
73+
logger = SafetyAuditLogger(output_path=log_path)
74+
logger.log_event(AuditEvent(
75+
timestamp="2026-07-10T12:00:00Z",
76+
tool_name="tool_a",
77+
decision="allow",
78+
risk_level=None,
79+
rule_ids=[],
80+
scan_duration_ms=1.0,
81+
sanitized=False,
82+
intercepted=False,
83+
script_hash="aaa",
84+
))
85+
logger.log_event(AuditEvent(
86+
timestamp="2026-07-10T12:01:00Z",
87+
tool_name="tool_b",
88+
decision="deny",
89+
risk_level="high",
90+
rule_ids=["NETWORK_PYTHON_004"],
91+
scan_duration_ms=2.0,
92+
sanitized=False,
93+
intercepted=True,
94+
script_hash="bbb",
95+
))
96+
97+
with open(log_path) as f:
98+
lines = f.readlines()
99+
assert len(lines) == 2
100+
parsed_a = json.loads(lines[0])
101+
parsed_b = json.loads(lines[1])
102+
assert parsed_a["tool_name"] == "tool_a"
103+
assert parsed_b["tool_name"] == "tool_b"
104+
finally:
105+
Path(log_path).unlink()
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# Tencent is pleased to support the open source community by making tRPC-Agent-Python available.
2+
#
3+
# Copyright (C) 2026 Tencent. All rights reserved.
4+
#
5+
# tRPC-Agent-Python is licensed under Apache-2.0.
6+
"""Audit logging for the Tool Script Safety Guard.
7+
8+
Writes structured audit events as JSONL (one JSON object per line).
9+
"""
10+
11+
from __future__ import annotations
12+
13+
import json
14+
from dataclasses import asdict
15+
from datetime import datetime
16+
from datetime import timezone
17+
from pathlib import Path
18+
from typing import Optional
19+
20+
from ._types import AuditEvent
21+
from ._types import ScanReport
22+
23+
24+
class SafetyAuditLogger:
25+
"""Logs safety scan events to a JSONL file."""
26+
27+
def __init__(self, output_path: str = "tool_safety_audit.jsonl"):
28+
self.output_path = Path(output_path)
29+
self.output_path.parent.mkdir(parents=True, exist_ok=True)
30+
31+
def log_event(self, event: AuditEvent):
32+
with open(self.output_path, "a", encoding="utf-8") as f:
33+
f.write(json.dumps(asdict(event), ensure_ascii=False) + "\n")
34+
35+
def log(
36+
self,
37+
report: ScanReport,
38+
tool_name: str,
39+
script_hash: str,
40+
sanitized: bool = False,
41+
):
42+
event = AuditEvent(
43+
timestamp=datetime.now(timezone.utc).isoformat(),
44+
tool_name=tool_name,
45+
decision=report.decision.value,
46+
risk_level=report.risk_level.value if report.risk_level else None,
47+
rule_ids=[f.rule_id for f in report.findings],
48+
scan_duration_ms=report.scan_duration_ms,
49+
sanitized=sanitized,
50+
intercepted=(report.decision.value == "deny"),
51+
script_hash=script_hash,
52+
)
53+
self.log_event(event)

0 commit comments

Comments
 (0)