mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-10-04 09:02:11 +00:00
feat: Add Corridor MCP analyzePlan integration (#1572)
* Add Corridor MCP analyzePlan integration * Update agent/integrations/corridor_mcp.py Co-authored-by: open-swe[bot] <215916821+open-swe[bot]@users.noreply.github.com> * Add Corridor analysis prompt * Only include Corridor prompt when tool loads --------- Co-authored-by: open-swe[bot] <215916821+open-swe[bot]@users.noreply.github.com>
This commit is contained in:
parent
055b83e723
commit
9db7eab134
6 changed files with 349 additions and 0 deletions
|
|
@ -77,6 +77,8 @@ GitHub operations are performed with `GH_TOKEN=dummy gh` inside the sandbox, bac
|
||||||
|
|
||||||
**Optional observability tools (server-side):** Admins can connect Datadog and LangSmith from team settings (Admin → Observability credentials). When connected, the agent gains Datadog tools (via Datadog's hosted MCP server, default `toolsets=core`) and read-only LangSmith tools (`langsmith_get_trace`, `langsmith_list_runs`). These run in the LangGraph server process using credentials encrypted at rest — the sandbox never holds Datadog or LangSmith keys. They are loaded **only for runs triggered by an authorized user** (admins, plus any emails in `OBSERVABILITY_AUTHORIZED_EMAILS`), so a prompt-injected run from an untrusted contributor cannot reach team observability data. Use scoped, read-oriented keys regardless: observability data (logs, traces) is attacker-influenced content that can carry prompt injection, and the agent has network egress — the same residual-risk class as `web_search` / `fetch_url`.
|
**Optional observability tools (server-side):** Admins can connect Datadog and LangSmith from team settings (Admin → Observability credentials). When connected, the agent gains Datadog tools (via Datadog's hosted MCP server, default `toolsets=core`) and read-only LangSmith tools (`langsmith_get_trace`, `langsmith_list_runs`). These run in the LangGraph server process using credentials encrypted at rest — the sandbox never holds Datadog or LangSmith keys. They are loaded **only for runs triggered by an authorized user** (admins, plus any emails in `OBSERVABILITY_AUTHORIZED_EMAILS`), so a prompt-injected run from an untrusted contributor cannot reach team observability data. Use scoped, read-oriented keys regardless: observability data (logs, traces) is attacker-influenced content that can carry prompt injection, and the agent has network egress — the same residual-risk class as `web_search` / `fetch_url`.
|
||||||
|
|
||||||
|
**Optional Corridor guardrails (server-side MCP):** Set `CORRIDOR_API_TOKEN` (or `CORRIDOR_MCP_TOKEN` / `CORRIDOR_TOKEN`) to load Corridor's hosted MCP server for each agent run. Open SWE exposes only Corridor's `analyzePlan` tool. `CORRIDOR_MCP_URL` defaults to `https://app.corridor.dev/api/mcp`; if set explicitly, Open SWE only accepts the same HTTPS host and `/api/mcp` path. Tokens are sent via `Authorization: Bearer ...` from the LangGraph server process and are never placed in the sandbox. A legacy `?token=...` URL is accepted and normalized into the header form.
|
||||||
|
|
||||||
### 4. Context Engineering — AGENTS.md + Source Context
|
### 4. Context Engineering — AGENTS.md + Source Context
|
||||||
|
|
||||||
Open SWE gathers context from two sources:
|
Open SWE gathers context from two sources:
|
||||||
|
|
|
||||||
126
agent/integrations/corridor_mcp.py
Normal file
126
agent/integrations/corridor_mcp.py
Normal file
|
|
@ -0,0 +1,126 @@
|
||||||
|
"""Server-side Corridor tools backed by Corridor's hosted MCP server.
|
||||||
|
|
||||||
|
Credentials are read from environment variables and attached as an
|
||||||
|
``Authorization: Bearer ...`` header to the MCP connection, which runs in the
|
||||||
|
LangGraph server process. The sandbox never holds Corridor credentials.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from datetime import timedelta
|
||||||
|
from urllib.parse import parse_qs, urlencode, urlparse, urlunparse
|
||||||
|
|
||||||
|
from langchain_core.tools import BaseTool
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
DEFAULT_CORRIDOR_MCP_URL = "https://app.corridor.dev/api/mcp"
|
||||||
|
_CORRIDOR_HOST = "app.corridor.dev"
|
||||||
|
_CORRIDOR_PATH = "/api/mcp"
|
||||||
|
_MCP_TIMEOUT_SECONDS = 30.0
|
||||||
|
_TOKEN_ENV_NAMES = (
|
||||||
|
"CORRIDOR_API_TOKEN",
|
||||||
|
"CORRIDOR_MCP_TOKEN",
|
||||||
|
"CORRIDOR_TOKEN",
|
||||||
|
)
|
||||||
|
_TOKEN_QUERY_PARAMS = frozenset({"token", "api_key"})
|
||||||
|
_URL_ENV_NAMES = (
|
||||||
|
"CORRIDOR_MCP_URL",
|
||||||
|
"CORRIDOR_MCP_SERVER_URL",
|
||||||
|
)
|
||||||
|
_ALLOWED_TOOL_NAMES = frozenset({"analyzePlan"})
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class CorridorMCPConfig:
|
||||||
|
url: str
|
||||||
|
token: str
|
||||||
|
|
||||||
|
|
||||||
|
def _first_env_value(names: tuple[str, ...]) -> str:
|
||||||
|
for name in names:
|
||||||
|
value = os.environ.get(name, "").strip()
|
||||||
|
if value:
|
||||||
|
return value
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_token_from_query(url: str) -> tuple[str, str]:
|
||||||
|
parsed = urlparse(url)
|
||||||
|
query = parse_qs(parsed.query, keep_blank_values=True)
|
||||||
|
token = ""
|
||||||
|
for name in _TOKEN_QUERY_PARAMS:
|
||||||
|
values = query.pop(name, [])
|
||||||
|
if not token:
|
||||||
|
token = next((value.strip() for value in values if value.strip()), "")
|
||||||
|
cleaned_query = urlencode(query, doseq=True)
|
||||||
|
cleaned_url = urlunparse(parsed._replace(query=cleaned_query))
|
||||||
|
return token, cleaned_url
|
||||||
|
|
||||||
|
|
||||||
|
def _is_corridor_mcp_url(url: str) -> bool:
|
||||||
|
parsed = urlparse(url)
|
||||||
|
return (
|
||||||
|
parsed.scheme == "https"
|
||||||
|
and parsed.hostname == _CORRIDOR_HOST
|
||||||
|
and parsed.path.rstrip("/") == _CORRIDOR_PATH
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def load_corridor_mcp_config() -> CorridorMCPConfig | None:
|
||||||
|
"""Return Corridor MCP config when the environment contains valid settings."""
|
||||||
|
url = _first_env_value(_URL_ENV_NAMES) or DEFAULT_CORRIDOR_MCP_URL
|
||||||
|
token = _first_env_value(_TOKEN_ENV_NAMES)
|
||||||
|
query_token, url = _extract_token_from_query(url)
|
||||||
|
if not token:
|
||||||
|
token = query_token
|
||||||
|
if not token:
|
||||||
|
return None
|
||||||
|
if not _is_corridor_mcp_url(url):
|
||||||
|
logger.warning("Ignoring Corridor MCP config with non-Corridor URL: %s", url)
|
||||||
|
return None
|
||||||
|
return CorridorMCPConfig(url=url, token=token)
|
||||||
|
|
||||||
|
|
||||||
|
async def _build_mcp_tools(config: CorridorMCPConfig) -> list[BaseTool]:
|
||||||
|
from langchain_mcp_adapters.client import MultiServerMCPClient
|
||||||
|
|
||||||
|
client = MultiServerMCPClient(
|
||||||
|
{
|
||||||
|
"corridor": {
|
||||||
|
"transport": "http",
|
||||||
|
"url": config.url,
|
||||||
|
"headers": {
|
||||||
|
"Authorization": f"Bearer {config.token}",
|
||||||
|
},
|
||||||
|
"timeout": timedelta(seconds=_MCP_TIMEOUT_SECONDS),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return await client.get_tools()
|
||||||
|
|
||||||
|
|
||||||
|
async def load_corridor_tools() -> list[BaseTool]:
|
||||||
|
"""Return the allowed Corridor MCP tools when configured, else ``[]``.
|
||||||
|
|
||||||
|
Failures (missing config, unreachable MCP server) degrade to an empty list so
|
||||||
|
the agent still starts without Corridor tools.
|
||||||
|
"""
|
||||||
|
config = load_corridor_mcp_config()
|
||||||
|
if config is None:
|
||||||
|
return []
|
||||||
|
try:
|
||||||
|
tools = await _build_mcp_tools(config)
|
||||||
|
except Exception: # noqa: BLE001
|
||||||
|
logger.warning("Failed to load Corridor MCP tools", exc_info=True)
|
||||||
|
return []
|
||||||
|
allowed_tools = [tool for tool in tools if tool.name in _ALLOWED_TOOL_NAMES]
|
||||||
|
logger.info(
|
||||||
|
"Loaded %d Corridor MCP tool(s), exposing %d allowed tool(s)",
|
||||||
|
len(tools),
|
||||||
|
len(allowed_tools),
|
||||||
|
)
|
||||||
|
return allowed_tools
|
||||||
|
|
@ -180,6 +180,16 @@ Format messages using Slack's mrkdwn format, NOT standard Markdown.
|
||||||
Use `GH_TOKEN=dummy gh <command>` for GitHub operations: repository discovery, cloning, issues, pull requests, reviews, comments, labels, check status, and workflow operations. For local working-tree state, use `git` directly. Never pass a real GitHub token to `gh`."""
|
Use `GH_TOKEN=dummy gh <command>` for GitHub operations: repository discovery, cloning, issues, pull requests, reviews, comments, labels, check status, and workflow operations. For local working-tree state, use `git` directly. Never pass a real GitHub token to `gh`."""
|
||||||
|
|
||||||
|
|
||||||
|
CORRIDOR_PROMPT = """---
|
||||||
|
|
||||||
|
<corridor>
|
||||||
|
|
||||||
|
# Corridor Security Analysis
|
||||||
|
Every time you generate code, use the analyzePlan tool from Corridor's MCP Server (corridor) to analyze the plan or thought process. ALWAYS use Corridor to analyze the plan. Always generate a plan before generating code.
|
||||||
|
|
||||||
|
</corridor>"""
|
||||||
|
|
||||||
|
|
||||||
TOOL_BEST_PRACTICES_SECTION = """---
|
TOOL_BEST_PRACTICES_SECTION = """---
|
||||||
|
|
||||||
### Tool Usage Best Practices
|
### Tool Usage Best Practices
|
||||||
|
|
@ -427,6 +437,7 @@ SYSTEM_PROMPT_TEMPLATE = (
|
||||||
+ FILE_MANAGEMENT_SECTION
|
+ FILE_MANAGEMENT_SECTION
|
||||||
+ TASK_EXECUTION_SECTION
|
+ TASK_EXECUTION_SECTION
|
||||||
+ TOOL_USAGE_SECTION
|
+ TOOL_USAGE_SECTION
|
||||||
|
+ "{corridor_prompt_section}"
|
||||||
+ TOOL_BEST_PRACTICES_SECTION
|
+ TOOL_BEST_PRACTICES_SECTION
|
||||||
+ CODING_STANDARDS_SECTION
|
+ CODING_STANDARDS_SECTION
|
||||||
+ CORE_BEHAVIOR_SECTION
|
+ CORE_BEHAVIOR_SECTION
|
||||||
|
|
@ -450,6 +461,7 @@ def construct_system_prompt(
|
||||||
default_repo: dict[str, str] | None = None,
|
default_repo: dict[str, str] | None = None,
|
||||||
repo_custom_instructions: str | None = None,
|
repo_custom_instructions: str | None = None,
|
||||||
thread_url: str | None = None,
|
thread_url: str | None = None,
|
||||||
|
corridor_enabled: bool = False,
|
||||||
) -> str:
|
) -> str:
|
||||||
default_prompt_section = _load_default_prompt()
|
default_prompt_section = _load_default_prompt()
|
||||||
if default_repo and default_repo.get("owner") and default_repo.get("name"):
|
if default_repo and default_repo.get("owner") and default_repo.get("name"):
|
||||||
|
|
@ -471,6 +483,7 @@ def construct_system_prompt(
|
||||||
linear_project_id=linear_project_id or "<PROJECT_ID>",
|
linear_project_id=linear_project_id or "<PROJECT_ID>",
|
||||||
linear_issue_number=linear_issue_number or "<ISSUE_NUMBER>",
|
linear_issue_number=linear_issue_number or "<ISSUE_NUMBER>",
|
||||||
default_prompt_section=default_prompt_section,
|
default_prompt_section=default_prompt_section,
|
||||||
|
corridor_prompt_section=CORRIDOR_PROMPT if corridor_enabled else "",
|
||||||
pr_policy_override_section=ALWAYS_CREATE_PR_SECTION if create_prs else "",
|
pr_policy_override_section=ALWAYS_CREATE_PR_SECTION if create_prs else "",
|
||||||
collaboration_section=_render_collaboration_section(triggering_user_identity, thread_url),
|
collaboration_section=_render_collaboration_section(triggering_user_identity, thread_url),
|
||||||
repo_instructions_section=_render_repo_instructions_section(repo_custom_instructions),
|
repo_instructions_section=_render_repo_instructions_section(repo_custom_instructions),
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,7 @@ from .dashboard.agent_usage import record_agent_thread_usage
|
||||||
from .dashboard.options import DEFAULT_MODEL_ID, SUPPORTED_MODEL_IDS, model_supports_effort
|
from .dashboard.options import DEFAULT_MODEL_ID, SUPPORTED_MODEL_IDS, model_supports_effort
|
||||||
from .dashboard.team_settings import get_team_default_model_pair, get_team_default_repo
|
from .dashboard.team_settings import get_team_default_model_pair, get_team_default_repo
|
||||||
from .dashboard.user_mappings import email_for_login
|
from .dashboard.user_mappings import email_for_login
|
||||||
|
from .integrations.corridor_mcp import load_corridor_tools
|
||||||
from .integrations.currents_tools import load_currents_tools
|
from .integrations.currents_tools import load_currents_tools
|
||||||
from .integrations.datadog_mcp import load_datadog_tools
|
from .integrations.datadog_mcp import load_datadog_tools
|
||||||
from .integrations.langsmith import _configure_github_proxy
|
from .integrations.langsmith import _configure_github_proxy
|
||||||
|
|
@ -530,6 +531,15 @@ async def _load_observability_tools(authorized: bool) -> list[Any]:
|
||||||
return [*datadog_tools, *langsmith_tools]
|
return [*datadog_tools, *langsmith_tools]
|
||||||
|
|
||||||
|
|
||||||
|
async def _load_corridor_mcp_tools() -> list[Any]:
|
||||||
|
"""Corridor MCP tools when the deployment environment has configured them."""
|
||||||
|
try:
|
||||||
|
return await load_corridor_tools()
|
||||||
|
except Exception:
|
||||||
|
logger.warning("Failed to load Corridor MCP tools", exc_info=True)
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
async def get_agent(config: RunnableConfig) -> Pregel:
|
async def get_agent(config: RunnableConfig) -> Pregel:
|
||||||
"""Get or create an agent with a sandbox for the given thread."""
|
"""Get or create an agent with a sandbox for the given thread."""
|
||||||
thread_id = config["configurable"].get("thread_id", None)
|
thread_id = config["configurable"].get("thread_id", None)
|
||||||
|
|
@ -679,6 +689,7 @@ async def get_agent(config: RunnableConfig) -> Pregel:
|
||||||
observability_tools = await _load_observability_tools(
|
observability_tools = await _load_observability_tools(
|
||||||
await _observability_authorized(config, profile_login)
|
await _observability_authorized(config, profile_login)
|
||||||
)
|
)
|
||||||
|
corridor_tools = await _load_corridor_mcp_tools()
|
||||||
|
|
||||||
currents_tools: list[Any] = []
|
currents_tools: list[Any] = []
|
||||||
if profile_login:
|
if profile_login:
|
||||||
|
|
@ -702,6 +713,7 @@ async def get_agent(config: RunnableConfig) -> Pregel:
|
||||||
default_repo=prompt_default_repo,
|
default_repo=prompt_default_repo,
|
||||||
repo_custom_instructions=repo_custom_instructions,
|
repo_custom_instructions=repo_custom_instructions,
|
||||||
thread_url=dashboard_thread_url(thread_id),
|
thread_url=dashboard_thread_url(thread_id),
|
||||||
|
corridor_enabled=bool(corridor_tools),
|
||||||
),
|
),
|
||||||
tools=[
|
tools=[
|
||||||
http_request,
|
http_request,
|
||||||
|
|
@ -718,6 +730,7 @@ async def get_agent(config: RunnableConfig) -> Pregel:
|
||||||
request_pr_review,
|
request_pr_review,
|
||||||
slack_read_thread_messages,
|
slack_read_thread_messages,
|
||||||
slack_thread_reply,
|
slack_thread_reply,
|
||||||
|
*corridor_tools,
|
||||||
*observability_tools,
|
*observability_tools,
|
||||||
*currents_tools,
|
*currents_tools,
|
||||||
],
|
],
|
||||||
|
|
|
||||||
180
tests/test_corridor_mcp.py
Normal file
180
tests/test_corridor_mcp.py
Normal file
|
|
@ -0,0 +1,180 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from unittest.mock import AsyncMock, MagicMock, patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from agent import server
|
||||||
|
from agent.integrations import corridor_mcp
|
||||||
|
|
||||||
|
|
||||||
|
class _FakeTool:
|
||||||
|
def __init__(self, name: str) -> None:
|
||||||
|
self.name = name
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def clear_corridor_env(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
for name in (
|
||||||
|
"CORRIDOR_API_TOKEN",
|
||||||
|
"CORRIDOR_MCP_TOKEN",
|
||||||
|
"CORRIDOR_TOKEN",
|
||||||
|
"CORRIDOR_MCP_URL",
|
||||||
|
"CORRIDOR_MCP_SERVER_URL",
|
||||||
|
):
|
||||||
|
monkeypatch.delenv(name, raising=False)
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_corridor_mcp_config_empty_without_token() -> None:
|
||||||
|
assert corridor_mcp.load_corridor_mcp_config() is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_corridor_mcp_config_uses_default_url(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
monkeypatch.setenv("CORRIDOR_API_TOKEN", "tok")
|
||||||
|
|
||||||
|
config = corridor_mcp.load_corridor_mcp_config()
|
||||||
|
|
||||||
|
assert config == corridor_mcp.CorridorMCPConfig(
|
||||||
|
url=corridor_mcp.DEFAULT_CORRIDOR_MCP_URL,
|
||||||
|
token="tok",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_corridor_mcp_config_accepts_query_token(
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
monkeypatch.setenv("CORRIDOR_MCP_URL", "https://app.corridor.dev/api/mcp?token=tok")
|
||||||
|
|
||||||
|
config = corridor_mcp.load_corridor_mcp_config()
|
||||||
|
|
||||||
|
assert config == corridor_mcp.CorridorMCPConfig(
|
||||||
|
url=corridor_mcp.DEFAULT_CORRIDOR_MCP_URL,
|
||||||
|
token="tok",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_corridor_mcp_config_rejects_non_corridor_url(
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
monkeypatch.setenv("CORRIDOR_API_TOKEN", "tok")
|
||||||
|
monkeypatch.setenv("CORRIDOR_MCP_URL", "https://example.com/api/mcp")
|
||||||
|
|
||||||
|
assert corridor_mcp.load_corridor_mcp_config() is None
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_load_corridor_tools_empty_when_not_configured() -> None:
|
||||||
|
assert await corridor_mcp.load_corridor_tools() == []
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_load_corridor_tools_degrades_on_error(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
monkeypatch.setenv("CORRIDOR_API_TOKEN", "tok")
|
||||||
|
|
||||||
|
with patch.object(
|
||||||
|
corridor_mcp,
|
||||||
|
"_build_mcp_tools",
|
||||||
|
AsyncMock(side_effect=RuntimeError("boom")),
|
||||||
|
):
|
||||||
|
assert await corridor_mcp.load_corridor_tools() == []
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_load_corridor_tools_returns_tools(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
monkeypatch.setenv("CORRIDOR_API_TOKEN", "tok")
|
||||||
|
analyze_plan = _FakeTool("analyzePlan")
|
||||||
|
other_tool = _FakeTool("otherTool")
|
||||||
|
|
||||||
|
with patch.object(
|
||||||
|
corridor_mcp,
|
||||||
|
"_build_mcp_tools",
|
||||||
|
AsyncMock(return_value=[other_tool, analyze_plan]),
|
||||||
|
):
|
||||||
|
assert await corridor_mcp.load_corridor_tools() == [analyze_plan]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_server_load_corridor_mcp_tools() -> None:
|
||||||
|
with patch.object(server, "load_corridor_tools", AsyncMock(return_value=["corridor"])):
|
||||||
|
assert await server._load_corridor_mcp_tools() == ["corridor"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_server_load_corridor_mcp_tools_degrades_on_error() -> None:
|
||||||
|
with patch.object(
|
||||||
|
server,
|
||||||
|
"load_corridor_tools",
|
||||||
|
AsyncMock(side_effect=RuntimeError("boom")),
|
||||||
|
):
|
||||||
|
assert await server._load_corridor_mcp_tools() == []
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_agent_passes_corridor_prompt_state() -> None:
|
||||||
|
config = {
|
||||||
|
"configurable": {
|
||||||
|
"__is_for_execution__": True,
|
||||||
|
"thread_id": "thread-123",
|
||||||
|
},
|
||||||
|
"metadata": {},
|
||||||
|
}
|
||||||
|
|
||||||
|
def fake_create_deep_agent(**_kwargs):
|
||||||
|
class _DummyAgent:
|
||||||
|
def with_config(self, _config):
|
||||||
|
return self
|
||||||
|
|
||||||
|
return _DummyAgent()
|
||||||
|
|
||||||
|
async def run_with_corridor_tools(corridor_tools: list[object]) -> bool:
|
||||||
|
with (
|
||||||
|
patch.object(
|
||||||
|
server,
|
||||||
|
"resolve_github_token",
|
||||||
|
new_callable=AsyncMock,
|
||||||
|
return_value=("ghp", None),
|
||||||
|
),
|
||||||
|
patch.object(server, "resolve_triggering_user_identity", return_value=None),
|
||||||
|
patch.object(
|
||||||
|
server,
|
||||||
|
"ensure_sandbox_for_thread",
|
||||||
|
new_callable=AsyncMock,
|
||||||
|
return_value=MagicMock(),
|
||||||
|
),
|
||||||
|
patch.object(
|
||||||
|
server,
|
||||||
|
"aresolve_sandbox_work_dir",
|
||||||
|
new_callable=AsyncMock,
|
||||||
|
return_value="/workspace",
|
||||||
|
),
|
||||||
|
patch.object(
|
||||||
|
server,
|
||||||
|
"get_team_default_model_pair",
|
||||||
|
new_callable=AsyncMock,
|
||||||
|
return_value=(("openai:gpt-5.5", "medium"), ("openai:gpt-5.5", "low")),
|
||||||
|
),
|
||||||
|
patch.object(server, "fallback_model_id_for", return_value=None),
|
||||||
|
patch.object(server, "make_model", return_value=MagicMock()),
|
||||||
|
patch.object(
|
||||||
|
server, "_load_observability_tools", new_callable=AsyncMock, return_value=[]
|
||||||
|
),
|
||||||
|
patch.object(
|
||||||
|
server,
|
||||||
|
"_observability_authorized",
|
||||||
|
new_callable=AsyncMock,
|
||||||
|
return_value=False,
|
||||||
|
),
|
||||||
|
patch.object(
|
||||||
|
server,
|
||||||
|
"_load_corridor_mcp_tools",
|
||||||
|
new_callable=AsyncMock,
|
||||||
|
return_value=corridor_tools,
|
||||||
|
),
|
||||||
|
patch.object(server, "construct_system_prompt", return_value="prompt") as prompt,
|
||||||
|
patch.object(server, "create_deep_agent", side_effect=fake_create_deep_agent),
|
||||||
|
):
|
||||||
|
await server.get_agent(config)
|
||||||
|
return bool(prompt.call_args.kwargs["corridor_enabled"])
|
||||||
|
|
||||||
|
assert await run_with_corridor_tools([]) is False
|
||||||
|
assert await run_with_corridor_tools([_FakeTool("analyzePlan")]) is True
|
||||||
|
|
@ -48,6 +48,21 @@ def test_construct_system_prompt_identifies_own_repo() -> None:
|
||||||
assert "langchain-ai/open-swe" in prompt
|
assert "langchain-ai/open-swe" in prompt
|
||||||
|
|
||||||
|
|
||||||
|
def test_construct_system_prompt_omits_corridor_prompt_by_default() -> None:
|
||||||
|
prompt = construct_system_prompt(working_dir="/workspace")
|
||||||
|
|
||||||
|
assert "<corridor>" not in prompt
|
||||||
|
assert "Corridor Security Analysis" not in prompt
|
||||||
|
|
||||||
|
|
||||||
|
def test_construct_system_prompt_includes_corridor_prompt_when_enabled() -> None:
|
||||||
|
prompt = construct_system_prompt(working_dir="/workspace", corridor_enabled=True)
|
||||||
|
|
||||||
|
assert "<corridor>" in prompt
|
||||||
|
assert "Corridor Security Analysis" in prompt
|
||||||
|
assert "analyzePlan" in prompt
|
||||||
|
|
||||||
|
|
||||||
def test_construct_system_prompt_omits_collaboration_section_without_identity() -> None:
|
def test_construct_system_prompt_omits_collaboration_section_without_identity() -> None:
|
||||||
prompt = construct_system_prompt(working_dir="/workspace")
|
prompt = construct_system_prompt(working_dir="/workspace")
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue