Dev #11

Merged
cpfarhood merged 86 commits from dev into local 2026-05-12 00:02:32 +00:00
11 changed files with 133 additions and 14 deletions
Showing only changes of commit a7b45938b7 - Show all commits
@@ -55,9 +55,15 @@ describe("command managed runtime", () => {
...process.env, ...process.env,
...input.env, ...input.env,
}; };
const command = input.command === "sh" ? "/bin/sh" : input.command; const command =
input.command === "sh" ? "/bin/sh" : input.command === "bash" ? "/bin/bash" : input.command;
const args = [...(input.args ?? [])]; const args = [...(input.args ?? [])];
if (input.stdin != null && input.command === "sh" && args[0] === "-lc" && typeof args[1] === "string") { if (
input.stdin != null &&
(input.command === "sh" || input.command === "bash") &&
args[0] === "-lc" &&
typeof args[1] === "string"
) {
env.PAPERCLIP_TEST_STDIN = input.stdin; env.PAPERCLIP_TEST_STDIN = input.stdin;
args[1] = `printf '%s' \"$PAPERCLIP_TEST_STDIN\" | (${args[1]})`; args[1] = `printf '%s' \"$PAPERCLIP_TEST_STDIN\" | (${args[1]})`;
} }
@@ -6,6 +6,7 @@ import {
type SandboxManagedRuntimeClient, type SandboxManagedRuntimeClient,
type SandboxRemoteExecutionSpec, type SandboxRemoteExecutionSpec,
} from "./sandbox-managed-runtime.js"; } from "./sandbox-managed-runtime.js";
import { preferredShellForSandbox } from "./sandbox-shell.js";
import type { RunProcessResult } from "./server-utils.js"; import type { RunProcessResult } from "./server-utils.js";
export interface CommandManagedRuntimeRunner { export interface CommandManagedRuntimeRunner {
@@ -23,6 +24,7 @@ export interface CommandManagedRuntimeRunner {
export interface CommandManagedRuntimeSpec { export interface CommandManagedRuntimeSpec {
providerKey?: string | null; providerKey?: string | null;
shellCommand?: "bash" | "sh" | null;
leaseId?: string | null; leaseId?: string | null;
remoteCwd: string; remoteCwd: string;
timeoutMs?: number | null; timeoutMs?: number | null;
@@ -58,10 +60,12 @@ export function createCommandManagedRuntimeClient(input: {
runner: CommandManagedRuntimeRunner; runner: CommandManagedRuntimeRunner;
remoteCwd: string; remoteCwd: string;
timeoutMs: number; timeoutMs: number;
shellCommand?: "bash" | "sh" | null;
}): SandboxManagedRuntimeClient { }): SandboxManagedRuntimeClient {
const shellCommand = preferredShellForSandbox(input.shellCommand);
const runShell = async (script: string, opts: { stdin?: string; timeoutMs?: number } = {}) => { const runShell = async (script: string, opts: { stdin?: string; timeoutMs?: number } = {}) => {
const result = await input.runner.execute({ const result = await input.runner.execute({
command: "sh", command: shellCommand,
args: ["-lc", script], args: ["-lc", script],
cwd: input.remoteCwd, cwd: input.remoteCwd,
stdin: opts.stdin, stdin: opts.stdin,
@@ -112,7 +116,7 @@ export function createCommandManagedRuntimeClient(input: {
}, },
remove: async (remotePath) => { remove: async (remotePath) => {
const result = await input.runner.execute({ const result = await input.runner.execute({
command: "sh", command: shellCommand,
args: ["-lc", `rm -rf ${shellQuote(remotePath)}`], args: ["-lc", `rm -rf ${shellQuote(remotePath)}`],
cwd: input.remoteCwd, cwd: input.remoteCwd,
timeoutMs: input.timeoutMs, timeoutMs: input.timeoutMs,
@@ -121,7 +125,7 @@ export function createCommandManagedRuntimeClient(input: {
}, },
run: async (command, options) => { run: async (command, options) => {
const result = await input.runner.execute({ const result = await input.runner.execute({
command: "sh", command: shellCommand,
args: ["-lc", command], args: ["-lc", command],
cwd: input.remoteCwd, cwd: input.remoteCwd,
timeoutMs: options.timeoutMs, timeoutMs: options.timeoutMs,
@@ -157,11 +161,13 @@ export async function prepareCommandManagedRuntime(input: {
runner: input.runner, runner: input.runner,
remoteCwd: workspaceRemoteDir, remoteCwd: workspaceRemoteDir,
timeoutMs, timeoutMs,
shellCommand: input.spec.shellCommand,
}); });
const shellCommand = preferredShellForSandbox(input.spec.shellCommand);
if (input.installCommand?.trim()) { if (input.installCommand?.trim()) {
const result = await input.runner.execute({ const result = await input.runner.execute({
command: "sh", command: shellCommand,
args: ["-lc", input.installCommand.trim()], args: ["-lc", input.installCommand.trim()],
cwd: workspaceRemoteDir, cwd: workspaceRemoteDir,
timeoutMs, timeoutMs,
@@ -39,7 +39,8 @@ describe("sandbox adapter execution targets", () => {
onSpawn?: (meta: { pid: number; startedAt: string }) => Promise<void>; onSpawn?: (meta: { pid: number; startedAt: string }) => Promise<void>;
}) => { }) => {
counter += 1; counter += 1;
return runChildProcess(`sandbox-run-${counter}`, input.command, input.args ?? [], { const command = input.command === "bash" ? "/bin/bash" : input.command;
return runChildProcess(`sandbox-run-${counter}`, command, input.args ?? [], {
cwd: input.cwd ?? process.cwd(), cwd: input.cwd ?? process.cwd(),
env: input.env ?? {}, env: input.env ?? {},
stdin: input.stdin, stdin: input.stdin,
@@ -140,6 +141,41 @@ describe("sandbox adapter execution targets", () => {
})); }));
}); });
it("uses the provider-declared shell for sandbox helper commands", async () => {
const runner = {
execute: vi.fn(async () => ({
exitCode: 0,
signal: null,
timedOut: false,
stdout: "/home/sandbox",
stderr: "",
pid: null,
startedAt: new Date().toISOString(),
})),
};
const target: AdapterSandboxExecutionTarget = {
kind: "remote",
transport: "sandbox",
providerKey: "custom-provider",
shellCommand: "bash",
remoteCwd: "/workspace",
runner,
};
await runAdapterExecutionTargetShellCommand("run-2b", target, 'printf %s "$HOME"', {
cwd: "/local/workspace",
env: {},
timeoutSec: 7,
});
expect(runner.execute).toHaveBeenCalledWith(expect.objectContaining({
command: "bash",
args: ["-lc", 'printf %s "$HOME"'],
cwd: "/workspace",
timeoutMs: 7000,
}));
});
it("starts a localhost Paperclip bridge for sandbox targets in bridge mode", async () => { it("starts a localhost Paperclip bridge for sandbox targets in bridge mode", async () => {
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-execution-target-bridge-")); const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-execution-target-bridge-"));
cleanupDirs.push(rootDir); cleanupDirs.push(rootDir);
+11 -1
View File
@@ -26,6 +26,7 @@ import {
type RunProcessResult, type RunProcessResult,
type TerminalResultCleanupOptions, type TerminalResultCleanupOptions,
} from "./server-utils.js"; } from "./server-utils.js";
import { preferredShellForSandbox } from "./sandbox-shell.js";
export interface AdapterLocalExecutionTarget { export interface AdapterLocalExecutionTarget {
kind: "local"; kind: "local";
@@ -47,6 +48,7 @@ export interface AdapterSandboxExecutionTarget {
kind: "remote"; kind: "remote";
transport: "sandbox"; transport: "sandbox";
providerKey?: string | null; providerKey?: string | null;
shellCommand?: "bash" | "sh" | null;
environmentId?: string | null; environmentId?: string | null;
leaseId?: string | null; leaseId?: string | null;
remoteCwd: string; remoteCwd: string;
@@ -214,6 +216,10 @@ function requireSandboxRunner(target: AdapterSandboxExecutionTarget): CommandMan
); );
} }
function preferredSandboxShell(target: AdapterSandboxExecutionTarget): "bash" | "sh" {
return preferredShellForSandbox(target.shellCommand);
}
export async function ensureAdapterExecutionTargetCommandResolvable( export async function ensureAdapterExecutionTargetCommandResolvable(
command: string, command: string,
target: AdapterExecutionTarget | null | undefined, target: AdapterExecutionTarget | null | undefined,
@@ -341,8 +347,9 @@ export async function runAdapterExecutionTargetShellCommand(
} }
} }
const shellCommand = preferredSandboxShell(target);
return await requireSandboxRunner(target).execute({ return await requireSandboxRunner(target).execute({
command: "sh", command: shellCommand,
args: ["-lc", command], args: ["-lc", command],
cwd: target.remoteCwd, cwd: target.remoteCwd,
env: options.env, env: options.env,
@@ -612,6 +619,7 @@ export async function prepareAdapterExecutionTargetRuntime(input: {
runner: requireSandboxRunner(target), runner: requireSandboxRunner(target),
spec: { spec: {
providerKey: target.providerKey, providerKey: target.providerKey,
shellCommand: target.shellCommand,
leaseId: target.leaseId, leaseId: target.leaseId,
remoteCwd: target.remoteCwd, remoteCwd: target.remoteCwd,
timeoutMs: target.timeoutMs, timeoutMs: target.timeoutMs,
@@ -745,6 +753,7 @@ export async function startAdapterExecutionTargetPaperclipBridge(input: {
runner: requireSandboxRunner(target), runner: requireSandboxRunner(target),
remoteCwd: target.remoteCwd, remoteCwd: target.remoteCwd,
timeoutMs: target.timeoutMs, timeoutMs: target.timeoutMs,
shellCommand: preferredSandboxShell(target),
}); });
worker = await startSandboxCallbackBridgeWorker({ worker = await startSandboxCallbackBridgeWorker({
client, client,
@@ -781,6 +790,7 @@ export async function startAdapterExecutionTargetPaperclipBridge(input: {
bridgeAsset, bridgeAsset,
timeoutMs: target.timeoutMs, timeoutMs: target.timeoutMs,
maxBodyBytes, maxBodyBytes,
shellCommand: preferredSandboxShell(target),
}); });
} catch (error) { } catch (error) {
await Promise.allSettled([ await Promise.allSettled([
@@ -37,9 +37,15 @@ describe("sandbox callback bridge", () => {
...process.env, ...process.env,
...input.env, ...input.env,
}; };
const command = input.command === "sh" ? "/bin/sh" : input.command; const command =
input.command === "sh" ? "/bin/sh" : input.command === "bash" ? "/bin/bash" : input.command;
const args = [...(input.args ?? [])]; const args = [...(input.args ?? [])];
if (input.stdin != null && input.command === "sh" && args[0] === "-lc" && typeof args[1] === "string") { if (
input.stdin != null &&
(input.command === "sh" || input.command === "bash") &&
args[0] === "-lc" &&
typeof args[1] === "string"
) {
env.PAPERCLIP_TEST_STDIN = input.stdin; env.PAPERCLIP_TEST_STDIN = input.stdin;
args[1] = `printf '%s' \"$PAPERCLIP_TEST_STDIN\" | (${args[1]})`; args[1] = `printf '%s' \"$PAPERCLIP_TEST_STDIN\" | (${args[1]})`;
} }
@@ -4,6 +4,7 @@ import os from "node:os";
import path from "node:path"; import path from "node:path";
import type { CommandManagedRuntimeRunner } from "./command-managed-runtime.js"; import type { CommandManagedRuntimeRunner } from "./command-managed-runtime.js";
import { preferredShellForSandbox } from "./sandbox-shell.js";
import type { RunProcessResult } from "./server-utils.js"; import type { RunProcessResult } from "./server-utils.js";
const DEFAULT_BRIDGE_TOKEN_BYTES = 24; const DEFAULT_BRIDGE_TOKEN_BYTES = 24;
@@ -133,9 +134,10 @@ async function runShell(
cwd: string, cwd: string,
script: string, script: string,
timeoutMs: number, timeoutMs: number,
shellCommand: "bash" | "sh" = "sh",
): Promise<RunProcessResult> { ): Promise<RunProcessResult> {
return await runner.execute({ return await runner.execute({
command: "sh", command: shellCommand,
args: ["-lc", script], args: ["-lc", script],
cwd, cwd,
timeoutMs, timeoutMs,
@@ -266,10 +268,12 @@ export function createCommandManagedSandboxCallbackBridgeQueueClient(input: {
runner: CommandManagedRuntimeRunner; runner: CommandManagedRuntimeRunner;
remoteCwd: string; remoteCwd: string;
timeoutMs?: number | null; timeoutMs?: number | null;
shellCommand?: "bash" | "sh" | null;
}): SandboxCallbackBridgeQueueClient { }): SandboxCallbackBridgeQueueClient {
const timeoutMs = normalizeTimeoutMs(input.timeoutMs, DEFAULT_BRIDGE_RESPONSE_TIMEOUT_MS); const timeoutMs = normalizeTimeoutMs(input.timeoutMs, DEFAULT_BRIDGE_RESPONSE_TIMEOUT_MS);
const shellCommand = preferredShellForSandbox(input.shellCommand);
const runChecked = async (action: string, script: string) => const runChecked = async (action: string, script: string) =>
requireSuccessfulResult(action, await runShell(input.runner, input.remoteCwd, script, timeoutMs)); requireSuccessfulResult(action, await runShell(input.runner, input.remoteCwd, script, timeoutMs, shellCommand));
return { return {
makeDir: async (remotePath) => { makeDir: async (remotePath) => {
@@ -288,6 +292,7 @@ export function createCommandManagedSandboxCallbackBridgeQueueClient(input: {
"fi", "fi",
].join("\n"), ].join("\n"),
timeoutMs, timeoutMs,
shellCommand,
); );
requireSuccessfulResult(`list ${remotePath}`, result); requireSuccessfulResult(`list ${remotePath}`, result);
return result.stdout return result.stdout
@@ -525,10 +530,12 @@ export async function startSandboxCallbackBridgeServer(input: {
responseTimeoutMs?: number | null; responseTimeoutMs?: number | null;
timeoutMs?: number | null; timeoutMs?: number | null;
nodeCommand?: string; nodeCommand?: string;
shellCommand?: "bash" | "sh" | null;
maxQueueDepth?: number | null; maxQueueDepth?: number | null;
maxBodyBytes?: number | null; maxBodyBytes?: number | null;
}): Promise<StartedSandboxCallbackBridgeServer> { }): Promise<StartedSandboxCallbackBridgeServer> {
const timeoutMs = normalizeTimeoutMs(input.timeoutMs, DEFAULT_BRIDGE_RESPONSE_TIMEOUT_MS); const timeoutMs = normalizeTimeoutMs(input.timeoutMs, DEFAULT_BRIDGE_RESPONSE_TIMEOUT_MS);
const shellCommand = preferredShellForSandbox(input.shellCommand);
const directories = sandboxCallbackBridgeDirectories(input.queueDir); const directories = sandboxCallbackBridgeDirectories(input.queueDir);
const remoteEntrypoint = path.posix.join(input.assetRemoteDir, SANDBOX_CALLBACK_BRIDGE_ENTRYPOINT); const remoteEntrypoint = path.posix.join(input.assetRemoteDir, SANDBOX_CALLBACK_BRIDGE_ENTRYPOINT);
if (input.bridgeAsset) { if (input.bridgeAsset) {
@@ -536,6 +543,7 @@ export async function startSandboxCallbackBridgeServer(input: {
runner: input.runner, runner: input.runner,
remoteCwd: input.remoteCwd, remoteCwd: input.remoteCwd,
timeoutMs, timeoutMs,
shellCommand,
}); });
await assetClient.makeDir(input.assetRemoteDir); await assetClient.makeDir(input.assetRemoteDir);
const entrypointSource = await fs.readFile(input.bridgeAsset.entrypoint, "utf8"); const entrypointSource = await fs.readFile(input.bridgeAsset.entrypoint, "utf8");
@@ -553,7 +561,7 @@ export async function startSandboxCallbackBridgeServer(input: {
}); });
const nodeCommand = input.nodeCommand?.trim() || "node"; const nodeCommand = input.nodeCommand?.trim() || "node";
const startResult = await input.runner.execute({ const startResult = await input.runner.execute({
command: "sh", command: shellCommand,
args: [ args: [
"-lc", "-lc",
[ [
@@ -594,6 +602,7 @@ export async function startSandboxCallbackBridgeServer(input: {
"exit 1", "exit 1",
].join("\n"), ].join("\n"),
timeoutMs, timeoutMs,
shellCommand,
); );
requireSuccessfulResult("wait for sandbox callback bridge readiness", readyResult); requireSuccessfulResult("wait for sandbox callback bridge readiness", readyResult);
@@ -626,7 +635,7 @@ export async function startSandboxCallbackBridgeServer(input: {
directories, directories,
stop: async () => { stop: async () => {
const stopResult = await input.runner.execute({ const stopResult = await input.runner.execute({
command: "sh", command: shellCommand,
args: [ args: [
"-lc", "-lc",
[ [
@@ -0,0 +1,3 @@
export function preferredShellForSandbox(shellCommand: string | null | undefined): "bash" | "sh" {
return shellCommand === "bash" ? "bash" : "sh";
}
@@ -127,6 +127,7 @@ function leaseMetadata(input: {
}) { }) {
return { return {
provider: "e2b", provider: "e2b",
shellCommand: "bash",
template: input.config.template, template: input.config.template,
timeoutMs: input.config.timeoutMs, timeoutMs: input.config.timeoutMs,
reuseLease: input.config.reuseLease, reuseLease: input.config.reuseLease,
@@ -97,4 +97,40 @@ describe("resolveEnvironmentExecutionTarget", () => {
paperclipTransport: "direct", paperclipTransport: "direct",
}); });
}); });
it("passes through a provider-declared sandbox shell command from lease metadata", async () => {
mockResolveEnvironmentDriverConfigForRuntime.mockResolvedValue({
driver: "sandbox",
config: {
provider: "fake-plugin",
reuseLease: false,
timeoutMs: 30_000,
},
});
const target = await resolveEnvironmentExecutionTarget({
db: {} as never,
companyId: "company-1",
adapterType: "claude_local",
environment: {
id: "env-1",
driver: "sandbox",
config: {
provider: "fake-plugin",
},
},
leaseId: "lease-1",
leaseMetadata: {
shellCommand: "bash",
},
lease: null,
environmentRuntime: null,
});
expect(target).toMatchObject({
kind: "remote",
transport: "sandbox",
shellCommand: "bash",
});
});
}); });
@@ -62,11 +62,16 @@ export async function resolveEnvironmentExecutionTarget(input: {
typeof input.leaseMetadata?.paperclipApiUrl === "string" && input.leaseMetadata.paperclipApiUrl.trim().length > 0 typeof input.leaseMetadata?.paperclipApiUrl === "string" && input.leaseMetadata.paperclipApiUrl.trim().length > 0
? input.leaseMetadata.paperclipApiUrl.trim() ? input.leaseMetadata.paperclipApiUrl.trim()
: null; : null;
const shellCommand =
input.leaseMetadata?.shellCommand === "bash" || input.leaseMetadata?.shellCommand === "sh"
? input.leaseMetadata.shellCommand
: null;
return { return {
kind: "remote", kind: "remote",
transport: "sandbox", transport: "sandbox",
providerKey: parsed.config.provider, providerKey: parsed.config.provider,
shellCommand,
remoteCwd, remoteCwd,
environmentId: input.environment.id ?? null, environmentId: input.environment.id ?? null,
leaseId: input.leaseId ?? null, leaseId: input.leaseId ?? null,
@@ -726,6 +726,7 @@ const INTERNAL_PLUGIN_SANDBOX_CONFIG_KEYS = new Set([
"pluginId", "pluginId",
"pluginKey", "pluginKey",
"providerMetadata", "providerMetadata",
"shellCommand",
"sandboxProviderPlugin", "sandboxProviderPlugin",
]); ]);