Spaces:
Sleeping
Sleeping
| """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 | |
| 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 = "" | |
| 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) | |