fix: mount project root read-only to prevent container escape (#392)

The main group's project root was mounted read-write, allowing the
container agent to modify host application code (e.g. dist/container-runner.js)
to inject arbitrary mounts on next restart — a full sandbox escape.

Fix: mount the project root read-only. Writable paths the agent needs
(group folder, IPC, .claude/) are already mounted separately. The
agent-runner source is now copied into a per-group writable location
so agents can still customize container-side behavior without affecting
host code or other groups.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
gavrielc
2026-02-22 20:57:57 +02:00
committed by GitHub
parent ef00320018
commit 5fb10645cd
3 changed files with 22 additions and 9 deletions

View File

@@ -40,6 +40,10 @@ private_key, .secret
- Container path validation (rejects `..` and absolute paths) - Container path validation (rejects `..` and absolute paths)
- `nonMainReadOnly` option forces read-only for non-main groups - `nonMainReadOnly` option forces read-only for non-main groups
**Read-Only Project Root:**
The main group's project root is mounted read-only. Writable paths the agent needs (group folder, IPC, `.claude/`) are mounted separately. This prevents the agent from modifying host application code (`src/`, `dist/`, `package.json`, etc.) which would bypass the sandbox entirely on next restart.
### 3. Session Isolation ### 3. Session Isolation
Each group has isolated Claude sessions at `data/sessions/{group}/.claude/`: Each group has isolated Claude sessions at `data/sessions/{group}/.claude/`:
@@ -82,7 +86,7 @@ const allowedVars = ['CLAUDE_CODE_OAUTH_TOKEN', 'ANTHROPIC_API_KEY'];
| Capability | Main Group | Non-Main Group | | Capability | Main Group | Non-Main Group |
|------------|------------|----------------| |------------|------------|----------------|
| Project root access | `/workspace/project` (rw) | None | | Project root access | `/workspace/project` (ro) | None |
| Group folder | `/workspace/group` (rw) | `/workspace/group` (rw) | | Group folder | `/workspace/group` (rw) | `/workspace/group` (rw) |
| Global memory | Implicit via project | `/workspace/global` (ro) | | Global memory | Implicit via project | `/workspace/global` (ro) |
| Additional mounts | Configurable | Read-only unless allowed | | Additional mounts | Configurable | Read-only unless allowed |

View File

@@ -61,11 +61,11 @@ This is the **main channel**, which has elevated privileges.
## Container Mounts ## Container Mounts
Main has access to the entire project: Main has read-only access to the project and read-write access to its group folder:
| Container Path | Host Path | Access | | Container Path | Host Path | Access |
|----------------|-----------|--------| |----------------|-----------|--------|
| `/workspace/project` | Project root | read-write | | `/workspace/project` | Project root | read-only |
| `/workspace/group` | `groups/main/` | read-write | | `/workspace/group` | `groups/main/` | read-write |
Key paths inside the container: Key paths inside the container:

View File

@@ -67,11 +67,15 @@ function buildVolumeMounts(
const projectRoot = process.cwd(); const projectRoot = process.cwd();
if (isMain) { if (isMain) {
// Main gets the entire project root mounted // Main gets the project root read-only. Writable paths the agent needs
// (group folder, IPC, .claude/) are mounted separately below.
// Read-only prevents the agent from modifying host application code
// (src/, dist/, package.json, etc.) which would bypass the sandbox
// entirely on next restart.
mounts.push({ mounts.push({
hostPath: projectRoot, hostPath: projectRoot,
containerPath: '/workspace/project', containerPath: '/workspace/project',
readonly: false, readonly: true,
}); });
// Main also gets its group folder as the working directory // Main also gets its group folder as the working directory
@@ -155,13 +159,18 @@ function buildVolumeMounts(
readonly: false, readonly: false,
}); });
// Mount agent-runner source from host — recompiled on container startup. // Copy agent-runner source into a per-group writable location so agents
// Bypasses sticky build cache for code changes. // can customize it (add tools, change behavior) without affecting other
// groups. Recompiled on container startup via entrypoint.sh.
const agentRunnerSrc = path.join(projectRoot, 'container', 'agent-runner', 'src'); const agentRunnerSrc = path.join(projectRoot, 'container', 'agent-runner', 'src');
const groupAgentRunnerDir = path.join(DATA_DIR, 'sessions', group.folder, 'agent-runner-src');
if (!fs.existsSync(groupAgentRunnerDir) && fs.existsSync(agentRunnerSrc)) {
fs.cpSync(agentRunnerSrc, groupAgentRunnerDir, { recursive: true });
}
mounts.push({ mounts.push({
hostPath: agentRunnerSrc, hostPath: groupAgentRunnerDir,
containerPath: '/app/src', containerPath: '/app/src',
readonly: true, readonly: false,
}); });
// Additional mounts validated against external allowlist (tamper-proof from containers) // Additional mounts validated against external allowlist (tamper-proof from containers)