@kokorolx/ai-sandbox-wrapper 3.2.0 โ†’ 3.3.0-beta.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
@@ -783,6 +783,83 @@ if [[ -d "$HOST_SKILLS_DIR" ]]; then
783
783
  SHARED_CACHE_MOUNTS="$SHARED_CACHE_MOUNTS -v $HOST_SKILLS_DIR:/home/agent/.config/opencode/skills:ro"
784
784
  fi
785
785
 
786
+ # Host Chrome for Playwright MCP (via CDP - Chrome DevTools Protocol)
787
+ # NOTE: macOS Chrome binary (Mach-O) cannot run inside a Linux container.
788
+ # Instead, we launch Chrome on the host with --remote-debugging-port and
789
+ # connect from the container via CDP. Each container gets its own port and
790
+ # its own MCP entry; entries are sweep-cleaned on every start.
791
+ HOST_CHROME_CDP=false
792
+ HOST_CHROME_CDP_PORT=19222
793
+ PLAYWRIGHT_MCP_NAME=""
794
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
795
+ # shellcheck source=lib/playwright-mcp-config.sh
796
+ [[ -f "$SCRIPT_DIR/lib/playwright-mcp-config.sh" ]] && source "$SCRIPT_DIR/lib/playwright-mcp-config.sh"
797
+
798
+ if [[ "$TOOL" == "opencode" ]] && command -v jq &>/dev/null && [[ -f "$AI_SANDBOX_CONFIG" ]] && declare -f pmcp::sanitize_name >/dev/null; then
799
+ PLAYWRIGHT_HOST_CHROME=$(jq -r '.mcp.chromePath // empty' "$AI_SANDBOX_CONFIG" 2>/dev/null)
800
+ if [[ -n "$PLAYWRIGHT_HOST_CHROME" ]] && [[ -f "$PLAYWRIGHT_HOST_CHROME" ]]; then
801
+ HOST_CHROME_CDP=true
802
+ echo "๐ŸŒ Host Chrome CDP mode: $PLAYWRIGHT_HOST_CHROME"
803
+
804
+ # CONTAINER_NAME has the form "--name foo" or is empty. Extract the value
805
+ # for hashing only โ€” the MCP key uses just the port (containers that
806
+ # collide on a port intentionally share the same Chrome / MCP entry).
807
+ CONTAINER_NAME_VALUE="${CONTAINER_NAME#--name }"
808
+ [[ "$CONTAINER_NAME_VALUE" == "$CONTAINER_NAME" ]] && CONTAINER_NAME_VALUE="anon-$$"
809
+
810
+ # Deterministic port per container name
811
+ CONTAINER_HASH=$(echo "$CONTAINER_NAME_VALUE" | md5sum | cut -c1-4)
812
+ HOST_CHROME_CDP_PORT=$((19222 + 0x$CONTAINER_HASH % 100))
813
+ PLAYWRIGHT_MCP_NAME="playwright_port_${HOST_CHROME_CDP_PORT}"
814
+
815
+ # Reuse-if-alive: probe before launching
816
+ if pmcp::probe_chrome "$HOST_CHROME_CDP_PORT"; then
817
+ echo " โœ… Chrome already running on port $HOST_CHROME_CDP_PORT (reusing)"
818
+ else
819
+ echo " ๐Ÿš€ Launching Chrome with remote debugging on port $HOST_CHROME_CDP_PORT..."
820
+ mkdir -p "$SANDBOX_DIR/chrome-profile-$HOST_CHROME_CDP_PORT"
821
+ "$PLAYWRIGHT_HOST_CHROME" \
822
+ --remote-debugging-port="$HOST_CHROME_CDP_PORT" \
823
+ --user-data-dir="$SANDBOX_DIR/chrome-profile-$HOST_CHROME_CDP_PORT" \
824
+ --no-first-run \
825
+ --no-default-browser-check \
826
+ &>/dev/null &
827
+ CHROME_PID=$!
828
+ for i in {1..20}; do
829
+ if pmcp::probe_chrome "$HOST_CHROME_CDP_PORT"; then
830
+ echo " โœ… Chrome ready (PID: $CHROME_PID, port: $HOST_CHROME_CDP_PORT)"
831
+ echo " ๐Ÿ‘€ You can watch the browser window to see what the AI is doing"
832
+ break
833
+ fi
834
+ sleep 0.25
835
+ done
836
+ if ! pmcp::probe_chrome "$HOST_CHROME_CDP_PORT"; then
837
+ echo " โš ๏ธ Chrome failed to start. Falling back to container Chromium."
838
+ HOST_CHROME_CDP=false
839
+ kill "$CHROME_PID" 2>/dev/null || true
840
+ PLAYWRIGHT_MCP_NAME=""
841
+ fi
842
+ fi
843
+
844
+ # Locked sweep+append on the shared OpenCode config
845
+ if [[ "$HOST_CHROME_CDP" == "true" ]]; then
846
+ OPENCODE_CONFIG_FILE="$HOME/.config/opencode/opencode.json"
847
+ LOCK_FILE="$HOME/.config/opencode/.playwright.lock"
848
+ mkdir -p "$(dirname "$OPENCODE_CONFIG_FILE")"
849
+ [[ -f "$OPENCODE_CONFIG_FILE" ]] || echo '{}' > "$OPENCODE_CONFIG_FILE"
850
+ pmcp::with_lock "$LOCK_FILE" pmcp::sweep_and_append "$OPENCODE_CONFIG_FILE" "$PLAYWRIGHT_MCP_NAME" "$HOST_CHROME_CDP_PORT"
851
+ rc=$?
852
+ if [[ "$rc" == "99" ]]; then
853
+ echo " โš ๏ธ Could not acquire MCP config lock within 5s; skipping registration."
854
+ PLAYWRIGHT_MCP_NAME=""
855
+ elif [[ "$rc" != "0" ]]; then
856
+ echo " โš ๏ธ MCP config update failed (rc=$rc); skipping registration."
857
+ PLAYWRIGHT_MCP_NAME=""
858
+ fi
859
+ fi
860
+ fi
861
+ fi
862
+
786
863
  # Nano-brain mount: writable so container can modify config, write memory, logs, etc.
