@kokorolx/ai-sandbox-wrapper 3.3.0 → 3.4.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
@@ -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() {
@@ -822,7 +823,17 @@ if [[ "$TOOL" == "opencode" ]] && command -v jq &>/dev/null && [[ -f "$AI_SANDBO
822
823
  # Deterministic port per container name
823
824
  CONTAINER_HASH=$(echo "$CONTAINER_NAME_VALUE" | md5sum | cut -c1-4)
824
825
  HOST_CHROME_CDP_PORT=$((19222 + 0x$CONTAINER_HASH % 100))
825
- PLAYWRIGHT_MCP_NAME="playwright_port_${HOST_CHROME_CDP_PORT}"
826
+
827
+ # Only register entries for MCP binaries actually present in the image
828
+ # (tracked in $AI_SANDBOX_CONFIG by setup.sh). Otherwise opencode would
829
+ # try to spawn a missing binary and fail. (is_mcp_installed is defined
830
+ # later in the file, so use jq inline here.)
831
+ if jq -e '.mcp.installed // [] | index("playwright") != null' "$AI_SANDBOX_CONFIG" &>/dev/null; then
832
+ PLAYWRIGHT_MCP_NAME="playwright_port_${HOST_CHROME_CDP_PORT}"
833
+ fi
834
+ if jq -e '.mcp.installed // [] | index("chrome-devtools") != null' "$AI_SANDBOX_CONFIG" &>/dev/null; then
835
+ CHROME_DEVTOOLS_MCP_NAME="chrome-devtools_port_${HOST_CHROME_CDP_PORT}"
836
+ fi
826
837
 
827
838
  # Reuse-if-alive: probe before launching
828
839
  if pmcp::probe_chrome "$HOST_CHROME_CDP_PORT"; then
@@ -850,23 +861,29 @@ if [[ "$TOOL" == "opencode" ]] && command -v jq &>/dev/null && [[ -f "$AI_SANDBO
850
861
  HOST_CHROME_CDP=false
851
862
  kill "$CHROME_PID" 2>/dev/null || true
852
863
  PLAYWRIGHT_MCP_NAME=""
864
+ CHROME_DEVTOOLS_MCP_NAME=""
853
865
  fi
854
866
  fi
855
867
 
856
- # Locked sweep+append on the shared OpenCode config
868
+ # Locked sweep+register on the shared OpenCode config — both
869
+ # playwright-mcp and chrome-devtools-mcp point at the same host Chrome.
857
870
  if [[ "$HOST_CHROME_CDP" == "true" ]]; then
858
871
  OPENCODE_CONFIG_FILE="$HOME/.config/opencode/opencode.json"
859
872
  LOCK_FILE="$HOME/.config/opencode/.playwright.lock"
860
873
  mkdir -p "$(dirname "$OPENCODE_CONFIG_FILE")"
861
874
  [[ -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"
875
+ pmcp::with_lock "$LOCK_FILE" pmcp::register_host_chrome \
876
+ "$OPENCODE_CONFIG_FILE" "$HOST_CHROME_CDP_PORT" \
877
+ "$PLAYWRIGHT_MCP_NAME" "$CHROME_DEVTOOLS_MCP_NAME"
863
878
  rc=$?
864
879
  if [[ "$rc" == "99" ]]; then
865
880
  echo " ⚠️ Could not acquire MCP config lock within 5s; skipping registration."
866
881
  PLAYWRIGHT_MCP_NAME=""
882
+ CHROME_DEVTOOLS_MCP_NAME=""
867
883
  elif [[ "$rc" != "0" ]]; then
868
884
  echo " ⚠️ MCP config update failed (rc=$rc); skipping registration."
869
885
  PLAYWRIGHT_MCP_NAME=""
886
+ CHROME_DEVTOOLS_MCP_NAME=""
870
887
  fi
871
888
  fi
872
889
  fi
@@ -2103,9 +2120,16 @@ configure_opencode_mcp() {
2103
2120
  for tool in "${all_tools[@]}"; do
2104
2121
  case "$tool" in
2105
2122
  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"
2123
+ # Skip static entry under host Chrome mode — per-container
2124
+ # chrome-devtools_port_<port> entry is registered at runtime.
2125
+ if [[ -n "$PLAYWRIGHT_HOST_CHROME" ]] && [[ -f "$PLAYWRIGHT_HOST_CHROME" ]]; then
2126
+ echo " ℹ️ Chrome DevTools MCP entry will be registered per-container at runtime (host Chrome mode)."
2108
2127
  configured_any=true
2128
+ else
2129
+ if add_mcp_config "chrome-devtools" "[\"chrome-devtools-mcp\", \"--headless\", \"--isolated\", \"--executablePath\", \"$CHROMIUM_PATH\", \"--chrome-arg=--no-sandbox\"]"; then
2130
+ echo " ✓ Configured Chrome DevTools MCP"
2131
+ configured_any=true
2132
+ fi
2109
2133
  fi
2110
2134
  ;;
2111
2135
  playwright)
@@ -2147,8 +2171,13 @@ configure_opencode_mcp() {
2147
2171
  if [[ "$tool_choice" =~ ^[Yy]$ ]]; then
2148
2172
  case "$tool" in
2149
2173
  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"
2174
+ if [[ -n "$PLAYWRIGHT_HOST_CHROME" ]] && [[ -f "$PLAYWRIGHT_HOST_CHROME" ]]; then
2175
+ echo " ℹ️ Chrome DevTools MCP entry will be registered per-container at runtime (host Chrome mode)."
2176
+ configured_any=true
2177
+ else
2178
+ if add_mcp_config "chrome-devtools" "[\"chrome-devtools-mcp\", \"--headless\", \"--isolated\", \"--executablePath\", \"$CHROMIUM_PATH\", \"--chrome-arg=--no-sandbox\"]"; then
2179
+ echo " ✓ Configured"
2180
+ fi
2152
2181
  fi
2153
2182
  ;;
2154
2183
  playwright)
@@ -2726,6 +2755,9 @@ if [[ -n "${PLAYWRIGHT_MCP_NAME:-}" ]]; then
2726
2755
  DOCKER_ARGS+=(-e "PLAYWRIGHT_MCP_NAME=$PLAYWRIGHT_MCP_NAME")
2727
2756
  DOCKER_ARGS+=(-e "PLAYWRIGHT_PORT=$HOST_CHROME_CDP_PORT")
2728
2757
  fi
2758
+ if [[ -n "${CHROME_DEVTOOLS_MCP_NAME:-}" ]]; then
2759
+ DOCKER_ARGS+=(-e "CHROME_DEVTOOLS_MCP_NAME=$CHROME_DEVTOOLS_MCP_NAME")
2760
+ fi
2729
2761
  DOCKER_ARGS+=($TERMINAL_SIZE)
2730
2762
 
2731
2763
  DOCKER_ARGS+=("$IMAGE")
@@ -29,6 +29,86 @@ 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 playwright-mcp and/or chrome-devtools-mcp for a host Chrome on
92
+ # a given CDP port. Sweeps dead entries of both prefixes first, then writes
93
+ # the requested entries. Pass empty string for a key to skip that one.
94
+ # MUST be called inside a flock.
95
+ # Args: $1 = cfg, $2 = port, $3 = playwright key (or ""), $4 = chrome-devtools key (or "")
96
+ pmcp::register_host_chrome() {
97
+ local cfg="$1" port="$2" pw_key="$3" cd_key="$4"
98
+ local url="http://$PMCP_DOCKER_HOST_IP:$port"
99
+
100
+ pmcp::sweep_dead "$cfg" "playwright_"
101
+ pmcp::sweep_dead "$cfg" "chrome-devtools_"
102
+ if [[ -n "$pw_key" ]]; then
103
+ pmcp::register "$cfg" "$pw_key" \
104
+ "[\"playwright-mcp\",\"--cdp-endpoint\",\"$url\"]"
105
+ fi
106
+ if [[ -n "$cd_key" ]]; then
107
+ pmcp::register "$cfg" "$cd_key" \
108
+ "[\"chrome-devtools-mcp\",\"--browserUrl\",\"$url\"]"
109
+ fi
110
+ }
111
+
32
112
  # Sweep dead playwright_* entries and append a new one. MUST be called inside
33
113
  # a flock by the caller. Does not acquire the lock itself, by design — locking
34
114
  # 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.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",