Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
86 commits
Select commit Hold shift + click to select a range
dacae15
report: consume findings instead of producing them
ericeil Aug 18, 2026
797646c
rustapp: submit the run's findings to the report
ericeil Aug 18, 2026
7b00603
docs: record the findings seam as built
ericeil Aug 18, 2026
902cf23
rustapp: name a finding by the same rule its verdict row uses
ericeil Aug 18, 2026
bb2a586
docs: say the findings story in simpler language
ericeil Aug 18, 2026
080c62c
PR3: Crucible backend (capstone)
ericeil Aug 11, 2026
6403dad
PR3 follow-up: make the Crucible app actually loadable
ericeil Aug 3, 2026
a5abd2c
crucible: make a counterexample triageable from the report alone
ericeil Aug 4, 2026
f47d05a
crucible: seal each component's tests behind its own feature
ericeil Aug 4, 2026
ed9a819
crucible: catch the gates up to master's plugin phase + prompt renderer
ericeil Aug 4, 2026
1e4330e
tests: pass the resolved provider service to standard_connections
ericeil Aug 4, 2026
3d2ec15
docs: record section isolation, and why the deliverable gate is not b…
ericeil Aug 4, 2026
73a0510
crucible: drop PluginManager.without_plugins, keep the change in the …
ericeil Aug 4, 2026
3585b5e
docs: the JSON RAG mechanism is PR 2's; Crucible is its first adopter
ericeil Aug 4, 2026
90d609e
crucible: port to the reworked SDK API
ericeil Aug 5, 2026
149d020
solana: implement the project seam, and read it from the wheel
ericeil Aug 6, 2026
c1ea86e
crucible: author in a session, and speak its own noun
ericeil Aug 6, 2026
535ae15
crucible: catch up to the SDK's judge split, phase model and emitter
ericeil Aug 7, 2026
22966ef
crucible: let the host state the verdict protocol, and say so once
ericeil Aug 7, 2026
7e86aec
crucible: one section per file, and a crate root written once
ericeil Aug 7, 2026
f4051b5
crucible: absorb the file scaffolding an author writes into a section
ericeil Aug 7, 2026
26f4eb6
crucible: a negative action reports success, because rejection is it …
ericeil Aug 7, 2026
3d7d81a
docs: propose a fix for cross-component finding attribution
ericeil Aug 7, 2026
270e680
crucible: a finding that names another component's property is not th…
ericeil Aug 7, 2026
fdff455
crucible: the fixture attempts and records, the component asserts
ericeil Aug 7, 2026
c5446dc
crucible: triage a crash from what ran, not from which test case it was
ericeil Aug 7, 2026
ffcf4b2
crucible: the gate that ships the crate is the one that builds it
ericeil Aug 10, 2026
13eef1e
docs: propose a shared fix for concurrent spec writes
ericeil Aug 10, 2026
260b6ee
crucible: the harness crate and the sandbox scratch move under certora
ericeil Aug 10, 2026
2171c54
crucible: the wheel splits into a module per part of the seam
ericeil Aug 10, 2026
d98f226
crucible: point the CLI at the harness crate absolutely
ericeil Aug 10, 2026
c21d1c9
crucible: a finding the run produced reaches the report
ericeil Aug 11, 2026
c846eec
crucible: the run says what it found and how hard it looked
ericeil Aug 18, 2026
143bc0f
crucible: tell the property author what the checker can see
ericeil Aug 11, 2026
3613798
crucible: every verdict says how far its campaign got
ericeil Aug 11, 2026
fa3546d
crucible: an absent optional account is the program id, not None
ericeil Aug 11, 2026
205041d
test: an e2e scenario on the IDL path, where the real targets are
ericeil Aug 11, 2026
0817eee
prep: the IDL path needs the program warmed and anchor executable
ericeil Aug 12, 2026
7f86c26
test: correct what the committed lock protects
ericeil Aug 12, 2026
1837c18
crucible: the author declares the checks, grouped into one campaign
ericeil Aug 18, 2026
3bdbc52
crucible: an invariant covers one property, because a tag does
ericeil Aug 12, 2026
83d83d5
crucible: a verdict names the section it came from
ericeil Aug 12, 2026
711c34b
docs: why a clean campaign is not a passing check
ericeil Aug 12, 2026
9780c06
crucible: a panic on fuzzed input costs the run, so say so where it i…
ericeil Aug 13, 2026
cbca3f8
docs: the shared fixture is the one artifact nothing reviews
ericeil Aug 13, 2026
5ba64af
crucible: a campaign's own output belongs where a run's other scratch…
ericeil Aug 13, 2026
676a5b9
crucible: absorb the Callout deliverable_path rename
ericeil Aug 13, 2026
2ca0202
crucible: move the committed KB manifest to the dual-product shape
ericeil Aug 13, 2026
63708c9
docs: the counting macro does not have to be crucible's
ericeil Aug 13, 2026
90fea80
Absorb master catch-up API changes in crucible tests
ericeil Aug 14, 2026
f32b4ec
docs: a count is for the green row, the gate only needs nonzero
ericeil Aug 14, 2026
9721bec
crucible: a clean campaign now has to say what it evaluated
ericeil Aug 14, 2026
5321ea8
sandbox: keep INTERNAL_DIR importable without pydantic
ericeil Aug 14, 2026
2d7f7b0
Absorb extra_context in the crucible granularity test
ericeil Aug 17, 2026
d8cc5f2
docs: the Crucible application has one current-state design note
ericeil Aug 17, 2026
86a9fac
test: keep only the assertions a regression would actually trip
ericeil Aug 18, 2026
8498362
Absorb master's callout error envelope in the crucible tests
ericeil Aug 18, 2026
7ffa750
tests: restore the cross-references that only resolve on this branch
ericeil Aug 18, 2026
8d2a39d
tests: pass the plugin tool binder at the crucible formalize call sites
ericeil Aug 18, 2026
c8beea3
crucible: settle the findings seam this branch rebased onto
ericeil Aug 18, 2026
cbe62dd
report: put findings synthesis back where master has it
ericeil Aug 19, 2026
0f3383c
report: let the backend say what a finding is made of
ericeil Aug 19, 2026
0494c04
crucible: write a campaign's violations up as findings
ericeil Aug 19, 2026
2b998b1
crucible: a verdict's evidence and its accounting are separate fields
ericeil Aug 19, 2026
8eca62a
report: rows that share one finding are written up once
ericeil Aug 19, 2026
36a7d24
crucible: the wheel declares what its findings rest on
ericeil Aug 19, 2026
3cec8c3
findings: one evidence shape for every backend
ericeil Aug 19, 2026
33e6288
findings: the synthesis is a record, not a table of hooks
ericeil Aug 19, 2026
ea596ab
findings: name the record for what it is
ericeil Aug 19, 2026
22ebc8c
findings: every backend's severity is assessed
ericeil Aug 19, 2026
31682c9
findings: one system-prompt contract for every backend
ericeil Aug 19, 2026
821d0b2
findings: name the write-up prompt for what it is
ericeil Aug 19, 2026
13dc9d9
findings: keep RuleEvidence and EvidenceFetcher in collect
ericeil Aug 19, 2026
24f4309
session: the mapping a tool builds is the mapping state accepts
ericeil Aug 19, 2026
d2bdf4d
gates: repair the expensive Solana and Crucible gates
ericeil Aug 19, 2026
55e1915
solana_vault_idl: pin the Solana release anchor builds the IDL under
ericeil Aug 19, 2026
6a7553b
session: write the mapping into state as the type state declares
ericeil Aug 20, 2026
790ae75
llm: stream Anthropic calls and give requests a real timeout
chandrakananandi Aug 19, 2026
1ff76e9
verdicts: bound the evidence a prompt carries
ericeil Aug 20, 2026
1666a1a
rustapp: give the Rust backend the retry floor the CVL one has
ericeil Aug 20, 2026
7c4b2c0
report: a finding names its evidence in the backend's own noun
ericeil Aug 20, 2026
5f49d9a
report: crucible's unit is the check, not the property
ericeil Aug 20, 2026
5094220
docs: propose per-check fuzz targets
ericeil Aug 20, 2026
8ea04e4
docs: one grain, and Stakes in place of Exploration
ericeil Aug 20, 2026
9faeeec
crucible: one fuzz target per check
ericeil Aug 20, 2026
6e12a19
rustapp: one PropertyCheckMapping, now that a family param keeps its …
ericeil Aug 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,8 @@ five steps and never inspects anything backend-specific:
depends on the other, so the prover's expensive AutoSetup/summary/invariant work overlaps
with per-unit property inference. Property extraction fans out one agent per *unit* —
`ecosystem.units(main)` — one per **component** of the main contract/program in both
ecosystems — bounded by a semaphore (`--max-concurrent`).
ecosystems ([docs/crucible.md](docs/crucible.md) §2) — bounded by a semaphore
(`--max-concurrent`).
4. **Per-unit formalization** (parallel). For each unit's properties, the backend's
`Formalizer.formalize()` is invoked. Results are cached by the backend's result type.
5. **Report assembly** (shared, best-effort). The driver collects per-unit verdicts via a
Expand Down Expand Up @@ -257,8 +258,9 @@ the authoring agent. A callback protocol streams per-rule outcomes to the UI and
Rust/Solana targets: a `SolanaApplication` analysis model, Solana-specific analysis/property
prompts ([templates/solana/](composer/templates/solana/)), and per-component `units()` — one
unit per `ProgramComponent`, the Solana analog of EVM's `ContractComponent`
([docs/ecosystem-abstraction.md](docs/ecosystem-abstraction.md) §4). It plugs into the same
driver via the `SOLANA` ecosystem; the matching verification backend (Crucible) lands separately.
([docs/crucible.md](docs/crucible.md) §2). It plugs into the same
driver via the `SOLANA` ecosystem; the matching verification backend is
[Crucible](docs/crucible.md).
- **NatSpec** ([composer/spec/natspec/](composer/spec/natspec/)) — a *greenfield* workflow
(its own asyncio orchestrator, not the generic driver) that goes from a design doc to Solidity
interfaces, stub implementations, and CVL. A semaphore-serialized "semantic registry"
Expand Down
22 changes: 22 additions & 0 deletions composer/crucible_launch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
"""Console entry points for the Crucible (Solana fuzzing) application.

Crucible is now a *pure-Rust app* (``docs/rust-applications.md``): the ``crucible_app`` wheel + its
descriptor define everything (the shared fixture, crate deliverable, workspace prep, sandbox
grants, verdict summary), so these are the same two-line shims echoprover uses — no
Crucible-specific Python package. ``import composer.bind`` runs first (inside
``composer.rustapp.cli``) for the import-time DI / test-tape bootstrap.
"""

