ouroboros/tests/test_tool_api_v2_public_surface.py

1571 lines
64 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import pathlib
import re
import shlex
import sys
import pytest
from ouroboros.tools.registry import ToolRegistry
LEGACY_PUBLIC_TOOL_NAMES = {
"repo_read",
"repo_write",
"repo_list",
"str_replace_editor",
"data_read",
"data_write",
"data_list",
"code_search",
"run_shell",
"git_status",
"git_diff",
"repo_commit",
"restore_to_head",
"revert_commit",
"rollback_to_target",
"schedule_task",
"wait_for_task",
"wait_for_tasks",
"advisory_pre_review",
"review_skill",
"multi_model_review",
}
def test_legacy_tool_names_are_not_public_schemas(tmp_path):
registry = ToolRegistry(repo_dir=tmp_path / "repo", drive_root=tmp_path / "data")
names = {schema["function"]["name"] for schema in registry.schemas()}
assert names.isdisjoint(LEGACY_PUBLIC_TOOL_NAMES)
for name in LEGACY_PUBLIC_TOOL_NAMES:
assert registry.get_schema_by_name(name) is None
assert registry.execute(name, {}).startswith("⚠️ Unknown tool")
# D10: the external coding gateway tool was retired; delegate_start is its
# successor. The dead name must be gone from the public schema surface.
# (Not folded into LEGACY_PUBLIC_TOOL_NAMES: docs/DEVELOPMENT.md keeps the
# D10 retirement record that mentions the old name; the runtime prompts no
# longer do — the prompt audit removed the retirement note from SYSTEM.md.)
assert "claude_code_edit" not in names
assert registry.get_schema_by_name("claude_code_edit") is None
assert registry.execute("claude_code_edit", {}).startswith("⚠️ Unknown tool")
assert {
"read_file",
"write_file",
"search_code",
"run_command",
"delegate_start",
"commit_reviewed",
"schedule_subagent",
"skill_review",
"task_acceptance_review",
} <= names
for tool_name in ("read_file", "list_files", "write_file", "edit_text", "search_code"):
schema = registry.get_schema_by_name(tool_name) or {}
root = (((schema.get("function") or {}).get("parameters") or {}).get("properties") or {}).get("root") or {}
assert "user_files" in set(root.get("enum") or []), tool_name
def test_schedule_subagent_handler_validation_is_derived_from_the_published_schema(tmp_path):
"""The handler's closed keyword set and the model-visible schema must be ONE object.
They used to be two: a hand-written frozenset whose own comment admitted it "mirrors" the
schema. A mirror drifts silently — add a parameter to the schema and the handler refuses it as
"unsupported"; drop one and the handler still accepts it. Derivation makes that
unrepresentable; this test asserts the derivation is still in force, and that adding a
parameter to the schema really does open the handler to it (a copy would fail here)."""
from ouroboros.tools import control
registry = ToolRegistry(repo_dir=tmp_path / "repo", drive_root=tmp_path / "data")
schema = registry.get_schema_by_name("schedule_subagent") or {}
parameters = ((schema.get("function") or {}).get("parameters") or {})
published = set((parameters.get("properties") or {}).keys())
assert published, "schedule_subagent must publish parameters"
assert control.schedule_subagent_param_names() == published
assert parameters.get("additionalProperties") is False
assert set(parameters.get("required") or []) <= published
# Internal scheduling options ride the positional-only `internal` mapping and must stay
# OUT of the published surface (and therefore out of the derived allowed set).
assert published.isdisjoint(control._INTERNAL_SCHEDULE_OPTIONS)
# The schema each caller gets is a fresh object: mutating one must not poison the next.
first = control.schedule_subagent_properties()
first["objective"]["description"] = "mutated"
assert control.schedule_subagent_properties()["objective"]["description"] != "mutated"
def test_list_files_schema_uses_path_not_dir(tmp_path):
registry = ToolRegistry(repo_dir=tmp_path / "repo", drive_root=tmp_path / "data")
schema = registry.get_schema_by_name("list_files") or {}
props = (((schema.get("function") or {}).get("parameters") or {}).get("properties") or {})
assert "path" in props
assert "dir" not in props
assert "TOOL_ARG_ERROR" in registry.execute("list_files", {"dir": "."})
def test_runtime_prompts_do_not_advertise_legacy_public_tool_names():
root = pathlib.Path(__file__).resolve().parent.parent
prompt_text = "\n".join(
(root / path).read_text(encoding="utf-8")
for path in ("prompts/SYSTEM.md", "prompts/SAFETY.md", "prompts/CONSCIOUSNESS.md")
)
for name in LEGACY_PUBLIC_TOOL_NAMES:
assert re.search(rf"(?<![A-Za-z0-9_]){re.escape(name)}(?![A-Za-z0-9_])", prompt_text) is None, name
for name in (
"read_file",
"run_command",
"commit_reviewed",
"preflight_review",
"schedule_subagent",
"task_acceptance_review",
):
assert name in prompt_text
assert "`runtime_data` for explicit runtime state/memory work" in prompt_text
assert "`runtime_data` for read-only runtime state" not in prompt_text
def test_frozen_registry_includes_service_tools(monkeypatch, tmp_path):
monkeypatch.setattr(__import__("sys"), "frozen", True, raising=False)
registry = ToolRegistry(repo_dir=pathlib.Path(tmp_path), drive_root=pathlib.Path(tmp_path))
schemas = registry.schemas()
names = {schema["function"]["name"] for schema in schemas}
assert {"start_service", "service_status", "service_logs", "stop_service"} <= names
start_schema = next(schema for schema in schemas if schema["function"]["name"] == "start_service")
assert "outputs" in start_schema["function"]["parameters"]["properties"]
def test_skill_payload_root_rejects_bucket_skill_traversal(tmp_path):
repo = tmp_path / "repo"
data = tmp_path / "data"
repo.mkdir()
(data / "settings.json").parent.mkdir(parents=True)
(data / "settings.json").write_text("secret", encoding="utf-8")
registry = ToolRegistry(repo_dir=repo, drive_root=data)
result = registry.execute(
"read_file",
{"root": "skill_payload", "bucket": "external", "skill_name": "../../settings.json", "path": "."},
)
assert "READ_FILE_ERROR" in result or "TOOL_ARG_ERROR" in result
assert "secret" not in result
def test_skill_payload_write_named_bible_is_not_system_protected(tmp_path, monkeypatch):
monkeypatch.setattr("ouroboros.safety.check_safety", lambda *a, **k: (True, ""))
repo = tmp_path / "repo"
data = tmp_path / "data"
repo.mkdir()
payload = data / "skills" / "external" / "alpha"
payload.mkdir(parents=True)
(payload / "SKILL.md").write_text("# alpha\n", encoding="utf-8")
registry = ToolRegistry(repo_dir=repo, drive_root=data)
result = registry.execute(
"write_file",
{
"root": "skill_payload",
"bucket": "external",
"skill_name": "alpha",
"path": "BIBLE.md",
"content": "skill docs",
},
)
assert result.startswith("OK:"), result
assert (data / "skills" / "external" / "alpha" / "BIBLE.md").read_text(encoding="utf-8") == "skill docs"
def _registry_under_fake_home(tmp_path, monkeypatch):
home = tmp_path / "home"
repo = home / "Ouroboros" / "repo"
data = home / "Ouroboros" / "data"
desktop = home / "Desktop"
repo.mkdir(parents=True)
data.mkdir(parents=True)
desktop.mkdir(parents=True)
monkeypatch.setattr(pathlib.Path, "home", lambda: home)
registry = ToolRegistry(repo_dir=repo, drive_root=data)
registry._ctx.task_id = "task1"
from ouroboros.tools import shell
original_env = shell._shell_env_for_cwd
def fixture_child_env(ctx, cwd):
return {**original_env(ctx, cwd), "HOME": str(home), "USERPROFILE": str(home),
"OUROBOROS_DATA_DIR": str(data), "OUROBOROS_SETTINGS_PATH": str(data / "settings.json")}
# Bind the process environment without moving the parent pytest live-data
# guard onto this fixture's production-shaped home/Ouroboros/data tree.
monkeypatch.setattr(shell, "_shell_env_for_cwd", fixture_child_env)
child_env = shell._shell_env_for_cwd(registry._ctx, registry._ctx.task_drive_root())
assert child_env["HOME"] == child_env["USERPROFILE"] == str(home)
assert child_env["OUROBOROS_DATA_DIR"] == str(data)
assert child_env["OUROBOROS_SETTINGS_PATH"] == str(data / "settings.json")
return registry, repo, data, desktop
def test_user_files_root_is_public_and_task_artifact_audited(tmp_path, monkeypatch):
from ouroboros.artifacts import collect_task_artifact_records
monkeypatch.setattr("ouroboros.safety.check_safety", lambda *a, **k: (True, ""))
monkeypatch.setenv("OUROBOROS_RUNTIME_MODE", "light")
registry, repo, data, desktop = _registry_under_fake_home(tmp_path, monkeypatch)
result = registry.execute(
"write_file",
{"root": "user_files", "path": "Desktop/report.html", "content": "<h1>ok</h1>"},
)
assert result.startswith("OK:"), result
assert "LIGHT_MODE_BLOCKED" not in result
assert (desktop / "report.html").read_text(encoding="utf-8") == "<h1>ok</h1>"
assert (data / "task_results" / "artifacts" / "task1" / "report.html").read_text(encoding="utf-8") == "<h1>ok</h1>"
records = collect_task_artifact_records(data, "task1")
assert records[0]["kind"] == "user_file"
assert records[0]["source_path"] == str(desktop / "report.html")
def test_user_files_root_blocks_ouroboros_control_plane(tmp_path, monkeypatch):
monkeypatch.setattr("ouroboros.safety.check_safety", lambda *a, **k: (True, ""))
monkeypatch.setenv("OUROBOROS_RUNTIME_MODE", "light")
registry, repo, _data, _desktop = _registry_under_fake_home(tmp_path, monkeypatch)
result = registry.execute(
"write_file",
{"root": "user_files", "path": str(repo / "README.md"), "content": "bad"},
)
# v6.54.3 root-label hybrid: a user_files WRITE whose absolute path resolves
# under the active workspace now gets the actionable ROOT_REQUIRED redirect
# BEFORE the handler (the retry under root=active_workspace passes through the
# full light-mode/protected-path discipline). Elsewhere the legacy block stays.
# The hard security invariant is identical either way: nothing is written.
assert (
"ROOT_REQUIRED_ACTIVE_WORKSPACE" in result
or ("WRITE_FILE_ERROR" in result and "user_files path blocked" in result)
), result
assert not (repo / "README.md").exists()
case_variant = pathlib.Path.home() / "ouroboros" / "repo" / "README.md"
case_result = registry.execute(
"write_file",
{"root": "user_files", "path": str(case_variant), "content": "bad"},
)
assert (
"ROOT_REQUIRED_ACTIVE_WORKSPACE" in case_result
or ("WRITE_FILE_ERROR" in case_result and "user_files path blocked" in case_result)
), case_result
assert not case_variant.exists()
def test_user_files_root_blocks_workspace_parent_reads_home_secrets(tmp_path, monkeypatch):
monkeypatch.setattr("ouroboros.safety.check_safety", lambda *a, **k: (True, ""))
monkeypatch.setenv("OUROBOROS_RUNTIME_MODE", "light")
registry, _repo, _data, _desktop = _registry_under_fake_home(tmp_path, monkeypatch)
home = pathlib.Path.home()
(home / ".ssh").mkdir()
(home / ".ssh" / "id_rsa").write_text("plain marker content", encoding="utf-8")
parent_result = registry.execute(
"write_file",
{"root": "user_files", "path": str(home / "Ouroboros" / "AGENTS.md"), "content": "bad"},
)
secret_result = registry.execute(
"read_file",
{"root": "user_files", "path": ".ssh/id_rsa"},
)
assert "WRITE_FILE_ERROR" in parent_result
assert "user_files path blocked" in parent_result
# capinv-447 / В23=A: the ROOT principal READS credential-shaped home
# paths (secret BYTES are masked at egress, not refused at read time).
assert "USER_FILES_PATH_BLOCKED" not in secret_result
assert "plain marker content" in secret_result
assert not (home / "Ouroboros" / "AGENTS.md").exists()
def test_user_files_root_preserves_ordinary_credential_named_files(tmp_path, monkeypatch):
monkeypatch.setattr("ouroboros.safety.check_safety", lambda *a, **k: (True, ""))
registry, _repo, _data, _desktop = _registry_under_fake_home(tmp_path, monkeypatch)
home = pathlib.Path.home()
keychain = home / "library" / "Keychains" / "login.keychain-db"
keychain.parent.mkdir(parents=True)
keychain.write_text("keychain marker", encoding="utf-8")
# READS of credential-shaped paths are allowed for root (capinv-447 / В23=A)...
library = registry.execute("read_file", {"root": "user_files", "path": "library/Keychains/login.keychain-db"})
# A project filename alone is not an owner credential store.
creds = registry.execute("write_file", {"root": "user_files", "path": "Desktop/Credentials.json", "content": "{}"})
# A key/certificate SUFFIX is not a credential: the write lands (owner
# answer Q6 of batch 2 - refusal authority is leaves and locations, not names).
pem = registry.execute("write_file", {"root": "user_files", "path": "Desktop/id_rsa.PEM", "content": "secret"})
assert "USER_FILES_PATH_BLOCKED" not in library
assert "keychain marker" in library
assert "ARTIFACT_OUTPUTS" in creds
assert (home / "Desktop/Credentials.json").read_text() == "{}"
assert "credential-like" not in pem
assert (home / "Desktop" / "id_rsa.PEM").read_text(encoding="utf-8") == "secret"
def test_list_files_user_files_blocks_ouroboros_control_plane(tmp_path, monkeypatch):
monkeypatch.setenv("OUROBOROS_RUNTIME_MODE", "light")
registry, repo, _data, _desktop = _registry_under_fake_home(tmp_path, monkeypatch)
(repo / "README.md").write_text("secret repo", encoding="utf-8")
result = registry.execute("list_files", {"root": "user_files", "path": "Ouroboros/repo"})
# Typed policy-denial prefix (v6.57.0 partition) — no generic LIST_FILES_ERROR wrap.
assert "USER_FILES_PATH_BLOCKED" in result
assert "user_files path blocked" in result
assert "README.md" not in result
def test_list_files_user_files_root_prunes_control_plane_children(tmp_path, monkeypatch):
monkeypatch.setenv("OUROBOROS_RUNTIME_MODE", "light")
registry, _repo, _data, desktop = _registry_under_fake_home(tmp_path, monkeypatch)
(desktop / "visible.txt").write_text("ok", encoding="utf-8")
result = registry.execute("list_files", {"root": "user_files", "path": "."})
assert "Desktop/" in result
assert "Ouroboros/" not in result
assert "hidden/control" in result
def test_light_mode_blocks_runtime_data_as_artifact_workaround(tmp_path, monkeypatch):
monkeypatch.setattr("ouroboros.safety.check_safety", lambda *a, **k: (True, ""))
monkeypatch.setenv("OUROBOROS_RUNTIME_MODE", "light")
registry, _repo, data, _desktop = _registry_under_fake_home(tmp_path, monkeypatch)
result = registry.execute(
"write_file",
{"root": "runtime_data", "path": "uploads/report.html", "content": "bad"},
)
assert "LIGHT_MODE_BLOCKED" in result
assert not (data / "uploads" / "report.html").exists()
@pytest.mark.serial
def test_light_mode_executes_process_runtime_data_upload_writes(tmp_path, monkeypatch):
monkeypatch.setattr("ouroboros.safety.check_safety", lambda *a, **k: (True, ""))
monkeypatch.setenv("OUROBOROS_RUNTIME_MODE", "light")
registry, _repo, data, _desktop = _registry_under_fake_home(tmp_path, monkeypatch)
upload = data / "uploads" / "report.html"
result = registry.execute(
"run_command",
{
"cmd": [
sys.executable,
"-c",
(
"from pathlib import Path\n"
f"p = Path({str(upload)!r})\n"
"p.parent.mkdir(parents=True, exist_ok=True)\n"
"p.write_text('bad')\n"
),
],
"cwd": str(registry._ctx.task_drive_root()),
},
)
assert "exit_code=0" in result, result
assert upload.exists()
@pytest.mark.serial
def test_light_mode_executes_run_script_runtime_data_upload_writes(tmp_path, monkeypatch):
monkeypatch.setattr("ouroboros.safety.check_safety", lambda *a, **k: (True, ""))
monkeypatch.setenv("OUROBOROS_RUNTIME_MODE", "light")
registry, _repo, data, _desktop = _registry_under_fake_home(tmp_path, monkeypatch)
upload = data / "uploads" / "report.html"
result = registry.execute(
"run_script",
{
"script": (
"from pathlib import Path\n"
f"p = Path({str(upload)!r})\n"
"p.parent.mkdir(parents=True, exist_ok=True)\n"
"p.write_text('bad')\n"
),
"cwd": "task_drive",
},
)
assert "exit_code=0" in result, result
assert upload.exists()
@pytest.mark.serial
def test_light_mode_executes_relative_process_runtime_data_upload_writes(tmp_path, monkeypatch):
monkeypatch.setattr("ouroboros.safety.check_safety", lambda *a, **k: (True, ""))
monkeypatch.setenv("OUROBOROS_RUNTIME_MODE", "light")
registry, _repo, data, _desktop = _registry_under_fake_home(tmp_path, monkeypatch)
upload = data / "uploads" / "relative-report.html"
task_drive = registry._ctx.task_drive_root()
result = registry.execute(
"run_command",
{
"cmd": [
sys.executable,
"-c",
(
"from pathlib import Path\n"
"p = Path('../../uploads/relative-report.html')\n"
"p.parent.mkdir(parents=True, exist_ok=True)\n"
"p.write_text('bad')\n"
),
],
"cwd": str(task_drive),
},
)
assert "exit_code=0" in result, result
assert upload.exists()
@pytest.mark.serial
def test_light_mode_executes_env_runtime_data_upload_writes(tmp_path, monkeypatch):
monkeypatch.setattr("ouroboros.safety.check_safety", lambda *a, **k: (True, ""))
monkeypatch.setenv("OUROBOROS_RUNTIME_MODE", "light")
registry, _repo, data, _desktop = _registry_under_fake_home(tmp_path, monkeypatch)
upload = data / "uploads" / "env-report.html"
result = registry.execute(
"run_command",
{
"cmd": ["sh", "-c", "mkdir -p \"$OUROBOROS_DATA_DIR/uploads\" && echo bad > \"$OUROBOROS_DATA_DIR/uploads/env-report.html\""],
"cwd": str(registry._ctx.task_drive_root()),
},
)
assert "exit_code=0" in result, result
assert upload.read_text() == "bad\n"
@pytest.mark.serial
def test_light_mode_executes_interpreter_runtime_data_touch_without_write_marker(tmp_path, monkeypatch):
monkeypatch.setattr("ouroboros.safety.check_safety", lambda *a, **k: (True, ""))
monkeypatch.setenv("OUROBOROS_RUNTIME_MODE", "light")
registry, _repo, data, _desktop = _registry_under_fake_home(tmp_path, monkeypatch)
upload = data / "uploads" / "touch-report.html"
python_exe = str(pathlib.Path(sys.executable))
result = registry.execute(
"run_command",
{
"cmd": [
python_exe,
"-c",
(
"from pathlib import Path\n"
f"p = Path({str(upload)!r})\n"
"p.parent.mkdir(parents=True, exist_ok=True)\n"
"p.touch()\n"
),
],
"cwd": str(registry._ctx.task_drive_root()),
},
)
assert "exit_code=0" in result, result
assert upload.exists()
@pytest.mark.serial
def test_light_mode_executes_home_runtime_data_upload_writes(tmp_path, monkeypatch):
monkeypatch.setattr("ouroboros.safety.check_safety", lambda *a, **k: (True, ""))
monkeypatch.setenv("OUROBOROS_RUNTIME_MODE", "light")
registry, _repo, data, _desktop = _registry_under_fake_home(tmp_path, monkeypatch)
home_upload = data / "uploads" / "home-report.html"
tilde_upload = data / "uploads" / "tilde-report.html"
home_result = registry.execute(
"run_command",
{
"cmd": [
"sh",
"-c",
'mkdir -p "$HOME/Ouroboros/data/uploads" && touch "$HOME/Ouroboros/data/uploads/home-report.html"',
],
"cwd": str(registry._ctx.task_drive_root()),
},
)
tilde_result = registry.execute(
"run_command",
{
"cmd": ["sh", "-c", "mkdir -p ~/Ouroboros/data/uploads && touch ~/Ouroboros/data/uploads/tilde-report.html"],
"cwd": str(registry._ctx.task_drive_root()),
},
)
assert "exit_code=0" in home_result, home_result
assert "exit_code=0" in tilde_result, tilde_result
assert home_upload.read_bytes() == tilde_upload.read_bytes() == b""
@pytest.mark.serial
def test_light_mode_executes_relative_run_script_runtime_data_upload_writes(tmp_path, monkeypatch):
monkeypatch.setattr("ouroboros.safety.check_safety", lambda *a, **k: (True, ""))
monkeypatch.setenv("OUROBOROS_RUNTIME_MODE", "light")
registry, _repo, data, _desktop = _registry_under_fake_home(tmp_path, monkeypatch)
upload = data / "uploads" / "relative-report.html"
result = registry.execute(
"run_script",
{
"script": (
"from pathlib import Path\n"
"p = Path('../../uploads/relative-report.html')\n"
"p.parent.mkdir(parents=True, exist_ok=True)\n"
"p.write_text('bad')\n"
),
"cwd": "task_drive",
},
)
assert "exit_code=0" in result, result
assert upload.exists()
def test_light_run_script_allows_readonly_repo_analysis_with_external_write(tmp_path, monkeypatch):
monkeypatch.setattr("ouroboros.safety.check_safety", lambda *a, **k: (True, ""))
monkeypatch.setenv("OUROBOROS_RUNTIME_MODE", "light")
registry, repo, _data, _desktop = _registry_under_fake_home(tmp_path, monkeypatch)
out = registry._ctx.task_drive_root() / "evidence.json"
result = registry.execute(
"run_script",
{
"script": (
"from pathlib import Path\n"
f"repo = {str(repo)!r}\n"
f"out = Path({str(out)!r})\n"
"out.write_text(repo)\n"
"import sys; sys.stdout.write(repo)\n"
),
"cwd": "task_drive",
"outputs": [str(out)],
},
)
assert "LIGHT_MODE_BLOCKED" not in result, result
assert out.read_text(encoding="utf-8") == str(repo)
@pytest.mark.serial
def test_light_run_script_executes_dynamic_repo_write_even_from_task_drive(tmp_path, monkeypatch):
monkeypatch.setattr("ouroboros.safety.check_safety", lambda *a, **k: (True, ""))
monkeypatch.setenv("OUROBOROS_RUNTIME_MODE", "light")
registry, repo, _data, _desktop = _registry_under_fake_home(tmp_path, monkeypatch)
target = repo / "dynamic.txt"
result = registry.execute(
"run_script",
{
"script": (
"from pathlib import Path\n"
f"repo = Path({str(repo)!r})\n"
"name = 'dynamic.txt'\n"
"(repo / name).write_text('bad')\n"
),
"cwd": "task_drive",
},
)
assert "exit_code=0" in result, result
assert target.read_text(encoding="utf-8") == "bad"
@pytest.mark.serial
def test_light_run_script_executes_path_open_repo_write(tmp_path, monkeypatch):
monkeypatch.setattr("ouroboros.safety.check_safety", lambda *a, **k: (True, ""))
monkeypatch.setenv("OUROBOROS_RUNTIME_MODE", "light")
registry, repo, _data, _desktop = _registry_under_fake_home(tmp_path, monkeypatch)
target = repo / "path-open.txt"
result = registry.execute(
"run_script",
{
"script": (
"from pathlib import Path\n"
f"Path({str(target)!r}).open('w').write('bad')\n"
),
"cwd": "task_drive",
},
)
assert "exit_code=0" in result, result
assert target.read_text(encoding="utf-8") == "bad"
def test_light_run_script_allows_constant_expression_task_drive_write(tmp_path, monkeypatch):
monkeypatch.setattr("ouroboros.safety.check_safety", lambda *a, **k: (True, ""))
monkeypatch.setenv("OUROBOROS_RUNTIME_MODE", "light")
registry, _repo, _data, _desktop = _registry_under_fake_home(tmp_path, monkeypatch)
target = registry._ctx.task_drive_root() / "out.txt"
result = registry.execute(
"run_script",
{
"script": (
"from pathlib import Path\n"
"name = 'out' + '.txt'\n"
"Path(name).write_text('ok')\n"
),
"cwd": "task_drive",
},
)
assert "LIGHT_MODE_BLOCKED" not in result, result
assert target.read_text(encoding="utf-8") == "ok"
def test_light_run_script_allows_resolved_open_handle_task_drive_write(tmp_path, monkeypatch):
monkeypatch.setattr("ouroboros.safety.check_safety", lambda *a, **k: (True, ""))
monkeypatch.setenv("OUROBOROS_RUNTIME_MODE", "light")
registry, _repo, _data, _desktop = _registry_under_fake_home(tmp_path, monkeypatch)
target = registry._ctx.task_drive_root() / "handle.txt"
result = registry.execute(
"run_script",
{
"script": (
"f = open('handle.txt', 'w')\n"
"f.write('ok')\n"
"f.close()\n"
),
"cwd": "task_drive",
},
)
assert "LIGHT_MODE_BLOCKED" not in result, result
assert target.read_text(encoding="utf-8") == "ok"
def test_light_run_script_allows_with_open_handle_task_drive_write(tmp_path, monkeypatch):
monkeypatch.setattr("ouroboros.safety.check_safety", lambda *a, **k: (True, ""))
monkeypatch.setenv("OUROBOROS_RUNTIME_MODE", "light")
registry, _repo, _data, _desktop = _registry_under_fake_home(tmp_path, monkeypatch)
target = registry._ctx.task_drive_root() / "with-open.txt"
result = registry.execute(
"run_script",
{"script": "with open('with-open.txt', 'w') as f:\n f.write('ok')\n", "cwd": "task_drive"},
)
assert "LIGHT_MODE_BLOCKED" not in result, result
assert target.read_text(encoding="utf-8") == "ok"
def test_light_run_script_allows_path_cwd_task_drive_write(tmp_path, monkeypatch):
monkeypatch.setattr("ouroboros.safety.check_safety", lambda *a, **k: (True, ""))
monkeypatch.setenv("OUROBOROS_RUNTIME_MODE", "light")
registry, _repo, _data, _desktop = _registry_under_fake_home(tmp_path, monkeypatch)
target = registry._ctx.task_drive_root() / "cwd-write.txt"
result = registry.execute(
"run_script",
{
"script": (
"from pathlib import Path\n"
"(Path.cwd() / 'cwd-write.txt').write_text('ok')\n"
),
"cwd": "task_drive",
},
)
assert "LIGHT_MODE_BLOCKED" not in result, result
assert target.read_text(encoding="utf-8") == "ok"
def test_light_run_script_allows_in_memory_write_method(tmp_path, monkeypatch):
monkeypatch.setattr("ouroboros.safety.check_safety", lambda *a, **k: (True, ""))
monkeypatch.setenv("OUROBOROS_RUNTIME_MODE", "light")
registry, _repo, _data, _desktop = _registry_under_fake_home(tmp_path, monkeypatch)
result = registry.execute(
"run_script",
{
"script": (
"import io\n"
"buf = io.StringIO()\n"
"buf.write('ok')\n"
"print(buf.getvalue())\n"
),
"cwd": "task_drive",
},
)
assert "LIGHT_MODE_BLOCKED" not in result, result
assert "ok" in result
def test_artifact_store_blocks_control_manifest_edits(tmp_path, monkeypatch):
monkeypatch.setattr("ouroboros.safety.check_safety", lambda *a, **k: (True, ""))
monkeypatch.setenv("OUROBOROS_RUNTIME_MODE", "light")
registry, _repo, data, _desktop = _registry_under_fake_home(tmp_path, monkeypatch)
write_result = registry.execute(
"write_file",
{"root": "artifact_store", "path": ".artifact_manifest.json", "content": "{}"},
)
hidden_result = registry.execute(
"write_file",
{"root": "artifact_store", "path": ".meta/report.json", "content": "{}"},
)
registry._ctx.is_direct_chat = True
edit_result = registry.execute(
"edit_text",
{"root": "artifact_store", "path": ".artifact_manifest.json", "old_str": "x", "new_str": "y"},
)
assert "WRITE_FILE_BLOCKED" in write_result
assert "OK: wrote artifact_store:.meta/report.json" in hidden_result
assert (data / "task_results/artifacts/task1/.meta/report.json").read_text() == "{}"
assert "EDIT_TEXT_BLOCKED" in edit_result
assert not (data / "task_results" / "artifacts" / "task1" / ".artifact_manifest.json").exists()
def test_write_file_batch_partial_failure_is_semantic_failure(tmp_path, monkeypatch):
from ouroboros.loop_tool_execution import _extract_result_metadata, _is_tool_execution_failure
monkeypatch.setattr("ouroboros.safety.check_safety", lambda *a, **k: (True, ""))
monkeypatch.setenv("OUROBOROS_RUNTIME_MODE", "light")
registry, _repo, data, _desktop = _registry_under_fake_home(tmp_path, monkeypatch)
result = registry.execute(
"write_file",
{
"root": "artifact_store",
"files": [
{"path": "ok.html", "content": "<p>ok</p>"},
{"path": ".artifact_manifest.json", "content": "{}"},
],
},
)
assert result.startswith("⚠️ WRITE_FILE_BATCH_PARTIAL_FAILURE"), result
assert "OK:" in result
assert "WRITE_FILE_BLOCKED" in result
assert _is_tool_execution_failure(True, result)
assert _extract_result_metadata("write_file", result, True)["status"] == "write_file_blocked"
assert (data / "task_results" / "artifacts" / "task1" / "ok.html").exists()
assert not (data / "task_results" / "artifacts" / "task1" / ".artifact_manifest.json").exists()
def test_run_script_light_explicit_task_drive_outputs_are_artifacts(tmp_path, monkeypatch):
monkeypatch.setattr("ouroboros.safety.check_safety", lambda *a, **k: (True, ""))
monkeypatch.setenv("OUROBOROS_RUNTIME_MODE", "light")
registry, repo, data, _desktop = _registry_under_fake_home(tmp_path, monkeypatch)
result = registry.execute(
"run_script",
{
"script": "from pathlib import Path\nif 1 > 0:\n Path('probe.txt').write_text('ok')\n",
"cwd": "task_drive",
"outputs": ["probe.txt"],
},
)
assert "LIGHT_MODE_BLOCKED" not in result
assert "exit_code=0" in result
assert not (repo / "probe.txt").exists()
assert (data / "task_drives" / "task1" / "probe.txt").read_text(encoding="utf-8") == "ok"
assert (data / "task_results" / "artifacts" / "task1" / "probe.txt").read_text(encoding="utf-8") == "ok"
def test_run_script_output_failure_starts_with_failure_prefix(tmp_path, monkeypatch):
monkeypatch.setenv("OUROBOROS_RUNTIME_MODE", "light")
monkeypatch.setattr("ouroboros.safety.check_safety", lambda *args, **kwargs: (True, ""))
repo = tmp_path / "repo"
data = tmp_path / "data"
repo.mkdir()
registry = ToolRegistry(repo_dir=repo, drive_root=data)
registry._ctx.task_id = "task1"
result = registry.execute(
"run_script",
{"script": "print('no output created')", "outputs": ["missing.html"]},
)
assert result.startswith("⚠️ ARTIFACT_OUTPUT_ERROR")
assert "# script_path=" in result
def test_run_script_light_cwd_user_files_allowed(tmp_path, monkeypatch):
monkeypatch.setattr("ouroboros.safety.check_safety", lambda *a, **k: (True, ""))
monkeypatch.setenv("OUROBOROS_RUNTIME_MODE", "light")
registry, _repo, data, desktop = _registry_under_fake_home(tmp_path, monkeypatch)
result = registry.execute(
"run_script",
{
"script": "from pathlib import Path\nPath('external.html').write_text('<p>ok</p>')\n",
"cwd": str(desktop),
"outputs": ["external.html"],
},
)
assert "SHELL_CWD_BLOCKED" not in result
assert "LIGHT_MODE_BLOCKED" not in result
assert "exit_code=0" in result
assert (desktop / "external.html").read_text(encoding="utf-8") == "<p>ok</p>"
assert (data / "task_results" / "artifacts" / "task1" / "external.html").read_text(encoding="utf-8") == "<p>ok</p>"
def test_run_script_registers_directory_outputs_as_manifest_and_zip(tmp_path, monkeypatch):
import json
import zipfile
monkeypatch.setattr("ouroboros.safety.check_safety", lambda *a, **k: (True, ""))
monkeypatch.setenv("OUROBOROS_RUNTIME_MODE", "light")
registry, _repo, data, _desktop = _registry_under_fake_home(tmp_path, monkeypatch)
result = registry.execute(
"run_script",
{
"script": (
"from pathlib import Path\n"
"Path('site/assets').mkdir(parents=True)\n"
"Path('site/index.html').write_text('<h1>ok</h1>')\n"
"Path('site/assets/app.js').write_text('console.log(1)')\n"
),
"cwd": "task_drive",
"outputs": ["site"],
},
)
artifact_dir = data / "task_results" / "artifacts" / "task1"
manifests = list(artifact_dir.glob("site.*.manifest.json"))
zips = list(artifact_dir.glob("site.*.zip"))
assert "registered directory output" in result
assert len(manifests) == 1
assert len(zips) == 1
manifest = json.loads(manifests[0].read_text(encoding="utf-8"))
assert manifest["file_count"] == 2
assert {item["path"] for item in manifest["files"]} == {"index.html", "assets/app.js"}
with zipfile.ZipFile(zips[0]) as archive:
assert sorted(archive.namelist()) == ["assets/app.js", "index.html"]
def test_run_command_light_creates_fresh_task_scoped_cwds(tmp_path, monkeypatch):
monkeypatch.setattr("ouroboros.safety.check_safety", lambda *a, **k: (True, ""))
monkeypatch.setenv("OUROBOROS_RUNTIME_MODE", "light")
repo = tmp_path / "repo"
data = tmp_path / "data"
repo.mkdir()
registry = ToolRegistry(repo_dir=repo, drive_root=data)
registry._ctx.task_id = "task1"
task_drive = data / "task_drives" / "task1"
artifact_store = data / "task_results" / "artifacts" / "task1"
assert not task_drive.exists()
assert not artifact_store.exists()
for cwd, filename in ((artifact_store, "canonical.html"), (task_drive, "scratch.html")):
result = registry.execute(
"run_command",
{
"cmd": [sys.executable, "-c", f"from pathlib import Path; Path({filename!r}).write_text('ok')"],
"cwd": str(cwd),
"outputs": [filename],
},
)
assert "SHELL_CWD_BLOCKED" not in result
assert "exit_code=0" in result
assert (cwd / filename).read_text(encoding="utf-8") == "ok"
assert (artifact_store / filename).read_text(encoding="utf-8") == "ok"
def test_run_command_user_files_audit_gap_is_effect_based(tmp_path, monkeypatch):
# R5: the artifact-audit nudge is now EFFECT-BASED — it fires only when a user_files
# command actually changed the cwd, not on every command. A read-only command no
# longer false-triggers it; a command that creates a deliverable (whose name is not a
# literal in the cmd, so the declaration-regex does not pre-empt it) still does.
monkeypatch.setattr("ouroboros.safety.check_safety", lambda *a, **k: (True, ""))
monkeypatch.setenv("OUROBOROS_RUNTIME_MODE", "light")
registry, _repo, _data, desktop = _registry_under_fake_home(tmp_path, monkeypatch)
read_only = registry.execute("run_command", {"cmd": ["python3", "-c", "print('ok')"], "cwd": str(desktop)})
assert "exit_code=0" in read_only
assert "ARTIFACT_AUDIT_GAP" not in read_only
creates_file = registry.execute(
"run_command",
{"cmd": ["python3", "-c", "open(chr(100)+'eliverable.dat','w').write('x')"], "cwd": str(desktop)},
)
assert "exit_code=0" in creates_file
assert "ARTIFACT_AUDIT_GAP" in creates_file
def test_run_command_audit_rejects_invalid_git_marker(tmp_path):
from ouroboros.tools.shell import _resolve_git_root
fake_repo = tmp_path / "fake-repo"
child = fake_repo / "child"
(fake_repo / ".git").mkdir(parents=True)
child.mkdir()
assert _resolve_git_root(child) is None
def test_run_command_outputs_registers_artifact(tmp_path, monkeypatch):
from ouroboros.artifacts import collect_task_artifact_records
monkeypatch.setattr("ouroboros.safety.check_safety", lambda *a, **k: (True, ""))
monkeypatch.setenv("OUROBOROS_RUNTIME_MODE", "light")
registry, _repo, data, desktop = _registry_under_fake_home(tmp_path, monkeypatch)
result = registry.execute(
"run_command",
{
"cmd": ["python3", "-c", "from pathlib import Path; Path('deliverable.txt').write_text('ok')"],
"cwd": str(desktop),
"outputs": ["deliverable.txt"],
},
)
assert "exit_code=0" in result
assert "ARTIFACT_OUTPUT_ERROR" not in result
assert "ARTIFACT_OUTPUT_UNDECLARED" not in result
assert "registered output" in result
assert (data / "task_results" / "artifacts" / "task1" / "deliverable.txt").read_text(encoding="utf-8") == "ok"
records = collect_task_artifact_records(data, "task1")
assert records[0]["kind"] == "process_output"
assert records[0]["source_path"] == str(desktop / "deliverable.txt")
def test_run_command_unchanged_output_is_cosmetic(tmp_path, monkeypatch):
monkeypatch.setattr("ouroboros.safety.check_safety", lambda *a, **k: (True, ""))
monkeypatch.setenv("OUROBOROS_RUNTIME_MODE", "light")
registry, _repo, data, desktop = _registry_under_fake_home(tmp_path, monkeypatch)
(desktop / "personal_notes.txt").write_text("old", encoding="utf-8")
result = registry.execute(
"run_command",
{
"cmd": ["python3", "-c", "print('no file generated')"],
"cwd": str(desktop),
"outputs": ["personal_notes.txt"],
},
)
# C2 (v6.36.0): present-but-unchanged declared output is a cosmetic note, not a
# blocking ARTIFACT_OUTPUT_ERROR (a missing output still blocks — sibling test).
assert not result.startswith("⚠️ ARTIFACT_OUTPUT_ERROR"), result
assert "unchanged output (cosmetic)" in result
assert not (data / "task_results" / "artifacts" / "task1" / "personal_notes.txt").exists()
# round-5: a cosmetic-only note must NOT borrow the canonical "ARTIFACT_OUTPUTS"
# marker — downstream (outcomes.py / loop_tool_execution.py) reads that exact
# substring as a real registration / false recovery signal. Use ARTIFACT_OUTPUT_NOTE.
assert "ARTIFACT_OUTPUT_NOTE" in result
assert "ARTIFACT_OUTPUTS" not in result
def test_run_command_outputs_missing_is_semantic_failure(tmp_path, monkeypatch):
monkeypatch.setattr("ouroboros.safety.check_safety", lambda *a, **k: (True, ""))
monkeypatch.setenv("OUROBOROS_RUNTIME_MODE", "light")
registry, _repo, _data, desktop = _registry_under_fake_home(tmp_path, monkeypatch)
result = registry.execute(
"run_command",
{"cmd": ["python3", "-c", "print('ok')"], "cwd": str(desktop), "outputs": ["missing.txt"]},
)
assert result.startswith("⚠️ ARTIFACT_OUTPUT_ERROR"), result
assert "missing output: missing.txt" in result
def test_run_command_outputs_block_protected_absolute_paths(tmp_path, monkeypatch):
monkeypatch.setattr("ouroboros.safety.check_safety", lambda *a, **k: (True, ""))
monkeypatch.setenv("OUROBOROS_RUNTIME_MODE", "light")
registry, _repo, data, desktop = _registry_under_fake_home(tmp_path, monkeypatch)
(data / "settings.json").write_text('{"secret":"x"}', encoding="utf-8")
result = registry.execute(
"run_command",
{
"cmd": ["python3", "-c", "print('ok')"],
"cwd": str(desktop),
"outputs": [str(data / "settings.json")],
},
)
assert result.startswith("⚠️ ARTIFACT_OUTPUT_ERROR"), result
assert "protected user_files output" in result
assert not (data / "task_results" / "artifacts" / "task1" / "settings.json").exists()
def test_run_command_outputs_block_protected_repo_files(tmp_path, monkeypatch):
monkeypatch.setattr("ouroboros.safety.check_safety", lambda *a, **k: (True, ""))
monkeypatch.setenv("OUROBOROS_RUNTIME_MODE", "light")
registry, repo, data, _desktop = _registry_under_fake_home(tmp_path, monkeypatch)
(repo / "BIBLE.md").write_text("constitutional", encoding="utf-8")
result = registry.execute(
"run_command",
{"cmd": ["python3", "-c", "print('ok')"], "cwd": str(repo), "outputs": ["BIBLE.md"]},
)
assert result.startswith("⚠️ ARTIFACT_OUTPUT_ERROR"), result
assert "protected repo output BIBLE.md" in result
assert not (data / "task_results" / "artifacts" / "task1" / "BIBLE.md").exists()
def test_run_command_without_outputs_blocks_absolute_user_file_writes(tmp_path, monkeypatch):
monkeypatch.setattr("ouroboros.safety.check_safety", lambda *a, **k: (True, ""))
monkeypatch.setenv("OUROBOROS_RUNTIME_MODE", "light")
registry, repo, data, desktop = _registry_under_fake_home(tmp_path, monkeypatch)
target = desktop / "live-regression.html"
result = registry.execute(
"run_command",
{
"cmd": [
"python3",
"-c",
f"from pathlib import Path; Path({str(target)!r}).write_text('<h1>ok</h1>')",
],
"cwd": str(repo),
},
)
assert result.startswith("⚠️ ARTIFACT_OUTPUT_UNDECLARED"), result
assert "without declaring outputs" in result
assert target.read_text(encoding="utf-8") == "<h1>ok</h1>"
assert not (data / "task_results" / "artifacts" / "task1" / target.name).exists()
def test_run_command_artifact_error_still_invalidates_repo_advisory(tmp_path, monkeypatch):
import subprocess
calls = []
monkeypatch.setattr("ouroboros.safety.check_safety", lambda *a, **k: (True, ""))
monkeypatch.setattr("ouroboros.tools.shell._invalidate_advisory", lambda *args, **kwargs: calls.append((args, kwargs)))
monkeypatch.setenv("OUROBOROS_RUNTIME_MODE", "advanced")
registry, repo, _data, desktop = _registry_under_fake_home(tmp_path, monkeypatch)
subprocess.run(["git", "init"], cwd=repo, check=True, capture_output=True)
target = desktop / "undeclared.html"
result = registry.execute(
"run_command",
{
"cmd": [
"python3",
"-c",
(
"from pathlib import Path; "
"Path('changed.txt').write_text('repo'); "
f"Path({str(target)!r}).write_text('external')"
),
],
"cwd": str(repo),
},
)
assert result.startswith("⚠️ ARTIFACT_OUTPUT_UNDECLARED"), result
assert calls
assert calls[-1][1]["source_tool"] == "run_command"
assert "changed.txt" in calls[-1][1]["changed_paths"]
def test_run_command_without_outputs_allows_absolute_user_file_reads(tmp_path, monkeypatch):
monkeypatch.setattr("ouroboros.safety.check_safety", lambda *a, **k: (True, ""))
monkeypatch.setenv("OUROBOROS_RUNTIME_MODE", "light")
registry, repo, _data, desktop = _registry_under_fake_home(tmp_path, monkeypatch)
target = desktop / "input.txt"
target.write_text("hello", encoding="utf-8")
result = registry.execute(
"run_command",
{
"cmd": [
"python3",
"-c",
f"open({str(target)!r}).read(); print('ok')",
],
"cwd": str(repo),
},
)
assert "ARTIFACT_OUTPUT_ERROR" not in result
assert "ARTIFACT_OUTPUT_UNDECLARED" not in result
assert "exit_code=0" in result
def test_run_command_without_outputs_blocks_absolute_user_file_open_writes(tmp_path, monkeypatch):
monkeypatch.setattr("ouroboros.safety.check_safety", lambda *a, **k: (True, ""))
monkeypatch.setenv("OUROBOROS_RUNTIME_MODE", "light")
registry, repo, data, desktop = _registry_under_fake_home(tmp_path, monkeypatch)
target = desktop / "open-write.html"
result = registry.execute(
"run_command",
{
"cmd": [
"python3",
"-c",
f"open({str(target)!r}, 'w').write('<h1>ok</h1>')",
],
"cwd": str(repo),
},
)
assert result.startswith("⚠️ ARTIFACT_OUTPUT_UNDECLARED"), result
assert "without declaring outputs" in result
assert target.read_text(encoding="utf-8") == "<h1>ok</h1>"
assert not (data / "task_results" / "artifacts" / "task1" / target.name).exists()
def test_run_script_without_outputs_flags_absolute_user_file_writes(tmp_path, monkeypatch):
# v6.56.0: run_script body-audit moved to POST-exec stat verification (parity
# with run_command), so a script that writes an undeclared absolute user_files
# path RUNS (the write happens — a post-exec side effect can't be un-done) and
# is then flagged ARTIFACT_OUTPUT_ERROR ("wrote"), not blocked before it runs.
monkeypatch.setattr("ouroboros.safety.check_safety", lambda *a, **k: (True, ""))
monkeypatch.setenv("OUROBOROS_RUNTIME_MODE", "light")
registry, _repo, data, desktop = _registry_under_fake_home(tmp_path, monkeypatch)
target = desktop / "script-write.html"
result = registry.execute(
"run_script",
{
"script": f"from pathlib import Path\nPath({str(target)!r}).write_text('<h1>ok</h1>')\n",
"interpreter": "python3",
},
)
assert result.startswith("⚠️ ARTIFACT_OUTPUT_UNDECLARED"), result
assert "run_script wrote user_files without declaring outputs" in result
assert str(target) in result
# #447/D5: the nudge must not REPLACE the payload. It held line 1 (the typed
# policy-denial surface the classifier reads) and returned nothing else, so a
# successful script's own output was discarded. The script path and the run
# result now ride behind the marker.
assert "# script_path=" in result
# The write happened (post-exec) but the file is NOT registered as an artifact.
assert target.read_text(encoding="utf-8") == "<h1>ok</h1>"
assert not (data / "task_results" / "artifacts" / "task1" / target.name).exists()
def test_run_command_without_outputs_detects_shell_redirection_to_user_files(tmp_path, monkeypatch):
monkeypatch.setattr("ouroboros.safety.check_safety", lambda *a, **k: (True, ""))
monkeypatch.setenv("OUROBOROS_RUNTIME_MODE", "light")
registry, repo, data, desktop = _registry_under_fake_home(tmp_path, monkeypatch)
target = desktop / "redirect.html"
result = registry.execute(
"run_command",
{"cmd": ["sh", "-c", f"echo ok > {shlex.quote(target.as_posix())}"], "cwd": str(desktop)},
)
assert result.startswith("⚠️ ARTIFACT_OUTPUT_UNDECLARED"), result
assert "without declaring outputs" in result
assert target.read_text(encoding="utf-8").strip() == "ok"
assert not (data / "task_results" / "artifacts" / "task1" / target.name).exists()
def test_run_command_outputs_block_preexisting_dirty_repo_secret(tmp_path, monkeypatch):
monkeypatch.setattr("ouroboros.safety.check_safety", lambda *a, **k: (True, ""))
monkeypatch.setenv("OUROBOROS_RUNTIME_MODE", "light")
registry, repo, data, _desktop = _registry_under_fake_home(tmp_path, monkeypatch)
(repo / ".env").write_text("TOKEN=x", encoding="utf-8")
result = registry.execute(
"run_command",
{"cmd": ["python3", "-c", "print('ok')"], "cwd": str(repo), "outputs": [".env"]},
)
assert result.startswith("⚠️ ARTIFACT_OUTPUT_ERROR"), result
assert "credential-like output .env" in result
assert not (data / "task_results" / "artifacts" / "task1" / ".env").exists()
def test_run_command_outputs_block_new_credential_like_repo_file(tmp_path, monkeypatch):
import subprocess
monkeypatch.setattr("ouroboros.safety.check_safety", lambda *a, **k: (True, ""))
monkeypatch.setenv("OUROBOROS_RUNTIME_MODE", "advanced")
registry, repo, data, _desktop = _registry_under_fake_home(tmp_path, monkeypatch)
subprocess.run(["git", "init"], cwd=repo, check=True, capture_output=True)
result = registry.execute(
"run_command",
{
"cmd": ["python3", "-c", "from pathlib import Path; Path('.env').write_text('TOKEN=x')"],
"cwd": str(repo),
"outputs": [".env"],
},
)
assert result.startswith("⚠️ ARTIFACT_OUTPUT_ERROR"), result
assert "credential-like output .env" in result
assert not (data / "task_results" / "artifacts" / "task1" / ".env").exists()
def test_workspace_run_command_outputs_can_reference_policy_visible_user_files(tmp_path, monkeypatch):
monkeypatch.setattr("ouroboros.safety.check_safety", lambda *a, **k: (True, ""))
registry, _repo, data, desktop = _registry_under_fake_home(tmp_path, monkeypatch)
workspace = tmp_path / "workspace"
workspace.mkdir()
(desktop / "outside.txt").write_text("outside", encoding="utf-8")
registry._ctx.workspace_mode = "workspace"
registry._ctx.workspace_root = str(workspace)
registry._ctx.active_repo_dir = lambda: workspace
result = registry.execute(
"run_command",
{"cmd": ["python3", "-c", "print('ok')"], "cwd": str(workspace), "outputs": [str(desktop / "outside.txt")]},
)
assert not result.startswith("⚠️ ARTIFACT_OUTPUT_ERROR"), result
assert "unchanged output (cosmetic)" in result
assert not (data / "task_results" / "artifacts" / "task1" / "outside.txt").exists()
def test_repeated_user_file_write_updates_same_canonical_artifact(tmp_path, monkeypatch):
from ouroboros.artifacts import collect_task_artifact_records
monkeypatch.setattr("ouroboros.safety.check_safety", lambda *a, **k: (True, ""))
monkeypatch.setenv("OUROBOROS_RUNTIME_MODE", "light")
registry, _repo, data, _desktop = _registry_under_fake_home(tmp_path, monkeypatch)
artifact_dir = data / "task_results" / "artifacts" / "task1"
version_dir = data / "task_results" / "artifact_versions" / "task1" / "report.html"
first = registry.execute(
"write_file",
{"root": "user_files", "path": "Desktop/report.html", "content": "<p>draft</p>"},
)
second = registry.execute(
"write_file",
{"root": "user_files", "path": "Desktop/report.html", "content": "<p>final</p>", "mode": "append"},
)
assert "artifact_store:report.html" in first
assert "artifact_store:report.html" in second
assert (artifact_dir / "report.html").read_text(encoding="utf-8") == "<p>draft</p><p>final</p>"
assert not list(artifact_dir.glob("report.*.html"))
versions = sorted(version_dir.glob("*"))
assert len(versions) == 1
assert versions[0].read_text(encoding="utf-8") == "<p>draft</p>"
records = collect_task_artifact_records(data, "task1")
assert [record["name"] for record in records] == ["report.html"]
def test_user_file_artifact_history_retains_last_five_versions(tmp_path, monkeypatch):
monkeypatch.setattr("ouroboros.safety.check_safety", lambda *a, **k: (True, ""))
monkeypatch.setenv("OUROBOROS_RUNTIME_MODE", "light")
registry, _repo, data, _desktop = _registry_under_fake_home(tmp_path, monkeypatch)
version_dir = data / "task_results" / "artifact_versions" / "task1" / "report.html"
for i in range(7):
result = registry.execute(
"write_file",
{"root": "user_files", "path": "Desktop/report.html", "content": f"v{i}"},
)
assert "artifact_store:report.html" in result
assert (data / "task_results" / "artifacts" / "task1" / "report.html").read_text(encoding="utf-8") == "v6"
version_contents = [path.read_text(encoding="utf-8") for path in sorted(version_dir.glob("*"))]
assert len(version_contents) == 5
assert version_contents == ["v1", "v2", "v3", "v4", "v5"]
def test_merge_artifact_records_preserves_specific_kind_against_generic_collection():
from ouroboros.artifacts import merge_artifact_records
merged = merge_artifact_records(
[{"kind": "verification_ledger", "name": "verification_ledger.json", "path": "/tmp/a/verification_ledger.json"}],
[{"kind": "task_artifact", "name": "verification_ledger.json", "path": "/tmp/a/verification_ledger.json", "size": 12}],
)
assert merged[0]["kind"] == "verification_ledger"
assert merged[0]["name"] == "verification_ledger.json"
assert merged[0]["size"] == 12
def test_collect_task_artifact_records_skips_symlinks(tmp_path):
from ouroboros.artifacts import collect_task_artifact_records, task_artifact_dir_path
drive = tmp_path / "data"
artifact_dir = task_artifact_dir_path(drive, "task1", create=True)
outside = tmp_path / "outside.txt"
outside.write_text("secret", encoding="utf-8")
try:
(artifact_dir / "leak.txt").symlink_to(outside)
except OSError:
return
records = collect_task_artifact_records(drive, "task1")
assert records == []
def test_search_code_user_files_default_path_prunes_protected_children(tmp_path, monkeypatch):
monkeypatch.setattr("ouroboros.safety.check_safety", lambda *a, **k: (True, ""))
monkeypatch.setenv("OUROBOROS_RUNTIME_MODE", "light")
registry, _repo, _data, desktop = _registry_under_fake_home(tmp_path, monkeypatch)
(desktop / "notes.txt").write_text("needle", encoding="utf-8")
result = registry.execute("search_code", {"root": "user_files", "query": "needle"})
assert "SEARCH_ERROR" not in result
assert "Desktop/notes.txt" in result
def test_task_drive_root_is_scratch_even_for_forked_workspace(tmp_path, monkeypatch):
registry, _repo, data, _desktop = _registry_under_fake_home(tmp_path, monkeypatch)
child_drive = data / "headless_tasks" / "task1" / "data"
registry._ctx.workspace_mode = "workspace"
registry._ctx.workspace_root = str(tmp_path / "workspace")
registry._ctx.task_metadata["child_drive_root"] = str(child_drive)
assert registry._ctx.task_drive_root() == data / "task_drives" / "task1"
def test_invalid_workspace_overlap_blocked_at_tool_boundary(tmp_path, monkeypatch):
monkeypatch.setattr("ouroboros.safety.check_safety", lambda *a, **k: (True, ""))
monkeypatch.setenv("OUROBOROS_RUNTIME_MODE", "light")
monkeypatch.setenv("ANTHROPIC_API_KEY", "test-key")
registry, repo, _data, _desktop = _registry_under_fake_home(tmp_path, monkeypatch)
registry._ctx.workspace_mode = "workspace"
registry._ctx.workspace_root = str(repo)
result = registry.execute("run_command", {"command": "echo forged-workspace-probe"})
assert "WORKSPACE_MODE_BLOCKED" in result
assert "overlaps" in result
assert "forged-workspace-probe" not in result
registry._ctx.workspace_root = str(pathlib.Path(str(repo).replace("Ouroboros", "ouroboros")))
case_result = registry.execute("run_command", {"command": "echo forged-workspace-probe"})
assert "WORKSPACE_MODE_BLOCKED" in case_result
assert "overlaps" in case_result
assert "forged-workspace-probe" not in case_result
def test_system_repo_write_targets_system_when_active_workspace_differs(tmp_path, monkeypatch):
monkeypatch.setattr("ouroboros.safety.check_safety", lambda *a, **k: (True, ""))
repo = tmp_path / "repo"
active = tmp_path / "workspace"
data = tmp_path / "data"
repo.mkdir()
active.mkdir()
registry = ToolRegistry(repo_dir=repo, drive_root=data)
registry._ctx.active_repo_dir = lambda: active
registry._ctx.system_repo_dir = str(repo)
result = registry.execute("write_file", {"root": "system_repo", "path": "x.txt", "content": "x"})
assert result.startswith("✅ Written")
assert "system_repo:x.txt" in result
assert not (active / "x.txt").exists()
assert (repo / "x.txt").read_text(encoding="utf-8") == "x"
@pytest.mark.serial
def test_light_mode_executes_interpreter_inline_repo_writes(tmp_path, monkeypatch):
monkeypatch.setattr("ouroboros.safety.check_safety", lambda *a, **k: (True, ""))
monkeypatch.setenv("OUROBOROS_RUNTIME_MODE", "light")
repo = tmp_path / "repo"
data = tmp_path / "data"
repo.mkdir()
registry = ToolRegistry(repo_dir=repo, drive_root=data)
result = registry.execute(
"run_script",
{"cwd": str(repo), "script": "open('tmp_probe_no_write', 'w').write('x')"},
)
assert "exit_code=0" in result, result
assert (repo / "tmp_probe_no_write").read_text(encoding="utf-8") == "x"
def test_light_mode_default_root_does_not_treat_repo_skills_path_as_payload(tmp_path, monkeypatch):
monkeypatch.setattr("ouroboros.safety.check_safety", lambda *a, **k: (True, ""))
monkeypatch.setenv("OUROBOROS_RUNTIME_MODE", "light")
repo = tmp_path / "repo"
data = tmp_path / "data"
(repo / "skills" / "external" / "alpha").mkdir(parents=True)
(data / "skills" / "external" / "alpha").mkdir(parents=True)
registry = ToolRegistry(repo_dir=repo, drive_root=data)
result = registry.execute(
"write_file",
{
"path": "skills/external/alpha/plugin.py",
"content": "x",
},
)
assert "LIGHT_MODE_BLOCKED" in result
assert not (repo / "skills" / "external" / "alpha" / "plugin.py").exists()
def test_get_runtime_mode_prefers_boot_baseline(monkeypatch):
from ouroboros import config as cfg
monkeypatch.setattr(cfg, "_BOOT_RUNTIME_MODE", "light", raising=True)
monkeypatch.setenv("OUROBOROS_RUNTIME_MODE", "pro")
assert cfg.get_runtime_mode() == "light"
# --- submarine unwind (2026-08-08): legible confinement + typed policy denials --
def test_user_files_block_names_subagent_projects_root(tmp_path, monkeypatch):
"""A refused coop-tree path names the ONE root that can actually reach it
(root=subagent_projects + the exact relative path) instead of four roots
that cannot (waves 2-3 rediscovery loop). Message-only: the root stays
read-only and the shell/write boundary is unchanged."""
registry, _repo, _data, _desktop = _registry_under_fake_home(tmp_path, monkeypatch)
home = pathlib.Path.home()
projects = home / "Ouroboros" / "projects"
(projects / "coop_abc123" / "render").mkdir(parents=True)
monkeypatch.setenv("OUROBOROS_SUBAGENT_PROJECTS_ROOT", str(projects))
result = registry.execute(
"list_files",
{"root": "user_files", "path": str(projects / "coop_abc123" / "render")},
)
assert "USER_FILES_PATH_BLOCKED" in result
assert "root=subagent_projects" in result
assert "coop_abc123" in result
# The suggested call actually works (read-only root, same profile).
listing = registry.execute(
"list_files", {"root": "subagent_projects", "path": "coop_abc123/render"}
)
assert "⚠️" not in listing
def test_user_files_read_block_status_is_policy_denial(tmp_path, monkeypatch):
"""The typed USER_FILES_PATH_BLOCKED status is partitioned as a policy denial
(v6.57.0): an unrecovered read-side confinement refusal lands in
execution.policy_denials, never unresolved/tool_failure."""
from ouroboros.loop_tool_execution import _extract_result_metadata
from ouroboros.outcomes import _POLICY_DENIAL_STATUSES, _classify_tool_errors
meta = _extract_result_metadata(
"list_files",
"⚠️ USER_FILES_PATH_BLOCKED: user_files path blocked: path overlaps ...",
True,
)
assert meta["status"] == "user_files_path_blocked"
assert meta["status"] in _POLICY_DENIAL_STATUSES
buckets = _classify_tool_errors({
"tool_calls": [
{"tool": "list_files", "is_error": True, "status": "user_files_path_blocked", "args": {}},
]
})
assert buckets["unresolved"] == []
assert len(buckets["policy_denials"]) == 1
def test_artifact_output_nudge_split_from_registration_failure():
"""The exit_code=0 undeclared-outputs NUDGE is typed apart from the real
'declared output registration failed' case: the nudge is a policy denial,
the registration failure stays a genuine blocking error."""
from ouroboros.loop_tool_execution import _extract_result_metadata, _is_tool_execution_failure
from ouroboros.outcomes import _BLOCKING_TOOL_STATUSES, _POLICY_DENIAL_STATUSES
nudge = (
"⚠️ ARTIFACT_OUTPUT_UNDECLARED: command appears to write user_files outputs "
"without declaring outputs=[...]. Paths: /x/y.txt.\n\nexit_code=0\n"
)
assert _is_tool_execution_failure(True, nudge) is True # honest ⚠️ in the trace
nudge_meta = _extract_result_metadata("run_command", nudge, True)
assert nudge_meta["status"] == "artifact_output_undeclared"
assert nudge_meta["status"] in _POLICY_DENIAL_STATUSES
real = "⚠️ ARTIFACT_OUTPUT_ERROR: command succeeded but declared output registration failed.\nexit_code=0\n"
real_meta = _extract_result_metadata("run_command", real, True)
assert real_meta["status"] == "artifact_output_error"
assert real_meta["status"] in _BLOCKING_TOOL_STATUSES
assert real_meta["status"] not in _POLICY_DENIAL_STATUSES
def test_python_cwd_failure_emits_canonical_cwd_message(tmp_path, monkeypatch):
"""A python/python3 launch with a confined cwd gets the SAME self-healing
SHELL_CWD_BLOCKED root list a non-python command gets — not an opaque
interpreter-provenance message (submarine waves 1/3, python3 -m http.server)."""
monkeypatch.setattr("ouroboros.safety.check_safety", lambda *a, **k: (True, ""))
registry, _repo, _data, _desktop = _registry_under_fake_home(tmp_path, monkeypatch)
home = pathlib.Path.home()
blocked_cwd = home / "Ouroboros" / "projects" / "coop_x"
blocked_cwd.mkdir(parents=True)
result = registry.execute(
"run_command",
{"cmd": ["python3", "-m", "http.server"], "cwd": str(blocked_cwd)},
)
assert "SHELL_CWD_BLOCKED" in result
assert "PYTHON_INTERPRETER_UNAVAILABLE" not in result
assert "task_drive=" in result # label=path root list (v6.54.3)
def test_service_cwd_failure_emits_canonical_cwd_message(tmp_path, monkeypatch):
monkeypatch.setattr("ouroboros.safety.check_safety", lambda *a, **k: (True, ""))
registry, _repo, _data, _desktop = _registry_under_fake_home(tmp_path, monkeypatch)
home = pathlib.Path.home()
blocked_cwd = home / "Ouroboros" / "projects" / "coop_y"
blocked_cwd.mkdir(parents=True)
result = registry.execute(
"start_service",
{"cmd": ["node", "server.js"], "name": "svc-cwd", "cwd": str(blocked_cwd)},
)
assert "SHELL_CWD_BLOCKED" in result
assert "SERVICE_CWD_ERROR" not in result
assert "task_drive=" in result
def test_preflight_review_rename_keeps_a_callable_unadvertised_alias(tmp_path):
"""Q1 rename: `preflight_review` is the advertised organ; `advisory_review`
stays CALLABLE as a compat alias (saved prompts/memories/configs keep
working) but never appears on the public schema surface, and a contract
that withholds either spelling silences both."""
from ouroboros.safety import POLICY_SKIP, TOOL_POLICY
from ouroboros.contracts.task_contract import build_task_contract
registry = ToolRegistry(repo_dir=tmp_path / "repo", drive_root=tmp_path / "data")
names = {schema["function"]["name"] for schema in registry.schemas()}
assert "preflight_review" in names
assert "advisory_review" not in names
assert "advisory_review" not in registry.available_tools()
# The core/heal envelope (schemas(core_only=True)) must not advertise the
# alias either — B1 class: the default path filtered it, the core path
# leaked it into heal-mode prompts.
core_names = {schema["function"]["name"] for schema in registry.schemas(core_only=True)}
assert "preflight_review" in core_names
assert "advisory_review" not in core_names
# The alias dispatches to the same organ (a refusal about tool identity
# would start with "Unknown tool").
assert not registry.execute("advisory_review", {}).startswith("⚠️ Unknown tool")
# And the unknown-tool refusal's Available listing never advertises it.
listing = registry.execute("definitely_not_a_tool", {})
assert "advisory_review" not in listing
# Policy rows exist for BOTH spellings (the alias call is policy-skipped
# exactly like the canonical one).
assert TOOL_POLICY.get("preflight_review") is POLICY_SKIP
assert TOOL_POLICY.get("advisory_review") is POLICY_SKIP
# disabled_tools compat: either spelling withholds both.
for withheld in ("advisory_review", "preflight_review"):
contract = build_task_contract({"description": "x", "disabled_tools": [withheld]})
reg = ToolRegistry(repo_dir=tmp_path / "repo", drive_root=tmp_path / "data")
reg._ctx.task_metadata = {"task_contract": contract}
listed = {schema["function"]["name"] for schema in reg.schemas()}
assert "preflight_review" not in listed