Skip to content

Commit d0eacf1

Browse files
committed
feat(files): add compiled-check endpoint and VFS path for binary document self-verification
1 parent 24d5306 commit d0eacf1

2 files changed

Lines changed: 144 additions & 1 deletion

File tree

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import { createLogger } from '@sim/logger'
2+
import { toError } from '@sim/utils/errors'
3+
import { type NextRequest, NextResponse } from 'next/server'
4+
import { getSession } from '@/lib/auth'
5+
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
6+
import { MAX_DOCUMENT_PREVIEW_CODE_BYTES } from '@/lib/execution/constants'
7+
import { runSandboxTask, SandboxUserCodeError } from '@/lib/execution/sandbox/run-task'
8+
import { downloadWorkspaceFile, getWorkspaceFile } from '@/lib/uploads/contexts/workspace'
9+
import { verifyWorkspaceMembership } from '@/app/api/workflows/utils'
10+
import type { SandboxTaskId } from '@/sandbox-tasks/registry'
11+
12+
export const dynamic = 'force-dynamic'
13+
export const runtime = 'nodejs'
14+
15+
const logger = createLogger('WorkspaceFileCompiledCheckAPI')
16+
17+
const EXT_TO_TASK: Record<string, SandboxTaskId> = {
18+
docx: 'docx-generate',
19+
pptx: 'pptx-generate',
20+
pdf: 'pdf-generate',
21+
}
22+
23+
/**
24+
* GET /api/workspaces/[id]/files/[fileId]/compiled-check
25+
*
26+
* Compiles the saved JavaScript source of a .docx / .pptx / .pdf file and
27+
* returns whether it succeeds. Used by the file agent to self-verify generated
28+
* code before finalising an edit.
29+
*
30+
* Returns:
31+
* 200 { ok: true }
32+
* 200 { ok: false, error: string, errorName: string } — user code error
33+
* 4xx on auth / missing file / unsupported extension
34+
* 500 on system (sandbox infra) failure
35+
*/
36+
export const GET = withRouteHandler(
37+
async (request: NextRequest, { params }: { params: Promise<{ id: string; fileId: string }> }) => {
38+
const { id: workspaceId, fileId } = await params
39+
40+
const session = await getSession()
41+
if (!session?.user?.id) {
42+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
43+
}
44+
45+
const membership = await verifyWorkspaceMembership(session.user.id, workspaceId)
46+
if (!membership) {
47+
return NextResponse.json({ error: 'Insufficient permissions' }, { status: 403 })
48+
}
49+
50+
const fileRecord = await getWorkspaceFile(workspaceId, fileId)
51+
if (!fileRecord) {
52+
return NextResponse.json({ error: 'File not found' }, { status: 404 })
53+
}
54+
55+
const ext = fileRecord.name.split('.').pop()?.toLowerCase() ?? ''
56+
const taskId = EXT_TO_TASK[ext]
57+
if (!taskId) {
58+
return NextResponse.json(
59+
{ error: `Compiled check only supports .docx, .pptx, and .pdf files` },
60+
{ status: 422 }
61+
)
62+
}
63+
64+
let buffer: Buffer
65+
try {
66+
buffer = await downloadWorkspaceFile(fileRecord)
67+
} catch (err) {
68+
logger.error('Failed to download file for compiled check', {
69+
fileId,
70+
error: toError(err).message,
71+
})
72+
return NextResponse.json({ error: 'Failed to read file' }, { status: 500 })
73+
}
74+
75+
const code = buffer.toString('utf-8')
76+
77+
if (Buffer.byteLength(code, 'utf-8') > MAX_DOCUMENT_PREVIEW_CODE_BYTES) {
78+
return NextResponse.json({ error: 'File source exceeds maximum size' }, { status: 413 })
79+
}
80+
81+
try {
82+
await runSandboxTask(taskId, { code, workspaceId }, { ownerKey: `user:${session.user.id}` })
83+
return NextResponse.json({ ok: true })
84+
} catch (err) {
85+
if (err instanceof SandboxUserCodeError) {
86+
logger.info('Compiled check failed with user code error', {
87+
fileId,
88+
taskId,
89+
error: toError(err).message,
90+
errorName: err.name,
91+
})
92+
return NextResponse.json({ ok: false, error: toError(err).message, errorName: err.name })
93+
}
94+
throw err
95+
}
96+
}
97+
)

