agent-shell-py 0.3.0__tar.gz → 0.3.1__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 (126) hide show
  1. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/AGENTS.md +8 -6
  2. agent_shell_py-0.3.0/README.md → agent_shell_py-0.3.1/PKG-INFO +22 -2
  3. agent_shell_py-0.3.0/PKG-INFO → agent_shell_py-0.3.1/README.md +13 -11
  4. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/skills/invoking-cli-agents/SKILL.md +12 -2
  5. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/skills/invoking-cli-agents/api-reference.md +6 -1
  6. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/src/agent_shell/_version.py +2 -2
  7. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/src/agent_shell/shell.py +21 -1
  8. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/tests/conftest.py +6 -0
  9. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/tests/integration/test_process_lifecycle.py +21 -0
  10. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/tests/unit/test_shell.py +58 -1
  11. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/.github/workflows/build.yml +0 -0
  12. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/.github/workflows/ci.yml +0 -0
  13. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/.github/workflows/publish.yml +0 -0
  14. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/.gitignore +0 -0
  15. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/.python-version +0 -0
  16. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/LICENSE +0 -0
  17. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/docs/assets/skill_banner.png +0 -0
  18. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/docs/development/agent_parameter_comparison.md +0 -0
  19. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/docs/development/disabled_tools.md +0 -0
  20. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/docs/development/info.md +0 -0
  21. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/docs/development/total_token_count.md +0 -0
  22. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/pyproject.toml +0 -0
  23. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/skills/delegating-code-review/SKILL.md +0 -0
  24. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/src/agent_shell/__init__.py +0 -0
  25. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/src/agent_shell/adapters/__init__.py +0 -0
  26. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/src/agent_shell/adapters/agent_adapter_protocol.py +0 -0
  27. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/src/agent_shell/adapters/claude_code_adapter.py +0 -0
  28. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/src/agent_shell/adapters/codex_adapter.py +0 -0
  29. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/src/agent_shell/adapters/copilot_cli_adapter.py +0 -0
  30. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/src/agent_shell/adapters/cursor_adapter.py +0 -0
  31. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/src/agent_shell/adapters/grok_adapter.py +0 -0
  32. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/src/agent_shell/adapters/health.py +0 -0
  33. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/src/agent_shell/adapters/model_discovery.py +0 -0
  34. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/src/agent_shell/adapters/opencode_adapter.py +0 -0
  35. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/src/agent_shell/adapters/outcome.py +0 -0
  36. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/src/agent_shell/adapters/pi_adapter.py +0 -0
  37. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/src/agent_shell/adapters/process_failure.py +0 -0
  38. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/src/agent_shell/adapters/response.py +0 -0
  39. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/src/agent_shell/adapters/stderr_format.py +0 -0
  40. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/src/agent_shell/adapters/tool_denial.py +0 -0
  41. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/src/agent_shell/execution.py +0 -0
  42. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/src/agent_shell/models/__init__.py +0 -0
  43. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/src/agent_shell/models/agent.py +0 -0
  44. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/src/agent_shell/process_cleanup.py +0 -0
  45. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/tests/__init__.py +0 -0
  46. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/tests/e2e/__init__.py +0 -0
  47. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/tests/e2e/test_claude_code_e2e.py +0 -0
  48. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/tests/e2e/test_codex_e2e.py +0 -0
  49. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/tests/e2e/test_copilot_cli_e2e.py +0 -0
  50. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/tests/e2e/test_cursor_e2e.py +0 -0
  51. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/tests/e2e/test_execution_host_e2e.py +0 -0
  52. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/tests/e2e/test_grok_e2e.py +0 -0
  53. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/tests/e2e/test_health_check_e2e.py +0 -0
  54. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/tests/e2e/test_model_discovery_e2e.py +0 -0
  55. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/tests/e2e/test_opencode_e2e.py +0 -0
  56. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/tests/e2e/test_pi_e2e.py +0 -0
  57. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/tests/integration/__init__.py +0 -0
  58. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/tests/integration/test_claude_code_integration.py +0 -0
  59. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/tests/integration/test_claude_code_mcp_integration.py +0 -0
  60. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/tests/integration/test_codex_integration.py +0 -0
  61. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/tests/integration/test_codex_mcp_integration.py +0 -0
  62. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/tests/integration/test_copilot_cli_integration.py +0 -0
  63. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/tests/integration/test_copilot_cli_mcp_integration.py +0 -0
  64. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/tests/integration/test_cursor_integration.py +0 -0
  65. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/tests/integration/test_cursor_mcp_integration.py +0 -0
  66. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/tests/integration/test_execution_host.py +0 -0
  67. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/tests/integration/test_grok_integration.py +0 -0
  68. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/tests/integration/test_grok_mcp_integration.py +0 -0
  69. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/tests/integration/test_health_check_integration.py +0 -0
  70. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/tests/integration/test_model_discovery_integration.py +0 -0
  71. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/tests/integration/test_opencode_integration.py +0 -0
  72. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/tests/integration/test_opencode_mcp_integration.py +0 -0
  73. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/tests/integration/test_pi_integration.py +0 -0
  74. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/tests/integration/test_pi_mcp_integration.py +0 -0
  75. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/tests/unit/__init__.py +0 -0
  76. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/tests/unit/adapter_matrix.py +0 -0
  77. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/tests/unit/codex_fixtures.py +0 -0
  78. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/tests/unit/copilot_fixtures.py +0 -0
  79. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/tests/unit/cursor_fixtures.py +0 -0
  80. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/tests/unit/fixtures.py +0 -0
  81. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/tests/unit/grok_fixtures.py +0 -0
  82. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/tests/unit/opencode_fixtures.py +0 -0
  83. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/tests/unit/pi_fixtures.py +0 -0
  84. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/tests/unit/test_adapter_transport.py +0 -0
  85. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/tests/unit/test_cancel.py +0 -0
  86. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/tests/unit/test_codex_cancel.py +0 -0
  87. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/tests/unit/test_codex_execute.py +0 -0
  88. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/tests/unit/test_codex_parse_event.py +0 -0
  89. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/tests/unit/test_codex_warnings.py +0 -0
  90. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/tests/unit/test_copilot_cli_cancel.py +0 -0
  91. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/tests/unit/test_copilot_cli_execute.py +0 -0
  92. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/tests/unit/test_copilot_cli_parse_event.py +0 -0
  93. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/tests/unit/test_copilot_cli_stream.py +0 -0
  94. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/tests/unit/test_cursor_cancel.py +0 -0
  95. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/tests/unit/test_cursor_execute.py +0 -0
  96. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/tests/unit/test_cursor_parse_event.py +0 -0
  97. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/tests/unit/test_cursor_warnings.py +0 -0
  98. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/tests/unit/test_execute.py +0 -0
  99. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/tests/unit/test_execute_outcome.py +0 -0
  100. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/tests/unit/test_grok_cancel.py +0 -0
  101. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/tests/unit/test_grok_execute.py +0 -0
  102. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/tests/unit/test_grok_parse_event.py +0 -0
  103. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/tests/unit/test_grok_warnings.py +0 -0
  104. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/tests/unit/test_health_probe.py +0 -0
  105. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/tests/unit/test_mcp_server_spec.py +0 -0
  106. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/tests/unit/test_model_discovery.py +0 -0
  107. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/tests/unit/test_models.py +0 -0
  108. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/tests/unit/test_opencode_cancel.py +0 -0
  109. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/tests/unit/test_opencode_execute.py +0 -0
  110. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/tests/unit/test_opencode_parse_event.py +0 -0
  111. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/tests/unit/test_opencode_spawn.py +0 -0
  112. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/tests/unit/test_opencode_stream.py +0 -0
  113. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/tests/unit/test_parse_event.py +0 -0
  114. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/tests/unit/test_pi_cancel.py +0 -0
  115. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/tests/unit/test_pi_execute.py +0 -0
  116. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/tests/unit/test_pi_parse_event.py +0 -0
  117. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/tests/unit/test_pi_warnings.py +0 -0
  118. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/tests/unit/test_process_cleanup.py +0 -0
  119. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/tests/unit/test_process_group_registration.py +0 -0
  120. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/tests/unit/test_response_aggregation.py +0 -0
  121. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/tests/unit/test_shell_cancellation.py +0 -0
  122. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/tests/unit/test_shell_mcp.py +0 -0
  123. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/tests/unit/test_stderr_format.py +0 -0
  124. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/tests/unit/test_stream.py +0 -0
  125. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/tests/unit/test_tool_denial.py +0 -0
  126. {agent_shell_py-0.3.0 → agent_shell_py-0.3.1}/uv.lock +0 -0
