Initial Commit
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
#!/usr/bin/env python3
|
||||
"""A second run in the same chat is watched, and git stays on the host.
|
||||
|
||||
python3 tests/test_vm_second_run.py
|
||||
|
||||
THE BUG THIS EXISTS FOR
|
||||
|
||||
A conversation asked for an app, got it, then asked for a fix. The second
|
||||
vm_agent call ran for 45 minutes and finished successfully - and the
|
||||
person watching saw an empty message and a toast saying the machine had
|
||||
been destroyed, and reasonably concluded the tool had given up.
|
||||
|
||||
The live-progress file is named after the CHAT, not the run. The first
|
||||
run left it with `done: true`. The second run's watcher started, polled
|
||||
once, read the previous run's ending, announced it and exited FOUR
|
||||
SECONDS IN. Its whole status history was:
|
||||
|
||||
Calling vm_agent
|
||||
VM · Starting a machine...
|
||||
VM · Done
|
||||
The machine has been destroyed.
|
||||
|
||||
while the run it was supposed to be watching carried on to sequence 1035.
|
||||
|
||||
Two guards, because a race with one guard is a race: the api deletes the
|
||||
file before the job is queued, and the watcher ignores anything stamped
|
||||
earlier than itself.
|
||||
|
||||
AND THE OTHER THING THAT RUN DID
|
||||
|
||||
It spent its last dozen turns fighting `git` inside the machine - exit
|
||||
128, "dubious ownership", `safe.directory`, `HOME` - having said "let me
|
||||
use the git_commit tool" at turn 99 and then gone back to raw git. A
|
||||
commit made in the guest also skips everything gitea.py does on the host:
|
||||
the hook and filter hardening, the right author, and leaving scratch out
|
||||
of what gets published. So it is refused, in tool output.
|
||||
"""
|
||||
|
||||
import ast
|
||||
import os
|
||||
import sys
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
ROOT = os.path.join(HERE, "..")
|
||||
|
||||
FAILS: list[str] = []
|
||||
|
||||
|
||||
def check(label: str, condition: bool, detail: str = "") -> None:
|
||||
if condition:
|
||||
print(f"ok {label}")
|
||||
else:
|
||||
FAILS.append(f"{label}: {detail}")
|
||||
print(f"FAIL {label}\n {detail}")
|
||||
|
||||
|
||||
def read(*parts: str) -> str:
|
||||
return open(os.path.join(ROOT, *parts), encoding="utf-8").read()
|
||||
|
||||
|
||||
# --- guard one: the api clears the last run's state before queueing -------
|
||||
api = read("app", "owuitools", "routers", "vm.py")
|
||||
submit = api[api.index("async def _submit"):]
|
||||
check("the api deletes the previous run's progress file",
|
||||
'os.path.join(JOBS_DIR, "progress"' in submit and "os.unlink(stale)" in submit,
|
||||
"otherwise the new watcher reads the old run's `done` and stops")
|
||||
check("and the answer to the previous run's confirmation",
|
||||
"answer.json" in submit,
|
||||
"a stale answer would decide a question the person was never asked")
|
||||
check("it happens before the job is queued, not after",
|
||||
submit.index("os.unlink(stale)") < submit.index('f"{job_id}.ready"'),
|
||||
"after the job is queued there is a window for the watcher to read it")
|
||||
|
||||
# --- guard two: the watcher ignores anything older than itself ------------
|
||||
flt = read("owui-functions", "vm_agent_progress.py")
|
||||
check("the watcher stamps when it started, on the wall clock",
|
||||
"began_at = time.time()" in flt,
|
||||
"the loop clock is monotonic and is not comparable with the file's stamp")
|
||||
check("and discards progress written before then",
|
||||
'float(data.get("updated") or 0) < began_at' in flt,
|
||||
"this is what a previous run's file looks like")
|
||||
check("the file it compares against is stamped on every flush",
|
||||
'"updated": round(time.time(), 2)' in read("vmagent", "app", "events.py"))
|
||||
|
||||
# --- git belongs to the host ---------------------------------------------
|
||||
vmd = read("vm", "vmd", "vmd.py")
|
||||
# Lifted out of vmd.py and run, rather than matched as text: the point
|
||||
# is what it decides, not what it looks like.
|
||||
tree = ast.parse(vmd)
|
||||
wanted = ("_GIT_WRITES", "_SEPARATORS", "_PREFIXES", "_git_write_in")
|
||||
body = []
|
||||
for node in tree.body:
|
||||
if isinstance(node, ast.FunctionDef) and node.name in wanted:
|
||||
body.append(node)
|
||||
elif isinstance(node, ast.Assign) and any(
|
||||
isinstance(t, ast.Name) and t.id in wanted for t in node.targets):
|
||||
body.append(node)
|
||||
check("the detector and everything it needs was found in vmd.py",
|
||||
len(body) == len(wanted), f"found {len(body)} of {len(wanted)}")
|
||||
|
||||
namespace: dict = {"os": os}
|
||||
exec(compile(ast.Module(body=body, type_ignores=[]), "vmd", "exec"), namespace)
|
||||
detect = namespace["_git_write_in"]
|
||||
|
||||
# Every one of these is a real command line from the session that failed.
|
||||
for command, want in [
|
||||
("cd /mnt/project && git add -A && git status --short", "add"),
|
||||
("cd /mnt/project && git config --global --add safe.directory /mnt/project "
|
||||
"&& git add -A && git status --short", "config"),
|
||||
("cd /mnt/project && export HOME=/root && git -c safe.directory=/mnt/project add -A "
|
||||
"&& git -c safe.directory=/mnt/project commit -m 'x'", "add"),
|
||||
("git -C /mnt/project commit -m x", "commit"),
|
||||
("/usr/bin/git push origin main", "push"),
|
||||
]:
|
||||
check(f"refused: {command[:52]}...", detect(command) == want, f"got {detect(command)!r}")
|
||||
|
||||
# And the things that must NOT be caught, or the refusal becomes the problem.
|
||||
for command in [
|
||||
"git log --oneline -1",
|
||||
"git status --short",
|
||||
"python3 -c 'print(\"git add is a string\")'",
|
||||
"grep -n 'git commit' notes.txt",
|
||||
"ls -la /mnt/project",
|
||||
"echo git commit > /tmp/note.txt",
|
||||
]:
|
||||
check(f"allowed: {command[:52]}", detect(command) is None, f"got {detect(command)!r}")
|
||||
|
||||
run_body = vmd[vmd.index("def v_run("):vmd.index("def v_write_file(")]
|
||||
check("v_run refuses before it reaches the machine",
|
||||
"_git_write_in(script)" in run_body and "raise VerbError" in run_body)
|
||||
check("and the refusal names the tool that does work",
|
||||
"git_commit" in run_body,
|
||||
"a refusal that does not say what to do instead is a wall")
|
||||
check("it explains the exit 128 rather than leaving it to be rediscovered",
|
||||
"dubious ownership" in run_body)
|
||||
|
||||
print()
|
||||
if FAILS:
|
||||
print(f"{len(FAILS)} failed")
|
||||
for line in FAILS:
|
||||
print(" -", line)
|
||||
sys.exit(1)
|
||||
print("a second run is watched, and git stays where it works")
|
||||
Reference in New Issue
Block a user