Skip to content

Agents provider scoping#7

Merged
LucaVor merged 2 commits into
mainfrom
agents-provider-scoping
Jun 15, 2026
Merged

Agents provider scoping#7
LucaVor merged 2 commits into
mainfrom
agents-provider-scoping

Conversation

@LucaVor

@LucaVor LucaVor commented Jun 15, 2026

Copy link
Copy Markdown
Collaborator

This pull request introduces a new agent runtime to the SDK, allowing users to run coding agents (either opencode or, 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 new client.agents interface, with both async and sync APIs, and raises explicit errors for unavailable providers. Documentation and examples are updated accordingly.

Agent runtime and provider management:

  • Added new agent runtime under 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 method run_sync. [1] [2] [3] [4] [5] [6] [7]
  • Introduced capability management methods: client.agents.providers(email) to list available providers and client.agents.set_provider(email, provider) to set the default provider, with REST API integration. [1] [2]
  • Added Provider enum for agent runtimes, with OpenCode as the default and ClaudeCode requiring capability gating. [1] [2] [3] [4] [5]

Error handling and configuration:

  • Introduced ProviderUnavailableError (raised if a provider is not available for the user) and AgentRunError (for mid-stream failures/timeouts). The agent runtime now keys identity and capability gating on user email (config.user_email or MANTIS_USER_EMAIL) instead of user ID. [1] [2] [3] [4] [5] [6]

Documentation and examples:

  • Updated README.md with usage instructions and event types for the new agent runtime, including async and sync examples, provider management, and capability gating. [1] [2]
  • Added examples/agent_run.py demonstrating how to run an agent session, handle events, and manage provider capabilities.

Other:

  • Bumped version to 0.12.0 and updated CHANGELOG.md to document the new features and breaking changes. [1] [2]

LucaVor added 2 commits June 15, 2026 16:11
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.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread mantis_sdk/agents.py
Comment on lines +120 to +122
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}"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-medium medium

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.

Suggested change
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='')}"

Comment thread mantis_sdk/agents.py
Comment on lines +130 to +132
self._ws = await websockets.connect(
self._ws_url(), additional_headers=headers, max_size=None, open_timeout=self.timeout
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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

Comment thread mantis_sdk/agents.py
Comment on lines +174 to +177
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread mantis_sdk/agents.py
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

@LucaVor LucaVor merged commit d99a136 into main Jun 15, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants