agentshim 0.4.0__tar.gz → 0.5.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. agentshim-0.5.0/.github/workflows/ci.yml +41 -0
  2. {agentshim-0.4.0 → agentshim-0.5.0}/PKG-INFO +88 -4
  3. {agentshim-0.4.0 → agentshim-0.5.0}/README.md +87 -3
  4. {agentshim-0.4.0 → agentshim-0.5.0}/agentshim/__init__.py +16 -0
  5. {agentshim-0.4.0 → agentshim-0.5.0}/agentshim/base.py +7 -3
  6. {agentshim-0.4.0 → agentshim-0.5.0}/agentshim/claude/agent.py +8 -5
  7. {agentshim-0.4.0 → agentshim-0.5.0}/agentshim/cli_agent.py +44 -124
  8. {agentshim-0.4.0 → agentshim-0.5.0}/agentshim/codex/agent.py +8 -5
  9. {agentshim-0.4.0 → agentshim-0.5.0}/agentshim/copilot/agent.py +7 -5
  10. agentshim-0.5.0/agentshim/executor.py +319 -0
  11. {agentshim-0.4.0 → agentshim-0.5.0}/agentshim/gemini/agent.py +8 -5
  12. {agentshim-0.4.0 → agentshim-0.5.0}/agentshim/opencode/agent.py +8 -5
  13. {agentshim-0.4.0 → agentshim-0.5.0}/pyproject.toml +1 -1
  14. agentshim-0.5.0/tests/unit/cli_agent/test_command_executor.py +245 -0
  15. {agentshim-0.4.0 → agentshim-0.5.0}/tests/unit/test_coding_agent_facade.py +7 -0
  16. {agentshim-0.4.0 → agentshim-0.5.0}/uv.lock +1 -1
  17. agentshim-0.4.0/.github/workflows/ci.yml +0 -18
  18. {agentshim-0.4.0 → agentshim-0.5.0}/.github/workflows/publish.yml +0 -0
  19. {agentshim-0.4.0 → agentshim-0.5.0}/.gitignore +0 -0
  20. {agentshim-0.4.0 → agentshim-0.5.0}/agentshim/claude/__init__.py +0 -0
  21. {agentshim-0.4.0 → agentshim-0.5.0}/agentshim/claude/events.py +0 -0
  22. {agentshim-0.4.0 → agentshim-0.5.0}/agentshim/claude/hooks/__init__.py +0 -0
  23. {agentshim-0.4.0 → agentshim-0.5.0}/agentshim/claude/hooks/confine_reads.py +0 -0
  24. {agentshim-0.4.0 → agentshim-0.5.0}/agentshim/claude_events.py +0 -0
  25. {agentshim-0.4.0 → agentshim-0.5.0}/agentshim/codex/__init__.py +0 -0
  26. {agentshim-0.4.0 → agentshim-0.5.0}/agentshim/codex/events.py +0 -0
  27. {agentshim-0.4.0 → agentshim-0.5.0}/agentshim/codex_events.py +0 -0
  28. {agentshim-0.4.0 → agentshim-0.5.0}/agentshim/copilot/__init__.py +0 -0
  29. {agentshim-0.4.0 → agentshim-0.5.0}/agentshim/copilot/events.py +0 -0
  30. {agentshim-0.4.0 → agentshim-0.5.0}/agentshim/copilot_events.py +0 -0
  31. {agentshim-0.4.0 → agentshim-0.5.0}/agentshim/events.py +0 -0
  32. {agentshim-0.4.0 → agentshim-0.5.0}/agentshim/gemini/__init__.py +0 -0
  33. {agentshim-0.4.0 → agentshim-0.5.0}/agentshim/gemini/events.py +0 -0
  34. {agentshim-0.4.0 → agentshim-0.5.0}/agentshim/gemini_events.py +0 -0
  35. {agentshim-0.4.0 → agentshim-0.5.0}/agentshim/llm_client.py +0 -0
  36. {agentshim-0.4.0 → agentshim-0.5.0}/agentshim/mcp_config.py +0 -0
  37. {agentshim-0.4.0 → agentshim-0.5.0}/agentshim/opencode/__init__.py +0 -0
  38. {agentshim-0.4.0 → agentshim-0.5.0}/agentshim/opencode/events.py +0 -0
  39. {agentshim-0.4.0 → agentshim-0.5.0}/agentshim/opencode_events.py +0 -0
  40. {agentshim-0.4.0 → agentshim-0.5.0}/agentshim/py.typed +0 -0
  41. {agentshim-0.4.0 → agentshim-0.5.0}/agentshim/sandbox.py +0 -0
  42. {agentshim-0.4.0 → agentshim-0.5.0}/agentshim/subagent.py +0 -0
  43. {agentshim-0.4.0 → agentshim-0.5.0}/agentshim/usage.py +0 -0
  44. {agentshim-0.4.0 → agentshim-0.5.0}/agentshim/utils.py +0 -0
  45. {agentshim-0.4.0 → agentshim-0.5.0}/tests/fixtures/copilot/session_turn_1.jsonl +0 -0
  46. {agentshim-0.4.0 → agentshim-0.5.0}/tests/fixtures/copilot/session_turn_2_resumed.jsonl +0 -0
  47. {agentshim-0.4.0 → agentshim-0.5.0}/tests/fixtures/copilot/streaming_dedup.jsonl +0 -0
  48. {agentshim-0.4.0 → agentshim-0.5.0}/tests/fixtures/copilot/tool_and_usage.jsonl +0 -0
  49. {agentshim-0.4.0 → agentshim-0.5.0}/tests/unit/cli_agent/test_agent_cli_cleanup.py +0 -0
  50. {agentshim-0.4.0 → agentshim-0.5.0}/tests/unit/cli_agent/test_check_cli.py +0 -0
  51. {agentshim-0.4.0 → agentshim-0.5.0}/tests/unit/cli_agent/test_cli_prompt_passing.py +0 -0
  52. {agentshim-0.4.0 → agentshim-0.5.0}/tests/unit/llm/conftest.py +0 -0
  53. {agentshim-0.4.0 → agentshim-0.5.0}/tests/unit/llm/test_claude_stream.py +0 -0
  54. {agentshim-0.4.0 → agentshim-0.5.0}/tests/unit/llm/test_gemini_fixture.py +0 -0
  55. {agentshim-0.4.0 → agentshim-0.5.0}/tests/unit/llm/test_gemini_stream.py +0 -0
  56. {agentshim-0.4.0 → agentshim-0.5.0}/tests/unit/test_agent_cli_claude.py +0 -0
  57. {agentshim-0.4.0 → agentshim-0.5.0}/tests/unit/test_agent_cli_codex.py +0 -0
  58. {agentshim-0.4.0 → agentshim-0.5.0}/tests/unit/test_agent_cli_copilot.py +0 -0
  59. {agentshim-0.4.0 → agentshim-0.5.0}/tests/unit/test_agent_cli_copilot_fixtures.py +0 -0
  60. {agentshim-0.4.0 → agentshim-0.5.0}/tests/unit/test_agent_cli_event_parsing.py +0 -0
  61. {agentshim-0.4.0 → agentshim-0.5.0}/tests/unit/test_agent_cli_mcp_unsupported.py +0 -0
  62. {agentshim-0.4.0 → agentshim-0.5.0}/tests/unit/test_agent_cli_resume.py +0 -0
  63. {agentshim-0.4.0 → agentshim-0.5.0}/tests/unit/test_agent_cli_sandbox.py +0 -0
  64. {agentshim-0.4.0 → agentshim-0.5.0}/tests/unit/test_cli_agent_usage.py +0 -0
  65. {agentshim-0.4.0 → agentshim-0.5.0}/tests/unit/test_event_handlers.py +0 -0
  66. {agentshim-0.4.0 → agentshim-0.5.0}/tests/unit/test_mcp_config.py +0 -0
