mcp-yieldshell 0.4.0__tar.gz → 0.5.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.
- {mcp_yieldshell-0.4.0 → mcp_yieldshell-0.5.1}/PKG-INFO +63 -42
- {mcp_yieldshell-0.4.0 → mcp_yieldshell-0.5.1}/README.md +62 -41
- {mcp_yieldshell-0.4.0 → mcp_yieldshell-0.5.1}/pyproject.toml +1 -1
- {mcp_yieldshell-0.4.0 → mcp_yieldshell-0.5.1}/src/mcp_yieldshell/config.py +55 -18
- mcp_yieldshell-0.5.1/src/mcp_yieldshell/policy.py +12 -0
- mcp_yieldshell-0.5.1/src/mcp_yieldshell/process/managed.py +57 -0
- mcp_yieldshell-0.5.1/src/mcp_yieldshell/process/manager.py +1151 -0
- mcp_yieldshell-0.5.1/src/mcp_yieldshell/process/ring_buffer.py +207 -0
- {mcp_yieldshell-0.4.0 → mcp_yieldshell-0.5.1}/src/mcp_yieldshell/process/spawn.py +22 -5
- mcp_yieldshell-0.5.1/src/mcp_yieldshell/security.py +194 -0
- {mcp_yieldshell-0.4.0 → mcp_yieldshell-0.5.1}/src/mcp_yieldshell/server.py +37 -8
- mcp_yieldshell-0.5.1/src/mcp_yieldshell/side_effects.py +109 -0
- {mcp_yieldshell-0.4.0 → mcp_yieldshell-0.5.1}/tests/test_config.py +82 -8
- {mcp_yieldshell-0.4.0 → mcp_yieldshell-0.5.1}/tests/test_integration.py +403 -11
- mcp_yieldshell-0.5.1/tests/test_lifecycle_hardening.py +944 -0
- mcp_yieldshell-0.5.1/tests/test_protocol.py +140 -0
- {mcp_yieldshell-0.4.0 → mcp_yieldshell-0.5.1}/tests/test_release.py +137 -0
- mcp_yieldshell-0.5.1/tests/test_ring_buffer.py +319 -0
- mcp_yieldshell-0.5.1/tests/test_security.py +323 -0
- mcp_yieldshell-0.4.0/src/mcp_yieldshell/process/manager.py +0 -844
- mcp_yieldshell-0.4.0/src/mcp_yieldshell/process/ring_buffer.py +0 -114
- mcp_yieldshell-0.4.0/src/mcp_yieldshell/security.py +0 -47
- mcp_yieldshell-0.4.0/tests/test_ring_buffer.py +0 -192
- mcp_yieldshell-0.4.0/tests/test_security.py +0 -102
- {mcp_yieldshell-0.4.0 → mcp_yieldshell-0.5.1}/.gitignore +0 -0
- {mcp_yieldshell-0.4.0 → mcp_yieldshell-0.5.1}/src/mcp_yieldshell/__init__.py +0 -0
- {mcp_yieldshell-0.4.0 → mcp_yieldshell-0.5.1}/src/mcp_yieldshell/__main__.py +0 -0
- {mcp_yieldshell-0.4.0 → mcp_yieldshell-0.5.1}/src/mcp_yieldshell/process/__init__.py +0 -0
- {mcp_yieldshell-0.4.0 → mcp_yieldshell-0.5.1}/src/mcp_yieldshell/types.py +0 -0
- {mcp_yieldshell-0.4.0 → mcp_yieldshell-0.5.1}/tests/__init__.py +0 -0
- {mcp_yieldshell-0.4.0 → mcp_yieldshell-0.5.1}/tests/test_side_effects.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mcp-yieldshell
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.1
|
|
4
4
|
Summary: A drop-in shell MCP that auto-yields long-running commands into managed background processes.
|
|
5
5
|
Requires-Python: >=3.11
|
|
6
6
|
Requires-Dist: mcp<2,>=1.9.0
|
|
@@ -14,11 +14,11 @@ A drop-in shell MCP server that auto-yields long-running commands into managed b
|
|
|
14
14
|
|
|
15
15
|
Most shell tools present a frustrating choice: either block the LLM agent until the command finishes, or force the agent to decide upfront that a command should run in the background.
|
|
16
16
|
|
|
17
|
-
**YieldShell MCP** solves this by keeping normal foreground semantics for fast commands, then automatically promoting long-running commands into managed background processes after a
|
|
17
|
+
**YieldShell MCP** solves this by keeping normal foreground semantics for fast commands, then automatically promoting long-running commands into managed background processes after a delay (`yield_ms`, default: 30 seconds).
|
|
18
18
|
|
|
19
19
|
```mermaid
|
|
20
20
|
graph TD
|
|
21
|
-
A[exec_command] --> B["Wait for yield_ms (default:
|
|
21
|
+
A[exec_command] --> B["Wait for yield_ms (default: 30s)"]
|
|
22
22
|
B --> C{Is process still running?}
|
|
23
23
|
C -->|Yes| D["backgrounded<br>Returns process_id"]
|
|
24
24
|
C -->|No| E["completed<br>Returns full output"]
|
|
@@ -146,15 +146,16 @@ Execute a shell command. If the command runs longer than `yield_ms`, it yields a
|
|
|
146
146
|
* `command` (string, **required**): The command string to execute in the shell.
|
|
147
147
|
* `side_effects` (array of string, **required**): The side-effect categories this command may plausibly have. Must contain at least one entry drawn from the enum below. Use `["NONE"]` for commands with no meaningful side effects. `NONE` is exclusive and must not be combined with any other category. The server rejects the call with `failed_to_start` if any declared category is configured as blocked.
|
|
148
148
|
* Allowed values: `CHANGES_NETWORK_CONFIGURATION`, `CHANGES_PACKAGES_OR_DEPENDENCIES`, `CONSUMES_SIGNIFICANT_RESOURCES`, `DELETES_FILES`, `EXPOSES_SECRETS`, `KILLS_AGENT_PROCESS`, `MAKES_NETWORK_REQUESTS`, `MODIFIES_OS_SETTINGS`, `MODIFIES_OS_USER_SETTINGS`, `MODIFIES_OUTSIDE_WORKSPACE`, `MODIFIES_PRODUCTION_SERVICES`, `MODIFIES_PROTECTED_FILES`, `MODIFIES_SECURITY_CONTROLS`, `MODIFIES_WORKSPACE_FILES`, `NONE`, `OTHER`, `RUNS_INLINE_CODE`, `RUNS_PRIVILEGED_COMMANDS`, `STOPS_OR_RESTARTS_SERVICES`, `UNKNOWN`, `USES_DESTRUCTIVE_GIT_OPERATION`.
|
|
149
|
-
* `RUNS_INLINE_CODE` is in the default blocklist. It covers commands that execute code supplied inline to an interpreter or shell (e.g. `python -c`, `node -e`, `curl ... | sh`). It does not cover simply creating a script or executable file unless the same command also executes inline code. The safer next action is to write the content to a reviewable workspace file and execute it in a small, inspectable step. Operators can
|
|
149
|
+
* `RUNS_INLINE_CODE` is in the default blocklist. It covers commands that execute code supplied inline to an interpreter or shell (e.g. `python -c`, `node -e`, `curl ... | sh`). It does not cover simply creating a script or executable file unless the same command also executes inline code. The safer next action is to write the content to a reviewable workspace file and execute it in a small, inspectable step. Operators can clear the blocklist via `MCP_YIELDSHELL_BLOCKED_SIDE_EFFECTS=,`.
|
|
150
150
|
* `cwd` (string, optional): Working directory for the command. Must be under allowed roots if `YIELDSHELL_ALLOWED_CWDS` is set. Defaults to `YIELDSHELL_DEFAULT_CWD`.
|
|
151
151
|
* `env` (object of string to string, optional): Additive environment variable overlay. Merged into the parent environment.
|
|
152
|
-
* `shell` (string, optional):
|
|
152
|
+
* `shell` (string, optional): Shell executable used to run the command. Defaults to the platform shell. Explicit shells are checked by the same allow/deny command policy.
|
|
153
153
|
* `stdin` (string, optional): Initial text input written to standard input immediately after spawning.
|
|
154
|
+
* `close_stdin` (boolean, default: `true`): Close standard input after the initial input is written. Set to `false` when follow-up `write` calls are expected.
|
|
154
155
|
* `name` (string, optional): A human-readable label/name to identify this process.
|
|
155
|
-
* `yield_ms` (integer, optional): Milliseconds to wait before yielding execution to background.
|
|
156
|
-
* `timeout_ms` (integer, optional): Total execution runtime limit in milliseconds. Process is
|
|
157
|
-
* `max_output_bytes` (integer, optional): Maximum
|
|
156
|
+
* `yield_ms` (integer, optional): Milliseconds to wait before yielding execution to background. Both omitted and explicit values are clamped to the lesser of `YIELDSHELL_MAX_YIELD_MS` and the transport-safe 55,000ms ceiling. Defaults to `YIELDSHELL_DEFAULT_YIELD_MS` (30,000ms).
|
|
157
|
+
* `timeout_ms` (integer, optional): Total execution runtime limit in milliseconds. Process is terminated if it runs longer than this. Defaults to `YIELDSHELL_DEFAULT_TIMEOUT_MS` (3,600,000ms). Pass `0` explicitly for unlimited execution.
|
|
158
|
+
* `max_output_bytes` (integer, optional): Maximum bytes retained per stdout/stderr ring buffer and returned in total across both response streams. Subject to the `YIELDSHELL_MAX_OUTPUT_BYTES` cap.
|
|
158
159
|
|
|
159
160
|
* **Side-Effects Guide**:
|
|
160
161
|
* `side_effects` is required and must be a non-empty list. Declare every plausible side-effect category before running the command.
|
|
@@ -171,27 +172,30 @@ Execute a shell command. If the command runs longer than `yield_ms`, it yields a
|
|
|
171
172
|
* Destructive file operations: `side_effects=["DELETES_FILES"]`
|
|
172
173
|
* Privileged command: `side_effects=["RUNS_PRIVILEGED_COMMANDS"]`
|
|
173
174
|
* Protected-file changes: `side_effects=["MODIFIES_PROTECTED_FILES"]`
|
|
174
|
-
* Inline code execution: prefer writing the content to a reviewable workspace file (for example `scripts/migrate.sql` or `tools/build.sh`) and run it in a small, inspectable step. Declaring `side_effects=["RUNS_INLINE_CODE"]` is rejected under the default policy; operators can clear that default with `MCP_YIELDSHELL_BLOCKED_SIDE_EFFECTS
|
|
175
|
+
* Inline code execution: prefer writing the content to a reviewable workspace file (for example `scripts/migrate.sql` or `tools/build.sh`) and run it in a small, inspectable step. Declaring `side_effects=["RUNS_INLINE_CODE"]` is rejected under the default policy; operators can clear that default with `MCP_YIELDSHELL_BLOCKED_SIDE_EFFECTS=,`.
|
|
175
176
|
|
|
176
177
|
* **Output Statuses**:
|
|
177
|
-
* `completed`: Process finished within `yield_ms`. Returns exit code, stdout, and stderr.
|
|
178
|
+
* `completed`: Process finished within `yield_ms`. Returns exit code, stdout, and stderr. If the response is truncated and terminal-process retention is enabled, it also returns `process_id` so retained output can be inspected with `read`.
|
|
178
179
|
* `backgrounded`: Process auto-yielded. Returns `process_id`, `pid`, a snapshot of initial stdout/stderr, `duration_ms`, `truncated`, and a `message` string describing that the process is running in the background.
|
|
179
180
|
* `timed_out`: Process exceeded `timeout_ms` and was terminated.
|
|
180
181
|
* `stopped`: Process was explicitly terminated.
|
|
181
182
|
* `failed_to_start`: Command could not be spawned (e.g., bad directory or policy violation).
|
|
182
183
|
* `failed`: An internal execution error occurred.
|
|
184
|
+
* If initial `stdin` delivery fails, responses for the retained process include
|
|
185
|
+
`stdin_error` with the transport error. Initial delivery runs as tracked
|
|
186
|
+
background work so pipe backpressure does not delay auto-yielding.
|
|
183
187
|
|
|
184
188
|
### `read`
|
|
185
189
|
Read stdout and/or stderr output from a running or completed background process.
|
|
186
190
|
|
|
187
191
|
* **Parameters**:
|
|
188
192
|
* `process_id` (string, **required**): Unique identifier of the process.
|
|
189
|
-
* `since_seq` (integer, optional):
|
|
190
|
-
* `max_output_bytes` (integer, optional): Clamps the
|
|
193
|
+
* `since_seq` (integer, optional): Byte-position cursor returned as `next_seq` by the previous read. Enables lossless incremental log polling.
|
|
194
|
+
* `max_output_bytes` (integer, optional): Clamps total output returned across the selected streams. Defaults to the server cap.
|
|
191
195
|
* `streams` (string, default: `"both"`): The streams to read. Options: `"both"`, `"stdout"`, or `"stderr"`.
|
|
192
196
|
|
|
193
197
|
* **Returns**:
|
|
194
|
-
* `process_id`, `status`, `exit_code`, `signal`, `next_seq` (
|
|
198
|
+
* `process_id`, `status`, `exit_code`, `signal`, `next_seq` (byte-position cursor to use in subsequent `since_seq` reads), `truncated` flag, and `stdin_error` when initial input delivery failed. `stdout` and `stderr` text are included based on the `streams` filter — `"both"` includes both, `"stdout"` includes only `stdout`, and `"stderr"` includes only `stderr`.
|
|
195
199
|
|
|
196
200
|
### `write`
|
|
197
201
|
Write text input to the standard input (`stdin`) of a running process.
|
|
@@ -200,58 +204,67 @@ Write text input to the standard input (`stdin`) of a running process.
|
|
|
200
204
|
* `process_id` (string, **required**): Unique identifier of the process.
|
|
201
205
|
* `input` (string, **required**): Text input to write.
|
|
202
206
|
* `newline` (boolean, default: `false`): If `true`, appends `\n` to the input.
|
|
207
|
+
* `close_stdin` (boolean, default: `false`): Close standard input after this write, delivering EOF to the process.
|
|
208
|
+
* Writes that remain backpressured are capped at the transport-safe 55-second
|
|
209
|
+
request ceiling and return `ok: false` with an error.
|
|
203
210
|
|
|
204
211
|
### `wait`
|
|
205
212
|
Block execution until the process exits or the wait timeout expires. This allows the LLM to pause and await completion without spawning a new execution loop.
|
|
206
213
|
|
|
207
214
|
* **Parameters**:
|
|
208
215
|
* `process_id` (string, **required**): Unique identifier of the process.
|
|
209
|
-
* `timeout_ms` (integer, default: `
|
|
210
|
-
* `max_output_bytes` (integer, optional): Maximum output bytes to return
|
|
216
|
+
* `timeout_ms` (integer, default: `55000`): Maximum time to wait.
|
|
217
|
+
* `max_output_bytes` (integer, optional): Maximum total output bytes to return across stdout and stderr.
|
|
211
218
|
|
|
212
219
|
* **Important**: If the wait timeout expires, `wait` returns the current status but **does not kill** the process. It continues running in the background.
|
|
213
220
|
* The effective wait duration is capped at 55 seconds to stay well under typical MCP request timeouts, even if a larger `timeout_ms` is requested.
|
|
214
|
-
* `wait` treats the
|
|
221
|
+
* `wait` treats the managed process group disappearing as completion. If the tracked shell exits while descendants in its process group remain alive, the record continues to report `running`. For normal process-group completion, stdout/stderr are drained before the response is returned. Final drain waits are bounded so inherited pipes cannot block a request indefinitely.
|
|
215
222
|
|
|
216
223
|
### `stop`
|
|
217
224
|
Gracefully terminate or force kill a running process.
|
|
218
225
|
|
|
219
226
|
* **Parameters**:
|
|
220
227
|
* `process_id` (string, **required**): Unique identifier of the process.
|
|
221
|
-
* `signal` (string, default: `"SIGTERM"`): OS signal to send (e.g. `SIGTERM`, `SIGKILL`, `SIGINT`).
|
|
222
|
-
* `force_after_ms` (integer, default: `
|
|
228
|
+
* `signal` (string, default: `"SIGTERM"`): OS signal to send (e.g. `SIGTERM`, `SIGKILL`, `SIGINT`). Invalid names are rejected without stopping the process. Valid names are ignored on Windows.
|
|
229
|
+
* `force_after_ms` (integer, default: `10000`): Grace period before escalating to force kill (`SIGKILL`). It is clamped below the transport-safe 55-second request ceiling so force-kill observation, final output drain, and subprocess reaping still fit within the request.
|
|
223
230
|
|
|
224
231
|
### `ps`
|
|
225
232
|
List all managed processes.
|
|
226
233
|
|
|
234
|
+
Terminal records are retained temporarily for inspection. Before each otherwise valid `exec` spawn, records older than `YIELDSHELL_PROCESS_RETENTION_MS` are removed and the remaining terminal set is reduced to `YIELDSHELL_MAX_RETAINED_PROCESSES`, oldest first. Running records are never automatically removed. If the shell exits but its process group still has live descendants, status remains `running` until descendants stop. Reaped IDs disappear from `ps` and become unknown to `read`, `wait`, `write`, and `stop`.
|
|
235
|
+
|
|
227
236
|
* **Parameters**:
|
|
228
237
|
* `include_completed` (boolean, default: `true`): If `false`, finished/stopped processes are excluded from the output.
|
|
229
238
|
* `limit` (integer, default: `50`): Maximum number of entries.
|
|
230
|
-
* **Returns**: `processes` — a list of process summary objects, each containing: `process_id`, `pid`, `name`, `command`, `cwd`, `status`, `exit_code`, `signal`, `started_at`, `ended_at`, `duration_ms`, `stdout_bytes`, `stderr_bytes
|
|
239
|
+
* **Returns**: `processes` — a list of process summary objects, each containing: `process_id`, `pid`, `name`, `command`, `cwd`, `status`, `exit_code`, `signal`, `started_at`, `ended_at`, `duration_ms`, `stdout_bytes`, `stderr_bytes`, and `stdin_error` (`null` unless initial input delivery failed).
|
|
231
240
|
|
|
232
241
|
### Error Responses
|
|
233
242
|
|
|
234
243
|
All tools that accept a `process_id` parameter return a structured error dict when the ID is unknown, e.g. `{"process_id": "proc_abc123", "error": "Unknown process_id: proc_abc123"}`. Tools that accept `process_id` always include it in the response alongside the error.
|
|
235
244
|
|
|
236
245
|
### `cleanup`
|
|
237
|
-
Prune completed
|
|
246
|
+
Prune completed, stopped, timed-out, and failed process records to free memory.
|
|
238
247
|
|
|
239
248
|
* **Parameters**:
|
|
240
|
-
* `completed_older_than_ms` (integer, default: `3600000`): Prunes completed processes older than this threshold (1 hour default).
|
|
241
|
-
* `stopped_older_than_ms` (integer, default: `3600000`): Prunes stopped, timed-out, or failed processes older than this threshold (1 hour default).
|
|
242
|
-
* **Returns**: `removed` — the count of process records that were pruned.
|
|
249
|
+
* `completed_older_than_ms` (non-negative integer, default: `3600000`): Prunes completed processes older than this threshold (1 hour default).
|
|
250
|
+
* `stopped_older_than_ms` (non-negative integer, default: `3600000`): Prunes stopped, timed-out, or failed processes older than this threshold (1 hour default).
|
|
251
|
+
* **Returns**: `removed` — the count of process records that were pruned. Negative thresholds are rejected without removing records and include an `error` message.
|
|
243
252
|
|
|
244
253
|
---
|
|
245
254
|
|
|
246
|
-
##
|
|
255
|
+
## Byte Cursors & Incremental Reads
|
|
247
256
|
|
|
248
|
-
To avoid sending duplicate data over the MCP protocol (which can consume context window space), the server implements a
|
|
257
|
+
To avoid sending duplicate data over the MCP protocol (which can consume context window space), the server implements a byte-position polling protocol:
|
|
249
258
|
|
|
250
|
-
1. Every
|
|
251
|
-
2.
|
|
252
|
-
3. To retrieve
|
|
259
|
+
1. Every stdout/stderr byte receives a unique position in a cursor shared by both streams.
|
|
260
|
+
2. `read` returns `next_seq`, the position immediately after the selected-stream range covered by the response. A response cap can therefore end safely inside a drain chunk.
|
|
261
|
+
3. To retrieve the next output without gaps, call `read` with `since_seq` set to the previously returned `next_seq` and keep the same `streams` selection.
|
|
253
262
|
4. Omitting `since_seq` returns the entire contents currently stored in the buffer (clamped by `max_output_bytes`).
|
|
254
|
-
5. If output exceeds the ring buffer's capacity between reads, older data is evicted and `since_seq` may no longer be available. In that case, `truncated` is set to `true` and the read returns data from the earliest retained
|
|
263
|
+
5. If output exceeds the ring buffer's capacity between reads, older data is evicted and `since_seq` may no longer be available. In that case, `truncated` is set to `true` and the read returns data from the earliest retained position onward. Later reads report truncation only when their requested range overlaps evicted data.
|
|
264
|
+
|
|
265
|
+
Cursor boundaries preserve valid UTF-8 characters. If a single character is larger than a very small requested page, that page may exceed `max_output_bytes` by at most three bytes so the cursor can make progress without corrupting the character.
|
|
266
|
+
|
|
267
|
+
Incremental cursors are scoped to the selected `streams` value. Keep the same stream selection while advancing a cursor; switching from `stdout`-only or `stderr`-only polling to another selection can intentionally skip bytes from the previously unselected stream.
|
|
255
268
|
|
|
256
269
|
When a process exits normally, `exec`/`wait` responses include output drained through stdout/stderr EOF. If a descendant keeps inherited stdout/stderr open after the tracked process exits, the server stops waiting on those inherited pipes to avoid indefinite blocking; output written only by that descendant after the tracked process exits is not part of the managed process result.
|
|
257
270
|
|
|
@@ -265,14 +278,16 @@ Configure the server by setting these environment variables prior to launch:
|
|
|
265
278
|
|---|---|---|
|
|
266
279
|
| `YIELDSHELL_DEFAULT_CWD` | Current directory | The fallback working directory for commands. |
|
|
267
280
|
| `YIELDSHELL_ALLOWED_CWDS` | *(none)* | A list of allowed directory paths separated by `os.pathsep` (e.g., `:` on UNIX, `;` on Windows). If set, all command execution paths must resolve inside one of these roots. |
|
|
268
|
-
| `YIELDSHELL_MAX_OUTPUT_BYTES` | `20000` | The default and maximum capacity of the ring buffers for stdout/stderr. |
|
|
269
|
-
| `YIELDSHELL_MAX_PROCESSES` | `50` | Maximum concurrent
|
|
270
|
-
| `YIELDSHELL_DEFAULT_YIELD_MS` | `
|
|
271
|
-
| `YIELDSHELL_MAX_YIELD_MS` | `300000` |
|
|
272
|
-
| `YIELDSHELL_DEFAULT_TIMEOUT_MS` | `
|
|
273
|
-
| `
|
|
274
|
-
| `
|
|
275
|
-
| `
|
|
281
|
+
| `YIELDSHELL_MAX_OUTPUT_BYTES` | `20000` | The default and maximum capacity of the ring buffers for stdout/stderr. Nonpositive or nonnumeric values use the default. |
|
|
282
|
+
| `YIELDSHELL_MAX_PROCESSES` | `50` | Maximum concurrent live managed process groups, including descendants that outlive a completed shell. Spawning a new command when this limit is reached returns `failed_to_start`. Nonpositive or nonnumeric values use the default. |
|
|
283
|
+
| `YIELDSHELL_DEFAULT_YIELD_MS` | `30000` | Fallback delay before auto-yielding. Effective yields are also capped at 55,000ms. |
|
|
284
|
+
| `YIELDSHELL_MAX_YIELD_MS` | `300000` | Configured maximum for `yield_ms`; the effective maximum is the lesser of this value and 55,000ms. |
|
|
285
|
+
| `YIELDSHELL_DEFAULT_TIMEOUT_MS` | `3600000` | Default hard runtime limit (1 hour). An explicit tool argument of `0` means no limit. |
|
|
286
|
+
| `YIELDSHELL_PROCESS_RETENTION_MS` | `3600000` | Age after which terminal process records are reaped before a valid spawn. Zero requests immediate age-based reaping. Negative or nonnumeric values use the default. |
|
|
287
|
+
| `YIELDSHELL_MAX_RETAINED_PROCESSES` | `100` | Maximum retained terminal records after age reaping; oldest records are removed first. Zero retains no prior terminal records. Negative or nonnumeric values use the default. Running records are excluded. |
|
|
288
|
+
| `YIELDSHELL_DENY_COMMAND_REGEX` | *(none)* | A regular expression pattern. Commands matching this pattern are blocked before starting. Invalid patterns cause startup to fail with a configuration error naming this variable. |
|
|
289
|
+
| `YIELDSHELL_ALLOW_COMMAND_REGEX` | *(none)* | A regular expression pattern. If set, only commands matching this pattern are permitted. Invalid patterns cause startup to fail with a configuration error naming this variable. |
|
|
290
|
+
| `YIELDSHELL_REDACT_ENV_REGEX` | *(none)* | Optional regex identifying sensitive environment variable names. When configured, matching non-empty values of at least 8 characters are snapshotted at startup and redacted in stdout/stderr outputs. Invalid patterns cause startup to fail with a configuration error naming this variable. |
|
|
276
291
|
| `MCP_YIELDSHELL_BLOCKED_SIDE_EFFECTS` | `KILLS_AGENT_PROCESS,MODIFIES_OS_SETTINGS,MODIFIES_OS_USER_SETTINGS,MODIFIES_PROTECTED_FILES,RUNS_INLINE_CODE` | Comma-separated list of `side_effects` enum names the server should reject. Names are case-sensitive. Surrounding whitespace is trimmed and empty entries are ignored. Invalid names cause startup to fail. Set to `,` (or any value that resolves to no entries) to clear the default blocklist. |
|
|
277
292
|
|
|
278
293
|
---
|
|
@@ -284,19 +299,25 @@ Configure the server by setting these environment variables prior to launch:
|
|
|
284
299
|
* **Inline Code Execution**: The `RUNS_INLINE_CODE` default discourages agents from executing code supplied inline to an interpreter or shell (e.g. `python -c`, `node -e`, `ruby -e`, `perl -e`, shell heredocs piped into interpreters, or `curl ... | sh`). The safer pattern is to write the content to a reviewable workspace file and execute it in a small, inspectable step with explicit matching `side_effects`. Operators can override the default to permit the category.
|
|
285
300
|
* **OS User Settings Damage**: `MODIFIES_OS_USER_SETTINGS` covers commands that change user-level configuration such as shell rc files, XDG config directories, dotfiles, or per-user application preferences. This is distinct from `MODIFIES_OS_SETTINGS`, which covers broader OS-level configuration such as systemd units, kernel parameters, `/etc` files, and package manager system config. Blocked by default; operators can override.
|
|
286
301
|
* **Agent Process Termination**: `KILLS_AGENT_PROCESS` covers commands that may terminate the MCP client, agent, or related process running the agent workflow (e.g., `kill` commands targeting the agent PID, or commands that cause the agent to exit). This is distinct from `STOPS_OR_RESTARTS_SERVICES`, which covers OS-level services. Blocked by default; operators can override.
|
|
287
|
-
* **Path Validation**: CWD path verification
|
|
302
|
+
* **Path Validation**: CWD path verification resolves traversal and symlinks, then requires the target to be an allowed root itself or a true descendant. Lexically similar siblings and symlink escapes are rejected before spawn.
|
|
288
303
|
* **Additive Environments**: The `env` argument overlays existing env parameters. It merges with the parent process environment instead of completely replacing it, protecting critical OS vars.
|
|
289
|
-
* **Best-effort Redaction**:
|
|
304
|
+
* **Opt-in Best-effort Redaction**: Redaction is disabled unless `YIELDSHELL_REDACT_ENV_REGEX` is configured. When enabled, matching environment values are snapshotted at startup, ordered longest first, and values shorter than 8 characters are excluded to avoid corrupting ordinary output. Matching values supplied through an `env` overlay are added for that process. Redaction is applied while streams are drained, including across subprocess chunks and incremental read pages. Restart the server to refresh the parent-environment snapshot after changes. Redaction does not remove variables from subprocess environments, and secrets not selected by the regex or printed through transformed formats might not be caught.
|
|
305
|
+
|
|
306
|
+
## Lifecycle and Compatibility
|
|
307
|
+
|
|
308
|
+
YieldShell gives graceful termination 10 seconds before force-killing, waits up to 5 seconds for a POSIX process group to disappear, and allows up to 3 seconds for final stream draining. On stdio server shutdown, all live managed processes are terminated concurrently using the same bounded graceful/forced policy. Already-terminal records are not changed by shutdown.
|
|
309
|
+
|
|
310
|
+
Current lifecycle defaults are a 30-second auto-yield, one-hour runtime timeout, 55-second effective request-wait and stop-grace ceilings, 10-second default stop grace, automatic terminal-record retention, opt-in streaming output redaction, and shutdown containment. Standard input closes after `exec` by default; callers that need an interactive session must set `close_stdin=false`. Use explicit shorter timing values where lower latency is preferred, and pass `timeout_ms=0` to retain unlimited runtime behavior.
|
|
290
311
|
|
|
291
312
|
---
|
|
292
313
|
|
|
293
314
|
## Platform Support
|
|
294
315
|
|
|
295
|
-
* **POSIX (Linux & macOS)**: Fully supported. Spawns processes in distinct sessions (`start_new_session=True`), allowing signals (`SIGTERM`/`SIGKILL`) to target the entire process group. This
|
|
296
|
-
* **Windows**: Supported with best-effort process
|
|
316
|
+
* **POSIX (Linux & macOS)**: Fully supported. Spawns processes in distinct sessions (`start_new_session=True`), allowing `stop`, runtime timeout, and server shutdown signals (`SIGTERM`/`SIGKILL`) to target the entire process group. This cleans up child processes started by managed commands.
|
|
317
|
+
* **Windows**: Supported with best-effort process controls. Windows lacks native POSIX process-group signals, so `stop`, `timeout_ms`, and server shutdown act on the primary process; child subprocesses might persist if they do not exit cleanly.
|
|
297
318
|
|
|
298
319
|
---
|
|
299
320
|
|
|
300
321
|
## License
|
|
301
322
|
|
|
302
|
-
MIT
|
|
323
|
+
MIT
|
|
@@ -6,11 +6,11 @@ A drop-in shell MCP server that auto-yields long-running commands into managed b
|
|
|
6
6
|
|
|
7
7
|
Most shell tools present a frustrating choice: either block the LLM agent until the command finishes, or force the agent to decide upfront that a command should run in the background.
|
|
8
8
|
|
|
9
|
-
**YieldShell MCP** solves this by keeping normal foreground semantics for fast commands, then automatically promoting long-running commands into managed background processes after a
|
|
9
|
+
**YieldShell MCP** solves this by keeping normal foreground semantics for fast commands, then automatically promoting long-running commands into managed background processes after a delay (`yield_ms`, default: 30 seconds).
|
|
10
10
|
|
|
11
11
|
```mermaid
|
|
12
12
|
graph TD
|
|
13
|
-
A[exec_command] --> B["Wait for yield_ms (default:
|
|
13
|
+
A[exec_command] --> B["Wait for yield_ms (default: 30s)"]
|
|
14
14
|
B --> C{Is process still running?}
|
|
15
15
|
C -->|Yes| D["backgrounded<br>Returns process_id"]
|
|
16
16
|
C -->|No| E["completed<br>Returns full output"]
|
|
@@ -138,15 +138,16 @@ Execute a shell command. If the command runs longer than `yield_ms`, it yields a
|
|
|
138
138
|
* `command` (string, **required**): The command string to execute in the shell.
|
|
139
139
|
* `side_effects` (array of string, **required**): The side-effect categories this command may plausibly have. Must contain at least one entry drawn from the enum below. Use `["NONE"]` for commands with no meaningful side effects. `NONE` is exclusive and must not be combined with any other category. The server rejects the call with `failed_to_start` if any declared category is configured as blocked.
|
|
140
140
|
* Allowed values: `CHANGES_NETWORK_CONFIGURATION`, `CHANGES_PACKAGES_OR_DEPENDENCIES`, `CONSUMES_SIGNIFICANT_RESOURCES`, `DELETES_FILES`, `EXPOSES_SECRETS`, `KILLS_AGENT_PROCESS`, `MAKES_NETWORK_REQUESTS`, `MODIFIES_OS_SETTINGS`, `MODIFIES_OS_USER_SETTINGS`, `MODIFIES_OUTSIDE_WORKSPACE`, `MODIFIES_PRODUCTION_SERVICES`, `MODIFIES_PROTECTED_FILES`, `MODIFIES_SECURITY_CONTROLS`, `MODIFIES_WORKSPACE_FILES`, `NONE`, `OTHER`, `RUNS_INLINE_CODE`, `RUNS_PRIVILEGED_COMMANDS`, `STOPS_OR_RESTARTS_SERVICES`, `UNKNOWN`, `USES_DESTRUCTIVE_GIT_OPERATION`.
|
|
141
|
-
* `RUNS_INLINE_CODE` is in the default blocklist. It covers commands that execute code supplied inline to an interpreter or shell (e.g. `python -c`, `node -e`, `curl ... | sh`). It does not cover simply creating a script or executable file unless the same command also executes inline code. The safer next action is to write the content to a reviewable workspace file and execute it in a small, inspectable step. Operators can
|
|
141
|
+
* `RUNS_INLINE_CODE` is in the default blocklist. It covers commands that execute code supplied inline to an interpreter or shell (e.g. `python -c`, `node -e`, `curl ... | sh`). It does not cover simply creating a script or executable file unless the same command also executes inline code. The safer next action is to write the content to a reviewable workspace file and execute it in a small, inspectable step. Operators can clear the blocklist via `MCP_YIELDSHELL_BLOCKED_SIDE_EFFECTS=,`.
|
|
142
142
|
* `cwd` (string, optional): Working directory for the command. Must be under allowed roots if `YIELDSHELL_ALLOWED_CWDS` is set. Defaults to `YIELDSHELL_DEFAULT_CWD`.
|
|
143
143
|
* `env` (object of string to string, optional): Additive environment variable overlay. Merged into the parent environment.
|
|
144
|
-
* `shell` (string, optional):
|
|
144
|
+
* `shell` (string, optional): Shell executable used to run the command. Defaults to the platform shell. Explicit shells are checked by the same allow/deny command policy.
|
|
145
145
|
* `stdin` (string, optional): Initial text input written to standard input immediately after spawning.
|
|
146
|
+
* `close_stdin` (boolean, default: `true`): Close standard input after the initial input is written. Set to `false` when follow-up `write` calls are expected.
|
|
146
147
|
* `name` (string, optional): A human-readable label/name to identify this process.
|
|
147
|
-
* `yield_ms` (integer, optional): Milliseconds to wait before yielding execution to background.
|
|
148
|
-
* `timeout_ms` (integer, optional): Total execution runtime limit in milliseconds. Process is
|
|
149
|
-
* `max_output_bytes` (integer, optional): Maximum
|
|
148
|
+
* `yield_ms` (integer, optional): Milliseconds to wait before yielding execution to background. Both omitted and explicit values are clamped to the lesser of `YIELDSHELL_MAX_YIELD_MS` and the transport-safe 55,000ms ceiling. Defaults to `YIELDSHELL_DEFAULT_YIELD_MS` (30,000ms).
|
|
149
|
+
* `timeout_ms` (integer, optional): Total execution runtime limit in milliseconds. Process is terminated if it runs longer than this. Defaults to `YIELDSHELL_DEFAULT_TIMEOUT_MS` (3,600,000ms). Pass `0` explicitly for unlimited execution.
|
|
150
|
+
* `max_output_bytes` (integer, optional): Maximum bytes retained per stdout/stderr ring buffer and returned in total across both response streams. Subject to the `YIELDSHELL_MAX_OUTPUT_BYTES` cap.
|
|
150
151
|
|
|
151
152
|
* **Side-Effects Guide**:
|
|
152
153
|
* `side_effects` is required and must be a non-empty list. Declare every plausible side-effect category before running the command.
|
|
@@ -163,27 +164,30 @@ Execute a shell command. If the command runs longer than `yield_ms`, it yields a
|
|
|
163
164
|
* Destructive file operations: `side_effects=["DELETES_FILES"]`
|
|
164
165
|
* Privileged command: `side_effects=["RUNS_PRIVILEGED_COMMANDS"]`
|
|
165
166
|
* Protected-file changes: `side_effects=["MODIFIES_PROTECTED_FILES"]`
|
|
166
|
-
* Inline code execution: prefer writing the content to a reviewable workspace file (for example `scripts/migrate.sql` or `tools/build.sh`) and run it in a small, inspectable step. Declaring `side_effects=["RUNS_INLINE_CODE"]` is rejected under the default policy; operators can clear that default with `MCP_YIELDSHELL_BLOCKED_SIDE_EFFECTS
|
|
167
|
+
* Inline code execution: prefer writing the content to a reviewable workspace file (for example `scripts/migrate.sql` or `tools/build.sh`) and run it in a small, inspectable step. Declaring `side_effects=["RUNS_INLINE_CODE"]` is rejected under the default policy; operators can clear that default with `MCP_YIELDSHELL_BLOCKED_SIDE_EFFECTS=,`.
|
|
167
168
|
|
|
168
169
|
* **Output Statuses**:
|
|
169
|
-
* `completed`: Process finished within `yield_ms`. Returns exit code, stdout, and stderr.
|
|
170
|
+
* `completed`: Process finished within `yield_ms`. Returns exit code, stdout, and stderr. If the response is truncated and terminal-process retention is enabled, it also returns `process_id` so retained output can be inspected with `read`.
|
|
170
171
|
* `backgrounded`: Process auto-yielded. Returns `process_id`, `pid`, a snapshot of initial stdout/stderr, `duration_ms`, `truncated`, and a `message` string describing that the process is running in the background.
|
|
171
172
|
* `timed_out`: Process exceeded `timeout_ms` and was terminated.
|
|
172
173
|
* `stopped`: Process was explicitly terminated.
|
|
173
174
|
* `failed_to_start`: Command could not be spawned (e.g., bad directory or policy violation).
|
|
174
175
|
* `failed`: An internal execution error occurred.
|
|
176
|
+
* If initial `stdin` delivery fails, responses for the retained process include
|
|
177
|
+
`stdin_error` with the transport error. Initial delivery runs as tracked
|
|
178
|
+
background work so pipe backpressure does not delay auto-yielding.
|
|
175
179
|
|
|
176
180
|
### `read`
|
|
177
181
|
Read stdout and/or stderr output from a running or completed background process.
|
|
178
182
|
|
|
179
183
|
* **Parameters**:
|
|
180
184
|
* `process_id` (string, **required**): Unique identifier of the process.
|
|
181
|
-
* `since_seq` (integer, optional):
|
|
182
|
-
* `max_output_bytes` (integer, optional): Clamps the
|
|
185
|
+
* `since_seq` (integer, optional): Byte-position cursor returned as `next_seq` by the previous read. Enables lossless incremental log polling.
|
|
186
|
+
* `max_output_bytes` (integer, optional): Clamps total output returned across the selected streams. Defaults to the server cap.
|
|
183
187
|
* `streams` (string, default: `"both"`): The streams to read. Options: `"both"`, `"stdout"`, or `"stderr"`.
|
|
184
188
|
|
|
185
189
|
* **Returns**:
|
|
186
|
-
* `process_id`, `status`, `exit_code`, `signal`, `next_seq` (
|
|
190
|
+
* `process_id`, `status`, `exit_code`, `signal`, `next_seq` (byte-position cursor to use in subsequent `since_seq` reads), `truncated` flag, and `stdin_error` when initial input delivery failed. `stdout` and `stderr` text are included based on the `streams` filter — `"both"` includes both, `"stdout"` includes only `stdout`, and `"stderr"` includes only `stderr`.
|
|
187
191
|
|
|
188
192
|
### `write`
|
|
189
193
|
Write text input to the standard input (`stdin`) of a running process.
|
|
@@ -192,58 +196,67 @@ Write text input to the standard input (`stdin`) of a running process.
|
|
|
192
196
|
* `process_id` (string, **required**): Unique identifier of the process.
|
|
193
197
|
* `input` (string, **required**): Text input to write.
|
|
194
198
|
* `newline` (boolean, default: `false`): If `true`, appends `\n` to the input.
|
|
199
|
+
* `close_stdin` (boolean, default: `false`): Close standard input after this write, delivering EOF to the process.
|
|
200
|
+
* Writes that remain backpressured are capped at the transport-safe 55-second
|
|
201
|
+
request ceiling and return `ok: false` with an error.
|
|
195
202
|
|
|
196
203
|
### `wait`
|
|
197
204
|
Block execution until the process exits or the wait timeout expires. This allows the LLM to pause and await completion without spawning a new execution loop.
|
|
198
205
|
|
|
199
206
|
* **Parameters**:
|
|
200
207
|
* `process_id` (string, **required**): Unique identifier of the process.
|
|
201
|
-
* `timeout_ms` (integer, default: `
|
|
202
|
-
* `max_output_bytes` (integer, optional): Maximum output bytes to return
|
|
208
|
+
* `timeout_ms` (integer, default: `55000`): Maximum time to wait.
|
|
209
|
+
* `max_output_bytes` (integer, optional): Maximum total output bytes to return across stdout and stderr.
|
|
203
210
|
|
|
204
211
|
* **Important**: If the wait timeout expires, `wait` returns the current status but **does not kill** the process. It continues running in the background.
|
|
205
212
|
* The effective wait duration is capped at 55 seconds to stay well under typical MCP request timeouts, even if a larger `timeout_ms` is requested.
|
|
206
|
-
* `wait` treats the
|
|
213
|
+
* `wait` treats the managed process group disappearing as completion. If the tracked shell exits while descendants in its process group remain alive, the record continues to report `running`. For normal process-group completion, stdout/stderr are drained before the response is returned. Final drain waits are bounded so inherited pipes cannot block a request indefinitely.
|
|
207
214
|
|
|
208
215
|
### `stop`
|
|
209
216
|
Gracefully terminate or force kill a running process.
|
|
210
217
|
|
|
211
218
|
* **Parameters**:
|
|
212
219
|
* `process_id` (string, **required**): Unique identifier of the process.
|
|
213
|
-
* `signal` (string, default: `"SIGTERM"`): OS signal to send (e.g. `SIGTERM`, `SIGKILL`, `SIGINT`).
|
|
214
|
-
* `force_after_ms` (integer, default: `
|
|
220
|
+
* `signal` (string, default: `"SIGTERM"`): OS signal to send (e.g. `SIGTERM`, `SIGKILL`, `SIGINT`). Invalid names are rejected without stopping the process. Valid names are ignored on Windows.
|
|
221
|
+
* `force_after_ms` (integer, default: `10000`): Grace period before escalating to force kill (`SIGKILL`). It is clamped below the transport-safe 55-second request ceiling so force-kill observation, final output drain, and subprocess reaping still fit within the request.
|
|
215
222
|
|
|
216
223
|
### `ps`
|
|
217
224
|
List all managed processes.
|
|
218
225
|
|
|
226
|
+
Terminal records are retained temporarily for inspection. Before each otherwise valid `exec` spawn, records older than `YIELDSHELL_PROCESS_RETENTION_MS` are removed and the remaining terminal set is reduced to `YIELDSHELL_MAX_RETAINED_PROCESSES`, oldest first. Running records are never automatically removed. If the shell exits but its process group still has live descendants, status remains `running` until descendants stop. Reaped IDs disappear from `ps` and become unknown to `read`, `wait`, `write`, and `stop`.
|
|
227
|
+
|
|
219
228
|
* **Parameters**:
|
|
220
229
|
* `include_completed` (boolean, default: `true`): If `false`, finished/stopped processes are excluded from the output.
|
|
221
230
|
* `limit` (integer, default: `50`): Maximum number of entries.
|
|
222
|
-
* **Returns**: `processes` — a list of process summary objects, each containing: `process_id`, `pid`, `name`, `command`, `cwd`, `status`, `exit_code`, `signal`, `started_at`, `ended_at`, `duration_ms`, `stdout_bytes`, `stderr_bytes
|
|
231
|
+
* **Returns**: `processes` — a list of process summary objects, each containing: `process_id`, `pid`, `name`, `command`, `cwd`, `status`, `exit_code`, `signal`, `started_at`, `ended_at`, `duration_ms`, `stdout_bytes`, `stderr_bytes`, and `stdin_error` (`null` unless initial input delivery failed).
|
|
223
232
|
|
|
224
233
|
### Error Responses
|
|
225
234
|
|
|
226
235
|
All tools that accept a `process_id` parameter return a structured error dict when the ID is unknown, e.g. `{"process_id": "proc_abc123", "error": "Unknown process_id: proc_abc123"}`. Tools that accept `process_id` always include it in the response alongside the error.
|
|
227
236
|
|
|
228
237
|
### `cleanup`
|
|
229
|
-
Prune completed
|
|
238
|
+
Prune completed, stopped, timed-out, and failed process records to free memory.
|
|
230
239
|
|
|
231
240
|
* **Parameters**:
|
|
232
|
-
* `completed_older_than_ms` (integer, default: `3600000`): Prunes completed processes older than this threshold (1 hour default).
|
|
233
|
-
* `stopped_older_than_ms` (integer, default: `3600000`): Prunes stopped, timed-out, or failed processes older than this threshold (1 hour default).
|
|
234
|
-
* **Returns**: `removed` — the count of process records that were pruned.
|
|
241
|
+
* `completed_older_than_ms` (non-negative integer, default: `3600000`): Prunes completed processes older than this threshold (1 hour default).
|
|
242
|
+
* `stopped_older_than_ms` (non-negative integer, default: `3600000`): Prunes stopped, timed-out, or failed processes older than this threshold (1 hour default).
|
|
243
|
+
* **Returns**: `removed` — the count of process records that were pruned. Negative thresholds are rejected without removing records and include an `error` message.
|
|
235
244
|
|
|
236
245
|
---
|
|
237
246
|
|
|
238
|
-
##
|
|
247
|
+
## Byte Cursors & Incremental Reads
|
|
239
248
|
|
|
240
|
-
To avoid sending duplicate data over the MCP protocol (which can consume context window space), the server implements a
|
|
249
|
+
To avoid sending duplicate data over the MCP protocol (which can consume context window space), the server implements a byte-position polling protocol:
|
|
241
250
|
|
|
242
|
-
1. Every
|
|
243
|
-
2.
|
|
244
|
-
3. To retrieve
|
|
251
|
+
1. Every stdout/stderr byte receives a unique position in a cursor shared by both streams.
|
|
252
|
+
2. `read` returns `next_seq`, the position immediately after the selected-stream range covered by the response. A response cap can therefore end safely inside a drain chunk.
|
|
253
|
+
3. To retrieve the next output without gaps, call `read` with `since_seq` set to the previously returned `next_seq` and keep the same `streams` selection.
|
|
245
254
|
4. Omitting `since_seq` returns the entire contents currently stored in the buffer (clamped by `max_output_bytes`).
|
|
246
|
-
5. If output exceeds the ring buffer's capacity between reads, older data is evicted and `since_seq` may no longer be available. In that case, `truncated` is set to `true` and the read returns data from the earliest retained
|
|
255
|
+
5. If output exceeds the ring buffer's capacity between reads, older data is evicted and `since_seq` may no longer be available. In that case, `truncated` is set to `true` and the read returns data from the earliest retained position onward. Later reads report truncation only when their requested range overlaps evicted data.
|
|
256
|
+
|
|
257
|
+
Cursor boundaries preserve valid UTF-8 characters. If a single character is larger than a very small requested page, that page may exceed `max_output_bytes` by at most three bytes so the cursor can make progress without corrupting the character.
|
|
258
|
+
|
|
259
|
+
Incremental cursors are scoped to the selected `streams` value. Keep the same stream selection while advancing a cursor; switching from `stdout`-only or `stderr`-only polling to another selection can intentionally skip bytes from the previously unselected stream.
|
|
247
260
|
|
|
248
261
|
When a process exits normally, `exec`/`wait` responses include output drained through stdout/stderr EOF. If a descendant keeps inherited stdout/stderr open after the tracked process exits, the server stops waiting on those inherited pipes to avoid indefinite blocking; output written only by that descendant after the tracked process exits is not part of the managed process result.
|
|
249
262
|
|
|
@@ -257,14 +270,16 @@ Configure the server by setting these environment variables prior to launch:
|
|
|
257
270
|
|---|---|---|
|
|
258
271
|
| `YIELDSHELL_DEFAULT_CWD` | Current directory | The fallback working directory for commands. |
|
|
259
272
|
| `YIELDSHELL_ALLOWED_CWDS` | *(none)* | A list of allowed directory paths separated by `os.pathsep` (e.g., `:` on UNIX, `;` on Windows). If set, all command execution paths must resolve inside one of these roots. |
|
|
260
|
-
| `YIELDSHELL_MAX_OUTPUT_BYTES` | `20000` | The default and maximum capacity of the ring buffers for stdout/stderr. |
|
|
261
|
-
| `YIELDSHELL_MAX_PROCESSES` | `50` | Maximum concurrent
|
|
262
|
-
| `YIELDSHELL_DEFAULT_YIELD_MS` | `
|
|
263
|
-
| `YIELDSHELL_MAX_YIELD_MS` | `300000` |
|
|
264
|
-
| `YIELDSHELL_DEFAULT_TIMEOUT_MS` | `
|
|
265
|
-
| `
|
|
266
|
-
| `
|
|
267
|
-
| `
|
|
273
|
+
| `YIELDSHELL_MAX_OUTPUT_BYTES` | `20000` | The default and maximum capacity of the ring buffers for stdout/stderr. Nonpositive or nonnumeric values use the default. |
|
|
274
|
+
| `YIELDSHELL_MAX_PROCESSES` | `50` | Maximum concurrent live managed process groups, including descendants that outlive a completed shell. Spawning a new command when this limit is reached returns `failed_to_start`. Nonpositive or nonnumeric values use the default. |
|
|
275
|
+
| `YIELDSHELL_DEFAULT_YIELD_MS` | `30000` | Fallback delay before auto-yielding. Effective yields are also capped at 55,000ms. |
|
|
276
|
+
| `YIELDSHELL_MAX_YIELD_MS` | `300000` | Configured maximum for `yield_ms`; the effective maximum is the lesser of this value and 55,000ms. |
|
|
277
|
+
| `YIELDSHELL_DEFAULT_TIMEOUT_MS` | `3600000` | Default hard runtime limit (1 hour). An explicit tool argument of `0` means no limit. |
|
|
278
|
+
| `YIELDSHELL_PROCESS_RETENTION_MS` | `3600000` | Age after which terminal process records are reaped before a valid spawn. Zero requests immediate age-based reaping. Negative or nonnumeric values use the default. |
|
|
279
|
+
| `YIELDSHELL_MAX_RETAINED_PROCESSES` | `100` | Maximum retained terminal records after age reaping; oldest records are removed first. Zero retains no prior terminal records. Negative or nonnumeric values use the default. Running records are excluded. |
|
|
280
|
+
| `YIELDSHELL_DENY_COMMAND_REGEX` | *(none)* | A regular expression pattern. Commands matching this pattern are blocked before starting. Invalid patterns cause startup to fail with a configuration error naming this variable. |
|
|
281
|
+
| `YIELDSHELL_ALLOW_COMMAND_REGEX` | *(none)* | A regular expression pattern. If set, only commands matching this pattern are permitted. Invalid patterns cause startup to fail with a configuration error naming this variable. |
|
|
282
|
+
| `YIELDSHELL_REDACT_ENV_REGEX` | *(none)* | Optional regex identifying sensitive environment variable names. When configured, matching non-empty values of at least 8 characters are snapshotted at startup and redacted in stdout/stderr outputs. Invalid patterns cause startup to fail with a configuration error naming this variable. |
|
|
268
283
|
| `MCP_YIELDSHELL_BLOCKED_SIDE_EFFECTS` | `KILLS_AGENT_PROCESS,MODIFIES_OS_SETTINGS,MODIFIES_OS_USER_SETTINGS,MODIFIES_PROTECTED_FILES,RUNS_INLINE_CODE` | Comma-separated list of `side_effects` enum names the server should reject. Names are case-sensitive. Surrounding whitespace is trimmed and empty entries are ignored. Invalid names cause startup to fail. Set to `,` (or any value that resolves to no entries) to clear the default blocklist. |
|
|
269
284
|
|
|
270
285
|
---
|
|
@@ -276,19 +291,25 @@ Configure the server by setting these environment variables prior to launch:
|
|
|
276
291
|
* **Inline Code Execution**: The `RUNS_INLINE_CODE` default discourages agents from executing code supplied inline to an interpreter or shell (e.g. `python -c`, `node -e`, `ruby -e`, `perl -e`, shell heredocs piped into interpreters, or `curl ... | sh`). The safer pattern is to write the content to a reviewable workspace file and execute it in a small, inspectable step with explicit matching `side_effects`. Operators can override the default to permit the category.
|
|
277
292
|
* **OS User Settings Damage**: `MODIFIES_OS_USER_SETTINGS` covers commands that change user-level configuration such as shell rc files, XDG config directories, dotfiles, or per-user application preferences. This is distinct from `MODIFIES_OS_SETTINGS`, which covers broader OS-level configuration such as systemd units, kernel parameters, `/etc` files, and package manager system config. Blocked by default; operators can override.
|
|
278
293
|
* **Agent Process Termination**: `KILLS_AGENT_PROCESS` covers commands that may terminate the MCP client, agent, or related process running the agent workflow (e.g., `kill` commands targeting the agent PID, or commands that cause the agent to exit). This is distinct from `STOPS_OR_RESTARTS_SERVICES`, which covers OS-level services. Blocked by default; operators can override.
|
|
279
|
-
* **Path Validation**: CWD path verification
|
|
294
|
+
* **Path Validation**: CWD path verification resolves traversal and symlinks, then requires the target to be an allowed root itself or a true descendant. Lexically similar siblings and symlink escapes are rejected before spawn.
|
|
280
295
|
* **Additive Environments**: The `env` argument overlays existing env parameters. It merges with the parent process environment instead of completely replacing it, protecting critical OS vars.
|
|
281
|
-
* **Best-effort Redaction**:
|
|
296
|
+
* **Opt-in Best-effort Redaction**: Redaction is disabled unless `YIELDSHELL_REDACT_ENV_REGEX` is configured. When enabled, matching environment values are snapshotted at startup, ordered longest first, and values shorter than 8 characters are excluded to avoid corrupting ordinary output. Matching values supplied through an `env` overlay are added for that process. Redaction is applied while streams are drained, including across subprocess chunks and incremental read pages. Restart the server to refresh the parent-environment snapshot after changes. Redaction does not remove variables from subprocess environments, and secrets not selected by the regex or printed through transformed formats might not be caught.
|
|
297
|
+
|
|
298
|
+
## Lifecycle and Compatibility
|
|
299
|
+
|
|
300
|
+
YieldShell gives graceful termination 10 seconds before force-killing, waits up to 5 seconds for a POSIX process group to disappear, and allows up to 3 seconds for final stream draining. On stdio server shutdown, all live managed processes are terminated concurrently using the same bounded graceful/forced policy. Already-terminal records are not changed by shutdown.
|
|
301
|
+
|
|
302
|
+
Current lifecycle defaults are a 30-second auto-yield, one-hour runtime timeout, 55-second effective request-wait and stop-grace ceilings, 10-second default stop grace, automatic terminal-record retention, opt-in streaming output redaction, and shutdown containment. Standard input closes after `exec` by default; callers that need an interactive session must set `close_stdin=false`. Use explicit shorter timing values where lower latency is preferred, and pass `timeout_ms=0` to retain unlimited runtime behavior.
|
|
282
303
|
|
|
283
304
|
---
|
|
284
305
|
|
|
285
306
|
## Platform Support
|
|
286
307
|
|
|
287
|
-
* **POSIX (Linux & macOS)**: Fully supported. Spawns processes in distinct sessions (`start_new_session=True`), allowing signals (`SIGTERM`/`SIGKILL`) to target the entire process group. This
|
|
288
|
-
* **Windows**: Supported with best-effort process
|
|
308
|
+
* **POSIX (Linux & macOS)**: Fully supported. Spawns processes in distinct sessions (`start_new_session=True`), allowing `stop`, runtime timeout, and server shutdown signals (`SIGTERM`/`SIGKILL`) to target the entire process group. This cleans up child processes started by managed commands.
|
|
309
|
+
* **Windows**: Supported with best-effort process controls. Windows lacks native POSIX process-group signals, so `stop`, `timeout_ms`, and server shutdown act on the primary process; child subprocesses might persist if they do not exit cleanly.
|
|
289
310
|
|
|
290
311
|
---
|
|
291
312
|
|
|
292
313
|
## License
|
|
293
314
|
|
|
294
|
-
MIT
|
|
315
|
+
MIT
|
|
@@ -5,6 +5,12 @@ from __future__ import annotations
|
|
|
5
5
|
import os
|
|
6
6
|
import re
|
|
7
7
|
|
|
8
|
+
from .policy import (
|
|
9
|
+
DEFAULT_MAX_RETAINED_PROCESSES,
|
|
10
|
+
DEFAULT_PROCESS_RETENTION_MS,
|
|
11
|
+
DEFAULT_TIMEOUT_MS,
|
|
12
|
+
DEFAULT_YIELD_MS,
|
|
13
|
+
)
|
|
8
14
|
from .types import DEFAULT_BLOCKED_SIDE_EFFECTS, SideEffect
|
|
9
15
|
|
|
10
16
|
|
|
@@ -14,30 +20,52 @@ class Config:
|
|
|
14
20
|
self.allowed_cwd_roots: list[str] = _parse_pathsep(
|
|
15
21
|
os.environ.get("YIELDSHELL_ALLOWED_CWDS", "")
|
|
16
22
|
)
|
|
17
|
-
self.max_output_bytes: int =
|
|
23
|
+
self.max_output_bytes: int = _parse_positive_int(
|
|
18
24
|
os.environ.get("YIELDSHELL_MAX_OUTPUT_BYTES", ""), 20000
|
|
19
25
|
)
|
|
20
|
-
self.max_processes: int =
|
|
26
|
+
self.max_processes: int = _parse_positive_int(
|
|
21
27
|
os.environ.get("YIELDSHELL_MAX_PROCESSES", ""), 50
|
|
22
28
|
)
|
|
23
|
-
self.default_yield_ms: int =
|
|
24
|
-
os.environ.get("YIELDSHELL_DEFAULT_YIELD_MS", ""),
|
|
29
|
+
self.default_yield_ms: int = _parse_nonnegative_int(
|
|
30
|
+
os.environ.get("YIELDSHELL_DEFAULT_YIELD_MS", ""), DEFAULT_YIELD_MS
|
|
25
31
|
)
|
|
26
|
-
self.max_yield_ms: int =
|
|
32
|
+
self.max_yield_ms: int = _parse_nonnegative_int(
|
|
27
33
|
os.environ.get("YIELDSHELL_MAX_YIELD_MS", ""), 300000
|
|
28
34
|
)
|
|
29
|
-
self.default_timeout_ms: int =
|
|
30
|
-
os.environ.get("YIELDSHELL_DEFAULT_TIMEOUT_MS", ""),
|
|
35
|
+
self.default_timeout_ms: int = _parse_nonnegative_int(
|
|
36
|
+
os.environ.get("YIELDSHELL_DEFAULT_TIMEOUT_MS", ""), DEFAULT_TIMEOUT_MS
|
|
37
|
+
)
|
|
38
|
+
self.process_retention_ms: int = _parse_nonnegative_int(
|
|
39
|
+
os.environ.get("YIELDSHELL_PROCESS_RETENTION_MS", ""),
|
|
40
|
+
DEFAULT_PROCESS_RETENTION_MS,
|
|
41
|
+
)
|
|
42
|
+
self.max_retained_processes: int = _parse_nonnegative_int(
|
|
43
|
+
os.environ.get("YIELDSHELL_MAX_RETAINED_PROCESSES", ""),
|
|
44
|
+
DEFAULT_MAX_RETAINED_PROCESSES,
|
|
31
45
|
)
|
|
32
46
|
self.deny_command_regex: re.Pattern[str] | None = _parse_regex(
|
|
33
|
-
|
|
47
|
+
"YIELDSHELL_DENY_COMMAND_REGEX",
|
|
48
|
+
os.environ.get("YIELDSHELL_DENY_COMMAND_REGEX", ""),
|
|
34
49
|
)
|
|
35
50
|
self.allow_command_regex: re.Pattern[str] | None = _parse_regex(
|
|
36
|
-
|
|
51
|
+
"YIELDSHELL_ALLOW_COMMAND_REGEX",
|
|
52
|
+
os.environ.get("YIELDSHELL_ALLOW_COMMAND_REGEX", ""),
|
|
37
53
|
)
|
|
38
|
-
self.redact_env_regex: re.Pattern[str] =
|
|
54
|
+
self.redact_env_regex: re.Pattern[str] | None = _parse_regex(
|
|
55
|
+
"YIELDSHELL_REDACT_ENV_REGEX",
|
|
39
56
|
os.environ.get("YIELDSHELL_REDACT_ENV_REGEX", ""),
|
|
40
|
-
|
|
57
|
+
)
|
|
58
|
+
self.sensitive_env: tuple[tuple[str, str], ...] = tuple(
|
|
59
|
+
sorted(
|
|
60
|
+
(
|
|
61
|
+
(name, value)
|
|
62
|
+
for name, value in os.environ.items()
|
|
63
|
+
if self.redact_env_regex is not None
|
|
64
|
+
and self.redact_env_regex.search(name)
|
|
65
|
+
and len(value) >= 8
|
|
66
|
+
),
|
|
67
|
+
key=lambda item: (-len(item[1]), item[0], item[1]),
|
|
68
|
+
)
|
|
41
69
|
)
|
|
42
70
|
self.blocked_side_effects: frozenset[SideEffect] = _parse_blocked_side_effects(
|
|
43
71
|
os.environ.get("MCP_YIELDSHELL_BLOCKED_SIDE_EFFECTS", "")
|
|
@@ -59,16 +87,25 @@ def _parse_int(value: str, default: int) -> int:
|
|
|
59
87
|
return default
|
|
60
88
|
|
|
61
89
|
|
|
62
|
-
def
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
return re.compile(value)
|
|
90
|
+
def _parse_nonnegative_int(value: str, default: int) -> int:
|
|
91
|
+
parsed = _parse_int(value, default)
|
|
92
|
+
return parsed if parsed >= 0 else default
|
|
66
93
|
|
|
67
94
|
|
|
68
|
-
def
|
|
95
|
+
def _parse_positive_int(value: str, default: int) -> int:
|
|
96
|
+
parsed = _parse_int(value, default)
|
|
97
|
+
return parsed if parsed > 0 else default
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _parse_regex(variable: str, value: str) -> re.Pattern[str] | None:
|
|
69
101
|
if not value:
|
|
70
|
-
return
|
|
71
|
-
|
|
102
|
+
return None
|
|
103
|
+
try:
|
|
104
|
+
return re.compile(value)
|
|
105
|
+
except re.error as exc:
|
|
106
|
+
raise ValueError(
|
|
107
|
+
f"{variable} contains invalid regular expression {value!r}: {exc}"
|
|
108
|
+
) from exc
|
|
72
109
|
|
|
73
110
|
|
|
74
111
|
def _parse_blocked_side_effects(value: str) -> frozenset[SideEffect]:
|