@sanctuary-framework/mcp-server 0.4.2 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -4058,550 +4058,1380 @@ function generateDashboardHTML(options) {
4058
4058
  <meta charset="utf-8">
4059
4059
  <meta name="viewport" content="width=device-width, initial-scale=1">
4060
4060
  <title>Sanctuary \u2014 Principal Dashboard</title>
4061
+ <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600&display=swap" rel="stylesheet">
4061
4062
  <style>
4062
4063
  :root {
4063
- --bg: #0f1117;
4064
- --bg-surface: #1a1d27;
4065
- --bg-elevated: #242736;
4066
- --border: #2e3244;
4067
- --text: #e4e6f0;
4068
- --text-muted: #8b8fa3;
4069
- --accent: #6c8aff;
4070
- --accent-hover: #839dff;
4071
- --approve: #3ecf8e;
4072
- --approve-hover: #5dd9a3;
4073
- --deny: #f87171;
4074
- --deny-hover: #fca5a5;
4075
- --warning: #fbbf24;
4076
- --tier1: #f87171;
4077
- --tier2: #fbbf24;
4078
- --tier3: #3ecf8e;
4079
- --font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
4080
- --mono: "SF Mono", "Fira Code", "Cascadia Code", monospace;
4081
- --radius: 8px;
4082
- }
4083
-
4084
- * { box-sizing: border-box; margin: 0; padding: 0; }
4064
+ --bg: #0d1117;
4065
+ --surface: #161b22;
4066
+ --border: #30363d;
4067
+ --text-primary: #e6edf3;
4068
+ --text-secondary: #8b949e;
4069
+ --green: #3fb950;
4070
+ --amber: #d29922;
4071
+ --red: #f85149;
4072
+ --blue: #58a6ff;
4073
+ --mono: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
4074
+ --sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
4075
+ --radius: 6px;
4076
+ }
4077
+
4078
+ * {
4079
+ box-sizing: border-box;
4080
+ margin: 0;
4081
+ padding: 0;
4082
+ }
4083
+
4084
+ html, body {
4085
+ width: 100%;
4086
+ height: 100%;
4087
+ overflow: hidden;
4088
+ }
4089
+
4085
4090
  body {
4086
- font-family: var(--font);
4091
+ font-family: var(--sans);
4087
4092
  background: var(--bg);
4088
- color: var(--text);
4089
- line-height: 1.5;
4090
- min-height: 100vh;
4093
+ color: var(--text-primary);
4094
+ display: flex;
4095
+ flex-direction: column;
4091
4096
  }
4092
4097
 
4093
- /* Layout */
4094
- .container { max-width: 960px; margin: 0 auto; padding: 24px 16px; }
4098
+ /* \u2500\u2500 Top Status Bar (fixed) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
4095
4099
 
4096
- header {
4097
- display: flex; align-items: center; justify-content: space-between;
4098
- padding-bottom: 20px; border-bottom: 1px solid var(--border);
4099
- margin-bottom: 24px;
4100
+ .status-bar {
4101
+ position: fixed;
4102
+ top: 0;
4103
+ left: 0;
4104
+ right: 0;
4105
+ height: 56px;
4106
+ background: var(--surface);
4107
+ border-bottom: 1px solid var(--border);
4108
+ display: flex;
4109
+ align-items: center;
4110
+ padding: 0 20px;
4111
+ gap: 24px;
4112
+ z-index: 1000;
4113
+ }
4114
+
4115
+ .status-bar-left {
4116
+ display: flex;
4117
+ align-items: center;
4118
+ gap: 12px;
4119
+ flex: 0 0 auto;
4100
4120
  }
4101
- header h1 { font-size: 20px; font-weight: 600; letter-spacing: -0.3px; }
4102
- header h1 span { color: var(--accent); }
4103
- .status-badge {
4104
- display: inline-flex; align-items: center; gap: 6px;
4105
- font-size: 12px; color: var(--text-muted);
4106
- padding: 4px 10px; border-radius: 12px;
4107
- background: var(--bg-surface); border: 1px solid var(--border);
4121
+
4122
+ .sanctuary-logo {
4123
+ font-weight: 700;
4124
+ font-size: 16px;
4125
+ letter-spacing: -0.5px;
4126
+ color: var(--text-primary);
4127
+ }
4128
+
4129
+ .sanctuary-logo span {
4130
+ color: var(--blue);
4108
4131
  }
4132
+
4133
+ .version {
4134
+ font-size: 11px;
4135
+ color: var(--text-secondary);
4136
+ font-family: var(--mono);
4137
+ }
4138
+
4139
+ .status-bar-center {
4140
+ flex: 1;
4141
+ display: flex;
4142
+ align-items: center;
4143
+ justify-content: center;
4144
+ }
4145
+
4146
+ .sovereignty-badge {
4147
+ display: flex;
4148
+ align-items: center;
4149
+ gap: 8px;
4150
+ padding: 6px 12px;
4151
+ background: rgba(88, 166, 255, 0.1);
4152
+ border: 1px solid var(--blue);
4153
+ border-radius: 20px;
4154
+ font-size: 13px;
4155
+ font-weight: 600;
4156
+ }
4157
+
4158
+ .sovereignty-score {
4159
+ display: flex;
4160
+ align-items: center;
4161
+ justify-content: center;
4162
+ width: 28px;
4163
+ height: 28px;
4164
+ border-radius: 50%;
4165
+ font-family: var(--mono);
4166
+ font-weight: 700;
4167
+ font-size: 12px;
4168
+ background: var(--blue);
4169
+ color: var(--bg);
4170
+ }
4171
+
4172
+ .sovereignty-score.high {
4173
+ background: var(--green);
4174
+ }
4175
+
4176
+ .sovereignty-score.medium {
4177
+ background: var(--amber);
4178
+ }
4179
+
4180
+ .sovereignty-score.low {
4181
+ background: var(--red);
4182
+ }
4183
+
4184
+ .status-bar-right {
4185
+ display: flex;
4186
+ align-items: center;
4187
+ gap: 16px;
4188
+ flex: 0 0 auto;
4189
+ }
4190
+
4191
+ .protections-indicator {
4192
+ display: flex;
4193
+ align-items: center;
4194
+ gap: 6px;
4195
+ font-size: 12px;
4196
+ color: var(--text-secondary);
4197
+ font-family: var(--mono);
4198
+ }
4199
+
4200
+ .protections-indicator .count {
4201
+ color: var(--text-primary);
4202
+ font-weight: 600;
4203
+ }
4204
+
4205
+ .uptime {
4206
+ display: flex;
4207
+ align-items: center;
4208
+ gap: 6px;
4209
+ font-size: 12px;
4210
+ color: var(--text-secondary);
4211
+ font-family: var(--mono);
4212
+ }
4213
+
4109
4214
  .status-dot {
4110
- width: 8px; height: 8px; border-radius: 50%;
4111
- background: var(--approve); animation: pulse 2s infinite;
4112
- }
4113
- .status-dot.disconnected { background: var(--deny); animation: none; }
4114
- @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
4115
-
4116
- /* Tabs */
4117
- .tabs {
4118
- display: flex; gap: 2px; margin-bottom: 20px;
4119
- background: var(--bg-surface); border-radius: var(--radius);
4120
- padding: 3px; border: 1px solid var(--border);
4121
- }
4122
- .tab {
4123
- flex: 1; padding: 8px 12px; text-align: center;
4124
- font-size: 13px; font-weight: 500; cursor: pointer;
4125
- border-radius: 6px; border: none; color: var(--text-muted);
4126
- background: transparent; transition: all 0.15s;
4127
- }
4128
- .tab:hover { color: var(--text); }
4129
- .tab.active { background: var(--bg-elevated); color: var(--text); }
4130
- .tab .count {
4131
- display: inline-flex; align-items: center; justify-content: center;
4132
- min-width: 18px; height: 18px; padding: 0 5px;
4133
- font-size: 11px; font-weight: 600; border-radius: 9px;
4134
- margin-left: 6px;
4135
- }
4136
- .tab .count.alert { background: var(--deny); color: white; }
4137
- .tab .count.muted { background: var(--border); color: var(--text-muted); }
4138
-
4139
- /* Tab Content */
4140
- .tab-content { display: none; }
4141
- .tab-content.active { display: block; }
4142
-
4143
- /* Pending Requests */
4144
- .pending-empty {
4145
- text-align: center; padding: 60px 20px; color: var(--text-muted);
4146
- }
4147
- .pending-empty .icon { font-size: 32px; margin-bottom: 12px; }
4148
- .pending-empty p { font-size: 14px; }
4149
-
4150
- .request-card {
4151
- background: var(--bg-surface); border: 1px solid var(--border);
4152
- border-radius: var(--radius); padding: 16px; margin-bottom: 12px;
4153
- animation: slideIn 0.2s ease-out;
4154
- }
4155
- @keyframes slideIn { from { opacity: 0; transform: translateY(-8px); } to { opacity: 1; transform: translateY(0); } }
4156
- .request-card.tier1 { border-left: 3px solid var(--tier1); }
4157
- .request-card.tier2 { border-left: 3px solid var(--tier2); }
4158
- .request-header {
4159
- display: flex; align-items: center; justify-content: space-between;
4160
- margin-bottom: 10px;
4161
- }
4162
- .request-op {
4163
- font-family: var(--mono); font-size: 14px; font-weight: 600;
4164
- }
4165
- .tier-badge {
4166
- font-size: 11px; font-weight: 600; padding: 2px 8px;
4167
- border-radius: 4px; text-transform: uppercase;
4168
- }
4169
- .tier-badge.tier1 { background: rgba(248,113,113,0.15); color: var(--tier1); }
4170
- .tier-badge.tier2 { background: rgba(251,191,36,0.15); color: var(--tier2); }
4171
- .request-reason {
4172
- font-size: 13px; color: var(--text-muted); margin-bottom: 12px;
4173
- }
4174
- .request-context {
4175
- font-family: var(--mono); font-size: 12px; color: var(--text-muted);
4176
- background: var(--bg); border-radius: 4px; padding: 8px 10px;
4177
- margin-bottom: 14px; white-space: pre-wrap; word-break: break-all;
4178
- max-height: 120px; overflow-y: auto;
4179
- }
4180
- .request-actions {
4181
- display: flex; align-items: center; gap: 10px;
4215
+ width: 8px;
4216
+ height: 8px;
4217
+ border-radius: 50%;
4218
+ background: var(--green);
4219
+ animation: pulse 2s ease-in-out infinite;
4182
4220
  }
