Files
dashback/SANDBOX.md
2026-10-07 19:16:42 +02:00

265 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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