A model works out and sets up what a project needs on its build VM; code makes sure that happens, bounds it and checks the result (D3). - Before each driver run on a project, one round trip checks the build VM: the harness's own baseline (missing: the item waits, a template problem), the workspace clone (missing: cloned again and said; the item resumes from what it pushed) and a per-project marker. - No marker (a new project, a new or wiped VM): the item's driver model runs a bounded preflight first, with its own prompt, the execution tools (screened as ever) and toolchain_ready. The harness runs each reported version command itself (version-shaped commands only, even from the marker, which the model's commands could edit) and writes the marker. Gaps and an unfinished preflight are facts and work goes on; the thread hears only of installs and gaps. A rebuilt VM starts from the last toolchain. - Later runs check the versions again; a change is a fact and a thread line. The PR body says what the work was tested with. - A changed host key is its own failure (HostKeyChanged): the item waits for the operator's `trust new host key`, which drops the pin; then the VM is set up again. - A prompt line: install what the work needs on the build VM, say why, and put lasting needs in the project's own pins. - tuning.preflight: max_turns, command_timeout_s. - The property walk gains a VM wipe and ready_before_work (checked against a mutant that skips preflight); parked_work_safe accepts that a wipe loses what wasn't pushed (D3). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PPQ14L3LikyefCMFBqEbKE
109 lines
3.9 KiB
Python
109 lines
3.9 KiB
Python
"""Running commands on a tool VM over SSH (L13), against a throwaway sshd."""
|
|
|
|
import os
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from nisse_dev.execution import REDACTED, HostUnavailable, SSHHost, redact
|
|
|
|
|
|
def test_exit_codes_and_streams_come_back(ssh_host: SSHHost) -> None:
|
|
result = ssh_host.execute("echo out; echo err >&2; exit 3")
|
|
assert (result.exit_code, result.stdout, result.stderr) == (3, "out\n", "err\n")
|
|
assert not result.ok
|
|
|
|
|
|
def test_cwd_and_stdin(ssh_host: SSHHost, tmp_path: Path) -> None:
|
|
result = ssh_host.execute("pwd; cat", cwd=str(tmp_path), stdin="line one\nline two")
|
|
assert result.stdout == f"{tmp_path}\nline one\nline two\n"
|
|
|
|
|
|
def test_stdin_that_looks_like_a_delimiter_survives(ssh_host: SSHHost) -> None:
|
|
payload = "NISSE_EOF\nEOF\n'quoted' $notexpanded `nor this`"
|
|
assert ssh_host.execute("cat", stdin=payload).stdout == payload + "\n"
|
|
|
|
|
|
def test_without_stdin_a_command_cannot_read_the_script(ssh_host: SSHHost) -> None:
|
|
# The script arrives on the same channel; the command must see /dev/null.
|
|
result = ssh_host.execute("cat; echo done")
|
|
assert result.stdout == "done\n"
|
|
|
|
|
|
def test_a_missing_cwd_fails_before_running(ssh_host: SSHHost, tmp_path: Path) -> None:
|
|
marker = tmp_path / "ran"
|
|
result = ssh_host.execute(f"touch {marker}", cwd=str(tmp_path / "absent"))
|
|
assert result.exit_code == 125
|
|
assert not marker.exists()
|
|
|
|
|
|
def test_a_command_exiting_255_is_not_mistaken_for_ssh(ssh_host: SSHHost) -> None:
|
|
assert ssh_host.execute("exit 255").exit_code == 254
|
|
|
|
|
|
def test_an_unreachable_host_raises(tmp_path: Path) -> None:
|
|
host = SSHHost("127.0.0.1", key=tmp_path / "k", state_dir=tmp_path / "s", port=1)
|
|
with pytest.raises(HostUnavailable):
|
|
host.execute("true")
|
|
|
|
|
|
def test_a_timeout_is_reported(ssh_host: SSHHost) -> None:
|
|
result = ssh_host.execute("sleep 30", timeout_s=1)
|
|
assert result.timed_out
|
|
|
|
|
|
def test_secrets_reach_the_command_and_never_its_output(ssh_host: SSHHost) -> None:
|
|
value = "s-" + os.urandom(12).hex()
|
|
result = ssh_host.execute(
|
|
'echo "$API_KEY"; echo "$API_KEY" >&2', secrets={"API_KEY": value}
|
|
)
|
|
assert result.ok
|
|
assert value not in result.stdout + result.stderr
|
|
assert result.stdout == f"{REDACTED}\n"
|
|
|
|
|
|
def test_secrets_are_never_on_a_command_line(ssh_host: SSHHost, tmp_path: Path) -> None:
|
|
# Made up here, so no command line in this test run can contain it except
|
|
# by leaking it.
|
|
value = "s-" + os.urandom(12).hex()
|
|
seen = tmp_path / "ps.txt"
|
|
ssh_host.execute(f"ps -eo args > {seen}", secrets={"API_KEY": value})
|
|
assert "bash -s" in seen.read_text() # the listing did include our shell
|
|
assert value not in seen.read_text()
|
|
|
|
|
|
def test_redact_masks_longest_values_first() -> None:
|
|
assert redact("abc-abcdef", ["abcdef", "abc-abcdef"]) == REDACTED
|
|
assert redact("short ab stays", ["ab"]) == "short ab stays" # too short to mask
|
|
|
|
|
|
def test_a_changed_host_key_is_its_own_failure_and_only_forgetting_clears_it(
|
|
ssh_host: SSHHost, tmp_path: Path
|
|
) -> None:
|
|
import subprocess
|
|
|
|
from nisse_dev.execution import HostKeyChanged
|
|
|
|
host = SSHHost(
|
|
ssh_host.address,
|
|
key=ssh_host._key,
|
|
user=ssh_host.user,
|
|
port=ssh_host.port,
|
|
state_dir=tmp_path / "state",
|
|
)
|
|
assert host.execute("true").ok # first contact pins the key
|
|
host.close()
|
|
# Someone else's key where the pinned one was: a rebuilt VM, or not it.
|
|
subprocess.run(
|
|
["ssh-keygen", "-q", "-t", "ed25519", "-N", "", "-f", str(tmp_path / "other")],
|
|
check=True,
|
|
)
|
|
other = (tmp_path / "other.pub").read_text().split()
|
|
known = tmp_path / "state" / "known_hosts"
|
|
name = host.address if host.port == 22 else f"[{host.address}]:{host.port}"
|
|
known.write_text(f"{name} {other[0]} {other[1]}\n")
|
|
with pytest.raises(HostKeyChanged):
|
|
host.execute("true")
|
|
host.forget_host_key()
|
|
assert host.execute("true").ok # the real key pinned again
|