@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.
package/dist/index.cjs CHANGED
@@ -3086,7 +3086,8 @@ __export(index_exports, {
3086
3086
  createIdentityChain: () => createIdentityChain,
3087
3087
  createPolicyEngine: () => createPolicyEngine,
3088
3088
  loadConfig: () => loadConfig,
3089
- renderTemplate: () => render
3089
+ renderTemplate: () => render,
3090
+ startWizardServer: () => startWizardServer
3090
3091
  });
3091
3092
  module.exports = __toCommonJS(index_exports);
3092
3093
 
@@ -3272,7 +3273,20 @@ var DEFAULTS = {
3272
3273
  sinks: [{ type: "jsonl", path: "~/.pi/agent/audit.jsonl" }]
3273
3274
  },
3274
3275
  dlp: {
3275
- enabled: false
3276
+ enabled: true,
3277
+ mode: "audit",
3278
+ on_input: "block",
3279
+ on_output: "mask",
3280
+ masking: {
3281
+ strategy: "partial",
3282
+ show_chars: 4,
3283
+ placeholder: "***"
3284
+ },
3285
+ severity_threshold: "low",
3286
+ built_in: {
3287
+ secrets: true,
3288
+ pii: true
3289
+ }
3276
3290
  }
3277
3291
  };
3278
3292
 
@@ -4375,6 +4389,1601 @@ function createApprovalFlow(config, ui) {
4375
4389
  }
4376
4390
  return new CliApprover(ui, config.timeout_seconds);
4377
4391
  }
