@kokorolx/ai-sandbox-wrapper 2.4.0 → 2.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/ai-run CHANGED
@@ -15,7 +15,9 @@ Options:
15
15
  -p, --password <value> Set OpenCode server password (for web/serve mode)
16
16
  --password-env <VAR> Read OpenCode server password from environment variable
17
17
  --allow-unsecured Allow OpenCode server to run without password (suppresses warning)
18
+ --git-fetch Enable git fetch-only mode (blocks push)
18
19
  -h, --help Show this help message
20
+ --help-env Show environment variables reference
19
21
 
20
22
  OpenCode Server Authentication (web/serve mode only):
21
23
  When running 'opencode web' or 'opencode serve', you can control authentication:
@@ -38,17 +40,92 @@ Examples:
38
40
  ai-run opencode web -p secret # Run with password
39
41
  ai-run opencode --shell # Start shell, run tool manually
40
42
  ai-run aider -n mynetwork # Connect to Docker network
43
+ ai-run opencode --git-fetch # Git fetch only (no push)
41
44
 
42
45
  Documentation: https://github.com/kokorolx/ai-sandbox-wrapper
43
46
  EOF
44
47
  exit 0
45
48
  }
46
49
 
50
+ show_help_env() {
51
+ cat << 'EOF'
52
+ Environment Variables Reference
53
+ ===============================
54
+
55
+ Runtime Environment Variables (inline with ai-run):
56
+ ---------------------------------------------------
57
+ AI_IMAGE_SOURCE=registry Use pre-built images from GitLab registry instead of local
58
+ Example: AI_IMAGE_SOURCE=registry ai-run opencode
59
+
60
+ AI_RUN_DEBUG=1 Enable debug output (shows Docker command details)
61
+ Example: AI_RUN_DEBUG=1 ai-run opencode
62
+
63
+ AI_RUN_PLATFORM=linux/amd64 Force specific platform (useful for cross-arch)
64
+ Example: AI_RUN_PLATFORM=linux/amd64 ai-run opencode
65
+
66
+ PORT_BIND=all Bind exposed ports to all interfaces (0.0.0.0) instead of localhost
67
+ ⚠️ Security risk - use only when needed
68
+ Example: PORT_BIND=all ai-run opencode web -e 4096
69
+
70
+ PORT=3000,4000 (Deprecated) Expose ports - use --expose flag instead
71
+ Example: PORT=3000 ai-run opencode
72
+
73
+ Build Environment Variables (inline with setup.sh or install scripts):
74
+ ----------------------------------------------------------------------
75
+ DOCKER_NO_CACHE=1 Force rebuild without Docker cache
76
+ Example: DOCKER_NO_CACHE=1 bash setup.sh
77
+
78
+ OPENCODE_VERSION=1.1.52 Install specific OpenCode version instead of latest
79
+ Example: OPENCODE_VERSION=1.1.52 bash lib/install-opencode.sh
80
+
81
+ INSTALL_CHROME_DEVTOOLS_MCP=1 Install Chrome DevTools MCP in base image
82
+ INSTALL_PLAYWRIGHT_MCP=1 Install Playwright MCP in base image
83
+ INSTALL_PLAYWRIGHT=1 Install Playwright browser automation
84
+ INSTALL_RUBY=1 Install Ruby 3.3.0 + Rails 8.0.2
85
+ INSTALL_SPEC_KIT=1 Install spec-kit (specify CLI)
86
+ INSTALL_UX_UI_PROMAX=1 Install uipro CLI
87
+ INSTALL_OPENSPEC=1 Install OpenSpec CLI
88
+
89
+ Container Environment Variables (passed to container via ~/.ai-sandbox/env):
90
+ -----------------------------------------------------------------------------
91
+ ANTHROPIC_API_KEY Anthropic API key for Claude
92
+ OPENAI_API_KEY OpenAI API key
93
+ OPENCODE_SERVER_PASSWORD Password for OpenCode web server
94
+ OPENCODE_SERVER_USERNAME Username for OpenCode web server (default: opencode)
95
+ GITHUB_TOKEN GitHub token for gh CLI authentication
96
+ GH_TOKEN Alternative GitHub token variable
97
+
98
+ Configuration Files:
99
+ --------------------
100
+ ~/.ai-sandbox/config.json Main configuration (workspaces, git, networks, MCP)
101
+ ~/.ai-sandbox/env API keys and secrets (KEY=value format)
102
+ ~/.ai-sandbox/workspaces Legacy workspace list (one path per line)
103
+
104
+ Examples:
105
+ # Debug mode with registry images
106
+ AI_RUN_DEBUG=1 AI_IMAGE_SOURCE=registry ai-run opencode
107
+
108
+ # Rebuild everything from scratch
109
+ DOCKER_NO_CACHE=1 bash setup.sh --no-cache
110
+
111
+ # Install specific OpenCode version
112
+ OPENCODE_VERSION=1.1.52 bash lib/install-opencode.sh
113
+
114
+ # Expose port to network (not just localhost)
115
+ PORT_BIND=all ai-run opencode web -e 4096
116
+ EOF
117
+ exit 0
118
+ }
119
+
47
120
  # Check for help flag before anything else
