Simplify delegated binding and disclose target drift

Co-authored-by: Ouroboros <311266734+ouroboros-agent@users.noreply.github.com>
This commit is contained in:
Ouroboros 2026-09-20 18:40:05 +03:00
parent 73e10a38a9
commit bc1b90ccba
13 changed files with 132 additions and 120 deletions

View file

@ -41,20 +41,18 @@ The harness can execute model-generated commands under the operator's OS
identity. It is not assumed hostile, but the host cannot review each command
before it runs.
When a private snapshot exists, the host appends a canonical execution binding
after the inherited work order: the snapshot is the only writable root and the
stable project root is a read-only identity until explicit integration. The
binding supersedes path fields in the inherited assignment; legacy engines that
expose only the snapshot as `scope.root` receive the same binding using that
root. Full native access does not make that path binding enforceable by itself,
so terminal capture rechecks the authority root against the recorded baseline.
A ready-no-changes result with drift or an unverifiable check is a failed capture
with retained evidence; an explicit reject can still release that empty private
snapshot, while apply remains refused. A ready-with-changes result retains its
private artifact and lets the existing locked apply check decide whether
integration is safe. Authority checks include eligible file inputs and excluded
untracked preimages, so an omitted sensitive file cannot turn a changed target
into a clean result.
When a private snapshot exists, the host appends a separate typed execution
binding after the immutable inherited work order: the snapshot is the writable
root and the stable project root is a read-only identity until explicit
integration. Legacy engines that expose only the snapshot as `scope.root`
receive the same binding using that root. Full native access does not make the
binding enforceable by itself, so terminal capture records authority-tree drift
as diagnostic evidence. A ready-no-changes result remains a normal no-change
capture even when the shared authority tree moved; the evidence names the
changed paths and keeps authorship unknown. A ready-with-changes result retains
its private artifact and the existing locked apply check decides whether
integration is safe. Excluded nested repositories remain outside the snapshot
inventory and are disclosed as an untracked residual.
A read-only child requests `mode: ask`, `access: readonly` under Claudexor's
ordinary envelope. The host reads effective access back for both shapes;

View file

