labgate 0.4.3 → 0.5.0
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/dist/cli.js +11 -0
- package/dist/cli.js.map +1 -1
- package/dist/lib/container.js +9 -4
- package/dist/lib/container.js.map +1 -1
- package/dist/lib/ui.d.ts +7 -0
- package/dist/lib/ui.html +861 -0
- package/dist/lib/ui.js +249 -0
- package/dist/lib/ui.js.map +1 -0
- package/package.json +2 -2
package/dist/lib/ui.html
ADDED
|
@@ -0,0 +1,861 @@
|
|
|
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>LabGate Settings</title>
|
|
7
|
+
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><rect width='100' height='100' rx='20' fill='%23171717'/><text x='50' y='68' font-size='52' font-family='system-ui' font-weight='700' fill='white' text-anchor='middle'>L</text></svg>">
|
|
8
|
+
<style>
|
|
9
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
10
|
+
|
|
11
|
+
:root {
|
|
12
|
+
--bg-primary: #F7F7F4;
|
|
13
|
+
--bg-secondary: #EFEFE9;
|
|
14
|
+
--text-primary: #171717;
|
|
15
|
+
--text-secondary: #525252;
|
|
16
|
+
--text-muted: #a3a3a3;
|
|
17
|
+
--border-color: #e5e5e0;
|
|
18
|
+
--card-bg: #ffffff;
|
|
19
|
+
--radius: 16px;
|
|
20
|
+
--radius-sm: 12px;
|
|
21
|
+
--green: #22c55e;
|
|
22
|
+
--red: #cf222e;
|
|
23
|
+
--blue: #0969da;
|
|
24
|
+
--yellow: #eab308;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
body {
|
|
28
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
29
|
+
color: var(--text-primary);
|
|
30
|
+
background: var(--bg-primary);
|
|
31
|
+
line-height: 1.6;
|
|
32
|
+
-webkit-font-smoothing: antialiased;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
code { font-family: 'SF Mono', Monaco, Consolas, monospace; }
|
|
36
|
+
|
|
37
|
+
/* ── Layout ─────────────────────────────────── */
|
|
38
|
+
.header {
|
|
39
|
+
display: flex;
|
|
40
|
+
align-items: center;
|
|
41
|
+
justify-content: space-between;
|
|
42
|
+
max-width: 960px;
|
|
43
|
+
margin: 0 auto;
|
|
44
|
+
padding: 16px 24px;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.logo {
|
|
48
|
+
font-size: 1.125rem;
|
|
49
|
+
font-weight: 600;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.config-path {
|
|
53
|
+
font-size: 0.8125rem;
|
|
54
|
+
color: var(--text-muted);
|
|
55
|
+
font-family: 'SF Mono', Monaco, Consolas, monospace;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.container {
|
|
59
|
+
max-width: 960px;
|
|
60
|
+
margin: 0 auto;
|
|
61
|
+
padding: 0 24px 80px;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/* ── Tabs ───────────────────────────────────── */
|
|
65
|
+
.tabs {
|
|
66
|
+
display: flex;
|
|
67
|
+
gap: 4px;
|
|
68
|
+
border-bottom: 1px solid var(--border-color);
|
|
69
|
+
margin-bottom: 32px;
|
|
70
|
+
overflow-x: auto;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.tab {
|
|
74
|
+
padding: 10px 16px;
|
|
75
|
+
font-size: 0.875rem;
|
|
76
|
+
font-weight: 500;
|
|
77
|
+
color: var(--text-secondary);
|
|
78
|
+
background: none;
|
|
79
|
+
border: none;
|
|
80
|
+
border-bottom: 2px solid transparent;
|
|
81
|
+
cursor: pointer;
|
|
82
|
+
white-space: nowrap;
|
|
83
|
+
transition: all 0.15s;
|
|
84
|
+
font-family: inherit;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.tab:hover { color: var(--text-primary); }
|
|
88
|
+
.tab.active {
|
|
89
|
+
color: var(--text-primary);
|
|
90
|
+
border-bottom-color: var(--text-primary);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.tab-panel { display: none; }
|
|
94
|
+
.tab-panel.active { display: block; }
|
|
95
|
+
|
|
96
|
+
/* ── Cards ──────────────────────────────────── */
|
|
97
|
+
.card {
|
|
98
|
+
background: var(--card-bg);
|
|
99
|
+
border: 1px solid var(--border-color);
|
|
100
|
+
border-radius: var(--radius);
|
|
101
|
+
padding: 24px;
|
|
102
|
+
margin-bottom: 20px;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.card h3 {
|
|
106
|
+
font-size: 1rem;
|
|
107
|
+
font-weight: 600;
|
|
108
|
+
margin-bottom: 16px;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.card-description {
|
|
112
|
+
font-size: 0.8125rem;
|
|
113
|
+
color: var(--text-muted);
|
|
114
|
+
margin-top: -12px;
|
|
115
|
+
margin-bottom: 16px;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/* ── Form elements ─────────────────────────── */
|
|
119
|
+
.field {
|
|
120
|
+
margin-bottom: 16px;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.field:last-child { margin-bottom: 0; }
|
|
124
|
+
|
|
125
|
+
.field label {
|
|
126
|
+
display: block;
|
|
127
|
+
font-size: 0.8125rem;
|
|
128
|
+
font-weight: 500;
|
|
129
|
+
color: var(--text-secondary);
|
|
130
|
+
margin-bottom: 6px;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.field input[type="text"],
|
|
134
|
+
.field input[type="number"],
|
|
135
|
+
.field select {
|
|
136
|
+
width: 100%;
|
|
137
|
+
padding: 8px 12px;
|
|
138
|
+
font-size: 0.875rem;
|
|
139
|
+
font-family: inherit;
|
|
140
|
+
border: 1px solid var(--border-color);
|
|
141
|
+
border-radius: 8px;
|
|
142
|
+
background: var(--bg-primary);
|
|
143
|
+
color: var(--text-primary);
|
|
144
|
+
transition: border-color 0.15s;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.field input:focus,
|
|
148
|
+
.field select:focus {
|
|
149
|
+
outline: none;
|
|
150
|
+
border-color: var(--text-primary);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.field input[type="number"] { max-width: 120px; }
|
|
154
|
+
|
|
155
|
+
.field-row {
|
|
156
|
+
display: grid;
|
|
157
|
+
grid-template-columns: 1fr 1fr;
|
|
158
|
+
gap: 16px;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/* Toggle switch */
|
|
162
|
+
.toggle-wrap {
|
|
163
|
+
display: flex;
|
|
164
|
+
align-items: center;
|
|
165
|
+
gap: 10px;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
.toggle {
|
|
169
|
+
position: relative;
|
|
170
|
+
width: 40px;
|
|
171
|
+
height: 22px;
|
|
172
|
+
appearance: none;
|
|
173
|
+
-webkit-appearance: none;
|
|
174
|
+
background: var(--border-color);
|
|
175
|
+
border-radius: 11px;
|
|
176
|
+
cursor: pointer;
|
|
177
|
+
transition: background 0.2s;
|
|
178
|
+
border: none;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.toggle:checked { background: var(--green); }
|
|
182
|
+
|
|
183
|
+
.toggle::after {
|
|
184
|
+
content: '';
|
|
185
|
+
position: absolute;
|
|
186
|
+
top: 2px;
|
|
187
|
+
left: 2px;
|
|
188
|
+
width: 18px;
|
|
189
|
+
height: 18px;
|
|
190
|
+
background: #fff;
|
|
191
|
+
border-radius: 50%;
|
|
192
|
+
transition: transform 0.2s;
|
|
193
|
+
box-shadow: 0 1px 3px rgba(0,0,0,0.15);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
.toggle:checked::after { transform: translateX(18px); }
|
|
197
|
+
|
|
198
|
+
.toggle-label {
|
|
199
|
+
font-size: 0.875rem;
|
|
200
|
+
font-weight: 500;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/* ── List editor ───────────────────────────── */
|
|
204
|
+
.list-editor {
|
|
205
|
+
border: 1px solid var(--border-color);
|
|
206
|
+
border-radius: 8px;
|
|
207
|
+
background: var(--bg-primary);
|
|
208
|
+
max-height: 220px;
|
|
209
|
+
overflow-y: auto;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
.list-item {
|
|
213
|
+
display: flex;
|
|
214
|
+
align-items: center;
|
|
215
|
+
padding: 6px 10px;
|
|
216
|
+
font-size: 0.8125rem;
|
|
217
|
+
font-family: 'SF Mono', Monaco, Consolas, monospace;
|
|
218
|
+
border-bottom: 1px solid var(--border-color);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
.list-item:last-child { border-bottom: none; }
|
|
222
|
+
|
|
223
|
+
.list-item span { flex: 1; }
|
|
224
|
+
|
|
225
|
+
.list-item .remove-btn {
|
|
226
|
+
background: none;
|
|
227
|
+
border: none;
|
|
228
|
+
color: var(--text-muted);
|
|
229
|
+
cursor: pointer;
|
|
230
|
+
font-size: 1rem;
|
|
231
|
+
padding: 0 4px;
|
|
232
|
+
line-height: 1;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
.list-item .remove-btn:hover { color: var(--red); }
|
|
236
|
+
|
|
237
|
+
.list-add {
|
|
238
|
+
display: flex;
|
|
239
|
+
gap: 8px;
|
|
240
|
+
margin-top: 8px;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
.list-add input {
|
|
244
|
+
flex: 1;
|
|
245
|
+
padding: 6px 10px;
|
|
246
|
+
font-size: 0.8125rem;
|
|
247
|
+
font-family: 'SF Mono', Monaco, Consolas, monospace;
|
|
248
|
+
border: 1px solid var(--border-color);
|
|
249
|
+
border-radius: 6px;
|
|
250
|
+
background: var(--bg-primary);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
.list-add input:focus { outline: none; border-color: var(--text-primary); }
|
|
254
|
+
|
|
255
|
+
.list-add select {
|
|
256
|
+
padding: 6px 8px;
|
|
257
|
+
font-size: 0.8125rem;
|
|
258
|
+
border: 1px solid var(--border-color);
|
|
259
|
+
border-radius: 6px;
|
|
260
|
+
background: var(--bg-primary);
|
|
261
|
+
font-family: inherit;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
.btn-add {
|
|
265
|
+
padding: 6px 14px;
|
|
266
|
+
font-size: 0.8125rem;
|
|
267
|
+
font-weight: 500;
|
|
268
|
+
background: var(--bg-secondary);
|
|
269
|
+
border: 1px solid var(--border-color);
|
|
270
|
+
border-radius: 6px;
|
|
271
|
+
cursor: pointer;
|
|
272
|
+
font-family: inherit;
|
|
273
|
+
transition: all 0.15s;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
.btn-add:hover { background: var(--border-color); }
|
|
277
|
+
|
|
278
|
+
/* ── Save bar ──────────────────────────────── */
|
|
279
|
+
.save-bar {
|
|
280
|
+
position: fixed;
|
|
281
|
+
bottom: 0;
|
|
282
|
+
left: 0;
|
|
283
|
+
right: 0;
|
|
284
|
+
background: var(--card-bg);
|
|
285
|
+
border-top: 1px solid var(--border-color);
|
|
286
|
+
padding: 12px 24px;
|
|
287
|
+
display: flex;
|
|
288
|
+
justify-content: center;
|
|
289
|
+
gap: 12px;
|
|
290
|
+
align-items: center;
|
|
291
|
+
z-index: 100;
|
|
292
|
+
transform: translateY(100%);
|
|
293
|
+
transition: transform 0.25s ease;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
.save-bar.visible { transform: translateY(0); }
|
|
297
|
+
|
|
298
|
+
.btn-save {
|
|
299
|
+
padding: 8px 24px;
|
|
300
|
+
font-size: 0.875rem;
|
|
301
|
+
font-weight: 500;
|
|
302
|
+
background: var(--text-primary);
|
|
303
|
+
color: #fff;
|
|
304
|
+
border: none;
|
|
305
|
+
border-radius: 8px;
|
|
306
|
+
cursor: pointer;
|
|
307
|
+
font-family: inherit;
|
|
308
|
+
transition: background 0.15s;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
.btn-save:hover { background: #404040; }
|
|
312
|
+
|
|
313
|
+
.btn-reset {
|
|
314
|
+
padding: 8px 16px;
|
|
315
|
+
font-size: 0.875rem;
|
|
316
|
+
font-weight: 500;
|
|
317
|
+
background: none;
|
|
318
|
+
color: var(--text-secondary);
|
|
319
|
+
border: 1px solid var(--border-color);
|
|
320
|
+
border-radius: 8px;
|
|
321
|
+
cursor: pointer;
|
|
322
|
+
font-family: inherit;
|
|
323
|
+
transition: all 0.15s;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
.btn-reset:hover { background: var(--bg-secondary); }
|
|
327
|
+
|
|
328
|
+
/* ── Toast ─────────────────────────────────── */
|
|
329
|
+
.toast {
|
|
330
|
+
position: fixed;
|
|
331
|
+
top: 20px;
|
|
332
|
+
right: 20px;
|
|
333
|
+
padding: 12px 20px;
|
|
334
|
+
border-radius: 8px;
|
|
335
|
+
font-size: 0.875rem;
|
|
336
|
+
font-weight: 500;
|
|
337
|
+
color: #fff;
|
|
338
|
+
z-index: 200;
|
|
339
|
+
transform: translateX(120%);
|
|
340
|
+
transition: transform 0.3s ease;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
.toast.visible { transform: translateX(0); }
|
|
344
|
+
.toast.success { background: #16a34a; }
|
|
345
|
+
.toast.error { background: var(--red); }
|
|
346
|
+
|
|
347
|
+
/* ── Sessions & Logs ───────────────────────── */
|
|
348
|
+
.data-table {
|
|
349
|
+
width: 100%;
|
|
350
|
+
border-collapse: collapse;
|
|
351
|
+
font-size: 0.8125rem;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
.data-table th {
|
|
355
|
+
text-align: left;
|
|
356
|
+
padding: 8px 12px;
|
|
357
|
+
font-weight: 600;
|
|
358
|
+
color: var(--text-secondary);
|
|
359
|
+
border-bottom: 2px solid var(--border-color);
|
|
360
|
+
background: var(--bg-secondary);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
.data-table td {
|
|
364
|
+
padding: 8px 12px;
|
|
365
|
+
border-bottom: 1px solid var(--border-color);
|
|
366
|
+
color: var(--text-secondary);
|
|
367
|
+
font-family: 'SF Mono', Monaco, Consolas, monospace;
|
|
368
|
+
font-size: 0.75rem;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
.data-table tr:last-child td { border-bottom: none; }
|
|
372
|
+
|
|
373
|
+
.empty-state {
|
|
374
|
+
text-align: center;
|
|
375
|
+
padding: 40px 20px;
|
|
376
|
+
color: var(--text-muted);
|
|
377
|
+
font-size: 0.875rem;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
.refresh-btn {
|
|
381
|
+
float: right;
|
|
382
|
+
padding: 4px 12px;
|
|
383
|
+
font-size: 0.75rem;
|
|
384
|
+
font-weight: 500;
|
|
385
|
+
background: var(--bg-secondary);
|
|
386
|
+
border: 1px solid var(--border-color);
|
|
387
|
+
border-radius: 6px;
|
|
388
|
+
cursor: pointer;
|
|
389
|
+
font-family: inherit;
|
|
390
|
+
transition: all 0.15s;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
.refresh-btn:hover { background: var(--border-color); }
|
|
394
|
+
|
|
395
|
+
/* ── Mount path editor ─────────────────────── */
|
|
396
|
+
.mount-item {
|
|
397
|
+
display: flex;
|
|
398
|
+
align-items: center;
|
|
399
|
+
gap: 8px;
|
|
400
|
+
padding: 6px 10px;
|
|
401
|
+
font-size: 0.8125rem;
|
|
402
|
+
border-bottom: 1px solid var(--border-color);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
.mount-item:last-child { border-bottom: none; }
|
|
406
|
+
.mount-item .mount-path { flex: 1; font-family: 'SF Mono', Monaco, Consolas, monospace; }
|
|
407
|
+
.mount-item .mount-mode {
|
|
408
|
+
font-size: 0.75rem;
|
|
409
|
+
padding: 2px 6px;
|
|
410
|
+
border-radius: 4px;
|
|
411
|
+
font-weight: 500;
|
|
412
|
+
}
|
|
413
|
+
.mount-item .mount-mode.rw { background: #fef3c7; color: #92400e; }
|
|
414
|
+
.mount-item .mount-mode.ro { background: #dbeafe; color: #1e40af; }
|
|
415
|
+
|
|
416
|
+
/* ── Responsive ────────────────────────────── */
|
|
417
|
+
@media (max-width: 640px) {
|
|
418
|
+
.field-row { grid-template-columns: 1fr; }
|
|
419
|
+
.header { flex-direction: column; gap: 8px; }
|
|
420
|
+
}
|
|
421
|
+
</style>
|
|
422
|
+
</head>
|
|
423
|
+
<body>
|
|
424
|
+
|
|
425
|
+
<header class="header">
|
|
426
|
+
<div class="logo">LabGate Settings</div>
|
|
427
|
+
<div class="config-path" id="configPath"></div>
|
|
428
|
+
</header>
|
|
429
|
+
|
|
430
|
+
<div class="container">
|
|
431
|
+
<div class="tabs">
|
|
432
|
+
<button class="tab active" data-tab="runtime">Runtime</button>
|
|
433
|
+
<button class="tab" data-tab="network">Network</button>
|
|
434
|
+
<button class="tab" data-tab="filesystem">Filesystem</button>
|
|
435
|
+
<button class="tab" data-tab="commands">Commands</button>
|
|
436
|
+
<button class="tab" data-tab="audit">Audit</button>
|
|
437
|
+
<button class="tab" data-tab="sessions">Sessions</button>
|
|
438
|
+
<button class="tab" data-tab="logs">Logs</button>
|
|
439
|
+
</div>
|
|
440
|
+
|
|
441
|
+
<!-- Runtime -->
|
|
442
|
+
<div class="tab-panel active" id="panel-runtime">
|
|
443
|
+
<div class="card">
|
|
444
|
+
<h3>Runtime & Image</h3>
|
|
445
|
+
<p class="card-description">Container runtime and base image for sandbox sessions.</p>
|
|
446
|
+
<div class="field-row">
|
|
447
|
+
<div class="field">
|
|
448
|
+
<label for="runtime">Runtime</label>
|
|
449
|
+
<select id="runtime">
|
|
450
|
+
<option value="auto">auto (detect best available)</option>
|
|
451
|
+
<option value="apptainer">apptainer</option>
|
|
452
|
+
<option value="singularity">singularity</option>
|
|
453
|
+
<option value="podman">podman</option>
|
|
454
|
+
<option value="docker">docker</option>
|
|
455
|
+
</select>
|
|
456
|
+
</div>
|
|
457
|
+
<div class="field">
|
|
458
|
+
<label for="timeout">Session Timeout (hours)</label>
|
|
459
|
+
<input type="number" id="timeout" min="0" step="1" value="8">
|
|
460
|
+
</div>
|
|
461
|
+
</div>
|
|
462
|
+
<div class="field">
|
|
463
|
+
<label for="image">Container Image</label>
|
|
464
|
+
<input type="text" id="image" placeholder="docker.io/library/node:20-slim">
|
|
465
|
+
</div>
|
|
466
|
+
</div>
|
|
467
|
+
</div>
|
|
468
|
+
|
|
469
|
+
<!-- Network -->
|
|
470
|
+
<div class="tab-panel" id="panel-network">
|
|
471
|
+
<div class="card">
|
|
472
|
+
<h3>Network Policy</h3>
|
|
473
|
+
<p class="card-description">Control how sandboxed agents access the network.</p>
|
|
474
|
+
<div class="field">
|
|
475
|
+
<label for="networkMode">Mode</label>
|
|
476
|
+
<select id="networkMode">
|
|
477
|
+
<option value="none">none (fully isolated)</option>
|
|
478
|
+
<option value="filtered">filtered (proxy + domain allowlist)</option>
|
|
479
|
+
<option value="host">host (full network access)</option>
|
|
480
|
+
</select>
|
|
481
|
+
</div>
|
|
482
|
+
</div>
|
|
483
|
+
<div class="card">
|
|
484
|
+
<h3>Allowed Domains</h3>
|
|
485
|
+
<p class="card-description">Domains agents can reach in filtered mode.</p>
|
|
486
|
+
<div class="list-editor" id="domainsList"></div>
|
|
487
|
+
<div class="list-add">
|
|
488
|
+
<input type="text" id="domainInput" placeholder="api.example.com" onkeydown="if(event.key==='Enter'){addDomain()}">
|
|
489
|
+
<button class="btn-add" onclick="addDomain()">Add</button>
|
|
490
|
+
</div>
|
|
491
|
+
</div>
|
|
492
|
+
</div>
|
|
493
|
+
|
|
494
|
+
<!-- Filesystem -->
|
|
495
|
+
<div class="tab-panel" id="panel-filesystem">
|
|
496
|
+
<div class="card">
|
|
497
|
+
<h3>Blocked Patterns</h3>
|
|
498
|
+
<p class="card-description">Glob patterns hidden from the sandbox via empty overlays.</p>
|
|
499
|
+
<div class="list-editor" id="blockedList"></div>
|
|
500
|
+
<div class="list-add">
|
|
501
|
+
<input type="text" id="blockedInput" placeholder="**/.ssh" onkeydown="if(event.key==='Enter'){addBlocked()}">
|
|
502
|
+
<button class="btn-add" onclick="addBlocked()">Add</button>
|
|
503
|
+
</div>
|
|
504
|
+
</div>
|
|
505
|
+
<div class="card">
|
|
506
|
+
<h3>Extra Mount Paths</h3>
|
|
507
|
+
<p class="card-description">Additional host paths to mount into the sandbox.</p>
|
|
508
|
+
<div class="list-editor" id="mountsList"></div>
|
|
509
|
+
<div class="list-add">
|
|
510
|
+
<input type="text" id="mountPathInput" placeholder="/data/shared" onkeydown="if(event.key==='Enter'){addMount()}">
|
|
511
|
+
<select id="mountModeInput">
|
|
512
|
+
<option value="ro">ro</option>
|
|
513
|
+
<option value="rw">rw</option>
|
|
514
|
+
</select>
|
|
515
|
+
<button class="btn-add" onclick="addMount()">Add</button>
|
|
516
|
+
</div>
|
|
517
|
+
</div>
|
|
518
|
+
</div>
|
|
519
|
+
|
|
520
|
+
<!-- Commands -->
|
|
521
|
+
<div class="tab-panel" id="panel-commands">
|
|
522
|
+
<div class="card">
|
|
523
|
+
<h3>Command Blacklist</h3>
|
|
524
|
+
<p class="card-description">Commands blocked inside the sandbox. Agents get a clear error instead of silent failure.</p>
|
|
525
|
+
<div class="list-editor" id="blacklistList"></div>
|
|
526
|
+
<div class="list-add">
|
|
527
|
+
<input type="text" id="blacklistInput" placeholder="ssh" onkeydown="if(event.key==='Enter'){addBlacklist()}">
|
|
528
|
+
<button class="btn-add" onclick="addBlacklist()">Add</button>
|
|
529
|
+
</div>
|
|
530
|
+
</div>
|
|
531
|
+
</div>
|
|
532
|
+
|
|
533
|
+
<!-- Audit -->
|
|
534
|
+
<div class="tab-panel" id="panel-audit">
|
|
535
|
+
<div class="card">
|
|
536
|
+
<h3>Audit Logging</h3>
|
|
537
|
+
<p class="card-description">Record session events to structured JSONL files.</p>
|
|
538
|
+
<div class="field">
|
|
539
|
+
<div class="toggle-wrap">
|
|
540
|
+
<input type="checkbox" class="toggle" id="auditEnabled">
|
|
541
|
+
<span class="toggle-label">Enable audit logging</span>
|
|
542
|
+
</div>
|
|
543
|
+
</div>
|
|
544
|
+
<div class="field" style="margin-top: 16px">
|
|
545
|
+
<label for="logDir">Log Directory</label>
|
|
546
|
+
<input type="text" id="logDir" placeholder="~/.labgate/logs">
|
|
547
|
+
</div>
|
|
548
|
+
</div>
|
|
549
|
+
</div>
|
|
550
|
+
|
|
551
|
+
<!-- Sessions -->
|
|
552
|
+
<div class="tab-panel" id="panel-sessions">
|
|
553
|
+
<div class="card">
|
|
554
|
+
<h3>Active Sessions <button class="refresh-btn" onclick="loadSessions()">Refresh</button></h3>
|
|
555
|
+
<div id="sessionsContent"><div class="empty-state">Loading...</div></div>
|
|
556
|
+
</div>
|
|
557
|
+
</div>
|
|
558
|
+
|
|
559
|
+
<!-- Logs -->
|
|
560
|
+
<div class="tab-panel" id="panel-logs">
|
|
561
|
+
<div class="card">
|
|
562
|
+
<h3>Recent Audit Logs <button class="refresh-btn" onclick="loadLogs()">Refresh</button></h3>
|
|
563
|
+
<div id="logsContent"><div class="empty-state">Loading...</div></div>
|
|
564
|
+
</div>
|
|
565
|
+
</div>
|
|
566
|
+
</div>
|
|
567
|
+
|
|
568
|
+
<div class="save-bar" id="saveBar">
|
|
569
|
+
<button class="btn-reset" onclick="resetConfig()">Reset</button>
|
|
570
|
+
<button class="btn-save" onclick="saveConfig()">Save Changes</button>
|
|
571
|
+
</div>
|
|
572
|
+
|
|
573
|
+
<div class="toast" id="toast"></div>
|
|
574
|
+
|
|
575
|
+
<script>
|
|
576
|
+
var config = {};
|
|
577
|
+
var originalConfig = '';
|
|
578
|
+
var dirty = false;
|
|
579
|
+
|
|
580
|
+
// ── Tab navigation ───────────────────────────
|
|
581
|
+
document.querySelectorAll('.tab').forEach(function(tab) {
|
|
582
|
+
tab.addEventListener('click', function() {
|
|
583
|
+
document.querySelectorAll('.tab').forEach(function(t) { t.classList.remove('active'); });
|
|
584
|
+
document.querySelectorAll('.tab-panel').forEach(function(p) { p.classList.remove('active'); });
|
|
585
|
+
tab.classList.add('active');
|
|
586
|
+
document.getElementById('panel-' + tab.dataset.tab).classList.add('active');
|
|
587
|
+
if (tab.dataset.tab === 'sessions') loadSessions();
|
|
588
|
+
if (tab.dataset.tab === 'logs') loadLogs();
|
|
589
|
+
});
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
// ── Toast ────────────────────────────────────
|
|
593
|
+
function showToast(msg, type) {
|
|
594
|
+
var toast = document.getElementById('toast');
|
|
595
|
+
toast.textContent = msg;
|
|
596
|
+
toast.className = 'toast ' + type + ' visible';
|
|
597
|
+
setTimeout(function() { toast.classList.remove('visible'); }, 3000);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// ── Dirty tracking ───────────────────────────
|
|
601
|
+
function markDirty() {
|
|
602
|
+
var current = JSON.stringify(config);
|
|
603
|
+
dirty = current !== originalConfig;
|
|
604
|
+
document.getElementById('saveBar').classList.toggle('visible', dirty);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// ── Populate UI from config ──────────────────
|
|
608
|
+
function populateUI() {
|
|
609
|
+
document.getElementById('runtime').value = config.runtime || 'auto';
|
|
610
|
+
document.getElementById('image').value = config.image || '';
|
|
611
|
+
document.getElementById('timeout').value = config.session_timeout_hours || 8;
|
|
612
|
+
document.getElementById('networkMode').value = config.network ? config.network.mode : 'none';
|
|
613
|
+
document.getElementById('auditEnabled').checked = config.audit ? config.audit.enabled : true;
|
|
614
|
+
document.getElementById('logDir').value = config.audit ? config.audit.log_dir : '~/.labgate/logs';
|
|
615
|
+
|
|
616
|
+
renderList('domainsList', config.network ? config.network.allowed_domains : [], removeDomain);
|
|
617
|
+
renderList('blockedList', config.filesystem ? config.filesystem.blocked_patterns : [], removeBlocked);
|
|
618
|
+
renderBlacklist();
|
|
619
|
+
renderMounts();
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// ── Collect config from UI ───────────────────
|
|
623
|
+
function collectConfig() {
|
|
624
|
+
config.runtime = document.getElementById('runtime').value;
|
|
625
|
+
config.image = document.getElementById('image').value;
|
|
626
|
+
config.session_timeout_hours = parseFloat(document.getElementById('timeout').value) || 8;
|
|
627
|
+
if (!config.network) config.network = {};
|
|
628
|
+
config.network.mode = document.getElementById('networkMode').value;
|
|
629
|
+
if (!config.audit) config.audit = {};
|
|
630
|
+
config.audit.enabled = document.getElementById('auditEnabled').checked;
|
|
631
|
+
config.audit.log_dir = document.getElementById('logDir').value;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// ── List rendering ───────────────────────────
|
|
635
|
+
function renderList(containerId, items, removeFn) {
|
|
636
|
+
var container = document.getElementById(containerId);
|
|
637
|
+
if (!items || items.length === 0) {
|
|
638
|
+
container.innerHTML = '<div class="empty-state" style="padding:16px">No items</div>';
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
container.innerHTML = items.map(function(item, i) {
|
|
642
|
+
return '<div class="list-item"><span>' + escapeHtml(item) + '</span><button class="remove-btn" data-index="' + i + '">×</button></div>';
|
|
643
|
+
}).join('');
|
|
644
|
+
container.querySelectorAll('.remove-btn').forEach(function(btn) {
|
|
645
|
+
btn.addEventListener('click', function() { removeFn(parseInt(btn.dataset.index)); });
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
function renderBlacklist() {
|
|
650
|
+
var items = config.commands ? config.commands.blacklist : [];
|
|
651
|
+
renderList('blacklistList', items, removeBlacklist);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
function renderMounts() {
|
|
655
|
+
var container = document.getElementById('mountsList');
|
|
656
|
+
var mounts = config.filesystem ? config.filesystem.extra_paths : [];
|
|
657
|
+
if (!mounts || mounts.length === 0) {
|
|
658
|
+
container.innerHTML = '<div class="empty-state" style="padding:16px">No extra mounts</div>';
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
container.innerHTML = mounts.map(function(m, i) {
|
|
662
|
+
return '<div class="mount-item"><span class="mount-path">' + escapeHtml(m.path) + '</span><span class="mount-mode ' + m.mode + '">' + m.mode + '</span><button class="remove-btn" data-index="' + i + '">×</button></div>';
|
|
663
|
+
}).join('');
|
|
664
|
+
container.querySelectorAll('.remove-btn').forEach(function(btn) {
|
|
665
|
+
btn.addEventListener('click', function() { removeMount(parseInt(btn.dataset.index)); });
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// ── Add/Remove helpers ───────────────────────
|
|
670
|
+
function addDomain() {
|
|
671
|
+
var input = document.getElementById('domainInput');
|
|
672
|
+
var val = input.value.trim();
|
|
673
|
+
if (!val) return;
|
|
674
|
+
collectConfig();
|
|
675
|
+
if (!config.network.allowed_domains) config.network.allowed_domains = [];
|
|
676
|
+
config.network.allowed_domains.push(val);
|
|
677
|
+
input.value = '';
|
|
678
|
+
renderList('domainsList', config.network.allowed_domains, removeDomain);
|
|
679
|
+
markDirty();
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
function removeDomain(i) {
|
|
683
|
+
collectConfig();
|
|
684
|
+
config.network.allowed_domains.splice(i, 1);
|
|
685
|
+
renderList('domainsList', config.network.allowed_domains, removeDomain);
|
|
686
|
+
markDirty();
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
function addBlocked() {
|
|
690
|
+
var input = document.getElementById('blockedInput');
|
|
691
|
+
var val = input.value.trim();
|
|
692
|
+
if (!val) return;
|
|
693
|
+
collectConfig();
|
|
694
|
+
if (!config.filesystem) config.filesystem = {};
|
|
695
|
+
if (!config.filesystem.blocked_patterns) config.filesystem.blocked_patterns = [];
|
|
696
|
+
config.filesystem.blocked_patterns.push(val);
|
|
697
|
+
input.value = '';
|
|
698
|
+
renderList('blockedList', config.filesystem.blocked_patterns, removeBlocked);
|
|
699
|
+
markDirty();
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
function removeBlocked(i) {
|
|
703
|
+
collectConfig();
|
|
704
|
+
config.filesystem.blocked_patterns.splice(i, 1);
|
|
705
|
+
renderList('blockedList', config.filesystem.blocked_patterns, removeBlocked);
|
|
706
|
+
markDirty();
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
function addBlacklist() {
|
|
710
|
+
var input = document.getElementById('blacklistInput');
|
|
711
|
+
var val = input.value.trim();
|
|
712
|
+
if (!val) return;
|
|
713
|
+
collectConfig();
|
|
714
|
+
if (!config.commands) config.commands = {};
|
|
715
|
+
if (!config.commands.blacklist) config.commands.blacklist = [];
|
|
716
|
+
config.commands.blacklist.push(val);
|
|
717
|
+
input.value = '';
|
|
718
|
+
renderBlacklist();
|
|
719
|
+
markDirty();
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
function removeBlacklist(i) {
|
|
723
|
+
collectConfig();
|
|
724
|
+
config.commands.blacklist.splice(i, 1);
|
|
725
|
+
renderBlacklist();
|
|
726
|
+
markDirty();
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
function addMount() {
|
|
730
|
+
var pathInput = document.getElementById('mountPathInput');
|
|
731
|
+
var modeInput = document.getElementById('mountModeInput');
|
|
732
|
+
var path = pathInput.value.trim();
|
|
733
|
+
if (!path) return;
|
|
734
|
+
collectConfig();
|
|
735
|
+
if (!config.filesystem) config.filesystem = {};
|
|
736
|
+
if (!config.filesystem.extra_paths) config.filesystem.extra_paths = [];
|
|
737
|
+
config.filesystem.extra_paths.push({ path: path, mode: modeInput.value });
|
|
738
|
+
pathInput.value = '';
|
|
739
|
+
renderMounts();
|
|
740
|
+
markDirty();
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
function removeMount(i) {
|
|
744
|
+
collectConfig();
|
|
745
|
+
config.filesystem.extra_paths.splice(i, 1);
|
|
746
|
+
renderMounts();
|
|
747
|
+
markDirty();
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
// ── Change listeners on simple fields ────────
|
|
751
|
+
['runtime', 'image', 'timeout', 'networkMode', 'auditEnabled', 'logDir'].forEach(function(id) {
|
|
752
|
+
var el = document.getElementById(id);
|
|
753
|
+
el.addEventListener('change', function() { collectConfig(); markDirty(); });
|
|
754
|
+
if (el.type === 'text' || el.type === 'number') {
|
|
755
|
+
el.addEventListener('input', function() { collectConfig(); markDirty(); });
|
|
756
|
+
}
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
// ── API calls ────────────────────────────────
|
|
760
|
+
function loadConfig() {
|
|
761
|
+
fetch('/api/config').then(function(r) { return r.json(); }).then(function(data) {
|
|
762
|
+
config = data;
|
|
763
|
+
originalConfig = JSON.stringify(data);
|
|
764
|
+
populateUI();
|
|
765
|
+
}).catch(function(err) {
|
|
766
|
+
showToast('Failed to load config', 'error');
|
|
767
|
+
});
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
function saveConfig() {
|
|
771
|
+
collectConfig();
|
|
772
|
+
fetch('/api/config', {
|
|
773
|
+
method: 'POST',
|
|
774
|
+
headers: { 'Content-Type': 'application/json' },
|
|
775
|
+
body: JSON.stringify(config)
|
|
776
|
+
}).then(function(r) { return r.json(); }).then(function(data) {
|
|
777
|
+
if (data.ok) {
|
|
778
|
+
originalConfig = JSON.stringify(config);
|
|
779
|
+
dirty = false;
|
|
780
|
+
document.getElementById('saveBar').classList.remove('visible');
|
|
781
|
+
showToast('Config saved', 'success');
|
|
782
|
+
} else {
|
|
783
|
+
showToast('Validation error: ' + (data.errors || []).join(', '), 'error');
|
|
784
|
+
}
|
|
785
|
+
}).catch(function(err) {
|
|
786
|
+
showToast('Save failed: ' + err.message, 'error');
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
function resetConfig() {
|
|
791
|
+
config = JSON.parse(originalConfig);
|
|
792
|
+
populateUI();
|
|
793
|
+
dirty = false;
|
|
794
|
+
document.getElementById('saveBar').classList.remove('visible');
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
function loadConfigPath() {
|
|
798
|
+
fetch('/api/config/path').then(function(r) { return r.json(); }).then(function(data) {
|
|
799
|
+
document.getElementById('configPath').textContent = data.path || '';
|
|
800
|
+
}).catch(function() {});
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
function loadSessions() {
|
|
804
|
+
var el = document.getElementById('sessionsContent');
|
|
805
|
+
el.innerHTML = '<div class="empty-state">Loading...</div>';
|
|
806
|
+
fetch('/api/sessions').then(function(r) { return r.json(); }).then(function(data) {
|
|
807
|
+
if (!data.sessions || data.sessions.length === 0) {
|
|
808
|
+
el.innerHTML = '<div class="empty-state">No active sessions</div>';
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
var html = '<table class="data-table"><thead><tr><th>Name</th><th>Status</th><th>Running</th></tr></thead><tbody>';
|
|
812
|
+
data.sessions.forEach(function(s) {
|
|
813
|
+
html += '<tr><td>' + escapeHtml(s.name) + '</td><td>' + escapeHtml(s.status) + '</td><td>' + escapeHtml(s.running || '') + '</td></tr>';
|
|
814
|
+
});
|
|
815
|
+
html += '</tbody></table>';
|
|
816
|
+
el.innerHTML = html;
|
|
817
|
+
}).catch(function(err) {
|
|
818
|
+
el.innerHTML = '<div class="empty-state">Could not load sessions: ' + escapeHtml(err.message) + '</div>';
|
|
819
|
+
});
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
function loadLogs() {
|
|
823
|
+
var el = document.getElementById('logsContent');
|
|
824
|
+
el.innerHTML = '<div class="empty-state">Loading...</div>';
|
|
825
|
+
fetch('/api/logs').then(function(r) { return r.json(); }).then(function(data) {
|
|
826
|
+
if (!data.entries || data.entries.length === 0) {
|
|
827
|
+
el.innerHTML = '<div class="empty-state">' + escapeHtml(data.message || 'No log entries') + '</div>';
|
|
828
|
+
return;
|
|
829
|
+
}
|
|
830
|
+
var html = '<table class="data-table"><thead><tr><th>Time</th><th>Session</th><th>Event</th><th>Details</th></tr></thead><tbody>';
|
|
831
|
+
data.entries.forEach(function(e) {
|
|
832
|
+
var time = (e.timestamp || '').slice(11, 19) || '??:??:??';
|
|
833
|
+
var sess = (e.session || '').slice(0, 8) || '????????';
|
|
834
|
+
var ev = e.event || 'unknown';
|
|
835
|
+
var details = [];
|
|
836
|
+
if (e.agent) details.push('agent=' + e.agent);
|
|
837
|
+
if (e.exit_code !== undefined) details.push('exit=' + e.exit_code);
|
|
838
|
+
if (e.timeout_hours) details.push('timeout=' + e.timeout_hours + 'h');
|
|
839
|
+
if (e.network_mode) details.push('net=' + e.network_mode);
|
|
840
|
+
html += '<tr><td>' + escapeHtml(time) + '</td><td>' + escapeHtml(sess) + '</td><td>' + escapeHtml(ev) + '</td><td>' + escapeHtml(details.join(' ')) + '</td></tr>';
|
|
841
|
+
});
|
|
842
|
+
html += '</tbody></table>';
|
|
843
|
+
el.innerHTML = html;
|
|
844
|
+
}).catch(function(err) {
|
|
845
|
+
el.innerHTML = '<div class="empty-state">Could not load logs: ' + escapeHtml(err.message) + '</div>';
|
|
846
|
+
});
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
function escapeHtml(str) {
|
|
850
|
+
var div = document.createElement('div');
|
|
851
|
+
div.textContent = str || '';
|
|
852
|
+
return div.innerHTML;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
// ── Init ─────────────────────────────────────
|
|
856
|
+
loadConfig();
|
|
857
|
+
loadConfigPath();
|
|
858
|
+
</script>
|
|
859
|
+
|
|
860
|
+
</body>
|
|
861
|
+
</html>
|