openscript-sdk 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.
Files changed (65) hide show
  1. openscript_sdk/__init__.py +217 -0
  2. openscript_sdk/contracts/__init__.py +82 -0
  3. openscript_sdk/contracts/interceptor.py +98 -0
  4. openscript_sdk/contracts/server_types.py +87 -0
  5. openscript_sdk/contracts/session_graph.py +123 -0
  6. openscript_sdk/contracts/types.py +76 -0
  7. openscript_sdk/events/__init__.py +0 -0
  8. openscript_sdk/events/approvals.py +309 -0
  9. openscript_sdk/events/models.py +26 -0
  10. openscript_sdk/events/store.py +79 -0
  11. openscript_sdk/events/writer.py +100 -0
  12. openscript_sdk/hooks/claude_code.py +479 -0
  13. openscript_sdk/hooks/claude_code_example.yaml +62 -0
  14. openscript_sdk/hooks/claude_code_strict_example.yaml +73 -0
  15. openscript_sdk/hooks/config.py +144 -0
  16. openscript_sdk/hooks/settings_example.json +28 -0
  17. openscript_sdk/hooks/settings_strict_example.json +28 -0
  18. openscript_sdk/integrations/langchain.py +32 -0
  19. openscript_sdk/integrations/langgraph.py +32 -0
  20. openscript_sdk/interceptors/base.py +15 -0
  21. openscript_sdk/interceptors/event_writer.py +131 -0
  22. openscript_sdk/interceptors/pii.py +168 -0
  23. openscript_sdk/interceptors/threat.py +674 -0
  24. openscript_sdk/logging.py +94 -0
  25. openscript_sdk/middleware/middleware.py +543 -0
  26. openscript_sdk/middleware/tool.py +247 -0
  27. openscript_sdk/observability/__init__.py +11 -0
  28. openscript_sdk/observability/metrics.py +105 -0
  29. openscript_sdk/observability/risk.py +54 -0
  30. openscript_sdk/observability/tracing.py +67 -0
  31. openscript_sdk/policies/compliance.py +261 -0
  32. openscript_sdk/policies/config.py +244 -0
  33. openscript_sdk/policies/harmful_request.py +617 -0
  34. openscript_sdk/policies/normalize.py +489 -0
  35. openscript_sdk/policies/output_schema.py +353 -0
  36. openscript_sdk/policies/policies_example.yaml +36 -0
  37. openscript_sdk/policies/redaction.py +359 -0
  38. openscript_sdk/policies/secrets.py +239 -0
  39. openscript_sdk/policies/semantic.py +348 -0
  40. openscript_sdk/policies/semantic_head.py +9 -0
  41. openscript_sdk/policies/session_taint.py +399 -0
  42. openscript_sdk/policies/taint_store.py +111 -0
  43. openscript_sdk/policies/tool_firewall.py +434 -0
  44. openscript_sdk/policies/tools_example.yaml +81 -0
  45. openscript_sdk/policies/toxicity.py +247 -0
  46. openscript_sdk/py.typed +0 -0
  47. openscript_sdk/server/__init__.py +1 -0
  48. openscript_sdk/server/app.py +153 -0
  49. openscript_sdk/server/auth.py +19 -0
  50. openscript_sdk/server/guard.py +126 -0
  51. openscript_sdk/server/guard_example.yaml +55 -0
  52. openscript_sdk/server/routers/__init__.py +0 -0
  53. openscript_sdk/server/routers/approvals.py +69 -0
  54. openscript_sdk/server/routers/guard.py +231 -0
  55. openscript_sdk/server/routers/sessions.py +80 -0
  56. openscript_sdk/server/routers/sse.py +69 -0
  57. openscript_sdk/server/routers/threat.py +119 -0
  58. openscript_sdk/server/static/dashboard/index.html +209 -0
  59. openscript_sdk/server/static/dashboard/session.html +339 -0
  60. openscript_sdk-0.1.0.dist-info/METADATA +225 -0
  61. openscript_sdk-0.1.0.dist-info/RECORD +65 -0
  62. openscript_sdk-0.1.0.dist-info/WHEEL +5 -0
  63. openscript_sdk-0.1.0.dist-info/entry_points.txt +2 -0
  64. openscript_sdk-0.1.0.dist-info/licenses/LICENSE +190 -0
  65. openscript_sdk-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,209 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>OpenScript — Sessions</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
