Repository navigation
emrg: a message cannot choose which tools the session gets — the table is fixed at daemon start #2465
Description
What is true today
emrg/server/daemon.py registers eight tools once, in build_tool_registry (daemon.py:669-685): the platform's shell dialect, read, write, edit, glob, grep, submit_rant, subagent. After start-up nothing narrows that table for any session or for any single message:
_run_tool_loopserialises the whole registry into every round's request (daemon.py:4419,tools_base = self.tools.to_openai_tools()), and the round loop re-sends it unchanged.- The execution side resolves a requested name out of the same global table (
daemon.py:5011,tool = self.tools.get(tc_name)), so a model that names a tool is answered from the full registry regardless of what it was offered. - The memory-reflection loop builds a second, independent copy of that table (
daemon.py:7822) and resolves names against the registry itself (daemon.py:7883). - The
taskmessage carries no tool field at all:TaskRequest(emrg/protocol.py:33-49) hassandboxand notools; the receiver (daemon.py:1644) reads the fields it knows; the producer (emrg/server/scheduler.py:2681) builds a dict withprompt,sandboxandinput_historyonly.
Consequence. A conversation the host typed and a scheduled task's round are served exactly the same eight tools, and there is no way to make them differ. A tool that is only meaningful for a scheduled task cannot be kept out of a host conversation except by a second mechanism inside the tool itself.
What the change must satisfy
TaskRequestgainstools: Optional[list[str]] = None, carried byto_dictthe waysandboxis (protocol.py:63-64); thetaskbranch reads it asdata.get("tools")(daemon.py:1644) and it reaches the tool loop asreq.tools.- Two named whitelists live in the daemon beside the registration: a default set and a scheduled-task set. Both are whitelists — a newly registered tool that neither names is invisible to that class of session. The shell entry is taken per platform (
shell_tool_name()), never the literal"bash": on Windows the tool is namedpwsh. - Effective set =
(the message's list if not None else the default set) ∩ registered names. A name that is not registered is dropped silently — no error, and no start-up validation. - The effective set is enforced in all three places, because any one of them alone is a false gate: the table serialised for the model (
daemon.py:4419), the execution-side lookup (daemon.py:5011), and the memory-reflection loop's own table and lookup (daemon.py:7822/:7883). - The scheduler's
task_msgnames the scheduled-task set (scheduler.py:2681). - A child session spawned by the
subagenttool inherits its parent's effective set.
Acceptance
- Four cases, each with its own test: a list is honoured by intersection; no list falls back to the default set; a list carrying an unregistered name drops that name and keeps the rest; a list of nothing but unknown names yields zero tools.
- The execution side is exercised for real, not only through the serialised table: a session whose set names only A must not get B's result even though B is registered.
- The memory-reflection table carries a test of its own, so a change to the main loop cannot fix it by accident.
- Mutation arms: removing the filter at the serialisation site, and removing it at the execution site, each turns its own test red.
Out of scope
The scheduled-task-only tool itself (a review / owner role). It is not part of this change; once it exists it is added to the scheduled-task set alone, which is what makes requirement 2 worth doing now.
Activity
argszero commentedon Oct 22, 2026
Handled by #2466