Skip to content

Multi-spec buffers: parallel, per-precision CVL verification - #215

Open
jar-ben wants to merge 33 commits into
masterfrom
jaroslav/multi-spec-buffers-v2
Open

Multi-spec buffers: parallel, per-precision CVL verification#215
jar-ben wants to merge 33 commits into
masterfrom
jaroslav/multi-spec-buffers-v2

Conversation

@jar-ben

@jar-ben jar-ben commented Sep 5, 2026

Copy link
Copy Markdown
Collaborator

Summary

Today the CVL-generation agent authors one spec with one global methods{} block, so
every rule is verified under the intersection of what all rules need precise — one property
that needs an expensive function (nonlinear math, hashing, a heavy external) kept exact forces
every rule to pay that cost. That coupling is a common source of timeouts, and a single
combined run cannot escape it.

This PR replaces the single-spec model with named spec buffers: the agent partitions its
properties into several self-contained CVL buffers, each with its own methods{} (so a
function summarized in one buffer can stay exact in another), and each verified and reviewed
independently and in parallel
. A function's precision cost is now paid only by the buffer that
needs it.

It also converts the prover interaction from a blocking, one-spec-at-a-time call into an
async submit/collect model, so buffers prove concurrently and the agent keeps working
(authoring the next buffer, or processing a finished result) instead of blocking on the slowest
job.

Validated end-to-end on a real project (see Validation).

The buffer model

  • Named buffers. put_buffer / edit_buffer / get_buffer / list_buffers /
    delete_buffer author CVL buffers; submit_buffer / collect_results verify them (replacing
    verify_spec).
  • Run-target vs shared. A run-target buffer declares property_rules (the properties it
    verifies + their rule names) and is run by the prover. A shared buffer (is_run_target=false)
    carries infrastructure (ghosts, common invariants, helper CVL, token/oracle models, summaries)
    and is never run itself.
  • Selective imports. A shared buffer is imported only by the run-targets that declare it — so
    a subset of run-targets can share one base while another subset shares a different one, and
    editing a shared buffer re-verifies only its importers.
  • Coverage is enforced. Every non-skipped property must appear in exactly one run-target,
    and each rule lives in exactly one buffer (validate_coverage / validate_disjoint_rules).
  • Per-buffer completion. Publishing requires, per run-target, both a prover and a
    feedback stamp keyed to a content digest over the buffer's import closure — so editing a
    buffer (or a shared buffer it imports) invalidates exactly the buffers affected, and nothing
    else is re-run.

The async prover model

  • submit_buffer(name) launches a prover job in the background and returns immediately
    (concurrency-throttled; idempotent; supersedes a stale in-flight job for the same buffer).
  • collect_results() drains finished jobs without blocking and returns a status board
    (complete / running / needs (re)submission); it blocks only when there is nothing else to
    do. The agent works a strict priority order — process a finished result, else author/submit the
    next buffer, else block on the next completion.
  • Snapshot isolation. Each job materializes its buffers to tagged {name}__{digest}.spec
    files with sibling imports retargeted, so concurrent jobs never read each other's in-flight
    edits.
  • Shared-base refine. Editing a shared buffer invalidates every importer (including
    already-verified ones); the board lists them under needs (re)submission so the agent re-runs
    exactly those.

Guidance & advisories

  • Prompt guidance now leads with the intersection argument and strongly recommends
    splitting by precision need + adding over-approximating performance summaries preemptively
    (before a monolithic run), including the assert-only vs satisfy soundness split. It is a
    recommendation, not a mandate.
  • Duplication linter (advisory, report-once): flags methods{}/ghost declarations duplicated
    verbatim across run-targets and suggests hoisting them into a shared buffer. It only tells
    it never blocks.

Reliability fixes (independent; bundled here — can be split if preferred)

  • Anthropic mid-stream retry (composer/llm/anthropic.py): a mid-stream connection drop
    surfaces as a raw httpx.RemoteProtocolError; treat it as retryable.
  • Cloud re-fetch, never re-prove (composer/prover/cloud.py): a transient failure while
    downloading a completed job's results used to bubble up and re-run the whole (often
    hours-long) proof. Now the fetch itself is retried with capped exponential backoff against the
    already-SUCCEEDED job.
  • Configurable sanity timeout (certora_autosetup/setup/sanity.py): AUTOPROVER_SANITY_TIMEOUT
    (default 1200s) caps the sanity phase's per-job timeout.

