agentcop 0.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.
@@ -0,0 +1,210 @@
1
+ Metadata-Version: 2.4
2
+ Name: agentcop
3
+ Version: 0.1.0
4
+ Summary: Universal forensic auditor for agent systems — OTel-aligned event schema, pluggable violation detectors
5
+ Author: Shay Mizuno, Hind Tagmouti
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/trusthandoff/agentcop
8
+ Project-URL: Repository, https://github.com/trusthandoff/agentcop
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3.11
11
+ Classifier: Programming Language :: Python :: 3.12
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Operating System :: OS Independent
14
+ Requires-Python: >=3.11
15
+ Description-Content-Type: text/markdown
16
+ Requires-Dist: pydantic<3,>=2.7
17
+ Provides-Extra: otel
18
+ Requires-Dist: opentelemetry-sdk>=1.20; extra == "otel"
19
+ Provides-Extra: dev
20
+ Requires-Dist: pytest; extra == "dev"
21
+
22
+ # sentinel-core
23
+
24
+ **The cop for agent fleets.**
25
+
26
+ Every agent fleet needs a cop. Agents delegate, handoff, and execute — and without forensic oversight, violations are invisible until they're incidents. `sentinel-core` is a universal auditor: ingest events from any agent system, run violation detectors, get structured findings.
27
+
28
+ OTel-aligned schema. Pluggable detectors. Adapter bridge to your stack. Zero required infrastructure.
29
+
30
+ ```
31
+ pip install sentinel-core
32
+ ```
33
+
34
+ ---
35
+
36
+ ## How it works
37
+
38
+ ```
39
+ your agent system
40
+
41
+
42
+ SentinelAdapter ← translate domain events to universal schema
43
+
44
+
45
+ Sentinel.ingest() ← load SentinelEvents into the auditor
46
+
47
+
48
+ detect_violations() ← run detectors, get ViolationRecords
49
+
50
+
51
+ report() / your sink ← stdout, OTel, alerting, whatever
52
+ ```
53
+
54
+ ---
55
+
56
+ ## Quickstart
57
+
58
+ ```python
59
+ from sentinel_core import Sentinel, SentinelEvent
60
+
61
+ sentinel = Sentinel()
62
+
63
+ # Feed it events (any source, any schema — adapt first)
64
+ sentinel.ingest([
65
+ SentinelEvent(
66
+ event_id="evt-001",
67
+ event_type="packet_rejected",
68
+ timestamp="2026-03-31T12:00:00Z",
69
+ severity="ERROR",
70
+ body="packet rejected — TTL expired",
71
+ source_system="my-agent",
72
+ attributes={"packet_id": "pkt-abc", "reason": "ttl_expired"},
73
+ )
74
+ ])
75
+
76
+ violations = sentinel.detect_violations()
77
+ # [ViolationRecord(violation_type='rejected_packet', severity='ERROR', ...)]
78
+
79
+ sentinel.report()
80
+ # [ERROR] rejected_packet — packet rejected — TTL expired
81
+ # packet_id: pkt-abc
82
+ # reason: ttl_expired
83
+ ```
84
+
85
+ Built-in detectors fire on four event types out of the box:
86
+
87
+ | `event_type` | Detector | Severity |
88
+ |-------------------------|-------------------------------|----------|
89
+ | `packet_rejected` | `detect_rejected_packet` | ERROR |
90
+ | `capability_stale` | `detect_stale_capability` | ERROR |
91
+ | `token_overlap_used` | `detect_overlap_window` | WARN |
92
+ | `ai_generated_payload` | `detect_ai_generated_payload` | WARN |
93
+
94
+ ---
95
+
96
+ ## Custom detectors
97
+
98
+ Detectors are plain functions. Register as many as you need.
99
+
100
+ ```python
101
+ from sentinel_core import Sentinel, SentinelEvent, ViolationRecord
102
+ from typing import Optional
103
+
104
+ def detect_unauthorized_tool(event: SentinelEvent) -> Optional[ViolationRecord]:
105
+ if event.event_type != "tool_call":
106
+ return None
107
+ if event.attributes.get("tool") in {"shell", "fs_write"}:
108
+ return ViolationRecord(
109
+ violation_type="unauthorized_tool",
110
+ severity="CRITICAL",
111
+ source_event_id=event.event_id,
112
+ trace_id=event.trace_id,
113
+ detail={"tool": event.attributes["tool"]},
114
+ )
115
+
116
+ sentinel = Sentinel()
117
+ sentinel.register_detector(detect_unauthorized_tool)
118
+ ```
119
+
120
+ ---
121
+
122
+ ## TrustHandoff adapter
123
+
124
+ [TrustHandoff](https://github.com/trusthandoff/trusthandoff) ships a first-class adapter. If you're using `trusthandoff` for cryptographic delegation, plug it in directly:
125
+
126
+ ```python
127
+ from trusthandoff.sentinel_adapter import TrustHandoffSentinelAdapter
128
+ from sentinel_core import Sentinel
129
+
130
+ adapter = TrustHandoffSentinelAdapter()
131
+ sentinel = Sentinel()
132
+
133
+ # raw_events: list of dicts from trusthandoff's forensic log
134
+ sentinel.ingest(adapter.to_sentinel_event(e) for e in raw_events)
135
+
136
+ violations = sentinel.detect_violations()
137
+ sentinel.report()
138
+ ```
139
+
140
+ The adapter maps trusthandoff's event fields — `packet_id`, `correlation_id`, `reason`, `event_type` — to the universal `SentinelEvent` schema. Severity is inferred from event type. Everything else lands in `attributes`.
141
+
142
+ ---
143
+
144
+ ## Write your own adapter
145
+
146
+ Implement the `SentinelAdapter` protocol to bridge any system:
147
+
148
+ ```python
149
+ from sentinel_core import SentinelAdapter, SentinelEvent
150
+ from typing import Dict, Any
151
+
152
+ class MySystemAdapter:
153
+ source_system = "my-system"
154
+
155
+ def to_sentinel_event(self, raw: Dict[str, Any]) -> SentinelEvent:
156
+ return SentinelEvent(
157
+ event_id=raw["id"],
158
+ event_type=raw["type"],
159
+ timestamp=raw["ts"],
160
+ severity=raw.get("level", "INFO"),
161
+ body=raw.get("message", ""),
162
+ source_system=self.source_system,
163
+ trace_id=raw.get("trace_id"),
164
+ attributes=raw.get("metadata", {}),
165
+ )
166
+ ```
167
+
168
+ ---
169
+
170
+ ## LangGraph integration *(coming soon)*
171
+
172
+ Native LangGraph adapter that hooks into graph execution events — node calls, edge transitions, tool invocations — and surfaces violations without any manual instrumentation.
173
+
174
+ ```python
175
+ # coming soon
176
+ from sentinel_core.adapters.langgraph import LangGraphSentinelAdapter
177
+ ```
178
+
179
+ ---
180
+
181
+ ## OpenTelemetry export *(optional)*
182
+
183
+ `sentinel-core` events use an OTel-aligned schema out of the box (`trace_id`, `span_id`, severity levels). To export events as OTel log records:
184
+
185
+ ```
186
+ pip install sentinel-core[otel]
187
+ ```
188
+
189
+ ```python
190
+ from sentinel_core.otel import OtelSentinelExporter
191
+ from opentelemetry.sdk._logs import LoggerProvider
192
+
193
+ exporter = OtelSentinelExporter(logger_provider=LoggerProvider())
194
+ exporter.export(events)
195
+ ```
196
+
197
+ Attributes are emitted under the `sentinel.*` namespace. `trace_id` and `span_id` are mapped to OTel trace context.
198
+
199
+ ---
200
+
201
+ ## Requirements
202
+
203
+ - Python 3.11+
204
+ - `pydantic>=2.7`
205
+
206
+ ---
207
+
208
+ ## License
209
+
210
+ MIT
@@ -0,0 +1,11 @@
1
+ sentinel_core/__init__.py,sha256=p2IbFHjCD3guHav_Mj0kzVxbFTXuMta0P6wBfMQlKw4,682
2
+ sentinel_core/event.py,sha256=-spz7zu6ahs1c8n-cuI_L-bXW-ttogS0RTAQkPOIcsY,1874
3
+ sentinel_core/otel.py,sha256=SfoWWa99CsgSqzAFFwObZgaabBZKk784rRo27sJoqcI,4740
4
+ sentinel_core/sentinel.py,sha256=i1um_G7d33OIXhCRljbbO3et4ihN1_1c_Y2C9BrzQuE,2702
5
+ sentinel_core/violations.py,sha256=L0H4A3-5kONZQ3zLXJ7Mw8rUaIslV_tinT09_AncOKg,2438
6
+ sentinel_core/adapters/__init__.py,sha256=XQzd5D2Jvf0eL1sAtapFJfCR47-CA5rl8AEkGlmg_C8,65
7
+ sentinel_core/adapters/base.py,sha256=sUbRid0p9sWb_au8UPYWM4WFk2vhtLVYBifOLfn0FT8,941
8
+ agentcop-0.1.0.dist-info/METADATA,sha256=qm3Bnv7VSOcpMFr6sP0eGmN9S9EjJpw-J0Iiy4mGwQI,6115
9
+ agentcop-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
10
+ agentcop-0.1.0.dist-info/top_level.txt,sha256=CgoprkX69GcboqQHPuoetUJznlJK47tJz7CaOsWdtzY,14
11
+ agentcop-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ sentinel_core
@@ -0,0 +1,29 @@
1
+ from .event import SentinelEvent, ViolationRecord
2
+ from .sentinel import Sentinel, ViolationDetector
3
+ from .violations import (
4
+ DEFAULT_DETECTORS,
5
+ detect_ai_generated_payload,
6
+ detect_overlap_window,
7
+ detect_rejected_packet,
8
+ detect_stale_capability,
9
+ )
10
+ from .adapters import SentinelAdapter
11
+
12
+ __version__ = "0.1.0"
13
+
14
+ __all__ = [
15
+ # Core schema
16
+ "SentinelEvent",
17
+ "ViolationRecord",
18
+ # Auditor
19
+ "Sentinel",
20
+ "ViolationDetector",
21
+ # Built-in detectors
22
+ "DEFAULT_DETECTORS",
23
+ "detect_rejected_packet",
24
+ "detect_stale_capability",
25
+ "detect_overlap_window",
26
+ "detect_ai_generated_payload",
27
+ # Adapter protocol
28
+ "SentinelAdapter",
29
+ ]
@@ -0,0 +1,3 @@
1
+ from .base import SentinelAdapter
2
+
3
+ __all__ = ["SentinelAdapter"]
@@ -0,0 +1,37 @@
1
+ """
2
+ SentinelAdapter protocol.
3
+
4
+ Implement this to bridge any system's raw event dicts into SentinelEvents.
5
+ The TrustHandoff adapter lives in trusthandoff.sentinel_adapter.
6
+ """
7
+
8
+ from typing import Any, Dict, Protocol, runtime_checkable
9
+
10
+ from sentinel_core.event import SentinelEvent
11
+
12
+
13
+ @runtime_checkable
14
+ class SentinelAdapter(Protocol):
15
+ """
16
+ Protocol for system-specific event adapters.
17
+
18
+ Implementors translate raw event dicts (or any domain object) into
19
+ the universal SentinelEvent schema.
20
+
21
+ Example::
22
+
23
+ class MySystemAdapter:
24
+ source_system = "my-system"
25
+
26
+ def to_sentinel_event(self, raw: dict) -> SentinelEvent:
27
+ return SentinelEvent(
28
+ event_id=raw["id"],
29
+ event_type=raw["type"],
30
+ ...
31
+ )
32
+ """
33
+
34
+ source_system: str
35
+
36
+ def to_sentinel_event(self, raw: Dict[str, Any]) -> SentinelEvent:
37
+ ...
sentinel_core/event.py ADDED
@@ -0,0 +1,57 @@
1
+ from datetime import datetime, timezone
2
+ from typing import Any, Dict, Literal, Optional
3
+ import uuid
4
+
5
+ from pydantic import BaseModel, Field
6
+
7
+
8
+ class SentinelEvent(BaseModel):
9
+ """
10
+ Universal forensic event — OTel Log Data Model aligned.
11
+
12
+ OTel field mapping:
13
+ TraceId → trace_id
14
+ SpanId → span_id
15
+ Timestamp → timestamp
16
+ ObservedTimestamp → observed_at
17
+ SeverityText → severity
18
+ Body → body
19
+ Attributes → attributes
20
+
21
+ Domain-specific fields (packet_id, capability_id, etc.) live in
22
+ `attributes` — the same pattern OTel uses for instrumentation libraries.
23
+ `source_system` identifies the adapter that produced the event.
24
+ """
25
+
26
+ event_id: str
27
+ event_type: str
28
+ timestamp: datetime
29
+ observed_at: datetime = Field(
30
+ default_factory=lambda: datetime.now(timezone.utc)
31
+ )
32
+ severity: Literal["INFO", "WARN", "ERROR", "CRITICAL"]
33
+ producer_id: Optional[str] = None
34
+ trace_id: Optional[str] = None # OTel TraceId / correlation_id
35
+ span_id: Optional[str] = None # OTel SpanId, optional
36
+ body: str
37
+ attributes: Dict[str, Any] = Field(default_factory=dict)
38
+ source_system: str
39
+
40
+
41
+ class ViolationRecord(BaseModel):
42
+ """
43
+ Structured output of a violation detector.
44
+
45
+ `source_event_id` links back to the SentinelEvent that triggered detection.
46
+ `detail` carries violation-specific fields (reason, capability_id, model, …).
47
+ """
48
+
49
+ violation_id: str = Field(default_factory=lambda: str(uuid.uuid4()))
50
+ violation_type: str
51
+ severity: Literal["WARN", "ERROR", "CRITICAL"]
52
+ detected_at: datetime = Field(
53
+ default_factory=lambda: datetime.now(timezone.utc)
54
+ )
55
+ source_event_id: str
56
+ trace_id: Optional[str] = None
57
+ detail: Dict[str, Any] = Field(default_factory=dict)
sentinel_core/otel.py ADDED
@@ -0,0 +1,143 @@
1
+ """
2
+ OpenTelemetry integration for sentinel-core.
3
+
4
+ Install the optional dependency to use this module:
5
+
6
+ pip install sentinel-core[otel]
7
+
8
+ This module provides:
9
+ - to_otel_log_record() — convert a SentinelEvent to an OTel LogRecord
10
+ - OtelSentinelExporter — emit SentinelEvents through a LoggerProvider
11
+
12
+ Example::
13
+
14
+ from opentelemetry.sdk._logs import LoggerProvider
15
+ from opentelemetry.sdk._logs.export import SimpleLogRecordProcessor
16
+ from opentelemetry.sdk._logs.export.in_memory_span_exporter import InMemoryLogExporter
17
+ from sentinel_core.otel import OtelSentinelExporter
18
+
19
+ provider = LoggerProvider()
20
+ exporter = InMemoryLogExporter()
21
+ provider.add_log_record_processor(SimpleLogRecordProcessor(exporter))
22
+
23
+ sentinel_exporter = OtelSentinelExporter(logger_provider=provider)
24
+ sentinel_exporter.export(sentinel.detect_violations_as_events())
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ from datetime import timezone
30
+ from typing import TYPE_CHECKING, List, Sequence
31
+
32
+ from .event import SentinelEvent
33
+
34
+ if TYPE_CHECKING:
35
+ pass
36
+
37
+ _SEVERITY_NUMBER_MAP = {
38
+ "INFO": 9, # OTel SeverityNumber.INFO
39
+ "WARN": 13, # OTel SeverityNumber.WARN
40
+ "ERROR": 17, # OTel SeverityNumber.ERROR
41
+ "CRITICAL": 21, # OTel SeverityNumber.FATAL
42
+ }
43
+
44
+
45
+ def _require_otel() -> None:
46
+ try:
47
+ import opentelemetry # noqa: F401
48
+ except ImportError as exc:
49
+ raise ImportError(
50
+ "OpenTelemetry integration requires 'opentelemetry-sdk'. "
51
+ "Install it with: pip install sentinel-core[otel]"
52
+ ) from exc
53
+
54
+
55
+ def to_otel_attributes(event: SentinelEvent) -> dict:
56
+ """
57
+ Flatten a SentinelEvent into a flat OTel-compatible attribute dict.
58
+
59
+ All `attributes` keys are namespaced under `sentinel.*` to avoid
60
+ collisions with standard OTel resource/span attributes.
61
+ """
62
+ attrs: dict = {
63
+ "sentinel.event_id": event.event_id,
64
+ "sentinel.event_type": event.event_type,
65
+ "sentinel.source_system": event.source_system,
66
+ }
67
+ if event.producer_id is not None:
68
+ attrs["sentinel.producer_id"] = event.producer_id
69
+ if event.trace_id is not None:
70
+ attrs["sentinel.trace_id"] = event.trace_id
71
+ if event.span_id is not None:
72
+ attrs["sentinel.span_id"] = event.span_id
73
+ for k, v in event.attributes.items():
74
+ if v is not None:
75
+ attrs[f"sentinel.{k}"] = str(v)
76
+ return attrs
77
+
78
+
79
+ def to_otel_log_record(event: SentinelEvent):
80
+ """
81
+ Convert a SentinelEvent to an opentelemetry.sdk._logs.LogRecord.
82
+
83
+ Requires ``opentelemetry-sdk`` to be installed.
84
+ """
85
+ _require_otel()
86
+ from opentelemetry.sdk._logs import LogRecord # type: ignore[import]
87
+ from opentelemetry._logs.severity import SeverityNumber # type: ignore[import]
88
+
89
+ severity_number_value = _SEVERITY_NUMBER_MAP.get(event.severity, 9)
90
+ severity_number = SeverityNumber(severity_number_value)
91
+
92
+ timestamp_ns = int(
93
+ event.timestamp.astimezone(timezone.utc).timestamp() * 1_000_000_000
94
+ )
95
+ observed_ns = int(
96
+ event.observed_at.astimezone(timezone.utc).timestamp() * 1_000_000_000
97
+ )
98
+
99
+ return LogRecord(
100
+ timestamp=timestamp_ns,
101
+ observed_timestamp=observed_ns,
102
+ severity_text=event.severity,
103
+ severity_number=severity_number,
104
+ body=event.body,
105
+ attributes=to_otel_attributes(event),
106
+ trace_id=int(event.trace_id, 16) if event.trace_id and len(event.trace_id) == 32 else 0,
107
+ span_id=int(event.span_id, 16) if event.span_id and len(event.span_id) == 16 else 0,
108
+ )
109
+
110
+
111
+ class OtelSentinelExporter:
112
+ """
113
+ Emit SentinelEvents as OTel log records through a LoggerProvider.
114
+
115
+ Requires ``opentelemetry-sdk`` to be installed.
116
+
117
+ Parameters
118
+ ----------
119
+ logger_provider:
120
+ An ``opentelemetry.sdk._logs.LoggerProvider`` instance.
121
+ If None, uses the global provider.
122
+ instrumentation_name:
123
+ Logger name used when acquiring the OTel logger (appears as
124
+ ``otel.scope.name`` in most exporters).
125
+ """
126
+
127
+ def __init__(
128
+ self,
129
+ logger_provider=None,
130
+ instrumentation_name: str = "sentinel-core",
131
+ ):
132
+ _require_otel()
133
+ from opentelemetry.sdk._logs import LoggerProvider as _LP # type: ignore[import]
134
+ from opentelemetry._logs import get_logger_provider # type: ignore[import]
135
+
136
+ self._provider = logger_provider or get_logger_provider()
137
+ self._logger = self._provider.get_logger(instrumentation_name)
138
+
139
+ def export(self, events: Sequence[SentinelEvent]) -> None:
140
+ """Emit each event as an OTel log record."""
141
+ for event in events:
142
+ record = to_otel_log_record(event)
143
+ self._logger.emit(record)
@@ -0,0 +1,80 @@
1
+ import threading
2
+ from typing import Callable, Iterable, List, Optional
3
+
4
+ from .event import SentinelEvent, ViolationRecord
5
+ from .violations import DEFAULT_DETECTORS
6
+
7
+ ViolationDetector = Callable[[SentinelEvent], Optional[ViolationRecord]]
8
+
9
+
10
+ class Sentinel:
11
+ """
12
+ Universal forensic auditor.
13
+
14
+ Ingests SentinelEvents, runs violation detectors, returns typed ViolationRecords.
15
+
16
+ Usage::
17
+
18
+ sentinel = Sentinel()
19
+ sentinel.ingest(adapter.to_sentinel_event(e) for e in raw_events)
20
+ violations = sentinel.detect_violations()
21
+
22
+ Custom detectors::
23
+
24
+ def my_detector(event: SentinelEvent) -> ViolationRecord | None:
25
+ if event.event_type == "custom_alert":
26
+ return ViolationRecord(
27
+ violation_type="custom_alert",
28
+ severity="WARN",
29
+ source_event_id=event.event_id,
30
+ trace_id=event.trace_id,
31
+ detail={"msg": event.body},
32
+ )
33
+
34
+ sentinel = Sentinel()
35
+ sentinel.register_detector(my_detector)
36
+ """
37
+
38
+ def __init__(self, detectors: Optional[List[ViolationDetector]] = None):
39
+ self._lock = threading.Lock()
40
+ self._events: List[SentinelEvent] = []
41
+ self._detectors: List[ViolationDetector] = (
42
+ list(detectors) if detectors is not None else list(DEFAULT_DETECTORS)
43
+ )
44
+
45
+ def register_detector(self, fn: ViolationDetector) -> None:
46
+ """Append a custom detector. Runs after all built-in detectors."""
47
+ with self._lock:
48
+ self._detectors.append(fn)
49
+
50
+ def ingest(self, events: Iterable[SentinelEvent]) -> None:
51
+ """Replace the internal event buffer with the provided events."""
52
+ ingested = list(events)
53
+ with self._lock:
54
+ self._events = ingested
55
+
56
+ def detect_violations(self) -> List[ViolationRecord]:
57
+ with self._lock:
58
+ events = list(self._events)
59
+ detectors = list(self._detectors)
60
+
61
+ violations: List[ViolationRecord] = []
62
+ for event in events:
63
+ for detector in detectors:
64
+ result = detector(event)
65
+ if result is not None:
66
+ violations.append(result)
67
+ return violations
68
+
69
+ def report(self) -> None:
70
+ violations = self.detect_violations()
71
+ if not violations:
72
+ print("No violations detected")
73
+ return
74
+ print("=== SENTINEL REPORT ===")
75
+ for v in violations:
76
+ print(
77
+ f"[{v.severity}] {v.violation_type}"
78
+ + (f" trace={v.trace_id}" if v.trace_id else "")
79
+ + (f" {v.detail}" if v.detail else "")
80
+ )
@@ -0,0 +1,82 @@
1
+ """
2
+ Built-in violation detectors for sentinel-core.
3
+
4
+ Each detector is a plain function:
5
+ (SentinelEvent) -> ViolationRecord | None
6
+
7
+ Return a ViolationRecord if the event represents a violation, None otherwise.
8
+ Register custom detectors via Sentinel.register_detector().
9
+ """
10
+
11
+ from typing import Optional
12
+
13
+ from .event import SentinelEvent, ViolationRecord
14
+
15
+
16
+ def detect_rejected_packet(event: SentinelEvent) -> Optional[ViolationRecord]:
17
+ if event.event_type != "packet_rejected":
18
+ return None
19
+ return ViolationRecord(
20
+ violation_type="rejected_packet",
21
+ severity="ERROR",
22
+ source_event_id=event.event_id,
23
+ trace_id=event.trace_id,
24
+ detail={
25
+ "packet_id": event.attributes.get("packet_id"),
26
+ "reason": event.attributes.get("reason"),
27
+ },
28
+ )
29
+
30
+
31
+ def detect_stale_capability(event: SentinelEvent) -> Optional[ViolationRecord]:
32
+ if event.event_type != "capability_stale":
33
+ return None
34
+ return ViolationRecord(
35
+ violation_type="stale_capability",
36
+ severity="ERROR",
37
+ source_event_id=event.event_id,
38
+ trace_id=event.trace_id,
39
+ detail={
40
+ "capability_id": event.attributes.get("capability_id"),
41
+ "reason": event.attributes.get("reason"),
42
+ },
43
+ )
44
+
45
+
46
+ def detect_overlap_window(event: SentinelEvent) -> Optional[ViolationRecord]:
47
+ if event.event_type != "token_overlap_used":
48
+ return None
49
+ return ViolationRecord(
50
+ violation_type="overlap_window_used",
51
+ severity="WARN",
52
+ source_event_id=event.event_id,
53
+ trace_id=event.trace_id,
54
+ detail={
55
+ "packet_id": event.attributes.get("packet_id"),
56
+ "reason": event.attributes.get("reason"),
57
+ },
58
+ )
59
+
60
+
61
+ def detect_ai_generated_payload(event: SentinelEvent) -> Optional[ViolationRecord]:
62
+ if event.event_type != "ai_generated_payload":
63
+ return None
64
+ return ViolationRecord(
65
+ violation_type="ai_generated_payload",
66
+ severity="WARN",
67
+ source_event_id=event.event_id,
68
+ trace_id=event.trace_id,
69
+ detail={
70
+ "packet_id": event.attributes.get("packet_id"),
71
+ "source": event.attributes.get("source"),
72
+ "model": event.attributes.get("model"),
73
+ },
74
+ )
75
+
76
+
77
+ DEFAULT_DETECTORS = [
78
+ detect_rejected_packet,
79
+ detect_stale_capability,
80
+ detect_overlap_window,
81
+ detect_ai_generated_payload,
82
+ ]