Skip to content

The bash tool's description omits the fresh-process contract its twin states, and neither twin names the marker a denial writes #2051

Description

@argszero

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions