rlgate 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. rlgate/__init__.py +7 -0
  2. rlgate/baseline/__init__.py +0 -0
  3. rlgate/baseline/invariant.py +30 -0
  4. rlgate/cli.py +75 -0
  5. rlgate/core/__init__.py +0 -0
  6. rlgate/core/context.py +47 -0
  7. rlgate/core/finding.py +40 -0
  8. rlgate/core/probe.py +44 -0
  9. rlgate/core/registry.py +46 -0
  10. rlgate/core/report.py +78 -0
  11. rlgate/core/task.py +102 -0
  12. rlgate/ingest/__init__.py +0 -0
  13. rlgate/ingest/presets/__init__.py +0 -0
  14. rlgate/ingest/schemas/__init__.py +0 -0
  15. rlgate/ingest/spec.py +75 -0
  16. rlgate/pipeline.py +64 -0
  17. rlgate/probes/__init__.py +0 -0
  18. rlgate/probes/forgery/__init__.py +0 -0
  19. rlgate/probes/forgery/world_readable_tests.py +44 -0
  20. rlgate/probes/grader_strength/__init__.py +0 -0
  21. rlgate/probes/grader_strength/revert_probe.py +44 -0
  22. rlgate/probes/hygiene/__init__.py +0 -0
  23. rlgate/probes/leakage/__init__.py +0 -0
  24. rlgate/probes/leakage/git_unreachable_objects.py +46 -0
  25. rlgate/probes/patterns/__init__.py +0 -0
  26. rlgate/probes/reward_shape/__init__.py +0 -0
  27. rlgate/probes/test_quality/__init__.py +0 -0
  28. rlgate/sandbox/__init__.py +0 -0
  29. rlgate/sandbox/_patch.py +30 -0
  30. rlgate/sandbox/base.py +63 -0
  31. rlgate/sandbox/docker.py +120 -0
  32. rlgate/sandbox/local.py +62 -0
  33. rlgate/sandbox/select.py +22 -0
  34. rlgate/variants/__init__.py +0 -0
  35. rlgate/variants/engine.py +76 -0
  36. rlgate/variants/revert.py +70 -0
  37. rlgate-0.0.1.dist-info/METADATA +81 -0
  38. rlgate-0.0.1.dist-info/RECORD +41 -0
  39. rlgate-0.0.1.dist-info/WHEEL +4 -0
  40. rlgate-0.0.1.dist-info/entry_points.txt +2 -0
  41. rlgate-0.0.1.dist-info/licenses/LICENSE +21 -0
