@kokorolx/ai-sandbox-wrapper 2.7.0 → 3.0.1

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/bin/ai-run CHANGED
@@ -4,9 +4,10 @@ set -e
4
4
  # Show help if requested
5
5
  show_help() {
6
6
  cat << 'EOF'
7
- Usage: ai-run <tool> [options] [-- tool-args...]
7
+ Usage: ai-run [tool] [options] [-- tool-args...]
8
8
 
9
9
  Run AI tools in a secure Docker sandbox.
10
+ When no tool is specified, opens an interactive shell with all installed tools.
10
11
 
11
12
  Options:
12
13
  -s, --shell Start interactive shell instead of running tool directly
@@ -16,6 +17,8 @@ Options:
16
17
  --password-env <VAR> Read OpenCode server password from environment variable
17
18
  --allow-unsecured Allow OpenCode server to run without password (suppresses warning)
18
19
  --git-fetch Enable git fetch-only mode (blocks push)
20
+ --no-nano-brain-auto-repair
21
+ Disable nano-brain preflight/auto-repair logic
19
22
  -h, --help Show this help message
20
23
  --help-env Show environment variables reference
21
24
 
@@ -35,12 +38,13 @@ OpenCode Server Authentication (web/serve mode only):
35
38
  Precedence: --password > --password-env > OPENCODE_SERVER_PASSWORD env > interactive prompt
36
39
 
37
40
  Examples:
41
+ ai-run # Open interactive shell with all tools
38
42
  ai-run claude # Run Claude in sandbox
39
43
  ai-run opencode web -e 4096 # Run OpenCode web with port exposed
40
44
  ai-run opencode web -p secret # Run with password
41
45
  ai-run opencode --shell # Start shell, run tool manually
42
46
  ai-run aider -n mynetwork # Connect to Docker network
43
- ai-run opencode --git-fetch # Git fetch only (no push)
47
+ ai-run opencode --git-fetch # Git fetch only (no push)
44
48
 
45
49
  Documentation: https://github.com/kokorolx/ai-sandbox-wrapper
46
50
  EOF
@@ -63,6 +67,10 @@ Runtime Environment Variables (inline with ai-run):
63
67
  AI_RUN_PLATFORM=linux/amd64 Force specific platform (useful for cross-arch)
64
68
  Example: AI_RUN_PLATFORM=linux/amd64 ai-run opencode
65
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
+
66
74
  PORT_BIND=all Bind exposed ports to all interfaces (0.0.0.0) instead of localhost
67
75
  ⚠️ Security risk - use only when needed
68
76
  Example: PORT_BIND=all ai-run opencode web -e 4096
@@ -126,11 +134,26 @@ if [[ "${1:-}" == "--help-env" ]]; then
126
134
  show_help_env
127
135
  fi
128
136
 
129
- TOOL="$1"
130
- shift 2>/dev/null || { echo " ERROR: No tool specified. Use 'ai-run --help' for usage."; exit 1; }
137
+ TOOL="${1:-}"
138
+ if [[ -n "$TOOL" && ! "$TOOL" =~ ^- ]]; then
139
+ shift
140
+ else
141
+ TOOL=""
142
+ fi
143
+
144
+ # Handle no tool specified
145
+ if [[ -z "$TOOL" ]]; then
146
+ if [[ ! -t 0 ]] || [[ ! -t 1 ]]; then
147
+ echo "❌ ERROR: No tool specified and no TTY available."
148
+ echo " Use: ai-run <tool> [args...]"
149
+ exit 1
150
+ fi
151
+ SHELL_MODE=true
152
+ fi
131
153
 
132
154
  # Parse flags
133
- SHELL_MODE=false
155
+ # Note: SHELL_MODE may already be true if no tool was specified
156
+ SHELL_MODE="${SHELL_MODE:-false}"
134
157
  NETWORK_FLAG=false
135
158
  NETWORK_ARG=""
136
159
  EXPOSE_ARG=""
@@ -138,8 +161,13 @@ SERVER_PASSWORD=""
138
161
  PASSWORD_ENV_VAR=""
139
162
  ALLOW_UNSECURED=false
140
163
  GIT_FETCH_ONLY_FLAG=false
164
+ NANO_BRAIN_AUTO_REPAIR=true
141
165
  TOOL_ARGS=()
142
166
 
167
+ if [[ "${AI_RUN_DISABLE_NANO_BRAIN_AUTO_REPAIR:-}" == "1" ]]; then
168
+ NANO_BRAIN_AUTO_REPAIR=false
169
+ fi
170
+
143
171
  while [[ $# -gt 0 ]]; do
144
172
  case "$1" in
145
173
  --shell|-s)
@@ -184,6 +212,10 @@ while [[ $# -gt 0 ]]; do
184
212
  GIT_FETCH_ONLY_FLAG=true
185
213
  shift
186
214
  ;;
215
+ --no-nano-brain-auto-repair)
216
+ NANO_BRAIN_AUTO_REPAIR=false
217
+ shift
218
+ ;;
187
219
  *)
188
220
  TOOL_ARGS+=("$1")
189
221
  shift
@@ -462,7 +494,7 @@ while IFS= read -r ws; do
462
494
  done < <(read_workspaces)
463
495
 
464
496
  if [[ "$ALLOWED" != "true" ]]; then
465
- echo "⚠️ SECURITY WARNING: You are running $TOOL outside a whitelisted workspace."
497
+ echo "⚠️ SECURITY WARNING: You are running ${TOOL:-shell} outside a whitelisted workspace."
466
498
  echo " Current path: $CURRENT_DIR"
467
499
  echo ""
468
500
  echo "Allowing this path gives the AI container access to this folder."
@@ -504,7 +536,7 @@ if [[ "$(uname)" == "Darwin" ]] && [[ -t 0 ]]; then
504
536
 
505
537
  # 2. Check if valid directory
506
538
  if [[ -d "$SCREENSHOT_DIR" ]]; then
507
-
539
+
508
540
  # 3. Check if ALREADY whitelisted (exact match or parent)
509
541
  IS_WHITELISTED=false
510
542
  while IFS= read -r ws; do
@@ -527,7 +559,7 @@ if [[ "$(uname)" == "Darwin" ]] && [[ -t 0 ]]; then
527
559
  echo ""
528
560
  # Use /dev/tty to ensure we read from user even if stdin is redirected
529
561
  read -p "Whitelist screenshots folder? [y/N]: " CONFIRM_SS < /dev/tty
530
-
562
+
531
563
  if [[ "$CONFIRM_SS" =~ ^[Yy]$ ]]; then
532
564
  add_workspace "$SCREENSHOT_DIR"
533
565
  echo "✅ Added to whitelist."
@@ -540,17 +572,86 @@ if [[ "$(uname)" == "Darwin" ]] && [[ -t 0 ]]; then
540
572
  fi
541
573
 
542
574
  if [[ "$AI_IMAGE_SOURCE" == "registry" ]]; then
543
- IMAGE="registry.gitlab.com/kokorolee/ai-sandbox-wrapper/ai-${TOOL}:latest"
575
+ IMAGE="registry.gitlab.com/kokorolee/ai-sandbox-wrapper/ai-sandbox:latest"
544
576
  else
545
- IMAGE="ai-${TOOL}:latest"
577
+ IMAGE="ai-sandbox:latest"
578
+ fi
579
+
580
+ # Tool validation (warn if tool not in config.json)
581
+ if [[ -n "$TOOL" ]]; then
582
+ if command -v jq &>/dev/null && [[ -f "$AI_SANDBOX_CONFIG" ]]; then
583
+ INSTALLED_TOOLS=$(jq -r '.tools.installed // [] | .[]' "$AI_SANDBOX_CONFIG" 2>/dev/null)
584
+ if [[ -n "$INSTALLED_TOOLS" ]] && ! echo "$INSTALLED_TOOLS" | grep -qx "$TOOL"; then
585
+ echo "⚠️ WARNING: Tool '$TOOL' may not be installed in the sandbox image."
586
+ echo " Installed tools: $(echo "$INSTALLED_TOOLS" | tr '\n' ', ' | sed 's/,$//')"
587
+ echo " Run setup.sh to add tools."
588
+ echo ""
546
589
  fi
590
+ fi
591
+ fi
547
592
 
548
- # V2 tool-centric paths
549
- HOME_DIR="$SANDBOX_DIR/tools/$TOOL/home"
593
+ # Shared home directory for all tools (unified image)
594
+ HOME_DIR="$SANDBOX_DIR/home"
550
595
  GIT_SHARED_DIR="$SANDBOX_DIR/shared/git"
596
+ CACHE_DIR="$SANDBOX_DIR/cache"
551
597
 
552
598
  mkdir -p "$HOME_DIR"
553
599
  mkdir -p "$GIT_SHARED_DIR"
600
+ mkdir -p "$CACHE_DIR/npm" "$CACHE_DIR/bun" "$CACHE_DIR/pip" "$CACHE_DIR/playwright-browsers"
601
+
602
+ # ============================================================================
603
+ # MIGRATION V3: Merge per-tool home directories into unified home
604
+ # ============================================================================
605
+ migrate_to_unified_home() {
606
+ local marker_file="$SANDBOX_DIR/.migrated-unified-home"
607
+
608
+ # Skip if already migrated
609
+ [[ -f "$marker_file" ]] && return 0
610
+
611
+ # Check if any per-tool home directories exist
612
+ local needs_migration=false
613
+ for tool_home in "$SANDBOX_DIR"/tools/*/home; do
614
+ if [[ -d "$tool_home" ]]; then
615
+ needs_migration=true
616
+ break
617
+ fi
618
+ done
619
+
620
+ [[ "$needs_migration" == "false" ]] && { touch "$marker_file"; return 0; }
621
+
622
+ echo "🔄 Migrating per-tool home directories to unified home..."
623
+
624
+ local migrated_count=0
625
+ for tool_home in "$SANDBOX_DIR"/tools/*/home; do
626
+ [[ -d "$tool_home" ]] || continue
627
+ local tool_name
628
+ tool_name=$(basename "$(dirname "$tool_home")")
629
+ echo " 🔄 Migrating tools/$tool_name/home/ → home/"
630
+
631
+ # Use cp -rn (no-clobber) - works on macOS and Linux
632
+ # Falls back to rsync if cp -rn fails (some older systems)
633
+ # KNOWN ISSUE: glob * doesn't match dotfiles (.nano-brain, .config, etc.)
634
+ # TODO: fix with dotglob or rsync-first approach
635
+ if cp -rn "$tool_home/"* "$HOME_DIR/" 2>/dev/null; then
636
+ ((migrated_count++))
637
+ elif command -v rsync &>/dev/null; then
638
+ rsync -a --ignore-existing "$tool_home/" "$HOME_DIR/" 2>/dev/null && ((migrated_count++))
639
+ else
640
+ echo " ⚠️ Could not migrate $tool_name (cp -rn and rsync unavailable)"
641
+ fi
642
+ done
643
+
644
+ # Create marker file
645
+ date -Iseconds > "$marker_file" 2>/dev/null || date > "$marker_file"
646
+
647
+ if [[ $migrated_count -gt 0 ]]; then
648
+ echo "✅ Migration complete! Merged $migrated_count tool home directories."
649
+ echo ""
650
+ echo "ℹ️ Old per-tool home directories preserved. Run 'npx @kokorolx/ai-sandbox-wrapper clean' to remove them."
651
+ fi
652
+ }
653
+
654
+ migrate_to_unified_home
554
655
 
555
656
  # Build volume mounts for all whitelisted workspaces
556
657
  VOLUME_MOUNTS=""
@@ -558,13 +659,47 @@ while IFS= read -r ws; do
558
659
  VOLUME_MOUNTS="$VOLUME_MOUNTS -v $ws:$ws:delegated"
559
660
  done < "$WORKSPACES_FILE"
560
661
 
561
- # Tool-specific config migration and setup
562
- # All tool configs now live in HOME_DIR (~/.ai-sandbox/home/$TOOL/)
563
- # which is mounted as /home/agent in the container
662
+ # Config mount registry for all tools
663
+ # Maps tool name to space-separated config paths (relative to $HOME)
664
+ get_tool_configs() {
665
+ case "$1" in
666
+ amp) echo ".config/amp .local/share/amp" ;;
667
+ opencode) echo ".config/opencode .local/share/opencode" ;;
668
+ claude) echo ".claude .ccs" ;;
669
+ openclaw) echo ".openclaw" ;;
670
+ droid) echo ".config/droid" ;;
671
+ qoder) echo ".config/qoder" ;;
672
+ auggie) echo ".config/auggie" ;;
673
+ codebuddy) echo ".config/codebuddy" ;;
674
+ jules) echo ".config/jules" ;;
675
+ shai) echo ".config/shai" ;;
676
+ gemini) echo ".config/gemini" ;;
677
+ aider) echo ".config/aider .aider" ;;
678
+ kilo) echo ".config/kilo" ;;
679
+ codex) echo ".config/codex" ;;
680
+ qwen) echo ".config/qwen" ;;
681
+ esac
682
+ }
683
+
684
+ # All known tools (for fallback)
685
+ ALL_KNOWN_TOOLS="amp opencode claude openclaw droid qoder auggie codebuddy jules shai gemini aider kilo codex qwen"
686
+
687
+ # Get list of installed tools from config.json, fallback to all known tools
688
+ get_installed_tools() {
689
+ local installed
690
+ if command -v jq &>/dev/null && [[ -f "$AI_SANDBOX_CONFIG" ]]; then
691
+ installed=$(jq -r '.tools.installed[]? // empty' "$AI_SANDBOX_CONFIG" 2>/dev/null)
692
+ fi
693
+ if [[ -z "$installed" ]]; then
694
+ # Fallback: return all known tools in registry
695
+ echo "$ALL_KNOWN_TOOLS"
696
+ else
697
+ echo "$installed"
698
+ fi
699
+ }
564
700
 
565
- # Tool-specific config persistence
566
- # Instead of copying (one-way), we now bind-mount host paths directly
567
- # to ensure changes inside the container persist to the host.
701
+ # Tool config persistence via bind mounts
702
+ # Bind-mount host paths directly to ensure changes persist to the host.
568
703
  TOOL_CONFIG_MOUNTS=""
569
704
 
570
705
  mount_tool_config() {
@@ -582,81 +717,60 @@ mount_tool_config() {
582
717
  fi
583
718
  }
584
719
 
585
- case "$TOOL" in
586
- amp)
587
- mount_tool_config "$HOME/.config/amp" ".config/amp"
588
- mount_tool_config "$HOME/.local/share/amp" ".local/share/amp"
589
- ;;
590
- opencode)
591
- mount_tool_config "$HOME/.config/opencode" ".config/opencode"
592
- mount_tool_config "$HOME/.local/share/opencode" ".local/share/opencode"
593
- # Bundle default skills (copy if not already present)
594
- AIRUN_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
595
- BUNDLED_SKILLS_DIR="$AIRUN_DIR/../skills"
596
- if [[ -d "$BUNDLED_SKILLS_DIR" ]]; then
597
- for skill_dir in "$BUNDLED_SKILLS_DIR"/*/; do
598
- [[ ! -d "$skill_dir" ]] && continue
599
- skill_name=$(basename "$skill_dir")
600
- target_dir="$HOME/.config/opencode/skills/$skill_name"
601
- if [[ ! -d "$target_dir" ]]; then
602
- mkdir -p "$target_dir"
603
- cp -r "$skill_dir"* "$target_dir/" 2>/dev/null || true
604
- fi
605
- done
606
- fi
607
- ;;
608
- openclaw)
609
- mount_tool_config "$HOME/.openclaw" ".openclaw"
610
- ;;
611
- claude)
612
- mount_tool_config "$HOME/.claude" ".claude"
613
- mount_tool_config "$HOME/.ccs" ".ccs"
614
- ;;
615
- droid)
616
- mount_tool_config "$HOME/.config/droid" ".config/droid"
617
- ;;
618
- qoder)
619
- mount_tool_config "$HOME/.config/qoder" ".config/qoder"
620
- ;;
621
- auggie)
622
- mount_tool_config "$HOME/.config/auggie" ".config/auggie"
623
- ;;
624
- codebuddy)
625
- mount_tool_config "$HOME/.config/codebuddy" ".config/codebuddy"
626
- ;;
627
- jules)
628
- mount_tool_config "$HOME/.config/jules" ".config/jules"
629
- ;;
630
- shai)
631
- mount_tool_config "$HOME/.config/shai" ".config/shai"
632
- ;;
633
- gemini)
634
- mount_tool_config "$HOME/.config/gemini" ".config/gemini"
635
- ;;
636
- aider)
637
- mount_tool_config "$HOME/.config/aider" ".config/aider"
638
- mount_tool_config "$HOME/.aider" ".aider"
639
- ;;
640
- kilo)
641
- mount_tool_config "$HOME/.config/kilo" ".config/kilo"
642
- ;;
643
- codex)
644
- mount_tool_config "$HOME/.config/codex" ".config/codex"
645
- ;;
646
- qwen)
647
- mount_tool_config "$HOME/.config/qwen" ".config/qwen"
648
- ;;
649
- esac
720
+ # Mount configs for all installed tools (unified image)
721
+ for tool in $(get_installed_tools); do
722
+ for cfg_path in $(get_tool_configs "$tool"); do
723
+ mount_tool_config "$HOME/$cfg_path" "$cfg_path"
724
+ done
725
+ done
726
+
727
+ # Bundle OpenCode default skills (if opencode is installed)
728
+ if get_installed_tools | grep -qw "opencode"; then
729
+ AIRUN_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
730
+ BUNDLED_SKILLS_DIR="$AIRUN_DIR/../skills"
731
+ if [[ -d "$BUNDLED_SKILLS_DIR" ]]; then
732
+ for skill_dir in "$BUNDLED_SKILLS_DIR"/*/; do
733
+ [[ ! -d "$skill_dir" ]] && continue
734
+ skill_name=$(basename "$skill_dir")
735
+ target_dir="$HOME/.config/opencode/skills/$skill_name"
736
+ if [[ ! -d "$target_dir" ]]; then
737
+ mkdir -p "$target_dir"
738
+ cp -r "$skill_dir"* "$target_dir/" 2>/dev/null || true
739
+ fi
740
+ done
741
+ fi
742
+ fi
650
743
 
