mirror of
https://github.com/razzant/ouroboros.git
synced 2026-10-03 04:07:04 +00:00
v7next F2: D07 finisher - terminal-evidence leaf, strict worktree registry, sdn facade stands
Owner forks resolved (batch 5, 31.08): 5.9A renamed the deferred terminal leaf of tools/delegate.py to tools/delegate_terminal_evidence.py (upstream owns ouroboros/delegate_terminal.py; F5 rename recorded); 5.10A carried the subagent_worktrees.py strict-registry delta in place with its 11-test pin suite (tip==merge-base for the module; landed bytes == the reference blob); 5.11A left the subagent_dispatch_notes facade standing. Ledger rows 3468-3476 cut from tip bytes (drift-probe: 7/9 byte-true, 2 re-emitted; ast=tokens=byte-roundtrip=True on all 9 spans, exit 0) with the MAXIMAL declared set - every parent-scope call-time read goes through _delegate() (12 names, LEAVES-pinned). tools/delegate.py 1600 -> 1263 leaves the hard cap and enters the 1001-1500 band; subagent_worktrees.py 1000 -> 1082 enters the band under the owner-sanctioned delta; manifest regenerated by the official tool. domains.toml untouched (coordinator seam). (cherry picked from commit c32e0cd6b304d90e63504e829ff5ee5ebcd1f0fe)
This commit is contained in:
parent
2878560ed2
commit
1b4a8da957
8 changed files with 907 additions and 381 deletions
|
|
@ -1473,3 +1473,88 @@ with evidence, found lane by lane. Applied to the campaign's carried ledger at F
|
|||
retargeted to the reading leaf module - same oracle retarget-to-owner
|
||||
shape as the _find_duplicate_task sites; the sentinel's teeth are
|
||||
restored (the leaf's import-bound name is the one the handler reads).
|
||||
|
||||
## From the F2 D07-finisher lane (base 2878560e, 2026-08-31)
|
||||
|
||||
1. Scope executed (the three D07 owner forks, decided 31.08 batch 5: 5.9A,
|
||||
5.10A, 5.11A): the deferred terminal leaf of tools/delegate.py, the
|
||||
subagent_worktrees.py strict-registry delta with its pin suite, and NO
|
||||
sdn retirement (the facade stays).
|
||||
2. F5-RENAME record (owner fork F-2=A, ledger rows 3468-3476): the reference
|
||||
leaf destination `ouroboros/tools/delegate_terminal.py` is renamed at
|
||||
landing to `ouroboros/tools/delegate_terminal_evidence.py`. Rationale:
|
||||
upstream already owns `ouroboros/delegate_terminal.py` ("terminal
|
||||
reconciliation boundary", 189 lines) and the ledger name would put two
|
||||
different delegate_terminal modules in neighbouring packages — a
|
||||
permanent grep/reading trap. Same class as the D01/D03 F5 destination
|
||||
renames. Rows 3468-3476 read onto the renamed file unchanged otherwise.
|
||||
3. Terminal leaf landed from tip bytes (rows 3468-3476, D36 handle
|
||||
`_delegate()`): drift-probe first (reference leaf `--check` against
|
||||
`git show HEAD:ouroboros/tools/delegate.py`): 7/9 spans byte-true,
|
||||
_terminal_payload and _delivered_terminal_payload upstream-drifted —
|
||||
matching the quiet lane's held-back probe evidence — so the leaf was
|
||||
EMITTED from tip bytes, no oracle semantics replayed. Final proof:
|
||||
ast=tokens=byte-roundtrip=True on all 9 symbols, leaf_invariants=[],
|
||||
undeclared_top_level=[], unread_declared=[], exit 0 (re-run after the
|
||||
manual TYPE_CHECKING preamble addition, the D07-quiet
|
||||
reconcile-leaf precedent).
|
||||
4. Declared-set recalc, MAXIMAL form (D10 tools/git precedent, finisher
|
||||
work-order): the reference cut this leaf with plain preamble imports and
|
||||
declared only {_emit}; the landed leaf declares EVERY parent-scope name
|
||||
the moved spans read at call time — 12 names: _Breach,
|
||||
_PAYLOAD_ENVELOPE_HEADROOM, _emit, _home_isolation_breach,
|
||||
_preview_payload, _resolve_full_primary_output, _stage_full_output,
|
||||
_widened_access, add_terminal_source_verification, custody,
|
||||
home_nested_under_operator_home, tool_result_limit — so patches on the
|
||||
historical `ouroboros.tools.delegate` surface keep their teeth. Only
|
||||
stdlib (json) and typing stay preamble imports; annotation-only names
|
||||
(_Breach for its `-> Optional[_Breach]` use, _RunCustody, ToolContext,
|
||||
DelegatedRunShape) ride an `if TYPE_CHECKING:` block, inert under future
|
||||
annotations. New LEAVES row pinned in
|
||||
tests/test_module_handle_extraction.py.
|
||||
5. Facade: tools/delegate.py = tip parent - the 9 moved spans (lines
|
||||
225-576 of the HEAD file) + the grouped EOF re-export block + noqa
|
||||
discipline: exactly four `# noqa: F401` markers on the import lines of
|
||||
parent members now read only through `_delegate()` at call time
|
||||
(_home_isolation_breach, _widened_access, home_nested_under_operator_home,
|
||||
add_terminal_source_verification — the bindings are load-bearing for the
|
||||
leaf and must survive ruff F). Every kept def/assign span proven
|
||||
byte-identical to `git show HEAD:ouroboros/tools/delegate.py` (the diff
|
||||
of the kept region is exactly those four marker lines); re-exports
|
||||
proven same-object by import smoke. tools/delegate.py 1600 -> 1263: the
|
||||
LAST 1600-hard-cap giant of the D07 organ leaves the cap and enters the
|
||||
1001-1500 band with a rationale. The reference facade-identity rows for this family (held
|
||||
back by the quiet lane) landed in tests/test_delegate_owner_facades.py
|
||||
under the renamed leaf.
|
||||
6. Ф-1 strict-registry delta (rows 1083-1092, owner sanction 5.10A —
|
||||
SANCTIONED SEMANTIC DELTA in an otherwise byte-preserving lane):
|
||||
drift-probe first — tip blob of ouroboros/subagent_worktrees.py ==
|
||||
merge-base 8028f1df blob (fd2db424, upstream never touched the module),
|
||||
so the reference diff (+104/-22) applied clean; the landed module is
|
||||
byte-identical to the reference module (blob ee694e4d on both sides).
|
||||
Semantics: absent registry stays an ordinary empty registry; malformed
|
||||
registry raises typed SubagentWorktreeRegistryCorrupt for every author/
|
||||
destructor (provision_worktree, provision_execution_snapshot,
|
||||
provision_payload_snapshot, find_execution_snapshot,
|
||||
remove_execution_snapshot, prune_execution_snapshots, remove_worktree,
|
||||
prune_orphans) instead of silently collapsing to empty; bytes are kept;
|
||||
one durable subagent_worktree_registry_corrupt event; inspection reads
|
||||
stay soft; registration moves INSIDE the cleanup scope on all three
|
||||
provisioning branches. Pin suite
|
||||
tests/test_subagent_worktree_registry_s6.py copied verbatim from the
|
||||
oracle (281 lines, 11 tests, red without the delta per D09 entry 10):
|
||||
imports only stdlib + the module itself, zero v7-only names to reverse-
|
||||
map; its docstring's sibling reference
|
||||
(test_delegated_skill_payload.py::test_registry_save_failure_leaves_no_orphan_snapshot_dir)
|
||||
exists on tip; the oracle registered it in no conftest path-keyed table.
|
||||
The one pre-existing tip test touching the registry
|
||||
(tests/test_acting_subagents.py:1298) uses the soft read, whose
|
||||
signature and behavior are unchanged.
|
||||
7. Ф-3 (sdn): no action, per owner 5.11A — the quiet lane's entry 8 stands
|
||||
(rows 3937-3938 satisfied as identity; retirement stays an F5
|
||||
consumer-rebind item).
|
||||
8. Ratchet (official regenerator): ouroboros/tools/delegate.py enters the
|
||||
band by extraction (1600->1263, rationale recorded);
|
||||
ouroboros/subagent_worktrees.py enters the band by the sanctioned delta
|
||||
(1000->1082, rationale recorded). domains.toml untouched (coordinator
|
||||
seam owns the map).
|
||||
|
|
|
|||
|
|
@ -144,12 +144,14 @@ BAND_PATHS = {
|
|||
"ouroboros/safety.py": "Entered the band from 954 lines with the safety-supervisor rate-limit fix: ONE shared model-call helper now serves both the primary and repair safety calls (it already deletes the duplicated call block), recognising a provider rate limit in BOTH wire shapes, taking one bounded deadline-capped backoff plus one retry, then blocking that one call with the typed non-verdict SAFETY_UNAVAILABLE outcome plus a durable audit row (a short storm latch answers further checks in the window without provider calls); the bounded newest-first conversation budget is the second half.",
|
||||
"ouroboros/skill_review_runner.py": None,
|
||||
"ouroboros/subagent_runtime.py": "Configured-retry refusals mirrored typed (triad 2026-08-30) push the module just over 1000; no new subsystem, same seam.",
|
||||
"ouroboros/subagent_worktrees.py": "Owner-sanctioned strict-registry delta (v7 rows 1083-1092, fork F-1=A) grew the module 1000->1082: typed refusal of a malformed registry instead of silent collapse-to-empty; shrink-only direction",
|
||||
"ouroboros/subagents.py": "D07 split brought the dispatch monolith DOWN from 1593 into the band (->1370); route-health family extracted to subagent_route_health.py, shrink-only direction",
|
||||
"ouroboros/task_results.py": "Authority reads need an explicit strict mode so malformed child records cannot become a false zero count.",
|
||||
"ouroboros/task_status.py": None,
|
||||
"ouroboros/tools/browser.py": None,
|
||||
"ouroboros/tools/commit_gate.py": "Grew INTO the band by the review-wave fix binding the actor reference (delivery class) into the commit review contract fingerprint \u2014 same-module contract identity, splitting it would separate the fingerprint from its gate.",
|
||||
"ouroboros/tools/core.py": "D05 ledger split (rows 311-349): read/list and owner-chat delivery spans moved to core_file_tools/core_artifacts; facade re-enters the band from above (2283 -> 1373) and shrinks further when the residual catalog split lands",
|
||||
"ouroboros/tools/delegate.py": "D07 finisher DEL1 split brought the nanny-verb monolith DOWN from the 1600 hard cap into the band (1600->1263); terminal-evidence family extracted to tools/delegate_terminal_evidence.py, shrink-only direction",
|
||||
"ouroboros/tools/plan_review_runtime.py": "Entered the band from 986 lines: timeout custody synthesis joined the existing plan-review runtime owner while preserving profile-continuity disclosures and typed health facts during target integration.",
|
||||
"ouroboros/tools/review_context_atlas.py": "Grew INTO the band by the #284 pack-arithmetic fixes: measured render charged at admission, exact per-row costs, target capped at the hard rail, honest eviction diagnostics \u2014 all in the module that owns the arithmetic.",
|
||||
"ouroboros/tools/skill_exec.py": None,
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ from __future__ import annotations
|
|||
|
||||
import contextlib
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
|
|
@ -32,6 +33,8 @@ from ouroboros.utils import atomic_write_json
|
|||
from ouroboros.config import DATA_DIR, get_subagent_projects_root, get_subagent_worktree_root
|
||||
from ouroboros.retention import age_cutoff, get_gc_retention_days
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
_REGISTRY_NAME = "subagent_worktrees.json"
|
||||
_LOCK_NAME = ".worktree_ops.lock"
|
||||
_LOCK_TIMEOUT_SEC = 120.0
|
||||
|
|
@ -99,16 +102,65 @@ def _safe_name(task_id: Any) -> str:
|
|||
return safe
|
||||
|
||||
|
||||
def _load_registry(data_dir: Optional[Any] = None) -> List[Dict[str, Any]]:
|
||||
class SubagentWorktreeRegistryCorrupt(RuntimeError):
|
||||
"""``state/subagent_worktrees.json`` exists but cannot be read as a registry.
|
||||
|
||||
Raised by every caller that would go on to REWRITE the file. Collapsing a
|
||||
malformed registry to an empty one hands the next write a clean slate: the
|
||||
rows are gone, and with them the only record naming the checkouts and the
|
||||
``refs/ouroboros/delegated/*`` refs those rows pin — a leak nothing can
|
||||
reconcile afterwards. The malformed bytes are kept instead, which is what
|
||||
the sibling registries (cancel intents, terminal deliveries) do and what
|
||||
the startup GC already does when the custody log is unreadable.
|
||||
"""
|
||||
|
||||
|
||||
def _load_registry(
|
||||
data_dir: Optional[Any] = None, *, strict: bool = False, op: str = "",
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""The registered rows; ``strict`` refuses a malformed registry.
|
||||
|
||||
ABSENT is an ordinary empty registry in both modes (the first-write case).
|
||||
MALFORMED is a different fact, and ``strict=True`` — passed by everything
|
||||
that authors a record or acts destructively on one — reports it as such
|
||||
instead of as "nothing is registered". Inspection reads (the UI listing)
|
||||
stay soft: they display what they can and destroy nothing.
|
||||
"""
|
||||
path = _registry_path(data_dir)
|
||||
if not path.is_file():
|
||||
return []
|
||||
try:
|
||||
raw = json.loads(path.read_text(encoding="utf-8"))
|
||||
except (OSError, json.JSONDecodeError, ValueError):
|
||||
entries = raw.get("worktrees") if isinstance(raw, dict) else raw
|
||||
if not isinstance(entries, list):
|
||||
raise ValueError("subagent worktree registry 'worktrees' is not a list")
|
||||
except (OSError, UnicodeDecodeError, json.JSONDecodeError, ValueError) as exc:
|
||||
if strict:
|
||||
raise _refuse_corrupt_registry(data_dir, op, exc) from exc
|
||||
return []
|
||||
entries = raw.get("worktrees") if isinstance(raw, dict) else raw
|
||||
if isinstance(entries, list):
|
||||
return [e for e in entries if isinstance(e, dict)]
|
||||
return []
|
||||
return [e for e in entries if isinstance(e, dict)]
|
||||
|
||||
|
||||
def _refuse_corrupt_registry(
|
||||
data_dir: Optional[Any], op: str, exc: Exception,
|
||||
) -> SubagentWorktreeRegistryCorrupt:
|
||||
"""Disclose an unreadable registry durably, then refuse the mutation."""
|
||||
path = _registry_path(data_dir)
|
||||
log.error(
|
||||
"subagent worktree registry is corrupt; %s refused, bytes kept (%s)",
|
||||
op or "mutation", exc,
|
||||
)
|
||||
try:
|
||||
from ouroboros.utils import append_jsonl, utc_now_iso
|
||||
|
||||
append_jsonl(
|
||||
_data_dir(data_dir) / "logs" / "events.jsonl",
|
||||
{"ts": utc_now_iso(), "type": "subagent_worktree_registry_corrupt",
|
||||
"op": str(op or ""), "registry": str(path), "error": str(exc)[:200]},
|
||||
)
|
||||
except Exception:
|
||||
log.debug("registry-corrupt event append failed", exc_info=True)
|
||||
return SubagentWorktreeRegistryCorrupt(str(exc))
|
||||
|
||||
|
||||
def _save_registry(entries: List[Dict[str, Any]], data_dir: Optional[Any] = None) -> None:
|
||||
|
|
@ -259,9 +311,21 @@ def provision_worktree(
|
|||
created_at=time.time(),
|
||||
parent_task_id=str(parent_task_id or ""),
|
||||
)
|
||||
entries = [e for e in _load_registry(data_dir) if e.get("path") != str(wt_path)]
|
||||
entries.append(asdict(handle))
|
||||
_save_registry(entries, data_dir)
|
||||
try:
|
||||
entries = [
|
||||
e for e in _load_registry(data_dir, strict=True, op="provision_worktree")
|
||||
if e.get("path") != str(wt_path)
|
||||
]
|
||||
entries.append(asdict(handle))
|
||||
_save_registry(entries, data_dir)
|
||||
except Exception:
|
||||
# Registration is INSIDE the cleanup scope, mirroring the snapshot
|
||||
# branches below: a worktree nothing registered is invisible to
|
||||
# disposal and retention, so a corrupt registry (strict load) or a
|
||||
# failed write would otherwise strand the checkout AND its branch
|
||||
# on every retry, without bound.
|
||||
_remove_paths(repo_dir, wt_path, branch, allowed_root=root)
|
||||
raise
|
||||
return handle
|
||||
|
||||
|
||||
|
|
@ -529,12 +593,24 @@ def provision_execution_snapshot(
|
|||
entry_count=entry_count,
|
||||
excluded_untracked=tuple(excluded),
|
||||
)
|
||||
entries = [e for e in _load_registry(data_dir) if e.get("path") != str(wt_path)]
|
||||
record = asdict(handle)
|
||||
record["kind"] = _KIND_DELEGATED_EXEC
|
||||
record["excluded_untracked"] = excluded
|
||||
entries.append(record)
|
||||
_save_registry(entries, data_dir)
|
||||
try:
|
||||
entries = [
|
||||
e for e in _load_registry(data_dir, strict=True, op="provision_execution_snapshot")
|
||||
if e.get("path") != str(wt_path)
|
||||
]
|
||||
record = asdict(handle)
|
||||
record["kind"] = _KIND_DELEGATED_EXEC
|
||||
record["excluded_untracked"] = excluded
|
||||
entries.append(record)
|
||||
_save_registry(entries, data_dir)
|
||||
except Exception:
|
||||
# Registration is INSIDE the cleanup scope, exactly like the payload
|
||||
# branch: a snapshot nothing registered is invisible to disposal and
|
||||
# retention, and on this branch it also strands the baseline ref,
|
||||
# which pins its commit against git's own GC for good.
|
||||
_remove_paths(target, wt_path, "", allowed_root=root)
|
||||
_git(target, "update-ref", "-d", baseline_ref, check=False)
|
||||
raise
|
||||
return handle
|
||||
|
||||
|
||||
|
|
@ -820,7 +896,10 @@ def provision_payload_snapshot(
|
|||
)
|
||||
# Registry write INSIDE the cleanup scope: an unregistered snapshot
|
||||
# directory would be invisible to disposal/retention (orphan leak).
|
||||
entries = [e for e in _load_registry(data_dir) if e.get("path") != str(wt_path)]
|
||||
entries = [
|
||||
e for e in _load_registry(data_dir, strict=True, op="provision_payload_snapshot")
|
||||
if e.get("path") != str(wt_path)
|
||||
]
|
||||
record = asdict(handle)
|
||||
record["kind"] = _KIND_DELEGATED_EXEC
|
||||
record["excluded_untracked"] = []
|
||||
|
|
@ -837,7 +916,7 @@ def find_execution_snapshot(snapshot_id: str, data_dir: Optional[Any] = None) ->
|
|||
snap = str(snapshot_id or "").strip()
|
||||
if not snap:
|
||||
return None
|
||||
for entry in _load_registry(data_dir):
|
||||
for entry in _load_registry(data_dir, strict=True, op="find_execution_snapshot"):
|
||||
if entry.get("kind") == _KIND_DELEGATED_EXEC and entry.get("snapshot_id") == snap:
|
||||
return entry
|
||||
return None
|
||||
|
|
@ -877,7 +956,7 @@ def remove_execution_snapshot(
|
|||
_git(target, "update-ref", "-d", ref, check=False)
|
||||
except Exception:
|
||||
pass
|
||||
survivors = [e for e in _load_registry(data_dir) if not (
|
||||
survivors = [e for e in _load_registry(data_dir, strict=True, op="remove_execution_snapshot") if not (
|
||||
e.get("kind") == _KIND_DELEGATED_EXEC and e.get("snapshot_id") == entry.get("snapshot_id")
|
||||
)]
|
||||
_save_registry(survivors, data_dir)
|
||||
|
|
@ -902,7 +981,7 @@ def prune_execution_snapshots(
|
|||
open_ids = {str(s) for s in (open_snapshot_ids or set())}
|
||||
removed: List[str] = []
|
||||
kept: List[str] = []
|
||||
for entry in list(_load_registry(data_dir)):
|
||||
for entry in list(_load_registry(data_dir, strict=True, op="prune_execution_snapshots")):
|
||||
if entry.get("kind") != _KIND_DELEGATED_EXEC:
|
||||
continue
|
||||
snap = str(entry.get("snapshot_id") or "")
|
||||
|
|
@ -924,7 +1003,7 @@ def remove_worktree(
|
|||
) -> bool:
|
||||
"""Tear down a worktree by task_id or path; unregister it. Returns success."""
|
||||
want_path = str(Path(path).resolve()) if path else ""
|
||||
entries = _load_registry(data_dir)
|
||||
entries = _load_registry(data_dir, strict=True, op="remove_worktree")
|
||||
match: Optional[Dict[str, Any]] = None
|
||||
for entry in entries:
|
||||
if task_id and entry.get("task_id") == str(task_id):
|
||||
|
|
@ -937,7 +1016,10 @@ def remove_worktree(
|
|||
with _ops_lock(root):
|
||||
if match is not None:
|
||||
_remove_paths(Path(match.get("repo_dir") or "."), Path(match.get("path") or ""), match.get("branch") or "", allowed_root=root)
|
||||
survivors = [e for e in _load_registry(data_dir) if e.get("path") != match.get("path")]
|
||||
survivors = [
|
||||
e for e in _load_registry(data_dir, strict=True, op="remove_worktree")
|
||||
if e.get("path") != match.get("path")
|
||||
]
|
||||
_save_registry(survivors, data_dir)
|
||||
return True
|
||||
# Unregistered path: best-effort directory removal, but ONLY inside the
|
||||
|
|
@ -965,7 +1047,7 @@ def prune_orphans(
|
|||
kept: List[Dict[str, Any]] = []
|
||||
repos: set[str] = set()
|
||||
with _ops_lock(root):
|
||||
for entry in _load_registry(data_dir):
|
||||
for entry in _load_registry(data_dir, strict=True, op="prune_orphans"):
|
||||
if entry.get("kind") == _KIND_DELEGATED_EXEC:
|
||||
# Delegated execution snapshots have their OWN lifecycle: they persist
|
||||
# until the run's explicit patch disposition, and the startup GC
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ from ouroboros.subagent_work_order import ( # noqa: F401 - compatibility re-exp
|
|||
assignment_instructions as _assignment_instructions,
|
||||
)
|
||||
from ouroboros.delegate_source_coverage import (
|
||||
add_terminal_source_verification,
|
||||
add_terminal_source_verification, # noqa: F401 (leaf reads it through _delegate() at call time)
|
||||
prepare_work_order_start_binding,
|
||||
record_started_custody,
|
||||
)
|
||||
|
|
@ -136,9 +136,9 @@ _TERMINAL_STATES = custody.TERMINAL_STATES
|
|||
from ouroboros.delegate_containment import ( # noqa: E402
|
||||
_ACCESS_UNVERIFIED, # noqa: F401 (re-export: tests address it through this module)
|
||||
_Breach,
|
||||
_home_isolation_breach,
|
||||
_widened_access,
|
||||
home_nested_under_operator_home,
|
||||
_home_isolation_breach, # noqa: F401 (leaf reads it through _delegate() at call time)
|
||||
_widened_access, # noqa: F401 (leaf reads it through _delegate() at call time)
|
||||
home_nested_under_operator_home, # noqa: F401 (leaf reads it through _delegate() at call time)
|
||||
)
|
||||
_POLL_INTERVAL_SEC = 3.0
|
||||
# Claudexor's own schema bound on maxSeconds (packages/schema/src/control.ts).
|
||||
|
|
@ -222,358 +222,6 @@ def _presence_delegate_read_refusal(ctx: ToolContext) -> str:
|
|||
)
|
||||
|
||||
|
||||
def _containment_breach(detail: Dict[str, Any], authority: "DelegatedRunShape") -> Optional[_Breach]:
|
||||
"""Everything the ENGINE enforced, checked against what the host asked for.
|
||||
|
||||
ONE reader for both halves of containment — the access profile and the harness
|
||||
HOME — because they fail identically: the request is only a request, the engine
|
||||
derives the truth, and a verification written for one half leaves the other
|
||||
trusting an echo. The HOME half is asked only of a run that carried the marker;
|
||||
a read-only child is scoped by Claudexor's ordinary envelope and asks for nothing.
|
||||
"""
|
||||
widened = _widened_access(detail, authority.access)
|
||||
if widened:
|
||||
return _Breach(
|
||||
"access_profile_widened",
|
||||
f"The delegated run was enforced at access profile {widened!r} while this "
|
||||
f"task is only entitled to {authority.access!r}.",
|
||||
{"entitled_access": authority.access, "effective_access": widened},
|
||||
)
|
||||
if authority.delegated:
|
||||
return _home_isolation_breach(detail)
|
||||
return None
|
||||
|
||||
|
||||
_NESTED_HOME_NOTE = (
|
||||
"The scoped harness HOME for this run sits INSIDE the operator's own home, which is "
|
||||
"where the engine roots its scoped homes. That is allowed and the run's work is usable, "
|
||||
"but it is not isolation from the operator's home: everything there — credential stores "
|
||||
"and the Claudexor daemon token included — stays readable at its absolute path. Do NOT "
|
||||
"describe this run as running in an isolated home"
|
||||
)
|
||||
|
||||
_NO_BOUNDARY_NOTE = (
|
||||
"NO OS-ENFORCED BOUNDARY was applied to this run. The engine reported no confinement "
|
||||
"mechanism for it, so the only containment it had is a scoped HOME — a redirect of "
|
||||
"`~`-relative lookups, which leaves the operator's home, credential stores and the "
|
||||
"Claudexor daemon token readable at their absolute paths. The run was allowed and its "
|
||||
"work is usable; do NOT describe it as sandboxed, confined or isolated, and weigh its "
|
||||
"output as coming from an unconfined shell in this worktree"
|
||||
)
|
||||
|
||||
|
||||
def _containment_evidence(detail: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""What the ARTIFACTS prove about this run's containment — never what was asked.
|
||||
|
||||
DESTINATION 3 of the disclosure: this is what the nanny hands its parent.
|
||||
|
||||
BOTH halves, in one reader, because a report that states only the scoped HOME is the
|
||||
defect this function was rewritten to remove: a run with a kernel-enforced boundary
|
||||
and a run with none produced BYTE-IDENTICAL evidence here, both reading
|
||||
``verified: true`` with a note about the HOME. Claudexor's own confinement document
|
||||
says the scoped home "is not a boundary and must never be reported as one".
|
||||
|
||||
The predicate is what the engine says it APPLIED (``confinement_mechanism`` plus the
|
||||
denied path it proved), never which OS this host is. Ouroboros does not know what the
|
||||
engine did — only the artifact does — and a platform test would additionally freeze
|
||||
today's answer: the day a boundary ships for another OS, this reader is already right.
|
||||
|
||||
Judged by the SAME predicate that halts a breached run, not by having been reached
|
||||
after it: a report whose honesty depends on its call site is one refactor away from
|
||||
claiming a containment nobody checked.
|
||||
|
||||
This is also where a MISSING fact lands, because it is a reporting question and not an
|
||||
enforcement one: an attempt that disclosed nothing proves nothing, so ``verified``
|
||||
stays false and ``disclosed`` says how much of the run is actually covered. Silence
|
||||
read as success and silence enforced as a fault are the two ways to be wrong here,
|
||||
and stating the count avoids both.
|
||||
"""
|
||||
from ouroboros.gateways.claudexor import attempt_containment
|
||||
|
||||
attempts = attempt_containment(str(custody.summary_of(detail).get("runDir") or ""))
|
||||
disclosed = sum(1 for attempt in attempts if attempt.home_isolated is not None)
|
||||
# An engine that reported nothing is indistinguishable from one that applied nothing,
|
||||
# and the mechanisms the ATTEMPTS name are the vocabulary — Ouroboros keeps no list of
|
||||
# its own to fall out of date. "Every attempt" and not "any": one unconfined attempt
|
||||
# is an unconfined run.
|
||||
mechanisms = sorted({attempt.boundary_mechanism for attempt in attempts})
|
||||
boundary = mechanisms[0] if attempts and len(mechanisms) == 1 and mechanisms[0] else ""
|
||||
# A3: the engine's own typed reason for a missing boundary — an AMPLIFIER of
|
||||
# the unconfined disclosure (why there is no mechanism on this host), parsed
|
||||
# from the same attempt artifact. Telemetry only, never an admission token.
|
||||
unavailable_reasons = sorted({
|
||||
attempt.confinement_unavailable_reason
|
||||
for attempt in attempts if attempt.confinement_unavailable_reason
|
||||
})
|
||||
# A3: a scoped home NESTED under the operator's own is allowed (the engine's
|
||||
# own layout — disclosed, never refused), but it is NOT "outside the
|
||||
# operator's own": the daemon token stays reachable at its absolute path.
|
||||
# Recorded on the report and honoured by every branch below, so a run that
|
||||
# ALSO carries an OS boundary can no longer be promoted to verified with a
|
||||
# note that contradicts its own artifact — and so `_record_containment` keeps
|
||||
# emitting the durable unconfined row for it.
|
||||
nested = home_nested_under_operator_home(detail)
|
||||
report = {"verified": False, "attempts": len(attempts), "disclosed": disclosed,
|
||||
"os_boundary": boundary, "nested_under_operator_home": nested}
|
||||
if unavailable_reasons:
|
||||
report["confinement_unavailable_reason"] = "; ".join(unavailable_reasons)
|
||||
breach = _home_isolation_breach(detail)
|
||||
if breach is not None:
|
||||
return {**report, "note": breach.detail}
|
||||
if not disclosed:
|
||||
return {**report, "note":
|
||||
"this run recorded no harness-HOME fact, so its confinement is UNPROVEN "
|
||||
"— do not report it as isolated"}
|
||||
if disclosed < len(attempts):
|
||||
return {**report, "note":
|
||||
"not every attempt of this run recorded a harness-HOME fact, so its "
|
||||
"confinement is UNPROVEN — do not report it as isolated"}
|
||||
if nested:
|
||||
note = _NESTED_HOME_NOTE
|
||||
if boundary:
|
||||
note += (
|
||||
f" (an {boundary} boundary WAS applied — weigh it as the real containment, "
|
||||
"but the scoped HOME is not one)"
|
||||
)
|
||||
if unavailable_reasons:
|
||||
note += " (engine-declared reason: " + "; ".join(unavailable_reasons) + ")"
|
||||
return {**report, "note": note}
|
||||
if not boundary:
|
||||
note = _NO_BOUNDARY_NOTE
|
||||
if unavailable_reasons:
|
||||
note += (
|
||||
" (engine-declared reason: " + "; ".join(unavailable_reasons) + ")"
|
||||
)
|
||||
return {**report, "note": note}
|
||||
return {**report, "verified": True, "note":
|
||||
f"every attempt recorded a scoped harness HOME outside the operator's own AND "
|
||||
f"an applied {boundary} boundary, proven against a path it denies"}
|
||||
|
||||
|
||||
def _terminal_payload(run_id: str, detail: Dict[str, Any],
|
||||
authority: "DelegatedRunShape") -> Dict[str, Any]:
|
||||
summary = custody.summary_of(detail)
|
||||
payload = {
|
||||
"status": "terminal",
|
||||
"run_id": run_id,
|
||||
"state": str(summary.get("state") or ""),
|
||||
# The APPLIED model, from the engine's own summary — '' when the run
|
||||
# never disclosed one (live unpinned runs really do), shown as absence
|
||||
# rather than the requested model dressed up as the applied one.
|
||||
"model": str(summary.get("model") or ""),
|
||||
"outcome_banner": detail.get("outcomeBanner"),
|
||||
"outcome_facts": summary.get("outcomeFacts"),
|
||||
"output_conformance": summary.get("outputConformance"),
|
||||
"final_summary": detail.get("finalSummary"),
|
||||
"primary_output": detail.get("primaryOutput"),
|
||||
"failure": summary.get("failure"),
|
||||
"last_seq": int(detail.get("lastSeq") or 0),
|
||||
"cost": _reported_cost(summary),
|
||||
# The ACCESS half of the same honesty, on EVERY terminal payload — see
|
||||
# `_access_evidence`. Both lanes: `readonly` staying `readonly` is the profile
|
||||
# that matters most, while `containment` is asked only of marker-carrying runs.
|
||||
"access_evidence": _access_evidence(detail, authority.access),
|
||||
}
|
||||
if authority.delegated:
|
||||
payload["containment"] = _containment_evidence(detail)
|
||||
facts = payload.get("outcome_facts")
|
||||
if isinstance(facts, dict) and str(facts.get("reason") or "") == "input_required":
|
||||
# The codex-shaped question (B4): that lane has no mid-run channel, so a
|
||||
# question arrives as this TERMINAL. There is deliberately NO rerun verb
|
||||
# here — the engine's rerun_with_feedback would start a run outside this
|
||||
# task's custody trail — so the honest answer path is a plain new start.
|
||||
payload["input_required_note"] = (
|
||||
"This run ended NEEDING INPUT (outcome_facts.reason=input_required — "
|
||||
"see outcome_facts.work_state.required_inputs). Its harness has no "
|
||||
"mid-run question channel, so the question arrives as this terminal. Answer it by "
|
||||
"starting a plain NEW delegate_start(subagent_id=..., prompt=...) whose "
|
||||
"prompt carries the original "
|
||||
"assignment plus the answers; custody of the new run stays with you. "
|
||||
"Do not look for a rerun/decision verb — none exists on this surface."
|
||||
)
|
||||
return payload
|
||||
|
||||
|
||||
def _access_evidence(detail: Dict[str, Any], expected: str) -> Dict[str, Any]:
|
||||
"""What the engine's own DERIVED profile proves about this finished run.
|
||||
|
||||
``effectiveAccess`` is the only witness: ``summary["access"]`` is computed as
|
||||
``effectiveAccess ?? the client's own request``, so reading it compares the request
|
||||
against itself and always passes. A WIDER profile is already a breach before this
|
||||
runs; an ABSENT one cannot be enforced on a run that is over — cancelling a
|
||||
succeeded run to punish missing evidence would destroy the result the lane exists
|
||||
to fetch (the v6.87.37 lesson) — so it is named here instead.
|
||||
"""
|
||||
summary = custody.summary_of(detail)
|
||||
effective = str(summary.get("effectiveAccess") or "")
|
||||
state = str(summary.get("state") or "")
|
||||
report = {"requested": expected, "effective": effective,
|
||||
"verified": bool(effective), "state": state}
|
||||
if effective:
|
||||
return report
|
||||
if state in custody.SUCCEEDED_STATES:
|
||||
return {**report, "note":
|
||||
"this run SUCCEEDED without ever disclosing an effective access "
|
||||
f"profile, so there is no evidence the engine enforced {expected!r} — "
|
||||
"do not report its containment as verified"}
|
||||
return {**report, "note":
|
||||
"no effective access profile was disclosed; a run that did not succeed may "
|
||||
"never have had one, so this is absence of evidence, not a breach"}
|
||||
|
||||
|
||||
def _record_containment(ctx: ToolContext, entry: Optional[_RunCustody],
|
||||
payload: Dict[str, Any]) -> None:
|
||||
"""DESTINATION 1 of the disclosure: the durable record, written once per run.
|
||||
|
||||
A missing boundary is not a fault and produces no refusal, which is exactly why it
|
||||
needs a durable line of its own — the run succeeds, its patch is integrated, and
|
||||
nothing else in the record would ever say the work came out of an unconfined shell.
|
||||
Emitted from what the PARENT was told, so the two cannot disagree.
|
||||
|
||||
"Once per run" is now a DURABLE fact rather than a process-local one: the custody
|
||||
entry is replayed from the event log, so a restarted worker polling an already
|
||||
terminal run does not append a second identical finding.
|
||||
|
||||
A NESTED scoped home is disclosed even when an OS boundary WAS recorded (A3):
|
||||
the boundary is real containment, the scoped home is not, and suppressing the
|
||||
row for that shape left the one durable line that says "this ran with the
|
||||
operator's home reachable" unwritten.
|
||||
"""
|
||||
containment = payload.get("containment")
|
||||
if not isinstance(containment, dict):
|
||||
return
|
||||
if containment.get("os_boundary") and not containment.get("nested_under_operator_home"):
|
||||
return
|
||||
if entry is not None and entry.containment_disclosed:
|
||||
return
|
||||
_emit(ctx, custody.UNCONFINED, {
|
||||
"run_id": entry.run_id if entry is not None else "",
|
||||
"route": entry.route_id if entry is not None else "",
|
||||
"state": str(payload.get("state") or ""),
|
||||
"os_boundary": str(containment.get("os_boundary") or ""),
|
||||
"attempts": containment.get("attempts"),
|
||||
"home_disclosed": containment.get("disclosed"),
|
||||
"nested_under_operator_home": bool(containment.get("nested_under_operator_home")),
|
||||
"note": containment.get("note"),
|
||||
**({"confinement_unavailable_reason": containment["confinement_unavailable_reason"]}
|
||||
if containment.get("confinement_unavailable_reason") else {}),
|
||||
})
|
||||
if entry is not None:
|
||||
entry.containment_disclosed = True
|
||||
|
||||
|
||||
def _reported_cost(summary: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""What this run cost, as the AGENT will read it.
|
||||
|
||||
This is the payload the nanny relays to its parent, so it must tell the same story
|
||||
the ledger does. It used to hardcode `$0.00 / final` — the exact shape the settlement
|
||||
fix was written to eliminate — so a run that really charged money settled honestly in
|
||||
the ledger and then told the reasoning path the work was free.
|
||||
"""
|
||||
spend, estimated = custody.disclosed_spend(summary)
|
||||
if spend is None:
|
||||
return {
|
||||
"cost_usd": None,
|
||||
"cost_final": False,
|
||||
"note": "the harness disclosed no spend for this run; treat the cost as UNKNOWN, not zero",
|
||||
}
|
||||
if estimated:
|
||||
# The amount is the best fact anyone has, so it rides; the FINALITY does not. An
|
||||
# estimated zero is not a proven free session and an estimated charge is not a
|
||||
# closed book — both are `cost_final: False`, matching the ledger row exactly.
|
||||
return {
|
||||
"cost_usd": spend,
|
||||
"cost_final": False,
|
||||
"note": "the harness ESTIMATED this run's spend rather than settling it; treat "
|
||||
"the amount as APPROXIMATE and the cost as NOT final",
|
||||
}
|
||||
if spend > 0:
|
||||
return {
|
||||
"cost_usd": spend,
|
||||
"cost_final": True,
|
||||
"note": "this run was BILLED — it did not ride the subscription",
|
||||
}
|
||||
return {
|
||||
"cost_usd": 0.0,
|
||||
"cost_final": True,
|
||||
"note": "subscription session — already paid; the nanny's own model calls are metered separately",
|
||||
}
|
||||
|
||||
|
||||
# -- output delivery -----------------------------------------------------------
|
||||
|
||||
|
||||
def _delivered_terminal_payload(ctx: ToolContext, run_id: str, detail: Dict[str, Any],
|
||||
authority: "DelegatedRunShape",
|
||||
entry: Optional[_RunCustody] = None,
|
||||
gateway: Any = None) -> Dict[str, Any]:
|
||||
"""The terminal payload, delivered whole or declared partial — never head-cut.
|
||||
|
||||
``final_summary``/``primary_output`` carry the run's real work product, and Claudexor
|
||||
returns a preview of up to 256 KiB. Outer truncation would head-cut that at the tool
|
||||
result limit and sever the JSON mid-string, which destroys the document rather than
|
||||
shortening it. So the payload bounds ITSELF against the same limit the truncator
|
||||
applies, and the remainder becomes a readable artifact — after the engine's bounded
|
||||
preview has been resolved to the verified full artifact, because a payload built on
|
||||
a truncated preview delivers 256 KiB wearing the whole result's name.
|
||||
"""
|
||||
full = _terminal_payload(run_id, detail, authority)
|
||||
if entry is not None:
|
||||
add_terminal_source_verification(full, entry)
|
||||
# Requested-vs-applied model, the review lane's own lexicon and rule
|
||||
# (AgentSessionReviewExecutor): compared only when BOTH are non-empty —
|
||||
# the engine writes aliases ('sonnet' beside 'claude-opus-5'), so a
|
||||
# mismatch is an advisory disclosure, never a failure of the run.
|
||||
requested_model = str(getattr(entry, "model", "") or "") if entry is not None else ""
|
||||
applied_model = str(full.get("model") or "")
|
||||
if requested_model and applied_model and requested_model != applied_model:
|
||||
full["capability_delta"] = [{
|
||||
"kind": "capability_delta",
|
||||
"requested": f"model {requested_model}",
|
||||
"effective": f"model {applied_model}",
|
||||
"reason": "session_route_resolves_its_own_model",
|
||||
}]
|
||||
primary, full_ok, full_note = _resolve_full_primary_output(
|
||||
gateway, run_id, full.get("primary_output"))
|
||||
full["primary_output"] = primary
|
||||
budget = tool_result_limit("delegate_wait")
|
||||
text = json.dumps(full, ensure_ascii=False, indent=2)
|
||||
if len(text) <= budget - _PAYLOAD_ENVELOPE_HEADROOM:
|
||||
full["output_delivery"] = {
|
||||
# An unresolved engine-side truncation makes even an inline-fitting payload
|
||||
# NOT the whole result: complete/consumed follow the verified fact.
|
||||
"complete": full_ok, "consumed": full_ok, "inline_is_preview": False,
|
||||
"total_chars": len(text), "artifact": None, "read_next": None,
|
||||
"note": ("The whole terminal payload is inline." if full_ok else
|
||||
"INLINE BUT INCOMPLETE AT THE SOURCE: the engine reported its "
|
||||
"primary output as a bounded preview and the full artifact could "
|
||||
"not be matched to the size or the preview the run itself reported "
|
||||
"(see primary_output_full). Treat this "
|
||||
"as incomplete evidence, not as the verdict."),
|
||||
}
|
||||
if full_note is not None:
|
||||
full["output_delivery"]["primary_output_full"] = full_note
|
||||
return full
|
||||
artifact = _stage_full_output(ctx, run_id, text)
|
||||
_emit(ctx, custody.OUTPUT_SPILLED, {"run_id": run_id, "total_chars": len(text),
|
||||
"artifact": (artifact or {}).get("path", ""),
|
||||
"bytes": (artifact or {}).get("bytes"),
|
||||
"sha256": (artifact or {}).get("sha256", ""),
|
||||
"staged": artifact is not None,
|
||||
"full_content": bool(full_ok and artifact is not None)})
|
||||
if entry is not None and artifact is not None:
|
||||
if entry.output_consumed and entry.output_sha and artifact["sha256"] != entry.output_sha:
|
||||
# The ack named OTHER bytes: a re-stage of different content at the same
|
||||
# path owes a fresh acknowledgement — consumed never transfers by path.
|
||||
entry.output_consumed = False
|
||||
entry.output_sha = artifact["sha256"]
|
||||
entry.output_artifact = artifact["path"]
|
||||
entry.output_complete = bool(full_ok)
|
||||
return _preview_payload(full, text, artifact, budget,
|
||||
consumed=bool(entry is not None and entry.output_consumed),
|
||||
full_ok=full_ok, full_note=full_note)
|
||||
|
||||
|
||||
# -- tools --------------------------------------------------------------------
|
||||
|
||||
|
||||
|
|
@ -1598,3 +1246,18 @@ def get_tools() -> List[ToolEntry]:
|
|||
|
||||
|
||||
__all__ = ["get_tools"]
|
||||
|
||||
|
||||
# v7next F2 (D07): moved spans live in their owner leaf; re-exported here
|
||||
# so this facade stays the single import surface for callers and tests.
|
||||
from ouroboros.tools.delegate_terminal_evidence import ( # noqa: E402, F401 -- intentional public re-exports
|
||||
_NESTED_HOME_NOTE,
|
||||
_NO_BOUNDARY_NOTE,
|
||||
_access_evidence,
|
||||
_containment_breach,
|
||||
_containment_evidence,
|
||||
_delivered_terminal_payload,
|
||||
_record_containment,
|
||||
_reported_cost,
|
||||
_terminal_payload,
|
||||
)
|
||||
|
|
|
|||
392
ouroboros/tools/delegate_terminal_evidence.py
Normal file
392
ouroboros/tools/delegate_terminal_evidence.py
Normal file
|
|
@ -0,0 +1,392 @@
|
|||
"""The terminal story of ONE delegated run, as the parent reads it.
|
||||
|
||||
Containment breach detection and evidence, the terminal payload with its access
|
||||
evidence and reported cost, and whole-or-declared-partial delivery. Extracted
|
||||
from ``ouroboros/tools/delegate.py`` at its size gate (v7 DEL1 split);
|
||||
``tools.delegate`` re-exports every name (same objects), so the wait loop, the
|
||||
tests and monkeypatch targets keep addressing them on THAT surface.
|
||||
|
||||
The v7 ledger named this leaf ``tools/delegate_terminal.py``; it lands as
|
||||
``delegate_terminal_evidence.py`` because upstream already owns
|
||||
``ouroboros/delegate_terminal.py`` (the terminal reconciliation boundary) and
|
||||
two neighbouring modules answering to one name would be a permanent grep trap
|
||||
(owner fork F-2=A; the rename is recorded in the carried ledger).
|
||||
|
||||
Every parent-scope name the moved bodies read at call time is DECLARED and read
|
||||
through ``_delegate()`` — the parent monolith keeps the one rebindable binding
|
||||
per member, so patches on the historical surface keep their teeth.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import TYPE_CHECKING, Any, Dict, Optional
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover - annotation-only names, lazy under future annotations
|
||||
from ouroboros.delegate_containment import _Breach
|
||||
from ouroboros.delegate_custody import RunCustody as _RunCustody
|
||||
from ouroboros.subagents import DelegatedRunShape
|
||||
from ouroboros.tools.registry import ToolContext
|
||||
|
||||
|
||||
def _delegate():
|
||||
"""The parent module, read at call time.
|
||||
|
||||
The parent owns the rebindable module state and the members tests
|
||||
monkeypatch there; reading them through the module at each call keeps
|
||||
one binding, where a from-import would freeze the value this leaf saw
|
||||
at import time (the owner-approved D18/D33 mechanical exception).
|
||||
"""
|
||||
from ouroboros.tools import delegate
|
||||
|
||||
return delegate
|
||||
|
||||
|
||||
def _containment_breach(detail: Dict[str, Any], authority: "DelegatedRunShape") -> Optional[_Breach]:
|
||||
"""Everything the ENGINE enforced, checked against what the host asked for.
|
||||
|
||||
ONE reader for both halves of containment — the access profile and the harness
|
||||
HOME — because they fail identically: the request is only a request, the engine
|
||||
derives the truth, and a verification written for one half leaves the other
|
||||
trusting an echo. The HOME half is asked only of a run that carried the marker;
|
||||
a read-only child is scoped by Claudexor's ordinary envelope and asks for nothing.
|
||||
"""
|
||||
widened = _delegate()._widened_access(detail, authority.access)
|
||||
if widened:
|
||||
return _delegate()._Breach(
|
||||
"access_profile_widened",
|
||||
f"The delegated run was enforced at access profile {widened!r} while this "
|
||||
f"task is only entitled to {authority.access!r}.",
|
||||
{"entitled_access": authority.access, "effective_access": widened},
|
||||
)
|
||||
if authority.delegated:
|
||||
return _delegate()._home_isolation_breach(detail)
|
||||
return None
|
||||
|
||||
|
||||
_NESTED_HOME_NOTE = (
|
||||
"The scoped harness HOME for this run sits INSIDE the operator's own home, which is "
|
||||
"where the engine roots its scoped homes. That is allowed and the run's work is usable, "
|
||||
"but it is not isolation from the operator's home: everything there — credential stores "
|
||||
"and the Claudexor daemon token included — stays readable at its absolute path. Do NOT "
|
||||
"describe this run as running in an isolated home"
|
||||
)
|
||||
|
||||
|
||||
_NO_BOUNDARY_NOTE = (
|
||||
"NO OS-ENFORCED BOUNDARY was applied to this run. The engine reported no confinement "
|
||||
"mechanism for it, so the only containment it had is a scoped HOME — a redirect of "
|
||||
"`~`-relative lookups, which leaves the operator's home, credential stores and the "
|
||||
"Claudexor daemon token readable at their absolute paths. The run was allowed and its "
|
||||
"work is usable; do NOT describe it as sandboxed, confined or isolated, and weigh its "
|
||||
"output as coming from an unconfined shell in this worktree"
|
||||
)
|
||||
|
||||
|
||||
def _containment_evidence(detail: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""What the ARTIFACTS prove about this run's containment — never what was asked.
|
||||
|
||||
DESTINATION 3 of the disclosure: this is what the nanny hands its parent.
|
||||
|
||||
BOTH halves, in one reader, because a report that states only the scoped HOME is the
|
||||
defect this function was rewritten to remove: a run with a kernel-enforced boundary
|
||||
and a run with none produced BYTE-IDENTICAL evidence here, both reading
|
||||
``verified: true`` with a note about the HOME. Claudexor's own confinement document
|
||||
says the scoped home "is not a boundary and must never be reported as one".
|
||||
|
||||
The predicate is what the engine says it APPLIED (``confinement_mechanism`` plus the
|
||||
denied path it proved), never which OS this host is. Ouroboros does not know what the
|
||||
engine did — only the artifact does — and a platform test would additionally freeze
|
||||
today's answer: the day a boundary ships for another OS, this reader is already right.
|
||||
|
||||
Judged by the SAME predicate that halts a breached run, not by having been reached
|
||||
after it: a report whose honesty depends on its call site is one refactor away from
|
||||
claiming a containment nobody checked.
|
||||
|
||||
This is also where a MISSING fact lands, because it is a reporting question and not an
|
||||
enforcement one: an attempt that disclosed nothing proves nothing, so ``verified``
|
||||
stays false and ``disclosed`` says how much of the run is actually covered. Silence
|
||||
read as success and silence enforced as a fault are the two ways to be wrong here,
|
||||
and stating the count avoids both.
|
||||
"""
|
||||
from ouroboros.gateways.claudexor import attempt_containment
|
||||
|
||||
attempts = attempt_containment(str(_delegate().custody.summary_of(detail).get("runDir") or ""))
|
||||
disclosed = sum(1 for attempt in attempts if attempt.home_isolated is not None)
|
||||
# An engine that reported nothing is indistinguishable from one that applied nothing,
|
||||
# and the mechanisms the ATTEMPTS name are the vocabulary — Ouroboros keeps no list of
|
||||
# its own to fall out of date. "Every attempt" and not "any": one unconfined attempt
|
||||
# is an unconfined run.
|
||||
mechanisms = sorted({attempt.boundary_mechanism for attempt in attempts})
|
||||
boundary = mechanisms[0] if attempts and len(mechanisms) == 1 and mechanisms[0] else ""
|
||||
# A3: the engine's own typed reason for a missing boundary — an AMPLIFIER of
|
||||
# the unconfined disclosure (why there is no mechanism on this host), parsed
|
||||
# from the same attempt artifact. Telemetry only, never an admission token.
|
||||
unavailable_reasons = sorted({
|
||||
attempt.confinement_unavailable_reason
|
||||
for attempt in attempts if attempt.confinement_unavailable_reason
|
||||
})
|
||||
# A3: a scoped home NESTED under the operator's own is allowed (the engine's
|
||||
# own layout — disclosed, never refused), but it is NOT "outside the
|
||||
# operator's own": the daemon token stays reachable at its absolute path.
|
||||
# Recorded on the report and honoured by every branch below, so a run that
|
||||
# ALSO carries an OS boundary can no longer be promoted to verified with a
|
||||
# note that contradicts its own artifact — and so `_record_containment` keeps
|
||||
# emitting the durable unconfined row for it.
|
||||
nested = _delegate().home_nested_under_operator_home(detail)
|
||||
report = {"verified": False, "attempts": len(attempts), "disclosed": disclosed,
|
||||
"os_boundary": boundary, "nested_under_operator_home": nested}
|
||||
if unavailable_reasons:
|
||||
report["confinement_unavailable_reason"] = "; ".join(unavailable_reasons)
|
||||
breach = _delegate()._home_isolation_breach(detail)
|
||||
if breach is not None:
|
||||
return {**report, "note": breach.detail}
|
||||
if not disclosed:
|
||||
return {**report, "note":
|
||||
"this run recorded no harness-HOME fact, so its confinement is UNPROVEN "
|
||||
"— do not report it as isolated"}
|
||||
if disclosed < len(attempts):
|
||||
return {**report, "note":
|
||||
"not every attempt of this run recorded a harness-HOME fact, so its "
|
||||
"confinement is UNPROVEN — do not report it as isolated"}
|
||||
if nested:
|
||||
note = _NESTED_HOME_NOTE
|
||||
if boundary:
|
||||
note += (
|
||||
f" (an {boundary} boundary WAS applied — weigh it as the real containment, "
|
||||
"but the scoped HOME is not one)"
|
||||
)
|
||||
if unavailable_reasons:
|
||||
note += " (engine-declared reason: " + "; ".join(unavailable_reasons) + ")"
|
||||
return {**report, "note": note}
|
||||
if not boundary:
|
||||
note = _NO_BOUNDARY_NOTE
|
||||
if unavailable_reasons:
|
||||
note += (
|
||||
" (engine-declared reason: " + "; ".join(unavailable_reasons) + ")"
|
||||
)
|
||||
return {**report, "note": note}
|
||||
return {**report, "verified": True, "note":
|
||||
f"every attempt recorded a scoped harness HOME outside the operator's own AND "
|
||||
f"an applied {boundary} boundary, proven against a path it denies"}
|
||||
|
||||
|
||||
def _terminal_payload(run_id: str, detail: Dict[str, Any],
|
||||
authority: "DelegatedRunShape") -> Dict[str, Any]:
|
||||
summary = _delegate().custody.summary_of(detail)
|
||||
payload = {
|
||||
"status": "terminal",
|
||||
"run_id": run_id,
|
||||
"state": str(summary.get("state") or ""),
|
||||
# The APPLIED model, from the engine's own summary — '' when the run
|
||||
# never disclosed one (live unpinned runs really do), shown as absence
|
||||
# rather than the requested model dressed up as the applied one.
|
||||
"model": str(summary.get("model") or ""),
|
||||
"outcome_banner": detail.get("outcomeBanner"),
|
||||
"outcome_facts": summary.get("outcomeFacts"),
|
||||
"output_conformance": summary.get("outputConformance"),
|
||||
"final_summary": detail.get("finalSummary"),
|
||||
"primary_output": detail.get("primaryOutput"),
|
||||
"failure": summary.get("failure"),
|
||||
"last_seq": int(detail.get("lastSeq") or 0),
|
||||
"cost": _reported_cost(summary),
|
||||
# The ACCESS half of the same honesty, on EVERY terminal payload — see
|
||||
# `_access_evidence`. Both lanes: `readonly` staying `readonly` is the profile
|
||||
# that matters most, while `containment` is asked only of marker-carrying runs.
|
||||
"access_evidence": _access_evidence(detail, authority.access),
|
||||
}
|
||||
if authority.delegated:
|
||||
payload["containment"] = _containment_evidence(detail)
|
||||
facts = payload.get("outcome_facts")
|
||||
if isinstance(facts, dict) and str(facts.get("reason") or "") == "input_required":
|
||||
# The codex-shaped question (B4): that lane has no mid-run channel, so a
|
||||
# question arrives as this TERMINAL. There is deliberately NO rerun verb
|
||||
# here — the engine's rerun_with_feedback would start a run outside this
|
||||
# task's custody trail — so the honest answer path is a plain new start.
|
||||
payload["input_required_note"] = (
|
||||
"This run ended NEEDING INPUT (outcome_facts.reason=input_required — "
|
||||
"see outcome_facts.work_state.required_inputs). Its harness has no "
|
||||
"mid-run question channel, so the question arrives as this terminal. Answer it by "
|
||||
"starting a plain NEW delegate_start(subagent_id=..., prompt=...) whose "
|
||||
"prompt carries the original "
|
||||
"assignment plus the answers; custody of the new run stays with you. "
|
||||
"Do not look for a rerun/decision verb — none exists on this surface."
|
||||
)
|
||||
return payload
|
||||
|
||||
|
||||
def _access_evidence(detail: Dict[str, Any], expected: str) -> Dict[str, Any]:
|
||||
"""What the engine's own DERIVED profile proves about this finished run.
|
||||
|
||||
``effectiveAccess`` is the only witness: ``summary["access"]`` is computed as
|
||||
``effectiveAccess ?? the client's own request``, so reading it compares the request
|
||||
against itself and always passes. A WIDER profile is already a breach before this
|
||||
runs; an ABSENT one cannot be enforced on a run that is over — cancelling a
|
||||
succeeded run to punish missing evidence would destroy the result the lane exists
|
||||
to fetch (the v6.87.37 lesson) — so it is named here instead.
|
||||
"""
|
||||
summary = _delegate().custody.summary_of(detail)
|
||||
effective = str(summary.get("effectiveAccess") or "")
|
||||
state = str(summary.get("state") or "")
|
||||
report = {"requested": expected, "effective": effective,
|
||||
"verified": bool(effective), "state": state}
|
||||
if effective:
|
||||
return report
|
||||
if state in _delegate().custody.SUCCEEDED_STATES:
|
||||
return {**report, "note":
|
||||
"this run SUCCEEDED without ever disclosing an effective access "
|
||||
f"profile, so there is no evidence the engine enforced {expected!r} — "
|
||||
"do not report its containment as verified"}
|
||||
return {**report, "note":
|
||||
"no effective access profile was disclosed; a run that did not succeed may "
|
||||
"never have had one, so this is absence of evidence, not a breach"}
|
||||
|
||||
|
||||
def _record_containment(ctx: ToolContext, entry: Optional[_RunCustody],
|
||||
payload: Dict[str, Any]) -> None:
|
||||
"""DESTINATION 1 of the disclosure: the durable record, written once per run.
|
||||
|
||||
A missing boundary is not a fault and produces no refusal, which is exactly why it
|
||||
needs a durable line of its own — the run succeeds, its patch is integrated, and
|
||||
nothing else in the record would ever say the work came out of an unconfined shell.
|
||||
Emitted from what the PARENT was told, so the two cannot disagree.
|
||||
|
||||
"Once per run" is now a DURABLE fact rather than a process-local one: the custody
|
||||
entry is replayed from the event log, so a restarted worker polling an already
|
||||
terminal run does not append a second identical finding.
|
||||
|
||||
A NESTED scoped home is disclosed even when an OS boundary WAS recorded (A3):
|
||||
the boundary is real containment, the scoped home is not, and suppressing the
|
||||
row for that shape left the one durable line that says "this ran with the
|
||||
operator's home reachable" unwritten.
|
||||
"""
|
||||
containment = payload.get("containment")
|
||||
if not isinstance(containment, dict):
|
||||
return
|
||||
if containment.get("os_boundary") and not containment.get("nested_under_operator_home"):
|
||||
return
|
||||
if entry is not None and entry.containment_disclosed:
|
||||
return
|
||||
_delegate()._emit(ctx, _delegate().custody.UNCONFINED, {
|
||||
"run_id": entry.run_id if entry is not None else "",
|
||||
"route": entry.route_id if entry is not None else "",
|
||||
"state": str(payload.get("state") or ""),
|
||||
"os_boundary": str(containment.get("os_boundary") or ""),
|
||||
"attempts": containment.get("attempts"),
|
||||
"home_disclosed": containment.get("disclosed"),
|
||||
"nested_under_operator_home": bool(containment.get("nested_under_operator_home")),
|
||||
"note": containment.get("note"),
|
||||
**({"confinement_unavailable_reason": containment["confinement_unavailable_reason"]}
|
||||
if containment.get("confinement_unavailable_reason") else {}),
|
||||
})
|
||||
if entry is not None:
|
||||
entry.containment_disclosed = True
|
||||
|
||||
|
||||
def _reported_cost(summary: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""What this run cost, as the AGENT will read it.
|
||||
|
||||
This is the payload the nanny relays to its parent, so it must tell the same story
|
||||
the ledger does. It used to hardcode `$0.00 / final` — the exact shape the settlement
|
||||
fix was written to eliminate — so a run that really charged money settled honestly in
|
||||
the ledger and then told the reasoning path the work was free.
|
||||
"""
|
||||
spend, estimated = _delegate().custody.disclosed_spend(summary)
|
||||
if spend is None:
|
||||
return {
|
||||
"cost_usd": None,
|
||||
"cost_final": False,
|
||||
"note": "the harness disclosed no spend for this run; treat the cost as UNKNOWN, not zero",
|
||||
}
|
||||
if estimated:
|
||||
# The amount is the best fact anyone has, so it rides; the FINALITY does not. An
|
||||
# estimated zero is not a proven free session and an estimated charge is not a
|
||||
# closed book — both are `cost_final: False`, matching the ledger row exactly.
|
||||
return {
|
||||
"cost_usd": spend,
|
||||
"cost_final": False,
|
||||
"note": "the harness ESTIMATED this run's spend rather than settling it; treat "
|
||||
"the amount as APPROXIMATE and the cost as NOT final",
|
||||
}
|
||||
if spend > 0:
|
||||
return {
|
||||
"cost_usd": spend,
|
||||
"cost_final": True,
|
||||
"note": "this run was BILLED — it did not ride the subscription",
|
||||
}
|
||||
return {
|
||||
"cost_usd": 0.0,
|
||||
"cost_final": True,
|
||||
"note": "subscription session — already paid; the nanny's own model calls are metered separately",
|
||||
}
|
||||
|
||||
|
||||
def _delivered_terminal_payload(ctx: ToolContext, run_id: str, detail: Dict[str, Any],
|
||||
authority: "DelegatedRunShape",
|
||||
entry: Optional[_RunCustody] = None,
|
||||
gateway: Any = None) -> Dict[str, Any]:
|
||||
"""The terminal payload, delivered whole or declared partial — never head-cut.
|
||||
|
||||
``final_summary``/``primary_output`` carry the run's real work product, and Claudexor
|
||||
returns a preview of up to 256 KiB. Outer truncation would head-cut that at the tool
|
||||
result limit and sever the JSON mid-string, which destroys the document rather than
|
||||
shortening it. So the payload bounds ITSELF against the same limit the truncator
|
||||
applies, and the remainder becomes a readable artifact — after the engine's bounded
|
||||
preview has been resolved to the verified full artifact, because a payload built on
|
||||
a truncated preview delivers 256 KiB wearing the whole result's name.
|
||||
"""
|
||||
full = _terminal_payload(run_id, detail, authority)
|
||||
if entry is not None:
|
||||
_delegate().add_terminal_source_verification(full, entry)
|
||||
# Requested-vs-applied model, the review lane's own lexicon and rule
|
||||
# (AgentSessionReviewExecutor): compared only when BOTH are non-empty —
|
||||
# the engine writes aliases ('sonnet' beside 'claude-opus-5'), so a
|
||||
# mismatch is an advisory disclosure, never a failure of the run.
|
||||
requested_model = str(getattr(entry, "model", "") or "") if entry is not None else ""
|
||||
applied_model = str(full.get("model") or "")
|
||||
if requested_model and applied_model and requested_model != applied_model:
|
||||
full["capability_delta"] = [{
|
||||
"kind": "capability_delta",
|
||||
"requested": f"model {requested_model}",
|
||||
"effective": f"model {applied_model}",
|
||||
"reason": "session_route_resolves_its_own_model",
|
||||
}]
|
||||
primary, full_ok, full_note = _delegate()._resolve_full_primary_output(
|
||||
gateway, run_id, full.get("primary_output"))
|
||||
full["primary_output"] = primary
|
||||
budget = _delegate().tool_result_limit("delegate_wait")
|
||||
text = json.dumps(full, ensure_ascii=False, indent=2)
|
||||
if len(text) <= budget - _delegate()._PAYLOAD_ENVELOPE_HEADROOM:
|
||||
full["output_delivery"] = {
|
||||
# An unresolved engine-side truncation makes even an inline-fitting payload
|
||||
# NOT the whole result: complete/consumed follow the verified fact.
|
||||
"complete": full_ok, "consumed": full_ok, "inline_is_preview": False,
|
||||
"total_chars": len(text), "artifact": None, "read_next": None,
|
||||
"note": ("The whole terminal payload is inline." if full_ok else
|
||||
"INLINE BUT INCOMPLETE AT THE SOURCE: the engine reported its "
|
||||
"primary output as a bounded preview and the full artifact could "
|
||||
"not be matched to the size or the preview the run itself reported "
|
||||
"(see primary_output_full). Treat this "
|
||||
"as incomplete evidence, not as the verdict."),
|
||||
}
|
||||
if full_note is not None:
|
||||
full["output_delivery"]["primary_output_full"] = full_note
|
||||
return full
|
||||
artifact = _delegate()._stage_full_output(ctx, run_id, text)
|
||||
_delegate()._emit(ctx, _delegate().custody.OUTPUT_SPILLED, {"run_id": run_id, "total_chars": len(text),
|
||||
"artifact": (artifact or {}).get("path", ""),
|
||||
"bytes": (artifact or {}).get("bytes"),
|
||||
"sha256": (artifact or {}).get("sha256", ""),
|
||||
"staged": artifact is not None,
|
||||
"full_content": bool(full_ok and artifact is not None)})
|
||||
if entry is not None and artifact is not None:
|
||||
if entry.output_consumed and entry.output_sha and artifact["sha256"] != entry.output_sha:
|
||||
# The ack named OTHER bytes: a re-stage of different content at the same
|
||||
# path owes a fresh acknowledgement — consumed never transfers by path.
|
||||
entry.output_consumed = False
|
||||
entry.output_sha = artifact["sha256"]
|
||||
entry.output_artifact = artifact["path"]
|
||||
entry.output_complete = bool(full_ok)
|
||||
return _delegate()._preview_payload(full, text, artifact, budget,
|
||||
consumed=bool(entry is not None and entry.output_consumed),
|
||||
full_ok=full_ok, full_note=full_note)
|
||||
|
|
@ -10,10 +10,12 @@ identity — the parent binding IS the leaf's object — and the hot-code label
|
|||
parity for the leaves, the same way the queue and loop splits pin both for
|
||||
theirs.
|
||||
|
||||
Two reference rows are deliberately absent: ``_capture_stranded_patch`` was
|
||||
One reference row is deliberately absent: ``_capture_stranded_patch`` was
|
||||
re-homed by upstream itself (public ``capture_stranded_patch`` in
|
||||
``tools/delegate_integration.py``), and the ``tools/delegate.py`` terminal
|
||||
leaf is hot-deferred on the delegate_terminal name-collision fork.
|
||||
``tools/delegate_integration.py``). The ``tools/delegate.py`` terminal leaf
|
||||
landed as ``delegate_terminal_evidence.py`` — the ledger's
|
||||
``tools/delegate_terminal.py`` name collided with upstream's own
|
||||
``ouroboros/delegate_terminal.py`` (owner fork F-2=A rename).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
|
@ -29,6 +31,13 @@ DELEGATE_LEAF_OWNERS: dict[str, dict[str, str]] = {
|
|||
"_retire_recovered_registration _reconcile_one"
|
||||
),
|
||||
},
|
||||
"ouroboros.tools.delegate": {
|
||||
"ouroboros.tools.delegate_terminal_evidence": (
|
||||
"_containment_breach _NESTED_HOME_NOTE _NO_BOUNDARY_NOTE _containment_evidence "
|
||||
"_terminal_payload _access_evidence _record_containment _reported_cost "
|
||||
"_delivered_terminal_payload"
|
||||
),
|
||||
},
|
||||
"ouroboros.tools.delegate_integration": {
|
||||
"ouroboros.tools.delegate_payload_patch": (
|
||||
"_reserved_payload_rel_path _snapshot_head_textual _write_payload_patch_artifacts "
|
||||
|
|
|
|||
|
|
@ -192,6 +192,19 @@ LEAVES: dict[str, tuple[str, str, frozenset[str]]] = {
|
|||
"ouroboros/tools/control_scheduling.py": ("ouroboros/tools/control.py", "_ctl", frozenset({
|
||||
"load_settings",
|
||||
})),
|
||||
# D07 finisher row (oracle ouroboros_v7_wip @ 9f691656, ledger rows
|
||||
# 3468-3476). The ledger's tools/delegate_terminal.py name collided with
|
||||
# upstream's own ouroboros/delegate_terminal.py, so the leaf landed as
|
||||
# delegate_terminal_evidence.py (owner fork F-2=A). The declared set is
|
||||
# maximal on tip bytes: EVERY parent-scope name the moved spans read at
|
||||
# call time goes through `_delegate()` (the reference cut this leaf with
|
||||
# plain preamble imports and declared only _emit).
|
||||
"ouroboros/tools/delegate_terminal_evidence.py": ("ouroboros/tools/delegate.py", "_delegate", frozenset({
|
||||
"_Breach", "_PAYLOAD_ENVELOPE_HEADROOM", "_emit", "_home_isolation_breach",
|
||||
"_preview_payload", "_resolve_full_primary_output", "_stage_full_output",
|
||||
"_widened_access", "add_terminal_source_verification", "custody",
|
||||
"home_nested_under_operator_home", "tool_result_limit",
|
||||
})),
|
||||
# D01 lane rows (L-B loop split + D38 agent dispatch), declared sets
|
||||
# re-derived on tip bytes by the transplant tool (reference table:
|
||||
# ouroboros_v7_wip @ 9f691656; tip additions over the reference are the
|
||||
|
|
|
|||
280
tests/test_subagent_worktree_registry_s6.py
Normal file
280
tests/test_subagent_worktree_registry_s6.py
Normal file
|
|
@ -0,0 +1,280 @@
|
|||
"""S6 C3/C4 — the private-snapshot registry when it cannot be read or written.
|
||||
|
||||
``state/subagent_worktrees.json`` is the third durable registry in the family
|
||||
(beside ``state/cancel_intents.json`` and ``state/terminal_deliveries.json``)
|
||||
and it now answers "malformed" the way they do: absent stays an ordinary empty
|
||||
registry, malformed refuses the mutation, keeps the bytes and discloses one
|
||||
typed ``subagent_worktree_registry_corrupt`` event. Pre-fix a live snapshot
|
||||
read as missing, the startup GC reported a clean sweep, and the next
|
||||
reconciliation overwrote the malformed bytes with a valid empty registry —
|
||||
stranding the checkout and the ``refs/ouroboros/delegated/*`` ref that pins its
|
||||
baseline with nothing left naming them.
|
||||
|
||||
C4 pins the write half: a failed registration now removes the checkout AND the
|
||||
baseline ref on the Git branch, the symmetry the payload sibling already had
|
||||
(``tests/test_delegated_skill_payload.py::test_registry_save_failure_leaves_no_orphan_snapshot_dir``).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import pathlib
|
||||
import subprocess
|
||||
|
||||
import pytest
|
||||
|
||||
from ouroboros import subagent_worktrees as wt
|
||||
|
||||
|
||||
MALFORMED = '"not a registry"'
|
||||
|
||||
|
||||
def _git(cwd, *args, check=True):
|
||||
return subprocess.run(
|
||||
["git", *args], cwd=str(cwd), capture_output=True, text=True, check=check,
|
||||
)
|
||||
|
||||
|
||||
def _seed_target(tmp_path: pathlib.Path) -> pathlib.Path:
|
||||
target = tmp_path / "target"
|
||||
target.mkdir()
|
||||
_git(target, "init")
|
||||
(target / "tracked.txt").write_text("one\n", encoding="utf-8")
|
||||
_git(target, "add", "-A")
|
||||
_git(target, "-c", "user.email=t@t", "-c", "user.name=t", "commit", "-m", "seed")
|
||||
return target
|
||||
|
||||
|
||||
def _registry(data_dir: pathlib.Path) -> pathlib.Path:
|
||||
return data_dir / "state" / "subagent_worktrees.json"
|
||||
|
||||
|
||||
def _events(data_dir: pathlib.Path):
|
||||
path = data_dir / "logs" / "events.jsonl"
|
||||
if not path.is_file():
|
||||
return []
|
||||
return [
|
||||
json.loads(line) for line in path.read_text(encoding="utf-8").splitlines()
|
||||
if line.strip()
|
||||
]
|
||||
|
||||
|
||||
def _snapshot(tmp_path, snapshot_id="snapS6"):
|
||||
"""One registered delegated execution snapshot; returns (target, snaps, data, handle)."""
|
||||
target = _seed_target(tmp_path)
|
||||
snaps, data = tmp_path / "snaps", tmp_path / "data"
|
||||
handle = wt.provision_execution_snapshot(
|
||||
target_root=target, task_id="t1", snapshot_id=snapshot_id,
|
||||
worktree_root=snaps, data_dir=data)
|
||||
return target, snaps, data, handle
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# C3 — absent is empty, malformed is refused
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_c3_absent_is_an_ordinary_empty_registry_in_both_modes(tmp_path):
|
||||
"""The strictness must separate ABSENT from MALFORMED: a never-written
|
||||
registry is the first-write case, never a refusal."""
|
||||
data = tmp_path / "data"
|
||||
(data / "state").mkdir(parents=True)
|
||||
|
||||
assert wt._load_registry(data_dir=data) == []
|
||||
assert wt._load_registry(data_dir=data, strict=True) == []
|
||||
assert wt.list_worktrees(data_dir=data) == []
|
||||
assert wt.find_execution_snapshot("nothing", data_dir=data) is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"payload", [MALFORMED, '{"worktrees": "nope"}', '{"worktrees": [', "\x00\x01"],
|
||||
)
|
||||
def test_c3_every_malformed_shape_is_refused_by_the_strict_read(tmp_path, payload):
|
||||
"""C3/O2: malformed is a fact of its own — the soft read still answers
|
||||
empty for the UI listing, the strict read raises for anything that writes."""
|
||||
data = tmp_path / "data"
|
||||
(data / "state").mkdir(parents=True)
|
||||
_registry(data).write_text(payload, encoding="utf-8")
|
||||
|
||||
assert wt._load_registry(data_dir=data) == [], "the inspection read stays soft"
|
||||
with pytest.raises(wt.SubagentWorktreeRegistryCorrupt):
|
||||
wt._load_registry(data_dir=data, strict=True)
|
||||
assert _registry(data).read_text(encoding="utf-8") == payload, "bytes are kept"
|
||||
assert any(
|
||||
row.get("type") == "subagent_worktree_registry_corrupt"
|
||||
for row in _events(data)
|
||||
), "the refusal is disclosed durably"
|
||||
|
||||
|
||||
def test_c3_a_live_snapshot_is_not_reported_missing_over_a_malformed_registry(tmp_path):
|
||||
"""C3/O2: the lookup that decides "does this binding still exist?" must not
|
||||
answer "no" from a file it could not read — the checkout and its pinned
|
||||
baseline ref are right there, and a false "missing" sends the caller off to
|
||||
provision a replacement."""
|
||||
target, _snaps, data, handle = _snapshot(tmp_path)
|
||||
assert wt.find_execution_snapshot("snapS6", data_dir=data) is not None
|
||||
_registry(data).write_text(MALFORMED, encoding="utf-8")
|
||||
|
||||
with pytest.raises(wt.SubagentWorktreeRegistryCorrupt):
|
||||
wt.find_execution_snapshot("snapS6", data_dir=data)
|
||||
assert pathlib.Path(handle.path).is_dir()
|
||||
assert _git(target, "rev-parse", handle.baseline_ref).stdout.strip() == handle.baseline_sha
|
||||
|
||||
|
||||
def test_c3_the_startup_gc_refuses_to_sweep_an_unreadable_registry(tmp_path):
|
||||
"""C3/O2: a destructive GC over an unknowable keep-set is exactly the case
|
||||
the delegated-snapshot prune already fails closed on when the custody log
|
||||
is unreadable (`server.py`). Reporting a clean sweep instead was the lie."""
|
||||
_target, snaps, data, handle = _snapshot(tmp_path)
|
||||
_registry(data).write_text(MALFORMED, encoding="utf-8")
|
||||
|
||||
with pytest.raises(wt.SubagentWorktreeRegistryCorrupt):
|
||||
wt.prune_execution_snapshots(set(), worktree_root=snaps, data_dir=data)
|
||||
assert pathlib.Path(handle.path).is_dir(), "nothing was removed"
|
||||
|
||||
|
||||
def test_c3_prune_orphans_never_overwrites_a_malformed_registry(tmp_path):
|
||||
"""C3/O2, the destructive half: startup reconciliation used to rewrite the
|
||||
malformed bytes as a valid EMPTY registry, taking the only record of the
|
||||
checkout and its pinned ref with it. It now refuses and keeps the bytes."""
|
||||
target, snaps, data, handle = _snapshot(tmp_path)
|
||||
_registry(data).write_text(MALFORMED, encoding="utf-8")
|
||||
|
||||
with pytest.raises(wt.SubagentWorktreeRegistryCorrupt):
|
||||
wt.prune_orphans(worktree_root=snaps, data_dir=data)
|
||||
|
||||
assert _registry(data).read_text(encoding="utf-8") == MALFORMED, "recovery material"
|
||||
assert pathlib.Path(handle.path).is_dir()
|
||||
assert _git(target, "rev-parse", handle.baseline_ref, check=False).returncode == 0
|
||||
|
||||
|
||||
def test_c3_a_new_snapshot_does_not_overwrite_a_malformed_registry(tmp_path):
|
||||
"""C3/O2: provisioning reads-appends-writes, so a soft read would replace a
|
||||
malformed registry with one holding only the new row. It refuses instead —
|
||||
and the refused attempt leaves nothing behind (the O3 cleanup path)."""
|
||||
target, snaps, data, first = _snapshot(tmp_path, snapshot_id="snapOne")
|
||||
_registry(data).write_text(MALFORMED, encoding="utf-8")
|
||||
|
||||
with pytest.raises(wt.SubagentWorktreeRegistryCorrupt):
|
||||
wt.provision_execution_snapshot(
|
||||
target_root=target, task_id="t1", snapshot_id="snapTwo",
|
||||
worktree_root=snaps, data_dir=data)
|
||||
|
||||
assert _registry(data).read_text(encoding="utf-8") == MALFORMED
|
||||
assert pathlib.Path(first.path).is_dir(), "the registered snapshot is untouched"
|
||||
assert not (snaps / "dlg_t1_snapTwo").exists(), "the refused attempt cleans up"
|
||||
assert _git(
|
||||
target, "rev-parse", "refs/ouroboros/delegated/snapTwo", check=False,
|
||||
).returncode != 0, "and leaves no pinned ref"
|
||||
|
||||
|
||||
def test_c3_the_healthy_registry_paths_are_unchanged(tmp_path):
|
||||
"""The fix must not cost the ordinary lifecycle anything."""
|
||||
target, snaps, data, handle = _snapshot(tmp_path)
|
||||
|
||||
assert wt.find_execution_snapshot("snapS6", data_dir=data)["path"] == handle.path
|
||||
assert wt.prune_execution_snapshots(
|
||||
{"snapS6"}, worktree_root=snaps, data_dir=data,
|
||||
) == {"removed": [], "kept": ["snapS6"]}
|
||||
assert wt.prune_orphans(worktree_root=snaps, data_dir=data) == {"removed": 0, "kept": 1}
|
||||
assert wt.remove_execution_snapshot("snapS6", worktree_root=snaps, data_dir=data) is True
|
||||
assert wt.list_worktrees(data_dir=data) == []
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# C4 — a registry write that fails on the Git branch
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_c4_a_git_branch_registry_write_failure_leaves_no_worktree_or_ref(
|
||||
tmp_path, monkeypatch,
|
||||
):
|
||||
"""C4/O3: registration is inside the cleanup scope on BOTH branches now.
|
||||
A failed registry write removes the checkout and deletes the baseline ref
|
||||
it pinned, instead of leaving a snapshot nothing can name plus a ref that
|
||||
holds its commit against git's own GC."""
|
||||
target = _seed_target(tmp_path)
|
||||
snaps, data = tmp_path / "snaps", tmp_path / "data"
|
||||
|
||||
def _boom(*_a, **_k):
|
||||
raise OSError("registry disk full")
|
||||
|
||||
monkeypatch.setattr(wt, "_save_registry", _boom)
|
||||
with pytest.raises(OSError, match="registry disk full"):
|
||||
wt.provision_execution_snapshot(
|
||||
target_root=target, task_id="t1", snapshot_id="snapFail",
|
||||
worktree_root=snaps, data_dir=data)
|
||||
|
||||
leftovers = sorted(p.name for p in snaps.glob("dlg_*")) if snaps.exists() else []
|
||||
assert leftovers == [], leftovers
|
||||
assert _git(
|
||||
target, "rev-parse", "refs/ouroboros/delegated/snapFail", check=False,
|
||||
).returncode != 0, "the baseline ref is gone with the checkout"
|
||||
assert _git(target, "worktree", "list").stdout.count("dlg_t1_snapFail") == 0
|
||||
|
||||
|
||||
def test_c4_b_the_acting_worktree_branch_cleans_up_on_registry_failure(
|
||||
tmp_path, monkeypatch,
|
||||
):
|
||||
"""C4/O3, the third provisioning branch: ``provision_worktree`` creates a
|
||||
checkout AND a task branch before it registers either. A corrupt registry
|
||||
(strict read) or a failed write must remove both — otherwise every retry
|
||||
strands one more unreclaimable worktree+branch pair, without bound."""
|
||||
target = _seed_target(tmp_path)
|
||||
snaps, data = tmp_path / "snaps", tmp_path / "data"
|
||||
|
||||
# Leg 1: corrupt registry — the strict read refuses AFTER the checkout
|
||||
# exists; the refused attempt must leave neither checkout nor branch.
|
||||
(data / "state").mkdir(parents=True)
|
||||
_registry(data).write_text(MALFORMED, encoding="utf-8")
|
||||
with pytest.raises(wt.SubagentWorktreeRegistryCorrupt):
|
||||
wt.provision_worktree(
|
||||
repo_dir=target, task_id="acting9", worktree_root=snaps, data_dir=data)
|
||||
assert not (snaps / "acting9").exists(), "the refused attempt cleans up"
|
||||
assert _git(
|
||||
target, "rev-parse", "--verify", f"{wt._BRANCH_PREFIX}acting9", check=False,
|
||||
).returncode != 0, "and takes the task branch with it"
|
||||
assert _registry(data).read_text(encoding="utf-8") == MALFORMED
|
||||
|
||||
# Leg 2: registry write failure over a healthy registry — same symmetry.
|
||||
_registry(data).unlink()
|
||||
|
||||
def _boom(*_a, **_k):
|
||||
raise OSError("registry disk full")
|
||||
|
||||
monkeypatch.setattr(wt, "_save_registry", _boom)
|
||||
with pytest.raises(OSError, match="registry disk full"):
|
||||
wt.provision_worktree(
|
||||
repo_dir=target, task_id="acting9", worktree_root=snaps, data_dir=data)
|
||||
assert not (snaps / "acting9").exists()
|
||||
assert _git(
|
||||
target, "rev-parse", "--verify", f"{wt._BRANCH_PREFIX}acting9", check=False,
|
||||
).returncode != 0
|
||||
assert _git(target, "worktree", "list").stdout.count("acting9") == 0
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Disclosure — the registry's own shape
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_the_registry_has_no_version_and_two_kind_discriminated_shapes(tmp_path):
|
||||
"""Disclosure (MIGRATION_v7.md): unlike its two sibling registries this one
|
||||
carries NO ``schema_version``, and its two row shapes are told apart only
|
||||
by ``kind == "delegated_exec"``. A future format change has to add the
|
||||
discriminator it lacks before it can migrate anything.
|
||||
"""
|
||||
target = _seed_target(tmp_path)
|
||||
snaps, data = tmp_path / "snaps", tmp_path / "data"
|
||||
wt.provision_worktree(
|
||||
repo_dir=target, task_id="acting1", worktree_root=snaps, data_dir=data)
|
||||
wt.provision_execution_snapshot(
|
||||
target_root=target, task_id="t1", snapshot_id="snapKind",
|
||||
worktree_root=snaps, data_dir=data)
|
||||
|
||||
envelope = json.loads(_registry(data).read_text(encoding="utf-8"))
|
||||
assert set(envelope) == {"worktrees"}, "no version field to dispatch on"
|
||||
kinds = [row.get("kind", "") for row in envelope["worktrees"]]
|
||||
assert sorted(kinds) == ["", "delegated_exec"], (
|
||||
"the acting-worktree row carries no kind at all; absence IS the shape"
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue