DEV Community

Finley Zhou
Finley Zhou

Posted on

An Agent Patch May Not Run Until the Fixture Digest Is Signed

An agent patch is not ready for a property suite because a model emitted it. It is ready only when three records exist and play different roles: a provenance note, a fixture digest signed outside the drafter, and a property class that limits what a pass or fail may mean. Miss any one of them and the run should not start.

A flake freeze is a later ledger. It cannot repair a missing signature. That admission order is the strategy. The steps below enforce it without treating a cheaper draft, or a cheaper runner, as evidence.

The failure this gate is for

Drafts are cheap to request. A locked input set is not. If the suite starts before the fixture is closed, the second run can see a different file, seed, or clock. The disagreement looks like a flake. It is an unbound fixture.

Call that an admission bug. Do not shrink a witness, spend a replay budget, or write a freeze record until admission has succeeded. Later tools assume the input object is already stable. This note does not restate shrink order, two-run record formats, or the rule for closing a freeze. It only keeps those tools from starting early.

Step 1. Write provenance, then refuse to trust it

Keep a short note about who drafted the patch and which interface returned it. The note is an audit field. It is not an oracle.

  1. Set draft_id from the operator log. Do not parse an id out of model prose.
  2. Set drafter to a role label such as human or model_lane. Leave the model name blank unless that name was verified for this call.
  3. Set received_at from the operator clock, in UTC, at second resolution.
  4. Set patch_sha256 to the hash of the exact diff bytes you intend to apply.

Stop if the body is empty. A success status with no patch is an intake failure. It is not a property failure, and it is not a flake. Transport may refetch once under its own policy. That refetch must not create a fixture digest and must not open a freeze.

Step 2. Sign the fixture digest before any property runs

A fixture is the closed input set: files, seed, clock bound, and the allow-list of commands. Hash that set. Sign the hash with a key the draft lane does not hold.

Keep the signer role and the executor role distinct, even on one laptop. An execution host can run the suite. It should not mint the digest. Ordinary executor failures include "unavailable" and "empty response." Those are not assertion results. If the same process both writes the fixture and signs it, the signature only attests that the process agreed with itself.

Build the digest from canonical bytes.

  1. Sort fixture paths by relative path.
  2. Hash each file with SHA-256.
  3. Append the seed, the clock bound, and the allowed commands as their own lines.
  4. Hash the concatenation. Call that fixture_digest.
  5. Sign it in the operator-key role and store the signature beside the patch record.

If a path is missing, admission fails closed. If the patch invokes a command outside the allow-list, admission fails closed. Do not edit the fixture inside the same job so the patch can pass. An edit is a new fixture and needs a new signature.

This shell sketch only builds the unsigned digest. It is a proposal, not a pipeline claim.

# Proposal only. Do not point this at untrusted paths.
find fixtures -type f -print | sort | while read -r f; do
  sha256sum "$f"
done > /tmp/fixture.list
printf 'seed:%s\nclock_bound_s:%s\n' "$SEED" "$CLOCK_BOUND" >> /tmp/fixture.list
sha256sum /tmp/fixture.list
Enter fullscreen mode Exit fullscreen mode

The last line is the unsigned digest. Signing stays outside this shell. The executor should receive the digest and the signature as read-only inputs. It should not be able to replace either file.

Step 3. Class the property before the runner starts

Each property needs a class id checked into the repo and written by a person. The class is what a result is allowed to mean. A pass without a class is noise.

Class A fail means A pass does not mean May open a freeze?
pure_fn A bound input broke the spec The rest of the tree is safe No. Reject or fix the patch.
idempotent_io Two applies left different trees Another seed would agree No, until both trees match the signed digest.
bounded_time The declared clock bound was exceeded The logic is wrong No. Slowness is not a flake.
external_probe A live dependency disagreed The patch is at fault No. Live probes never freeze.

Anything that reads the network, an unbounded wall clock, or an unsigned file is outside the table. Pull it from the gate, or rewrite it until it fits pure_fn or idempotent_io.

Tautologies are a class failure, not a weak pass. An assertion of True, or a check that compares the patch to a copy of itself, never enters the runner. Reject that property id before admission. Do not give it a softer class so the queue stays green.

Step 4. Let the stability ledger start only after admission

Run an admitted property twice on the same fixture_digest. Compare the two outcome hashes. Only an eligible class, plus a real disagreement between those runs, may open a freeze record. A counterexample that appears in both runs is a defect. Freeze nothing. A missing digest is not a disagreement. Freeze nothing.

Before the second run, confirm the digest did not drift. This check is part of the proposal, not a recorded benchmark.

test "$DIGEST_RUN_1" = "$DIGEST_RUN_2"
Enter fullscreen mode Exit fullscreen mode

A non-zero exit here is an admission bug that escaped step 2. Stop. Do not label it flaky. When a freeze is allowed, point it at the digest and the two outcome hashes. Expire it under the policy you already run. Do not invent a shorter expiry inside the runner to make the queue look clean.

