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.
Files changed (144) hide show
  1. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/AGENTS.md +36 -6
  2. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/PKG-INFO +148 -8
  3. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/README.md +147 -7
  4. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/skills/invoking-cli-agents/SKILL.md +39 -7
  5. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/skills/invoking-cli-agents/api-reference.md +67 -9
  6. agent_shell_py-0.3.2/src/agent_shell/__init__.py +33 -0
  7. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/src/agent_shell/_version.py +2 -2
  8. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/src/agent_shell/execution.py +56 -13
  9. agent_shell_py-0.3.2/src/agent_shell/herdr.py +629 -0
  10. agent_shell_py-0.3.2/src/agent_shell/herdr_protocol.py +45 -0
  11. agent_shell_py-0.3.2/src/agent_shell/herdr_worker.py +300 -0
  12. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/src/agent_shell/process_cleanup.py +6 -0
  13. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/src/agent_shell/shell.py +21 -1
  14. agent_shell_py-0.3.2/src/agent_shell/terminal_protocol.py +42 -0
  15. agent_shell_py-0.3.2/src/agent_shell/terminal_window.py +710 -0
  16. agent_shell_py-0.3.2/src/agent_shell/terminal_worker.py +261 -0
  17. agent_shell_py-0.3.2/src/agent_shell/tmux.py +596 -0
  18. agent_shell_py-0.3.2/src/agent_shell/tmux_bridge.py +232 -0
  19. agent_shell_py-0.3.2/src/agent_shell/tmux_protocol.py +44 -0
  20. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/conftest.py +6 -0
  21. agent_shell_py-0.3.2/tests/e2e/test_execution_host_e2e.py +291 -0
  22. agent_shell_py-0.3.2/tests/e2e/test_terminal_window_execution_host_e2e.py +194 -0
  23. agent_shell_py-0.3.2/tests/e2e/test_tmux_execution_host_e2e.py +238 -0
  24. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/integration/test_execution_host.py +262 -5
  25. agent_shell_py-0.3.2/tests/integration/test_herdr_execution_host.py +1022 -0
  26. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/integration/test_process_lifecycle.py +99 -8
  27. agent_shell_py-0.3.2/tests/integration/test_terminal_window_execution_host.py +877 -0
  28. agent_shell_py-0.3.2/tests/integration/test_tmux_execution_host.py +710 -0
  29. agent_shell_py-0.3.2/tests/unit/test_public_exports.py +16 -0
  30. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/unit/test_shell.py +58 -1
  31. agent_shell_py-0.3.2/tests/unit/test_terminal_window_execution_host.py +83 -0
  32. agent_shell_py-0.3.0/tests/e2e/test_execution_host_e2e.py +0 -46
  33. agent_shell_py-0.3.0/tests/unit/__init__.py +0 -0
  34. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/.github/workflows/build.yml +0 -0
  35. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/.github/workflows/ci.yml +0 -0
  36. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/.github/workflows/publish.yml +0 -0
  37. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/.gitignore +0 -0
  38. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/.python-version +0 -0
  39. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/LICENSE +0 -0
  40. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/docs/assets/skill_banner.png +0 -0
  41. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/docs/development/agent_parameter_comparison.md +0 -0
  42. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/docs/development/disabled_tools.md +0 -0
  43. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/docs/development/info.md +0 -0
  44. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/docs/development/total_token_count.md +0 -0
  45. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/pyproject.toml +0 -0
  46. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/skills/delegating-code-review/SKILL.md +0 -0
  47. {agent_shell_py-0.3.0/src/agent_shell → agent_shell_py-0.3.2/src/agent_shell/adapters}/__init__.py +0 -0
  48. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/src/agent_shell/adapters/agent_adapter_protocol.py +0 -0
  49. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/src/agent_shell/adapters/claude_code_adapter.py +0 -0
  50. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/src/agent_shell/adapters/codex_adapter.py +0 -0
  51. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/src/agent_shell/adapters/copilot_cli_adapter.py +0 -0
  52. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/src/agent_shell/adapters/cursor_adapter.py +0 -0
  53. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/src/agent_shell/adapters/grok_adapter.py +0 -0
  54. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/src/agent_shell/adapters/health.py +0 -0
  55. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/src/agent_shell/adapters/model_discovery.py +0 -0
  56. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/src/agent_shell/adapters/opencode_adapter.py +0 -0
  57. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/src/agent_shell/adapters/outcome.py +0 -0
  58. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/src/agent_shell/adapters/pi_adapter.py +0 -0
  59. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/src/agent_shell/adapters/process_failure.py +0 -0
  60. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/src/agent_shell/adapters/response.py +0 -0
  61. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/src/agent_shell/adapters/stderr_format.py +0 -0
  62. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/src/agent_shell/adapters/tool_denial.py +0 -0
  63. {agent_shell_py-0.3.0/src/agent_shell/adapters → agent_shell_py-0.3.2/src/agent_shell/models}/__init__.py +0 -0
  64. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/src/agent_shell/models/agent.py +0 -0
  65. {agent_shell_py-0.3.0/src/agent_shell/models → agent_shell_py-0.3.2/tests}/__init__.py +0 -0
  66. {agent_shell_py-0.3.0/tests → agent_shell_py-0.3.2/tests/e2e}/__init__.py +0 -0
  67. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/e2e/test_claude_code_e2e.py +0 -0
  68. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/e2e/test_codex_e2e.py +0 -0
  69. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/e2e/test_copilot_cli_e2e.py +0 -0
  70. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/e2e/test_cursor_e2e.py +0 -0
  71. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/e2e/test_grok_e2e.py +0 -0
  72. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/e2e/test_health_check_e2e.py +0 -0
  73. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/e2e/test_model_discovery_e2e.py +0 -0
  74. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/e2e/test_opencode_e2e.py +0 -0
  75. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/e2e/test_pi_e2e.py +0 -0
  76. {agent_shell_py-0.3.0/tests/e2e → agent_shell_py-0.3.2/tests/integration}/__init__.py +0 -0
  77. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/integration/test_claude_code_integration.py +0 -0
  78. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/integration/test_claude_code_mcp_integration.py +0 -0
  79. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/integration/test_codex_integration.py +0 -0
  80. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/integration/test_codex_mcp_integration.py +0 -0
  81. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/integration/test_copilot_cli_integration.py +0 -0
  82. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/integration/test_copilot_cli_mcp_integration.py +0 -0
  83. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/integration/test_cursor_integration.py +0 -0
  84. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/integration/test_cursor_mcp_integration.py +0 -0
  85. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/integration/test_grok_integration.py +0 -0
  86. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/integration/test_grok_mcp_integration.py +0 -0
  87. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/integration/test_health_check_integration.py +0 -0
  88. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/integration/test_model_discovery_integration.py +0 -0
  89. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/integration/test_opencode_integration.py +0 -0
  90. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/integration/test_opencode_mcp_integration.py +0 -0
  91. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/integration/test_pi_integration.py +0 -0
  92. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/integration/test_pi_mcp_integration.py +0 -0
  93. {agent_shell_py-0.3.0/tests/integration → agent_shell_py-0.3.2/tests/unit}/__init__.py +0 -0
  94. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/unit/adapter_matrix.py +0 -0
  95. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/unit/codex_fixtures.py +0 -0
  96. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/unit/copilot_fixtures.py +0 -0
  97. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/unit/cursor_fixtures.py +0 -0
  98. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/unit/fixtures.py +0 -0
  99. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/unit/grok_fixtures.py +0 -0
  100. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/unit/opencode_fixtures.py +0 -0
  101. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/unit/pi_fixtures.py +0 -0
  102. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/unit/test_adapter_transport.py +0 -0
  103. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/unit/test_cancel.py +0 -0
  104. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/unit/test_codex_cancel.py +0 -0
  105. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/unit/test_codex_execute.py +0 -0
  106. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/unit/test_codex_parse_event.py +0 -0
  107. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/unit/test_codex_warnings.py +0 -0
  108. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/unit/test_copilot_cli_cancel.py +0 -0
  109. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/unit/test_copilot_cli_execute.py +0 -0
  110. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/unit/test_copilot_cli_parse_event.py +0 -0
  111. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/unit/test_copilot_cli_stream.py +0 -0
  112. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/unit/test_cursor_cancel.py +0 -0
  113. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/unit/test_cursor_execute.py +0 -0
  114. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/unit/test_cursor_parse_event.py +0 -0
  115. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/unit/test_cursor_warnings.py +0 -0
  116. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/unit/test_execute.py +0 -0
  117. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/unit/test_execute_outcome.py +0 -0
  118. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/unit/test_grok_cancel.py +0 -0
  119. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/unit/test_grok_execute.py +0 -0
  120. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/unit/test_grok_parse_event.py +0 -0
  121. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/unit/test_grok_warnings.py +0 -0
  122. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/unit/test_health_probe.py +0 -0
  123. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/unit/test_mcp_server_spec.py +0 -0
  124. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/unit/test_model_discovery.py +0 -0
  125. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/unit/test_models.py +0 -0
  126. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/unit/test_opencode_cancel.py +0 -0
  127. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/unit/test_opencode_execute.py +0 -0
  128. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/unit/test_opencode_parse_event.py +0 -0
  129. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/unit/test_opencode_spawn.py +0 -0
  130. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/unit/test_opencode_stream.py +0 -0
  131. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/unit/test_parse_event.py +0 -0
  132. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/unit/test_pi_cancel.py +0 -0
  133. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/unit/test_pi_execute.py +0 -0
  134. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/unit/test_pi_parse_event.py +0 -0
  135. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/unit/test_pi_warnings.py +0 -0
  136. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/unit/test_process_cleanup.py +0 -0
  137. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/unit/test_process_group_registration.py +0 -0
  138. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/unit/test_response_aggregation.py +0 -0
  139. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/unit/test_shell_cancellation.py +0 -0
  140. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/unit/test_shell_mcp.py +0 -0
  141. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/unit/test_stderr_format.py +0 -0
  142. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/unit/test_stream.py +0 -0
  143. {agent_shell_py-0.3.0 → agent_shell_py-0.3.2}/tests/unit/test_tool_denial.py +0 -0
  144. {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()`. `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
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.0
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 settings means
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 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.
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` 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.
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 settings means
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 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.
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` 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.
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 (`NativeExecutionHost` today)
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: omitting both execution settings selects native execution
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
- 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
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. `NativeExecutionHost` is the only shipped host today. Tmux and Herdr are
84
- future host possibilities, not current APIs.
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, # default NoIsolation()
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
- `NativeExecutionHost` is currently the only concrete host. Agent adapters use handles internally;
207
- existing `execute()` and `stream()` callers do not need to manage them.
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. 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.
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`. The host/policy selection applies to `execute()`, `stream()`, and
218
- `health_check()`; model discovery and MCP configuration remain local management operations.
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.0'
22
- __version_tuple__ = version_tuple = (0, 3, 0)
21
+ __version__ = version = '0.3.2'
22
+ __version_tuple__ = version_tuple = (0, 3, 2)
23
23
 
24
24
  __commit_id__ = commit_id = None