@rubytech/create-realagent-code 0.1.184 → 0.1.185

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.
Files changed (53) hide show
  1. package/dist/__tests__/platform-port-stamp.test.js +4 -0
  2. package/dist/index.js +1 -0
  3. package/dist/port-resolution.js +1 -0
  4. package/package.json +1 -1
  5. package/payload/platform/config/brand.json +1 -1
  6. package/payload/platform/package-lock.json +178 -0
  7. package/payload/platform/plugins/.claude-plugin/marketplace.json +15 -0
  8. package/payload/platform/plugins/admin/PLUGIN.md +0 -1
  9. package/payload/platform/plugins/browser/.claude-plugin/plugin.json +17 -0
  10. package/payload/platform/plugins/browser/PLUGIN.md +44 -0
  11. package/payload/platform/plugins/browser/mcp/dist/index.d.ts +2 -0
  12. package/payload/platform/plugins/browser/mcp/dist/index.d.ts.map +1 -0
  13. package/payload/platform/plugins/browser/mcp/dist/index.js +61 -0
  14. package/payload/platform/plugins/browser/mcp/dist/index.js.map +1 -0
  15. package/payload/platform/plugins/browser/mcp/dist/lib/cdp-render.d.ts +66 -0
  16. package/payload/platform/plugins/browser/mcp/dist/lib/cdp-render.d.ts.map +1 -0
  17. package/payload/platform/plugins/browser/mcp/dist/lib/cdp-render.js +248 -0
  18. package/payload/platform/plugins/browser/mcp/dist/lib/cdp-render.js.map +1 -0
  19. package/payload/platform/plugins/browser/mcp/package.json +19 -0
  20. package/payload/platform/plugins/docs/references/internals.md +1 -1
  21. package/payload/platform/plugins/docs/references/plugins-guide.md +3 -0
  22. package/payload/platform/plugins/docs/references/troubleshooting.md +4 -4
  23. package/payload/platform/plugins/prompt-optimiser/.claude-plugin/plugin.json +8 -0
  24. package/payload/platform/plugins/prompt-optimiser/PLUGIN.md +14 -0
  25. package/payload/platform/plugins/prompt-optimiser/skills/prompt-optimiser/SKILL.md +301 -0
  26. package/payload/platform/plugins/url-get/.claude-plugin/plugin.json +17 -0
  27. package/payload/platform/plugins/url-get/PLUGIN.md +86 -0
  28. package/payload/platform/plugins/url-get/mcp/dist/index.d.ts +9 -0
  29. package/payload/platform/plugins/url-get/mcp/dist/index.d.ts.map +1 -0
  30. package/payload/platform/plugins/url-get/mcp/dist/index.js +53 -0
  31. package/payload/platform/plugins/url-get/mcp/dist/index.js.map +1 -0
  32. package/payload/platform/plugins/url-get/mcp/dist/lib/summarise.d.ts +8 -0
  33. package/payload/platform/plugins/url-get/mcp/dist/lib/summarise.d.ts.map +1 -0
  34. package/payload/platform/plugins/url-get/mcp/dist/lib/summarise.js +83 -0
  35. package/payload/platform/plugins/url-get/mcp/dist/lib/summarise.js.map +1 -0
  36. package/payload/platform/plugins/url-get/mcp/dist/tools/url-get.d.ts +21 -0
  37. package/payload/platform/plugins/url-get/mcp/dist/tools/url-get.d.ts.map +1 -0
  38. package/payload/platform/plugins/url-get/mcp/dist/tools/url-get.js +147 -0
  39. package/payload/platform/plugins/url-get/mcp/dist/tools/url-get.js.map +1 -0
  40. package/payload/platform/plugins/url-get/mcp/package.json +22 -0
  41. package/payload/platform/scripts/setup-account.sh +0 -6
  42. package/payload/platform/scripts/vnc.sh +1 -1
  43. package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts.map +1 -1
  44. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js +8 -0
  45. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js.map +1 -1
  46. package/payload/platform/services/claude-session-manager/dist/specialist-drift.js +2 -2
  47. package/payload/platform/services/claude-session-manager/dist/specialist-drift.js.map +1 -1
  48. package/payload/platform/templates/specialists/agents/content-producer.md +3 -3
  49. package/payload/platform/templates/specialists/agents/librarian.md +1 -1
  50. package/payload/platform/templates/specialists/agents/personal-assistant.md +3 -3
  51. package/payload/platform/templates/specialists/agents/research-assistant.md +3 -3
  52. package/payload/platform/plugins/admin/hooks/__tests__/playwright-file-guard.test.sh +0 -278
  53. package/payload/platform/plugins/admin/hooks/playwright-file-guard.sh +0 -214
@@ -3,7 +3,7 @@ name: personal-assistant
3
3
  description: "Scheduling, platform settings, messaging channels (Telegram, WhatsApp, email, Outlook), tunnel and domain setup, and browser automation. Delegate when the work is on your calendar, configuring the platform, sending or reading messages, or driving a browser."
4
4
  summary: "Handles the operational tasks you'd give a personal assistant: scheduling meetings, managing your platform settings, connecting messaging channels, and completing browser-based tasks on your behalf."
5
5
  model: claude-sonnet-4-6
