mirror of
https://github.com/n8n-io/n8n.git
synced 2026-05-12 16:10:30 +02:00
fix(core): Fix daytona proxy bug (#27974)
Some checks failed
CI: Master (Build, Test, Lint) / Build for Github Cache (push) Waiting to run
CI: Master (Build, Test, Lint) / Unit tests (22.x) (push) Waiting to run
CI: Master (Build, Test, Lint) / Unit tests (24.14.1) (push) Waiting to run
CI: Master (Build, Test, Lint) / Unit tests (25.x) (push) Waiting to run
CI: Master (Build, Test, Lint) / Lint (push) Waiting to run
CI: Master (Build, Test, Lint) / Performance (push) Waiting to run
CI: Master (Build, Test, Lint) / Notify Slack on failure (push) Blocked by required conditions
Build: Benchmark Image / build (push) Has been cancelled
Util: Sync API Docs / sync-public-api (push) Has been cancelled
Release: Schedule Patch Release PRs / Create patch release PR (${{ matrix.track }}) (beta) (push) Has been cancelled
Release: Schedule Patch Release PRs / Create patch release PR (${{ matrix.track }}) (stable) (push) Has been cancelled
Release: Schedule Patch Release PRs / Create patch release PR (${{ matrix.track }}) (v1) (push) Has been cancelled
Some checks failed
CI: Master (Build, Test, Lint) / Build for Github Cache (push) Waiting to run
CI: Master (Build, Test, Lint) / Unit tests (22.x) (push) Waiting to run
CI: Master (Build, Test, Lint) / Unit tests (24.14.1) (push) Waiting to run
CI: Master (Build, Test, Lint) / Unit tests (25.x) (push) Waiting to run
CI: Master (Build, Test, Lint) / Lint (push) Waiting to run
CI: Master (Build, Test, Lint) / Performance (push) Waiting to run
CI: Master (Build, Test, Lint) / Notify Slack on failure (push) Blocked by required conditions
Build: Benchmark Image / build (push) Has been cancelled
Util: Sync API Docs / sync-public-api (push) Has been cancelled
Release: Schedule Patch Release PRs / Create patch release PR (${{ matrix.track }}) (beta) (push) Has been cancelled
Release: Schedule Patch Release PRs / Create patch release PR (${{ matrix.track }}) (stable) (push) Has been cancelled
Release: Schedule Patch Release PRs / Create patch release PR (${{ matrix.track }}) (v1) (push) Has been cancelled
Co-authored-by: Mutasem Aldmour <mutasem@n8n.io>
This commit is contained in:
parent
663f2c5086
commit
c754724caf
|
|
@ -104,15 +104,15 @@ describe('createSandbox', () => {
|
|||
process.env.NODE_ENV = originalEnv;
|
||||
});
|
||||
|
||||
it('should return undefined when sandbox is disabled', () => {
|
||||
it('should return undefined when sandbox is disabled', async () => {
|
||||
const config: SandboxConfig = { enabled: false, provider: 'local' };
|
||||
|
||||
const result = createSandbox(config);
|
||||
const result = await createSandbox(config);
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return a DaytonaSandbox for "daytona" provider', () => {
|
||||
it('should return a DaytonaSandbox for "daytona" provider', async () => {
|
||||
const config: SandboxConfig = {
|
||||
enabled: true,
|
||||
provider: 'daytona',
|
||||
|
|
@ -122,7 +122,7 @@ describe('createSandbox', () => {
|
|||
timeout: 60_000,
|
||||
};
|
||||
|
||||
const result = createSandbox(config);
|
||||
const result = await createSandbox(config);
|
||||
|
||||
expect(result).toBeInstanceOf(DaytonaSandbox);
|
||||
expect((result as unknown as MockWithOpts<Record<string, unknown>>).opts).toEqual(
|
||||
|
|
@ -136,25 +136,47 @@ describe('createSandbox', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('should use default timeout of 300_000 for "daytona" provider when not specified', () => {
|
||||
it('should resolve apiKey via getAuthToken in proxy mode', async () => {
|
||||
const getAuthToken = jest.fn().mockResolvedValue('jwt-token-123');
|
||||
const config: SandboxConfig = {
|
||||
enabled: true,
|
||||
provider: 'daytona',
|
||||
daytonaApiUrl: 'https://proxy.example.com',
|
||||
getAuthToken,
|
||||
timeout: 60_000,
|
||||
};
|
||||
|
||||
const result = await createSandbox(config);
|
||||
|
||||
expect(getAuthToken).toHaveBeenCalledTimes(1);
|
||||
expect(result).toBeInstanceOf(DaytonaSandbox);
|
||||
expect((result as unknown as MockWithOpts<Record<string, unknown>>).opts).toEqual(
|
||||
expect.objectContaining({
|
||||
apiKey: 'jwt-token-123',
|
||||
apiUrl: 'https://proxy.example.com',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should use default timeout of 300_000 for "daytona" provider when not specified', async () => {
|
||||
const config: SandboxConfig = {
|
||||
enabled: true,
|
||||
provider: 'daytona',
|
||||
};
|
||||
|
||||
const result = createSandbox(config);
|
||||
const result = await createSandbox(config);
|
||||
|
||||
expect(result).toBeInstanceOf(DaytonaSandbox);
|
||||
expect((result as unknown as MockWithOpts<Record<string, unknown>>).opts.timeout).toBe(300_000);
|
||||
});
|
||||
|
||||
it('should not include image in DaytonaSandbox config when not specified', () => {
|
||||
it('should not include image in DaytonaSandbox config when not specified', async () => {
|
||||
const config: SandboxConfig = {
|
||||
enabled: true,
|
||||
provider: 'daytona',
|
||||
};
|
||||
|
||||
const result = createSandbox(config);
|
||||
const result = await createSandbox(config);
|
||||
|
||||
expect(result).toBeInstanceOf(DaytonaSandbox);
|
||||
expect((result as unknown as MockWithOpts<Record<string, unknown>>).opts).not.toHaveProperty(
|
||||
|
|
@ -162,11 +184,11 @@ describe('createSandbox', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('should return a LocalSandbox for "local" provider in non-production', () => {
|
||||
it('should return a LocalSandbox for "local" provider in non-production', async () => {
|
||||
process.env.NODE_ENV = 'development';
|
||||
const config: SandboxConfig = { enabled: true, provider: 'local' };
|
||||
|
||||
const result = createSandbox(config);
|
||||
const result = await createSandbox(config);
|
||||
|
||||
expect(result).toBeInstanceOf(LocalSandbox);
|
||||
expect((result as unknown as MockWithOpts<{ workingDirectory: string }>).opts).toEqual({
|
||||
|
|
@ -174,16 +196,16 @@ describe('createSandbox', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('should throw in production when provider is "local"', () => {
|
||||
it('should throw in production when provider is "local"', async () => {
|
||||
process.env.NODE_ENV = 'production';
|
||||
const config: SandboxConfig = { enabled: true, provider: 'local' };
|
||||
|
||||
expect(() => createSandbox(config)).toThrow(
|
||||
await expect(createSandbox(config)).rejects.toThrow(
|
||||
'LocalSandbox (provider: "local") is not allowed in production. Use "daytona" provider for isolated sandbox execution.',
|
||||
);
|
||||
});
|
||||
|
||||
it('should return an N8nSandboxServiceSandbox for "n8n-sandbox" provider', () => {
|
||||
it('should return an N8nSandboxServiceSandbox for "n8n-sandbox" provider', async () => {
|
||||
const config: SandboxConfig = {
|
||||
enabled: true,
|
||||
provider: 'n8n-sandbox',
|
||||
|
|
@ -192,7 +214,7 @@ describe('createSandbox', () => {
|
|||
timeout: 45_000,
|
||||
};
|
||||
|
||||
const result = createSandbox(config);
|
||||
const result = await createSandbox(config);
|
||||
|
||||
expect(result).toBeInstanceOf(N8nSandboxServiceSandbox);
|
||||
expect((result as unknown as MockWithOpts<Record<string, unknown>>).opts).toEqual({
|
||||
|
|
|
|||
|
|
@ -51,14 +51,16 @@ export type SandboxConfig =
|
|||
* - 'daytona': Isolated Docker container via Daytona API (production)
|
||||
* - 'local': Direct host execution via LocalSandbox (development only, no isolation)
|
||||
*/
|
||||
export function createSandbox(
|
||||
export async function createSandbox(
|
||||
config: SandboxConfig,
|
||||
): DaytonaSandbox | LocalSandbox | N8nSandboxServiceSandbox | undefined {
|
||||
): Promise<DaytonaSandbox | LocalSandbox | N8nSandboxServiceSandbox | undefined> {
|
||||
if (!config.enabled) return undefined;
|
||||
|
||||
if (config.provider === 'daytona') {
|
||||
// In proxy mode, resolve a fresh token via getAuthToken; in direct mode use the static key.
|
||||
const apiKey = config.getAuthToken ? await config.getAuthToken() : config.daytonaApiKey;
|
||||
return new DaytonaSandbox({
|
||||
apiKey: config.daytonaApiKey,
|
||||
apiKey,
|
||||
apiUrl: config.daytonaApiUrl,
|
||||
...(config.image ? { image: config.image } : {}),
|
||||
language: 'typescript',
|
||||
|
|
|
|||
|
|
@ -123,7 +123,10 @@ export class InstanceAiService {
|
|||
/** Active sandboxes keyed by thread ID — persisted across messages within a conversation. */
|
||||
private readonly sandboxes = new Map<
|
||||
string,
|
||||
{ sandbox: ReturnType<typeof createSandbox>; workspace: ReturnType<typeof createWorkspace> }
|
||||
{
|
||||
sandbox: Awaited<ReturnType<typeof createSandbox>>;
|
||||
workspace: ReturnType<typeof createWorkspace>;
|
||||
}
|
||||
>();
|
||||
|
||||
/** Singleton local filesystem provider — created lazily when filesystem config is enabled. */
|
||||
|
|
@ -318,7 +321,7 @@ export class InstanceAiService {
|
|||
const config = await this.resolveSandboxConfig(user);
|
||||
if (!config.enabled) return undefined;
|
||||
|
||||
const sandbox = createSandbox(config);
|
||||
const sandbox = await createSandbox(config);
|
||||
const workspace = createWorkspace(sandbox);
|
||||
if (!sandbox || !workspace) return undefined;
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user