@kokorolx/ai-sandbox-wrapper 3.4.3 → 4.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/bin/ai-run CHANGED
@@ -141,6 +141,290 @@ 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
+ -p "127.0.0.1:${host_port}:7456"
315
+ )
316
+
317
+ if [[ "$expose" == "true" ]]; then
318
+ echo "ℹ️ Port ${host_port} already published by default (127.0.0.1:${host_port}:7456)"
319
+ fi
320
+
321
+ run_args+=("$OD_IMAGE")
322
+
323
+ echo "🔄 Starting $OD_CONTAINER_NAME..."
324
+ docker "${run_args[@]}" >/dev/null
325
+ echo "✅ $OD_CONTAINER_NAME running on network '$OD_NETWORK'"
326
+ echo " Published to host: http://localhost:${host_port}"
327
+ }
328
+
329
+ od_stop() {
330
+ if docker ps --format '{{.Names}}' | grep -q "^${OD_CONTAINER_NAME}$"; then
331
+ echo "🔄 Stopping $OD_CONTAINER_NAME..."
332
+ docker stop "$OD_CONTAINER_NAME" >/dev/null
333
+ echo "✅ $OD_CONTAINER_NAME stopped (data preserved in volume '$OD_VOLUME')"
334
+ else
335
+ echo "ℹ️ $OD_CONTAINER_NAME is not running"
336
+ fi
337
+ }
338
+
339
+ od_restart() {
340
+ od_stop
341
+ od_start "$@"
342
+ }
343
+
344
+ od_status() {
345
+ echo "Container : $OD_CONTAINER_NAME"
346
+ if docker ps --format '{{.Names}}' | grep -q "^${OD_CONTAINER_NAME}$"; then
347
+ echo "State : running"
348
+ local uptime
349
+ uptime="$(docker inspect -f '{{.State.StartedAt}}' "$OD_CONTAINER_NAME" 2>/dev/null || echo unknown)"
350
+ echo "Started : $uptime"
351
+ local ports
352
+ ports="$(docker inspect -f '{{json .NetworkSettings.Ports}}' "$OD_CONTAINER_NAME" 2>/dev/null || echo '{}')"
353
+ echo "Ports : $ports"
354
+ elif docker ps -a --format '{{.Names}}' | grep -q "^${OD_CONTAINER_NAME}$"; then
355
+ echo "State : stopped"
356
+ echo " Hint : ai-run open-design start"
357
+ else
358
+ echo "State : not installed"
359
+ echo " Hint : ai-run open-design init && ai-run open-design start"
360
+ fi
361
+
362
+ echo "Network : $OD_NETWORK"
363
+ echo "Volume : $OD_VOLUME"
364
+ if od_env_has_token; then
365
+ local tok
366
+ tok="$(od_read_token)"
367
+ echo "API token : set (***${tok: -4})"
368
+ else
369
+ echo "API token : (unset — run 'ai-run open-design init')"
370
+ fi
371
+
372
+ # Try health check (only meaningful if container is running)
373
+ if docker ps --format '{{.Names}}' | grep -q "^${OD_CONTAINER_NAME}$"; then
374
+ # Try in-container curl first (fast path); fall back to one-off container on same network
375
+ # because upstream daemon image may not bundle curl
376
+ local health_ok=false
377
+ if docker exec "$OD_CONTAINER_NAME" sh -c 'command -v curl' >/dev/null 2>&1; then
378
+ if docker exec "$OD_CONTAINER_NAME" curl -sf --max-time 3 http://127.0.0.1:7456/api/health >/dev/null 2>&1; then
379
+ health_ok=true
380
+ fi
381
+ else
382
+ # Fallback: probe via a tiny one-off container in the same network
383
+ if docker run --rm --network "$OD_NETWORK" curlimages/curl:latest \
384
+ -sf --max-time 3 "http://${OD_CONTAINER_NAME}:7456/api/health" >/dev/null 2>&1; then
385
+ health_ok=true
386
+ fi
387
+ fi
388
+ if [[ "$health_ok" == "true" ]]; then
389
+ echo "Health : OK"
390
+ else
391
+ echo "Health : FAIL (daemon not responding on /api/health)"
392
+ fi
393
+ fi
394
+ }
395
+
396
+ od_logs() {
397
+ if ! docker ps -a --format '{{.Names}}' | grep -q "^${OD_CONTAINER_NAME}$"; then
398
+ echo "❌ ERROR: container '$OD_CONTAINER_NAME' does not exist" >&2
399
+ exit 1
400
+ fi
401
+ docker logs "$@" "$OD_CONTAINER_NAME"
402
+ }
403
+
404
+ # Dispatch subcommand
405
+ SUBCMD="${1:-}"
406
+ if [[ -n "$SUBCMD" ]]; then shift; fi
407
+ case "$SUBCMD" in
408
+ init) od_init "$@" ;;
409
+ start) od_start "$@" ;;
410
+ stop) od_stop ;;
411
+ restart) od_restart "$@" ;;
412
+ status) od_status ;;
413
+ logs) od_logs "$@" ;;
414
+ --help|-h|"") od_print_help ;;
415
+ *)
416
+ echo "❌ ERROR: unknown subcommand '$SUBCMD'" >&2
417
+ echo ""
418
+ od_print_help
419
+ exit 1
420
+ ;;
421
+ esac
422
+ exit 0
423
+ fi
424
+ # ────────────────────────────────────────────────────────────────
425
+ # END OPEN-DESIGN DISPATCHER
426
+ # ────────────────────────────────────────────────────────────────
427
+
144
428
  # Handle no tool specified