Decision matrix

First matching row wins. admit means the suite may start. It does not mean merge.

Provenance Signature Class eligible Runner role Decision
Incomplete Any Any Any reject_provenance
Complete Invalid or absent Any Any reject_fixture_role
Complete Valid, operator key No Any reject_class
Complete Signed by the executor Yes Execution host acting as signer reject_fixture_role
Complete Valid, operator key Yes Execution host as executor only admit
Complete Valid, operator key Yes Local executor admit

Three traces fall out of that table. They are classifications, not measured incident rates.

  • Empty diff with a success status. Decision: reject_provenance. Do not retry inside the gate.
  • A green run whose fixture bytes changed, with the signature produced by the host that executed. Decision: reject_fixture_role.
  • Intermittent name lookup inside a property labeled external_probe. Decision: reject_class. Do not convert DNS into a freeze.

Proposed admission function

The Python below is unexecuted proposal code. It is not a measured gate, and it does not verify a cryptographic signature. The caller must set fixture_sig_ok only after a real verify in the operator-key role. Shipping the boolean as a model-controlled field would let the draft approve itself.

import hashlib
import json
from dataclasses import dataclass

ELIGIBLE = {"pure_fn", "idempotent_io"}

@dataclass(frozen=True)
class Intake:
    draft_id: str
    drafter: str
    patch_sha256: str
    fixture_digest: str
    fixture_sig_ok: bool
    property_class: str
    signer_role: str

def decide(item: Intake) -> str:
    if not item.draft_id or len(item.patch_sha256) != 64:
        return "reject_provenance"
    if item.signer_role != "operator_key" or not item.fixture_sig_ok:
        return "reject_fixture_role"
    if item.property_class not in ELIGIBLE:
        return "reject_class"
    if not item.fixture_digest:
        return "reject_fixture_role"
    return "admit"

def fixture_digest(files: dict, seed: int, commands: list) -> str:
    lines = []
    for path in sorted(files):
        digest = hashlib.sha256(files[path]).hexdigest()
        lines.append(path + ":" + digest)
    lines.append("seed:" + str(seed))
    lines.append("cmds:" + ",".join(commands))
    return hashlib.sha256("\n".join(lines).encode()).hexdigest()

if __name__ == "__main__":
    digest = fixture_digest({"src/app.py": b"print(1)\n"}, 7, ["pytest -q"])
    sample = Intake(
        "d-100", "model_lane", "ab" * 32, digest, True, "pure_fn", "operator_key"
    )
    print(json.dumps({"decision": decide(sample), "digest": digest}))
Enter fullscreen mode Exit fullscreen mode

Sketch the three rejects before you trust the function. Clear draft_id and expect reject_provenance. Set signer_role to execution_host and expect reject_fixture_role. Set property_class to external_probe and expect reject_class. Stop there. Do not attach the sketch to a live service to watch it fail open.

Where the free lanes fit

Disclosure: This article was prepared as part of MonkeyCode's product outreach.

MonkeyCode's free model access fits the draft lane in step 1. You can request candidate patches while rehearsing the gate, without treating that access as a test oracle. MonkeyCode's free server option fits the executor row in the matrix. It can run an already admitted suite against a digest signed elsewhere. Those two statements are availability claims only. They do not name a model, a quota, a machine shape, a time limit, or a promise that the terms stay fixed. Read the current product documentation before you plan capacity around them.

If the free server is down, log an availability miss. Do not store that miss as a failed property, and do not open a freeze from it. If a free model call returns no diff, stay on reject_provenance. A second call does not promote an empty body into a fixture.

Teams that already separate drafting from signing can use those two free options to practice the matrix on a private repository before they point the same door at a paid runner.

Limits of the gate

Admission does not show that the patch is correct. It shows that the suite was allowed to speak. A signed digest can still encode a bad spec. Two agreeing runs can both be wrong when step 3 let a tautology through. The signature does not review license, intent, or whether the diff touches code the operator may change.

fixture_sig_ok is the dangerous field in the sketch. A caller-supplied true value would self-approve. Compute it in the operator-key role and pass the boolean in as an already verified fact. Keep the private key off the draft lane and off the execution host.

Free execution adds an axis you do not control. Cold starts and empty bodies stay in transport handling. If your logs cannot separate that axis from an assertion, do not host this gate on that runner. The matrix will then mix availability with correctness, which is the bug it was written to prevent.

Who should skip it

Skip the matrix if tests still call live services, read an unbounded clock, or build fixtures inside the test process. Also skip it when the same credentials draft the patch and sign the digest. The table will admit work that the role split was meant to block.

Do not use this rehearsal as a safety case, a regulated evidence pack, or a substitute for human merge review. And do not spend a shrinker, a replay budget, or a freeze to launder a reject row into a pass. Those tools start after admit. Used earlier, they hide the unbound fixture instead of catching it.

Top comments (0)