4runr-os 2.10.74 → 2.10.75
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/tui-handlers.js +33 -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 +98 -49
- package/mk3-tui/src/main.rs +89 -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 +549 -0
- package/package.json +2 -2
|
@@ -0,0 +1,549 @@
|
|
|
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)]
|
|
38
|
+
pub struct ShieldDashboardState {
|
|
39
|
+
pub enabled: bool,
|
|
40
|
+
pub mode: String,
|
|
41
|
+
pub health_status: String,
|
|
42
|
+
pub pii_enabled: bool,
|
|
43
|
+
pub injection_enabled: bool,
|
|
44
|
+
pub injection_action: String,
|
|
45
|
+
pub hallucination_enabled: bool,
|
|
46
|
+
pub hallucination_action: String,
|
|
47
|
+
pub decisions_total: u64,
|
|
48
|
+
pub blocks_total: u64,
|
|
49
|
+
pub masks_total: u64,
|
|
50
|
+
pub rewrites_total: u64,
|
|
51
|
+
pub warnings: Vec<String>,
|
|
52
|
+
pub recent_blocks: Vec<ShieldBlockRow>,
|
|
53
|
+
pub loading: bool,
|
|
54
|
+
pub error: Option<String>,
|
|
55
|
+
pub last_refresh: Option<std::time::Instant>,
|
|
56
|
+
pub auto_refresh_enabled: bool,
|
|
57
|
+
pub auto_refresh_interval: std::time::Duration,
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
impl Default for ShieldDashboardState {
|
|
61
|
+
fn default() -> Self {
|
|
62
|
+
Self {
|
|
63
|
+
enabled: false,
|
|
64
|
+
mode: "off".to_string(),
|
|
65
|
+
health_status: String::new(),
|
|
66
|
+
pii_enabled: false,
|
|
67
|
+
injection_enabled: false,
|
|
68
|
+
injection_action: String::new(),
|
|
69
|
+
hallucination_enabled: false,
|
|
70
|
+
hallucination_action: String::new(),
|
|
71
|
+
decisions_total: 0,
|
|
72
|
+
blocks_total: 0,
|
|
73
|
+
masks_total: 0,
|
|
74
|
+
rewrites_total: 0,
|
|
75
|
+
warnings: Vec::new(),
|
|
76
|
+
recent_blocks: Vec::new(),
|
|
77
|
+
loading: false,
|
|
78
|
+
error: None,
|
|
79
|
+
last_refresh: None,
|
|
80
|
+
auto_refresh_enabled: true,
|
|
81
|
+
auto_refresh_interval: std::time::Duration::from_secs(5),
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
impl ShieldDashboardState {
|
|
87
|
+
|
|
88
|
+
pub fn apply_load(
|
|
89
|
+
&mut self,
|
|
90
|
+
health: &ShieldHealthSnap,
|
|
91
|
+
config: &ShieldConfigSnap,
|
|
92
|
+
metrics: &ShieldMetricsSnap,
|
|
93
|
+
warnings: Vec<String>,
|
|
94
|
+
recent: Vec<ShieldBlockRow>,
|
|
95
|
+
) {
|
|
96
|
+
self.loading = false;
|
|
97
|
+
self.error = None;
|
|
98
|
+
self.enabled = health.enabled;
|
|
99
|
+
self.mode = health.mode.clone();
|
|
100
|
+
self.health_status = health.status.clone();
|
|
101
|
+
self.pii_enabled = config.pii_enabled;
|
|
102
|
+
self.injection_enabled = config.injection_enabled;
|
|
103
|
+
self.injection_action = config.injection_action.clone();
|
|
104
|
+
self.hallucination_enabled = config.hallucination_enabled;
|
|
105
|
+
self.hallucination_action = config.hallucination_action.clone();
|
|
106
|
+
self.decisions_total = metrics.decisions;
|
|
107
|
+
self.blocks_total = metrics.blocks;
|
|
108
|
+
self.masks_total = metrics.masks;
|
|
109
|
+
self.rewrites_total = metrics.rewrites;
|
|
110
|
+
self.warnings = warnings;
|
|
111
|
+
self.recent_blocks = recent;
|
|
112
|
+
self.last_refresh = Some(std::time::Instant::now());
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
#[derive(Debug, Clone, Default)]
|
|
117
|
+
pub struct ShieldHealthSnap {
|
|
118
|
+
pub enabled: bool,
|
|
119
|
+
pub mode: String,
|
|
120
|
+
pub status: String,
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
#[derive(Debug, Clone, Default)]
|
|
124
|
+
pub struct ShieldConfigSnap {
|
|
125
|
+
pub pii_enabled: bool,
|
|
126
|
+
pub injection_enabled: bool,
|
|
127
|
+
pub injection_action: String,
|
|
128
|
+
pub hallucination_enabled: bool,
|
|
129
|
+
pub hallucination_action: String,
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
#[derive(Debug, Clone, Default)]
|
|
133
|
+
pub struct ShieldMetricsSnap {
|
|
134
|
+
pub decisions: u64,
|
|
135
|
+
pub blocks: u64,
|
|
136
|
+
pub masks: u64,
|
|
137
|
+
pub rewrites: u64,
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
pub fn parse_shield_load(
|
|
141
|
+
health_val: &Value,
|
|
142
|
+
config_val: &Value,
|
|
143
|
+
metrics_val: &Value,
|
|
144
|
+
gateway_health_val: &Value,
|
|
145
|
+
runs_val: &Value,
|
|
146
|
+
) -> (
|
|
147
|
+
ShieldHealthSnap,
|
|
148
|
+
ShieldConfigSnap,
|
|
149
|
+
ShieldMetricsSnap,
|
|
150
|
+
Vec<String>,
|
|
151
|
+
Vec<ShieldBlockRow>,
|
|
152
|
+
) {
|
|
153
|
+
let health_obj = health_val.as_object();
|
|
154
|
+
let health = ShieldHealthSnap {
|
|
155
|
+
enabled: health_obj
|
|
156
|
+
.and_then(|o| o.get("enabled"))
|
|
157
|
+
.and_then(|v| v.as_bool())
|
|
158
|
+
.unwrap_or(false),
|
|
159
|
+
mode: health_obj
|
|
160
|
+
.and_then(|o| o.get("mode"))
|
|
161
|
+
.and_then(|v| v.as_str())
|
|
162
|
+
.unwrap_or("off")
|
|
163
|
+
.to_string(),
|
|
164
|
+
status: if health_obj
|
|
165
|
+
.and_then(|o| o.get("enabled"))
|
|
166
|
+
.and_then(|v| v.as_bool())
|
|
167
|
+
.unwrap_or(false)
|
|
168
|
+
{
|
|
169
|
+
"active".to_string()
|
|
170
|
+
} else {
|
|
171
|
+
"disabled".to_string()
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
let cfg = config_val
|
|
176
|
+
.get("config")
|
|
177
|
+
.or(Some(config_val))
|
|
178
|
+
.and_then(|v| v.as_object());
|
|
179
|
+
let pii = cfg.and_then(|c| c.get("pii")).and_then(|v| v.as_object());
|
|
180
|
+
let injection = cfg.and_then(|c| c.get("injection")).and_then(|v| v.as_object());
|
|
181
|
+
let hallucination = cfg
|
|
182
|
+
.and_then(|c| c.get("hallucination"))
|
|
183
|
+
.and_then(|v| v.as_object());
|
|
184
|
+
|
|
185
|
+
let config = ShieldConfigSnap {
|
|
186
|
+
pii_enabled: pii
|
|
187
|
+
.and_then(|o| o.get("enabled"))
|
|
188
|
+
.and_then(|v| v.as_bool())
|
|
189
|
+
.unwrap_or(false),
|
|
190
|
+
injection_enabled: injection
|
|
191
|
+
.and_then(|o| o.get("enabled"))
|
|
192
|
+
.and_then(|v| v.as_bool())
|
|
193
|
+
.unwrap_or(false),
|
|
194
|
+
injection_action: injection
|
|
195
|
+
.and_then(|o| o.get("action"))
|
|
196
|
+
.and_then(|v| v.as_str())
|
|
197
|
+
.unwrap_or("—")
|
|
198
|
+
.to_string(),
|
|
199
|
+
hallucination_enabled: hallucination
|
|
200
|
+
.and_then(|o| o.get("enabled"))
|
|
201
|
+
.and_then(|v| v.as_bool())
|
|
202
|
+
.unwrap_or(false),
|
|
203
|
+
hallucination_action: hallucination
|
|
204
|
+
.and_then(|o| o.get("action"))
|
|
205
|
+
.and_then(|v| v.as_str())
|
|
206
|
+
.unwrap_or("—")
|
|
207
|
+
.to_string(),
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
let metrics = ShieldMetricsSnap {
|
|
211
|
+
decisions: metrics_val
|
|
212
|
+
.get("decisions")
|
|
213
|
+
.and_then(|v| v.as_u64())
|
|
214
|
+
.unwrap_or(0),
|
|
215
|
+
blocks: metrics_val
|
|
216
|
+
.get("blocks")
|
|
217
|
+
.and_then(|v| v.as_u64())
|
|
218
|
+
.unwrap_or(0),
|
|
219
|
+
masks: metrics_val
|
|
220
|
+
.get("masks")
|
|
221
|
+
.and_then(|v| v.as_u64())
|
|
222
|
+
.unwrap_or(0),
|
|
223
|
+
rewrites: metrics_val
|
|
224
|
+
.get("rewrites")
|
|
225
|
+
.and_then(|v| v.as_u64())
|
|
226
|
+
.unwrap_or(0),
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
let mut warnings: Vec<String> = Vec::new();
|
|
230
|
+
if let Some(sh) = gateway_health_val
|
|
231
|
+
.get("checks")
|
|
232
|
+
.and_then(|c| c.get("shield"))
|
|
233
|
+
.and_then(|v| v.as_object())
|
|
234
|
+
{
|
|
235
|
+
if let Some(st) = sh.get("status").and_then(|v| v.as_str()) {
|
|
236
|
+
if st != "ok" && st != "up" {
|
|
237
|
+
warnings.push(format!("Gateway health: {}", st));
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
if let Some(arr) = sh.get("warnings").and_then(|v| v.as_array()) {
|
|
241
|
+
for w in arr {
|
|
242
|
+
if let Some(s) = w.as_str() {
|
|
243
|
+
warnings.push(s.to_string());
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
let mut recent: Vec<ShieldBlockRow> = Vec::new();
|
|
250
|
+
let runs_arr = runs_val
|
|
251
|
+
.as_array()
|
|
252
|
+
.or_else(|| runs_val.get("runs").and_then(|v| v.as_array()));
|
|
253
|
+
if let Some(arr) = runs_arr {
|
|
254
|
+
for item in arr {
|
|
255
|
+
let o = match item.as_object() {
|
|
256
|
+
Some(o) => o,
|
|
257
|
+
None => continue,
|
|
258
|
+
};
|
|
259
|
+
let output = o.get("output").and_then(|v| v.as_object());
|
|
260
|
+
let err = output
|
|
261
|
+
.and_then(|oo| oo.get("error"))
|
|
262
|
+
.and_then(|v| v.as_str())
|
|
263
|
+
.unwrap_or("");
|
|
264
|
+
let logs_shield = o
|
|
265
|
+
.get("logs")
|
|
266
|
+
.and_then(|l| l.as_array())
|
|
267
|
+
.map(|arr| {
|
|
268
|
+
arr.iter().any(|e| {
|
|
269
|
+
e.get("message")
|
|
270
|
+
.and_then(|m| m.as_str())
|
|
271
|
+
.map(|s| s.to_lowercase().contains("shield"))
|
|
272
|
+
.unwrap_or(false)
|
|
273
|
+
})
|
|
274
|
+
})
|
|
275
|
+
.unwrap_or(false);
|
|
276
|
+
if !err.to_lowercase().contains("shield") && !logs_shield {
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
let id = o
|
|
280
|
+
.get("id")
|
|
281
|
+
.and_then(|v| v.as_str())
|
|
282
|
+
.unwrap_or("?")
|
|
283
|
+
.to_string();
|
|
284
|
+
let name = o
|
|
285
|
+
.get("name")
|
|
286
|
+
.and_then(|v| v.as_str())
|
|
287
|
+
.unwrap_or("Run")
|
|
288
|
+
.to_string();
|
|
289
|
+
let reason = output
|
|
290
|
+
.and_then(|oo| oo.get("reason"))
|
|
291
|
+
.and_then(|v| v.as_str())
|
|
292
|
+
.unwrap_or(err)
|
|
293
|
+
.to_string();
|
|
294
|
+
let created_at = o
|
|
295
|
+
.get("createdAt")
|
|
296
|
+
.and_then(|v| v.as_str())
|
|
297
|
+
.map(|s| s.chars().take(19).collect())
|
|
298
|
+
.unwrap_or_else(|| "—".to_string());
|
|
299
|
+
recent.push(ShieldBlockRow {
|
|
300
|
+
id,
|
|
301
|
+
name,
|
|
302
|
+
reason,
|
|
303
|
+
created_at,
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
recent.truncate(12);
|
|
308
|
+
|
|
309
|
+
(health, config, metrics, warnings, recent)
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
pub fn render(f: &mut Frame, state: &AppState) {
|
|
313
|
+
let area = f.size();
|
|
314
|
+
let sd = &state.shield_dashboard;
|
|
315
|
+
|
|
316
|
+
use ratatui::layout::{Constraint, Direction, Layout};
|
|
317
|
+
|
|
318
|
+
let live = if sd.auto_refresh_enabled { " · LIVE" } else { "" };
|
|
319
|
+
let header_title = format!(
|
|
320
|
+
" 🛡️ Shield — {} · {} block(s) · {} mask(s){} ",
|
|
321
|
+
sd.mode.to_uppercase(),
|
|
322
|
+
sd.blocks_total,
|
|
323
|
+
sd.masks_total,
|
|
324
|
+
live
|
|
325
|
+
);
|
|
326
|
+
|
|
327
|
+
let chunks = Layout::default()
|
|
328
|
+
.direction(Direction::Vertical)
|
|
329
|
+
.constraints([
|
|
330
|
+
Constraint::Length(3),
|
|
331
|
+
Constraint::Min(10),
|
|
332
|
+
Constraint::Length(8),
|
|
333
|
+
Constraint::Length(3),
|
|
334
|
+
])
|
|
335
|
+
.split(area);
|
|
336
|
+
|
|
337
|
+
let header = Block::default()
|
|
338
|
+
.title(header_title)
|
|
339
|
+
.borders(Borders::ALL)
|
|
340
|
+
.border_style(Style::default().fg(SHIELD_ORANGE))
|
|
341
|
+
.style(Style::default().bg(BG_PANEL));
|
|
342
|
+
f.render_widget(header, chunks[0]);
|
|
343
|
+
|
|
344
|
+
let mid = Layout::default()
|
|
345
|
+
.direction(Direction::Horizontal)
|
|
346
|
+
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
|
|
347
|
+
.split(chunks[1]);
|
|
348
|
+
|
|
349
|
+
render_status_panel(f, mid[0], sd);
|
|
350
|
+
render_metrics_panel(f, mid[1], sd);
|
|
351
|
+
render_enforcement_panel(f, chunks[2], sd);
|
|
352
|
+
|
|
353
|
+
let footer_msg = if sd.loading {
|
|
354
|
+
"Loading live Shield state from Gateway…".to_string()
|
|
355
|
+
} else if let Some(e) = &sd.error {
|
|
356
|
+
e.clone()
|
|
357
|
+
} else {
|
|
358
|
+
"R Refresh · A Live toggle · ESC Close".to_string()
|
|
359
|
+
};
|
|
360
|
+
let footer_color = if sd.error.is_some() {
|
|
361
|
+
Color::Rgb(255, 69, 69)
|
|
362
|
+
} else {
|
|
363
|
+
TEXT_DIM
|
|
364
|
+
};
|
|
365
|
+
let footer = Block::default()
|
|
366
|
+
.title(" ⌨️ Actions ")
|
|
367
|
+
.borders(Borders::ALL)
|
|
368
|
+
.border_style(Style::default().fg(TEXT_MUTED));
|
|
369
|
+
let inner = footer.inner(chunks[3]);
|
|
370
|
+
f.render_widget(footer, chunks[3]);
|
|
371
|
+
f.render_widget(
|
|
372
|
+
Paragraph::new(footer_msg)
|
|
373
|
+
.style(Style::default().fg(footer_color))
|
|
374
|
+
.alignment(Alignment::Center),
|
|
375
|
+
inner,
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
fn render_status_panel(f: &mut Frame, area: Rect, sd: &ShieldDashboardState) {
|
|
380
|
+
let block = Block::default()
|
|
381
|
+
.title(" Status & detectors ")
|
|
382
|
+
.borders(Borders::ALL)
|
|
383
|
+
.border_style(Style::default().fg(CYBER_CYAN))
|
|
384
|
+
.style(Style::default().bg(BG_PANEL));
|
|
385
|
+
let inner = block.inner(area);
|
|
386
|
+
f.render_widget(block, area);
|
|
387
|
+
|
|
388
|
+
let mode_color = match sd.mode.as_str() {
|
|
389
|
+
"enforce" => NEON_GREEN,
|
|
390
|
+
"monitor" => AMBER_WARN,
|
|
391
|
+
_ => TEXT_MUTED,
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
let mut lines = vec![
|
|
395
|
+
Line::from(vec![
|
|
396
|
+
Span::styled("Enabled: ", Style::default().fg(TEXT_DIM)),
|
|
397
|
+
Span::styled(
|
|
398
|
+
if sd.enabled { "YES" } else { "NO" },
|
|
399
|
+
Style::default()
|
|
400
|
+
.fg(if sd.enabled { NEON_GREEN } else { TEXT_MUTED })
|
|
401
|
+
.bold(),
|
|
402
|
+
),
|
|
403
|
+
]),
|
|
404
|
+
Line::from(vec![
|
|
405
|
+
Span::styled("Mode: ", Style::default().fg(TEXT_DIM)),
|
|
406
|
+
Span::styled(
|
|
407
|
+
sd.mode.to_uppercase(),
|
|
408
|
+
Style::default().fg(mode_color).bold(),
|
|
409
|
+
),
|
|
410
|
+
]),
|
|
411
|
+
Line::from(""),
|
|
412
|
+
Line::from("Detectors (production path):").style(Style::default().fg(TEXT_DIM)),
|
|
413
|
+
detector_line("PII", sd.pii_enabled, "mask on input/output".to_string()),
|
|
414
|
+
detector_line(
|
|
415
|
+
"Injection",
|
|
416
|
+
sd.injection_enabled,
|
|
417
|
+
format!("action: {}", sd.injection_action),
|
|
418
|
+
),
|
|
419
|
+
detector_line(
|
|
420
|
+
"Hallucination",
|
|
421
|
+
sd.hallucination_enabled,
|
|
422
|
+
format!("action: {}", sd.hallucination_action),
|
|
423
|
+
),
|
|
424
|
+
];
|
|
425
|
+
|
|
426
|
+
if !sd.warnings.is_empty() {
|
|
427
|
+
lines.push(Line::from(""));
|
|
428
|
+
lines.push(
|
|
429
|
+
Line::from("Warnings:")
|
|
430
|
+
.style(Style::default().fg(AMBER_WARN).bold()),
|
|
431
|
+
);
|
|
432
|
+
for w in &sd.warnings {
|
|
433
|
+
lines.push(Line::from(format!(" ⚠ {}", w)).style(Style::default().fg(AMBER_WARN)));
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
lines.push(Line::from(""));
|
|
438
|
+
for line in ABOUT_SHIELD.iter().take(4) {
|
|
439
|
+
lines.push(Line::from(*line).style(Style::default().fg(TEXT_MUTED)));
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
f.render_widget(Paragraph::new(lines).wrap(Wrap { trim: false }), inner);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
fn detector_line(name: &str, on: bool, detail: String) -> Line<'static> {
|
|
446
|
+
Line::from(vec![
|
|
447
|
+
Span::raw(" "),
|
|
448
|
+
Span::styled(
|
|
449
|
+
if on { "●" } else { "○" },
|
|
450
|
+
Style::default().fg(if on { NEON_GREEN } else { TEXT_MUTED }),
|
|
451
|
+
),
|
|
452
|
+
Span::styled(format!(" {:<14}", name), Style::default().fg(TEXT_PRIMARY)),
|
|
453
|
+
Span::styled(detail, Style::default().fg(TEXT_DIM)),
|
|
454
|
+
])
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
fn render_metrics_panel(f: &mut Frame, area: Rect, sd: &ShieldDashboardState) {
|
|
458
|
+
let block = Block::default()
|
|
459
|
+
.title(" Gateway metrics (cumulative) ")
|
|
460
|
+
.borders(Borders::ALL)
|
|
461
|
+
.border_style(Style::default().fg(BRAND_PURPLE))
|
|
462
|
+
.style(Style::default().bg(BG_PANEL));
|
|
463
|
+
let inner = block.inner(area);
|
|
464
|
+
f.render_widget(block, area);
|
|
465
|
+
|
|
466
|
+
let lines = vec![
|
|
467
|
+
Line::from(""),
|
|
468
|
+
metric_line("Decisions total", sd.decisions_total, TEXT_PRIMARY),
|
|
469
|
+
metric_line("Blocks total", sd.blocks_total, SHIELD_ORANGE),
|
|
470
|
+
metric_line("Masks total", sd.masks_total, CYBER_CYAN),
|
|
471
|
+
metric_line("Rewrites total", sd.rewrites_total, AMBER_WARN),
|
|
472
|
+
Line::from(""),
|
|
473
|
+
Line::from("Source: Gateway Prometheus /metrics").style(Style::default().fg(TEXT_MUTED)),
|
|
474
|
+
Line::from("Updates on refresh — counts all production runs.").style(Style::default().fg(TEXT_MUTED)),
|
|
475
|
+
Line::from(""),
|
|
476
|
+
Line::from(ABOUT_SHIELD[4]).style(Style::default().fg(TEXT_DIM)),
|
|
477
|
+
Line::from(ABOUT_SHIELD[5]).style(Style::default().fg(TEXT_DIM)),
|
|
478
|
+
Line::from(ABOUT_SHIELD[6]).style(Style::default().fg(TEXT_DIM)),
|
|
479
|
+
Line::from(ABOUT_SHIELD[7]).style(Style::default().fg(TEXT_DIM)),
|
|
480
|
+
];
|
|
481
|
+
|
|
482
|
+
f.render_widget(Paragraph::new(lines), inner);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
fn metric_line(label: &str, value: u64, color: Color) -> Line<'static> {
|
|
486
|
+
Line::from(vec![
|
|
487
|
+
Span::styled(format!(" {:<18}", label), Style::default().fg(TEXT_DIM)),
|
|
488
|
+
Span::styled(
|
|
489
|
+
value.to_string(),
|
|
490
|
+
Style::default().fg(color).bold(),
|
|
491
|
+
),
|
|
492
|
+
])
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
fn render_enforcement_panel(f: &mut Frame, area: Rect, sd: &ShieldDashboardState) {
|
|
496
|
+
let block = Block::default()
|
|
497
|
+
.title(format!(
|
|
498
|
+
" Recent enforcement ({} blocked run(s) in history) ",
|
|
499
|
+
sd.recent_blocks.len()
|
|
500
|
+
))
|
|
501
|
+
.borders(Borders::ALL)
|
|
502
|
+
.border_style(Style::default().fg(SHIELD_ORANGE))
|
|
503
|
+
.style(Style::default().bg(BG_PANEL));
|
|
504
|
+
let inner = block.inner(area);
|
|
505
|
+
f.render_widget(block, area);
|
|
506
|
+
|
|
507
|
+
if sd.recent_blocks.is_empty() {
|
|
508
|
+
let empty = vec![
|
|
509
|
+
Line::from(""),
|
|
510
|
+
Line::from("No blocked runs in Gateway history yet.").style(Style::default().fg(TEXT_DIM)),
|
|
511
|
+
Line::from("Shield is active on every run — blocks appear here when production").style(Style::default().fg(TEXT_MUTED)),
|
|
512
|
+
Line::from("traffic triggers injection/PII policy (failed status + SHIELD in Run Manager).").style(Style::default().fg(TEXT_MUTED)),
|
|
513
|
+
];
|
|
514
|
+
f.render_widget(Paragraph::new(empty), inner);
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
let items: Vec<ListItem> = sd
|
|
519
|
+
.recent_blocks
|
|
520
|
+
.iter()
|
|
521
|
+
.map(|r| {
|
|
522
|
+
let short_id = if r.id.len() > 8 {
|
|
523
|
+
&r.id[r.id.len() - 8..]
|
|
524
|
+
} else {
|
|
525
|
+
&r.id
|
|
526
|
+
};
|
|
527
|
+
ListItem::new(Line::from(vec![
|
|
528
|
+
Span::styled(
|
|
529
|
+
format!("{} ", r.created_at),
|
|
530
|
+
Style::default().fg(TEXT_MUTED),
|
|
531
|
+
),
|
|
532
|
+
Span::styled(
|
|
533
|
+
format!("[{}] ", short_id),
|
|
534
|
+
Style::default().fg(CYBER_CYAN),
|
|
535
|
+
),
|
|
536
|
+
Span::styled(
|
|
537
|
+
format!("{} — ", r.name),
|
|
538
|
+
Style::default().fg(TEXT_PRIMARY),
|
|
539
|
+
),
|
|
540
|
+
Span::styled(
|
|
541
|
+
r.reason.chars().take(48).collect::<String>(),
|
|
542
|
+
Style::default().fg(SHIELD_ORANGE),
|
|
543
|
+
),
|
|
544
|
+
]))
|
|
545
|
+
})
|
|
546
|
+
.collect();
|
|
547
|
+
|
|
548
|
+
f.render_widget(List::new(items), inner);
|
|
549
|
+
}
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "4runr-os",
|
|
3
|
-
"version": "2.10.
|
|
3
|
+
"version": "2.10.75",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "4Runr AI Agent OS - Secure terminal interface for AI agents. v2.10.
|
|
5
|
+
"description": "4Runr AI Agent OS - Secure terminal interface for AI agents. v2.10.75: Dedicated Shield dashboard (shield command) — live metrics, detectors, enforcement history.",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"bin": {
|
|
8
8
|
"4runr": "dist/index.js",
|