mcp-yieldshell 0.6.0__tar.gz → 1.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {mcp_yieldshell-0.6.0 → mcp_yieldshell-1.1.0}/PKG-INFO +122 -50
- {mcp_yieldshell-0.6.0 → mcp_yieldshell-1.1.0}/README.md +120 -48
- {mcp_yieldshell-0.6.0 → mcp_yieldshell-1.1.0}/pyproject.toml +3 -3
- mcp_yieldshell-1.1.0/src/mcp_yieldshell/execution/__init__.py +1 -0
- {mcp_yieldshell-0.6.0/src/mcp_yieldshell/process → mcp_yieldshell-1.1.0/src/mcp_yieldshell/execution}/managed.py +4 -4
- {mcp_yieldshell-0.6.0/src/mcp_yieldshell/process → mcp_yieldshell-1.1.0/src/mcp_yieldshell/execution}/manager.py +174 -161
- {mcp_yieldshell-0.6.0/src/mcp_yieldshell/process → mcp_yieldshell-1.1.0/src/mcp_yieldshell/execution}/ring_buffer.py +1 -1
- {mcp_yieldshell-0.6.0/src/mcp_yieldshell/process → mcp_yieldshell-1.1.0/src/mcp_yieldshell/execution}/spawn.py +1 -1
- {mcp_yieldshell-0.6.0 → mcp_yieldshell-1.1.0}/src/mcp_yieldshell/server.py +50 -32
- {mcp_yieldshell-0.6.0 → mcp_yieldshell-1.1.0}/src/mcp_yieldshell/types.py +7 -7
- {mcp_yieldshell-0.6.0 → mcp_yieldshell-1.1.0}/tests/test_integration.py +440 -304
- {mcp_yieldshell-0.6.0 → mcp_yieldshell-1.1.0}/tests/test_lifecycle_hardening.py +216 -214
- {mcp_yieldshell-0.6.0 → mcp_yieldshell-1.1.0}/tests/test_protocol.py +54 -13
- {mcp_yieldshell-0.6.0 → mcp_yieldshell-1.1.0}/tests/test_ring_buffer.py +1 -1
- {mcp_yieldshell-0.6.0 → mcp_yieldshell-1.1.0}/tests/test_side_effects.py +102 -100
- mcp_yieldshell-0.6.0/src/mcp_yieldshell/process/__init__.py +0 -1
- {mcp_yieldshell-0.6.0 → mcp_yieldshell-1.1.0}/.gitignore +0 -0
- {mcp_yieldshell-0.6.0 → mcp_yieldshell-1.1.0}/src/mcp_yieldshell/__init__.py +0 -0
- {mcp_yieldshell-0.6.0 → mcp_yieldshell-1.1.0}/src/mcp_yieldshell/__main__.py +0 -0
- {mcp_yieldshell-0.6.0 → mcp_yieldshell-1.1.0}/src/mcp_yieldshell/config.py +0 -0
- {mcp_yieldshell-0.6.0 → mcp_yieldshell-1.1.0}/src/mcp_yieldshell/policy.py +0 -0
- {mcp_yieldshell-0.6.0 → mcp_yieldshell-1.1.0}/src/mcp_yieldshell/security.py +0 -0
- {mcp_yieldshell-0.6.0 → mcp_yieldshell-1.1.0}/src/mcp_yieldshell/side_effects.py +0 -0
- {mcp_yieldshell-0.6.0 → mcp_yieldshell-1.1.0}/tests/__init__.py +0 -0
- {mcp_yieldshell-0.6.0 → mcp_yieldshell-1.1.0}/tests/test_config.py +0 -0
- {mcp_yieldshell-0.6.0 → mcp_yieldshell-1.1.0}/tests/test_release.py +0 -0
- {mcp_yieldshell-0.6.0 → mcp_yieldshell-1.1.0}/tests/test_security.py +0 -0
|
@@ -1,31 +1,37 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mcp-yieldshell
|
|
3
|
-
Version:
|
|
4
|
-
Summary: A drop-in shell MCP that auto-yields long-running commands into managed background
|
|
3
|
+
Version: 1.1.0
|
|
4
|
+
Summary: A drop-in shell MCP that auto-yields long-running commands into managed background executions.
|
|
5
5
|
Requires-Python: >=3.11
|
|
6
6
|
Requires-Dist: mcp<2,>=1.9.0
|
|
7
7
|
Description-Content-Type: text/markdown
|
|
8
8
|
|
|
9
9
|
# YieldShell MCP
|
|
10
10
|
|
|
11
|
-
A drop-in shell MCP server that auto-yields long-running commands into managed
|
|
11
|
+
A drop-in shell MCP server that auto-yields long-running commands into managed
|
|
12
|
+
background executions.
|
|
12
13
|
|
|
13
14
|
## Why Auto-Yielding?
|
|
14
15
|
|
|
15
16
|
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
17
|
|
|
17
|
-
**YieldShell MCP** solves this by keeping normal foreground semantics for fast
|
|
18
|
+
**YieldShell MCP** solves this by keeping normal foreground semantics for fast
|
|
19
|
+
commands, then automatically promoting long-running commands into managed
|
|
20
|
+
background executions after a delay (`yield_ms`, default: 30 seconds).
|
|
18
21
|
|
|
19
22
|
```mermaid
|
|
20
23
|
graph TD
|
|
21
|
-
A[
|
|
22
|
-
B --> C{Is
|
|
23
|
-
C -->|Yes| D["backgrounded<br>Returns
|
|
24
|
+
A[execute] --> B["Wait for yield_ms (default: 30s)"]
|
|
25
|
+
B --> C{Is execution still running?}
|
|
26
|
+
C -->|Yes| D["backgrounded<br>Returns execution_id"]
|
|
24
27
|
C -->|No| E["completed<br>Returns full output"]
|
|
25
28
|
```
|
|
26
29
|
|
|
27
30
|
- **Fast Commands** (e.g., `echo hello`, `ls`): Complete instantly, returning the output immediately.
|
|
28
|
-
- **Long-Running Commands** (e.g., `npm run dev`, `docker build`, `sleep 60`):
|
|
31
|
+
- **Long-Running Commands** (e.g., `npm run dev`, `docker build`, `sleep 60`):
|
|
32
|
+
Automatically yield control back to the agent with an `execution_id` and a
|
|
33
|
+
snapshot of initial output, letting the agent decide when to `read`, `wait`,
|
|
34
|
+
or `stop` the execution.
|
|
29
35
|
|
|
30
36
|
---
|
|
31
37
|
|
|
@@ -135,12 +141,19 @@ Add to your OpenCode MCP settings:
|
|
|
135
141
|
}
|
|
136
142
|
```
|
|
137
143
|
|
|
144
|
+
These client command and environment snippets are unchanged in 1.0; the
|
|
145
|
+
breaking changes affect tool calls and response fields, not server launch
|
|
146
|
+
configuration.
|
|
147
|
+
|
|
138
148
|
---
|
|
139
149
|
|
|
140
150
|
## Tool Reference
|
|
141
151
|
|
|
142
|
-
### `
|
|
143
|
-
Execute a shell command. If
|
|
152
|
+
### `execute`
|
|
153
|
+
Execute a shell command. If it runs longer than `yield_ms`, the execution moves
|
|
154
|
+
to the background. Addressable responses include an opaque `execution_id`;
|
|
155
|
+
clients must pass it through unchanged to lifecycle tools. `process_id` is the
|
|
156
|
+
numeric OS PID of the initial shell and is not a lifecycle handle.
|
|
144
157
|
|
|
145
158
|
* **Parameters**:
|
|
146
159
|
* `command` (string, **required**): The command string to execute in the shell.
|
|
@@ -152,7 +165,7 @@ Execute a shell command. If the command runs longer than `yield_ms`, it yields a
|
|
|
152
165
|
* `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
166
|
* `stdin` (string, optional): Initial text input written to standard input immediately after spawning.
|
|
154
167
|
* `close_stdin` (boolean, default: `true`): Close standard input after the initial input is written. Set to `false` when follow-up `write` calls are expected.
|
|
155
|
-
* `name` (string, optional): A human-readable label
|
|
168
|
+
* `name` (string, optional): A human-readable label for this execution.
|
|
156
169
|
* `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
170
|
* `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
171
|
* `max_output_bytes` (integer, optional): Maximum bytes returned in total across both response streams. Subject to the `YIELDSHELL_MAX_OUTPUT_BYTES` cap. This bounds the response only — retention is controlled separately by `YIELDSHELL_MAX_BUFFER_BYTES`, so output withheld here stays readable via `read`.
|
|
@@ -175,37 +188,52 @@ Execute a shell command. If the command runs longer than `yield_ms`, it yields a
|
|
|
175
188
|
* 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=,`.
|
|
176
189
|
|
|
177
190
|
* **Output Statuses**:
|
|
178
|
-
* Every status returns the cursor fields described under [Byte Cursors & Incremental Reads](#byte-cursors--incremental-reads): `start_seq`, `next_seq`, `latest_seq`, `capped`, and `evicted`. The inline snapshot advances the
|
|
179
|
-
*
|
|
180
|
-
* `
|
|
181
|
-
* `
|
|
182
|
-
* `
|
|
183
|
-
* `
|
|
191
|
+
* Every status returns the cursor fields described under [Byte Cursors & Incremental Reads](#byte-cursors--incremental-reads): `start_seq`, `next_seq`, `latest_seq`, `capped`, and `evicted`. The inline snapshot advances the execution's server-side read cursor, so a following `read` or `wait` continues after it rather than repeating it.
|
|
192
|
+
* Every retained result (`completed`, `backgrounded`, `timed_out`, `stopped`, or `failed`) returns `execution_id` and, when available, the initial shell's numeric `process_id`.
|
|
193
|
+
* `completed`: Execution finished within `yield_ms`. Returns exit code, stdout, and stderr; retained output remains addressable with `execution_id`.
|
|
194
|
+
* `backgrounded`: Execution auto-yielded. Returns both identifiers, a snapshot of initial stdout/stderr, `duration_ms`, cursor fields, and a lifecycle-tool hint.
|
|
195
|
+
* `timed_out`: Execution exceeded `timeout_ms` and its process group was terminated.
|
|
196
|
+
* `stopped`: Execution was explicitly terminated.
|
|
197
|
+
* `failed_to_start`: The command could not create a managed execution (for example, because of a bad directory or policy violation). This outcome is not addressable and may omit both identifiers.
|
|
184
198
|
* `failed`: An internal execution error occurred.
|
|
185
|
-
* If initial `stdin` delivery fails, responses for the retained
|
|
199
|
+
* If initial `stdin` delivery fails, responses for the retained execution include
|
|
186
200
|
`stdin_error` with the transport error. Initial delivery runs as tracked
|
|
187
201
|
background work so pipe backpressure does not delay auto-yielding.
|
|
188
202
|
|
|
203
|
+
Example background response (cursor/output fields abbreviated):
|
|
204
|
+
|
|
205
|
+
```json
|
|
206
|
+
{
|
|
207
|
+
"status": "backgrounded",
|
|
208
|
+
"execution_id": "a97e81c9034f",
|
|
209
|
+
"process_id": 42137,
|
|
210
|
+
"stdout": "",
|
|
211
|
+
"stderr": "",
|
|
212
|
+
"next_seq": 1,
|
|
213
|
+
"message": "Execution is running in the background. Use read, wait, or stop with execution_id."
|
|
214
|
+
}
|
|
215
|
+
```
|
|
216
|
+
|
|
189
217
|
### `read`
|
|
190
|
-
Read stdout and/or stderr
|
|
218
|
+
Read stdout and/or stderr from a running or completed execution.
|
|
191
219
|
|
|
192
220
|
* **Parameters**:
|
|
193
|
-
* `
|
|
221
|
+
* `execution_id` (string, **required**): Opaque execution handle returned by `execute`; pass it through unchanged.
|
|
194
222
|
* `since_seq` (integer, optional): Byte-position cursor returned as `next_seq` by the previous read. Enables lossless incremental log polling. When omitted, the read resumes from the server-side cursor instead (see [Byte Cursors & Incremental Reads](#byte-cursors--incremental-reads)); pass `since_seq=1` to re-read from the beginning.
|
|
195
223
|
* `max_output_bytes` (integer, optional): Clamps total output returned across the selected streams. Defaults to the server cap.
|
|
196
224
|
* `streams` (string, default: `"both"`): The streams to read. Options: `"both"`, `"stdout"`, or `"stderr"`.
|
|
197
225
|
* `tail_lines` (integer, optional): Return only the newest N lines instead of reading forward. Useful for monitoring a noisy build or test run without paging through it. Mutually exclusive with `since_seq`; must be positive. If the requested tail exceeds `max_output_bytes`, the newest bytes are kept rather than the oldest.
|
|
198
226
|
|
|
199
227
|
* **Returns**:
|
|
200
|
-
* `
|
|
228
|
+
* `execution_id`, `status`, `exit_code`, `signal`, 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`.
|
|
201
229
|
* Cursor fields: `start_seq` (position of the first byte returned), `next_seq` (cursor to use in the next `since_seq` read), and `latest_seq` (how far output has advanced overall — when it exceeds `next_seq`, more is waiting).
|
|
202
230
|
* Withheld-output flags: `capped` means more output is available and reading again continues from `next_seq`; `evicted` means output fell out of the ring buffer before it was read and cannot be recovered. When either is set, a `hint` field names the concrete follow-up call.
|
|
203
231
|
|
|
204
232
|
### `write`
|
|
205
|
-
Write text input to the standard input (`stdin`) of a running
|
|
233
|
+
Write text input to the standard input (`stdin`) of a running execution.
|
|
206
234
|
|
|
207
235
|
* **Parameters**:
|
|
208
|
-
* `
|
|
236
|
+
* `execution_id` (string, **required**): Opaque execution handle returned by `execute`; pass it through unchanged.
|
|
209
237
|
* `input` (string, **required**): Text input to write.
|
|
210
238
|
* `newline` (boolean, default: `false`): If `true`, appends `\n` to the input.
|
|
211
239
|
* `close_stdin` (boolean, default: `false`): Close standard input after this write, delivering EOF to the process.
|
|
@@ -213,11 +241,11 @@ Write text input to the standard input (`stdin`) of a running process.
|
|
|
213
241
|
request ceiling and return `ok: false` with an error.
|
|
214
242
|
|
|
215
243
|
### `wait`
|
|
216
|
-
Block
|
|
244
|
+
Block until an execution exits or the wait timeout expires.
|
|
217
245
|
|
|
218
246
|
* **Parameters**:
|
|
219
|
-
* `
|
|
220
|
-
* `timeout_ms` (integer, default: `55000`): Maximum time to wait. Never
|
|
247
|
+
* `execution_id` (string, **required**): Opaque execution handle returned by `execute`; pass it through unchanged.
|
|
248
|
+
* `timeout_ms` (integer, default: `55000`): Maximum time to wait. Never stops the execution.
|
|
221
249
|
* `max_output_bytes` (integer, optional): Maximum total output bytes to return across stdout and stderr.
|
|
222
250
|
* `since_seq` (integer, optional): Byte-position cursor from a previous response. When omitted, `wait` resumes from the server-side cursor, so polling in a loop returns each byte exactly once without any cursor bookkeeping.
|
|
223
251
|
* `tail_lines` (integer, optional): Return only the newest N lines. Mutually exclusive with `since_seq`.
|
|
@@ -227,7 +255,7 @@ Block execution until the process exits or the wait timeout expires. This allows
|
|
|
227
255
|
* `waited_ms`: time actually spent waiting.
|
|
228
256
|
* `max_wait_ms`: the effective budget after capping, so a clamped request is visible rather than silent.
|
|
229
257
|
|
|
230
|
-
* **Important**: If the wait timeout expires, `wait` returns the current status but **does not
|
|
258
|
+
* **Important**: If the wait timeout expires, `wait` returns the current status but **does not stop** the execution. It continues running in the background.
|
|
231
259
|
* 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. The response reports that cap in `max_wait_ms`.
|
|
232
260
|
* `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.
|
|
233
261
|
|
|
@@ -237,45 +265,63 @@ Three separate timers control different things. Only one of them ever terminates
|
|
|
237
265
|
|
|
238
266
|
| Parameter | Meaning | Terminates the process? | Default | Effective cap |
|
|
239
267
|
| :--- | :--- | :--- | :--- | :--- |
|
|
240
|
-
| `
|
|
241
|
-
| `
|
|
268
|
+
| `execute(yield_ms)` | How long `execute` stays inline before handing back an `execution_id` and letting the command continue in the background. | No | 30,000ms | Lesser of `YIELDSHELL_MAX_YIELD_MS` and 55,000ms |
|
|
269
|
+
| `execute(timeout_ms)` | Total execution limit. On expiry the process group is sent `SIGTERM`, then `SIGKILL`, and the status becomes `timed_out`. | **Yes** | 3,600,000ms | None (`0` means unlimited) |
|
|
242
270
|
| `wait(timeout_ms)` | Maximum time a single `wait` call blocks. On expiry it returns `wait_result: "deadline_reached"` and the process keeps running. | No | 55,000ms | 55,000ms |
|
|
243
271
|
|
|
244
272
|
If `wait` returns with `status: "running"`, check `wait_result`: `"deadline_reached"` means the wait budget expired while the tracked shell or one of its descendants was still alive. The process keeps running and can be polled again.
|
|
245
273
|
|
|
246
274
|
### `stop`
|
|
247
|
-
Gracefully terminate or force kill a running
|
|
275
|
+
Gracefully terminate or force kill a running execution.
|
|
248
276
|
|
|
249
277
|
* **Parameters**:
|
|
250
|
-
* `
|
|
278
|
+
* `execution_id` (string, **required**): Opaque execution handle returned by `execute`; pass it through unchanged.
|
|
251
279
|
* `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.
|
|
252
280
|
* `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.
|
|
253
281
|
|
|
254
282
|
### `ps`
|
|
255
|
-
List
|
|
283
|
+
List managed executions, including retained terminal records.
|
|
256
284
|
|
|
257
|
-
Terminal records are retained temporarily for inspection. Before each otherwise valid `
|
|
285
|
+
Terminal records are retained temporarily for inspection. Before each otherwise valid `execute` 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`.
|
|
258
286
|
|
|
259
287
|
* **Parameters**:
|
|
260
|
-
* `include_completed` (boolean, default: `true`): If `false`,
|
|
288
|
+
* `include_completed` (boolean, default: `true`): If `false`, terminal executions are excluded.
|
|
261
289
|
* `limit` (integer, default: `50`): Maximum number of entries.
|
|
262
|
-
* **Returns**: `
|
|
290
|
+
* **Returns**: `executions` — a list of execution summaries. Each contains opaque `execution_id`, numeric OS `process_id` (or `null` if unavailable), `name`, `command`, `cwd`, `status`, `exit_code`, `signal`, `started_at`, `ended_at`, `duration_ms`, `stdout_bytes`, `stderr_bytes`, and `stdin_error`.
|
|
263
291
|
* Activity fields help distinguish a genuine hang from quiet work:
|
|
264
292
|
* `last_output_at`: wall-clock time output last arrived on either stream, or `null` if none has.
|
|
265
293
|
* `idle_ms`: milliseconds since that moment, or `null` if no output has arrived. A large and growing `idle_ms` on a `running` process is the signal that it may be stuck.
|
|
266
|
-
* `latest_seq`: the
|
|
294
|
+
* `latest_seq`: the execution-wide cursor position just past the newest captured byte. Comparing it to a `next_seq` you hold shows how much output is waiting.
|
|
295
|
+
|
|
296
|
+
```json
|
|
297
|
+
{
|
|
298
|
+
"executions": [
|
|
299
|
+
{
|
|
300
|
+
"execution_id": "a97e81c9034f",
|
|
301
|
+
"process_id": 42137,
|
|
302
|
+
"status": "running",
|
|
303
|
+
"idle_ms": 125.4,
|
|
304
|
+
"latest_seq": 2049
|
|
305
|
+
}
|
|
306
|
+
]
|
|
307
|
+
}
|
|
308
|
+
```
|
|
267
309
|
|
|
268
310
|
### Error Responses
|
|
269
311
|
|
|
270
|
-
All tools that accept
|
|
312
|
+
All tools that accept an `execution_id` return a structured error when the
|
|
313
|
+
opaque handle is unknown, for example:
|
|
314
|
+
`{"execution_id": "a97e81c9034f", "error": "Unknown execution_id: a97e81c9034f"}`.
|
|
315
|
+
The response echoes `execution_id`; a numeric `process_id` is not accepted as
|
|
316
|
+
a substitute.
|
|
271
317
|
|
|
272
318
|
### `cleanup`
|
|
273
|
-
Prune completed, stopped, timed-out, and failed
|
|
319
|
+
Prune completed, stopped, timed-out, and failed execution records.
|
|
274
320
|
|
|
275
321
|
* **Parameters**:
|
|
276
|
-
* `completed_older_than_ms` (non-negative integer, default: `3600000`): Prunes completed
|
|
277
|
-
* `stopped_older_than_ms` (non-negative integer, default: `3600000`): Prunes stopped, timed-out, or failed
|
|
278
|
-
* **Returns**: `removed` — the count of
|
|
322
|
+
* `completed_older_than_ms` (non-negative integer, default: `3600000`): Prunes completed executions older than this threshold (1 hour default).
|
|
323
|
+
* `stopped_older_than_ms` (non-negative integer, default: `3600000`): Prunes stopped, timed-out, or failed executions older than this threshold (1 hour default).
|
|
324
|
+
* **Returns**: `removed` — the count of execution records pruned. Negative thresholds are rejected without removing records and include an `error` message.
|
|
279
325
|
|
|
280
326
|
---
|
|
281
327
|
|
|
@@ -284,8 +330,8 @@ Prune completed, stopped, timed-out, and failed process records to free memory.
|
|
|
284
330
|
To avoid sending duplicate data over the MCP protocol (which can consume context window space), the server implements a byte-position polling protocol:
|
|
285
331
|
|
|
286
332
|
1. Every stdout/stderr byte receives a unique position in a cursor shared by both streams. Positions start at 1 and only ever increase.
|
|
287
|
-
2. `read`, `wait`, and `
|
|
288
|
-
3. **You do not have to track cursors.** A `read` or `wait` with no `since_seq` and no `tail_lines` resumes from where the last such call stopped, then advances a server-side cursor held per
|
|
333
|
+
2. `read`, `wait`, and `execute` all return `next_seq`, the position immediately after the range covered by the response. A response cap can therefore end safely inside a drain chunk.
|
|
334
|
+
3. **You do not have to track cursors.** A `read` or `wait` with no `since_seq` and no `tail_lines` resumes from where the last such call stopped, then advances a server-side cursor held per execution. Polling in a loop therefore returns each byte exactly once. `execute`'s inline snapshot advances the same cursor, so a following `wait` continues after it.
|
|
289
335
|
4. To drive the cursor yourself instead, pass the previous `next_seq` back as `since_seq` and keep the same `streams` selection.
|
|
290
336
|
5. **A cursorless read consumes.** Once output has been delivered, a later cursorless read will not return it again. To inspect output that was already delivered, pass `since_seq=1` to re-read from the beginning, or use `tail_lines`.
|
|
291
337
|
6. Requests that pass `since_seq`, pass `tail_lines`, or narrow `streams` are treated as **out-of-band**: they neither consult nor advance the server-side cursor. This means a peek at the tail or at stderr cannot silently skip output the polling stream has not seen yet.
|
|
@@ -297,11 +343,11 @@ To avoid sending duplicate data over the MCP protocol (which can consume context
|
|
|
297
343
|
|
|
298
344
|
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.
|
|
299
345
|
|
|
300
|
-
Positions are never reused and never reset. Eviction does not rewind them: it advances the oldest retained position while `next_seq` keeps climbing, so a cursor is safe to hold indefinitely. A cursor pointing at data that has since been evicted is not an error — the read returns data from the earliest retained position onward and sets `evicted` to `true`, and `start_seq` shows where the returned range actually begins. Positions are scoped to one
|
|
346
|
+
Positions are never reused and never reset. Eviction does not rewind them: it advances the oldest retained position while `next_seq` keeps climbing, so a cursor is safe to hold indefinitely. A cursor pointing at data that has since been evicted is not an error — the read returns data from the earliest retained position onward and sets `evicted` to `true`, and `start_seq` shows where the returned range actually begins. Positions are scoped to one execution; a cursor from one `execution_id` is meaningless for another.
|
|
301
347
|
|
|
302
348
|
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.
|
|
303
349
|
|
|
304
|
-
When
|
|
350
|
+
When the initial shell exits normally, `execute`/`wait` responses include output drained through stdout/stderr EOF. If a descendant keeps inherited stdout/stderr open, the server stops waiting on those inherited pipes to avoid indefinite blocking; output written only by that descendant after the shell exits is not part of the managed execution result.
|
|
305
351
|
|
|
306
352
|
---
|
|
307
353
|
|
|
@@ -314,12 +360,12 @@ Configure the server by setting these environment variables prior to launch:
|
|
|
314
360
|
| `YIELDSHELL_DEFAULT_CWD` | Current directory | The fallback working directory for commands. |
|
|
315
361
|
| `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. |
|
|
316
362
|
| `YIELDSHELL_MAX_OUTPUT_BYTES` | `20000` | The default and maximum bytes any single response may return across stdout and stderr. This is a response cap only; it does not limit what is retained. Nonpositive or nonnumeric values use the default. |
|
|
317
|
-
| `YIELDSHELL_MAX_BUFFER_BYTES` | `262144` | Capacity of each per-
|
|
363
|
+
| `YIELDSHELL_MAX_BUFFER_BYTES` | `262144` | Capacity of each per-execution stdout/stderr ring buffer. Retention is deliberately larger than one response so `since_seq` cursors and `tail_lines` still resolve after a gap between polls. Raise it for very chatty commands, at a cost of roughly this value × 2 streams × live executions in memory. Nonpositive or nonnumeric values use the default. |
|
|
318
364
|
| `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. |
|
|
319
365
|
| `YIELDSHELL_DEFAULT_YIELD_MS` | `30000` | Fallback delay before auto-yielding. Effective yields are also capped at 55,000ms. |
|
|
320
366
|
| `YIELDSHELL_MAX_YIELD_MS` | `300000` | Configured maximum for `yield_ms`; the effective maximum is the lesser of this value and 55,000ms. |
|
|
321
367
|
| `YIELDSHELL_DEFAULT_TIMEOUT_MS` | `3600000` | Default hard runtime limit (1 hour). An explicit tool argument of `0` means no limit. |
|
|
322
|
-
| `YIELDSHELL_PROCESS_RETENTION_MS` | `3600000` | Age after which terminal
|
|
368
|
+
| `YIELDSHELL_PROCESS_RETENTION_MS` | `3600000` | Age after which terminal execution records are reaped before a valid spawn. Zero requests immediate age-based reaping. Negative or nonnumeric values use the default. |
|
|
323
369
|
| `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. |
|
|
324
370
|
| `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. |
|
|
325
371
|
| `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. |
|
|
@@ -331,7 +377,7 @@ Configure the server by setting these environment variables prior to launch:
|
|
|
331
377
|
## Security Notes
|
|
332
378
|
|
|
333
379
|
* **Arbitrary Code Execution**: This server executes shell commands on the host system. Always run the server inside a container, sandbox, or isolated development VM.
|
|
334
|
-
* **Side-Effect Declarations**: Every `
|
|
380
|
+
* **Side-Effect Declarations**: Every `execute` call must declare its plausible side-effect categories via `side_effects`. By default, `KILLS_AGENT_PROCESS`, `MODIFIES_OS_SETTINGS`, `MODIFIES_OS_USER_SETTINGS`, `MODIFIES_PROTECTED_FILES`, and `RUNS_INLINE_CODE` are blocked. Operators can adjust the blocklist via `MCP_YIELDSHELL_BLOCKED_SIDE_EFFECTS` (including cleared to `,` to disable every default). This is an explicit risk signal — it is not a complete sandbox, and LLM under-declaration remains possible.
|
|
335
381
|
* **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.
|
|
336
382
|
* **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.
|
|
337
383
|
* **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.
|
|
@@ -341,9 +387,35 @@ Configure the server by setting these environment variables prior to launch:
|
|
|
341
387
|
|
|
342
388
|
## Lifecycle and Compatibility
|
|
343
389
|
|
|
344
|
-
YieldShell gives graceful termination 10 seconds before force-killing, waits up
|
|
390
|
+
YieldShell gives graceful termination 10 seconds before force-killing, waits up
|
|
391
|
+
to 5 seconds for a POSIX process group to disappear, and allows up to 3 seconds
|
|
392
|
+
for final stream draining. On stdio server shutdown, all live managed
|
|
393
|
+
executions are terminated concurrently using the same bounded graceful/forced
|
|
394
|
+
policy. Already-terminal records are not changed by shutdown.
|
|
395
|
+
|
|
396
|
+
`execution_id` is an opaque, server-generated string. Clients must pass it
|
|
397
|
+
through unchanged rather than parsing or pattern-matching it. Its current
|
|
398
|
+
shape is an implementation detail, not a client contract.
|
|
399
|
+
|
|
400
|
+
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 `execute` 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.
|
|
401
|
+
|
|
402
|
+
### Migration (0.x → 1.0)
|
|
403
|
+
|
|
404
|
+
Version 1.0.0 is a clean breaking release. There are no compatibility aliases
|
|
405
|
+
or dual-emitted legacy fields.
|
|
406
|
+
|
|
407
|
+
| 0.x API | 1.0 API |
|
|
408
|
+
| :--- | :--- |
|
|
409
|
+
| Tool `exec` | Tool `execute` |
|
|
410
|
+
| Managed handle `process_id` (string) | Managed handle `execution_id` (opaque string) |
|
|
411
|
+
| OS field `pid` | OS field `process_id` (integer) |
|
|
412
|
+
| `ps` response key `processes` | `ps` response key `executions` |
|
|
413
|
+
| Assumed ID pattern `^proc_[0-9a-f]{12}$` | No supported pattern; treat `execution_id` as opaque |
|
|
345
414
|
|
|
346
|
-
|
|
415
|
+
The most important migration trap is that `process_id` still exists but now
|
|
416
|
+
means the numeric OS PID. Store `execution_id` from `execute` and pass it
|
|
417
|
+
unchanged to `read`, `write`, `wait`, and `stop`. Calls that send the OS
|
|
418
|
+
`process_id` as the handle fail with `Unknown execution_id`.
|
|
347
419
|
|
|
348
420
|
---
|
|
349
421
|
|