265 lines
12 KiB
Markdown
265 lines
12 KiB
Markdown
# 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:
|
|
|
|
1. **Language & Tooling Constraints:** Agents are restricted to JavaScript. They cannot run shell scripts, Python, `jq`, `ripgrep`, `git`, or standard command-line tools.
|
|
2. **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.
|
|
3. **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`
|
|
```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:
|
|
|
|
1. **Preparation:**
|
|
```javascript
|
|
const vfsRoot = await resolveVfsRoot(googleAuthId);
|
|
await prepareVfs(googleAuthId); // Hydrates files from `modelvfs` table
|
|
```
|
|
2. **Mounting:**
|
|
The directory `data/vfs/<companyId>` on the host is mounted to `/home/agent/workspace` inside the container.
|
|
3. **Execution:**
|
|
Command is executed with a hard timeout (e.g. default 30s, max 120s).
|
|
4. **Synchronization:**
|
|
```javascript
|
|
await syncVfsToDb(googleAuthId); // Syncs any new or edited files back to `modelvfs`
|
|
```
|
|
|
|
### 5.2 Tool Implementation (`src/tools/bash.js`)
|
|
|
|
```javascript
|
|
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
|
|
|
|
1. **Phase 1: Docker Setup & Scripting**
|
|
- Add `docker/sandbox/Dockerfile`.
|
|
- Add build script: `npm run docker:sandbox:build` to `package.json`.
|
|
- Verify image build and base tools (`node -v`, `python3 --version`, `rg --version`, `jq --version`).
|
|
2. **Phase 2: Bash Tool Implementation**
|
|
- Create `src/tools/bash.js` conforming to `defineTool`.
|
|
- Integrate with `prepareVfs` and `syncVfsToDb`.
|
|
- Add unit tests validating command execution, timeouts, and file synchronization.
|
|
3. **Phase 3: Integration into Tool Registries**
|
|
- Register `createBashTool` in the agent execution registry alongside existing tools (`javascript.js`, `read-file.js`, `patch.js`).
|
|
4. **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.
|