@@ -0,0 +1,41 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ pull_request:
6
+
7
+ permissions:
8
+ contents: read
9
+
10
+ jobs:
11
+ lint:
12
+ runs-on: ubuntu-latest
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+ - uses: astral-sh/setup-uv@v6
16
+ with:
17
+ enable-cache: true
18
+ cache-dependency-glob: uv.lock
19
+ - uses: actions/setup-python@v5
20
+ with:
21
+ python-version: "3.11"
22
+ - run: uv sync --locked --dev
23
+ - run: uv run ruff check .
24
+
25
+ test:
26
+ runs-on: ubuntu-latest
27
+ strategy:
28
+ fail-fast: false
29
+ matrix:
30
+ python-version: ["3.10", "3.11", "3.12"]
31
+ steps:
32
+ - uses: actions/checkout@v4
33
+ - uses: astral-sh/setup-uv@v6
34
+ with:
35
+ enable-cache: true
36
+ cache-dependency-glob: uv.lock
37
+ - uses: actions/setup-python@v5
38
+ with:
39
+ python-version: ${{ matrix.python-version }}
40
+ - run: uv sync --locked --dev
41
+ - run: uv run pytest
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentshim
3
- Version: 0.4.0
3
+ Version: 0.5.0
4
4
  Summary: Provider-agnostic coding-agent CLI shims