from composer.rustapp.cli import console_main, tui_main

_MODULE = "crucible_app"


def console_crucible() -> int:
"""Run the Crucible application in console mode."""
return console_main(_MODULE)


def tui_crucible() -> int:
"""Run the Crucible application in the Textual TUI."""
return tui_main(_MODULE)
22 changes: 22 additions & 0 deletions composer/layout.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
"""Project-root directory names shared across backends.

These two paths are the pair every surface agrees on: :data:`CERTORA_DIR` is
what a user keeps, :data:`INTERNAL_DIR` is what a user ignores. They live here
— not in :mod:`composer.spec.gen_types` — because :mod:`composer.sandbox` must
import :data:`INTERNAL_DIR` without pulling pydantic. The 6.1 escape suite
collects ``composer.sandbox`` in a guest that has only pytest and
``annotated-types``.
"""

from pathlib import Path

#: Deliverable layout under the project root (specs, confs, reports).
CERTORA_DIR = Path("certora")

#: Everything generated that is NOT a deliverable: diagnostics, scratch, and
#: the build/fuzz outputs a run accumulates. A project that ignores this
#: directory ignores all of it, and every source surface withholds it whole
#: (``fs_forbidden_read``, ``RUST_FORBIDDEN_READ``). Spelled once because a
#: subdirectory that grows without bound is only safe while it is *inside*
#: the directory those rules name.
INTERNAL_DIR = Path(".certora_internal")
22 changes: 19 additions & 3 deletions composer/llm/anthropic.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from functools import cache

