@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.
@@ -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)">&#x1f465;</div>
718
+ <h2>Roles</h2>
719
+ <span class="section-badge badge-required">Required</span>
720
+ <span class="section-chevron">&#9660;</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)">&#x1f6e1;</div>
732
+ <h2>Data Loss Prevention</h2>
733
+ <span class="section-badge badge-optional">Optional</span>
734
+ <span class="section-chevron">&#9660;</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)">&#x1f4bb;</div>
831
+ <h2>Bash Classification</h2>
832
+ <span class="section-badge badge-optional">Optional</span>
833
+ <span class="section-chevron">&#9660;</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)">&#x1f9d1;</div>
854
+ <h2>Human-in-the-Loop</h2>
855
+ <span class="section-badge badge-required">Required</span>
856
+ <span class="section-chevron">&#9660;</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)">&#x1f4dd;</div>
891
+ <h2>Audit Logging</h2>
892
+ <span class="section-badge badge-required">Required</span>
893
+ <span class="section-chevron">&#9660;</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)">&#x1f511;</div>
906
+ <h2>Authentication</h2>
907
+ <span class="section-badge badge-optional">Optional</span>
908
+ <span class="section-chevron">&#9660;</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">&#10003;</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()">&times;</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()">&times;</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()">&times;</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,'&amp;').replace(/"/g,'&quot;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
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 import_node_fs2 = require("fs");
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, import_node_fs2.existsSync)(rulesFile)) {
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, import_node_fs2.existsSync)(newRulesFile)) {
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
  });