@ -14,7 +14,7 @@ The manifest is the SSOT of the module→domain assignment (1:1, complete over t
| D04 | Tool execution: registry, access & typed results | 21 | 0 |
| D05 | Tool surfaces: files, code, shell, media, external | 28 | 0 |
| D06 | Review stack | 67 | 0 |
| D07 | Delegation, subagents & Claudexor | 51 | 0 |
| D07 | Delegation, subagents & Claudexor | 52 | 0 |
| D08 | Supervisor: queue, workers, events & runtime control | 46 | 0 |
| D09 | Cancellation, owner control & process custody | 13 | 0 |
| D10 | Git, update & release machinery | 28 | 0 |
@ -28,7 +28,7 @@ The manifest is the SSOT of the module→domain assignment (1:1, complete over t
| D18 | Launcher, packaging, platform & shared substrate | 14 | 0 |
| D19 | Frozen contracts (ABI) | 10 | 0 |
| D20 | Presence | 10 | 0 |
| **total** | | **555** | **0** |
| **total** | | **556** | **0** |
## Dependency direction matrix (strict, pinned)
@ -419,6 +419,7 @@ No function body (≥ 10 normalized lines) is shared verbatim across domains. Ne
- `ouroboros/delegate_start_instructions.py`
- `ouroboros/delegate_state_sweep.py`
- `ouroboros/delegate_supervision.py`
- `ouroboros/delegate_target_drift.py`
- `ouroboros/delegate_terminal.py`
- `ouroboros/depth_evidence.py`
- `ouroboros/gateways/__init__.py`

View file

@ -472,6 +472,7 @@ server.py (Starlette+uvicorn) ← HTTP + WebSocket on configurable host:port (de
│ ├── join_ledger.py ← Child-result absorption: validates lineage + exact hashes; dispositions integrated/irrelevant/deferred; `CHILD_RESULT_STALE`; keeps peek_task/discard_child_result
│ ├── delegate.py ← Delegation facade verbs `delegate_start` (with `retry_of`), `delegate_wait`, `delegate_cancel`, `delegate_answer`; the host pre-start rides the same wrapper and the shared `subagent_runtime.exact_start` (§6 Delegated subagents)
│ ├── delegate_integration.py ← Delegated-patch integration: `_mutation_authority`, `_provision_snapshot`, retry-binding validation, `_capture_terminal_patch`; the skill-payload cluster (`_payload_mutation_authority`, `_write_payload_patch_artifacts`; reserved paths refuse the WHOLE apply as `blocked_reserved_paths`) and `integrate_payload_patch` (CAS, index-free git apply in NO-REPOSITORY mode under `GIT_CEILING_DIRECTORIES`, typed `INTEGRATE_APPLY_NO_OP`; a successful apply QUEUES the extension reconcile request via `request_extension_reconcile`) (§6 Delegated subagents)
│ ├── delegate_target_drift.py ← Read-only authority-tree drift evidence for delegated capture; records changed paths without attributing them to the child or blocking a normal no-change disposition (§6 Delegated subagents)
│ ├── delegate_payload_patch.py, delegate_terminal_evidence.py, subagent_integration_delegated.py ← Delegation leaves: the skill-payload patch pipeline (capture artifacts, guards, the live apply); the terminal story of ONE delegated run as the parent reads it; `integrate_delegated_patch`, the delegated-run disposition seam
│ ├── subagent_integration.py ← integrate_subagent_patch (sha256, 3-way --index, protected-path gated, genesis refused), external-workspace audited verdict, `coop_already_in_tree` no-op, compare_subagent_patches
│ └── patch_verdict.py ← The ONE verdict writer for both patch pipelines (`_write_verdict`): subjects are minted by the writer (`run_<rid>`), never prefix-matched by readers; each decision lands twice — artifact + typed `delegate_run_patch_verdict` custody row
@ -670,4 +671,3 @@ Bundled resources use the CLI / Headless Boundary lookup order rather than assum
The generated `docs/inventories/DATA_LAYOUT_INVENTORY.md` probes every entry of this tree: its last literal path segment must be a tracked repo path or directory, or a literal in the runtime sources. A durable file renamed in code while its row here survives therefore turns red, not silent — but this is a basename-and-substring check and proves nothing stronger about an entry.
---

View file

@ -321,7 +321,7 @@ WHERE a mutating run's changes are destined is the second, separate record — t
A payload target gets a standalone private Git snapshot (`subagent_worktrees.provision_payload_snapshot`): the live payload is never initialized as Git, and capture trusts nothing under the child-writable snapshot's `.git`. Disposition (`integrate_payload_patch`) applies a live, index-free `git apply` in no-repository mode under a whole-payload content-hash CAS (drift = typed conflict; identical content = idempotent applied) — `GIT_CEILING_DIRECTORIES` is pinned at the payload's resolved PARENT, because git still searches the ceiling entry itself and an ancestor Git worktree above the runtime data root could otherwise make git skip every hunk at rc=0 — and reserved paths refuse the WHOLE apply as `blocked_reserved_paths` with the candidate preserved. The post-apply outcome set is complete: a live loader hash equal to the recorded RESULT hash is the success; a hash equal to the recorded BASELINE hash with a non-empty touched set is a provable non-mutation that RESOLVES the apply intent (typed `INTEGRATE_APPLY_NO_OP`, the `apply_no_op` arm: no success, no disposition, no reconcile queued, retry lane open); anything else is the ambiguous mismatch, whose intent stays PENDING and whose reconcile marker IS queued because the payload did mutate. A successful apply queues the extension reconcile (`request_extension_reconcile`) and the skill's review goes STALE pending fresh `skill_preflight`/`skill_review`; a run whose ONLY change is a mode flip is already refused at CAPTURE time as `unreviewable_metadata_change`, so a live hash still equal to the baseline means nothing was written.
**A mutating run executes in a PRIVATE EXECUTION SNAPSHOT, never in the shared tree** (metered children keep sharing the tree; their patches integrate through `integrate_subagent_patch` — sha256-bound, 3-way `--index`, protected-path gated, genesis refused, `coop_already_in_tree` a no-op). At `delegate_start` the host snapshots the target's real state — tracked + staged + eligible untracked, with the sensitive/credential veto DECIDED BEFORE ANYTHING IS HASHED: a blanket `git add -A` would write vetoed secrets, `.env` included, into the shared object database. The baseline commit is pinned by a `refs/ouroboros/delegated/` ref and checked out as a detached worktree (`provision_execution_snapshot`, registered before the start intent); `scope.root` stays the stable authority target while `execution.workspaceRoot` names the snapshot. The host appends a canonical execution binding after the inherited assignment: the snapshot is the sole writable root, and the authority target is a read-only identity until explicit integration. This binding is required because full native access has no filesystem sandbox and a model can otherwise select an absolute authority path directly. Terminal capture records one durable, read-only authority check before minting `PATCH_CAPTURED`: a ready-no-changes result with proven target drift or an unverifiable check becomes a typed failed capture with the snapshot and evidence retained, never `ready_no_changes` or a private-only provenance claim. An explicit reject may release that empty result, while apply remains refused. A ready-with-changes result retains its private artifact and discloses target drift; integration still runs the existing locked baseline proof, which refuses changed or unverifiable targets. The authority check covers file-input and excluded-untracked preimages as well as Git paths. Skill-payload captures use their existing content-hash CAS rather than a Git target check. The typed snapshot binding is recorded durably on the custody rows BEFORE the POST; an explicit retry reproduces it exactly, and a GC-collected snapshot is a typed `execution_snapshot_missing` refusal, never a re-mint. Worktrees live in the durable registry `state/subagent_worktrees.json` (`subagent_worktrees.py`; `provision_genesis_project` never enters registry or GC); removal is explicit or custody-cross-checked startup GC, fail-closed — an unreadable custody log replays as "no open runs", so the prune skips rather than destroy a child's only copy of its work. The run still runs `live` from the engine's view, which is why the scoped-HOME/`delegated` marker below applies unchanged.
**A mutating run normally executes in a PRIVATE EXECUTION SNAPSHOT** (metered children keep sharing the tree; their patches integrate through `integrate_subagent_patch` — sha256-bound, 3-way `--index`, protected-path gated, genesis refused, `coop_already_in_tree` a no-op). At `delegate_start` the host snapshots the target's real state — tracked + staged + eligible untracked, with the sensitive/credential veto DECIDED BEFORE ANYTHING IS HASHED: a blanket `git add -A` would write vetoed secrets, `.env` included, into the shared object database. The baseline commit is pinned by a `refs/ouroboros/delegated/` ref and checked out as a detached worktree (`provision_execution_snapshot`, registered before the start intent); `scope.root` stays the stable authority target while `execution.workspaceRoot` names the snapshot. The host appends a separate typed execution binding after the immutable inherited work order: the snapshot is the writable root and the authority target is a read-only identity until explicit integration. Directory-copy runs use the engine-created copy and never invent the source folder as its execution root. This binding is required because full native access has no filesystem sandbox and a model can otherwise select an absolute authority path directly. Terminal capture records authority-tree drift as diagnostic evidence; a ready-no-changes result stays a normal no-change capture with authorship unknown, while ready-with-changes retains its private artifact and the existing locked baseline proof decides whether integration is safe. Nested untracked repositories are excluded from the snapshot inventory and disclosed. Skill-payload captures use their existing content-hash CAS rather than a Git target check. The typed snapshot binding is recorded durably on the custody rows BEFORE the POST; an explicit retry reproduces it exactly, and a GC-collected snapshot is a typed `execution_snapshot_missing` refusal, never a re-mint. Worktrees live in the durable registry `state/subagent_worktrees.json` (`subagent_worktrees.py`; `provision_genesis_project` never enters registry or GC); removal is explicit or custody-cross-checked startup GC, fail-closed — an unreadable custody log replays as "no open runs", so the prune skips rather than destroy a child's only copy of its work. The run still runs `live` from the engine's view, which is why the scoped-HOME/`delegated` marker below applies unchanged.
At terminal, `delegate_wait` captures the run's diff against the baseline durably into the task's artifact store; NOTHING reaches the target automatically — the nanny explicitly applies or rejects through `integrate_delegated_patch`. Git and skill captures are whole-result operations: omitted `paths` and an exact empty list select the same captured result, disclosed when explicit; nonempty selectors remain directory-only. Engine-directory empty/subset semantics are unchanged. The staging substrate differs: a GIT workspace target applies under the repo git lock after PROVING no touched path drifted from `baseline_sha` — a plain `git apply` relocates hunks by offset, and the touched-path set is read from `git apply --numstat` in BOTH directions because each direction names only the paths it writes — then applies and STAGES, never commits. A SKILL-PAYLOAD target captures through the payload adapter over a parent-owned trusted index and applies LIVE into the non-Git payload — nothing is staged into any active root and no `.git` or index is created in the payload. The protected-path gate applies only when the target IS the Ouroboros body; a conflict (proven drift) is owned by the still-running nanny, with snapshot and patch persisting until explicit resolution or discard. Mutation rides an apply-intent protocol: a durable `delegate_run_patch_apply_started` row lands before any tree mutation, so on replay a pending intent without a disposition answers typed `INTEGRATE_DELEGATED_APPLY_AMBIGUOUS`, resolved only by explicit `acknowledge_ambiguous=true`, while the provably non-mutating outcomes (a lock error, proven baseline drift, a failed apply, a verified revert, a baseline-equal payload hash) RESOLVE the intent. `patch_verdict.py` is the ONE verdict writer for both pipelines: subjects are minted `run_<rid>` by the writer, never prefix-matched by readers, and each decision lands twice — artifact plus typed `delegate_run_patch_verdict` custody row — with a failed artifact write disclosed on the row. `artifacts.delegated_capture_read_target` narrowly rebinds `artifact_store` READS for the owning task's own `delegated_runs/` prefix, and `delegate_shared.orphan_capture_read_target` extends the same read-only, one-directory READ to the terminal-owner ORPHAN the disposition rule authorizes (confirmed by `orphan_disposition_status`), so the actor that may dispose a patch can inspect it without widened write authority. A read-only child stays in Claudexor's default envelope — one transport with one derived difference, not a second pipeline.

View file

@ -2,7 +2,6 @@
from __future__ import annotations
import json
from hashlib import sha256
@ -88,8 +87,8 @@ def execution_binding_instruction(execution_root: str, authority_root: str) -> s
if not execution:
return ""
return (
"\n\nDELEGATED EXECUTION BINDING (canonical host fact; supersedes path fields "
"in the inherited contract for this run): "
"\n\nDELEGATED EXECUTION BINDING (separate typed host fact; the canonical "
"work order remains byte-identical): "
f"the sole writable execution root for this run is {execution}. "
"Use relative paths or absolute paths under that root for every shell, "
"file, and patch operation. The stable authority/project root "
@ -101,51 +100,23 @@ def execution_binding_instruction(execution_root: str, authority_root: str) -> s
)
def _rebind_json_block(text: str, marker: str, execution_root: str, authority_root: str) -> str:
start = text.find(marker)
if start < 0:
return text
json_start = text.find("\n", start)
if json_start < 0:
return text
json_start += 1
try:
value, used = json.JSONDecoder().raw_decode(text[json_start:])
except (TypeError, ValueError):
return text
if not isinstance(value, dict):
return text
def rewrite(node):
if isinstance(node, dict):
for key, child in list(node.items()):
if key in {"workspace_root", "write_root"} and isinstance(child, str) and child:
node[key] = execution_root
else:
rewrite(child)
node.setdefault("authority_target_root", authority_root)
elif isinstance(node, list):
for child in node:
rewrite(child)
rewrite(value)
rendered = json.dumps(value, ensure_ascii=False, sort_keys=True)
return text[:json_start] + rendered + text[json_start + used:]
def directory_copy_binding_instruction(authority_root: str) -> str:
"""Tell a directory-copy child that the engine creates its write root later."""
authority = str(authority_root or "").strip() or "(unknown)"
return (
"\n\nDELEGATED DIRECTORY COPY BINDING (separate typed host fact; the canonical "
"work order remains byte-identical): the engine will create a private "
"execution copy for this run. Use only the engine-provided working "
"directory/cwd for writes; the selected authority folder "
f"{authority} is a read-only source reference. Do not write to that "
"authority folder directly."
)
def bind_execution_assignment(text: str, execution_root: str, authority_root: str) -> str:
"""Rewrite child-facing structured path fields before appending the binding."""
bound = _rebind_json_block(text, "HOST TASK CONTRACT AUTHORITY", execution_root, authority_root)
return _rebind_json_block(bound, "HOST AUTHORITY BINDING", execution_root, authority_root)
def apply_execution_binding(instructions: str, prompt: str, compiled: bool,
execution_root: str, authority_root: str) -> tuple[str, str]:
binding = execution_binding_instruction(execution_root, authority_root)
instructions = bind_execution_assignment(instructions, execution_root, authority_root) + binding
if compiled:
prompt = bind_execution_assignment(prompt, execution_root, authority_root) + binding
return instructions, prompt
def apply_execution_binding(instructions: str, execution_root: str,
authority_root: str) -> str:
"""Append one typed binding without rewriting canonical work-order bytes."""
return instructions + execution_binding_instruction(execution_root, authority_root)
def append_coordination_context(

View file

@ -3,6 +3,7 @@
from __future__ import annotations
import pathlib
import os
import subprocess
from typing import Any, Dict, List
@ -20,13 +21,14 @@ def _target_drift_evidence(entry: Any) -> Dict[str, Any]:
evidence["error"] = f"authority target is not a Git worktree: {target}"
return evidence
try:
git_env = {**os.environ, "GIT_OPTIONAL_LOCKS": "0"}
def git_error(proc, fallback):
detail = proc.stderr or proc.stdout or b""
return (detail.decode("utf-8", "replace") if isinstance(detail, bytes) else str(detail)).strip() or fallback
baseline_files = subprocess.run(
["git", "ls-tree", "-r", "-z", "--name-only", baseline],
cwd=str(target), capture_output=True, check=False,
cwd=str(target), capture_output=True, check=False, env=git_env,
)
if baseline_files.returncode != 0:
evidence["error"] = git_error(baseline_files, f"git ls-tree exited {baseline_files.returncode}")
@ -62,6 +64,8 @@ def _target_drift_evidence(entry: Any) -> Dict[str, Any]:
for row in excluded_rows:
if not isinstance(row, dict) or not row.get("path"):
continue
if row.get("reason") == "nested_repository" and not isinstance(row.get("baseline"), dict):
continue
relative, before = str(row["path"]), row.get("baseline")
if not isinstance(before, dict):
evidence["error"] = f"baseline identity unavailable for excluded path {relative}"
@ -70,7 +74,7 @@ def _target_drift_evidence(entry: Any) -> Dict[str, Any]:
evidence["paths"].append(relative)
diff = subprocess.run(
["git", "diff", "--name-only", "-z", "--no-renames", baseline, "--"],
cwd=str(target), capture_output=True, check=False,
cwd=str(target), capture_output=True, check=False, env=git_env,
)
if diff.returncode != 0:
evidence["error"] = git_error(diff, f"git diff exited {diff.returncode}")
@ -82,7 +86,7 @@ def _target_drift_evidence(entry: Any) -> Dict[str, Any]:
)
untracked = subprocess.run(
["git", "ls-files", "-z", "--others", "--exclude-standard"],
cwd=str(target), capture_output=True, check=False,
cwd=str(target), capture_output=True, check=False, env=git_env,
)
if untracked.returncode != 0:
evidence["error"] = (untracked.stderr or b"").decode("utf-8", "replace").strip() or \
@ -109,7 +113,6 @@ def _target_drift_paths(entry: Any) -> List[str]:
def _persist_target_drift(manifest_path: pathlib.Path, manifest: Dict[str, Any],
evidence: Dict[str, Any]) -> Dict[str, Any]:
from ouroboros.headless import ARTIFACT_STATUS_READY_NO_CHANGES
from ouroboros.utils import atomic_write_json, utc_now_iso
updated = dict(manifest)
@ -121,9 +124,5 @@ def _persist_target_drift(manifest_path: pathlib.Path, manifest: Dict[str, Any],
}
updated["authority_drift_status"] = (
"unknown" if evidence.get("error") else "changed" if evidence.get("paths") else "clean")
if (evidence.get("error") or evidence.get("paths")) \
and updated.get("status") == ARTIFACT_STATUS_READY_NO_CHANGES:
updated["status"] = "failed"
updated["note"] = "Authority-tree drift was not accepted as a clean no-change capture; snapshot preserved."
atomic_write_json(manifest_path, updated, trailing_newline=True)
return updated

View file

@ -146,6 +146,7 @@ D20 = "Presence"
"ouroboros/delegate_source_coverage.py" = "D07"
"ouroboros/delegate_start_claims.py" = "D07"
"ouroboros/delegate_start_instructions.py" = "D07"
"ouroboros/delegate_target_drift.py" = "D07"
"ouroboros/delegate_supervision.py" = "D07"
"ouroboros/delegate_terminal.py" = "D07"
"ouroboros/deliverables_paths.py" = "D17"

View file

@ -545,6 +545,10 @@ def provision_execution_snapshot(
file_inputs: List[str] = []
capture_warnings: List[Dict[str, Any]] = []
for rel in (p for p in untracked_raw.split("\0") if p):
candidate = target / rel
if candidate.is_dir() and not candidate.is_symlink():
excluded.append({"path": rel, "reason": "nested_repository"})
continue
reference: List[str] = []
reason = untracked_capture_veto_reason(target, rel, file_outputs=reference, warnings=capture_warnings)
if reference:

View file

@ -57,6 +57,7 @@ from ouroboros.delegate_start_instructions import (
access_instruction,
append_coordination_context,
apply_execution_binding,
directory_copy_binding_instruction,
)
from ouroboros.subagent_runtime import ( # noqa: F401 - shared primitive re-export
delegate_start_entry as _delegate_start_entry,
@ -471,10 +472,11 @@ def _delegate_start(ctx: ToolContext, prompt: str, max_seconds: Optional[int] =
resource_ref = dict(record_auth.get("resource_ref") or {})
execution_root = (root if directory_options.get("isolation") == "live" else "") if directory_options else delegated_execution_workspace_root(gateway, authority, root)
scope_root = target_root if execution_root or directory_options else root
if snapshot is not None or (directory_options and directory_options.get("isolation") == "envelope"):
instructions, text = apply_execution_binding(
instructions, text, bool(actor.get("compiled_work_order")),
execution_root or root, target_root)
if snapshot is not None:
instructions = apply_execution_binding(
instructions, execution_root or root, target_root)
elif directory_options and directory_options.get("isolation") == "envelope":
instructions += directory_copy_binding_instruction(target_root)
(project_id, owned_project_id, project_persistent) = resolve_registration(
gateway, scope_root, execution_root, getattr(authority, "access", ""))
if directory_options:
@ -499,9 +501,7 @@ def _delegate_start(ctx: ToolContext, prompt: str, max_seconds: Optional[int] =
idempotency_key=key, invocation_id=invocation_id,
max_seconds=seconds, request=request_body, project_id=project_id,
project_owned=bool(owned_project_id), project_persistent=project_persistent, route=route.route_id,
# Recovered pending invocations retain their original lineage.
root_task_id=str(lineage.get("root_task_id") or ""), parent_task_id=str(lineage.get("parent_task_id") or ""),
# Before POST, persist target/baseline and only known execution paths.
snapshot_id=snapshot_id, execution_root=(root if snapshot_id or resource_ref.get("strategy") == "direct" else ""),
baseline_sha=baseline_sha, target_root=target_root,
authority_source=authority_source, resource_ref=resource_ref,

View file

@ -465,37 +465,25 @@ def _capture_block(entry: _RunCustody, cap_dir: pathlib.Path,
block["target_drift_checked"] = True
if drift_paths:
block["target_mutated_during_run"] = drift_paths
if status == ARTIFACT_STATUS_READY_WITH_CHANGES:
block["note"] = (
"NOT APPLIED: the run edited its private execution snapshot only. "
"Authority-tree drift was observed while the result was captured; "
"the existing locked baseline check will decide whether integration "
"is safe. Nothing reaches the shared tree until explicit disposition."
)
block["note"] = (
"The private execution snapshot has no captured file changes, but the "
"authority tree changed while the run was open. The author is unknown: "
"the child, a neighbor, or another process may have written there. "
"The drift is diagnostic evidence; it is not attributed to this child."
if status == ARTIFACT_STATUS_READY_NO_CHANGES else
"NOT APPLIED: the run edited its private execution snapshot only. "
"Authority-tree drift was observed while the result was captured; "
"the existing locked baseline check will decide whether integration "
"is safe. Nothing reaches the shared tree until explicit disposition."
)
if drift_error:
block["target_drift_unknown"] = drift_error
if status == ARTIFACT_STATUS_READY_WITH_CHANGES:
if status == ARTIFACT_STATUS_READY_NO_CHANGES:
block["note"] = (
"NOT APPLIED: the run edited its private execution snapshot only. "
f"Authority-tree drift could not be verified ({drift_error}); the "
"locked integration check remains fail-closed. Nothing reaches the "
"shared tree until explicit disposition."
)
if (drift_paths or drift_error) and status == ARTIFACT_STATUS_READY_NO_CHANGES:
block["status"] = "failed"
if drift_error:
block["note"] = (
"TARGET DRIFT UNKNOWN: the host could not prove that the authority "
f"tree stayed unchanged ({drift_error}). No disposition is authorized; "
"preserve the snapshot and captured material for inspection."
)
else:
block["note"] = (
"TARGET MUTATED DURING RUN: the authority tree changed relative to the "
"delegated baseline while this run was open. The host cannot attribute "
"those bytes to the child, so the private capture is not a clean "
"no-changes result and no disposition is authorized. Preserve the "
"snapshot and captured material for inspection."
"The private execution snapshot has no captured file changes, but "
f"authority-tree drift could not be verified ({drift_error}). "
"The author is unknown; preserve this diagnostic fact and use the "
"normal no-change disposition."
)
if status not in {ARTIFACT_STATUS_READY_WITH_CHANGES, ARTIFACT_STATUS_READY_NO_CHANGES}:
# A failed manifest's own typed note (unreviewable_metadata_change,

View file

@ -2,6 +2,7 @@
from __future__ import annotations
class _GitOps:
def __init__(self):
self.safe_restart_calls = []
@ -63,3 +64,24 @@ def test_bootstrap_matching_repo_identity_keeps_managed_reset(monkeypatch, tmp_p
assert message == "reset"
assert git.safe_restart_calls == [{"reason": "bootstrap", "unsynced_policy": "rescue_and_reset"}]
assert git.local_sync_calls == []
def test_bootstrap_legacy_launcher_metadata_keeps_managed_reset(monkeypatch, tmp_path):
import server
repo = tmp_path / "managed"
(repo / ".git").mkdir(parents=True)
(repo / ".git" / "ouroboros-managed.json").write_text("{}", encoding="utf-8")
git = _GitOps()
monkeypatch.setattr(server, "REPO_DIR", repo)
monkeypatch.setattr(server, "DATA_DIR", tmp_path / "data")
monkeypatch.setattr(server, "_LAUNCHER_MANAGED", True)
monkeypatch.setattr(server, "_LAUNCHER_MANAGED_REPO_DIR", "")
monkeypatch.setattr(server, "setup_remote_if_configured", lambda *args: None)
monkeypatch.setattr(server, "_has_active_evolution_transaction", lambda: False)
monkeypatch.setattr(server, "_safe_restart_serialized", lambda fn, **kwargs: fn(**kwargs))
ok, message = server._bootstrap_supervisor_repo({}, git)
assert ok is True and message == "reset"
assert git.safe_restart_calls == [{"reason": "bootstrap", "unsynced_policy": "rescue_and_reset"}]

View file

@ -24,7 +24,7 @@ def test_drift_capture_freezes_evidence_and_safe_reject_releases_empty_snapshot(
(target / "neighbor.txt").write_text("owner change\n", encoding="utf-8")
capture = _capture_terminal_patch(ctx, entry)
assert capture["status"] == "failed"
assert capture["status"] == "ready_no_changes"
assert capture["target_mutated_during_run"] == ["neighbor.txt"]
manifest_path = custody.delegated_capture_dir(custody.custody_root(ctx), "t-nanny", "snapDrift") / "workspace_patch.json"
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
@ -32,7 +32,7 @@ def test_drift_capture_freezes_evidence_and_safe_reject_releases_empty_snapshot(
(target / "neighbor.txt").unlink()
replay = _capture_terminal_patch(ctx, entry)
assert replay["status"] == "failed"
assert replay["status"] == "ready_no_changes"
assert replay["target_mutated_during_run"] == ["neighbor.txt"]
assert "Rejected delegated run" in _integrate_delegated_patch(
ctx, "run-drift", "reject", "preserve for inspection")
@ -51,7 +51,22 @@ def test_excluded_untracked_baseline_drift_is_not_clean(tmp_path, monkeypatch):
(target / ".env").write_text("SECRET=changed\n", encoding="utf-8")
capture = _capture_terminal_patch(ctx, entry)
assert capture["status"] == "failed"
assert capture["status"] == "ready_no_changes"
assert capture["target_mutated_during_run"] == [".env"]
assert entry.patch_captured is False
assert entry.patch_captured is True
custody._CUSTODY.clear()
def test_nested_untracked_git_repository_does_not_break_snapshot_provision(tmp_path):
target = _seed_target(tmp_path)
nested = target / "vendor" / "inner"
nested.mkdir(parents=True)
import subprocess
subprocess.run(["git", "init", "-q"], cwd=nested, check=True)
(nested / "README").write_text("nested\n", encoding="utf-8")
handle = provision_execution_snapshot(
target_root=target, task_id="t-nested", snapshot_id="snapNested")
assert Path(handle.path).exists()
assert any(row.get("reason") == "nested_repository" for row in handle.excluded_untracked)

View file

@ -6,7 +6,8 @@ import subprocess
from types import SimpleNamespace
from ouroboros.delegate_start_instructions import (
apply_execution_binding, execution_binding_instruction,
apply_execution_binding, directory_copy_binding_instruction,
execution_binding_instruction,
)
from ouroboros.tools.delegate_integration import (
_capture_block, _target_drift_evidence, _target_drift_paths,
@ -31,20 +32,28 @@ def test_runtime_child_environment_drops_launcher_authority(monkeypatch):
from ouroboros.settings_integrity import runtime_environ
monkeypatch.setenv("OUROBOROS_MANAGED_BY_LAUNCHER", "1")
monkeypatch.setenv("OUROBOROS_MANAGED_REPO_DIR", "/private/launcher-repo")
assert "OUROBOROS_MANAGED_BY_LAUNCHER" not in runtime_environ()
assert "OUROBOROS_MANAGED_REPO_DIR" not in runtime_environ()
def test_compiled_work_order_rebinds_child_facing_write_fields():
def test_execution_binding_appends_without_rewriting_canonical_work_order():
instructions = (
"HOST TASK CONTRACT AUTHORITY (complete normalized JSON; exact strings are authority):\n"
'{"workspace_root":"/authority","task_constraint":{"write_root":"/authority"}}'
)
bound, prompt = apply_execution_binding(
instructions, instructions, True, "/private/snapshot", "/authority")
assert '"workspace_root": "/private/snapshot"' in bound
assert '"write_root": "/private/snapshot"' in bound
assert '"authority_target_root": "/authority"' in bound
assert "/private/snapshot" in prompt
bound = apply_execution_binding(instructions, "/private/snapshot", "/authority")
assert bound.startswith(instructions)
assert '"workspace_root":"/authority"' in bound
assert '"write_root":"/authority"' in bound
assert "sole writable execution root" in bound
def test_directory_copy_binding_does_not_invent_source_as_execution_root():
text = directory_copy_binding_instruction("/authority")
assert "/authority" in text
assert "engine will create a private execution copy" in text
assert "Do not write to that authority folder directly" in text
def test_target_drift_is_detected_without_staging_or_rewriting_index(tmp_path):
@ -60,12 +69,15 @@ def test_target_drift_is_detected_without_staging_or_rewriting_index(tmp_path):
(target / "tracked.txt").write_text("owner changed\n", encoding="utf-8")
(target / "new.txt").write_text("owner file\n", encoding="utf-8")
before = _git(target, "status", "--porcelain").stdout
index = target / ".git" / "index"
before_index = index.read_bytes()
entry = SimpleNamespace(target_root=str(target), baseline_sha=baseline)
changed = _target_drift_paths(entry)
assert changed == ["new.txt", "tracked.txt"]
assert _git(target, "status", "--porcelain").stdout == before
assert index.read_bytes() == before_index
def test_capture_block_does_not_claim_private_only_after_target_drift(tmp_path):
@ -80,10 +92,11 @@ def test_capture_block_does_not_claim_private_only_after_target_drift(tmp_path):
{"status": "ready_no_changes", "sha256": "", "diffstat": "0 files"},
["neighbor.txt"],
)
assert block["status"] == "failed"
assert block["status"] == "ready_no_changes"
assert block["target_mutated_during_run"] == ["neighbor.txt"]
assert "TARGET MUTATED DURING RUN" in block["note"]
assert "authority tree changed" in block["note"]
assert "private execution snapshot only" not in block["note"]
assert "author is unknown" in block["note"]
def test_target_drift_probe_failure_is_unknown_not_clean(tmp_path, monkeypatch):