145
429
  if [[ -z "$TOOL" ]]; then
146
430
  if [[ ! -t 0 ]] || [[ ! -t 1 ]]; then
@@ -810,6 +1094,20 @@ unset -f _pmcp_resolve_script_dir
810
1094
 
811
1095
  if [[ "$TOOL" == "opencode" ]] && command -v jq &>/dev/null && [[ -f "$AI_SANDBOX_CONFIG" ]] && declare -f pmcp::sanitize_name >/dev/null; then
812
1096
  PLAYWRIGHT_HOST_CHROME=$(jq -r '.mcp.chromePath // empty' "$AI_SANDBOX_CONFIG" 2>/dev/null)
1097
+ if [[ -n "$PLAYWRIGHT_HOST_CHROME" ]] && [[ -f "$PLAYWRIGHT_HOST_CHROME" ]]; then
1098
+ # Ask user whether to open host Chrome browser
1099
+ if [[ -t 0 ]]; then
1100
+ echo ""
1101
+ echo "🌐 Host Chrome browser is configured: $PLAYWRIGHT_HOST_CHROME"
1102
+ printf " Open browser for AI agent? [y/N] "
1103
+ read -r -t 10 OPEN_CHROME_ANSWER || OPEN_CHROME_ANSWER=""
1104
+ echo ""
1105
+ if [[ ! "$OPEN_CHROME_ANSWER" =~ ^[Yy]$ ]]; then
1106
+ echo " ⏭️ Skipping host Chrome (using container Chromium if available)"
1107
+ PLAYWRIGHT_HOST_CHROME=""
1108
+ fi
1109
+ fi
1110
+ fi
813
1111
  if [[ -n "$PLAYWRIGHT_HOST_CHROME" ]] && [[ -f "$PLAYWRIGHT_HOST_CHROME" ]]; then
814
1112
  HOST_CHROME_CDP=true
815
1113
  echo "🌐 Host Chrome CDP mode: $PLAYWRIGHT_HOST_CHROME"
@@ -1000,6 +1298,19 @@ get_network_containers() {
1000
1298
  docker network inspect "$network" --format '{{range .Containers}}{{.Name}} {{end}}' 2>/dev/null | xargs | tr ' ' ', '
1001
1299
  }
