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.
- agent_control_plane/__init__.py +662 -0
- agent_control_plane/a2a_adapter.py +543 -0
- agent_control_plane/adapter.py +417 -0
- agent_control_plane/agent_hibernation.py +394 -0
- agent_control_plane/agent_kernel.py +470 -0
- agent_control_plane/compliance.py +720 -0
- agent_control_plane/constraint_graphs.py +478 -0
- agent_control_plane/control_plane.py +854 -0
- agent_control_plane/example_executors.py +195 -0
- agent_control_plane/execution_engine.py +231 -0
- agent_control_plane/flight_recorder.py +846 -0
- agent_control_plane/governance_layer.py +435 -0
- agent_control_plane/hf_utils.py +563 -0
- agent_control_plane/interfaces/__init__.py +55 -0
- agent_control_plane/interfaces/kernel_interface.py +361 -0
- agent_control_plane/interfaces/plugin_interface.py +497 -0
- agent_control_plane/interfaces/protocol_interfaces.py +387 -0
- agent_control_plane/kernel_space.py +1009 -0
- agent_control_plane/langchain_adapter.py +424 -0
- agent_control_plane/lifecycle.py +3113 -0
- agent_control_plane/mcp_adapter.py +653 -0
- agent_control_plane/ml_safety.py +563 -0
- agent_control_plane/multimodal.py +727 -0
- agent_control_plane/mute_agent.py +422 -0
- agent_control_plane/observability.py +787 -0
- agent_control_plane/orchestrator.py +482 -0
- agent_control_plane/plugin_registry.py +750 -0
- agent_control_plane/policy_engine.py +954 -0
- agent_control_plane/process_isolation.py +777 -0
- agent_control_plane/shadow_mode.py +310 -0
- agent_control_plane/signals.py +493 -0
- agent_control_plane/supervisor_agents.py +430 -0
- agent_control_plane/time_travel_debugger.py +557 -0
- agent_control_plane/tool_registry.py +452 -0
- agent_control_plane/vfs.py +697 -0
- agent_kernel/__init__.py +69 -0
- agent_kernel/analyzer.py +435 -0
- agent_kernel/auditor.py +36 -0
- agent_kernel/completeness_auditor.py +237 -0
- agent_kernel/detector.py +203 -0
- agent_kernel/kernel.py +744 -0
- agent_kernel/memory_manager.py +85 -0
- agent_kernel/models.py +374 -0
- agent_kernel/nudge_mechanism.py +263 -0
- agent_kernel/outcome_analyzer.py +338 -0
- agent_kernel/patcher.py +582 -0
- agent_kernel/semantic_analyzer.py +316 -0
- agent_kernel/semantic_purge.py +349 -0
- agent_kernel/simulator.py +449 -0
- agent_kernel/teacher.py +85 -0
- agent_kernel/triage.py +152 -0
- agent_os/__init__.py +409 -0
- agent_os/_adversarial_impl.py +200 -0
- agent_os/_circuit_breaker_impl.py +232 -0
- agent_os/_mcp_metrics.py +193 -0
- agent_os/adversarial.py +20 -0
- agent_os/agents_compat.py +490 -0
- agent_os/audit_logger.py +135 -0
- agent_os/base_agent.py +651 -0
- agent_os/circuit_breaker.py +34 -0
- agent_os/cli/__init__.py +659 -0
- agent_os/cli/cmd_audit.py +128 -0
- agent_os/cli/cmd_init.py +152 -0
- agent_os/cli/cmd_policy.py +41 -0
- agent_os/cli/cmd_policy_gen.py +180 -0
- agent_os/cli/cmd_validate.py +258 -0
- agent_os/cli/mcp_scan.py +265 -0
- agent_os/cli/output.py +192 -0
- agent_os/cli/policy_checker.py +330 -0
- agent_os/compat.py +74 -0
- agent_os/constraint_graph.py +234 -0
- agent_os/content_governance.py +140 -0
- agent_os/context_budget.py +305 -0
- agent_os/credential_redactor.py +224 -0
- agent_os/diff_policy.py +89 -0
- agent_os/egress_policy.py +159 -0
- agent_os/escalation.py +276 -0
- agent_os/event_bus.py +124 -0
- agent_os/exceptions.py +180 -0
- agent_os/execution_context_policy.py +141 -0
- agent_os/github_enterprise.py +96 -0
- agent_os/health.py +20 -0
- agent_os/integrations/__init__.py +279 -0
- agent_os/integrations/a2a_adapter.py +279 -0
- agent_os/integrations/agent_lightning/__init__.py +30 -0
- agent_os/integrations/anthropic_adapter.py +420 -0
- agent_os/integrations/autogen_adapter.py +620 -0
- agent_os/integrations/base.py +1137 -0
- agent_os/integrations/compat.py +229 -0
- agent_os/integrations/config.py +98 -0
- agent_os/integrations/conversation_guardian.py +957 -0
- agent_os/integrations/crewai_adapter.py +467 -0
- agent_os/integrations/drift_detector.py +425 -0
- agent_os/integrations/dry_run.py +124 -0
- agent_os/integrations/escalation.py +582 -0
- agent_os/integrations/gemini_adapter.py +364 -0
- agent_os/integrations/google_adk_adapter.py +633 -0
- agent_os/integrations/guardrails_adapter.py +394 -0
- agent_os/integrations/health.py +197 -0
- agent_os/integrations/langchain_adapter.py +654 -0
- agent_os/integrations/llamafirewall.py +343 -0
- agent_os/integrations/llamaindex_adapter.py +188 -0
- agent_os/integrations/logging.py +191 -0
- agent_os/integrations/maf_adapter.py +631 -0
- agent_os/integrations/mistral_adapter.py +365 -0
- agent_os/integrations/openai_adapter.py +816 -0
- agent_os/integrations/openai_agents_sdk.py +406 -0
- agent_os/integrations/policy_compose.py +171 -0
- agent_os/integrations/profiling.py +144 -0
- agent_os/integrations/pydantic_ai_adapter.py +420 -0
- agent_os/integrations/rate_limiter.py +130 -0
- agent_os/integrations/rbac.py +143 -0
- agent_os/integrations/registry.py +113 -0
- agent_os/integrations/scope_guard.py +303 -0
- agent_os/integrations/semantic_kernel_adapter.py +769 -0
- agent_os/integrations/smolagents_adapter.py +629 -0
- agent_os/integrations/templates.py +178 -0
- agent_os/integrations/token_budget.py +134 -0
- agent_os/integrations/tool_aliases.py +190 -0
- agent_os/integrations/webhooks.py +177 -0
- agent_os/lite.py +208 -0
- agent_os/mcp_gateway.py +385 -0
- agent_os/mcp_message_signer.py +273 -0
- agent_os/mcp_protocols.py +161 -0
- agent_os/mcp_response_scanner.py +232 -0
- agent_os/mcp_security.py +924 -0
- agent_os/mcp_session_auth.py +231 -0
- agent_os/mcp_sliding_rate_limiter.py +184 -0
- agent_os/memory_guard.py +409 -0
- agent_os/metrics.py +134 -0
- agent_os/mute.py +428 -0
- agent_os/mute_agent.py +209 -0
- agent_os/policies/__init__.py +77 -0
- agent_os/policies/async_evaluator.py +275 -0
- agent_os/policies/backends.py +670 -0
- agent_os/policies/bridge.py +169 -0
- agent_os/policies/budget.py +85 -0
- agent_os/policies/cli.py +294 -0
- agent_os/policies/conflict_resolution.py +270 -0
- agent_os/policies/data_classification.py +252 -0
- agent_os/policies/evaluator.py +239 -0
- agent_os/policies/policy_schema.json +228 -0
- agent_os/policies/rate_limiting.py +145 -0
- agent_os/policies/schema.py +115 -0
- agent_os/policies/shared.py +331 -0
- agent_os/prompt_injection.py +694 -0
- agent_os/providers.py +182 -0
- agent_os/py.typed +0 -0
- agent_os/retry.py +81 -0
- agent_os/reversibility.py +251 -0
- agent_os/sandbox.py +432 -0
- agent_os/sandbox_provider.py +140 -0
- agent_os/secure_codegen.py +525 -0
- agent_os/security_skills.py +538 -0
- agent_os/semantic_policy.py +422 -0
- agent_os/server/__init__.py +15 -0
- agent_os/server/__main__.py +25 -0
- agent_os/server/app.py +277 -0
- agent_os/server/models.py +104 -0
- agent_os/shift_left_metrics.py +130 -0
- agent_os/stateless.py +742 -0
- agent_os/supervisor.py +148 -0
- agent_os/task_outcome.py +148 -0
- agent_os/transparency.py +181 -0
- agent_os/trust_root.py +128 -0
- agent_os_kernel-3.1.0.dist-info/METADATA +1269 -0
- agent_os_kernel-3.1.0.dist-info/RECORD +337 -0
- agent_os_kernel-3.1.0.dist-info/WHEEL +4 -0
- agent_os_kernel-3.1.0.dist-info/entry_points.txt +2 -0
- agent_os_kernel-3.1.0.dist-info/licenses/LICENSE +21 -0
- agent_os_observability/__init__.py +27 -0
- agent_os_observability/dashboards.py +898 -0
- agent_os_observability/metrics.py +398 -0
- agent_os_observability/server.py +223 -0
- agent_os_observability/tracer.py +232 -0
- agent_primitives/__init__.py +24 -0
- agent_primitives/failures.py +84 -0
- agent_primitives/py.typed +0 -0
- amb_core/__init__.py +177 -0
- amb_core/adapters/__init__.py +57 -0
- amb_core/adapters/aws_sqs_broker.py +376 -0
- amb_core/adapters/azure_servicebus_broker.py +340 -0
- amb_core/adapters/kafka_broker.py +260 -0
- amb_core/adapters/nats_broker.py +285 -0
- amb_core/adapters/rabbitmq_broker.py +235 -0
- amb_core/adapters/redis_broker.py +262 -0
- amb_core/broker.py +145 -0
- amb_core/bus.py +481 -0
- amb_core/cloudevents.py +509 -0
- amb_core/dlq.py +345 -0
- amb_core/hf_utils.py +536 -0
- amb_core/memory_broker.py +410 -0
- amb_core/models.py +141 -0
- amb_core/persistence.py +529 -0
- amb_core/schema.py +294 -0
- amb_core/tracing.py +358 -0
- atr/__init__.py +640 -0
- atr/access.py +348 -0
- atr/composition.py +645 -0
- atr/decorator.py +357 -0
- atr/executor.py +384 -0
- atr/health.py +557 -0
- atr/hf_utils.py +449 -0
- atr/injection.py +422 -0
- atr/metrics.py +440 -0
- atr/policies.py +403 -0
- atr/py.typed +2 -0
- atr/registry.py +452 -0
- atr/schema.py +480 -0
- atr/tools/safe/__init__.py +75 -0
- atr/tools/safe/calculator.py +467 -0
- atr/tools/safe/datetime_tool.py +443 -0
- atr/tools/safe/file_reader.py +402 -0
- atr/tools/safe/http_client.py +316 -0
- atr/tools/safe/json_parser.py +374 -0
- atr/tools/safe/text_tool.py +537 -0
- atr/tools/safe/toolkit.py +175 -0
- caas/__init__.py +162 -0
- caas/api/__init__.py +7 -0
- caas/api/server.py +1328 -0
- caas/caching.py +834 -0
- caas/cli.py +210 -0
- caas/conversation.py +223 -0
- caas/decay.py +72 -0
- caas/detection/__init__.py +9 -0
- caas/detection/detector.py +238 -0
- caas/enrichment.py +130 -0
- caas/gateway/__init__.py +27 -0
- caas/gateway/trust_gateway.py +474 -0
- caas/hf_utils.py +479 -0
- caas/ingestion/__init__.py +23 -0
- caas/ingestion/processors.py +253 -0
- caas/ingestion/structure_parser.py +188 -0
- caas/models.py +356 -0
- caas/pragmatic_truth.py +444 -0
- caas/routing/__init__.py +10 -0
- caas/routing/heuristic_router.py +58 -0
- caas/storage/__init__.py +9 -0
- caas/storage/store.py +389 -0
- caas/triad.py +213 -0
- caas/tuning/__init__.py +9 -0
- caas/tuning/tuner.py +329 -0
- caas/vfs/__init__.py +14 -0
- caas/vfs/filesystem.py +452 -0
- cmvk/__init__.py +218 -0
- cmvk/audit.py +402 -0
- cmvk/benchmarks.py +478 -0
- cmvk/constitutional.py +904 -0
- cmvk/hf_utils.py +301 -0
- cmvk/metrics.py +473 -0
- cmvk/profiles.py +300 -0
- cmvk/py.typed +0 -0
- cmvk/types.py +12 -0
- cmvk/verification.py +956 -0
- emk/__init__.py +89 -0
- emk/causal.py +352 -0
- emk/hf_utils.py +421 -0
- emk/indexer.py +83 -0
- emk/py.typed +0 -0
- emk/schema.py +204 -0
- emk/sleep_cycle.py +347 -0
- emk/store.py +281 -0
- iatp/__init__.py +166 -0
- iatp/attestation.py +461 -0
- iatp/cli.py +317 -0
- iatp/hf_utils.py +472 -0
- iatp/ipc_pipes.py +580 -0
- iatp/main.py +412 -0
- iatp/models/__init__.py +447 -0
- iatp/policy_engine.py +337 -0
- iatp/py.typed +2 -0
- iatp/recovery.py +321 -0
- iatp/security/__init__.py +270 -0
- iatp/sidecar/__init__.py +519 -0
- iatp/telemetry/__init__.py +164 -0
- iatp/tests/__init__.py +1 -0
- iatp/tests/test_attestation.py +370 -0
- iatp/tests/test_cli.py +131 -0
- iatp/tests/test_ed25519_attestation.py +211 -0
- iatp/tests/test_models.py +130 -0
- iatp/tests/test_policy_engine.py +347 -0
- iatp/tests/test_recovery.py +281 -0
- iatp/tests/test_security.py +222 -0
- iatp/tests/test_sidecar.py +167 -0
- iatp/tests/test_telemetry.py +175 -0
- mcp_kernel_server/__init__.py +28 -0
- mcp_kernel_server/cli.py +274 -0
- mcp_kernel_server/resources.py +217 -0
- mcp_kernel_server/server.py +564 -0
- mcp_kernel_server/tools.py +1174 -0
- mute_agent/__init__.py +68 -0
- mute_agent/core/__init__.py +1 -0
- mute_agent/core/execution_agent.py +166 -0
- mute_agent/core/handshake_protocol.py +201 -0
- mute_agent/core/reasoning_agent.py +238 -0
- mute_agent/knowledge_graph/__init__.py +1 -0
- mute_agent/knowledge_graph/graph_elements.py +65 -0
- mute_agent/knowledge_graph/multidimensional_graph.py +170 -0
- mute_agent/knowledge_graph/subgraph.py +224 -0
- mute_agent/listener/__init__.py +43 -0
- mute_agent/listener/adapters/__init__.py +31 -0
- mute_agent/listener/adapters/base_adapter.py +189 -0
- mute_agent/listener/adapters/caas_adapter.py +344 -0
- mute_agent/listener/adapters/control_plane_adapter.py +436 -0
- mute_agent/listener/adapters/iatp_adapter.py +332 -0
- mute_agent/listener/adapters/scak_adapter.py +251 -0
- mute_agent/listener/listener.py +610 -0
- mute_agent/listener/state_observer.py +436 -0
- mute_agent/listener/threshold_config.py +313 -0
- mute_agent/super_system/__init__.py +1 -0
- mute_agent/super_system/router.py +204 -0
- mute_agent/visualization/__init__.py +10 -0
- mute_agent/visualization/graph_debugger.py +502 -0
- nexus/README.md +60 -0
- nexus/__init__.py +51 -0
- nexus/arbiter.py +359 -0
- nexus/client.py +466 -0
- nexus/dmz.py +444 -0
- nexus/escrow.py +430 -0
- nexus/exceptions.py +286 -0
- nexus/pyproject.toml +36 -0
- nexus/registry.py +393 -0
- nexus/reputation.py +425 -0
- nexus/schemas/__init__.py +51 -0
- nexus/schemas/compliance.py +276 -0
- nexus/schemas/escrow.py +251 -0
- nexus/schemas/manifest.py +225 -0
- nexus/schemas/receipt.py +208 -0
- nexus/tests/__init__.py +0 -0
- nexus/tests/conftest.py +146 -0
- nexus/tests/test_arbiter.py +192 -0
- nexus/tests/test_dmz.py +194 -0
- nexus/tests/test_escrow.py +276 -0
- nexus/tests/test_exceptions.py +225 -0
- nexus/tests/test_registry.py +232 -0
- nexus/tests/test_reputation.py +328 -0
- 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
|
+
|