agent-loop-guard-runtime 0.6.0a2__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 (76) hide show
  1. agent_loop_guard_runtime-0.6.0a2.dist-info/METADATA +407 -0
  2. agent_loop_guard_runtime-0.6.0a2.dist-info/RECORD +76 -0
  3. agent_loop_guard_runtime-0.6.0a2.dist-info/WHEEL +5 -0
  4. agent_loop_guard_runtime-0.6.0a2.dist-info/entry_points.txt +2 -0
  5. agent_loop_guard_runtime-0.6.0a2.dist-info/licenses/LICENSE +202 -0
  6. agent_loop_guard_runtime-0.6.0a2.dist-info/top_level.txt +1 -0
  7. app/__init__.py +6 -0
  8. app/api/__init__.py +1 -0
  9. app/api/admin_routes.py +179 -0
  10. app/api/anthropic_routes.py +21 -0
  11. app/api/common.py +457 -0
  12. app/api/mcp_routes.py +218 -0
  13. app/api/openai_routes.py +26 -0
  14. app/api/replay_routes.py +202 -0
  15. app/api/ui_routes.py +295 -0
  16. app/benchmark/__init__.py +2 -0
  17. app/benchmark/adapters.py +97 -0
  18. app/benchmark/data/starter-v1.json +38 -0
  19. app/benchmark/dataset.py +59 -0
  20. app/benchmark/models.py +46 -0
  21. app/benchmark/runner.py +63 -0
  22. app/benchmark/scorers.py +34 -0
  23. app/benchmark/statistics.py +48 -0
  24. app/benchmark/storage.py +78 -0
  25. app/cli.py +624 -0
  26. app/core/config.py +196 -0
  27. app/core/demo.py +109 -0
  28. app/core/loop_detector.py +120 -0
  29. app/core/policy_engine.py +167 -0
  30. app/core/redaction.py +84 -0
  31. app/core/security.py +54 -0
  32. app/core/token_meter.py +67 -0
  33. app/db/models.py +296 -0
  34. app/db/repository.py +1488 -0
  35. app/db/session.py +57 -0
  36. app/main.py +59 -0
  37. app/mcp/__init__.py +1 -0
  38. app/mcp/gateway.py +230 -0
  39. app/mcp/policy.py +281 -0
  40. app/mcp/presets/development.yml +25 -0
  41. app/mcp/presets/filesystem.yml +18 -0
  42. app/mcp/stdio.py +142 -0
  43. app/platform/__init__.py +1 -0
  44. app/platform/alembic/__init__.py +2 -0
  45. app/platform/alembic/env.py +38 -0
  46. app/platform/alembic/script.py.mako +24 -0
  47. app/platform/alembic/versions/0001_initial_schema.py +18 -0
  48. app/platform/alembic/versions/__init__.py +2 -0
  49. app/platform/events.py +44 -0
  50. app/platform/maintenance.py +138 -0
  51. app/platform/migrations.py +24 -0
  52. app/platform/setup.py +92 -0
  53. app/providers/__init__.py +5 -0
  54. app/providers/base.py +23 -0
  55. app/providers/mock.py +169 -0
  56. app/providers/upstream.py +101 -0
  57. app/replay/__init__.py +1 -0
  58. app/replay/costs.py +37 -0
  59. app/replay/formats.py +87 -0
  60. app/replay/sdk.py +102 -0
  61. app/sandbox/__init__.py +2 -0
  62. app/sandbox/policy.py +22 -0
  63. app/sandbox/workspace.py +281 -0
  64. app/static/styles.css +327 -0
  65. app/templates/agents.html +48 -0
  66. app/templates/base.html +27 -0
  67. app/templates/dashboard.html +55 -0
  68. app/templates/demo.html +36 -0
  69. app/templates/mcp.html +69 -0
  70. app/templates/policies.html +30 -0
  71. app/templates/replay.html +63 -0
  72. app/templates/replay_compare.html +74 -0
  73. app/templates/replay_detail.html +130 -0
  74. app/templates/session_detail.html +71 -0
  75. app/templates/sessions.html +24 -0
  76. app/templates/settings.html +20 -0
