v7next F1: domain D17 - headless split and three test-giant splits, proof-green

headless.py (1573 at tip) gives up its two ledger-assigned leaves again:
ouroboros/headless_status.py (50, 11 symbols) and
ouroboros/workspace_patch_capture.py (668, 19 symbols). All 30 spans are
byte-identical between the reference leaves and git show HEAD bytes — the
hardened transplant --check (mandatory byte gate, undeclared-top-level check,
def681bd) is green on every symbol of both leaves. The facade (947) replays
the oracle's exact edit script over tip bytes: its only divergence from the
reference facade is genuine upstream residue drift (child_ref promotion
machinery, import changes), verified hunk by hunk.

Test splits per the ledger, upstream bytes as truth:
- test_headless_cli.py 2824 -> 462 + five themed siblings + shared fixtures;
  93 test functions preserved exactly (lossless set equality), one adapted
  span kept (the _PATCH_MAX_UNTRACKED_FILE_BYTES monkeypatch retargeted to
  the new leaf, ledger row 739's own adaptation); nine oracle spans carrying
  OTHER domains' v7 spellings reverse-mapped to upstream signatures keyed to
  git show HEAD (registry._run_shell_safety_check string form, tools.core
  _repo_read, queue.init 3-arg, queue.QUEUE_SNAPSHOT_PATH).
- test_workspace_executor.py 1995 -> 541 + three siblings + shared builder;
  two reference spans byte-falsified by upstream drift (06339bb7 readiness
  truth, a849c9a6 probe uncertainty) re-emitted from tip bytes and recorded
  in docs/v7next/LEDGER_CORRECTIONS.md.
- test_agent_task_pipeline.py 1658 -> 1515: only the five ledger-assigned
  _store_task_result rows carved into tests/test_store_task_result.py; the
  other siblings belong to their own lanes.
Thirteen post-cutoff upstream tests have no ledger rows; placed by the
split's theme rule (task_api x4, task_artifacts x1, docker x6, services x2),
disclosed in LEDGER_CORRECTIONS for F5 row-minting.

Pins and mirrors: test_headless_extraction.py transplanted with the
tool_module_inventory clause reduced under an oracle-SHA note (that leaf
belongs to the tools lane); conftest serial table gains the executor family;
the process-custody Popen allowlist row moves headless.py ->
workspace_patch_capture.py with the oracle's justification. All 14 non-split
D17 runtime modules re-proven zero-v7-delta (task_results.py included:
upstream-hot drift stands, no ledger split assigned).

size-ratchet manifest regenerated with the official tool (three test giants
leave GIANT_PATHS, no new band entries); --check green. ruff F clean.
135/105/244/113 tests green in 4-var isolation; HEAD held after every run.

(cherry picked from commit 8dac8303006085bfdb636bacbfc402d6905fae7c)
This commit is contained in:
Ouroboros 2026-08-30 16:27:21 +00:00
parent 88479fa756
commit 57230ee2ef
22 changed files with 5180 additions and 4661 deletions

View file

@ -147,3 +147,53 @@ with evidence, found lane by lane. Applied to the campaign's carried ledger at F
re-sweep loop (65b5d19f), so one live child yields two token-less sweep
calls instead of one; the pinned durable fact (the owner-stop sweep is
token-less) is unchanged.
## From the D17 lane (base def681bd, 2026-08-30)
7. Runtime split rows 465-494 (`headless.py` -> `headless_status.py` (11) +
`workspace_patch_capture.py` (19), "verbatim extraction") — RE-PROVEN at
this tip: all 30 spans byte-identical between the reference leaves and
`git show HEAD:ouroboros/headless.py` (hardened transplant --check, ast/
tokens/bytes all green, both leaves, exit 0). The facade differs from the
reference only by upstream residue drift (child_ref promotion machinery,
`TASK_COST_META_FIELDS`/`replace_atomic` import changes) — replayed from
tip bytes, 947 lines.
8. Test-split rows for `tests/test_workspace_executor.py` ->
`test_workspace_executor_services.py` ("verbatim") — BYTE-FALSIFIED as a
copy source for exactly two functions, transform still valid: upstream
06339bb7 ("fix: preserve service readiness truth") rewrote
`test_executor_local_service_lifecycle_hides_private_snapshot` (the READY
marker is now planted before a 25k log suffix and asserted scanned) and
upstream a849c9a6 ("fix: preserve executor probe uncertainty") extended
`test_executor_service_status_and_durable_record_redact_secret_like_args`
(adds the `'"readiness"' not in durable_text` clause). Both re-emitted
from tip giant bytes; the other 26 moved wexec spans are byte-identical.
9. Reference residual `tests/test_headless_cli.py` and sibling
`test_headless_workspace_shell.py` carry OTHER domains' v7 spellings
inside 9 moved/kept spans (`_run_shell_safety_check(registry, ...)` typed
result + `core_file_tools._repo_read` — D04/D05 split; `queue.init(path)`
1-arg signature and `supervisor.state.QUEUE_SNAPSHOT_PATH` — D08/D33).
On this tree those leaves/signatures do not exist; per §5.3-Δ item 2 every
such span was reverse-mapped to the upstream spelling keyed to
`git show HEAD:tests/test_headless_cli.py` (upstream: string-returning
`registry._run_shell_safety_check`, module-binding `_repo_read`,
`queue.init(path, 600, 1800)`, `queue.QUEUE_SNAPSHOT_PATH`). These
adaptations return with their owning lanes, not with D17.
10. Thirteen upstream test functions written after the reference cutoff have
NO ledger rows (hcli: 4 task-api + 1 artifact-endpoint; wexec: 6 docker
stop/cleanup + 2 readiness). Placed by the split's own theme rule with
imports satisfied by the target headers (task_api×4, task_artifacts×1,
docker×6, services×2 + one `SimpleNamespace` header import); the carried
ledger needs rows minted for them at F5. Placement is disclosed, not
ledger-derived.
11. Row evidence `tests/test_headless_extraction.py` (rows 465-494): the
reference pin imports `ouroboros.tool_module_inventory` (a D04-family v7
leaf absent from this tree); the transplanted pin keeps every clause that
types against THIS tree and replaces the frozen-tool-inventory clause
with an oracle-SHA note — the clause returns with the tools lane.
12. `ouroboros/task_results.py` (upstream-hot, +555 lines drift): the ledger
assigns NO D17 runtime split to it, and the reference copy is
byte-identical to the merge base (zero v7 delta) — nothing to transplant,
upstream bytes stand. Same zero-v7-delta fact re-proven for all 14
non-split D17 runtime modules (task_status, retention, coop_checkpoint,
projects_registry, project_dialogue, project_lease, project_naming,
project_sources, tools/project_journal, workspace_admission,
workspace_preflight, workspace_executor, workspace_patch_rules).

View file

@ -11,19 +11,53 @@ import logging
import os
import pathlib
import shutil
import subprocess
import tempfile
import threading
import subprocess # noqa: F401
import tempfile # noqa: F401
import threading # noqa: F401
from datetime import datetime, timezone
from hashlib import sha256
from typing import Any, BinaryIO, Dict, Iterable, List, Optional, Sequence, Tuple
from typing import Any, BinaryIO, Dict, Iterable, List, Optional, Sequence, Tuple # noqa: F401
from ouroboros.contracts.task_constraint import normalize_task_constraint
from ouroboros.contracts.task_constraint import normalize_task_constraint # noqa: F401
from ouroboros.post_task_checkpoint import project_replica_task_result_fields
from ouroboros.task_results import (
cancellation_blocks_child_result, load_task_result, validate_task_id, write_task_result,
)
from ouroboros.utils import atomic_write_json, utc_now_iso
from ouroboros.headless_status import ( # noqa: F401
ARTIFACT_STATUS_FAILED,
ARTIFACT_STATUS_FINALIZING,
ARTIFACT_STATUS_MISSING,
ARTIFACT_STATUS_PENDING,
ARTIFACT_STATUS_READY,
ARTIFACT_STATUS_READY_NO_CHANGES,
ARTIFACT_STATUS_READY_WITH_CHANGES,
ARTIFACT_TERMINAL_STATUSES,
_ARTIFACT_LIFECYCLE_FIELDS,
_FINAL_STATUSES,
_LOCAL_READONLY_SUBAGENT_MODE,
)
from ouroboros.workspace_patch_capture import ( # noqa: F401
SCRATCH_MANIFEST_NAME,
_GIT_UNBORN_HEAD,
_acting_constraint_from_task,
_append_git_output,
_empty_patch_manifest,
_git_bytes,
_git_empty_tree_oid,
_git_path_list,
_git_stdout,
_head_reflog_exists,
_looks_like_git_oid,
_preflight_head_from_task,
_preflight_head_present,
_untracked_blob_exclude_reason,
_workspace_patch_base,
_write_patch_separator,
build_workspace_patch,
untracked_capture_veto_reason,
write_workspace_patch_artifacts,
)
log = logging.getLogger(__name__)
@ -31,38 +65,8 @@ log = logging.getLogger(__name__)
HEADLESS_TASKS_DIR = pathlib.Path("state") / "headless_tasks"
ARTIFACTS_DIR = pathlib.Path("task_results") / "artifacts"
TASK_DRIVES_DIR = pathlib.Path("task_drives")
ARTIFACT_STATUS_PENDING = "pending"
ARTIFACT_STATUS_FINALIZING = "finalizing"
ARTIFACT_STATUS_READY = "ready"
ARTIFACT_STATUS_READY_WITH_CHANGES = "ready_with_changes"
ARTIFACT_STATUS_READY_NO_CHANGES = "ready_no_changes"
ARTIFACT_STATUS_MISSING = "missing"
ARTIFACT_STATUS_FAILED = "failed"
ARTIFACT_TERMINAL_STATUSES = {
ARTIFACT_STATUS_READY,
ARTIFACT_STATUS_READY_WITH_CHANGES,
ARTIFACT_STATUS_READY_NO_CHANGES,
ARTIFACT_STATUS_MISSING,
ARTIFACT_STATUS_FAILED,
}
# Mirrors task_status.SETTLED_STATUSES; a module-level import would close the
# headless → task_status → outcomes → headless cycle, and the smoke test below
# pins equality so the literal cannot drift from the SSOT.
_FINAL_STATUSES = frozenset({"completed", "failed", "cancelled", "rejected_duplicate"})
# Mirrors tool_capabilities.LOCAL_READONLY_SUBAGENT_MODE; a module-level import would risk
# an import cycle (same rationale as _FINAL_STATUSES above), and the smoke test pins equality
# so the literal cannot drift from this SSOT — the kind of re-derivation drift that stranded
# the reaper's artifact finalization before task_is_readonly_subagent consolidated the gate.
_LOCAL_READONLY_SUBAGENT_MODE = "local_readonly_subagent"
_ARTIFACT_LIFECYCLE_FIELDS = {
"artifact_status",
"artifact_error",
"artifact_bundle",
"artifact_finalized_at",
}
# The PURE patch/snapshot eligibility rules (env/cache dirs, junk artifacts,
# incidental lockfiles, credential-shaped names) live in their own module (size
# gate); re-exported here (same objects) because project_sources, coop_checkpoint
@ -84,14 +88,6 @@ from ouroboros.workspace_patch_rules import ( # noqa: F401
_sensitive_untracked_reason,
)
# v6.52.2: the task-scoped manifest of {ABSOLUTE_path: sha256} fingerprints the agent declared via
# run_command/run_script `scratch=[...]` (ephemeral verification files). The patch capture below
# EXCLUDES a matching untracked path ONLY while its current content still matches the recorded sha
# (so a later real file at the same path is not dropped). SSOT for the name; ouroboros.artifacts
# imports this (headless is the lower-level module).
SCRATCH_MANIFEST_NAME = ".scratch_manifest.json"
_GIT_UNBORN_HEAD = "(unborn)"
def task_state_dir(drive_root: pathlib.Path, task_id: str) -> pathlib.Path:
return pathlib.Path(drive_root) / HEADLESS_TASKS_DIR / validate_task_id(task_id)
@ -844,265 +840,6 @@ def finalize_task_artifacts(parent_drive_root: pathlib.Path, task: Dict[str, Any
return artifacts
def build_workspace_patch(workspace_root: pathlib.Path) -> str:
"""Return a git patch for tracked changes plus untracked files."""
with tempfile.TemporaryDirectory() as tmp:
artifacts, manifest = write_workspace_patch_artifacts(
pathlib.Path(workspace_root),
pathlib.Path(tmp),
task={},
)
if manifest.get("status") == ARTIFACT_STATUS_FAILED:
return ""
for artifact in artifacts:
if artifact.get("kind") == "workspace_patch":
path = pathlib.Path(str(artifact.get("path") or ""))
return path.read_text(encoding="utf-8") if path.is_file() else ""
return ""
def write_workspace_patch_artifacts(
workspace_root: pathlib.Path,
artifact_dir: pathlib.Path,
*,
task: Dict[str, Any],
) -> Tuple[List[Dict[str, Any]], Dict[str, Any]]:
"""Stream workspace patch and manifest artifacts into ``artifact_dir``."""
root = pathlib.Path(workspace_root).resolve(strict=False)
artifact_dir.mkdir(parents=True, exist_ok=True)
patch_path = artifact_dir / "workspace.patch"
manifest_path = artifact_dir / "workspace_patch.json"
errors: List[Dict[str, Any]] = []
diagnostics: List[Dict[str, Any]] = []
excluded: List[Dict[str, str]] = []
tracked_excluded: List[Dict[str, str]] = []
sensitive: List[Dict[str, str]] = []
included_untracked: List[str] = []
acting_constraint = _acting_constraint_from_task(task)
task_base_sha = str(acting_constraint.base_sha or "").strip() if acting_constraint else ""
preflight_head = _preflight_head_from_task(task)
if not task_base_sha and not preflight_head and _preflight_head_present(task):
preflight_head = _GIT_UNBORN_HEAD
base_ref, base_head, base_is_empty_tree = _workspace_patch_base(
root,
errors,
expected_base_sha=task_base_sha or preflight_head,
)
changed_tracked = _git_path_list(
["git", "diff", "--name-only", "-z", "--no-ext-diff", "--no-color", base_ref, "--"],
root,
errors,
)
diffstat = ""
untracked = _git_path_list(["git", "ls-files", "-z", "--others", "--exclude-standard"], root, errors)
# v6.52.2: exclude declared ephemeral scratch (run_command/run_script `scratch=[...]`) so a
# throwaway verification file the agent forgot to delete never leaks into the workspace patch.
# The manifest stores {abs_path: sha256}; a file is excluded ONLY while its CURRENT content
# still matches the recorded scratch sha — so a LATER real file written to the same path
# (different content) is NOT dropped. Empty/absent/mismatched => included (no regression).
scratch_sha_by_rel: dict = {}
scratch_sha_by_abs: dict = {}
try:
_scratch_map = json.loads((artifact_dir / SCRATCH_MANIFEST_NAME).read_text(encoding="utf-8")).get("scratch")
if isinstance(_scratch_map, dict):
for _abs, _sha in _scratch_map.items():
try:
_resolved = pathlib.Path(str(_abs)).resolve(strict=False)
scratch_sha_by_abs[os.path.normcase(str(_resolved))] = str(_sha)
scratch_sha_by_rel[_resolved.relative_to(root).as_posix()] = str(_sha)
except Exception:
continue
except Exception:
scratch_sha_by_rel = {}
scratch_sha_by_abs = {}
for rel in untracked:
_want_sha = scratch_sha_by_rel.get(rel) or scratch_sha_by_abs.get(os.path.normcase(str((root / rel).resolve(strict=False))))
if _want_sha:
try:
_cur_sha = sha256((root / rel).read_bytes()).hexdigest()
except OSError:
_cur_sha = None
if _cur_sha == _want_sha:
excluded.append({"path": rel, "reason": "declared ephemeral scratch (v6.52.2)"})
continue
sensitive_reason = _sensitive_untracked_reason(rel)
if sensitive_reason:
sensitive.append({"path": rel, "reason": sensitive_reason})
continue
reason = _patch_exclude_reason(rel)
if reason:
excluded.append({"path": rel, "reason": reason})
continue
blob_reason = _untracked_blob_exclude_reason(root, rel)
if blob_reason:
excluded.append({"path": rel, "reason": blob_reason})
continue
included_untracked.append(rel)
incidental_lock_excludes = _incidental_lockfile_excludes([*changed_tracked, *included_untracked])
if incidental_lock_excludes:
kept_untracked: List[str] = []
for rel in included_untracked:
if rel in incidental_lock_excludes:
excluded.append({"path": rel, "reason": "incidental lockfile without sibling manifest change"})
else:
kept_untracked.append(rel)
included_untracked = kept_untracked
if sensitive:
errors.append({
"type": "sensitive_untracked_files",
"message": "untracked sensitive-looking files are not included in workspace patch",
"paths": [item["path"] for item in sensitive],
})
hasher = sha256()
total_size = 0
with patch_path.open("wb") as fh:
if not errors:
tracked_lock_excludes = sorted(set(changed_tracked) & incidental_lock_excludes)
tracked_pathspec = ["--"]
if tracked_lock_excludes:
tracked_pathspec += ["."] + [f":(exclude){rel}" for rel in tracked_lock_excludes]
for rel in tracked_lock_excludes:
tracked_excluded.append({"path": rel, "reason": "incidental lockfile without sibling manifest change"})
diffstat = _git_stdout(
["git", "diff", "--stat", "--no-ext-diff", "--no-color", base_ref, *tracked_pathspec],
root,
allow_rc={0},
errors=errors,
)
total_size += _append_git_output(
["git", "diff", "--binary", "--no-ext-diff", "--no-color", base_ref, *tracked_pathspec],
root,
fh,
hasher,
allow_rc={0},
errors=errors,
diagnostics=diagnostics,
)
for rel in included_untracked:
if total_size:
total_size += _write_patch_separator(fh, hasher)
total_size += _append_git_output(
["git", "diff", "--no-index", "--binary", "--no-ext-diff", "--no-color", "--", os.devnull, rel],
root,
fh,
hasher,
allow_rc={0, 1},
errors=errors,
diagnostics=diagnostics,
)
if errors:
try:
patch_path.unlink()
except OSError:
pass
total_size = 0
digest = ""
else:
digest = hasher.hexdigest()
head_error: Dict[str, Any] | None = None
head_errors: List[Dict[str, Any]] = []
current_head = _git_stdout(["git", "rev-parse", "--verify", "HEAD"], root, allow_rc={0}, errors=head_errors).strip()
# Q11: the moved-HEAD fail-closed tripwire applies ONLY to a child's private
# self_worktree, where a moved HEAD can only mean the worktree itself
# rewrote history under the patch (its base is always a real provisioned
# commit, never unborn). In a SHARED tree (external_workspace/genesis) the
# parent's own legitimate commits move HEAD too — enforcing it there failed
# every innocent in-flight sibling; shared-tree integrity is verified by the
# reverse-patch check in tools/subagent_integration (verified_shared_workspace),
# and base_sha stays the patch BASE so parent-committed work is still captured.
if task_base_sha and acting_constraint is not None and acting_constraint.surface == "self_worktree":
if not current_head:
errors.extend(head_errors)
head_error = {
"type": "workspace_head_unverified",
"message": "workspace HEAD could not be verified at artifact finalization",
"expected_head": base_head,
"current_head": "",
}
errors.append(head_error)
elif current_head != base_head:
head_error = {
"type": "workspace_head_changed",
"message": "workspace HEAD changed during task execution; patch artifact is invalid",
"expected_head": base_head,
"current_head": current_head,
}
errors.append(head_error)
if head_error:
try:
patch_path.unlink()
except OSError:
pass
total_size = 0
digest = ""
if errors:
status = ARTIFACT_STATUS_FAILED
elif total_size > 0:
status = ARTIFACT_STATUS_READY_WITH_CHANGES
else:
status = ARTIFACT_STATUS_READY_NO_CHANGES
try:
patch_path.unlink()
except OSError:
pass
digest = ""
manifest = {
"schema_version": 1,
"created_at": utc_now_iso(),
"status": status,
"workspace_root": str(root),
"patch_name": "workspace.patch",
"manifest_name": "workspace_patch.json",
"base_ref": base_ref,
"base_head": base_head,
"base_is_empty_tree": base_is_empty_tree,
"current_head": current_head or (_GIT_UNBORN_HEAD if base_is_empty_tree else ""),
"patch_size": total_size,
"sha256": digest,
"diffstat": diffstat,
"counts": {
"tracked_changed": len(changed_tracked),
"tracked_excluded": len(tracked_excluded),
"untracked_included": len(included_untracked),
"untracked_excluded": len(excluded),
"sensitive_blocked": len(sensitive),
},
"tracked_changed": changed_tracked,
"tracked_excluded": tracked_excluded,
"untracked_included": included_untracked,
"untracked_excluded": excluded,
"sensitive_blocked": sensitive,
"exclude_rules_version": _PATCH_EXCLUDE_RULES_VERSION,
"diagnostics": diagnostics,
"errors": errors,
}
atomic_write_json(manifest_path, manifest, trailing_newline=True)
artifacts = [
{
"kind": "workspace_patch_manifest",
"name": "workspace_patch.json",
"path": str(manifest_path),
"size": manifest_path.stat().st_size if manifest_path.exists() else 0,
"workspace_root": str(root),
}
]
if status == ARTIFACT_STATUS_READY_WITH_CHANGES:
artifacts.insert(0, {
"kind": "workspace_patch",
"name": "workspace.patch",
"path": str(patch_path),
"size": total_size,
"sha256": digest,
"workspace_root": str(root),
})
return artifacts, manifest
def build_memory_export(child_drive_root: pathlib.Path, task: Dict[str, Any]) -> Dict[str, Any]:
"""Create an explicit export artifact without merging it into parent memory."""
@ -1167,369 +904,6 @@ def _workspace_root_from_task(task: Dict[str, Any]) -> Optional[pathlib.Path]:
return pathlib.Path(text) if text else None
def _git_stdout(
cmd: Sequence[str],
cwd: pathlib.Path,
*,
allow_rc: Iterable[int] = (0,),
errors: Optional[List[Dict[str, Any]]] = None,
) -> str:
"""Text projection of ``_git_bytes`` (same rc/timeout/error handling)."""
return _git_bytes(cmd, cwd, allow_rc=allow_rc, errors=errors).decode("utf-8", errors="replace")
def _workspace_patch_base(
root: pathlib.Path,
errors: List[Dict[str, Any]],
*,
expected_base_sha: str = "",
) -> Tuple[str, str, bool]:
"""Return the git tree-ish used as the patch baseline.
A freshly initialized external workspace is a valid git worktree even when
it has no commits. In that state ``git diff HEAD`` fails, so patch capture
compares against Git's canonical empty tree instead of forcing adapters to
create a synthetic target commit in the user's workspace.
"""
if expected_base_sha:
if expected_base_sha == _GIT_UNBORN_HEAD:
empty_tree = _git_empty_tree_oid(root, errors)
if empty_tree:
return empty_tree, _GIT_UNBORN_HEAD, True
return "HEAD", _GIT_UNBORN_HEAD, False
if not _looks_like_git_oid(expected_base_sha):
errors.append({
"type": "workspace_base_sha_invalid",
"message": "acting subagent base_sha is not a git object id; refusing to build patch artifact",
"base_sha": expected_base_sha,
})
return "HEAD", expected_base_sha, False
verify_errors: List[Dict[str, Any]] = []
resolved = _git_stdout(
["git", "rev-parse", "--verify", f"{expected_base_sha}^{{commit}}"],
root,
allow_rc={0},
errors=verify_errors,
).strip()
if not resolved:
errors.extend(verify_errors)
errors.append({
"type": "workspace_base_sha_missing",
"message": "acting subagent base_sha is not available in workspace git history",
"base_sha": expected_base_sha,
})
return expected_base_sha, expected_base_sha, False
return resolved, resolved, False
head_errors: List[Dict[str, Any]] = []
head = _git_stdout(["git", "rev-parse", "--verify", "HEAD"], root, allow_rc={0}, errors=head_errors).strip()
if head:
return head, head, False
worktree_errors: List[Dict[str, Any]] = []
inside = _git_stdout(
["git", "rev-parse", "--is-inside-work-tree"],
root,
allow_rc={0},
errors=worktree_errors,
).strip()
if inside == "true" and _head_reflog_exists(root):
errors.extend(head_errors)
errors.append({
"type": "git_invalid_head",
"command": ["git", "rev-parse", "--verify", "HEAD"],
"message": "HEAD could not be resolved but the repository has HEAD history; refusing to treat it as unborn",
})
return "HEAD", "", False
if inside == "true":
empty_tree = _git_empty_tree_oid(root, errors)
if empty_tree:
return empty_tree, _GIT_UNBORN_HEAD, True
errors.extend(head_errors or worktree_errors)
return "HEAD", "", False
def _git_empty_tree_oid(root: pathlib.Path, errors: List[Dict[str, Any]]) -> str:
try:
result = subprocess.run(
["git", "hash-object", "-t", "tree", "--stdin"],
cwd=str(root),
input="",
capture_output=True,
text=True,
timeout=30,
)
except Exception as exc:
errors.append({"type": "git_exception", "command": ["git", "hash-object", "-t", "tree", "--stdin"], "message": f"{type(exc).__name__}: {exc}"})
return ""
if result.returncode != 0:
errors.append({
"type": "git_error",
"command": ["git", "hash-object", "-t", "tree", "--stdin"],
"returncode": result.returncode,
"stderr": (result.stderr or "")[-2000:],
})
return ""
return (result.stdout or "").strip()
def _head_reflog_exists(root: pathlib.Path) -> bool:
path_text = _git_stdout(["git", "rev-parse", "--git-path", "logs/HEAD"], root, allow_rc={0}).strip()
if not path_text:
return False
path = pathlib.Path(path_text)
if not path.is_absolute():
path = root / path
try:
return path.is_file() and path.stat().st_size > 0
except OSError:
return False
def _looks_like_git_oid(value: str) -> bool:
text = str(value or "").strip()
return 7 <= len(text) <= 64 and all(ch in "0123456789abcdefABCDEF" for ch in text)
def _git_path_list(cmd: Sequence[str], root: pathlib.Path, errors: Optional[List[Dict[str, Any]]] = None) -> List[str]:
output = _git_bytes(cmd, root, errors=errors)
if not output:
return []
return [part.decode("utf-8", errors="replace") for part in output.split(b"\0") if part]
def _git_bytes(
cmd: Sequence[str],
cwd: pathlib.Path,
*,
allow_rc: Iterable[int] = (0,),
errors: Optional[List[Dict[str, Any]]] = None,
) -> bytes:
try:
result = subprocess.run(
list(cmd),
cwd=str(cwd),
capture_output=True,
timeout=30,
)
except subprocess.TimeoutExpired:
if errors is not None:
errors.append({"type": "git_timeout", "command": list(cmd), "message": "git command timed out"})
return b""
except Exception as exc:
if errors is not None:
errors.append({"type": "git_exception", "command": list(cmd), "message": f"{type(exc).__name__}: {exc}"})
return b""
if result.returncode not in set(allow_rc):
if errors is not None:
errors.append({
"type": "git_error",
"command": list(cmd),
"returncode": result.returncode,
"stderr": (result.stderr or b"").decode("utf-8", errors="replace")[-2000:],
})
return b""
return result.stdout or b""
def _append_git_output(
cmd: Sequence[str],
cwd: pathlib.Path,
fh: BinaryIO,
hasher: Any,
*,
allow_rc: set[int],
errors: List[Dict[str, Any]],
diagnostics: List[Dict[str, Any]],
) -> int:
written_box = {"value": 0}
read_errors: List[str] = []
try:
with tempfile.TemporaryFile() as stderr_fh:
proc = subprocess.Popen(
list(cmd),
cwd=str(cwd),
stdout=subprocess.PIPE,
stderr=stderr_fh,
)
assert proc.stdout is not None
def _reader() -> None:
try:
while True:
chunk = proc.stdout.read(1024 * 128)
if not chunk:
break
fh.write(chunk)
hasher.update(chunk)
written_box["value"] += len(chunk)
except Exception as exc:
read_errors.append(f"{type(exc).__name__}: {exc}")
reader = threading.Thread(target=_reader, name="workspace-patch-git-stdout", daemon=True)
reader.start()
try:
proc.wait(timeout=30)
except subprocess.TimeoutExpired:
try:
proc.kill()
except Exception:
pass
try:
proc.wait(timeout=5)
except Exception:
pass
reader.join(timeout=5)
if reader.is_alive():
errors.append({"type": "git_timeout", "command": list(cmd), "message": "git stdout reader timed out"})
errors.append({"type": "git_timeout", "command": list(cmd), "message": "git command timed out"})
return int(written_box["value"])
reader.join(timeout=5)
if reader.is_alive():
errors.append({"type": "git_timeout", "command": list(cmd), "message": "git stdout reader timed out"})
for read_error in read_errors:
errors.append({"type": "git_exception", "command": list(cmd), "message": read_error})
stderr_fh.seek(0)
stderr = stderr_fh.read() or b""
except subprocess.TimeoutExpired:
try:
proc.kill() # type: ignore[possibly-undefined]
except Exception:
pass
errors.append({"type": "git_timeout", "command": list(cmd), "message": "git command timed out"})
return int(written_box["value"])
except Exception as exc:
errors.append({"type": "git_exception", "command": list(cmd), "message": f"{type(exc).__name__}: {exc}"})
return int(written_box["value"])
if proc.returncode not in allow_rc:
errors.append({
"type": "git_error",
"command": list(cmd),
"returncode": proc.returncode,
"stderr": stderr.decode("utf-8", errors="replace")[-2000:],
})
written = int(written_box["value"])
diagnostics.append({"command": list(cmd), "returncode": proc.returncode, "bytes": written})
return written
def _write_patch_separator(fh: BinaryIO, hasher: Any) -> int:
data = b"\n"
fh.write(data)
hasher.update(data)
return len(data)
def _untracked_blob_exclude_reason(root: pathlib.Path, rel: str) -> str:
"""Reason to drop an untracked file from the workspace patch when it is a
build/runtime BINARY or exceeds the per-file size cap. Keeps real-usage
patches source-shaped without losing data (the file stays in the workspace
and is recorded under ``untracked_excluded``). On any git/stat failure the
file is INCLUDED (conservative — the main binary diff still applies)."""
try:
size = (root / rel).lstat().st_size
except OSError:
return "" # unreadable/symlink races: include and let git decide
if size > _PATCH_MAX_UNTRACKED_FILE_BYTES:
return f"untracked file exceeds size cap ({size}B > {_PATCH_MAX_UNTRACKED_FILE_BYTES}B)"
numstat = _git_stdout(
["git", "diff", "--no-index", "--numstat", "--no-ext-diff", "--no-color", "--", os.devnull, rel],
root,
allow_rc={0, 1},
errors=None,
)
first = numstat.strip().splitlines()[0] if numstat.strip() else ""
if first.startswith("-\t-"):
return "binary file"
return ""
def untracked_capture_veto_reason(root: pathlib.Path, rel: str) -> str:
"""Why an untracked file must NOT ride into a workspace snapshot or patch.
The delegated-run baseline snapshot
(``subagent_worktrees.provision_execution_snapshot``) asks the SAME three
checks, in the SAME order, that ``write_workspace_patch_artifacts`` applies
to untracked files: sensitive/credential-shaped names first, then the
static junk rules, then the binary/size veto. One combined predicate here so
the snapshot and the patch cannot drift apart about eligibility.
Returns the human-readable reason, or "" when the file is eligible.
"""
reason = _sensitive_untracked_reason(rel)
if reason:
return reason
reason = _patch_exclude_reason(rel)
if reason:
return reason
return _untracked_blob_exclude_reason(root, rel)
def _preflight_head_from_task(task: Dict[str, Any]) -> str:
meta = task.get("metadata") if isinstance(task.get("metadata"), dict) else {}
preflight = meta.get("workspace_preflight") if isinstance(meta.get("workspace_preflight"), dict) else {}
git = preflight.get("git") if isinstance(preflight.get("git"), dict) else {}
return str(git.get("head") or "")
def _preflight_head_present(task: Dict[str, Any]) -> bool:
meta = task.get("metadata") if isinstance(task.get("metadata"), dict) else {}
preflight = meta.get("workspace_preflight") if isinstance(meta.get("workspace_preflight"), dict) else {}
git = preflight.get("git") if isinstance(preflight.get("git"), dict) else {}
return "head" in git
def _acting_constraint_from_task(task: Dict[str, Any]):
"""Normalized acting-subagent constraint carried by ``task``, or None."""
raw = task.get("task_constraint") if isinstance(task.get("task_constraint"), dict) else {}
if not raw:
meta = task.get("metadata") if isinstance(task.get("metadata"), dict) else {}
raw = meta.get("task_constraint") if isinstance(meta.get("task_constraint"), dict) else {}
try:
constraint = normalize_task_constraint(raw)
except Exception:
return None
return constraint if constraint and constraint.mode == "acting_subagent" else None
def _empty_patch_manifest(
workspace_root: pathlib.Path,
*,
status: str,
errors: List[Dict[str, Any]],
) -> Dict[str, Any]:
return {
"schema_version": 1,
"created_at": utc_now_iso(),
"status": status,
"workspace_root": str(workspace_root),
"patch_name": "workspace.patch",
"manifest_name": "workspace_patch.json",
"base_ref": "",
"base_head": "",
"base_is_empty_tree": False,
"current_head": "",
"patch_size": 0,
"sha256": "",
"diffstat": "",
"counts": {
"tracked_changed": 0,
"untracked_included": 0,
"untracked_excluded": 0,
"sensitive_blocked": 0,
},
"tracked_changed": [],
"untracked_included": [],
"untracked_excluded": [],
"sensitive_blocked": [],
"exclude_rules_version": _PATCH_EXCLUDE_RULES_VERSION,
"diagnostics": [],
"errors": errors,
}
def _merge_artifacts(
existing: List[Dict[str, Any]],
new_items: List[Dict[str, Any]],

View file

@ -0,0 +1,50 @@
"""Artifact and task lifecycle vocabulary shared by the headless owners.
The artifact-status values a task result may carry, the terminal subset the
pruners and the copy-back gate test against, the lifecycle fields a late child
copy-back must preserve, and the two literals headless mirrors instead of
importing (settled task statuses, the local-readonly subagent mode) because a
module-level import of their SSOT would close an import cycle. Constants only:
every consumer of this vocabulary owns its own behaviour.
"""
from __future__ import annotations
ARTIFACT_STATUS_PENDING = "pending"
ARTIFACT_STATUS_FINALIZING = "finalizing"
ARTIFACT_STATUS_READY = "ready"
ARTIFACT_STATUS_READY_WITH_CHANGES = "ready_with_changes"
ARTIFACT_STATUS_READY_NO_CHANGES = "ready_no_changes"
ARTIFACT_STATUS_MISSING = "missing"
ARTIFACT_STATUS_FAILED = "failed"
ARTIFACT_TERMINAL_STATUSES = {
ARTIFACT_STATUS_READY,
ARTIFACT_STATUS_READY_WITH_CHANGES,
ARTIFACT_STATUS_READY_NO_CHANGES,
ARTIFACT_STATUS_MISSING,
ARTIFACT_STATUS_FAILED,
}
# Mirrors task_status.SETTLED_STATUSES; a module-level import would close the
# headless → task_status → outcomes → headless cycle, and the smoke test below
# pins equality so the literal cannot drift from the SSOT.
_FINAL_STATUSES = frozenset({"completed", "failed", "cancelled", "rejected_duplicate"})
# Mirrors tool_capabilities.LOCAL_READONLY_SUBAGENT_MODE; a module-level import would risk
# an import cycle (same rationale as _FINAL_STATUSES above), and the smoke test pins equality
# so the literal cannot drift from this SSOT — the kind of re-derivation drift that stranded
# the reaper's artifact finalization before task_is_readonly_subagent consolidated the gate.
_LOCAL_READONLY_SUBAGENT_MODE = "local_readonly_subagent"
_ARTIFACT_LIFECYCLE_FIELDS = {
"artifact_status",
"artifact_error",
"artifact_bundle",
"artifact_finalized_at",
}

View file

@ -19,7 +19,6 @@ GIANT_PATHS = (
"supervisor/events.py",
"supervisor/git_ops.py",
"supervisor/workers.py",
"tests/test_agent_task_pipeline.py",
"tests/test_cancel_intents_phase_a.py",
"tests/test_claudexor_owned_daemon.py",
"tests/test_delegated_subagent_transport.py",
@ -29,7 +28,6 @@ GIANT_PATHS = (
"tests/test_extensions_api.py",
"tests/test_git_ops_recovery.py",
"tests/test_git_review_pipeline.py",
"tests/test_headless_cli.py",
"tests/test_loop_misc.py",
"tests/test_model_slot_role_model.py",
"tests/test_osworld_cu_bridge.py",
@ -46,7 +44,6 @@ GIANT_PATHS = (
"tests/test_task_status_flow.py",
"tests/test_tool_capabilities.py",
"tests/test_ui_smoke_playwright.py",
"tests/test_workspace_executor.py",
"web/modules/chat.js",
"web/tests/harness_accounts.test.js",
)

View file

@ -0,0 +1,668 @@
"""Workspace patch capture: the patch artifact, its manifest, and its git plumbing.
Owns the streamed `workspace.patch` and `workspace_patch.json` pair — patch
baseline resolution (including the unborn-HEAD empty-tree case and the acting
subagent `base_sha` binding), the bounded git process helpers the capture runs
on, the declared-scratch and untracked eligibility filtering, the moved-HEAD
tripwire for a private self worktree, and the empty manifest a failed
finalization falls back to. The static eligibility rules live in
``workspace_patch_rules``; the task-drive, child-result and artifact
finalization owners stay with ``headless``.
"""
from __future__ import annotations
import json
import os
import pathlib
import subprocess
import tempfile
import threading
from hashlib import sha256
from typing import Any, BinaryIO, Dict, Iterable, List, Optional, Sequence, Tuple
from ouroboros.contracts.task_constraint import normalize_task_constraint
from ouroboros.headless_status import (
ARTIFACT_STATUS_FAILED,
ARTIFACT_STATUS_READY_NO_CHANGES,
ARTIFACT_STATUS_READY_WITH_CHANGES,
)
from ouroboros.utils import atomic_write_json, utc_now_iso
from ouroboros.workspace_patch_rules import (
_PATCH_EXCLUDE_RULES_VERSION,
_PATCH_MAX_UNTRACKED_FILE_BYTES,
_incidental_lockfile_excludes,
_patch_exclude_reason,
_sensitive_untracked_reason,
)
# v6.52.2: the task-scoped manifest of {ABSOLUTE_path: sha256} fingerprints the agent declared via
# run_command/run_script `scratch=[...]` (ephemeral verification files). The patch capture below
# EXCLUDES a matching untracked path ONLY while its current content still matches the recorded sha
# (so a later real file at the same path is not dropped). SSOT for the name; ouroboros.artifacts
# imports this (headless is the lower-level module).
SCRATCH_MANIFEST_NAME = ".scratch_manifest.json"
_GIT_UNBORN_HEAD = "(unborn)"
def build_workspace_patch(workspace_root: pathlib.Path) -> str:
"""Return a git patch for tracked changes plus untracked files."""
with tempfile.TemporaryDirectory() as tmp:
artifacts, manifest = write_workspace_patch_artifacts(
pathlib.Path(workspace_root),
pathlib.Path(tmp),
task={},
)
if manifest.get("status") == ARTIFACT_STATUS_FAILED:
return ""
for artifact in artifacts:
if artifact.get("kind") == "workspace_patch":
path = pathlib.Path(str(artifact.get("path") or ""))
return path.read_text(encoding="utf-8") if path.is_file() else ""
return ""
def write_workspace_patch_artifacts(
workspace_root: pathlib.Path,
artifact_dir: pathlib.Path,
*,
task: Dict[str, Any],
) -> Tuple[List[Dict[str, Any]], Dict[str, Any]]:
"""Stream workspace patch and manifest artifacts into ``artifact_dir``."""
root = pathlib.Path(workspace_root).resolve(strict=False)
artifact_dir.mkdir(parents=True, exist_ok=True)
patch_path = artifact_dir / "workspace.patch"
manifest_path = artifact_dir / "workspace_patch.json"
errors: List[Dict[str, Any]] = []
diagnostics: List[Dict[str, Any]] = []
excluded: List[Dict[str, str]] = []
tracked_excluded: List[Dict[str, str]] = []
sensitive: List[Dict[str, str]] = []
included_untracked: List[str] = []
acting_constraint = _acting_constraint_from_task(task)
task_base_sha = str(acting_constraint.base_sha or "").strip() if acting_constraint else ""
preflight_head = _preflight_head_from_task(task)
if not task_base_sha and not preflight_head and _preflight_head_present(task):
preflight_head = _GIT_UNBORN_HEAD
base_ref, base_head, base_is_empty_tree = _workspace_patch_base(
root,
errors,
expected_base_sha=task_base_sha or preflight_head,
)
changed_tracked = _git_path_list(
["git", "diff", "--name-only", "-z", "--no-ext-diff", "--no-color", base_ref, "--"],
root,
errors,
)
diffstat = ""
untracked = _git_path_list(["git", "ls-files", "-z", "--others", "--exclude-standard"], root, errors)
# v6.52.2: exclude declared ephemeral scratch (run_command/run_script `scratch=[...]`) so a
# throwaway verification file the agent forgot to delete never leaks into the workspace patch.
# The manifest stores {abs_path: sha256}; a file is excluded ONLY while its CURRENT content
# still matches the recorded scratch sha — so a LATER real file written to the same path
# (different content) is NOT dropped. Empty/absent/mismatched => included (no regression).
scratch_sha_by_rel: dict = {}
scratch_sha_by_abs: dict = {}
try:
_scratch_map = json.loads((artifact_dir / SCRATCH_MANIFEST_NAME).read_text(encoding="utf-8")).get("scratch")
if isinstance(_scratch_map, dict):
for _abs, _sha in _scratch_map.items():
try:
_resolved = pathlib.Path(str(_abs)).resolve(strict=False)
scratch_sha_by_abs[os.path.normcase(str(_resolved))] = str(_sha)
scratch_sha_by_rel[_resolved.relative_to(root).as_posix()] = str(_sha)
except Exception:
continue
except Exception:
scratch_sha_by_rel = {}
scratch_sha_by_abs = {}
for rel in untracked:
_want_sha = scratch_sha_by_rel.get(rel) or scratch_sha_by_abs.get(os.path.normcase(str((root / rel).resolve(strict=False))))
if _want_sha:
try:
_cur_sha = sha256((root / rel).read_bytes()).hexdigest()
except OSError:
_cur_sha = None
if _cur_sha == _want_sha:
excluded.append({"path": rel, "reason": "declared ephemeral scratch (v6.52.2)"})
continue
sensitive_reason = _sensitive_untracked_reason(rel)
if sensitive_reason:
sensitive.append({"path": rel, "reason": sensitive_reason})
continue
reason = _patch_exclude_reason(rel)
if reason:
excluded.append({"path": rel, "reason": reason})
continue
blob_reason = _untracked_blob_exclude_reason(root, rel)
if blob_reason:
excluded.append({"path": rel, "reason": blob_reason})
continue
included_untracked.append(rel)
incidental_lock_excludes = _incidental_lockfile_excludes([*changed_tracked, *included_untracked])
if incidental_lock_excludes:
kept_untracked: List[str] = []
for rel in included_untracked:
if rel in incidental_lock_excludes:
excluded.append({"path": rel, "reason": "incidental lockfile without sibling manifest change"})
else:
kept_untracked.append(rel)
included_untracked = kept_untracked
if sensitive:
errors.append({
"type": "sensitive_untracked_files",
"message": "untracked sensitive-looking files are not included in workspace patch",
"paths": [item["path"] for item in sensitive],
})
hasher = sha256()
total_size = 0
with patch_path.open("wb") as fh:
if not errors:
tracked_lock_excludes = sorted(set(changed_tracked) & incidental_lock_excludes)
tracked_pathspec = ["--"]
if tracked_lock_excludes:
tracked_pathspec += ["."] + [f":(exclude){rel}" for rel in tracked_lock_excludes]
for rel in tracked_lock_excludes:
tracked_excluded.append({"path": rel, "reason": "incidental lockfile without sibling manifest change"})
diffstat = _git_stdout(
["git", "diff", "--stat", "--no-ext-diff", "--no-color", base_ref, *tracked_pathspec],
root,
allow_rc={0},
errors=errors,
)
total_size += _append_git_output(
["git", "diff", "--binary", "--no-ext-diff", "--no-color", base_ref, *tracked_pathspec],
root,
fh,
hasher,
allow_rc={0},
errors=errors,
diagnostics=diagnostics,
)
for rel in included_untracked:
if total_size:
total_size += _write_patch_separator(fh, hasher)
total_size += _append_git_output(
["git", "diff", "--no-index", "--binary", "--no-ext-diff", "--no-color", "--", os.devnull, rel],
root,
fh,
hasher,
allow_rc={0, 1},
errors=errors,
diagnostics=diagnostics,
)
if errors:
try:
patch_path.unlink()
except OSError:
pass
total_size = 0
digest = ""
else:
digest = hasher.hexdigest()
head_error: Dict[str, Any] | None = None
head_errors: List[Dict[str, Any]] = []
current_head = _git_stdout(["git", "rev-parse", "--verify", "HEAD"], root, allow_rc={0}, errors=head_errors).strip()
# Q11: the moved-HEAD fail-closed tripwire applies ONLY to a child's private
# self_worktree, where a moved HEAD can only mean the worktree itself
# rewrote history under the patch (its base is always a real provisioned
# commit, never unborn). In a SHARED tree (external_workspace/genesis) the
# parent's own legitimate commits move HEAD too — enforcing it there failed
# every innocent in-flight sibling; shared-tree integrity is verified by the
# reverse-patch check in tools/subagent_integration (verified_shared_workspace),
# and base_sha stays the patch BASE so parent-committed work is still captured.
if task_base_sha and acting_constraint is not None and acting_constraint.surface == "self_worktree":
if not current_head:
errors.extend(head_errors)
head_error = {
"type": "workspace_head_unverified",
"message": "workspace HEAD could not be verified at artifact finalization",
"expected_head": base_head,
"current_head": "",
}
errors.append(head_error)
elif current_head != base_head:
head_error = {
"type": "workspace_head_changed",
"message": "workspace HEAD changed during task execution; patch artifact is invalid",
"expected_head": base_head,
"current_head": current_head,
}
errors.append(head_error)
if head_error:
try:
patch_path.unlink()
except OSError:
pass
total_size = 0
digest = ""
if errors:
status = ARTIFACT_STATUS_FAILED
elif total_size > 0:
status = ARTIFACT_STATUS_READY_WITH_CHANGES
else:
status = ARTIFACT_STATUS_READY_NO_CHANGES
try:
patch_path.unlink()
except OSError:
pass
digest = ""
manifest = {
"schema_version": 1,
"created_at": utc_now_iso(),
"status": status,
"workspace_root": str(root),
"patch_name": "workspace.patch",
"manifest_name": "workspace_patch.json",
"base_ref": base_ref,
"base_head": base_head,
"base_is_empty_tree": base_is_empty_tree,
"current_head": current_head or (_GIT_UNBORN_HEAD if base_is_empty_tree else ""),
"patch_size": total_size,
"sha256": digest,
"diffstat": diffstat,
"counts": {
"tracked_changed": len(changed_tracked),
"tracked_excluded": len(tracked_excluded),
"untracked_included": len(included_untracked),
"untracked_excluded": len(excluded),
"sensitive_blocked": len(sensitive),
},
"tracked_changed": changed_tracked,
"tracked_excluded": tracked_excluded,
"untracked_included": included_untracked,
"untracked_excluded": excluded,
"sensitive_blocked": sensitive,
"exclude_rules_version": _PATCH_EXCLUDE_RULES_VERSION,
"diagnostics": diagnostics,
"errors": errors,
}
atomic_write_json(manifest_path, manifest, trailing_newline=True)
artifacts = [
{
"kind": "workspace_patch_manifest",
"name": "workspace_patch.json",
"path": str(manifest_path),
"size": manifest_path.stat().st_size if manifest_path.exists() else 0,
"workspace_root": str(root),
}
]
if status == ARTIFACT_STATUS_READY_WITH_CHANGES:
artifacts.insert(0, {
"kind": "workspace_patch",
"name": "workspace.patch",
"path": str(patch_path),
"size": total_size,
"sha256": digest,
"workspace_root": str(root),
})
return artifacts, manifest
def _git_stdout(
cmd: Sequence[str],
cwd: pathlib.Path,
*,
allow_rc: Iterable[int] = (0,),
errors: Optional[List[Dict[str, Any]]] = None,
) -> str:
"""Text projection of ``_git_bytes`` (same rc/timeout/error handling)."""
return _git_bytes(cmd, cwd, allow_rc=allow_rc, errors=errors).decode("utf-8", errors="replace")
def _workspace_patch_base(
root: pathlib.Path,
errors: List[Dict[str, Any]],
*,
expected_base_sha: str = "",
) -> Tuple[str, str, bool]:
"""Return the git tree-ish used as the patch baseline.
A freshly initialized external workspace is a valid git worktree even when
it has no commits. In that state ``git diff HEAD`` fails, so patch capture
compares against Git's canonical empty tree instead of forcing adapters to
create a synthetic target commit in the user's workspace.
"""
if expected_base_sha:
if expected_base_sha == _GIT_UNBORN_HEAD:
empty_tree = _git_empty_tree_oid(root, errors)
if empty_tree:
return empty_tree, _GIT_UNBORN_HEAD, True
return "HEAD", _GIT_UNBORN_HEAD, False
if not _looks_like_git_oid(expected_base_sha):
errors.append({
"type": "workspace_base_sha_invalid",
"message": "acting subagent base_sha is not a git object id; refusing to build patch artifact",
"base_sha": expected_base_sha,
})
return "HEAD", expected_base_sha, False
verify_errors: List[Dict[str, Any]] = []
resolved = _git_stdout(
["git", "rev-parse", "--verify", f"{expected_base_sha}^{{commit}}"],
root,
allow_rc={0},
errors=verify_errors,
).strip()
if not resolved:
errors.extend(verify_errors)
errors.append({
"type": "workspace_base_sha_missing",
"message": "acting subagent base_sha is not available in workspace git history",
"base_sha": expected_base_sha,
})
return expected_base_sha, expected_base_sha, False
return resolved, resolved, False
head_errors: List[Dict[str, Any]] = []
head = _git_stdout(["git", "rev-parse", "--verify", "HEAD"], root, allow_rc={0}, errors=head_errors).strip()
if head:
return head, head, False
worktree_errors: List[Dict[str, Any]] = []
inside = _git_stdout(
["git", "rev-parse", "--is-inside-work-tree"],
root,
allow_rc={0},
errors=worktree_errors,
).strip()
if inside == "true" and _head_reflog_exists(root):
errors.extend(head_errors)
errors.append({
"type": "git_invalid_head",
"command": ["git", "rev-parse", "--verify", "HEAD"],
"message": "HEAD could not be resolved but the repository has HEAD history; refusing to treat it as unborn",
})
return "HEAD", "", False
if inside == "true":
empty_tree = _git_empty_tree_oid(root, errors)
if empty_tree:
return empty_tree, _GIT_UNBORN_HEAD, True
errors.extend(head_errors or worktree_errors)
return "HEAD", "", False
def _git_empty_tree_oid(root: pathlib.Path, errors: List[Dict[str, Any]]) -> str:
try:
result = subprocess.run(
["git", "hash-object", "-t", "tree", "--stdin"],
cwd=str(root),
input="",
capture_output=True,
text=True,
timeout=30,
)
except Exception as exc:
errors.append({"type": "git_exception", "command": ["git", "hash-object", "-t", "tree", "--stdin"], "message": f"{type(exc).__name__}: {exc}"})
return ""
if result.returncode != 0:
errors.append({
"type": "git_error",
"command": ["git", "hash-object", "-t", "tree", "--stdin"],
"returncode": result.returncode,
"stderr": (result.stderr or "")[-2000:],
})
return ""
return (result.stdout or "").strip()
def _head_reflog_exists(root: pathlib.Path) -> bool:
path_text = _git_stdout(["git", "rev-parse", "--git-path", "logs/HEAD"], root, allow_rc={0}).strip()
if not path_text:
return False
path = pathlib.Path(path_text)
if not path.is_absolute():
path = root / path
try:
return path.is_file() and path.stat().st_size > 0
except OSError:
return False
def _looks_like_git_oid(value: str) -> bool:
text = str(value or "").strip()
return 7 <= len(text) <= 64 and all(ch in "0123456789abcdefABCDEF" for ch in text)
def _git_path_list(cmd: Sequence[str], root: pathlib.Path, errors: Optional[List[Dict[str, Any]]] = None) -> List[str]:
output = _git_bytes(cmd, root, errors=errors)
if not output:
return []
return [part.decode("utf-8", errors="replace") for part in output.split(b"\0") if part]
def _git_bytes(
cmd: Sequence[str],
cwd: pathlib.Path,
*,
allow_rc: Iterable[int] = (0,),
errors: Optional[List[Dict[str, Any]]] = None,
) -> bytes:
try:
result = subprocess.run(
list(cmd),
cwd=str(cwd),
capture_output=True,
timeout=30,
)
except subprocess.TimeoutExpired:
if errors is not None:
errors.append({"type": "git_timeout", "command": list(cmd), "message": "git command timed out"})
return b""
except Exception as exc:
if errors is not None:
errors.append({"type": "git_exception", "command": list(cmd), "message": f"{type(exc).__name__}: {exc}"})
return b""
if result.returncode not in set(allow_rc):
if errors is not None:
errors.append({
"type": "git_error",
"command": list(cmd),
"returncode": result.returncode,
"stderr": (result.stderr or b"").decode("utf-8", errors="replace")[-2000:],
})
return b""
return result.stdout or b""
def _append_git_output(
cmd: Sequence[str],
cwd: pathlib.Path,
fh: BinaryIO,
hasher: Any,
*,
allow_rc: set[int],
errors: List[Dict[str, Any]],
diagnostics: List[Dict[str, Any]],
) -> int:
written_box = {"value": 0}
read_errors: List[str] = []
try:
with tempfile.TemporaryFile() as stderr_fh:
proc = subprocess.Popen(
list(cmd),
cwd=str(cwd),
stdout=subprocess.PIPE,
stderr=stderr_fh,
)
assert proc.stdout is not None
def _reader() -> None:
try:
while True:
chunk = proc.stdout.read(1024 * 128)
if not chunk:
break
fh.write(chunk)
hasher.update(chunk)
written_box["value"] += len(chunk)
except Exception as exc:
read_errors.append(f"{type(exc).__name__}: {exc}")
reader = threading.Thread(target=_reader, name="workspace-patch-git-stdout", daemon=True)
reader.start()
try:
proc.wait(timeout=30)
except subprocess.TimeoutExpired:
try:
proc.kill()
except Exception:
pass
try:
proc.wait(timeout=5)
except Exception:
pass
reader.join(timeout=5)
if reader.is_alive():
errors.append({"type": "git_timeout", "command": list(cmd), "message": "git stdout reader timed out"})
errors.append({"type": "git_timeout", "command": list(cmd), "message": "git command timed out"})
return int(written_box["value"])
reader.join(timeout=5)
if reader.is_alive():
errors.append({"type": "git_timeout", "command": list(cmd), "message": "git stdout reader timed out"})
for read_error in read_errors:
errors.append({"type": "git_exception", "command": list(cmd), "message": read_error})
stderr_fh.seek(0)
stderr = stderr_fh.read() or b""
except subprocess.TimeoutExpired:
try:
proc.kill() # type: ignore[possibly-undefined]
except Exception:
pass
errors.append({"type": "git_timeout", "command": list(cmd), "message": "git command timed out"})
return int(written_box["value"])
except Exception as exc:
errors.append({"type": "git_exception", "command": list(cmd), "message": f"{type(exc).__name__}: {exc}"})
return int(written_box["value"])
if proc.returncode not in allow_rc:
errors.append({
"type": "git_error",
"command": list(cmd),
"returncode": proc.returncode,
"stderr": stderr.decode("utf-8", errors="replace")[-2000:],
})
written = int(written_box["value"])
diagnostics.append({"command": list(cmd), "returncode": proc.returncode, "bytes": written})
return written
def _write_patch_separator(fh: BinaryIO, hasher: Any) -> int:
data = b"\n"
fh.write(data)
hasher.update(data)
return len(data)
def _untracked_blob_exclude_reason(root: pathlib.Path, rel: str) -> str:
"""Reason to drop an untracked file from the workspace patch when it is a
build/runtime BINARY or exceeds the per-file size cap. Keeps real-usage
patches source-shaped without losing data (the file stays in the workspace
and is recorded under ``untracked_excluded``). On any git/stat failure the
file is INCLUDED (conservative — the main binary diff still applies)."""
try:
size = (root / rel).lstat().st_size
except OSError:
return "" # unreadable/symlink races: include and let git decide
if size > _PATCH_MAX_UNTRACKED_FILE_BYTES:
return f"untracked file exceeds size cap ({size}B > {_PATCH_MAX_UNTRACKED_FILE_BYTES}B)"
numstat = _git_stdout(
["git", "diff", "--no-index", "--numstat", "--no-ext-diff", "--no-color", "--", os.devnull, rel],
root,
allow_rc={0, 1},
errors=None,
)
first = numstat.strip().splitlines()[0] if numstat.strip() else ""
if first.startswith("-\t-"):
return "binary file"
return ""
def untracked_capture_veto_reason(root: pathlib.Path, rel: str) -> str:
"""Why an untracked file must NOT ride into a workspace snapshot or patch.
The delegated-run baseline snapshot
(``subagent_worktrees.provision_execution_snapshot``) asks the SAME three
checks, in the SAME order, that ``write_workspace_patch_artifacts`` applies
to untracked files: sensitive/credential-shaped names first, then the
static junk rules, then the binary/size veto. One combined predicate here so
the snapshot and the patch cannot drift apart about eligibility.
Returns the human-readable reason, or "" when the file is eligible.
"""
reason = _sensitive_untracked_reason(rel)
if reason:
return reason
reason = _patch_exclude_reason(rel)
if reason:
return reason
return _untracked_blob_exclude_reason(root, rel)
def _preflight_head_from_task(task: Dict[str, Any]) -> str:
meta = task.get("metadata") if isinstance(task.get("metadata"), dict) else {}
preflight = meta.get("workspace_preflight") if isinstance(meta.get("workspace_preflight"), dict) else {}
git = preflight.get("git") if isinstance(preflight.get("git"), dict) else {}
return str(git.get("head") or "")
def _preflight_head_present(task: Dict[str, Any]) -> bool:
meta = task.get("metadata") if isinstance(task.get("metadata"), dict) else {}
preflight = meta.get("workspace_preflight") if isinstance(meta.get("workspace_preflight"), dict) else {}
git = preflight.get("git") if isinstance(preflight.get("git"), dict) else {}
return "head" in git
def _acting_constraint_from_task(task: Dict[str, Any]):
"""Normalized acting-subagent constraint carried by ``task``, or None."""
raw = task.get("task_constraint") if isinstance(task.get("task_constraint"), dict) else {}
if not raw:
meta = task.get("metadata") if isinstance(task.get("metadata"), dict) else {}
raw = meta.get("task_constraint") if isinstance(meta.get("task_constraint"), dict) else {}
try:
constraint = normalize_task_constraint(raw)
except Exception:
return None
return constraint if constraint and constraint.mode == "acting_subagent" else None
def _empty_patch_manifest(
workspace_root: pathlib.Path,
*,
status: str,
errors: List[Dict[str, Any]],
) -> Dict[str, Any]:
return {
"schema_version": 1,
"created_at": utc_now_iso(),
"status": status,
"workspace_root": str(workspace_root),
"patch_name": "workspace.patch",
"manifest_name": "workspace_patch.json",
"base_ref": "",
"base_head": "",
"base_is_empty_tree": False,
"current_head": "",
"patch_size": 0,
"sha256": "",
"diffstat": "",
"counts": {
"tracked_changed": 0,
"untracked_included": 0,
"untracked_excluded": 0,
"sensitive_blocked": 0,
},
"tracked_changed": [],
"untracked_included": [],
"untracked_excluded": [],
"sensitive_blocked": [],
"exclude_rules_version": _PATCH_EXCLUDE_RULES_VERSION,
"diagnostics": [],
"errors": errors,
}

View file

@ -0,0 +1,35 @@
"""Shared fixtures and helpers for the headless task/CLI suites.
Split out of ``tests/test_headless_cli.py`` when that module was divided by
theme; the definitions are verbatim so every sibling suite keeps the exact
fixture semantics it was written against. ``_managed_worker_pool_available``
is autouse, so importing it into a test module re-applies it there.
"""
from __future__ import annotations
import subprocess
from types import SimpleNamespace
import pytest
@pytest.fixture(autouse=True)
def _managed_worker_pool_available(monkeypatch):
"""HTTP task tests model a ready server unless a case overrides the pool."""
import supervisor.workers as workers
monkeypatch.setattr(workers, "WORKERS", {0: SimpleNamespace()})
monkeypatch.setattr(workers, "_WORKER_POOL_DISABLED_REASON", "")
def _init_repo_with_file(repo, name="tracked.txt", content="old\n"):
repo.mkdir()
subprocess.run(["git", "init"], cwd=repo, check=True, capture_output=True)
(repo / name).write_text(content, encoding="utf-8")
subprocess.run(["git", "add", name], cwd=repo, check=True, capture_output=True)
subprocess.run(
["git", "-c", "user.email=t@example.com", "-c", "user.name=T", "commit", "-m", "init"],
cwd=repo,
check=True,
capture_output=True,
)

View file

@ -0,0 +1,23 @@
"""The git-repo builder shared by the workspace-executor suites.
Split out of ``tests/test_workspace_executor.py`` when that module was divided by
theme; the helper is verbatim, so every sibling suite keeps the exact repository layout
it was written against.
"""
from __future__ import annotations
import subprocess
from pathlib import Path
def _init_repo(path: Path) -> None:
path.mkdir(parents=True, exist_ok=True)
subprocess.run(["git", "init"], cwd=path, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
subprocess.run(["git", "config", "user.email", "test@example.invalid"], cwd=path, check=True)
subprocess.run(["git", "config", "user.name", "Test"], cwd=path, check=True)
(path / "README.md").write_text("x\n", encoding="utf-8")
subprocess.run(["git", "add", "README.md"], cwd=path, check=True)
subprocess.run(["git", "commit", "-m", "init"], cwd=path, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)

View file

@ -170,6 +170,11 @@ def _mock_pollution_files(root: pathlib.Path) -> set[pathlib.Path]:
# See docs/DEVELOPMENT.md "Pytest marker lanes".
_SERIAL_TEST_FILES = frozenset({
"test_workspace_executor.py",
# Themed siblings of test_workspace_executor.py; they spawn the same real
# processes, so the whole family stays in the serial lane.
"test_workspace_executor_services.py",
"test_workspace_executor_docker.py",
"test_workspace_executor_admission.py",
"test_workspace_executor_cleanup.py",
"test_process_custody.py",
"test_kill_process_tree_orphans.py",

View file

@ -1263,149 +1263,6 @@ def test_multi_round_zero_tool_task_uses_llm_summary_prompt(tmp_path, monkeypatc
assert payload["rounds"] == 3
def test_store_task_result_persists_review_evidence(tmp_path):
env = SimpleNamespace(drive_root=tmp_path)
pipeline._store_task_result(
env=env,
task={"id": "task-store", "type": "task", "text": "hi"},
text="done",
usage={"rounds": 2, "cost": 0.1},
llm_trace={"tool_calls": [], "reasoning_notes": []},
review_evidence={"has_evidence": True, "open_obligations": [{"item": "tests_affected"}]},
)
payload = json.loads((tmp_path / "task_results" / "task-store.json").read_text(encoding="utf-8"))
assert payload["review_evidence"]["has_evidence"] is True
assert payload["review_evidence"]["open_obligations"][0]["item"] == "tests_affected"
def test_store_task_result_persists_only_compact_review_projection(tmp_path):
env = SimpleNamespace(drive_root=tmp_path)
trace = {
"tool_calls": [],
"review_runs": [{
"request": {"surface": "task_acceptance", "policy": {"min_successful_slots": 1}},
"authority": "host_root",
"aggregate_signal": "DEGRADED",
"actors": [{
"slot_id": "slot_1", "model": "openai/gpt-5.6-sol", "status": "ok",
"parsed": {"verdict": "DEGRADED", "summary": "not enough evidence"},
"signal": "DEGRADED", "raw_text": "PRIVATE RAW MODEL RESPONSE",
}],
}],
}
pipeline._store_task_result(
env=env,
task={"id": "task-review-projection", "type": "task", "text": "hi"},
text="done",
usage={"rounds": 1, "cost": 0.0},
llm_trace=trace,
review_evidence={},
)
payload = json.loads(
(tmp_path / "task_results" / "task-review-projection.json").read_text(encoding="utf-8")
)
actor = payload["review_projection"]["panels"][0]["actors"][0]
assert actor["model"] == "openai/gpt-5.6-sol"
assert actor["parse_status"] == "valid"
assert actor["semantic_verdict"] == "DEGRADED"
assert "raw_text" not in actor
assert "PRIVATE RAW MODEL RESPONSE" not in json.dumps(payload)
def test_store_task_result_preserves_failed_status(tmp_path):
from ouroboros.task_results import STATUS_FAILED, write_task_result
env = SimpleNamespace(drive_root=tmp_path)
write_task_result(tmp_path, "task-failed", STATUS_FAILED, result="initial failure")
pipeline._store_task_result(
env=env,
task={"id": "task-failed", "type": "task", "text": "hi"},
text="final failure reply",
usage={"rounds": 1, "cost": 0.0},
llm_trace={"tool_calls": [], "reasoning_notes": []},
review_evidence={},
)
payload = json.loads((tmp_path / "task_results" / "task-failed.json").read_text(encoding="utf-8"))
assert payload["status"] == STATUS_FAILED
assert payload["result"] == "final failure reply"
def test_store_task_result_marks_unresolved_tool_failure_failed(tmp_path):
from ouroboros.task_results import STATUS_COMPLETED
env = SimpleNamespace(drive_root=tmp_path)
pipeline._store_task_result(
env=env,
task={"id": "task-tool-failed", "type": "task", "text": "make file"},
text="Created the file.",
usage={"rounds": 2, "cost": 0.0},
llm_trace={
"tool_calls": [{
"tool": "run_command",
"args": {"cmd": "python3 -c ..."},
"result": "⚠️ ARTIFACT_OUTPUT_ERROR: command succeeded but declared output registration failed.",
"is_error": True,
"status": "artifact_output_error",
}],
"reasoning_notes": [],
},
review_evidence={},
)
payload = json.loads((tmp_path / "task_results" / "task-tool-failed.json").read_text(encoding="utf-8"))
assert payload["status"] == STATUS_COMPLETED
assert payload["outcome_axes"]["execution"]["status"] == "degraded"
assert payload["outcome_axes"]["objective"]["status"] == "not_evaluated"
assert payload["reason_code"] == "tool_failure"
assert payload["loop_outcome"]["failure"]["tool_errors"][0]["status"] == "artifact_output_error"
def test_store_task_result_allows_recovered_tool_failure_success(tmp_path):
from ouroboros.task_results import STATUS_COMPLETED
env = SimpleNamespace(drive_root=tmp_path)
pipeline._store_task_result(
env=env,
task={"id": "task-tool-recovered", "type": "task", "text": "make file"},
text="Created the file.",
usage={"rounds": 3, "cost": 0.0},
llm_trace={
"tool_calls": [
{
"tool": "edit_text",
"args": {"path": "Desktop/report.html"},
"result": "⚠️ EDIT_TEXT_ERROR: old_str matched 0 times",
"is_error": True,
"status": "edit_text_blocked",
},
{
"tool": "write_file",
"args": {"root": "user_files", "path": "Desktop/report.html"},
"result": "OK: wrote user_files:Desktop/report.html\nARTIFACT_OUTPUTS: registered user file -> artifact_store:report.html",
"is_error": False,
"status": "ok",
"artifact_registered": True,
},
],
"reasoning_notes": [],
},
review_evidence={},
)
payload = json.loads((tmp_path / "task_results" / "task-tool-recovered.json").read_text(encoding="utf-8"))
assert payload["status"] == STATUS_COMPLETED
assert payload["outcome_axes"]["execution"]["status"] == "ok"
assert payload["outcome_axes"]["objective"]["status"] == "not_evaluated"
assert payload["loop_outcome"]["failure"] is None
def test_collect_review_evidence_keeps_recent_attempts_task_scoped(tmp_path):
from ouroboros.review_evidence import collect_review_evidence
from ouroboros.review_state import AdvisoryReviewState, CommitAttemptRecord, make_repo_key, save_state

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,123 @@
"""Structural contracts for the semantic-no-op headless extraction."""
from __future__ import annotations
import ast
import pathlib
from ouroboros import headless, headless_status, workspace_patch_capture
REPO = pathlib.Path(__file__).parents[1]
_LEAVES = (headless_status, workspace_patch_capture)
_MOVED_OWNERS = {
"ARTIFACT_STATUS_FAILED": headless_status,
"ARTIFACT_STATUS_FINALIZING": headless_status,
"ARTIFACT_STATUS_MISSING": headless_status,
"ARTIFACT_STATUS_PENDING": headless_status,
"ARTIFACT_STATUS_READY": headless_status,
"ARTIFACT_STATUS_READY_NO_CHANGES": headless_status,
"ARTIFACT_STATUS_READY_WITH_CHANGES": headless_status,
"ARTIFACT_TERMINAL_STATUSES": headless_status,
"_ARTIFACT_LIFECYCLE_FIELDS": headless_status,
"_FINAL_STATUSES": headless_status,
"_LOCAL_READONLY_SUBAGENT_MODE": headless_status,
"SCRATCH_MANIFEST_NAME": workspace_patch_capture,
"_GIT_UNBORN_HEAD": workspace_patch_capture,
"_acting_constraint_from_task": workspace_patch_capture,
"_append_git_output": workspace_patch_capture,
"_empty_patch_manifest": workspace_patch_capture,
"_git_bytes": workspace_patch_capture,
"_git_empty_tree_oid": workspace_patch_capture,
"_git_path_list": workspace_patch_capture,
"_git_stdout": workspace_patch_capture,
"_head_reflog_exists": workspace_patch_capture,
"_looks_like_git_oid": workspace_patch_capture,
"_preflight_head_from_task": workspace_patch_capture,
"_preflight_head_present": workspace_patch_capture,
"_untracked_blob_exclude_reason": workspace_patch_capture,
"_workspace_patch_base": workspace_patch_capture,
"_write_patch_separator": workspace_patch_capture,
"build_workspace_patch": workspace_patch_capture,
"untracked_capture_veto_reason": workspace_patch_capture,
"write_workspace_patch_artifacts": workspace_patch_capture,
}
def test_headless_leaves_are_non_catalog_owners_without_headless_backedges():
for module in (headless, *_LEAVES):
source_path = pathlib.Path(module.__file__)
tree = ast.parse(source_path.read_text(encoding="utf-8"))
assert not any(
isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))
and node.name == "get_tools"
for node in tree.body
)
for module in _LEAVES:
tree = ast.parse(pathlib.Path(module.__file__).read_text(encoding="utf-8"))
assert not any(
isinstance(node, ast.ImportFrom) and node.module == "ouroboros.headless"
for node in ast.walk(tree)
)
assert not any(
isinstance(node, ast.Import)
and any(alias.name == "ouroboros.headless" for alias in node.names)
for node in ast.walk(tree)
)
# v7next transplant note: the reference test (ouroboros_v7_wip @ 9f691656)
# additionally proves the three modules stay out of the frozen tool-module
# inventory via ouroboros.tool_module_inventory; that leaf belongs to the
# tools domain and is not on this integration branch yet — the clause
# returns with its lane. The static guarantee it rested on is kept above:
# none of the three modules defines get_tools, so no catalog can adopt them.
def test_headless_public_export_list_is_unchanged():
"""``__all__`` is the module's declared contract; the extraction moved owners,
never the surface, so every published name still resolves on ``headless``."""
assert headless.__all__ == [
"ARTIFACT_STATUS_FAILED",
"ARTIFACT_STATUS_FINALIZING",
"ARTIFACT_STATUS_PENDING",
"ARTIFACT_STATUS_READY",
"build_memory_export",
"build_workspace_patch",
"copy_child_task_result",
"finalize_task_artifacts",
"task_is_readonly_subagent",
"prepare_task_drive",
"prune_headless_task_drives",
"prune_task_drives",
"task_artifacts_dir",
"task_state_dir",
"write_workspace_patch_artifacts",
"write_workspace_preflight_artifact",
]
for name in headless.__all__:
assert hasattr(headless, name), name
def test_headless_facade_reexports_every_moved_identity():
"""``headless`` keeps the exact objects, so the supervisor, the gateway,
outcomes, task_status, artifacts and the delegation owners see no identity
change."""
for name, owner in _MOVED_OWNERS.items():
assert hasattr(headless, name), name
assert getattr(headless, name) is getattr(owner, name), name
owned = {name for module in _LEAVES for name in vars(module)}
assert set(_MOVED_OWNERS) <= owned
def test_headless_extraction_size_bounds_have_meaningful_headroom():
counts = {
module.__name__: len(
pathlib.Path(module.__file__).read_text(encoding="utf-8").splitlines()
)
for module in (headless, *_LEAVES)
}
assert counts["ouroboros.headless"] <= 1000
assert all(count <= 1000 for count in counts.values())
assert 400 <= counts["ouroboros.workspace_patch_capture"] <= 1000

View file

@ -0,0 +1,596 @@
"""Gateway task-creation API: admission, validation and lineage authority.
Split verbatim out of ``tests/test_headless_cli.py`` by theme. This module
owns ``POST /api/tasks`` behaviour — child-drive creation, reservation and
admission refusals, payload validation, and the forgery guards on task id,
workspace root, subagent role and lineage.
"""
from __future__ import annotations
import json
import subprocess
import pytest
from starlette.applications import Starlette
from starlette.routing import Route
from starlette.testclient import TestClient
from ouroboros.gateway.tasks import (
_compose_task_text,
_resolve_workspace_root,
api_tasks_create,
)
from ouroboros.headless import (
task_artifacts_dir,
)
from tests._headless_cli_shared import ( # noqa: F401 (autouse fixture applies on import)
_managed_worker_pool_available,
)
def test_task_api_enqueue_workspace_creates_child_drive(tmp_path, monkeypatch):
workspace = tmp_path / "workspace"
workspace.mkdir()
subprocess.run(["git", "init"], cwd=workspace, check=True, capture_output=True)
repo = tmp_path / "repo"
repo.mkdir()
data = tmp_path / "data"
(data / "memory").mkdir(parents=True)
(data / "memory" / "identity.md").write_text("seed identity", encoding="utf-8")
captured = []
bootstrapped = []
def fake_enqueue(task):
captured.append(dict(task))
return task
monkeypatch.setattr("supervisor.queue.enqueue_task", fake_enqueue)
monkeypatch.setattr("supervisor.queue.persist_queue_snapshot", lambda reason="": True)
monkeypatch.setattr("ouroboros.workspace_admission.bootstrap_process_path", lambda: bootstrapped.append(True) or [])
app = Starlette(routes=[Route("/api/tasks", endpoint=api_tasks_create, methods=["POST"])])
app.state.drive_root = data
app.state.repo_dir = repo
response = TestClient(app).post(
"/api/tasks",
json={
"description": "fix it",
"workspace_root": str(workspace),
"memory_mode": "forked",
"expected_output": "A workspace patch and concise handoff.",
"constraints": "No network.",
"allowed_resources": {"web": False, "network": False},
"resource_policy": {
"protected_artifacts": [
{
"id": "reference",
"role": "black_box_reference",
"paths": ["reference.bin"],
"allow": ["execute"],
}
]
},
"deadline_at": "2026-06-04T12:00:00Z",
"service_teardown": "keep",
"context_requires_self_body_docs": "false",
"metadata": {
"root_task_id": "forged-root",
"parent_task_id": "forged-parent",
"delegation_role": "root",
"child_drive_root": "/tmp/forged-child",
},
},
)
assert response.status_code == 200
payload = response.json()
assert payload["task_id"]
assert bootstrapped
assert captured and captured[0]["workspace_root"] == str(workspace.resolve(strict=False))
assert captured[0]["deadline_at"] == "2026-06-04T12:00:00Z"
assert captured[0]["metadata"]["service_teardown"] == "keep"
assert captured[0]["allowed_resources"] == {"web": False, "network": False}
assert captured[0]["context_requires_self_body_docs"] is False
assert captured[0]["task_contract"]["expected_output"] == "A workspace patch and concise handoff."
assert captured[0]["task_contract"]["constraints"] == "No network."
assert captured[0]["task_contract"]["context_requires_self_body_docs"] is False
assert captured[0]["task_contract"]["resource_policy"]["protected_artifacts"][0]["paths"] == ["reference.bin"]
child_drive = captured[0]["drive_root"]
assert child_drive
assert (tmp_path / "data" / "task_results" / f"{payload['task_id']}.json").is_file()
assert "seed identity" in (data / "state" / "headless_tasks" / payload["task_id"] / "data" / "memory" / "identity.md").read_text(encoding="utf-8")
result = json.loads((data / "task_results" / f"{payload['task_id']}.json").read_text(encoding="utf-8"))
assert result["artifact_status"] == "pending"
assert captured[0]["root_task_id"] == payload["task_id"]
assert captured[0]["parent_task_id"] is None
assert captured[0]["delegation_role"] == "root"
assert result["metadata"]["root_task_id"] == payload["task_id"]
assert result["metadata"]["parent_task_id"] == ""
assert result["metadata"]["delegation_role"] == "root"
assert result["task_contract"]["deadline_at"] == "2026-06-04T12:00:00Z"
assert result["task_contract"]["allowed_resources"] == {"web": False, "network": False}
assert result["task_contract"]["resource_policy"]["protected_artifacts"][0]["id"] == "reference"
assert result["metadata"]["child_drive_root"] == captured[0]["child_drive_root"]
assert "/tmp/forged-child" not in json.dumps(result["metadata"])
assert result["metadata"]["workspace_preflight"]["git"]["head"] == ""
assert any(item["kind"] == "workspace_preflight" for item in result["artifacts"])
assert "workspace_preflight:" in captured[0]["text"]
assert "target workspace, not the Ouroboros system repo" in captured[0]["text"]
def test_task_api_admission_refusal_is_terminal_not_scheduled_phantom(tmp_path, monkeypatch):
from ouroboros.task_results import STATUS_FAILED, load_task_result
repo = tmp_path / "repo"
repo.mkdir()
data = tmp_path / "data"
(data / "memory").mkdir(parents=True)
persisted = []
monkeypatch.setattr(
"supervisor.queue.enqueue_task",
lambda task: {
**task,
"_admission_blocked": "project_routing_fence",
"_project_id": "closed-project",
"_project_lifecycle": "deleting",
},
)
monkeypatch.setattr(
"supervisor.queue.persist_queue_snapshot",
lambda reason="": persisted.append(reason),
)
app = Starlette(routes=[Route("/api/tasks", endpoint=api_tasks_create, methods=["POST"])])
app.state.drive_root = data
app.state.repo_dir = repo
response = TestClient(app).post(
"/api/tasks",
json={
"description": "must not run",
"task_id": "blocked-root",
"project_id": "closed-project",
},
)
assert response.status_code == 409
payload = response.json()
assert payload["task_id"] == "blocked-root"
assert payload["status"] == STATUS_FAILED
assert payload["admission"]["reason_code"] == "project_routing_fence"
assert payload["admission"]["project_lifecycle"] == "deleting"
assert persisted == []
result = load_task_result(data, "blocked-root")
assert result["status"] == STATUS_FAILED
assert result["reason_code"] == "project_routing_fence"
assert result["admission_cleanup"] == {"child_drive_removed": True}
assert not (data / "state" / "headless_tasks" / "blocked-root").exists()
def test_task_api_refuses_when_durable_queue_snapshot_fails(tmp_path, monkeypatch):
import supervisor.queue as queue
from ouroboros.task_results import STATUS_FAILED, load_task_result
repo = tmp_path / "repo"
repo.mkdir()
data = tmp_path / "data"
(data / "memory").mkdir(parents=True)
pending = []
monkeypatch.setattr(queue, "DRIVE_ROOT", data)
monkeypatch.setattr(queue, "PENDING", pending)
monkeypatch.setattr(queue, "RUNNING", {})
calls = []
def persist(reason=""):
calls.append(reason)
return reason == "api_task_create_rollback"
monkeypatch.setattr(queue, "persist_queue_snapshot", persist)
app = Starlette(routes=[Route("/api/tasks", endpoint=api_tasks_create, methods=["POST"])])
app.state.drive_root = data
app.state.repo_dir = repo
response = TestClient(app).post(
"/api/tasks",
json={"description": "must be durable", "task_id": "snapshot-fail"},
)
assert response.status_code == 503
assert response.json()["admission"]["reason_code"] == "queue_snapshot_persist_failed"
assert pending == []
assert calls == ["api_task_create", "api_task_create_rollback"]
assert load_task_result(data, "snapshot-fail")["status"] == STATUS_FAILED
assert not (data / "state" / "headless_tasks" / "snapshot-fail").exists()
def test_task_api_releases_reservation_when_payload_composition_fails(
tmp_path, monkeypatch,
):
import supervisor.queue as queue
from ouroboros.gateway import tasks
data = tmp_path / "data"
repo = tmp_path / "repo"
data.mkdir()
repo.mkdir()
task_id = "compose-failure"
real_compose = tasks._compose_task_text
monkeypatch.setattr(
tasks,
"_compose_task_text",
lambda *_args, **_kwargs: (_ for _ in ()).throw(RuntimeError("compose failed")),
)
monkeypatch.setattr(queue, "enqueue_task", lambda task: task)
monkeypatch.setattr(queue, "persist_queue_snapshot", lambda **_kwargs: True)
app = Starlette(routes=[Route("/api/tasks", endpoint=api_tasks_create, methods=["POST"])])
app.state.drive_root = data
app.state.repo_dir = repo
client = TestClient(app)
failed = client.post(
"/api/tasks", json={"task_id": task_id, "description": "compose me"}
)
assert failed.status_code == 503
assert task_id not in queue.ADMISSION_RESERVATIONS
assert not task_artifacts_dir(data, task_id, create=False).exists()
monkeypatch.setattr(tasks, "_compose_task_text", real_compose)
retried = client.post(
"/api/tasks", json={"task_id": task_id, "description": "compose me"}
)
assert retried.status_code == 200, retried.text
def test_api_tasks_create_requires_description_not_legacy_aliases(monkeypatch):
captured = []
monkeypatch.setattr("supervisor.queue.enqueue_task", lambda task: captured.append(task) or task)
app = Starlette(routes=[Route("/api/tasks", endpoint=api_tasks_create, methods=["POST"])])
client = TestClient(app)
for payload in ({"text": "legacy task"}, {"prompt": "legacy task"}, {"description": ""}):
response = client.post("/api/tasks", json=payload)
assert response.status_code == 400, (payload, response.text)
assert "description is required" in response.json().get("error", "")
response = client.post("/api/tasks", json={"description": "x", "service_teardown": "detach"})
assert response.status_code == 400
assert "service_teardown" in response.json().get("error", "")
assert captured == []
def test_api_tasks_create_rejects_internal_task_types(tmp_path, monkeypatch):
repo = tmp_path / "repo"
repo.mkdir()
data = tmp_path / "data"
(data / "memory").mkdir(parents=True)
monkeypatch.setattr("supervisor.queue.enqueue_task", lambda task: task)
monkeypatch.setattr("supervisor.queue.persist_queue_snapshot", lambda reason="": True)
monkeypatch.setattr("ouroboros.workspace_admission.bootstrap_process_path", lambda: [])
app = Starlette(routes=[Route("/api/tasks", endpoint=api_tasks_create, methods=["POST"])])
app.state.drive_root = data
app.state.repo_dir = repo
client = TestClient(app)
for internal_type in ("evolution", "review", "deep_self_review"):
resp = client.post("/api/tasks", json={"description": "x", "type": internal_type})
assert resp.status_code == 400, (internal_type, resp.text)
assert "internal" in resp.json().get("error", "").lower()
# A normal task type is still accepted.
ok = client.post("/api/tasks", json={"description": "do normal work", "type": "task"})
assert ok.status_code == 200, ok.text
def test_compose_task_text_extends_existing_headless_workspace_block(tmp_path):
text = _compose_task_text(
"fix\n\n[HEADLESS_WORKSPACE]\nexisting: yes\n[END_HEADLESS_WORKSPACE]",
workspace_root=tmp_path,
workspace_mode="external",
memory_mode="empty",
workspace_preflight={"error": "probe failed"},
attachments=[],
)
assert text.count("[HEADLESS_WORKSPACE]") == 1
assert "existing: yes" in text
assert "preflight_error: probe failed" in text
assert text.index("workspace_root:") < text.index("[END_HEADLESS_WORKSPACE]")
def test_task_api_rejects_unsafe_task_id_and_system_workspace(tmp_path, monkeypatch):
workspace = tmp_path / "workspace"
workspace.mkdir()
subprocess.run(["git", "init"], cwd=workspace, check=True, capture_output=True)
repo = tmp_path / "repo"
repo.mkdir()
subprocess.run(["git", "init"], cwd=repo, check=True, capture_output=True)
data = tmp_path / "data"
data.mkdir()
monkeypatch.setattr("supervisor.queue.enqueue_task", lambda task: task)
monkeypatch.setattr("supervisor.queue.persist_queue_snapshot", lambda reason="": True)
app = Starlette(routes=[Route("/api/tasks", endpoint=api_tasks_create, methods=["POST"])])
app.state.drive_root = data
app.state.repo_dir = repo
client = TestClient(app)
bad_id = client.post("/api/tasks", json={"description": "x", "task_id": "../settings", "workspace_root": str(workspace)})
assert bad_id.status_code == 400
assert not (data / "settings.json").exists()
system_repo = client.post("/api/tasks", json={"description": "x", "workspace_root": str(repo)})
assert system_repo.status_code == 400
assert "system repo" in system_repo.json()["error"]
bad_numbers = client.post("/api/tasks", json={"description": "x", "chat_id": "not-int", "workspace_root": str(workspace)})
assert bad_numbers.status_code == 400
bad_deadline = client.post("/api/tasks", json={"description": "x", "deadline_at": "not-a-date", "workspace_root": str(workspace)})
assert bad_deadline.status_code == 400
assert "deadline_at" in bad_deadline.json()["error"]
naive_deadline = client.post("/api/tasks", json={"description": "x", "deadline_at": "2026-06-04T12:00:00", "workspace_root": str(workspace)})
assert naive_deadline.status_code == 400
assert "timezone" in naive_deadline.json()["error"]
first = client.post("/api/tasks", json={"description": "x", "task_id": "fixed1", "workspace_root": str(workspace)})
assert first.status_code == 200
duplicate = client.post("/api/tasks", json={"description": "x", "task_id": "fixed1", "workspace_root": str(workspace)})
assert duplicate.status_code == 409
typed = client.post("/api/tasks", json={"description": "x", "type": "deep_self_review", "workspace_root": str(workspace)})
assert typed.status_code == 400
def test_resolve_workspace_root_blocks_case_variant_control_plane(tmp_path):
system_repo = tmp_path / "Ouroboros" / "repo"
drive = tmp_path / "Ouroboros" / "data"
workspace_repo_case = tmp_path / "ouroboros" / "repo"
workspace_data_case = tmp_path / "ouroboros" / "data" / "workspace"
for path in (system_repo, drive / "workspace"):
path.mkdir(parents=True)
with pytest.raises(ValueError, match="Ouroboros system repo"):
_resolve_workspace_root(workspace_repo_case, system_repo_dir=system_repo, drive_root=drive)
with pytest.raises(ValueError, match="Ouroboros data drive"):
_resolve_workspace_root(workspace_data_case, system_repo_dir=system_repo, drive_root=drive)
def test_task_api_rejects_forged_subagent_without_child_drive_side_effect(tmp_path, monkeypatch):
workspace = tmp_path / "workspace"
workspace.mkdir()
subprocess.run(["git", "init"], cwd=workspace, check=True, capture_output=True)
repo = tmp_path / "repo"
repo.mkdir()
subprocess.run(["git", "init"], cwd=repo, check=True, capture_output=True)
data = tmp_path / "data"
data.mkdir()
monkeypatch.setattr("supervisor.queue.enqueue_task", lambda task: pytest.fail("forged subagent enqueued"))
monkeypatch.setattr("supervisor.queue.persist_queue_snapshot", lambda reason="": True)
app = Starlette(routes=[Route("/api/tasks", endpoint=api_tasks_create, methods=["POST"])])
app.state.drive_root = data
app.state.repo_dir = repo
client = TestClient(app)
top_level = client.post(
"/api/tasks",
json={"description": "x", "task_id": "forged1", "workspace_root": str(workspace), "delegation_role": "subagent"},
)
metadata = client.post(
"/api/tasks",
json={"description": "x", "task_id": "forged2", "workspace_root": str(workspace), "metadata": {"delegation_role": "subagent"}},
)
assert top_level.status_code == 400
assert metadata.status_code == 400
assert "internal schedule_subagent" in top_level.json()["error"]
assert not (data / "state" / "headless_tasks" / "forged1").exists()
assert not (data / "state" / "headless_tasks" / "forged2").exists()
def test_task_api_rejects_external_lineage_forgery(tmp_path, monkeypatch):
workspace = tmp_path / "workspace"
workspace.mkdir()
subprocess.run(["git", "init"], cwd=workspace, check=True, capture_output=True)
repo = tmp_path / "repo"
repo.mkdir()
subprocess.run(["git", "init"], cwd=repo, check=True, capture_output=True)
data = tmp_path / "data"
data.mkdir()
monkeypatch.setattr("supervisor.queue.enqueue_task", lambda task: pytest.fail("forged lineage enqueued"))
monkeypatch.setattr("supervisor.queue.persist_queue_snapshot", lambda reason="": True)
app = Starlette(routes=[Route("/api/tasks", endpoint=api_tasks_create, methods=["POST"])])
app.state.drive_root = data
app.state.repo_dir = repo
response = TestClient(app).post(
"/api/tasks",
json={
"description": "x",
"workspace_root": str(workspace),
"parent_task_id": "parent1",
"root_task_id": "root1",
},
)
assert response.status_code == 400
assert "internal lineage fields" in response.json()["error"]
assert not list((data / "task_results").glob("*.json"))
def test_task_api_preserves_top_level_actor_id_after_metadata_sanitization(tmp_path, monkeypatch):
workspace = tmp_path / "workspace"
workspace.mkdir()
subprocess.run(["git", "init"], cwd=workspace, check=True, capture_output=True)
repo = tmp_path / "repo"
repo.mkdir()
subprocess.run(["git", "init"], cwd=repo, check=True, capture_output=True)
data = tmp_path / "data"
data.mkdir()
captured = []
monkeypatch.setattr("supervisor.queue.enqueue_task", lambda task: captured.append(dict(task)) or task)
monkeypatch.setattr("supervisor.queue.persist_queue_snapshot", lambda reason="": True)
app = Starlette(routes=[Route("/api/tasks", endpoint=api_tasks_create, methods=["POST"])])
app.state.drive_root = data
app.state.repo_dir = repo
response = TestClient(app).post(
"/api/tasks",
json={
"description": "x",
"workspace_root": str(workspace),
"memory_mode": "forked",
"actor_id": "operator-1",
"metadata": {"actor_id": "forged-metadata"},
},
)
assert response.status_code == 200
assert captured[0]["actor_id"] == "operator-1"
result = json.loads((data / "task_results" / f"{response.json()['task_id']}.json").read_text(encoding="utf-8"))
assert result["metadata"]["actor_id"] == "operator-1"
assert "forged-metadata" not in json.dumps(result)
def test_task_api_attachment_admission_is_atomic_by_default(tmp_path, monkeypatch):
import supervisor.queue as queue
from ouroboros.task_results import load_task_result
data = tmp_path / "data"
repo = tmp_path / "repo"
data.mkdir()
repo.mkdir()
good = tmp_path / "good.txt"
good.write_text("ok", encoding="utf-8")
captured = []
monkeypatch.setattr(queue, "enqueue_task", lambda task: captured.append(task) or task)
monkeypatch.setattr(queue, "persist_queue_snapshot", lambda reason="": True)
app = Starlette(routes=[Route("/api/tasks", endpoint=api_tasks_create, methods=["POST"])])
app.state.drive_root = data
app.state.repo_dir = repo
response = TestClient(app).post(
"/api/tasks",
json={
"task_id": "atomic-attachments",
"description": "needs both",
"attachments": [
{"path": str(good), "label": "good"},
{"path": str(tmp_path / "missing.txt"), "label": "missing"},
],
},
)
assert response.status_code == 422
payload = response.json()
assert payload["reason_code"] == "attachment_admission_rejected"
assert [row["status"] for row in payload["attachment_manifest"]] == ["staged", "rejected"]
assert payload["attachment_manifest"][1]["reason"] == "source_missing"
assert captured == []
assert load_task_result(data, "atomic-attachments") is None
assert "atomic-attachments" not in queue.ADMISSION_RESERVATIONS
assert not task_artifacts_dir(data, "atomic-attachments", create=False).exists()
def test_task_api_explicit_partial_attachments_reaches_caller_contract_and_actor(
tmp_path, monkeypatch,
):
import supervisor.queue as queue
data = tmp_path / "data"
repo = tmp_path / "repo"
data.mkdir()
repo.mkdir()
good = tmp_path / "good.txt"
good.write_text("ok", encoding="utf-8")
captured = []
monkeypatch.setattr(queue, "enqueue_task", lambda task: captured.append(task) or task)
monkeypatch.setattr(queue, "persist_queue_snapshot", lambda reason="": True)
app = Starlette(routes=[Route("/api/tasks", endpoint=api_tasks_create, methods=["POST"])])
app.state.drive_root = data
app.state.repo_dir = repo
response = TestClient(app).post(
"/api/tasks",
json={
"task_id": "partial-attachments",
"description": "work with what arrived",
"allow_partial_attachments": True,
"attachments": [
{"path": str(good), "label": "good"},
{"path": str(tmp_path / "missing.txt"), "label": "missing"},
],
},
)
assert response.status_code == 200
manifest = response.json()["attachment_manifest"]
assert [row["status"] for row in manifest] == ["staged", "rejected"]
task = captured[0]
assert task["attachments"] == manifest
assert task["task_contract"]["attachment_manifest"] == manifest
assert "reason=source_missing" in task["text"]
result = json.loads(
(data / "task_results" / "partial-attachments.json").read_text(encoding="utf-8")
)
assert result["attachment_manifest"] == manifest
def test_late_api_identity_lookup_failure_preserves_exact_result(tmp_path):
from ouroboros.gateway.tasks import _admission_rejection_response
result_path = tmp_path / "task_results" / "api-corrupt.json"
result_path.parent.mkdir()
original = b"{api-corrupt"
result_path.write_bytes(original)
response = _admission_rejection_response(
{"_admission_blocked": "task_id_lookup_failed"},
drive_root=tmp_path, task_id="api-corrupt", project_id="",
workspace_root=None, child_drive=None,
)
assert response is not None and response.status_code == 409
assert json.loads(response.body)["admission"]["reason_code"] == "task_id_lookup_failed"
assert result_path.read_bytes() == original
def test_task_api_rejects_negative_depth_before_reservation_or_queue(tmp_path, monkeypatch):
from supervisor import queue as queue_module
repo = tmp_path / "repo"
data = tmp_path / "data"
repo.mkdir()
data.mkdir()
captured = []
monkeypatch.setattr("supervisor.queue.enqueue_task", lambda task: captured.append(task) or task)
monkeypatch.setattr("supervisor.queue.persist_queue_snapshot", lambda reason="": True)
def fail_reservation(*_args, **_kwargs):
pytest.fail("invalid depth must be rejected before admission reservation")
monkeypatch.setattr(queue_module, "reserve_task_admission", fail_reservation)
app = Starlette(routes=[Route("/api/tasks", endpoint=api_tasks_create, methods=["POST"])])
app.state.drive_root = data
app.state.repo_dir = repo
client = TestClient(app)
cases = ((-1, "depth must be a non-negative integer"),
(-0.5, "depth must be a non-negative integer"),
("-1", "depth must be a non-negative integer"),
("not-a-depth", "chat_id and depth must be integers"))
for index, (raw_depth, expected_error) in enumerate(cases):
task_id = f"api-invalid-depth-{index}"
response = client.post(
"/api/tasks", json={"task_id": task_id, "description": "x", "depth": raw_depth}
)
assert response.status_code == 400
assert response.json()["error"] == expected_error
assert task_id not in queue_module.ADMISSION_RESERVATIONS
assert not (data / "task_results" / f"{task_id}.json").exists()
assert captured == []

View file

@ -0,0 +1,654 @@
"""Child result copyback, artifact endpoints and task-drive lifecycle.
Split verbatim out of ``tests/test_headless_cli.py`` by theme. This module
owns what happens to a finished task's artifacts: copyback accounting and
acceptance markers, the artifact-serving endpoint, memory export, startup
pruning of terminal drives/scratch, and external child budget state.
"""
from __future__ import annotations
import json
import os
import subprocess
import time
from types import SimpleNamespace
from starlette.applications import Starlette
from starlette.routing import Route
from starlette.testclient import TestClient
from ouroboros.gateway.tasks import (
api_task_artifact,
)
from ouroboros.headless import (
ARTIFACT_STATUS_FINALIZING,
ARTIFACT_STATUS_READY,
ARTIFACT_STATUS_READY_WITH_CHANGES,
build_memory_export,
finalize_task_artifacts,
prune_headless_task_drives,
prune_task_drives,
task_artifacts_dir,
)
from ouroboros.task_results import write_task_result
from tests._headless_cli_shared import ( # noqa: F401 (autouse fixture applies on import)
_init_repo_with_file,
_managed_worker_pool_available,
)
def test_copy_child_result_cannot_overwrite_finalized_accounting(tmp_path):
"""F2: once the root's terminal checkpoint has finalized accounting
(task_cost_finalized rides the same write as post_task_synthesis), a late
headless-mirror copy-back may still enrich the result but the parent-owned
cost/round/token fields stay finalized (the saga displayed the $66 root-only
mirror cost instead of the $128 finalized subtree total)."""
from ouroboros.headless import copy_child_task_result
from ouroboros.task_results import STATUS_COMPLETED
parent = tmp_path / "data"
child = tmp_path / "child"
parent.mkdir()
child.mkdir()
task_id = "costfinal"
write_task_result(
parent, task_id, STATUS_COMPLETED,
result="root done",
root_phase_checkpoint={"post_task_synthesis": "completed"},
cost_usd=127.97, cost_final=True,
cost_usd_with_children=127.97, cost_with_children_partial=False,
total_rounds=200, prompt_tokens=1000, completion_tokens=500,
)
write_task_result(
child, task_id, STATUS_COMPLETED,
result="mirror done",
cost_usd=66.30, cost_final=True,
cost_usd_with_children=66.30, cost_with_children_partial=True,
total_rounds=150, prompt_tokens=700, completion_tokens=300,
mirror_only_fact="from-child",
)
merged = copy_child_task_result(parent, {"id": task_id, "drive_root": str(child)})
assert merged is not None
assert merged["cost_usd"] == 127.97
assert merged["cost_usd_with_children"] == 127.97
assert merged["cost_with_children_partial"] is False
assert merged["total_rounds"] == 200
assert merged["prompt_tokens"] == 1000
assert merged["completion_tokens"] == 500
# Non-accounting enrichment from the child mirror still lands.
assert merged["mirror_only_fact"] == "from-child"
assert merged["result"] == "mirror done"
assert merged["root_phase_checkpoint"]["post_task_synthesis"] == "completed"
def test_copy_child_result_merges_cost_before_finalization(tmp_path):
"""Before the terminal checkpoint finalizes accounting, the child mirror's
cost projection is still the freshest fact and must keep flowing."""
from ouroboros.headless import copy_child_task_result
from ouroboros.task_results import STATUS_COMPLETED
parent = tmp_path / "data"
child = tmp_path / "child"
parent.mkdir()
child.mkdir()
task_id = "costlive"
write_task_result(parent, task_id, STATUS_COMPLETED, result="root running")
write_task_result(
child, task_id, STATUS_COMPLETED,
result="mirror done", cost_usd=12.5, total_rounds=42,
)
merged = copy_child_task_result(parent, {"id": task_id, "drive_root": str(child)})
assert merged is not None
assert merged["cost_usd"] == 12.5
assert merged["total_rounds"] == 42
def test_effective_result_preserves_workspace_artifact_status_with_child_drive(tmp_path):
from ouroboros.headless import copy_child_task_result
from ouroboros.task_results import STATUS_COMPLETED
from ouroboros.task_status import load_effective_task_result
parent = tmp_path / "data"
child = tmp_path / "child"
repo = tmp_path / "repo"
parent.mkdir()
child.mkdir()
_init_repo_with_file(repo)
old_head = subprocess.run(["git", "rev-parse", "HEAD"], cwd=repo, capture_output=True, text=True, check=True).stdout.strip()
(repo / "tracked.txt").write_text("new\n", encoding="utf-8")
subprocess.run(["git", "add", "tracked.txt"], cwd=repo, check=True, capture_output=True)
subprocess.run(
["git", "-c", "user.email=t@example.com", "-c", "user.name=T", "commit", "-m", "move"],
cwd=repo,
check=True,
capture_output=True,
)
task_id = "patchfail"
write_task_result(
child,
task_id,
STATUS_COMPLETED,
result="child done",
artifact_status=ARTIFACT_STATUS_READY,
artifact_bundle={"status": ARTIFACT_STATUS_READY, "artifacts": [], "errors": []},
ts="2026-01-01T00:00:02Z",
)
ledger_path = parent / "task_results" / "artifacts" / task_id / "verification_ledger.json"
ledger_path.parent.mkdir(parents=True)
ledger_path.write_text(
json.dumps({
"schema_version": 2,
"outcome_axes": {
"artifacts": {"status": "finalizing"},
"objective": {"status": "not_evaluated", "source": "none"},
},
"entries": [{"kind": "objective_outcome", "status": "not_evaluated"}],
}),
encoding="utf-8",
)
write_task_result(
parent,
task_id,
STATUS_COMPLETED,
result="child done",
workspace_root=str(repo),
child_drive_root=str(child),
artifact_status="finalizing",
artifacts=[{"kind": "verification_ledger", "name": "verification_ledger.json", "path": str(ledger_path)}],
child_status=STATUS_COMPLETED,
)
finalize_task_artifacts(
parent,
{
"id": task_id,
"workspace_root": str(repo),
"drive_root": str(child),
"metadata": {"workspace_preflight": {"git": {"head": old_head}}},
},
)
effective = load_effective_task_result(parent, task_id)
assert effective["artifact_status"] == ARTIFACT_STATUS_READY_WITH_CHANGES
assert not effective.get("artifact_error")
assert effective["artifact_bundle"]["status"] == ARTIFACT_STATUS_READY_WITH_CHANGES
refreshed_ledger = json.loads(ledger_path.read_text(encoding="utf-8"))
assert refreshed_ledger["outcome_axes"]["artifacts"]["status"] == ARTIFACT_STATUS_READY_WITH_CHANGES
copied = copy_child_task_result(parent, {"id": task_id, "workspace_root": str(repo), "drive_root": str(child)})
assert copied is not None
assert copied["artifact_status"] == ARTIFACT_STATUS_READY_WITH_CHANGES
assert not copied.get("artifact_error")
assert copied["artifact_bundle"]["status"] == ARTIFACT_STATUS_READY_WITH_CHANGES
readonly_task_id = "readonlychild"
write_task_result(
child,
readonly_task_id,
STATUS_COMPLETED,
result="readonly handoff",
workspace_root=str(repo),
workspace_mode="external",
delegation_role="subagent",
task_constraint={"mode": "local_readonly_subagent"},
)
copied_readonly = copy_child_task_result(
parent,
{
"id": readonly_task_id,
"workspace_root": str(repo),
"drive_root": str(child),
"delegation_role": "subagent",
"task_constraint": {"mode": "local_readonly_subagent"},
},
)
assert copied_readonly is not None
assert copied_readonly.get("artifact_status", "") != "finalizing"
assert "child_status" not in copied_readonly
effective_readonly = load_effective_task_result(parent, readonly_task_id)
assert effective_readonly["status"] == STATUS_COMPLETED
assert effective_readonly["workspace_root"] == str(repo)
def test_child_copyback_preserves_acceptance_verdict_and_terminal_post_task_marker(tmp_path):
from ouroboros.headless import copy_child_task_result
from ouroboros.task_results import STATUS_COMPLETED, write_task_result
parent = tmp_path / "data"
child = tmp_path / "child"
parent.mkdir()
child.mkdir()
task_id = "root-checkpoint"
write_task_result(
child,
task_id,
STATUS_COMPLETED,
root_phase_checkpoint={
"phase": "task_acceptance",
"status": "degraded",
"pass_index": 2,
"post_task_synthesis": "pending_once",
},
)
write_task_result(
parent,
task_id,
STATUS_COMPLETED,
root_phase_checkpoint={
"phase": "task_acceptance",
"status": "not_required",
"pass_index": 0,
"post_task_synthesis": "completed",
},
)
copied = copy_child_task_result(parent, {"id": task_id, "drive_root": str(child)})
assert copied is not None
assert copied["root_phase_checkpoint"] == {
"phase": "task_acceptance",
"status": "degraded",
"pass_index": 2,
"post_task_synthesis": "completed",
}
def test_finalize_task_artifacts_preserves_existing_artifact_axis_fields(tmp_path):
from ouroboros.cli import _is_terminal_result
from ouroboros.task_results import STATUS_COMPLETED, load_task_result
parent = tmp_path / "data"
repo = tmp_path / "repo"
parent.mkdir()
_init_repo_with_file(repo)
(repo / "tracked.txt").write_text("new\n", encoding="utf-8")
task_id = "axisfields"
write_task_result(
parent,
task_id,
STATUS_COMPLETED,
workspace_root=str(repo),
artifact_status=ARTIFACT_STATUS_FINALIZING,
artifact_bundle={"schema_version": 1, "status": "pending", "artifacts": [], "errors": []},
outcome_axes={
"lifecycle": {"status": STATUS_COMPLETED},
"artifacts": {
"status": ARTIFACT_STATUS_FINALIZING,
"diagnostics": {"existing": True},
"error_count": 0,
},
"objective": {"status": "not_evaluated", "source": "none"},
},
)
finalize_task_artifacts(parent, {"id": task_id, "workspace_root": str(repo)})
result = load_task_result(parent, task_id)
artifact_axis = result["outcome_axes"]["artifacts"]
assert artifact_axis["status"] == result["artifact_bundle"]["status"]
assert result["artifact_bundle"]["status"] == result["artifact_status"]
assert result["artifact_bundle"]["status"] not in {"pending", "finalizing"}
assert _is_terminal_result(result) is True
assert artifact_axis["diagnostics"] == {"existing": True}
assert artifact_axis["error_count"] == 0
def test_effective_result_preserves_workspace_patch_kind_with_child_drive(tmp_path):
from ouroboros.artifacts import copy_file_to_task_artifacts
from ouroboros.cli import _patch_from_result
from ouroboros.task_results import STATUS_COMPLETED
from ouroboros.task_status import load_effective_task_result
parent = tmp_path / "data"
child = tmp_path / "child"
repo = tmp_path / "repo"
parent.mkdir()
child.mkdir()
_init_repo_with_file(repo)
(repo / "tracked.txt").write_text("new\n", encoding="utf-8")
task_id = "patchkind"
report = tmp_path / "report.html"
report.write_text("<h1>done</h1>", encoding="utf-8")
child_record = copy_file_to_task_artifacts(SimpleNamespace(drive_root=child, task_id=task_id), report, kind="user_file")
assert child_record is not None
write_task_result(
child,
task_id,
STATUS_COMPLETED,
result="child done",
artifacts=[child_record],
artifact_status=ARTIFACT_STATUS_READY,
ts="2026-01-01T00:00:02Z",
)
write_task_result(
parent,
task_id,
STATUS_COMPLETED,
result="child done",
workspace_root=str(repo),
child_drive_root=str(child),
artifacts=[child_record],
artifact_status="finalizing",
child_status=STATUS_COMPLETED,
)
finalize_task_artifacts(parent, {"id": task_id, "workspace_root": str(repo), "drive_root": str(child)})
effective = load_effective_task_result(parent, task_id)
patch_artifacts = [
item
for item in effective.get("artifacts") or []
if isinstance(item, dict) and item.get("name") == "workspace.patch"
]
assert patch_artifacts
assert patch_artifacts[0]["kind"] == "workspace_patch"
assert any(item.get("kind") == "user_file" for item in effective.get("artifacts") or [] if isinstance(item, dict))
class FakeClient:
def __init__(self):
self.paths = []
def get_bytes(self, path):
self.paths.append(path)
return b"diff --git a/tracked.txt b/tracked.txt\n"
client = FakeClient()
assert _patch_from_result(client, task_id, effective, strict=True).startswith("diff --git")
assert client.paths == [f"/api/tasks/{task_id}/artifacts/workspace.patch"]
def test_task_artifact_endpoint_serves_only_declared_artifacts(tmp_path):
data = tmp_path / "data"
artifact_dir = task_artifacts_dir(data, "task-artifact")
patch_path = artifact_dir / "workspace.patch"
patch_path.write_text("diff --git a/a b/a\n", encoding="utf-8")
write_task_result(
data,
"task-artifact",
"completed",
artifacts=[{"kind": "workspace_patch", "name": "workspace.patch", "path": str(patch_path), "size": patch_path.stat().st_size}],
artifact_status="ready",
)
app = Starlette(routes=[Route("/api/tasks/{task_id}/artifacts/{name}", endpoint=api_task_artifact, methods=["GET"])])
app.state.drive_root = data
client = TestClient(app)
assert client.get("/api/tasks/task-artifact/artifacts/workspace.patch").text.startswith("diff --git")
assert client.get("/api/tasks/task-artifact/artifacts/missing.patch").status_code == 404
assert client.get("/api/tasks/task-artifact/artifacts/bad%5Cname").status_code == 400
def test_task_artifact_endpoint_serves_manifest_artifact_after_status_repair(tmp_path):
from ouroboros.artifacts import copy_file_to_task_artifacts
data = tmp_path / "data"
source_dir = tmp_path / "Desktop"
source_dir.mkdir()
source = source_dir / "report.html"
source.write_text("<h1>ok</h1>", encoding="utf-8")
copy_file_to_task_artifacts(SimpleNamespace(drive_root=data, task_id="orphaned"), source, kind="user_file")
write_task_result(
data,
"orphaned",
"running",
result_status="infra_failed",
reason_code="provider_failure",
result="provider failed before normal finalization",
)
(data / "state").mkdir(parents=True, exist_ok=True)
(data / "state" / "queue_snapshot.json").write_text('{"pending": [], "running": []}', encoding="utf-8")
app = Starlette(routes=[Route("/api/tasks/{task_id}/artifacts/{name}", endpoint=api_task_artifact, methods=["GET"])])
app.state.drive_root = data
response = TestClient(app).get("/api/tasks/orphaned/artifacts/report.html")
assert response.status_code == 200
assert response.text == "<h1>ok</h1>"
def test_task_artifact_endpoint_rebases_child_drive_artifact_after_status_repair(tmp_path):
from ouroboros.artifacts import collect_task_artifact_records, copy_file_to_task_artifacts
data = tmp_path / "data"
child = tmp_path / "child"
source_dir = tmp_path / "Desktop"
source_dir.mkdir()
source = source_dir / "report.html"
source.write_text("<h1>child</h1>", encoding="utf-8")
copy_file_to_task_artifacts(SimpleNamespace(drive_root=child, task_id="childart"), source, kind="user_file")
child_artifacts = collect_task_artifact_records(child, "childart")
write_task_result(
child,
"childart",
"completed",
result="done",
artifacts=child_artifacts,
artifact_status="ready",
ts="2026-01-01T00:00:02Z",
)
write_task_result(
data,
"childart",
"running",
child_drive_root=str(child),
workspace_root=str(tmp_path / "workspace"),
result_status="infra_failed",
reason_code="provider_failure",
result="provider failed before normal finalization",
)
(data / "state").mkdir(parents=True, exist_ok=True)
(data / "state" / "queue_snapshot.json").write_text('{"pending": [], "running": []}', encoding="utf-8")
app = Starlette(routes=[Route("/api/tasks/{task_id}/artifacts/{name}", endpoint=api_task_artifact, methods=["GET"])])
app.state.drive_root = data
response = TestClient(app).get("/api/tasks/childart/artifacts/report.html")
parent_artifact = task_artifacts_dir(data, "childart", create=False) / "report.html"
assert response.status_code == 200
assert response.text == "<h1>child</h1>"
assert parent_artifact.read_text(encoding="utf-8") == "<h1>child</h1>"
def test_task_artifact_endpoint_rejects_metadata_name_path_mismatch(tmp_path):
data = tmp_path / "data"
artifact_dir = task_artifacts_dir(data, "task-artifact")
wrong_path = artifact_dir / "memory_export.json"
wrong_path.write_text("{}", encoding="utf-8")
write_task_result(
data,
"task-artifact",
"completed",
artifacts=[{"kind": "workspace_patch", "name": "workspace.patch", "path": str(wrong_path), "size": wrong_path.stat().st_size}],
artifact_status="ready",
)
app = Starlette(routes=[Route("/api/tasks/{task_id}/artifacts/{name}", endpoint=api_task_artifact, methods=["GET"])])
app.state.drive_root = data
assert TestClient(app).get("/api/tasks/task-artifact/artifacts/workspace.patch").status_code == 500
def test_memory_export_includes_nested_memory_files(tmp_path):
drive = tmp_path / "child"
memory = drive / "memory"
nested = memory / "knowledge" / "patterns"
nested.mkdir(parents=True)
(memory / "identity.md").write_text("id\n", encoding="utf-8")
(nested / "cli.md").write_text("pattern\n", encoding="utf-8")
export = build_memory_export(drive, {"id": "task-1", "memory_mode": "forked"})
assert export["files"]["identity.md"] == "id\n"
assert export["files"]["knowledge/patterns/cli.md"] == "pattern\n"
def test_startup_prune_removes_only_old_terminal_child_drives(tmp_path):
data = tmp_path / "data"
terminal_dir = data / "state" / "headless_tasks" / "oldterminal"
pending_dir = data / "state" / "headless_tasks" / "oldpending"
fresh_timestamp_dir = data / "state" / "headless_tasks" / "freshresult"
terminal_drive = terminal_dir / "data"
pending_drive = pending_dir / "data"
fresh_timestamp_drive = fresh_timestamp_dir / "data"
terminal_drive.mkdir(parents=True)
pending_drive.mkdir(parents=True)
fresh_timestamp_drive.mkdir(parents=True)
now = time.time()
old = now - (8 * 86400)
old_iso = time.strftime("%Y-%m-%dT%H:%M:%S+00:00", time.gmtime(old))
fresh_iso = time.strftime("%Y-%m-%dT%H:%M:%S+00:00", time.gmtime(now))
write_task_result(data, "oldterminal", "completed", child_drive_root=str(terminal_drive), artifact_status="ready", result="done", ts=old_iso)
write_task_result(data, "oldpending", "scheduled", child_drive_root=str(pending_drive), result="queued")
write_task_result(data, "freshresult", "completed", child_drive_root=str(fresh_timestamp_drive), artifact_status="ready", result="done", ts=fresh_iso)
os.utime(terminal_dir, (old, old))
os.utime(pending_dir, (old, old))
os.utime(fresh_timestamp_dir, (old, old))
report = prune_headless_task_drives(data, retention_days=7, now=now)
assert [item["task_id"] for item in report["pruned"]] == ["oldterminal"]
assert not terminal_dir.exists()
assert pending_dir.exists()
assert fresh_timestamp_dir.exists()
assert any(item["task_id"] == "oldpending" and item["reason"] == "parent_not_terminal" for item in report["skipped"])
assert any(item["task_id"] == "freshresult" and item["reason"] == "younger_than_retention" for item in report["skipped"])
def test_startup_prune_uses_effective_terminal_status(tmp_path):
data = tmp_path / "data"
task_drive = data / "task_drives" / "stalerun"
child_dir = data / "state" / "headless_tasks" / "stalechild"
child_drive = child_dir / "data"
task_drive.mkdir(parents=True)
child_drive.mkdir(parents=True)
(task_drive / "scratch.txt").write_text("scratch", encoding="utf-8")
(child_drive / "scratch.txt").write_text("child", encoding="utf-8")
(data / "state").mkdir(parents=True, exist_ok=True)
(data / "state" / "queue_snapshot.json").write_text('{"pending": [], "running": []}', encoding="utf-8")
now = time.time()
old = now - (8 * 86400)
old_iso = time.strftime("%Y-%m-%dT%H:%M:%S+00:00", time.gmtime(old))
for task_id, extra in (
("stalerun", {}),
("stalechild", {"child_drive_root": str(child_drive)}),
):
write_task_result(
data,
task_id,
"running",
result_status="infra_failed",
reason_code="provider_failure",
result="provider failed",
ts=old_iso,
**extra,
)
os.utime(task_drive, (old, old))
os.utime(child_dir, (old, old))
direct_report = prune_task_drives(data, retention_days=7, now=now)
child_report = prune_headless_task_drives(data, retention_days=7, now=now)
assert [item["task_id"] for item in direct_report["pruned"]] == ["stalerun"]
assert [item["task_id"] for item in child_report["pruned"]] == ["stalechild"]
assert not task_drive.exists()
assert not child_dir.exists()
def test_startup_prune_removes_only_old_terminal_task_scratch(tmp_path):
data = tmp_path / "data"
old_terminal = data / "task_drives" / "oldterminal"
old_pending = data / "task_drives" / "oldpending"
fresh_terminal = data / "task_drives" / "freshterminal"
for path in (old_terminal, old_pending, fresh_terminal):
path.mkdir(parents=True)
(path / "scratch.txt").write_text("scratch", encoding="utf-8")
now = time.time()
old = now - (8 * 86400)
old_iso = time.strftime("%Y-%m-%dT%H:%M:%S+00:00", time.gmtime(old))
fresh_iso = time.strftime("%Y-%m-%dT%H:%M:%S+00:00", time.gmtime(now))
write_task_result(data, "oldterminal", "completed", result="done", ts=old_iso)
write_task_result(data, "oldpending", "running", result="running")
write_task_result(data, "freshterminal", "completed", result="done", ts=fresh_iso)
os.utime(old_terminal, (old, old))
os.utime(old_pending, (old, old))
os.utime(fresh_terminal, (old, old))
report = prune_task_drives(data, retention_days=7, now=now)
assert [item["task_id"] for item in report["pruned"]] == ["oldterminal"]
assert not old_terminal.exists()
assert old_pending.exists()
assert fresh_terminal.exists()
assert any(item["task_id"] == "oldpending" and item["reason"] == "task_not_terminal" for item in report["skipped"])
assert any(item["task_id"] == "freshterminal" and item["reason"] == "younger_than_retention" for item in report["skipped"])
def test_external_child_task_budget_uses_parent_drive_state(tmp_path, monkeypatch):
from ouroboros import usage_accounting
from ouroboros.agent import Env, OuroborosAgent
repo = tmp_path / "repo"
parent = tmp_path / "parent-data"
child = tmp_path / "child-data"
for root in (repo, parent, child):
root.mkdir()
for drive in (parent, child):
(drive / "state").mkdir()
(drive / "logs").mkdir()
# Compatibility projections are deliberately misleading here: the physical-attempt
# ledger in the parent budget root is the sole monetary authority.
(parent / "state" / "state.json").write_text('{"spent_usd": 0.0}\n', encoding="utf-8")
(child / "state" / "state.json").write_text('{"spent_usd": 0.0}\n', encoding="utf-8")
reservation = usage_accounting.reserve_attempt(usage_accounting.AttemptRequest(
model="test/model",
provider="test",
reservation_usd=9.0,
drive_root=parent,
task_id="prior-task",
root_task_id="prior-task",
source="test",
))
usage_accounting.mark_dispatched(reservation)
usage_accounting.settle_attempt(reservation, {}, cost_usd=9.0, cost_final=True)
monkeypatch.setenv("TOTAL_BUDGET", "10")
monkeypatch.setattr(OuroborosAgent, "_log_worker_boot_once", lambda self: None)
monkeypatch.setattr("ouroboros.agent.build_llm_messages", lambda **kwargs: ([], {}))
agent = OuroborosAgent(Env(repo_dir=repo, drive_root=child))
ctx, _messages, cap_info = agent._prepare_task_context({
"id": "budget-task",
"type": "task",
"text": "x",
"budget_drive_root": str(parent),
})
assert cap_info["budget_remaining"] == 1.0
assert ctx.task_metadata["budget_drive_root"] == str(parent)
def test_task_artifact_endpoint_serves_exact_chat_media_without_task_result(tmp_path):
from ouroboros.artifacts import collect_task_artifact_records, store_chat_media_bytes
data = tmp_path / "data"
stored = store_chat_media_bytes(data, "ephemeral1", b"photo-bytes", "image/png")
assert stored is not None
app = Starlette(routes=[Route("/api/tasks/{task_id}/artifacts/{name}", endpoint=api_task_artifact, methods=["GET"])])
app.state.drive_root = data
client = TestClient(app)
response = client.get(f"/api/tasks/ephemeral1/artifacts/{stored['name']}")
assert response.status_code == 200
assert response.content == b"photo-bytes"
assert collect_task_artifact_records(data, "ephemeral1") == []
assert client.get("/api/tasks/ephemeral1/artifacts/chat-media-bad.png").status_code == 404

View file

@ -0,0 +1,326 @@
"""Task event replay, log tails and effective child status projection.
Split verbatim out of ``tests/test_headless_cli.py`` by theme. This module
owns what readers see after a task runs: event replay, lineage-filtered log
tails, SSE finalization order, task listing, and the effective-status/result
projection that waits for workspace artifacts.
"""
from __future__ import annotations
import json
import pytest
from starlette.applications import Starlette
from starlette.routing import Route
from starlette.testclient import TestClient
from ouroboros.gateway.tasks import (
api_task_events,
api_task_get,
api_tasks_list,
iter_task_events,
)
from ouroboros.task_results import write_task_result
from tests._headless_cli_shared import ( # noqa: F401 (autouse fixture applies on import)
_managed_worker_pool_available,
)
def test_task_event_replay_uses_existing_logs_and_result(tmp_path):
data = tmp_path / "data"
logs = data / "logs"
logs.mkdir(parents=True)
task_id = "abc123"
(logs / "progress.jsonl").write_text(
json.dumps({"ts": "2026-01-01T00:00:00Z", "task_id": task_id, "content": "working"}) + "\n",
encoding="utf-8",
)
result_dir = data / "task_results"
result_dir.mkdir()
(result_dir / f"{task_id}.json").write_text(
json.dumps({"task_id": task_id, "status": "completed", "result": "done", "ts": "2026-01-01T00:00:01Z"}),
encoding="utf-8",
)
events = iter_task_events(data, task_id)
assert [event["type"] for event in events] == ["progress", "task_result"]
assert events[0]["seq"] == 1
assert events[1]["data"]["result"] == "done"
def test_task_event_replay_parent_includes_child_lineage_events(tmp_path):
data = tmp_path / "data"
logs = data / "logs"
logs.mkdir(parents=True)
parent_id = "parent1"
child_id = "child1"
(logs / "progress.jsonl").write_text(
"\n".join([
json.dumps({"ts": "2026-01-01T00:00:00Z", "task_id": parent_id, "content": "parent"}),
json.dumps({
"ts": "2026-01-01T00:00:01Z",
"task_id": child_id,
"parent_task_id": parent_id,
"root_task_id": parent_id,
"delegation_role": "subagent",
"subagent_task_id": child_id,
"content": "child progress",
}),
]) + "\n",
encoding="utf-8",
)
write_task_result(
data,
parent_id,
"running",
result="parent pending",
ts="2026-01-01T00:00:00Z",
)
write_task_result(
data,
child_id,
"running",
result="child pending",
parent_task_id=parent_id,
root_task_id=parent_id,
delegation_role="subagent",
ts="2026-01-01T00:00:01Z",
)
events = iter_task_events(data, parent_id)
progress_events = [event for event in events if event["type"] == "progress"]
assert [event["task_id"] for event in progress_events] == [parent_id, child_id]
assert progress_events[1]["data"]["content"] == "child progress"
def test_logs_tail_parent_filter_includes_child_lineage_events(tmp_path):
from ouroboros.gateway.logs import api_logs_tail
data = tmp_path / "data"
logs = data / "logs"
logs.mkdir(parents=True)
(logs / "progress.jsonl").write_text(
"\n".join([
json.dumps({"ts": "2026-01-01T00:00:00Z", "task_id": "parent1", "content": "parent"}),
json.dumps({
"ts": "2026-01-01T00:00:01Z",
"task_id": "child1",
"subagent_task_id": "child1",
"parent_task_id": "parent1",
"root_task_id": "parent1",
"delegation_role": "subagent",
"content": "child",
}),
json.dumps({"ts": "2026-01-01T00:00:02Z", "task_id": "other", "content": "other"}),
]) + "\n",
encoding="utf-8",
)
app = Starlette(routes=[Route("/api/logs/{name}", endpoint=api_logs_tail, methods=["GET"])])
app.state.drive_root = data
response = TestClient(app).get("/api/logs/progress?task_id=parent1&limit=10")
payload = response.json()
assert response.status_code == 200
assert [row["content"] for row in payload["entries"]] == ["parent", "child"]
def test_workspace_event_replay_suppresses_task_done_until_artifacts_terminal(tmp_path):
data = tmp_path / "data"
logs = data / "logs"
logs.mkdir(parents=True)
task_id = "abc123"
(logs / "events.jsonl").write_text(
json.dumps({"ts": "2026-01-01T00:00:01Z", "type": "task_done", "task_id": task_id}) + "\n",
encoding="utf-8",
)
write_task_result(
data,
task_id,
"completed",
workspace_root=str(tmp_path / "workspace"),
artifact_status="finalizing",
child_status="completed",
)
events = iter_task_events(data, task_id)
assert "task_done" not in [event["type"] for event in events]
assert events[-1]["type"] == "task_result"
def test_effective_child_completion_waits_for_artifacts(tmp_path):
data = tmp_path / "data"
child = tmp_path / "child"
for root in (data, child):
(root / "task_results").mkdir(parents=True)
write_task_result(
data,
"task-artifacts",
"scheduled",
child_drive_root=str(child),
workspace_root=str(tmp_path / "workspace"),
artifact_status="pending",
result="queued",
)
write_task_result(
child,
"task-artifacts",
"completed",
result="done",
ts="2026-01-01T00:00:02Z",
outcome_axes={
"lifecycle": {"status": "completed"},
"artifacts": {"status": "not_applicable"},
},
)
app = Starlette(routes=[Route("/api/tasks/{task_id}", endpoint=api_task_get, methods=["GET"])])
app.state.drive_root = data
payload = TestClient(app).get("/api/tasks/task-artifacts").json()
assert payload["status"] == "running"
assert payload["artifact_status"] == "finalizing"
assert payload["child_status"] == "completed"
assert payload["outcome_axes"]["lifecycle"]["status"] == "running"
assert payload["outcome_axes"]["artifacts"]["status"] == "finalizing"
write_task_result(data, "task-artifacts", "completed", artifact_status="ready", child_drive_root=str(child), workspace_root=str(tmp_path / "workspace"))
payload = TestClient(app).get("/api/tasks/task-artifacts").json()
assert payload["status"] == "completed"
assert payload["artifact_status"] == "ready"
def test_public_task_result_strips_nested_legacy_result_status(tmp_path):
data = tmp_path / "data"
(data / "task_results").mkdir(parents=True)
write_task_result(
data,
"legacy-loop",
"completed",
result="done",
loop_outcome={"result_status": "failed", "compat_result_status": "failed", "reason_code": "legacy"},
verification_ledger={
"entries": [
{"kind": "legacy", "result_status": "partial"},
{"kind": "nested", "payload": {"compat_result_status": "infra_failed"}},
{"kind": "list", "items": [{"result_status": "failed"}]},
],
},
)
app = Starlette(routes=[Route("/api/tasks/{task_id}", endpoint=api_task_get, methods=["GET"])])
app.state.drive_root = data
payload = TestClient(app).get("/api/tasks/legacy-loop").json()
assert "result_status" not in payload
assert "result_status" not in payload["loop_outcome"]
assert "compat_result_status" not in payload["loop_outcome"]
rendered = json.dumps(payload)
assert "result_status" not in rendered
assert "compat_result_status" not in rendered
def test_effective_child_failure_waits_for_artifacts(tmp_path):
data = tmp_path / "data"
child = tmp_path / "child"
for root in (data, child):
(root / "task_results").mkdir(parents=True)
write_task_result(
data,
"task-failed",
"failed",
child_drive_root=str(child),
workspace_root=str(tmp_path / "workspace"),
artifact_status="finalizing",
child_status="failed",
result="boom",
)
write_task_result(child, "task-failed", "failed", result="boom", ts="2026-01-01T00:00:02Z")
app = Starlette(routes=[Route("/api/tasks/{task_id}", endpoint=api_task_get, methods=["GET"])])
app.state.drive_root = data
payload = TestClient(app).get("/api/tasks/task-failed").json()
assert payload["status"] == "running"
assert payload["artifact_status"] == "finalizing"
assert payload["child_status"] == "failed"
def test_task_sse_emits_final_result_after_cursor_saw_scheduled_result(tmp_path):
data = tmp_path / "data"
(data / "task_results").mkdir(parents=True)
task_id = "abc123"
(data / "task_results" / f"{task_id}.json").write_text(
json.dumps({"task_id": task_id, "status": "completed", "result": "done", "ts": "2026-01-01T00:00:01Z"}),
encoding="utf-8",
)
app = Starlette(routes=[Route("/api/tasks/{task_id}/events", endpoint=api_task_events, methods=["GET"])])
app.state.drive_root = data
response = TestClient(app).get(f"/api/tasks/{task_id}/events?cursor=1&wait=0")
assert response.status_code == 200
assert '"type": "task_result"' in response.text
assert '"status": "completed"' in response.text
def test_task_list_filters_on_effective_child_status(tmp_path):
data = tmp_path / "data"
child_running = tmp_path / "child-running"
child_done = tmp_path / "child-done"
for root in (data, child_running, child_done):
(root / "task_results").mkdir(parents=True)
write_task_result(data, "task-running", "scheduled", child_drive_root=str(child_running), result="queued")
write_task_result(child_running, "task-running", "running", result="working", ts="2026-01-01T00:00:01Z")
write_task_result(data, "task-done", "scheduled", child_drive_root=str(child_done), result="queued")
write_task_result(child_done, "task-done", "completed", result="done", ts="2026-01-01T00:00:02Z")
app = Starlette(routes=[Route("/api/tasks", endpoint=api_tasks_list, methods=["GET"])])
app.state.drive_root = data
client = TestClient(app)
running = client.get("/api/tasks?status=running").json()["tasks"]
completed = client.get("/api/tasks?status=completed").json()["tasks"]
assert [task["task_id"] for task in running] == ["task-running"]
assert running[0]["result"] == "working"
assert [task["task_id"] for task in completed] == ["task-done"]
assert completed[0]["result"] == "done"
@pytest.mark.parametrize("status", ["cancelled", "failed"])
def test_effective_task_result_preserves_parent_terminal_status(tmp_path, status):
data = tmp_path / "data"
child = tmp_path / "child"
for root in (data, child):
(root / "task_results").mkdir(parents=True)
write_task_result(
data,
"task-terminal",
status,
child_drive_root=str(child),
result="parent terminal",
ts="2026-01-01T00:00:02Z",
)
write_task_result(
child,
"task-terminal",
"running",
result="child stale",
ts="2026-01-01T00:00:03Z",
)
app = Starlette(routes=[Route("/api/tasks/{task_id}", endpoint=api_task_get, methods=["GET"])])
app.state.drive_root = data
payload = TestClient(app).get("/api/tasks/task-terminal").json()
assert payload["status"] == status
assert payload["result"] == "parent terminal"
assert payload["ts"] == "2026-01-01T00:00:02Z"

View file

@ -0,0 +1,412 @@
"""Workspace patch capture and finalization.
Split verbatim out of ``tests/test_headless_cli.py`` by theme. This module
owns ``build_workspace_patch``/``write_workspace_patch_artifacts``: which
files a patch carries, the unborn/invalid HEAD cases, the sensitive-file
vetoes, and the acting-base-sha manifest contract.
"""
from __future__ import annotations
import json
import subprocess
import pytest
from ouroboros.headless import (
ARTIFACT_STATUS_FAILED,
ARTIFACT_STATUS_READY_WITH_CHANGES,
_incidental_lockfile_excludes,
build_workspace_patch,
finalize_task_artifacts,
task_artifacts_dir,
write_workspace_patch_artifacts,
)
from ouroboros.task_results import write_task_result
from tests._headless_cli_shared import ( # noqa: F401 (autouse fixture applies on import)
_init_repo_with_file,
_managed_worker_pool_available,
)
def test_workspace_patch_includes_tracked_and_untracked_files(tmp_path):
repo = tmp_path / "repo"
repo.mkdir()
subprocess.run(["git", "init"], cwd=repo, check=True, capture_output=True)
(repo / "tracked.txt").write_text("old\n", encoding="utf-8")
subprocess.run(["git", "add", "tracked.txt"], cwd=repo, check=True, capture_output=True)
subprocess.run(
["git", "-c", "user.email=t@example.com", "-c", "user.name=T", "commit", "-m", "init"],
cwd=repo,
check=True,
capture_output=True,
)
(repo / "tracked.txt").write_text("new\n", encoding="utf-8")
(repo / "new.txt").write_text("hello\n", encoding="utf-8")
patch = build_workspace_patch(repo)
assert "diff --git a/tracked.txt b/tracked.txt" in patch
assert "+new" in patch
assert "diff --git" in patch and "new.txt" in patch
def test_workspace_patch_lockfile_without_manifest_is_incidental_only_with_code_changes():
assert _incidental_lockfile_excludes(["package-lock.json"]) == set()
assert _incidental_lockfile_excludes(["package-lock.json", "package.json", "app.js"]) == set()
assert _incidental_lockfile_excludes(["package-lock.json", "app.js"]) == {"package-lock.json"}
assert _incidental_lockfile_excludes(["pkg/poetry.lock", "pkg/module.py"]) == {"pkg/poetry.lock"}
def test_workspace_patch_preserves_lockfile_when_other_changes_are_junk(tmp_path):
repo = tmp_path / "repo"
repo.mkdir()
subprocess.run(["git", "init"], cwd=repo, check=True, capture_output=True)
(repo / "README.md").write_text("base\n", encoding="utf-8")
subprocess.run(["git", "add", "README.md"], cwd=repo, check=True, capture_output=True)
subprocess.run(
["git", "-c", "user.email=t@example.com", "-c", "user.name=T", "commit", "-m", "init"],
cwd=repo,
check=True,
capture_output=True,
)
(repo / "package-lock.json").write_text('{"lockfileVersion": 3}\n', encoding="utf-8")
(repo / "dist").mkdir()
(repo / "dist" / "out.txt").write_text("junk\n", encoding="utf-8")
_artifacts, manifest = write_workspace_patch_artifacts(repo, tmp_path / "artifacts", task={})
patch = (tmp_path / "artifacts" / "workspace.patch").read_text(encoding="utf-8")
assert "package-lock.json" in patch
assert "dist/out.txt" not in patch
assert manifest["counts"]["untracked_included"] == 1
assert manifest["counts"]["untracked_excluded"] == 1
def test_workspace_patch_excludes_binary_junk_and_oversize(tmp_path, monkeypatch):
"""T7 (v6.35.0): the real-usage workspace patch drops untracked build/runtime
binaries, junk artifacts, and oversize blobs (recorded, not silently lost),
while keeping real source additions."""
import ouroboros.workspace_patch_capture as workspace_patch_capture
repo = tmp_path / "repo"
repo.mkdir()
subprocess.run(["git", "init"], cwd=repo, check=True, capture_output=True)
(repo / "seed.txt").write_text("seed\n", encoding="utf-8")
subprocess.run(["git", "add", "seed.txt"], cwd=repo, check=True, capture_output=True)
subprocess.run(
["git", "-c", "user.email=t@example.com", "-c", "user.name=T", "commit", "-m", "init"],
cwd=repo, check=True, capture_output=True,
)
# Untracked additions: a real source file (keep), a compiled binary (drop),
# a redis dump + log junk (drop), and an oversize text file (drop).
(repo / "fix.py").write_text("def fixed():\n return 1\n", encoding="utf-8")
(repo / "app").write_bytes(b"\x7fELF\x00\x01\x02\x03binary\x00blob") # compiled binary
(repo / "dump.rdb").write_bytes(b"REDIS\x00\x01")
(repo / "run.log").write_text("noise\n", encoding="utf-8")
(repo / "htmlcov").mkdir()
(repo / "htmlcov" / "index.html").write_text("<html></html>\n", encoding="utf-8") # top-level coverage junk
monkeypatch.setattr(workspace_patch_capture, "_PATCH_MAX_UNTRACKED_FILE_BYTES", 100)
(repo / "big.txt").write_text("x" * 200, encoding="utf-8") # 200 bytes > cap; small files pass size
artifacts, manifest = write_workspace_patch_artifacts(repo, tmp_path / "artifacts", task={})
assert manifest["exclude_rules_version"] == 2
excluded = {item["path"]: item["reason"] for item in manifest["untracked_excluded"]}
assert "binary file" in excluded.get("app", "")
assert "binary file" in excluded.get("dump.rdb", "") or "junk artifact" in excluded.get("dump.rdb", "")
assert "junk artifact" in excluded.get("run.log", "")
assert "junk artifact" in excluded.get("htmlcov/index.html", "") # top-level htmlcov excluded
assert "size cap" in excluded.get("big.txt", "")
assert "fix.py" in manifest["untracked_included"]
patch = (tmp_path / "artifacts" / "workspace.patch").read_text(encoding="utf-8")
assert "fix.py" in patch
assert "diff --git a/app b/app" not in patch
assert "dump.rdb" not in patch
assert "run.log" not in patch
assert "big.txt" not in patch
def test_workspace_patch_supports_unborn_git_worktree(tmp_path):
repo = tmp_path / "repo"
repo.mkdir()
subprocess.run(["git", "init"], cwd=repo, check=True, capture_output=True)
(repo / "created.txt").write_text("hello\n", encoding="utf-8")
artifacts, manifest = write_workspace_patch_artifacts(repo, tmp_path / "artifacts", task={})
assert manifest["status"] == ARTIFACT_STATUS_READY_WITH_CHANGES
assert manifest["base_is_empty_tree"] is True
assert manifest["base_head"] == "(unborn)"
assert manifest["current_head"] == "(unborn)"
assert any(item["kind"] == "workspace_patch" for item in artifacts)
patch = (tmp_path / "artifacts" / "workspace.patch").read_text(encoding="utf-8")
assert "created.txt" in patch
assert "+hello" in patch
head = subprocess.run(["git", "rev-parse", "--verify", "HEAD"], cwd=repo, capture_output=True)
assert head.returncode != 0
def test_workspace_patch_supports_unborn_sha256_git_worktree(tmp_path):
repo = tmp_path / "repo"
repo.mkdir()
init = subprocess.run(["git", "init", "--object-format=sha256"], cwd=repo, capture_output=True)
if init.returncode != 0:
pytest.skip("git does not support sha256 object-format")
(repo / "created.txt").write_text("hello\n", encoding="utf-8")
artifacts, manifest = write_workspace_patch_artifacts(repo, tmp_path / "artifacts", task={})
assert manifest["status"] == ARTIFACT_STATUS_READY_WITH_CHANGES
assert manifest["base_is_empty_tree"] is True
assert len(manifest["base_ref"]) == 64
assert any(item["kind"] == "workspace_patch" for item in artifacts)
def test_workspace_patch_allows_external_workspace_first_commit(tmp_path):
repo = tmp_path / "repo"
repo.mkdir()
subprocess.run(["git", "init"], cwd=repo, check=True, capture_output=True)
(repo / "created.txt").write_text("hello\n", encoding="utf-8")
subprocess.run(["git", "add", "created.txt"], cwd=repo, check=True, capture_output=True)
subprocess.run(
["git", "-c", "user.email=t@example.com", "-c", "user.name=T", "commit", "-m", "first"],
cwd=repo,
check=True,
capture_output=True,
)
task = {"metadata": {"workspace_preflight": {"git": {"head": ""}}}}
artifacts, manifest = write_workspace_patch_artifacts(repo, tmp_path / "artifacts", task=task)
assert manifest["status"] == ARTIFACT_STATUS_READY_WITH_CHANGES
assert manifest["errors"] == []
assert any(item["kind"] == "workspace_patch" for item in artifacts)
def test_workspace_patch_fails_on_invalid_head_not_unborn(tmp_path):
repo = tmp_path / "repo"
_init_repo_with_file(repo)
head_ref = subprocess.run(["git", "symbolic-ref", "--quiet", "HEAD"], cwd=repo, capture_output=True, text=True, check=True).stdout.strip()
ref_path = repo / ".git" / head_ref
ref_path.unlink()
artifacts, manifest = write_workspace_patch_artifacts(repo, tmp_path / "artifacts", task={})
assert manifest["status"] == ARTIFACT_STATUS_FAILED
assert any(error["type"] == "git_invalid_head" for error in manifest["errors"])
assert not any(item["kind"] == "workspace_patch" for item in artifacts)
def test_workspace_patch_manifest_excludes_env_cache_dirs(tmp_path):
repo = tmp_path / "repo"
_init_repo_with_file(repo)
(repo / "new.txt").write_text("hello\n", encoding="utf-8")
(repo / "node_modules" / "pkg").mkdir(parents=True)
(repo / "node_modules" / "pkg" / "index.js").write_text("generated\n", encoding="utf-8")
artifact_dir = tmp_path / "artifacts"
artifacts, manifest = write_workspace_patch_artifacts(repo, artifact_dir, task={})
assert manifest["status"] == "ready_with_changes"
assert "new.txt" in (artifact_dir / "workspace.patch").read_text(encoding="utf-8")
assert "node_modules" not in (artifact_dir / "workspace.patch").read_text(encoding="utf-8")
assert manifest["counts"]["untracked_excluded"] == 1
assert any(item["kind"] == "workspace_patch_manifest" for item in artifacts)
def test_workspace_patch_fails_on_sensitive_untracked_file(tmp_path):
repo = tmp_path / "repo"
_init_repo_with_file(repo)
(repo / ".npmrc").write_text("//registry.npmjs.org/:_authToken=secret\n", encoding="utf-8")
artifacts, manifest = write_workspace_patch_artifacts(repo, tmp_path / "artifacts", task={})
assert manifest["status"] == ARTIFACT_STATUS_FAILED
assert manifest["errors"][0]["type"] == "sensitive_untracked_files"
assert manifest["sensitive_blocked"][0]["path"] == ".npmrc"
assert not any(item["kind"] == "workspace_patch" for item in artifacts)
def test_workspace_patch_fails_on_sensitive_untracked_file_inside_excluded_dir(tmp_path):
repo = tmp_path / "repo"
_init_repo_with_file(repo)
secret = repo / "node_modules" / "pkg" / "service-account.json"
secret.parent.mkdir(parents=True)
secret.write_text("TOKEN=secret\n", encoding="utf-8")
artifacts, manifest = write_workspace_patch_artifacts(repo, tmp_path / "artifacts", task={})
assert manifest["status"] == ARTIFACT_STATUS_FAILED
assert manifest["counts"]["sensitive_blocked"] == 1
assert manifest["sensitive_blocked"][0]["path"] == "node_modules/pkg/service-account.json"
assert not any(item["kind"] == "workspace_patch" for item in artifacts)
def test_workspace_patch_fails_on_common_credential_paths(tmp_path):
repo = tmp_path / "repo"
_init_repo_with_file(repo)
(repo / "credentials").write_text("secret\n", encoding="utf-8")
(repo / "prod.env").write_text("SECRET=1\n", encoding="utf-8")
(repo / "settings.env.local").write_text("SECRET=1\n", encoding="utf-8")
(repo / ".aws").mkdir()
(repo / ".aws" / "credentials").write_text("secret\n", encoding="utf-8")
artifacts, manifest = write_workspace_patch_artifacts(repo, tmp_path / "artifacts", task={})
assert manifest["status"] == ARTIFACT_STATUS_FAILED
assert {item["path"] for item in manifest["sensitive_blocked"]} == {
"credentials",
"prod.env",
"settings.env.local",
".aws/credentials",
}
assert not any(item["kind"] == "workspace_patch" for item in artifacts)
def test_workspace_patch_allows_benign_tokenizer_json(tmp_path):
repo = tmp_path / "repo"
_init_repo_with_file(repo)
(repo / "tokenizer.json").write_text("{}\n", encoding="utf-8")
artifacts, manifest = write_workspace_patch_artifacts(repo, tmp_path / "artifacts", task={})
assert manifest["status"] == ARTIFACT_STATUS_READY_WITH_CHANGES
assert manifest["sensitive_blocked"] == []
assert any(item["kind"] == "workspace_patch" for item in artifacts)
def test_failed_refinalization_drops_stale_workspace_patch_metadata(tmp_path):
parent = tmp_path / "data"
repo = tmp_path / "repo"
parent.mkdir()
_init_repo_with_file(repo)
(repo / "tracked.txt").write_text("new\n", encoding="utf-8")
task = {"id": "task-stale", "workspace_root": str(repo)}
write_task_result(parent, "task-stale", "completed", workspace_root=str(repo), artifact_status="finalizing")
finalize_task_artifacts(parent, task)
result = json.loads((parent / "task_results" / "task-stale.json").read_text(encoding="utf-8"))
assert any(item.get("kind") == "workspace_patch" for item in result["artifacts"])
(repo / ".env").write_text("TOKEN=secret\n", encoding="utf-8")
finalize_task_artifacts(parent, task)
result = json.loads((parent / "task_results" / "task-stale.json").read_text(encoding="utf-8"))
assert result["artifact_status"] == ARTIFACT_STATUS_FAILED
assert not any(item.get("kind") == "workspace_patch" for item in result["artifacts"])
def test_workspace_patch_preserves_untracked_paths_with_whitespace(tmp_path):
repo = tmp_path / "repo"
_init_repo_with_file(repo)
leading = repo / " leading.txt"
nested = repo / "dir with space" / "file name.txt"
leading.write_text("leading\n", encoding="utf-8")
nested.parent.mkdir()
nested.write_text("nested\n", encoding="utf-8")
_artifacts, manifest = write_workspace_patch_artifacts(repo, tmp_path / "artifacts", task={})
assert manifest["status"] == "ready_with_changes"
assert " leading.txt" in manifest["untracked_included"]
assert "dir with space/file name.txt" in manifest["untracked_included"]
assert manifest["patch_size"] > 0
def test_finalize_workspace_patch_allows_external_workspace_head_changed(tmp_path):
parent = tmp_path / "data"
repo = tmp_path / "repo"
parent.mkdir()
_init_repo_with_file(repo)
old_head = subprocess.run(["git", "rev-parse", "HEAD"], cwd=repo, capture_output=True, text=True, check=True).stdout.strip()
(repo / "tracked.txt").write_text("new\n", encoding="utf-8")
subprocess.run(["git", "add", "tracked.txt"], cwd=repo, check=True, capture_output=True)
subprocess.run(["git", "-c", "user.email=t@example.com", "-c", "user.name=T", "commit", "-m", "move"], cwd=repo, check=True, capture_output=True)
task = {
"id": "task-head",
"workspace_root": str(repo),
"metadata": {"workspace_preflight": {"git": {"head": old_head}}},
}
write_task_result(parent, "task-head", "completed", workspace_root=str(repo), artifact_status="finalizing")
finalize_task_artifacts(parent, task)
result = json.loads((parent / "task_results" / "task-head.json").read_text(encoding="utf-8"))
assert result["artifact_status"] == ARTIFACT_STATUS_READY_WITH_CHANGES
manifest = json.loads((task_artifacts_dir(parent, "task-head") / "workspace_patch.json").read_text(encoding="utf-8"))
assert manifest["errors"] == []
def test_finalize_workspace_patch_exception_manifest_keeps_base_fields(tmp_path, monkeypatch):
import ouroboros.headless as headless
parent = tmp_path / "data"
repo = tmp_path / "repo"
parent.mkdir()
_init_repo_with_file(repo)
task = {"id": "task-exception", "workspace_root": str(repo)}
write_task_result(parent, "task-exception", "completed", workspace_root=str(repo), artifact_status="finalizing")
def boom(*_args, **_kwargs):
raise RuntimeError("artifact failure")
monkeypatch.setattr(headless, "write_workspace_patch_artifacts", boom)
headless.finalize_task_artifacts(parent, task)
result = json.loads((parent / "task_results" / "task-exception.json").read_text(encoding="utf-8"))
manifest = json.loads((task_artifacts_dir(parent, "task-exception") / "workspace_patch.json").read_text(encoding="utf-8"))
assert result["artifact_status"] == ARTIFACT_STATUS_FAILED
assert manifest["status"] == ARTIFACT_STATUS_FAILED
assert manifest["base_ref"] == ""
assert manifest["base_head"] == ""
assert manifest["base_is_empty_tree"] is False
assert manifest["current_head"] == ""
def test_workspace_patch_uses_acting_base_sha_without_preflight_metadata(tmp_path):
repo = tmp_path / "repo"
_init_repo_with_file(repo)
base_head = subprocess.run(["git", "rev-parse", "HEAD"], cwd=repo, capture_output=True, text=True, check=True).stdout.strip()
(repo / "tracked.txt").write_text("acting edit\n", encoding="utf-8")
task = {
"task_constraint": {
"mode": "acting_subagent",
"surface": "self_worktree",
"base_sha": base_head,
},
}
artifacts, manifest = write_workspace_patch_artifacts(repo, tmp_path / "artifacts", task=task)
assert manifest["status"] == "ready_with_changes"
assert manifest["base_ref"] == base_head
assert manifest["base_head"] == base_head
assert manifest["current_head"] == base_head
assert any(item["kind"] == "workspace_patch" for item in artifacts)
def test_workspace_patch_fails_when_acting_base_sha_head_changed(tmp_path):
repo = tmp_path / "repo"
_init_repo_with_file(repo)
base_head = subprocess.run(["git", "rev-parse", "HEAD"], cwd=repo, capture_output=True, text=True, check=True).stdout.strip()
(repo / "tracked.txt").write_text("committed by child\n", encoding="utf-8")
subprocess.run(["git", "add", "tracked.txt"], cwd=repo, check=True, capture_output=True)
subprocess.run(["git", "-c", "user.email=t@example.com", "-c", "user.name=T", "commit", "-m", "child commit"], cwd=repo, check=True, capture_output=True)
moved_head = subprocess.run(["git", "rev-parse", "HEAD"], cwd=repo, capture_output=True, text=True, check=True).stdout.strip()
task = {
"task_constraint": {
"mode": "acting_subagent",
"surface": "self_worktree",
"base_sha": base_head,
},
}
artifacts, manifest = write_workspace_patch_artifacts(repo, tmp_path / "artifacts", task=task)
assert manifest["status"] == ARTIFACT_STATUS_FAILED
assert manifest["base_ref"] == base_head
assert manifest["errors"][-1]["type"] == "workspace_head_changed"
assert manifest["errors"][-1]["expected_head"] == base_head
assert manifest["errors"][-1]["current_head"] == moved_head
assert not any(item["kind"] == "workspace_patch" for item in artifacts)

View file

@ -0,0 +1,483 @@
"""Workspace tool context and run_shell routing/safety in headless tasks.
Split verbatim out of ``tests/test_headless_cli.py`` by theme. This module
owns where a workspace task may read, write and execute: project-file
routing, allowed shell cwds, redirect and symlink-escape guards, task-local
git, and the preflight inference of binaries from manifests.
"""
from __future__ import annotations
import pathlib
import sys
import pytest
from ouroboros.tools.core import _repo_read
from ouroboros.tools.registry import ToolContext, ToolRegistry
from ouroboros.workspace_preflight import _infer_tools_from_manifests
from tests._headless_cli_shared import ( # noqa: F401 (autouse fixture applies on import)
_init_repo_with_file,
_managed_worker_pool_available,
)
def test_workspace_context_routes_project_files_and_keeps_system_tools_reachable(tmp_path):
system_repo = tmp_path / "system"
workspace = tmp_path / "workspace"
data = tmp_path / "data"
system_repo.mkdir()
workspace.mkdir()
data.mkdir()
(system_repo / "README.md").write_text("system", encoding="utf-8")
(workspace / "README.md").write_text("workspace", encoding="utf-8")
(workspace / "BIBLE.md").write_text("external bible", encoding="utf-8")
ctx = ToolContext(
repo_dir=system_repo,
drive_root=data,
workspace_root=workspace,
workspace_mode="external",
)
assert "workspace" in _repo_read(ctx, "README.md")
registry = ToolRegistry(repo_dir=system_repo, drive_root=data)
registry.set_context(ctx)
commit_result = registry.execute("commit_reviewed", {"commit_message": "nope"})
assert "WORKSPACE_MODE_BLOCKED" not in commit_result
assert registry.get_schema_by_name("commit_reviewed") is not None
assert registry.get_schema_by_name("request_restart") is not None
assert "Written" in registry.execute("write_file", {"path": "BIBLE.md", "content": "external edit"})
assert (workspace / "BIBLE.md").read_text(encoding="utf-8") == "external edit"
replaced = registry.execute(
"edit_text",
{"path": "README.md", "old_str": "workspace", "new_str": "workspace edited"},
)
assert "Replaced" in replaced
assert (workspace / "README.md").read_text(encoding="utf-8") == "workspace edited"
def test_workspace_run_shell_cwd_allows_scratch_and_explicit_system(tmp_path, monkeypatch):
"""External-workspace tasks may run from host scratch (a sibling checkout, a
/tmp tree) and explicitly select the system repo; generic runtime data stays
off-limits and system-repo mutation remains independently governed."""
monkeypatch.setenv("OUROBOROS_RUNTIME_MODE", "advanced")
# Pin $HOME outside tmp_path so the host-scratch cwd allowance holds on Windows
# CI too (where pytest's tmp dir lives UNDER home and the data-parent-under-home
# protection would otherwise block the sibling scratch cwd). See the same fixture
# in test_external_workspace_access.py.
fake_home = tmp_path / "_home"
fake_home.mkdir()
monkeypatch.setattr(pathlib.Path, "home", lambda: fake_home)
system_repo = tmp_path / "system"
workspace = tmp_path / "workspace"
outside = tmp_path / "outside"
data = tmp_path / "data"
for path in (system_repo, workspace, outside, data):
path.mkdir()
ctx = ToolContext(
repo_dir=system_repo,
drive_root=data,
workspace_root=workspace,
workspace_mode="external",
)
registry = ToolRegistry(repo_dir=system_repo, drive_root=data)
registry.set_context(ctx)
# Host scratch outside the declared workspace is now a legitimate cwd...
scratch_cwd = registry.execute("run_command", {"cmd": ["pwd"], "cwd": str(outside)})
assert "SHELL_CWD_BLOCKED" not in scratch_cwd
# The approved root contract makes system_repo an explicit cwd; generic
# runtime_data remains unavailable to process tools.
runtime_repo_cwd = registry.execute("run_command", {"cmd": ["pwd"], "cwd": str(system_repo)})
assert "SHELL_CWD_BLOCKED" not in runtime_repo_cwd
assert f"cwd={system_repo.resolve()}" in runtime_repo_cwd
runtime_data_cwd = registry.execute("run_command", {"cmd": ["pwd"], "cwd": str(data)})
assert "SHELL_CWD_BLOCKED" in runtime_data_cwd
# READ-ONLY git at a runtime target is ALLOWED (owner contract "read-only
# everywhere"; the f14baf8f false-block class). Only MUTATING git is target-checked.
git_read = registry._run_shell_safety_check(
{"cmd": ["git", "-C", str(system_repo), "status"]}, "advanced"
)
assert git_read is None, git_read
git_escape = registry._run_shell_safety_check(
{"cmd": ["git", "-C", str(system_repo), "commit", "-m", "x"]}, "advanced"
)
assert git_escape and "WORKSPACE_GIT_BLOCKED" in git_escape
git_chain = registry.execute("run_command", {"cmd": ["sh", "-c", "true && git --version; echo git binary OK"]})
assert "WORKSPACE_GIT_BLOCKED" not in git_chain
outside_write = registry.execute("run_command", {"cmd": ["touch", str(system_repo / "README.md")]})
assert "WORKSPACE_SHELL_BLOCKED" in outside_write
embedded_outside_write = registry.execute(
"run_command",
{"cmd": ["python", "-c", "open('/tmp/ouroboros-outside.txt','w').write('x')"]},
)
assert "WORKSPACE_SHELL_BLOCKED" in embedded_outside_write
def test_workspace_shell_safe_stdio_redirects_are_not_write_like(tmp_path, monkeypatch):
monkeypatch.setenv("OUROBOROS_RUNTIME_MODE", "advanced")
system_repo = tmp_path / "system"
workspace = tmp_path / "workspace"
outside = tmp_path / "outside"
data = tmp_path / "data"
for path in (system_repo, workspace, outside, data):
path.mkdir()
(outside / "visible.txt").write_text("ok\n", encoding="utf-8")
ctx = ToolContext(repo_dir=system_repo, drive_root=data, workspace_root=workspace, workspace_mode="external")
registry = ToolRegistry(repo_dir=system_repo, drive_root=data)
registry.set_context(ctx)
stderr_sink = registry.execute("run_command", {"cmd": f"find {outside} -maxdepth 1 2>/dev/null"})
fd_dup = registry.execute("run_command", {"cmd": f"ls {outside} 2>&1 | head -n 1"})
fd_close = registry.execute("run_command", {"cmd": f"find {outside} -maxdepth 1 2>&-"})
real_redirect = registry.execute("run_command", {"cmd": f"echo x > {outside / 'out.txt'}"})
assert "WORKSPACE_SHELL_BLOCKED" not in stderr_sink, stderr_sink
assert "WORKSPACE_SHELL_BLOCKED" not in fd_dup, fd_dup
assert "WORKSPACE_SHELL_BLOCKED" not in fd_close, fd_close
assert "WORKSPACE_SHELL_BLOCKED" in real_redirect
def test_workspace_shell_blocks_windows_absolute_redirects_before_shell_execution(tmp_path, monkeypatch):
monkeypatch.setenv("OUROBOROS_RUNTIME_MODE", "advanced")
system_repo = tmp_path / "system"
workspace = tmp_path / "workspace"
data = tmp_path / "data"
for path in (system_repo, workspace, data):
path.mkdir()
ctx = ToolContext(repo_dir=system_repo, drive_root=data, workspace_root=workspace, workspace_mode="external")
registry = ToolRegistry(repo_dir=system_repo, drive_root=data)
registry.set_context(ctx)
drive_redirect = registry.execute("run_command", {"cmd": r"echo x > C:\ouroboros-outside\out.txt"})
unc_redirect = registry.execute("run_command", {"cmd": r"echo x > \\server\share\out.txt"})
assert "WORKSPACE_SHELL_BLOCKED" in drive_redirect
assert "SHELL_SYNTAX_UNSUPPORTED" not in drive_redirect
assert "WORKSPACE_SHELL_BLOCKED" in unc_redirect
assert "SHELL_SYNTAX_UNSUPPORTED" not in unc_redirect
def test_workspace_shell_keeps_symlinked_workspace_absolute_paths_allowed(tmp_path, monkeypatch):
monkeypatch.setenv("OUROBOROS_RUNTIME_MODE", "advanced")
system_repo = tmp_path / "system"
real_workspace = tmp_path / "real_workspace"
workspace_link = tmp_path / "workspace_link"
data = tmp_path / "data"
for path in (system_repo, real_workspace, data):
path.mkdir()
try:
workspace_link.symlink_to(real_workspace, target_is_directory=True)
except OSError as exc:
pytest.skip(f"symlink unavailable on this platform: {exc}")
ctx = ToolContext(repo_dir=system_repo, drive_root=data, workspace_root=workspace_link, workspace_mode="external")
registry = ToolRegistry(repo_dir=system_repo, drive_root=data)
registry.set_context(ctx)
target = workspace_link / "inside.txt"
result = registry.execute("run_command", {"cmd": [sys.executable, "-c", f"open({str(target)!r}, 'w').write('ok')"]})
assert "WORKSPACE_SHELL_BLOCKED" not in result, result
assert (real_workspace / "inside.txt").exists()
def test_workspace_shell_blocks_nested_symlink_escape_absolute_path(tmp_path, monkeypatch):
monkeypatch.setenv("OUROBOROS_RUNTIME_MODE", "advanced")
system_repo = tmp_path / "system"
workspace = tmp_path / "workspace"
outside = tmp_path / "outside"
data = tmp_path / "data"
for path in (system_repo, workspace, outside, data):
path.mkdir()
outlink = workspace / "outlink"
outside_file = outside / "target.txt"
outside_file.write_text("old\n", encoding="utf-8")
filelink = workspace / "filelink"
executable_name_link = workspace / "touch"
try:
outlink.symlink_to(outside, target_is_directory=True)
filelink.symlink_to(outside_file)
executable_name_link.symlink_to(outside_file)
except OSError as exc:
pytest.skip(f"symlink unavailable on this platform: {exc}")
ctx = ToolContext(repo_dir=system_repo, drive_root=data, workspace_root=workspace, workspace_mode="external")
registry = ToolRegistry(repo_dir=system_repo, drive_root=data)
registry.set_context(ctx)
result = registry.execute("run_command", {"cmd": f"touch {outlink / 'escaped.txt'}"})
relative_result = registry.execute("run_command", {"cmd": "touch outlink/escaped-relative.txt"})
bare_result = registry.execute("run_command", {"cmd": "touch outlink"})
executable_name_result = registry.execute("run_command", {"cmd": ["touch", "touch"]})
redirect_result = registry.execute("run_command", {"cmd": "echo changed > filelink"})
compact_redirect_result = registry.execute("run_command", {"cmd": "echo changed >filelink"})
shell_inline_result = registry.execute("run_command", {"cmd": ["sh", "-c", "echo changed > filelink"]})
shell_inline_touch_result = registry.execute("run_command", {"cmd": ["sh", "-c", "touch filelink"]})
bash_redirect_result = registry.execute("run_command", {"cmd": ["bash", "-c", "echo changed &> filelink"]})
compact_bash_redirect_result = registry.execute("run_command", {"cmd": ["bash", "-c", "echo changed &>filelink"]})
tee_result = registry.execute("run_command", {"cmd": "printf changed | tee filelink"})
shell_inline_tee_result = registry.execute("run_command", {"cmd": ["sh", "-c", "printf changed | tee filelink"]})
python_inline_result = registry.execute(
"run_command",
{"cmd": [sys.executable, "-c", "open('filelink', 'w').write('changed')"]},
)
python_versioned_result = registry.execute(
"run_command",
{"cmd": ["python3.12", "-c", "open('filelink', 'w').write('changed')"]},
)
node_script_result = registry.execute(
"run_script",
{
"interpreter": "node",
"script": "require('fs').writeFileSync('filelink', 'changed')",
},
)
assert "WORKSPACE_SHELL_BLOCKED" in result
assert "WORKSPACE_SHELL_BLOCKED" in relative_result
assert "WORKSPACE_SHELL_BLOCKED" in bare_result
assert "WORKSPACE_SHELL_BLOCKED" in executable_name_result
assert "WORKSPACE_SHELL_BLOCKED" in redirect_result
assert "WORKSPACE_SHELL_BLOCKED" in compact_redirect_result
assert "WORKSPACE_SHELL_BLOCKED" in shell_inline_result
assert "WORKSPACE_SHELL_BLOCKED" in shell_inline_touch_result
assert "WORKSPACE_SHELL_BLOCKED" in bash_redirect_result
assert "WORKSPACE_SHELL_BLOCKED" in compact_bash_redirect_result
assert "WORKSPACE_SHELL_BLOCKED" in tee_result
assert "WORKSPACE_SHELL_BLOCKED" in shell_inline_tee_result
assert "WORKSPACE_SHELL_BLOCKED" in python_inline_result
assert "WORKSPACE_SHELL_BLOCKED" in python_versioned_result
assert "WORKSPACE_SHELL_BLOCKED" in node_script_result
assert not (outside / "escaped.txt").exists()
assert not (outside / "escaped-relative.txt").exists()
assert outside_file.read_text(encoding="utf-8") == "old\n"
def test_external_workspace_shell_allows_task_local_git(tmp_path, monkeypatch):
system_repo = tmp_path / "system"
workspace = tmp_path / "workspace"
data = tmp_path / "data"
system_repo.mkdir()
data.mkdir()
_init_repo_with_file(workspace)
ctx = ToolContext(repo_dir=system_repo, drive_root=data, workspace_root=workspace, workspace_mode="external")
registry = ToolRegistry(repo_dir=system_repo, drive_root=data)
registry.set_context(ctx)
monkeypatch.setenv("OUROBOROS_TEST_RUNTIME_REPO", str(system_repo))
allowed = [
["git", "for-each-ref", "--format=%(refname)"],
["git", "rev-list", "--count", "HEAD"],
["git", "show-ref", "--heads"],
["git", "branch", "--show-current"],
["git", "branch", "--list"],
["git", "branch", "--list", "ma*"],
["git", "branch", "-av"],
["git", "tag", "-l"],
["git", "tag", "--list", "v*"],
["git", "branch", "new-branch"],
["git", "branch", "-v", "new-branch"],
["git", "branch", "--verbose", "new-branch"],
["git", "branch", "-d", "main"],
["git", "tag", "v1"],
["git", "tag", "-a", "v1", "-m", "x"],
["git", "commit", "--allow-empty", "-m", "task-local commit"],
["sh", "-c", "git --version; echo git binary OK"],
]
for cmd in allowed:
assert registry._run_shell_safety_check({"cmd": cmd}, "advanced") is None, cmd
# READ-ONLY git reaches the runtime through EVERY retarget vector — that is the
# owner contract ("read-only everywhere, including at a runtime target") and the
# recorded false-block class f14baf8f. Before the Q4=A composition these four
# were refused: the target-aware resolver let them through and the
# external-workspace runtime-READ guard then blocked them as
# WORKSPACE_SHELL_BLOCKED, naming the wrong reason.
for cmd in (
["git", "-C", str(system_repo), "status"],
["git", "--git-dir", str(system_repo / ".git"), "status"],
# as_posix(): a POSIX shell (sh -c) uses forward slashes; a Windows
# backslash literal would be eaten as shell escapes during parsing.
["sh", "-c", f"cd {system_repo.as_posix()} && git status"],
["sh", "-c", "git -C $OUROBOROS_TEST_RUNTIME_REPO status"],
):
result = registry._run_shell_safety_check({"cmd": cmd}, "advanced")
assert result is None, (cmd, result)
# ...while the MUTATING form of each vector stays blocked.
for cmd in (
["git", "-C", str(system_repo), "commit", "-m", "x"],
["git", "--git-dir", str(system_repo / ".git"), "commit", "-m", "x"],
["sh", "-c", f"cd {system_repo.as_posix()} && git commit -m x"],
["sh", "-c", "git -C $OUROBOROS_TEST_RUNTIME_REPO commit -m x"],
):
result = registry._run_shell_safety_check({"cmd": cmd}, "advanced")
assert result and "WORKSPACE_GIT_BLOCKED" in result, (cmd, result)
# The read-only exemption is ALL-or-NOTHING per segment: a compound that only
# STARTS with git still meets the runtime/secret read guard in full.
mixed = registry._run_shell_safety_check(
{"cmd": ["sh", "-c", f"git status && cat {(data / 'settings.json').as_posix()}"]},
"advanced",
)
assert mixed and "WORKSPACE_SHELL_BLOCKED" in mixed, mixed
def test_workspace_shell_git_ls_remote_requires_network_contract(tmp_path):
system_repo = tmp_path / "system"
workspace = tmp_path / "workspace"
data = tmp_path / "data"
system_repo.mkdir()
data.mkdir()
_init_repo_with_file(workspace)
contract = {
"allowed_resources": {"network": False},
"resource_policy": {},
}
ctx = ToolContext(
repo_dir=system_repo,
drive_root=data,
workspace_root=workspace,
workspace_mode="external",
task_contract=contract,
task_metadata={"task_contract": contract},
)
registry = ToolRegistry(repo_dir=system_repo, drive_root=data)
registry.set_context(ctx)
for cmd in (
["git", "ls-remote", "origin"],
["git", "submodule", "update", "--init", "--recursive"],
):
result = registry._run_shell_safety_check({"cmd": cmd}, "advanced")
assert result and "RESOURCE_CONSTRAINT_BLOCKED" in result, (cmd, result)
def test_workspace_run_shell_allows_absolute_cwd_under_workspace_and_child_drive(tmp_path):
system_repo = tmp_path / "system"
workspace = tmp_path / "workspace"
parent_data = tmp_path / "data"
parent_task_dir = parent_data / "task_drives" / "task-workspace" / "scratch"
child_drive = tmp_path / "child-data"
child_dir = child_drive / "task_drives" / "task-workspace" / "scratch"
child_control_dir = child_drive / "memory"
for path in (system_repo, workspace, parent_data / "logs", parent_task_dir, child_dir, child_control_dir):
path.mkdir(parents=True)
ctx = ToolContext(
repo_dir=system_repo,
drive_root=parent_data,
workspace_root=workspace,
workspace_mode="external",
task_id="task-workspace",
task_metadata={"drive_root": str(child_drive), "budget_drive_root": str(parent_data)},
)
registry = ToolRegistry(repo_dir=system_repo, drive_root=parent_data)
registry.set_context(ctx)
def assert_python_cwd(path):
output = registry.execute(
"run_command",
{"cmd": [sys.executable, "-c", "import os; print(os.getcwd())"], "cwd": str(path)},
)
assert "exit_code=0" in output
cwd_output = output.rsplit("STDOUT:\n", 1)[-1].strip()
assert pathlib.Path(cwd_output).resolve() == path.resolve()
assert_python_cwd(workspace)
assert_python_cwd(child_dir)
child_control = registry.execute("run_command", {"cmd": ["pwd"], "cwd": str(child_control_dir)})
assert "SHELL_CWD_BLOCKED" in child_control
blocked = registry.execute("run_command", {"cmd": ["pwd"], "cwd": str(parent_data / "logs")})
assert "SHELL_CWD_BLOCKED" in blocked
# Read-only git is allowed everywhere now; the escape check uses a MUTATING form.
git_read = registry._run_shell_safety_check(
{"cmd": ["git", "-C", "../other-repo", "status"], "cwd": str(child_dir)},
"advanced",
)
assert git_read is None, git_read
git_escape = registry._run_shell_safety_check(
{"cmd": ["git", "-C", "..", "commit", "-m", "x"], "cwd": str(child_dir)},
"advanced",
)
assert git_escape and "WORKSPACE_GIT_BLOCKED" in git_escape, git_escape
protected_escape = registry._run_shell_safety_check(
{"cmd": ["touch", "../data/state/state.json"]},
"pro",
)
assert "WORKSPACE_SHELL_BLOCKED" in protected_escape
task_drive_write = registry.execute("run_command", {"cmd": ["touch", "output.txt"], "cwd": str(child_dir)})
assert "WORKSPACE_SHELL_BLOCKED" not in task_drive_write
assert (child_dir / "output.txt").is_file()
parent_task_drive_write = registry.execute("run_command", {"cmd": ["touch", "output.txt"], "cwd": str(parent_task_dir)})
assert "WORKSPACE_SHELL_BLOCKED" not in parent_task_drive_write
assert (parent_task_dir / "output.txt").is_file()
absolute_task_drive_file = parent_task_dir / "absolute-python.txt"
absolute_task_drive_write = registry.execute(
"run_command",
{"cmd": [sys.executable, "-c", f"open({str(absolute_task_drive_file)!r}, 'w').write('ok')"]},
)
assert "WORKSPACE_SHELL_BLOCKED" not in absolute_task_drive_write
assert absolute_task_drive_file.read_text(encoding="utf-8") == "ok"
def test_workspace_shell_allows_nested_relative_write_paths(tmp_path):
system_repo = tmp_path / "system"
workspace = tmp_path / "workspace"
data = tmp_path / "data"
for path in (system_repo, workspace, data):
path.mkdir(parents=True)
ctx = ToolContext(repo_dir=system_repo, drive_root=data, workspace_root=workspace, workspace_mode="external")
registry = ToolRegistry(repo_dir=system_repo, drive_root=data)
registry.set_context(ctx)
assert registry._run_shell_safety_check({"cmd": ["touch", "subdir/file.txt"]}, "advanced") is None
assert registry._run_shell_safety_check({"cmd": ["mkdir", "-p", "build/output"]}, "advanced") is None
python_write = {"cmd": [sys.executable, "-c", "open('subdir/python.txt', 'w').write('ok')"]}
assert registry._run_shell_safety_check(python_write, "advanced") is None
def test_workspace_shell_sudo_and_pro_passthrough_policy(tmp_path):
system_repo = tmp_path / "system"
workspace = tmp_path / "workspace"
data = tmp_path / "data"
for path in (system_repo, workspace, data):
path.mkdir()
ctx = ToolContext(repo_dir=system_repo, drive_root=data, workspace_root=workspace, workspace_mode="external")
registry = ToolRegistry(repo_dir=system_repo, drive_root=data)
registry.set_context(ctx)
assert "SUDO_INTERACTIVE_BLOCKED" in registry._run_shell_safety_check({"cmd": ["sudo", "true"]}, "pro")
assert "SUDO_INTERACTIVE_BLOCKED" in registry._run_shell_safety_check({"cmd": ["sh", "-c", "sudo true"]}, "pro")
assert "SUDO_INTERACTIVE_BLOCKED" in registry._run_shell_safety_check({"cmd": ["sudo", "-S", "true"]}, "pro")
assert "SUDO_INTERACTIVE_BLOCKED" in registry._run_shell_safety_check({"cmd": ["sudo", "-nS", "true"]}, "pro")
assert "SUDO_INTERACTIVE_BLOCKED" in registry._run_shell_safety_check({"cmd": ["sudoedit", "/etc/hosts"]}, "pro")
assert registry._run_shell_safety_check({"cmd": ["sudo", "-n", "python", "-S", "-c", "print(1)"]}, "pro") is None
assert "SAFETY_VIOLATION" in registry._run_shell_safety_check({"cmd": ["sh", "-c", "gh\nrepo\ncreate x"]}, "pro")
assert "SAFETY_VIOLATION" in registry._run_shell_safety_check({"cmd": ["sh", "-c", "gh\nauth\nlogin"]}, "pro")
outside_write = {"cmd": ["python", "-c", "open('/tmp/ouroboros-pro.txt','w').write('x')"]}
assert "WORKSPACE_SHELL_BLOCKED" in registry._run_shell_safety_check(outside_write, "advanced")
assert registry._run_shell_safety_check(outside_write, "pro") is None
def test_workspace_preflight_infers_binaries_from_script_commands():
tools = _infer_tools_from_manifests([
{
"type": "node",
"scripts": ["test"],
"script_commands": {"test": "vitest --run"},
}
])
assert "vitest" in tools
assert "test" not in tools
noisy = _infer_tools_from_manifests([
{
"type": "node",
"scripts": ["build"],
"script_commands": {"build": "NODE_ENV=production cd web && vite build"},
}
])
assert "NODE_ENV=production" not in noisy
assert "cd" not in noisy
assert "vite" in noisy

View file

@ -46,7 +46,7 @@ _POPEN_ALLOWLIST = {
"ouroboros/packaged_cli.py", # user-facing CLI wrapper (foreground)
"ouroboros/cli.py", # dev CLI (foreground)
"ouroboros/server_control.py", # restart exec path
"ouroboros/headless.py", # waited synchronous child
"ouroboros/workspace_patch_capture.py", # waited synchronous child
"ouroboros/preflight_runner.py", # waited hermetic pytest child
"ouroboros/tools/shell.py", # bounded foreground commands (waited + tracked)
"ouroboros/tools/skill_exec.py", # bounded skill run (waited + tracked)

View file

@ -0,0 +1,155 @@
"""``_store_task_result`` persistence semantics.
Split out of ``tests/test_agent_task_pipeline.py`` when that module was divided
by theme; every moved block is verbatim. Covers review-evidence persistence,
the compact review projection (no raw model text), failed-status preservation,
and the unresolved-vs-recovered tool-failure outcome axes.
"""
import json
from types import SimpleNamespace
import ouroboros.agent_task_pipeline as pipeline
def test_store_task_result_persists_review_evidence(tmp_path):
env = SimpleNamespace(drive_root=tmp_path)
pipeline._store_task_result(
env=env,
task={"id": "task-store", "type": "task", "text": "hi"},
text="done",
usage={"rounds": 2, "cost": 0.1},
llm_trace={"tool_calls": [], "reasoning_notes": []},
review_evidence={"has_evidence": True, "open_obligations": [{"item": "tests_affected"}]},
)
payload = json.loads((tmp_path / "task_results" / "task-store.json").read_text(encoding="utf-8"))
assert payload["review_evidence"]["has_evidence"] is True
assert payload["review_evidence"]["open_obligations"][0]["item"] == "tests_affected"
def test_store_task_result_persists_only_compact_review_projection(tmp_path):
env = SimpleNamespace(drive_root=tmp_path)
trace = {
"tool_calls": [],
"review_runs": [{
"request": {"surface": "task_acceptance", "policy": {"min_successful_slots": 1}},
"authority": "host_root",
"aggregate_signal": "DEGRADED",
"actors": [{
"slot_id": "slot_1", "model": "openai/gpt-5.6-sol", "status": "ok",
"parsed": {"verdict": "DEGRADED", "summary": "not enough evidence"},
"signal": "DEGRADED", "raw_text": "PRIVATE RAW MODEL RESPONSE",
}],
}],
}
pipeline._store_task_result(
env=env,
task={"id": "task-review-projection", "type": "task", "text": "hi"},
text="done",
usage={"rounds": 1, "cost": 0.0},
llm_trace=trace,
review_evidence={},
)
payload = json.loads(
(tmp_path / "task_results" / "task-review-projection.json").read_text(encoding="utf-8")
)
actor = payload["review_projection"]["panels"][0]["actors"][0]
assert actor["model"] == "openai/gpt-5.6-sol"
assert actor["parse_status"] == "valid"
assert actor["semantic_verdict"] == "DEGRADED"
assert "raw_text" not in actor
assert "PRIVATE RAW MODEL RESPONSE" not in json.dumps(payload)
def test_store_task_result_preserves_failed_status(tmp_path):
from ouroboros.task_results import STATUS_FAILED, write_task_result
env = SimpleNamespace(drive_root=tmp_path)
write_task_result(tmp_path, "task-failed", STATUS_FAILED, result="initial failure")
pipeline._store_task_result(
env=env,
task={"id": "task-failed", "type": "task", "text": "hi"},
text="final failure reply",
usage={"rounds": 1, "cost": 0.0},
llm_trace={"tool_calls": [], "reasoning_notes": []},
review_evidence={},
)
payload = json.loads((tmp_path / "task_results" / "task-failed.json").read_text(encoding="utf-8"))
assert payload["status"] == STATUS_FAILED
assert payload["result"] == "final failure reply"
def test_store_task_result_marks_unresolved_tool_failure_failed(tmp_path):
from ouroboros.task_results import STATUS_COMPLETED
env = SimpleNamespace(drive_root=tmp_path)
pipeline._store_task_result(
env=env,
task={"id": "task-tool-failed", "type": "task", "text": "make file"},
text="Created the file.",
usage={"rounds": 2, "cost": 0.0},
llm_trace={
"tool_calls": [{
"tool": "run_command",
"args": {"cmd": "python3 -c ..."},
"result": "⚠️ ARTIFACT_OUTPUT_ERROR: command succeeded but declared output registration failed.",
"is_error": True,
"status": "artifact_output_error",
}],
"reasoning_notes": [],
},
review_evidence={},
)
payload = json.loads((tmp_path / "task_results" / "task-tool-failed.json").read_text(encoding="utf-8"))
assert payload["status"] == STATUS_COMPLETED
assert payload["outcome_axes"]["execution"]["status"] == "degraded"
assert payload["outcome_axes"]["objective"]["status"] == "not_evaluated"
assert payload["reason_code"] == "tool_failure"
assert payload["loop_outcome"]["failure"]["tool_errors"][0]["status"] == "artifact_output_error"
def test_store_task_result_allows_recovered_tool_failure_success(tmp_path):
from ouroboros.task_results import STATUS_COMPLETED
env = SimpleNamespace(drive_root=tmp_path)
pipeline._store_task_result(
env=env,
task={"id": "task-tool-recovered", "type": "task", "text": "make file"},
text="Created the file.",
usage={"rounds": 3, "cost": 0.0},
llm_trace={
"tool_calls": [
{
"tool": "edit_text",
"args": {"path": "Desktop/report.html"},
"result": "⚠️ EDIT_TEXT_ERROR: old_str matched 0 times",
"is_error": True,
"status": "edit_text_blocked",
},
{
"tool": "write_file",
"args": {"root": "user_files", "path": "Desktop/report.html"},
"result": "OK: wrote user_files:Desktop/report.html\nARTIFACT_OUTPUTS: registered user file -> artifact_store:report.html",
"is_error": False,
"status": "ok",
"artifact_registered": True,
},
],
"reasoning_notes": [],
},
review_evidence={},
)
payload = json.loads((tmp_path / "task_results" / "task-tool-recovered.json").read_text(encoding="utf-8"))
assert payload["status"] == STATUS_COMPLETED
assert payload["outcome_axes"]["execution"]["status"] == "ok"
assert payload["outcome_axes"]["objective"]["status"] == "not_evaluated"
assert payload["loop_outcome"]["failure"] is None

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,383 @@
"""What ``POST /api/tasks`` accepts as an executor reference.
Split verbatim out of ``tests/test_workspace_executor.py`` by theme. This module owns
the normalized reference it admits and every refusal around it: no external workspace,
an empty or malformed reference or mapping entry, a mapping onto the system repo or the
data drive, a mapping that does not cover the workspace, the reserved metadata aliases,
and ``network=none`` asked of the local backend.
Whole-file serial suite: it spawns real processes, so ``tests/conftest.py`` tags it
``serial`` and the parallel pass excludes it.
"""
from __future__ import annotations
import json
import asyncio
from types import SimpleNamespace
from tests._workspace_executor_shared import _init_repo
def test_api_task_metadata_accepts_normalized_executor_ref(tmp_path, monkeypatch):
from ouroboros.gateway import tasks
import supervisor.queue as queue
import supervisor.workers as workers
captured: dict[str, object] = {}
async def fake_request_json_or(_request, _default):
return {
"description": "x",
"workspace_root": str(tmp_path / "workspace"),
"workspace_mode": "external",
"memory_mode": "empty",
"executor_ref": {
"type": "local",
"id": "local-api",
"workspace_host_path": str(tmp_path / "workspace"),
"workspace_backend_path": "/workspace",
},
}
def fake_enqueue(task):
captured.update(task)
return task
_init_repo(tmp_path / "workspace")
(tmp_path / "data").mkdir()
monkeypatch.setattr(tasks, "request_json_or", fake_request_json_or)
monkeypatch.setattr(tasks, "request_drive_root", lambda _request: tmp_path / "data")
monkeypatch.setattr(tasks, "request_repo_dir", lambda _request: tmp_path / "repo")
monkeypatch.setattr(queue, "enqueue_task", fake_enqueue)
monkeypatch.setattr(queue, "persist_queue_snapshot", lambda *a, **k: True)
monkeypatch.setattr(workers, "WORKERS", {0: SimpleNamespace()})
monkeypatch.setattr(workers, "_WORKER_POOL_DISABLED_REASON", "")
request = SimpleNamespace(app=SimpleNamespace(state=SimpleNamespace(supervisor_ready_event=None)))
response = asyncio.run(tasks.api_tasks_create(request))
body = json.loads(response.body.decode("utf-8"))
assert body["ok"] is True
metadata = captured["metadata"]
assert isinstance(metadata, dict)
assert metadata["executor_ref"]["type"] == "local"
assert metadata["executor_ref"]["id"] == "local-api"
assert metadata["executor_ref"]["workspace_backend_path"] == "/workspace"
assert metadata["executor_ref"]["path_mappings"][0]["host_path"] == str((tmp_path / "workspace").resolve(strict=False))
def test_api_task_rejects_executor_ref_without_external_workspace(tmp_path, monkeypatch):
from ouroboros.gateway import tasks
async def fake_request_json_or(_request, _default):
return {
"description": "x",
"executor_ref": {"type": "local", "workspace_host_path": str(tmp_path), "workspace_backend_path": "/workspace"},
}
monkeypatch.setattr(tasks, "request_json_or", fake_request_json_or)
monkeypatch.setattr(tasks, "request_drive_root", lambda _request: tmp_path / "data")
monkeypatch.setattr(tasks, "request_repo_dir", lambda _request: tmp_path / "repo")
(tmp_path / "data").mkdir()
(tmp_path / "repo").mkdir()
request = SimpleNamespace(app=SimpleNamespace(state=SimpleNamespace(supervisor_ready_event=None)))
response = asyncio.run(tasks.api_tasks_create(request))
body = json.loads(response.body.decode("utf-8"))
assert response.status_code == 400
assert "executor_ref requires an external workspace_root" in body["error"]
def test_api_task_rejects_empty_executor_ref(tmp_path, monkeypatch):
from ouroboros.gateway import tasks
workspace = tmp_path / "workspace"
repo = tmp_path / "repo"
data = tmp_path / "data"
_init_repo(workspace)
repo.mkdir()
data.mkdir()
async def fake_request_json_or(_request, _default):
return {
"description": "x",
"workspace_root": str(workspace),
"workspace_mode": "external",
"memory_mode": "empty",
"executor_ref": {},
}
monkeypatch.setattr(tasks, "request_json_or", fake_request_json_or)
monkeypatch.setattr(tasks, "request_drive_root", lambda _request: data)
monkeypatch.setattr(tasks, "request_repo_dir", lambda _request: repo)
request = SimpleNamespace(app=SimpleNamespace(state=SimpleNamespace(supervisor_ready_event=None)))
response = asyncio.run(tasks.api_tasks_create(request))
body = json.loads(response.body.decode("utf-8"))
assert response.status_code == 400
assert "executor_ref must be a JSON object" in body["error"]
def test_api_task_rejects_executor_ref_mapping_to_system_repo(tmp_path, monkeypatch):
from ouroboros.gateway import tasks
repo = tmp_path / "repo"
workspace = tmp_path / "workspace"
data = tmp_path / "data"
_init_repo(repo)
_init_repo(workspace)
data.mkdir()
async def fake_request_json_or(_request, _default):
return {
"description": "x",
"workspace_root": str(workspace),
"workspace_mode": "external",
"memory_mode": "empty",
"executor_ref": {"type": "local", "workspace_host_path": str(repo), "workspace_backend_path": "/workspace"},
}
monkeypatch.setattr(tasks, "request_json_or", fake_request_json_or)
monkeypatch.setattr(tasks, "request_drive_root", lambda _request: data)
monkeypatch.setattr(tasks, "request_repo_dir", lambda _request: repo)
request = SimpleNamespace(app=SimpleNamespace(state=SimpleNamespace(supervisor_ready_event=None)))
response = asyncio.run(tasks.api_tasks_create(request))
body = json.loads(response.body.decode("utf-8"))
assert response.status_code == 400
assert "must not overlap the Ouroboros system repo" in body["error"]
def test_api_task_rejects_executor_ref_mapping_to_data_drive(tmp_path, monkeypatch):
from ouroboros.gateway import tasks
repo = tmp_path / "repo"
workspace = tmp_path / "workspace"
data = tmp_path / "data"
_init_repo(repo)
_init_repo(workspace)
data.mkdir()
async def fake_request_json_or(_request, _default):
return {
"description": "x",
"workspace_root": str(workspace),
"workspace_mode": "external",
"memory_mode": "empty",
"executor_ref": {"type": "local", "workspace_host_path": str(data), "workspace_backend_path": "/workspace"},
}
monkeypatch.setattr(tasks, "request_json_or", fake_request_json_or)
monkeypatch.setattr(tasks, "request_drive_root", lambda _request: data)
monkeypatch.setattr(tasks, "request_repo_dir", lambda _request: repo)
request = SimpleNamespace(app=SimpleNamespace(state=SimpleNamespace(supervisor_ready_event=None)))
response = asyncio.run(tasks.api_tasks_create(request))
body = json.loads(response.body.decode("utf-8"))
assert response.status_code == 400
assert "must not overlap the Ouroboros data drive" in body["error"]
def test_api_task_rejects_executor_ref_not_covering_workspace(tmp_path, monkeypatch):
from ouroboros.gateway import tasks
repo = tmp_path / "repo"
workspace = tmp_path / "workspace"
other = tmp_path / "other"
data = tmp_path / "data"
_init_repo(repo)
_init_repo(workspace)
other.mkdir()
data.mkdir()
async def fake_request_json_or(_request, _default):
return {
"description": "x",
"workspace_root": str(workspace),
"workspace_mode": "external",
"memory_mode": "empty",
"executor_ref": {"type": "local", "workspace_host_path": str(other), "workspace_backend_path": "/workspace"},
}
monkeypatch.setattr(tasks, "request_json_or", fake_request_json_or)
monkeypatch.setattr(tasks, "request_drive_root", lambda _request: data)
monkeypatch.setattr(tasks, "request_repo_dir", lambda _request: repo)
request = SimpleNamespace(app=SimpleNamespace(state=SimpleNamespace(supervisor_ready_event=None)))
response = asyncio.run(tasks.api_tasks_create(request))
body = json.loads(response.body.decode("utf-8"))
assert response.status_code == 400
assert "mappings must cover workspace_root" in body["error"]
def test_api_task_rejects_reserved_executor_metadata_aliases(tmp_path, monkeypatch):
from ouroboros.gateway import tasks
repo = tmp_path / "repo"
workspace = tmp_path / "workspace"
data = tmp_path / "data"
_init_repo(repo)
_init_repo(workspace)
data.mkdir()
async def fake_request_json_or(_request, _default):
return {
"description": "x",
"workspace_root": str(workspace),
"workspace_mode": "external",
"memory_mode": "empty",
"metadata": {"workspace_executor": {"type": "local"}},
}
monkeypatch.setattr(tasks, "request_json_or", fake_request_json_or)
monkeypatch.setattr(tasks, "request_drive_root", lambda _request: data)
monkeypatch.setattr(tasks, "request_repo_dir", lambda _request: repo)
request = SimpleNamespace(app=SimpleNamespace(state=SimpleNamespace(supervisor_ready_event=None)))
response = asyncio.run(tasks.api_tasks_create(request))
body = json.loads(response.body.decode("utf-8"))
assert response.status_code == 400
assert "metadata.executor_ref/workspace_executor is reserved" in body["error"]
def test_api_task_rejects_reserved_executor_metadata_ref(tmp_path, monkeypatch):
from ouroboros.gateway import tasks
repo = tmp_path / "repo"
workspace = tmp_path / "workspace"
data = tmp_path / "data"
_init_repo(repo)
_init_repo(workspace)
data.mkdir()
async def fake_request_json_or(_request, _default):
return {
"description": "x",
"workspace_root": str(workspace),
"workspace_mode": "external",
"memory_mode": "empty",
"metadata": {"executor_ref": {"type": "local"}},
}
monkeypatch.setattr(tasks, "request_json_or", fake_request_json_or)
monkeypatch.setattr(tasks, "request_drive_root", lambda _request: data)
monkeypatch.setattr(tasks, "request_repo_dir", lambda _request: repo)
request = SimpleNamespace(app=SimpleNamespace(state=SimpleNamespace(supervisor_ready_event=None)))
response = asyncio.run(tasks.api_tasks_create(request))
body = json.loads(response.body.decode("utf-8"))
assert response.status_code == 400
assert "metadata.executor_ref/workspace_executor is reserved" in body["error"]
def test_api_task_rejects_local_network_none(tmp_path, monkeypatch):
from ouroboros.gateway import tasks
repo = tmp_path / "repo"
workspace = tmp_path / "workspace"
data = tmp_path / "data"
_init_repo(repo)
_init_repo(workspace)
data.mkdir()
async def fake_request_json_or(_request, _default):
return {
"description": "x",
"workspace_root": str(workspace),
"workspace_mode": "external",
"memory_mode": "empty",
"executor_ref": {
"type": "local",
"network": "none",
"workspace_host_path": str(workspace),
"workspace_backend_path": "/workspace",
},
}
monkeypatch.setattr(tasks, "request_json_or", fake_request_json_or)
monkeypatch.setattr(tasks, "request_drive_root", lambda _request: data)
monkeypatch.setattr(tasks, "request_repo_dir", lambda _request: repo)
request = SimpleNamespace(app=SimpleNamespace(state=SimpleNamespace(supervisor_ready_event=None)))
response = asyncio.run(tasks.api_tasks_create(request))
body = json.loads(response.body.decode("utf-8"))
assert response.status_code == 400
assert "local executor_ref cannot enforce network=none" in body["error"]
def test_api_task_rejects_malformed_executor_mapping_entry(tmp_path, monkeypatch):
from ouroboros.gateway import tasks
repo = tmp_path / "repo"
workspace = tmp_path / "workspace"
data = tmp_path / "data"
_init_repo(repo)
_init_repo(workspace)
data.mkdir()
async def fake_request_json_or(_request, _default):
return {
"description": "x",
"workspace_root": str(workspace),
"workspace_mode": "external",
"memory_mode": "empty",
"executor_ref": {
"type": "local",
"workspace_host_path": str(workspace),
"workspace_backend_path": "/workspace",
"path_mappings": [{"host_path": str(tmp_path / "missing_backend")}],
},
}
monkeypatch.setattr(tasks, "request_json_or", fake_request_json_or)
monkeypatch.setattr(tasks, "request_drive_root", lambda _request: data)
monkeypatch.setattr(tasks, "request_repo_dir", lambda _request: repo)
request = SimpleNamespace(app=SimpleNamespace(state=SimpleNamespace(supervisor_ready_event=None)))
response = asyncio.run(tasks.api_tasks_create(request))
body = json.loads(response.body.decode("utf-8"))
assert response.status_code == 400
assert "path_mappings entries require host_path and backend_path" in body["error"]
def test_api_task_rejects_malformed_executor_ref(tmp_path, monkeypatch):
from ouroboros.gateway import tasks
workspace = tmp_path / "workspace"
repo = tmp_path / "repo"
data = tmp_path / "data"
_init_repo(workspace)
repo.mkdir()
data.mkdir()
async def fake_request_json_or(_request, _default):
return {
"description": "x",
"workspace_root": str(workspace),
"workspace_mode": "external",
"memory_mode": "empty",
"executor_ref": {"workspace_host_path": str(workspace), "workspace_backend_path": "/workspace"},
}
monkeypatch.setattr(tasks, "request_json_or", fake_request_json_or)
monkeypatch.setattr(tasks, "request_drive_root", lambda _request: data)
monkeypatch.setattr(tasks, "request_repo_dir", lambda _request: repo)
request = SimpleNamespace(app=SimpleNamespace(state=SimpleNamespace(supervisor_ready_event=None)))
response = asyncio.run(tasks.api_tasks_create(request))
body = json.loads(response.body.decode("utf-8"))
assert response.status_code == 400
assert "executor_ref.type is required" in body["error"]

View file

@ -0,0 +1,623 @@
"""The docker executor backend: paths, network fence, timeouts and stop failures.
Split verbatim out of ``tests/test_workspace_executor.py`` by theme. This module owns
the service handle a failed stop must preserve, the process-group stop the service shell
uses, the backend script path and backend-absolute write targets, the ``network=none``
enforced before exec and refused when the container already has a network, and the
backend process a timeout cleans up.
Whole-file serial suite: it spawns real processes, so ``tests/conftest.py`` tags it
``serial`` and the parallel pass excludes it.
"""
from __future__ import annotations
import subprocess
from types import SimpleNamespace
import pytest
from ouroboros.tools.registry import ToolContext, ToolRegistry
from ouroboros.workspace_executor import execute, normalize_executor_ref
from tests._workspace_executor_shared import _init_repo
def test_docker_executor_stop_failure_preserves_service_handle(tmp_path, monkeypatch):
import ouroboros.workspace_executor as workspace_executor
workspace = tmp_path / "workspace"
workspace.mkdir()
data = tmp_path / "data"
data.mkdir()
ctx = ToolContext(
repo_dir=tmp_path / "repo",
drive_root=data,
workspace_root=workspace,
workspace_mode="external",
task_id="docker-stop",
executor_ref={
"type": "docker_exec",
"id": "pb-container",
"container_name": "pb-container",
"network": "none",
"workspace_host_path": str(workspace),
"workspace_backend_path": "/workspace",
},
)
workspace_executor._SERVICES.clear()
calls: list[list[str]] = []
def fake_run(cmd, **kwargs):
calls.append([str(part) for part in cmd])
if cmd[:3] == ["docker", "inspect", "-f"]:
return subprocess.CompletedProcess(cmd, 0, stdout="none\n", stderr="")
if cmd[:2] == ["docker", "exec"] and "nohup" in str(cmd[-1]):
return subprocess.CompletedProcess(cmd, 0, stdout="12345\n", stderr="")
if cmd[:2] == ["docker", "exec"] and "kill -TERM" in str(cmd[-1]):
return subprocess.CompletedProcess(cmd, 1, stdout="", stderr="permission denied")
if cmd[:2] == ["docker", "exec"] and "kill -0" in str(cmd[-1]):
return subprocess.CompletedProcess(cmd, 0, stdout="running\n", stderr="")
raise AssertionError(cmd)
monkeypatch.setattr(workspace_executor.subprocess, "run", fake_run)
workspace_executor.start_service(
ctx,
name="svc",
cmd=["sleep", "30"],
host_cwd=workspace,
cwd_root="active_workspace",
readiness={},
outputs=[],
before_outputs={},
)
failed = workspace_executor.stop_service(ctx, "svc")
assert failed and failed["stop_failed"] is True
assert "permission denied" in failed["stop_error"]
assert workspace_executor.service_status(ctx, "svc") is not None
def test_docker_executor_service_shell_uses_process_group_stop():
from ouroboros.workspace_executor import _docker_service_start_shell, _docker_service_stop_shell
record = SimpleNamespace(cmd=["python3", "-c", "import time; time.sleep(30)"], backend_cwd="/workspace")
start_shell = _docker_service_start_shell(record, "/tmp/ouroboros-service-test.log")
stop_shell = _docker_service_stop_shell("12345")
assert "setsid" in start_shell
assert "sh -c 'exec python3" in start_shell
assert "& echo $!" in start_shell
assert "kill -TERM -$pid" in stop_shell
assert "kill -KILL -$pid" in stop_shell
def test_docker_executor_run_script_uses_backend_script_path(tmp_path, monkeypatch):
import ouroboros.safety as safety_mod
import ouroboros.tools.shell as shell_mod
monkeypatch.setenv("OUROBOROS_RUNTIME_MODE", "advanced")
monkeypatch.setattr(safety_mod, "check_safety", lambda *a, **k: (True, ""))
system_repo = tmp_path / "system"
workspace = tmp_path / "workspace"
data = tmp_path / "data"
_init_repo(system_repo)
_init_repo(workspace)
data.mkdir()
captured: dict[str, object] = {}
def fake_execute(ctx, cmd, cwd, timeout_sec):
captured["cmd"] = list(cmd)
captured["cwd"] = str(cwd)
return SimpleNamespace(returncode=0, stdout="ok\n", stderr="", backend_trace={"executor_id": "pb-container"}, args=list(cmd))
monkeypatch.setattr(shell_mod, "executor_execute", fake_execute)
ctx = ToolContext(
repo_dir=system_repo,
drive_root=data,
workspace_root=workspace,
workspace_mode="external",
executor_ref={
"type": "docker_exec",
"id": "pb-container",
"container_name": "pb-container",
"network": "none",
"workspace_host_path": str(workspace),
"workspace_backend_path": "/workspace",
},
)
registry = ToolRegistry(repo_dir=system_repo, drive_root=data)
registry.set_context(ctx)
result = registry.execute("run_script", {"script": "print('ok')", "interpreter": "python3"})
assert "ok" in result
assert captured["cmd"][1].startswith("/workspace/.ouroboros/tmp_scripts/script_")
assert not str(captured["cmd"][1]).startswith(str(workspace))
def test_docker_executor_accepts_backend_absolute_write_targets_and_outputs(tmp_path, monkeypatch):
import ouroboros.safety as safety_mod
import ouroboros.tools.shell as shell_mod
monkeypatch.setenv("OUROBOROS_RUNTIME_MODE", "advanced")
monkeypatch.setattr(safety_mod, "check_safety", lambda *a, **k: (True, ""))
system_repo = tmp_path / "system"
workspace = tmp_path / "workspace"
data = tmp_path / "data"
_init_repo(system_repo)
_init_repo(workspace)
data.mkdir()
captured: dict[str, object] = {}
def fake_execute(ctx, cmd, cwd, timeout_sec):
captured["cmd"] = list(cmd)
(workspace / "backend-output.txt").write_text("ok\n", encoding="utf-8")
return SimpleNamespace(returncode=0, stdout="wrote\n", stderr="", backend_trace={"executor_id": "pb-container"}, args=list(cmd))
monkeypatch.setattr(shell_mod, "executor_execute", fake_execute)
ctx = ToolContext(
repo_dir=system_repo,
drive_root=data,
workspace_root=workspace,
workspace_mode="external",
task_id="backend-output",
executor_ref={
"type": "docker_exec",
"id": "pb-container",
"container_name": "pb-container",
"network": "none",
"workspace_host_path": str(workspace),
"workspace_backend_path": "/workspace",
},
)
registry = ToolRegistry(repo_dir=system_repo, drive_root=data)
registry.set_context(ctx)
result = registry.execute(
"run_command",
{
"cmd": ["sh", "-c", "printf ok > /workspace/backend-output.txt"],
"outputs": ["/workspace/backend-output.txt"],
},
)
assert "WORKSPACE_SHELL_BLOCKED" not in result
assert "ARTIFACT_OUTPUT_ERROR" not in result
assert "backend-output.txt" in result
assert captured["cmd"] == ["sh", "-c", "printf ok > /workspace/backend-output.txt"]
def test_docker_executor_enforces_network_none_before_exec(tmp_path, monkeypatch):
workspace = tmp_path / "workspace"
workspace.mkdir()
ctx = ToolContext(
repo_dir=tmp_path / "repo",
drive_root=tmp_path / "data",
workspace_root=workspace,
workspace_mode="external",
executor_ref={
"type": "docker_exec",
"id": "pb-container",
"container_name": "pb-container",
"network": "none",
"workspace_host_path": str(workspace),
"workspace_backend_path": "/workspace",
},
)
calls: list[list[str]] = []
def fake_run(cmd, **kwargs):
calls.append([str(part) for part in cmd])
if cmd[:3] == ["docker", "inspect", "-f"]:
return subprocess.CompletedProcess(cmd, 0, stdout="none\n", stderr="")
raise AssertionError(cmd)
class FakePopen:
pid = 999991
returncode = 0
def __init__(self, cmd, **kwargs):
calls.append([str(part) for part in cmd])
self.args = cmd
def communicate(self, timeout=None):
return "ok\n", ""
import ouroboros.workspace_executor as workspace_executor
monkeypatch.setattr(workspace_executor.subprocess, "run", fake_run)
monkeypatch.setattr(workspace_executor.subprocess, "Popen", FakePopen)
result = execute(ctx, ["echo", "ok"], workspace, 30)
assert result.returncode == 0
assert result.stdout == "ok\n"
assert calls[0][:4] == ["docker", "inspect", "-f", "{{.HostConfig.NetworkMode}}"]
assert calls[1][:4] == ["docker", "exec", "--workdir", "/workspace"]
def test_docker_executor_timeout_cleans_backend_process(tmp_path, monkeypatch):
workspace = tmp_path / "workspace"
workspace.mkdir()
ctx = ToolContext(
repo_dir=tmp_path / "repo",
drive_root=tmp_path / "data",
workspace_root=workspace,
workspace_mode="external",
executor_ref={
"type": "docker_exec",
"id": "pb-container",
"container_name": "pb-container",
"network": "none",
"workspace_host_path": str(workspace),
"workspace_backend_path": "/workspace",
},
)
calls: list[list[str]] = []
def fake_run(cmd, **kwargs):
calls.append([str(part) for part in cmd])
if cmd[:3] == ["docker", "inspect", "-f"]:
return subprocess.CompletedProcess(cmd, 0, stdout="none\n", stderr="")
if cmd[:2] == ["docker", "exec"] and "kill -TERM -$pid" in str(cmd[-1]):
return subprocess.CompletedProcess(cmd, 0, stdout="", stderr="")
raise AssertionError(cmd)
class FakePopen:
pid = 999992
returncode = None
def __init__(self, cmd, **kwargs):
calls.append([str(part) for part in cmd])
self.args = cmd
def communicate(self, timeout=None):
raise subprocess.TimeoutExpired(self.args, timeout=timeout)
def wait(self, timeout=None):
self.returncode = -9
return self.returncode
import ouroboros.workspace_executor as workspace_executor
monkeypatch.setattr(workspace_executor.subprocess, "run", fake_run)
monkeypatch.setattr(workspace_executor.subprocess, "Popen", FakePopen)
with pytest.raises(subprocess.TimeoutExpired):
execute(ctx, ["sleep", "30"], workspace, 1)
assert any("cat /tmp/ouroboros-exec-" in call[-1] for call in calls if call[:2] == ["docker", "exec"])
assert any("kill -TERM -$pid" in call[-1] for call in calls if call[:2] == ["docker", "exec"])
def test_docker_executor_rejects_network_none_when_container_has_network(tmp_path, monkeypatch):
workspace = tmp_path / "workspace"
workspace.mkdir()
ref = normalize_executor_ref(
{
"type": "docker_exec",
"container_name": "pb-container",
"network": "none",
"workspace_host_path": str(workspace),
"workspace_backend_path": "/workspace",
}
)
assert ref is not None
ctx = ToolContext(
repo_dir=tmp_path / "repo",
drive_root=tmp_path / "data",
workspace_root=workspace,
workspace_mode="external",
executor_ref={
"type": "docker_exec",
"container_name": "pb-container",
"network": "none",
"workspace_host_path": str(workspace),
"workspace_backend_path": "/workspace",
},
)
def fake_run(cmd, **kwargs):
return subprocess.CompletedProcess(cmd, 0, stdout="bridge\n", stderr="")
import ouroboros.workspace_executor as workspace_executor
monkeypatch.setattr(workspace_executor.subprocess, "run", fake_run)
try:
execute(ctx, ["echo", "ok"], workspace, 30)
except RuntimeError as exc:
assert "NetworkMode=none" in str(exc)
else: # pragma: no cover - kept explicit for failure readability
raise AssertionError("docker network mismatch was not rejected")
def test_docker_executor_stop_success_without_terminal_kill_preserves_handle(tmp_path, monkeypatch):
import ouroboros.workspace_executor as workspace_executor
workspace = tmp_path / "workspace"
workspace.mkdir()
data = tmp_path / "data"
data.mkdir()
ctx = ToolContext(
repo_dir=tmp_path / "repo",
drive_root=data,
workspace_root=workspace,
workspace_mode="external",
task_id="docker-stop-race",
executor_ref={
"type": "docker_exec",
"id": "pb-container",
"container_name": "pb-container",
"network": "none",
"workspace_host_path": str(workspace),
"workspace_backend_path": "/workspace",
},
)
workspace_executor._SERVICES.clear()
def fake_run(cmd, **kwargs):
if cmd[:3] == ["docker", "inspect", "-f"]:
return subprocess.CompletedProcess(cmd, 0, stdout="none\n", stderr="")
if cmd[:2] == ["docker", "exec"] and "nohup" in str(cmd[-1]):
return subprocess.CompletedProcess(cmd, 0, stdout="12345\n", stderr="")
if cmd[:2] == ["docker", "exec"] and "kill -TERM" in str(cmd[-1]):
# The stop shell itself returned success, but the subsequent
# kill-0 confirmation still observes a live backend.
return subprocess.CompletedProcess(cmd, 0, stdout="", stderr="")
if cmd[:2] == ["docker", "exec"] and "kill -0" in str(cmd[-1]):
return subprocess.CompletedProcess(cmd, 0, stdout="running\n", stderr="")
raise AssertionError(cmd)
monkeypatch.setattr(workspace_executor.subprocess, "run", fake_run)
workspace_executor.start_service(
ctx,
name="svc",
cmd=["sleep", "30"],
host_cwd=workspace,
cwd_root="active_workspace",
readiness={},
outputs=[],
before_outputs={},
)
failed = workspace_executor.stop_service(ctx, "svc")
assert failed and failed["stop_failed"] is True
assert "kill-0" in failed["stop_error"]
assert workspace_executor.service_status(ctx, "svc") is not None
def test_docker_executor_stop_unknown_probe_preserves_handle(tmp_path, monkeypatch):
import ouroboros.workspace_executor as workspace_executor
workspace = tmp_path / "workspace"
workspace.mkdir()
data = tmp_path / "data"
data.mkdir()
ctx = ToolContext(
repo_dir=tmp_path / "repo",
drive_root=data,
workspace_root=workspace,
workspace_mode="external",
task_id="docker-stop-unknown",
executor_ref={
"type": "docker_exec",
"id": "pb-container",
"container_name": "pb-container",
"network": "none",
"workspace_host_path": str(workspace),
"workspace_backend_path": "/workspace",
},
)
workspace_executor._SERVICES.clear()
def fake_run(cmd, **kwargs):
if cmd[:3] == ["docker", "inspect", "-f"]:
return subprocess.CompletedProcess(cmd, 0, stdout="none\n", stderr="")
if cmd[:2] == ["docker", "exec"] and "nohup" in str(cmd[-1]):
return subprocess.CompletedProcess(cmd, 0, stdout="12345\n", stderr="")
if cmd[:2] == ["docker", "exec"] and "kill -TERM" in str(cmd[-1]):
return subprocess.CompletedProcess(cmd, 0, stdout="", stderr="")
if cmd[:2] == ["docker", "exec"] and "kill -0" in str(cmd[-1]):
return subprocess.CompletedProcess(cmd, 7, stdout="", stderr="daemon unavailable")
raise AssertionError(cmd)
monkeypatch.setattr(workspace_executor.subprocess, "run", fake_run)
workspace_executor.start_service(
ctx,
name="svc",
cmd=["sleep", "30"],
host_cwd=workspace,
cwd_root="active_workspace",
readiness={},
outputs=[],
before_outputs={},
)
failed = workspace_executor.stop_service(ctx, "svc")
assert failed and failed["stop_failed"] is True
assert "unknown" in failed["stop_error"]
assert failed["state"] == "unknown"
assert workspace_executor.service_status(ctx, "svc") is not None
def test_docker_executor_stop_state_exception_preserves_handle(tmp_path, monkeypatch):
import ouroboros.workspace_executor as workspace_executor
workspace = tmp_path / "workspace"
workspace.mkdir()
data = tmp_path / "data"
data.mkdir()
ctx = ToolContext(
repo_dir=tmp_path / "repo",
drive_root=data,
workspace_root=workspace,
workspace_mode="external",
task_id="docker-stop-exception",
executor_ref={
"type": "docker_exec",
"id": "pb-container",
"container_name": "pb-container",
"network": "none",
"workspace_host_path": str(workspace),
"workspace_backend_path": "/workspace",
},
)
workspace_executor._SERVICES.clear()
def fake_run(cmd, **kwargs):
if cmd[:3] == ["docker", "inspect", "-f"]:
return subprocess.CompletedProcess(cmd, 0, stdout="none\n", stderr="")
if cmd[:2] == ["docker", "exec"] and "nohup" in str(cmd[-1]):
return subprocess.CompletedProcess(cmd, 0, stdout="12345\n", stderr="")
if cmd[:2] == ["docker", "exec"] and "kill -TERM" in str(cmd[-1]):
return subprocess.CompletedProcess(cmd, 0, stdout="", stderr="")
raise AssertionError(cmd)
monkeypatch.setattr(workspace_executor.subprocess, "run", fake_run)
monkeypatch.setattr(workspace_executor, "_service_state", lambda _record: (_ for _ in ()).throw(RuntimeError("probe boom")))
workspace_executor.start_service(
ctx,
name="svc",
cmd=["sleep", "30"],
host_cwd=workspace,
cwd_root="active_workspace",
readiness={},
outputs=[],
before_outputs={},
)
failed = workspace_executor.stop_service(ctx, "svc")
assert failed and failed["stop_failed"] is True
assert failed["state"] == "unknown"
assert workspace_executor.service_status(ctx, "svc") is not None
def test_docker_executor_global_cleanup_unknown_state_keeps_handle(tmp_path, monkeypatch):
import ouroboros.workspace_executor as workspace_executor
workspace = tmp_path / "workspace"
workspace.mkdir()
data = tmp_path / "data"
data.mkdir()
ctx = ToolContext(
repo_dir=tmp_path / "repo",
drive_root=data,
workspace_root=workspace,
workspace_mode="external",
task_id="docker-cleanup-unknown",
executor_ref={
"type": "docker_exec",
"id": "pb-container",
"container_name": "pb-container",
"network": "none",
"workspace_host_path": str(workspace),
"workspace_backend_path": "/workspace",
},
)
workspace_executor._SERVICES.clear()
def fake_run(cmd, **kwargs):
if cmd[:3] == ["docker", "inspect", "-f"]:
return subprocess.CompletedProcess(cmd, 0, stdout="none\n", stderr="")
if cmd[:2] == ["docker", "exec"] and "nohup" in str(cmd[-1]):
return subprocess.CompletedProcess(cmd, 0, stdout="12345\n", stderr="")
if cmd[:2] == ["docker", "exec"] and "kill -TERM" in str(cmd[-1]):
return subprocess.CompletedProcess(cmd, 0, stdout="", stderr="")
if cmd[:2] == ["docker", "exec"] and "kill -0" in str(cmd[-1]):
return subprocess.CompletedProcess(cmd, 7, stdout="", stderr="daemon unavailable")
raise AssertionError(cmd)
monkeypatch.setattr(workspace_executor.subprocess, "run", fake_run)
workspace_executor.start_service(
ctx,
name="svc",
cmd=["sleep", "30"],
host_cwd=workspace,
cwd_root="active_workspace",
readiness={},
outputs=[],
before_outputs={},
)
result = workspace_executor.kill_all_services(data)
current = next(item for item in result if item.get("name") == "svc")
assert current["cleanup_dispatched"] is False
assert current["stop_failed"] is True
assert current["state"] == "unknown"
assert workspace_executor.service_status(ctx, "svc") is not None
def test_docker_durable_cleanup_keeps_record_until_kill_zero_terminal(tmp_path, monkeypatch):
import ouroboros.workspace_executor as workspace_executor
data = tmp_path / "data"
data.mkdir()
workspace_executor._SERVICES.clear()
path = workspace_executor._register_process(
data,
{
"record_type": "service",
"executor_type": "docker_exec",
"executor_id": "pb-container",
"container_name": "pb-container",
"backend_pid": "12345",
"service_id": "task:durable",
"task_id": "task",
"name": "durable",
},
)
assert path is not None
def fake_run(cmd, **kwargs):
if cmd[:2] == ["docker", "exec"] and "kill -TERM" in str(cmd[-1]):
return subprocess.CompletedProcess(cmd, 0, stdout="", stderr="")
if cmd[:2] == ["docker", "exec"] and "kill -0" in str(cmd[-1]):
return subprocess.CompletedProcess(cmd, 0, stdout="running\n", stderr="")
raise AssertionError(cmd)
monkeypatch.setattr(workspace_executor.subprocess, "run", fake_run)
result = workspace_executor._kill_durable_service_records(data)
assert result[0]["state"] == "cleanup_pending"
assert result[0]["cleanup_dispatched"] is False
assert path.exists()
def test_docker_durable_cleanup_keeps_record_on_unknown_kill_zero(tmp_path, monkeypatch):
import ouroboros.workspace_executor as workspace_executor
data = tmp_path / "data"
data.mkdir()
workspace_executor._SERVICES.clear()
path = workspace_executor._register_process(
data,
{
"record_type": "service",
"executor_type": "docker_exec",
"executor_id": "pb-container",
"container_name": "pb-container",
"backend_pid": "12345",
"service_id": "task:durable-unknown",
"task_id": "task",
"name": "durable-unknown",
},
)
assert path is not None
def fake_run(cmd, **kwargs):
if cmd[:2] == ["docker", "exec"] and "kill -TERM" in str(cmd[-1]):
return subprocess.CompletedProcess(cmd, 0, stdout="", stderr="")
if cmd[:2] == ["docker", "exec"] and "kill -0" in str(cmd[-1]):
return subprocess.CompletedProcess(cmd, 7, stdout="", stderr="daemon unavailable")
raise AssertionError(cmd)
monkeypatch.setattr(workspace_executor.subprocess, "run", fake_run)
result = workspace_executor._kill_durable_service_records(data)
assert result[0]["state"] == "cleanup_pending"
assert result[0]["cleanup_dispatched"] is False
assert path.exists()

View file

@ -0,0 +1,521 @@
"""Executor-backed services: their lifecycle, their records and their teardown.
Split verbatim out of ``tests/test_workspace_executor.py`` by theme. This module owns
the private snapshot a local service hides, the restart after exit, the sanitized env
and redacted logs, the status and durable record that redact secret-like arguments, the
task and global cleanup they participate in, the keep-alive that survives task
teardown, the panic cleanup that kills durable foreground and service processes, and
the child-drive records a parent data root must scan.
Whole-file serial suite: it spawns real processes, so ``tests/conftest.py`` tags it
``serial`` and the parallel pass excludes it.
"""
from __future__ import annotations
import json
import subprocess
import sys
from types import SimpleNamespace
from ouroboros.tools.registry import ToolContext, ToolRegistry
from tests._workspace_executor_shared import _init_repo
def test_executor_local_service_lifecycle_hides_private_snapshot(tmp_path, monkeypatch):
import ouroboros.safety as safety_mod
import ouroboros.workspace_executor as workspace_executor
monkeypatch.setenv("OUROBOROS_RUNTIME_MODE", "advanced")
monkeypatch.setattr(safety_mod, "check_safety", lambda *a, **k: (True, ""))
bootstrap_calls: list[str] = []
monkeypatch.setattr(workspace_executor, "bootstrap_process_path", lambda: bootstrap_calls.append("bootstrap"))
system_repo = tmp_path / "system"
workspace = tmp_path / "workspace"
data = tmp_path / "data"
_init_repo(system_repo)
_init_repo(workspace)
data.mkdir()
ctx = ToolContext(
repo_dir=system_repo,
drive_root=data,
workspace_root=workspace,
workspace_mode="external",
task_id="svc-test",
executor_ref={
"type": "local",
"id": "local-service",
"workspace_host_path": str(workspace),
"workspace_backend_path": "/workspace",
},
)
registry = ToolRegistry(repo_dir=system_repo, drive_root=data)
registry.set_context(ctx)
started = json.loads(
registry.execute(
"start_service",
{
"name": "svc",
"cmd": [
sys.executable,
"-c",
"import os,time; os.write(1, b'READY\\n' + b'x' * 25000); time.sleep(30)",
],
"readiness": {"log_contains": "READY", "timeout_sec": 5},
},
)
)
status = json.loads(registry.execute("service_status", {"name": "svc"}))
logs = json.loads(registry.execute("service_logs", {"name": "svc", "tail": 1000}))
stopped_raw = registry.execute("stop_service", {"name": "svc"})
stopped = json.loads(stopped_raw)
assert started["ready"] is True
assert started["ready_observed_at"]
assert status["state"] == "running"
assert "READY" not in logs["tail"]
assert "x" in logs["tail"]
assert stopped["state"] == "stopped"
assert "_before_outputs" not in stopped_raw
assert bootstrap_calls
def test_start_service_with_executor_ref_uses_local_for_unmapped_task_drive_cwd(tmp_path, monkeypatch):
import ouroboros.safety as safety_mod
from ouroboros.tool_access import resource_root_path
monkeypatch.setenv("OUROBOROS_RUNTIME_MODE", "advanced")
monkeypatch.setattr(safety_mod, "check_safety", lambda *a, **k: (True, ""))
system_repo = tmp_path / "system"
workspace = tmp_path / "workspace"
data = tmp_path / "data"
_init_repo(system_repo)
_init_repo(workspace)
data.mkdir()
ctx = ToolContext(
repo_dir=system_repo,
drive_root=data,
workspace_root=workspace,
workspace_mode="external",
task_id="svc-task-drive",
executor_ref={
"type": "local",
"id": "local-service",
"workspace_host_path": str(workspace),
"workspace_backend_path": "/workspace",
},
)
task_drive = resource_root_path(ctx, "task_drive")
task_drive.mkdir(parents=True, exist_ok=True)
registry = ToolRegistry(repo_dir=system_repo, drive_root=data)
registry.set_context(ctx)
started = json.loads(
registry.execute(
"start_service",
{
"name": "svc",
"cmd": [sys.executable, "-c", "import time; print('READY', flush=True); time.sleep(30)"],
"cwd": str(task_drive),
"readiness": {"log_contains": "READY", "timeout_sec": 5},
},
)
)
status = json.loads(registry.execute("service_status", {"name": "svc"}))
logs = json.loads(registry.execute("service_logs", {"name": "svc", "tail": 1000}))
stopped = json.loads(registry.execute("stop_service", {"name": "svc"}))
assert "executor" not in started
assert started["cwd_root"] == "task_drive"
assert status["state"] == "running"
assert "READY" in logs["tail"]
assert stopped["state"] == "exited"
def test_executor_local_service_can_restart_after_exit(tmp_path, monkeypatch):
import ouroboros.safety as safety_mod
monkeypatch.setenv("OUROBOROS_RUNTIME_MODE", "advanced")
monkeypatch.setattr(safety_mod, "check_safety", lambda *a, **k: (True, ""))
system_repo = tmp_path / "system"
workspace = tmp_path / "workspace"
data = tmp_path / "data"
_init_repo(system_repo)
_init_repo(workspace)
data.mkdir()
ctx = ToolContext(
repo_dir=system_repo,
drive_root=data,
workspace_root=workspace,
workspace_mode="external",
task_id="svc-restart",
executor_ref={
"type": "local",
"id": "local-service",
"workspace_host_path": str(workspace),
"workspace_backend_path": "/workspace",
},
)
registry = ToolRegistry(repo_dir=system_repo, drive_root=data)
registry.set_context(ctx)
first = json.loads(registry.execute("start_service", {"name": "short", "cmd": [sys.executable, "-c", "print('one')"]}))
import time
time.sleep(0.5)
second = json.loads(registry.execute("start_service", {"name": "short", "cmd": [sys.executable, "-c", "print('two')"]}))
assert first["backend_pid"] != second["backend_pid"]
assert second.get("note") != "already_running"
records = list((data / "state" / "workspace_executor_processes").glob("*.json"))
assert len(records) == 1
durable = json.loads(records[0].read_text(encoding="utf-8"))
assert str(durable["host_pid"]) == str(second["backend_pid"])
assert str(durable["host_pid"]) != str(first["backend_pid"])
def test_executor_local_service_sanitizes_env_and_redacts_logs(tmp_path, monkeypatch):
import ouroboros.safety as safety_mod
monkeypatch.setenv("OUROBOROS_RUNTIME_MODE", "advanced")
monkeypatch.setenv("OPENROUTER_API_KEY", "sk-secret-executor-service")
monkeypatch.setattr(safety_mod, "check_safety", lambda *a, **k: (True, ""))
system_repo = tmp_path / "system"
workspace = tmp_path / "workspace"
data = tmp_path / "data"
_init_repo(system_repo)
_init_repo(workspace)
data.mkdir()
ctx = ToolContext(
repo_dir=system_repo,
drive_root=data,
workspace_root=workspace,
workspace_mode="external",
task_id="svc-env",
executor_ref={
"type": "local",
"id": "local-service",
"workspace_host_path": str(workspace),
"workspace_backend_path": "/workspace",
},
)
registry = ToolRegistry(repo_dir=system_repo, drive_root=data)
registry.set_context(ctx)
registry.execute(
"start_service",
{
"name": "svc",
"cmd": [
sys.executable,
"-c",
"import os, time; print(os.environ.get('OPENROUTER_API_KEY','missing'), flush=True); time.sleep(30)",
],
"readiness": {"log_contains": "missing", "timeout_sec": 5},
},
)
logs = json.loads(registry.execute("service_logs", {"name": "svc", "tail": 1000}))
registry.execute("stop_service", {"name": "svc"})
assert "missing" in logs["tail"]
assert "sk-secret-executor-service" not in logs["tail"]
def test_executor_service_status_and_durable_record_redact_secret_like_args(tmp_path, monkeypatch):
import ouroboros.safety as safety_mod
monkeypatch.setenv("OUROBOROS_RUNTIME_MODE", "advanced")
monkeypatch.setattr(safety_mod, "check_safety", lambda *a, **k: (True, ""))
system_repo = tmp_path / "system"
workspace = tmp_path / "workspace"
data = tmp_path / "data"
_init_repo(system_repo)
_init_repo(workspace)
data.mkdir()
secret = "OPENAI_API_KEY=sk-secretservicetraceabcdefghijk123456"
ctx = ToolContext(
repo_dir=system_repo,
drive_root=data,
workspace_root=workspace,
workspace_mode="external",
task_id="svc-redact",
executor_ref={
"type": "local",
"id": "local-service",
"workspace_host_path": str(workspace),
"workspace_backend_path": "/workspace",
},
)
registry = ToolRegistry(repo_dir=system_repo, drive_root=data)
registry.set_context(ctx)
registry.execute(
"start_service",
{
"name": "svc",
"cmd": [sys.executable, "-c", "import time; print('READY', flush=True); time.sleep(30)", secret],
"readiness": {"log_contains": "READY", "timeout_sec": 5},
},
)
try:
status_raw = registry.execute("service_status", {"name": "svc"})
records = list((data / "state" / "workspace_executor_processes").glob("*.json"))
durable_text = "\n".join(path.read_text(encoding="utf-8") for path in records)
finally:
registry.execute("stop_service", {"name": "svc"})
assert secret not in status_raw
assert secret not in durable_text
assert '"readiness"' not in durable_text
assert "***REDACTED***" in status_raw
assert "***REDACTED***" in durable_text
def test_executor_services_participate_in_task_and_global_cleanup(tmp_path, monkeypatch):
import ouroboros.safety as safety_mod
from ouroboros.tools.services import kill_all_services, stop_task_services
import ouroboros.workspace_executor as workspace_executor
monkeypatch.setenv("OUROBOROS_RUNTIME_MODE", "advanced")
monkeypatch.setattr(safety_mod, "check_safety", lambda *a, **k: (True, ""))
workspace_executor._SERVICES.clear()
system_repo = tmp_path / "system"
workspace = tmp_path / "workspace"
data = tmp_path / "data"
_init_repo(system_repo)
_init_repo(workspace)
data.mkdir()
ctx = ToolContext(
repo_dir=system_repo,
drive_root=data,
workspace_root=workspace,
workspace_mode="external",
task_id="svc-cleanup",
executor_ref={
"type": "local",
"id": "local-service",
"workspace_host_path": str(workspace),
"workspace_backend_path": "/workspace",
},
)
registry = ToolRegistry(repo_dir=system_repo, drive_root=data)
registry.set_context(ctx)
registry.execute("start_service", {"name": "tasksvc", "cmd": [sys.executable, "-c", "import time; time.sleep(30)"]})
stopped = stop_task_services(ctx)
assert any(item.get("name") == "tasksvc" for item in stopped)
assert workspace_executor.service_status(ctx, "tasksvc") is None
registry.execute("start_service", {"name": "globalsvc", "cmd": [sys.executable, "-c", "import time; time.sleep(30)"]})
killed = kill_all_services(data)
assert any(item.get("name") == "globalsvc" for item in killed)
assert workspace_executor.service_status(ctx, "globalsvc") is None
def test_executor_keep_alive_service_survives_task_teardown(tmp_path, monkeypatch):
import ouroboros.safety as safety_mod
import ouroboros.workspace_executor as workspace_executor
from ouroboros.tools.services import kill_all_services, stop_task_services
monkeypatch.setenv("OUROBOROS_RUNTIME_MODE", "advanced")
monkeypatch.setattr(safety_mod, "check_safety", lambda *a, **k: (True, ""))
workspace_executor._SERVICES.clear()
system_repo = tmp_path / "system"
workspace = tmp_path / "workspace"
data = tmp_path / "data"
_init_repo(system_repo)
_init_repo(workspace)
data.mkdir()
ctx = ToolContext(
repo_dir=system_repo,
drive_root=data,
workspace_root=workspace,
workspace_mode="external",
task_id="svc-keep",
executor_ref={
"type": "local",
"id": "local-service",
"workspace_host_path": str(workspace),
"workspace_backend_path": "/workspace",
},
)
registry = ToolRegistry(repo_dir=system_repo, drive_root=data)
registry.set_context(ctx)
registry.execute("start_service", {
"name": "keptsvc",
"cmd": [sys.executable, "-c", "import time; time.sleep(30)"],
"keep_alive": True,
})
finalized = stop_task_services(ctx)
assert finalized[0]["name"] == "keptsvc"
assert finalized[0]["lifecycle"] == "kept"
assert workspace_executor.service_status(ctx, "keptsvc") is not None
killed = kill_all_services(data)
assert any(item.get("name") == "keptsvc" for item in killed)
assert workspace_executor.service_status(ctx, "keptsvc") is None
def test_executor_panic_cleanup_kills_durable_foreground_and_service_processes(tmp_path):
import time
import ouroboros.workspace_executor as workspace_executor
from ouroboros.platform_layer import subprocess_new_group_kwargs
data = tmp_path / "data"
data.mkdir()
foreground = subprocess.Popen(
[sys.executable, "-c", "import time; time.sleep(30)"],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
stdin=subprocess.DEVNULL,
**subprocess_new_group_kwargs(),
)
service = subprocess.Popen(
[sys.executable, "-c", "import time; time.sleep(30)"],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
stdin=subprocess.DEVNULL,
**subprocess_new_group_kwargs(),
)
try:
workspace_executor._register_process(
data,
{
"record_type": "foreground",
"executor_type": "local",
"executor_id": "local-foreground",
"host_pid": foreground.pid,
},
)
workspace_executor._register_process(
data,
{
"record_type": "service",
"service_id": "task:svc",
"task_id": "task",
"name": "svc",
"executor_type": "local",
"executor_id": "local-service",
"host_pid": service.pid,
},
)
killed_foreground = workspace_executor.kill_all_foreground(data, wait=False)
killed_services = workspace_executor.kill_all_services(data, wait=False)
deadline = time.time() + 15
while time.time() < deadline and (foreground.poll() is None or service.poll() is None):
time.sleep(0.05)
assert foreground.poll() is not None
assert service.poll() is not None
assert any(item.get("executor_type") == "local" for item in killed_foreground)
assert any(item.get("service_id") == "task:svc" for item in killed_services)
assert not list((data / "state" / "workspace_executor_processes").glob("*.json"))
finally:
for proc in (foreground, service):
if proc.poll() is None:
proc.kill()
def test_executor_cleanup_scans_child_drive_records_from_parent_data_root(tmp_path):
import time
import ouroboros.workspace_executor as workspace_executor
from ouroboros.platform_layer import subprocess_new_group_kwargs
data = tmp_path / "data"
child_data = data / "state" / "headless_tasks" / "task-1" / "data"
child_data.mkdir(parents=True)
proc = subprocess.Popen(
[sys.executable, "-c", "import time; time.sleep(30)"],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
stdin=subprocess.DEVNULL,
**subprocess_new_group_kwargs(),
)
# kill_all_foreground's PID-reuse safety check compares the process command-sha recorded at
# registration against the one it recomputes at kill time. Right after fork+exec the child's
# command line may not yet be readable, so registering too eagerly makes the two shas diverge
# and the kill is silently skipped (flaky). Wait until the command-sha is readable & stable.
for _ in range(200):
if workspace_executor._process_command_sha256(proc.pid):
break
time.sleep(0.02)
try:
workspace_executor._register_process(
child_data,
{
"record_type": "foreground",
"executor_type": "local",
"executor_id": "child-local",
"host_pid": proc.pid,
},
)
killed = workspace_executor.kill_all_foreground(data, wait=False)
deadline = time.time() + 15
while time.time() < deadline and proc.poll() is None:
time.sleep(0.05)
assert proc.poll() is not None
assert any(item.get("executor_type") == "local" for item in killed)
assert not list((child_data / "state" / "workspace_executor_processes").glob("*.json"))
finally:
if proc.poll() is None:
proc.kill()
def test_executor_readiness_scans_before_large_log_suffix(tmp_path, monkeypatch):
import ouroboros.workspace_executor as workspace_executor
log_path = tmp_path / "executor-service.log"
log_path.write_bytes(b"READY\n" + (b"x" * 25_000))
record = SimpleNamespace(
executor=SimpleNamespace(kind="local"),
backend_log_path=str(log_path),
local_proc=SimpleNamespace(poll=lambda: None),
ready=False,
)
monkeypatch.setattr(workspace_executor.time, "sleep", lambda _seconds: None)
workspace_executor._wait_readiness(
record,
{"log_contains": "READY", "timeout_sec": 0.05},
)
assert record.ready is True
def test_executor_terminal_payload_clears_readiness(tmp_path):
import ouroboros.workspace_executor as workspace_executor
record = SimpleNamespace(
service_id="task:svc",
name="svc",
task_id="task",
executor=SimpleNamespace(
executor_id="local-service",
kind="local",
network="host",
),
backend_pid="4321",
backend_cwd="/workspace",
host_cwd=tmp_path,
cwd_root="active_workspace",
cwd_base=str(tmp_path),
cwd_source="active_workspace",
skill_name="",
cmd=["service"],
outputs=[],
keep_alive=False,
backend_log_path=str(tmp_path / "service.log"),
started_at=workspace_executor.time.time(),
ready=True,
)
payload = workspace_executor._service_payload(record, state="exited")
assert payload["state"] == "exited"
assert payload["ready"] is False
assert record.ready is False