1002
1300
 
1301
+ # Ensure shared Docker network exists for cross-container service discovery
1302
+ # (e.g., agent containers reaching the open-design daemon by name)
1303
+ ensure_network() {
1304
+ local net="${1:-ai-sandbox}"
1305
+ if ! docker network inspect "$net" >/dev/null 2>&1; then
1306
+ docker network create "$net" >/dev/null 2>&1 || {
1307
+ echo "⚠️ WARNING: failed to create Docker network '$net'" >&2
1308
+ return 1
1309
+ }
1310
+ fi
1311
+ return 0
1312
+ }
1313
+
1003
1314
  # Interactive network selection menu (multi-select)
1004
1315
  show_network_menu() {
1005
1316
  local compose_nets=()
@@ -2597,7 +2908,15 @@ fi
2597
2908
 
2598
2909
  # Nano-brain targeted preflight + auto-repair wrapper
2599
2910
  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]}"
2911
+ # Separate hook from following command with newline so bash parses them
2912
+ # as distinct statements. Without this, the hook's trailing `export -f`
2913
+ # line gets joined with the next `echo` call, producing:
2914
+ # export -f nano_brain_shell_wrapper npx echo ''; echo '...'
2915
+ # which makes bash try to `export -f echo` (a builtin, not a function)
2916
+ # and `export -f ''` (empty name), emitting two harmless but ugly errors:
2917
+ # bash: line 68: export: echo: not a function
2918
+ # bash: line 68: export: : not a function
2919
+ DOCKER_COMMAND[1]="$NANO_BRAIN_SHELL_HOOK"$'\n'"${DOCKER_COMMAND[1]}"
2601
2920
  fi
2602
2921
 
2603
2922
  if [[ "$SHELL_MODE" != "true" ]] && is_nano_brain_command; then
@@ -2739,12 +3058,23 @@ DOCKER_ARGS+=($TOOL_CONFIG_MOUNTS)
2739
3058
  DOCKER_ARGS+=($RG_COMPAT_MOUNT)
2740
3059
  DOCKER_ARGS+=($GIT_MOUNTS)
2741
3060
  DOCKER_ARGS+=($SSH_AGENT_ENV)
3061
+ # Default to ai-sandbox network for service discovery if user didn't specify.
3062
+ # Only add --network if creation succeeded; otherwise Docker uses its default bridge.
3063
+ if [[ -z "$NETWORK_OPTIONS" ]]; then
3064
+ if ensure_network "ai-sandbox" >/dev/null 2>&1; then
3065
+ DOCKER_ARGS+=(--network ai-sandbox)
3066
+ fi
3067
+ fi
2742
3068
  DOCKER_ARGS+=($NETWORK_OPTIONS)
2743
3069
  DOCKER_ARGS+=($DISPLAY_FLAGS)
2744
3070
  DOCKER_ARGS+=($HOST_ACCESS_ARGS)
2745
3071
  DOCKER_ARGS+=($PORT_MAPPINGS)
2746
3072
  DOCKER_ARGS+=($OPENCODE_PASSWORD_ENV)
2747
3073
  DOCKER_ARGS+=(-v "$HOME_DIR":/home/agent)
3074
+ # Auto-mount open-design data volume read-only so agents can read generated artifacts
3075
+ if docker volume inspect ai-open-design-data >/dev/null 2>&1; then
3076
+ DOCKER_ARGS+=(-v "ai-open-design-data:/workspace/.od:ro")
3077
+ fi
2748
3078
  DOCKER_ARGS+=($SHARED_CACHE_MOUNTS)
2749
3079
  DOCKER_ARGS+=($NANO_BRAIN_MOUNT)
2750
3080
  DOCKER_ARGS+=(-w "$CURRENT_DIR")
@@ -2763,5 +3093,41 @@ DOCKER_ARGS+=($TERMINAL_SIZE)
2763
3093
  DOCKER_ARGS+=("$IMAGE")
