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:
co-authored by
Jakob Wennberg
Claude Fable 5
parent
dcb5d329a3
commit
cd9fb5b717
@@ -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' })
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user