Abrar144 commited on
Commit
2374c60
·
1 Parent(s): 054755e

Parse LobsterTrap JSON output; show full payload on diagnostics page

Browse files
agents/agents.py CHANGED
@@ -422,16 +422,18 @@ def _lobster_trap_inspect(question: str) -> dict:
422
 
423
  try:
424
  result = subprocess.run(cmd, capture_output=True, text=True, timeout=5)
425
- output = ((result.stdout or "") + "\n" + (result.stderr or "")).strip()
 
 
426
 
427
- # Treat non-zero exit as blocked; also scan for common deny keywords.
428
- blocked = (result.returncode != 0) or ("DENY" in output) or ("BLOCK" in output)
429
 
430
  return {
431
- "is_safe": not blocked,
432
- "risk_score": 100 if blocked else 0,
433
- "reason": output if blocked else "Clean",
434
- "raw_output": output,
 
435
  }
436
  except subprocess.TimeoutExpired:
437
  return {"is_safe": True, "risk_score": 0, "reason": "LobsterTrap timeout (fallback allow)"}
@@ -439,6 +441,79 @@ def _lobster_trap_inspect(question: str) -> dict:
439
  return {"is_safe": True, "risk_score": 0, "reason": f"LobsterTrap error: {e} (fallback allow)"}
440
 
441
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
442
  # ════════════════════════════════════════════════
443
  # MAIN PIPELINE
444
  # ════════════════════════════════════════════════
 
422
 
423
  try:
424
  result = subprocess.run(cmd, capture_output=True, text=True, timeout=5)
425
+ stdout = (result.stdout or "").strip()
426
+ stderr = (result.stderr or "").strip()
427
+ combined = (stdout + "\n" + stderr).strip()
428
 
429
+ verdict_data = _parse_lobstertrap_output(stdout, stderr, result.returncode)
 
430
 
431
  return {
432
+ "is_safe": verdict_data["is_safe"],
433
+ "risk_score": verdict_data["risk_score"],
434
+ "reason": verdict_data["reason"],
435
+ "raw_output": combined,
436
+ "exit_code": result.returncode,
437
  }
438
  except subprocess.TimeoutExpired:
439
  return {"is_safe": True, "risk_score": 0, "reason": "LobsterTrap timeout (fallback allow)"}
 
441
  return {"is_safe": True, "risk_score": 0, "reason": f"LobsterTrap error: {e} (fallback allow)"}
442
 
443
 
444
+ def _parse_lobstertrap_output(stdout: str, stderr: str, returncode: int) -> dict:
445
+ """
446
+ Parse LobsterTrap's verdict from its output.
447
+
448
+ Robust to multiple output shapes:
449
+ 1. JSON object on stdout (current binary behavior). Fields we care about:
450
+ - verdict / action : "DENY", "ALLOW", "HUMAN_REVIEW", "LOG"
451
+ - risk_score : 0.0–1.0
452
+ - deny_message : human-readable explanation if blocked
453
+ 2. Plain text containing "[LOBSTER TRAP] Blocked: ..." lines.
454
+ 3. Non-zero exit code (treated as block).
455
+ """
456
+ import json
457
+
458
+ # Defaults: ALLOW
459
+ is_safe = True
460
+ risk_score = 0
461
+ reason = "Clean"
462
+
463
+ # Path 1: try JSON
464
+ parsed = None
465
+ if stdout:
466
+ try:
467
+ parsed = json.loads(stdout)
468
+ except Exception:
469
+ # Some tools concatenate multiple JSON objects; try the first one
470
+ try:
471
+ first_brace = stdout.find("{")
472
+ if first_brace >= 0:
473
+ parsed = json.loads(stdout[first_brace:])
474
+ except Exception:
475
+ parsed = None
476
+
477
+ if isinstance(parsed, dict):
478
+ verdict = str(parsed.get("verdict") or parsed.get("action") or "").upper()
479
+ risk = parsed.get("risk_score")
480
+ deny_msg = parsed.get("deny_message") or parsed.get("reason") or ""
481
+
482
+ if isinstance(risk, (int, float)):
483
+ risk_score = int(round(float(risk) * 100))
484
+
485
+ # Block on explicit DENY, or if risk_score >= 0.6 (matches policy's review_high_risk),
486
+ # or if any rule fired with a deny_message.
487
+ if verdict in ("DENY", "BLOCK", "DENIED", "BLOCKED"):
488
+ is_safe = False
489
+ reason = deny_msg or f"Verdict: {verdict}"
490
+ elif verdict == "HUMAN_REVIEW":
491
+ # Fail closed: in production we'd queue this; for now treat as block.
492
+ is_safe = False
493
+ reason = deny_msg or "Flagged for human review (treating as block)"
494
+ elif isinstance(risk, (int, float)) and float(risk) >= 0.6:
495
+ is_safe = False
496
+ reason = deny_msg or f"High risk score: {risk}"
497
+ elif deny_msg and "[LOBSTER TRAP] Blocked" in deny_msg:
498
+ is_safe = False
499
+ reason = deny_msg
500
+ else:
501
+ is_safe = True
502
+ reason = "Clean"
503
+ return {"is_safe": is_safe, "risk_score": risk_score, "reason": reason}
504
+
505
+ # Path 2: text-based fallback
506
+ combined = (stdout + "\n" + stderr).strip()
507
+ if "[LOBSTER TRAP] Blocked" in combined or "DENY" in combined.upper() or "BLOCK" in combined.upper():
508
+ return {"is_safe": False, "risk_score": 100, "reason": combined.splitlines()[0] if combined else "Blocked"}
509
+
510
+ # Path 3: exit code
511
+ if returncode != 0:
512
+ return {"is_safe": False, "risk_score": 100, "reason": f"non-zero exit code {returncode}: {combined[:200]}"}
513
+
514
+ return {"is_safe": True, "risk_score": 0, "reason": "Clean"}
515
+
516
+
517
  # ════════════════════════════════════════════════
