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.
@@ -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};
@@ -431,6 +432,7 @@ pub struct AppState {
431
432
  pub agent_builder: AgentBuilderState,
432
433
  pub run_manager: RunManagerState,
433
434
  pub sentinel_config: SentinelConfigState,
435
+ pub shield_dashboard: ShieldDashboardState,
434
436
  pub settings: SettingsState,
435
437
  pub agent_list: AgentListState,
436
438
  pub connection_portal: ConnectionPortalState,
@@ -463,6 +465,7 @@ pub struct AppState {
463
465
 
464
466
  pub pending_sentinel_load_id: Option<String>,
465
467
  pub pending_sentinel_apply_id: Option<String>,
468
+ pub pending_shield_load_id: Option<String>,
466
469
 
467
470
  // Deletion confirmation (Step 5.5)
468
471
  pub agent_delete_requested: bool,
@@ -530,6 +533,7 @@ impl Default for AppState {
530
533
  agent_builder: AgentBuilderState::default(),
531
534
  run_manager: RunManagerState::default(),
532
535
  sentinel_config: SentinelConfigState::default(),
536
+ shield_dashboard: ShieldDashboardState::default(),
533
537
  settings: SettingsState::default(),
534
538
  agent_list: AgentListState::default(),
535
539
  connection_portal: ConnectionPortalState::default(),
@@ -554,6 +558,7 @@ impl Default for AppState {
554
558
  pending_run_quick_id: None,
555
559
  pending_sentinel_load_id: None,
556
560
  pending_sentinel_apply_id: None,
561
+ pending_shield_load_id: None,
557
562
  agent_delete_requested: false,
558
563
  operation_mode: OperationMode::Local,
559
564
  cache: Cache::new().ok(),
@@ -832,6 +837,59 @@ impl App {
832
837
  }
833
838
  }
834
839
 
840
+ pub fn open_shield_dashboard(&mut self, ws: Option<&WebSocketClient>) {
841
+ self.push_overlay(Screen::ShieldDashboard);
842
+ self.state
843
+ .logs
844
+ .push_back("[NAV] Opening Shield (production safety layer)...".into());
845
+ if self.state.operation_mode == OperationMode::Connected {
846
+ if let Some(ws) = ws {
847
+ self.begin_shield_load_request(ws, false);
848
+ }
849
+ } else {
850
+ self.state.shield_dashboard.loading = false;
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, force: bool) {
859
+ if self.state.operation_mode != OperationMode::Connected {
860
+ return;
861
+ }
862
+ if !force && self.state.pending_shield_load_id.is_some() {
863
+ return;
864
+ }
865
+ if force {
866
+ self.state.pending_shield_load_id = None;
867
+ }
868
+ self.state.shield_dashboard.mark_loading();
869
+ self.state.shield_dashboard.error = None;
870
+ if let Ok(id) = ws.send_command("shield.load", None) {
871
+ self.state.pending_shield_load_id = Some(id);
872
+ } else {
873
+ self.state.shield_dashboard.fail_loading(
874
+ "Failed to send shield.load to CLI.".to_string(),
875
+ );
876
+ }
877
+ }
878
+
879
+ pub fn open_run_from_shield_block(&mut self, ws: Option<&WebSocketClient>, run_id: &str) {
880
+ self.push_overlay(Screen::RunManager);
881
+ self.state.run_manager.detail_run_id = Some(run_id.to_string());
882
+ self.add_log(format!(
883
+ "[SHIELD] Opening blocked run {} in Run Manager",
884
+ &run_id[run_id.len().saturating_sub(8)..]
885
+ ));
886
+ if let Some(ws) = ws {
887
+ self.begin_run_list_request(ws, false);
888
+ self.begin_run_get_request(ws, run_id);
889
+ }
890
+ self.request_immediate_render("shield_open_run");
891
+ }
892
+
835
893
  pub fn open_sentinel_config(&mut self, ws: Option<&WebSocketClient>) {
836
894
  self.state.pending_sentinel_load_id = None;
837
895
  self.state.pending_sentinel_apply_id = None;
@@ -1320,6 +1378,11 @@ impl App {
1320
1378
  return self.handle_sentinel_config_input(key, ws_client);
1321
1379
  }
1322
1380
 
1381
+ // === SHIELD DASHBOARD INPUT HANDLING ===
1382
+ if self.state.navigation.current_screen() == &Screen::ShieldDashboard {
1383
+ return self.handle_shield_dashboard_input(key, ws_client);
1384
+ }
1385
+
1323
1386
  // === SETTINGS INPUT HANDLING ===
1324
1387
  if self.state.navigation.current_screen() == &Screen::Settings {
1325
1388
  return self.handle_settings_input(key);
@@ -1493,6 +1556,10 @@ impl App {
1493
1556
  " sentinel - Configure Sentinel policies (templates)"
1494
1557
  .to_string(),
1495
1558
  );
1559
+ self.state.logs.push_back(
1560
+ " shield - Shield safety dashboard (live metrics + enforcement)"
1561
+ .to_string(),
1562
+ );
1496
1563
  self.state.logs.push_back(
1497
1564
  " build - Open Agent Builder (6-step wizard)"
1498
1565
  .to_string(),
@@ -1574,55 +1641,8 @@ impl App {
1574
1641
  "sentinel" | "sentinel config" | "sentinel policies" => {
1575
1642
  self.open_sentinel_config(ws_client);
1576
1643
  }
1577
- "shield" | "shield status" => {
1578
- if self.state.operation_mode == OperationMode::Connected {
1579
- if let Some(ws) = ws_client {
1580
- if let Ok(id) = ws.send_command("shield.status", None) {
1581
- self.add_log(format!(
1582
- "[SHIELD] Fetching live status (id: {})",
1583
- &id[id.len().saturating_sub(8)..]
1584
- ));
1585
- }
1586
- }
1587
- } else {
1588
- self.add_log(
1589
- "[SHIELD] Connect to Gateway first.".to_string(),
1590
- );
1591
- }
1592
- }
1593
- "shield probe" => {
1594
- if self.state.operation_mode == OperationMode::Connected {
1595
- if let Some(ws) = ws_client {
1596
- if let Ok(id) = ws.send_command("shield.probe", None) {
1597
- self.add_log(format!(
1598
- "[SHIELD] Probing injection detector (id: {})",
1599
- &id[id.len().saturating_sub(8)..]
1600
- ));
1601
- }
1602
- }
1603
- } else {
1604
- self.add_log(
1605
- "[SHIELD] Connect to Gateway first.".to_string(),
1606
- );
1607
- }
1608
- }
1609
- "shield demo" => {
1610
- if self.state.operation_mode == OperationMode::Connected {
1611
- if let Some(ws) = ws_client {
1612
- if let Ok(id) = ws.send_command("shield.demo", None) {
1613
- self.add_log(format!(
1614
- "[SHIELD] Starting demo run — check Run Manager (id: {})",
1615
- &id[id.len().saturating_sub(8)..]
1616
- ));
1617
- }
1618
- self.push_overlay(Screen::RunManager);
1619
- self.begin_run_list_request(ws, false);
1620
- }
1621
- } else {
1622
- self.add_log(
1623
- "[SHIELD] Connect to Gateway first.".to_string(),
1624
- );
1625
- }
1644
+ "shield" | "shield status" | "shield dashboard" => {
1645
+ self.open_shield_dashboard(ws_client);
1626
1646
  }
1627
1647
  "config" | "settings" => {
1628
1648
  self.push_overlay(Screen::Settings);
@@ -1908,6 +1928,10 @@ impl App {
1908
1928
  use crate::ui::sentinel_config;
1909
1929
  sentinel_config::render(f, &self.state);
1910
1930
  }
1931
+ Screen::ShieldDashboard => {
1932
+ use crate::ui::shield_dashboard;
1933
+ shield_dashboard::render(f, &self.state);
1934
+ }
1911
1935
  Screen::Settings => {
1912
1936
  use crate::ui::settings;
1913
1937
  settings::render(f, &self.state);
@@ -2642,6 +2666,121 @@ impl App {
2642
2666
  Ok(false)
2643
2667
  }
2644
2668
 
2669
+ fn handle_shield_dashboard_input(
2670
+ &mut self,
2671
+ key: KeyEvent,
2672
+ ws_client: Option<&WebSocketClient>,
2673
+ ) -> anyhow::Result<bool> {
2674
+ use crate::ui::shield_dashboard::ShieldTab;
2675
+ use crossterm::event::KeyModifiers;
2676
+
2677
+ if key.modifiers.contains(KeyModifiers::CONTROL) {
2678
+ match key.code {
2679
+ KeyCode::Char('c') | KeyCode::Char('q') => return Ok(true),
2680
+ _ => return Ok(false),
2681
+ }
2682
+ }
2683
+
2684
+ match key.code {
2685
+ KeyCode::Tab => {
2686
+ self.state.shield_dashboard.tab = self.state.shield_dashboard.tab.next();
2687
+ self.request_render("shield_tab");
2688
+ }
2689
+ KeyCode::BackTab => {
2690
+ self.state.shield_dashboard.tab = self.state.shield_dashboard.tab.prev();
2691
+ self.request_render("shield_tab_back");
2692
+ }
2693
+ KeyCode::Left => {
2694
+ self.state.shield_dashboard.tab = self.state.shield_dashboard.tab.prev();
2695
+ self.request_render("shield_tab_left");
2696
+ }
2697
+ KeyCode::Right => {
2698
+ self.state.shield_dashboard.tab = self.state.shield_dashboard.tab.next();
2699
+ self.request_render("shield_tab_right");
2700
+ }
2701
+ KeyCode::Up => {
2702
+ if self.state.shield_dashboard.tab == ShieldTab::Enforcement
2703
+ && self.state.shield_dashboard.selected_block_index > 0
2704
+ {
2705
+ self.state.shield_dashboard.selected_block_index -= 1;
2706
+ self.request_render("shield_block_up");
2707
+ }
2708
+ }
2709
+ KeyCode::Down => {
2710
+ if self.state.shield_dashboard.tab == ShieldTab::Enforcement {
2711
+ let max = self.state.shield_dashboard.recent_blocks.len();
2712
+ if max > 0 && self.state.shield_dashboard.selected_block_index + 1 < max {
2713
+ self.state.shield_dashboard.selected_block_index += 1;
2714
+ self.request_render("shield_block_down");
2715
+ }
2716
+ }
2717
+ }
2718
+ KeyCode::Enter => {
2719
+ if self.state.shield_dashboard.tab == ShieldTab::Enforcement {
2720
+ if let Some(row) = self
2721
+ .state
2722
+ .shield_dashboard
2723
+ .recent_blocks
2724
+ .get(self.state.shield_dashboard.selected_block_index)
2725
+ {
2726
+ let run_id = row.id.clone();
2727
+ self.open_run_from_shield_block(ws_client, &run_id);
2728
+ }
2729
+ }
2730
+ }
2731
+ KeyCode::Char('r') | KeyCode::Char('R') => {
2732
+ if self.state.operation_mode == OperationMode::Connected {
2733
+ if let Some(ws) = ws_client {
2734
+ self.begin_shield_load_request(ws, true);
2735
+ }
2736
+ } else {
2737
+ self.state.shield_dashboard.fail_loading(
2738
+ "Connect to Gateway first (connect portal).".to_string(),
2739
+ );
2740
+ }
2741
+ self.request_render("shield_refresh");
2742
+ }
2743
+ KeyCode::Char('o') | KeyCode::Char('O') => {
2744
+ self.push_overlay(Screen::RunManager);
2745
+ if let Some(ws) = ws_client {
2746
+ self.begin_run_list_request(ws, false);
2747
+ }
2748
+ self.add_log("[SHIELD] Opened Run Manager".to_string());
2749
+ self.request_render("shield_open_runs");
2750
+ }
2751
+ KeyCode::Char('p') | KeyCode::Char('P') => {
2752
+ if self.state.operation_mode == OperationMode::Connected {
2753
+ self.state.navigation.navigate_to_base(Screen::PortalMonitoring);
2754
+ if let Some(ws) = ws_client {
2755
+ self.begin_portal_observability_request(ws);
2756
+ }
2757
+ self.add_log("[SHIELD] Opened Portal Monitoring".to_string());
2758
+ } else {
2759
+ self.add_log("[SHIELD] Connect to Gateway first.".to_string());
2760
+ }
2761
+ self.request_render("shield_open_monitoring");
2762
+ }
2763
+ KeyCode::Char('a') | KeyCode::Char('A') => {
2764
+ self.state.shield_dashboard.auto_refresh_enabled =
2765
+ !self.state.shield_dashboard.auto_refresh_enabled;
2766
+ let status = if self.state.shield_dashboard.auto_refresh_enabled {
2767
+ "ON"
2768
+ } else {
2769
+ "OFF"
2770
+ };
2771
+ self.add_log(format!("[SHIELD] Live refresh {}", status));
2772
+ self.request_render("shield_live_toggle");
2773
+ }
2774
+ KeyCode::Esc => {
2775
+ self.pop_overlay();
2776
+ self.request_render("shield_close");
2777
+ }
2778
+ _ => {}
2779
+ }
2780
+
2781
+ Ok(false)
2782
+ }
2783
+
2645
2784
  // ============================================================
2646
2785
  // SETTINGS INPUT HANDLING (Step 4.8)
2647
2786
  // ============================================================
@@ -189,6 +189,44 @@ 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, false);
209
+ }
210
+ }
211
+
212
+ // Loading timeout — never spin forever if CLI deduped or dropped a response
213
+ let loading_stuck = {
214
+ let sd = &app.state.shield_dashboard;
215
+ sd.loading
216
+ && sd
217
+ .loading_started
218
+ .map(|t| t.elapsed() >= std::time::Duration::from_secs(12))
219
+ .unwrap_or(false)
220
+ };
221
+ if loading_stuck {
222
+ app.state.shield_dashboard.fail_loading(
223
+ "Shield load timed out — press R to refresh.".to_string(),
224
+ );
225
+ app.state.pending_shield_load_id = None;
226
+ app.request_render("shield_load_timeout");
227
+ }
228
+ }
229
+
192
230
  // Run Manager: live poll run.list every ~3s when enabled (no manual R)
193
231
  if matches!(current_screen, Screen::RunManager)
194
232
  && app.state.operation_mode == OperationMode::Connected
@@ -669,6 +707,66 @@ fn main() -> Result<()> {
669
707
  }
670
708
  app.request_render("run_quick_ok");
671
709
  }
710
+ // Shield dashboard: load health + config + metrics + history
711
+ else if Some(&resp.id)
712
+ == app.state.pending_shield_load_id.as_ref()
713
+ {
714
+ app.state.pending_shield_load_id = None;
715
+ if obj.get("shieldLoad").and_then(|v| v.as_bool()) == Some(true)
716
+ {
717
+ use crate::ui::shield_dashboard::parse_shield_load;
718
+ let health_val = obj
719
+ .get("health")
720
+ .cloned()
721
+ .unwrap_or(serde_json::Value::Null);
722
+ let config_val = obj
723
+ .get("config")
724
+ .cloned()
725
+ .unwrap_or(serde_json::Value::Null);
726
+ let metrics_val = obj
727
+ .get("metrics")
728
+ .cloned()
729
+ .unwrap_or(serde_json::Value::Null);
730
+ let gateway_health_val = obj
731
+ .get("gatewayHealth")
732
+ .cloned()
733
+ .unwrap_or(serde_json::Value::Null);
734
+ let runs_val = obj
735
+ .get("recentRuns")
736
+ .cloned()
737
+ .unwrap_or(serde_json::Value::Null);
738
+ let warnings_vec: Vec<String> = obj
739
+ .get("warnings")
740
+ .and_then(|v| v.as_array())
741
+ .map(|a| {
742
+ a.iter()
743
+ .filter_map(|w| {
744
+ w.as_str().map(|s| s.to_string())
745
+ })
746
+ .collect()
747
+ })
748
+ .unwrap_or_default();
749
+ let (h, c, m, mut w, recent) = parse_shield_load(
750
+ &health_val,
751
+ &config_val,
752
+ &metrics_val,
753
+ &gateway_health_val,
754
+ &runs_val,
755
+ );
756
+ w.extend(warnings_vec);
757
+ app.state.shield_dashboard.apply_load(
758
+ &h, &c, &m, w, recent,
759
+ );
760
+ app.state.shield_mode = h.mode.clone();
761
+ app.state.shield_blocks_total = m.blocks;
762
+ app.state.shield_masks_total = m.masks;
763
+ app.add_log(format!(
764
+ "✓ [{}] Shield loaded — {} block(s), {} mask(s)",
765
+ short_id, m.blocks, m.masks
766
+ ));
767
+ }
768
+ app.request_render("shield_loaded");
769
+ }
672
770
  // Sentinel config: load templates + current + health
673
771
  else if Some(&resp.id)
674
772
  == app.state.pending_sentinel_load_id.as_ref()
@@ -1542,6 +1640,15 @@ fn main() -> Result<()> {
1542
1640
  "✗ [{}] run.quick failed: {}",
1543
1641
  short_id, error_msg
1544
1642
  ));
1643
+ } else if Some(&resp.id) == app.state.pending_shield_load_id.as_ref() {
1644
+ app.state.pending_shield_load_id = None;
1645
+ app.state
1646
+ .shield_dashboard
1647
+ .fail_loading(error_msg.clone());
1648
+ app.add_log(format!(
1649
+ "✗ [{}] shield.load failed: {}",
1650
+ short_id, error_msg
1651
+ ));
1545
1652
  } else if Some(&resp.id) == app.state.pending_sentinel_load_id.as_ref()
1546
1653
  {
1547
1654
  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",
@@ -8,5 +8,6 @@ pub mod portal_monitoring;
8
8
  pub mod run_manager;
9
9
  pub mod safe_viewport;
10
10
  pub mod sentinel_config;
11
+ pub mod shield_dashboard;
11
12
  pub mod settings;
12
13
  pub mod setup_portal;