Spaces:
Sleeping
Sleeping
Parse LobsterTrap JSON output; show full payload on diagnostics page
Browse files- agents/agents.py +82 -7
- frontend/pages/05_Security_Diagnostics.py +67 -13
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 |
-
|
|
|
|
|
|
|
| 426 |
|
| 427 |
-
|
| 428 |
-
blocked = (result.returncode != 0) or ("DENY" in output) or ("BLOCK" in output)
|
| 429 |
|
| 430 |
return {
|
| 431 |
-
"is_safe":
|
| 432 |
-
"risk_score":
|
| 433 |
-
"reason":
|
| 434 |
-
"raw_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 |
-
|
| 74 |
-
|
|
|
|
| 75 |
return {
|
| 76 |
"returncode": result.returncode,
|
| 77 |
-
"stdout":
|
| 78 |
-
"stderr":
|
| 79 |
-
"verdict":
|
| 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"
|
| 94 |
elif verdict == "ALLOWED":
|
| 95 |
-
st.success(f"
|
| 96 |
else:
|
| 97 |
-
st.warning(f"
|
| 98 |
st.write(f"- exit code: `{res['returncode']}`")
|
| 99 |
if res["stdout"]:
|
| 100 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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"
|
| 117 |
elif verdict == "ALLOWED":
|
| 118 |
-
st.success(f"
|
| 119 |
else:
|
| 120 |
-
st.warning(f"
|
| 121 |
st.write(f"- exit code: `{res['returncode']}`")
|
| 122 |
if res["stdout"]:
|
| 123 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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")
|