feat(proof): one Firecracker experiment VM per paid job with a generic in-guest runner - #253
Merged
Merged
Conversation
Generic in-guest experiment binding read from the signed topic's constraints.params (runner id, pinned pack digest, optional size), held under configurable per-VM ceilings (16 vCPU / 32 GiB RAM / 32 GiB disk), created per Baseline/Evaluate job and destroyed after it. Host stages the pack over vsock and attests the run as experiment_vm; agent caps how many run at once. No benchmark, pack, or runner is named in git. Co-authored-by: Mathis <echobt@users.noreply.github.com>
…er adaptors
Transport-agnostic guest agent (vsock or stdio) for the RLM / experiment
microVM: hello, tmpfs secret staging, pack verify + unpack, and every job
routed to <runners_dir>/<runner id>/{run,inspect,propose_rules} under a
documented env contract. No runner, adaptor, pack, verifying artefact,
report, or finite value = RlmToHost::Failed; secrets are redacted from
every tail and evidence document. Nothing benchmark-specific in git.
Co-authored-by: Mathis <echobt@users.noreply.github.com>
… adaptor, experiment env knobs deploy/guest/bake-rootfs.sh builds the enlarged guest image (Debian minbase, rootless podman + crun + fuse-overlayfs, optional pinned Harbor CLI, the guest agent as PID-1 service, operator adaptors under /opt/proof/runners), enforces a tree size budget, and names the image after the sha256 it computed with RE-LOCK steps. Adaptor contract + honest rootless-podman limits in deploy/guest/runners/README.md; the Harbor-over-podman adaptor is an example operator capability driven entirely by topic params. Env examples gain the experiment ceilings and pack dir. Co-authored-by: Mathis <echobt@users.noreply.github.com>
…ock steps New runbook docs/runbooks/proof-experiment-vms.md (generic params, lock ceilings 16 vCPU / 32 GiB / 32 GiB disk, pack staging, guest image bake and size budget, RE-LOCK on metal, fail-closed rows, stated limitations); pointers from the orchestrator runbook, PROOF.md bindings + isolation boundary, root/deploy/docs AGENTS, ARCHITECTURE, COMPLETENESS, and the miner guide. Wire check validates the experiment ceilings and image pin. Co-authored-by: Mathis <echobt@users.noreply.github.com>
Co-authored-by: Mathis <echobt@users.noreply.github.com>
Co-authored-by: Mathis <echobt@users.noreply.github.com>
Default per-experiment VM 4 vCPU / 8 GiB; a topic may override up to 8 vCPU / 16 GiB; writable disk >= 16 GiB (32 GiB default). Ceilings gain operator defaults (PROOF_EXPERIMENT_VM_VCPUS / _MEM_MIB) beside the maxima; the disk floor is enforced on both sides; docs, env examples, and the wire check follow. baseline_runner=<operator adaptor id> is the canonical example. Co-authored-by: Mathis <echobt@users.noreply.github.com>
echobt
marked this pull request as ready for review
September 9, 2026 13:39
DEFAULT_EXPERIMENT_VCPUS=16, DEFAULT_EXPERIMENT_MEM_MIB=32768, DEFAULT_MAX_EXPERIMENT_VCPUS=16, DEFAULT_MAX_EXPERIMENT_MEM_MIB=32768: a silent topic gets the lock and no topic may ask for more (less is fine). Disk stays 32 GiB default / 16 GiB floor. Docs, AGENTS, runbooks, env examples, and the wire check say 16/32 everywhere; still generalist. Co-authored-by: Mathis <echobt@users.noreply.github.com>
Contributor
Author
|
@greptileai please re-review after Mathis 16vCPU/32GiB caps bump (40bd4bb). |
Greptile SummarySummary
Merge SafetySafe to merge. No blocking issues remain. Confidence Score: 5/5Safe to merge; no outstanding blocking issues remain. No outstanding findings remain. Reviews (4): Last reviewed commit: "fix(proof-experiment): an unset default ..." | Re-trigger Greptile |
… destroyed run_paid_job returned the job outcome after a teardown that answered Ok(false) or failed, logging a warning only — a result could be scored while the dedicated experiment VM still held host capacity. The outcome is now returned only when the orchestrator confirms the destroy; anything else is VmError::TeardownUnconfirmed (503, no row, no baseline), naming the VM and, when the job itself also failed, both causes. VmBackedRunner::evaluate and TopicSetup::baseline both route through it. The fake orchestrator can inject Ok(false) / Err teardowns and keeps the VM alive as a host would. Co-authored-by: Mathis <echobt@users.noreply.github.com>
ExperimentCeilings::validate accepted max_vcpus up to 64 and max_mem_mib up to 131072, so an operator ceiling above the Mathis lock sized and admitted 64-vCPU / 128 GiB experiment VMs on both sides. LOCK_MAX_EXPERIMENT_VCPUS / LOCK_MAX_EXPERIMENT_MEM_MIB (16 / 32768) are now compiled-in maxima: validate refuses any ceiling above them (AboveLock — the CP and the KVM agent refuse to boot), shape() and admit() refuse any VmShape above them whatever the ceilings say, and a smaller host may only lower a ceiling. DEFAULT_* and DEFAULT_MAX_* equal the lock. The disk ceiling stays an operator knob. The wire check fails (not warns) on a knob above the lock; env examples and orchestrator flags say so. Co-authored-by: Mathis <echobt@users.noreply.github.com>
download() buffered the whole body with resp.bytes() before comparing its length to MAX_ARTIFACT_BYTES, so a server omitting Content-Length could stream an arbitrarily large body into the experiment VM's memory before the cap fired. The body is now read chunk by chunk and the fetch aborts (and drops the connection) the moment it would exceed the cap; an honest Content-Length over the cap is still refused before the first body byte. Test: a chunked, endless loopback body is cut at the cap promptly; chunked bodies at and under the cap arrive whole. Co-authored-by: Mathis <echobt@users.noreply.github.com>
…ut while draining Two distinct signed constraints.params names that normalise to the same PROOF_PARAM_* variable (foo-bar / foo_bar) silently overwrote each other in the adaptor environment. param_env now refuses the collision, naming both keys and the variable, before run / inspect / propose_rules exec anything. stdout and stderr were read to completion into unbounded vectors, with the 64 KiB tail applied only after exit, and sequentially (a full stderr pipe could stall a run until its deadline). Both streams are now drained concurrently into rolling Tail buffers of STREAM_TAIL_BYTES each while the process runs, so a flooding adaptor costs the guest at most MAX_TAIL_BYTES; the run log carries a marker for what was cut. Co-authored-by: Mathis <echobt@users.noreply.github.com>
…rbor harness for generic hooks storage.conf pointed runroot / graphroot at /run/containers/storage and /var/lib/containers/storage — root-owned, the latter on the read-only rootfs — while init.sh only made /run/user/<uid> and /var/lib/proof writable for the runner, so rootless podman could not initialise its store. The bake now writes runroot = /run/user/<uid>/containers and graphroot = rootless_storage_path = /var/lib/proof/containers/storage, and init.sh creates and chowns both before the agent starts. deploy/guest/runners/harbor-podman (run + summarize.py) named and drove one benchmark harness and scored its trial format — challenge content in git, and its summariser substituted the first unrelated rewards value when 'reward' was missing. Both files are removed. runners/ now ships the adaptor contract and a fail-closed skeleton only; the bake's --with-harbor / --harbor-version give way to generic --extra-pkgs / --overlay / --chroot-hook so operators bake their own harness tooling outside git. Tests: dry-run plans the hooks and refuses malformed ones, the storage paths match what init makes writable, and nothing under deploy/guest names a harness or a benchmark. Co-authored-by: Mathis <echobt@users.noreply.github.com>
…unbook and agents docs Runbook, root / deploy AGENTS.md, PROOF.md, COMPLETENESS.md, the orchestrator runbook, and the miner guide now state the lock as a hard maximum (a ceiling above it does not boot), that a paid run scores only once its experiment VM is confirmed destroyed, that param names must stay distinct as PROOF_PARAM_* variables, that the artefact fetch is streamed under the 64 MiB cap, and that deploy/guest/runners ships the adaptor contract and a skeleton only — the harness, adaptor, and scoring rule are operator artefacts baked with the generic hooks, never in git. Example runner ids no longer name a harness. Co-authored-by: Mathis <echobt@users.noreply.github.com>
Contributor
Author
|
@greptileai please re-review after Greptile P1 fixes (ec1defa). |
ExperimentPolicy::from_env kept the lock default (16 / 32768) when an operator lowered only PROOF_EXPERIMENT_VM_MAX_VCPUS or _MAX_MEM_MIB, so validate rejected default_vcpus / default_mem_mib and the control plane could not start on a smaller host. Ceilings are now read first and an unset default is clamped to its ceiling, as the KVM host config already does; a default the operator set explicitly is still validated as written and refused above its ceiling, never silently clamped. The reader is a pure from_lookup over any name -> value source so the vCPU-only, memory-only, disk-only, explicit-over-ceiling, above-lock, and image-pin cases are tested without touching the process env. Co-authored-by: Mathis <echobt@users.noreply.github.com>
Contributor
Author
8 tasks
echobt
added a commit
that referenced
this pull request
Sep 9, 2026
* deploy: staging pins for 4252130 images.yml built GHCR digests on main then could not push the pin commit (GH013: PRs required + Greptile). Land the same promote via PR. Co-authored-by: Mathis <echobt@users.noreply.github.com> * fix(deploy): keep full prior staging release in previous Sequential promote.sh calls were snapshotting previous per service, so rollback of 4252130 would restore a mixed pin set. Capture the 1dd07f7 release once and write it back after the promote loop. Co-authored-by: Mathis <echobt@users.noreply.github.com> * deploy: staging pins for f013abb Retarget #245: main moved to #246. Digests from images run 34281980177. previous stays the full 1dd07f7 staging release. Co-authored-by: Mathis <echobt@users.noreply.github.com> * deploy: staging pins for 890ce5b Retarget #245: main moved to #247. Digests from images run 34286333214. previous stays the full 1dd07f7 staging release. Co-authored-by: Mathis <echobt@users.noreply.github.com> * deploy: staging pins for c0ce094 Retarget #245: main moved through #248/#252. Digests from images run 34297422117. previous stays the full 1dd07f7 staging release. Co-authored-by: Mathis <echobt@users.noreply.github.com> * deploy: staging pins for 85ab2b7 Retarget #245: main moved to #251. Digests from images run 34299348722. previous stays the full 1dd07f7 staging release. Co-authored-by: Mathis <echobt@users.noreply.github.com> * deploy: staging pins for e30d51d Retarget #245: main moved to #250. Digests from images run 34302039784. previous stays the full 1dd07f7 staging release. Co-authored-by: Mathis <echobt@users.noreply.github.com> * deploy: staging pins for a8b9018 Retarget #245: main moved to #253. Digests from images run 34373475123. previous stays the full 1dd07f7 staging release. Co-authored-by: Mathis <echobt@users.noreply.github.com> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Mathis <echobt@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Architecte RELOCK C, generalist form: topics whose signed
constraints.paramsselect an in-guest runner get one dedicated Firecracker microVM perBaseline/Evaluatejob — created for the job, sized by the topic's ask under a hard operator lock, fed the topic-pinned experiment pack over vsock, run by an operator adaptor baked into the guest image, attested by the host asexperiment_vm, destroyed afterwards — and scored only once the orchestrator confirms that VM destroyed. Parallel experiments are parallel VMs, never containers sharing one VM. Path B (vsock → host harness) is not implemented. Rootless podman inside the guest is the accepted C (not Docker-in-VM).Nothing benchmark-, dataset-, harness-, adaptor-, or pack-specific is compiled in or committed. The words that select the path are generic knobs whose values are topic data:
constraints.paramsbaseline_runner(synonymin_guest_benchmark_runner)/opt/proof/runners/<id>/(e.g.operator_adaptor_v0) — the adaptor itself lives outside gitexperiment_pack_digest(+ optionalexperiment_pack_path)sha256:of the pack tar the KVM host stages; required with a runner, never defaultedexperiment_vcpus/experiment_mem_mib/experiment_disk_mibmodel_pin, every other paramPROOF_MODEL_PIN/PROOF_PARAM_<KEY>(two signed names that collide after upper-casing /-→_are refused before anything runs)Resource caps (Mathis LOCK — hard):
DEFAULT_EXPERIMENT_VCPUS=16,DEFAULT_EXPERIMENT_MEM_MIB=32768(what a silent topic gets),DEFAULT_MAX_EXPERIMENT_VCPUS=16,DEFAULT_MAX_EXPERIMENT_MEM_MIB=32768(the most a topic may ask for), andLOCK_MAX_EXPERIMENT_VCPUS=16/LOCK_MAX_EXPERIMENT_MEM_MIB=32768as compiled-in maxima:ExperimentCeilings::validaterefuses any CP (PROOF_EXPERIMENT_VM_MAX_*) or host (PROOF_VM_AGENT_EXPERIMENT_MAX_*) ceiling above them so the process does not boot, andshape()/admit()refuse any VM shape above them whatever the ceilings say. A smaller host may only lower a ceiling — and lowering only theMAXknob is enough: an unset default follows it (a default set above its ceiling is refused, never clamped);PROOF_EXPERIMENT_VM_DISK_MIB), 16 GiB floor, max raised by the operator when the metal has more (the disk ceiling is not locked);PROOF_VM_AGENT_MAX_EXPERIMENT_VMS(default 2) caps how many run at once; the topic VM keeps its locked 4 / 8192;What changed
crates/proof-experiment(new): the binding parser (ExperimentBinding::from_params, fail-closed on a half selection),ExperimentCeilings(defaults + maxima + hard lock + disk floor) /VmShape::locked/ExperimentPolicy::from_env(an unset default follows a lowered ceiling; a set default above its ceiling is refused), wireExperimentSpec.proof-vm-proto:HostToRlm::StagePack/RlmToHost::PackStaged(≤ 160 MiB),GuestMode { sister | experiment_vm }on the attestation (serde default = sister, wire-compatible),VmRecord.experiment, healthexperiment_vms/max_experiment_vms,ErrorCode::Capacity(503).proof-rlm:TopicVmSpec.experiment,run_paid_job(create → run → destroy → return the outcome only on a confirmed destroy;Ok(false)or a teardown error isVmError::TeardownUnconfirmed— 503, no row, no baseline — even after a successful run, naming the VM),VmBackedRunner::with_experiments;TopicSetup::baselineroutes through it (the baseline is the first experiment VM). Fake orchestrator keeps experiment VMs out ofattach, records teardowns, and can inject unconfirmed / failed teardowns.proof-vm-agent: experiment VMs bypass the one-per-topic rule, never answerattach, count against the host capacity, ids<topic>-x<n>; the fake hypervisor attestsexperiment_vm.crates/proof-fc-experiment(new, host layer overFirecrackerHypervisor): ceilings (under the lock) → pack resolved underPROOF_VM_AGENT_EXPERIMENT_PACK_DIRand re-hashed withverify_artifactbefore any jail → boot withdisk_mibscratch →StagePackover vsock (destroy on failure) → job bound to the VM's runner + pack →experiment_vmattestation for that VM and job.proof-fc-hostchanges are 3 lines (scratch size from the spec, attestation mode).proof-vm-fc/bins/proof-challenge: policy from env, an experiment attestation must name the VM the job was dispatched to; orchestrator binary gains the--experiment-*/--max-experiment-vmsflags (a vCPU / memory ceiling above the lock refuses to boot) and wraps the backend.crates/proof-vm-guest+bins/proof-vm-guest-agent(new): the generic guest agent (vsock:5000viatokio-vsock, or--stdio). Hello, tmpfs secrets (chowned to the run-as user), pack verify + unpack, artefact fetch streamed under a hard 64 MiB cap (cut mid-body, never buffered first), adaptor exec contract (run/inspect/propose_rules, documentedPROOF_*env,report.json/checklist.json/rules.json), param-name collision refusal before exec, stdout / stderr drained concurrently into rolling bounded tails while the process runs, deadline kill of the process group, secret redaction in every tail and evidence string. No default value ever: no runner / adaptor / pack / verifying artefact / report / finite value →RlmToHost::Failed→ 502 → 503, no row.ProposeRuleswithout an adaptor entrypoint echoes the signed topic's own checklist.deploy/guest/:bake-rootfs.sh(Debian minbase + rootless podman/crun/fuse-overlayfs/pasta +catatonit; the podman store on run-as-owned pathsinit.shcreates —graphroot = /var/lib/proof/containers/storageon the scratch drive,runroot = /run/user/<uid>/containers— never the root-owned/var/lib/containers//run/containers; generic operator hooks--extra-pkgs/--overlay/--chroot-hookfor harness tooling;--check-kernel-config; tree budget 2560 MiB / image 3072 MiB; image named after its ownsha256sum; RE-LOCK steps printed;--dry-runwithout root),init.sh/agent-loop.sh(PID 1, scratch on/dev/vdb, cgroup v2, run-as user, both podman paths pre-created and chowned),runners/README.md(adaptor contract + a fail-closed skeleton + honest rootless-podman limits). No adaptor ships in git — the harness CLI, agent, task format, and trial →primary_valuerule are operator artefacts baked outside the repo and selected by signed params; a test grepsdeploy/guest/for harness names.docs/runbooks/proof-experiment-vms.md(params, caps incl. the hard lock, flow incl. confirmed destroy, bake + size budget, RE-LOCK on metal, fail-closed rows incl. the new ones, limitations); pointers in the orchestrator runbook,PROOF.md, root / deploy / docsAGENTS.md,ARCHITECTURE.md,COMPLETENESS.md, miner guide; env examples; wire check fails a ceiling knob above the lock, validates integers, the 16 GiB disk floor, the image pin.Greptile findings addressed (head
85e26c69)proof-rlm/src/vm.rs)run_paid_jobreturns the outcome only onteardown → Ok(true);Ok(false)/Err→VmError::TeardownUnconfirmed(503, no row, no baseline), naming the VM and any job failure too. Tests inproof-rlm(evaluate + baseline, injectedOk(false)andErr) andproof-rlm-scorer(setup with an unconfirmed destroy seals no baseline).bake-rootfs.sh)runroot = /run/user/<uid>/containers,graphroot = rootless_storage_path = /var/lib/proof/containers/storage;init.shcreates and chowns both before the agent starts. Verified in a private mount namespace: uid 1000 can write both, cannot create the old paths. Test asserts the bake's paths are what init makes writable.fetch.rs)download_cappedreadsresp.chunk()incrementally and aborts the moment the cap would be exceeded; honestContent-Lengthover the cap refused before the body. Test: endless chunked body withoutContent-Lengthis cut promptly; bodies at / under the cap arrive whole.proof-experiment)LOCK_MAX_EXPERIMENT_*compiled in;validaterejectsmax_vcpus > 16/max_mem_mib > 32768(AboveLock),shape/admitreject any such shape; orchestrator refuses--experiment-max-vcpus 32etc.; wire check fails such knobs. Tests on both sides.runners/harbor-podman/*)--with-harbor/--harbor-versionreplaced by generic--extra-pkgs/--overlay/--chroot-hook; README ships the contract + a fail-closed skeleton only and states a trial without a measurement is never scored from another value. Test grepsdeploy/guest/for harness names.runner.rs)param_envrefuses two signed names mapping to onePROOF_PARAM_*(names both + the variable) beforerun/inspect/propose_rulesexec. Test: control (foo-baralone maps) + collision (adaptor never runs) on all three paths.proof-experiment, second review)ExperimentPolicy::from_envreads theMAXknobs first; an unset default follows a lowered ceiling (host parity), a default set above its ceiling is still refused, the lock still holds. Purefrom_lookup+ test over the vCPU-only / memory-only / disk-only configurations.runner.rs)Tails ofSTREAM_TAIL_BYTESeach while the process runs (≤MAX_TAIL_BYTEStotal, marker for what was cut). Test: 8 MiB floods on both streams complete under the deadline with a bounded, marked tail.Honest limits (also in the runbook)
CONFIG_USER_NS, overlayfs, fuse, veth, tun, cgroups, seccomp… — a stock microVM config lacks several (--check-kernel-confignames them).flops_usedon this path is guest-agent-authored (fromreport.json, relayed by the host); an agentic harness has no FLOP counter, so the topic setsflops_budget: 0or supplies an accounting param — never invented.inspectentrypoint (topic-RLM work); without one such a topic cannot reachEvaluate.g-8vcpu-32gbbeside the 4/8 topic VM — the dedicated production host must carry it; the colocated staging droplet lowers the caps on both sides (below the lock is allowed) or keeps the count at 1 with a smaller topic ask.init.sh, and the bake's dry run. No digest is invented anywhere; the pack digest and runner id are topic data staged outside git.Greptile
@greptileai reviewTest plan
cargo test --workspace(all green; new:proof-rlmwithheld-unless-destroyed,proof-rlm-scorerbaseline withheld on unconfirmed destroy,proof-experimenthard lock + lowered-ceiling-only policy,proof-vm-orchestratorabove-lock flags refused,proof-fc-experimentabove-lock shape refused,proof-vm-guestparam collision / output flood / streamed fetch cap / bake hooks / podman paths / no-harness grep)cargo fmt --all -- --checkcargo clippy --workspace --all-targets -- -D warnings,cargo deny check,xtask loc-cap/consensus-lint/spec-check/design-check/external-docs-check, dcap clippyRisk
Additive wire changes (
serde(default)everywhere; an old agent given an experiment spec boots a topic VM, stages no pack, attests nothing → the CP refuses the run, 503). No emission, signature-domain, or miner CVM measurement impact. New host env knobs default to the 16 / 32 lock and 2 concurrent experiment VMs and refuse to boot above the lock; size the KVM host formax_experiment_vms × 16 vCPU / 32 GiB / 32 GiB diskon top of the 4/8 topic VM. A host that fails to destroy an experiment VM now surfaces as 503s on that topic (with the VM named) instead of silently leaking capacity.Naming
I did not rename
BASE_*environment variables, deployed host paths (/opt/base,/run/base, …), GHCRbaseintelligence/basepackage names, orbase-*-v1cryptographic domain tags.