@kokorolx/ai-sandbox-wrapper 3.4.1 → 3.4.3-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/ai-run +370 -1
- package/bin/cli.js +74 -2
- package/lib/build-sandbox.sh +6 -0
- package/lib/install-base.sh +45 -0
- package/lib/install-open-design.sh +60 -0
- package/package.json +1 -1
- package/setup.sh +79 -14
- package/skills/dd-pup/SKILL.md +186 -0
- package/dockerfiles/base/Dockerfile +0 -103
- package/dockerfiles/base/skills/rtk/SKILL.md +0 -103
- package/dockerfiles/base/skills/rtk-setup/SKILL.md +0 -118
- package/dockerfiles/opencode/Dockerfile +0 -9
- package/dockerfiles/sandbox/Dockerfile +0 -119
- package/dockerfiles/sandbox/skills/rtk/SKILL.md +0 -103
- package/dockerfiles/sandbox/skills/rtk-setup/SKILL.md +0 -118
package/bin/ai-run
CHANGED
|
@@ -141,6 +141,293 @@ else
|
|
|
141
141
|
TOOL=""
|
|
142
142
|
fi
|
|
143
143
|
|
|
144
|
+
# ────────────────────────────────────────────────────────────────
|
|
145
|
+
# OPEN-DESIGN SERVICE-TYPE TOOL DISPATCHER
|
|
146
|
+
# Unlike ephemeral CLI tools, open-design is a long-running daemon.
|
|
147
|
+
# Routes init/start/stop/restart/status/logs subcommands and exits.
|
|
148
|
+
# ────────────────────────────────────────────────────────────────
|
|
149
|
+
if [[ "$TOOL" == "open-design" ]]; then
|
|
150
|
+
OD_CONTAINER_NAME="ai-open-design"
|
|
151
|
+
OD_IMAGE="ai-open-design:latest"
|
|
152
|
+
OD_NETWORK="ai-sandbox"
|
|
153
|
+
OD_VOLUME="ai-open-design-data"
|
|
154
|
+
OD_ENV_FILE="$HOME/.ai-sandbox/env"
|
|
155
|
+
OD_DEFAULT_URL="http://ai-open-design:7456"
|
|
156
|
+
|
|
157
|
+
od_print_help() {
|
|
158
|
+
cat <<HLP
|
|
159
|
+
Usage: ai-run open-design <subcommand> [options]
|
|
160
|
+
|
|
161
|
+
Subcommands:
|
|
162
|
+
init [--force] One-time setup: generate API token, create network/volume
|
|
163
|
+
start [--expose] [--port N] Boot daemon (detached). --expose publishes port to host
|
|
164
|
+
stop Stop daemon (preserves container for restart)
|
|
165
|
+
restart [start-flags...] Stop then start
|
|
166
|
+
status Show daemon state, network, port, token, health
|
|
167
|
+
logs [-f|--follow] Show daemon logs (-f to follow)
|
|
168
|
+
--help, -h Show this help
|
|
169
|
+
|
|
170
|
+
Examples:
|
|
171
|
+
ai-run open-design init
|
|
172
|
+
ai-run open-design start
|
|
173
|
+
ai-run open-design start --expose # publishes 7456 to host
|
|
174
|
+
ai-run open-design start --expose --port 17456
|
|
175
|
+
ai-run open-design status
|
|
176
|
+
HLP
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
od_env_has_token() {
|
|
180
|
+
[[ -f "$OD_ENV_FILE" ]] && grep -q "^OD_API_TOKEN=" "$OD_ENV_FILE"
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
od_read_token() {
|
|
184
|
+
[[ -f "$OD_ENV_FILE" ]] && grep "^OD_API_TOKEN=" "$OD_ENV_FILE" | head -1 | cut -d= -f2-
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
od_init() {
|
|
188
|
+
local force=false
|
|
189
|
+
if [[ "${1:-}" == "--force" ]]; then
|
|
190
|
+
force=true
|
|
191
|
+
fi
|
|
192
|
+
|
|
193
|
+
mkdir -p "$(dirname "$OD_ENV_FILE")"
|
|
194
|
+
touch "$OD_ENV_FILE"
|
|
195
|
+
chmod 600 "$OD_ENV_FILE"
|
|
196
|
+
|
|
197
|
+
if od_env_has_token && [[ "$force" != "true" ]]; then
|
|
198
|
+
echo "ℹ️ OD_API_TOKEN already set in $OD_ENV_FILE — nothing to do"
|
|
199
|
+
echo " Use 'ai-run open-design init --force' to regenerate (will invalidate running sessions)"
|
|
200
|
+
else
|
|
201
|
+
if [[ "$force" == "true" ]] && od_env_has_token; then
|
|
202
|
+
printf "⚠️ This will replace the existing OD_API_TOKEN. Continue? [y/N] "
|
|
203
|
+
read -r confirm
|
|
204
|
+
if [[ "$confirm" != "y" && "$confirm" != "Y" ]]; then
|
|
205
|
+
echo "Aborted."
|
|
206
|
+
exit 0
|
|
207
|
+
fi
|
|
208
|
+
# Strip existing OD_API_TOKEN line
|
|
209
|
+
if [[ "$(uname)" == "Darwin" ]]; then
|
|
210
|
+
sed -i '' '/^OD_API_TOKEN=/d' "$OD_ENV_FILE"
|
|
211
|
+
else
|
|
212
|
+
sed -i '/^OD_API_TOKEN=/d' "$OD_ENV_FILE"
|
|
213
|
+
fi
|
|
214
|
+
fi
|
|
215
|
+
if ! command -v openssl >/dev/null 2>&1; then
|
|
216
|
+
echo "❌ ERROR: 'openssl' is required to generate a token" >&2
|
|
217
|
+
exit 1
|
|
218
|
+
fi
|
|
219
|
+
local token
|
|
220
|
+
token="$(openssl rand -hex 32)"
|
|
221
|
+
# Ensure trailing newline before append to avoid joining with previous line
|
|
222
|
+
if [[ -s "$OD_ENV_FILE" && -n "$(tail -c 1 "$OD_ENV_FILE" 2>/dev/null)" ]]; then
|
|
223
|
+
echo "" >> "$OD_ENV_FILE"
|
|
224
|
+
fi
|
|
225
|
+
echo "OD_API_TOKEN=$token" >> "$OD_ENV_FILE"
|
|
226
|
+
echo "✅ Generated OD_API_TOKEN (256-bit, written to $OD_ENV_FILE)"
|
|
227
|
+
fi
|
|
228
|
+
|
|
229
|
+
# Ensure OD_DAEMON_URL line
|
|
230
|
+
if ! grep -q "^OD_DAEMON_URL=" "$OD_ENV_FILE"; then
|
|
231
|
+
if [[ -s "$OD_ENV_FILE" && -n "$(tail -c 1 "$OD_ENV_FILE" 2>/dev/null)" ]]; then
|
|
232
|
+
echo "" >> "$OD_ENV_FILE"
|
|
233
|
+
fi
|
|
234
|
+
echo "OD_DAEMON_URL=$OD_DEFAULT_URL" >> "$OD_ENV_FILE"
|
|
235
|
+
echo "✅ Set OD_DAEMON_URL=$OD_DEFAULT_URL in $OD_ENV_FILE"
|
|
236
|
+
fi
|
|
237
|
+
|
|
238
|
+
chmod 600 "$OD_ENV_FILE"
|
|
239
|
+
|
|
240
|
+
# Ensure network
|
|
241
|
+
if ensure_network "$OD_NETWORK"; then
|
|
242
|
+
echo "✅ Docker network '$OD_NETWORK' ready"
|
|
243
|
+
fi
|
|
244
|
+
|
|
245
|
+
# Ensure volume
|
|
246
|
+
if ! docker volume inspect "$OD_VOLUME" >/dev/null 2>&1; then
|
|
247
|
+
docker volume create "$OD_VOLUME" >/dev/null
|
|
248
|
+
echo "✅ Docker volume '$OD_VOLUME' created"
|
|
249
|
+
else
|
|
250
|
+
echo "ℹ️ Docker volume '$OD_VOLUME' already exists"
|
|
251
|
+
fi
|
|
252
|
+
|
|
253
|
+
echo ""
|
|
254
|
+
echo "Next: ai-run open-design start"
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
od_start() {
|
|
258
|
+
local expose=false
|
|
259
|
+
local host_port=7456
|
|
260
|
+
|
|
261
|
+
while [[ $# -gt 0 ]]; do
|
|
262
|
+
case "$1" in
|
|
263
|
+
--expose) expose=true; shift ;;
|
|
264
|
+
--port)
|
|
265
|
+
shift
|
|
266
|
+
if [[ $# -gt 0 && "$1" =~ ^[0-9]+$ ]]; then
|
|
267
|
+
if (( $1 < 1 || $1 > 65535 )); then
|
|
268
|
+
echo "❌ ERROR: --port value '$1' out of range (1-65535)" >&2; exit 1
|
|
269
|
+
fi
|
|
270
|
+
host_port="$1"
|
|
271
|
+
shift
|
|
272
|
+
else
|
|
273
|
+
echo "❌ ERROR: --port requires a numeric value (e.g. 7456)" >&2; exit 1
|
|
274
|
+
fi
|
|
275
|
+
;;
|
|
276
|
+
*) echo "❌ ERROR: unknown start flag: $1" >&2; exit 1 ;;
|
|
277
|
+
esac
|
|
278
|
+
done
|
|
279
|
+
|
|
280
|
+
if ! od_env_has_token; then
|
|
281
|
+
echo "❌ ERROR: OD_API_TOKEN not found in $OD_ENV_FILE"
|
|
282
|
+
echo " Run: ai-run open-design init"
|
|
283
|
+
exit 1
|
|
284
|
+
fi
|
|
285
|
+
|
|
286
|
+
if ! docker image inspect "$OD_IMAGE" >/dev/null 2>&1; then
|
|
287
|
+
echo "❌ ERROR: image '$OD_IMAGE' not built"
|
|
288
|
+
echo " Run: bash lib/install-open-design.sh"
|
|
289
|
+
exit 1
|
|
290
|
+
fi
|
|
291
|
+
|
|
292
|
+
ensure_network "$OD_NETWORK" || exit 1
|
|
293
|
+
docker volume inspect "$OD_VOLUME" >/dev/null 2>&1 || docker volume create "$OD_VOLUME" >/dev/null
|
|
294
|
+
|
|
295
|
+
# If a container with this name exists, handle it gracefully
|
|
296
|
+
if docker ps -a --format '{{.Names}}' | grep -q "^${OD_CONTAINER_NAME}$"; then
|
|
297
|
+
if docker ps --format '{{.Names}}' | grep -q "^${OD_CONTAINER_NAME}$"; then
|
|
298
|
+
echo "ℹ️ '$OD_CONTAINER_NAME' is already running"
|
|
299
|
+
echo " Use 'ai-run open-design restart' to apply new flags"
|
|
300
|
+
return 0
|
|
301
|
+
else
|
|
302
|
+
echo "🔄 Removing stopped container '$OD_CONTAINER_NAME' to recreate with current flags..."
|
|
303
|
+
docker rm "$OD_CONTAINER_NAME" >/dev/null
|
|
304
|
+
fi
|
|
305
|
+
fi
|
|
306
|
+
|
|
307
|
+
local run_args=(
|
|
308
|
+
run -d
|
|
309
|
+
--name "$OD_CONTAINER_NAME"
|
|
310
|
+
--network "$OD_NETWORK"
|
|
311
|
+
--restart unless-stopped
|
|
312
|
+
-v "$OD_VOLUME:/app/.od"
|
|
313
|
+
--env-file "$OD_ENV_FILE"
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
if [[ "$expose" == "true" ]]; then
|
|
317
|
+
run_args+=(-p "${host_port}:7456")
|
|
318
|
+
fi
|
|
319
|
+
|
|
320
|
+
run_args+=("$OD_IMAGE")
|
|
321
|
+
|
|
322
|
+
echo "🔄 Starting $OD_CONTAINER_NAME..."
|
|
323
|
+
docker "${run_args[@]}" >/dev/null
|
|
324
|
+
echo "✅ $OD_CONTAINER_NAME running on network '$OD_NETWORK'"
|
|
325
|
+
if [[ "$expose" == "true" ]]; then
|
|
326
|
+
echo " Published to host: http://localhost:${host_port}"
|
|
327
|
+
else
|
|
328
|
+
echo " Internal-only: reachable from sandbox containers as http://ai-open-design:7456"
|
|
329
|
+
fi
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
od_stop() {
|
|
333
|
+
if docker ps --format '{{.Names}}' | grep -q "^${OD_CONTAINER_NAME}$"; then
|
|
334
|
+
echo "🔄 Stopping $OD_CONTAINER_NAME..."
|
|
335
|
+
docker stop "$OD_CONTAINER_NAME" >/dev/null
|
|
336
|
+
echo "✅ $OD_CONTAINER_NAME stopped (data preserved in volume '$OD_VOLUME')"
|
|
337
|
+
else
|
|
338
|
+
echo "ℹ️ $OD_CONTAINER_NAME is not running"
|
|
339
|
+
fi
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
od_restart() {
|
|
343
|
+
od_stop
|
|
344
|
+
od_start "$@"
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
od_status() {
|
|
348
|
+
echo "Container : $OD_CONTAINER_NAME"
|
|
349
|
+
if docker ps --format '{{.Names}}' | grep -q "^${OD_CONTAINER_NAME}$"; then
|
|
350
|
+
echo "State : running"
|
|
351
|
+
local uptime
|
|
352
|
+
uptime="$(docker inspect -f '{{.State.StartedAt}}' "$OD_CONTAINER_NAME" 2>/dev/null || echo unknown)"
|
|
353
|
+
echo "Started : $uptime"
|
|
354
|
+
local ports
|
|
355
|
+
ports="$(docker inspect -f '{{json .NetworkSettings.Ports}}' "$OD_CONTAINER_NAME" 2>/dev/null || echo '{}')"
|
|
356
|
+
echo "Ports : $ports"
|
|
357
|
+
elif docker ps -a --format '{{.Names}}' | grep -q "^${OD_CONTAINER_NAME}$"; then
|
|
358
|
+
echo "State : stopped"
|
|
359
|
+
echo " Hint : ai-run open-design start"
|
|
360
|
+
else
|
|
361
|
+
echo "State : not installed"
|
|
362
|
+
echo " Hint : ai-run open-design init && ai-run open-design start"
|
|
363
|
+
fi
|
|
364
|
+
|
|
365
|
+
echo "Network : $OD_NETWORK"
|
|
366
|
+
echo "Volume : $OD_VOLUME"
|
|
367
|
+
if od_env_has_token; then
|
|
368
|
+
local tok
|
|
369
|
+
tok="$(od_read_token)"
|
|
370
|
+
echo "API token : set (***${tok: -4})"
|
|
371
|
+
else
|
|
372
|
+
echo "API token : (unset — run 'ai-run open-design init')"
|
|
373
|
+
fi
|
|
374
|
+
|
|
375
|
+
# Try health check (only meaningful if container is running)
|
|
376
|
+
if docker ps --format '{{.Names}}' | grep -q "^${OD_CONTAINER_NAME}$"; then
|
|
377
|
+
# Try in-container curl first (fast path); fall back to one-off container on same network
|
|
378
|
+
# because upstream daemon image may not bundle curl
|
|
379
|
+
local health_ok=false
|
|
380
|
+
if docker exec "$OD_CONTAINER_NAME" sh -c 'command -v curl' >/dev/null 2>&1; then
|
|
381
|
+
if docker exec "$OD_CONTAINER_NAME" curl -sf --max-time 3 http://127.0.0.1:7456/api/health >/dev/null 2>&1; then
|
|
382
|
+
health_ok=true
|
|
383
|
+
fi
|
|
384
|
+
else
|
|
385
|
+
# Fallback: probe via a tiny one-off container in the same network
|
|
386
|
+
if docker run --rm --network "$OD_NETWORK" curlimages/curl:latest \
|
|
387
|
+
-sf --max-time 3 "http://${OD_CONTAINER_NAME}:7456/api/health" >/dev/null 2>&1; then
|
|
388
|
+
health_ok=true
|
|
389
|
+
fi
|
|
390
|
+
fi
|
|
391
|
+
if [[ "$health_ok" == "true" ]]; then
|
|
392
|
+
echo "Health : OK"
|
|
393
|
+
else
|
|
394
|
+
echo "Health : FAIL (daemon not responding on /api/health)"
|
|
395
|
+
fi
|
|
396
|
+
fi
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
od_logs() {
|
|
400
|
+
if ! docker ps -a --format '{{.Names}}' | grep -q "^${OD_CONTAINER_NAME}$"; then
|
|
401
|
+
echo "❌ ERROR: container '$OD_CONTAINER_NAME' does not exist" >&2
|
|
402
|
+
exit 1
|
|
403
|
+
fi
|
|
404
|
+
docker logs "$@" "$OD_CONTAINER_NAME"
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
# Dispatch subcommand
|
|
408
|
+
SUBCMD="${1:-}"
|
|
409
|
+
if [[ -n "$SUBCMD" ]]; then shift; fi
|
|
410
|
+
case "$SUBCMD" in
|
|
411
|
+
init) od_init "$@" ;;
|
|
412
|
+
start) od_start "$@" ;;
|
|
413
|
+
stop) od_stop ;;
|
|
414
|
+
restart) od_restart "$@" ;;
|
|
415
|
+
status) od_status ;;
|
|
416
|
+
logs) od_logs "$@" ;;
|
|
417
|
+
--help|-h|"") od_print_help ;;
|
|
418
|
+
*)
|
|
419
|
+
echo "❌ ERROR: unknown subcommand '$SUBCMD'" >&2
|
|
420
|
+
echo ""
|
|
421
|
+
od_print_help
|
|
422
|
+
exit 1
|
|
423
|
+
;;
|
|
424
|
+
esac
|
|
425
|
+
exit 0
|
|
426
|
+
fi
|
|
427
|
+
# ────────────────────────────────────────────────────────────────
|
|
428
|
+
# END OPEN-DESIGN DISPATCHER
|
|
429
|
+
# ────────────────────────────────────────────────────────────────
|
|
430
|
+
|
|
144
431
|
# Handle no tool specified
|
|
145
432
|
if [[ -z "$TOOL" ]]; then
|
|
146
433
|
if [[ ! -t 0 ]] || [[ ! -t 1 ]]; then
|
|
@@ -810,6 +1097,20 @@ unset -f _pmcp_resolve_script_dir
|
|
|
810
1097
|
|
|
811
1098
|
if [[ "$TOOL" == "opencode" ]] && command -v jq &>/dev/null && [[ -f "$AI_SANDBOX_CONFIG" ]] && declare -f pmcp::sanitize_name >/dev/null; then
|
|
812
1099
|
PLAYWRIGHT_HOST_CHROME=$(jq -r '.mcp.chromePath // empty' "$AI_SANDBOX_CONFIG" 2>/dev/null)
|
|
1100
|
+
if [[ -n "$PLAYWRIGHT_HOST_CHROME" ]] && [[ -f "$PLAYWRIGHT_HOST_CHROME" ]]; then
|
|
1101
|
+
# Ask user whether to open host Chrome browser
|
|
1102
|
+
if [[ -t 0 ]]; then
|
|
1103
|
+
echo ""
|
|
1104
|
+
echo "🌐 Host Chrome browser is configured: $PLAYWRIGHT_HOST_CHROME"
|
|
1105
|
+
printf " Open browser for AI agent? [y/N] "
|
|
1106
|
+
read -r -t 10 OPEN_CHROME_ANSWER || OPEN_CHROME_ANSWER=""
|
|
1107
|
+
echo ""
|
|
1108
|
+
if [[ ! "$OPEN_CHROME_ANSWER" =~ ^[Yy]$ ]]; then
|
|
1109
|
+
echo " ⏭️ Skipping host Chrome (using container Chromium if available)"
|
|
1110
|
+
PLAYWRIGHT_HOST_CHROME=""
|
|
1111
|
+
fi
|
|
1112
|
+
fi
|
|
1113
|
+
fi
|
|
813
1114
|
if [[ -n "$PLAYWRIGHT_HOST_CHROME" ]] && [[ -f "$PLAYWRIGHT_HOST_CHROME" ]]; then
|
|
814
1115
|
HOST_CHROME_CDP=true
|
|
815
1116
|
echo "🌐 Host Chrome CDP mode: $PLAYWRIGHT_HOST_CHROME"
|
|
@@ -1000,6 +1301,19 @@ get_network_containers() {
|
|
|
1000
1301
|
docker network inspect "$network" --format '{{range .Containers}}{{.Name}} {{end}}' 2>/dev/null | xargs | tr ' ' ', '
|
|
1001
1302
|
}
|
|
1002
1303
|
|
|
1304
|
+
# Ensure shared Docker network exists for cross-container service discovery
|
|
1305
|
+
# (e.g., agent containers reaching the open-design daemon by name)
|
|
1306
|
+
ensure_network() {
|
|
1307
|
+
local net="${1:-ai-sandbox}"
|
|
1308
|
+
if ! docker network inspect "$net" >/dev/null 2>&1; then
|
|
1309
|
+
docker network create "$net" >/dev/null 2>&1 || {
|
|
1310
|
+
echo "⚠️ WARNING: failed to create Docker network '$net'" >&2
|
|
1311
|
+
return 1
|
|
1312
|
+
}
|
|
1313
|
+
fi
|
|
1314
|
+
return 0
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1003
1317
|
# Interactive network selection menu (multi-select)
|
|
1004
1318
|
show_network_menu() {
|
|
1005
1319
|
local compose_nets=()
|
|
@@ -2597,7 +2911,15 @@ fi
|
|
|
2597
2911
|
|
|
2598
2912
|
# Nano-brain targeted preflight + auto-repair wrapper
|
|
2599
2913
|
if [[ "$SHELL_MODE" == "true" ]] && [[ "$NANO_BRAIN_AUTO_REPAIR" == "true" ]] && [[ "${DOCKER_COMMAND[0]:-}" == "-c" ]]; then
|
|
2600
|
-
|
|
2914
|
+
# Separate hook from following command with newline so bash parses them
|
|
2915
|
+
# as distinct statements. Without this, the hook's trailing `export -f`
|
|
2916
|
+
# line gets joined with the next `echo` call, producing:
|
|
2917
|
+
# export -f nano_brain_shell_wrapper npx echo ''; echo '...'
|
|
2918
|
+
# which makes bash try to `export -f echo` (a builtin, not a function)
|
|
2919
|
+
# and `export -f ''` (empty name), emitting two harmless but ugly errors:
|
|
2920
|
+
# bash: line 68: export: echo: not a function
|
|
2921
|
+
# bash: line 68: export: : not a function
|
|
2922
|
+
DOCKER_COMMAND[1]="$NANO_BRAIN_SHELL_HOOK"$'\n'"${DOCKER_COMMAND[1]}"
|
|
2601
2923
|
fi
|
|
2602
2924
|
|
|
2603
2925
|
if [[ "$SHELL_MODE" != "true" ]] && is_nano_brain_command; then
|
|
@@ -2739,12 +3061,23 @@ DOCKER_ARGS+=($TOOL_CONFIG_MOUNTS)
|
|
|
2739
3061
|
DOCKER_ARGS+=($RG_COMPAT_MOUNT)
|
|
2740
3062
|
DOCKER_ARGS+=($GIT_MOUNTS)
|
|
2741
3063
|
DOCKER_ARGS+=($SSH_AGENT_ENV)
|
|
3064
|
+
# Default to ai-sandbox network for service discovery if user didn't specify.
|
|
3065
|
+
# Only add --network if creation succeeded; otherwise Docker uses its default bridge.
|
|
3066
|
+
if [[ -z "$NETWORK_OPTIONS" ]]; then
|
|
3067
|
+
if ensure_network "ai-sandbox" >/dev/null 2>&1; then
|
|
3068
|
+
DOCKER_ARGS+=(--network ai-sandbox)
|
|
3069
|
+
fi
|
|
3070
|
+
fi
|
|
2742
3071
|
DOCKER_ARGS+=($NETWORK_OPTIONS)
|
|
2743
3072
|
DOCKER_ARGS+=($DISPLAY_FLAGS)
|
|
2744
3073
|
DOCKER_ARGS+=($HOST_ACCESS_ARGS)
|
|
2745
3074
|
DOCKER_ARGS+=($PORT_MAPPINGS)
|
|
2746
3075
|
DOCKER_ARGS+=($OPENCODE_PASSWORD_ENV)
|
|
2747
3076
|
DOCKER_ARGS+=(-v "$HOME_DIR":/home/agent)
|
|
3077
|
+
# Auto-mount open-design data volume read-only so agents can read generated artifacts
|
|
3078
|
+
if docker volume inspect ai-open-design-data >/dev/null 2>&1; then
|
|
3079
|
+
DOCKER_ARGS+=(-v "ai-open-design-data:/workspace/.od:ro")
|
|
3080
|
+
fi
|
|
2748
3081
|
DOCKER_ARGS+=($SHARED_CACHE_MOUNTS)
|
|
2749
3082
|
DOCKER_ARGS+=($NANO_BRAIN_MOUNT)
|
|
2750
3083
|
DOCKER_ARGS+=(-w "$CURRENT_DIR")
|
|
@@ -2763,5 +3096,41 @@ DOCKER_ARGS+=($TERMINAL_SIZE)
|
|
|
2763
3096
|
DOCKER_ARGS+=("$IMAGE")
|
|
2764
3097
|
DOCKER_ARGS+=("${DOCKER_COMMAND[@]}")
|
|
2765
3098
|
|
|
3099
|
+
# Auto-start open-design daemon if image exists but container not running
|
|
3100
|
+
if docker image inspect ai-open-design:latest >/dev/null 2>&1; then
|
|
3101
|
+
if ! docker ps --format '{{.Names}}' | grep -q "^ai-open-design$"; then
|
|
3102
|
+
if [[ -t 0 && -t 1 ]]; then
|
|
3103
|
+
printf "🎨 Open Design daemon is not running. Start it? [Y/n] "
|
|
3104
|
+
read -r OD_ANSWER
|
|
3105
|
+
if [[ ! "$OD_ANSWER" =~ ^[Nn]$ ]]; then
|
|
3106
|
+
OD_ENV_FILE="$HOME/.ai-sandbox/env"
|
|
3107
|
+
OD_NETWORK="ai-sandbox"
|
|
3108
|
+
OD_VOLUME="ai-open-design-data"
|
|
3109
|
+
# Auto-init if token missing
|
|
3110
|
+
if ! grep -q "^OD_API_TOKEN=" "$OD_ENV_FILE" 2>/dev/null; then
|
|
3111
|
+
mkdir -p "$(dirname "$OD_ENV_FILE")"
|
|
3112
|
+
touch "$OD_ENV_FILE"
|
|
3113
|
+
chmod 600 "$OD_ENV_FILE"
|
|
3114
|
+
OD_TOKEN="$(openssl rand -hex 32)"
|
|
3115
|
+
echo "OD_API_TOKEN=$OD_TOKEN" >> "$OD_ENV_FILE"
|
|
3116
|
+
echo "OD_DAEMON_URL=http://ai-open-design:7456" >> "$OD_ENV_FILE"
|
|
3117
|
+
echo "✅ Generated OD_API_TOKEN"
|
|
3118
|
+
fi
|
|
3119
|
+
# Ensure network and volume
|
|
3120
|
+
docker network inspect "$OD_NETWORK" >/dev/null 2>&1 || docker network create "$OD_NETWORK" >/dev/null
|
|
3121
|
+
docker volume inspect "$OD_VOLUME" >/dev/null 2>&1 || docker volume create "$OD_VOLUME" >/dev/null
|
|
3122
|
+
# Remove stopped container if exists
|
|
3123
|
+
if docker ps -a --format '{{.Names}}' | grep -q "^ai-open-design$"; then
|
|
3124
|
+
docker rm ai-open-design >/dev/null 2>&1 || true
|
|
3125
|
+
fi
|
|
3126
|
+
docker run -d --name ai-open-design --network "$OD_NETWORK" \
|
|
3127
|
+
--restart unless-stopped -v "$OD_VOLUME:/app/.od" \
|
|
3128
|
+
--env-file "$OD_ENV_FILE" ai-open-design:latest >/dev/null
|
|
3129
|
+
echo "✅ Open Design daemon started (http://ai-open-design:7456)"
|
|
3130
|
+
fi
|
|
3131
|
+
fi
|
|
3132
|
+
fi
|
|
3133
|
+
fi
|
|
3134
|
+
|
|
2766
3135
|
# Execute docker run with proper argument handling
|
|
2767
3136
|
docker run "${DOCKER_ARGS[@]}"
|
package/bin/cli.js
CHANGED
|
@@ -8,7 +8,7 @@ const readline = require('readline');
|
|
|
8
8
|
|
|
9
9
|
const args = process.argv.slice(2);
|
|
10
10
|
const packageRoot = path.resolve(__dirname, '..');
|
|
11
|
-
const flags = { noCache: args.includes('--no-cache') };
|
|
11
|
+
const flags = { noCache: args.includes('--no-cache') || args.includes('--fresh') };
|
|
12
12
|
const positionalArgs = args.filter(arg => !arg.startsWith('--'));
|
|
13
13
|
const command = positionalArgs[0];
|
|
14
14
|
|
|
@@ -21,6 +21,7 @@ Usage:
|
|
|
21
21
|
|
|
22
22
|
Commands:
|
|
23
23
|
setup Run interactive setup (configure workspaces, select tools)
|
|
24
|
+
rebuild [--fresh] Rebuild Docker image using existing config (no menu required)
|
|
24
25
|
update Interactive menu to manage config (workspaces, git, networks)
|
|
25
26
|
clean Interactive cleanup for caches/configs
|
|
26
27
|
clean cache [type] Clear shared package caches (npm, bun, pip, playwright-browsers)
|
|
@@ -44,13 +45,16 @@ Commands:
|
|
|
44
45
|
help Show this help message
|
|
45
46
|
|
|
46
47
|
Options:
|
|
47
|
-
--
|
|
48
|
+
--fresh Build Docker image without using layer cache (full rebuild)
|
|
49
|
+
--no-cache Alias for --fresh (note: use --fresh when running via npx)
|
|
48
50
|
--json Output in JSON format (for config show)
|
|
49
51
|
--global Apply to global scope (for network commands)
|
|
50
52
|
--workspace Apply to specific workspace (for network commands)
|
|
51
53
|
|
|
52
54
|
Examples:
|
|
53
55
|
npx @kokorolx/ai-sandbox-wrapper setup
|
|
56
|
+
npx @kokorolx/ai-sandbox-wrapper rebuild
|
|
57
|
+
npx @kokorolx/ai-sandbox-wrapper rebuild --fresh
|
|
54
58
|
npx @kokorolx/ai-sandbox-wrapper update
|
|
55
59
|
npx @kokorolx/ai-sandbox-wrapper config show --json
|
|
56
60
|
npx @kokorolx/ai-sandbox-wrapper config tool claude
|
|
@@ -108,6 +112,71 @@ function runSetup() {
|
|
|
108
112
|
});
|
|
109
113
|
}
|
|
110
114
|
|
|
115
|
+
function runRebuild() {
|
|
116
|
+
const buildScript = path.join(packageRoot, 'lib', 'build-sandbox.sh');
|
|
117
|
+
|
|
118
|
+
if (!fs.existsSync(buildScript)) {
|
|
119
|
+
console.error('❌ Error: lib/build-sandbox.sh not found at', buildScript);
|
|
120
|
+
process.exit(1);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const config = readConfig();
|
|
124
|
+
const toolsInstalled = (config.tools && config.tools.installed) || [];
|
|
125
|
+
const mcpInstalled = (config.mcp && config.mcp.installed) || [];
|
|
126
|
+
|
|
127
|
+
if (toolsInstalled.length === 0) {
|
|
128
|
+
console.error('❌ No tools found in ~/.ai-sandbox/config.json');
|
|
129
|
+
console.error(' Run `npx @kokorolx/ai-sandbox-wrapper setup` first.');
|
|
130
|
+
process.exit(1);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const hasMcp = (name) => mcpInstalled.includes(name);
|
|
134
|
+
const useHostChrome = !!(config.mcp && config.mcp.chromePath);
|
|
135
|
+
|
|
136
|
+
const buildEnv = {
|
|
137
|
+
...process.env,
|
|
138
|
+
TOOLS: toolsInstalled.join(','),
|
|
139
|
+
INSTALL_PLAYWRIGHT_MCP: hasMcp('playwright') ? '1' : '0',
|
|
140
|
+
INSTALL_CHROME_DEVTOOLS_MCP: hasMcp('chrome-devtools') ? '1' : '0',
|
|
141
|
+
INSTALL_PLAYWRIGHT_HOST: useHostChrome ? '1' : '0',
|
|
142
|
+
INSTALL_RTK: '0',
|
|
143
|
+
INSTALL_PUP: '0',
|
|
144
|
+
INSTALL_OD_HELPERS: '1',
|
|
145
|
+
INSTALL_SPEC_KIT: '0',
|
|
146
|
+
INSTALL_UX_UI_PROMAX: '0',
|
|
147
|
+
INSTALL_OPENSPEC: '0',
|
|
148
|
+
INSTALL_PLAYWRIGHT: '0',
|
|
149
|
+
INSTALL_RUBY: '0'
|
|
150
|
+
};
|
|
151
|
+
if (flags.noCache) {
|
|
152
|
+
buildEnv.DOCKER_NO_CACHE = '1';
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
console.log('🔨 Rebuilding Docker image with current config...');
|
|
156
|
+
console.log(` Tools: ${toolsInstalled.join(', ')}`);
|
|
157
|
+
if (mcpInstalled.length > 0) {
|
|
158
|
+
console.log(` MCP: ${mcpInstalled.join(', ')}`);
|
|
159
|
+
}
|
|
160
|
+
if (flags.noCache) {
|
|
161
|
+
console.log(' --no-cache: skipping Docker layer cache');
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const child = spawn('bash', [buildScript], {
|
|
165
|
+
cwd: packageRoot,
|
|
166
|
+
stdio: 'inherit',
|
|
167
|
+
env: buildEnv
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
child.on('error', (err) => {
|
|
171
|
+
console.error('❌ Error running rebuild:', err.message);
|
|
172
|
+
process.exit(1);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
child.on('close', (code) => {
|
|
176
|
+
process.exit(code || 0);
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
111
180
|
function expandHome(inputPath) {
|
|
112
181
|
if (!inputPath) {
|
|
113
182
|
return inputPath;
|
|
@@ -1313,6 +1382,9 @@ switch (command) {
|
|
|
1313
1382
|
case undefined:
|
|
1314
1383
|
runSetup();
|
|
1315
1384
|
break;
|
|
1385
|
+
case 'rebuild':
|
|
1386
|
+
runRebuild();
|
|
1387
|
+
break;
|
|
1316
1388
|
case 'help':
|
|
1317
1389
|
case '--help':
|
|
1318
1390
|
case '-h':
|
package/lib/build-sandbox.sh
CHANGED
|
@@ -18,6 +18,8 @@ echo "🔄 Generating unified sandbox Dockerfile..."
|
|
|
18
18
|
echo " Tools: $TOOLS"
|
|
19
19
|
|
|
20
20
|
GENERATE_ONLY=1 INSTALL_RTK="${INSTALL_RTK:-0}" \
|
|
21
|
+
INSTALL_PUP="${INSTALL_PUP:-0}" \
|
|
22
|
+
INSTALL_OD_HELPERS="${INSTALL_OD_HELPERS:-1}" \
|
|
21
23
|
INSTALL_PLAYWRIGHT_MCP="${INSTALL_PLAYWRIGHT_MCP:-0}" \
|
|
22
24
|
INSTALL_CHROME_DEVTOOLS_MCP="${INSTALL_CHROME_DEVTOOLS_MCP:-0}" \
|
|
23
25
|
INSTALL_PLAYWRIGHT="${INSTALL_PLAYWRIGHT:-0}" \
|
|
@@ -65,6 +67,10 @@ if [[ -d "dockerfiles/base/skills" ]]; then
|
|
|
65
67
|
cp -r "dockerfiles/base/skills" "$SANDBOX_DIR/"
|
|
66
68
|
fi
|
|
67
69
|
|
|
70
|
+
if [[ -d "dockerfiles/base/scripts" ]]; then
|
|
71
|
+
cp -r "dockerfiles/base/scripts" "$SANDBOX_DIR/"
|
|
72
|
+
fi
|
|
73
|
+
|
|
68
74
|
echo "✅ Dockerfile generated at $SANDBOX_DIR/Dockerfile"
|
|
69
75
|
|
|
70
76
|
echo "🔨 Building ai-sandbox:latest..."
|
package/lib/install-base.sh
CHANGED
|
@@ -67,6 +67,51 @@ COPY skills/rtk-setup/SKILL.md /home/agent/.config/opencode/skills/rtk-setup/SKI
|
|
|
67
67
|
fi
|
|
68
68
|
fi
|
|
69
69
|
|
|
70
|
+
if [[ "${INSTALL_PUP:-0}" -eq 1 ]]; then
|
|
71
|
+
echo "📦 Pup (Datadog CLI) will be installed in base image (multi-stage build)"
|
|
72
|
+
DOCKERFILE_BUILD_STAGES+='# Build Pup from source (multi-stage: only binary is kept, Rust toolchain discarded)
|
|
73
|
+
FROM rust:bookworm AS pup-builder
|
|
74
|
+
RUN cargo install --git https://github.com/DataDog/pup --locked
|
|
75
|
+
'
|
|
76
|
+
ADDITIONAL_TOOLS_INSTALL+='# Install Pup - Datadog CLI for AI agents (built from source)
|
|
77
|
+
COPY --from=pup-builder /usr/local/cargo/bin/pup /usr/local/bin/pup
|
|
78
|
+
'
|
|
79
|
+
# Copy Pup OpenCode skill into build context so it can be COPY'd into the image
|
|
80
|
+
SCRIPT_BASE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
81
|
+
PUP_SKILLS_SRC="${SCRIPT_BASE_DIR}/../skills"
|
|
82
|
+
if [[ -d "$PUP_SKILLS_SRC/dd-pup" ]]; then
|
|
83
|
+
mkdir -p "dockerfiles/base/skills/dd-pup"
|
|
84
|
+
cp "$PUP_SKILLS_SRC/dd-pup/SKILL.md" "dockerfiles/base/skills/dd-pup/SKILL.md"
|
|
85
|
+
ADDITIONAL_TOOLS_INSTALL+='# Install Pup OpenCode skill (auto-discovered by OpenCode agents)
|
|
86
|
+
RUN mkdir -p /home/agent/.config/opencode/skills/dd-pup
|
|
87
|
+
COPY skills/dd-pup/SKILL.md /home/agent/.config/opencode/skills/dd-pup/SKILL.md
|
|
88
|
+
'
|
|
89
|
+
echo " ✅ Pup OpenCode skill will be copied into container"
|
|
90
|
+
else
|
|
91
|
+
echo " ⚠️ Pup skill not found at $PUP_SKILLS_SRC/dd-pup — skipping skill installation"
|
|
92
|
+
fi
|
|
93
|
+
fi
|
|
94
|
+
|
|
95
|
+
if [[ "${INSTALL_OD_HELPERS:-1}" -eq 1 ]]; then
|
|
96
|
+
echo "📦 open-design helper scripts (od-status, od-health) will be installed in base image"
|
|
97
|
+
# Copy helper scripts into build context so they can be COPY'd into the image
|
|
98
|
+
SCRIPT_BASE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
99
|
+
OD_HELPERS_SRC="${SCRIPT_BASE_DIR}/../scripts"
|
|
100
|
+
if [[ -f "$OD_HELPERS_SRC/od-status" && -f "$OD_HELPERS_SRC/od-health" ]]; then
|
|
101
|
+
mkdir -p "dockerfiles/base/scripts"
|
|
102
|
+
cp "$OD_HELPERS_SRC/od-status" "dockerfiles/base/scripts/od-status"
|
|
103
|
+
cp "$OD_HELPERS_SRC/od-health" "dockerfiles/base/scripts/od-health"
|
|
104
|
+
ADDITIONAL_TOOLS_INSTALL+='# Install open-design helper scripts (od-status, od-health) for agent containers
|
|
105
|
+
COPY scripts/od-status /usr/local/bin/od-status
|
|
106
|
+
COPY scripts/od-health /usr/local/bin/od-health
|
|
107
|
+
RUN chmod +x /usr/local/bin/od-status /usr/local/bin/od-health
|
|
108
|
+
'
|
|
109
|
+
echo " ✅ open-design helpers will be copied into container"
|
|
110
|
+
else
|
|
111
|
+
echo " ⚠️ open-design helpers not found at $OD_HELPERS_SRC — skipping"
|
|
112
|
+
fi
|
|
113
|
+
fi
|
|
114
|
+
|
|
70
115
|
if [[ "${INSTALL_PLAYWRIGHT:-0}" -eq 1 ]]; then
|
|
71
116
|
echo "📦 Playwright will be installed in base image"
|
|
72
117
|
ADDITIONAL_TOOLS_INSTALL+='# Install Playwright system dependencies
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -e
|
|
3
|
+
|
|
4
|
+
dockerfile_snippet() {
|
|
5
|
+
cat <<'SNIPPET'
|
|
6
|
+
# open-design is a service-type tool (long-running daemon)
|
|
7
|
+
# It uses its own upstream image, not ai-base
|
|
8
|
+
# This snippet is included for convention only; the base image builder
|
|
9
|
+
# does NOT inline open-design (it runs as a separate container)
|
|
10
|
+
SNIPPET
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if [[ "${SNIPPET_MODE:-}" == "1" ]]; then
|
|
14
|
+
return 0 2>/dev/null || exit 0
|
|
15
|
+
fi
|
|
16
|
+
|
|
17
|
+
TOOL="open-design"
|
|
18
|
+
# NOTE: upstream vanjayak/open-design currently publishes only the 'latest' tag (as of 2026-05).
|
|
19
|
+
# When upstream starts publishing version tags (e.g., 0.8.0-preview), pin via OPEN_DESIGN_IMAGE_TAG
|
|
20
|
+
# or OPEN_DESIGN_IMAGE to avoid breaking changes.
|
|
21
|
+
OPEN_DESIGN_IMAGE_TAG="${OPEN_DESIGN_IMAGE_TAG:-latest}"
|
|
22
|
+
OPEN_DESIGN_IMAGE="${OPEN_DESIGN_IMAGE:-docker.io/vanjayak/open-design:${OPEN_DESIGN_IMAGE_TAG}}"
|
|
23
|
+
OPEN_DESIGN_VERSION="${OPEN_DESIGN_VERSION:-${OPEN_DESIGN_IMAGE_TAG}}"
|
|
24
|
+
|
|
25
|
+
echo "Installing $TOOL (Open Design daemon — long-running HTTP service)..."
|
|
26
|
+
echo " Upstream image: $OPEN_DESIGN_IMAGE"
|
|
27
|
+
|
|
28
|
+
mkdir -p "dockerfiles/$TOOL"
|
|
29
|
+
mkdir -p "$HOME/.ai-sandbox/tools/$TOOL/home"
|
|
30
|
+
|
|
31
|
+
# Generate Dockerfile (idempotent — overwrites existing)
|
|
32
|
+
cat > "dockerfiles/$TOOL/Dockerfile" <<EOF
|
|
33
|
+
FROM $OPEN_DESIGN_IMAGE
|
|
34
|
+
|
|
35
|
+
# Force daemon to bind on all interfaces inside the container.
|
|
36
|
+
# Bearer token auth (OD_API_TOKEN env) protects the daemon.
|
|
37
|
+
ENV OD_BIND_HOST=0.0.0.0
|
|
38
|
+
|
|
39
|
+
# Document the port (publishing is controlled by ai-run --expose)
|
|
40
|
+
EXPOSE 7456
|
|
41
|
+
|
|
42
|
+
# Daemon entrypoint is provided by upstream image (do not override)
|
|
43
|
+
EOF
|
|
44
|
+
|
|
45
|
+
# Build image
|
|
46
|
+
echo "Building Docker image for $TOOL..."
|
|
47
|
+
docker build ${DOCKER_NO_CACHE:+--no-cache} -t "ai-$TOOL:latest" "dockerfiles/$TOOL"
|
|
48
|
+
|
|
49
|
+
echo "✅ $TOOL installed (Open Design daemon)"
|
|
50
|
+
echo ""
|
|
51
|
+
echo "Features:"
|
|
52
|
+
echo " ✓ Long-running HTTP daemon (port 7456 inside container)"
|
|
53
|
+
echo " ✓ Bearer token auth (OD_API_TOKEN)"
|
|
54
|
+
echo " ✓ Persistent state via named volume (ai-open-design-data)"
|
|
55
|
+
echo " ✓ Internal-only by default (use --expose to publish to host)"
|
|
56
|
+
echo ""
|
|
57
|
+
echo "Usage:"
|
|
58
|
+
echo " ai-run open-design init # one-time: generate token, network, volume"
|
|
59
|
+
echo " ai-run open-design start # boot daemon"
|
|
60
|
+
echo " ai-run open-design status # check health"
|
package/package.json
CHANGED