@kokorolx/ai-sandbox-wrapper 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +540 -0
  2. package/bin/ai-debug +116 -0
  3. package/bin/ai-network +144 -0
  4. package/bin/ai-run +631 -0
  5. package/bin/cli.js +83 -0
  6. package/bin/setup-ssh-config +328 -0
  7. package/dockerfiles/AGENTS.md +92 -0
  8. package/dockerfiles/aider/Dockerfile +5 -0
  9. package/dockerfiles/amp/Dockerfile +10 -0
  10. package/dockerfiles/auggie/Dockerfile +12 -0
  11. package/dockerfiles/base/Dockerfile +73 -0
  12. package/dockerfiles/claude/Dockerfile +11 -0
  13. package/dockerfiles/codebuddy/Dockerfile +12 -0
  14. package/dockerfiles/codex/Dockerfile +9 -0
  15. package/dockerfiles/droid/Dockerfile +8 -0
  16. package/dockerfiles/gemini/Dockerfile +9 -0
  17. package/dockerfiles/jules/Dockerfile +12 -0
  18. package/dockerfiles/kilo/Dockerfile +25 -0
  19. package/dockerfiles/opencode/Dockerfile +10 -0
  20. package/dockerfiles/qoder/Dockerfile +12 -0
  21. package/dockerfiles/qwen/Dockerfile +10 -0
  22. package/dockerfiles/shai/Dockerfile +9 -0
  23. package/lib/AGENTS.md +58 -0
  24. package/lib/generate-ai-run.sh +19 -0
  25. package/lib/install-aider.sh +30 -0
  26. package/lib/install-amp.sh +39 -0
  27. package/lib/install-auggie.sh +36 -0
  28. package/lib/install-base.sh +139 -0
  29. package/lib/install-claude.sh +42 -0
  30. package/lib/install-codebuddy.sh +36 -0
  31. package/lib/install-codeserver.sh +171 -0
  32. package/lib/install-codex.sh +40 -0
  33. package/lib/install-droid.sh +27 -0
  34. package/lib/install-gemini.sh +39 -0
  35. package/lib/install-jules.sh +36 -0
  36. package/lib/install-kilo.sh +57 -0
  37. package/lib/install-opencode.sh +39 -0
  38. package/lib/install-qoder.sh +37 -0
  39. package/lib/install-qwen.sh +40 -0
  40. package/lib/install-shai.sh +33 -0
  41. package/lib/install-tool.sh +40 -0
  42. package/lib/install-vscode.sh +219 -0
  43. package/lib/ssh-key-selector.sh +189 -0
  44. package/package.json +46 -0
  45. package/setup.sh +530 -0
