@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 +146 -30
- package/lib/install-base.sh +13 -1
- package/lib/playwright-mcp-config.sh +118 -0
- package/package.json +4 -2
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2010
|
-
|
|
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
|
-
|
|
2046
|
-
|
|
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
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
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[@]}"
|
package/lib/install-base.sh
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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":
|
|
26
|
+
"bin": {
|
|
27
|
+
"ai-sandbox-wrapper": "bin/cli.js"
|
|
28
|
+
},
|
|
27
29
|
"files": [
|
|
28
30
|
"bin/",
|
|
29
31
|
"lib/",
|