This document describes the gRPC-based sandbox execution architecture used in ProDisco. The design decouples code execution from the MCP server, enabling flexible deployment options and improved isolation.
- Overview
- Key Design Decisions
- Directory Structure
- Protocol Definition
- Component Details
- Execution Flows
- Error Handling
- Configuration
- TCP Transport
- Container Isolation
- Transport Security
- Testing
- Future Enhancements
The sandbox system follows a client-server model with per-session isolation. Each MCP client session gets its own Sandbox CRD, which provisions a dedicated pod and service:
The gRPC sandbox server has Kubernetes and Prometheus context baked in:
- Loads
KubeConfigfrom the environment at startup - Provides pre-configured
k8smodule andkc(KubeConfig instance) - Supports
require("prometheus-query")for metrics queries
The sandbox server lives in packages/sandbox-server/ as an npm workspace package. This allows:
- Easy development and testing alongside the MCP server
- Future extraction to a separate repository if needed
- Independent versioning and deployment
Communication supports both Unix domain sockets and TCP:
Unix Socket (default) - Best for local execution:
- Low latency
- Simple setup with no network configuration
- Secure by default (file system permissions)
TCP Transport - Enables remote execution:
- Connect to sandbox servers on different hosts
- Suitable for containerized deployments
- Configurable via options or environment variables
The gRPC protocol is designed to be language-agnostic:
ExecuteRPC accepts generic "code" (not TypeScript-specific)- Different server implementations could execute Go, Python, etc.
- The protocol focuses on execution semantics, not language details
packages/sandbox-server/
├── proto/
│ └── sandbox.proto # gRPC service definition
├── src/
│ ├── generated/ # Auto-generated TypeScript from proto
│ │ └── sandbox.ts
│ ├── server/
│ │ ├── index.ts # Server entry point
│ │ ├── sandbox-service.ts # gRPC service implementation
│ │ ├── executor.ts # VM execution logic
│ │ ├── execution-registry.ts # Async execution state management
│ │ └── cache-manager.ts # Script caching with deduplication
│ ├── client/
│ │ └── index.ts # gRPC client wrapper
│ └── __tests__/ # Test files
│ ├── integration.test.ts
│ ├── streaming-execution.test.ts
│ ├── async-execution.test.ts
│ ├── execution-registry.test.ts
│ ├── cache-manager.test.ts
│ └── cluster-integration.test.ts
├── package.json
├── tsconfig.json
└── buf.gen.yaml # Proto code generation config
The gRPC service is defined in proto/sandbox.proto:
syntax = "proto3";
package prodisco.sandbox.v1;
service SandboxService {
// Synchronous execution (blocks until complete)
rpc Execute(ExecuteRequest) returns (ExecuteResponse);
// Streaming execution (real-time output)
rpc ExecuteStream(ExecuteRequest) returns (stream ExecuteChunk);
// Async execution (fire-and-forget with polling)
rpc ExecuteAsync(ExecuteRequest) returns (ExecuteAsyncResponse);
rpc GetExecution(GetExecutionRequest) returns (GetExecutionResponse);
rpc CancelExecution(CancelExecutionRequest) returns (CancelExecutionResponse);
rpc ListExecutions(ListExecutionsRequest) returns (ListExecutionsResponse);
// Test execution (unit testing with structured results)
rpc ExecuteTest(ExecuteTestRequest) returns (ExecuteTestResponse);
// Health and cache management
rpc HealthCheck(HealthCheckRequest) returns (HealthCheckResponse);
rpc ListCache(ListCacheRequest) returns (ListCacheResponse);
rpc ClearCache(ClearCacheRequest) returns (ClearCacheResponse);
}The API supports three execution modes for different use cases:
| Mode | RPC | Use Case |
|---|---|---|
| Synchronous | Execute |
Simple scripts, short execution time |
| Streaming | ExecuteStream |
Real-time output display, long-running scripts |
| Async | ExecuteAsync + polling |
Background execution, cancellation support |
| Test | ExecuteTest |
Unit testing with structured results |
message ExecuteRequest {
oneof source {
string code = 1; // Code to execute
string cached = 2; // Name of cached script to run
}
optional int32 timeout_ms = 3;
}
message ExecuteResponse {
bool success = 1;
string output = 2;
optional string error = 3;
int64 execution_time_ms = 4;
optional CacheEntry cached = 5; // Set if newly cached
}
message CacheEntry {
string name = 1; // e.g., "script-2024-01-15T10-30-00-abc123def456.ts"
string description = 2; // Extracted from code comments
int64 created_at_ms = 3;
string content_hash = 4; // For deduplication
}Async and streaming executions track state through their lifecycle:
enum ExecutionState {
EXECUTION_STATE_UNSPECIFIED = 0;
EXECUTION_STATE_PENDING = 1; // Queued, waiting to run
EXECUTION_STATE_RUNNING = 2; // Currently executing
EXECUTION_STATE_COMPLETED = 3; // Finished successfully
EXECUTION_STATE_FAILED = 4; // Finished with error
EXECUTION_STATE_CANCELLED = 5; // Cancelled by user
EXECUTION_STATE_TIMEOUT = 6; // Exceeded time limit
}message ExecuteChunk {
string execution_id = 1;
oneof chunk {
string output = 2; // stdout/console.log
string error_output = 3; // stderr/console.error
ExecuteResult result = 4; // Final result (last chunk)
}
int64 timestamp_ms = 5;
}
message ExecuteResult {
bool success = 1;
optional string error = 2;
int64 execution_time_ms = 3;
ExecutionState state = 4;
optional CacheEntry cached = 5;
}message ExecuteAsyncResponse {
string execution_id = 1; // UUID for tracking
ExecutionState state = 2; // Initial state (PENDING or RUNNING)
}
message GetExecutionRequest {
string execution_id = 1;
bool wait = 2; // Long-poll until completion
int64 output_offset = 3; // For incremental output reads
}
message GetExecutionResponse {
string execution_id = 1;
ExecutionState state = 2;
string output = 3; // Buffered stdout since offset
string error_output = 4; // Buffered stderr since offset
int64 output_length = 5; // Total output length (for next offset)
int64 error_output_length = 6;
optional ExecuteResult result = 7; // Set when in terminal state
}
message CancelExecutionRequest {
string execution_id = 1;
}
message CancelExecutionResponse {
bool success = 1;
ExecutionState state = 2;
optional string message = 3; // Error message if cancellation failed
}
message ListExecutionsRequest {
repeated ExecutionState states = 1; // Filter by state (empty = all)
int32 limit = 2;
int64 include_completed_within_ms = 3; // Include recent completions
}
message ListExecutionsResponse {
repeated ExecutionInfo executions = 1;
}
message ExecutionInfo {
string execution_id = 1;
ExecutionState state = 2;
int64 started_at_ms = 3;
optional int64 finished_at_ms = 4;
string code_preview = 5; // First 100 chars
bool is_cached = 6;
optional string cached_name = 7;
}message ExecuteTestRequest {
optional string code = 1; // Implementation code to test
string tests = 2; // Test code using uvu assertions
optional int32 timeout_ms = 3;
}
message ExecuteTestResponse {
bool success = 1; // True if all tests passed
TestSummary summary = 2;
repeated TestResult tests = 3;
string output = 4; // Console output from tests
int64 execution_time_ms = 5;
optional string error = 6; // Error if execution failed before tests ran
}
message TestSummary {
int32 total = 1;
int32 passed = 2;
int32 failed = 3;
int32 skipped = 4;
}
message TestResult {
string name = 1;
bool passed = 2;
optional string error = 3; // Assertion error message if failed
int64 duration_ms = 4;
}Note: The uvu testing framework is automatically available in test mode. Do not import it - test() and assert are pre-injected globals.
The MCP server spawns the sandbox server as a subprocess on startup:
async function startSandboxServer(): Promise<void> {
const sandboxServerPath = path.resolve(__dirname, '../packages/sandbox-server/dist/server/index.js');
const socketPath = process.env.SANDBOX_SOCKET_PATH || '/tmp/prodisco-sandbox.sock';
sandboxProcess = spawn('node', [sandboxServerPath], {
env: {
...process.env,
SANDBOX_SOCKET_PATH: socketPath,
SCRIPTS_CACHE_DIR,
},
});
// Wait for health check to pass
const client = getSandboxClient({ socketPath });
const healthy = await client.waitForHealthy(10000);
if (!healthy) {
throw new Error('Sandbox server failed to start within timeout');
}
}The MCP tool exposes all gRPC execution methods through a unified multi-mode API:
// Schema with 7 execution modes
const RunSandboxInputSchema = z.object({
mode: z.enum(['execute', 'stream', 'async', 'status', 'cancel', 'list', 'test']).default('execute'),
// Execute/Stream/Async mode
code: z.string().optional(),
cached: z.string().optional(),
timeout: z.number().max(120000).default(30000).optional(),
// Status/Cancel mode
executionId: z.string().optional(),
wait: z.boolean().optional(),
outputOffset: z.number().optional(),
// List mode
states: z.array(z.enum(['pending', 'running', 'completed', 'failed', 'cancelled', 'timeout'])).optional(),
limit: z.number().max(100).default(10).optional(),
includeCompletedWithinMs: z.number().optional(),
// Test mode
tests: z.string().optional(), // Test code using uvu assertions
});
async execute(input) {
const { mode = 'execute' } = input;
switch (mode) {
case 'execute':
return executeExecuteMode(input); // Blocking execution
case 'stream':
return executeStreamMode(input); // Streaming with real-time output
case 'async':
return executeAsyncMode(input); // Fire-and-forget with execution ID
case 'status':
return executeStatusMode(input); // Poll/wait for execution status
case 'cancel':
return executeCancelMode(input); // Cancel running execution
case 'list':
return executeListMode(input); // List active/recent executions
case 'test':
return executeTestMode(input); // Unit testing with structured results
}
}The client provides a high-level interface with connection management:
export class SandboxClient {
constructor(options?: SandboxClientOptions);
// Synchronous execution
async execute(options: ExecuteOptions): Promise<ExecuteResult>;
// Streaming execution (async generator)
async *executeStream(options: ExecuteOptions): AsyncGenerator<StreamChunk>;
async *executeStreamWithAbort(options: ExecuteOptions, signal?: AbortSignal): AsyncGenerator<StreamChunk>;
// Async execution
async executeAsync(options: ExecuteOptions): Promise<{ executionId: string; state: ExecutionState }>;
async getExecution(executionId: string, options?: { wait?: boolean; outputOffset?: number }): Promise<ExecutionStatus>;
async waitForExecution(executionId: string): Promise<ExecutionStatus>;
async cancelExecution(executionId: string): Promise<{ success: boolean; state: ExecutionState; message?: string }>;
async listExecutions(options?: ListExecutionsOptions): Promise<ExecutionSummary[]>;
// Cache management
async listCache(filter?: string): Promise<CacheEntry[]>;
async clearCache(): Promise<number>;
// Health check
async healthCheck(): Promise<{ healthy: boolean; kubernetesContext: string }>;
async waitForHealthy(timeoutMs: number): Promise<boolean>;
close(): void;
}
// Singleton pattern for connection reuse
export function getSandboxClient(options?: SandboxClientOptions): SandboxClient;
export function closeSandboxClient(): void;Streaming Example:
import { SandboxClient } from '@prodisco/sandbox-server';
const client = new SandboxClient();
// Stream output in real-time
for await (const chunk of client.executeStream({ code: 'console.log("hello")' })) {
if (chunk.type === 'output') {
process.stdout.write(chunk.data);
} else if (chunk.type === 'error') {
process.stderr.write(chunk.data);
} else if (chunk.type === 'result') {
console.log('Finished:', chunk.data.success);
}
}
// With cancellation support
const controller = new AbortController();
setTimeout(() => controller.abort(), 5000); // Cancel after 5 seconds
try {
for await (const chunk of client.executeStreamWithAbort({ code: longRunningCode }, controller.signal)) {
console.log(chunk);
}
} catch (e) {
if (e.name === 'AbortError') console.log('Cancelled by user');
}Async Execution Example:
// Start execution in background
const { executionId, state } = await client.executeAsync({
code: `
for (let i = 0; i < 100; i++) {
console.log("Processing:", i);
await new Promise(r => setTimeout(r, 100));
}
`,
timeoutMs: 30000,
});
console.log('Started execution:', executionId);
// Poll for incremental output
let offset = 0;
while (true) {
const status = await client.getExecution(executionId, { outputOffset: offset });
if (status.output) {
process.stdout.write(status.output);
offset = status.outputLength;
}
if (status.state >= 3) { // Terminal state
console.log('Finished:', status.result);
break;
}
await new Promise(r => setTimeout(r, 500));
}
// Or wait for completion with long-polling
const finalStatus = await client.waitForExecution(executionId);
// Cancel a running execution
const cancelResult = await client.cancelExecution(executionId);
if (cancelResult.success) {
console.log('Cancelled successfully');
}The server binds to the Unix socket and handles graceful shutdown:
export async function startServer(config: ServerConfig = {}): Promise<grpc.Server> {
const socketPath = config.socketPath || process.env.SANDBOX_SOCKET_PATH || DEFAULT_SOCKET_PATH;
cleanupSocket(socketPath); // Remove stale socket file
const server = new grpc.Server();
const sandboxService = createSandboxService({
prometheusUrl: config.prometheusUrl || process.env.PROMETHEUS_URL,
cacheDir: config.cacheDir || process.env.SCRIPTS_CACHE_DIR,
});
server.addService(SandboxServiceService, sandboxService);
return new Promise((resolve, reject) => {
server.bindAsync(
`unix://${socketPath}`,
grpc.ServerCredentials.createInsecure(),
(error) => {
if (error) reject(error);
else resolve(server);
}
);
});
}The executor runs code in a Node.js VM with a sandboxed context:
export class Executor {
private kc: k8s.KubeConfig;
constructor(config: ExecutorConfig = {}) {
this.kc = new k8s.KubeConfig();
this.kc.loadFromDefault();
}
async execute(code: string, timeoutMs: number = 30000): Promise<ExecuteResult> {
// Transform TypeScript to JavaScript
const transformed = await esbuild.transform(code, {
loader: 'ts',
target: 'node18',
format: 'cjs',
});
// Create sandbox context
const context = vm.createContext({
k8s,
kc: this.kc,
console: capturedConsole,
require: sandboxRequire, // Whitelisted modules only
process: { env: process.env },
setTimeout, setInterval, clearTimeout, clearInterval,
Buffer, JSON, Date, Math, Promise, Array, Object, /* ... */
});
// Execute with timeout
const script = new vm.Script(`(async () => { ${transformed.code} })()`);
await script.runInContext(context, { timeout: timeoutMs });
return { success: true, output: capturedOutput, executionTimeMs };
}
}The execution registry manages async execution state, output buffering, and lifecycle:
export class ExecutionRegistry {
// Create a new execution record
create(options: CreateOptions): Execution;
// Get execution by ID
get(id: string): Execution | undefined;
// Update execution state
setState(id: string, state: ExecutionState): void;
// Append output (notifies listeners)
appendOutput(id: string, data: string, isError?: boolean): void;
// Set final result
setResult(id: string, result: ExecuteResult): void;
// Add listener for streaming output
addOutputListener(id: string, callback: (chunk: StreamChunk) => void): () => void;
// Cancel a running execution
cancel(id: string): boolean;
// List executions with filtering
list(options?: ListOptions): Execution[];
// Check if state is terminal (COMPLETED, FAILED, CANCELLED, TIMEOUT)
isTerminalState(state: ExecutionState): boolean;
// Stop the registry (cleanup interval)
stop(): void;
}
interface Execution {
id: string; // UUID
code: string;
state: ExecutionState;
output: string; // Buffered stdout
errorOutput: string; // Buffered stderr
result?: ExecuteResult;
abortController: AbortController;
startedAtMs: number;
finishedAtMs?: number;
isCached: boolean;
cachedName?: string;
}The cache manager handles script persistence with deduplication:
export class CacheManager {
private mutex = new Mutex();
async cache(code: string): Promise<CacheEntry | undefined> {
const release = await this.mutex.acquire();
try {
const hash = this.hashCode(code);
// Check for existing script with same content
const existing = await this.findByHash(hash);
if (existing) return undefined; // Already cached
const filename = this.generateFilename(code, hash);
const content = this.addHeader(code);
await fs.writeFile(path.join(this.cacheDir, filename), content);
return {
name: filename,
description: this.extractDescription(code),
createdAtMs: Date.now(),
contentHash: hash,
};
} finally {
release();
}
}
find(nameOrPattern: string): CachedCode | null;
list(filter?: string): CacheEntry[];
clear(): number;
}User -> MCP Server -> runSandbox Tool -> gRPC Client
|
v
ExecuteRequest
{ code: "...", timeout_ms: 30000 }
|
v (Unix Socket)
|
gRPC Server <----------+
|
v
SandboxService.Execute()
|
+----+----+
v v
Executor CacheManager
(VM run) (save if success)
| |
+----+----+
v
ExecuteResponse
{ success: true, output: "...", cached_as: "script-abc123.ts" }
|
v (Unix Socket)
|
User <-- MCP Server <-- runSandbox Tool <-- gRPC Client
User -> MCP Server -> runSandbox Tool -> gRPC Client
|
v
ExecuteRequest
{ cached: "list-pods.ts" }
|
v (Unix Socket)
|
gRPC Server <----------+
|
v
SandboxService.Execute()
|
v
CacheManager.find("list-pods.ts")
|
v
Executor.execute(cachedCode)
|
v
ExecuteResponse
{ success: true, output: "...", cached: null }
Client Server
| |
|-- ExecuteStream(request) --->|
| |
| ExecutionRegistry.create()
| |
| Executor.execute(code)
| |
|<--- ExecuteChunk(output) ----| (console.log)
|<--- ExecuteChunk(output) ----| (console.log)
|<--- ExecuteChunk(error) -----| (console.error)
|<--- ExecuteChunk(output) ----| (console.log)
| |
|<--- ExecuteChunk(result) ----| (final result)
| |
v v
Client Server
| |
|-- ExecuteAsync(request) ---->|
| |
| ExecutionRegistry.create()
|<-- ExecuteAsyncResponse -----| { executionId, state: PENDING }
| |
| Executor.execute() (background)
| |
|-- GetExecution(id) --------->|
|<-- GetExecutionResponse -----| { state: RUNNING, output: "..." }
| |
|-- GetExecution(id, wait) --->|
| ...long poll... |
|<-- GetExecutionResponse -----| { state: COMPLETED, result: {...} }
v v
Client Server
| |
|-- ExecuteAsync(request) ---->|
|<-- { executionId } ----------|
| |
| (execution running)
| |
|-- CancelExecution(id) ------>|
| |
| AbortController.abort()
| ExecutionRegistry.cancel()
| |
|<-- { success: true, ---------|
| state: CANCELLED } |
v v
User -> MCP Server -> runSandbox Tool -> gRPC Client
|
v
ExecuteTestRequest
{ code: "function add(a,b) {...}",
tests: "test('adds', () => {...})" }
|
v (Unix Socket)
|
gRPC Server <----------+
|
v
SandboxService.ExecuteTest()
|
v
Executor.executeTest()
|
v
Build test harness:
- Inject test() and assert globals
- Combine code + tests
- Execute in VM
- Parse structured results
|
v
ExecuteTestResponse
{ success: true,
summary: { total: 2, passed: 2, failed: 0 },
tests: [{ name: "adds", passed: true, durationMs: 1 }] }
|
v (Unix Socket)
|
User <-- MCP Server <-- runSandbox Tool <-- gRPC Client
Test Mode Usage Example:
// Test implementation code before running in production
const result = await runSandbox({
mode: 'test',
code: `
function fibonacci(n: number): number[] {
if (n <= 0) return [];
if (n === 1) return [0];
const seq = [0, 1];
for (let i = 2; i < n; i++) {
seq.push(seq[i-1] + seq[i-2]);
}
return seq;
}
`,
tests: `
test("fibonacci(0) returns empty array", () => {
assert.equal(fibonacci(0), []);
});
test("fibonacci(5) returns correct sequence", () => {
assert.equal(fibonacci(5), [0, 1, 1, 2, 3]);
});
test("each number is sum of previous two", () => {
const seq = fibonacci(10);
for (let i = 2; i < seq.length; i++) {
assert.is(seq[i], seq[i-1] + seq[i-2]);
}
});
`,
});
// Result:
// {
// mode: 'test',
// success: true,
// summary: { total: 3, passed: 3, failed: 0, skipped: 0 },
// tests: [
// { name: 'fibonacci(0) returns empty array', passed: true, durationMs: 0 },
// { name: 'fibonacci(5) returns correct sequence', passed: true, durationMs: 0 },
// { name: 'each number is sum of previous two', passed: true, durationMs: 1 }
// ],
// executionTimeMs: 45
// }Available Assertions (from uvu/assert):
| Assertion | Description |
|---|---|
assert.is(a, b) |
Strict equality (===) |
assert.equal(a, b) |
Deep equality for objects/arrays |
assert.ok(val) |
Truthy check |
assert.not(val) |
Falsy check |
assert.throws(fn) |
Expects function to throw |
| Error Type | gRPC Status | Description |
|---|---|---|
| Script not found | NOT_FOUND |
Cached script doesn't exist |
| Execution not found | NOT_FOUND |
Unknown execution ID |
| Syntax error | INVALID_ARGUMENT |
TypeScript/JavaScript parse error |
| Timeout | DEADLINE_EXCEEDED |
Execution exceeded timeout |
| Module not allowed | PERMISSION_DENIED |
Attempted to require blocked module |
| Runtime error | INTERNAL |
Uncaught exception during execution |
| Already cancelled | FAILED_PRECONDITION |
Execution already in terminal state |
| Tests missing | INVALID_ARGUMENT |
Test mode requires tests parameter |
| Test assertion failed | (success=false) | Test returned in result with error message |
| Variable | Default | Description |
|---|---|---|
SANDBOX_SOCKET_PATH |
/tmp/prodisco-sandbox.sock |
Unix socket path |
SANDBOX_USE_TCP |
false |
Use TCP transport instead of Unix socket (true or 1) |
SANDBOX_TCP_HOST |
0.0.0.0 (server) / localhost (client) |
TCP host to bind/connect to |
SANDBOX_TCP_PORT |
50051 |
TCP port to bind/connect to |
| Variable | Default | Description |
|---|---|---|
SANDBOX_TRANSPORT_MODE |
insecure |
Transport security mode: insecure, tls, or mtls |
SANDBOX_TLS_CERT_PATH |
(none) | Server certificate path |
SANDBOX_TLS_KEY_PATH |
(none) | Server private key path |
SANDBOX_TLS_CA_PATH |
(none) | CA certificate for verification |
SANDBOX_TLS_CLIENT_CERT_PATH |
(none) | Client certificate (mTLS, client-side only) |
SANDBOX_TLS_CLIENT_KEY_PATH |
(none) | Client private key (mTLS, client-side only) |
SANDBOX_TLS_SERVER_NAME |
(none) | Override server name for TLS verification |
| Variable | Default | Description |
|---|---|---|
SCRIPTS_CACHE_DIR |
/tmp/prodisco-scripts |
Directory for cached scripts |
PROMETHEUS_URL |
(none) | Prometheus server URL |
KUBECONFIG |
~/.kube/config |
Kubernetes config path |
The sandbox server supports TCP transport for remote execution. This enables running the sandbox server on a different host or in a container.
Start the server with TCP transport:
// Programmatic configuration
import { startServer } from '@prodisco/sandbox-server';
await startServer({
useTcp: true,
tcpHost: '0.0.0.0', // Bind to all interfaces
tcpPort: 50051,
});
// Or using environment variables
// SANDBOX_USE_TCP=true SANDBOX_TCP_HOST=0.0.0.0 SANDBOX_TCP_PORT=50051 node server.jsConnect to a remote sandbox server:
import { SandboxClient } from '@prodisco/sandbox-server';
// Explicit TCP configuration
const client = new SandboxClient({
useTcp: true,
tcpHost: 'sandbox.example.com',
tcpPort: 50051,
});
// Or infer TCP from host/port (useTcp is optional when host/port are specified)
const client2 = new SandboxClient({
tcpHost: 'sandbox.example.com',
tcpPort: 50051,
});
// Or using environment variables
// SANDBOX_USE_TCP=true SANDBOX_TCP_HOST=sandbox.example.com SANDBOX_TCP_PORT=50051
const client3 = new SandboxClient();| Use Case | Recommended Transport |
|---|---|
| Local development | Unix socket (default) |
| MCP server and sandbox on same host | Unix socket |
| Sandbox in separate container | TCP |
| Sandbox on remote host | TCP |
| Production with network isolation | TCP with TLS (see Transport Security) |
The sandbox server can run in a Kubernetes cluster for stronger isolation. This is the recommended deployment model for production.
Files:
- Dockerfile:
packages/sandbox-server/Dockerfile- Multi-stage build that produces a minimal production image - Kubernetes manifests:
packages/sandbox-server/k8s/deployment.yaml- Namespace, ServiceAccount, RBAC, Deployment, and Service
# Build the Docker image
docker build -f packages/sandbox-server/Dockerfile -t prodisco/sandbox-server:latest .
# For kind clusters, load the image
kind load docker-image prodisco/sandbox-server:latest
# Deploy to Kubernetes
kubectl apply -f packages/sandbox-server/k8s/deployment.yaml
# Connect from outside the cluster
kubectl -n prodisco port-forward service/sandbox-server 50051:50051import { SandboxClient } from '@prodisco/sandbox-server';
// Via port-forward
const client = new SandboxClient({
useTcp: true,
tcpHost: 'localhost',
tcpPort: 50051,
});
// Or via in-cluster DNS
const client = new SandboxClient({
useTcp: true,
tcpHost: 'sandbox-server.prodisco.svc.cluster.local',
tcpPort: 50051,
});The sandbox server supports three transport security modes for different deployment scenarios.
| Mode | Description | Use Case |
|---|---|---|
insecure |
No encryption (default) | Local development, Unix socket |
tls |
Server-side TLS | Production with server authentication |
mtls |
Mutual TLS | High-security production deployments |
Server Configuration:
import { startServer } from '@prodisco/sandbox-server';
// TLS mode (server-side TLS)
await startServer({
useTcp: true,
transportMode: 'tls',
tls: {
certPath: '/etc/sandbox-tls/tls.crt',
keyPath: '/etc/sandbox-tls/tls.key',
},
});
// mTLS mode (mutual TLS)
await startServer({
useTcp: true,
transportMode: 'mtls',
tls: {
certPath: '/etc/sandbox-tls/tls.crt',
keyPath: '/etc/sandbox-tls/tls.key',
caPath: '/etc/sandbox-tls/ca.crt', // CA to verify client certs
},
});Client Configuration:
import { SandboxClient } from '@prodisco/sandbox-server';
// TLS mode
const client = new SandboxClient({
useTcp: true,
tcpHost: 'sandbox-server.prodisco.svc.cluster.local',
tcpPort: 50051,
transportMode: 'tls',
tls: {
caPath: '/etc/sandbox-tls/ca.crt', // CA to verify server
},
});
// mTLS mode
const mtlsClient = new SandboxClient({
useTcp: true,
tcpHost: 'sandbox-server.prodisco.svc.cluster.local',
tcpPort: 50051,
transportMode: 'mtls',
tls: {
caPath: '/etc/sandbox-tls/ca.crt',
certPath: '/etc/sandbox-tls/tls.crt',
keyPath: '/etc/sandbox-tls/tls.key',
},
});For Kubernetes deployments, use cert-manager to automatically issue and renew TLS certificates.
Prerequisites:
# Install cert-manager
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.14.0/cert-manager.yamlApply cert-manager resources:
# Apply the issuer and certificates
kubectl apply -f packages/sandbox-server/k8s/cert-manager/issuer.yaml
kubectl apply -f packages/sandbox-server/k8s/cert-manager/server-certificate.yaml
kubectl apply -f packages/sandbox-server/k8s/cert-manager/client-certificate.yaml # For mTLScert-manager creates these secrets:
sandbox-ca-secret- Root CA certificatesandbox-server-tls- Server certificate (mounted in sandbox-server pod)sandbox-client-tls- Client certificate for mTLS (mounted in MCP server pod)
The deployment manifest at packages/sandbox-server/k8s/deployment.yaml is pre-configured for TLS:
env:
- name: SANDBOX_TRANSPORT_MODE
value: "tls" # Change to "mtls" for mutual TLS
- name: SANDBOX_TLS_CERT_PATH
value: "/etc/sandbox-tls/tls.crt"
- name: SANDBOX_TLS_KEY_PATH
value: "/etc/sandbox-tls/tls.key"
- name: SANDBOX_TLS_CA_PATH
value: "/etc/sandbox-tls/ca.crt"
volumeMounts:
- name: tls-certs
mountPath: /etc/sandbox-tls
readOnly: true
volumes:
- name: tls-certs
secret:
secretName: sandbox-server-tlsSecurity Mode Selection:
| Deployment | Recommended Mode |
|---|---|
| Local development (Unix socket) | insecure |
| Local development (TCP) | insecure |
| Kubernetes (internal cluster) | tls |
| Kubernetes (high security) | mtls |
| Cross-cluster communication | mtls |
Tests start a real gRPC server with a test-specific socket:
beforeAll(async () => {
process.env.SANDBOX_SOCKET_PATH = '/tmp/prodisco-sandbox-test.sock';
grpcServer = await startServer({ socketPath: TEST_SOCKET_PATH });
const client = getSandboxClient({ socketPath: TEST_SOCKET_PATH });
await client.waitForHealthy(5000);
});
afterAll(() => {
closeSandboxClient();
grpcServer.forceShutdown();
});Run tests with:
npm testFor integration tests, see integration-testing.md.
Per-execution resource constraints (not yet implemented):
message ResourceLimits {
int64 max_memory_bytes = 1;
int32 max_cpu_percent = 2;
int32 max_file_descriptors = 3;
}Queue executions when at capacity:
message ExecuteAsyncResponse {
string execution_id = 1;
ExecutionState state = 2;
int32 queue_position = 3; // Position in queue if PENDING
}