import anthropic
import httpx

from composer.input.files import UploaderBase, ContentRenderer
from composer.input.types import ModelConfiguration
Expand Down Expand Up @@ -225,8 +226,14 @@ def should_retry(self, exc: Exception) -> bool:
and every 5xx, which covers 529 overloaded) plus connection-level
failures (``APITimeoutError`` subclasses ``APIConnectionError``).
400-class request errors are deterministic — an over-long prompt fails
identically on every attempt — and are deliberately excluded."""
if isinstance(exc, anthropic.APIConnectionError):
identically on every attempt — and are deliberately excluded.

``httpx.TimeoutException`` is here because we stream: the SDK's own retries and its
exception wrapping both cover the *request*, and once it has handed the response stream to
the caller a stall surfaces raw from the transport, as ``httpx.ReadTimeout``. It means the
same thing as an ``APITimeoutError`` — the provider went quiet — and nothing has been
produced that a retry would duplicate."""
if isinstance(exc, (anthropic.APIConnectionError, httpx.TimeoutException)):
return True
if isinstance(exc, anthropic.APIStatusError):
return exc.status_code in (408, 409, 429) or exc.status_code >= 500
Expand Down Expand Up @@ -293,7 +300,16 @@ def builder_for(
return ChatAnthropic(
model_name=self.model_name,
max_tokens_to_sample=opts.tokens,
timeout=None,
# An explicit None DISABLES the SDK's timeouts (None != not-given), so a
# socket that dies silently mid-stream hangs the session forever. A float
# is a per-phase httpx timeout — for a streamed response, the max silence
# between chunks, not a cap on the whole turn.
timeout=300.0,
# Stream every request: a long authoring turn (Opus + thinking on a large
# prompt) can exceed the SDK's 600s non-streaming ceiling, and a silent
# 10-minute wait is long enough for NAT/idle killers to drop the socket
# (surfaces as APIConnectionError mid-run). Streaming keeps bytes flowing.
streaming=True,
max_retries=8,
stop=None,
betas=betas,
Expand Down
26 changes: 16 additions & 10 deletions composer/pipeline/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@
from composer.llm.types import CacheLevel
from composer.input.files import Document
from composer.spec.source.report.build import build_report
from composer.spec.source.report.collect import ReportComponentInput, Verdict, EvidenceFetcher, Formalized
from composer.spec.source.report.collect import Formalized, ReportComponentInput, Verdict
from composer.spec.source.report.findings import FindingsPolicy
from composer.spec.source.report.schema import (
AutoProverReport, RuleName, ReportBackend, SourceEditRecord, VerificationArtifactRecord,
)
Expand Down Expand Up @@ -204,10 +205,16 @@ async def fetch_verdicts(self, formalized: Formalized[FormT]) -> dict[RuleName,
Never called for gave-up or budget-curtailed components."""
...

def findings_evidence(self) -> EvidenceFetcher | None:
"""The per-rule evidence source for findings synthesis, or None if this backend produces no
findings. Returning None is how a backend opts out — the report then builds no findings for
it, with no backend-specific branching in the report layer. Default: None."""
def findings_policy(
self, outcomes: list[ComponentOutcome[FormT, U]]
) -> FindingsPolicy | None:
"""How this backend writes its violated rules up as findings, or None if it produces none.

Returning None is how a backend opts out — the report then builds no findings for it, with
no backend-specific branching in the report layer. Default: None.

Takes ``outcomes`` because a backend whose evidence is in its own results has nowhere else
to read it from; one whose evidence is a run-scoped store ignores them."""
return None

async def finalize(self, outcomes: list[ComponentOutcome[FormT, U]], run: PipelineRun) -> None:
Expand Down Expand Up @@ -695,18 +702,17 @@ async def _run(batch: _Batch[U]) -> ComponentOutcome[FormT, U]:
for o in outcomes
for pa in o.artifacts
]
findings_evidence = formalizer.findings_evidence()
findings = formalizer.findings_policy(outcomes)
try:
async def _report() -> AutoProverReport:
return await build_report(
contract_name=source.contract_name, backend=formalizer.backend_tag,
components=inputs, llm=run.env.llm_lite(), fetch_verdicts=formalizer.fetch_verdicts,
source_edits=await formalizer.source_edits(outcomes, run),
verification_artifacts=artifact_records,
# Findings only when the backend supplies evidence — skip the heavy model otherwise.
findings_llm=run.env.llm_heavy() if findings_evidence else None,
fetch_evidence=findings_evidence,

# Findings only when the backend declared a policy — skip the heavy model otherwise.
findings_llm=run.env.llm_heavy() if findings else None,
findings=findings,
)
report = await run.runner(
job=_report,
Expand Down
36 changes: 29 additions & 7 deletions composer/pipeline/ecosystem.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@
Solana model + prompts and reuses the shared ``RUST`` language facet. See ``docs/ecosystem-abstraction.md``.
"""

import re
from dataclasses import dataclass
from pathlib import PurePath
from typing import Any, Callable, Collection, Literal, Mapping, TypedDict

from composer.spec.gen_types import INTERNAL_DIR
from composer.spec.context import SourceCode
from composer.spec.code_explorer import CodeExplorerPromptParams
from composer.spec.gen_types import TypedTemplate
Expand Down Expand Up @@ -250,13 +252,33 @@ def _evm_analysis_extra_input(source: SourceCode) -> list[str | dict]:
#: Cargo/Anchor project layout: hide build output, VCS, lockfiles, and the JS side; keep the
#: crate sources and `tests/`. A pattern suffices here — unlike the Foundry-shaped
#: ``fs_forbidden_read``, nothing needs carving back out of an excluded directory.
RUST_FORBIDDEN_READ = r"(^target/.*)|(^\.git.*)|(^node_modules/.*)|(.*\.lock$)"
# NOTE: the confined-build scratch dirs (``.sandbox_cargo`` / ``.sandbox_rustup`` /
# ``.sandbox_tmp`` and nested ``target/``) also have to be excluded — a build fills them with
# hundreds of MB the source tools' file-listing would pull into the model's context — but that
# extension lives with the *backend* that runs confined Rust builds inside the workdir. Nothing in
# the front half, and nothing in the Rust application framework itself, creates them: a Rust
# backend need not build a crate to validate the program, nor use the sandbox at all.
#:
#: The second group covers the hundreds of MB of build/scratch **this backend** generates inside the
#: workdir mid-run (nothing in the front half or the Rust framework creates them), which the source
#: tools' file-listing would otherwise pull into LLM context and blow the model's window:
#: • ``INTERNAL_DIR`` — the whole of it, which is what that directory is *for*: the sandbox's
#: per-run scratch (a private ``CARGO_HOME`` holding the entire cargo registry, ~19.5k files /
#: ~520 MB; a private ``RUSTUP_HOME`` whose ``toolchains`` symlinks the shared rustup home, so a
#: naive listing enumerates the whole Rust toolchain), the run's logs, and a backend's own
#: accumulating outputs. Named as a whole rather than subdirectory-by-subdirectory because the
#: next thing to grow without bound is the one nobody remembered to add: a klend run put 48k
#: files into a fuzzer corpus/crashes pair sitting outside this rule and lost 7 of 15 components
#: to prompts of 1.6–4.7M tokens against a 1M limit. This matches ``fs_forbidden_read``, which
#: has always withheld the directory whole for Solidity.
#: • nested ``target/`` — cargo build output below the root (e.g. the generated harness
#: crate's own ``target``, ~4k files, ~900 MB); the top-level ``^target/`` misses it.
#: These are never source, so they are never readable by the source tools (belt-and-suspenders with
#: each run's own cleanup: a re-run or cached CI workspace can leave them behind).
RUST_FORBIDDEN_READ = "|".join(
(
r"(^target/.*)",
r"(^\.git.*)",
r"(^node_modules/.*)",
r"(.*\.lock$)",
rf"(^{re.escape(INTERNAL_DIR.as_posix())}/.*)",
r"(.*/target/.*)",
)
)

RUST = Language(
name="rust",
Expand Down
13 changes: 8 additions & 5 deletions composer/rag/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,14 +49,17 @@
DEFAULT_CONNECTION: str = f"postgresql://rag_user:rag_password@{_RAG_HOST}:{_RAG_PORT}/rag_db"
SANITY_DEFAULT_CONNECTION: str = f"postgresql://extended_rag_user:rag_password@{_RAG_HOST}:{_RAG_PORT}/rag_db"
FOUNDRY_DEFAULT_CONNECTION: str = f"postgresql://foundry_rag_user:rag_password@{_RAG_HOST}:{_RAG_PORT}/rag_db"
CRUCIBLE_DEFAULT_CONNECTION: str = f"postgresql://crucible_rag_user:rag_password@{_RAG_HOST}:{_RAG_PORT}/rag_db"

# Logical knowledge-base tag -> default DB connection, for corpora ingested by the generic importer
# (`composer.scripts.rag_import`). The tag is the one the manifest carries (== a wheel's
# `rag_db_default`), so the import target and the runtime search tools resolve by one name —
# `composer.tools.rag_env` requires both halves before a tag is usable. Empty until the first such
# corpus lands with the application that declares it; the CVL/Foundry builders use the constants
# above instead.
KNOWLEDGE_BASES: dict[str, str] = {}
# `rag_db_default`), so the import target and the runtime search tools resolve by one name — see
# `composer.tools.rag_env`, which requires both halves before a tag is usable. The CVL/Foundry
# builders keep using their constants above.
KNOWLEDGE_BASES: dict[str, str] = {
# Crucible harness-authoring docs; manifest at `rust/crucible-app/crucible_kb.rag.json`.
"crucible_kb": CRUCIBLE_DEFAULT_CONNECTION,
}


type _RagHeader = str | None
Expand Down
Loading
Loading