2764
3094
  DOCKER_ARGS+=("${DOCKER_COMMAND[@]}")
2765
3095
 
3096
+ # Auto-start open-design daemon if image exists but container not running
3097
+ if docker image inspect ai-open-design:latest >/dev/null 2>&1; then
3098
+ if ! docker ps --format '{{.Names}}' | grep -q "^ai-open-design$"; then
3099
+ if [[ -t 0 && -t 1 ]]; then
3100
+ printf "🎨 Open Design daemon is not running. Start it? [Y/n] "
3101
+ read -r OD_ANSWER
3102
+ if [[ ! "$OD_ANSWER" =~ ^[Nn]$ ]]; then
3103
+ OD_ENV_FILE="$HOME/.ai-sandbox/env"
3104
+ OD_NETWORK="ai-sandbox"
3105
+ OD_VOLUME="ai-open-design-data"
3106
+ # Auto-init if token missing
3107
+ if ! grep -q "^OD_API_TOKEN=" "$OD_ENV_FILE" 2>/dev/null; then
3108
+ mkdir -p "$(dirname "$OD_ENV_FILE")"
3109
+ touch "$OD_ENV_FILE"
3110
+ chmod 600 "$OD_ENV_FILE"
3111
+ OD_TOKEN="$(openssl rand -hex 32)"
3112
+ echo "OD_API_TOKEN=$OD_TOKEN" >> "$OD_ENV_FILE"
3113
+ echo "OD_DAEMON_URL=http://ai-open-design:7456" >> "$OD_ENV_FILE"
3114
+ echo "✅ Generated OD_API_TOKEN"
3115
+ fi
3116
+ # Ensure network and volume
3117
+ docker network inspect "$OD_NETWORK" >/dev/null 2>&1 || docker network create "$OD_NETWORK" >/dev/null
3118
+ docker volume inspect "$OD_VOLUME" >/dev/null 2>&1 || docker volume create "$OD_VOLUME" >/dev/null
3119
+ # Remove stopped container if exists
3120
+ if docker ps -a --format '{{.Names}}' | grep -q "^ai-open-design$"; then
3121
+ docker rm ai-open-design >/dev/null 2>&1 || true
3122
+ fi
3123
+ docker run -d --name ai-open-design --network "$OD_NETWORK" \
3124
+ --restart unless-stopped -v "$OD_VOLUME:/app/.od" \
3125
+ --env-file "$OD_ENV_FILE" ai-open-design:latest >/dev/null
3126
+ echo "✅ Open Design daemon started (http://ai-open-design:7456)"
3127
+ fi
3128
+ fi
3129
+ fi
3130
+ fi
3131
+
2766
3132
  # Execute docker run with proper argument handling
2767
3133
  docker run "${DOCKER_ARGS[@]}"