48
121
  if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
49
122
  show_help
50
123
  fi
51
124
 
125
+ if [[ "${1:-}" == "--help-env" ]]; then
126
+ show_help_env
127
+ fi
128
+
52
129
  TOOL="$1"
53
130
  shift 2>/dev/null || { echo "❌ ERROR: No tool specified. Use 'ai-run --help' for usage."; exit 1; }
54
131
 
@@ -60,6 +137,7 @@ EXPOSE_ARG=""
60
137
  SERVER_PASSWORD=""
61
138
  PASSWORD_ENV_VAR=""
62
139
  ALLOW_UNSECURED=false
140
+ GIT_FETCH_ONLY_FLAG=false
63
141
  TOOL_ARGS=()
64
142
 
65
143
  while [[ $# -gt 0 ]]; do
@@ -102,6 +180,10 @@ while [[ $# -gt 0 ]]; do
102
180
  ALLOW_UNSECURED=true
103
181
  shift
104
182
  ;;
183
+ --git-fetch)
184
+ GIT_FETCH_ONLY_FLAG=true
185
+ shift
186
+ ;;
105
187
  *)
106
188
  TOOL_ARGS+=("$1")
107
189
  shift
@@ -336,7 +418,7 @@ upgrade_config_to_v2() {
336
418
  fi
337
419
 
338
420
  # Add missing v2 fields while preserving existing networks
339
- jq '.version = 2 | .workspaces = (.workspaces // []) | .git = (.git // {"allowedWorkspaces":[],"keySelections":{}})' \
421
+ jq '.version = 2 | .workspaces = (.workspaces // []) | .git = (.git // {"allowedWorkspaces":[],"keySelections":{}}) | .git.fetchOnlyWorkspaces = (.git.fetchOnlyWorkspaces // [])' \
340
422
  "$AI_SANDBOX_CONFIG" > "$AI_SANDBOX_CONFIG.tmp" \
341
423
  && mv "$AI_SANDBOX_CONFIG.tmp" "$AI_SANDBOX_CONFIG"
342
424
  chmod 600 "$AI_SANDBOX_CONFIG"
@@ -351,7 +433,7 @@ init_config() {
351
433
 
352
434
  if [[ ! -f "$AI_SANDBOX_CONFIG" ]]; then
353
435
  # Create new v2 config
354
- local initial_config='{"version":2,"workspaces":[],"git":{"allowedWorkspaces":[],"keySelections":{}},"networks":{"global":[],"workspaces":{}}}'
436
+ local initial_config='{"version":2,"workspaces":[],"git":{"allowedWorkspaces":[],"fetchOnlyWorkspaces":[],"keySelections":{}},"networks":{"global":[],"workspaces":{}}}'
355
437
  echo "$initial_config" > "$AI_SANDBOX_CONFIG"
356
438
  chmod 600 "$AI_SANDBOX_CONFIG"
357
439
 
@@ -508,9 +590,27 @@ case "$TOOL" in
508
590
  opencode)
509
591
  mount_tool_config "$HOME/.config/opencode" ".config/opencode"
510
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"
511
610
  ;;
512
611
  claude)
513
612
  mount_tool_config "$HOME/.claude" ".claude"
613
+ mount_tool_config "$HOME/.ccs" ".ccs"
514
614
  ;;
515
615
  droid)
516
616
  mount_tool_config "$HOME/.config/droid" ".config/droid"
@@ -972,8 +1072,48 @@ is_git_allowed() {
972
1072
  return 1
973
1073
  }
974
1074
 
1075
+ # Check if workspace is in fetch-only mode (config.json)
1076
+ is_git_fetch_only() {
1077
+ local ws="$1"
1078
+ if has_jq && [[ -f "$AI_SANDBOX_CONFIG" ]]; then
1079
+ local fetch_only=$(jq -r --arg ws "$ws" '.git.fetchOnlyWorkspaces // [] | index($ws) // -1' "$AI_SANDBOX_CONFIG" 2>/dev/null)
1080
+ [[ "$fetch_only" -ge 0 ]] && return 0
1081
+ fi
1082
+ return 1
1083
+ }
1084
+
1085
+ # Apply fetch-only restrictions by adding pushInsteadOf to gitconfig
1086
+ apply_git_fetch_only() {
1087
+ local gitconfig_path="$HOME_DIR/.gitconfig"
1088
+ # Ensure .gitconfig exists
1089
+ touch "$gitconfig_path"
1090
+ # Append push-blocking config (redirects all push URLs to non-existent protocol)
1091
+ cat >> "$gitconfig_path" << 'FETCHONLY'
1092
+
1093
+ # AI Sandbox: Git fetch-only mode (push disabled)
1094
+ [url "no-push://blocked"]
1095
+ pushInsteadOf = git@
1096
+ pushInsteadOf = ssh://
1097
+ pushInsteadOf = https://
1098
+ pushInsteadOf = http://
1099
+ FETCHONLY
1100
+ echo "🔒 Git fetch-only mode: push operations are blocked"
1101
+ }
1102
+
1103
+ GIT_FETCH_ONLY_MODE=false
1104
+
1105
+ # Check --git-fetch flag
1106
+ if [[ "$GIT_FETCH_ONLY_FLAG" == "true" ]]; then
1107
+ GIT_FETCH_ONLY_MODE=true
1108
+ fi
1109
+
1110
+ # Check if workspace is in fetch-only list
1111
+ if is_git_fetch_only "$CURRENT_DIR"; then
1112
+ GIT_FETCH_ONLY_MODE=true
1113
+ fi
1114
+
975
1115
  # Check if Git access is allowed for this workspace
976
- if is_git_allowed "$CURRENT_DIR"; then
1116
+ if is_git_allowed "$CURRENT_DIR" || is_git_fetch_only "$CURRENT_DIR" || [[ "$GIT_FETCH_ONLY_FLAG" == "true" ]]; then
977
1117
  # Previously allowed for this workspace
978
1118
  # Check if saved keys exist for this workspace
979
1119
  WORKSPACE_MD5=$(echo "$CURRENT_DIR" | md5sum | cut -c1-8)
@@ -1015,7 +1155,7 @@ if is_git_allowed "$CURRENT_DIR"; then
1015
1155
  if [ -x "$SETUP_SSH" ]; then
1016
1156
  # Join SAVED_KEYS into a comma-separated string for --keys
1017
1157
  KEYS_ARG=$(IFS=,; echo "${SAVED_KEYS[*]}")
1018
- output=$("$SETUP_SSH" --keys "$KEYS_ARG" 2>&1)
1158
+ output=$( "$SETUP_SSH" --keys "$KEYS_ARG" 2>&1 ) || true
1019
1159
  TEMP_CONFIG=$(echo "$output" | grep "Config:" | tail -1 | awk '{print $NF}')
1020
1160
  if [ -f "$TEMP_CONFIG" ]; then
1021
1161
  cp "$TEMP_CONFIG" "$GIT_CACHE_DIR/ssh/config" 2>/dev/null || true
@@ -1048,6 +1188,11 @@ if is_git_allowed "$CURRENT_DIR"; then
1048
1188
  # Copy gitconfig to HOME_DIR (can't mount file inside mounted directory)
1049
1189
  cp "$HOME/.gitconfig" "$HOME_DIR/.gitconfig" 2>/dev/null || true
1050
1190
  fi
1191
+
1192
+ # Apply fetch-only restrictions if mode is active
1193
+ if [[ "$GIT_FETCH_ONLY_MODE" == "true" ]]; then
1194
+ apply_git_fetch_only
1195
+ fi
1051
1196
  else
1052
1197
  # Ask user if they want Git access for this workspace (only in interactive mode)
1053
1198
  if [[ -t 0 ]] && ([ -d "$HOME/.ssh" ] || [ -f "$HOME/.gitconfig" ]); then
@@ -1060,11 +1205,13 @@ else
1060
1205
  echo " 1) Yes, allow once (this session only)"
1061
1206
  echo " 2) Yes, always allow for this workspace"
1062
1207
  echo " 3) No, keep Git disabled (secure default)"
1208
+ echo " 4) Fetch only - allow once (no push, this session)"
1209
+ echo " 5) Fetch only - always for this workspace (no push)"
1063
1210
  echo ""
1064
- read -p "Choice [1-3]: " git_choice
1211
+ read -p "Choice [1-5]: " git_choice
1065
1212
 
1066
1213
  case "$git_choice" in
1067
- 1|2)
1214
+ 1|2|4|5)
1068
1215
  # Interactive SSH key selection
