agent_os_kernel 3.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (337) hide show
  1. agent_control_plane/__init__.py +662 -0
  2. agent_control_plane/a2a_adapter.py +543 -0
  3. agent_control_plane/adapter.py +417 -0
  4. agent_control_plane/agent_hibernation.py +394 -0
  5. agent_control_plane/agent_kernel.py +470 -0
  6. agent_control_plane/compliance.py +720 -0
  7. agent_control_plane/constraint_graphs.py +478 -0
  8. agent_control_plane/control_plane.py +854 -0
  9. agent_control_plane/example_executors.py +195 -0
  10. agent_control_plane/execution_engine.py +231 -0
  11. agent_control_plane/flight_recorder.py +846 -0
  12. agent_control_plane/governance_layer.py +435 -0
  13. agent_control_plane/hf_utils.py +563 -0
  14. agent_control_plane/interfaces/__init__.py +55 -0
  15. agent_control_plane/interfaces/kernel_interface.py +361 -0
  16. agent_control_plane/interfaces/plugin_interface.py +497 -0
  17. agent_control_plane/interfaces/protocol_interfaces.py +387 -0
  18. agent_control_plane/kernel_space.py +1009 -0
  19. agent_control_plane/langchain_adapter.py +424 -0
  20. agent_control_plane/lifecycle.py +3113 -0
  21. agent_control_plane/mcp_adapter.py +653 -0
  22. agent_control_plane/ml_safety.py +563 -0
  23. agent_control_plane/multimodal.py +727 -0
  24. agent_control_plane/mute_agent.py +422 -0
  25. agent_control_plane/observability.py +787 -0
  26. agent_control_plane/orchestrator.py +482 -0
  27. agent_control_plane/plugin_registry.py +750 -0
  28. agent_control_plane/policy_engine.py +954 -0
  29. agent_control_plane/process_isolation.py +777 -0
  30. agent_control_plane/shadow_mode.py +310 -0
  31. agent_control_plane/signals.py +493 -0
  32. agent_control_plane/supervisor_agents.py +430 -0
  33. agent_control_plane/time_travel_debugger.py +557 -0
  34. agent_control_plane/tool_registry.py +452 -0
  35. agent_control_plane/vfs.py +697 -0
  36. agent_kernel/__init__.py +69 -0
  37. agent_kernel/analyzer.py +435 -0
  38. agent_kernel/auditor.py +36 -0
  39. agent_kernel/completeness_auditor.py +237 -0
  40. agent_kernel/detector.py +203 -0
  41. agent_kernel/kernel.py +744 -0
  42. agent_kernel/memory_manager.py +85 -0
  43. agent_kernel/models.py +374 -0
  44. agent_kernel/nudge_mechanism.py +263 -0
  45. agent_kernel/outcome_analyzer.py +338 -0
  46. agent_kernel/patcher.py +582 -0
  47. agent_kernel/semantic_analyzer.py +316 -0
  48. agent_kernel/semantic_purge.py +349 -0
  49. agent_kernel/simulator.py +449 -0
  50. agent_kernel/teacher.py +85 -0
  51. agent_kernel/triage.py +152 -0
  52. agent_os/__init__.py +409 -0
  53. agent_os/_adversarial_impl.py +200 -0
  54. agent_os/_circuit_breaker_impl.py +232 -0
  55. agent_os/_mcp_metrics.py +193 -0
  56. agent_os/adversarial.py +20 -0
  57. agent_os/agents_compat.py +490 -0
  58. agent_os/audit_logger.py +135 -0
  59. agent_os/base_agent.py +651 -0
  60. agent_os/circuit_breaker.py +34 -0
  61. agent_os/cli/__init__.py +659 -0
  62. agent_os/cli/cmd_audit.py +128 -0
  63. agent_os/cli/cmd_init.py +152 -0
  64. agent_os/cli/cmd_policy.py +41 -0
  65. agent_os/cli/cmd_policy_gen.py +180 -0
  66. agent_os/cli/cmd_validate.py +258 -0
  67. agent_os/cli/mcp_scan.py +265 -0
  68. agent_os/cli/output.py +192 -0
  69. agent_os/cli/policy_checker.py +330 -0
  70. agent_os/compat.py +74 -0
  71. agent_os/constraint_graph.py +234 -0
  72. agent_os/content_governance.py +140 -0
  73. agent_os/context_budget.py +305 -0
  74. agent_os/credential_redactor.py +224 -0
  75. agent_os/diff_policy.py +89 -0
  76. agent_os/egress_policy.py +159 -0
  77. agent_os/escalation.py +276 -0
  78. agent_os/event_bus.py +124 -0
  79. agent_os/exceptions.py +180 -0
  80. agent_os/execution_context_policy.py +141 -0
  81. agent_os/github_enterprise.py +96 -0
  82. agent_os/health.py +20 -0
  83. agent_os/integrations/__init__.py +279 -0
  84. agent_os/integrations/a2a_adapter.py +279 -0
  85. agent_os/integrations/agent_lightning/__init__.py +30 -0
  86. agent_os/integrations/anthropic_adapter.py +420 -0
  87. agent_os/integrations/autogen_adapter.py +620 -0
  88. agent_os/integrations/base.py +1137 -0
  89. agent_os/integrations/compat.py +229 -0
  90. agent_os/integrations/config.py +98 -0
  91. agent_os/integrations/conversation_guardian.py +957 -0
  92. agent_os/integrations/crewai_adapter.py +467 -0
  93. agent_os/integrations/drift_detector.py +425 -0
  94. agent_os/integrations/dry_run.py +124 -0
  95. agent_os/integrations/escalation.py +582 -0
  96. agent_os/integrations/gemini_adapter.py +364 -0
  97. agent_os/integrations/google_adk_adapter.py +633 -0
  98. agent_os/integrations/guardrails_adapter.py +394 -0
  99. agent_os/integrations/health.py +197 -0
  100. agent_os/integrations/langchain_adapter.py +654 -0
  101. agent_os/integrations/llamafirewall.py +343 -0
  102. agent_os/integrations/llamaindex_adapter.py +188 -0
  103. agent_os/integrations/logging.py +191 -0
  104. agent_os/integrations/maf_adapter.py +631 -0
  105. agent_os/integrations/mistral_adapter.py +365 -0
  106. agent_os/integrations/openai_adapter.py +816 -0
  107. agent_os/integrations/openai_agents_sdk.py +406 -0
  108. agent_os/integrations/policy_compose.py +171 -0
  109. agent_os/integrations/profiling.py +144 -0
  110. agent_os/integrations/pydantic_ai_adapter.py +420 -0
  111. agent_os/integrations/rate_limiter.py +130 -0
  112. agent_os/integrations/rbac.py +143 -0
  113. agent_os/integrations/registry.py +113 -0
  114. agent_os/integrations/scope_guard.py +303 -0
  115. agent_os/integrations/semantic_kernel_adapter.py +769 -0
  116. agent_os/integrations/smolagents_adapter.py +629 -0
  117. agent_os/integrations/templates.py +178 -0
  118. agent_os/integrations/token_budget.py +134 -0
  119. agent_os/integrations/tool_aliases.py +190 -0
  120. agent_os/integrations/webhooks.py +177 -0
  121. agent_os/lite.py +208 -0
  122. agent_os/mcp_gateway.py +385 -0
  123. agent_os/mcp_message_signer.py +273 -0
  124. agent_os/mcp_protocols.py +161 -0
  125. agent_os/mcp_response_scanner.py +232 -0
  126. agent_os/mcp_security.py +924 -0
  127. agent_os/mcp_session_auth.py +231 -0
  128. agent_os/mcp_sliding_rate_limiter.py +184 -0
  129. agent_os/memory_guard.py +409 -0
  130. agent_os/metrics.py +134 -0
  131. agent_os/mute.py +428 -0
  132. agent_os/mute_agent.py +209 -0
  133. agent_os/policies/__init__.py +77 -0
  134. agent_os/policies/async_evaluator.py +275 -0
  135. agent_os/policies/backends.py +670 -0
  136. agent_os/policies/bridge.py +169 -0
  137. agent_os/policies/budget.py +85 -0
  138. agent_os/policies/cli.py +294 -0
  139. agent_os/policies/conflict_resolution.py +270 -0
  140. agent_os/policies/data_classification.py +252 -0
  141. agent_os/policies/evaluator.py +239 -0
  142. agent_os/policies/policy_schema.json +228 -0
  143. agent_os/policies/rate_limiting.py +145 -0
  144. agent_os/policies/schema.py +115 -0
  145. agent_os/policies/shared.py +331 -0
  146. agent_os/prompt_injection.py +694 -0
  147. agent_os/providers.py +182 -0
  148. agent_os/py.typed +0 -0
  149. agent_os/retry.py +81 -0
  150. agent_os/reversibility.py +251 -0
  151. agent_os/sandbox.py +432 -0
  152. agent_os/sandbox_provider.py +140 -0
  153. agent_os/secure_codegen.py +525 -0
  154. agent_os/security_skills.py +538 -0
  155. agent_os/semantic_policy.py +422 -0
  156. agent_os/server/__init__.py +15 -0
  157. agent_os/server/__main__.py +25 -0
  158. agent_os/server/app.py +277 -0
  159. agent_os/server/models.py +104 -0
  160. agent_os/shift_left_metrics.py +130 -0
  161. agent_os/stateless.py +742 -0
  162. agent_os/supervisor.py +148 -0
  163. agent_os/task_outcome.py +148 -0
  164. agent_os/transparency.py +181 -0
  165. agent_os/trust_root.py +128 -0
  166. agent_os_kernel-3.1.0.dist-info/METADATA +1269 -0
  167. agent_os_kernel-3.1.0.dist-info/RECORD +337 -0
  168. agent_os_kernel-3.1.0.dist-info/WHEEL +4 -0
  169. agent_os_kernel-3.1.0.dist-info/entry_points.txt +2 -0
  170. agent_os_kernel-3.1.0.dist-info/licenses/LICENSE +21 -0
  171. agent_os_observability/__init__.py +27 -0
  172. agent_os_observability/dashboards.py +898 -0
  173. agent_os_observability/metrics.py +398 -0
  174. agent_os_observability/server.py +223 -0
  175. agent_os_observability/tracer.py +232 -0
  176. agent_primitives/__init__.py +24 -0
  177. agent_primitives/failures.py +84 -0
  178. agent_primitives/py.typed +0 -0
  179. amb_core/__init__.py +177 -0
  180. amb_core/adapters/__init__.py +57 -0
  181. amb_core/adapters/aws_sqs_broker.py +376 -0
  182. amb_core/adapters/azure_servicebus_broker.py +340 -0
  183. amb_core/adapters/kafka_broker.py +260 -0
  184. amb_core/adapters/nats_broker.py +285 -0
  185. amb_core/adapters/rabbitmq_broker.py +235 -0
  186. amb_core/adapters/redis_broker.py +262 -0
  187. amb_core/broker.py +145 -0
  188. amb_core/bus.py +481 -0
  189. amb_core/cloudevents.py +509 -0
  190. amb_core/dlq.py +345 -0
  191. amb_core/hf_utils.py +536 -0
  192. amb_core/memory_broker.py +410 -0
  193. amb_core/models.py +141 -0
  194. amb_core/persistence.py +529 -0
  195. amb_core/schema.py +294 -0
  196. amb_core/tracing.py +358 -0
  197. atr/__init__.py +640 -0
  198. atr/access.py +348 -0
  199. atr/composition.py +645 -0
  200. atr/decorator.py +357 -0
  201. atr/executor.py +384 -0
  202. atr/health.py +557 -0
  203. atr/hf_utils.py +449 -0
  204. atr/injection.py +422 -0
  205. atr/metrics.py +440 -0
  206. atr/policies.py +403 -0
  207. atr/py.typed +2 -0
  208. atr/registry.py +452 -0
  209. atr/schema.py +480 -0
  210. atr/tools/safe/__init__.py +75 -0
  211. atr/tools/safe/calculator.py +467 -0
  212. atr/tools/safe/datetime_tool.py +443 -0
  213. atr/tools/safe/file_reader.py +402 -0
  214. atr/tools/safe/http_client.py +316 -0
  215. atr/tools/safe/json_parser.py +374 -0
  216. atr/tools/safe/text_tool.py +537 -0
  217. atr/tools/safe/toolkit.py +175 -0
  218. caas/__init__.py +162 -0
  219. caas/api/__init__.py +7 -0
  220. caas/api/server.py +1328 -0
  221. caas/caching.py +834 -0
  222. caas/cli.py +210 -0
  223. caas/conversation.py +223 -0
  224. caas/decay.py +72 -0
  225. caas/detection/__init__.py +9 -0
  226. caas/detection/detector.py +238 -0
  227. caas/enrichment.py +130 -0
  228. caas/gateway/__init__.py +27 -0
  229. caas/gateway/trust_gateway.py +474 -0
  230. caas/hf_utils.py +479 -0
  231. caas/ingestion/__init__.py +23 -0
  232. caas/ingestion/processors.py +253 -0
  233. caas/ingestion/structure_parser.py +188 -0
  234. caas/models.py +356 -0
  235. caas/pragmatic_truth.py +444 -0
  236. caas/routing/__init__.py +10 -0
  237. caas/routing/heuristic_router.py +58 -0
  238. caas/storage/__init__.py +9 -0
  239. caas/storage/store.py +389 -0
  240. caas/triad.py +213 -0
  241. caas/tuning/__init__.py +9 -0
  242. caas/tuning/tuner.py +329 -0
  243. caas/vfs/__init__.py +14 -0
  244. caas/vfs/filesystem.py +452 -0
  245. cmvk/__init__.py +218 -0
  246. cmvk/audit.py +402 -0
  247. cmvk/benchmarks.py +478 -0
  248. cmvk/constitutional.py +904 -0
  249. cmvk/hf_utils.py +301 -0
  250. cmvk/metrics.py +473 -0
  251. cmvk/profiles.py +300 -0
  252. cmvk/py.typed +0 -0
  253. cmvk/types.py +12 -0
  254. cmvk/verification.py +956 -0
  255. emk/__init__.py +89 -0
  256. emk/causal.py +352 -0
  257. emk/hf_utils.py +421 -0
  258. emk/indexer.py +83 -0
  259. emk/py.typed +0 -0
  260. emk/schema.py +204 -0
  261. emk/sleep_cycle.py +347 -0
  262. emk/store.py +281 -0
  263. iatp/__init__.py +166 -0
  264. iatp/attestation.py +461 -0
  265. iatp/cli.py +317 -0
  266. iatp/hf_utils.py +472 -0
  267. iatp/ipc_pipes.py +580 -0
  268. iatp/main.py +412 -0
  269. iatp/models/__init__.py +447 -0
  270. iatp/policy_engine.py +337 -0
  271. iatp/py.typed +2 -0
  272. iatp/recovery.py +321 -0
  273. iatp/security/__init__.py +270 -0
  274. iatp/sidecar/__init__.py +519 -0
  275. iatp/telemetry/__init__.py +164 -0
  276. iatp/tests/__init__.py +1 -0
  277. iatp/tests/test_attestation.py +370 -0
  278. iatp/tests/test_cli.py +131 -0
  279. iatp/tests/test_ed25519_attestation.py +211 -0
  280. iatp/tests/test_models.py +130 -0
  281. iatp/tests/test_policy_engine.py +347 -0
  282. iatp/tests/test_recovery.py +281 -0
  283. iatp/tests/test_security.py +222 -0
  284. iatp/tests/test_sidecar.py +167 -0
  285. iatp/tests/test_telemetry.py +175 -0
  286. mcp_kernel_server/__init__.py +28 -0
  287. mcp_kernel_server/cli.py +274 -0
  288. mcp_kernel_server/resources.py +217 -0
  289. mcp_kernel_server/server.py +564 -0
  290. mcp_kernel_server/tools.py +1174 -0
  291. mute_agent/__init__.py +68 -0
  292. mute_agent/core/__init__.py +1 -0
  293. mute_agent/core/execution_agent.py +166 -0
  294. mute_agent/core/handshake_protocol.py +201 -0
  295. mute_agent/core/reasoning_agent.py +238 -0
  296. mute_agent/knowledge_graph/__init__.py +1 -0
  297. mute_agent/knowledge_graph/graph_elements.py +65 -0
  298. mute_agent/knowledge_graph/multidimensional_graph.py +170 -0
  299. mute_agent/knowledge_graph/subgraph.py +224 -0
  300. mute_agent/listener/__init__.py +43 -0
  301. mute_agent/listener/adapters/__init__.py +31 -0
  302. mute_agent/listener/adapters/base_adapter.py +189 -0
  303. mute_agent/listener/adapters/caas_adapter.py +344 -0
  304. mute_agent/listener/adapters/control_plane_adapter.py +436 -0
  305. mute_agent/listener/adapters/iatp_adapter.py +332 -0
  306. mute_agent/listener/adapters/scak_adapter.py +251 -0
  307. mute_agent/listener/listener.py +610 -0
  308. mute_agent/listener/state_observer.py +436 -0
  309. mute_agent/listener/threshold_config.py +313 -0
  310. mute_agent/super_system/__init__.py +1 -0
  311. mute_agent/super_system/router.py +204 -0
  312. mute_agent/visualization/__init__.py +10 -0
  313. mute_agent/visualization/graph_debugger.py +502 -0
  314. nexus/README.md +60 -0
  315. nexus/__init__.py +51 -0
  316. nexus/arbiter.py +359 -0
  317. nexus/client.py +466 -0
  318. nexus/dmz.py +444 -0
  319. nexus/escrow.py +430 -0
  320. nexus/exceptions.py +286 -0
  321. nexus/pyproject.toml +36 -0
  322. nexus/registry.py +393 -0
  323. nexus/reputation.py +425 -0
  324. nexus/schemas/__init__.py +51 -0
  325. nexus/schemas/compliance.py +276 -0
  326. nexus/schemas/escrow.py +251 -0
  327. nexus/schemas/manifest.py +225 -0
  328. nexus/schemas/receipt.py +208 -0
  329. nexus/tests/__init__.py +0 -0
  330. nexus/tests/conftest.py +146 -0
  331. nexus/tests/test_arbiter.py +192 -0
  332. nexus/tests/test_dmz.py +194 -0
  333. nexus/tests/test_escrow.py +276 -0
  334. nexus/tests/test_exceptions.py +225 -0
  335. nexus/tests/test_registry.py +232 -0
  336. nexus/tests/test_reputation.py +328 -0
  337. nexus/tests/test_schemas.py +295 -0
