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.
- rlgate-0.0.1/.github/workflows/ci.yml +19 -0
- rlgate-0.0.1/.github/workflows/release.yml +18 -0
- rlgate-0.0.1/.gitignore +12 -0
- rlgate-0.0.1/ARCHITECTURE.md +452 -0
- rlgate-0.0.1/CONTRIBUTING.md +39 -0
- rlgate-0.0.1/LICENSE +21 -0
- rlgate-0.0.1/PKG-INFO +81 -0
- rlgate-0.0.1/PLAN.md +448 -0
- rlgate-0.0.1/QUALITY_PROCESS.md +130 -0
- rlgate-0.0.1/README.md +55 -0
- rlgate-0.0.1/pyproject.toml +52 -0
- rlgate-0.0.1/rlgate/__init__.py +7 -0
- rlgate-0.0.1/rlgate/baseline/__init__.py +0 -0
- rlgate-0.0.1/rlgate/baseline/invariant.py +30 -0
- rlgate-0.0.1/rlgate/cli.py +75 -0
- rlgate-0.0.1/rlgate/core/__init__.py +0 -0
- rlgate-0.0.1/rlgate/core/context.py +47 -0
- rlgate-0.0.1/rlgate/core/finding.py +40 -0
- rlgate-0.0.1/rlgate/core/probe.py +44 -0
- rlgate-0.0.1/rlgate/core/registry.py +46 -0
- rlgate-0.0.1/rlgate/core/report.py +78 -0
- rlgate-0.0.1/rlgate/core/task.py +102 -0
- rlgate-0.0.1/rlgate/ingest/__init__.py +0 -0
- rlgate-0.0.1/rlgate/ingest/presets/__init__.py +0 -0
- rlgate-0.0.1/rlgate/ingest/schemas/__init__.py +0 -0
- rlgate-0.0.1/rlgate/ingest/spec.py +75 -0
- rlgate-0.0.1/rlgate/pipeline.py +64 -0
- rlgate-0.0.1/rlgate/probes/__init__.py +0 -0
- rlgate-0.0.1/rlgate/probes/forgery/__init__.py +0 -0
- rlgate-0.0.1/rlgate/probes/forgery/world_readable_tests.py +44 -0
- rlgate-0.0.1/rlgate/probes/grader_strength/__init__.py +0 -0
- rlgate-0.0.1/rlgate/probes/grader_strength/revert_probe.py +44 -0
- rlgate-0.0.1/rlgate/probes/hygiene/__init__.py +0 -0
- rlgate-0.0.1/rlgate/probes/leakage/__init__.py +0 -0
- rlgate-0.0.1/rlgate/probes/leakage/git_unreachable_objects.py +46 -0
- rlgate-0.0.1/rlgate/probes/patterns/__init__.py +0 -0
- rlgate-0.0.1/rlgate/probes/reward_shape/__init__.py +0 -0
- rlgate-0.0.1/rlgate/probes/test_quality/__init__.py +0 -0
- rlgate-0.0.1/rlgate/sandbox/__init__.py +0 -0
- rlgate-0.0.1/rlgate/sandbox/_patch.py +30 -0
- rlgate-0.0.1/rlgate/sandbox/base.py +63 -0
- rlgate-0.0.1/rlgate/sandbox/docker.py +120 -0
- rlgate-0.0.1/rlgate/sandbox/local.py +62 -0
- rlgate-0.0.1/rlgate/sandbox/select.py +22 -0
- rlgate-0.0.1/rlgate/variants/__init__.py +0 -0
- rlgate-0.0.1/rlgate/variants/engine.py +76 -0
- rlgate-0.0.1/rlgate/variants/revert.py +70 -0
- rlgate-0.0.1/tests/core/test_registry.py +18 -0
- rlgate-0.0.1/tests/core/test_revert_determinism.py +32 -0
- rlgate-0.0.1/tests/core/test_sandbox_select.py +29 -0
- rlgate-0.0.1/tests/fixtures/grader_strength/weak_suite/fix.patch +15 -0
- rlgate-0.0.1/tests/fixtures/grader_strength/weak_suite/repo/calc.py +14 -0
- rlgate-0.0.1/tests/fixtures/grader_strength/weak_suite/repo/check.py +5 -0
- rlgate-0.0.1/tests/fixtures/grader_strength/weak_suite/task.rlgate.yaml +7 -0
- rlgate-0.0.1/tests/probes/test_docker_backend.py +29 -0
- rlgate-0.0.1/tests/probes/test_revert_e2e.py +22 -0
- rlgate-0.0.1/tests/probes/test_world_readable.py +31 -0
- 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
|
rlgate-0.0.1/.gitignore
ADDED
|
@@ -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.
|