1069
1216
  echo ""
1070
1217
  echo "🔑 SSH Key Selection"
@@ -1120,7 +1267,7 @@ else
1120
1267
  # Join SELECTED_SSH_KEYS into a comma-separated string for --keys
1121
1268
  KEYS_ARG=$(IFS=,; echo "${SELECTED_SSH_KEYS[*]}")
1122
1269
  # Run it and capture the filtered config path
1123
- output=$("$SETUP_SSH" --keys "$KEYS_ARG" 2>&1)
1270
+ output=$( "$SETUP_SSH" --keys "$KEYS_ARG" 2>&1 ) || true
1124
1271
  TEMP_CONFIG=$(echo "$output" | grep "Config:" | tail -1 | awk '{print $NF}')
1125
1272
  if [ -f "$TEMP_CONFIG" ]; then
1126
1273
  cp "$TEMP_CONFIG" "$GIT_CACHE_DIR/ssh/config" 2>/dev/null || true
@@ -1156,6 +1303,9 @@ else
1156
1303
  cp "$HOME/.gitconfig" "$HOME_DIR/.gitconfig" 2>/dev/null || true
1157
1304
  fi
1158
1305
 
1306
+ if [[ "$git_choice" == "4" || "$git_choice" == "5" ]]; then
1307
+ GIT_FETCH_ONLY_MODE=true
1308
+ fi
1159
1309
  if [ "$git_choice" = "2" ]; then