apps/sim/lib/copilot/vfs/workspace-vfs.ts

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ import {
5858
getAccessibleOAuthCredentials,
5959
} from '@/lib/credentials/environment'
6060
import { getPersonalAndWorkspaceEnv } from '@/lib/environment/utils'
61+
import { runSandboxTask, SandboxUserCodeError } from '@/lib/execution/sandbox/run-task'
6162
import { getKnowledgeBases } from '@/lib/knowledge/service'
6263
import { listTables } from '@/lib/table/service'
6364
import {
@@ -79,6 +80,7 @@ import {
7980
} from '@/lib/workspaces/permissions/utils'
8081
import { getAllBlocks } from '@/blocks/registry'
8182
import { CONNECTOR_REGISTRY } from '@/connectors/registry'
83+
import type { SandboxTaskId } from '@/sandbox-tasks/registry'
8284
import { tools as toolRegistry } from '@/tools/registry'
8385
import { getLatestVersionTools, stripVersionSuffix } from '@/tools/utils'
8486
import { TRIGGER_REGISTRY } from '@/triggers/registry'
@@ -313,6 +315,8 @@ function getStaticComponentFiles(): Map<string, string> {
313315
* tables/{name}/meta.json
314316
* files/{name}/meta.json
315317
* files/by-id/{id}/meta.json
318+
* files/by-id/{id}/style (dynamic — OOXML theme/font extraction for .docx/.pptx)
319+
* files/by-id/{id}/compiled-check (dynamic — compile JS source via sandbox, returns {ok,error?})
316320
* jobs/{title}/meta.json
317321
* jobs/{title}/history.json
318322
* jobs/{title}/executions.json
@@ -451,10 +455,52 @@ export class WorkspaceVFS {
451455
/**
452456
* Attempt to read dynamic workspace file content from storage.
453457
* Handles images (base64), parseable documents (PDF, etc.), and text files.
454-
* Also handles `files/by-id/{id}/style` for OOXML theme/style extraction.
458+
* Also handles:
459+
* `files/by-id/{id}/style` — OOXML theme/style extraction (.docx / .pptx only)
460+
* `files/by-id/{id}/compiled-check` — sandbox compile check for JS-source binary files
455461
* Returns null if the path doesn't match `files/{name}` / `files/by-id/{id}` or the file isn't found.
456462
*/
457463
async readFileContent(path: string): Promise<FileReadResult | null> {
464+
// Handle compiled-check path: files/by-id/{id}/compiled-check
465+
const compiledCheckMatch = path.match(/^files\/by-id\/([^/]+)\/compiled-check$/)
466+
if (compiledCheckMatch) {
467+
const fileId = compiledCheckMatch[1]
468+
try {
469+
const record = await getWorkspaceFile(this._workspaceId, fileId)
470+
if (!record) return null
471+
const ext = record.name.split('.').pop()?.toLowerCase() ?? ''
472+
const EXT_TASK: Record<string, SandboxTaskId> = {
473+
docx: 'docx-generate',
474+
pptx: 'pptx-generate',
475+
pdf: 'pdf-generate',
476+
}
477+
const taskId = EXT_TASK[ext]
478+
if (!taskId) return null
479+
const buffer = await downloadWorkspaceFile(record)
480+
const code = buffer.toString('utf-8')
481+
let result: { ok: boolean; error?: string; errorName?: string }
482+
try {
483+
await runSandboxTask(taskId, { code, workspaceId: this._workspaceId })
484+
result = { ok: true }
485+
} catch (err) {
486+
if (err instanceof SandboxUserCodeError) {
487+
result = { ok: false, error: toError(err).message, errorName: err.name }
488+
} else {
489+
throw err
490+
}
491+
}
492+
const json = JSON.stringify(result)
493+
return { content: json, totalLines: 1 }
494+
} catch (err) {
495+
logger.warn('Compiled check failed via VFS', {
496+
workspaceId: this._workspaceId,
497+
fileId,
498+
error: toError(err).message,
499+
})
500+
return null
501+
}
502+
}
503+
458504
// Handle style extraction path: files/by-id/{id}/style
459505
const styleMatch = path.match(/^files\/by-id\/([^/]+)\/style$/)
460506
if (styleMatch) {

0 commit comments

Comments
 (0)