4runr-os 2.10.73 → 2.10.74

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.
@@ -279,6 +279,58 @@ fn main() -> Result<()> {
279
279
  app.state.posture_status = posture_str.to_string();
280
280
  }
281
281
  }
282
+ if let Some(sh) =
283
+ obj.get("shield").and_then(|v| v.as_object())
284
+ {
285
+ if let Some(m) =
286
+ sh.get("mode").and_then(|v| v.as_str())
287
+ {
288
+ app.state.shield_mode = m.to_string();
289
+ }
290
+ if let Some(dets) =
291
+ sh.get("detectors").and_then(|v| v.as_array())
292
+ {
293
+ app.state.shield_detectors = dets
294
+ .iter()
295
+ .filter_map(|v| {
296
+ v.as_str().map(|s| s.to_string())
297
+ })
298
+ .collect();
299
+ }
300
+ app.state.shield_blocks_total = sh
301
+ .get("blocksTotal")
302
+ .and_then(|v| v.as_u64())
303
+ .unwrap_or(0);
304
+ app.state.shield_masks_total = sh
305
+ .get("masksTotal")
306
+ .and_then(|v| v.as_u64())
307
+ .unwrap_or(0);
308
+ }
309
+ if let Some(sent) =
310
+ obj.get("sentinel").and_then(|v| v.as_object())
311
+ {
312
+ app.state.sentinel_active_runs = sent
313
+ .get("watchedRuns")
314
+ .and_then(|v| v.as_u64())
315
+ .unwrap_or(0) as usize;
316
+ let enabled = sent
317
+ .get("enabled")
318
+ .and_then(|v| v.as_bool())
319
+ .unwrap_or(false);
320
+ let healthy = sent
321
+ .get("healthy")
322
+ .and_then(|v| v.as_bool())
323
+ .unwrap_or(false);
324
+ app.state.sentinel_state = if !enabled {
325
+ "off".to_string()
326
+ } else if app.state.sentinel_active_runs > 0 {
327
+ "watching".to_string()
328
+ } else if healthy {
329
+ "idle".to_string()
330
+ } else {
331
+ "degraded".to_string()
332
+ };
333
+ }
282
334
  app.add_log(format!(
283
335
  "✓ [{}] Mode: {}, Posture: {}",
284
336
  short_id,
@@ -288,6 +340,61 @@ fn main() -> Result<()> {
288
340
  .unwrap_or("?")
289
341
  ));
290
342
  }
343
+ else if obj.get("shieldDemo").and_then(|v| v.as_bool())
344
+ == Some(true)
345
+ {
346
+ let msg = obj
347
+ .get("message")
348
+ .and_then(|v| v.as_str())
349
+ .unwrap_or("Shield demo run started");
350
+ app.add_log(format!("✓ [{}] {}", short_id, msg));
351
+ if let Some(rid) =
352
+ obj.get("runId").and_then(|v| v.as_str())
353
+ {
354
+ app.add_log(format!(
355
+ "[SHIELD] Demo run id: {} — open Run Manager",
356
+ rid
357
+ ));
358
+ }
359
+ }
360
+ else if obj.get("shieldProbe").and_then(|v| v.as_bool())
361
+ == Some(true)
362
+ {
363
+ let result = obj.get("result");
364
+ let action = result
365
+ .and_then(|r| r.get("decision"))
366
+ .and_then(|d| d.get("action"))
367
+ .and_then(|a| a.as_str())
368
+ .unwrap_or("?");
369
+ app.add_log(format!(
370
+ "✓ [{}] Shield probe: action={}",
371
+ short_id, action
372
+ ));
373
+ }
374
+ else if obj.get("shieldStatus").and_then(|v| v.as_bool())
375
+ == Some(true)
376
+ {
377
+ let metrics = obj.get("metrics").and_then(|v| v.as_object());
378
+ let blocks = metrics
379
+ .and_then(|m| m.get("blocks"))
380
+ .and_then(|v| v.as_u64())
381
+ .unwrap_or(0);
382
+ let masks = metrics
383
+ .and_then(|m| m.get("masks"))
384
+ .and_then(|v| v.as_u64())
385
+ .unwrap_or(0);
386
+ app.state.shield_blocks_total = blocks;
387
+ app.state.shield_masks_total = masks;
388
+ let mode = obj
389
+ .get("health")
390
+ .and_then(|h| h.get("mode"))
391
+ .and_then(|v| v.as_str())
392
+ .unwrap_or("?");
393
+ app.add_log(format!(
394
+ "✓ [{}] Shield {} — {} block(s), {} mask(s)",
395
+ short_id, mode, blocks, masks
396
+ ));
397
+ }
291
398
  // Handle agent.list response
292
399
  else if let Some(agents_data) = obj.get("agents") {
293
400
  if let Some(agents_array) = agents_data.as_array() {
@@ -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() {
@@ -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 error_message = o
217
- .get("output")
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 line = Line::from(vec![
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
- // Add error if present
768
- if let Some(error) = &run.error_message {
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()));
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "4runr-os",
3
- "version": "2.10.73",
3
+ "version": "2.10.74",
4
4
  "type": "module",
5
- "description": "4Runr AI Agent OS - Secure terminal interface for AI agents. v2.10.73: Disconnect stops local Gateway + Postgres/Redis; reconnect ensures Docker stack before /ready.",
5
+ "description": "4Runr AI Agent OS - Secure terminal interface for AI agents. v2.10.74: Shield visible in Run Manager, System Status, monitoring; shield demo/probe/status commands; stronger tools smoke.",
6
6
  "main": "dist/index.js",
7
7
  "bin": {
8
8
  "4runr": "dist/index.js",