4183
- .btn {
4184
- padding: 7px 16px; border-radius: 6px; font-size: 13px;
4185
- font-weight: 600; border: none; cursor: pointer;
4186
- transition: all 0.15s;
4221
+
4222
+ .status-dot.disconnected {
4223
+ background: var(--red);
4224
+ animation: none;
4225
+ }
4226
+
4227
+ @keyframes pulse {
4228
+ 0%, 100% { opacity: 1; }
4229
+ 50% { opacity: 0.5; }
4230
+ }
4231
+
4232
+ .pending-badge {
4233
+ display: inline-flex;
4234
+ align-items: center;
4235
+ justify-content: center;
4236
+ min-width: 24px;
4237
+ height: 24px;
4238
+ padding: 0 6px;
4239
+ background: var(--red);
4240
+ color: white;
4241
+ border-radius: 12px;
4242
+ font-size: 11px;
4243
+ font-weight: 700;
4244
+ animation: pulse 1s ease-in-out infinite;
4245
+ }
4246
+
4247
+ .pending-badge.hidden {
4248
+ display: none;
4249
+ }
4250
+
4251
+ /* \u2500\u2500 Main Layout \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
4252
+
4253
+ .main-container {
4254
+ flex: 1;
4255
+ display: flex;
4256
+ margin-top: 56px;
4257
+ overflow: hidden;
4187
4258
  }
4188
- .btn-approve { background: var(--approve); color: #0f1117; }
4189
- .btn-approve:hover { background: var(--approve-hover); }
4190
- .btn-deny { background: var(--deny); color: white; }
4191
- .btn-deny:hover { background: var(--deny-hover); }
4192
- .countdown {
4193
- margin-left: auto; font-size: 12px; color: var(--text-muted);
4259
+
4260
+ .activity-feed {
4261
+ flex: 3;
4262
+ display: flex;
4263
+ flex-direction: column;
4264
+ border-right: 1px solid var(--border);
4265
+ overflow: hidden;
4266
+ }
4267
+
4268
+ .feed-header {
4269
+ padding: 16px 20px;
4270
+ border-bottom: 1px solid var(--border);
4271
+ display: flex;
4272
+ align-items: center;
4273
+ gap: 8px;
4274
+ font-size: 12px;
4275
+ font-weight: 600;
4276
+ text-transform: uppercase;
4277
+ letter-spacing: 0.5px;
4278
+ color: var(--text-secondary);
4279
+ }
4280
+
4281
+ .feed-header-dot {
4282
+ width: 6px;
4283
+ height: 6px;
4284
+ border-radius: 50%;
4285
+ background: var(--green);
4286
+ }
4287
+
4288
+ .activity-list {
4289
+ flex: 1;
4290
+ overflow-y: auto;
4291
+ overflow-x: hidden;
4292
+ }
4293
+
4294
+ .activity-item {
4295
+ padding: 12px 20px;
4296
+ border-bottom: 1px solid rgba(48, 54, 61, 0.5);
4297
+ font-size: 13px;
4194
4298
  font-family: var(--mono);
4299
+ cursor: pointer;
4300
+ transition: background 0.15s;
4301
+ display: flex;
4302
+ align-items: flex-start;
4303
+ gap: 10px;
4304
+ }
4305
+
4306
+ .activity-item:hover {
4307
+ background: rgba(88, 166, 255, 0.05);
4308
+ }
4309
+
4310
+ .activity-item-icon {
4311
+ flex: 0 0 auto;
4312
+ width: 16px;
4313
+ text-align: center;
4314
+ font-size: 12px;
4315
+ color: var(--text-secondary);
4316
+ margin-top: 1px;
4317
+ }
4318
+
4319
+ .activity-item-content {
4320
+ flex: 1;
4321
+ min-width: 0;
4322
+ }
4323
+
4324
+ .activity-time {
4325
+ color: var(--text-secondary);
4326
+ font-size: 11px;
4327
+ margin-bottom: 2px;
4328
+ }
4329
+
4330
+ .activity-main {
4331
+ display: flex;
4332
+ gap: 8px;
4333
+ align-items: baseline;
4334
+ margin-bottom: 4px;
4335
+ }
4336
+
4337
+ .activity-tier {
4338
+ display: inline-flex;
4339
+ align-items: center;
4340
+ justify-content: center;
4341
+ width: 24px;
4342
+ height: 16px;
4343
+ font-size: 10px;
4344
+ font-weight: 700;
4345
+ border-radius: 3px;
4346
+ text-transform: uppercase;
4347
+ flex: 0 0 auto;
4348
+ }
4349
+
4350
+ .activity-tier.t1 {
4351
+ background: rgba(248, 81, 73, 0.2);
4352
+ color: var(--red);
4353
+ }
4354
+
4355
+ .activity-tier.t2 {
4356
+ background: rgba(210, 153, 34, 0.2);
4357
+ color: var(--amber);
4358
+ }
4359
+
4360
+ .activity-tier.t3 {
4361
+ background: rgba(63, 185, 80, 0.2);
4362
+ color: var(--green);
4363
+ }
4364
+
4365
+ .activity-tool {
4366
+ color: var(--text-primary);
4367
+ font-weight: 600;
4368
+ }
4369
+
4370
+ .activity-outcome {
4371
+ color: var(--green);
4372
+ }
4373
+
4374
+ .activity-outcome.denied {
4375
+ color: var(--red);
4376
+ }
4377
+
4378
+ .activity-detail {
4379
+ font-size: 12px;
4380
+ color: var(--text-secondary);
4381
+ margin-left: 0;
4382
+ }
4383
+
4384
+ .activity-item.expanded .activity-detail {
4385
+ display: block;
4386
+ margin-top: 8px;
4387
+ padding: 10px;
4388
+ background: rgba(88, 166, 255, 0.08);
4389
+ border-left: 2px solid var(--blue);
4390
+ border-radius: 4px;
4195
4391
  }
4196
- .countdown.urgent { color: var(--deny); font-weight: 600; }
4197
4392
 
4198
- /* Audit Log */
4199
- .audit-table { width: 100%; border-collapse: collapse; }
4200
- .audit-table th {
4201
- text-align: left; font-size: 11px; font-weight: 600;
4202
- text-transform: uppercase; letter-spacing: 0.5px;
4203
- color: var(--text-muted); padding: 8px 10px;
4393
+ .activity-empty {
4394
+ display: flex;
4395
+ flex-direction: column;
4396
+ align-items: center;
4397
+ justify-content: center;
4398
+ height: 100%;
4399
+ color: var(--text-secondary);
4400
+ }
4401
+
4402
+ .activity-empty-icon {
4403
+ font-size: 32px;
4404
+ margin-bottom: 12px;
4405
+ }
4406
+
4407
+ .activity-empty-text {
4408
+ font-size: 14px;
4409
+ }
4410
+
4411
+ /* \u2500\u2500 Protection Status Sidebar (40%) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
4412
+
4413
+ .protection-sidebar {
4414
+ flex: 2;
4415
+ display: flex;
4416
+ flex-direction: column;
4417
+ background: rgba(22, 27, 34, 0.5);
4418
+ overflow: hidden;
4419
+ }
4420
+
4421
+ .sidebar-header {
4422
+ padding: 16px 20px;
4204
4423
  border-bottom: 1px solid var(--border);
4424
+ font-size: 12px;
4425
+ font-weight: 600;
4426
+ text-transform: uppercase;
4427
+ letter-spacing: 0.5px;
4428
+ color: var(--text-secondary);
4429
+ display: flex;
4430
+ align-items: center;
4431
+ gap: 8px;
4432
+ }
4433
+
4434
+ .sidebar-content {
4435
+ flex: 1;
4436
+ overflow-y: auto;
4437
+ padding: 16px 16px;
4438
+ display: grid;
4439
+ grid-template-columns: 1fr 1fr;
4440
+ gap: 12px;
4441
+ }
4442
+
4443
+ .protection-card {
4444
+ background: var(--surface);
4445
+ border: 1px solid var(--border);
4446
+ border-radius: var(--radius);
4447
+ padding: 14px;
4448
+ display: flex;
4449
+ flex-direction: column;
4450
+ gap: 8px;
4451
+ }
4452
+
4453
+ .protection-card-icon {
4454
+ font-size: 14px;
4455
+ }
4456
+
4457
+ .protection-card-label {
4458
+ font-size: 11px;
4459
+ font-weight: 600;
4460
+ text-transform: uppercase;
4461
+ letter-spacing: 0.5px;
4462
+ color: var(--text-secondary);
4463
+ }
4464
+
4465
+ .protection-card-status {
4466
+ display: flex;
4467
+ align-items: center;
4468
+ gap: 6px;
4469
+ font-size: 12px;
4470
+ font-weight: 600;
4471
+ }
4472
+
4473
+ .protection-card-status.active {
4474
+ color: var(--green);
4475
+ }
4476
+
4477
+ .protection-card-status.inactive {
4478
+ color: var(--text-secondary);
4205
4479
  }
4206
- .audit-table td {
4207
- font-size: 13px; padding: 8px 10px;
4480
+
4481
+ .protection-card-stat {
4482
+ font-size: 11px;
4483
+ color: var(--text-secondary);
4484
+ font-family: var(--mono);
4485
+ margin-top: 4px;
4486
+ }
4487
+
4488
+ /* \u2500\u2500 Pending Approvals Overlay \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
4489
+
4490
+ .pending-overlay {
4491
+ position: fixed;
4492
+ top: 56px;
4493
+ right: 0;
4494
+ bottom: 0;
4495
+ width: 0;
4496
+ background: var(--surface);
4497
+ border-left: 1px solid var(--border);
4498
+ z-index: 999;
4499
+ overflow-y: auto;
4500
+ transition: width 0.3s ease-out;
4501
+ display: flex;
4502
+ flex-direction: column;
4503
+ }
4504
+
4505
+ .pending-overlay.active {
4506
+ width: 380px;
4507
+ }
4508
+
4509
+ @media (max-width: 1400px) {
4510
+ .pending-overlay.active {
4511
+ width: 100%;
4512
+ right: auto;
4513
+ left: 0;
4514
+ }
4515
+ }
4516
+
4517
+ .pending-overlay-header {
4518
+ padding: 16px 20px;
4208
4519
  border-bottom: 1px solid var(--border);
4520
+ display: flex;
4521
+ align-items: center;
4522
+ justify-content: space-between;
4523
+ flex: 0 0 auto;
4524
+ }
4525
+
4526
+ .pending-overlay-title {
4527
+ font-size: 13px;
4528
+ font-weight: 600;
4529
+ text-transform: uppercase;
4530
+ letter-spacing: 0.5px;
4531
+ color: var(--text-primary);
4532
+ }
4533
+
4534
+ .pending-overlay-close {
4535
+ background: none;
4536
+ border: none;
4537
+ color: var(--text-secondary);
4538
+ cursor: pointer;
4539
+ font-size: 18px;
4540
+ padding: 0;
4541
+ display: flex;
4542
+ align-items: center;
4543
+ justify-content: center;
4544
+ }
4545
+
4546
+ .pending-overlay-close:hover {
4547
+ color: var(--text-primary);
4548
+ }
4549
+
4550
+ .pending-list {
4551
+ flex: 1;
4552
+ overflow-y: auto;
4553
+ }
4554
+
4555
+ .pending-item {
4556
+ padding: 16px 20px;
4557
+ border-bottom: 1px solid rgba(48, 54, 61, 0.5);
4558
+ display: flex;
4559
+ flex-direction: column;
4560
+ gap: 10px;
4209
4561
  }
4210
- .audit-table tr { transition: background 0.1s; }
4211
- .audit-table tr:hover { background: var(--bg-elevated); }
4212
- .audit-table tr.new { animation: highlight 1s ease-out; }
4213
- @keyframes highlight { from { background: rgba(108,138,255,0.15); } to { background: transparent; } }
4214
- .audit-time { font-family: var(--mono); font-size: 12px; color: var(--text-muted); }
4215
- .audit-op { font-family: var(--mono); font-size: 12px; }
4216
- .audit-layer {
4217
- font-size: 11px; font-weight: 600; padding: 1px 6px;
4218
- border-radius: 3px; text-transform: uppercase;
4219
- }
4220
- .audit-layer.l1 { background: rgba(108,138,255,0.15); color: var(--accent); }
4221
- .audit-layer.l2 { background: rgba(251,191,36,0.15); color: var(--tier2); }
4222
- .audit-layer.l3 { background: rgba(62,207,142,0.15); color: var(--tier3); }
4223
- .audit-layer.l4 { background: rgba(168,85,247,0.15); color: #a855f7; }
4224
-
4225
- /* Baseline & Policy */
4226
- .info-section {
4227
- background: var(--bg-surface); border: 1px solid var(--border);
4228
- border-radius: var(--radius); padding: 16px; margin-bottom: 16px;
4229
- }
4230
- .info-section h3 {
4231
- font-size: 13px; font-weight: 600; text-transform: uppercase;
4232
- letter-spacing: 0.5px; color: var(--text-muted); margin-bottom: 12px;
4233
- }
4234
- .info-row {
4235
- display: flex; justify-content: space-between; align-items: center;
4236
- padding: 6px 0; font-size: 13px;
4237
- }
4238
- .info-label { color: var(--text-muted); }
4239
- .info-value { font-family: var(--mono); font-size: 12px; }
4240
- .tag-list { display: flex; flex-wrap: wrap; gap: 4px; }
4241
- .tag {
4242
- font-family: var(--mono); font-size: 11px; padding: 2px 8px;
4243
- background: var(--bg-elevated); border-radius: 4px;
4244
- color: var(--text-muted); border: 1px solid var(--border);
4245
- }
4246
- .policy-op {
4247
- font-family: var(--mono); font-size: 12px; padding: 3px 0;
4248
- }
4249
-
4250
- /* Footer */
4251
- footer {
4252
- margin-top: 32px; padding-top: 16px;
4562
+
4563
+ .pending-item-header {
4564
+ display: flex;
4565
+ align-items: center;
4566
+ gap: 8px;
4567
+ }
4568
+
4569
+ .pending-item-op {
4570
+ font-family: var(--mono);
4571
+ font-size: 12px;
4572
+ font-weight: 600;
4573
+ color: var(--text-primary);
4574
+ flex: 1;
4575
+ }
4576
+
4577
+ .pending-item-tier {
4578
+ display: inline-flex;
4579
+ align-items: center;
4580
+ justify-content: center;
4581
+ width: 28px;
4582
+ height: 20px;
4583
+ font-size: 9px;
4584
+ font-weight: 700;
4585
+ border-radius: 3px;
4586
+ text-transform: uppercase;
4587
+ color: white;
4588
+ }
4589
+
4590
+ .pending-item-tier.tier1 {
4591
+ background: var(--red);
4592
+ }
4593
+
4594
+ .pending-item-tier.tier2 {
4595
+ background: var(--amber);
4596
+ }
4597
+
4598
+ .pending-item-reason {
4599
+ font-size: 12px;
4600
+ color: var(--text-secondary);
4601
+ }
4602
+
4603
+ .pending-item-timer {
4604
+ display: flex;
4605
+ align-items: center;
4606
+ gap: 6px;
4607
+ font-size: 11px;
4608
+ font-family: var(--mono);
4609
+ color: var(--text-secondary);
4610
+ }
4611
+
4612
+ .pending-item-timer-bar {
4613
+ flex: 1;
4614
+ height: 4px;
4615
+ background: rgba(48, 54, 61, 0.8);
4616
+ border-radius: 2px;
4617
+ overflow: hidden;
4618
+ }
4619
+
4620
+ .pending-item-timer-fill {
4621
+ height: 100%;
4622
+ background: var(--blue);
4623
+ transition: width 0.1s linear;
4624
+ }
4625
+
4626
+ .pending-item-timer.urgent .pending-item-timer-fill {
4627
+ background: var(--red);
4628
+ }
4629
+
4630
+ .pending-item-actions {
4631
+ display: flex;
4632
+ gap: 8px;
4633
+ }
4634
+
4635
+ .btn {
4636
+ flex: 1;
4637
+ padding: 8px 12px;
4638
+ border: none;
4639
+ border-radius: var(--radius);
4640
+ font-size: 12px;
4641
+ font-weight: 600;
4642
+ cursor: pointer;
4643
+ transition: all 0.15s;
4644
+ font-family: var(--sans);
4645
+ }
4646
+
4647
+ .btn-approve {
4648
+ background: var(--green);
4649
+ color: var(--bg);
4650
+ }
4651
+
4652
+ .btn-approve:hover {
4653
+ background: #4ecf5e;
4654
+ }
4655
+
4656
+ .btn-deny {
4657
+ background: var(--red);
4658
+ color: white;
4659
+ }
4660
+
4661
+ .btn-deny:hover {
4662
+ background: #f9605e;
4663
+ }
4664
+
4665
+ /* \u2500\u2500 Threat Panel (collapsible footer) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
4666
+
4667
+ .threat-panel {
4668
+ position: fixed;
4669
+ bottom: 0;
4670
+ left: 0;
4671
+ right: 0;
4672
+ background: var(--surface);
4253
4673
  border-top: 1px solid var(--border);
4254
- font-size: 12px; color: var(--text-muted);
4674
+ max-height: 240px;
4675
+ z-index: 500;
4676
+ display: flex;
4677
+ flex-direction: column;
4678
+ transition: max-height 0.3s ease-out;
4679
+ }
4680
+
4681
+ .threat-panel.collapsed {
4682
+ max-height: 40px;
4683
+ }
4684
+
4685
+ .threat-header {
4686
+ padding: 12px 20px;
4687
+ cursor: pointer;
4688
+ display: flex;
4689
+ align-items: center;
4690
+ gap: 8px;
4691
+ font-size: 12px;
4692
+ font-weight: 600;
4693
+ text-transform: uppercase;
4694
+ letter-spacing: 0.5px;
4695
+ color: var(--text-secondary);
4696
+ flex: 0 0 auto;
4697
+ }
4698
+
4699
+ .threat-header:hover {
4700
+ background: rgba(88, 166, 255, 0.05);
4701
+ }
4702
+
4703
+ .threat-icon {
4704
+ font-size: 14px;
4705
+ }
4706
+
4707
+ .threat-content {
4708
+ flex: 1;
4709
+ overflow-y: auto;
4710
+ padding: 0 20px 12px;
4711
+ display: flex;
4712
+ flex-direction: column;
4713
+ gap: 10px;
4714
+ }
4715
+
4716
+ .threat-item {
4717
+ padding: 8px 10px;
4718
+ background: rgba(248, 81, 73, 0.1);
4719
+ border-left: 2px solid var(--red);
4720
+ border-radius: 4px;
4721
+ font-size: 11px;
4722
+ color: var(--text-secondary);
4723
+ }
4724
+
4725
+ .threat-item-type {
4726
+ font-weight: 600;
4727
+ color: var(--red);
4728
+ font-family: var(--mono);
4729
+ }
4730
+
4731
+ .threat-empty {
4255
4732
  text-align: center;
4733
+ padding: 20px 10px;
4734
+ color: var(--text-secondary);
4735
+ font-size: 12px;
4736
+ }
4737
+
4738
+ /* \u2500\u2500 Scrollbars \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
4739
+
4740
+ ::-webkit-scrollbar {
4741
+ width: 6px;
4742
+ }
4743
+
4744
+ ::-webkit-scrollbar-track {
4745
+ background: transparent;
4746
+ }
4747
+
4748
+ ::-webkit-scrollbar-thumb {
4749
+ background: var(--border);
4750
+ border-radius: 3px;
4751
+ }
4752
+
4753
+ ::-webkit-scrollbar-thumb:hover {
4754
+ background: rgba(88, 166, 255, 0.3);
4755
+ }
4756
+
4757
+ /* \u2500\u2500 Responsive \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
4758
+
4759
+ @media (max-width: 1200px) {
4760
+ .protection-sidebar {
4761
+ display: none;
4762
+ }
4763
+
4764
+ .activity-feed {
4765
+ border-right: none;
4766
+ }
4767
+ }
4768
+
4769
+ @media (max-width: 768px) {
4770
+ .status-bar {
4771
+ padding: 0 12px;
4772
+ gap: 12px;
4773
+ height: 48px;
4774
+ }
4775
+
4776
+ .sanctuary-logo {
4777
+ font-size: 14px;
4778
+ }
4779
+
4780
+ .status-bar-center {
4781
+ display: none;
4782
+ }
4783
+
4784
+ .main-container {
4785
+ margin-top: 48px;
4786
+ }
4787
+
4788
+ .activity-item {
4789
+ padding: 10px 12px;
4790
+ }
4791
+
4792
+ .pending-overlay.active {
4793
+ width: 100%;
4794
+ }
4795
+
4796
+ .threat-panel {
4797
+ max-height: 200px;
4798
+ }
4256
4799
  }
4257
4800
  </style>
4258
4801
  </head>
4259
4802
  <body>
4260
- <div class="container">
4261
- <header>
4262
- <h1><span>Sanctuary</span> Principal Dashboard</h1>
4263
- <div class="status-badge">
4264
- <div class="status-dot" id="statusDot"></div>
4265
- <span id="statusText">Connected</span>
4803
+
4804
+ <!-- Status Bar (fixed, top) -->
4805
+ <div class="status-bar">
4806
+ <div class="status-bar-left">
4807
+ <div class="sanctuary-logo"><span>\u25C6</span> SANCTUARY</div>
4808
+ <div class="version">v${options.serverVersion}</div>
4809
+ </div>
4810
+ <div class="status-bar-center">
4811
+ <div class="sovereignty-badge">
4812
+ <div class="sovereignty-score" id="sovereigntyScore">85</div>
4813
+ <span>Sovereignty Health</span>
4814
+ </div>
4815
+ </div>
4816
+ <div class="status-bar-right">
4817
+ <div class="protections-indicator">
4818
+ <span class="count" id="activeProtections">6</span>/6 protections
4819
+ </div>
4820
+ <div class="uptime">
4821
+ <span id="uptimeText">\u2014</span>
4266
4822
  </div>
4267
- </header>
4268
-
4269
- <div class="tabs">
4270
- <button class="tab active" data-tab="pending">
4271
- Pending<span class="count muted" id="pendingCount">0</span>
4272
- </button>
4273
- <button class="tab" data-tab="audit">
4274
- Audit Log<span class="count muted" id="auditCount">0</span>
4275
- </button>
4276
- <button class="tab" data-tab="baseline">Baseline</button>
4277
- <button class="tab" data-tab="policy">Policy</button>
4823
+ <div class="status-dot" id="statusDot"></div>
4824
+ <div class="pending-badge hidden" id="pendingBadge">0</div>
4278
4825
  </div>
4826
+ </div>
4827
+
4828
+ <!-- Main Layout -->
4829
+ <div class="main-container">
4830
+ <!-- Activity Feed -->
4831
+ <div class="activity-feed">
4832
+ <div class="feed-header">
4833
+ <div class="feed-header-dot"></div>
4834
+ Live Activity
4835
+ </div>
4836
+ <div class="activity-list" id="activityList">
4837
+ <div class="activity-empty">
4838
+ <div class="activity-empty-icon">\u2192</div>
4839
+ <div class="activity-empty-text">Waiting for activity...</div>
4840
+ </div>
4841
+ </div>
4842
+ </div>
4843
+
4844
+ <!-- Protection Status Sidebar -->
4845
+ <div class="protection-sidebar" id="protectionSidebar">
4846
+ <div class="sidebar-header">
4847
+ <span>\u25C6</span> Protection Status
4848
+ </div>
4849
+ <div class="sidebar-content">
4850
+ <div class="protection-card">
4851
+ <div class="protection-card-icon">\u{1F510}</div>
4852
+ <div class="protection-card-label">Encryption</div>
4853
+ <div class="protection-card-status active" id="encryptionStatus">\u2713 Active</div>
4854
+ <div class="protection-card-stat" id="encryptionStat">Ed25519</div>
4855
+ </div>
4856
+
4857
+ <div class="protection-card">
4858
+ <div class="protection-card-icon">\u2713</div>
4859
+ <div class="protection-card-label">Approval Gate</div>
4860
+ <div class="protection-card-status active" id="approvalStatus">\u2713 Active</div>
4861
+ <div class="protection-card-stat" id="approvalStat">T1: 2 | T2: 3</div>
4862
+ </div>
4863
+
4864
+ <div class="protection-card">
4865
+ <div class="protection-card-icon">\u{1F3AF}</div>
4866
+ <div class="protection-card-label">Context Gating</div>
4867
+ <div class="protection-card-status active" id="contextStatus">\u2713 Active</div>
4868
+ <div class="protection-card-stat" id="contextStat">12 filtered</div>
4869
+ </div>
4870
+
4871
+ <div class="protection-card">
4872
+ <div class="protection-card-icon">\u26A0</div>
4873
+ <div class="protection-card-label">Injection Detection</div>
4874
+ <div class="protection-card-status active" id="injectionStatus">\u2713 Active</div>
4875
+ <div class="protection-card-stat" id="injectionStat">3 flags today</div>
4876
+ </div>
4877
+
4878
+ <div class="protection-card">
4879
+ <div class="protection-card-icon">\u{1F4CA}</div>
4880
+ <div class="protection-card-label">Behavioral Baseline</div>
4881
+ <div class="protection-card-status active" id="baselineStatus">\u2713 Active</div>
4882
+ <div class="protection-card-stat" id="baselineStat">0 anomalies</div>
4883
+ </div>
4884
+
4885
+ <div class="protection-card">
4886
+ <div class="protection-card-icon">\u{1F4CB}</div>
4887
+ <div class="protection-card-label">Audit Trail</div>
4888
+ <div class="protection-card-status active" id="auditStatus">\u2713 Active</div>
4889
+ <div class="protection-card-stat" id="auditStat">284 entries</div>
4890
+ </div>
4891
+ </div>
4892
+ </div>
4893
+ </div>
4894
+
4895
+ <!-- Pending Approvals Overlay -->
4896
+ <div class="pending-overlay" id="pendingOverlay">
4897
+ <div class="pending-overlay-header">
4898
+ <div class="pending-overlay-title">Pending Approvals</div>
4899
+ <button class="pending-overlay-close" onclick="closePendingOverlay()">\xD7</button>
4900
+ </div>
4901
+ <div class="pending-list" id="pendingList"></div>
4902
+ </div>
4903
+
4904
+ <!-- Threat Panel (collapsible footer) -->
4905
+ <div class="threat-panel collapsed" id="threatPanel">
4906
+ <div class="threat-header" onclick="toggleThreatPanel()">
4907
+ <span class="threat-icon">\u26A0</span>
4908
+ Recent Threats
4909
+ <span id="threatCount" style="margin-left: auto; color: var(--red); font-weight: 700;">0</span>
4910
+ </div>
4911
+ <div class="threat-content" id="threatContent">
4912
+ <div class="threat-empty">No threats detected</div>
4913
+ </div>
4914
+ </div>
4915
+
4916
+ <script>
4917
+ (function() {
4918
+ 'use strict';
4919
+
4920
+ // \u2500\u2500 Configuration \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
4921
+
4922
+ const TIMEOUT_SECONDS = ${options.timeoutSeconds};
4923
+ const AUTH_TOKEN = ${options.authToken ? JSON.stringify(options.authToken) : "null"};
4924
+ const MAX_ACTIVITY_ITEMS = 100;
4925
+ const MAX_THREAT_ITEMS = 20;
4926
+
4927
+ // \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
4928
+
4929
+ let SESSION_ID = null;
4930
+ let evtSource = null;
4931
+ let startTime = Date.now();
4932
+ let activityCount = 0;
4933
+ let threatCount = 0;
4934
+ const pendingRequests = new Map();
4935
+ const activityItems = [];
4936
+ const threatItems = [];
4937
+ let sovereigntyScore = 85;
4938
+
4939
+ // \u2500\u2500 Auth Helpers (SEC-012) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
4940
+
4941
+ function authHeaders() {
4942
+ const h = { 'Content-Type': 'application/json' };
4943
+ if (AUTH_TOKEN) h['Authorization'] = 'Bearer ' + AUTH_TOKEN;
4944
+ return h;
4945
+ }
4946
+
4947
+ function sessionQuery(url) {
4948
+ if (!SESSION_ID) return url;
4949
+ const sep = url.includes('?') ? '&' : '?';
4950
+ return url + sep + 'session=' + SESSION_ID;
4951
+ }
4952
+
4953
+ async function exchangeSession() {
4954
+ if (!AUTH_TOKEN) return;
4955
+ try {
4956
+ const resp = await fetch('/auth/session', { method: 'POST', headers: authHeaders() });
4957
+ if (resp.ok) {
4958
+ const data = await resp.json();
4959
+ SESSION_ID = data.session_id;
4960
+ const refreshMs = (data.expires_in_seconds || 300) * 800;
4961
+ setTimeout(() => { exchangeSession(); reconnectSSE(); }, refreshMs);
4962
+ }
4963
+ } catch (e) {
4964
+ // Retry on next connect
4965
+ }
4966
+ }
4967
+
4968
+ // \u2500\u2500 UI Utilities \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
4969
+
4970
+ function esc(s) {
4971
+ const d = document.createElement('div');
4972
+ d.textContent = String(s || '');
4973
+ return d.innerHTML;
4974
+ }
4975
+
4976
+ function closePendingOverlay() {
4977
+ document.getElementById('pendingOverlay').classList.remove('active');
4978
+ }
4979
+
4980
+ function toggleThreatPanel() {
4981
+ document.getElementById('threatPanel').classList.toggle('collapsed');
4982
+ }
4983
+
4984
+ function updateUptime() {
4985
+ const elapsed = Math.floor((Date.now() - startTime) / 1000);
4986
+ const hours = Math.floor(elapsed / 3600);
4987
+ const mins = Math.floor((elapsed % 3600) / 60);
4988
+ const secs = elapsed % 60;
4989
+ let uptimeStr = '';
4990
+ if (hours > 0) uptimeStr += hours + 'h ';
4991
+ if (mins > 0) uptimeStr += mins + 'm ';
4992
+ uptimeStr += secs + 's';
4993
+ document.getElementById('uptimeText').textContent = uptimeStr;
4994
+ }
4995
+
4996
+ // \u2500\u2500 Sovereignty Score \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
4997
+
4998
+ function updateSovereigntyScore(score) {
4999
+ sovereigntyScore = Math.min(100, Math.max(0, score || 85));
5000
+ const badge = document.getElementById('sovereigntyScore');
5001
+ badge.textContent = sovereigntyScore;
5002
+ badge.className = 'sovereignty-score';
5003
+ if (sovereigntyScore >= 80) {
5004
+ badge.classList.add('high');
5005
+ } else if (sovereigntyScore >= 50) {
5006
+ badge.classList.add('medium');
5007
+ } else {
5008
+ badge.classList.add('low');
5009
+ }
5010
+ }
5011
+
5012
+ // \u2500\u2500 Activity Feed \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
5013
+
5014
+ function addActivityItem(data) {
5015
+ const {
5016
+ timestamp,
5017
+ tier,
5018
+ tool,
5019
+ outcome,
5020
+ detail,
5021
+ hasInjection,
5022
+ isContextGated
5023
+ } = data;
5024
+
5025
+ const item = {
5026
+ id: 'activity-' + activityCount++,
5027
+ timestamp: timestamp || new Date().toISOString(),
5028
+ tier: tier || 1,
5029
+ tool: tool || 'unknown_tool',
5030
+ outcome: outcome || 'executed',
5031
+ detail: detail || '',
5032
+ hasInjection: !!hasInjection,
5033
+ isContextGated: !!isContextGated
5034
+ };
5035
+
5036
+ activityItems.unshift(item);
5037
+ if (activityItems.length > MAX_ACTIVITY_ITEMS) {
5038
+ activityItems.pop();
5039
+ }
5040
+
5041
+ renderActivityFeed();
5042
+ }
5043
+
5044
+ function renderActivityFeed() {
5045
+ const list = document.getElementById('activityList');
5046
+
5047
+ if (activityItems.length === 0) {
5048
+ list.innerHTML = '<div class="activity-empty"><div class="activity-empty-icon">\u2192</div><div class="activity-empty-text">Waiting for activity...</div></div>';
5049
+ return;
5050
+ }
5051
+
5052
+ list.innerHTML = '';
5053
+ for (const item of activityItems) {
5054
+ const tr = document.createElement('div');
5055
+ tr.className = 'activity-item';
5056
+ tr.id = item.id;
5057
+
5058
+ const time = new Date(item.timestamp);
5059
+ const timeStr = time.toLocaleTimeString();
5060
+
5061
+ const tierClass = 't' + item.tier;
5062
+ const outcomeClass = item.outcome === 'denied' ? 'outcome denied' : 'outcome';
5063
+
5064
+ let icon = '\u25CF';
5065
+ if (item.isContextGated) icon = '\u{1F3AF}';
5066
+ else if (item.hasInjection) icon = '\u26A0';
5067
+ else if (item.outcome === 'denied') icon = '\u2717';
5068
+ else icon = '\u2713';
5069
+
5070
+ tr.innerHTML =
5071
+ '<div class="activity-item-icon">' + esc(icon) + '</div>' +
5072
+ '<div class="activity-item-content">' +
5073
+ '<div class="activity-time">' + esc(timeStr) + '</div>' +
5074
+ '<div class="activity-main">' +
5075
+ '<span class="activity-tier ' + tierClass + '">T' + item.tier + '</span>' +
5076
+ '<span class="activity-tool">' + esc(item.tool) + '</span>' +
5077
+ '<span class="activity-outcome ' + (outcomeClass === 'outcome denied' ? 'denied' : '') + '">' + (item.outcome === 'denied' ? '\u2717 denied' : '\u2713 allowed') + '</span>' +
5078
+ '</div>' +
5079
+ '<div class="activity-detail">' + esc(item.detail) + '</div>' +
5080
+ '</div>' +
5081
+ '';
5082
+
5083
+ tr.addEventListener('click', () => {
5084
+ tr.classList.toggle('expanded');
5085
+ });
5086
+
5087
+ list.appendChild(tr);
5088
+ }
5089
+ }
5090
+
5091
+ // \u2500\u2500 Pending Approvals \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
5092
+
5093
+ function addPendingRequest(data) {
5094
+ const {
5095
+ request_id,
5096
+ operation,
5097
+ tier,
5098
+ reason,
5099
+ context,
5100
+ timestamp
5101
+ } = data;
5102
+
5103
+ const pending = {
5104
+ id: request_id,
5105
+ operation: operation || 'unknown',
5106
+ tier: tier || 1,
5107
+ reason: reason || '',
5108
+ context: context || {},
5109
+ timestamp: timestamp || new Date().toISOString(),
5110
+ remaining: TIMEOUT_SECONDS
5111
+ };
5112
+
5113
+ pendingRequests.set(request_id, pending);
5114
+ updatePendingUI();
5115
+ }
5116
+
5117
+ function removePendingRequest(id) {
5118
+ pendingRequests.delete(id);
5119
+ updatePendingUI();
5120
+ }
5121
+
5122
+ function updatePendingUI() {
5123
+ const count = pendingRequests.size;
5124
+ const badge = document.getElementById('pendingBadge');
5125
+
5126
+ if (count > 0) {
5127
+ badge.classList.remove('hidden');
5128
+ badge.textContent = count;
5129
+ document.getElementById('pendingOverlay').classList.add('active');
5130
+ } else {
5131
+ badge.classList.add('hidden');
5132
+ document.getElementById('pendingOverlay').classList.remove('active');
5133
+ }
5134
+
5135
+ renderPendingList();
5136
+ }
5137
+
5138
+ function renderPendingList() {
5139
+ const list = document.getElementById('pendingList');
5140
+ list.innerHTML = '';
5141
+
5142
+ for (const [id, req] of pendingRequests) {
5143
+ const item = document.createElement('div');
5144
+ item.className = 'pending-item';
5145
+
5146
+ const tier = req.tier || 1;
5147
+ const tierClass = 'tier' + tier;
5148
+ const pct = Math.max(0, Math.min(100, (req.remaining / TIMEOUT_SECONDS) * 100));
5149
+ const isUrgent = req.remaining <= 30;
5150
+
5151
+ item.innerHTML =
5152
+ '<div class="pending-item-header">' +
5153
+ '<div class="pending-item-op">' + esc(req.operation) + '</div>' +
5154
+ '<div class="pending-item-tier ' + tierClass + '">T' + tier + '</div>' +
5155
+ '</div>' +
5156
+ '<div class="pending-item-reason">' + esc(req.reason) + '</div>' +
5157
+ '<div class="pending-item-timer ' + (isUrgent ? 'urgent' : '') + '">' +
5158
+ '<div class="pending-item-timer-bar">' +
5159
+ '<div class="pending-item-timer-fill" style="width: ' + pct + '%"></div>' +
5160
+ '</div>' +
5161
+ '<span id="timer-' + id + '">' + req.remaining + 's</span>' +
5162
+ '</div>' +
5163
+ '<div class="pending-item-actions">' +
5164
+ '<button class="btn btn-approve" onclick="handleApprove('' + id + '')">Approve</button>' +
5165
+ '<button class="btn btn-deny" onclick="handleDeny('' + id + '')">Deny</button>' +
5166
+ '</div>' +
5167
+ '';
5168
+
5169
+ list.appendChild(item);
5170
+ }
5171
+ }
4279
5172
 
4280
- <!-- Pending Approvals -->
4281
- <div class="tab-content active" id="tab-pending">
4282
- <div class="pending-empty" id="pendingEmpty">
4283
- <div class="icon">&#x2714;</div>
4284
- <p>No pending approval requests.</p>
4285
- <p style="font-size:12px; margin-top:4px;">Requests will appear here in real time.</p>
4286
- </div>
4287
- <div id="pendingList"></div>
4288
- </div>
5173
+ window.handleApprove = function(id) {
5174
+ fetch('/api/approve/' + id, { method: 'POST', headers: authHeaders() }).then(() => {
5175
+ removePendingRequest(id);
5176
+ }).catch(() => {});
5177
+ };
4289
5178
 
4290
- <!-- Audit Log -->
4291
- <div class="tab-content" id="tab-audit">
4292
- <table class="audit-table">
4293
- <thead>
4294
- <tr><th>Time</th><th>Layer</th><th>Operation</th><th>Identity</th></tr>
4295
- </thead>
4296
- <tbody id="auditBody"></tbody>
4297
- </table>
4298
- </div>
5179
+ window.handleDeny = function(id) {
5180
+ fetch('/api/deny/' + id, { method: 'POST', headers: authHeaders() }).then(() => {
5181
+ removePendingRequest(id);
5182
+ }).catch(() => {});
5183
+ };
4299
5184
 
4300
- <!-- Baseline -->
4301
- <div class="tab-content" id="tab-baseline">
4302
- <div class="info-section">
4303
- <h3>Session Info</h3>
4304
- <div class="info-row"><span class="info-label">First session</span><span class="info-value" id="bFirstSession">\u2014</span></div>
4305
- <div class="info-row"><span class="info-label">Started</span><span class="info-value" id="bStarted">\u2014</span></div>
4306
- </div>
4307
- <div class="info-section">
4308
- <h3>Known Namespaces</h3>
4309
- <div class="tag-list" id="bNamespaces"><span class="tag">\u2014</span></div>
4310
- </div>
4311
- <div class="info-section">
4312
- <h3>Known Counterparties</h3>
4313
- <div class="tag-list" id="bCounterparties"><span class="tag">\u2014</span></div>
4314
- </div>
4315
- <div class="info-section">
4316
- <h3>Tool Call Counts</h3>
4317
- <div id="bToolCalls"><span class="info-value">\u2014</span></div>
4318
- </div>
4319
- </div>
5185
+ // \u2500\u2500 Threats \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
4320
5186
 
4321
- <!-- Policy -->
4322
- <div class="tab-content" id="tab-policy">
4323
- <div class="info-section">
4324
- <h3>Tier 1 \u2014 Always Requires Approval</h3>
4325
- <div id="pTier1"></div>
4326
- </div>
4327
- <div class="info-section">
4328
- <h3>Tier 2 \u2014 Anomaly Detection</h3>
4329
- <div id="pTier2"></div>
4330
- </div>
4331
- <div class="info-section">
4332
- <h3>Tier 3 \u2014 Always Allowed</h3>
4333
- <div class="info-row">
4334
- <span class="info-label">Operations</span>
4335
- <span class="info-value" id="pTier3Count">\u2014</span>
4336
- </div>
4337
- </div>
4338
- <div class="info-section">
4339
- <h3>Approval Channel</h3>
4340
- <div id="pChannel"></div>
4341
- </div>
4342
- </div>
5187
+ function addThreat(data) {
5188
+ const {
5189
+ timestamp,
5190
+ severity,
5191
+ type,
5192
+ details
5193
+ } = data;
4343
5194
 
4344
- <footer>Sanctuary Framework v${options.serverVersion} \u2014 Principal Dashboard</footer>
4345
- </div>
5195
+ const threat = {
5196
+ id: 'threat-' + threatCount++,
5197
+ timestamp: timestamp || new Date().toISOString(),
5198
+ severity: severity || 'medium',
5199
+ type: type || 'unknown',
5200
+ details: details || ''
5201
+ };
4346
5202
 
4347
- <script>
4348
- (function() {
4349
- const TIMEOUT = ${options.timeoutSeconds};
4350
- // SEC-012: Auth token is passed via Authorization header only \u2014 never in URLs.
4351
- // The token is provided by the server at generation time (embedded for initial auth).
4352
- const AUTH_TOKEN = ${options.authToken ? JSON.stringify(options.authToken) : "null"};
4353
- let SESSION_ID = null; // Short-lived session for SSE and URL-based requests
4354
- const pending = new Map();
4355
- let auditCount = 0;
5203
+ threatItems.unshift(threat);
5204
+ if (threatItems.length > MAX_THREAT_ITEMS) {
5205
+ threatItems.pop();
5206
+ }
4356
5207
 
4357
- // Auth helpers \u2014 SEC-012: token goes in header, session goes in URL
4358
- function authHeaders() {
4359
- const h = { 'Content-Type': 'application/json' };
4360
- if (AUTH_TOKEN) h['Authorization'] = 'Bearer ' + AUTH_TOKEN;
4361
- return h;
4362
- }
4363
- function sessionQuery(url) {
4364
- if (!SESSION_ID) return url;
4365
- const sep = url.includes('?') ? '&' : '?';
4366
- return url + sep + 'session=' + SESSION_ID;
5208
+ if (threatCount > 0) {
5209
+ document.getElementById('threatPanel').classList.remove('collapsed');
5210
+ }
5211
+
5212
+ renderThreats();
4367
5213
  }
4368
5214
 
4369
- // SEC-012: Exchange the long-lived token for a short-lived session
4370
- async function exchangeSession() {
4371
- if (!AUTH_TOKEN) return;
4372
- try {
4373
- const resp = await fetch('/auth/session', { method: 'POST', headers: authHeaders() });
4374
- if (resp.ok) {
4375
- const data = await resp.json();
4376
- SESSION_ID = data.session_id;
4377
- // Refresh session before expiry (at 80% of TTL)
4378
- const refreshMs = (data.expires_in_seconds || 300) * 800;
4379
- setTimeout(async () => { await exchangeSession(); reconnectSSE(); }, refreshMs);
4380
- }
4381
- } catch(e) { /* will retry on next connect */ }
5215
+ function renderThreats() {
5216
+ const content = document.getElementById('threatContent');
5217
+ const badge = document.getElementById('threatCount');
5218
+
5219
+ if (threatItems.length === 0) {
5220
+ content.innerHTML = '<div class="threat-empty">No threats detected</div>';
5221
+ badge.textContent = '0';
5222
+ return;
5223
+ }
5224
+
5225
+ badge.textContent = threatItems.length;
5226
+ content.innerHTML = '';
5227
+
5228
+ for (const threat of threatItems) {
5229
+ const div = document.createElement('div');
5230
+ div.className = 'threat-item';
5231
+ const time = new Date(threat.timestamp).toLocaleTimeString();
5232
+ div.innerHTML =
5233
+ '<div style="margin-bottom: 3px;">' +
5234
+ '<span class="threat-item-type">' + esc(threat.type) + '</span>' +
5235
+ '<span style="font-size: 10px; color: var(--text-secondary); margin-left: 6px;">' + esc(time) + '</span>' +
5236
+ '</div>' +
5237
+ '<div>' + esc(threat.details) + '</div>' +
5238
+ '';
5239
+ content.appendChild(div);
5240
+ }
4382
5241
  }
4383
5242
 
4384
- // Tab switching
4385
- document.querySelectorAll('.tab').forEach(tab => {
4386
- tab.addEventListener('click', () => {
4387
- document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
4388
- document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
4389
- tab.classList.add('active');
4390
- document.getElementById('tab-' + tab.dataset.tab).classList.add('active');
4391
- });
4392
- });
5243
+ // \u2500\u2500 SSE Connection \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
4393
5244
 
4394
- // SSE Connection \u2014 SEC-012: uses short-lived session token in URL, not auth token
4395
- let evtSource;
4396
5245
  function reconnectSSE() {
4397
- if (evtSource) { evtSource.close(); }
5246
+ if (evtSource) evtSource.close();
4398
5247
  connect();
4399
5248
  }
5249
+
4400
5250
  function connect() {
4401
5251
  evtSource = new EventSource(sessionQuery('/events'));
5252
+
4402
5253
  evtSource.onopen = () => {
4403
5254
  document.getElementById('statusDot').classList.remove('disconnected');
4404
- document.getElementById('statusText').textContent = 'Connected';
4405
5255
  };
5256
+
4406
5257
  evtSource.onerror = () => {
4407
5258
  document.getElementById('statusDot').classList.add('disconnected');
4408
- document.getElementById('statusText').textContent = 'Reconnecting...';
4409
5259
  };
5260
+
5261
+ evtSource.addEventListener('init', (e) => {
5262
+ const data = JSON.parse(e.data);
5263
+ if (data.baseline) {
5264
+ updateBaseline(data.baseline);
5265
+ }
5266
+ if (data.policy) {
5267
+ updatePolicy(data.policy);
5268
+ }
5269
+ if (data.pending) {
5270
+ data.pending.forEach(addPendingRequest);
5271
+ }
5272
+ });
5273
+
4410
5274
  evtSource.addEventListener('pending-request', (e) => {
4411
5275
  const data = JSON.parse(e.data);
4412
5276
  addPendingRequest(data);
4413
5277
  });
5278
+
4414
5279
  evtSource.addEventListener('request-resolved', (e) => {
4415
5280
  const data = JSON.parse(e.data);
4416
5281
  removePendingRequest(data.request_id);
4417
5282
  });
4418
- evtSource.addEventListener('audit-entry', (e) => {
5283
+
5284
+ evtSource.addEventListener('tool-call', (e) => {
4419
5285
  const data = JSON.parse(e.data);
4420
- addAuditEntry(data);
5286
+ addActivityItem({
5287
+ timestamp: data.timestamp,
5288
+ tier: data.tier || 1,
5289
+ tool: data.tool || 'unknown',
5290
+ outcome: data.outcome || 'executed',
5291
+ detail: data.detail || ''
5292
+ });
4421
5293
  });
4422
- evtSource.addEventListener('baseline-update', (e) => {
5294
+
5295
+ evtSource.addEventListener('context-gate-decision', (e) => {
4423
5296
  const data = JSON.parse(e.data);
4424
- updateBaseline(data);
5297
+ addActivityItem({
5298
+ timestamp: data.timestamp,
5299
+ tier: data.tier || 1,
5300
+ tool: data.tool || 'unknown',
5301
+ outcome: data.outcome || 'gated',
5302
+ detail: data.fields_filtered ? 'Filtered ' + data.fields_filtered + ' fields' : data.reason || '',
5303
+ isContextGated: true
5304
+ });
4425
5305
  });
4426
- evtSource.addEventListener('policy-update', (e) => {
5306
+
5307
+ evtSource.addEventListener('injection-alert', (e) => {
4427
5308
  const data = JSON.parse(e.data);
4428
- updatePolicy(data);
5309
+ addActivityItem({
5310
+ timestamp: data.timestamp,
5311
+ tier: data.tier || 2,
5312
+ tool: data.tool || 'unknown',
5313
+ outcome: data.allowed ? 'allowed' : 'denied',
5314
+ detail: data.signal || 'Injection detected',
5315
+ hasInjection: true
5316
+ });
5317
+ addThreat({
5318
+ timestamp: data.timestamp,
5319
+ severity: data.severity || 'medium',
5320
+ type: 'Injection Alert',
5321
+ details: data.signal || 'Suspicious pattern detected'
5322
+ });
4429
5323
  });
4430
- evtSource.addEventListener('init', (e) => {
5324
+
5325
+ evtSource.addEventListener('protection-status', (e) => {
4431
5326
  const data = JSON.parse(e.data);
4432
- if (data.baseline) updateBaseline(data.baseline);
4433
- if (data.policy) updatePolicy(data.policy);
4434
- if (data.pending) data.pending.forEach(addPendingRequest);
4435
- if (data.audit) data.audit.forEach(addAuditEntry);
5327
+ updateProtectionStatus(data);
4436
5328
  });
4437
- }
4438
-
4439
- // Pending requests
4440
- function addPendingRequest(req) {
4441
- pending.set(req.request_id, { ...req, remaining: TIMEOUT });
4442
- renderPending();
4443
- updatePendingCount();
4444
- flashTab('pending');
4445
- }
4446
5329
 
4447
- function removePendingRequest(id) {
4448
- pending.delete(id);
4449
- renderPending();
4450
- updatePendingCount();
4451
- }
5330
+ evtSource.addEventListener('audit-entry', (e) => {
5331
+ const data = JSON.parse(e.data);
5332
+ // Audit entries don't show in activity by default, but we could add them
5333
+ });
4452
5334
 
4453
- function renderPending() {
4454
- const list = document.getElementById('pendingList');
4455
- const empty = document.getElementById('pendingEmpty');
4456
- if (pending.size === 0) {
4457
- list.innerHTML = '';
4458
- empty.style.display = 'block';
4459
- return;
4460
- }
4461
- empty.style.display = 'none';
4462
- list.innerHTML = '';
4463
- for (const [id, req] of pending) {
4464
- const card = document.createElement('div');
4465
- card.className = 'request-card tier' + req.tier;
4466
- card.id = 'req-' + id;
4467
- const ctx = typeof req.context === 'string' ? req.context : JSON.stringify(req.context, null, 2);
4468
- card.innerHTML =
4469
- '<div class="request-header">' +
4470
- '<span class="request-op">' + esc(req.operation) + '</span>' +
4471
- '<span class="tier-badge tier' + req.tier + '">Tier ' + req.tier + '</span>' +
4472
- '</div>' +
4473
- '<div class="request-reason">' + esc(req.reason) + '</div>' +
4474
- '<div class="request-context">' + esc(ctx) + '</div>' +
4475
- '<div class="request-actions">' +
4476
- '<button class="btn btn-approve" onclick="handleApprove(\\'' + id + '\\')">Approve</button>' +
4477
- '<button class="btn btn-deny" onclick="handleDeny(\\'' + id + '\\')">Deny</button>' +
4478
- '<span class="countdown" id="cd-' + id + '">' + req.remaining + 's</span>' +
4479
- '</div>';
4480
- list.appendChild(card);
4481
- }
5335
+ evtSource.addEventListener('baseline-update', (e) => {
5336
+ const data = JSON.parse(e.data);
5337
+ updateBaseline(data);
5338
+ });
4482
5339
  }
4483
5340
 
4484
- function updatePendingCount() {
4485
- const el = document.getElementById('pendingCount');
4486
- el.textContent = pending.size;
4487
- el.className = pending.size > 0 ? 'count alert' : 'count muted';
5341
+ function updateBaseline(baseline) {
5342
+ if (!baseline) return;
5343
+ // Update baseline-derived stats if needed
4488
5344
  }
4489
5345
 
4490
- function flashTab(name) {
4491
- const tab = document.querySelector('[data-tab="' + name + '"]');
4492
- if (!tab.classList.contains('active')) {
4493
- tab.style.background = 'rgba(248,113,113,0.15)';
4494
- setTimeout(() => { tab.style.background = ''; }, 1500);
5346
+ function updatePolicy(policy) {
5347
+ if (!policy) return;
5348
+ // Update policy-derived stats
5349
+ if (policy.approval_channel) {
5350
+ // Policy info updated
4495
5351
  }
4496
5352
  }
4497
5353
 
4498
- // Countdown timer
4499
- setInterval(() => {
4500
- for (const [id, req] of pending) {
4501
- req.remaining = Math.max(0, req.remaining - 1);
4502
- const el = document.getElementById('cd-' + id);
4503
- if (el) {
4504
- el.textContent = req.remaining + 's';
4505
- el.className = req.remaining <= 30 ? 'countdown urgent' : 'countdown';
4506
- }
5354
+ function updateProtectionStatus(status) {
5355
+ if (status.sovereignty_score !== undefined) {
5356
+ updateSovereigntyScore(status.sovereignty_score);
5357
+ }
5358
+ if (status.active_protections !== undefined) {
5359
+ document.getElementById('activeProtections').textContent = status.active_protections;
5360
+ }
5361
+ // Update individual protection cards
5362
+ if (status.encryption !== undefined) {
5363
+ const el = document.getElementById('encryptionStatus');
5364
+ el.className = 'protection-card-status ' + (status.encryption ? 'active' : 'inactive');
5365
+ el.textContent = status.encryption ? '\u2713 Active' : '\u2717 Inactive';
5366
+ }
5367
+ if (status.approval_gate !== undefined) {
5368
+ const el = document.getElementById('approvalStatus');
5369
+ el.className = 'protection-card-status ' + (status.approval_gate ? 'active' : 'inactive');
5370
+ el.textContent = status.approval_gate ? '\u2713 Active' : '\u2717 Inactive';
5371
+ }
5372
+ if (status.context_gating !== undefined) {
5373
+ const el = document.getElementById('contextStatus');
5374
+ el.className = 'protection-card-status ' + (status.context_gating ? 'active' : 'inactive');
5375
+ el.textContent = status.context_gating ? '\u2713 Active' : '\u2717 Inactive';
5376
+ }
5377
+ if (status.injection_detection !== undefined) {
5378
+ const el = document.getElementById('injectionStatus');
5379
+ el.className = 'protection-card-status ' + (status.injection_detection ? 'active' : 'inactive');
5380
+ el.textContent = status.injection_detection ? '\u2713 Active' : '\u2717 Inactive';
5381
+ }
5382
+ if (status.baseline !== undefined) {
5383
+ const el = document.getElementById('baselineStatus');
5384
+ el.className = 'protection-card-status ' + (status.baseline ? 'active' : 'inactive');
5385
+ el.textContent = status.baseline ? '\u2713 Active' : '\u2717 Inactive';
5386
+ }
5387
+ if (status.audit_trail !== undefined) {
5388
+ const el = document.getElementById('auditStatus');
5389
+ el.className = 'protection-card-status ' + (status.audit_trail ? 'active' : 'inactive');
5390
+ el.textContent = status.audit_trail ? '\u2713 Active' : '\u2717 Inactive';
4507
5391
  }
4508
- }, 1000);
4509
-
4510
- // Approve / Deny handlers (global scope)
4511
- window.handleApprove = function(id) {
4512
- fetch('/api/approve/' + id, { method: 'POST', headers: authHeaders() }).then(() => {
4513
- removePendingRequest(id);
4514
- });
4515
- };
4516
- window.handleDeny = function(id) {
4517
- fetch('/api/deny/' + id, { method: 'POST', headers: authHeaders() }).then(() => {
4518
- removePendingRequest(id);
4519
- });
4520
- };
4521
-
4522
- // Audit log
4523
- function addAuditEntry(entry) {
4524
- auditCount++;
4525
- document.getElementById('auditCount').textContent = auditCount;
4526
- const tbody = document.getElementById('auditBody');
4527
- const tr = document.createElement('tr');
4528
- tr.className = 'new';
4529
- const time = entry.timestamp ? new Date(entry.timestamp).toLocaleTimeString() : '\u2014';
4530
- const layer = entry.layer || '\u2014';
4531
- tr.innerHTML =
4532
- '<td class="audit-time">' + esc(time) + '</td>' +
4533
- '<td><span class="audit-layer ' + layer + '">' + esc(layer) + '</span></td>' +
4534
- '<td class="audit-op">' + esc(entry.operation || '\u2014') + '</td>' +
4535
- '<td style="font-size:12px;color:var(--text-muted)">' + esc(entry.identity_id || '\u2014') + '</td>';
4536
- tbody.insertBefore(tr, tbody.firstChild);
4537
- // Keep last 100 entries
4538
- while (tbody.children.length > 100) tbody.removeChild(tbody.lastChild);
4539
- }
4540
-
4541
- // Baseline
4542
- function updateBaseline(b) {
4543
- if (!b) return;
4544
- document.getElementById('bFirstSession').textContent = b.is_first_session ? 'Yes' : 'No';
4545
- document.getElementById('bStarted').textContent = b.started_at ? new Date(b.started_at).toLocaleString() : '\u2014';
4546
- const ns = document.getElementById('bNamespaces');
4547
- ns.innerHTML = (b.known_namespaces || []).length > 0
4548
- ? (b.known_namespaces || []).map(n => '<span class="tag">' + esc(n) + '</span>').join('')
4549
- : '<span class="tag">none</span>';
4550
- const cp = document.getElementById('bCounterparties');
4551
- cp.innerHTML = (b.known_counterparties || []).length > 0
4552
- ? (b.known_counterparties || []).map(c => '<span class="tag">' + esc(c.slice(0,16)) + '...</span>').join('')
4553
- : '<span class="tag">none</span>';
4554
- const tc = document.getElementById('bToolCalls');
4555
- const counts = b.tool_call_counts || {};
4556
- const entries = Object.entries(counts).sort((a,b) => b[1] - a[1]);
4557
- tc.innerHTML = entries.length > 0
4558
- ? entries.map(([k,v]) => '<div class="info-row"><span class="info-label">' + esc(k) + '</span><span class="info-value">' + v + '</span></div>').join('')
4559
- : '<span class="info-value">no calls yet</span>';
4560
- }
4561
-
4562
- // Policy
4563
- function updatePolicy(p) {
4564
- if (!p) return;
4565
- const t1 = document.getElementById('pTier1');
4566
- t1.innerHTML = (p.tier1_always_approve || []).map(op =>
4567
- '<div class="policy-op">' + esc(op) + '</div>'
4568
- ).join('');
4569
- const t2 = document.getElementById('pTier2');
4570
- const cfg = p.tier2_anomaly || {};
4571
- t2.innerHTML = Object.entries(cfg).map(([k,v]) =>
4572
- '<div class="info-row"><span class="info-label">' + esc(k) + '</span><span class="info-value">' + esc(String(v)) + '</span></div>'
4573
- ).join('');
4574
- document.getElementById('pTier3Count').textContent = (p.tier3_always_allow || []).length + ' operations';
4575
- const ch = document.getElementById('pChannel');
4576
- const chan = p.approval_channel || {};
4577
- ch.innerHTML = Object.entries(chan).filter(([k]) => k !== 'webhook_secret').map(([k,v]) =>
4578
- '<div class="info-row"><span class="info-label">' + esc(k) + '</span><span class="info-value">' + esc(String(v)) + '</span></div>'
4579
- ).join('');
4580
5392
  }
4581
5393
 
4582
- function esc(s) {
4583
- if (!s) return '';
4584
- const d = document.createElement('div');
4585
- d.textContent = String(s);
4586
- return d.innerHTML;
4587
- }
5394
+ // \u2500\u2500 Initialization \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
4588
5395
 
4589
- // Init \u2014 SEC-012: exchange token for session before connecting SSE
4590
5396
  (async function init() {
4591
5397
  await exchangeSession();
4592
- // Clean token from URL if present (legacy bookmarks)
5398
+ // Clean legacy ?token= from URL
4593
5399
  if (window.location.search.includes('token=')) {
4594
- const clean = window.location.pathname;
4595
- window.history.replaceState({}, '', clean);
5400
+ window.history.replaceState({}, '', window.location.pathname);
4596
5401
  }
4597
5402
  connect();
4598
- fetch('/api/status', { headers: authHeaders() }).then(r => r.json()).then(data => {
4599
- if (data.baseline) updateBaseline(data.baseline);
4600
- if (data.policy) updatePolicy(data.policy);
4601
- }).catch(() => {});
5403
+
5404
+ // Start uptime ticker
5405
+ setInterval(updateUptime, 1000);
5406
+ updateUptime();
5407
+
5408
+ // Pending request countdown timer
5409
+ setInterval(() => {
5410
+ for (const [id, req] of pendingRequests) {
5411
+ req.remaining = Math.max(0, req.remaining - 1);
5412
+ const el = document.getElementById('timer-' + id);
5413
+ if (el) {
5414
+ el.textContent = req.remaining + 's';
5415
+ }
5416
+ }
5417
+ }, 1000);
5418
+
5419
+ // Load initial status
5420
+ try {
5421
+ const resp = await fetch('/api/status', { headers: authHeaders() });
5422
+ if (resp.ok) {
5423
+ const status = await resp.json();
5424
+ if (status.baseline) updateBaseline(status.baseline);
5425
+ if (status.policy) updatePolicy(status.policy);
5426
+ }
5427
+ } catch (e) {
5428
+ // Ignore
5429
+ }
4602
5430
  })();
5431
+
4603
5432
  })();
4604
5433
  </script>
5434
+
4605
5435
  </body>
4606
5436
  </html>`;
4607
5437
  }
@@ -5137,6 +5967,25 @@ data: ${JSON.stringify(data)}
5137
5967
  this.broadcastSSE("baseline-update", this.baseline.getProfile());
5138
5968
  }
5139
5969
  }
5970
+ /**
5971
+ * Broadcast a tool call event to connected dashboards.
5972
+ * Called from the gate or router when a tool is invoked.
5973
+ */
5974
+ broadcastToolCall(data) {
5975
+ this.broadcastSSE("tool-call", data);
5976
+ }
5977
+ /**
5978
+ * Broadcast a context gate decision to connected dashboards.
5979
+ */
5980
+ broadcastContextGateDecision(data) {
5981
+ this.broadcastSSE("context-gate-decision", data);
5982
+ }
5983
+ /**
5984
+ * Broadcast current protection status to connected dashboards.
5985
+ */
5986
+ broadcastProtectionStatus(data) {
5987
+ this.broadcastSSE("protection-status", data);
5988
+ }
5140
5989
  /** Get the number of pending requests */
5141
5990
  get pendingCount() {
5142
5991
  return this.pending.size;
@@ -5346,36 +6195,554 @@ var WebhookApprovalChannel = class {
5346
6195
  );
5347
6196
  return;
5348
6197
  }
5349
- const pending = this.pending.get(requestId);
5350
- if (!pending) {
5351
- res.writeHead(404, { "Content-Type": "application/json" });
5352
- res.end(
5353
- JSON.stringify({
5354
- error: "Request not found or already resolved"
5355
- })
5356
- );
5357
- return;
6198
+ const pending = this.pending.get(requestId);
6199
+ if (!pending) {
6200
+ res.writeHead(404, { "Content-Type": "application/json" });
6201
+ res.end(
6202
+ JSON.stringify({
6203
+ error: "Request not found or already resolved"
6204
+ })
6205
+ );
6206
+ return;
6207
+ }
6208
+ clearTimeout(pending.timer);
6209
+ this.pending.delete(requestId);
6210
+ const response = {
6211
+ decision: callbackPayload.decision,
6212
+ decided_at: (/* @__PURE__ */ new Date()).toISOString(),
6213
+ decided_by: "human"
6214
+ };
6215
+ pending.resolve(response);
6216
+ res.writeHead(200, { "Content-Type": "application/json" });
6217
+ res.end(
6218
+ JSON.stringify({
6219
+ success: true,
6220
+ decision: callbackPayload.decision
6221
+ })
6222
+ );
6223
+ });
6224
+ }
6225
+ /** Get the number of pending requests */
6226
+ get pendingCount() {
6227
+ return this.pending.size;
6228
+ }
6229
+ };
6230
+
6231
+ // src/security/injection-detector.ts
6232
+ var ROLE_OVERRIDE_PATTERNS = [
6233
+ /ignore\s+(?:(?:previous|prior|all)\s+)?instructions/i,
6234
+ /you\s+are\s+now/i,
6235
+ /\bsystem\s*:\s+(?!working|process|design|architecture)/i,
6236
+ /forget\s+(?:everything|all|prior)/i,
6237
+ /disregard\s+(?:the\s+)?(?:previous\s+)?instructions/i,
6238
+ /new\s+instructions\s*:/i,
6239
+ /updated?\s+instructions\s*:/i
6240
+ ];
6241
+ var SECURITY_BYPASS_PATTERNS = [
6242
+ /skip\s+(?:the\s+)?(?:filter|gate|check|verify|approve)/i,
6243
+ /bypass\s+(?:the\s+)?(?:filter|gate|security|check)/i,
6244
+ /disable\s+(?:the\s+)?(?:filter|gate|approval|security|audit|log|encrypt|verify)/i,
6245
+ /do\s+not\s+(?:audit|log|encrypt|verify|approve|check|sign)/i
6246
+ ];
6247
+ var TOOL_INVOCATION_PATTERNS = [
6248
+ /sanctuary\//i,
6249
+ /concordia\//i,
6250
+ /bridge_/i,
6251
+ /handshake_/i
6252
+ ];
6253
+ var URL_PATTERN = /https?:\/\/[^\s"'<>]+/i;
6254
+ var EMAIL_PATTERN = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/;
6255
+ var ZERO_WIDTH_CHARS = [
6256
+ "\u200B",
6257
+ // Zero-width space
6258
+ "\u200C",
6259
+ // Zero-width non-joiner
6260
+ "\u200D",
6261
+ // Zero-width joiner
6262
+ "\uFEFF"
6263
+ // Zero-width no-break space
6264
+ ];
6265
+ var InjectionDetector = class {
6266
+ config;
6267
+ stats = {
6268
+ total_scans: 0,
6269
+ total_flags: 0,
6270
+ total_blocks: 0,
6271
+ signals_by_type: {}
6272
+ };
6273
+ constructor(config = {}) {
6274
+ this.config = {
6275
+ enabled: config.enabled ?? true,
6276
+ sensitivity: config.sensitivity ?? "medium",
6277
+ on_detection: config.on_detection ?? "escalate",
6278
+ custom_patterns: config.custom_patterns ?? []
6279
+ };
6280
+ }
6281
+ /**
6282
+ * Scan tool arguments for injection signals.
6283
+ * @param toolName Full tool name (e.g., "sanctuary/state_read")
6284
+ * @param args Tool arguments
6285
+ * @returns DetectionResult with all detected signals
6286
+ */
6287
+ scan(toolName, args) {
6288
+ this.stats.total_scans++;
6289
+ if (!this.config.enabled) {
6290
+ return {
6291
+ flagged: false,
6292
+ confidence: 0,
6293
+ signals: [],
6294
+ recommendation: "allow"
6295
+ };
6296
+ }
6297
+ const signals = [];
6298
+ const visited = /* @__PURE__ */ new Set();
6299
+ this.scanValue(args, "", toolName, signals, visited);
6300
+ const flagged = signals.length > 0;
6301
+ if (flagged) {
6302
+ this.stats.total_flags++;
6303
+ }
6304
+ for (const sig of signals) {
6305
+ this.stats.signals_by_type[sig.type] = (this.stats.signals_by_type[sig.type] ?? 0) + 1;
6306
+ }
6307
+ const recommendation = this.computeRecommendation(
6308
+ signals,
6309
+ this.config.sensitivity
6310
+ );
6311
+ if (recommendation === "block") {
6312
+ this.stats.total_blocks++;
6313
+ }
6314
+ return {
6315
+ flagged,
6316
+ confidence: this.computeConfidence(signals),
6317
+ signals,
6318
+ recommendation
6319
+ };
6320
+ }
6321
+ /**
6322
+ * Recursively scan a value and all nested values.
6323
+ */
6324
+ scanValue(value, path, toolName, signals, visited) {
6325
+ if (typeof value === "object" && value !== null) {
6326
+ if (visited.has(value)) return;
6327
+ visited.add(value);
6328
+ }
6329
+ if (typeof value === "string") {
6330
+ this.scanString(value, path, toolName, signals);
6331
+ } else if (Array.isArray(value)) {
6332
+ for (let i = 0; i < value.length; i++) {
6333
+ this.scanValue(value[i], `${path}[${i}]`, toolName, signals, visited);
6334
+ }
6335
+ } else if (typeof value === "object" && value !== null) {
6336
+ for (const [key, val] of Object.entries(value)) {
6337
+ this.scanValue(val, path ? `${path}.${key}` : key, toolName, signals, visited);
6338
+ }
6339
+ }
6340
+ }
6341
+ /**
6342
+ * Scan a single string for injection signals.
6343
+ */
6344
+ scanString(value, path, _toolName, signals) {
6345
+ if (this.isSafeField(path)) {
6346
+ return;
6347
+ }
6348
+ const location = path || "root";
6349
+ const normalized = this.normalizeConfusables(value.normalize("NFKC"));
6350
+ if (normalized !== value) {
6351
+ signals.push({
6352
+ type: "encoding_evasion",
6353
+ pattern: "unicode_normalization_delta",
6354
+ location,
6355
+ severity: "medium"
6356
+ });
6357
+ }
6358
+ for (const pattern of ROLE_OVERRIDE_PATTERNS) {
6359
+ if (pattern.test(normalized)) {
6360
+ signals.push({
6361
+ type: "role_override",
6362
+ pattern: pattern.source,
6363
+ location,
6364
+ severity: "high"
6365
+ });
6366
+ break;
6367
+ }
6368
+ }
6369
+ for (const pattern of SECURITY_BYPASS_PATTERNS) {
6370
+ if (pattern.test(normalized)) {
6371
+ signals.push({
6372
+ type: "security_bypass",
6373
+ pattern: pattern.source,
6374
+ location,
6375
+ severity: "high"
6376
+ });
6377
+ break;
6378
+ }
6379
+ }
6380
+ if (!this.isToolNameField(path)) {
6381
+ for (const pattern of TOOL_INVOCATION_PATTERNS) {
6382
+ if (pattern.test(normalized)) {
6383
+ signals.push({
6384
+ type: "tool_invocation_in_string",
6385
+ pattern: pattern.source,
6386
+ location,
6387
+ severity: "medium"
6388
+ });
6389
+ break;
6390
+ }
6391
+ }
6392
+ }
6393
+ this.detectEncodingEvasion(value, location, signals);
6394
+ this.detectDataExfiltration(value, location, signals);
6395
+ this.detectPromptStuffing(value, location, signals);
6396
+ }
6397
+ /**
6398
+ * Detect base64 strings and zero-width character evasion.
6399
+ */
6400
+ detectEncodingEvasion(value, path, signals) {
6401
+ if (value.length > 50 && /^[A-Za-z0-9+/]+={0,2}$/.test(value.trim())) {
6402
+ signals.push({
6403
+ type: "encoding_evasion",
6404
+ pattern: "base64_string",
6405
+ location: path || "root",
6406
+ severity: "medium"
6407
+ });
6408
+ }
6409
+ let zeroWidthCount = 0;
6410
+ for (const char of ZERO_WIDTH_CHARS) {
6411
+ zeroWidthCount += (value.match(new RegExp(char, "g")) || []).length;
6412
+ }
6413
+ if (zeroWidthCount > 0) {
6414
+ signals.push({
6415
+ type: "encoding_evasion",
6416
+ pattern: "zero_width_characters",
6417
+ location: path || "root",
6418
+ severity: "medium"
6419
+ });
6420
+ }
6421
+ const hasLatin = /[a-zA-Z]/.test(value);
6422
+ const hasCJK = /[\u4E00-\u9FFF\u3040-\u309F\uAC00-\uD7AF]/.test(value);
6423
+ const hasArabic = /[\u0600-\u06FF]/.test(value);
6424
+ const hasCyrillic = /[\u0400-\u04FF]/.test(value);
6425
+ const unicodeCategories = [hasLatin, hasCJK, hasArabic, hasCyrillic].filter(
6426
+ (x) => x
6427
+ ).length;
6428
+ if (unicodeCategories >= 3) {
6429
+ signals.push({
6430
+ type: "encoding_evasion",
6431
+ pattern: "unicode_category_mixing",
6432
+ location: path || "root",
6433
+ severity: "medium"
6434
+ });
6435
+ }
6436
+ }
6437
+ /**
6438
+ * Detect URLs and emails in fields that shouldn't have them.
6439
+ */
6440
+ detectDataExfiltration(value, path, signals) {
6441
+ if (this.isUrlSafeField(path)) {
6442
+ return;
6443
+ }
6444
+ if (URL_PATTERN.test(value)) {
6445
+ signals.push({
6446
+ type: "data_exfiltration",
6447
+ pattern: "url_in_string",
6448
+ location: path || "root",
6449
+ severity: "medium"
6450
+ });
6451
+ }
6452
+ if (EMAIL_PATTERN.test(value) && !this.isEmailSafeField(path)) {
6453
+ signals.push({
6454
+ type: "data_exfiltration",
6455
+ pattern: "email_in_string",
6456
+ location: path || "root",
6457
+ severity: "medium"
6458
+ });
6459
+ }
6460
+ if (value.length > 30 && value.length < 1e4 && !this.isStructuredField(path)) {
6461
+ const hasJsonContent = /\{[^}]*"[^"]*"[^}]*\}/.test(value);
6462
+ const hasXmlContent = /<[^>]+>[\s\S]*?<\/[^>]+>/.test(value);
6463
+ if (hasJsonContent || hasXmlContent) {
6464
+ signals.push({
6465
+ type: "data_exfiltration",
6466
+ pattern: "structured_data_in_string",
6467
+ location: path || "root",
6468
+ severity: "medium"
6469
+ });
6470
+ }
6471
+ }
6472
+ }
6473
+ /**
6474
+ * Detect prompt stuffing: very large strings or high repetition.
6475
+ */
6476
+ detectPromptStuffing(value, path, signals) {
6477
+ if (value.length > 10240) {
6478
+ signals.push({
6479
+ type: "prompt_stuffing",
6480
+ pattern: "large_string",
6481
+ location: path || "root",
6482
+ severity: "low"
6483
+ });
6484
+ }
6485
+ if (value.length >= 100) {
6486
+ const windowSizes = [10, 20, 50];
6487
+ for (const windowSize of windowSizes) {
6488
+ if (value.length < windowSize * 5) continue;
6489
+ const pattern = value.substring(0, windowSize);
6490
+ let count = 0;
6491
+ let idx = 0;
6492
+ while (idx <= value.length - windowSize) {
6493
+ if (value.substring(idx, idx + windowSize) === pattern) {
6494
+ count++;
6495
+ idx += windowSize;
6496
+ } else {
6497
+ idx++;
6498
+ }
6499
+ if (count >= 10) break;
6500
+ }
6501
+ if (count >= 10) {
6502
+ signals.push({
6503
+ type: "prompt_stuffing",
6504
+ pattern: "high_repetition",
6505
+ location: path || "root",
6506
+ severity: "low"
6507
+ });
6508
+ break;
6509
+ }
6510
+ }
6511
+ }
6512
+ }
6513
+ /**
6514
+ * Determine if this field is inherently safe from role override.
6515
+ */
6516
+ isSafeField(path) {
6517
+ const safePaths = [
6518
+ /\.version$/i,
6519
+ /\.timestamp$/i,
6520
+ /\.id$/i,
6521
+ /\.uuid$/i,
6522
+ /\.hash$/i,
6523
+ /\.signature$/i,
6524
+ /\.public_key$/i,
6525
+ /\.private_key$/i,
6526
+ /\.did$/i,
6527
+ /\.nonce$/i,
6528
+ /\.salt$/i,
6529
+ /\.iv$/i,
6530
+ /^ciphertext$/i,
6531
+ /^encrypted$/i
6532
+ ];
6533
+ return safePaths.some((p) => p.test(path));
6534
+ }
6535
+ /**
6536
+ * Determine if this is a tool name field (where tool refs are expected).
6537
+ */
6538
+ isToolNameField(path) {
6539
+ const toolFields = [
6540
+ /tool_name/i,
6541
+ /\.tool$/i,
6542
+ /^tool$/i,
6543
+ /operation/i
6544
+ ];
6545
+ return toolFields.some((p) => p.test(path));
6546
+ }
6547
+ /**
6548
+ * Determine if this field is safe for URLs.
6549
+ */
6550
+ isUrlSafeField(path) {
6551
+ const urlFields = [
6552
+ /url/i,
6553
+ /endpoint/i,
6554
+ /webhook/i,
6555
+ /callback/i
6556
+ ];
6557
+ return urlFields.some((p) => p.test(path));
6558
+ }
6559
+ /**
6560
+ * Determine if this field is safe for emails.
6561
+ */
6562
+ isEmailSafeField(path) {
6563
+ const emailFields = [
6564
+ /email/i,
6565
+ /contact/i,
6566
+ /recipient/i,
6567
+ /sender/i,
6568
+ /from/i,
6569
+ /to/i
6570
+ ];
6571
+ return emailFields.some((p) => p.test(path));
6572
+ }
6573
+ /**
6574
+ * Determine if this field is safe for structured data (JSON/XML).
6575
+ */
6576
+ isStructuredField(path) {
6577
+ const structuredFields = [
6578
+ /data/i,
6579
+ /payload/i,
6580
+ /body/i,
6581
+ /json/i,
6582
+ /xml/i
6583
+ ];
6584
+ return structuredFields.some((p) => p.test(path));
6585
+ }
6586
+ /**
6587
+ * SEC-032: Map common cross-script confusable characters to their Latin equivalents.
6588
+ * NFKC normalization handles fullwidth and compatibility forms, but does NOT map
6589
+ * Cyrillic/Greek lookalikes to Latin (they're distinct codepoints by design).
6590
+ * This covers the most common confusables used in injection evasion.
6591
+ */
6592
+ normalizeConfusables(value) {
6593
+ const confusables = {
6594
+ // Cyrillic → Latin
6595
+ "\u0410": "A",
6596
+ "\u0430": "a",
6597
+ // А а
6598
+ "\u0412": "B",
6599
+ "\u0432": "b",
6600
+ // В (not exact) в (not exact)
6601
+ "\u0421": "C",
6602
+ "\u0441": "c",
6603
+ // С с
6604
+ "\u0415": "E",
6605
+ "\u0435": "e",
6606
+ // Е е
6607
+ "\u041D": "H",
6608
+ "\u043D": "h",
6609
+ // Н (not exact) н (not exact)
6610
+ "\u041A": "K",
6611
+ "\u043A": "k",
6612
+ // К к (not exact)
6613
+ "\u041C": "M",
6614
+ "\u043C": "m",
6615
+ // М (not exact) м (not exact)
6616
+ "\u041E": "O",
6617
+ "\u043E": "o",
6618
+ // О о
6619
+ "\u0420": "P",
6620
+ "\u0440": "p",
6621
+ // Р р
6622
+ "\u0422": "T",
6623
+ "\u0442": "t",
6624
+ // Т (not exact) т (not exact)
6625
+ "\u0425": "X",
6626
+ "\u0445": "x",
6627
+ // Х х
6628
+ "\u0423": "Y",
6629
+ "\u0443": "y",
6630
+ // У (not exact) у
6631
+ // Greek → Latin
6632
+ "\u0391": "A",
6633
+ "\u03B1": "a",
6634
+ // Α α (not exact)
6635
+ "\u0392": "B",
6636
+ "\u03B2": "b",
6637
+ // Β β (not exact)
6638
+ "\u0395": "E",
6639
+ "\u03B5": "e",
6640
+ // Ε ε (not exact)
6641
+ "\u0397": "H",
6642
+ // Η
6643
+ "\u0399": "I",
6644
+ "\u03B9": "i",
6645
+ // Ι ι
6646
+ "\u039A": "K",
6647
+ "\u03BA": "k",
6648
+ // Κ κ
6649
+ "\u039C": "M",
6650
+ // Μ
6651
+ "\u039D": "N",
6652
+ // Ν
6653
+ "\u039F": "O",
6654
+ "\u03BF": "o",
6655
+ // Ο ο
6656
+ "\u03A1": "P",
6657
+ "\u03C1": "p",
6658
+ // Ρ ρ (not exact)
6659
+ "\u03A4": "T",
6660
+ "\u03C4": "t",
6661
+ // Τ τ (not exact)
6662
+ "\u03A5": "Y",
6663
+ "\u03C5": "y",
6664
+ // Υ υ (not exact)
6665
+ "\u03A7": "X",
6666
+ "\u03C7": "x"
6667
+ // Χ χ (not exact)
6668
+ };
6669
+ let result = value;
6670
+ if (/[^\x00-\x7F]/.test(value)) {
6671
+ const chars = [];
6672
+ for (const ch of result) {
6673
+ chars.push(confusables[ch] ?? ch);
5358
6674
  }
5359
- clearTimeout(pending.timer);
5360
- this.pending.delete(requestId);
5361
- const response = {
5362
- decision: callbackPayload.decision,
5363
- decided_at: (/* @__PURE__ */ new Date()).toISOString(),
5364
- decided_by: "human"
5365
- };
5366
- pending.resolve(response);
5367
- res.writeHead(200, { "Content-Type": "application/json" });
5368
- res.end(
5369
- JSON.stringify({
5370
- success: true,
5371
- decision: callbackPayload.decision
5372
- })
5373
- );
5374
- });
6675
+ result = chars.join("");
6676
+ }
6677
+ return result;
5375
6678
  }
5376
- /** Get the number of pending requests */
5377
- get pendingCount() {
5378
- return this.pending.size;
6679
+ /**
6680
+ * Compute confidence score based on signals.
6681
+ * More high-severity signals = higher confidence.
6682
+ */
6683
+ computeConfidence(signals) {
6684
+ if (signals.length === 0) return 0;
6685
+ let score = 0;
6686
+ let highCount = 0;
6687
+ for (const sig of signals) {
6688
+ switch (sig.severity) {
6689
+ case "high":
6690
+ highCount++;
6691
+ score += 0.35;
6692
+ break;
6693
+ case "medium":
6694
+ score += 0.15;
6695
+ break;
6696
+ case "low":
6697
+ score += 0.05;
6698
+ break;
6699
+ }
6700
+ }
6701
+ if (highCount > 1) {
6702
+ score += (highCount - 1) * 0.15;
6703
+ }
6704
+ return Math.min(score, 1);
6705
+ }
6706
+ /**
6707
+ * Compute recommendation based on signals and sensitivity.
6708
+ */
6709
+ computeRecommendation(signals, sensitivity) {
6710
+ if (signals.length === 0) return "allow";
6711
+ const highSeverity = signals.filter((s) => s.severity === "high");
6712
+ const mediumSeverity = signals.filter((s) => s.severity === "medium");
6713
+ switch (sensitivity) {
6714
+ case "low":
6715
+ return highSeverity.length > 0 ? "escalate" : "allow";
6716
+ case "medium":
6717
+ if (highSeverity.length > 0) return "block";
6718
+ return mediumSeverity.length > 0 ? "escalate" : "allow";
6719
+ case "high":
6720
+ if (highSeverity.length > 0 || mediumSeverity.length > 1) return "block";
6721
+ if (mediumSeverity.length > 0) return "block";
6722
+ return signals.length > 0 ? "escalate" : "allow";
6723
+ }
6724
+ }
6725
+ /**
6726
+ * Get statistics about scans performed.
6727
+ */
6728
+ getStats() {
6729
+ return {
6730
+ total_scans: this.stats.total_scans,
6731
+ total_flags: this.stats.total_flags,
6732
+ total_blocks: this.stats.total_blocks,
6733
+ signals_by_type: { ...this.stats.signals_by_type }
6734
+ };
6735
+ }
6736
+ /**
6737
+ * Reset statistics.
6738
+ */
6739
+ resetStats() {
6740
+ this.stats = {
6741
+ total_scans: 0,
6742
+ total_flags: 0,
6743
+ total_blocks: 0,
6744
+ signals_by_type: {}
6745
+ };
5379
6746
  }
5380
6747
  };
5381
6748
 
@@ -5385,11 +6752,15 @@ var ApprovalGate = class {
5385
6752
  baseline;
5386
6753
  channel;
5387
6754
  auditLog;
5388
- constructor(policy, baseline, channel, auditLog) {
6755
+ injectionDetector;
6756
+ onInjectionAlert;
6757
+ constructor(policy, baseline, channel, auditLog, injectionDetector, onInjectionAlert) {
5389
6758
  this.policy = policy;
5390
6759
  this.baseline = baseline;
5391
6760
  this.channel = channel;
5392
6761
  this.auditLog = auditLog;
6762
+ this.injectionDetector = injectionDetector ?? new InjectionDetector();
6763
+ this.onInjectionAlert = onInjectionAlert;
5393
6764
  }
5394
6765
  /**
5395
6766
  * Evaluate a tool call against the Principal Policy.
@@ -5401,6 +6772,48 @@ var ApprovalGate = class {
5401
6772
  async evaluate(toolName, args) {
5402
6773
  const operation = extractOperationName(toolName);
5403
6774
  this.baseline.recordToolCall(operation);
6775
+ const injectionResult = this.injectionDetector.scan(toolName, args);
6776
+ if (injectionResult.flagged) {
6777
+ this.auditLog.append("l2", `injection_detected:${operation}`, "system", {
6778
+ confidence: injectionResult.confidence,
6779
+ signals: injectionResult.signals.map((s) => ({
6780
+ type: s.type,
6781
+ location: s.location,
6782
+ severity: s.severity
6783
+ })),
6784
+ recommendation: injectionResult.recommendation
6785
+ });
6786
+ if (this.onInjectionAlert) {
6787
+ this.onInjectionAlert({
6788
+ toolName,
6789
+ result: injectionResult,
6790
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
6791
+ });
6792
+ }
6793
+ if (injectionResult.recommendation === "block") {
6794
+ return {
6795
+ allowed: false,
6796
+ tier: 1,
6797
+ reason: `Blocked: prompt injection detected in "${operation}" (confidence: ${(injectionResult.confidence * 100).toFixed(0)}%)`,
6798
+ approval_required: false
6799
+ };
6800
+ }
6801
+ if (injectionResult.recommendation === "escalate") {
6802
+ return this.requestApproval(
6803
+ operation,
6804
+ 1,
6805
+ `Potential prompt injection detected in "${operation}" (confidence: ${(injectionResult.confidence * 100).toFixed(0)}%, ${injectionResult.signals.length} signal(s))`,
6806
+ {
6807
+ operation,
6808
+ injection_detection: {
6809
+ confidence: injectionResult.confidence,
6810
+ signal_count: injectionResult.signals.length,
6811
+ signal_types: [...new Set(injectionResult.signals.map((s) => s.type))]
6812
+ }
6813
+ }
6814
+ );
6815
+ }
6816
+ }
5404
6817
  if (this.policy.tier1_always_approve.includes(operation)) {
5405
6818
  return this.requestApproval(operation, 1, `"${operation}" is a Tier 1 operation (always requires approval)`, {
5406
6819
  operation,
@@ -5579,6 +6992,10 @@ var ApprovalGate = class {
5579
6992
  getBaseline() {
5580
6993
  return this.baseline;
5581
6994
  }
6995
+ /** Get the injection detector for stats/configuration access */
6996
+ getInjectionDetector() {
6997
+ return this.injectionDetector;
6998
+ }
5582
6999
  };
5583
7000
 
5584
7001
  // src/principal-policy/tools.ts
@@ -8775,9 +10192,345 @@ function matchesFieldPattern(normalizedField, pattern) {
8775
10192
  return false;
8776
10193
  }
8777
10194
 
10195
+ // src/l2-operational/context-gate-enforcer.ts
10196
+ init_encoding();
10197
+ init_hashing();
10198
+ var BUILTIN_SENSITIVE_PATTERNS = [
10199
+ "*_key",
10200
+ "*_token",
10201
+ "*_secret",
10202
+ "api_key",
10203
+ "access_token",
10204
+ "refresh_token",
10205
+ "password",
10206
+ "passwd",
10207
+ "credential*",
10208
+ "auth_*",
10209
+ "ssn",
10210
+ "social_security*",
10211
+ "tax_id*",
10212
+ "credit_card*",
10213
+ "card_number*",
10214
+ "cvv",
10215
+ "cvc",
10216
+ "private_key",
10217
+ "secret_key",
10218
+ "master_key"
10219
+ ];
10220
+ var ContextGateEnforcer = class {
10221
+ policyStore;
10222
+ auditLog;
10223
+ config;
10224
+ stats = {
10225
+ calls_inspected: 0,
10226
+ calls_bypassed: 0,
10227
+ fields_redacted: 0,
10228
+ fields_hashed: 0,
10229
+ fields_blocked: 0,
10230
+ calls_blocked: 0
10231
+ };
10232
+ constructor(policyStore, auditLog, config) {
10233
+ this.policyStore = policyStore;
10234
+ this.auditLog = auditLog;
10235
+ this.config = config;
10236
+ }
10237
+ /**
10238
+ * Wrap a tool handler to apply automatic context gating.
10239
+ *
10240
+ * The wrapped handler:
10241
+ * 1. Checks if tool should be filtered (based on bypass_prefixes)
10242
+ * 2. If not filtering, calls original handler directly
10243
+ * 3. If filtering:
10244
+ * a. Gets the active policy or falls back to built-in patterns
10245
+ * b. Calls filterContext() with tool arguments
10246
+ * c. If any field triggered "deny" and on_deny is "block", returns error
10247
+ * d. If on_deny is "redact", replaces denied fields with "[REDACTED]"
10248
+ * e. Calls original handler with filtered arguments
10249
+ * f. Logs the filtering decision
10250
+ * 4. In log_only mode: runs filter, logs what would happen, passes original args
10251
+ */
10252
+ wrapHandler(toolName, originalHandler) {
10253
+ return async (args) => {
10254
+ if (!this.config.enabled) {
10255
+ return originalHandler(args);
10256
+ }
10257
+ if (!this.shouldFilter(toolName)) {
10258
+ this.stats.calls_bypassed++;
10259
+ return originalHandler(args);
10260
+ }
10261
+ this.stats.calls_inspected++;
10262
+ const policy = this.config.default_policy_id ? await this.policyStore.get(this.config.default_policy_id) : null;
10263
+ if (policy) {
10264
+ return this.filterWithPolicy(
10265
+ toolName,
10266
+ args,
10267
+ originalHandler,
10268
+ policy
10269
+ );
10270
+ } else {
10271
+ return this.filterWithBuiltinPatterns(
10272
+ toolName,
10273
+ args,
10274
+ originalHandler
10275
+ );
10276
+ }
10277
+ };
10278
+ }
10279
+ /**
10280
+ * Filter tool arguments using an explicit policy.
10281
+ */
10282
+ async filterWithPolicy(toolName, args, originalHandler, policy) {
10283
+ const provider = this.extractProviderCategory(toolName);
10284
+ const result = filterContext(policy, provider, args);
10285
+ const deniedFields = result.decisions.filter((d) => d.action === "deny");
10286
+ if (deniedFields.length > 0) {
10287
+ if (this.config.on_deny === "block") {
10288
+ this.stats.calls_blocked++;
10289
+ this.auditLog.append(
10290
+ "l2",
10291
+ "context_gate_enforcer_block",
10292
+ "system",
10293
+ {
10294
+ tool_name: toolName,
10295
+ policy_id: policy.policy_id,
10296
+ provider,
10297
+ denied_fields: deniedFields.map((d) => d.field),
10298
+ original_context_hash: result.original_context_hash
10299
+ }
10300
+ );
10301
+ return toolResult({
10302
+ error: "context_gating_blocked",
10303
+ message: "Tool call contains fields that trigger deny action",
10304
+ tool: toolName,
10305
+ denied_fields: deniedFields.map((d) => d.field),
10306
+ recommendation: "Remove the denied fields from context or update the context-gating policy."
10307
+ });
10308
+ }
10309
+ }
10310
+ const filteredArgs = this.buildFilteredArgs(args, result.decisions);
10311
+ if (this.config.log_only) {
10312
+ this.auditLog.append(
10313
+ "l2",
10314
+ "context_gate_enforcer_log_only",
10315
+ "system",
10316
+ {
10317
+ tool_name: toolName,
10318
+ policy_id: policy.policy_id,
10319
+ provider,
10320
+ fields_total: Object.keys(args).length,
10321
+ fields_redacted: result.fields_redacted,
10322
+ fields_hashed: result.fields_hashed,
10323
+ fields_blocked: deniedFields.length,
10324
+ original_context_hash: result.original_context_hash
10325
+ }
10326
+ );
10327
+ this.stats.fields_redacted += result.fields_redacted;
10328
+ this.stats.fields_hashed += result.fields_hashed;
10329
+ this.stats.fields_blocked += deniedFields.length;
10330
+ return originalHandler(args);
10331
+ }
10332
+ this.auditLog.append(
10333
+ "l2",
10334
+ "context_gate_enforcer_filter",
10335
+ "system",
10336
+ {
10337
+ tool_name: toolName,
10338
+ policy_id: policy.policy_id,
10339
+ provider,
10340
+ fields_total: Object.keys(args).length,
10341
+ fields_redacted: result.fields_redacted,
10342
+ fields_hashed: result.fields_hashed,
10343
+ fields_blocked: deniedFields.length,
10344
+ original_context_hash: result.original_context_hash
10345
+ }
10346
+ );
10347
+ this.stats.fields_redacted += result.fields_redacted;
10348
+ this.stats.fields_hashed += result.fields_hashed;
10349
+ this.stats.fields_blocked += deniedFields.length;
10350
+ return originalHandler(filteredArgs);
10351
+ }
10352
+ /**
10353
+ * Filter tool arguments using built-in sensitive patterns.
10354
+ * This provides baseline protection when no explicit policy is configured.
10355
+ */
10356
+ async filterWithBuiltinPatterns(toolName, args, originalHandler) {
10357
+ const fieldsToRedact = [];
10358
+ const originalHash = hashToString(
10359
+ stringToBytes(JSON.stringify(args))
10360
+ );
10361
+ for (const field of Object.keys(args)) {
10362
+ if (matchesPattern(field, BUILTIN_SENSITIVE_PATTERNS)) {
10363
+ fieldsToRedact.push(field);
10364
+ }
10365
+ }
10366
+ if (fieldsToRedact.length === 0) {
10367
+ this.auditLog.append(
10368
+ "l2",
10369
+ "context_gate_enforcer_builtin_pass",
10370
+ "system",
10371
+ {
10372
+ tool_name: toolName,
10373
+ reason: "No sensitive field patterns detected"
10374
+ }
10375
+ );
10376
+ return originalHandler(args);
10377
+ }
10378
+ const filteredArgs = {};
10379
+ for (const [key, value] of Object.entries(args)) {
10380
+ if (fieldsToRedact.includes(key)) {
10381
+ filteredArgs[key] = "[REDACTED]";
10382
+ } else {
10383
+ filteredArgs[key] = value;
10384
+ }
10385
+ }
10386
+ const filteredHash = hashToString(
10387
+ stringToBytes(JSON.stringify(filteredArgs))
10388
+ );
10389
+ if (this.config.log_only) {
10390
+ this.auditLog.append(
10391
+ "l2",
10392
+ "context_gate_enforcer_builtin_log_only",
10393
+ "system",
10394
+ {
10395
+ tool_name: toolName,
10396
+ fields_redacted: fieldsToRedact.length,
10397
+ redacted_fields: fieldsToRedact,
10398
+ original_context_hash: originalHash
10399
+ }
10400
+ );
10401
+ this.stats.fields_redacted += fieldsToRedact.length;
10402
+ return originalHandler(args);
10403
+ }
10404
+ this.auditLog.append(
10405
+ "l2",
10406
+ "context_gate_enforcer_builtin_filter",
10407
+ "system",
10408
+ {
10409
+ tool_name: toolName,
10410
+ fields_redacted: fieldsToRedact.length,
10411
+ redacted_fields: fieldsToRedact,
10412
+ original_context_hash: originalHash,
10413
+ filtered_context_hash: filteredHash
10414
+ }
10415
+ );
10416
+ this.stats.fields_redacted += fieldsToRedact.length;
10417
+ return originalHandler(filteredArgs);
10418
+ }
10419
+ /**
10420
+ * Check if a tool should be filtered based on bypass prefixes.
10421
+ *
10422
+ * SEC-033: Uses exact namespace component matching, not bare startsWith().
10423
+ * A prefix of "sanctuary/" matches "sanctuary/state_read" but NOT
10424
+ * "sanctuary_evil/steal_data" (no slash boundary confusion). The prefix
10425
+ * must match exactly up to its length, and the prefix must end with "/"
10426
+ * to enforce namespace boundaries (if it doesn't, we add one for safety).
10427
+ */
10428
+ shouldFilter(toolName) {
10429
+ for (const prefix of this.config.bypass_prefixes) {
10430
+ const safePrefix = prefix.endsWith("/") ? prefix : prefix + "/";
10431
+ if (toolName === safePrefix.slice(0, -1) || toolName.startsWith(safePrefix)) {
10432
+ return false;
10433
+ }
10434
+ }
10435
+ return true;
10436
+ }
10437
+ /**
10438
+ * Extract provider category from tool name.
10439
+ * Default: "tool-api". Override for specific patterns.
10440
+ */
10441
+ extractProviderCategory(toolName) {
10442
+ if (toolName.includes("inference") || toolName.includes("llm")) {
10443
+ return "inference";
10444
+ }
10445
+ if (toolName.includes("log") || toolName.includes("telemetry")) {
10446
+ return "logging";
10447
+ }
10448
+ if (toolName.includes("analytics") || toolName.includes("metric")) {
10449
+ return "analytics";
10450
+ }
10451
+ return "tool-api";
10452
+ }
10453
+ /**
10454
+ * Build filtered arguments from filter decisions.
10455
+ */
10456
+ buildFilteredArgs(originalArgs, decisions) {
10457
+ const filtered = {};
10458
+ for (const decision of decisions) {
10459
+ switch (decision.action) {
10460
+ case "allow":
10461
+ filtered[decision.field] = originalArgs[decision.field];
10462
+ break;
10463
+ case "redact":
10464
+ filtered[decision.field] = "[REDACTED]";
10465
+ break;
10466
+ case "hash":
10467
+ filtered[decision.field] = decision.hash_value;
10468
+ break;
10469
+ case "summarize":
10470
+ filtered[decision.field] = originalArgs[decision.field];
10471
+ break;
10472
+ }
10473
+ }
10474
+ return filtered;
10475
+ }
10476
+ /**
10477
+ * Set the active policy ID.
10478
+ */
10479
+ setDefaultPolicy(policyId) {
10480
+ this.config.default_policy_id = policyId;
10481
+ }
10482
+ /**
10483
+ * Get current enforcer status and stats.
10484
+ */
10485
+ getStatus() {
10486
+ return {
10487
+ enabled: this.config.enabled,
10488
+ log_only: this.config.log_only,
10489
+ default_policy_id: this.config.default_policy_id ?? null,
10490
+ stats: { ...this.stats }
10491
+ };
10492
+ }
10493
+ /**
10494
+ * Toggle enforcer enabled state.
10495
+ */
10496
+ setEnabled(enabled) {
10497
+ this.config.enabled = enabled;
10498
+ }
10499
+ /**
10500
+ * Toggle log_only mode.
10501
+ */
10502
+ setLogOnly(logOnly) {
10503
+ this.config.log_only = logOnly;
10504
+ }
10505
+ /**
10506
+ * Reset stats counters.
10507
+ */
10508
+ resetStats() {
10509
+ this.stats = {
10510
+ calls_inspected: 0,
10511
+ calls_bypassed: 0,
10512
+ fields_redacted: 0,
10513
+ fields_hashed: 0,
10514
+ fields_blocked: 0,
10515
+ calls_blocked: 0
10516
+ };
10517
+ }
10518
+ };
10519
+
8778
10520
  // src/l2-operational/context-gate-tools.ts
8779
10521
  function createContextGateTools(storage, masterKey, auditLog) {
8780
10522
  const policyStore = new ContextGatePolicyStore(storage, masterKey);
10523
+ const enforcerConfig = {
10524
+ enabled: false,
10525
+ // Off by default; agents must explicitly enable it
10526
+ bypass_prefixes: ["sanctuary/"],
10527
+ // Skip internal tools by default
10528
+ log_only: false,
10529
+ // Filter immediately
10530
+ on_deny: "block"
10531
+ // Block requests with denied fields
10532
+ };
10533
+ const enforcer = new ContextGateEnforcer(policyStore, auditLog, enforcerConfig);
8781
10534
  const tools = [
8782
10535
  // ── Set Policy ──────────────────────────────────────────────────
8783
10536
  {
@@ -9130,9 +10883,121 @@ function createContextGateTools(storage, masterKey, auditLog) {
9130
10883
  message: policies.length === 0 ? "No context-gating policies configured. Use sanctuary/context_gate_set_policy to create one." : `${policies.length} context-gating ${policies.length === 1 ? "policy" : "policies"} configured.`
9131
10884
  });
9132
10885
  }
10886
+ },
10887
+ // ── Enforcer Status ─────────────────────────────────────────────────
10888
+ {
10889
+ name: "sanctuary/context_gate_enforcer_status",
10890
+ description: "Get the status of the automatic context gate enforcer, including enabled/disabled state, log_only mode, active policy, and statistics. The enforcer automatically filters tool arguments when enabled. Use this to monitor what the enforcer has been filtering.",
10891
+ inputSchema: {
10892
+ type: "object",
10893
+ properties: {}
10894
+ },
10895
+ handler: async () => {
10896
+ const status = enforcer.getStatus();
10897
+ auditLog.append(
10898
+ "l2",
10899
+ "context_gate_enforcer_status_query",
10900
+ "system",
10901
+ {
10902
+ enabled: status.enabled,
10903
+ log_only: status.log_only,
10904
+ default_policy_id: status.default_policy_id
10905
+ }
10906
+ );
10907
+ return toolResult({
10908
+ enforcer_status: status,
10909
+ description: "The enforcer is " + (status.enabled ? "enabled" : "disabled") + ". " + (status.log_only ? "Currently in log_only mode \u2014 filtering is logged but not applied." : "Filtering is actively applied to tool arguments."),
10910
+ guidance: status.stats.calls_inspected > 0 ? `Over ${status.stats.calls_inspected} tool calls, ${status.stats.fields_redacted} sensitive fields were redacted. Use sanctuary/context_gate_enforcer_configure to adjust settings.` : "No tool calls have been inspected yet."
10911
+ });
10912
+ }
10913
+ },
10914
+ // ── Enforcer Configuration ──────────────────────────────────────────
10915
+ {
10916
+ name: "sanctuary/context_gate_enforcer_configure",
10917
+ description: "Configure the automatic context gate enforcer. Control whether it filters tool arguments, toggle log_only mode for gradual rollout, set the active policy, and choose what to do when denied fields are encountered (block the request or redact the field). Use this to enable automatic context protection.",
10918
+ inputSchema: {
10919
+ type: "object",
10920
+ properties: {
10921
+ enabled: {
10922
+ type: "boolean",
10923
+ description: "Enable or disable the automatic enforcer. When disabled, no filtering occurs. Default: leave unchanged."
10924
+ },
10925
+ log_only: {
10926
+ type: "boolean",
10927
+ description: "Enable log_only mode: filter decisions are logged but original args are passed to handlers. Useful for monitoring before enabling actual filtering. Default: leave unchanged."
10928
+ },
10929
+ default_policy_id: {
10930
+ type: "string",
10931
+ description: "Set the default context-gating policy to use for filtering. If not set, the enforcer uses built-in sensitive field patterns. Default: leave unchanged."
10932
+ },
10933
+ on_deny: {
10934
+ type: "string",
10935
+ enum: ["block", "redact"],
10936
+ description: "Action to take when a field triggers the deny action: 'block' returns an error and prevents the call, 'redact' replaces the denied field with [REDACTED] and continues. Default: leave unchanged."
10937
+ },
10938
+ reset_stats: {
10939
+ type: "boolean",
10940
+ description: "Reset the enforcer statistics counters to zero. Default: false."
10941
+ }
10942
+ }
10943
+ },
10944
+ handler: async (args) => {
10945
+ const changes = {};
10946
+ if (args.enabled !== void 0) {
10947
+ enforcer.setEnabled(args.enabled);
10948
+ changes.enabled = args.enabled;
10949
+ }
10950
+ if (args.log_only !== void 0) {
10951
+ enforcer.setLogOnly(args.log_only);
10952
+ changes.log_only = args.log_only;
10953
+ }
10954
+ if (args.default_policy_id !== void 0) {
10955
+ const policyId = args.default_policy_id;
10956
+ const policy = await policyStore.get(policyId);
10957
+ if (!policy) {
10958
+ return toolResult({
10959
+ error: "policy_not_found",
10960
+ message: `No context-gating policy found with ID "${policyId}"`
10961
+ });
10962
+ }
10963
+ enforcer.setDefaultPolicy(policyId);
10964
+ changes.default_policy_id = policyId;
10965
+ }
10966
+ if (args.on_deny !== void 0) {
10967
+ const onDeny = args.on_deny;
10968
+ if (onDeny !== "block" && onDeny !== "redact") {
10969
+ return toolResult({
10970
+ error: "invalid_on_deny",
10971
+ message: "on_deny must be 'block' or 'redact'"
10972
+ });
10973
+ }
10974
+ enforcerConfig.on_deny = onDeny;
10975
+ changes.on_deny = onDeny;
10976
+ }
10977
+ if (args.reset_stats === true) {
10978
+ enforcer.resetStats();
10979
+ changes.reset_stats = true;
10980
+ }
10981
+ const newStatus = enforcer.getStatus();
10982
+ auditLog.append(
10983
+ "l2",
10984
+ "context_gate_enforcer_configure",
10985
+ "system",
10986
+ {
10987
+ changes,
10988
+ new_status: newStatus
10989
+ }
10990
+ );
10991
+ return toolResult({
10992
+ configured: true,
10993
+ changes,
10994
+ new_status: newStatus,
10995
+ message: Object.keys(changes).length > 0 ? "Enforcer configuration updated." : "No changes made (no configuration parameters provided)."
10996
+ });
10997
+ }
9133
10998
  }
9134
10999
  ];
9135
- return { tools, policyStore };
11000
+ return { tools, policyStore, enforcer };
9136
11001
  }
9137
11002
  function checkMemoryProtection() {
9138
11003
  const checks = {
@@ -9932,11 +11797,7 @@ async function createSanctuaryServer(options) {
9932
11797
  handshakeResults
9933
11798
  );
9934
11799
  const { tools: auditTools } = createAuditTools(config);
9935
- const { tools: contextGateTools } = createContextGateTools(
9936
- storage,
9937
- masterKey,
9938
- auditLog
9939
- );
11800
+ const { tools: contextGateTools, enforcer: contextGateEnforcer } = createContextGateTools(storage, masterKey, auditLog);
9940
11801
  const hardeningTools = createL2HardeningTools(config.storage_path, auditLog);
9941
11802
  const policy = await loadPrincipalPolicy(config.storage_path);
9942
11803
  const baseline = new BaselineTracker(storage, masterKey);
@@ -9974,9 +11835,27 @@ async function createSanctuaryServer(options) {
9974
11835
  } else {
9975
11836
  approvalChannel = new StderrApprovalChannel(policy.approval_channel);
9976
11837
  }
9977
- const gate = new ApprovalGate(policy, baseline, approvalChannel, auditLog);
11838
+ const injectionDetector = new InjectionDetector({
11839
+ enabled: true,
11840
+ sensitivity: "medium",
11841
+ on_detection: "escalate"
11842
+ });
11843
+ const onInjectionAlert = dashboard ? (alert) => {
11844
+ dashboard.broadcastSSE("injection-alert", {
11845
+ tool: alert.toolName,
11846
+ confidence: alert.result.confidence,
11847
+ signals: alert.result.signals.map((s) => ({
11848
+ type: s.type,
11849
+ location: s.location,
11850
+ severity: s.severity
11851
+ })),
11852
+ recommendation: alert.result.recommendation,
11853
+ timestamp: alert.timestamp
11854
+ });
11855
+ } : void 0;
11856
+ const gate = new ApprovalGate(policy, baseline, approvalChannel, auditLog, injectionDetector, onInjectionAlert);
9978
11857
  const policyTools = createPrincipalPolicyTools(policy, baseline, auditLog);
9979
- const allTools = [
11858
+ let allTools = [
9980
11859
  ...l1Tools,
9981
11860
  ...l2Tools,
9982
11861
  ...l3Tools,
@@ -9991,6 +11870,10 @@ async function createSanctuaryServer(options) {
9991
11870
  ...hardeningTools,
9992
11871
  manifestTool
9993
11872
  ];
11873
+ allTools = allTools.map((tool) => ({
11874
+ ...tool,
11875
+ handler: contextGateEnforcer.wrapHandler(tool.name, tool.handler)
11876
+ }));
9994
11877
  const server = createServer(allTools, { gate });
9995
11878
  await saveConfig(config);
9996
11879
  const saveBaseline = () => {
@@ -10021,10 +11904,12 @@ exports.BaselineTracker = BaselineTracker;
10021
11904
  exports.CONTEXT_GATE_TEMPLATES = TEMPLATES;
10022
11905
  exports.CallbackApprovalChannel = CallbackApprovalChannel;
10023
11906
  exports.CommitmentStore = CommitmentStore;
11907
+ exports.ContextGateEnforcer = ContextGateEnforcer;
10024
11908
  exports.ContextGatePolicyStore = ContextGatePolicyStore;
10025
11909
  exports.DashboardApprovalChannel = DashboardApprovalChannel;
10026
11910
  exports.FederationRegistry = FederationRegistry;
10027
11911
  exports.FilesystemStorage = FilesystemStorage;
11912
+ exports.InjectionDetector = InjectionDetector;
10028
11913
  exports.MemoryStorage = MemoryStorage;
10029
11914
  exports.PolicyStore = PolicyStore;
10030
11915
  exports.ReputationStore = ReputationStore;