Skip to content

Commit 30a13f6

Browse files
gavrielcclaude
andauthored
Fix/shadow env in container (qwibitai#646)
* fix: shadow .env file in container to prevent agents from reading secrets The main agent's container mounts the project root read-only, which inadvertently exposed the .env file containing API keys. Mount /dev/null over /workspace/project/.env to shadow it — secrets are already passed via stdin and never need to be read from disk inside the container. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: adapt .env shadowing and runtime for Apple Container Apple Container (VirtioFS) only supports directory mounts, not file mounts. The previous /dev/null host-side mount over .env crashes with VZErrorDomain "A directory sharing device configuration is invalid". - Dockerfile: entrypoint now shadows .env via mount --bind inside the container, then drops privileges via setpriv to the host UID/GID - container-runner: main containers skip --user and pass RUN_UID/RUN_GID env vars so entrypoint starts as root for mount --bind - container-runtime: switch to Apple Container CLI (container), fix cleanupOrphans to use container list --format json - Skill: add Dockerfile and container-runner.ts to convert-to-apple-container skill (v1.1.0) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: revert src to Docker runtime, keep Apple Container in skill only The source files should remain Docker-compatible. The Apple Container adaptations live in the convert-to-apple-container skill and are applied on demand. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 60d1760 commit 30a13f6

7 files changed

Lines changed: 850 additions & 3 deletions

File tree

.claude/skills/convert-to-apple-container/SKILL.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,13 @@ This skill switches NanoClaw's container runtime from Docker to Apple Container
1313
- Startup check: `docker info``container system status` (with auto-start)
1414
- Orphan detection: `docker ps --filter``container ls --format json`
1515
- Build script default: `docker``container`
16+
- Dockerfile entrypoint: `.env` shadowing via `mount --bind` inside the container (Apple Container only supports directory mounts, not file mounts like Docker's `/dev/null` overlay)
17+
- Container runner: main-group containers start as root for `mount --bind`, then drop privileges via `setpriv`
1618

1719
**What stays the same:**
18-
- Dockerfile (shared by both runtimes)
19-
- Container runner code (`src/container-runner.ts`)
2020
- Mount security/allowlist validation
21+
- All exported interfaces and IPC protocol
22+
- Non-main container behavior (still uses `--user` flag)
2123
- All other functionality
2224

2325
## Prerequisites
@@ -72,11 +74,15 @@ npx tsx scripts/apply-skill.ts .claude/skills/convert-to-apple-container
7274
This deterministically:
7375
- Replaces `src/container-runtime.ts` with the Apple Container implementation
7476
- Replaces `src/container-runtime.test.ts` with Apple Container-specific tests
77+
- Updates `src/container-runner.ts` with .env shadow mount fix and privilege dropping
78+
- Updates `container/Dockerfile` with entrypoint that shadows .env via `mount --bind`
7579
- Updates `container/build.sh` to default to `container` runtime
7680
- Records the application in `.nanoclaw/state.yaml`
7781

7882
If the apply reports merge conflicts, read the intent files:
7983
- `modify/src/container-runtime.ts.intent.md` — what changed and invariants
84+
- `modify/src/container-runner.ts.intent.md` — .env shadow and privilege drop changes
85+
- `modify/container/Dockerfile.intent.md` — entrypoint changes for .env shadowing
8086
- `modify/container/build.sh.intent.md` — what changed for build script
8187

8288
### Validate code changes
@@ -172,4 +178,6 @@ Check directory permissions on the host. The container runs as uid 1000.
172178
|------|----------------|
173179
| `src/container-runtime.ts` | Full replacement — Docker → Apple Container API |
174180
| `src/container-runtime.test.ts` | Full replacement — tests for Apple Container behavior |
181+
| `src/container-runner.ts` | .env shadow mount removed, main containers start as root with privilege drop |
182+
| `container/Dockerfile` | Entrypoint: `mount --bind` for .env shadowing, `setpriv` privilege drop |
175183
| `container/build.sh` | Default runtime: `docker``container` |

.claude/skills/convert-to-apple-container/manifest.yaml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
skill: convert-to-apple-container
2-
version: 1.0.0
2+
version: 1.1.0
33
description: "Switch container runtime from Docker to Apple Container (macOS)"
44
core_version: 0.1.0
55
adds: []
66
modifies:
77
- src/container-runtime.ts
88
- src/container-runtime.test.ts
9+
- src/container-runner.ts
910
- container/build.sh
11+
- container/Dockerfile
1012
structured: {}
1113
conflicts: []
1214
depends: []
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# NanoClaw Agent Container
2+
# Runs Claude Agent SDK in isolated Linux VM with browser automation
3+
4+
FROM node:22-slim
5+
6+
# Install system dependencies for Chromium
7+
RUN apt-get update && apt-get install -y \
8+
chromium \
9+
fonts-liberation \
10+
fonts-noto-color-emoji \
11+
libgbm1 \
12+
libnss3 \
13+
libatk-bridge2.0-0 \
14+
libgtk-3-0 \
15+
libx11-xcb1 \
16+
libxcomposite1 \
17+
libxdamage1 \
18+
libxrandr2 \
19+
libasound2 \
20+
libpangocairo-1.0-0 \
21+
libcups2 \
22+
libdrm2 \
23+
libxshmfence1 \
24+
curl \
25+
git \
26+
&& rm -rf /var/lib/apt/lists/*
27+
28+
# Set Chromium path for agent-browser
29+
ENV AGENT_BROWSER_EXECUTABLE_PATH=/usr/bin/chromium
30+
ENV PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/usr/bin/chromium
31+
32+
# Install agent-browser and claude-code globally
33+
RUN npm install -g agent-browser @anthropic-ai/claude-code
34+
35+
# Create app directory
36+
WORKDIR /app
37+
38+
# Copy package files first for better caching
39+
COPY agent-runner/package*.json ./
40+
41+
# Install dependencies
42+
RUN npm install
43+
44+
# Copy source code
45+
COPY agent-runner/ ./
46+
47+
# Build TypeScript
48+
RUN npm run build
49+
50+
# Create workspace directories
51+
RUN mkdir -p /workspace/group /workspace/global /workspace/extra /workspace/ipc/messages /workspace/ipc/tasks /workspace/ipc/input
52+
53+
# Create entrypoint script
54+
# Secrets are passed via stdin JSON — temp file is deleted immediately after Node reads it
55+
# Follow-up messages arrive via IPC files in /workspace/ipc/input/
56+
# Apple Container only supports directory mounts (VirtioFS), so .env cannot be
57+
# shadowed with a host-side /dev/null file mount. Instead the entrypoint starts
58+
# as root, uses mount --bind to shadow .env, then drops to the host user via setpriv.
59+
RUN printf '#!/bin/bash\nset -e\n\n# Shadow .env so the agent cannot read host secrets (requires root)\nif [ "$(id -u)" = "0" ] && [ -f /workspace/project/.env ]; then\n mount --bind /dev/null /workspace/project/.env\nfi\n\n# Compile agent-runner\ncd /app && npx tsc --outDir /tmp/dist 2>&1 >&2\nln -s /app/node_modules /tmp/dist/node_modules\nchmod -R a-w /tmp/dist\n\n# Capture stdin (secrets JSON) to temp file\ncat > /tmp/input.json\n\n# Drop privileges if running as root (main-group containers)\nif [ "$(id -u)" = "0" ] && [ -n "$RUN_UID" ]; then\n chown "$RUN_UID:$RUN_GID" /tmp/input.json /tmp/dist\n exec setpriv --reuid="$RUN_UID" --regid="$RUN_GID" --clear-groups -- node /tmp/dist/index.js < /tmp/input.json\nfi\n\nexec node /tmp/dist/index.js < /tmp/input.json\n' > /app/entrypoint.sh && chmod +x /app/entrypoint.sh
60+
61+
# Set ownership to node user (non-root) for writable directories
62+
RUN chown -R node:node /workspace && chmod 777 /home/node
63+
64+
# Set working directory to group workspace
65+
WORKDIR /workspace/group
66+
67+
# Entry point reads JSON from stdin, outputs JSON to stdout
68+
ENTRYPOINT ["/app/entrypoint.sh"]
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Intent: container/Dockerfile modifications
2+
3+
## What changed
4+
Updated the entrypoint script to shadow `.env` inside the container and drop privileges at runtime, replacing the Docker-style host-side file mount approach.
5+
6+
## Why
7+
Apple Container (VirtioFS) only supports directory mounts, not file mounts. The Docker approach of mounting `/dev/null` over `.env` from the host causes `VZErrorDomain Code=2 "A directory sharing device configuration is invalid"`. The fix moves the shadowing into the entrypoint using `mount --bind` (which works inside the Linux VM).
8+
9+
## Key sections
10+
11+
### Entrypoint script
12+
- Added: `mount --bind /dev/null /workspace/project/.env` when running as root and `.env` exists
13+
- Added: Privilege drop via `setpriv --reuid=$RUN_UID --regid=$RUN_GID --clear-groups` for main-group containers
14+
- Added: `chown` of `/tmp/input.json` and `/tmp/dist` to target user before dropping privileges
15+
- Removed: `USER node` directive — main containers start as root to perform the bind mount, then drop privileges in the entrypoint. Non-main containers still get `--user` from the host.
16+
17+
### Dual-path execution
18+
- Root path (main containers): shadow .env → compile → capture stdin → chown → setpriv drop → exec node
19+
- Non-root path (other containers): compile → capture stdin → exec node
20+
21+
## Invariants
22+
- The entrypoint still reads JSON from stdin and runs the agent-runner
23+
- The compiled output goes to `/tmp/dist` (read-only after build)
24+
- `node_modules` is symlinked, not copied
25+
- Non-main containers are unaffected (they arrive as non-root via `--user`)
26+
27+
## Must-keep
28+
- The `set -e` at the top
29+
- The stdin capture to `/tmp/input.json` (required because setpriv can't forward stdin piping)
30+
- The `chmod -R a-w /tmp/dist` (prevents agent from modifying its own runner)
31+
- The `chown -R node:node /workspace` in the build step

0 commit comments

Comments
 (0)