@kokorolx/ai-sandbox-wrapper 3.4.2 → 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 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
- DOCKER_COMMAND[1]="$NANO_BRAIN_SHELL_HOOK ${DOCKER_COMMAND[1]}"
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,7 +21,7 @@ Usage:
21
21
 
22
22
  Commands:
23
23
  setup Run interactive setup (configure workspaces, select tools)
24
- rebuild Rebuild Docker image using existing config (no menu required)
24
+ rebuild [--fresh] Rebuild Docker image using existing config (no menu required)
25
25
  update Interactive menu to manage config (workspaces, git, networks)
26
26
  clean Interactive cleanup for caches/configs
27
27
  clean cache [type] Clear shared package caches (npm, bun, pip, playwright-browsers)
@@ -45,7 +45,8 @@ Commands:
45
45
  help Show this help message
46
46
 
47
47
  Options:
48
- --no-cache Build Docker images without using cache (fresh build)
48
+ --fresh Build Docker image without using layer cache (full rebuild)
49
+ --no-cache Alias for --fresh (note: use --fresh when running via npx)
49
50
  --json Output in JSON format (for config show)
50
51
  --global Apply to global scope (for network commands)
51
52
  --workspace Apply to specific workspace (for network commands)
@@ -53,7 +54,7 @@ Options:
53
54
  Examples:
54
55
  npx @kokorolx/ai-sandbox-wrapper setup
55
56
  npx @kokorolx/ai-sandbox-wrapper rebuild
56
- npx @kokorolx/ai-sandbox-wrapper rebuild --no-cache
57
+ npx @kokorolx/ai-sandbox-wrapper rebuild --fresh
57
58
  npx @kokorolx/ai-sandbox-wrapper update
58
59
  npx @kokorolx/ai-sandbox-wrapper config show --json
59
60
  npx @kokorolx/ai-sandbox-wrapper config tool claude
@@ -139,6 +140,8 @@ function runRebuild() {
139
140
  INSTALL_CHROME_DEVTOOLS_MCP: hasMcp('chrome-devtools') ? '1' : '0',
140
141
  INSTALL_PLAYWRIGHT_HOST: useHostChrome ? '1' : '0',
141
142
  INSTALL_RTK: '0',
143
+ INSTALL_PUP: '0',
144
+ INSTALL_OD_HELPERS: '1',
142
145
  INSTALL_SPEC_KIT: '0',
143
146
  INSTALL_UX_UI_PROMAX: '0',
144
147
  INSTALL_OPENSPEC: '0',
@@ -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..."
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kokorolx/ai-sandbox-wrapper",
3
- "version": "3.4.2",
3
+ "version": "3.4.3-beta.2",
4
4
  "description": "Docker-based security sandbox for AI coding agents. Isolate Claude, Gemini, Aider, and other AI tools from your host system.",
5
5
  "keywords": [
6
6
  "ai",