Agents provider scoping#7
Conversation
Run claude_code / opencode coding agents scoped to a space, streaming normalized events. Contract verified live against the running stack during recon. - client.agents.session(space_id, provider=..., user_email=...) → async streaming AgentSession; run.ask(...) yields AgentEvent (text/tool_use/tool_result/thinking/ complete/fail); run.result() → AgentResult. run_sync(...) for the sync path. - Provider enum (OpenCode default + universal, ClaudeCode opt-in + capability-gated), sent as model_id per run (per-message scoping, matching the backend). - Provider gating up front via GET /agent_execution/providers/: raises ProviderUnavailableError instead of the backend's silent fallback, and also if a run reports a different provider than requested. set_provider() over .../providers/set/. - Transport: ws/chat/<chat_id>/<url-encoded-email>/default/?model_id=<provider> (email-in- path route so identity + capability gating resolve). websockets imported lazily. - ConfigurationManager.user_email / MANTIS_USER_EMAIL (agents key on email, not user_id). - 16 mocked unit tests (fake WS replaying the recon event stream + capability stubs); README "Agents" section, examples/agent_run.py, CHANGELOG 0.12.0. Full suite: 50 passed, ruff clean, rest-only import still works without playwright.
The agent_execution routes are included under path('api/', ...) in backend/urls.py,
so the proxy-relative paths are /api/agent_execution/providers[/set]/, matching the
convention used by every other resource. Caught while wiring the MantisAPI contract guard.
There was a problem hiding this comment.
Code Review
This pull request introduces a provider-scoped agent runtime (client.agents) supporting opencode and claude_code agents, featuring async streaming via AgentSession, capability management, and new exceptions (ProviderUnavailableError, AgentRunError). The review feedback focuses on enhancing robustness by URL-encoding dynamic path/query parameters (chat_id, space_id) and wrapping raw third-party WebSocket exceptions in SDK-specific exceptions to prevent leakage.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| url = f"{ws_base}/ws/chat/{self.chat_id}/{quote(self.user_email, safe='')}/default/?model_id={self.provider.value}" | ||
| if self.space_id: | ||
| url += f"&space_id={self.space_id}" |
There was a problem hiding this comment.
To prevent potential URL malformation or path traversal issues, any user-provided path or query parameters (such as chat_id and space_id) should be URL-encoded using quote before being interpolated into the WebSocket URL.
| url = f"{ws_base}/ws/chat/{self.chat_id}/{quote(self.user_email, safe='')}/default/?model_id={self.provider.value}" | |
| if self.space_id: | |
| url += f"&space_id={self.space_id}" | |
| url = f"{ws_base}/ws/chat/{quote(self.chat_id, safe='')}/{quote(self.user_email, safe='')}/default/?model_id={self.provider.value}" | |
| if self.space_id: | |
| url += f"&space_id={quote(self.space_id, safe='')}" |
| self._ws = await websockets.connect( | ||
| self._ws_url(), additional_headers=headers, max_size=None, open_timeout=self.timeout | ||
| ) |
There was a problem hiding this comment.
To ensure that the SDK's exception contract is honored (where all SDK errors inherit from MantisError), connection and handshake failures during WebSocket establishment should be caught and wrapped in appropriate SDK-specific exceptions (such as AuthenticationError, APIStatusError, or APIConnectionError) instead of leaking raw websockets exceptions.
try:
self._ws = await websockets.connect(
self._ws_url(), additional_headers=headers, max_size=None, open_timeout=self.timeout
)
except Exception as exc:
class_name = exc.__class__.__name__
if class_name == "InvalidStatusCode":
status_code = getattr(exc, "status_code", 500)
if status_code in (401, 403):
from .exceptions import AuthenticationError
raise AuthenticationError(
f"Authentication failed during agent session connection: {exc}",
status_code=status_code,
url=self._ws_url(),
) from exc
from .exceptions import APIStatusError
raise APIStatusError(
f"Failed to connect to agent session: {exc}",
status_code=status_code,
url=self._ws_url(),
) from exc
from .exceptions import APIConnectionError
raise APIConnectionError(f"Failed to establish agent session connection: {exc}") from exc| try: | ||
| raw = await asyncio.wait_for(self._ws.recv(), timeout=remaining) | ||
| except asyncio.TimeoutError as exc: | ||
| raise AgentRunError(f"agent run timed out after {self.timeout}s") from exc |
There was a problem hiding this comment.
If the WebSocket connection is closed unexpectedly mid-stream, self._ws.recv() will raise a websockets.exceptions.ConnectionClosed exception. To prevent raw third-party exceptions from leaking to the client, this should be caught and wrapped in AgentRunError.
| try: | |
| raw = await asyncio.wait_for(self._ws.recv(), timeout=remaining) | |
| except asyncio.TimeoutError as exc: | |
| raise AgentRunError(f"agent run timed out after {self.timeout}s") from exc | |
| try: | |
| raw = await asyncio.wait_for(self._ws.recv(), timeout=remaining) | |
| except asyncio.TimeoutError as exc: | |
| raise AgentRunError(f"agent run timed out after {self.timeout}s") from exc | |
| except Exception as exc: | |
| import websockets | |
| if isinstance(exc, websockets.exceptions.ConnectionClosed): | |
| raise AgentRunError("Connection closed unexpectedly during agent run") from exc | |
| raise |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b48f9e32f3
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| cookie = self._resource.http.cookie | ||
| headers = {"Cookie": cookie} if cookie else None | ||
| self._ws = await websockets.connect( | ||
| self._ws_url(), additional_headers=headers, max_size=None, open_timeout=self.timeout |
There was a problem hiding this comment.
Use the websocket header keyword supported by declared deps
In environments satisfying the package's declared websockets>=10.4 dependency but still on the 10.x–13.x API, websockets.connect expects extra_headers, not additional_headers, so entering any client.agents.session(...) raises before the socket opens. Either keep this call compatible with the supported versions or raise the dependency floor to a version that accepts additional_headers.
Useful? React with 👍 / 👎.
This pull request introduces a new agent runtime to the SDK, allowing users to run coding agents (either
opencodeor, if permitted,claude_code) scoped to a space with streaming output. The agent runtime is provider-scoped, supports capability management, and identifies users by email. The SDK now exposes these features through a newclient.agentsinterface, with both async and sync APIs, and raises explicit errors for unavailable providers. Documentation and examples are updated accordingly.Agent runtime and provider management:
client.agents, supporting provider-scoped agent runs (default:opencode, opt-in:claude_code), with streaming of normalized events via async context manager (AgentSession). Also added synchronous convenience methodrun_sync. [1] [2] [3] [4] [5] [6] [7]client.agents.providers(email)to list available providers andclient.agents.set_provider(email, provider)to set the default provider, with REST API integration. [1] [2]Providerenum for agent runtimes, withOpenCodeas the default andClaudeCoderequiring capability gating. [1] [2] [3] [4] [5]Error handling and configuration:
ProviderUnavailableError(raised if a provider is not available for the user) andAgentRunError(for mid-stream failures/timeouts). The agent runtime now keys identity and capability gating on user email (config.user_emailorMANTIS_USER_EMAIL) instead of user ID. [1] [2] [3] [4] [5] [6]Documentation and examples:
README.mdwith usage instructions and event types for the new agent runtime, including async and sync examples, provider management, and capability gating. [1] [2]examples/agent_run.pydemonstrating how to run an agent session, handle events, and manage provider capabilities.Other:
CHANGELOG.mdto document the new features and breaking changes. [1] [2]