fix(scope): render configured URLs as hosts

This commit is contained in:
oyasumi 2026-08-19 00:44:25 +00:00
parent ee82875ea6
commit 7b41840e4d
5 changed files with 76 additions and 14 deletions

View file

@ -76,7 +76,13 @@ SYSTEM-VERIFIED SCOPE:
AUTHORIZED TARGETS:
{% for target in system_prompt_context.authorized_targets %}
{% if target.type == "web_host" %}
- web_host: {{ target.value }} (includes {{ target.value }} and *.{{ target.value }})
{% elif target.type == "ip_address" %}
- ip_address: {{ target.value }} (exact address)
{% else %}
- {{ target.type }}: {{ target.value }}{% if target.workspace_path %} (workspace: {{ target.workspace_path }}){% endif %}
{% endif %}
{% endfor %}
{% endif %}

View file

@ -2,8 +2,10 @@
from __future__ import annotations
import ipaddress
import json
from typing import TYPE_CHECKING, Any
from urllib.parse import urlsplit
from agents.model_settings import ModelSettings
from openai.types.shared import Reasoning
@ -206,8 +208,30 @@ def build_root_task(scan_config: dict[str, Any]) -> str:
)
def _network_scope_target(value: str) -> tuple[str, str]:
"""Reduce a web URL to its hostname-level prompt scope."""
hostname = (urlsplit(value).hostname or "").rstrip(".").lower()
if not hostname:
return "web_application", value
try:
ipaddress.ip_address(hostname)
except ValueError:
return "web_host", hostname
return "ip_address", hostname
def build_scope_context(scan_config: dict[str, Any]) -> dict[str, Any]:
authorized: list[dict[str, str]] = []
authorized_keys: set[tuple[str, str, str]] = set()
def add_authorized(ttype: str, value: str, workspace_path: str = "") -> None:
key = (ttype, value, workspace_path)
if key not in authorized_keys:
authorized.append(
{"type": ttype, "value": value, "workspace_path": workspace_path},
)
authorized_keys.add(key)
value_keys = {
"repository": "target_repo",
"local_code": "target_path",
@ -223,17 +247,18 @@ def build_scope_context(scan_config: dict[str, Any]) -> dict[str, Any]:
workspace_subdir = details.get("workspace_subdir")
workspace_path = f"/workspace/{workspace_subdir}" if workspace_subdir else ""
authorized.append(
{"type": ttype, "value": value, "workspace_path": workspace_path},
)
if ttype == "web_application":
scope_type, scope_value = _network_scope_target(str(value or ""))
add_authorized(scope_type, scope_value)
else:
add_authorized(str(ttype), str(value or ""), workspace_path)
# An API spec authorizes the hosts it declares as in-scope web targets
# so the agent can exercise every endpoint without expanding scope.
if ttype == "api_spec":
authorized.extend(
{"type": "web_application", "value": base_url, "workspace_path": ""}
for base_url in details.get("base_urls") or []
)
for base_url in details.get("base_urls") or []:
scope_type, scope_value = _network_scope_target(str(base_url))
add_authorized(scope_type, scope_value)
return {
"scope_source": "system_scan_config",

View file

@ -148,5 +148,5 @@ def test_build_scope_context_authorizes_base_urls(tmp_path: Path) -> None:
types = {a["type"] for a in authorized}
assert "api_spec" in types
assert "web_application" in types
assert any(a["value"] == "https://api.shop.test/v1" for a in authorized)
assert "web_host" in types
assert any(a["value"] == "api.shop.test" for a in authorized)

View file

@ -246,7 +246,11 @@ def test_scope_prompt_authorizes_flag_and_instruction_hosts_with_subdomains() ->
{
"type": "web_application",
"details": {"target_url": "https://app.example.com/search?q=test"},
}
},
{
"type": "web_application",
"details": {"target_url": "https://app.example.com/blog/"},
},
],
"user_instructions": "Also test https://api.example.net/v1.",
}
@ -256,7 +260,13 @@ def test_scope_prompt_authorizes_flag_and_instruction_hosts_with_subdomains() ->
task = build_root_task(config)
assert "SYSTEM-VERIFIED SCOPE" in prompt
assert "https://app.example.com/search?q=test" in prompt
assert context["authorized_targets"] == [
{"type": "web_host", "value": "app.example.com", "workspace_path": ""}
]
assert "web_host: app.example.com (includes app.example.com and *.app.example.com)" in prompt
assert prompt.count("web_host: app.example.com") == 1
assert "https://app.example.com/search?q=test" not in prompt
assert "https://app.example.com/search?q=test" in task
assert "https://api.example.net/v1" in task
assert "Every network host explicitly named in the user's root scan task" in prompt
assert "exact hostname and all of its descendant subdomains" in prompt
@ -264,6 +274,27 @@ def test_scope_prompt_authorizes_flag_and_instruction_hosts_with_subdomains() ->
assert "not `example.com`, sibling hosts such as `api.example.com`" in prompt
def test_scope_prompt_keeps_web_ip_targets_exact() -> None:
context = build_scope_context(
{
"targets": [
{
"type": "web_application",
"details": {"target_url": "https://192.0.2.10:8443/admin"},
}
]
}
)
prompt = render_system_prompt(scan_mode="quick", is_root=True, system_prompt_context=context)
assert context["authorized_targets"] == [
{"type": "ip_address", "value": "192.0.2.10", "workspace_path": ""}
]
assert "ip_address: 192.0.2.10 (exact address)" in prompt
assert "https://192.0.2.10:8443/admin" not in prompt
def test_scope_prompt_does_not_make_repository_origin_a_live_target() -> None:
context = build_scope_context(
{

View file

@ -106,8 +106,8 @@ async def test_root_prompt_options_flow_into_root_agent(
"authorization_source": "strix_platform_verified_targets",
"authorized_targets": [
{
"type": "web_application",
"value": "https://example.com",
"type": "web_host",
"value": "example.com",
"workspace_path": "",
},
],
@ -128,7 +128,7 @@ async def test_root_prompt_options_flow_into_root_agent(
instructions_override = kwargs["instructions_override"]
assert "SYSTEM-VERIFIED SCOPE" in instructions_override
assert "AUTHORIZED TARGETS" in instructions_override
assert "https://example.com" in instructions_override
assert "web_host: example.com (includes example.com and *.example.com)" in instructions_override
assert "exact hostname and all of its descendant subdomains" in instructions_override
assert "CUSTOM SCAN PROMPT" in instructions_override
assert "Network hosts explicitly named in these root scan instructions" in instructions_override