@kokorolx/ai-sandbox-wrapper 1.1.2 → 1.2.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/README.md CHANGED
@@ -252,6 +252,50 @@ echo '/path/to/project' >> ~/.ai-workspaces
252
252
  cat ~/.ai-workspaces
253
253
  ```
254
254
 
255
+ ### Network Configuration
256
+
257
+ AI containers can join Docker networks to communicate with other services (databases, APIs, MetaMCP).
258
+
259
+ #### Runtime Selection (Recommended)
260
+
261
+ ```bash
262
+ # Interactive network selection
263
+ ai-run opencode -n
264
+
265
+ # Direct network specification
266
+ ai-run opencode -n metamcp_metamcp-network
267
+ ai-run opencode -n network1,network2,network3
268
+ ```
269
+
270
+ #### Saved Configuration
271
+
272
+ Network selections are saved to `~/.ai-sandbox/config.json`:
273
+ - **Per-workspace**: Saved for specific project directories
274
+ - **Global**: Default for all workspaces
275
+
276
+ ```bash
277
+ # View current config
278
+ cat ~/.ai-sandbox/config.json
279
+
280
+ # Example config structure
281
+ {
282
+ "version": 1,
283
+ "networks": {
284
+ "global": ["shared-services"],
285
+ "workspaces": {
286
+ "/Users/you/projects/my-app": ["my-app_default", "redis_network"]
287
+ }
288
+ }
289
+ }
290
+ ```
291
+
292
+ #### Without `-n` Flag
293
+
294
+ When running without the flag, saved networks are used silently:
295
+ - Workspace-specific config takes priority
296
+ - Falls back to global config
297
+ - Non-existent networks are skipped silently
298
+
255
299
  ### Environment Variables
256
300
 
257
301
  All environment variables are configured in `~/.ai-env` or passed at runtime:
@@ -596,4 +640,4 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
596
640
 
597
641
  ## 📝 License
598
642
 
599
- MIT
643
+ MIT
package/bin/ai-run CHANGED
@@ -6,6 +6,8 @@ shift
6
6
 
7
7
  # Parse flags
8
8
  SHELL_MODE=false
9
+ NETWORK_FLAG=false
10
+ NETWORK_ARG=""
9
11
  TOOL_ARGS=()
10
12
 
11
13
  while [[ $# -gt 0 ]]; do
@@ -14,6 +16,15 @@ while [[ $# -gt 0 ]]; do
14
16
  SHELL_MODE=true
15
17
  shift
16
18
  ;;
19
+ --network|-n)
20
+ NETWORK_FLAG=true
21
+ shift
22
+ # Check if next arg is a network name (not another flag)
23
+ if [[ $# -gt 0 && ! "$1" =~ ^- ]]; then
24
+ NETWORK_ARG="$1"
25
+ shift
26
+ fi
27
+ ;;
17
28
  *)
18
29
  TOOL_ARGS+=("$1")
19
30
  shift
@@ -148,131 +159,418 @@ else
148
159
  esac
149
160
  fi
150
161
 
151
- # Git access control (opt-in per workspace)
152
- GIT_MOUNTS=""
153
- GIT_ALLOWED_FILE="$HOME/.ai-git-allowed"
154
- GIT_CACHE_DIR="$HOME/.ai-cache/git"
155
- touch "$GIT_ALLOWED_FILE"
162
+ # ============================================================================
163
+ # NETWORK CONFIGURATION
164
+ # ============================================================================
156
165
 
157
- # Network configuration for Docker network access
158
- NETWORK_FILE="$HOME/.ai-networks"
159
- NETWORK_MOUNTS=""
160
- NETWORK_OPTIONS=""
161
- HOST_ACCESS_ARGS=""
162
- METAMCP_JOINED=false
166
+ AI_SANDBOX_CONFIG="$HOME/.ai-sandbox/config.json"
163
167
 
164
- # Read configured networks (with deduplication)
165
- if [[ -f "$NETWORK_FILE" ]]; then
166
- while IFS= read -r network; do
167
- [ -z "$network" ] && continue
168
-
169
- # Skip if already added to NETWORK_OPTIONS (deduplication)
170
- if [[ "$NETWORK_OPTIONS" == *"--network $network"* ]]; then
171
- continue
168
+ # Ensure jq is available (fallback to basic parsing if not)
169
+ has_jq() {
170
+ command -v jq &>/dev/null
171
+ }
172
+
173
+ # Initialize config file if it doesn't exist
174
+ init_config() {
175
+ mkdir -p "$(dirname "$AI_SANDBOX_CONFIG")"
176
+ if [[ ! -f "$AI_SANDBOX_CONFIG" ]]; then
177
+ echo '{"version":1,"networks":{"global":[],"workspaces":{}}}' > "$AI_SANDBOX_CONFIG"
178
+ chmod 600 "$AI_SANDBOX_CONFIG"
179
+ fi
180
+ }
181
+
182
+ # Read networks for current workspace (workspace > global > empty)
183
+ read_network_config() {
184
+ init_config
185
+ local workspace="$CURRENT_DIR"
186
+
187
+ if has_jq; then
188
+ # Try workspace-specific first
189
+ local ws_networks=$(jq -r --arg ws "$workspace" '.networks.workspaces[$ws] // empty | .[]?' "$AI_SANDBOX_CONFIG" 2>/dev/null)
190
+ if [[ -n "$ws_networks" ]]; then
191
+ echo "$ws_networks"
192
+ return
172
193
  fi
194
+ # Fall back to global
195
+ jq -r '.networks.global[]?' "$AI_SANDBOX_CONFIG" 2>/dev/null
196
+ else
197
+ # Fallback: grep-based parsing (basic)
198
+ grep -o '"global":\s*\[[^]]*\]' "$AI_SANDBOX_CONFIG" 2>/dev/null | grep -o '"[^"]*"' | tr -d '"' | grep -v global
199
+ fi
200
+ }
201
+
202
+ # Write networks to config (scope: workspace or global)
203
+ write_network_config() {
204
+ local scope="$1" # "workspace" or "global"
205
+ shift
206
+ local networks=("$@")
207
+
208
+ init_config
209
+
210
+ if has_jq; then
211
+ local json_array=$(printf '%s\n' "${networks[@]}" | jq -R . | jq -s .)
173
212
 
174
- # Check if network exists
175
- if docker network inspect "$network" >/dev/null 2>&1; then
176
- NETWORK_OPTIONS="$NETWORK_OPTIONS --network $network"
177
-
178
- # Check if this network requires host access
179
- if [[ "$network" == *"metamcp"* ]]; then
180
- # Add host.docker.internal for host service access (Linux requires --add-host)
181
- # Docker Desktop on Mac/Windows has this by default
182
- HOST_ACCESS_ARGS="--add-host=host.docker.internal:host-gateway"
183
- METAMCP_JOINED=true
184
- fi
213
+ if [[ "$scope" == "workspace" ]]; then
214
+ jq --arg ws "$CURRENT_DIR" --argjson nets "$json_array" \
215
+ '.networks.workspaces[$ws] = $nets' "$AI_SANDBOX_CONFIG" > "$AI_SANDBOX_CONFIG.tmp" \
216
+ && mv "$AI_SANDBOX_CONFIG.tmp" "$AI_SANDBOX_CONFIG"
185
217
  else
186
- echo "âš ī¸ Network '$network' not found (may have been removed)"
218
+ jq --argjson nets "$json_array" \
219
+ '.networks.global = $nets' "$AI_SANDBOX_CONFIG" > "$AI_SANDBOX_CONFIG.tmp" \
220
+ && mv "$AI_SANDBOX_CONFIG.tmp" "$AI_SANDBOX_CONFIG"
187
221
  fi
188
- done < "$NETWORK_FILE"
189
- fi
222
+ chmod 600 "$AI_SANDBOX_CONFIG"
223
+ else
224
+ echo "âš ī¸ jq not found. Please install jq to save network configuration."
225
+ return 1
226
+ fi
227
+ }
190
228
 
191
- # Check if we should prompt about detected MetaMCP network
192
- # Only prompt if: network exists AND not already joined AND interactive mode
193
- if [[ "$METAMCP_JOINED" != "true" ]] && docker network inspect "metamcp_metamcp-network" >/dev/null 2>&1; then
194
- if [[ -t 0 ]]; then
195
- # Interactive arrow-key menu
196
- cursor=0
197
- options=("Join network (container-to-container)" "Use host.docker.internal (host access)")
229
+ # Validate networks exist, return only valid ones
230
+ validate_networks() {
231
+ local networks=("$@")
232
+ local valid=()
233
+
234
+ for net in "${networks[@]}"; do
235
+ [[ -z "$net" ]] && continue
236
+ if docker network inspect "$net" &>/dev/null; then
237
+ valid+=("$net")
238
+ fi
239
+ done
240
+
241
+ printf '%s\n' "${valid[@]}"
242
+ }
243
+
244
+ # Discover Docker Compose networks (have com.docker.compose.project label)
245
+ discover_compose_networks() {
246
+ docker network ls --filter "label=com.docker.compose.project" --format "{{.Name}}" 2>/dev/null | sort
247
+ }
248
+
249
+ # Discover custom networks (not system, not compose)
250
+ discover_custom_networks() {
251
+ local compose_nets=$(discover_compose_networks)
252
+ docker network ls --format "{{.Name}}" 2>/dev/null | while read -r net; do
253
+ # Skip system networks
254
+ [[ "$net" == "bridge" || "$net" == "host" || "$net" == "none" ]] && continue
255
+ # Skip compose networks
256
+ echo "$compose_nets" | grep -q "^${net}$" && continue
257
+ echo "$net"
258
+ done | sort
259
+ }
260
+
261
+ # Get containers in a network
262
+ get_network_containers() {
263
+ local network="$1"
264
+ docker network inspect "$network" --format '{{range .Containers}}{{.Name}} {{end}}' 2>/dev/null | xargs | tr ' ' ', '
265
+ }
266
+
267
+ # Interactive network selection menu (multi-select)
268
+ show_network_menu() {
269
+ local compose_nets=()
270
+ local custom_nets=()
271
+ local all_nets=()
272
+ local selected=()
273
+
274
+ echo "🔍 Discovering Docker networks..."
275
+ echo ""
276
+
277
+ # Gather networks
278
+ while IFS= read -r net; do
279
+ [[ -n "$net" ]] && compose_nets+=("$net")
280
+ done < <(discover_compose_networks)
281
+
282
+ while IFS= read -r net; do
283
+ [[ -n "$net" ]] && custom_nets+=("$net")
284
+ done < <(discover_custom_networks)
285
+
286
+ # Build combined list: select-all first, then compose, then custom, then "none"
287
+ # Index layout: [0]=select-all, [1..compose_count]=compose, [compose_count+1..custom_end]=custom, [last]=none
288
+ local network_count=$((${#compose_nets[@]} + ${#custom_nets[@]}))
289
+ all_nets=("select-all" "${compose_nets[@]}" "${custom_nets[@]}" "none")
290
+
291
+ if [[ $network_count -eq 0 ]]; then
292
+ echo "â„šī¸ No Docker networks found (besides system networks)."
293
+ SELECTED_NETWORKS=()
294
+ return 0
295
+ fi
296
+
297
+ # Initialize selection array (none is pre-selected)
298
+ local sel=()
299
+ local select_all_idx=0
300
+ local compose_start=1
301
+ local compose_end=$((1 + ${#compose_nets[@]}))
302
+ local custom_start=$compose_end
303
+ local none_idx=$((${#all_nets[@]} - 1))
304
+
305
+ for ((i=0; i<${#all_nets[@]}; i++)); do
306
+ if [[ "${all_nets[$i]}" == "none" ]]; then
307
+ sel[$i]=1
308
+ else
309
+ sel[$i]=0
310
+ fi
311
+ done
312
+
313
+ local cursor=0
314
+
315
+ tput civis
316
+ trap 'tput cnorm' INT TERM EXIT
317
+
318
+ while true; do
319
+ clear
320
+ echo "🔗 Network Selection"
321
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
322
+ echo ""
198
323
 
199
- tput civis
200
- trap 'tput cnorm; exit' INT TERM
324
+ # Select All option
325
+ local check="[ ]"
326
+ [[ ${sel[$select_all_idx]} -eq 1 ]] && check="[x]"
327
+ if [[ $cursor -eq $select_all_idx ]]; then
328
+ tput setaf 6
329
+ printf " ➔ %s Select All\n" "$check"
330
+ else
331
+ printf " %s Select All\n" "$check"
332
+ fi
333
+ tput sgr0
334
+ echo ""
201
335
 
202
- while true; do
203
- clear
204
- echo "🔗 MetaMCP Network Configuration"
205
- echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
206
- echo "A Docker network 'metamcp_metamcp-network' was detected."
207
- echo ""
208
- echo "Choose how AI tools should access MetaMCP:"
336
+ # Compose Networks section
337
+ if [[ ${#compose_nets[@]} -gt 0 ]]; then
338
+ echo "Compose Networks:"
339
+ for ((i=compose_start; i<compose_end; i++)); do
340
+ local net="${all_nets[$i]}"
341
+ local containers=$(get_network_containers "$net")
342
+ local check="[ ]"
343
+ [[ ${sel[$i]} -eq 1 ]] && check="[x]"
344
+
345
+ if [[ $i -eq $cursor ]]; then
346
+ tput setaf 6
347
+ printf " ➔ %s %-30s" "$check" "$net"
348
+ else
349
+ printf " %s %-30s" "$check" "$net"
350
+ fi
351
+
352
+ if [[ -n "$containers" ]]; then
353
+ tput setaf 8
354
+ printf " (%s)" "$containers"
355
+ fi
356
+ tput sgr0
357
+ echo ""
358
+ done
209
359
  echo ""
210
-
211
- for i in "${!options[@]}"; do
212
- if [ "$i" -eq "$cursor" ]; then
213
- prefix="➔ "
360
+ fi
361
+
362
+ # Custom Networks section
363
+ if [[ ${#custom_nets[@]} -gt 0 ]]; then
364
+ echo "Other Networks:"
365
+ for ((i=custom_start; i<none_idx; i++)); do
366
+ local net="${all_nets[$i]}"
367
+ local containers=$(get_network_containers "$net")
368
+ local check="[ ]"
369
+ [[ ${sel[$i]} -eq 1 ]] && check="[x]"
370
+
371
+ if [[ $i -eq $cursor ]]; then
214
372
  tput setaf 6
373
+ printf " ➔ %s %-30s" "$check" "$net"
215
374
  else
216
- prefix=" "
375
+ printf " %s %-30s" "$check" "$net"
217
376
  fi
218
377
 
219
- printf "%s %s\n" "$prefix" "${options[$i]}"
378
+ if [[ -n "$containers" ]]; then
379
+ tput setaf 8
380
+ printf " (%s)" "$containers"
381
+ fi
220
382
  tput sgr0
383
+ echo ""
221
384
  done
222
-
223
385
  echo ""
224
- echo "Use ARROWS to move, ENTER to select"
225
-
226
- IFS= read -rsn1 key
227
- if [[ "$key" == $'\x1b' ]]; then
228
- read -rsn1 -t 1 next1
229
- read -rsn1 -t 1 next2
230
- case "$next1$next2" in
231
- '[A') ((cursor--)) ;;
232
- '[B') ((cursor++)) ;;
233
- esac
386
+ fi
387
+
388
+ # None option
389
+ echo ""
390
+ local check="[ ]"
391
+ [[ ${sel[$none_idx]} -eq 1 ]] && check="[x]"
392
+ if [[ $cursor -eq $none_idx ]]; then
393
+ tput setaf 6
394
+ printf " ➔ %s None (no network)\n" "$check"
395
+ else
396
+ printf " %s None (no network)\n" "$check"
397
+ fi
398
+ tput sgr0
399
+
400
+ echo ""
401
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
402
+ echo "↑↓ Move SPACE Select a Select All ENTER Confirm"
403
+
404
+ # Read input
405
+ IFS= read -rsn1 key
406
+ if [[ "$key" == $'\x1b' ]]; then
407
+ read -rsn2 -t 1 escape_seq
408
+ case "$escape_seq" in
409
+ '[A') ((cursor > 0)) && ((cursor--)) ;;
410
+ '[B') ((cursor < ${#all_nets[@]} - 1)) && ((cursor++)) ;;
411
+ esac
412
+ else
413
+ case "$key" in
414
+ ' ')
415
+ # Toggle selection
416
+ if [[ $cursor -eq $select_all_idx ]]; then
417
+ # Toggle select all
418
+ if [[ ${sel[$select_all_idx]} -eq 1 ]]; then
419
+ # Deselect all, select none
420
+ for ((i=0; i<${#all_nets[@]}; i++)); do sel[$i]=0; done
421
+ sel[$none_idx]=1
422
+ else
423
+ # Select all networks, deselect none
424
+ for ((i=0; i<${#all_nets[@]}; i++)); do sel[$i]=1; done
425
+ sel[$none_idx]=0
426
+ fi
427
+ elif [[ $cursor -eq $none_idx ]]; then
428
+ # Selecting "none" clears all others
429
+ for ((i=0; i<${#all_nets[@]}; i++)); do sel[$i]=0; done
430
+ sel[$none_idx]=1
431
+ else
432
+ # Selecting a network clears "none" and updates select-all state
433
+ sel[$none_idx]=0
434
+ if [[ ${sel[$cursor]} -eq 1 ]]; then
435
+ sel[$cursor]=0
436
+ sel[$select_all_idx]=0
437
+ else
438
+ sel[$cursor]=1
439
+ # Check if all networks are now selected
440
+ local all_selected=1
441
+ for ((i=compose_start; i<none_idx; i++)); do
442
+ [[ ${sel[$i]} -eq 0 ]] && all_selected=0 && break
443
+ done
444
+ sel[$select_all_idx]=$all_selected
445
+ fi
446
+ fi
447
+ ;;
448
+ 'a'|'A')
449
+ # Quick select all
450
+ for ((i=0; i<${#all_nets[@]}; i++)); do sel[$i]=1; done
451
+ sel[$none_idx]=0
452
+ ;;
453
+ ''|$'\n'|$'\r')
454
+ break
455
+ ;;
456
+ k) ((cursor > 0)) && ((cursor--)) ;;
457
+ j) ((cursor < ${#all_nets[@]} - 1)) && ((cursor++)) ;;
458
+ esac
459
+ fi
460
+ done
461
+
462
+ tput cnorm
463
+ trap - INT TERM EXIT
464
+
465
+ # Collect selected networks (exclude select-all and none)
466
+ SELECTED_NETWORKS=()
467
+ for ((i=1; i<${#all_nets[@]}; i++)); do
468
+ if [[ ${sel[$i]} -eq 1 && "${all_nets[$i]}" != "none" && "${all_nets[$i]}" != "select-all" ]]; then
469
+ SELECTED_NETWORKS+=("${all_nets[$i]}")
470
+ fi
471
+ done
472
+ }
473
+
474
+ # Save prompt after selection
475
+ show_save_prompt() {
476
+ local options=("This workspace" "Global (all workspaces)" "Don't save")
477
+ local cursor=0
478
+
479
+ echo ""
480
+
481
+ tput civis
482
+ trap 'tput cnorm' INT TERM EXIT
483
+
484
+ while true; do
485
+ # Move cursor up to redraw (3 options + header)
486
+ tput cuu 5 2>/dev/null || true
487
+ tput ed 2>/dev/null || true
488
+
489
+ echo "Save selection?"
490
+ for ((i=0; i<${#options[@]}; i++)); do
491
+ if [[ $i -eq $cursor ]]; then
492
+ tput setaf 6
493
+ if [[ $i -eq 0 ]]; then
494
+ printf " ➔ %s (%s)\n" "${options[$i]}" "$CURRENT_DIR"
495
+ else
496
+ printf " ➔ %s\n" "${options[$i]}"
497
+ fi
234
498
  else
235
- case "$key" in
236
- k) ((cursor--)) ;;
237
- j) ((cursor++)) ;;
238
- "") break ;;
239
- $'\n'|$'\r') break ;;
240
- esac
499
+ if [[ $i -eq 0 ]]; then
500
+ printf " %s (%s)\n" "${options[$i]}" "$CURRENT_DIR"
501
+ else
502
+ printf " %s\n" "${options[$i]}"
503
+ fi
241
504
  fi
242
-
243
- if [ "$cursor" -lt 0 ]; then cursor=$((${#options[@]} - 1)); fi
244
- if [ "$cursor" -ge "${#options[@]}" ]; then cursor=0; fi
505
+ tput sgr0
245
506
  done
246
507
 
247
- tput cnorm
248
-
249
- if [ "$cursor" -eq 0 ]; then
250
- # Join network - but only if not already in file
251
- if ! grep -q "^metamcp_metamcp-network$" "$NETWORK_FILE" 2>/dev/null; then
252
- echo "metamcp_metamcp-network" >> "$NETWORK_FILE"
253
- fi
254
- NETWORK_OPTIONS="$NETWORK_OPTIONS --network metamcp_metamcp-network"
255
- HOST_ACCESS_ARGS="--add-host=host.docker.internal:host-gateway"
256
- METAMCP_JOINED=true
257
- echo ""
258
- echo "✅ Network joined. Both host.docker.internal and MetaMCP network enabled."
508
+ IFS= read -rsn1 key
509
+ if [[ "$key" == $'\x1b' ]]; then
510
+ read -rsn2 -t 1 escape_seq
511
+ case "$escape_seq" in
512
+ '[A') ((cursor > 0)) && ((cursor--)) ;;
513
+ '[B') ((cursor < 2)) && ((cursor++)) ;;
514
+ esac
259
515
  else
260
- # Use host.docker.internal
261
- HOST_ACCESS_ARGS="--add-host=host.docker.internal:host-gateway"
262
- echo ""
263
- echo "â„šī¸ Using host.docker.internal only. MetaMCP accessible at localhost:12008 on host."
516
+ case "$key" in
517
+ ''|$'\n'|$'\r') break ;;
518
+ k) ((cursor > 0)) && ((cursor--)) ;;
519
+ j) ((cursor < 2)) && ((cursor++)) ;;
520
+ esac
521
+ fi
522
+ done
523
+
524
+ tput cnorm
525
+ trap - INT TERM EXIT
526
+
527
+ SAVE_CHOICE=$cursor # 0=workspace, 1=global, 2=don't save
528
+ }
529
+
530
+ # Network configuration
531
+ NETWORK_OPTIONS=""
532
+ HOST_ACCESS_ARGS="--add-host=host.docker.internal:host-gateway"
533
+ SELECTED_NETWORKS=()
534
+
535
+ if [[ "$NETWORK_FLAG" == "true" ]]; then
536
+ if [[ -n "$NETWORK_ARG" ]]; then
537
+ # Direct specification: -n net1,net2
538
+ IFS=',' read -ra SELECTED_NETWORKS <<< "$NETWORK_ARG"
539
+ elif [[ -t 0 ]]; then
540
+ # Interactive mode: show menu
541
+ show_network_menu
542
+
543
+ if [[ ${#SELECTED_NETWORKS[@]} -gt 0 ]]; then
544
+ show_save_prompt
545
+ case $SAVE_CHOICE in
546
+ 0) write_network_config "workspace" "${SELECTED_NETWORKS[@]}" && echo "✅ Saved for this workspace" ;;
547
+ 1) write_network_config "global" "${SELECTED_NETWORKS[@]}" && echo "✅ Saved globally" ;;
548
+ *) echo "â„šī¸ Using for this session only" ;;
549
+ esac
264
550
  fi
265
- echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
266
- echo ""
267
551
  else
268
- # Non-interactive: just use host.docker.internal
269
- HOST_ACCESS_ARGS="--add-host=host.docker.internal:host-gateway"
552
+ echo "âš ī¸ Non-interactive mode. Use -n network1,network2 to specify networks."
270
553
  fi
271
- elif [[ "$METAMCP_JOINED" != "true" ]]; then
272
- # No network, but ensure host.docker.internal is available
273
- HOST_ACCESS_ARGS="--add-host=host.docker.internal:host-gateway"
554
+ else
555
+ # No flag: use saved config silently
556
+ while IFS= read -r net; do
557
+ [[ -n "$net" ]] && SELECTED_NETWORKS+=("$net")
558
+ done < <(read_network_config)
274
559
  fi
275
560
 
561
+ # Validate and build network options
562
+ if [[ ${#SELECTED_NETWORKS[@]} -gt 0 ]]; then
563
+ while IFS= read -r net; do
564
+ [[ -n "$net" ]] && NETWORK_OPTIONS="$NETWORK_OPTIONS --network $net"
565
+ done < <(validate_networks "${SELECTED_NETWORKS[@]}")
566
+ fi
567
+
568
+ # Git access control (opt-in per workspace)
569
+ GIT_MOUNTS=""
570
+ GIT_ALLOWED_FILE="$HOME/.ai-git-allowed"
571
+ GIT_CACHE_DIR="$HOME/.ai-cache/git"
572
+ touch "$GIT_ALLOWED_FILE"
573
+
276
574
  # Check if Git access is allowed for this workspace
277
575
  if grep -q "^$CURRENT_DIR$" "$GIT_ALLOWED_FILE" 2>/dev/null; then
278
576
  # Previously allowed for this workspace
@@ -26,11 +26,13 @@ RUN curl -fsSL https://deb.nodesource.com/setup_lts.x | bash - \
26
26
  # Install pnpm globally via npm
27
27
  RUN npm install -g pnpm
28
28
 
29
+ # Install TypeScript and LSP tools for AI coding assistants
30
+ RUN npm install -g typescript typescript-language-server
31
+
29
32
  # Verify installations
30
- RUN node --version && npm --version && pnpm --version && bun --version
33
+ RUN node --version && npm --version && pnpm --version && bun --version && tsc --version
31
34
 
32
35
  # Install additional tools (if selected)
33
- RUN pipx install specify-cli --pip-args="git+https://github.com/github/spec-kit.git"
34
36
  RUN bun install -g uipro-cli
35
37
  RUN mkdir -p /usr/local/lib/openspec && \
36
38
  cd /usr/local/lib/openspec && \
@@ -62,6 +64,36 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
62
64
  && rm -rf /var/lib/apt/lists/*
63
65
  # Install Playwright and browsers via npm (avoids pnpm global bin issues)
64
66
  RUN npm install -g playwright && npx playwright install
67
+ RUN apt-get update && apt-get install -y --no-install-recommends \
68
+ libssl-dev \
69
+ libreadline-dev \
70
+ zlib1g-dev \
71
+ libyaml-dev \
72
+ libffi-dev \
73
+ libgdbm-dev \
74
+ libncurses5-dev \
75
+ libpq-dev \
76
+ default-libmysqlclient-dev \
77
+ libsqlite3-dev \
78
+ imagemagick \
79
+ libmagickwand-dev \
80
+ libvips-dev \
81
+ autoconf \
82
+ bison \
83
+ rustc \
84
+ libxml2-dev \
85
+ libxslt1-dev \
86
+ && rm -rf /var/lib/apt/lists/*
87
+
88
+ RUN git clone https://github.com/rbenv/rbenv.git /usr/local/rbenv && \
89
+ git clone https://github.com/rbenv/ruby-build.git /usr/local/rbenv/plugins/ruby-build
90
+
91
+ ENV RBENV_ROOT=/usr/local/rbenv
92
+ ENV PATH=$RBENV_ROOT/bin:$RBENV_ROOT/shims:$PATH
93
+
94
+ RUN rbenv install 3.3.0 && rbenv global 3.3.0 && rbenv rehash
95
+
96
+ RUN gem install rails -v 8.0.2 && gem install bundler && rbenv rehash
65
97
 
66
98
  # Create workspace
67
99
  WORKDIR /workspace
@@ -60,8 +60,7 @@ fi
60
60
 
61
61
  if [[ "${INSTALL_RUBY:-0}" -eq 1 ]]; then
62
62
  echo "đŸ“Ļ Ruby 3.3.0 + Rails 8.0.2 will be installed in base image"
63
- ADDITIONAL_TOOLS_INSTALL+='# Install Ruby build dependencies
64
- RUN apt-get update && apt-get install -y --no-install-recommends \
63
+ ADDITIONAL_TOOLS_INSTALL+='RUN apt-get update && apt-get install -y --no-install-recommends \
65
64
  libssl-dev \
66
65
  libreadline-dev \
67
66
  zlib1g-dev \
@@ -69,19 +68,27 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
69
68
  libffi-dev \
70
69
  libgdbm-dev \
71
70
  libncurses5-dev \
71
+ libpq-dev \
72
+ default-libmysqlclient-dev \
73
+ libsqlite3-dev \
74
+ imagemagick \
75
+ libmagickwand-dev \
76
+ libvips-dev \
77
+ autoconf \
78
+ bison \
79
+ rustc \
80
+ libxml2-dev \
81
+ libxslt1-dev \
72
82
  && rm -rf /var/lib/apt/lists/*
73
83
 
74
- # Install rbenv and ruby-build
75
84
  RUN git clone https://github.com/rbenv/rbenv.git /usr/local/rbenv && \
76
85
  git clone https://github.com/rbenv/ruby-build.git /usr/local/rbenv/plugins/ruby-build
77
86
 
78
87
  ENV RBENV_ROOT=/usr/local/rbenv
79
88
  ENV PATH=$RBENV_ROOT/bin:$RBENV_ROOT/shims:$PATH
80
89
 
81
- # Install Ruby 3.3.0
82
90
  RUN rbenv install 3.3.0 && rbenv global 3.3.0 && rbenv rehash
83
91
 
84
- # Install Rails 8.0.2 and Bundler
85
92
  RUN gem install rails -v 8.0.2 && gem install bundler && rbenv rehash
86
93
  '
87
94
  fi
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kokorolx/ai-sandbox-wrapper",
3
- "version": "1.1.2",
3
+ "version": "1.2.1",
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",
package/setup.sh CHANGED
@@ -231,66 +231,6 @@ echo "📁 Whitelisted workspaces saved to: $WORKSPACES_FILE"
231
231
  # Use first workspace as default for backwards compatibility
232
232
  WORKSPACE="${WORKSPACES[0]}"
233
233
 
234
- # Network configuration for Docker network access
235
- echo ""
236
- echo "🔗 Docker Network Configuration"
237
- echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
238
- echo "You can configure AI tools to join existing Docker networks"
239
- echo "(e.g., for MetaMCP services, databases, or other containers)."
240
- echo ""
241
-
242
- # Check for existing MetaMCP network
243
- if docker network inspect "metamcp_metamcp-network" >/dev/null 2>&1; then
244
- echo "✅ Found existing network: metamcp_metamcp-network"
245
- echo ""
246
-
247
- # Use single_select for interactive choice
248
- single_select "MetaMCP Access Method" "join,host-only" "Join network (container-to-container communication),Use host.docker.internal (host access)"
249
-
250
- case "$SELECTED_ITEM" in
251
- join)
252
- NETWORK_FILE="$HOME/.ai-networks"
253
- echo "metamcp_metamcp-network" >> "$NETWORK_FILE"
254
- chmod 600 "$NETWORK_FILE"
255
- echo ""
256
- echo "✅ Network joined. Both host.docker.internal and MetaMCP network enabled."
257
- ;;
258
- host-only|"")
259
- echo ""
260
- echo "â„šī¸ Using host.docker.internal only. MetaMCP accessible at localhost:12008 on host."
261
- ;;
262
- esac
263
- else
264
- echo "No existing MetaMCP network detected."
265
- echo ""
266
- echo "You have two options:"
267
- echo ""
268
- echo "Option 1: Use host.docker.internal (recommended for most cases)"
269
- echo " MetaMCP at localhost:12008 on your host machine"
270
- echo " Already enabled by default"
271
- echo ""
272
- echo "Option 2: Join a Docker network"
273
- echo " For container-to-container communication"
274
- echo ""
275
- read -p "Enter a Docker network name to join (leave empty to skip): " network_name
276
-
277
- if [[ -n "$network_name" ]]; then
278
- if docker network inspect "$network_name" >/dev/null 2>&1; then
279
- NETWORK_FILE="$HOME/.ai-networks"
280
- echo "$network_name" >> "$NETWORK_FILE"
281
- chmod 600 "$NETWORK_FILE"
282
- echo "✅ Network '$network_name' saved"
283
- else
284
- echo "âš ī¸ Network '$network_name' not found. Skipping."
285
- fi
286
- else
287
- echo "â„šī¸ Skipped. host.docker.internal is enabled for host access."
288
- fi
289
- fi
290
-
291
- echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
292
- echo ""
293
-
294
234
  # Tool definitions
295
235
  TOOL_OPTIONS="amp,opencode,droid,claude,gemini,kilo,qwen,codex,qoder,auggie,codebuddy,jules,shai,vscode,codeserver"
296
236
  TOOL_DESCS="AI coding assistant from @sourcegraph/amp,Open-source coding tool from opencode-ai,Factory CLI from factory.ai,Claude Code CLI from Anthropic,Google Gemini CLI (free tier),AI pair programmer (Git-native),Kilo Code (500+ models),Alibaba Qwen CLI (1M context),OpenAI Codex terminal agent,Qoder AI CLI assistant,Augment Auggie CLI,Tencent CodeBuddy CLI,Google Jules CLI,OVHcloud SHAI agent,VSCode Desktop in Docker (X11),VSCode in browser (fast)"
package/bin/ai-network DELETED
@@ -1,144 +0,0 @@
1
- #!/usr/bin/env bash
2
- # Network management helper for AI Sandbox Wrapper
3
- # Usage: ai-network <command> [network-name]
4
- #
5
- # Commands:
6
- # list List configured networks
7
- # add <name> Add a network to join
8
- # remove <name> Remove a network
9
- # check <name> Check if network exists
10
- # metamcp Quick command to add MetaMCP network
11
-
12
- set -e
13
-
14
- NETWORK_FILE="$HOME/.ai-networks"
15
- COMMAND="${1:-list}"
16
-
17
- # Ensure network file exists
18
- touch "$NETWORK_FILE"
19
-
20
- list_networks() {
21
- echo "📋 Configured Networks:"
22
- echo ""
23
- if [[ -s "$NETWORK_FILE" ]]; then
24
- while IFS= read -r network; do
25
- if [[ -n "$network" ]]; then
26
- if docker network inspect "$network" >/dev/null 2>&1; then
27
- echo " ✅ $network"
28
- else
29
- echo " ❌ $network (not found)"
30
- fi
31
- fi
32
- done < "$NETWORK_FILE"
33
- else
34
- echo " No networks configured."
35
- echo ""
36
- echo " To add a network:"
37
- echo " ai-network add <network-name>"
38
- fi
39
- }
40
-
41
- add_network() {
42
- local network_name="$2"
43
-
44
- if [[ -z "$network_name" ]]; then
45
- echo "❌ Error: Network name required"
46
- echo " Usage: ai-network add <network-name>"
47
- exit 1
48
- fi
49
-
50
- # Check if network exists
51
- if ! docker network inspect "$network_name" >/dev/null 2>&1; then
52
- echo "âš ī¸ Network '$network_name' does not exist yet."
53
- read -p "Add it anyway? [y/N]: " confirm
54
- if [[ ! "$confirm" =~ ^[Yy]$ ]]; then
55
- exit 0
56
- fi
57
- fi
58
-
59
- # Add to config (avoid duplicates)
60
- if grep -q "^${network_name}$" "$NETWORK_FILE" 2>/dev/null; then
61
- echo "✅ Network '$network_name' already configured"
62
- else
63
- echo "$network_name" >> "$NETWORK_FILE"
64
- chmod 600 "$NETWORK_FILE"
65
- echo "✅ Added network: $network_name"
66
- fi
67
- }
68
-
69
- remove_network() {
70
- local network_name="$2"
71
-
72
- if [[ -z "$network_name" ]]; then
73
- echo "❌ Error: Network name required"
74
- echo " Usage: ai-network remove <network-name>"
75
- exit 1
76
- fi
77
-
78
- if grep -q "^${network_name}$" "$NETWORK_FILE" 2>/dev/null; then
79
- # Remove the network from file
80
- grep -v "^${network_name}$" "$NETWORK_FILE" > "$NETWORK_FILE.tmp"
81
- mv "$NETWORK_FILE.tmp" "$NETWORK_FILE"
82
- chmod 600 "$NETWORK_FILE"
83
- echo "✅ Removed network: $network_name"
84
- else
85
- echo "âš ī¸ Network '$network_name' not found in config"
86
- fi
87
- }
88
-
89
- check_network() {
90
- local network_name="${2:-metamcp_metamcp-network}"
91
-
92
- if docker network inspect "$network_name" >/dev/null 2>&1; then
93
- echo "✅ Network '$network_name' exists"
94
- return 0
95
- else
96
- echo "❌ Network '$network_name' not found"
97
- return 1
98
- fi
99
- }
100
-
101
- add_metamcp() {
102
- echo "🔗 Adding MetaMCP network..."
103
- add_network "metamcp_metamcp-network"
104
- }
105
-
106
- case "$COMMAND" in
107
- list)
108
- list_networks
109
- ;;
110
- add)
111
- add_network "$@"
112
- ;;
113
- remove|rm|delete)
114
- remove_network "$@"
115
- ;;
116
- check)
117
- check_network "$@"
118
- ;;
119
- metamcp|meta)
120
- add_metamcp
121
- ;;
122
- help|--help|-h)
123
- echo "AI Network Manager"
124
- echo ""
125
- echo "Usage: ai-network <command> [network-name]"
126
- echo ""
127
- echo "Commands:"
128
- echo " list Show all configured networks"
129
- echo " add <name> Add a Docker network"
130
- echo " remove <name> Remove a configured network"
131
- echo " check [name] Check if a network exists"
132
- echo " metamcp Quick-add MetaMCP network"
133
- echo ""
134
- echo "Examples:"
135
- echo " ai-network list"
136
- echo " ai-network add my-network"
137
- echo " ai-network metamcp"
138
- ;;
139
- *)
140
- echo "Unknown command: $COMMAND"
141
- echo "Run 'ai-network help' for usage"
142
- exit 1
143
- ;;
144
- esac