package/bin/cli.js ADDED
@@ -0,0 +1,83 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { spawn } = require('child_process');
4
+ const path = require('path');
5
+ const fs = require('fs');
6
+
7
+ const args = process.argv.slice(2);
8
+ const command = args[0];
9
+ const packageRoot = path.resolve(__dirname, '..');
10
+
11
+ function showHelp() {
12
+ console.log(`
13
+ 🔒 AI Sandbox Wrapper
14
+
15
+ Usage:
16
+ npx @kokorolx/ai-sandbox-wrapper <command>
17
+
18
+ Commands:
19
+ setup Run interactive setup (configure workspaces, select tools)
20
+ help Show this help message
21
+
22
+ Examples:
23
+ npx @kokorolx/ai-sandbox-wrapper setup
24
+
25
+ Documentation: https://github.com/kokorolx/ai-sandbox-wrapper
26
+ `);
27
+ }
28
+
29
+ function runSetup() {
30
+ const setupScript = path.join(packageRoot, 'setup.sh');
31
+
32
+ if (!fs.existsSync(setupScript)) {
33
+ console.error('❌ Error: setup.sh not found at', setupScript);
34
+ console.error('This may indicate a corrupted installation.');
35
+ process.exit(1);
36
+ }
37
+
38
+ try {
39
+ fs.chmodSync(setupScript, '755');
40
+ } catch (err) {
41
+ /* Windows doesn't support chmod */
42
+ }
43
+
44
+ const child = spawn('bash', [setupScript], {
45
+ cwd: packageRoot,
46
+ stdio: 'inherit',
47
+ env: {
48
+ ...process.env,
49
+ AI_SANDBOX_ROOT: packageRoot
50
+ }
51
+ });
52
+
53
+ child.on('error', (err) => {
54
+ if (err.code === 'ENOENT') {
55
+ console.error('❌ Error: bash not found. Please install bash to run setup.');
56
+ console.error(' macOS/Linux: bash is usually pre-installed');
57
+ console.error(' Windows: Use WSL2 or Git Bash');
58
+ } else {
59
+ console.error('❌ Error running setup:', err.message);
60
+ }
61
+ process.exit(1);
62
+ });
63
+
64
+ child.on('close', (code) => {
65
+ process.exit(code || 0);
66
+ });
67
+ }
68
+
69
+ switch (command) {
70
+ case 'setup':
71
+ case undefined:
72
+ runSetup();
73
+ break;
74
+ case 'help':
75
+ case '--help':
76
+ case '-h':
77
+ showHelp();
78
+ break;
79
+ default:
80
+ console.error(`❌ Unknown command: ${command}`);
81
+ showHelp();
82
+ process.exit(1);
83
+ }
@@ -0,0 +1,328 @@
1
+ #!/bin/bash
2
+ # Extract SSH config entries needed for this repo's git remotes
3
+ # Also collects and fixes SSH keys for use in containers
4
+
5
+ set -e
6
+
7
+ SSH_CONFIG_FILE="${HOME}/.ssh/config"
8
+ SSH_DIR="${HOME}/.ssh"
9
+ TEMP_CONFIG="/tmp/ssh_config_filtered"
10
+ TEMP_KEYS_DIR="/tmp/ssh_keys_filtered"
11
+ COLLECTED_KEYS=()
12
+ STRICT_MODE=0
13
+ HOSTS=""
14
+
15
+ # Show usage
16
+ usage() {
17
+ cat << EOF
18
+ Usage: $(basename "$0") [OPTIONS]
19
+
20
+ Extract SSH config entries and keys needed for git remotes (or custom hosts).
21
+
22
+ OPTIONS:
23
+ --hosts HOSTS Comma/space-separated list of SSH host aliases to extract
24
+ (default: auto-detect from git remotes)
25
+ --keys KEYS Comma/space-separated list of SSH key filenames (e.g. id_rsa)
26
+ to filter by. Only hosts using these keys will be included.
27
+ --strict Fail if any key file is missing (default: warn and continue)
28
+ --help Show this help message
29
+
30
+ EXAMPLES:
31
+ # Use git remotes (default)
32
+ $(basename "$0")
33
+
34
+ # Extract specific hosts
35
+ $(basename "$0") --hosts github.com-kokorolx,gitlab.com-kokorolee
36
+
37
+ # Filter by specific keys
38
+ $(basename "$0") --keys id_rsa_work,id_ed25519_personal
39
+
40
+ # Fail on missing keys
41
+ $(basename "$0") --strict
42
+
43
+ EOF
44
+ exit "${1:-0}"
45
+ }
46
+
47
+ # Parse arguments
48
+ ALLOWED_KEYS_INPUT=""
49
+ while [[ $# -gt 0 ]]; do
50
+ case "$1" in
51
+ --hosts)
52
+ HOSTS="$2"
53
+ shift 2
54
+ ;;
55
+ --keys)
56
+ ALLOWED_KEYS_INPUT="$2"
57
+ shift 2
58
+ ;;
59
+ --strict)
60
+ STRICT_MODE=1
61
+ shift
62
+ ;;
63
+ --help|-h)
64
+ usage 0
65
+ ;;
66
+ *)
67
+ echo "❌ Unknown option: $1"
68
+ usage 1
69
+ ;;
70
+ esac
71
+ done
72
+
73
+ # Validate SSH files exist
74
+ if [ ! -f "$SSH_CONFIG_FILE" ]; then
75
+ echo "❌ Error: SSH config not found at $SSH_CONFIG_FILE"
76
+ exit 1
77
+ fi
78
+
79
+ if [ ! -d "$SSH_DIR" ]; then
80
+ echo "❌ Error: SSH directory not found at $SSH_DIR"
81
+ exit 1
82
+ fi
83
+
84
+ # Normalize allowed keys to array of basenames
85
+ ALLOWED_KEYS=()
86
+ if [ -n "$ALLOWED_KEYS_INPUT" ]; then
87
+ IFS=', ' read -r -a keys_array <<< "$ALLOWED_KEYS_INPUT"
88
+ for k in "${keys_array[@]}"; do
89
+ [ -z "$k" ] && continue
90
+ ALLOWED_KEYS+=("$(basename "$k")")
91
+ done
92
+ fi
93
+
94
+ # Get hosts from keys if provided, otherwise from git remotes if --hosts not specified
95
+ if [ ${#ALLOWED_KEYS[@]} -gt 0 ]; then
96
+ echo "🔑 Filtering by keys: ${ALLOWED_KEYS[*]}"
97
+
98
+ # Find all Host entries that use any of the allowed keys
99
+ # This awk script finds Host blocks and checks if any IdentityFile matches our allowed keys
100
+ MATCHING_HOSTS=$(awk -v keys="${ALLOWED_KEYS[*]}" '
101
+ BEGIN {
102
+ split(keys, k_arr, " ");
103
+ for (i in k_arr) allowed_keys[k_arr[i]] = 1;
104
+ }
105
+ /^Host / {
106
+ if (current_host != "" && host_matches) {
107
+ print current_host
108
+ }
109
+ current_host = $2;
110
+ host_matches = 0;
111
+ }
112
+ /^[[:space:]]*IdentityFile[[:space:]]+/ {
113
+ split($2, path_parts, "/");
114
+ filename = path_parts[length(path_parts)];
115
+ # Remove any trailing comments or quotes
116
+ gsub(/[[:space:]].*$/, "", filename);
117
+ gsub(/^"|"$/, "", filename);
118
+
119
+ if (allowed_keys[filename]) {
120
+ host_matches = 1;
121
+ }
122
+ }
123
+ END {
124
+ if (current_host != "" && host_matches) {
125
+ print current_host
126
+ }
127
+ }
128
+ ' "$SSH_CONFIG_FILE")
129
+
130
+ if [ -z "$HOSTS" ]; then
131
+ HOSTS="$MATCHING_HOSTS"
132
+ fi
133
+
134
+ if [ -z "$HOSTS" ]; then
135
+ echo "⚠️ No Host entries found in SSH config matching the provided keys."
136
+ # We allow it to continue if hosts were explicitly provided, otherwise exit
137
+ if [ -z "$ALLOWED_KEYS_INPUT" ]; then exit 1; fi
138
+ fi
139
+ elif [ -z "$HOSTS" ]; then
140
+ HOSTS=$(git config --get-regexp 'remote\..*\.url' 2>/dev/null | sed -E 's/.*@([^:]+):.*/\1/' | sort -u)
141
+
142
+ if [ -z "$HOSTS" ]; then
143
+ echo "⚠️ No git remotes found with SSH URLs and no --hosts/--keys specified"
144
+ usage 1
145
+ fi
146
+ fi
147
+
148
+ # Normalize hosts to newline-separated
149
+ HOSTS=$(echo "$HOSTS" | tr ',' '\n' | tr ' ' '\n' | sort -u | grep -v '^$')
150
+
151
+ echo "📋 Processing hosts:"
152
+ echo "$HOSTS" | sed 's/^/ - /'
153
+ echo ""
154
+
155
+ # Create temp directories
156
+ rm -rf "$TEMP_CONFIG" "$TEMP_KEYS_DIR"
157
+ mkdir -p "$TEMP_KEYS_DIR"
158
+
159
+ # Start with Host * (global settings)
160
+ {
161
+ awk '/^Host \*$/,/^Host [a-zA-Z0-9]/ { if (/^Host [a-zA-Z0-9]/ && !/^Host \*$/) exit; print }' "$SSH_CONFIG_FILE"
162
+ echo ""
163
+ } >> "$TEMP_CONFIG"
164
+
165
+ # Extract Host blocks for each needed host
166
+ failed_keys=0
167
+ while IFS= read -r host; do
168
+ [ -z "$host" ] && continue
169
+
170
+ echo "Processing SSH config for host: $host"
171
+
172
+ # Extract the Host block and filter IdentityFile entries if keys were provided
173
+ # We use awk to:
174
+ # 1. Find the Host block
175
+ # 2. Within that block, if --keys were provided, only keep IdentityFile lines that match
176
+ # 3. If no --keys provided, keep all IdentityFile lines
177
+ awk -v target="$host" -v keys="${ALLOWED_KEYS[*]}" '
178
+ BEGIN {
179
+ use_key_filter = (keys != "");
180
+ split(keys, k_arr, " ");
181
+ for (i in k_arr) allowed_keys[k_arr[i]] = 1;
182
+ }
183
+ /^Host / && $2 == target {
184
+ found=1;
185
+ print;
186
+ next;
187
+ }
188
+ found {
189
+ if (/^Host / && $2 != target) {
190
+ exit;
191
+ }
192
+
193
+ if (/^[[:space:]]*IdentityFile[[:space:]]+/) {
194
+ if (!use_key_filter) {
195
+ print;
196
+ } else {
197
+ # Check if this IdentityFile is allowed
198
+ line = $0;
199
+ split($2, path_parts, "/");
200
+ filename = path_parts[length(path_parts)];
201
+ gsub(/[[:space:]].*$/, "", filename);
202
+ gsub(/^"|"$/, "", filename);
203
+
204
+ if (allowed_keys[filename]) {
205
+ print line;
206
+ }
207
+ }
208
+ } else {
209
+ print;
210
+ }
211
+ }
212
+ ' "$SSH_CONFIG_FILE" >> "$TEMP_CONFIG"
213
+
214
+ # Extract IdentityFile paths to copy
215
+ keys_to_copy=$(awk -v target="$host" -v keys="${ALLOWED_KEYS[*]}" '
216
+ BEGIN {
217
+ use_key_filter = (keys != "");
218
+ split(keys, k_arr, " ");
219
+ for (i in k_arr) allowed_keys[k_arr[i]] = 1;
220
+ }
221
+ /^Host / && $2 == target {
222
+ found=1
223
+ }
224
+ found {
225
+ if (/^Host / && $2 != target) {
226
+ exit
227
+ }
228
+ if (/^[[:space:]]*IdentityFile[[:space:]]+/) {
229
+ orig_line = $0;
230
+ gsub(/^[[:space:]]*IdentityFile[[:space:]]+/, "")
231
+ path = $0
232
+ gsub(/[[:space:]].*$/, "", path); # Remove comments
233
+ gsub(/^"|"$/, "", path); # Remove quotes
234
+
235
+ # Check if allowed if filter is active
236
+ split(path, path_parts, "/");
237
+ filename = path_parts[length(path_parts)];
238
+
239
+ if (!use_key_filter || allowed_keys[filename]) {
240
+ # Expand tilde using shell variable
241
+ if (path ~ /^~/) {
242
+ sub(/^~/, "'"$HOME"'", path)
243
+ }
244
+ print path
245
+ }
246
+ }
247
+ }
248
+ ' "$SSH_CONFIG_FILE")
249
+
250
+ if [ -z "$keys_to_copy" ]; then
251
+ echo " ⚠️ No matching IdentityFile entries found for this host"
252
+ else
253
+ while IFS= read -r keypath; do
254
+ [ -z "$keypath" ] && continue
255
+
256
+ if [ -f "$keypath" ]; then
257
+ COLLECTED_KEYS+=("$keypath")
258
+ echo " ✓ Found key: $keypath"
259
+ else
260
+ # Try relative to SSH_DIR if not absolute and not found
261
+ if [[ "$keypath" != /* ]] && [ -f "$SSH_DIR/$keypath" ]; then
262
+ COLLECTED_KEYS+=("$SSH_DIR/$keypath")
263
+ echo " ✓ Found key: $SSH_DIR/$keypath"
264
+ else
265
+ echo " ✗ Key not found: $keypath"
266
+ ((failed_keys++))
267
+ if [ $STRICT_MODE -eq 1 ]; then
268
+ echo "❌ Strict mode: aborting due to missing key"
269
+ exit 1
270
+ fi
271
+ fi
272
+ fi
273
+ done <<< "$keys_to_copy"
274
+ fi
275
+
276
+ echo ""
277
+ done <<< "$HOSTS"
278
+
279
+ # Fix paths in config: change /Users/tamlh or ~ to ~/.ssh for portability
280
+ echo "🔧 Fixing paths in SSH config..."
281
+ # Also normalize any absolute paths to ~/.ssh/ for the container
282
+ sed -i.bak \
283
+ -e "s|IdentityFile /Users/[^/]*/\.ssh/|IdentityFile ~/.ssh/|g" \
284
+ -e "s|IdentityFile /home/[^/]*/\.ssh/|IdentityFile ~/.ssh/|g" \
285
+ -e "s|IdentityFile \./|IdentityFile ~/.ssh/|g" \
286
+ "$TEMP_CONFIG"
287
+ rm -f "$TEMP_CONFIG.bak"
288
+
289
+ echo ""
290
+ echo "📋 Filtered SSH config:"
291
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
292
+ if [ -f "$TEMP_CONFIG" ]; then
293
+ cat "$TEMP_CONFIG"
294
+ else
295
+ echo "(Empty config)"
296
+ fi
297
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
298
+
299
+ if [ ${#COLLECTED_KEYS[@]} -gt 0 ]; then
300
+ # Remove duplicates (bash 3.2 compatible)
301
+ UNIQUE_KEYS=()
302
+ while IFS= read -r key; do
303
+ if [[ -n "$key" ]]; then
304
+ UNIQUE_KEYS+=("$key")
305
+ fi
306
+ done < <(printf '%s\n' "${COLLECTED_KEYS[@]}" | sort -u)
307
+
308
+ echo ""
309
+ echo "🔑 Collected SSH keys (${#UNIQUE_KEYS[@]} total):"
310
+
311
+ for keypath in "${UNIQUE_KEYS[@]}"; do
312
+ filename=$(basename "$keypath")
313
+ cp "$keypath" "$TEMP_KEYS_DIR/$filename"
314
+ echo " ✓ $filename"
315
+ done
316
+
317
+ echo ""
318
+ echo "🚀 Info:"
319
+ echo " Config: $TEMP_CONFIG"
320
+ echo " Keys: $TEMP_KEYS_DIR"
321
+ else
322
+ echo "⚠️ No SSH keys were found!"
323
+ fi
324
+
325
+ if [ $failed_keys -gt 0 ]; then
326
+ echo ""
327
+ echo "⚠️ $failed_keys key(s) were missing (use --strict to fail on these)"
328
+ fi
@@ -0,0 +1,92 @@
1
+ # Dockerfiles/ - Container Images
2
+
3
+ **Purpose:** Docker container definitions for AI coding tool isolation
4
+
5
+ ## Overview
6
+
7
+ One Dockerfile per supported AI tool. Each creates a secure, sandboxed environment with only the necessary runtime and access to whitelisted workspaces.
8
+
9
+ ## Structure
10
+
11
+ ```
12
+ dockerfiles/
13
+ ├── base/ # Bun runtime, shared by most tools
14
+ ├── claude/ # Claude Code CLI
15
+ ├── gemini/ # Google Gemini CLI
16
+ ├── aider/ # Aider pair programmer
17
+ ├── opencode/ # Open-source AI coding
18
+ ├── kilo/ # 500+ model support
19
+ ├── codex/ # OpenAI Codex
20
+ ├── amp/ # Sourcegraph Amp
21
+ ├── qwen/ # Alibaba Qwen (1M context)
22
+ ├── droid/ # Factory CLI
23
+ ├── jules/ # Google Jules
24
+ ├── qoder/ # Qoder AI assistant
25
+ ├── auggie/ # Augment Auggie
26
+ ├── codebuddy/ # Tencent CodeBuddy
27
+ └── shai/ # OVHcloud SHAI
28
+ ```
29
+
30
+ ## Where to Look
31
+
32
+ | Task | Location | Notes |
33
+ |------|----------|-------|
34
+ | Modify base image | `base/Dockerfile` | Bun runtime, shared deps |
35
+ | Tool-specific config | `{tool}/Dockerfile` | Tool install, env setup |
36
+ | Security settings | All Dockerfiles | User, permissions, caps |
37
+
38
+ ## Image Patterns
39
+
40
+ **Base Image (base/):**
41
+ - Bun runtime installation
42
+ - Non-root `agent` user
43
+ - Workspace mount point at `/workspace`
44
+
45
+ **Tool Images:**
46
+ - FROM `ai-base:latest`
47
+ - Tool-specific installation
48
+ - Environment variables for API keys
49
+ - Entry point configuration
50
+
51
+ ## Security Settings (All Images)
52
+
53
+ ```dockerfile
54
+ # Non-root user
55
+ RUN adduser -D agent
56
+
57
+ # Workspace mount
58
+ VOLUME ["/workspace"]
59
+
60
+ # Entry point
61
+ USER agent
62
+ WORKDIR /home/agent
63
+ ```
64
+
65
+ ## Image Source Options
66
+
67
+ - **Local build:** `AI_IMAGE_SOURCE=local` (default)
68
+ - **Registry pull:** `AI_IMAGE_SOURCE=registry` (faster)
69
+
70
+ Registry: `registry.gitlab.com/kokorolee/ai-sandbox-wrapper/ai-{tool}:latest`
71
+
72
+ ## Platform Support
73
+
74
+ - linux/amd64 (x64)
75
+ - linux/arm64 (Apple Silicon)
76
+ - Configurable via `AI_RUN_PLATFORM` env var
77
+
78
+ ## Host Access
79
+
80
+ **Accessing host services from containers:**
81
+
82
+ When joining Docker networks (e.g., MetaMCP), containers can access host services via `host.docker.internal`:
83
+
84
+ ```bash
85
+ # Inside container, access MetaMCP at port 12008
86
+ curl http://host.docker.internal:12008/metamcp/default/sse
87
+
88
+ # Access PostgreSQL on host at port 5432
89
+ psql -h host.docker.internal -p 5432 -U myuser mydb
90
+ ```
91
+
92
+ **Configuration:** See [METAMCP_GUIDE.md](../METAMCP_GUIDE.md) for detailed examples.
@@ -0,0 +1,5 @@
1
+ FROM ai-base:latest
2
+ USER agent
3
+ # Install aider via aider-install
4
+ RUN python3 -m pip install --break-system-packages aider-install && aider-install
5
+ ENTRYPOINT ["aider"]
@@ -0,0 +1,10 @@
1
+ FROM ai-base:latest
2
+ USER root
3
+ # Install Amp globally into a persistent directory (not shadowed by home)
4
+ RUN mkdir -p /usr/local/lib/amp && \
5
+ cd /usr/local/lib/amp && \
6
+ bun init -y && \
7
+ bun add @sourcegraph/amp && \
8
+ ln -s /usr/local/lib/amp/node_modules/.bin/amp /usr/local/bin/amp
9
+ USER agent
10
+ ENTRYPOINT ["amp"]
@@ -0,0 +1,12 @@
1
+ FROM ai-base:latest
2
+ USER root
3
+
4
+ # Install Auggie CLI to a non-shadowed path
5
+ RUN mkdir -p /usr/local/lib/auggie && \
6
+ cd /usr/local/lib/auggie && \
7
+ bun init -y && \
8
+ bun add @augmentcode/auggie && \
9
+ ln -s /usr/local/lib/auggie/node_modules/.bin/auggie /usr/local/bin/auggie
10
+
11
+ USER agent
12
+ ENTRYPOINT ["auggie"]
@@ -0,0 +1,73 @@
1
+ FROM oven/bun:latest
2
+
3
+ # Install common dependencies (Bun + Node.js + Python for full package manager support)
4
+ RUN apt-get update && apt-get install -y --no-install-recommends \
5
+ git \
6
+ curl \
7
+ ssh \
8
+ ca-certificates \
9
+ python3 \
10
+ python3-pip \
11
+ python3-venv \
12
+ python3-dev \
13
+ python3-setuptools \
14
+ build-essential \
15
+ libopenblas-dev \
16
+ pipx \
17
+ && curl -LsSf https://astral.sh/uv/install.sh | UV_INSTALL_DIR=/usr/local/bin sh \
18
+ && rm -rf /var/lib/apt/lists/* \
19
+ && pipx ensurepath
20
+
21
+ # Install Node.js LTS (includes npm) via NodeSource
22
+ RUN curl -fsSL https://deb.nodesource.com/setup_lts.x | bash - \
23
+ && apt-get install -y nodejs \
24
+ && rm -rf /var/lib/apt/lists/*
25
+
26
+ # Install pnpm globally via npm
27
+ RUN npm install -g pnpm
28
+
29
+ # Verify installations
30
+ RUN node --version && npm --version && pnpm --version && bun --version
31
+
32
+ # Install additional tools (if selected)
33
+ RUN pipx install specify-cli --pip-args="git+https://github.com/github/spec-kit.git"
34
+ RUN bun install -g uipro-cli
35
+ RUN mkdir -p /usr/local/lib/openspec && \
36
+ cd /usr/local/lib/openspec && \
37
+ bun init -y && \
38
+ bun add @fission-ai/openspec && \
39
+ ln -sf /usr/local/lib/openspec/node_modules/.bin/openspec /usr/local/bin/openspec
40
+ # Install Playwright system dependencies
41
+ RUN apt-get update && apt-get install -y --no-install-recommends \
42
+ libglib2.0-0 \
43
+ libnspr4 \
44
+ libnss3 \
45
+ libdbus-1-3 \
46
+ libatk1.0-0 \
47
+ libatk-bridge2.0-0 \
48
+ libcups2 \
49
+ libxcb1 \
50
+ libxkbcommon0 \
51
+ libatspi2.0-0 \
52
+ libx11-6 \
53
+ libxcomposite1 \
54
+ libxdamage1 \
55
+ libxext6 \
56
+ libxfixes3 \
57
+ libxrandr2 \
58
+ libgbm1 \
59
+ libcairo2 \
60
+ libpango-1.0-0 \
61
+ libasound2 \
62
+ && rm -rf /var/lib/apt/lists/*
63
+ # Install Playwright and browsers via npm (avoids pnpm global bin issues)
64
+ RUN npm install -g playwright && npx playwright install
65
+
66
+ # Create workspace
67
+ WORKDIR /workspace
68
+
69
+ # Non-root user for security
70
+ RUN useradd -m -u 1001 -d /home/agent agent && \
71
+ chown -R agent:agent /workspace
72
+ USER agent
73
+ ENV HOME=/home/agent
@@ -0,0 +1,11 @@
1
+ FROM ai-base:latest
2
+
3
+ USER root
4
+ # Install Claude Code using official native installer
5
+ RUN curl -fsSL https://claude.ai/install.sh | bash && \
6
+ mkdir -p /usr/local/share && \
7
+ mv /home/agent/.local/share/claude /usr/local/share/claude && \
8
+ ln -sf /usr/local/share/claude/versions/$(ls /usr/local/share/claude/versions | head -1) /usr/local/bin/claude
9
+
10
+ USER agent
11
+ ENTRYPOINT ["claude"]
@@ -0,0 +1,12 @@
1
+ FROM ai-base:latest
2
+ USER root
3
+
4
+ # Install CodeBuddy CLI to a non-shadowed path
5
+ RUN mkdir -p /usr/local/lib/codebuddy && \
6
+ cd /usr/local/lib/codebuddy && \
7
+ bun init -y && \
8
+ bun add @tencent-ai/codebuddy-code && \
9
+ ln -s /usr/local/lib/codebuddy/node_modules/.bin/codebuddy /usr/local/bin/codebuddy
10
+
11
+ USER agent
12
+ ENTRYPOINT ["codebuddy"]
@@ -0,0 +1,9 @@
1
+ FROM ai-base:latest
2
+ USER root
3
+ RUN mkdir -p /usr/local/lib/codex && \
4
+ cd /usr/local/lib/codex && \
5
+ bun init -y && \
6
+ bun add @openai/codex && \
7
+ ln -s /usr/local/lib/codex/node_modules/.bin/codex /usr/local/bin/codex
8
+ USER agent
9
+ ENTRYPOINT ["codex"]
@@ -0,0 +1,8 @@
1
+ FROM ai-base:latest
2
+ USER root
3
+ RUN mkdir -p /home/agent/.factory && \
4
+ bash -c "curl -fsSL https://app.factory.ai/cli | sh" && \
5
+ mv /home/agent/.local/bin/droid /usr/local/bin/droid && \
6
+ chown -R agent:agent /home/agent/.factory
7
+ USER agent
8
+ ENTRYPOINT ["bash", "-c", "exec droid \"$@\"", "--"]
@@ -0,0 +1,9 @@
1
+ FROM ai-base:latest
2
+ USER root
3
+ RUN mkdir -p /usr/local/lib/gemini && \
4
+ cd /usr/local/lib/gemini && \
5
+ bun init -y && \
6
+ bun add @google/gemini-cli && \
7
+ ln -s /usr/local/lib/gemini/node_modules/.bin/gemini /usr/local/bin/gemini
8
+ USER agent
9
+ ENTRYPOINT ["gemini"]
@@ -0,0 +1,12 @@
1
+ FROM ai-base:latest
2
+ USER root
3
+
4
+ # Install Jules CLI to a non-shadowed path
5
+ RUN mkdir -p /usr/local/lib/jules && \
6
+ cd /usr/local/lib/jules && \
7
+ bun init -y && \
8
+ bun add @google/jules && \
9
+ ln -s /usr/local/lib/jules/node_modules/.bin/jules /usr/local/bin/jules
10
+
11
+ USER agent
12
+ ENTRYPOINT ["jules"]