agent-shell-py 0.3.0__tar.gz → 0.3.2__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.3.0 → agent_shell_py-0.3.2}/AGENTS.md +36 -6
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/PKG-INFO +148 -8
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/README.md +147 -7
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/skills/invoking-cli-agents/SKILL.md +39 -7
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/skills/invoking-cli-agents/api-reference.md +67 -9
- agent_shell_py-0.3.2/src/agent_shell/__init__.py +33 -0
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/src/agent_shell/_version.py +2 -2
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/src/agent_shell/execution.py +56 -13
- agent_shell_py-0.3.2/src/agent_shell/herdr.py +629 -0
- agent_shell_py-0.3.2/src/agent_shell/herdr_protocol.py +45 -0
- agent_shell_py-0.3.2/src/agent_shell/herdr_worker.py +300 -0
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/src/agent_shell/process_cleanup.py +6 -0
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/src/agent_shell/shell.py +21 -1
- agent_shell_py-0.3.2/src/agent_shell/terminal_protocol.py +42 -0
- agent_shell_py-0.3.2/src/agent_shell/terminal_window.py +710 -0
- agent_shell_py-0.3.2/src/agent_shell/terminal_worker.py +261 -0
- agent_shell_py-0.3.2/src/agent_shell/tmux.py +596 -0
- agent_shell_py-0.3.2/src/agent_shell/tmux_bridge.py +232 -0
- agent_shell_py-0.3.2/src/agent_shell/tmux_protocol.py +44 -0
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/conftest.py +6 -0
- agent_shell_py-0.3.2/tests/e2e/test_execution_host_e2e.py +291 -0
- agent_shell_py-0.3.2/tests/e2e/test_terminal_window_execution_host_e2e.py +194 -0
- agent_shell_py-0.3.2/tests/e2e/test_tmux_execution_host_e2e.py +238 -0
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/integration/test_execution_host.py +262 -5
- agent_shell_py-0.3.2/tests/integration/test_herdr_execution_host.py +1022 -0
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/integration/test_process_lifecycle.py +99 -8
- agent_shell_py-0.3.2/tests/integration/test_terminal_window_execution_host.py +877 -0
- agent_shell_py-0.3.2/tests/integration/test_tmux_execution_host.py +710 -0
- agent_shell_py-0.3.2/tests/unit/test_public_exports.py +16 -0
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/unit/test_shell.py +58 -1
- agent_shell_py-0.3.2/tests/unit/test_terminal_window_execution_host.py +83 -0
- agent_shell_py-0.3.0/tests/e2e/test_execution_host_e2e.py +0 -46
- agent_shell_py-0.3.0/tests/unit/__init__.py +0 -0
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/.github/workflows/build.yml +0 -0
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/.github/workflows/ci.yml +0 -0
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/.github/workflows/publish.yml +0 -0
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/.gitignore +0 -0
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/.python-version +0 -0
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/LICENSE +0 -0
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/docs/assets/skill_banner.png +0 -0
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/docs/development/agent_parameter_comparison.md +0 -0
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/docs/development/disabled_tools.md +0 -0
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/docs/development/info.md +0 -0
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/docs/development/total_token_count.md +0 -0
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/pyproject.toml +0 -0
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/skills/delegating-code-review/SKILL.md +0 -0
- {agent_shell_py-0.3.0/src/agent_shell → agent_shell_py-0.3.2/src/agent_shell/adapters}/__init__.py +0 -0
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/src/agent_shell/adapters/agent_adapter_protocol.py +0 -0
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/src/agent_shell/adapters/claude_code_adapter.py +0 -0
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/src/agent_shell/adapters/codex_adapter.py +0 -0
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/src/agent_shell/adapters/copilot_cli_adapter.py +0 -0
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/src/agent_shell/adapters/cursor_adapter.py +0 -0
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/src/agent_shell/adapters/grok_adapter.py +0 -0
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/src/agent_shell/adapters/health.py +0 -0
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/src/agent_shell/adapters/model_discovery.py +0 -0
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/src/agent_shell/adapters/opencode_adapter.py +0 -0
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/src/agent_shell/adapters/outcome.py +0 -0
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/src/agent_shell/adapters/pi_adapter.py +0 -0
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/src/agent_shell/adapters/process_failure.py +0 -0
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/src/agent_shell/adapters/response.py +0 -0
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/src/agent_shell/adapters/stderr_format.py +0 -0
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/src/agent_shell/adapters/tool_denial.py +0 -0
- {agent_shell_py-0.3.0/src/agent_shell/adapters → agent_shell_py-0.3.2/src/agent_shell/models}/__init__.py +0 -0
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/src/agent_shell/models/agent.py +0 -0
- {agent_shell_py-0.3.0/src/agent_shell/models → agent_shell_py-0.3.2/tests}/__init__.py +0 -0
- {agent_shell_py-0.3.0/tests → agent_shell_py-0.3.2/tests/e2e}/__init__.py +0 -0
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/e2e/test_claude_code_e2e.py +0 -0
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/e2e/test_codex_e2e.py +0 -0
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/e2e/test_copilot_cli_e2e.py +0 -0
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/e2e/test_cursor_e2e.py +0 -0
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/e2e/test_grok_e2e.py +0 -0
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/e2e/test_health_check_e2e.py +0 -0
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/e2e/test_model_discovery_e2e.py +0 -0
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/e2e/test_opencode_e2e.py +0 -0
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/e2e/test_pi_e2e.py +0 -0
- {agent_shell_py-0.3.0/tests/e2e → agent_shell_py-0.3.2/tests/integration}/__init__.py +0 -0
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/integration/test_claude_code_integration.py +0 -0
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/integration/test_claude_code_mcp_integration.py +0 -0
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/integration/test_codex_integration.py +0 -0
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/integration/test_codex_mcp_integration.py +0 -0
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/integration/test_copilot_cli_integration.py +0 -0
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/integration/test_copilot_cli_mcp_integration.py +0 -0
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/integration/test_cursor_integration.py +0 -0
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/integration/test_cursor_mcp_integration.py +0 -0
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/integration/test_grok_integration.py +0 -0
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/integration/test_grok_mcp_integration.py +0 -0
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/integration/test_health_check_integration.py +0 -0
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/integration/test_model_discovery_integration.py +0 -0
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/integration/test_opencode_integration.py +0 -0
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/integration/test_opencode_mcp_integration.py +0 -0
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/integration/test_pi_integration.py +0 -0
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/integration/test_pi_mcp_integration.py +0 -0
- {agent_shell_py-0.3.0/tests/integration → agent_shell_py-0.3.2/tests/unit}/__init__.py +0 -0
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/unit/adapter_matrix.py +0 -0
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/unit/codex_fixtures.py +0 -0
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/unit/copilot_fixtures.py +0 -0
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/unit/cursor_fixtures.py +0 -0
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/unit/fixtures.py +0 -0
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/unit/grok_fixtures.py +0 -0
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/unit/opencode_fixtures.py +0 -0
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/unit/pi_fixtures.py +0 -0
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/unit/test_adapter_transport.py +0 -0
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/unit/test_cancel.py +0 -0
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/unit/test_codex_cancel.py +0 -0
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/unit/test_codex_execute.py +0 -0
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/unit/test_codex_parse_event.py +0 -0
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/unit/test_codex_warnings.py +0 -0
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/unit/test_copilot_cli_cancel.py +0 -0
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/unit/test_copilot_cli_execute.py +0 -0
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/unit/test_copilot_cli_parse_event.py +0 -0
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/unit/test_copilot_cli_stream.py +0 -0
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/unit/test_cursor_cancel.py +0 -0
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/unit/test_cursor_execute.py +0 -0
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/unit/test_cursor_parse_event.py +0 -0
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/unit/test_cursor_warnings.py +0 -0
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/unit/test_execute.py +0 -0
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/unit/test_execute_outcome.py +0 -0
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/unit/test_grok_cancel.py +0 -0
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/unit/test_grok_execute.py +0 -0
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/unit/test_grok_parse_event.py +0 -0
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/unit/test_grok_warnings.py +0 -0
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/unit/test_health_probe.py +0 -0
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/unit/test_mcp_server_spec.py +0 -0
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/unit/test_model_discovery.py +0 -0
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/unit/test_models.py +0 -0
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/unit/test_opencode_cancel.py +0 -0
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/unit/test_opencode_execute.py +0 -0
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/unit/test_opencode_parse_event.py +0 -0
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/unit/test_opencode_spawn.py +0 -0
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/unit/test_opencode_stream.py +0 -0
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/unit/test_parse_event.py +0 -0
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/unit/test_pi_cancel.py +0 -0
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/unit/test_pi_execute.py +0 -0
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/unit/test_pi_parse_event.py +0 -0
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/unit/test_pi_warnings.py +0 -0
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/unit/test_process_cleanup.py +0 -0
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/unit/test_process_group_registration.py +0 -0
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/unit/test_response_aggregation.py +0 -0
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/unit/test_shell_cancellation.py +0 -0
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/unit/test_shell_mcp.py +0 -0
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/unit/test_stderr_format.py +0 -0
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/unit/test_stream.py +0 -0
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/unit/test_tool_denial.py +0 -0
- {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/uv.lock +0 -0
|
@@ -105,6 +105,21 @@ classDiagram
|
|
|
105
105
|
+launch(command, cwd, env, stdin, isolation_policy) NativeRunHandle
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
class HerdrExecutionHost {
|
|
109
|
+
<<experimental>>
|
|
110
|
+
+launch(command, cwd, env, stdin, isolation_policy) RunHandle
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
class TmuxExecutionHost {
|
|
114
|
+
<<experimental>>
|
|
115
|
+
+launch(command, cwd, env, stdin, isolation_policy) RunHandle
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
class TerminalWindowExecutionHost {
|
|
119
|
+
<<experimental>>
|
|
120
|
+
+launch(command, cwd, env, stdin, isolation_policy) RunHandle
|
|
121
|
+
}
|
|
122
|
+
|
|
108
123
|
class IsolationPolicy {
|
|
109
124
|
<<Protocol>>
|
|
110
125
|
+prepare(command, env) PreparedLaunch
|
|
@@ -140,6 +155,9 @@ classDiagram
|
|
|
140
155
|
AgentShell --> ExecutionHost : selects
|
|
141
156
|
AgentShell --> IsolationPolicy : selects
|
|
142
157
|
NativeExecutionHost ..|> ExecutionHost : satisfies
|
|
158
|
+
HerdrExecutionHost ..|> ExecutionHost : satisfies
|
|
159
|
+
TmuxExecutionHost ..|> ExecutionHost : satisfies
|
|
160
|
+
TerminalWindowExecutionHost ..|> ExecutionHost : satisfies
|
|
143
161
|
NativeExecutionHost --> NativeRunHandle : creates
|
|
144
162
|
NativeRunHandle ..|> RunHandle : satisfies
|
|
145
163
|
NoIsolation ..|> IsolationPolicy : satisfies
|
|
@@ -157,13 +175,25 @@ classDiagram
|
|
|
157
175
|
|
|
158
176
|
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
177
|
|
|
178
|
+
`HerdrExecutionHost`, `TmuxExecutionHost`, and `TerminalWindowExecutionHost` are experimental,
|
|
179
|
+
opt-in APIs. Their constructors, placement options, supported platforms, and lifecycle behaviour
|
|
180
|
+
may change in a later minor release. `NativeExecutionHost` remains the backwards-compatible
|
|
181
|
+
default, and an unavailable experimental host never silently falls back to it.
|
|
182
|
+
|
|
160
183
|
Execution location and protection are separate axes. Existing callers default to
|
|
161
|
-
`NativeExecutionHost()` plus `NoIsolation()
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
184
|
+
`NativeExecutionHost()` plus `NoIsolation()` when `AGENTSHELL_ISOLATION_POLICY` is unset. The
|
|
185
|
+
environment accepts `none` or `linux-pid-namespace` as a process-wide construction default;
|
|
186
|
+
explicit `isolation_policy=` takes precedence, and invalid or empty values raise `ValueError`.
|
|
187
|
+
`LinuxPidNamespaceIsolation` is a direct signal boundary: a tiny init/reaper is PID 1 and the CLI
|
|
188
|
+
is PID 2 or later, so child-namespace processes cannot directly signal AgentShell's ancestors.
|
|
189
|
+
The default `mount_proc=True` also hides those ancestors through a private `/proc` mount.
|
|
190
|
+
Explicit `mount_proc=False` retains the PID boundary and descendant cleanup while inheriting the
|
|
191
|
+
outer `/proc` view, exposing process metadata and potentially confusing tools that use its PIDs.
|
|
192
|
+
It does not create a private mount namespace. The environment value `linux-pid-namespace` remains
|
|
193
|
+
strict; each policy probes its own requested mode. It requires Linux, `unshare`, and enabled
|
|
194
|
+
unprivileged user/PID namespaces; an unavailable request raises `IsolationUnavailableError` and
|
|
195
|
+
never falls back. This is not a general sandbox and does not restrict filesystem, credentials,
|
|
196
|
+
network, tools, or resources. The host/policy applies to
|
|
167
197
|
`execute()`, `stream()`, and `health_check()`; model discovery and MCP configuration remain local.
|
|
168
198
|
|
|
169
199
|
`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).
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: agent-shell-py
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
4
4
|
Summary: A lightweight abstraction for executing CLI coding agents headlessly
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -83,13 +83,24 @@ separately.
|
|
|
83
83
|
|
|
84
84
|
### Execution host and isolation
|
|
85
85
|
|
|
86
|
-
Existing callers remain unchanged. Omitting both
|
|
87
|
-
`NativeExecutionHost()` plus `NoIsolation()`:
|
|
86
|
+
Existing callers remain unchanged when `AGENTSHELL_ISOLATION_POLICY` is unset. Omitting both
|
|
87
|
+
constructor settings then means `NativeExecutionHost()` plus `NoIsolation()`:
|
|
88
88
|
|
|
89
89
|
```python
|
|
90
90
|
shell = AgentShell(agent_type=AgentType.CLAUDE_CODE)
|
|
91
91
|
```
|
|
92
92
|
|
|
93
|
+
Set a process-wide deployment default without changing Python call sites:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
export AGENTSHELL_ISOLATION_POLICY=linux-pid-namespace
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
The accepted values are `none` and `linux-pid-namespace`. AgentShell reads the value when each
|
|
100
|
+
shell is constructed. An empty or unknown value raises `ValueError` rather than silently disabling
|
|
101
|
+
isolation. An explicit constructor policy takes precedence over the environment, so a caller can
|
|
102
|
+
still force one shell to use `NoIsolation()` or a custom policy.
|
|
103
|
+
|
|
93
104
|
To protect the AgentShell owner from broad same-user cleanup commands such as `pkill -f`,
|
|
94
105
|
explicitly request Linux PID namespace isolation:
|
|
95
106
|
|
|
@@ -103,17 +114,146 @@ shell = AgentShell(
|
|
|
103
114
|
```
|
|
104
115
|
|
|
105
116
|
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
|
|
107
|
-
|
|
108
|
-
|
|
117
|
+
that child namespace cannot signal AgentShell's ancestor processes. With the default private
|
|
118
|
+
`/proc` mount they cannot see those ancestors either. The feature requires Linux, the `unshare`
|
|
119
|
+
command, and kernel support for unprivileged user/PID namespaces. If any are unavailable,
|
|
120
|
+
launching raises `IsolationUnavailableError`; it never silently falls back.
|
|
121
|
+
|
|
122
|
+
`LinuxPidNamespaceIsolation()` mounts a private `/proc` view by default, so tools such as `ps`
|
|
123
|
+
show namespace PIDs. In a restricted container where that mount is denied but the PID namespace
|
|
124
|
+
itself is available, opt into the inherited outer `/proc` view explicitly:
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
isolation_policy=LinuxPidNamespaceIsolation(mount_proc=False)
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
The PID and signal boundary, PID 1 reaper, and descendant cleanup are retained, but `/proc` and
|
|
131
|
+
`ps` can then expose outer PID information that disagrees with the child's `os.getpid()`. The
|
|
132
|
+
selected mode is checked during the availability probe and still fails closed when unavailable;
|
|
133
|
+
it does not create a private mount namespace. The environment setting
|
|
134
|
+
`AGENTSHELL_ISOLATION_POLICY=linux-pid-namespace` always selects the default `mount_proc=True`
|
|
135
|
+
mode; use the explicit constructor for the opt-in mode.
|
|
109
136
|
|
|
110
137
|
This is **direct-signal protection, not a sandbox**. It does not restrict files, credentials,
|
|
111
138
|
network access, tools, or resource consumption. Any background descendants are also terminated
|
|
112
139
|
when the isolated namespace ends. `execute()`, `stream()`, and `health_check()` use the selected
|
|
113
140
|
host/policy; model discovery and MCP configuration remain local management operations.
|
|
114
141
|
|
|
115
|
-
`NativeExecutionHost`
|
|
116
|
-
|
|
142
|
+
`NativeExecutionHost` remains the default. Execution hosts are opt-in and do not add Python
|
|
143
|
+
dependencies.
|
|
144
|
+
|
|
145
|
+
> [!WARNING]
|
|
146
|
+
> `HerdrExecutionHost`, `TmuxExecutionHost`, and `TerminalWindowExecutionHost` are experimental.
|
|
147
|
+
> They are opt-in, and their constructors, placement options, supported platforms, and lifecycle
|
|
148
|
+
> behaviour may change in a later minor release as real-world usage expands. Explicit requests
|
|
149
|
+
> continue to fail closed when their external prerequisite or requested policy is unavailable;
|
|
150
|
+
> AgentShell never silently substitutes `NativeExecutionHost`.
|
|
151
|
+
|
|
152
|
+
#### Herdr execution host (experimental)
|
|
153
|
+
|
|
154
|
+
An opt-in `HerdrExecutionHost` runs each command in a
|
|
155
|
+
uniquely owned Herdr pane while preserving the same adapter API:
|
|
156
|
+
|
|
157
|
+
```python
|
|
158
|
+
from agent_shell import HerdrExecutionHost
|
|
159
|
+
|
|
160
|
+
shell = AgentShell(
|
|
161
|
+
agent_type=AgentType.CLAUDE_CODE,
|
|
162
|
+
execution_host=HerdrExecutionHost(),
|
|
163
|
+
)
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
The `herdr` executable is an optional external prerequisite; AgentShell does not install a Python
|
|
167
|
+
Herdr client or silently fall back to native execution. Herdr host v1 supports `NoIsolation()`
|
|
168
|
+
only, and supports `DEVNULL` or `PIPE` stdin. A private, standard-library bridge carries the
|
|
169
|
+
command, environment, output, status, and cancellation between the host and the Herdr worker, so
|
|
170
|
+
secrets are not placed in the worker's launcher arguments. `RunHandle.pid` identifies the bridge
|
|
171
|
+
worker, while the target CLI's normal or signal return status is preserved. Every run cleans up
|
|
172
|
+
only its own Herdr pane, workspace, and bridge directory. Cleanup is bounded (five seconds by
|
|
173
|
+
default; configure `cleanup_timeout=` when constructing the host). Model discovery and MCP
|
|
174
|
+
configuration are local management operations and do not create Herdr panes.
|
|
175
|
+
|
|
176
|
+
On Linux, both the bridge worker and its target use a parent-death guard, so an abrupt Herdr pane
|
|
177
|
+
or worker exit cannot leave the target running. Other Unix platforms use the normal bridge
|
|
178
|
+
disconnect cleanup but do not provide this kernel-level guard; an abrupt worker or pane death may
|
|
179
|
+
therefore require external cleanup.
|
|
180
|
+
|
|
181
|
+
#### tmux execution host (experimental)
|
|
182
|
+
|
|
183
|
+
For a visible tmux run, opt into `TmuxExecutionHost` (the `tmux` executable itself is an optional
|
|
184
|
+
system prerequisite):
|
|
185
|
+
|
|
186
|
+
```python
|
|
187
|
+
from agent_shell import TmuxExecutionHost, TmuxPlacement
|
|
188
|
+
|
|
189
|
+
shell = AgentShell(
|
|
190
|
+
agent_type=AgentType.CLAUDE_CODE,
|
|
191
|
+
execution_host=TmuxExecutionHost(),
|
|
192
|
+
)
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
By default, each run creates and owns one uniquely named tmux session and pane. Placement can be
|
|
196
|
+
chosen explicitly: `TmuxPlacement.new_session(name=None)` creates an owned session, while
|
|
197
|
+
`TmuxPlacement.new_window(session, focus=False)` borrows an existing session and owns only the
|
|
198
|
+
new window. `TmuxPlacement.current_session(focus=False)` is a convenience for creating a window
|
|
199
|
+
in the current tmux session and fails clearly when called outside tmux. `focus` defaults to false,
|
|
200
|
+
so creating a window does not change the user's active window.
|
|
201
|
+
|
|
202
|
+
A private, one-shot stdlib bridge keeps stdout and stderr as separate raw streams for the adapter
|
|
203
|
+
while mirroring output into the pane; the command, working directory, environment, and prompt are
|
|
204
|
+
sent over the private Unix socket, so secrets are not placed in the tmux command line.
|
|
205
|
+
`RunHandle.pid` identifies the bridge/worker that owns the lifecycle, while `returncode` is the
|
|
206
|
+
target CLI's exact exit or signal status.
|
|
207
|
+
|
|
208
|
+
Version 1 supports `NoIsolation` and `DEVNULL` or `PIPE` stdin only. Unsupported isolation
|
|
209
|
+
policies and arbitrary file descriptors fail closed before a tmux session is created. Completed,
|
|
210
|
+
cancelled, abandoned, and interpreter-exit runs clean up their AgentShell-owned resource: a new
|
|
211
|
+
session for session placement, or exactly the new window for borrowed-session placement. The host
|
|
212
|
+
never kills unrelated tmux sessions or windows. Missing sessions, explicit name collisions, and
|
|
213
|
+
unidentifiable windows fail closed without falling back to a new session. If tmux is unavailable,
|
|
214
|
+
`TmuxUnavailableError` is raised and AgentShell does not silently fall back to the native host.
|
|
215
|
+
Model discovery and MCP configuration remain local management operations.
|
|
216
|
+
|
|
217
|
+
#### Terminal-window execution host (experimental)
|
|
218
|
+
|
|
219
|
+
To watch a headless run in a new local graphical terminal window, inject the host into
|
|
220
|
+
`AgentShell`:
|
|
221
|
+
|
|
222
|
+
```python
|
|
223
|
+
from agent_shell import TerminalWindowExecutionHost
|
|
224
|
+
from agent_shell.shell import AgentShell
|
|
225
|
+
from agent_shell.models.agent import AgentType
|
|
226
|
+
|
|
227
|
+
shell = AgentShell(
|
|
228
|
+
agent_type=AgentType.CLAUDE_CODE,
|
|
229
|
+
execution_host=TerminalWindowExecutionHost(),
|
|
230
|
+
)
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
The host is Linux-focused in v1 and needs an installed terminal emulator plus a usable X11 or
|
|
234
|
+
Wayland session. It discovers `x-terminal-emulator`, `gnome-terminal`, `konsole`, `kitty`,
|
|
235
|
+
`alacritty`, `foot`, `wezterm`, or `xterm` in that order. These are optional external
|
|
236
|
+
prerequisites; AgentShell has no terminal-emulator or Python runtime dependency. A caller can
|
|
237
|
+
inject a `TerminalWindowLauncher` strategy (for example, `SubprocessTerminalLauncher`) for a
|
|
238
|
+
different emulator or platform. If no launcher or graphical session is available, the request
|
|
239
|
+
raises `TerminalWindowUnavailableError` and does not fall back to native execution.
|
|
240
|
+
For a simple local override, set `AGENTSHELL_TERMINAL_LAUNCHER` to an executable name or path;
|
|
241
|
+
strategies are preferred when custom argument conventions are needed.
|
|
242
|
+
|
|
243
|
+
Each run opens a one-shot worker. The worker receives the command, environment, and prompt-bearing
|
|
244
|
+
CLI arguments over a private mode-0700 directory and Unix socket; only the non-secret socket path
|
|
245
|
+
is passed to the terminal launcher. Raw stdout and stderr bytes are forwarded unchanged to
|
|
246
|
+
AgentShell and mirrored to the visible terminal. `RunHandle.pid` is the worker PID, and the
|
|
247
|
+
target command's exact exit or signal status is returned.
|
|
248
|
+
|
|
249
|
+
Only `NoIsolation` and `DEVNULL`/`PIPE` stdin are supported in v1. Other isolation policies and
|
|
250
|
+
arbitrary file descriptors are rejected before launch. Runs are headless and machine-controlled
|
|
251
|
+
despite being visible. Completed windows and transport resources close by default; there is no
|
|
252
|
+
hold-open option in v1. Cancellation is sent through the private socket, and abandoning the
|
|
253
|
+
owner cleans the worker, socket, and temporary directory where interpreter shutdown permits.
|
|
254
|
+
|
|
255
|
+
Execution host and isolation policy remain separate axes, avoiding one host class per host/policy
|
|
256
|
+
combination. Optional hosts fail closed when a requested isolation policy cannot be transported.
|
|
117
257
|
|
|
118
258
|
### Execute
|
|
119
259
|
|
|
@@ -74,13 +74,24 @@ separately.
|
|
|
74
74
|
|
|
75
75
|
### Execution host and isolation
|
|
76
76
|
|
|
77
|
-
Existing callers remain unchanged. Omitting both
|
|
78
|
-
`NativeExecutionHost()` plus `NoIsolation()`:
|
|
77
|
+
Existing callers remain unchanged when `AGENTSHELL_ISOLATION_POLICY` is unset. Omitting both
|
|
78
|
+
constructor settings then means `NativeExecutionHost()` plus `NoIsolation()`:
|
|
79
79
|
|
|
80
80
|
```python
|
|
81
81
|
shell = AgentShell(agent_type=AgentType.CLAUDE_CODE)
|
|
82
82
|
```
|
|
83
83
|
|
|
84
|
+
Set a process-wide deployment default without changing Python call sites:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
export AGENTSHELL_ISOLATION_POLICY=linux-pid-namespace
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
The accepted values are `none` and `linux-pid-namespace`. AgentShell reads the value when each
|
|
91
|
+
shell is constructed. An empty or unknown value raises `ValueError` rather than silently disabling
|
|
92
|
+
isolation. An explicit constructor policy takes precedence over the environment, so a caller can
|
|
93
|
+
still force one shell to use `NoIsolation()` or a custom policy.
|
|
94
|
+
|
|
84
95
|
To protect the AgentShell owner from broad same-user cleanup commands such as `pkill -f`,
|
|
85
96
|
explicitly request Linux PID namespace isolation:
|
|
86
97
|
|
|
@@ -94,17 +105,146 @@ shell = AgentShell(
|
|
|
94
105
|
```
|
|
95
106
|
|
|
96
107
|
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
|
|
98
|
-
|
|
99
|
-
|
|
108
|
+
that child namespace cannot signal AgentShell's ancestor processes. With the default private
|
|
109
|
+
`/proc` mount they cannot see those ancestors either. The feature requires Linux, the `unshare`
|
|
110
|
+
command, and kernel support for unprivileged user/PID namespaces. If any are unavailable,
|
|
111
|
+
launching raises `IsolationUnavailableError`; it never silently falls back.
|
|
112
|
+
|
|
113
|
+
`LinuxPidNamespaceIsolation()` mounts a private `/proc` view by default, so tools such as `ps`
|
|
114
|
+
show namespace PIDs. In a restricted container where that mount is denied but the PID namespace
|
|
115
|
+
itself is available, opt into the inherited outer `/proc` view explicitly:
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
isolation_policy=LinuxPidNamespaceIsolation(mount_proc=False)
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
The PID and signal boundary, PID 1 reaper, and descendant cleanup are retained, but `/proc` and
|
|
122
|
+
`ps` can then expose outer PID information that disagrees with the child's `os.getpid()`. The
|
|
123
|
+
selected mode is checked during the availability probe and still fails closed when unavailable;
|
|
124
|
+
it does not create a private mount namespace. The environment setting
|
|
125
|
+
`AGENTSHELL_ISOLATION_POLICY=linux-pid-namespace` always selects the default `mount_proc=True`
|
|
126
|
+
mode; use the explicit constructor for the opt-in mode.
|
|
100
127
|
|
|
101
128
|
This is **direct-signal protection, not a sandbox**. It does not restrict files, credentials,
|
|
102
129
|
network access, tools, or resource consumption. Any background descendants are also terminated
|
|
103
130
|
when the isolated namespace ends. `execute()`, `stream()`, and `health_check()` use the selected
|
|
104
131
|
host/policy; model discovery and MCP configuration remain local management operations.
|
|
105
132
|
|
|
106
|
-
`NativeExecutionHost`
|
|
107
|
-
|
|
133
|
+
`NativeExecutionHost` remains the default. Execution hosts are opt-in and do not add Python
|
|
134
|
+
dependencies.
|
|
135
|
+
|
|
136
|
+
> [!WARNING]
|
|
137
|
+
> `HerdrExecutionHost`, `TmuxExecutionHost`, and `TerminalWindowExecutionHost` are experimental.
|
|
138
|
+
> They are opt-in, and their constructors, placement options, supported platforms, and lifecycle
|
|
139
|
+
> behaviour may change in a later minor release as real-world usage expands. Explicit requests
|
|
140
|
+
> continue to fail closed when their external prerequisite or requested policy is unavailable;
|
|
141
|
+
> AgentShell never silently substitutes `NativeExecutionHost`.
|
|
142
|
+
|
|
143
|
+
#### Herdr execution host (experimental)
|
|
144
|
+
|
|
145
|
+
An opt-in `HerdrExecutionHost` runs each command in a
|
|
146
|
+
uniquely owned Herdr pane while preserving the same adapter API:
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
from agent_shell import HerdrExecutionHost
|
|
150
|
+
|
|
151
|
+
shell = AgentShell(
|
|
152
|
+
agent_type=AgentType.CLAUDE_CODE,
|
|
153
|
+
execution_host=HerdrExecutionHost(),
|
|
154
|
+
)
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
The `herdr` executable is an optional external prerequisite; AgentShell does not install a Python
|
|
158
|
+
Herdr client or silently fall back to native execution. Herdr host v1 supports `NoIsolation()`
|
|
159
|
+
only, and supports `DEVNULL` or `PIPE` stdin. A private, standard-library bridge carries the
|
|
160
|
+
command, environment, output, status, and cancellation between the host and the Herdr worker, so
|
|
161
|
+
secrets are not placed in the worker's launcher arguments. `RunHandle.pid` identifies the bridge
|
|
162
|
+
worker, while the target CLI's normal or signal return status is preserved. Every run cleans up
|
|
163
|
+
only its own Herdr pane, workspace, and bridge directory. Cleanup is bounded (five seconds by
|
|
164
|
+
default; configure `cleanup_timeout=` when constructing the host). Model discovery and MCP
|
|
165
|
+
configuration are local management operations and do not create Herdr panes.
|
|
166
|
+
|
|
167
|
+
On Linux, both the bridge worker and its target use a parent-death guard, so an abrupt Herdr pane
|
|
168
|
+
or worker exit cannot leave the target running. Other Unix platforms use the normal bridge
|
|
169
|
+
disconnect cleanup but do not provide this kernel-level guard; an abrupt worker or pane death may
|
|
170
|
+
therefore require external cleanup.
|
|
171
|
+
|
|
172
|
+
#### tmux execution host (experimental)
|
|
173
|
+
|
|
174
|
+
For a visible tmux run, opt into `TmuxExecutionHost` (the `tmux` executable itself is an optional
|
|
175
|
+
system prerequisite):
|
|
176
|
+
|
|
177
|
+
```python
|
|
178
|
+
from agent_shell import TmuxExecutionHost, TmuxPlacement
|
|
179
|
+
|
|
180
|
+
shell = AgentShell(
|
|
181
|
+
agent_type=AgentType.CLAUDE_CODE,
|
|
182
|
+
execution_host=TmuxExecutionHost(),
|
|
183
|
+
)
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
By default, each run creates and owns one uniquely named tmux session and pane. Placement can be
|
|
187
|
+
chosen explicitly: `TmuxPlacement.new_session(name=None)` creates an owned session, while
|
|
188
|
+
`TmuxPlacement.new_window(session, focus=False)` borrows an existing session and owns only the
|
|
189
|
+
new window. `TmuxPlacement.current_session(focus=False)` is a convenience for creating a window
|
|
190
|
+
in the current tmux session and fails clearly when called outside tmux. `focus` defaults to false,
|
|
191
|
+
so creating a window does not change the user's active window.
|
|
192
|
+
|
|
193
|
+
A private, one-shot stdlib bridge keeps stdout and stderr as separate raw streams for the adapter
|
|
194
|
+
while mirroring output into the pane; the command, working directory, environment, and prompt are
|
|
195
|
+
sent over the private Unix socket, so secrets are not placed in the tmux command line.
|
|
196
|
+
`RunHandle.pid` identifies the bridge/worker that owns the lifecycle, while `returncode` is the
|
|
197
|
+
target CLI's exact exit or signal status.
|
|
198
|
+
|
|
199
|
+
Version 1 supports `NoIsolation` and `DEVNULL` or `PIPE` stdin only. Unsupported isolation
|
|
200
|
+
policies and arbitrary file descriptors fail closed before a tmux session is created. Completed,
|
|
201
|
+
cancelled, abandoned, and interpreter-exit runs clean up their AgentShell-owned resource: a new
|
|
202
|
+
session for session placement, or exactly the new window for borrowed-session placement. The host
|
|
203
|
+
never kills unrelated tmux sessions or windows. Missing sessions, explicit name collisions, and
|
|
204
|
+
unidentifiable windows fail closed without falling back to a new session. If tmux is unavailable,
|
|
205
|
+
`TmuxUnavailableError` is raised and AgentShell does not silently fall back to the native host.
|
|
206
|
+
Model discovery and MCP configuration remain local management operations.
|
|
207
|
+
|
|
208
|
+
#### Terminal-window execution host (experimental)
|
|
209
|
+
|
|
210
|
+
To watch a headless run in a new local graphical terminal window, inject the host into
|
|
211
|
+
`AgentShell`:
|
|
212
|
+
|
|
213
|
+
```python
|
|
214
|
+
from agent_shell import TerminalWindowExecutionHost
|
|
215
|
+
from agent_shell.shell import AgentShell
|
|
216
|
+
from agent_shell.models.agent import AgentType
|
|
217
|
+
|
|
218
|
+
shell = AgentShell(
|
|
219
|
+
agent_type=AgentType.CLAUDE_CODE,
|
|
220
|
+
execution_host=TerminalWindowExecutionHost(),
|
|
221
|
+
)
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
The host is Linux-focused in v1 and needs an installed terminal emulator plus a usable X11 or
|
|
225
|
+
Wayland session. It discovers `x-terminal-emulator`, `gnome-terminal`, `konsole`, `kitty`,
|
|
226
|
+
`alacritty`, `foot`, `wezterm`, or `xterm` in that order. These are optional external
|
|
227
|
+
prerequisites; AgentShell has no terminal-emulator or Python runtime dependency. A caller can
|
|
228
|
+
inject a `TerminalWindowLauncher` strategy (for example, `SubprocessTerminalLauncher`) for a
|
|
229
|
+
different emulator or platform. If no launcher or graphical session is available, the request
|
|
230
|
+
raises `TerminalWindowUnavailableError` and does not fall back to native execution.
|
|
231
|
+
For a simple local override, set `AGENTSHELL_TERMINAL_LAUNCHER` to an executable name or path;
|
|
232
|
+
strategies are preferred when custom argument conventions are needed.
|
|
233
|
+
|
|
234
|
+
Each run opens a one-shot worker. The worker receives the command, environment, and prompt-bearing
|
|
235
|
+
CLI arguments over a private mode-0700 directory and Unix socket; only the non-secret socket path
|
|
236
|
+
is passed to the terminal launcher. Raw stdout and stderr bytes are forwarded unchanged to
|
|
237
|
+
AgentShell and mirrored to the visible terminal. `RunHandle.pid` is the worker PID, and the
|
|
238
|
+
target command's exact exit or signal status is returned.
|
|
239
|
+
|
|
240
|
+
Only `NoIsolation` and `DEVNULL`/`PIPE` stdin are supported in v1. Other isolation policies and
|
|
241
|
+
arbitrary file descriptors are rejected before launch. Runs are headless and machine-controlled
|
|
242
|
+
despite being visible. Completed windows and transport resources close by default; there is no
|
|
243
|
+
hold-open option in v1. Cancellation is sent through the private socket, and abandoning the
|
|
244
|
+
owner cleans the worker, socket, and temporary directory where interpreter shutdown permits.
|
|
245
|
+
|
|
246
|
+
Execution host and isolation policy remain separate axes, avoiding one host class per host/policy
|
|
247
|
+
combination. Optional hosts fail closed when a requested isolation policy cannot be transported.
|
|
108
248
|
|
|
109
249
|
### Execute
|
|
110
250
|
|
|
@@ -47,19 +47,30 @@ yields events in real-time — plus helpers for model discovery, health checks,
|
|
|
47
47
|
management. All are async. Invocation has three independent choices:
|
|
48
48
|
|
|
49
49
|
- `agent_type`: which CLI (Claude Code, Codex, etc.)
|
|
50
|
-
- `execution_host`: where/how the process is owned (
|
|
50
|
+
- `execution_host`: where/how the process is owned (native by default; experimental visible hosts
|
|
51
|
+
are available explicitly)
|
|
51
52
|
- `isolation_policy`: what protection surrounds it (`NoIsolation` or Linux PID isolation)
|
|
52
53
|
|
|
53
54
|
### Execution Host and Isolation Policy
|
|
54
55
|
|
|
55
|
-
Existing code is backward-compatible:
|
|
56
|
-
with no isolation.
|
|
56
|
+
Existing code is backward-compatible: when `AGENTSHELL_ISOLATION_POLICY` is unset, omitting both
|
|
57
|
+
execution settings selects native execution with no isolation.
|
|
57
58
|
|
|
58
59
|
```python
|
|
59
60
|
shell = AgentShell(agent_type=AgentType.CODEX)
|
|
60
61
|
# Equivalent to NativeExecutionHost() + NoIsolation()
|
|
61
62
|
```
|
|
62
63
|
|
|
64
|
+
For a process-wide deployment default, set the environment before constructing a shell:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
export AGENTSHELL_ISOLATION_POLICY=linux-pid-namespace
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Accepted values are `none` and `linux-pid-namespace`. Empty or unknown values raise `ValueError`.
|
|
71
|
+
An explicit `isolation_policy=` object wins over the environment, which remains the way to select
|
|
72
|
+
a custom policy or override the deployment default for one shell.
|
|
73
|
+
|
|
63
74
|
Opt into direct-signal protection when an agent may run broad same-user cleanup commands such as
|
|
64
75
|
`pkill -f`:
|
|
65
76
|
|
|
@@ -73,15 +84,36 @@ shell = AgentShell(
|
|
|
73
84
|
```
|
|
74
85
|
|
|
75
86
|
The Linux policy puts a tiny init/reaper at namespace PID 1 and the actual CLI at PID 2 or later.
|
|
76
|
-
|
|
77
|
-
|
|
87
|
+
With the default private `/proc` mount, the CLI cannot see AgentShell's ancestors; both modes keep
|
|
88
|
+
those ancestors outside the CLI's direct signal boundary. Use
|
|
89
|
+
`LinuxPidNamespaceIsolation(mount_proc=False)` in a restricted container where the private proc
|
|
90
|
+
mount is denied; that mode inherits outer `/proc` visibility while retaining the PID boundary,
|
|
91
|
+
reaper, and descendant cleanup. The environment value `linux-pid-namespace` always selects the
|
|
92
|
+
strict default (`mount_proc=True`). It requires Linux, `unshare`, and kernel support for
|
|
93
|
+
unprivileged user/PID namespaces. An unavailable requested policy raises
|
|
78
94
|
`IsolationUnavailableError` before the CLI starts; AgentShell never silently falls back.
|
|
79
95
|
|
|
80
96
|
This policy is **not a sandbox**: filesystem, credentials, network, agent tools, and resource use
|
|
81
97
|
remain available. Background descendants cannot outlive the isolated namespace. It applies to
|
|
82
98
|
`execute()`, `stream()`, and `health_check()`; `list_models()` and MCP configuration are local
|
|
83
|
-
management operations.
|
|
84
|
-
|
|
99
|
+
management operations.
|
|
100
|
+
|
|
101
|
+
### Experimental Execution Hosts
|
|
102
|
+
|
|
103
|
+
`NativeExecutionHost` remains the backwards-compatible default. Three visible execution hosts
|
|
104
|
+
are shipped as **experimental, opt-in APIs**:
|
|
105
|
+
|
|
106
|
+
- `HerdrExecutionHost()` creates and owns a one-shot Herdr pane/workspace.
|
|
107
|
+
- `TmuxExecutionHost()` creates an owned tmux session by default; `TmuxPlacement` can instead
|
|
108
|
+
select a named or current session and own only the newly-created window.
|
|
109
|
+
- `TerminalWindowExecutionHost()` opens a headless, machine-controlled run in a graphical
|
|
110
|
+
terminal window through an injectable launcher.
|
|
111
|
+
|
|
112
|
+
Their constructors, placement options, platform support, and lifecycle behaviour may change in a
|
|
113
|
+
later minor release. They require optional external executables, support only `NoIsolation` in
|
|
114
|
+
their first version, and fail closed rather than silently falling back to native execution. Use
|
|
115
|
+
the stable default when visible placement is not required. See the execution-host section in
|
|
116
|
+
[`api-reference.md`](api-reference.md) before selecting an experimental host.
|
|
85
117
|
|
|
86
118
|
### Discover Available Model Strings
|
|
87
119
|
|
|
@@ -150,7 +150,7 @@ class AgentShell:
|
|
|
150
150
|
self,
|
|
151
151
|
agent_type: AgentType,
|
|
152
152
|
execution_host: ExecutionHost | None = None, # default NativeExecutionHost()
|
|
153
|
-
isolation_policy: IsolationPolicy | None = None, #
|
|
153
|
+
isolation_policy: IsolationPolicy | None = None, # env setting, then NoIsolation()
|
|
154
154
|
): ...
|
|
155
155
|
# raises ValueError for an AgentType with no registered adapter
|
|
156
156
|
|
|
@@ -188,6 +188,16 @@ class AgentShell:
|
|
|
188
188
|
## Execution Hosts and Isolation
|
|
189
189
|
|
|
190
190
|
```python
|
|
191
|
+
from agent_shell import (
|
|
192
|
+
HerdrExecutionHost,
|
|
193
|
+
SubprocessTerminalLauncher,
|
|
194
|
+
TerminalWindowExecutionHost,
|
|
195
|
+
TerminalWindowLauncher,
|
|
196
|
+
TerminalWindowUnavailableError,
|
|
197
|
+
TmuxExecutionHost,
|
|
198
|
+
TmuxPlacement,
|
|
199
|
+
TmuxUnavailableError,
|
|
200
|
+
)
|
|
191
201
|
from agent_shell.execution import (
|
|
192
202
|
ExecutionHost,
|
|
193
203
|
IsolationPolicy,
|
|
@@ -203,19 +213,67 @@ from agent_shell.execution import (
|
|
|
203
213
|
|
|
204
214
|
`ExecutionHost` creates a `RunHandle` for one command. The handle exposes `pid`, `stdin`,
|
|
205
215
|
`stdout`, `stderr`, `returncode`, `wait()`, `communicate()`, `cancel()`, and `release()`.
|
|
206
|
-
|
|
207
|
-
|
|
216
|
+
Agent adapters use handles internally; existing `execute()` and `stream()` callers do not need to
|
|
217
|
+
manage them. `NativeExecutionHost` remains the backwards-compatible default.
|
|
218
|
+
|
|
219
|
+
> **Experimental hosts:** `HerdrExecutionHost`, `TmuxExecutionHost`, and
|
|
220
|
+
> `TerminalWindowExecutionHost` are opt-in experimental APIs. Their constructors, placement
|
|
221
|
+
> options, supported platforms, and lifecycle behaviour may change in a later minor release. An
|
|
222
|
+
> explicit request fails closed when its prerequisite or requested policy is unavailable; none of
|
|
223
|
+
> these hosts silently falls back to native execution.
|
|
224
|
+
|
|
225
|
+
| Host | Placement | Optional prerequisite | First-version limits |
|
|
226
|
+
|------|-----------|-----------------------|----------------------|
|
|
227
|
+
| `NativeExecutionHost` | Current local process environment | None | Composes with the selected isolation policy |
|
|
228
|
+
| `HerdrExecutionHost` **(experimental)** | Owned one-shot Herdr pane/workspace | `herdr` executable | `NoIsolation`; `DEVNULL` or `PIPE` stdin |
|
|
229
|
+
| `TmuxExecutionHost` **(experimental)** | Owned session, or owned window in a borrowed session | `tmux` executable | `NoIsolation`; `DEVNULL` or `PIPE` stdin |
|
|
230
|
+
| `TerminalWindowExecutionHost` **(experimental)** | New graphical terminal window | Supported or injected terminal launcher | Linux-focused; headless/machine-controlled; `NoIsolation`; `DEVNULL` or `PIPE` stdin |
|
|
231
|
+
|
|
232
|
+
Tmux placement is explicit when the default owned session is not suitable:
|
|
233
|
+
|
|
234
|
+
```python
|
|
235
|
+
TmuxExecutionHost() # uniquely named, AgentShell-owned session
|
|
236
|
+
TmuxExecutionHost(placement=TmuxPlacement.new_session(name="review"))
|
|
237
|
+
TmuxExecutionHost(placement=TmuxPlacement.new_window("existing", focus=False))
|
|
238
|
+
TmuxExecutionHost(placement=TmuxPlacement.current_session(focus=False))
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
The terminal host discovers a small built-in set of Linux terminal emulators. Set
|
|
242
|
+
`AGENTSHELL_TERMINAL_LAUNCHER` for a simple executable override, or inject a
|
|
243
|
+
`TerminalWindowLauncher`/`SubprocessTerminalLauncher` when the emulator needs custom arguments.
|
|
244
|
+
Its visible window mirrors the agent's raw streams, but the agent remains headless and controlled
|
|
245
|
+
by AgentShell; this is not the agent CLI's interactive harness interface.
|
|
208
246
|
|
|
209
247
|
`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.
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
248
|
+
user + PID namespaces, with a tiny PID 1 reaper and the CLI at PID 2 or later. Its constructor is:
|
|
249
|
+
|
|
250
|
+
```python
|
|
251
|
+
LinuxPidNamespaceIsolation(*, mount_proc: bool = True)
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
The default mounts a private `/proc` view, while `mount_proc=False` omits only that mount. The
|
|
255
|
+
opt-in mode retains the PID and direct-signal boundary, PID 1 reaper, and descendant cleanup, but
|
|
256
|
+
inherits the outer `/proc` view; `ps` and `/proc` can then expose host PID information that differs
|
|
257
|
+
from the child's `os.getpid()`. This is useful in restricted containers where the private proc
|
|
258
|
+
mount is denied, and it does not create a private mount namespace. The argument must be a real
|
|
259
|
+
`bool`; other values raise `TypeError`.
|
|
260
|
+
|
|
261
|
+
Both modes protect AgentShell's ancestors from direct same-user signals sent inside the child
|
|
262
|
+
namespace. The policy is not a filesystem, credential, network, tool, resource, or general
|
|
263
|
+
security sandbox. Background descendants terminate when the namespace ends.
|
|
264
|
+
|
|
265
|
+
When `isolation_policy` is omitted, `AgentShell` reads `AGENTSHELL_ISOLATION_POLICY` at
|
|
266
|
+
construction. The accepted values are `none` and `linux-pid-namespace`; if the variable is absent,
|
|
267
|
+
the default remains `NoIsolation`. Empty or unknown values raise `ValueError`. Passing an explicit
|
|
268
|
+
policy object takes precedence over the environment.
|
|
214
269
|
|
|
215
270
|
The Linux policy requires `unshare` and supporting kernel configuration. An explicit request that
|
|
216
271
|
cannot be satisfied raises `IsolationUnavailableError` before launching the CLI and never falls
|
|
217
|
-
back to `NoIsolation
|
|
218
|
-
`
|
|
272
|
+
back to `NoIsolation`; the availability probe uses the selected `mount_proc` mode. The environment
|
|
273
|
+
setting `AGENTSHELL_ISOLATION_POLICY=linux-pid-namespace` selects the strict default
|
|
274
|
+
`mount_proc=True` mode; use an explicit policy object for `mount_proc=False`. The host/policy
|
|
275
|
+
selection applies to `execute()`, `stream()`, and `health_check()`; model discovery and MCP
|
|
276
|
+
configuration remain local management operations.
|
|
219
277
|
|
|
220
278
|
### Model discovery semantics
|
|
221
279
|
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Public package exports for AgentShell."""
|
|
2
|
+
|
|
3
|
+
from agent_shell.herdr import (
|
|
4
|
+
HerdrClient,
|
|
5
|
+
HerdrExecutionHost,
|
|
6
|
+
HerdrPane,
|
|
7
|
+
HerdrUnavailableError,
|
|
8
|
+
)
|
|
9
|
+
from agent_shell.terminal_window import (
|
|
10
|
+
SubprocessTerminalLauncher,
|
|
11
|
+
TerminalWindowExecutionHost,
|
|
12
|
+
TerminalWindowLauncher,
|
|
13
|
+
TerminalWindowRunHandle,
|
|
14
|
+
TerminalWindowUnavailableError,
|
|
15
|
+
discover_terminal_launcher,
|
|
16
|
+
)
|
|
17
|
+
from agent_shell.tmux import TmuxExecutionHost, TmuxPlacement, TmuxUnavailableError
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"HerdrClient",
|
|
21
|
+
"HerdrExecutionHost",
|
|
22
|
+
"HerdrPane",
|
|
23
|
+
"HerdrUnavailableError",
|
|
24
|
+
"SubprocessTerminalLauncher",
|
|
25
|
+
"TerminalWindowExecutionHost",
|
|
26
|
+
"TerminalWindowLauncher",
|
|
27
|
+
"TerminalWindowRunHandle",
|
|
28
|
+
"TerminalWindowUnavailableError",
|
|
29
|
+
"TmuxExecutionHost",
|
|
30
|
+
"TmuxPlacement",
|
|
31
|
+
"TmuxUnavailableError",
|
|
32
|
+
"discover_terminal_launcher",
|
|
33
|
+
]
|
|
@@ -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.3.
|
|
22
|
-
__version_tuple__ = version_tuple = (0, 3,
|
|
21
|
+
__version__ = version = '0.3.2'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 3, 2)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|