Skip to content

Commit d5b9e48

Browse files
committed
fix(mothership): preserve partial downloads during compilation
1 parent f67d81c commit d5b9e48

3 files changed

Lines changed: 70 additions & 1 deletion

File tree

‎apps/sim/lib/execution/sandbox/run-task.ts‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { AsyncLocalStorage } from 'node:async_hooks'
22
import { createLogger } from '@sim/logger'
33
import { generateShortId } from '@sim/utils/id'
4+
import { ContentRefusedError } from '@/lib/core/application'
45
import {
56
executeInIsolatedVM,
67
type IsolatedVMBrokerHandler,
@@ -65,8 +66,16 @@ export async function runSandboxTask<TInput extends SandboxTaskInput>(
6566
}
6667
const brokers: Record<string, IsolatedVMBrokerHandler> = {}
6768
const runInCallerContext = AsyncLocalStorage.snapshot()
69+
let brokerRefusal: ContentRefusedError | undefined
6870
for (const broker of task.brokers) {
69-
brokers[broker.name] = (args) => runInCallerContext(() => broker.handle(brokerContext, args))
71+
brokers[broker.name] = async (args) => {
72+
try {
73+
return await runInCallerContext(() => broker.handle(brokerContext, args))
74+
} catch (error) {
75+
if (error instanceof ContentRefusedError) brokerRefusal = error
76+
throw error
77+
}
78+
}
7079
}
7180

7281
const request: IsolatedVMExecutionRequest = {
@@ -124,6 +133,8 @@ export async function runSandboxTask<TInput extends SandboxTaskInput>(
124133
if (result.error.stack) err.stack = result.error.stack
125134
throw err
126135
}
136+
// IPC carries strings; retain a matching host refusal without reclassifying user-thrown errors.
137+
if (brokerRefusal && result.error.message === brokerRefusal.message) throw brokerRefusal
127138
throw new SandboxUserCodeError(
128139
result.error.message,
129140
result.error.name || 'SandboxTaskError',

‎apps/sim/lib/mothership/__integration__/content-delivery.integration.ts‎

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ import {
149149
} from '@/lib/uploads/contexts/workspace/workspace-file-manager'
150150
import type { WorkspaceFileSecretProvenance } from '@/lib/uploads/contexts/workspace/workspace-file-secret-provenance'
151151
import { WORKSPACE_FILES_DELEGATION_AUDIENCE } from '@/lib/workspace-files/application/authorization'
152+
import { downloadWorkspaceFileItems } from '@/lib/workspace-files/application/download-workspace-file-items'
152153
import { readWorkspaceFileContent } from '@/lib/workspace-files/application/read-workspace-file-content'
153154
import {
154155
getWorkspaceFileShares,
@@ -431,6 +432,59 @@ describe('content reaching Chat is admitted per item', () => {
431432
expect(read.body).toContain(fixture.files.entries)
432433
})
433434

435+
it.each(['uncached', 'cached', 'recovered', 'user-error', 'subsequent-error'] as const)(
436+
'keeps generated-document refusals per item (%s archive)',
437+
async (kind) => {
438+
const input = fixture.files.unknown
439+
const read = `await getFileBase64('${input}');`
440+
const code =
441+
kind === 'recovered'
442+
? `try { ${read} } catch {} pdf.addPage().drawText('safe fallback');`
443+
: kind === 'user-error'
444+
? `const error = new Error('File cannot be sent to a model because its secret provenance is unavailable'); error.name = 'ContentRefusedError'; throw error;`
445+
: kind === 'subsequent-error'
446+
? `try { ${read} } catch {} throw new Error('fixture compiler failure');`
447+
: `${read} pdf.addPage().drawText('fixture render');`
448+
const document = await uploadWorkspaceFile(
449+
ws(),
450+
fixture.ids.aliceId,
451+
Buffer.from(`const archiveFixture = '${generateId()}'; ${code}`),
452+
`archive-refusal-${generateId()}.pdf`,
453+
'text/x-python-pdf',
454+
{ notifyWorkspaceChange: false, secretProvenance: { status: 'exact', entries: [] } }
455+
)
456+
if (kind === 'cached') {
457+
const warmed = await downloadWorkspaceFileItems.execute({
458+
principal: createSessionPrincipal({ userId: fixture.ids.aliceId }),
459+
input: { workspaceId: ws(), fileIds: [document.id], folderIds: [] },
460+
})
461+
expect(warmed.renderedDocuments.get(document.id)?.subarray(0, 4).toString()).toBe('%PDF')
462+
}
463+
const route = `/api/v2/files/bulk-download?workspaceId=${ws()}&fileIds=`
464+
const mixed = await cli('GET', `${route}${fixture.files.clean},${document.id}`)
465+
if (kind === 'user-error' || kind === 'subsequent-error') {
466+
expect(mixed.status).toBe(500)
467+
expect(mixed.body).toContain('INTERNAL_ERROR')
468+
return
469+
}
470+
expect(mixed.status, mixed.body).toBe(200)
471+
expect(mixed.body).toContain(CANARY.clean)
472+
expect(mixed.body).not.toContain(CANARY.unknown)
473+
expect(mixed.registry.isPermanentlyIncomplete()).toBe(false)
474+
if (kind === 'recovered') {
475+
expect(mixed.body).toContain('%PDF')
476+
expect(mixed.body).not.toContain('sim-download-report.json')
477+
} else {
478+
expect(mixed.body).not.toContain('%PDF')
479+
expect(mixed.body).toContain('sim-download-report.json')
480+
expect(mixed.body).toContain(document.id)
481+
const alone = await cli('GET', `${route}${document.id}`)
482+
expect(alone.status, alone.body).toBe(403)
483+
expect(alone.body).not.toContain(CANARY.unknown)
484+
}
485+
}
486+
)
487+
434488
it('answers files search with locations and withheld unreadable previews (#5, misroute)', async () => {
435489
const read = await cli(
436490
'GET',

‎apps/sim/lib/workspace-files/application/download-workspace-file-items.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,10 @@ async function executeDownloadWorkspaceFileItems({
219219
renderedBytes += buffer.length
220220
renderedDocuments.set(file.id, buffer)
221221
} catch (error) {
222+
if (error instanceof ContentRefusedError) {
223+
filesToZip = filesToZip.filter((admitted) => admitted.id !== file.id)
224+
continue
225+
}
222226
if (error instanceof PayloadSizeLimitError) {
223227
validationError(
224228
allowance === MAX_RENDERED_DOCUMENT_BYTES

0 commit comments

Comments
 (0)