feat(serverless): add basic sandbox isolation and flexible driver options (#17176)

## Overview
- Add a DISABLED serverless driver to explicitly turn off execution
- Clarify self-hosting docs with driver options and recommended usage
- Keep integration coverage for serverless function execution (default +
external package example)

## Notes
- Local driver remains the default for development usage; Lambda or
Disabled recommended for production deployments
- No functional changes to Lambda execution

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> Introduces flexible serverless execution modes and safer local
execution.
> 
> - **New driver:** `DISABLED` serverless driver with wiring in
`serverless.interface`, factory, module provider, and GraphQL exception
mapping; new exception code `SERVERLESS_FUNCTION_DISABLED`.
> - **Local driver hardening:** Strip `NODE_OPTIONS` when spawning child
processes; cleanup promise signature; better log capture.
> - **Dependency build reliability:** Use `execFile` with bundled Yarn
(`.yarn/releases/yarn-4.9.2.cjs`), strip `NODE_OPTIONS`, improved error
messages, and parallel cleanup excluding `node_modules`.
> - **Docs:** Add serverless section detailing `SERVERLESS_TYPE` options
(LOCAL, LAMBDA, DISABLED), security notice, and recommended configs.
> - **Config/env:** Default
`IS_WORKSPACE_CREATION_LIMITED_TO_SERVER_ADMINS` set to `true`
(examples/tests default `false`); sample envs updated.
> - **Tests:** Add integration tests and GraphQL helpers for creating,
updating, publishing, executing, and deleting serverless functions,
including external package usage and error paths.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
1a2958cc19cff1b0108c51b83095bbf95e75d931. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
This commit is contained in:
Félix Malfait
2026-01-16 15:54:44 +01:00
committed by GitHub
parent 9620961f16
commit 6a709d9c50
21 changed files with 677 additions and 22 deletions
@@ -0,0 +1,22 @@
import {
type ServerlessDriver,
type ServerlessExecuteResult,
} from 'src/engine/core-modules/serverless/drivers/interfaces/serverless-driver.interface';
import {
ServerlessFunctionException,
ServerlessFunctionExceptionCode,
} from 'src/engine/metadata-modules/serverless-function/serverless-function.exception';
export class DisabledDriver implements ServerlessDriver {
async delete(): Promise<void> {
// No-op when disabled
}
async execute(): Promise<ServerlessExecuteResult> {
throw new ServerlessFunctionException(
'Serverless function execution is disabled. Set SERVERLESS_TYPE to LOCAL or LAMBDA to enable.',
ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_DISABLED,
);
}
}
@@ -284,9 +284,13 @@ export class LocalDriver implements ServerlessDriver {
stack?: string;
stdout: string;
stderr: string;
}>((resolve, _) => {
}>((resolve) => {
// Strip NODE_OPTIONS to prevent tsx loader from being inherited
const { NODE_OPTIONS: _n1, ...cleanProcessEnv } = process.env;
const { NODE_OPTIONS: _n2, ...cleanUserEnv } = env;
const child = spawn(process.execPath, [runnerPath], {
env: { ...process.env, ...env },
env: { ...cleanProcessEnv, ...cleanUserEnv },
stdio: ['pipe', 'pipe', 'pipe', 'ipc'],
});
@@ -1,12 +1,12 @@
import { statSync, promises as fs } from 'fs';
import { promisify } from 'util';
import { exec } from 'child_process';
import { execFile } from 'child_process';
import { promises as fs, statSync } from 'fs';
import { join } from 'path';
import { promisify } from 'util';
import { getLayerDependenciesDirName } from 'src/engine/core-modules/serverless/drivers/utils/get-layer-dependencies-dir-name';
import type { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
const execPromise = promisify(exec);
const execFilePromise = promisify(execFile);
export const copyAndBuildDependencies = async (
buildDirectory: string,
@@ -32,23 +32,35 @@ export const copyAndBuildDependencies = async (
recursive: true,
});
const localYarnPath = join(buildDirectory, '.yarn/releases/yarn-4.9.2.cjs');
// Strip NODE_OPTIONS to prevent tsx loader from interfering with yarn
const { NODE_OPTIONS: _nodeOptions, ...cleanEnv } = process.env;
try {
await execPromise('yarn', { cwd: buildDirectory });
await execFilePromise(process.execPath, [localYarnPath], {
cwd: buildDirectory,
env: cleanEnv,
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
throw new Error(error.stdout);
const errorMessage =
[error?.stdout, error?.stderr].filter(Boolean).join('\n') ||
'Failed to install serverless dependencies';
throw new Error(errorMessage);
}
const objects = await fs.readdir(buildDirectory);
objects.forEach((object) => {
const fullPath = join(buildDirectory, object);
await Promise.all(
objects
.filter((object) => object !== 'node_modules')
.map((object) => {
const fullPath = join(buildDirectory, object);
if (object === 'node_modules') return;
if (statSync(fullPath).isDirectory()) {
fs.rm(fullPath, { recursive: true, force: true });
} else {
fs.rm(fullPath);
}
});
return statSync(fullPath).isDirectory()
? fs.rm(fullPath, { recursive: true, force: true })
: fs.rm(fullPath);
}),
);
};
@@ -15,6 +15,11 @@ export const serverlessModuleFactory = async (
const options = { fileStorageService };
switch (driverType) {
case ServerlessDriverType.DISABLED: {
return {
type: ServerlessDriverType.DISABLED,
};
}
case ServerlessDriverType.LOCAL: {
return {
type: ServerlessDriverType.LOCAL,
@@ -4,10 +4,15 @@ import { type LambdaDriverOptions } from 'src/engine/core-modules/serverless/dri
import { type LocalDriverOptions } from 'src/engine/core-modules/serverless/drivers/local.driver';
export enum ServerlessDriverType {
DISABLED = 'DISABLED',
LAMBDA = 'LAMBDA',
LOCAL = 'LOCAL',
}
export interface DisabledDriverFactoryOptions {
type: ServerlessDriverType.DISABLED;
}
export interface LocalDriverFactoryOptions {
type: ServerlessDriverType.LOCAL;
options: LocalDriverOptions;
@@ -19,6 +24,7 @@ export interface LambdaDriverFactoryOptions {
}
export type ServerlessModuleOptions =
| DisabledDriverFactoryOptions
| LocalDriverFactoryOptions
| LambdaDriverFactoryOptions;
@@ -1,6 +1,7 @@
import { type DynamicModule, Global } from '@nestjs/common';
import { AddPackagesCommand } from 'src/engine/core-modules/serverless/commands/add-packages.command';
import { DisabledDriver } from 'src/engine/core-modules/serverless/drivers/disabled.driver';
import { LambdaDriver } from 'src/engine/core-modules/serverless/drivers/lambda.driver';
import { LocalDriver } from 'src/engine/core-modules/serverless/drivers/local.driver';
import { SERVERLESS_DRIVER } from 'src/engine/core-modules/serverless/serverless.constants';
@@ -19,9 +20,21 @@ export class ServerlessModule {
useFactory: async (...args: any[]) => {
const config = await options.useFactory(...args);
return config?.type === ServerlessDriverType.LOCAL
? new LocalDriver(config.options)
: new LambdaDriver(config.options);
switch (config?.type) {
case ServerlessDriverType.DISABLED:
return new DisabledDriver();
case ServerlessDriverType.LOCAL:
return new LocalDriver(config.options);
case ServerlessDriverType.LAMBDA:
return new LambdaDriver(config.options);
default: {
const unknownConfig = config as { type?: string };
throw new Error(
`Unknown serverless driver type: ${unknownConfig?.type}`,
);
}
}
},
inject: options.inject || [],
};
@@ -375,7 +375,7 @@ export class ConfigVariables {
type: ConfigVariableType.BOOLEAN,
})
@IsOptional()
IS_WORKSPACE_CREATION_LIMITED_TO_SERVER_ADMINS = false;
IS_WORKSPACE_CREATION_LIMITED_TO_SERVER_ADMINS = true;
@ConfigVariablesMetadata({
group: ConfigVariablesGroup.STORAGE_CONFIG,