package/bin/cli.js CHANGED
@@ -140,6 +140,8 @@ function runRebuild() {
140
140
  INSTALL_CHROME_DEVTOOLS_MCP: hasMcp('chrome-devtools') ? '1' : '0',
141
141
  INSTALL_PLAYWRIGHT_HOST: useHostChrome ? '1' : '0',
142
142
  INSTALL_RTK: '0',
143
+ INSTALL_PUP: '0',
144
+ INSTALL_OD_HELPERS: '1',
143
145
  INSTALL_SPEC_KIT: '0',
144
146
  INSTALL_UX_UI_PROMAX: '0',
145
147
  INSTALL_OPENSPEC: '0',
@@ -1,3 +1,6 @@
1
+ # Build RTK from source (multi-stage: only binary is kept, Rust toolchain discarded)
2
+ FROM rust:bookworm AS rtk-builder
3
+ RUN cargo install --git https://github.com/rtk-ai/rtk --locked
1
4
 
2
5
  FROM node:22-bookworm-slim
3
6
 
@@ -23,6 +26,30 @@ RUN npm install -g typescript typescript-language-server pyright vscode-langserv
23
26
  RUN node --version && npm --version && pnpm --version && tsc --version
24
27
 
25
28
  # Install additional tools (if selected)
29
+ RUN PIPX_HOME=/opt/pipx PIPX_BIN_DIR=/usr/local/bin pipx install specify-cli --pip-args="git+https://github.com/github/spec-kit.git" && \
30
+ chmod +x /usr/local/bin/specify && \
31
+ ln -sf /usr/local/bin/specify /usr/local/bin/specify-cli
32
+ RUN mkdir -p /usr/local/lib/uipro-cli && \
33
+ cd /usr/local/lib/uipro-cli && \
34
+ npm init -y && \
35
+ npm install uipro-cli && \
36
+ ln -sf /usr/local/lib/uipro-cli/node_modules/.bin/uipro /usr/local/bin/uipro && \
37
+ ln -sf /usr/local/bin/uipro /usr/local/bin/uipro-cli && \
38
+ chmod -R 755 /usr/local/lib/uipro-cli && \
39
+ chmod +x /usr/local/bin/uipro
40
+ RUN mkdir -p /usr/local/lib/openspec && \
41
+ cd /usr/local/lib/openspec && \
42
+ npm init -y && \
43
+ npm install @fission-ai/openspec && \
44
+ ln -sf /usr/local/lib/openspec/node_modules/.bin/openspec /usr/local/bin/openspec && \
45
+ chmod -R 755 /usr/local/lib/openspec && \
46
+ chmod +x /usr/local/bin/openspec
47
+ # Install RTK - token optimizer for AI coding agents (built from source)
48
+ COPY --from=rtk-builder /usr/local/cargo/bin/rtk /usr/local/bin/rtk
49
+ # Install RTK OpenCode skills (auto-discovered by OpenCode agents)
50
+ RUN mkdir -p /home/agent/.config/opencode/skills/rtk /home/agent/.config/opencode/skills/rtk-setup
51
+ COPY skills/rtk/SKILL.md /home/agent/.config/opencode/skills/rtk/SKILL.md
52
+ COPY skills/rtk-setup/SKILL.md /home/agent/.config/opencode/skills/rtk-setup/SKILL.md
26
53
  RUN apt-get update && apt-get install -y --no-install-recommends \
27
54
  libglib2.0-0 \
28
55
  libnspr4 \
@@ -56,11 +83,13 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
56
83
  ENV PLAYWRIGHT_BROWSERS_PATH=/opt/playwright-browsers
57
84
  RUN mkdir -p /opt/playwright-browsers && \
58
85
  npm install -g @playwright/mcp@latest && \
59
- touch /opt/.mcp-playwright-installed
86
+ npx playwright-core install --no-shell chromium && \
87
+ npx playwright-core install-deps chromium && \
88
+ chmod -R 777 /opt/playwright-browsers && \
89
+ ln -sf $(ls -d /opt/playwright-browsers/chromium-*/chrome-linux/chrome | sort -V | tail -1) /opt/chromium
60
90
  ENV CHROME_DEVTOOLS_MCP_NO_USAGE_STATISTICS=1
61
91
  RUN npm install -g chrome-devtools-mcp@latest && \
62
92
  touch /opt/.mcp-chrome-devtools-installed
63
- RUN touch /opt/.mcp-playwright-installed
64
93
 
65
94
  # Create workspace
66
95
  WORKDIR /workspace
@@ -1,3 +1,6 @@
1
+ # Build RTK from source (multi-stage: only binary is kept, Rust toolchain discarded)
2
+ FROM rust:bookworm AS rtk-builder
3
+ RUN cargo install --git https://github.com/rtk-ai/rtk --locked
1
4
 
2
5
  FROM node:22-bookworm-slim
3
6
 
@@ -23,6 +26,30 @@ RUN npm install -g typescript typescript-language-server pyright vscode-langserv
23
26
  RUN node --version && npm --version && pnpm --version && tsc --version
24
27
 
25
28
  # Install additional tools (if selected)
29
+ RUN PIPX_HOME=/opt/pipx PIPX_BIN_DIR=/usr/local/bin pipx install specify-cli --pip-args="git+https://github.com/github/spec-kit.git" && \
30
+ chmod +x /usr/local/bin/specify && \
31
+ ln -sf /usr/local/bin/specify /usr/local/bin/specify-cli
32
+ RUN mkdir -p /usr/local/lib/uipro-cli && \
33
+ cd /usr/local/lib/uipro-cli && \
34
+ npm init -y && \
35
+ npm install uipro-cli && \
36
+ ln -sf /usr/local/lib/uipro-cli/node_modules/.bin/uipro /usr/local/bin/uipro && \
37
+ ln -sf /usr/local/bin/uipro /usr/local/bin/uipro-cli && \
38
+ chmod -R 755 /usr/local/lib/uipro-cli && \
39
+ chmod +x /usr/local/bin/uipro
40
+ RUN mkdir -p /usr/local/lib/openspec && \
41
+ cd /usr/local/lib/openspec && \
42
+ npm init -y && \
43
+ npm install @fission-ai/openspec && \
44
+ ln -sf /usr/local/lib/openspec/node_modules/.bin/openspec /usr/local/bin/openspec && \
45
+ chmod -R 755 /usr/local/lib/openspec && \
46
+ chmod +x /usr/local/bin/openspec
47
+ # Install RTK - token optimizer for AI coding agents (built from source)
48
+ COPY --from=rtk-builder /usr/local/cargo/bin/rtk /usr/local/bin/rtk
49
+ # Install RTK OpenCode skills (auto-discovered by OpenCode agents)
50
+ RUN mkdir -p /home/agent/.config/opencode/skills/rtk /home/agent/.config/opencode/skills/rtk-setup
51
+ COPY skills/rtk/SKILL.md /home/agent/.config/opencode/skills/rtk/SKILL.md
52
+ COPY skills/rtk-setup/SKILL.md /home/agent/.config/opencode/skills/rtk-setup/SKILL.md
26
53
  RUN apt-get update && apt-get install -y --no-install-recommends \
27
54
  libglib2.0-0 \
28
55
  libnspr4 \
@@ -56,11 +83,13 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
56
83
  ENV PLAYWRIGHT_BROWSERS_PATH=/opt/playwright-browsers
57
84
  RUN mkdir -p /opt/playwright-browsers && \
58
85
  npm install -g @playwright/mcp@latest && \
59
- touch /opt/.mcp-playwright-installed
86
+ npx playwright-core install --no-shell chromium && \
87
+ npx playwright-core install-deps chromium && \
88
+ chmod -R 777 /opt/playwright-browsers && \
89
+ ln -sf $(ls -d /opt/playwright-browsers/chromium-*/chrome-linux/chrome | sort -V | tail -1) /opt/chromium
60
90
  ENV CHROME_DEVTOOLS_MCP_NO_USAGE_STATISTICS=1
61
91
  RUN npm install -g chrome-devtools-mcp@latest && \
62
92
  touch /opt/.mcp-chrome-devtools-installed
63
- RUN touch /opt/.mcp-playwright-installed
64
93
 
65
94
  # Create workspace
66
95
  WORKDIR /workspace
@@ -77,13 +106,12 @@ RUN curl -fsSL https://opencode.ai/install | bash && \
77
106
  mv /root/.opencode/bin/opencode /usr/local/bin/opencode && \
78
107
  rm -rf /root/.opencode
79
108
 
80
- # === codex ===
109
+ # === claude ===
81
110
  USER root
82
- RUN mkdir -p /usr/local/lib/codex && \
83
- cd /usr/local/lib/codex && \
84
- bun init -y && \
85
- bun add @openai/codex && \
86
- ln -s /usr/local/lib/codex/node_modules/.bin/codex /usr/local/bin/codex
111
+ RUN export HOME=/root && curl -fsSL https://claude.ai/install.sh | bash && \
112
+ mkdir -p /usr/local/share && \
113
+ mv /root/.local/share/claude /usr/local/share/claude && \
114
+ ln -sf /usr/local/share/claude/versions/$(ls /usr/local/share/claude/versions | head -1) /usr/local/bin/claude
87
115
  USER agent
88
116
 
89
117
  USER agent
@@ -9,7 +9,7 @@ fi
9
9
 
10
10
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
11
  PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
12
- cd "$PROJECT_DIR"
12
+ cd "$PROJECT_DIR" || exit 1
13
13
 
14
14
  SANDBOX_DIR="dockerfiles/sandbox"
15
15
  mkdir -p "$SANDBOX_DIR"
@@ -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}" \
@@ -51,6 +53,7 @@ BASE_PREAMBLE=$(echo "$BASE_CONTENT" | sed '/^USER agent$/,$d')
51
53
  fi
