mirror of
https://github.com/n8n-io/n8n.git
synced 2026-06-05 02:59:27 +02:00
fix(core): Keep Instance AI builder sandboxes thread-scoped and non-ephemeral (#31745)
This commit is contained in:
parent
c74fc95a3b
commit
2993afb31d
|
|
@ -47,7 +47,6 @@ describe('createSandbox', () => {
|
|||
language: 'typescript',
|
||||
timeout: 60_000,
|
||||
createTimeoutSeconds: 900,
|
||||
ephemeral: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
|
@ -70,7 +69,6 @@ describe('createSandbox', () => {
|
|||
expect(getPrivateOptions(result)).toEqual(
|
||||
expect.objectContaining({
|
||||
createTimeoutSeconds: 300,
|
||||
ephemeral: true,
|
||||
labels: {
|
||||
'n8n-builder': 'instance-ai-thread-thread-1',
|
||||
thread_id: 'thread-1',
|
||||
|
|
|
|||
|
|
@ -120,7 +120,6 @@ export async function createSandbox(
|
|||
labels: config.labels,
|
||||
...(image ? { image } : {}),
|
||||
...(snapshot ? { snapshot } : {}),
|
||||
ephemeral: true,
|
||||
language: 'typescript',
|
||||
timeout: config.timeout ?? 300_000,
|
||||
createTimeoutSeconds: config.createTimeoutSeconds ?? 300,
|
||||
|
|
|
|||
|
|
@ -525,7 +525,6 @@ type WorkspaceServiceInternals = {
|
|||
threadId: string,
|
||||
user: User,
|
||||
context: InstanceAiContext,
|
||||
runId?: string,
|
||||
) => Promise<unknown>;
|
||||
};
|
||||
|
||||
|
|
@ -947,21 +946,15 @@ describe('InstanceAiService — runtime workspace setup', () => {
|
|||
(createWorkspace as jest.Mock).mockReturnValue(workspace);
|
||||
(setupSandboxWorkspace as jest.Mock).mockResolvedValue(undefined);
|
||||
|
||||
await service.getOrCreateWorkspace(
|
||||
'thread-1',
|
||||
fakeUser,
|
||||
{} as InstanceAiContext,
|
||||
'run_123456789',
|
||||
);
|
||||
await service.getOrCreateWorkspace('thread-1', fakeUser, {} as InstanceAiContext);
|
||||
|
||||
expect(createSandbox).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
id: 'acme-eval-run-1234-instance-ai-thread-thread-1',
|
||||
name: 'acme-eval-run-1234-instance-ai-thread-thread-1',
|
||||
id: 'acme-eval-instance-ai-thread-thread-1',
|
||||
name: 'acme-eval-instance-ai-thread-thread-1',
|
||||
labels: expect.objectContaining({
|
||||
'n8n-builder': 'instance-ai-thread-thread-1',
|
||||
name_prefix: 'Acme-Eval',
|
||||
run_id: 'run_123456789',
|
||||
thread_id: 'thread-1',
|
||||
}),
|
||||
}),
|
||||
|
|
@ -1173,11 +1166,10 @@ describe('InstanceAiService — runtime workspace setup', () => {
|
|||
expect(createSandbox).toHaveBeenCalledTimes(1);
|
||||
expect(createSandbox).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
id: 'run-1-instance-ai-thread-thread-1',
|
||||
name: 'run-1-instance-ai-thread-thread-1',
|
||||
id: 'instance-ai-thread-thread-1',
|
||||
name: 'instance-ai-thread-thread-1',
|
||||
labels: expect.objectContaining({
|
||||
'n8n-builder': 'instance-ai-thread-thread-1',
|
||||
run_id: 'run-1',
|
||||
thread_id: 'thread-1',
|
||||
}),
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -181,7 +181,6 @@ type RuntimeSandboxEntry = {
|
|||
const SANDBOX_NAME_MAX_LEN = 63;
|
||||
const SANDBOX_LABEL_MAX_LEN = 63;
|
||||
const NAME_PREFIX_SLUG_MAX_LEN = 24;
|
||||
const SHORT_RUN_ID_LEN = 8;
|
||||
const DEFAULT_SANDBOX_TTL_MS = 15 * 60 * 1000;
|
||||
|
||||
function slugifySandboxName(value: string, maxLen: number): string {
|
||||
|
|
@ -204,20 +203,12 @@ function getThreadScopedSandboxName(threadId: string): string {
|
|||
return `instance-ai-thread-${threadId}`;
|
||||
}
|
||||
|
||||
function buildThreadScopedSandboxName(
|
||||
threadId: string,
|
||||
namePrefix: string | undefined,
|
||||
runId: string | undefined,
|
||||
): string {
|
||||
function buildThreadScopedSandboxName(threadId: string, namePrefix: string | undefined): string {
|
||||
const parts: string[] = [];
|
||||
if (namePrefix) {
|
||||
const prefixSlug = slugifySandboxName(namePrefix, NAME_PREFIX_SLUG_MAX_LEN);
|
||||
if (prefixSlug) parts.push(prefixSlug);
|
||||
}
|
||||
if (runId) {
|
||||
const runSlug = slugifySandboxName(runId, SHORT_RUN_ID_LEN);
|
||||
if (runSlug) parts.push(runSlug);
|
||||
}
|
||||
const threadSlug = slugifySandboxName(getThreadScopedSandboxName(threadId), SANDBOX_NAME_MAX_LEN);
|
||||
if (threadSlug) parts.push(threadSlug);
|
||||
const name = slugifySandboxName(parts.join('-'), SANDBOX_NAME_MAX_LEN);
|
||||
|
|
@ -228,7 +219,6 @@ function buildThreadScopedSandboxName(
|
|||
function buildThreadScopedSandboxLabels(
|
||||
threadId: string,
|
||||
namePrefix: string | undefined,
|
||||
runId: string | undefined,
|
||||
): Record<string, string> {
|
||||
const baseName = getThreadScopedSandboxName(threadId);
|
||||
const labels: Record<string, string> = {
|
||||
|
|
@ -236,24 +226,19 @@ function buildThreadScopedSandboxLabels(
|
|||
thread_id: slugifySandboxLabel(threadId, SANDBOX_LABEL_MAX_LEN),
|
||||
};
|
||||
if (namePrefix) labels.name_prefix = slugifySandboxLabel(namePrefix, SANDBOX_LABEL_MAX_LEN);
|
||||
if (runId) labels.run_id = slugifySandboxLabel(runId, SANDBOX_LABEL_MAX_LEN);
|
||||
return labels;
|
||||
}
|
||||
|
||||
function withThreadScopedSandboxIdentity(
|
||||
config: SandboxConfig,
|
||||
threadId: string,
|
||||
runId?: string,
|
||||
): SandboxConfig {
|
||||
function withThreadScopedSandboxIdentity(config: SandboxConfig, threadId: string): SandboxConfig {
|
||||
if (!config.enabled || config.provider !== 'daytona') return config;
|
||||
|
||||
const name = buildThreadScopedSandboxName(threadId, config.namePrefix, runId);
|
||||
const name = buildThreadScopedSandboxName(threadId, config.namePrefix);
|
||||
return {
|
||||
...config,
|
||||
id: name,
|
||||
name,
|
||||
labels: {
|
||||
...buildThreadScopedSandboxLabels(threadId, config.namePrefix, runId),
|
||||
...buildThreadScopedSandboxLabels(threadId, config.namePrefix),
|
||||
...config.labels,
|
||||
},
|
||||
};
|
||||
|
|
@ -797,7 +782,6 @@ export class InstanceAiService {
|
|||
private async getOrCreateWorkspaceEntry(
|
||||
threadId: string,
|
||||
user: User,
|
||||
runId?: string,
|
||||
): Promise<RuntimeSandboxEntry | undefined> {
|
||||
const existing = this.sandboxes.get(threadId);
|
||||
if (existing) {
|
||||
|
|
@ -812,7 +796,7 @@ export class InstanceAiService {
|
|||
const pending = this.sandboxCreations.get(threadId);
|
||||
if (pending) return await pending;
|
||||
|
||||
const creation = this.createWorkspaceEntry(threadId, user, runId);
|
||||
const creation = this.createWorkspaceEntry(threadId, user);
|
||||
this.sandboxCreations.set(threadId, creation);
|
||||
try {
|
||||
return await creation;
|
||||
|
|
@ -826,9 +810,8 @@ export class InstanceAiService {
|
|||
threadId: string,
|
||||
user: User,
|
||||
context: InstanceAiContext,
|
||||
runId?: string,
|
||||
): Promise<RuntimeSandboxEntry | undefined> {
|
||||
const entry = await this.getOrCreateWorkspaceEntry(threadId, user, runId);
|
||||
const entry = await this.getOrCreateWorkspaceEntry(threadId, user);
|
||||
if (entry) await this.ensureWorkspaceSetup(entry, context);
|
||||
return entry;
|
||||
}
|
||||
|
|
@ -853,13 +836,8 @@ export class InstanceAiService {
|
|||
private async createWorkspaceEntry(
|
||||
threadId: string,
|
||||
user: User,
|
||||
runId?: string,
|
||||
): Promise<RuntimeSandboxEntry | undefined> {
|
||||
const config = withThreadScopedSandboxIdentity(
|
||||
await this.resolveSandboxConfig(user),
|
||||
threadId,
|
||||
runId,
|
||||
);
|
||||
const config = withThreadScopedSandboxIdentity(await this.resolveSandboxConfig(user), threadId);
|
||||
if (!config.enabled) return undefined;
|
||||
|
||||
const sandbox = await createSandbox(config, {
|
||||
|
|
@ -3066,7 +3044,7 @@ export class InstanceAiService {
|
|||
|
||||
let sandboxEntryPromise: Promise<RuntimeSandboxEntry | undefined> | undefined;
|
||||
const getSandboxEntry = async () => {
|
||||
sandboxEntryPromise ??= this.getOrCreateWorkspaceEntry(threadId, user, runId).catch(
|
||||
sandboxEntryPromise ??= this.getOrCreateWorkspaceEntry(threadId, user).catch(
|
||||
(error: unknown) => {
|
||||
sandboxEntryPromise = undefined;
|
||||
throw error;
|
||||
|
|
@ -3076,7 +3054,7 @@ export class InstanceAiService {
|
|||
return await sandboxEntryPromise;
|
||||
};
|
||||
const getSetupSandboxEntry = async () => {
|
||||
return await this.getOrCreateWorkspace(threadId, user, context, runId);
|
||||
return await this.getOrCreateWorkspace(threadId, user, context);
|
||||
};
|
||||
|
||||
const scopeWorkspaceForAgent = async (
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user