The asymmetry. The two shell dialects' tool descriptions are the model's only view of how a run behaves, and the design's premise (§14.5 item 1, the principle #2039 rests on) is that the dialects differ only in the shell. #2046 pinned that shut for one contract — the exit marker — but two further facts are stated by pwsh_tool_v2 and silently absent from bash_tool_v2, and both are true of the bash tool:
| the description of |
bash_tool_v2 |
pwsh_tool_v2 |
| … the exit marker |
states it (#2046) |
states it |
| … a fresh process per call |
silent |
"Each call is a fresh process — no state (working directory, variables, functions) persists between calls, so pass workdir instead of using cd." |
| … what a refused write looks like |
"…are refused whatever language or subprocess attempts them." (no marker) |
"…is reported as a sandbox denial" (no marker) |
Read from the two definition()s by AST: 404 chars for bash against 1488 for pwsh.
The fresh-process fact is true of bash, and measured on this host. Two consecutive calls through the tool:
call 1: cd /tmp && ... -> pwd=/tmp, shell pid 46218
call 2: ... -> pwd=<the session cwd>, shell pid 46227
A different pid and the working directory reset, because bash_tool_v2 builds [_INNER_SHELL, "-c", command] and runs it with asyncio.create_subprocess_exec per call (line 452 / 479). So the sentence is not Windows-only: it is the bash tool's own execution model, and a model that does cd in one call and expects it to persist in the next is being told less by the dialect it uses most.
The denial marker is one vocabulary for both families. emrg/sandbox/contract.py:234:
The model-facing denial marker — one vocabulary for both enforcing families. The bash family (a refused file effect) and, from P7, the in-process fence (a refused mutation) report a denial with the same line, so the model recognizes a policy denial identically whichever layer refused it.
and it returns [sandbox: file access denied under <mode> mode]. bash_tool_v2 imports and renders it (render_result, its sandbox.get("denied") branch). So the bash description tells the model a write will be refused but not what that refusal looks like, while both descriptions leave the model to guess a marker the renderer writes by name in a shared function.
Nothing else says either thing. grep -rn "fresh process\|does not persist\|no state persists\|instead of using .cd" across emrg/, tests/ and the prompt templates returns no other hit — the same reading #2046 made for the exit marker, which found the system prompt silent too.
What finishing this looks like. Both descriptions state the fresh-process contract in the same words, both name the marker their renderer writes, and tests/test_shell_tool_descriptions.py — whose premise is exactly this pairing — pins both against the behaviour they promise rather than against the sentence alone.
The asymmetry. The two shell dialects' tool descriptions are the model's only view of how a run behaves, and the design's premise (§14.5 item 1, the principle #2039 rests on) is that the dialects differ only in the shell. #2046 pinned that shut for one contract — the exit marker — but two further facts are stated by
pwsh_tool_v2and silently absent frombash_tool_v2, and both are true of the bash tool:bash_tool_v2pwsh_tool_v2workdirinstead of usingcd."Read from the two
definition()s by AST: 404 chars for bash against 1488 for pwsh.The fresh-process fact is true of bash, and measured on this host. Two consecutive calls through the tool:
A different pid and the working directory reset, because
bash_tool_v2builds[_INNER_SHELL, "-c", command]and runs it withasyncio.create_subprocess_execper call (line 452 / 479). So the sentence is not Windows-only: it is the bash tool's own execution model, and a model that doescdin one call and expects it to persist in the next is being told less by the dialect it uses most.The denial marker is one vocabulary for both families.
emrg/sandbox/contract.py:234:and it returns
[sandbox: file access denied under <mode> mode].bash_tool_v2imports and renders it (render_result, itssandbox.get("denied")branch). So the bash description tells the model a write will be refused but not what that refusal looks like, while both descriptions leave the model to guess a marker the renderer writes by name in a shared function.Nothing else says either thing.
grep -rn "fresh process\|does not persist\|no state persists\|instead of using .cd"acrossemrg/,tests/and the prompt templates returns no other hit — the same reading #2046 made for the exit marker, which found the system prompt silent too.What finishing this looks like. Both descriptions state the fresh-process contract in the same words, both name the marker their renderer writes, and
tests/test_shell_tool_descriptions.py— whose premise is exactly this pairing — pins both against the behaviour they promise rather than against the sentence alone.