From c2e45111fb640ec83f4fb1ad1f1c6b72570c4ed6 Mon Sep 17 00:00:00 2001
From: Ouroboros <311266734+ouroboros-agent@users.noreply.github.com>
Date: Sat, 26 Sep 2026 23:25:59 +0300
Subject: [PATCH 1/3] fix: restore portable release checks and symlink
diagnostics
Preserve declared-entry hash failures and typed evidence omissions under Python 3.13. Keep the approved Windows artifact refusal explicit, isolate custody and nested pytest state, and synchronize the permission, lock, CSP, TLS and dispatcher fixtures without changing runtime policy.
---
docs/architecture/04-server-api-endpoints.md | 2 +-
ouroboros/skill_loader.py | 8 ++--
ouroboros/tools/plan_evidence.py | 7 +++-
tests/conftest.py | 9 +++--
tests/test_author_ui_kit_browser.py | 8 ++--
tests/test_child_drive_settlement.py | 42 +++++++++++++++-----
tests/test_cybergym_dispatch.py | 17 +++++---
tests/test_evolution_commit_receipt.py | 19 ++++++++-
tests/test_headless_task_artifacts.py | 24 +++++++----
tests/test_large_task_artifacts.py | 22 +++++++---
tests/test_net_transport_extra_ca.py | 8 ++++
tests/test_review_source_handles.py | 8 +++-
tests/test_test_environment.py | 2 +-
13 files changed, 130 insertions(+), 46 deletions(-)
diff --git a/docs/architecture/04-server-api-endpoints.md b/docs/architecture/04-server-api-endpoints.md
index 2618a7317..c8b17880c 100644
--- a/docs/architecture/04-server-api-endpoints.md
+++ b/docs/architecture/04-server-api-endpoints.md
@@ -85,7 +85,7 @@ Every `/api/files/*` operation resolves its requested path and refuses the opera
| GET | `/api/tasks/{task_id}` | `gateway.tasks.api_task_get` |
| GET | `/api/tasks/{task_id}/events` | `gateway.tasks.api_task_events` (legacy integer rank) |
| POST | `/api/tasks/{task_id}/events` | `gateway.tasks.api_task_events` (read-only v2 cursor) |
-| GET | `/api/tasks/{task_id}/artifacts/{name}` | `gateway.tasks.api_task_artifact` (the task's own stores via `task_archive`: a bare name is a top-level file, `?relpath=` a nested one, `?archive=
` a directory ZIP; the detail's `artifact_archives` says what each ZIP holds. A row that records a digest is served only when its bytes still match it — a changed mutable file is 409 `artifact_identity_changed` naming the recorded digest, a failed capture 404 `artifact_unverified` — and the response says `x-ouroboros-artifact-identity: verified` or `unmeasured`; a ZIP member follows the same rule) |
+| GET | `/api/tasks/{task_id}/artifacts/{name}` | `gateway.tasks.api_task_artifact` (the task's own stores via `task_archive`: a bare name is a top-level file, `?relpath=` a nested one, `?archive=` a directory ZIP; the detail's `artifact_archives` says what each ZIP holds. A row that records a digest is served only when its bytes still match it — a changed mutable file is 409 `artifact_identity_changed` naming the recorded digest, a failed capture 404 `artifact_unverified` — and the response says `x-ouroboros-artifact-identity: verified` or `unmeasured`; a ZIP member follows the same rule. Windows ordinary file/chat-media/ZIP downloads return HTTP 503 pending #1297; bound `?source=` review downloads remain available) |
| POST | `/api/tasks/{task_id}/cancel` | `gateway.tasks.api_task_cancel` |
| POST | `/api/tasks/{task_id}/hurry` | `gateway.tasks.api_task_hurry` |
| POST | `/api/tasks/{task_id}/resume` | `gateway.tasks.api_task_resume` |
diff --git a/ouroboros/skill_loader.py b/ouroboros/skill_loader.py
index 9f98fda90..46ba8e190 100644
--- a/ouroboros/skill_loader.py
+++ b/ouroboros/skill_loader.py
@@ -11,6 +11,7 @@ from __future__ import annotations
import hashlib
import logging
import pathlib
+import stat
from dataclasses import asdict, dataclass, field
from typing import Any, Callable, Dict, Iterable, List, Optional
@@ -356,10 +357,11 @@ def _iter_payload_files(
resolved = (skill_dir / rel).resolve()
try:
resolved.relative_to(resolved_root)
- except ValueError:
+ # is_file() hides ELOOP; an unreadable declared entry cannot be omitted from its hash.
+ if stat.S_ISREG(resolved.stat().st_mode):
+ _add(resolved)
+ except (ValueError, FileNotFoundError, NotADirectoryError):
return
- if resolved.is_file():
- _add(resolved)
# Broad walk: everything runtime-reachable, minus metadata/cache names.
# Every candidate is resolved back under skill_dir so symlinks cannot leak
diff --git a/ouroboros/tools/plan_evidence.py b/ouroboros/tools/plan_evidence.py
index 225f56b6e..55422714f 100644
--- a/ouroboros/tools/plan_evidence.py
+++ b/ouroboros/tools/plan_evidence.py
@@ -14,6 +14,7 @@ from __future__ import annotations
import codecs
import ast
+import errno
from hashlib import sha256
import pathlib
import json
@@ -214,13 +215,15 @@ def _read_evidence(
def _path_kind(path: pathlib.Path) -> str:
- """``missing`` · ``directory`` · ``file`` (regular) · ``unreadable`` (stat failure or a
+ """``missing`` · ``directory`` · ``file`` · ``symlink_loop`` · ``unreadable`` (stat failure or a
non-regular node: fifo/device/socket would block or never end a read)."""
try:
mode = path.stat().st_mode
except FileNotFoundError:
return "missing"
- except (OSError, ValueError, RuntimeError):
+ except OSError as exc:
+ return "symlink_loop" if exc.errno == errno.ELOOP else "unreadable"
+ except (ValueError, RuntimeError):
return "unreadable"
if stat.S_ISDIR(mode):
return "directory"
diff --git a/tests/conftest.py b/tests/conftest.py
index 34648369d..429b76471 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -586,13 +586,16 @@ def _rebind_runtime_roots_between_tests():
@pytest.fixture(autouse=True)
def _reset_custody_memo_between_tests():
- """The custody row memo is process-local and keyed by events-log path; a test
- that rewrites its log in place (``write_text``) or reuses a path must never
- inherit another test's consumed prefix (``delegate_custody_memo``)."""
+ """Isolate both custody caches: the row memo is keyed by events-log path,
+ while active custody is keyed only by run ID. Tests reuse both identities;
+ neither a consumed prefix nor a previous run's first-wins binding may leak."""
+ from ouroboros import delegate_custody
from ouroboros.delegate_custody_memo import reset_custody_memo
+ delegate_custody._CUSTODY.clear()
reset_custody_memo()
yield
+ delegate_custody._CUSTODY.clear()
reset_custody_memo()
diff --git a/tests/test_author_ui_kit_browser.py b/tests/test_author_ui_kit_browser.py
index 642cb10f3..b99e10ad9 100644
--- a/tests/test_author_ui_kit_browser.py
+++ b/tests/test_author_ui_kit_browser.py
@@ -196,7 +196,7 @@ def test_author_kit_authenticated_mount_and_lifetime(author_kit_server, tmp_path
assert route.evaluate("window.kitCspViolations") == []
assert module.evaluate("document.documentElement.dataset.theme") == "dark"
page.evaluate("() => window.ouroTheme.set('light')")
- module.wait_for_function("document.documentElement.dataset.theme === 'light'")
+ module.locator('html[data-theme="light"]').wait_for(state="attached")
assert page.evaluate(
"node => document.querySelector('[data-widget-key=\\\"module-old\\\"] iframe') === node",
module_node,
@@ -205,7 +205,7 @@ def test_author_kit_authenticated_mount_and_lifetime(author_kit_server, tmp_path
"getComputedStyle(document.querySelector('.ui-control')).backgroundColor"
) == "rgb(245, 246, 248)"
page.evaluate("() => window.ouroTheme.set('dark')")
- module.wait_for_function("document.documentElement.dataset.theme === 'dark'")
+ module.locator('html[data-theme="dark"]').wait_for(state="attached")
for frame in (module, route):
frame.get_by_role("button", name="Preview", exact=True).wait_for()
assert frame.get_by_label("Title", exact=True).input_value() == "My notes"
@@ -322,7 +322,7 @@ def test_author_kit_authenticated_mount_and_lifetime(author_kit_server, tmp_path
}""")
page.wait_for_function("window.handlers.size === 1")
page.evaluate("() => handlers.forEach(handler => handler({type:'ext:export_widget:tick', data:{value:'delivered'}}))")
- module.wait_for_function("document.getElementById('root').dataset.event === 'delivered'")
+ module.locator('#root[data-event="delivered"]').wait_for(state="attached")
with page.expect_download() as download:
module.get_by_role("button", name="Export example", exact=True).click()
assert Path(download.value.path()).read_text(encoding="utf-8") == "author kit export"
@@ -335,7 +335,7 @@ def test_author_kit_authenticated_mount_and_lifetime(author_kit_server, tmp_path
evidence = Path(os.environ.get("OUROBOROS_UI_EVIDENCE_OUT", str(tmp_path / "evidence")))
evidence.mkdir(parents=True, exist_ok=True)
page.evaluate("() => window.ouroTheme.set('light')")
- module.wait_for_function("document.documentElement.dataset.theme === 'light'")
+ module.locator('html[data-theme="light"]').wait_for(state="attached")
module.locator('body').screenshot(
path=str(evidence / f"author-kit-{browser_name}-module-light.png")
)
diff --git a/tests/test_child_drive_settlement.py b/tests/test_child_drive_settlement.py
index 9d537e740..30d7c40eb 100644
--- a/tests/test_child_drive_settlement.py
+++ b/tests/test_child_drive_settlement.py
@@ -117,7 +117,7 @@ def test_a_recorded_capture_whose_source_is_gone_stays_held_by_its_verified_cano
@pytest.mark.parametrize("material", ["child_result", "registration", "file", "directory", "mailbox", "receipts"])
-def test_unreadable_material_retains_the_drive(tmp_path, material):
+def test_unreadable_material_retains_the_drive(tmp_path, monkeypatch, material):
data, drive, record = _cancelled_with_capture(tmp_path)
store = artifacts.task_artifact_dir_path(drive, TASK)
if material == "child_result":
@@ -129,12 +129,26 @@ def test_unreadable_material_retains_the_drive(tmp_path, material):
elif material == "file":
extra = store / "notes.txt"
extra.write_text("mutable note", encoding="utf-8")
- extra.chmod(0)
+ original_open = Path.open
+
+ def unreadable_open(path, *args, **kwargs):
+ if path == extra:
+ raise PermissionError("file read denied")
+ return original_open(path, *args, **kwargs)
+
+ monkeypatch.setattr(Path, "open", unreadable_open)
expected = "child_artifact_store_unreadable"
elif material == "directory":
(store / "tree").mkdir()
(store / "tree" / "leaf.txt").write_text("leaf", encoding="utf-8")
- (store / "tree").chmod(0)
+ original_scandir = os.scandir
+
+ def unreadable_scandir(path):
+ if Path(path) == store / "tree":
+ raise PermissionError("directory read denied")
+ return original_scandir(path)
+
+ monkeypatch.setattr(os, "scandir", unreadable_scandir)
expected = "child_artifact_store_unreadable"
elif material == "mailbox":
mailbox = owner_mailbox._mailbox_path(drive, TASK)
@@ -144,16 +158,22 @@ def test_unreadable_material_retains_the_drive(tmp_path, material):
else:
verification_receipts_path(drive, TASK, create=True).write_text("{torn\n", encoding="utf-8")
expected = "verification_receipts_uncustodied"
- try:
- if os.name != "nt" and material in {"file", "directory"} and os.geteuid() == 0:
- pytest.skip("root reads unreadable files")
- outcome = _settle(data, drive)
- finally:
- for path in (store / "notes.txt", store / "tree"):
- if path.exists():
- path.chmod(0o755)
+ # chmod(0) does not deny Windows reads and is bypassed by POSIX root. Inject
+ # the actual read failure so retention and recovery are tested on every host.
+ outcome = _settle(data, drive)
assert outcome["status"] == "retained" and outcome["reason"] == expected, outcome
assert drive.is_dir() and not _canonical(data, record["name"]).exists()
+ if material in {"file", "directory"}:
+ if material == "file":
+ monkeypatch.setattr(Path, "open", original_open)
+ else:
+ monkeypatch.setattr(os, "scandir", original_scandir)
+ assert _settle(data, drive)["status"] == "removed"
+ assert not drive.exists()
+ extra_name = "notes.txt" if material == "file" else "tree/leaf.txt"
+ assert _canonical(data, extra_name).read_text(encoding="utf-8") == (
+ "mutable note" if material == "file" else "leaf"
+ )
def test_a_write_that_does_not_land_or_read_back_deletes_and_seals_nothing(tmp_path, monkeypatch):
diff --git a/tests/test_cybergym_dispatch.py b/tests/test_cybergym_dispatch.py
index b54c49a32..19e0fede9 100644
--- a/tests/test_cybergym_dispatch.py
+++ b/tests/test_cybergym_dispatch.py
@@ -443,11 +443,18 @@ def _transport_row(task_id: str) -> dict:
}
-def test_breaker_pauses_probes_and_resumes_instead_of_abandoning(tmp_path):
+def test_breaker_pauses_probes_and_resumes_instead_of_abandoning(tmp_path, monkeypatch):
"""full1507: three transport rows from a ~100 s stall, not a dead isolate."""
+ from concurrent.futures import ALL_COMPLETED, wait
+ from devtools.benchmarks.cybergym import cybergym_dispatch
from devtools.benchmarks.cybergym.cybergym_dispatch import run_dispatched
+ # This scenario delivers a whole failed wave before any later success can
+ # reset the consecutive-failure streak. Worker scheduling is not its oracle.
+ monkeypatch.setattr(cybergym_dispatch, "wait", lambda futures, **kwargs: wait(
+ futures, **{**kwargs, "return_when": ALL_COMPLETED}))
+
clock = _PausingClock()
probes: list[float] = []
# Gateway is "stalled" for the first two probes and answers on the third.
@@ -457,17 +464,16 @@ def test_breaker_pauses_probes_and_resumes_instead_of_abandoning(tmp_path):
probes.append(clock.now)
return next(probe_answers)
- outcomes = iter(["transport", "transport", "transport", "ok", "ok", "ok"])
+ tasks = [_Task(f"arvo:{index}") for index in range(1, 7)]
+ failed_task_ids = {task.task_id for task in tasks[:3]}
def run_one(task):
- if next(outcomes) == "ok":
+ if task.task_id not in failed_task_ids:
return {"task_id": task.task_id, "status": "completed"}
return _transport_row(task.task_id)
events: list[dict] = []
- tasks = [_Task(f"arvo:{index}") for index in range(1, 7)]
for workers in (1, 3):
- outcomes = iter(["transport", "transport", "transport", "ok", "ok", "ok"])
probe_answers = iter([False, False, True])
probes.clear()
events.clear()
@@ -486,6 +492,7 @@ def test_breaker_pauses_probes_and_resumes_instead_of_abandoning(tmp_path):
clock=clock.monotonic,
)
+ assert [row["task_id"] for row in rows] == [task.task_id for task in tasks]
assert [row["status"] for row in rows] == ["infra_failed"] * 3 + ["completed"] * 3
# Backoff schedule: first probe after 30 s, then +60 s, then +120 s.
assert probes == [30.0, 90.0, 210.0]
diff --git a/tests/test_evolution_commit_receipt.py b/tests/test_evolution_commit_receipt.py
index 0ea50aa3e..b412eb3b5 100644
--- a/tests/test_evolution_commit_receipt.py
+++ b/tests/test_evolution_commit_receipt.py
@@ -226,6 +226,7 @@ def test_rescue_link_uses_shared_campaign_cas_and_preserves_commit_receipt(
def test_commit_receipt_uses_campaign_sidecar_before_rescue(tmp_path, monkeypatch):
+ from ouroboros import platform_layer
from ouroboros.platform_layer import (
acquire_exclusive_file_lock,
release_exclusive_file_lock,
@@ -239,8 +240,20 @@ def test_commit_receipt_uses_campaign_sidecar_before_rescue(tmp_path, monkeypatc
lock_fd = acquire_exclusive_file_lock(lock_path, timeout_sec=1.0)
assert lock_fd is not None
done = threading.Event()
+ acquiring = threading.Event()
+ released = threading.Event()
result = {}
+ def _acquire_after_release(path, **kwargs):
+ if path == lock_path:
+ acquiring.set()
+ released.wait()
+ return acquire_exclusive_file_lock(path, **kwargs)
+
+ # Keep real contention, but start the acquisition timeout after the fixture
+ # releases its lock, as in the rescue interleaving test above.
+ monkeypatch.setattr(platform_layer, "acquire_exclusive_file_lock", _acquire_after_release)
+
def _record() -> None:
result.update(evolution_lifecycle.record_evolution_commit(
campaign["id"], tx["transaction_id"], tx["task_id"], "4" * 40,
@@ -250,11 +263,15 @@ def test_commit_receipt_uses_campaign_sidecar_before_rescue(tmp_path, monkeypatc
thread = threading.Thread(target=_record, daemon=True)
thread.start()
try:
+ assert acquire_exclusive_file_lock(lock_path, timeout_sec=0.001) is None
+ assert acquiring.wait(2.0) is True
assert done.wait(0.1) is False
finally:
release_exclusive_file_lock(lock_path, lock_fd)
+ released.set()
+ thread.join(timeout=2.0)
assert done.wait(2.0) is True
- thread.join(timeout=1.0)
+ assert not thread.is_alive()
assert result["ok"] is True
assert evolution_lifecycle._read_evolution_campaign()["active_transaction"][
"commit_receipt"
diff --git a/tests/test_headless_task_artifacts.py b/tests/test_headless_task_artifacts.py
index 64906ab5f..965f90cf4 100644
--- a/tests/test_headless_task_artifacts.py
+++ b/tests/test_headless_task_artifacts.py
@@ -17,6 +17,7 @@ from starlette.applications import Starlette
from starlette.routing import Route
from starlette.testclient import TestClient
+from ouroboros.gateway import task_archive
from ouroboros.gateway.tasks import (
api_task_artifact,
)
@@ -39,6 +40,16 @@ from tests._headless_cli_shared import ( # noqa: F401 (autouse fixture applies
)
+def _assert_file_response(response, content: bytes) -> None:
+ # Owner-approved #1297: platforms without confined opens return a typed 503.
+ if task_archive.CONFINED:
+ assert response.status_code == 200
+ assert response.content == content
+ else:
+ assert response.status_code == 503
+ assert response.json()["reason_code"] == "artifact_unavailable"
+
+
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
@@ -386,7 +397,7 @@ def test_task_artifact_endpoint_serves_only_declared_artifacts(tmp_path):
app.state.drive_root = data
client = TestClient(app)
- assert client.get("/api/tasks/task-artifact/artifacts/workspace.patch").text.startswith("diff --git")
+ _assert_file_response(client.get("/api/tasks/task-artifact/artifacts/workspace.patch"), b"diff --git a/a b/a\n")
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
@@ -415,8 +426,7 @@ def test_task_artifact_endpoint_serves_manifest_artifact_after_status_repair(tmp
response = TestClient(app).get("/api/tasks/orphaned/artifacts/report.html")
- assert response.status_code == 200
- assert response.text == "ok
"
+ _assert_file_response(response, b"ok
")
def test_task_artifact_endpoint_serves_child_drive_artifact_read_only_after_status_repair(tmp_path):
@@ -457,9 +467,8 @@ def test_task_artifact_endpoint_serves_child_drive_artifact_read_only_after_stat
response = TestClient(app).get("/api/tasks/childart/artifacts/report.html")
- # Two-root read: the task's own child store serves it; nothing is copied or created.
- assert response.status_code == 200
- assert response.text == "child
"
+ # The own child store is read when supported; neither outcome copies or creates it.
+ _assert_file_response(response, b"child
")
assert not task_artifacts_dir(data, "childart", create=False).exists()
@@ -664,8 +673,7 @@ def test_task_artifact_endpoint_serves_exact_chat_media_without_task_result(tmp_
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_file_response(response, b"photo-bytes")
assert collect_task_artifact_records(data, "ephemeral1") == []
assert client.get("/api/tasks/ephemeral1/artifacts/chat-media-bad.png").status_code == 404
diff --git a/tests/test_large_task_artifacts.py b/tests/test_large_task_artifacts.py
index 3faac8bf9..57503c868 100644
--- a/tests/test_large_task_artifacts.py
+++ b/tests/test_large_task_artifacts.py
@@ -629,7 +629,7 @@ def test_file_verification_does_not_run_on_the_asgi_loop(tmp_path, monkeypatch,
import uvicorn
from starlette.applications import Starlette
from starlette.routing import Route
- from ouroboros.gateway import tasks
+ from ouroboros.gateway import task_archive, tasks
from ouroboros.task_results import write_task_result
api_task_artifact = tasks.api_task_artifact
@@ -667,14 +667,24 @@ def test_file_verification_does_not_run_on_the_asgi_loop(tmp_path, monkeypatch,
async def request():
async with httpx.AsyncClient(timeout=10) as client:
reply = await client.request(method, f'http://127.0.0.1:{sock.getsockname()[1]}/api/tasks/download/artifacts/{record["name"]}')
- assert reply.status_code == 200, reply.text
- assert reply.content == (source.read_bytes() if method == 'GET' else b'')
+ if task_archive.CONFINED:
+ assert reply.status_code == 200, reply.text
+ assert reply.content == (source.read_bytes() if method == 'GET' else b'')
+ else:
+ assert reply.status_code == 503, reply.text
+ if method == 'GET':
+ assert reply.json()['reason_code'] == 'artifact_unavailable'
+ else:
+ assert reply.content == b''
asyncio.run(request())
assert bool(materialization_calls) is (not registered)
assert all(identity != thread.ident for identity in materialization_calls)
- if registered:
- assert len(calls) == 1, "registered downloads verify once per request"
- assert calls and all(identity != thread.ident for identity in calls), 'whole-file hashing ran synchronously on the ASGI event loop'
+ if task_archive.CONFINED:
+ if registered:
+ assert len(calls) == 1, "registered downloads verify once per request"
+ assert calls and all(identity != thread.ident for identity in calls), 'whole-file hashing ran synchronously on the ASGI event loop'
+ else:
+ assert calls == [], "unsupported platforms refuse before reading file bytes (#1297)"
finally:
server.should_exit = True
thread.join(10)
diff --git a/tests/test_net_transport_extra_ca.py b/tests/test_net_transport_extra_ca.py
index 6f4451933..cdb401ffb 100644
--- a/tests/test_net_transport_extra_ca.py
+++ b/tests/test_net_transport_extra_ca.py
@@ -45,11 +45,18 @@ def _throwaway_ca(tmp_path: pathlib.Path):
now = _dt.datetime.now(_dt.timezone.utc)
ca_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
ca_name = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, "Ouroboros throwaway test CA")])
+ # Python 3.13 enables strict X.509 verification: CA SKI/key usage and leaf AKI are required.
ca = (
x509.CertificateBuilder().subject_name(ca_name).issuer_name(ca_name)
.public_key(ca_key.public_key()).serial_number(x509.random_serial_number())
.not_valid_before(now - _dt.timedelta(days=1)).not_valid_after(now + _dt.timedelta(days=2))
.add_extension(x509.BasicConstraints(ca=True, path_length=None), critical=True)
+ .add_extension(x509.SubjectKeyIdentifier.from_public_key(ca_key.public_key()), critical=False)
+ .add_extension(x509.KeyUsage(
+ digital_signature=False, content_commitment=False, key_encipherment=False,
+ data_encipherment=False, key_agreement=False, key_cert_sign=True, crl_sign=True,
+ encipher_only=False, decipher_only=False,
+ ), critical=True)
.sign(ca_key, hashes.SHA256())
)
leaf_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
@@ -59,6 +66,7 @@ def _throwaway_ca(tmp_path: pathlib.Path):
.issuer_name(ca_name).public_key(leaf_key.public_key()).serial_number(x509.random_serial_number())
.not_valid_before(now - _dt.timedelta(days=1)).not_valid_after(now + _dt.timedelta(days=2))
.add_extension(x509.SubjectAlternativeName([x509.IPAddress(ipaddress.ip_address("127.0.0.1"))]), critical=False)
+ .add_extension(x509.AuthorityKeyIdentifier.from_issuer_public_key(ca_key.public_key()), critical=False)
.sign(ca_key, hashes.SHA256())
)
tmp_path.mkdir(parents=True, exist_ok=True)
diff --git a/tests/test_review_source_handles.py b/tests/test_review_source_handles.py
index 966d31a7e..14f37869a 100644
--- a/tests/test_review_source_handles.py
+++ b/tests/test_review_source_handles.py
@@ -9,6 +9,7 @@ from starlette.routing import Route
from starlette.testclient import TestClient
from ouroboros import artifacts, review_projection
+from ouroboros.gateway import task_archive
from ouroboros.gateway.tasks import api_task_artifact
from ouroboros.headless import copy_child_task_result, prepare_task_drive, remove_subagent_task_drive
from ouroboros.task_results import write_task_result
@@ -82,7 +83,12 @@ def test_source_download_is_bound_and_distinct_from_same_named_user_file(tmp_pat
app.state.drive_root = tmp_path
with TestClient(app) as client:
url = f"/api/tasks/applied/artifacts/{name}"
- assert client.get(url).content == b"user result"
+ artifact = client.get(url)
+ if task_archive.CONFINED:
+ assert artifact.status_code == 200 and artifact.content == b"user result"
+ else:
+ assert artifact.status_code == 503
+ assert artifact.json()["reason_code"] == "artifact_unavailable"
source = client.get(url, params={"source": ref["path"]})
assert source.status_code == 200
assert hashlib.sha256(source.content).hexdigest() == ref["sha256"]
diff --git a/tests/test_test_environment.py b/tests/test_test_environment.py
index f8d84054b..6a83b46df 100644
--- a/tests/test_test_environment.py
+++ b/tests/test_test_environment.py
@@ -294,7 +294,7 @@ _SESSION_PROBE = """
import json, os, pathlib, tempfile
def pytest_sessionstart(session):
- worker = os.environ.get("PYTEST_XDIST_WORKER", "controller")
+ worker = getattr(session.config, "workerinput", {}).get("workerid", "controller")
record = {"tmpdir": os.environ.get("TMPDIR", ""), "gettempdir": tempfile.gettempdir(),
"session_root": str(pathlib.Path(os.environ["OUROBOROS_DATA_DIR"]).parent)}
pathlib.Path(os.environ["SESSION_PROBE_OUT"], worker + ".json").write_text(json.dumps(record))
From 6e1e28abfa4015fb0c926021ed50611e07948b7d Mon Sep 17 00:00:00 2001
From: Ouroboros <311266734+ouroboros-agent@users.noreply.github.com>
Date: Sat, 26 Sep 2026 23:53:53 +0300
Subject: [PATCH 2/3] test: remove the retired duplicate probe from subagent
E2E
Keep strict replay consumption and every lineage, custody, cost and finalization assertion. Subagent admission no longer runs the semantic duplicate veto removed in 9dd3c4ee3.
---
tests/system_e2e/test_system_scenarios_w2.py | 9 +++------
1 file changed, 3 insertions(+), 6 deletions(-)
diff --git a/tests/system_e2e/test_system_scenarios_w2.py b/tests/system_e2e/test_system_scenarios_w2.py
index f279b7f25..ce3919c77 100644
--- a/tests/system_e2e/test_system_scenarios_w2.py
+++ b/tests/system_e2e/test_system_scenarios_w2.py
@@ -266,10 +266,9 @@ _CHILD_SHA_RE = re.compile(r"child_result_sha256[\"'=:\s]+([0-9a-f]{64})")
def _s6_slot_binder(body: dict) -> str:
- """Slot = wire model id × tool-bearing shape: the parent loop runs on
- ``mock-model`` with tools, the supervisor's semantic-duplicate probe runs on the
- light slot (same slug) WITHOUT tools, and the child runs on ``mock-child`` — so
- every fixture ordinal is deterministic even while parent and child overlap."""
+ """Bind parent ``mock-model`` and child ``mock-child`` by model and request shape.
+ Tool-less calls keep a separate slot, so an unexpected auxiliary model request
+ is a fixture miss even while parent and child overlap."""
return f"{body.get('model') or ''}|{'tools' if body.get('tools') else 'plain'}"
@@ -301,8 +300,6 @@ S6_FIXTURE = {
"objective": "List the repository root and report the entries you saw.",
"expected_output": "A short list of repository root entries.",
}},
- # The supervisor's admission duplicate-probe (light slot, tool-less).
- ("root", "mock-model|plain", 1): {"final": "No existing task duplicates this request."},
("root", "mock-model|tools", 2): _s6_wait_step,
("root", "mock-model|tools", 3): _s6_dispose_step,
("root", "mock-model|tools", 4): {"final": f"{S6_PARENT_MARKER}: child absorbed; done."},
From 4b34cd46213f5826b2a01eceb8a45ad4bb04e977 Mon Sep 17 00:00:00 2001
From: Ouroboros <311266734+ouroboros-agent@users.noreply.github.com>
Date: Sun, 27 Sep 2026 00:24:21 +0300
Subject: [PATCH 3/3] docs: use the permitted tracker reference for Windows
artifacts
---
docs/architecture/04-server-api-endpoints.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/architecture/04-server-api-endpoints.md b/docs/architecture/04-server-api-endpoints.md
index c8b17880c..22b583b52 100644
--- a/docs/architecture/04-server-api-endpoints.md
+++ b/docs/architecture/04-server-api-endpoints.md
@@ -85,7 +85,7 @@ Every `/api/files/*` operation resolves its requested path and refuses the opera
| GET | `/api/tasks/{task_id}` | `gateway.tasks.api_task_get` |
| GET | `/api/tasks/{task_id}/events` | `gateway.tasks.api_task_events` (legacy integer rank) |
| POST | `/api/tasks/{task_id}/events` | `gateway.tasks.api_task_events` (read-only v2 cursor) |
-| GET | `/api/tasks/{task_id}/artifacts/{name}` | `gateway.tasks.api_task_artifact` (the task's own stores via `task_archive`: a bare name is a top-level file, `?relpath=` a nested one, `?archive=` a directory ZIP; the detail's `artifact_archives` says what each ZIP holds. A row that records a digest is served only when its bytes still match it — a changed mutable file is 409 `artifact_identity_changed` naming the recorded digest, a failed capture 404 `artifact_unverified` — and the response says `x-ouroboros-artifact-identity: verified` or `unmeasured`; a ZIP member follows the same rule. Windows ordinary file/chat-media/ZIP downloads return HTTP 503 pending #1297; bound `?source=` review downloads remain available) |
+| GET | `/api/tasks/{task_id}/artifacts/{name}` | `gateway.tasks.api_task_artifact` (the task's own stores via `task_archive`: a bare name is a top-level file, `?relpath=` a nested one, `?archive=` a directory ZIP; the detail's `artifact_archives` says what each ZIP holds. A row that records a digest is served only when its bytes still match it — a changed mutable file is 409 `artifact_identity_changed` naming the recorded digest, a failed capture 404 `artifact_unverified` — and the response says `x-ouroboros-artifact-identity: verified` or `unmeasured`; a ZIP member follows the same rule. Windows ordinary file/chat-media/ZIP downloads return HTTP 503 pending confined opens (issue #1297); bound `?source=` review downloads remain available) |
| POST | `/api/tasks/{task_id}/cancel` | `gateway.tasks.api_task_cancel` |
| POST | `/api/tasks/{task_id}/hurry` | `gateway.tasks.api_task_hurry` |
| POST | `/api/tasks/{task_id}/resume` | `gateway.tasks.api_task_resume` |