1160
1310
  # Save workspace and selected keys for future sessions
1161
1311
  echo "$CURRENT_DIR" >> "$GIT_ALLOWED_FILE"
@@ -1170,6 +1320,20 @@ else
1170
1320
  mkdir -p "$GIT_SHARED_DIR/keys"
1171
1321
  printf "%s\n" "${SELECTED_SSH_KEYS[@]}" > "$GIT_SHARED_DIR/keys/$WORKSPACE_MD5"
1172
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"
1329
+ 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"
1173
1337
  else
1174
1338
  echo "✅ Git access enabled for this session"
1175
1339
  fi
@@ -1185,6 +1349,11 @@ else
1185
1349
  echo "🔒 Git access disabled (secure mode)"
1186
1350
  ;;
1187
1351
  esac
1352
+
1353
+ # Apply fetch-only restrictions if mode is active
1354
+ if [[ "$GIT_FETCH_ONLY_MODE" == "true" ]]; then
1355
+ apply_git_fetch_only
1356
+ fi
1188
1357
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
1189
1358
  echo ""
1190
1359
  fi
@@ -1398,6 +1567,273 @@ if is_opencode_web_mode; then
1398
1567
  resolve_opencode_password
1399
1568
  fi
1400
1569
 
1570
+ # ============================================================================
1571
+ # MCP AUTO-CONFIGURATION FOR OPENCODE
1572
+ # ============================================================================
1573
+
1574
+ # Check if MCP tool is installed (from config.json, set during setup.sh)
1575
+ is_mcp_installed() {
1576
+ local mcp_tool="$1"
1577
+ if has_jq && [[ -f "$AI_SANDBOX_CONFIG" ]]; then
1578
+ jq -e --arg tool "$mcp_tool" '.mcp.installed | index($tool) != null' "$AI_SANDBOX_CONFIG" &>/dev/null
1579
+ else
1580
+ return 1
1581
+ fi
1582
+ }
1583
+
1584
+ # Check if MCP is already configured in OpenCode config
1585
+ is_mcp_configured() {
1586
+ local mcp_name="$1"
1587
+ local config_file="$HOME/.config/opencode/opencode.json"
1588
+
1589
+ if [[ ! -f "$config_file" ]]; then
1590
+ return 1
1591
+ fi
1592
+
1593
+ if has_jq; then
1594
+ jq -e --arg name "$mcp_name" '.mcp[$name] // empty' "$config_file" &>/dev/null
1595
+ else
1596
+ grep -q "\"$mcp_name\"" "$config_file" 2>/dev/null
1597
+ fi
1598
+ }
1599
+
1600
+ # Add MCP configuration to OpenCode config
1601
+ add_mcp_config() {
1602
+ local mcp_name="$1"
1603
+ local mcp_command="$2"
1604
+ local force="${3:-false}"
1605
+ local config_file="$HOME/.config/opencode/opencode.json"
1606
+
1607
+ # Check if already configured and warn user
1608
+ if [[ "$force" != "true" ]] && is_mcp_configured "$mcp_name"; then
1609
+ echo ""
1610
+ echo " ⚠️ WARNING: '$mcp_name' is already configured!"
1611
+ echo " Reconfiguring will overwrite your existing settings."
1612
+ echo " Any custom credentials or options will be lost."
1613
+ read -p " Are you sure you want to overwrite? [y/N]: " overwrite_choice
1614
+ if [[ ! "$overwrite_choice" =~ ^[Yy]$ ]]; then
1615
+ echo " ⏭️ Kept existing configuration"
1616
+ return 1
1617
+ fi
1618
+ fi
1619
+
1620
+ mkdir -p "$(dirname "$config_file")"
1621
+
1622
+ if [[ ! -f "$config_file" ]]; then
1623
+ # Create new config with MCP
1624
+ echo "{\"mcp\": {\"$mcp_name\": {\"type\": \"local\", \"command\": $mcp_command}}}" > "$config_file"
1625
+ elif has_jq; then
1626
+ # Add MCP to existing config
1627
+ jq --arg name "$mcp_name" --argjson cmd "$mcp_command" \
1628
+ '.mcp = (.mcp // {}) | .mcp[$name] = {"type": "local", "command": $cmd}' \
1629
+ "$config_file" > "$config_file.tmp" && mv "$config_file.tmp" "$config_file"
1630
+ else
1631
+ echo "⚠️ jq not found. Please manually add MCP configuration to $config_file"
1632
+ return 1
1633
+ fi
1634
+ chmod 600 "$config_file"
1635
+ }
1636
+
1637
+ # Mark MCP as skipped for this workspace (don't ask again)
1638
+ mark_mcp_skipped() {
1639
+ local skip_file="$SANDBOX_DIR/.mcp-skip-$WORKSPACE_MD5"
1640
+ date -Iseconds > "$skip_file" 2>/dev/null || date > "$skip_file"
1641
+ }
1642
+
1643
+ # Check if MCP prompt was skipped for this workspace
1644
+ is_mcp_skipped() {
1645
+ local skip_file="$SANDBOX_DIR/.mcp-skip-$WORKSPACE_MD5"
1646
+ [[ -f "$skip_file" ]]
1647
+ }
1648
+
1649
+ # Configure MCP tools for OpenCode
1650
+ configure_opencode_mcp() {
1651
+ # Only run for opencode tool in interactive mode
1652
+ [[ "$TOOL" != "opencode" ]] && return 0
1653
+ [[ ! -t 0 ]] && return 0
1654
+
1655
+ local config_file="$HOME/.config/opencode/opencode.json"
1656
+
1657
+ # Generate workspace hash for skip tracking
1658
+ WORKSPACE_MD5=$(echo "$CURRENT_DIR" | md5sum | cut -c1-8)
1659
+
1660
+ # Check if already skipped for this workspace
1661
+ if is_mcp_skipped; then
1662
+ return 0
1663
+ fi
1664
+
1665
+ # Detect installed MCP tools (from config.json, set during setup.sh)
1666
+ local chrome_installed=false
1667
+ local playwright_installed=false
1668
+ local chrome_configured=false
1669
+ local playwright_configured=false
1670
+
1671
+ if is_mcp_installed "chrome-devtools"; then
1672
+ chrome_installed=true
1673
+ is_mcp_configured "chrome-devtools" && chrome_configured=true
1674
+ fi
1675
+
1676
+ if is_mcp_installed "playwright"; then
1677
+ playwright_installed=true
1678
+ is_mcp_configured "playwright" && playwright_configured=true
1679
+ fi
1680
+
1681
+ # If no MCP tools installed in image, return
1682
+ if [[ "$chrome_installed" == "false" && "$playwright_installed" == "false" ]]; then
1683
+ return 0
1684
+ fi
1685
+
1686
+ # If all installed tools are already configured, return silently
1687
+ local all_configured=true
1688
+ [[ "$chrome_installed" == "true" && "$chrome_configured" == "false" ]] && all_configured=false
1689
+ [[ "$playwright_installed" == "true" && "$playwright_configured" == "false" ]] && all_configured=false
1690
+
1691
+ if [[ "$all_configured" == "true" ]]; then
1692
+ return 0
1693
+ fi
1694
+
1695
+ # Build lists of configured and unconfigured tools
1696
+ local unconfigured=()
1697
+ local configured=()
1698
+ [[ "$chrome_installed" == "true" && "$chrome_configured" == "false" ]] && unconfigured+=("chrome-devtools")
1699
+ [[ "$chrome_installed" == "true" && "$chrome_configured" == "true" ]] && configured+=("chrome-devtools")
1700
+ [[ "$playwright_installed" == "true" && "$playwright_configured" == "false" ]] && unconfigured+=("playwright")
1701
+ [[ "$playwright_installed" == "true" && "$playwright_configured" == "true" ]] && configured+=("playwright")
1702
+
1703
+ # Show prompt
1704
+ echo ""
1705
+ echo "🔌 MCP Tools Detected"
1706
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
1707
+
1708
+ # Show already configured tools
1709
+ if [[ ${#configured[@]} -gt 0 ]]; then
1710
+ echo "Already configured:"
1711
+ for tool in "${configured[@]}"; do
1712
+ case "$tool" in
1713
+ chrome-devtools)
1714
+ echo " ✓ Chrome DevTools MCP"
1715
+ ;;
1716
+ playwright)
1717
+ echo " ✓ Playwright MCP"
1718
+ ;;
1719
+ esac
1720
+ done
1721
+ echo ""
1722
+ fi
1723
+
1724
+ # Show unconfigured tools
1725
+ if [[ ${#unconfigured[@]} -gt 0 ]]; then
1726
+ echo "Not yet configured:"
1727
+ for tool in "${unconfigured[@]}"; do
1728
+ case "$tool" in
1729
+ chrome-devtools)
1730
+ echo " • Chrome DevTools MCP - browser automation + performance profiling"
1731
+ ;;
1732
+ playwright)
1733
+ echo " • Playwright MCP - multi-browser automation"
1734
+ ;;
1735
+ esac
1736
+ done
1737
+ echo ""
1738
+ fi
1739
+
1740
+ echo "Configure MCP tools in OpenCode?"
1741
+ echo " 1) Yes, configure all detected tools"
1742
+ echo " 2) Yes, let me choose which ones"
1743
+ echo " 3) No, skip (I'll configure manually)"
1744
+ echo " 4) No, don't ask again for this workspace"
1745
+ echo ""
1746
+ read -p "Choice [1-4]: " mcp_choice
1747
+
1748
+ local configured_any=false
1749
+
1750
+ case "$mcp_choice" in
1751
+ 1)
1752
+ # Configure all (both unconfigured and offer to reconfigure configured ones)
1753
+ local all_tools=()
1754
+ [[ "$chrome_installed" == "true" ]] && all_tools+=("chrome-devtools")
1755
+ [[ "$playwright_installed" == "true" ]] && all_tools+=("playwright")
1756
+
1757
+ for tool in "${all_tools[@]}"; do
1758
+ case "$tool" in
1759
+ chrome-devtools)
1760
+ if add_mcp_config "chrome-devtools" '["chrome-devtools-mcp", "--headless", "--isolated", "--executablePath", "/opt/chromium"]'; then
1761
+ echo " ✓ Configured Chrome DevTools MCP"
1762
+ configured_any=true
1763
+ fi
1764
+ ;;
1765
+ playwright)
1766
+ if add_mcp_config "playwright" '["npx", "@playwright/mcp@latest", "--headless", "--browser", "chromium"]'; then
1767
+ echo " ✓ Configured Playwright MCP"
1768
+ configured_any=true
1769
+ fi
1770
+ ;;
1771
+ esac
1772
+ done
1773
+ ;;
1774
+ 2)
1775
+ # Let user choose from all installed tools
1776
+ local all_tools=()
1777
+ [[ "$chrome_installed" == "true" ]] && all_tools+=("chrome-devtools")
1778
+ [[ "$playwright_installed" == "true" ]] && all_tools+=("playwright")
1779
+
1780
+ for tool in "${all_tools[@]}"; do
1781
+ local tool_desc=""
1782
+ local status_hint=""
1783
+ case "$tool" in
1784
+ chrome-devtools)
1785
+ tool_desc="Chrome DevTools MCP (browser automation + DevTools)"
1786
+ is_mcp_configured "chrome-devtools" && status_hint=" [already configured]"
1787
+ ;;
1788
+ playwright)
1789
+ tool_desc="Playwright MCP (multi-browser automation)"
1790
+ is_mcp_configured "playwright" && status_hint=" [already configured]"
1791
+ ;;
1792
+ esac
1793
+ read -p " Configure $tool_desc$status_hint? [y/N]: " tool_choice
1794
+ if [[ "$tool_choice" =~ ^[Yy]$ ]]; then
1795
+ case "$tool" in
1796
+ chrome-devtools)
1797
+ if add_mcp_config "chrome-devtools" '["chrome-devtools-mcp", "--headless", "--isolated", "--executablePath", "/opt/chromium"]'; then
1798
+ echo " ✓ Configured"
1799
+ configured_any=true
1800
+ fi
1801
+ ;;
1802
+ playwright)
1803
+ if add_mcp_config "playwright" '["npx", "@playwright/mcp@latest", "--headless", "--browser", "chromium"]'; then
1804
+ echo " ✓ Configured"
1805
+ configured_any=true
1806
+ fi
1807
+ ;;
1808
+ esac
1809
+ else
1810
+ echo " ⏭️ Skipped"
1811
+ fi
1812
+ done
1813
+ ;;
1814
+ 4)
1815
+ mark_mcp_skipped
1816
+ echo "ℹ️ Won't ask again for this workspace"
1817
+ ;;
1818
+ *)
1819
+ echo "ℹ️ Skipped."
1820
+ ;;
1821
+ esac
1822
+
1823
+ # Show config file path for easy editing
1824
+ echo ""
1825
+ if [[ "$configured_any" == "true" ]]; then
1826
+ echo "✅ MCP configuration saved!"
1827
+ fi
1828
+ echo "📄 Config file: $config_file"
1829
+ echo " Edit this file to customize MCP settings or add credentials."
1830
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
1831
+ echo ""
1832
+ }
1833
+
1834
+ # Run MCP configuration check for opencode
1835
+ configure_opencode_mcp
1836
+
1401
1837
  # ============================================================================