9
+ <style>
10
+ [x-cloak] { display: none !important; }
11
+ .threat-row { background-color: #fff1f2; }
12
+ .threat-row:hover { background-color: #ffe4e6; }
13
+ </style>
14
+ </head>
15
+ <body class="bg-gray-50 text-gray-900 min-h-screen" x-data="app()" x-init="init()" x-cloak>
16
+
17
+ <!-- API key modal -->
18
+ <div x-show="showModal"
19
+ class="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
20
+ <div class="bg-white rounded-xl shadow-2xl p-6 w-96">
21
+ <h2 class="text-lg font-semibold mb-3">Set API Key</h2>
22
+ <p class="text-sm text-gray-500 mb-4">
23
+ Enter your <code class="bg-gray-100 px-1 rounded">OPENSCRIPT_API_KEY</code> to authenticate requests.
24
+ </p>
25
+ <input x-model="inputKey" type="password" placeholder="API key"
26
+ @keydown.enter="saveKey()"
27
+ class="w-full border rounded-lg px-3 py-2 text-sm mb-4 focus:outline-none focus:ring-2 focus:ring-blue-500" />
28
+ <div class="flex gap-2 justify-end">
29
+ <button x-show="apiKey" @click="showModal = false"
30
+ class="px-4 py-2 text-sm rounded-lg border hover:bg-gray-50">Cancel</button>
31
+ <button @click="saveKey()"
32
+ class="px-4 py-2 text-sm rounded-lg bg-blue-600 text-white hover:bg-blue-700">Save</button>
33
+ </div>
34
+ </div>
35
+ </div>
36
+
37
+ <!-- Header -->
38
+ <header class="bg-gray-900 text-white px-6 py-4 flex items-center gap-3">
39
+ <svg class="w-6 h-6 text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
40
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
41
+ d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"/>
42
+ </svg>
43
+ <span class="text-lg font-semibold tracking-tight">OpenScript</span>
44
+ <span class="text-gray-400 text-sm ml-1">Agent Security &amp; Observability</span>
45
+ <div class="ml-auto">
46
+ <span x-show="!apiKey" class="text-xs text-yellow-300">⚠ No API key set</span>
47
+ </div>
48
+ </header>
49
+
50
+ <!-- Main -->
51
+ <main class="max-w-6xl mx-auto px-6 py-8">
52
+
53
+ <!-- Toolbar -->
54
+ <div class="flex items-center justify-between mb-6">
55
+ <div>
56
+ <h1 class="text-2xl font-bold">Sessions</h1>
57
+ <p class="text-sm text-gray-500 mt-0.5">All agent sessions recorded by the event store</p>
58
+ </div>
59
+ <div class="flex items-center gap-3">
60
+ <span x-show="loading" class="text-sm text-gray-400 animate-pulse">Refreshing…</span>
61
+ <span x-show="error" x-text="error" class="text-sm text-red-500"></span>
62
+ <button @click="showModal = true; inputKey = apiKey"
63
+ class="text-sm px-3 py-1.5 rounded-lg border hover:bg-gray-100">
64
+ 🔑 API Key
65
+ </button>
66
+ <button @click="fetchSessions()"
67
+ class="text-sm px-3 py-1.5 rounded-lg bg-blue-600 text-white hover:bg-blue-700">
68
+ Refresh
69
+ </button>
70
+ </div>
71
+ </div>
72
+
73
+ <!-- Stats strip -->
74
+ <div class="grid grid-cols-3 gap-4 mb-6">
75
+ <div class="bg-white rounded-xl border p-4">
76
+ <div class="text-2xl font-bold" x-text="sessions.length"></div>
77
+ <div class="text-sm text-gray-500">Total sessions</div>
78
+ </div>
79
+ <div class="bg-white rounded-xl border p-4">
80
+ <div class="text-2xl font-bold text-red-600"
81
+ x-text="sessions.filter(s => s.flagged).length"></div>
82
+ <div class="text-sm text-gray-500">Flagged sessions</div>
83
+ </div>
84
+ <div class="bg-white rounded-xl border p-4">
85
+ <div class="text-2xl font-bold"
86
+ x-text="sessions.reduce((a, s) => a + s.event_count, 0)"></div>
87
+ <div class="text-sm text-gray-500">Total events</div>
88
+ </div>
89
+ </div>
90
+
91
+ <!-- Empty state -->
92
+ <div x-show="!loading && sessions.length === 0 && !error"
93
+ class="bg-white rounded-xl border p-16 text-center">
94
+ <div class="text-4xl mb-3">🔍</div>
95
+ <div class="text-lg font-medium mb-1">No sessions yet</div>
96
+ <div class="text-sm text-gray-500">
97
+ Run <code class="bg-gray-100 px-1 rounded">python demo/injection_demo.py</code> to generate some.
98
+ </div>
99
+ </div>
100
+
101
+ <!-- Sessions table -->
102
+ <div x-show="sessions.length > 0" class="bg-white rounded-xl border overflow-hidden">
103
+ <table class="w-full text-sm">
104
+ <thead class="bg-gray-50 border-b">
105
+ <tr>
106
+ <th class="text-left px-4 py-3 font-medium text-gray-600">Session ID</th>
107
+ <th class="text-right px-4 py-3 font-medium text-gray-600">Events</th>
108
+ <th class="text-right px-4 py-3 font-medium text-gray-600">Threats</th>
109
+ <th class="text-left px-4 py-3 font-medium text-gray-600">Status</th>
110
+ <th class="text-right px-4 py-3 font-medium text-gray-600">Last Active</th>
111
+ </tr>
112
+ </thead>
113
+ <tbody>
114
+ <template x-for="s in sessions" :key="s.session_id">
115
+ <tr :class="s.flagged ? 'threat-row cursor-pointer' : 'hover:bg-gray-50 cursor-pointer'"
116
+ @click="openSession(s.session_id)">
117
+ <td class="px-4 py-3 font-mono text-xs" x-text="s.session_id"></td>
118
+ <td class="px-4 py-3 text-right" x-text="s.event_count"></td>
119
+ <td class="px-4 py-3 text-right">
120
+ <span x-show="s.threat_count > 0"
121
+ class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-700"
122
+ x-text="s.threat_count + ' threat' + (s.threat_count !== 1 ? 's' : '')"></span>
123
+ <span x-show="s.threat_count === 0" class="text-gray-400">—</span>
124
+ </td>
125
+ <td class="px-4 py-3">
126
+ <span x-show="s.flagged"
127
+ class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-700">
128
+ 🚨 Flagged
129
+ </span>
130
+ <span x-show="!s.flagged"
131
+ class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-700">
132
+ ✓ Clean
133
+ </span>
134
+ </td>
135
+ <td class="px-4 py-3 text-right text-gray-400 text-xs"
136
+ x-text="formatTime(s.last_activity)"></td>
137
+ </tr>
138
+ </template>
139
+ </tbody>
140
+ </table>
141
+ </div>
142
+
143
+ <p class="text-xs text-gray-400 mt-4 text-right">Auto-refreshes every 5 seconds</p>
144
+ </main>
145
+
146
+ <script>
147
+ function app() {
148
+ return {
149
+ apiKey: localStorage.getItem('openscript_api_key') || '',
150
+ showModal: false,
151
+ inputKey: '',
152
+ sessions: [],
153
+ loading: false,
154
+ error: '',
155
+
156
+ init() {
157
+ this.showModal = !this.apiKey;
158
+ this.fetchSessions();
159
+ setInterval(() => this.fetchSessions(), 5000);
160
+ },
161
+
162
+ saveKey() {
163
+ const key = this.inputKey.trim();
164
+ if (!key) return;
165
+ this.apiKey = key;
166
+ localStorage.setItem('openscript_api_key', key);
167
+ this.showModal = false;
168
+ this.error = '';
169
+ this.fetchSessions();
170
+ },
171
+
172
+ async fetchSessions() {
173
+ if (!this.apiKey) return;
174
+ this.loading = true;
175
+ this.error = '';
176
+ try {
177
+ const res = await fetch('/sessions', {
178
+ headers: { 'X-API-KEY': this.apiKey },
179
+ });
180
+ if (res.status === 401) {
181
+ this.error = 'Invalid API key';
182
+ this.showModal = true;
183
+ return;
184
+ }
185
+ if (!res.ok) { this.error = `Server error ${res.status}`; return; }
186
+ const data = await res.json();
187
+ this.sessions = data.sessions || [];
188
+ } catch {
189
+ this.error = 'Cannot reach server';
190
+ } finally {
191
+ this.loading = false;
192
+ }
193
+ },
194
+
195
+ openSession(id) {
196
+ window.location.href = `/dashboard/session.html?session_id=${encodeURIComponent(id)}`;
197
+ },
198
+
199
+ formatTime(iso) {
200
+ if (!iso) return '—';
201
+ return new Date(iso).toLocaleString(undefined, {
202
+ month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit',
203
+ });
204
+ },
205
+ };
206
+ }
207
+ </script>
208
+ </body>
209
+ </html>
@@ -0,0 +1,339 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>OpenScript — Session Detail</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
9
+ <script src="https://cdn.jsdelivr.net/npm/vis-network@9.1.9/standalone/umd/vis-network.min.js"></script>
10
+ <style>
11
+ [x-cloak] { display: none !important; }
12
+ #graph-container {
13
+ width: 100%;
14
+ height: 480px;
15
+ border-radius: 0.5rem;
16
+ border: 1px solid #e5e7eb;
17
+ background: #fafafa;
18
+ }
19
+ .event-threat { background: #fff1f2; border-left: 3px solid #f43f5e; }
20
+ .event-interceptor { border-left: 3px solid #8b5cf6; }
21
+ .event-tool { border-left: 3px solid #f97316; }
22
+ .event-turn { border-left: 3px solid #3b82f6; }
23
+ .event-default { border-left: 3px solid #9ca3af; }
24
+ </style>
25
+ </head>
26
+ <body class="bg-gray-50 text-gray-900 min-h-screen" x-data="sessionApp()" x-init="init()" x-cloak>
27
+
28
+ <!-- API key modal -->
29
+ <div x-show="showKeyModal"
30
+ class="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
31
+ <div class="bg-white rounded-xl shadow-2xl p-6 w-96">
32
+ <h2 class="text-lg font-semibold mb-3">Set API Key</h2>
33
+ <p class="text-sm text-gray-500 mb-4">
34
+ Enter your <code class="bg-gray-100 px-1 rounded">OPENSCRIPT_API_KEY</code> to authenticate requests.
35
+ </p>
36
+ <input x-model="inputKey" type="password" placeholder="API key"
37
+ @keydown.enter="saveKey()"
38
+ class="w-full border rounded-lg px-3 py-2 text-sm mb-4 focus:outline-none focus:ring-2 focus:ring-blue-500" />
39
+ <div class="flex gap-2 justify-end">
40
+ <button x-show="apiKey" @click="showKeyModal = false"
41
+ class="px-4 py-2 text-sm rounded-lg border hover:bg-gray-50">Cancel</button>
42
+ <button @click="saveKey()"
43
+ class="px-4 py-2 text-sm rounded-lg bg-blue-600 text-white hover:bg-blue-700">Save</button>
44
+ </div>
45
+ </div>
46
+ </div>
47
+
48
+ <!-- Header -->
49
+ <header class="bg-gray-900 text-white px-6 py-4 flex items-center gap-3">
50
+ <a href="/dashboard/" class="text-gray-400 hover:text-white text-sm">← Sessions</a>
51
+ <svg class="w-5 h-5 text-red-400 ml-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
52
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
53
+ d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"/>
54
+ </svg>
55
+ <span class="text-lg font-semibold tracking-tight">OpenScript</span>
56
+ <button @click="showKeyModal = true; inputKey = apiKey"
57
+ class="ml-auto text-sm px-3 py-1.5 rounded-lg border border-gray-600 hover:border-gray-400 text-gray-300 hover:text-white">
58
+ 🔑 API Key
59
+ </button>
60
+ </header>
61
+
62
+ <main class="max-w-7xl mx-auto px-6 py-8">
63
+
64
+ <!-- Error banner -->
65
+ <div x-show="error"
66
+ class="mb-4 bg-red-50 border border-red-200 text-red-700 rounded-lg px-4 py-3 text-sm"
67
+ x-text="error"></div>
68
+
69
+ <!-- Session ID + stats strip -->
70
+ <div class="flex items-start justify-between mb-6">
71
+ <div>
72
+ <h1 class="text-xl font-bold font-mono" x-text="sessionId || 'Loading…'"></h1>
73
+ <p class="text-sm text-gray-500 mt-0.5">Session detail — event timeline &amp; causal graph</p>
74
+ </div>
75
+ <div class="flex gap-3 text-sm">
76
+ <div class="bg-white border rounded-lg px-4 py-2 text-center">
77
+ <div class="font-bold text-lg" x-text="stats.event_count || 0"></div>
78
+ <div class="text-gray-500 text-xs">Events</div>
79
+ </div>
80
+ <div class="bg-white border rounded-lg px-4 py-2 text-center"
81
+ :class="stats.threat_count > 0 ? 'border-red-300 bg-red-50' : ''">
82
+ <div class="font-bold text-lg" :class="stats.threat_count > 0 ? 'text-red-600' : ''"
83
+ x-text="stats.threat_count || 0"></div>
84
+ <div class="text-gray-500 text-xs">Threats</div>
85
+ </div>
86
+ <div class="bg-white border rounded-lg px-4 py-2 text-center">
87
+ <div class="font-bold text-lg"
88
+ :class="(stats.max_threat_score || 0) >= 0.7 ? 'text-red-600' : 'text-green-600'"
89
+ x-text="stats.max_threat_score ? stats.max_threat_score.toFixed(2) : '0.00'"></div>
90
+ <div class="text-gray-500 text-xs">Max Score</div>
91
+ </div>
92
+ <div class="bg-white border rounded-lg px-4 py-2 text-center">
93
+ <span x-show="stats.flagged"
94
+ class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-700">
95
+ 🚨 Flagged
96
+ </span>
97
+ <span x-show="!stats.flagged"
98
+ class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-700">
99
+ ✓ Clean
100
+ </span>
101
+ <div class="text-gray-500 text-xs mt-0.5">Status</div>
102
+ </div>
103
+ </div>
104
+ </div>
105
+
106
+ <!-- Two-column layout -->
107
+ <div class="grid grid-cols-2 gap-6">
108
+
109
+ <!-- Left: Graph -->
110
+ <div class="bg-white rounded-xl border p-4">
111
+ <div class="flex items-center justify-between mb-3">
112
+ <h2 class="font-semibold text-sm">Causal Session Graph</h2>
113
+ <div class="flex gap-3 text-xs text-gray-500">
114
+ <span class="flex items-center gap-1"><span class="w-3 h-3 rounded-full bg-blue-500 inline-block"></span>Turn</span>
115
+ <span class="flex items-center gap-1"><span class="w-3 h-3 rounded-full bg-red-500 inline-block"></span>Threat</span>
116
+ <span class="flex items-center gap-1"><span class="w-3 h-3 rounded-full bg-purple-500 inline-block"></span>Interceptor</span>
117
+ <span class="flex items-center gap-1"><span class="w-3 h-3 rounded-full bg-orange-400 inline-block"></span>Tool</span>
118
+ </div>
119
+ </div>
120
+ <div id="graph-container"></div>
121
+ <p x-show="graphLoading" class="text-xs text-gray-400 text-center mt-2">Building graph…</p>
122
+ </div>
123
+
124
+ <!-- Right: Event timeline -->
125
+ <div class="bg-white rounded-xl border">
126
+ <div class="flex items-center justify-between px-4 py-3 border-b">
127
+ <h2 class="font-semibold text-sm">Event Timeline</h2>
128
+ <span class="flex items-center gap-1 text-xs text-blue-600">
129
+ <span class="w-2 h-2 rounded-full bg-blue-500 animate-pulse inline-block"></span>
130
+ Polling
131
+ </span>
132
+ </div>
133
+ <div class="divide-y overflow-y-auto" style="max-height: 440px;">
134
+ <template x-for="ev in events" :key="ev.id">
135
+ <div class="px-4 py-2.5 text-xs" :class="eventClass(ev)">
136
+ <div class="flex items-center justify-between mb-1">
137
+ <span class="font-medium font-mono" x-text="ev.event_type"></span>
138
+ <span class="text-gray-400" x-text="formatTime(ev.timestamp)"></span>
139
+ </div>
140
+ <div class="text-gray-600 font-mono truncate" x-text="payloadSummary(ev)"></div>
141
+ <div x-show="ev.event_type === 'threat_detected'" class="mt-1">
142
+ <span class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full bg-red-100 text-red-700 font-medium">
143
+ score: <span x-text="ev.payload?.score?.toFixed ? ev.payload.score.toFixed(3) : ev.payload?.score"></span>
144
+ </span>
145
+ <span class="ml-1 text-gray-500"
146
+ x-text="Object.keys(ev.payload?.signals || {}).join(', ')"></span>
147
+ </div>
148
+ </div>
149
+ </template>
150
+ <div x-show="events.length === 0 && !loading"
151
+ class="px-4 py-8 text-center text-gray-400 text-sm">No events yet</div>
152
+ </div>
153
+ </div>
154
+
155
+ </div>
156
+ </main>
157
+
158
+ <script>
159
+ const NODE_COLORS = {
160
+ turn: { background: '#3b82f6', border: '#1d4ed8' },
161
+ threat: { background: '#ef4444', border: '#b91c1c' },
162
+ interceptor: { background: '#8b5cf6', border: '#6d28d9' },
163
+ tool_call: { background: '#f97316', border: '#c2410c' },
164
+ memory_write: { background: '#10b981', border: '#047857' },
165
+ policy: { background: '#f59e0b', border: '#b45309' },
166
+ agent_message:{ background: '#6b7280', border: '#374151' },
167
+ };
168
+
169
+ function sessionApp() {
170
+ return {
171
+ sessionId: new URLSearchParams(location.search).get('session_id') || '',
172
+ events: [],
173
+ stats: {},
174
+ loading: true,
175
+ graphLoading: true,
176
+ error: '',
177
+ showKeyModal: false,
178
+ inputKey: '',
179
+ _network: null,
180
+
181
+ get apiKey() {
182
+ return localStorage.getItem('openscript_api_key') || '';
183
+ },
184
+
185
+ saveKey() {
186
+ const key = this.inputKey.trim();
187
+ if (!key) return;
188
+ localStorage.setItem('openscript_api_key', key);
189
+ this.showKeyModal = false;
190
+ this.error = '';
191
+ this.init();
192
+ },
193
+
194
+ async init() {
195
+ if (!this.sessionId) return;
196
+ if (!this.apiKey) { this.showKeyModal = true; return; }
197
+ await Promise.all([this.fetchEvents(), this.fetchStats()]);
198
+ await this.fetchGraph();
199
+ this.startPolling();
200
+ },
201
+
202
+ async fetchEvents() {
203
+ this.loading = true;
204
+ try {
205
+ const res = await fetch(`/events/${encodeURIComponent(this.sessionId)}`, {
206
+ headers: { 'X-API-KEY': this.apiKey },
207
+ });
208
+ if (res.status === 401) { this.showKeyModal = true; return; }
209
+ if (!res.ok) { this.error = `Events endpoint returned ${res.status} — check server logs`; return; }
210
+ const data = await res.json();
211
+ this.events = data.events || [];
212
+ if (this.events.length === 0) {
213
+ this.error = 'No events found for this session. Was DATABASE_URL set when the demo ran?';
214
+ }
215
+ } catch (e) {
216
+ this.error = `Cannot reach server: ${e.message}`;
217
+ } finally {
218
+ this.loading = false;
219
+ }
220
+ },
221
+
222
+ async fetchStats() {
223
+ try {
224
+ const res = await fetch(`/sessions/${encodeURIComponent(this.sessionId)}/stats`, {
225
+ headers: { 'X-API-KEY': this.apiKey },
226
+ });
227
+ if (res.ok) this.stats = await res.json();
228
+ } catch {}
229
+ },
230
+
231
+ async fetchGraph() {
232
+ this.graphLoading = true;
233
+ try {
234
+ const res = await fetch(`/sessions/${encodeURIComponent(this.sessionId)}/graph`, {
235
+ headers: { 'X-API-KEY': this.apiKey },
236
+ });
237
+ if (!res.ok) { this.error = `Graph endpoint returned ${res.status} — check server logs`; return; }
238
+ const data = await res.json();
239
+ if (data.node_count === 0) return;
240
+ this.$nextTick(() => this.renderGraph(data));
241
+ } finally {
242
+ this.graphLoading = false;
243
+ }
244
+ },
245
+
246
+ startPolling() {
247
+ const poll = async () => {
248
+ const maxSeq = this.events.length > 0
249
+ ? Math.max(...this.events.map(e => e.sequence_num))
250
+ : 0;
251
+ try {
252
+ const res = await fetch(
253
+ `/events/${encodeURIComponent(this.sessionId)}?after_sequence=${maxSeq}`,
254
+ { headers: { 'X-API-KEY': this.apiKey } },
255
+ );
256
+ if (res.ok) {
257
+ const data = await res.json();
258
+ const newEvents = data.events || [];
259
+ if (newEvents.length > 0) {
260
+ this.events.push(...newEvents);
261
+ await this.fetchStats();
262
+ }
263
+ }
264
+ } catch {}
265
+ setTimeout(poll, 2000);
266
+ };
267
+ setTimeout(poll, 2000);
268
+ },
269
+
270
+ renderGraph(data) {
271
+ const container = document.getElementById('graph-container');
272
+ if (!container) return;
273
+
274
+ const nodes = data.nodes.map(n => {
275
+ const c = NODE_COLORS[n.node_type] || NODE_COLORS.agent_message;
276
+ return {
277
+ id: n.id,
278
+ label: `#${n.sequence_num}\n${n.node_type}`,
279
+ color: { background: c.background, border: c.border,
280
+ highlight: { background: c.background, border: '#1f2937' } },
281
+ font: { color: '#fff', size: 10 },
282
+ shape: n.node_type === 'threat' ? 'diamond' : 'dot',
283
+ size: n.node_type === 'threat' ? 18 : 12,
284
+ };
285
+ });
286
+
287
+ const edges = data.edges.map((e, i) => ({
288
+ id: i,
289
+ from: e.source,
290
+ to: e.target,
291
+ arrows: { to: { enabled: true, scaleFactor: 0.5 } },
292
+ color: { color: e.type === 'parent' ? '#f97316' : '#9ca3af' },
293
+ dashes: e.type === 'parent',
294
+ width: e.type === 'parent' ? 1.5 : 1,
295
+ }));
296
+
297
+ this._network = new vis.Network(
298
+ container,
299
+ { nodes: new vis.DataSet(nodes), edges: new vis.DataSet(edges) },
300
+ {
301
+ layout: { hierarchical: { direction: 'LR', sortMethod: 'directed', levelSeparation: 120 } },
302
+ physics: false,
303
+ interaction: { hover: true, tooltipDelay: 100 },
304
+ edges: { smooth: { type: 'cubicBezier', forceDirection: 'horizontal' } },
305
+ },
306
+ );
307
+ },
308
+
309
+ eventClass(ev) {
310
+ const t = ev.event_type || '';
311
+ if (t === 'threat_detected') return 'event-threat';
312
+ if (t.startsWith('interceptor')) return 'event-interceptor';
313
+ if (t.startsWith('tool')) return 'event-tool';
314
+ if (t.startsWith('turn')) return 'event-turn';
315
+ return 'event-default';
316
+ },
317
+
318
+ payloadSummary(ev) {
319
+ const p = ev.payload || {};
320
+ if (ev.event_type === 'threat_detected')
321
+ return `score=${p.score} | reason=${p.reason || ''}`;
322
+ if (ev.event_type === 'interceptor_before')
323
+ return `action=${p.action} | input="${(p.input_data || {}).input || JSON.stringify(p.input_data).slice(0, 80)}"`;
324
+ if (ev.event_type === 'interceptor_after')
325
+ return `action=${p.action} | output=${JSON.stringify(p.output_data || {}).slice(0, 80)}`;
326
+ return JSON.stringify(p).slice(0, 100);
327
+ },
328
+
329
+ formatTime(iso) {
330
+ if (!iso) return '';
331
+ return new Date(iso).toLocaleTimeString(undefined, {
332
+ hour: '2-digit', minute: '2-digit', second: '2-digit',
333
+ });
334
+ },
335
+ };
336
+ }
337
+ </script>
338
+ </body>
339
+ </html>