Skip to content

Commit 4eb1436

Browse files
committed
fix
1 parent 5cbafee commit 4eb1436

2 files changed

Lines changed: 78 additions & 33 deletions

File tree

apps/sim/lib/copilot/request/lifecycle/run.test.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,63 @@ describe('runCopilotLifecycle', () => {
331331
})
332332
})
333333

334+
it('projects large tool catalogs at the tool-definition boundary', async () => {
335+
const registry = new ResolvedSecretTraceRegistry([
336+
{ name: 'TOKEN', plaintext: 'catalog-secret', encryptedValue: 'ciphertext' },
337+
])
338+
const toolCount = 4_000
339+
const propertiesPerTool = 8
340+
// These definitions are individually small, but flattening their semantic fields into one
341+
// synthetic projection value creates 108,001 traversal nodes and crosses the per-value budget.
342+
const integrationTools = Array.from({ length: toolCount }, (_, toolIndex) => {
343+
const properties = Object.fromEntries(
344+
Array.from({ length: propertiesPerTool }, (_, propertyIndex) => [
345+
`field_${propertyIndex}`,
346+
{
347+
type: 'string',
348+
description: `Field ${propertyIndex} for tool ${toolIndex}`,
349+
},
350+
])
351+
)
352+
353+
return {
354+
name: `tool_${toolIndex}`,
355+
description: `Tool ${toolIndex} uses catalog-secret`,
356+
input_schema: {
357+
type: 'object',
358+
properties,
359+
required: Object.keys(properties),
360+
},
361+
}
362+
})
363+
let capturedRequestBody = ''
364+
mockRunStreamLoop.mockImplementationOnce(async (_url: string, request: RequestInit) => {
365+
capturedRequestBody = String(request.body)
366+
})
367+
368+
const result = await runCopilotLifecycle(
369+
{
370+
message: 'Use the integration catalog',
371+
messageId: 'stream-large-tool-catalog',
372+
integrationTools,
373+
},
374+
{
375+
userId: 'user-1',
376+
workspaceId: 'ws-1',
377+
executionContext: { userId: 'user-1', workflowId: '', workspaceId: 'ws-1' },
378+
resolvedSecretTraceRegistry: registry,
379+
}
380+
)
381+
382+
expect(result.success).toBe(true)
383+
expect(mockRunStreamLoop).toHaveBeenCalledOnce()
384+
const sent = JSON.parse(capturedRequestBody)
385+
expect(sent.integrationTools).toHaveLength(integrationTools.length)
386+
expect(sent.integrationTools[0].description).toBe('Tool 0 uses {{TOKEN}}')
387+
expect(sent.integrationTools.at(-1).name).toBe(`tool_${toolCount - 1}`)
388+
expect(capturedRequestBody).not.toContain('catalog-secret')
389+
})
390+
334391
it('projects selected JSON and attachment fields exactly once when plaintext overlaps its alias', async () => {
335392
const registry = new ResolvedSecretTraceRegistry([
336393
{ name: 'TOKEN', plaintext: 'TOKEN', encryptedValue: 'ciphertext' },

apps/sim/lib/copilot/request/lifecycle/run.ts

Lines changed: 21 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,7 @@ import {
3838
COPILOT_VFS_ROUTING_KEYS,
3939
isCopilotModelTextKey,
4040
} from '@/lib/copilot/model-visible-content'
41-
import {
42-
collectModelVisibleSchemaContent,
43-
getModelVisibleSchemaAction,
44-
} from '@/lib/copilot/model-visible-schema'
41+
import { getModelVisibleSchemaAction } from '@/lib/copilot/model-visible-schema'
4542
import { getAutoAllowedTools } from '@/lib/copilot/persistence/tool-permission/auto-allow'
4643
import { createStreamingContext } from '@/lib/copilot/request/context/request-context'
4744
import { buildToolCallSummaries } from '@/lib/copilot/request/context/result'
@@ -332,9 +329,9 @@ function schemaContentAction(
332329
}
333330

334331
const toolContentSelector: SelectedContentSelector = (path, key, value) => {
335-
if (path.length === 1 && key === 'description') return 'project'
336-
if (path.length === 1 && key === 'name') return 'verify'
337-
if (path.length === 1 && TOOL_SCHEMA_KEYS.has(key)) return 'traverse'
332+
if (path.length === 0 && key === 'description') return 'project'
333+
if (path.length === 0 && key === 'name') return 'verify'
334+
if (path.length === 0 && TOOL_SCHEMA_KEYS.has(key)) return 'traverse'
338335

339336
const schemaRootIndex = path.findIndex((segment) => TOOL_SCHEMA_KEYS.has(segment))
340337
if (schemaRootIndex >= 0) {
@@ -402,31 +399,27 @@ const desktopContentSelector: SelectedContentSelector = (_path, key, value) => {
402399
return value !== null && typeof value === 'object' ? 'traverse' : 'preserve'
403400
}
404401

405-
function isModelSafeToolPayload(
406-
candidate: unknown,
402+
function projectModelSafeToolPayloads(
403+
value: unknown,
407404
registry: ResolvedSecretTraceRegistry
408-
): boolean {
409-
if (!isPlainRecord(candidate) || typeof candidate.name !== 'string') return false
410-
if (!isResolvedSecretModelContentUnchanged(candidate.name, registry)) return false
405+
): unknown[] {
406+
if (!Array.isArray(value)) throw new CopilotModelContentProjectionError()
411407

412-
try {
413-
for (const schemaKey of TOOL_SCHEMA_KEYS) {
414-
if (!Object.hasOwn(candidate, schemaKey)) continue
415-
const guardedValues = collectModelVisibleSchemaContent(candidate[schemaKey]).guardedValues
416-
if (!isResolvedSecretModelContentUnchanged(guardedValues, registry)) return false
408+
const projected: unknown[] = []
409+
for (const candidate of value) {
410+
if (!isPlainRecord(candidate) || typeof candidate.name !== 'string') continue
411+
412+
try {
413+
projected.push(projectStructuredContent(candidate, registry, toolContentSelector, 'record'))
414+
} catch {
415+
// Tool definitions are independent protocol entities. Reject an unsafe definition without
416+
// turning the entire catalog into one synthetic projection value or failing safe siblings.
417417
}
418-
return true
419-
} catch {
420-
return false
421418
}
422-
}
423419

424-
function filterModelSafeToolPayloads(
425-
value: unknown,
426-
registry: ResolvedSecretTraceRegistry
427-
): unknown[] {
428-
if (!Array.isArray(value)) throw new CopilotModelContentProjectionError()
429-
return value.filter((candidate) => isModelSafeToolPayload(candidate, registry))
420+
// Projection completeness is a request-level invariant, even when every candidate was rejected.
421+
projectModelContent([], registry)
422+
return projected
430423
}
431424

432425
function hasModelSafeRoutingFields(
@@ -645,12 +638,7 @@ function projectInitialCopilotPayload(
645638
}
646639
for (const key of TOOL_PAYLOAD_KEYS) {
647640
if (Object.hasOwn(payload, key)) {
648-
projectedPayload[key] = projectStructuredContent(
649-
filterModelSafeToolPayloads(payload[key], registry),
650-
registry,
651-
toolContentSelector,
652-
'array'
653-
)
641+
projectedPayload[key] = projectModelSafeToolPayloads(payload[key], registry)
654642
}
655643
}
656644
if (Object.hasOwn(payload, 'responseFormat')) {

0 commit comments

Comments
 (0)