Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions apps/sim/app/api/copilot/tools/execute/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,4 +125,52 @@ describe('POST /api/copilot/tools/execute (in-band)', () => {
)
expect(mockPrepareEnvironmentContext).toHaveBeenCalledTimes(1)
})

/**
* The generated key is a browser-only artifact. This lane's response is what Go feeds the
* model, so it must carry the same projection the resume lane produces — the status message
* alone. A `key` field here would put the plaintext credential in model context.
*/
it('returns only the status message for generate_api_key, never the key', async () => {
const message = 'API key "demo" created. You did NOT receive the key value'
mockHandler.mockResolvedValue({
success: true,
output: { id: 'key-1', name: 'demo', key: 'sk_live_plaintext', workspaceId: 'ws-1', message },
})

const res = await POST(
makeRequest({
...BASE_BODY,
toolName: 'generate_api_key',
params: { name: 'demo' },
messageId: 'msg-api-key',
}) as never
)
const body = await res.json()

expect(body).toEqual({ success: true, output: message })
expect(JSON.stringify(body)).not.toContain('sk_live_plaintext')
})

it('leaves a non-generate_api_key result carrying a key field untouched', async () => {
mockHandler.mockResolvedValue({ success: true, output: { key: 'lookup-key', value: 42 } })
const res = await POST(makeRequest({ ...BASE_BODY, messageId: 'msg-other-tool-key' }) as never)
await expect(res.json()).resolves.toEqual({
success: true,
output: { key: 'lookup-key', value: 42 },
})
})

it('passes a failed generate_api_key call through with its error', async () => {
mockHandler.mockResolvedValue({ success: false, error: 'name is required' })
const res = await POST(
makeRequest({
...BASE_BODY,
toolName: 'generate_api_key',
params: {},
messageId: 'msg-api-key-error',
}) as never
)
await expect(res.json()).resolves.toEqual({ success: false, error: 'name is required' })
})
})
11 changes: 10 additions & 1 deletion apps/sim/app/api/copilot/tools/execute/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { getErrorMessage } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { copilotToolExecuteInternalBodySchema } from '@/lib/api/contracts/copilot'
import { validationErrorResponse } from '@/lib/api/server'
import { toolResultForModel } from '@/lib/copilot/chat/sim-key-redaction'
import { prepareCopilotEnvironmentContext } from '@/lib/copilot/environment-context'
import { MothershipStreamV1ToolOutcome } from '@/lib/copilot/generated/mothership-stream-v1'
import { TraceAttr } from '@/lib/copilot/generated/trace-attributes-v1'
Expand Down Expand Up @@ -243,9 +244,17 @@ export const POST = withRouteHandler((request: NextRequest) =>
})
})
}
/**
* The response IS the model-facing channel on this lane — Go relays it straight into
* the turn — so it carries the same projection the resume lane's
* `getToolCallTerminalData` produces, not the raw handler output. Without this,
* `generate_api_key`'s freshly minted plaintext key crossed to the model here while
* the redaction held on the other lane. Every other tool is returned unchanged.
*/
const modelOutput = toolResultForModel(toolName, projected.output)
return NextResponse.json({
success: projected.success,
...(projected.output !== undefined ? { output: projected.output } : {}),
...(modelOutput !== undefined ? { output: modelOutput } : {}),
...(projected.error ? { error: projected.error } : {}),
})
} catch (err) {
Expand Down
Loading