@@ -158,12 +158,14 @@ classDiagram
158
158
  The adapter pattern uses Python's `Protocol` (structural typing) rather than ABC, so adapters satisfy the contract implicitly without inheritance. Each adapter translates agent-specific CLI flags and NDJSON output into the shared `StreamEvent`/`AgentResponse` models, while the selected `ExecutionHost` owns process creation and returns a per-run `RunHandle`.
159
159
 
160
160
  Execution location and protection are separate axes. Existing callers default to
161
- `NativeExecutionHost()` plus `NoIsolation()`. `LinuxPidNamespaceIsolation` is an opt-in direct
162
- signal boundary: a tiny init/reaper is PID 1 and the CLI is PID 2 or later, so child-namespace
163
- processes cannot see or signal AgentShell's ancestors. It requires Linux, `unshare`, and enabled
164
- unprivileged user/PID namespaces; an unavailable explicit request raises
165
- `IsolationUnavailableError` and never falls back. This is not a general sandbox and does not
166
- restrict filesystem, credentials, network, tools, or resources. The host/policy applies to
161
+ `NativeExecutionHost()` plus `NoIsolation()` when `AGENTSHELL_ISOLATION_POLICY` is unset. The
162
+ environment accepts `none` or `linux-pid-namespace` as a process-wide construction default;
163
+ explicit `isolation_policy=` takes precedence, and invalid or empty values raise `ValueError`.
164
+ `LinuxPidNamespaceIsolation` is a direct signal boundary: a tiny init/reaper is PID 1 and the CLI
165
+ is PID 2 or later, so child-namespace processes cannot see or signal AgentShell's ancestors. It
166
+ requires Linux, `unshare`, and enabled unprivileged user/PID namespaces; an unavailable request
167
+ raises `IsolationUnavailableError` and never falls back. This is not a general sandbox and does
168
+ not restrict filesystem, credentials, network, tools, or resources. The host/policy applies to
167
169
  `execute()`, `stream()`, and `health_check()`; model discovery and MCP configuration remain local.
