Files
accounted/lib/entitlements/__tests__/capability-maps.test.ts
T
Mattsson 5d7952a01e feat(mcp): model-free document upload via signed URL (#1378)
* feat(mcp): model-free document upload via signed URL (#748)

Adds gnubok_create_document_upload + gnubok_complete_document_upload so
document bytes reach storage through a short-lived signed PUT URL and
never pass through the model context. Fixes silent base64 corruption on
real-size PDFs and the context blowup on batch uploads.

- pending/ staage keys with TTL cleanup; completion validates magic
  bytes + SHA-256, moves bytes to the WORM key and adopts the reserved
  UUID as document id, making retries and concurrent completions
  idempotent
- legacy gnubok_upload_document kept for clients without file access,
  description now points to the signed-URL pair; shared mime resolution
  and inbox-item creation extracted
- both new tools mapped in TOOL_SCOPE_MAP (transactions:write) and
  MCP_TOOL_CAPABILITY_MAP (ai) so the paywall and scope gates hold
- payload guard ceiling 58.5K to 59K after trimming the create tool's
  outputSchema to upload_id/upload_url/expires_at

Fixes #748

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(mcp): satisfy capability-map lock and phantom-column scanner

The exact-entries lock in capability-maps.test.ts now includes the
signed-URL pair as dispatch-only AI tools, and the inbox insert uses a
literal payload (explicit UUID instead of a conditional spread) so the
no-phantom-columns scanner can resolve every column.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-03 18:41:05 +02:00

76 lines
2.9 KiB
TypeScript

import { describe, it, expect } from 'vitest'
import {
MCP_TOOL_CAPABILITY_MAP,
PAID_OPERATION_CAPABILITY_MAP,
PAID_CAPABILITIES,
CAPABILITY,
} from '../keys'
/**
* These maps are the contract that gates the paid MCP/agent path (dispatch +
* commit). Locking the exact entries is the guard against a future paid
* external-service tool silently bypassing the paywall: mirrors the
* TOOL_SCOPE_MAP assertions in the mcp-server tests.
*/
/**
* MCP tools that invoke a paid capability directly (no stage→commit round-trip),
* so they are gated at DISPATCH only and have no commit-time (operation-map)
* counterpart. The document upload tools run Bedrock OCR inline via
* extractInvoiceFields: they never stage a pending_operation.
*/
const DISPATCH_ONLY_MCP_TOOLS = new Set<string>([
'gnubok_upload_document',
'gnubok_create_document_upload',
'gnubok_complete_document_upload',
])
describe('MCP_TOOL_CAPABILITY_MAP', () => {
it('gates exactly the paid MCP tools (3 external-service staging tools + the AI OCR tools)', () => {
expect(MCP_TOOL_CAPABILITY_MAP).toEqual({
gnubok_send_invoice: CAPABILITY.email_send,
gnubok_vat_declaration_submit: CAPABILITY.skatteverket,
gnubok_agi_submit: CAPABILITY.skatteverket,
// Dispatch-only AI tools: inline Bedrock OCR, no staged operation. The
// signed-URL pair is gated at create AND complete so a free-tier key can
// neither reserve nor finalize a paid extraction.
gnubok_upload_document: CAPABILITY.ai,
gnubok_create_document_upload: CAPABILITY.ai,
gnubok_complete_document_upload: CAPABILITY.ai,
})
})
it('only maps tools to PAID capabilities', () => {
for (const key of Object.values(MCP_TOOL_CAPABILITY_MAP)) {
expect(PAID_CAPABILITIES).toContain(key)
}
})
})
describe('PAID_OPERATION_CAPABILITY_MAP', () => {
it('gates exactly the three paid pending-operation types', () => {
expect(PAID_OPERATION_CAPABILITY_MAP).toEqual({
send_invoice: CAPABILITY.email_send,
submit_vat_declaration: CAPABILITY.skatteverket,
submit_agi: CAPABILITY.skatteverket,
})
})
it('only maps operations to PAID capabilities', () => {
for (const key of Object.values(PAID_OPERATION_CAPABILITY_MAP)) {
expect(PAID_CAPABILITIES).toContain(key)
}
})
it('covers the same capabilities as the STAGING MCP tools (dispatch ↔ commit parity)', () => {
// Parity applies to staging tools only: an op that can be staged via MCP OR
// approved in the UI must be gated on both transports. Dispatch-only tools
// (inline AI OCR) have no commit counterpart and are excluded.
const stagingMcpCaps = new Set(
Object.entries(MCP_TOOL_CAPABILITY_MAP)
.filter(([tool]) => !DISPATCH_ONLY_MCP_TOOLS.has(tool))
.map(([, cap]) => cap),
)
expect(new Set(Object.values(PAID_OPERATION_CAPABILITY_MAP))).toEqual(stagingMcpCaps)
})
})