ouroboros/tests/test_workspace_authority_binding.py
Ouroboros b9bb1fe311 Restore ordinary conversation capabilities and complete chosen inputs
Keep independent native Main and Project turns addressable through post-task
work and model-access waits, with Project operations bound to their folder.
Separate web-tool restrictions from network access and preserve owner controls.

Deliver complete chosen delegation inputs and keep full operative plan sources
outside the bounded review-state index, retaining paid and legacy custody.

Co-authored-by: Ouroboros <311266734+ouroboros-agent@users.noreply.github.com>
2026-09-08 19:26:53 +03:00

531 lines
19 KiB
Python

from __future__ import annotations
import pathlib
import pytest
from ouroboros.tool_access import _POLICY, build_resolved_resource_binding
from ouroboros.tools.registry import ToolContext, ToolRegistry
_EXPECTED_TOP_LEVEL_POLICY = {
"active_workspace": {"read", "list", "search", "write", "edit", "shell", "vcs", "review", "service"},
"system_repo": {"read", "list", "search", "write", "edit", "shell", "vcs", "review", "service"},
"runtime_data": {"read", "list", "search", "write", "edit"},
"task_drive": {"read", "list", "write", "edit", "shell", "service"},
"skill_payload": {"read", "list", "search", "write", "edit", "review", "shell"},
"artifact_store": {"read", "list", "write", "shell", "service"},
"user_files": {"read", "list", "search", "write", "edit", "shell", "service"},
"subagent_projects": {"read", "list", "search"},
"deliverables": {"read", "list", "search"},
}
def test_ordinary_top_level_presets_share_one_exact_principal_matrix():
ordinary = ("workspace_task", "external_workspace_task", "self_modification")
for profile in ordinary:
assert _POLICY[profile] == _EXPECTED_TOP_LEVEL_POLICY
assert _POLICY[ordinary[0]] is _POLICY[ordinary[1]]
assert _POLICY[ordinary[1]] is _POLICY[ordinary[2]]
def test_shared_top_level_principal_does_not_widen_specialized_profiles():
assert "shell" not in _POLICY["local_readonly_subagent"]["skill_payload"]
assert "skill_repair" not in _POLICY
assert "skill_payload" not in _POLICY["acting_subagent"]
for profile in ("local_readonly_subagent", "acting_subagent"):
assert "search" not in _POLICY[profile]["runtime_data"]
assert "delegate" in _POLICY["operator_control"]["active_workspace"]
def test_private_binding_argument_is_rejected_at_public_boundary(tmp_path):
repo = tmp_path / "repo"
data = tmp_path / "data"
repo.mkdir()
data.mkdir()
registry = ToolRegistry(repo_dir=repo, drive_root=data)
result = registry.execute(
"read_file",
{"path": "README.md", "_resolved_binding": "model-forged"},
)
assert result.startswith("⚠️ TOOL_ARG_ERROR (read_file)"), result
assert "_resolved_binding" not in result
def test_public_unknown_argument_is_named_in_the_refusal(tmp_path):
"""A refusal that does not name the offending key leaves the caller guessing.
A PUBLIC unknown key is named; a private dispatch carrier still is not."""
repo = tmp_path / "repo"
data = tmp_path / "data"
repo.mkdir()
data.mkdir()
registry = ToolRegistry(repo_dir=repo, drive_root=data)
read_result = registry.execute("read_file", {"path": "README.md", "description": "x"})
assert "unsupported argument(s): description" in read_result, read_result
list_result = registry.execute("list_files", {"dir": "."})
assert "unsupported argument(s): dir" in list_result, list_result
def _skill(root: pathlib.Path, location: str, name: str) -> pathlib.Path:
skill_dir = root / "skills" / location / name
skill_dir.mkdir(parents=True)
(skill_dir / "SKILL.md").write_text(f"# {name}\n", encoding="utf-8")
return skill_dir
def test_binding_projects_markerless_native_as_external_from_forked_drive(tmp_path):
repo = tmp_path / "repo"
parent_data = tmp_path / "parent-data"
child_data = tmp_path / "child-data"
repo.mkdir()
child_data.mkdir()
native = _skill(parent_data, "native", "alpha")
(native / "notes.txt").write_text("canonical", encoding="utf-8")
ctx = ToolContext(
repo_dir=repo,
drive_root=child_data,
task_metadata={"budget_drive_root": str(parent_data)},
)
binding = build_resolved_resource_binding(
ctx,
root="skill_payload",
operation="read",
path="notes.txt",
bucket="native",
skill_name="alpha",
)
assert binding.base_path == native.resolve()
assert binding.target_path == (native / "notes.txt").resolve()
assert binding.state_drive_root == parent_data.resolve()
assert binding.source == "external"
assert child_data not in binding.target_path.parents
def test_binding_selects_configured_user_repo_without_migration(tmp_path, monkeypatch):
repo = tmp_path / "repo"
data = tmp_path / "data"
checkout = tmp_path / "skills-checkout"
repo.mkdir()
data.mkdir()
skill_dir = checkout / "group" / "beta"
skill_dir.mkdir(parents=True)
(skill_dir / "SKILL.md").write_text("# beta\n", encoding="utf-8")
(skill_dir / "tool.py").write_text("VALUE = 1\n", encoding="utf-8")
monkeypatch.setenv("OUROBOROS_SKILLS_REPO_PATH", str(checkout))
ctx = ToolContext(repo_dir=repo, drive_root=data)
binding = build_resolved_resource_binding(
ctx,
root="skill_payload",
operation="edit",
path="tool.py",
bucket="user_repo",
skill_name="beta",
)
assert binding.base_path == skill_dir.resolve()
assert binding.target_path == (skill_dir / "tool.py").resolve()
assert binding.source == "user_repo"
assert not (data / "skills" / "user_repo").exists()
def test_binding_collision_blocks_mutation_but_exact_read_stays_inspectable(tmp_path, monkeypatch):
repo = tmp_path / "repo"
data = tmp_path / "data"
checkout = tmp_path / "checkout"
repo.mkdir()
data_skill = _skill(data, "external", "same")
user_skill = checkout / "same"
user_skill.mkdir(parents=True)
(user_skill / "SKILL.md").write_text("# same\n", encoding="utf-8")
monkeypatch.setenv("OUROBOROS_SKILLS_REPO_PATH", str(checkout))
ctx = ToolContext(repo_dir=repo, drive_root=data)
read_binding = build_resolved_resource_binding(
ctx,
root="skill_payload",
operation="read",
bucket="external",
skill_name="same",
)
assert read_binding.base_path == data_skill.resolve()
with pytest.raises(ValueError, match="collision"):
build_resolved_resource_binding(
ctx,
root="skill_payload",
operation="write",
path="SKILL.md",
bucket="external",
skill_name="same",
)
assert not (data / "state" / "skills" / "same").exists()
@pytest.mark.parametrize("operation", ["read", "list", "search", "write", "edit", "shell", "vcs"])
def test_binding_preserves_the_project_room_target_for_every_operation(tmp_path, operation):
repo = tmp_path / "repo"
data = tmp_path / "data"
room = tmp_path / "room"
for path in (repo, data, room):
path.mkdir()
ctx = ToolContext(
repo_dir=repo,
drive_root=data,
is_direct_chat=True,
task_metadata={"_project_room_dir": str(room)},
)
binding = build_resolved_resource_binding(
ctx, root="active_workspace", operation=operation, path="README.md"
)
assert binding.base_path == room.resolve()
def test_binding_synthesizes_only_manifest_first_external_write_target(tmp_path):
repo = tmp_path / "repo"
data = tmp_path / "data"
repo.mkdir()
data.mkdir()
ctx = ToolContext(repo_dir=repo, drive_root=data)
binding = build_resolved_resource_binding(
ctx,
root="skill_payload",
operation="write",
path="SKILL.md",
bucket="external",
skill_name="new-skill",
)
assert binding.base_path == (data / "skills" / "external" / "new-skill").resolve()
assert not binding.base_path.exists()
with pytest.raises(ValueError, match="not found"):
build_resolved_resource_binding(
ctx,
root="skill_payload",
operation="write",
path="SKILL.md",
bucket="clawhub",
skill_name="missing",
)
def test_registry_builds_once_and_injects_the_same_private_object(tmp_path, monkeypatch):
import ouroboros.safety as safety
import ouroboros.tools.registry as registry_module
repo = tmp_path / "repo"
data = tmp_path / "data"
repo.mkdir()
data.mkdir()
(repo / "README.md").write_text("hello\n", encoding="utf-8")
registry = ToolRegistry(repo_dir=repo, drive_root=data)
original = registry_module.build_resolved_resource_binding
built = []
observed = []
def counted(*args, **kwargs):
value = original(*args, **kwargs)
built.append(value)
return value
def handler(ctx, path, root="active_workspace", _resolved_binding=None):
observed.append(_resolved_binding)
return "OK"
monkeypatch.setattr(registry_module, "build_resolved_resource_binding", counted)
monkeypatch.setattr(safety, "check_safety", lambda *args, **kwargs: (True, ""))
registry.override_handler("read_file", handler)
assert registry.execute("read_file", {"path": "README.md"}) == "OK"
assert len(built) == 1
assert observed == [built[0]]
def test_forged_private_argument_is_rejected_before_binding(tmp_path, monkeypatch):
import ouroboros.tools.registry as registry_module
repo = tmp_path / "repo"
data = tmp_path / "data"
repo.mkdir()
data.mkdir()
registry = ToolRegistry(repo_dir=repo, drive_root=data)
calls = []
monkeypatch.setattr(
registry_module,
"build_resolved_resource_binding",
lambda *args, **kwargs: calls.append((args, kwargs)),
)
result = registry.execute(
"read_file", {"path": "README.md", "_resolved_binding": "forged"}
)
assert result.startswith("⚠️ TOOL_ARG_ERROR (read_file)")
assert calls == []
def test_target_sensitive_override_without_private_keyword_fails_loudly(tmp_path, monkeypatch):
import ouroboros.safety as safety
repo = tmp_path / "repo"
data = tmp_path / "data"
repo.mkdir()
data.mkdir()
registry = ToolRegistry(repo_dir=repo, drive_root=data)
monkeypatch.setattr(safety, "check_safety", lambda *args, **kwargs: (True, ""))
registry.override_handler("read_file", lambda ctx, path: "must not run")
result = registry.execute("read_file", {"path": "README.md"})
assert result.startswith("⚠️ TOOL_INTERNAL_ERROR (read_file)")
assert "_resolved_binding" in result
def test_direct_handler_fallback_builds_once(tmp_path, monkeypatch):
import ouroboros.tools.core_file_tools as core
repo = tmp_path / "repo"
data = tmp_path / "data"
repo.mkdir()
data.mkdir()
(repo / "README.md").write_text("direct\n", encoding="utf-8")
ctx = ToolContext(repo_dir=repo, drive_root=data)
original = core.build_resolved_resource_binding
calls = []
def counted(*args, **kwargs):
calls.append(kwargs)
return original(*args, **kwargs)
monkeypatch.setattr(core, "build_resolved_resource_binding", counted)
result = core._read_file(ctx, "README.md")
assert "direct" in result
assert len(calls) == 1
def test_write_batch_carries_ordered_binding_tuple(tmp_path, monkeypatch):
import ouroboros.safety as safety
repo = tmp_path / "repo"
data = tmp_path / "data"
repo.mkdir()
data.mkdir()
registry = ToolRegistry(repo_dir=repo, drive_root=data)
observed = []
def handler(ctx, files=None, _resolved_binding=None, **kwargs):
observed.extend(_resolved_binding)
return "OK"
monkeypatch.setattr(safety, "check_safety", lambda *args, **kwargs: (True, ""))
registry.override_handler("write_file", handler)
result = registry.execute("write_file", {"files": [
{"path": "b.txt", "content": "b"},
{"path": "a.txt", "content": "a"},
]})
assert result == "OK"
assert [item.target_path.name for item in observed] == ["b.txt", "a.txt"]
def test_explicit_skill_root_bypasses_repo_named_directories(tmp_path, monkeypatch):
import ouroboros.safety as safety
repo = tmp_path / "repo"
data = tmp_path / "data"
repo.mkdir()
data.mkdir()
(repo / "scripts").mkdir()
(repo / "tests").mkdir()
skill = _skill(data, "external", "alpha")
(skill / "scripts").mkdir()
(skill / "tests").mkdir()
(skill / "scripts" / "tool.py").write_text("SKILL_MARKER = 1\n", encoding="utf-8")
(skill / "tests" / "test_x.py").write_text("old = 1\n", encoding="utf-8")
registry = ToolRegistry(repo_dir=repo, drive_root=data)
monkeypatch.setattr(safety, "check_safety", lambda *args, **kwargs: (True, ""))
selector = {"root": "skill_payload", "bucket": "external", "skill_name": "alpha"}
read = registry.execute("read_file", {**selector, "path": "scripts/tool.py"})
listing = registry.execute("list_files", {**selector, "path": "tests"})
search = registry.execute("search_code", {
**selector, "path": "scripts", "query": "SKILL_MARKER",
})
written = registry.execute("write_file", {
**selector, "path": "scripts/new.py", "content": "VALUE = 2\n",
})
edited = registry.execute("edit_text", {
**selector, "path": "tests/test_x.py", "old_str": "old = 1", "new_str": "new = 2",
})
assert "SKILL_MARKER" in read
assert "test_x.py" in listing
assert "SKILL_MARKER" in search
assert written.startswith("OK: wrote")
assert "Replaced" in edited
assert (skill / "scripts" / "new.py").read_text(encoding="utf-8") == "VALUE = 2\n"
assert (skill / "tests" / "test_x.py").read_text(encoding="utf-8") == "new = 2\n"
assert not (repo / "scripts" / "new.py").exists()
def test_query_code_explicit_skill_root_is_reachable_through_registry(tmp_path, monkeypatch):
import ouroboros.safety as safety
repo = tmp_path / "repo"
data = tmp_path / "data"
repo.mkdir()
data.mkdir()
skill = _skill(data, "external", "alpha")
(skill / "worker.py").write_text("class SkillWorker:\n pass\n", encoding="utf-8")
registry = ToolRegistry(repo_dir=repo, drive_root=data)
monkeypatch.setattr(safety, "check_safety", lambda *args, **kwargs: (True, ""))
result = registry.execute("query_code", {
"op": "symbols",
"root": "skill_payload",
"bucket": "external",
"skill_name": "alpha",
"path": ".",
})
assert "SkillWorker" in result
assert "worker.py" in result
def test_selectors_without_skill_root_do_not_retarget_workspace(tmp_path, monkeypatch):
import ouroboros.safety as safety
repo = tmp_path / "repo"
data = tmp_path / "data"
repo.mkdir()
data.mkdir()
(repo / "module.py").write_text("repo = 1\n", encoding="utf-8")
skill = _skill(data, "external", "alpha")
(skill / "module.py").write_text("skill = 1\n", encoding="utf-8")
registry = ToolRegistry(repo_dir=repo, drive_root=data)
monkeypatch.setattr(safety, "check_safety", lambda *args, **kwargs: (True, ""))
result = registry.execute("edit_text", {
"path": "module.py", "old_str": "repo = 1", "new_str": "repo = 2",
"bucket": "external", "skill_name": "alpha",
})
assert "Replaced" in result
assert (repo / "module.py").read_text(encoding="utf-8") == "repo = 2\n"
assert (skill / "module.py").read_text(encoding="utf-8") == "skill = 1\n"
def test_explicit_user_repo_keeps_existing_payload_sidecar_block(tmp_path, monkeypatch):
import ouroboros.safety as safety
repo = tmp_path / "repo"
data = tmp_path / "data"
checkout = tmp_path / "checkout"
repo.mkdir()
data.mkdir()
skill = checkout / "alpha"
skill.mkdir(parents=True)
(skill / "SKILL.md").write_text("# alpha\n", encoding="utf-8")
(skill / "tool.py").write_text("VALUE = 1\n", encoding="utf-8")
monkeypatch.setenv("OUROBOROS_SKILLS_REPO_PATH", str(checkout))
monkeypatch.setattr(safety, "check_safety", lambda *args, **kwargs: (True, ""))
registry = ToolRegistry(repo_dir=repo, drive_root=data)
edited = registry.execute("edit_text", {
"root": "skill_payload", "bucket": "user_repo", "skill_name": "alpha",
"path": "tool.py", "old_str": "VALUE = 1", "new_str": "VALUE = 2",
})
result = registry.execute("write_file", {
"root": "skill_payload", "bucket": "user_repo", "skill_name": "alpha",
"path": ".clawhub.json", "content": "{}\n",
})
assert "Replaced" in edited
assert (skill / "tool.py").read_text(encoding="utf-8") == "VALUE = 2\n"
assert result.startswith("⚠️ DATA_WRITE_BLOCKED")
assert not (skill / ".clawhub.json").exists()
def test_skill_repair_explicit_root_infers_its_existing_selector(tmp_path, monkeypatch):
from ouroboros.contracts.task_constraint import TaskConstraint
repo = tmp_path / "repo"
data = tmp_path / "data"
repo.mkdir()
data.mkdir()
skill = _skill(data, "external", "alpha")
(skill / "notes.txt").write_text("repair target\n", encoding="utf-8")
registry = ToolRegistry(repo_dir=repo, drive_root=data)
registry._ctx.task_constraint = TaskConstraint(
mode="skill_repair", skill_name="alpha", payload_root="skills/external/alpha"
)
result = registry.execute(
"read_file", {"root": "skill_payload", "path": "notes.txt"}
)
assert "repair target" in result
def test_registry_tool_resolution_owner_facades_preserve_identity():
"""The tool_resolution extraction is a semantic no-op: the registry facade
re-exports the exact objects, and the characterized signatures hold.
Carried from the v7 reference (ouroboros_v7_wip @ 9f691656); the reference's
companion test of the TYPED dispatch-path projection
(``_normalize_dispatch_path_args_result``) is deliberately NOT carried —
that machinery is part of the deferred typed-result cutover and this tree
keeps the upstream string-returning body.
"""
import inspect
from ouroboros.tools import registry, tool_resolution
names = (
"_coerce_real_path",
"active_repo_dir_for",
"system_repo_dir_for",
"_PATH_NORMALIZED_TOOLS",
"_normalize_dispatch_path_args",
"_GENERIC_VCS_TARGET_TOOLS",
"_TARGET_BINDING_OPERATIONS",
"_SKILL_LIFECYCLE_TARGET_TOOLS",
"_PROCESS_TARGET_TOOLS",
"_VERIFY_RUN_KINDS",
"_target_binding_operation",
"_build_builtin_target_binding",
"_binding_items",
"_binding_set_targets_system_repo",
"_binding_set_is_light_restricted",
)
for name in names:
assert getattr(registry, name) is getattr(tool_resolution, name)
callables = {
"_coerce_real_path": "(value: 'Any') -> 'pathlib.Path | None'",
"active_repo_dir_for": "(ctx: 'Any') -> 'pathlib.Path'",
"system_repo_dir_for": "(ctx: 'Any') -> 'pathlib.Path'",
"_normalize_dispatch_path_args": "(ctx: 'Any', name: 'str', args: 'Dict[str, Any]') -> 'str'",
"_target_binding_operation": "(name: 'str', args: 'dict[str, Any]') -> 'str | None'",
"_build_builtin_target_binding": "(ctx: 'Any', name: 'str', args: 'dict[str, Any]') -> 'Any'",
"_binding_items": "(binding: 'Any') -> 'tuple[Any, ...]'",
"_binding_set_targets_system_repo": "(ctx: 'Any', binding: 'Any') -> 'bool'",
"_binding_set_is_light_restricted": "(ctx: 'Any', binding: 'Any') -> 'bool'",
}
assert {
name: str(inspect.signature(getattr(tool_resolution, name)))
for name in callables
} == callables