@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.
- package/README.md +540 -0
- package/bin/ai-debug +116 -0
- package/bin/ai-network +144 -0
- package/bin/ai-run +631 -0
- package/bin/cli.js +83 -0
- package/bin/setup-ssh-config +328 -0
- package/dockerfiles/AGENTS.md +92 -0
- package/dockerfiles/aider/Dockerfile +5 -0
- package/dockerfiles/amp/Dockerfile +10 -0
- package/dockerfiles/auggie/Dockerfile +12 -0
- package/dockerfiles/base/Dockerfile +73 -0
- package/dockerfiles/claude/Dockerfile +11 -0
- package/dockerfiles/codebuddy/Dockerfile +12 -0
- package/dockerfiles/codex/Dockerfile +9 -0
- package/dockerfiles/droid/Dockerfile +8 -0
- package/dockerfiles/gemini/Dockerfile +9 -0
- package/dockerfiles/jules/Dockerfile +12 -0
- package/dockerfiles/kilo/Dockerfile +25 -0
- package/dockerfiles/opencode/Dockerfile +10 -0
- package/dockerfiles/qoder/Dockerfile +12 -0
- package/dockerfiles/qwen/Dockerfile +10 -0
- package/dockerfiles/shai/Dockerfile +9 -0
- package/lib/AGENTS.md +58 -0
- package/lib/generate-ai-run.sh +19 -0
- package/lib/install-aider.sh +30 -0
- package/lib/install-amp.sh +39 -0
- package/lib/install-auggie.sh +36 -0
- package/lib/install-base.sh +139 -0
- package/lib/install-claude.sh +42 -0
- package/lib/install-codebuddy.sh +36 -0
- package/lib/install-codeserver.sh +171 -0
- package/lib/install-codex.sh +40 -0
- package/lib/install-droid.sh +27 -0
- package/lib/install-gemini.sh +39 -0
- package/lib/install-jules.sh +36 -0
- package/lib/install-kilo.sh +57 -0
- package/lib/install-opencode.sh +39 -0
- package/lib/install-qoder.sh +37 -0
- package/lib/install-qwen.sh +40 -0
- package/lib/install-shai.sh +33 -0
- package/lib/install-tool.sh +40 -0
- package/lib/install-vscode.sh +219 -0
- package/lib/ssh-key-selector.sh +189 -0
- package/package.json +46 -0
- 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[@]}"
|