5
5
  Requires-Python: >=3.10
6
6
  Requires-Dist: litellm>=1.0.0
@@ -22,6 +22,7 @@ prompting, session resumption, event parsing, or MCP configuration.
22
22
  - a shared CLI agent abstraction with a provider registry
23
23
  - adapters for Claude Code, Codex, Gemini, and Opencode
24
24
  - stateful chat sessions that automatically resume provider-native threads
25
+ - injectable command executors for custom process launching or sandboxing
25
26
  - MCP server config models for providers that support MCP
26
27
  - sandbox settings helpers for Claude Code
27
28
  - a lightweight LiteLLM client and subagent helper
@@ -152,7 +153,90 @@ agent = CodingAgent(provider="claude")
152
153
  reply = agent.generate("Return only the answer.", silent=True)
153
154
  ```
154
155
 
155
- ### 3. Instantiate a Specific Provider Directly
156
+ ### 3. Run Commands Through a Custom Executor
157
+
158
+ Provider classes accept an optional `executor=`. The executor controls binary
159
+ lookup, CLI validation, and streaming process execution while `agentshim` keeps
160
+ owning provider command construction, stdout/stderr parsing, session state, and
161
+ event emission.
162
+
163
+ This is useful when a caller needs to run the CLI somewhere other than the
164
+ current host process, for example through an existing container, remote shell,
165
+ or custom sandbox.
166
+
167
+ ```python
168
+ from agentshim import (
169
+ CodexCodingAgent,
170
+ CommandHandle,
171
+ CommandRequest,
172
+ CommandResult,
173
+ CommandStreamSink,
174
+ )
175
+
176
+
177
+ class MyCommandHandle:
178
+ def terminate(self) -> None:
179
+ ...
180
+
181
+ def kill(self) -> None:
182
+ ...
183
+
184
+
185
+ class MyExecutor:
186
+ def find_binary(self, binary_name: str, env: dict[str, str]) -> str:
187
+ # This value becomes request.argv[0]. Container or remote executors can
188
+ # return the binary name if lookup happens in the target runtime.
189
+ return binary_name
190
+
191
+ def check_binary(self, binary_path: str, env: dict[str, str], *, timeout: int) -> None:
192
+ # Raise RuntimeError if the target CLI is unavailable. No-op is fine
193
+ # when validation is not cheap or is handled by the runtime.
194
+ return None
195
+
196
+ def run(self, request: CommandRequest, sink: CommandStreamSink) -> CommandResult:
197
+ handle = MyCommandHandle()
198
+ sink.started(handle)
199
+
200
+ stdout = ""
201
+ stderr = ""
202
+
203
+ # Run request.argv in your target runtime, with request.stdin,
204
+ # request.cwd, request.env, and request.timeout. Stream each complete
205
+ # line as it arrives, preserving trailing newlines when present.
206
+ line = "streamed output\n"
207
+ stdout += line
208
+ sink.stdout(line)
209
+
210
+ return CommandResult(returncode=0, stdout=stdout, stderr=stderr)
211
+
212
+
213
+ agent = CodexCodingAgent(executor=MyExecutor())
214
+ ```
215
+
216
+ The default `HostCommandExecutor` preserves the normal local `subprocess`
217
+ behavior. `CodingAgent(provider=..., executor=...)` forwards the same executor
218
+ to the selected provider.
219
+
220
+ Executor contract:
221
+
222
+ - `CommandRequest.argv` is the complete provider CLI command. `argv[0]` is the
223
+ value returned by `find_binary`.
224
+ - `CommandRequest.stdin` is the prompt text to write to the command's standard
225
+ input, then stdin should be closed.
226
+ - `CommandRequest.cwd`, `env`, and `timeout` should be honored by the executor.
227
+ - Call `sink.started(handle)` once after the command starts. The handle only
228
+ needs `terminate()` and `kill()`.
229
+ - Call `sink.stdout(line)` and `sink.stderr(line)` as output is produced. Lines
230
+ should include trailing newlines when the underlying stream provided them.
231
+ - Return `CommandResult(returncode, stdout, stderr)` after the command exits.
232
+ The returned text should match what was streamed through the sink.
233
+
234
+ If you were using the pre-0.5 executor preview, replace
235
+ `run_streaming(cmd, ..., on_stdout, on_stderr, on_process_started)` with
236
+ `run(request, sink)`. The parser/event APIs remain internal; custom executors
237
+ only provide a stable command runtime.
238
+
239
+ ### 4. Instantiate a Specific Provider Directly
156
240
 
157
241
  If you already know which backend you want, construct the provider class
158
242
  yourself.
@@ -174,7 +258,7 @@ The bundled provider classes are:
174
258
  - `GeminiCodingAgent`
175
259
  - `OpencodeCodingAgent`
176
260
 
177
- ### 4. Configure MCP Servers
261
+ ### 5. Configure MCP Servers
178
262
 
179
263
  Claude Code and Codex can be configured with MCP servers by passing
180
264
  `HttpMcpServer` and `StdioMcpServer` objects at construction time.
@@ -251,7 +335,7 @@ Notes:
251
335
  - Registration is import-driven. Your provider is available only after the module defining it has been imported in the current Python process.
252
336
  - `list_providers()` returns canonical provider names only. Aliases resolve via `get_provider_class(...)` and `CodingAgent(provider=...)`.
253
337
  - `register_provider(...)` rejects invalid names, abstract classes, and accidental name collisions unless you pass `overwrite=True`.
254
- - If you want `CodingAgent(...)` to instantiate your provider, its constructor should accept the shared kwargs `model`, `event_handler`, `event_handlers`, `mcp_servers`, and `sandbox` as needed.
338
+ - If you want `CodingAgent(...)` to instantiate your provider, its constructor should accept the shared kwargs `model`, `event_handler`, `event_handlers`, `mcp_servers`, `sandbox`, and `executor` as needed.
255
339
  - If your provider needs extra constructor arguments beyond the shared portable set, pass them via `backend_kwargs={...}` when constructing `CodingAgent(...)`.
256
340
 
257
341
  ## Development
@@ -11,6 +11,7 @@ prompting, session resumption, event parsing, or MCP configuration.
11
11
  - a shared CLI agent abstraction with a provider registry
12
12
  - adapters for Claude Code, Codex, Gemini, and Opencode
13
13
  - stateful chat sessions that automatically resume provider-native threads
14
+ - injectable command executors for custom process launching or sandboxing
14
15
  - MCP server config models for providers that support MCP
15
16
  - sandbox settings helpers for Claude Code
16
17
  - a lightweight LiteLLM client and subagent helper
@@ -141,7 +142,90 @@ agent = CodingAgent(provider="claude")
141
142
  reply = agent.generate("Return only the answer.", silent=True)
142
143
  ```