787
864
  NANO_BRAIN_MOUNT=""
788
865
  if [[ -d "$HOME/.nano-brain" ]]; then
@@ -1921,6 +1998,12 @@ configure_opencode_mcp() {
1921
1998
  is_mcp_configured "playwright" && playwright_configured=true
1922
1999
  fi
1923
2000
 
2001
+ # Get host Chrome path if using playwright-host
2002
+ local PLAYWRIGHT_HOST_CHROME=""
2003
+ if command -v jq &>/dev/null && [[ -f "$AI_SANDBOX_CONFIG" ]]; then
2004
+ PLAYWRIGHT_HOST_CHROME=$(jq -r '.mcp.chromePath // empty' "$AI_SANDBOX_CONFIG" 2>/dev/null)
2005
+ fi
2006
+
1924
2007
  # If no MCP tools installed in image, return
1925
2008
  if [[ "$chrome_installed" == "false" && "$playwright_installed" == "false" ]]; then
1926
2009
  return 0
@@ -1957,7 +2040,11 @@ configure_opencode_mcp() {
1957
2040
  echo " โœ“ Chrome DevTools MCP"
1958
2041
  ;;
1959
2042
  playwright)
1960
- echo " โœ“ Playwright MCP"
2043
+ if [[ -n "$PLAYWRIGHT_HOST_CHROME" ]] && [[ -f "$PLAYWRIGHT_HOST_CHROME" ]]; then
2044
+ echo " โœ“ Playwright MCP (host Chrome)"
2045
+ else
2046
+ echo " โœ“ Playwright MCP"
2047
+ fi
1961
2048
  ;;
1962
2049
  esac
1963
2050
  done
@@ -1973,7 +2060,11 @@ configure_opencode_mcp() {
1973
2060
  echo " โ€ข Chrome DevTools MCP - browser automation + performance profiling"
1974
2061
  ;;
1975
2062
  playwright)
1976
- echo " โ€ข Playwright MCP - multi-browser automation"
2063
+ if [[ -n "$PLAYWRIGHT_HOST_CHROME" ]] && [[ -f "$PLAYWRIGHT_HOST_CHROME" ]]; then
2064
+ echo " โ€ข Playwright MCP (host Chrome) - use your installed Chrome browser"
2065
+ else
2066
+ echo " โ€ข Playwright MCP - multi-browser automation"
2067
+ fi
1977
2068
  ;;
1978
2069
  esac
1979
2070
  done
@@ -2006,9 +2097,16 @@ configure_opencode_mcp() {
2006
2097
  fi
2007
2098
  ;;
2008
2099
  playwright)
2009
- if add_mcp_config "playwright" '["playwright-mcp", "--headless", "--browser", "chromium"]'; then
2010
- echo " โœ“ Configured Playwright MCP"
2100
+ # Check if using host Chrome via CDP
2101
+ if [[ -n "$PLAYWRIGHT_HOST_CHROME" ]] && [[ -f "$PLAYWRIGHT_HOST_CHROME" ]]; then
2102
+ echo " โ„น๏ธ Playwright MCP entry will be registered per-container at runtime (host Chrome mode)."
2011
2103
  configured_any=true
2104
+ else
2105
+ # Use container Chromium
2106
+ if add_mcp_config "playwright" '["playwright-mcp", "--headless", "--browser", "chromium"]'; then
2107
+ echo " โœ“ Configured Playwright MCP"
2108
+ configured_any=true
2109
+ fi
2012
2110
  fi
2013
2111
  ;;
2014
2112
  esac
@@ -2042,9 +2140,16 @@ configure_opencode_mcp() {
2042
2140
  fi
2043
2141
  ;;
2044
2142
  playwright)
2045
- if add_mcp_config "playwright" '["playwright-mcp", "--headless", "--browser", "chromium"]'; then
2046
- echo " โœ“ Configured"
2143
+ # Check if using host Chrome via CDP
2144
+ if [[ -n "$PLAYWRIGHT_HOST_CHROME" ]] && [[ -f "$PLAYWRIGHT_HOST_CHROME" ]]; then
2145
+ echo " โ„น๏ธ Playwright MCP entry will be registered per-container at runtime (host Chrome mode)."
2047
2146
  configured_any=true
2147
+ else
2148
+ # Use container Chromium
2149
+ if add_mcp_config "playwright" '["playwright-mcp", "--headless", "--browser", "chromium"]'; then
2150
+ echo " โœ“ Configured"
2151
+ configured_any=true
2152
+ fi
2048
2153
  fi
2049
2154
  ;;
2050
2155
  esac
@@ -2581,27 +2686,38 @@ EOF
2581
2686
  up --remove-orphans
2582
2687
  fi
2583
2688
 
2584
- docker run $CONTAINER_NAME --rm $TTY_FLAGS \
2585
- --init \
2586
- --platform "$PLATFORM" \
2587
- $ENTRYPOINT_OVERRIDE \
2588
- $VOLUME_MOUNTS \
2589
- $CONFIG_MOUNT \
2590
- $TOOL_CONFIG_MOUNTS \
2591
- $RG_COMPAT_MOUNT \
2592
- $GIT_MOUNTS \
2593
- $SSH_AGENT_ENV \
2594
- $NETWORK_OPTIONS \
2595
- $DISPLAY_FLAGS \
2596
- $HOST_ACCESS_ARGS \
2597
- $PORT_MAPPINGS \
2598
- $OPENCODE_PASSWORD_ENV \
2599
- -v "$HOME_DIR":/home/agent \
2600
- $SHARED_CACHE_MOUNTS \
2601
- $NANO_BRAIN_MOUNT \
2602
- -w "$CURRENT_DIR" \
2603
- --env-file "$ENV_FILE" \
2604
- -e TERM="$TERM" \
2605
- -e COLORTERM="$COLORTERM" \
2606
- $TERMINAL_SIZE \
2607
- "$IMAGE" "${DOCKER_COMMAND[@]}"
2689
+ # Build docker run arguments as an array (handles paths with spaces correctly)
2690
+ DOCKER_ARGS=()
2691
+ DOCKER_ARGS+=($CONTAINER_NAME --rm $TTY_FLAGS)
2692
+ DOCKER_ARGS+=(--init)
2693
+ DOCKER_ARGS+=(--platform "$PLATFORM")
2694
+ DOCKER_ARGS+=($ENTRYPOINT_OVERRIDE)
2695
+ DOCKER_ARGS+=($VOLUME_MOUNTS)
2696
+ DOCKER_ARGS+=($CONFIG_MOUNT)
2697
+ DOCKER_ARGS+=($TOOL_CONFIG_MOUNTS)
2698
+ DOCKER_ARGS+=($RG_COMPAT_MOUNT)
2699
+ DOCKER_ARGS+=($GIT_MOUNTS)
2700
+ DOCKER_ARGS+=($SSH_AGENT_ENV)
2701
+ DOCKER_ARGS+=($NETWORK_OPTIONS)
2702
+ DOCKER_ARGS+=($DISPLAY_FLAGS)
2703
+ DOCKER_ARGS+=($HOST_ACCESS_ARGS)
2704
+ DOCKER_ARGS+=($PORT_MAPPINGS)
2705
+ DOCKER_ARGS+=($OPENCODE_PASSWORD_ENV)
2706
+ DOCKER_ARGS+=(-v "$HOME_DIR":/home/agent)
2707
+ DOCKER_ARGS+=($SHARED_CACHE_MOUNTS)
2708
+ DOCKER_ARGS+=($NANO_BRAIN_MOUNT)
2709
+ DOCKER_ARGS+=(-w "$CURRENT_DIR")
2710
+ DOCKER_ARGS+=(--env-file "$ENV_FILE")
2711
+ DOCKER_ARGS+=(-e TERM="$TERM")
2712
+ DOCKER_ARGS+=(-e COLORTERM="$COLORTERM")
2713
+ if [[ -n "${PLAYWRIGHT_MCP_NAME:-}" ]]; then
2714
+ DOCKER_ARGS+=(-e "PLAYWRIGHT_MCP_NAME=$PLAYWRIGHT_MCP_NAME")
2715
+ DOCKER_ARGS+=(-e "PLAYWRIGHT_PORT=$HOST_CHROME_CDP_PORT")
2716
+ fi
2717
+ DOCKER_ARGS+=($TERMINAL_SIZE)
2718
+
2719
+ DOCKER_ARGS+=("$IMAGE")
2720
+ DOCKER_ARGS+=("${DOCKER_COMMAND[@]}")
2721
+
2722
+ # Execute docker run with proper argument handling
2723
+ docker run "${DOCKER_ARGS[@]}"
@@ -170,13 +170,25 @@ if [[ "${INSTALL_CHROME_DEVTOOLS_MCP:-0}" -eq 1 ]] || [[ "${INSTALL_PLAYWRIGHT_M
170
170
  wget \
171
171
  && rm -rf /var/lib/apt/lists/*
172
172
  ENV PLAYWRIGHT_BROWSERS_PATH=/opt/playwright-browsers
173
- RUN mkdir -p /opt/playwright-browsers && \
173
+ '
174
+
175
+ # Only install Chromium if not using host Chrome
176
+ if [[ "${INSTALL_PLAYWRIGHT_HOST:-0}" -eq 1 ]]; then
177
+ echo " ๐Ÿ“ฆ Using host Chrome - skipping Chromium installation"
178
+ ADDITIONAL_TOOLS_INSTALL+='RUN mkdir -p /opt/playwright-browsers && \
179
+ npm install -g @playwright/mcp@latest && \
180
+ touch /opt/.mcp-playwright-installed
181
+ '
182
+ else
183
+ echo " ๐Ÿ“ฆ Installing Chromium browser for MCP tools"
184
+ ADDITIONAL_TOOLS_INSTALL+='RUN mkdir -p /opt/playwright-browsers && \
174
185
  npm install -g @playwright/mcp@latest && \
175
186
  npx playwright-core install --no-shell chromium && \
176
187
  npx playwright-core install-deps chromium && \
177
188
  chmod -R 777 /opt/playwright-browsers && \
178
189
  ln -sf $(ls -d /opt/playwright-browsers/chromium-*/chrome-linux/chrome | sort -V | tail -1) /opt/chromium
179
190
  '
191
+ fi
180
192
  fi
181
193
 
182
194
  if [[ "${INSTALL_CHROME_DEVTOOLS_MCP:-0}" -eq 1 ]]; then
@@ -0,0 +1,118 @@
1
+ #!/usr/bin/env bash
2
+ # Helpers for managing per-container Playwright MCP entries in the shared
3
+ # OpenCode config (~/.config/opencode/opencode.json). All functions are pure
4
+ # except where noted. Callers are responsible for holding the flock around
5
+ # pmcp::sweep_and_append.
6
+
7
+ # Replace any character outside [A-Za-z0-9_-] with underscore. Empty input
8
+ # becomes "unnamed". Used to keep MCP keys jq-safe.
9
+ pmcp::sanitize_name() {
10
+ local input="${1:-}"
11
+ if [[ -z "$input" ]]; then
12
+ echo "unnamed"
13
+ return
14
+ fi
15
+ printf '%s' "$input" | tr -c 'A-Za-z0-9_-' '_'
16
+ }
17
+
18
+ # Probe a port for a valid Chrome CDP endpoint. Returns 0 if /json/version
19
+ # responds with JSON containing a "Browser" field within the timeout.
20
+ # Args: $1 = port
21
+ pmcp::probe_chrome() {
22
+ local port="$1"
23
+ local body
24
+ body=$(curl -fsS --max-time 0.5 "http://localhost:$port/json/version" 2>/dev/null) || return 1
25
+ [[ "$body" == *'"Browser"'* ]] || return 1
26
+ return 0
27
+ }
28
+
29
+ # Host address from container (Docker Desktop on Mac).
30
+ PMCP_DOCKER_HOST_IP="${PMCP_DOCKER_HOST_IP:-192.168.65.254}"
31
+
32
+ # Sweep dead playwright_* entries and append a new one. MUST be called inside
33
+ # a flock by the caller. Does not acquire the lock itself, by design โ€” locking
34
+ # happens around a larger critical section in the caller.
35
+ # Args: $1 = config file path, $2 = full MCP key (e.g. playwright_foo_19223), $3 = port
36
+ pmcp::sweep_and_append() {
37
+ local cfg="$1" name="$2" port="$3"
38
+
39
+ if [[ ! -f "$cfg" ]]; then
40
+ echo " โš ๏ธ pmcp: config file not found: $cfg" >&2
41
+ return 1
42
+ fi
43
+
44
+ # Collect dead keys
45
+ local keys dead_keys=()
46
+ keys=$(jq -r '(.mcp // {}) | keys[] | select(startswith("playwright_"))' "$cfg" 2>/dev/null || true)
47
+ while IFS= read -r key; do
48
+ [[ -z "$key" ]] && continue
49
+ local cmd_url
50
+ cmd_url=$(jq -r --arg k "$key" '.mcp[$k].command[]? | select(startswith("http://"))' "$cfg" 2>/dev/null | head -1)
51
+ [[ -z "$cmd_url" ]] && continue
52
+ local entry_port="${cmd_url##*:}"
53
+ if ! pmcp::probe_chrome "$entry_port"; then
54
+ dead_keys+=("$key")
55
+ fi
56
+ done <<< "$keys"
57
+
58
+ # Build --arg flags for each dead key, then run a single jq invocation
59
+ # that deletes them all and appends the new entry.
60
+ local tmp="$cfg.tmp.$$"
61
+ local args=()
62
+ local i=0
63
+ for k in "${dead_keys[@]+"${dead_keys[@]}"}"; do
64
+ args+=(--arg "k$i" "$k")
65
+ i=$((i + 1))
66
+ done
67
+ jq "${args[@]+"${args[@]}"}" --arg name "$name" --arg host "$PMCP_DOCKER_HOST_IP" --arg port "$port" \
68
+ '
69
+ def setup($args; $name; $host; $port):
70
+ .mcp = (.mcp // {})
71
+ | reduce ($args[]) as $k (.; del(.mcp[$k]))
72
+ | .mcp[$name] = {"type":"local","command":["playwright-mcp","--cdp-endpoint","http://" + $host + ":" + $port]};
73
+ setup([$ARGS.named | to_entries[] | select(.key|startswith("k")) | .value]; $name; $host; $port)
74
+ ' "$cfg" > "$tmp"
75
+
76
+ mv "$tmp" "$cfg"
77
+ chmod 600 "$cfg"
78
+
79
+ if (( ${#dead_keys[@]} > 0 )); then
80
+ echo " ๐Ÿงน pmcp: removed ${#dead_keys[@]} stale entr$([ ${#dead_keys[@]} -eq 1 ] && echo y || echo ies): ${dead_keys[*]}"
81
+ fi
82
+ echo " โž• pmcp: registered $name โ†’ http://$PMCP_DOCKER_HOST_IP:$port"
83
+ }
84
+
85
+ # Run a command while holding an exclusive lock on $1. Uses flock(1) if available,
86
+ # else falls back to a mkdir-based mutex (portable across macOS where flock is
87
+ # not built-in). Times out after 5 seconds; on timeout, returns 99 without
88
+ # running the command. Returns the command's exit status otherwise.
89
+ # Args: $1 = lockfile path, $2... = command + args
90
+ pmcp::with_lock() {
91
+ local lockfile="$1"; shift
92
+ local timeout=5
93
+
94
+ if command -v flock >/dev/null 2>&1; then
95
+ (
96
+ flock -w "$timeout" 9 || exit 99
97
+ "$@"
98
+ ) 9>"$lockfile"
99
+ return $?
100
+ fi
101
+
102
+ # mkdir-based fallback. mkdir is atomic on POSIX filesystems.
103
+ local mutex="${lockfile}.d"
104
+ local waited=0
105
+ while ! mkdir "$mutex" 2>/dev/null; do
106
+ if (( waited >= timeout * 10 )); then
107
+ return 99
108
+ fi
109
+ sleep 0.1
110
+ waited=$((waited + 1))
111
+ done
112
+ trap "rmdir '$mutex' 2>/dev/null || true" EXIT
113
+ "$@"
114
+ local rc=$?
115
+ rmdir "$mutex" 2>/dev/null || true
116
+ trap - EXIT
117
+ return $rc
118
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kokorolx/ai-sandbox-wrapper",
3
- "version": "3.2.0",
3
+ "version": "3.3.0-beta.1",
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",
@@ -23,7 +23,9 @@
23
23
  },
24
24
  "license": "MIT",
25
25
  "author": "kokorolx",
26
- "bin": "./bin/cli.js",
26
+ "bin": {
27
+ "ai-sandbox-wrapper": "bin/cli.js"
28
+ },
27
29
  "files": [
28
30
  "bin/",
29
31
  "lib/",