651
744
  # Ensure required directories exist in HOME_DIR
652
745
  mkdir -p "$HOME_DIR/.config" "$HOME_DIR/.local/share" "$HOME_DIR/.cache" "$HOME_DIR/.bun"
653
746
 
654
- # Project-level config mount (if exists)
655
- CONFIG_MOUNT=""
656
- PROJECT_CONFIG="$CURRENT_DIR/.$TOOL.json"
747
+ SHARED_CACHE_MOUNTS="-v $CACHE_DIR/npm:/home/agent/.npm:delegated"
748
+ SHARED_CACHE_MOUNTS="$SHARED_CACHE_MOUNTS -v $CACHE_DIR/bun:/home/agent/.bun/install/cache:delegated"
749
+ SHARED_CACHE_MOUNTS="$SHARED_CACHE_MOUNTS -v $CACHE_DIR/pip:/home/agent/.cache/pip:delegated"
750
+ SHARED_CACHE_MOUNTS="$SHARED_CACHE_MOUNTS -v $CACHE_DIR/playwright-browsers:/opt/playwright-browsers:delegated"
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
657
758
 
658
- if [[ -f "$PROJECT_CONFIG" ]]; then
659
- CONFIG_MOUNT="-v $PROJECT_CONFIG:$CURRENT_DIR/.$TOOL.json:delegated"
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
+
767
+ # Project-level config mount (if exists and tool specified)
768
+ CONFIG_MOUNT=""
769
+ if [[ -n "$TOOL" ]]; then
770
+ PROJECT_CONFIG="$CURRENT_DIR/.$TOOL.json"
771
+ if [[ -f "$PROJECT_CONFIG" ]]; then
772
+ CONFIG_MOUNT="-v $PROJECT_CONFIG:$CURRENT_DIR/.$TOOL.json:delegated"
773
+ fi
660
774
  fi
661
775
 
662
776
  # ============================================================================