168
170
 
169
171
  `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,3 +1,12 @@
1
+ Metadata-Version: 2.5
2
+ Name: agent-shell-py
3
+ Version: 0.3.1
4
+ Summary: A lightweight abstraction for executing CLI coding agents headlessly
5
+ License-Expression: MIT
6
+ License-File: LICENSE
7
+ Requires-Python: >=3.12
8
+ Description-Content-Type: text/markdown
9
+
1
10
  # Agent Shell
2
11
  Agent Shell is a light weight abstraction for executing a cli coding agent headlessly
3
12
  and returning the output that can be used programatically as a unified contract
@@ -74,13 +83,24 @@ separately.
74
83
 
75
84
  ### Execution host and isolation
76
85
 
77
- Existing callers remain unchanged. Omitting both settings means
78
- `NativeExecutionHost()` plus `NoIsolation()`:
86
+ Existing callers remain unchanged when `AGENTSHELL_ISOLATION_POLICY` is unset. Omitting both
87
+ constructor settings then means `NativeExecutionHost()` plus `NoIsolation()`:
79
88
 
80
89
  ```python
81
90
  shell = AgentShell(agent_type=AgentType.CLAUDE_CODE)
82
91
  ```
83
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
+
84
104
  To protect the AgentShell owner from broad same-user cleanup commands such as `pkill -f`,
85
105
  explicitly request Linux PID namespace isolation:
86
106
 
@@ -1,12 +1,3 @@
1
- Metadata-Version: 2.5
2
- Name: agent-shell-py
3
- Version: 0.3.0
4
- Summary: A lightweight abstraction for executing CLI coding agents headlessly
5
- License-Expression: MIT
6
- License-File: LICENSE
7
- Requires-Python: >=3.12
8
- Description-Content-Type: text/markdown
9
-
10
1
  # Agent Shell
11
2
  Agent Shell is a light weight abstraction for executing a cli coding agent headlessly
12
3
  and returning the output that can be used programatically as a unified contract
@@ -83,13 +74,24 @@ separately.
83
74
 
84
75
  ### Execution host and isolation
85
76
 
86
- Existing callers remain unchanged. Omitting both settings means
87
- `NativeExecutionHost()` plus `NoIsolation()`:
77
+ Existing callers remain unchanged when `AGENTSHELL_ISOLATION_POLICY` is unset. Omitting both
78
+ constructor settings then means `NativeExecutionHost()` plus `NoIsolation()`:
88
79
 
89
80
  ```python
