{"record":{"id":"6833d0b294dc4ab2","repo":"affaan-m/ECC","slug":"approved-hash-must-be-lowercase-sha-256-hexadecimal","errorCode":null,"errorMessage":"approved hash must be lowercase SHA-256 hexadecimal","messagePattern":"approved hash must be lowercase SHA-256 hexadecimal","errorType":"validation","errorClass":"ClaimError","httpStatus":null,"severity":"error","filePath":"skills/operator-approval-loop/references/approval_claims.py","lineNumber":63,"sourceCode":"        db.rollback()\n        if isinstance(error, sqlite3.Error):\n            raise ClaimError('claim transaction failed; no permission granted') from error\n        raise\n\n\ndef _snapshot(db, obligation_id, decision_id):\n    row = db.execute('''SELECT * FROM approval_bound_drafts\n        WHERE obligation_id=? AND decision_id=?''', (obligation_id, decision_id)).fetchone()\n    if row is None:\n        raise ClaimError('a current bound approved draft is required')\n    try:\n        digest = hashlib.sha256(row['draft_text'].encode('utf-8')).hexdigest()\n    except (AttributeError, UnicodeError) as error:\n        raise ClaimError('approved text must be valid UTF-8 text') from error\n    stored_digest = row['draft_sha256']\n    if (not isinstance(stored_digest, str) or len(stored_digest) != 64\n            or any(character not in '0123456789abcdef' for character in stored_digest)):\n        raise ClaimError('approved hash must be lowercase SHA-256 hexadecimal')\n    if not secrets.compare_digest(digest, stored_digest):\n        raise ClaimError('approved text hash does not match')\n    return dict(row)\n\n\ndef _claim_row(db, token):\n    if not isinstance(token, str) or not token:\n        raise ClaimError('a claim token is required')\n    row = db.execute('SELECT * FROM obligation_delivery_claims WHERE token=?', (token,)).fetchone()\n    if row is None:\n        raise ClaimError('unknown claim token')\n    return row\n\n\ndef claim(db, obligation_id, decision_id, *, now):\n    \"\"\"Reserve one already-authorized decision; return only a random claim token.\"\"\"\n    with _transaction(db, now):\n        _snapshot(db, obligation_id, decision_id)","sourceCodeStart":45,"sourceCodeEnd":81,"githubUrl":"https://github.com/affaan-m/ECC/blob/8321021c54d670126ce3b2969d5deb880b4b0c2a/skills/operator-approval-loop/references/approval_claims.py#L45-L81","documentation":"Each approval snapshot stores the expected SHA-256 of the draft in draft_sha256, and this library requires it to be exactly a 64-character lowercase hex string. Malformed hashes mean the stored approval metadata cannot be trusted, so the library refuses to verify or dispatch.","triggerScenarios":"draft_sha256 stored as None, an int, an uppercase-hex digest, a digest with '0x' prefix, or any string whose length != 64 or containing non-[0-9a-f] characters; calling claim()/begin_dispatch() against such a row.","commonSituations":"A custom writer using hexdigest().upper() or hashing with SHA-1/MD5 (40/32 chars); storing the hash as bytes or a BLOB; hand-edited rows or a schema migration that reformatted the column.","solutions":["Store hashlib.sha256(draft_text.encode('utf-8')).hexdigest() (lowercase, 64 chars) when writing the snapshot","Normalize an existing wrong hash: recompute it from draft_text and UPDATE the row","Check the row: SELECT draft_sha256 FROM approval_bound_drafts ... and validate len==64 and all chars in 0-9a-f before calling","Fix the writing tool/migration to always produce lowercase SHA-256 hex"],"exampleFix":"// before\ndigest = hashlib.sha256(text.encode()).hexdigest().upper()\ndb.execute('UPDATE approval_bound_drafts SET draft_sha256=? ...', (digest,))\n\n// after\ndigest = hashlib.sha256(text.encode('utf-8')).hexdigest()  # lowercase 64-hex\ndb.execute('UPDATE approval_bound_drafts SET draft_sha256=? ...', (digest,))","handlingStrategy":"validation","validationCode":"import re\nHEX64 = re.compile(r'^[0-9a-f]{64}$')\n\ndef hash_ok(db, oid, did) -> bool:\n    row = db.execute('SELECT draft_sha256 FROM approval_bound_drafts WHERE obligation_id=? AND decision_id=?',\n                     (oid, did)).fetchone()\n    return isinstance(row['draft_sha256'], str) and HEX64.fullmatch(row['draft_sha256']) is not None","typeGuard":"def is_sha256_hex(v) -> bool:\n    return isinstance(v, str) and len(v) == 64 and all(c in '0123456789abcdef' for c in v)","tryCatchPattern":"try:\n    token = claim(db, oid, did, now=ts)\nexcept ClaimError as e:\n    if 'lowercase SHA-256 hexadecimal' in str(e):\n        fix_hash_from_text(db, oid, did)  # recompute and UPDATE via trusted writer\n        token = claim(db, oid, did, now=ts)\n    else:\n        raise","preventionTips":["Always write hashes with hashlib.sha256(text.encode('utf-8')).hexdigest()","Add a CHECK constraint or writer-side validator for 64-char lowercase hex","Never uppercase, prefix, or truncate digests","Audit snapshot rows with the is_sha256_hex guard before dispatch runs"],"tags":["sqlite","validation","sha256"],"backgroundTag":"invalid-argument-format","analyzedSha":"8321021c54d670126ce3b2969d5deb880b4b0c2a","analyzedAt":"2026-09-16T10:08:13.343Z","contentChangedAt":"2026-09-16T10:08:13.343Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}