4runr-os 2.10.73 → 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/apps/gateway/package-lock.json +103 -88
- package/dist/gateway-observability.d.ts +4 -0
- package/dist/gateway-observability.d.ts.map +1 -1
- package/dist/gateway-observability.js +14 -0
- package/dist/gateway-observability.js.map +1 -1
- package/dist/tui-handlers.js +187 -12
- 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 +103 -0
- package/mk3-tui/src/main.rs +196 -0
- package/mk3-tui/src/screens/mod.rs +3 -1
- package/mk3-tui/src/ui/layout.rs +12 -0
- package/mk3-tui/src/ui/mod.rs +1 -0
- package/mk3-tui/src/ui/run_manager.rs +51 -6
- package/mk3-tui/src/ui/shield_dashboard.rs +549 -0
- package/package.json +2 -2
- package/scripts/os-tools-smoke.cjs +549 -460
package/mk3-tui/src/app.rs
CHANGED
|
@@ -5,6 +5,7 @@ use crate::storage::Cache;
|
|
|
5
5
|
use crate::ui::agent_builder::AgentBuilderState;
|
|
6
6
|
use crate::ui::run_manager::RunManagerState;
|
|
7
7
|
use crate::ui::sentinel_config::SentinelConfigState;
|
|
8
|
+
use crate::ui::shield_dashboard::ShieldDashboardState;
|
|
8
9
|
use crate::ui::settings::SettingsState;
|
|
9
10
|
use crate::websocket::WebSocketClient;
|
|
10
11
|
use crossterm::event::{KeyCode, KeyEvent, KeyEventKind};
|
|
@@ -386,6 +387,8 @@ pub struct AppState {
|
|
|
386
387
|
pub posture_status: String,
|
|
387
388
|
pub shield_mode: String, // "off", "monitor", "enforce"
|
|
388
389
|
pub shield_detectors: Vec<String>, // ["pii", "injection", "hallucination"]
|
|
390
|
+
pub shield_blocks_total: u64,
|
|
391
|
+
pub shield_masks_total: u64,
|
|
389
392
|
pub sentinel_state: String, // "idle", "watching", "triggered"
|
|
390
393
|
pub sentinel_active_runs: usize,
|
|
391
394
|
|
|
@@ -429,6 +432,7 @@ pub struct AppState {
|
|
|
429
432
|
pub agent_builder: AgentBuilderState,
|
|
430
433
|
pub run_manager: RunManagerState,
|
|
431
434
|
pub sentinel_config: SentinelConfigState,
|
|
435
|
+
pub shield_dashboard: ShieldDashboardState,
|
|
432
436
|
pub settings: SettingsState,
|
|
433
437
|
pub agent_list: AgentListState,
|
|
434
438
|
pub connection_portal: ConnectionPortalState,
|
|
@@ -461,6 +465,7 @@ pub struct AppState {
|
|
|
461
465
|
|
|
462
466
|
pub pending_sentinel_load_id: Option<String>,
|
|
463
467
|
pub pending_sentinel_apply_id: Option<String>,
|
|
468
|
+
pub pending_shield_load_id: Option<String>,
|
|
464
469
|
|
|
465
470
|
// Deletion confirmation (Step 5.5)
|
|
466
471
|
pub agent_delete_requested: bool,
|
|
@@ -489,6 +494,8 @@ impl Default for AppState {
|
|
|
489
494
|
posture_status: "Demo Mode".to_string(),
|
|
490
495
|
shield_mode: "enforce".to_string(),
|
|
491
496
|
shield_detectors: vec!["pii".into(), "injection".into(), "hallucination".into()],
|
|
497
|
+
shield_blocks_total: 0,
|
|
498
|
+
shield_masks_total: 0,
|
|
492
499
|
sentinel_state: "idle".to_string(),
|
|
493
500
|
sentinel_active_runs: 0,
|
|
494
501
|
total_runs: 0,
|
|
@@ -526,6 +533,7 @@ impl Default for AppState {
|
|
|
526
533
|
agent_builder: AgentBuilderState::default(),
|
|
527
534
|
run_manager: RunManagerState::default(),
|
|
528
535
|
sentinel_config: SentinelConfigState::default(),
|
|
536
|
+
shield_dashboard: ShieldDashboardState::default(),
|
|
529
537
|
settings: SettingsState::default(),
|
|
530
538
|
agent_list: AgentListState::default(),
|
|
531
539
|
connection_portal: ConnectionPortalState::default(),
|
|
@@ -550,6 +558,7 @@ impl Default for AppState {
|
|
|
550
558
|
pending_run_quick_id: None,
|
|
551
559
|
pending_sentinel_load_id: None,
|
|
552
560
|
pending_sentinel_apply_id: None,
|
|
561
|
+
pending_shield_load_id: None,
|
|
553
562
|
agent_delete_requested: false,
|
|
554
563
|
operation_mode: OperationMode::Local,
|
|
555
564
|
cache: Cache::new().ok(),
|
|
@@ -828,6 +837,40 @@ impl App {
|
|
|
828
837
|
}
|
|
829
838
|
}
|
|
830
839
|
|
|
840
|
+
pub fn open_shield_dashboard(&mut self, ws: Option<&WebSocketClient>) {
|
|
841
|
+
self.state.pending_shield_load_id = None;
|
|
842
|
+
self.push_overlay(Screen::ShieldDashboard);
|
|
843
|
+
self.state
|
|
844
|
+
.logs
|
|
845
|
+
.push_back("[NAV] Opening Shield (production safety layer)...".into());
|
|
846
|
+
if self.state.operation_mode == OperationMode::Connected {
|
|
847
|
+
if let Some(ws) = ws {
|
|
848
|
+
self.begin_shield_load_request(ws);
|
|
849
|
+
}
|
|
850
|
+
} else {
|
|
851
|
+
self.state.shield_dashboard.error = Some(
|
|
852
|
+
"Connect to Gateway first (connect portal).".to_string(),
|
|
853
|
+
);
|
|
854
|
+
}
|
|
855
|
+
self.request_immediate_render("open_shield_dashboard");
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
pub fn begin_shield_load_request(&mut self, ws: &WebSocketClient) {
|
|
859
|
+
if self.state.operation_mode != OperationMode::Connected {
|
|
860
|
+
return;
|
|
861
|
+
}
|
|
862
|
+
if self.state.pending_shield_load_id.is_some() {
|
|
863
|
+
return;
|
|
864
|
+
}
|
|
865
|
+
self.state.shield_dashboard.loading = true;
|
|
866
|
+
self.state.shield_dashboard.error = None;
|
|
867
|
+
if let Ok(id) = ws.send_command("shield.load", None) {
|
|
868
|
+
self.state.pending_shield_load_id = Some(id);
|
|
869
|
+
} else {
|
|
870
|
+
self.state.shield_dashboard.loading = false;
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
|
|
831
874
|
pub fn open_sentinel_config(&mut self, ws: Option<&WebSocketClient>) {
|
|
832
875
|
self.state.pending_sentinel_load_id = None;
|
|
833
876
|
self.state.pending_sentinel_apply_id = None;
|
|
@@ -1316,6 +1359,11 @@ impl App {
|
|
|
1316
1359
|
return self.handle_sentinel_config_input(key, ws_client);
|
|
1317
1360
|
}
|
|
1318
1361
|
|
|
1362
|
+
// === SHIELD DASHBOARD INPUT HANDLING ===
|
|
1363
|
+
if self.state.navigation.current_screen() == &Screen::ShieldDashboard {
|
|
1364
|
+
return self.handle_shield_dashboard_input(key, ws_client);
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1319
1367
|
// === SETTINGS INPUT HANDLING ===
|
|
1320
1368
|
if self.state.navigation.current_screen() == &Screen::Settings {
|
|
1321
1369
|
return self.handle_settings_input(key);
|
|
@@ -1489,6 +1537,10 @@ impl App {
|
|
|
1489
1537
|
" sentinel - Configure Sentinel policies (templates)"
|
|
1490
1538
|
.to_string(),
|
|
1491
1539
|
);
|
|
1540
|
+
self.state.logs.push_back(
|
|
1541
|
+
" shield - Shield safety dashboard (live metrics + enforcement)"
|
|
1542
|
+
.to_string(),
|
|
1543
|
+
);
|
|
1492
1544
|
self.state.logs.push_back(
|
|
1493
1545
|
" build - Open Agent Builder (6-step wizard)"
|
|
1494
1546
|
.to_string(),
|
|
@@ -1570,6 +1622,9 @@ impl App {
|
|
|
1570
1622
|
"sentinel" | "sentinel config" | "sentinel policies" => {
|
|
1571
1623
|
self.open_sentinel_config(ws_client);
|
|
1572
1624
|
}
|
|
1625
|
+
"shield" | "shield status" | "shield dashboard" => {
|
|
1626
|
+
self.open_shield_dashboard(ws_client);
|
|
1627
|
+
}
|
|
1573
1628
|
"config" | "settings" => {
|
|
1574
1629
|
self.push_overlay(Screen::Settings);
|
|
1575
1630
|
self.state
|
|
@@ -1854,6 +1909,10 @@ impl App {
|
|
|
1854
1909
|
use crate::ui::sentinel_config;
|
|
1855
1910
|
sentinel_config::render(f, &self.state);
|
|
1856
1911
|
}
|
|
1912
|
+
Screen::ShieldDashboard => {
|
|
1913
|
+
use crate::ui::shield_dashboard;
|
|
1914
|
+
shield_dashboard::render(f, &self.state);
|
|
1915
|
+
}
|
|
1857
1916
|
Screen::Settings => {
|
|
1858
1917
|
use crate::ui::settings;
|
|
1859
1918
|
settings::render(f, &self.state);
|
|
@@ -2588,6 +2647,50 @@ impl App {
|
|
|
2588
2647
|
Ok(false)
|
|
2589
2648
|
}
|
|
2590
2649
|
|
|
2650
|
+
fn handle_shield_dashboard_input(
|
|
2651
|
+
&mut self,
|
|
2652
|
+
key: KeyEvent,
|
|
2653
|
+
ws_client: Option<&WebSocketClient>,
|
|
2654
|
+
) -> anyhow::Result<bool> {
|
|
2655
|
+
use crossterm::event::KeyModifiers;
|
|
2656
|
+
|
|
2657
|
+
if key.modifiers.contains(KeyModifiers::CONTROL) {
|
|
2658
|
+
match key.code {
|
|
2659
|
+
KeyCode::Char('c') | KeyCode::Char('q') => return Ok(true),
|
|
2660
|
+
_ => return Ok(false),
|
|
2661
|
+
}
|
|
2662
|
+
}
|
|
2663
|
+
|
|
2664
|
+
match key.code {
|
|
2665
|
+
KeyCode::Char('r') | KeyCode::Char('R') => {
|
|
2666
|
+
if self.state.operation_mode == OperationMode::Connected {
|
|
2667
|
+
if let Some(ws) = ws_client {
|
|
2668
|
+
self.begin_shield_load_request(ws);
|
|
2669
|
+
}
|
|
2670
|
+
}
|
|
2671
|
+
self.request_render("shield_refresh");
|
|
2672
|
+
}
|
|
2673
|
+
KeyCode::Char('a') | KeyCode::Char('A') => {
|
|
2674
|
+
self.state.shield_dashboard.auto_refresh_enabled =
|
|
2675
|
+
!self.state.shield_dashboard.auto_refresh_enabled;
|
|
2676
|
+
let status = if self.state.shield_dashboard.auto_refresh_enabled {
|
|
2677
|
+
"ON"
|
|
2678
|
+
} else {
|
|
2679
|
+
"OFF"
|
|
2680
|
+
};
|
|
2681
|
+
self.add_log(format!("[SHIELD] Live refresh {}", status));
|
|
2682
|
+
self.request_render("shield_live_toggle");
|
|
2683
|
+
}
|
|
2684
|
+
KeyCode::Esc => {
|
|
2685
|
+
self.pop_overlay();
|
|
2686
|
+
self.request_render("shield_close");
|
|
2687
|
+
}
|
|
2688
|
+
_ => {}
|
|
2689
|
+
}
|
|
2690
|
+
|
|
2691
|
+
Ok(false)
|
|
2692
|
+
}
|
|
2693
|
+
|
|
2591
2694
|
// ============================================================
|
|
2592
2695
|
// SETTINGS INPUT HANDLING (Step 4.8)
|
|
2593
2696
|
// ============================================================
|
package/mk3-tui/src/main.rs
CHANGED
|
@@ -189,6 +189,27 @@ fn main() -> Result<()> {
|
|
|
189
189
|
}
|
|
190
190
|
}
|
|
191
191
|
|
|
192
|
+
// Shield dashboard: live poll shield.load every ~5s when enabled
|
|
193
|
+
if matches!(current_screen, Screen::ShieldDashboard)
|
|
194
|
+
&& app.state.operation_mode == OperationMode::Connected
|
|
195
|
+
{
|
|
196
|
+
let shield_poll = {
|
|
197
|
+
let sd = &app.state.shield_dashboard;
|
|
198
|
+
if !sd.auto_refresh_enabled || sd.loading {
|
|
199
|
+
false
|
|
200
|
+
} else {
|
|
201
|
+
sd.last_refresh
|
|
202
|
+
.map(|lr| lr.elapsed() >= sd.auto_refresh_interval)
|
|
203
|
+
.unwrap_or(true)
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
if shield_poll {
|
|
207
|
+
if let Some(ws) = &ws_client {
|
|
208
|
+
app.begin_shield_load_request(ws);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
192
213
|
// Run Manager: live poll run.list every ~3s when enabled (no manual R)
|
|
193
214
|
if matches!(current_screen, Screen::RunManager)
|
|
194
215
|
&& app.state.operation_mode == OperationMode::Connected
|
|
@@ -279,6 +300,58 @@ fn main() -> Result<()> {
|
|
|
279
300
|
app.state.posture_status = posture_str.to_string();
|
|
280
301
|
}
|
|
281
302
|
}
|
|
303
|
+
if let Some(sh) =
|
|
304
|
+
obj.get("shield").and_then(|v| v.as_object())
|
|
305
|
+
{
|
|
306
|
+
if let Some(m) =
|
|
307
|
+
sh.get("mode").and_then(|v| v.as_str())
|
|
308
|
+
{
|
|
309
|
+
app.state.shield_mode = m.to_string();
|
|
310
|
+
}
|
|
311
|
+
if let Some(dets) =
|
|
312
|
+
sh.get("detectors").and_then(|v| v.as_array())
|
|
313
|
+
{
|
|
314
|
+
app.state.shield_detectors = dets
|
|
315
|
+
.iter()
|
|
316
|
+
.filter_map(|v| {
|
|
317
|
+
v.as_str().map(|s| s.to_string())
|
|
318
|
+
})
|
|
319
|
+
.collect();
|
|
320
|
+
}
|
|
321
|
+
app.state.shield_blocks_total = sh
|
|
322
|
+
.get("blocksTotal")
|
|
323
|
+
.and_then(|v| v.as_u64())
|
|
324
|
+
.unwrap_or(0);
|
|
325
|
+
app.state.shield_masks_total = sh
|
|
326
|
+
.get("masksTotal")
|
|
327
|
+
.and_then(|v| v.as_u64())
|
|
328
|
+
.unwrap_or(0);
|
|
329
|
+
}
|
|
330
|
+
if let Some(sent) =
|
|
331
|
+
obj.get("sentinel").and_then(|v| v.as_object())
|
|
332
|
+
{
|
|
333
|
+
app.state.sentinel_active_runs = sent
|
|
334
|
+
.get("watchedRuns")
|
|
335
|
+
.and_then(|v| v.as_u64())
|
|
336
|
+
.unwrap_or(0) as usize;
|
|
337
|
+
let enabled = sent
|
|
338
|
+
.get("enabled")
|
|
339
|
+
.and_then(|v| v.as_bool())
|
|
340
|
+
.unwrap_or(false);
|
|
341
|
+
let healthy = sent
|
|
342
|
+
.get("healthy")
|
|
343
|
+
.and_then(|v| v.as_bool())
|
|
344
|
+
.unwrap_or(false);
|
|
345
|
+
app.state.sentinel_state = if !enabled {
|
|
346
|
+
"off".to_string()
|
|
347
|
+
} else if app.state.sentinel_active_runs > 0 {
|
|
348
|
+
"watching".to_string()
|
|
349
|
+
} else if healthy {
|
|
350
|
+
"idle".to_string()
|
|
351
|
+
} else {
|
|
352
|
+
"degraded".to_string()
|
|
353
|
+
};
|
|
354
|
+
}
|
|
282
355
|
app.add_log(format!(
|
|
283
356
|
"✓ [{}] Mode: {}, Posture: {}",
|
|
284
357
|
short_id,
|
|
@@ -288,6 +361,61 @@ fn main() -> Result<()> {
|
|
|
288
361
|
.unwrap_or("?")
|
|
289
362
|
));
|
|
290
363
|
}
|
|
364
|
+
else if obj.get("shieldDemo").and_then(|v| v.as_bool())
|
|
365
|
+
== Some(true)
|
|
366
|
+
{
|
|
367
|
+
let msg = obj
|
|
368
|
+
.get("message")
|
|
369
|
+
.and_then(|v| v.as_str())
|
|
370
|
+
.unwrap_or("Shield demo run started");
|
|
371
|
+
app.add_log(format!("✓ [{}] {}", short_id, msg));
|
|
372
|
+
if let Some(rid) =
|
|
373
|
+
obj.get("runId").and_then(|v| v.as_str())
|
|
374
|
+
{
|
|
375
|
+
app.add_log(format!(
|
|
376
|
+
"[SHIELD] Demo run id: {} — open Run Manager",
|
|
377
|
+
rid
|
|
378
|
+
));
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
else if obj.get("shieldProbe").and_then(|v| v.as_bool())
|
|
382
|
+
== Some(true)
|
|
383
|
+
{
|
|
384
|
+
let result = obj.get("result");
|
|
385
|
+
let action = result
|
|
386
|
+
.and_then(|r| r.get("decision"))
|
|
387
|
+
.and_then(|d| d.get("action"))
|
|
388
|
+
.and_then(|a| a.as_str())
|
|
389
|
+
.unwrap_or("?");
|
|
390
|
+
app.add_log(format!(
|
|
391
|
+
"✓ [{}] Shield probe: action={}",
|
|
392
|
+
short_id, action
|
|
393
|
+
));
|
|
394
|
+
}
|
|
395
|
+
else if obj.get("shieldStatus").and_then(|v| v.as_bool())
|
|
396
|
+
== Some(true)
|
|
397
|
+
{
|
|
398
|
+
let metrics = obj.get("metrics").and_then(|v| v.as_object());
|
|
399
|
+
let blocks = metrics
|
|
400
|
+
.and_then(|m| m.get("blocks"))
|
|
401
|
+
.and_then(|v| v.as_u64())
|
|
402
|
+
.unwrap_or(0);
|
|
403
|
+
let masks = metrics
|
|
404
|
+
.and_then(|m| m.get("masks"))
|
|
405
|
+
.and_then(|v| v.as_u64())
|
|
406
|
+
.unwrap_or(0);
|
|
407
|
+
app.state.shield_blocks_total = blocks;
|
|
408
|
+
app.state.shield_masks_total = masks;
|
|
409
|
+
let mode = obj
|
|
410
|
+
.get("health")
|
|
411
|
+
.and_then(|h| h.get("mode"))
|
|
412
|
+
.and_then(|v| v.as_str())
|
|
413
|
+
.unwrap_or("?");
|
|
414
|
+
app.add_log(format!(
|
|
415
|
+
"✓ [{}] Shield {} — {} block(s), {} mask(s)",
|
|
416
|
+
short_id, mode, blocks, masks
|
|
417
|
+
));
|
|
418
|
+
}
|
|
291
419
|
// Handle agent.list response
|
|
292
420
|
else if let Some(agents_data) = obj.get("agents") {
|
|
293
421
|
if let Some(agents_array) = agents_data.as_array() {
|
|
@@ -562,6 +690,66 @@ fn main() -> Result<()> {
|
|
|
562
690
|
}
|
|
563
691
|
app.request_render("run_quick_ok");
|
|
564
692
|
}
|
|
693
|
+
// Shield dashboard: load health + config + metrics + history
|
|
694
|
+
else if Some(&resp.id)
|
|
695
|
+
== app.state.pending_shield_load_id.as_ref()
|
|
696
|
+
{
|
|
697
|
+
app.state.pending_shield_load_id = None;
|
|
698
|
+
if obj.get("shieldLoad").and_then(|v| v.as_bool()) == Some(true)
|
|
699
|
+
{
|
|
700
|
+
use crate::ui::shield_dashboard::parse_shield_load;
|
|
701
|
+
let health_val = obj
|
|
702
|
+
.get("health")
|
|
703
|
+
.cloned()
|
|
704
|
+
.unwrap_or(serde_json::Value::Null);
|
|
705
|
+
let config_val = obj
|
|
706
|
+
.get("config")
|
|
707
|
+
.cloned()
|
|
708
|
+
.unwrap_or(serde_json::Value::Null);
|
|
709
|
+
let metrics_val = obj
|
|
710
|
+
.get("metrics")
|
|
711
|
+
.cloned()
|
|
712
|
+
.unwrap_or(serde_json::Value::Null);
|
|
713
|
+
let gateway_health_val = obj
|
|
714
|
+
.get("gatewayHealth")
|
|
715
|
+
.cloned()
|
|
716
|
+
.unwrap_or(serde_json::Value::Null);
|
|
717
|
+
let runs_val = obj
|
|
718
|
+
.get("recentRuns")
|
|
719
|
+
.cloned()
|
|
720
|
+
.unwrap_or(serde_json::Value::Null);
|
|
721
|
+
let warnings_vec: Vec<String> = obj
|
|
722
|
+
.get("warnings")
|
|
723
|
+
.and_then(|v| v.as_array())
|
|
724
|
+
.map(|a| {
|
|
725
|
+
a.iter()
|
|
726
|
+
.filter_map(|w| {
|
|
727
|
+
w.as_str().map(|s| s.to_string())
|
|
728
|
+
})
|
|
729
|
+
.collect()
|
|
730
|
+
})
|
|
731
|
+
.unwrap_or_default();
|
|
732
|
+
let (h, c, m, mut w, recent) = parse_shield_load(
|
|
733
|
+
&health_val,
|
|
734
|
+
&config_val,
|
|
735
|
+
&metrics_val,
|
|
736
|
+
&gateway_health_val,
|
|
737
|
+
&runs_val,
|
|
738
|
+
);
|
|
739
|
+
w.extend(warnings_vec);
|
|
740
|
+
app.state.shield_dashboard.apply_load(
|
|
741
|
+
&h, &c, &m, w, recent,
|
|
742
|
+
);
|
|
743
|
+
app.state.shield_mode = h.mode.clone();
|
|
744
|
+
app.state.shield_blocks_total = m.blocks;
|
|
745
|
+
app.state.shield_masks_total = m.masks;
|
|
746
|
+
app.add_log(format!(
|
|
747
|
+
"✓ [{}] Shield loaded — {} block(s), {} mask(s)",
|
|
748
|
+
short_id, m.blocks, m.masks
|
|
749
|
+
));
|
|
750
|
+
}
|
|
751
|
+
app.request_render("shield_loaded");
|
|
752
|
+
}
|
|
565
753
|
// Sentinel config: load templates + current + health
|
|
566
754
|
else if Some(&resp.id)
|
|
567
755
|
== app.state.pending_sentinel_load_id.as_ref()
|
|
@@ -1435,6 +1623,14 @@ fn main() -> Result<()> {
|
|
|
1435
1623
|
"✗ [{}] run.quick failed: {}",
|
|
1436
1624
|
short_id, error_msg
|
|
1437
1625
|
));
|
|
1626
|
+
} else if Some(&resp.id) == app.state.pending_shield_load_id.as_ref() {
|
|
1627
|
+
app.state.pending_shield_load_id = None;
|
|
1628
|
+
app.state.shield_dashboard.loading = false;
|
|
1629
|
+
app.state.shield_dashboard.error = Some(error_msg.clone());
|
|
1630
|
+
app.add_log(format!(
|
|
1631
|
+
"✗ [{}] shield.load failed: {}",
|
|
1632
|
+
short_id, error_msg
|
|
1633
|
+
));
|
|
1438
1634
|
} else if Some(&resp.id) == app.state.pending_sentinel_load_id.as_ref()
|
|
1439
1635
|
{
|
|
1440
1636
|
app.state.pending_sentinel_load_id = None;
|
|
@@ -18,6 +18,7 @@ pub enum Screen {
|
|
|
18
18
|
AgentBuilder,
|
|
19
19
|
RunManager,
|
|
20
20
|
SentinelConfig,
|
|
21
|
+
ShieldDashboard,
|
|
21
22
|
Settings,
|
|
22
23
|
AgentList,
|
|
23
24
|
ConnectionPortal,
|
|
@@ -40,7 +41,7 @@ impl Screen {
|
|
|
40
41
|
pub fn is_overlay(&self) -> bool {
|
|
41
42
|
matches!(
|
|
42
43
|
self,
|
|
43
|
-
Screen::AgentBuilder | Screen::RunManager | Screen::SentinelConfig | Screen::Settings | Screen::AgentList
|
|
44
|
+
Screen::AgentBuilder | Screen::RunManager | Screen::SentinelConfig | Screen::ShieldDashboard | Screen::Settings | Screen::AgentList
|
|
44
45
|
)
|
|
45
46
|
}
|
|
46
47
|
|
|
@@ -70,6 +71,7 @@ impl Screen {
|
|
|
70
71
|
Screen::AgentBuilder => "Agent Builder",
|
|
71
72
|
Screen::RunManager => "Run Manager",
|
|
72
73
|
Screen::SentinelConfig => "Sentinel Config",
|
|
74
|
+
Screen::ShieldDashboard => "Shield",
|
|
73
75
|
Screen::Settings => "Settings",
|
|
74
76
|
Screen::AgentList => "Agent List",
|
|
75
77
|
Screen::ConnectionPortal => "Connection Portal",
|
package/mk3-tui/src/ui/layout.rs
CHANGED
|
@@ -394,6 +394,18 @@ fn render_left_column(f: &mut Frame, area: Rect, state: &AppState) {
|
|
|
394
394
|
Style::default().fg(TEXT_DIM),
|
|
395
395
|
),
|
|
396
396
|
]));
|
|
397
|
+
if state.shield_blocks_total > 0 || state.shield_masks_total > 0 {
|
|
398
|
+
lines.push(Line::from(vec![
|
|
399
|
+
Span::raw(" "),
|
|
400
|
+
Span::styled(
|
|
401
|
+
format!(
|
|
402
|
+
"↳ {} block(s) · {} mask(s) (Gateway metrics)",
|
|
403
|
+
state.shield_blocks_total, state.shield_masks_total
|
|
404
|
+
),
|
|
405
|
+
Style::default().fg(TEXT_DIM),
|
|
406
|
+
),
|
|
407
|
+
]));
|
|
408
|
+
}
|
|
397
409
|
|
|
398
410
|
// Sentinel status
|
|
399
411
|
let sentinel_color = match state.sentinel_state.as_str() {
|
package/mk3-tui/src/ui/mod.rs
CHANGED
|
@@ -67,6 +67,9 @@ pub struct RunInfo {
|
|
|
67
67
|
pub total_cost: Option<f64>,
|
|
68
68
|
pub model: Option<String>,
|
|
69
69
|
pub error_message: Option<String>,
|
|
70
|
+
/// True when Gateway output indicates Shield blocked or masked on the run path.
|
|
71
|
+
pub shield_blocked: bool,
|
|
72
|
+
pub safety_reason: Option<String>,
|
|
70
73
|
pub logs: Vec<String>,
|
|
71
74
|
}
|
|
72
75
|
|
|
@@ -213,12 +216,20 @@ pub fn run_info_from_gateway_value(v: &Value) -> Option<RunInfo> {
|
|
|
213
216
|
.collect()
|
|
214
217
|
})
|
|
215
218
|
.unwrap_or_default();
|
|
216
|
-
let
|
|
217
|
-
|
|
218
|
-
.and_then(|out| out.as_object())
|
|
219
|
+
let output_obj = o.get("output").and_then(|out| out.as_object());
|
|
220
|
+
let error_message = output_obj
|
|
219
221
|
.and_then(|oo| oo.get("error"))
|
|
220
222
|
.and_then(|e| e.as_str())
|
|
221
223
|
.map(|s| s.to_string());
|
|
224
|
+
let safety_reason = output_obj
|
|
225
|
+
.and_then(|oo| oo.get("reason"))
|
|
226
|
+
.and_then(|r| r.as_str())
|
|
227
|
+
.map(|s| s.to_string());
|
|
228
|
+
let shield_blocked = error_message
|
|
229
|
+
.as_deref()
|
|
230
|
+
.map(|e| e.to_lowercase().contains("shield"))
|
|
231
|
+
.unwrap_or(false)
|
|
232
|
+
|| logs.iter().any(|l| l.to_lowercase().contains("shield blocked"));
|
|
222
233
|
let model = o
|
|
223
234
|
.get("input")
|
|
224
235
|
.and_then(|i| i.as_object())
|
|
@@ -248,6 +259,8 @@ pub fn run_info_from_gateway_value(v: &Value) -> Option<RunInfo> {
|
|
|
248
259
|
total_cost: None,
|
|
249
260
|
model,
|
|
250
261
|
error_message,
|
|
262
|
+
shield_blocked,
|
|
263
|
+
safety_reason,
|
|
251
264
|
logs,
|
|
252
265
|
})
|
|
253
266
|
}
|
|
@@ -542,7 +555,7 @@ fn render_run_list(f: &mut Frame, area: Rect, state: &RunManagerState) {
|
|
|
542
555
|
let duration_str = run.duration.as_deref().unwrap_or("--");
|
|
543
556
|
|
|
544
557
|
// Build line with colored spans
|
|
545
|
-
let
|
|
558
|
+
let mut spans = vec![
|
|
546
559
|
Span::styled(
|
|
547
560
|
if is_selected { "▶ " } else { " " },
|
|
548
561
|
Style::default().fg(BRAND_PURPLE).bold(),
|
|
@@ -551,6 +564,16 @@ fn render_run_list(f: &mut Frame, area: Rect, state: &RunManagerState) {
|
|
|
551
564
|
format!("{:<10}", run.status.as_str()),
|
|
552
565
|
Style::default().fg(run.status.color()).bold(),
|
|
553
566
|
),
|
|
567
|
+
];
|
|
568
|
+
if run.shield_blocked {
|
|
569
|
+
spans.push(Span::styled(
|
|
570
|
+
" SHIELD ",
|
|
571
|
+
Style::default()
|
|
572
|
+
.fg(Color::Rgb(255, 140, 0))
|
|
573
|
+
.add_modifier(Modifier::BOLD),
|
|
574
|
+
));
|
|
575
|
+
}
|
|
576
|
+
spans.extend([
|
|
554
577
|
Span::styled(" │ ", Style::default().fg(TEXT_DIM)),
|
|
555
578
|
Span::styled(
|
|
556
579
|
format!("{:<20}", run.name),
|
|
@@ -583,6 +606,8 @@ fn render_run_list(f: &mut Frame, area: Rect, state: &RunManagerState) {
|
|
|
583
606
|
),
|
|
584
607
|
]);
|
|
585
608
|
|
|
609
|
+
let line = Line::from(spans);
|
|
610
|
+
|
|
586
611
|
ListItem::new(line)
|
|
587
612
|
})
|
|
588
613
|
.collect();
|
|
@@ -764,8 +789,28 @@ fn render_detail_view(f: &mut Frame, area: Rect, state: &RunManagerState) {
|
|
|
764
789
|
]));
|
|
765
790
|
}
|
|
766
791
|
|
|
767
|
-
|
|
768
|
-
|
|
792
|
+
if run.shield_blocked {
|
|
793
|
+
info_lines.push(Line::from(""));
|
|
794
|
+
info_lines.push(
|
|
795
|
+
Line::from("Safety (Shield):").style(Style::default().fg(Color::Rgb(255, 140, 0)).bold()),
|
|
796
|
+
);
|
|
797
|
+
info_lines.push(Line::from(vec![
|
|
798
|
+
Span::styled(" Action: ", Style::default().fg(TEXT_DIM)),
|
|
799
|
+
Span::styled("BLOCKED", Style::default().fg(Color::Rgb(255, 140, 0)).bold()),
|
|
800
|
+
]));
|
|
801
|
+
if let Some(reason) = &run.safety_reason {
|
|
802
|
+
info_lines.push(Line::from(vec![
|
|
803
|
+
Span::styled(" Reason: ", Style::default().fg(TEXT_DIM)),
|
|
804
|
+
Span::styled(reason, Style::default().fg(TEXT_PRIMARY)),
|
|
805
|
+
]));
|
|
806
|
+
}
|
|
807
|
+
if let Some(error) = &run.error_message {
|
|
808
|
+
info_lines.push(Line::from(vec![
|
|
809
|
+
Span::styled(" Error: ", Style::default().fg(TEXT_DIM)),
|
|
810
|
+
Span::styled(error, Style::default().fg(Color::Rgb(255, 69, 69))),
|
|
811
|
+
]));
|
|
812
|
+
}
|
|
813
|
+
} else if let Some(error) = &run.error_message {
|
|
769
814
|
info_lines.push(Line::from(""));
|
|
770
815
|
info_lines
|
|
771
816
|
.push(Line::from("Error:").style(Style::default().fg(Color::Rgb(255, 69, 69)).bold()));
|