52
54
 
53
55
  echo "# === $tool ==="
56
+ # shellcheck source=/dev/null
54
57
  SNIPPET_MODE=1 source "$INSTALL_SCRIPT"
55
58
  dockerfile_snippet
56
59
  echo ""
@@ -65,6 +68,10 @@ if [[ -d "dockerfiles/base/skills" ]]; then
65
68
  cp -r "dockerfiles/base/skills" "$SANDBOX_DIR/"
66
69
  fi
67
70
 
71
+ if [[ -d "dockerfiles/base/scripts" ]]; then
72
+ cp -r "dockerfiles/base/scripts" "$SANDBOX_DIR/"
73
+ fi
74
+
68
75
  echo "✅ Dockerfile generated at $SANDBOX_DIR/Dockerfile"
69
76
 
70
77
  echo "🔨 Building ai-sandbox:latest..."
@@ -3,8 +3,9 @@ set -e
3
3
 
4
4
  dockerfile_snippet() {
5
5
  cat <<'SNIPPET'
6
+ USER root
7
+ RUN UV_TOOL_BIN_DIR=/usr/local/bin uv tool install aider-chat
6
8
  USER agent
7
- RUN python3 -m pip install --break-system-packages aider-install && aider-install
8
9
  SNIPPET
9
10
  }
