Hmbown/CodeWhale · error · RuntimeContractError

{kind} field `{'.'.join((*base, 'identity_sha256'))}` must m

Error message

{kind} field `{'.'.join((*base, 'identity_sha256'))}` must match the owned sorted tool_names

What it means

Each tool surface stores identity_sha256 = tool_identity_digest(names) = hashlib.sha256 of the NUL-joined ("\0".join) sorted tool_names, hex-encoded. This error fires when the stored digest does not match the digest recomputed from the document's own tool_names - names and digest are out of sync within one document. It protects the identity half of the ratchet: compare() later requires receipt and budget digests to be byte-equal (error 209).

Source

Thrown at scripts/check-runtime-contract-budget.py:234

            dotted_names = ".".join((*base, "tool_names"))
            if (
                not isinstance(names, list)
                or any(not isinstance(name, str) or not name for name in names)
                or names != sorted(set(names))
            ):
                raise RuntimeContractError(
                    f"{kind} field `{dotted_names}` must be sorted unique non-empty strings"
                )
            count = metric_value(document, (*base, "tools"), kind)
            if count != len(names):
                raise RuntimeContractError(
                    f"{kind} metric `{'.'.join((*base, 'tools'))}` must equal the "
                    f"owned tool_names length ({len(names)})"
                )
            digest = required_value(document, (*base, "identity_sha256"), kind)
            expected = tool_identity_digest(names)
            if digest != expected:
                raise RuntimeContractError(
                    f"{kind} field `{'.'.join((*base, 'identity_sha256'))}` must "
                    "match the owned sorted tool_names"
                )

    for stage, _label in REPRESENTATIVE_STAGES:
        path = ("representative_context", "stages", stage, "identity_sha256")
        digest = required_value(document, path, kind)
        if not isinstance(digest, str) or re.fullmatch(r"[0-9a-f]{64}", digest) is None:
            raise RuntimeContractError(
                f"{kind} field `{'.'.join(path)}` must be a lowercase SHA-256 digest"
            )


def validate_receipt(receipt: dict[str, Any]) -> None:
    validate_document(receipt, RECEIPT_KIND, "receipt")
    skill_discovery = receipt.get("skill_discovery")
    identical = (
        skill_discovery.get("prompts_byte_identical")

View on GitHub (pinned to 8880682c63)

Solutions

  1. Recompute the digest for the named node: python3 -c "import hashlib;print(hashlib.sha256('\0'.join(sorted(names)).encode()).hexdigest())" using the exact sorted tool_names from the document
  2. Prefer regenerating all identity fields at once: re-run scripts/measure-runtime-contract.py and record its receipt values into the budget as one maintainer decision
  3. If the names were edited by mistake, revert them so the stored digest becomes correct again

Example fix

# before - names edited, digest stale
"tool_names": ["agent", "apply_patch", "bash"],
"identity_sha256": "<digest of the old name set>"

# after
import hashlib
names = ["agent", "apply_patch", "bash"]
digest = hashlib.sha256("\0".join(names).encode("utf-8")).hexdigest()
# paste `digest` into identity_sha256
Defensive patterns

Strategy: validation

Validate before calling

import hashlib


def expected_digest(names: list[str]) -> str:
    return hashlib.sha256("\0".join(names).encode("utf-8")).hexdigest()


def digests_consistent(doc: dict) -> bool:
    for mode in ("plan", "act", "operate"):
        for surface in ("full", "active"):
            node = (
                doc.get("tool_catalog", {})
                .get("modes", {})
                .get(mode, {})
                .get(surface, {})
            )
            names = node.get("tool_names")
            if not isinstance(names, list):
                return False
            if node.get("identity_sha256") != expected_digest(names):
                return False
    return True

Prevention

When it happens

Trigger: Editing tool_names (add/remove/rename/reorder) in the budget JSON or a receipt without recomputing identity_sha256; changing the digest algorithm, join separator, or sort order in a fork; a partial hand-merge that updates names but restores the old digest.

Common situations: Same hand-edit workflow as errors 200/201: a maintainer records a new tool in the budget, updates the count, but pastes the old digest. Also seen when someone recomputes the digest with a newline or comma join instead of the NUL separator.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@8880682c63 (2026-08-16). Data as JSON: /api/errors/278c6ca765f215bb. Report an issue: GitHub.