4392
+
4393
+ // src/lib/wizard/server.ts
4394
+ var import_node_http = require("http");
4395
+ var import_node_fs4 = require("fs");
4396
+ var import_node_path3 = require("path");
4397
+ var import_yaml6 = require("yaml");
4398
+
4399
+ // src/lib/wizard/html.ts
4400
+ var WIZARD_HTML = `<!DOCTYPE html>
4401
+ <html lang="en">
4402
+ <head>
4403
+ <meta charset="UTF-8">
4404
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
4405
+ <title>Pi Governance \u2014 Setup Wizard</title>
4406
+ <style>
4407
+ :root {
4408
+ --bg: #f8f9fa;
4409
+ --bg-surface: #ffffff;
4410
+ --bg-surface-alt: #f1f3f5;
4411
+ --bg-code: #e9ecef;
4412
+ --text: #212529;
4413
+ --text-muted: #6c757d;
4414
+ --text-inverse: #ffffff;
4415
+ --border: #dee2e6;
4416
+ --border-focus: #4263eb;
4417
+ --primary: #4263eb;
4418
+ --primary-hover: #3b5bdb;
4419
+ --primary-subtle: #dbe4ff;
4420
+ --success: #2f9e44;
4421
+ --success-subtle: #d3f9d8;
4422
+ --danger: #e03131;
4423
+ --danger-subtle: #ffe3e3;
4424
+ --warning: #f08c00;
4425
+ --warning-subtle: #fff3bf;
4426
+ --radius: 8px;
4427
+ --radius-sm: 4px;
4428
+ --shadow: 0 1px 3px rgba(0,0,0,0.08);
4429
+ --shadow-lg: 0 4px 12px rgba(0,0,0,0.1);
4430
+ --font-mono: 'SF Mono', 'Cascadia Code', 'Fira Code', monospace;
4431
+ --font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
4432
+ --transition: 150ms ease;
4433
+ }
4434
+
4435
+ @media (prefers-color-scheme: dark) {
4436
+ :root {
4437
+ --bg: #1a1b1e;
4438
+ --bg-surface: #25262b;
4439
+ --bg-surface-alt: #2c2e33;
4440
+ --bg-code: #2c2e33;
4441
+ --text: #c1c2c5;
4442
+ --text-muted: #909296;
4443
+ --text-inverse: #1a1b1e;
4444
+ --border: #373a40;
4445
+ --border-focus: #5c7cfa;
4446
+ --primary: #5c7cfa;
4447
+ --primary-hover: #748ffc;
4448
+ --primary-subtle: #1b2559;
4449
+ --success: #51cf66;
4450
+ --success-subtle: #0b3d1a;
4451
+ --danger: #ff6b6b;
4452
+ --danger-subtle: #3d0b0b;
4453
+ --warning: #fcc419;
4454
+ --warning-subtle: #3d2e00;
4455
+ --shadow: 0 1px 3px rgba(0,0,0,0.3);
4456
+ --shadow-lg: 0 4px 12px rgba(0,0,0,0.4);
4457
+ }
4458
+ }
4459
+
4460
+ * { margin: 0; padding: 0; box-sizing: border-box; }
4461
+
4462
+ body {
4463
+ font-family: var(--font-sans);
4464
+ background: var(--bg);
4465
+ color: var(--text);
4466
+ line-height: 1.6;
4467
+ min-height: 100vh;
4468
+ }
4469
+
4470
+ .layout {
4471
+ display: grid;
4472
+ grid-template-columns: 1fr 420px;
4473
+ gap: 0;
4474
+ min-height: 100vh;
4475
+ }
4476
+
4477
+ @media (max-width: 1024px) {
4478
+ .layout { grid-template-columns: 1fr; }
4479
+ .preview-panel { display: none; }
4480
+ }
4481
+
4482
+ /* --- Left column: Form --- */
4483
+ .form-column {
4484
+ padding: 32px 40px 80px;
4485
+ overflow-y: auto;
4486
+ max-height: 100vh;
4487
+ }
4488
+
4489
+ .logo {
4490
+ display: flex;
4491
+ align-items: center;
4492
+ gap: 10px;
4493
+ margin-bottom: 8px;
4494
+ }
4495
+
4496
+ .logo-icon {
4497
+ width: 36px; height: 36px;
4498
+ background: var(--primary);
4499
+ border-radius: var(--radius);
4500
+ display: flex; align-items: center; justify-content: center;
4501
+ color: var(--text-inverse);
4502
+ font-weight: 700; font-size: 18px;
4503
+ }
4504
+
4505
+ .logo-text {
4506
+ font-size: 20px;
4507
+ font-weight: 700;
4508
+ color: var(--text);
4509
+ }
4510
+
4511
+ .logo-text span { color: var(--text-muted); font-weight: 400; }
4512
+
4513
+ h1 {
4514
+ font-size: 28px;
4515
+ font-weight: 700;
4516
+ margin: 24px 0 8px;
4517
+ }
4518
+
4519
+ .subtitle {
4520
+ color: var(--text-muted);
4521
+ font-size: 15px;
4522
+ margin-bottom: 32px;
4523
+ }
4524
+
4525
+ /* Sections */
4526
+ .section {
4527
+ background: var(--bg-surface);
4528
+ border: 1px solid var(--border);
4529
+ border-radius: var(--radius);
4530
+ padding: 24px;
4531
+ margin-bottom: 20px;
4532
+ box-shadow: var(--shadow);
4533
+ }
4534
+
4535
+ .section-header {
4536
+ display: flex;
4537
+ align-items: center;
4538
+ gap: 10px;
4539
+ margin-bottom: 16px;
4540
+ cursor: pointer;
4541
+ user-select: none;
4542
+ }
4543
+
4544
+ .section-header h2 {
4545
+ font-size: 16px;
4546
+ font-weight: 600;
4547
+ flex: 1;
4548
+ }
4549
+
4550
+ .section-badge {
4551
+ font-size: 11px;
4552
+ padding: 2px 8px;
4553
+ border-radius: 10px;
4554
+ font-weight: 600;
4555
+ text-transform: uppercase;
4556
+ letter-spacing: 0.5px;
4557
+ }
4558
+
4559
+ .badge-required { background: var(--danger-subtle); color: var(--danger); }
4560
+ .badge-optional { background: var(--primary-subtle); color: var(--primary); }
4561
+
4562
+ .section-icon {
4563
+ width: 32px; height: 32px;
4564
+ border-radius: var(--radius-sm);
4565
+ display: flex; align-items: center; justify-content: center;
4566
+ font-size: 16px;
4567
+ flex-shrink: 0;
4568
+ }
4569
+
4570
+ .section-body { display: block; }
4571
+ .section.collapsed .section-body { display: none; }
4572
+ .section-chevron {
4573
+ transition: transform var(--transition);
4574
+ color: var(--text-muted);
4575
+ font-size: 12px;
4576
+ }
4577
+ .section.collapsed .section-chevron { transform: rotate(-90deg); }
4578
+
4579
+ /* Form elements */
4580
+ label {
4581
+ display: block;
4582
+ font-size: 13px;
4583
+ font-weight: 600;
4584
+ color: var(--text);
4585
+ margin-bottom: 4px;
4586
+ }
4587
+
4588
+ .label-hint {
4589
+ font-weight: 400;
4590
+ color: var(--text-muted);
4591
+ font-size: 12px;
4592
+ }
4593
+
4594
+ input[type="text"],
4595
+ input[type="number"],
4596
+ select {
4597
+ width: 100%;
4598
+ padding: 8px 12px;
4599
+ border: 1px solid var(--border);
4600
+ border-radius: var(--radius-sm);
4601
+ background: var(--bg-surface);
4602
+ color: var(--text);
4603
+ font-size: 14px;
4604
+ font-family: var(--font-sans);
4605
+ transition: border-color var(--transition);
4606
+ outline: none;
4607
+ }
4608
+
4609
+ input:focus, select:focus {
4610
+ border-color: var(--border-focus);
4611
+ box-shadow: 0 0 0 2px var(--primary-subtle);
4612
+ }
4613
+
4614
+ .field { margin-bottom: 16px; }
4615
+
4616
+ .field-row {
4617
+ display: grid;
4618
+ grid-template-columns: 1fr 1fr;
4619
+ gap: 12px;
4620
+ }
4621
+
4622
+ .field-row-3 {
4623
+ display: grid;
4624
+ grid-template-columns: 1fr 1fr 1fr;
4625
+ gap: 12px;
4626
+ }
4627
+
4628
+ /* Toggle switch */
4629
+ .toggle-row {
4630
+ display: flex;
4631
+ align-items: center;
4632
+ gap: 10px;
4633
+ margin-bottom: 12px;
4634
+ }
4635
+
4636
+ .toggle {
4637
+ position: relative;
4638
+ width: 40px; height: 22px;
4639
+ flex-shrink: 0;
4640
+ }
4641
+
4642
+ .toggle input { display: none; }
4643
+
4644
+ .toggle-slider {
4645
+ position: absolute;
4646
+ inset: 0;
4647
+ background: var(--border);
4648
+ border-radius: 11px;
4649
+ cursor: pointer;
4650
+ transition: background var(--transition);
4651
+ }
4652
+
4653
+ .toggle-slider::before {
4654
+ content: '';
4655
+ position: absolute;
4656
+ width: 16px; height: 16px;
4657
+ left: 3px; top: 3px;
4658
+ background: white;
4659
+ border-radius: 50%;
4660
+ transition: transform var(--transition);
4661
+ }
4662
+
4663
+ .toggle input:checked + .toggle-slider {
4664
+ background: var(--primary);
4665
+ }
4666
+
4667
+ .toggle input:checked + .toggle-slider::before {
4668
+ transform: translateX(18px);
4669
+ }
4670
+
4671
+ .toggle-label {
4672
+ font-size: 14px;
4673
+ font-weight: 500;
4674
+ }
4675
+
4676
+ /* Role cards */
4677
+ .role-grid {
4678
+ display: grid;
4679
+ grid-template-columns: 1fr 1fr;
4680
+ gap: 12px;
4681
+ }
4682
+
4683
+ .role-card {
4684
+ border: 2px solid var(--border);
4685
+ border-radius: var(--radius);
4686
+ padding: 16px;
4687
+ cursor: pointer;
4688
+ transition: all var(--transition);
4689
+ position: relative;
4690
+ }
4691
+
4692
+ .role-card:hover { border-color: var(--primary); }
4693
+
4694
+ .role-card.selected {
4695
+ border-color: var(--primary);
4696
+ background: var(--primary-subtle);
4697
+ }
4698
+
4699
+ .role-card-check {
4700
+ position: absolute;
4701
+ top: 12px; right: 12px;
4702
+ width: 20px; height: 20px;
4703
+ border: 2px solid var(--border);
4704
+ border-radius: 4px;
4705
+ display: flex; align-items: center; justify-content: center;
4706
+ font-size: 12px;
4707
+ color: transparent;
4708
+ transition: all var(--transition);
4709
+ }
4710
+
4711
+ .role-card.selected .role-card-check {
4712
+ background: var(--primary);
4713
+ border-color: var(--primary);
4714
+ color: white;
4715
+ }
4716
+
4717
+ .role-name {
4718
+ font-weight: 600;
4719
+ font-size: 14px;
4720
+ margin-bottom: 4px;
4721
+ }
4722
+
4723
+ .role-desc {
4724
+ font-size: 12px;
4725
+ color: var(--text-muted);
4726
+ margin-bottom: 8px;
4727
+ }
4728
+
4729
+ .role-tags {
4730
+ display: flex;
4731
+ flex-wrap: wrap;
4732
+ gap: 4px;
4733
+ }
4734
+
4735
+ .role-tag {
4736
+ font-size: 11px;
4737
+ padding: 1px 6px;
4738
+ border-radius: 3px;
4739
+ background: var(--bg-surface-alt);
4740
+ color: var(--text-muted);
4741
+ font-family: var(--font-mono);
4742
+ }
4743
+
4744
+ .role-details {
4745
+ display: none;
4746
+ margin-top: 12px;
4747
+ padding-top: 12px;
4748
+ border-top: 1px solid var(--border);
4749
+ }
4750
+
4751
+ .role-card.selected .role-details { display: block; }
4752
+
4753
+ /* Chip selector */
4754
+ .chip-group {
4755
+ display: flex;
4756
+ gap: 6px;
4757
+ flex-wrap: wrap;
4758
+ margin-bottom: 12px;
4759
+ }
4760
+
4761
+ .chip {
4762
+ padding: 5px 14px;
4763
+ border: 1px solid var(--border);
4764
+ border-radius: 16px;
4765
+ font-size: 13px;
4766
+ cursor: pointer;
4767
+ transition: all var(--transition);
4768
+ background: var(--bg-surface);
4769
+ color: var(--text);
4770
+ }
4771
+
4772
+ .chip:hover { border-color: var(--primary); }
4773
+
4774
+ .chip.active {
4775
+ background: var(--primary);
4776
+ border-color: var(--primary);
4777
+ color: var(--text-inverse);
4778
+ }
4779
+
4780
+ /* Pattern list */
4781
+ .pattern-list { margin-top: 12px; }
4782
+
4783
+ .pattern-item {
4784
+ display: grid;
4785
+ grid-template-columns: 1fr 1.5fr auto auto;
4786
+ gap: 8px;
4787
+ align-items: center;
4788
+ margin-bottom: 8px;
4789
+ }
4790
+
4791
+ .pattern-item input, .pattern-item select {
4792
+ font-size: 13px;
4793
+ padding: 6px 8px;
4794
+ }
4795
+
4796
+ .btn-remove {
4797
+ background: none;
4798
+ border: none;
4799
+ color: var(--danger);
4800
+ cursor: pointer;
4801
+ font-size: 18px;
4802
+ padding: 4px;
4803
+ line-height: 1;
4804
+ }
4805
+
4806
+ .btn-add {
4807
+ background: none;
4808
+ border: 1px dashed var(--border);
4809
+ border-radius: var(--radius-sm);
4810
+ padding: 6px 14px;
4811
+ color: var(--primary);
4812
+ cursor: pointer;
4813
+ font-size: 13px;
4814
+ font-weight: 500;
4815
+ transition: all var(--transition);
4816
+ }
4817
+
4818
+ .btn-add:hover {
4819
+ border-color: var(--primary);
4820
+ background: var(--primary-subtle);
4821
+ }
4822
+
4823
+ /* Allowlist */
4824
+ .allowlist-item {
4825
+ display: flex;
4826
+ gap: 8px;
4827
+ margin-bottom: 8px;
4828
+ }
4829
+
4830
+ .allowlist-item input { flex: 1; font-size: 13px; padding: 6px 8px; }
4831
+
4832
+ /* Sink list */
4833
+ .sink-item {
4834
+ background: var(--bg-surface-alt);
4835
+ border-radius: var(--radius-sm);
4836
+ padding: 12px;
4837
+ margin-bottom: 8px;
4838
+ position: relative;
4839
+ }
4840
+
4841
+ .sink-item .btn-remove {
4842
+ position: absolute;
4843
+ top: 8px; right: 8px;
4844
+ }
4845
+
4846
+ /* --- Right column: Preview --- */
4847
+ .preview-panel {
4848
+ background: var(--bg-surface-alt);
4849
+ border-left: 1px solid var(--border);
4850
+ padding: 24px;
4851
+ display: flex;
4852
+ flex-direction: column;
4853
+ position: sticky;
4854
+ top: 0;
4855
+ height: 100vh;
4856
+ overflow: hidden;
4857
+ }
4858
+
4859
+ .preview-header {
4860
+ display: flex;
4861
+ align-items: center;
4862
+ justify-content: space-between;
4863
+ margin-bottom: 16px;
4864
+ flex-shrink: 0;
4865
+ }
4866
+
4867
+ .preview-header h3 {
4868
+ font-size: 14px;
4869
+ font-weight: 600;
4870
+ }
4871
+
4872
+ .preview-tabs {
4873
+ display: flex;
4874
+ gap: 4px;
4875
+ margin-bottom: 12px;
4876
+ flex-shrink: 0;
4877
+ }
4878
+
4879
+ .preview-tab {
4880
+ padding: 4px 12px;
4881
+ font-size: 12px;
4882
+ border: 1px solid var(--border);
4883
+ border-radius: var(--radius-sm);
4884
+ background: var(--bg-surface);
4885
+ cursor: pointer;
4886
+ color: var(--text-muted);
4887
+ transition: all var(--transition);
4888
+ }
4889
+
4890
+ .preview-tab.active {
4891
+ background: var(--primary);
4892
+ border-color: var(--primary);
4893
+ color: var(--text-inverse);
4894
+ }
4895
+
4896
+ .preview-content {
4897
+ flex: 1;
4898
+ overflow-y: auto;
4899
+ background: var(--bg-surface);
4900
+ border: 1px solid var(--border);
4901
+ border-radius: var(--radius);
4902
+ padding: 16px;
4903
+ }
4904
+
4905
+ .preview-content pre {
4906
+ font-family: var(--font-mono);
4907
+ font-size: 12px;
4908
+ line-height: 1.5;
4909
+ white-space: pre;
4910
+ color: var(--text);
4911
+ margin: 0;
4912
+ }
4913
+
4914
+ /* Bottom bar */
4915
+ .bottom-bar {
4916
+ position: fixed;
4917
+ bottom: 0;
4918
+ left: 0;
4919
+ right: 420px;
4920
+ padding: 16px 40px;
4921
+ background: var(--bg-surface);
4922
+ border-top: 1px solid var(--border);
4923
+ display: flex;
4924
+ align-items: center;
4925
+ justify-content: space-between;
4926
+ z-index: 100;
4927
+ box-shadow: 0 -2px 8px rgba(0,0,0,0.06);
4928
+ }
4929
+
4930
+ @media (max-width: 1024px) {
4931
+ .bottom-bar { right: 0; }
4932
+ }
4933
+
4934
+ .btn-primary {
4935
+ padding: 10px 28px;
4936
+ background: var(--primary);
4937
+ color: var(--text-inverse);
4938
+ border: none;
4939
+ border-radius: var(--radius);
4940
+ font-size: 14px;
4941
+ font-weight: 600;
4942
+ cursor: pointer;
4943
+ transition: background var(--transition);
4944
+ }
4945
+
4946
+ .btn-primary:hover { background: var(--primary-hover); }
4947
+ .btn-primary:disabled { opacity: 0.5; cursor: not-allowed; }
4948
+
4949
+ .btn-secondary {
4950
+ padding: 10px 20px;
4951
+ background: var(--bg-surface);
4952
+ color: var(--text);
4953
+ border: 1px solid var(--border);
4954
+ border-radius: var(--radius);
4955
+ font-size: 14px;
4956
+ font-weight: 500;
4957
+ cursor: pointer;
4958
+ transition: all var(--transition);
4959
+ }
4960
+
4961
+ .btn-secondary:hover { border-color: var(--primary); color: var(--primary); }
4962
+
4963
+ .save-status {
4964
+ font-size: 13px;
4965
+ color: var(--text-muted);
4966
+ }
4967
+
4968
+ .save-status.success { color: var(--success); }
4969
+ .save-status.error { color: var(--danger); }
4970
+
4971
+ /* Toast */
4972
+ .toast {
4973
+ position: fixed;
4974
+ top: 20px;
4975
+ right: 20px;
4976
+ padding: 12px 20px;
4977
+ border-radius: var(--radius);
4978
+ font-size: 14px;
4979
+ font-weight: 500;
4980
+ z-index: 1000;
4981
+ box-shadow: var(--shadow-lg);
4982
+ transform: translateY(-10px);
4983
+ opacity: 0;
4984
+ transition: all 300ms ease;
4985
+ pointer-events: none;
4986
+ }
4987
+
4988
+ .toast.visible { transform: translateY(0); opacity: 1; pointer-events: auto; }
4989
+ .toast.success { background: var(--success); color: white; }
4990
+ .toast.error { background: var(--danger); color: white; }
4991
+
4992
+ /* Helpers */
4993
+ .help-text {
4994
+ font-size: 12px;
4995
+ color: var(--text-muted);
4996
+ margin-top: 4px;
4997
+ }
4998
+
4999
+ .divider {
5000
+ border: none;
5001
+ border-top: 1px solid var(--border);
5002
+ margin: 16px 0;
5003
+ }
5004
+
5005
+ .hidden { display: none !important; }
5006
+ </style>
5007
+ </head>
5008
+ <body>
5009
+ <div class="layout">
5010
+ <!-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 LEFT: FORM \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->
5011
+ <div class="form-column">
5012
+ <div class="logo">
5013
+ <div class="logo-icon">G</div>
5014
+ <div class="logo-text">Pi Governance <span>Setup Wizard</span></div>
5015
+ </div>
5016
+
5017
+ <h1>Configure your governance policy</h1>
5018
+ <p class="subtitle">
5019
+ AI coding agents are powerful but need guardrails. This wizard generates a
5020
+ <code>governance.yaml</code> and <code>governance-rules.yaml</code> to control
5021
+ tool access, bash safety, data-loss prevention, human approvals, and audit logging.
5022
+ </p>
5023
+
5024
+ <!-- \u2500\u2500 1. Roles \u2500\u2500 -->
5025
+ <div class="section" id="sec-roles">
5026
+ <div class="section-header" onclick="toggleSection('sec-roles')">
5027
+ <div class="section-icon" style="background:var(--primary-subtle);color:var(--primary)">&#x1f465;</div>
5028
+ <h2>Roles</h2>
5029
+ <span class="section-badge badge-required">Required</span>
5030
+ <span class="section-chevron">&#9660;</span>
5031
+ </div>
5032
+ <div class="section-body">
5033
+ <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>
5034
+ <div class="role-grid" id="role-grid"></div>
5035
+ </div>
5036
+ </div>
5037
+
5038
+ <!-- \u2500\u2500 2. DLP \u2500\u2500 -->
5039
+ <div class="section" id="sec-dlp">
5040
+ <div class="section-header" onclick="toggleSection('sec-dlp')">
5041
+ <div class="section-icon" style="background:var(--warning-subtle);color:var(--warning)">&#x1f6e1;</div>
5042
+ <h2>Data Loss Prevention</h2>
5043
+ <span class="section-badge badge-optional">Optional</span>
5044
+ <span class="section-chevron">&#9660;</span>
5045
+ </div>
5046
+ <div class="section-body">
5047
+ <div class="toggle-row">
5048
+ <label class="toggle"><input type="checkbox" id="dlp-enabled" checked onchange="updatePreview()"><span class="toggle-slider"></span></label>
5049
+ <span class="toggle-label">Enable DLP scanning</span>
5050
+ </div>
5051
+
5052
+ <div id="dlp-options">
5053
+ <div class="field">
5054
+ <label>Default Mode</label>
5055
+ <div class="chip-group" id="dlp-mode">
5056
+ <span class="chip active" data-value="audit" onclick="selectChip(this)">Audit</span>
5057
+ <span class="chip" data-value="mask" onclick="selectChip(this)">Mask</span>
5058
+ <span class="chip" data-value="block" onclick="selectChip(this)">Block</span>
5059
+ </div>
5060
+ </div>
5061
+
5062
+ <div class="field-row">
5063
+ <div class="field">
5064
+ <label>On Input <span class="label-hint">(agent receives)</span></label>
5065
+ <select id="dlp-on-input" onchange="updatePreview()">
5066
+ <option value="">Use default mode</option>
5067
+ <option value="audit">Audit</option>
5068
+ <option value="mask">Mask</option>
5069
+ <option value="block" selected>Block</option>
5070
+ </select>
5071
+ </div>
5072
+ <div class="field">
5073
+ <label>On Output <span class="label-hint">(agent produces)</span></label>
5074
+ <select id="dlp-on-output" onchange="updatePreview()">
5075
+ <option value="">Use default mode</option>
5076
+ <option value="audit">Audit</option>
5077
+ <option value="mask" selected>Mask</option>
5078
+ <option value="block">Block</option>
5079
+ </select>
5080
+ </div>
5081
+ </div>
5082
+
5083
+ <hr class="divider">
5084
+ <label>Built-in Patterns</label>
5085
+ <div class="toggle-row" style="margin-top:6px">
5086
+ <label class="toggle"><input type="checkbox" id="dlp-secrets" checked onchange="updatePreview()"><span class="toggle-slider"></span></label>
5087
+ <span class="toggle-label">Secrets <span class="label-hint">(API keys, tokens, passwords)</span></span>
5088
+ </div>
5089
+ <div class="toggle-row">
5090
+ <label class="toggle"><input type="checkbox" id="dlp-pii" checked onchange="updatePreview()"><span class="toggle-slider"></span></label>
5091
+ <span class="toggle-label">PII <span class="label-hint">(emails, phone numbers, SSNs)</span></span>
5092
+ </div>
5093
+
5094
+ <hr class="divider">
5095
+ <label>Masking Options</label>
5096
+ <div class="field-row-3" style="margin-top:8px">
5097
+ <div class="field">
5098
+ <label>Strategy</label>
5099
+ <select id="dlp-mask-strategy" onchange="updatePreview()">
5100
+ <option value="partial" selected>Partial</option>
5101
+ <option value="full">Full</option>
5102
+ <option value="hash">Hash</option>
5103
+ </select>
5104
+ </div>
5105
+ <div class="field">
5106
+ <label>Show Chars</label>
5107
+ <input type="number" id="dlp-mask-show" value="4" min="0" onchange="updatePreview()">
5108
+ </div>
5109
+ <div class="field">
5110
+ <label>Placeholder</label>
5111
+ <input type="text" id="dlp-mask-placeholder" value="***" onchange="updatePreview()">
5112
+ </div>
5113
+ </div>
5114
+
5115
+ <hr class="divider">
5116
+ <label>Severity Threshold <span class="label-hint">(minimum severity to trigger DLP)</span></label>
5117
+ <div class="chip-group" id="dlp-severity" style="margin-top:6px">
5118
+ <span class="chip active" data-value="low" onclick="selectChip(this)">Low</span>
5119
+ <span class="chip" data-value="medium" onclick="selectChip(this)">Medium</span>
5120
+ <span class="chip" data-value="high" onclick="selectChip(this)">High</span>
5121
+ <span class="chip" data-value="critical" onclick="selectChip(this)">Critical</span>
5122
+ </div>
5123
+
5124
+ <hr class="divider">
5125
+ <label>Custom Patterns</label>
5126
+ <div class="pattern-list" id="dlp-custom-patterns"></div>
5127
+ <button class="btn-add" onclick="addCustomPattern()">+ Add pattern</button>
5128
+
5129
+ <hr class="divider">
5130
+ <label>Allowlist <span class="label-hint">(patterns to ignore)</span></label>
5131
+ <div id="dlp-allowlist"></div>
5132
+ <button class="btn-add" onclick="addAllowlistEntry()" style="margin-top:8px">+ Add entry</button>
5133
+ </div>
5134
+ </div>
5135
+ </div>
5136
+
5137
+ <!-- \u2500\u2500 3. Bash Classification \u2500\u2500 -->
5138
+ <div class="section" id="sec-bash">
5139
+ <div class="section-header" onclick="toggleSection('sec-bash')">
5140
+ <div class="section-icon" style="background:var(--danger-subtle);color:var(--danger)">&#x1f4bb;</div>
5141
+ <h2>Bash Classification</h2>
5142
+ <span class="section-badge badge-optional">Optional</span>
5143
+ <span class="section-chevron">&#9660;</span>
5144
+ </div>
5145
+ <div class="section-body">
5146
+ <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>
5147
+ <div class="field">
5148
+ <label>Auto-block Severity</label>
5149
+ <p class="help-text">Commands at or above this severity are blocked without HITL.</p>
5150
+ <div class="chip-group" id="bash-severity" style="margin-top:6px">
5151
+ <span class="chip" data-value="low" onclick="selectChip(this)">Low</span>
5152
+ <span class="chip" data-value="medium" onclick="selectChip(this)">Medium</span>
5153
+ <span class="chip active" data-value="high" onclick="selectChip(this)">High</span>
5154
+ <span class="chip" data-value="critical" onclick="selectChip(this)">Critical</span>
5155
+ </div>
5156
+ </div>
5157
+ </div>
5158
+ </div>
5159
+
5160
+ <!-- \u2500\u2500 4. HITL \u2500\u2500 -->
5161
+ <div class="section" id="sec-hitl">
5162
+ <div class="section-header" onclick="toggleSection('sec-hitl')">
5163
+ <div class="section-icon" style="background:var(--success-subtle);color:var(--success)">&#x1f9d1;</div>
5164
+ <h2>Human-in-the-Loop</h2>
5165
+ <span class="section-badge badge-required">Required</span>
5166
+ <span class="section-chevron">&#9660;</span>
5167
+ </div>
5168
+ <div class="section-body">
5169
+ <div class="field">
5170
+ <label>Default Execution Mode</label>
5171
+ <div class="chip-group" id="hitl-mode">
5172
+ <span class="chip" data-value="autonomous" onclick="selectChip(this)">Autonomous</span>
5173
+ <span class="chip active" data-value="supervised" onclick="selectChip(this)">Supervised</span>
5174
+ <span class="chip" data-value="dry_run" onclick="selectChip(this)">Dry Run</span>
5175
+ </div>
5176
+ </div>
5177
+ <div class="field-row">
5178
+ <div class="field">
5179
+ <label>Approval Channel</label>
5180
+ <select id="hitl-channel" onchange="updatePreview()">
5181
+ <option value="cli" selected>CLI (terminal prompt)</option>
5182
+ <option value="webhook">Webhook</option>
5183
+ </select>
5184
+ </div>
5185
+ <div class="field">
5186
+ <label>Timeout <span class="label-hint">(seconds)</span></label>
5187
+ <input type="number" id="hitl-timeout" value="300" min="10" max="3600" onchange="updatePreview()">
5188
+ </div>
5189
+ </div>
5190
+ <div class="field hidden" id="hitl-webhook-field">
5191
+ <label>Webhook URL</label>
5192
+ <input type="text" id="hitl-webhook-url" placeholder="https://..." onchange="updatePreview()">
5193
+ </div>
5194
+ </div>
5195
+ </div>
5196
+
5197
+ <!-- \u2500\u2500 5. Audit \u2500\u2500 -->
5198
+ <div class="section" id="sec-audit">
5199
+ <div class="section-header" onclick="toggleSection('sec-audit')">
5200
+ <div class="section-icon" style="background:var(--primary-subtle);color:var(--primary)">&#x1f4dd;</div>
5201
+ <h2>Audit Logging</h2>
5202
+ <span class="section-badge badge-required">Required</span>
5203
+ <span class="section-chevron">&#9660;</span>
5204
+ </div>
5205
+ <div class="section-body">
5206
+ <p class="help-text" style="margin-bottom:12px">All governance events are logged to one or more sinks.</p>
5207
+ <div id="audit-sinks"></div>
5208
+ <button class="btn-add" onclick="addAuditSink()" style="margin-top:8px">+ Add sink</button>
5209
+ </div>
5210
+ </div>
5211
+
5212
+ <!-- \u2500\u2500 6. Auth \u2500\u2500 -->
5213
+ <div class="section collapsed" id="sec-auth">
5214
+ <div class="section-header" onclick="toggleSection('sec-auth')">
5215
+ <div class="section-icon" style="background:var(--bg-surface-alt);color:var(--text-muted)">&#x1f511;</div>
5216
+ <h2>Authentication</h2>
5217
+ <span class="section-badge badge-optional">Optional</span>
5218
+ <span class="section-chevron">&#9660;</span>
5219
+ </div>
5220
+ <div class="section-body">
5221
+ <div class="field">
5222
+ <label>Provider</label>
5223
+ <div class="chip-group" id="auth-provider">
5224
+ <span class="chip active" data-value="env" onclick="selectChip(this)">Environment Vars</span>
5225
+ <span class="chip" data-value="local" onclick="selectChip(this)">Local File</span>
5226
+ <span class="chip" data-value="oidc" onclick="selectChip(this)">OIDC</span>
5227
+ </div>
5228
+ </div>
5229
+ <div id="auth-env-fields">
5230
+ <div class="field-row-3">
5231
+ <div class="field">
5232
+ <label>User Var</label>
5233
+ <input type="text" id="auth-user-var" value="GRWND_USER" onchange="updatePreview()">
5234
+ </div>
5235
+ <div class="field">
5236
+ <label>Role Var</label>
5237
+ <input type="text" id="auth-role-var" value="GRWND_ROLE" onchange="updatePreview()">
5238
+ </div>
5239
+ <div class="field">
5240
+ <label>Org Unit Var</label>
5241
+ <input type="text" id="auth-org-unit-var" value="GRWND_ORG_UNIT" onchange="updatePreview()">
5242
+ </div>
5243
+ </div>
5244
+ </div>
5245
+ <div id="auth-local-fields" class="hidden">
5246
+ <div class="field">
5247
+ <label>Users File</label>
5248
+ <input type="text" id="auth-users-file" value="./users.yaml" onchange="updatePreview()">
5249
+ </div>
5250
+ </div>
5251
+ </div>
5252
+ </div>
5253
+
5254
+ <div style="height:80px"></div>
5255
+ </div>
5256
+
5257
+ <!-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 RIGHT: PREVIEW \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->
5258
+ <div class="preview-panel">
5259
+ <div class="preview-header">
5260
+ <h3>Live Preview</h3>
5261
+ </div>
5262
+ <div class="preview-tabs">
5263
+ <span class="preview-tab active" data-tab="governance" onclick="switchTab(this)">governance.yaml</span>
5264
+ <span class="preview-tab" data-tab="rules" onclick="switchTab(this)">governance-rules.yaml</span>
5265
+ </div>
5266
+ <div class="preview-content">
5267
+ <pre id="preview-yaml"></pre>
5268
+ </div>
5269
+ </div>
5270
+ </div>
5271
+
5272
+ <!-- \u2500\u2500 Bottom bar \u2500\u2500 -->
5273
+ <div class="bottom-bar">
5274
+ <span class="save-status" id="save-status"></span>
5275
+ <div style="display:flex;gap:10px">
5276
+ <button class="btn-secondary" onclick="handleClose()">Cancel</button>
5277
+ <button class="btn-primary" id="btn-save" onclick="handleSave()">Save Configuration</button>
5278
+ </div>
5279
+ </div>
5280
+
5281
+ <!-- Toast -->
5282
+ <div class="toast" id="toast"></div>
5283
+
5284
+ <script>
5285
+ // \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
5286
+ const PRESET_ROLES = {
5287
+ analyst: {
5288
+ label: 'Analyst',
5289
+ desc: 'Read-only access, every action requires approval.',
5290
+ allowed_tools: ['read','grep','find','ls'],
5291
+ blocked_tools: ['write','edit','bash'],
5292
+ prompt_template: 'analyst',
5293
+ execution_mode: 'supervised',
5294
+ human_approval: { required_for: ['all'] },
5295
+ token_budget_daily: 100000,
5296
+ allowed_paths: ['{{project_path}}/**'],
5297
+ blocked_paths: ['**/secrets/**', '**/.env*']
5298
+ },
5299
+ project_lead: {
5300
+ label: 'Project Lead',
5301
+ desc: 'Full tools, bash & write need human approval.',
5302
+ allowed_tools: ['read','write','edit','bash','grep','find','ls'],
5303
+ blocked_tools: [],
5304
+ prompt_template: 'project-lead',
5305
+ execution_mode: 'supervised',
5306
+ human_approval: { required_for: ['bash','write'], auto_approve: ['read','edit','grep','find','ls'] },
5307
+ token_budget_daily: 500000,
5308
+ allowed_paths: ['{{project_path}}/**'],
5309
+ blocked_paths: ['**/secrets/**', '**/.env*'],
5310
+ bash_overrides: { additional_blocked: ['sudo','ssh','curl.*\\\\|.*sh'] }
5311
+ },
5312
+ admin: {
5313
+ label: 'Admin',
5314
+ desc: 'Full autonomous access, no approvals, unlimited budget.',
5315
+ allowed_tools: ['all'],
5316
+ blocked_tools: [],
5317
+ prompt_template: 'admin',
5318
+ execution_mode: 'autonomous',
5319
+ human_approval: { required_for: [] },
5320
+ token_budget_daily: -1,
5321
+ allowed_paths: ['**'],
5322
+ blocked_paths: []
5323
+ },
5324
+ auditor: {
5325
+ label: 'Auditor',
5326
+ desc: 'Dry-run: all calls logged, nothing executed.',
5327
+ allowed_tools: ['read','grep','find','ls'],
5328
+ blocked_tools: ['write','edit','bash'],
5329
+ prompt_template: 'analyst',
5330
+ execution_mode: 'dry_run',
5331
+ human_approval: { required_for: ['all'] },
5332
+ token_budget_daily: 50000,
5333
+ allowed_paths: ['**'],
5334
+ blocked_paths: ['**/secrets/**']
5335
+ }
5336
+ };
5337
+
5338
+ let selectedRoles = { analyst: false, project_lead: true, admin: false, auditor: false };
5339
+ let customPatterns = [];
5340
+ let allowlistEntries = [];
5341
+ let auditSinks = [{ type: 'jsonl', path: '~/.pi/agent/audit.jsonl' }];
5342
+ let activePreviewTab = 'governance';
5343
+
5344
+ // \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
5345
+ document.addEventListener('DOMContentLoaded', async () => {
5346
+ renderRoles();
5347
+ renderAuditSinks();
5348
+ updatePreview();
5349
+
5350
+ try {
5351
+ const [configRes, defaultsRes] = await Promise.all([
5352
+ fetch('/api/config').catch(() => null),
5353
+ fetch('/api/defaults').catch(() => null)
5354
+ ]);
5355
+ if (configRes && configRes.ok) {
5356
+ const cfg = await configRes.json();
5357
+ applyConfig(cfg);
5358
+ }
5359
+ if (defaultsRes && defaultsRes.ok) {
5360
+ const defs = await defaultsRes.json();
5361
+ if (!configRes || !configRes.ok) applyConfig(defs);
5362
+ }
5363
+ } catch (e) { /* use built-in defaults */ }
5364
+
5365
+ // watch for webhook channel
5366
+ document.getElementById('hitl-channel').addEventListener('change', (e) => {
5367
+ document.getElementById('hitl-webhook-field').classList.toggle('hidden', e.target.value !== 'webhook');
5368
+ updatePreview();
5369
+ });
5370
+
5371
+ // watch dlp toggle
5372
+ document.getElementById('dlp-enabled').addEventListener('change', (e) => {
5373
+ document.getElementById('dlp-options').classList.toggle('hidden', !e.target.checked);
5374
+ updatePreview();
5375
+ });
5376
+ });
5377
+
5378
+ // \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
5379
+ function applyConfig(cfg) {
5380
+ if (!cfg) return;
5381
+
5382
+ // Auth
5383
+ if (cfg.auth) {
5384
+ setChipValue('auth-provider', cfg.auth.provider || 'env');
5385
+ if (cfg.auth.env) {
5386
+ if (cfg.auth.env.user_var) document.getElementById('auth-user-var').value = cfg.auth.env.user_var;
5387
+ if (cfg.auth.env.role_var) document.getElementById('auth-role-var').value = cfg.auth.env.role_var;
5388
+ if (cfg.auth.env.org_unit_var) document.getElementById('auth-org-unit-var').value = cfg.auth.env.org_unit_var;
5389
+ }
5390
+ if (cfg.auth.local && cfg.auth.local.users_file) {
5391
+ document.getElementById('auth-users-file').value = cfg.auth.local.users_file;
5392
+ }
5393
+ }
5394
+
5395
+ // HITL
5396
+ if (cfg.hitl) {
5397
+ if (cfg.hitl.default_mode) setChipValue('hitl-mode', cfg.hitl.default_mode);
5398
+ if (cfg.hitl.approval_channel) {
5399
+ document.getElementById('hitl-channel').value = cfg.hitl.approval_channel;
5400
+ document.getElementById('hitl-webhook-field').classList.toggle('hidden', cfg.hitl.approval_channel !== 'webhook');
5401
+ }
5402
+ if (cfg.hitl.timeout_seconds) document.getElementById('hitl-timeout').value = cfg.hitl.timeout_seconds;
5403
+ if (cfg.hitl.webhook && cfg.hitl.webhook.url) document.getElementById('hitl-webhook-url').value = cfg.hitl.webhook.url;
5404
+ }
5405
+
5406
+ // DLP
5407
+ if (cfg.dlp) {
5408
+ document.getElementById('dlp-enabled').checked = cfg.dlp.enabled !== false;
5409
+ document.getElementById('dlp-options').classList.toggle('hidden', !cfg.dlp.enabled);
5410
+ if (cfg.dlp.mode) setChipValue('dlp-mode', cfg.dlp.mode);
5411
+ if (cfg.dlp.on_input) document.getElementById('dlp-on-input').value = cfg.dlp.on_input;
5412
+ if (cfg.dlp.on_output) document.getElementById('dlp-on-output').value = cfg.dlp.on_output;
5413
+ if (cfg.dlp.masking) {
5414
+ if (cfg.dlp.masking.strategy) document.getElementById('dlp-mask-strategy').value = cfg.dlp.masking.strategy;
5415
+ if (cfg.dlp.masking.show_chars != null) document.getElementById('dlp-mask-show').value = cfg.dlp.masking.show_chars;
5416
+ if (cfg.dlp.masking.placeholder) document.getElementById('dlp-mask-placeholder').value = cfg.dlp.masking.placeholder;
5417
+ }
5418
+ if (cfg.dlp.severity_threshold) setChipValue('dlp-severity', cfg.dlp.severity_threshold);
5419
+ if (cfg.dlp.built_in) {
5420
+ if (cfg.dlp.built_in.secrets != null) document.getElementById('dlp-secrets').checked = cfg.dlp.built_in.secrets;
5421
+ if (cfg.dlp.built_in.pii != null) document.getElementById('dlp-pii').checked = cfg.dlp.built_in.pii;
5422
+ }
5423
+ if (cfg.dlp.custom_patterns) {
5424
+ customPatterns = cfg.dlp.custom_patterns;
5425
+ renderCustomPatterns();
5426
+ }
5427
+ if (cfg.dlp.allowlist) {
5428
+ allowlistEntries = cfg.dlp.allowlist.map(e => e.pattern || e);
5429
+ renderAllowlist();
5430
+ }
5431
+ }
5432
+
5433
+ // Audit
5434
+ if (cfg.audit && cfg.audit.sinks) {
5435
+ auditSinks = cfg.audit.sinks;
5436
+ renderAuditSinks();
5437
+ }
5438
+
5439
+ updatePreview();
5440
+ }
5441
+
5442
+ // \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
5443
+ function renderRoles() {
5444
+ const grid = document.getElementById('role-grid');
5445
+ grid.innerHTML = '';
5446
+ for (const [key, role] of Object.entries(PRESET_ROLES)) {
5447
+ const sel = selectedRoles[key];
5448
+ const card = document.createElement('div');
5449
+ card.className = 'role-card' + (sel ? ' selected' : '');
5450
+ card.dataset.role = key;
5451
+ card.onclick = () => { toggleRole(key); };
5452
+ card.innerHTML =
5453
+ '<div class="role-card-check">&#10003;</div>' +
5454
+ '<div class="role-name">' + role.label + '</div>' +
5455
+ '<div class="role-desc">' + role.desc + '</div>' +
5456
+ '<div class="role-tags">' +
5457
+ '<span class="role-tag">' + role.execution_mode + '</span>' +
5458
+ '<span class="role-tag">' + (role.token_budget_daily === -1 ? 'unlimited' : role.token_budget_daily.toLocaleString()) + ' budget</span>' +
5459
+ '</div>' +
5460
+ '<div class="role-details">' +
5461
+ '<div class="field"><label>Allowed Tools</label>' +
5462
+ '<input type="text" value="' + role.allowed_tools.join(', ') + '" onchange="updateRoleField(\\'' + key + '\\', \\'allowed_tools\\', this.value)" onclick="event.stopPropagation()">' +
5463
+ '</div>' +
5464
+ '<div class="field"><label>Blocked Tools</label>' +
5465
+ '<input type="text" value="' + role.blocked_tools.join(', ') + '" onchange="updateRoleField(\\'' + key + '\\', \\'blocked_tools\\', this.value)" onclick="event.stopPropagation()">' +
5466
+ '</div>' +
5467
+ '<div class="field"><label>Execution Mode</label>' +
5468
+ '<select onchange="updateRoleField(\\'' + key + '\\', \\'execution_mode\\', this.value)" onclick="event.stopPropagation()">' +
5469
+ '<option value="supervised"' + (role.execution_mode === 'supervised' ? ' selected' : '') + '>Supervised</option>' +
5470
+ '<option value="autonomous"' + (role.execution_mode === 'autonomous' ? ' selected' : '') + '>Autonomous</option>' +
5471
+ '<option value="dry_run"' + (role.execution_mode === 'dry_run' ? ' selected' : '') + '>Dry Run</option>' +
5472
+ '</select>' +
5473
+ '</div>' +
5474
+ '<div class="field"><label>Token Budget Daily</label>' +
5475
+ '<input type="number" value="' + role.token_budget_daily + '" onchange="updateRoleField(\\'' + key + '\\', \\'token_budget_daily\\', parseInt(this.value))" onclick="event.stopPropagation()">' +
5476
+ '</div>' +
5477
+ '</div>';
5478
+ grid.appendChild(card);
5479
+ }
5480
+ }
5481
+
5482
+ function toggleRole(key) {
5483
+ selectedRoles[key] = !selectedRoles[key];
5484
+ renderRoles();
5485
+ updatePreview();
5486
+ }
5487
+
5488
+ function updateRoleField(key, field, value) {
5489
+ if (field === 'allowed_tools' || field === 'blocked_tools') {
5490
+ PRESET_ROLES[key][field] = value.split(',').map(s => s.trim()).filter(Boolean);
5491
+ } else {
5492
+ PRESET_ROLES[key][field] = value;
5493
+ }
5494
+ updatePreview();
5495
+ }
5496
+
5497
+ // \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
5498
+ function toggleSection(id) {
5499
+ document.getElementById(id).classList.toggle('collapsed');
5500
+ }
5501
+
5502
+ // \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
5503
+ function selectChip(el) {
5504
+ const group = el.parentElement;
5505
+ group.querySelectorAll('.chip').forEach(c => c.classList.remove('active'));
5506
+ el.classList.add('active');
5507
+
5508
+ // Auth provider visibility
5509
+ if (group.id === 'auth-provider') {
5510
+ const val = el.dataset.value;
5511
+ document.getElementById('auth-env-fields').classList.toggle('hidden', val !== 'env');
5512
+ document.getElementById('auth-local-fields').classList.toggle('hidden', val !== 'local');
5513
+ }
5514
+
5515
+ updatePreview();
5516
+ }
5517
+
5518
+ function getChipValue(groupId) {
5519
+ const active = document.querySelector('#' + groupId + ' .chip.active');
5520
+ return active ? active.dataset.value : '';
5521
+ }
5522
+
5523
+ function setChipValue(groupId, value) {
5524
+ const group = document.getElementById(groupId);
5525
+ if (!group) return;
5526
+ group.querySelectorAll('.chip').forEach(c => {
5527
+ c.classList.toggle('active', c.dataset.value === value);
5528
+ });
5529
+ }
5530
+
5531
+ // \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
5532
+ function addCustomPattern() {
5533
+ customPatterns.push({ name: '', pattern: '', severity: 'medium', action: 'audit' });
5534
+ renderCustomPatterns();
5535
+ }
5536
+
5537
+ function renderCustomPatterns() {
5538
+ const container = document.getElementById('dlp-custom-patterns');
5539
+ container.innerHTML = '';
5540
+ customPatterns.forEach((p, i) => {
5541
+ const div = document.createElement('div');
5542
+ div.className = 'pattern-item';
5543
+ div.innerHTML =
5544
+ '<input type="text" placeholder="Name" value="' + esc(p.name) + '" onchange="customPatterns[' + i + '].name=this.value;updatePreview()">' +
5545
+ '<input type="text" placeholder="Regex pattern" value="' + esc(p.pattern) + '" onchange="customPatterns[' + i + '].pattern=this.value;updatePreview()">' +
5546
+ '<select onchange="customPatterns[' + i + '].severity=this.value;updatePreview()">' +
5547
+ '<option value="low"' + (p.severity==='low'?' selected':'') + '>Low</option>' +
5548
+ '<option value="medium"' + (p.severity==='medium'?' selected':'') + '>Medium</option>' +
5549
+ '<option value="high"' + (p.severity==='high'?' selected':'') + '>High</option>' +
5550
+ '<option value="critical"' + (p.severity==='critical'?' selected':'') + '>Critical</option>' +
5551
+ '</select>' +
5552
+ '<button class="btn-remove" onclick="customPatterns.splice(' + i + ',1);renderCustomPatterns();updatePreview()">&times;</button>';
5553
+ container.appendChild(div);
5554
+ });
5555
+ }
5556
+
5557
+ // \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
5558
+ function addAllowlistEntry() {
5559
+ allowlistEntries.push('');
5560
+ renderAllowlist();
5561
+ }
5562
+
5563
+ function renderAllowlist() {
5564
+ const container = document.getElementById('dlp-allowlist');
5565
+ container.innerHTML = '';
5566
+ allowlistEntries.forEach((entry, i) => {
5567
+ const div = document.createElement('div');
5568
+ div.className = 'allowlist-item';
5569
+ div.innerHTML =
5570
+ '<input type="text" placeholder="Pattern to allow" value="' + esc(entry) + '" onchange="allowlistEntries[' + i + ']=this.value;updatePreview()">' +
5571
+ '<button class="btn-remove" onclick="allowlistEntries.splice(' + i + ',1);renderAllowlist();updatePreview()">&times;</button>';
5572
+ container.appendChild(div);
5573
+ });
5574
+ }
5575
+
5576
+ // \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
5577
+ function addAuditSink() {
5578
+ auditSinks.push({ type: 'jsonl', path: '' });
5579
+ renderAuditSinks();
5580
+ }
5581
+
5582
+ function renderAuditSinks() {
5583
+ const container = document.getElementById('audit-sinks');
5584
+ container.innerHTML = '';
5585
+ auditSinks.forEach((sink, i) => {
5586
+ const div = document.createElement('div');
5587
+ div.className = 'sink-item';
5588
+ let inner = '<button class="btn-remove" onclick="auditSinks.splice(' + i + ',1);renderAuditSinks();updatePreview()">&times;</button>';
5589
+ inner += '<div class="field"><label>Sink Type</label>' +
5590
+ '<select onchange="auditSinks[' + i + '].type=this.value;renderAuditSinks();updatePreview()">' +
5591
+ '<option value="jsonl"' + (sink.type==='jsonl'?' selected':'') + '>JSONL File</option>' +
5592
+ '<option value="webhook"' + (sink.type==='webhook'?' selected':'') + '>Webhook</option>' +
5593
+ '<option value="postgres"' + (sink.type==='postgres'?' selected':'') + '>PostgreSQL</option>' +
5594
+ '</select></div>';
5595
+ if (sink.type === 'jsonl') {
5596
+ inner += '<div class="field"><label>File Path</label>' +
5597
+ '<input type="text" value="' + esc(sink.path || '') + '" placeholder="~/.pi/agent/audit.jsonl" ' +
5598
+ 'onchange="auditSinks[' + i + '].path=this.value;updatePreview()"></div>';
5599
+ } else if (sink.type === 'webhook') {
5600
+ inner += '<div class="field"><label>Webhook URL</label>' +
5601
+ '<input type="text" value="' + esc(sink.url || '') + '" placeholder="https://..." ' +
5602
+ 'onchange="auditSinks[' + i + '].url=this.value;updatePreview()"></div>';
5603
+ } else if (sink.type === 'postgres') {
5604
+ inner += '<div class="field"><label>Connection String</label>' +
5605
+ '<input type="text" value="' + esc(sink.connection || '') + '" placeholder="postgresql://..." ' +
5606
+ 'onchange="auditSinks[' + i + '].connection=this.value;updatePreview()"></div>';
5607
+ }
5608
+ div.innerHTML = inner;
5609
+ container.appendChild(div);
5610
+ });
5611
+ }
5612
+
5613
+ // \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
5614
+ function switchTab(el) {
5615
+ document.querySelectorAll('.preview-tab').forEach(t => t.classList.remove('active'));
5616
+ el.classList.add('active');
5617
+ activePreviewTab = el.dataset.tab;
5618
+ updatePreview();
5619
+ }
5620
+
5621
+ // \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
5622
+ function toYaml(obj, indent) {
5623
+ indent = indent || 0;
5624
+ const pad = ' '.repeat(indent);
5625
+ let out = '';
5626
+
5627
+ if (Array.isArray(obj)) {
5628
+ if (obj.length === 0) return ' []\\n';
5629
+ for (const item of obj) {
5630
+ if (typeof item === 'object' && item !== null && !Array.isArray(item)) {
5631
+ const keys = Object.keys(item);
5632
+ if (keys.length > 0) {
5633
+ out += pad + '- ' + keys[0] + ': ' + formatScalar(item[keys[0]]) + '\\n';
5634
+ for (let k = 1; k < keys.length; k++) {
5635
+ const val = item[keys[k]];
5636
+ if (typeof val === 'object' && val !== null) {
5637
+ out += pad + ' ' + keys[k] + ':\\n' + toYaml(val, indent + 2);
5638
+ } else {
5639
+ out += pad + ' ' + keys[k] + ': ' + formatScalar(val) + '\\n';
5640
+ }
5641
+ }
5642
+ }
5643
+ } else {
5644
+ out += pad + '- ' + formatScalar(item) + '\\n';
5645
+ }
5646
+ }
5647
+ return out;
5648
+ }
5649
+
5650
+ if (typeof obj === 'object' && obj !== null) {
5651
+ for (const [key, val] of Object.entries(obj)) {
5652
+ if (val === undefined || val === null) continue;
5653
+ if (typeof val === 'object') {
5654
+ const yamlVal = toYaml(val, indent + 1);
5655
+ if (Array.isArray(val) && val.length === 0) {
5656
+ out += pad + key + ': []\\n';
5657
+ } else {
5658
+ out += pad + key + ':\\n' + yamlVal;
5659
+ }
5660
+ } else {
5661
+ out += pad + key + ': ' + formatScalar(val) + '\\n';
5662
+ }
5663
+ }
5664
+ return out;
5665
+ }
5666
+
5667
+ return pad + formatScalar(obj) + '\\n';
5668
+ }
5669
+
5670
+ function formatScalar(val) {
5671
+ if (typeof val === 'boolean') return val ? 'true' : 'false';
5672
+ if (typeof val === 'number') return String(val);
5673
+ if (typeof val === 'string') {
5674
+ if (val === '') return "''";
5675
+ if (val === 'true' || val === 'false' || !isNaN(val)) return "'" + val + "'";
5676
+ if (/[:#{}\\[\\],&*?|\\->!%@]/.test(val) || val.includes('\\\\')) return "'" + val.replace(/'/g, "''") + "'";
5677
+ return val;
5678
+ }
5679
+ return String(val);
5680
+ }
5681
+
5682
+ // \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
5683
+ function buildGovernanceConfig() {
5684
+ const cfg = {};
5685
+
5686
+ // Auth
5687
+ const authProvider = getChipValue('auth-provider');
5688
+ cfg.auth = { provider: authProvider };
5689
+ if (authProvider === 'env') {
5690
+ cfg.auth.env = {
5691
+ user_var: document.getElementById('auth-user-var').value || 'GRWND_USER',
5692
+ role_var: document.getElementById('auth-role-var').value || 'GRWND_ROLE',
5693
+ org_unit_var: document.getElementById('auth-org-unit-var').value || 'GRWND_ORG_UNIT'
5694
+ };
5695
+ } else if (authProvider === 'local') {
5696
+ cfg.auth.local = {
5697
+ users_file: document.getElementById('auth-users-file').value || './users.yaml'
5698
+ };
5699
+ }
5700
+
5701
+ // Policy
5702
+ cfg.policy = {
5703
+ engine: 'yaml',
5704
+ yaml: { rules_file: './governance-rules.yaml' }
5705
+ };
5706
+
5707
+ // HITL
5708
+ cfg.hitl = {
5709
+ default_mode: getChipValue('hitl-mode') || 'supervised',
5710
+ approval_channel: document.getElementById('hitl-channel').value,
5711
+ timeout_seconds: parseInt(document.getElementById('hitl-timeout').value) || 300
5712
+ };
5713
+ if (cfg.hitl.approval_channel === 'webhook') {
5714
+ const url = document.getElementById('hitl-webhook-url').value;
5715
+ if (url) cfg.hitl.webhook = { url: url };
5716
+ }
5717
+
5718
+ // Audit
5719
+ cfg.audit = { sinks: auditSinks.filter(s => {
5720
+ if (s.type === 'jsonl') return s.path;
5721
+ if (s.type === 'webhook') return s.url;
5722
+ if (s.type === 'postgres') return s.connection;
5723
+ return false;
5724
+ }).map(s => {
5725
+ if (s.type === 'jsonl') return { type: 'jsonl', path: s.path };
5726
+ if (s.type === 'webhook') return { type: 'webhook', url: s.url };
5727
+ if (s.type === 'postgres') return { type: 'postgres', connection: s.connection };
5728
+ return s;
5729
+ })};
5730
+
5731
+ // DLP
5732
+ if (document.getElementById('dlp-enabled').checked) {
5733
+ cfg.dlp = {
5734
+ enabled: true,
5735
+ mode: getChipValue('dlp-mode') || 'audit'
5736
+ };
5737
+ const onInput = document.getElementById('dlp-on-input').value;
5738
+ const onOutput = document.getElementById('dlp-on-output').value;
5739
+ if (onInput) cfg.dlp.on_input = onInput;
5740
+ if (onOutput) cfg.dlp.on_output = onOutput;
5741
+ cfg.dlp.masking = {
5742
+ strategy: document.getElementById('dlp-mask-strategy').value,
5743
+ show_chars: parseInt(document.getElementById('dlp-mask-show').value) || 4,
5744
+ placeholder: document.getElementById('dlp-mask-placeholder').value || '***'
5745
+ };
5746
+ cfg.dlp.severity_threshold = getChipValue('dlp-severity') || 'low';
5747
+ cfg.dlp.built_in = {
5748
+ secrets: document.getElementById('dlp-secrets').checked,
5749
+ pii: document.getElementById('dlp-pii').checked
5750
+ };
5751
+ const patterns = customPatterns.filter(p => p.name && p.pattern);
5752
+ if (patterns.length > 0) cfg.dlp.custom_patterns = patterns;
5753
+ const al = allowlistEntries.filter(Boolean);
5754
+ if (al.length > 0) cfg.dlp.allowlist = al.map(p => ({ pattern: p }));
5755
+ } else {
5756
+ cfg.dlp = { enabled: false };
5757
+ }
5758
+
5759
+ return cfg;
5760
+ }
5761
+
5762
+ function buildRulesConfig() {
5763
+ const roles = {};
5764
+ for (const [key, sel] of Object.entries(selectedRoles)) {
5765
+ if (!sel) continue;
5766
+ const r = PRESET_ROLES[key];
5767
+ const role = {
5768
+ allowed_tools: r.allowed_tools,
5769
+ blocked_tools: r.blocked_tools,
5770
+ prompt_template: r.prompt_template,
5771
+ execution_mode: r.execution_mode,
5772
+ human_approval: r.human_approval,
5773
+ token_budget_daily: r.token_budget_daily,
5774
+ allowed_paths: r.allowed_paths,
5775
+ blocked_paths: r.blocked_paths
5776
+ };
5777
+ if (r.bash_overrides) role.bash_overrides = r.bash_overrides;
5778
+ roles[key] = role;
5779
+ }
5780
+ return { roles: roles };
5781
+ }
5782
+
5783
+ // \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
5784
+ function updatePreview() {
5785
+ const el = document.getElementById('preview-yaml');
5786
+ if (activePreviewTab === 'governance') {
5787
+ const cfg = buildGovernanceConfig();
5788
+ el.textContent = '# governance.yaml\\n# Generated by Pi Governance Setup Wizard\\n\\n' + toYaml(cfg);
5789
+ } else {
5790
+ const rules = buildRulesConfig();
5791
+ el.textContent = '# governance-rules.yaml\\n# Generated by Pi Governance Setup Wizard\\n\\n' + toYaml(rules);
5792
+ }
5793
+ }
5794
+
5795
+ // \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
5796
+ async function handleSave() {
5797
+ const btn = document.getElementById('btn-save');
5798
+ const status = document.getElementById('save-status');
5799
+ btn.disabled = true;
5800
+ status.textContent = 'Saving...';
5801
+ status.className = 'save-status';
5802
+
5803
+ try {
5804
+ const payload = {
5805
+ governance: buildGovernanceConfig(),
5806
+ rules: buildRulesConfig()
5807
+ };
5808
+ const res = await fetch('/api/save', {
5809
+ method: 'POST',
5810
+ headers: { 'Content-Type': 'application/json' },
5811
+ body: JSON.stringify(payload)
5812
+ });
5813
+ if (!res.ok) {
5814
+ const err = await res.text();
5815
+ throw new Error(err || 'Server error');
5816
+ }
5817
+ const result = await res.json();
5818
+ showToast('Configuration saved!', 'success');
5819
+ status.textContent = 'Saved: ' + (result.files || []).join(', ');
5820
+ status.className = 'save-status success';
5821
+ } catch (e) {
5822
+ showToast('Failed to save: ' + e.message, 'error');
5823
+ status.textContent = 'Error: ' + e.message;
5824
+ status.className = 'save-status error';
5825
+ } finally {
5826
+ btn.disabled = false;
5827
+ }
5828
+ }
5829
+
5830
+ async function handleClose() {
5831
+ try { await fetch('/api/close', { method: 'POST' }); } catch (e) { /* ignore */ }
5832
+ window.close();
5833
+ }
5834
+
5835
+ // \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
5836
+ function showToast(msg, type) {
5837
+ const toast = document.getElementById('toast');
5838
+ toast.textContent = msg;
5839
+ toast.className = 'toast ' + type + ' visible';
5840
+ setTimeout(() => { toast.classList.remove('visible'); }, 3000);
5841
+ }
5842
+
5843
+ // \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
5844
+ function esc(s) {
5845
+ return String(s).replace(/&/g,'&amp;').replace(/"/g,'&quot;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
5846
+ }
5847
+ </script>
5848
+ </body>
5849
+ </html>`;
5850
+
5851
+ // src/lib/wizard/server.ts
5852
+ var AUTO_SHUTDOWN_MS = 10 * 60 * 1e3;
5853
+ var BUILTIN_ROLES = {
5854
+ analyst: {
5855
+ allowed_tools: ["read", "grep", "find", "ls"],
5856
+ blocked_tools: ["bash", "write", "edit"],
5857
+ hitl_mode: "dry_run"
5858
+ },
5859
+ project_lead: {
5860
+ allowed_tools: ["read", "write", "edit", "grep", "find", "ls", "bash"],
5861
+ blocked_tools: [],
5862
+ hitl_mode: "supervised"
5863
+ },
5864
+ admin: {
5865
+ allowed_tools: ["read", "write", "edit", "grep", "find", "ls", "bash"],
5866
+ blocked_tools: [],
5867
+ hitl_mode: "autonomous"
5868
+ },
5869
+ auditor: {
5870
+ allowed_tools: ["read", "grep", "find", "ls"],
5871
+ blocked_tools: ["bash", "write", "edit"],
5872
+ hitl_mode: "dry_run"
5873
+ }
5874
+ };
5875
+ function setCorsHeaders(res) {
5876
+ res.setHeader("Access-Control-Allow-Origin", "http://localhost");
5877
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
5878
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type");
5879
+ }
5880
+ function sendJson(res, status, data) {
5881
+ res.writeHead(status, { "Content-Type": "application/json" });
5882
+ res.end(JSON.stringify(data));
5883
+ }
5884
+ function readBody(req) {
5885
+ return new Promise((resolve2, reject) => {
5886
+ const chunks = [];
5887
+ req.on("data", (chunk) => chunks.push(chunk));
5888
+ req.on("end", () => resolve2(Buffer.concat(chunks).toString("utf-8")));
5889
+ req.on("error", reject);
5890
+ });
5891
+ }
5892
+ function startWizardServer(options) {
5893
+ return new Promise((resolve2, reject) => {
5894
+ let shutdownTimer;
5895
+ const server = (0, import_node_http.createServer)((req, res) => {
5896
+ setCorsHeaders(res);
5897
+ if (req.method === "OPTIONS") {
5898
+ res.writeHead(204);
5899
+ res.end();
5900
+ return;
5901
+ }
5902
+ const url = req.url ?? "/";
5903
+ try {
5904
+ if (req.method === "GET" && url === "/") {
5905
+ res.writeHead(200, { "Content-Type": "text/html" });
5906
+ res.end(WIZARD_HTML);
5907
+ return;
5908
+ }
5909
+ if (req.method === "GET" && url === "/api/config") {
5910
+ sendJson(res, 200, options.existingConfig ?? DEFAULTS);
5911
+ return;
5912
+ }
5913
+ if (req.method === "GET" && url === "/api/defaults") {
5914
+ sendJson(res, 200, { defaults: DEFAULTS, roles: BUILTIN_ROLES });
5915
+ return;
5916
+ }
5917
+ if (req.method === "POST" && url === "/api/save") {
5918
+ readBody(req).then((body) => {
5919
+ const parsed = JSON.parse(body);
5920
+ if (typeof parsed !== "object" || parsed === null || !("governance" in parsed)) {
5921
+ sendJson(res, 400, { error: 'Request body must include "governance" property' });
5922
+ return;
5923
+ }
5924
+ const payload = parsed;
5925
+ const governanceYaml = (0, import_yaml6.stringify)(payload.governance);
5926
+ const piDir = (0, import_node_path3.join)(options.workingDirectory, ".pi");
5927
+ const governancePath = (0, import_node_path3.join)(piDir, "governance.yaml");
5928
+ (0, import_node_fs4.mkdirSync)(piDir, { recursive: true });
5929
+ (0, import_node_fs4.writeFileSync)(governancePath, governanceYaml, "utf-8");
5930
+ const files = [
5931
+ { path: governancePath, content: governanceYaml }
5932
+ ];
5933
+ if (payload.rules !== void 0) {
5934
+ const rulesYaml = (0, import_yaml6.stringify)(payload.rules);
5935
+ const rulesPath = (0, import_node_path3.join)(options.workingDirectory, "governance-rules.yaml");
5936
+ (0, import_node_fs4.writeFileSync)(rulesPath, rulesYaml, "utf-8");
5937
+ files.push({ path: rulesPath, content: rulesYaml });
5938
+ }
5939
+ sendJson(res, 200, { ok: true, files: files.map((f) => f.path) });
5940
+ options.onComplete(files);
5941
+ }).catch((err) => {
5942
+ const message = err instanceof Error ? err.message : String(err);
5943
+ sendJson(res, 400, { error: `Invalid request body: ${message}` });
5944
+ });
5945
+ return;
5946
+ }
5947
+ if (req.method === "POST" && url === "/api/close") {
5948
+ sendJson(res, 200, { ok: true });
5949
+ setTimeout(() => {
5950
+ closeServer();
5951
+ }, 100);
5952
+ return;
5953
+ }
5954
+ sendJson(res, 404, { error: "Not found" });
5955
+ } catch (err) {
5956
+ const error = err instanceof Error ? err : new Error(String(err));
5957
+ options.onError(error);
5958
+ sendJson(res, 500, { error: "Internal server error" });
5959
+ }
5960
+ });
5961
+ function closeServer() {
5962
+ if (shutdownTimer !== void 0) {
5963
+ clearTimeout(shutdownTimer);
5964
+ shutdownTimer = void 0;
5965
+ }
5966
+ server.close();
5967
+ }
5968
+ server.on("error", (err) => {
5969
+ options.onError(err);
5970
+ reject(err);
5971
+ });
5972
+ server.listen(0, () => {
5973
+ const addr = server.address();
5974
+ if (addr === null || typeof addr === "string") {
5975
+ const err = new Error("Failed to get server address");
5976
+ options.onError(err);
5977
+ reject(err);
5978
+ return;
5979
+ }
5980
+ shutdownTimer = setTimeout(() => {
5981
+ closeServer();
5982
+ }, AUTO_SHUTDOWN_MS);
5983
+ resolve2({ port: addr.port, close: closeServer });
5984
+ });
5985
+ });
5986
+ }
4378
5987
  // Annotate the CommonJS export names for ESM import in node:
4379
5988
  0 && (module.exports = {
4380
5989
  AuditLogger,
@@ -4404,6 +6013,7 @@ function createApprovalFlow(config, ui) {
4404
6013
  createIdentityChain,
4405
6014
  createPolicyEngine,
4406
6015
  loadConfig,
4407
- renderTemplate
6016
+ renderTemplate,
6017
+ startWizardServer
4408
6018
  });
4409
6019
  //# sourceMappingURL=index.cjs.map