Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 33 additions & 3 deletions coding-agent/src/coding_agent/ui/http_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -860,6 +860,30 @@ def _session_origin_from_request(
return origin


def _setup_phase_exception_detail(exc: BaseException) -> str | None:
notes = [
note
for note in getattr(exc, "__notes__", ())
if isinstance(note, str) and note.startswith("setup phase ")
]
if not notes:
return None

returncode = getattr(exc, "returncode", None)
if isinstance(returncode, int):
prefix = f"setup phase failed with exit code {returncode}"
else:
prefix = "setup phase failed"
return "\n".join([prefix, *notes])


def _http_exception_detail(exc: BaseException) -> str:
setup_detail = _setup_phase_exception_detail(exc)
if setup_detail is not None:
return setup_detail
return str(exc)


def _http_safe_tool_result_payload(msg: ToolResultDelta) -> dict[str, Any]:
return {
"session_id": msg.session_id,
Expand Down Expand Up @@ -1300,10 +1324,16 @@ async def create_session(
if isinstance(exc, HTTPException):
raise exc
if isinstance(exc, RuntimeError):
raise HTTPException(status_code=500, detail=str(exc)) from exc
raise HTTPException(
status_code=500, detail=_http_exception_detail(exc)
) from exc
if isinstance(exc, (ValueError, TypeError)):
raise HTTPException(status_code=400, detail=str(exc)) from exc
raise HTTPException(status_code=500, detail=str(exc)) from exc
raise HTTPException(
status_code=400, detail=_http_exception_detail(exc)
) from exc
raise HTTPException(
status_code=500, detail=_http_exception_detail(exc)
) from exc

logger.info(f"Created session: {session_id}")
return SessionResponse(session_id=session_id)
Expand Down
51 changes: 51 additions & 0 deletions coding-agent/tests/cli/test_remote_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -3154,6 +3154,57 @@ def post(
assert "connection refused" in result.output


def test_remote_repl_reports_setup_failure_without_docker_command(
tmp_path: Path, monkeypatch
) -> None:
config_path = tmp_path / "remotes.json"
monkeypatch.setenv("CODING_AGENT_REMOTES_FILE", str(config_path))
runner = CliRunner()
runner.invoke(
main,
["remote", "add", "dev", "http://agent.example"],
catch_exceptions=False,
)

class FakeClient:
def __init__(self, **kwargs: object) -> None:
del kwargs

def __enter__(self) -> FakeClient:
return self

def __exit__(self, exc_type: object, exc: object, tb: object) -> None:
return None

def post(
self, path: str, json: dict[str, object] | None = None
) -> httpx.Response:
del path, json
request = httpx.Request("POST", "http://agent.example/sessions")
return httpx.Response(
500,
request=request,
json={
"detail": (
"setup phase failed with exit code 42\n"
"setup phase stderr:\nsetup failed intentionally\n"
)
},
)

monkeypatch.setattr("coding_agent.remote.client.httpx.Client", FakeClient)

result = runner.invoke(
main,
["remote", "repl", "dev", "--empty-workspace", "--goal", "hello"],
)

assert result.exit_code != 0
assert "setup phase failed with exit code 42" in result.output
assert "setup failed intentionally" in result.output
assert "docker run" not in result.output


def test_stream_prompt_reports_non_200_sse_response(monkeypatch) -> None:
class FakeResponse:
status_code = 503
Expand Down
45 changes: 45 additions & 0 deletions coding-agent/tests/ui/test_http_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -1021,6 +1021,51 @@ async def test_create_session_rejects_workspace_provisioning_when_disabled(
"cloud workspace provisioning requires cloud_workspace.enabled=true"
)

async def test_create_session_surfaces_redacted_setup_failure_detail(
self, client, monkeypatch, tmp_path
):
monkeypatch.setattr(
"coding_agent.ui.http_server._load_cloud_workspace_config",
lambda: {
"enabled": True,
"provider": "docker",
"workspace_root": str(tmp_path),
"container_name_prefix": "agent-",
**_test_runtime_profile_config(),
},
)

def fail_setup(config: dict[str, object], source: dict[str, object]):
del config, source
exc = subprocess.CalledProcessError(
returncode=42,
cmd=["docker", "run", "--name", "secret-container"],
)
exc.add_note("setup phase stdout:\n[REDACTED]\n")
exc.add_note("setup phase stderr:\nsetup failed intentionally\n")
raise exc

monkeypatch.setattr(
"coding_agent.ui.http_server.provision_cloud_binding_from_config",
fail_setup,
)

response = await client.post(
"/sessions",
json={"workspace_source": {"kind": "docker"}},
)

assert response.status_code == 500
detail = response.json()["detail"]
assert detail == (
"setup phase failed with exit code 42\n"
"setup phase stdout:\n[REDACTED]\n"
"\n"
"setup phase stderr:\nsetup failed intentionally\n"
)
assert "docker run" not in detail
assert "secret-container" not in detail

async def test_create_session_provisions_git_workspace_source(
self, client, monkeypatch, tmp_path
):
Expand Down