6
- tools: Skill, mcp__admin__system-status, mcp__admin__brand-settings, mcp__admin__account-manage, mcp__admin__account-update, mcp__admin__logs-read, mcp__admin__plugin-read, mcp__admin__file-attach, mcp__admin__wifi, mcp__contacts__contact-create, mcp__contacts__contact-lookup, mcp__contacts__contact-update, mcp__contacts__contact-delete, mcp__contacts__contact-list, mcp__contacts__contact-export, mcp__contacts__contact-erase, mcp__contacts__group-create, mcp__contacts__group-manage, mcp__telegram__message, mcp__telegram__message-history, mcp__telegram__telegram-webhook-register, mcp__whatsapp__whatsapp-login-start, mcp__whatsapp__whatsapp-login-wait, mcp__whatsapp__whatsapp-status, mcp__whatsapp__whatsapp-disconnect, mcp__whatsapp__whatsapp-send, mcp__whatsapp__whatsapp-send-document, mcp__whatsapp__whatsapp-config, mcp__whatsapp__whatsapp-activity, mcp__whatsapp__whatsapp-conversations, mcp__whatsapp__whatsapp-messages, mcp__whatsapp__whatsapp-conversation-graph-state, mcp__whatsapp__whatsapp-group-info, mcp__email__email-setup, mcp__email__email-read, mcp__email__email-send, mcp__email__email-reply, mcp__email__email-search, mcp__email__email-graph-query, mcp__email__email-otp-extract, mcp__email__email-status, mcp__email__email-fetch, mcp__email__email-ingest, mcp__outlook__outlook-account-register, mcp__outlook__outlook-mail-list, mcp__outlook__outlook-mail-search, mcp__outlook__outlook-calendar-list, mcp__outlook__outlook-calendar-event, mcp__outlook__outlook-contacts-list, mcp__outlook__outlook-mailbox-info, mcp__scheduling__schedule-event, mcp__scheduling__schedule-list, mcp__scheduling__schedule-get, mcp__scheduling__schedule-update, mcp__scheduling__schedule-cancel, mcp__scheduling__schedule-export-ics, mcp__scheduling__schedule-import-ics, mcp__scheduling__time-resolve, mcp__memory__memory-search, mcp__memory__profile-update, mcp__plugin_playwright_playwright__browser_navigate, mcp__plugin_playwright_playwright__browser_navigate_back, mcp__plugin_playwright_playwright__browser_snapshot, mcp__plugin_playwright_playwright__browser_click, mcp__plugin_playwright_playwright__browser_fill, mcp__plugin_playwright_playwright__browser_fill_form, mcp__plugin_playwright_playwright__browser_type, mcp__plugin_playwright_playwright__browser_press_key, mcp__plugin_playwright_playwright__browser_hover, mcp__plugin_playwright_playwright__browser_select_option, mcp__plugin_playwright_playwright__browser_wait_for, mcp__plugin_playwright_playwright__browser_handle_dialog, mcp__plugin_playwright_playwright__browser_evaluate, mcp__plugin_playwright_playwright__browser_console_messages, mcp__plugin_playwright_playwright__browser_resize, mcp__plugin_playwright_playwright__browser_tabs, mcp__plugin_playwright_playwright__browser_close
6
+ tools: Skill, mcp__admin__system-status, mcp__admin__brand-settings, mcp__admin__account-manage, mcp__admin__account-update, mcp__admin__logs-read, mcp__admin__plugin-read, mcp__admin__file-attach, mcp__admin__wifi, mcp__contacts__contact-create, mcp__contacts__contact-lookup, mcp__contacts__contact-update, mcp__contacts__contact-delete, mcp__contacts__contact-list, mcp__contacts__contact-export, mcp__contacts__contact-erase, mcp__contacts__group-create, mcp__contacts__group-manage, mcp__telegram__message, mcp__telegram__message-history, mcp__telegram__telegram-webhook-register, mcp__whatsapp__whatsapp-login-start, mcp__whatsapp__whatsapp-login-wait, mcp__whatsapp__whatsapp-status, mcp__whatsapp__whatsapp-disconnect, mcp__whatsapp__whatsapp-send, mcp__whatsapp__whatsapp-send-document, mcp__whatsapp__whatsapp-config, mcp__whatsapp__whatsapp-activity, mcp__whatsapp__whatsapp-conversations, mcp__whatsapp__whatsapp-messages, mcp__whatsapp__whatsapp-conversation-graph-state, mcp__whatsapp__whatsapp-group-info, mcp__email__email-setup, mcp__email__email-read, mcp__email__email-send, mcp__email__email-reply, mcp__email__email-search, mcp__email__email-graph-query, mcp__email__email-otp-extract, mcp__email__email-status, mcp__email__email-fetch, mcp__email__email-ingest, mcp__outlook__outlook-account-register, mcp__outlook__outlook-mail-list, mcp__outlook__outlook-mail-search, mcp__outlook__outlook-calendar-list, mcp__outlook__outlook-calendar-event, mcp__outlook__outlook-contacts-list, mcp__outlook__outlook-mailbox-info, mcp__scheduling__schedule-event, mcp__scheduling__schedule-list, mcp__scheduling__schedule-get, mcp__scheduling__schedule-update, mcp__scheduling__schedule-cancel, mcp__scheduling__schedule-export-ics, mcp__scheduling__schedule-import-ics, mcp__scheduling__time-resolve, mcp__memory__memory-search, mcp__memory__profile-update, mcp__url-get__url-get, mcp__browser__browser-render
7
7
  ---
8
8
 
9
9
  # Personal Assistant
@@ -38,9 +38,9 @@ Each domain has a small set of tools and, where it exists, a skill that drives t
38
38
 