app/core/security.py ADDED
@@ -0,0 +1,54 @@
1
+ from __future__ import annotations
2
+
3
+ from fastapi import HTTPException, Request
4
+
5
+
6
+ def bearer_token(request: Request) -> str:
7
+ auth = request.headers.get("authorization", "")
8
+ if auth.lower().startswith("bearer "):
9
+ return auth.split(" ", 1)[1].strip()
10
+ api_key = request.headers.get("x-api-key") or request.headers.get("api-key")
11
+ if api_key:
12
+ return api_key.strip()
13
+ raise HTTPException(
14
+ status_code=401,
15
+ detail={"error": {"type": "agent_loop_guard_auth", "message": "Missing gateway key."}},
16
+ )
17
+
18
+
19
+ def external_session_id(request: Request, protocol: str) -> str | None:
20
+ explicit = request.headers.get("x-alg-session-id")
21
+ if explicit:
22
+ return explicit.strip()
23
+ if protocol == "anthropic":
24
+ claude_id = request.headers.get("x-claude-code-session-id")
25
+ if claude_id:
26
+ return claude_id.strip()
27
+ return None
28
+
29
+
30
+ def filtered_upstream_headers(headers: dict[str, str]) -> dict[str, str]:
31
+ hop_by_hop = {
32
+ "host",
33
+ "content-length",
34
+ "connection",
35
+ "keep-alive",
36
+ "proxy-authenticate",
37
+ "proxy-authorization",
38
+ "te",
39
+ "trailer",
40
+ "transfer-encoding",
41
+ "upgrade",
42
+ }
43
+ result: dict[str, str] = {}
44
+ for key, value in headers.items():
45
+ lower = key.lower()
46
+ if lower in hop_by_hop:
47
+ continue
48
+ if lower.startswith("x-alg-"):
49
+ continue
50
+ if lower in {"authorization", "x-api-key", "api-key"}:
51
+ continue
52
+ result[key] = value
53
+ return result
54
+
@@ -0,0 +1,67 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+
6
+ def _strings(value: Any) -> list[str]:
7
+ if isinstance(value, str):
8
+ return [value]
9
+ if isinstance(value, dict):
10
+ result: list[str] = []
11
+ for item in value.values():
12
+ result.extend(_strings(item))
13
+ return result
14
+ if isinstance(value, list):
15
+ result = []
16
+ for item in value:
17
+ result.extend(_strings(item))
18
+ return result
19
+ return []
20
+
21
+
22
+ def estimate_tokens(value: Any) -> int:
23
+ chars = sum(len(item) for item in _strings(value))
24
+ return max(1, (chars + 3) // 4)
25
+
26
+
27
+ def usage_from_response(protocol: str, response_json: dict[str, Any] | None) -> dict[str, int | bool] | None:
28
+ if not response_json:
29
+ return None
30
+ usage = response_json.get("usage")
31
+ if not isinstance(usage, dict):
32
+ return None
33
+ if protocol == "anthropic":
34
+ input_tokens = int(usage.get("input_tokens") or 0)
35
+ output_tokens = int(usage.get("output_tokens") or 0)
36
+ return {
37
+ "input_tokens": input_tokens,
38
+ "output_tokens": output_tokens,
39
+ "total_tokens": input_tokens + output_tokens,
40
+ "estimated": False,
41
+ }
42
+ input_tokens = int(usage.get("input_tokens") or usage.get("prompt_tokens") or 0)
43
+ output_tokens = int(usage.get("output_tokens") or usage.get("completion_tokens") or 0)
44
+ total = int(usage.get("total_tokens") or input_tokens + output_tokens)
45
+ return {
46
+ "input_tokens": input_tokens,
47
+ "output_tokens": output_tokens,
48
+ "total_tokens": total,
49
+ "estimated": False,
50
+ }
51
+
52
+
53
+ def usage_or_estimate(
54
+ protocol: str, request_body: Any, response_json: dict[str, Any] | None = None
55
+ ) -> dict[str, int | bool]:
56
+ usage = usage_from_response(protocol, response_json)
57
+ if usage is not None and int(usage["total_tokens"]) > 0:
58
+ return usage
59
+ input_tokens = estimate_tokens(request_body)
60
+ output_tokens = estimate_tokens(response_json or "")
61
+ return {
62
+ "input_tokens": input_tokens,
63
+ "output_tokens": output_tokens,
64
+ "total_tokens": input_tokens + output_tokens,
65
+ "estimated": True,
66
+ }
67
+
app/db/models.py ADDED
@@ -0,0 +1,296 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import UTC, datetime
4
+
5
+ from sqlalchemy import BigInteger, Boolean, DateTime, ForeignKey, Integer, String, Text
6
+ from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
7
+
8
+
9
+ def utcnow() -> datetime:
10
+ return datetime.now(UTC)
11
+
12
+
13
+ class Base(DeclarativeBase):
14
+ pass
15
+
16
+
17
+ class Project(Base):
18
+ __tablename__ = "projects"
19
+
20
+ id: Mapped[str] = mapped_column(String(64), primary_key=True)
21
+ name: Mapped[str] = mapped_column(String(160), nullable=False)
22
+ mode: Mapped[str] = mapped_column(String(32), nullable=False, default="shadow")
23
+ provider: Mapped[str] = mapped_column(String(32), nullable=False, default="mock")
24
+ created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow)
25
+
26
+ agents: Mapped[list[Agent]] = relationship(back_populates="project")
27
+ policies: Mapped[list[Policy]] = relationship(back_populates="project")
28
+
29
+
30
+ class Agent(Base):
31
+ __tablename__ = "agents"
32
+
33
+ id: Mapped[str] = mapped_column(String(64), primary_key=True)
34
+ project_id: Mapped[str] = mapped_column(ForeignKey("projects.id"), nullable=False)
35
+ name: Mapped[str] = mapped_column(String(160), nullable=False)
36
+ protocol: Mapped[str] = mapped_column(String(32), nullable=False, default="openai")
37
+ key_hash: Mapped[str] = mapped_column(String(96), nullable=False, index=True)
38
+ enabled: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
39
+ paused: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
40
+ created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow)
41
+
42
+ project: Mapped[Project] = relationship(back_populates="agents")
43
+
44
+
45
+ class Policy(Base):
46
+ __tablename__ = "policies"
47
+
48
+ id: Mapped[str] = mapped_column(String(64), primary_key=True)
49
+ project_id: Mapped[str] = mapped_column(ForeignKey("projects.id"), nullable=False, index=True)
50
+ rule_type: Mapped[str] = mapped_column(String(64), nullable=False)
51
+ threshold: Mapped[int] = mapped_column(Integer, nullable=False)
52
+ window_size: Mapped[int] = mapped_column(Integer, nullable=False, default=8)
53
+ action: Mapped[str] = mapped_column(String(32), nullable=False, default="block")
54
+ enabled: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
55
+
56
+ project: Mapped[Project] = relationship(back_populates="policies")
57
+
58
+
59
+ class GuardSession(Base):
60
+ __tablename__ = "sessions"
61
+
62
+ id: Mapped[str] = mapped_column(String(64), primary_key=True)
63
+ project_id: Mapped[str] = mapped_column(ForeignKey("projects.id"), nullable=False, index=True)
64
+ agent_id: Mapped[str] = mapped_column(ForeignKey("agents.id"), nullable=False, index=True)
65
+ external_session_id: Mapped[str | None] = mapped_column(String(160), nullable=True, index=True)
66
+ status: Mapped[str] = mapped_column(String(32), nullable=False, default="active")
67
+ name: Mapped[str | None] = mapped_column(String(200), nullable=True)
68
+ note: Mapped[str | None] = mapped_column(Text, nullable=True)
69
+ started_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow)
70
+ updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow)
71
+ ended_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
72
+ request_count: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
73
+ input_tokens: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
74
+ output_tokens: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
75
+ total_tokens: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
76
+ flagged_count: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
77
+ blocked_count: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
78
+
79
+ requests: Mapped[list[RequestRecord]] = relationship(back_populates="session")
80
+ events: Mapped[list[Event]] = relationship(back_populates="session")
81
+
82
+
83
+ class RequestRecord(Base):
84
+ __tablename__ = "requests"
85
+
86
+ id: Mapped[str] = mapped_column(String(64), primary_key=True)
87
+ session_id: Mapped[str] = mapped_column(ForeignKey("sessions.id"), nullable=False, index=True)
88
+ protocol: Mapped[str] = mapped_column(String(32), nullable=False)
89
+ endpoint: Mapped[str] = mapped_column(String(160), nullable=False)
90
+ model: Mapped[str | None] = mapped_column(String(200), nullable=True)
91
+ fingerprint: Mapped[str] = mapped_column(String(96), nullable=False, index=True)
92
+ error_fingerprint: Mapped[str | None] = mapped_column(String(96), nullable=True, index=True)
93
+ status: Mapped[int] = mapped_column(Integer, nullable=False)
94
+ latency_ms: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
95
+ input_tokens: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
96
+ output_tokens: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
97
+ total_tokens: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
98
+ estimated_tokens: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
99
+ decision: Mapped[str] = mapped_column(String(32), nullable=False, default="allow")
100
+ reason_code: Mapped[str | None] = mapped_column(String(64), nullable=True)
101
+ request_preview: Mapped[str | None] = mapped_column(Text, nullable=True)
102
+ response_preview: Mapped[str | None] = mapped_column(Text, nullable=True)
103
+ created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow)
104
+
105
+ session: Mapped[GuardSession] = relationship(back_populates="requests")
106
+ tool_calls: Mapped[list[ToolCall]] = relationship(back_populates="request")
107
+
108
+
109
+ class Event(Base):
110
+ __tablename__ = "events"
111
+
112
+ id: Mapped[str] = mapped_column(String(64), primary_key=True)
113
+ session_id: Mapped[str] = mapped_column(ForeignKey("sessions.id"), nullable=False, index=True)
114
+ request_id: Mapped[str | None] = mapped_column(ForeignKey("requests.id"), nullable=True)
115
+ type: Mapped[str] = mapped_column(String(64), nullable=False)
116
+ rule_id: Mapped[str | None] = mapped_column(String(64), nullable=True)
117
+ severity: Mapped[str] = mapped_column(String(32), nullable=False, default="info")
118
+ reason_code: Mapped[str | None] = mapped_column(String(64), nullable=True)
119
+ mode: Mapped[str] = mapped_column(String(32), nullable=False, default="shadow")
120
+ payload_json: Mapped[str] = mapped_column(Text, nullable=False, default="{}")
121
+ created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow)
122
+
123
+ session: Mapped[GuardSession] = relationship(back_populates="events")
124
+
125
+
126
+ class ToolCall(Base):
127
+ __tablename__ = "tool_calls"
128
+
129
+ id: Mapped[str] = mapped_column(String(64), primary_key=True)
130
+ request_id: Mapped[str] = mapped_column(ForeignKey("requests.id"), nullable=False, index=True)
131
+ tool_name: Mapped[str] = mapped_column(String(160), nullable=False)
132
+ args_hash: Mapped[str] = mapped_column(String(96), nullable=False, index=True)
133
+ result_status: Mapped[str | None] = mapped_column(String(64), nullable=True)
134
+
135
+ request: Mapped[RequestRecord] = relationship(back_populates="tool_calls")
136
+
137
+
138
+ class TraceRun(Base):
139
+ __tablename__ = "trace_runs"
140
+
141
+ id: Mapped[str] = mapped_column(String(64), primary_key=True)
142
+ source_session_id: Mapped[str | None] = mapped_column(
143
+ ForeignKey("sessions.id"), nullable=True, index=True
144
+ )
145
+ project_id: Mapped[str] = mapped_column(String(64), nullable=False, index=True, default="default")
146
+ task_id: Mapped[str | None] = mapped_column(String(160), nullable=True, index=True)
147
+ task_fingerprint: Mapped[str | None] = mapped_column(String(96), nullable=True, index=True)
148
+ agent_name: Mapped[str | None] = mapped_column(String(160), nullable=True)
149
+ model: Mapped[str | None] = mapped_column(String(200), nullable=True)
150
+ status: Mapped[str] = mapped_column(String(32), nullable=False, default="running", index=True)
151
+ failure_tag: Mapped[str | None] = mapped_column(String(80), nullable=True, index=True)
152
+ input_tokens: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
153
+ output_tokens: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
154
+ total_tokens: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
155
+ total_cost_micros: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
156
+ duration_ms: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
157
+ span_count: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
158
+ event_count: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
159
+ pinned: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
160
+ attributes_json: Mapped[str] = mapped_column(Text, nullable=False, default="{}")
161
+ created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow)
162
+ updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow)
163
+
164
+ spans: Mapped[list[TraceSpan]] = relationship(
165
+ back_populates="run", cascade="all, delete-orphan"
166
+ )
167
+ events: Mapped[list[TraceEvent]] = relationship(
168
+ back_populates="run", cascade="all, delete-orphan"
169
+ )
170
+ artifacts: Mapped[list[TraceArtifact]] = relationship(
171
+ back_populates="run", cascade="all, delete-orphan"
172
+ )
173
+
174
+
175
+ class TraceSpan(Base):
176
+ __tablename__ = "trace_spans"
177
+
178
+ id: Mapped[str] = mapped_column(String(64), primary_key=True)
179
+ trace_id: Mapped[str] = mapped_column(ForeignKey("trace_runs.id"), nullable=False, index=True)
180
+ parent_span_id: Mapped[str | None] = mapped_column(String(64), nullable=True, index=True)
181
+ name: Mapped[str] = mapped_column(String(120), nullable=False, index=True)
182
+ status: Mapped[str] = mapped_column(String(32), nullable=False, default="ok", index=True)
183
+ start_ns: Mapped[int] = mapped_column(BigInteger, nullable=False)
184
+ end_ns: Mapped[int | None] = mapped_column(BigInteger, nullable=True)
185
+ duration_ms: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
186
+ attributes_json: Mapped[str] = mapped_column(Text, nullable=False, default="{}")
187
+
188
+ run: Mapped[TraceRun] = relationship(back_populates="spans")
189
+ events: Mapped[list[TraceEvent]] = relationship(
190
+ back_populates="span", cascade="all, delete-orphan"
191
+ )
192
+
193
+
194
+ class TraceEvent(Base):
195
+ __tablename__ = "trace_events"
196
+
197
+ id: Mapped[str] = mapped_column(String(64), primary_key=True)
198
+ trace_id: Mapped[str] = mapped_column(ForeignKey("trace_runs.id"), nullable=False, index=True)
199
+ span_id: Mapped[str | None] = mapped_column(ForeignKey("trace_spans.id"), nullable=True, index=True)
200
+ name: Mapped[str] = mapped_column(String(120), nullable=False, index=True)
201
+ severity: Mapped[str] = mapped_column(String(32), nullable=False, default="info")
202
+ timestamp_ns: Mapped[int] = mapped_column(BigInteger, nullable=False)
203
+ attributes_json: Mapped[str] = mapped_column(Text, nullable=False, default="{}")
204
+
205
+ run: Mapped[TraceRun] = relationship(back_populates="events")
206
+ span: Mapped[TraceSpan | None] = relationship(back_populates="events")
207
+
208
+
209
+ class TraceArtifact(Base):
210
+ __tablename__ = "trace_artifacts"
211
+
212
+ id: Mapped[str] = mapped_column(String(64), primary_key=True)
213
+ trace_id: Mapped[str] = mapped_column(ForeignKey("trace_runs.id"), nullable=False, index=True)
214
+ path: Mapped[str] = mapped_column(String(500), nullable=False)
215
+ mime_type: Mapped[str | None] = mapped_column(String(120), nullable=True)
216
+ size: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
217
+ sha256: Mapped[str | None] = mapped_column(String(96), nullable=True, index=True)
218
+ attributes_json: Mapped[str] = mapped_column(Text, nullable=False, default="{}")
219
+ created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow)
220
+
221
+ run: Mapped[TraceRun] = relationship(back_populates="artifacts")
222
+
223
+
224
+ class MCPServer(Base):
225
+ __tablename__ = "mcp_servers"
226
+
227
+ id: Mapped[str] = mapped_column(String(64), primary_key=True)
228
+ name: Mapped[str] = mapped_column(String(160), nullable=False)
229
+ transport: Mapped[str] = mapped_column(String(32), nullable=False, default="stdio")
230
+ target: Mapped[str] = mapped_column(String(500), nullable=False)
231
+ fingerprint: Mapped[str] = mapped_column(String(96), nullable=False, index=True)
232
+ enabled: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
233
+ first_seen_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow)
234
+
235
+
236
+ class MCPSession(Base):
237
+ __tablename__ = "mcp_sessions"
238
+
239
+ id: Mapped[str] = mapped_column(String(64), primary_key=True)
240
+ server_id: Mapped[str] = mapped_column(ForeignKey("mcp_servers.id"), nullable=False, index=True)
241
+ client_name: Mapped[str | None] = mapped_column(String(160), nullable=True)
242
+ protocol_version: Mapped[str | None] = mapped_column(String(32), nullable=True)
243
+ mode: Mapped[str] = mapped_column(String(32), nullable=False, default="enforce")
244
+ started_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow)
245
+ ended_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
246
+
247
+
248
+ class MCPToolSnapshot(Base):
249
+ __tablename__ = "mcp_tool_snapshots"
250
+
251
+ id: Mapped[str] = mapped_column(String(64), primary_key=True)
252
+ session_id: Mapped[str] = mapped_column(ForeignKey("mcp_sessions.id"), nullable=False, index=True)
253
+ server_id: Mapped[str] = mapped_column(ForeignKey("mcp_servers.id"), nullable=False, index=True)
254
+ tool_name: Mapped[str] = mapped_column(String(160), nullable=False, index=True)
255
+ schema_hash: Mapped[str] = mapped_column(String(96), nullable=False)
256
+ schema_json: Mapped[str] = mapped_column(Text, nullable=False, default="{}")
257
+ risk_tags_json: Mapped[str] = mapped_column(Text, nullable=False, default="[]")
258
+ created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow)
259
+
260
+
261
+ class MCPDecisionEvent(Base):
262
+ __tablename__ = "mcp_decision_events"
263
+
264
+ id: Mapped[str] = mapped_column(String(64), primary_key=True)
265
+ session_id: Mapped[str | None] = mapped_column(
266
+ ForeignKey("mcp_sessions.id"), nullable=True, index=True
267
+ )
268
+ server_id: Mapped[str] = mapped_column(ForeignKey("mcp_servers.id"), nullable=False, index=True)
269
+ trace_id: Mapped[str | None] = mapped_column(String(64), nullable=True, index=True)
270
+ request_id: Mapped[str | None] = mapped_column(String(160), nullable=True, index=True)
271
+ tool_name: Mapped[str] = mapped_column(String(160), nullable=False, index=True)
272
+ argument_hash: Mapped[str] = mapped_column(String(96), nullable=False, index=True)
273
+ policy_version: Mapped[str] = mapped_column(String(64), nullable=False)
274
+ action: Mapped[str] = mapped_column(String(32), nullable=False, index=True)
275
+ reason: Mapped[str] = mapped_column(String(300), nullable=False)
276
+ rule_id: Mapped[str | None] = mapped_column(String(160), nullable=True)
277
+ mode: Mapped[str] = mapped_column(String(32), nullable=False, default="enforce")
278
+ latency_ms: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
279
+ result_status: Mapped[str | None] = mapped_column(String(64), nullable=True)
280
+ attributes_json: Mapped[str] = mapped_column(Text, nullable=False, default="{}")
281
+ created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow)
282
+
283
+
284
+ class MCPApproval(Base):
285
+ __tablename__ = "mcp_approvals"
286
+
287
+ id: Mapped[str] = mapped_column(String(64), primary_key=True)
288
+ decision_event_id: Mapped[str] = mapped_column(
289
+ ForeignKey("mcp_decision_events.id"), nullable=False, index=True
290
+ )
291
+ status: Mapped[str] = mapped_column(String(32), nullable=False, default="pending", index=True)
292
+ scope: Mapped[str] = mapped_column(String(32), nullable=False, default="once")
293
+ argument_hash: Mapped[str] = mapped_column(String(96), nullable=False)
294
+ expires_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
295
+ decided_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
296
+ created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow)