Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 38 additions & 6 deletions emrg/server/daemon.py
Original file line number Diff line number Diff line change
Expand Up @@ -4258,9 +4258,15 @@ def _inject_tool_arguments(
) -> None:
"""Inject the parts of a tool call the model must not choose.

Three rules. The first two are one fact — the session's cwd is where a
filesystem tool works — split by what the value decides for that tool: a
boundary (pinned) or a preference (a default). The third is the tier.
Five classes, in the order the body writes them. The first two are one
fact — the session's cwd is where a filesystem tool works — split by what
the value decides for that tool: a boundary (pinned) or a preference (a
default). The third is the tier. The last two are the ones a caller would
get wrong left to itself: the root list, and the parent a delegation is
born from. Every key this method writes is named below
(``tests/test_session_sandbox.py`` pins the enumeration to the code), and
that is the point of the list — it is the only map a reader has for asking
what the daemon overrides in a call it did not write.

* **the session's cwd is where a filesystem tool works — for the tools
that run a command.** Injected unconditionally into the shell tools,
Expand Down Expand Up @@ -4298,9 +4304,35 @@ def _inject_tool_arguments(
test_shell_tool_mount.py`` now pins by driving this method and then the
tool rather than asserting the argument dict alone.
* **the sandbox tier is the task's, not the agent's** (rant
2026-08-20T15:46:50). ``write``/``edit`` also receive it, together with
the workspace boundary, so that under read-only they cannot clobber the
host's uncommitted tree (community issue #979).
2026-08-20T15:46:50). ``write``/``edit`` also receive it, under the key
``sandbox``, together with the workspace boundary, so that under
read-only they cannot clobber the host's uncommitted tree (community
issue #979).
* **the host names the writable roots, and only the host.** The tools
that write under a root — the shell tools, ``write`` and ``edit``, the
same set the tier reaches — receive ``writable_roots`` from the
session's ``sandbox_roots``, injected **unconditionally** (rant
2026-10-09T09:43:39 §4). The difference from the tier above is
deliberate on both sides: the tier's injection is guarded by
``req.sandbox`` because a call carrying no tier belongs to a host
session whose unconfined default is silence, and silence cannot widen
anything. A list of roots has no such neutral value, and an honoured
model-supplied list would be a sandbox whose boundary the sandboxed
decided — so whatever the session holds, including nothing, is what the
call runs with.
* **the parent of a delegation is a fact, not a field.** ``subagent`` and
``boss`` receive ``_parent_session_id``, ``_parent_cwd``,
``_parent_tools`` and ``_parent_sandbox``, all of them unconditional
(issues #2451, #2465, #2467), for one reason in four parts: the child's
slot is derived from the parent's id and cwd, so a caller naming its own
would name a wrong one; the child inherits **this turn's** tool set, so
a round that may not use a tool cannot hand it to a session it spawns;
and it inherits **this turn's** tier — ``req.sandbox``, the same value
injected above — so a task's child is as confined as the task. Reading
that tier from the session object instead was a hole: a scheduled task's
tier is resolved from ``tasks.yml`` into the request and never written
onto the session, so every child such a task spawned ran unconfined at
``policy.DEFAULT_MODE`` while the task body itself was confined.

``workspace`` is a new key for the shell tools and the old executor
ignores it (it reads ``command``/``timeout``/``workdir``/``sandbox``
Expand Down
53 changes: 53 additions & 0 deletions tests/test_session_sandbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,59 @@ def test_the_client_turn_path_resolves_the_tier_before_the_loop_starts():
assert "resolve_client_tier" not in inject_source


def test_the_injection_documents_every_argument_it_writes():
"""The docstring is the only map of what this method overrides.

A reader asking what the daemon puts into a tool call it did not write has
one instrument — the enumeration in this method's docstring — and nothing
else reads it. It said "three rules" while the body wrote nine keys in five
classes, and the two it left out were exactly the safety injections: the
host's writable roots, and the parent a delegation is born from. A reader
who trusted the list concluded that both came from the model, which is the
opposite of what the code does — so the prose did not merely lag the code,
it misdescribed the boundary the code exists to hold.

Prose drifts while every behavioural test stays green, so the drift is
measured here instead of remembered. The key set is derived from the source,
so a key the method starts writing is visible without anyone maintaining a
list of keys; ``pinned`` then turns that into a decision rather than an
accident, and fails in the other direction too — a key dropped from the code
whose bullet outlives it leaves the prose documenting something that is not
written.

Both directions are the guard, and neither is a behavioural claim: the
injection's own behaviour is pinned by the tests that drive it
(``test_shell_tool_mount.py``, ``test_boss_tool.py``,
``test_subagent_tool.py``). This one pins the map.
"""
source = inspect.getsource(EmrgServer._inject_tool_arguments)
written = set(re.findall(r'args\["(\w+)"\]\s*=', source))

pinned = {
"workdir", # the session cwd: pinned for the shell tools, a default for glob
"path", # grep's spelling of the same fact
"sandbox", # the task's tier
"workspace", # the write boundary that rides with the tier
"writable_roots", # the host's roots, injected unconditionally
"_parent_session_id", # a delegation's parent — four facts the model
"_parent_cwd", # must not choose, all injected unconditionally
"_parent_tools",
"_parent_sandbox",
}
assert written == pinned, (
f"the injection writes {sorted(written)}, the guard expects "
f"{sorted(pinned)} — decide which is right, then update both the code's "
f"docstring and this pin"
)

doc = EmrgServer._inject_tool_arguments.__doc__ or ""
undocumented = sorted(k for k in written if f"``{k}``" not in doc)
assert not undocumented, (
f"the injection writes {undocumented} and its docstring never names "
f"them — the list a reader audits the sandbox surface from is incomplete"
)


# ── the daemon's one write path and its broadcast ─────────────────────────


Expand Down
Loading