Skip to content

Commit 08bdbea

Browse files
authored
chore(search): diagnose unsupported Fireflies response shapes (#9488)
* test(search): guard Fireflies response diagnostic privacy * chore(search): diagnose unsupported Fireflies response shapes * test(search): cover encoded and oversized diagnostic text * fix(search): retain bounded provider diagnostics in production
1 parent 7860fbe commit 08bdbea

2 files changed

Lines changed: 107 additions & 1 deletion

File tree

‎apps/sim/lib/sim-search/live/fireflies-mcp.ts‎

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { createLogger } from '@sim/logger'
12
import { toArray, toRecord } from '@sim/utils/object'
23
import { nativeText } from '@/lib/sim-search/live/dates'
34
import { NativeSearchError, string } from '@/lib/sim-search/live/http'
@@ -6,7 +7,49 @@ import { boundedMeetingContent } from '@/lib/sim-search/live/meeting-content'
67
import type { NativeDocument, NativePage, NativeSearchInput } from '@/lib/sim-search/live/types'
78

89
const MAX_MEETING_CONTENT = 200_000
10+
const MAX_DIAGNOSTIC_TEXT_LENGTH = 65_536
911
const UTC_DAY_MS = 86_400_000
12+
const logger = createLogger('FirefliesMcpSearch', { logLevel: 'WARN' })
13+
14+
/** Only fixed envelope keys and value kinds may enter diagnostics, never provider keys or values. */
15+
function responseShape(value: unknown) {
16+
const record = toRecord(value)
17+
const kind = (entry: unknown) =>
18+
entry === null ? 'null' : Array.isArray(entry) ? 'array' : typeof entry
19+
return {
20+
type: kind(value),
21+
fields: Object.fromEntries(
22+
[
23+
'transcripts',
24+
'data',
25+
'result',
26+
'results',
27+
'items',
28+
'output',
29+
'text',
30+
'error',
31+
'errors',
32+
].flatMap((key) => (Object.hasOwn(record, key) ? [[key, kind(record[key])]] : []))
33+
),
34+
}
35+
}
36+
37+
function responseTextShape(value: unknown) {
38+
if (typeof value !== 'string') return undefined
39+
if (value.length > MAX_DIAGNOSTIC_TEXT_LENGTH)
40+
return { length: value.length, inspection: 'skipped_size_limit' }
41+
const text = value.trim()
42+
const fenced = text.match(/^```(?:json)?\r?\n([\s\S]*)\r?\n```$/i)?.[1]
43+
let jsonShape: ReturnType<typeof responseShape> | undefined
44+
try {
45+
jsonShape = responseShape(JSON.parse(fenced ?? text))
46+
} catch {}
47+
return {
48+
length: value.length,
49+
format: fenced !== undefined ? 'code_fence' : jsonShape ? 'json' : 'text',
50+
...(jsonShape ? { jsonShape } : {}),
51+
}
52+
}
1053

1154
function requireMeetingId(id: string): string {
1255
if (!/^[\w-]{1,200}$/.test(id))
@@ -139,11 +182,19 @@ export async function searchFirefliesMcp(
139182
const rows = Array.isArray(result)
140183
? result
141184
: (data.transcripts ?? toRecord(data.data).transcripts)
142-
if (!Array.isArray(rows))
185+
if (!Array.isArray(rows)) {
186+
logger.warn('Unsupported Fireflies transcript list response shape', {
187+
response: responseShape(result),
188+
data: responseShape(data.data),
189+
result: responseShape(data.result),
190+
output: responseShape(data.output),
191+
text: responseTextShape(typeof result === 'string' ? result : data.text),
192+
})
143193
throw new NativeSearchError(
144194
'unavailable',
145195
'Fireflies returned an unsupported transcript list format.'
146196
)
197+
}
147198
const documents = rows.slice(0, limit).flatMap((value) => {
148199
const document = meetingDocument(toRecord(value))
149200
return document ? [document] : []

‎apps/sim/lib/sim-search/live/meeting-mcp.test.ts‎

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { getAllMockLoggers } from '@sim/testing/mocks/logger.mock'
12
import { describe, expect, it } from 'vitest'
23
import { readFirefliesMcp, searchFirefliesMcp } from '@/lib/sim-search/live/fireflies-mcp'
34
import { readGranolaMcp, searchGranolaMcp } from '@/lib/sim-search/live/granola-mcp'
@@ -157,6 +158,60 @@ describe('meeting MCP provider wire contracts', () => {
157158
await expect(searchFirefliesMcp(client, input)).rejects.toThrow('unsupported')
158159
})
159160

161+
it.each(['text', 'json', 'code_fence', 'oversized'] as const)(
162+
'diagnoses unsupported Fireflies %s envelopes without logging private values or arbitrary keys',
163+
async (format) => {
164+
const privateMarker = 'PRIVATE_MEETING_OR_CREDENTIAL'
165+
const json = JSON.stringify({ items: [{ title: privateMarker }], [privateMarker]: 'secret' })
166+
const text =
167+
format === 'oversized'
168+
? JSON.stringify({ items: [privateMarker.repeat(3000)] })
169+
: format === 'code_fence'
170+
? ['```json', json, '```'].join('\n')
171+
: format === 'json'
172+
? json
173+
: privateMarker.repeat(1000)
174+
const client: ManagedSearchMcpClient = {
175+
async call() {
176+
return {
177+
text,
178+
data: { error: privateMarker },
179+
result: { items: [{ title: privateMarker }] },
180+
...Object.fromEntries(
181+
Array.from({ length: 1000 }, (_, index) => [
182+
`${privateMarker}_${index}`,
183+
privateMarker,
184+
])
185+
),
186+
}
187+
},
188+
}
189+
await expect(searchFirefliesMcp(client, { ...input, query: privateMarker })).rejects.toThrow(
190+
'unsupported'
191+
)
192+
const warnings = getAllMockLoggers().flatMap((logger) => logger.warn.mock.calls)
193+
expect(warnings).toHaveLength(1)
194+
expect(warnings[0]?.[1]).toMatchObject({
195+
response: { type: 'object', fields: { text: 'string', data: 'object', result: 'object' } },
196+
data: { type: 'object', fields: { error: 'string' } },
197+
result: { type: 'object', fields: { items: 'array' } },
198+
text:
199+
format === 'oversized'
200+
? { length: text.length, inspection: 'skipped_size_limit' }
201+
: {
202+
length: text.length,
203+
format,
204+
...(format === 'text'
205+
? {}
206+
: { jsonShape: { type: 'object', fields: { items: 'array' } } }),
207+
},
208+
})
209+
const serialized = JSON.stringify(warnings)
210+
expect(serialized).not.toContain(privateMarker)
211+
expect(serialized.length).toBeLessThan(2000)
212+
}
213+
)
214+
160215
it('rejects invalid continuation offsets before Fireflies receives a query', async () => {
161216
const client = {
162217
async call() {

0 commit comments

Comments
 (0)