@kokorolx/ai-sandbox-wrapper 1.0.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.
Files changed (45) hide show
  1. package/README.md +540 -0
  2. package/bin/ai-debug +116 -0
  3. package/bin/ai-network +144 -0
  4. package/bin/ai-run +631 -0
  5. package/bin/cli.js +83 -0
  6. package/bin/setup-ssh-config +328 -0
  7. package/dockerfiles/AGENTS.md +92 -0
  8. package/dockerfiles/aider/Dockerfile +5 -0
  9. package/dockerfiles/amp/Dockerfile +10 -0
  10. package/dockerfiles/auggie/Dockerfile +12 -0
  11. package/dockerfiles/base/Dockerfile +73 -0
  12. package/dockerfiles/claude/Dockerfile +11 -0
  13. package/dockerfiles/codebuddy/Dockerfile +12 -0
  14. package/dockerfiles/codex/Dockerfile +9 -0
  15. package/dockerfiles/droid/Dockerfile +8 -0
  16. package/dockerfiles/gemini/Dockerfile +9 -0
  17. package/dockerfiles/jules/Dockerfile +12 -0
  18. package/dockerfiles/kilo/Dockerfile +25 -0
  19. package/dockerfiles/opencode/Dockerfile +10 -0
  20. package/dockerfiles/qoder/Dockerfile +12 -0
  21. package/dockerfiles/qwen/Dockerfile +10 -0
  22. package/dockerfiles/shai/Dockerfile +9 -0
  23. package/lib/AGENTS.md +58 -0
  24. package/lib/generate-ai-run.sh +19 -0
  25. package/lib/install-aider.sh +30 -0
  26. package/lib/install-amp.sh +39 -0
  27. package/lib/install-auggie.sh +36 -0
  28. package/lib/install-base.sh +139 -0
  29. package/lib/install-claude.sh +42 -0
  30. package/lib/install-codebuddy.sh +36 -0
  31. package/lib/install-codeserver.sh +171 -0
  32. package/lib/install-codex.sh +40 -0
  33. package/lib/install-droid.sh +27 -0
  34. package/lib/install-gemini.sh +39 -0
  35. package/lib/install-jules.sh +36 -0
  36. package/lib/install-kilo.sh +57 -0
  37. package/lib/install-opencode.sh +39 -0
  38. package/lib/install-qoder.sh +37 -0
  39. package/lib/install-qwen.sh +40 -0
  40. package/lib/install-shai.sh +33 -0
  41. package/lib/install-tool.sh +40 -0
  42. package/lib/install-vscode.sh +219 -0
  43. package/lib/ssh-key-selector.sh +189 -0
  44. package/package.json +46 -0
  45. package/setup.sh +530 -0