143
144
 
144
- ### 3. Instantiate a Specific Provider Directly
145
+ ### 3. Run Commands Through a Custom Executor
146
+
147
+ Provider classes accept an optional `executor=`. The executor controls binary
148
+ lookup, CLI validation, and streaming process execution while `agentshim` keeps
149
+ owning provider command construction, stdout/stderr parsing, session state, and
150
+ event emission.
151
+
152
+ This is useful when a caller needs to run the CLI somewhere other than the
153
+ current host process, for example through an existing container, remote shell,
154
+ or custom sandbox.
155
+
156
+ ```python
157
+ from agentshim import (
158
+ CodexCodingAgent,
159
+ CommandHandle,
160
+ CommandRequest,
161
+ CommandResult,
162
+ CommandStreamSink,
163
+ )
164
+
165
+
166
+ class MyCommandHandle:
167
+ def terminate(self) -> None:
168
+ ...
169
+
170
+ def kill(self) -> None:
171
+ ...
172
+
173
+
174
+ class MyExecutor:
175
+ def find_binary(self, binary_name: str, env: dict[str, str]) -> str:
176
+ # This value becomes request.argv[0]. Container or remote executors can
177
+ # return the binary name if lookup happens in the target runtime.
178
+ return binary_name
179
+
180
+ def check_binary(self, binary_path: str, env: dict[str, str], *, timeout: int) -> None:
181
+ # Raise RuntimeError if the target CLI is unavailable. No-op is fine
182
+ # when validation is not cheap or is handled by the runtime.
183
+ return None
184
+
185
+ def run(self, request: CommandRequest, sink: CommandStreamSink) -> CommandResult:
186
+ handle = MyCommandHandle()
187
+ sink.started(handle)
188
+
189
+ stdout = ""
190
+ stderr = ""
191
+
192
+ # Run request.argv in your target runtime, with request.stdin,
193
+ # request.cwd, request.env, and request.timeout. Stream each complete
194
+ # line as it arrives, preserving trailing newlines when present.
195
+ line = "streamed output\n"
196
+ stdout += line
197
+ sink.stdout(line)
198
+
199
+ return CommandResult(returncode=0, stdout=stdout, stderr=stderr)
200
+
201
+
202
+ agent = CodexCodingAgent(executor=MyExecutor())
203
+ ```
204
+
205
+ The default `HostCommandExecutor` preserves the normal local `subprocess`
206
+ behavior. `CodingAgent(provider=..., executor=...)` forwards the same executor
207
+ to the selected provider.
208
+
209
+ Executor contract:
210
+
211
+ - `CommandRequest.argv` is the complete provider CLI command. `argv[0]` is the
212
+ value returned by `find_binary`.
213
+ - `CommandRequest.stdin` is the prompt text to write to the command's standard
214
+ input, then stdin should be closed.
215
+ - `CommandRequest.cwd`, `env`, and `timeout` should be honored by the executor.
216
+ - Call `sink.started(handle)` once after the command starts. The handle only
217
+ needs `terminate()` and `kill()`.
218
+ - Call `sink.stdout(line)` and `sink.stderr(line)` as output is produced. Lines
219
+ should include trailing newlines when the underlying stream provided them.
220
+ - Return `CommandResult(returncode, stdout, stderr)` after the command exits.
221
+ The returned text should match what was streamed through the sink.
222
+
223
+ If you were using the pre-0.5 executor preview, replace
224
+ `run_streaming(cmd, ..., on_stdout, on_stderr, on_process_started)` with
225
+ `run(request, sink)`. The parser/event APIs remain internal; custom executors
226
+ only provide a stable command runtime.
227
+
228
+ ### 4. Instantiate a Specific Provider Directly
145
229
 