rlgate/__init__.py ADDED
@@ -0,0 +1,7 @@
1
+ """rlgate — a deterministic auditor for reward-hacking vulnerabilities in RL/eval environments.
2
+
3
+ Public surface is intentionally small; everything extensible lives behind the registries
4
+ in ``rlgate.core.registry`` (probes, loaders, sandboxes, scoring schemas).
5
+ """
6
+
7
+ __version__ = "0.0.1"
File without changes
@@ -0,0 +1,30 @@
1
+ """Phase B: oracle==1.0 / nop==0.0 at k=5, plus flaky detection. Fail-fast — if the baseline
2
+ is not sane, probes that assume a sane baseline are skipped (they'd be noise)."""
3
+
4
+ from __future__ import annotations
5
+
6
+ from rlgate.core.context import Baseline
7
+ from rlgate.core.task import Task
8
+ from rlgate.sandbox.base import Sandbox
9
+
10
+
11
+ def establish(task: Task, sandbox: Sandbox, k: int = 5) -> Baseline:
12
+ oracle_runs = [_run(task, sandbox, gold=True) for _ in range(k)]
13
+ nop_runs = [_run(task, sandbox, gold=False) for _ in range(k)]
14
+
15
+ flaky = set()
16
+ if len(set(oracle_runs)) > 1:
17
+ flaky.add("oracle")
18
+ if len(set(nop_runs)) > 1:
19
+ flaky.add("nop")
20
+
21
+ oracle = min(oracle_runs) # worst-case; a single miss is a flaky/unfair signal
22
+ nop = max(nop_runs)
23
+ return Baseline(oracle=oracle, nop=nop, k=k, flaky_checks=frozenset(flaky))
24
+
25
+
26
+ def _run(task: Task, sandbox: Sandbox, gold: bool) -> float:
27
+ patch = task.gold_patch.text if (gold and task.gold_patch) else None
28
+ with sandbox.overlay(patch) as ovl:
29
+ res = ovl.exec(task.grader.run)
30
+ return task.grader.pass_reward if res.exit_code == 0 else 0.0
rlgate/cli.py ADDED
@@ -0,0 +1,75 @@
1
+ """`rlgate` CLI. Stdlib argparse only — no runtime CLI deps."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import sys
7
+
8
+ from rlgate import __version__
9
+ from rlgate.core.registry import discover
10
+ from rlgate.core.report import to_json, to_markdown
11
+
12
+
13
+ def main(argv: list[str] | None = None) -> int:
14
+ parser = argparse.ArgumentParser(prog="rlgate", description=__doc__)
15
+ parser.add_argument("--version", action="version", version=f"rlgate {__version__}")
16
+ sub = parser.add_subparsers(dest="cmd", required=True)
17
+
18
+ p_list = sub.add_parser("list-probes", help="show all discovered probes")
19
+ p_list.add_argument("--family", help="filter by family")
20
+
21
+ p_audit = sub.add_parser("audit", help="audit an environment (path to descriptor or dir)")
22
+ p_audit.add_argument("target")
23
+ p_audit.add_argument("-k", type=int, default=5, help="baseline repetitions (default 5)")
24
+ p_audit.add_argument("--backend", choices=["auto", "local", "docker"], default="auto",
25
+ help="sandbox backend (default auto: docker if the task has an image)")
26
+ p_audit.add_argument("--probe", action="append", help="restrict to these probe ids")
27
+ p_audit.add_argument("--format", choices=["md", "json"], default="md")
28
+ p_audit.add_argument("--gate", action="store_true",
29
+ help="exit non-zero on any FIX/DROP (CI mode)")
30
+
31
+ args = parser.parse_args(argv)
32
+
33
+ if args.cmd == "list-probes":
34
+ return _list_probes(args.family)
35
+ if args.cmd == "audit":
36
+ return _audit(args)
37
+ return 2
38
+
39
+
40
+ def _list_probes(family: str | None) -> int:
41
+ probes = discover()
42
+ for pid in sorted(probes):
43
+ meta = probes[pid].meta
44
+ if family and meta.family != family:
45
+ continue
46
+ reqs = ",".join(c.value for c in meta.requires) or "-"
47
+ print(f"{pid:42} {meta.kind.value:7} {meta.severity.name:8} needs[{reqs}] {meta.doc}")
48
+ return 0
49
+
50
+
51
+ def _audit(args) -> int:
52
+ from rlgate.core.report import eqs, triage
53
+ from rlgate.ingest import spec
54
+ from rlgate.pipeline import audit
55
+
56
+ task = spec.load(args.target)
57
+ result = audit(task, k=args.k, only=set(args.probe) if args.probe else None,
58
+ backend=args.backend)
59
+
60
+ if args.format == "json":
61
+ print(to_json(result.task_id, result.findings, result.baseline_sane))
62
+ else:
63
+ print(to_markdown(result.task_id, result.findings, result.baseline_sane))
64
+
65
+ if args.gate:
66
+ t = triage(result.findings, result.baseline_sane)
67
+ if t.value != "KEEP":
68
+ print(f"\nGATE: {t.value} (eqs={eqs(result.findings, result.baseline_sane):.2f})",
69
+ file=sys.stderr)
70
+ return 1
71
+ return 0
72
+
73
+
74
+ if __name__ == "__main__":
75
+ raise SystemExit(main())
File without changes
rlgate/core/context.py ADDED
@@ -0,0 +1,47 @@
1
+ """Shared, expensive state built once and handed to every probe."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from typing import TYPE_CHECKING, Any
7
+
8
+ if TYPE_CHECKING:
9
+ from rlgate.sandbox.base import Sandbox
10
+ from rlgate.variants.engine import VariantEngine
11
+
12
+
13
+ @dataclass
14
+ class Baseline:
15
+ """Phase B result: oracle/nop at k runs, flaky checks, and the per-test→region map."""
16
+
17
+ oracle: float # gold reward (want 1.0)
18
+ nop: float # empty-submission reward (want 0.0)
19
+ k: int
20
+ flaky_checks: frozenset[str] = frozenset() # checks that disagreed across k runs
21
+ per_test: dict[str, bool] = field(default_factory=dict) # test id → passed under gold
22
+ ok: bool = False # oracle==1.0 and nop==0.0 and no flaky checks
23
+
24
+ @property
25
+ def sane(self) -> bool:
26
+ return self.oracle == 1.0 and self.nop == 0.0 and not self.flaky_checks
27
+
28
+
29
+ class ResultCache:
30
+ """Content-addressed cache so identical variant runs are free across a session."""
31
+
32
+ def __init__(self) -> None:
33
+ self._store: dict[str, Any] = {}
34
+
35
+ def get_or_run(self, key: str, fn):
36
+ if key not in self._store:
37
+ self._store[key] = fn()
38
+ return self._store[key]
39
+
40
+
41
+ @dataclass
42
+ class ProbeContext:
43
+ sandbox: Sandbox
44
+ baseline: Baseline
45
+ variants: VariantEngine
46
+ cache: ResultCache = field(default_factory=ResultCache)
47
+ config: dict = field(default_factory=dict)
rlgate/core/finding.py ADDED
@@ -0,0 +1,40 @@
1
+ """What every probe returns. A finding is a reproduced exploit or a concrete code location —
2
+ never a threshold on a score."""
3
+
4
+ from __future__ import annotations
5
+
6
+ from dataclasses import dataclass, field
7
+ from enum import Enum, IntEnum
8
+ from pathlib import Path
9
+
10
+
11
+ class Severity(IntEnum):
12
+ LOW = 1
13
+ MEDIUM = 2
14
+ HIGH = 3
15
+ CRITICAL = 4
16
+
17
+
18
+ class VerdictTier(Enum):
19
+ CONFIRMED = "confirmed" # execution-proven: a reproduced exploit or a concrete code fact
20
+ REVIEW = "review" # detected deterministically; fairness needs NL/human adjudication
21
+ DEFERRED = "deferred" # out of the deterministic core's scope; needs the LLM layer
22
+
23
+
24
+ @dataclass
25
+ class Evidence:
26
+ kind: str # "reproduced_exploit"|"code_line"|"variant"|"perms"|"diff"
27
+ detail: str
28
+ replay: str | None = None # exact command to reproduce
29
+ artifacts: list[Path] = field(default_factory=list)
30
+
31
+
32
+ @dataclass
33
+ class Finding:
34
+ probe_id: str
35
+ title: str
36
+ severity: Severity
37
+ verdict: VerdictTier
38
+ evidence: Evidence
39
+ location: str | None = None # file:line the finding anchors to
40
+ fix_hint: str | None = None # the actionable remediation (FIX, not just DROP)
rlgate/core/probe.py ADDED
@@ -0,0 +1,44 @@
1
+ """The one interface every check implements. A probe is a self-contained module in
2
+ ``rlgate/probes/<family>/`` decorated with ``@register`` — discovery picks it up."""
3
+
4
+ from __future__ import annotations
5
+
6
+ from abc import ABC, abstractmethod
7
+ from dataclasses import dataclass, field
8
+ from enum import Enum
9
+
10
+ from rlgate.core.context import ProbeContext
11
+ from rlgate.core.finding import Finding, Severity
12
+ from rlgate.core.task import Capability, Task
13
+
14
+
15
+ class ProbeKind(Enum):
16
+ GENERIC = "generic" # free with the engine (runs on any task with the capabilities)
17
+ PATTERN = "pattern" # per-grader pattern library (§3.6–§3.8); provable once written
18
+
19
+
20
+ @dataclass(frozen=True)
21
+ class ProbeMeta:
22
+ id: str # stable, e.g. "forgery.world_readable_tests"
23
+ family: str
24
+ kind: ProbeKind
25
+ severity: Severity
26
+ needs_live: bool = False # requires a live run (e.g. race probes)
27
+ languages: frozenset[str] | None = None # None = language-agnostic
28
+ requires: frozenset[Capability] = field(default_factory=frozenset) # task must provide ALL
29
+ doc: str = "" # one-line summary → probe-catalog.md
30
+
31
+
32
+ class Probe(ABC):
33
+ meta: ProbeMeta
34
+
35
+ def applicable(self, task: Task) -> bool:
36
+ """Gate on CAPABILITIES, never on task.source / benchmark identity."""
37
+ caps_ok = self.meta.requires <= task.capabilities
38
+ lang_ok = self.meta.languages is None or bool(self.meta.languages & task.languages)
39
+ return caps_ok and lang_ok
40
+
41
+ @abstractmethod
42
+ def run(self, task: Task, ctx: ProbeContext) -> list[Finding]:
43
+ """Deterministic generation; stable verdicts. Return zero or more findings."""
44
+ raise NotImplementedError
@@ -0,0 +1,46 @@
1
+ """Discovery: a contributor drops a ``@register``-decorated probe into
2
+ ``rlgate/probes/<family>/`` and it is picked up automatically. External packages ship probes
3
+ via the ``rlgate.probes`` entry-point group without touching this repo."""
4
+
5
+ from __future__ import annotations
6
+
7
+ import importlib
8
+ import pkgutil
9
+ from importlib import metadata
10
+
11
+ from rlgate.core.probe import Probe
12
+
13
+ _PROBES: dict[str, type[Probe]] = {}
14
+
15
+
16
+ def register(cls: type[Probe]) -> type[Probe]:
17
+ pid = cls.meta.id
18
+ if pid in _PROBES and _PROBES[pid] is not cls:
19
+ raise ValueError(f"duplicate probe id: {pid}")
20
+ _PROBES[pid] = cls
21
+ return cls
22
+
23
+
24
+ def _import_tree(package_name: str) -> None:
25
+ pkg = importlib.import_module(package_name)
26
+ for mod in pkgutil.walk_packages(pkg.__path__, prefix=package_name + "."):
27
+ if not mod.name.rsplit(".", 1)[-1].startswith("_"):
28
+ importlib.import_module(mod.name)
29
+
30
+
31
+ def discover() -> dict[str, type[Probe]]:
32
+ """Import every in-repo probe module, then load third-party entry points. Idempotent."""
33
+ _import_tree("rlgate.probes")
34
+ try:
35
+ eps = metadata.entry_points(group="rlgate.probes")
36
+ except Exception: # noqa: BLE001 - entry-point loading can raise many things; best-effort
37
+ eps = []
38
+ for ep in eps:
39
+ obj = ep.load()
40
+ if isinstance(obj, type) and issubclass(obj, Probe):
41
+ register(obj)
42
+ return dict(_PROBES)
43
+
44
+
45
+ def all_probes() -> dict[str, type[Probe]]:
46
+ return dict(_PROBES)
rlgate/core/report.py ADDED
@@ -0,0 +1,78 @@
1
+ """Turn findings into an EQS + KEEP/FIX/DROP triage + JSON/Markdown."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from dataclasses import asdict
7
+ from enum import Enum
8
+
9
+ from rlgate.core.finding import Finding, Severity, VerdictTier
10
+
11
+
12
+ class Triage(Enum):
13
+ KEEP = "KEEP"
14
+ FIX = "FIX"
15
+ DROP = "DROP"
16
+
17
+
18
+ def triage(findings: list[Finding], baseline_sane: bool) -> Triage:
19
+ if not baseline_sane:
20
+ return Triage.DROP
21
+ if any(f.severity >= Severity.CRITICAL for f in findings):
22
+ return Triage.DROP
23
+ if findings:
24
+ return Triage.FIX
25
+ return Triage.KEEP
26
+
27
+
28
+ def eqs(findings: list[Finding], baseline_sane: bool) -> float:
29
+ """Environment Quality Score in [0,1]: 1.0 clean, down-weighted by severity."""
30
+ if not baseline_sane:
31
+ return 0.0
32
+ penalty = sum(0.1 * int(f.severity) for f in findings)
33
+ return max(0.0, 1.0 - penalty)
34
+
35
+
36
+ def to_json(task_id: str, findings: list[Finding], baseline_sane: bool) -> str:
37
+ payload = {
38
+ "task": task_id,
39
+ "eqs": eqs(findings, baseline_sane),
40
+ "triage": triage(findings, baseline_sane).value,
41
+ "baseline_sane": baseline_sane,
42
+ "findings": [_finding_json(f) for f in findings],
43
+ }
44
+ return json.dumps(payload, indent=2)
45
+
46
+
47
+ def _finding_json(f: Finding) -> dict:
48
+ d = asdict(f)
49
+ d["severity"] = f.severity.name
50
+ d["verdict"] = f.verdict.value
51
+ d["evidence"]["artifacts"] = [str(p) for p in f.evidence.artifacts]
52
+ return d
53
+
54
+
55
+ def to_markdown(task_id: str, findings: list[Finding], baseline_sane: bool) -> str:
56
+ tier = {VerdictTier.CONFIRMED: "[CONFIRMED]", VerdictTier.REVIEW: "[REVIEW]",
57
+ VerdictTier.DEFERRED: "[DEFERRED]"}
58
+ lines = [
59
+ f"# rlgate audit — {task_id}",
60
+ (f"**EQS {eqs(findings, baseline_sane):.2f} · "
61
+ f"{triage(findings, baseline_sane).value}** "
62
+ f"· baseline {'OK' if baseline_sane else 'BROKEN'}"),
63
+ "",
64
+ ]
65
+ if not findings:
66
+ lines.append("_No findings._")
67
+ for f in sorted(findings, key=lambda x: x.severity, reverse=True):
68
+ lines += [
69
+ f"## {tier[f.verdict]} {f.title} ({f.severity.name})",
70
+ f"- probe: `{f.probe_id}`" + (f" · at `{f.location}`" if f.location else ""),
71
+ f"- evidence: {f.evidence.detail}",
72
+ ]
73
+ if f.evidence.replay:
74
+ lines.append(f"- replay: `{f.evidence.replay}`")
75
+ if f.fix_hint:
76
+ lines.append(f"- fix: {f.fix_hint}")
77
+ lines.append("")
78
+ return "\n".join(lines)
rlgate/core/task.py ADDED
@@ -0,0 +1,102 @@
1
+ """The normalized environment contract. Every loader produces a ``Task``; every probe
2
+ consumes one. Probes gate on ``Task.capabilities`` — never on a benchmark name."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import hashlib
7
+ from dataclasses import dataclass, field
8
+ from enum import Enum
9
+ from functools import cached_property
10
+ from pathlib import Path
11
+
12
+ try:
13
+ from unidiff import PatchSet
14
+ except ImportError: # keep import-time soft so `list-probes` works without extras
15
+ PatchSet = None # type: ignore
16
+
17
+
18
+ class Capability(Enum):
19
+ """What a task *provides*. Probes require these; this is the only thing they gate on."""
20
+
21
+ GOLD_PATCH = "gold_patch" # a reference fix with addressable hunks
22
+ GIT_REPO = "git_repo" # workspace is a git checkout
23
+ CONTAINER = "container" # grader runs in a container we control
24
+ SCORING_SPEC = "scoring_spec" # a declared reward formula (any schema plugin)
25
+ PER_TEST = "per_test" # grader emits per-test results (junit/trx/go-json/…)
26
+ INSTRUCTION = "instruction" # an agent-facing prompt exists
27
+ # extend freely — adding a capability is additive.
28
+
29
+
30
+ @dataclass(frozen=True)
31
+ class Patch:
32
+ """A unified diff. Canonicalized + hashed once so hunk decomposition is stable."""
33
+
34
+ text: str
35
+
36
+ @cached_property
37
+ def sha256(self) -> str:
38
+ return hashlib.sha256(self.text.encode()).hexdigest()
39
+
40
+ @cached_property
41
+ def patchset(self) -> PatchSet:
42
+ if PatchSet is None:
43
+ raise RuntimeError("unidiff is required to parse patches (pip install rlgate[dev])")
44
+ return PatchSet(self.text)
45
+
46
+
47
+ @dataclass(frozen=True)
48
+ class ReportSpec:
49
+ format: str # "junit" | "trx" | "vitest_json" | "go_json" | "cpp_exit"
50
+ path: str # where the grader writes it (inside the sandbox)
51
+
52
+
53
+ @dataclass(frozen=True)
54
+ class GraderSpec:
55
+ """How to score a submission. Image optional (local run); report optional (per-test)."""
56
+
57
+ run: str # command that scores the current workspace
58
+ image: str | None = None # Dockerfile path or image ref; None → local sandbox
59
+ report: ReportSpec | None = None
60
+ pass_reward: float = 1.0 # reward when the run signals full success
61
+
62
+ @property
63
+ def capabilities(self) -> set[Capability]:
64
+ caps: set[Capability] = set()
65
+ if self.image:
66
+ caps.add(Capability.CONTAINER)
67
+ if self.report:
68
+ caps.add(Capability.PER_TEST)
69
+ return caps
70
+
71
+
72
+ @dataclass(frozen=True)
73
+ class ScoringSpec:
74
+ schema: str # name of a registered ScoringSchema plugin
75
+ path: str # path to the scoring file(s) this schema reads
76
+
77
+
78
+ @dataclass(frozen=True)
79
+ class Task:
80
+ id: str
81
+ source: str # free-form provenance; NOT a probe switch
82
+ repo_dir: Path # the as-shipped workspace
83
+ grader: GraderSpec
84
+ instruction: str | None = None
85
+ gold_patch: Patch | None = None
86
+ scoring_spec: ScoringSpec | None = None
87
+ languages: frozenset[str] = frozenset()
88
+ meta: dict = field(default_factory=dict) # source-specific; generic probes never read it
89
+
90
+ @cached_property
91
+ def capabilities(self) -> frozenset[Capability]:
92
+ caps: set[Capability] = set()
93
+ if self.gold_patch:
94
+ caps.add(Capability.GOLD_PATCH)
95
+ if self.scoring_spec:
96
+ caps.add(Capability.SCORING_SPEC)
97
+ if self.instruction:
98
+ caps.add(Capability.INSTRUCTION)
99
+ if (self.repo_dir / ".git").exists():
100
+ caps.add(Capability.GIT_REPO)
101
+ caps |= self.grader.capabilities
102
+ return frozenset(caps)
File without changes
File without changes
File without changes
rlgate/ingest/spec.py ADDED
@@ -0,0 +1,75 @@
1
+ """Generic, declarative ingestion: describe ANY environment in `task.rlgate.yaml` and get a
2
+ `Task`. Capabilities are derived from which keys are present — no benchmark-specific code.
3
+
4
+ Example:
5
+ instruction_path: TASK.md
6
+ workspace: ./repo
7
+ gold_patch: ./solution/fix.patch # optional → GOLD_PATCH probes
8
+ grader:
9
+ run: "bash run_tests.sh"
10
+ image: ./Dockerfile # optional → CONTAINER
11
+ report: {format: junit, path: /out/results.xml} # optional → PER_TEST
12
+ scoring_spec: # optional → reward_shape probes
13
+ schema: harbor_scorepy
14
+ path: ./score.py
15
+ languages: [python]
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ from pathlib import Path
21
+
22
+ import yaml
23
+
24
+ from rlgate.core.task import GraderSpec, Patch, ReportSpec, ScoringSpec, Task
25
+
26
+ DESCRIPTOR_NAME = "task.rlgate.yaml"
27
+
28
+
29
+ def load(descriptor_path: str | Path) -> Task:
30
+ path = Path(descriptor_path)
31
+ if path.is_dir():
32
+ path = path / DESCRIPTOR_NAME
33
+ data = yaml.safe_load(path.read_text())
34
+ base = path.parent
35
+
36
+ def rel(p: str) -> Path:
37
+ return (base / p).resolve()
38
+
39
+ g = data["grader"]
40
+ report = None
41
+ if g.get("report"):
42
+ report = ReportSpec(format=g["report"]["format"], path=g["report"]["path"])
43
+ grader = GraderSpec(
44
+ run=g["run"],
45
+ image=str(rel(g["image"])) if g.get("image") else None,
46
+ report=report,
47
+ pass_reward=float(g.get("pass_reward", 1.0)),
48
+ )
49
+
50
+ instruction = None
51
+ if data.get("instruction_path"):
52
+ instruction = rel(data["instruction_path"]).read_text()
53
+ elif data.get("instruction"):
54
+ instruction = data["instruction"]
55
+
56
+ gold = None
57
+ if data.get("gold_patch"):
58
+ gold = Patch(text=rel(data["gold_patch"]).read_text())
59
+
60
+ scoring = None
61
+ if data.get("scoring_spec"):
62
+ s = data["scoring_spec"]
63
+ scoring = ScoringSpec(schema=s["schema"], path=str(rel(s["path"])))
64
+
65
+ return Task(
66
+ id=data.get("id", path.parent.name),
67
+ source=data.get("source", str(path)),
68
+ repo_dir=rel(data["workspace"]),
69
+ grader=grader,
70
+ instruction=instruction,
71
+ gold_patch=gold,
72
+ scoring_spec=scoring,
73
+ languages=frozenset(data.get("languages", [])),
74
+ meta=data.get("meta", {}),
75
+ )
rlgate/pipeline.py ADDED
@@ -0,0 +1,64 @@
1
+ """Orchestration: ingest → baseline (fail-fast) → applicable probes (capability-gated) →
2
+ report. Probes gate on capabilities only; the pipeline never branches on benchmark identity."""
3
+
4
+ from __future__ import annotations
5
+
6
+ from dataclasses import dataclass
7
+
8
+ from rlgate.baseline.invariant import establish
9
+ from rlgate.core.context import ProbeContext
10
+ from rlgate.core.finding import Finding
11
+ from rlgate.core.registry import discover
12
+ from rlgate.core.task import Task
13
+ from rlgate.sandbox.base import Sandbox
14
+ from rlgate.sandbox.select import build_sandbox
15
+ from rlgate.variants.engine import VariantEngine
16
+
17
+
18
+ @dataclass
19
+ class AuditResult:
20
+ task_id: str
21
+ baseline_sane: bool
22
+ findings: list[Finding]
23
+
24
+
25
+ def audit(task: Task, sandbox: Sandbox | None = None, k: int = 5,
26
+ only: set[str] | None = None, backend: str = "auto") -> AuditResult:
27
+ sandbox = sandbox or build_sandbox(task, prefer=backend)
28
+ sandbox.build()
29
+
30
+ baseline = establish(task, sandbox, k=k)
31
+ ctx = ProbeContext(
32
+ sandbox=sandbox,
33
+ baseline=baseline,
34
+ variants=VariantEngine(task, sandbox, baseline),
35
+ )
36
+
37
+ findings: list[Finding] = []
38
+ for pid, cls in discover().items():
39
+ if only and pid not in only:
40
+ continue
41
+ probe = cls()
42
+ if not probe.applicable(task):
43
+ continue
44
+ # Fail-fast: when the baseline is broken, skip probes that assume it's sane.
45
+ if not baseline.sane and probe.meta.family in {"grader_strength", "reward_shape"}:
46
+ continue
47
+ findings.extend(probe.run(task, ctx))
48
+
49
+ if not baseline.sane:
50
+ findings.insert(0, _baseline_finding(baseline))
51
+ return AuditResult(task.id, baseline.sane, findings)
52
+
53
+
54
+ def _baseline_finding(baseline) -> Finding:
55
+ from rlgate.core.finding import Evidence, Severity, VerdictTier
56
+ detail = f"oracle={baseline.oracle} nop={baseline.nop} flaky={sorted(baseline.flaky_checks)}"
57
+ return Finding(
58
+ probe_id="baseline.invariant",
59
+ title="baseline not sane (oracle!=1.0 / nop!=0.0 / flaky)",
60
+ severity=Severity.CRITICAL,
61
+ verdict=VerdictTier.CONFIRMED,
62
+ evidence=Evidence(kind="reproduced_exploit", detail=detail),
63
+ fix_hint="the environment is broken before hacking is even discussed; fix this first.",
64
+ )
File without changes
File without changes
@@ -0,0 +1,44 @@
1
+ """§3.1 — held-out sources / scorer readable by the candidate UID (not chmod 700)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from rlgate.core.context import ProbeContext
6
+ from rlgate.core.finding import Evidence, Finding, Severity, VerdictTier
7
+ from rlgate.core.probe import Probe, ProbeKind, ProbeMeta
8
+ from rlgate.core.registry import register
9
+ from rlgate.core.task import Task
10
+
11
+
12
+ @register
13
+ class WorldReadableTests(Probe):
14
+ meta = ProbeMeta(
15
+ id="forgery.world_readable_tests",
16
+ family="forgery",
17
+ kind=ProbeKind.GENERIC,
18
+ severity=Severity.HIGH,
19
+ doc="/tests (held-out sources, scorer) is readable by the candidate UID.",
20
+ )
21
+
22
+ # The directory holding graded sources; overridable via task.meta without special-casing.
23
+ def _tests_dir(self, task: Task) -> str:
24
+ return task.meta.get("tests_dir", "tests")
25
+
26
+ def run(self, task: Task, ctx: ProbeContext) -> list[Finding]:
27
+ tdir = self._tests_dir(task)
28
+ with ctx.sandbox.overlay(None) as ovl:
29
+ perms = ovl.read_perms(tdir)
30
+ if perms.world_readable:
31
+ return [Finding(
32
+ probe_id=self.meta.id,
33
+ title=f"{tdir}/ is world-readable; held-out names leak at runtime",
34
+ severity=Severity.HIGH,
35
+ verdict=VerdictTier.CONFIRMED,
36
+ location=tdir,
37
+ evidence=Evidence(
38
+ kind="perms",
39
+ detail=f"mode={perms.mode:#o}",
40
+ replay=f"rlgate audit <task> --probe {self.meta.id}",
41
+ ),
42
+ fix_hint="chmod 700 the graded dir; delete graded sources after build.",
43
+ )]
44
+ return []
File without changes