uofa-demo / src /uofa_cli /oos /runner.py
cloudronin's picture
push build context (uofa source + packs + space app)
a28ec65 verified
Raw
History Blame Contribute Delete
4.8 kB
"""Python wrapper that invokes the Java OOS engine via subprocess.
Mirrors the C3 wrapper pattern in `uofa_cli/commands/rules.py:run_structured()`:
locates the Java binary and the unified fat JAR via `paths.py`, builds the
subprocess argv (using the `oos` subcommand of net.uofa.Engine), spawns,
and parses the JSON result file the engine writes to a temp path.
Returns `None` when the resolved `OOSConfig` says OOS is disabled — callers
omit the `oos_results` field from the report entirely (per spec §1.4 and the
load-bearing "omit None fields" rule that preserves byte-identical reports).
"""
from __future__ import annotations
import json
import subprocess
import tempfile
from dataclasses import dataclass, field
from pathlib import Path
from uofa_cli import paths
from uofa_cli.oos.config import OOSConfig
DEFAULT_TIMEOUT_SECONDS = 120
@dataclass(frozen=True)
class OOSResult:
"""Structured result of one OOS engine invocation.
`firings` is the parsed JSON array the Java engine writes — one element
per OOS verdict (rule × candidate-binding pair). Each element matches the
schema in `src/uofa_cli/oos/SCHEMA_NOTES.md` §2 (top-level fields plus a
nested `evidence_gap` with `path_two_metadata`).
`provenance` records which path activated OOS (per OOSConfig.source) so
report consumers can tell at a glance whether OOS was on or off because
of pack config, CLI flag, or pack absence — used by spec §2.4.
"""
config: OOSConfig
returncode: int
firings: list[dict] = field(default_factory=list)
raw_stdout: str = ""
raw_stderr: str = ""
@property
def provenance(self) -> dict:
return {
"source": self.config.source,
"rule_files_loaded": [str(rf) for rf in self.config.rule_files],
}
def run_structured(
package_path: Path,
config: OOSConfig,
*,
context_path: Path | None = None,
root: Path | None = None,
timeout: float = DEFAULT_TIMEOUT_SECONDS,
) -> OOSResult | None:
"""Invoke the Java OOS engine. Returns None if `config.enabled` is False.
Caller is responsible for omitting the `oos_results` / `oos_provenance`
fields from the unified report when this returns None — that's how
byte-identical compatibility with pre-OOS reports is preserved
(spec §1.4, §5.5).
"""
if not config.enabled:
return None
if not package_path.exists():
raise FileNotFoundError(f"Package not found: {package_path}")
java = paths.java_executable()
jar = paths.jar_path(root=root)
if not jar.exists():
raise FileNotFoundError(
f"OOS engine JAR not found: {jar}. "
f"Run: cd src/weakener-engine && mvn package"
)
ctx = context_path or paths.context_file(root=root)
# Engine writes results to a JSON file. Use a temp file scoped to this
# call so concurrent invocations don't trample each other.
with tempfile.NamedTemporaryFile(
suffix=".json", prefix="oos_engine_", delete=False
) as tmp:
out_path = Path(tmp.name)
try:
cmd = [
java, "-jar", str(jar), "oos",
"--package", str(package_path),
"--context", str(ctx),
"--output", str(out_path),
]
for rf in config.rule_files:
cmd.extend(["--rules", str(rf)])
completed = subprocess.run(
cmd,
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
timeout=timeout,
)
firings: list[dict] = []
if out_path.exists() and out_path.stat().st_size > 0:
try:
firings = json.loads(out_path.read_text(encoding="utf-8"))
if not isinstance(firings, list):
raise RuntimeError(
f"OOS engine produced non-array JSON output: "
f"{type(firings).__name__}"
)
except json.JSONDecodeError as exc:
raise RuntimeError(
f"OOS engine produced invalid JSON at {out_path}: {exc}\n"
f"engine stderr:\n{completed.stderr}"
) from exc
if completed.returncode != 0:
# Surface the engine's stderr so the failure is diagnosable.
raise RuntimeError(
f"OOS engine exited non-zero ({completed.returncode}).\n"
f"stderr:\n{completed.stderr}"
)
return OOSResult(
config=config,
returncode=completed.returncode,
firings=firings,
raw_stdout=completed.stdout,
raw_stderr=completed.stderr,
)
finally:
out_path.unlink(missing_ok=True)