10
11
 
@@ -24,9 +25,9 @@ mkdir -p "$HOME/.ai-sandbox/tools/$TOOL/home"
24
25
  # Create Dockerfile (extends base image which has Python)
25
26
  cat <<'EOF' > "dockerfiles/$TOOL/Dockerfile"
26
27
  FROM ai-base:latest
28
+ USER root
29
+ RUN UV_TOOL_BIN_DIR=/usr/local/bin uv tool install aider-chat
27
30
  USER agent
28
- # Install aider via aider-install
29
- RUN python3 -m pip install --break-system-packages aider-install && aider-install
30
31
  ENTRYPOINT ["aider"]
31
32
  EOF
32
33
 
@@ -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
@@ -134,10 +179,7 @@ fi
134
179
 
135
180
  # MCP Tools for AI agent browser automation
136
181
  # Both tools share Playwright's Chromium (native ARM64/x86_64, avoids Puppeteer arch issues)
137
- MCP_BROWSER_INSTALLED=false
138
-
139
182
  if [[ "${INSTALL_CHROME_DEVTOOLS_MCP:-0}" -eq 1 ]] || [[ "${INSTALL_PLAYWRIGHT_MCP:-0}" -eq 1 ]]; then
140
- MCP_BROWSER_INSTALLED=true
141
183
  echo "📦 Installing shared Chromium browser for MCP tools"
142
184
  ADDITIONAL_TOOLS_INSTALL+='RUN apt-get update && apt-get install -y --no-install-recommends \
143
185
  libglib2.0-0 \