ouroboros/tests/test_workspace_write_shape.py
Ouroboros 4ec7840952 Integrate subscriptions with current task and service ownership
Preserve the shared decision ingress, received HTTP status with byte payloads, both mailbox custody holds, visible ephemeral work and numeric owners. The dependency release and final public-byte pin remain pending.
2026-09-07 16:26:28 +00:00

1146 lines
49 KiB
Python

"""Mode-aware write-shape classification for interpreter shell commands.
The coarse ``open(`` token marks a read-only ``open(p, 'rb')`` as writeish.
The light-mode runtime_data lane already re-judges that class mode-aware
("the original GAIA class", tests/test_runtime_reliability_v655.py); the
workspace write guard's ``writeish`` composition did not, so a pure-read
hash/compare one-liner in an external workspace was refused as a
"write-like shell command" — a false reason with no route. These tests pin
the mode-aware composition: pure interpreter reads are not write-shaped,
every real write shape still is, and the runtime/secret READ policy for
external workspaces stays intact via its own honest guard.
"""
from __future__ import annotations
import pathlib
import pytest
# Shares the real-subprocess-adjacent registry harness with
# test_external_workspace_access.py; keep it in the serial lane.
pytestmark = pytest.mark.serial
from ouroboros.contracts.task_constraint import TaskConstraint
from ouroboros.tools.registry import ToolContext, ToolRegistry
from ouroboros.tools.shell_guards import interpreter_write_shape, shell_has_write_indicator
from tests._typed_guard_shared import _shell_guard_text
@pytest.fixture(autouse=True)
def _home_outside_tmp(tmp_path, monkeypatch):
# Same premise as test_external_workspace_access.py: keep tmp scratch
# outside $HOME on every platform so host-scratch reads stay non-runtime.
fake_home = tmp_path / "_home"
fake_home.mkdir(exist_ok=True)
monkeypatch.setattr(pathlib.Path, "home", lambda: fake_home)
def _registry(tmp_path: pathlib.Path, *, mode: str = "external", acting: bool = False) -> ToolRegistry:
system = tmp_path / "system"
workspace = tmp_path / "workspace"
data = tmp_path / "data"
for p in (system, workspace, data):
p.mkdir(exist_ok=True)
reg = ToolRegistry(repo_dir=system, drive_root=data)
reg.set_context(
ToolContext(
repo_dir=system,
drive_root=data,
workspace_root=workspace,
workspace_mode=mode,
task_id="task-write-shape",
task_constraint=(TaskConstraint(mode="acting_subagent", surface="external_workspace",
write_root=str(workspace)) if acting else None),
)
)
return reg
READ_ONLY_HASH_SCRIPT = (
"import hashlib\n"
"def h(p):\n"
" with open(p, 'rb') as f:\n"
" return hashlib.sha256(f.read()).hexdigest()\n"
"print(h({target!r}))\n"
)
# Outside-workspace write refusals below use an ACTING child. Ordinary roots
# have the same user_files write authority regardless of their selected cwd.
# --- unit layer: the classifier itself -------------------------------------
def test_read_only_open_is_not_interpreter_write_shape():
cmd = ["python3", "-c", "with open('f.bin', 'rb') as f:\n print(len(f.read()))"]
assert interpreter_write_shape(cmd) is False
# The legacy coarse classifier keeps its pinned behavior for its other
# consumers (_protected_shell_block, ws5 carryover).
assert shell_has_write_indicator(cmd) is True
def test_write_mode_open_and_pathlib_open_stay_write_shaped():
assert interpreter_write_shape(["python3", "-c", "open('/d/x', 'w').write('hi')"]) is True
assert (
interpreter_write_shape(
["python3", "-c", "from pathlib import Path; Path('/d/x').open('w')"]
)
is True
)
def test_opaque_subprocess_and_library_saves_stay_write_shaped():
assert (
interpreter_write_shape(
["python3", "-c", "import subprocess; subprocess.run(['rm', '-rf', '/d/x'])"]
)
is True
)
assert interpreter_write_shape(["python3", "-c", "df.to_csv('out.csv')"]) is True
assert interpreter_write_shape(["python3", "-c", "fh.writelines(rows)"]) is True
assert (
interpreter_write_shape(
["node", "-e", "const {writeFileSync} = require('fs'); writeFileSync('x', 'y')"]
)
is True
)
def test_shell_level_signals_still_write_shaped_for_interpreters():
assert interpreter_write_shape(["sh", "-c", "python3 -c 'print(1)' && rm -rf /tmp/x"]) is True
assert interpreter_write_shape("python3 gen.py > out.txt") is True
assert interpreter_write_shape(["sh", "-c", "python3 gen.py && cp out.txt /tmp/y"]) is True
def test_ruby_perl_pure_reads_are_not_write_shaped():
"""LIGHT_SHELL_WRITER_COMMANDS membership (a coarse shell-writer role) must not
re-add the write shape the mode-aware classifier just re-judged."""
assert interpreter_write_shape(["ruby", "-e", "puts File.read('/tmp/f.txt')"]) is False
# perl 3-arg READ open: the filename's own letters (the 'x' in f.txt) must not
# classify the mode.
assert (
interpreter_write_shape(["perl", "-e", "open(my $fh, '<', '/tmp/f.txt'); print <$fh>"])
is False
)
assert interpreter_write_shape(["ruby", "-e", "File.write('/tmp/x', 'y')"]) is True
def test_perl_ruby_native_write_idioms_stay_write_shaped():
"""fable-5 review: dropping the membership floor demands the vocabulary
actually SEE perl/ruby write spellings — '>'-mode opens, File.delete,
FileUtils with a variable argument, IO.binwrite."""
assert interpreter_write_shape(["perl", "-e", "open(FH,'>','/outside/x'); print FH 'data'"]) is True
assert interpreter_write_shape(["perl", "-e", "open(FH, '>>', $log); print FH $line"]) is True
assert interpreter_write_shape(["ruby", "-e", "File.delete('/outside/x')"]) is True
assert interpreter_write_shape(["ruby", "-e", "f='/x'; FileUtils.rm_rf(f)"]) is True
assert interpreter_write_shape(["ruby", "-e", "IO.binwrite('a.bin', d)"]) is True
def test_ruby_file_open_is_mode_aware(tmp_path):
"""sol review: File.open must be a write target only with a write-mode 2nd arg —
File.open('/x','r') is a READ and must not be reported as a write, while
File.open('/x','w') and File.new('/x','w') stay writes."""
from ouroboros.tools.shell_guards import writer_target_tokens
read = ["ruby", "-e", "File.open('/tmp/r','r') { |f| puts f.read }"]
# The literal path is NOT emitted as a write target for a read-mode open.
assert "/tmp/r" not in writer_target_tokens(read)
assert interpreter_write_shape(read) is False
assert interpreter_write_shape(["ruby", "-e", "File.open('/tmp/o','w') { |f| f.puts 'x' }"]) is True
assert "/tmp/o" in writer_target_tokens(["ruby", "-e", "File.open('/tmp/o','w') { |f| f.puts 'x' }"])
assert interpreter_write_shape(["ruby", "-e", "File.new('/tmp/o','w')"]) is True
assert interpreter_write_shape(["ruby", "-e", "File.new('/tmp/o','r')"]) is False
def test_keyword_mode_open_is_write_shaped():
"""sol review: open('/x', mode='w') with a statically-known keyword mode is a
real write (distinct from the disclosed variable-mode residual)."""
assert interpreter_write_shape(["python3", "-c", "open('/tmp/o', mode='w')"]) is True
assert interpreter_write_shape(["python3", "-c", "open('/tmp/o', mode='rb')"]) is False
def test_unspaced_posix_redirect_is_write_shaped():
"""fable-5 review: `python3 gen.py>out.txt` is ONE shlex token; the redirect
shape must be recognized mid-token, while located inline-code bodies keep
their '>' comparisons/filehandles as reads."""
assert interpreter_write_shape("python3 gen.py>out.txt") is True
assert interpreter_write_shape("python3 gen.py>>log.txt") is True
assert interpreter_write_shape(["python3", "-c", "print(1 if a > b else 2)"]) is False
assert interpreter_write_shape(["python3", "-c", "x = {'k': 'v => w'}; print(x)"]) is False
def test_prose_words_are_not_write_shapes_for_interpreters():
"""Natural-language words in code text ('scp done', 'count deleted rows') are
not write evidence; structural spellings (os.remove, rm in a compound) are."""
assert interpreter_write_shape(["python3", "-c", "print(open('/tmp/f').read()); print('scp done')"]) is False
assert interpreter_write_shape(["python3", "-c", "print('count deleted rows'); print(open('/tmp/f').read())"]) is False
assert interpreter_write_shape(["python3", "-c", "print('results truncated')"]) is False
assert interpreter_write_shape(["python3", "-c", "import os; os.remove('/tmp/x')"]) is True
assert interpreter_write_shape(["python3", "-c", "f.truncate(0)"]) is True
# --- guard layer: workspace lanes ------------------------------------------
def test_external_pure_read_outside_runtime_is_allowed(tmp_path):
"""The census class (rows 1-5): a read-only interpreter hash/inspect over
host scratch was refused as 'write-like'; it is a plain allowed read."""
reg = _registry(tmp_path, mode="external")
scratch = tmp_path / "scratch"
scratch.mkdir()
target = scratch / "artifact.bin"
target.write_bytes(b"payload")
cmd = ["python3", "-c", READ_ONLY_HASH_SCRIPT.format(target=str(target))]
assert _shell_guard_text(reg, {"cmd": cmd, "cwd": str(tmp_path / "workspace")}, "advanced") is None
def test_workspace_mode_pure_read_outside_root_is_allowed(tmp_path):
reg = _registry(tmp_path, mode="workspace")
scratch = tmp_path / "scratch"
scratch.mkdir()
target = scratch / "report.txt"
target.write_text("data", encoding="utf-8")
cmd = ["python3", "-c", f"print(open({str(target)!r}, 'r').read())"]
assert _shell_guard_text(reg, {"cmd": cmd, "cwd": str(tmp_path / "workspace")}, "advanced") is None
def test_external_sh_wrapped_pure_read_is_allowed(tmp_path):
reg = _registry(tmp_path, mode="external")
scratch = tmp_path / "scratch"
scratch.mkdir()
target = scratch / "blob.bin"
target.write_bytes(b"x" * 16)
inner = f"python3 -c \"print(open({str(target)!r}, 'rb').read(8))\""
assert _shell_guard_text(reg, {"cmd": ["sh", "-c", inner], "cwd": str(tmp_path / "workspace")}, "advanced") is None
def test_external_pure_read_of_runtime_still_blocked_via_read_guard(tmp_path):
"""The owner contract stands: external shell may not READ the runtime/data
drive — but the block now comes from the honest read guard, which names
the gated read_file route instead of calling a read 'write-like'."""
reg = _registry(tmp_path, mode="external")
data = tmp_path / "data"
(data / "settings.json").write_text("{}", encoding="utf-8")
cmd = [
"python3",
"-c",
READ_ONLY_HASH_SCRIPT.format(target=str(data / "settings.json")),
]
out = _shell_guard_text(reg, {"cmd": cmd, "cwd": str(tmp_path / "workspace")}, "advanced") or ""
assert "WORKSPACE_SHELL_BLOCKED" in out
assert "read_file" in out
assert "write-like" not in out
def test_external_write_mode_open_to_runtime_still_blocked(tmp_path):
reg = _registry(tmp_path, mode="external")
data = tmp_path / "data"
cmd = ["python3", "-c", f"open({str(data / 'x')!r}, 'w').write('hi')"]
out = _shell_guard_text(reg, {"cmd": cmd, "cwd": str(tmp_path / "workspace")}, "advanced") or ""
assert "WORKSPACE_SHELL_BLOCKED" in out
# A bare write-mode open with NO .write( chain (truncation alone) as well.
bare = ["python3", "-c", f"open({str(data / 'x')!r}, 'w')"]
out2 = _shell_guard_text(reg, {"cmd": bare, "cwd": str(tmp_path / "workspace")}, "advanced") or ""
assert "WORKSPACE_SHELL_BLOCKED" in out2
def test_external_ruby_pure_read_allowed_and_ruby_write_blocked(tmp_path):
reg = _registry(tmp_path, mode="external")
scratch = tmp_path / "scratch"
scratch.mkdir()
target = scratch / "f.txt"
target.write_text("data", encoding="utf-8")
read_cmd = ["ruby", "-e", f"puts File.read({str(target)!r})"]
assert _shell_guard_text(reg, {"cmd": read_cmd, "cwd": str(tmp_path / "workspace")}, "advanced") is None
data = tmp_path / "data"
write_cmd = ["ruby", "-e", f"File.write({str(data / 'x')!r}, 'y')"]
out = _shell_guard_text(reg, {"cmd": write_cmd, "cwd": str(tmp_path / "workspace")}, "advanced") or ""
assert "WORKSPACE_SHELL_BLOCKED" in out
def test_external_pathlib_write_open_to_runtime_still_blocked(tmp_path):
reg = _registry(tmp_path, mode="external")
data = tmp_path / "data"
cmd = [
"python3",
"-c",
f"from pathlib import Path; Path({str(data / 'x')!r}).open('w')",
]
out = _shell_guard_text(reg, {"cmd": cmd, "cwd": str(tmp_path / "workspace")}, "advanced") or ""
assert "WORKSPACE_SHELL_BLOCKED" in out
def test_external_opaque_subprocess_naming_runtime_still_blocked(tmp_path):
reg = _registry(tmp_path, mode="external")
data = tmp_path / "data"
cmd = [
"python3",
"-c",
f"import subprocess; subprocess.run(['rm', '-rf', {str(data / 'x')!r}])",
]
out = _shell_guard_text(reg, {"cmd": cmd, "cwd": str(tmp_path / "workspace")}, "advanced") or ""
assert "WORKSPACE_SHELL_BLOCKED" in out
def test_pure_filter_reads_outside_root_are_allowed(tmp_path):
"""Scope-C: sort/uniq/sed -n/tar -tf/gzip -l READ invocations must not be
'write-like' — membership alone is not a write channel."""
reg = _registry(tmp_path, mode="external")
scratch = tmp_path / "scratch"
scratch.mkdir()
data_file = scratch / "data.csv"
data_file.write_text("b\na\n", encoding="utf-8")
archive = scratch / "a.tar"
archive.write_bytes(b"x" * 16)
for cmd in (
["sort", str(data_file)],
["uniq", str(data_file)],
["sed", "-n", "1p", str(data_file)],
["tar", "-tf", str(archive)],
["gzip", "-l", str(archive)],
):
out = _shell_guard_text(reg, {"cmd": cmd, "cwd": str(tmp_path / "workspace")}, "advanced")
assert out is None, (cmd, out)
def test_sed_script_write_channels_stay_write_shaped(tmp_path):
"""fable-5 round-2: sed writes WITHOUT -i too — the POSIX in-script `w FILE`
command, the `s///w` flag, GNU `s///e` execute, a -f script file (unprovable),
and the GNU attached `-ibak` suffix. All must keep writer targets; plain
filters and patterns containing prose words stay reads."""
from ouroboros.tools.shell_guards import writer_target_tokens
for cmd in (
["sed", "w out.py", "f"],
["sed", "-n", "s/a/b/w out.txt", "f"],
["sed", "s/x/y/e", "f"],
["sed", "-f", "script.sed", "f"],
["sed", "-e", "w dump.txt", "f"],
["sed", "-ibak", "s/x/y/", "f"],
):
assert writer_target_tokens(cmd), cmd
for cmd in (
["sed", "-n", "1,40p", "f"],
["sed", "-n", "/delete/p", "f"],
["sed", "s/hello/world/g", "f"],
):
assert writer_target_tokens(cmd) == [], cmd
# e2e: the in-script write to a runtime path is refused; the same shape
# reading host scratch passes.
reg = _registry(tmp_path, mode="external")
data = tmp_path / "data"
out = _shell_guard_text(reg,
{"cmd": ["sed", f"w {data / 'x'}", "/etc/hostname"], "cwd": str(tmp_path / "workspace")},
"advanced",
) or ""
assert "WORKSPACE_SHELL_BLOCKED" in out
scratch = tmp_path / "scratch"
scratch.mkdir()
target = scratch / "f.txt"
target.write_text("x", encoding="utf-8")
assert _shell_guard_text(reg,
{"cmd": ["sed", "-n", "/delete/p", str(target)], "cwd": str(tmp_path / "workspace")},
"advanced",
) is None
def test_sol_r2_channel_grammar_writes_stay_write_shaped():
"""sol-max round-2: option/operand grammar gaps — every one of these is a
real write and must report a target (or write shape)."""
from ouroboros.tools.shell_guards import writer_target_tokens
for cmd in (
["sort", "--output", "/o", "/etc/hosts"],
["uniq", "-", "/o"],
["sed", "-nibak", "s/a/b/", "/f"],
["sed", "-n", "1w /o", "/etc/hosts"],
["tar", "-cf/o.tar", "/etc/hosts"],
["tar", "--extract", "--file=/i.tar", "--directory=/od"],
["tar", "xf", "/a.tar"],
["gzip", "-S.tgz", "/f"],
):
assert writer_target_tokens(cmd), cmd
# Old-style/list/read spellings stay reads.
for cmd in (
["tar", "tf", "/a.tar"],
["tar", "-tf", "/a.tar"],
["gzip", "-l", "/a.gz"],
["sed", "s/e/x/", "/f"],
["sed", "-n", "/e/p", "/f"],
):
assert writer_target_tokens(cmd) == [], cmd
def test_sol_r2_compound_and_carve_holes_closed(tmp_path):
"""sol-max round-2: a pure-filter HEAD speaks only for its own segment, and
uniq's '-' stdin operand cannot hide its output operand from the carve."""
from ouroboros.tools.registry import _is_pure_read_inspection
from ouroboros.tools.write_shape import non_interpreter_write_shape
assert (
non_interpreter_write_shape(
"sort /etc/hosts && find /d -name x -delete",
["sort"], "sort", is_pure_read=_is_pure_read_inspection,
)
is True
)
assert _is_pure_read_inspection("printf x | uniq - data/settings.json") is False
assert _is_pure_read_inspection("uniq /var/log/a.txt") is True
def test_inline_body_isolation_for_joined_and_wrapped_forms():
"""sol-max round-2: a '>' comparison inside a located body is not a redirect
even when the body rides a joined flag or an sh -c wrap; real redirects
outside the body still classify."""
assert interpreter_write_shape(["python3", "-cprint(2 > 1)"]) is False
assert (
interpreter_write_shape(
["sh", "-c", "python3 -c 'print(2 > 1); print(open(\"/etc/hosts\").read())'"]
)
is False
)
assert interpreter_write_shape(["sh", "-c", "python3 gen.py > out.txt"]) is True
def test_pure_filter_write_channels_still_blocked(tmp_path):
"""The real channels stay writes: sort -o, sed -i, uniq's second operand,
tar extract, gzip default all still take guard B."""
reg = _registry(tmp_path, mode="external", acting=True)
scratch = tmp_path / "scratch"
scratch.mkdir()
src = scratch / "in.txt"
src.write_text("x\n", encoding="utf-8")
for cmd in (
["sort", "-o", str(scratch / "out.txt"), str(src)],
["sed", "-i", "s/a/b/", str(src)],
["uniq", str(src), str(scratch / "out.txt")],
["tar", "-xf", str(scratch / "a.tar"), "-C", str(scratch)],
["gzip", str(src)],
):
out = _shell_guard_text(reg, {"cmd": cmd, "cwd": str(tmp_path / "workspace")}, "advanced") or ""
assert "WORKSPACE_SHELL_BLOCKED" in out, (cmd, out)
def test_workspace_write_block_message_names_path_and_route(tmp_path):
"""The five formerly byte-identical guard-B messages carry the resolved
offending path (the light-lane message is the exemplar)."""
reg = _registry(tmp_path, mode="external")
data = tmp_path / "data"
cmd = ["python3", "-c", f"open({str(data / 'x')!r}, 'w').write('hi')"]
out = _shell_guard_text(reg, {"cmd": cmd, "cwd": str(tmp_path / "workspace")}, "advanced") or ""
assert "Blocked path:" in out
assert "read_file" in out
def test_outside_root_write_block_message_names_path_and_root(tmp_path):
"""The outside-process-root variant names the blocked path and the selected
process root, so the agent can self-correct instead of guessing."""
reg = _registry(tmp_path, mode="external", acting=True)
scratch = tmp_path / "scratch"
scratch.mkdir()
cmd = ["python3", "-c", f"open({str(scratch / 'out.txt')!r}, 'w').write('hi')"]
out = _shell_guard_text(reg, {"cmd": cmd, "cwd": str(tmp_path / "workspace")}, "advanced") or ""
assert "WORKSPACE_SHELL_BLOCKED" in out
assert "outside the selected process root" in out
assert "Blocked path:" in out
assert "Selected process root:" in out
@pytest.mark.parametrize(
"cmd",
(
["/usr/bin/python3", "-c", 'import os; os.write(1, b"x")'],
["/opt/homebrew/bin/node", "-e", "console.log(1)"],
),
ids=("python-os-write-fd", "node-console-log"),
)
def test_unprovable_row_never_promotes_absolute_executable_to_write(tmp_path, cmd):
from ouroboros.tools.registry import _workspace_write_candidates
forced_uncertain_row = [(cmd, [], (cmd[-1],), True)]
assert not any(
token == cmd[0] and is_write
for token, is_write, _row in _workspace_write_candidates(forced_uncertain_row, [], cmd)
)
reg = _registry(tmp_path, mode="external")
assert _shell_guard_text(reg,
{"cmd": cmd, "cwd": str(tmp_path / "workspace")}, "advanced"
) is None
def test_uncertain_python_body_naming_outside_path_stays_blocked(tmp_path):
reg = _registry(tmp_path, mode="external", acting=True)
cmd = [
"/usr/bin/python3", "-c",
'import subprocess; subprocess.run(["rm", "/Users/Shared/x"])',
]
out = _shell_guard_text(reg,
{"cmd": cmd, "cwd": str(tmp_path / "workspace")}, "advanced"
) or ""
assert "outside the selected process root" in out
assert "/Users/Shared/x" in out
def test_os_write_through_literal_os_open_outside_stays_blocked(tmp_path):
from ouroboros.tools.shell_guards import _python_write_targets_and_unknown
reg = _registry(tmp_path, mode="external", acting=True)
code = (
'import os; fd=os.open("/Users/Shared/out", os.O_WRONLY|os.O_CREAT); '
'os.write(fd, b"x")'
)
targets, unknown = _python_write_targets_and_unknown(code)
assert targets == ["/Users/Shared/out"] and unknown is False
out = _shell_guard_text(reg,
{"cmd": ["/usr/bin/python3", "-c", code], "cwd": str(tmp_path / "workspace")},
"advanced",
) or ""
assert "outside the selected process root" in out
assert "/Users/Shared/out" in out
def test_split_redirections_grammar():
"""One redirect grammar: both the glued and the split spellings, reads and
descriptor duplication yield no target, and an operator away from a token's
start is left alone."""
from ouroboros.shell_parse import shell_argv, split_redirections
assert split_redirections(shell_argv("cp a b 2>/dev/null")) == (["cp", "a", "b"], [])
assert split_redirections(shell_argv("cp x y >> log.txt")) == (["cp", "x", "y"], ["log.txt"])
assert split_redirections(shell_argv("node t.js > out.log 2>&1")) == (
["node", "t.js"],
["out.log"],
)
assert split_redirections(shell_argv("printf x >&2")) == (["printf", "x"], [])
assert split_redirections(shell_argv("tee out.txt < in.txt")) == (["tee", "out.txt"], [])
assert split_redirections(shell_argv("cat <<'EOF' > out.txt")) == (["cat"], ["out.txt"])
# The split spelling the operator-aware lexer emits: a bare descriptor token
# belongs to the operator that follows it.
assert split_redirections(["cp", "a", "b", "2", ">", "/dev/null"]) == (["cp", "a", "b"], [])
assert split_redirections(shell_argv("echo hi >|out")) == (["echo", "hi"], ["out"])
assert split_redirections(shell_argv("echo hi >& /outside/log")) == (["echo", "hi"], ["/outside/log"])
assert split_redirections(shell_argv("echo hi >1")) == (["echo", "hi"], ["1"])
assert split_redirections(shell_argv("echo hi >2")) == (["echo", "hi"], ["2"])
assert split_redirections(shell_argv("echo hi >&word")) == (["echo", "hi"], ["word"])
for descriptor_redirect in ("2>&1", ">&-", ">&2"):
assert split_redirections(shell_argv(f"echo hi {descriptor_redirect}")) == (["echo", "hi"], [])
# A `>`/`<` away from position 0 is a literal byte, not an operator.
assert split_redirections(["sed", "s/>/x/", "f"]) == (["sed", "s/>/x/", "f"], [])
assert split_redirections(["git", "log", "--pretty=<x>"]) == (
["git", "log", "--pretty=<x>"],
[],
)
def test_writer_targets_recover_the_destination_behind_a_redirect():
"""The writer-target lane reads the operator-aware view, so a redirect no
longer displaces the command's own destination."""
from ouroboros.tools.shell_guards import writer_target_rows, writer_target_tokens
assert writer_target_tokens(["cp", "x", "y", ">>", "log.txt"]) == ["y", "log.txt"]
# A row is (segment_argv, targets, inline_code, unprovable); a `cd` operand
# takes the target policy because it is how a later relative write escapes.
assert writer_target_rows("cd . && cp src.txt /D/.env") == [
(["cd", "."], ["."], (), False),
(["cp", "src.txt", "/D/.env"], ["/D/.env"], (), False),
]
def test_workspace_rows_filter_bodies_without_weakening_the_light_fence():
from ouroboros.tools.shell_guards import writer_target_rows, writer_target_tokens
body = "print '/outside/mentioned.txt'"
# The light fence keeps the historical unfiltered signal. The workspace
# lane removes the body from its path targets; its mode-aware classifier
# proves this body read-only, so it carries no row uncertainty.
assert writer_target_tokens(["perl", "-e", body]) == [body]
assert writer_target_rows(["perl", "-e", body]) == [
(["perl", "-e", body], [], (body,), False),
]
def test_cp_source_outside_root_is_a_read_and_destination_still_blocked(tmp_path):
"""A writer's SOURCE operand is a read: copying an outside file INTO the
process root is the sanctioned transfer route, while an outside DESTINATION
stays refused."""
reg = _registry(tmp_path, mode="external", acting=True)
workspace = str(tmp_path / "workspace")
outside = tmp_path / "outside"
outside.mkdir()
def check(cmd):
return _shell_guard_text(reg, {"cmd": cmd, "cwd": workspace}, "advanced")
assert check(["cp", str(outside / "widget.js"), "widget.js"]) is None
assert check(["ln", "-s", str(outside / "src"), "link.js"]) is None
assert check(["cat", str(outside / "widget.js")]) is None
destination_block = check(["cp", "widget.js", str(outside / "copy.js")]) or ""
assert "outside the selected process root" in destination_block
assert str(outside / "copy.js") in destination_block
def test_relative_protected_root_source_still_blocked(tmp_path):
"""A protected runtime path refuses on MENTION for every candidate, so a
writer naming it as a SOURCE still cannot launder a read through shell."""
reg = _registry(tmp_path, mode="external")
workspace = str(tmp_path / "workspace")
(tmp_path / "data" / "settings.json").write_text("{}", encoding="utf-8")
(tmp_path / "system" / "ouroboros").mkdir(parents=True, exist_ok=True)
(tmp_path / "system" / "ouroboros" / "safety.py").write_text("x = 1\n", encoding="utf-8")
for cmd, blocked in (
(["cp", "../data/settings.json", "./x"], tmp_path / "data" / "settings.json"),
(
["cp", "../system/ouroboros/safety.py", "./x"],
tmp_path / "system" / "ouroboros" / "safety.py",
),
):
out = _shell_guard_text(reg, {"cmd": cmd, "cwd": workspace}, "advanced") or ""
assert "mentions Ouroboros system/data paths" in out, cmd
# The blocked path is the FILE, i.e. the per-candidate containment branch
# rather than the whole-command text scan.
assert f"Blocked path: {blocked}" in out, cmd
def test_glued_operator_is_not_a_path_candidate(tmp_path):
"""A redirection glued to the following separator is not a path: `2>/dev/null;`
was forged into the target `/dev/null;` and refused a pure read."""
reg = _registry(tmp_path, mode="external", acting=True)
workspace = str(tmp_path / "workspace")
outside = tmp_path / "outside"
outside.mkdir()
def check(cmd):
return _shell_guard_text(reg, {"cmd": cmd, "cwd": workspace}, "advanced")
assert check(["sh", "-c", "git reset HEAD scratch/ 2>/dev/null; rm -rf scratch/"]) is None
assert check(["sh", "-c", "node build.js 2>/dev/null; echo ok"]) is None
redirect_block = check(["sh", "-c", f"node t.js > {outside / 'out.log'} 2>&1"]) or ""
assert "outside the selected process root" in redirect_block
assert str(outside / "out.log") in redirect_block
@pytest.mark.parametrize(
"body",
("(cd sub && make) 2>&1", "echo $(date) 2>/dev/null"),
ids=("subshell-stderr-dup", "command-substitution-dev-null"),
)
def test_round5_redirect_only_segments_are_allowed_without_crash(tmp_path, body):
reg = _registry(tmp_path, mode="external")
workspace = tmp_path / "workspace"
(workspace / "sub").mkdir()
assert _shell_guard_text(reg,
{"cmd": ["sh", "-c", body], "cwd": str(workspace)}, "advanced"
) is None
def test_inline_code_segment_keeps_the_mention_scan(tmp_path):
"""An interpreter body contributes its EXTRACTED write targets as writes and
everything else as a mention: regex punctuation stops being a forged path, an
extracted outside write target still refuses, and a protected runtime mention
inside the body still refuses.
DISCLOSED FLIP: an outside path merely READ by an in-root writer no longer
refuses — the same class as a cp source operand."""
reg = _registry(tmp_path, mode="external", acting=True)
workspace = str(tmp_path / "workspace")
outside = tmp_path / "outside"
outside.mkdir()
protected = tmp_path / "data" / "settings.json"
protected.write_text("{}", encoding="utf-8")
def check(code):
return _shell_guard_text(reg,
{"cmd": ["python3", "-c", code], "cwd": workspace}, "advanced"
)
# (a) regex punctuation harvested out of the body is not a path
assert check("import re; re.sub('/[^/]+$','',''); open('x','w')") is None
# (b) an extracted write target outside the root still refuses
outside_write = check(f"open({str(outside / 'ok')!r},'w')") or ""
assert "outside the selected process root" in outside_write
assert str(outside / "ok") in outside_write
# (c) a protected runtime path mentioned by an in-root writer still refuses
protected_read = check(f"open('ok','w'); print(open({str(protected)!r}).read())") or ""
assert "SUBAGENT_SECRET_READ_BLOCKED" in protected_read
# (d) the disclosed flip: an ordinary outside path merely READ is allowed
assert check(f"open('ok','w'); print(open({str(outside / 'y')!r}).read())") is None
def test_sed_in_script_target_survives_the_narrowed_scan(tmp_path):
"""sed's in-script `w FILE` hides the path inside the script operand, so the
parsed targets keep their embedded-path pass."""
reg = _registry(tmp_path, mode="external", acting=True)
workspace = str(tmp_path / "workspace")
scratch = tmp_path / "scratch"
scratch.mkdir()
out = _shell_guard_text(reg,
{"cmd": ["sed", f"w {scratch / 'x'}", "f"], "cwd": workspace}, "advanced"
) or ""
assert "outside the selected process root" in out
assert f"Blocked path: {scratch / 'x'}" in out
def _outside_write_result(tmp_path, cmd):
reg = _registry(tmp_path, mode="external", acting=True)
workspace = str(tmp_path / "workspace")
outside = tmp_path / "outside"
outside.mkdir(exist_ok=True)
rendered = cmd(outside)
return rendered, _shell_guard_text(reg,
{"cmd": rendered, "cwd": workspace}, "advanced"
) or ""
def test_nested_shell_write_outside_workspace_stays_blocked(tmp_path):
cmd, out = _outside_write_result(
tmp_path,
lambda outside: ["sh", "-c", f"sh -c 'echo x > {outside / 'nested.txt'}'"],
)
assert "WORKSPACE_SHELL_BLOCKED" in out, cmd
def test_find_exec_rm_outside_workspace_stays_blocked(tmp_path):
cmd, out = _outside_write_result(
tmp_path,
lambda outside: ["find", str(outside), "-exec", "rm", "{}", ";"],
)
assert "WORKSPACE_SHELL_BLOCKED" in out, cmd
def test_python_shutil_copy_outside_workspace_stays_blocked(tmp_path):
cmd, out = _outside_write_result(
tmp_path,
lambda outside: [
"python3", "-c", f"import shutil; shutil.copy('a','{outside / 'copy.txt'}')",
],
)
assert "WORKSPACE_SHELL_BLOCKED" in out, cmd
def test_f1_find_exec_tee_placeholder_stays_blocked(tmp_path):
cmd, out = _outside_write_result(
tmp_path,
lambda outside: f"find {outside} -exec tee {{}} \\;",
)
assert "WORKSPACE_SHELL_BLOCKED" in out, cmd
def test_f2_python_unknown_widens_even_with_recovered_target(tmp_path):
cmd, out = _outside_write_result(
tmp_path,
lambda outside: [
"python3", "-c",
f'import subprocess; open("inside","w"); subprocess.run(["rm","{outside / "py-mixed"}"])',
],
)
assert "WORKSPACE_SHELL_BLOCKED" in out, cmd
def test_f3_env_chdir_is_effective_cwd_for_wrapped_write(tmp_path):
for option in ("-C", "--chdir"):
cmd, out = _outside_write_result(
tmp_path,
lambda outside: [
"env", option, str(outside), "python3", "-c", 'open("relative","w")',
],
)
assert "WORKSPACE_SHELL_BLOCKED" in out, cmd
cmd, out = _outside_write_result(
tmp_path,
lambda outside: [
"env", f"--chdir={outside}", "python3", "-c", 'open("relative","w")',
],
)
assert "WORKSPACE_SHELL_BLOCKED" in out, cmd
def test_f4_perl_body_uncertainty_ignores_unrelated_operand(tmp_path):
cmd, out = _outside_write_result(
tmp_path,
lambda outside: [
"perl", "-e", f'open(F, ">", "{outside / "perl-out"}"); print F "x"',
"input.txt",
],
)
assert "WORKSPACE_SHELL_BLOCKED" in out, cmd
def test_f5_ruby_fileutils_move_uses_destination(tmp_path):
cmd, out = _outside_write_result(
tmp_path,
lambda outside: [
"ruby", "-e",
f'require "fileutils"; FileUtils.mv("inside", "{outside / "ruby-move"}")',
],
)
assert "WORKSPACE_SHELL_BLOCKED" in out, cmd
def test_f6_node_opaque_exec_widens_with_recovered_target(tmp_path):
cmd, out = _outside_write_result(
tmp_path,
lambda outside: [
"node", "-e",
f'require("fs").writeFileSync("inside","x"); '
f'require("child_process").execSync("rm {outside / "node-mixed"}")',
],
)
assert "WORKSPACE_SHELL_BLOCKED" in out, cmd
def test_f7_python_literal_heredoc_read_stays_allowed(tmp_path):
reg = _registry(tmp_path, mode="external")
outside = tmp_path / "outside"
outside.mkdir()
command = f"python3 - <<'EOF'\nprint(open(\"{outside / 'read'}\").read())\nEOF"
assert _shell_guard_text(reg,
{"cmd": command, "cwd": str(tmp_path / "workspace")}, "advanced"
) is None
def test_round5_sequential_effective_cwd_blocks_nested_escape(tmp_path):
reg = _registry(tmp_path, mode="external", acting=True)
workspace = tmp_path / "workspace"
fixtures = workspace / "fixtures"
fixtures.mkdir()
for body in (
"cd .. && echo x > ../outside",
"cd .. && touch ../docs/X",
"pushd .. && echo x > ../outside",
"env -C .. sh -c 'echo x > ../outside'",
):
out = _shell_guard_text(reg,
{"cmd": ["sh", "-c", body], "cwd": str(fixtures)}, "advanced"
) or ""
assert "WORKSPACE_SHELL_BLOCKED" in out, (body, out)
def test_round5_sequential_effective_cwd_keeps_in_workspace_write_allowed(tmp_path):
reg = _registry(tmp_path, mode="external")
workspace = tmp_path / "workspace"
(workspace / "sub").mkdir()
(workspace / "tools").mkdir()
for body in ("cd sub && echo x > f", "cd sub && touch ../file", "cd tools && touch ../shell_parse.py"):
args = {"cmd": ["sh", "-c", body], "cwd": str(workspace)}
assert _shell_guard_text(reg, args, "advanced") is None, body
def test_round6_redirect_file_targets_outside_workspace_are_blocked(tmp_path):
reg = _registry(tmp_path, mode="external", acting=True)
workspace = tmp_path / "workspace"
outside = tmp_path / "outside"
outside.mkdir()
(workspace / "1").symlink_to(outside / "numeric-log")
for body in (f"echo x >& {outside / 'redirect-log'}", "echo x >1"):
args = {"cmd": ["sh", "-c", body], "cwd": str(workspace)}
out = _shell_guard_text(reg, args, "advanced") or ""
assert "WORKSPACE_SHELL_BLOCKED" in out, (body, out)
@pytest.mark.parametrize(
"command",
(
"python3 <<'EOF'\nopen({outside!r}, 'w')\nEOF",
"sh <<'EOF'\necho x > {outside}\nEOF",
"node <<'EOF'\nrequire('fs').writeFileSync({outside!r}, 'x')\nEOF",
),
ids=("python-no-dash", "sh-stdin", "node-stdin"),
)
def test_round5_stdin_heredoc_writes_outside_are_blocked(tmp_path, command):
reg = _registry(tmp_path, mode="external", acting=True)
outside = tmp_path / "outside"
outside.mkdir()
target = outside / "heredoc-write"
rendered = command.format(outside=str(target))
out = _shell_guard_text(reg,
{"cmd": rendered, "cwd": str(tmp_path / "workspace")}, "advanced"
) or ""
assert "WORKSPACE_SHELL_BLOCKED" in out, (rendered, out)
def test_round5_python_stdin_heredoc_read_without_dash_is_allowed(tmp_path):
reg = _registry(tmp_path, mode="external")
outside = tmp_path / "outside"
outside.mkdir()
command = f"python3 <<'EOF'\nprint(open({str(outside / 'read')!r}).read())\nEOF"
assert _shell_guard_text(reg,
{"cmd": command, "cwd": str(tmp_path / "workspace")}, "advanced"
) is None
@pytest.mark.parametrize("write", (False, True), ids=("read-allowed", "write-blocked"))
def test_round6_piped_python_heredoc_write_policy(tmp_path, write):
reg = _registry(tmp_path, mode="external", acting=True)
outside = tmp_path / "outside"
outside.mkdir()
target = outside / ("write" if write else "read")
statement = f"open({str(target)!r}, 'w')" if write else f"print(open({str(target)!r}).read())"
command = f"python3 <<'EOF' | cat\n{statement}\nEOF"
out = _shell_guard_text(reg,
{"cmd": command, "cwd": str(tmp_path / "workspace")}, "advanced"
) or ""
assert ("WORKSPACE_SHELL_BLOCKED" in out) is write, out
@pytest.mark.parametrize(
"argv",
(
["node", "-e", "require('fs').writeFileSync('inside','x'); require('fs').writeFile('/outside','x',()=>{})"],
["ruby", "-e", "File.write('inside','x'); File.delete('/outside')"],
),
ids=("node-async-write", "ruby-file-delete"),
)
def test_round5_mixed_literal_writer_targets_are_all_modelled(tmp_path, argv):
reg = _registry(tmp_path, mode="external", acting=True)
out = _shell_guard_text(reg,
{"cmd": argv, "cwd": str(tmp_path / "workspace")}, "advanced"
) or ""
assert "WORKSPACE_SHELL_BLOCKED" in out, (argv, out)
def test_f8_uncertain_perl_row_does_not_widen_independent_cat(tmp_path):
reg = _registry(tmp_path, mode="external")
outside = tmp_path / "outside"
outside.mkdir()
command = f"perl -e 'print 1' && cat {outside / 'read'}"
assert _shell_guard_text(reg,
{"cmd": command, "cwd": str(tmp_path / "workspace")}, "advanced"
) is None
def test_round3_old_block_coverage_stays_blocked(tmp_path):
import shlex
reg = _registry(tmp_path, mode="external", acting=True)
workspace = str(tmp_path / "workspace")
outside = tmp_path / "outside"
outside.mkdir()
def nested_shell(depth):
body = f"cp x {outside / 'nested'}"
for _ in range(depth - 1):
body = f"sh -c {shlex.quote(body)}"
return ["sh", "-c", body]
commands = (
("eval_quoting_layers", ["sh", "-c", f'eval "cp x {outside / "eval"}"']),
("nested_depth_3", nested_shell(3)),
("nested_depth_4_fallback", nested_shell(4)),
("timeout_shell", ["timeout", "5", "sh", "-c", f"cp x {outside / 'timeout'}"]),
("nohup_shell", ["nohup", "bash", "-c", f"echo x > {outside / 'nohup'}"]),
("xargs_visible_producer", f"printf {outside / 'xargs'} | xargs -I{{}} cp x {{}}"),
("xargs_custom_placeholder", f"printf {outside / 'xargs-custom'} | xargs -I@ cp x @"),
("cd_relative_write", f"cd {outside} && echo x > rel"),
("pushd_relative_write", f"pushd {outside} && echo x > rel"),
(
"env_python_shutil_move",
["env", "FOO=1", "python3", "-c", f'import shutil; shutil.move("x","{outside / "py-move"}")'],
),
(
"python_heredoc_write",
f"python3 - <<'EOF'\nopen(\"{outside / 'py-heredoc'}\",\"w\")\nEOF",
),
("perl_open_write", ["perl", "-e", f'open(F, ">", "{outside / "perl"}")']),
("ruby_file_write", ["ruby", "-e", f'File.write("{outside / "ruby"}", "x")']),
(
"node_write_file_sync",
["node", "-e", f'require("fs").writeFileSync("{outside / "node"}","x")'],
),
("awk_redirect", f"awk '{{print $1}}' input > {outside / 'awk'}"),
("rsync_destination", ["rsync", "src", str(outside / "rsync")]),
("tar_chdir_extract", ["tar", "-C", str(outside), "-xf", "a.tar"]),
("append_redirect", f"echo x >> {outside / 'append'}"),
("stderr_redirect", f"awk '{{print $1}}' input 2> {outside / 'stderr'}"),
("combined_redirect", f"awk '{{print $1}}' input &> {outside / 'combined'}"),
("windows_drive", ["cp", "x", r"C:\outside\drive.txt"]),
("windows_unc", ["cp", "x", r"\\server\share\unc.txt"]),
)
for name, command in commands:
out = _shell_guard_text(reg, {"cmd": command, "cwd": workspace}, "advanced") or ""
assert "WORKSPACE_SHELL_BLOCKED" in out, (name, command, out)
def test_round3_body_uncertainty_variants_are_row_scoped():
from ouroboros.tools.shell_guards import writer_target_rows
find_row = writer_target_rows(["find", "/tree", "-exec", "tee", "copy-{}", ";"])[0]
assert find_row[1] == []
assert find_row[3] is True
for method in ("exec", "spawn", "execSync", "spawnSync"):
row = writer_target_rows(
["node", "-e", f'child_process.{method}("rm /outside")']
)[0]
assert row[3] is True, method
ruby_row = writer_target_rows(
["ruby", "-e", 'FileUtils.mv("/outside/source", destination)']
)[0]
assert ruby_row[3] is True
def test_round3_ordinary_outside_reads_stay_allowed(tmp_path):
reg = _registry(tmp_path, mode="external")
workspace = str(tmp_path / "workspace")
outside = tmp_path / "outside"
outside.mkdir()
commands = (
("grep", ["grep", "x", str(outside / "read")]),
("rg", ["rg", "x", str(outside / "read")]),
("cat", ["cat", str(outside / "read")]),
("ls", ["ls", str(outside)]),
("git_status", ["git", "-C", str(outside), "status"]),
("pytest", ["pytest", str(outside / "test.py")]),
(
"python_open_read",
["python3", "-c", f'print(open("{outside / "read"}").read())'],
),
(
"python_heredoc_read",
f"python3 - <<'EOF'\nprint(open(\"{outside / 'read'}\").read())\nEOF",
),
("mixed_perl_cat", f"perl -e 'print 1' && cat {outside / 'read'}"),
(
"perl_open_read",
["perl", "-e", f'open(my $fh, "<", "{outside / "read"}"); print <$fh>'],
),
)
for name, command in commands:
out = _shell_guard_text(reg, {"cmd": command, "cwd": workspace}, "advanced")
assert out is None, (name, command, out)
# --- Windows spellings: the first windows-latest execution of this serial suite ---
#
# Five failures, two root causes, neither in tokenization (shlex posix mode strips
# only UNQUOTED backslashes, and the raw-text harvest lane already recovers such a
# spelling): (1) a Windows path typed into a plain Python literal ("C:\Users\x") is
# not a valid Python string (\U opens a unicode escape), so the AST lane reported
# UNKNOWN and the row fail-closed into an outside-root WRITE for a pure read;
# (2) the block named only the natively RESOLVED path (C:\Users\Shared\x for a
# command that wrote /Users/Shared/x). The tests below push the exact Windows
# spellings through the host-independent seams. The one branch that cannot run
# here — pathlib resolving a drive path natively (`os.name == "nt"` in
# _workspace_shell_write_block) — is exercised through the message builder with
# PureWindowsPath inputs: PosixPath cannot resolve a drive path, and patching
# os.name breaks pathlib's flavour selection rather than simulating Windows.
WINDOWS_TMP = r"C:\Users\runneradmin\AppData\Local\Temp\pytest-of-runneradmin\pytest-0"
def test_windows_spelled_python_literals_are_read_verbatim():
from ouroboros.tools.shell_guards import _python_write_targets_and_unknown
from ouroboros.tools.write_shape import python_body_ast
read = f'print(open("{WINDOWS_TMP}\\outside\\read").read())'
assert python_body_ast(read) is not None
assert _python_write_targets_and_unknown(read) == ([], False)
assert _python_write_targets_and_unknown(
f"print(open('{WINDOWS_TMP}\\scratch\\blob.bin', 'rb').read(8))"
) == ([], False)
# Writes keep their targets in the spelling the model typed, through every
# modelled channel: mode-open, os.open, pathlib join.
out = f"{WINDOWS_TMP}\\outside\\out"
assert _python_write_targets_and_unknown(f'open("{out}", "w").write("x")') == ([out], False)
assert _python_write_targets_and_unknown(
f'import os; fd=os.open("{out}", os.O_WRONLY|os.O_CREAT); os.write(fd, b"x")'
) == ([out], False)
assert _python_write_targets_and_unknown(
f'from pathlib import Path; (Path("{WINDOWS_TMP}") / "outside" / "out").write_text("y")'
) == ([out], False)
# An already-escaped spelling parses first time and keeps the same value.
assert _python_write_targets_and_unknown(f"open({out!r}, 'w')") == ([out], False)
# Uncertainty is untouched: an opaque body stays UNKNOWN, and so does a body
# unparseable for any reason other than its literals (python2 print).
assert _python_write_targets_and_unknown(
f'import subprocess; subprocess.run(["rm", "{out}"])'
)[1] is True
assert _python_write_targets_and_unknown(f'print "{out}"') == ([], True)
def test_verbatim_string_source_respells_only_unescaped_backslashes():
import ast
from ouroboros.tools.write_shape import _verbatim_string_source
for source, value in (
(r'"C:\Users\x"', "C:\\Users\\x"),
(r"b'C:\Users\x'", b"C:\\Users\\x"),
(r'"""C:\Users\x"""', "C:\\Users\\x"),
(r'"a\"b\\c"', 'a"b\\c'), # quote and backslash escapes are kept
(r"'it\'s'", "it's"),
('"\\n"', "\\n"), # verbatim, not a newline
):
assert ast.literal_eval(_verbatim_string_source(source)) == value, source
assert _verbatim_string_source(r'r"C:\Users\x"') == r'r"C:\Users\x"'
assert _verbatim_string_source('x = 1\ny = "C:\\Users\\x"\n') == 'x = 1\ny = "C:\\\\Users\\\\x"\n'
assert _verbatim_string_source('open("unterminated') is None
def test_windows_spelled_pure_reads_stay_allowed_on_every_host(tmp_path):
"""The three windows-latest shapes, spelled exactly as the Windows run spelled
them, allowed on a POSIX host too — the classification is host-independent."""
reg = _registry(tmp_path, mode="external")
workspace = str(tmp_path / "workspace")
read = f"{WINDOWS_TMP}\\outside\\read"
for name, command in (
("python_c", ["python3", "-c", f'print(open("{read}").read())']),
("python_heredoc", f"python3 - <<'EOF'\nprint(open(\"{read}\").read())\nEOF"),
("sh_wrapped_repr", ["sh", "-c", f"python3 -c \"print(open({read!r}, 'rb').read(8))\""]),
):
out = _shell_guard_text(reg, {"cmd": command, "cwd": workspace}, "advanced")
assert out is None, (name, command, out)
def test_windows_spelled_writes_stay_blocked_and_named(tmp_path):
reg = _registry(tmp_path, mode="external", acting=True)
workspace = str(tmp_path / "workspace")
out = f"{WINDOWS_TMP}\\outside\\out"
for command in (
["python3", "-c", f'open("{out}", "w").write("x")'],
["sh", "-c", f"python3 -c \"open({out!r}, 'w').write('x')\""],
f"python3 - <<'EOF'\nopen(\"{out}\", \"w\").write(\"x\")\nEOF",
):
text = _shell_guard_text(reg, {"cmd": command, "cwd": workspace}, "advanced") or ""
assert "outside the selected process root" in text, command
assert f"Blocked path: {out}" in text, command
def test_outside_root_block_names_the_spelling_the_model_used(tmp_path):
"""When the resolved path differs from the operand (a relative spelling, a
symlink alias — or, on Windows, a POSIX-rooted spelling resolved onto the
cwd drive), the block names both, resolved first so earlier pins hold."""
reg = _registry(tmp_path, mode="external", acting=True)
workspace = tmp_path / "workspace"
real = tmp_path / "outside_real"
real.mkdir()
alias = tmp_path / "alias"
alias.symlink_to(real, target_is_directory=True)
for spelled in (f"{alias / 'f'}", "../outside_real/f"):
text = _shell_guard_text(reg,
{"cmd": ["sh", "-c", f"echo x > {spelled}"], "cwd": str(workspace)}, "advanced",
) or ""
assert f"Blocked path: {real / 'f'} (as written: {spelled})." in text, spelled
# Identical spellings are named once.
text = _shell_guard_text(reg,
{"cmd": ["sh", "-c", f"echo x > {real / 'g'}"], "cwd": str(workspace)}, "advanced",
) or ""
assert f"Blocked path: {real / 'g'}." in text
def test_block_messages_carry_a_windows_resolution_beside_the_spelling():
"""The `os.name == "nt"` resolving branch hands the builders a natively
resolved WindowsPath for a POSIX-rooted operand; the message must still
show the operand the model typed."""
from ouroboros.tools.registry_guards import (
_workspace_write_block_outside_root_message,
_workspace_write_block_runtime_message,
)
resolved = pathlib.PureWindowsPath(r"C:\Users\Shared\x")
root = pathlib.PureWindowsPath(r"C:\Users\runneradmin\ws")
text = _workspace_write_block_outside_root_message(resolved, root, spelled="/Users/Shared/x")
assert r"Blocked path: C:\Users\Shared\x (as written: /Users/Shared/x)." in text
assert r"Selected process root: C:\Users\runneradmin\ws." in text
runtime = _workspace_write_block_runtime_message(resolved, spelled=str(resolved))
assert r"Blocked path: C:\Users\Shared\x." in runtime
assert "(as written" not in runtime
assert "Blocked path" not in _workspace_write_block_runtime_message("")