1402
1838
  # WEB COMMAND DETECTION AND PORT EXPOSURE
1403
1839
  # ============================================================================
@@ -1637,6 +2073,69 @@ fi
1637
2073
  # Detect display configuration (clipboard integration)
1638
2074
  DISPLAY_FLAGS=$(detect_display_config)
1639
2075
 
2076
+ # ============================================================================
2077
+ # OPENCLAW DOCKER-COMPOSE MODE
2078
+ # ============================================================================
2079
+ if [[ "$TOOL" == "openclaw" ]]; then
2080
+ OPENCLAW_REPO_DIR="$HOME/.ai-sandbox/tools/openclaw/repo"
2081
+
2082
+ if [[ ! -d "$OPENCLAW_REPO_DIR" ]]; then
2083
+ echo "❌ ERROR: OpenClaw repository not found at $OPENCLAW_REPO_DIR"
2084
+ echo " Run: npx @kokorolx/ai-sandbox-wrapper setup"
2085
+ exit 1
2086
+ fi
2087
+
2088
+ cd "$OPENCLAW_REPO_DIR"
2089
+
2090
+ OPENCLAW_COMPOSE_FILE="$OPENCLAW_REPO_DIR/docker-compose.yml"
2091
+ OPENCLAW_OVERRIDE_FILE="$OPENCLAW_REPO_DIR/docker-compose.override.yml"
2092
+
2093
+ echo "🔄 Generating OpenClaw docker-compose override..."
2094
+
2095
+ cat > "$OPENCLAW_OVERRIDE_FILE" <<EOF
2096
+ services:
2097
+ openclaw-gateway:
2098
+ environment:
2099
+ HOME: /home/node
2100
+ OPENCLAW_GATEWAY_TOKEN: \${OPENCLAW_GATEWAY_TOKEN:-}
2101
+ volumes:
2102
+ - $HOME/.openclaw:/home/node/.openclaw
2103
+ EOF
2104
+
2105
+ for workspace in "${WORKSPACES[@]}"; do
2106
+ echo " - $workspace:$workspace" >> "$OPENCLAW_OVERRIDE_FILE"
2107
+ done
2108
+
2109
+ cat >> "$OPENCLAW_OVERRIDE_FILE" <<EOF
2110
+ ports:
2111
+ - "18789:18789"
2112
+ - "18790:18790"
2113
+ working_dir: $CURRENT_DIR
2114
+ EOF
2115
+
2116
+ if [[ -n "$NETWORK_OPTIONS" ]]; then
2117
+ echo " networks:" >> "$OPENCLAW_OVERRIDE_FILE"
2118
+ for net in ${DOCKER_NETWORKS//,/ }; do
2119
+ echo " - $net" >> "$OPENCLAW_OVERRIDE_FILE"
2120
+ done
2121
+ echo "" >> "$OPENCLAW_OVERRIDE_FILE"
2122
+ echo "networks:" >> "$OPENCLAW_OVERRIDE_FILE"
2123
+ for net in ${DOCKER_NETWORKS//,/ }; do
2124
+ echo " $net:" >> "$OPENCLAW_OVERRIDE_FILE"
2125
+ echo " external: true" >> "$OPENCLAW_OVERRIDE_FILE"
2126
+ done
2127
+ fi
2128
+
2129
+ echo "🚀 Starting OpenClaw with docker-compose..."
2130
+ echo "🌐 Gateway: http://localhost:18789"
2131
+ echo "🌐 Bridge: http://localhost:18790"
2132
+ echo ""
2133
+
2134
+ exec docker compose -f "$OPENCLAW_COMPOSE_FILE" -f "$OPENCLAW_OVERRIDE_FILE" \
2135
+ --env-file "$ENV_FILE" \
2136
+ up --remove-orphans
2137
+ fi
2138
+
1640
2139
  docker run $CONTAINER_NAME --rm $TTY_FLAGS \
1641
2140
  --init \
1642
2141
  --platform "$PLATFORM" \