90
81
  shell = AgentShell(agent_type=AgentType.CLAUDE_CODE)
91
82
  ```
92
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
+
93
95
  To protect the AgentShell owner from broad same-user cleanup commands such as `pkill -f`,
94
96
  explicitly request Linux PID namespace isolation:
95
97
 
@@ -52,14 +52,24 @@ management. All are async. Invocation has three independent choices:
52
52
 
53
53
  ### Execution Host and Isolation Policy
54
54
 
55
- Existing code is backward-compatible: omitting both execution settings selects native execution
56
- with no isolation.
55
+ Existing code is backward-compatible: when `AGENTSHELL_ISOLATION_POLICY` is unset, omitting both
56
+ execution settings selects native execution with no isolation.
57
57
 
58
58
  ```python
59
59
  shell = AgentShell(agent_type=AgentType.CODEX)
60
60
  # Equivalent to NativeExecutionHost() + NoIsolation()
61
61
  ```
62
62
 
63
+ For a process-wide deployment default, set the environment before constructing a shell:
64
+
65
+ ```bash
66
+ export AGENTSHELL_ISOLATION_POLICY=linux-pid-namespace
67
+ ```
68
+
69
+ Accepted values are `none` and `linux-pid-namespace`. Empty or unknown values raise `ValueError`.
70
+ An explicit `isolation_policy=` object wins over the environment, which remains the way to select
71
+ a custom policy or override the deployment default for one shell.
72
+
63
73
  Opt into direct-signal protection when an agent may run broad same-user cleanup commands such as
64
74
  `pkill -f`:
65
75
 
@@ -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
 
@@ -212,6 +212,11 @@ AgentShell's ancestors from direct same-user signals sent inside the child names
212
212
  filesystem, credential, network, tool, resource, or general security sandbox. Background
213
213
  descendants terminate when the namespace ends.
214
214
 
215
+ When `isolation_policy` is omitted, `AgentShell` reads `AGENTSHELL_ISOLATION_POLICY` at
216
+ construction. The accepted values are `none` and `linux-pid-namespace`; if the variable is absent,
217
+ the default remains `NoIsolation`. Empty or unknown values raise `ValueError`. Passing an explicit
218
+ policy object takes precedence over the environment.
219
+
215
220
  The Linux policy requires `unshare` and supporting kernel configuration. An explicit request that
216
221
  cannot be satisfied raises `IsolationUnavailableError` before launching the CLI and never falls
217
222
  back to `NoIsolation`. The host/policy selection applies to `execute()`, `stream()`, and
@@ -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.1'
22
+ __version_tuple__ = version_tuple = (0, 3, 1)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -1,4 +1,5 @@
1
1
  import asyncio
2
+ import os
2
3
  from pathlib import Path
3
4
  from typing import AsyncIterator
4
5
 
@@ -13,6 +14,7 @@ from agent_shell.adapters.pi_adapter import PiAdapter
13
14
  from agent_shell.execution import (
14
15
  ExecutionHost,
15
16
  IsolationPolicy,
17
+ LinuxPidNamespaceIsolation,
16
18
  NativeExecutionHost,
17
19
  NoIsolation,
18
20
  )
@@ -37,6 +39,22 @@ _ADAPTERS: dict[AgentType, type[AgentAdapter]] = {
37
39
  AgentType.GROK: GrokAdapter,
38
40
  }
39
41
 
42
+ _ISOLATION_POLICY_ENV = "AGENTSHELL_ISOLATION_POLICY"
43
+
44
+
45
+ def _isolation_policy_from_environment() -> IsolationPolicy:
46
+ value = os.environ.get(_ISOLATION_POLICY_ENV)
47
+ if value is None:
48
+ return NoIsolation()
49
+ if value == "none":
50
+ return NoIsolation()
51
+ if value == "linux-pid-namespace":
52
+ return LinuxPidNamespaceIsolation()
53
+ raise ValueError(
54
+ f"Unsupported {_ISOLATION_POLICY_ENV} value {value!r}: "
55
+ "expected 'none' or 'linux-pid-namespace'"
56
+ )
57
+
40
58
 
41
59
  class AgentShell():
42
60
  def __init__(
@@ -49,7 +67,9 @@ class AgentShell():
49
67
  execution_host if execution_host is not None else NativeExecutionHost()
50
68
  )
51
69
  self.isolation_policy = (
52
- isolation_policy if isolation_policy is not None else NoIsolation()
70
+ isolation_policy
71
+ if isolation_policy is not None
72
+ else _isolation_policy_from_environment()
53
73
  )
54
74
  self._adapter = self._resolve_adapter(agent_type=agent_type)
55
75
 
@@ -4,6 +4,12 @@ import pytest
4
4
  from agent_shell import process_cleanup
5
5
 
6
6
 
7
+ @pytest.fixture(autouse=True)
8
+ def isolate_agentshell_environment(monkeypatch):
9
+ """Keep deployment-level AgentShell configuration explicit in each test."""
10
+ monkeypatch.delenv("AGENTSHELL_ISOLATION_POLICY", raising=False)
11
+
12
+
7
13
  @pytest.fixture(autouse=True)
8
14
  def isolate_process_group_registry():
9
15
  """Give every test an isolated guardian registry and close anything it leaks."""
@@ -510,6 +510,27 @@ async def test_agentshell_applies_the_requested_pid_isolation(fake_cli, tmp_path
510
510
  assert response.response == ""
511
511
 
512
512
 
513
+ async def test_agentshell_applies_pid_isolation_selected_by_environment(
514
+ fake_cli, tmp_path, monkeypatch):
515
+ # Arrange — exercise the deployment-level configuration through the public AgentShell
516
+ # boundary. Namespace PID 2 proves the selected policy reached the real CLI process.
517
+ monkeypatch.setenv(
518
+ "AGENTSHELL_ISOLATION_POLICY",
519
+ "linux-pid-namespace",
520
+ )
521
+ shell = AgentShell(agent_type=AgentType.CLAUDE_CODE)
522
+ fake_cli(stdout=[OK_RESULT_EVENT[ClaudeCodeAdapter]], required_pid=2)
523
+
524
+ # Act
525
+ try:
526
+ response = await shell.execute(cwd=str(tmp_path), prompt="ping")
527
+ except IsolationUnavailableError as error:
528
+ pytest.skip(str(error))
529
+
530
+ # Assert
531
+ assert response.response == ""
532
+
533
+
513
534
  @pytest.mark.parametrize("adapter_cls", ADAPTERS)
514
535
  async def test_cancelled_agent_shell_stream_kills_the_real_process_tree(
515
536
  adapter_cls, fake_cli, tmp_path):
@@ -8,12 +8,52 @@ from agent_shell.adapters.cursor_adapter import CursorAdapter
8
8
  from agent_shell.adapters.grok_adapter import GrokAdapter
9
9
  from agent_shell.adapters.opencode_adapter import OpenCodeAdapter
10
10
  from agent_shell.adapters.pi_adapter import PiAdapter
11
- from agent_shell.execution import NativeExecutionHost, NoIsolation
11
+ from agent_shell.execution import (
12
+ LinuxPidNamespaceIsolation,
13
+ NativeExecutionHost,
14
+ NoIsolation,
15
+ )
12
16
  from agent_shell.models.agent import AgentResponse, AgentType
13
17
  from agent_shell.shell import AgentShell
14
18
 
15
19
 
16
20
  class TestExecutionDefaults:
21
+ def test_environment_can_select_linux_pid_namespace_isolation(self, monkeypatch):
22
+ # Arrange
23
+ monkeypatch.setenv(
24
+ "AGENTSHELL_ISOLATION_POLICY",
25
+ "linux-pid-namespace",
26
+ )
27
+
28
+ # Act
29
+ shell = AgentShell(agent_type=AgentType.CLAUDE_CODE)
30
+
31
+ # Assert
32
+ assert isinstance(shell.isolation_policy, LinuxPidNamespaceIsolation)
33
+
34
+ @pytest.mark.parametrize("value", ["", "container"])
35
+ def test_invalid_environment_is_rejected_instead_of_disabling_isolation(
36
+ self, monkeypatch, value):
37
+ # Arrange
38
+ monkeypatch.setenv("AGENTSHELL_ISOLATION_POLICY", value)
39
+
40
+ # Act / Assert
41
+ with pytest.raises(
42
+ ValueError,
43
+ match="AGENTSHELL_ISOLATION_POLICY",
44
+ ):
45
+ AgentShell(agent_type=AgentType.CLAUDE_CODE)
46
+
47
+ def test_environment_can_explicitly_select_no_isolation(self, monkeypatch):
48
+ # Arrange
49
+ monkeypatch.setenv("AGENTSHELL_ISOLATION_POLICY", "none")
50
+
51
+ # Act
52
+ shell = AgentShell(agent_type=AgentType.CLAUDE_CODE)
53
+
54
+ # Assert
55
+ assert isinstance(shell.isolation_policy, NoIsolation)
56
+
17
57
  def test_existing_constructor_defaults_to_native_without_isolation(self):
18
58
  # Arrange / Act
19
59
  shell = AgentShell(agent_type=AgentType.CLAUDE_CODE)
@@ -38,6 +78,23 @@ class TestExecutionDefaults:
38
78
  assert shell.execution_host is host
39
79
  assert shell.isolation_policy is policy
40
80
 
81
+ def test_explicit_isolation_policy_overrides_environment(self, monkeypatch):
82
+ # Arrange
83
+ monkeypatch.setenv(
84
+ "AGENTSHELL_ISOLATION_POLICY",
85
+ "linux-pid-namespace",
86
+ )
87
+ policy = NoIsolation()
88
+
89
+ # Act
90
+ shell = AgentShell(
91
+ agent_type=AgentType.CLAUDE_CODE,
92
+ isolation_policy=policy,
93
+ )
94
+
95
+ # Assert
96
+ assert shell.isolation_policy is policy
97
+
41
98
 
42
99
  class TestResolveAdapter:
43
100
  def test_resolves_claude_code(self):
File without changes
File without changes