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
- 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
- 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
- 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 names change, recompute identity_sha256 in the same edit with the NUL-join recipe above
- Add a pre-commit hook that runs digests_consistent() on runtime-contract-budget.json
- Record intentional identity changes with rationale in the budget _comment so future drift is explainable
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
- {kind} metric `{'.'.join((*base, 'tools'))}` must equal the
- {kind} field `{'.'.join(path)}` must be a lowercase SHA-256
- identity changed for {label} [`{'.'.join(path)}`]{detail}
- ${label} contains an invalid checksum row: ${trimmed}
- unit must be 'chars' or 'lines'
AI-assisted analysis of Hmbown/CodeWhale@8880682c63 (2026-08-16).
Data as JSON: /api/errors/278c6ca765f215bb.
Report an issue: GitHub.