1313from .models import FilterDecision
1414from .models import Finding
1515from .models import ReviewReport
16+ from .models import SandboxRun
1617from .models import utc_now
1718from .redaction import redact_text
1819from .reporting import render_markdown
@@ -98,6 +99,89 @@ def mark_stage(name: str, stage_start: float) -> None:
9899 diff_text = diff .diff_text )
99100 mark_stage ("storage_create_task" , storage_start )
100101
102+ try :
103+ return await _run_review_after_task_created (
104+ start = start ,
105+ stage_durations_ms = stage_durations_ms ,
106+ task_id = task_id ,
107+ created_at = created_at ,
108+ diff = diff ,
109+ input_redactions = input_redactions ,
110+ skill_audit = skill_audit ,
111+ review_store = review_store ,
112+ output_dir = output_dir ,
113+ sandbox = sandbox ,
114+ dry_run = dry_run ,
115+ container_image = container_image ,
116+ docker_path = docker_path ,
117+ docker_base_url = docker_base_url ,
118+ cube_template = cube_template ,
119+ cube_api_url = cube_api_url ,
120+ cube_api_key = cube_api_key ,
121+ cube_sandbox_id = cube_sandbox_id ,
122+ timeout_sec = timeout_sec ,
123+ max_output_bytes = max_output_bytes ,
124+ filter_timeout_budget_sec = filter_timeout_budget_sec ,
125+ filter_max_output_bytes = filter_max_output_bytes ,
126+ network_policy = network_policy ,
127+ test_command = test_command ,
128+ custom_rule_script = custom_rule_script ,
129+ include_network_scanners = include_network_scanners ,
130+ sandbox_runner = sandbox_runner ,
131+ )
132+ except Exception as exc :
133+ redacted = redact_text (str (exc ))
134+ review_store .fail_task (
135+ task_id ,
136+ completed_at = utc_now (),
137+ exception_type = exc .__class__ .__name__ ,
138+ message = redacted .text ,
139+ )
140+ raise
141+
142+
143+ async def _run_review_after_task_created (
144+ * ,
145+ start : float ,
146+ stage_durations_ms : dict [str , int ],
147+ task_id : str ,
148+ created_at : str ,
149+ diff : DiffInput ,
150+ input_redactions : int ,
151+ skill_audit : dict ,
152+ review_store : ReviewStore ,
153+ output_dir : Path ,
154+ sandbox : str ,
155+ dry_run : bool ,
156+ container_image : str ,
157+ docker_path : str | None ,
158+ docker_base_url : str | None ,
159+ cube_template : str | None ,
160+ cube_api_url : str | None ,
161+ cube_api_key : str | None ,
162+ cube_sandbox_id : str | None ,
163+ timeout_sec : float ,
164+ max_output_bytes : int ,
165+ filter_timeout_budget_sec : float ,
166+ filter_max_output_bytes : int ,
167+ network_policy : str ,
168+ test_command : str | None ,
169+ custom_rule_script : str | None ,
170+ include_network_scanners : bool ,
171+ sandbox_runner : SandboxRunner | None ,
172+ ) -> ReviewReport :
173+
174+ def mark_stage (name : str , stage_start : float ) -> None :
175+ stage_durations_ms [name ] = int ((time .monotonic () - stage_start ) * 1000 )
176+
177+ skill_audit ["sdk_skill_runtime" ] = {
178+ "executed" : False ,
179+ "reason" : (
180+ "SDK skill_load/skill_run smoke is available through --skill-smoke; normal reviews do not execute "
181+ "local workspace runtime before Filter approval."
182+ ),
183+ }
184+
101185 filter_start = time .monotonic ()
102186 sandbox_requests , request_build_decisions , request_build_redactions = _build_sandbox_requests_for_review (
103187 timeout_sec = timeout_sec ,
@@ -143,7 +227,7 @@ def mark_stage(name: str, stage_start: float) -> None:
143227 sandbox_runs = []
144228 sandbox_start = time .monotonic ()
145229 for request in allowed_requests :
146- run = await runner . run ( request , diff , skill_dir = SKILL_DIR )
230+ run = await _run_sandbox_request_safely ( runner , request , diff )
147231 sandbox_runs .append (run )
148232 mark_stage ("sandbox" , sandbox_start )
149233 storage_start = time .monotonic ()
@@ -164,7 +248,7 @@ def mark_stage(name: str, stage_start: float) -> None:
164248 needs_human_review ,
165249 )
166250 deduped_finding_count += merged_deduped_count
167- sandbox_redactions = sum (redact_text ( run .stdout ). count + redact_text ( run . stderr ). count for run in sandbox_runs )
251+ sandbox_redactions = sum (run .redaction_count for run in sandbox_runs )
168252 mark_stage ("rules" , rules_start )
169253 storage_start = time .monotonic ()
170254 review_store .save_findings (task_id , "finding" , findings )
@@ -212,6 +296,7 @@ def mark_stage(name: str, stage_start: float) -> None:
212296 "filter_max_output_bytes" : filter_max_output_bytes ,
213297 "env_whitelist" : sorted (ENV_WHITELIST ),
214298 "network_policy" : network_policy ,
299+ "network_enforcement" : _network_enforcement_summary (runner .runtime_name ),
215300 },
216301 filter_policy = filter_policy .audit (),
217302 input = diff .to_dict (),
@@ -242,6 +327,31 @@ def mark_stage(name: str, stage_start: float) -> None:
242327 return report
243328
244329
330+ async def _run_sandbox_request_safely (
331+ runner : SandboxRunner ,
332+ request : SandboxRequest ,
333+ diff : DiffInput ,
334+ ) -> SandboxRun :
335+ start = time .monotonic ()
336+ try :
337+ return await runner .run (request , diff , skill_dir = SKILL_DIR )
338+ except Exception as exc : # pylint: disable=broad-except
339+ redacted = redact_text (str (exc ))
340+ return SandboxRun (
341+ name = request .name ,
342+ runtime = runner .runtime_name ,
343+ command = request .command ,
344+ status = "failed" ,
345+ exit_code = None ,
346+ duration_ms = int ((time .monotonic () - start ) * 1000 ),
347+ stdout = "" ,
348+ stderr = redacted .text [:request .max_output_bytes ],
349+ exception_type = exc .__class__ .__name__ ,
350+ output_truncated = len (redacted .text ) > request .max_output_bytes ,
351+ redaction_count = redacted .count ,
352+ )
353+
354+
245355def query_task (db_path : Path , task_id : str ) -> dict :
246356 return SQLiteReviewStore (db_path ).get_task_bundle (task_id )
247357
@@ -456,6 +566,24 @@ def _build_conclusion(findings, warnings, needs_human_review, sandbox_runs, filt
456566 return "No blocking issues detected by the offline review pipeline."
457567
458568
569+ def _network_enforcement_summary (runtime_name : str ) -> str :
570+ if runtime_name == "container" :
571+ return (
572+ "container host_config sets network_mode=none; network-backed scanners require a separately approved "
573+ "runtime image/policy."
574+ )
575+ if runtime_name == "cube" :
576+ return (
577+ "Filter gates requested network domains; per-run egress enforcement must be provided by the Cube/E2B "
578+ "workspace policy."
579+ )
580+ if runtime_name == "fake" :
581+ return "fake runtime simulates scanner behavior without network access."
582+ if runtime_name == "local" :
583+ return "local runtime is development fallback only and does not enforce network isolation."
584+ return "custom runtime must enforce network isolation according to the active Filter policy."
585+
586+
459587def _dedupe_finding_buckets (
460588 findings : list [Finding ],
461589 warnings : list [Finding ],
@@ -506,14 +634,16 @@ def _findings_from_scanner_runs(sandbox_runs,
506634 needs_human_review : list [Finding ] = []
507635 redactions = 0
508636 for run in sandbox_runs :
509- if run . name != "scanner_probe" or not run .stdout .strip ().startswith ("{" ):
637+ if not run .stdout .strip ().startswith ("{" ):
510638 continue
511639 try :
512640 import json
513641
514642 payload = json .loads (run .stdout )
515643 except Exception :
516644 continue
645+ if "scanner_runs" not in payload :
646+ continue
517647 for scanner_run in payload .get ("scanner_runs" , []):
518648 for item in scanner_run .get ("findings" , []):
519649 evidence = str (item .get ("evidence" , "" ))
0 commit comments