@grwnd/pi-governance 1.6.0 → 1.7.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/README.md +28 -256
- package/dist/extensions/index.cjs +1710 -50
- package/dist/extensions/index.cjs.map +1 -1
- package/dist/extensions/index.js +1704 -47
- package/dist/extensions/index.js.map +1 -1
- package/dist/index.cjs +1599 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +15 -1
- package/dist/index.d.ts +15 -1
- package/dist/index.js +1597 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
2
3
|
var __defProp = Object.defineProperty;
|
|
3
4
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
5
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __esm = (fn, res) => function __init() {
|
|
9
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
10
|
+
};
|
|
6
11
|
var __export = (target, all) => {
|
|
7
12
|
for (var name in all)
|
|
8
13
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
@@ -15,15 +20,1694 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
15
20
|
}
|
|
16
21
|
return to;
|
|
17
22
|
};
|
|
23
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
24
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
25
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
26
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
27
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
28
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
29
|
+
mod
|
|
30
|
+
));
|
|
18
31
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
32
|
|
|
33
|
+
// src/lib/config/defaults.ts
|
|
34
|
+
var DEFAULTS;
|
|
35
|
+
var init_defaults = __esm({
|
|
36
|
+
"src/lib/config/defaults.ts"() {
|
|
37
|
+
"use strict";
|
|
38
|
+
DEFAULTS = {
|
|
39
|
+
auth: {
|
|
40
|
+
provider: "env",
|
|
41
|
+
env: {
|
|
42
|
+
user_var: "GRWND_USER",
|
|
43
|
+
role_var: "GRWND_ROLE",
|
|
44
|
+
org_unit_var: "GRWND_ORG_UNIT"
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
policy: {
|
|
48
|
+
engine: "yaml",
|
|
49
|
+
yaml: {
|
|
50
|
+
rules_file: "./governance-rules.yaml"
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
templates: {
|
|
54
|
+
directory: "./templates/",
|
|
55
|
+
default: "project-lead"
|
|
56
|
+
},
|
|
57
|
+
hitl: {
|
|
58
|
+
default_mode: "supervised",
|
|
59
|
+
approval_channel: "cli",
|
|
60
|
+
timeout_seconds: 300
|
|
61
|
+
},
|
|
62
|
+
audit: {
|
|
63
|
+
sinks: [{ type: "jsonl", path: "~/.pi/agent/audit.jsonl" }]
|
|
64
|
+
},
|
|
65
|
+
dlp: {
|
|
66
|
+
enabled: true,
|
|
67
|
+
mode: "audit",
|
|
68
|
+
on_input: "block",
|
|
69
|
+
on_output: "mask",
|
|
70
|
+
masking: {
|
|
71
|
+
strategy: "partial",
|
|
72
|
+
show_chars: 4,
|
|
73
|
+
placeholder: "***"
|
|
74
|
+
},
|
|
75
|
+
severity_threshold: "low",
|
|
76
|
+
built_in: {
|
|
77
|
+
secrets: true,
|
|
78
|
+
pii: true
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// src/lib/wizard/html.ts
|
|
86
|
+
var WIZARD_HTML;
|
|
87
|
+
var init_html = __esm({
|
|
88
|
+
"src/lib/wizard/html.ts"() {
|
|
89
|
+
"use strict";
|
|
90
|
+
WIZARD_HTML = `<!DOCTYPE html>
|
|
91
|
+
<html lang="en">
|
|
92
|
+
<head>
|
|
93
|
+
<meta charset="UTF-8">
|
|
94
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
95
|
+
<title>Pi Governance \u2014 Setup Wizard</title>
|
|
96
|
+
<style>
|
|
97
|
+
:root {
|
|
98
|
+
--bg: #f8f9fa;
|
|
99
|
+
--bg-surface: #ffffff;
|
|
100
|
+
--bg-surface-alt: #f1f3f5;
|
|
101
|
+
--bg-code: #e9ecef;
|
|
102
|
+
--text: #212529;
|
|
103
|
+
--text-muted: #6c757d;
|
|
104
|
+
--text-inverse: #ffffff;
|
|
105
|
+
--border: #dee2e6;
|
|
106
|
+
--border-focus: #4263eb;
|
|
107
|
+
--primary: #4263eb;
|
|
108
|
+
--primary-hover: #3b5bdb;
|
|
109
|
+
--primary-subtle: #dbe4ff;
|
|
110
|
+
--success: #2f9e44;
|
|
111
|
+
--success-subtle: #d3f9d8;
|
|
112
|
+
--danger: #e03131;
|
|
113
|
+
--danger-subtle: #ffe3e3;
|
|
114
|
+
--warning: #f08c00;
|
|
115
|
+
--warning-subtle: #fff3bf;
|
|
116
|
+
--radius: 8px;
|
|
117
|
+
--radius-sm: 4px;
|
|
118
|
+
--shadow: 0 1px 3px rgba(0,0,0,0.08);
|
|
119
|
+
--shadow-lg: 0 4px 12px rgba(0,0,0,0.1);
|
|
120
|
+
--font-mono: 'SF Mono', 'Cascadia Code', 'Fira Code', monospace;
|
|
121
|
+
--font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
|
|
122
|
+
--transition: 150ms ease;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
@media (prefers-color-scheme: dark) {
|
|
126
|
+
:root {
|
|
127
|
+
--bg: #1a1b1e;
|
|
128
|
+
--bg-surface: #25262b;
|
|
129
|
+
--bg-surface-alt: #2c2e33;
|
|
130
|
+
--bg-code: #2c2e33;
|
|
131
|
+
--text: #c1c2c5;
|
|
132
|
+
--text-muted: #909296;
|
|
133
|
+
--text-inverse: #1a1b1e;
|
|
134
|
+
--border: #373a40;
|
|
135
|
+
--border-focus: #5c7cfa;
|
|
136
|
+
--primary: #5c7cfa;
|
|
137
|
+
--primary-hover: #748ffc;
|
|
138
|
+
--primary-subtle: #1b2559;
|
|
139
|
+
--success: #51cf66;
|
|
140
|
+
--success-subtle: #0b3d1a;
|
|
141
|
+
--danger: #ff6b6b;
|
|
142
|
+
--danger-subtle: #3d0b0b;
|
|
143
|
+
--warning: #fcc419;
|
|
144
|
+
--warning-subtle: #3d2e00;
|
|
145
|
+
--shadow: 0 1px 3px rgba(0,0,0,0.3);
|
|
146
|
+
--shadow-lg: 0 4px 12px rgba(0,0,0,0.4);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
151
|
+
|
|
152
|
+
body {
|
|
153
|
+
font-family: var(--font-sans);
|
|
154
|
+
background: var(--bg);
|
|
155
|
+
color: var(--text);
|
|
156
|
+
line-height: 1.6;
|
|
157
|
+
min-height: 100vh;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.layout {
|
|
161
|
+
display: grid;
|
|
162
|
+
grid-template-columns: 1fr 420px;
|
|
163
|
+
gap: 0;
|
|
164
|
+
min-height: 100vh;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
@media (max-width: 1024px) {
|
|
168
|
+
.layout { grid-template-columns: 1fr; }
|
|
169
|
+
.preview-panel { display: none; }
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/* --- Left column: Form --- */
|
|
173
|
+
.form-column {
|
|
174
|
+
padding: 32px 40px 80px;
|
|
175
|
+
overflow-y: auto;
|
|
176
|
+
max-height: 100vh;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
.logo {
|
|
180
|
+
display: flex;
|
|
181
|
+
align-items: center;
|
|
182
|
+
gap: 10px;
|
|
183
|
+
margin-bottom: 8px;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
.logo-icon {
|
|
187
|
+
width: 36px; height: 36px;
|
|
188
|
+
background: var(--primary);
|
|
189
|
+
border-radius: var(--radius);
|
|
190
|
+
display: flex; align-items: center; justify-content: center;
|
|
191
|
+
color: var(--text-inverse);
|
|
192
|
+
font-weight: 700; font-size: 18px;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.logo-text {
|
|
196
|
+
font-size: 20px;
|
|
197
|
+
font-weight: 700;
|
|
198
|
+
color: var(--text);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.logo-text span { color: var(--text-muted); font-weight: 400; }
|
|
202
|
+
|
|
203
|
+
h1 {
|
|
204
|
+
font-size: 28px;
|
|
205
|
+
font-weight: 700;
|
|
206
|
+
margin: 24px 0 8px;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
.subtitle {
|
|
210
|
+
color: var(--text-muted);
|
|
211
|
+
font-size: 15px;
|
|
212
|
+
margin-bottom: 32px;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/* Sections */
|
|
216
|
+
.section {
|
|
217
|
+
background: var(--bg-surface);
|
|
218
|
+
border: 1px solid var(--border);
|
|
219
|
+
border-radius: var(--radius);
|
|
220
|
+
padding: 24px;
|
|
221
|
+
margin-bottom: 20px;
|
|
222
|
+
box-shadow: var(--shadow);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
.section-header {
|
|
226
|
+
display: flex;
|
|
227
|
+
align-items: center;
|
|
228
|
+
gap: 10px;
|
|
229
|
+
margin-bottom: 16px;
|
|
230
|
+
cursor: pointer;
|
|
231
|
+
user-select: none;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
.section-header h2 {
|
|
235
|
+
font-size: 16px;
|
|
236
|
+
font-weight: 600;
|
|
237
|
+
flex: 1;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
.section-badge {
|
|
241
|
+
font-size: 11px;
|
|
242
|
+
padding: 2px 8px;
|
|
243
|
+
border-radius: 10px;
|
|
244
|
+
font-weight: 600;
|
|
245
|
+
text-transform: uppercase;
|
|
246
|
+
letter-spacing: 0.5px;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
.badge-required { background: var(--danger-subtle); color: var(--danger); }
|
|
250
|
+
.badge-optional { background: var(--primary-subtle); color: var(--primary); }
|
|
251
|
+
|
|
252
|
+
.section-icon {
|
|
253
|
+
width: 32px; height: 32px;
|
|
254
|
+
border-radius: var(--radius-sm);
|
|
255
|
+
display: flex; align-items: center; justify-content: center;
|
|
256
|
+
font-size: 16px;
|
|
257
|
+
flex-shrink: 0;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
.section-body { display: block; }
|
|
261
|
+
.section.collapsed .section-body { display: none; }
|
|
262
|
+
.section-chevron {
|
|
263
|
+
transition: transform var(--transition);
|
|
264
|
+
color: var(--text-muted);
|
|
265
|
+
font-size: 12px;
|
|
266
|
+
}
|
|
267
|
+
.section.collapsed .section-chevron { transform: rotate(-90deg); }
|
|
268
|
+
|
|
269
|
+
/* Form elements */
|
|
270
|
+
label {
|
|
271
|
+
display: block;
|
|
272
|
+
font-size: 13px;
|
|
273
|
+
font-weight: 600;
|
|
274
|
+
color: var(--text);
|
|
275
|
+
margin-bottom: 4px;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
.label-hint {
|
|
279
|
+
font-weight: 400;
|
|
280
|
+
color: var(--text-muted);
|
|
281
|
+
font-size: 12px;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
input[type="text"],
|
|
285
|
+
input[type="number"],
|
|
286
|
+
select {
|
|
287
|
+
width: 100%;
|
|
288
|
+
padding: 8px 12px;
|
|
289
|
+
border: 1px solid var(--border);
|
|
290
|
+
border-radius: var(--radius-sm);
|
|
291
|
+
background: var(--bg-surface);
|
|
292
|
+
color: var(--text);
|
|
293
|
+
font-size: 14px;
|
|
294
|
+
font-family: var(--font-sans);
|
|
295
|
+
transition: border-color var(--transition);
|
|
296
|
+
outline: none;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
input:focus, select:focus {
|
|
300
|
+
border-color: var(--border-focus);
|
|
301
|
+
box-shadow: 0 0 0 2px var(--primary-subtle);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
.field { margin-bottom: 16px; }
|
|
305
|
+
|
|
306
|
+
.field-row {
|
|
307
|
+
display: grid;
|
|
308
|
+
grid-template-columns: 1fr 1fr;
|
|
309
|
+
gap: 12px;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
.field-row-3 {
|
|
313
|
+
display: grid;
|
|
314
|
+
grid-template-columns: 1fr 1fr 1fr;
|
|
315
|
+
gap: 12px;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/* Toggle switch */
|
|
319
|
+
.toggle-row {
|
|
320
|
+
display: flex;
|
|
321
|
+
align-items: center;
|
|
322
|
+
gap: 10px;
|
|
323
|
+
margin-bottom: 12px;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
.toggle {
|
|
327
|
+
position: relative;
|
|
328
|
+
width: 40px; height: 22px;
|
|
329
|
+
flex-shrink: 0;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
.toggle input { display: none; }
|
|
333
|
+
|
|
334
|
+
.toggle-slider {
|
|
335
|
+
position: absolute;
|
|
336
|
+
inset: 0;
|
|
337
|
+
background: var(--border);
|
|
338
|
+
border-radius: 11px;
|
|
339
|
+
cursor: pointer;
|
|
340
|
+
transition: background var(--transition);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
.toggle-slider::before {
|
|
344
|
+
content: '';
|
|
345
|
+
position: absolute;
|
|
346
|
+
width: 16px; height: 16px;
|
|
347
|
+
left: 3px; top: 3px;
|
|
348
|
+
background: white;
|
|
349
|
+
border-radius: 50%;
|
|
350
|
+
transition: transform var(--transition);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
.toggle input:checked + .toggle-slider {
|
|
354
|
+
background: var(--primary);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
.toggle input:checked + .toggle-slider::before {
|
|
358
|
+
transform: translateX(18px);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
.toggle-label {
|
|
362
|
+
font-size: 14px;
|
|
363
|
+
font-weight: 500;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/* Role cards */
|
|
367
|
+
.role-grid {
|
|
368
|
+
display: grid;
|
|
369
|
+
grid-template-columns: 1fr 1fr;
|
|
370
|
+
gap: 12px;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
.role-card {
|
|
374
|
+
border: 2px solid var(--border);
|
|
375
|
+
border-radius: var(--radius);
|
|
376
|
+
padding: 16px;
|
|
377
|
+
cursor: pointer;
|
|
378
|
+
transition: all var(--transition);
|
|
379
|
+
position: relative;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
.role-card:hover { border-color: var(--primary); }
|
|
383
|
+
|
|
384
|
+
.role-card.selected {
|
|
385
|
+
border-color: var(--primary);
|
|
386
|
+
background: var(--primary-subtle);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
.role-card-check {
|
|
390
|
+
position: absolute;
|
|
391
|
+
top: 12px; right: 12px;
|
|
392
|
+
width: 20px; height: 20px;
|
|
393
|
+
border: 2px solid var(--border);
|
|
394
|
+
border-radius: 4px;
|
|
395
|
+
display: flex; align-items: center; justify-content: center;
|
|
396
|
+
font-size: 12px;
|
|
397
|
+
color: transparent;
|
|
398
|
+
transition: all var(--transition);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
.role-card.selected .role-card-check {
|
|
402
|
+
background: var(--primary);
|
|
403
|
+
border-color: var(--primary);
|
|
404
|
+
color: white;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
.role-name {
|
|
408
|
+
font-weight: 600;
|
|
409
|
+
font-size: 14px;
|
|
410
|
+
margin-bottom: 4px;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
.role-desc {
|
|
414
|
+
font-size: 12px;
|
|
415
|
+
color: var(--text-muted);
|
|
416
|
+
margin-bottom: 8px;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
.role-tags {
|
|
420
|
+
display: flex;
|
|
421
|
+
flex-wrap: wrap;
|
|
422
|
+
gap: 4px;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
.role-tag {
|
|
426
|
+
font-size: 11px;
|
|
427
|
+
padding: 1px 6px;
|
|
428
|
+
border-radius: 3px;
|
|
429
|
+
background: var(--bg-surface-alt);
|
|
430
|
+
color: var(--text-muted);
|
|
431
|
+
font-family: var(--font-mono);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
.role-details {
|
|
435
|
+
display: none;
|
|
436
|
+
margin-top: 12px;
|
|
437
|
+
padding-top: 12px;
|
|
438
|
+
border-top: 1px solid var(--border);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
.role-card.selected .role-details { display: block; }
|
|
442
|
+
|
|
443
|
+
/* Chip selector */
|
|
444
|
+
.chip-group {
|
|
445
|
+
display: flex;
|
|
446
|
+
gap: 6px;
|
|
447
|
+
flex-wrap: wrap;
|
|
448
|
+
margin-bottom: 12px;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
.chip {
|
|
452
|
+
padding: 5px 14px;
|
|
453
|
+
border: 1px solid var(--border);
|
|
454
|
+
border-radius: 16px;
|
|
455
|
+
font-size: 13px;
|
|
456
|
+
cursor: pointer;
|
|
457
|
+
transition: all var(--transition);
|
|
458
|
+
background: var(--bg-surface);
|
|
459
|
+
color: var(--text);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
.chip:hover { border-color: var(--primary); }
|
|
463
|
+
|
|
464
|
+
.chip.active {
|
|
465
|
+
background: var(--primary);
|
|
466
|
+
border-color: var(--primary);
|
|
467
|
+
color: var(--text-inverse);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/* Pattern list */
|
|
471
|
+
.pattern-list { margin-top: 12px; }
|
|
472
|
+
|
|
473
|
+
.pattern-item {
|
|
474
|
+
display: grid;
|
|
475
|
+
grid-template-columns: 1fr 1.5fr auto auto;
|
|
476
|
+
gap: 8px;
|
|
477
|
+
align-items: center;
|
|
478
|
+
margin-bottom: 8px;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
.pattern-item input, .pattern-item select {
|
|
482
|
+
font-size: 13px;
|
|
483
|
+
padding: 6px 8px;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
.btn-remove {
|
|
487
|
+
background: none;
|
|
488
|
+
border: none;
|
|
489
|
+
color: var(--danger);
|
|
490
|
+
cursor: pointer;
|
|
491
|
+
font-size: 18px;
|
|
492
|
+
padding: 4px;
|
|
493
|
+
line-height: 1;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
.btn-add {
|
|
497
|
+
background: none;
|
|
498
|
+
border: 1px dashed var(--border);
|
|
499
|
+
border-radius: var(--radius-sm);
|
|
500
|
+
padding: 6px 14px;
|
|
501
|
+
color: var(--primary);
|
|
502
|
+
cursor: pointer;
|
|
503
|
+
font-size: 13px;
|
|
504
|
+
font-weight: 500;
|
|
505
|
+
transition: all var(--transition);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
.btn-add:hover {
|
|
509
|
+
border-color: var(--primary);
|
|
510
|
+
background: var(--primary-subtle);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/* Allowlist */
|
|
514
|
+
.allowlist-item {
|
|
515
|
+
display: flex;
|
|
516
|
+
gap: 8px;
|
|
517
|
+
margin-bottom: 8px;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
.allowlist-item input { flex: 1; font-size: 13px; padding: 6px 8px; }
|
|
521
|
+
|
|
522
|
+
/* Sink list */
|
|
523
|
+
.sink-item {
|
|
524
|
+
background: var(--bg-surface-alt);
|
|
525
|
+
border-radius: var(--radius-sm);
|
|
526
|
+
padding: 12px;
|
|
527
|
+
margin-bottom: 8px;
|
|
528
|
+
position: relative;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
.sink-item .btn-remove {
|
|
532
|
+
position: absolute;
|
|
533
|
+
top: 8px; right: 8px;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/* --- Right column: Preview --- */
|
|
537
|
+
.preview-panel {
|
|
538
|
+
background: var(--bg-surface-alt);
|
|
539
|
+
border-left: 1px solid var(--border);
|
|
540
|
+
padding: 24px;
|
|
541
|
+
display: flex;
|
|
542
|
+
flex-direction: column;
|
|
543
|
+
position: sticky;
|
|
544
|
+
top: 0;
|
|
545
|
+
height: 100vh;
|
|
546
|
+
overflow: hidden;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
.preview-header {
|
|
550
|
+
display: flex;
|
|
551
|
+
align-items: center;
|
|
552
|
+
justify-content: space-between;
|
|
553
|
+
margin-bottom: 16px;
|
|
554
|
+
flex-shrink: 0;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
.preview-header h3 {
|
|
558
|
+
font-size: 14px;
|
|
559
|
+
font-weight: 600;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
.preview-tabs {
|
|
563
|
+
display: flex;
|
|
564
|
+
gap: 4px;
|
|
565
|
+
margin-bottom: 12px;
|
|
566
|
+
flex-shrink: 0;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
.preview-tab {
|
|
570
|
+
padding: 4px 12px;
|
|
571
|
+
font-size: 12px;
|
|
572
|
+
border: 1px solid var(--border);
|
|
573
|
+
border-radius: var(--radius-sm);
|
|
574
|
+
background: var(--bg-surface);
|
|
575
|
+
cursor: pointer;
|
|
576
|
+
color: var(--text-muted);
|
|
577
|
+
transition: all var(--transition);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
.preview-tab.active {
|
|
581
|
+
background: var(--primary);
|
|
582
|
+
border-color: var(--primary);
|
|
583
|
+
color: var(--text-inverse);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
.preview-content {
|
|
587
|
+
flex: 1;
|
|
588
|
+
overflow-y: auto;
|
|
589
|
+
background: var(--bg-surface);
|
|
590
|
+
border: 1px solid var(--border);
|
|
591
|
+
border-radius: var(--radius);
|
|
592
|
+
padding: 16px;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
.preview-content pre {
|
|
596
|
+
font-family: var(--font-mono);
|
|
597
|
+
font-size: 12px;
|
|
598
|
+
line-height: 1.5;
|
|
599
|
+
white-space: pre;
|
|
600
|
+
color: var(--text);
|
|
601
|
+
margin: 0;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/* Bottom bar */
|
|
605
|
+
.bottom-bar {
|
|
606
|
+
position: fixed;
|
|
607
|
+
bottom: 0;
|
|
608
|
+
left: 0;
|
|
609
|
+
right: 420px;
|
|
610
|
+
padding: 16px 40px;
|
|
611
|
+
background: var(--bg-surface);
|
|
612
|
+
border-top: 1px solid var(--border);
|
|
613
|
+
display: flex;
|
|
614
|
+
align-items: center;
|
|
615
|
+
justify-content: space-between;
|
|
616
|
+
z-index: 100;
|
|
617
|
+
box-shadow: 0 -2px 8px rgba(0,0,0,0.06);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
@media (max-width: 1024px) {
|
|
621
|
+
.bottom-bar { right: 0; }
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
.btn-primary {
|
|
625
|
+
padding: 10px 28px;
|
|
626
|
+
background: var(--primary);
|
|
627
|
+
color: var(--text-inverse);
|
|
628
|
+
border: none;
|
|
629
|
+
border-radius: var(--radius);
|
|
630
|
+
font-size: 14px;
|
|
631
|
+
font-weight: 600;
|
|
632
|
+
cursor: pointer;
|
|
633
|
+
transition: background var(--transition);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
.btn-primary:hover { background: var(--primary-hover); }
|
|
637
|
+
.btn-primary:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
638
|
+
|
|
639
|
+
.btn-secondary {
|
|
640
|
+
padding: 10px 20px;
|
|
641
|
+
background: var(--bg-surface);
|
|
642
|
+
color: var(--text);
|
|
643
|
+
border: 1px solid var(--border);
|
|
644
|
+
border-radius: var(--radius);
|
|
645
|
+
font-size: 14px;
|
|
646
|
+
font-weight: 500;
|
|
647
|
+
cursor: pointer;
|
|
648
|
+
transition: all var(--transition);
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
.btn-secondary:hover { border-color: var(--primary); color: var(--primary); }
|
|
652
|
+
|
|
653
|
+
.save-status {
|
|
654
|
+
font-size: 13px;
|
|
655
|
+
color: var(--text-muted);
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
.save-status.success { color: var(--success); }
|
|
659
|
+
.save-status.error { color: var(--danger); }
|
|
660
|
+
|
|
661
|
+
/* Toast */
|
|
662
|
+
.toast {
|
|
663
|
+
position: fixed;
|
|
664
|
+
top: 20px;
|
|
665
|
+
right: 20px;
|
|
666
|
+
padding: 12px 20px;
|
|
667
|
+
border-radius: var(--radius);
|
|
668
|
+
font-size: 14px;
|
|
669
|
+
font-weight: 500;
|
|
670
|
+
z-index: 1000;
|
|
671
|
+
box-shadow: var(--shadow-lg);
|
|
672
|
+
transform: translateY(-10px);
|
|
673
|
+
opacity: 0;
|
|
674
|
+
transition: all 300ms ease;
|
|
675
|
+
pointer-events: none;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
.toast.visible { transform: translateY(0); opacity: 1; pointer-events: auto; }
|
|
679
|
+
.toast.success { background: var(--success); color: white; }
|
|
680
|
+
.toast.error { background: var(--danger); color: white; }
|
|
681
|
+
|
|
682
|
+
/* Helpers */
|
|
683
|
+
.help-text {
|
|
684
|
+
font-size: 12px;
|
|
685
|
+
color: var(--text-muted);
|
|
686
|
+
margin-top: 4px;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
.divider {
|
|
690
|
+
border: none;
|
|
691
|
+
border-top: 1px solid var(--border);
|
|
692
|
+
margin: 16px 0;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
.hidden { display: none !important; }
|
|
696
|
+
</style>
|
|
697
|
+
</head>
|
|
698
|
+
<body>
|
|
699
|
+
<div class="layout">
|
|
700
|
+
<!-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 LEFT: FORM \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->
|
|
701
|
+
<div class="form-column">
|
|
702
|
+
<div class="logo">
|
|
703
|
+
<div class="logo-icon">G</div>
|
|
704
|
+
<div class="logo-text">Pi Governance <span>Setup Wizard</span></div>
|
|
705
|
+
</div>
|
|
706
|
+
|
|
707
|
+
<h1>Configure your governance policy</h1>
|
|
708
|
+
<p class="subtitle">
|
|
709
|
+
AI coding agents are powerful but need guardrails. This wizard generates a
|
|
710
|
+
<code>governance.yaml</code> and <code>governance-rules.yaml</code> to control
|
|
711
|
+
tool access, bash safety, data-loss prevention, human approvals, and audit logging.
|
|
712
|
+
</p>
|
|
713
|
+
|
|
714
|
+
<!-- \u2500\u2500 1. Roles \u2500\u2500 -->
|
|
715
|
+
<div class="section" id="sec-roles">
|
|
716
|
+
<div class="section-header" onclick="toggleSection('sec-roles')">
|
|
717
|
+
<div class="section-icon" style="background:var(--primary-subtle);color:var(--primary)">👥</div>
|
|
718
|
+
<h2>Roles</h2>
|
|
719
|
+
<span class="section-badge badge-required">Required</span>
|
|
720
|
+
<span class="section-chevron">▼</span>
|
|
721
|
+
</div>
|
|
722
|
+
<div class="section-body">
|
|
723
|
+
<p class="help-text" style="margin-bottom:12px">Select the roles your team needs. Each role defines tool access, execution mode, and approval rules.</p>
|
|
724
|
+
<div class="role-grid" id="role-grid"></div>
|
|
725
|
+
</div>
|
|
726
|
+
</div>
|
|
727
|
+
|
|
728
|
+
<!-- \u2500\u2500 2. DLP \u2500\u2500 -->
|
|
729
|
+
<div class="section" id="sec-dlp">
|
|
730
|
+
<div class="section-header" onclick="toggleSection('sec-dlp')">
|
|
731
|
+
<div class="section-icon" style="background:var(--warning-subtle);color:var(--warning)">🛡</div>
|
|
732
|
+
<h2>Data Loss Prevention</h2>
|
|
733
|
+
<span class="section-badge badge-optional">Optional</span>
|
|
734
|
+
<span class="section-chevron">▼</span>
|
|
735
|
+
</div>
|
|
736
|
+
<div class="section-body">
|
|
737
|
+
<div class="toggle-row">
|
|
738
|
+
<label class="toggle"><input type="checkbox" id="dlp-enabled" checked onchange="updatePreview()"><span class="toggle-slider"></span></label>
|
|
739
|
+
<span class="toggle-label">Enable DLP scanning</span>
|
|
740
|
+
</div>
|
|
741
|
+
|
|
742
|
+
<div id="dlp-options">
|
|
743
|
+
<div class="field">
|
|
744
|
+
<label>Default Mode</label>
|
|
745
|
+
<div class="chip-group" id="dlp-mode">
|
|
746
|
+
<span class="chip active" data-value="audit" onclick="selectChip(this)">Audit</span>
|
|
747
|
+
<span class="chip" data-value="mask" onclick="selectChip(this)">Mask</span>
|
|
748
|
+
<span class="chip" data-value="block" onclick="selectChip(this)">Block</span>
|
|
749
|
+
</div>
|
|
750
|
+
</div>
|
|
751
|
+
|
|
752
|
+
<div class="field-row">
|
|
753
|
+
<div class="field">
|
|
754
|
+
<label>On Input <span class="label-hint">(agent receives)</span></label>
|
|
755
|
+
<select id="dlp-on-input" onchange="updatePreview()">
|
|
756
|
+
<option value="">Use default mode</option>
|
|
757
|
+
<option value="audit">Audit</option>
|
|
758
|
+
<option value="mask">Mask</option>
|
|
759
|
+
<option value="block" selected>Block</option>
|
|
760
|
+
</select>
|
|
761
|
+
</div>
|
|
762
|
+
<div class="field">
|
|
763
|
+
<label>On Output <span class="label-hint">(agent produces)</span></label>
|
|
764
|
+
<select id="dlp-on-output" onchange="updatePreview()">
|
|
765
|
+
<option value="">Use default mode</option>
|
|
766
|
+
<option value="audit">Audit</option>
|
|
767
|
+
<option value="mask" selected>Mask</option>
|
|
768
|
+
<option value="block">Block</option>
|
|
769
|
+
</select>
|
|
770
|
+
</div>
|
|
771
|
+
</div>
|
|
772
|
+
|
|
773
|
+
<hr class="divider">
|
|
774
|
+
<label>Built-in Patterns</label>
|
|
775
|
+
<div class="toggle-row" style="margin-top:6px">
|
|
776
|
+
<label class="toggle"><input type="checkbox" id="dlp-secrets" checked onchange="updatePreview()"><span class="toggle-slider"></span></label>
|
|
777
|
+
<span class="toggle-label">Secrets <span class="label-hint">(API keys, tokens, passwords)</span></span>
|
|
778
|
+
</div>
|
|
779
|
+
<div class="toggle-row">
|
|
780
|
+
<label class="toggle"><input type="checkbox" id="dlp-pii" checked onchange="updatePreview()"><span class="toggle-slider"></span></label>
|
|
781
|
+
<span class="toggle-label">PII <span class="label-hint">(emails, phone numbers, SSNs)</span></span>
|
|
782
|
+
</div>
|
|
783
|
+
|
|
784
|
+
<hr class="divider">
|
|
785
|
+
<label>Masking Options</label>
|
|
786
|
+
<div class="field-row-3" style="margin-top:8px">
|
|
787
|
+
<div class="field">
|
|
788
|
+
<label>Strategy</label>
|
|
789
|
+
<select id="dlp-mask-strategy" onchange="updatePreview()">
|
|
790
|
+
<option value="partial" selected>Partial</option>
|
|
791
|
+
<option value="full">Full</option>
|
|
792
|
+
<option value="hash">Hash</option>
|
|
793
|
+
</select>
|
|
794
|
+
</div>
|
|
795
|
+
<div class="field">
|
|
796
|
+
<label>Show Chars</label>
|
|
797
|
+
<input type="number" id="dlp-mask-show" value="4" min="0" onchange="updatePreview()">
|
|
798
|
+
</div>
|
|
799
|
+
<div class="field">
|
|
800
|
+
<label>Placeholder</label>
|
|
801
|
+
<input type="text" id="dlp-mask-placeholder" value="***" onchange="updatePreview()">
|
|
802
|
+
</div>
|
|
803
|
+
</div>
|
|
804
|
+
|
|
805
|
+
<hr class="divider">
|
|
806
|
+
<label>Severity Threshold <span class="label-hint">(minimum severity to trigger DLP)</span></label>
|
|
807
|
+
<div class="chip-group" id="dlp-severity" style="margin-top:6px">
|
|
808
|
+
<span class="chip active" data-value="low" onclick="selectChip(this)">Low</span>
|
|
809
|
+
<span class="chip" data-value="medium" onclick="selectChip(this)">Medium</span>
|
|
810
|
+
<span class="chip" data-value="high" onclick="selectChip(this)">High</span>
|
|
811
|
+
<span class="chip" data-value="critical" onclick="selectChip(this)">Critical</span>
|
|
812
|
+
</div>
|
|
813
|
+
|
|
814
|
+
<hr class="divider">
|
|
815
|
+
<label>Custom Patterns</label>
|
|
816
|
+
<div class="pattern-list" id="dlp-custom-patterns"></div>
|
|
817
|
+
<button class="btn-add" onclick="addCustomPattern()">+ Add pattern</button>
|
|
818
|
+
|
|
819
|
+
<hr class="divider">
|
|
820
|
+
<label>Allowlist <span class="label-hint">(patterns to ignore)</span></label>
|
|
821
|
+
<div id="dlp-allowlist"></div>
|
|
822
|
+
<button class="btn-add" onclick="addAllowlistEntry()" style="margin-top:8px">+ Add entry</button>
|
|
823
|
+
</div>
|
|
824
|
+
</div>
|
|
825
|
+
</div>
|
|
826
|
+
|
|
827
|
+
<!-- \u2500\u2500 3. Bash Classification \u2500\u2500 -->
|
|
828
|
+
<div class="section" id="sec-bash">
|
|
829
|
+
<div class="section-header" onclick="toggleSection('sec-bash')">
|
|
830
|
+
<div class="section-icon" style="background:var(--danger-subtle);color:var(--danger)">💻</div>
|
|
831
|
+
<h2>Bash Classification</h2>
|
|
832
|
+
<span class="section-badge badge-optional">Optional</span>
|
|
833
|
+
<span class="section-chevron">▼</span>
|
|
834
|
+
</div>
|
|
835
|
+
<div class="section-body">
|
|
836
|
+
<p class="help-text" style="margin-bottom:12px">The built-in bash classifier categorizes commands by danger level. Adjust the threshold for auto-blocking.</p>
|
|
837
|
+
<div class="field">
|
|
838
|
+
<label>Auto-block Severity</label>
|
|
839
|
+
<p class="help-text">Commands at or above this severity are blocked without HITL.</p>
|
|
840
|
+
<div class="chip-group" id="bash-severity" style="margin-top:6px">
|
|
841
|
+
<span class="chip" data-value="low" onclick="selectChip(this)">Low</span>
|
|
842
|
+
<span class="chip" data-value="medium" onclick="selectChip(this)">Medium</span>
|
|
843
|
+
<span class="chip active" data-value="high" onclick="selectChip(this)">High</span>
|
|
844
|
+
<span class="chip" data-value="critical" onclick="selectChip(this)">Critical</span>
|
|
845
|
+
</div>
|
|
846
|
+
</div>
|
|
847
|
+
</div>
|
|
848
|
+
</div>
|
|
849
|
+
|
|
850
|
+
<!-- \u2500\u2500 4. HITL \u2500\u2500 -->
|
|
851
|
+
<div class="section" id="sec-hitl">
|
|
852
|
+
<div class="section-header" onclick="toggleSection('sec-hitl')">
|
|
853
|
+
<div class="section-icon" style="background:var(--success-subtle);color:var(--success)">🧑</div>
|
|
854
|
+
<h2>Human-in-the-Loop</h2>
|
|
855
|
+
<span class="section-badge badge-required">Required</span>
|
|
856
|
+
<span class="section-chevron">▼</span>
|
|
857
|
+
</div>
|
|
858
|
+
<div class="section-body">
|
|
859
|
+
<div class="field">
|
|
860
|
+
<label>Default Execution Mode</label>
|
|
861
|
+
<div class="chip-group" id="hitl-mode">
|
|
862
|
+
<span class="chip" data-value="autonomous" onclick="selectChip(this)">Autonomous</span>
|
|
863
|
+
<span class="chip active" data-value="supervised" onclick="selectChip(this)">Supervised</span>
|
|
864
|
+
<span class="chip" data-value="dry_run" onclick="selectChip(this)">Dry Run</span>
|
|
865
|
+
</div>
|
|
866
|
+
</div>
|
|
867
|
+
<div class="field-row">
|
|
868
|
+
<div class="field">
|
|
869
|
+
<label>Approval Channel</label>
|
|
870
|
+
<select id="hitl-channel" onchange="updatePreview()">
|
|
871
|
+
<option value="cli" selected>CLI (terminal prompt)</option>
|
|
872
|
+
<option value="webhook">Webhook</option>
|
|
873
|
+
</select>
|
|
874
|
+
</div>
|
|
875
|
+
<div class="field">
|
|
876
|
+
<label>Timeout <span class="label-hint">(seconds)</span></label>
|
|
877
|
+
<input type="number" id="hitl-timeout" value="300" min="10" max="3600" onchange="updatePreview()">
|
|
878
|
+
</div>
|
|
879
|
+
</div>
|
|
880
|
+
<div class="field hidden" id="hitl-webhook-field">
|
|
881
|
+
<label>Webhook URL</label>
|
|
882
|
+
<input type="text" id="hitl-webhook-url" placeholder="https://..." onchange="updatePreview()">
|
|
883
|
+
</div>
|
|
884
|
+
</div>
|
|
885
|
+
</div>
|
|
886
|
+
|
|
887
|
+
<!-- \u2500\u2500 5. Audit \u2500\u2500 -->
|
|
888
|
+
<div class="section" id="sec-audit">
|
|
889
|
+
<div class="section-header" onclick="toggleSection('sec-audit')">
|
|
890
|
+
<div class="section-icon" style="background:var(--primary-subtle);color:var(--primary)">📝</div>
|
|
891
|
+
<h2>Audit Logging</h2>
|
|
892
|
+
<span class="section-badge badge-required">Required</span>
|
|
893
|
+
<span class="section-chevron">▼</span>
|
|
894
|
+
</div>
|
|
895
|
+
<div class="section-body">
|
|
896
|
+
<p class="help-text" style="margin-bottom:12px">All governance events are logged to one or more sinks.</p>
|
|
897
|
+
<div id="audit-sinks"></div>
|
|
898
|
+
<button class="btn-add" onclick="addAuditSink()" style="margin-top:8px">+ Add sink</button>
|
|
899
|
+
</div>
|
|
900
|
+
</div>
|
|
901
|
+
|
|
902
|
+
<!-- \u2500\u2500 6. Auth \u2500\u2500 -->
|
|
903
|
+
<div class="section collapsed" id="sec-auth">
|
|
904
|
+
<div class="section-header" onclick="toggleSection('sec-auth')">
|
|
905
|
+
<div class="section-icon" style="background:var(--bg-surface-alt);color:var(--text-muted)">🔑</div>
|
|
906
|
+
<h2>Authentication</h2>
|
|
907
|
+
<span class="section-badge badge-optional">Optional</span>
|
|
908
|
+
<span class="section-chevron">▼</span>
|
|
909
|
+
</div>
|
|
910
|
+
<div class="section-body">
|
|
911
|
+
<div class="field">
|
|
912
|
+
<label>Provider</label>
|
|
913
|
+
<div class="chip-group" id="auth-provider">
|
|
914
|
+
<span class="chip active" data-value="env" onclick="selectChip(this)">Environment Vars</span>
|
|
915
|
+
<span class="chip" data-value="local" onclick="selectChip(this)">Local File</span>
|
|
916
|
+
<span class="chip" data-value="oidc" onclick="selectChip(this)">OIDC</span>
|
|
917
|
+
</div>
|
|
918
|
+
</div>
|
|
919
|
+
<div id="auth-env-fields">
|
|
920
|
+
<div class="field-row-3">
|
|
921
|
+
<div class="field">
|
|
922
|
+
<label>User Var</label>
|
|
923
|
+
<input type="text" id="auth-user-var" value="GRWND_USER" onchange="updatePreview()">
|
|
924
|
+
</div>
|
|
925
|
+
<div class="field">
|
|
926
|
+
<label>Role Var</label>
|
|
927
|
+
<input type="text" id="auth-role-var" value="GRWND_ROLE" onchange="updatePreview()">
|
|
928
|
+
</div>
|
|
929
|
+
<div class="field">
|
|
930
|
+
<label>Org Unit Var</label>
|
|
931
|
+
<input type="text" id="auth-org-unit-var" value="GRWND_ORG_UNIT" onchange="updatePreview()">
|
|
932
|
+
</div>
|
|
933
|
+
</div>
|
|
934
|
+
</div>
|
|
935
|
+
<div id="auth-local-fields" class="hidden">
|
|
936
|
+
<div class="field">
|
|
937
|
+
<label>Users File</label>
|
|
938
|
+
<input type="text" id="auth-users-file" value="./users.yaml" onchange="updatePreview()">
|
|
939
|
+
</div>
|
|
940
|
+
</div>
|
|
941
|
+
</div>
|
|
942
|
+
</div>
|
|
943
|
+
|
|
944
|
+
<div style="height:80px"></div>
|
|
945
|
+
</div>
|
|
946
|
+
|
|
947
|
+
<!-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 RIGHT: PREVIEW \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->
|
|
948
|
+
<div class="preview-panel">
|
|
949
|
+
<div class="preview-header">
|
|
950
|
+
<h3>Live Preview</h3>
|
|
951
|
+
</div>
|
|
952
|
+
<div class="preview-tabs">
|
|
953
|
+
<span class="preview-tab active" data-tab="governance" onclick="switchTab(this)">governance.yaml</span>
|
|
954
|
+
<span class="preview-tab" data-tab="rules" onclick="switchTab(this)">governance-rules.yaml</span>
|
|
955
|
+
</div>
|
|
956
|
+
<div class="preview-content">
|
|
957
|
+
<pre id="preview-yaml"></pre>
|
|
958
|
+
</div>
|
|
959
|
+
</div>
|
|
960
|
+
</div>
|
|
961
|
+
|
|
962
|
+
<!-- \u2500\u2500 Bottom bar \u2500\u2500 -->
|
|
963
|
+
<div class="bottom-bar">
|
|
964
|
+
<span class="save-status" id="save-status"></span>
|
|
965
|
+
<div style="display:flex;gap:10px">
|
|
966
|
+
<button class="btn-secondary" onclick="handleClose()">Cancel</button>
|
|
967
|
+
<button class="btn-primary" id="btn-save" onclick="handleSave()">Save Configuration</button>
|
|
968
|
+
</div>
|
|
969
|
+
</div>
|
|
970
|
+
|
|
971
|
+
<!-- Toast -->
|
|
972
|
+
<div class="toast" id="toast"></div>
|
|
973
|
+
|
|
974
|
+
<script>
|
|
975
|
+
// \u2500\u2500\u2500 State \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
976
|
+
const PRESET_ROLES = {
|
|
977
|
+
analyst: {
|
|
978
|
+
label: 'Analyst',
|
|
979
|
+
desc: 'Read-only access, every action requires approval.',
|
|
980
|
+
allowed_tools: ['read','grep','find','ls'],
|
|
981
|
+
blocked_tools: ['write','edit','bash'],
|
|
982
|
+
prompt_template: 'analyst',
|
|
983
|
+
execution_mode: 'supervised',
|
|
984
|
+
human_approval: { required_for: ['all'] },
|
|
985
|
+
token_budget_daily: 100000,
|
|
986
|
+
allowed_paths: ['{{project_path}}/**'],
|
|
987
|
+
blocked_paths: ['**/secrets/**', '**/.env*']
|
|
988
|
+
},
|
|
989
|
+
project_lead: {
|
|
990
|
+
label: 'Project Lead',
|
|
991
|
+
desc: 'Full tools, bash & write need human approval.',
|
|
992
|
+
allowed_tools: ['read','write','edit','bash','grep','find','ls'],
|
|
993
|
+
blocked_tools: [],
|
|
994
|
+
prompt_template: 'project-lead',
|
|
995
|
+
execution_mode: 'supervised',
|
|
996
|
+
human_approval: { required_for: ['bash','write'], auto_approve: ['read','edit','grep','find','ls'] },
|
|
997
|
+
token_budget_daily: 500000,
|
|
998
|
+
allowed_paths: ['{{project_path}}/**'],
|
|
999
|
+
blocked_paths: ['**/secrets/**', '**/.env*'],
|
|
1000
|
+
bash_overrides: { additional_blocked: ['sudo','ssh','curl.*\\\\|.*sh'] }
|
|
1001
|
+
},
|
|
1002
|
+
admin: {
|
|
1003
|
+
label: 'Admin',
|
|
1004
|
+
desc: 'Full autonomous access, no approvals, unlimited budget.',
|
|
1005
|
+
allowed_tools: ['all'],
|
|
1006
|
+
blocked_tools: [],
|
|
1007
|
+
prompt_template: 'admin',
|
|
1008
|
+
execution_mode: 'autonomous',
|
|
1009
|
+
human_approval: { required_for: [] },
|
|
1010
|
+
token_budget_daily: -1,
|
|
1011
|
+
allowed_paths: ['**'],
|
|
1012
|
+
blocked_paths: []
|
|
1013
|
+
},
|
|
1014
|
+
auditor: {
|
|
1015
|
+
label: 'Auditor',
|
|
1016
|
+
desc: 'Dry-run: all calls logged, nothing executed.',
|
|
1017
|
+
allowed_tools: ['read','grep','find','ls'],
|
|
1018
|
+
blocked_tools: ['write','edit','bash'],
|
|
1019
|
+
prompt_template: 'analyst',
|
|
1020
|
+
execution_mode: 'dry_run',
|
|
1021
|
+
human_approval: { required_for: ['all'] },
|
|
1022
|
+
token_budget_daily: 50000,
|
|
1023
|
+
allowed_paths: ['**'],
|
|
1024
|
+
blocked_paths: ['**/secrets/**']
|
|
1025
|
+
}
|
|
1026
|
+
};
|
|
1027
|
+
|
|
1028
|
+
let selectedRoles = { analyst: false, project_lead: true, admin: false, auditor: false };
|
|
1029
|
+
let customPatterns = [];
|
|
1030
|
+
let allowlistEntries = [];
|
|
1031
|
+
let auditSinks = [{ type: 'jsonl', path: '~/.pi/agent/audit.jsonl' }];
|
|
1032
|
+
let activePreviewTab = 'governance';
|
|
1033
|
+
|
|
1034
|
+
// \u2500\u2500\u2500 Init \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1035
|
+
document.addEventListener('DOMContentLoaded', async () => {
|
|
1036
|
+
renderRoles();
|
|
1037
|
+
renderAuditSinks();
|
|
1038
|
+
updatePreview();
|
|
1039
|
+
|
|
1040
|
+
try {
|
|
1041
|
+
const [configRes, defaultsRes] = await Promise.all([
|
|
1042
|
+
fetch('/api/config').catch(() => null),
|
|
1043
|
+
fetch('/api/defaults').catch(() => null)
|
|
1044
|
+
]);
|
|
1045
|
+
if (configRes && configRes.ok) {
|
|
1046
|
+
const cfg = await configRes.json();
|
|
1047
|
+
applyConfig(cfg);
|
|
1048
|
+
}
|
|
1049
|
+
if (defaultsRes && defaultsRes.ok) {
|
|
1050
|
+
const defs = await defaultsRes.json();
|
|
1051
|
+
if (!configRes || !configRes.ok) applyConfig(defs);
|
|
1052
|
+
}
|
|
1053
|
+
} catch (e) { /* use built-in defaults */ }
|
|
1054
|
+
|
|
1055
|
+
// watch for webhook channel
|
|
1056
|
+
document.getElementById('hitl-channel').addEventListener('change', (e) => {
|
|
1057
|
+
document.getElementById('hitl-webhook-field').classList.toggle('hidden', e.target.value !== 'webhook');
|
|
1058
|
+
updatePreview();
|
|
1059
|
+
});
|
|
1060
|
+
|
|
1061
|
+
// watch dlp toggle
|
|
1062
|
+
document.getElementById('dlp-enabled').addEventListener('change', (e) => {
|
|
1063
|
+
document.getElementById('dlp-options').classList.toggle('hidden', !e.target.checked);
|
|
1064
|
+
updatePreview();
|
|
1065
|
+
});
|
|
1066
|
+
});
|
|
1067
|
+
|
|
1068
|
+
// \u2500\u2500\u2500 Apply Config \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1069
|
+
function applyConfig(cfg) {
|
|
1070
|
+
if (!cfg) return;
|
|
1071
|
+
|
|
1072
|
+
// Auth
|
|
1073
|
+
if (cfg.auth) {
|
|
1074
|
+
setChipValue('auth-provider', cfg.auth.provider || 'env');
|
|
1075
|
+
if (cfg.auth.env) {
|
|
1076
|
+
if (cfg.auth.env.user_var) document.getElementById('auth-user-var').value = cfg.auth.env.user_var;
|
|
1077
|
+
if (cfg.auth.env.role_var) document.getElementById('auth-role-var').value = cfg.auth.env.role_var;
|
|
1078
|
+
if (cfg.auth.env.org_unit_var) document.getElementById('auth-org-unit-var').value = cfg.auth.env.org_unit_var;
|
|
1079
|
+
}
|
|
1080
|
+
if (cfg.auth.local && cfg.auth.local.users_file) {
|
|
1081
|
+
document.getElementById('auth-users-file').value = cfg.auth.local.users_file;
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
// HITL
|
|
1086
|
+
if (cfg.hitl) {
|
|
1087
|
+
if (cfg.hitl.default_mode) setChipValue('hitl-mode', cfg.hitl.default_mode);
|
|
1088
|
+
if (cfg.hitl.approval_channel) {
|
|
1089
|
+
document.getElementById('hitl-channel').value = cfg.hitl.approval_channel;
|
|
1090
|
+
document.getElementById('hitl-webhook-field').classList.toggle('hidden', cfg.hitl.approval_channel !== 'webhook');
|
|
1091
|
+
}
|
|
1092
|
+
if (cfg.hitl.timeout_seconds) document.getElementById('hitl-timeout').value = cfg.hitl.timeout_seconds;
|
|
1093
|
+
if (cfg.hitl.webhook && cfg.hitl.webhook.url) document.getElementById('hitl-webhook-url').value = cfg.hitl.webhook.url;
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
// DLP
|
|
1097
|
+
if (cfg.dlp) {
|
|
1098
|
+
document.getElementById('dlp-enabled').checked = cfg.dlp.enabled !== false;
|
|
1099
|
+
document.getElementById('dlp-options').classList.toggle('hidden', !cfg.dlp.enabled);
|
|
1100
|
+
if (cfg.dlp.mode) setChipValue('dlp-mode', cfg.dlp.mode);
|
|
1101
|
+
if (cfg.dlp.on_input) document.getElementById('dlp-on-input').value = cfg.dlp.on_input;
|
|
1102
|
+
if (cfg.dlp.on_output) document.getElementById('dlp-on-output').value = cfg.dlp.on_output;
|
|
1103
|
+
if (cfg.dlp.masking) {
|
|
1104
|
+
if (cfg.dlp.masking.strategy) document.getElementById('dlp-mask-strategy').value = cfg.dlp.masking.strategy;
|
|
1105
|
+
if (cfg.dlp.masking.show_chars != null) document.getElementById('dlp-mask-show').value = cfg.dlp.masking.show_chars;
|
|
1106
|
+
if (cfg.dlp.masking.placeholder) document.getElementById('dlp-mask-placeholder').value = cfg.dlp.masking.placeholder;
|
|
1107
|
+
}
|
|
1108
|
+
if (cfg.dlp.severity_threshold) setChipValue('dlp-severity', cfg.dlp.severity_threshold);
|
|
1109
|
+
if (cfg.dlp.built_in) {
|
|
1110
|
+
if (cfg.dlp.built_in.secrets != null) document.getElementById('dlp-secrets').checked = cfg.dlp.built_in.secrets;
|
|
1111
|
+
if (cfg.dlp.built_in.pii != null) document.getElementById('dlp-pii').checked = cfg.dlp.built_in.pii;
|
|
1112
|
+
}
|
|
1113
|
+
if (cfg.dlp.custom_patterns) {
|
|
1114
|
+
customPatterns = cfg.dlp.custom_patterns;
|
|
1115
|
+
renderCustomPatterns();
|
|
1116
|
+
}
|
|
1117
|
+
if (cfg.dlp.allowlist) {
|
|
1118
|
+
allowlistEntries = cfg.dlp.allowlist.map(e => e.pattern || e);
|
|
1119
|
+
renderAllowlist();
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
// Audit
|
|
1124
|
+
if (cfg.audit && cfg.audit.sinks) {
|
|
1125
|
+
auditSinks = cfg.audit.sinks;
|
|
1126
|
+
renderAuditSinks();
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
updatePreview();
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
// \u2500\u2500\u2500 Roles \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1133
|
+
function renderRoles() {
|
|
1134
|
+
const grid = document.getElementById('role-grid');
|
|
1135
|
+
grid.innerHTML = '';
|
|
1136
|
+
for (const [key, role] of Object.entries(PRESET_ROLES)) {
|
|
1137
|
+
const sel = selectedRoles[key];
|
|
1138
|
+
const card = document.createElement('div');
|
|
1139
|
+
card.className = 'role-card' + (sel ? ' selected' : '');
|
|
1140
|
+
card.dataset.role = key;
|
|
1141
|
+
card.onclick = () => { toggleRole(key); };
|
|
1142
|
+
card.innerHTML =
|
|
1143
|
+
'<div class="role-card-check">✓</div>' +
|
|
1144
|
+
'<div class="role-name">' + role.label + '</div>' +
|
|
1145
|
+
'<div class="role-desc">' + role.desc + '</div>' +
|
|
1146
|
+
'<div class="role-tags">' +
|
|
1147
|
+
'<span class="role-tag">' + role.execution_mode + '</span>' +
|
|
1148
|
+
'<span class="role-tag">' + (role.token_budget_daily === -1 ? 'unlimited' : role.token_budget_daily.toLocaleString()) + ' budget</span>' +
|
|
1149
|
+
'</div>' +
|
|
1150
|
+
'<div class="role-details">' +
|
|
1151
|
+
'<div class="field"><label>Allowed Tools</label>' +
|
|
1152
|
+
'<input type="text" value="' + role.allowed_tools.join(', ') + '" onchange="updateRoleField(\\'' + key + '\\', \\'allowed_tools\\', this.value)" onclick="event.stopPropagation()">' +
|
|
1153
|
+
'</div>' +
|
|
1154
|
+
'<div class="field"><label>Blocked Tools</label>' +
|
|
1155
|
+
'<input type="text" value="' + role.blocked_tools.join(', ') + '" onchange="updateRoleField(\\'' + key + '\\', \\'blocked_tools\\', this.value)" onclick="event.stopPropagation()">' +
|
|
1156
|
+
'</div>' +
|
|
1157
|
+
'<div class="field"><label>Execution Mode</label>' +
|
|
1158
|
+
'<select onchange="updateRoleField(\\'' + key + '\\', \\'execution_mode\\', this.value)" onclick="event.stopPropagation()">' +
|
|
1159
|
+
'<option value="supervised"' + (role.execution_mode === 'supervised' ? ' selected' : '') + '>Supervised</option>' +
|
|
1160
|
+
'<option value="autonomous"' + (role.execution_mode === 'autonomous' ? ' selected' : '') + '>Autonomous</option>' +
|
|
1161
|
+
'<option value="dry_run"' + (role.execution_mode === 'dry_run' ? ' selected' : '') + '>Dry Run</option>' +
|
|
1162
|
+
'</select>' +
|
|
1163
|
+
'</div>' +
|
|
1164
|
+
'<div class="field"><label>Token Budget Daily</label>' +
|
|
1165
|
+
'<input type="number" value="' + role.token_budget_daily + '" onchange="updateRoleField(\\'' + key + '\\', \\'token_budget_daily\\', parseInt(this.value))" onclick="event.stopPropagation()">' +
|
|
1166
|
+
'</div>' +
|
|
1167
|
+
'</div>';
|
|
1168
|
+
grid.appendChild(card);
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
function toggleRole(key) {
|
|
1173
|
+
selectedRoles[key] = !selectedRoles[key];
|
|
1174
|
+
renderRoles();
|
|
1175
|
+
updatePreview();
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
function updateRoleField(key, field, value) {
|
|
1179
|
+
if (field === 'allowed_tools' || field === 'blocked_tools') {
|
|
1180
|
+
PRESET_ROLES[key][field] = value.split(',').map(s => s.trim()).filter(Boolean);
|
|
1181
|
+
} else {
|
|
1182
|
+
PRESET_ROLES[key][field] = value;
|
|
1183
|
+
}
|
|
1184
|
+
updatePreview();
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
// \u2500\u2500\u2500 Section Toggle \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1188
|
+
function toggleSection(id) {
|
|
1189
|
+
document.getElementById(id).classList.toggle('collapsed');
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
// \u2500\u2500\u2500 Chip Groups \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1193
|
+
function selectChip(el) {
|
|
1194
|
+
const group = el.parentElement;
|
|
1195
|
+
group.querySelectorAll('.chip').forEach(c => c.classList.remove('active'));
|
|
1196
|
+
el.classList.add('active');
|
|
1197
|
+
|
|
1198
|
+
// Auth provider visibility
|
|
1199
|
+
if (group.id === 'auth-provider') {
|
|
1200
|
+
const val = el.dataset.value;
|
|
1201
|
+
document.getElementById('auth-env-fields').classList.toggle('hidden', val !== 'env');
|
|
1202
|
+
document.getElementById('auth-local-fields').classList.toggle('hidden', val !== 'local');
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
updatePreview();
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
function getChipValue(groupId) {
|
|
1209
|
+
const active = document.querySelector('#' + groupId + ' .chip.active');
|
|
1210
|
+
return active ? active.dataset.value : '';
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
function setChipValue(groupId, value) {
|
|
1214
|
+
const group = document.getElementById(groupId);
|
|
1215
|
+
if (!group) return;
|
|
1216
|
+
group.querySelectorAll('.chip').forEach(c => {
|
|
1217
|
+
c.classList.toggle('active', c.dataset.value === value);
|
|
1218
|
+
});
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
// \u2500\u2500\u2500 Custom Patterns \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1222
|
+
function addCustomPattern() {
|
|
1223
|
+
customPatterns.push({ name: '', pattern: '', severity: 'medium', action: 'audit' });
|
|
1224
|
+
renderCustomPatterns();
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
function renderCustomPatterns() {
|
|
1228
|
+
const container = document.getElementById('dlp-custom-patterns');
|
|
1229
|
+
container.innerHTML = '';
|
|
1230
|
+
customPatterns.forEach((p, i) => {
|
|
1231
|
+
const div = document.createElement('div');
|
|
1232
|
+
div.className = 'pattern-item';
|
|
1233
|
+
div.innerHTML =
|
|
1234
|
+
'<input type="text" placeholder="Name" value="' + esc(p.name) + '" onchange="customPatterns[' + i + '].name=this.value;updatePreview()">' +
|
|
1235
|
+
'<input type="text" placeholder="Regex pattern" value="' + esc(p.pattern) + '" onchange="customPatterns[' + i + '].pattern=this.value;updatePreview()">' +
|
|
1236
|
+
'<select onchange="customPatterns[' + i + '].severity=this.value;updatePreview()">' +
|
|
1237
|
+
'<option value="low"' + (p.severity==='low'?' selected':'') + '>Low</option>' +
|
|
1238
|
+
'<option value="medium"' + (p.severity==='medium'?' selected':'') + '>Medium</option>' +
|
|
1239
|
+
'<option value="high"' + (p.severity==='high'?' selected':'') + '>High</option>' +
|
|
1240
|
+
'<option value="critical"' + (p.severity==='critical'?' selected':'') + '>Critical</option>' +
|
|
1241
|
+
'</select>' +
|
|
1242
|
+
'<button class="btn-remove" onclick="customPatterns.splice(' + i + ',1);renderCustomPatterns();updatePreview()">×</button>';
|
|
1243
|
+
container.appendChild(div);
|
|
1244
|
+
});
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
// \u2500\u2500\u2500 Allowlist \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1248
|
+
function addAllowlistEntry() {
|
|
1249
|
+
allowlistEntries.push('');
|
|
1250
|
+
renderAllowlist();
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
function renderAllowlist() {
|
|
1254
|
+
const container = document.getElementById('dlp-allowlist');
|
|
1255
|
+
container.innerHTML = '';
|
|
1256
|
+
allowlistEntries.forEach((entry, i) => {
|
|
1257
|
+
const div = document.createElement('div');
|
|
1258
|
+
div.className = 'allowlist-item';
|
|
1259
|
+
div.innerHTML =
|
|
1260
|
+
'<input type="text" placeholder="Pattern to allow" value="' + esc(entry) + '" onchange="allowlistEntries[' + i + ']=this.value;updatePreview()">' +
|
|
1261
|
+
'<button class="btn-remove" onclick="allowlistEntries.splice(' + i + ',1);renderAllowlist();updatePreview()">×</button>';
|
|
1262
|
+
container.appendChild(div);
|
|
1263
|
+
});
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
// \u2500\u2500\u2500 Audit Sinks \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1267
|
+
function addAuditSink() {
|
|
1268
|
+
auditSinks.push({ type: 'jsonl', path: '' });
|
|
1269
|
+
renderAuditSinks();
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
function renderAuditSinks() {
|
|
1273
|
+
const container = document.getElementById('audit-sinks');
|
|
1274
|
+
container.innerHTML = '';
|
|
1275
|
+
auditSinks.forEach((sink, i) => {
|
|
1276
|
+
const div = document.createElement('div');
|
|
1277
|
+
div.className = 'sink-item';
|
|
1278
|
+
let inner = '<button class="btn-remove" onclick="auditSinks.splice(' + i + ',1);renderAuditSinks();updatePreview()">×</button>';
|
|
1279
|
+
inner += '<div class="field"><label>Sink Type</label>' +
|
|
1280
|
+
'<select onchange="auditSinks[' + i + '].type=this.value;renderAuditSinks();updatePreview()">' +
|
|
1281
|
+
'<option value="jsonl"' + (sink.type==='jsonl'?' selected':'') + '>JSONL File</option>' +
|
|
1282
|
+
'<option value="webhook"' + (sink.type==='webhook'?' selected':'') + '>Webhook</option>' +
|
|
1283
|
+
'<option value="postgres"' + (sink.type==='postgres'?' selected':'') + '>PostgreSQL</option>' +
|
|
1284
|
+
'</select></div>';
|
|
1285
|
+
if (sink.type === 'jsonl') {
|
|
1286
|
+
inner += '<div class="field"><label>File Path</label>' +
|
|
1287
|
+
'<input type="text" value="' + esc(sink.path || '') + '" placeholder="~/.pi/agent/audit.jsonl" ' +
|
|
1288
|
+
'onchange="auditSinks[' + i + '].path=this.value;updatePreview()"></div>';
|
|
1289
|
+
} else if (sink.type === 'webhook') {
|
|
1290
|
+
inner += '<div class="field"><label>Webhook URL</label>' +
|
|
1291
|
+
'<input type="text" value="' + esc(sink.url || '') + '" placeholder="https://..." ' +
|
|
1292
|
+
'onchange="auditSinks[' + i + '].url=this.value;updatePreview()"></div>';
|
|
1293
|
+
} else if (sink.type === 'postgres') {
|
|
1294
|
+
inner += '<div class="field"><label>Connection String</label>' +
|
|
1295
|
+
'<input type="text" value="' + esc(sink.connection || '') + '" placeholder="postgresql://..." ' +
|
|
1296
|
+
'onchange="auditSinks[' + i + '].connection=this.value;updatePreview()"></div>';
|
|
1297
|
+
}
|
|
1298
|
+
div.innerHTML = inner;
|
|
1299
|
+
container.appendChild(div);
|
|
1300
|
+
});
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
// \u2500\u2500\u2500 Preview Tabs \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1304
|
+
function switchTab(el) {
|
|
1305
|
+
document.querySelectorAll('.preview-tab').forEach(t => t.classList.remove('active'));
|
|
1306
|
+
el.classList.add('active');
|
|
1307
|
+
activePreviewTab = el.dataset.tab;
|
|
1308
|
+
updatePreview();
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
// \u2500\u2500\u2500 YAML Generator \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1312
|
+
function toYaml(obj, indent) {
|
|
1313
|
+
indent = indent || 0;
|
|
1314
|
+
const pad = ' '.repeat(indent);
|
|
1315
|
+
let out = '';
|
|
1316
|
+
|
|
1317
|
+
if (Array.isArray(obj)) {
|
|
1318
|
+
if (obj.length === 0) return ' []\\n';
|
|
1319
|
+
for (const item of obj) {
|
|
1320
|
+
if (typeof item === 'object' && item !== null && !Array.isArray(item)) {
|
|
1321
|
+
const keys = Object.keys(item);
|
|
1322
|
+
if (keys.length > 0) {
|
|
1323
|
+
out += pad + '- ' + keys[0] + ': ' + formatScalar(item[keys[0]]) + '\\n';
|
|
1324
|
+
for (let k = 1; k < keys.length; k++) {
|
|
1325
|
+
const val = item[keys[k]];
|
|
1326
|
+
if (typeof val === 'object' && val !== null) {
|
|
1327
|
+
out += pad + ' ' + keys[k] + ':\\n' + toYaml(val, indent + 2);
|
|
1328
|
+
} else {
|
|
1329
|
+
out += pad + ' ' + keys[k] + ': ' + formatScalar(val) + '\\n';
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
} else {
|
|
1334
|
+
out += pad + '- ' + formatScalar(item) + '\\n';
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
return out;
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
if (typeof obj === 'object' && obj !== null) {
|
|
1341
|
+
for (const [key, val] of Object.entries(obj)) {
|
|
1342
|
+
if (val === undefined || val === null) continue;
|
|
1343
|
+
if (typeof val === 'object') {
|
|
1344
|
+
const yamlVal = toYaml(val, indent + 1);
|
|
1345
|
+
if (Array.isArray(val) && val.length === 0) {
|
|
1346
|
+
out += pad + key + ': []\\n';
|
|
1347
|
+
} else {
|
|
1348
|
+
out += pad + key + ':\\n' + yamlVal;
|
|
1349
|
+
}
|
|
1350
|
+
} else {
|
|
1351
|
+
out += pad + key + ': ' + formatScalar(val) + '\\n';
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
return out;
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
return pad + formatScalar(obj) + '\\n';
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
function formatScalar(val) {
|
|
1361
|
+
if (typeof val === 'boolean') return val ? 'true' : 'false';
|
|
1362
|
+
if (typeof val === 'number') return String(val);
|
|
1363
|
+
if (typeof val === 'string') {
|
|
1364
|
+
if (val === '') return "''";
|
|
1365
|
+
if (val === 'true' || val === 'false' || !isNaN(val)) return "'" + val + "'";
|
|
1366
|
+
if (/[:#{}\\[\\],&*?|\\->!%@]/.test(val) || val.includes('\\\\')) return "'" + val.replace(/'/g, "''") + "'";
|
|
1367
|
+
return val;
|
|
1368
|
+
}
|
|
1369
|
+
return String(val);
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
// \u2500\u2500\u2500 Build Config Objects \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1373
|
+
function buildGovernanceConfig() {
|
|
1374
|
+
const cfg = {};
|
|
1375
|
+
|
|
1376
|
+
// Auth
|
|
1377
|
+
const authProvider = getChipValue('auth-provider');
|
|
1378
|
+
cfg.auth = { provider: authProvider };
|
|
1379
|
+
if (authProvider === 'env') {
|
|
1380
|
+
cfg.auth.env = {
|
|
1381
|
+
user_var: document.getElementById('auth-user-var').value || 'GRWND_USER',
|
|
1382
|
+
role_var: document.getElementById('auth-role-var').value || 'GRWND_ROLE',
|
|
1383
|
+
org_unit_var: document.getElementById('auth-org-unit-var').value || 'GRWND_ORG_UNIT'
|
|
1384
|
+
};
|
|
1385
|
+
} else if (authProvider === 'local') {
|
|
1386
|
+
cfg.auth.local = {
|
|
1387
|
+
users_file: document.getElementById('auth-users-file').value || './users.yaml'
|
|
1388
|
+
};
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
// Policy
|
|
1392
|
+
cfg.policy = {
|
|
1393
|
+
engine: 'yaml',
|
|
1394
|
+
yaml: { rules_file: './governance-rules.yaml' }
|
|
1395
|
+
};
|
|
1396
|
+
|
|
1397
|
+
// HITL
|
|
1398
|
+
cfg.hitl = {
|
|
1399
|
+
default_mode: getChipValue('hitl-mode') || 'supervised',
|
|
1400
|
+
approval_channel: document.getElementById('hitl-channel').value,
|
|
1401
|
+
timeout_seconds: parseInt(document.getElementById('hitl-timeout').value) || 300
|
|
1402
|
+
};
|
|
1403
|
+
if (cfg.hitl.approval_channel === 'webhook') {
|
|
1404
|
+
const url = document.getElementById('hitl-webhook-url').value;
|
|
1405
|
+
if (url) cfg.hitl.webhook = { url: url };
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
// Audit
|
|
1409
|
+
cfg.audit = { sinks: auditSinks.filter(s => {
|
|
1410
|
+
if (s.type === 'jsonl') return s.path;
|
|
1411
|
+
if (s.type === 'webhook') return s.url;
|
|
1412
|
+
if (s.type === 'postgres') return s.connection;
|
|
1413
|
+
return false;
|
|
1414
|
+
}).map(s => {
|
|
1415
|
+
if (s.type === 'jsonl') return { type: 'jsonl', path: s.path };
|
|
1416
|
+
if (s.type === 'webhook') return { type: 'webhook', url: s.url };
|
|
1417
|
+
if (s.type === 'postgres') return { type: 'postgres', connection: s.connection };
|
|
1418
|
+
return s;
|
|
1419
|
+
})};
|
|
1420
|
+
|
|
1421
|
+
// DLP
|
|
1422
|
+
if (document.getElementById('dlp-enabled').checked) {
|
|
1423
|
+
cfg.dlp = {
|
|
1424
|
+
enabled: true,
|
|
1425
|
+
mode: getChipValue('dlp-mode') || 'audit'
|
|
1426
|
+
};
|
|
1427
|
+
const onInput = document.getElementById('dlp-on-input').value;
|
|
1428
|
+
const onOutput = document.getElementById('dlp-on-output').value;
|
|
1429
|
+
if (onInput) cfg.dlp.on_input = onInput;
|
|
1430
|
+
if (onOutput) cfg.dlp.on_output = onOutput;
|
|
1431
|
+
cfg.dlp.masking = {
|
|
1432
|
+
strategy: document.getElementById('dlp-mask-strategy').value,
|
|
1433
|
+
show_chars: parseInt(document.getElementById('dlp-mask-show').value) || 4,
|
|
1434
|
+
placeholder: document.getElementById('dlp-mask-placeholder').value || '***'
|
|
1435
|
+
};
|
|
1436
|
+
cfg.dlp.severity_threshold = getChipValue('dlp-severity') || 'low';
|
|
1437
|
+
cfg.dlp.built_in = {
|
|
1438
|
+
secrets: document.getElementById('dlp-secrets').checked,
|
|
1439
|
+
pii: document.getElementById('dlp-pii').checked
|
|
1440
|
+
};
|
|
1441
|
+
const patterns = customPatterns.filter(p => p.name && p.pattern);
|
|
1442
|
+
if (patterns.length > 0) cfg.dlp.custom_patterns = patterns;
|
|
1443
|
+
const al = allowlistEntries.filter(Boolean);
|
|
1444
|
+
if (al.length > 0) cfg.dlp.allowlist = al.map(p => ({ pattern: p }));
|
|
1445
|
+
} else {
|
|
1446
|
+
cfg.dlp = { enabled: false };
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
return cfg;
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
function buildRulesConfig() {
|
|
1453
|
+
const roles = {};
|
|
1454
|
+
for (const [key, sel] of Object.entries(selectedRoles)) {
|
|
1455
|
+
if (!sel) continue;
|
|
1456
|
+
const r = PRESET_ROLES[key];
|
|
1457
|
+
const role = {
|
|
1458
|
+
allowed_tools: r.allowed_tools,
|
|
1459
|
+
blocked_tools: r.blocked_tools,
|
|
1460
|
+
prompt_template: r.prompt_template,
|
|
1461
|
+
execution_mode: r.execution_mode,
|
|
1462
|
+
human_approval: r.human_approval,
|
|
1463
|
+
token_budget_daily: r.token_budget_daily,
|
|
1464
|
+
allowed_paths: r.allowed_paths,
|
|
1465
|
+
blocked_paths: r.blocked_paths
|
|
1466
|
+
};
|
|
1467
|
+
if (r.bash_overrides) role.bash_overrides = r.bash_overrides;
|
|
1468
|
+
roles[key] = role;
|
|
1469
|
+
}
|
|
1470
|
+
return { roles: roles };
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
// \u2500\u2500\u2500 Update Preview \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1474
|
+
function updatePreview() {
|
|
1475
|
+
const el = document.getElementById('preview-yaml');
|
|
1476
|
+
if (activePreviewTab === 'governance') {
|
|
1477
|
+
const cfg = buildGovernanceConfig();
|
|
1478
|
+
el.textContent = '# governance.yaml\\n# Generated by Pi Governance Setup Wizard\\n\\n' + toYaml(cfg);
|
|
1479
|
+
} else {
|
|
1480
|
+
const rules = buildRulesConfig();
|
|
1481
|
+
el.textContent = '# governance-rules.yaml\\n# Generated by Pi Governance Setup Wizard\\n\\n' + toYaml(rules);
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
// \u2500\u2500\u2500 Save \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1486
|
+
async function handleSave() {
|
|
1487
|
+
const btn = document.getElementById('btn-save');
|
|
1488
|
+
const status = document.getElementById('save-status');
|
|
1489
|
+
btn.disabled = true;
|
|
1490
|
+
status.textContent = 'Saving...';
|
|
1491
|
+
status.className = 'save-status';
|
|
1492
|
+
|
|
1493
|
+
try {
|
|
1494
|
+
const payload = {
|
|
1495
|
+
governance: buildGovernanceConfig(),
|
|
1496
|
+
rules: buildRulesConfig()
|
|
1497
|
+
};
|
|
1498
|
+
const res = await fetch('/api/save', {
|
|
1499
|
+
method: 'POST',
|
|
1500
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1501
|
+
body: JSON.stringify(payload)
|
|
1502
|
+
});
|
|
1503
|
+
if (!res.ok) {
|
|
1504
|
+
const err = await res.text();
|
|
1505
|
+
throw new Error(err || 'Server error');
|
|
1506
|
+
}
|
|
1507
|
+
const result = await res.json();
|
|
1508
|
+
showToast('Configuration saved!', 'success');
|
|
1509
|
+
status.textContent = 'Saved: ' + (result.files || []).join(', ');
|
|
1510
|
+
status.className = 'save-status success';
|
|
1511
|
+
} catch (e) {
|
|
1512
|
+
showToast('Failed to save: ' + e.message, 'error');
|
|
1513
|
+
status.textContent = 'Error: ' + e.message;
|
|
1514
|
+
status.className = 'save-status error';
|
|
1515
|
+
} finally {
|
|
1516
|
+
btn.disabled = false;
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
async function handleClose() {
|
|
1521
|
+
try { await fetch('/api/close', { method: 'POST' }); } catch (e) { /* ignore */ }
|
|
1522
|
+
window.close();
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
// \u2500\u2500\u2500 Toast \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1526
|
+
function showToast(msg, type) {
|
|
1527
|
+
const toast = document.getElementById('toast');
|
|
1528
|
+
toast.textContent = msg;
|
|
1529
|
+
toast.className = 'toast ' + type + ' visible';
|
|
1530
|
+
setTimeout(() => { toast.classList.remove('visible'); }, 3000);
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
// \u2500\u2500\u2500 Util \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1534
|
+
function esc(s) {
|
|
1535
|
+
return String(s).replace(/&/g,'&').replace(/"/g,'"').replace(/</g,'<').replace(/>/g,'>');
|
|
1536
|
+
}
|
|
1537
|
+
</script>
|
|
1538
|
+
</body>
|
|
1539
|
+
</html>`;
|
|
1540
|
+
}
|
|
1541
|
+
});
|
|
1542
|
+
|
|
1543
|
+
// src/lib/wizard/server.ts
|
|
1544
|
+
function setCorsHeaders(res) {
|
|
1545
|
+
res.setHeader("Access-Control-Allow-Origin", "http://localhost");
|
|
1546
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
1547
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
1548
|
+
}
|
|
1549
|
+
function sendJson(res, status, data) {
|
|
1550
|
+
res.writeHead(status, { "Content-Type": "application/json" });
|
|
1551
|
+
res.end(JSON.stringify(data));
|
|
1552
|
+
}
|
|
1553
|
+
function readBody(req) {
|
|
1554
|
+
return new Promise((resolve, reject) => {
|
|
1555
|
+
const chunks = [];
|
|
1556
|
+
req.on("data", (chunk) => chunks.push(chunk));
|
|
1557
|
+
req.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
|
|
1558
|
+
req.on("error", reject);
|
|
1559
|
+
});
|
|
1560
|
+
}
|
|
1561
|
+
function startWizardServer(options) {
|
|
1562
|
+
return new Promise((resolve, reject) => {
|
|
1563
|
+
let shutdownTimer;
|
|
1564
|
+
const server = (0, import_node_http.createServer)((req, res) => {
|
|
1565
|
+
setCorsHeaders(res);
|
|
1566
|
+
if (req.method === "OPTIONS") {
|
|
1567
|
+
res.writeHead(204);
|
|
1568
|
+
res.end();
|
|
1569
|
+
return;
|
|
1570
|
+
}
|
|
1571
|
+
const url = req.url ?? "/";
|
|
1572
|
+
try {
|
|
1573
|
+
if (req.method === "GET" && url === "/") {
|
|
1574
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
1575
|
+
res.end(WIZARD_HTML);
|
|
1576
|
+
return;
|
|
1577
|
+
}
|
|
1578
|
+
if (req.method === "GET" && url === "/api/config") {
|
|
1579
|
+
sendJson(res, 200, options.existingConfig ?? DEFAULTS);
|
|
1580
|
+
return;
|
|
1581
|
+
}
|
|
1582
|
+
if (req.method === "GET" && url === "/api/defaults") {
|
|
1583
|
+
sendJson(res, 200, { defaults: DEFAULTS, roles: BUILTIN_ROLES });
|
|
1584
|
+
return;
|
|
1585
|
+
}
|
|
1586
|
+
if (req.method === "POST" && url === "/api/save") {
|
|
1587
|
+
readBody(req).then((body) => {
|
|
1588
|
+
const parsed = JSON.parse(body);
|
|
1589
|
+
if (typeof parsed !== "object" || parsed === null || !("governance" in parsed)) {
|
|
1590
|
+
sendJson(res, 400, { error: 'Request body must include "governance" property' });
|
|
1591
|
+
return;
|
|
1592
|
+
}
|
|
1593
|
+
const payload = parsed;
|
|
1594
|
+
const governanceYaml = (0, import_yaml5.stringify)(payload.governance);
|
|
1595
|
+
const piDir = (0, import_node_path2.join)(options.workingDirectory, ".pi");
|
|
1596
|
+
const governancePath = (0, import_node_path2.join)(piDir, "governance.yaml");
|
|
1597
|
+
(0, import_node_fs2.mkdirSync)(piDir, { recursive: true });
|
|
1598
|
+
(0, import_node_fs2.writeFileSync)(governancePath, governanceYaml, "utf-8");
|
|
1599
|
+
const files = [
|
|
1600
|
+
{ path: governancePath, content: governanceYaml }
|
|
1601
|
+
];
|
|
1602
|
+
if (payload.rules !== void 0) {
|
|
1603
|
+
const rulesYaml = (0, import_yaml5.stringify)(payload.rules);
|
|
1604
|
+
const rulesPath = (0, import_node_path2.join)(options.workingDirectory, "governance-rules.yaml");
|
|
1605
|
+
(0, import_node_fs2.writeFileSync)(rulesPath, rulesYaml, "utf-8");
|
|
1606
|
+
files.push({ path: rulesPath, content: rulesYaml });
|
|
1607
|
+
}
|
|
1608
|
+
sendJson(res, 200, { ok: true, files: files.map((f) => f.path) });
|
|
1609
|
+
options.onComplete(files);
|
|
1610
|
+
}).catch((err) => {
|
|
1611
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1612
|
+
sendJson(res, 400, { error: `Invalid request body: ${message}` });
|
|
1613
|
+
});
|
|
1614
|
+
return;
|
|
1615
|
+
}
|
|
1616
|
+
if (req.method === "POST" && url === "/api/close") {
|
|
1617
|
+
sendJson(res, 200, { ok: true });
|
|
1618
|
+
setTimeout(() => {
|
|
1619
|
+
closeServer();
|
|
1620
|
+
}, 100);
|
|
1621
|
+
return;
|
|
1622
|
+
}
|
|
1623
|
+
sendJson(res, 404, { error: "Not found" });
|
|
1624
|
+
} catch (err) {
|
|
1625
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
1626
|
+
options.onError(error);
|
|
1627
|
+
sendJson(res, 500, { error: "Internal server error" });
|
|
1628
|
+
}
|
|
1629
|
+
});
|
|
1630
|
+
function closeServer() {
|
|
1631
|
+
if (shutdownTimer !== void 0) {
|
|
1632
|
+
clearTimeout(shutdownTimer);
|
|
1633
|
+
shutdownTimer = void 0;
|
|
1634
|
+
}
|
|
1635
|
+
server.close();
|
|
1636
|
+
}
|
|
1637
|
+
server.on("error", (err) => {
|
|
1638
|
+
options.onError(err);
|
|
1639
|
+
reject(err);
|
|
1640
|
+
});
|
|
1641
|
+
server.listen(0, () => {
|
|
1642
|
+
const addr = server.address();
|
|
1643
|
+
if (addr === null || typeof addr === "string") {
|
|
1644
|
+
const err = new Error("Failed to get server address");
|
|
1645
|
+
options.onError(err);
|
|
1646
|
+
reject(err);
|
|
1647
|
+
return;
|
|
1648
|
+
}
|
|
1649
|
+
shutdownTimer = setTimeout(() => {
|
|
1650
|
+
closeServer();
|
|
1651
|
+
}, AUTO_SHUTDOWN_MS);
|
|
1652
|
+
resolve({ port: addr.port, close: closeServer });
|
|
1653
|
+
});
|
|
1654
|
+
});
|
|
1655
|
+
}
|
|
1656
|
+
var import_node_http, import_node_fs2, import_node_path2, import_yaml5, AUTO_SHUTDOWN_MS, BUILTIN_ROLES;
|
|
1657
|
+
var init_server = __esm({
|
|
1658
|
+
"src/lib/wizard/server.ts"() {
|
|
1659
|
+
"use strict";
|
|
1660
|
+
import_node_http = require("http");
|
|
1661
|
+
import_node_fs2 = require("fs");
|
|
1662
|
+
import_node_path2 = require("path");
|
|
1663
|
+
import_yaml5 = require("yaml");
|
|
1664
|
+
init_defaults();
|
|
1665
|
+
init_html();
|
|
1666
|
+
AUTO_SHUTDOWN_MS = 10 * 60 * 1e3;
|
|
1667
|
+
BUILTIN_ROLES = {
|
|
1668
|
+
analyst: {
|
|
1669
|
+
allowed_tools: ["read", "grep", "find", "ls"],
|
|
1670
|
+
blocked_tools: ["bash", "write", "edit"],
|
|
1671
|
+
hitl_mode: "dry_run"
|
|
1672
|
+
},
|
|
1673
|
+
project_lead: {
|
|
1674
|
+
allowed_tools: ["read", "write", "edit", "grep", "find", "ls", "bash"],
|
|
1675
|
+
blocked_tools: [],
|
|
1676
|
+
hitl_mode: "supervised"
|
|
1677
|
+
},
|
|
1678
|
+
admin: {
|
|
1679
|
+
allowed_tools: ["read", "write", "edit", "grep", "find", "ls", "bash"],
|
|
1680
|
+
blocked_tools: [],
|
|
1681
|
+
hitl_mode: "autonomous"
|
|
1682
|
+
},
|
|
1683
|
+
auditor: {
|
|
1684
|
+
allowed_tools: ["read", "grep", "find", "ls"],
|
|
1685
|
+
blocked_tools: ["bash", "write", "edit"],
|
|
1686
|
+
hitl_mode: "dry_run"
|
|
1687
|
+
}
|
|
1688
|
+
};
|
|
1689
|
+
}
|
|
1690
|
+
});
|
|
1691
|
+
|
|
1692
|
+
// src/lib/wizard/index.ts
|
|
1693
|
+
var wizard_exports = {};
|
|
1694
|
+
__export(wizard_exports, {
|
|
1695
|
+
startWizardServer: () => startWizardServer
|
|
1696
|
+
});
|
|
1697
|
+
var init_wizard = __esm({
|
|
1698
|
+
"src/lib/wizard/index.ts"() {
|
|
1699
|
+
"use strict";
|
|
1700
|
+
init_server();
|
|
1701
|
+
}
|
|
1702
|
+
});
|
|
1703
|
+
|
|
20
1704
|
// src/extensions/index.ts
|
|
21
1705
|
var extensions_exports = {};
|
|
22
1706
|
__export(extensions_exports, {
|
|
23
1707
|
default: () => extensions_default
|
|
24
1708
|
});
|
|
25
1709
|
module.exports = __toCommonJS(extensions_exports);
|
|
26
|
-
var
|
|
1710
|
+
var import_node_fs3 = require("fs");
|
|
27
1711
|
|
|
28
1712
|
// src/lib/config/loader.ts
|
|
29
1713
|
var import_fs = require("fs");
|
|
@@ -178,53 +1862,8 @@ var GovernanceConfigSchema = import_typebox.Type.Object({
|
|
|
178
1862
|
org_units: import_typebox.Type.Optional(import_typebox.Type.Record(import_typebox.Type.String(), OrgUnitOverride))
|
|
179
1863
|
});
|
|
180
1864
|
|
|
181
|
-
// src/lib/config/defaults.ts
|
|
182
|
-
var DEFAULTS = {
|
|
183
|
-
auth: {
|
|
184
|
-
provider: "env",
|
|
185
|
-
env: {
|
|
186
|
-
user_var: "GRWND_USER",
|
|
187
|
-
role_var: "GRWND_ROLE",
|
|
188
|
-
org_unit_var: "GRWND_ORG_UNIT"
|
|
189
|
-
}
|
|
190
|
-
},
|
|
191
|
-
policy: {
|
|
192
|
-
engine: "yaml",
|
|
193
|
-
yaml: {
|
|
194
|
-
rules_file: "./governance-rules.yaml"
|
|
195
|
-
}
|
|
196
|
-
},
|
|
197
|
-
templates: {
|
|
198
|
-
directory: "./templates/",
|
|
199
|
-
default: "project-lead"
|
|
200
|
-
},
|
|
201
|
-
hitl: {
|
|
202
|
-
default_mode: "supervised",
|
|
203
|
-
approval_channel: "cli",
|
|
204
|
-
timeout_seconds: 300
|
|
205
|
-
},
|
|
206
|
-
audit: {
|
|
207
|
-
sinks: [{ type: "jsonl", path: "~/.pi/agent/audit.jsonl" }]
|
|
208
|
-
},
|
|
209
|
-
dlp: {
|
|
210
|
-
enabled: true,
|
|
211
|
-
mode: "audit",
|
|
212
|
-
on_input: "block",
|
|
213
|
-
on_output: "mask",
|
|
214
|
-
masking: {
|
|
215
|
-
strategy: "partial",
|
|
216
|
-
show_chars: 4,
|
|
217
|
-
placeholder: "***"
|
|
218
|
-
},
|
|
219
|
-
severity_threshold: "low",
|
|
220
|
-
built_in: {
|
|
221
|
-
secrets: true,
|
|
222
|
-
pii: true
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
};
|
|
226
|
-
|
|
227
1865
|
// src/lib/config/loader.ts
|
|
1866
|
+
init_defaults();
|
|
228
1867
|
function getConfigPaths() {
|
|
229
1868
|
return [
|
|
230
1869
|
process.env["GRWND_GOVERNANCE_CONFIG"],
|
|
@@ -1354,7 +2993,7 @@ var piGovernance = (pi) => {
|
|
|
1354
2993
|
const chain = createIdentityChain(config.auth);
|
|
1355
2994
|
identity = await chain.resolve();
|
|
1356
2995
|
const rulesFile = config.policy?.yaml?.rules_file ?? "./governance-rules.yaml";
|
|
1357
|
-
if ((0,
|
|
2996
|
+
if ((0, import_node_fs3.existsSync)(rulesFile)) {
|
|
1358
2997
|
policyEngine = new YamlPolicyEngine(rulesFile);
|
|
1359
2998
|
} else {
|
|
1360
2999
|
policyEngine = new YamlPolicyEngine({
|
|
@@ -1426,7 +3065,7 @@ var piGovernance = (pi) => {
|
|
|
1426
3065
|
(newConfig) => {
|
|
1427
3066
|
config = newConfig;
|
|
1428
3067
|
const newRulesFile = newConfig.policy?.yaml?.rules_file ?? "./governance-rules.yaml";
|
|
1429
|
-
if ((0,
|
|
3068
|
+
if ((0, import_node_fs3.existsSync)(newRulesFile)) {
|
|
1430
3069
|
policyEngine = new YamlPolicyEngine(newRulesFile);
|
|
1431
3070
|
}
|
|
1432
3071
|
const newOverrides = policyEngine.getBashOverrides(identity.role);
|
|
@@ -1763,8 +3402,29 @@ var piGovernance = (pi) => {
|
|
|
1763
3402
|
...[...summary.entries()].map(([k, v]) => ` ${k}: ${v}`)
|
|
1764
3403
|
];
|
|
1765
3404
|
ctx.ui.notify(lines.join("\n"), "info");
|
|
3405
|
+
} else if (subcommand === "init") {
|
|
3406
|
+
const { startWizardServer: startWizardServer2 } = await Promise.resolve().then(() => (init_wizard(), wizard_exports));
|
|
3407
|
+
ctx.ui.notify("Starting governance configuration wizard...", "info");
|
|
3408
|
+
const { port, close } = await startWizardServer2({
|
|
3409
|
+
workingDirectory: ctx.workingDirectory,
|
|
3410
|
+
existingConfig: config,
|
|
3411
|
+
onComplete: (files) => {
|
|
3412
|
+
const names = files.map((f) => f.path).join(", ");
|
|
3413
|
+
ctx.ui.notify(`Configuration saved: ${names}`, "info");
|
|
3414
|
+
close();
|
|
3415
|
+
},
|
|
3416
|
+
onError: (err) => {
|
|
3417
|
+
ctx.ui.notify(`Wizard error: ${err.message}`, "error");
|
|
3418
|
+
close();
|
|
3419
|
+
}
|
|
3420
|
+
});
|
|
3421
|
+
const url = `http://localhost:${port}`;
|
|
3422
|
+
const { exec } = await import("child_process");
|
|
3423
|
+
const openCmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
3424
|
+
exec(`${openCmd} ${url}`);
|
|
3425
|
+
ctx.ui.notify(`Wizard running at ${url}`, "info");
|
|
1766
3426
|
} else {
|
|
1767
|
-
ctx.ui.notify("Usage: /governance status", "info");
|
|
3427
|
+
ctx.ui.notify("Usage: /governance status | init", "info");
|
|
1768
3428
|
}
|
|
1769
3429
|
}
|
|
1770
3430
|
});
|