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