@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/README.md +25 -0
- package/bin/ai-run +623 -308
- package/bin/cli.js +51 -7
- package/dockerfiles/base/Dockerfile +15 -3
- package/dockerfiles/base/skills/rtk/SKILL.md +103 -0
- package/dockerfiles/base/skills/rtk-setup/SKILL.md +118 -0
- package/dockerfiles/sandbox/Dockerfile +133 -0
- package/dockerfiles/sandbox/skills/rtk/SKILL.md +103 -0
- package/dockerfiles/sandbox/skills/rtk-setup/SKILL.md +118 -0
- package/lib/AGENTS.md +14 -0
- package/lib/build-sandbox.sh +89 -0
- package/lib/install-aider.sh +11 -1
- package/lib/install-amp.sh +20 -12
- package/lib/install-auggie.sh +16 -1
- package/lib/install-base.sh +36 -2
- package/lib/install-claude.sh +25 -4
- package/lib/install-codebuddy.sh +16 -1
- package/lib/install-codex.sh +16 -1
- package/lib/install-droid.sh +17 -4
- package/lib/install-gemini.sh +16 -1
- package/lib/install-jules.sh +16 -1
- package/lib/install-kilo.sh +12 -2
- package/lib/install-openclaw.sh +10 -1
- package/lib/install-opencode.sh +17 -4
- package/lib/install-qoder.sh +16 -1
- package/lib/install-qwen.sh +16 -1
- package/lib/install-shai.sh +15 -3
- package/package.json +1 -1
- package/setup.sh +55 -52
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
575
|
+
IMAGE="registry.gitlab.com/kokorolee/ai-sandbox-wrapper/ai-sandbox:latest"
|
|
544
576
|
else
|
|
545
|
-
IMAGE="ai
|
|
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
|
-
#
|
|
549
|
-
HOME_DIR="$SANDBOX_DIR/
|
|
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
|
-
#
|
|
562
|
-
#
|
|
563
|
-
|
|
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
|
|
566
|
-
#
|
|
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
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
mount_tool_config "$HOME
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
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
|
-
|
|
655
|
-
|
|
656
|
-
|
|
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
|
-
|
|
659
|
-
|
|
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
|
-
#
|
|
1124
|
-
|
|
1125
|
-
if
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
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
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
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
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
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
|
-
|
|
1184
|
-
|
|
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
|
-
#
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
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
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
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
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
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
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
#
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
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
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
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
|
-
|
|
1512
|
+
GIT_MOUNTS="$GIT_MOUNTS -v $GIT_CACHE_DIR/ssh:/home/agent/.ssh:ro"
|
|
1299
1513
|
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
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
|
-
|
|
1307
|
-
|
|
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
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
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 "
|
|
1555
|
+
echo "⚠️ No SSH keys selected. Git access disabled."
|
|
1339
1556
|
fi
|
|
1340
1557
|
else
|
|
1341
|
-
echo "⚠️
|
|
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" '["
|
|
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" '["
|
|
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"
|
|
2023
|
-
#
|
|
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
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
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" \
|