This commit is contained in:
Sweepy 2026-01-29 19:00:16 +00:00 committed by GitHub
commit 21436b21b6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 51 additions and 7 deletions

View File

@ -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",

View File

@ -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`

View File

@ -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.

View File

@ -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

View File

@ -372,6 +372,7 @@ async function runExecProcess(opts: {
workdir: opts.containerWorkdir ?? opts.sandbox.containerWorkdir,
env: opts.env,
tty: opts.usePty,
shellCommand: opts.sandbox.shellCommand,
}),
],
options: {

View File

@ -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;
}

View File

@ -5,7 +5,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";
@ -413,4 +413,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"]);
});
});

View File

@ -275,6 +275,7 @@ export function createMoltbotCodingTools(options?: {
workspaceDir: sandbox.workspaceDir,
containerWorkdir: sandbox.containerWorkdir,
env: sandbox.docker.env,
shellCommand: sandbox.docker.shellCommand,
}
: undefined,
});

View File

@ -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,

View File

@ -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]);
}
}

View File

@ -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;

View File

@ -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":

View File

@ -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). */

View File

@ -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(),