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