@loopstack/sandbox-tool
v0.7.4
Published
Provides isolated execution environments for running untrusted or experimental code within Loopstack workflows
Maintainers
Readme
title: Sandbox Tool description: Docker sandbox tool for Loopstack — SandboxToolModule, SandboxInit, SandboxCommand, SandboxDestroy tools, DockerContainerManagerService. Create isolated Docker containers, execute commands, manage container lifecycle in workflows.
@loopstack/sandbox-tool
Sandbox execution module for the Loopstack automation framework.
Provides isolated Docker-based execution environments for running untrusted or experimental code safely within Loopstack workflows. It manages container lifecycle (create, execute, destroy) and mounts host directories into containers for file exchange.
When to Use
- You need to run untrusted or AI-generated code in a sandboxed environment
- Your workflow requires executing commands in specific runtime environments (Node.js, Python, etc.) without affecting the host
- You want disposable containers that are created, used, and destroyed within a single workflow run
- Pair with
@loopstack/sandbox-filesystemwhen you also need file read/write operations inside the container
Installation
npm install @loopstack/sandbox-toolPrerequisite: Docker must be installed and running on the host system.
Register the module in your NestJS module:
import { Module } from '@nestjs/common';
import { SandboxToolModule } from '@loopstack/sandbox-tool';
@Module({
imports: [SandboxToolModule],
providers: [MyWorkflow],
exports: [MyWorkflow],
})
export class MyModule {}Quick Start
Inject the tool classes via the constructor, then call them in your transitions:
import { z } from 'zod';
import { BaseWorkflow, ToolResult, Transition, Workflow } from '@loopstack/common';
import type { RunContext } from '@loopstack/common';
import { SandboxCommand, SandboxDestroy, SandboxInit } from '@loopstack/sandbox-tool';
interface SandboxState {
containerId?: string;
}
@Workflow({
schema: z.object({
outputDir: z.string().default(process.cwd() + '/out'),
}),
})
export class MySandboxWorkflow extends BaseWorkflow<{ outputDir: string }, SandboxState> {
constructor(
private readonly sandboxInit: SandboxInit,
private readonly sandboxCommand: SandboxCommand,
private readonly sandboxDestroy: SandboxDestroy,
) {
super();
}
@Transition({ to: 'sandbox_ready' })
async createSandbox(state: SandboxState, ctx: RunContext): Promise<SandboxState> {
const args = ctx.args as { outputDir: string };
const result: ToolResult<{ containerId: string; dockerId: string }> = await this.sandboxInit.call({
containerId: 'my-sandbox',
imageName: 'node:18',
containerName: 'my-node-sandbox',
projectOutPath: args.outputDir,
rootPath: 'workspace',
});
return { ...state, containerId: result.data!.containerId };
}
@Transition({ from: 'sandbox_ready', to: 'code_executed' })
async runCode(state: SandboxState): Promise<SandboxState> {
await this.sandboxCommand.call({
containerId: state.containerId!,
executable: 'node',
args: ['-e', "console.log('Hello from sandbox!')"],
workingDirectory: '/workspace',
timeout: 30000,
});
return state;
}
@Transition({ from: 'code_executed', to: 'end' })
async cleanup(state: SandboxState): Promise<unknown> {
await this.sandboxDestroy.call({
containerId: state.containerId!,
removeContainer: true,
});
return {};
}
}How It Works
The module manages Docker containers through a three-step lifecycle:
sandbox_init --> sandbox_command (repeatable) --> sandbox_destroy- Init creates (or reuses) a Docker container from a specified image, mounts a host directory, and keeps the container alive with
sleep infinity.sandbox_initis idempotent — calling it again with the samecontainerIdreuses the existing container instead of creating a duplicate, so workflows don't need to track container state themselves. - Command executes shell commands inside the running container via
docker exec. Commands are shell-escaped to prevent injection.stdoutandstderrare captured separately on the result. By default, output is trimmed of leading and trailing whitespace. A configurable timeout (default 30s) kills runaway processes — when a timeout fires, the result hastimedOut: trueandexitCode: -1, and stdout/stderr contain whatever was emitted before the kill. - Destroy stops and optionally removes the container, freeing resources.
The DockerContainerManagerService handles the underlying Docker operations: image pulling, container creation, volume binding, stream demuxing, and concurrent access serialization via per-container locks. It implements OnModuleDestroy to stop all managed containers when the NestJS application shuts down.
Security
- Path traversal prevention — working directories are normalized and checked for
..sequences - Shell argument escaping — all executable names and arguments are single-quote escaped
- Isolated containers — code runs in Docker containers with only the mounted volume accessible
- Configurable timeouts — commands that exceed the timeout are killed and return
timedOut: true
Tools Reference
sandbox_init
Initialize a new sandbox container.
| Arg | Type | Required | Description |
| ---------------- | -------- | ------------------------- | ----------------------------------------------------------- |
| containerId | string | Yes | Unique identifier for this container instance |
| imageName | string | Yes | Docker image to use (e.g., node:18, python:3.11) |
| containerName | string | Yes | Name for the Docker container |
| projectOutPath | string | Yes | Host path to mount into the container |
| rootPath | string | No (default: workspace) | Path inside the container where projectOutPath is mounted |
Returns: { containerId: string; dockerId: string }
sandbox_command
Execute a command inside a running sandbox container.
| Arg | Type | Required | Description |
| ------------------ | ---------- | --------------------- | ------------------------------------------------- |
| containerId | string | Yes | ID of the registered container |
| executable | string | Yes | Executable to run (e.g., npm, node, python) |
| args | string[] | No | Arguments to pass to the executable |
| workingDirectory | string | No (default: /) | Working directory for execution |
| envVars | string[] | No | Environment variables in KEY=VALUE format |
| timeout | number | No (default: 30000) | Timeout in milliseconds |
Returns: { stdout: string; stderr: string; exitCode: number; timedOut: boolean }
sandbox_destroy
Stop and destroy a sandbox container.
| Arg | Type | Required | Description |
| ----------------- | --------- | -------------------- | ----------------------------------------------- |
| containerId | string | Yes | ID of the container to destroy |
| removeContainer | boolean | No (default: true) | Whether to remove the container or just stop it |
Returns: { containerId: string; removed: boolean }
Public API
Module
SandboxToolModule— NestJS module that registers all tools and services
Tools
SandboxInit—@Tool({ name: 'sandbox_init' })— create and start a containerSandboxCommand—@Tool({ name: 'sandbox_command' })— execute a command in a containerSandboxDestroy—@Tool({ name: 'sandbox_destroy' })— stop/remove a container
Services
DockerContainerManagerService— low-level Docker container management (register, ensure, execute, stop, remove)
Types
ContainerConfig— configuration for a registered containerCommandExecutionResult— stdout, stderr, exitCode, timedOutDOCKER_CLIENT— injection token for providing a customDocker(dockerode) instance
Dependencies
| Package | Role |
| ------------------- | -------------------------------------------- |
| @loopstack/common | Base tool class, decorators, types |
| @nestjs/common | NestJS dependency injection |
| zod | Schema validation for tool args |
| dockerode | Docker Engine API client (direct dependency) |
Related
- Sandbox Execution docs — setup guide, available tools across both sandbox packages, and security details
- @loopstack/sandbox-filesystem — file read/write/delete/list operations inside sandbox containers
- sandbox-example-workflow — full sandbox lifecycle example using both
@loopstack/sandbox-tooland@loopstack/sandbox-filesystem
About
Author: Tobias Blattermann, Jakob Klippel
License: MIT
