fix(pending-ops): MCP approval of bulk-book works and failed approvals no longer consume the op (#1852)

* fix(pending-ops): MCP approval of bulk-book works and failed approvals no longer consume the op

Feedback seq 261545 (deepCFO): approving a bulk_book_transactions op over
MCP returned BULK_BOOK_UNAUTHORIZED, yet the op vanished from /pending
with nothing booked; the user believed it had been approved.

Two defects:

1. The bulk_book_transactions RPC gates on auth.uid(), which is NULL on
   the cookieless service client every MCP approval runs on, so EVERY
   API-key approval of a samlingsverifikat was refused. New migration
   20260824170000 adds p_user_id, honored only for service_role callers
   (same gate as match_batch_allocate 20260817150000 and undo_sie_import);
   the executor passes the approving user, who is now also the actor
   stamped on the verifikat. pg-real test covers member/spoof/no-JWT/
   grants like the precedent.

2. The dispatcher consumed the op on ANY executor error other than 404/
   409. An authorization refusal happens before any side-effect and says
   nothing about the op, so 401/403 now release the claim back to
   'pending'. The executor maps RPC codes through the structured-error
   registry so 403/404/409 are distinguishable from 400. Every
   CommitResult carries operation_status (pending | committed | rejected
   | failed_partial), exposed on gnubok_approve_pending_operation, so
   agents stop inferring consumption from status 'failed'.

Catalog token ceiling 59.95K -> 60K per the documented ratchet protocol.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ScVhg6XsDtNXkiEQNV7LaZ

* fix(pending-ops): revoke anon explicitly on the service-actor bulk_book signature

Default privileges grant EXECUTE on new functions to anon; the pg-real
grants test (mirroring match_batch_allocate) caught it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ScVhg6XsDtNXkiEQNV7LaZ

---------

Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Jakob Wennberg
2026-08-25 10:32:12 +02:00
committed by GitHub
co-authored by Claude Fable 5 Jakob Wennberg
parent e35714518f
commit dc92fb5c0c
7 changed files with 801 additions and 4 deletions
@@ -198,9 +198,14 @@ describe('tools/list payload size guard', () => {
// so strict clients stop failing successful unmatched uploads (seq
// 261972). Prose trimmed to the floor first; headroom before the
// change was ~19 tokens, so even the trimmed contract crossed.
// * 59.95K to 60K with operation_status on gnubok_approve_pending_operation
// (feedback seq 261545): a failed approve used to consume the op
// silently, and agents inferred "consumed" from status 'failed' both
// ways. The enum is the contract; the description is one clause;
// headroom before the change was ~15 tokens, so even that crossed.
// Long-term answer to growth is leaning harder on gnubok_search_tools: if this
// fires again, prefer trimming descriptions or making a tool opt-in via search
// before bumping further.
expect(approxTokens).toBeLessThan(59_950)
expect(approxTokens).toBeLessThan(60_000)
})
})
+2
View File
@@ -16843,6 +16843,7 @@ export const tools: McpTool[] = [
error: { type: 'string' },
error_code: { type: 'string' },
auto_rejected: { type: 'boolean' },
operation_status: { type: 'string', enum: ['pending', 'committed', 'rejected', 'failed_partial'], description: 'pending = not consumed, re-approvable' },
},
required: ['status', 'operation_id'],
},
@@ -16956,6 +16957,7 @@ export const tools: McpTool[] = [
...(result.error ? { error: result.error } : {}),
...(result.code ? { error_code: result.code } : {}),
...(result.auto_rejected ? { auto_rejected: true } : {}),
...(result.operation_status ? { operation_status: result.operation_status } : {}),
}
},
},