@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 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
- # Use previously saved key selection
1208
- echo "📋 Syncing Git credentials..."
1209
- if [ -d "$GIT_CACHE_DIR/ssh" ]; then
1210
- chmod -R 700 "$GIT_CACHE_DIR/ssh" 2>/dev/null || true
1211
- rm -rf "$GIT_CACHE_DIR/ssh" 2>/dev/null || true
1212
- fi
1213
- mkdir -p "$GIT_CACHE_DIR/ssh"
1214
-
1215
- # Read saved keys and copy them
1216
- SAVED_KEYS=()
1217
- while IFS= read -r key; do
1218
- [ -n "$key" ] && SAVED_KEYS+=("$key")
1219
- done < "$SAVED_KEYS_FILE"
1220
-
1221
- for key in "${SAVED_KEYS[@]}"; do
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
- done
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
- # Copy filtered SSH config (only hosts needed for this repo)
1237
- if [ -f "$HOME/.ssh/config" ]; then
1238
- SETUP_SSH="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/setup-ssh-config"
1239
- if [ -x "$SETUP_SSH" ]; then
1240
- # Join SAVED_KEYS into a comma-separated string for --keys
1241
- KEYS_ARG=$(IFS=,; echo "${SAVED_KEYS[*]}")
1242
- output=$( "$SETUP_SSH" --keys "$KEYS_ARG" 2>&1 ) || true
1243
- TEMP_CONFIG=$(echo "$output" | grep "Config:" | tail -1 | awk '{print $NF}')
1244
- if [ -f "$TEMP_CONFIG" ]; then
1245
- cp "$TEMP_CONFIG" "$GIT_CACHE_DIR/ssh/config" 2>/dev/null || true
1246
- chmod 600 "$GIT_CACHE_DIR/ssh/config" 2>/dev/null || true
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
- if [ -f "$HOME/.ssh/known_hosts" ]; then
1258
- cp "$HOME/.ssh/known_hosts" "$GIT_CACHE_DIR/ssh/known_hosts" 2>/dev/null || true
1259
- chmod 600 "$GIT_CACHE_DIR/ssh/known_hosts" 2>/dev/null || true
1260
- fi
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
- # Ensure all directories have correct permissions (recursive)
1263
- chmod 700 "$GIT_CACHE_DIR/ssh"
1264
- find "$GIT_CACHE_DIR/ssh" -type d -exec chmod 700 {} \;
1265
- find "$GIT_CACHE_DIR/ssh" -type f ! -name "config" ! -name "known_hosts" -exec chmod 600 {} \;
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
- GIT_MOUNTS="$GIT_MOUNTS -v $GIT_CACHE_DIR/ssh:/home/agent/.ssh:ro"
1268
- echo "✅ Git credentials synced"
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
- # Interactive SSH key selection
1300
- echo ""
1301
- echo "🔑 SSH Key Selection"
1302
- echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
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
- # Source the SSH key selector library
1305
- # Resolve symlink to get actual project directory
1306
- SCRIPT_PATH="${BASH_SOURCE[0]}"
1307
- while [ -L "$SCRIPT_PATH" ]; do
1308
- SCRIPT_DIR="$(cd "$(dirname "$SCRIPT_PATH")" && pwd)"
1309
- SCRIPT_PATH="$(readlink "$SCRIPT_PATH")"
1310
- [[ $SCRIPT_PATH != /* ]] && SCRIPT_PATH="$SCRIPT_DIR/$SCRIPT_PATH"
1311
- done
1312
- SCRIPT_DIR="$(cd "$(dirname "$SCRIPT_PATH")" && pwd)"
1313
- source "$SCRIPT_DIR/../lib/ssh-key-selector.sh"
1314
-
1315
- # Let user select keys
1316
- if select_ssh_keys; then
1317
- if [ ${#SELECTED_SSH_KEYS[@]} -gt 0 ]; then
1318
- echo "📋 Copying selected credentials to cache..."
1319
- if [ -d "$GIT_CACHE_DIR/ssh" ]; then
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
- mkdir -p "$GIT_CACHE_DIR/ssh"
1324
-
1325
- # Copy selected SSH keys (preserve directory structure exactly)
1326
- for key in "${SELECTED_SSH_KEYS[@]}"; do
1327
- echo " Copying $key..."
1328
- src_file="$HOME/.ssh/$key"
1329
- dst_file="$GIT_CACHE_DIR/ssh/$key"
1330
-
1331
- # Create parent directory with correct permissions
1332
- dst_dir=$(dirname "$dst_file")
1333
- mkdir -p "$dst_dir"
1334
- chmod 700 "$dst_dir"
1335
-
1336
- # Copy the file and set permissions
1337
- if [ -f "$src_file" ]; then
1338
- cp "$src_file" "$dst_file"
1339
- chmod 600 "$dst_file"
1340
- else
1341
- echo " ⚠️ Warning: $src_file not found, skipping"
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
- done
1344
-
1345
- # Copy filtered SSH config (only hosts needed for this repo)
1346
- if [ -f "$HOME/.ssh/config" ]; then
1347
- echo " → Generating filtered SSH config..."
1348
- # Run setup-ssh-config to get filtered config
1349
- SETUP_SSH="$SCRIPT_DIR/setup-ssh-config"
1350
- if [ -x "$SETUP_SSH" ]; then
1351
- # Join SELECTED_SSH_KEYS into a comma-separated string for --keys
1352
- KEYS_ARG=$(IFS=,; echo "${SELECTED_SSH_KEYS[*]}")
1353
- # Run it and capture the filtered config path
1354
- output=$( "$SETUP_SSH" --keys "$KEYS_ARG" 2>&1 ) || true
1355
- TEMP_CONFIG=$(echo "$output" | grep "Config:" | tail -1 | awk '{print $NF}')
1356
- if [ -f "$TEMP_CONFIG" ]; then
1357
- cp "$TEMP_CONFIG" "$GIT_CACHE_DIR/ssh/config" 2>/dev/null || true
1358
- chmod 600 "$GIT_CACHE_DIR/ssh/config" 2>/dev/null || true
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 to copying full config if setup-ssh-config fails
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
- if [ -f "$HOME/.ssh/known_hosts" ]; then
1372
- echo " → Copying known_hosts..."
1373
- cp "$HOME/.ssh/known_hosts" "$GIT_CACHE_DIR/ssh/known_hosts" 2>/dev/null || true
1374
- chmod 600 "$GIT_CACHE_DIR/ssh/known_hosts" 2>/dev/null || true
1375
- fi
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
- # Ensure all directories and files have correct permissions (recursive)
1378
- chmod 700 "$GIT_CACHE_DIR/ssh"
1379
- find "$GIT_CACHE_DIR/ssh" -type d -exec chmod 700 {} \;
1380
- find "$GIT_CACHE_DIR/ssh" -type f ! -name "config" ! -name "known_hosts" -exec chmod 600 {} \;
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
- GIT_MOUNTS="$GIT_MOUNTS -v $GIT_CACHE_DIR/ssh:/home/agent/.ssh:ro"
1512
+ GIT_MOUNTS="$GIT_MOUNTS -v $GIT_CACHE_DIR/ssh:/home/agent/.ssh:ro"
1383
1513
 
1384
- # Copy gitconfig
1385
- if [ -f "$HOME/.gitconfig" ]; then
1386
- echo " → Copying .gitconfig..."
1387
- cp "$HOME/.gitconfig" "$HOME_DIR/.gitconfig" 2>/dev/null || true
1388
- fi
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
- if [[ "$git_choice" == "4" || "$git_choice" == "5" ]]; then
1391
- GIT_FETCH_ONLY_MODE=true
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
- # Save selected keys (one per line for easier parsing)
1403
- WORKSPACE_MD5=$(echo "$CURRENT_DIR" | md5sum | cut -c1-8)
1404
- mkdir -p "$GIT_SHARED_DIR/keys"
1405
- printf "%s\n" "${SELECTED_SSH_KEYS[@]}" > "$GIT_SHARED_DIR/keys/$WORKSPACE_MD5"
1406
- echo "✅ Git access enabled and saved for: $CURRENT_DIR"
1407
- elif [ "$git_choice" = "5" ]; then
1408
- # Save workspace to fetchOnlyWorkspaces
1409
- if has_jq && [[ -f "$AI_SANDBOX_CONFIG" ]]; then
1410
- jq --arg ws "$CURRENT_DIR" '.git.fetchOnlyWorkspaces = ((.git.fetchOnlyWorkspaces // []) + [$ws] | unique)' "$AI_SANDBOX_CONFIG" > "$AI_SANDBOX_CONFIG.tmp" \
1411
- && mv "$AI_SANDBOX_CONFIG.tmp" "$AI_SANDBOX_CONFIG"
1412
- chmod 600 "$AI_SANDBOX_CONFIG"
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 " Git access enabled for this session"
1555
+ echo "⚠️ No SSH keys selected. Git access disabled."
1423
1556
  fi
1424
1557
  else
1425
- echo "⚠️ No SSH keys selected. Git access disabled."
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" '["npx", "@playwright/mcp@latest", "--headless", "--browser", "chromium"]'; then
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" '["npx", "@playwright/mcp@latest", "--headless", "--browser", "chromium"]'; then
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
@@ -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
@@ -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
- RUN curl -fsSL https://claude.ai/install.sh | bash && \
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 /home/agent/.local/share/claude /usr/local/share/claude && \
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 /home/agent/.local/share/claude /usr/local/share/claude && \
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
@@ -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 /home/agent/.local/bin/droid /usr/local/bin/droid && \
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 /home/agent/.local/bin/droid /usr/local/bin/droid && \
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
@@ -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 /home/agent/.local/bin/shai /usr/local/bin/shai
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 /home/agent/.local/bin/shai /usr/local/bin/shai
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kokorolx/ai-sandbox-wrapper",
3
- "version": "3.0.0",
3
+ "version": "3.0.2",
4
4
  "description": "Docker-based security sandbox for AI coding agents. Isolate Claude, Gemini, Aider, and other AI tools from your host system.",
5
5
  "keywords": [
6
6
  "ai",