Agent chats run commands, and stop to ask first
The four tools an agent chat has -- shell_run, file_read, file_write, file_list -- and the mode table wired into the loop that decides which of them stop for approval. Verified end to end against a real Kali container over SSH: the card shows the command, allowing it runs it there, and the file it writes is visible from outside. The mode is enforced in `_authorise`, in the generation loop, server-side, keyed on each tool's declared risk. Not in the prompt: a model is told which mode it is in so it behaves sensibly, but everything it reads -- a web page, a README, the output of the last command -- is untrusted, and a rule written only into a system message is one a poisoned file can argue with. Within an agent chat every call goes through the table, including the built-in ones, because notes_edit writes and Plan mode meaning "look but do not touch" has to mean that too. Two things this turned up. The runners re-check the mode as a backstop, and that backstop refused the very thing a person had just approved -- the mode says "ask", and asking was exactly what happened. Approval is now threaded per call, on a copy of the context, because a round runs its calls together and only some of them were allowed. And the harness said nothing at all, because `registry` maps an offered tool *name* back to a family and did not know the agent tools existed. So shell_run resolved to no family and the fragment naming the machine, the directory and the mode was never admitted. The same omission cost custom tools their guidance once already; there is a test for it now. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -131,6 +131,10 @@ def context_variables(
|
||||
"skills": skills_service.index_block(db, user) if "skills" in families else "",
|
||||
"knowledge_bases": "",
|
||||
"document_names": "",
|
||||
"agent_target": "",
|
||||
"agent_dir": "",
|
||||
"agent_mode": "",
|
||||
"agent_rewound": "",
|
||||
}
|
||||
|
||||
if chat is not None:
|
||||
@@ -145,9 +149,38 @@ def context_variables(
|
||||
values["knowledge_bases"] = ", ".join(base.name for base in chat.knowledge_bases)
|
||||
values["document_names"] = _document_names(db, chat)
|
||||
|
||||
# The one thing a tool description cannot carry, because a description
|
||||
# is schema: which machine, which directory, and what this chat's mode
|
||||
# currently permits. `max_rounds` is corrected here too, or an agent
|
||||
# chat with forty rounds is told it has three.
|
||||
if "agent" in families:
|
||||
values.update(_agent_values(db, chat, user))
|
||||
|
||||
return values
|
||||
|
||||
|
||||
def _agent_values(db: DBSession, chat, user) -> dict[str, str]:
|
||||
"""What an agent chat's harness needs to say about where it is."""
|
||||
from lembas.services.agent import policy
|
||||
from lembas.services.agent import session as agent_session
|
||||
|
||||
context = agent_session.resolve(db, chat, user)
|
||||
if context is None:
|
||||
return {}
|
||||
|
||||
rewound = ""
|
||||
if getattr(chat, "rewound_at", None) is not None:
|
||||
rewound = chat.rewound_at.strftime("on %-d %B at %H:%M")
|
||||
|
||||
return {
|
||||
"agent_target": context.label,
|
||||
"agent_dir": context.project_dir or "the login directory",
|
||||
"agent_mode": policy.MODE_GUIDANCE.get(context.mode, ""),
|
||||
"agent_rewound": rewound,
|
||||
"max_rounds": str(context.limits.steps),
|
||||
}
|
||||
|
||||
|
||||
def limit_for(db: DBSession) -> int:
|
||||
"""The ceiling on the assembled block."""
|
||||
stored = settings_store.get(db, "max_harness_chars", key=settings_store.PROMPTS)
|
||||
|
||||
Reference in New Issue
Block a user