146
230
  If you already know which backend you want, construct the provider class
147
231
  yourself.
@@ -163,7 +247,7 @@ The bundled provider classes are:
163
247
  - `GeminiCodingAgent`
164
248
  - `OpencodeCodingAgent`
165
249
 
166
- ### 4. Configure MCP Servers
250
+ ### 5. Configure MCP Servers
167
251
 
168
252
  Claude Code and Codex can be configured with MCP servers by passing
169
253
  `HttpMcpServer` and `StdioMcpServer` objects at construction time.
@@ -240,7 +324,7 @@ Notes:
240
324
  - Registration is import-driven. Your provider is available only after the module defining it has been imported in the current Python process.
241
325
  - `list_providers()` returns canonical provider names only. Aliases resolve via `get_provider_class(...)` and `CodingAgent(provider=...)`.
242
326
  - `register_provider(...)` rejects invalid names, abstract classes, and accidental name collisions unless you pass `overwrite=True`.
243
- - If you want `CodingAgent(...)` to instantiate your provider, its constructor should accept the shared kwargs `model`, `event_handler`, `event_handlers`, `mcp_servers`, and `sandbox` as needed.
327
+ - If you want `CodingAgent(...)` to instantiate your provider, its constructor should accept the shared kwargs `model`, `event_handler`, `event_handlers`, `mcp_servers`, `sandbox`, and `executor` as needed.
244
328
  - If your provider needs extra constructor arguments beyond the shared portable set, pass them via `backend_kwargs={...}` when constructing `CodingAgent(...)`.
