u
This commit is contained in:
264
SANDBOX.md
Normal file
264
SANDBOX.md
Normal file
@@ -0,0 +1,264 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user