@clawdreyhepburn/carapace 0.3.2 → 0.3.3
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.
- package/CHANGELOG.md +21 -0
- package/README.md +36 -1
- package/dist/cedar-engine-cedarling.d.ts +81 -0
- package/dist/cedar-engine-cedarling.js +651 -0
- package/dist/cedar-engine-cedarling.js.map +1 -0
- package/dist/cedar-engine.d.ts +77 -0
- package/dist/cedar-engine.js +374 -0
- package/dist/cedar-engine.js.map +1 -0
- package/dist/gui/html.d.ts +5 -0
- package/dist/gui/html.js +930 -0
- package/dist/gui/html.js.map +1 -0
- package/dist/gui/server.d.ts +28 -0
- package/dist/gui/server.js +159 -0
- package/dist/gui/server.js.map +1 -0
- package/dist/index.d.ts +46 -0
- package/dist/index.js +584 -0
- package/dist/index.js.map +1 -0
- package/dist/llm-proxy.d.ts +75 -0
- package/dist/llm-proxy.js +565 -0
- package/dist/llm-proxy.js.map +1 -0
- package/dist/mcp-aggregator.d.ts +29 -0
- package/dist/mcp-aggregator.js +144 -0
- package/dist/mcp-aggregator.js.map +1 -0
- package/dist/policy-source.d.ts +26 -0
- package/dist/policy-source.js +28 -0
- package/dist/policy-source.js.map +1 -0
- package/dist/types.d.ts +135 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/docs/carapace_proxy_tool_filter_flow_v3.svg +96 -0
- package/docs/ungated_ai_agent_capabilities.svg +143 -0
- package/package.json +1 -1
- package/src/gui/html.ts +14 -0
- package/src/gui/server.ts +7 -1
- package/src/index.ts +12 -0
- package/src/policy-source.ts +44 -0
- package/vitest.config.ts +8 -0
package/dist/gui/html.js
ADDED
|
@@ -0,0 +1,930 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single-file GUI for Carapace.
|
|
3
|
+
* Returns complete HTML with embedded CSS and JS — no build step needed.
|
|
4
|
+
*/
|
|
5
|
+
export function guiHtml() {
|
|
6
|
+
return `<!DOCTYPE html>
|
|
7
|
+
<html lang="en">
|
|
8
|
+
<head>
|
|
9
|
+
<meta charset="UTF-8">
|
|
10
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
11
|
+
<title>Carapace</title>
|
|
12
|
+
<style>
|
|
13
|
+
:root {
|
|
14
|
+
--bg: #0a0a0a;
|
|
15
|
+
--surface: #1a1a1a;
|
|
16
|
+
--surface-2: #222;
|
|
17
|
+
--border: #2a2a2a;
|
|
18
|
+
--border-focus: #444;
|
|
19
|
+
--text: #f5f0eb;
|
|
20
|
+
--muted: #8a8078;
|
|
21
|
+
--gold: #c4a87c;
|
|
22
|
+
--gold-dim: rgba(196, 168, 124, 0.15);
|
|
23
|
+
--tiffany: #81d8d0;
|
|
24
|
+
--tiffany-dim: rgba(129, 216, 208, 0.1);
|
|
25
|
+
--red: #e06060;
|
|
26
|
+
--red-dim: rgba(224, 96, 96, 0.1);
|
|
27
|
+
--green: #60c080;
|
|
28
|
+
--green-dim: rgba(96, 192, 128, 0.1);
|
|
29
|
+
--purple: #b090d0;
|
|
30
|
+
--purple-dim: rgba(176, 144, 208, 0.1);
|
|
31
|
+
--blue: #60a0e0;
|
|
32
|
+
--blue-dim: rgba(96, 160, 224, 0.1);
|
|
33
|
+
--orange: #e0a060;
|
|
34
|
+
--orange-dim: rgba(224, 160, 96, 0.1);
|
|
35
|
+
|
|
36
|
+
/* Category colors */
|
|
37
|
+
--cat-read: var(--tiffany);
|
|
38
|
+
--cat-read-dim: var(--tiffany-dim);
|
|
39
|
+
--cat-browse: var(--blue);
|
|
40
|
+
--cat-browse-dim: var(--blue-dim);
|
|
41
|
+
--cat-write: var(--orange);
|
|
42
|
+
--cat-write-dim: var(--orange-dim);
|
|
43
|
+
--cat-execute: var(--red);
|
|
44
|
+
--cat-execute-dim: var(--red-dim);
|
|
45
|
+
}
|
|
46
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
47
|
+
body {
|
|
48
|
+
background: var(--bg); color: var(--text);
|
|
49
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
50
|
+
font-size: 14px; line-height: 1.6;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
header {
|
|
54
|
+
border-bottom: 1px solid var(--border);
|
|
55
|
+
padding: 1rem 2rem;
|
|
56
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
57
|
+
}
|
|
58
|
+
header h1 { font-size: 1.2rem; font-weight: 600; color: var(--gold); }
|
|
59
|
+
header .stats { color: var(--muted); font-size: 0.85rem; }
|
|
60
|
+
header .stats .count { color: var(--tiffany); font-weight: 600; }
|
|
61
|
+
|
|
62
|
+
.container { max-width: 1200px; margin: 0 auto; padding: 1.5rem 2rem; }
|
|
63
|
+
|
|
64
|
+
/* Server cards */
|
|
65
|
+
.servers {
|
|
66
|
+
display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
|
67
|
+
gap: 1rem; margin-bottom: 1.5rem;
|
|
68
|
+
}
|
|
69
|
+
.server-card {
|
|
70
|
+
background: var(--surface); border: 1px solid var(--border);
|
|
71
|
+
border-radius: 8px; padding: 1rem;
|
|
72
|
+
}
|
|
73
|
+
.server-card .name {
|
|
74
|
+
font-weight: 600; margin-bottom: 0.25rem;
|
|
75
|
+
display: flex; align-items: center; gap: 0.5rem;
|
|
76
|
+
}
|
|
77
|
+
.dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; }
|
|
78
|
+
.dot.connected { background: var(--green); }
|
|
79
|
+
.dot.disconnected { background: var(--red); }
|
|
80
|
+
.server-card .meta { color: var(--muted); font-size: 0.85rem; }
|
|
81
|
+
|
|
82
|
+
h2 {
|
|
83
|
+
font-size: 1rem; color: var(--gold); margin-bottom: 1rem;
|
|
84
|
+
padding-bottom: 0.5rem; border-bottom: 1px solid var(--border);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/* ── Filter bar ── */
|
|
88
|
+
.filter-bar {
|
|
89
|
+
display: flex; gap: 0.75rem; margin-bottom: 1rem;
|
|
90
|
+
align-items: center; flex-wrap: wrap;
|
|
91
|
+
}
|
|
92
|
+
.search-box {
|
|
93
|
+
background: var(--surface); border: 1px solid var(--border);
|
|
94
|
+
color: var(--text); padding: 0.4rem 0.75rem; border-radius: 6px;
|
|
95
|
+
font-size: 0.85rem; width: 220px;
|
|
96
|
+
}
|
|
97
|
+
.search-box:focus { border-color: var(--gold); outline: none; }
|
|
98
|
+
.search-box::placeholder { color: var(--muted); }
|
|
99
|
+
|
|
100
|
+
.filter-group {
|
|
101
|
+
display: flex; gap: 0; border-radius: 6px; overflow: hidden;
|
|
102
|
+
}
|
|
103
|
+
.filter-btn {
|
|
104
|
+
padding: 0.35rem 0.75rem; border: 1px solid var(--border);
|
|
105
|
+
background: transparent; color: var(--muted); cursor: pointer;
|
|
106
|
+
font-size: 0.8rem; transition: all 0.15s; white-space: nowrap;
|
|
107
|
+
margin-left: -1px;
|
|
108
|
+
}
|
|
109
|
+
.filter-btn:first-child { margin-left: 0; border-radius: 6px 0 0 6px; }
|
|
110
|
+
.filter-btn:last-child { border-radius: 0 6px 6px 0; }
|
|
111
|
+
.filter-btn.active { background: var(--surface); color: var(--text); border-color: var(--border-focus); }
|
|
112
|
+
.filter-btn:hover { color: var(--text); }
|
|
113
|
+
|
|
114
|
+
/* Category filter buttons with color indicators */
|
|
115
|
+
.cat-btn { position: relative; padding-left: 1.5rem; }
|
|
116
|
+
.cat-btn::before {
|
|
117
|
+
content: ''; position: absolute; left: 0.5rem; top: 50%; transform: translateY(-50%);
|
|
118
|
+
width: 8px; height: 8px; border-radius: 2px;
|
|
119
|
+
}
|
|
120
|
+
.cat-btn[data-cat="read"]::before { background: var(--cat-read); }
|
|
121
|
+
.cat-btn[data-cat="browse"]::before { background: var(--cat-browse); }
|
|
122
|
+
.cat-btn[data-cat="write"]::before { background: var(--cat-write); }
|
|
123
|
+
.cat-btn[data-cat="execute"]::before { background: var(--cat-execute); }
|
|
124
|
+
.cat-btn[data-cat="read"].active { border-color: var(--cat-read); color: var(--cat-read); }
|
|
125
|
+
.cat-btn[data-cat="browse"].active { border-color: var(--cat-browse); color: var(--cat-browse); }
|
|
126
|
+
.cat-btn[data-cat="write"].active { border-color: var(--cat-write); color: var(--cat-write); }
|
|
127
|
+
.cat-btn[data-cat="execute"].active { border-color: var(--cat-execute); color: var(--cat-execute); }
|
|
128
|
+
|
|
129
|
+
.filter-label { font-size: 0.75rem; color: var(--muted); text-transform: uppercase; letter-spacing: 0.05em; }
|
|
130
|
+
|
|
131
|
+
select.filter-select {
|
|
132
|
+
background: var(--surface); border: 1px solid var(--border);
|
|
133
|
+
color: var(--text); padding: 0.35rem 0.5rem; border-radius: 6px;
|
|
134
|
+
font-size: 0.8rem; cursor: pointer;
|
|
135
|
+
}
|
|
136
|
+
select.filter-select:focus { border-color: var(--gold); outline: none; }
|
|
137
|
+
select.filter-select option { background: var(--surface); }
|
|
138
|
+
|
|
139
|
+
/* Tools table */
|
|
140
|
+
.tools-table { width: 100%; border-collapse: collapse; }
|
|
141
|
+
.tools-table th {
|
|
142
|
+
text-align: left; padding: 0.5rem 0.75rem; color: var(--muted);
|
|
143
|
+
font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.05em;
|
|
144
|
+
border-bottom: 1px solid var(--border); cursor: pointer; user-select: none;
|
|
145
|
+
white-space: nowrap;
|
|
146
|
+
}
|
|
147
|
+
.tools-table th:hover { color: var(--gold); }
|
|
148
|
+
.tools-table th .sort-arrow { font-size: 0.65rem; margin-left: 0.25rem; }
|
|
149
|
+
.tools-table td {
|
|
150
|
+
padding: 0.5rem 0.75rem; border-bottom: 1px solid var(--border); vertical-align: middle;
|
|
151
|
+
}
|
|
152
|
+
.tools-table tr:hover { background: rgba(255,255,255,0.02); }
|
|
153
|
+
.tools-table tr.cat-write { border-left: 3px solid var(--cat-write); }
|
|
154
|
+
.tools-table tr.cat-execute { border-left: 3px solid var(--cat-execute); }
|
|
155
|
+
.tools-table tr.cat-read { border-left: 3px solid var(--cat-read); }
|
|
156
|
+
.tools-table tr.cat-browse { border-left: 3px solid var(--cat-browse); }
|
|
157
|
+
|
|
158
|
+
.tool-name { font-weight: 500; font-family: monospace; font-size: 0.9rem; }
|
|
159
|
+
.tool-server { color: var(--muted); font-size: 0.85rem; }
|
|
160
|
+
.tool-desc { color: var(--muted); font-size: 0.82rem; max-width: 350px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
161
|
+
.tool-desc:hover { white-space: normal; overflow: visible; }
|
|
162
|
+
|
|
163
|
+
/* Category badge */
|
|
164
|
+
.cat-badge {
|
|
165
|
+
display: inline-flex; align-items: center; gap: 0.3rem;
|
|
166
|
+
font-size: 0.72rem; font-weight: 600; text-transform: uppercase;
|
|
167
|
+
letter-spacing: 0.04em; padding: 0.15rem 0.5rem; border-radius: 4px;
|
|
168
|
+
white-space: nowrap;
|
|
169
|
+
}
|
|
170
|
+
.cat-badge.read { background: var(--cat-read-dim); color: var(--cat-read); }
|
|
171
|
+
.cat-badge.browse { background: var(--cat-browse-dim); color: var(--cat-browse); }
|
|
172
|
+
.cat-badge.write { background: var(--cat-write-dim); color: var(--cat-write); }
|
|
173
|
+
.cat-badge.execute { background: var(--cat-execute-dim); color: var(--cat-execute); }
|
|
174
|
+
|
|
175
|
+
/* Toggle switch */
|
|
176
|
+
.toggle { position: relative; width: 36px; height: 20px; cursor: pointer; }
|
|
177
|
+
.toggle input { display: none; }
|
|
178
|
+
.toggle .slider {
|
|
179
|
+
position: absolute; inset: 0; background: var(--border);
|
|
180
|
+
border-radius: 10px; transition: background 0.2s;
|
|
181
|
+
}
|
|
182
|
+
.toggle .slider::before {
|
|
183
|
+
content: ''; position: absolute; width: 14px; height: 14px;
|
|
184
|
+
left: 3px; top: 3px; background: var(--muted);
|
|
185
|
+
border-radius: 50%; transition: all 0.2s;
|
|
186
|
+
}
|
|
187
|
+
.toggle input:checked + .slider { background: var(--green); }
|
|
188
|
+
.toggle input:checked + .slider::before { transform: translateX(16px); background: white; }
|
|
189
|
+
|
|
190
|
+
/* Empty state */
|
|
191
|
+
.empty-state { text-align: center; padding: 3rem; color: var(--muted); }
|
|
192
|
+
|
|
193
|
+
/* Results count */
|
|
194
|
+
.results-count { font-size: 0.8rem; color: var(--muted); margin-left: auto; white-space: nowrap; }
|
|
195
|
+
|
|
196
|
+
/* ── Tab & action bar ── */
|
|
197
|
+
.actions {
|
|
198
|
+
display: flex; gap: 0.75rem; margin-bottom: 1rem; align-items: center; flex-wrap: wrap;
|
|
199
|
+
}
|
|
200
|
+
button {
|
|
201
|
+
background: var(--surface); border: 1px solid var(--border);
|
|
202
|
+
color: var(--text); padding: 0.5rem 1rem; border-radius: 6px;
|
|
203
|
+
cursor: pointer; font-size: 0.85rem; transition: all 0.2s;
|
|
204
|
+
}
|
|
205
|
+
button:hover { border-color: var(--gold); color: var(--gold); }
|
|
206
|
+
button.primary { border-color: var(--tiffany); color: var(--tiffany); }
|
|
207
|
+
button.primary:hover { background: var(--tiffany-dim); }
|
|
208
|
+
button.danger { border-color: var(--red); color: var(--red); }
|
|
209
|
+
button.danger:hover { background: var(--red-dim); }
|
|
210
|
+
.verify-status { color: var(--muted); font-size: 0.85rem; line-height: 36px; }
|
|
211
|
+
.verify-status.ok { color: var(--green); }
|
|
212
|
+
.verify-status.fail { color: var(--red); }
|
|
213
|
+
|
|
214
|
+
.tabs { display: flex; gap: 0; }
|
|
215
|
+
.tab {
|
|
216
|
+
padding: 0.5rem 1.25rem; border: 1px solid var(--border);
|
|
217
|
+
background: transparent; color: var(--muted); cursor: pointer; font-size: 0.85rem;
|
|
218
|
+
}
|
|
219
|
+
.tab:first-child { border-radius: 6px 0 0 6px; }
|
|
220
|
+
.tab:last-child { border-radius: 0 6px 6px 0; }
|
|
221
|
+
.tab.active { background: var(--surface); color: var(--gold); border-color: var(--gold); }
|
|
222
|
+
|
|
223
|
+
/* ── Policies Tab ── */
|
|
224
|
+
.policy-card {
|
|
225
|
+
background: var(--surface); border: 1px solid var(--border);
|
|
226
|
+
border-radius: 8px; margin-bottom: 0.75rem; overflow: hidden;
|
|
227
|
+
}
|
|
228
|
+
.policy-card:hover { border-color: var(--border-focus); }
|
|
229
|
+
.policy-card.permit { border-left: 3px solid var(--green); }
|
|
230
|
+
.policy-card.forbid { border-left: 3px solid var(--red); }
|
|
231
|
+
.policy-header {
|
|
232
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
233
|
+
padding: 0.75rem 1rem; cursor: pointer; user-select: none;
|
|
234
|
+
}
|
|
235
|
+
.policy-header:hover { background: rgba(255,255,255,0.02); }
|
|
236
|
+
.policy-header .left { display: flex; align-items: center; gap: 0.75rem; }
|
|
237
|
+
.effect-badge {
|
|
238
|
+
font-size: 0.75rem; font-weight: 600; text-transform: uppercase;
|
|
239
|
+
padding: 0.15rem 0.5rem; border-radius: 4px; letter-spacing: 0.04em;
|
|
240
|
+
}
|
|
241
|
+
.effect-badge.permit { background: var(--green-dim); color: var(--green); }
|
|
242
|
+
.effect-badge.forbid { background: var(--red-dim); color: var(--red); }
|
|
243
|
+
.policy-header .policy-id { font-family: monospace; font-size: 0.85rem; color: var(--muted); }
|
|
244
|
+
.policy-header .chevron { color: var(--muted); transition: transform 0.2s; font-size: 0.8rem; }
|
|
245
|
+
.policy-card.expanded .chevron { transform: rotate(90deg); }
|
|
246
|
+
.policy-header .right { display: flex; gap: 0.5rem; align-items: center; }
|
|
247
|
+
.policy-body { display: none; padding: 0 1rem 1rem; border-top: 1px solid var(--border); }
|
|
248
|
+
.policy-card.expanded .policy-body { display: block; padding-top: 0.75rem; }
|
|
249
|
+
.policy-editor {
|
|
250
|
+
width: 100%; min-height: 120px; background: var(--bg);
|
|
251
|
+
border: 1px solid var(--border); border-radius: 6px;
|
|
252
|
+
color: var(--text); font-family: 'JetBrains Mono', 'Fira Code', monospace;
|
|
253
|
+
font-size: 0.85rem; padding: 0.75rem; line-height: 1.6; resize: vertical;
|
|
254
|
+
}
|
|
255
|
+
.policy-editor:focus { border-color: var(--gold); outline: none; }
|
|
256
|
+
.policy-actions { display: flex; gap: 0.5rem; margin-top: 0.5rem; justify-content: flex-end; }
|
|
257
|
+
|
|
258
|
+
/* ── Policy Builder ── */
|
|
259
|
+
.builder-overlay {
|
|
260
|
+
display: none; position: fixed; inset: 0;
|
|
261
|
+
background: rgba(0,0,0,0.6); z-index: 100;
|
|
262
|
+
justify-content: center; align-items: center;
|
|
263
|
+
}
|
|
264
|
+
.builder-overlay.open { display: flex; }
|
|
265
|
+
.builder {
|
|
266
|
+
background: var(--surface); border: 1px solid var(--border);
|
|
267
|
+
border-radius: 12px; width: 640px; max-height: 90vh;
|
|
268
|
+
overflow-y: auto; padding: 1.5rem;
|
|
269
|
+
}
|
|
270
|
+
.builder h3 {
|
|
271
|
+
font-size: 1.1rem; color: var(--gold); margin-bottom: 1rem;
|
|
272
|
+
padding-bottom: 0.5rem; border-bottom: 1px solid var(--border);
|
|
273
|
+
}
|
|
274
|
+
.builder-row {
|
|
275
|
+
display: grid; grid-template-columns: 120px 1fr;
|
|
276
|
+
gap: 0.75rem; margin-bottom: 0.75rem; align-items: center;
|
|
277
|
+
}
|
|
278
|
+
.builder-row label { font-size: 0.85rem; color: var(--muted); text-align: right; font-weight: 500; }
|
|
279
|
+
.builder select, .builder input[type="text"] {
|
|
280
|
+
background: var(--bg); border: 1px solid var(--border);
|
|
281
|
+
color: var(--text); padding: 0.5rem 0.75rem; border-radius: 6px; font-size: 0.85rem; width: 100%;
|
|
282
|
+
}
|
|
283
|
+
.builder select:focus, .builder input:focus { border-color: var(--gold); outline: none; }
|
|
284
|
+
.builder select option { background: var(--surface); }
|
|
285
|
+
.builder-preview {
|
|
286
|
+
background: var(--bg); border: 1px solid var(--border);
|
|
287
|
+
border-radius: 6px; padding: 0.75rem; margin-top: 1rem;
|
|
288
|
+
font-family: 'JetBrains Mono', 'Fira Code', monospace;
|
|
289
|
+
font-size: 0.85rem; line-height: 1.6; white-space: pre;
|
|
290
|
+
color: var(--tiffany); min-height: 80px;
|
|
291
|
+
}
|
|
292
|
+
.builder-footer {
|
|
293
|
+
display: flex; justify-content: flex-end; gap: 0.75rem;
|
|
294
|
+
margin-top: 1rem; padding-top: 0.75rem; border-top: 1px solid var(--border);
|
|
295
|
+
}
|
|
296
|
+
.condition-row { display: flex; gap: 0.5rem; margin-bottom: 0.5rem; align-items: center; }
|
|
297
|
+
.condition-row select, .condition-row input {
|
|
298
|
+
background: var(--bg); border: 1px solid var(--border);
|
|
299
|
+
color: var(--text); padding: 0.4rem 0.6rem; border-radius: 4px; font-size: 0.8rem;
|
|
300
|
+
}
|
|
301
|
+
.condition-row select { width: 140px; }
|
|
302
|
+
.condition-row input { flex: 1; }
|
|
303
|
+
.condition-row .remove-cond { background: none; border: none; color: var(--red); cursor: pointer; font-size: 1rem; padding: 0 0.25rem; }
|
|
304
|
+
.add-condition {
|
|
305
|
+
font-size: 0.8rem; color: var(--tiffany); background: none;
|
|
306
|
+
border: 1px dashed var(--border); padding: 0.3rem 0.75rem; border-radius: 4px; cursor: pointer;
|
|
307
|
+
}
|
|
308
|
+
.add-condition:hover { border-color: var(--tiffany); }
|
|
309
|
+
|
|
310
|
+
/* Schema tab */
|
|
311
|
+
.schema-editor {
|
|
312
|
+
width: 100%; min-height: 300px; background: var(--bg);
|
|
313
|
+
border: 1px solid var(--border); border-radius: 6px;
|
|
314
|
+
color: var(--text); font-family: 'JetBrains Mono', 'Fira Code', monospace;
|
|
315
|
+
font-size: 0.85rem; padding: 0.75rem; line-height: 1.6; resize: vertical;
|
|
316
|
+
}
|
|
317
|
+
.schema-editor:focus { border-color: var(--gold); outline: none; }
|
|
318
|
+
|
|
319
|
+
/* Toast */
|
|
320
|
+
.toast {
|
|
321
|
+
position: fixed; bottom: 1.5rem; right: 1.5rem;
|
|
322
|
+
padding: 0.75rem 1.25rem; border-radius: 8px;
|
|
323
|
+
font-size: 0.85rem; z-index: 200;
|
|
324
|
+
animation: slideIn 0.3s ease, fadeOut 0.3s ease 2.7s;
|
|
325
|
+
pointer-events: none;
|
|
326
|
+
}
|
|
327
|
+
.toast.success { background: var(--green-dim); color: var(--green); border: 1px solid var(--green); }
|
|
328
|
+
.toast.error { background: var(--red-dim); color: var(--red); border: 1px solid var(--red); }
|
|
329
|
+
@keyframes slideIn { from { transform: translateY(20px); opacity: 0; } }
|
|
330
|
+
@keyframes fadeOut { to { opacity: 0; } }
|
|
331
|
+
</style>
|
|
332
|
+
</head>
|
|
333
|
+
<body>
|
|
334
|
+
<header>
|
|
335
|
+
<h1>🦞 Carapace</h1>
|
|
336
|
+
<div class="stats">
|
|
337
|
+
<span class="count" id="enabled-count">-</span> / <span class="count" id="total-count">-</span> tools enabled
|
|
338
|
+
</div>
|
|
339
|
+
</header>
|
|
340
|
+
|
|
341
|
+
<div class="container">
|
|
342
|
+
<div id="servers-section">
|
|
343
|
+
<h2>MCP Servers</h2>
|
|
344
|
+
<div class="servers" id="servers"></div>
|
|
345
|
+
</div>
|
|
346
|
+
|
|
347
|
+
<div class="actions">
|
|
348
|
+
<div class="tabs">
|
|
349
|
+
<button class="tab active" data-tab="tools" onclick="switchTab('tools')">Tools</button>
|
|
350
|
+
<button class="tab" data-tab="policies" onclick="switchTab('policies')">Policies</button>
|
|
351
|
+
<button class="tab" data-tab="schema" onclick="switchTab('schema')">Schema</button>
|
|
352
|
+
</div>
|
|
353
|
+
<button class="primary" onclick="openBuilder()" id="new-policy-btn" style="display:none;">+ New Policy</button>
|
|
354
|
+
<button class="primary" onclick="verify()" style="font-size:0.8rem;">⚡ Verify</button>
|
|
355
|
+
<span class="verify-status" id="verify-status"></span>
|
|
356
|
+
<button onclick="refresh()" style="margin-left: auto;">↻ Refresh</button>
|
|
357
|
+
</div>
|
|
358
|
+
|
|
359
|
+
<!-- Tools Tab -->
|
|
360
|
+
<div id="tab-tools">
|
|
361
|
+
<!-- Filter bar -->
|
|
362
|
+
<div class="filter-bar">
|
|
363
|
+
<input type="text" class="search-box" id="search-box" placeholder="Search tools..." oninput="renderTools()">
|
|
364
|
+
|
|
365
|
+
<span class="filter-label">Type</span>
|
|
366
|
+
<div class="filter-group" id="cat-filters">
|
|
367
|
+
<button class="filter-btn cat-btn active" data-cat="all" onclick="toggleCatFilter('all')">All</button>
|
|
368
|
+
<button class="filter-btn cat-btn active" data-cat="write" onclick="toggleCatFilter('write')">✏️ Write</button>
|
|
369
|
+
<button class="filter-btn cat-btn active" data-cat="execute" onclick="toggleCatFilter('execute')">⚡ Execute</button>
|
|
370
|
+
<button class="filter-btn cat-btn active" data-cat="browse" onclick="toggleCatFilter('browse')">🔍 Browse</button>
|
|
371
|
+
<button class="filter-btn cat-btn active" data-cat="read" onclick="toggleCatFilter('read')">📖 Read</button>
|
|
372
|
+
</div>
|
|
373
|
+
|
|
374
|
+
<span class="filter-label">Status</span>
|
|
375
|
+
<div class="filter-group">
|
|
376
|
+
<button class="filter-btn active" data-status="all" onclick="setStatusFilter('all')">All</button>
|
|
377
|
+
<button class="filter-btn" data-status="enabled" onclick="setStatusFilter('enabled')">Enabled</button>
|
|
378
|
+
<button class="filter-btn" data-status="disabled" onclick="setStatusFilter('disabled')">Disabled</button>
|
|
379
|
+
</div>
|
|
380
|
+
|
|
381
|
+
<span class="filter-label">Server</span>
|
|
382
|
+
<select class="filter-select" id="server-filter" onchange="renderTools()">
|
|
383
|
+
<option value="all">All servers</option>
|
|
384
|
+
</select>
|
|
385
|
+
|
|
386
|
+
<span class="results-count" id="results-count"></span>
|
|
387
|
+
</div>
|
|
388
|
+
|
|
389
|
+
<table class="tools-table">
|
|
390
|
+
<thead>
|
|
391
|
+
<tr>
|
|
392
|
+
<th onclick="setSort('enabled')" style="width:60px;">On <span class="sort-arrow" id="sort-enabled"></span></th>
|
|
393
|
+
<th onclick="setSort('category')" style="width:90px;">Type <span class="sort-arrow" id="sort-category"></span></th>
|
|
394
|
+
<th onclick="setSort('name')">Tool <span class="sort-arrow" id="sort-name"></span></th>
|
|
395
|
+
<th onclick="setSort('server')" style="width:100px;">Server <span class="sort-arrow" id="sort-server"></span></th>
|
|
396
|
+
<th>Description</th>
|
|
397
|
+
</tr>
|
|
398
|
+
</thead>
|
|
399
|
+
<tbody id="tools-body">
|
|
400
|
+
<tr><td colspan="5" class="empty-state">Loading...</td></tr>
|
|
401
|
+
</tbody>
|
|
402
|
+
</table>
|
|
403
|
+
</div>
|
|
404
|
+
|
|
405
|
+
<!-- Policies Tab -->
|
|
406
|
+
<div id="tab-policies" style="display:none;">
|
|
407
|
+
<div id="policies-list"></div>
|
|
408
|
+
</div>
|
|
409
|
+
|
|
410
|
+
<!-- Schema Tab -->
|
|
411
|
+
<div id="tab-schema" style="display:none;">
|
|
412
|
+
<p style="color:var(--muted);font-size:0.85rem;margin-bottom:0.75rem;">
|
|
413
|
+
Cedar schema defines entity types, actions, and their relationships. Changes here affect what the policy builder offers.
|
|
414
|
+
</p>
|
|
415
|
+
<textarea class="schema-editor" id="schema-editor" spellcheck="false"></textarea>
|
|
416
|
+
<div style="display:flex;gap:0.5rem;margin-top:0.75rem;justify-content:flex-end;">
|
|
417
|
+
<button onclick="saveSchema()">Save Schema</button>
|
|
418
|
+
</div>
|
|
419
|
+
</div>
|
|
420
|
+
</div>
|
|
421
|
+
|
|
422
|
+
<!-- Policy Builder Modal -->
|
|
423
|
+
<div class="builder-overlay" id="builder-overlay" onclick="if(event.target===this)closeBuilder()">
|
|
424
|
+
<div class="builder">
|
|
425
|
+
<h3 id="builder-title">New Policy</h3>
|
|
426
|
+
<div class="builder-row">
|
|
427
|
+
<label>Effect</label>
|
|
428
|
+
<select id="b-effect" onchange="updatePreview()">
|
|
429
|
+
<option value="permit">permit (allow)</option>
|
|
430
|
+
<option value="forbid">forbid (deny)</option>
|
|
431
|
+
</select>
|
|
432
|
+
</div>
|
|
433
|
+
<div class="builder-row">
|
|
434
|
+
<label>Principal</label>
|
|
435
|
+
<select id="b-principal-type" onchange="updatePreview()">
|
|
436
|
+
<option value="any">Any principal</option>
|
|
437
|
+
</select>
|
|
438
|
+
</div>
|
|
439
|
+
<div class="builder-row" id="b-principal-id-row" style="display:none;">
|
|
440
|
+
<label>Principal ID</label>
|
|
441
|
+
<input type="text" id="b-principal-id" placeholder='e.g. "openclaw"' oninput="updatePreview()">
|
|
442
|
+
</div>
|
|
443
|
+
<div class="builder-row">
|
|
444
|
+
<label>Action</label>
|
|
445
|
+
<select id="b-action" onchange="updatePreview()">
|
|
446
|
+
<option value="any">Any action</option>
|
|
447
|
+
</select>
|
|
448
|
+
</div>
|
|
449
|
+
<div class="builder-row">
|
|
450
|
+
<label>Resource</label>
|
|
451
|
+
<select id="b-resource-type" onchange="updateResourceOptions();updatePreview()">
|
|
452
|
+
<option value="any">Any resource</option>
|
|
453
|
+
</select>
|
|
454
|
+
</div>
|
|
455
|
+
<div class="builder-row" id="b-resource-id-row" style="display:none;">
|
|
456
|
+
<label>Resource ID</label>
|
|
457
|
+
<select id="b-resource-id" onchange="updatePreview()">
|
|
458
|
+
<option value="">Select a tool...</option>
|
|
459
|
+
</select>
|
|
460
|
+
</div>
|
|
461
|
+
<div class="builder-row" style="align-items:start;">
|
|
462
|
+
<label style="margin-top:0.5rem;">Conditions</label>
|
|
463
|
+
<div>
|
|
464
|
+
<div id="b-conditions"></div>
|
|
465
|
+
<button class="add-condition" onclick="addCondition()">+ Add condition</button>
|
|
466
|
+
</div>
|
|
467
|
+
</div>
|
|
468
|
+
<div class="builder-row">
|
|
469
|
+
<label>Policy ID</label>
|
|
470
|
+
<input type="text" id="b-policy-id" placeholder="auto-generated" oninput="updatePreview()">
|
|
471
|
+
</div>
|
|
472
|
+
<div style="margin-top:1rem;">
|
|
473
|
+
<label style="font-size:0.8rem;color:var(--muted);text-transform:uppercase;letter-spacing:0.05em;">Preview</label>
|
|
474
|
+
<div class="builder-preview" id="b-preview"></div>
|
|
475
|
+
</div>
|
|
476
|
+
<div class="builder-footer">
|
|
477
|
+
<button onclick="closeBuilder()">Cancel</button>
|
|
478
|
+
<button class="primary" onclick="saveBuilderPolicy()">Save Policy</button>
|
|
479
|
+
</div>
|
|
480
|
+
</div>
|
|
481
|
+
</div>
|
|
482
|
+
|
|
483
|
+
<script>
|
|
484
|
+
// ── State ──
|
|
485
|
+
let state = { servers: {}, tools: [], policies: [], schema: null };
|
|
486
|
+
let schema = { entities: [], actions: [], raw: '' };
|
|
487
|
+
|
|
488
|
+
// Filter/sort state
|
|
489
|
+
let activeCategories = new Set(['write', 'execute', 'browse', 'read']);
|
|
490
|
+
let statusFilter = 'all';
|
|
491
|
+
let sortField = 'category';
|
|
492
|
+
let sortDir = 'asc'; // asc = risky first for category
|
|
493
|
+
|
|
494
|
+
// ── Tool categorization ──
|
|
495
|
+
const CAT_RULES = [
|
|
496
|
+
// Write: modifies, creates, or moves data
|
|
497
|
+
{ cat: 'write', emoji: '✏️', patterns: [/write/, /edit/, /create/, /move/, /gzip/] },
|
|
498
|
+
// Execute: triggers operations, toggles state, long-running
|
|
499
|
+
{ cat: 'execute', emoji: '⚡', patterns: [/toggle/, /trigger/, /simulate/, /long.?running/] },
|
|
500
|
+
// Browse: lists, searches, inspects metadata
|
|
501
|
+
{ cat: 'browse', emoji: '🔍', patterns: [/list/, /search/, /tree/, /info/, /allowed/, /env$/, /annotated/, /reference/, /links/] },
|
|
502
|
+
// Read: retrieves content
|
|
503
|
+
{ cat: 'read', emoji: '📖', patterns: [/read/, /echo/, /get/, /sum/, /image/, /structured/] },
|
|
504
|
+
];
|
|
505
|
+
|
|
506
|
+
const CAT_ORDER = { write: 0, execute: 1, browse: 2, read: 3 };
|
|
507
|
+
const CAT_LABELS = { write: '✏️ Write', execute: '⚡ Execute', browse: '🔍 Browse', read: '📖 Read' };
|
|
508
|
+
|
|
509
|
+
function categorizeTool(name) {
|
|
510
|
+
const lower = name.toLowerCase();
|
|
511
|
+
for (const rule of CAT_RULES) {
|
|
512
|
+
if (rule.patterns.some(p => p.test(lower))) return rule.cat;
|
|
513
|
+
}
|
|
514
|
+
return 'browse'; // default fallback
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// ── Data fetching ──
|
|
518
|
+
async function refresh() {
|
|
519
|
+
try {
|
|
520
|
+
const [statusRes, schemaRes] = await Promise.all([
|
|
521
|
+
fetch('/api/status'), fetch('/api/schema')
|
|
522
|
+
]);
|
|
523
|
+
state = await statusRes.json();
|
|
524
|
+
schema = await schemaRes.json();
|
|
525
|
+
// Enrich tools with category
|
|
526
|
+
state.tools.forEach(t => { t.category = categorizeTool(t.name); });
|
|
527
|
+
render();
|
|
528
|
+
} catch (err) { console.error('Refresh failed:', err); }
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// ── Rendering ──
|
|
532
|
+
function render() {
|
|
533
|
+
document.getElementById('total-count').textContent = state.toolCount ?? state.tools.length;
|
|
534
|
+
document.getElementById('enabled-count').textContent = state.enabledCount ?? state.tools.filter(t => t.enabled).length;
|
|
535
|
+
renderServers();
|
|
536
|
+
renderTools();
|
|
537
|
+
renderPolicies();
|
|
538
|
+
renderSchemaEditor();
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function renderServers() {
|
|
542
|
+
const section = document.getElementById('servers-section');
|
|
543
|
+
const el = document.getElementById('servers');
|
|
544
|
+
const serverFilter = document.getElementById('server-filter');
|
|
545
|
+
const serverNames = Object.keys(state.servers);
|
|
546
|
+
|
|
547
|
+
// Hide entire section when no MCP servers are configured
|
|
548
|
+
if (serverNames.length === 0) {
|
|
549
|
+
section.style.display = 'none';
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
section.style.display = '';
|
|
553
|
+
|
|
554
|
+
el.innerHTML = Object.entries(state.servers).map(([name, s]) =>
|
|
555
|
+
'<div class="server-card"><div class="name">' +
|
|
556
|
+
'<span class="dot ' + (s.connected ? 'connected' : 'disconnected') + '"></span>' +
|
|
557
|
+
esc(name) + '</div><div class="meta">' + s.toolCount + ' tools' +
|
|
558
|
+
(s.error ? ' · ' + esc(s.error) : '') + '</div></div>'
|
|
559
|
+
).join('');
|
|
560
|
+
|
|
561
|
+
// Update server filter dropdown (preserve selection)
|
|
562
|
+
const prev = serverFilter.value;
|
|
563
|
+
serverFilter.innerHTML = '<option value="all">All servers</option>' +
|
|
564
|
+
serverNames.map(n => '<option value="' + esc(n) + '">' + esc(n) + '</option>').join('');
|
|
565
|
+
serverFilter.value = prev || 'all';
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
function renderTools() {
|
|
569
|
+
const search = document.getElementById('search-box').value.toLowerCase();
|
|
570
|
+
const serverVal = document.getElementById('server-filter').value;
|
|
571
|
+
|
|
572
|
+
// Filter
|
|
573
|
+
let tools = state.tools.filter(t => {
|
|
574
|
+
if (search && !t.qualifiedName.toLowerCase().includes(search) && !(t.description||'').toLowerCase().includes(search)) return false;
|
|
575
|
+
if (!activeCategories.has(t.category)) return false;
|
|
576
|
+
if (statusFilter === 'enabled' && !t.enabled) return false;
|
|
577
|
+
if (statusFilter === 'disabled' && t.enabled) return false;
|
|
578
|
+
if (serverVal !== 'all' && t.server !== serverVal) return false;
|
|
579
|
+
return true;
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
// Sort
|
|
583
|
+
tools.sort((a, b) => {
|
|
584
|
+
let cmp = 0;
|
|
585
|
+
if (sortField === 'category') cmp = (CAT_ORDER[a.category]??9) - (CAT_ORDER[b.category]??9);
|
|
586
|
+
else if (sortField === 'name') cmp = a.qualifiedName.localeCompare(b.qualifiedName);
|
|
587
|
+
else if (sortField === 'server') cmp = a.server.localeCompare(b.server);
|
|
588
|
+
else if (sortField === 'enabled') cmp = (b.enabled?1:0) - (a.enabled?1:0);
|
|
589
|
+
return sortDir === 'asc' ? cmp : -cmp;
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
// Update sort arrows
|
|
593
|
+
['enabled','category','name','server'].forEach(f => {
|
|
594
|
+
const el = document.getElementById('sort-' + f);
|
|
595
|
+
if (el) el.textContent = sortField === f ? (sortDir === 'asc' ? '▲' : '▼') : '';
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
// Results count
|
|
599
|
+
document.getElementById('results-count').textContent =
|
|
600
|
+
tools.length === state.tools.length ? '' : tools.length + ' of ' + state.tools.length;
|
|
601
|
+
|
|
602
|
+
const tbody = document.getElementById('tools-body');
|
|
603
|
+
if (tools.length === 0) {
|
|
604
|
+
tbody.innerHTML = '<tr><td colspan="5" class="empty-state">No tools match filters</td></tr>';
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
tbody.innerHTML = tools.map(t => {
|
|
609
|
+
const cat = t.category;
|
|
610
|
+
return '<tr class="cat-' + cat + '">' +
|
|
611
|
+
'<td><label class="toggle"><input type="checkbox" ' +
|
|
612
|
+
(t.enabled ? 'checked' : '') +
|
|
613
|
+
' onchange="toggleTool(\\'' + esc(t.qualifiedName) + '\\',this.checked)">' +
|
|
614
|
+
'<span class="slider"></span></label></td>' +
|
|
615
|
+
'<td><span class="cat-badge ' + cat + '">' + CAT_LABELS[cat] + '</span></td>' +
|
|
616
|
+
'<td class="tool-name" style="color:var(--cat-' + cat + ')">' + esc(t.qualifiedName) + '</td>' +
|
|
617
|
+
'<td class="tool-server">' + esc(t.server) + '</td>' +
|
|
618
|
+
'<td class="tool-desc" title="' + esc(t.description||'') + '">' + esc(t.description || '—') + '</td>' +
|
|
619
|
+
'</tr>';
|
|
620
|
+
}).join('');
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// ── Filter controls ──
|
|
624
|
+
function toggleCatFilter(cat) {
|
|
625
|
+
if (cat === 'all') {
|
|
626
|
+
// If all are active, deactivate all. If any are inactive, activate all.
|
|
627
|
+
const allActive = activeCategories.size === 4;
|
|
628
|
+
activeCategories = allActive ? new Set() : new Set(['write','execute','browse','read']);
|
|
629
|
+
} else {
|
|
630
|
+
if (activeCategories.has(cat)) activeCategories.delete(cat);
|
|
631
|
+
else activeCategories.add(cat);
|
|
632
|
+
}
|
|
633
|
+
// Update button states
|
|
634
|
+
document.querySelectorAll('#cat-filters .cat-btn').forEach(btn => {
|
|
635
|
+
const c = btn.dataset.cat;
|
|
636
|
+
if (c === 'all') {
|
|
637
|
+
btn.classList.toggle('active', activeCategories.size === 4);
|
|
638
|
+
} else {
|
|
639
|
+
btn.classList.toggle('active', activeCategories.has(c));
|
|
640
|
+
}
|
|
641
|
+
});
|
|
642
|
+
renderTools();
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
function setStatusFilter(val) {
|
|
646
|
+
statusFilter = val;
|
|
647
|
+
document.querySelectorAll('[data-status]').forEach(btn => {
|
|
648
|
+
btn.classList.toggle('active', btn.dataset.status === val);
|
|
649
|
+
});
|
|
650
|
+
renderTools();
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
function setSort(field) {
|
|
654
|
+
if (sortField === field) sortDir = sortDir === 'asc' ? 'desc' : 'asc';
|
|
655
|
+
else { sortField = field; sortDir = 'asc'; }
|
|
656
|
+
renderTools();
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// ── Tool toggling ──
|
|
660
|
+
async function toggleTool(tool, enabled) {
|
|
661
|
+
try {
|
|
662
|
+
await fetch('/api/toggle', {
|
|
663
|
+
method: 'POST',
|
|
664
|
+
headers: { 'Content-Type': 'application/json' },
|
|
665
|
+
body: JSON.stringify({ tool, enabled })
|
|
666
|
+
});
|
|
667
|
+
await refresh();
|
|
668
|
+
toast(tool + (enabled ? ' enabled' : ' disabled'), 'success');
|
|
669
|
+
} catch (err) { toast('Toggle failed: ' + err, 'error'); }
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// ── Policies ──
|
|
673
|
+
function renderPolicies() {
|
|
674
|
+
const el = document.getElementById('policies-list');
|
|
675
|
+
const policies = state.policies ?? [];
|
|
676
|
+
if (policies.length === 0) {
|
|
677
|
+
const mode = state.defaultPolicy === 'deny-all' ? 'Default deny is active — all tools are blocked.' : 'Default allow is active — all tools are permitted.';
|
|
678
|
+
el.innerHTML = '<div class="empty-state">No policies loaded. ' + mode + '<br><br>' +
|
|
679
|
+
'<button class="primary" onclick="openBuilder()">+ Create your first policy</button></div>';
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
el.innerHTML = policies.map(p =>
|
|
683
|
+
'<div class="policy-card ' + esc(p.effect) + '" id="pc-' + esc(p.id) + '">' +
|
|
684
|
+
'<div class="policy-header" onclick="togglePolicy(\\'' + esc(p.id) + '\\')">' +
|
|
685
|
+
'<div class="left">' +
|
|
686
|
+
'<span class="chevron">▶</span>' +
|
|
687
|
+
'<span class="effect-badge ' + esc(p.effect) + '">' + esc(p.effect) + '</span>' +
|
|
688
|
+
'<span class="policy-id">' + esc(p.id) + '</span>' +
|
|
689
|
+
'</div>' +
|
|
690
|
+
'<div class="right">' +
|
|
691
|
+
'<button onclick="event.stopPropagation();editPolicy(\\'' + esc(p.id) + '\\')" style="padding:0.25rem 0.5rem;font-size:0.8rem;">✏️ Edit</button>' +
|
|
692
|
+
'<button class="danger" onclick="event.stopPropagation();deletePolicy(\\'' + esc(p.id) + '\\')" style="padding:0.25rem 0.5rem;font-size:0.8rem;">🗑</button>' +
|
|
693
|
+
'</div>' +
|
|
694
|
+
'</div>' +
|
|
695
|
+
'<div class="policy-body">' +
|
|
696
|
+
'<textarea class="policy-editor" id="pe-' + esc(p.id) + '" spellcheck="false">' + esc(p.raw) + '</textarea>' +
|
|
697
|
+
'<div class="policy-actions">' +
|
|
698
|
+
'<button class="danger" onclick="deletePolicy(\\'' + esc(p.id) + '\\')">Delete</button>' +
|
|
699
|
+
'<button class="primary" onclick="saveInlinePolicy(\\'' + esc(p.id) + '\\')">Save Changes</button>' +
|
|
700
|
+
'</div>' +
|
|
701
|
+
'</div>' +
|
|
702
|
+
'</div>'
|
|
703
|
+
).join('');
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
function togglePolicy(id) {
|
|
707
|
+
const el = document.getElementById('pc-' + id);
|
|
708
|
+
if (el) el.classList.toggle('expanded');
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
async function saveInlinePolicy(id) {
|
|
712
|
+
const el = document.getElementById('pe-' + id);
|
|
713
|
+
if (!el) return;
|
|
714
|
+
try {
|
|
715
|
+
await fetch('/api/policy', {
|
|
716
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
717
|
+
body: JSON.stringify({ id, raw: el.value })
|
|
718
|
+
});
|
|
719
|
+
await refresh(); toast('Policy saved: ' + id, 'success');
|
|
720
|
+
} catch (err) { toast('Save failed: ' + err, 'error'); }
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
async function deletePolicy(id) {
|
|
724
|
+
if (!confirm('Delete policy "' + id + '"?')) return;
|
|
725
|
+
try {
|
|
726
|
+
await fetch('/api/policy', {
|
|
727
|
+
method: 'DELETE', headers: { 'Content-Type': 'application/json' },
|
|
728
|
+
body: JSON.stringify({ id })
|
|
729
|
+
});
|
|
730
|
+
await refresh(); toast('Policy deleted: ' + id, 'success');
|
|
731
|
+
} catch (err) { toast('Delete failed: ' + err, 'error'); }
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
function editPolicy(id) {
|
|
735
|
+
const el = document.getElementById('pc-' + id);
|
|
736
|
+
if (el && !el.classList.contains('expanded')) el.classList.add('expanded');
|
|
737
|
+
const editor = document.getElementById('pe-' + id);
|
|
738
|
+
if (editor) editor.focus();
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
// ── Schema ──
|
|
742
|
+
function renderSchemaEditor() {
|
|
743
|
+
const el = document.getElementById('schema-editor');
|
|
744
|
+
if (el && !el.matches(':focus')) el.value = schema.raw || '';
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
async function saveSchema() {
|
|
748
|
+
const raw = document.getElementById('schema-editor').value;
|
|
749
|
+
try {
|
|
750
|
+
await fetch('/api/schema', {
|
|
751
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
752
|
+
body: JSON.stringify({ raw })
|
|
753
|
+
});
|
|
754
|
+
await refresh(); toast('Schema saved', 'success');
|
|
755
|
+
} catch (err) { toast('Schema save failed: ' + err, 'error'); }
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// ── Verify ──
|
|
759
|
+
async function verify() {
|
|
760
|
+
const el = document.getElementById('verify-status');
|
|
761
|
+
el.textContent = 'Verifying...'; el.className = 'verify-status';
|
|
762
|
+
try {
|
|
763
|
+
const res = await fetch('/api/verify', { method: 'POST' });
|
|
764
|
+
const result = await res.json();
|
|
765
|
+
if (result.ok) { el.textContent = '✅ Verified (' + result.durationMs + 'ms)'; el.className = 'verify-status ok'; }
|
|
766
|
+
else { el.textContent = '⚠️ ' + result.issues.join('; '); el.className = 'verify-status fail'; }
|
|
767
|
+
} catch (err) { el.textContent = '❌ Error'; el.className = 'verify-status fail'; }
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
// ── Tab switching ──
|
|
771
|
+
function switchTab(tab) {
|
|
772
|
+
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
|
773
|
+
document.querySelector('[data-tab="' + tab + '"]').classList.add('active');
|
|
774
|
+
['tools', 'policies', 'schema'].forEach(t => {
|
|
775
|
+
document.getElementById('tab-' + t).style.display = t === tab ? '' : 'none';
|
|
776
|
+
});
|
|
777
|
+
document.getElementById('new-policy-btn').style.display = (tab === 'policies') ? '' : 'none';
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
// ── Policy Builder ──
|
|
781
|
+
function openBuilder() {
|
|
782
|
+
populateBuilderDropdowns();
|
|
783
|
+
document.getElementById('b-effect').value = 'permit';
|
|
784
|
+
document.getElementById('b-principal-type').value = 'any';
|
|
785
|
+
document.getElementById('b-principal-id').value = '';
|
|
786
|
+
document.getElementById('b-principal-id-row').style.display = 'none';
|
|
787
|
+
document.getElementById('b-action').value = 'any';
|
|
788
|
+
document.getElementById('b-resource-type').value = 'any';
|
|
789
|
+
document.getElementById('b-resource-id-row').style.display = 'none';
|
|
790
|
+
document.getElementById('b-conditions').innerHTML = '';
|
|
791
|
+
document.getElementById('b-policy-id').value = '';
|
|
792
|
+
document.getElementById('b-policy-id').dataset.auto = '1';
|
|
793
|
+
document.getElementById('builder-title').textContent = 'New Policy';
|
|
794
|
+
updatePreview();
|
|
795
|
+
document.getElementById('builder-overlay').classList.add('open');
|
|
796
|
+
}
|
|
797
|
+
function closeBuilder() { document.getElementById('builder-overlay').classList.remove('open'); }
|
|
798
|
+
|
|
799
|
+
function populateBuilderDropdowns() {
|
|
800
|
+
const princSelect = document.getElementById('b-principal-type');
|
|
801
|
+
princSelect.innerHTML = '<option value="any">Any principal</option>';
|
|
802
|
+
for (const e of schema.entities ?? []) {
|
|
803
|
+
princSelect.innerHTML += '<option value="specific:' + e.name + '">' + e.name + ' (specific)</option>';
|
|
804
|
+
princSelect.innerHTML += '<option value="type:' + e.name + '">All ' + e.name + '</option>';
|
|
805
|
+
}
|
|
806
|
+
const actSelect = document.getElementById('b-action');
|
|
807
|
+
actSelect.innerHTML = '<option value="any">Any action</option>';
|
|
808
|
+
for (const a of schema.actions ?? []) {
|
|
809
|
+
actSelect.innerHTML += '<option value="' + a.name + '">' + a.name + '</option>';
|
|
810
|
+
}
|
|
811
|
+
const resSelect = document.getElementById('b-resource-type');
|
|
812
|
+
resSelect.innerHTML = '<option value="any">Any resource</option>';
|
|
813
|
+
for (const e of schema.entities ?? []) {
|
|
814
|
+
resSelect.innerHTML += '<option value="specific:' + e.name + '">' + e.name + ' (specific)</option>';
|
|
815
|
+
resSelect.innerHTML += '<option value="type:' + e.name + '">All ' + e.name + '</option>';
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
function updateResourceOptions() {
|
|
820
|
+
const val = document.getElementById('b-resource-type').value;
|
|
821
|
+
const idRow = document.getElementById('b-resource-id-row');
|
|
822
|
+
if (val.startsWith('specific:')) {
|
|
823
|
+
idRow.style.display = '';
|
|
824
|
+
const resIdSelect = document.getElementById('b-resource-id');
|
|
825
|
+
resIdSelect.innerHTML = '<option value="">Select...</option>';
|
|
826
|
+
if (val === 'specific:Tool') {
|
|
827
|
+
for (const t of state.tools ?? []) {
|
|
828
|
+
resIdSelect.innerHTML += '<option value="' + esc(t.qualifiedName) + '">' + esc(t.qualifiedName) + '</option>';
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
resIdSelect.innerHTML += '<option value="__custom__">Custom ID...</option>';
|
|
832
|
+
} else { idRow.style.display = 'none'; }
|
|
833
|
+
const princVal = document.getElementById('b-principal-type').value;
|
|
834
|
+
document.getElementById('b-principal-id-row').style.display = princVal.startsWith('specific:') ? '' : 'none';
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
function addCondition() {
|
|
838
|
+
const container = document.getElementById('b-conditions');
|
|
839
|
+
const row = document.createElement('div');
|
|
840
|
+
row.className = 'condition-row';
|
|
841
|
+
let attrOpts = '<option value="">attribute...</option>';
|
|
842
|
+
for (const e of schema.entities ?? []) {
|
|
843
|
+
for (const a of e.attributes ?? []) {
|
|
844
|
+
attrOpts += '<option value="resource.' + a.name + '">' + a.name + ' (' + a.type + ')</option>';
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
attrOpts += '<option value="context.arguments">context.arguments</option>';
|
|
848
|
+
row.innerHTML =
|
|
849
|
+
'<select onchange="updatePreview()">' + attrOpts + '</select>' +
|
|
850
|
+
'<select onchange="updatePreview()" style="width:80px;"><option value="==">=</option><option value="!=">≠</option><option value="has">has</option><option value="in">in</option></select>' +
|
|
851
|
+
'<input type="text" placeholder="value" oninput="updatePreview()">' +
|
|
852
|
+
'<span class="remove-cond" onclick="this.parentElement.remove();updatePreview()">×</span>';
|
|
853
|
+
container.appendChild(row);
|
|
854
|
+
updatePreview();
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
function updatePreview() {
|
|
858
|
+
const effect = document.getElementById('b-effect').value;
|
|
859
|
+
const princType = document.getElementById('b-principal-type').value;
|
|
860
|
+
const princId = document.getElementById('b-principal-id').value.trim();
|
|
861
|
+
const action = document.getElementById('b-action').value;
|
|
862
|
+
const resType = document.getElementById('b-resource-type').value;
|
|
863
|
+
const resId = document.getElementById('b-resource-id')?.value ?? '';
|
|
864
|
+
document.getElementById('b-principal-id-row').style.display = princType.startsWith('specific:') ? '' : 'none';
|
|
865
|
+
updateResourceOptions();
|
|
866
|
+
let lines = [effect + '('];
|
|
867
|
+
if (princType === 'any') lines.push(' principal,');
|
|
868
|
+
else if (princType.startsWith('specific:')) {
|
|
869
|
+
const type = princType.split(':')[1];
|
|
870
|
+
lines.push(princId ? ' principal == ' + type + '::"' + princId + '",' : ' principal is ' + type + ',');
|
|
871
|
+
} else if (princType.startsWith('type:')) lines.push(' principal is ' + princType.split(':')[1] + ',');
|
|
872
|
+
if (action === 'any') lines.push(' action,');
|
|
873
|
+
else lines.push(' action == Action::"' + action + '",');
|
|
874
|
+
if (resType === 'any') lines.push(' resource');
|
|
875
|
+
else if (resType.startsWith('specific:')) {
|
|
876
|
+
const type = resType.split(':')[1];
|
|
877
|
+
lines.push(resId && resId !== '__custom__' ? ' resource == ' + type + '::"' + resId + '"' : ' resource is ' + type);
|
|
878
|
+
} else if (resType.startsWith('type:')) lines.push(' resource is ' + resType.split(':')[1]);
|
|
879
|
+
const condRows = document.querySelectorAll('#b-conditions .condition-row');
|
|
880
|
+
const conds = [];
|
|
881
|
+
condRows.forEach(row => {
|
|
882
|
+
const selects = row.querySelectorAll('select');
|
|
883
|
+
const input = row.querySelector('input');
|
|
884
|
+
const attr = selects[0]?.value; const op = selects[1]?.value; const val = input?.value?.trim();
|
|
885
|
+
if (attr && val) {
|
|
886
|
+
if (op === 'has') conds.push(attr.split('.')[0] + ' has ' + val);
|
|
887
|
+
else { const isLit = val==='true'||val==='false'||!isNaN(Number(val)); conds.push(attr+' '+op+' '+(isLit?val:'"'+val+'"')); }
|
|
888
|
+
}
|
|
889
|
+
});
|
|
890
|
+
if (conds.length > 0) { lines.push(') when {'); lines.push(' ' + conds.join(' &&\\n ')); lines.push('};'); }
|
|
891
|
+
else lines.push(');');
|
|
892
|
+
document.getElementById('b-preview').textContent = lines.join('\\n');
|
|
893
|
+
const idInput = document.getElementById('b-policy-id');
|
|
894
|
+
if (!idInput.value || idInput.dataset.auto === '1') {
|
|
895
|
+
let autoId = effect;
|
|
896
|
+
if (action !== 'any') autoId += '-' + action.replace(/_/g, '-');
|
|
897
|
+
if (resId && resId !== '__custom__') autoId += '-' + resId.replace(/\\//g, '-');
|
|
898
|
+
else if (resType !== 'any') autoId += '-' + resType.split(':')[1].toLowerCase();
|
|
899
|
+
idInput.value = autoId; idInput.dataset.auto = '1';
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
async function saveBuilderPolicy() {
|
|
904
|
+
const id = document.getElementById('b-policy-id').value.trim();
|
|
905
|
+
const raw = document.getElementById('b-preview').textContent;
|
|
906
|
+
if (!id) { toast('Policy ID is required', 'error'); return; }
|
|
907
|
+
try {
|
|
908
|
+
await fetch('/api/policy', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({id,raw}) });
|
|
909
|
+
closeBuilder(); await refresh(); toast('Policy created: ' + id, 'success'); switchTab('policies');
|
|
910
|
+
} catch (err) { toast('Save failed: ' + err, 'error'); }
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
// ── Utilities ──
|
|
914
|
+
function esc(s) { const d = document.createElement('div'); d.textContent = s ?? ''; return d.innerHTML; }
|
|
915
|
+
function toast(msg, type) {
|
|
916
|
+
const el = document.createElement('div'); el.className = 'toast ' + type;
|
|
917
|
+
el.textContent = msg; document.body.appendChild(el); setTimeout(() => el.remove(), 3000);
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
// ── Init ──
|
|
921
|
+
refresh();
|
|
922
|
+
setInterval(refresh, 5000);
|
|
923
|
+
document.getElementById('b-policy-id')?.addEventListener('input', function() {
|
|
924
|
+
if (this === document.activeElement) this.dataset.auto = '0';
|
|
925
|
+
});
|
|
926
|
+
</script>
|
|
927
|
+
</body>
|
|
928
|
+
</html>`;
|
|
929
|
+
}
|
|
930
|
+
//# sourceMappingURL=html.js.map
|