245
329
 
246
330
  ## Development
@@ -3,6 +3,15 @@ from .claude import ClaudeCodeCodingAgent
3
3
  from .copilot import CopilotCodingAgent
4
4
  from .codex import CodexCodingAgent
5
5
  from .events import CompositeEventHandler, ConsoleEventHandler, NullEventHandler
6
+ from .executor import (
7
+ CallbackCommandStreamSink,
8
+ CommandExecutor,
9
+ CommandHandle,
10
+ CommandRequest,
11
+ CommandResult,
12
+ CommandStreamSink,
13
+ HostCommandExecutor,
14
+ )
6
15
  from .gemini import GeminiCodingAgent
7
16
  from .mcp_config import HttpMcpServer, McpServerConfig, StdioMcpServer
8
17
  from .opencode import OpencodeCodingAgent
@@ -18,6 +27,13 @@ __all__ = [
18
27
  "CompositeEventHandler",
19
28
  "ConsoleEventHandler",
20
29
  "NullEventHandler",
30
+ "CallbackCommandStreamSink",
31
+ "CommandExecutor",
32
+ "CommandHandle",
33
+ "CommandRequest",
34
+ "CommandResult",
35
+ "CommandStreamSink",
36
+ "HostCommandExecutor",
21
37
  "CopilotCodingAgent",
22
38
  "CodexCodingAgent",
23
39
  "GeminiCodingAgent",
@@ -7,6 +7,7 @@ from collections.abc import Callable, Sequence
7
7
  from typing import Any, TypeVar
8
8
 
9
9
  from agentshim.events import AgentEventHandler
10
+ from agentshim.executor import CommandExecutor, CommandHandle
10
11
  from agentshim.mcp_config import McpServerConfig
11
12
  from agentshim.sandbox import SandboxConfig
12
13
 
@@ -83,7 +84,7 @@ class BaseAgentSession(ABC):
83
84
  cwd: str | None = None,
84
85
  timeout: int | None = None,
85
86
  silent: bool | None = None,
86
- on_process_started: Callable[[Any], None] | None = None,
87
+ on_process_started: Callable[[CommandHandle], None] | None = None,
87
88
  ) -> str:
88
89
  """Send ``prompt`` within an existing chat session."""
89
90
 
