From 34da91603004995ef4dee8a2d9bc4df0aadc5daf Mon Sep 17 00:00:00 2001 From: Marius Ciepluch <11855163+norandom@users.noreply.github.com> Date: Thu, 1 Oct 2026 14:04:28 +0200 Subject: [PATCH] bench(learn): harvest signals for the decision engine (decision-engine task 5) `ousast plane harvest` (plane/harvest.py) writes two Runs over a label snapshot: verify a/b + agree on the verify-family units (both sides of each labelled pair as an opaque files-only Workspace, candidates outside the hunt's root, no declared sites; v1 and dev-php pins with repo-facts first) and model roles on the candidate files (pair excerpts grouped, one Task per population pin). Budgets sum under the ceiling by construction. New roles task template; the generator re-pins it. Spent by attribution: verify $2.54 (1961 calls, 150 units, 182 hunts per pass), roles $1.40 (1034 calls, 799 files, 0 unclassified chunks); DeepSeek balance 19.63 -> 18.23. Engine on the pair corpus (benchmarks/learn/engine_pairs.py, frozen e6ed785, one 3g container) is PARTIAL: 384 of 686 sides at commit, still running; unfinished sides stay missing. benchmarks/learn/build_features.py built 1712 static+plane records into the memory store; counts-only record under benchmarks/measurements/2026-10-01-decision-engine-harvest/. source/roles parts are `none`, not `failed`, for a language without a declaration pattern (C). ops/ax/README: Substrate golden-actor disk leak and the inline Workspace limit (~20 KB). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo --- .kiro/specs/learned-decision-engine/tasks.md | 6 +- benchmarks/learn/build_features.py | 363 +++++++++++++ benchmarks/learn/engine_pairs.py | 181 +++++++ .../measurements/2026-09-08-module-audit.json | 31 +- .../record.json | 477 ++++++++++++++++++ ops/ax/README.md | 7 + plane/tasks/agree.yaml | 2 +- plane/tasks/alerts.yaml | 2 +- plane/tasks/features.yaml | 2 +- plane/tasks/loop.yaml | 2 +- plane/tasks/remember.yaml | 2 +- plane/tasks/repo-facts.yaml | 2 +- plane/tasks/roles.yaml | 22 + plane/tasks/validation-46.yaml | 240 ++++----- plane/tasks/verify.yaml | 2 +- src/openultrasast/cli.py | 33 ++ src/openultrasast/learn/features.py | 9 +- src/openultrasast/plane/generate.py | 2 +- src/openultrasast/plane/harvest.py | 379 ++++++++++++++ tests/test_learn_roles.py | 12 + tests/test_plane_harvest.py | 100 ++++ 21 files changed, 1739 insertions(+), 137 deletions(-) create mode 100644 benchmarks/learn/build_features.py create mode 100644 benchmarks/learn/engine_pairs.py create mode 100644 benchmarks/measurements/2026-10-01-decision-engine-harvest/record.json create mode 100644 plane/tasks/roles.yaml create mode 100644 src/openultrasast/plane/harvest.py create mode 100644 tests/test_plane_harvest.py diff --git a/.kiro/specs/learned-decision-engine/tasks.md b/.kiro/specs/learned-decision-engine/tasks.md index 1a4c1e9..2229da6 100644 --- a/.kiro/specs/learned-decision-engine/tasks.md +++ b/.kiro/specs/learned-decision-engine/tasks.md @@ -28,10 +28,14 @@ the maintainer confirms its budget. commits (separate, never merged); a first label snapshot committed as counts only. - _Requirements: 2.1, 2.2, 2.3, 7.1_ -- [ ] 5. Harvest (needs budget go-ahead) +- [x] 5. Harvest (needs budget go-ahead) - A plane Run of verify a/b on the verify-family pairs and development cases (~$6, ceiling $10), model roles on the candidate files (ceiling $10), engine features for the pair corpus on the host (frozen source, one container at a time, background). + - Done 2026-10-01 (`benchmarks/measurements/2026-10-01-decision-engine-harvest/record.json`): verify $2.54 and roles + $1.40 by attribution. The engine part was PARTIAL at commit (384 of 686 pair sides recorded, the job still + running): unfinished sides are `missing` in the feature records; rebuild them with + `benchmarks/learn/build_features.py` once `benchmarks/learn/engine_pairs.py` has finished. - _Requirements: 1.1_ - [ ] 6. Train, calibrate, evaluate diff --git a/benchmarks/learn/build_features.py b/benchmarks/learn/build_features.py new file mode 100644 index 0000000..faa503a --- /dev/null +++ b/benchmarks/learn/build_features.py @@ -0,0 +1,363 @@ +"""Feature records for every labelled pin unit, from the harvested signals (learned-decision-engine task 5, step 3). + +Host only. For each labelled pin unit of a label snapshot (``ousast learn labels --out``; conditional rows excluded), +one ``static`` and one ``plane`` record per labelled (candidate, family), built with :mod:`openultrasast.learn.features` +from: + +- quick rules and deterministic roles, run here on the unit's tree: a pair side materialised from its excerpt, a + population or recipe pin exported from its clone or checkout (one at a time, deleted after, disk-guarded); +- the engine: ``benchmarks/learn/engine_pairs.py`` records for pairs, the recorded protocol scans + (``~/ousast-results/independent-v{1,2}/scans/--.json``) for populations; +- verify passes a/b and ``agree`` from the harvest Run (``harvest-verify``) and, for population v2, the recorded + ``validation-46`` Run; model roles from ``harvest-roles``; +- callers and entry-point distance on a repository (``not_applicable`` on a pair excerpt: no repository exists). + +An instrument that has not run for a unit yet (the engine job still running, a Run task not done) is recorded as +``failed`` with version ``missing:not-run`` -- null features, never zeros -- and counted as *missing*. Rows go to the +memory store (``OUSAST_MEMORY``, default ``~/ousast-results/plane/memory``) as kind ``features``; a rerun replaces +them by id. ``--counts`` writes the counts-only coverage record (no candidate, path or repository name). + +Usage:: + + python benchmarks/learn/build_features.py --labels labels-.jsonl --units /units.json --counts counts.json +""" + +from __future__ import annotations + +import argparse +import json +import shutil +import subprocess +import tempfile +from collections import Counter, defaultdict +from collections.abc import Mapping +from pathlib import Path +from typing import Any + +from openultrasast.findings import quick_scan_findings +from openultrasast.learn.features import ( + NONE, + NOT_APPLICABLE, + Part, + SourceIndex, + Vocabulary, + _callers_of, + _signals, + agree_part, + engine_part, + entry_distance, + entry_names, + facts_part, + function_of, + model_sinks_part, + quick_part, + record, + roles_part, + ruleset_digest, + source_part, + verify_part, +) +from openultrasast.learn.roles import RoleSet, from_model_roles, infer_for_checkout +from openultrasast.pairs import DEFAULT_CATALOG, _materialize_side, _targets, load_pair_catalog +from openultrasast.plane import memory +from openultrasast.plane.tasks.alerts import covers +from openultrasast.plane.tasks.alerts import engine_languages as _engine_languages +from openultrasast.plane.tasks.alerts import quick_languages as _quick_languages +from openultrasast.plane.tasks.repo_facts import GLOBAL +from openultrasast.preprocess import detect_language +from openultrasast.rank import rank_targets +from openultrasast.ruleset import DEFAULT_RULESET_DIR, load_ruleset + +RESULTS = Path.home() / "ousast-results" +MISSING = Part("failed", version="missing:not-run") +CONDITIONAL_SOURCES = ("benign_control", "assumed_benign") +PASSES = ("a", "b", "c") +QUICK_LANGUAGES = _quick_languages() +ENGINE_LANGUAGES = _engine_languages() + + +def jsonl(path: Path) -> list[dict[str, Any]]: + return [json.loads(x) for x in path.read_text(encoding="utf-8").splitlines() if x.strip()] if path.is_file() else [] + + +def read_json(path: Path) -> Any: + return json.loads(path.read_text(encoding="utf-8")) if path.is_file() else None + + +def done(run: Path, entry: str) -> bool: + state = (read_json(run / "state.json") or {}).get("tasks", {}) + return state.get(entry, {}).get("status") == "done" + + +class Signals: + """The harvested model signals: verify/agree per unit name, model roles per unit name.""" + + def __init__(self, verify_run: Path, roles_run: Path, v46: Path) -> None: + self.verify_run, self.roles_run, self.v46 = verify_run, roles_run, v46 + self.roles: dict[str, tuple[dict[str, Any], set[str]]] = {} + state = (read_json(roles_run / "state.json") or {}).get("tasks", {}) + for entry, info in state.items(): + if info.get("status") != "done": + continue + payload = read_json(roles_run / entry / "roles.json") or {} + classified = {str(r["path"]) for r in jsonl(roles_run / entry / "units.jsonl") if "error" not in r} + self._index_roles(entry.removesuffix("-roles"), payload, classified) + + def _index_roles(self, owner: str, payload: Mapping[str, Any], classified: set[str]) -> None: + grouped = any("/" in p and p.split("/", 1)[0].startswith("h-") for p in classified) + if not grouped: # a pin's Task: the paths are the repository's + self.roles[owner] = (dict(payload), classified) + return + for unit in {p.split("/", 1)[0] for p in classified}: + prefix = unit + "/" + + def strip(paths: Any, prefix: str = prefix) -> list[str]: + return [p.removeprefix(prefix) for p in paths if str(p).startswith(prefix)] + + rows = [ + r | {"path": r["path"].removeprefix(prefix)} + for r in payload.get("roles") or () + if str(r.get("path", "")).startswith(prefix) + ] + unit_payload = { + "model": payload.get("model"), + "roles": rows, + "unclassified_paths": strip(payload.get("unclassified_paths") or ()), + } + self.roles[unit] = (unit_payload, set(strip(classified))) + + def passes(self, base: Path, entry_stem: str) -> tuple[dict[str, list], dict[str, str | None], Any] | None: + if not (done(base, f"{entry_stem}-va") and done(base, f"{entry_stem}-vb")): + return None + passes = {p: jsonl(base / f"{entry_stem}-v{p}" / "units.jsonl") for p in PASSES} + models = {p: (read_json(base / f"{entry_stem}-v{p}" / "summary.json") or {}).get("model") for p in PASSES} + final = base / f"{entry_stem}-final" / "agreed.json" + agreed = read_json(final if final.is_file() else base / f"{entry_stem}-agree" / "agreed.json") + return passes, models, agreed + + +def final_of(agreed: Any, candidate: str) -> str | None: + if not agreed: + return None + disputed = {str(d["candidate"]) for d in agreed.get("disputed") or []} + for row in agreed.get("candidates") or []: + if str(row["candidate"]) == candidate: + return "agreed" if row.get("agreed") else "disputed" if candidate in disputed else "rejected" + return None + + +def plane_parts(signals: Signals, stems: list[tuple[Path, str]], roles_owner: str | None, candidate: str, line: int, + path: str, function: str, + verify_expected: bool) -> dict[str, Part]: # fmt: skip + """verify, agree, model_sinks of one candidate: ``stems`` are (run dir, entry stem) that may have asked it.""" + parts: dict[str, Part] = {"verify": NONE, "agree": NONE} + pending = False + for base, stem in stems: + got = signals.passes(base, stem) + if got is None: + pending = pending or (base == signals.verify_run and verify_expected) + continue + passes, models, agreed = got + part = verify_part(passes, candidate, line, "+".join(sorted({m for m in models.values() if m})) or None) + if part.state == "ran": + parts = {"verify": part, "agree": agree_part(final_of(agreed, candidate))} + break + else: + if pending: + parts = {"verify": MISSING, "agree": MISSING} + if roles_owner is None or roles_owner not in signals.roles: + parts["model_sinks"] = MISSING + else: + payload, classified = signals.roles[roles_owner] + if path not in classified: + parts["model_sinks"] = MISSING + else: + parts["model_sinks"] = model_sinks_part(from_model_roles(payload), path, function, str(payload.get("model") or "") or None) + return parts + + +def static_parts(root: Path, index: SourceIndex, quick: Mapping, engine: Mapping, engine_state: Part | None, engine_result: Mapping | None, + inferred: RoleSet, rules: Mapping, quick_version: str, path: str, function: str, family: str, repository: bool, + entries: tuple[set[str], set[str]] | None) -> dict[str, Part]: # fmt: skip + language = detect_language(Path(path)) or "other" + key = (path, function, family) + parts: dict[str, Part] = {} + parts["quick"] = quick_part(quick.get(key, []), rules, quick_version) if covers(QUICK_LANGUAGES, language) else NONE + if language not in ENGINE_LANGUAGES: + parts["engine"] = NONE + elif engine_state is not None: + parts["engine"] = engine_state + else: + result = engine_result or {} + questions, completed = int(result.get("questions") or 0), int(result.get("completed") or 0) + parts["engine"] = engine_part( + engine.get(key, []), completion=completed / questions if questions else None, degraded=bool(result.get("degradations")), + vocabulary=Vocabulary.load(language), version=str(result.get("image") or "recorded"), + ) # fmt: skip + lines = index.lines(path) + parts["source"] = source_part(lines, language, function) + parts["roles"] = roles_part(function_of(lines, path, language, function), inferred, language) + parts["delta"] = NOT_APPLICABLE + if repository and entries is not None: + distance = 0 if function == GLOBAL and path in entries[1] else entry_distance(function, entries[0], index.callers()) + parts["entry_points"] = Part("ran", {"facts.entry_distance": distance}) + parts["facts"] = facts_part(_callers_of(index, index.callers(), path, function), function, "host") + else: + parts["entry_points"] = NOT_APPLICABLE + parts["facts"] = NOT_APPLICABLE + return parts + + +def engine_for(result: Mapping[str, Any] | None) -> Part | None: + """None when the engine ran (its findings feed engine_part); else the state Part.""" + if result is None: + return MISSING + state = result.get("state", "ran" if result.get("questions") else "failed") + if state == "ran" and int(result.get("questions") or 0) > 0: + return None + if state == "none": + return NONE + return Part("failed", version=str(result.get("image") or "recorded")) + + +def export(clone: Path, pin: str, target: Path) -> bool: + target.mkdir(parents=True) + archive = target.parent / "pin.tar" + with archive.open("wb") as stream: + ok = subprocess.run(["git", "-C", str(clone), "archive", pin], stdout=stream, check=False).returncode == 0 + ok = ok and subprocess.run(["tar", "-xf", str(archive), "-C", str(target)], check=False).returncode == 0 + archive.unlink(missing_ok=True) + return ok and any(target.iterdir()) + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + parser.add_argument("--labels", type=Path, required=True) + parser.add_argument("--units", type=Path, required=True, help="units.json of the harvest plane") + parser.add_argument("--catalog", type=Path, default=DEFAULT_CATALOG) + parser.add_argument("--cache", type=Path, default=Path.home() / ".cache" / "openultrasast") + parser.add_argument("--engine", type=Path, default=RESULTS / "plane" / "harvest-engine") + parser.add_argument("--verify-run", type=Path, default=RESULTS / "plane" / "harvest-verify") + parser.add_argument("--roles-run", type=Path, default=RESULTS / "plane" / "harvest-roles") + parser.add_argument("--v46", type=Path, default=RESULTS / "plane" / "validation-46") + parser.add_argument("--scratch", type=Path, required=True) + parser.add_argument("--min-free-gb", type=float, default=1.2) + parser.add_argument("--counts", type=Path, required=True) + parser.add_argument("--store", help="OUSAST_MEMORY spec (default: the environment's)") + parser.add_argument("--limit", type=int, default=0, help="stop after this many targets (a test)") + parser.add_argument("--kinds", default="pairs,pin", help="which targets: pairs, pin (population and recipe pins)") + args = parser.parse_args() + + labels = [r for r in jsonl(args.labels) if r["unit"] == "pin" and r["source"] not in CONDITIONAL_SOURCES and not r.get("conditional")] + units = json.loads(args.units.read_text(encoding="utf-8")) + by_pair = {(u["ref"], u["side"]): n for n, u in units.items() if u["source"] == "pairs"} + by_pin: dict[tuple[str, str], list[str]] = defaultdict(list) + for n, u in units.items(): + if u["source"] != "pairs": + by_pin[(u["repo"], u["pin"])].append(n) + targets: dict[tuple[str, str, str], list[dict[str, Any]]] = defaultdict(list) + for row in labels: + key = ("pairs", row["source_ref"], row["pin_role"]) if row["source"] == "pairs" else ("pin", row["repo"], row["pin"]) + targets[key].append(row) + cases = {c.name: c for c in load_pair_catalog(args.catalog)} + loaded = load_ruleset(DEFAULT_RULESET_DIR) + rules = {r.rule_id: r for r in loaded} + quick_version = ruleset_digest(loaded) + signals = Signals(args.verify_run, args.roles_run, args.v46) + store = memory.open_store(args.store) + tally: Counter[tuple[str, ...]] = Counter() + coverage: dict[str, Counter[str]] = defaultdict(Counter) + skipped: Counter[str] = Counter() + args.scratch.mkdir(parents=True, exist_ok=True) + chosen = [(k, r) for k, r in sorted(targets.items()) if k[0] in args.kinds.split(",")] + for number, ((kind, ref, role_or_pin), rows) in enumerate(chosen): + if args.limit and number >= args.limit: + break + with tempfile.TemporaryDirectory(dir=args.scratch) as tmp: + root = Path(tmp) / "tree" + if kind == "pairs": + name = by_pair.get((ref, role_or_pin)) + if name is None or ref not in cases: + skipped["pair side not in the harvest units"] += len(rows) + continue + unit = units[name] + _materialize_side(root, cases[ref], side="vuln" if role_or_pin == "vulnerable" else "fixed") + engine_result = read_json(args.engine / f"{ref}--{'vuln' if role_or_pin == 'vulnerable' else 'fixed'}.json") + stems = [(args.verify_run, name)] + owner, repository, repo, pin = name, False, unit["repo"], unit["pin"] + else: + names = by_pin.get((ref, role_or_pin), []) + if not names: + skipped["pin not in the harvest units"] += len(rows) + continue + unit = units[names[0]] + repo, pin, repository = ref, role_or_pin, True + if shutil.disk_usage(args.scratch).free / 1e9 < args.min_free_gb: + skipped["disk guard: pin not exported"] += len(rows) + continue + if unit["source"] == "dev-php": + source_dir = args.cache / "repos" / unit["ref"].split(":")[0] / pin[:12] + shutil.copytree(source_dir, root, symlinks=True, ignore=shutil.ignore_patterns(".git")) + engine_result = None + else: + case_id = unit["ref"] + if not export(args.cache / "independent" / case_id, pin, root): + skipped["pin export failed"] += len(rows) + continue + split = {r["split"] for r in rows} | {unit["source"]} + population = "independent-v1" if "population-v1" in split else "independent-v2" + engine_result = read_json(RESULTS / population / "scans" / f"{case_id}--{rows[0]['pin_role']}.json") + stems = [(args.verify_run, n) for n in names] + [(args.v46, unit["ref"])] + owner = next( + (n for n in names if f"{n}-roles" in {e for e in (read_json(args.roles_run / "state.json") or {}).get("tasks", {})}), + names[0], + ) + index = SourceIndex(root) + targets_ = _targets(root) + findings = quick_scan_findings(root, targets_, rank_targets(targets_), None) + engine_state = engine_for(engine_result) + quick, engine = _signals(index, findings, engine_result if engine_state is None else None, rules) + inferred = infer_for_checkout(root) + entries = entry_names(root, {r["candidate"].partition("::")[0] for r in rows}) if repository else None + out_rows: list[dict[str, Any]] = [] + seen: set[tuple[str, str]] = set() + for row in rows: + candidate, family = row["candidate"], row["family"] + if (candidate, family) in seen: + continue + seen.add((candidate, family)) + path, _, function = candidate.partition("::") + cand_line = next((c[2] for n in ([owner] if kind == "pairs" else by_pin[(repo, pin)]) for c in units[n]["candidates"] + if f"{c[0]}::{c[1]}" == candidate), None) # fmt: skip + parts = static_parts(root, index, quick, engine, engine_state, engine_result, inferred, rules, quick_version, path, + function, family, repository, entries) # fmt: skip + language = detect_language(Path(path)) or "other" + verify_expected = any(units[n]["family"] == family for n in ([owner] if kind == "pairs" else by_pin[(repo, pin)])) + plane = {**parts, **plane_parts(signals, stems, owner, candidate, cand_line or 1, path, function, verify_expected)} + for profile, use in (("static", parts), ("plane", plane)): + rec = record(candidate, family, language, profile, use) + out_rows.append({ + "id": memory.row_id("features", "harvest", owner, f"{candidate}:{family}:{profile}"), "kind": "features", + "repo": memory.repo_key(repo), "pin": pin, "run": "harvest", "task": owner, "population": row["source"], + "split": row["split"], "image": str((engine_result or {}).get("image") or "none"), **rec, + }) # fmt: skip + tally[(profile, family)] += 1 + if profile == "plane": + for inst, info in rec["instruments"].items(): + state = "missing" if info.get("version") == "missing:not-run" else info["state"] + coverage[f"{family}:{inst}"][state] += 1 + store.ingest_rows("harvest", f"features:{owner}", out_rows) + print(json.dumps({"target": owner, "rows": len(out_rows)}), flush=True) + counts = { + "records": {f"{p}:{f}": n for (p, f), n in sorted(tally.items())}, + "instrument_states_plane": {k: dict(sorted(v.items())) for k, v in sorted(coverage.items())}, + "skipped_label_rows": dict(skipped), + "store": store.describe(), + } + args.counts.write_text(json.dumps(counts, indent=1, sort_keys=True) + "\n", encoding="utf-8") + print(json.dumps({"records": sum(tally.values()), "skipped": dict(skipped)})) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/benchmarks/learn/engine_pairs.py b/benchmarks/learn/engine_pairs.py new file mode 100644 index 0000000..6d55536 --- /dev/null +++ b/benchmarks/learn/engine_pairs.py @@ -0,0 +1,181 @@ +"""Engine signals for the labelled pair corpus, on the host (learned-decision-engine task 5, design "Cost on the 7 GB host"). + +Per labelled pair (``ousast learn labels`` rows of source ``pairs``) and side (``vuln``, ``fixed``): the side's +excerpt -- with the documents that register it -- is materialised into a temporary directory, and the Joern engine +runs on it in ``openultrasast:dev`` (``benchmarks/push/finding_dump.py`` from a FROZEN source export, ``--network +none --memory 3g``, the pair's labelled families, ONE container at a time). The result is one JSON per side under +``--out`` (``--.json``), keyed by the side's content sha256 and the engine image id; a rerun skips sides +already recorded, so the job resumes where it stopped. + +The instrument must have read its input (:func:`openultrasast.plane.engine_alerts.read_record`): no result file, no +``read N files`` line or ``questions == 0`` is recorded as ``state: failed`` with the reason (retried once), never as +zero findings. A side whose language the engine has no model for is ``state: none`` and is not run. + +Guards: free disk is checked before every side and every 60 s while a container runs; below ``--min-free-gb`` the +container is killed by its name, the side is left unrecorded (``missing``) and the job stops. A file ``STOP`` in +``--out`` stops the job cleanly between sides. Progress lines go to ``--out/progress.jsonl``. + +Usage:: + + python benchmarks/learn/engine_pairs.py --labels labels-.jsonl --source --out +""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import re +import shutil +import subprocess +import sys +import tempfile +import threading +from pathlib import Path + +from openultrasast.pairs import DEFAULT_CATALOG, _materialize_side, load_pair_catalog +from openultrasast.plane.engine_alerts import EngineAlertsError, docker_command, frozen_source, read_record +from openultrasast.plane.tasks.alerts import engine_languages +from openultrasast.preprocess import detect_language + +SIDES = ("vuln", "fixed") +_OFFERED = re.compile(r"^read (\d+) files, \d+ bytes; (\d+) regions offer", re.MULTILINE) + + +def free_gb(path: Path) -> float: + return shutil.disk_usage(path).free / 1e9 + + +def units(labels: Path, catalog: Path) -> list[tuple[object, str, tuple[str, ...]]]: + """(pair case, side, families) for every labelled pair, in name order.""" + families: dict[str, set[str]] = {} + for line in labels.read_text(encoding="utf-8").splitlines(): + row = json.loads(line) + if row["source"] == "pairs" and row["unit"] == "pin" and row["family"] != "unknown": + families.setdefault(row["source_ref"], set()).add(row["family"]) + cases = {case.name: case for case in load_pair_catalog(catalog)} + return [(cases[name], side, tuple(sorted(families[name]))) for name in sorted(families) for side in SIDES] + + +def image_id(image: str) -> str: + done = subprocess.run(["docker", "image", "inspect", "--format", "{{.Id}}", image], capture_output=True, text=True, check=False) + if done.returncode != 0 or not done.stdout.strip(): + raise SystemExit(f"no engine image {image}") + return done.stdout.strip() + + +def run_side( + command: list[str], name: str, timeout: float, out: Path, min_free: float +) -> tuple[subprocess.CompletedProcess[str] | None, str]: + """Run one container; a watchdog kills it by name when free disk drops below ``min_free`` GB.""" + tripped: list[str] = [] + done = threading.Event() + + def watch() -> None: + while not done.wait(60.0): + if free_gb(out) < min_free: + tripped.append(f"free disk {free_gb(out):.2f} GB < {min_free} GB") + subprocess.run(["docker", "kill", name], capture_output=True, check=False) + return + + watcher = threading.Thread(target=watch, daemon=True) + watcher.start() + try: + completed = subprocess.run(command, capture_output=True, text=True, timeout=timeout, check=False) + except subprocess.TimeoutExpired: + subprocess.run(["docker", "kill", name], capture_output=True, check=False) + completed = None + done.set() + return completed, (tripped[0] if tripped else "") + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + parser.add_argument("--labels", type=Path, required=True) + parser.add_argument("--catalog", type=Path, default=DEFAULT_CATALOG) + parser.add_argument("--source", type=Path, required=True, help="frozen export: git archive HEAD src benchmarks/push/finding_dump.py") + parser.add_argument("--out", type=Path, required=True) + parser.add_argument("--image", default="openultrasast:dev") + parser.add_argument("--deadline", type=float, default=600.0) + parser.add_argument("--min-free-gb", type=float, default=1.0) + parser.add_argument("--limit", type=int, default=0, help="stop after this many sides were run (0: all)") + args = parser.parse_args() + source = frozen_source(args.source) + args.out.mkdir(parents=True, exist_ok=True) + image = image_id(args.image) + covered = engine_languages() + progress = (args.out / "progress.jsonl").open("a", encoding="utf-8") + ran = 0 + for case, side, families in units(args.labels, args.catalog): + result_path = args.out / f"{case.name}--{side}.json" + if result_path.is_file(): + continue + if (args.out / "STOP").exists() or (args.limit and ran >= args.limit): + print("stopped between sides", flush=True) + return 0 + if free_gb(args.out) < args.min_free_gb: + print(f"disk guard: {free_gb(args.out):.2f} GB free; stopping", flush=True) + return 4 + excerpt = case.vuln_file if side == "vuln" else case.fixed_file + if not excerpt.is_file(): + entry = {"state": "failed", "reason": "excerpt not materialised"} + else: + entry = {"sha256": hashlib.sha256(excerpt.read_bytes()).hexdigest()} + language = detect_language(Path(case.relpath)) or "other" + if language not in covered: + entry.update(state="none", reason=f"no engine model for {language}") + else: + entry.update(_scan(case, side, families, source, args, image)) + ran += 1 + if entry.get("state") == "missing": + print(json.dumps({"pair": case.name, "side": side, **entry}), flush=True) + return 4 + record = {"pair": case.name, "side": side, "families": list(families), "image": image, **entry} + result_path.write_text(json.dumps(record, indent=1, sort_keys=True) + "\n", encoding="utf-8") + line = {k: record.get(k) for k in ("pair", "side", "state", "questions", "completed", "seconds", "reason")} + line["findings"] = len(record.get("findings") or []) + progress.write(json.dumps(line) + "\n") + progress.flush() + print(json.dumps(line), flush=True) + return 0 + + +def _scan(case: object, side: str, families: tuple[str, ...], source: Path, args: argparse.Namespace, image: str) -> dict: + reason = "" + for attempt in (1, 2): + with tempfile.TemporaryDirectory(prefix="ep-", dir=args.out) as tmp: + checkout = _materialize_side(Path(tmp) / "case", case, side=side) + out_dir = Path(tmp) / "out" + out_dir.mkdir() + name = f"ousast-ep-{hashlib.sha1(f'{case.name}{side}'.encode()).hexdigest()[:12]}" + command = docker_command(source, checkout, out_dir, "result", ",".join(families), args.deadline, args.image) + command[2:2] = ["--name", name] + completed, tripped = run_side(command, name, args.deadline + 300, args.out, args.min_free_gb) + if tripped: + return {"state": "missing", "reason": tripped} + if completed is None: + reason = f"timeout after {args.deadline + 300:.0f}s" + continue + log = (completed.stdout or "") + "\n" + (completed.stderr or "") + try: + record = read_record(out_dir / "result.json", log, completed.returncode) + except EngineAlertsError as exc: + reason = f"attempt {attempt}: {exc}"[:600] + offered = _OFFERED.search(log) + result = out_dir / "result.json" + if offered is not None and int(offered.group(1)) > 0 and result.is_file(): + # It read its input and finished without asking: deterministic (a retry asks nothing either). Still + # `failed` by the design's rule (questions == 0 is never zero findings); the reason keeps it apart + # from a launch failure, because on a fixed side "no question" may mean "no sink left". + recorded = json.loads(result.read_text(encoding="utf-8")) + regions = int(offered.group(2)) + why = "no region offers the families" if regions == 0 else f"no question asked for {regions} regions" + return {"state": "failed", "reason": why, "files": int(offered.group(1)), "regions": regions, + "seconds": recorded.get("seconds"), "degradations": recorded.get("degradations")} # fmt: skip + continue + return {"state": "ran", **{k: v for k, v in record.items() if k != "root"}} + return {"state": "failed", "reason": reason} + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/benchmarks/measurements/2026-09-08-module-audit.json b/benchmarks/measurements/2026-09-08-module-audit.json index 14d1d90..318aa90 100644 --- a/benchmarks/measurements/2026-09-08-module-audit.json +++ b/benchmarks/measurements/2026-09-08-module-audit.json @@ -1,7 +1,7 @@ { - "modules": 143, + "modules": 144, "counts": { - "load_bearing": 136, + "load_bearing": 137, "standalone_capability": 7 }, "orphaned": [], @@ -354,6 +354,7 @@ "cli", "learn.features", "learn.roles", + "plane.harvest", "plane.tasks.features", "plane.tasks.remember", "plane.tasks.roles" @@ -372,13 +373,14 @@ "classification": "load_bearing", "reason": "reachable from gate, map_gate, pair_gate or the CLI", "importers": [ - "cli" + "cli", + "plane.harvest" ] }, { "module": "learn.roles", "classification": "load_bearing", - "reason": "imported by learn.features, plane.tasks.features, plane.tasks.roles", + "reason": "reachable from gate, map_gate, pair_gate or the CLI", "importers": [ "learn.features", "plane.tasks.features", @@ -709,6 +711,7 @@ "learn.labels", "model.candidates", "pair_gate", + "plane.harvest", "plane.tasks.loop" ] }, @@ -722,6 +725,7 @@ "plane.egress", "plane.engine_alerts", "plane.generate", + "plane.harvest", "plane.memory", "plane.reconciler", "plane.router", @@ -769,6 +773,15 @@ "module": "plane.generate", "classification": "load_bearing", "reason": "ai-service-plane task 7: an increment's Workspaces, per-case Tasks and Run from a recorded set", + "importers": [ + "cli", + "plane.harvest" + ] + }, + { + "module": "plane.harvest", + "classification": "load_bearing", + "reason": "reachable from gate, map_gate, pair_gate or the CLI", "importers": [ "cli" ] @@ -780,6 +793,7 @@ "importers": [ "plane.egress", "plane.generate", + "plane.harvest", "plane.reconciler", "plane.router", "plane.runner", @@ -803,6 +817,7 @@ "reason": "ai-service-plane Req 3.1-3.5, 7.3: ax-backed Run execution, status attribution and doctor; `ousast plane`", "importers": [ "plane.generate", + "plane.harvest", "plane.memory", "plane.workspaces" ] @@ -833,6 +848,7 @@ "learn.roles", "plane.engine_alerts", "plane.generate", + "plane.harvest", "plane.memory", "plane.tasks.agree", "plane.tasks.alerts", @@ -888,6 +904,7 @@ "learn.labels", "learn.roles", "plane.generate", + "plane.harvest", "plane.tasks.alerts", "plane.tasks.features", "plane.tasks.remember", @@ -898,7 +915,9 @@ "module": "plane.tasks.roles", "classification": "load_bearing", "reason": "learned-decision-engine Req 8.1: model role classification per file chunk (model_sinks.py moved in); its Run template comes with the harvest (task 5)", - "importers": [] + "importers": [ + "plane.harvest" + ] }, { "module": "plane.tasks.verify", @@ -906,6 +925,7 @@ "reason": "ai-service-plane Req 5.3/2.1-2.3/7.2: the batched hunt with known callers, metered and resumable per file hunt", "importers": [ "plane.generate", + "plane.harvest", "plane.tasks.agree", "plane.tasks.roles" ] @@ -966,6 +986,7 @@ "model.partitions", "model.scan", "pairs", + "plane.harvest", "plane.tasks.alerts", "plane.tasks.features", "plane.tasks.repo_facts", diff --git a/benchmarks/measurements/2026-10-01-decision-engine-harvest/record.json b/benchmarks/measurements/2026-10-01-decision-engine-harvest/record.json new file mode 100644 index 0000000..6227238 --- /dev/null +++ b/benchmarks/measurements/2026-10-01-decision-engine-harvest/record.json @@ -0,0 +1,477 @@ +{ + "balance_usd": { + "after_roles": 18.23, + "after_verify": 18.75, + "before": 19.63 + }, + "ceilings_usd": { + "roles": 10.0, + "verify": 10.0 + }, + "created": "2026-10-01", + "engine": { + "frozen_source_commit": "e6ed7854bb8ef17d6a4c39971ea253b4e385af23", + "image": "openultrasast:dev sha256:e6f8dc6cc7547f7118bd058687de676e0fcf741799d1d3839b3e8648ce190fbb", + "pair_sides_total": 686, + "population_engine": "recorded protocol scans of independent-v1/v2 (vulnerable and fixed pins); dev-php not run (missing)", + "sides_missing": 302, + "sides_recorded": { + "failed (read its input, asked no question)": 286, + "none": 6, + "ran": 92 + }, + "status": "running in the background at commit time; unfinished sides are `missing` in the records, never zero" + }, + "features": { + "instrument_states_plane": { + "access_control:agree": { + "none": 26 + }, + "access_control:delta": { + "not_applicable": 26 + }, + "access_control:engine": { + "missing": 6, + "ran": 20 + }, + "access_control:entry_points": { + "not_applicable": 26 + }, + "access_control:facts": { + "not_applicable": 26 + }, + "access_control:language": { + "ran": 26 + }, + "access_control:model_sinks": { + "ran": 26 + }, + "access_control:quick": { + "ran": 26 + }, + "access_control:roles": { + "failed": 13, + "ran": 13 + }, + "access_control:source": { + "failed": 13, + "ran": 13 + }, + "access_control:verify": { + "none": 26 + }, + "config_secrets:agree": { + "none": 4, + "ran": 31 + }, + "config_secrets:delta": { + "not_applicable": 35 + }, + "config_secrets:engine": { + "missing": 4, + "none": 23, + "ran": 8 + }, + "config_secrets:entry_points": { + "not_applicable": 8, + "ran": 27 + }, + "config_secrets:facts": { + "not_applicable": 8, + "ran": 27 + }, + "config_secrets:language": { + "ran": 35 + }, + "config_secrets:model_sinks": { + "ran": 35 + }, + "config_secrets:quick": { + "ran": 35 + }, + "config_secrets:roles": { + "failed": 14, + "ran": 21 + }, + "config_secrets:source": { + "failed": 4, + "ran": 31 + }, + "config_secrets:verify": { + "none": 4, + "ran": 31 + }, + "deserialization:agree": { + "none": 40 + }, + "deserialization:delta": { + "not_applicable": 40 + }, + "deserialization:engine": { + "failed": 21, + "ran": 19 + }, + "deserialization:entry_points": { + "not_applicable": 40 + }, + "deserialization:facts": { + "not_applicable": 40 + }, + "deserialization:language": { + "ran": 40 + }, + "deserialization:model_sinks": { + "ran": 40 + }, + "deserialization:quick": { + "ran": 40 + }, + "deserialization:roles": { + "failed": 20, + "ran": 20 + }, + "deserialization:source": { + "failed": 20, + "ran": 20 + }, + "deserialization:verify": { + "none": 40 + }, + "injection:agree": { + "missing": 4, + "none": 25, + "ran": 139 + }, + "injection:delta": { + "not_applicable": 168 + }, + "injection:engine": { + "failed": 22, + "missing": 78, + "none": 16, + "ran": 52 + }, + "injection:entry_points": { + "not_applicable": 70, + "ran": 98 + }, + "injection:facts": { + "not_applicable": 70, + "ran": 98 + }, + "injection:language": { + "ran": 168 + }, + "injection:model_sinks": { + "missing": 4, + "ran": 164 + }, + "injection:quick": { + "none": 2, + "ran": 166 + }, + "injection:roles": { + "failed": 44, + "none": 2, + "ran": 122 + }, + "injection:source": { + "failed": 36, + "none": 2, + "ran": 130 + }, + "injection:verify": { + "missing": 4, + "none": 25, + "ran": 139 + }, + "memory:agree": { + "none": 346 + }, + "memory:delta": { + "not_applicable": 346 + }, + "memory:engine": { + "failed": 116, + "missing": 218, + "none": 6, + "ran": 6 + }, + "memory:entry_points": { + "not_applicable": 346 + }, + "memory:facts": { + "not_applicable": 346 + }, + "memory:language": { + "ran": 346 + }, + "memory:model_sinks": { + "missing": 4, + "ran": 342 + }, + "memory:quick": { + "none": 4, + "ran": 342 + }, + "memory:roles": { + "none": 346 + }, + "memory:source": { + "none": 346 + }, + "memory:verify": { + "none": 346 + }, + "output_encoding:agree": { + "none": 112 + }, + "output_encoding:delta": { + "not_applicable": 112 + }, + "output_encoding:engine": { + "failed": 24, + "missing": 82, + "ran": 6 + }, + "output_encoding:entry_points": { + "not_applicable": 112 + }, + "output_encoding:facts": { + "not_applicable": 112 + }, + "output_encoding:language": { + "ran": 112 + }, + "output_encoding:model_sinks": { + "ran": 112 + }, + "output_encoding:quick": { + "ran": 112 + }, + "output_encoding:roles": { + "failed": 16, + "ran": 96 + }, + "output_encoding:source": { + "failed": 16, + "ran": 96 + }, + "output_encoding:verify": { + "none": 112 + }, + "path:agree": { + "none": 29 + }, + "path:delta": { + "not_applicable": 29 + }, + "path:engine": { + "failed": 22, + "missing": 5, + "ran": 2 + }, + "path:entry_points": { + "not_applicable": 28, + "ran": 1 + }, + "path:facts": { + "not_applicable": 28, + "ran": 1 + }, + "path:language": { + "ran": 29 + }, + "path:model_sinks": { + "ran": 29 + }, + "path:quick": { + "ran": 29 + }, + "path:roles": { + "failed": 15, + "ran": 14 + }, + "path:source": { + "failed": 15, + "ran": 14 + }, + "path:verify": { + "none": 29 + }, + "unknown:agree": { + "none": 4 + }, + "unknown:delta": { + "not_applicable": 4 + }, + "unknown:engine": { + "missing": 2, + "none": 2 + }, + "unknown:entry_points": { + "not_applicable": 4 + }, + "unknown:facts": { + "not_applicable": 4 + }, + "unknown:language": { + "ran": 4 + }, + "unknown:model_sinks": { + "ran": 4 + }, + "unknown:quick": { + "ran": 4 + }, + "unknown:roles": { + "none": 4 + }, + "unknown:source": { + "none": 4 + }, + "unknown:verify": { + "none": 4 + }, + "untrusted_destination:agree": { + "none": 11, + "ran": 85 + }, + "untrusted_destination:delta": { + "not_applicable": 96 + }, + "untrusted_destination:engine": { + "failed": 38, + "missing": 12, + "none": 17, + "ran": 29 + }, + "untrusted_destination:entry_points": { + "not_applicable": 52, + "ran": 44 + }, + "untrusted_destination:facts": { + "not_applicable": 52, + "ran": 44 + }, + "untrusted_destination:language": { + "ran": 96 + }, + "untrusted_destination:model_sinks": { + "ran": 96 + }, + "untrusted_destination:quick": { + "ran": 96 + }, + "untrusted_destination:roles": { + "failed": 39, + "ran": 57 + }, + "untrusted_destination:source": { + "failed": 33, + "ran": 63 + }, + "untrusted_destination:verify": { + "none": 11, + "ran": 85 + } + }, + "records": { + "plane:access_control": 26, + "plane:config_secrets": 35, + "plane:deserialization": 40, + "plane:injection": 168, + "plane:memory": 346, + "plane:output_encoding": 112, + "plane:path": 29, + "plane:unknown": 4, + "plane:untrusted_destination": 96, + "static:access_control": 26, + "static:config_secrets": 35, + "static:deserialization": 40, + "static:injection": 168, + "static:memory": 346, + "static:output_encoding": 112, + "static:path": 29, + "static:unknown": 4, + "static:untrusted_destination": 96 + }, + "skipped_label_rows": {}, + "store": "file://~/ousast-results/plane/memory (kind features, run harvest)" + }, + "host_peaks": { + "disk_free_mb_min": 960, + "disk_incident": "Substrate kept every golden actor (~24 MB each, deletion failing on a missing snapshot bucket); free disk fell to 0.95 GB, runs were stopped, the bucket created and orphaned actor directories removed", + "mem_available_mb_min": 1990 + }, + "label_snapshot_sha256": "18337014d8a5c8df67e06f4e9d8e72c1fb8f82b1ed1b9a18836ab37e6cf4fdc9", + "note": "Harvest of the learned decision engine (task 5), COUNTS ONLY. Spend is the plane's attribution table (`ousast plane status`, task meters priced at plane/models/deepseek-flash.yaml); the DeepSeek account moved less (balance below). Feature records are in the local memory store (kind `features`, run `harvest`); the label rows, the units index and the manifests name functions and stay local.", + "roles": { + "attribution": { + "cache_hit_tokens": null, + "calls": 1034, + "output_tokens": null, + "prompt_tokens": null, + "usd": 1.4036 + }, + "cache_hit_tokens": 329600, + "chunks": 1034, + "files": 799, + "output_tokens": 370155, + "prompt_tokens": 2398669, + "task_budget_sum_usd": 4.917, + "tasks_done": 151, + "unclassified_chunks": 0 + }, + "runner_image": "localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884", + "verify": { + "attribution": { + "cache_hit_tokens": 6275060, + "calls": 1961, + "output_tokens": 306941, + "prompt_tokens": 10921167, + "usd": 2.5373 + }, + "file_hunts_done": 364, + "flagged_by_pass_units": { + "dev-php:vulnerable": { + "both": 2 + }, + "pairs:fixed": { + "both": 4, + "none": 58, + "one": 1 + }, + "pairs:vulnerable": { + "both": 45, + "none": 11, + "one": 7 + }, + "population-v1:fixed": { + "both": 5, + "none": 2, + "one": 4 + }, + "population-v1:vulnerable": { + "both": 8, + "none": 2, + "one": 1 + } + }, + "hunts_per_pass": 182, + "infrastructure_failures": "ax apply / actor template not found for Workspaces over the inline limit (units then skipped); two tasks interrupted by a disk stop were re-run", + "skipped_units": { + "pairs": 4, + "reason": "excerpt over the 20 KB inline Workspace limit" + }, + "task_budget_sum_usd": 8.624, + "tasks_done": 474, + "units": { + "dev-php:vulnerable": 2, + "pairs:fixed": 63, + "pairs:vulnerable": 63, + "population-v1:fixed": 11, + "population-v1:vulnerable": 11 + } + } +} diff --git a/ops/ax/README.md b/ops/ax/README.md index a18b313..fd65f3f 100644 --- a/ops/ax/README.md +++ b/ops/ax/README.md @@ -41,6 +41,13 @@ afterwards (`go clean -cache -modcache`); rerunning the full Substrate install o only; the gateway dials the address the actor connected to, so the receiver is a Service dialled by ClusterIP with its name as Host. The Envoy gateway needs a Rust build; the agentgateway variant needs none. - Apply a Task after the Workspaces it binds; ax copies them at create time. +- Every Task is its own actor template with a golden actor (~24 MB under `/var/lib/ate/actors` on the node). Deleting + them failed while the snapshot bucket ax's templates name (`dberkov-gke-dev3`, from `AX_SNAPSHOTS_BUCKET`) did not + exist in rustfs: golden actors piled up in `DELETING` and filled the disk during a 600-task harvest (2026-10-01). + Create that bucket in rustfs (an aws-cli pod with the `rustfs-bucket-init` env); even then a deleted actor's + directory stays on the node, so long Runs need a janitor that removes directories no live actor owns. +- A Workspace's `files` reach the actor inline in one environment variable: above ~20 KB of content the template + fails with "actor template not found" (an 86 KB and a 31 KB excerpt did; 7 KB ran). ## Moving to a separate Kubernetes cluster (planned) diff --git a/plane/tasks/agree.yaml b/plane/tasks/agree.yaml index b24d2b3..75fa308 100644 --- a/plane/tasks/agree.yaml +++ b/plane/tasks/agree.yaml @@ -7,7 +7,7 @@ metadata: name: agree atespace: default spec: - image: "localhost:5001/ousast-runner@sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699" + image: "localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884" command: ["agree"] resources: requests: diff --git a/plane/tasks/alerts.yaml b/plane/tasks/alerts.yaml index f6a8785..d17f5fc 100644 --- a/plane/tasks/alerts.yaml +++ b/plane/tasks/alerts.yaml @@ -7,7 +7,7 @@ metadata: name: alerts atespace: default spec: - image: "localhost:5001/ousast-runner@sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699" + image: "localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884" command: ["alerts"] resources: requests: diff --git a/plane/tasks/features.yaml b/plane/tasks/features.yaml index 598de3e..e844fd4 100644 --- a/plane/tasks/features.yaml +++ b/plane/tasks/features.yaml @@ -7,7 +7,7 @@ metadata: name: features atespace: default spec: - image: "localhost:5001/ousast-runner@sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699" + image: "localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884" command: ["features"] resources: requests: diff --git a/plane/tasks/loop.yaml b/plane/tasks/loop.yaml index d7a7f03..926e6c1 100644 --- a/plane/tasks/loop.yaml +++ b/plane/tasks/loop.yaml @@ -7,7 +7,7 @@ metadata: name: loop atespace: default spec: - image: "localhost:5001/ousast-runner@sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699" + image: "localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884" command: ["loop"] resources: requests: diff --git a/plane/tasks/remember.yaml b/plane/tasks/remember.yaml index 17a97db..85f7af9 100644 --- a/plane/tasks/remember.yaml +++ b/plane/tasks/remember.yaml @@ -7,7 +7,7 @@ metadata: name: remember atespace: default spec: - image: "localhost:5001/ousast-runner@sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699" + image: "localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884" command: ["remember"] resources: requests: diff --git a/plane/tasks/repo-facts.yaml b/plane/tasks/repo-facts.yaml index 88cd3a4..8ebc7fb 100644 --- a/plane/tasks/repo-facts.yaml +++ b/plane/tasks/repo-facts.yaml @@ -8,7 +8,7 @@ metadata: name: repo-facts atespace: default spec: - image: "localhost:5001/ousast-runner@sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699" + image: "localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884" command: ["repo-facts"] resources: requests: diff --git a/plane/tasks/roles.yaml b/plane/tasks/roles.yaml new file mode 100644 index 0000000..8ba31c2 --- /dev/null +++ b/plane/tasks/roles.yaml @@ -0,0 +1,22 @@ +# roles (learned-decision-engine design section 2, Req 8.1): the model reads a repository's source files and names +# their roles (sink, source, sanitizer, guard, dispatch), no vocabulary and no framework table. Bound to its Model by +# the `openultrasast.io/model` annotation, which the reconciler strips and renders as OUSAST_MODEL / +# OUSAST_MODEL_PARAMS. The template of the per-pin Tasks of a harvest Run (`ousast plane harvest`), which name the +# files to classify as OUSAST_INPUT_FILES. The image is digest-pinned. +apiVersion: ax.io/v1alpha1 +kind: Task +metadata: + name: roles + atespace: default + annotations: + openultrasast.io/model: deepseek-flash +spec: + image: "localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884" + command: ["roles"] + resources: + requests: + cpu: "250m" + memory: "384Mi" + limits: + cpu: "1" + memory: "1Gi" diff --git a/plane/tasks/validation-46.yaml b/plane/tasks/validation-46.yaml index acb36c7..7ab64e1 100644 --- a/plane/tasks/validation-46.yaml +++ b/plane/tasks/validation-46.yaml @@ -5,9 +5,9 @@ metadata: name: repo-facts-budibase-mongo-template-nosqli atespace: default annotations: - openultrasast.io/memory-key: '{"candidates":"b4ef709df34ab08c","image":"sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699","pin":"b7e63df737eede85d951aa9665b0c0778ece7ccb","repo":"github.com/Budibase/budibase"}' + openultrasast.io/memory-key: '{"candidates":"b4ef709df34ab08c","image":"sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884","pin":"b7e63df737eede85d951aa9665b0c0778ece7ccb","repo":"github.com/Budibase/budibase"}' spec: - image: localhost:5001/ousast-runner@sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699 + image: localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884 command: - repo-facts env: @@ -36,7 +36,7 @@ metadata: annotations: openultrasast.io/model: deepseek-flash spec: - image: localhost:5001/ousast-runner@sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699 + image: localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884 command: - verify env: @@ -67,7 +67,7 @@ metadata: annotations: openultrasast.io/model: deepseek-flash spec: - image: localhost:5001/ousast-runner@sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699 + image: localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884 command: - verify env: @@ -96,7 +96,7 @@ metadata: name: agree-budibase-mongo-template-nosqli atespace: default spec: - image: localhost:5001/ousast-runner@sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699 + image: localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884 command: - agree env: @@ -123,9 +123,9 @@ metadata: name: repo-facts-cct-studio-server-cmdi atespace: default annotations: - openultrasast.io/memory-key: '{"candidates":"6c9fa4a6e47cda37","image":"sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699","pin":"acdb5893412a81eaec9862ee24a4613bf296ab45","repo":"github.com/davila7/claude-code-templates"}' + openultrasast.io/memory-key: '{"candidates":"6c9fa4a6e47cda37","image":"sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884","pin":"acdb5893412a81eaec9862ee24a4613bf296ab45","repo":"github.com/davila7/claude-code-templates"}' spec: - image: localhost:5001/ousast-runner@sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699 + image: localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884 command: - repo-facts env: @@ -154,7 +154,7 @@ metadata: annotations: openultrasast.io/model: deepseek-flash spec: - image: localhost:5001/ousast-runner@sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699 + image: localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884 command: - verify env: @@ -185,7 +185,7 @@ metadata: annotations: openultrasast.io/model: deepseek-flash spec: - image: localhost:5001/ousast-runner@sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699 + image: localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884 command: - verify env: @@ -214,7 +214,7 @@ metadata: name: agree-cct-studio-server-cmdi atespace: default spec: - image: localhost:5001/ousast-runner@sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699 + image: localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884 command: - agree env: @@ -241,9 +241,9 @@ metadata: name: repo-facts-contextforge-prompt-ssti atespace: default annotations: - openultrasast.io/memory-key: '{"candidates":"b272af6456664ebc","image":"sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699","pin":"0f8e0ad679f1fff685f486e17f642ac863c8ca9e","repo":"github.com/IBM/mcp-context-forge"}' + openultrasast.io/memory-key: '{"candidates":"b272af6456664ebc","image":"sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884","pin":"0f8e0ad679f1fff685f486e17f642ac863c8ca9e","repo":"github.com/IBM/mcp-context-forge"}' spec: - image: localhost:5001/ousast-runner@sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699 + image: localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884 command: - repo-facts env: @@ -272,7 +272,7 @@ metadata: annotations: openultrasast.io/model: deepseek-flash spec: - image: localhost:5001/ousast-runner@sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699 + image: localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884 command: - verify env: @@ -303,7 +303,7 @@ metadata: annotations: openultrasast.io/model: deepseek-flash spec: - image: localhost:5001/ousast-runner@sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699 + image: localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884 command: - verify env: @@ -332,7 +332,7 @@ metadata: name: agree-contextforge-prompt-ssti atespace: default spec: - image: localhost:5001/ousast-runner@sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699 + image: localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884 command: - agree env: @@ -359,9 +359,9 @@ metadata: name: repo-facts-froxlor-admin-ip-second-order-sqli atespace: default annotations: - openultrasast.io/memory-key: '{"candidates":"c52240d980ef0105","image":"sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699","pin":"9bf03de643a2edee4642bfc6c94db1f05253da02","repo":"github.com/froxlor/froxlor"}' + openultrasast.io/memory-key: '{"candidates":"c52240d980ef0105","image":"sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884","pin":"9bf03de643a2edee4642bfc6c94db1f05253da02","repo":"github.com/froxlor/froxlor"}' spec: - image: localhost:5001/ousast-runner@sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699 + image: localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884 command: - repo-facts env: @@ -390,7 +390,7 @@ metadata: annotations: openultrasast.io/model: deepseek-flash spec: - image: localhost:5001/ousast-runner@sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699 + image: localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884 command: - verify env: @@ -421,7 +421,7 @@ metadata: annotations: openultrasast.io/model: deepseek-flash spec: - image: localhost:5001/ousast-runner@sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699 + image: localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884 command: - verify env: @@ -450,7 +450,7 @@ metadata: name: agree-froxlor-admin-ip-second-order-sqli atespace: default spec: - image: localhost:5001/ousast-runner@sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699 + image: localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884 command: - agree env: @@ -477,9 +477,9 @@ metadata: name: repo-facts-fuxa-tdengine-escape-sqli atespace: default annotations: - openultrasast.io/memory-key: '{"candidates":"ec699ee54d38a8ee","image":"sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699","pin":"193a888f8e3f805325072829b1644a2c768c948b","repo":"github.com/frangoteam/FUXA"}' + openultrasast.io/memory-key: '{"candidates":"ec699ee54d38a8ee","image":"sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884","pin":"193a888f8e3f805325072829b1644a2c768c948b","repo":"github.com/frangoteam/FUXA"}' spec: - image: localhost:5001/ousast-runner@sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699 + image: localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884 command: - repo-facts env: @@ -508,7 +508,7 @@ metadata: annotations: openultrasast.io/model: deepseek-flash spec: - image: localhost:5001/ousast-runner@sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699 + image: localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884 command: - verify env: @@ -539,7 +539,7 @@ metadata: annotations: openultrasast.io/model: deepseek-flash spec: - image: localhost:5001/ousast-runner@sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699 + image: localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884 command: - verify env: @@ -568,7 +568,7 @@ metadata: name: agree-fuxa-tdengine-escape-sqli atespace: default spec: - image: localhost:5001/ousast-runner@sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699 + image: localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884 command: - agree env: @@ -595,9 +595,9 @@ metadata: name: repo-facts-lmdeploy-media-url-ssrf atespace: default annotations: - openultrasast.io/memory-key: '{"candidates":"1337c9c717f5ac87","image":"sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699","pin":"71648d239c978452720a6d6692fe914efc8a7b94","repo":"github.com/InternLM/lmdeploy"}' + openultrasast.io/memory-key: '{"candidates":"1337c9c717f5ac87","image":"sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884","pin":"71648d239c978452720a6d6692fe914efc8a7b94","repo":"github.com/InternLM/lmdeploy"}' spec: - image: localhost:5001/ousast-runner@sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699 + image: localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884 command: - repo-facts env: @@ -626,7 +626,7 @@ metadata: annotations: openultrasast.io/model: deepseek-flash spec: - image: localhost:5001/ousast-runner@sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699 + image: localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884 command: - verify env: @@ -657,7 +657,7 @@ metadata: annotations: openultrasast.io/model: deepseek-flash spec: - image: localhost:5001/ousast-runner@sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699 + image: localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884 command: - verify env: @@ -686,7 +686,7 @@ metadata: name: agree-lmdeploy-media-url-ssrf atespace: default spec: - image: localhost:5001/ousast-runner@sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699 + image: localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884 command: - agree env: @@ -713,9 +713,9 @@ metadata: name: repo-facts-medplum-external-auth-open-redirect atespace: default annotations: - openultrasast.io/memory-key: '{"candidates":"17ba609d5b1a1a00","image":"sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699","pin":"5d4a8654b02aa67c444f96ae1eb5663c09854006","repo":"github.com/medplum/medplum"}' + openultrasast.io/memory-key: '{"candidates":"17ba609d5b1a1a00","image":"sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884","pin":"5d4a8654b02aa67c444f96ae1eb5663c09854006","repo":"github.com/medplum/medplum"}' spec: - image: localhost:5001/ousast-runner@sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699 + image: localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884 command: - repo-facts env: @@ -744,7 +744,7 @@ metadata: annotations: openultrasast.io/model: deepseek-flash spec: - image: localhost:5001/ousast-runner@sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699 + image: localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884 command: - verify env: @@ -775,7 +775,7 @@ metadata: annotations: openultrasast.io/model: deepseek-flash spec: - image: localhost:5001/ousast-runner@sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699 + image: localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884 command: - verify env: @@ -804,7 +804,7 @@ metadata: name: agree-medplum-external-auth-open-redirect atespace: default spec: - image: localhost:5001/ousast-runner@sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699 + image: localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884 command: - agree env: @@ -831,9 +831,9 @@ metadata: name: repo-facts-pgadmin-maintenance-sqli atespace: default annotations: - openultrasast.io/memory-key: '{"candidates":"7f1b83a88024c2d4","image":"sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699","pin":"d112dc3b962218efc3613fa727d54f0fc5b1d527","repo":"github.com/pgadmin-org/pgadmin4"}' + openultrasast.io/memory-key: '{"candidates":"7f1b83a88024c2d4","image":"sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884","pin":"d112dc3b962218efc3613fa727d54f0fc5b1d527","repo":"github.com/pgadmin-org/pgadmin4"}' spec: - image: localhost:5001/ousast-runner@sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699 + image: localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884 command: - repo-facts env: @@ -862,7 +862,7 @@ metadata: annotations: openultrasast.io/model: deepseek-flash spec: - image: localhost:5001/ousast-runner@sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699 + image: localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884 command: - verify env: @@ -893,7 +893,7 @@ metadata: annotations: openultrasast.io/model: deepseek-flash spec: - image: localhost:5001/ousast-runner@sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699 + image: localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884 command: - verify env: @@ -922,7 +922,7 @@ metadata: name: agree-pgadmin-maintenance-sqli atespace: default spec: - image: localhost:5001/ousast-runner@sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699 + image: localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884 command: - agree env: @@ -949,9 +949,9 @@ metadata: name: repo-facts-qwed-sympy-eval atespace: default annotations: - openultrasast.io/memory-key: '{"candidates":"f071e8db5f5517ab","image":"sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699","pin":"06801f68cbf9c9b5e54cc17f2a61cb2792da7a26","repo":"github.com/QWED-AI/qwed-verification"}' + openultrasast.io/memory-key: '{"candidates":"f071e8db5f5517ab","image":"sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884","pin":"06801f68cbf9c9b5e54cc17f2a61cb2792da7a26","repo":"github.com/QWED-AI/qwed-verification"}' spec: - image: localhost:5001/ousast-runner@sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699 + image: localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884 command: - repo-facts env: @@ -980,7 +980,7 @@ metadata: annotations: openultrasast.io/model: deepseek-flash spec: - image: localhost:5001/ousast-runner@sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699 + image: localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884 command: - verify env: @@ -1011,7 +1011,7 @@ metadata: annotations: openultrasast.io/model: deepseek-flash spec: - image: localhost:5001/ousast-runner@sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699 + image: localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884 command: - verify env: @@ -1040,7 +1040,7 @@ metadata: name: agree-qwed-sympy-eval atespace: default spec: - image: localhost:5001/ousast-runner@sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699 + image: localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884 command: - agree env: @@ -1067,9 +1067,9 @@ metadata: name: repo-facts-reactpress-findall-column-sqli atespace: default annotations: - openultrasast.io/memory-key: '{"candidates":"ecb82a10249c6fed","image":"sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699","pin":"c3dd10681b3d2b5e224f7ac647b3769557603975","repo":"github.com/fecommunity/reactpress"}' + openultrasast.io/memory-key: '{"candidates":"ecb82a10249c6fed","image":"sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884","pin":"c3dd10681b3d2b5e224f7ac647b3769557603975","repo":"github.com/fecommunity/reactpress"}' spec: - image: localhost:5001/ousast-runner@sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699 + image: localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884 command: - repo-facts env: @@ -1098,7 +1098,7 @@ metadata: annotations: openultrasast.io/model: deepseek-flash spec: - image: localhost:5001/ousast-runner@sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699 + image: localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884 command: - verify env: @@ -1129,7 +1129,7 @@ metadata: annotations: openultrasast.io/model: deepseek-flash spec: - image: localhost:5001/ousast-runner@sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699 + image: localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884 command: - verify env: @@ -1158,7 +1158,7 @@ metadata: name: agree-reactpress-findall-column-sqli atespace: default spec: - image: localhost:5001/ousast-runner@sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699 + image: localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884 command: - agree env: @@ -1185,9 +1185,9 @@ metadata: name: repo-facts-unleash-addon-webhook-ssrf atespace: default annotations: - openultrasast.io/memory-key: '{"candidates":"4d3debd51fd86f9c","image":"sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699","pin":"b9f3217d327f24094b4352130baa30387f85cffe","repo":"github.com/Unleash/unleash"}' + openultrasast.io/memory-key: '{"candidates":"4d3debd51fd86f9c","image":"sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884","pin":"b9f3217d327f24094b4352130baa30387f85cffe","repo":"github.com/Unleash/unleash"}' spec: - image: localhost:5001/ousast-runner@sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699 + image: localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884 command: - repo-facts env: @@ -1216,7 +1216,7 @@ metadata: annotations: openultrasast.io/model: deepseek-flash spec: - image: localhost:5001/ousast-runner@sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699 + image: localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884 command: - verify env: @@ -1247,7 +1247,7 @@ metadata: annotations: openultrasast.io/model: deepseek-flash spec: - image: localhost:5001/ousast-runner@sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699 + image: localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884 command: - verify env: @@ -1276,7 +1276,7 @@ metadata: name: agree-unleash-addon-webhook-ssrf atespace: default spec: - image: localhost:5001/ousast-runner@sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699 + image: localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884 command: - agree env: @@ -1303,9 +1303,9 @@ metadata: name: repo-facts-winml-cli-serve-cors atespace: default annotations: - openultrasast.io/memory-key: '{"candidates":"e3b0c44298fc1c14","image":"sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699","pin":"02179d185f0e3ac23518236b5ca1f61fd37ef680","repo":"github.com/microsoft/winml-cli"}' + openultrasast.io/memory-key: '{"candidates":"e3b0c44298fc1c14","image":"sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884","pin":"02179d185f0e3ac23518236b5ca1f61fd37ef680","repo":"github.com/microsoft/winml-cli"}' spec: - image: localhost:5001/ousast-runner@sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699 + image: localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884 command: - repo-facts env: @@ -1334,7 +1334,7 @@ metadata: annotations: openultrasast.io/model: deepseek-flash spec: - image: localhost:5001/ousast-runner@sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699 + image: localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884 command: - verify env: @@ -1365,7 +1365,7 @@ metadata: annotations: openultrasast.io/model: deepseek-flash spec: - image: localhost:5001/ousast-runner@sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699 + image: localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884 command: - verify env: @@ -1394,7 +1394,7 @@ metadata: name: agree-winml-cli-serve-cors atespace: default spec: - image: localhost:5001/ousast-runner@sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699 + image: localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884 command: - agree env: @@ -1421,9 +1421,9 @@ metadata: name: repo-facts-wp-directorist-avatar-ssrf atespace: default annotations: - openultrasast.io/memory-key: '{"candidates":"d6963021f588f39e","image":"sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699","pin":"77cfc9bc8b453f97ab7e720330706fa4ef056867","repo":"github.com/sovware/directorist"}' + openultrasast.io/memory-key: '{"candidates":"d6963021f588f39e","image":"sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884","pin":"77cfc9bc8b453f97ab7e720330706fa4ef056867","repo":"github.com/sovware/directorist"}' spec: - image: localhost:5001/ousast-runner@sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699 + image: localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884 command: - repo-facts env: @@ -1452,7 +1452,7 @@ metadata: annotations: openultrasast.io/model: deepseek-flash spec: - image: localhost:5001/ousast-runner@sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699 + image: localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884 command: - verify env: @@ -1483,7 +1483,7 @@ metadata: annotations: openultrasast.io/model: deepseek-flash spec: - image: localhost:5001/ousast-runner@sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699 + image: localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884 command: - verify env: @@ -1512,7 +1512,7 @@ metadata: name: agree-wp-directorist-avatar-ssrf atespace: default spec: - image: localhost:5001/ousast-runner@sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699 + image: localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884 command: - agree env: @@ -1539,9 +1539,9 @@ metadata: name: repo-facts-wp-gopay-log-filter-sqli atespace: default annotations: - openultrasast.io/memory-key: '{"candidates":"fde4d17144a13d73","image":"sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699","pin":"626bc8b5f8d33de6eefd921e3a05ed52ac320bcd","repo":"github.com/gopaycommunity/gopay-woocommerce-integration"}' + openultrasast.io/memory-key: '{"candidates":"fde4d17144a13d73","image":"sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884","pin":"626bc8b5f8d33de6eefd921e3a05ed52ac320bcd","repo":"github.com/gopaycommunity/gopay-woocommerce-integration"}' spec: - image: localhost:5001/ousast-runner@sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699 + image: localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884 command: - repo-facts env: @@ -1570,7 +1570,7 @@ metadata: annotations: openultrasast.io/model: deepseek-flash spec: - image: localhost:5001/ousast-runner@sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699 + image: localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884 command: - verify env: @@ -1601,7 +1601,7 @@ metadata: annotations: openultrasast.io/model: deepseek-flash spec: - image: localhost:5001/ousast-runner@sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699 + image: localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884 command: - verify env: @@ -1630,7 +1630,7 @@ metadata: name: agree-wp-gopay-log-filter-sqli atespace: default spec: - image: localhost:5001/ousast-runner@sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699 + image: localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884 command: - agree env: @@ -1657,9 +1657,9 @@ metadata: name: repo-facts-wp-groundhogg-confirm-redirect atespace: default annotations: - openultrasast.io/memory-key: '{"candidates":"fde4d17144a13d73","image":"sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699","pin":"9472a1dd79d4556c40eece501a61930793f170d3","repo":"github.com/groundhoggwp/groundhogg"}' + openultrasast.io/memory-key: '{"candidates":"fde4d17144a13d73","image":"sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884","pin":"9472a1dd79d4556c40eece501a61930793f170d3","repo":"github.com/groundhoggwp/groundhogg"}' spec: - image: localhost:5001/ousast-runner@sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699 + image: localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884 command: - repo-facts env: @@ -1688,7 +1688,7 @@ metadata: annotations: openultrasast.io/model: deepseek-flash spec: - image: localhost:5001/ousast-runner@sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699 + image: localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884 command: - verify env: @@ -1719,7 +1719,7 @@ metadata: annotations: openultrasast.io/model: deepseek-flash spec: - image: localhost:5001/ousast-runner@sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699 + image: localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884 command: - verify env: @@ -1748,7 +1748,7 @@ metadata: name: agree-wp-groundhogg-confirm-redirect atespace: default spec: - image: localhost:5001/ousast-runner@sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699 + image: localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884 command: - agree env: @@ -1777,7 +1777,7 @@ metadata: annotations: openultrasast.io/model: deepseek-flash spec: - image: localhost:5001/ousast-runner@sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699 + image: localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884 command: - verify env: @@ -1808,7 +1808,7 @@ metadata: annotations: openultrasast.io/model: deepseek-flash spec: - image: localhost:5001/ousast-runner@sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699 + image: localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884 command: - verify env: @@ -1839,7 +1839,7 @@ metadata: annotations: openultrasast.io/model: deepseek-flash spec: - image: localhost:5001/ousast-runner@sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699 + image: localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884 command: - verify env: @@ -1870,7 +1870,7 @@ metadata: annotations: openultrasast.io/model: deepseek-flash spec: - image: localhost:5001/ousast-runner@sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699 + image: localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884 command: - verify env: @@ -1901,7 +1901,7 @@ metadata: annotations: openultrasast.io/model: deepseek-flash spec: - image: localhost:5001/ousast-runner@sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699 + image: localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884 command: - verify env: @@ -1932,7 +1932,7 @@ metadata: annotations: openultrasast.io/model: deepseek-flash spec: - image: localhost:5001/ousast-runner@sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699 + image: localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884 command: - verify env: @@ -1963,7 +1963,7 @@ metadata: annotations: openultrasast.io/model: deepseek-flash spec: - image: localhost:5001/ousast-runner@sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699 + image: localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884 command: - verify env: @@ -1994,7 +1994,7 @@ metadata: annotations: openultrasast.io/model: deepseek-flash spec: - image: localhost:5001/ousast-runner@sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699 + image: localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884 command: - verify env: @@ -2025,7 +2025,7 @@ metadata: annotations: openultrasast.io/model: deepseek-flash spec: - image: localhost:5001/ousast-runner@sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699 + image: localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884 command: - verify env: @@ -2056,7 +2056,7 @@ metadata: annotations: openultrasast.io/model: deepseek-flash spec: - image: localhost:5001/ousast-runner@sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699 + image: localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884 command: - verify env: @@ -2087,7 +2087,7 @@ metadata: annotations: openultrasast.io/model: deepseek-flash spec: - image: localhost:5001/ousast-runner@sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699 + image: localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884 command: - verify env: @@ -2118,7 +2118,7 @@ metadata: annotations: openultrasast.io/model: deepseek-flash spec: - image: localhost:5001/ousast-runner@sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699 + image: localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884 command: - verify env: @@ -2149,7 +2149,7 @@ metadata: annotations: openultrasast.io/model: deepseek-flash spec: - image: localhost:5001/ousast-runner@sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699 + image: localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884 command: - verify env: @@ -2180,7 +2180,7 @@ metadata: annotations: openultrasast.io/model: deepseek-flash spec: - image: localhost:5001/ousast-runner@sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699 + image: localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884 command: - verify env: @@ -2211,7 +2211,7 @@ metadata: annotations: openultrasast.io/model: deepseek-flash spec: - image: localhost:5001/ousast-runner@sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699 + image: localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884 command: - verify env: @@ -2240,7 +2240,7 @@ metadata: name: features-budibase-mongo-template-nosqli atespace: default spec: - image: localhost:5001/ousast-runner@sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699 + image: localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884 command: - features env: @@ -2263,7 +2263,7 @@ metadata: name: remember-budibase-mongo-template-nosqli atespace: default spec: - image: localhost:5001/ousast-runner@sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699 + image: localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884 command: - remember env: @@ -2288,7 +2288,7 @@ metadata: name: features-cct-studio-server-cmdi atespace: default spec: - image: localhost:5001/ousast-runner@sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699 + image: localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884 command: - features env: @@ -2311,7 +2311,7 @@ metadata: name: remember-cct-studio-server-cmdi atespace: default spec: - image: localhost:5001/ousast-runner@sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699 + image: localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884 command: - remember env: @@ -2336,7 +2336,7 @@ metadata: name: features-contextforge-prompt-ssti atespace: default spec: - image: localhost:5001/ousast-runner@sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699 + image: localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884 command: - features env: @@ -2359,7 +2359,7 @@ metadata: name: remember-contextforge-prompt-ssti atespace: default spec: - image: localhost:5001/ousast-runner@sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699 + image: localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884 command: - remember env: @@ -2384,7 +2384,7 @@ metadata: name: features-froxlor-admin-ip-second-order-sqli atespace: default spec: - image: localhost:5001/ousast-runner@sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699 + image: localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884 command: - features env: @@ -2407,7 +2407,7 @@ metadata: name: remember-froxlor-admin-ip-second-order-sqli atespace: default spec: - image: localhost:5001/ousast-runner@sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699 + image: localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884 command: - remember env: @@ -2432,7 +2432,7 @@ metadata: name: features-fuxa-tdengine-escape-sqli atespace: default spec: - image: localhost:5001/ousast-runner@sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699 + image: localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884 command: - features env: @@ -2455,7 +2455,7 @@ metadata: name: remember-fuxa-tdengine-escape-sqli atespace: default spec: - image: localhost:5001/ousast-runner@sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699 + image: localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884 command: - remember env: @@ -2480,7 +2480,7 @@ metadata: name: features-lmdeploy-media-url-ssrf atespace: default spec: - image: localhost:5001/ousast-runner@sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699 + image: localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884 command: - features env: @@ -2503,7 +2503,7 @@ metadata: name: remember-lmdeploy-media-url-ssrf atespace: default spec: - image: localhost:5001/ousast-runner@sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699 + image: localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884 command: - remember env: @@ -2528,7 +2528,7 @@ metadata: name: features-medplum-external-auth-open-redirect atespace: default spec: - image: localhost:5001/ousast-runner@sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699 + image: localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884 command: - features env: @@ -2551,7 +2551,7 @@ metadata: name: remember-medplum-external-auth-open-redirect atespace: default spec: - image: localhost:5001/ousast-runner@sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699 + image: localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884 command: - remember env: @@ -2576,7 +2576,7 @@ metadata: name: features-pgadmin-maintenance-sqli atespace: default spec: - image: localhost:5001/ousast-runner@sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699 + image: localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884 command: - features env: @@ -2599,7 +2599,7 @@ metadata: name: remember-pgadmin-maintenance-sqli atespace: default spec: - image: localhost:5001/ousast-runner@sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699 + image: localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884 command: - remember env: @@ -2624,7 +2624,7 @@ metadata: name: features-qwed-sympy-eval atespace: default spec: - image: localhost:5001/ousast-runner@sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699 + image: localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884 command: - features env: @@ -2647,7 +2647,7 @@ metadata: name: remember-qwed-sympy-eval atespace: default spec: - image: localhost:5001/ousast-runner@sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699 + image: localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884 command: - remember env: @@ -2672,7 +2672,7 @@ metadata: name: features-reactpress-findall-column-sqli atespace: default spec: - image: localhost:5001/ousast-runner@sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699 + image: localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884 command: - features env: @@ -2695,7 +2695,7 @@ metadata: name: remember-reactpress-findall-column-sqli atespace: default spec: - image: localhost:5001/ousast-runner@sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699 + image: localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884 command: - remember env: @@ -2720,7 +2720,7 @@ metadata: name: features-unleash-addon-webhook-ssrf atespace: default spec: - image: localhost:5001/ousast-runner@sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699 + image: localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884 command: - features env: @@ -2743,7 +2743,7 @@ metadata: name: remember-unleash-addon-webhook-ssrf atespace: default spec: - image: localhost:5001/ousast-runner@sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699 + image: localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884 command: - remember env: @@ -2768,7 +2768,7 @@ metadata: name: features-winml-cli-serve-cors atespace: default spec: - image: localhost:5001/ousast-runner@sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699 + image: localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884 command: - features env: @@ -2791,7 +2791,7 @@ metadata: name: remember-winml-cli-serve-cors atespace: default spec: - image: localhost:5001/ousast-runner@sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699 + image: localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884 command: - remember env: @@ -2816,7 +2816,7 @@ metadata: name: features-wp-directorist-avatar-ssrf atespace: default spec: - image: localhost:5001/ousast-runner@sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699 + image: localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884 command: - features env: @@ -2839,7 +2839,7 @@ metadata: name: remember-wp-directorist-avatar-ssrf atespace: default spec: - image: localhost:5001/ousast-runner@sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699 + image: localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884 command: - remember env: @@ -2864,7 +2864,7 @@ metadata: name: features-wp-gopay-log-filter-sqli atespace: default spec: - image: localhost:5001/ousast-runner@sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699 + image: localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884 command: - features env: @@ -2887,7 +2887,7 @@ metadata: name: remember-wp-gopay-log-filter-sqli atespace: default spec: - image: localhost:5001/ousast-runner@sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699 + image: localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884 command: - remember env: @@ -2912,7 +2912,7 @@ metadata: name: features-wp-groundhogg-confirm-redirect atespace: default spec: - image: localhost:5001/ousast-runner@sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699 + image: localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884 command: - features env: @@ -2935,7 +2935,7 @@ metadata: name: remember-wp-groundhogg-confirm-redirect atespace: default spec: - image: localhost:5001/ousast-runner@sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699 + image: localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884 command: - remember env: diff --git a/plane/tasks/verify.yaml b/plane/tasks/verify.yaml index cd8c124..2ca6050 100644 --- a/plane/tasks/verify.yaml +++ b/plane/tasks/verify.yaml @@ -10,7 +10,7 @@ metadata: annotations: openultrasast.io/model: deepseek-flash spec: - image: "localhost:5001/ousast-runner@sha256:e9964977659c4dcea7d8b25390ca0e16368ac901dd076aa6138df8303a0bb699" + image: "localhost:5001/ousast-runner@sha256:412db0a1504af8f1b23a395af8d1dcbd152c1bb6d16594f2b733067163a31884" command: ["verify"] resources: requests: diff --git a/src/openultrasast/cli.py b/src/openultrasast/cli.py index a3f2c98..cbc6777 100644 --- a/src/openultrasast/cli.py +++ b/src/openultrasast/cli.py @@ -296,6 +296,21 @@ def _main(argv: list[str] | None) -> int: plane_engine.add_argument("--image", default="openultrasast:dev") plane_engine.add_argument("--repos", type=Path, help="the case cache (default ~/.cache/openultrasast/independent)") plane_engine.add_argument("--work", type=Path, help="where pins are exported and results land (default: a temporary directory)") + plane_harvest = plane_sub.add_parser( + "harvest", help="the decision engine's harvest Runs (verify a/b + agree, model roles) over a label snapshot" + ) + plane_harvest.add_argument("--labels", type=Path, required=True, help="the full label rows (`ousast learn labels --out`)") + plane_harvest.add_argument( + "--plane", type=Path, required=True, help="where to write runs/, tasks/, workspaces/, models/, units.json (scratch)" + ) + plane_harvest.add_argument("--name", default="harvest", help="Run name prefix: -verify and -roles") + plane_harvest.add_argument("--templates", type=Path, default=Path("plane"), help="the plane root holding tasks/ and models/ templates") + plane_harvest.add_argument("--catalog", type=Path, default=Path("benchmarks/pairs/catalog.toml")) + plane_harvest.add_argument( + "--cache", type=Path, default=Path.home() / ".cache" / "openultrasast", help="case clones and recipe checkouts" + ) + plane_harvest.add_argument("--ceiling", type=float, default=10.0, help="USD: each Run's task budgets sum to at most this") + plane_harvest.add_argument("--runner-image", type=Path, help="re-pin the task templates to the image in this file first (ops/ax/up.sh)") plane_ws = plane_sub.add_parser("workspaces", help="one Workspace manifest per case pin of a population file") plane_ws.add_argument("population", type=Path) plane_ws.add_argument("--out", type=Path, default=Path("plane/workspaces")) @@ -1371,6 +1386,8 @@ def _plane(args: argparse.Namespace) -> int: return 0 if args.plane_command == "alerts-engine": return _plane_alerts_engine(args) + if args.plane_command == "harvest": + return _plane_harvest(args) if args.plane_command == "doctor": checks = reconciler.doctor() for name, ok, text in checks: @@ -1385,6 +1402,22 @@ def _plane(args: argparse.Namespace) -> int: return 0 +def _plane_harvest(args: argparse.Namespace) -> int: + from .plane.generate import repin_templates + from .plane.harvest import write_harvest + + if args.runner_image is not None: + repin_templates(args.templates, args.runner_image) + command = f"ousast plane harvest --labels {args.labels.name} --name {args.name} --ceiling {args.ceiling:g}" + files = write_harvest( + args.labels, args.plane, name=args.name, command=command, catalog=args.catalog, cache=args.cache, templates=args.templates, + ceiling=args.ceiling, + ) # fmt: skip + for relative in sorted(k for k in files if k.startswith("runs/")): + print(f"{args.plane / relative}: {files[relative].splitlines()[1].lstrip('# ')}") + return 0 + + def _plane_alerts_engine(args: argparse.Namespace) -> int: from .plane import engine_alerts diff --git a/src/openultrasast/learn/features.py b/src/openultrasast/learn/features.py index b7cd064..8d311cd 100644 --- a/src/openultrasast/learn/features.py +++ b/src/openultrasast/learn/features.py @@ -254,7 +254,9 @@ def function_span(lines: Sequence[str], language: str, function: str) -> int | N def source_part(lines: Sequence[str] | None, language: str, function: str) -> Part: - if lines is None: + """The function's span; ``none`` without the file or a declaration pattern for its language (C, say: no coverage, + not a failure), ``failed`` when a covered file does not declare the function.""" + if lines is None or (function != GLOBAL and language not in DECLARATION): return NONE span = function_span(lines, language, function) return Part("ran", {"facts.function_lines": span}) if span is not None else Part("failed") @@ -294,8 +296,9 @@ def agree_part(final: str | None) -> Part: def roles_part(function: Function | None, roles: RoleSet | None, language: str) -> Part: """The deterministic roles of the candidate's function (language-level entries plus wrapper inference; priors - off): ``none`` when inference did not run, ``failed`` when the function's body could not be found.""" - if roles is None or "inferred_wrapper" not in roles.sources: + off): ``none`` when inference did not run or cannot read the language's declarations, ``failed`` when the + function's body could not be found.""" + if roles is None or "inferred_wrapper" not in roles.sources or language not in DECLARATION: return NONE if function is None: return Part("failed", version=VERSION) diff --git a/src/openultrasast/plane/generate.py b/src/openultrasast/plane/generate.py index 45733d5..e354d5e 100644 --- a/src/openultrasast/plane/generate.py +++ b/src/openultrasast/plane/generate.py @@ -87,7 +87,7 @@ CASE_PATH = "/workspace/case" INPUTS_PATH = "/workspace/inputs" FIXED_PATH = "/workspace/fixed" PROJECT_PATH = "/workspace/project" -TEMPLATES = ("repo-facts", "verify", "agree", "remember", "alerts", "loop", "features") +TEMPLATES = ("repo-facts", "verify", "agree", "remember", "alerts", "loop", "features", "roles") FIXED_RANGES_BASIS = "git diff -U0 , new side: the fix's lines at the fixed pin" LOOP_MANIFEST = "benchmarks/manifests/java-spring-boot-vulnerable.toml" # the equality baseline's `improve` manifest LOOP_CATALOG = "benchmarks/pairs/catalog.toml" diff --git a/src/openultrasast/plane/harvest.py b/src/openultrasast/plane/harvest.py new file mode 100644 index 0000000..a08c19f --- /dev/null +++ b/src/openultrasast/plane/harvest.py @@ -0,0 +1,379 @@ +"""The harvest Runs of the learned decision engine (design, commit sequence item 5; Req 1.1). + +``ousast plane harvest --labels labels-.jsonl --plane DIR`` writes two Runs over the labelled units of a label +snapshot (``ousast learn labels --out``), with the Workspaces and Tasks they need, under ``DIR`` (a scratch plane +root: the excerpts and candidates are inputs, never committed): + +- ``runs/-verify.yaml``: verify passes a and b and ``agree`` (no tie-break) on every verify-family unit + (:data:`.tasks.verify.OPERATIONS`): both sides of each labelled pair, and the development pins -- population v1's + vulnerable and fixed pins, the dev-php recipes -- with ``repo-facts`` first for their callers; +- ``runs/-roles.yaml``: ``roles`` (model roles) on the candidate files of every labelled pin unit: the pair + sides, grouped many to a Workspace (a roles prompt sees one chunk of one file, so grouping changes nothing it + reads), and one Task per population or recipe pin. + +Units carry no label. A pair side is a files-only Workspace holding the excerpt (and the documents that register +it) under ``repo/`` and the candidates under ``inputs/`` -- outside the hunt's root, so the tools cannot read them; +its name is opaque (``h-``): the pair's name says its CWE and its side. ``agree`` gets no declared sites, so +no ``site_match`` is computed. ``units.json`` maps each unit name to its source, reference, side, repository, pin +and candidates for the host-side feature build; it names functions and stays local. + +Every Run entry has a budget; the sum over each Run is held under ``ceiling`` (USD) by construction: verify gets +:data:`USD_PER_HUNT` per hunt of an excerpt or a repository (scaled down when the hunts would exceed the ceiling), +roles a per-chunk estimate times a headroom, scaled down to the ceiling. +A pair side's pin is the git blob sha1 of its excerpt (the store keys rows by +a 40-hex pin; an excerpt has no commit of its own). +""" + +from __future__ import annotations + +import hashlib +import json +import math +from collections.abc import Iterable, Mapping, Sequence +from dataclasses import dataclass, field +from pathlib import Path +from typing import Any + +import yaml + +from ..preprocess import detect_language +from .generate import ATESPACE, CALLS_PER_HUNT, CASE_PATH, INPUTS_PATH, _dump, _json, _task +from .manifests import GIT_COMMITS_ANNOTATION, RUN_API_VERSION, Task +from .reconciler import _ax_name, _doc +from .tasks.repo_facts import DECLARATION, GLOBAL, _declared_name +from .tasks.roles import CHUNK_LINES +from .tasks.verify import OPERATIONS, units_of + +__all__ = ["Unit", "declaration_line", "pair_units", "population_units", "render_harvest", "write_harvest"] + +Candidate = tuple[str, str, int] +REPO = "repo" +# Per verify hunt (pilot 2026-10-01: an excerpt hunt cost $0.003-0.004, a repository hunt $0.019-0.022): ~3x and ~2.5x. +USD_PER_HUNT = {"excerpt": 0.012, "repository": 0.05} +ROLES_GROUP_FILES = 40 +# A Workspace's files travel inline: ax hands them to the actor in one environment variable (AX_WORKSPACES_YAML), which +# Linux caps at 128 KiB (MAX_ARG_STRLEN) after YAML escaping. An 86 KB excerpt failed with "actor template not found" +# (2026-10-01), so did a 31 KB one; 7 KB ran. A unit whose own files exceed the limit is skipped, listed in `units.json`. +MAX_INLINE_BYTES = 20_000 +ROLES_OUTPUT_TOKENS = 700 # per chunk, generous: a role list with evidence quotes +ROLES_HEADROOM = 2.5 +UNKNOWN_SIZE = (80_000, 2_000) # (characters, lines) assumed for a candidate file the host could not read +PRICES = {"input_per_m": 0.44, "output_per_m": 1.32} # plane/models/deepseek-flash.yaml +POPULATION_SOURCES = ("population-v1", "population-v2", "dev-php", "adjudications") +VERIFY_SOURCES = ("population-v1", "dev-php") # the development cases (design section 3); v2 has its recorded Run + + +@dataclass(frozen=True) +class Unit: + """One pin of the harvest: a repository at a commit (``files`` empty) or one side of a pair (``files`` holds the + excerpt). ``family`` is the verify family, ``""`` when the unit is classified by ``roles`` only.""" + + name: str + source: str + ref: str + side: str + repo: str + pin: str + family: str + candidates: tuple[Candidate, ...] + files: tuple[tuple[str, str], ...] = field(default=()) + verify: tuple[Candidate, ...] = field(default=()) # the candidates of ``family`` (all of them when empty) + sizes: tuple[tuple[str, int, int], ...] = field(default=()) # (path, characters, lines) of a pin's candidate files + + @property + def paths(self) -> list[str]: + return sorted({c[0] for c in self.candidates}) + + def index_row(self) -> dict[str, Any]: + return {"source": self.source, "ref": self.ref, "side": self.side, "repo": self.repo, "pin": self.pin, + "family": self.family, "candidates": [list(c) for c in self.candidates]} # fmt: skip + + +def _opaque(*parts: str) -> str: + return "h-" + hashlib.sha256("\x00".join(parts).encode("utf-8")).hexdigest()[:12] + + +def blob_sha1(text: str) -> str: + data = text.encode("utf-8") + return hashlib.sha1(b"blob %d\x00" % len(data) + data).hexdigest() + + +def declaration_line(lines: Sequence[str] | None, path: str, function: str) -> int: + """1-based line of the first declaration of ``function`` in ``lines``; 1 for ```` or when not declared.""" + pattern = DECLARATION.get(detect_language(Path(path)) or "") + if not lines or pattern is None or function == GLOBAL: + return 1 + return next((i + 1 for i, line in enumerate(lines) if _declared_name(pattern, line) == function), 1) + + +def _read_labels(labels: Path) -> list[dict[str, Any]]: + return [json.loads(line) for line in labels.read_text(encoding="utf-8").splitlines() if line.strip()] + + +def pair_units(labels: Path, catalog: Path) -> list[Unit]: + """Both sides of every labelled pair (pin units), each with its labelled candidates of every family; ``family`` is + the pair's verify family when it has one.""" + from ..pairs import load_pair_catalog + + wanted: dict[str, dict[str, set[str]]] = {} + for row in _read_labels(labels): + if row["source"] == "pairs" and row["unit"] == "pin": + wanted.setdefault(row["source_ref"], {}).setdefault(row["family"], set()).add(row["candidate"]) + cases = {case.name: case for case in load_pair_catalog(catalog)} + out: list[Unit] = [] + for name in sorted(wanted): + case = cases[name] + families = wanted[name] + verify = sorted(f for f in families if f in OPERATIONS) + for side in ("vuln", "fixed"): + excerpt = case.vuln_file if side == "vuln" else case.fixed_file + if not excerpt.is_file(): + raise ValueError(f"{name}: the {side} excerpt is not materialised ({excerpt}); fetch pointer pairs first") + texts = {case.relpath: excerpt.read_text(encoding="utf-8", errors="replace")} + for relpath, vuln_doc, fixed_doc in case.context_files: + texts[relpath] = (vuln_doc if side == "vuln" else fixed_doc).read_text(encoding="utf-8", errors="replace") + lines = {path: text.splitlines() for path, text in texts.items()} + + def located(names: Iterable[str], lines: Mapping[str, list[str]] = lines) -> tuple[Candidate, ...]: + return tuple(sorted({(p, fn, declaration_line(lines.get(p), p, fn)) for c in names for p, _, fn in [c.partition("::")]})) + + role = "vulnerable" if side == "vuln" else "fixed" + out.append(Unit( + _opaque("pairs", name, side), "pairs", name, role, case.repo or f"pairs/{case.slice}/{name}", + blob_sha1(texts[case.relpath]), verify[0] if verify else "", located(set().union(*families.values())), + tuple(sorted(texts.items())), located(families[verify[0]]) if verify else (), + )) # fmt: skip + return out + + +def population_units(labels: Path, cache: Path) -> list[Unit]: + """One unit per (repository, pin, family) of the population, recipe and adjudication pin labels, candidates with + their declaration line at that pin (read from the case clone or the recipe checkout). ``family`` is set for the + development cases' verify families (:data:`VERIFY_SOURCES`, and v1 adjudications), else ``""``.""" + groups: dict[tuple[str, str, str], dict[str, Any]] = {} + for row in _read_labels(labels): + if row["unit"] != "pin" or row["source"] not in POPULATION_SOURCES: + continue + key = (row["repo"], row["pin"], row["family"]) + group = groups.setdefault(key, {"sources": set(), "refs": set(), "side": row["pin_role"], "candidates": set()}) + group["sources"].add(row["split"] if row["source"] == "adjudications" else row["source"]) + group["refs"].add(row["source_ref"]) + group["candidates"].add(row["candidate"]) + case_ids = {r: ref for (r, _, _), g in groups.items() for ref in g["refs"] if ":" not in ref and "#" not in ref} + out: list[Unit] = [] + for (repo, pin, family), group in sorted(groups.items()): + source = sorted(group["sources"])[0] + ref = case_ids[repo] if source.startswith("population") and repo in case_ids else str(sorted(group["refs"])[0]) + reader = _reader(cache, source, ref, pin) + texts = {p: reader(p) for p in {c.partition("::")[0] for c in group["candidates"]}} + candidates = sorted({(p, fn, declaration_line(texts[p], p, fn)) for c in group["candidates"] for p, _, fn in [c.partition("::")]}) + sizes = tuple((p, sum(len(x) + 1 for x in t), len(t)) for p, t in sorted(texts.items()) if t is not None) + verify = family if family in OPERATIONS and source in VERIFY_SOURCES else "" + out.append(Unit(_opaque(repo, pin, family), source, ref, group["side"], repo, pin, verify, tuple(candidates), sizes=sizes)) + return out + + +def _reader(cache: Path, source: str, ref: str, pin: str) -> Any: + """Lines of a path at the pin: the case clone (populations), else the recipe checkout (dev-php).""" + from ..learn.labels import Clone, LabelSourceError + + if source == "dev-php": + root = cache / "repos" / ref.split(":")[0] / pin[:12] + return lambda path: (root / path).read_text(encoding="utf-8", errors="replace").splitlines() if (root / path).is_file() else None + try: + clone = Clone(cache / "independent" / ref) + except LabelSourceError: + return lambda path: None + return lambda path: clone.lines(pin, path) + + +# --- manifests ------------------------------------------------------------------------------------------------------- + + +def _git_workspace(unit: Unit) -> dict[str, Any]: + doc = _doc("Workspace", _ax_name(unit.name, "pin"), {"git": [{"name": REPO, "repo": unit.repo, "dir": REPO, "depth": 1}]}, ATESPACE) + doc["metadata"]["annotations"] = {GIT_COMMITS_ANNOTATION: f"{REPO}={unit.pin}"} + return doc + + +def _files_workspace(name: str, files: Mapping[str, str]) -> dict[str, Any]: + return _doc("Workspace", name, {"files": [{"path": k, "content": v} for k, v in sorted(files.items())]}, ATESPACE) + + +def _verify_docs(unit: Unit, templates: Mapping[str, Task], usd_per_hunt: float) -> tuple[list[dict], list[dict], list[dict]]: + """(Workspaces, Tasks, Run entries) of one verify unit.""" + asked = list(unit.verify or unit.candidates) + candidates = {"family": unit.family, "candidates": [list(c) for c in asked]} + hunts = max(len(units_of(asked)), 1) + budget = {"usd": math.floor(hunts * usd_per_hunt * 1000) / 1000, "calls": hunts * CALLS_PER_HUNT} + on_repo = {"OUSAST_WORKSPACE_DIR": f"{CASE_PATH}/{REPO}"} + workspaces: list[dict] = [] + tasks: list[dict] = [] + entries: list[dict] = [] + if unit.files: + files = {f"{REPO}/{path}": text for path, text in unit.files} | {"inputs/candidates.json": _json(candidates)} + workspace = _files_workspace(_ax_name(unit.name, "side"), files) + workspaces.append(workspace) + bindings = [(workspace["metadata"]["name"], CASE_PATH)] + inputs = f"{CASE_PATH}/inputs/candidates.json" + else: + pinned = _git_workspace(unit) + functions = [{"path": p, "function": fn, "line": ln} for p, fn, ln in asked] + files_doc = _files_workspace( + _ax_name(unit.name, "inputs"), {"candidates.json": _json(candidates), "functions.json": _json(functions)} + ) + workspaces += [pinned, files_doc] + bindings = [(pinned["metadata"]["name"], CASE_PATH), (files_doc["metadata"]["name"], INPUTS_PATH)] + inputs = f"{INPUTS_PATH}/candidates.json" + facts_env = {**on_repo, "OUSAST_INPUT_CANDIDATES": f"{INPUTS_PATH}/functions.json"} + tasks.append(_task(templates["repo-facts"], f"repo-facts-{unit.name}", facts_env, bindings)) + entries.append({"name": f"{unit.name}-facts", "task": f"repo-facts-{unit.name}", "outputs": ["facts.json", "summary.json"], + "budget": {"usd": 0, "calls": 0}, "serialize": "repo-facts"}) # fmt: skip + for label in ("a", "b"): + env = {**on_repo, "OUSAST_INPUT_CANDIDATES": inputs, "OUSAST_PASS": label} + tasks.append(_task(templates["verify"], f"verify-{label}-{unit.name}", env, bindings)) + entry: dict[str, Any] = {"name": f"{unit.name}-v{label}", "task": f"verify-{label}-{unit.name}", + "outputs": ["units.jsonl", "summary.json"], "budget": dict(budget)} # fmt: skip + if not unit.files: + entry["inputs"] = {"facts": f"{unit.name}-facts/facts.json"} + entries.append(entry) + tasks.append( + _task(templates["agree"], f"agree-{unit.name}", {"OUSAST_INPUT_CANDIDATES": inputs}, bindings[-1:] if not unit.files else bindings) + ) + entries.append({ + "name": f"{unit.name}-agree", "task": f"agree-{unit.name}", + "inputs": {"pass_a": f"{unit.name}-va/units.jsonl", "pass_b": f"{unit.name}-vb/units.jsonl"}, + "outputs": ["agreed.json", "disputed.json", "summary.json"], "budget": {"usd": 0, "calls": 0}, + }) # fmt: skip + return workspaces, tasks, entries + + +def _roles_estimate(sizes: Iterable[tuple[int, int]]) -> tuple[float, int]: + """(usd, chunks) of classifying files of ``(characters, lines)``: input at ~3.5 characters a token plus the + prompt, a generous output.""" + usd, chunks = 0.0, 0 + for characters, count in sizes: + lines = max(count, 1) + n = math.ceil(lines / CHUNK_LINES) + tokens_in = (characters + 8 * lines) / 3.5 + 400 * n + usd += tokens_in * PRICES["input_per_m"] / 1e6 + n * ROLES_OUTPUT_TOKENS * PRICES["output_per_m"] / 1e6 + chunks += n + return usd, chunks + + +def _kind(unit: Unit) -> str: + return "excerpt" if unit.files else "repository" + + +def _inline(unit: Unit, *, roles: bool = False) -> int: + """Bytes of the unit's files a Workspace would carry inline (roles: its candidate files only).""" + return sum(len(text.encode("utf-8")) for path, text in unit.files if not roles or path in unit.paths) + + +def oversized(unit: Unit) -> str | None: + """Why a unit cannot run on ax, or None: its files exceed :data:`MAX_INLINE_BYTES`.""" + return f"excerpt {_inline(unit)} bytes over the {MAX_INLINE_BYTES}-byte inline limit" if _inline(unit) > MAX_INLINE_BYTES else None + + +def _roles_groups(units: Sequence[Unit]) -> list[list[Unit]]: + groups: list[list[Unit]] = [[]] + size = 0 + for unit in units: + weight = _inline(unit, roles=True) + if groups[-1] and (len(groups[-1]) >= ROLES_GROUP_FILES or size + weight > MAX_INLINE_BYTES): + groups.append([]) + size = 0 + groups[-1].append(unit) + size += weight + return [g for g in groups if g] + + +def render_harvest( + units: Sequence[Unit], templates: Mapping[str, Task], name: str, command: str, *, ceiling: float = 10.0 +) -> dict[str, str]: + """Relative path under the plane root -> file text: both Runs, their Workspaces and Tasks, and ``units.json``. + A population file whose size is unknown is estimated at :data:`UNKNOWN_SIZE`.""" + header = f"# generated by `{command}`; do not edit, regenerate.\n" + out: dict[str, str] = {} + verify_units = [u for u in units if u.family and oversized(u) is None] + hunts = sum(max(len(units_of(list(u.verify or u.candidates))), 1) for u in verify_units) + wanted = 2 * sum(max(len(units_of(list(u.verify or u.candidates))), 1) * USD_PER_HUNT[_kind(u)] for u in verify_units) # passes a, b + scale = min(1.0, ceiling * 0.98 / wanted) if wanted else 1.0 + workspaces: list[dict] = [] + tasks: list[dict] = [] + entries: list[dict] = [] + for unit in verify_units: + w, t, e = _verify_docs(unit, templates, USD_PER_HUNT[_kind(unit)] * scale) + workspaces += w + tasks += t + entries += e + out.update(_run_files(header, f"{name}-verify", workspaces, tasks, entries, f"{len(verify_units)} units, {hunts} hunts per pass")) + emitted = {w["metadata"]["name"] for w in workspaces} # a pin Workspace both Runs bind is written once + workspaces, tasks, entries, estimates = [], [], [], [] + for group in _roles_groups([u for u in units if u.files and _inline(u, roles=True) <= MAX_INLINE_BYTES]): + files = {f"{REPO}/{u.name}/{p}": t for u in group for p, t in u.files if p in u.paths} + listing = sorted(f.removeprefix(f"{REPO}/") for f in files) + workspace = _files_workspace(_ax_name(name, "roles", group[0].name), files | {"inputs/files.json": _json(listing)}) + workspaces.append(workspace) + estimates.append(_roles_estimate((len(text), len(text.splitlines())) for text in files.values())) + bound = [(workspace["metadata"]["name"], CASE_PATH)] + env = {"OUSAST_WORKSPACE_DIR": f"{CASE_PATH}/{REPO}", "OUSAST_INPUT_FILES": f"{CASE_PATH}/inputs/files.json"} + tasks.append(_task(templates["roles"], f"roles-{group[0].name}", env, bound)) + entries.append( + {"name": f"{group[0].name}-roles", "task": f"roles-{group[0].name}", "outputs": ["roles.json", "units.jsonl", "summary.json"]} + ) + seen: set[tuple[str, str]] = set() + for unit in (u for u in units if not u.files): + if (unit.repo, unit.pin) in seen: + continue + seen.add((unit.repo, unit.pin)) + same = [u for u in units if not u.files and (u.repo, u.pin) == (unit.repo, unit.pin)] + paths = sorted({p for u in same for p in u.paths}) + known = {p: (chars, lines) for u in same for p, chars, lines in u.sizes} + pinned = _git_workspace(unit) + files_doc = _files_workspace(_ax_name(unit.name, "roles-in"), {"files.json": _json(paths)}) + workspaces += [w for w in (pinned, files_doc) if w["metadata"]["name"] not in emitted] + estimates.append(_roles_estimate(known.get(p, UNKNOWN_SIZE) for p in paths)) + bound = [(pinned["metadata"]["name"], CASE_PATH), (files_doc["metadata"]["name"], INPUTS_PATH)] + env = {"OUSAST_WORKSPACE_DIR": f"{CASE_PATH}/{REPO}", "OUSAST_INPUT_FILES": f"{INPUTS_PATH}/files.json"} + tasks.append(_task(templates["roles"], f"roles-{unit.name}", env, bound)) + entries.append( + {"name": f"{unit.name}-roles", "task": f"roles-{unit.name}", "outputs": ["roles.json", "units.jsonl", "summary.json"]} + ) + wanted = sum(usd for usd, _ in estimates) * ROLES_HEADROOM + scale = min(1.0, ceiling * 0.98 / wanted) if wanted else 1.0 + for entry, (usd, chunks) in zip(entries, estimates, strict=True): + entry["budget"] = {"usd": max(math.floor(usd * ROLES_HEADROOM * scale * 1000) / 1000, 0.001), "calls": chunks * 2 + 2} + out.update(_run_files(header, f"{name}-roles", workspaces, tasks, entries, f"{len(entries)} roles tasks")) + index = {u.name: u.index_row() | ({"skipped": oversized(u)} if oversized(u) else {}) for u in units} + out["units.json"] = json.dumps(index, indent=1, sort_keys=True) + "\n" + return out + + +def _run_files(header: str, run: str, workspaces: list[dict], tasks: list[dict], entries: list[dict], note: str) -> dict[str, str]: + total = sum(float(e["budget"]["usd"]) for e in entries) + metadata = {"name": run, "annotations": {"openultrasast.io/population": "harvest", "openultrasast.io/split": "learn"}} + doc = {"apiVersion": RUN_API_VERSION, "kind": "Run", "metadata": metadata, "spec": {"tasks": entries}} + files = {f"workspaces/{run}.yaml": _dump(header, workspaces), f"tasks/{run}.yaml": _dump(header, tasks)} + files[f"runs/{run}.yaml"] = _dump(header + f"# {note}; usd ceiling {total:.3f}.\n", [doc]) + return files + + +def write_harvest( + labels: Path, plane: Path, *, name: str, command: str, catalog: Path, cache: Path, templates: Path, ceiling: float = 10.0 +) -> dict[str, str]: + """Build the units, render, write under ``plane`` (with the Model manifests copied beside); returns the rendered + files.""" + from .manifests import load_manifests + + units = pair_units(labels, catalog) + population_units(labels, cache) + loaded = load_manifests([templates / "tasks" / f"{n}.yaml" for n in ("repo-facts", "verify", "agree", "roles")]).tasks + files = render_harvest(units, loaded, name, command, ceiling=ceiling) + for path in sorted((templates / "models").glob("*.yaml")): + files[f"models/{path.name}"] = path.read_text(encoding="utf-8") + for relative, text in files.items(): + target = plane / relative + target.parent.mkdir(parents=True, exist_ok=True) + target.write_text(text, encoding="utf-8") + yaml.safe_load_all(files[f"runs/{name}-verify.yaml"]) # parse check + return files diff --git a/tests/test_learn_roles.py b/tests/test_learn_roles.py index df7e4b7..9068e4c 100644 --- a/tests/test_learn_roles.py +++ b/tests/test_learn_roles.py @@ -156,6 +156,18 @@ def test_roles_feature_part(tmp_path: Path) -> None: assert [f.name for f in functions_of("a.php", lines, "php")][:2] == ["run_sql", "fetch_rows"] +def test_a_language_without_declarations_is_none_not_failed() -> None: + """C has no declaration pattern: the span and wrapper roles cannot run there (no coverage), which is not a broken + instrument (harvest 2026-10-01: every memory pair read as `failed`).""" + from openultrasast.learn.features import source_part + from openultrasast.learn.roles import RoleSet + + lines = ["int f(char *s) {", " return strcpy(buf, s);", "}"] + assert source_part(lines, "c", "f").state == "none" + assert roles_part(None, RoleSet(sources=("inferred_wrapper",)), "c").state == "none" + assert source_part(["def g():", " pass"], "python", "missing").state == "failed" + + def test_build_for_scan_records_the_roles_instrument(tmp_path: Path) -> None: from openultrasast.findings import quick_scan_findings from openultrasast.preprocess import preprocess_repository diff --git a/tests/test_plane_harvest.py b/tests/test_plane_harvest.py new file mode 100644 index 0000000..7e772bf --- /dev/null +++ b/tests/test_plane_harvest.py @@ -0,0 +1,100 @@ +"""The decision engine's harvest Runs (learned-decision-engine task 5): `ousast plane harvest`'s generator.""" + +from __future__ import annotations + +import json +import subprocess +from pathlib import Path + +import yaml + +from openultrasast.plane import reconciler +from openultrasast.plane.harvest import MAX_INLINE_BYTES, Unit, blob_sha1, declaration_line, render_harvest +from openultrasast.plane.manifests import load_manifests + +PLANE = Path("plane") +PIN = "0123456789abcdef0123456789abcdef01234567" + + +def _templates() -> dict: + return load_manifests([PLANE / "tasks" / f"{n}.yaml" for n in ("repo-facts", "verify", "agree", "roles")]).tasks + + +def _units() -> list[Unit]: + code = "import os\n\ndef run(cmd):\n os.system(cmd)\n" + excerpt = Unit("h-aaaaaaaaaaaa", "pairs", "x-cwe-078-os-system", "vulnerable", "pairs/github/x", blob_sha1(code), "injection", + (("app.py", "run", 3),), (("app.py", code),)) # fmt: skip + roles_only = Unit("h-bbbbbbbbbbbb", "pairs", "y-cwe-416", "fixed", "pairs/vfc/y", blob_sha1("int f(){}\n"), "", + (("f.c", "f", 1),), (("f.c", "int f(){}\n"),)) # fmt: skip + big = "x = 1\n" * (MAX_INLINE_BYTES // 6 + 10) + oversized = Unit( + "h-cccccccccccc", + "pairs", + "z", + "vulnerable", + "pairs/github/z", + blob_sha1(big), + "injection", + (("big.py", GLOBAL_, 1),), + (("big.py", big),), + ) + repo = Unit("h-dddddddddddd", "population-v1", "case", "vulnerable", "https://github.com/o/r", PIN, "injection", + (("a.py", "q", 10), ("b.py", "r", 20)), sizes=(("a.py", 4000, 100), ("b.py", 8000, 900))) # fmt: skip + return [excerpt, roles_only, oversized, repo] + + +GLOBAL_ = "" + + +def _render(tmp_path: Path, ceiling: float = 10.0) -> tuple[dict[str, str], Path]: + files = render_harvest(_units(), _templates(), "hv", "test", ceiling=ceiling) + for rel, text in files.items(): + (tmp_path / rel).parent.mkdir(parents=True, exist_ok=True) + (tmp_path / rel).write_text(text) + (tmp_path / "models").mkdir() + (tmp_path / "models" / "deepseek-flash.yaml").write_text((PLANE / "models" / "deepseek-flash.yaml").read_text()) + return files, tmp_path + + +def test_verify_run_shape_and_ceiling(tmp_path: Path) -> None: + files, root = _render(tmp_path, ceiling=0.05) + run, manifests = reconciler.load_run(root / "runs" / "hv-verify.yaml") + names = [t.name for t in run.tasks] + assert names == [ + "h-aaaaaaaaaaaa-va", "h-aaaaaaaaaaaa-vb", "h-aaaaaaaaaaaa-agree", + "h-dddddddddddd-facts", "h-dddddddddddd-va", "h-dddddddddddd-vb", "h-dddddddddddd-agree", + ] # fmt: skip + assert sum(float(t.budget.usd) for t in run.tasks) <= 0.05 # scaled down to the ceiling + assert all(float(t.budget.usd) == 0 for t in run.tasks if t.name.endswith(("-agree", "-facts"))) + excerpt = manifests.tasks["verify-a-h-aaaaaaaaaaaa"] + env = {e.name: e.value for e in excerpt.env} + assert env["OUSAST_WORKSPACE_DIR"].endswith("/repo") and "/inputs/" in env["OUSAST_INPUT_CANDIDATES"] + workspace = manifests.workspaces["h-aaaaaaaaaaaa-side"] + paths = {f.path for f in workspace.files} + assert paths == {"repo/app.py", "inputs/candidates.json"} # candidates outside the hunt's root + assert "cwe" not in yaml.safe_dump([workspace.metadata.name, *paths]) # the pair's name never reaches the sandbox + units = json.loads(files["units.json"]) + assert "skipped" in units["h-cccccccccccc"] and "skipped" not in units["h-aaaaaaaaaaaa"] + agree = {e.name: e.value for e in manifests.tasks["agree-h-dddddddddddd"].env} + assert "OUSAST_INPUT_SITES" not in agree # no declared sites: no site_match is computed + + +def test_roles_run_groups_excerpts_and_pins(tmp_path: Path) -> None: + _, root = _render(tmp_path) + run, manifests = reconciler.load_run(root / "runs" / "hv-roles.yaml") + assert len(run.tasks) == 2 and sum(float(t.budget.usd) for t in run.tasks) <= 10.0 + grouped = next(w for w in manifests.workspaces.values() if w.metadata.name.startswith("hv-roles")) + listing = json.loads(next(f.content for f in grouped.files if f.path == "inputs/files.json")) + assert listing == ["h-aaaaaaaaaaaa/app.py", "h-bbbbbbbbbbbb/f.c"] # the oversized excerpt is left out + assert sum(len(f.content) for f in grouped.files) <= MAX_INLINE_BYTES + 200 + pin = manifests.tasks["roles-h-dddddddddddd"] + assert {e.name: e.value for e in pin.env}["OUSAST_INPUT_FILES"].endswith("/files.json") + + +def test_blob_sha1_is_gits_and_declaration_lines() -> None: + text = "def a():\n pass\n\ndef b():\n pass\n" + done = subprocess.run(["git", "hash-object", "--stdin"], input=text, capture_output=True, text=True, check=True) + assert blob_sha1(text) == done.stdout.strip() + lines = text.splitlines() + assert declaration_line(lines, "m.py", "b") == 4 + assert declaration_line(lines, "m.py", "missing") == 1 and declaration_line(None, "m.py", "a") == 1