agent-shell-py 0.2.4__tar.gz → 0.3.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.
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/AGENTS.md +64 -2
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/PKG-INFO +53 -6
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/README.md +51 -4
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/docs/development/agent_parameter_comparison.md +7 -7
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/docs/development/info.md +3 -2
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/skills/invoking-cli-agents/SKILL.md +47 -4
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/skills/invoking-cli-agents/api-reference.md +50 -4
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/src/agent_shell/_version.py +2 -2
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/src/agent_shell/adapters/claude_code_adapter.py +35 -20
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/src/agent_shell/adapters/codex_adapter.py +33 -16
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/src/agent_shell/adapters/copilot_cli_adapter.py +37 -21
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/src/agent_shell/adapters/cursor_adapter.py +201 -23
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/src/agent_shell/adapters/grok_adapter.py +33 -18
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/src/agent_shell/adapters/opencode_adapter.py +34 -17
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/src/agent_shell/adapters/pi_adapter.py +34 -17
- agent_shell_py-0.3.0/src/agent_shell/adapters/process_failure.py +35 -0
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/src/agent_shell/adapters/response.py +6 -0
- agent_shell_py-0.3.0/src/agent_shell/execution.py +341 -0
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/src/agent_shell/models/agent.py +10 -1
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/src/agent_shell/process_cleanup.py +39 -20
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/src/agent_shell/shell.py +32 -7
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/tests/e2e/test_cursor_e2e.py +14 -4
- agent_shell_py-0.3.0/tests/e2e/test_execution_host_e2e.py +46 -0
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/tests/e2e/test_health_check_e2e.py +2 -3
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/tests/integration/test_copilot_cli_integration.py +13 -0
- agent_shell_py-0.3.0/tests/integration/test_cursor_mcp_integration.py +367 -0
- agent_shell_py-0.3.0/tests/integration/test_execution_host.py +310 -0
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/tests/integration/test_process_lifecycle.py +104 -14
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/tests/unit/test_cancel.py +4 -6
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/tests/unit/test_codex_cancel.py +4 -6
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/tests/unit/test_copilot_cli_cancel.py +4 -6
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/tests/unit/test_cursor_cancel.py +4 -6
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/tests/unit/test_grok_cancel.py +4 -6
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/tests/unit/test_opencode_cancel.py +4 -6
- agent_shell_py-0.3.0/tests/unit/test_pi_cancel.py +19 -0
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/tests/unit/test_process_cleanup.py +32 -36
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/tests/unit/test_process_group_registration.py +7 -5
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/tests/unit/test_shell.py +32 -5
- agent_shell_py-0.2.4/tests/integration/test_cursor_mcp_integration.py +0 -36
- agent_shell_py-0.2.4/tests/unit/test_pi_cancel.py +0 -21
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/.github/workflows/build.yml +0 -0
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/.github/workflows/ci.yml +0 -0
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/.github/workflows/publish.yml +0 -0
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/.gitignore +0 -0
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/.python-version +0 -0
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/LICENSE +0 -0
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/docs/assets/skill_banner.png +0 -0
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/docs/development/disabled_tools.md +0 -0
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/docs/development/total_token_count.md +0 -0
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/pyproject.toml +0 -0
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/skills/delegating-code-review/SKILL.md +0 -0
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/src/agent_shell/__init__.py +0 -0
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/src/agent_shell/adapters/__init__.py +0 -0
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/src/agent_shell/adapters/agent_adapter_protocol.py +0 -0
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/src/agent_shell/adapters/health.py +0 -0
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/src/agent_shell/adapters/model_discovery.py +0 -0
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/src/agent_shell/adapters/outcome.py +0 -0
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/src/agent_shell/adapters/stderr_format.py +0 -0
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/src/agent_shell/adapters/tool_denial.py +0 -0
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/src/agent_shell/models/__init__.py +0 -0
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/tests/__init__.py +0 -0
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/tests/conftest.py +0 -0
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/tests/e2e/__init__.py +0 -0
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/tests/e2e/test_claude_code_e2e.py +0 -0
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/tests/e2e/test_codex_e2e.py +0 -0
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/tests/e2e/test_copilot_cli_e2e.py +0 -0
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/tests/e2e/test_grok_e2e.py +0 -0
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/tests/e2e/test_model_discovery_e2e.py +0 -0
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/tests/e2e/test_opencode_e2e.py +0 -0
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/tests/e2e/test_pi_e2e.py +0 -0
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/tests/integration/__init__.py +0 -0
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/tests/integration/test_claude_code_integration.py +0 -0
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/tests/integration/test_claude_code_mcp_integration.py +0 -0
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/tests/integration/test_codex_integration.py +0 -0
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/tests/integration/test_codex_mcp_integration.py +0 -0
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/tests/integration/test_copilot_cli_mcp_integration.py +0 -0
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/tests/integration/test_cursor_integration.py +0 -0
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/tests/integration/test_grok_integration.py +0 -0
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/tests/integration/test_grok_mcp_integration.py +0 -0
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/tests/integration/test_health_check_integration.py +0 -0
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/tests/integration/test_model_discovery_integration.py +0 -0
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/tests/integration/test_opencode_integration.py +0 -0
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/tests/integration/test_opencode_mcp_integration.py +0 -0
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/tests/integration/test_pi_integration.py +0 -0
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/tests/integration/test_pi_mcp_integration.py +0 -0
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/tests/unit/__init__.py +0 -0
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/tests/unit/adapter_matrix.py +0 -0
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/tests/unit/codex_fixtures.py +0 -0
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/tests/unit/copilot_fixtures.py +0 -0
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/tests/unit/cursor_fixtures.py +0 -0
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/tests/unit/fixtures.py +0 -0
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/tests/unit/grok_fixtures.py +0 -0
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/tests/unit/opencode_fixtures.py +0 -0
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/tests/unit/pi_fixtures.py +0 -0
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/tests/unit/test_adapter_transport.py +0 -0
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/tests/unit/test_codex_execute.py +0 -0
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/tests/unit/test_codex_parse_event.py +0 -0
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/tests/unit/test_codex_warnings.py +0 -0
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/tests/unit/test_copilot_cli_execute.py +0 -0
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/tests/unit/test_copilot_cli_parse_event.py +0 -0
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/tests/unit/test_copilot_cli_stream.py +0 -0
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/tests/unit/test_cursor_execute.py +0 -0
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/tests/unit/test_cursor_parse_event.py +0 -0
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/tests/unit/test_cursor_warnings.py +0 -0
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/tests/unit/test_execute.py +0 -0
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/tests/unit/test_execute_outcome.py +0 -0
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/tests/unit/test_grok_execute.py +0 -0
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/tests/unit/test_grok_parse_event.py +0 -0
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/tests/unit/test_grok_warnings.py +0 -0
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/tests/unit/test_health_probe.py +0 -0
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/tests/unit/test_mcp_server_spec.py +0 -0
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/tests/unit/test_model_discovery.py +0 -0
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/tests/unit/test_models.py +0 -0
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/tests/unit/test_opencode_execute.py +0 -0
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/tests/unit/test_opencode_parse_event.py +0 -0
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/tests/unit/test_opencode_spawn.py +0 -0
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/tests/unit/test_opencode_stream.py +0 -0
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/tests/unit/test_parse_event.py +0 -0
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/tests/unit/test_pi_execute.py +0 -0
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/tests/unit/test_pi_parse_event.py +0 -0
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/tests/unit/test_pi_warnings.py +0 -0
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/tests/unit/test_response_aggregation.py +0 -0
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/tests/unit/test_shell_cancellation.py +0 -0
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/tests/unit/test_shell_mcp.py +0 -0
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/tests/unit/test_stderr_format.py +0 -0
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/tests/unit/test_stream.py +0 -0
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/tests/unit/test_tool_denial.py +0 -0
- {agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/uv.lock +0 -0
|
@@ -8,6 +8,8 @@ A lightweight, async Python package that executes CLI coding agents headlessly a
|
|
|
8
8
|
classDiagram
|
|
9
9
|
class AgentShell {
|
|
10
10
|
-AgentAdapter _adapter
|
|
11
|
+
+ExecutionHost execution_host
|
|
12
|
+
+IsolationPolicy isolation_policy
|
|
11
13
|
+execute(cwd, prompt, ...) AgentResponse
|
|
12
14
|
+stream(cwd, prompt, ...) AsyncIterator~StreamEvent~
|
|
13
15
|
+health_check(cwd, model, timeout) HealthCheckResult
|
|
@@ -49,6 +51,8 @@ classDiagram
|
|
|
49
51
|
+str session_id
|
|
50
52
|
+float duration
|
|
51
53
|
+int output_tokens
|
|
54
|
+
+int returncode
|
|
55
|
+
+int signal
|
|
52
56
|
}
|
|
53
57
|
|
|
54
58
|
class MCPServerSpec {
|
|
@@ -88,8 +92,39 @@ classDiagram
|
|
|
88
92
|
+str session_id
|
|
89
93
|
+int output_tokens
|
|
90
94
|
+str error
|
|
95
|
+
+int returncode
|
|
96
|
+
+int signal
|
|
91
97
|
}
|
|
92
98
|
|
|
99
|
+
class ExecutionHost {
|
|
100
|
+
<<Protocol>>
|
|
101
|
+
+launch(command, cwd, env, stdin, isolation_policy) RunHandle
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
class NativeExecutionHost {
|
|
105
|
+
+launch(command, cwd, env, stdin, isolation_policy) NativeRunHandle
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
class IsolationPolicy {
|
|
109
|
+
<<Protocol>>
|
|
110
|
+
+prepare(command, env) PreparedLaunch
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
class NoIsolation
|
|
114
|
+
class LinuxPidNamespaceIsolation
|
|
115
|
+
|
|
116
|
+
class RunHandle {
|
|
117
|
+
<<Protocol>>
|
|
118
|
+
+int pid
|
|
119
|
+
+int returncode
|
|
120
|
+
+wait() int
|
|
121
|
+
+communicate(input) tuple
|
|
122
|
+
+cancel() None
|
|
123
|
+
+release() None
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
class NativeRunHandle
|
|
127
|
+
|
|
93
128
|
class AgentType {
|
|
94
129
|
<<StrEnum>>
|
|
95
130
|
CLAUDE_CODE
|
|
@@ -98,9 +133,17 @@ classDiagram
|
|
|
98
133
|
CODEX
|
|
99
134
|
PI
|
|
100
135
|
CURSOR
|
|
136
|
+
GROK
|
|
101
137
|
}
|
|
102
138
|
|
|
103
139
|
AgentShell --> AgentAdapter : delegates to
|
|
140
|
+
AgentShell --> ExecutionHost : selects
|
|
141
|
+
AgentShell --> IsolationPolicy : selects
|
|
142
|
+
NativeExecutionHost ..|> ExecutionHost : satisfies
|
|
143
|
+
NativeExecutionHost --> NativeRunHandle : creates
|
|
144
|
+
NativeRunHandle ..|> RunHandle : satisfies
|
|
145
|
+
NoIsolation ..|> IsolationPolicy : satisfies
|
|
146
|
+
LinuxPidNamespaceIsolation ..|> IsolationPolicy : satisfies
|
|
104
147
|
AgentShell --> AgentType : resolves via
|
|
105
148
|
ClaudeCodeAdapter ..|> AgentAdapter : satisfies
|
|
106
149
|
AgentShell ..> AgentResponse : returns on success
|
|
@@ -112,7 +155,16 @@ classDiagram
|
|
|
112
155
|
ClaudeCodeAdapter ..> StreamEvent : parses NDJSON into
|
|
113
156
|
```
|
|
114
157
|
|
|
115
|
-
The adapter pattern uses Python's `Protocol` (structural typing) rather than ABC, so adapters satisfy the contract implicitly without inheritance. Each adapter
|
|
158
|
+
The adapter pattern uses Python's `Protocol` (structural typing) rather than ABC, so adapters satisfy the contract implicitly without inheritance. Each adapter translates agent-specific CLI flags and NDJSON output into the shared `StreamEvent`/`AgentResponse` models, while the selected `ExecutionHost` owns process creation and returns a per-run `RunHandle`.
|
|
159
|
+
|
|
160
|
+
Execution location and protection are separate axes. Existing callers default to
|
|
161
|
+
`NativeExecutionHost()` plus `NoIsolation()`. `LinuxPidNamespaceIsolation` is an opt-in direct
|
|
162
|
+
signal boundary: a tiny init/reaper is PID 1 and the CLI is PID 2 or later, so child-namespace
|
|
163
|
+
processes cannot see or signal AgentShell's ancestors. It requires Linux, `unshare`, and enabled
|
|
164
|
+
unprivileged user/PID namespaces; an unavailable explicit request raises
|
|
165
|
+
`IsolationUnavailableError` and never falls back. This is not a general sandbox and does not
|
|
166
|
+
restrict filesystem, credentials, network, tools, or resources. The host/policy applies to
|
|
167
|
+
`execute()`, `stream()`, and `health_check()`; model discovery and MCP configuration remain local.
|
|
116
168
|
|
|
117
169
|
`output_tokens` is a cost measure — the billed output-token count, which **includes reasoning tokens** (billed at the output rate). Each adapter normalises this so the value is consistent across agents (e.g. OpenCode reports reasoning in a sibling field, so its adapter adds it back).
|
|
118
170
|
|
|
@@ -171,9 +223,19 @@ All adapters write to user-scope configuration:
|
|
|
171
223
|
| OpenCode | direct JSON file write | `~/.config/opencode/opencode.json` |
|
|
172
224
|
| Copilot CLI | direct JSON file write | `~/.copilot/mcp-config.json` |
|
|
173
225
|
| Codex | `codex mcp add` subprocess | Codex config |
|
|
226
|
+
| Cursor | direct JSON file write | `~/.cursor/mcp.json` |
|
|
174
227
|
| Grok | `grok mcp add --scope user` subprocess | `~/.grok/config.toml` (managed by CLI) |
|
|
175
228
|
|
|
176
|
-
Adds are idempotent (
|
|
229
|
+
Adds are idempotent (update existing entries with the same name). Cursor preserves native fields
|
|
230
|
+
that `MCPServerSpec` cannot represent when an update keeps the same transport, and writes its file
|
|
231
|
+
atomically with user-only permissions. Removes warn rather than raise when the named server is not
|
|
232
|
+
found. Claude Code listing reads the user-scope `mcpServers`
|
|
233
|
+
entries from `~/.claude.json` directly, avoiding the health checks and human-readable output of
|
|
234
|
+
`claude mcp list`. Cursor manages user-scope MCP entries directly in `~/.cursor/mcp.json` because
|
|
235
|
+
its `mcp` subcommands have no add/remove commands. Grok listing reads user-scope `mcp_servers`
|
|
236
|
+
entries from `~/.grok/config.toml` directly for the same reason. Pi's MCP add/remove/list methods
|
|
237
|
+
raise `NotImplementedError`. Pi manages capability via `pi install` extensions, which needs
|
|
238
|
+
investigation before wiring up.
|
|
177
239
|
|
|
178
240
|
## Test Philosophy
|
|
179
241
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: agent-shell-py
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: A lightweight abstraction for executing CLI coding agents headlessly
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -19,6 +19,8 @@ and returning the output that can be used programatically as a unified contract
|
|
|
19
19
|
behind a common adapter protocol.
|
|
20
20
|
- **Execute or stream** — get one `AgentResponse` (raises `AgentExecutionError` on a failed run),
|
|
21
21
|
or async-iterate normalized `StreamEvent`s with optional thinking/reasoning.
|
|
22
|
+
- **Composable execution policy** — preserve native execution by default, or opt into Linux PID
|
|
23
|
+
namespace isolation without changing an agent adapter.
|
|
22
24
|
- **Session resumption** — continue any conversation by passing back its `session_id`.
|
|
23
25
|
- **Normalized cost & tokens** — consistent `cost` and `output_tokens` (reasoning included)
|
|
24
26
|
regardless of how each CLI reports them.
|
|
@@ -79,6 +81,40 @@ separately.
|
|
|
79
81
|
|
|
80
82
|
## Examples
|
|
81
83
|
|
|
84
|
+
### Execution host and isolation
|
|
85
|
+
|
|
86
|
+
Existing callers remain unchanged. Omitting both settings means
|
|
87
|
+
`NativeExecutionHost()` plus `NoIsolation()`:
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
shell = AgentShell(agent_type=AgentType.CLAUDE_CODE)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
To protect the AgentShell owner from broad same-user cleanup commands such as `pkill -f`,
|
|
94
|
+
explicitly request Linux PID namespace isolation:
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
from agent_shell.execution import LinuxPidNamespaceIsolation
|
|
98
|
+
|
|
99
|
+
shell = AgentShell(
|
|
100
|
+
agent_type=AgentType.CLAUDE_CODE,
|
|
101
|
+
isolation_policy=LinuxPidNamespaceIsolation(),
|
|
102
|
+
)
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
The policy runs a tiny namespace init as PID 1 and the real CLI as PID 2 or later. Processes in
|
|
106
|
+
that child namespace cannot see or signal AgentShell's ancestor processes. The feature requires
|
|
107
|
+
Linux, the `unshare` command, and kernel support for unprivileged user/PID namespaces. If any are
|
|
108
|
+
unavailable, launching raises `IsolationUnavailableError`; it never silently falls back.
|
|
109
|
+
|
|
110
|
+
This is **direct-signal protection, not a sandbox**. It does not restrict files, credentials,
|
|
111
|
+
network access, tools, or resource consumption. Any background descendants are also terminated
|
|
112
|
+
when the isolated namespace ends. `execute()`, `stream()`, and `health_check()` use the selected
|
|
113
|
+
host/policy; model discovery and MCP configuration remain local management operations.
|
|
114
|
+
|
|
115
|
+
`NativeExecutionHost` is currently the only host implementation. Host and isolation are separate
|
|
116
|
+
so future tmux/Herdr hosts can compose with policies without creating one class per combination.
|
|
117
|
+
|
|
82
118
|
### Execute
|
|
83
119
|
|
|
84
120
|
```python
|
|
@@ -117,7 +153,9 @@ follow_up = await shell.execute(
|
|
|
117
153
|
`execute()` raises `AgentExecutionError` instead of returning when a run failed — an `error`
|
|
118
154
|
event was emitted, the terminal `result` had `content == "error"`, or no terminal `result`
|
|
119
155
|
arrived at all. `str(e)` is the bare reason; the exception also carries whatever partial
|
|
120
|
-
`response`/`cost`/`session_id`/`duration`/`output_tokens` the run produced before failing.
|
|
156
|
+
`response`/`cost`/`session_id`/`duration`/`output_tokens` the run produced before failing. When
|
|
157
|
+
the CLI process itself exits unsuccessfully, `returncode` is also populated; signal termination
|
|
158
|
+
uses Python's negative-returncode convention and supplies the positive signal number separately.
|
|
121
159
|
|
|
122
160
|
```python
|
|
123
161
|
from agent_shell.models.agent import AgentExecutionError
|
|
@@ -126,6 +164,7 @@ try:
|
|
|
126
164
|
response = await shell.execute(cwd="/path/to/project", prompt="Fix the failing test")
|
|
127
165
|
except AgentExecutionError as e:
|
|
128
166
|
print(f"run failed: {e}") # e.g. "500 model name=qwen3.6-27b-8Q failed to load"
|
|
167
|
+
print(e.returncode, e.signal) # e.g. -15, 15 for SIGTERM; otherwise None when unavailable
|
|
129
168
|
```
|
|
130
169
|
|
|
131
170
|
### Stream
|
|
@@ -276,8 +315,9 @@ print(f"Session: {response.session_id}")
|
|
|
276
315
|
> tools are auto-*rejected* but the run still completes. `allowed_tools`, `effort`, and
|
|
277
316
|
> `disallowed_tools` are **ignored** — Cursor exposes no per-call tool policy or effort flag
|
|
278
317
|
> (tool policy lives in `.cursor/cli.json`), so each emits a `UserWarning`. On a Free plan
|
|
279
|
-
> only `model=None`/`"auto"` works. MCP
|
|
280
|
-
> `
|
|
318
|
+
> only `model=None`/`"auto"` works. MCP add/remove/list are supported by directly managing
|
|
319
|
+
> the user-scope `~/.cursor/mcp.json` file because `cursor-agent mcp` has no add/remove
|
|
320
|
+
> subcommands.
|
|
281
321
|
|
|
282
322
|
### Grok
|
|
283
323
|
|
|
@@ -348,7 +388,14 @@ await shell.add_mcp_server(MCPServerSpec(
|
|
|
348
388
|
))
|
|
349
389
|
```
|
|
350
390
|
|
|
351
|
-
`add_mcp_server`
|
|
391
|
+
`add_mcp_server` adds or updates a server with the same name. Cursor preserves native fields
|
|
392
|
+
that `MCPServerSpec` cannot represent when an update keeps the same transport. The configuration
|
|
393
|
+
is written atomically with user-only permissions. `remove_mcp_server` warns rather than raises
|
|
394
|
+
when the named server is not found. `list_mcp_servers()` works for
|
|
395
|
+
Claude Code, OpenCode, Copilot CLI, Codex, Cursor, and Grok. Claude Code reads user-scope
|
|
396
|
+
entries directly from `~/.claude.json`, Cursor from `~/.cursor/mcp.json`, and Grok from
|
|
397
|
+
`~/.grok/config.toml`, so listing does not launch configured servers for health checks. MCP
|
|
398
|
+
is not supported for Pi; all three MCP methods raise `NotImplementedError`.
|
|
352
399
|
|
|
353
400
|
## Logging
|
|
354
401
|
|
|
@@ -10,6 +10,8 @@ and returning the output that can be used programatically as a unified contract
|
|
|
10
10
|
behind a common adapter protocol.
|
|
11
11
|
- **Execute or stream** — get one `AgentResponse` (raises `AgentExecutionError` on a failed run),
|
|
12
12
|
or async-iterate normalized `StreamEvent`s with optional thinking/reasoning.
|
|
13
|
+
- **Composable execution policy** — preserve native execution by default, or opt into Linux PID
|
|
14
|
+
namespace isolation without changing an agent adapter.
|
|
13
15
|
- **Session resumption** — continue any conversation by passing back its `session_id`.
|
|
14
16
|
- **Normalized cost & tokens** — consistent `cost` and `output_tokens` (reasoning included)
|
|
15
17
|
regardless of how each CLI reports them.
|
|
@@ -70,6 +72,40 @@ separately.
|
|
|
70
72
|
|
|
71
73
|
## Examples
|
|
72
74
|
|
|
75
|
+
### Execution host and isolation
|
|
76
|
+
|
|
77
|
+
Existing callers remain unchanged. Omitting both settings means
|
|
78
|
+
`NativeExecutionHost()` plus `NoIsolation()`:
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
shell = AgentShell(agent_type=AgentType.CLAUDE_CODE)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
To protect the AgentShell owner from broad same-user cleanup commands such as `pkill -f`,
|
|
85
|
+
explicitly request Linux PID namespace isolation:
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
from agent_shell.execution import LinuxPidNamespaceIsolation
|
|
89
|
+
|
|
90
|
+
shell = AgentShell(
|
|
91
|
+
agent_type=AgentType.CLAUDE_CODE,
|
|
92
|
+
isolation_policy=LinuxPidNamespaceIsolation(),
|
|
93
|
+
)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
The policy runs a tiny namespace init as PID 1 and the real CLI as PID 2 or later. Processes in
|
|
97
|
+
that child namespace cannot see or signal AgentShell's ancestor processes. The feature requires
|
|
98
|
+
Linux, the `unshare` command, and kernel support for unprivileged user/PID namespaces. If any are
|
|
99
|
+
unavailable, launching raises `IsolationUnavailableError`; it never silently falls back.
|
|
100
|
+
|
|
101
|
+
This is **direct-signal protection, not a sandbox**. It does not restrict files, credentials,
|
|
102
|
+
network access, tools, or resource consumption. Any background descendants are also terminated
|
|
103
|
+
when the isolated namespace ends. `execute()`, `stream()`, and `health_check()` use the selected
|
|
104
|
+
host/policy; model discovery and MCP configuration remain local management operations.
|
|
105
|
+
|
|
106
|
+
`NativeExecutionHost` is currently the only host implementation. Host and isolation are separate
|
|
107
|
+
so future tmux/Herdr hosts can compose with policies without creating one class per combination.
|
|
108
|
+
|
|
73
109
|
### Execute
|
|
74
110
|
|
|
75
111
|
```python
|
|
@@ -108,7 +144,9 @@ follow_up = await shell.execute(
|
|
|
108
144
|
`execute()` raises `AgentExecutionError` instead of returning when a run failed — an `error`
|
|
109
145
|
event was emitted, the terminal `result` had `content == "error"`, or no terminal `result`
|
|
110
146
|
arrived at all. `str(e)` is the bare reason; the exception also carries whatever partial
|
|
111
|
-
`response`/`cost`/`session_id`/`duration`/`output_tokens` the run produced before failing.
|
|
147
|
+
`response`/`cost`/`session_id`/`duration`/`output_tokens` the run produced before failing. When
|
|
148
|
+
the CLI process itself exits unsuccessfully, `returncode` is also populated; signal termination
|
|
149
|
+
uses Python's negative-returncode convention and supplies the positive signal number separately.
|
|
112
150
|
|
|
113
151
|
```python
|
|
114
152
|
from agent_shell.models.agent import AgentExecutionError
|
|
@@ -117,6 +155,7 @@ try:
|
|
|
117
155
|
response = await shell.execute(cwd="/path/to/project", prompt="Fix the failing test")
|
|
118
156
|
except AgentExecutionError as e:
|
|
119
157
|
print(f"run failed: {e}") # e.g. "500 model name=qwen3.6-27b-8Q failed to load"
|
|
158
|
+
print(e.returncode, e.signal) # e.g. -15, 15 for SIGTERM; otherwise None when unavailable
|
|
120
159
|
```
|
|
121
160
|
|
|
122
161
|
### Stream
|
|
@@ -267,8 +306,9 @@ print(f"Session: {response.session_id}")
|
|
|
267
306
|
> tools are auto-*rejected* but the run still completes. `allowed_tools`, `effort`, and
|
|
268
307
|
> `disallowed_tools` are **ignored** — Cursor exposes no per-call tool policy or effort flag
|
|
269
308
|
> (tool policy lives in `.cursor/cli.json`), so each emits a `UserWarning`. On a Free plan
|
|
270
|
-
> only `model=None`/`"auto"` works. MCP
|
|
271
|
-
> `
|
|
309
|
+
> only `model=None`/`"auto"` works. MCP add/remove/list are supported by directly managing
|
|
310
|
+
> the user-scope `~/.cursor/mcp.json` file because `cursor-agent mcp` has no add/remove
|
|
311
|
+
> subcommands.
|
|
272
312
|
|
|
273
313
|
### Grok
|
|
274
314
|
|
|
@@ -339,7 +379,14 @@ await shell.add_mcp_server(MCPServerSpec(
|
|
|
339
379
|
))
|
|
340
380
|
```
|
|
341
381
|
|
|
342
|
-
`add_mcp_server`
|
|
382
|
+
`add_mcp_server` adds or updates a server with the same name. Cursor preserves native fields
|
|
383
|
+
that `MCPServerSpec` cannot represent when an update keeps the same transport. The configuration
|
|
384
|
+
is written atomically with user-only permissions. `remove_mcp_server` warns rather than raises
|
|
385
|
+
when the named server is not found. `list_mcp_servers()` works for
|
|
386
|
+
Claude Code, OpenCode, Copilot CLI, Codex, Cursor, and Grok. Claude Code reads user-scope
|
|
387
|
+
entries directly from `~/.claude.json`, Cursor from `~/.cursor/mcp.json`, and Grok from
|
|
388
|
+
`~/.grok/config.toml`, so listing does not launch configured servers for health checks. MCP
|
|
389
|
+
is not supported for Pi; all three MCP methods raise `NotImplementedError`.
|
|
343
390
|
|
|
344
391
|
## Logging
|
|
345
392
|
|
{agent_shell_py-0.2.4 → agent_shell_py-0.3.0}/docs/development/agent_parameter_comparison.md
RENAMED
|
@@ -128,9 +128,9 @@ test checks all seven real discovery commands.
|
|
|
128
128
|
- **Headless mode**: `-p` / `--prompt` for one-shot; `--acp` for Agent Client Protocol
|
|
129
129
|
- **Model**: `--model <model>`; use `auto` to let Copilot choose
|
|
130
130
|
- **Effort**: `--effort` / `--reasoning-effort` with choices
|
|
131
|
-
`none|minimal|low|medium|high|xhigh|max`. AgentShell
|
|
132
|
-
|
|
133
|
-
|
|
131
|
+
`none|minimal|low|medium|high|xhigh|max`. AgentShell treats `None` and `""` as omitted, so
|
|
132
|
+
Copilot uses its default. Explicit values, including `"none"`, are accepted case-insensitively,
|
|
133
|
+
validated against these choices, and passed to Copilot via `--effort` in lowercase.
|
|
134
134
|
- **Allowed tools**: `--allow-tool`, `--deny-tool`, `--available-tools`,
|
|
135
135
|
`--excluded-tools`, `--allow-all-tools`
|
|
136
136
|
- **Auto-approve**: `--allow-all-tools`; `--allow-all` / `--yolo` also grant path and URL
|
|
@@ -193,10 +193,10 @@ test checks all seven real discovery commands.
|
|
|
193
193
|
cursor-agent ACCEPTS an unknown id — `--resume=<never-seen-uuid>` starts a session under that
|
|
194
194
|
id and echoes it back rather than failing, so id identity proves the flag was passed through
|
|
195
195
|
and honoured, not that a prior transcript was replayed (measured 2026-07-26)
|
|
196
|
-
- **MCP**: `cursor-agent mcp` = login/list/list-tools/enable/disable only (no add/remove)
|
|
197
|
-
servers
|
|
198
|
-
|
|
199
|
-
|
|
196
|
+
- **MCP**: `cursor-agent mcp` = login/list/list-tools/enable/disable only (no add/remove).
|
|
197
|
+
The adapter manages user-scope servers directly in `~/.cursor/mcp.json`, which provides the
|
|
198
|
+
transport details that `mcp list` omits. Writes are atomic and user-only; same-transport
|
|
199
|
+
updates preserve Cursor-native fields that `MCPServerSpec` cannot represent.
|
|
200
200
|
- **Usage**: the terminal `result` event carries `usage.outputTokens` (undocumented but real)
|
|
201
201
|
and `duration_ms`; there is no cost field, so `cost` is `0.0`
|
|
202
202
|
|
|
@@ -61,8 +61,9 @@ uv run pytest tests/e2e -v
|
|
|
61
61
|
|
|
62
62
|
> [!WARNING]
|
|
63
63
|
> E2E tests may mutate real user configuration files. MCP tests can call an agent's real
|
|
64
|
-
>
|
|
65
|
-
> `~/.config/opencode/opencode.json`, `~/.copilot/mcp-config.json`,
|
|
64
|
+
> MCP commands or edit its config directly, affecting files such as `~/.claude.json`,
|
|
65
|
+
> `~/.config/opencode/opencode.json`, `~/.copilot/mcp-config.json`, `~/.cursor/mcp.json`,
|
|
66
|
+
> or Codex configuration.
|
|
66
67
|
> Tests use unique names and `finally` cleanup where implemented, but forced termination,
|
|
67
68
|
> a CLI crash, or a machine failure can prevent cleanup. The CLI may also rewrite config
|
|
68
69
|
> formatting even when the temporary entry is removed. Review the selected E2E test before
|
|
@@ -6,7 +6,7 @@ description: >-
|
|
|
6
6
|
restricting tools, or checking agent/model health. Supports Claude Code, OpenCode,
|
|
7
7
|
Copilot CLI, Codex, Pi, Cursor, and Grok. Keywords: AgentShell, list_models, headless
|
|
8
8
|
agent, model discovery, subprocess, allowed_tools, disallowed_tools, session_id, cost,
|
|
9
|
-
output_tokens.
|
|
9
|
+
output_tokens, execution host, PID namespace, isolation policy.
|
|
10
10
|
---
|
|
11
11
|
|
|
12
12
|
# Invoking CLI Agents with AgentShell
|
|
@@ -44,7 +44,44 @@ uv add agent-shell-py
|
|
|
44
44
|
|
|
45
45
|
AgentShell has two invocation methods — `execute()` collects a complete response, `stream()`
|
|
46
46
|
yields events in real-time — plus helpers for model discovery, health checks, and MCP server
|
|
47
|
-
management. All are async.
|
|
47
|
+
management. All are async. Invocation has three independent choices:
|
|
48
|
+
|
|
49
|
+
- `agent_type`: which CLI (Claude Code, Codex, etc.)
|
|
50
|
+
- `execution_host`: where/how the process is owned (`NativeExecutionHost` today)
|
|
51
|
+
- `isolation_policy`: what protection surrounds it (`NoIsolation` or Linux PID isolation)
|
|
52
|
+
|
|
53
|
+
### Execution Host and Isolation Policy
|
|
54
|
+
|
|
55
|
+
Existing code is backward-compatible: omitting both execution settings selects native execution
|
|
56
|
+
with no isolation.
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
shell = AgentShell(agent_type=AgentType.CODEX)
|
|
60
|
+
# Equivalent to NativeExecutionHost() + NoIsolation()
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Opt into direct-signal protection when an agent may run broad same-user cleanup commands such as
|
|
64
|
+
`pkill -f`:
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
from agent_shell.execution import LinuxPidNamespaceIsolation
|
|
68
|
+
|
|
69
|
+
shell = AgentShell(
|
|
70
|
+
agent_type=AgentType.CODEX,
|
|
71
|
+
isolation_policy=LinuxPidNamespaceIsolation(),
|
|
72
|
+
)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
The Linux policy puts a tiny init/reaper at namespace PID 1 and the actual CLI at PID 2 or later.
|
|
76
|
+
The CLI cannot see or signal AgentShell's ancestor processes. It requires Linux, `unshare`, and
|
|
77
|
+
kernel support for unprivileged user/PID namespaces. An unavailable requested policy raises
|
|
78
|
+
`IsolationUnavailableError` before the CLI starts; AgentShell never silently falls back.
|
|
79
|
+
|
|
80
|
+
This policy is **not a sandbox**: filesystem, credentials, network, agent tools, and resource use
|
|
81
|
+
remain available. Background descendants cannot outlive the isolated namespace. It applies to
|
|
82
|
+
`execute()`, `stream()`, and `health_check()`; `list_models()` and MCP configuration are local
|
|
83
|
+
management operations. `NativeExecutionHost` is the only shipped host today. Tmux and Herdr are
|
|
84
|
+
future host possibilities, not current APIs.
|
|
48
85
|
|
|
49
86
|
### Discover Available Model Strings
|
|
50
87
|
|
|
@@ -117,6 +154,7 @@ async for event in shell.stream(
|
|
|
117
154
|
print(event.content)
|
|
118
155
|
elif event.type == "error":
|
|
119
156
|
print(f"[error] {event.content}")
|
|
157
|
+
print(event.returncode, event.signal) # populated for process-level failures
|
|
120
158
|
elif event.type == "result":
|
|
121
159
|
print(f"Done ({event.content}). Cost: ${event.cost:.4f}, {event.output_tokens} tok")
|
|
122
160
|
```
|
|
@@ -187,7 +225,7 @@ Capabilities differ by agent. `output_tokens` is populated on all of them; the r
|
|
|
187
225
|
| Copilot CLI | ✅ | ⚠️ `bash`, `edit` only | ✅ | ❌ `0.0` | ✅ real | ✅ |
|
|
188
226
|
| Codex | ❌ | ⚠️ `web_search` only | ✅ | ❌ `0.0` | ❌ `0.0` | ✅ |
|
|
189
227
|
| Pi | ✅ | ⚠️ `bash`, `edit`, `read` | ✅ | ⚠️ paid providers only | ❌ `0.0` | ❌ raises |
|
|
190
|
-
| Cursor | ❌ warns | ❌ none — warns | ❌ warns | ❌ `0.0` | ✅ real |
|
|
228
|
+
| Cursor | ❌ warns | ❌ none — warns | ❌ warns | ❌ `0.0` | ✅ real | ✅ user-scope |
|
|
191
229
|
| Grok | ✅ | ✅ all canonical | ✅ | ⚠️ may be `0.0` | ✅ real | ✅ user-scope |
|
|
192
230
|
|
|
193
231
|
A `✅` for `allowed_tools` means the flag is passed — but it only *enforces* with
|
|
@@ -271,6 +309,7 @@ except AgentExecutionError as e:
|
|
|
271
309
|
print(f"failed: {e}") # str(e) == e.reason, the bare cause
|
|
272
310
|
print(e.response) # text produced before the failure, if any
|
|
273
311
|
print(e.cost, e.session_id, e.duration, e.output_tokens)
|
|
312
|
+
print(e.returncode, e.signal) # -15 and 15 for SIGTERM; None if not process-derived
|
|
274
313
|
else:
|
|
275
314
|
print(response.response)
|
|
276
315
|
```
|
|
@@ -316,7 +355,8 @@ succeeded = saw_ok and error is None # absent result => succeeded stays
|
|
|
316
355
|
**not** run your prompt, so use the stream-based check above when you care about a specific
|
|
317
356
|
call's outcome.
|
|
318
357
|
- **MCP server management** — `add_mcp_server`, `remove_mcp_server`, `list_mcp_servers` manage
|
|
319
|
-
|
|
358
|
+
user-scope MCP configuration. Pi raises `NotImplementedError`; Cursor is managed directly
|
|
359
|
+
through `~/.cursor/mcp.json` because `cursor-agent mcp` has no add/remove subcommands.
|
|
320
360
|
|
|
321
361
|
See [api-reference.md](api-reference.md) for their full signatures and the `MCPServerSpec` model.
|
|
322
362
|
|
|
@@ -346,6 +386,7 @@ logging.getLogger("agent_shell").addHandler(logging.StreamHandler())
|
|
|
346
386
|
| See agent thinking | `include_thinking=True` in `stream()` |
|
|
347
387
|
| Check an agent/model works | `await shell.health_check(cwd, model=...)` |
|
|
348
388
|
| Cancel a running agent | `KeyboardInterrupt` (handled automatically) |
|
|
389
|
+
| Protect the owner from broad process kills | `isolation_policy=LinuxPidNamespaceIsolation()` |
|
|
349
390
|
|
|
350
391
|
## Common Mistakes
|
|
351
392
|
|
|
@@ -365,6 +406,8 @@ logging.getLogger("agent_shell").addHandler(logging.StreamHandler())
|
|
|
365
406
|
| Not catching `AgentExecutionError` | `execute()` raises on a failed run — catch it |
|
|
366
407
|
| Ignoring `UserWarning` on a deny | An unenforceable deny is warned, not applied — the tool is NOT blocked |
|
|
367
408
|
| Ignoring `session_id` for multi-step work | Without it, each call starts fresh |
|
|
409
|
+
| Treating PID isolation as a sandbox | It only blocks direct signalling of ancestors; separately restrict files, network, credentials, tools, and resources |
|
|
410
|
+
| Assuming isolation silently degrades | Requested isolation raises `IsolationUnavailableError` when unavailable; catch it or fail the operation |
|
|
368
411
|
|
|
369
412
|
## API Reference
|
|
370
413
|
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
`MCPServerSpec`, `HealthCheckResult`
|
|
5
5
|
- [StreamEvent types](#event-types)
|
|
6
6
|
- [AgentShell class](#agentshell-class) — invocation, model discovery, health, MCP management
|
|
7
|
+
- [Execution hosts and isolation](#execution-hosts-and-isolation)
|
|
7
8
|
- [AgentAdapter protocol](#agentadapter-protocol)
|
|
8
9
|
- [Agent-specific notes](#agent-specific-notes)
|
|
9
10
|
|
|
@@ -58,6 +59,8 @@ class AgentExecutionError(Exception):
|
|
|
58
59
|
session_id: str | None = None,
|
|
59
60
|
duration: float = 0.0,
|
|
60
61
|
output_tokens: int = 0,
|
|
62
|
+
returncode: int | None = None, # raw process status; negative means signal
|
|
63
|
+
signal: int | None = None, # positive signal number for signal termination
|
|
61
64
|
): ...
|
|
62
65
|
```
|
|
63
66
|
|
|
@@ -75,6 +78,8 @@ class StreamEvent:
|
|
|
75
78
|
session_id: str | None = None # On session-start and "result" events
|
|
76
79
|
output_tokens: int = 0 # Cumulative generated tokens (on "result" events)
|
|
77
80
|
error: str | None = None # Why a failing "result" failed, when recoverable (Pi)
|
|
81
|
+
returncode: int | None = None # Set on process-level "error" events
|
|
82
|
+
signal: int | None = None # Positive signal number when returncode is negative
|
|
78
83
|
```
|
|
79
84
|
|
|
80
85
|
### MCPServerSpec
|
|
@@ -124,7 +129,8 @@ Canonical event types emitted by `stream()`:
|
|
|
124
129
|
|
|
125
130
|
> A `result` event carries `cost`, `duration`, `output_tokens` and `session_id` on the agents
|
|
126
131
|
> that report them. On a failing result, `error` holds the reason when the adapter recovered a
|
|
127
|
-
> structured one (Pi); it is `None` otherwise.
|
|
132
|
+
> structured one (Pi); it is `None` otherwise. A process-level `error` also carries
|
|
133
|
+
> `returncode`; if a signal terminated it, `returncode` is negative and `signal` is positive.
|
|
128
134
|
|
|
129
135
|
> Codex emits the session-start event as `type="session"` (not `"system"`). If you branch on
|
|
130
136
|
> the session event across agents, match both.
|
|
@@ -140,7 +146,12 @@ Canonical event types emitted by `stream()`:
|
|
|
140
146
|
from agent_shell.shell import AgentShell
|
|
141
147
|
|
|
142
148
|
class AgentShell:
|
|
143
|
-
def __init__(
|
|
149
|
+
def __init__(
|
|
150
|
+
self,
|
|
151
|
+
agent_type: AgentType,
|
|
152
|
+
execution_host: ExecutionHost | None = None, # default NativeExecutionHost()
|
|
153
|
+
isolation_policy: IsolationPolicy | None = None, # default NoIsolation()
|
|
154
|
+
): ...
|
|
144
155
|
# raises ValueError for an AgentType with no registered adapter
|
|
145
156
|
|
|
146
157
|
async def execute(
|
|
@@ -174,6 +185,38 @@ class AgentShell:
|
|
|
174
185
|
async def list_mcp_servers(self) -> list[MCPServerSpec]: ...
|
|
175
186
|
```
|
|
176
187
|
|
|
188
|
+
## Execution Hosts and Isolation
|
|
189
|
+
|
|
190
|
+
```python
|
|
191
|
+
from agent_shell.execution import (
|
|
192
|
+
ExecutionHost,
|
|
193
|
+
IsolationPolicy,
|
|
194
|
+
IsolationUnavailableError,
|
|
195
|
+
LinuxPidNamespaceIsolation,
|
|
196
|
+
NativeExecutionHost,
|
|
197
|
+
NativeRunHandle,
|
|
198
|
+
NoIsolation,
|
|
199
|
+
PreparedLaunch,
|
|
200
|
+
RunHandle,
|
|
201
|
+
)
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
`ExecutionHost` creates a `RunHandle` for one command. The handle exposes `pid`, `stdin`,
|
|
205
|
+
`stdout`, `stderr`, `returncode`, `wait()`, `communicate()`, `cancel()`, and `release()`.
|
|
206
|
+
`NativeExecutionHost` is currently the only concrete host. Agent adapters use handles internally;
|
|
207
|
+
existing `execute()` and `stream()` callers do not need to manage them.
|
|
208
|
+
|
|
209
|
+
`NoIsolation` preserves historical native execution. `LinuxPidNamespaceIsolation` uses rootless
|
|
210
|
+
user + PID namespaces, with a tiny PID 1 reaper and the CLI at PID 2 or later. It protects
|
|
211
|
+
AgentShell's ancestors from direct same-user signals sent inside the child namespace. It is not a
|
|
212
|
+
filesystem, credential, network, tool, resource, or general security sandbox. Background
|
|
213
|
+
descendants terminate when the namespace ends.
|
|
214
|
+
|
|
215
|
+
The Linux policy requires `unshare` and supporting kernel configuration. An explicit request that
|
|
216
|
+
cannot be satisfied raises `IsolationUnavailableError` before launching the CLI and never falls
|
|
217
|
+
back to `NoIsolation`. The host/policy selection applies to `execute()`, `stream()`, and
|
|
218
|
+
`health_check()`; model discovery and MCP configuration remain local management operations.
|
|
219
|
+
|
|
177
220
|
### Model discovery semantics
|
|
178
221
|
|
|
179
222
|
`list_models()` reads the selected CLI's account/workspace-aware catalog without sending an
|
|
@@ -273,8 +316,11 @@ class AgentAdapter(Protocol):
|
|
|
273
316
|
it rather than failing (like Pi; unlike Claude Code, OpenCode, Copilot and Codex, which all
|
|
274
317
|
reject one). A matching id is therefore not proof a prior transcript was continued.
|
|
275
318
|
- `duration` and `output_tokens` are real (`usage.outputTokens`); `cost` is always `0.0` — Cursor
|
|
276
|
-
reports no cost.
|
|
277
|
-
|
|
319
|
+
reports no cost.
|
|
320
|
+
- MCP add/remove/list are supported by directly managing user-scope `~/.cursor/mcp.json`, because
|
|
321
|
+
`cursor-agent mcp` has no add/remove subcommands and its list output lacks full configuration.
|
|
322
|
+
Writes are atomic and user-only. Same-transport updates preserve Cursor-native fields that
|
|
323
|
+
`MCPServerSpec` cannot represent.
|
|
278
324
|
|
|
279
325
|
### Grok
|
|
280
326
|
- Headless: `grok -p --output-format streaming-messages-json` (full assistant blocks; not
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '0.
|
|
22
|
-
__version_tuple__ = version_tuple = (0,
|
|
21
|
+
__version__ = version = '0.3.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 3, 0)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|