@kokorolx/ai-sandbox-wrapper 3.0.0 → 3.0.2
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.
- package/README.md +29 -0
- package/bin/ai-run +474 -206
- package/dockerfiles/base/Dockerfile +8 -2
- package/dockerfiles/sandbox/Dockerfile +22 -2
- package/lib/AGENTS.md +14 -0
- package/lib/install-base.sh +11 -2
- package/lib/install-claude.sh +8 -6
- package/lib/install-droid.sh +6 -8
- package/lib/install-shai.sh +4 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -192,6 +192,30 @@ npx @kokorolx/ai-sandbox-wrapper git fetch-only ~/projects/myrepo
|
|
|
192
192
|
npx @kokorolx/ai-sandbox-wrapper git full ~/projects/myrepo
|
|
193
193
|
```
|
|
194
194
|
|
|
195
|
+
### Nano-brain Auto-Repair
|
|
196
|
+
|
|
197
|
+
When running nano-brain inside the sandbox, `ai-run` performs a targeted preflight and automatic retry for common native-module failures (for example tree-sitter binding issues).
|
|
198
|
+
|
|
199
|
+
It also suppresses known **non-fatal** tree-sitter symbol-graph warnings when the command succeeds, so normal query output stays clean. To see suppressed diagnostics, run with debug mode (`AI_RUN_DEBUG=1`).
|
|
200
|
+
|
|
201
|
+
This behavior applies to both:
|
|
202
|
+
- direct mode (`ai-run npx nano-brain ...`)
|
|
203
|
+
- interactive shell mode (`ai-run`, then run `npx nano-brain ...` inside the container shell)
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
# Auto-repair enabled by default
|
|
207
|
+
ai-run npx nano-brain status
|
|
208
|
+
|
|
209
|
+
# Disable per-command
|
|
210
|
+
ai-run npx nano-brain status --no-nano-brain-auto-repair
|
|
211
|
+
|
|
212
|
+
# Disable via environment variable
|
|
213
|
+
AI_RUN_DISABLE_NANO_BRAIN_AUTO_REPAIR=1 ai-run npx nano-brain status
|
|
214
|
+
|
|
215
|
+
# Show suppressed non-fatal warning details
|
|
216
|
+
AI_RUN_DEBUG=1 ai-run npx nano-brain query "hello"
|
|
217
|
+
```
|
|
218
|
+
|
|
195
219
|
### Clipboard
|
|
196
220
|
|
|
197
221
|
Clipboard access in containers requires a terminal that supports **OSC52** protocol.
|
|
@@ -320,6 +344,10 @@ opencode -n mynetwork # Join Docker network
|
|
|
320
344
|
# Git fetch-only
|
|
321
345
|
opencode --git-fetch # Fetch only (no push)
|
|
322
346
|
|
|
347
|
+
# Nano-brain
|
|
348
|
+
ai-run npx nano-brain status # With auto-repair
|
|
349
|
+
AI_RUN_DISABLE_NANO_BRAIN_AUTO_REPAIR=1 ai-run npx nano-brain status
|
|
350
|
+
|
|
323
351
|
# Management
|
|
324
352
|
npx @kokorolx/ai-sandbox-wrapper workspace list
|
|
325
353
|
npx @kokorolx/ai-sandbox-wrapper clean
|
|
@@ -336,6 +364,7 @@ npx @kokorolx/ai-sandbox-wrapper clean
|
|
|
336
364
|
| Port already in use | Stop the process or use different port |
|
|
337
365
|
| Docker not found | Install and start Docker Desktop |
|
|
338
366
|
| Clipboard not working | Use OSC52-compatible terminal. See [CLIPBOARD_SUPPORT.md](CLIPBOARD_SUPPORT.md) |
|
|
367
|
+
| nano-brain native binding/tree-sitter error | Fatal errors auto-repair and retry once by default; known non-fatal symbol-graph warnings are suppressed unless `AI_RUN_DEBUG=1` |
|
|
339
368
|
|
|
340
369
|
---
|
|
341
370
|
|
package/bin/ai-run
CHANGED
|
@@ -17,6 +17,8 @@ Options:
|
|
|
17
17
|
--password-env <VAR> Read OpenCode server password from environment variable
|
|
18
18
|
--allow-unsecured Allow OpenCode server to run without password (suppresses warning)
|
|
19
19
|
--git-fetch Enable git fetch-only mode (blocks push)
|
|
20
|
+
--no-nano-brain-auto-repair
|
|
21
|
+
Disable nano-brain preflight/auto-repair logic
|
|
20
22
|
-h, --help Show this help message
|
|
21
23
|
--help-env Show environment variables reference
|
|
22
24
|
|
|
@@ -65,6 +67,10 @@ Runtime Environment Variables (inline with ai-run):
|
|
|
65
67
|
AI_RUN_PLATFORM=linux/amd64 Force specific platform (useful for cross-arch)
|
|
66
68
|
Example: AI_RUN_PLATFORM=linux/amd64 ai-run opencode
|
|
67
69
|
|
|
70
|
+
AI_RUN_DISABLE_NANO_BRAIN_AUTO_REPAIR=1
|
|
71
|
+
Disable nano-brain preflight and automatic repair behavior
|
|
72
|
+
Example: AI_RUN_DISABLE_NANO_BRAIN_AUTO_REPAIR=1 ai-run npx nano-brain status
|
|
73
|
+
|
|
68
74
|
PORT_BIND=all Bind exposed ports to all interfaces (0.0.0.0) instead of localhost
|
|
69
75
|
⚠️ Security risk - use only when needed
|
|
70
76
|
Example: PORT_BIND=all ai-run opencode web -e 4096
|
|
@@ -155,8 +161,13 @@ SERVER_PASSWORD=""
|
|
|
155
161
|
PASSWORD_ENV_VAR=""
|
|
156
162
|
ALLOW_UNSECURED=false
|
|
157
163
|
GIT_FETCH_ONLY_FLAG=false
|
|
164
|
+
NANO_BRAIN_AUTO_REPAIR=true
|
|
158
165
|
TOOL_ARGS=()
|
|
159
166
|
|
|
167
|
+
if [[ "${AI_RUN_DISABLE_NANO_BRAIN_AUTO_REPAIR:-}" == "1" ]]; then
|
|
168
|
+
NANO_BRAIN_AUTO_REPAIR=false
|
|
169
|
+
fi
|
|
170
|
+
|
|
160
171
|
while [[ $# -gt 0 ]]; do
|
|
161
172
|
case "$1" in
|
|
162
173
|
--shell|-s)
|
|
@@ -201,6 +212,10 @@ while [[ $# -gt 0 ]]; do
|
|
|
201
212
|
GIT_FETCH_ONLY_FLAG=true
|
|
202
213
|
shift
|
|
203
214
|
;;
|
|
215
|
+
--no-nano-brain-auto-repair)
|
|
216
|
+
NANO_BRAIN_AUTO_REPAIR=false
|
|
217
|
+
shift
|
|
218
|
+
;;
|
|
204
219
|
*)
|
|
205
220
|
TOOL_ARGS+=("$1")
|
|
206
221
|
shift
|
|
@@ -521,7 +536,7 @@ if [[ "$(uname)" == "Darwin" ]] && [[ -t 0 ]]; then
|
|
|
521
536
|
|
|
522
537
|
# 2. Check if valid directory
|
|
523
538
|
if [[ -d "$SCREENSHOT_DIR" ]]; then
|
|
524
|
-
|
|
539
|
+
|
|
525
540
|
# 3. Check if ALREADY whitelisted (exact match or parent)
|
|
526
541
|
IS_WHITELISTED=false
|
|
527
542
|
while IFS= read -r ws; do
|
|
@@ -544,7 +559,7 @@ if [[ "$(uname)" == "Darwin" ]] && [[ -t 0 ]]; then
|
|
|
544
559
|
echo ""
|
|
545
560
|
# Use /dev/tty to ensure we read from user even if stdin is redirected
|
|
546
561
|
read -p "Whitelist screenshots folder? [y/N]: " CONFIRM_SS < /dev/tty
|
|
547
|
-
|
|
562
|
+
|
|
548
563
|
if [[ "$CONFIRM_SS" =~ ^[Yy]$ ]]; then
|
|
549
564
|
add_workspace "$SCREENSHOT_DIR"
|
|
550
565
|
echo "✅ Added to whitelist."
|
|
@@ -734,6 +749,21 @@ SHARED_CACHE_MOUNTS="$SHARED_CACHE_MOUNTS -v $CACHE_DIR/bun:/home/agent/.bun/ins
|
|
|
734
749
|
SHARED_CACHE_MOUNTS="$SHARED_CACHE_MOUNTS -v $CACHE_DIR/pip:/home/agent/.cache/pip:delegated"
|
|
735
750
|
SHARED_CACHE_MOUNTS="$SHARED_CACHE_MOUNTS -v $CACHE_DIR/playwright-browsers:/opt/playwright-browsers:delegated"
|
|
736
751
|
|
|
752
|
+
# Shared skills mount (from host, read-only)
|
|
753
|
+
HOST_SKILLS_DIR="$HOME/.config/agents/skills"
|
|
754
|
+
if [[ -d "$HOST_SKILLS_DIR" ]]; then
|
|
755
|
+
SHARED_CACHE_MOUNTS="$SHARED_CACHE_MOUNTS -v $HOST_SKILLS_DIR:/home/agent/.config/agents/skills:ro"
|
|
756
|
+
SHARED_CACHE_MOUNTS="$SHARED_CACHE_MOUNTS -v $HOST_SKILLS_DIR:/home/agent/.config/opencode/skills:ro"
|
|
757
|
+
fi
|
|
758
|
+
|
|
759
|
+
# Nano-brain mount disabled to prevent SQLite conflicts between host and container
|
|
760
|
+
# All tools should use the MCP interface to interact with nano-brain on the host
|
|
761
|
+
if [[ -d "$HOME/.nano-brain" ]]; then
|
|
762
|
+
echo "ℹ️ Skipping .nano-brain mount to prevent SQLite database corruption"
|
|
763
|
+
echo " Use MCP interface for nano-brain access instead of direct CLI"
|
|
764
|
+
fi
|
|
765
|
+
|
|
766
|
+
|
|
737
767
|
# Project-level config mount (if exists and tool specified)
|
|
738
768
|
CONFIG_MOUNT=""
|
|
739
769
|
if [[ -n "$TOOL" ]]; then
|
|
@@ -1137,8 +1167,55 @@ if [[ ${#SELECTED_NETWORKS[@]} -gt 0 ]]; then
|
|
|
1137
1167
|
done < <(validate_networks "${SELECTED_NETWORKS[@]}")
|
|
1138
1168
|
fi
|
|
1139
1169
|
|
|
1170
|
+
# Detect SSH agent socket (prefer agent forwarding over key copying)
|
|
1171
|
+
get_ssh_agent_socket() {
|
|
1172
|
+
# macOS Docker Desktop: special socket path
|
|
1173
|
+
if [[ "$(uname)" == "Darwin" ]]; then
|
|
1174
|
+
local docker_sock="/run/host-services/ssh-auth.sock"
|
|
1175
|
+
# Docker Desktop forwards the host agent to this path inside the VM
|
|
1176
|
+
if [[ -n "$SSH_AUTH_SOCK" ]] && [[ -S "$SSH_AUTH_SOCK" ]]; then
|
|
1177
|
+
echo "$SSH_AUTH_SOCK"
|
|
1178
|
+
return 0
|
|
1179
|
+
fi
|
|
1180
|
+
fi
|
|
1181
|
+
# Standard SSH agent socket (Linux, macOS with regular agent)
|
|
1182
|
+
if [[ -n "$SSH_AUTH_SOCK" ]] && [[ -S "$SSH_AUTH_SOCK" ]]; then
|
|
1183
|
+
echo "$SSH_AUTH_SOCK"
|
|
1184
|
+
return 0
|
|
1185
|
+
fi
|
|
1186
|
+
return 1
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
# Mount SSH agent socket + known_hosts/config (no private keys)
|
|
1190
|
+
setup_ssh_agent_forwarding() {
|
|
1191
|
+
local agent_sock="$1"
|
|
1192
|
+
|
|
1193
|
+
# Mount the agent socket
|
|
1194
|
+
GIT_MOUNTS="$GIT_MOUNTS -v $agent_sock:/ssh-agent"
|
|
1195
|
+
SSH_AGENT_ENV="-e SSH_AUTH_SOCK=/ssh-agent"
|
|
1196
|
+
|
|
1197
|
+
# Still need known_hosts and config for host verification
|
|
1198
|
+
mkdir -p "$GIT_CACHE_DIR/ssh"
|
|
1199
|
+
|
|
1200
|
+
if [ -f "$HOME/.ssh/known_hosts" ]; then
|
|
1201
|
+
cp "$HOME/.ssh/known_hosts" "$GIT_CACHE_DIR/ssh/known_hosts" 2>/dev/null || true
|
|
1202
|
+
chmod 600 "$GIT_CACHE_DIR/ssh/known_hosts" 2>/dev/null || true
|
|
1203
|
+
fi
|
|
1204
|
+
|
|
1205
|
+
if [ -f "$HOME/.ssh/config" ]; then
|
|
1206
|
+
cp "$HOME/.ssh/config" "$GIT_CACHE_DIR/ssh/config" 2>/dev/null || true
|
|
1207
|
+
chmod 600 "$GIT_CACHE_DIR/ssh/config" 2>/dev/null || true
|
|
1208
|
+
fi
|
|
1209
|
+
|
|
1210
|
+
chmod 700 "$GIT_CACHE_DIR/ssh"
|
|
1211
|
+
GIT_MOUNTS="$GIT_MOUNTS -v $GIT_CACHE_DIR/ssh:/home/agent/.ssh:ro"
|
|
1212
|
+
|
|
1213
|
+
echo "🔒 SSH agent forwarding active (keys never enter container)"
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1140
1216
|
# Git access control (opt-in per workspace)
|
|
1141
1217
|
GIT_MOUNTS=""
|
|
1218
|
+
SSH_AGENT_ENV=""
|
|
1142
1219
|
GIT_ALLOWED_FILE="$SANDBOX_DIR/git-allowed"
|
|
1143
1220
|
GIT_CACHE_DIR="$GIT_SHARED_DIR" # V2: Uses shared/git instead of cache/git
|
|
1144
1221
|
touch "$GIT_ALLOWED_FILE" 2>/dev/null || true
|
|
@@ -1204,68 +1281,79 @@ if is_git_allowed "$CURRENT_DIR" || is_git_fetch_only "$CURRENT_DIR" || [[ "$GIT
|
|
|
1204
1281
|
SAVED_KEYS_FILE="$GIT_SHARED_DIR/keys/$WORKSPACE_MD5" # V2: Uses shared/git/keys/
|
|
1205
1282
|
|
|
1206
1283
|
if [ -f "$SAVED_KEYS_FILE" ]; then
|
|
1207
|
-
#
|
|
1208
|
-
|
|
1209
|
-
if
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
[ -z "$key" ] && continue
|
|
1223
|
-
src_file="$HOME/.ssh/$key"
|
|
1224
|
-
dst_file="$GIT_CACHE_DIR/ssh/$key"
|
|
1225
|
-
|
|
1226
|
-
dst_dir=$(dirname "$dst_file")
|
|
1227
|
-
mkdir -p "$dst_dir" 2>/dev/null || true
|
|
1228
|
-
chmod 700 "$dst_dir" 2>/dev/null || true
|
|
1229
|
-
|
|
1230
|
-
if [ -f "$src_file" ]; then
|
|
1231
|
-
cp "$src_file" "$dst_file" 2>/dev/null || true
|
|
1232
|
-
chmod 600 "$dst_file" 2>/dev/null || true
|
|
1284
|
+
# Try SSH agent forwarding first (more secure)
|
|
1285
|
+
AGENT_SOCK=""
|
|
1286
|
+
if AGENT_SOCK=$(get_ssh_agent_socket); then
|
|
1287
|
+
echo "📋 Setting up Git credentials (agent forwarding)..."
|
|
1288
|
+
setup_ssh_agent_forwarding "$AGENT_SOCK"
|
|
1289
|
+
echo "✅ Git credentials ready (agent forwarding)"
|
|
1290
|
+
else
|
|
1291
|
+
# Fallback: copy key files (less secure)
|
|
1292
|
+
echo "📋 Syncing Git credentials..."
|
|
1293
|
+
echo "⚠️ SSH agent not detected. Falling back to key file mounting."
|
|
1294
|
+
echo " For better security, start ssh-agent: eval \"\$(ssh-agent -s)\" && ssh-add"
|
|
1295
|
+
|
|
1296
|
+
if [ -d "$GIT_CACHE_DIR/ssh" ]; then
|
|
1297
|
+
chmod -R 700 "$GIT_CACHE_DIR/ssh" 2>/dev/null || true
|
|
1298
|
+
rm -rf "$GIT_CACHE_DIR/ssh" 2>/dev/null || true
|
|
1233
1299
|
fi
|
|
1234
|
-
|
|
1300
|
+
mkdir -p "$GIT_CACHE_DIR/ssh"
|
|
1301
|
+
|
|
1302
|
+
# Read saved keys and copy them
|
|
1303
|
+
SAVED_KEYS=()
|
|
1304
|
+
while IFS= read -r key; do
|
|
1305
|
+
[ -n "$key" ] && SAVED_KEYS+=("$key")
|
|
1306
|
+
done < "$SAVED_KEYS_FILE"
|
|
1307
|
+
|
|
1308
|
+
for key in "${SAVED_KEYS[@]}"; do
|
|
1309
|
+
[ -z "$key" ] && continue
|
|
1310
|
+
src_file="$HOME/.ssh/$key"
|
|
1311
|
+
dst_file="$GIT_CACHE_DIR/ssh/$key"
|
|
1312
|
+
|
|
1313
|
+
dst_dir=$(dirname "$dst_file")
|
|
1314
|
+
mkdir -p "$dst_dir" 2>/dev/null || true
|
|
1315
|
+
chmod 700 "$dst_dir" 2>/dev/null || true
|
|
1316
|
+
|
|
1317
|
+
if [ -f "$src_file" ]; then
|
|
1318
|
+
cp "$src_file" "$dst_file" 2>/dev/null || true
|
|
1319
|
+
chmod 600 "$dst_file" 2>/dev/null || true
|
|
1320
|
+
fi
|
|
1321
|
+
done
|
|
1235
1322
|
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1323
|
+
# Copy filtered SSH config (only hosts needed for this repo)
|
|
1324
|
+
if [ -f "$HOME/.ssh/config" ]; then
|
|
1325
|
+
SETUP_SSH="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/setup-ssh-config"
|
|
1326
|
+
if [ -x "$SETUP_SSH" ]; then
|
|
1327
|
+
# Join SAVED_KEYS into a comma-separated string for --keys
|
|
1328
|
+
KEYS_ARG=$(IFS=,; echo "${SAVED_KEYS[*]}")
|
|
1329
|
+
output=$( "$SETUP_SSH" --keys "$KEYS_ARG" 2>&1 ) || true
|
|
1330
|
+
TEMP_CONFIG=$(echo "$output" | grep "Config:" | tail -1 | awk '{print $NF}')
|
|
1331
|
+
if [ -f "$TEMP_CONFIG" ]; then
|
|
1332
|
+
cp "$TEMP_CONFIG" "$GIT_CACHE_DIR/ssh/config" 2>/dev/null || true
|
|
1333
|
+
chmod 600 "$GIT_CACHE_DIR/ssh/config" 2>/dev/null || true
|
|
1334
|
+
else
|
|
1335
|
+
cp "$HOME/.ssh/config" "$GIT_CACHE_DIR/ssh/config" 2>/dev/null || true
|
|
1336
|
+
chmod 600 "$GIT_CACHE_DIR/ssh/config" 2>/dev/null || true
|
|
1337
|
+
fi
|
|
1247
1338
|
else
|
|
1248
1339
|
cp "$HOME/.ssh/config" "$GIT_CACHE_DIR/ssh/config" 2>/dev/null || true
|
|
1249
1340
|
chmod 600 "$GIT_CACHE_DIR/ssh/config" 2>/dev/null || true
|
|
1250
1341
|
fi
|
|
1251
|
-
else
|
|
1252
|
-
cp "$HOME/.ssh/config" "$GIT_CACHE_DIR/ssh/config" 2>/dev/null || true
|
|
1253
|
-
chmod 600 "$GIT_CACHE_DIR/ssh/config" 2>/dev/null || true
|
|
1254
1342
|
fi
|
|
1255
|
-
fi
|
|
1256
1343
|
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1344
|
+
if [ -f "$HOME/.ssh/known_hosts" ]; then
|
|
1345
|
+
cp "$HOME/.ssh/known_hosts" "$GIT_CACHE_DIR/ssh/known_hosts" 2>/dev/null || true
|
|
1346
|
+
chmod 600 "$GIT_CACHE_DIR/ssh/known_hosts" 2>/dev/null || true
|
|
1347
|
+
fi
|
|
1261
1348
|
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1349
|
+
# Ensure all directories have correct permissions (recursive)
|
|
1350
|
+
chmod 700 "$GIT_CACHE_DIR/ssh"
|
|
1351
|
+
find "$GIT_CACHE_DIR/ssh" -type d -exec chmod 700 {} \;
|
|
1352
|
+
find "$GIT_CACHE_DIR/ssh" -type f ! -name "config" ! -name "known_hosts" -exec chmod 600 {} \;
|
|
1266
1353
|
|
|
1267
|
-
|
|
1268
|
-
|
|
1354
|
+
GIT_MOUNTS="$GIT_MOUNTS -v $GIT_CACHE_DIR/ssh:/home/agent/.ssh:ro"
|
|
1355
|
+
echo "✅ Git credentials synced"
|
|
1356
|
+
fi
|
|
1269
1357
|
fi
|
|
1270
1358
|
|
|
1271
1359
|
if [ -f "$HOME/.gitconfig" ]; then
|
|
@@ -1296,136 +1384,179 @@ else
|
|
|
1296
1384
|
|
|
1297
1385
|
case "$git_choice" in
|
|
1298
1386
|
1|2|4|5)
|
|
1299
|
-
#
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1387
|
+
# Try SSH agent forwarding first (more secure - no key selection needed)
|
|
1388
|
+
AGENT_SOCK=""
|
|
1389
|
+
if AGENT_SOCK=$(get_ssh_agent_socket); then
|
|
1390
|
+
echo ""
|
|
1391
|
+
echo "🔒 SSH Agent Forwarding"
|
|
1392
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
1393
|
+
setup_ssh_agent_forwarding "$AGENT_SOCK"
|
|
1303
1394
|
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
chmod -R 700 "$GIT_CACHE_DIR/ssh" 2>/dev/null || true
|
|
1321
|
-
rm -rf "$GIT_CACHE_DIR/ssh" 2>/dev/null || true
|
|
1395
|
+
# Copy gitconfig
|
|
1396
|
+
if [ -f "$HOME/.gitconfig" ]; then
|
|
1397
|
+
cp "$HOME/.gitconfig" "$HOME_DIR/.gitconfig" 2>/dev/null || true
|
|
1398
|
+
fi
|
|
1399
|
+
|
|
1400
|
+
if [[ "$git_choice" == "4" || "$git_choice" == "5" ]]; then
|
|
1401
|
+
GIT_FETCH_ONLY_MODE=true
|
|
1402
|
+
fi
|
|
1403
|
+
|
|
1404
|
+
# Save workspace preference (same logic as before)
|
|
1405
|
+
if [ "$git_choice" = "2" ]; then
|
|
1406
|
+
echo "$CURRENT_DIR" >> "$GIT_ALLOWED_FILE"
|
|
1407
|
+
if has_jq && [[ -f "$AI_SANDBOX_CONFIG" ]]; then
|
|
1408
|
+
jq --arg ws "$CURRENT_DIR" '.git.allowedWorkspaces += [$ws] | .git.allowedWorkspaces |= unique' "$AI_SANDBOX_CONFIG" > "$AI_SANDBOX_CONFIG.tmp" \
|
|
1409
|
+
&& mv "$AI_SANDBOX_CONFIG.tmp" "$AI_SANDBOX_CONFIG"
|
|
1410
|
+
chmod 600 "$AI_SANDBOX_CONFIG"
|
|
1322
1411
|
fi
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1412
|
+
echo "✅ Git access enabled and saved for: $CURRENT_DIR"
|
|
1413
|
+
elif [ "$git_choice" = "5" ]; then
|
|
1414
|
+
if has_jq && [[ -f "$AI_SANDBOX_CONFIG" ]]; then
|
|
1415
|
+
jq --arg ws "$CURRENT_DIR" '.git.fetchOnlyWorkspaces = ((.git.fetchOnlyWorkspaces // []) + [$ws] | unique)' "$AI_SANDBOX_CONFIG" > "$AI_SANDBOX_CONFIG.tmp" \
|
|
1416
|
+
&& mv "$AI_SANDBOX_CONFIG.tmp" "$AI_SANDBOX_CONFIG"
|
|
1417
|
+
chmod 600 "$AI_SANDBOX_CONFIG"
|
|
1418
|
+
fi
|
|
1419
|
+
echo "✅ Git fetch-only access enabled and saved for: $CURRENT_DIR"
|
|
1420
|
+
elif [ "$git_choice" = "4" ]; then
|
|
1421
|
+
echo "✅ Git fetch-only access enabled for this session"
|
|
1422
|
+
else
|
|
1423
|
+
echo "✅ Git access enabled for this session"
|
|
1424
|
+
fi
|
|
1425
|
+
else
|
|
1426
|
+
# No SSH agent — fall back to key selection + copy
|
|
1427
|
+
echo ""
|
|
1428
|
+
echo "⚠️ SSH agent not detected. Using key file mounting."
|
|
1429
|
+
echo " For better security: eval \"\$(ssh-agent -s)\" && ssh-add"
|
|
1430
|
+
echo ""
|
|
1431
|
+
echo "🔑 SSH Key Selection"
|
|
1432
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
1433
|
+
|
|
1434
|
+
# Source the SSH key selector library
|
|
1435
|
+
# Resolve symlink to get actual project directory
|
|
1436
|
+
SCRIPT_PATH="${BASH_SOURCE[0]}"
|
|
1437
|
+
while [ -L "$SCRIPT_PATH" ]; do
|
|
1438
|
+
SCRIPT_DIR="$(cd "$(dirname "$SCRIPT_PATH")" && pwd)"
|
|
1439
|
+
SCRIPT_PATH="$(readlink "$SCRIPT_PATH")"
|
|
1440
|
+
[[ $SCRIPT_PATH != /* ]] && SCRIPT_PATH="$SCRIPT_DIR/$SCRIPT_PATH"
|
|
1441
|
+
done
|
|
1442
|
+
SCRIPT_DIR="$(cd "$(dirname "$SCRIPT_PATH")" && pwd)"
|
|
1443
|
+
source "$SCRIPT_DIR/../lib/ssh-key-selector.sh"
|
|
1444
|
+
|
|
1445
|
+
# Let user select keys
|
|
1446
|
+
if select_ssh_keys; then
|
|
1447
|
+
if [ ${#SELECTED_SSH_KEYS[@]} -gt 0 ]; then
|
|
1448
|
+
echo "📋 Copying selected credentials to cache..."
|
|
1449
|
+
if [ -d "$GIT_CACHE_DIR/ssh" ]; then
|
|
1450
|
+
chmod -R 700 "$GIT_CACHE_DIR/ssh" 2>/dev/null || true
|
|
1451
|
+
rm -rf "$GIT_CACHE_DIR/ssh" 2>/dev/null || true
|
|
1342
1452
|
fi
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
#
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1453
|
+
mkdir -p "$GIT_CACHE_DIR/ssh"
|
|
1454
|
+
|
|
1455
|
+
# Copy selected SSH keys (preserve directory structure exactly)
|
|
1456
|
+
for key in "${SELECTED_SSH_KEYS[@]}"; do
|
|
1457
|
+
echo " → Copying $key..."
|
|
1458
|
+
src_file="$HOME/.ssh/$key"
|
|
1459
|
+
dst_file="$GIT_CACHE_DIR/ssh/$key"
|
|
1460
|
+
|
|
1461
|
+
# Create parent directory with correct permissions
|
|
1462
|
+
dst_dir=$(dirname "$dst_file")
|
|
1463
|
+
mkdir -p "$dst_dir"
|
|
1464
|
+
chmod 700 "$dst_dir"
|
|
1465
|
+
|
|
1466
|
+
# Copy the file and set permissions
|
|
1467
|
+
if [ -f "$src_file" ]; then
|
|
1468
|
+
cp "$src_file" "$dst_file"
|
|
1469
|
+
chmod 600 "$dst_file"
|
|
1470
|
+
else
|
|
1471
|
+
echo " ⚠️ Warning: $src_file not found, skipping"
|
|
1472
|
+
fi
|
|
1473
|
+
done
|
|
1474
|
+
|
|
1475
|
+
# Copy filtered SSH config (only hosts needed for this repo)
|
|
1476
|
+
if [ -f "$HOME/.ssh/config" ]; then
|
|
1477
|
+
echo " → Generating filtered SSH config..."
|
|
1478
|
+
# Run setup-ssh-config to get filtered config
|
|
1479
|
+
SETUP_SSH="$SCRIPT_DIR/setup-ssh-config"
|
|
1480
|
+
if [ -x "$SETUP_SSH" ]; then
|
|
1481
|
+
# Join SELECTED_SSH_KEYS into a comma-separated string for --keys
|
|
1482
|
+
KEYS_ARG=$(IFS=,; echo "${SELECTED_SSH_KEYS[*]}")
|
|
1483
|
+
# Run it and capture the filtered config path
|
|
1484
|
+
output=$( "$SETUP_SSH" --keys "$KEYS_ARG" 2>&1 ) || true
|
|
1485
|
+
TEMP_CONFIG=$(echo "$output" | grep "Config:" | tail -1 | awk '{print $NF}')
|
|
1486
|
+
if [ -f "$TEMP_CONFIG" ]; then
|
|
1487
|
+
cp "$TEMP_CONFIG" "$GIT_CACHE_DIR/ssh/config" 2>/dev/null || true
|
|
1488
|
+
chmod 600 "$GIT_CACHE_DIR/ssh/config" 2>/dev/null || true
|
|
1489
|
+
else
|
|
1490
|
+
# Fallback to copying full config if setup-ssh-config fails
|
|
1491
|
+
cp "$HOME/.ssh/config" "$GIT_CACHE_DIR/ssh/config" 2>/dev/null || true
|
|
1492
|
+
chmod 600 "$GIT_CACHE_DIR/ssh/config" 2>/dev/null || true
|
|
1493
|
+
fi
|
|
1359
1494
|
else
|
|
1360
|
-
# Fallback
|
|
1495
|
+
# Fallback: copy full config
|
|
1361
1496
|
cp "$HOME/.ssh/config" "$GIT_CACHE_DIR/ssh/config" 2>/dev/null || true
|
|
1362
1497
|
chmod 600 "$GIT_CACHE_DIR/ssh/config" 2>/dev/null || true
|
|
1363
1498
|
fi
|
|
1364
|
-
else
|
|
1365
|
-
# Fallback: copy full config
|
|
1366
|
-
cp "$HOME/.ssh/config" "$GIT_CACHE_DIR/ssh/config" 2>/dev/null || true
|
|
1367
|
-
chmod 600 "$GIT_CACHE_DIR/ssh/config" 2>/dev/null || true
|
|
1368
1499
|
fi
|
|
1369
|
-
fi
|
|
1370
1500
|
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1501
|
+
if [ -f "$HOME/.ssh/known_hosts" ]; then
|
|
1502
|
+
echo " → Copying known_hosts..."
|
|
1503
|
+
cp "$HOME/.ssh/known_hosts" "$GIT_CACHE_DIR/ssh/known_hosts" 2>/dev/null || true
|
|
1504
|
+
chmod 600 "$GIT_CACHE_DIR/ssh/known_hosts" 2>/dev/null || true
|
|
1505
|
+
fi
|
|
1376
1506
|
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1507
|
+
# Ensure all directories and files have correct permissions (recursive)
|
|
1508
|
+
chmod 700 "$GIT_CACHE_DIR/ssh"
|
|
1509
|
+
find "$GIT_CACHE_DIR/ssh" -type d -exec chmod 700 {} \;
|
|
1510
|
+
find "$GIT_CACHE_DIR/ssh" -type f ! -name "config" ! -name "known_hosts" -exec chmod 600 {} \;
|
|
1381
1511
|
|
|
1382
|
-
|
|
1512
|
+
GIT_MOUNTS="$GIT_MOUNTS -v $GIT_CACHE_DIR/ssh:/home/agent/.ssh:ro"
|
|
1383
1513
|
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1514
|
+
# Copy gitconfig
|
|
1515
|
+
if [ -f "$HOME/.gitconfig" ]; then
|
|
1516
|
+
echo " → Copying .gitconfig..."
|
|
1517
|
+
cp "$HOME/.gitconfig" "$HOME_DIR/.gitconfig" 2>/dev/null || true
|
|
1518
|
+
fi
|
|
1389
1519
|
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
fi
|
|
1393
|
-
if [ "$git_choice" = "2" ]; then
|
|
1394
|
-
# Save workspace and selected keys for future sessions
|
|
1395
|
-
echo "$CURRENT_DIR" >> "$GIT_ALLOWED_FILE"
|
|
1396
|
-
# Also save to config.json
|
|
1397
|
-
if has_jq && [[ -f "$AI_SANDBOX_CONFIG" ]]; then
|
|
1398
|
-
jq --arg ws "$CURRENT_DIR" '.git.allowedWorkspaces += [$ws] | .git.allowedWorkspaces |= unique' "$AI_SANDBOX_CONFIG" > "$AI_SANDBOX_CONFIG.tmp" \
|
|
1399
|
-
&& mv "$AI_SANDBOX_CONFIG.tmp" "$AI_SANDBOX_CONFIG"
|
|
1400
|
-
chmod 600 "$AI_SANDBOX_CONFIG"
|
|
1520
|
+
if [[ "$git_choice" == "4" || "$git_choice" == "5" ]]; then
|
|
1521
|
+
GIT_FETCH_ONLY_MODE=true
|
|
1401
1522
|
fi
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1523
|
+
if [ "$git_choice" = "2" ]; then
|
|
1524
|
+
# Save workspace and selected keys for future sessions
|
|
1525
|
+
echo "$CURRENT_DIR" >> "$GIT_ALLOWED_FILE"
|
|
1526
|
+
# Also save to config.json
|
|
1527
|
+
if has_jq && [[ -f "$AI_SANDBOX_CONFIG" ]]; then
|
|
1528
|
+
jq --arg ws "$CURRENT_DIR" '.git.allowedWorkspaces += [$ws] | .git.allowedWorkspaces |= unique' "$AI_SANDBOX_CONFIG" > "$AI_SANDBOX_CONFIG.tmp" \
|
|
1529
|
+
&& mv "$AI_SANDBOX_CONFIG.tmp" "$AI_SANDBOX_CONFIG"
|
|
1530
|
+
chmod 600 "$AI_SANDBOX_CONFIG"
|
|
1531
|
+
fi
|
|
1532
|
+
# Save selected keys (one per line for easier parsing)
|
|
1533
|
+
WORKSPACE_MD5=$(echo "$CURRENT_DIR" | md5sum | cut -c1-8)
|
|
1534
|
+
mkdir -p "$GIT_SHARED_DIR/keys"
|
|
1535
|
+
printf "%s\n" "${SELECTED_SSH_KEYS[@]}" > "$GIT_SHARED_DIR/keys/$WORKSPACE_MD5"
|
|
1536
|
+
echo "✅ Git access enabled and saved for: $CURRENT_DIR"
|
|
1537
|
+
elif [ "$git_choice" = "5" ]; then
|
|
1538
|
+
# Save workspace to fetchOnlyWorkspaces
|
|
1539
|
+
if has_jq && [[ -f "$AI_SANDBOX_CONFIG" ]]; then
|
|
1540
|
+
jq --arg ws "$CURRENT_DIR" '.git.fetchOnlyWorkspaces = ((.git.fetchOnlyWorkspaces // []) + [$ws] | unique)' "$AI_SANDBOX_CONFIG" > "$AI_SANDBOX_CONFIG.tmp" \
|
|
1541
|
+
&& mv "$AI_SANDBOX_CONFIG.tmp" "$AI_SANDBOX_CONFIG"
|
|
1542
|
+
chmod 600 "$AI_SANDBOX_CONFIG"
|
|
1543
|
+
fi
|
|
1544
|
+
# Save selected keys (one per line for easier parsing)
|
|
1545
|
+
WORKSPACE_MD5=$(echo "$CURRENT_DIR" | md5sum | cut -c1-8)
|
|
1546
|
+
mkdir -p "$GIT_SHARED_DIR/keys"
|
|
1547
|
+
printf "%s\n" "${SELECTED_SSH_KEYS[@]}" > "$GIT_SHARED_DIR/keys/$WORKSPACE_MD5"
|
|
1548
|
+
echo "✅ Git fetch-only access enabled and saved for: $CURRENT_DIR"
|
|
1549
|
+
elif [ "$git_choice" = "4" ]; then
|
|
1550
|
+
echo "✅ Git fetch-only access enabled for this session"
|
|
1551
|
+
else
|
|
1552
|
+
echo "✅ Git access enabled for this session"
|
|
1413
1553
|
fi
|
|
1414
|
-
# Save selected keys (one per line for easier parsing)
|
|
1415
|
-
WORKSPACE_MD5=$(echo "$CURRENT_DIR" | md5sum | cut -c1-8)
|
|
1416
|
-
mkdir -p "$GIT_SHARED_DIR/keys"
|
|
1417
|
-
printf "%s\n" "${SELECTED_SSH_KEYS[@]}" > "$GIT_SHARED_DIR/keys/$WORKSPACE_MD5"
|
|
1418
|
-
echo "✅ Git fetch-only access enabled and saved for: $CURRENT_DIR"
|
|
1419
|
-
elif [ "$git_choice" = "4" ]; then
|
|
1420
|
-
echo "✅ Git fetch-only access enabled for this session"
|
|
1421
1554
|
else
|
|
1422
|
-
echo "
|
|
1555
|
+
echo "⚠️ No SSH keys selected. Git access disabled."
|
|
1423
1556
|
fi
|
|
1424
1557
|
else
|
|
1425
|
-
echo "⚠️
|
|
1558
|
+
echo "⚠️ SSH key selection cancelled. Git access disabled."
|
|
1426
1559
|
fi
|
|
1427
|
-
else
|
|
1428
|
-
echo "⚠️ SSH key selection cancelled. Git access disabled."
|
|
1429
1560
|
fi
|
|
1430
1561
|
;;
|
|
1431
1562
|
*)
|
|
@@ -1600,7 +1731,7 @@ resolve_opencode_password() {
|
|
|
1600
1731
|
echo " 3) No password (⚠️ unsecured - localhost only)"
|
|
1601
1732
|
echo ""
|
|
1602
1733
|
read -p "Choice [1-3]: " password_choice
|
|
1603
|
-
|
|
1734
|
+
|
|
1604
1735
|
case "$password_choice" in
|
|
1605
1736
|
1)
|
|
1606
1737
|
local generated_password=$(openssl rand -base64 24 | tr -d '/+=' | head -c 24)
|
|
@@ -1669,11 +1800,11 @@ is_mcp_installed() {
|
|
|
1669
1800
|
is_mcp_configured() {
|
|
1670
1801
|
local mcp_name="$1"
|
|
1671
1802
|
local config_file="$HOME/.config/opencode/opencode.json"
|
|
1672
|
-
|
|
1803
|
+
|
|
1673
1804
|
if [[ ! -f "$config_file" ]]; then
|
|
1674
1805
|
return 1
|
|
1675
1806
|
fi
|
|
1676
|
-
|
|
1807
|
+
|
|
1677
1808
|
if has_jq; then
|
|
1678
1809
|
jq -e --arg name "$mcp_name" '.mcp[$name] // empty' "$config_file" &>/dev/null
|
|
1679
1810
|
else
|
|
@@ -1687,7 +1818,7 @@ add_mcp_config() {
|
|
|
1687
1818
|
local mcp_command="$2"
|
|
1688
1819
|
local force="${3:-false}"
|
|
1689
1820
|
local config_file="$HOME/.config/opencode/opencode.json"
|
|
1690
|
-
|
|
1821
|
+
|
|
1691
1822
|
# Check if already configured and warn user
|
|
1692
1823
|
if [[ "$force" != "true" ]] && is_mcp_configured "$mcp_name"; then
|
|
1693
1824
|
echo ""
|
|
@@ -1700,9 +1831,9 @@ add_mcp_config() {
|
|
|
1700
1831
|
return 1
|
|
1701
1832
|
fi
|
|
1702
1833
|
fi
|
|
1703
|
-
|
|
1834
|
+
|
|
1704
1835
|
mkdir -p "$(dirname "$config_file")"
|
|
1705
|
-
|
|
1836
|
+
|
|
1706
1837
|
if [[ ! -f "$config_file" ]]; then
|
|
1707
1838
|
# Create new config with MCP
|
|
1708
1839
|
echo "{\"mcp\": {\"$mcp_name\": {\"type\": \"local\", \"command\": $mcp_command}}}" > "$config_file"
|
|
@@ -1735,47 +1866,47 @@ configure_opencode_mcp() {
|
|
|
1735
1866
|
# Only run for opencode tool in interactive mode
|
|
1736
1867
|
[[ "$TOOL" != "opencode" ]] && return 0
|
|
1737
1868
|
[[ ! -t 0 ]] && return 0
|
|
1738
|
-
|
|
1869
|
+
|
|
1739
1870
|
local config_file="$HOME/.config/opencode/opencode.json"
|
|
1740
|
-
|
|
1871
|
+
|
|
1741
1872
|
# Generate workspace hash for skip tracking
|
|
1742
1873
|
WORKSPACE_MD5=$(echo "$CURRENT_DIR" | md5sum | cut -c1-8)
|
|
1743
|
-
|
|
1874
|
+
|
|
1744
1875
|
# Check if already skipped for this workspace
|
|
1745
1876
|
if is_mcp_skipped; then
|
|
1746
1877
|
return 0
|
|
1747
1878
|
fi
|
|
1748
|
-
|
|
1879
|
+
|
|
1749
1880
|
# Detect installed MCP tools (from config.json, set during setup.sh)
|
|
1750
1881
|
local chrome_installed=false
|
|
1751
1882
|
local playwright_installed=false
|
|
1752
1883
|
local chrome_configured=false
|
|
1753
1884
|
local playwright_configured=false
|
|
1754
|
-
|
|
1885
|
+
|
|
1755
1886
|
if is_mcp_installed "chrome-devtools"; then
|
|
1756
1887
|
chrome_installed=true
|
|
1757
1888
|
is_mcp_configured "chrome-devtools" && chrome_configured=true
|
|
1758
1889
|
fi
|
|
1759
|
-
|
|
1890
|
+
|
|
1760
1891
|
if is_mcp_installed "playwright"; then
|
|
1761
1892
|
playwright_installed=true
|
|
1762
1893
|
is_mcp_configured "playwright" && playwright_configured=true
|
|
1763
1894
|
fi
|
|
1764
|
-
|
|
1895
|
+
|
|
1765
1896
|
# If no MCP tools installed in image, return
|
|
1766
1897
|
if [[ "$chrome_installed" == "false" && "$playwright_installed" == "false" ]]; then
|
|
1767
1898
|
return 0
|
|
1768
1899
|
fi
|
|
1769
|
-
|
|
1900
|
+
|
|
1770
1901
|
# If all installed tools are already configured, return silently
|
|
1771
1902
|
local all_configured=true
|
|
1772
1903
|
[[ "$chrome_installed" == "true" && "$chrome_configured" == "false" ]] && all_configured=false
|
|
1773
1904
|
[[ "$playwright_installed" == "true" && "$playwright_configured" == "false" ]] && all_configured=false
|
|
1774
|
-
|
|
1905
|
+
|
|
1775
1906
|
if [[ "$all_configured" == "true" ]]; then
|
|
1776
1907
|
return 0
|
|
1777
1908
|
fi
|
|
1778
|
-
|
|
1909
|
+
|
|
1779
1910
|
# Build lists of configured and unconfigured tools
|
|
1780
1911
|
local unconfigured=()
|
|
1781
1912
|
local configured=()
|
|
@@ -1783,12 +1914,12 @@ configure_opencode_mcp() {
|
|
|
1783
1914
|
[[ "$chrome_installed" == "true" && "$chrome_configured" == "true" ]] && configured+=("chrome-devtools")
|
|
1784
1915
|
[[ "$playwright_installed" == "true" && "$playwright_configured" == "false" ]] && unconfigured+=("playwright")
|
|
1785
1916
|
[[ "$playwright_installed" == "true" && "$playwright_configured" == "true" ]] && configured+=("playwright")
|
|
1786
|
-
|
|
1917
|
+
|
|
1787
1918
|
# Show prompt
|
|
1788
1919
|
echo ""
|
|
1789
1920
|
echo "🔌 MCP Tools Detected"
|
|
1790
1921
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
1791
|
-
|
|
1922
|
+
|
|
1792
1923
|
# Show already configured tools
|
|
1793
1924
|
if [[ ${#configured[@]} -gt 0 ]]; then
|
|
1794
1925
|
echo "Already configured:"
|
|
@@ -1804,7 +1935,7 @@ configure_opencode_mcp() {
|
|
|
1804
1935
|
done
|
|
1805
1936
|
echo ""
|
|
1806
1937
|
fi
|
|
1807
|
-
|
|
1938
|
+
|
|
1808
1939
|
# Show unconfigured tools
|
|
1809
1940
|
if [[ ${#unconfigured[@]} -gt 0 ]]; then
|
|
1810
1941
|
echo "Not yet configured:"
|
|
@@ -1820,7 +1951,7 @@ configure_opencode_mcp() {
|
|
|
1820
1951
|
done
|
|
1821
1952
|
echo ""
|
|
1822
1953
|
fi
|
|
1823
|
-
|
|
1954
|
+
|
|
1824
1955
|
echo "Configure MCP tools in OpenCode?"
|
|
1825
1956
|
echo " 1) Yes, configure all detected tools"
|
|
1826
1957
|
echo " 2) Yes, let me choose which ones"
|
|
@@ -1828,16 +1959,16 @@ configure_opencode_mcp() {
|
|
|
1828
1959
|
echo " 4) No, don't ask again for this workspace"
|
|
1829
1960
|
echo ""
|
|
1830
1961
|
read -p "Choice [1-4]: " mcp_choice
|
|
1831
|
-
|
|
1962
|
+
|
|
1832
1963
|
local configured_any=false
|
|
1833
|
-
|
|
1964
|
+
|
|
1834
1965
|
case "$mcp_choice" in
|
|
1835
1966
|
1)
|
|
1836
1967
|
# Configure all (both unconfigured and offer to reconfigure configured ones)
|
|
1837
1968
|
local all_tools=()
|
|
1838
1969
|
[[ "$chrome_installed" == "true" ]] && all_tools+=("chrome-devtools")
|
|
1839
1970
|
[[ "$playwright_installed" == "true" ]] && all_tools+=("playwright")
|
|
1840
|
-
|
|
1971
|
+
|
|
1841
1972
|
for tool in "${all_tools[@]}"; do
|
|
1842
1973
|
case "$tool" in
|
|
1843
1974
|
chrome-devtools)
|
|
@@ -1847,7 +1978,7 @@ configure_opencode_mcp() {
|
|
|
1847
1978
|
fi
|
|
1848
1979
|
;;
|
|
1849
1980
|
playwright)
|
|
1850
|
-
if add_mcp_config "playwright" '["
|
|
1981
|
+
if add_mcp_config "playwright" '["playwright-mcp", "--headless", "--browser", "chromium"]'; then
|
|
1851
1982
|
echo " ✓ Configured Playwright MCP"
|
|
1852
1983
|
configured_any=true
|
|
1853
1984
|
fi
|
|
@@ -1860,7 +1991,7 @@ configure_opencode_mcp() {
|
|
|
1860
1991
|
local all_tools=()
|
|
1861
1992
|
[[ "$chrome_installed" == "true" ]] && all_tools+=("chrome-devtools")
|
|
1862
1993
|
[[ "$playwright_installed" == "true" ]] && all_tools+=("playwright")
|
|
1863
|
-
|
|
1994
|
+
|
|
1864
1995
|
for tool in "${all_tools[@]}"; do
|
|
1865
1996
|
local tool_desc=""
|
|
1866
1997
|
local status_hint=""
|
|
@@ -1884,7 +2015,7 @@ configure_opencode_mcp() {
|
|
|
1884
2015
|
fi
|
|
1885
2016
|
;;
|
|
1886
2017
|
playwright)
|
|
1887
|
-
if add_mcp_config "playwright" '["
|
|
2018
|
+
if add_mcp_config "playwright" '["playwright-mcp", "--headless", "--browser", "chromium"]'; then
|
|
1888
2019
|
echo " ✓ Configured"
|
|
1889
2020
|
configured_any=true
|
|
1890
2021
|
fi
|
|
@@ -1903,7 +2034,7 @@ configure_opencode_mcp() {
|
|
|
1903
2034
|
echo "ℹ️ Skipped."
|
|
1904
2035
|
;;
|
|
1905
2036
|
esac
|
|
1906
|
-
|
|
2037
|
+
|
|
1907
2038
|
# Show config file path for easy editing
|
|
1908
2039
|
echo ""
|
|
1909
2040
|
if [[ "$configured_any" == "true" ]]; then
|
|
@@ -1969,7 +2100,7 @@ check_port_in_use() {
|
|
|
1969
2100
|
else
|
|
1970
2101
|
return 2
|
|
1971
2102
|
fi
|
|
1972
|
-
|
|
2103
|
+
|
|
1973
2104
|
docker ps --format "{{.Ports}}" 2>/dev/null | grep -q ":$port->" && return 0
|
|
1974
2105
|
return 1
|
|
1975
2106
|
}
|
|
@@ -2023,10 +2154,10 @@ if detect_opencode_web; then
|
|
|
2023
2154
|
if [[ -z "$WEB_PORT" ]]; then
|
|
2024
2155
|
WEB_PORT=4096
|
|
2025
2156
|
fi
|
|
2026
|
-
|
|
2157
|
+
|
|
2027
2158
|
add_port_to_list "$WEB_PORT" "auto-detected"
|
|
2028
2159
|
echo "🌐 Detected web command. Auto-exposing port $WEB_PORT."
|
|
2029
|
-
|
|
2160
|
+
|
|
2030
2161
|
if ! has_hostname_arg; then
|
|
2031
2162
|
TOOL_ARGS+=("--hostname" "0.0.0.0")
|
|
2032
2163
|
fi
|
|
@@ -2081,7 +2212,7 @@ if [[ -n "$EXPOSE_PORTS_LIST" ]]; then
|
|
|
2081
2212
|
done
|
|
2082
2213
|
|
|
2083
2214
|
echo "🔌 Port mappings: $EXPOSE_PORTS_LIST"
|
|
2084
|
-
|
|
2215
|
+
|
|
2085
2216
|
if [[ "$WEB_DETECTED" == "true" ]]; then
|
|
2086
2217
|
echo "🌐 Web UI available at http://localhost:$WEB_PORT"
|
|
2087
2218
|
fi
|
|
@@ -2101,6 +2232,128 @@ if [[ "${AI_RUN_DEBUG:-}" == "1" ]]; then
|
|
|
2101
2232
|
echo "🔧 Debug: EXPOSE_PORTS_LIST='$EXPOSE_PORTS_LIST'"
|
|
2102
2233
|
fi
|
|
2103
2234
|
|
|
2235
|
+
is_nano_brain_command() {
|
|
2236
|
+
if [[ "$TOOL" == "nano-brain" ]]; then
|
|
2237
|
+
return 0
|
|
2238
|
+
fi
|
|
2239
|
+
|
|
2240
|
+
if [[ "$TOOL" == "npx" ]]; then
|
|
2241
|
+
for arg in "${TOOL_ARGS[@]}"; do
|
|
2242
|
+
if [[ "$arg" == "nano-brain" ]]; then
|
|
2243
|
+
return 0
|
|
2244
|
+
fi
|
|
2245
|
+
done
|
|
2246
|
+
fi
|
|
2247
|
+
|
|
2248
|
+
return 1
|
|
2249
|
+
}
|
|
2250
|
+
|
|
2251
|
+
NANO_BRAIN_AUTOREPAIR_SCRIPT='
|
|
2252
|
+
set -e
|
|
2253
|
+
ORIG_CMD=("$@")
|
|
2254
|
+
REPAIR_PATTERN="(tree-sitter|native binding|Cannot find module.*tree-sitter|compiled against a different Node.js version|Exec format error|invalid ELF header|Native bindings not available)"
|
|
2255
|
+
|
|
2256
|
+
echo "🔎 nano-brain preflight: checking runtime cache paths..."
|
|
2257
|
+
mkdir -p /home/agent/.npm /home/agent/.cache/node-gyp 2>/dev/null || true
|
|
2258
|
+
|
|
2259
|
+
run_with_capture() {
|
|
2260
|
+
local err_file
|
|
2261
|
+
err_file=$(mktemp)
|
|
2262
|
+
|
|
2263
|
+
set +e
|
|
2264
|
+
"${ORIG_CMD[@]}" 2>"$err_file"
|
|
2265
|
+
local exit_code=$?
|
|
2266
|
+
set -e
|
|
2267
|
+
|
|
2268
|
+
if [[ $exit_code -ne 0 ]] && grep -Eqi "$REPAIR_PATTERN" "$err_file"; then
|
|
2269
|
+
cat "$err_file" >&2
|
|
2270
|
+
echo "⚠️ Detected nano-brain native module issue."
|
|
2271
|
+
echo "🔧 Running automatic repair (clearing npx/node-gyp caches)..."
|
|
2272
|
+
rm -rf /home/agent/.npm/_npx /home/agent/.cache/node-gyp 2>/dev/null || true
|
|
2273
|
+
npm cache clean --force >/dev/null 2>&1 || true
|
|
2274
|
+
echo "🔁 Retrying nano-brain command once..."
|
|
2275
|
+
set +e
|
|
2276
|
+
"${ORIG_CMD[@]}"
|
|
2277
|
+
local retry_code=$?
|
|
2278
|
+
set -e
|
|
2279
|
+
rm -f "$err_file"
|
|
2280
|
+
return $retry_code
|
|
2281
|
+
fi
|
|
2282
|
+
|
|
2283
|
+
if [[ $exit_code -eq 0 ]] && grep -Eqi "(\\[treesitter\\] Native bindings not available|symbol graph disabled|tree-sitter-typescript\\.node|No such file or directory)" "$err_file"; then
|
|
2284
|
+
if [[ "${AI_RUN_DEBUG:-}" == "1" ]]; then
|
|
2285
|
+
echo "ℹ️ nano-brain: non-fatal tree-sitter warning captured." >&2
|
|
2286
|
+
cat "$err_file" >&2
|
|
2287
|
+
else
|
|
2288
|
+
grep -Eiv "(\\[treesitter\\] Native bindings not available|symbol graph disabled|tree-sitter-typescript\\.node|No such file or directory)" "$err_file" >&2 || true
|
|
2289
|
+
fi
|
|
2290
|
+
rm -f "$err_file"
|
|
2291
|
+
return 0
|
|
2292
|
+
fi
|
|
2293
|
+
|
|
2294
|
+
cat "$err_file" >&2
|
|
2295
|
+
rm -f "$err_file"
|
|
2296
|
+
return $exit_code
|
|
2297
|
+
}
|
|
2298
|
+
|
|
2299
|
+
run_with_capture
|
|
2300
|
+
'
|
|
2301
|
+
|
|
2302
|
+
NANO_BRAIN_SHELL_HOOK=$(cat <<'EOF'
|
|
2303
|
+
nano_brain_shell_wrapper() {
|
|
2304
|
+
local ORIG_CMD=("$@")
|
|
2305
|
+
local REPAIR_PATTERN="(tree-sitter|native binding|Cannot find module.*tree-sitter|compiled against a different Node.js version|Exec format error|invalid ELF header|Native bindings not available)"
|
|
2306
|
+
local WARN_PATTERN="(\\[treesitter\\] Native bindings not available|symbol graph disabled|tree-sitter-typescript\\.node|No such file or directory)"
|
|
2307
|
+
|
|
2308
|
+
local err_file
|
|
2309
|
+
err_file=$(mktemp)
|
|
2310
|
+
|
|
2311
|
+
set +e
|
|
2312
|
+
"${ORIG_CMD[@]}" 2>"$err_file"
|
|
2313
|
+
local exit_code=$?
|
|
2314
|
+
set -e
|
|
2315
|
+
|
|
2316
|
+
if [[ $exit_code -ne 0 ]] && grep -Eqi "$REPAIR_PATTERN" "$err_file"; then
|
|
2317
|
+
cat "$err_file" >&2
|
|
2318
|
+
echo "⚠️ Detected nano-brain native module issue."
|
|
2319
|
+
echo "🔧 Running automatic repair (clearing npx/node-gyp caches)..."
|
|
2320
|
+
rm -rf /home/agent/.npm/_npx /home/agent/.cache/node-gyp 2>/dev/null || true
|
|
2321
|
+
npm cache clean --force >/dev/null 2>&1 || true
|
|
2322
|
+
echo "🔁 Retrying nano-brain command once..."
|
|
2323
|
+
"${ORIG_CMD[@]}"
|
|
2324
|
+
local retry_code=$?
|
|
2325
|
+
rm -f "$err_file"
|
|
2326
|
+
return $retry_code
|
|
2327
|
+
fi
|
|
2328
|
+
|
|
2329
|
+
if [[ $exit_code -eq 0 ]] && grep -Eqi "$WARN_PATTERN" "$err_file"; then
|
|
2330
|
+
if [[ "${AI_RUN_DEBUG:-}" == "1" ]]; then
|
|
2331
|
+
echo "ℹ️ nano-brain: non-fatal tree-sitter warning captured." >&2
|
|
2332
|
+
cat "$err_file" >&2
|
|
2333
|
+
else
|
|
2334
|
+
grep -Eiv "$WARN_PATTERN" "$err_file" >&2 || true
|
|
2335
|
+
fi
|
|
2336
|
+
rm -f "$err_file"
|
|
2337
|
+
return 0
|
|
2338
|
+
fi
|
|
2339
|
+
|
|
2340
|
+
cat "$err_file" >&2
|
|
2341
|
+
rm -f "$err_file"
|
|
2342
|
+
return $exit_code
|
|
2343
|
+
}
|
|
2344
|
+
|
|
2345
|
+
npx() {
|
|
2346
|
+
if [[ "${1:-}" == "nano-brain" ]]; then
|
|
2347
|
+
nano_brain_shell_wrapper command npx "$@"
|
|
2348
|
+
return $?
|
|
2349
|
+
fi
|
|
2350
|
+
command npx "$@"
|
|
2351
|
+
}
|
|
2352
|
+
|
|
2353
|
+
export -f nano_brain_shell_wrapper npx
|
|
2354
|
+
EOF
|
|
2355
|
+
)
|
|
2356
|
+
|
|
2104
2357
|
# Prepare command based on mode
|
|
2105
2358
|
ENTRYPOINT_OVERRIDE=""
|
|
2106
2359
|
if [[ -n "$TOOL" && "$SHELL_MODE" != "true" ]]; then
|
|
@@ -2133,6 +2386,20 @@ else
|
|
|
2133
2386
|
DOCKER_COMMAND=("${TOOL_ARGS[@]}")
|
|
2134
2387
|
fi
|
|
2135
2388
|
|
|
2389
|
+
# Nano-brain targeted preflight + auto-repair wrapper
|
|
2390
|
+
if [[ "$SHELL_MODE" == "true" ]] && [[ "$NANO_BRAIN_AUTO_REPAIR" == "true" ]] && [[ "${DOCKER_COMMAND[0]:-}" == "-c" ]]; then
|
|
2391
|
+
DOCKER_COMMAND[1]="$NANO_BRAIN_SHELL_HOOK ${DOCKER_COMMAND[1]}"
|
|
2392
|
+
fi
|
|
2393
|
+
|
|
2394
|
+
if [[ "$SHELL_MODE" != "true" ]] && is_nano_brain_command; then
|
|
2395
|
+
if [[ "$NANO_BRAIN_AUTO_REPAIR" == "true" ]]; then
|
|
2396
|
+
ENTRYPOINT_OVERRIDE="--entrypoint bash"
|
|
2397
|
+
DOCKER_COMMAND=("-lc" "$NANO_BRAIN_AUTOREPAIR_SCRIPT" "nano-brain-wrapper" "$TOOL" "${TOOL_ARGS[@]}")
|
|
2398
|
+
else
|
|
2399
|
+
echo "ℹ️ nano-brain auto-repair disabled"
|
|
2400
|
+
fi
|
|
2401
|
+
fi
|
|
2402
|
+
|
|
2136
2403
|
# Detect platform architecture (avoid slow emulation)
|
|
2137
2404
|
PLATFORM="${AI_RUN_PLATFORM:-}"
|
|
2138
2405
|
if [[ -z "$PLATFORM" ]]; then
|
|
@@ -2184,20 +2451,20 @@ DISPLAY_FLAGS=$(detect_display_config)
|
|
|
2184
2451
|
# ============================================================================
|
|
2185
2452
|
if [[ "$TOOL" == "openclaw" ]]; then
|
|
2186
2453
|
OPENCLAW_REPO_DIR="$HOME/.ai-sandbox/tools/openclaw/repo"
|
|
2187
|
-
|
|
2454
|
+
|
|
2188
2455
|
if [[ ! -d "$OPENCLAW_REPO_DIR" ]]; then
|
|
2189
2456
|
echo "❌ ERROR: OpenClaw repository not found at $OPENCLAW_REPO_DIR"
|
|
2190
2457
|
echo " Run: npx @kokorolx/ai-sandbox-wrapper setup"
|
|
2191
2458
|
exit 1
|
|
2192
2459
|
fi
|
|
2193
|
-
|
|
2460
|
+
|
|
2194
2461
|
cd "$OPENCLAW_REPO_DIR"
|
|
2195
|
-
|
|
2462
|
+
|
|
2196
2463
|
OPENCLAW_COMPOSE_FILE="$OPENCLAW_REPO_DIR/docker-compose.yml"
|
|
2197
2464
|
OPENCLAW_OVERRIDE_FILE="$OPENCLAW_REPO_DIR/docker-compose.override.yml"
|
|
2198
|
-
|
|
2465
|
+
|
|
2199
2466
|
echo "🔄 Generating OpenClaw docker-compose override..."
|
|
2200
|
-
|
|
2467
|
+
|
|
2201
2468
|
cat > "$OPENCLAW_OVERRIDE_FILE" <<EOF
|
|
2202
2469
|
services:
|
|
2203
2470
|
openclaw-gateway:
|
|
@@ -2207,18 +2474,18 @@ services:
|
|
|
2207
2474
|
volumes:
|
|
2208
2475
|
- $HOME/.openclaw:/home/node/.openclaw
|
|
2209
2476
|
EOF
|
|
2210
|
-
|
|
2477
|
+
|
|
2211
2478
|
for workspace in "${WORKSPACES[@]}"; do
|
|
2212
2479
|
echo " - $workspace:$workspace" >> "$OPENCLAW_OVERRIDE_FILE"
|
|
2213
2480
|
done
|
|
2214
|
-
|
|
2481
|
+
|
|
2215
2482
|
cat >> "$OPENCLAW_OVERRIDE_FILE" <<EOF
|
|
2216
2483
|
ports:
|
|
2217
2484
|
- "18789:18789"
|
|
2218
2485
|
- "18790:18790"
|
|
2219
2486
|
working_dir: $CURRENT_DIR
|
|
2220
2487
|
EOF
|
|
2221
|
-
|
|
2488
|
+
|
|
2222
2489
|
if [[ -n "$NETWORK_OPTIONS" ]]; then
|
|
2223
2490
|
echo " networks:" >> "$OPENCLAW_OVERRIDE_FILE"
|
|
2224
2491
|
for net in ${DOCKER_NETWORKS//,/ }; do
|
|
@@ -2231,12 +2498,12 @@ EOF
|
|
|
2231
2498
|
echo " external: true" >> "$OPENCLAW_OVERRIDE_FILE"
|
|
2232
2499
|
done
|
|
2233
2500
|
fi
|
|
2234
|
-
|
|
2501
|
+
|
|
2235
2502
|
echo "🚀 Starting OpenClaw with docker-compose..."
|
|
2236
2503
|
echo "🌐 Gateway: http://localhost:18789"
|
|
2237
2504
|
echo "🌐 Bridge: http://localhost:18790"
|
|
2238
2505
|
echo ""
|
|
2239
|
-
|
|
2506
|
+
|
|
2240
2507
|
exec docker compose -f "$OPENCLAW_COMPOSE_FILE" -f "$OPENCLAW_OVERRIDE_FILE" \
|
|
2241
2508
|
--env-file "$ENV_FILE" \
|
|
2242
2509
|
up --remove-orphans
|
|
@@ -2250,6 +2517,7 @@ docker run $CONTAINER_NAME --rm $TTY_FLAGS \
|
|
|
2250
2517
|
$CONFIG_MOUNT \
|
|
2251
2518
|
$TOOL_CONFIG_MOUNTS \
|
|
2252
2519
|
$GIT_MOUNTS \
|
|
2520
|
+
$SSH_AGENT_ENV \
|
|
2253
2521
|
$NETWORK_OPTIONS \
|
|
2254
2522
|
$DISPLAY_FLAGS \
|
|
2255
2523
|
$HOST_ACCESS_ARGS \
|
|
@@ -6,7 +6,10 @@ FROM node:22-bookworm-slim
|
|
|
6
6
|
|
|
7
7
|
ARG AGENT_UID=1001
|
|
8
8
|
|
|
9
|
-
RUN apt-get update && apt-get install -y --no-install-recommends git curl ssh ca-certificates jq python3 python3-pip python3-venv python3-dev python3-setuptools build-essential libopenblas-dev pipx unzip xclip wl-clipboard ripgrep && curl -LsSf https://astral.sh/uv/install.sh | UV_INSTALL_DIR=/usr/local/bin sh && rm -rf /var/lib/apt/lists/* && pipx ensurepath
|
|
9
|
+
RUN apt-get update && apt-get install -y --no-install-recommends git curl ssh ca-certificates jq python3 python3-pip python3-venv python3-dev python3-setuptools build-essential libopenblas-dev pipx unzip xclip wl-clipboard ripgrep tmux fd-find sqlite3 poppler-utils qpdf tesseract-ocr && curl -LsSf https://astral.sh/uv/install.sh | UV_INSTALL_DIR=/usr/local/bin sh && rm -rf /var/lib/apt/lists/* && pipx ensurepath
|
|
10
|
+
|
|
11
|
+
# Install Python PDF processing tools for PDF skill
|
|
12
|
+
RUN pip3 install --no-cache-dir --break-system-packages pypdf pdfplumber reportlab pytesseract pdf2image
|
|
10
13
|
|
|
11
14
|
RUN curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg && chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg && echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | tee /etc/apt/sources.list.d/github-cli.list > /dev/null && apt-get update && apt-get install -y gh && rm -rf /var/lib/apt/lists/*
|
|
12
15
|
|
|
@@ -17,7 +20,7 @@ RUN npm install -g bun
|
|
|
17
20
|
RUN npm install -g pnpm
|
|
18
21
|
|
|
19
22
|
# Install TypeScript and LSP tools using npm
|
|
20
|
-
RUN npm install -g typescript typescript-language-server
|
|
23
|
+
RUN npm install -g typescript typescript-language-server pyright vscode-langservers-extracted
|
|
21
24
|
|
|
22
25
|
# Verify installations
|
|
23
26
|
RUN node --version && npm --version && pnpm --version && tsc --version
|
|
@@ -84,6 +87,9 @@ RUN mkdir -p /opt/playwright-browsers && \
|
|
|
84
87
|
npx playwright-core install-deps chromium && \
|
|
85
88
|
chmod -R 777 /opt/playwright-browsers && \
|
|
86
89
|
ln -sf $(ls -d /opt/playwright-browsers/chromium-*/chrome-linux/chrome | head -1) /opt/chromium
|
|
90
|
+
ENV CHROME_DEVTOOLS_MCP_NO_USAGE_STATISTICS=1
|
|
91
|
+
RUN npm install -g chrome-devtools-mcp@latest && \
|
|
92
|
+
touch /opt/.mcp-chrome-devtools-installed
|
|
87
93
|
RUN touch /opt/.mcp-playwright-installed
|
|
88
94
|
|
|
89
95
|
# Create workspace
|
|
@@ -6,7 +6,10 @@ FROM node:22-bookworm-slim
|
|
|
6
6
|
|
|
7
7
|
ARG AGENT_UID=1001
|
|
8
8
|
|
|
9
|
-
RUN apt-get update && apt-get install -y --no-install-recommends git curl ssh ca-certificates jq python3 python3-pip python3-venv python3-dev python3-setuptools build-essential libopenblas-dev pipx unzip xclip wl-clipboard ripgrep && curl -LsSf https://astral.sh/uv/install.sh | UV_INSTALL_DIR=/usr/local/bin sh && rm -rf /var/lib/apt/lists/* && pipx ensurepath
|
|
9
|
+
RUN apt-get update && apt-get install -y --no-install-recommends git curl ssh ca-certificates jq python3 python3-pip python3-venv python3-dev python3-setuptools build-essential libopenblas-dev pipx unzip xclip wl-clipboard ripgrep tmux fd-find sqlite3 poppler-utils qpdf tesseract-ocr && curl -LsSf https://astral.sh/uv/install.sh | UV_INSTALL_DIR=/usr/local/bin sh && rm -rf /var/lib/apt/lists/* && pipx ensurepath
|
|
10
|
+
|
|
11
|
+
# Install Python PDF processing tools for PDF skill
|
|
12
|
+
RUN pip3 install --no-cache-dir --break-system-packages pypdf pdfplumber reportlab pytesseract pdf2image
|
|
10
13
|
|
|
11
14
|
RUN curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg && chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg && echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | tee /etc/apt/sources.list.d/github-cli.list > /dev/null && apt-get update && apt-get install -y gh && rm -rf /var/lib/apt/lists/*
|
|
12
15
|
|
|
@@ -17,7 +20,7 @@ RUN npm install -g bun
|
|
|
17
20
|
RUN npm install -g pnpm
|
|
18
21
|
|
|
19
22
|
# Install TypeScript and LSP tools using npm
|
|
20
|
-
RUN npm install -g typescript typescript-language-server
|
|
23
|
+
RUN npm install -g typescript typescript-language-server pyright vscode-langservers-extracted
|
|
21
24
|
|
|
22
25
|
# Verify installations
|
|
23
26
|
RUN node --version && npm --version && pnpm --version && tsc --version
|
|
@@ -84,6 +87,9 @@ RUN mkdir -p /opt/playwright-browsers && \
|
|
|
84
87
|
npx playwright-core install-deps chromium && \
|
|
85
88
|
chmod -R 777 /opt/playwright-browsers && \
|
|
86
89
|
ln -sf $(ls -d /opt/playwright-browsers/chromium-*/chrome-linux/chrome | head -1) /opt/chromium
|
|
90
|
+
ENV CHROME_DEVTOOLS_MCP_NO_USAGE_STATISTICS=1
|
|
91
|
+
RUN npm install -g chrome-devtools-mcp@latest && \
|
|
92
|
+
touch /opt/.mcp-chrome-devtools-installed
|
|
87
93
|
RUN touch /opt/.mcp-playwright-installed
|
|
88
94
|
|
|
89
95
|
# Create workspace
|
|
@@ -109,6 +115,20 @@ RUN curl -fsSL https://opencode.ai/install | bash && \
|
|
|
109
115
|
mv /root/.opencode/bin/opencode /usr/local/bin/opencode && \
|
|
110
116
|
rm -rf /root/.opencode
|
|
111
117
|
|
|
118
|
+
# === claude ===
|
|
119
|
+
USER root
|
|
120
|
+
RUN apt-get update && apt-get install -y --no-install-recommends tmux && rm -rf /var/lib/apt/lists/*
|
|
121
|
+
RUN npm install -g @kaitranntt/ccs --ignore-scripts && \
|
|
122
|
+
mkdir -p /home/agent/.ccs && \
|
|
123
|
+
chown -R agent:agent /home/agent/.ccs && \
|
|
124
|
+
which ccs && ccs --version && \
|
|
125
|
+
sed -i 's/fs\.symlinkSync(sourcePath, targetPath, symlinkType)/fs\.symlinkSync(require("path").relative(require("path").dirname(targetPath), sourcePath), targetPath, symlinkType)/g' /usr/local/lib/node_modules/@kaitranntt/ccs/dist/utils/claude-symlink-manager.js
|
|
126
|
+
RUN export HOME=/root && curl -fsSL https://claude.ai/install.sh | bash && \
|
|
127
|
+
mkdir -p /usr/local/share && \
|
|
128
|
+
mv /root/.local/share/claude /usr/local/share/claude && \
|
|
129
|
+
ln -sf /usr/local/share/claude/versions/$(ls /usr/local/share/claude/versions | head -1) /usr/local/bin/claude
|
|
130
|
+
USER agent
|
|
131
|
+
|
|
112
132
|
USER agent
|
|
113
133
|
ENV HOME=/home/agent
|
|
114
134
|
CMD ["bash"]
|
package/lib/AGENTS.md
CHANGED
|
@@ -49,6 +49,20 @@ lib/
|
|
|
49
49
|
| CodeServer | `install-codeserver.sh` | VSCode browser |
|
|
50
50
|
| VSCode | `install-vscode.sh` | Desktop X11 |
|
|
51
51
|
|
|
52
|
+
## File Writing Rules (MANDATORY)
|
|
53
|
+
|
|
54
|
+
**NEVER write an entire file at once.** Always use chunk-by-chunk editing:
|
|
55
|
+
|
|
56
|
+
1. **Use the Edit tool** (find-and-replace) for all file modifications — insert, update, or delete content in targeted chunks
|
|
57
|
+
2. **Only use the Write tool** for brand-new files that don't exist yet, AND only if the file is small (< 50 lines)
|
|
58
|
+
3. **For new large files (50+ lines):** Write a skeleton first (headers/structure only), then use Edit to fill in each section chunk by chunk
|
|
59
|
+
4. **Why:** Writing entire files at once causes truncation, context window overflow, and silent data loss on large files
|
|
60
|
+
|
|
61
|
+
**Anti-patterns (NEVER do these):**
|
|
62
|
+
- `Write` tool to overwrite an existing file with full content
|
|
63
|
+
- `Write` tool to create a file with 100+ lines in one shot
|
|
64
|
+
- Regenerating an entire file to change a few lines
|
|
65
|
+
|
|
52
66
|
## Conventions
|
|
53
67
|
|
|
54
68
|
- Scripts use `set -e` for fail-fast
|
package/lib/install-base.sh
CHANGED
|
@@ -128,7 +128,7 @@ ENV PATH=$RBENV_ROOT/bin:$RBENV_ROOT/shims:$PATH
|
|
|
128
128
|
|
|
129
129
|
RUN rbenv install 3.3.0 && rbenv global 3.3.0 && rbenv rehash
|
|
130
130
|
|
|
131
|
-
RUN gem install rails -v 8.0.2 && gem install bundler && rbenv rehash
|
|
131
|
+
RUN gem install rails -v 8.0.2 && gem install bundler solargraph && rbenv rehash
|
|
132
132
|
'
|
|
133
133
|
fi
|
|
134
134
|
|
|
@@ -217,10 +217,19 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
|
217
217
|
xclip \
|
|
218
218
|
wl-clipboard \
|
|
219
219
|
ripgrep \
|
|
220
|
+
tmux \
|
|
221
|
+
fd-find \
|
|
222
|
+
sqlite3 \
|
|
223
|
+
poppler-utils \
|
|
224
|
+
qpdf \
|
|
225
|
+
tesseract-ocr \
|
|
220
226
|
&& curl -LsSf https://astral.sh/uv/install.sh | UV_INSTALL_DIR=/usr/local/bin sh \
|
|
221
227
|
&& rm -rf /var/lib/apt/lists/* \
|
|
222
228
|
&& pipx ensurepath
|
|
223
229
|
|
|
230
|
+
# Install Python PDF processing tools for PDF skill
|
|
231
|
+
RUN pip3 install --no-cache-dir --break-system-packages pypdf pdfplumber reportlab pytesseract pdf2image
|
|
232
|
+
|
|
224
233
|
RUN curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg \
|
|
225
234
|
&& chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg \
|
|
226
235
|
&& echo "deb [arch=\$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | tee /etc/apt/sources.list.d/github-cli.list > /dev/null \
|
|
@@ -235,7 +244,7 @@ RUN npm install -g bun
|
|
|
235
244
|
RUN npm install -g pnpm
|
|
236
245
|
|
|
237
246
|
# Install TypeScript and LSP tools using npm
|
|
238
|
-
RUN npm install -g typescript typescript-language-server
|
|
247
|
+
RUN npm install -g typescript typescript-language-server pyright vscode-langservers-extracted
|
|
239
248
|
|
|
240
249
|
# Verify installations
|
|
241
250
|
RUN node --version && npm --version && pnpm --version && tsc --version
|
package/lib/install-claude.sh
CHANGED
|
@@ -8,10 +8,11 @@ RUN apt-get update && apt-get install -y --no-install-recommends tmux && rm -rf
|
|
|
8
8
|
RUN npm install -g @kaitranntt/ccs --ignore-scripts && \
|
|
9
9
|
mkdir -p /home/agent/.ccs && \
|
|
10
10
|
chown -R agent:agent /home/agent/.ccs && \
|
|
11
|
-
which ccs && ccs --version
|
|
12
|
-
|
|
11
|
+
which ccs && ccs --version && \
|
|
12
|
+
sed -i 's/fs\.symlinkSync(sourcePath, targetPath, symlinkType)/fs\.symlinkSync(require("path").relative(require("path").dirname(targetPath), sourcePath), targetPath, symlinkType)/g' /usr/local/lib/node_modules/@kaitranntt/ccs/dist/utils/claude-symlink-manager.js
|
|
13
|
+
RUN export HOME=/root && curl -fsSL https://claude.ai/install.sh | bash && \
|
|
13
14
|
mkdir -p /usr/local/share && \
|
|
14
|
-
mv /
|
|
15
|
+
mv /root/.local/share/claude /usr/local/share/claude && \
|
|
15
16
|
ln -sf /usr/local/share/claude/versions/$(ls /usr/local/share/claude/versions | head -1) /usr/local/bin/claude
|
|
16
17
|
USER agent
|
|
17
18
|
SNIPPET
|
|
@@ -43,12 +44,13 @@ RUN apt-get update && apt-get install -y --no-install-recommends tmux && rm -rf
|
|
|
43
44
|
RUN npm install -g @kaitranntt/ccs --ignore-scripts && \
|
|
44
45
|
mkdir -p /home/agent/.ccs && \
|
|
45
46
|
chown -R agent:agent /home/agent/.ccs && \
|
|
46
|
-
which ccs && ccs --version
|
|
47
|
+
which ccs && ccs --version && \
|
|
48
|
+
sed -i 's/fs\.symlinkSync(sourcePath, targetPath, symlinkType)/fs\.symlinkSync(require("path").relative(require("path").dirname(targetPath), sourcePath), targetPath, symlinkType)/g' /usr/local/lib/node_modules/@kaitranntt/ccs/dist/utils/claude-symlink-manager.js
|
|
47
49
|
|
|
48
50
|
# Install Claude Code using official native installer
|
|
49
|
-
RUN curl -fsSL https://claude.ai/install.sh | bash && \
|
|
51
|
+
RUN export HOME=/root && curl -fsSL https://claude.ai/install.sh | bash && \
|
|
50
52
|
mkdir -p /usr/local/share && \
|
|
51
|
-
mv /
|
|
53
|
+
mv /root/.local/share/claude /usr/local/share/claude && \
|
|
52
54
|
ln -sf /usr/local/share/claude/versions/$(ls /usr/local/share/claude/versions | head -1) /usr/local/bin/claude
|
|
53
55
|
|
|
54
56
|
USER agent
|
package/lib/install-droid.sh
CHANGED
|
@@ -4,10 +4,9 @@ set -e
|
|
|
4
4
|
dockerfile_snippet() {
|
|
5
5
|
cat <<'SNIPPET'
|
|
6
6
|
USER root
|
|
7
|
-
RUN mkdir -p /home/agent/.factory && \
|
|
8
|
-
bash -c "curl -fsSL https://app.factory.ai/cli | sh" && \
|
|
9
|
-
mv /
|
|
10
|
-
chown -R agent:agent /home/agent/.factory
|
|
7
|
+
RUN mkdir -p /home/agent/.factory && chown -R agent:agent /home/agent/.factory && \
|
|
8
|
+
export HOME=/root && bash -c "curl -fsSL https://app.factory.ai/cli | sh" && \
|
|
9
|
+
mv /root/.local/bin/droid /usr/local/bin/droid
|
|
11
10
|
USER agent
|
|
12
11
|
SNIPPET
|
|
13
12
|
}
|
|
@@ -27,10 +26,9 @@ mkdir -p "$HOME/.ai-sandbox/tools/droid/home"
|
|
|
27
26
|
cat <<'EOF' > "dockerfiles/droid/Dockerfile"
|
|
28
27
|
FROM ai-base:latest
|
|
29
28
|
USER root
|
|
30
|
-
RUN mkdir -p /home/agent/.factory && \
|
|
31
|
-
bash -c "curl -fsSL https://app.factory.ai/cli | sh" && \
|
|
32
|
-
mv /
|
|
33
|
-
chown -R agent:agent /home/agent/.factory
|
|
29
|
+
RUN mkdir -p /home/agent/.factory && chown -R agent:agent /home/agent/.factory && \
|
|
30
|
+
export HOME=/root && bash -c "curl -fsSL https://app.factory.ai/cli | sh" && \
|
|
31
|
+
mv /root/.local/bin/droid /usr/local/bin/droid
|
|
34
32
|
USER agent
|
|
35
33
|
ENTRYPOINT ["bash", "-c", "exec droid \"$@\"", "--"]
|
|
36
34
|
EOF
|
package/lib/install-shai.sh
CHANGED
|
@@ -4,8 +4,8 @@ set -e
|
|
|
4
4
|
dockerfile_snippet() {
|
|
5
5
|
cat <<'SNIPPET'
|
|
6
6
|
USER root
|
|
7
|
-
RUN curl -fsSL https://raw.githubusercontent.com/ovh/shai/main/install.sh | bash && \
|
|
8
|
-
mv /
|
|
7
|
+
RUN export HOME=/root && curl -fsSL https://raw.githubusercontent.com/ovh/shai/main/install.sh | bash && \
|
|
8
|
+
mv /root/.local/bin/shai /usr/local/bin/shai
|
|
9
9
|
USER agent
|
|
10
10
|
SNIPPET
|
|
11
11
|
}
|
|
@@ -29,8 +29,8 @@ FROM ai-base:latest
|
|
|
29
29
|
USER root
|
|
30
30
|
|
|
31
31
|
# Install SHAI native binary and relocate to /usr/local/bin
|
|
32
|
-
RUN curl -fsSL https://raw.githubusercontent.com/ovh/shai/main/install.sh | bash && \
|
|
33
|
-
mv /
|
|
32
|
+
RUN export HOME=/root && curl -fsSL https://raw.githubusercontent.com/ovh/shai/main/install.sh | bash && \
|
|
33
|
+
mv /root/.local/bin/shai /usr/local/bin/shai
|
|
34
34
|
|
|
35
35
|
USER agent
|
|
36
36
|
ENTRYPOINT ["shai"]
|
package/package.json
CHANGED