rlgate 0.0.1__tar.gz

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 (58) hide show
  1. rlgate-0.0.1/.github/workflows/ci.yml +19 -0
  2. rlgate-0.0.1/.github/workflows/release.yml +18 -0
  3. rlgate-0.0.1/.gitignore +12 -0
  4. rlgate-0.0.1/ARCHITECTURE.md +452 -0
  5. rlgate-0.0.1/CONTRIBUTING.md +39 -0
  6. rlgate-0.0.1/LICENSE +21 -0
  7. rlgate-0.0.1/PKG-INFO +81 -0
  8. rlgate-0.0.1/PLAN.md +448 -0
  9. rlgate-0.0.1/QUALITY_PROCESS.md +130 -0
  10. rlgate-0.0.1/README.md +55 -0
  11. rlgate-0.0.1/pyproject.toml +52 -0
  12. rlgate-0.0.1/rlgate/__init__.py +7 -0
  13. rlgate-0.0.1/rlgate/baseline/__init__.py +0 -0
  14. rlgate-0.0.1/rlgate/baseline/invariant.py +30 -0
  15. rlgate-0.0.1/rlgate/cli.py +75 -0
  16. rlgate-0.0.1/rlgate/core/__init__.py +0 -0
  17. rlgate-0.0.1/rlgate/core/context.py +47 -0
  18. rlgate-0.0.1/rlgate/core/finding.py +40 -0
  19. rlgate-0.0.1/rlgate/core/probe.py +44 -0
  20. rlgate-0.0.1/rlgate/core/registry.py +46 -0
  21. rlgate-0.0.1/rlgate/core/report.py +78 -0
  22. rlgate-0.0.1/rlgate/core/task.py +102 -0
  23. rlgate-0.0.1/rlgate/ingest/__init__.py +0 -0
  24. rlgate-0.0.1/rlgate/ingest/presets/__init__.py +0 -0
  25. rlgate-0.0.1/rlgate/ingest/schemas/__init__.py +0 -0
  26. rlgate-0.0.1/rlgate/ingest/spec.py +75 -0
  27. rlgate-0.0.1/rlgate/pipeline.py +64 -0
  28. rlgate-0.0.1/rlgate/probes/__init__.py +0 -0
  29. rlgate-0.0.1/rlgate/probes/forgery/__init__.py +0 -0
  30. rlgate-0.0.1/rlgate/probes/forgery/world_readable_tests.py +44 -0
  31. rlgate-0.0.1/rlgate/probes/grader_strength/__init__.py +0 -0
  32. rlgate-0.0.1/rlgate/probes/grader_strength/revert_probe.py +44 -0
  33. rlgate-0.0.1/rlgate/probes/hygiene/__init__.py +0 -0
  34. rlgate-0.0.1/rlgate/probes/leakage/__init__.py +0 -0
  35. rlgate-0.0.1/rlgate/probes/leakage/git_unreachable_objects.py +46 -0
  36. rlgate-0.0.1/rlgate/probes/patterns/__init__.py +0 -0
  37. rlgate-0.0.1/rlgate/probes/reward_shape/__init__.py +0 -0
  38. rlgate-0.0.1/rlgate/probes/test_quality/__init__.py +0 -0
  39. rlgate-0.0.1/rlgate/sandbox/__init__.py +0 -0
  40. rlgate-0.0.1/rlgate/sandbox/_patch.py +30 -0
  41. rlgate-0.0.1/rlgate/sandbox/base.py +63 -0
  42. rlgate-0.0.1/rlgate/sandbox/docker.py +120 -0
  43. rlgate-0.0.1/rlgate/sandbox/local.py +62 -0
  44. rlgate-0.0.1/rlgate/sandbox/select.py +22 -0
  45. rlgate-0.0.1/rlgate/variants/__init__.py +0 -0
  46. rlgate-0.0.1/rlgate/variants/engine.py +76 -0
  47. rlgate-0.0.1/rlgate/variants/revert.py +70 -0
  48. rlgate-0.0.1/tests/core/test_registry.py +18 -0
  49. rlgate-0.0.1/tests/core/test_revert_determinism.py +32 -0
  50. rlgate-0.0.1/tests/core/test_sandbox_select.py +29 -0
  51. rlgate-0.0.1/tests/fixtures/grader_strength/weak_suite/fix.patch +15 -0
  52. rlgate-0.0.1/tests/fixtures/grader_strength/weak_suite/repo/calc.py +14 -0
  53. rlgate-0.0.1/tests/fixtures/grader_strength/weak_suite/repo/check.py +5 -0
  54. rlgate-0.0.1/tests/fixtures/grader_strength/weak_suite/task.rlgate.yaml +7 -0
  55. rlgate-0.0.1/tests/probes/test_docker_backend.py +29 -0
  56. rlgate-0.0.1/tests/probes/test_revert_e2e.py +22 -0
  57. rlgate-0.0.1/tests/probes/test_world_readable.py +31 -0
  58. rlgate-0.0.1/uv.lock +201 -0
@@ -0,0 +1,19 @@
1
+ name: ci
2
+ on:
3
+ push:
4
+ pull_request:
5
+
6
+ jobs:
7
+ test:
8
+ runs-on: ubuntu-latest
9
+ steps:
10
+ - uses: actions/checkout@v4
11
+ - uses: astral-sh/setup-uv@v3
12
+ - name: Set up Python
13
+ run: uv python install 3.12
14
+ - name: Install
15
+ run: uv pip install --system -e ".[dev]"
16
+ - name: Lint
17
+ run: ruff check .
18
+ - name: Test
19
+ run: pytest -q
@@ -0,0 +1,18 @@
1
+ name: release
2
+ on:
3
+ push:
4
+ tags: ["v*"] # e.g. v0.0.1
5
+
6
+ jobs:
7
+ pypi:
8
+ runs-on: ubuntu-latest
9
+ environment: pypi # configure PyPI Trusted Publishing for this repo+environment
10
+ permissions:
11
+ id-token: write # OIDC token for Trusted Publishing (no API token needed)
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+ - uses: astral-sh/setup-uv@v3
15
+ - name: Build
16
+ run: uv build
17
+ - name: Publish to PyPI (Trusted Publishing via OIDC)
18
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,12 @@
1
+ .venv/
2
+ dist/
3
+ build/
4
+ __pycache__/
5
+
6
+ # Evaluation data (MiMo envs etc.) — large, kept out of git
7
+ data/
8
+ *.egg-info/
9
+ .pytest_cache/
10
+ .ruff_cache/
11
+ *.pyc
12
+ .DS_Store
@@ -0,0 +1,452 @@
1
+ # rlgate — Architecture & Codebase Structure
2
+
3
+ Design goals, in priority order:
4
+
5
+ 1. **One check = one file.** Every probe is a self-contained module in a family folder,
6
+ auto-discovered — no central list to edit, no merge conflicts between contributors.
7
+ 2. **Everything pluggable, not just probes.** Mutators, adapters, sandbox backends, and
8
+ report parsers all use the *same* registry pattern, so the engine extends in every
9
+ direction.
10
+ 3. **Every contribution ships with a fixture.** A probe PR includes a known-bad task it fires
11
+ on *and* a clean task it must stay silent on. CI runs them. This is what keeps an
12
+ open-source probe bank trustworthy as it grows.
13
+ 4. **Honest metadata on every probe.** Each probe declares whether it's `GENERIC` (free with
14
+ the engine) or `PATTERN` (per-grader library), whether it `needs_live`, and its verdict
15
+ tier — so reports never overclaim (see PLAN.md §3.6–§3.8, §7 caveats).
16
+
17
+ Stack: **Python 3.12+**, `uv`, `ruff` + `pyright`, `pytest`. MIT-licensed core.
18
+
19
+ ---
20
+
21
+ ## 1. Directory tree
22
+
23
+ ```
24
+ rlgate/
25
+ ├── pyproject.toml # deps, entry points, tool config
26
+ ├── README.md
27
+ ├── PLAN.md # the methodology (already written)
28
+ ├── ARCHITECTURE.md # this file
29
+ ├── CONTRIBUTING.md # how to add a probe / adapter (the 2-file flow)
30
+ ├── LICENSE # MIT
31
+ ├── .github/
32
+ │ └── workflows/ci.yml # lint + type + unit + fixture gate
33
+ │
34
+ ├── src/rlgate/
35
+ │ ├── __init__.py
36
+ │ ├── cli.py # `rlgate audit <task>`, `list-probes`, `explain`
37
+ │ ├── config.py # TOML/env config, profiles (fast / full / ci)
38
+ │ ├── pipeline.py # orchestration: adapt → baseline → probes → report → gate
39
+ │ │
40
+ │ ├── core/ # stable contracts — changes here are breaking, reviewed hard
41
+ │ │ ├── task.py # Task: the normalized env schema
42
+ │ │ ├── finding.py # Finding, Severity, VerdictTier, Evidence, Replay
43
+ │ │ ├── probe.py # Probe ABC + ProbeMeta (family, kind, needs_live, ...)
44
+ │ │ ├── context.py # ProbeContext: sandbox handle, baseline, shared caches
45
+ │ │ ├── registry.py # @register + discovery (scan + entry points)
46
+ │ │ └── report.py # EQS, KEEP/FIX/DROP triage, JSON + Markdown renderers
47
+ │ │
48
+ │ ├── sandbox/ # execution substrate (pluggable backend)
49
+ │ │ ├── base.py # Sandbox ABC: build, snapshot, overlay, exec, perms, pkill
50
+ │ │ ├── docker.py
51
+ │ │ ├── daytona.py
52
+ │ │ ├── local.py # dev-only, no isolation guarantees
53
+ │ │ └── parsers/ # native report parsers (pluggable)
54
+ │ │ ├── base.py # ReportParser ABC -> list[TestResult]
55
+ │ │ ├── junit.py # Python / generic
56
+ │ │ ├── trx.py # C# / .NET
57
+ │ │ ├── vitest_json.py # TypeScript
58
+ │ │ ├── go_json.py # Go `-json`
59
+ │ │ └── cpp_exit.py # C++ exit-code corpus
60
+ │ │
61
+ │ ├── baseline/ # Phase B — runs once per task, feeds every probe
62
+ │ │ ├── invariant.py # oracle==1.0 / nop==0.0 @ k=5, flaky detection
63
+ │ │ └── pertest.py # per-test capture → test→region map (for subset execution)
64
+ │ │
65
+ │ ├── variants/ # §3.4 machinery, shared by grader-strength probes
66
+ │ │ ├── engine.py # generate → build-gate classify → run on SUBSET → score
67
+ │ │ ├── revert.py # gold-minus-one-hunk generator
68
+ │ │ ├── stub.py # body-gutting generator
69
+ │ │ ├── classify.py # {did-not-build, failed-all, discriminated}
70
+ │ │ ├── equivalence.py # mutation non-equivalence confirmation
71
+ │ │ └── mutators/ # AST mutators, one file per language (pluggable)
72
+ │ │ ├── base.py # Mutator ABC
73
+ │ │ ├── python.py
74
+ │ │ ├── go.py
75
+ │ │ ├── cpp.py
76
+ │ │ └── typescript.py
77
+ │ │
78
+ │ ├── probes/ # ★ the heart: one check per file, auto-discovered
79
+ │ │ ├── forgery/ # §3.1 (GENERIC)
80
+ │ │ │ ├── unrestored_inputs.py
81
+ │ │ │ ├── world_readable_tests.py
82
+ │ │ │ ├── forgeable_report.py
83
+ │ │ │ ├── fail_closed_on_missing.py
84
+ │ │ │ ├── detached_child_race.py # needs_live=True
85
+ │ │ │ ├── answer_in_artifact.py
86
+ │ │ │ └── graded_source_tamper.py
87
+ │ │ ├── hygiene/ # §3.2 (GENERIC, static)
88
+ │ │ │ ├── no_bind_mounts.py
89
+ │ │ │ ├── pinned_deps.py
90
+ │ │ │ ├── no_bare_nproc.py
91
+ │ │ │ ├── environment_mode_separate.py
92
+ │ │ │ ├── no_build_time_service.py
93
+ │ │ │ └── gold_patch_roundtrip.py
94
+ │ │ ├── leakage/ # §3.3 (GENERIC)
95
+ │ │ │ ├── git_later_history.py
96
+ │ │ │ ├── git_unreachable_objects.py
97
+ │ │ │ ├── mtime_clustering.py
98
+ │ │ │ ├── answer_in_filename.py
99
+ │ │ │ └── cache_scan.py
100
+ │ │ ├── grader_strength/ # §3.4 (GENERIC — thin wrappers over variants/engine)
101
+ │ │ │ ├── revert_probe.py
102
+ │ │ │ ├── stub_probe.py
103
+ │ │ │ ├── mutation_probe.py
104
+ │ │ │ └── heldout_redundancy.py
105
+ │ │ ├── reward_shape/ # §3.5 (GENERIC but FORMAT-SPECIFIC linter; A1–A7)
106
+ │ │ │ ├── _schema.py # reads a ScoringSchema (ingest/schemas); requires SCORING_SPEC
107
+ │ │ │ ├── a1_heldout_no_gradient.py
108
+ │ │ │ ├── a2_cosmetic_milestones.py
109
+ │ │ │ ├── a3_inconsistent_weights.py
110
+ │ │ │ ├── a4_p2p_multiplier_cliff.py
111
+ │ │ │ ├── a5_lint_measures_nothing.py
112
+ │ │ │ ├── a6_hack_flag_direction.py
113
+ │ │ │ └── a7_stale_metadata.py
114
+ │ │ ├── test_quality/ # §3.9 (GENERIC static)
115
+ │ │ │ ├── exact_string_pinning.py
116
+ │ │ │ ├── call_count_assertions.py
117
+ │ │ │ ├── trivial_transcription.py
118
+ │ │ │ └── discarded_instructed_work.py
119
+ │ │ └── patterns/ # §3.6–§3.8 (PATTERN library — per-grader, grows over time)
120
+ │ │ ├── structural/ # "what does the grader check?"
121
+ │ │ ├── ablation/ # "gold only where checked, degenerate elsewhere"
122
+ │ │ └── fault_injection/# compile-error / transient-fault injection
123
+ │ │
124
+ │ └── ingest/ # ANY env → Task (generic-first, presets optional)
125
+ │ ├── spec.py # ★ declarative descriptor: describe any env in config, NO code
126
+ │ ├── base.py # Loader ABC (for when config isn't enough)
127
+ │ ├── schemas/ # scoring-spec schema plugins (capability SCORING_SPEC)
128
+ │ │ ├── base.py # ScoringSchema ABC — reward_shape probes read THIS, not a format
129
+ │ │ └── harbor_scorepy.py # one example schema; add others as plugins
130
+ │ └── presets/ # OPTIONAL convenience loaders for popular layouts
131
+ │ ├── swebench.py # thin: dataset row → spec.py
132
+ │ ├── harbor.py
133
+ │ ├── karotte.py
134
+ │ └── mimo.py
135
+ │
136
+ ├── tests/
137
+ │ ├── conftest.py
138
+ │ ├── fixtures/ # ★ known-bad + known-clean tasks (the §7 catalog, realized)
139
+ │ │ ├── forgery/
140
+ │ │ │ └── unrestored_package_json/ # a task that IS forgeable + an oracle that isn't
141
+ │ │ ├── leakage/
142
+ │ │ ├── grader_strength/
143
+ │ │ └── ...
144
+ │ ├── core/ # contract tests
145
+ │ └── probes/ # one test module per probe, driven by fixtures
146
+ │
147
+ └── docs/
148
+ ├── writing-a-probe.md
149
+ ├── writing-an-adapter.md
150
+ ├── writing-a-pattern.md
151
+ └── probe-catalog.md # generated from ProbeMeta (CI keeps it in sync)
152
+ ```
153
+
154
+ ---
155
+
156
+ ## 2. The core contracts (what a contributor codes against)
157
+
158
+ ### `core/task.py` — the normalized environment
159
+ Every adapter produces this; every probe consumes it. The single interop point.
160
+
161
+ ```python
162
+ class Capability(Enum):
163
+ """What a task *provides* — probes require these, never a benchmark name."""
164
+ GOLD_PATCH = "gold_patch" # a reference fix with addressable hunks
165
+ GIT_REPO = "git_repo" # workspace is a git checkout
166
+ CONTAINER = "container" # grader runs in a container we control
167
+ SCORING_SPEC = "scoring_spec" # a declared reward formula (any schema plugin)
168
+ PER_TEST = "per_test" # grader emits per-test results (junit/trx/go-json/…)
169
+ INSTRUCTION = "instruction" # an agent-facing prompt exists
170
+ # extend freely — adding a capability is additive
171
+
172
+ @dataclass(frozen=True)
173
+ class Task:
174
+ id: str
175
+ source: str # where it came from (free-form; NOT a probe switch)
176
+ instruction: str | None
177
+ repo_dir: Path # the task's on-disk workspace (as shipped)
178
+ grader: GraderSpec # how to score a submission (cmd, image, report fmt)
179
+ gold_patch: Patch | None = None # present → GOLD_PATCH capability
180
+ scoring_spec: ScoringSpec | None = None # present → SCORING_SPEC capability
181
+ languages: frozenset[str] = frozenset() # drives language gating
182
+ meta: dict = field(default_factory=dict) # source-specific extras; generic probes never read it
183
+
184
+ @cached_property
185
+ def capabilities(self) -> frozenset[Capability]:
186
+ """Derived from what's actually present — the single thing probes gate on."""
187
+ caps = set()
188
+ if self.gold_patch: caps.add(Capability.GOLD_PATCH)
189
+ if self.scoring_spec: caps.add(Capability.SCORING_SPEC)
190
+ if self.instruction: caps.add(Capability.INSTRUCTION)
191
+ if (self.repo_dir / ".git").exists(): caps.add(Capability.GIT_REPO)
192
+ caps |= self.grader.capabilities # CONTAINER, PER_TEST, …
193
+ return frozenset(caps)
194
+ ```
195
+
196
+ ### `core/finding.py` — what every probe returns
197
+ ```python
198
+ class Severity(IntEnum): LOW=1; MEDIUM=2; HIGH=3; CRITICAL=4
199
+ class VerdictTier(Enum): CONFIRMED="confirmed"; REVIEW="review"; DEFERRED="deferred"
200
+
201
+ @dataclass
202
+ class Evidence:
203
+ kind: Literal["reproduced_exploit","code_line","variant","perms","diff"]
204
+ detail: str
205
+ replay: str | None # exact command to reproduce the finding
206
+ artifacts: list[Path] = field(default_factory=list)
207
+
208
+ @dataclass
209
+ class Finding:
210
+ probe_id: str
211
+ title: str
212
+ severity: Severity
213
+ verdict: VerdictTier
214
+ location: str | None # file:line the finding anchors to
215
+ evidence: Evidence
216
+ fix_hint: str | None # the actionable remediation (FIX, not just DROP)
217
+ ```
218
+
219
+ ### `core/probe.py` — the one interface every check implements
220
+ ```python
221
+ class ProbeKind(Enum): GENERIC="generic"; PATTERN="pattern" # honest §7 marker
222
+
223
+ @dataclass(frozen=True)
224
+ class ProbeMeta:
225
+ id: str # stable, e.g. "forgery.unrestored_inputs"
226
+ family: str # "forgery" | "leakage" | ...
227
+ kind: ProbeKind # GENERIC (free) vs PATTERN (per-grader)
228
+ severity: Severity
229
+ needs_live: bool = False # requires a live run (e.g. race probes)
230
+ languages: frozenset[str] | None = None # None = language-agnostic
231
+ requires: frozenset[Capability] = frozenset() # task must provide ALL of these
232
+ doc: str = "" # one-line summary → probe-catalog.md
233
+
234
+ class Probe(ABC):
235
+ meta: ProbeMeta # class attribute
236
+
237
+ def applicable(self, task: Task) -> bool:
238
+ """Default: task has every capability in meta.requires, and languages match.
239
+ Probes gate on CAPABILITIES, never on task.source / benchmark identity."""
240
+ return (self.meta.requires <= task.capabilities and
241
+ (self.meta.languages is None or self.meta.languages & task.languages))
242
+
243
+ @abstractmethod
244
+ def run(self, task: Task, ctx: ProbeContext) -> list[Finding]:
245
+ """Deterministic. Use ctx.sandbox / ctx.baseline / ctx.variants. No network
246
+ unless declared. Returns zero or more Findings."""
247
+ ```
248
+
249
+ ### `core/context.py` — shared, expensive state (built once, handed to every probe)
250
+ ```python
251
+ @dataclass
252
+ class ProbeContext:
253
+ sandbox: Sandbox # snapshot + copy-on-write overlays
254
+ baseline: Baseline # oracle/nop @ k=5 result + per-test→region map
255
+ variants: VariantEngine # revert/stub/mutation, build-gated, subset-run
256
+ cache: ResultCache # content-hashed; variant reruns are free
257
+ config: Config
258
+ ```
259
+
260
+ ### `core/registry.py` — why there's no central list to edit
261
+ ```python
262
+ _PROBES: dict[str, type[Probe]] = {}
263
+
264
+ def register(cls: type[Probe]) -> type[Probe]:
265
+ if cls.meta.id in _PROBES:
266
+ raise ValueError(f"duplicate probe id {cls.meta.id}")
267
+ _PROBES[cls.meta.id] = cls
268
+ return cls
269
+
270
+ def discover() -> list[type[Probe]]:
271
+ # 1. import every module under rlgate.probes.* (in-repo contributions)
272
+ # 2. load third-party probes via entry points: group="rlgate.probes"
273
+ ...
274
+ ```
275
+
276
+ A contributor drops a file in `probes/<family>/` with `@register` on the class. Discovery
277
+ imports it; it appears in `list-probes`, the catalog, and every audit. External packages can
278
+ ship probes via the `rlgate.probes` entry-point group without touching this repo at all.
279
+
280
+ ### Ingestion is generic-first (this is what makes it benchmark-agnostic)
281
+ rlgate is **not** built for a fixed list of benchmarks. The default way to onboard *any*
282
+ environment is a **declarative descriptor** — you describe it once, in config, with no code:
283
+
284
+ ```yaml
285
+ # task.rlgate.yaml — works for ANY environment
286
+ instruction_path: TASK.md # or inline
287
+ workspace: ./repo # the as-shipped workspace
288
+ gold_patch: ./solution/fix.patch # optional → enables GOLD_PATCH probes
289
+ grader:
290
+ image: ./Dockerfile # or an existing image
291
+ run: "bash run_tests.sh"
292
+ report: {format: junit, path: /out/results.xml} # → PER_TEST capability
293
+ scoring_spec: # optional → enables reward_shape probes
294
+ schema: harbor_scorepy
295
+ path: ./score.py
296
+ ```
297
+
298
+ `ingest/spec.py` turns that into a `Task`; its `capabilities` are **derived from which keys
299
+ are present**. No gold patch → no revert probe, silently. No `scoring_spec` → no reward_shape,
300
+ silently. Nothing in any probe mentions the benchmark.
301
+
302
+ - **`presets/`** are *optional* thin loaders — e.g. `swebench.py` just maps a dataset row onto
303
+ the same descriptor so you don't hand-write YAML for a known layout. A preset is a
304
+ convenience, never a dependency: deleting all of them leaves a fully working tool driven by
305
+ config.
306
+ - **`schemas/`** is how reward-shape stays general: `reward_shape` probes read a
307
+ `ScoringSchema` interface, not a specific `score.py`. Harbor's format is one schema plugin;
308
+ a new reward format = one schema file, and A1–A7 apply to it if it exposes the relevant
309
+ fields.
310
+
311
+ ### `sandbox/base.py` and `ingest/base.py`
312
+ Same shape: small ABCs, registered the same way. `Sandbox` exposes `build`, `snapshot`,
313
+ `overlay(patch)`, `exec`, `read_perms`, `pkill_user` — enough for forgery/race probes. A
314
+ `Loader` (only needed when config can't express the env) exposes
315
+ `discover(source) -> Iterable[Task]`.
316
+
317
+ ---
318
+
319
+ ## 2a. Determinism guarantees (rules for anyone writing a generator or operator)
320
+
321
+ Separate two things both called "deterministic":
322
+
323
+ - **Variant *generation*** (producing a hunk-subtracted patch / mutant) — **must be a pure
324
+ function** of pinned inputs. Guaranteed by construction; see rules below.
325
+ - **Variant *scoring*** (running the grader) — **not** inherently deterministic (flaky tests,
326
+ timing, parallelism, network). Handled separately: baseline at **k=5**, flaky checks flagged
327
+ as unfair tests, network isolation, pinned deps. A finding is reported only when generation
328
+ is reproducible **and** the verdict is stable across the k runs.
329
+
330
+ **Generation rules (enforced; a PR that breaks one fails review):**
331
+
332
+ 1. **Canonicalize + hash the gold patch once.** Normalize to a canonical unified diff (fixed
333
+ `-U3` context, normalized whitespace/EOL) and record `gold_patch_sha256`. Hunk
334
+ decomposition is then byte-identical across machines and git versions.
335
+ 2. **Address hunks by structure, not search.** A hunk is `(file, hunk_index)` from the parsed
336
+ diff — no heuristics. Reverse-apply exactly one hunk with **zero fuzz (`-C0`)**: it either
337
+ applies cleanly (one result) or is bucketed as `could-not-generate` (deterministically).
338
+ 3. **No RNG in generators.** Revert/stub are direct text ops. The mutation generator walks the
339
+ AST in **canonical source-position order** and applies operators in **fixed registry
340
+ order**, one mutant per `(node, operator)`. If a per-task cap is hit, select by a
341
+ **deterministic stable sort**, never `random.sample`. A seed is recorded for audit, but
342
+ nothing draws from it.
343
+ 4. **Content-addressed IDs.** `variant_id = hash(gold_patch_sha256 + file + hunk_index +
344
+ operator_version)`. Same inputs → same ID → cache hit → exact replay.
345
+ 5. **Versioned operator set.** Operators are a frozen, ordered registry (`operators@vN`).
346
+ Adding/changing an operator **bumps the version**; it never silently alters an existing
347
+ variant.
348
+
349
+ **Run manifest** (written per audit, hashed, attached to findings so they replay): gold sha,
350
+ patch canonicalization params, git-apply flags, `operators@vN`, tool/image versions, k, seed,
351
+ and the resolved probe set with their `ProbeMeta`. This is the generation-side analogue of a
352
+ campaign manifest — it makes every finding reproducible from the manifest alone.
353
+
354
+ ---
355
+
356
+ ## 3. A complete example probe (the shape every contributor copies)
357
+
358
+ `src/rlgate/probes/forgery/world_readable_tests.py`:
359
+ ```python
360
+ from rlgate.core.probe import Probe, ProbeMeta, ProbeKind
361
+ from rlgate.core.finding import Finding, Severity, VerdictTier, Evidence
362
+ from rlgate.core.registry import register
363
+ from rlgate.core.task import Task
364
+ from rlgate.core.context import ProbeContext
365
+
366
+ @register
367
+ class WorldReadableTests(Probe):
368
+ meta = ProbeMeta(
369
+ id="forgery.world_readable_tests",
370
+ family="forgery",
371
+ kind=ProbeKind.GENERIC,
372
+ severity=Severity.HIGH,
373
+ doc="Held-out sources / scorer are readable by the candidate UID (not chmod 700).",
374
+ )
375
+
376
+ def run(self, task: Task, ctx: ProbeContext) -> list[Finding]:
377
+ perms = ctx.sandbox.read_perms("/tests")
378
+ if perms.world_readable:
379
+ return [Finding(
380
+ probe_id=self.meta.id,
381
+ title="/tests is world-readable; held-out names leak at runtime",
382
+ severity=Severity.HIGH,
383
+ verdict=VerdictTier.CONFIRMED,
384
+ location="/tests",
385
+ evidence=Evidence(
386
+ kind="perms", detail=f"mode={perms.mode:o}",
387
+ replay="rlgate audit <task> --probe forgery.world_readable_tests",
388
+ ),
389
+ fix_hint="chmod 700 /tests; delete graded sources after build.",
390
+ )]
391
+ return []
392
+ ```
393
+
394
+ Its fixture (ships in the same PR): `tests/fixtures/forgery/world_readable_tests/` with a
395
+ `bad/` task (mode 755) the probe must flag and an `oracle/` task (mode 700) it must not.
396
+ `tests/probes/test_world_readable_tests.py` runs both.
397
+
398
+ ---
399
+
400
+ ## 4. The contribution flow (CONTRIBUTING.md, in one line)
401
+
402
+ > **Add a probe = add two files.** `probes/<family>/<name>.py` (the `@register`ed class) and
403
+ > `tests/fixtures/<family>/<name>/` (a `bad/` it flags + an `oracle/` it stays silent on). No
404
+ > central registry, no pipeline edits. CI runs your fixture; the catalog regenerates itself.
405
+
406
+ Same for adapters (`adapters/<name>.py` + a sample task) and patterns (`probes/patterns/.../`
407
+ + the specific grader it targets, with a note on what it assumes).
408
+
409
+ ---
410
+
411
+ ## 5. How the pipeline wires it together (`pipeline.py`)
412
+
413
+ ```
414
+ source ──ingest (descriptor, or optional preset)──▶ [Task] (capabilities derived)
415
+ Task ──baseline.invariant (k=5) + baseline.pertest──▶ Baseline # Phase B, fail-fast
416
+ Task + ProbeContext ──discover() ▸ applicable(capabilities) ▸ run──▶ [Finding] # parallel
417
+ [Finding] ──report.render──▶ EQS + KEEP/FIX/DROP + JSON/Markdown
418
+ └──▶ CI gate: exit non-zero on any release-blocker (PLAN §8a/§8b)
419
+ ```
420
+
421
+ - **Baseline is fail-fast:** if `oracle != 1.0` or a check is flaky at k=5, we emit that
422
+ finding and skip the probes that assume a sane baseline (they'd be noise).
423
+ - **Probes run in parallel** over the shared `ProbeContext`; the `VariantEngine` fans out
424
+ variant runs across sandbox workers and caches by content hash.
425
+ - **`needs_live` probes** are grouped and run on a real container pass (never synthetic-only).
426
+ - **Profiles:** `--profile fast` skips live/variant-heavy probes for quick local feedback;
427
+ `--profile ci` runs everything and enforces the go/no-go gate.
428
+
429
+ ---
430
+
431
+ ## 6. What stays out of core (so the project stays extensible)
432
+
433
+ - No probe imports another probe. Shared logic lives in `variants/`, `baseline/`, or a
434
+ `core/` helper — never probe-to-probe coupling.
435
+ - Source-specific data rides in `Task.meta`; **generic probes never read `meta`** and never
436
+ branch on `task.source` — they gate only on `Task.capabilities`.
437
+ - The LLM layer, when it comes, is just another probe `kind` (`ADJUDICATOR`) plus an
438
+ `llm/` client module — it reuses this exact contract and the execution gate. Nothing in
439
+ §2 changes to add it.
440
+ ```
441
+ ```
442
+
443
+ ---
444
+
445
+ ## 7. Milestone 0 skeleton (smallest thing that runs end-to-end)
446
+
447
+ To prove the architecture before breadth: `core/` contracts + `sandbox/docker.py` +
448
+ `baseline/invariant.py` + `ingest/spec.py` (+ a thin SWE-bench preset over it) + **three** probes
449
+ (`leakage.git_unreachable_objects`, `grader_strength.revert_probe`,
450
+ `forgery.world_readable_tests`) + their fixtures + the CI gate. That exercises every seam
451
+ (adapter, baseline, sandbox, variants, a static probe, a live-ish probe, reporting) with the
452
+ minimum code, so the next 40 probes are pure fill-in.
@@ -0,0 +1,39 @@
1
+ # Contributing to rlgate
2
+
3
+ ## Add a probe = add two files
4
+
5
+ 1. **The probe** — `rlgate/probes/<family>/<name>.py`: a `@register`-decorated `Probe`
6
+ subclass with a `ProbeMeta`. Discovery picks it up automatically; there is **no central
7
+ list to edit** and no pipeline change.
8
+ 2. **A fixture** — `tests/fixtures/<family>/<name>/` with a `bad/` task your probe must flag
9
+ and an `oracle/` (clean) task it must stay silent on, plus a test in `tests/probes/`.
10
+
11
+ That's it. CI runs your fixture; the probe catalog regenerates from `ProbeMeta`.
12
+
13
+ ### Rules
14
+
15
+ - **Gate on capabilities, never on a benchmark name.** Use `ProbeMeta.requires`; never read
16
+ `task.source` or branch on a format. If you need source-specific data it lives in
17
+ `task.meta`, and only a companion probe for that source may read it.
18
+ - **Deterministic generation.** Any new variant generator / mutation operator must follow
19
+ ARCHITECTURE §2a: no RNG, content-addressed IDs, versioned operator set. Scoring
20
+ nondeterminism (flaky tests) is handled by the baseline, not your probe.
21
+ - **A finding is evidence, not a score.** Return a reproduced exploit or a concrete
22
+ `file:line` with a `replay` command and a `fix_hint`. Never classify from a reward value.
23
+ - **Mark `kind` honestly.** `GENERIC` = runs on any task with the capabilities. `PATTERN` =
24
+ needs per-grader understanding (§3.6–§3.8); say so.
25
+ - **Set `needs_live`** if the probe requires a real container run (e.g. race probes).
26
+
27
+ ## Add a new environment type
28
+
29
+ Prefer the declarative descriptor (`task.rlgate.yaml`) — most environments need **no code**.
30
+ Only add a preset under `rlgate/ingest/presets/` for a popular layout, or a scoring-schema
31
+ plugin under `rlgate/ingest/schemas/` for a new reward format.
32
+
33
+ ## Dev loop
34
+
35
+ ```bash
36
+ uv pip install -e ".[dev]"
37
+ ruff check .
38
+ pytest -q
39
+ ```
rlgate-0.0.1/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 rlgate contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
rlgate-0.0.1/PKG-INFO ADDED
@@ -0,0 +1,81 @@
1
+ Metadata-Version: 2.5
2
+ Name: rlgate
3
+ Version: 0.0.1
4
+ Summary: A deterministic, third-party auditor that finds reward-hacking vulnerabilities in RL/eval environments.
5
+ Project-URL: Homepage, https://github.com/deepak-foundryai/rlgate
6
+ Project-URL: Repository, https://github.com/deepak-foundryai/rlgate
7
+ Project-URL: Issues, https://github.com/deepak-foundryai/rlgate/issues
8
+ Author: rlgate contributors
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: auditing,evals,grader,llm,reward,reward-hacking,rl
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
17
+ Classifier: Topic :: Software Development :: Testing
18
+ Requires-Python: >=3.12
19
+ Requires-Dist: pyyaml>=6.0
20
+ Requires-Dist: unidiff>=0.7.5
21
+ Provides-Extra: dev
22
+ Requires-Dist: pyright>=1.1; extra == 'dev'
23
+ Requires-Dist: pytest>=8.0; extra == 'dev'
24
+ Requires-Dist: ruff>=0.6; extra == 'dev'
25
+ Description-Content-Type: text/markdown
26
+
27
+ # rlgate
28
+
29
+ **A deterministic, third-party auditor that finds reward-hacking vulnerabilities in RL/eval
30
+ environments** — before you train on them.
31
+
32
+ rlgate is not an environment framework and not a trajectory benchmark. It *consumes* any
33
+ environment (via a one-file declarative descriptor) and attacks its grader with deterministic
34
+ probes: it reverts hunks of the gold fix to find unguarded tests, plants forged artifacts to
35
+ find grading-channel forgery, walks the git object store to find leaked answers, and lints the
36
+ reward formula for design debt. Every finding is a **reproduced exploit or a line of code** —
37
+ never a threshold on a score.
38
+
39
+ See **[PLAN.md](PLAN.md)** for the methodology and **[ARCHITECTURE.md](ARCHITECTURE.md)** for
40
+ the design.
41
+
42
+ ## Quickstart
43
+
44
+ Run it as an isolated tool straight from the repo (no clone, no install):
45
+
46
+ ```bash
47
+ uvx --from git+https://github.com/deepak-foundryai/rlgate rlgate list-probes
48
+ uvx --from git+https://github.com/deepak-foundryai/rlgate rlgate audit path/to/env --gate
49
+ ```
50
+
51
+ Or for development:
52
+
53
+ ```bash
54
+ uv pip install -e ".[dev]" # or: pip install -e ".[dev]"
55
+ rlgate list-probes
56
+ rlgate audit path/to/env --gate # exits non-zero on FIX/DROP (CI mode)
57
+ ```
58
+
59
+ Once published to PyPI: `uvx rlgate audit ./env` or `uv tool install rlgate`.
60
+
61
+ Describe any environment once — no code:
62
+
63
+ ```yaml
64
+ # task.rlgate.yaml
65
+ workspace: ./repo
66
+ gold_patch: ./solution/fix.patch # optional → enables the revert probe
67
+ grader:
68
+ run: "bash run_tests.sh"
69
+ report: {format: junit, path: /out/results.xml}
70
+ ```
71
+
72
+ Capabilities are derived from which keys you fill in; probes that need a missing capability
73
+ skip silently. Nothing in a probe mentions a benchmark.
74
+
75
+ ## Status
76
+
77
+ Milestone 0: core contracts + local sandbox + baseline invariant + generic ingestion + three
78
+ probes (leakage, revert, forgery) + fixtures + CI. Contributions welcome — see
79
+ [CONTRIBUTING.md](CONTRIBUTING.md).
80
+
81
+ MIT licensed.