From d6f4bc5ced7118b713dcf280bd133cb76513f5a7 Mon Sep 17 00:00:00 2001 From: ian-at-strix Date: Mon, 26 Oct 2026 15:49:51 -0400 Subject: [PATCH] feat(runner): let root_instructions_override replace the root's scan prompt (#1592) --- strix/core/runner.py | 7 +++++++ tests/test_runner_root_prompt.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/strix/core/runner.py b/strix/core/runner.py index ddace4ea1..e44c4b0e8 100644 --- a/strix/core/runner.py +++ b/strix/core/runner.py @@ -181,9 +181,12 @@ def _compose_root_instructions_override( supports_images: bool, report_tool: str, coverage_tools: bool, + replace_scan_prompt: bool, ) -> str | None: if root_instructions_override is None: return None + if replace_scan_prompt: + return f"{root_instructions_override}\n\n{render_scope_prompt(system_prompt_context)}" base_instructions = render_system_prompt( skills=skills, @@ -225,6 +228,7 @@ async def run_strix_scan( cleanup_on_exit: bool = True, event_sink: StreamEventSink | None = None, root_instructions_override: str | None = None, + replace_root_scan_prompt: bool = False, extra_system_prompt_context: dict[str, Any] | None = None, status_sink: StatusSink | None = None, mcp_connection_requests: list[McpConnectionRequest] | None = None, @@ -238,6 +242,8 @@ async def run_strix_scan( ``root_instructions_override`` adds root scan instructions to the rendered root prompt without replacing the system-verified scope block. + ``replace_root_scan_prompt=True`` makes it the root's whole prompt, followed + by the scope, instead of adding it to the rendered scan prompt. ``extra_files`` entries (``{"workspace_path", "content"}``) are placed into the sandbox workspace at session bring-up; see :func:`strix.runtime.session_manager.create_or_reuse`. @@ -521,6 +527,7 @@ async def run_strix_scan( supports_images=supports_images, report_tool=report_tool.name, coverage_tools=coverage_tools, + replace_scan_prompt=replace_root_scan_prompt, ) root_agent = build_strix_agent( diff --git a/tests/test_runner_root_prompt.py b/tests/test_runner_root_prompt.py index a4942ebc8..6840a3359 100644 --- a/tests/test_runner_root_prompt.py +++ b/tests/test_runner_root_prompt.py @@ -68,6 +68,7 @@ def _patch_engine_scaffold( extra_body=None, ), runtime=types.SimpleNamespace(max_context_images=3), + telemetry=types.SimpleNamespace(run_record_timings=False), ) monkeypatch.setattr(runner, "load_settings", lambda: settings) monkeypatch.setattr(runner, "configure_sdk_model_defaults", lambda _settings: None) @@ -161,6 +162,36 @@ async def test_root_prompt_options_flow_into_root_agent( } +@pytest.mark.asyncio +async def test_replace_root_scan_prompt_drops_the_scan_prompt( + monkeypatch: pytest.MonkeyPatch, + tmp_path: Any, +) -> None: + scope_context = { + "scope_source": "user_scan_config", + "authorized_targets": [ + {"type": "web_application", "value": "https://example.com", "workspace_path": ""}, + ], + } + captured = _patch_engine_scaffold(monkeypatch, tmp_path, scope_context) + + await runner.run_strix_scan( + scan_config={"targets": [], "scan_mode": "deep"}, + scan_id="scan-replace", + image="img", + coordinator=AgentCoordinator(), + root_instructions_override="CUSTOM SCAN PROMPT", + replace_root_scan_prompt=True, + ) + + instructions_override = captured["kwargs"]["instructions_override"] + assert instructions_override.startswith("CUSTOM SCAN PROMPT") + assert instructions_override.count("SCOPE:") == 1 + assert "https://example.com" in instructions_override + assert "" not in instructions_override + assert "application security validation agent" not in instructions_override + + @pytest.mark.asyncio async def test_extra_system_prompt_context_cannot_override_scope_context( monkeypatch: pytest.MonkeyPatch,