package/bin/ai-run ADDED
@@ -0,0 +1,631 @@
1
+ #!/usr/bin/env bash
2
+ set -e
3
+
4
+ TOOL="$1"
5
+ shift
6
+
7
+ # Parse flags
8
+ SHELL_MODE=false
9
+ TOOL_ARGS=()
10
+
11
+ while [[ $# -gt 0 ]]; do
12
+ case "$1" in
13
+ --shell|-s)
14
+ SHELL_MODE=true
15
+ shift
16
+ ;;
17
+ *)
18
+ TOOL_ARGS+=("$1")
19
+ shift
20
+ ;;
21
+ esac
22
+ done
23
+
24
+ WORKSPACES_FILE="$HOME/.ai-workspaces"
25
+ CURRENT_DIR="$(pwd)"
26
+ ENV_FILE="$HOME/.ai-env"
27
+
28
+ # Check if workspaces file exists
29
+ if [[ ! -f "$WORKSPACES_FILE" ]]; then
30
+ echo "❌ Workspaces not configured. Run setup.sh first."
31
+ exit 1
32
+ fi
33
+
34
+ # Check if current directory is inside any whitelisted workspace
35
+ ALLOWED=false
36
+ while IFS= read -r ws; do
37
+ if [[ "$CURRENT_DIR" == "$ws"* ]]; then
38
+ ALLOWED=true
39
+ break
40
+ fi
41
+ done < "$WORKSPACES_FILE"
42
+
43
+ if [[ "$ALLOWED" != "true" ]]; then
44
+ echo "⚠️ SECURITY WARNING: You are running $TOOL outside a whitelisted workspace."
45
+ echo " Current path: $CURRENT_DIR"
46
+ echo ""
47
+ echo "Allowing this path gives the AI container access to this folder."
48
+
49
+ # Only prompt if running interactively (TTY attached)
50
+ if [[ -t 0 ]]; then
51
+ read -p "Do you want to whitelist the current directory? [y/N]: " CONFIRM
52
+ if [[ "$CONFIRM" =~ ^[Yy]$ ]]; then
53
+ echo "$CURRENT_DIR" >> "$WORKSPACES_FILE"
54
+ echo "✅ Added $CURRENT_DIR to $WORKSPACES_FILE"
55
+ else
56
+ echo "❌ Operation cancelled. Access denied."
57
+ echo "📁 Allowed workspaces:"
58
+ cat "$WORKSPACES_FILE"
59
+ exit 1
60
+ fi
61
+ else
62
+ # Non-interactive: fail securely
63
+ echo "❌ Access denied (non-interactive mode). Please add this path manually:"
64
+ echo " echo '$CURRENT_DIR' >> $WORKSPACES_FILE"
65
+ echo "📁 Allowed workspaces:"
66
+ cat "$WORKSPACES_FILE"
67
+ exit 1
68
+ fi
69
+ fi
70
+
71
+ # Image source selection: local or registry (GitLab)
72
+ # Set AI_IMAGE_SOURCE=registry in ~/.ai-env to use pre-built images
73
+ AI_IMAGE_SOURCE="${AI_IMAGE_SOURCE:-local}"
74
+
75
+ if [[ "$AI_IMAGE_SOURCE" == "registry" ]]; then
76
+ IMAGE="registry.gitlab.com/kokorolee/ai-sandbox-wrapper/ai-${TOOL}:latest"
77
+ else
78
+ IMAGE="ai-${TOOL}:latest"
79
+ fi
80
+
81
+ CACHE_DIR="$HOME/.ai-cache/$TOOL"
82
+ HOME_DIR="$HOME/.ai-home/$TOOL"
83
+
84
+ mkdir -p "$CACHE_DIR" "$HOME_DIR"
85
+
86
+ # Build volume mounts for all whitelisted workspaces
87
+ VOLUME_MOUNTS=""
88
+ while IFS= read -r ws; do
89
+ VOLUME_MOUNTS="$VOLUME_MOUNTS -v $ws:$ws:delegated"
90
+ done < "$WORKSPACES_FILE"
91
+
92
+ # Tool-specific config mounts (project-level takes precedence over global)
93
+ CONFIG_MOUNT=""
94
+ PROJECT_CONFIG="$CURRENT_DIR/.$TOOL.json"
95
+
96
+ if [[ -f "$PROJECT_CONFIG" ]]; then
97
+ # Use project-level config if it exists
98
+ CONFIG_MOUNT="-v $PROJECT_CONFIG:$CURRENT_DIR/.$TOOL.json:delegated"
99
+ else
100
+ # Use global configs based on tool
101
+ case "$TOOL" in
102
+ amp)
103
+ CONFIG_DIR="$HOME/.config/amp"
104
+ DATA_DIR="$HOME/.local/share/amp"
105
+ mkdir -p "$CONFIG_DIR" "$DATA_DIR"
106
+ CONFIG_MOUNT="-v $CONFIG_DIR:/home/agent/.config/amp:delegated -v $DATA_DIR:/home/agent/.local/share/amp:delegated"
107
+ ;;
108
+ opencode)
109
+ CONFIG_DIR="$HOME/.config/opencode"
110
+ mkdir -p "$CONFIG_DIR"
111
+ CONFIG_MOUNT="-v $CONFIG_DIR:/home/agent/.config/opencode:delegated"
112
+ ;;
113
+ claude)
114
+ CONFIG_DIR="$HOME/.claude"
115
+ mkdir -p "$CONFIG_DIR"
116
+ CONFIG_MOUNT="-v $CONFIG_DIR:/home/agent/.claude:delegated"
117
+ ;;
118
+ droid)
119
+ CONFIG_DIR="$HOME/.config/droid"
120
+ mkdir -p "$CONFIG_DIR"
121
+ CONFIG_MOUNT="-v $CONFIG_DIR:/home/agent/.config/droid:delegated"
122
+ ;;
123
+ qoder)
124
+ CONFIG_DIR="$HOME/.config/qoder"
125
+ mkdir -p "$CONFIG_DIR"
126
+ CONFIG_MOUNT="-v $CONFIG_DIR:/home/agent/.config/qoder:delegated"
127
+ ;;
128
+ auggie)
129
+ CONFIG_DIR="$HOME/.config/auggie"
130
+ mkdir -p "$CONFIG_DIR"
131
+ CONFIG_MOUNT="-v $CONFIG_DIR:/home/agent/.config/auggie:delegated"
132
+ ;;
133
+ codebuddy)
134
+ CONFIG_DIR="$HOME/.config/codebuddy"
135
+ mkdir -p "$CONFIG_DIR"
136
+ CONFIG_MOUNT="-v $CONFIG_DIR:/home/agent/.config/codebuddy:delegated"
137
+ ;;
138
+ jules)
139
+ CONFIG_DIR="$HOME/.config/jules"
140
+ mkdir -p "$CONFIG_DIR"
141
+ CONFIG_MOUNT="-v $CONFIG_DIR:/home/agent/.config/jules:delegated"
142
+ ;;
143
+ shai)
144
+ CONFIG_DIR="$HOME/.config/shai"
145
+ mkdir -p "$CONFIG_DIR"
146
+ CONFIG_MOUNT="-v $CONFIG_DIR:/home/agent/.config/shai:delegated"
147
+ ;;
148
+ esac
149
+ fi
150
+
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"
156
+
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
163
+
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
172
+ fi
173
+
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
185
+ else
186
+ echo "⚠️ Network '$network' not found (may have been removed)"
187
+ fi
188
+ done < "$NETWORK_FILE"
189
+ fi
190
+
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)")
198
+
199
+ tput civis
200
+ trap 'tput cnorm; exit' INT TERM
201
+
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:"
209
+ echo ""
210
+
211
+ for i in "${!options[@]}"; do
212
+ if [ "$i" -eq "$cursor" ]; then
213
+ prefix="➔ "
214
+ tput setaf 6
215
+ else
216
+ prefix=" "
217
+ fi
218
+
219
+ printf "%s %s\n" "$prefix" "${options[$i]}"
220
+ tput sgr0
221
+ done
222
+
223
+ 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
234
+ else
235
+ case "$key" in
236
+ k) ((cursor--)) ;;
237
+ j) ((cursor++)) ;;
238
+ "") break ;;
239
+ $'\n'|$'\r') break ;;
240
+ esac
241
+ fi
242
+
243
+ if [ "$cursor" -lt 0 ]; then cursor=$((${#options[@]} - 1)); fi
244
+ if [ "$cursor" -ge "${#options[@]}" ]; then cursor=0; fi
245
+ done
246
+
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."
259
+ 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."
264
+ fi
265
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
266
+ echo ""
267
+ else
268
+ # Non-interactive: just use host.docker.internal
269
+ HOST_ACCESS_ARGS="--add-host=host.docker.internal:host-gateway"
270
+ 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"
274
+ fi
275
+
276
+ # Check if Git access is allowed for this workspace
277
+ if grep -q "^$CURRENT_DIR$" "$GIT_ALLOWED_FILE" 2>/dev/null; then
278
+ # Previously allowed for this workspace
279
+ # Check if saved keys exist for this workspace
280
+ WORKSPACE_MD5=$(echo "$CURRENT_DIR" | md5sum | cut -c1-8)
281
+ SAVED_KEYS_FILE="$HOME/.ai-git-keys-$WORKSPACE_MD5"
282
+
283
+ if [ -f "$SAVED_KEYS_FILE" ]; then
284
+ # Use previously saved key selection
285
+ echo "📋 Syncing Git credentials..."
286
+ if [ -d "$GIT_CACHE_DIR/ssh" ]; then
287
+ chmod -R 700 "$GIT_CACHE_DIR/ssh" 2>/dev/null || true
288
+ rm -rf "$GIT_CACHE_DIR/ssh"
289
+ fi
290
+ mkdir -p "$GIT_CACHE_DIR/ssh"
291
+
292
+ # Read saved keys and copy them
293
+ SAVED_KEYS=()
294
+ while IFS= read -r key; do
295
+ [ -n "$key" ] && SAVED_KEYS+=("$key")
296
+ done < "$SAVED_KEYS_FILE"
297
+
298
+ for key in "${SAVED_KEYS[@]}"; do
299
+ [ -z "$key" ] && continue
300
+ src_file="$HOME/.ssh/$key"
301
+ dst_file="$GIT_CACHE_DIR/ssh/$key"
302
+
303
+ dst_dir=$(dirname "$dst_file")
304
+ mkdir -p "$dst_dir"
305
+ chmod 700 "$dst_dir"
306
+
307
+ if [ -f "$src_file" ]; then
308
+ cp "$src_file" "$dst_file"
309
+ chmod 600 "$dst_file"
310
+ fi
311
+ done
312
+
313
+ # Copy filtered SSH config (only hosts needed for this repo)
314
+ if [ -f "$HOME/.ssh/config" ]; then
315
+ SETUP_SSH="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/setup-ssh-config"
316
+ if [ -x "$SETUP_SSH" ]; then
317
+ # Join SAVED_KEYS into a comma-separated string for --keys
318
+ KEYS_ARG=$(IFS=,; echo "${SAVED_KEYS[*]}")
319
+ output=$("$SETUP_SSH" --keys "$KEYS_ARG" 2>&1)
320
+ TEMP_CONFIG=$(echo "$output" | grep "Config:" | tail -1 | awk '{print $NF}')
321
+ if [ -f "$TEMP_CONFIG" ]; then
322
+ cp "$TEMP_CONFIG" "$GIT_CACHE_DIR/ssh/config"
323
+ chmod 600 "$GIT_CACHE_DIR/ssh/config"
324
+ else
325
+ cp "$HOME/.ssh/config" "$GIT_CACHE_DIR/ssh/config"
326
+ chmod 600 "$GIT_CACHE_DIR/ssh/config"
327
+ fi
328
+ else
329
+ cp "$HOME/.ssh/config" "$GIT_CACHE_DIR/ssh/config"
330
+ chmod 600 "$GIT_CACHE_DIR/ssh/config"
331
+ fi
332
+ fi
333
+
334
+ if [ -f "$HOME/.ssh/known_hosts" ]; then
335
+ cp "$HOME/.ssh/known_hosts" "$GIT_CACHE_DIR/ssh/known_hosts"
336
+ chmod 600 "$GIT_CACHE_DIR/ssh/known_hosts"
337
+ fi
338
+
339
+ # Ensure all directories have correct permissions (recursive)
340
+ chmod 700 "$GIT_CACHE_DIR/ssh"
341
+ find "$GIT_CACHE_DIR/ssh" -type d -exec chmod 700 {} \;
342
+ find "$GIT_CACHE_DIR/ssh" -type f ! -name "config" ! -name "known_hosts" -exec chmod 600 {} \;
343
+
344
+ GIT_MOUNTS="$GIT_MOUNTS -v $GIT_CACHE_DIR/ssh:/home/agent/.ssh:ro"
345
+ echo "✅ Git credentials synced"
346
+ fi
347
+
348
+ if [ -f "$HOME/.gitconfig" ]; then
349
+ # Copy gitconfig to HOME_DIR (can't mount file inside mounted directory)
350
+ cp "$HOME/.gitconfig" "$HOME_DIR/.gitconfig" 2>/dev/null || true
351
+ fi
352
+ else
353
+ # Ask user if they want Git access for this workspace (only in interactive mode)
354
+ if [[ -t 0 ]] && ([ -d "$HOME/.ssh" ] || [ -f "$HOME/.gitconfig" ]); then
355
+ echo ""
356
+ echo "🔐 Git Access Control"
357
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
358
+ echo "Allow AI tool to access Git credentials for this workspace?"
359
+ echo "Workspace: $CURRENT_DIR"
360
+ echo ""
361
+ echo " 1) Yes, allow once (this session only)"
362
+ echo " 2) Yes, always allow for this workspace"
363
+ echo " 3) No, keep Git disabled (secure default)"
364
+ echo ""
365
+ read -p "Choice [1-3]: " git_choice
366
+
367
+ case "$git_choice" in
368
+ 1|2)
369
+ # Interactive SSH key selection
370
+ echo ""
371
+ echo "🔑 SSH Key Selection"
372
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
373
+
374
+ # Source the SSH key selector library
375
+ # Resolve symlink to get actual project directory
376
+ SCRIPT_PATH="${BASH_SOURCE[0]}"
377
+ while [ -L "$SCRIPT_PATH" ]; do
378
+ SCRIPT_DIR="$(cd "$(dirname "$SCRIPT_PATH")" && pwd)"
379
+ SCRIPT_PATH="$(readlink "$SCRIPT_PATH")"
380
+ [[ $SCRIPT_PATH != /* ]] && SCRIPT_PATH="$SCRIPT_DIR/$SCRIPT_PATH"
381
+ done
382
+ SCRIPT_DIR="$(cd "$(dirname "$SCRIPT_PATH")" && pwd)"
383
+ source "$SCRIPT_DIR/../lib/ssh-key-selector.sh"
384
+
385
+ # Let user select keys
386
+ if select_ssh_keys; then
387
+ if [ ${#SELECTED_SSH_KEYS[@]} -gt 0 ]; then
388
+ echo "📋 Copying selected credentials to cache..."
389
+ if [ -d "$GIT_CACHE_DIR/ssh" ]; then
390
+ chmod -R 700 "$GIT_CACHE_DIR/ssh" 2>/dev/null || true
391
+ rm -rf "$GIT_CACHE_DIR/ssh"
392
+ fi
393
+ mkdir -p "$GIT_CACHE_DIR/ssh"
394
+
395
+ # Copy selected SSH keys (preserve directory structure exactly)
396
+ for key in "${SELECTED_SSH_KEYS[@]}"; do
397
+ echo " → Copying $key..."
398
+ src_file="$HOME/.ssh/$key"
399
+ dst_file="$GIT_CACHE_DIR/ssh/$key"
400
+
401
+ # Create parent directory with correct permissions
402
+ dst_dir=$(dirname "$dst_file")
403
+ mkdir -p "$dst_dir"
404
+ chmod 700 "$dst_dir"
405
+
406
+ # Copy the file and set permissions
407
+ if [ -f "$src_file" ]; then
408
+ cp "$src_file" "$dst_file"
409
+ chmod 600 "$dst_file"
410
+ else
411
+ echo " ⚠️ Warning: $src_file not found, skipping"
412
+ fi
413
+ done
414
+
415
+ # Copy filtered SSH config (only hosts needed for this repo)
416
+ if [ -f "$HOME/.ssh/config" ]; then
417
+ echo " → Generating filtered SSH config..."
418
+ # Run setup-ssh-config to get filtered config
419
+ SETUP_SSH="$SCRIPT_DIR/setup-ssh-config"
420
+ if [ -x "$SETUP_SSH" ]; then
421
+ # Join SELECTED_SSH_KEYS into a comma-separated string for --keys
422
+ KEYS_ARG=$(IFS=,; echo "${SELECTED_SSH_KEYS[*]}")
423
+ # Run it and capture the filtered config path
424
+ output=$("$SETUP_SSH" --keys "$KEYS_ARG" 2>&1)
425
+ TEMP_CONFIG=$(echo "$output" | grep "Config:" | tail -1 | awk '{print $NF}')
426
+ if [ -f "$TEMP_CONFIG" ]; then
427
+ cp "$TEMP_CONFIG" "$GIT_CACHE_DIR/ssh/config"
428
+ chmod 600 "$GIT_CACHE_DIR/ssh/config"
429
+ else
430
+ # Fallback to copying full config if setup-ssh-config fails
431
+ cp "$HOME/.ssh/config" "$GIT_CACHE_DIR/ssh/config"
432
+ chmod 600 "$GIT_CACHE_DIR/ssh/config"
433
+ fi
434
+ else
435
+ # Fallback: copy full config
436
+ cp "$HOME/.ssh/config" "$GIT_CACHE_DIR/ssh/config"
437
+ chmod 600 "$GIT_CACHE_DIR/ssh/config"
438
+ fi
439
+ fi
440
+
441
+ if [ -f "$HOME/.ssh/known_hosts" ]; then
442
+ echo " → Copying known_hosts..."
443
+ cp "$HOME/.ssh/known_hosts" "$GIT_CACHE_DIR/ssh/known_hosts"
444
+ chmod 600 "$GIT_CACHE_DIR/ssh/known_hosts"
445
+ fi
446
+
447
+ # Ensure all directories and files have correct permissions (recursive)
448
+ chmod 700 "$GIT_CACHE_DIR/ssh"
449
+ find "$GIT_CACHE_DIR/ssh" -type d -exec chmod 700 {} \;
450
+ find "$GIT_CACHE_DIR/ssh" -type f ! -name "config" ! -name "known_hosts" -exec chmod 600 {} \;
451
+
452
+ GIT_MOUNTS="$GIT_MOUNTS -v $GIT_CACHE_DIR/ssh:/home/agent/.ssh:ro"
453
+
454
+ # Copy gitconfig
455
+ if [ -f "$HOME/.gitconfig" ]; then
456
+ echo " → Copying .gitconfig..."
457
+ cp "$HOME/.gitconfig" "$HOME_DIR/.gitconfig" 2>/dev/null || true
458
+ fi
459
+
460
+ if [ "$git_choice" = "2" ]; then
461
+ # Save workspace and selected keys for future sessions
462
+ echo "$CURRENT_DIR" >> "$GIT_ALLOWED_FILE"
463
+ # Save selected keys (one per line for easier parsing)
464
+ WORKSPACE_MD5=$(echo "$CURRENT_DIR" | md5sum | cut -c1-8)
465
+ printf "%s\n" "${SELECTED_SSH_KEYS[@]}" > "$HOME/.ai-git-keys-$WORKSPACE_MD5"
466
+ echo "✅ Git access enabled and saved for: $CURRENT_DIR"
467
+ else
468
+ echo "✅ Git access enabled for this session"
469
+ fi
470
+ else
471
+ echo "⚠️ No SSH keys selected. Git access disabled."
472
+ fi
473
+ else
474
+ echo "⚠️ SSH key selection cancelled. Git access disabled."
475
+ fi
476
+ ;;
477
+ *)
478
+ # Default: no Git access
479
+ echo "🔒 Git access disabled (secure mode)"
480
+ ;;
481
+ esac
482
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
483
+ echo ""
484
+ fi
485
+ fi
486
+
487
+ # Generate container name based on tool and folder
488
+ # Format: {tool}-{sanitized_folder_name}-{random_suffix}
489
+ generate_container_name() {
490
+ local folder_name=$(basename "$CURRENT_DIR")
491
+
492
+ # Sanitize: keep only alphanumeric, hyphens, underscores
493
+ # Replace spaces with hyphens, remove special chars
494
+ folder_name=$(echo "$folder_name" | tr ' ' '-' | tr -cd '[:alnum:]_-' | tr '[:upper:]' '[:lower:]')
495
+
496
+ # Limit length (max 50 chars for container name, leaving room for random suffix)
497
+ if [[ ${#folder_name} -gt 40 ]]; then
498
+ folder_name="${folder_name:0:40}"
499
+ fi
500
+
501
+ # Remove trailing hyphens/underscores
502
+ folder_name=$(echo "$folder_name" | sed 's/[-_]*$//')
503
+
504
+ # If empty after sanitization, use "workspace"
505
+ if [[ -z "$folder_name" ]]; then
506
+ folder_name="workspace"
507
+ fi
508
+
509
+ # Generate random 6-character suffix (hex)
510
+ local random_suffix=$(openssl rand -hex 3)
511
+
512
+ echo "${TOOL}-${folder_name}-${random_suffix}"
513
+ }
514
+
515
+ # Container name and TTY allocation
516
+ CONTAINER_NAME=""
517
+ TTY_FLAGS="-it" # Default to interactive mode
518
+
519
+ # Check if we have a proper TTY and are in interactive mode
520
+ if [[ ! -t 0 ]] || [[ ! -t 1 ]]; then
521
+ # No TTY available or non-interactive mode
522
+ TTY_FLAGS=""
523
+ echo "⚠️ Non-interactive mode detected. Terminal interface may be limited."
524
+ elif [[ -n "$CI" ]] || [[ -n "$GITHUB_ACTIONS" ]]; then
525
+ # CI environment - disable interactive features
526
+ TTY_FLAGS=""
527
+ echo "ℹ️ CI environment detected. Running in non-interactive mode."
528
+ fi
529
+
530
+ # Only set container name for interactive mode to avoid conflicts
531
+ if [[ -n "$TTY_FLAGS" ]]; then
532
+ CONTAINER_NAME="--name $(generate_container_name)"
533
+ fi
534
+
535
+ # Port exposure configuration
536
+ PORT_MAPPINGS=""
537
+ if [[ -n "${PORT:-}" ]]; then
538
+ PORT_BIND="${PORT_BIND:-localhost}"
539
+ BIND_ADDR="127.0.0.1"
540
+
541
+ if [[ "$PORT_BIND" == "all" ]]; then
542
+ BIND_ADDR="0.0.0.0"
543
+ echo "⚠️ WARNING: Ports will be accessible from network (PORT_BIND=all)"
544
+ fi
545
+
546
+ IFS=',' read -ra PORTS <<< "$PORT"
547
+ for port in "${PORTS[@]}"; do
548
+ # Trim whitespace
549
+ port=$(echo "$port" | tr -d ' ')
550
+ # Validate port number (1-65535)
551
+ if [[ "$port" =~ ^[0-9]+$ ]] && [ "$port" -ge 1 ] && [ "$port" -le 65535 ]; then
552
+ PORT_MAPPINGS="$PORT_MAPPINGS -p $BIND_ADDR:$port:$port"
553
+ else
554
+ echo "⚠️ WARNING: Invalid port number: $port (skipped)"
555
+ fi
556
+ done
557
+
558
+ if [[ -n "$PORT_MAPPINGS" ]]; then
559
+ echo "🔌 Port mappings: ${PORT//,/ }"
560
+ fi
561
+ fi
562
+
563
+ # Debug output (only in verbose mode)
564
+ if [[ "${AI_RUN_DEBUG:-}" == "1" ]]; then
565
+ echo "🔧 Debug: TTY_FLAGS='$TTY_FLAGS'"
566
+ echo "🔧 Debug: CONTAINER_NAME='$CONTAINER_NAME'"
567
+ echo "🔧 Debug: IMAGE='$IMAGE'"
568
+ echo "🔧 Debug: SHELL_MODE='$SHELL_MODE'"
569
+ echo "🔧 Debug: TOOL_ARGS='${TOOL_ARGS[@]}'"
570
+ echo "🔧 Debug: PORT='${PORT:-}'"
571
+ echo "🔧 Debug: PORT_BIND='${PORT_BIND:-localhost}'"
572
+ echo "🔧 Debug: PORT_MAPPINGS='$PORT_MAPPINGS'"
573
+ fi
574
+
575
+ # Prepare command based on mode
576
+ ENTRYPOINT_OVERRIDE=""
577
+ if [[ "$SHELL_MODE" == "true" ]]; then
578
+ # Shell mode: override entrypoint to bash and show welcome message
579
+ ENTRYPOINT_OVERRIDE="--entrypoint bash"
580
+ DOCKER_COMMAND=(
581
+ "-c"
582
+ "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"
583
+ )
584
+ else
585
+ # Direct mode: use image's default entrypoint with arguments
586
+ DOCKER_COMMAND=("${TOOL_ARGS[@]}")
587
+ fi
588
+
589
+ # Detect platform architecture (avoid slow emulation)
590
+ PLATFORM="${AI_RUN_PLATFORM:-}"
591
+ if [[ -z "$PLATFORM" ]]; then
592
+ case "$(uname -m)" in
593
+ x86_64)
594
+ PLATFORM="linux/amd64"
595
+ ;;
596
+ aarch64|arm64)
597
+ PLATFORM="linux/arm64"
598
+ ;;
599
+ *)
600
+ PLATFORM="linux/$(uname -m)"
601
+ ;;
602
+ esac
603
+ fi
604
+
605
+ # Terminal size for TUI apps (important for opencode, aider, etc.)
606
+ TERMINAL_SIZE=""
607
+ if [[ -n "$TTY_FLAGS" ]]; then
608
+ # Get current terminal size
609
+ TERM_COLS=$(tput cols 2>/dev/null || echo "120")
610
+ TERM_LINES=$(tput lines 2>/dev/null || echo "40")
611
+ TERMINAL_SIZE="-e COLUMNS=$TERM_COLS -e LINES=$TERM_LINES"
612
+ fi
613
+
614
+ docker run $CONTAINER_NAME --rm $TTY_FLAGS \
615
+ --init \
616
+ --platform "$PLATFORM" \
617
+ $ENTRYPOINT_OVERRIDE \
618
+ $VOLUME_MOUNTS \
619
+ $CONFIG_MOUNT \
620
+ $GIT_MOUNTS \
621
+ $NETWORK_OPTIONS \
622
+ $HOST_ACCESS_ARGS \
623
+ $PORT_MAPPINGS \
624
+ -v "$CACHE_DIR":/home/agent/.cache \
625
+ -v "$HOME_DIR":/home/agent \
626
+ -w "$CURRENT_DIR" \
627
+ --env-file "$ENV_FILE" \
628
+ -e TERM="$TERM" \
629
+ -e COLORTERM="$COLORTERM" \
630
+ $TERMINAL_SIZE \
631
+ "$IMAGE" "${DOCKER_COMMAND[@]}"