mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-18 20:49:28 +00:00
search-with-proof group 4 + image: board, spend reservation, coordinator, search-task image
Board as immutable versioned blobs (coordinator sole writer); budget reserve/settle before model calls; per-task and run caps; coordinator loop over an injected executor; batch.py accepts a fixed allow-list of search variables; XSS browser oracle; ousast-search-task image (Python, Node/TypeScript, PHP, Java 17, sqlite, bwrap, chromium) built by CI. 103 tests on the host. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
6394ee473d
commit
4b38239f68
@@ -157,3 +157,21 @@ def test_bounded_lanes_oom_and_instrument_retry(tmp_path):
|
||||
|
||||
assert dispatch([item("fail")], workload(), tmp_path, {"ax": failed}, lanes=("ax",))
|
||||
assert attempts == [0, 1]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("step", ["reason", "explore", "verify"])
|
||||
def test_search_environment(step):
|
||||
it = item()
|
||||
it.extra_env = {"SEARCH_STEP": step, "SEARCH_ID": "search-1", "SEARCH_TASK_ID": "task-2"}
|
||||
urls = {"SPEC_URL": "https://files.example/spec.dat", "RESULT_URL": "https://files.example/result.dat"}
|
||||
doc = manifest("ousast-engine-search-1", workload(), it, urls)
|
||||
assert {"name": "SEARCH_STEP", "value": step} in doc["spec"]["env"]
|
||||
for key, value in [
|
||||
("SEARCH_STEP", "shell"),
|
||||
("SEARCH_ID", "https://secret"),
|
||||
("SEARCH_TASK_ID", "x\nTOKEN=y"),
|
||||
("MODEL_API_KEY", "secret"),
|
||||
]:
|
||||
it.extra_env = {key: value}
|
||||
with pytest.raises(ValueError):
|
||||
manifest("ousast-engine-search-1", workload(), it, urls)
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
from openultrasast.plane.memory import FileStore
|
||||
from openultrasast.search.board import Board
|
||||
|
||||
|
||||
def test_versions_provenance_claims_and_resume(tmp_path):
|
||||
store = FileStore(tmp_path)
|
||||
board = Board(store, "search-1", coordinator="host")
|
||||
first = board.commit(
|
||||
writer="host", facts=[dict(id="f1", text="sink", evidence_refs=["engine:1"], from_intents=[], step="triage", task_id="seed")]
|
||||
)
|
||||
second = board.commit(
|
||||
writer="host",
|
||||
intents=[dict(id="i1", description="trace input", from_facts=["f1"], status="open", claimant=None, step="reason", task_id="r1")],
|
||||
)
|
||||
assert first != second
|
||||
assert json.loads(store.get_blob(board.prefix, first))["intents"] == []
|
||||
assert Board.resume(store, "search-1", second, coordinator="host").state == board.state
|
||||
with pytest.raises(PermissionError):
|
||||
board.commit(writer="worker", hints=[])
|
||||
state = board.state
|
||||
state["facts"].clear()
|
||||
assert board.state["facts"]
|
||||
assert len(board.snapshot(max_bytes=256).encode()) <= 256
|
||||
with pytest.raises(ValueError):
|
||||
board.commit(writer="host", end_reason="clean")
|
||||
|
||||
|
||||
def test_bad_references_and_oversize_are_atomic(tmp_path):
|
||||
board = Board(FileStore(tmp_path), "one", coordinator="host", max_bytes=1024)
|
||||
before = board.head
|
||||
with pytest.raises(ValueError):
|
||||
board.commit(writer="host", facts=[dict(id="f", text="x", evidence_refs=[], from_intents=["missing"], step="explore", task_id="t")])
|
||||
with pytest.raises(ValueError):
|
||||
board.commit(writer="host", hints=[dict(id="h", text="x" * 2048, step="human", task_id="operator")])
|
||||
assert board.head == before
|
||||
@@ -0,0 +1,50 @@
|
||||
import html
|
||||
import shutil
|
||||
import subprocess
|
||||
|
||||
import pytest
|
||||
|
||||
from openultrasast.search.oracles import BrowserExecutor
|
||||
|
||||
|
||||
def test_browser_launch_failure_is_not_negative(monkeypatch):
|
||||
def fail(*args, **kwargs):
|
||||
return subprocess.CompletedProcess(args, 1, "", "failed")
|
||||
|
||||
monkeypatch.setattr(subprocess, "run", fail)
|
||||
with pytest.raises(OSError):
|
||||
BrowserExecutor(binary="/fake/chromium")('<script>alert("ousast-xss")</script>', "nonce")
|
||||
|
||||
|
||||
@pytest.mark.skipif(not (shutil.which("chromium") or shutil.which("chromium-browser")), reason="chromium not installed")
|
||||
def test_real_browser_executes_not_reflects():
|
||||
browser = BrowserExecutor()
|
||||
payload = '<script>alert("ousast-xss")</script>'
|
||||
try:
|
||||
observed = browser(payload, "nonce")
|
||||
except OSError as exc:
|
||||
# A present binary is insufficient on restricted hosts. Only known
|
||||
# host permission denials skip; ordinary browser failures remain failures.
|
||||
if "Operation not permitted" in str(exc) or "Permission denied" in str(exc):
|
||||
pytest.skip("host denies browser isolation/socket setup: " + str(exc))
|
||||
raise
|
||||
assert observed
|
||||
assert not browser(html.escape(payload), "nonce")
|
||||
assert not browser("<p>nonce</p>", "nonce")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("executed", [False, True])
|
||||
def test_marker_is_dom_attribute_not_source_text(monkeypatch, executed):
|
||||
import re
|
||||
from urllib.parse import unquote
|
||||
|
||||
def run(command, **kwargs):
|
||||
assert "--headless" in command and "--dump-dom" in command
|
||||
assert "--unshare-all" in command and "--no-sandbox" not in command
|
||||
document = unquote(command[-1].split(",", 1)[1])
|
||||
attr, value = re.search(r'setAttribute\("([^"]+)","([^"]+)"\)', document).groups()
|
||||
root = f'<html {attr}="{value}">' if executed else "<html>"
|
||||
return subprocess.CompletedProcess(command, 0, root + document + "</html>", "")
|
||||
|
||||
monkeypatch.setattr(subprocess, "run", run)
|
||||
assert BrowserExecutor(binary="/usr/bin/chromium")("<p>reflected nonce</p>", "nonce") is executed
|
||||
@@ -0,0 +1,58 @@
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
|
||||
import pytest
|
||||
|
||||
from openultrasast.plane.budget import BudgetExhausted, SpendBudget
|
||||
from openultrasast.search.budget import SearchBudget
|
||||
|
||||
|
||||
def test_reserve_settle_release():
|
||||
meter = SpendBudget(1)
|
||||
reservation = meter.reserve(0.7)
|
||||
with pytest.raises(BudgetExhausted):
|
||||
meter.reserve(0.4)
|
||||
reservation.settle(0.2)
|
||||
assert meter.spent == 0.2 and meter.reserved == 0
|
||||
other = meter.reserve(0.8)
|
||||
other.release()
|
||||
assert meter.reserved == 0
|
||||
with pytest.raises(ValueError):
|
||||
reservation.settle(0.1)
|
||||
for bad in [-1, float("nan"), float("inf")]:
|
||||
with pytest.raises(ValueError):
|
||||
meter.reserve(bad)
|
||||
|
||||
|
||||
def test_concurrent_reservations_cannot_overrun():
|
||||
meter = SpendBudget(1)
|
||||
|
||||
def reserve(_):
|
||||
try:
|
||||
return meter.reserve(0.3)
|
||||
except BudgetExhausted:
|
||||
return None
|
||||
|
||||
with ThreadPoolExecutor(max_workers=12) as pool:
|
||||
reservations = list(pool.map(reserve, range(20)))
|
||||
assert sum(r is not None for r in reservations) == 3
|
||||
assert meter.reserved <= 1
|
||||
|
||||
|
||||
def test_config_validation():
|
||||
assert SearchBudget().task_wall_seconds == 900
|
||||
for kwargs in [dict(retries=-1), dict(memory_bytes=0), dict(wall_seconds=float("nan"))]:
|
||||
with pytest.raises(ValueError):
|
||||
SearchBudget(**kwargs)
|
||||
|
||||
|
||||
def test_settlement_bound_and_release_then_readmission():
|
||||
meter = SpendBudget(0.5)
|
||||
reserved = meter.reserve(0.5)
|
||||
with pytest.raises(ValueError, match="exceeds reservation"):
|
||||
reserved.settle(0.6)
|
||||
assert meter.reserved == 0.5 and meter.spent == 0
|
||||
reserved.release()
|
||||
meter.reserve(0.5).settle(0.5)
|
||||
assert meter.spent == 0.5
|
||||
with pytest.raises(BudgetExhausted):
|
||||
meter.reserve(0.001)
|
||||
@@ -0,0 +1,166 @@
|
||||
import pytest
|
||||
|
||||
from openultrasast.plane.memory import FileStore
|
||||
from openultrasast.search.board import Board
|
||||
from openultrasast.search.budget import SearchBudget
|
||||
from openultrasast.search.coordinator import Coordinator
|
||||
|
||||
|
||||
def board(tmp_path):
|
||||
return Board(FileStore(tmp_path), "search-1", coordinator="host")
|
||||
|
||||
|
||||
def test_reason_explore_verify_and_resume(tmp_path):
|
||||
calls = []
|
||||
|
||||
def execute(task):
|
||||
calls.append(task)
|
||||
assert task.limits["memory_bytes"] > 0
|
||||
if task.step == "reason":
|
||||
return {"status": "ok", "cost_usd": 0.01, "intents": [{"description": "try input", "from_facts": []}]}
|
||||
if task.step == "explore":
|
||||
return {"status": "ok", "cost_usd": 0.02, "fact": {"text": "candidate", "evidence_refs": ["capture:1"], "demo": "demo:1"}}
|
||||
return {"status": "ok", "outcome": "demonstrated", "evidence_refs": ["verify:1"]}
|
||||
|
||||
b = board(tmp_path)
|
||||
result = Coordinator(b, execute).run()
|
||||
assert result["end_reason"] == "goal_met"
|
||||
assert [t.step for t in calls] == ["reason", "explore", "verify"]
|
||||
assert result["checkpoint"]["spent_usd"] == pytest.approx(0.03)
|
||||
resumed = Board.resume(b.store, "search-1", b.head, coordinator="host")
|
||||
assert Coordinator(resumed, execute).run() == result
|
||||
assert len(calls) == 3
|
||||
|
||||
|
||||
def test_reason_claim_cannot_prove_goal(tmp_path):
|
||||
result = Coordinator(board(tmp_path), lambda task: {"status": "ok", "cost_usd": 0, "complete": True, "facts": []}).run()
|
||||
assert result["end_reason"] == "exhausted"
|
||||
|
||||
|
||||
def test_refusal_before_dispatch_and_run_deadline(tmp_path):
|
||||
calls = []
|
||||
result = Coordinator(board(tmp_path), calls.append, budget=SearchBudget(spend_usd=0.01)).run()
|
||||
assert result["end_reason"] == "budget_spent" and calls == []
|
||||
times = iter([0, 100, 100, 100, 100])
|
||||
result = Coordinator(board(tmp_path / "two"), calls.append, budget=SearchBudget(wall_seconds=1), clock=lambda: next(times)).run()
|
||||
assert result["end_reason"] == "budget_spent" and calls == []
|
||||
|
||||
|
||||
def test_retry_fallback_bounded_and_cost_released(tmp_path):
|
||||
calls = []
|
||||
|
||||
def fail(task):
|
||||
calls.append(task.lane)
|
||||
return {"status": "sandbox_failure", "cost_usd": 0}
|
||||
|
||||
state = Coordinator(board(tmp_path), fail, budget=SearchBudget(retries=1)).run()
|
||||
assert calls == ["ax", "docker"]
|
||||
assert state["end_reason"] == "exhausted"
|
||||
assert state["checkpoint"]["retries"] == 1
|
||||
assert state["checkpoint"]["spent_usd"] == 0
|
||||
|
||||
|
||||
def test_claim_checkpoint_survives_interrupt_without_free_spend(tmp_path):
|
||||
b = board(tmp_path)
|
||||
|
||||
def interrupt(task):
|
||||
raise KeyboardInterrupt
|
||||
|
||||
with pytest.raises(KeyboardInterrupt):
|
||||
Coordinator(b, interrupt).run()
|
||||
checkpoint = b.head
|
||||
resumed = Board.resume(b.store, "search-1", checkpoint, coordinator="host")
|
||||
state = Coordinator(resumed, lambda task: {"status": "ok", "cost_usd": 0, "intents": []}).run()
|
||||
assert state["checkpoint"]["spent_usd"] == 0.05
|
||||
assert state["checkpoint"]["tasks"] == 2
|
||||
|
||||
|
||||
def test_invalid_fact_fails_closed_and_records_failure(tmp_path):
|
||||
def execute(task):
|
||||
if task.step == "reason":
|
||||
return {"status": "ok", "cost_usd": 0, "intents": [{"description": "x", "from_facts": []}]}
|
||||
return {"status": "ok", "cost_usd": 0, "fact": {"text": "unbacked"}}
|
||||
|
||||
state = Coordinator(board(tmp_path), execute, budget=SearchBudget(reason_rounds=1)).run()
|
||||
assert not state["facts"]
|
||||
assert state["intents"][0]["status"] == "concluded"
|
||||
assert state["intents"][0]["failure"] == "invalid_result"
|
||||
|
||||
|
||||
def test_cleanup_failure_halts_and_charges_unknown_spend(tmp_path):
|
||||
b = board(tmp_path)
|
||||
calls = []
|
||||
|
||||
def broken(task):
|
||||
calls.append(task)
|
||||
raise RuntimeError("AX cleanup failed; stop dispatching")
|
||||
|
||||
with pytest.raises(RuntimeError, match="cleanup"):
|
||||
Coordinator(b, broken).run()
|
||||
assert len(calls) == 1
|
||||
assert b.state["checkpoint"]["spent_usd"] == 0.05
|
||||
assert b.state["checkpoint"]["pending"]["id"] == calls[0].id
|
||||
|
||||
|
||||
def test_overclaimed_cost_stops_before_another_dispatch(tmp_path):
|
||||
calls = []
|
||||
|
||||
def overrun(task):
|
||||
calls.append(task)
|
||||
return {"status": "ok", "cost_usd": 99, "intents": []}
|
||||
|
||||
result = Coordinator(board(tmp_path), overrun).run()
|
||||
assert result["end_reason"] == "budget_spent"
|
||||
assert len(calls) == 1
|
||||
|
||||
|
||||
def test_claim_is_visible_in_worker_snapshot(tmp_path):
|
||||
import yaml
|
||||
|
||||
def execute(task):
|
||||
if task.step == "reason":
|
||||
return {"status": "ok", "cost_usd": 0, "intents": [{"description": "read", "from_facts": []}]}
|
||||
intent = yaml.safe_load(task.snapshot)["intents"][0]
|
||||
assert intent["status"] == "claimed" and intent["claimant"] == task.id
|
||||
return {"status": "execution_failure", "cost_usd": 0}
|
||||
|
||||
Coordinator(board(tmp_path), execute, budget=SearchBudget(reason_rounds=1)).run()
|
||||
|
||||
|
||||
def test_batch_adapter_offline_checkpoint_and_step_dispatch(tmp_path, monkeypatch):
|
||||
from dataclasses import asdict
|
||||
from pathlib import Path
|
||||
|
||||
monkeypatch.syspath_prepend(str(Path(__file__).resolve().parents[1]))
|
||||
from openultrasast.search.coordinator import BatchExecutor, SearchTask
|
||||
|
||||
calls = []
|
||||
|
||||
def lane(item, output, attempt):
|
||||
import json
|
||||
|
||||
spec = json.loads(item.inputs["SPEC_URL"])
|
||||
calls.append(spec)
|
||||
assert item.extra_env["SEARCH_STEP"] == spec["step"]
|
||||
return {"status": "ok", "cost_usd": 0, "intents": []}
|
||||
|
||||
adapter = BatchExecutor(
|
||||
image="registry/task@sha256:" + "a" * 64, command=("python3", "/worker.py"), output=tmp_path, lanes={"ax": lane}
|
||||
)
|
||||
for index, step in enumerate(("reason", "explore", "verify")):
|
||||
task = SearchTask(f"task-{index}", "search-1", step, "facts: []", {}, asdict(SearchBudget()))
|
||||
assert adapter(task)["status"] == "ok"
|
||||
assert adapter(task)["status"] == "ok"
|
||||
assert len(calls) == 3
|
||||
|
||||
|
||||
def test_invalid_identifiers_refused_before_paths(tmp_path):
|
||||
from dataclasses import asdict
|
||||
|
||||
from openultrasast.search.coordinator import SearchTask
|
||||
|
||||
for bad in ("../escape", "/tmp/escape", "bad\nNAME", ""):
|
||||
with pytest.raises(ValueError):
|
||||
Board(FileStore(tmp_path), bad, coordinator="host")
|
||||
with pytest.raises(ValueError):
|
||||
SearchTask(bad, "search-1", "reason", "", {}, asdict(SearchBudget()))
|
||||
@@ -0,0 +1,18 @@
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
|
||||
|
||||
def test_image_matches_runner_and_publishes_digest():
|
||||
engine = Path("plane/Dockerfile.engine-task").read_text()
|
||||
image = Path("plane/Dockerfile.search-task").read_text()
|
||||
runner = "FROM ghcr.io/norandom/ax-task-runner:v0.3.1"
|
||||
assert runner in engine and runner in image
|
||||
for package in ("python3", "nodejs", "node-typescript", "php-cli", "sqlite3", "bubblewrap", "util-linux", "chromium"):
|
||||
assert package in image
|
||||
assert "17-jdk" in image and "--no-install-recommends" in image
|
||||
assert "rm -rf /var/lib/apt/lists/" in image
|
||||
workflow_text = Path(".github/workflows/engine-image.yml").read_text()
|
||||
entries = yaml.safe_load(workflow_text)["jobs"]["image"]["strategy"]["matrix"]["include"]
|
||||
assert {"package": "ousast-search-task", "dockerfile": "plane/Dockerfile.search-task"} in entries
|
||||
assert "steps.build.outputs.digest" in workflow_text and "push: true" in workflow_text
|
||||
Reference in New Issue
Block a user