From 7c57843cf64461395634abadf8e0893b8e646328 Mon Sep 17 00:00:00 2001 From: Tomi Turtiainen <10324676+tomi@users.noreply.github.com> Date: Fri, 8 May 2026 11:32:02 +0300 Subject: [PATCH] refactor(ai-builder): Replace hand-rolled sandbox client with @n8n/sandbox-client SDK (no-changelog) (#29879) --- packages/@n8n/instance-ai/package.json | 1 + .../__tests__/builder-sandbox-factory.test.ts | 8 - .../__tests__/n8n-sandbox-client.test.ts | 152 ----- .../src/workspace/builder-sandbox-factory.ts | 10 - .../src/workspace/n8n-sandbox-client.ts | 539 ------------------ .../src/workspace/n8n-sandbox-filesystem.ts | 4 +- .../workspace/n8n-sandbox-image-manager.ts | 30 - .../src/workspace/n8n-sandbox-sandbox.ts | 16 +- pnpm-lock.yaml | 13 + 9 files changed, 21 insertions(+), 752 deletions(-) delete mode 100644 packages/@n8n/instance-ai/src/workspace/__tests__/n8n-sandbox-client.test.ts delete mode 100644 packages/@n8n/instance-ai/src/workspace/n8n-sandbox-client.ts delete mode 100644 packages/@n8n/instance-ai/src/workspace/n8n-sandbox-image-manager.ts diff --git a/packages/@n8n/instance-ai/package.json b/packages/@n8n/instance-ai/package.json index b42fbb515d6..70ee64b7189 100644 --- a/packages/@n8n/instance-ai/package.json +++ b/packages/@n8n/instance-ai/package.json @@ -42,6 +42,7 @@ "langsmith": "catalog:", "@mozilla/readability": "^0.6.0", "@n8n/api-types": "workspace:*", + "@n8n/sandbox-client": "0.0.1", "@n8n/utils": "workspace:*", "@n8n/workflow-sdk": "workspace:*", "linkedom": "^0.18.9", diff --git a/packages/@n8n/instance-ai/src/workspace/__tests__/builder-sandbox-factory.test.ts b/packages/@n8n/instance-ai/src/workspace/__tests__/builder-sandbox-factory.test.ts index e543a8f579b..4d1e1955e89 100644 --- a/packages/@n8n/instance-ai/src/workspace/__tests__/builder-sandbox-factory.test.ts +++ b/packages/@n8n/instance-ai/src/workspace/__tests__/builder-sandbox-factory.test.ts @@ -109,14 +109,6 @@ jest.mock('../n8n-sandbox-sandbox', () => ({ }, })); -jest.mock('../n8n-sandbox-image-manager', () => ({ - N8nSandboxImageManager: class { - getDockerfile() { - return 'FROM node:20'; - } - }, -})); - jest.mock('../pack-workspace-sdk', () => ({ packWorkspaceSdk: jest.fn().mockResolvedValue(null), isLinkWorkspaceSdkEnabled: jest.fn().mockReturnValue(false), diff --git a/packages/@n8n/instance-ai/src/workspace/__tests__/n8n-sandbox-client.test.ts b/packages/@n8n/instance-ai/src/workspace/__tests__/n8n-sandbox-client.test.ts deleted file mode 100644 index 58e53b25aad..00000000000 --- a/packages/@n8n/instance-ai/src/workspace/__tests__/n8n-sandbox-client.test.ts +++ /dev/null @@ -1,152 +0,0 @@ -import { - DockerfileStepsBuilder, - N8nSandboxClient, - N8nSandboxServiceError, -} from '../n8n-sandbox-client'; - -function createJsonResponse(body: unknown, init?: ResponseInit): Response { - return new Response(JSON.stringify(body), { - status: 200, - headers: { 'Content-Type': 'application/json' }, - ...init, - }); -} - -function bodyToRecord(body: unknown): Record { - if (typeof body !== 'string') { - throw new Error('Expected request body to be a JSON string'); - } - - try { - return JSON.parse(body) as Record; - } catch { - throw new Error('Expected request body to be valid JSON'); - } -} - -describe('N8nSandboxClient', () => { - const originalFetch = global.fetch; - - afterEach(() => { - global.fetch = originalFetch; - jest.restoreAllMocks(); - }); - - it('should send dockerfile_steps when DockerfileStepsBuilder is provided', async () => { - const fetchMock = jest.fn().mockResolvedValueOnce( - createJsonResponse({ - id: 'sandbox-1', - status: 'running', - provider: 'n8n-sandbox', - image_id: 'img-123', - created_at: 1, - last_active_at: 2, - }), - ) as jest.MockedFunction; - global.fetch = fetchMock; - - const client = new N8nSandboxClient({ - baseUrl: 'https://sandbox.example.com', - apiKey: 'sandbox-key', - }); - - const dockerfile = new DockerfileStepsBuilder() - .run('apt-get update') - .run('apt-get install -y git'); - - await client.createSandbox({ dockerfile }); - - const body = bodyToRecord(fetchMock.mock.calls[0]?.[1]?.body); - expect(body.dockerfile_steps).toEqual(['RUN apt-get update', 'RUN apt-get install -y git']); - }); - - it('should send no body when no options are provided', async () => { - const fetchMock = jest.fn().mockResolvedValueOnce( - createJsonResponse({ - id: 'sandbox-1', - status: 'running', - provider: 'n8n-sandbox', - image_id: '', - created_at: 1, - last_active_at: 2, - }), - ) as jest.MockedFunction; - global.fetch = fetchMock; - - const client = new N8nSandboxClient({ - baseUrl: 'https://sandbox.example.com', - apiKey: 'sandbox-key', - }); - - await client.createSandbox(); - - expect(fetchMock.mock.calls[0]?.[1]?.body).toBeUndefined(); - }); - - it('should parse streamed exec output', async () => { - const stream = new ReadableStream({ - start(controller) { - controller.enqueue( - new TextEncoder().encode( - '{"type":"stdout","data":"hello\\n"}\n' + - '{"type":"stderr","data":"warn\\n"}\n' + - '{"type":"exit","exit_code":0,"success":true,"execution_time_ms":42,"timed_out":false,"killed":false}\n', - ), - ); - controller.close(); - }, - }); - global.fetch = jest.fn().mockResolvedValue( - new Response(stream, { - status: 200, - headers: { 'Content-Type': 'application/x-ndjson' }, - }), - ) as jest.MockedFunction; - - const client = new N8nSandboxClient({ - baseUrl: 'https://sandbox.example.com', - apiKey: 'sandbox-key', - }); - - const stdoutChunks: string[] = []; - const stderrChunks: string[] = []; - const result = await client.exec('sandbox-1', { - command: 'echo hello', - onStdout: (data) => stdoutChunks.push(data), - onStderr: (data) => stderrChunks.push(data), - }); - - expect(result).toEqual({ - exitCode: 0, - stdout: 'hello\n', - stderr: 'warn\n', - executionTimeMs: 42, - timedOut: false, - killed: false, - success: true, - }); - expect(stdoutChunks).toEqual(['hello\n']); - expect(stderrChunks).toEqual(['warn\n']); - }); - - it('should convert JSON error responses into service errors', async () => { - global.fetch = jest.fn().mockResolvedValue( - createJsonResponse( - { - error: 'sandbox missing', - code: 404, - }, - { status: 404 }, - ), - ) as jest.MockedFunction; - - const client = new N8nSandboxClient({ - baseUrl: 'https://sandbox.example.com', - apiKey: 'sandbox-key', - }); - - await expect(client.getSandbox('sandbox-1')).rejects.toMatchObject( - new N8nSandboxServiceError('sandbox missing', 404, 404), - ); - }); -}); diff --git a/packages/@n8n/instance-ai/src/workspace/builder-sandbox-factory.ts b/packages/@n8n/instance-ai/src/workspace/builder-sandbox-factory.ts index 6fb3bca641a..d199823b6e0 100644 --- a/packages/@n8n/instance-ai/src/workspace/builder-sandbox-factory.ts +++ b/packages/@n8n/instance-ai/src/workspace/builder-sandbox-factory.ts @@ -17,7 +17,6 @@ import type { ErrorReporter, Logger } from '../logger'; import type { SandboxConfig } from './create-workspace'; import { DaytonaFilesystem } from './daytona-filesystem'; import { N8nSandboxFilesystem } from './n8n-sandbox-filesystem'; -import { N8nSandboxImageManager } from './n8n-sandbox-image-manager'; import { N8nSandboxServiceSandbox } from './n8n-sandbox-sandbox'; import { isLinkWorkspaceSdkEnabled, @@ -70,8 +69,6 @@ async function cleanupTrackedSandboxProcesses(workspace: Workspace): Promise { const config = this.assertIsN8nSandbox(); - const dockerfile = this.getN8nSandboxImageManager().getDockerfile(); const catalog = await this.getNodeCatalog(context); const sandbox = new N8nSandboxServiceSandbox({ apiKey: config.apiKey, serviceUrl: config.serviceUrl, timeout: config.timeout ?? 300_000, - dockerfile, }); const destroySandbox = async (): Promise => { diff --git a/packages/@n8n/instance-ai/src/workspace/n8n-sandbox-client.ts b/packages/@n8n/instance-ai/src/workspace/n8n-sandbox-client.ts deleted file mode 100644 index 4bd806830c2..00000000000 --- a/packages/@n8n/instance-ai/src/workspace/n8n-sandbox-client.ts +++ /dev/null @@ -1,539 +0,0 @@ -import type { FileContent } from '@mastra/core/workspace'; -import { z } from 'zod'; - -/** Error payload returned by the sandbox service. */ -export interface N8nSandboxServiceErrorPayload { - error: string; - code: number; -} - -export class N8nSandboxServiceError extends Error { - constructor( - message: string, - readonly status: number, - readonly code?: number, - ) { - super(message); - this.name = 'N8nSandboxServiceError'; - } -} - -/** Sandbox metadata exposed by the service API. */ -export interface N8nSandboxRecord { - id: string; - status: string; - provider: string; - imageId: string; - createdAt: number; - lastActiveAt: number; -} - -/** Directory entry returned by the service file listing API. */ -export interface N8nSandboxFileEntry { - name: string; - size: number; - isDir: boolean; - type: 'file' | 'directory'; - modTime: string; -} - -/** File stat payload mapped into Mastra-friendly shape. */ -export interface N8nSandboxFileStat { - name: string; - path: string; - type: 'file' | 'directory'; - size: number; - createdAt: string; - modifiedAt: string; -} - -/** Aggregated result of an execute-to-completion shell command. */ -export interface N8nSandboxExecResult { - exitCode: number; - stdout: string; - stderr: string; - executionTimeMs: number; - timedOut: boolean; - killed: boolean; - success: boolean; -} - -// ── Exec event schemas (streamed NDJSON from `/exec`) ──────────────────────── - -const execEventStdoutSchema = z.object({ type: z.literal('stdout'), data: z.string() }); -const execEventStderrSchema = z.object({ type: z.literal('stderr'), data: z.string() }); -const execEventExitSchema = z.object({ - type: z.literal('exit'), - exit_code: z.number(), - success: z.boolean(), - execution_time_ms: z.number(), - timed_out: z.boolean(), - killed: z.boolean(), -}); -const execEventErrorSchema = z.object({ type: z.literal('error'), error: z.string() }); - -const execEventSchema = z.discriminatedUnion('type', [ - execEventStdoutSchema, - execEventStderrSchema, - execEventExitSchema, - execEventErrorSchema, -]); - -type ExecEvent = z.infer; - -// ── Service response schemas ───────────────────────────────────────────────── - -const createSandboxResponseSchema = z.object({ - id: z.string(), - status: z.string(), - provider: z.string(), - image_id: z.string().optional(), - created_at: z.number(), - last_active_at: z.number(), -}); - -type CreateSandboxResponse = z.infer; - -const fileEntryResponseSchema = z.object({ - name: z.string(), - size: z.number(), - is_dir: z.boolean(), - type: z.enum(['file', 'directory']), - mod_time: z.string(), -}); - -type FileEntryResponse = z.infer; - -const fileStatResponseSchema = z.object({ - name: z.string(), - path: z.string(), - type: z.enum(['file', 'directory']), - size: z.number(), - created_at: z.string(), - modified_at: z.string(), -}); - -type FileStatResponse = z.infer; - -/** Client configuration for talking to the sandbox service. */ -export interface N8nSandboxClientOptions { - apiKey?: string; - baseUrl?: string; -} - -/** Fluent builder for constructing Dockerfile instructions sent at sandbox creation. */ -export class DockerfileStepsBuilder { - private readonly steps: string[] = []; - - /** Append one or more RUN instructions. */ - run(command: string | string[]): this { - const commands = Array.isArray(command) ? command : [command]; - for (const cmd of commands) { - this.steps.push(`RUN ${cmd}`); - } - return this; - } - - build(): string[] { - return [...this.steps]; - } -} - -/** Options used when creating a sandbox instance. */ -interface CreateSandboxOptions { - dockerfile?: DockerfileStepsBuilder; -} - -/** Command execution request sent to `/exec`. */ -interface N8nSandboxExecRequest { - command: string; - env?: Record; - workdir?: string; - timeoutMs?: number; - abortSignal?: AbortSignal; - onStdout?: (data: string) => void; - onStderr?: (data: string) => void; -} - -/** Exit metadata captured from the final `/exec` event. */ -interface ExecExitMeta { - exitCode: number; - executionTimeMs: number; - timedOut: boolean; - killed: boolean; - success: boolean; -} - -function normalizeBaseUrl(baseUrl?: string): string { - return (baseUrl ?? '').replace(/\/+$/, ''); -} - -function mapSandboxRecord(payload: CreateSandboxResponse): N8nSandboxRecord { - return { - id: payload.id, - status: payload.status, - provider: payload.provider, - imageId: payload.image_id ?? '', - createdAt: payload.created_at, - lastActiveAt: payload.last_active_at, - }; -} - -function asBuffer(content: FileContent): Buffer { - return typeof content === 'string' ? Buffer.from(content, 'utf-8') : Buffer.from(content); -} - -/** Yields parsed objects from an NDJSON ReadableStream, one per line. */ -async function* readNdjsonStream( - stream: ReadableStream, - parse: (line: string) => T, -): AsyncGenerator { - const decoder = new TextDecoder(); - let pending = ''; - - for await (const chunk of stream) { - pending += decoder.decode(chunk, { stream: true }); - let newlineIndex = pending.indexOf('\n'); - while (newlineIndex !== -1) { - const line = pending.slice(0, newlineIndex).trim(); - pending = pending.slice(newlineIndex + 1); - if (line.length > 0) { - yield parse(line); - } - newlineIndex = pending.indexOf('\n'); - } - } - - // Flush any remaining partial line - pending += decoder.decode(); - const last = pending.trim(); - if (last.length > 0) { - yield parse(last); - } -} - -function parseExecEvent(line: string): ExecEvent { - try { - const json: unknown = JSON.parse(line); - return execEventSchema.parse(json); - } catch { - return { type: 'error', error: 'Invalid exec event payload' }; - } -} - -/** - * Thin HTTP client for the n8n sandbox service. - * - * It handles sandbox lifecycle, file operations, streamed command execution, - * and lazy image instantiation for builder prewarming. - */ -export class N8nSandboxClient { - private readonly baseUrl: string; - - constructor(private readonly options: N8nSandboxClientOptions) { - this.baseUrl = normalizeBaseUrl(options.baseUrl); - } - - async createSandbox(options: CreateSandboxOptions = {}): Promise { - const body: Record = {}; - - const steps = options.dockerfile?.build(); - if (steps?.length) { - body.dockerfile_steps = steps; - } - - return mapSandboxRecord( - await this.requestJson('POST', '/sandboxes', { - body: Object.keys(body).length > 0 ? JSON.stringify(body) : undefined, - }), - ); - } - - async getSandbox(id: string): Promise { - return mapSandboxRecord( - await this.requestJson('GET', `/sandboxes/${id}`), - ); - } - - async deleteSandbox(id: string): Promise { - await this.expectSuccess(this.request('DELETE', `/sandboxes/${id}`)); - } - - async deleteImage(id: string): Promise { - await this.expectSuccess(this.request('DELETE', `/images/${id}`)); - } - - async exec(id: string, request: N8nSandboxExecRequest): Promise { - const response = await this.request('POST', `/sandboxes/${id}/exec`, { - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - command: request.command, - env: request.env, - workdir: request.workdir, - timeout_ms: request.timeoutMs, - }), - signal: request.abortSignal, - }); - - if (!response.ok) { - throw await this.toError(response); - } - - return await this.readExecResult(response, request); - } - - async readFile(id: string, path: string): Promise { - const response = await this.request('GET', `/sandboxes/${id}/files/content`, { - query: { path }, - }); - if (!response.ok) { - throw await this.toError(response); - } - - return Buffer.from(await response.arrayBuffer()); - } - - async writeFile(id: string, path: string, content: FileContent, overwrite = true): Promise { - await this.expectSuccess( - this.request('PUT', `/sandboxes/${id}/files`, { - query: { path, overwrite: String(overwrite) }, - headers: { 'Content-Type': 'application/octet-stream' }, - body: asBuffer(content), - }), - ); - } - - async appendFile(id: string, path: string, content: FileContent): Promise { - await this.expectSuccess( - this.request('POST', `/sandboxes/${id}/files`, { - query: { path }, - headers: { 'Content-Type': 'application/octet-stream' }, - body: asBuffer(content), - }), - ); - } - - async deleteFile( - id: string, - path: string, - options?: { recursive?: boolean; force?: boolean }, - ): Promise { - await this.expectSuccess( - this.request('DELETE', `/sandboxes/${id}/files`, { - query: { - path, - recursive: String(options?.recursive ?? false), - force: String(options?.force ?? false), - }, - }), - ); - } - - async copyFile( - id: string, - request: { src: string; dest: string; recursive?: boolean; overwrite?: boolean }, - ): Promise { - await this.expectSuccess( - this.request('POST', `/sandboxes/${id}/files/copy`, { - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - src: request.src, - dest: request.dest, - recursive: request.recursive ?? false, - overwrite: request.overwrite ?? false, - }), - }), - ); - } - - async moveFile( - id: string, - request: { src: string; dest: string; overwrite?: boolean }, - ): Promise { - await this.expectSuccess( - this.request('POST', `/sandboxes/${id}/files/move`, { - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - src: request.src, - dest: request.dest, - overwrite: request.overwrite ?? false, - }), - }), - ); - } - - async mkdir(id: string, path: string, recursive = false): Promise { - await this.expectSuccess( - this.request('POST', `/sandboxes/${id}/mkdir`, { - query: { path, recursive: String(recursive) }, - }), - ); - } - - async listFiles( - id: string, - request: { path?: string; recursive?: boolean; extension?: string } = {}, - ): Promise { - const payload = await this.requestJson('GET', `/sandboxes/${id}/files`, { - query: { - ...(request.path ? { path: request.path } : {}), - ...(request.recursive !== undefined ? { recursive: String(request.recursive) } : {}), - ...(request.extension ? { extension: request.extension } : {}), - }, - }); - - return payload.map((entry) => ({ - name: entry.name, - size: entry.size, - isDir: entry.is_dir, - type: entry.type, - modTime: entry.mod_time, - })); - } - - async stat(id: string, path: string): Promise { - const payload = await this.requestJson('GET', `/sandboxes/${id}/stat`, { - query: { path }, - }); - - return { - name: payload.name, - path: payload.path, - type: payload.type, - size: payload.size, - createdAt: payload.created_at, - modifiedAt: payload.modified_at, - }; - } - - private async readExecResult( - response: Response, - request: Pick, - ): Promise { - if (!response.body) { - throw new Error('Sandbox exec response body is not readable'); - } - - let stdout = ''; - let stderr = ''; - let exitMeta: ExecExitMeta | null = null; - - for await (const event of readNdjsonStream(response.body, parseExecEvent)) { - switch (event.type) { - case 'stdout': - stdout += event.data; - request.onStdout?.(event.data); - break; - case 'stderr': - stderr += event.data; - request.onStderr?.(event.data); - break; - case 'error': - throw new Error(event.error); - case 'exit': - exitMeta = { - exitCode: event.exit_code, - executionTimeMs: event.execution_time_ms, - timedOut: event.timed_out, - killed: event.killed, - success: event.success, - }; - break; - } - } - - const finalExitMeta = this.requireExecExitMeta(exitMeta); - return { - exitCode: finalExitMeta.exitCode, - stdout, - stderr, - executionTimeMs: finalExitMeta.executionTimeMs, - timedOut: finalExitMeta.timedOut, - killed: finalExitMeta.killed, - success: finalExitMeta.success, - }; - } - - private requireExecExitMeta(exitMeta: ExecExitMeta | null): ExecExitMeta { - if (!exitMeta) { - throw new Error('Sandbox exec stream ended without an exit event'); - } - - return exitMeta; - } - - private async expectSuccess(responsePromise: Promise): Promise { - const response = await responsePromise; - if (!response.ok) { - throw await this.toError(response); - } - } - - private async requestJson( - method: string, - path: string, - options: { - body?: string | Buffer; - headers?: Record; - query?: Record; - signal?: AbortSignal; - } = {}, - ): Promise { - const response = await this.request(method, path, options); - if (!response.ok) { - throw await this.toError(response); - } - - return (await response.json()) as T; - } - - private async request( - method: string, - path: string, - options: { - body?: string | Buffer; - headers?: Record; - query?: Record; - signal?: AbortSignal; - } = {}, - ): Promise { - if (!this.baseUrl) { - throw new Error('n8n sandbox service URL is not configured'); - } - - const url = new URL(`${this.baseUrl}${path}`); - for (const [key, value] of Object.entries(options.query ?? {})) { - url.searchParams.set(key, value); - } - - const headers = new Headers(options.headers); - if (this.options.apiKey) { - headers.set('X-Api-Key', this.options.apiKey); - } - - return await fetch(url, { - method, - headers, - body: options.body, - signal: options.signal, - }); - } - - private async toError(response: Response): Promise { - const contentType = response.headers.get('content-type') ?? ''; - if (contentType.includes('application/json')) { - const payload = (await response.json()) as Partial; - return new N8nSandboxServiceError( - payload.error ?? `Sandbox service request failed with status ${response.status}`, - response.status, - payload.code, - ); - } - - const text = await response.text(); - return new N8nSandboxServiceError( - text || `Sandbox service request failed with status ${response.status}`, - response.status, - ); - } -} diff --git a/packages/@n8n/instance-ai/src/workspace/n8n-sandbox-filesystem.ts b/packages/@n8n/instance-ai/src/workspace/n8n-sandbox-filesystem.ts index b370c6ac006..1b2b880fd77 100644 --- a/packages/@n8n/instance-ai/src/workspace/n8n-sandbox-filesystem.ts +++ b/packages/@n8n/instance-ai/src/workspace/n8n-sandbox-filesystem.ts @@ -10,9 +10,9 @@ import type { WriteOptions, } from '@mastra/core/workspace'; import { MastraFilesystem } from '@mastra/core/workspace'; +import { SandboxServiceError } from '@n8n/sandbox-client'; import { dirname } from 'node:path/posix'; -import { N8nSandboxServiceError } from './n8n-sandbox-client'; import type { N8nSandboxServiceSandbox } from './n8n-sandbox-sandbox'; function getParentDirectory(path: string): string | null { @@ -129,7 +129,7 @@ export class N8nSandboxFilesystem extends MastraFilesystem { await client.stat(sandboxId, path); return true; } catch (error) { - if (error instanceof N8nSandboxServiceError && error.status === 404) { + if (error instanceof SandboxServiceError && error.status === 404) { return false; } throw error; diff --git a/packages/@n8n/instance-ai/src/workspace/n8n-sandbox-image-manager.ts b/packages/@n8n/instance-ai/src/workspace/n8n-sandbox-image-manager.ts deleted file mode 100644 index 4cc5c85d64a..00000000000 --- a/packages/@n8n/instance-ai/src/workspace/n8n-sandbox-image-manager.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { DockerfileStepsBuilder } from './n8n-sandbox-client'; -import { - BUILD_MJS, - N8N_SANDBOX_WORKSPACE_ROOT, - PACKAGE_JSON, - TSCONFIG_JSON, -} from './sandbox-setup'; - -function b64(content: string): string { - return Buffer.from(content, 'utf-8').toString('base64'); -} - -const ROOT = N8N_SANDBOX_WORKSPACE_ROOT; - -export class N8nSandboxImageManager { - private cachedDockerfile: DockerfileStepsBuilder | null = null; - - getDockerfile(): DockerfileStepsBuilder { - if (this.cachedDockerfile) return this.cachedDockerfile; - - this.cachedDockerfile = new DockerfileStepsBuilder() - .run(`mkdir -p ${ROOT}/src ${ROOT}/chunks ${ROOT}/node-types`) - .run(`echo '${b64(PACKAGE_JSON)}' | base64 -d > ${ROOT}/package.json`) - .run(`echo '${b64(TSCONFIG_JSON)}' | base64 -d > ${ROOT}/tsconfig.json`) - .run(`echo '${b64(BUILD_MJS)}' | base64 -d > ${ROOT}/build.mjs`) - .run(`cd ${ROOT} && npm install --ignore-scripts`); - - return this.cachedDockerfile; - } -} diff --git a/packages/@n8n/instance-ai/src/workspace/n8n-sandbox-sandbox.ts b/packages/@n8n/instance-ai/src/workspace/n8n-sandbox-sandbox.ts index 63da0675ed8..bfaffedc75d 100644 --- a/packages/@n8n/instance-ai/src/workspace/n8n-sandbox-sandbox.ts +++ b/packages/@n8n/instance-ai/src/workspace/n8n-sandbox-sandbox.ts @@ -5,17 +5,15 @@ import type { ProviderStatus, SandboxInfo, } from '@mastra/core/workspace'; +import { SandboxClient } from '@n8n/sandbox-client'; import assert from 'node:assert/strict'; import { randomUUID } from 'node:crypto'; -import { N8nSandboxClient, type DockerfileStepsBuilder } from './n8n-sandbox-client'; - export interface N8nSandboxServiceSandboxOptions { id?: string; apiKey?: string; serviceUrl?: string; timeout?: number; - dockerfile?: DockerfileStepsBuilder; } function shellEscape(value: string): string { @@ -37,13 +35,13 @@ export class N8nSandboxServiceSandbox extends MastraSandbox { private readonly instanceId = `n8n-sandbox-${randomUUID()}`; - private readonly client: N8nSandboxClient; + private readonly client: SandboxClient; private sandboxId?: string; constructor(private readonly options: N8nSandboxServiceSandboxOptions) { super({ name: 'N8nSandboxServiceSandbox' }); - this.client = new N8nSandboxClient({ + this.client = new SandboxClient({ apiKey: options.apiKey, baseUrl: options.serviceUrl, }); @@ -60,9 +58,7 @@ export class N8nSandboxServiceSandbox extends MastraSandbox { return; } - const sandbox = await this.client.createSandbox({ - dockerfile: this.options.dockerfile, - }); + const sandbox = await this.client.createSandbox(); this.sandboxId = sandbox.id; } @@ -83,8 +79,6 @@ export class N8nSandboxServiceSandbox extends MastraSandbox { lastUsedAt: new Date(sandbox.lastActiveAt * 1000), metadata: { remoteStatus: sandbox.status, - imageId: sandbox.imageId, - remoteProvider: sandbox.provider, }, }; } @@ -118,7 +112,7 @@ export class N8nSandboxServiceSandbox extends MastraSandbox { }; } - getClient(): N8nSandboxClient { + getClient(): SandboxClient { return this.client; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 40e696ca843..ccb86a34744 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1703,6 +1703,9 @@ importers: '@n8n/api-types': specifier: workspace:* version: link:../api-types + '@n8n/sandbox-client': + specifier: 0.0.1 + version: 0.0.1 '@n8n/utils': specifier: workspace:* version: link:../utils @@ -8402,6 +8405,10 @@ packages: resolution: {integrity: sha512-qmDvJIjcNsZ6tXWy2G9yuCgMPTTn35GMA3dPpSLm7QJVpbQzYdw0ALy1bKoivXnEM3U93/OrK+/M719b+fg84Q==} engines: {node: '>=18'} + '@n8n/sandbox-client@0.0.1': + resolution: {integrity: sha512-qvfc8/qv+rPz0nsM/r0pL/tWm5Rcry3X/d80/uxobBkWDtjZs9cFj10NBzU8+yrSs8F3dsk08FIWyVCzSt9jKA==} + engines: {node: '>=22.16', pnpm: '>=10.22.0'} + '@n8n/typeorm@0.3.20-16': resolution: {integrity: sha512-XEfVKqbkDkLhU0tn3/zUvolDA/8u6/khDxP4pPvGKv68kPxC2h25eesszmXtfIKBWosE/8sAOOTtowA1b4jFYQ==} engines: {node: '>=16.13.0'} @@ -28301,6 +28308,12 @@ snapshots: outvariant: 1.4.3 strict-event-emitter: 0.5.1 + '@n8n/sandbox-client@0.0.1': + dependencies: + axios: 1.16.0 + transitivePeerDependencies: + - debug + '@n8n/typeorm@0.3.20-16(@sentry/node@10.36.0)(mysql2@3.17.0)(pg@8.17.0)(sqlite3@5.1.7)': dependencies: app-root-path: 3.1.0