39
39
  **WhatsApp ToS.** Automated broadcast is forbidden. The platform blocks broadcast for WhatsApp regardless of channel config; do not try to design around it.
40
40
 
41
- **Cloudflare lives in the operator's browser.** Cloudflare exposes zero MCP tools. The operator's logged-in dashboard is the single source of truth. Do not drive the dashboard via Playwright. Do not synthesise `cloudflared` flag combinations. Do not call the Cloudflare API or SDK. Do not edit `cert.pem`, `tunnel.state`, `config.yml`, or `alias-domains.json` directly. When a sanctioned surface fails, report with evidence and cite `plugins/cloudflare/references/reset-guide.md`. User-facing language: "Cloudflare account", "domain", "address", "sign in", "browser". Never say "zone", "CNAME", "account ID", "API", "SDK", or any hexadecimal identifier. After a tunnel script finishes, verify with `curl -I https://<hostname>`; a non-530 response means the tunnel is live.
41
+ **Cloudflare lives in the operator's browser.** Cloudflare exposes zero MCP tools. The operator's logged-in dashboard is the single source of truth. Do not drive the dashboard with a browser tool. Do not synthesise `cloudflared` flag combinations. Do not call the Cloudflare API or SDK. Do not edit `cert.pem`, `tunnel.state`, `config.yml`, or `alias-domains.json` directly. When a sanctioned surface fails, report with evidence and cite `plugins/cloudflare/references/reset-guide.md`. User-facing language: "Cloudflare account", "domain", "address", "sign in", "browser". Never say "zone", "CNAME", "account ID", "API", "SDK", or any hexadecimal identifier. After a tunnel script finishes, verify with `curl -I https://<hostname>`; a non-530 response means the tunnel is live.
42
42
 
43
- **Browser is visible.** Playwright is rendered to the operator's VNC viewer. They see every navigation, every fill, every click. After `browser_navigate`, call `browser_tabs` with `action: "select"` on the current tab index so the VNC viewer shows the page you just opened. Prefer `browser_snapshot` (accessibility tree with `ref` IDs) over `browser_take_screenshot`; take a screenshot only when you need to verify layout, colour, image, or canvas content. Use `browser_fill_form` over repeated `browser_fill`. Use `browser_evaluate` for state checks. Check `browser_console_messages` when something behaves unexpectedly. Dismiss cookie consent before the main task. Report CAPTCHA or bot detection as a blocker; do not try to solve it.
43
+ **Reading JavaScript-rendered pages.** When a page needs JavaScript to show its content and `url-get` comes back empty or as a shell use `browser-render` with the page URL. It renders the page in the device's Chromium and returns the rendered HTML plus the visible text. It is read-only: it does not click, fill, or navigate within a page, and it does not save PDFs or screenshots (an interactive automation surface is tracked in Task 496). Report CAPTCHA or bot detection as a blocker; do not try to solve it.
44
44
 
45
45
  ## Optional capabilities
46
46
 
@@ -3,7 +3,7 @@ name: research-assistant
3
3
  description: "Deep research on the web, knowledge management, and writing structured summaries with citations from multiple sources. Delegate when the operator wants you to research a topic, read web pages, combine findings into a summary, or reorganise stored knowledge."
4
4
  summary: "Researches topics online, manages your knowledge graph, and produces supporting visuals."
5
5
  model: claude-opus-4-7
6
- tools: WebSearch, WebFetch, Skill, mcp__memory__memory-search, mcp__memory__memory-write, mcp__memory__memory-reindex, mcp__replicate__image-generate, mcp__plugin_playwright_playwright__browser_navigate, mcp__plugin_playwright_playwright__browser_snapshot, mcp__plugin_playwright_playwright__browser_evaluate
6
+ tools: WebSearch, WebFetch, Skill, mcp__memory__memory-search, mcp__memory__memory-write, mcp__memory__memory-reindex, mcp__replicate__image-generate, mcp__browser__browser-render, mcp__url-get__url-get
7
7
  ---
8
8
 
9
9
  # Research Assistant
@@ -30,7 +30,7 @@ These three rules win when anything else in this prompt conflicts with them.
30
30
 
31
31
  **Source hierarchy.** Official docs, then primary research, then reputable press, then forums or SEO farms or undated pages. Flag anything older than 18 months as `[possibly outdated]`.
32
32
 
33
- **Fetch and extract.** Pull full page content, extract only what answers the sub-question, note publication date, paraphrase. Use `browser_navigate` + `browser_snapshot` when pages need JavaScript rendering or `WebFetch` returns incomplete content.
33
+ **Fetch and extract.** Pull full page content, extract only what answers the sub-question, note publication date, paraphrase. Use `browser-render` when pages need JavaScript rendering or `WebFetch` returns incomplete content — it renders the page in the device Chromium and returns the rendered HTML and visible text.
34
34
 
35
35
  **Synthesise.** Write a structured response that answers the original question. Cite sources inline as `[1]`, `[2]`. List full sources at the end with title and URL.
36
36
 
@@ -66,7 +66,7 @@ Return to the admin agent: sources (numbered list with title and URL); key findi
66
66
 
67
67
  ## When a tool returns an error
68
68
 