@@ -1053,8 +1167,55 @@ if [[ ${#SELECTED_NETWORKS[@]} -gt 0 ]]; then
1053
1167
  done < <(validate_networks "${SELECTED_NETWORKS[@]}")
1054
1168
  fi
1055
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
+
1056
1216
  # Git access control (opt-in per workspace)
1057
1217
  GIT_MOUNTS=""
1218
+ SSH_AGENT_ENV=""
1058
1219
  GIT_ALLOWED_FILE="$SANDBOX_DIR/git-allowed"
1059
1220
  GIT_CACHE_DIR="$GIT_SHARED_DIR" # V2: Uses shared/git instead of cache/git
1060
1221
  touch "$GIT_ALLOWED_FILE" 2>/dev/null || true
@@ -1120,68 +1281,79 @@ if is_git_allowed "$CURRENT_DIR" || is_git_fetch_only "$CURRENT_DIR" || [[ "$GIT
1120
1281
  SAVED_KEYS_FILE="$GIT_SHARED_DIR/keys/$WORKSPACE_MD5" # V2: Uses shared/git/keys/
1121
1282
 
1122
1283
  if [ -f "$SAVED_KEYS_FILE" ]; then
1123
- # Use previously saved key selection
1124
- echo "📋 Syncing Git credentials..."
1125
- if [ -d "$GIT_CACHE_DIR/ssh" ]; then
1126
- chmod -R 700 "$GIT_CACHE_DIR/ssh" 2>/dev/null || true
1127
- rm -rf "$GIT_CACHE_DIR/ssh" 2>/dev/null || true
1128
- fi
1129
- mkdir -p "$GIT_CACHE_DIR/ssh"
1130
-
1131
- # Read saved keys and copy them
1132
- SAVED_KEYS=()
1133
- while IFS= read -r key; do
1134
- [ -n "$key" ] && SAVED_KEYS+=("$key")
1135
- done < "$SAVED_KEYS_FILE"
1136
-
1137
- for key in "${SAVED_KEYS[@]}"; do
1138
- [ -z "$key" ] && continue
1139
- src_file="$HOME/.ssh/$key"
1140
- dst_file="$GIT_CACHE_DIR/ssh/$key"
1141
-
1142
- dst_dir=$(dirname "$dst_file")
1143
- mkdir -p "$dst_dir" 2>/dev/null || true
1144
- chmod 700 "$dst_dir" 2>/dev/null || true
1145
-
1146
- if [ -f "$src_file" ]; then
1147
- cp "$src_file" "$dst_file" 2>/dev/null || true
1148
- 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
1149
1299
  fi
1150
- 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
1151
1322
 
1152
- # Copy filtered SSH config (only hosts needed for this repo)
1153
- if [ -f "$HOME/.ssh/config" ]; then
1154
- SETUP_SSH="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/setup-ssh-config"
1155
- if [ -x "$SETUP_SSH" ]; then
1156
- # Join SAVED_KEYS into a comma-separated string for --keys
1157
- KEYS_ARG=$(IFS=,; echo "${SAVED_KEYS[*]}")
1158
- output=$( "$SETUP_SSH" --keys "$KEYS_ARG" 2>&1 ) || true
1159
- TEMP_CONFIG=$(echo "$output" | grep "Config:" | tail -1 | awk '{print $NF}')
1160
- if [ -f "$TEMP_CONFIG" ]; then
1161
- cp "$TEMP_CONFIG" "$GIT_CACHE_DIR/ssh/config" 2>/dev/null || true
1162
- 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
1163
1338
  else
1164
1339
  cp "$HOME/.ssh/config" "$GIT_CACHE_DIR/ssh/config" 2>/dev/null || true
1165
1340
  chmod 600 "$GIT_CACHE_DIR/ssh/config" 2>/dev/null || true
1166
1341
  fi
1167
- else
1168
- cp "$HOME/.ssh/config" "$GIT_CACHE_DIR/ssh/config" 2>/dev/null || true
1169
- chmod 600 "$GIT_CACHE_DIR/ssh/config" 2>/dev/null || true
1170
1342
  fi
1171
- fi
1172
1343
 
1173
- if [ -f "$HOME/.ssh/known_hosts" ]; then
1174
- cp "$HOME/.ssh/known_hosts" "$GIT_CACHE_DIR/ssh/known_hosts" 2>/dev/null || true
1175
- chmod 600 "$GIT_CACHE_DIR/ssh/known_hosts" 2>/dev/null || true
1176
- 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
1177
1348
 
1178
- # Ensure all directories have correct permissions (recursive)
1179
- chmod 700 "$GIT_CACHE_DIR/ssh"
1180
- find "$GIT_CACHE_DIR/ssh" -type d -exec chmod 700 {} \;
1181
- 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 {} \;
1182
1353
 
1183
- GIT_MOUNTS="$GIT_MOUNTS -v $GIT_CACHE_DIR/ssh:/home/agent/.ssh:ro"
1184
- 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
1185
1357
  fi
1186
1358
 
1187
1359
  if [ -f "$HOME/.gitconfig" ]; then
@@ -1212,136 +1384,179 @@ else
1212
1384
 
1213
1385
  case "$git_choice" in
1214
1386
  1|2|4|5)
1215
- # Interactive SSH key selection
1216
- echo ""
1217
- echo "🔑 SSH Key Selection"
1218
- 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"
1219
1394
 
1220
- # Source the SSH key selector library
1221
- # Resolve symlink to get actual project directory
1222
- SCRIPT_PATH="${BASH_SOURCE[0]}"
1223
- while [ -L "$SCRIPT_PATH" ]; do
1224
- SCRIPT_DIR="$(cd "$(dirname "$SCRIPT_PATH")" && pwd)"
1225
- SCRIPT_PATH="$(readlink "$SCRIPT_PATH")"
1226
- [[ $SCRIPT_PATH != /* ]] && SCRIPT_PATH="$SCRIPT_DIR/$SCRIPT_PATH"
1227
- done
1228
- SCRIPT_DIR="$(cd "$(dirname "$SCRIPT_PATH")" && pwd)"
1229
- source "$SCRIPT_DIR/../lib/ssh-key-selector.sh"
1230
-
1231
- # Let user select keys
1232
- if select_ssh_keys; then
1233
- if [ ${#SELECTED_SSH_KEYS[@]} -gt 0 ]; then
1234
- echo "📋 Copying selected credentials to cache..."
1235
- if [ -d "$GIT_CACHE_DIR/ssh" ]; then
1236
- chmod -R 700 "$GIT_CACHE_DIR/ssh" 2>/dev/null || true
1237
- 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"
1238
1411
  fi
1239
- mkdir -p "$GIT_CACHE_DIR/ssh"
1240
-
1241
- # Copy selected SSH keys (preserve directory structure exactly)
1242
- for key in "${SELECTED_SSH_KEYS[@]}"; do
1243
- echo " Copying $key..."
1244
- src_file="$HOME/.ssh/$key"
1245
- dst_file="$GIT_CACHE_DIR/ssh/$key"
1246
-
1247
- # Create parent directory with correct permissions
1248
- dst_dir=$(dirname "$dst_file")
1249
- mkdir -p "$dst_dir"
1250
- chmod 700 "$dst_dir"
1251
-
1252
- # Copy the file and set permissions
1253
- if [ -f "$src_file" ]; then
1254
- cp "$src_file" "$dst_file"
1255
- chmod 600 "$dst_file"
1256
- else
1257
- 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
1258
1452
  fi
1259
- done
1260
-
1261
- # Copy filtered SSH config (only hosts needed for this repo)
1262
- if [ -f "$HOME/.ssh/config" ]; then
1263
- echo " → Generating filtered SSH config..."
1264
- # Run setup-ssh-config to get filtered config
1265
- SETUP_SSH="$SCRIPT_DIR/setup-ssh-config"
1266
- if [ -x "$SETUP_SSH" ]; then
1267
- # Join SELECTED_SSH_KEYS into a comma-separated string for --keys
1268
- KEYS_ARG=$(IFS=,; echo "${SELECTED_SSH_KEYS[*]}")
1269
- # Run it and capture the filtered config path
1270
- output=$( "$SETUP_SSH" --keys "$KEYS_ARG" 2>&1 ) || true
1271
- TEMP_CONFIG=$(echo "$output" | grep "Config:" | tail -1 | awk '{print $NF}')
1272
- if [ -f "$TEMP_CONFIG" ]; then
1273
- cp "$TEMP_CONFIG" "$GIT_CACHE_DIR/ssh/config" 2>/dev/null || true
1274
- 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"
1275
1470
  else
1276
- # Fallback to copying full config if setup-ssh-config fails
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
1494
+ else
1495
+ # Fallback: copy full config
1277
1496
  cp "$HOME/.ssh/config" "$GIT_CACHE_DIR/ssh/config" 2>/dev/null || true
1278
1497
  chmod 600 "$GIT_CACHE_DIR/ssh/config" 2>/dev/null || true
1279
1498
  fi
1280
- else
1281
- # Fallback: copy full config
1282
- cp "$HOME/.ssh/config" "$GIT_CACHE_DIR/ssh/config" 2>/dev/null || true
1283
- chmod 600 "$GIT_CACHE_DIR/ssh/config" 2>/dev/null || true
1284
1499
  fi
1285
- fi
1286
1500
 
1287
- if [ -f "$HOME/.ssh/known_hosts" ]; then
1288
- echo " → Copying known_hosts..."
1289
- cp "$HOME/.ssh/known_hosts" "$GIT_CACHE_DIR/ssh/known_hosts" 2>/dev/null || true
1290
- chmod 600 "$GIT_CACHE_DIR/ssh/known_hosts" 2>/dev/null || true
1291
- 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
1292
1506
 
1293
- # Ensure all directories and files have correct permissions (recursive)
1294
- chmod 700 "$GIT_CACHE_DIR/ssh"
1295
- find "$GIT_CACHE_DIR/ssh" -type d -exec chmod 700 {} \;
1296
- 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 {} \;
1297
1511
 
1298
- 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"
1299
1513
 
1300
- # Copy gitconfig
1301
- if [ -f "$HOME/.gitconfig" ]; then
1302
- echo " → Copying .gitconfig..."
1303
- cp "$HOME/.gitconfig" "$HOME_DIR/.gitconfig" 2>/dev/null || true
1304
- 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
1305
1519
 
1306
- if [[ "$git_choice" == "4" || "$git_choice" == "5" ]]; then
1307
- GIT_FETCH_ONLY_MODE=true
1308
- fi
1309
- if [ "$git_choice" = "2" ]; then
1310
- # Save workspace and selected keys for future sessions
1311
- echo "$CURRENT_DIR" >> "$GIT_ALLOWED_FILE"
1312
- # Also save to config.json
1313
- if has_jq && [[ -f "$AI_SANDBOX_CONFIG" ]]; then
1314
- jq --arg ws "$CURRENT_DIR" '.git.allowedWorkspaces += [$ws] | .git.allowedWorkspaces |= unique' "$AI_SANDBOX_CONFIG" > "$AI_SANDBOX_CONFIG.tmp" \
1315
- && mv "$AI_SANDBOX_CONFIG.tmp" "$AI_SANDBOX_CONFIG"
1316
- chmod 600 "$AI_SANDBOX_CONFIG"
1520
+ if [[ "$git_choice" == "4" || "$git_choice" == "5" ]]; then
1521
+ GIT_FETCH_ONLY_MODE=true
1317
1522
  fi
1318
- # Save selected keys (one per line for easier parsing)
1319
- WORKSPACE_MD5=$(echo "$CURRENT_DIR" | md5sum | cut -c1-8)
1320
- mkdir -p "$GIT_SHARED_DIR/keys"
1321
- printf "%s\n" "${SELECTED_SSH_KEYS[@]}" > "$GIT_SHARED_DIR/keys/$WORKSPACE_MD5"
1322
- echo "✅ Git access enabled and saved for: $CURRENT_DIR"
1323
- elif [ "$git_choice" = "5" ]; then
1324
- # Save workspace to fetchOnlyWorkspaces
1325
- if has_jq && [[ -f "$AI_SANDBOX_CONFIG" ]]; then
1326
- jq --arg ws "$CURRENT_DIR" '.git.fetchOnlyWorkspaces = ((.git.fetchOnlyWorkspaces // []) + [$ws] | unique)' "$AI_SANDBOX_CONFIG" > "$AI_SANDBOX_CONFIG.tmp" \
1327
- && mv "$AI_SANDBOX_CONFIG.tmp" "$AI_SANDBOX_CONFIG"
1328
- 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"
1329
1553
  fi
1330
- # Save selected keys (one per line for easier parsing)
1331
- WORKSPACE_MD5=$(echo "$CURRENT_DIR" | md5sum | cut -c1-8)
1332
- mkdir -p "$GIT_SHARED_DIR/keys"
1333
- printf "%s\n" "${SELECTED_SSH_KEYS[@]}" > "$GIT_SHARED_DIR/keys/$WORKSPACE_MD5"
1334
- echo "✅ Git fetch-only access enabled and saved for: $CURRENT_DIR"
1335
- elif [ "$git_choice" = "4" ]; then
1336
- echo "✅ Git fetch-only access enabled for this session"
1337
1554
  else
1338
- echo " Git access enabled for this session"
1555
+ echo "⚠️ No SSH keys selected. Git access disabled."
1339
1556
  fi
1340
1557
  else
1341
- echo "⚠️ No SSH keys selected. Git access disabled."
1558
+ echo "⚠️ SSH key selection cancelled. Git access disabled."
1342
1559
  fi
1343
- else
1344
- echo "⚠️ SSH key selection cancelled. Git access disabled."
1345
1560
  fi
1346
1561
  ;;
1347
1562
  *)
@@ -1436,7 +1651,7 @@ generate_container_name() {
1436
1651
  # Generate random 6-character suffix (hex)
1437
1652
  local random_suffix=$(openssl rand -hex 3)
1438
1653
 
1439
- echo "${TOOL}-${folder_name}-${random_suffix}"
1654
+ echo "${TOOL:-shell}-${folder_name}-${random_suffix}"
1440
1655
  }
1441
1656
 
1442
1657
  # Container name and TTY allocation
@@ -1516,7 +1731,7 @@ resolve_opencode_password() {
1516
1731
  echo " 3) No password (⚠️ unsecured - localhost only)"
1517
1732
  echo ""
1518
1733
  read -p "Choice [1-3]: " password_choice
1519
-
1734
+
1520
1735
  case "$password_choice" in
1521
1736
  1)
1522
1737
  local generated_password=$(openssl rand -base64 24 | tr -d '/+=' | head -c 24)
@@ -1585,11 +1800,11 @@ is_mcp_installed() {
1585
1800
  is_mcp_configured() {
1586
1801
  local mcp_name="$1"
1587
1802
  local config_file="$HOME/.config/opencode/opencode.json"
1588
-
1803
+
1589
1804
  if [[ ! -f "$config_file" ]]; then
1590
1805
  return 1
1591
1806
  fi
1592
-
1807
+
1593
1808
  if has_jq; then
1594
1809
  jq -e --arg name "$mcp_name" '.mcp[$name] // empty' "$config_file" &>/dev/null
1595
1810
  else
@@ -1603,7 +1818,7 @@ add_mcp_config() {
1603
1818
  local mcp_command="$2"
1604
1819
  local force="${3:-false}"
1605
1820
  local config_file="$HOME/.config/opencode/opencode.json"
1606
-
1821
+
1607
1822
  # Check if already configured and warn user
1608
1823
  if [[ "$force" != "true" ]] && is_mcp_configured "$mcp_name"; then
1609
1824
  echo ""
@@ -1616,9 +1831,9 @@ add_mcp_config() {
1616
1831
  return 1
1617
1832
  fi
1618
1833
  fi
1619
-
1834
+
1620
1835
  mkdir -p "$(dirname "$config_file")"
1621
-
1836
+
1622
1837
  if [[ ! -f "$config_file" ]]; then
1623
1838
  # Create new config with MCP
1624
1839
  echo "{\"mcp\": {\"$mcp_name\": {\"type\": \"local\", \"command\": $mcp_command}}}" > "$config_file"
@@ -1651,47 +1866,47 @@ configure_opencode_mcp() {
1651
1866
  # Only run for opencode tool in interactive mode
1652
1867
  [[ "$TOOL" != "opencode" ]] && return 0
1653
1868
  [[ ! -t 0 ]] && return 0
1654
-
1869
+
1655
1870
  local config_file="$HOME/.config/opencode/opencode.json"
1656
-
1871
+
1657
1872
  # Generate workspace hash for skip tracking
1658
1873
  WORKSPACE_MD5=$(echo "$CURRENT_DIR" | md5sum | cut -c1-8)
1659
-
1874
+
1660
1875
  # Check if already skipped for this workspace
1661
1876
  if is_mcp_skipped; then
1662
1877
  return 0
1663
1878
  fi
1664
-
1879
+
1665
1880
  # Detect installed MCP tools (from config.json, set during setup.sh)
1666
1881
  local chrome_installed=false
1667
1882
  local playwright_installed=false
1668
1883
  local chrome_configured=false
1669
1884
  local playwright_configured=false
1670
-
1885
+
1671
1886
  if is_mcp_installed "chrome-devtools"; then
1672
1887
  chrome_installed=true
1673
1888
  is_mcp_configured "chrome-devtools" && chrome_configured=true
1674
1889
  fi
1675
-
1890
+
1676
1891
  if is_mcp_installed "playwright"; then
1677
1892
  playwright_installed=true
1678
1893
  is_mcp_configured "playwright" && playwright_configured=true
1679
1894
  fi
1680
-
1895
+
1681
1896
  # If no MCP tools installed in image, return
1682
1897
  if [[ "$chrome_installed" == "false" && "$playwright_installed" == "false" ]]; then
1683
1898
  return 0
1684
1899
  fi
1685
-
1900
+
1686
1901
  # If all installed tools are already configured, return silently
1687
1902
  local all_configured=true
1688
1903
  [[ "$chrome_installed" == "true" && "$chrome_configured" == "false" ]] && all_configured=false
1689
1904
  [[ "$playwright_installed" == "true" && "$playwright_configured" == "false" ]] && all_configured=false
1690
-
1905
+
1691
1906
  if [[ "$all_configured" == "true" ]]; then
1692
1907
  return 0
1693
1908
  fi
1694
-
1909
+
1695
1910
  # Build lists of configured and unconfigured tools
1696
1911
  local unconfigured=()
1697
1912
  local configured=()
@@ -1699,12 +1914,12 @@ configure_opencode_mcp() {
1699
1914
  [[ "$chrome_installed" == "true" && "$chrome_configured" == "true" ]] && configured+=("chrome-devtools")
1700
1915
  [[ "$playwright_installed" == "true" && "$playwright_configured" == "false" ]] && unconfigured+=("playwright")
1701
1916
  [[ "$playwright_installed" == "true" && "$playwright_configured" == "true" ]] && configured+=("playwright")
1702
-
1917
+
1703
1918
  # Show prompt
1704
1919
  echo ""
1705
1920
  echo "🔌 MCP Tools Detected"
1706
1921
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
1707
-
1922
+
1708
1923
  # Show already configured tools
1709
1924
  if [[ ${#configured[@]} -gt 0 ]]; then
1710
1925
  echo "Already configured:"
@@ -1720,7 +1935,7 @@ configure_opencode_mcp() {
1720
1935
  done
1721
1936
  echo ""
1722
1937
  fi
1723
-
1938
+
1724
1939
  # Show unconfigured tools
1725
1940
  if [[ ${#unconfigured[@]} -gt 0 ]]; then
1726
1941
  echo "Not yet configured:"
@@ -1736,7 +1951,7 @@ configure_opencode_mcp() {
1736
1951
  done
1737
1952
  echo ""
1738
1953
  fi
1739
-
1954
+
1740
1955
  echo "Configure MCP tools in OpenCode?"
1741
1956
  echo " 1) Yes, configure all detected tools"
1742
1957
  echo " 2) Yes, let me choose which ones"
@@ -1744,16 +1959,16 @@ configure_opencode_mcp() {
1744
1959
  echo " 4) No, don't ask again for this workspace"
1745
1960
  echo ""
1746
1961
  read -p "Choice [1-4]: " mcp_choice
1747
-
1962
+
1748
1963
  local configured_any=false
1749
-
1964
+
1750
1965
  case "$mcp_choice" in
1751
1966
  1)
1752
1967
  # Configure all (both unconfigured and offer to reconfigure configured ones)
1753
1968
  local all_tools=()
1754
1969
  [[ "$chrome_installed" == "true" ]] && all_tools+=("chrome-devtools")
1755
1970
  [[ "$playwright_installed" == "true" ]] && all_tools+=("playwright")
1756
-
1971
+
1757
1972
  for tool in "${all_tools[@]}"; do
1758
1973
  case "$tool" in
1759
1974
  chrome-devtools)
@@ -1763,7 +1978,7 @@ configure_opencode_mcp() {
1763
1978
  fi
1764
1979
  ;;
1765
1980
  playwright)
1766
- 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
1767
1982
  echo " ✓ Configured Playwright MCP"
1768
1983
  configured_any=true
1769
1984
  fi
@@ -1776,7 +1991,7 @@ configure_opencode_mcp() {
1776
1991
  local all_tools=()
1777
1992
  [[ "$chrome_installed" == "true" ]] && all_tools+=("chrome-devtools")
1778
1993
  [[ "$playwright_installed" == "true" ]] && all_tools+=("playwright")
1779
-
1994
+
1780
1995
  for tool in "${all_tools[@]}"; do
1781
1996
  local tool_desc=""
1782
1997
  local status_hint=""
@@ -1800,7 +2015,7 @@ configure_opencode_mcp() {
1800
2015
  fi
1801
2016
  ;;
1802
2017
  playwright)
1803
- 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
1804
2019
  echo " ✓ Configured"
1805
2020
  configured_any=true
1806
2021
  fi
@@ -1819,7 +2034,7 @@ configure_opencode_mcp() {
1819
2034
  echo "ℹ️ Skipped."
1820
2035
  ;;
1821
2036
  esac
1822
-
2037
+
1823
2038
  # Show config file path for easy editing
1824
2039
  echo ""
1825
2040
  if [[ "$configured_any" == "true" ]]; then
@@ -1885,7 +2100,7 @@ check_port_in_use() {
1885
2100
  else
1886
2101
  return 2
1887
2102
  fi
1888
-
2103
+
1889
2104
  docker ps --format "{{.Ports}}" 2>/dev/null | grep -q ":$port->" && return 0
1890
2105
  return 1
1891
2106
  }
@@ -1939,10 +2154,10 @@ if detect_opencode_web; then
1939
2154
  if [[ -z "$WEB_PORT" ]]; then
1940
2155
  WEB_PORT=4096
1941
2156
  fi
1942
-
2157
+
1943
2158
  add_port_to_list "$WEB_PORT" "auto-detected"
1944
2159
  echo "🌐 Detected web command. Auto-exposing port $WEB_PORT."
1945
-
2160
+
1946
2161
  if ! has_hostname_arg; then
1947
2162
  TOOL_ARGS+=("--hostname" "0.0.0.0")
1948
2163
  fi
@@ -1997,7 +2212,7 @@ if [[ -n "$EXPOSE_PORTS_LIST" ]]; then
1997
2212
  done
1998
2213
 
1999
2214
  echo "🔌 Port mappings: $EXPOSE_PORTS_LIST"
2000
-
2215
+
2001
2216
  if [[ "$WEB_DETECTED" == "true" ]]; then
2002
2217
  echo "🌐 Web UI available at http://localhost:$WEB_PORT"
2003
2218
  fi
@@ -2017,20 +2232,115 @@ if [[ "${AI_RUN_DEBUG:-}" == "1" ]]; then
2017
2232
  echo "🔧 Debug: EXPOSE_PORTS_LIST='$EXPOSE_PORTS_LIST'"
2018
2233
  fi
2019
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
+
2020
2302
  # Prepare command based on mode
2021
2303
  ENTRYPOINT_OVERRIDE=""
2022
- if [[ "$SHELL_MODE" == "true" ]]; then
2023
- # Shell mode: override entrypoint to bash and show welcome message
2304
+ if [[ -n "$TOOL" && "$SHELL_MODE" != "true" ]]; then
2305
+ # Direct tool execution: override entrypoint to the tool
2306
+ ENTRYPOINT_OVERRIDE="--entrypoint $TOOL"
2307
+ DOCKER_COMMAND=("${TOOL_ARGS[@]}")
2308
+ elif [[ "$SHELL_MODE" == "true" || -z "$TOOL" ]]; then
2309
+ # Shell mode (explicit --shell or no tool specified)
2024
2310
  ENTRYPOINT_OVERRIDE="--entrypoint bash"
2025
- DOCKER_COMMAND=(
2026
- "-c"
2027
- "echo ''; echo '🚀 AI Tool Container - Interactive Shell'; echo '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'; echo ''; echo \"Tool available: ${TOOL}\"; echo 'Run the tool: ${TOOL}'; echo 'Exit container: exit or Ctrl+D'; echo ''; echo \"Additional tools:\"; echo ' - specify (spec-kit): Spec-driven development'; echo ' - uipro (ux-ui-promax): UI/UX design intelligence'; echo ' - openspec: OpenSpec workflow'; echo ''; echo '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'; echo ''; exec bash"
2028
- )
2311
+ # Build welcome message with installed tools
2312
+ INSTALLED_TOOLS_MSG=""
2313
+ if command -v jq &>/dev/null && [[ -f "$SANDBOX_DIR/config.json" ]]; then
2314
+ INSTALLED_TOOLS_MSG=$(jq -r '.tools.installed // [] | join(", ")' "$SANDBOX_DIR/config.json" 2>/dev/null || echo "")
2315
+ fi
2316
+ if [[ -n "$TOOL" ]]; then
2317
+ # Shell mode with specific tool (ai-run claude --shell)
2318
+ DOCKER_COMMAND=(
2319
+ "-c"
2320
+ "echo ''; echo '🚀 AI Tool Container - Interactive Shell'; echo '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'; echo ''; echo \"Tool available: ${TOOL}\"; echo 'Run the tool: ${TOOL}'; echo 'Exit container: exit or Ctrl+D'; echo ''; echo \"Additional tools:\"; echo ' - specify (spec-kit): Spec-driven development'; echo ' - uipro (ux-ui-promax): UI/UX design intelligence'; echo ' - openspec: OpenSpec workflow'; echo ''; echo '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'; echo ''; exec bash"
2321
+ )
2322
+ else
2323
+ # Shell mode without tool (ai-run with no args)
2324
+ DOCKER_COMMAND=(
2325
+ "-c"
2326
+ "echo ''; echo '🚀 AI Sandbox - Interactive Shell'; echo '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'; echo ''; echo 'Installed tools: ${INSTALLED_TOOLS_MSG:-unknown}'; echo ''; echo 'Run any tool by name: claude, opencode, gemini, etc.'; echo 'Exit: exit or Ctrl+D'; echo ''; echo 'Enhancement tools: specify, uipro, openspec, rtk'; echo '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'; echo ''; exec bash"
2327
+ )
2328
+ fi
2029
2329
  else
2030
- # Direct mode: use image's default entrypoint with arguments
2330
+ # Fallback: direct mode with image's default entrypoint
2031
2331
  DOCKER_COMMAND=("${TOOL_ARGS[@]}")
2032
2332
  fi
2033
2333
 
2334
+ # Nano-brain targeted preflight + auto-repair wrapper
2335
+ if [[ "$SHELL_MODE" != "true" ]] && is_nano_brain_command; then
2336
+ if [[ "$NANO_BRAIN_AUTO_REPAIR" == "true" ]]; then
2337
+ ENTRYPOINT_OVERRIDE="--entrypoint bash"
2338
+ DOCKER_COMMAND=("-lc" "$NANO_BRAIN_AUTOREPAIR_SCRIPT" "nano-brain-wrapper" "$TOOL" "${TOOL_ARGS[@]}")
2339
+ else
2340
+ echo "ℹ️ nano-brain auto-repair disabled"
2341
+ fi
2342
+ fi
2343
+
2034
2344
  # Detect platform architecture (avoid slow emulation)
2035
2345
  PLATFORM="${AI_RUN_PLATFORM:-}"
2036
2346
  if [[ -z "$PLATFORM" ]]; then
@@ -2061,13 +2371,17 @@ fi
2061
2371
  mkdir -p "$HOME_DIR" "$GIT_SHARED_DIR"
2062
2372
  chmod -R u+w "$HOME_DIR" "$GIT_SHARED_DIR" 2>/dev/null || true
2063
2373
 
2064
- # Selective Cache Isolation (Anonymous Volumes)
2065
- # Use anonymous volumes to "hide" host-poisoned node_modules and caches
2066
- # while keeping configurations and auth data.
2067
- CACHE_MOUNTS=""
2068
- if [[ "$TOOL" == "opencode" ]]; then
2069
- # Isolate heavy caches and node_modules to prevent Bun/Node conflicts
2070
- CACHE_MOUNTS="-v /home/agent/.npm -v /home/agent/.cache -v /home/agent/.opencode/node_modules"
2374
+ if [[ ! -f "$CACHE_DIR/playwright-browsers/.seeded" ]]; then
2375
+ if docker run --rm "$IMAGE" test -d /opt/playwright-browsers 2>/dev/null; then
2376
+ echo "🔄 Seeding shared Playwright browser cache..."
2377
+ docker run --rm -v "$CACHE_DIR/playwright-browsers":/export "$IMAGE" \
2378
+ cp -a /opt/playwright-browsers/. /export/ 2>/dev/null && \
2379
+ touch "$CACHE_DIR/playwright-browsers/.seeded" && \
2380
+ echo " Playwright browsers cached" || \
2381
+ echo "⚠️ Playwright browser seeding failed (will retry next run)"
2382
+ else
2383
+ touch "$CACHE_DIR/playwright-browsers/.seeded"
2384
+ fi
2071
2385
  fi
2072
2386
 
2073
2387
  # Detect display configuration (clipboard integration)
@@ -2078,20 +2392,20 @@ DISPLAY_FLAGS=$(detect_display_config)
2078
2392
  # ============================================================================
2079
2393
  if [[ "$TOOL" == "openclaw" ]]; then
2080
2394
  OPENCLAW_REPO_DIR="$HOME/.ai-sandbox/tools/openclaw/repo"
2081
-
2395
+
2082
2396
  if [[ ! -d "$OPENCLAW_REPO_DIR" ]]; then
2083
2397
  echo "❌ ERROR: OpenClaw repository not found at $OPENCLAW_REPO_DIR"
2084
2398
  echo " Run: npx @kokorolx/ai-sandbox-wrapper setup"
2085
2399
  exit 1
2086
2400
  fi
2087
-
2401
+
2088
2402
  cd "$OPENCLAW_REPO_DIR"
2089
-
2403
+
2090
2404
  OPENCLAW_COMPOSE_FILE="$OPENCLAW_REPO_DIR/docker-compose.yml"
2091
2405
  OPENCLAW_OVERRIDE_FILE="$OPENCLAW_REPO_DIR/docker-compose.override.yml"
2092
-
2406
+
2093
2407
  echo "🔄 Generating OpenClaw docker-compose override..."
2094
-
2408
+
2095
2409
  cat > "$OPENCLAW_OVERRIDE_FILE" <<EOF
2096
2410
  services:
2097
2411
  openclaw-gateway:
@@ -2101,18 +2415,18 @@ services:
2101
2415
  volumes:
2102
2416
  - $HOME/.openclaw:/home/node/.openclaw
2103
2417
  EOF
2104
-
2418
+
2105
2419
  for workspace in "${WORKSPACES[@]}"; do
2106
2420
  echo " - $workspace:$workspace" >> "$OPENCLAW_OVERRIDE_FILE"
2107
2421
  done
2108
-
2422
+
2109
2423
  cat >> "$OPENCLAW_OVERRIDE_FILE" <<EOF
2110
2424
  ports:
2111
2425
  - "18789:18789"
2112
2426
  - "18790:18790"
2113
2427
  working_dir: $CURRENT_DIR
2114
2428
  EOF
2115
-
2429
+
2116
2430
  if [[ -n "$NETWORK_OPTIONS" ]]; then
2117
2431
  echo " networks:" >> "$OPENCLAW_OVERRIDE_FILE"
2118
2432
  for net in ${DOCKER_NETWORKS//,/ }; do
@@ -2125,12 +2439,12 @@ EOF
2125
2439
  echo " external: true" >> "$OPENCLAW_OVERRIDE_FILE"
2126
2440
  done
2127
2441
  fi
2128
-
2442
+
2129
2443
  echo "🚀 Starting OpenClaw with docker-compose..."
2130
2444
  echo "🌐 Gateway: http://localhost:18789"
2131
2445
  echo "🌐 Bridge: http://localhost:18790"
2132
2446
  echo ""
2133
-
2447
+
2134
2448
  exec docker compose -f "$OPENCLAW_COMPOSE_FILE" -f "$OPENCLAW_OVERRIDE_FILE" \
2135
2449
  --env-file "$ENV_FILE" \
2136
2450
  up --remove-orphans
@@ -2144,13 +2458,14 @@ docker run $CONTAINER_NAME --rm $TTY_FLAGS \
2144
2458
  $CONFIG_MOUNT \
2145
2459
  $TOOL_CONFIG_MOUNTS \
2146
2460
  $GIT_MOUNTS \
2461
+ $SSH_AGENT_ENV \
2147
2462
  $NETWORK_OPTIONS \
2148
- $CACHE_MOUNTS \
2149
2463
  $DISPLAY_FLAGS \
2150
2464
  $HOST_ACCESS_ARGS \
2151
2465
  $PORT_MAPPINGS \
2152
2466
  $OPENCODE_PASSWORD_ENV \
2153
2467
  -v "$HOME_DIR":/home/agent \
2468
+ $SHARED_CACHE_MOUNTS \
2154
2469
  -w "$CURRENT_DIR" \
2155
2470
  --env-file "$ENV_FILE" \
2156
2471
  -e TERM="$TERM" \