@kokorolx/ai-sandbox-wrapper 3.3.0 → 3.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/ai-run CHANGED
@@ -791,6 +791,7 @@ fi
791
791
  HOST_CHROME_CDP=false
792
792
  HOST_CHROME_CDP_PORT=19222
793
793
  PLAYWRIGHT_MCP_NAME=""
794
+ CHROME_DEVTOOLS_MCP_NAME=""
794
795
  # Portable symlink-resolving SCRIPT_DIR (macOS readlink has no -f).
795
796
  # Needed because ai-run is typically invoked via the ~/bin/ai-run symlink.
796
797
  _pmcp_resolve_script_dir() {
@@ -823,6 +824,7 @@ if [[ "$TOOL" == "opencode" ]] && command -v jq &>/dev/null && [[ -f "$AI_SANDBO
823
824
  CONTAINER_HASH=$(echo "$CONTAINER_NAME_VALUE" | md5sum | cut -c1-4)
824
825
  HOST_CHROME_CDP_PORT=$((19222 + 0x$CONTAINER_HASH % 100))
825
826
  PLAYWRIGHT_MCP_NAME="playwright_port_${HOST_CHROME_CDP_PORT}"
827
+ CHROME_DEVTOOLS_MCP_NAME="chrome-devtools_port_${HOST_CHROME_CDP_PORT}"
826
828
 
827
829
  # Reuse-if-alive: probe before launching
828
830
  if pmcp::probe_chrome "$HOST_CHROME_CDP_PORT"; then
@@ -850,23 +852,29 @@ if [[ "$TOOL" == "opencode" ]] && command -v jq &>/dev/null && [[ -f "$AI_SANDBO
850
852
  HOST_CHROME_CDP=false
851
853
  kill "$CHROME_PID" 2>/dev/null || true
852
854
  PLAYWRIGHT_MCP_NAME=""
855
+ CHROME_DEVTOOLS_MCP_NAME=""
853
856
  fi
854
857
  fi
855
858
 
856
- # Locked sweep+append on the shared OpenCode config
859
+ # Locked sweep+register on the shared OpenCode config — both
860
+ # playwright-mcp and chrome-devtools-mcp point at the same host Chrome.
857
861
  if [[ "$HOST_CHROME_CDP" == "true" ]]; then
858
862
  OPENCODE_CONFIG_FILE="$HOME/.config/opencode/opencode.json"
859
863
  LOCK_FILE="$HOME/.config/opencode/.playwright.lock"
860
864
  mkdir -p "$(dirname "$OPENCODE_CONFIG_FILE")"
861
865
  [[ -f "$OPENCODE_CONFIG_FILE" ]] || echo '{}' > "$OPENCODE_CONFIG_FILE"
862
- pmcp::with_lock "$LOCK_FILE" pmcp::sweep_and_append "$OPENCODE_CONFIG_FILE" "$PLAYWRIGHT_MCP_NAME" "$HOST_CHROME_CDP_PORT"
866
+ pmcp::with_lock "$LOCK_FILE" pmcp::register_host_chrome \
867
+ "$OPENCODE_CONFIG_FILE" "$HOST_CHROME_CDP_PORT" \
868
+ "$PLAYWRIGHT_MCP_NAME" "$CHROME_DEVTOOLS_MCP_NAME"
863
869
  rc=$?
864
870
  if [[ "$rc" == "99" ]]; then
865
871
  echo " ⚠️ Could not acquire MCP config lock within 5s; skipping registration."
866
872
  PLAYWRIGHT_MCP_NAME=""
873
+ CHROME_DEVTOOLS_MCP_NAME=""
867
874
  elif [[ "$rc" != "0" ]]; then
868
875
  echo " ⚠️ MCP config update failed (rc=$rc); skipping registration."
869
876
  PLAYWRIGHT_MCP_NAME=""
877
+ CHROME_DEVTOOLS_MCP_NAME=""
870
878
  fi
871
879
  fi
872
880
  fi
@@ -2103,9 +2111,16 @@ configure_opencode_mcp() {
2103
2111
  for tool in "${all_tools[@]}"; do
2104
2112
  case "$tool" in
2105
2113
  chrome-devtools)
2106
- if add_mcp_config "chrome-devtools" "[\"chrome-devtools-mcp\", \"--headless\", \"--isolated\", \"--executablePath\", \"$CHROMIUM_PATH\", \"--chrome-arg=--no-sandbox\"]"; then
2107
- echo " ✓ Configured Chrome DevTools MCP"
2114
+ # Skip static entry under host Chrome mode — per-container
2115
+ # chrome-devtools_port_<port> entry is registered at runtime.
2116
+ if [[ -n "$PLAYWRIGHT_HOST_CHROME" ]] && [[ -f "$PLAYWRIGHT_HOST_CHROME" ]]; then
2117
+ echo " ℹ️ Chrome DevTools MCP entry will be registered per-container at runtime (host Chrome mode)."
2108
2118
  configured_any=true
2119
+ else
2120
+ if add_mcp_config "chrome-devtools" "[\"chrome-devtools-mcp\", \"--headless\", \"--isolated\", \"--executablePath\", \"$CHROMIUM_PATH\", \"--chrome-arg=--no-sandbox\"]"; then
2121
+ echo " ✓ Configured Chrome DevTools MCP"
2122
+ configured_any=true
2123
+ fi
2109
2124
  fi
2110
2125
  ;;
2111
2126
  playwright)
@@ -2147,8 +2162,13 @@ configure_opencode_mcp() {
2147
2162
  if [[ "$tool_choice" =~ ^[Yy]$ ]]; then
2148
2163
  case "$tool" in
2149
2164
  chrome-devtools)
2150
- if add_mcp_config "chrome-devtools" "[\"chrome-devtools-mcp\", \"--headless\", \"--isolated\", \"--executablePath\", \"$CHROMIUM_PATH\", \"--chrome-arg=--no-sandbox\"]"; then
2151
- echo " Configured"
2165
+ if [[ -n "$PLAYWRIGHT_HOST_CHROME" ]] && [[ -f "$PLAYWRIGHT_HOST_CHROME" ]]; then
2166
+ echo " ℹ️ Chrome DevTools MCP entry will be registered per-container at runtime (host Chrome mode)."
2167
+ configured_any=true
2168
+ else
2169
+ if add_mcp_config "chrome-devtools" "[\"chrome-devtools-mcp\", \"--headless\", \"--isolated\", \"--executablePath\", \"$CHROMIUM_PATH\", \"--chrome-arg=--no-sandbox\"]"; then
2170
+ echo " ✓ Configured"
2171
+ fi
2152
2172
  fi
2153
2173
  ;;
2154
2174
  playwright)
@@ -2726,6 +2746,9 @@ if [[ -n "${PLAYWRIGHT_MCP_NAME:-}" ]]; then
2726
2746
  DOCKER_ARGS+=(-e "PLAYWRIGHT_MCP_NAME=$PLAYWRIGHT_MCP_NAME")
2727
2747
  DOCKER_ARGS+=(-e "PLAYWRIGHT_PORT=$HOST_CHROME_CDP_PORT")
2728
2748
  fi
2749
+ if [[ -n "${CHROME_DEVTOOLS_MCP_NAME:-}" ]]; then
2750
+ DOCKER_ARGS+=(-e "CHROME_DEVTOOLS_MCP_NAME=$CHROME_DEVTOOLS_MCP_NAME")
2751
+ fi
2729
2752
  DOCKER_ARGS+=($TERMINAL_SIZE)
2730
2753
 
2731
2754
  DOCKER_ARGS+=("$IMAGE")
@@ -29,6 +29,81 @@ pmcp::probe_chrome() {
29
29
  # Host address from container (Docker Desktop on Mac).
30
30
  PMCP_DOCKER_HOST_IP="${PMCP_DOCKER_HOST_IP:-192.168.65.254}"
31
31
 
32
+ # Probe and remove .mcp.<prefix>* entries whose CDP port is no longer alive.
33
+ # Used to garbage-collect stale per-container Chrome MCP entries.
34
+ # Args: $1 = config file path, $2 = key prefix (e.g. "playwright_", "chrome-devtools_")
35
+ pmcp::sweep_dead() {
36
+ local cfg="$1" prefix="$2"
37
+ [[ -f "$cfg" ]] || return 0
38
+
39
+ local keys dead_keys=()
40
+ keys=$(jq -r --arg p "$prefix" '(.mcp // {}) | keys[] | select(startswith($p))' "$cfg" 2>/dev/null || true)
41
+ while IFS= read -r key; do
42
+ [[ -z "$key" ]] && continue
43
+ # Find an http:// or ws:// URL in the command — any flag name works.
44
+ local cmd_url
45
+ cmd_url=$(jq -r --arg k "$key" \
46
+ '.mcp[$k].command[]? | select(startswith("http://") or startswith("ws://"))' \
47
+ "$cfg" 2>/dev/null | head -1)
48
+ [[ -z "$cmd_url" ]] && continue
49
+ # Extract port from http://host:port[/path] or ws://host:port[/path]
50
+ local hostport="${cmd_url#*://}" # host:port[/path]
51
+ hostport="${hostport%%/*}" # host:port
52
+ local entry_port="${hostport##*:}" # port
53
+ if ! pmcp::probe_chrome "$entry_port"; then
54
+ dead_keys+=("$key")
55
+ fi
56
+ done <<< "$keys"
57
+
58
+ (( ${#dead_keys[@]} == 0 )) && return 0
59
+
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[@]}"}" \
68
+ 'reduce ([$ARGS.named | to_entries[] | select(.key|startswith("k")) | .value][]) as $k (.; del(.mcp[$k]))' \
69
+ "$cfg" > "$tmp"
70
+ mv "$tmp" "$cfg"
71
+ chmod 600 "$cfg"
72
+
73
+ echo " 🧹 pmcp: removed ${#dead_keys[@]} stale ${prefix}* entr$([ ${#dead_keys[@]} -eq 1 ] && echo y || echo ies): ${dead_keys[*]}"
74
+ }
75
+
76
+ # Register (or overwrite) an MCP entry.
77
+ # Args: $1 = config file path, $2 = key, $3 = command as JSON array string
78
+ # e.g. pmcp::register cfg playwright_port_19222 '["playwright-mcp","--cdp-endpoint","http://192.168.65.254:19222"]'
79
+ pmcp::register() {
80
+ local cfg="$1" key="$2" cmd_json="$3"
81
+ [[ -f "$cfg" ]] || echo '{}' > "$cfg"
82
+ local tmp="$cfg.tmp.$$"
83
+ jq --arg key "$key" --argjson cmd "$cmd_json" \
84
+ '.mcp = (.mcp // {}) | .mcp[$key] = {"type":"local","command":$cmd}' \
85
+ "$cfg" > "$tmp"
86
+ mv "$tmp" "$cfg"
87
+ chmod 600 "$cfg"
88
+ echo " ➕ pmcp: registered $key"
89
+ }
90
+
91
+ # Register both playwright-mcp and chrome-devtools-mcp for a host Chrome on
92
+ # a given CDP port. Sweeps dead entries of both prefixes first, then writes
93
+ # the new entries. MUST be called inside a flock.
94
+ # Args: $1 = cfg, $2 = port, $3 = playwright key, $4 = chrome-devtools key
95
+ pmcp::register_host_chrome() {
96
+ local cfg="$1" port="$2" pw_key="$3" cd_key="$4"
97
+ local url="http://$PMCP_DOCKER_HOST_IP:$port"
98
+
99
+ pmcp::sweep_dead "$cfg" "playwright_"
100
+ pmcp::sweep_dead "$cfg" "chrome-devtools_"
101
+ pmcp::register "$cfg" "$pw_key" \
102
+ "[\"playwright-mcp\",\"--cdp-endpoint\",\"$url\"]"
103
+ pmcp::register "$cfg" "$cd_key" \
104
+ "[\"chrome-devtools-mcp\",\"--browserUrl\",\"$url\"]"
105
+ }
106
+
32
107
  # Sweep dead playwright_* entries and append a new one. MUST be called inside
33
108
  # a flock by the caller. Does not acquire the lock itself, by design — locking
34
109
  # happens around a larger critical section in the caller.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kokorolx/ai-sandbox-wrapper",
3
- "version": "3.3.0",
3
+ "version": "3.4.0",
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",