69
- Name the tool, the target URL, and the `[tool-failure-diag]` block if present. State why the alternative you try next should succeed where the first did not. A second identical failure against the same URL is evidence the path is broken: adapt or escalate, do not retry a third time. Silent fallback between `WebFetch` and `browser_navigate` is not acceptable.
69
+ Name the tool, the target URL, and the `[tool-failure-diag]` block if present. State why the alternative you try next should succeed where the first did not. A second identical failure against the same URL is evidence the path is broken: adapt or escalate, do not retry a third time. Silent fallback between `WebFetch` and `browser-render` is not acceptable.
70
70
 
71
71
  ## Plain English
72
72
 
@@ -1,278 +0,0 @@
1
- #!/usr/bin/env bash
2
- # Regression test for playwright-file-guard.sh.
3
- #
4
- # Covers:
5
- # 1. file:// URL → hook emits hookSpecificOutput.updatedInput.url JSON,
6
- # starts a backgrounded python3 -m http.server on a free port rooted
7
- # at the file's parent dir, writes /tmp/playwright-file-guard.<port>.pid,
8
- # and the rewritten URL responds 200.
9
- # 2. https:// URL → silent passthrough, exit 0, no stdout JSON.
10
- # 3. Free-port under contention: hold 8080, run hook on file://, assert
11
- # chosen port != 8080.
12
- # 4. Stale-PID cleanup mode: pre-create stale PID file pointing at a real
13
- # http.server we control, run `cleanup`, assert process killed + file
14
- # removed.
15
- # 5. Reused-PID guard: PID file points at non-http.server process,
16
- # cleanup leaves the process alone.
17
- # 6. Fail-open on missing python3 (PATH override).
18
- # 7. Malformed stdin → exit 0 silent passthrough.
19
- # 8. Terminal stdin guard preserved.
20
- # 9. Hook source contains no copy of the old menu phrase.
21
-
22
- set -u
23
-
24
- HOOK="$(cd "$(dirname "$0")/.." && pwd)/playwright-file-guard.sh"
25
- if [[ ! -x "$HOOK" ]]; then
26
- echo "FAIL: $HOOK not executable" >&2
27
- exit 1
28
- fi
29
-
30
- # Track every PID we spawn so we can reap on EXIT.
31
- TRACKED_PIDS=()
32
- TMPFILES=()
33
-
34
- cleanup_test_state() {
35
- for p in "${TRACKED_PIDS[@]:-}"; do
36
- [[ -n "$p" ]] && kill -9 "$p" 2>/dev/null || true
37
- done
38
- for f in "${TMPFILES[@]:-}"; do
39
- [[ -n "$f" ]] && rm -f "$f" 2>/dev/null || true
40
- done
41
- rm -f /tmp/playwright-file-guard.*.pid 2>/dev/null || true
42
- }
43
- trap cleanup_test_state EXIT
44
-
45
- PASS=0
46
- FAIL=0
47
-
48
- pass() { echo "PASS: $1"; PASS=$((PASS + 1)); }
49
- fail() { echo "FAIL: $1" >&2; FAIL=$((FAIL + 1)); }
50
-
51
- # Sweep stale PID files BEFORE running tests so prior runs don't poison the
52
- # fixture. The hook's opportunistic cleanup also triggers on every call, but
53
- # a clean slate makes assertions deterministic.
54
- rm -f /tmp/playwright-file-guard.*.pid 2>/dev/null || true
55
-
56
- # ---- Test 1: file:// rewrite happy path -------------------------------------
57
-
58
- TEST_HTML=$(mktemp -t playwright-file-guard-XXXXXX).html
59
- echo "<html><body>hello</body></html>" > "$TEST_HTML"
60
- TMPFILES+=("$TEST_HTML")
61
- TEST_BASENAME=$(basename "$TEST_HTML")
62
-
63
- INPUT_JSON=$(printf '{"hook_event_name":"PreToolUse","tool_name":"mcp__plugin_playwright_playwright__browser_navigate","tool_input":{"url":"file://%s"}}' "$TEST_HTML")
64
- STDOUT_FILE=$(mktemp); STDERR_FILE=$(mktemp); TMPFILES+=("$STDOUT_FILE" "$STDERR_FILE")
65
-
66
- printf '%s' "$INPUT_JSON" | bash "$HOOK" >"$STDOUT_FILE" 2>"$STDERR_FILE"
67
- RC=$?
68
-
69
- if [[ "$RC" -ne 0 ]]; then
70
- fail "Test 1: hook exited $RC on file:// rewrite (expected 0)"
71
- elif ! grep -q '"hookSpecificOutput"' "$STDOUT_FILE"; then
72
- fail "Test 1: stdout missing hookSpecificOutput JSON. Got: $(cat "$STDOUT_FILE")"
73
- elif ! grep -q '"updatedInput"' "$STDOUT_FILE"; then
74
- fail "Test 1: stdout missing updatedInput field. Got: $(cat "$STDOUT_FILE")"
75
- elif ! grep -qE '"url":\s*"http://127.0.0.1:[0-9]+/'"$TEST_BASENAME"'"' "$STDOUT_FILE"; then
76
- fail "Test 1: stdout url not rewritten as expected. Got: $(cat "$STDOUT_FILE")"
77
- elif ! grep -q '\[playwright-file-guard\] action=rewrite' "$STDERR_FILE"; then
78
- fail "Test 1: stderr missing rewrite log line. Got: $(cat "$STDERR_FILE")"
79
- else
80
- PORT=$(grep -oE 'http://127\.0\.0\.1:[0-9]+/' "$STDOUT_FILE" | head -1 | grep -oE ':[0-9]+/' | tr -d ':/')
81
- PID_FILE="/tmp/playwright-file-guard.${PORT}.pid"
82
- if [[ ! -f "$PID_FILE" ]]; then
83
- fail "Test 1: PID file $PID_FILE not created"
84
- else
85
- SERVER_PID=$(cat "$PID_FILE")
86
- TRACKED_PIDS+=("$SERVER_PID")
87
- HTTP_BODY=$(curl -s --max-time 2 "http://127.0.0.1:${PORT}/${TEST_BASENAME}" 2>/dev/null || true)
88
- if echo "$HTTP_BODY" | grep -q "hello"; then
89
- pass "Test 1: file:// → rewrite + spawned server responds 200"
90
- else
91
- fail "Test 1: spawned server at port $PORT did not return file content. Body: $HTTP_BODY"
92
- fi
93
- fi
94
- fi
95
-
96
- # ---- Test 2: https:// passthrough -------------------------------------------
97
-
98
- INPUT_JSON='{"hook_event_name":"PreToolUse","tool_name":"mcp__plugin_playwright_playwright__browser_navigate","tool_input":{"url":"https://example.com"}}'
99
- STDOUT_FILE=$(mktemp); STDERR_FILE=$(mktemp); TMPFILES+=("$STDOUT_FILE" "$STDERR_FILE")
100
- printf '%s' "$INPUT_JSON" | bash "$HOOK" >"$STDOUT_FILE" 2>"$STDERR_FILE"
101
- RC=$?
102
-
103
- if [[ "$RC" -ne 0 ]]; then
104
- fail "Test 2: hook exited $RC on https:// passthrough"
105
- elif [[ -s "$STDOUT_FILE" ]]; then
106
- fail "Test 2: stdout should be empty on passthrough. Got: $(cat "$STDOUT_FILE")"
107
- elif ! grep -q '\[playwright-file-guard\] action=passthrough scheme=https' "$STDERR_FILE"; then
108
- fail "Test 2: stderr missing passthrough log line. Got: $(cat "$STDERR_FILE")"
109
- else
110
- pass "Test 2: https:// → silent passthrough"
111
- fi
112
-
113
- # ---- Test 3: free port under :8080 contention -------------------------------
114
-
115
- python3 -c "
116
- import socket, time, sys
117
- s = socket.socket()
118
- try:
119
- s.bind(('127.0.0.1', 8080))
120
- s.listen(1)
121
- print('READY', flush=True)
122
- time.sleep(15)
123
- except OSError as e:
124
- print('SKIP', e, flush=True)
125
- sys.exit(0)
126
- " >/tmp/playwright-file-guard-test-blocker.stdout 2>&1 &
127
- BLOCKER_PID=$!
128
- TRACKED_PIDS+=("$BLOCKER_PID")
129
- TMPFILES+=("/tmp/playwright-file-guard-test-blocker.stdout")
130
-
131
- for _ in 1 2 3 4 5 6 7 8 9 10; do
132
- if grep -qE 'READY|SKIP' /tmp/playwright-file-guard-test-blocker.stdout 2>/dev/null; then break; fi
133
- sleep 0.1
134
- done
135
-
136
- if grep -q SKIP /tmp/playwright-file-guard-test-blocker.stdout 2>/dev/null; then
137
- echo "SKIP: Test 3 — port 8080 already held by another process; cannot run contention test"
138
- else
139
- TEST_HTML2=$(mktemp -t playwright-file-guard-XXXXXX).html
140
- echo "<html>2</html>" > "$TEST_HTML2"
141
- TMPFILES+=("$TEST_HTML2")
142
- INPUT_JSON=$(printf '{"hook_event_name":"PreToolUse","tool_name":"mcp__plugin_playwright_playwright__browser_navigate","tool_input":{"url":"file://%s"}}' "$TEST_HTML2")
143
- STDOUT_FILE=$(mktemp); TMPFILES+=("$STDOUT_FILE")
144
- printf '%s' "$INPUT_JSON" | bash "$HOOK" >"$STDOUT_FILE" 2>/dev/null
145
- CHOSEN_PORT=$(grep -oE 'http://127\.0\.0\.1:[0-9]+/' "$STDOUT_FILE" | head -1 | grep -oE ':[0-9]+/' | tr -d ':/')
146
- if [[ -z "$CHOSEN_PORT" ]]; then
147
- fail "Test 3: hook did not produce a rewrite under contention. Stdout: $(cat "$STDOUT_FILE")"
148
- elif [[ "$CHOSEN_PORT" == "8080" ]]; then
149
- fail "Test 3: hook picked 8080 even though it was bound"
150
- else
151
- SERVER_PID=$(cat "/tmp/playwright-file-guard.${CHOSEN_PORT}.pid" 2>/dev/null || echo "")
152
- [[ -n "$SERVER_PID" ]] && TRACKED_PIDS+=("$SERVER_PID")
153
- pass "Test 3: free-port under :8080 contention picked $CHOSEN_PORT"
154
- fi
155
- fi
156
-
157
- # ---- Test 4: stale-PID cleanup mode (explicit argv) -------------------------
158
-
159
- STALE_DIR=$(mktemp -d); TMPFILES+=("$STALE_DIR")
160
- ( cd "$STALE_DIR" && exec python3 -u -m http.server 0 >/tmp/playwright-file-guard-stale.stdout 2>&1 ) &
161
- STALE_PID=$!
162
- TRACKED_PIDS+=("$STALE_PID")
163
- TMPFILES+=("/tmp/playwright-file-guard-stale.stdout")
164
- # Wait up to 2s for the http.server banner to flush.
165
- for _ in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20; do
166
- if grep -qE 'port [0-9]+' /tmp/playwright-file-guard-stale.stdout 2>/dev/null; then break; fi
167
- sleep 0.1
168
- done
169
- STALE_PORT=$(grep -oE 'port [0-9]+' /tmp/playwright-file-guard-stale.stdout 2>/dev/null | head -1 | grep -oE '[0-9]+' || echo "")
170
- if [[ -z "$STALE_PORT" ]]; then
171
- fail "Test 4 setup: could not extract port from python3 http.server"
172
- else
173
- STALE_PID_FILE="/tmp/playwright-file-guard.${STALE_PORT}.pid"
174
- echo "$STALE_PID" > "$STALE_PID_FILE"
175
- TMPFILES+=("$STALE_PID_FILE")
176
- touch -t "$(date -u -v-2H +%Y%m%d%H%M.%S 2>/dev/null || date -u -d '2 hours ago' +%Y%m%d%H%M.%S)" "$STALE_PID_FILE"
177
-
178
- bash "$HOOK" cleanup >/dev/null 2>&1
179
-
180
- # SIGTERM is asynchronous — wait up to 1s for the process to actually die.
181
- for _ in 1 2 3 4 5 6 7 8 9 10; do
182
- if ! kill -0 "$STALE_PID" 2>/dev/null; then break; fi
183
- sleep 0.1
184
- done
185
-
186
- if kill -0 "$STALE_PID" 2>/dev/null; then
187
- fail "Test 4: stale PID $STALE_PID still alive after cleanup"
188
- kill -9 "$STALE_PID" 2>/dev/null || true
189
- elif [[ -f "$STALE_PID_FILE" ]]; then
190
- fail "Test 4: stale PID file $STALE_PID_FILE not removed after cleanup"
191
- else
192
- pass "Test 4: stale-PID cleanup killed http.server + removed PID file"
193
- fi
194
- fi
195
-
196
- # ---- Test 5: reused-PID guard ----------------------------------------------
197
-
198
- REUSED_PID_FILE="/tmp/playwright-file-guard.59999.pid"
199
- echo "$$" > "$REUSED_PID_FILE"
200
- TMPFILES+=("$REUSED_PID_FILE")
201
- touch -t "$(date -u -v-2H +%Y%m%d%H%M.%S 2>/dev/null || date -u -d '2 hours ago' +%Y%m%d%H%M.%S)" "$REUSED_PID_FILE"
202
-
203
- bash "$HOOK" cleanup >/dev/null 2>&1
204
-
205
- if ! kill -0 "$$" 2>/dev/null; then
206
- fail "Test 5: reused-PID guard killed our own bash process — fatal"
207
- elif [[ -f "$REUSED_PID_FILE" ]]; then
208
- fail "Test 5: reused-PID file $REUSED_PID_FILE not removed (hook should drop the stale ref)"
209
- else
210
- pass "Test 5: reused-PID guard left process alive, dropped stale PID file"
211
- fi
212
-
213
- # ---- Test 6: missing python3 fails open ------------------------------------
214
-
215
- EMPTY_PATH_DIR=$(mktemp -d); TMPFILES+=("$EMPTY_PATH_DIR")
216
- ln -s "$(command -v bash)" "$EMPTY_PATH_DIR/bash"
217
- ln -s "$(command -v grep)" "$EMPTY_PATH_DIR/grep"
218
- ln -s "$(command -v cat)" "$EMPTY_PATH_DIR/cat"
219
- ln -s "$(command -v rm)" "$EMPTY_PATH_DIR/rm"
220
- ln -s "$(command -v mktemp)" "$EMPTY_PATH_DIR/mktemp"
221
-
222
- INPUT_JSON='{"hook_event_name":"PreToolUse","tool_name":"mcp__plugin_playwright_playwright__browser_navigate","tool_input":{"url":"file:///tmp/missing-python.html"}}'
223
- STDOUT_FILE=$(mktemp); STDERR_FILE=$(mktemp); TMPFILES+=("$STDOUT_FILE" "$STDERR_FILE")
224
- PATH="$EMPTY_PATH_DIR" printf '%s' "$INPUT_JSON" | PATH="$EMPTY_PATH_DIR" bash "$HOOK" >"$STDOUT_FILE" 2>"$STDERR_FILE"
225
- RC=$?
226
-
227
- if [[ "$RC" -ne 0 ]]; then
228
- fail "Test 6: missing python3 should fail open (exit 0), got $RC"
229
- elif [[ -s "$STDOUT_FILE" ]]; then
230
- fail "Test 6: stdout should be empty on fail-open. Got: $(cat "$STDOUT_FILE")"
231
- elif ! grep -q '\[playwright-file-guard\] action=fail reason=python3-missing' "$STDERR_FILE"; then
232
- fail "Test 6: stderr missing python3-missing log line. Got: $(cat "$STDERR_FILE")"
233
- else
234
- pass "Test 6: missing python3 → fail open with diagnostic"
235
- fi
236
-
237
- # ---- Test 7: malformed stdin → silent passthrough ---------------------------
238
-
239
- STDOUT_FILE=$(mktemp); STDERR_FILE=$(mktemp); TMPFILES+=("$STDOUT_FILE" "$STDERR_FILE")
240
- printf '%s' 'not json at all { ' | bash "$HOOK" >"$STDOUT_FILE" 2>"$STDERR_FILE"
241
- RC=$?
242
-
243
- if [[ "$RC" -ne 0 ]]; then
244
- fail "Test 7: malformed stdin should fail open (exit 0), got $RC"
245
- elif [[ -s "$STDOUT_FILE" ]]; then
246
- fail "Test 7: stdout should be empty on malformed stdin"
247
- else
248
- pass "Test 7: malformed stdin → silent passthrough"
249
- fi
250
-
251
- # ---- Test 8: terminal stdin guard preserved --------------------------------
252
-
253
- if ! grep -q '\[ -t 0 \]' "$HOOK"; then
254
- fail "Test 8: terminal stdin guard ([ -t 0 ]) missing from hook"
255
- else
256
- pass "Test 8: terminal stdin guard present"
257
- fi
258
-
259
- # ---- Test 9: zero-grep-hits for old menu copy in this hook ------------------
260
-
261
- # The pattern uses [l] to break self-match: this file's bytes contain "[l]ocal"
262
- # but the hook source contains "local" if the menu copy was reintroduced. The
263
- # extended-regex bracket class matches "local" without our query containing
264
- # the literal string we're forbidding.
265
- OLD_MENU_PATTERN='Start a [l]ocal HTTP server'
266
- if grep -qE "$OLD_MENU_PATTERN" "$HOOK"; then
267
- fail "Test 9: hook still contains the old menu copy"
268
- else
269
- pass "Test 9: hook does not contain the old menu copy"
270
- fi
271
-
272
- echo
273
- echo "──────── playwright-file-guard test summary ────────"
274
- echo "PASS: $PASS"
275
- echo "FAIL: $FAIL"
276
-
277
- [[ "$FAIL" -gt 0 ]] && exit 1
278
- exit 0
@@ -1,214 +0,0 @@
1
- #!/usr/bin/env bash
2
- # PreToolUse hook — rewrites file:// browser_navigate calls to a backgrounded
3
- # loopback HTTP server, so Playwright never sees a file:// URL (which it
4
- # blocks behind a 2-minute MCP timeout).
5
- #
6
- # Scoped to mcp__plugin_playwright_playwright__browser_navigate via the
7
- # settings.json matcher.
8
- #
9
- # Modes:
10
- # default (stdin = PreToolUse JSON):
11
- # - Sweeps stale /tmp/playwright-file-guard.*.pid (>1h old) opportunistically.
12
- # - If tool_input.url is file://, picks a free port, backgrounds
13
- # `python3 -m http.server <port>` rooted at the file's parent dir,
14
- # connect-verifies the server within 1s, writes PID to
15
- # /tmp/playwright-file-guard.<port>.pid, and emits a JSON object on
16
- # stdout setting hookSpecificOutput.updatedInput.url to
17
- # http://127.0.0.1:<port>/<basename>.
18
- # - Other URLs: passthrough log to stderr, exit 0, no stdout.
19
- #
20
- # `cleanup` argv: only runs the stale-PID sweep; does not read stdin.
21
- #
22
- # Error philosophy: fail open. Any internal failure → exit 0 with a stderr
23
- # diagnostic, letting Playwright handle the original URL (the legacy
24
- # 2-minute timeout) rather than block on a parser bug.
25
- #
26
- # Observability: every action emits one stderr line of the shape
27
- # [playwright-file-guard] action=<rewrite|passthrough|cleanup|fail> ...
28
-
29
- set -u
30
-
31
- MODE="${1:-rewrite}"
32
-
33
- # Terminal stdin guard. Cleanup mode never reads stdin.
34
- if [[ "$MODE" != "cleanup" ]] && [ -t 0 ]; then
35
- exit 0
36
- fi
37
-
38
- # Without python3 we cannot do anything useful. Drain stdin (so the upstream
39
- # tool dispatcher does not get SIGPIPE) and fail open.
40
- if ! command -v python3 >/dev/null 2>&1; then
41
- if [[ "$MODE" != "cleanup" ]]; then
42
- cat >/dev/null
43
- fi
44
- echo "[playwright-file-guard] action=fail reason=python3-missing" >&2
45
- exit 0
46
- fi
47
-
48
- if [[ "$MODE" == "cleanup" ]]; then
49
- INPUT=""
50
- else
51
- INPUT=$(cat)
52
- fi
53
-
54
- MODE="$MODE" INPUT="$INPUT" python3 <<'PY' || exit 0
55
- import os, sys, json, glob, time, socket, subprocess
56
- from urllib.parse import unquote
57
-
58
- MODE = os.environ.get('MODE', 'rewrite')
59
- INPUT = os.environ.get('INPUT', '')
60
- NOW = time.time()
61
- STALE_THRESHOLD = 3600 # 1h
62
- PID_GLOB = '/tmp/playwright-file-guard.*.pid'
63
-
64
- def log(line):
65
- print(line, file=sys.stderr, flush=True)
66
-
67
- def port_from_pidfile(path):
68
- parts = os.path.basename(path).split('.')
69
- return parts[1] if len(parts) >= 3 else 'unknown'
70
-
71
- def sweep_stale():
72
- for pid_file in glob.glob(PID_GLOB):
73
- try:
74
- mtime = os.stat(pid_file).st_mtime
75
- except OSError:
76
- continue
77
- if NOW - mtime < STALE_THRESHOLD:
78
- continue
79
- port = port_from_pidfile(pid_file)
80
- try:
81
- with open(pid_file) as f:
82
- pid = int(f.read().strip())
83
- except Exception:
84
- try: os.remove(pid_file)
85
- except OSError: pass
86
- log(f'[playwright-file-guard] action=cleanup port={port} status=unparseable-pid')
87
- continue
88
- cmd = subprocess.run(
89
- ['ps', '-p', str(pid), '-o', 'command='],
90
- capture_output=True, text=True,
91
- )
92
- cmdline = cmd.stdout if cmd.returncode == 0 else ''
93
- # macOS ps resolves /usr/local/bin/python3 → Python.app binary "Python";
94
- # Linux/Pi ps shows "python3". Match either via case-insensitive
95
- # `python` substring + the unambiguous `http.server` invocation.
96
- cmdline_lower = cmdline.lower()
97
- if 'python' in cmdline_lower and 'http.server' in cmdline_lower:
98
- subprocess.run(['kill', str(pid)], capture_output=True)
99
- log(f'[playwright-file-guard] action=cleanup port={port} pid={pid} status=killed')
100
- else:
101
- log(f'[playwright-file-guard] action=cleanup port={port} pid={pid} status=pid-reused-skip')
102
- try: os.remove(pid_file)
103
- except OSError: pass
104
-
105
- # Opportunistic sweep on every invocation — closes the gap that nothing
106
- # currently calls cleanup on a periodic schedule.
107
- sweep_stale()
108
-
109
- if MODE == 'cleanup':
110
- sys.exit(0)
111
-
112
- try:
113
- payload = json.loads(INPUT) if INPUT else {}
114
- except Exception:
115
- sys.exit(0)
116
-
117
- tool_input = payload.get('tool_input') or {}
118
- url = tool_input.get('url')
119
- if not isinstance(url, str) or not url:
120
- sys.exit(0)
121
-
122
- clean_url = url.split('#', 1)[0].split('?', 1)[0]
123
-
124
- if '://' not in clean_url:
125
- log('[playwright-file-guard] action=passthrough scheme=none')
126
- sys.exit(0)
127
-
128
- scheme, rest = clean_url.split('://', 1)
129
- scheme = scheme.lower()
130
- if scheme != 'file':
131
- log(f'[playwright-file-guard] action=passthrough scheme={scheme}')
132
- sys.exit(0)
133
-
134
- # file:///abs/path → /abs/path; file://host/path → /path (host ignored).
135
- if rest.startswith('/'):
136
- file_path = rest
137
- elif '/' in rest:
138
- file_path = '/' + rest.split('/', 1)[1]
139
- else:
140
- file_path = '/'
141
-
142
- file_path = unquote(file_path)
143
- abs_path = os.path.abspath(file_path)
144
-
145
- if not os.path.isfile(abs_path):
146
- log(f'[playwright-file-guard] action=fail reason=file-not-found path={abs_path}')
147
- sys.exit(0)
148
-
149
- served_dir = os.path.dirname(abs_path)
150
- basename = os.path.basename(abs_path)
151
-
152
- try:
153
- s = socket.socket()
154
- s.bind(('127.0.0.1', 0))
155
- port = s.getsockname()[1]
156
- s.close()
157
- except Exception as e:
158
- log(f'[playwright-file-guard] action=fail reason=port-pick-failed err={type(e).__name__}')
159
- sys.exit(0)
160
-
161
- try:
162
- proc = subprocess.Popen(
163
- [sys.executable, '-m', 'http.server', str(port), '--bind', '127.0.0.1'],
164
- cwd=served_dir,
165
- stdout=subprocess.DEVNULL,
166
- stderr=subprocess.DEVNULL,
167
- start_new_session=True,
168
- )
169
- except Exception as e:
170
- log(f'[playwright-file-guard] action=fail reason=spawn-failed err={type(e).__name__}')
171
- sys.exit(0)
172
-
173
- pid_file = f'/tmp/playwright-file-guard.{port}.pid'
174
- try:
175
- with open(pid_file, 'w') as f:
176
- f.write(str(proc.pid))
177
- except Exception:
178
- pass
179
-
180
- # Connect-verify within 1s closes the TOCTOU window between bind(0) and the
181
- # http.server actually accepting connections.
182
- deadline = time.time() + 1.0
183
- ready = False
184
- while time.time() < deadline:
185
- try:
186
- sock = socket.create_connection(('127.0.0.1', port), timeout=0.2)
187
- sock.close()
188
- ready = True
189
- break
190
- except OSError:
191
- time.sleep(0.05)
192
-
193
- if not ready:
194
- try: proc.kill()
195
- except Exception: pass
196
- try: os.remove(pid_file)
197
- except OSError: pass
198
- log(f'[playwright-file-guard] action=fail reason=server-not-ready port={port}')
199
- sys.exit(0)
200
-
201
- new_url = f'http://127.0.0.1:{port}/{basename}'
202
- updated_input = dict(tool_input)
203
- updated_input['url'] = new_url
204
-
205
- print(json.dumps({
206
- 'hookSpecificOutput': {
207
- 'hookEventName': 'PreToolUse',
208
- 'permissionDecision': 'allow',
209
- 'permissionDecisionReason': 'file:// rewritten to local http.server',
210
- 'updatedInput': updated_input,
211
- },
212
- }))
213
- log(f'[playwright-file-guard] action=rewrite original=file://{abs_path} served_dir={served_dir} port={port} pid={proc.pid}')
214
- PY