Skip to content

feat(proof): generic dynamic RLM engine (DB-backed rules, per-topic VM boundary, artefacts, fail-closed runner registry) - #243

Merged
echobt merged 14 commits into
mainfrom
cursor/proof-generic-rlm-engine-1fd2
Sep 8, 2026
Merged

feat(proof): generic dynamic RLM engine (DB-backed rules, per-topic VM boundary, artefacts, fail-closed runner registry)#243
echobt merged 14 commits into
mainfrom
cursor/proof-generic-rlm-engine-1fd2

Conversation

@echobt

@echobt echobt commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Summary

Proof becomes a dynamic agentic challenge system in code: the binary is a generic orchestrator (schema, DB, isolation boundary, artefact store, runner registry). Zero challenge content is compiled in — no benchmark, metric, model, rule list, repository, or topic catalog. The first live topic is a signed document its RLM sets up, never a code branch.

Rebased onto main after #242 (0d418243, EvalExecutorOffer); this PR consumes that contract and does not duplicate it (see "Executor" below).

What this PR delivers

  1. Generic signed-topic bindings (proof-task, new proof-canon): constraints.{firecracker_required, model_pin, task_slice (opaque), params} and a checklist: [{id, text}] rule vector, all shape-checked only and omitted from the signed payload when unset (same rule as feat(proof): EvalExecutorOffer contract + harvest 1x / deadline enforcement #242's eval_executor), so previously signed topics keep verifying. metric.custom_id is topic data: any well-formed id drafts; a custom topic may open only when a runner is registered under its id (the compiled-in custom id list is gone). proof-canon holds canonical JSON, id shapes, and the shared Constraints / ChecklistRule shapes (keeps proof-task under the LOC cap).
  2. DB-backed rules / checklists / lifecycle / continuum (crates/db/migrations/0020_proof_rlm.sql, new proof-rlm-store): proof_topic_version, proof_rule_version (RLM-written rules versioned with the topic, digest-bound), proof_checklist, proof_lifecycle_event, proof_baseline_measurement, proof_artefact, proof_promotion_event (best pointer + history). Append-only for base_app. PgRlmStore + MemoryRlmStore share one contract test (Postgres half runs when DATABASE_URL is set).
  3. Generic RLM core (new proof-rlm): versioned RuleSet + Checklist + SpendToken (no paid inference behind a red/incomplete/stale checklist — the token has no public constructor), lifecycle draft → owner_presend → awaiting_owner_keys → provisioning → baselining → open ⇄ evaluating → promoting → closed with askUser-style owner hook and presence-only key probe, CustomRunner + RunnerRegistry (empty by default), TopicVmOrchestrator boundary (create/attach/run/teardown; VmJobs carry public data only) with UnwiredVmOrchestrator and the generic VmBackedRunner, decide_promote.
  4. Host side (new proof-rlm-scorer): RlmScorer (LiveScorer for the whole custom family, routed via FamilyMux), topic-scoped ArtefactStore (/artefacts/{topic_id}/{submission_id}.zip, best.json, events.jsonl), TopicSetup driver over the VM boundary.
  5. Wiring: proof-http (per-topic 503 before any row/rent, scorable_topics + registered_custom on /v1/status, automatic champion via the family scorer, async persist hook); bins/proof-challenge (Postgres RLM store from BASE_DATABASE_URL(_FILE), migrated at boot; empty registry; PROOF_ARTEFACT_ROOT).

Executor (#242) — consumed, not duplicated

TopicEvalExecutor, the pin ceilings, EvalExecutorOffer, ExecutorPlan, and the harvest 1x / deadline enforcement are used as merged. FamilyMux forwards LiveScorer::plan; on the custom path each run request records the resolved plan's deadline (tighter of topic and plan) and config_commitment as provenance (CustomRunRequest::with_executor_plan), and rows stamp executor_commitment like every other scored row.

Fail-closed by construction

  • Unregistered / unwired custom_id503, no row, no rent, never a harvest fallback.
  • Red, missing, duplicated, evidence-less, or stale-version checklist item → persisted reject, no paid inference.
  • Unwired orchestrator / unpinned RLM VM image → refuse (no host-local RLM path exists).
  • open custom topic without a registered runner → 400 at publish; the same document drafts.
  • Custom submission without artifact_uri400 at intake, no row; the runner never gets a request without a locator.
  • Runner report without a measured flops_used against a topic budget → 503, no row; measured over the signed budget (flops_over_budget) or over the miner's declared_flops (flops_under_declared) → persisted reject.
  • mark_sealed opens only a status: open, structurally valid, operator-signed document whose sealed measurement is the one the RLM produced.

Greptile P1 fixes (round 1, head 20b5f18b) — all four threads resolved

P1 Fix Commits
Artifact location discarded LiveScorer::score / eval_after_freeze / FamilyMux take artifact_uri; proof-http forwards it; RlmScorer puts it in CustomRunRequest. Then: a custom submission without a locator is a 400 at intake (no row) and the scorer refuses a request without one — never None for a miner submission. e2e asserts the inspect + evaluate VM jobs carry the submitted locator. 3211601, 9f6e525
FLOP budget bypass CustomRunRequest.flops_budget + CustomRunReport.flops_used (runner-measured). flops_used_for is the only read: None against a budget is FlopsMissing (binding fails closed → 503). Verdict carries the measurement; over budget → reject + flops_over_budget. Then: the miner's declared_flops travels in the request (runner may enforce it as a hard cap) and a measurement over it is flops_under_declaredreject. Setup refuses an over-budget / unmeasured baseline. 40456c3, ad3c685
Worse result becomes champion Per-topic lease held from score through on_persisted; auto_promote decides against the harder of the caller's bar and the store's best; persist compare-and-swaps on the best pointer and requires strictly better, else promotion_refused (no row, pointer untouched). Abandoned leases reaped after DEFAULT_LEASE_TTL (5 min). 58e1ade
Invalid topic can open mark_sealed(topic, pin, registered_custom, sealed) refuses non-open, invalid, unsigned, mis-sealed, or mismatched documents before anything moves or is stored. 8296cf7

Removed (per direction)

All earlier challenge-specific work was stripped before this branch was pushed: the fixed topic id / metric id / model pin / task-slice constants and if-branches, the hardcoded eight-rule checklist, the example draft JSON catalog under docs/, the pre-existing compiled-in custom id list, and the family-specific runner. proof-rlm carries a test asserting no such literals appear in its non-test source.

Out of scope

A live VM orchestrator and runner registration mechanics (interfaces + fail-closed stubs only). No proof-eval digest was invented.

Greptile

Every PR is reviewed by Greptile before merge. Config: .greptile/.

  • Greptile has reviewed this PR; findings are fixed or answered
  • If the bot was silent, I commented @greptileai review

Test plan

  • cargo test --workspace on the current head (1258 passed, 0 failed) — includes router-level e2e for unregistered-503, score / red-reject / below-best through the registry, the setup driver, and the P1 coverage: locator passthrough + 400 without one, over-budget and under-declared rejects + unmeasured 503, lease blocking + stale-bar loss + CAS-refused crown + reaped lease, and every mark_sealed refusal
  • SQLX_OFFLINE=true DATABASE_URL=… cargo test -p proof-rlm-store against a local Postgres 16: migration 0020 applies in the full chain and both stores honour the contract; the binary boots against Postgres, runs migration 20, creates the seven tables with INSERT+SELECT-only grants
  • cargo fmt --all -- --check
  • cargo clippy --workspace --all-targets -- -D warnings, cargo deny check, xtask loc-cap / consensus-lint / spec-check / design-check / external-docs-check
  • proof-rlm anti-hardcode test (no_challenge_content_is_compiled_in) green

Risk

New crates and one additive migration; Constraints is no longer Copy (callers clone) and now lives in proof-canon (re-exported from proof-task, no call-site change). New topic knobs are omitted when unset, so existing signatures hold. No BASE_* rename, no crypto domain change, no emission semantics change. Behaviour change: an open custom topic without a registered runner is now a publish 400 (it drafts), and custom topics answer 503 until a runner is registered — both fail-closed. On custom topics artifact_uri is now required (400 without) and the runner's measured usage must stay within the miner's declared_flops; nll / throughput intake and scoring are unchanged. LiveScorer::score gained artifact_uri and declared_flops parameters (the Lium harvest ignores both); CustomRunReport gained flops_used (required whenever the topic has a budget); ProofCheatCode gained flops_under_declared; TopicSetup::mark_sealed takes the pin, the registered custom ids, and the sealed measurement.

Naming

I did not rename BASE_* environment variables, deployed host paths
(/opt/base, /run/base, …), GHCR baseintelligence/base package names, or
base-*-v1 cryptographic domain tags.

Open in Web Open in Cursor 

@echobt
echobt marked this pull request as ready for review September 8, 2026 13:11
@greptile-apps

greptile-apps Bot commented Sep 8, 2026

Copy link
Copy Markdown

Greptile Summary

Summary

This PR adds a generic, database-backed Proof RLM workflow with topic lifecycle management, runner registration, artifact persistence, executor planning, and automatic promotion.

It also carries submitted artifact locators and runner-measured FLOP usage through custom evaluation, rejects missing or excessive measurements, serializes promotion through persistence, and requires a valid signed, baseline-bound topic before opening it.

Confidence Score: 5/5

Safe to merge; there are no outstanding blocking issues.

No new correctness issues remain. The previously reported issues are fully addressed in the current implementation: custom submissions require and forward an artifact locator, runner-measured FLOP usage is required when budgeted and enforced against both the topic budget and declared cap, promotion protection remains in place through persistence, and sealed topics can open only after status, validation, signature, baseline-binding, and lifecycle checks pass.

Reviews (2): Last reviewed commit: "fix(proof-rlm): hold the measured flops ..." | Re-trigger Greptile

Comment thread crates/proof-rlm-scorer/src/scorer.rs
Comment thread crates/proof-rlm-scorer/src/scorer.rs Outdated
Comment thread crates/proof-rlm-scorer/src/scorer.rs Outdated
Comment thread crates/proof-rlm-scorer/src/setup.rs Outdated
Proof topics are the challenge; the code is not. The signed topic schema
gains only generic, shape-checked bindings whose values come from the
document:

- constraints.{firecracker_required, model_pin (vendor/model[:tag]),
  task_slice (opaque), params (<=32 opaque slug -> value)}
- checklist: [{id, text}] anti-cheat rule vector (<=64, unique slug ids)
- eval_executor.{require_offer_commitment (64 hex), max_proof_deadline_s}
  mirroring the inference judge pattern; the deadline is tighten-only
  against a new pin knob max_proof_deadline_s (crate lock 21600 s)
- metric.custom_id is topic data: any [a-z0-9][a-z0-9_-]{1,63} drafts,
  and a custom topic may open only when a runner is registered under its
  id on the host (validate takes the registered set). The compiled-in
  custom id list is gone.

proof-canon is a new tiny crate holding canonical JSON and the id shapes
shared by every Proof crate (moved out of proof-task, which sits near the
LOC cap). proof-score exposes relative_win / novelty_bar for reuse.

Signed topics need re-signing (the signing payload carries the new
default fields), as with the inference addition.
Drop the static supported_custom() list. Custom metric ids now come from
the host's live scorer (LiveScorer::custom_ids, registered_custom): a
topic mints its id, a runner registered under it makes the topic
scorable. FamilyMux routes the whole custom family to one pluggable
scorer and everything else to the digest-pinned harvest; with no custom
scorer, every custom topic is RunnerUnwired (503), never a harvest
fallback.

LiveScorer gains ready_for_topic (per-topic refusal before any row or
rent), custom_ids, and async auto_promote / on_persisted hooks so a
family scorer can crown a pass and write artefacts once the row has its
id. EvalError::RunnerUnwired names the missing runner as the root cause.
Core types for Proof as a dynamic agentic challenge system. Nothing here
names a benchmark, metric, model, rule list, or repository (a self-check
test scans the non-test source for such literals).

- RuleSet / Checklist / SpendToken: rules are a versioned vector (v1 =
  the signed topic's checklist, later versions written by the RLM). A
  checklist binds to a rule version and digest and is green only when
  every rule is ticked with evidence and passes; the token has no public
  constructor, so no paid run can be reached from a red, incomplete, or
  stale checklist.
- Lifecycle: draft -> owner_presend -> awaiting_owner_keys ->
  provisioning -> baselining -> open <-> evaluating -> promoting ->
  closed as an explicit table, with an askUser-style OwnerHook (refuses
  without a hook) and a presence-only owner key probe.
- CustomRunner + RunnerRegistry: custom_id -> runner, empty by default;
  an unregistered id is RunnerError::Unregistered (the 503 root cause).
- TopicVmOrchestrator: create / attach / run / teardown for per-topic
  VMs; VmJobs carry public data only (never a host path, key, or
  origin). UnwiredVmOrchestrator refuses and names the env vars; the
  generic VmBackedRunner turns inspect / evaluate into VM jobs and is
  only ever registered by an operator.
- decide_promote: pass + green checklist + relative win over the bar,
  direction from the topic.

Fixtures (fake orchestrator, placeholder topic) ship behind the
test-fixtures feature for sibling crates' tests.
Migration 0020_proof_rlm.sql: proof_topic_version (signed documents per
re-sign), proof_rule_version (RLM-written rule vectors, versioned with
the topic, digest-bound), proof_checklist (every inspection, red or
green), proof_lifecycle_event, proof_baseline_measurement,
proof_artefact (zip metadata), proof_promotion_event (the learning
continuum: baseline -> runs -> best -> what the next run must beat).
Append-only for base_app; the current best is the newest promotion row.

proof-rlm-store: the RlmStore contract, PgRlmStore over crates/db (plain
runtime sqlx), and MemoryRlmStore for CI / local. One contract test runs
against both; the Postgres half runs on an isolated migrated schema when
DATABASE_URL is set and is skipped otherwise.
RlmScorer is the LiveScorer for the custom family: resolve custom_id in
the registry (unknown or unwired -> RunnerUnwired, 503, no row), load the
topic's current rule version from the store (seeding v1 from the signed
document), inspect through the runner -> checklist persisted, red ->
reject document with no paid inference, green -> spend token -> evaluate
-> custom_value. Evaluations are serialised per topic and every
lifecycle move is written to the store; a stale evaluating/promoting
mirror is recovered rather than refused forever.

ArtefactStore writes {root}/{topic_id}/{submission_id}.zip (manifest,
artifact/, report, checklist, baseline_ref, logs) deterministically with
path validation, best.json on promotion, and public events.jsonl;
metadata and promotion rows mirror into the RLM store.

TopicSetup drives draft -> owner_presend -> awaiting_owner_keys ->
provisioning -> baselining over the VM boundary: owner hook, key probe,
provision or attach, RLM ProposeRules -> rules vN in store, Baseline job
-> measurement in store; mark_sealed closes baselining -> open. A re-run
resumes from the persisted state.

Router-level tests cover the unregistered 503, score / red-reject /
below-best paths through the registry with a fake orchestrator, and the
setup driver end to end.
proof-http: submit refuses a topic whose runner is not registered or
wired (503, no row) right after host readiness; publish validates custom
ids against the host's registered set (draft with any id, open only with
a runner); /v1/status gains scorable_topics and registered_custom, and
can_score means at least one open topic is scorable here; a pass the
family scorer crowns persists as champion and the async on_persisted
hook fires with the row id.

bins/proof-challenge: BASE_DATABASE_URL(_FILE) selects the Postgres RLM
store (migrated at boot; configured-but-unreachable is fatal, unset
falls back to memory with a warning); the live harvest is wrapped in
FamilyMux with an RlmScorer over an empty RunnerRegistry, so every custom
topic answers 503 until an operator / RLM registers a runner. No runner,
benchmark, model, or repository is compiled in. PROOF_ARTEFACT_ROOT
(default /artefacts) names where zips land.
PROOF.md: custom ids as topic data (draft freely, open with a registered
runner, empty registry by default), rules as a topic-carried vector
re-versioned by the RLM into the DB, the per-topic VM isolation boundary,
the lifecycle, the registry, artefacts / best pointer / public events,
scorable_topics + registered_custom on /v1/status, and the re-sign note.
external-miner/proof.md: what a custom-family submission is, the
topic-carried checklist a miner must pass, the new 503 / 201-rejected
rows, champion state. COMPLETENESS.md: RLM engine row (generic /
fail-closed). AGENTS.md: product rule (zero challenge content in git).
deploy: PROOF_ARTEFACT_ROOT on the proof-artifacts volume plus the
orchestrator / image / owner-key env var names (names only, no values).
@cursor
cursor Bot force-pushed the cursor/proof-generic-rlm-engine-1fd2 branch from 1aa2bc4 to 20b5f18 Compare September 8, 2026 13:42
Greptile P1 on #243: the custom submission flow accepted `artifact_uri`
but `RlmScorer` built `CustomRunRequest` with `None`, so the runner got
the digest and no way to retrieve the bytes.

- `LiveScorer::score`, `eval_after_freeze`, and `FamilyMux` take the
  miner's `artifact_uri`; `proof-http` forwards `body.artifact_uri`; the
  Lium harvest ignores it (it fetches by digest).
- `RlmScorer::evaluate` threads it into `CustomRunRequest::from_topic`;
  the request normalises whitespace/empty to `None`.
- e2e: the inspect and evaluate VM jobs carry the submitted locator with
  the digest; unit test for the normalisation.
Greptile P1 on #243: every custom-run verdict reported `flops_used: 0`
while neither the run request nor the verified report carried a usage
measurement, so the judge compared zero with the signed budget and an
over-budget run could pass.

- `CustomRunRequest` carries the topic's `flops_budget`; `CustomRunReport`
  carries the runner-measured `flops_used`. `flops_used_for` is the only
  way to read it: `None` against a budget is `ReportError::FlopsMissing`
  (report binding fails closed, 503, no row); zero is only a measurement.
- `RlmScorer` puts that figure in the verdict. A measurement over the
  budget is `reject` + `flops_over_budget`, which the judge fails on;
  the pre-spend reject keeps `0` because nothing ran.
- `TopicSetup::baseline` refuses a baseline that spent over the topic
  budget or measured nothing (`SetupError::BaselineOverBudget`).
- Fixtures: `FakeOrchestrator::set_flops_used`; tests for the binding,
  the persisted over-budget reject, the 503, and the setup refusals.
Greptile P1 on #243: the per-topic guard ended when `score` returned, so
two completed runs could both promote against the same old bar and the
persist path appended both promotions and moved the best pointer
unconditionally; a later, worse run could replace a better champion.

- A run now holds its topic lease from `score` until `on_persisted`
  (the guard moves into the pending entry). The next run of the topic
  cannot score, decide, or persist until the previous row landed.
- `auto_promote` decides under that lease against the harder of the
  caller's bar and the store's current best, and records the best it
  was decided against.
- `persist` runs a compare-and-swap before writing: the best pointer
  must still be the one the decision saw and the run must be strictly
  better than it, else the crown is refused (`promotion_refused`,
  manifest `promoted: false`, no promotion row, pointer untouched). The
  promotion row is written to the RLM store whether or not a filesystem
  artefact store is configured.
- A scored run whose row never lands releases its lease after
  `DEFAULT_LEASE_TTL` (5 min, `with_lease_ttl` for tests) so a failed
  insert cannot block a topic; the lifecycle is recovered on next score.
- e2e: a second run blocks until the first persists and then loses
  against the new best with the stale bar; an externally moved best
  refuses a decided crown; an abandoned lease is reaped.
Greptile P1 on #243: `mark_sealed` advanced `baselining → open` and
stored the supplied document as the new version without checking that
it was open, sealed, structurally valid, or signed, so an invalid draft
could become the version behind an open lifecycle.

`TopicSetup::mark_sealed(topic, pin, registered_custom, sealed)` now
refuses, in order and before anything is written or moved: a document
that is not `status: open` (`SetupError::NotOpen`); one that does not
validate as an open topic on this host — sealed baseline, registered
`custom_id`, tighten-only floors (`SetupError::Topic`); one whose
operator signature does not verify under the pin's topic key; a sealed
`BaselineMeasurement` that does not bind to the document (commitment,
holdout, image digest) or whose `custom_value` is not the primary the
RLM measured in the topic VM (`SetupError::Seal`); and a lifecycle not
at `baselining`. The version is stored, then the transition recorded.

e2e: draft, unsigned, unregistered, mis-sealed, and mismatched documents
all leave the lifecycle at `baselining` with version 1; the signed open
document sealing the measured value opens as version 2; a second seal is
illegal. Test-only `crypto` dev-dependency for the topic key.
Describe what the Greptile P1 fixes on #243 changed for operators and
miners: the run request carries `artifact_uri` and `flops_budget`, the
verdict carries the runner-measured `flops_used` (missing → 503, over
budget → reject; `declared_flops` is never the enforced figure);
promotion is decided and persisted under a per-topic lease with a
compare-and-swap on the best pointer; `mark_sealed` opens only a signed,
valid, open document sealing the RLM's measured value.
Follow-up to the Greptile P1 "artifact location discarded" on #243: the
locator now reaches the runner, but a custom submission without one gave
the runner nothing to retrieve at all — the request would still carry
`None` for a real artefact.

- `proof-http` intake: on a custom-family topic a missing / blank
  `artifact_uri` is a 400 (`artifact_uri is required for custom topics`)
  with no row and no rent; `nll` / `throughput` keep it optional (the
  digest-pinned image fetches by digest).
- `RlmScorer::evaluate` refuses a request without a locator rather than
  hand the runner one it cannot act on; `CustomRunRequest.artifact_uri`
  is never `None` for a miner submission.
- Tests: http 400 with no row and no scorer call (null and blank), the
  harvest path unaffected; e2e 400 before any VM job, every custom body
  carries a locator. Docs: required on custom topics.
Follow-up to the Greptile P1 "FLOP budget bypass" on #243: the verdict
now carries the runner's measurement against the signed budget, but the
miner's `declared_flops` (checked `<=` budget at intake) had no teeth —
a run could spend far more than it declared and still pass.

- `LiveScorer::score` / `eval_after_freeze` / `FamilyMux` carry
  `declared_flops`; `proof-http` forwards the body's value; the Lium
  harvest ignores it (its agent observes usage against the budget).
- `CustomRunRequest.declared_flops`: the runner may enforce it as a hard
  cap inside the topic VM. The operator baseline declares the budget.
- `RlmScorer::paid_verdict`: measured usage over the budget is
  `flops_over_budget`, over the declaration is the new
  `ProofCheatCode::FlopsUnderDeclared`; either is a `reject` the judge
  fails the row on. Missing usage stays a 503 (not evidence).
- e2e: declared 1, measured 2 → persisted reject with
  `flops_under_declared` only; declared the budget, measured one over →
  `flops_over_budget`; measured exactly the declared budget → crowned.
  Docs: declare what you will use, up to the budget.
@echobt

echobt commented Sep 8, 2026

Copy link
Copy Markdown
Contributor Author

@greptileai please re-review after P1 fixes: artifact locator, measured flops fail-closed, promote lease+CAS, mark_sealed signed/valid only.

@echobt

echobt commented Sep 8, 2026

Copy link
Copy Markdown
Contributor Author

@greptileai review

@echobt
echobt merged commit 04d6ebb into main Sep 8, 2026
5 checks passed
@echobt
echobt deleted the cursor/proof-generic-rlm-engine-1fd2 branch September 8, 2026 15:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants