mirror of
https://github.com/razzant/ouroboros.git
synced 2026-10-02 19:58:46 +00:00
616 lines
31 KiB
Python
616 lines
31 KiB
Python
"""The access profile a delegated run may hold, and the guards that keep it there.
|
|
|
|
Split verbatim out of ``tests/test_delegated_subagent_transport.py`` by theme. This
|
|
module owns what a mutating and a read-only child may ask the harness for, why an
|
|
effective profile is verified rather than assumed, the task that alone may touch a
|
|
live run, and the write-root and workspace refusals that precede any daemon call.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import datetime
|
|
import json
|
|
import pytest
|
|
from ouroboros.config import CLAUDEXOR_DELEGATED_MARKER_MIN_VERSION
|
|
|
|
from tests._delegated_transport_shared import ( # noqa: F401 (autouse fixture applies on import)
|
|
_delegating_ctx,
|
|
_owned_gateway_uses_each_test_transport,
|
|
_started_request,
|
|
)
|
|
|
|
|
|
def test_a_mutating_child_runs_live_in_a_private_snapshot_not_the_shared_tree(tmp_path, monkeypatch):
|
|
# C1: `live` still means the harness edits its scope root in place — but that root
|
|
# is a PRIVATE execution snapshot of the nanny's write root. The shared tree gets
|
|
# nothing until the nanny explicitly integrates the captured diff.
|
|
import pathlib as _pl
|
|
|
|
request, payload = _started_request(tmp_path, acting=True, monkeypatch=monkeypatch)
|
|
assert request["access"] == "workspace_write"
|
|
assert request["mode"] == "agent"
|
|
assert request["execution"] == {"isolation": "live", "delegated": True}
|
|
worktree = tmp_path.parent / f"wt-{tmp_path.name}"
|
|
assert request["scope"]["kind"] == "project"
|
|
scope_root = _pl.Path(str(request["scope"]["root"]))
|
|
assert scope_root.resolve() != worktree.resolve(), "the run must NEVER scope the shared tree"
|
|
assert scope_root.resolve().is_relative_to((tmp_path / "snap_root").resolve())
|
|
assert payload["execution_root"] == str(request["scope"]["root"])
|
|
assert _pl.Path(payload["authority_target_root"]).resolve() == worktree.resolve()
|
|
assert payload["baseline_id"], "the baseline commit is the binding's third leg"
|
|
# The snapshot genuinely carries the target's current state.
|
|
assert (scope_root / "README.md").read_text(encoding="utf-8") == "seed\n"
|
|
assert payload["access"] == "workspace_write" and payload["isolation"] == "live"
|
|
|
|
|
|
def test_a_read_only_child_uses_the_same_transport_with_a_narrower_profile(tmp_path, monkeypatch):
|
|
# One nanny, one transport: the ONLY difference is the derived profile and the run
|
|
# shape it implies. `execution.isolation='live'` is agent-only in Claudexor — a
|
|
# non-agent run carrying it is refused at the boundary — and a read-only child has
|
|
# nothing to write back anyway.
|
|
request, payload = _started_request(tmp_path, acting=False, monkeypatch=monkeypatch)
|
|
assert request["access"] == "readonly"
|
|
assert request["mode"] == "ask"
|
|
assert "execution" not in request
|
|
assert payload["access"] == "readonly"
|
|
|
|
|
|
@pytest.mark.parametrize("named_default", [
|
|
{"directory_strategy": "direct"},
|
|
{"scope_paths": []},
|
|
{"directory_strategy": "direct", "scope_paths": []},
|
|
])
|
|
def test_naming_the_documented_default_starts_exactly_like_omitting_it(
|
|
tmp_path, monkeypatch, named_default,
|
|
):
|
|
"""#882: `direct` IS what omission means, and `[]` selects nothing.
|
|
|
|
A read-only child never opens an ordinary-folder session, so neither argument
|
|
can change its run — yet the presence test they used to meet refused the start
|
|
before any POST, and the startup receipt forbade the child any other substrate.
|
|
The first real use of the parameter (a read-only auditor naming the documented
|
|
default) died unrun and still cost two paid rounds. Naming a default is not a
|
|
different request.
|
|
"""
|
|
roots = [tmp_path / "omit", tmp_path / "named"]
|
|
for root in roots:
|
|
root.mkdir()
|
|
omitted, _ = _started_request(roots[0], acting=False, monkeypatch=monkeypatch)
|
|
explicit, payload = _started_request(
|
|
roots[1], acting=False, monkeypatch=monkeypatch, start_kwargs=named_default)
|
|
assert "execution" not in explicit
|
|
assert payload["access"] == "readonly"
|
|
assert {key: value for key, value in explicit.items() if key != "scope"} == {
|
|
key: value for key, value in omitted.items() if key != "scope"}
|
|
|
|
|
|
@pytest.mark.parametrize("geometry", [
|
|
{"scope_paths": ["src"]},
|
|
{"directory_strategy": "direct", "scope_paths": ["src"]},
|
|
{"directory_strategy": "copy", "scope_paths": ["."]},
|
|
])
|
|
def test_real_geometry_on_a_read_only_child_refuses_before_the_daemon(
|
|
tmp_path, monkeypatch, geometry,
|
|
):
|
|
"""A REAL geometry request still refuses — typed, unrun, and repairable.
|
|
|
|
`copy` or a selected footprint asks for something a read-only child cannot do,
|
|
so the refusal stands. What it must carry: `definitely_unrun` (the host provably
|
|
started nothing, so the child ends at $0 instead of waking the model with a fault
|
|
it cannot act on), a durable start-blocked row (the child's own evidence used to
|
|
read "delegate_start never attempted" over a call the registry saw), and a repair
|
|
the parent can apply — omit the arguments — rather than "an ordinary writable
|
|
folder", which sends an auditor looking for a mutating session it never needed.
|
|
"""
|
|
from ouroboros import delegate_custody as custody
|
|
from ouroboros.delegate_evidence import START_BLOCKED
|
|
from ouroboros.gateways import claudexor as _gw
|
|
from ouroboros.tools import delegate
|
|
|
|
reached = []
|
|
|
|
class _NeverReached:
|
|
def handshake(self, **_kw): reached.append("handshake"); return {}
|
|
def close(self): pass
|
|
|
|
monkeypatch.setattr(_gw, "ClaudexorGateway", lambda *a, **k: _NeverReached())
|
|
monkeypatch.setenv("OUROBOROS_SUBAGENT_HARNESS", "some-route=weak-model:low")
|
|
ctx = _delegating_ctx(tmp_path, acting=False, task_id="t-geometry")
|
|
refused = json.loads(delegate._delegate_start(ctx, "audit the folder", **geometry).text)
|
|
assert refused["status"] == "refused"
|
|
assert refused["reason"] == "directory_execution_unavailable"
|
|
assert refused["definitely_unrun"] is True
|
|
assert "omit directory_strategy and scope_paths" in refused["detail"]
|
|
assert reached == [], "an argument refusal never reaches the daemon"
|
|
custody._CUSTODY.clear()
|
|
rows = [json.loads(line) for line in
|
|
custody.event_log_path(tmp_path).read_text(encoding="utf-8").splitlines()]
|
|
blocked = [row for row in rows if row.get("type") == START_BLOCKED]
|
|
assert [row["reason"] for row in blocked] == ["directory_execution_unavailable"]
|
|
assert blocked[0]["task_id"] == "t-geometry"
|
|
|
|
|
|
def test_the_host_states_its_prohibitions_on_every_delegated_run(tmp_path, monkeypatch):
|
|
request, _ = _started_request(tmp_path, acting=True, monkeypatch=monkeypatch)
|
|
instructions = request["instructions"].lower()
|
|
assert "git commit" in instructions and "outside this root" in instructions
|
|
|
|
|
|
def test_the_model_has_no_argument_that_could_widen_the_profile():
|
|
from ouroboros.tools import delegate
|
|
|
|
entry = next(e for e in delegate.get_tools() if e.name == "delegate_start")
|
|
properties = set(entry.schema["parameters"]["properties"])
|
|
# `retry_of` names an INVOCATION, not authority (ownership-checked replay);
|
|
# `continue_from` names this task's OWN settled run (custody-checked, same
|
|
# executor and authority, #1196); root/bucket/skill_name are a SELECTOR
|
|
# resolved through the same ResolvedResourceBinding authorizer as ordinary
|
|
# writes (R1 item 9).
|
|
assert properties == {
|
|
"prompt", "subagent_id", "max_seconds", "retry_of", "continue_from", "root", "bucket",
|
|
"skill_name", "directory_strategy", "scope_paths", "access",
|
|
}
|
|
assert entry.schema["parameters"]["properties"]["root"]["enum"] == ["skill_payload"]
|
|
assert entry.schema["parameters"]["properties"]["access"]["enum"] == ["readonly", "workspace_write"]
|
|
assert not properties & {"mode", "isolation", "scope", "write_surface", "cwd"}
|
|
|
|
|
|
def test_a_read_only_task_cannot_obtain_workspace_write(tmp_path):
|
|
from ouroboros.contracts.task_constraint import TaskConstraint
|
|
from ouroboros.tools.delegate import _derive_authority
|
|
from ouroboros.tools.registry import ToolContext
|
|
|
|
for constraint in (
|
|
None, # no constraint at all
|
|
TaskConstraint(mode="local_readonly_subagent"), # explicitly read-only
|
|
TaskConstraint(mode="acting_subagent", surface=""), # acting but unresolved surface
|
|
TaskConstraint(mode="acting_subagent", surface="bogus"), # acting with an invalid surface
|
|
):
|
|
ctx = ToolContext(repo_dir=tmp_path, drive_root=tmp_path, task_constraint=constraint)
|
|
ctx.task_metadata = {"parent_task_id": "p"}
|
|
authority = _derive_authority(ctx)
|
|
assert authority.access == "readonly", constraint
|
|
assert authority.mode == "ask" and authority.isolation == ""
|
|
|
|
|
|
@pytest.mark.parametrize("effective,entitled,widened", [
|
|
("readonly", "readonly", ""),
|
|
("readonly", "workspace_write", ""), # narrower than asked is fine
|
|
("workspace_write", "workspace_write", ""),
|
|
("workspace_write", "readonly", "workspace_write"),
|
|
("full", "workspace_write", "full"),
|
|
("inherit_native", "workspace_write", "inherit_native"),
|
|
("a-profile-from-a-future-engine", "workspace_write", "a-profile-from-a-future-engine"),
|
|
])
|
|
def test_effective_access_is_verified_not_assumed(effective, entitled, widened):
|
|
from ouroboros.tools.delegate import _widened_access
|
|
|
|
detail = {"lastSeq": 12, "summary": {"effectiveAccess": effective, "state": "running"}}
|
|
assert _widened_access(detail, entitled) == widened
|
|
|
|
|
|
def test_an_undisclosed_effective_profile_is_unverified_not_compliant():
|
|
"""Absence of evidence is not evidence of narrowness.
|
|
|
|
An earlier version returned "" (compliant) whenever the field was missing, and a test
|
|
codified that as `# not disclosed yet: nothing to judge` — so any daemon build, harness
|
|
or malformed response that omitted the field turned the only containment gate into a
|
|
silent no-op while the run kept writing. It also fell back to `summary["access"]`,
|
|
which the daemon computes as `effectiveAccess ?? the client's own request`: that
|
|
compares our request against itself and can only ever pass.
|
|
"""
|
|
from ouroboros.tools.delegate import _ACCESS_UNVERIFIED, _widened_access
|
|
|
|
# Before admission there really is nothing to judge.
|
|
assert _widened_access({"summary": {"state": "queued"}}, "readonly") == ""
|
|
assert _widened_access({"summary": {}}, "readonly") == ""
|
|
|
|
# Absence only means "no evidence" while the run can still ACT, and only after it
|
|
# has produced anything. The daemon marks a run `running` at DEQUEUE — before the
|
|
# orchestrator writes the contract the profile is derived from — so judging that
|
|
# moment cancelled healthy runs, and judging a terminal state reported a run that
|
|
# merely failed to start as a containment breach.
|
|
assert _widened_access({"lastSeq": 0, "summary": {"state": "running"}}, "readonly") == ""
|
|
for state in ("succeeded", "failed", "cancelled", "interrupted"):
|
|
detail = {"lastSeq": 40, "summary": {"state": state}}
|
|
assert _widened_access(detail, "readonly") == "", state
|
|
|
|
# A live run that HAS produced events and still discloses nothing has no evidence.
|
|
live = {"lastSeq": 12, "summary": {"state": "running"}}
|
|
assert _widened_access(live, "readonly") == _ACCESS_UNVERIFIED
|
|
|
|
# The echo must not be accepted as an independent witness.
|
|
detail = {"lastSeq": 12, "summary": {"state": "running", "access": "workspace_write"}}
|
|
assert _widened_access(detail, "workspace_write") == _ACCESS_UNVERIFIED
|
|
|
|
# A really widened profile is still caught in every state.
|
|
for state in ("running", "succeeded"):
|
|
detail = {"lastSeq": 12, "summary": {"state": state, "effectiveAccess": "full"}}
|
|
assert _widened_access(detail, "readonly") == "full", state
|
|
|
|
|
|
def test_a_succeeded_run_that_never_proved_its_profile_says_so_in_its_result():
|
|
"""P34P1.4: a SUCCEEDED run whose summary carries no `effectiveAccess` was accepted
|
|
as compliant — a result with no evidence that the profile the host asked for is the
|
|
profile the engine enforced, which is the name-without-proof class this module
|
|
exists to refuse.
|
|
|
|
Enforcement is NOT the answer for a finished run: it is over, there is nothing left
|
|
to contain, and routing absence through the breach path would CANCEL a succeeded run
|
|
and destroy the very result the lane exists to fetch (the v6.87.37 lesson — the
|
|
containment gate stopped cancelling healthy runs for exactly this reason). So it is
|
|
DISCLOSED, on the same terminal payload the parent reads, like the HOME half's
|
|
missing fact. Both lanes get it: `readonly` staying `readonly` is the profile that
|
|
matters most, and the `containment` block is asked only of marker-carrying runs."""
|
|
from ouroboros.subagents import delegated_run_shape
|
|
from ouroboros.tools.delegate import _terminal_payload
|
|
|
|
# A succeeded run with NO disclosed profile: unverified, and it says why.
|
|
silent = {"lastSeq": 40, "summary": {"state": "succeeded"}}
|
|
evidence = _terminal_payload("run-1", silent, delegated_run_shape(False))["access_evidence"]
|
|
assert evidence["verified"] is False and evidence["effective"] == ""
|
|
assert evidence["requested"] == "readonly" and evidence["state"] == "succeeded"
|
|
assert "SUCCEEDED without ever disclosing" in evidence["note"]
|
|
|
|
# A succeeded run that DID disclose one is verified, with no note.
|
|
proven = {"lastSeq": 40, "summary": {"state": "succeeded", "effectiveAccess": "readonly"}}
|
|
evidence = _terminal_payload("run-1", proven, delegated_run_shape(False))["access_evidence"]
|
|
assert evidence == {"requested": "readonly", "effective": "readonly",
|
|
"verified": True, "state": "succeeded"}
|
|
|
|
# A run that did NOT succeed keeps the softer wording: it may never have had a
|
|
# profile at all, so this is absence of evidence rather than a missing proof.
|
|
for state in ("failed", "cancelled", "interrupted"):
|
|
detail = {"lastSeq": 40, "summary": {"state": state}}
|
|
evidence = _terminal_payload("run-1", detail, delegated_run_shape(False))["access_evidence"]
|
|
assert evidence["verified"] is False, state
|
|
assert "absence of evidence, not a breach" in evidence["note"], state
|
|
|
|
# The ECHO is never a witness: the daemon computes `access` as
|
|
# `effectiveAccess ?? our own request`, so a payload carrying only the echo must
|
|
# still read unverified.
|
|
echo = {"lastSeq": 40, "summary": {"state": "succeeded", "access": "readonly"}}
|
|
assert _terminal_payload("run-1", echo, delegated_run_shape(False))[
|
|
"access_evidence"]["verified"] is False
|
|
|
|
# The mutating lane carries BOTH halves, and neither displaces the other.
|
|
mutating = _terminal_payload("run-1", silent, delegated_run_shape(True))
|
|
assert mutating["access_evidence"]["verified"] is False
|
|
assert mutating["containment"]["verified"] is False
|
|
|
|
|
|
def test_a_mutating_run_requires_an_ACTIVE_workspace_not_merely_agreement(tmp_path):
|
|
"""Agreement alone reopened the critical it was written to close.
|
|
|
|
`active_repo_dir_for` falls back to `repo_dir` when workspace mode is off, so a
|
|
constraint whose `write_root` happens to name that same directory made the equality
|
|
check pass — and handed an external shell the live repository, which is exactly the
|
|
original defect.
|
|
"""
|
|
from ouroboros.contracts.task_constraint import TaskConstraint
|
|
from ouroboros.subagents import delegated_run_shape
|
|
from ouroboros.tools.delegate import _mutation_authority
|
|
from ouroboros.tools.registry import ToolContext
|
|
|
|
repo = tmp_path / "repo"
|
|
repo.mkdir()
|
|
ctx = ToolContext(
|
|
repo_dir=repo, drive_root=tmp_path,
|
|
task_constraint=TaskConstraint(mode="acting_subagent", surface="self_worktree",
|
|
write_root=str(repo)),
|
|
)
|
|
ctx.workspace_root = None
|
|
ctx.workspace_mode = ""
|
|
record, refusal = _mutation_authority(
|
|
ctx, delegated_run_shape(True))
|
|
assert refusal and "workspace_not_active" in refusal.text, refusal
|
|
assert record == {}
|
|
|
|
|
|
def test_a_widened_run_is_cancelled_and_typed_not_reported_as_progress(tmp_path, monkeypatch):
|
|
import ouroboros.tools.delegate as delegate
|
|
from ouroboros.gateways import claudexor as gw
|
|
|
|
cancelled = {}
|
|
|
|
class _Stub:
|
|
def handshake(self, **_kw): return {}
|
|
def get_run(self, rid, **_kw):
|
|
return {"lastSeq": 7, "summary": {
|
|
"state": "cancelled" if cancelled else "running",
|
|
"effectiveAccess": "full",
|
|
}}
|
|
def cancel_run(self, rid, reason=""):
|
|
cancelled["reason"] = reason
|
|
return {"accepted": True}
|
|
def remove_project(self, pid): pass
|
|
def close(self): pass
|
|
|
|
monkeypatch.setattr(gw, "ClaudexorGateway", lambda *a, **k: _Stub())
|
|
ctx = _delegating_ctx(tmp_path, acting=True)
|
|
delegate._CUSTODY.clear()
|
|
delegate._CUSTODY["run-1"] = delegate._RunCustody(
|
|
task_id="t-nanny", route_id="some-route", model="m",
|
|
project_id="prj", project_owned=False,
|
|
)
|
|
out = json.loads(delegate._delegate_wait(ctx, "run-1", wait_sec=1))
|
|
delegate._CUSTODY.clear()
|
|
assert out["status"] == "refused"
|
|
assert out["reason"] == "access_profile_widened"
|
|
assert out["effective_access"] == "full" and out["entitled_access"] == "workspace_write"
|
|
assert cancelled["reason"] == "access_profile_widened"
|
|
|
|
|
|
def test_a_delegated_run_can_only_be_touched_by_the_task_that_started_it(tmp_path):
|
|
"""The daemon bearer token grants the ENTIRE Claudexor API, so naming a run is
|
|
reaching it. Without custody binding, a child could pass any run id it observed and
|
|
read — or CANCEL — the owner's own unrelated work, or a sibling reviewer's run, and
|
|
cancelling a reviewer destroys the verdict that was the whole point of running it."""
|
|
import json
|
|
|
|
import ouroboros.tools.delegate as delegate
|
|
from ouroboros.tools.registry import ToolContext
|
|
|
|
def _ctx(task_id):
|
|
ctx = ToolContext(repo_dir=tmp_path, drive_root=tmp_path)
|
|
ctx.task_id = task_id
|
|
ctx.task_metadata = {"root_task_id": task_id}
|
|
return ctx
|
|
|
|
delegate._CUSTODY.clear()
|
|
delegate._CUSTODY["run-mine"] = delegate._RunCustody(
|
|
task_id="task-a", route_id="codex", model="m", project_id="prj", project_owned=False,
|
|
)
|
|
|
|
for tool, call in (
|
|
("delegate_wait", lambda ctx, rid: delegate._delegate_wait(ctx, rid, wait_sec=1)),
|
|
# delegate_wait is the tick contract (str); cancel answers natively.
|
|
("delegate_cancel", lambda ctx, rid: delegate._delegate_cancel(ctx, rid, reason="x").text),
|
|
):
|
|
# A run with NO durable start record anywhere: ownership is UNKNOWN, which is a
|
|
# different fact from "demonstrably someone else's" and is refused on its own name.
|
|
out = json.loads(call(_ctx("task-a"), "run-someone-elses"))
|
|
assert out["status"] == "refused", (tool, out)
|
|
assert out["reason"] == "run_ownership_unknown", (tool, out)
|
|
|
|
# A run a SIBLING task started in the same worker process.
|
|
out = json.loads(call(_ctx("task-b"), "run-mine"))
|
|
assert out["status"] == "refused", (tool, out)
|
|
assert out["reason"] == "run_not_owned", (tool, out)
|
|
|
|
delegate._CUSTODY.clear()
|
|
|
|
|
|
def test_a_mutating_run_is_refused_when_the_root_and_the_granted_write_root_disagree(tmp_path, monkeypatch):
|
|
"""AUTHORITY and ROOT came from two different predicates and were never compared.
|
|
|
|
Authority comes from `task_constraint` via `active_tool_profile`. The root came from
|
|
`active_repo_dir_for`, and `ToolContext.active_repo_dir()` falls back to `repo_dir` —
|
|
the LIVE Ouroboros source tree — whenever `is_workspace_mode()` is false, which
|
|
`workspace_mode_block_reason` makes happen for a worktree overlapping the repo or the
|
|
data drive, or for a task record missing its workspace fields. In that state the host
|
|
would have handed an external SHELL `workspace_write` on its own repository, and no
|
|
per-tool guard applies because a shell is not a tool. Two independent reviewers found
|
|
this on the same branch.
|
|
"""
|
|
import json
|
|
|
|
import ouroboros.tools.delegate as delegate
|
|
from ouroboros.contracts.task_constraint import TaskConstraint
|
|
from ouroboros.gateways import claudexor as gw
|
|
from ouroboros.tools.registry import ToolContext
|
|
|
|
class _Stub:
|
|
engine_version = CLAUDEXOR_DELEGATED_MARKER_MIN_VERSION
|
|
|
|
def handshake(self, **_kw): return {}
|
|
def agent_capabilities(self):
|
|
return {"harnesses": [{"id": "some-route", "enabled": True, "status": "ok",
|
|
"accessProfilesSupported": ["readonly", "workspace_write"]}]}
|
|
def quota_snapshots(self): return []
|
|
def start_run(self, request, *, idempotency_key=""):
|
|
raise AssertionError("must refuse before starting")
|
|
def close(self): pass
|
|
|
|
monkeypatch.setenv("OUROBOROS_SUBAGENT_HARNESS", "some-route=weak-model:low")
|
|
monkeypatch.setattr(gw, "ClaudexorGateway", lambda *a, **k: _Stub())
|
|
|
|
repo = tmp_path / "repo"
|
|
repo.mkdir()
|
|
inside_the_drive = tmp_path / "wt"
|
|
inside_the_drive.mkdir()
|
|
|
|
ctx = ToolContext(
|
|
repo_dir=repo, drive_root=tmp_path,
|
|
task_constraint=TaskConstraint(
|
|
mode="acting_subagent", surface="self_worktree",
|
|
write_root=str(inside_the_drive),
|
|
),
|
|
)
|
|
ctx.task_id = "t-nanny"
|
|
ctx.task_metadata = {"root_task_id": "t-root"}
|
|
ctx.workspace_root = str(inside_the_drive)
|
|
ctx.workspace_mode = "self_worktree"
|
|
|
|
out = json.loads(delegate._delegate_start(ctx, "edit the README").text)
|
|
assert out["status"] == "refused", out
|
|
# A worktree overlapping the data drive is refused as "not an active workspace" —
|
|
# `workspace_mode_block_reason` fires first and is the stronger statement.
|
|
assert out["reason"] in ("write_root_mismatch", "workspace_not_active"), out
|
|
|
|
# And a mutating child whose constraint granted no write_root at all is refused too,
|
|
# rather than the host picking a directory on its behalf.
|
|
ctx.task_constraint = TaskConstraint(mode="acting_subagent", surface="self_worktree")
|
|
out = json.loads(delegate._delegate_start(ctx, "edit the README").text)
|
|
assert out["status"] == "refused", out
|
|
assert out["reason"] in ("write_root_missing", "workspace_not_active"), out
|
|
|
|
|
|
def test_the_guards_that_protect_a_delegated_run_fail_closed(tmp_path, monkeypatch):
|
|
"""Three guards that each failed OPEN in exactly the case they existed for."""
|
|
import ouroboros.tools.delegate as delegate
|
|
from ouroboros.tools.registry import ToolContext
|
|
|
|
# 1. Custody with an unknown identity on either side is refused, not waved through.
|
|
delegate._CUSTODY.clear()
|
|
delegate._CUSTODY["run-x"] = delegate._RunCustody(
|
|
task_id="", route_id="r", model="m", project_id="p", project_owned=False)
|
|
ctx = ToolContext(repo_dir=tmp_path, drive_root=tmp_path)
|
|
ctx.task_id = "t-a"
|
|
ctx.task_metadata = {"root_task_id": "t-a"}
|
|
assert json.loads(delegate._delegate_cancel(ctx, "run-x").text)["reason"] == "run_not_owned"
|
|
ctx.task_id = ""
|
|
delegate._CUSTODY["run-x"] = delegate._RunCustody(
|
|
task_id="t-a", route_id="r", model="m", project_id="p", project_owned=False)
|
|
assert json.loads(delegate._delegate_cancel(ctx, "run-x").text)["reason"] == "run_not_owned"
|
|
delegate._CUSTODY.clear()
|
|
|
|
# 2. A run with no knowable deadline gets a conservative cap, never an omitted one:
|
|
# an omitted cap is Claudexor's 7-day schema bound on a run nobody can cancel.
|
|
bare = ToolContext(repo_dir=tmp_path, drive_root=tmp_path)
|
|
bare.task_id = "t-a"
|
|
bare.task_metadata = {"root_task_id": "t-a"} # no deadline_at at all
|
|
# The cap is the EXISTING operation-window SSOT, not a second hardcoded one: a 1h guess
|
|
# would have truncated a headless/benchmark run that legitimately has no deadline. A
|
|
# task without a lifetime bound (the shipped default) still sends a FINITE cap.
|
|
from ouroboros.config import OPERATION_WINDOW_FALLBACK_SEC, get_task_abs_ceiling_sec
|
|
|
|
assert get_task_abs_ceiling_sec() is None
|
|
|
|
def _seconds(context, requested):
|
|
return delegate.bounded_max_seconds(context, requested).seconds
|
|
|
|
assert _seconds(bare, None) == OPERATION_WINDOW_FALLBACK_SEC
|
|
# A finite configured lifetime is the window itself.
|
|
monkeypatch.setenv("OUROBOROS_TASK_ABS_CEILING_SEC", "7200")
|
|
assert _seconds(bare, None) == 7200
|
|
|
|
# ...but never past Claudexor's own schema bound. The task ceiling clamps only from
|
|
# BELOW, so an owner who raises it past a week would make every deadline-less start
|
|
# send an out-of-schema value and get a 400 instead of a run.
|
|
monkeypatch.setenv("OUROBOROS_TASK_ABS_CEILING_SEC", "1000000")
|
|
assert _seconds(bare, None) == delegate._CLAUDEXOR_MAX_SECONDS
|
|
|
|
# ...and an EXPLICIT ask is clamped by the same bound. `max_seconds` is a
|
|
# model-supplied tool argument with no maximum in its schema, so clamping only the
|
|
# fallback branch left the ask itself able to sail past it — the same defect, one
|
|
# branch over from the one that was fixed.
|
|
assert _seconds(bare, 1_000_000) == delegate._CLAUDEXOR_MAX_SECONDS
|
|
assert _seconds(bare, 120) == 120
|
|
# An explicit narrower ask still wins — the cap is a floor for the unknown case only.
|
|
assert _seconds(bare, 120) == 120
|
|
|
|
# 3. P34P1.8: an EXPIRED deadline is NOT the same fact as having none.
|
|
# `deadline_remaining_sec` answers 0.0 for both, so the fallback above handed an
|
|
# already-expired nanny the absolute task ceiling — hours of delegated work, and
|
|
# real quota, beginning after the instant its own deadline demanded it stop.
|
|
monkeypatch.delenv("OUROBOROS_TASK_ABS_CEILING_SEC", raising=False)
|
|
expired = ToolContext(repo_dir=tmp_path, drive_root=tmp_path)
|
|
expired.task_id = "t-a"
|
|
expired.task_metadata = {"root_task_id": "t-a", "deadline_at": "2020-01-01T00:00:00Z"}
|
|
assert delegate.deadline_expired(expired) is True
|
|
assert delegate.deadline_expired(bare) is False, "no deadline is not an expired one"
|
|
|
|
from ouroboros.deadline_utils import utc_now
|
|
|
|
live = ToolContext(repo_dir=tmp_path, drive_root=tmp_path)
|
|
live.task_id = "t-a"
|
|
live.task_metadata = {"root_task_id": "t-a",
|
|
"deadline_at": (utc_now() + datetime.timedelta(hours=1)).isoformat()}
|
|
assert delegate.deadline_expired(live) is False
|
|
# ...and the live deadline still NARROWS the bound, as it always did.
|
|
assert 0 < _seconds(live, None) <= 3600
|
|
|
|
# The refusal is at the START, before the daemon is touched: nothing spent, nothing
|
|
# registered, and the reason names the honest next move.
|
|
reached = []
|
|
monkeypatch.setenv("OUROBOROS_SUBAGENT_HARNESS", "some-route=weak-model:low")
|
|
|
|
class _NeverReached:
|
|
def handshake(self, **_kw): reached.append("handshake"); return {}
|
|
def close(self): pass
|
|
|
|
from ouroboros.gateways import claudexor as _gw
|
|
|
|
monkeypatch.setattr(_gw, "ClaudexorGateway", lambda *a, **k: _NeverReached())
|
|
refused = json.loads(delegate._delegate_start(expired, "start something new").text)
|
|
assert refused["status"] == "refused" and refused["reason"] == "task_deadline_expired"
|
|
assert refused["definitely_unrun"] is True
|
|
assert reached == [], "expired nanny never reaches daemon"
|
|
|
|
|
|
def test_an_unresolvable_write_root_is_a_typed_refusal_not_a_traceback(tmp_path):
|
|
""""Can this path be resolved at all" is ONE question, not an exception set.
|
|
|
|
Embedded nulls and symlink loops have changed their exact `Path.resolve()` failure
|
|
behaviour across supported Python versions. Either escaping `_mutating_run_root`
|
|
aborts `delegate_start` with a traceback instead of the typed refusal the function
|
|
exists to produce — and a guard that raises delivers no decision at all.
|
|
"""
|
|
import os
|
|
|
|
from ouroboros.contracts.task_constraint import TaskConstraint
|
|
from ouroboros.delegate_containment import _resolved as containment_resolved
|
|
from ouroboros.subagents import delegated_run_shape
|
|
from ouroboros.tools.delegate import _mutation_authority, _resolved
|
|
from ouroboros.tools.registry import ToolContext
|
|
|
|
os.symlink(tmp_path / "b", tmp_path / "a")
|
|
os.symlink(tmp_path / "a", tmp_path / "b")
|
|
assert _resolved(tmp_path / "a" / "x") is None, "a symlink loop must resolve to None"
|
|
assert containment_resolved(tmp_path / "a" / "x") is None
|
|
assert _resolved("/etc/passwd\x00") is None, "an embedded null must resolve to None"
|
|
assert _resolved(tmp_path) == tmp_path.resolve(), "an ordinary path still resolves"
|
|
missing = tmp_path / "missing" / "leaf"
|
|
assert _resolved(missing) == missing.resolve(strict=False)
|
|
assert containment_resolved(missing) == missing.resolve(strict=False)
|
|
|
|
workspace = tmp_path.parent / f"ws-{tmp_path.name}"
|
|
workspace.mkdir()
|
|
ctx = ToolContext(
|
|
repo_dir=tmp_path / "repo", drive_root=tmp_path,
|
|
task_constraint=TaskConstraint(mode="acting_subagent", surface="self_worktree",
|
|
write_root=str(tmp_path / "a" / "x")),
|
|
)
|
|
ctx.workspace_root = str(workspace)
|
|
ctx.workspace_mode = "self_worktree"
|
|
record, refusal = _mutation_authority(
|
|
ctx, delegated_run_shape(True))
|
|
assert refusal and "write_root_mismatch" in refusal.text, refusal
|
|
assert record == {}
|
|
|
|
|
|
def test_an_inactive_workspace_is_refused_even_when_the_root_is_set(tmp_path):
|
|
"""The DISTINGUISHING case for the round-3 predicate fix, which had no test.
|
|
|
|
The old check was `workspace_mode_block_reason(ctx) == "" and workspace_root set`,
|
|
and `workspace_mode_block_reason` returns "" precisely WHEN `workspace_mode` is
|
|
empty — so with a root set and the mode empty, the old condition passed and handed a
|
|
shell the fallback root. Every existing test cleared BOTH fields, which the old
|
|
predicate also refused via its `workspace_root` leg, so reverting the fix left the
|
|
suite green. This is the one shape that tells the two predicates apart.
|
|
"""
|
|
from ouroboros.contracts.task_constraint import TaskConstraint
|
|
from ouroboros.tool_access import workspace_mode_block_reason
|
|
from ouroboros.subagents import delegated_run_shape
|
|
from ouroboros.tools.delegate import _mutation_authority
|
|
from ouroboros.tools.registry import ToolContext
|
|
|
|
repo = tmp_path / "repo"
|
|
repo.mkdir()
|
|
ctx = ToolContext(
|
|
repo_dir=repo, drive_root=tmp_path,
|
|
task_constraint=TaskConstraint(mode="acting_subagent", surface="self_worktree",
|
|
write_root=str(repo)),
|
|
)
|
|
ctx.workspace_root = str(repo) # SET...
|
|
ctx.workspace_mode = "" # ...but the mode is not, so the workspace is not active
|
|
|
|
assert workspace_mode_block_reason(ctx) == "", "the old predicate's leg is satisfied here"
|
|
assert ctx.is_workspace_mode() is False, "yet the workspace is genuinely inactive"
|
|
|
|
record, refusal = _mutation_authority(
|
|
ctx, delegated_run_shape(True))
|
|
assert refusal, "an inactive workspace must be refused"
|
|
assert "workspace_not_active" in refusal.text, refusal
|
|
assert record == {}
|