12 KiB
12 KiB
Proposal: Dockerized Bash Sandbox for Dashback Agent Tools
1. Overview & Motivation
Dashback currently equips LLM agents with code execution capabilities via javascript.js running inside a Node.js worker_threads VM with a virtualized file system (vfs.js). While effective for lightweight JavaScript transformations, this approach has limitations:
- Language & Tooling Constraints: Agents are restricted to JavaScript. They cannot run shell scripts, Python,
jq,ripgrep,git, or standard command-line tools. - Isolation Boundaries: Worker threads share host process memory and kernel resources. A native or non-VM shell tool running directly on the host would pose severe security and privilege escalation risks.
- Reproducibility: A containerized environment guarantees identical toolchain versions and execution environments across development and production deployments.
This proposal specifies an isolated, container-based Bash Tool (bash.js) powered by a lightweight Docker image with sandboxed access to the company's virtual file system (VFS).
2. Sandboxing Architecture
┌────────────────────────────────────────┐
│ dashback Host │
│ │
│ 1. prepareVfs(googleAuthId) │
│ └─ hydrates DB -> data/vfs/<cid>/ │
└──────────────────┬─────────────────────┘
│
Mounts host path│ (scoped to company)
data/vfs/<cid> │ as /home/agent/workspace
▼
┌────────────────────────────────────────────────────────────────────────┐
│ Docker Sandbox Container (`dashback-sandbox`) │
│ │
│ User: agent (UID 1000, non-root) │
│ Rootfs: Read-Only (no modifying /usr, /bin, /lib) │
│ Capabilities: None (--cap-drop=ALL) │
│ Privilege Escalation: Blocked (no-new-privileges) │
│ Tmpfs: /tmp (64M, nosuid), /home/agent/.cache (64M, nosuid) │
│ Limits: 512MB RAM, 1.0 CPU, 100 PIDs (fork-bomb proof) │
│ Network: None (--network none) or Internal Bridge │
│ │
│ /home/agent/workspace/ <─ (Shared VFS Mount) │
│ ├── package.json │
│ ├── script.js │
│ └── output.csv │
└────────────────────────────────────┬───────────────────────────────────┘
│
┌──────────────────┴─────────────────────┐
│ 3. syncVfsToDb(googleAuthId) │
│ └─ persists created/updated files │
│ back to modelvfs table │
│ 4. Return stdout/stderr to LLM │
└────────────────────────────────────────┘
3. Docker Image Specification
Location
docker/sandbox/Dockerfile
Dockerfile
FROM node:22-bookworm-slim
# Prevent interactive prompts during package install
ENV DEBIAN_FRONTEND=noninteractive
# Install essential CLI tools and dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
bash \
ca-certificates \
curl \
git \
jq \
ripgrep \
procps \
tree \
tar \
unzip \
gzip \
python3 \
bc \
sed \
gawk \
&& rm -rf /var/lib/apt/lists/*
# Set up unprivileged user
ENV USER=agent
ENV HOME=/home/agent
ENV WORKSPACE=/home/agent/workspace
RUN groupadd -g 1000 ${USER} && \
useradd -u 1000 -g ${USER} -d ${HOME} -s /bin/bash -m ${USER} && \
mkdir -p ${WORKSPACE} && \
chown -R ${USER}:${USER} ${HOME}
# Switch context to agent
WORKDIR ${WORKSPACE}
USER ${USER}
# Provide local node bin in PATH
ENV PATH="${WORKSPACE}/node_modules/.bin:${HOME}/.local/bin:${PATH}"
ENTRYPOINT ["/bin/bash"]
CMD ["-l"]
4. Security & Runtime Constraints
Containers must never be invoked with default privileged parameters. The runner applies strict Linux namespace and cgroup boundaries:
| Flag | Rationale |
|---|---|
--user 1000:1000 |
Executes strictly as unprivileged user agent. Matches UID with host file permissions. |
--cap-drop=ALL |
Drops all Linux capabilities (prevents raw network creation, kernel module loading, chroot, ptrace). |
--security-opt=no-new-privileges:true |
Disallows gaining privileges via setuid/setgid binaries. |
--read-only |
The root filesystem is mounted read-only. The container cannot modify OS binaries, libraries, or configs. |
--tmpfs /tmp:rw,nosuid,nodev,size=64m |
Provides a volatile, memory-backed scratch space with nosuid and nodev. |
--tmpfs /home/agent/.cache:rw,nosuid,size=64m |
Allows tools like npm or python to write temporary caches without disk persistence. |
--pids-limit 100 |
Protects host from fork bombs (`:(){ : |
--memory 512m |
Caps RAM usage; terminates process if memory leaks or runaway allocations occur. |
--cpus 1.0 |
Restricts CPU usage to a single core equivalent to avoid CPU starvation. |
--network none |
Prevents unauthorized external data exfiltration by default (can be bridged if specifically required). |
-v <vfs_root>:/home/agent/workspace:rw |
Restricts file access solely to the company's workspace directory. |
5. Integration with Dashback
5.1 VFS Lifecycle
Each execution turn synchronizes through the existing Dashback VFS pipeline:
- Preparation:
const vfsRoot = await resolveVfsRoot(googleAuthId); await prepareVfs(googleAuthId); // Hydrates files from `modelvfs` table - Mounting:
The directory
data/vfs/<companyId>on the host is mounted to/home/agent/workspaceinside the container. - Execution: Command is executed with a hard timeout (e.g. default 30s, max 120s).
- Synchronization:
await syncVfsToDb(googleAuthId); // Syncs any new or edited files back to `modelvfs`
5.2 Tool Implementation (src/tools/bash.js)
import { execFile } from 'node:child_process';
import { promisify } from 'node:util';
import path from 'node:path';
import { defineTool, ToolError } from 'libllmorbiter';
import { resolveVfsRoot, prepareVfs, syncVfsToDb } from './vfs.js';
const execFileAsync = promisify(execFile);
const IMAGE_NAME = 'dashback-sandbox:latest';
const DEFAULT_TIMEOUT_MS = 30_000;
const MAX_BUFFER = 1024 * 1024; // 1 MB output cap
export function createBashTool(googleAuthId) {
return defineTool({
name: 'bash_execute',
description: 'Execute a bash command in a secure sandboxed Linux environment with Node.js, Python, git, jq, and ripgrep. Working directory is the company workspace.',
parameters: {
type: 'object',
properties: {
command: {
type: 'string',
description: 'The bash command line to execute (e.g., "node script.js", "jq . data.json", "rg pattern").',
},
timeout_ms: {
type: 'integer',
description: 'Execution timeout in milliseconds (default 30000, max 120000).',
},
},
required: ['command'],
additionalProperties: false,
},
invoke: async ({ command, timeout_ms = DEFAULT_TIMEOUT_MS }) => {
const vfsRoot = await resolveVfsRoot(googleAuthId);
await prepareVfs(googleAuthId);
const effectiveTimeout = Math.min(Math.max(timeout_ms, 1000), 120000);
const dockerArgs = [
'run', '--rm', '-i',
'--user', '1000:1000',
'--cap-drop=ALL',
'--security-opt=no-new-privileges:true',
'--read-only',
'--tmpfs', '/tmp:rw,nosuid,nodev,size=64m',
'--tmpfs', '/home/agent/.cache:rw,nosuid,size=64m',
'--pids-limit', '100',
'--memory', '512m',
'--cpus', '1.0',
'--network', 'none',
'-v', `${path.resolve(vfsRoot)}:/home/agent/workspace:rw`,
IMAGE_NAME,
'-c', command,
];
try {
const { stdout, stderr } = await execFileAsync('docker', dockerArgs, {
timeout: effectiveTimeout,
maxBuffer: MAX_BUFFER,
});
await syncVfsToDb(googleAuthId);
return {
stdout: stdout.trim(),
stderr: stderr.trim(),
exitCode: 0,
};
} catch (err) {
// Ensure partial file modifications are synced even on exit error
try { await syncVfsToDb(googleAuthId); } catch (_) {}
if (err.killed || err.signal === 'SIGTERM') {
throw new ToolError(`Execution timed out after ${effectiveTimeout}ms.`);
}
return {
stdout: err.stdout ? err.stdout.trim() : '',
stderr: err.stderr ? err.stderr.trim() : err.message,
exitCode: err.code ?? 1,
};
}
},
});
}
6. Execution Models: Ephemeral vs. Persistent Daemon
| Consideration | Ephemeral (docker run --rm) |
Persistent Container (docker exec) |
|---|---|---|
| Startup Overhead | ~200-400ms per command | ~20-50ms per command |
| State Cleanliness | Completely fresh OS environment every call | Processes/background jobs remain running |
| Lifecycle Complexity | None. Docker handles lifecycle and cleanup | Requires container pool management and reaper |
| Failure Recovery | Automatic | If container crashes, must be recreated |
| Recommendation | Adopt for Phase 1 | Evaluate if high execution frequency warrants it |
7. Implementation Roadmap
- Phase 1: Docker Setup & Scripting
- Add
docker/sandbox/Dockerfile. - Add build script:
npm run docker:sandbox:buildtopackage.json. - Verify image build and base tools (
node -v,python3 --version,rg --version,jq --version).
- Add
- Phase 2: Bash Tool Implementation
- Create
src/tools/bash.jsconforming todefineTool. - Integrate with
prepareVfsandsyncVfsToDb. - Add unit tests validating command execution, timeouts, and file synchronization.
- Create
- Phase 3: Integration into Tool Registries
- Register
createBashToolin the agent execution registry alongside existing tools (javascript.js,read-file.js,patch.js).
- Register
- Phase 4: Optional Network & API Bridge
- If commands inside the container need to invoke Dashback endpoints (
http://host.docker.internal:4208/api), introduce an opt-in network mode using Docker custom bridge networks with short-lived JWT tokens.
- If commands inside the container need to invoke Dashback endpoints (