Configurable sandbox shell command
Co-authored-by: sweepies <7191851+sweepies@users.noreply.github.com>
This commit is contained in:
parent
6372242da7
commit
c889c75d39
@ -2181,7 +2181,8 @@ cross-session isolation. Use `scope: "session"` for per-session isolation.
|
||||
Legacy: `perSession` is still supported (`true` → `scope: "session"`,
|
||||
`false` → `scope: "shared"`).
|
||||
|
||||
`setupCommand` runs **once** after the container is created (inside the container via `sh -lc`).
|
||||
`setupCommand` runs **once** after the container is created (inside the container via `sh -lc`, or
|
||||
the command specified in `agents.defaults.sandbox.docker.shellCommand`).
|
||||
For package installs, ensure network egress, a writable root FS, and a root user.
|
||||
|
||||
```json5
|
||||
@ -2204,6 +2205,7 @@ For package installs, ensure network egress, a writable root FS, and a root user
|
||||
capDrop: ["ALL"],
|
||||
env: { LANG: "C.UTF-8" },
|
||||
setupCommand: "apt-get update && apt-get install -y git curl jq",
|
||||
shellCommand: ["bash", "-lc"],
|
||||
// Per-agent override (multi-agent): agents.list[].sandbox.docker.*
|
||||
pidsLimit: 256,
|
||||
memory: "1g",
|
||||
|
||||
@ -123,7 +123,8 @@ Docker installs and the containerized gateway live here:
|
||||
|
||||
## setupCommand (one-time container setup)
|
||||
`setupCommand` runs **once** after the sandbox container is created (not on every run).
|
||||
It executes inside the container via `sh -lc`.
|
||||
It executes inside the container via `sh -lc` unless `agents.defaults.sandbox.docker.shellCommand`
|
||||
overrides it.
|
||||
|
||||
Paths:
|
||||
- Global: `agents.defaults.sandbox.docker.setupCommand`
|
||||
|
||||
@ -441,7 +441,8 @@ Example:
|
||||
- Container not running: it will auto-create per session on demand.
|
||||
- Permission errors in sandbox: set `docker.user` to a UID:GID that matches your
|
||||
mounted workspace ownership (or chown the workspace folder).
|
||||
- Custom tools not found: Moltbot runs commands with `sh -lc` (login shell), which
|
||||
- Custom tools not found: Moltbot runs commands with `sh -lc` (login shell) unless
|
||||
`agents.defaults.sandbox.docker.shellCommand` overrides it. The login shell
|
||||
sources `/etc/profile` and may reset PATH. Set `docker.env.PATH` to prepend your
|
||||
custom tool paths (e.g., `/custom/bin:/usr/local/share/npm-global/bin`), or add
|
||||
a script under `/etc/profile.d/` in your Dockerfile.
|
||||
|
||||
@ -66,7 +66,8 @@ Example:
|
||||
already sets `env.PATH`). The daemon itself still runs with a minimal `PATH`:
|
||||
- macOS: `/opt/homebrew/bin`, `/usr/local/bin`, `/usr/bin`, `/bin`
|
||||
- Linux: `/usr/local/bin`, `/usr/bin`, `/bin`
|
||||
- `host=sandbox`: runs `sh -lc` (login shell) inside the container, so `/etc/profile` may reset `PATH`.
|
||||
- `host=sandbox`: runs `sh -lc` (login shell) inside the container unless
|
||||
`agents.defaults.sandbox.docker.shellCommand` overrides it, so `/etc/profile` may reset `PATH`.
|
||||
Moltbot prepends `env.PATH` after profile sourcing via an internal env var (no shell interpolation);
|
||||
`tools.exec.pathPrepend` applies here too.
|
||||
- `host=node`: only env overrides you pass are sent to the node. `tools.exec.pathPrepend` only applies
|
||||
|
||||
@ -15,8 +15,17 @@ export type BashSandboxConfig = {
|
||||
workspaceDir: string;
|
||||
containerWorkdir: string;
|
||||
env?: Record<string, string>;
|
||||
shellCommand?: string[];
|
||||
};
|
||||
|
||||
const DEFAULT_SANDBOX_SHELL_COMMAND = ["sh", "-lc"];
|
||||
|
||||
export function resolveSandboxShellCommand(shellCommand?: string[]) {
|
||||
if (!shellCommand) return DEFAULT_SANDBOX_SHELL_COMMAND;
|
||||
const trimmed = shellCommand.map((entry) => entry.trim()).filter(Boolean);
|
||||
return trimmed.length > 0 ? trimmed : DEFAULT_SANDBOX_SHELL_COMMAND;
|
||||
}
|
||||
|
||||
export function buildSandboxEnv(params: {
|
||||
defaultPath: string;
|
||||
paramsEnv?: Record<string, string>;
|
||||
@ -51,6 +60,7 @@ export function buildDockerExecArgs(params: {
|
||||
workdir?: string;
|
||||
env: Record<string, string>;
|
||||
tty: boolean;
|
||||
shellCommand?: string[];
|
||||
}) {
|
||||
const args = ["exec", "-i"];
|
||||
if (params.tty) args.push("-t");
|
||||
@ -72,7 +82,8 @@ export function buildDockerExecArgs(params: {
|
||||
const pathExport = hasCustomPath
|
||||
? 'export PATH="${CLAWDBOT_PREPEND_PATH}:$PATH"; unset CLAWDBOT_PREPEND_PATH; '
|
||||
: "";
|
||||
args.push(params.containerName, "sh", "-lc", `${pathExport}${params.command}`);
|
||||
const [shell, ...shellArgs] = resolveSandboxShellCommand(params.shellCommand);
|
||||
args.push(params.containerName, shell, ...shellArgs, `${pathExport}${params.command}`);
|
||||
return args;
|
||||
}
|
||||
|
||||
|
||||
@ -4,7 +4,7 @@ import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import { peekSystemEvents, resetSystemEventsForTest } from "../infra/system-events.js";
|
||||
import { getFinishedSession, resetProcessRegistryForTests } from "./bash-process-registry.js";
|
||||
import { createExecTool, createProcessTool, execTool, processTool } from "./bash-tools.js";
|
||||
import { buildDockerExecArgs } from "./bash-tools.shared.js";
|
||||
import { buildDockerExecArgs, resolveSandboxShellCommand } from "./bash-tools.shared.js";
|
||||
import { sanitizeBinaryOutput } from "./shell-utils.js";
|
||||
|
||||
const isWin = process.platform === "win32";
|
||||
@ -394,4 +394,22 @@ describe("buildDockerExecArgs", () => {
|
||||
|
||||
expect(args).toContain("-t");
|
||||
});
|
||||
|
||||
it("uses configured sandbox shell command when provided", () => {
|
||||
const args = buildDockerExecArgs({
|
||||
containerName: "test-container",
|
||||
command: "echo test",
|
||||
env: { HOME: "/home/user" },
|
||||
tty: false,
|
||||
shellCommand: ["bash", "-lc"],
|
||||
});
|
||||
|
||||
expect(args).toContain("bash");
|
||||
expect(args).toContain("-lc");
|
||||
});
|
||||
|
||||
it("falls back to default shell command when override is empty", () => {
|
||||
const resolved = resolveSandboxShellCommand(["", " "]);
|
||||
expect(resolved).toEqual(["sh", "-lc"]);
|
||||
});
|
||||
});
|
||||
|
||||
@ -275,6 +275,7 @@ export function createMoltbotCodingTools(options?: {
|
||||
workspaceDir: sandbox.workspaceDir,
|
||||
containerWorkdir: sandbox.containerWorkdir,
|
||||
env: sandbox.docker.env,
|
||||
shellCommand: sandbox.docker.shellCommand,
|
||||
}
|
||||
: undefined,
|
||||
});
|
||||
|
||||
@ -66,6 +66,7 @@ export function resolveSandboxDockerConfig(params: {
|
||||
capDrop: agentDocker?.capDrop ?? globalDocker?.capDrop ?? ["ALL"],
|
||||
env,
|
||||
setupCommand: agentDocker?.setupCommand ?? globalDocker?.setupCommand,
|
||||
shellCommand: agentDocker?.shellCommand ?? globalDocker?.shellCommand,
|
||||
pidsLimit: agentDocker?.pidsLimit ?? globalDocker?.pidsLimit,
|
||||
memory: agentDocker?.memory ?? globalDocker?.memory,
|
||||
memorySwap: agentDocker?.memorySwap ?? globalDocker?.memorySwap,
|
||||
|
||||
@ -3,6 +3,7 @@ import { spawn } from "node:child_process";
|
||||
import { defaultRuntime } from "../../runtime.js";
|
||||
import { formatCliCommand } from "../../cli/command-format.js";
|
||||
import { DEFAULT_SANDBOX_IMAGE, SANDBOX_AGENT_WORKSPACE_MOUNT } from "./constants.js";
|
||||
import { resolveSandboxShellCommand } from "../bash-tools.shared.js";
|
||||
import { readRegistry, updateRegistry } from "./registry.js";
|
||||
import { computeSandboxConfigHash } from "./config-hash.js";
|
||||
import { resolveSandboxAgentId, resolveSandboxScopeKey, slugifySessionKey } from "./shared.js";
|
||||
@ -203,7 +204,8 @@ async function createSandboxContainer(params: {
|
||||
await execDocker(["start", name]);
|
||||
|
||||
if (cfg.setupCommand?.trim()) {
|
||||
await execDocker(["exec", "-i", name, "sh", "-lc", cfg.setupCommand]);
|
||||
const [shell, ...shellArgs] = resolveSandboxShellCommand(cfg.shellCommand);
|
||||
await execDocker(["exec", "-i", name, shell, ...shellArgs, cfg.setupCommand]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -9,6 +9,7 @@ export type SandboxDockerConfig = {
|
||||
capDrop: string[];
|
||||
env?: Record<string, string>;
|
||||
setupCommand?: string;
|
||||
shellCommand?: string[];
|
||||
pidsLimit?: number;
|
||||
memory?: string | number;
|
||||
memorySwap?: string | number;
|
||||
|
||||
@ -421,6 +421,7 @@ const FIELD_HELP: Record<string, string> = {
|
||||
"tools.exec.pathPrepend": "Directories to prepend to PATH for exec runs (gateway/sandbox).",
|
||||
"tools.exec.safeBins":
|
||||
"Allow stdin-only safe binaries to run without explicit allowlist entries.",
|
||||
"agents.defaults.sandbox.docker.shellCommand": "Sandbox shell command + args (default: sh -lc).",
|
||||
"tools.message.allowCrossContextSend":
|
||||
"Legacy override: allow cross-context sends across all providers.",
|
||||
"tools.message.crossContext.allowWithinProvider":
|
||||
|
||||
@ -19,6 +19,8 @@ export type SandboxDockerSettings = {
|
||||
env?: Record<string, string>;
|
||||
/** Optional setup command run once after container creation. */
|
||||
setupCommand?: string;
|
||||
/** Optional shell command + args for sandbox exec (default: ["sh", "-lc"]). */
|
||||
shellCommand?: string[];
|
||||
/** Limit container PIDs (0 = Docker default). */
|
||||
pidsLimit?: number;
|
||||
/** Limit container memory (e.g. 512m, 2g, or bytes as number). */
|
||||
|
||||
@ -91,6 +91,7 @@ export const SandboxDockerSchema = z
|
||||
capDrop: z.array(z.string()).optional(),
|
||||
env: z.record(z.string(), z.string()).optional(),
|
||||
setupCommand: z.string().optional(),
|
||||
shellCommand: z.array(z.string()).optional(),
|
||||
pidsLimit: z.number().int().positive().optional(),
|
||||
memory: z.union([z.string(), z.number()]).optional(),
|
||||
memorySwap: z.union([z.string(), z.number()]).optional(),
|
||||
|
||||
Loading…
Reference in New Issue
Block a user