diff --git a/strix/interface/viewer/frontend/src/components/live/tool-renderers/VulnReportDeleteRenderer.tsx b/strix/interface/viewer/frontend/src/components/live/tool-renderers/VulnReportDeleteRenderer.tsx
index dc72aa89..90e3ccec 100644
--- a/strix/interface/viewer/frontend/src/components/live/tool-renderers/VulnReportDeleteRenderer.tsx
+++ b/strix/interface/viewer/frontend/src/components/live/tool-renderers/VulnReportDeleteRenderer.tsx
@@ -19,12 +19,15 @@ export default function VulnReportDeleteRenderer({ args, result, status }: ToolR
const title = typeof res?.title === "string" ? res.title : "";
const severity = typeof res?.severity === "string" ? res.severity.toLowerCase() : "";
const failed = res != null ? !ok : status === "failed" || status === "error";
+ const pending = res == null && !failed;
+ const label = pending ? "withdrawing report\u2026" : failed ? "report not withdrawn" : "report withdrawn";
+ const labelColor = pending ? "text-[#888]" : failed ? "text-red-400/80" : "text-yellow-400/80";
return (
-
- {failed ? "report not withdrawn" : "report withdrawn"}
+
+ {label}
{reportId && {reportId}}
{severity && (
diff --git a/strix/interface/viewer/static/assets/index-DNPiqLUu.js b/strix/interface/viewer/static/assets/index-DbPDzbcJ.js
similarity index 99%
rename from strix/interface/viewer/static/assets/index-DNPiqLUu.js
rename to strix/interface/viewer/static/assets/index-DbPDzbcJ.js
index f40b7604..5d395f7e 100644
--- a/strix/interface/viewer/static/assets/index-DNPiqLUu.js
+++ b/strix/interface/viewer/static/assets/index-DbPDzbcJ.js
@@ -498,7 +498,7 @@ https://github.com/highlightjs/highlight.js/issues/2277`),Ht=ye,ft=Le),Qe===void
`)}function g7({toolName:e,args:t,result:r}){const a=e==="write_stdin",s=a?t.chars??t.input??"":t.command??t.cmd??"",o=r;let c=null,d=null,h=null;if(o&&typeof o=="object"){c=typeof o.content=="string"?o.content:null,d=typeof o.error=="string"?o.error:null,h=typeof o.exit_code=="number"?o.exit_code:null;const p=typeof o.status=="string"?o.status:"";(p==="running"||p==="command still running")&&(c=null)}else typeof o=="string"&&(c=o);const f=c?p7(m7(c,s)):null;return m.jsxs("div",{children:[m.jsx("span",{className:"text-emerald-400/80 font-semibold text-sm",children:a?"Terminal input":"Terminal"}),s&&m.jsx(bg,{code:s,language:"bash",collapsible:!0}),d&&m.jsx(Ei,{className:"text-red-400/70",children:d}),f&&m.jsx(Ei,{className:"text-[#666]",children:f}),h!=null&&h!==0&&m.jsxs("div",{className:"font-mono text-[13px] text-red-400/70 mt-0.5",children:["exit code ",h]})]})}const c_={back:"going back in browser history",forward:"going forward in browser history",scroll_down:"scrolling down",scroll_up:"scrolling up",refresh:"refreshing",close_tab:"closing tab",switch_tab:"switching tab",list_tabs:"listing tabs",view_source:"viewing page source",get_console_logs:"getting console logs",screenshot:"taking screenshot",wait:"waiting...",close:"closing"},u_={click:"clicking",double_click:"double clicking",hover:"hovering"};function Im({prefix:e,url:t,suffix:r}){return m.jsxs("span",{className:"text-[#888] text-[13px]",children:[e,t&&m.jsx("a",{href:t,target:"_blank",rel:"noopener noreferrer",className:"text-cyan-400/80 hover:underline",children:t}),r]})}function x7(e){const t=e.action??"",r=e.url??void 0;if(t in c_)return c_[t];if(t==="launch")return r?m.jsx(Im,{prefix:"launching ",url:r}):"launching";if(t==="goto"||t==="navigate")return m.jsx(Im,{prefix:"navigating to ",url:r});if(t==="new_tab")return m.jsx(Im,{prefix:"opening tab ",url:r});if(t in u_)return u_[t];if(t==="type")return`typing "${(e.text??"").slice(0,40)}"`;if(t==="press_key"||t==="key_press")return`pressing key ${e.key??""}`;if(t==="save_pdf"||t==="save_as_pdf"){const a=e.file_path??"";return`saving PDF${a?` to ${a}`:""}`}return t==="execute_js"?"executing javascript":t||"browser action"}function b7({args:e}){const r=(e.action??"")==="execute_js"?e.js_code??e.code??"":"",a=x7(e);return m.jsxs("div",{children:[m.jsxs("div",{className:"flex items-center gap-2 min-w-0",children:[m.jsx("span",{className:"text-blue-400/80 font-semibold text-sm shrink-0",children:"Browser"}),m.jsx("span",{className:"min-w-0 truncate text-[#888] text-[13px]",children:a})]}),r&&m.jsx(bg,{code:r,language:"javascript",collapsible:!0})]})}function yg(e){return e.length>60?"..."+e.slice(-57):e}const pu=30;function y7({toolName:e,args:t}){const r=t.path??t.file_path??"",a=t.command??"",s=t.old_str??"",o=t.new_str??"",c=t.regex??"";let d;e==="list_files"?d="list":e==="search_files"?d="search":a==="view"?d="view":a==="create"?d="create":a==="str_replace"?d="edit":a==="undo_edit"?d="undo":a==="insert"?d="insert":d="file";const h=r?yg(r):"",f=c?` /${c}/`:"",p=s?s.split(`
`):[],g=o?o.split(`
`):[],b=p.length+g.length,y=b>pu,_=y?Math.round(pu*(p.length/b)):p.length,N=y?pu-_:g.length;return m.jsxs("div",{children:[m.jsxs("div",{className:"flex items-baseline gap-2",children:[m.jsx("span",{className:"text-sky-400/80 font-semibold text-sm shrink-0",children:d}),h&&m.jsx("span",{className:"text-[#888] font-mono text-[13px] break-all",children:h})]}),f&&m.jsx("div",{className:"text-purple-400/60 font-mono text-[13px] break-all mt-0.5",children:f}),(s||o)&&m.jsxs("div",{className:"font-mono text-[13px] leading-relaxed mt-1.5",children:[p.slice(0,_).map((S,w)=>m.jsxs("div",{className:"text-red-400/60",children:[m.jsx("span",{className:"select-none text-red-400/30 mr-1",children:"-"}),S]},`o${w}`)),g.slice(0,N).map((S,w)=>m.jsxs("div",{className:"text-emerald-400/60",children:[m.jsx("span",{className:"select-none text-emerald-400/30 mr-1",children:"+"}),S]},`n${w}`)),y&&m.jsxs("div",{className:"text-[#444] mt-0.5",children:["... ",b-pu," more lines"]})]})]})}const gu=30,v7="*** Begin Patch",_7="*** End Patch",d_="*** Add File: ",f_="*** Update File: ",h_="*** Delete File: ",w7={add:"create",update:"edit",delete:"delete"};function E7(e){const t=e.patch;return typeof t=="string"?t:t&&typeof t=="object"&&typeof t.patch=="string"?t.patch:typeof e.input=="string"?e.input:""}function N7(e){const t=[];let r=null;const a=()=>{r&&t.push(r),r=null};for(const s of e.split(`
-`))if(!(s===v7||s===_7))if(s.startsWith(d_))a(),r={kind:"add",path:s.slice(d_.length).trim(),oldLines:[],newLines:[]};else if(s.startsWith(f_))a(),r={kind:"update",path:s.slice(f_.length).trim(),oldLines:[],newLines:[]};else if(s.startsWith(h_))a(),r={kind:"delete",path:s.slice(h_.length).trim(),oldLines:[],newLines:[]};else if((r==null?void 0:r.kind)==="update"){if(s.startsWith("@@"))continue;s.startsWith("-")&&!s.startsWith("---")?r.oldLines.push(s.slice(1)):s.startsWith("+")&&!s.startsWith("+++")&&r.newLines.push(s.slice(1))}else(r==null?void 0:r.kind)==="add"&&(s.startsWith("+")?r.newLines.push(s.slice(1)):s.trim()&&r.newLines.push(s));return a(),t}function S7({op:e}){const t=w7[e.kind]??"file",r=e.oldLines.length+e.newLines.length,a=r>gu,s=a&&r>0?Math.round(gu*(e.oldLines.length/r)):e.oldLines.length,o=a?gu-s:e.newLines.length;return m.jsxs("div",{children:[m.jsxs("div",{className:"flex items-baseline gap-2",children:[m.jsx("span",{className:"text-sky-400/80 font-semibold text-sm shrink-0",children:t}),e.path&&m.jsx("span",{className:"text-[#888] font-mono text-[13px] break-all",children:yg(e.path)})]}),(e.oldLines.length>0||e.newLines.length>0)&&m.jsxs("div",{className:"font-mono text-[13px] leading-relaxed mt-1.5",children:[e.oldLines.slice(0,s).map((c,d)=>m.jsxs("div",{className:"text-red-400/60",children:[m.jsx("span",{className:"select-none text-red-400/30 mr-1",children:"-"}),c]},`o${d}`)),e.newLines.slice(0,o).map((c,d)=>m.jsxs("div",{className:"text-emerald-400/60",children:[m.jsx("span",{className:"select-none text-emerald-400/30 mr-1",children:"+"}),c]},`n${d}`)),a&&m.jsxs("div",{className:"text-[#444] mt-0.5",children:["... ",r-gu," more lines"]})]})]})}function k7({args:e,result:t,status:r}){const a=N7(E7(e));return a.length===0?m.jsxs("div",{children:[m.jsx("span",{className:"text-sky-400/80 font-semibold text-sm",children:"patch"}),r==="failed"&&typeof t=="string"&&t.trim()&&m.jsx("div",{className:"text-red-400/70 text-[13px] mt-1",children:t.trim()})]}):m.jsxs("div",{className:"space-y-2",children:[a.map((s,o)=>m.jsx(S7,{op:s},o)),r==="failed"&&typeof t=="string"&&t.trim()&&m.jsx("div",{className:"text-red-400/70 text-[13px]",children:t.trim()})]})}const C7=/data:image\/(png|jpe?g|gif|webp);base64,([A-Za-z0-9+/]+={0,2})/;function T7(e){let t=null;if(typeof e=="string")t=e;else if(e&&typeof e=="object"){const a=e;typeof a.image_url=="string"?t=a.image_url:typeof a.url=="string"&&(t=a.url)}if(!t)return null;const r=C7.exec(t);return!r||r[2].length<100||r[2].length%4!==0?null:`data:image/${r[1]};base64,${r[2]}`}function A7({args:e,result:t}){const r=(e.path??"").trim(),a=T7(t);let s=null;if(!a&&typeof t=="string"){const o=t.trim();o&&!o.toLowerCase().startsWith("data:image/")&&!o.startsWith("{")&&(s=o)}return m.jsxs("div",{children:[m.jsxs("div",{className:"flex items-baseline gap-2",children:[m.jsx("span",{className:"text-sky-400/80 font-semibold text-sm shrink-0",children:"view image"}),r&&m.jsx("span",{className:"text-[#888] font-mono text-[13px] break-all",children:yg(r)})]}),a&&m.jsx("img",{src:a,alt:r||"Tool image output",className:"mt-1.5 max-w-full max-h-96 rounded-lg border border-white/[0.06] object-contain"}),s&&m.jsx("div",{className:"text-red-400/70 text-[13px] mt-1",children:s})]})}const M7={critical:"text-red-400",high:"text-orange-400",medium:"text-yellow-400",low:"text-blue-400",info:"text-cyan-400"},O7={high:"text-emerald-400",medium:"text-yellow-400",low:"text-orange-400"};function R7({args:e,result:t}){const r=e.title??"",a=e.description??"",s=e.impact??"",o=e.target??"",c=e.endpoint??"",d=e.method??"",h=e.technical_analysis??"",f=e.poc_description??"",{language:p,code:g}=EE(e.poc_script_code??""),b=e.remediation_steps??"",y=e.cve??"",_=e.cwe??"",N=e.counterevidence??"",S=(e.confidence??"").toLowerCase(),w=e.confidence_rationale??"",C=e.severity_change_conditions??"",E=e.fix_verification??"",A=t,B=(A&&typeof A=="object"?A.severity:null)??e.severity??"medium",R=String(B).toLowerCase(),H=(A&&typeof A=="object"?A.cvss_score:null)??e.cvss??null,z=M7[R]??"text-yellow-400";return m.jsxs("div",{className:"space-y-3",children:[m.jsxs("div",{className:"flex items-center gap-2 flex-wrap",children:[m.jsx("span",{className:`font-semibold text-sm ${z}`,children:R.toUpperCase()}),H!=null&&m.jsxs("span",{className:"text-[#888] text-[13px]",children:["CVSS ",H]}),y&&m.jsx("span",{className:"text-[#888] font-mono text-[13px]",children:y}),_&&m.jsx("span",{className:"text-[#888] font-mono text-[13px]",children:_}),S&&m.jsxs("span",{className:`text-[13px] ${O7[S]??"text-[#888]"}`,children:[S," confidence"]})]}),r&&m.jsx("div",{className:"text-[15px] text-white/80 font-semibold",children:r}),(o||c)&&m.jsxs("div",{className:"text-[13px] text-[#888] font-mono",children:[o,c?` ${d} ${c}`:""]}),a&&m.jsx(qt,{text:a,maxLines:20}),s&&m.jsxs("div",{children:[m.jsx("span",{className:"text-emerald-400/60 text-sm font-semibold",children:"Impact"}),m.jsx("div",{className:"mt-1",children:m.jsx(qt,{text:s,maxLines:15})})]}),h&&m.jsxs("div",{children:[m.jsx("span",{className:"text-emerald-400/60 text-sm font-semibold",children:"Technical Analysis"}),m.jsx("div",{className:"mt-1",children:m.jsx(qt,{text:h,maxLines:20})})]}),w&&m.jsx("div",{className:"text-[#777] text-xs leading-snug",children:w}),N&&m.jsxs("div",{children:[m.jsx("span",{className:"text-emerald-400/60 text-sm font-semibold",children:"Counterevidence"}),m.jsx("div",{className:"mt-1",children:m.jsx(qt,{text:N,maxLines:12})})]}),C&&m.jsxs("div",{children:[m.jsx("span",{className:"text-emerald-400/60 text-sm font-semibold",children:"Severity would change if"}),m.jsx("div",{className:"mt-1",children:m.jsx(qt,{text:C,maxLines:10})})]}),(f||g)&&m.jsxs("div",{children:[m.jsx("span",{className:"text-emerald-400/60 text-sm font-semibold",children:"Proof of Concept"}),f&&m.jsx("div",{className:"mt-1",children:m.jsx(da,{text:f})}),g&&m.jsx(wE,{className:p?`language-${p}`:void 0,children:g})]}),b&&m.jsxs("div",{children:[m.jsx("span",{className:"text-emerald-400/60 text-sm font-semibold",children:"Remediation"}),m.jsx("div",{className:"mt-1",children:m.jsx(qt,{text:b,maxLines:15})})]}),E&&m.jsxs("div",{children:[m.jsx("span",{className:"text-emerald-400/60 text-sm font-semibold",children:"Fix verification"}),m.jsx("div",{className:"mt-1",children:m.jsx(qt,{text:E,maxLines:12})})]})]})}const j7={critical:"text-red-400",high:"text-orange-400",medium:"text-yellow-400",low:"text-blue-400",info:"text-cyan-400"};function D7({args:e,result:t,status:r}){const a=e.report_id??"",s=e.delete_reason??"",o=t&&typeof t=="object"?t:null,c=(o==null?void 0:o.success)===!0,d=typeof(o==null?void 0:o.error)=="string"?o.error:"",h=typeof(o==null?void 0:o.title)=="string"?o.title:"",f=typeof(o==null?void 0:o.severity)=="string"?o.severity.toLowerCase():"",p=o!=null?!c:r==="failed"||r==="error";return m.jsxs("div",{className:"space-y-2",children:[m.jsxs("div",{className:"flex items-center gap-2 flex-wrap",children:[m.jsx("span",{className:`font-semibold text-sm ${p?"text-red-400/80":"text-yellow-400/80"}`,children:p?"report not withdrawn":"report withdrawn"}),a&&m.jsx("span",{className:"text-[#555] font-mono text-[13px]",children:a}),f&&m.jsx("span",{className:`text-[13px] line-through ${j7[f]??"text-[#888]"}`,children:f.toUpperCase()})]}),h&&m.jsx("div",{className:"text-[15px] text-white/60 line-through",children:h}),s&&m.jsxs("div",{children:[m.jsx("span",{className:"text-emerald-400/60 text-sm font-semibold",children:"Why"}),m.jsx("div",{className:"mt-1",children:m.jsx(qt,{text:s,maxLines:10})})]}),d&&m.jsx("div",{className:"text-red-400/80 text-[13px]",children:d})]})}const L7={critical:"text-red-400",high:"text-orange-400",medium:"text-yellow-400",low:"text-blue-400",info:"text-cyan-400",none:"text-[#888]"};function z7(e){if(!e.agent_name&&!e.by_you)return null;const t=e.by_you?"you":e.agent_name;return m.jsxs("span",{className:"text-[#666] text-xs ml-1.5",children:["(",t,")"]})}function Bm(e){const t=String(e??"").toLowerCase(),r=L7[t]??"text-yellow-400";return m.jsx("span",{className:`font-semibold text-[13px] ${r}`,children:t.toUpperCase()||"—"})}function m_({toolName:e,result:t}){const r=t,a=r!=null&&typeof r=="object"&&r.success===!0;if(e==="get_report"){const f=a?r.report:void 0;return m.jsxs("div",{children:[m.jsx("span",{className:"text-red-400/80 font-semibold text-sm",children:"report"}),f?m.jsxs("div",{className:"mt-1.5 space-y-2",children:[m.jsxs("div",{className:"flex items-center gap-2 flex-wrap",children:[Bm(f.severity),f.cvss!=null&&m.jsxs("span",{className:"text-[#888] text-[13px]",children:["CVSS ",f.cvss]}),f.id&&m.jsx("span",{className:"text-[#555] font-mono text-[13px]",children:f.id}),f.cve&&m.jsx("span",{className:"text-[#888] font-mono text-[13px]",children:f.cve}),f.cwe&&m.jsx("span",{className:"text-[#888] font-mono text-[13px]",children:f.cwe}),(f.agent_name||f.by_you)&&m.jsx("span",{className:"text-[#666] text-[13px]",children:f.by_you?"you":f.agent_name})]}),f.title&&m.jsx("div",{className:"text-[15px] text-white/80 font-semibold",children:f.title}),(f.target||f.endpoint)&&m.jsxs("div",{className:"text-[13px] text-[#888] font-mono",children:[f.target,f.endpoint?` ${f.method??""} ${f.endpoint}`:""]}),f.description&&m.jsx(qt,{text:f.description,maxLines:20})]}):m.jsx("div",{className:"mt-1 text-[#555] text-xs",children:r&&typeof r=="object"&&r.error||"Report not found"})]})}const s=a?r.reports:null,o=Array.isArray(s)?s:[],c=a&&typeof r.total_count=="number"?r.total_count:o.length,d=a&&r.severity_counts&&typeof r.severity_counts=="object"?r.severity_counts:{},h=Object.entries(d);return m.jsxs("div",{children:[m.jsxs("div",{className:"flex items-center gap-2 flex-wrap",children:[m.jsx("span",{className:"text-red-400/80 font-semibold text-sm",children:"reports"}),m.jsxs("span",{className:"text-[#555] text-[13px]",children:["(",c,")"]}),h.map(([f,p])=>m.jsxs("span",{className:"text-[13px]",children:[Bm(f),m.jsx("span",{className:"text-[#888] ml-0.5",children:p})]},f))]}),o.length>0?m.jsx("div",{className:"mt-1.5 space-y-1",children:o.map((f,p)=>m.jsxs("div",{className:"text-[13px]",children:[m.jsx("span",{className:"text-[#555] mr-1",children:"-"}),Bm(f.severity),f.id&&m.jsx("span",{className:"text-[#555] font-mono ml-1.5",children:f.id}),m.jsx("span",{className:"text-[#999] ml-1.5",children:f.title??"(untitled)"}),z7(f),(f.target||f.endpoint)&&m.jsxs("div",{className:"ml-3 text-[#666] font-mono text-xs",children:[f.target,f.endpoint?` ${f.method??""} ${f.endpoint}`:""]}),f.description_preview&&m.jsx("div",{className:"ml-3",children:m.jsx(da,{text:f.description_preview})})]},f.id??p))}):m.jsx("div",{className:"mt-1 text-[#555] text-xs",children:"No reports filed yet"})]})}const p2=200,g2={GET:"text-emerald-400/80",POST:"text-blue-400/80",PUT:"text-yellow-400/80",PATCH:"text-orange-400/80",DELETE:"text-red-400/80"};function vg(e){return e<300?"text-emerald-400/80":e<400?"text-yellow-400/80":e<500?"text-orange-400/80":"text-red-400/80"}function Xr(e,t=80){return e.length>t?e.slice(0,t-3)+"...":e}function _p(e,t=150){return Xr(e.replace(/\n/g," ").replace(/\r/g,"").replace(/\t/g," "),t)}function wp(e,t){const r=e.split(`
+`))if(!(s===v7||s===_7))if(s.startsWith(d_))a(),r={kind:"add",path:s.slice(d_.length).trim(),oldLines:[],newLines:[]};else if(s.startsWith(f_))a(),r={kind:"update",path:s.slice(f_.length).trim(),oldLines:[],newLines:[]};else if(s.startsWith(h_))a(),r={kind:"delete",path:s.slice(h_.length).trim(),oldLines:[],newLines:[]};else if((r==null?void 0:r.kind)==="update"){if(s.startsWith("@@"))continue;s.startsWith("-")&&!s.startsWith("---")?r.oldLines.push(s.slice(1)):s.startsWith("+")&&!s.startsWith("+++")&&r.newLines.push(s.slice(1))}else(r==null?void 0:r.kind)==="add"&&(s.startsWith("+")?r.newLines.push(s.slice(1)):s.trim()&&r.newLines.push(s));return a(),t}function S7({op:e}){const t=w7[e.kind]??"file",r=e.oldLines.length+e.newLines.length,a=r>gu,s=a&&r>0?Math.round(gu*(e.oldLines.length/r)):e.oldLines.length,o=a?gu-s:e.newLines.length;return m.jsxs("div",{children:[m.jsxs("div",{className:"flex items-baseline gap-2",children:[m.jsx("span",{className:"text-sky-400/80 font-semibold text-sm shrink-0",children:t}),e.path&&m.jsx("span",{className:"text-[#888] font-mono text-[13px] break-all",children:yg(e.path)})]}),(e.oldLines.length>0||e.newLines.length>0)&&m.jsxs("div",{className:"font-mono text-[13px] leading-relaxed mt-1.5",children:[e.oldLines.slice(0,s).map((c,d)=>m.jsxs("div",{className:"text-red-400/60",children:[m.jsx("span",{className:"select-none text-red-400/30 mr-1",children:"-"}),c]},`o${d}`)),e.newLines.slice(0,o).map((c,d)=>m.jsxs("div",{className:"text-emerald-400/60",children:[m.jsx("span",{className:"select-none text-emerald-400/30 mr-1",children:"+"}),c]},`n${d}`)),a&&m.jsxs("div",{className:"text-[#444] mt-0.5",children:["... ",r-gu," more lines"]})]})]})}function k7({args:e,result:t,status:r}){const a=N7(E7(e));return a.length===0?m.jsxs("div",{children:[m.jsx("span",{className:"text-sky-400/80 font-semibold text-sm",children:"patch"}),r==="failed"&&typeof t=="string"&&t.trim()&&m.jsx("div",{className:"text-red-400/70 text-[13px] mt-1",children:t.trim()})]}):m.jsxs("div",{className:"space-y-2",children:[a.map((s,o)=>m.jsx(S7,{op:s},o)),r==="failed"&&typeof t=="string"&&t.trim()&&m.jsx("div",{className:"text-red-400/70 text-[13px]",children:t.trim()})]})}const C7=/data:image\/(png|jpe?g|gif|webp);base64,([A-Za-z0-9+/]+={0,2})/;function T7(e){let t=null;if(typeof e=="string")t=e;else if(e&&typeof e=="object"){const a=e;typeof a.image_url=="string"?t=a.image_url:typeof a.url=="string"&&(t=a.url)}if(!t)return null;const r=C7.exec(t);return!r||r[2].length<100||r[2].length%4!==0?null:`data:image/${r[1]};base64,${r[2]}`}function A7({args:e,result:t}){const r=(e.path??"").trim(),a=T7(t);let s=null;if(!a&&typeof t=="string"){const o=t.trim();o&&!o.toLowerCase().startsWith("data:image/")&&!o.startsWith("{")&&(s=o)}return m.jsxs("div",{children:[m.jsxs("div",{className:"flex items-baseline gap-2",children:[m.jsx("span",{className:"text-sky-400/80 font-semibold text-sm shrink-0",children:"view image"}),r&&m.jsx("span",{className:"text-[#888] font-mono text-[13px] break-all",children:yg(r)})]}),a&&m.jsx("img",{src:a,alt:r||"Tool image output",className:"mt-1.5 max-w-full max-h-96 rounded-lg border border-white/[0.06] object-contain"}),s&&m.jsx("div",{className:"text-red-400/70 text-[13px] mt-1",children:s})]})}const M7={critical:"text-red-400",high:"text-orange-400",medium:"text-yellow-400",low:"text-blue-400",info:"text-cyan-400"},O7={high:"text-emerald-400",medium:"text-yellow-400",low:"text-orange-400"};function R7({args:e,result:t}){const r=e.title??"",a=e.description??"",s=e.impact??"",o=e.target??"",c=e.endpoint??"",d=e.method??"",h=e.technical_analysis??"",f=e.poc_description??"",{language:p,code:g}=EE(e.poc_script_code??""),b=e.remediation_steps??"",y=e.cve??"",_=e.cwe??"",N=e.counterevidence??"",S=(e.confidence??"").toLowerCase(),w=e.confidence_rationale??"",C=e.severity_change_conditions??"",E=e.fix_verification??"",A=t,B=(A&&typeof A=="object"?A.severity:null)??e.severity??"medium",R=String(B).toLowerCase(),H=(A&&typeof A=="object"?A.cvss_score:null)??e.cvss??null,z=M7[R]??"text-yellow-400";return m.jsxs("div",{className:"space-y-3",children:[m.jsxs("div",{className:"flex items-center gap-2 flex-wrap",children:[m.jsx("span",{className:`font-semibold text-sm ${z}`,children:R.toUpperCase()}),H!=null&&m.jsxs("span",{className:"text-[#888] text-[13px]",children:["CVSS ",H]}),y&&m.jsx("span",{className:"text-[#888] font-mono text-[13px]",children:y}),_&&m.jsx("span",{className:"text-[#888] font-mono text-[13px]",children:_}),S&&m.jsxs("span",{className:`text-[13px] ${O7[S]??"text-[#888]"}`,children:[S," confidence"]})]}),r&&m.jsx("div",{className:"text-[15px] text-white/80 font-semibold",children:r}),(o||c)&&m.jsxs("div",{className:"text-[13px] text-[#888] font-mono",children:[o,c?` ${d} ${c}`:""]}),a&&m.jsx(qt,{text:a,maxLines:20}),s&&m.jsxs("div",{children:[m.jsx("span",{className:"text-emerald-400/60 text-sm font-semibold",children:"Impact"}),m.jsx("div",{className:"mt-1",children:m.jsx(qt,{text:s,maxLines:15})})]}),h&&m.jsxs("div",{children:[m.jsx("span",{className:"text-emerald-400/60 text-sm font-semibold",children:"Technical Analysis"}),m.jsx("div",{className:"mt-1",children:m.jsx(qt,{text:h,maxLines:20})})]}),w&&m.jsx("div",{className:"text-[#777] text-xs leading-snug",children:w}),N&&m.jsxs("div",{children:[m.jsx("span",{className:"text-emerald-400/60 text-sm font-semibold",children:"Counterevidence"}),m.jsx("div",{className:"mt-1",children:m.jsx(qt,{text:N,maxLines:12})})]}),C&&m.jsxs("div",{children:[m.jsx("span",{className:"text-emerald-400/60 text-sm font-semibold",children:"Severity would change if"}),m.jsx("div",{className:"mt-1",children:m.jsx(qt,{text:C,maxLines:10})})]}),(f||g)&&m.jsxs("div",{children:[m.jsx("span",{className:"text-emerald-400/60 text-sm font-semibold",children:"Proof of Concept"}),f&&m.jsx("div",{className:"mt-1",children:m.jsx(da,{text:f})}),g&&m.jsx(wE,{className:p?`language-${p}`:void 0,children:g})]}),b&&m.jsxs("div",{children:[m.jsx("span",{className:"text-emerald-400/60 text-sm font-semibold",children:"Remediation"}),m.jsx("div",{className:"mt-1",children:m.jsx(qt,{text:b,maxLines:15})})]}),E&&m.jsxs("div",{children:[m.jsx("span",{className:"text-emerald-400/60 text-sm font-semibold",children:"Fix verification"}),m.jsx("div",{className:"mt-1",children:m.jsx(qt,{text:E,maxLines:12})})]})]})}const j7={critical:"text-red-400",high:"text-orange-400",medium:"text-yellow-400",low:"text-blue-400",info:"text-cyan-400"};function D7({args:e,result:t,status:r}){const a=e.report_id??"",s=e.delete_reason??"",o=t&&typeof t=="object"?t:null,c=(o==null?void 0:o.success)===!0,d=typeof(o==null?void 0:o.error)=="string"?o.error:"",h=typeof(o==null?void 0:o.title)=="string"?o.title:"",f=typeof(o==null?void 0:o.severity)=="string"?o.severity.toLowerCase():"",p=o!=null?!c:r==="failed"||r==="error",g=o==null&&!p,b=g?"withdrawing report…":p?"report not withdrawn":"report withdrawn",y=g?"text-[#888]":p?"text-red-400/80":"text-yellow-400/80";return m.jsxs("div",{className:"space-y-2",children:[m.jsxs("div",{className:"flex items-center gap-2 flex-wrap",children:[m.jsx("span",{className:`font-semibold text-sm ${y}`,"aria-live":"polite",children:b}),a&&m.jsx("span",{className:"text-[#555] font-mono text-[13px]",children:a}),f&&m.jsx("span",{className:`text-[13px] line-through ${j7[f]??"text-[#888]"}`,children:f.toUpperCase()})]}),h&&m.jsx("div",{className:"text-[15px] text-white/60 line-through",children:h}),s&&m.jsxs("div",{children:[m.jsx("span",{className:"text-emerald-400/60 text-sm font-semibold",children:"Why"}),m.jsx("div",{className:"mt-1",children:m.jsx(qt,{text:s,maxLines:10})})]}),d&&m.jsx("div",{className:"text-red-400/80 text-[13px]",children:d})]})}const L7={critical:"text-red-400",high:"text-orange-400",medium:"text-yellow-400",low:"text-blue-400",info:"text-cyan-400",none:"text-[#888]"};function z7(e){if(!e.agent_name&&!e.by_you)return null;const t=e.by_you?"you":e.agent_name;return m.jsxs("span",{className:"text-[#666] text-xs ml-1.5",children:["(",t,")"]})}function Bm(e){const t=String(e??"").toLowerCase(),r=L7[t]??"text-yellow-400";return m.jsx("span",{className:`font-semibold text-[13px] ${r}`,children:t.toUpperCase()||"—"})}function m_({toolName:e,result:t}){const r=t,a=r!=null&&typeof r=="object"&&r.success===!0;if(e==="get_report"){const f=a?r.report:void 0;return m.jsxs("div",{children:[m.jsx("span",{className:"text-red-400/80 font-semibold text-sm",children:"report"}),f?m.jsxs("div",{className:"mt-1.5 space-y-2",children:[m.jsxs("div",{className:"flex items-center gap-2 flex-wrap",children:[Bm(f.severity),f.cvss!=null&&m.jsxs("span",{className:"text-[#888] text-[13px]",children:["CVSS ",f.cvss]}),f.id&&m.jsx("span",{className:"text-[#555] font-mono text-[13px]",children:f.id}),f.cve&&m.jsx("span",{className:"text-[#888] font-mono text-[13px]",children:f.cve}),f.cwe&&m.jsx("span",{className:"text-[#888] font-mono text-[13px]",children:f.cwe}),(f.agent_name||f.by_you)&&m.jsx("span",{className:"text-[#666] text-[13px]",children:f.by_you?"you":f.agent_name})]}),f.title&&m.jsx("div",{className:"text-[15px] text-white/80 font-semibold",children:f.title}),(f.target||f.endpoint)&&m.jsxs("div",{className:"text-[13px] text-[#888] font-mono",children:[f.target,f.endpoint?` ${f.method??""} ${f.endpoint}`:""]}),f.description&&m.jsx(qt,{text:f.description,maxLines:20})]}):m.jsx("div",{className:"mt-1 text-[#555] text-xs",children:r&&typeof r=="object"&&r.error||"Report not found"})]})}const s=a?r.reports:null,o=Array.isArray(s)?s:[],c=a&&typeof r.total_count=="number"?r.total_count:o.length,d=a&&r.severity_counts&&typeof r.severity_counts=="object"?r.severity_counts:{},h=Object.entries(d);return m.jsxs("div",{children:[m.jsxs("div",{className:"flex items-center gap-2 flex-wrap",children:[m.jsx("span",{className:"text-red-400/80 font-semibold text-sm",children:"reports"}),m.jsxs("span",{className:"text-[#555] text-[13px]",children:["(",c,")"]}),h.map(([f,p])=>m.jsxs("span",{className:"text-[13px]",children:[Bm(f),m.jsx("span",{className:"text-[#888] ml-0.5",children:p})]},f))]}),o.length>0?m.jsx("div",{className:"mt-1.5 space-y-1",children:o.map((f,p)=>m.jsxs("div",{className:"text-[13px]",children:[m.jsx("span",{className:"text-[#555] mr-1",children:"-"}),Bm(f.severity),f.id&&m.jsx("span",{className:"text-[#555] font-mono ml-1.5",children:f.id}),m.jsx("span",{className:"text-[#999] ml-1.5",children:f.title??"(untitled)"}),z7(f),(f.target||f.endpoint)&&m.jsxs("div",{className:"ml-3 text-[#666] font-mono text-xs",children:[f.target,f.endpoint?` ${f.method??""} ${f.endpoint}`:""]}),f.description_preview&&m.jsx("div",{className:"ml-3",children:m.jsx(da,{text:f.description_preview})})]},f.id??p))}):m.jsx("div",{className:"mt-1 text-[#555] text-xs",children:"No reports filed yet"})]})}const p2=200,g2={GET:"text-emerald-400/80",POST:"text-blue-400/80",PUT:"text-yellow-400/80",PATCH:"text-orange-400/80",DELETE:"text-red-400/80"};function vg(e){return e<300?"text-emerald-400/80":e<400?"text-yellow-400/80":e<500?"text-orange-400/80":"text-red-400/80"}function Xr(e,t=80){return e.length>t?e.slice(0,t-3)+"...":e}function _p(e,t=150){return Xr(e.replace(/\n/g," ").replace(/\r/g,"").replace(/\t/g," "),t)}function wp(e,t){const r=e.split(`
`),a=r.slice(0,t).map(s=>Xr(s,p2-5)).join(`
`);return r.length>t?a+`
...`:a}function I7({args:e,result:t}){const r=e.httpql_filter??"",a=t,s=a?a.requests:null,o=Array.isArray(s)?s:[];return m.jsxs("div",{children:[m.jsxs("div",{className:"flex items-center gap-2",children:[m.jsx("span",{className:"text-purple-400/80 font-semibold text-sm",children:"listing requests"}),r&&m.jsx("span",{className:"text-[#888] text-[13px]",children:Xr(r,150)})]}),o.length>0&&m.jsxs("div",{className:"mt-1.5 font-mono text-[13px] space-y-0.5",children:[o.slice(0,20).map((c,d)=>{const h=(c.method??"GET").toUpperCase(),f=c.host??"",p=c.path??"",g=c.response,b=(g==null?void 0:g.statusCode)??null;return m.jsxs("div",{className:"flex gap-2",children:[m.jsx("span",{className:`w-10 shrink-0 font-bold ${g2[h]??"text-[#888]"}`,children:h}),m.jsx("span",{className:"text-[#777] truncate",children:Xr(f+p,180)}),b!=null&&m.jsx("span",{className:`ml-auto shrink-0 ${vg(b)}`,children:b})]},d)}),o.length>20&&m.jsxs("div",{className:"text-[#555]",children:["... +",o.length-20," more"]})]})]})}function B7({args:e,result:t}){const r=e.request_id,a=e.part??"request",s=e.search_pattern??"",o=t,c=o?o.matches:null,d=Array.isArray(c)?c:[],h=o?o.content??null:null,f=o?!!o.has_more:!1;return m.jsxs("div",{children:[m.jsxs("div",{className:"flex items-center gap-2",children:[m.jsxs("span",{className:"text-purple-400/80 font-semibold text-sm",children:[s?"searching":"viewing"," ",a]}),r!=null&&m.jsxs("span",{className:"text-[#888] text-[13px]",children:["#",r]}),s&&m.jsxs("span",{className:"text-[#666] font-mono text-[13px]",children:["/",Xr(s,100),"/"]})]}),d.length>0&&m.jsxs("div",{className:"mt-1.5 font-mono text-[13px] space-y-1",children:[d.slice(0,5).map((p,g)=>{const b=(p.before??"").replace(/\n/g," ").replace(/\r/g,"").slice(-100),y=(p.after??"").replace(/\n/g," ").replace(/\r/g,"").slice(0,100);return m.jsxs("div",{children:[b&&m.jsxs("span",{className:"text-[#555]",children:["...",b]}),m.jsx("span",{className:"text-amber-400/80 font-bold",children:p.match}),y&&m.jsxs("span",{className:"text-[#555]",children:[y,"..."]})]},g)}),d.length>5&&m.jsxs("div",{className:"text-[#555]",children:["... +",d.length-5," more matches"]})]}),h&&!d.length&&(()=>{const p=h.split(`
diff --git a/strix/interface/viewer/static/index.html b/strix/interface/viewer/static/index.html
index f677077d..c167f186 100644
--- a/strix/interface/viewer/static/index.html
+++ b/strix/interface/viewer/static/index.html
@@ -6,7 +6,7 @@
Strix Results
-
+
diff --git a/strix/report/state.py b/strix/report/state.py
index bd86b9b8..5d13483e 100644
--- a/strix/report/state.py
+++ b/strix/report/state.py
@@ -36,16 +36,20 @@ _global_report_state: Optional["ReportState"] = None
_CONTROL_CHARS = re.compile(r"[\x00-\x1f\x7f]+")
-class ReportOwnershipError(PermissionError):
- """Raised when an agent tries to withdraw a report another agent filed."""
+class ReportRollbackError(RuntimeError):
+ """Raised when a failed deletion could not rewrite the on-disk indexes.
- def __init__(self, report_id: str, *, filed_by: str, caller: str | None) -> None:
+ The report is still on file in memory; the indexes are rewritten from
+ memory on the next save.
+ """
+
+ def __init__(self, report_id: str, *, cause: BaseException) -> None:
self.report_id = report_id
- self.filed_by = filed_by
- self.caller = caller
+ self.cause = cause
super().__init__(
- f"Report '{report_id}' was filed by agent {filed_by}, not by "
- f"{caller or 'the caller'}; only the agent that filed a report can withdraw it"
+ f"Deletion of report '{report_id}' failed ({cause}) and the on-disk indexes "
+ "could not be restored; the report is still on file and the indexes are "
+ "rewritten on the next save"
)
@@ -558,27 +562,23 @@ class ReportState:
) -> dict[str, Any] | None:
"""Withdraw a report from the run, keeping a record of the withdrawal.
- Returns the removed report, or ``None`` when the id is unknown. Only the
- agent that filed a report may withdraw it (:class:`ReportOwnershipError`
- otherwise). The report leaves ``vulnerability_reports`` and its rendered
- artifacts; the run record keeps who withdrew it and why, so the id is
+ Returns the removed report, or ``None`` when the id is unknown. Every
+ agent in a run shares one trust boundary, so any of them may withdraw
+ any report (just as any of them may revise one); the run record keeps
+ who filed it, who withdrew it and why. The report leaves
+ ``vulnerability_reports`` and its rendered artifacts, and its id is
never reissued.
The rewritten artifacts and the ``vulnerability_deleted_callback`` must
both accept the deletion first: if either fails the report is put back
- and the error propagates, so the deletion can be retried.
+ and the error propagates, so the deletion can be retried
+ (:class:`ReportRollbackError` when the indexes could not be put back).
"""
report = next((r for r in self.vulnerability_reports if r.get("id") == report_id), None)
if report is None:
logger.warning("cannot delete unknown vulnerability report %s", report_id)
return None
- filed_by = report.get("agent_id")
- if filed_by and deleted_by_agent_id != filed_by:
- raise ReportOwnershipError(
- report_id, filed_by=str(filed_by), caller=deleted_by_agent_id
- )
-
entry: dict[str, Any] = {
"id": report_id,
"title": report.get("title"),
@@ -608,7 +608,7 @@ class ReportState:
self._write_artifacts()
if self.vulnerability_deleted_callback:
self.vulnerability_deleted_callback({**report, "deletion": entry})
- except Exception:
+ except Exception as exc:
self.vulnerability_reports.insert(position, report)
if was_saved:
self._saved_vuln_ids.add(report_id)
@@ -616,7 +616,13 @@ class ReportState:
self.run_record["deleted_vulnerability_reports"] = history
else:
self.run_record.pop("deleted_vulnerability_reports", None)
- self._save_artifacts()
+ try:
+ self._write_artifacts()
+ except Exception as rollback_exc:
+ logger.exception(
+ "could not restore artifacts after failed deletion of %s", report_id
+ )
+ raise ReportRollbackError(report_id, cause=exc) from rollback_exc
raise
md_path = self.get_run_dir() / "vulnerabilities" / f"{report_id}.md"
diff --git a/strix/tools/reporting/tool.py b/strix/tools/reporting/tool.py
index 7ac898f3..45f4f658 100644
--- a/strix/tools/reporting/tool.py
+++ b/strix/tools/reporting/tool.py
@@ -720,7 +720,7 @@ def _do_delete(
),
}
- from strix.report.state import ReportOwnershipError, get_global_report_state
+ from strix.report.state import ReportRollbackError, get_global_report_state
report_state = get_global_report_state()
if report_state is None:
@@ -736,23 +736,18 @@ def _do_delete(
deleted_by_agent_id=agent_id,
deleted_by_agent_name=agent_name,
)
- except ReportOwnershipError as e:
- return {
- "success": False,
- "error": (
- f"{e!s}. If you have disproved another agent's finding, send that agent "
- "your counterevidence so it can withdraw the report itself."
- ),
- "report_id": report_id,
- }
except Exception as e:
logger.exception("delete_vulnerability_report persistence failed")
+ if isinstance(e, ReportRollbackError):
+ outcome = (
+ f"{e.cause!s}. The report is still on file, but its on-disk indexes could "
+ "not be rewritten and may be stale until the next report is saved"
+ )
+ else:
+ outcome = f"{e!s}. The report is still on file"
return {
"success": False,
- "error": (
- f"Failed to delete report '{report_id}': {e!s}. "
- "The report is still on file; retry the deletion."
- ),
+ "error": f"Failed to delete report '{report_id}': {outcome}; retry the deletion.",
"report_id": report_id,
}
if deleted is None:
@@ -1654,11 +1649,10 @@ async def delete_vulnerability_report(
impact and the counterevidence come down together. Do NOT rewrite a
report into a "retracted" or "false positive" note either: delete it.
- You can only withdraw a report you filed; a report filed by another
- agent is refused, so send that agent your counterevidence instead. Call
- ``get_report`` first to read what it claims, then state in
+ Only withdraw a report you have re-tested yourself, whoever filed it.
+ Call ``get_report`` first to read what it claims, then state in
``delete_reason`` exactly what disproved it, so the scan history shows
- why the finding is gone. A withdrawn report cannot be restored; if new
+ who withdrew the finding and why. A withdrawn report cannot be restored; if new
evidence later proves the issue, file it again.
Args:
diff --git a/tests/test_reporting_fields.py b/tests/test_reporting_fields.py
index 72d8ea2e..46d55af3 100644
--- a/tests/test_reporting_fields.py
+++ b/tests/test_reporting_fields.py
@@ -13,7 +13,8 @@ from strix.report.dedupe import (
_prepare_report_for_comparison,
check_duplicate,
)
-from strix.report.state import ReportOwnershipError, ReportState, set_global_report_state
+from strix.report.state import ReportRollbackError, ReportState, set_global_report_state
+from strix.report.writer import write_run_record
from strix.tools.finish.tool import finish_scan
from strix.tools.reporting import tool as reporting_tool
from strix.tools.reporting.tool import (
@@ -2061,61 +2062,28 @@ def _assert_report_still_filed(
assert report_state._next_report_id() == "vuln-0010"
-@pytest.mark.parametrize(
- ("agent_id", "agent_name"),
- [("834f79fb", "Validation Agent"), (None, None)],
- ids=["another agent", "no caller identity"],
-)
-def test_delete_refuses_a_report_the_caller_did_not_file(
- report_state: ReportState, agent_id: str | None, agent_name: str | None
-) -> None:
- """Report ids are sequential and listed to every agent; knowing one is not owning it."""
+def test_any_agent_in_the_run_may_delete_a_report(report_state: ReportState) -> None:
+ """Agents in a run share one trust boundary: a report another agent filed can be
+ withdrawn once disproved, and the history keeps both identities apart."""
run_dir = _seed_saved_report(report_state)
- original = dict(report_state.vulnerability_reports[0])
calls: list[dict[str, Any]] = []
report_state.vulnerability_deleted_callback = calls.append
result = _do_delete(
report_id="vuln-0009",
delete_reason="Disproved.",
- agent_id=agent_id,
- agent_name=agent_name,
+ agent_id="834f79fb",
+ agent_name="Validation Agent",
)
- assert result["success"] is False
- assert "filed by agent aaaa1111" in result["error"]
- assert "send that agent your counterevidence" in result["error"]
- assert calls == [], "a refused deletion never reaches persistence"
- _assert_report_still_filed(report_state, run_dir, original)
-
-
-def test_delete_refuses_a_foreign_report_at_the_state_layer(report_state: ReportState) -> None:
- _seed_saved_report(report_state)
-
- with pytest.raises(ReportOwnershipError) as excinfo:
- report_state.delete_vulnerability_report(
- "vuln-0009", delete_reason="Disproved.", deleted_by_agent_id="834f79fb"
- )
-
- assert excinfo.value.filed_by == "aaaa1111"
- assert excinfo.value.caller == "834f79fb"
- assert len(report_state.vulnerability_reports) == 1
-
-
-def test_delete_allows_a_report_filed_without_an_agent(report_state: ReportState) -> None:
- """A report with no filer on record (host-side or legacy) has no owner to check."""
- _seed_saved_report(report_state)
- del report_state.vulnerability_reports[0]["agent_id"]
-
- deleted = report_state.delete_vulnerability_report(
- "vuln-0009", delete_reason="Disproved.", deleted_by_agent_id="834f79fb"
- )
-
- assert deleted is not None
+ assert result["success"] is True
assert report_state.vulnerability_reports == []
- history = report_state.run_record["deleted_vulnerability_reports"]
- assert history[0]["agent_id"] == "834f79fb"
- assert "filed_by_agent_id" not in history[0]
+ assert [c["id"] for c in calls] == ["vuln-0009"]
+ assert not (run_dir / "vulnerabilities" / "vuln-0009.md").exists()
+ (deletion,) = report_state.run_record["deleted_vulnerability_reports"]
+ assert deletion["filed_by_agent_id"] == "aaaa1111"
+ assert deletion["agent_id"] == "834f79fb"
+ assert deletion["agent_name"] == "Validation Agent"
def test_delete_reports_persistence_failure_and_keeps_the_report(
@@ -2169,6 +2137,45 @@ def test_delete_reports_an_artifact_write_failure_and_keeps_the_report(
_assert_report_still_filed(report_state, run_dir, original)
+def test_delete_reports_when_the_rollback_itself_cannot_be_written(
+ report_state: ReportState, monkeypatch: pytest.MonkeyPatch
+) -> None:
+ """A disk that stays full: memory is restored, the caller is told the indexes
+ may be stale, and the next save rewrites them."""
+ run_dir = _seed_saved_report(report_state)
+ original = dict(report_state.vulnerability_reports[0])
+ calls: list[dict[str, Any]] = []
+ report_state.vulnerability_deleted_callback = calls.append
+ disk_full = True
+
+ def write_run_record_while_disk_full(run_dir_: Path, record: dict[str, Any]) -> None:
+ if disk_full:
+ raise OSError(28, "No space left on device")
+ write_run_record(run_dir_, record)
+
+ monkeypatch.setattr("strix.report.state.write_run_record", write_run_record_while_disk_full)
+
+ with pytest.raises(ReportRollbackError) as excinfo:
+ report_state.delete_vulnerability_report(
+ "vuln-0009", delete_reason="Disproved.", deleted_by_agent_id="aaaa1111"
+ )
+
+ assert isinstance(excinfo.value.cause, OSError)
+ assert calls == []
+ assert report_state.vulnerability_reports == [original]
+ assert "vuln-0009" in report_state._saved_vuln_ids
+ assert "deleted_vulnerability_reports" not in report_state.run_record
+
+ result = _do_delete(report_id="vuln-0009", delete_reason="Disproved.", agent_id="aaaa1111")
+ assert result["success"] is False
+ assert "No space left on device" in result["error"]
+ assert "may be stale" in result["error"]
+
+ disk_full = False
+ report_state.save_run_data()
+ _assert_report_still_filed(report_state, run_dir, original)
+
+
def test_delete_can_be_retried_after_persistence_recovers(report_state: ReportState) -> None:
run_dir = _seed_saved_report(report_state)
outcomes = iter([RuntimeError("persistence failed"), None])