@@ -185,7 +186,7 @@ def register_provider(
185
186
  *extra_names: str,
186
187
  aliases: tuple[str, ...] = (),
187
188
  overwrite: bool = False,
188
- ) -> Callable[[type[_T]], _T]:
189
+ ) -> Callable[[type[_T]], type[_T]]:
189
190
  """Decorator to register a coding agent provider.
190
191
 
191
192
  Registration is import-driven: the decorated class becomes available only
@@ -201,7 +202,7 @@ def register_provider(
201
202
  all_aliases = (*extra_names, *aliases)
202
203
  _PROVIDER_REGISTRY._normalize_names(canonical_name, all_aliases)
203
204
 
204
- def decorator(cls: type[_T]) -> _T:
205
+ def decorator(cls: type[_T]) -> type[_T]:
205
206
  return _PROVIDER_REGISTRY.register(
206
207
  cls,
207
208
  canonical_name=canonical_name,
@@ -237,6 +238,7 @@ class CodingAgent(BaseCodingAgent):
237
238
  event_handlers: Sequence[AgentEventHandler] | None = None,
238
239
  mcp_servers: Sequence[McpServerConfig] | None = None,
239
240
  sandbox: bool | SandboxConfig | None = False,
241
+ executor: CommandExecutor | None = None,
240
242
  backend_kwargs: dict[str, Any] | None = None,
241
243
  ) -> None:
242
244
  requested_provider = _PROVIDER_REGISTRY.get_canonical_name(provider)
@@ -254,6 +256,8 @@ class CodingAgent(BaseCodingAgent):
254
256
  portable_kwargs["mcp_servers"] = list(mcp_servers)
255
257
  if sandbox is not None and sandbox is not False:
256
258
  portable_kwargs["sandbox"] = sandbox
259
+ if executor is not None:
260
+ portable_kwargs["executor"] = executor
257
261
 
258
262
  advanced_kwargs = dict(backend_kwargs or {})
259
263
  overlapping_keys = sorted(portable_kwargs.keys() & advanced_kwargs.keys())
@@ -1,12 +1,12 @@
1
1
  import json
2
- import subprocess
3
2
  import time
4
- from collections.abc import Callable, Iterable
3
+ from collections.abc import Callable, Iterable, Sequence
5
4
  from typing import Any
6
5
 
7
6
  from ..base import register_provider
8
7
  from ..cli_agent import CLICodingAgent, CLIGenerationSession
9
8
  from ..events import AgentEventHandler
9
+ from ..executor import CommandExecutor, CommandHandle
10
10
  from ..mcp_config import HttpMcpServer, McpServerConfig
11
11
  from ..sandbox import SandboxConfig, build_claude_sandbox_settings, resolve_sandbox
12
12
  from ..usage import ProviderUsage, TokenUsage
@@ -130,8 +130,9 @@ class ClaudeCodeCodingAgent(CLICodingAgent):
130
130
  model: str | None = None,
131
131
  event_handler: AgentEventHandler | None = None,
132
132
  event_handlers: Iterable[AgentEventHandler] | None = None,
133
- mcp_servers: list[McpServerConfig] | None = None,
133
+ mcp_servers: Sequence[McpServerConfig] | None = None,
134
134
  sandbox: bool | SandboxConfig = False,
135
+ executor: CommandExecutor | None = None,
135
136
  ):
136
137
  """Initialize the Claude Code coding agent.
137
138
 
@@ -145,8 +146,9 @@ class ClaudeCodeCodingAgent(CLICodingAgent):
145
146
  ``--settings``. Only bash subprocess commands are
146
147
  sandboxed; the Claude process itself is not wrapped.
147
148
  Defaults to False (no sandbox).
149
+ executor: Optional command executor for binary lookup and process execution.
148
150
  """
149
- super().__init__("claude", model, event_handler, event_handlers, mcp_servers)
151
+ super().__init__("claude", model, event_handler, event_handlers, mcp_servers, executor=executor)
150
152
  self.sandbox = resolve_sandbox(sandbox)
151
153
  if self.sandbox is not None:
152
154
  # Without this, Claude Code cd's into a per-invocation scratch dir
@@ -223,7 +225,7 @@ class ClaudeCodeCodingAgent(CLICodingAgent):
223
225
  cwd: str | None = None,
224
226
  timeout: int = 300,
225
227
  silent: bool = False,
226
- on_process_started: Callable[[subprocess.Popen[str]], None] | None = None,
228
+ on_process_started: Callable[[CommandHandle], None] | None = None,
227
229
  ) -> ClaudeGenerationSession:
228
230
  return ClaudeGenerationSession(
229
231
  binary_name=self.binary_name,
@@ -235,5 +237,6 @@ class ClaudeCodeCodingAgent(CLICodingAgent):
235
237
  timeout=timeout,
236
238
  silent=silent,
237
239
  event_handler=self.event_handler,
240
+ executor=self.executor,
238
241
  on_process_started=on_process_started,
239
242
  )