Config knobs

  • AUTOPROVER_MAX_SPEC_BUFFERS — max run-target buffers (default 6).
  • AUTOPROVER_SANITY_TIMEOUT — sanity-phase per-job timeout in seconds (default 1200).

Files

New substrate & tools:

  • composer/spec/source/spec_buffers.py — pure buffer substrate (model, import closure, digests,
    coverage/disjointness validation, completion stamps, dup detection).
  • composer/spec/source/buffer_tools.py — the authoring tools (put/get/edit/list/delete_buffer,
    cap enforcement).

Wiring & orchestration:

  • composer/spec/source/prover.py — async submit_buffer / collect_results, materialization,
    job supersession, dup-linter surfacing.
  • composer/spec/source/author.py — buffer-only authoring mode, per-buffer review/feedback,
    buffer guidance.
  • composer/spec/source/pipeline.py, autoprove_common.py, composer/spec/cvl_generation.py,
    composer/certora_env.py, certora_autosetup/cache/content_cache.py — supporting wiring.
  • composer/templates/property_generation_system_prompt.j2 — prompt updates.

Testing

51 new unit tests across 7 files, all pure (no live prover / no jar):

  • test_spec_buffers.py (26) — substrate: ownership, import closure, digests, coverage,
    disjoint rules, completion stamps, dup detection, buffer-map reducer.
  • test_buffer_completion.py (6), test_spec_buffer_prover.py (5), test_buffer_tools.py (5) —
    completion tracking, async submit/collect + per-buffer feedback, cap enforcement.
  • test_cloud_fetch_retry.py (2), test_sanity_timeout.py (4), test_anthropic_retry.py (3) —
    the reliability fixes.

Full non-expensive suite green (1219 passed; the only errors are pre-existing
test_rag_db.py [postgres] env cases that need the local RAG container). pyright clean.

Validation (live cloud run)

Run on a testing real project:

  • The agent split into 6 run-target buffers across multiple property classes (
    init/ownership, impl/upgrade-auth, attack-vectors; combinatorial-collateral: core, operations,
    attack) — i.e. multiple buffers within a property class, not merely one per class.
  • 52 cloud jobs driven in parallel (up to 3 concurrent in flight), with re-submissions as
    buffers were refined.
  • 0 rules timed out (146 VERIFIED / 75 VIOLATED instantiations; the VIOLATED set is the
    expected-failure attack vectors + refinement CEXes).
  • The dup-linter fired advisorily (e.g. owner() across the 3 buffers) and the agent
    correctly treated it as a non-blocking suggestion.
  • The cloud re-fetch path saw 0 connection failures across the full 52-job fetch load (the
    same volume that previously surfaced transient stalls).

Follow-ups (out of scope)

  • Comment-only re-verification (TODO in spec_buffers.py): a pure-comment edit changes the
    buffer digest and re-runs an identical proof. The fix is a separate comment-stripped prover
    digest distinct from the raw feedback digest — noted, not done here.
  • A spurious-CEX issue observed on the run traces to an agent-authored summary in
    custom_summaries.spec
    (SafeTransferLib path not covered) and the summarizer↔CVL-agent
    one-way handoff — a pre-existing, unrelated issue, not introduced by this PR; handed off
    separately.

@jar-ben jar-ben changed the title Jaroslav/multi spec buffers v2 Multi-spec buffers: parallel, per-precision CVL verification Sep 7, 2026
@jar-ben
jar-ben force-pushed the jaroslav/multi-spec-buffers-v2 branch from e917f8e to 1caafbb Compare September 8, 2026 07:51
Comment thread composer/spec/source/spec_buffers.py Outdated
Comment on lines +55 to +56
"""One named CVL spec buffer the agent authors. Frozen and pydantic so it is both the substrate's
algorithm type and the shape stored (serializably) in graph state."""

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wut?

Comment on lines +61 to +62
name: str
#: The buffer's own CVL text — its rules, its ``methods{}`` block, and its ``import`` statements.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume it is an invariant that buffers[nm].name == nm for all nm?

Comment thread composer/spec/source/spec_buffers.py Outdated
Comment on lines +140 to +141
returned sorted by name. A run-target buffer imports a shared (``is_run_target=false``) buffer only
when it does, so a shared buffer belongs to the closure (and invalidation set) of exactly the

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"a run target buffer imports a shared buffer only when it does"

????? what?

return tuple(out)


def import_closure(buffers: Mapping[str, NamedBuffer], name: str) -> list[NamedBuffer]:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there a reason we don't have the prover create this inventory for us? The answer might be "because it doesn't produce the output we want, and this needs to land independently of that". I buy that answer, to be clear, but I want to make sure I understand the reason.

(e.g. skipped-property or conf-flag markers). Editing the buffer OR any buffer it imports changes
the digest, so it keys the buffer's cached verify/review. Mirrors
:meth:`ContentCache.compute_cache_key` (content-keyed, order-independent)."""
# TODO: a pure-comment edit (e.g. reframing a property's justification docstring) changes this

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WHAT IF WE USED THE CANONICAL TAC

Comment thread composer/spec/source/author.py Outdated
Verifying — submit / collect (buffers prove in parallel; never wait on a slow buffer to work on a fast
one):
- **Settle the shared base before submitting anything that imports it.** Submit a run-target buffer only
once BOTH (i) that buffer is finished AND (ii) every shared buffer it imports is finished — you do not

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what does it mean for a run-target buffer to be "finished"? Like, finished editing?

Comment thread composer/spec/source/author.py Outdated
Comment on lines +839 to +841
under-approximation), edit the shared buffer; that invalidates every buffer importing it — the
board lists them under `needs (re)submission` (including ones already verified) — so re-submit
each of them.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

er, no it doesn't. This makes it sound like the board tells the agent what jobs need to be resubmitted based on the fix required for a shared spec fix. But it has no way of knowing a CEX remediation requires a shared spec fix?

working_dir=pathlib.Path(run_root),
curr_spec=st["curr_spec"],
curr_spec=None,
prover_runner=WrappedProverRunner(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UH. Wait, hang on. This breaks the plugin API in a BIG way that will break some other code. let's discuss

Comment thread composer/spec/source/buffer_tools.py Outdated
Comment on lines +51 to +71
class _PutBufferTemplate(BaseModel):
name: str = Field(description="Unique buffer name (also its on-disk spec stem).")
cvl: str = Field(description="The buffer's full CVL text (rules, methods{}, imports).")
property_rules: dict[str, list[str]] = Field(
default_factory=dict,
description="The properties this buffer verifies and, for each (by its snake_case title), the "
"rule/invariant names in this buffer's CVL that verify it. Across all run-target buffers every "
"non-skipped property must appear in exactly one buffer. Omit for a shared buffer.",
)
is_run_target: bool = Field(
default=True, description="False for a shared, imported-only buffer that runs no rules."
)


def put_buffer[S: WithBuffers](ty: type[S]) -> BaseTool:
schema = create_model(
"PutBuffer", __base__=_PutBufferTemplate, __doc__=put_buffer_description,
state=(Annotated[ty, InjectedState], ...),
tool_call_id=(Annotated[str, InjectedToolCallId], ...),
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

turns out you can subscript generic pydantic models with concrete types just fine, so this create model template dance is totally unnecessary.

Comment thread composer/spec/source/buffer_tools.py Outdated
Comment on lines +74 to +77
def put_buffer(**args) -> str | Command:
if (err := cvl_syntax_error(args["cvl"])) is not None:
return err
buffers_now = args["state"].get("buffers") or {}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess ... what's the point of the argument schema if you're gonna throw it all away at the argument typing level? But with the above understanding, just use the "WithImplementation" and what not to avoid all this boilerplate.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants