diff --git a/.changeset/daytona-sandbox-create-on-error.md b/.changeset/daytona-sandbox-create-on-error.md new file mode 100644 index 0000000000..15826afa50 --- /dev/null +++ b/.changeset/daytona-sandbox-create-on-error.md @@ -0,0 +1,6 @@ +--- +'@truefoundry/trueforge-core': patch +'@truefoundry/trueforge': patch +--- + +Surface Daytona sandbox create failures through a host `onError` hook (authz still ignored for P1) instead of a provider-owned critical alert callback. diff --git a/packages/trueforge-core/src/core/sandbox/provider/DaytonaProvider.ts b/packages/trueforge-core/src/core/sandbox/provider/DaytonaProvider.ts index c8376d311f..7c35f9a24b 100644 --- a/packages/trueforge-core/src/core/sandbox/provider/DaytonaProvider.ts +++ b/packages/trueforge-core/src/core/sandbox/provider/DaytonaProvider.ts @@ -81,8 +81,11 @@ export interface DaytonaSandboxProviderOptions { /** Defaults to 1 hour (same as the gateway's max agent execution time). */ previewUrlExpirySeconds?: number; logger: Logger; - /** Host alert hook (e.g. Sentry P1). */ - onCriticalAlert?: (error: Error) => void; + /** + * Host hook for fresh-create failures only (wired through getOrCreateSandbox). + * Other provider catch paths do not call this; the host decides what to alert on. + */ + onError?: (error: Error) => void; } export class DaytonaSandboxProvider implements SandboxProvider { @@ -99,7 +102,7 @@ export class DaytonaSandboxProvider implements SandboxProvider void) | undefined; + private readonly onError: ((error: Error) => void) | undefined; private readonly daytona: Daytona; private static readonly cachedSandboxes = new Map(); // De-dupes concurrent recovery attempts on the same sandbox to a single refreshData+start round-trip. @@ -117,7 +120,7 @@ export class DaytonaSandboxProvider implements SandboxProvider { + /** New create: reject an inactive tip before calling Daytona create. */ + private async createNewSandbox({ + environment, + onError, + }: { + environment?: DaytonaSandboxEnvironment; + onError?: (error: Error) => void; + }): Promise { const env = this.requireEnvironment(environment); try { await this.rejectIfSnapshotInactive(env.snapshot_ref); return await this.daytona.create(this.buildCreateParams(env)); } catch (error) { - if (error instanceof DaytonaError && (error.statusCode === 401 || error.statusCode === 403)) { - throw error; - } - const alertError = error instanceof Error ? error : new Error('Daytona sandbox create failed', { cause: error }); + const createError = error instanceof Error ? error : new Error('Daytona sandbox create failed', { cause: error }); this.logger.error('Daytona sandbox create failed', { snapshot_ref: env.snapshot_ref, - ...extractErrorLogFields(alertError), + ...extractErrorLogFields(createError), }); - this.onCriticalAlert?.(alertError); - throw error; + onError?.(createError); + throw createError; } } - private async getOrCreateSandbox( - sandboxId?: string, - environment?: DaytonaSandboxEnvironment, - ): Promise<{ sandbox: Sandbox; defaultTimeoutMs: number }> { + private async getOrCreateSandbox({ + sandboxId, + environment, + onError, + }: { + sandboxId?: string; + environment?: DaytonaSandboxEnvironment; + /** Only used on the fresh-create path; restore/exec paths omit this. */ + onError?: (error: Error) => void; + }): Promise<{ sandbox: Sandbox; defaultTimeoutMs: number }> { if (sandboxId) { validateSandboxOwnedByTenant({ sandboxId, tenantName: this.tenantName }); const cached = DaytonaSandboxProvider.cachedSandboxes.get(sandboxId); @@ -374,7 +386,7 @@ export class DaytonaSandboxProvider implements SandboxProvider { return context.with(suppressTracing(context.active()), async () => { - const { sandbox } = await this.getOrCreateSandbox(undefined, environment); + const { sandbox } = await this.getOrCreateSandbox({ + environment, + onError: this.onError, + }); this.logger.debug(`Sandbox created: name=${sandbox.name}`); return { sandboxId: sandbox.name }; }); @@ -486,7 +501,7 @@ export class DaytonaSandboxProvider implements SandboxProvider => { try { return await this.executeWithSandboxRecovery(params.sandboxId, async () => { - const { sandbox, defaultTimeoutMs } = await this.getOrCreateSandbox(params.sandboxId); + const { sandbox, defaultTimeoutMs } = await this.getOrCreateSandbox({ sandboxId: params.sandboxId }); const response = await sandbox.process.executeCommand( params.command, params.cwd, @@ -519,7 +534,7 @@ export class DaytonaSandboxProvider implements SandboxProvider { try { return await this.executeWithSandboxRecovery(params.sandboxId, async () => { - const { sandbox } = await this.getOrCreateSandbox(params.sandboxId); + const { sandbox } = await this.getOrCreateSandbox({ sandboxId: params.sandboxId }); const info = await this.getFileInfo(sandbox, params.path); if (info.isDir) { @@ -548,7 +563,7 @@ export class DaytonaSandboxProvider implements SandboxProvider { try { await this.executeWithSandboxRecovery(params.sandboxId, async () => { - const { sandbox } = await this.getOrCreateSandbox(params.sandboxId); + const { sandbox } = await this.getOrCreateSandbox({ sandboxId: params.sandboxId }); await sandbox.fs.uploadFile(params.content, params.remotePath); }); } catch (e: unknown) { @@ -563,7 +578,7 @@ export class DaytonaSandboxProvider implements SandboxProvider { try { return await this.executeWithSandboxRecovery(params.sandboxId, async () => { - const { sandbox } = await this.getOrCreateSandbox(params.sandboxId); + const { sandbox } = await this.getOrCreateSandbox({ sandboxId: params.sandboxId }); const signed = await sandbox.getSignedPreviewUrl(params.port, params.expiresInSeconds); return signed.url; }); diff --git a/packages/trueforge/src/sandbox/providerUtils.ts b/packages/trueforge/src/sandbox/providerUtils.ts index cbf74bac47..312e4dae9d 100644 --- a/packages/trueforge/src/sandbox/providerUtils.ts +++ b/packages/trueforge/src/sandbox/providerUtils.ts @@ -104,7 +104,10 @@ export function toDaytonaSandboxProvider({ tenantName: tenant_id, fileMaxBytesForDownload: configuration.SANDBOX_FILE_MAX_BYTES_FOR_DOWNLOAD, logger, - onCriticalAlert: error => { + onError: error => { + if (isDaytonaAuthError(error) || isDaytonaPermissionError(error)) { + return; + } captureCriticalException(error, { tags: { module: 'DaytonaProvider', operation: 'sandboxCreate', priority: 'P1' }, });