518
  # MAIN PIPELINE
519
  # ════════════════════════════════════════════════
frontend/pages/05_Security_Diagnostics.py CHANGED
@@ -70,13 +70,14 @@ def run_inspect(question: str) -> dict:
70
  cmd = [str(BINARY), "inspect", "--policy", str(POLICY), question]
71
  try:
72
  result = subprocess.run(cmd, capture_output=True, text=True, timeout=5)
73
- output = ((result.stdout or "") + "\n" + (result.stderr or "")).strip()
74
- blocked = (result.returncode != 0) or ("DENY" in output) or ("BLOCK" in output)
 
75
  return {
76
  "returncode": result.returncode,
77
- "stdout": result.stdout,
78
- "stderr": result.stderr,
79
- "verdict": "BLOCKED" if blocked else "ALLOWED",
80
  }
81
  except subprocess.TimeoutExpired:
82
  return {"returncode": None, "stdout": "", "stderr": "timeout", "verdict": "TIMEOUT"}
@@ -84,20 +85,66 @@ def run_inspect(question: str) -> dict:
84
  return {"returncode": None, "stdout": "", "stderr": str(exc), "verdict": "ERROR"}
85
 
86
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87
  if st.button("▶ Run canary tests", type="primary", use_container_width=True):
88
  for label, question in CANARIES:
89
  with st.expander(f"**{label}** — `{question}`", expanded=True):
90
  res = run_inspect(question)
91
  verdict = res["verdict"]
92
  if verdict == "BLOCKED":
93
- st.error(f"Verdict: {verdict}")
94
  elif verdict == "ALLOWED":
95
- st.success(f"Verdict: {verdict}")
96
  else:
97
- st.warning(f"Verdict: {verdict}")
98
  st.write(f"- exit code: `{res['returncode']}`")
99
  if res["stdout"]:
100
- st.code(res["stdout"], language="text")
 
 
 
 
 
 
 
 
101
  if res["stderr"]:
102
  st.caption("stderr")
103
  st.code(res["stderr"], language="text")
@@ -113,14 +160,21 @@ if st.button("Inspect"):
113
  res = run_inspect(custom)
114
  verdict = res["verdict"]
115
  if verdict == "BLOCKED":
116
- st.error(f"Verdict: {verdict}")
117
  elif verdict == "ALLOWED":
118
- st.success(f"Verdict: {verdict}")
119
  else:
120
- st.warning(f"Verdict: {verdict}")
121
  st.write(f"- exit code: `{res['returncode']}`")
122
  if res["stdout"]:
123
- st.code(res["stdout"], language="text")
 
 
 
 
 
 
 
124
  if res["stderr"]:
125
  st.caption("stderr")
126
  st.code(res["stderr"], language="text")
 
70
  cmd = [str(BINARY), "inspect", "--policy", str(POLICY), question]
71
  try:
72
  result = subprocess.run(cmd, capture_output=True, text=True, timeout=5)
