chore(p0): drop never-enabled hunter tools, test-only functions and receiver delivery

Off every active line (PATHFINDER-2026-10-13/03-unified-proposal.md, cuts).
Deleted: the four hunter tools no caller enables (read_definition,
entry_points, flows, obligations) with their schemas and dispatch, the dead
mapping branch of find_refs; 20 functions whose only caller was one test
(mapping task records, compute_ranking_metrics, build_quick_hunter_prompt,
validate_evidence_transition, should_fuse, harness processors,
rank_targets_with_model, model/report slice rendering, taxonomy
hierarchical/related, config_value.verdict, taint.verdict, roles.load_roles,
features.language_of and build_for_scan, folds.leave_one_*,
features.recorded_case, search BatchExecutor and the three __main__ shims,
delivery.put_input); the receiver delivery mode of the plane. push_inputs.py
moved to benchmarks/push. memory.purge stays: the live-S3 test cleanup uses
it. Net: 2,730 lines deleted, 207 added. 473 targeted tests passed; 3,282
tests collect. breadth.toml records the removals.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
Marius Ciepluch
2026-10-13 11:06:09 +02:00
co-authored by Claude Fable 5.1
parent f5a904b7f2
commit f63703a604
60 changed files with 212 additions and 2576 deletions
-42
View File
@@ -47,16 +47,6 @@ class FalsePositiveLearning:
demotion: float = 0.5
@dataclass(frozen=True)
class RankingMetrics:
tier: str
verified_findings: int
false_positives: int
time_to_verification_seconds: float
missed_fixture_vulnerabilities: int
false_positive_reasons: dict[str, int]
@dataclass(frozen=True)
class RankingCalibration:
path: str
@@ -105,38 +95,6 @@ def learning_from_finding_outcome(
)
def compute_ranking_metrics(events: list[dict[str, object]], *, tier: str) -> RankingMetrics:
verified = 0
false_positives = 0
reason_counts: dict[str, int] = {}
verification_times: list[float] = []
missed = 0
for event in events:
if event.get("tier") != tier:
continue
outcome = event.get("outcome")
if outcome == FindingOutcome.VERIFIED:
verified += 1
if outcome in _FALSE_POSITIVE_OUTCOMES:
false_positives += 1
reason = str(event.get("reason", FalsePositiveReason.UNVERIFIED))
reason_counts[reason] = reason_counts.get(reason, 0) + 1
duration = event.get("time_to_verification_seconds")
if isinstance(duration, int | float) and not isinstance(duration, bool):
verification_times.append(float(duration))
missed_value = event.get("missed_fixture_vulnerabilities", 0)
if isinstance(missed_value, int) and not isinstance(missed_value, bool):
missed += missed_value
return RankingMetrics(
tier=tier,
verified_findings=verified,
false_positives=false_positives,
time_to_verification_seconds=round(sum(verification_times) / len(verification_times), 2) if verification_times else 0.0,
missed_fixture_vulnerabilities=missed,
false_positive_reasons=dict(sorted(reason_counts.items())),
)
def calibrate_ranking(ranking: RankingScore, learnings: list[FalsePositiveLearning]) -> RankingCalibration:
applicable = [learning for learning in learnings if _learning_applies(ranking.path, learning)]
calibrated = ranking.priority
+1 -1
View File
@@ -548,7 +548,7 @@ def _run_scan(path: Path, config_path: Path, mode: str, fail_on: str) -> ScanOut
config=config,
trace_writer=HarnessTraceWriter(run.root / "trace" / "events.jsonl", redact=config.hardening.redact_secrets),
)
write_harness_config(config=config, processors=[], contract_mode="strict", path=run.root / "harness.json")
write_harness_config(config=config, path=run.root / "harness.json")
runtime.start(mode=mode, target=run.target)
policy = runtime.run_stage("policy_load", load_policy)
ledger_path = run.target / CALIBRATION_DIR / "false_positive_learnings.json"
-11
View File
@@ -99,17 +99,6 @@ def uncovered_languages(
return sorted(counts.items())
def build_quick_hunter_prompt(target: FileTarget, source_excerpt: str) -> str:
return (
"Review this file for security-relevant issues. Return structured findings only when evidence exists.\n"
f"Path: {target.path}\n"
f"Language: {target.language}\n"
f"Tags: {', '.join(target.tags) or 'none'}\n"
"Source excerpt:\n"
f"{source_excerpt[:4000]}"
)
def write_findings(findings: list[StaticFinding], path: Path) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
payload = {"findings": [asdict(finding) for finding in findings]}
+1 -6
View File
@@ -91,10 +91,6 @@ def fusion_triggers(
return sorted(triggers)
def should_fuse(finding: StaticFinding, verification: VerificationResult | None = None, *, high_assurance: bool = False) -> bool:
return bool(fusion_triggers(finding, verification, high_assurance=high_assurance))
def _panel_a(finding: StaticFinding, verification: VerificationResult | None) -> PanelVerdict:
"""Vulnerability-leaning panel: steel-mans the vulnerability case."""
reachable = finding.reachability_status == "reachable"
@@ -213,5 +209,4 @@ __all__ = [
"fuse_finding",
"fuse_findings",
"fusion_triggers",
"should_fuse",
]
]
+1 -77
View File
@@ -6,7 +6,7 @@ from collections.abc import Callable
from dataclasses import asdict, dataclass, field
from datetime import UTC, datetime
from pathlib import Path
from typing import Any, Literal, Protocol
from typing import Any, Literal
from .config import ResolvedConfig
@@ -21,11 +21,6 @@ EventType = Literal[
"task_end",
]
ContractMode = Literal["strict", "warn"]
class HarnessContractError(ValueError):
"""Raised when a processor violates its declared state contract."""
@dataclass(frozen=True)
@@ -37,27 +32,9 @@ class HarnessEvent:
payload: dict[str, Any] = field(default_factory=dict)
@dataclass(frozen=True)
class ProcessorSpec:
name: str
version: str
handles: tuple[EventType, ...]
reads: tuple[str, ...] = ()
writes: tuple[str, ...] = ()
class Processor(Protocol):
spec: ProcessorSpec
def handle(self, event: HarnessEvent, state: dict[str, Any]) -> dict[str, Any] | None:
raise NotImplementedError
@dataclass(frozen=True)
class SerializedHarnessConfig:
contract_mode: ContractMode
model_roles: dict[str, str | None]
processor_versions: dict[str, str]
prompt_hashes: dict[str, str]
@@ -84,14 +61,10 @@ class HarnessRuntime:
scan_id: str,
config: ResolvedConfig,
trace_writer: HarnessTraceWriter,
processors: list[Processor] | None = None,
contract_mode: ContractMode = "strict",
) -> None:
self.scan_id = scan_id
self.config = config
self.trace_writer = trace_writer
self.processors = processors or []
self.contract_mode = contract_mode
self.state: dict[str, Any] = {"degradations": []}
def start(self, *, mode: str, target: Path) -> None:
@@ -119,71 +92,22 @@ class HarnessRuntime:
payload=payload,
)
self.trace_writer.append(event)
for processor in self.processors:
if event_type in processor.spec.handles:
self._run_processor(processor, event)
def _run_processor(self, processor: Processor, event: HarnessEvent) -> None:
before = set(self.state)
missing_reads = [key for key in processor.spec.reads if key not in self.state]
if missing_reads:
self._contract_violation(processor.spec.name, f"missing reads: {', '.join(missing_reads)}")
return
updates = processor.handle(event, dict(self.state)) or {}
if not isinstance(updates, dict):
self._contract_violation(processor.spec.name, "processor returned a non-dict update")
return
undeclared = sorted(set(updates) - set(processor.spec.writes))
if undeclared:
self._contract_violation(processor.spec.name, f"undeclared writes: {', '.join(undeclared)}")
return
self.state.update(updates)
changed = sorted(set(self.state) - before | set(updates))
self.trace_writer.append(
HarnessEvent(
event_type=event.event_type,
scan_id=self.scan_id,
stage=event.stage,
timestamp=datetime.now(UTC).isoformat(),
payload={"processor": processor.spec.name, "changed_state": changed},
)
)
def _contract_violation(self, processor_name: str, reason: str) -> None:
message = f"processor {processor_name} contract violation: {reason}"
if self.contract_mode == "strict":
raise HarnessContractError(message)
self.state.setdefault("degradations", []).append(message)
self.trace_writer.append(
HarnessEvent(
event_type="stage_end",
scan_id=self.scan_id,
stage="processor_contract",
timestamp=datetime.now(UTC).isoformat(),
payload={"status": "degraded", "reason": message},
)
)
def write_harness_config(
*,
config: ResolvedConfig,
processors: list[Processor],
contract_mode: ContractMode,
path: Path,
prompts: dict[str, str] | None = None,
) -> None:
prompts = prompts or {}
serialized = SerializedHarnessConfig(
contract_mode=contract_mode,
model_roles={
"ranker": config.models.ranker,
"hunter": config.models.hunter,
"verifier": None, # schema kept byte for byte; LLM verification runs on the plane
"patcher": None, # schema kept; `[models] patcher` was retired 2026-10-02 (never read)
},
processor_versions={processor.spec.name: processor.spec.version for processor in processors},
prompt_hashes={name: _hash_text(prompt) for name, prompt in sorted(prompts.items())},
)
path.parent.mkdir(parents=True, exist_ok=True)
+3 -237
View File
@@ -2,10 +2,10 @@ from __future__ import annotations
import os
import re
from collections.abc import Iterable, Iterator, Mapping
from collections.abc import Iterator
from pathlib import Path
from .preprocess import FileTarget, enumerate_source_files
from .preprocess import enumerate_source_files
class PathEscapesRepo(ValueError):
@@ -80,25 +80,9 @@ def grep_repo(root: Path, pattern: str, *, max_matches: int) -> list[dict[str, o
return matches
def find_refs(root: Path, symbol: str, mapping_index: object) -> list[dict[str, object]]:
def find_refs(root: Path, symbol: str) -> list[dict[str, object]]:
resolved_root = root.resolve()
refs: list[dict[str, object]] = []
for record in _iter_mapping_records(mapping_index):
if not _record_matches_symbol(record, symbol):
continue
raw_path = _record_path(record)
if raw_path is None:
continue
clamped = clamp_repo_path(resolved_root, raw_path)
name = _record_value(record, "name", "function_name", "symbol")
refs.append(
{
"path": clamped.relative_to(resolved_root).as_posix(),
"name": str(name) if name is not None else symbol,
"line": _as_line(_record_value(record, "line")),
"source": "mapping",
}
)
if symbol:
compiled = re.compile(rf"\b{re.escape(symbol)}\b")
for relative, lines in _source_lines(resolved_root):
@@ -113,221 +97,3 @@ def _iter_clamped_source_files(root: Path) -> Iterator[tuple[str, Path]]:
for path in enumerate_source_files(resolved_root):
relative = path.relative_to(resolved_root).as_posix()
yield relative, clamp_repo_path(resolved_root, relative)
def _iter_mapping_records(mapping_index: object) -> Iterable[object]:
if mapping_index is None:
return ()
if isinstance(mapping_index, Mapping):
return (mapping_index,)
if isinstance(mapping_index, (str, bytes, bytearray)):
return ()
if isinstance(mapping_index, Iterable):
return mapping_index
return (mapping_index,)
def _record_matches_symbol(record: object, symbol: str) -> bool:
for field in ("name", "function_name", "symbol"):
value = _record_value(record, field)
if value is not None and str(value) == symbol:
return True
return False
def _record_path(record: object) -> str | None:
value = record.get("path") if isinstance(record, Mapping) else getattr(record, "path", None)
if value is None:
return None
return str(value)
def _record_value(record: object, *names: str) -> object | None:
for name in names:
raw: object | None
if isinstance(record, Mapping):
if name not in record:
continue
raw = record[name]
else:
raw = getattr(record, name, None)
if raw is not None:
return raw
return None
def _as_line(value: object) -> int | None:
if isinstance(value, bool) or value is None:
return None
if isinstance(value, int):
return value
return None
# --- curated structural tools (learning-harness, Req 6.4, 6.5) --------------------------------------
# A detector reads code with `read_file` and `read_definition`; the three list tools below answer with
# identifiers and line spans only, so they can never become a second way to dump the repository. Every
# path a caller supplies is clamped to the scan root first. No caching: a detector calls these a handful
# of times per region, and a stale view of a tree that a round may have rewritten is worse than a reparse.
def read_definition(root: Path, symbol: str, *, max_chars: int, path: str | None = None) -> dict[str, object] | None:
"""The definition of one named function: where it is, how far it runs, and its text. None when absent."""
resolved_root = root.resolve()
scope = clamp_repo_path(resolved_root, path) if path is not None else None
for relative, target in _iter_clamped_source_files(resolved_root):
if scope is not None and target != scope:
continue
text = target.read_text(errors="ignore")
span = _named_span(relative, text, symbol)
if span is None:
continue
lines = text.splitlines()
start, end = span
while start > 1 and lines[start - 2].lstrip().startswith("@"):
start -= 1 # a handler's decorators are part of what the detector must see
body = "\n".join(lines[start - 1 : end])
return {"path": relative, "name": symbol, "start_line": start, "end_line": end, "text": body[: max(max_chars, 0)]}
return None
def entry_points(root: Path, *, path: str | None = None) -> list[dict[str, object]]:
"""Handlers the mapper names, with their access classification and evidence. No source text."""
from .mapping import analyze_entry_points
resolved_root = root.resolve()
relative = _relative_or_none(resolved_root, path)
targets = _repo_targets(resolved_root)
rows: list[dict[str, object]] = []
for record in analyze_entry_points(resolved_root, targets):
if relative is not None and record.path != relative:
continue
rows.append(
{
"path": record.path,
"function": record.function_name,
"name": record.name,
"kind": record.kind,
"line": record.line,
"end_line": record.end_line,
"access_level": record.access_level,
"evidence": list(record.access_evidence),
}
)
return rows
def flows(root: Path, *, path: str, function: str) -> list[dict[str, object]]:
"""Adjudicated source-to-sink records inside one function: which source, which sink, what the overlay said."""
from .findings import quick_scan_findings
from .rank import rank_targets
from .semantic import adjudicate
resolved_root = root.resolve()
relative = _relative_or_none(resolved_root, path)
targets = _repo_targets(resolved_root)
span = _span_in(resolved_root, relative, function)
if span is None:
return []
findings = [item for item in quick_scan_findings(resolved_root, targets, rank_targets(targets), None) if item.status != "shadow"]
rows: list[dict[str, object]] = []
for record in adjudicate(root=resolved_root, targets=targets, findings=findings):
if record.path != relative or record.line is None or not span[0] <= record.line <= span[1]:
continue
rows.append(
{
"path": record.path,
"function": function,
"line": record.line,
"sources": list(record.sources),
"sinks": list(record.sinks),
"disposition": record.disposition,
"cwe": record.cwe,
}
)
return rows
def obligations(root: Path, *, path: str, function: str) -> list[dict[str, object]]:
"""What one function owes and has not discharged: the operation, the missing discharger, why it is owed."""
from .config import ObligationsConfig
from .semantic.facts import FactLoadError, load_facts
from .semantic.ir import parse_file
from .semantic.obligations import check_obligations, load_obligation_facts
from .semantic.obligations.dominance import OrderDominance
resolved_root = root.resolve()
relative = _relative_or_none(resolved_root, path)
targets = _repo_targets(resolved_root)
try:
flow_facts = load_facts()
except FactLoadError:
return []
irs: dict[str, tuple[object, str]] = {}
texts: dict[str, str] = {}
for target in targets:
try:
text = (resolved_root / target.path).read_text(errors="ignore")
except OSError:
continue
texts[target.path] = text
irs[target.path] = (parse_file(target.path, text, target.language), text)
from .mapping import analyze_entry_points
result = check_obligations(
irs=irs, # type: ignore[arg-type]
entries=analyze_entry_points(resolved_root, targets),
facts=load_obligation_facts(),
flow_facts=flow_facts,
policy=None,
paths=(),
dominance=OrderDominance(texts=texts),
store_shapes=(),
min_siblings=ObligationsConfig().min_siblings,
)
return [
{
"path": finding.operation.path,
"function": finding.operation.function,
"line": finding.operation.line,
"operation": finding.operation.kind,
"resource": finding.operation.resource,
"missing": finding.missing,
"label": finding.label,
"evidence": list(finding.evidence),
}
for finding in result.findings
if (relative is None or finding.operation.path == relative) and finding.operation.function == function
]
def _repo_targets(root: Path) -> list[FileTarget]:
from .preprocess import preprocess_repository
_, targets = preprocess_repository(root)
return list(targets)
def _relative_or_none(root: Path, path: str | None) -> str | None:
if path is None:
return None
return clamp_repo_path(root, path).relative_to(root).as_posix()
def _named_span(relative: str, text: str, symbol: str) -> tuple[int, int] | None:
from .preprocess import detect_language
from .semantic.functions import named_function_ranges, spans_named
ranges = named_function_ranges(relative, text, detect_language(Path(relative)))
spans = spans_named(ranges or (), symbol)
return spans[0] if spans else None
def _span_in(root: Path, relative: str | None, function: str) -> tuple[int, int] | None:
if relative is None:
return None
try:
text = (root / relative).read_text(errors="ignore")
except OSError:
return None
return _named_span(relative, text, function)
+5 -135
View File
@@ -1,10 +1,6 @@
"""Feature records per candidate, from the signals every instrument emitted (learned-decision-engine design section 1).
No model. The same per-instrument builders serve two callers:
- the plane task ``features`` (:mod:`..plane.tasks.features`), which reads a case's delivered artifacts;
- the host: :func:`build_for_scan` over a checkout's quick findings and an engine result, for ``scan`` and
``pre-push`` (a whole-repository scan, or a base..head delta with :class:`Delta`).
The plane task ``features`` (:mod:`..plane.tasks.features`) reads a case's delivered artifacts.
A candidate is ``(path, function, family)`` of one repository and pin: a function where at least one instrument
emitted a signal of that family. The key stays in the record's envelope (``candidate`` is ``path::function``); the
@@ -26,8 +22,8 @@ from typing import Any
from ..model.taxonomy import load_families
from ..plane.tasks.repo_facts import _COMMENT_PREFIXES, DECLARATION, GLOBAL, _declared_name, enclosing, product_files
from ..preprocess import build_file_target, detect_language
from ..ruleset import DEFAULT_RULESET_DIR, PatternRule, load_ruleset
from .roles import CWE_OPERATION, SOURCE_KIND, VERSION, Function, RoleSet, body_roles, functions_of, infer_for_checkout, origin_of
from ..ruleset import PatternRule
from .roles import CWE_OPERATION, SOURCE_KIND, VERSION, Function, RoleSet, body_roles, functions_of, origin_of
from .schema import (
BY_NAME,
LANGUAGES,
@@ -62,12 +58,6 @@ NONE = Part("none")
NOT_APPLICABLE = Part("not_applicable")
def language_of(path: str) -> str:
"""The closed language of a path (its extension), ``other`` outside :data:`.schema.LANGUAGES`."""
language = detect_language(Path(path))
return language if language in LANGUAGES else "other"
def _clip(name: str, value: Any) -> Any:
spec = BY_NAME[name]
if value is None or spec.type != "int" or isinstance(value, bool):
@@ -328,18 +318,6 @@ def function_of(lines: Sequence[str] | None, path: str, language: str, function:
# --- the host path ---------------------------------------------------------------------------------------------------
@dataclass(frozen=True)
class Delta:
"""A base..head delta: the base checkout, the head-side changed line ranges per path (``git diff -U0``), and the
base's quick findings and engine result, whose signals decide novelty."""
root: Path
changed: Mapping[str, Sequence[tuple[int, int]]]
findings: Sequence[Any] = ()
engine_result: Mapping[str, Any] | None = None
pin: str | None = None
class SourceIndex:
"""A checkout's product files read once: lines per path, the enclosing function of a line, and the name-level
caller graph (the repo-facts patterns) for the entry-point distance."""
@@ -423,12 +401,6 @@ def _family_of(rule: PatternRule | None) -> str:
return family.id if family is not None else UNKNOWN_FAMILY
def _engine_state(result: Mapping[str, Any] | None) -> str:
if result is None:
return "none"
return "ran" if int(result.get("questions") or 0) > 0 else "failed"
def _signals(
index: SourceIndex, findings: Sequence[Any], engine_result: Mapping[str, Any] | None, rules: Mapping[str, PatternRule]
) -> tuple[dict[tuple[str, str, str], list[tuple[str, str]]], dict[tuple[str, str, str], list[Mapping[str, Any]]]]:
@@ -447,108 +419,6 @@ def _signals(
return quick, engine
def _changed_in(index: SourceIndex, changed: Mapping[str, Sequence[tuple[int, int]]]) -> dict[tuple[str, str], int]:
"""(path, function) -> changed lines inside it, by the head's enclosing declarations."""
counts: dict[tuple[str, str], int] = {}
for path, ranges in changed.items():
for lo, hi in ranges:
for line in range(int(lo), int(hi) + 1):
key = (path, index.function_at(path, line))
counts[key] = counts.get(key, 0) + 1
return counts
def _body_calls(lines: Sequence[str] | None, language: str, function: str) -> set[str]:
if not lines:
return set()
return {m.group(1) for i, line in enumerate(lines) if enclosing(lines, i, language) == function for m in _CALL.finditer(line)}
def build_for_scan(
root: Path,
findings: Sequence[Any],
engine_result: Mapping[str, Any] | None,
*,
base: Delta | None = None,
rules: Sequence[PatternRule] | None = None,
quick_languages: Iterable[str] | None = None,
engine_languages: Iterable[str] | None = None,
roles: RoleSet | None | bool = True,
) -> list[dict[str, Any]]:
"""``static`` records for a checkout: one per (path, function, family) with a quick or engine signal. ``findings``
are the quick findings (enabled and shadow alike), ``engine_result`` the engine's record (``findings`` with
``site``/``family``/``rung``/``witness``, ``questions``, ``completed``, ``degradations``; ``None`` when the engine
did not run, ``questions == 0`` is ``failed``). With ``base`` only functions enclosing a changed line are
candidates, and ``delta.*`` compares their signals with the base's. ``roles``: ``True`` infers wrapper roles over
the checkout (priors off), a :class:`.roles.RoleSet` is used as given, ``False``/``None`` leaves ``roles`` ``none``."""
from ..plane.tasks.alerts import covers
from ..plane.tasks.alerts import engine_languages as engine_covered
from ..plane.tasks.alerts import quick_languages as quick_covered
from ..semantic.facts import FactLoadError, load_facts
root = Path(root)
loaded = tuple(rules) if rules is not None else load_ruleset(DEFAULT_RULESET_DIR)
by_id = {rule.rule_id: rule for rule in loaded}
quick_langs = set(quick_languages if quick_languages is not None else quick_covered())
engine_langs = set(engine_languages if engine_languages is not None else engine_covered())
index = SourceIndex(root)
quick, engine = _signals(index, findings, engine_result, by_id)
keys = sorted(set(quick) | set(engine))
changed: dict[tuple[str, str], int] = {}
base_quick: dict[tuple[str, str, str], list[tuple[str, str]]] = {}
base_engine: dict[tuple[str, str, str], list[Mapping[str, Any]]] = {}
base_index: SourceIndex | None = None
if base is not None:
changed = _changed_in(index, base.changed)
keys = [k for k in keys if (k[0], k[1]) in changed]
base_index = SourceIndex(base.root)
base_quick, base_engine = _signals(base_index, base.findings, base.engine_result, by_id)
entries, file_entries = entry_names(root, {k[0] for k in keys}) if keys else (set(), set())
callers = index.callers() if keys else {}
quick_version = ruleset_digest(loaded)
engine_state = _engine_state(engine_result)
completed, questions = (engine_result or {}).get("completed"), (engine_result or {}).get("questions")
completion = int(completed) / int(questions) if isinstance(completed, int) and isinstance(questions, int) and questions else None
degraded = bool((engine_result or {}).get("degradations"))
vocabularies: dict[str, Vocabulary] = {}
inferred = (infer_for_checkout(root) if keys else RoleSet(sources=("inferred_wrapper",))) if roles is True else roles or None
try:
sanitizer_calls = {language: {c for s in load_facts().for_language(language).sanitizers for c in s.calls} for language in LANGUAGES}
except FactLoadError:
sanitizer_calls = {}
out: list[dict[str, Any]] = []
for key in keys:
path, function, family = key
language = detect_language(Path(path))
parts: dict[str, Part] = {}
parts["quick"] = quick_part(quick.get(key, []), by_id, quick_version) if covers(quick_langs, language) else NONE
if language in engine_langs and engine_state != "none":
vocabulary = vocabularies.setdefault(language, Vocabulary.load(language))
parts["engine"] = (
engine_part(engine.get(key, []), completion=completion, degraded=degraded, vocabulary=vocabulary)
if engine_state == "ran" else Part("failed")
) # 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)
distance = 0 if function == GLOBAL and path in file_entries else entry_distance(function, entries, callers)
parts["entry_points"] = Part("ran", {"facts.entry_distance": distance})
parts["facts"] = facts_part(_callers_of(index, callers, path, function), function)
if base is None or base_index is None:
parts["delta"] = NOT_APPLICABLE
else:
head_n = len(quick.get(key, [])) + len(engine.get(key, []))
base_n = len(base_quick.get(key, [])) + len(base_engine.get(key, []))
novelty = "new" if base_n == 0 else "worsened" if head_n > base_n else "unchanged"
known = sanitizer_calls.get(language, set())
removed = bool((_body_calls(base_index.lines(path), language, function) - _body_calls(lines, language, function)) & known)
values = {"delta.novelty": novelty, "delta.changed_lines": changed.get((path, function), 0), "delta.sanitizer_removed": removed}
parts["delta"] = Part("ran", values)
unit, pin = ("delta", base.pin) if base is not None else ("pin", None)
out.append(record(f"{path}::{function}", family, language, "static", parts, unit=unit, base=pin))
return out
def _callers_of(index: SourceIndex, callers: Mapping[str, set[str]], path: str, function: str) -> list[dict[str, Any]]:
"""The call sites of ``function`` in other product files, as repo-facts records them (capped at 40)."""
if function == GLOBAL or function not in callers:
@@ -567,8 +437,8 @@ def _callers_of(index: SourceIndex, callers: Mapping[str, set[str]], path: str,
__all__ = [
"CWE_OPERATION", "NONE", "NOT_APPLICABLE", "SOURCE_KIND", "Delta", "Part", "SourceIndex", "Vocabulary", "agree_part",
"CWE_OPERATION", "NONE", "NOT_APPLICABLE", "SOURCE_KIND", "Part", "SourceIndex", "Vocabulary", "agree_part",
"function_of", "model_sinks_part", "roles_part",
"build_for_scan", "engine_part", "entry_distance", "entry_names", "facts_part", "function_span", "language_of", "quick_part", "record",
"engine_part", "entry_distance", "entry_names", "facts_part", "function_span", "quick_part", "record",
"ruleset_digest", "source_part", "verify_part",
] # fmt: skip
+2 -34
View File
@@ -38,7 +38,7 @@ class FoldError(RuntimeError):
class Fold:
"""One evaluation (or compile, or deployment) boundary: the groups evaluated, and what retrieval must not use."""
kind: str # outer | leave_one_source_out | leave_one_framework_out | compile | deployment
kind: str # outer | compile | deployment
name: str
eval_groups: frozenset[str]
held_source: str | None = None
@@ -126,37 +126,6 @@ def outer_folds(examples: Sequence[Example], *, k: int = OUTER_K, seed: int = 0,
return [Fold("outer", f"outer-{i}", frozenset(groups)) for i, groups in enumerate(assigned)]
def leave_one_source_out(examples: Sequence[Example], *, exclude: Iterable[str] = ()) -> list[Fold]:
excluded = set(exclude)
by_source: dict[str, set[str]] = {}
for example in examples:
if example.group not in excluded:
by_source.setdefault(example.source, set()).add(example.group)
return [Fold("leave_one_source_out", f"source-{s}", frozenset(g), held_source=s) for s, g in sorted(by_source.items())]
def leave_one_framework_out(
examples: Sequence[Example], *, exclude: Iterable[str] = (), min_groups: int = FRAMEWORK_MIN_GROUPS
) -> list[Fold]:
"""One fold per framework seen; below ``min_groups`` groups it is ``insufficient data`` and evaluates nothing."""
excluded = set(exclude)
by_framework: dict[str, set[str]] = {}
for example in examples:
for framework in example.frameworks:
if example.group not in excluded:
by_framework.setdefault(framework, set()).add(example.group)
folds = []
for framework, groups in sorted(by_framework.items()):
enough = len(groups) >= min_groups
folds.append(
Fold(
"leave_one_framework_out", f"framework-{framework}", frozenset(groups) if enough else frozenset(),
held_framework=framework, status="ok" if enough else INSUFFICIENT,
)
) # fmt: skip
return folds
def assert_disjoint(split: CompileSplit, folds: Iterable[Fold]) -> None:
"""The compile split shares no group with any evaluation fold; outer folds are pairwise disjoint."""
seen: dict[str, str] = {}
@@ -181,6 +150,5 @@ def folds_digest(split: CompileSplit, folds: Iterable[Fold]) -> str:
__all__ = [
"COMPILE_FRACTION", "FRAMEWORK_MIN_GROUPS", "INSUFFICIENT", "OUTER_K", "CompileSplit", "Fold", "FoldError", "GroupInfo",
"assert_disjoint", "compile_split", "folds_digest", "group_table", "leave_one_framework_out", "leave_one_source_out",
"outer_folds",
"assert_disjoint", "compile_split", "folds_digest", "group_table", "outer_folds",
] # fmt: skip
+1 -6
View File
@@ -23,7 +23,6 @@ finding. A role's ``path``/``function`` are provenance, never features.
from __future__ import annotations
import json
import re
from collections import Counter
from collections.abc import Iterable, Mapping, Sequence
@@ -536,13 +535,9 @@ def _confidence(role: Role) -> float:
return round(1.0 / (role.depth + 1), 6)
def load_roles(path: Path) -> RoleSet:
return RoleSet.from_json(json.loads(Path(path).read_text(encoding="utf-8")))
__all__ = [
"CWE_OPERATION", "DEPENDENCY_DIRS", "MAX_DEPTH", "MODEL_ROLE_KINDS", "OPERATION_CWE", "ROLE_KINDS", "SOURCE_KIND", "VERSION",
"Function", "Role", "RoleSet", "base_roles", "body_roles", "dependency_files", "from_model_roles", "functions_of",
"infer_for_checkout", "infer_wrappers", "language_key", "load_roles", "mentions", "origin_of", "parameters",
"infer_for_checkout", "infer_wrappers", "language_key", "mentions", "origin_of", "parameters",
"vocabulary_overlay",
] # fmt: skip
-77
View File
@@ -35,31 +35,6 @@ class StaticHint:
evidence_candidate: bool = True
@dataclass(frozen=True)
class MappingTaskRecord:
kind: MappingKind
path: str
rule_id: str
task: str
rationale: str
@dataclass(frozen=True)
class DifferentialMappingRecord:
path: str
change_kind: str
trust_boundary: str | None
rationale: str
@dataclass(frozen=True)
class SharpEdgeRecord:
path: str
category: str
surface: str
rationale: str
@dataclass(frozen=True)
class EntryPointRecord:
path: str
@@ -185,58 +160,6 @@ def attach_reachability_hints(targets: list[FileTarget], entry_points: list[Entr
return [replace(target, reachability_hints=hints_by_path.get(target.path, target.reachability_hints)) for target in targets]
def verifier_evidence_candidates(hints: list[StaticHint], *, path: str | None = None) -> list[dict[str, object]]:
selected = [hint for hint in hints if hint.evidence_candidate and (path is None or hint.path == path)]
return [
{
"source": hint.analyzer,
"rule_id": hint.rule_id,
"path": hint.path,
"line": hint.start_line,
"severity": hint.severity,
"message": hint.message,
"provenance": hint.provenance,
}
for hint in selected
]
def semgrep_mapping_tasks(hints: list[StaticHint]) -> list[MappingTaskRecord]:
return [
MappingTaskRecord(
kind="semgrep",
path=hint.path,
rule_id=hint.rule_id,
task="pattern_variant_review",
rationale="Use Semgrep evidence to check variants and rule precision before verifier promotion.",
)
for hint in hints
if hint.analyzer == "semgrep"
]
def codeql_mapping_tasks(hints: list[StaticHint]) -> list[MappingTaskRecord]:
return [
MappingTaskRecord(
kind="codeql",
path=hint.path,
rule_id=hint.rule_id,
task="source_sink_sanitizer_path_review",
rationale="Use CodeQL evidence to inspect source, sink, sanitizer, and path explanation.",
)
for hint in hints
if hint.analyzer == "codeql"
]
def differential_mapping_record(path: str, *, change_kind: str, trust_boundary: str | None, rationale: str) -> DifferentialMappingRecord:
return DifferentialMappingRecord(path=path, change_kind=change_kind, trust_boundary=trust_boundary, rationale=rationale)
def sharp_edge_record(path: str, *, category: str, surface: str, rationale: str) -> SharpEdgeRecord:
return SharpEdgeRecord(path=path, category=category, surface=surface, rationale=rationale)
def write_static_hints(hints: list[StaticHint], path: Path) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps({"static_hints": [asdict(hint) for hint in hints]}, indent=2, sort_keys=True) + "\n")
+1 -7
View File
@@ -87,12 +87,6 @@ def verdicts(cpg: CpgResult, spec: ConfigSpec, *, function: str = "", file: str
return []
def verdict(cpg: CpgResult, spec: ConfigSpec, *, function: str = "", file: str = "") -> Verdict | None:
"""The strongest single verdict, kept for callers that ask one question and want one answer."""
answers = verdicts(cpg, spec, function=function, file=file)
return answers[0] if answers else None
def _location(row: Mapping[str, object], file: str) -> str:
"""``path:line:function`` of the setting, when the region named a file.
@@ -156,4 +150,4 @@ def _settings(rows: object, spec: ConfigSpec, *, function: str) -> list[dict[str
return sorted(kept, key=lambda row: (str(row["method"]), _line(row), str(row["setting"])))
__all__ = ["request_params", "verdict", "verdicts"]
__all__ = ["request_params", "verdicts"]
+1 -26
View File
@@ -29,12 +29,10 @@ from typing import Any
from ..cpg.backend import CpgResult
from ..redaction import redact_secrets
from .config_value import verdict as config_verdict
from .config_value import verdicts as config_verdicts
from .dominance import verdict as dominance_verdict
from .ladder import Rung, Verdict
from .specs import ConfigSpec, DominanceSpec, TaintSpec
from .taint import verdict as taint_verdict
from .taint import verdicts as taint_verdicts
logger = logging.getLogger(__name__)
@@ -93,34 +91,11 @@ def _arbitrate_all(
if isinstance(spec, ConfigSpec):
return config_verdicts(cpg, spec, function=function, file=path)
if isinstance(spec, DominanceSpec):
answer = _arbitrate(cpg, spec, path=path, function=function, parameter_sources=parameter_sources, call_depth=call_depth)
answer = dominance_verdict(cpg, spec, function=function, file=path)
return [answer] if answer is not None else []
return taint_verdicts(cpg, spec, function=function, file=path, parameter_sources=parameter_sources, call_depth=call_depth)
def _arbitrate(
cpg: CpgResult,
spec: TaintSpec | DominanceSpec | ConfigSpec,
*,
path: str,
function: str,
parameter_sources: bool,
call_depth: int,
) -> Verdict | None:
"""Route a family to the arbiter that can actually decide it.
Dispatching on the spec type rather than on a family name keeps this honest: a family without an arbiter
cannot be silently routed to the wrong one. The pipeline first shipped taint-only, so access_control and
config_secrets -- whose arbiters had worked since group 4 -- produced nothing, and every one of their
pairs read as `both_silent` on the first end-to-end run.
"""
if isinstance(spec, DominanceSpec):
return dominance_verdict(cpg, spec, function=function, file=path)
if isinstance(spec, ConfigSpec):
return config_verdict(cpg, spec, function=function, file=path)
return taint_verdict(cpg, spec, function=function, file=path, parameter_sources=parameter_sources, call_depth=call_depth)
def scan_region(
cpg: CpgResult,
spec: TaintSpec | DominanceSpec | ConfigSpec,
+2 -80
View File
@@ -1,87 +1,9 @@
"""Reporting that cannot flatter itself (model-grounded-detection, Req 9.3, 11.1).
Three disciplines survive from the deleted publish/scoring layer, and each is here because its absence caused
a real error in this project's history rather than because it is tidy:
* **Per-slice rows, always.** A headline over mixed slices once hid that agent-written code scored 44.4%
while the hand-written development slice scored 83.3%. `render` refuses to produce anything without rows.
* **The overfitting gap beside the headline.** The distance between the development slice and the deciding
real-world one is stated, never left for the reader to compute. When a slice is missing the gap is
*absent*, not zero — reading a missing measurement as "no overfitting" is the flattering failure.
* **The rung on every finding.** A finding's strength is what established it; omitting the rung invites a
reader to treat a suspicion as a result.
"""
"""Report model coverage for the scanned regions."""
from __future__ import annotations
from collections.abc import Sequence
from dataclasses import dataclass
from ..findings import StaticFinding
@dataclass(frozen=True)
class SliceRow:
"""One slice's coverage. `covered` is pairs the model *split*, not pairs it flagged."""
slice: str
pairs: int
covered: int
@property
def rate(self) -> float:
return self.covered / self.pairs if self.pairs else 0.0
def overfitting_gap(rows: Sequence[SliceRow], *, development: str = "", deciding: str = "") -> float | None:
"""Development-slice coverage minus the deciding slice's, or ``None`` when either is absent.
``None`` rather than ``0.0`` on purpose: a slice that was never measured must not read as agreement
between them.
"""
by_name = {row.slice: row for row in rows}
if development not in by_name or deciding not in by_name:
return None
return by_name[development].rate - by_name[deciding].rate
def render(rows: Sequence[SliceRow], *, development: str = "", deciding: str = "") -> str:
"""The report. Raises when there are no per-slice rows, because a bare aggregate is the thing to prevent."""
if not rows:
raise ValueError("a report needs per-slice rows: there is no headline without them")
lines = ["## Model coverage", "", "| slice | pairs | covered | coverage |", "|---|---|---|---|"]
for row in sorted(rows, key=lambda r: r.slice):
lines.append(f"| {row.slice} | {row.pairs} | {row.covered} | {row.rate:.0%} |")
total_pairs = sum(row.pairs for row in rows)
total_covered = sum(row.covered for row in rows)
lines += [
"",
f"Across every slice: {total_covered}/{total_pairs} = {total_covered / total_pairs:.0%} — read the rows above it, not this line.",
]
gap = overfitting_gap(rows, development=development, deciding=deciding)
if gap is not None:
lines += [
"",
f"**Overfitting gap: {gap:+.0%}** — `{development}` (development) minus `{deciding}`, which is the deciding real-world slice.",
]
elif development or deciding:
lines += ["", f"**Overfitting gap: not measurable** — `{development}` or `{deciding}` is absent from this run."]
return "\n".join(lines) + "\n"
def render_findings(findings: Sequence[StaticFinding]) -> str:
"""Findings with the rung that established each one (Req 5.3)."""
lines = ["## Findings", "", "| finding | location | rung | severity |", "|---|---|---|---|"]
for finding in findings:
where = f"{finding.path}:{finding.line}" if finding.line else finding.path
lines.append(f"| {finding.title} | {where} | `{finding.rung}` | {finding.severity} |")
return "\n".join(lines) + "\n"
__all__ = ["SliceRow", "overfitting_gap", "render", "render_findings"]
__all__ = []
def coverage_rows(regions: Sequence[object]) -> list[dict[str, object]]:
+4 -45
View File
@@ -5,7 +5,7 @@ graph establishes it and the LLM's claim is no longer load-bearing. A path a san
`CORROBORATED`: the flow is real, but whether the sanitizer is *sufficient* is a semantic judgement the model
cannot make, and that residual is the LLM's — checked against this verdict rather than averaged over runs.
No path at all yields `None`, not a negative verdict: the caller reports `suspicion`. And a query that failed
No path at all yields an empty list, not a negative verdict: the caller reports `suspicion`. And a query that failed
yields `None` too, because "the engine could not decide" is not "there is no flow" — conflating them is how a
missing engine would silently become a clean bill of health.
"""
@@ -69,37 +69,6 @@ def request_params(
}
def verdict(
cpg: CpgResult, spec: TaintSpec, *, function: str = "", file: str = "", parameter_sources: bool = False, call_depth: int = 0
) -> Verdict | None:
"""The strongest verdict the taint query supports for ``spec`` in ``function``, or ``None`` to stay at suspicion.
``parameter_sources`` additionally treats the labeled function's own parameters as untrusted. That is right
for a function-level pair, where the function boundary *is* the trust boundary, and wrong for a whole
repository, where most parameters carry internal values — so it is off by default and the caller opts in.
An ENTRY POINT is the case where both are true at once: its parameters are attacker input by definition,
which is what the driver uses to decide.
``call_depth`` admits sinks that many call-graph levels below the labeled method. Zero keeps the old
function-local question; above zero is what lets a handler's parameter reach a sink in another module,
which is the shape VAmPI's SQL injection has and the shape a function-scoped question cannot see.
"""
# The strongest of the per-site verdicts, which is what this always meant. It used to run its own
# selection over the union of flows, and two copies of "which flow decides" is the duplication this
# module has been bitten by before: the per-site preference added to `verdicts` did not reach here, so
# the two entry points disagreed about the same rows.
#
# A flow the graph cannot see a discharge for is the finding; a sanitized or BOUNDED one is a question.
# libpng bounds `strcpy(outname+len, ".png")` with `(len = strlen(inname)) > 250` against a char[256],
# and entailing it is a claim the code contradicts. But the bound reaches the sink through an `error`
# flag rather than by dominating it, so the graph has not shown the copy is guarded either -- and a
# bound is no proof of safety in any case, off-by-one being the classic way they fail. Corroborated is
# the honest rung: the model cannot assert this, so the judge is asked instead of the site being
# silenced.
answers = verdicts(cpg, spec, function=function, file=file, parameter_sources=parameter_sources, call_depth=call_depth)
return answers[0] if answers else None
def _usable_flows(
cpg: CpgResult,
spec: TaintSpec,
@@ -137,17 +106,7 @@ def verdicts(
parameter_sources: bool = False,
call_depth: int = 0,
) -> list[Verdict]:
"""One verdict per distinct SINK SITE, strongest first.
``verdict`` answers "what is the strongest thing to say about this region", which is the right question
for a pair: one labelled function, one bug, one answer. It is the wrong question for a region that spans
a file. A PHP file with an SQL injection on line 6 and a command injection on line 13 got ONE injection
verdict, and `system` won it on flow length -- so the SQL injection went unreported, by construction
rather than by any failure of the analysis.
The first element is exactly what ``verdict`` returns, so the pair path and the committed measurements
that rest on it are unchanged.
"""
"""One verdict per distinct sink site, strongest first."""
flows = _usable_flows(cpg, spec, function=function, file=file, parameter_sources=parameter_sources, call_depth=call_depth)
if not flows:
return []
@@ -164,7 +123,7 @@ def verdicts(
preferred = [flow for flow in site_flows if _from_modelled_source(flow, spec)] or list(site_flows)
open_flows = [flow for flow in preferred if not flow["sanitized"] and not flow["bounded"]]
chosen.append(min(open_flows or preferred, key=lambda flow: _rank(flow, spec)))
# Same total order the single-verdict path uses, so `verdicts(...)[0] == verdict(...)`.
# Sort strongest first, then by site for stable results.
chosen.sort(key=lambda flow: (bool(flow["sanitized"]) or bool(flow["bounded"]), _rank(flow, spec)))
return [_verdict_for(flow, spec) for flow in chosen]
@@ -309,4 +268,4 @@ def _location(flow: Mapping[str, object]) -> str:
return f"{where}:{line}:{flow.get('sinkMethod') or '?'}"
__all__ = ["request_params", "verdict"]
__all__ = ["request_params", "verdicts"]
-21
View File
@@ -13,7 +13,6 @@ from __future__ import annotations
import tomllib
from dataclasses import dataclass
from pathlib import Path
from typing import Literal
FAMILY_IDS = (
"injection",
@@ -28,7 +27,6 @@ FAMILY_IDS = (
"unknown",
)
VERIFIER_KINDS = ("canary", "static", "none")
Relation = Literal["same", "parent_child", "lateral", "fabricated"]
DEFAULT_FAMILIES_PATH = Path(__file__).resolve().parents[1] / "ruleset" / "families.toml"
_FAMILY_FIELDS = frozenset({"id", "description", "cwes", "mechanisms", "verifier", "parent", "limits"})
@@ -72,24 +70,6 @@ class FamilyTaxonomy:
def family_of_mechanism(self, mechanism: str) -> Family | None:
return next((family for family in self.families if mechanism in family.mechanisms), None)
@property
def hierarchical(self) -> bool:
"""True when some family declares a parent. While this is False, partial credit can never be earned."""
return any(family.parent for family in self.families)
def related(self, left: str, right: str) -> Relation:
"""How two family answers relate. Abstention is not partial credit: ``unknown`` against a real
family is ``lateral``, never ``parent_child``."""
known = {family.id: family for family in self.families}
if left not in known or right not in known:
return "fabricated"
if left == right:
return "same"
if known[left].parent == right or known[right].parent == left:
return "parent_child"
return "lateral"
def _load_families_uncached(path: Path | None = None, *, require_all: bool = True) -> FamilyTaxonomy:
"""Load the taxonomy. ``require_all`` demands the full closed set in order (the shipped file);
a scoped fixture may declare a subset, but never a family outside ``FAMILY_IDS``."""
@@ -189,7 +169,6 @@ __all__ = [
"FamiliesError",
"Family",
"FamilyTaxonomy",
"Relation",
"load_families",
]
-6
View File
@@ -13,7 +13,6 @@ Store layout under the 30-day ``runs/`` expiry rule (``memory.RUNS_RULE_ID``)::
runs/<run>/<task>/output.tar the runner's PUT
runs/<run>/<task>/<artifact> each declared output, re-put by the reconciler
runs/<run>/<task>/done.json status, bytes, file count, finished
runs/<run>/inputs/<name> an input no task produces (``put_input``)
Nothing here logs or prints a URL.
"""
@@ -164,11 +163,6 @@ class Delivery:
put.append(artifact)
return put
def put_input(self, name: str, path: Path) -> str:
"""Store a file no task produces as ``runs/<run>/inputs/<name>``; returns its presigned GET URL."""
self.store._put(self.key(INPUTS_DIR, name), Path(path).read_bytes())
return self.store.presign_get(self.key(INPUTS_DIR, name), self.expires)
# --- state ---------------------------------------------------------------------------------------------------
def save_state(self, data: Mapping[str, Any]) -> None:
+5 -168
View File
@@ -1,61 +1,29 @@
"""Per-task network egress on Agent Substrate (ai-service-plane, task 0: gateway + receiver Service).
"""Per-task TLS egress to the store, Git hosts and model endpoints.
Every TCP connection an actor opens (DNS aside) is redirected to Substrate's egress gateway
(``atenet-egress.ate-system``), which denies by default: an actor without an ``EgressPolicy`` gets no tunnel, and
a refused connection is accepted and then closed. :func:`policy_for` grants a task exactly what it needs:
* ``http`` on port 80 to the artifact receiver's cluster name (``ousast-receiver.ax-system.svc.cluster.local``, a
selector-less Service whose endpoint is this host's receiver, ``ops/ax/receiver-service.yaml.tmpl``). The actor
cannot resolve cluster names, so the runner dials ``OUSAST_ARTIFACT_DIAL`` (the Service's ClusterIP, port 80)
with the receiver's name as ``Host``. The gateway decides the policy on that ``Host`` but connects to the
address the actor dialled (measured live: agentgateway's upstream is the CONNECT authority, not the Host), so
the dial address must be one the gateway pod reaches: the ClusterIP, which kube-proxy maps to the host.
* ``tls_passthrough`` on 443 for the Git hosts of the bound Workspaces' ``https`` repos (smart HTTP talks to the
repo's host only) and for the bound Model's ``openultrasast.io/egress-hosts`` (``api.deepseek.com``).
Nothing else: no wildcard, no IP address (the gateway rejects them), no ``https`` rule (that one intercepts TLS and
the actor would have to trust the gateway CA, which ax templates do not mount). :class:`Egress` writes the policy
with ``kubectl ate create egress-policy`` after ``ax apply`` created the actor, reads it back, and before resume
waits ``OUSAST_EGRESS_SETTLE`` s (11) because the gateway caches a missing policy as deny for 10 s. The kubectl-ate
CLI has no delete verb: Substrate's store deletes the policy with its actor (``ON DELETE CASCADE``), so
:meth:`Egress.delete` runs after ``ax delete task`` and confirms the policy is gone.
The gateway denies by default. Egress writes and checks each actor's policy before resume,
then waits for the cached denial to expire. Deleting the actor removes its policy.
"""
from __future__ import annotations
import io
import ipaddress
import json
import os
import shutil
import subprocess
import tarfile
import threading
import time
from collections.abc import Iterable, Mapping
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from typing import Any
from urllib.parse import unquote, urlsplit
from urllib.parse import urlsplit
from .manifests import Model, Task, Workspace
__all__ = [
"INPUTS_PATH",
"RECEIVER_HOST",
"RECEIVER_PORT",
"Egress",
"Receiver",
"git_hosts",
"receiver_cluster_ip",
"policy_for",
"receiver_address",
"rules_of",
]
RECEIVER_HOST = "ousast-receiver.ax-system.svc.cluster.local"
INPUTS_PATH = "/inputs/" # GET <receiver>/inputs/<producer>/<artifact>: a consumer fetches a declared input
RECEIVER_PORT = 18090 # the receiver Service's targetPort; ops/ax/up.sh renders the Service with the same number
_NOT_FOUND = ("not found", "notfound")
@@ -89,12 +57,10 @@ def policy_for(
workspaces: Iterable[Workspace],
model: Model | None,
store_host: str | None,
receiver_host: str | None = None,
) -> dict[str, Any]:
"""The task's EgressPolicy body (plane-on-kubernetes design section 3): one ``tlsPassthrough`` rule on 443 over
the sorted union of the store's hostname (``store_host``: where the runner PUTs its tar and GETs its inputs), the
bound Workspaces' https Git hosts and the bound Model's ``openultrasast.io/egress-hosts``. No ``http`` rule, unless
``receiver_host`` names the old receiver (the path task 2.6 removes). An http Git repo cannot be reached under
bound Workspaces' https Git hosts and the bound Model's ``openultrasast.io/egress-hosts``. An http Git repo cannot be reached under
this policy and is refused; a store given as an address is refused naming ``S3_ENDPOINT`` (the gateway allows
hostnames only)."""
tls, plain = git_hosts(workspaces)
@@ -106,8 +72,6 @@ def policy_for(
raise ValueError(f"Task/{task.metadata.name}: the store endpoint is an address ({store_host}); S3_ENDPOINT must be a hostname")
hosts.add(store_host.lower())
rules: list[dict[str, Any]] = []
if receiver_host and not _is_ip(receiver_host): # the receiver path, kept for group 2 only (task 2.6 deletes it)
rules.append({"http": {"hostnames": [receiver_host.lower()], "ports": {"numbers": [80]}}})
if hosts:
rules.append({"tlsPassthrough": {"hostnames": sorted(hosts), "ports": {"numbers": [443]}}})
return {"rules": rules}
@@ -125,41 +89,6 @@ def rules_of(policy: Mapping[str, Any]) -> list[dict[str, Any]]:
return sorted(out, key=lambda r: json.dumps(r, sort_keys=True))
def receiver_cluster_ip(context: str) -> str | None:
"""The ClusterIP of the ``ousast-receiver`` Service (``kubectl get svc`` in the profile's ``context``); None when
kubectl cannot say."""
if not context:
raise RuntimeError("receiver_cluster_ip needs the profile's kube context")
argv = ["kubectl", "--context", context, "-n", "ax-system", "get", "svc", "ousast-receiver", "-o", "jsonpath={.spec.clusterIP}"]
try:
proc = subprocess.run(argv, capture_output=True, text=True, check=False, timeout=20)
except (OSError, subprocess.TimeoutExpired):
return None
address = proc.stdout.strip()
return address if proc.returncode == 0 and _is_ip(address) else None
def receiver_address(port: int, context: str | None) -> tuple[str, str | None]:
"""(``OUSAST_ARTIFACT_URL``, ``OUSAST_ARTIFACT_DIAL``) for a receiver bound on ``port``; ``context`` is the
profile's kube context the Service's ClusterIP is looked up in.
``OUSAST_ARTIFACT_HOST`` set: the old direct URL ``http://<host>:<port>/`` and no dial (tests, a host the actor
reaches without the gateway). Otherwise the receiver Service's name, dialled at ``OUSAST_ARTIFACT_DIAL`` or the
Service's ClusterIP on port 80: the gateway checks the policy against ``Host`` but connects to the address the
actor dialled (its CONNECT authority), so that address must reach the receiver from the gateway pod.
"""
host = os.environ.get("OUSAST_ARTIFACT_HOST")
if host:
return f"http://{host}:{port}/", None
dial = os.environ.get("OUSAST_ARTIFACT_DIAL")
if not dial:
if not context:
raise RuntimeError("receiver_address needs the profile's kube context to look the receiver Service up")
address = receiver_cluster_ip(context)
dial = f"{address}:80" if address else None
return f"http://{RECEIVER_HOST}/", dial
class Egress:
"""``kubectl ate`` for one actor's egress policy; the executable is injectable so tests use a fake."""
@@ -230,95 +159,3 @@ class Egress:
time.sleep(float(os.environ.get("OUSAST_EGRESS_SETTLE") or 11))
class Receiver(ThreadingHTTPServer):
"""Accepts one tar per running task and extracts it under ``<run dir>/<task>/`` (POST); serves a running task
the producer artifacts it declared as inputs (``GET /inputs/<producer>/<artifact>``, ``inputs[task]``)."""
daemon_threads = True
def __init__(self, run_name: str, base: Path, port: int, context: str | None = None) -> None:
super().__init__(("0.0.0.0", port), _Handler)
self.run_name, self.base, self.running, self.delivered = run_name, base, set[str](), set[str]()
self.inputs: dict[str, frozenset[str]] = {}
self.lock = threading.Lock()
self.url, self.dial = receiver_address(self.server_address[1], context)
def _task(self, headers: Any) -> str | None:
"""The ``X-Ousast-Task`` of a request naming this run and a task that is running now, else None."""
task = str(headers.get("X-Ousast-Task", ""))
with self.lock:
return task if headers.get("X-Ousast-Run") == self.run_name and task in self.running else None
def input_file(self, headers: Any, ref: str) -> tuple[int, str | Path]:
"""(200, the file) when ``ref`` is one of the requesting task's declared inputs, else (403/404, why).
``ref`` must equal a declared ``<producer>/<artifact>`` exactly and resolve to a regular file under the run
directory; nothing is normalised first, so ``..`` or an absolute path never names a declared input."""
task = self._task(headers)
if task is None:
return 403, "unknown run or task"
with self.lock:
declared = ref in self.inputs.get(task, frozenset())
if not declared:
return 403, f"{ref!r} is not an input of {task}"
root = self.base.resolve()
path = (root / ref).resolve()
if not path.is_relative_to(root) or not path.is_file():
return 404, f"{ref} was not delivered"
return 200, path
def accept(self, headers: Any, body: bytes) -> tuple[int, str]:
task = self._task(headers)
if task is None:
return 403, "unknown run or task"
if not str(headers.get("Content-Type", "")).startswith("application/x-tar"):
return 415, "expected application/x-tar"
dest = (self.base / task).resolve()
try:
with tarfile.open(fileobj=io.BytesIO(body), mode="r:") as tar:
members = tar.getmembers()
for m in members:
if m.issym() or m.islnk() or not (dest / m.name).resolve().is_relative_to(dest):
return 400, f"rejected member {m.name}"
dest.mkdir(parents=True, exist_ok=True)
tar.extractall(dest, members) # every member checked above
except tarfile.TarError as exc:
return 400, f"bad tar: {exc}"
with self.lock:
self.delivered.add(task)
return 200, "ok"
class _Handler(BaseHTTPRequestHandler):
def do_GET(self) -> None: # noqa: N802 - http.server's naming
server: Receiver = self.server # type: ignore[assignment]
path = urlsplit(self.path).path
if not path.startswith(INPUTS_PATH):
self._text(404, "not found")
return
code, found = server.input_file(self.headers, unquote(path[len(INPUTS_PATH) :]))
if not isinstance(found, Path):
self._text(code, found)
return
with found.open("rb") as source: # streamed: a producer artifact can be far larger than any env value
self.send_response(200)
self.send_header("Content-Type", "application/octet-stream")
self.send_header("Content-Length", str(os.fstat(source.fileno()).st_size))
self.end_headers()
shutil.copyfileobj(source, self.wfile, 1 << 16)
def do_POST(self) -> None: # noqa: N802 - http.server's naming
server: Receiver = self.server # type: ignore[assignment]
body = self.rfile.read(int(self.headers.get("Content-Length") or 0))
self._text(*server.accept(self.headers, body))
def _text(self, code: int, text: str) -> None:
data = text.encode()
self.send_response(code)
self.send_header("Content-Type", "text/plain")
self.send_header("Content-Length", str(len(data)))
self.end_headers()
self.wfile.write(data)
def log_message(self, format: str, *args: object) -> None:
return
+12 -34
View File
@@ -10,8 +10,7 @@ credential from this process's environment, never from a file (Req 4.6), and the
``delivery.py`` (plane-on-kubernetes design section 3). Completion is the runner's output tar appearing in the
memory store (ax has no Completed phase), which the reconciler polls, downloads, extracts and re-publishes per
declared output; ``Failed``, the task vanishing, a refused start or ``OUSAST_TASK_TIMEOUT`` s (7200) without
delivery fail it; ``ax delete task`` follows. ``OUSAST_DELIVERY=receiver`` keeps the host receiver of the kind
profile for group 2 only (task 2.6 removes it).
delivery fail it; ``ax delete task`` follows.
ax rejects unknown fields, so rendered documents carry only ax's fields; ``openultrasast.io/model`` reaches the
task as ``OUSAST_MODEL`` plus ``OUSAST_MODEL_PARAMS`` (Req 7.1), each Workspace's pin annotation as
@@ -40,13 +39,12 @@ from dataclasses import dataclass, field
from datetime import UTC, datetime
from pathlib import Path
from typing import Any
from urllib.parse import urlsplit
import yaml
from .delivery import Delivery, DeliveryError, declared
from .doctor import doctor
from .egress import RECEIVER_PORT, Egress, Receiver, policy_for
from .egress import Egress, policy_for
from .manifests import AX_API_VERSION, Manifests, Model, Run, RunTask, Task, Workspace, load_manifests
from .memory import open_store
from .profile import PlaneProfile, load_profile
@@ -142,16 +140,14 @@ def _model_doc(model: Model) -> dict[str, Any]:
return _doc("Model", model.metadata.name, spec, model.metadata.atespace)
def render_task(run: Run, entry: RunTask, manifests: Manifests, artifact_url: str = "", dial: str | None = None) -> list[dict[str, Any]]:
def render_task(run: Run, entry: RunTask, manifests: Manifests) -> list[dict[str, Any]]:
"""The documents ``ax apply`` receives for one Run task: its Model, its Workspaces, the Task last. Task-produced
inputs are never rendered (Substrate caps one env value at 32768 characters): the runner fetches them
(``OUSAST_INPUTS``) after the start by presigned URL, or from the receiver when ``artifact_url`` is given."""
(``OUSAST_INPUTS``) after the start by presigned URL."""
task: Task = manifests.tasks[entry.task]
ax_name = _ax_name(run.metadata.name, entry.name, limit=TASK_NAME_LIMIT)
env = {e.name: e.value for e in task.env}
env.update(OUSAST_RUN=run.metadata.name, OUSAST_TASK=entry.name, OUSAST_OUTPUT_DIR=f"{OUTPUT_ROOT}/{entry.name}")
if artifact_url: # receiver mode only (task 2.6 removes it): the store path carries no URL in the env
env.update(OUSAST_ARTIFACT_URL=artifact_url, **({"OUSAST_ARTIFACT_DIAL": dial} if dial else {}))
budget = _fields(entry.budget) if entry.budget else {}
env.update({f"OUSAST_BUDGET_{k.upper()}": str(v) for k, v in budget.items()})
inputs = {re.sub(r"[^A-Z0-9]+", "_", name.upper()): ref for name, ref in entry.inputs.items()}
@@ -341,25 +337,22 @@ def _execute(
base: Path,
delivery: Delivery,
router: Router,
receiver: Receiver | None = None,
) -> tuple[str, str]:
ax_name, task = _ax_name(run.metadata.name, entry.name, limit=TASK_NAME_LIMIT), manifests.tasks[entry.task]
atespace, manifest = task.metadata.atespace or "default", base / entry.name / "task.yaml"
manifest.parent.mkdir(parents=True, exist_ok=True)
docs = render_task(run, entry, manifests, receiver.url if receiver else "", receiver.dial if receiver else None)
docs = render_task(run, entry, manifests)
manifest.write_text(yaml.safe_dump_all(docs, sort_keys=False), encoding="utf-8")
model = manifests.models.get(task.metadata.annotations.get(MODEL_ANNOTATION) or "")
workspaces = [manifests.workspaces[b.name] for b in task.workspaces]
body = None if receiver else delivery.body(entry.name, entry.inputs) # presigned URLs ride in the start body
store_host, receiver_host = (None, urlsplit(receiver.url).hostname) if receiver else (delivery.host, None)
if receiver is None:
delivery.reset(entry.name) # a previous attempt's tar must not complete this one
body = delivery.body(entry.name, entry.inputs) # presigned URLs ride in the start body
delivery.reset(entry.name) # a previous attempt's tar must not complete this one
def delivered() -> bool:
return entry.name in receiver.delivered if receiver is not None else delivery.delivered(entry.name)
return delivery.delivered(entry.name)
try: # Req 4.6: the bound Model's credential is read here and only ever sent in the start request
policy = policy_for(task, workspaces, model, store_host, receiver_host)
policy = policy_for(task, workspaces, model, delivery.host)
start = router.starter(atespace, ax_name, run.metadata.name, credentials(model), body)
ax.apply(manifest) # creates the Substrate actor; its egress is denied until the policy exists
egress.apply(ax_name, atespace, policy)
@@ -369,7 +362,7 @@ def _execute(
return "failed", str(exc)
try:
failure = _await(ax, ax_name, delivered, start)
if failure is None and receiver is None:
if failure is None:
delivery.collect(entry.name, declared(run)[entry.name]) # download, extract, re-publish what is declared
except DeliveryError as exc:
failure = str(exc)
@@ -408,11 +401,6 @@ def run(
_read_json(base / "state.json") or {"run": run_spec.metadata.name, "started": _now(), "tasks": {}},
mirror=delivery.save_state,
)
receiver: Receiver | None = None
if os.environ.get("OUSAST_DELIVERY") == "receiver": # the kind profile's host receiver, group 2 only (2.6 removes it)
port = os.environ.get("OUSAST_ARTIFACT_PORT") or (0 if os.environ.get("OUSAST_ARTIFACT_HOST") else RECEIVER_PORT)
receiver = Receiver(run_spec.metadata.name, base, int(port), profile.kube_context) # the Service targets RECEIVER_PORT
threading.Thread(target=receiver.serve_forever, daemon=True).start()
cli, egress, finished = Ax(ax), Egress(kubectl_ate, profile.kube_context), queue.Queue[tuple[str, str, str]]()
pending = [t for t in run_spec.tasks if state.status(t.name) != "done"]
for entry in run_spec.tasks:
@@ -427,7 +415,7 @@ def run(
cli.delete(_ax_name(run_spec.metadata.name, entry.name, limit=TASK_NAME_LIMIT)) # an interrupted attempt; ax must not keep it
state.set(entry.name, status="running", started=_now(), finished=None)
try:
outcome = _execute(cli, egress, run_spec, entry, manifests, base, delivery, router, receiver)
outcome = _execute(cli, egress, run_spec, entry, manifests, base, delivery, router)
except Exception as exc: # noqa: BLE001 - a worker that dies silently would hang the run on the queue
outcome = ("failed", f"{type(exc).__name__}: {exc}")
finished.put((entry.name, *outcome))
@@ -444,27 +432,17 @@ def run(
pending.remove(entry)
running[entry.name] = entry
blocked.add(entry.serialize or "")
if receiver is not None:
with receiver.lock: # the receiver serves a running task its declared inputs, nothing else
receiver.running.add(entry.name)
receiver.inputs[entry.name] = frozenset(entry.inputs.values())
threading.Thread(target=worker, args=(entry,), daemon=True).start()
if not running:
break # nothing can start: a producer stopped short
name, status, reason = finished.get()
del running[name]
if receiver is not None:
with receiver.lock:
receiver.running.discard(name)
receiver.inputs.pop(name, None)
summary = _read_json(base / name / "summary.json") or {}
state.set(name, status=status, finished=_now(), usd=summary.get("usd"), model=summary.get("model"), reason=reason or None)
halted = halted or status == "failed"
if status == "unfinished":
pending = [t for t in pending if name not in t.producers]
finally:
if receiver is not None:
receiver.shutdown()
lock.unlink(missing_ok=True)
statuses = [state.status(t.name) for t in run_spec.tasks]
result = "failed" if "failed" in statuses else "done" if all(s == "done" for s in statuses) else "unfinished"
@@ -546,4 +524,4 @@ def status(run_name: str, *, units: bool = False, results_root: Path | None = No
return "\n".join(lines)
__all__ = ["Ax", "Receiver", "attribution", "doctor", "load_run", "mark_done", "outcome_of", "render_task", "run", "run_dir", "status"]
__all__ = ["Ax", "attribution", "doctor", "load_run", "mark_done", "outcome_of", "render_task", "run", "run_dir", "status"]
+20 -54
View File
@@ -25,8 +25,7 @@ The command runs as a child process, ``python -m openultrasast.plane.tasks.<spec
in its own process group, with the first workspace as its working directory and ``AX_METADATA_URL`` pointing at
this server. ax has no artifact channel and never reports that a command finished, so after the child exits the
runner reads ``summary.json`` (a crash without one becomes ``failed`` with the exit code and the stderr tail),
PUTs the whole ``OUSAST_OUTPUT_DIR`` as a tar to ``delivery.put`` (or POSTs it to ``OUSAST_ARTIFACT_URL``, the receiver
path kept until task 2.6 removes it) -- that delivery is the task's
PUTs the whole ``OUSAST_OUTPUT_DIR`` as a tar to ``delivery.put`` -- that delivery is the task's
completion (Requirement 4.4); a failed one is reported ``failed``, never done -- and writes the completion marker
``<OUSAST_STATE_DIR>/<task>.done.json`` (``exit``, ``status``, ``delivered``). Then it keeps serving until SIGTERM,
as ax requires of PID 1. A boot that finds the marker never reruns the command (a resumed actor must not repeat
@@ -48,13 +47,9 @@ Environment the runner reads, all optional except the two ax variables:
OUSAST_TERM_GRACE seconds between SIGTERM and SIGKILL of the command's group, default 10
OUSAST_CASE_CACHE a directory of git checkouts (``benchmarks/independent`` cache layout)
OUSAST_GIT_PINS JSON ``{"<workspace>/<git name>": "<40-hex commit>"}`` from the reconciler
OUSAST_ARTIFACT_URL, OUSAST_RUN where to deliver, and the run name carried in ``X-Ousast-Run``
OUSAST_ARTIFACT_DIAL ``host:port`` to connect to instead of the URL's host, which is sent as
``Host`` (the egress gateway decides on and resolves that name)
OUSAST_DELIVERY_BACKOFF seconds before the second delivery attempt, doubled per retry
OUSAST_INPUTS JSON ``{"<NAME>": "<producer>/<artifact>"}``: after the start and before the
command each is fetched from ``delivery.inputs[NAME]`` (else from
``<OUSAST_ARTIFACT_URL>/inputs/<ref>`` with the receiver's dial and headers)
command each is fetched from ``delivery.inputs[NAME]``
to ``OUSAST_INPUT_<NAME>``, with the same retries as delivery; a failed
fetch delivers a failed summary naming the input, the command never runs
@@ -700,16 +695,6 @@ def build_tar(output_dir: Path) -> bytes:
return buffer.getvalue()
def _dialled(url: str, headers: Mapping[str, str], dial: str | None) -> tuple[str, dict[str, str]]:
"""(URL to connect to, headers): with ``dial`` the connection goes there and the URL's host is sent as ``Host``."""
out = dict(headers)
if not dial:
return url, out
parts = urlsplit(url)
out["Host"] = parts.netloc
return parts._replace(netloc=dial).geturl(), out
def safe_url(url: str) -> str:
"""``scheme://host/path`` of a URL: what a log line or an error may carry (a presigned query is a credential)."""
parts = urlsplit(url)
@@ -723,18 +708,14 @@ def deliver(
*,
attempts: int = DELIVERY_ATTEMPTS,
backoff: float = 1.0,
dial: str | None = None,
method: str = "POST",
) -> int:
"""Send ``payload`` to ``url`` (``POST`` to the receiver, ``PUT`` to a presigned store URL) with ``headers``; retry
with doubling backoff; return the 2xx status. Log lines and errors carry the URL without its query.
"""PUT the payload with retries and doubling backoff; return the 2xx status.
``dial`` (``host:port``): connect there instead and send the URL's host as ``Host``, as the actor must -- it
cannot resolve the receiver's cluster name, and the egress gateway decides on ``Host`` and resolves it itself.
Log lines and errors carry the URL without its query.
"""
last, (target, headers) = "no attempt made", _dialled(url, headers, dial)
last = "no attempt made"
for attempt in range(1, attempts + 1):
request = urllib.request.Request(target, data=payload, method=method, headers=headers)
request = urllib.request.Request(url, data=payload, method="PUT", headers=headers)
try:
with urllib.request.urlopen(request, timeout=60) as response: # noqa: S310 - the reconciler's URL
status = int(response.status)
@@ -752,16 +733,16 @@ def deliver(
def fetch(
url: str, headers: Mapping[str, str], dest: Path, *, attempts: int = DELIVERY_ATTEMPTS, backoff: float = 1.0, dial: str | None = None
url: str, headers: Mapping[str, str], dest: Path, *, attempts: int = DELIVERY_ATTEMPTS, backoff: float = 1.0
) -> int:
"""GET ``url`` into ``dest`` (streamed to a sibling temp file, then renamed); retried like :func:`deliver`.
Returns the byte count; :class:`DeliveryError` names the last cause when every attempt failed."""
last, (target, headers) = "no attempt made", _dialled(url, headers, dial)
last = "no attempt made"
dest.parent.mkdir(parents=True, exist_ok=True)
partial = dest.with_name(dest.name + ".part")
for attempt in range(1, attempts + 1):
request = urllib.request.Request(target, method="GET", headers=headers)
request = urllib.request.Request(url, method="GET", headers=headers)
try:
with urllib.request.urlopen(request, timeout=60) as response, partial.open("wb") as out: # noqa: S310
shutil.copyfileobj(response, out, 1 << 16)
@@ -784,7 +765,7 @@ def fetch(
def fetch_inputs(environ: Mapping[str, str], delivery: DeliveryTargets | None = None) -> list[Path]:
"""Fetch every ``OUSAST_INPUTS`` entry to its ``OUSAST_INPUT_<NAME>`` path: from ``delivery.inputs[NAME]`` (a
presigned GET, no headers) when the start request carries one, else from the receiver.
presigned GET, no headers) carried by the start request.
:class:`RunnerError` names the input when one cannot be fetched: a task never runs with an input missing."""
raw = environ.get("OUSAST_INPUTS")
@@ -796,21 +777,15 @@ def fetch_inputs(environ: Mapping[str, str], delivery: DeliveryTargets | None =
raise RunnerError(f"OUSAST_INPUTS is not JSON: {exc}") from None
if not isinstance(inputs, dict):
raise RunnerError("OUSAST_INPUTS must map input names to <producer>/<artifact>")
url = environ.get("OUSAST_ARTIFACT_URL")
headers = {"X-Ousast-Run": environ.get("OUSAST_RUN", ""), "X-Ousast-Task": environ.get("OUSAST_TASK", "")}
backoff = float(environ.get("OUSAST_DELIVERY_BACKOFF", "1.0"))
fetched: list[Path] = []
for name, ref in sorted(inputs.items()):
dest = environ.get(f"OUSAST_INPUT_{name}")
presigned = delivery.inputs.get(name) if delivery is not None else None
if not dest or not isinstance(ref, str) or not (presigned or url):
raise RunnerError(f"input {name} ({ref}) not fetched: no delivery URL, OUSAST_ARTIFACT_URL or OUSAST_INPUT_{name}")
if not dest or not isinstance(ref, str) or not presigned:
raise RunnerError(f"input {name} ({ref}) not fetched: no delivery URL or OUSAST_INPUT_{name}")
try:
if presigned:
size = fetch(presigned, {}, Path(dest), backoff=backoff)
else:
source = f"{str(url).rstrip('/')}/inputs/{urllib.parse.quote(ref)}"
size = fetch(source, headers, Path(dest), backoff=backoff, dial=environ.get("OUSAST_ARTIFACT_DIAL") or None)
size = fetch(presigned, {}, Path(dest), backoff=backoff)
except DeliveryError as exc:
raise RunnerError(f"input {name} ({ref}) not fetched: {exc}") from None
log.info("input %s (%s): %d bytes to %s", name, ref, size, dest)
@@ -818,23 +793,14 @@ def fetch_inputs(environ: Mapping[str, str], delivery: DeliveryTargets | None =
return fetched
def _deliver_output(task: Task, output_dir: Path, environ: Mapping[str, str], delivery: DeliveryTargets | None = None) -> bool:
"""PUT the output tar to ``delivery.put`` when the start request carried one, else POST it to the receiver;
def _deliver_output(output_dir: Path, environ: Mapping[str, str], delivery: DeliveryTargets | None = None) -> bool:
"""PUT the output tar to ``delivery.put`` when the start request carried one;
False when there is nowhere to deliver; :class:`DeliveryError` when it fails."""
backoff = float(environ.get("OUSAST_DELIVERY_BACKOFF", "1.0"))
if delivery is not None and delivery.put:
deliver(delivery.put, build_tar(output_dir), {"Content-Type": "application/x-tar"}, backoff=backoff, method="PUT")
deliver(delivery.put, build_tar(output_dir), {"Content-Type": "application/x-tar"}, backoff=backoff)
return True
url = environ.get("OUSAST_ARTIFACT_URL")
if not url:
return False
# the Run's task name (``OUSAST_TASK``) is what the receiver knows; the ax Task is named ``<run>-<task>``
headers = {"Content-Type": "application/x-tar", "X-Ousast-Task": environ.get("OUSAST_TASK") or task.metadata.name}
run = environ.get("OUSAST_RUN")
if run:
headers["X-Ousast-Run"] = run
deliver(url, build_tar(output_dir), headers, backoff=backoff, dial=environ.get("OUSAST_ARTIFACT_DIAL") or None)
return True
return False
# --- entrypoint --------------------------------------------------------------------------------------------------
@@ -849,7 +815,7 @@ class _Session:
@property
def marker(self) -> Path:
"""``<state>/<task>.done.json``: the command finished; ``delivered`` says whether the receiver has it."""
"""``<state>/<task>.done.json``: the command finished; ``delivered`` says whether the store has it."""
return self.state_dir / f"{self.task.metadata.name}.done.json"
@property
@@ -893,7 +859,7 @@ def _finish(
"""
marker: dict[str, Any] = {"exit": exit_code, "status": str(summary.get("status")), "delivered": False, "summary": dict(summary)}
try:
marker["delivered"] = _deliver_output(session.task, session.output_dir, env, delivery)
marker["delivered"] = _deliver_output(session.output_dir, env, delivery)
except DeliveryError as exc:
log.error("artifact delivery failed: %s", exc)
marker.update(status="failed", reason=f"artifact delivery failed: {exc}")
@@ -938,7 +904,7 @@ def _run_once(env: MutableMapping[str, str], ready: threading.Event, stop: threa
return 0
try:
_materialise_once(session, env)
fetch_inputs(env, start.delivery) # after the start: the URLs arrive with it (and a receiver serves a running task only)
fetch_inputs(env, start.delivery) # after the start: the URLs arrive with it
except RunnerError as exc:
log.error("runner failed: %s", exc)
failed = _failed(f"runner failed: {exc}", read_summary(session.output_dir))
+1 -17
View File
@@ -199,22 +199,6 @@ def _model(summary: object) -> str | None:
return str(summary["model"]) if isinstance(summary, dict) and summary.get("model") else None
def recorded_case(base: Path, case: str, workspace: Path | None = None) -> list[dict[str, Any]]:
""":func:`case_features` over a recorded run directory's artifacts (the final ``agreed.json`` when present)."""
final = base / f"{case}-final" / "agreed.json"
return case_features(
facts=_json(base / f"{case}-facts" / "facts.json"),
passes={p: _jsonl(base / f"{case}-v{p}" / "units.jsonl") for p in PASSES},
models={p: _model(_json(base / f"{case}-v{p}" / "summary.json")) for p in PASSES},
agreed=_json(final if final.is_file() else base / f"{case}-agree" / "agreed.json"),
alerts=_jsonl(base / f"{case}-alerts" / "alerts.jsonl"),
alerts_summary=_json(base / f"{case}-alerts" / "summary.json"),
workspace=workspace,
facts_summary=_json(base / f"{case}-facts" / "summary.json"),
model_roles=_json(base / f"{case}-roles" / "roles.json"),
)
def _path(env: Mapping[str, str], name: str) -> Path | None:
return Path(env[name]) if env.get(name) else None
@@ -266,7 +250,7 @@ def main(environ: Mapping[str, str] | None = None) -> int:
return 0 if summary["status"] == "done" else 2
__all__ = ["DROPPED_ON_READ", "case_features", "main", "recorded_case"]
__all__ = ["DROPPED_ON_READ", "case_features", "main", ]
if __name__ == "__main__":
-158
View File
@@ -1,158 +0,0 @@
"""Offline-first provenance checks for declared push evaluation populations.
This validates inputs, never detector outcomes or capability admission. Source bytes come
from pinned Git blobs, so a dirty cached worktree cannot silently change the experiment.
"""
from __future__ import annotations
import argparse
import hashlib
import json
import re
import subprocess
from dataclasses import replace
from pathlib import Path, PurePosixPath
from typing import Any
from openultrasast.repos import RepoUnavailable, load_repo_recipe, resolve
class InputError(ValueError):
"""An input identity or label cannot be trusted."""
def _blob(root: Path, commit: str, path: str) -> bytes:
pure = PurePosixPath(path)
if pure.is_absolute() or ".." in pure.parts or not path or "\\" in path:
raise InputError(f"unsafe source path: {path!r}")
# Git object reads neither follow worktree symlinks nor execute project code.
result = subprocess.run(
["git", "-C", str(root), "cat-file", "blob", f"{commit}:{path}"],
capture_output=True,
check=True,
timeout=30,
)
if not result.stdout:
raise InputError(f"empty source bytes: {path}")
return result.stdout
def _snapshot(item: dict[str, Any], directory: Path, cache_root: Path | None, fetch: bool) -> dict[str, Any]:
result: dict[str, Any] = {"id": item["id"], "status": "invalid_input", "bytes_read": 0, "files": []}
try:
recipe = load_repo_recipe(directory / item["recipe"])
commit = item.get("commit", recipe.commit)
if not isinstance(commit, str) or not re.fullmatch("[0-9a-f]{40}", commit):
raise InputError("commit must be a full immutable SHA")
recipe = replace(recipe, commit=commit)
root = resolve(recipe, fetch=fetch, cache_root=cache_root)
result.update(commit=commit, repository=recipe.url, license=recipe.license)
files = item["files"]
paths = [entry["path"] for entry in files]
if recipe.license_file not in paths or len(paths) < 2 or len(paths) != len(set(paths)):
raise InputError("declare a unique license file and at least one source file")
for entry in files:
content = _blob(root, commit, entry["path"])
result["bytes_read"] += len(content)
edit = entry.get("edit")
if edit:
if not isinstance(edit, dict):
raise InputError("authored edit must be an object")
if not edit.get("provenance", "").startswith("authored"):
raise InputError("transformation must identify authored provenance")
before, after = edit["before"].encode(), edit["after"].encode()
if not before or content.count(before) != 1 or before == after:
raise InputError(f"authored edit must match exactly once: {entry['path']}")
if hashlib.sha256(content).hexdigest() != entry.get("original_sha256"):
raise InputError(f"original hash mismatch: {entry['path']}")
content = content.replace(before, after, 1)
digest = hashlib.sha256(content).hexdigest()
if len(content) != entry["size"] or digest != entry["sha256"] or not content:
raise InputError(f"source size/hash mismatch: {entry['path']}")
result["files"].append({"path": entry["path"], "size": len(content), "sha256": digest})
result["status"] = "ready"
except RepoUnavailable:
result.update(
status="missing_prerequisite",
problem=f"{recipe.name}: pinned checkout {commit} unavailable; rerun this validator with --fetch to fetch it",
)
except (ValueError, KeyError, TypeError, OSError, subprocess.SubprocessError) as exc:
result["problem"] = str(exc)
return result
def validate_manifest(path: Path, *, cache_root: Path | None = None, fetch: bool = False) -> dict[str, Any]:
"""Retain every case, marking unavailable inputs and unreviewed labels explicitly.
`ready` means input provenance is verified. It never means a finding was reproduced.
Explicit --fetch is required even if the ordinary repository network env var is set.
"""
manifest_bytes = path.read_bytes()
data = json.loads(manifest_bytes)
if not isinstance(data, dict) or data.get("schema_version") != 1:
raise InputError("unsupported push input schema version")
for field in ("snapshots", "cases"):
if not isinstance(data.get(field), list) or not data[field]:
raise InputError(f"{field} must be a nonempty list")
for item in data[field]:
if not isinstance(item, dict) or not isinstance(item.get("id"), str) or not item["id"]:
raise InputError(f"{field} entry requires an identity")
snapshots = [_snapshot(item, path.parent, cache_root, fetch) for item in data["snapshots"]]
indexed = {item["id"]: item for item in snapshots}
if len(indexed) != len(snapshots):
raise InputError("duplicate snapshot identity")
cases = []
ids: set[str] = set()
for item in data["cases"]:
case = dict(item, status="invalid_input", admission="experimental")
try:
if item["id"] in ids:
raise InputError("duplicate case identity")
ids.add(item["id"])
if item["label"] not in {"vulnerable", "fixed", "benign", "regression", "unsupported", "unreviewed"}:
raise InputError("invalid case label")
if item["role"] not in {"development", "transfer", "regression", "envelope", "reserved_holdout"}:
raise InputError("invalid workload role")
if not item["capability"] or not item["provenance"]:
raise InputError("capability and provenance are required")
if item["label"] == "unreviewed" and not item["prerequisites"]:
raise InputError("unreviewed labels require an explicit prerequisite")
inputs = [indexed[item[key]] for key in ("base", "tip")]
problems = [value.get("problem", "") for value in inputs if value["status"] != "ready"]
if any(value["status"] == "invalid_input" for value in inputs):
case["problem"] = "; ".join(problems)
elif problems or item["prerequisites"]:
case.update(status="missing_prerequisite", problem="; ".join(problems + item["prerequisites"]))
else:
case["status"] = "ready"
except (ValueError, KeyError, TypeError) as exc:
case["problem"] = str(exc)
cases.append(case)
return {
"schema_version": 1,
"manifest_sha256": hashlib.sha256(manifest_bytes).hexdigest(),
"population": len(cases),
"capability_prerequisites": data.get("capability_prerequisites", {}),
"snapshots": snapshots,
"cases": cases,
"note": "Input validation only; every capability remains experimental pending independent evaluation.",
}
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("manifest", type=Path)
parser.add_argument("--fetch", action="store_true")
args = parser.parse_args()
try:
result = validate_manifest(args.manifest, fetch=args.fetch)
except (ValueError, OSError) as exc:
print(json.dumps({"status": "invalid_input", "problem": str(exc)}))
return 2
print(json.dumps(result, indent=2))
return 0 if all(case["status"] == "ready" for case in result["cases"]) else 1
if __name__ == "__main__":
raise SystemExit(main())
+1 -51
View File
@@ -4,7 +4,7 @@ import json
from collections.abc import Collection
from dataclasses import asdict, dataclass, replace
from pathlib import Path
from typing import TYPE_CHECKING, Protocol
from typing import TYPE_CHECKING
from .preprocess import FileTarget
@@ -28,11 +28,6 @@ class RankingScore:
static_boosts: list[str]
class RankerClient(Protocol):
def complete_json(self, *, model: str, messages: list[dict[str, str]], timeout_seconds: int = 60) -> object:
raise NotImplementedError
def rank_targets(targets: list[FileTarget], model_rankings: object | None = None, model_id: str | None = None) -> list[RankingScore]:
model_by_path = parse_ranker_response(model_rankings, model_id) if model_rankings is not None else {}
rankings: list[RankingScore] = []
@@ -43,32 +38,6 @@ def rank_targets(targets: list[FileTarget], model_rankings: object | None = None
return sorted(rankings, key=lambda item: (-item.priority, item.path))
def rank_targets_with_model(
targets: list[FileTarget],
*,
client: RankerClient,
model: str,
chunk_size: int = 150,
timeout_seconds: int = 60,
) -> list[RankingScore]:
if chunk_size < 1:
raise RankingError("chunk_size must be at least 1")
model_rankings: list[dict[str, object]] = []
for start in range(0, len(targets), chunk_size):
chunk = targets[start : start + chunk_size]
response = client.complete_json(
model=model,
messages=[
{"role": "system", "content": _ranker_system_prompt()},
{"role": "user", "content": json.dumps([_target_prompt_payload(target) for target in chunk], sort_keys=True)},
],
timeout_seconds=timeout_seconds,
)
for ranking in parse_ranker_response(response, model).values():
model_rankings.append(asdict(ranking))
return rank_targets(targets, {"rankings": model_rankings}, model_id=model)
def write_rankings(rankings: list[RankingScore], path: Path) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
payload = {"rankings": [asdict(ranking) for ranking in rankings]}
@@ -205,25 +174,6 @@ def _clamp_score(value: int) -> int:
return min(5, max(1, value))
def _ranker_system_prompt() -> str:
return (
"Score each file for security review priority. Return JSON only with a rankings array. "
"Each item must include path, surface, influence, reachability, rationale, and optional static_boosts. "
"Scores must be integers from 1 to 5."
)
def _target_prompt_payload(target: FileTarget) -> dict[str, object]:
return {
"path": target.path,
"language": target.language,
"loc": target.loc,
"tags": target.tags,
"has_fuzz_entry_point": target.has_fuzz_entry_point,
"reachability_hints": target.reachability_hints,
}
def _reachability_from_hints(hints: list[dict[str, object]]) -> int:
score = 0
for hint in hints:
-82
View File
@@ -7,20 +7,14 @@ call is conservatively charged its reservation on resume, never silently free.
from __future__ import annotations
import argparse
import hashlib
import json
import re
import time
from collections import Counter
from collections.abc import Callable
from dataclasses import asdict, dataclass, replace
from importlib import import_module
from pathlib import Path
from typing import Any, Protocol
from ..plane.budget import BudgetExhausted, SpendBudget
from ..plane.memory import open_store
from .board import Board
from .budget import SearchBudget
@@ -421,79 +415,3 @@ class Coordinator:
@staticmethod
def _refs(refs: Any) -> bool:
return isinstance(refs, list) and bool(refs) and all(isinstance(ref, str) and bool(ref) for ref in refs)
class BatchExecutor:
"""Adapter to the existing AX/VM transfer and checkpoint protocol.
Pass AXLane/DockerLane instances and a trusted worker command. No default
worker is invented here: reason/explore entrypoints belong to group 5.
The coordinator owns retries, so batch performs exactly one attempt.
"""
def __init__(self, *, image: str, command: tuple[str, ...], output: Path, lanes: dict[str, Any]) -> None:
batch = import_module("benchmarks.ax.batch")
batch.validate_image(image)
if not command:
raise ValueError("worker command required")
self.image, self.command, self.output, self.lanes = image, command, output, lanes
def __call__(self, task: SearchTask) -> dict[str, Any]:
import copy
batch = import_module("benchmarks.ax.batch")
if task.lane not in self.lanes:
return {"status": "sandbox_failure", "cost_usd": 0}
data = json.dumps(asdict(task), sort_keys=True, allow_nan=False).encode()
digest = hashlib.sha256(data).hexdigest()
identity = task.search_id + "-" + task.id
item = batch.Item(
identity,
{"SPEC_URL": data},
"result.dat",
self.command,
{"SEARCH_STEP": task.step, "SEARCH_ID": task.search_id, "SEARCH_TASK_ID": task.id},
digest,
)
def validate(raw: bytes, output: Path) -> None:
if len(raw) > 262144:
raise ValueError("search result size exceeded")
record = json.loads(raw)
if not isinstance(record, dict) or record.get("status") not in (
"ok",
"sandbox_failure",
"instrument_failure",
"oom",
"execution_failure",
):
raise ValueError("invalid search result")
(output / "result.json").write_text(json.dumps(record))
workload = batch.Workload(self.image, frozenset({"SPEC_URL", "RESULT_URL"}), task.limits["task_wall_seconds"], validate)
lane = copy.copy(self.lanes[task.lane])
if hasattr(lane, "workload"):
lane.workload = workload
out = self.output / task.search_id / task.id
batch.dispatch([item], workload, out, {task.lane: lane}, lanes=(task.lane,), max_attempts=1)
record_path = out / (hashlib.sha256(identity.encode()).hexdigest() + ".json")
if not record_path.exists():
return {"status": "execution_failure", "cost_usd": 0}
record: dict[str, Any] = json.loads(record_path.read_text())
return record
def main() -> None:
"""Offline checkpoint inspection, also useful before resuming with an executor."""
parser = argparse.ArgumentParser(description="Inspect a search board checkpoint (no model calls)")
parser.add_argument("--memory", required=True)
parser.add_argument("--search-id", required=True)
parser.add_argument("--head", required=True)
args = parser.parse_args()
board = Board.resume(open_store(args.memory), args.search_id, args.head, coordinator="inspect")
print(board.snapshot(), end="")
if __name__ == "__main__":
main()
+1 -18
View File
@@ -22,7 +22,7 @@ import zipfile
from collections.abc import Callable
from concurrent.futures import ThreadPoolExecutor
from contextlib import suppress
from dataclasses import asdict, dataclass, field, replace
from dataclasses import dataclass, field, replace
from pathlib import Path
from typing import Any, cast
@@ -709,20 +709,3 @@ def verify(
isolation_mode=mode,
chromium_no_sandbox=bool(getattr(browser, "no_sandbox", False)),
)
def main() -> None:
import argparse
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("affected", type=Path)
parser.add_argument("safe", type=Path)
parser.add_argument("artefact", type=Path)
parser.add_argument("family")
args = parser.parse_args()
result = verify(Side(args.affected), Side(args.safe), args.artefact, args.family)
print(json.dumps(asdict(result), indent=2))
if __name__ == "__main__":
main()
+1 -45
View File
@@ -2,11 +2,9 @@
from __future__ import annotations
import argparse
import copy
import json
import math
import os
import time
from pathlib import Path
from typing import Any, Protocol, cast
@@ -15,12 +13,11 @@ import yaml
from ..model.endpoint import Prices
from ..plane.budget import BudgetExhausted, SpendBudget, cost_of
from ..provider.openrouter import OpenRouterChatClient, parse_json_content
from ..provider.openrouter import parse_json_content
from . import _sandbox
from .budget import SearchBudget
from .coordinator import SearchTask
from .demo import DEMO_SCHEMA, FAMILY_ORACLES, MAX_DEMO_BYTES, load_demo, validate_demo, validate_oracle
from .executor import ObjectStoreExecutor
from .task_storage import exception_reason
@@ -525,44 +522,3 @@ class Worker:
"cost_usd": meter.spent,
"isolation_mode": getattr(self.executor, "isolation_mode", "brain-only"),
}
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--task", required=True, type=Path)
parser.add_argument("--repo", required=True, type=Path)
parser.add_argument("--demo", required=True, type=Path)
parser.add_argument("--model", required=True)
parser.add_argument("--input-per-m", required=True, type=float)
parser.add_argument("--output-per-m", required=True, type=float)
parser.add_argument("--command-put-url")
parser.add_argument("--result-get-url")
args = parser.parse_args()
if bool(args.command_put_url) != bool(args.result_get_url):
parser.error("both executor mailbox URLs are required")
executor = ObjectStoreExecutor(args.command_put_url, args.result_get_url) if args.command_put_url else None
task = SearchTask(**json.loads(args.task.read_text()))
if task.step == "explore" and executor is None:
parser.error("explore requires a separate repository executor")
client = OpenRouterChatClient(
api_key=os.environ["OPENROUTER_API_KEY"],
base_url=os.environ.get("OPENROUTER_BASE_URL", OpenRouterChatClient.base_url),
max_attempts=1,
)
worker = Worker(
client,
repo=args.repo,
demo=args.demo,
model=args.model,
prices=Prices(args.input_per_m, args.input_per_m, args.output_per_m),
executor=executor,
)
try:
print(json.dumps(worker(task)))
finally:
if executor is not None:
executor.submit("stop", {}, timeout_seconds=10)
if __name__ == "__main__":
main()
+4 -78
View File
@@ -15,12 +15,8 @@ from .findings import StaticFinding
from .hunter_tools import (
PathEscapesRepo,
clamp_repo_path,
entry_points,
find_refs,
flows,
grep_repo,
obligations,
read_definition,
read_file,
)
from .provider.openrouter import OpenRouterChatClient, OpenRouterError
@@ -31,7 +27,6 @@ DEFAULT_MAX_STEPS = 4
CLIENT_ENV = "OPENULTRASAST_HUNTER_CLIENT"
_FINDING_ID_PREFIX = "tool-hunter:"
DEFAULT_TOOL_NAMES = ("read_file", "grep_repo", "find_refs")
_HUNTER_TOOL_NAMES = frozenset(DEFAULT_TOOL_NAMES) | {"read_definition", "entry_points", "flows", "obligations"}
_SEVERITIES = frozenset({"critical", "high", "medium", "low", "info"})
_SCRIPTED_FLAGS = frozenset({"scripted", "script"})
_DUMP_FLAGS = frozenset({"dump", "dump-only"})
@@ -91,50 +86,6 @@ HUNTER_TOOLS: list[dict[str, object]] = [
},
},
},
{
"type": "function",
"function": {
"name": "read_definition",
"description": "Resolve one named function to its file, line span and body. Use it instead of guessing what a callee does.",
"parameters": {
"type": "object",
"properties": {"symbol": {"type": "string"}, "path": {"type": "string"}, "max_chars": {"type": "integer"}},
"required": ["symbol"],
},
},
},
{
"type": "function",
"function": {
"name": "entry_points",
"description": "List the handlers this repository exposes, with their access classification. Identifiers and line spans only.",
"parameters": {"type": "object", "properties": {"path": {"type": "string"}}, "required": []},
},
},
{
"type": "function",
"function": {
"name": "flows",
"description": "List adjudicated source-to-sink records inside one function. Identifiers and line numbers only.",
"parameters": {
"type": "object",
"properties": {"path": {"type": "string"}, "function": {"type": "string"}},
"required": ["path", "function"],
},
},
},
{
"type": "function",
"function": {
"name": "obligations",
"description": "List what one function owes and has not discharged: the operation, the missing discharger and why it is owed.",
"parameters": {
"type": "object",
"properties": {"path": {"type": "string"}, "function": {"type": "string"}},
"required": ["path", "function"],
},
},
},
]
_SYSTEM_PROMPT = (
@@ -330,15 +281,13 @@ def run_tool_hunter(
system_prompt: str = _SYSTEM_PROMPT,
user_prompt: str | None = None,
context_files: Sequence[str] = (),
tools: Sequence[str] | None = None,
max_chars: int = _DEFAULT_MAX_CHARS,
tags: Sequence[str] = ("tool-hunter",),
) -> list[StaticFinding]:
"""One bounded hunt. Every keyword defaults to today's behaviour; a per-family detector overrides the
prompt, the context it starts from, the tools it may call and the tags its findings carry."""
"""One bounded hunt with a prompt, starting context and finding tags."""
if max_steps <= 0:
return []
schemas = _tool_schemas(tools)
schemas = HUNTER_TOOLS
messages: list[dict[str, object]] = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": _user_message(root, hotspots, user_prompt, context_files, max_chars)},
@@ -350,7 +299,7 @@ def run_tool_hunter(
if tool_calls:
messages.append(_assistant_tool_message(response.content, tool_calls, response.reasoning))
for call in tool_calls:
if call.name in _HUNTER_TOOL_NAMES:
if call.name in DEFAULT_TOOL_NAMES:
used_tools = True
messages.append(_tool_result_message(root, call, max_chars))
continue
@@ -414,12 +363,6 @@ def _looks_like_json(content: str) -> bool:
return stripped.startswith(("[", "{"))
def _tool_schemas(names: Sequence[str] | None) -> list[dict[str, object]]:
wanted = tuple(names) if names is not None else DEFAULT_TOOL_NAMES
by_name = {str(schema["function"]["name"]): schema for schema in HUNTER_TOOLS} # type: ignore[index]
return [by_name[name] for name in wanted if name in by_name]
def _user_message(root: Path, hotspots: Sequence[Hotspot], user_prompt: str | None, context_files: Sequence[str], max_chars: int) -> str:
# Context files FIRST, the question LAST. DeepSeek bills a cached prefix at 1/30 of a fresh one, and the cache
# is by prefix: with the question first, every candidate in the same file broke the prefix at its first
@@ -502,30 +445,13 @@ def _dispatch_tool(root: Path, name: str, arguments: Mapping[str, object], defau
path = _require_str(arguments, "path")
max_chars = _optional_int(arguments, "max_chars", default_max_chars)
return {"path": path, "content": read_file(root, path, max_chars=max_chars)}
if name == "read_definition":
symbol = _require_str(arguments, "symbol")
scope = arguments.get("path")
found = read_definition(
root,
symbol,
max_chars=_optional_int(arguments, "max_chars", default_max_chars),
path=str(scope) if isinstance(scope, str) and scope else None,
)
return found if found is not None else {"symbol": symbol, "found": False}
if name == "entry_points":
scope = arguments.get("path")
return entry_points(root, path=str(scope) if isinstance(scope, str) and scope else None)
if name == "flows":
return flows(root, path=_require_str(arguments, "path"), function=_require_str(arguments, "function"))
if name == "obligations":
return obligations(root, path=_require_str(arguments, "path"), function=_require_str(arguments, "function"))
if name == "grep_repo":
pattern = _require_str(arguments, "pattern")
max_matches = _optional_int(arguments, "max_matches", _DEFAULT_MAX_MATCHES)
return grep_repo(root, pattern, max_matches=max_matches)
if name == "find_refs":
symbol = _require_str(arguments, "symbol")
return find_refs(root, symbol, None)
return find_refs(root, symbol)
raise ValueError(f"unknown tool: {name}")
-10
View File
@@ -36,16 +36,6 @@ class VerificationResult:
context_sources: list[str]
def validate_evidence_transition(current: EvidenceLevel | str, target: EvidenceLevel | str) -> EvidenceLevel:
current_level = EvidenceLevel(current)
target_level = EvidenceLevel(target)
if _EVIDENCE_ORDER[target_level] < _EVIDENCE_ORDER[current_level]:
raise ValueError(f"cannot move evidence level backward: {current_level} -> {target_level}")
if _EVIDENCE_ORDER[target_level] - _EVIDENCE_ORDER[current_level] > 1:
raise ValueError(f"cannot skip evidence levels: {current_level} -> {target_level}")
return target_level
def is_report_verified(evidence_level: EvidenceLevel | str, status: VerificationStatus | str) -> bool:
level = EvidenceLevel(evidence_level)
verification_status = VerificationStatus(status)