4runr-os 2.10.74 → 2.10.76
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/apps/gateway/package-lock.json +55 -55
- package/dist/tui-handlers.d.ts.map +1 -1
- package/dist/tui-handlers.js +36 -11
- package/dist/tui-handlers.js.map +1 -1
- package/mk3-tui/binaries/win32-x64/mk3-tui.exe +0 -0
- package/mk3-tui/src/app.rs +188 -49
- package/mk3-tui/src/main.rs +107 -0
- package/mk3-tui/src/screens/mod.rs +3 -1
- package/mk3-tui/src/ui/mod.rs +1 -0
- package/mk3-tui/src/ui/shield_dashboard.rs +723 -0
- package/package.json +2 -2
|
@@ -0,0 +1,723 @@
|
|
|
1
|
+
use crate::app::AppState;
|
|
2
|
+
use ratatui::prelude::*;
|
|
3
|
+
use ratatui::widgets::{Block, Borders, List, ListItem, Paragraph, Wrap};
|
|
4
|
+
use serde_json::Value;
|
|
5
|
+
|
|
6
|
+
const BRAND_PURPLE: Color = Color::Rgb(138, 43, 226);
|
|
7
|
+
const CYBER_CYAN: Color = Color::Rgb(0, 255, 255);
|
|
8
|
+
const NEON_GREEN: Color = Color::Rgb(57, 255, 20);
|
|
9
|
+
const AMBER_WARN: Color = Color::Rgb(255, 191, 0);
|
|
10
|
+
const SHIELD_ORANGE: Color = Color::Rgb(255, 140, 0);
|
|
11
|
+
const TEXT_PRIMARY: Color = Color::Rgb(230, 230, 240);
|
|
12
|
+
const TEXT_DIM: Color = Color::Rgb(100, 100, 120);
|
|
13
|
+
const TEXT_MUTED: Color = Color::Rgb(60, 60, 80);
|
|
14
|
+
const BG_PANEL: Color = Color::Rgb(18, 18, 25);
|
|
15
|
+
|
|
16
|
+
const ABOUT_SHIELD: &[&str] = &[
|
|
17
|
+
"Shield is 4Runr's input/output safety layer on the Gateway run path.",
|
|
18
|
+
"",
|
|
19
|
+
"Production enforcement (every real run):",
|
|
20
|
+
" • Input check before agent execution starts",
|
|
21
|
+
" • Injection patterns → block (failed run)",
|
|
22
|
+
" • PII in prompts → mask or block per policy",
|
|
23
|
+
" • Output check after agent returns",
|
|
24
|
+
"",
|
|
25
|
+
"This screen shows live Gateway state — no test runs required.",
|
|
26
|
+
"Counters increment as real agent/API traffic is evaluated.",
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
#[derive(Debug, Clone)]
|
|
30
|
+
pub struct ShieldBlockRow {
|
|
31
|
+
pub id: String,
|
|
32
|
+
pub name: String,
|
|
33
|
+
pub reason: String,
|
|
34
|
+
pub created_at: String,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
38
|
+
pub enum ShieldTab {
|
|
39
|
+
Overview,
|
|
40
|
+
Enforcement,
|
|
41
|
+
Help,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
impl ShieldTab {
|
|
45
|
+
pub fn label(self) -> &'static str {
|
|
46
|
+
match self {
|
|
47
|
+
ShieldTab::Overview => "Overview",
|
|
48
|
+
ShieldTab::Enforcement => "Enforcement",
|
|
49
|
+
ShieldTab::Help => "Help",
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
pub fn next(self) -> Self {
|
|
54
|
+
match self {
|
|
55
|
+
ShieldTab::Overview => ShieldTab::Enforcement,
|
|
56
|
+
ShieldTab::Enforcement => ShieldTab::Help,
|
|
57
|
+
ShieldTab::Help => ShieldTab::Overview,
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
pub fn prev(self) -> Self {
|
|
62
|
+
match self {
|
|
63
|
+
ShieldTab::Overview => ShieldTab::Help,
|
|
64
|
+
ShieldTab::Enforcement => ShieldTab::Overview,
|
|
65
|
+
ShieldTab::Help => ShieldTab::Enforcement,
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
#[derive(Debug, Clone)]
|
|
71
|
+
pub struct ShieldDashboardState {
|
|
72
|
+
pub tab: ShieldTab,
|
|
73
|
+
pub selected_block_index: usize,
|
|
74
|
+
pub loaded_once: bool,
|
|
75
|
+
pub enabled: bool,
|
|
76
|
+
pub mode: String,
|
|
77
|
+
pub health_status: String,
|
|
78
|
+
pub pii_enabled: bool,
|
|
79
|
+
pub injection_enabled: bool,
|
|
80
|
+
pub injection_action: String,
|
|
81
|
+
pub hallucination_enabled: bool,
|
|
82
|
+
pub hallucination_action: String,
|
|
83
|
+
pub decisions_total: u64,
|
|
84
|
+
pub blocks_total: u64,
|
|
85
|
+
pub masks_total: u64,
|
|
86
|
+
pub rewrites_total: u64,
|
|
87
|
+
pub warnings: Vec<String>,
|
|
88
|
+
pub recent_blocks: Vec<ShieldBlockRow>,
|
|
89
|
+
pub loading: bool,
|
|
90
|
+
pub loading_started: Option<std::time::Instant>,
|
|
91
|
+
pub error: Option<String>,
|
|
92
|
+
pub last_refresh: Option<std::time::Instant>,
|
|
93
|
+
pub auto_refresh_enabled: bool,
|
|
94
|
+
pub auto_refresh_interval: std::time::Duration,
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
impl Default for ShieldDashboardState {
|
|
98
|
+
fn default() -> Self {
|
|
99
|
+
Self {
|
|
100
|
+
tab: ShieldTab::Overview,
|
|
101
|
+
selected_block_index: 0,
|
|
102
|
+
loaded_once: false,
|
|
103
|
+
enabled: false,
|
|
104
|
+
mode: "off".to_string(),
|
|
105
|
+
health_status: String::new(),
|
|
106
|
+
pii_enabled: false,
|
|
107
|
+
injection_enabled: false,
|
|
108
|
+
injection_action: String::new(),
|
|
109
|
+
hallucination_enabled: false,
|
|
110
|
+
hallucination_action: String::new(),
|
|
111
|
+
decisions_total: 0,
|
|
112
|
+
blocks_total: 0,
|
|
113
|
+
masks_total: 0,
|
|
114
|
+
rewrites_total: 0,
|
|
115
|
+
warnings: Vec::new(),
|
|
116
|
+
recent_blocks: Vec::new(),
|
|
117
|
+
loading: false,
|
|
118
|
+
loading_started: None,
|
|
119
|
+
error: None,
|
|
120
|
+
last_refresh: None,
|
|
121
|
+
auto_refresh_enabled: true,
|
|
122
|
+
auto_refresh_interval: std::time::Duration::from_secs(5),
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
impl ShieldDashboardState {
|
|
128
|
+
|
|
129
|
+
pub fn apply_load(
|
|
130
|
+
&mut self,
|
|
131
|
+
health: &ShieldHealthSnap,
|
|
132
|
+
config: &ShieldConfigSnap,
|
|
133
|
+
metrics: &ShieldMetricsSnap,
|
|
134
|
+
warnings: Vec<String>,
|
|
135
|
+
recent: Vec<ShieldBlockRow>,
|
|
136
|
+
) {
|
|
137
|
+
self.loading = false;
|
|
138
|
+
self.loading_started = None;
|
|
139
|
+
self.error = None;
|
|
140
|
+
self.loaded_once = true;
|
|
141
|
+
self.enabled = health.enabled;
|
|
142
|
+
self.mode = health.mode.clone();
|
|
143
|
+
self.health_status = health.status.clone();
|
|
144
|
+
self.pii_enabled = config.pii_enabled;
|
|
145
|
+
self.injection_enabled = config.injection_enabled;
|
|
146
|
+
self.injection_action = config.injection_action.clone();
|
|
147
|
+
self.hallucination_enabled = config.hallucination_enabled;
|
|
148
|
+
self.hallucination_action = config.hallucination_action.clone();
|
|
149
|
+
self.decisions_total = metrics.decisions;
|
|
150
|
+
self.blocks_total = metrics.blocks;
|
|
151
|
+
self.masks_total = metrics.masks;
|
|
152
|
+
self.rewrites_total = metrics.rewrites;
|
|
153
|
+
self.warnings = warnings;
|
|
154
|
+
self.recent_blocks = recent;
|
|
155
|
+
if self.selected_block_index >= self.recent_blocks.len() {
|
|
156
|
+
self.selected_block_index = 0;
|
|
157
|
+
}
|
|
158
|
+
self.last_refresh = Some(std::time::Instant::now());
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
pub fn mark_loading(&mut self) {
|
|
162
|
+
self.loading = true;
|
|
163
|
+
if self.loading_started.is_none() {
|
|
164
|
+
self.loading_started = Some(std::time::Instant::now());
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
pub fn fail_loading(&mut self, message: String) {
|
|
169
|
+
self.loading = false;
|
|
170
|
+
self.loading_started = None;
|
|
171
|
+
self.error = Some(message);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
#[derive(Debug, Clone, Default)]
|
|
176
|
+
pub struct ShieldHealthSnap {
|
|
177
|
+
pub enabled: bool,
|
|
178
|
+
pub mode: String,
|
|
179
|
+
pub status: String,
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
#[derive(Debug, Clone, Default)]
|
|
183
|
+
pub struct ShieldConfigSnap {
|
|
184
|
+
pub pii_enabled: bool,
|
|
185
|
+
pub injection_enabled: bool,
|
|
186
|
+
pub injection_action: String,
|
|
187
|
+
pub hallucination_enabled: bool,
|
|
188
|
+
pub hallucination_action: String,
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
#[derive(Debug, Clone, Default)]
|
|
192
|
+
pub struct ShieldMetricsSnap {
|
|
193
|
+
pub decisions: u64,
|
|
194
|
+
pub blocks: u64,
|
|
195
|
+
pub masks: u64,
|
|
196
|
+
pub rewrites: u64,
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
pub fn parse_shield_load(
|
|
200
|
+
health_val: &Value,
|
|
201
|
+
config_val: &Value,
|
|
202
|
+
metrics_val: &Value,
|
|
203
|
+
gateway_health_val: &Value,
|
|
204
|
+
runs_val: &Value,
|
|
205
|
+
) -> (
|
|
206
|
+
ShieldHealthSnap,
|
|
207
|
+
ShieldConfigSnap,
|
|
208
|
+
ShieldMetricsSnap,
|
|
209
|
+
Vec<String>,
|
|
210
|
+
Vec<ShieldBlockRow>,
|
|
211
|
+
) {
|
|
212
|
+
let health_obj = health_val.as_object();
|
|
213
|
+
let health = ShieldHealthSnap {
|
|
214
|
+
enabled: health_obj
|
|
215
|
+
.and_then(|o| o.get("enabled"))
|
|
216
|
+
.and_then(|v| v.as_bool())
|
|
217
|
+
.unwrap_or(false),
|
|
218
|
+
mode: health_obj
|
|
219
|
+
.and_then(|o| o.get("mode"))
|
|
220
|
+
.and_then(|v| v.as_str())
|
|
221
|
+
.unwrap_or("off")
|
|
222
|
+
.to_string(),
|
|
223
|
+
status: if health_obj
|
|
224
|
+
.and_then(|o| o.get("enabled"))
|
|
225
|
+
.and_then(|v| v.as_bool())
|
|
226
|
+
.unwrap_or(false)
|
|
227
|
+
{
|
|
228
|
+
"active".to_string()
|
|
229
|
+
} else {
|
|
230
|
+
"disabled".to_string()
|
|
231
|
+
},
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
let cfg = config_val
|
|
235
|
+
.get("config")
|
|
236
|
+
.or(Some(config_val))
|
|
237
|
+
.and_then(|v| v.as_object());
|
|
238
|
+
let pii = cfg.and_then(|c| c.get("pii")).and_then(|v| v.as_object());
|
|
239
|
+
let injection = cfg.and_then(|c| c.get("injection")).and_then(|v| v.as_object());
|
|
240
|
+
let hallucination = cfg
|
|
241
|
+
.and_then(|c| c.get("hallucination"))
|
|
242
|
+
.and_then(|v| v.as_object());
|
|
243
|
+
|
|
244
|
+
let config = ShieldConfigSnap {
|
|
245
|
+
pii_enabled: pii
|
|
246
|
+
.and_then(|o| o.get("enabled"))
|
|
247
|
+
.and_then(|v| v.as_bool())
|
|
248
|
+
.unwrap_or(false),
|
|
249
|
+
injection_enabled: injection
|
|
250
|
+
.and_then(|o| o.get("enabled"))
|
|
251
|
+
.and_then(|v| v.as_bool())
|
|
252
|
+
.unwrap_or(false),
|
|
253
|
+
injection_action: injection
|
|
254
|
+
.and_then(|o| o.get("action"))
|
|
255
|
+
.and_then(|v| v.as_str())
|
|
256
|
+
.unwrap_or("—")
|
|
257
|
+
.to_string(),
|
|
258
|
+
hallucination_enabled: hallucination
|
|
259
|
+
.and_then(|o| o.get("enabled"))
|
|
260
|
+
.and_then(|v| v.as_bool())
|
|
261
|
+
.unwrap_or(false),
|
|
262
|
+
hallucination_action: hallucination
|
|
263
|
+
.and_then(|o| o.get("action"))
|
|
264
|
+
.and_then(|v| v.as_str())
|
|
265
|
+
.unwrap_or("—")
|
|
266
|
+
.to_string(),
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
let metrics = ShieldMetricsSnap {
|
|
270
|
+
decisions: metrics_val
|
|
271
|
+
.get("decisions")
|
|
272
|
+
.and_then(|v| v.as_u64())
|
|
273
|
+
.unwrap_or(0),
|
|
274
|
+
blocks: metrics_val
|
|
275
|
+
.get("blocks")
|
|
276
|
+
.and_then(|v| v.as_u64())
|
|
277
|
+
.unwrap_or(0),
|
|
278
|
+
masks: metrics_val
|
|
279
|
+
.get("masks")
|
|
280
|
+
.and_then(|v| v.as_u64())
|
|
281
|
+
.unwrap_or(0),
|
|
282
|
+
rewrites: metrics_val
|
|
283
|
+
.get("rewrites")
|
|
284
|
+
.and_then(|v| v.as_u64())
|
|
285
|
+
.unwrap_or(0),
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
let mut warnings: Vec<String> = Vec::new();
|
|
289
|
+
if let Some(sh) = gateway_health_val
|
|
290
|
+
.get("checks")
|
|
291
|
+
.and_then(|c| c.get("shield"))
|
|
292
|
+
.and_then(|v| v.as_object())
|
|
293
|
+
{
|
|
294
|
+
if let Some(st) = sh.get("status").and_then(|v| v.as_str()) {
|
|
295
|
+
if st != "ok" && st != "up" {
|
|
296
|
+
warnings.push(format!("Gateway health: {}", st));
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
if let Some(arr) = sh.get("warnings").and_then(|v| v.as_array()) {
|
|
300
|
+
for w in arr {
|
|
301
|
+
if let Some(s) = w.as_str() {
|
|
302
|
+
warnings.push(s.to_string());
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
let mut recent: Vec<ShieldBlockRow> = Vec::new();
|
|
309
|
+
let runs_arr = runs_val
|
|
310
|
+
.as_array()
|
|
311
|
+
.or_else(|| runs_val.get("runs").and_then(|v| v.as_array()));
|
|
312
|
+
if let Some(arr) = runs_arr {
|
|
313
|
+
for item in arr {
|
|
314
|
+
let o = match item.as_object() {
|
|
315
|
+
Some(o) => o,
|
|
316
|
+
None => continue,
|
|
317
|
+
};
|
|
318
|
+
let output = o.get("output").and_then(|v| v.as_object());
|
|
319
|
+
let err = output
|
|
320
|
+
.and_then(|oo| oo.get("error"))
|
|
321
|
+
.and_then(|v| v.as_str())
|
|
322
|
+
.unwrap_or("");
|
|
323
|
+
let logs_shield = o
|
|
324
|
+
.get("logs")
|
|
325
|
+
.and_then(|l| l.as_array())
|
|
326
|
+
.map(|arr| {
|
|
327
|
+
arr.iter().any(|e| {
|
|
328
|
+
e.get("message")
|
|
329
|
+
.and_then(|m| m.as_str())
|
|
330
|
+
.map(|s| s.to_lowercase().contains("shield"))
|
|
331
|
+
.unwrap_or(false)
|
|
332
|
+
})
|
|
333
|
+
})
|
|
334
|
+
.unwrap_or(false);
|
|
335
|
+
if !err.to_lowercase().contains("shield") && !logs_shield {
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
let id = o
|
|
339
|
+
.get("id")
|
|
340
|
+
.and_then(|v| v.as_str())
|
|
341
|
+
.unwrap_or("?")
|
|
342
|
+
.to_string();
|
|
343
|
+
let name = o
|
|
344
|
+
.get("name")
|
|
345
|
+
.and_then(|v| v.as_str())
|
|
346
|
+
.unwrap_or("Run")
|
|
347
|
+
.to_string();
|
|
348
|
+
let reason = output
|
|
349
|
+
.and_then(|oo| oo.get("reason"))
|
|
350
|
+
.and_then(|v| v.as_str())
|
|
351
|
+
.unwrap_or(err)
|
|
352
|
+
.to_string();
|
|
353
|
+
let created_at = o
|
|
354
|
+
.get("createdAt")
|
|
355
|
+
.and_then(|v| v.as_str())
|
|
356
|
+
.map(|s| s.chars().take(19).collect())
|
|
357
|
+
.unwrap_or_else(|| "—".to_string());
|
|
358
|
+
recent.push(ShieldBlockRow {
|
|
359
|
+
id,
|
|
360
|
+
name,
|
|
361
|
+
reason,
|
|
362
|
+
created_at,
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
recent.truncate(12);
|
|
367
|
+
|
|
368
|
+
(health, config, metrics, warnings, recent)
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
pub fn render(f: &mut Frame, state: &AppState) {
|
|
372
|
+
let area = f.size();
|
|
373
|
+
let sd = &state.shield_dashboard;
|
|
374
|
+
|
|
375
|
+
use ratatui::layout::{Constraint, Direction, Layout};
|
|
376
|
+
|
|
377
|
+
let refresh_hint = if sd.loading {
|
|
378
|
+
" ↻"
|
|
379
|
+
} else {
|
|
380
|
+
""
|
|
381
|
+
};
|
|
382
|
+
let live = if sd.auto_refresh_enabled { " · LIVE" } else { "" };
|
|
383
|
+
let header_title = format!(
|
|
384
|
+
" 🛡️ Shield — {} · {} block(s) · {} mask(s){}{} ",
|
|
385
|
+
if sd.loaded_once {
|
|
386
|
+
sd.mode.to_uppercase()
|
|
387
|
+
} else {
|
|
388
|
+
"…".to_string()
|
|
389
|
+
},
|
|
390
|
+
sd.blocks_total,
|
|
391
|
+
sd.masks_total,
|
|
392
|
+
live,
|
|
393
|
+
refresh_hint
|
|
394
|
+
);
|
|
395
|
+
|
|
396
|
+
let chunks = Layout::default()
|
|
397
|
+
.direction(Direction::Vertical)
|
|
398
|
+
.constraints([
|
|
399
|
+
Constraint::Length(3),
|
|
400
|
+
Constraint::Length(3),
|
|
401
|
+
Constraint::Min(8),
|
|
402
|
+
Constraint::Length(3),
|
|
403
|
+
])
|
|
404
|
+
.split(area);
|
|
405
|
+
|
|
406
|
+
let header = Block::default()
|
|
407
|
+
.title(header_title)
|
|
408
|
+
.borders(Borders::ALL)
|
|
409
|
+
.border_style(Style::default().fg(SHIELD_ORANGE))
|
|
410
|
+
.style(Style::default().bg(BG_PANEL));
|
|
411
|
+
f.render_widget(header, chunks[0]);
|
|
412
|
+
|
|
413
|
+
render_tab_bar(f, chunks[1], sd);
|
|
414
|
+
|
|
415
|
+
if !sd.loaded_once && sd.loading {
|
|
416
|
+
let block = Block::default()
|
|
417
|
+
.title(" Loading ")
|
|
418
|
+
.borders(Borders::ALL)
|
|
419
|
+
.border_style(Style::default().fg(CYBER_CYAN));
|
|
420
|
+
let inner = block.inner(chunks[2]);
|
|
421
|
+
f.render_widget(block, chunks[2]);
|
|
422
|
+
f.render_widget(
|
|
423
|
+
Paragraph::new(vec![
|
|
424
|
+
Line::from(""),
|
|
425
|
+
Line::from("Fetching Shield health, config, metrics, and run history…")
|
|
426
|
+
.style(Style::default().fg(TEXT_PRIMARY)),
|
|
427
|
+
Line::from(""),
|
|
428
|
+
Line::from("Press R to retry if this takes more than a few seconds.")
|
|
429
|
+
.style(Style::default().fg(TEXT_DIM)),
|
|
430
|
+
])
|
|
431
|
+
.alignment(Alignment::Center),
|
|
432
|
+
inner,
|
|
433
|
+
);
|
|
434
|
+
} else {
|
|
435
|
+
match sd.tab {
|
|
436
|
+
ShieldTab::Overview => {
|
|
437
|
+
let mid = Layout::default()
|
|
438
|
+
.direction(Direction::Horizontal)
|
|
439
|
+
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
|
|
440
|
+
.split(chunks[2]);
|
|
441
|
+
render_status_panel(f, mid[0], sd);
|
|
442
|
+
render_metrics_panel(f, mid[1], sd);
|
|
443
|
+
}
|
|
444
|
+
ShieldTab::Enforcement => render_enforcement_panel(f, chunks[2], sd),
|
|
445
|
+
ShieldTab::Help => render_help_panel(f, chunks[2], sd),
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
let status_line = if let Some(e) = &sd.error {
|
|
450
|
+
format!("⚠ {} | ", e)
|
|
451
|
+
} else if sd.loading {
|
|
452
|
+
"Refreshing… | ".to_string()
|
|
453
|
+
} else {
|
|
454
|
+
String::new()
|
|
455
|
+
};
|
|
456
|
+
let footer_msg = format!(
|
|
457
|
+
"{}Tab/←→ Switch view · ↑↓ Select block · Enter Open run · R Refresh · O Runs · P Monitoring · A Live · ESC Close",
|
|
458
|
+
status_line
|
|
459
|
+
);
|
|
460
|
+
let footer_color = if sd.error.is_some() {
|
|
461
|
+
Color::Rgb(255, 69, 69)
|
|
462
|
+
} else {
|
|
463
|
+
TEXT_DIM
|
|
464
|
+
};
|
|
465
|
+
let footer = Block::default()
|
|
466
|
+
.title(" ⌨️ Actions ")
|
|
467
|
+
.borders(Borders::ALL)
|
|
468
|
+
.border_style(Style::default().fg(TEXT_MUTED));
|
|
469
|
+
let inner = footer.inner(chunks[3]);
|
|
470
|
+
f.render_widget(footer, chunks[3]);
|
|
471
|
+
f.render_widget(
|
|
472
|
+
Paragraph::new(footer_msg)
|
|
473
|
+
.style(Style::default().fg(footer_color))
|
|
474
|
+
.alignment(Alignment::Center),
|
|
475
|
+
inner,
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
fn render_tab_bar(f: &mut Frame, area: Rect, sd: &ShieldDashboardState) {
|
|
480
|
+
let tabs = [ShieldTab::Overview, ShieldTab::Enforcement, ShieldTab::Help];
|
|
481
|
+
let spans: Vec<Span> = tabs
|
|
482
|
+
.iter()
|
|
483
|
+
.flat_map(|tab| {
|
|
484
|
+
let active = *tab == sd.tab;
|
|
485
|
+
[
|
|
486
|
+
Span::styled(
|
|
487
|
+
format!(" {} ", tab.label()),
|
|
488
|
+
Style::default()
|
|
489
|
+
.fg(if active { SHIELD_ORANGE } else { TEXT_DIM })
|
|
490
|
+
.add_modifier(if active {
|
|
491
|
+
Modifier::BOLD
|
|
492
|
+
} else {
|
|
493
|
+
Modifier::empty()
|
|
494
|
+
}),
|
|
495
|
+
),
|
|
496
|
+
Span::raw("│"),
|
|
497
|
+
]
|
|
498
|
+
})
|
|
499
|
+
.collect();
|
|
500
|
+
let block = Block::default()
|
|
501
|
+
.borders(Borders::LEFT | Borders::RIGHT)
|
|
502
|
+
.border_style(Style::default().fg(TEXT_MUTED));
|
|
503
|
+
f.render_widget(
|
|
504
|
+
Paragraph::new(Line::from(spans)).alignment(Alignment::Center),
|
|
505
|
+
block.inner(area),
|
|
506
|
+
);
|
|
507
|
+
f.render_widget(block, area);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
fn render_help_panel(f: &mut Frame, area: Rect, sd: &ShieldDashboardState) {
|
|
511
|
+
let block = Block::default()
|
|
512
|
+
.title(" How Shield works in production ")
|
|
513
|
+
.borders(Borders::ALL)
|
|
514
|
+
.border_style(Style::default().fg(BRAND_PURPLE))
|
|
515
|
+
.style(Style::default().bg(BG_PANEL));
|
|
516
|
+
let inner = block.inner(area);
|
|
517
|
+
f.render_widget(block, area);
|
|
518
|
+
|
|
519
|
+
let mut lines: Vec<Line> = ABOUT_SHIELD
|
|
520
|
+
.iter()
|
|
521
|
+
.map(|l| Line::from(*l).style(Style::default().fg(TEXT_PRIMARY)))
|
|
522
|
+
.collect();
|
|
523
|
+
lines.push(Line::from(""));
|
|
524
|
+
lines.push(
|
|
525
|
+
Line::from("Navigation from this screen:")
|
|
526
|
+
.style(Style::default().fg(NEON_GREEN).bold()),
|
|
527
|
+
);
|
|
528
|
+
lines.push(Line::from(" O — open Run Manager (all runs, SHIELD badge on blocks)"));
|
|
529
|
+
lines.push(Line::from(" P — open Portal Monitoring (Prometheus Shield counters)"));
|
|
530
|
+
lines.push(Line::from(" Enforcement tab → Enter on a row opens that blocked run"));
|
|
531
|
+
if !sd.warnings.is_empty() {
|
|
532
|
+
lines.push(Line::from(""));
|
|
533
|
+
lines.push(Line::from("Active warnings:").style(Style::default().fg(AMBER_WARN).bold()));
|
|
534
|
+
for w in &sd.warnings {
|
|
535
|
+
lines.push(Line::from(format!(" ⚠ {}", w)).style(Style::default().fg(AMBER_WARN)));
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
f.render_widget(Paragraph::new(lines).wrap(Wrap { trim: false }), inner);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
fn render_status_panel(f: &mut Frame, area: Rect, sd: &ShieldDashboardState) {
|
|
542
|
+
let block = Block::default()
|
|
543
|
+
.title(" Status & detectors ")
|
|
544
|
+
.borders(Borders::ALL)
|
|
545
|
+
.border_style(Style::default().fg(CYBER_CYAN))
|
|
546
|
+
.style(Style::default().bg(BG_PANEL));
|
|
547
|
+
let inner = block.inner(area);
|
|
548
|
+
f.render_widget(block, area);
|
|
549
|
+
|
|
550
|
+
let mode_color = match sd.mode.as_str() {
|
|
551
|
+
"enforce" => NEON_GREEN,
|
|
552
|
+
"monitor" => AMBER_WARN,
|
|
553
|
+
_ => TEXT_MUTED,
|
|
554
|
+
};
|
|
555
|
+
|
|
556
|
+
let mut lines = vec![
|
|
557
|
+
Line::from(vec![
|
|
558
|
+
Span::styled("Enabled: ", Style::default().fg(TEXT_DIM)),
|
|
559
|
+
Span::styled(
|
|
560
|
+
if sd.enabled { "YES" } else { "NO" },
|
|
561
|
+
Style::default()
|
|
562
|
+
.fg(if sd.enabled { NEON_GREEN } else { TEXT_MUTED })
|
|
563
|
+
.bold(),
|
|
564
|
+
),
|
|
565
|
+
]),
|
|
566
|
+
Line::from(vec![
|
|
567
|
+
Span::styled("Mode: ", Style::default().fg(TEXT_DIM)),
|
|
568
|
+
Span::styled(
|
|
569
|
+
sd.mode.to_uppercase(),
|
|
570
|
+
Style::default().fg(mode_color).bold(),
|
|
571
|
+
),
|
|
572
|
+
]),
|
|
573
|
+
Line::from(""),
|
|
574
|
+
Line::from("Detectors (production path):").style(Style::default().fg(TEXT_DIM)),
|
|
575
|
+
detector_line("PII", sd.pii_enabled, "mask on input/output".to_string()),
|
|
576
|
+
detector_line(
|
|
577
|
+
"Injection",
|
|
578
|
+
sd.injection_enabled,
|
|
579
|
+
format!("action: {}", sd.injection_action),
|
|
580
|
+
),
|
|
581
|
+
detector_line(
|
|
582
|
+
"Hallucination",
|
|
583
|
+
sd.hallucination_enabled,
|
|
584
|
+
format!("action: {}", sd.hallucination_action),
|
|
585
|
+
),
|
|
586
|
+
];
|
|
587
|
+
|
|
588
|
+
if !sd.warnings.is_empty() {
|
|
589
|
+
lines.push(Line::from(""));
|
|
590
|
+
lines.push(
|
|
591
|
+
Line::from("Warnings:")
|
|
592
|
+
.style(Style::default().fg(AMBER_WARN).bold()),
|
|
593
|
+
);
|
|
594
|
+
for w in &sd.warnings {
|
|
595
|
+
lines.push(Line::from(format!(" ⚠ {}", w)).style(Style::default().fg(AMBER_WARN)));
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
lines.push(Line::from(""));
|
|
600
|
+
for line in ABOUT_SHIELD.iter().take(4) {
|
|
601
|
+
lines.push(Line::from(*line).style(Style::default().fg(TEXT_MUTED)));
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
f.render_widget(Paragraph::new(lines).wrap(Wrap { trim: false }), inner);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
fn detector_line(name: &str, on: bool, detail: String) -> Line<'static> {
|
|
608
|
+
Line::from(vec![
|
|
609
|
+
Span::raw(" "),
|
|
610
|
+
Span::styled(
|
|
611
|
+
if on { "●" } else { "○" },
|
|
612
|
+
Style::default().fg(if on { NEON_GREEN } else { TEXT_MUTED }),
|
|
613
|
+
),
|
|
614
|
+
Span::styled(format!(" {:<14}", name), Style::default().fg(TEXT_PRIMARY)),
|
|
615
|
+
Span::styled(detail, Style::default().fg(TEXT_DIM)),
|
|
616
|
+
])
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
fn render_metrics_panel(f: &mut Frame, area: Rect, sd: &ShieldDashboardState) {
|
|
620
|
+
let block = Block::default()
|
|
621
|
+
.title(" Gateway metrics (cumulative) ")
|
|
622
|
+
.borders(Borders::ALL)
|
|
623
|
+
.border_style(Style::default().fg(BRAND_PURPLE))
|
|
624
|
+
.style(Style::default().bg(BG_PANEL));
|
|
625
|
+
let inner = block.inner(area);
|
|
626
|
+
f.render_widget(block, area);
|
|
627
|
+
|
|
628
|
+
let lines = vec![
|
|
629
|
+
Line::from(""),
|
|
630
|
+
metric_line("Decisions total", sd.decisions_total, TEXT_PRIMARY),
|
|
631
|
+
metric_line("Blocks total", sd.blocks_total, SHIELD_ORANGE),
|
|
632
|
+
metric_line("Masks total", sd.masks_total, CYBER_CYAN),
|
|
633
|
+
metric_line("Rewrites total", sd.rewrites_total, AMBER_WARN),
|
|
634
|
+
Line::from(""),
|
|
635
|
+
Line::from("Source: Gateway Prometheus /metrics").style(Style::default().fg(TEXT_MUTED)),
|
|
636
|
+
Line::from("Updates on refresh — counts all production runs.").style(Style::default().fg(TEXT_MUTED)),
|
|
637
|
+
Line::from(""),
|
|
638
|
+
Line::from(ABOUT_SHIELD[4]).style(Style::default().fg(TEXT_DIM)),
|
|
639
|
+
Line::from(ABOUT_SHIELD[5]).style(Style::default().fg(TEXT_DIM)),
|
|
640
|
+
Line::from(ABOUT_SHIELD[6]).style(Style::default().fg(TEXT_DIM)),
|
|
641
|
+
Line::from(ABOUT_SHIELD[7]).style(Style::default().fg(TEXT_DIM)),
|
|
642
|
+
];
|
|
643
|
+
|
|
644
|
+
f.render_widget(Paragraph::new(lines), inner);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
fn metric_line(label: &str, value: u64, color: Color) -> Line<'static> {
|
|
648
|
+
Line::from(vec![
|
|
649
|
+
Span::styled(format!(" {:<18}", label), Style::default().fg(TEXT_DIM)),
|
|
650
|
+
Span::styled(
|
|
651
|
+
value.to_string(),
|
|
652
|
+
Style::default().fg(color).bold(),
|
|
653
|
+
),
|
|
654
|
+
])
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
fn render_enforcement_panel(f: &mut Frame, area: Rect, sd: &ShieldDashboardState) {
|
|
658
|
+
let block = Block::default()
|
|
659
|
+
.title(format!(
|
|
660
|
+
" Blocked runs ({}) — ↑↓ select · Enter open in Run Manager ",
|
|
661
|
+
sd.recent_blocks.len()
|
|
662
|
+
))
|
|
663
|
+
.borders(Borders::ALL)
|
|
664
|
+
.border_style(Style::default().fg(SHIELD_ORANGE))
|
|
665
|
+
.style(Style::default().bg(BG_PANEL));
|
|
666
|
+
let inner = block.inner(area);
|
|
667
|
+
f.render_widget(block, area);
|
|
668
|
+
|
|
669
|
+
if sd.recent_blocks.is_empty() {
|
|
670
|
+
let empty = vec![
|
|
671
|
+
Line::from(""),
|
|
672
|
+
Line::from("No blocked runs in Gateway history yet.").style(Style::default().fg(TEXT_DIM)),
|
|
673
|
+
Line::from(""),
|
|
674
|
+
Line::from("Shield still evaluates every agent/API run on input.").style(Style::default().fg(TEXT_MUTED)),
|
|
675
|
+
Line::from("Press O to open Run Manager · P for live Prometheus metrics.").style(Style::default().fg(NEON_GREEN)),
|
|
676
|
+
];
|
|
677
|
+
f.render_widget(Paragraph::new(empty).alignment(Alignment::Center), inner);
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
let items: Vec<ListItem> = sd
|
|
682
|
+
.recent_blocks
|
|
683
|
+
.iter()
|
|
684
|
+
.enumerate()
|
|
685
|
+
.map(|(idx, r)| {
|
|
686
|
+
let short_id = if r.id.len() > 8 {
|
|
687
|
+
&r.id[r.id.len() - 8..]
|
|
688
|
+
} else {
|
|
689
|
+
&r.id
|
|
690
|
+
};
|
|
691
|
+
let prefix = if idx == sd.selected_block_index {
|
|
692
|
+
"▶ "
|
|
693
|
+
} else {
|
|
694
|
+
" "
|
|
695
|
+
};
|
|
696
|
+
ListItem::new(Line::from(vec![
|
|
697
|
+
Span::styled(prefix, Style::default().fg(SHIELD_ORANGE).bold()),
|
|
698
|
+
Span::styled(
|
|
699
|
+
format!("{} ", r.created_at),
|
|
700
|
+
Style::default().fg(TEXT_MUTED),
|
|
701
|
+
),
|
|
702
|
+
Span::styled(
|
|
703
|
+
format!("[{}] ", short_id),
|
|
704
|
+
Style::default().fg(CYBER_CYAN),
|
|
705
|
+
),
|
|
706
|
+
Span::styled(
|
|
707
|
+
format!("{} — ", r.name),
|
|
708
|
+
Style::default().fg(if idx == sd.selected_block_index {
|
|
709
|
+
CYBER_CYAN
|
|
710
|
+
} else {
|
|
711
|
+
TEXT_PRIMARY
|
|
712
|
+
}),
|
|
713
|
+
),
|
|
714
|
+
Span::styled(
|
|
715
|
+
r.reason.chars().take(48).collect::<String>(),
|
|
716
|
+
Style::default().fg(SHIELD_ORANGE),
|
|
717
|
+
),
|
|
718
|
+
]))
|
|
719
|
+
})
|
|
720
|
+
.collect();
|
|
721
|
+
|
|
722
|
+
f.render_widget(List::new(items), inner);
|
|
723
|
+
}
|