73
+ stdout = result.stdout or ""
74
+ stderr = result.stderr or ""
75
+ verdict = _classify(stdout, stderr, result.returncode)
76
  return {
77
  "returncode": result.returncode,
78
+ "stdout": stdout,
79
+ "stderr": stderr,
80
+ "verdict": verdict,
81
  }
82
  except subprocess.TimeoutExpired:
83
  return {"returncode": None, "stdout": "", "stderr": "timeout", "verdict": "TIMEOUT"}
 
85
  return {"returncode": None, "stdout": "", "stderr": str(exc), "verdict": "ERROR"}
86
 
87
 
88
+ def _classify(stdout: str, stderr: str, returncode: int) -> str:
89
+ """Mirror the agent's verdict logic so this page agrees with what the chat blocks."""
90
+ import json as _json
91
+ try:
92
+ # Try strict JSON, then JSON starting at first '{'
93
+ text = stdout.strip()
94
+ try:
95
+ payload = _json.loads(text)
96
+ except Exception:
97
+ idx = text.find("{")
98
+ payload = _json.loads(text[idx:]) if idx >= 0 else None
99
+ except Exception:
100
+ payload = None
101
+
102
+ if isinstance(payload, dict):
103
+ v = str(payload.get("verdict") or payload.get("action") or "").upper()
104
+ risk = payload.get("risk_score")
105
+ if v in ("DENY", "BLOCK", "DENIED", "BLOCKED"):
106
+ return "BLOCKED"
107
+ if v == "HUMAN_REVIEW":
108
+ return "BLOCKED"
109
+ if isinstance(risk, (int, float)) and float(risk) >= 0.6:
110
+ return "BLOCKED"
111
+ deny_msg = str(payload.get("deny_message") or payload.get("reason") or "")
112
+ if "[LOBSTER TRAP] Blocked" in deny_msg:
113
+ return "BLOCKED"
114
+ if v == "ALLOW" or v == "ALLOWED":
115
+ return "ALLOWED"
116
+ # JSON present but no decisive field — fall through
117
+
118
+ combined = (stdout + "\n" + stderr).upper()
119
+ if "[LOBSTER TRAP] BLOCKED" in combined or "DENY" in combined or "BLOCK" in combined:
120
+ return "BLOCKED"
121
+ if returncode != 0:
122
+ return "BLOCKED"
123
+ return "ALLOWED"
124
+
125
+
126
  if st.button("▶ Run canary tests", type="primary", use_container_width=True):
127
  for label, question in CANARIES:
128
  with st.expander(f"**{label}** — `{question}`", expanded=True):
129
  res = run_inspect(question)
130
  verdict = res["verdict"]
131
  if verdict == "BLOCKED":
132
+ st.error(f"Wrapper verdict: {verdict}")
133
  elif verdict == "ALLOWED":
134
+ st.success(f"Wrapper verdict: {verdict}")
135
  else:
136
+ st.warning(f"Wrapper verdict: {verdict}")
137
  st.write(f"- exit code: `{res['returncode']}`")
138
  if res["stdout"]:
139
+ # Try to render as JSON if possible — easier to read full payload.
140
+ import json as _json
141
+ try:
142
+ payload = _json.loads(res["stdout"])
143
+ st.caption("stdout (parsed as JSON)")
144
+ st.json(payload)
145
+ except Exception:
146
+ st.caption("stdout (raw)")
147
+ st.code(res["stdout"], language="text")
148
  if res["stderr"]:
149
  st.caption("stderr")
150
  st.code(res["stderr"], language="text")
 
160
  res = run_inspect(custom)
161
  verdict = res["verdict"]
162
  if verdict == "BLOCKED":
163
+ st.error(f"Wrapper verdict: {verdict}")
164
  elif verdict == "ALLOWED":
165
+ st.success(f"Wrapper verdict: {verdict}")
166
  else:
167
+ st.warning(f"Wrapper verdict: {verdict}")
168
  st.write(f"- exit code: `{res['returncode']}`")
169
  if res["stdout"]:
170
+ import json as _json
171
+ try:
172
+ payload = _json.loads(res["stdout"])
173
+ st.caption("stdout (parsed as JSON)")
174
+ st.json(payload)
175
+ except Exception:
176
+ st.caption("stdout (raw)")
177
+ st.code(res["stdout"], language="text")
178
  if res["stderr"]:
179
  st.caption("stderr")
180
  st.code(res["stderr"], language="text")