@@ -0,0 +1,846 @@
1
+ # Copyright (c) Microsoft Corporation.
2
+ # Licensed under the MIT License.
3
+
4
+ """
5
+ Flight Recorder - Black Box Audit Logger for Agent Control Plane
6
+
7
+ This module provides SQLite-based audit logging for all agent actions,
8
+ capturing the exact state for forensic analysis and compliance.
9
+
10
+ Performance optimizations:
11
+ - WAL mode for concurrent reads during writes
12
+ - Batched writes with configurable flush interval
13
+ - Connection pooling to reduce overhead
14
+
15
+ Security features:
16
+ - Merkle chain for tamper detection
17
+ - Hash verification on reads
18
+ """
19
+
20
+ import sqlite3
21
+ import uuid
22
+ import hashlib
23
+ import threading
24
+ import atexit
25
+ from typing import Dict, Any, Optional, List
26
+ from datetime import datetime
27
+ from pathlib import Path
28
+ from collections import deque
29
+ import json
30
+ import logging
31
+
32
+
33
+ class FlightRecorder:
34
+ """The Black Box Recorder for AI Agents.
35
+
36
+ Logs every action attempt with full context for forensic analysis.
37
+ Similar to an aircraft's flight data recorder, this captures:
38
+
39
+ - **Timestamp**: When the action was attempted
40
+ - **AgentID**: Which agent attempted it
41
+ - **InputPrompt**: The original user/agent intent
42
+ - **IntendedAction**: What the agent tried to do
43
+ - **PolicyVerdict**: Whether it was allowed or blocked
44
+ - **Result**: What actually happened
45
+
46
+ Performance features:
47
+ - WAL mode for better concurrent performance
48
+ - Batched writes (configurable ``batch_size`` and ``flush_interval``)
49
+ - Connection reuse within threads via thread-local storage
50
+
51
+ Security features:
52
+ - Merkle chain: Each entry includes SHA-256 hash of previous entry
53
+ - Tamper detection: ``verify_integrity()`` checks the full hash chain
54
+
55
+ Example:
56
+ Basic recording workflow::
57
+
58
+ recorder = FlightRecorder(db_path="audit.db")
59
+
60
+ # Start a trace before executing a tool
61
+ trace_id = recorder.start_trace(
62
+ agent_id="agent-001",
63
+ tool_name="web_search",
64
+ tool_args={"query": "latest news"},
65
+ input_prompt="Find me today's headlines",
66
+ )
67
+
68
+ # Log the outcome
69
+ recorder.log_success(trace_id, result="Found 10 articles", execution_time_ms=152.3)
70
+
71
+ # Query the audit log
72
+ violations = recorder.query_logs(policy_verdict="blocked")
73
+
74
+ # Verify tamper-proof integrity
75
+ integrity = recorder.verify_integrity()
76
+ assert integrity["valid"]
77
+
78
+ recorder.close()
79
+ """
80
+
81
+ def __init__(
82
+ self,
83
+ db_path: str = "flight_recorder.db",
84
+ batch_size: int = 100,
85
+ flush_interval_seconds: float = 5.0,
86
+ enable_batching: bool = True
87
+ ):
88
+ """Initialize the Flight Recorder.
89
+
90
+ Sets up the SQLite database with WAL mode, creates the audit log
91
+ schema if it doesn't exist, and restores the Merkle hash chain
92
+ from the last recorded entry.
93
+
94
+ Args:
95
+ db_path: Path to the SQLite database file. The file is created
96
+ if it does not exist. Defaults to ``"flight_recorder.db"``.
97
+ batch_size: Number of write operations to buffer before
98
+ committing to disk. Larger values improve throughput at the
99
+ cost of increased memory usage. Defaults to ``100``.
100
+ flush_interval_seconds: Maximum number of seconds between
101
+ automatic flushes, regardless of buffer size. Defaults to
102
+ ``5.0``.
103
+ enable_batching: When ``False``, every write is committed
104
+ immediately (legacy behaviour). Defaults to ``True``.
105
+
106
+ Raises:
107
+ sqlite3.OperationalError: If the database file cannot be opened
108
+ or the schema migration fails.
109
+ """
110
+ self.db_path = db_path
111
+ self.logger = logging.getLogger("FlightRecorder")
112
+ self.batch_size = batch_size
113
+ self.flush_interval = flush_interval_seconds
114
+ self.enable_batching = enable_batching
115
+
116
+ # Batching state
117
+ self._write_buffer: deque = deque()
118
+ self._buffer_lock = threading.Lock()
119
+ self._last_flush = datetime.utcnow()
120
+ self._last_hash: Optional[str] = None
121
+
122
+ # Cache immutable trace data for content hash recomputation
123
+ self._trace_data: Dict[str, Dict[str, Any]] = {}
124
+
125
+ # Thread-local connections for better performance
126
+ self._local = threading.local()
127
+
128
+ self._init_database()
129
+
130
+ # Register cleanup on exit
131
+ atexit.register(self._flush_and_close)
132
+
133
+ def _get_connection(self) -> sqlite3.Connection:
134
+ """Get a thread-local database connection with WAL mode."""
135
+ if not hasattr(self._local, 'conn') or self._local.conn is None:
136
+ conn = sqlite3.connect(self.db_path, check_same_thread=False)
137
+ # Enable WAL mode for better concurrent performance
138
+ conn.execute("PRAGMA journal_mode=WAL")
139
+ conn.execute("PRAGMA synchronous=NORMAL") # Good balance of safety/speed
140
+ conn.execute("PRAGMA cache_size=-64000") # 64MB cache
141
+ self._local.conn = conn
142
+ return self._local.conn
143
+
144
+ def _compute_hash(self, data: str, previous_hash: Optional[str] = None) -> str:
145
+ """Compute SHA256 hash for Merkle chain."""
146
+ content = f"{previous_hash or 'genesis'}:{data}"
147
+ return hashlib.sha256(content.encode()).hexdigest()
148
+
149
+ def _compute_content_hash(
150
+ self,
151
+ trace_id: str,
152
+ timestamp: str,
153
+ agent_id: str,
154
+ tool_name: str,
155
+ tool_args: Optional[str],
156
+ policy_verdict: str,
157
+ violation_reason: Optional[str] = None,
158
+ result: Optional[str] = None,
159
+ execution_time_ms: Optional[float] = None,
160
+ ) -> str:
161
+ """Compute SHA-256 hash over all substantive fields for tamper detection.
162
+
163
+ Unlike ``_compute_hash`` which builds the Merkle chain and is set
164
+ once at INSERT time, the content hash is recomputed whenever the
165
+ row is updated (e.g. when a verdict changes from *pending* to
166
+ *allowed*). ``verify_integrity`` uses this hash to detect
167
+ post-hoc field tampering.
168
+ """
169
+ data = (
170
+ f"{trace_id}:{timestamp}:{agent_id}:{tool_name}:{tool_args}"
171
+ f":{policy_verdict}:{violation_reason}:{result}:{execution_time_ms}"
172
+ )
173
+ return hashlib.sha256(data.encode()).hexdigest()
174
+
175
+ def _recompute_content_hash(
176
+ self,
177
+ trace_id: str,
178
+ policy_verdict: str,
179
+ violation_reason: Optional[str] = None,
180
+ result: Optional[str] = None,
181
+ execution_time_ms: Optional[float] = None,
182
+ ) -> Optional[str]:
183
+ """Recompute content hash using cached trace data.
184
+
185
+ Looks up the immutable fields (timestamp, agent_id, etc.) from the
186
+ in-memory ``_trace_data`` cache populated at ``start_trace`` time,
187
+ then delegates to ``_compute_content_hash`` with the updated
188
+ mutable fields. Returns ``None`` if the trace_id is not in the
189
+ cache (e.g. recorder was re-instantiated between start and log).
190
+ """
191
+ trace = self._trace_data.get(trace_id)
192
+ if trace is None:
193
+ return None
194
+ return self._compute_content_hash(
195
+ trace_id,
196
+ trace['timestamp'],
197
+ trace['agent_id'],
198
+ trace['tool_name'],
199
+ trace['tool_args_json'],
200
+ policy_verdict,
201
+ violation_reason=violation_reason,
202
+ result=result,
203
+ execution_time_ms=execution_time_ms,
204
+ )
205
+
206
+ def _flush_buffer(self):
207
+ """Flush pending writes to database."""
208
+ with self._buffer_lock:
209
+ if not self._write_buffer:
210
+ return
211
+
212
+ conn = self._get_connection()
213
+ cursor = conn.cursor()
214
+
215
+ try:
216
+ while self._write_buffer:
217
+ operation = self._write_buffer.popleft()
218
+ cursor.execute(operation['sql'], operation['params'])
219
+
220
+ conn.commit()
221
+ self._last_flush = datetime.utcnow()
222
+ self.logger.debug(f"Flushed write buffer")
223
+ except Exception as e:
224
+ conn.rollback()
225
+ self.logger.error(f"Failed to flush buffer: {e}")
226
+ raise
227
+
228
+ def _maybe_flush(self):
229
+ """Flush if batch size reached or interval exceeded."""
230
+ if not self.enable_batching:
231
+ self._flush_buffer()
232
+ return
233
+
234
+ should_flush = (
235
+ len(self._write_buffer) >= self.batch_size or
236
+ (datetime.utcnow() - self._last_flush).total_seconds() >= self.flush_interval
237
+ )
238
+ if should_flush:
239
+ self._flush_buffer()
240
+
241
+ def _queue_write(self, sql: str, params: tuple):
242
+ """Queue a write operation."""
243
+ with self._buffer_lock:
244
+ self._write_buffer.append({'sql': sql, 'params': params})
245
+ self._maybe_flush()
246
+
247
+ def _flush_and_close(self):
248
+ """Flush buffer and close connections on exit."""
249
+ try:
250
+ self._flush_buffer()
251
+ if hasattr(self._local, 'conn') and self._local.conn:
252
+ self._local.conn.close()
253
+ self._local.conn = None
254
+ except Exception as e:
255
+ self.logger.error(f"Error during cleanup: {e}")
256
+
257
+ def _init_database(self):
258
+ """Initialize the SQLite database schema with WAL mode."""
259
+ conn = sqlite3.connect(self.db_path)
260
+ cursor = conn.cursor()
261
+
262
+ # Enable WAL mode for concurrent reads during writes
263
+ cursor.execute("PRAGMA journal_mode=WAL")
264
+ cursor.execute("PRAGMA synchronous=NORMAL")
265
+
266
+ # Create the main audit log table with hash column for Merkle chain
267
+ cursor.execute(
268
+ """
269
+ CREATE TABLE IF NOT EXISTS audit_log (
270
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
271
+ trace_id TEXT UNIQUE NOT NULL,
272
+ timestamp TEXT NOT NULL,
273
+ agent_id TEXT NOT NULL,
274
+ tool_name TEXT NOT NULL,
275
+ tool_args TEXT,
276
+ input_prompt TEXT,
277
+ policy_verdict TEXT NOT NULL,
278
+ violation_reason TEXT,
279
+ result TEXT,
280
+ execution_time_ms REAL,
281
+ metadata TEXT,
282
+ entry_hash TEXT,
283
+ previous_hash TEXT,
284
+ content_hash TEXT
285
+ )
286
+ """
287
+ )
288
+
289
+ # Add hash columns if they don't exist (migration for existing DBs)
290
+ try:
291
+ cursor.execute("ALTER TABLE audit_log ADD COLUMN entry_hash TEXT")
292
+ except sqlite3.OperationalError:
293
+ pass # Column already exists
294
+ try:
295
+ cursor.execute("ALTER TABLE audit_log ADD COLUMN previous_hash TEXT")
296
+ except sqlite3.OperationalError:
297
+ pass # Column already exists
298
+ try:
299
+ cursor.execute("ALTER TABLE audit_log ADD COLUMN content_hash TEXT")
300
+ except sqlite3.OperationalError:
301
+ pass # Column already exists
302
+
303
+ # Create indexes for common queries
304
+ cursor.execute(
305
+ """
306
+ CREATE INDEX IF NOT EXISTS idx_agent_id ON audit_log(agent_id)
307
+ """
308
+ )
309
+ cursor.execute(
310
+ """
311
+ CREATE INDEX IF NOT EXISTS idx_timestamp ON audit_log(timestamp)
312
+ """
313
+ )
314
+ cursor.execute(
315
+ """
316
+ CREATE INDEX IF NOT EXISTS idx_policy_verdict ON audit_log(policy_verdict)
317
+ """
318
+ )
319
+
320
+ conn.commit()
321
+ conn.close()
322
+
323
+ # Get last hash for Merkle chain
324
+ conn = self._get_connection()
325
+ cursor = conn.cursor()
326
+ cursor.execute("SELECT entry_hash FROM audit_log ORDER BY id DESC LIMIT 1")
327
+ row = cursor.fetchone()
328
+ self._last_hash = row[0] if row else None
329
+
330
+ self.logger.info(f"Flight Recorder initialized with WAL mode: {self.db_path}")
331
+
332
+ def start_trace(
333
+ self,
334
+ agent_id: str,
335
+ tool_name: str,
336
+ tool_args: Optional[Dict[str, Any]] = None,
337
+ input_prompt: Optional[str] = None,
338
+ ) -> str:
339
+ """Start a new trace for an agent action.
340
+
341
+ Creates a pending audit log entry and links it into the Merkle
342
+ hash chain for tamper detection. The returned ``trace_id`` must be
343
+ passed to one of the outcome methods (``log_success``,
344
+ ``log_violation``, ``log_error``, ``log_shadow_exec``) to finalize
345
+ the entry.
346
+
347
+ Args:
348
+ agent_id: Unique identifier of the agent performing the action.
349
+ tool_name: Name of the tool being called (e.g. ``"web_search"``).
350
+ tool_args: Keyword arguments passed to the tool. Serialized as
351
+ JSON in the audit log. Defaults to ``None``.
352
+ input_prompt: The original user or agent prompt that triggered
353
+ this tool call. Defaults to ``None``.
354
+
355
+ Returns:
356
+ A UUID string uniquely identifying this trace. Use this value
357
+ with the ``log_*`` methods to record the outcome.
358
+
359
+ Example:
360
+ >>> trace_id = recorder.start_trace(
361
+ ... agent_id="agent-001",
362
+ ... tool_name="file_write",
363
+ ... tool_args={"path": "/tmp/out.txt", "data": "hello"},
364
+ ... )
365
+ >>> recorder.log_success(trace_id, result="wrote 5 bytes")
366
+ """
367
+ trace_id = str(uuid.uuid4())
368
+ timestamp = datetime.utcnow().isoformat()
369
+ tool_args_json = json.dumps(tool_args) if tool_args else None
370
+
371
+ # Compute hash for Merkle chain (immutable after INSERT)
372
+ data = f"{trace_id}:{timestamp}:{agent_id}:{tool_name}:{tool_args_json}:pending"
373
+ entry_hash = self._compute_hash(data, self._last_hash)
374
+ previous_hash = self._last_hash
375
+ self._last_hash = entry_hash
376
+
377
+ # Compute content hash covering all substantive fields
378
+ content_hash = self._compute_content_hash(
379
+ trace_id, timestamp, agent_id, tool_name, tool_args_json,
380
+ 'pending',
381
+ )
382
+
383
+ # Cache immutable data so log_success/log_violation can recompute
384
+ self._trace_data[trace_id] = {
385
+ 'timestamp': timestamp,
386
+ 'agent_id': agent_id,
387
+ 'tool_name': tool_name,
388
+ 'tool_args_json': tool_args_json,
389
+ }
390
+
391
+ self._queue_write(
392
+ """
393
+ INSERT INTO audit_log
394
+ (trace_id, timestamp, agent_id, tool_name, tool_args, input_prompt, policy_verdict, entry_hash, previous_hash, content_hash)
395
+ VALUES (?, ?, ?, ?, ?, ?, 'pending', ?, ?, ?)
396
+ """,
397
+ (trace_id, timestamp, agent_id, tool_name, tool_args_json, input_prompt, entry_hash, previous_hash, content_hash)
398
+ )
399
+
400
+ return trace_id
401
+
402
+ def log_violation(self, trace_id: str, violation_reason: str):
403
+ """Log a policy violation for a trace.
404
+
405
+ Updates the audit entry identified by ``trace_id`` to ``blocked``
406
+ status and records the reason. A warning is emitted to the logger.
407
+
408
+ Args:
409
+ trace_id: The trace ID returned by ``start_trace``.
410
+ violation_reason: Human-readable explanation of why the action
411
+ was blocked (e.g. ``"Tool 'rm_rf' not in allowed_tools"``).
412
+ """
413
+ content_hash = self._recompute_content_hash(
414
+ trace_id, 'blocked', violation_reason=violation_reason,
415
+ )
416
+
417
+ self._queue_write(
418
+ """
419
+ UPDATE audit_log
420
+ SET policy_verdict = 'blocked',
421
+ violation_reason = ?,
422
+ content_hash = ?
423
+ WHERE trace_id = ?
424
+ """,
425
+ (violation_reason, content_hash, trace_id)
426
+ )
427
+
428
+ self.logger.warning(f"BLOCKED: {trace_id} - {violation_reason}")
429
+
430
+ def log_shadow_exec(self, trace_id: str, simulated_result: Optional[str] = None):
431
+ """Log a shadow mode execution (simulated, not real).
432
+
433
+ Shadow mode allows the governance layer to return a plausible
434
+ simulated result to the agent without actually executing the tool.
435
+ This is useful for testing policy enforcement in production
436
+ without impacting real systems.
437
+
438
+ Args:
439
+ trace_id: The trace ID returned by ``start_trace``.
440
+ simulated_result: The simulated result string returned to the
441
+ agent. Defaults to ``"Simulated success"`` when ``None``.
442
+ """
443
+ result_val = simulated_result or "Simulated success"
444
+ content_hash = self._recompute_content_hash(
445
+ trace_id, 'shadow', result=result_val,
446
+ )
447
+
448
+ self._queue_write(
449
+ """
450
+ UPDATE audit_log
451
+ SET policy_verdict = 'shadow',
452
+ result = ?,
453
+ content_hash = ?
454
+ WHERE trace_id = ?
455
+ """,
456
+ (result_val, content_hash, trace_id)
457
+ )
458
+
459
+ self.logger.info(f"SHADOW: {trace_id}")
460
+
461
+ def log_success(
462
+ self, trace_id: str, result: Optional[Any] = None, execution_time_ms: Optional[float] = None
463
+ ):
464
+ """Log a successful execution.
465
+
466
+ Updates the audit entry to ``allowed`` status and records the
467
+ result and timing information.
468
+
469
+ Args:
470
+ trace_id: The trace ID returned by ``start_trace``.
471
+ result: The return value of the tool execution. Non-string
472
+ values are JSON-serialized before storage. Defaults to
473
+ ``None``.
474
+ execution_time_ms: Wall-clock execution time in milliseconds.
475
+ Defaults to ``None``.
476
+ """
477
+ result_str = (
478
+ json.dumps(result)
479
+ if result and not isinstance(result, str)
480
+ else str(result) if result else None
481
+ )
482
+
483
+ content_hash = self._recompute_content_hash(
484
+ trace_id, 'allowed', result=result_str,
485
+ execution_time_ms=execution_time_ms,
486
+ )
487
+
488
+ self._queue_write(
489
+ """
490
+ UPDATE audit_log
491
+ SET policy_verdict = 'allowed',
492
+ result = ?,
493
+ execution_time_ms = ?,
494
+ content_hash = ?
495
+ WHERE trace_id = ?
496
+ """,
497
+ (result_str, execution_time_ms, content_hash, trace_id)
498
+ )
499
+
500
+ self.logger.info(f"ALLOWED: {trace_id}")
501
+
502
+ def log_error(self, trace_id: str, error: str):
503
+ """Log an execution error.
504
+
505
+ Updates the audit entry to ``error`` status. Unlike violations,
506
+ errors indicate that the tool was *allowed* by policy but failed
507
+ during execution (e.g. network timeout, invalid arguments).
508
+
509
+ Args:
510
+ trace_id: The trace ID returned by ``start_trace``.
511
+ error: Error message describing the failure.
512
+ """
513
+ content_hash = self._recompute_content_hash(
514
+ trace_id, 'error', violation_reason=error,
515
+ )
516
+
517
+ self._queue_write(
518
+ """
519
+ UPDATE audit_log
520
+ SET policy_verdict = 'error',
521
+ violation_reason = ?,
522
+ content_hash = ?
523
+ WHERE trace_id = ?
524
+ """,
525
+ (error, content_hash, trace_id)
526
+ )
527
+
528
+ self.logger.error(f"ERROR: {trace_id} - {error}")
529
+
530
+ def query_logs(
531
+ self,
532
+ agent_id: Optional[str] = None,
533
+ policy_verdict: Optional[str] = None,
534
+ start_time: Optional[datetime] = None,
535
+ end_time: Optional[datetime] = None,
536
+ limit: int = 100,
537
+ ) -> list:
538
+ """Query the audit logs with filters.
539
+
540
+ Opens a new read-only connection (safe for concurrent access under
541
+ WAL mode) and returns matching entries ordered by timestamp
542
+ descending.
543
+
544
+ Args:
545
+ agent_id: Filter by agent identifier. When ``None``, all
546
+ agents are included.
547
+ policy_verdict: Filter by verdict string. Valid values are
548
+ ``"allowed"``, ``"blocked"``, ``"shadow"``, ``"error"``,
549
+ and ``"pending"``. When ``None``, all verdicts are included.
550
+ start_time: Include only entries at or after this timestamp.
551
+ end_time: Include only entries at or before this timestamp.
552
+ limit: Maximum number of results to return. Defaults to ``100``.
553
+
554
+ Returns:
555
+ A list of dictionaries, each representing one audit log entry
556
+ with keys: ``trace_id``, ``timestamp``, ``agent_id``,
557
+ ``tool_name``, ``tool_args``, ``input_prompt``,
558
+ ``policy_verdict``, ``violation_reason``, ``result``,
559
+ ``execution_time_ms``, ``metadata``, ``entry_hash``,
560
+ ``previous_hash``.
561
+
562
+ Example:
563
+ >>> blocked = recorder.query_logs(
564
+ ... agent_id="agent-001",
565
+ ... policy_verdict="blocked",
566
+ ... limit=50,
567
+ ... )
568
+ >>> for entry in blocked:
569
+ ... print(entry["violation_reason"])
570
+ """
571
+ conn = sqlite3.connect(self.db_path)
572
+ conn.row_factory = sqlite3.Row
573
+ cursor = conn.cursor()
574
+
575
+ query = "SELECT * FROM audit_log WHERE 1=1"
576
+ params = []
577
+
578
+ if agent_id:
579
+ query += " AND agent_id = ?"
580
+ params.append(agent_id)
581
+
582
+ if policy_verdict:
583
+ query += " AND policy_verdict = ?"
584
+ params.append(policy_verdict)
585
+
586
+ if start_time:
587
+ query += " AND timestamp >= ?"
588
+ params.append(start_time.isoformat())
589
+
590
+ if end_time:
591
+ query += " AND timestamp <= ?"
592
+ params.append(end_time.isoformat())
593
+
594
+ query += " ORDER BY timestamp DESC LIMIT ?"
595
+ params.append(limit)
596
+
597
+ cursor.execute(query, params)
598
+ results = [dict(row) for row in cursor.fetchall()]
599
+
600
+ conn.close()
601
+
602
+ return results
603
+
604
+ def get_statistics(self) -> Dict[str, Any]:
605
+ """Get aggregate statistics about the audit log.
606
+
607
+ Returns:
608
+ A dictionary containing:
609
+
610
+ - ``total_actions`` (int): Total number of recorded actions.
611
+ - ``by_verdict`` (Dict[str, int]): Action counts grouped by
612
+ policy verdict (e.g. ``{"allowed": 42, "blocked": 3}``).
613
+ - ``top_agents`` (List[Dict]): Up to 10 most active agents,
614
+ each with ``agent_id`` and ``count`` keys.
615
+ - ``avg_execution_time_ms`` (Optional[float]): Mean execution
616
+ time across all successful actions, or ``None`` if no timing
617
+ data is available.
618
+ """
619
+ conn = sqlite3.connect(self.db_path)
620
+ cursor = conn.cursor()
621
+
622
+ # Total actions
623
+ cursor.execute("SELECT COUNT(*) FROM audit_log")
624
+ total = cursor.fetchone()[0]
625
+
626
+ # By verdict
627
+ cursor.execute(
628
+ """
629
+ SELECT policy_verdict, COUNT(*) as count
630
+ FROM audit_log
631
+ GROUP BY policy_verdict
632
+ """
633
+ )
634
+ by_verdict = {row[0]: row[1] for row in cursor.fetchall()}
635
+
636
+ # By agent
637
+ cursor.execute(
638
+ """
639
+ SELECT agent_id, COUNT(*) as count
640
+ FROM audit_log
641
+ GROUP BY agent_id
642
+ ORDER BY count DESC
643
+ LIMIT 10
644
+ """
645
+ )
646
+ top_agents = [{"agent_id": row[0], "count": row[1]} for row in cursor.fetchall()]
647
+
648
+ # Average execution time
649
+ cursor.execute(
650
+ """
651
+ SELECT AVG(execution_time_ms)
652
+ FROM audit_log
653
+ WHERE execution_time_ms IS NOT NULL
654
+ """
655
+ )
656
+ avg_exec_time = cursor.fetchone()[0]
657
+
658
+ conn.close()
659
+
660
+ return {
661
+ "total_actions": total,
662
+ "by_verdict": by_verdict,
663
+ "top_agents": top_agents,
664
+ "avg_execution_time_ms": avg_exec_time,
665
+ }
666
+
667
+ def close(self):
668
+ """Clean up resources by flushing the write buffer and closing connections."""
669
+ self._flush_and_close()
670
+
671
+ def flush(self):
672
+ """Manually flush the write buffer to disk."""
673
+ self._flush_buffer()
674
+
675
+ # ===== Tamper Detection =====
676
+
677
+ def verify_integrity(self) -> Dict[str, Any]:
678
+ """Verify the integrity of the audit log.
679
+
680
+ Flushes any buffered writes, then walks the entire audit log in
681
+ insertion order performing two checks for every entry:
682
+
683
+ 1. **Chain integrity** – each entry's ``previous_hash`` must match
684
+ the ``entry_hash`` of its predecessor.
685
+ 2. **Content integrity** – the stored ``content_hash`` must match
686
+ a freshly computed hash over all substantive row fields
687
+ (including the current ``policy_verdict``). This detects
688
+ post-hoc tampering with the verdict or any other field.
689
+
690
+ Returns:
691
+ A dictionary with the following keys:
692
+
693
+ - ``valid`` (bool): ``True`` if both checks pass for every
694
+ entry.
695
+ - ``total_entries`` (int): Number of entries checked.
696
+ - ``message`` (str): Human-readable summary (when valid).
697
+ - ``first_tampered_id`` (int): Row ID of the first entry
698
+ where a check fails (only present when invalid).
699
+ - ``error`` (str): Description of the integrity failure
700
+ (only present when invalid).
701
+
702
+ Example:
703
+ >>> result = recorder.verify_integrity()
704
+ >>> if not result["valid"]:
705
+ ... print(f"Tampered at entry {result['first_tampered_id']}")
706
+ """
707
+ self._flush_buffer() # Ensure all writes are committed
708
+
709
+ conn = self._get_connection()
710
+ conn.row_factory = sqlite3.Row
711
+ cursor = conn.cursor()
712
+
713
+ cursor.execute("""
714
+ SELECT id, trace_id, timestamp, agent_id, tool_name, tool_args,
715
+ policy_verdict, violation_reason, result, execution_time_ms,
716
+ entry_hash, previous_hash, content_hash
717
+ FROM audit_log
718
+ ORDER BY id ASC
719
+ """)
720
+
721
+ entries = cursor.fetchall()
722
+
723
+ if not entries:
724
+ return {"valid": True, "total_entries": 0, "message": "No entries to verify"}
725
+
726
+ expected_previous_hash = None
727
+
728
+ for entry in entries:
729
+ # 1. Verify chain link (previous_hash matches predecessor)
730
+ if entry['previous_hash'] != expected_previous_hash:
731
+ # First entry should have None/null previous_hash
732
+ if entry['id'] == 1 and entry['previous_hash'] is None:
733
+ pass # OK - genesis entry
734
+ else:
735
+ return {
736
+ "valid": False,
737
+ "total_entries": len(entries),
738
+ "first_tampered_id": entry['id'],
739
+ "error": f"Hash chain broken at entry {entry['id']}: expected previous_hash {expected_previous_hash}, got {entry['previous_hash']}"
740
+ }
741
+
742
+ # 2. Verify content hash (detects field tampering including verdict)
743
+ if entry['content_hash']:
744
+ expected_content = self._compute_content_hash(
745
+ entry['trace_id'],
746
+ entry['timestamp'],
747
+ entry['agent_id'],
748
+ entry['tool_name'],
749
+ entry['tool_args'],
750
+ entry['policy_verdict'],
751
+ violation_reason=entry['violation_reason'],
752
+ result=entry['result'],
753
+ execution_time_ms=entry['execution_time_ms'],
754
+ )
755
+ if expected_content != entry['content_hash']:
756
+ return {
757
+ "valid": False,
758
+ "total_entries": len(entries),
759
+ "first_tampered_id": entry['id'],
760
+ "error": f"Content hash mismatch at entry {entry['id']}: field tampering detected"
761
+ }
762
+
763
+ expected_previous_hash = entry['entry_hash']
764
+
765
+ return {
766
+ "valid": True,
767
+ "total_entries": len(entries),
768
+ "message": "Hash chain integrity verified"
769
+ }
770
+
771
+ # ===== Time-Travel Debugging Support =====
772
+
773
+ def get_log(self) -> list:
774
+ """Get the complete audit log for time-travel debugging.
775
+
776
+ Returns all entries ordered by timestamp ascending, enabling
777
+ chronological replay of agent actions.
778
+
779
+ Returns:
780
+ A list of dictionaries representing every audit log entry,
781
+ ordered oldest-first.
782
+ """
783
+ self._flush_buffer() # Ensure all writes are committed
784
+
785
+ conn = sqlite3.connect(self.db_path)
786
+ conn.row_factory = sqlite3.Row
787
+ cursor = conn.cursor()
788
+
789
+ cursor.execute(
790
+ """
791
+ SELECT * FROM audit_log
792
+ ORDER BY timestamp ASC
793
+ """
794
+ )
795
+ results = [dict(row) for row in cursor.fetchall()]
796
+
797
+ conn.close()
798
+
799
+ return results
800
+
801
+ def get_events_in_time_range(
802
+ self,
803
+ start_time: datetime,
804
+ end_time: datetime,
805
+ agent_id: Optional[str] = None
806
+ ) -> list:
807
+ """Get events within a specific time range for time-travel replay.
808
+
809
+ Useful for replaying agent behaviour during a specific incident
810
+ window or for generating compliance reports.
811
+
812
+ Args:
813
+ start_time: Inclusive start of the time range.
814
+ end_time: Inclusive end of the time range.
815
+ agent_id: When provided, only entries for this agent are
816
+ returned. Defaults to ``None`` (all agents).
817
+
818
+ Returns:
819
+ A list of audit log entry dictionaries within the given time
820
+ range, ordered oldest-first.
821
+ """
822
+ self._flush_buffer() # Ensure all writes are committed
823
+
824
+ conn = sqlite3.connect(self.db_path)
825
+ conn.row_factory = sqlite3.Row
826
+ cursor = conn.cursor()
827
+
828
+ query = """
829
+ SELECT * FROM audit_log
830
+ WHERE timestamp >= ? AND timestamp <= ?
831
+ """
832
+ params = [start_time.isoformat(), end_time.isoformat()]
833
+
834
+ if agent_id:
835
+ query += " AND agent_id = ?"
836
+ params.append(agent_id)
837
+
838
+ query += " ORDER BY timestamp ASC"
839
+
840
+ cursor.execute(query, params)
841
+ results = [dict(row) for row in cursor.fetchall()]
842
+
843
+ conn.close()
844
+
845
+ return results
846
+