affaan-m/ECC · error · ClaimError

a current bound approved draft is required

Error message

a current bound approved draft is required

What it means

Before granting any permission the library loads the immutable approval snapshot from approval_bound_drafts keyed by (obligation_id, decision_id). If no such row exists, there is no bound, approved draft to dispatch against, so it raises this error. It enforces that only previously approved content can ever be claimed or dispatched.

Solutions

  1. Verify the draft was recorded: SELECT * FROM approval_bound_drafts WHERE obligation_id=? AND decision_id=? before calling claim()
  2. Correct the obligation_id/decision_id argument order or values
  3. Ensure you are connected to the database where the approval actually happened
  4. Restore the missing snapshot via the trusted decision writer; never hand-insert rows to bypass the approval flow

Example fix

// before
token = claim(db, obligation_id, decision_id, now=ts)  # snapshot missing

// after
row = db.execute('SELECT 1 FROM approval_bound_drafts WHERE obligation_id=? AND decision_id=?',
                 (obligation_id, decision_id)).fetchone()
if row is None:
    raise AppError('no approved draft for this decision; run the approval flow first')
token = claim(db, obligation_id, decision_id, now=ts)
Defensive patterns

Strategy: validation

Validate before calling

def snapshot_exists(db, oid, did) -> bool:
    return db.execute('SELECT 1 FROM approval_bound_drafts WHERE obligation_id=? AND decision_id=?',
                      (oid, did)).fetchone() is not None

Try / catch

try:
    token = claim(db, oid, did, now=ts)
except ClaimError as e:
    if 'current bound approved draft is required' in str(e):
        raise AppError(f'no approved draft for {oid}/{did}; run approval flow first') from e
    raise

Prevention

When it happens

Trigger: Calling claim(db, obligation_id, decision_id, now=ts) with an obligation_id/decision_id pair never written by the trusted decision writer; calling begin_dispatch() whose claim references a snapshot row that was deleted; typos or wrong IDs (e.g. swapped argument order).

Common situations: Running the claims module against a database where the approval workflow never recorded the draft; pointing at the wrong environment's DB (staging vs production); a migration or cleanup job deleting approval_bound_drafts rows while claims still reference them.

Understand the failure class

Background: Record Not Found Errors: "not found", RecordNotFound, and "was not found" — what they mean and how to fix them — this error's family across 28 libraries.

Related errors


AI-assisted analysis of affaan-m/ECC@8321021c54 (2026-09-16). Data as JSON: /api/errors/29cc9cca8fa42343. Report an issue: GitHub.

Appendix: source

Thrown at skills/operator-approval-loop/references/approval_claims.py:55

    if any(db.execute(f'PRAGMA {name}').fetchone()[0] != 1
           for name in ('foreign_keys', 'recursive_triggers')):
        raise ClaimError('required SQLite guards are disabled')
    try:
        db.execute('BEGIN IMMEDIATE')
        yield
        db.commit()
    except BaseException as error:
        db.rollback()
        if isinstance(error, sqlite3.Error):
            raise ClaimError('claim transaction failed; no permission granted') from error
        raise


def _snapshot(db, obligation_id, decision_id):
    row = db.execute('''SELECT * FROM approval_bound_drafts
        WHERE obligation_id=? AND decision_id=?''', (obligation_id, decision_id)).fetchone()
    if row is None:
        raise ClaimError('a current bound approved draft is required')
    try:
        digest = hashlib.sha256(row['draft_text'].encode('utf-8')).hexdigest()
    except (AttributeError, UnicodeError) as error:
        raise ClaimError('approved text must be valid UTF-8 text') from error
    stored_digest = row['draft_sha256']
    if (not isinstance(stored_digest, str) or len(stored_digest) != 64
            or any(character not in '0123456789abcdef' for character in stored_digest)):
        raise ClaimError('approved hash must be lowercase SHA-256 hexadecimal')
    if not secrets.compare_digest(digest, stored_digest):
        raise ClaimError('approved text hash does not match')
    return dict(row)


def _claim_row(db, token):
    if not isinstance(token, str) or not token:
        raise ClaimError('a claim token is required')
    row = db.execute('SELECT * FROM obligation_delivery_claims WHERE token=?', (token,)).fetchone()
    if row is None:

View on GitHub (pinned to 8321021c54)