fix(core): Keep Instance AI builder sandboxes thread-scoped and non-ephemeral (#31745)

This commit is contained in:
Jaakko Husso 2026-06-04 16:36:17 +03:00 committed by GitHub
parent c74fc95a3b
commit 2993afb31d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 14 additions and 47 deletions

View File

@ -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',

View File

@ -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,

View File

@ -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',
}),
}),

View File

@ -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 (