feat(mcp): byte-exact SIE upload path + brevity and memory-first onboarding (#1954)

From the fourth E2E run: the agent correctly refused to reproduce a
104 KB SIE file token by token (silent mid-verifikat truncation) and
dead-ended to the web wizard, and its replies were walls of compliance
prose.

1. gnubok_create_sie_upload: signed same-origin upload URL (reuses the
   pending-document infra; .se/.sie/.si only, 50 MB HTTP cap).
   gnubok_sie_preflight and gnubok_import_sie accept upload_id as the
   byte-exact source, plus optional sha256 (hex of the raw bytes)
   verified on the upload_id/base64 paths so truncation is DETECTED,
   never silent. Inline content above 120k chars is refused with a
   pointer to the upload flow. Scope bookkeeping:write (same intent as
   import_sie).

2. Skill: brevity rule (max ~8 short lines per reply, one warning per
   step, no legal essays), memory-first rule (check what is already
   known before asking the opening questions), the upload-first SIE
   step, and gnubok_explain_voucher_gap after import for skipped
   voucher numbers.

3. CONNECTORS.md starter prompt rewritten memory-first so it stays
   copy-paste ready without the user's own data in it. Plugin v1.2.2.

tools/list ceiling 62K to 62.4K documented in the bench.

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-26 16:11:11 +02:00
committed by GitHub
co-authored by Jakob Wennberg Claude Fable 5
parent dcb5d329a3
commit cd9fb5b717
7 changed files with 245 additions and 40 deletions
@@ -230,9 +230,13 @@ describe('tools/list payload size guard', () => {
// the scan-before-import step the skill instructs for shared SIE
// files, so default-catalog for the same Claude.ai reason; ~390
// tokens (schema carries the mappings-passthrough contract).
// * 62K to 62.4K with gnubok_create_sie_upload + upload_id/sha256 on the
// two SIE tools: the byte-exact upload path after a real 104 KB file
// dead-ended in chat (a model cannot reproduce 30k tokens verbatim
// without silent-truncation risk); skill-instructed, so default catalog.
// 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(62_000)
expect(approxTokens).toBeLessThan(62_400)
})
})
@@ -184,4 +184,45 @@ describe('gnubok_sie_preflight', () => {
it('rejects a call with neither content field', async () => {
await expect(run({})).rejects.toMatchObject({ code: 'VALIDATION_ERROR' })
})
it('refuses oversized inline content instead of risking silent mid-verifikat truncation', async () => {
const huge = VALID_SIE + '\n' + '#KONTO 9999 "x"\n'.repeat(10_000)
await expect(run({ file_content: huge })).rejects.toMatchObject({
code: 'VALIDATION_ERROR',
message: expect.stringContaining('gnubok_create_sie_upload'),
})
})
it('verifies sha256 on the base64 path and rejects a mismatch as truncation', async () => {
const bytes = Buffer.from(VALID_SIE, 'utf8')
const { createHash } = await import('node:crypto')
const good = createHash('sha256').update(bytes).digest('hex')
const ok = await run({ file_content_base64: bytes.toString('base64'), sha256: good })
expect(ok.verdict).toBe('ok')
await expect(
run({ file_content_base64: bytes.toString('base64'), sha256: 'a'.repeat(64) })
).rejects.toMatchObject({
code: 'VALIDATION_ERROR',
message: expect.stringContaining('sha256 mismatch'),
})
})
})
describe('gnubok_create_sie_upload', () => {
const uploadTool = tools.find((t) => t.name === 'gnubok_create_sie_upload')!
it('is registered with the filename-only contract', () => {
expect(uploadTool).toBeDefined()
expect(
(uploadTool.inputSchema as { required: string[] }).required
).toEqual(['filename'])
})
it('rejects a filename that is not a SIE file', async () => {
await expect(
uploadTool.execute({ filename: 'export.xlsx' }, COMPANY_ID, 'user-1', {} as never)
).rejects.toMatchObject({ code: 'VALIDATION_ERROR' })
})
})