feat: support API specs and Postman collections as targets (#866)

This commit is contained in:
Anurag Mewar
2026-08-03 21:07:44 -07:00
committed by GitHub
parent ea6d53f4e9
commit 6719a70611
14 changed files with 1069 additions and 20 deletions
+57 -15
View File
@@ -33,6 +33,50 @@ def _accepts_required_tool_choice(model_name: str | None) -> bool:
return name.startswith("openai/") or is_known_openai_bare_model(name)
def _render_diff_scope(diff_scope: dict[str, Any]) -> list[str]:
"""Render pull-request diff-scope constraints as root-task lines."""
if not diff_scope.get("active"):
return []
parts: list[str] = [
"\n\nScope Constraints:",
"- Pull request diff-scope mode is active. Prioritize changed files "
"and use other files only for context.",
]
for repo_scope in diff_scope.get("repos", []) or []:
label = repo_scope.get("workspace_subdir") or repo_scope.get("source_path") or "repository"
changed = repo_scope.get("analyzable_files_count", 0)
deleted = repo_scope.get("deleted_files_count", 0)
parts.append(f"- {label}: {changed} changed file(s) in primary scope")
if deleted:
parts.append(f"- {label}: {deleted} deleted file(s) are context-only")
return parts
def _render_api_spec(details: dict[str, Any]) -> list[str]:
"""Render an API spec target as root-task lines.
The spec itself is in the workspace, so the task points at the file and lets
the agent read the contract rather than restating a parsed summary of it.
"""
title = details.get("spec_title") or details.get("target_spec", "API")
workspace_path = details.get("workspace_path", "")
lines = [
f"- {title} ({details.get('spec_format', 'api')} specification"
+ (f", available at: {workspace_path}" if workspace_path else "")
+ ")"
]
if base_urls := details.get("base_urls") or []:
lines.append(" - Base URL(s): " + ", ".join(base_urls))
lines.append(
" - Read the specification and test every operation it declares, using "
"its declared parameters, request bodies, and auth. Endpoints in the "
"specification are in scope even when nothing links to them. Load the "
"`api_spec_testing` skill for the methodology, or spawn a specialist "
"with it."
)
return lines
def build_root_task(scan_config: dict[str, Any]) -> str:
targets = scan_config.get("targets", []) or []
diff_scope = scan_config.get("diff_scope") or {}
@@ -43,6 +87,7 @@ def build_root_task(scan_config: dict[str, Any]) -> str:
"Local Codebases": [],
"URLs": [],
"IP Addresses": [],
"API Specifications": [],
}
for target in targets:
@@ -68,6 +113,8 @@ def build_root_task(scan_config: dict[str, Any]) -> str:
sections["URLs"].append(f"- {details.get('target_url', '')}")
elif ttype == "ip_address":
sections["IP Addresses"].append(f"- {details.get('target_ip', '')}")
elif ttype == "api_spec":
sections["API Specifications"].extend(_render_api_spec(details))
parts: list[str] = []
for label, items in sections.items():
@@ -92,21 +139,7 @@ def build_root_task(scan_config: dict[str, Any]) -> str:
"truth for what to do."
)
if diff_scope.get("active"):
parts.append("\n\nScope Constraints:")
parts.append(
"- Pull request diff-scope mode is active. Prioritize changed files "
"and use other files only for context.",
)
for repo_scope in diff_scope.get("repos", []) or []:
label = (
repo_scope.get("workspace_subdir") or repo_scope.get("source_path") or "repository"
)
changed = repo_scope.get("analyzable_files_count", 0)
deleted = repo_scope.get("deleted_files_count", 0)
parts.append(f"- {label}: {changed} changed file(s) in primary scope")
if deleted:
parts.append(f"- {label}: {deleted} deleted file(s) are context-only")
parts.extend(_render_diff_scope(diff_scope))
task = " ".join(parts)
if user_instructions:
@@ -121,6 +154,7 @@ def build_scope_context(scan_config: dict[str, Any]) -> dict[str, Any]:
"local_code": "target_path",
"web_application": "target_url",
"ip_address": "target_ip",
"api_spec": "target_spec",
}
for target in scan_config.get("targets", []) or []:
ttype = target.get("type", "unknown")
@@ -134,6 +168,14 @@ def build_scope_context(scan_config: dict[str, Any]) -> dict[str, Any]:
{"type": ttype, "value": value, "workspace_path": 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 []
)
return {
"scope_source": "system_scan_config",
"authorization_source": "strix_platform_verified_targets",