Skip to content

Add an optional Receipt Required guard to destructive row deletion tools - #11

Open
FutureEnterprises wants to merge 3 commits into
ayyazzafar:mainfrom
FutureEnterprises:receipt-required-delete-guard
Open

Add an optional Receipt Required guard to destructive row deletion tools#11
FutureEnterprises wants to merge 3 commits into
ayyazzafar:mainfrom
FutureEnterprises:receipt-required-delete-guard

Conversation

@FutureEnterprises

Copy link
Copy Markdown

This PR adds an optional Receipt Required guard around destructive Baserow row deletion tools. Missing receipt returns 428, a valid receipt allows deletion, replay is refused, and tampering is refused. It is intentionally scoped to delete_row and batch_delete_rows, with tests proving no delete happens without a valid receipt.

What it does

Before baserow_delete_row or baserow_batch_delete_rows executes, the call must carry an authorization_receipt — a verifiable EP-RECEIPT-v1 proving a named human accountably approved this exact deletion. No receipt → the tool returns a structured Receipt-Required challenge (HTTP 428 shape) telling the agent exactly what to bring, instead of silently deleting. A well-behaved agent obtains one and retries.

What this is — and isn't

Not auth ("who are you") and not permissions ("are you allowed here"). It is portable accountability evidence the operator keeps for their own liability — necessary, not sufficient. It sits alongside your Baserow tokens/scopes for the destructive path; it does not replace them.

Footprint

  • No backend, no network — verification is offline Ed25519 over canonical JSON.
  • One dependency: @emilia-protocol/require-receipt (Apache-2.0).
  • The other six tools are untouched. The receipt arg is optional in the schema, so the missing-receipt path returns the challenge rather than a schema error.

Conformance (RR-1), proven against the real handler

  1. missing receipt → challenge, no delete
  2. valid action-bound receipt → delete runs
  3. same receipt replayed → refused (one-time consumption)
  4. tampered receipt → refused

Implementation note

@emilia-protocol/require-receipt is ESM-only, while this project emits CommonJS, so the guard preserves dynamic import through TypeScript output. Happy to adjust if you prefer an .mjs boundary or tsconfig change.

Production note

The guard currently passes allowInlineKey: true, which proves a receipt's integrity but not issuer trust. For production, pin trustedKeys to the issuers you trust and drop allowInlineKey. The consumed-receipt set is in-memory (per process); back it with shared storage if you run multiple instances.


Format and semantics track draft-schrock-ep-authorization-receipts, an individual IETF Internet-Draft (not an RFC, no IETF consensus implied). Happy to adjust scope, naming, or the import boundary to fit the project's conventions.

FutureEnterprises and others added 3 commits June 26, 2026 21:57
Gate baserow_delete_row and baserow_batch_delete_rows behind a verifiable
authorization receipt (EP-RECEIPT-v1): missing -> 428 Receipt Required, valid
-> deletes, replay -> refused, tampered -> refused. Optional schema arg, other
tools untouched, no backend, one Apache-2.0 dependency. RR-1 conformance proven
against the real handler.
…tized rejections

Three defects fixed across both guarded delete tools:

A. Consume the receipt only AFTER the irreversible delete succeeds. guardReceipt
   now returns a commit() callback instead of consuming at verify time; row.ts
   calls it only after deleteRow/batchDeleteRows resolves. A failed delete leaves
   the approval retryable. Replay protection still enforced at verify time.

B. Bind the receipt to the specific target, not just the action type:
   baserow_delete_row       -> baserow.row.delete:<table_id>:<row_id>
   baserow_batch_delete_rows -> baserow.rows.batch_delete:<table_id>:<sorted row_ids>
   A receipt minted for one row/table/set cannot delete another.

C. Sanitize rejected-receipt output to { rejected: { reason } } — no longer echoes
   the full verified object (signer/subject/detail).

Verified: npm run build (tsc) green; RR-1 (missing/valid/replay/forged) plus
cross-row, cross-table, and cross-set binding checks all pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the hand-rolled verify/replay/consume/sanitize logic in the delete
guard with the now-published canonical makeReceiptGate. The hardening
(per-target binding, verify, replay refusal, consume-after-success, sanitized
{reason} rejections) now comes from the reviewed helper instead of bespoke code,
shrinking the guard. Behavior is identical: receipts stay bound per row /
per sorted row-set, are consumed only after a successful delete (failures stay
retryable via gate.run's release), replays and cross-target receipts are
refused, and the optional authorization_receipt tool arg is unchanged.

- Bump @emilia-protocol/require-receipt to ^0.4.0
- receipt-guard.ts thin-wraps makeReceiptGate (loaded via the same dynamic
  import() indirection, since this package is CJS and require-receipt is ESM)
- delete_row / batch_delete_rows call through gate.run
- ambient shim declares makeReceiptGate + its gate/run return shapes

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant