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