Crucible prototype#64
Draft
ericeil wants to merge 128 commits into
Draft
Conversation
…-pipeline-refactor
…-pipeline-refactor
Implement the framework that lets AutoProver formalization backends and whole applications be written in Rust and driven by the generic Python pipeline, per docs/rust-formalization-backends.md and docs/rust-applications.md. Rust (rust/): - autoprover-sdk: the crate new apps import — the JSON ABI (descriptor, Command/Observation IoC protocol, results, verdicts), the Application / FormalizeSession traits, sync FFI helpers, and the export_app! macro that emits the PyO3 module. - example-app: the `echoprover` demo application, built into a wheel via maturin; exercises every Command variant. - Cargo workspace + maturin build infra (abi3-py312, extension-module). Python (composer/rustapp/): - The inversion-of-control effect loop: Python owns the async event loop and every effect (LLM, prover, cache, event streaming); Rust only decides the next one. No pyo3-async bridge. - Adapter implementing the real PipelineBackend / PreparedSystem / Formalizer protocols over a Rust wheel, plus RealEffects (LangGraph stream writer, model, injectable prover/feedback hooks). - Descriptor models, cacheable FormT (RustFormalResult), artifact store, phase-enum synthesis, run_rust_pipeline, build_application. Also widen ReportBackend to `| str` so a Rust app can stamp its own report tag. Tested end-to-end (tests/test_rustapp.py, 6 passing) driving the real echoprover wheel through the loop with a fake effect handler: publish, give-up, and cache-hit paths, plus descriptor/core-phase synthesis and result round-trip. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Complete the Rust application vertical: a Rust wheel now becomes a runnable application with no bespoke Python, everything synthesized from the descriptor. composer/rustapp/entry.py — descriptor-driven async entry point (rust_entry_point): argparse built from the descriptor's declared args + standard flags, precondition validation delegated to the Rust validate_preconditions hook, a neutral RAG-free env (build_neutral_env, overridable), and build_arg_parser for introspection. Service wiring (Postgres pools, thread logger, WorkflowContext) mirrors the foundry entry point. composer/rustapp/frontend.py — GenericRustApp (Textual MultiJobApp), GenericRustTaskHandler, and GenericRustConsoleHandler, all data-driven by the descriptor's event_kinds: a Rust Command::Emit becomes a custom-stream payload the handler renders if its type is declared. No per-app subclass needed. composer/rustapp/cli.py — tui_main(module) / console_main(module); an app's CLI is two lines. Imports composer.bind first (DI/tape bootstrap), like the built-in mains. host.py — run_application builds the backend from a pre-synthesized RustApplication so the frontend's phase_labels and the backend's core_phases share ONE enum object (the identity the frontend's label lookup relies on); prevents silent label misses. Tests (10 passing): descriptor-driven argparse (declared-flag defaults + override), the shared-enum identity invariant, console-handler event rendering (declared shown, undeclared ignored), and Textual app construction. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add docs/ecosystem-abstraction.md proposing a second pipeline axis — the ecosystem (blockchain/source domain) — orthogonal to the backend axis, so the shared front half (system model, analysis + property-extraction prompts, source conventions, connectivity validation) stops being silently hardwired to Solidity. Factors an ecosystem into a language facet (solidity, rust) and a chain facet (evm, solana, soroban), composed via Jinja prompt fragments. The rust language facet — Cargo fs conventions, the code_explorer prompt, and the rust failure-mode fragment (overflow, panics) — is authored once and shared by both Solana and Soroban; only each chain's model and platform failure modes differ. Notes that the sharing is not strictly hierarchical (Soroban's contract-owns-typed-storage model is closer to EVM's than to Solana's account model), motivating composable fragments over rigid inheritance. Includes the Solidity-coupling audit, the Language/Chain seam, ecosystem selection via an application parameter (built-in apps pass EVM; the rustapp AppDescriptor gains an ecosystem: ChainTag field), the behavior-preserving EVM extraction, the Solana and Soroban chain sketches, a phased plan, open questions, and key files. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Behavior-preserving refactor introducing the ecosystem seam and capturing today's
behavior as EVM = SOLIDITY ⊕ evm (see docs/ecosystem-abstraction.md §10 phase 1).
The shared front half (system analysis + property extraction) stops being silently
hardwired to Solidity; the driver defaults to EVM, so existing apps are unchanged.
New composer/pipeline/ecosystem.py:
- PromptPair, Language (source-level facet), Ecosystem (chain facet) dataclasses.
- main_instance moved here (it is EVM's locate_main); re-exported from
composer.pipeline.core so foundry/prover/rustapp importers are unaffected.
- SOLIDITY + EVM instances wiring the existing SourceApplication, prompt template
names, _validate_connectivity, and unit enumeration — a move, not a rewrite —
plus an ECOSYSTEMS registry ({"evm": EVM}).
Threaded through, all with behavior-preserving defaults:
- run_component_analysis: keyword-only system_template / initial_template / validate.
- run_property_inference: system_template / initial_template, threaded down through
_run_bug_analysis_inner -> _run_bug_round -> _get_initial_prompt.
- run_pipeline: takes ecosystem (default EVM) and drives analysis/extraction from it
(system_model, prompts, validate, analysis_extra_input, units). The one EVM
assumption kept for now — prepare_system(analyzed: SourceApplication) — is bridged
with a documented cast, removed by phase 2's App type parameter.
Verified: EVM reproduces the prior template names, validate identity, and the
verbatim analysis front-matter; no import cycle; 115 passed / 4 skipped (the 2
tree-parsing failures and rag_db errors are pre-existing env issues — missing
certoraRun CLI and DB containers — unrelated to this change). The doc's golden-run
gate needs Docker/Postgres/LLM and should be run in CI before merge.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… param Static-typing generalization (no runtime behavior change): make the analyzed application model a first-class type parameter so the backend and the ecosystem that produces its model are paired by type, and the Phase 1 cast disappears. - Ecosystem is now generic over App (composer/pipeline/ecosystem.py): system_model: type[App], locate_main: Callable[[App, ...]]; EVM: Ecosystem[SourceApplication]; registry typed dict[ChainTag, Ecosystem[Any]]. validate_analysis stays typed over BaseApplication (it narrows the produced model internally, and this keeps it assignable to run_component_analysis's validate parameter). - PipelineBackend gains the App type parameter; prepare_system(analyzed: App). - run_pipeline is [P, FormT, H, A, App] with ecosystem: Ecosystem[App] as an explicit argument; `analyzed` flows as App straight into prepare_system — the Phase 1 cast(SourceApplication, analyzed) and the unused `cast` import are removed. - The four callers (prover, foundry, both rustapp entries) pass ecosystem=EVM. - Also fix a pre-existing pyright nit in build_phase_enum (functional enum.Enum → type[Enum]). SystemAnalysisSpec is intentionally NOT parameterized — it carries only analysis_key + extra_input, no App-typed member (the analyzed type lives on the ecosystem). Verified: pyright reports 0 errors on the touched files (the pairing type-checks with no cast; one pre-existing _batch_cache_key TypeVar warning remains). Compiles, imports cleanly, unit tests green. No end-to-end gate needed — PEP 695 generics / Protocol are erased at runtime and removing a cast is a no-op, so the Phase 1 gate result stands. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…criptor
Wire ecosystem selection into the Rust application framework, so a Rust wheel
declares which ecosystem (chain) its backend targets and the host routes the
shared front half accordingly — replacing the hardcoded EVM in rustapp.
- Rust SDK (rust/autoprover-sdk): AppDescriptor gains `ecosystem: String`
(serde default "evm", so descriptors built before the field still deserialize);
echoprover sets ecosystem="evm".
- Python mirror (composer/rustapp/descriptor.py): AppDescriptor.ecosystem:
ChainTag = "evm" (ChainTag defined locally to keep the ABI-mirror decoupled
from the pipeline).
- Host (composer/rustapp/host.py): new resolve_ecosystem(descriptor) does the
ECOSYSTEMS registry lookup with a clear error for an unregistered chain;
threaded through build_application (stored on RustApplication.ecosystem),
run_application, and run_rust_pipeline. Exported from the package.
- Tests: descriptor carries + resolves ecosystem to EVM; build_application carries
the resolved ecosystem; an unregistered chain ("solana") raises; an absent field
defaults to "evm".
Only `evm` resolves today (Solana/Soroban register in phases 4-5), so behavior is
unchanged; the plumbing is now in place. Also marks phases 1-3 done in
docs/ecosystem-abstraction.md and records the solc-provisioning finding from the
phase-1 gate run.
Verified: cargo build clean, pyright 0 errors on touched Python, 14 rustapp tests pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…del/ecosystem
Foundation for the Solana chain: generalize the shared driver over ecosystem-provided
Unit/Main types, add the standalone Solana system model, and register the RUST language
facet + SOLANA chain. EVM behavior is preserved (still bound to
ContractComponentInstance/ContractInstance). Solana prompts, a sample Anchor scenario,
and the live-LLM gate follow in the next commit.
Driver generalization (composer/pipeline/core.py, system_model.py, prop_inference.py):
- FeatureUnit protocol (in system_model) captures the per-unit interface the driver needs
(display_name / slug / unit_index / cache_material / context_tag). ContractComponentInstance
implements it with byte-identical cache keys/tags, so EVM behavior is unchanged.
- Thread Unit/Main type params through Ecosystem, PreparedSystem, PipelineBackend, Formalizer,
BackendJob/ComponentOutcome/_Batch/CorePipelineResult, run_pipeline, _extract_all, and
run_property_inference. The driver now uses the protocol members instead of ContractComponent
fields.
- Relax BaseApplication's type bound from SystemComponent to BaseModel so non-EVM component
unions fit.
Solana model (composer/spec/solana/model.py): SolanaApplication (programs + authorities),
SolanaProgram / SolanaInstruction / AccountConstraint (roles + program-enforced checks) /
CpiCall — accounts-passed-in, signers, PDAs, CPIs, native fields. SolanaProgramInstance /
SolanaInstructionInstance are the Main/Unit; the instruction instance satisfies FeatureUnit.
Ecosystem (composer/pipeline/ecosystem.py): Ecosystem[App, Main, Unit]; RUST language (Cargo
forbidden_read, Rust/Solana code_explorer prompt); SOLANA chain (validate/locate_main/units,
solana/*.j2 prompt names); ECOSYSTEMS = {"evm", "solana"}. Backend annotations updated to the
new param counts; a pre-existing llm_factory(args) typing nit in the rust entry cleaned up.
Verified: pyright 0 errors on all touched files (one pre-existing _batch_cache_key warning);
84 unit tests pass; a Rust wheel with ecosystem="solana" resolves end-to-end.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Complete the Solana chain: the prompt fragments/templates, a reusable null Solana backend,
a sample Anchor scenario, and the live-LLM front-half gate.
Prompts (composer/templates), with the fragment-composition convention introduced:
- rust/_failure_modes.j2 — the SHARED Rust language failure-mode fragment (overflow-panic DoS,
unwrap/panic aborts, truncating casts, unchecked results); will be reused by Soroban (phase 5).
- solana/_failure_modes.j2 — Solana platform failure modes (missing signer/owner, account
substitution/confused-deputy, unvalidated PDA/bump, arbitrary CPI, lamport/rent & close bugs,
duplicate mutable accounts, reinit, sysvar spoofing).
- solana/{analysis_system,analysis_prompt,property_system,property_prompt,instruction_context}.j2
— Solana analysis (produces SolanaApplication) + per-instruction property extraction; the
property prompt {% include %}s the shared rust/ + solana/ failure-mode fragments.
Null backend (composer/spec/solana/null_backend.py): NullSolanaBackend over the Solana
(App, Main, Unit) triple — records extracted properties without verifying (the gate's
null/echo backend, and the reference a real Solana verifier is modeled on).
Scenario (test_scenarios/solana_vault): a small Anchor lamports-vault program + design doc.
Gate (tests/test_solana_gate.py, expensive): runs real analysis + per-instruction extraction
through SOLANA + the null backend on the vault (Postgres via testcontainers, no prover/solc).
Gate result: PASSED (3 instructions, 27 properties) with sane Solana properties — signer/owner
checks, PDA/bump canonicity, System-Program substitution, reinit-once, arithmetic overflow.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Update §10: phase 4 done — driver Unit/Main generalization (deferred from phase 2),
standalone SolanaApplication model, RUST language + SOLANA chain, the {% include %}
fragment convention (a base template proved unnecessary), a null Solana backend, and
the Anchor vault scenario. Records the live-gate result (3 instructions, 27 sane
properties). Status line now reads phases 1–4 done; 5 (Soroban) and 6 (backends) remain.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds docs/crucible-application.md: a plan to pair the existing `solana` ecosystem front half with a new Crucible (Solana fuzzer) backend, built as a Rust application on the PyO3 framework. Key decisions captured: - Crucible is the Solana analog of Foundry (authors a source-language artifact, gates it with a local CLI, refutation-oriented verdicts). - A general RunCommand effect (Rust decides argv, LLM authors only file contents) replaces the prover-specific IoC vocabulary. - Sandboxing every RunCommand is a required, definition-of-done phase (bwrap on Linux; macOS dev mechanism TBD), because the LLM-authored harness runs as native code (verified against Crucible source: cargo build + Command::new, LiteSVM only sandboxes the program under test, no isolation in-tree). - Version compatibility (Crucible/Solana-Anchor/Rust) and a shared Solana build pipeline reused across backends (Crucible no-munge; a future Prover backend munge-and-rebuild). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds §7.5: how the harness author gets Crucible documentation, designed for the large-corpus future (a Certora Prover/CVLR Solana backend) rather than Crucible's small doc set. - Build tool-enabled call_llm now as a shared rustapp capability (host-assembled tool belt: backend RAG search over the descriptor's ComposerRAGDB + learned-KB + source tools), so CVLR-Solana reuses it with zero framework change. Fixes the gap that today's IoC call_llm is a tool-less single ainvoke. - Knowledge rides the backend axis (per-wheel rag_db_default), not the ecosystem axis; static injection of a harness cheat-sheet is a Crucible content shortcut layered on top. - Wire the knowledge seam into Phase 3; add open question on one-DB-vs-multiple for CVLR; add key-files rows for the RAG precedent + new crucible_kb builder. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A backend-agnostic "run a local command over a set of files" effect, replacing
the prover-specific shape for CLI-gated backends (Crucible, cargo build-sbf,
anchor idl). The Rust decider authors program+args; only file contents may be
LLM-derived.
- SDK: Command::RunCommand { program, args, files } + Observation::CommandResult.
- composer/rustapp/command.py: run_local_command — the single command choke point
(write files into a confined workdir, exec-not-shell, timeout, optional
semaphore, capture). This is what phase-6 sandboxing (docs §7.4) will wrap.
- loop.py / adapter.py: Effects.run_command, drive_session branch, and
RealEffects.run_command with a lazily-created per-formalize workdir.
- tests/test_rustapp_command.py: round-trip, path confinement, no-shell-injection,
missing binary, non-zero exit, timeout.
No sandbox yet (network-off/clean-env/resource-caps is phase 6); run only on
trusted input for now.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The RustBackend adapter was hardwired to the EVM types (SourceApplication /
ContractInstance / ContractComponentInstance / main_instance), so a non-EVM
(e.g. solana) wheel could not run. Generalize it over the resolved ecosystem:
- FeatureUnit gains feature_json() — the generic way to marshal a unit's
semantic content across the FFI. EVM returns the component model_dump
(byte-identical to before); Solana returns {program, instruction}.
- RustBackend holds the resolved ecosystem and uses ecosystem.locate_main
instead of main_instance; prepare_system/formalize/to_artifact_id/finalize
now work through FeatureUnit (feature_json / slug / display_name) rather than
EVM-specific attrs.
- host.build_backend / build_application thread the ecosystem in.
EVM path preserved (echoprover + rustapp tests green, pyright clean).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A new Solana verification application (ecosystem="solana") backed by the Crucible fuzzer. Phase 1 provides the declarative descriptor + a real validate_preconditions; the authoring loop is a deliberate stub (phase 1 covers preconditions + build/IDL + dry-run infra, no LLM). - rust/crucible-app: descriptor (phases incl. a UI-only Build Harness phase; args --crucible-version/--fuzz-timeout/--fuzz-cores/--stateful; rag_db_default "crucible_kb"; fuzz-flavored event kinds; provisional artifact layout) + validate_preconditions (crucible/cargo-build-sbf/anchor on PATH via a pure PATH scan, plus a buildable Cargo workspace check) + refutation-oriented backend_guidance. - Added to the rust workspace; maturin added as a dev dependency to build the wheel. Verified: cargo check clean; `maturin develop` builds+installs the wheel; the host loads the descriptor, resolves ecosystem -> solana/rust, and validate_preconditions reports the missing workspace correctly. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Completes Crucible phase 1: the build + dry-run infrastructure, gated end to end. - composer/spec/solana/build.py: build_program — the shared Solana build capability (source -> target/deploy/<program>.so [+ optional IDL]) routed through the same run_local_command choke point the RunCommand effect uses. Crucible calls it in no-munge mode; a future Prover/CVLR backend calls it in munge-and-rebuild mode. - test_scenarios/solana_vault: made a buildable Cargo workspace (Cargo.toml, programs/vault/Cargo.toml, rust-toolchain.toml) and fixed the program so it compiles against anchor-lang 1.0.1 (valid declare_id, invoke-based system transfer, renamed the #[program] module to vault_program to avoid a crate/module name clash in the harness). Build outputs + the generated fuzz harness are gitignored. - tests/test_crucible_gate.py (expensive): loads the descriptor (ecosystem -> solana), runs validate_preconditions, builds vault.so, materializes a trivial hand-written fuzz harness (crate deps resolved from CRUCIBLE_REPO), and asserts `crucible run vault invariant_vault --dry-run` exits 0. No LLM, no authoring. Gate verified green locally (cargo-build-sbf + crucible on PATH, CRUCIBLE_REPO=~/src/crucible). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Implements §7.1: a Crucible deliverable is one Cargo crate (single [[bin]] invariant_test) assembled from a shared fixture + one test fn per component, not one file per component. Phase 1 revealed crucible's CLI hardcodes the bin name, so per-component bins are a dead end; components are selected by a Cargo feature whose name equals the test fn (Crucible's #[invariant_test] macro self-gates main() by #[cfg(feature = "<fn name>")]). - composer/crucible/harness.py: CrucibleHarness assembler (renders Cargo.toml feature list + src/main.rs = shared fixture + verbatim per-component test fns) and CrucibleDep (resolves crucible/solana/anchor deps from a local checkout, §6.1). - composer/crucible/store.py: CrucibleArtifactStore — per-component write_artifact folds the test fn into the crate and re-renders it under fuzz/<program>/, while the shared base writes metadata under certora/crucible/ (the same split Foundry uses). - tests/test_crucible_gate.py: phase-2 gate — a hand-authored fixture + one test written through the store assembles a crate that `crucible run vault c_deposit --dry-run` accepts; metadata lands under certora/crucible/ and a co-located EVM certora/specs/ deliverable is untouched. Gate verified green. - docs §7.1 corrected to the verified macro-self-gating mechanism (the earlier #[cfg]-wrapped-section description was wrong; the gate caught it). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
RealEffects.call_llm now runs a bounded, tool-enabled agent turn (env.all_tools: source navigation + RAG search + learned-KB, plus a result tool) via run_to_completion, instead of a bare single ainvoke. This is the shared framework change §7.5 calls for: the harness author can pull in framework docs / read the program mid-turn, and a large-corpus backend (CVLR-Solana) reuses it by shipping only a knowledge DB — no framework change. echoprover unit tests use a fake Effects (not RealEffects) so are unaffected; real validation is the phase-3 authoring gate. (The pyright NotRequired/MessagesState warning on the new state class is the pre-existing langgraph-stub false positive that also affects composer/spec/code_explorer.py.) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The fuzz budget now travels as a descriptor-declared arg injected into the component context (declared_args -> AuthorInput.context['fuzz_timeout']), so the dedicated fuzz_timeout_s field/param on BackendOptions, RustBackend, RustFormalizer, and build_application was dead. Removed. Backends read the value via the context. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…stinction explicit Audit confirmed the two axes are already cleanly separated in code — the analyzed program's language rides on the ecosystem (Ecosystem.language), and the entry point derives forbidden_read from app.ecosystem.language, never from the fact that the backend is a Rust wheel; no implementation-language notion is attached to the ecosystem or descriptor. echoprover (a Rust-implemented backend analyzing Solidity/EVM) proves the independence. This only tightens the prose so a reader can't conflate them: - Ecosystem.Language docstring + module docstring: state the facet is the language of the *code under analysis*, explicitly not the backend's implementation language. - composer/rustapp docstring: state that "Rust" there is the *backend implementation* language, orthogonal to the ecosystem, and that the host reads the analyzed-source language from app.ecosystem.language rather than assuming it. No behavioral change (docstrings only); pyright clean, imports OK. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…FeatureUnit) The shared core hardcoded ContractComponentInstance as a stand-in for "the unit", so the ecosystem-generic driver had to cast every non-EVM unit to the EVM type — a cast that was actually a lie for Solana (a SolanaInvariantUnit is not a ContractComponentInstance; it only worked because the generic path touches no EVM-specific members). Parametrize BackendJob / ComponentOutcome / _Batch / Formalizer / PreparedSystem / PipelineBackend over `U: FeatureUnit` and let each backend declare its concrete unit: CVL & Foundry → ContractComponentInstance (their finalize now reads .slugified_name / .component cast-free), Rust & null-Solana → FeatureUnit (null's to_artifact_id widened from the too-narrow SolanaInstructionInstance). CorePipelineResult stays mono in the unit (it only reads .display_name). The ecosystem is invariant in its Unit typevar and callers pass a concrete chain, so it can't be tied to the backend's U at the signature; extraction therefore yields FeatureUnit batches and run_pipeline reunites them with the backend's U via one honest widening cast. Net: two clearly-scoped boundary casts (extraction→U, outcomes→FeatureUnit for the rollup) replace the misleading per-unit ContractComponentInstance casts, and the whole core is now honestly typed. pyright clean across all 19 pipeline-type consumers; 246 non-expensive tests pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…_all The two strategies are duals — global extraction infers once over the whole program and fans each property into its own unit (one prop per batch), while per-component infers per unit (concurrently) and keeps that unit's properties grouped — but they shared context-building, inference, and batch construction with the code duplicated across both branches. Factor the shared machinery into `_unit_ctx` (derive a unit's context) and `_extract` (run inference), and reduce both strategies to a list of (unit, properties, context) triples materialized by one shared `_Batch` comprehension. The branch now expresses only the genuine difference (fan direction); no behavior change (per-component still reuses its extraction context — no redundant `.child`; global still builds a fresh per-property context). pyright clean; 246 non-expensive tests pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Self-contained command-sandbox mechanism, extracted from eric/crucible as the
first of the 4 stacked PRs (docs/pr-split-plan.md). Both sides ship together so
a parallel Python project can adopt it immediately:
from composer.sandbox import run_local_command, rust_build_policy, ...
Python (composer/sandbox/, stdlib-only, no composer deps outside itself):
- policy.py tool-agnostic seam: SandboxPolicy / SandboxProvider / LaunchSpec,
the `none` passthrough, the provider registry, fail-closed helpers
- command.py run_local_command — the RunCommand primitive (materialize input
files into a workdir, run a command there); the LLM controls only
file contents, never the command line
- launcher.py the `launcher` provider: maps a policy to a run-confined invocation
($RUN_CONFINED_BIN -> PATH -> repo build dir)
- recipes.py rust_build_policy + DEFAULT_ENV_PASSTHROUGH (the shared "compile/run
Rust" confinement recipe, usable by Rust and future Python backends)
- config.py SandboxConfig
Rust (rust/run-confined): the trusted launcher — Landlock filesystem + seccomp
network/ptrace + rlimits + scrubbed env, then execve. Workspace root trimmed to
members = ["run-confined"] (the pyo3 framework crates rejoin in later PRs);
Cargo.lock regenerated for that subset.
Packaging: scripts/Dockerfile builds run-confined into the image on PATH;
.dockerignore + .gitignore exclude the Rust build output.
Docs: docs/command-sandbox.md.
Gate: tests/test_sandbox{,_command,_config,_launcher,_run_confined,_escape}.py
— 42 passed (escape gate: all vectors denied + unconfined control; run-confined
confinement exercised, not skipped).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the raw landlock_create_ruleset syscall in --probe with the same BestEffort ruleset negotiation apply_landlock uses, inspecting RulesetStatus. Drops the unsafe block and the hand-rolled LANDLOCK_CREATE_RULESET_VERSION constant; the crate deliberately hides the numeric ABI, so probe now reports the enforcement status instead. Python's available() only checks the exit code and stderr, so the stdout change is safe. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The launcher's --probe no longer reports the numeric Landlock ABI (the crate hides it); it builds a best-effort ruleset and reports whether Landlock actually enforces. Update §7 and §9 step 2 to match, following the switch of probe() to the crate's public API. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…up apps` Previously the crucible_app / echoprover PyO3 wheels were installed out-of-band (`maturin develop`), so every `uv sync` pruned them as extraneous and you had to rebuild by hand each time. Declare them as editable path dependencies in a dedicated `apps` group so `uv sync --group apps` builds them via maturin AND preserves them (no more pruning). The group sits outside the default groups, so the container image (final stage has no Rust toolchain; syncs with UV_NO_DEV=1 --group ragbuild) never tries to compile them — verified the image's sync line excludes the apps packages, and `uv lock --check` stays green for its --frozen build. Also add `maturin-import-hook` to the group: after a one-time `python -m maturin_import_hook site install`, editing a crate transparently recompiles it on import (with the venv activated so maturin is on PATH), so the manual `maturin develop` step is gone entirely. docs/crucible-demo.md updated: the "uv sync prunes the wheels" gotcha and the manual rebuild steps are replaced with the --group apps + import-hook flow. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Give the Crucible backend a `judge_prompt` (peer of Foundry's feedback judge),
so an authored test suite is reviewed for *meaningfulness* before it counts —
not just that it compiles. Modeled on Foundry's 8-criteria prompt but retargeted
to LiteSVM fuzzing: the load-bearing axis is reachability (can the fuzzer, via the
fixture's `action_*` methods, drive a state where the invariant could fail?).
SDK:
- `FailureKind` (Compile | Judge) on `Failure`, serde-defaulted to Compile, so a
re-author can tell a build failure from a judge rejection.
crucible-app (the wheel):
- `judge_prompt` for `kind="component"` (skipped for the shared fixture): a
reviewer persona + criteria, emitting the `{accept, feedback}` JSON the host's
`_parse_judge` consumes; a rejection re-authors with the feedback framed as
review notes, NOT compiler errors (`judge_revise_suffix` vs `revise_suffix`,
dispatched on `FailureKind`).
- `judge` event kind so the verdict surfaces in the TUI/console.
adapter (the host):
- tag judge rejections `kind="judge"`; label the judge's LLM turn distinctly
("<app> judge turn", was mislabeled "authoring turn"); emit the verdict.
Two authoring fixes found via the e2e run (solana_vault, real model):
- TEST_CHEAT_SHEET: the real raw-lamports API (`read_account`/`get_account`
return `Result<Account>` — unwrap first; there is NO `get_account_lamports`),
which the model kept mis-guessing and burning re-authors on; plus a
transaction-fee caveat (don't assert an exact lamport delta on a signer/fee
payer — the ~5000-lamport fee makes it a false oracle).
- the judge's Criterion 7 gets the same real-execution-costs principle, to catch
that false-oracle class if the author writes it anyway.
Verified: `test_crucible_e2e_gate` passes with the judge live (13/13 delivered,
judge fired + reasoned per component, reject/re-author path exercised, 0 give-ups);
unit tests in test_crucible_events cover the judge prompt, the setup skip, and the
judge-vs-compile revise framing.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…vability) A refuted property surfaced as a bare `BAD` with no clue why — `validate` classified a `[FUZZ_FINDING]` and discarded Crucible's `summary:<msg>` (the failed `fuzz_assert_*` message with the actual vs expected values). Capture it and make it durable. - SDK: `Verdict.detail` — optional explanation of a non-GOOD outcome. - crucible-app `validate`: `finding_detail()` parses `[FUZZ_FINDING] crash:<id> … summary:<msg>` into the BAD verdict; the ERROR paths carry their error text in `detail` too (and stop overloading `unit_file`, which is a report identity key). - report: thread it through as `Verdict.message` → `RuleVerdict.message`, so it persists in report.json; render it on the rule row (`.finding`) in the HTML. Additive and defaulted to None — prover/foundry are unchanged. - adapter: `fetch_verdicts` maps `detail → message`; the live `verdict` event line appends the detail. Now the `deposit_increases_balance_exactly` BAD from the e2e would read, in report.json and the rendered report, "crash <id>: deposit(...) must decrease depositor lamports by exactly that amount: expected … but got …" instead of a bare BAD. Tests: verdict `detail → report message` threading (test_crucible_events) and the finding message rendering on the row (test_autoprove_report); report suite green (prover/foundry unaffected). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… turn The judge was a stateless host-driven turn fired after every author attempt, which re-explored the program from scratch and ~5×'d the e2e (docs/crucible-judge-cost.md). Move it into the author loop, matching CVL/Foundry's `feedback_tool` shape (docs/crucible-judge-in-loop.md): the author calls a `request_review` tool, self- revises against the feedback in one session, and a `bind_standard` gate blocks `result` until it submits a judge-accepted draft. Author and judge share the run memory, so facts persist across components instead of being re-derived. Purely host-side — the wheel API is unchanged. The judge runs in-loop whenever the wheel supplies one, detected by probing the pure `judge_prompt` callout (it returns None exactly when there is no judge for the input); no descriptor flag. The separate `_judge_turn` host branch is gone (still used inside the in-loop hook and the judgeless setup path). `echoprover` (no judge) keeps the single-shot author. e2e (solana_vault, real model) vs the host-driven run: 1:33:20 (was 1:53:04); 13/13 GOOD (was 12 GOOD + 1 BAD — the fee-oracle test now correct, BAD→GOOD, with the judge observed reasoning about the LiteSVM fee payer); author turns 23→14; build-fail re-authors 3→0 (the earlier get_account_lamports cheat-sheet fix). The review sub-agent still explores, so the wall-clock win is modest — a follow-up clamps its tool belt/recursion for the rest. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The in-loop review sub-agent still re-explored the program (17 nested `code_explorer` calls in the last e2e), which dominated its cost — each `code_explorer` is itself a multi-call sub-agent. It doesn't need it: the judge prompt already carries the program API + fixture and the review shares the run memory, so direct `get_file`/`grep` spot-checks suffice. `run_llm_agent` gains an `exclude_tools` filter; `_judge_turn` drops `code_explorer` (`_JUDGE_EXCLUDE_TOOLS`). The author keeps the full belt. Phase 1 of docs/crucible-judge-cost.md (tool clamp + memory continuity); the recursion cap and cadence tweaks (Phase 2) remain open. Behavior/verdict impact to be confirmed on the next e2e. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Crucible generated one fuzz harness per invariant and ran each separately (N builds + N campaigns, serialized). The fuzz *engine* was already whole-program (one shared fixture, explore mode) — only the authored test, run, and verdict were sharded (docs/crucible-unit-granularity.md §2). Collapse them: one harness fn (`c_invariants`) asserting every invariant, one build, one fuzz run. Driver: `Ecosystem.collapse_units` (set on SOLANA) keeps all invariants in ONE batch on the whole-program unit (`_extract_all`), so `formalize` is called once with every property. Wheel: crucible-app authors a single `c_invariants` fn (each assertion tagged `[<title>]`); `units()` returns a report row per property (`c_<slug>`) that all share one fuzz `target` (SDK `Unit.target`, new). Attribution stays per-property AND lives in the backend (not the host — see the translation-layer cleanup below): `validate` runs the shared target once and returns a `Verdict` per covered unit (`ValidateOutcome::Verdicts`), pinning a counterexample to the property its finding names and holding the rest GOOD. Translation-layer cleanup of composer.rustapp: the host no longer computes verdicts or parses a backend's finding format — it runs each declared `target` once and records the backend's per-unit verdicts verbatim (dropped `_attribute_run`). Also generalized comments/docstrings in the generic host + SDK so they don't assume fuzzing (Crucible stays a labeled example). Measured: the whole vault (13 invariants) e2e ran in 0:59:22 — one build + one campaign vs 13 — down from 1:33:20 (per-invariant in-loop) and 1:53:04 (original). NOTE: that run predates the per-unit-verdict `validate` contract change; the new contract is unit-tested (test_crucible_events / test_crucible_granularity) but not yet re-exercised end-to-end. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…importer Replace the Crucible-specific RAG builder with a common JSON manifest format and one corpus-agnostic importer, and ship Crucible's corpus as a checked-in artifact so no crucible checkout is needed at build or run time. - composer/rag/import_format.py: the manifest models (RagManifest/Section/Block), free of any RAG-stack imports so a producer needs only these to emit a corpus. - composer/scripts/rag_import.py: the generic importer. Loads/validates a manifest, resolves knowledge_base -> connection (composer/rag/db.py KNOWLEDGE_BASES, overridable by --output), and always feeds both indexes (vector via add_chunks_batch + manual via add_manual_section) with shared BlockBuilder chunking and per-output part-numbering. - rust/crucible-app/crucible_kb.rag.json: the Crucible corpus, generated once from the crucible repo docs/ (126 sections), committed alongside the app. - Container/demo import the committed manifest: the Dockerfile copies it to $AUTOPROVE_HOME and autoprove-entrypoint.sh's setup-db runs rag_import into the crucible_rag schema (alongside the CVL rag_db build); crucible-demo.md 1g imports the same file. Previously the crucible RAG was never built in the container. - Remove the Crucible-specific scripts (crucible_ragbuild.py, populate_crucible_rag.sh); composer ships only the generic importer + schema. Regeneration recipe recorded in docs/rag-import-format.md. Foundry and CVL builders are untouched. No runtime code changes: search tools, rag_env.py, and the DB API are unchanged — this only changes how the Crucible corpus gets in. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the large inline string consts and `format!` literals in crucible-app's
lib.rs with askama templates under `templates/*.j2` — the same `.j2` convention
used by composer/templates/*.j2, here on the Rust side. Templates are checked at
build time and embedded in the wheel (no runtime loading or packaging).
- 15 templates: the static prose (backend guidance, cheat-sheets, worked example,
judge system/guidance, probe fn), the prompt-assembly (author setup/component,
judge instruction, the two revise suffixes), the crate files (Cargo.toml + deps),
and the api-facts block (rendered from a prepared struct via a {% for %} loop).
- lib.rs: string consts / format! bodies -> `#[derive(Template)]` structs + render;
JSON-shaping logic (api_facts extraction) stays in Rust and feeds the template.
`escape = "none"` (prompts/Rust/TOML, not HTML); askama.toml pins whitespace.
- LLM-prompt prose is line-wrapped to 120 with a tag-aware wrapper (never splits a
{{…}}/{%…%} tag). The code-bearing cheat-sheets/example, the api-facts listing,
and the byte-exact crate files are left with their own line structure.
Verified: the crate files (Cargo.toml/deps) render BYTE-IDENTICAL to the former
format! output (parity test keeps the old code as the oracle); static templates
preserve their bytes; author/judge/revise prompts render end-to-end with no
template residue. cargo test + clippy clean; a fresh maturin wheel renders the
wrapped prompts; the Python crucible tests pass; run-confined still builds.
Note: maturin-import-hook watches the crate's Rust sources, not templates/, so after
editing a .j2 run `maturin develop` (or touch src/lib.rs) to force a rebuild; any
real cargo/maturin/CI build reads the current templates (askama rerun-if-changed).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Deny io_uring and non-AF_UNIX sockets in seccomp, install Landlock scopes (signals/abstract UDS) and TCP default-deny, grant only shared cargo bin/ (not credentials.toml), and extend the escape suite.
The seccomp deny-list keyed on exact x86_64 syscall numbers was bypassable via the x32 ABI: an x32 call runs under the same AUDIT_ARCH_X86_64 identity (so seccompiler's arch guard passes it), but its syscall number is OR'd with __X32_SYSCALL_BIT (0x4000_0000), so it misses every exact-number rule and hits default-allow — a full bypass of the socket/io_uring/ptrace/process_vm denies. This matters most on kernels < 6.7 (the AL2023 6.1 target), where Landlock does no network filtering and seccomp is the sole network control, so the bypass is unconditional egress to the network + IMDS. Fix: mirror every deny onto its x32-tagged syscall number in apply_seccomp (x86_64 only; aarch64 has no per-syscall compat bit). Deriving the mirror from the already-built rule map means any future deny is covered automatically. Regression test records the errno of an x32 socket() attempt and asserts EPERM (seccomp caught it) rather than ENOSYS (kernel rejected it after the filter let it through) — a real guard even on x32-disabled CI kernels. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Landlock + seccomp are kernel-mediated and a container shares the host kernel, so ubuntu-latest cannot validate run-confined on the 6.1 kernel we ship on. This job boots the real Amazon Linux 2023 kernel-6.1 KVM cloud image under QEMU/KVM (SHA256-verified, UEFI/OVMF, cloud-init SSH seed) and runs tests/test_sandbox_escape.py inside it. On 6.1 the suite exercises the 6.1 contract specifically: Landlock FS enforced, network seccomp-only (no Landlock net < 6.7), scopes absent (< 6.12, so the signal / abstract-UDS vectors self-skip), and the x32 deny-mirror asserted regardless. The guest is kept dependency-light: the suite is stdlib + pytest only, so we install pytest via uv on PYTHONPATH and pass --noconftest to skip tests/conftest.py's heavy imports (its fixtures are all in-module). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The "copy repo into the VM" step tar'd the whole workspace, which by that point holds the running VM's al2023.qcow2 (QEMU actively writing it) plus seed.iso / serial.log / repo.tgz — tar aborts with "file changed as we read it". Use git archive HEAD instead: a clean snapshot of tracked source, which is all the escape suite needs and structurally cannot include the runtime artifacts. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
actions/checkout@v4 and upload-artifact@v4 run on the deprecated node20 runtime, which emits a deprecation warning now that the runner defaults to node24. Bump both to v5 (first major on node24) to clear it; our usage (plain checkout, path-list upload) is unaffected by the majors' breaking changes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ressions Negative control for the sandbox-escape (kernel 6.1) workflow. Disables the seccomp socket() network block so socket(AF_INET/AF_NETLINK/AF_VSOCK,…) succeeds, breaking the "No network" guarantee. The escape suite must go RED (inet_sock / netlink / vsock / net_ext / imds leak) — proving the CI fails when the sandbox is broken, not just when it works. REVERT THIS COMMIT before merging. Verified locally: run-confined builds and tests/test_sandbox_escape.py fails on the network probes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ches regressions" This reverts commit 6f965c1.
The base image baked the run-confined command-sandbox launcher onto PATH unconditionally (Dockerfile stage 1b + a COPY into /usr/local/bin). Not every AutoProver container needs the sandbox, so pull it out of the base image and provide it as a runtime-mounted, opt-in building block instead. - scripts/Dockerfile: drop stage 1b and the run-confined COPY; leave a pointer to the overlay. - scripts/Dockerfile.sandbox: builds run-confined in a throwaway Rust stage and publishes the binary into a mounted volume via a minimal bookworm-slim image. - scripts/docker-compose.sandbox.yml: overlay with a one-shot run-confined-build service that populates a run_confined named volume, plus the wiring onto the autoprove service (read-only mount + RUN_CONFINED_BIN, gated behind the builder via service_completed_successfully). Consumers that don't add this overlay get a sandbox-free image. - docs/command-sandbox.md: update the §9 note — the launcher is resolved via $RUN_CONFINED_BIN (overlay-mounted) rather than baked into the image. The launcher provider already resolves $RUN_CONFINED_BIN ahead of PATH (composer/sandbox/launcher.py), so no code change is needed. Verified end-to-end: the publisher lands a 0755 ELF in the volume and `run-confined --probe` reports `landlock FullyEnforced` from a read-only mount. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bring the command-sandbox hardening + the opt-in Docker overlay from eric/sandbox onto the Crucible branch. Crucible's run-confined was the original pre-hardening baseline (its main.rs etc. were byte-identical to the sandbox PR-1 commit), so the security fixes merge cleanly as "take theirs": - run-confined: close the io_uring network bypass and the x32-ABI seccomp bypass (unconditional egress on kernels < 6.7, incl. the AL2023 6.1 target), Landlock scopes + TCP default-deny, grant only shared cargo bin/. Plus the recipes.py / command-sandbox.md / escape-suite updates that go with them. - CI: the sandbox-escape-6.1.yml workflow + provision script. Docker: the run-confined launcher is no longer baked into the base image. Consumers opt in via the new scripts/docker-compose.sandbox.yml overlay (builds scripts/Dockerfile.sandbox, mounts the binary at $RUN_CONFINED_BIN). Crucible's containerized path — whose default provider is fail-closed — must add that overlay. Conflict resolution notes: - rust/Cargo.toml: kept crucible's (workspace members autoprover-sdk / crucible-app / example-app; sandbox never changed this file from base). - rust/Cargo.lock: dropped the incoming file — crucible gitignores it and the incoming lock covers only run-confined, not the app members. - scripts/Dockerfile: applied the overlay change (dropped stage 1b + the run-confined COPY) while keeping crucible's crucible_kb.rag.json COPY. Verified: cargo build -p run-confined --release succeeds and `--probe` reports landlock FullyEnforced. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The base image no longer bundles run-confined (it moved to the scripts/docker-compose.sandbox.yml overlay). Document the binary-resolution order for the host demo (dev fallback still works unchanged) and tell containerized Crucible runs to add the overlay, since the launcher provider is fail-closed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a Crucible toolchain image + compose overlay so `console-crucible` and the crucible test gates run entirely in-container, and make the gates container-aware. Toolchain image (Option 1 / one blessed combo — docs/crucible-toolchain-versioning.md): - scripts/Dockerfile.crucible layers on the lean base and bakes rustc 1.89.0 (the scenario's harness toolchain) + a newer CLI toolchain (anchor-cli needs rustc >= 1.91), Solana/agave platform-tools 3.1.10, anchor-cli 1.1.2, and the public crucible v0.2.0 checkout + CLI, plus the crucible_app + test dep groups. - The toolchain installs into the runtime $HOME (/opt/autoprove/home) — the exact layout the RunCommand sandbox recipe grants (RUSTUP_HOME/CARGO_HOME + ~/.local/share/solana + ~/.cache/solana), so the confined build can exec it. A build-time sBPF build of the bundled scenario pre-installs platform-tools so the offline/non-root run-time build finds them present. - scripts/docker-compose.crucible.yml swaps the autoprove service onto this image; it stacks on the sandbox overlay (Crucible is fail-closed, so run-confined is mounted from there). Wiring: - autoprove-entrypoint.sh: console-crucible/tui-crucible fail-fast if the toolchain or run-confined is missing (RAG DSN derives from PG env; no --rag-db needed). - docker-compose.sandbox.yml: drop the profile gate on run-confined-build — a depends_on a profiled service is "undefined" to `compose build`/`config` unless that profile is also passed, which broke the single-profile crucible build. Tests runnable in-container: - conftest: pg_container targets an existing postgres via $COMPOSER_TEST_PG_URL (the compose service) instead of testcontainers — no docker-in-docker. - test_crucible_e2e_gate / test_crucible_sandbox_gate: build a writable copy of the scenario (the in-image copy is read-only for the non-root user; also stops host runs polluting test_scenarios/), and the e2e provisions roles/DBs idempotently against the persistent compose postgres. - docs/crucible-demo.md §8: the full containerized run + in-container e2e recipe. Verified: base + crucible images build from scratch; the sandbox gate (cargo-build-sbf under run-confined, offline) passes in-container on the clean image and on the host. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The package-data glob `templates/*.j2` only matched the top level, so the wheel omitted composer/templates/solana/*.j2 and templates/rust/*.j2 (and the report's prism-cvl.js). Any run whose CWD isn't the source tree loaded templates from the installed wheel and hit `TemplateNotFound: 'solana/analysis_system.j2'` — which is exactly the containerized `console-crucible` (CWD=/work). The source-tree runs (pytest inserts the repo root on sys.path) masked it. Recurse into the subdirs and include the JS asset. Verified: the built wheel now contains solana/*.j2 (7), rust/*.j2, and prism-cvl.js; console-crucible in the container gets through analysis + property extraction. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The confined Rust build failed on a read-only shared RUSTUP_HOME:
error: could not create temp file /…/.rustup/tmp/…: Permission denied (os error 13)
Even with a fully pre-installed toolchain, the rustup proxy (cargo/rustc/
cargo-build-sbf) writes scratch into $RUSTUP_HOME/tmp on every invocation, and the
recipe granted RUSTUP_HOME read-only — so the containerized harness build (whose
image bakes a shared, read-only toolchain) died on every setup attempt, and Crucible
gave up "did not pass compile/judge in 7 attempts". Host dev flows masked it: there
~/.rustup is writable.
Fix mirrors the existing private per-run CARGO_HOME (sandbox_rustup_home): point
RUSTUP_HOME at <workdir>/.sandbox_rustup, symlink `toolchains` back to the shared
home (still granted read-only, so toolchain bytes are shared not copied) and keep
tmp/downloads/update-hashes as this run's writable scratch. `settings.toml` is copied
so default/override resolution still works. No new shared-writable grant, so the
untrusted-build isolation the escape suite guards is unchanged.
Also exclude `.sandbox_rustup/` from RUST_FORBIDDEN_READ — its `toolchains` symlink
would otherwise make the source tools enumerate the whole toolchain into the prompt.
Verified: the confined sbf build (a rustup proxy) passes on the host under the new
per-run home.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…s it)
The in-container e2e reached the harness setup and failed every attempt with, in
the offline confined build:
error: could not download file from '…/channel-rust-stable.toml.sha256'
to '…/.sandbox_rustup/tmp/…': dns error (offline sandbox)
Root cause: crucible-fuzz-cli pins the harness build to the `stable` toolchain — it
writes a `rust-toolchain.toml` with `channel = "stable"` into the fuzz crate and sets
`RUSTUP_TOOLCHAIN=stable` (crates/crucible-fuzz-cli/src/lib.rs). The image installed
1.89.0 (scenario) + a newer CLI toolchain but NOT `stable`, so the confined, offline
build tried to rustup-download it and failed. Host dev flows always have a `stable`
toolchain, which is why only the baked image hit this.
Fix: preinstall `stable` alongside the others so the offline build finds it present.
(The per-run RUSTUP_HOME from the previous commit remains correct defense-in-depth —
rustup needs writable scratch against the read-only baked toolchain — but installing
`stable` is what actually unblocks the harness build.)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.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.
No description provided.