import os import json import time import hashlib import asyncio from pathlib import Path from nats.aio.client import Client as NATS NATS_URL = os.getenv('NATS_URL', 'nats://localhost:4222') AUDIT_FILE = os.getenv('AGX_AUDIT_FILE', 'artifacts/audit.log.jsonl') MAX_BYTES = int(os.getenv('AGX_AUDIT_MAX_BYTES', '4096')) REDACT_KEYS = set(k.strip().lower() for k in os.getenv('AGX_AUDIT_REDACT_KEYS', 'token,secret,password,authorization,cookie,api_key,signature').split(',')) TRACE_ID = os.getenv('AGX_TRACE_ID', '') def _sanitize(obj): if isinstance(obj, dict): return {k: ("[REDACTED]" if k.lower() in REDACT_KEYS else _sanitize(v)) for k, v in obj.items()} if isinstance(obj, list): return [_sanitize(v) for v in obj] return obj def _preview(obj): try: sanitized = _sanitize(obj) raw = json.dumps(sanitized, ensure_ascii=False, sort_keys=True, default=str) size = len(raw.encode("utf-8")) if size > MAX_BYTES: return raw[:MAX_BYTES], True, size return raw, False, size except Exception: return None, True, 0 def _hash(obj) -> str: try: raw = json.dumps(obj, ensure_ascii=False, sort_keys=True, default=str) except Exception: raw = str(obj) return hashlib.sha256(raw.encode()).hexdigest()[:16] async def _publish(subject: str, payload: dict): nc = NATS() await nc.connect(servers=[NATS_URL]) await nc.publish(subject, json.dumps(payload, ensure_ascii=False).encode()) await nc.flush(1) await nc.drain() def audit_tool_call(tool: str, inputs: dict, outputs: dict, success: bool, duration_ms: int): inputs_preview, in_trunc, in_size = _preview(inputs) outputs_preview, out_trunc, out_size = _preview(outputs) event = { 'trace_id': TRACE_ID, 'tool': tool, 'inputs_hash': _hash(inputs), 'outputs_hash': _hash(outputs), 'inputs_preview': inputs_preview, 'outputs_preview': outputs_preview, 'payload_truncated': (in_trunc or out_trunc), 'payload_size': {'inputs': in_size, 'outputs': out_size}, 'success': success, 'duration_ms': duration_ms, 'ts': int(time.time() * 1000) } Path(AUDIT_FILE).parent.mkdir(parents=True, exist_ok=True) with open(AUDIT_FILE, 'a', encoding='utf-8') as f: f.write(json.dumps(event, ensure_ascii=False) + '\n') try: asyncio.run(_publish('agx.audit.toolcall', event)) except Exception: pass