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