Files
2026-09-15 15:24:03 -04:00

89 lines
3.8 KiB
Python

"""The one question vm_agent stops to ask, and how it decides to ask it.
WHY THIS IS A SEPARATE MODULE
Two reasons, and the second is the real one.
It is dependency-free, so it can be unit-tested on the host without the
container - the same arrangement as matlab_lint.py, and for the same
reason: the interesting half of this rule cannot be tested over HTTP. A
task that is let through starts a real VM session, which is minutes long
by design, and the smoke suite's per-case timeout is 90 seconds.
And it puts the rule somewhere findable. It is the only pre-flight
refusal in the suite that is about the CALLER's judgement rather than
about a malformed argument, so it will be argued with, and the argument
should happen against a file that explains itself.
WHY THE RULE EXISTS
A Python script runs on both machines. A `.sh` runs only on Ubuntu; a
`.ps1` or `.bat` only on Windows. The model has to choose, and it has no
way to know unless the user said - but the machine is destroyed at the
end of the session, so guessing wrong does not cost a retry, it costs the
whole session.
The system prompt asks the model to check first. That is not enough on
its own: this file's oldest and most-repeated finding is that the model
treats tool output as fact and system-prompt text as advice, and a prompt
rule loses to an explicit user demand. So the tool refuses, in its own
output, and hands back the exact question to relay.
WHY IT HAS TO BE NARROW
Firing when the caller has already said which machine would be worse than
not firing at all. It would teach the model that the way past this tool
is to add a magic word, rather than to ask - and a model that has learned
to appease a check has stopped reading it.
"""
from __future__ import annotations
import re
# Things that run on both machines. Word-bounded throughout: `\bgo\b`
# rather than `go`, or every mention of "gopher" and "going" trips it.
CROSS_PLATFORM = re.compile(
r"\bpython3?\b|\bpy\b|\.py\b|\bpip3?\b|\bnode\b|\bnpm\b|\.js\b"
r"|\bjava\b|\bgo\b|\bgolang\b|\brust\b|\bcargo\b|\bdotnet\b|\bruby\b|\bperl\b",
re.IGNORECASE,
)
# Deliberately generous. The point is to catch a silent guess, not to
# interrogate a caller who has already been clear - so a mention of the
# shell, the file extension or the package manager counts as having said.
_SAYS_UBUNTU = (
"ubuntu", "linux", "debian", "bash", "sh script", ".sh", "shell script",
"apt ", "apt-get", "apt install", "/usr/", "/etc/", "/home/", "systemd",
)
_SAYS_WINDOWS = (
"windows", "powershell", "pwsh", ".ps1", "batch", ".bat", ".cmd",
"cmd.exe", "winget", "c:\\", "registry", ".exe",
)
def names_its_machine(task: str, os_name: str) -> bool:
"""Has the caller already said which machine, in the task itself?"""
lowered = (task or "").lower()
words = _SAYS_WINDOWS if os_name == "windows" else _SAYS_UBUNTU
return any(word in lowered for word in words)
def should_ask(task: str, os_name: str) -> bool:
"""True when the tool should stop and have the user asked."""
return bool(CROSS_PLATFORM.search(task or "")) and not names_its_machine(task, os_name)
def question(os_name: str) -> str:
"""The refusal, written for the model to relay rather than to obey."""
other = "Windows" if os_name == "ubuntu" else "Ubuntu"
return (
f"CANNOT DO THAT YET: this task mentions something that runs on both machines, "
f"and you chose {os_name}. Ask the user which they want - {other} is just as "
"likely and there is nothing in the task text to tell you.\n\n"
"Put it to them in one round, then call again with their answer. If they do not "
f"mind, say which you picked and call again with the task naming {os_name} "
"explicitly. Guessing costs the whole session: the machine is destroyed at the "
"end of it, so there is nothing to correct afterwards."
)