browser-automation-skill 0.71.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.
Files changed (117) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +144 -0
  3. package/SECURITY.md +39 -0
  4. package/SKILL.md +206 -0
  5. package/bin/cli.mjs +55 -0
  6. package/install.sh +143 -0
  7. package/package.json +54 -0
  8. package/references/adapter-candidates.md +40 -0
  9. package/references/browser-mcp-cheatsheet.md +132 -0
  10. package/references/browser-stats-cheatsheet.md +155 -0
  11. package/references/chrome-devtools-mcp-cheatsheet.md +232 -0
  12. package/references/midscene-integration.md +359 -0
  13. package/references/obscura-cheatsheet.md +103 -0
  14. package/references/playwright-cli-cheatsheet.md +64 -0
  15. package/references/playwright-lib-cheatsheet.md +90 -0
  16. package/references/recipes/add-a-tool-adapter.md +134 -0
  17. package/references/recipes/agent-workflows/README.md +37 -0
  18. package/references/recipes/agent-workflows/cache-driven-bulk-operation.md +110 -0
  19. package/references/recipes/agent-workflows/flow-record-and-replay.md +102 -0
  20. package/references/recipes/agent-workflows/incremental-pattern-discovery.md +125 -0
  21. package/references/recipes/agent-workflows/login-then-scrape.md +100 -0
  22. package/references/recipes/anti-patterns-tool-extension.md +182 -0
  23. package/references/recipes/body-bytes-not-body.md +139 -0
  24. package/references/recipes/cache-write-security.md +210 -0
  25. package/references/recipes/fingerprint-rescue.md +154 -0
  26. package/references/recipes/model-routing.md +143 -0
  27. package/references/recipes/path-security.md +138 -0
  28. package/references/recipes/privacy-canary.md +96 -0
  29. package/references/recipes/visual-rescue-hook.md +182 -0
  30. package/references/stats-prices.json +42 -0
  31. package/references/stats-schema.json +77 -0
  32. package/references/tool-versions.md +8 -0
  33. package/scripts/browser-add-site.sh +113 -0
  34. package/scripts/browser-assert.sh +106 -0
  35. package/scripts/browser-audit.sh +68 -0
  36. package/scripts/browser-baseline.sh +135 -0
  37. package/scripts/browser-click.sh +100 -0
  38. package/scripts/browser-creds-add.sh +254 -0
  39. package/scripts/browser-creds-list.sh +67 -0
  40. package/scripts/browser-creds-migrate.sh +122 -0
  41. package/scripts/browser-creds-remove.sh +69 -0
  42. package/scripts/browser-creds-rotate-totp.sh +109 -0
  43. package/scripts/browser-creds-show.sh +82 -0
  44. package/scripts/browser-creds-totp.sh +94 -0
  45. package/scripts/browser-do.sh +630 -0
  46. package/scripts/browser-doctor.sh +365 -0
  47. package/scripts/browser-drag.sh +90 -0
  48. package/scripts/browser-extract.sh +192 -0
  49. package/scripts/browser-fill.sh +142 -0
  50. package/scripts/browser-flow.sh +316 -0
  51. package/scripts/browser-history.sh +187 -0
  52. package/scripts/browser-hover.sh +92 -0
  53. package/scripts/browser-inspect.sh +188 -0
  54. package/scripts/browser-list-sessions.sh +78 -0
  55. package/scripts/browser-list-sites.sh +42 -0
  56. package/scripts/browser-login.sh +279 -0
  57. package/scripts/browser-mcp.sh +65 -0
  58. package/scripts/browser-migrate.sh +195 -0
  59. package/scripts/browser-open.sh +134 -0
  60. package/scripts/browser-press.sh +80 -0
  61. package/scripts/browser-remove-session.sh +72 -0
  62. package/scripts/browser-remove-site.sh +68 -0
  63. package/scripts/browser-replay.sh +206 -0
  64. package/scripts/browser-route.sh +174 -0
  65. package/scripts/browser-select.sh +122 -0
  66. package/scripts/browser-show-session.sh +57 -0
  67. package/scripts/browser-show-site.sh +37 -0
  68. package/scripts/browser-snapshot.sh +176 -0
  69. package/scripts/browser-stats.sh +522 -0
  70. package/scripts/browser-tab-close.sh +112 -0
  71. package/scripts/browser-tab-list.sh +70 -0
  72. package/scripts/browser-tab-switch.sh +111 -0
  73. package/scripts/browser-upload.sh +132 -0
  74. package/scripts/browser-use.sh +60 -0
  75. package/scripts/browser-vlm.sh +707 -0
  76. package/scripts/browser-wait.sh +97 -0
  77. package/scripts/install-git-hooks.sh +16 -0
  78. package/scripts/lib/capture.sh +356 -0
  79. package/scripts/lib/common.sh +262 -0
  80. package/scripts/lib/credential.sh +237 -0
  81. package/scripts/lib/fingerprint-rescue.js +123 -0
  82. package/scripts/lib/flow.sh +448 -0
  83. package/scripts/lib/flow_record.sh +210 -0
  84. package/scripts/lib/mask.sh +49 -0
  85. package/scripts/lib/memory.sh +427 -0
  86. package/scripts/lib/migrate.sh +390 -0
  87. package/scripts/lib/migrators/README.md +23 -0
  88. package/scripts/lib/migrators/memory/v1_to_v2.sh +15 -0
  89. package/scripts/lib/migrators/recent_urls/README.md +13 -0
  90. package/scripts/lib/migrators/stats/README.md +24 -0
  91. package/scripts/lib/node/chrome-devtools-bridge.mjs +1812 -0
  92. package/scripts/lib/node/mcp-server.mjs +531 -0
  93. package/scripts/lib/node/mcp-tools.json +68 -0
  94. package/scripts/lib/node/playwright-driver.mjs +1104 -0
  95. package/scripts/lib/node/totp-core.mjs +52 -0
  96. package/scripts/lib/node/totp.mjs +52 -0
  97. package/scripts/lib/node/url-pattern-cluster.mjs +102 -0
  98. package/scripts/lib/node/url-pattern-resolver.mjs +77 -0
  99. package/scripts/lib/output.sh +79 -0
  100. package/scripts/lib/router.sh +342 -0
  101. package/scripts/lib/sanitize.sh +107 -0
  102. package/scripts/lib/secret/keychain.sh +91 -0
  103. package/scripts/lib/secret/libsecret.sh +74 -0
  104. package/scripts/lib/secret/plaintext.sh +75 -0
  105. package/scripts/lib/secret_backend_select.sh +57 -0
  106. package/scripts/lib/session.sh +153 -0
  107. package/scripts/lib/site.sh +126 -0
  108. package/scripts/lib/stats.sh +419 -0
  109. package/scripts/lib/tool/.gitkeep +0 -0
  110. package/scripts/lib/tool/chrome-devtools-mcp.sh +349 -0
  111. package/scripts/lib/tool/obscura.sh +249 -0
  112. package/scripts/lib/tool/playwright-cli.sh +155 -0
  113. package/scripts/lib/tool/playwright-lib.sh +106 -0
  114. package/scripts/lib/verb_helpers.sh +222 -0
  115. package/scripts/lib/visual-rescue-default.sh +145 -0
  116. package/scripts/regenerate-docs.sh +99 -0
  117. package/uninstall.sh +51 -0
@@ -0,0 +1,342 @@
1
+ # shellcheck shell=bash
2
+ # scripts/lib/router.sh — single source of truth for routing precedence.
3
+ # Verb scripts call pick_tool; the router returns "TOOL_NAME\tWHY".
4
+ # Adding a new precedence rule = define a function + append to ROUTING_RULES.
5
+ # Adding a new adapter that's NEVER the default for any verb = ZERO edits here
6
+ # (the adapter is reachable via --tool=<name> but won't be picked otherwise).
7
+ # See: docs/superpowers/specs/2026-04-30-tool-adapter-extension-model-design.md §4
8
+
9
+ [ -n "${BROWSER_SKILL_ROUTER_LOADED:-}" ] && return 0
10
+ readonly BROWSER_SKILL_ROUTER_LOADED=1
11
+
12
+ # init_paths must have been called (LIB_TOOL_DIR is set there).
13
+ # common.sh is required (EXIT_* constants, die, _has_flag).
14
+
15
+ # ROUTING_RULES is an ordered list of rule-function names. The first rule
16
+ # whose function returns 0 (after also passing the capability filter) wins.
17
+ # Rules are appended in priority order. Add a new rule = define _rule_<name>
18
+ # function below + append its NAME to this array.
19
+ # Order matters: top-down, first-match-with-capability-support wins.
20
+ # Adding a tool that's NEVER the default for any verb = ZERO edits to this array.
21
+ ROUTING_RULES=(
22
+ rule_session_required
23
+ rule_capture_flags
24
+ rule_audit_or_perf
25
+ rule_inspect_default
26
+ rule_scrape_flag
27
+ rule_stealth_flag
28
+ rule_extract_default
29
+ rule_press_default
30
+ rule_select_default
31
+ rule_hover_default
32
+ rule_wait_default
33
+ rule_drag_default
34
+ rule_upload_default
35
+ rule_route_default
36
+ rule_tab_list_default
37
+ rule_tab_switch_default
38
+ rule_tab_close_default
39
+ rule_default_navigation
40
+ )
41
+
42
+ # _has_flag FLAG ARGS... — returns 0 if FLAG appears in ARGS, else 1.
43
+ # Used by rule functions to detect routing-trigger flags in the verb's argv.
44
+ _has_flag() {
45
+ local needle="$1"
46
+ shift
47
+ local arg
48
+ for arg in "$@"; do
49
+ [ "${arg}" = "${needle}" ] && return 0
50
+ done
51
+ return 1
52
+ }
53
+
54
+ # _tool_supports TOOL_NAME VERB [FLAGS...] — returns 0 if the adapter declares
55
+ # support for VERB in its tool_capabilities() output, 1 otherwise. Sources the
56
+ # adapter in a subshell to keep verb-dispatch namespace clean.
57
+ #
58
+ # Phase 12 part 2 audit: per-process memo. Worst-case pick_tool walks 18 rules
59
+ # and each rule may probe `_tool_supports`; the un-cached version forked
60
+ # `source + tool_capabilities + jq -e` for every probe. The cache makes each
61
+ # (tool, verb) tuple cost one fork on first probe and zero forks thereafter.
62
+ declare -gA _TOOL_SUPPORTS_CACHE 2>/dev/null || true
63
+ _tool_supports() {
64
+ local tool="$1" verb="$2"
65
+ shift 2
66
+ local key="${tool}\t${verb}"
67
+ case "${_TOOL_SUPPORTS_CACHE[${key}]:-}" in
68
+ yes) return 0 ;;
69
+ no) return 1 ;;
70
+ esac
71
+ [ -f "${LIB_TOOL_DIR}/${tool}.sh" ] || { _TOOL_SUPPORTS_CACHE[${key}]=no; return 1; }
72
+ if jq -e --arg v "${verb}" '.verbs | has($v)' >/dev/null 2>&1 <<<"$(
73
+ # shellcheck source=/dev/null
74
+ source "${LIB_TOOL_DIR}/${tool}.sh"
75
+ tool_capabilities 2>/dev/null
76
+ )"; then
77
+ _TOOL_SUPPORTS_CACHE[${key}]=yes
78
+ return 0
79
+ fi
80
+ _TOOL_SUPPORTS_CACHE[${key}]=no
81
+ return 1
82
+ }
83
+
84
+ # --- Precedence rules (in order). Each fn echoes "TOOL\tWHY" if it matches.
85
+ # Add a rule = define a function + append its NAME to ROUTING_RULES above.
86
+
87
+ # Session-loading required: when verb_helpers.sh::resolve_session_storage_state
88
+ # resolved a storageState file (BROWSER_SKILL_STORAGE_STATE non-empty), prefer
89
+ # playwright-lib — the only adapter declaring session_load: true.
90
+ # Reads env var (not argv) because session resolution happens before pick_tool.
91
+ rule_session_required() {
92
+ local verb="$1"
93
+ if [ -n "${BROWSER_SKILL_STORAGE_STATE:-}" ]; then
94
+ case "${verb}" in
95
+ open|click|fill|snapshot|login)
96
+ printf 'playwright-lib\t%s\n' "session loading required (BROWSER_SKILL_STORAGE_STATE set)"
97
+ ;;
98
+ esac
99
+ fi
100
+ }
101
+
102
+ # Capture flags require the dedicated console + network MCP tools that only
103
+ # chrome-devtools-mcp exposes (per parent spec Appendix B). Triggered by
104
+ # `--capture-console` or `--capture-network` on any verb.
105
+ rule_capture_flags() {
106
+ local verb="$1"
107
+ shift
108
+ if _has_flag --capture-console "$@" || _has_flag --capture-network "$@"; then
109
+ printf 'chrome-devtools-mcp\t%s\n' "--capture-* requested (only cdt-mcp exposes console/network MCP tools)"
110
+ fi
111
+ }
112
+
113
+ # Lighthouse + perf-trace verbs/flags route to chrome-devtools-mcp — only
114
+ # adapter with `lighthouse_audit` and `performance_*` MCP tools (Appendix B).
115
+ rule_audit_or_perf() {
116
+ local verb="$1"
117
+ shift
118
+ case "${verb}" in
119
+ audit)
120
+ printf 'chrome-devtools-mcp\t%s\n' "verb=audit (only cdt-mcp has lighthouse/perf)"
121
+ return 0
122
+ ;;
123
+ esac
124
+ if _has_flag --lighthouse "$@" || _has_flag --perf-trace "$@"; then
125
+ printf 'chrome-devtools-mcp\t%s\n' "--lighthouse/--perf-trace requested (only cdt-mcp has them)"
126
+ fi
127
+ }
128
+
129
+ # Default tool for `inspect` per parent spec Appendix B — chrome-devtools-mcp
130
+ # is the only adapter with dedicated console + network + screenshot MCP tools
131
+ # bundled into a single inspection surface.
132
+ rule_inspect_default() {
133
+ local verb="$1"
134
+ case "${verb}" in
135
+ inspect)
136
+ printf 'chrome-devtools-mcp\t%s\n' "inspect default per Appendix B"
137
+ ;;
138
+ esac
139
+ }
140
+
141
+ # Phase 8 part 2-i (Path B): --scrape routes to obscura. Higher precedence
142
+ # than rule_extract_default so `extract --scrape` reaches obscura instead of
143
+ # chrome-devtools-mcp. Capability filter rejects mismatched verbs (e.g.
144
+ # `open --scrape` falls through since obscura doesn't declare verb=open).
145
+ rule_scrape_flag() {
146
+ local verb="$1"
147
+ shift
148
+ if _has_flag --scrape "$@"; then
149
+ printf 'obscura\t%s\n' "--scrape requested (only obscura declares scrape backend)"
150
+ fi
151
+ }
152
+
153
+ # Phase 8 part 2-i (Path B): --stealth routes to obscura. Single-URL anti-
154
+ # detect mode. Same precedence reasoning as rule_scrape_flag.
155
+ rule_stealth_flag() {
156
+ local verb="$1"
157
+ shift
158
+ if _has_flag --stealth "$@"; then
159
+ printf 'obscura\t%s\n' "--stealth requested (only obscura declares stealth backend)"
160
+ fi
161
+ }
162
+
163
+ # Default tool for `extract` per parent spec Appendix B — chrome-devtools-mcp
164
+ # pairs `evaluate_script` with `list_network_requests` for selector/eval +
165
+ # multi-URL inspection. `--scrape` / `--stealth` route to obscura via the
166
+ # higher-precedence rule_scrape_flag / rule_stealth_flag rules (Phase 8-2-i).
167
+ rule_extract_default() {
168
+ local verb="$1"
169
+ case "${verb}" in
170
+ extract)
171
+ printf 'chrome-devtools-mcp\t%s\n' "extract default per Appendix B"
172
+ ;;
173
+ esac
174
+ }
175
+
176
+ # Phase-6 part 1: keyboard press routes to chrome-devtools-mcp. cdt-mcp's
177
+ # `press_key` MCP tool is the canonical input mechanism; playwright-cli/lib
178
+ # don't declare press today (could be added later via their respective
179
+ # `keyboard.press` APIs).
180
+ rule_press_default() {
181
+ local verb="$1"
182
+ case "${verb}" in
183
+ press)
184
+ printf 'chrome-devtools-mcp\t%s\n' "press default (only cdt-mcp declares press today)"
185
+ ;;
186
+ esac
187
+ }
188
+
189
+ # Phase-6 part 2: <select> option pick routes to chrome-devtools-mcp.
190
+ # Stateful — requires daemon (refMap precondition). MCP `select_option` tool
191
+ # accepts uid + one of value/label/index.
192
+ rule_select_default() {
193
+ local verb="$1"
194
+ case "${verb}" in
195
+ select)
196
+ printf 'chrome-devtools-mcp\t%s\n' "select default (only cdt-mcp declares select today)"
197
+ ;;
198
+ esac
199
+ }
200
+
201
+ # Phase-6 part 3: pointer hover routes to chrome-devtools-mcp. Stateful —
202
+ # requires daemon (refMap precondition). MCP `hover` tool accepts uid.
203
+ rule_hover_default() {
204
+ local verb="$1"
205
+ case "${verb}" in
206
+ hover)
207
+ printf 'chrome-devtools-mcp\t%s\n' "hover default (only cdt-mcp declares hover today)"
208
+ ;;
209
+ esac
210
+ }
211
+
212
+ # Phase-6 part 4: explicit wait for an element state. Stateless — no refMap
213
+ # required (selector-based). MCP `wait_for` tool accepts {selector, state,
214
+ # timeout}. Routes one-shot or daemon-routed (parallel to eval/audit).
215
+ rule_wait_default() {
216
+ local verb="$1"
217
+ case "${verb}" in
218
+ wait)
219
+ printf 'chrome-devtools-mcp\t%s\n' "wait default (only cdt-mcp declares wait today)"
220
+ ;;
221
+ esac
222
+ }
223
+
224
+ # Phase-6 part 5: pointer drag (src → dst by refs). Stateful — requires
225
+ # daemon (refMap precondition for both src + dst). MCP `drag` tool accepts
226
+ # {src_uid, dst_uid}.
227
+ rule_drag_default() {
228
+ local verb="$1"
229
+ case "${verb}" in
230
+ drag)
231
+ printf 'chrome-devtools-mcp\t%s\n' "drag default (only cdt-mcp declares drag today)"
232
+ ;;
233
+ esac
234
+ }
235
+
236
+ # Phase-6 part 6: file upload (<input type=file>). Stateful — requires
237
+ # daemon (refMap precondition). MCP `upload_file` tool accepts {uid, path}.
238
+ # Bash-side validates path security before reaching the daemon.
239
+ rule_upload_default() {
240
+ local verb="$1"
241
+ case "${verb}" in
242
+ upload)
243
+ printf 'chrome-devtools-mcp\t%s\n' "upload default (only cdt-mcp declares upload today)"
244
+ ;;
245
+ esac
246
+ }
247
+
248
+ # Phase-6 part 7: network-route rule (request interception/mocking).
249
+ # Daemon-state-mutating (routeRules array). Routes to chrome-devtools-mcp.
250
+ rule_route_default() {
251
+ local verb="$1"
252
+ case "${verb}" in
253
+ route)
254
+ printf 'chrome-devtools-mcp\t%s\n' "route default (only cdt-mcp declares route today)"
255
+ ;;
256
+ esac
257
+ }
258
+
259
+ # Phase-6 part 8-i: tab enumeration. Read-only; daemon caches tabs[] slot
260
+ # so 8-ii (tab-switch) / 8-iii (tab-close) can reference the same shape.
261
+ rule_tab_list_default() {
262
+ local verb="$1"
263
+ case "${verb}" in
264
+ tab-list)
265
+ printf 'chrome-devtools-mcp\t%s\n' "tab-list default (only cdt-mcp declares tab-list today)"
266
+ ;;
267
+ esac
268
+ }
269
+
270
+ # Phase-6 part 8-ii: tab switching. First state-mutation on tabs[] (adds
271
+ # currentTab pointer in the daemon). Routes to chrome-devtools-mcp.
272
+ rule_tab_switch_default() {
273
+ local verb="$1"
274
+ case "${verb}" in
275
+ tab-switch)
276
+ printf 'chrome-devtools-mcp\t%s\n' "tab-switch default (only cdt-mcp declares tab-switch today)"
277
+ ;;
278
+ esac
279
+ }
280
+
281
+ # Phase-6 part 8-iii: tab close. Splice from tabs[] + close upstream page +
282
+ # null currentTab on match. Routes to chrome-devtools-mcp.
283
+ rule_tab_close_default() {
284
+ local verb="$1"
285
+ case "${verb}" in
286
+ tab-close)
287
+ printf 'chrome-devtools-mcp\t%s\n' "tab-close default (only cdt-mcp declares tab-close today)"
288
+ ;;
289
+ esac
290
+ }
291
+
292
+ # Default for navigation/inspection verbs — playwright-cli is the cheap,
293
+ # stable, multi-browser default per parent spec Appendix B.
294
+ rule_default_navigation() {
295
+ local verb="$1"
296
+ case "${verb}" in
297
+ open|click|fill|snapshot)
298
+ printf 'playwright-cli\t%s\n' "default for ${verb}"
299
+ ;;
300
+ esac
301
+ }
302
+
303
+ # pick_tool VERB [FLAGS...] — echoes "TOOL_NAME\tWHY" on success.
304
+ # Two-stage:
305
+ # 1. --tool=X (via $ARG_TOOL env var): validate X exists + supports verb.
306
+ # 2. Walk ROUTING_RULES top-down. First matching rule whose tool ALSO
307
+ # passes the capability filter wins.
308
+ # On exhaustion: dies with EXIT_TOOL_MISSING.
309
+ pick_tool() {
310
+ local verb="$1"
311
+ shift
312
+
313
+ if [ -n "${ARG_TOOL:-}" ]; then
314
+ if [ ! -f "${LIB_TOOL_DIR}/${ARG_TOOL}.sh" ]; then
315
+ die "${EXIT_USAGE_ERROR}" "--tool=${ARG_TOOL}: no such adapter (no ${LIB_TOOL_DIR}/${ARG_TOOL}.sh)"
316
+ fi
317
+ if ! _tool_supports "${ARG_TOOL}" "${verb}" "$@"; then
318
+ die "${EXIT_USAGE_ERROR}" "--tool=${ARG_TOOL} does not support verb=${verb} (per tool_capabilities)"
319
+ fi
320
+ printf '%s\t%s\n' "${ARG_TOOL}" "user-specified"
321
+ return 0
322
+ fi
323
+
324
+ local rule
325
+ for rule in "${ROUTING_RULES[@]}"; do
326
+ local rule_out
327
+ rule_out="$("${rule}" "${verb}" "$@" 2>/dev/null || true)"
328
+ [ -z "${rule_out}" ] && continue
329
+
330
+ local picked_tool picked_why
331
+ picked_tool="${rule_out%%$'\t'*}"
332
+ picked_why="${rule_out#*$'\t'}"
333
+
334
+ if _tool_supports "${picked_tool}" "${verb}" "$@"; then
335
+ printf '%s\t%s\n' "${picked_tool}" "${picked_why}"
336
+ return 0
337
+ fi
338
+ warn "router: rule ${rule} picked ${picked_tool} but it doesn't support verb=${verb}; falling through"
339
+ done
340
+
341
+ die "${EXIT_TOOL_MISSING}" "no adapter supports verb=${verb} with flags: $*"
342
+ }
@@ -0,0 +1,107 @@
1
+ # scripts/lib/sanitize.sh — capture sanitization (Phase 7 part 1-ii).
2
+ #
3
+ # Two-function API:
4
+ # sanitize_har — redact sensitive HAR fields (request/response headers,
5
+ # URL params). Reads HAR JSON on stdin; emits redacted
6
+ # HAR JSON on stdout.
7
+ # sanitize_console — redact sensitive console message fields (password,
8
+ # secret, token values inline in the message text).
9
+ # Reads console-array JSON on stdin; emits redacted
10
+ # array on stdout.
11
+ #
12
+ # Per parent spec §8.3:
13
+ # - Header sentinel: "***REDACTED***" (whole-value replace).
14
+ # - URL/console mask: "***" (param-value or field-value replace; key preserved).
15
+ #
16
+ # Sensitive HEADER name set (case-insensitive, ascii_downcase compared):
17
+ # request: authorization | cookie | x-api-key | x-auth-token
18
+ # response: set-cookie | authorization
19
+ #
20
+ # Sensitive URL PARAM key set (key=value pairs in the query string):
21
+ # api_key | token | access_token | client_secret
22
+ #
23
+ # Sensitive CONSOLE FIELD key set (case-insensitive in the message text):
24
+ # password | secret | token
25
+ #
26
+ # 7-1-ii scope: pure functions, no verb integration. 7-1-iii wires this into
27
+ # `inspect --capture-console --capture-network --capture` so console.json +
28
+ # network.har are sanitized before disk-persist.
29
+ #
30
+ # jq compatibility: avoids named-capture groups (`(?<name>...)`) since older
31
+ # jq builds reject them. Per-key sub() loop is portable across jq 1.6+.
32
+
33
+ [ -n "${_BROWSER_LIB_SANITIZE_LOADED:-}" ] && return 0
34
+ readonly _BROWSER_LIB_SANITIZE_LOADED=1
35
+
36
+ # sanitize_har — read HAR JSON from stdin, write redacted HAR to stdout.
37
+ sanitize_har() {
38
+ jq '
39
+ def _redact_header_request:
40
+ if (.name | ascii_downcase) as $n
41
+ | $n == "authorization" or $n == "cookie"
42
+ or $n == "x-api-key" or $n == "x-auth-token"
43
+ then .value = "***REDACTED***" else . end;
44
+
45
+ def _redact_header_response:
46
+ if (.name | ascii_downcase) as $n
47
+ | $n == "authorization" or $n == "set-cookie"
48
+ then .value = "***REDACTED***" else . end;
49
+
50
+ def _mask_url_params:
51
+ reduce ("api_key", "token", "access_token", "client_secret") as $k
52
+ (.; sub("(?<pre>[?&])" + $k + "=[^&]*"; "\(.pre)" + $k + "=***"));
53
+
54
+ .log.entries |= map(
55
+ .request.headers |= map(_redact_header_request)
56
+ | .response.headers |= map(_redact_header_response)
57
+ | .request.url |= _mask_url_params
58
+ )
59
+ '
60
+ }
61
+
62
+ # sanitize_console — read console-array JSON from stdin, write redacted array.
63
+ # Each entry is {level, text, ...}; text gets per-key field masking.
64
+ sanitize_console() {
65
+ jq '
66
+ def _mask_console_text:
67
+ reduce ("password", "secret", "token") as $k
68
+ (.; gsub("(?i)(?<pre>\\b" + $k + "\\b\\s*[:=]\\s*)\\S+"; "\(.pre)***"));
69
+
70
+ map(
71
+ if has("text") and (.text | type) == "string"
72
+ then .text |= _mask_console_text
73
+ else . end
74
+ )
75
+ '
76
+ }
77
+
78
+ # sanitize_inspect_reply (Phase 7 part 1-iii) — applied to the bridge's
79
+ # combined inspect reply. Sanitizes both .console_messages (via the same
80
+ # rules as sanitize_console) and .network_requests (each request entry is
81
+ # wrapped in a HAR envelope and run through sanitize_har in-memory).
82
+ # Reads inspect-shaped JSON on stdin, emits same shape with sensitive values
83
+ # redacted in place. Non-sensitive fields (verb, tool, why, status, matches,
84
+ # screenshot_path, etc.) pass through untouched. Used by browser-inspect.sh
85
+ # --capture for both stdout-side (agent-visibility) and disk-side (per-aspect
86
+ # files: console.json + network.har) sanitization — single transformation,
87
+ # both sinks.
88
+ sanitize_inspect_reply() {
89
+ local raw out
90
+ raw="$(cat)"
91
+ out="${raw}"
92
+
93
+ if printf '%s' "${raw}" | jq -e 'has("console_messages") and (.console_messages | type == "array")' >/dev/null 2>&1; then
94
+ local sc
95
+ sc="$(printf '%s' "${raw}" | jq '.console_messages' | sanitize_console)"
96
+ out="$(printf '%s' "${out}" | jq --argjson sc "${sc}" '.console_messages = $sc')"
97
+ fi
98
+
99
+ if printf '%s' "${raw}" | jq -e 'has("network_requests") and (.network_requests | type == "array")' >/dev/null 2>&1; then
100
+ local sr_envelope sr
101
+ sr_envelope="$(printf '%s' "${raw}" | jq '{log: {entries: .network_requests}}' | sanitize_har)"
102
+ sr="$(printf '%s' "${sr_envelope}" | jq '.log.entries')"
103
+ out="$(printf '%s' "${out}" | jq --argjson sr "${sr}" '.network_requests = $sr')"
104
+ fi
105
+
106
+ printf '%s' "${out}"
107
+ }
@@ -0,0 +1,91 @@
1
+ # scripts/lib/secret/keychain.sh — macOS Keychain credentials backend.
2
+ #
3
+ # Implements the 4-fn secret backend contract used by lib/credential.sh:
4
+ # secret_set NAME (stdin → keychain via `security add-generic-password`)
5
+ # secret_get NAME (keychain → stdout via `security find-generic-password -w`)
6
+ # secret_delete NAME (idempotent rm via `security delete-generic-password`)
7
+ # secret_exists NAME (probe via `security find-generic-password` no -w)
8
+ #
9
+ # All entries share a single keychain service prefix:
10
+ # ${BROWSER_SKILL_KEYCHAIN_SERVICE:-browser-skill}
11
+ # Per-credential entries use account = NAME.
12
+ #
13
+ # AP-7 documented exception:
14
+ # The macOS `security` CLI takes the password on argv via `-w PASSWORD`. There
15
+ # is no clean stdin-input path in the upstream tool — the only stdin alternative
16
+ # is an interactive TTY prompt which doesn't compose with non-TTY pipelines.
17
+ # Working around this would require either:
18
+ # - A python+keyring runtime dep (rejected: adds an external dep for one OS)
19
+ # - A compiled Swift/ObjC helper binary (rejected: adds build step)
20
+ # - osascript-mediated Keychain Services API (rejected: secrets via osascript
21
+ # -e are also argv-visible)
22
+ # The skill's own code never puts secrets on argv (`secret_set` reads stdin and
23
+ # constructs the `security` invocation locally). The leak surface is the brief
24
+ # `security` subprocess (~50ms wall-clock). Mitigations:
25
+ # 1. Subprocess is short-lived; ps polling at any practical rate misses it.
26
+ # 2. The -U flag makes the call idempotent (no second invocation needed).
27
+ # 3. Linux libsecret backend (phase-05 part 2c) uses `secret-tool` which IS
28
+ # stdin-clean — the AP-7 exception stays macOS-specific.
29
+ # This is the "honest documented exception" pattern: AP-7 is the invariant for
30
+ # our code; the upstream tool's argv-only design is an unavoidable upstream
31
+ # constraint we acknowledge in this header + the cheatsheet (when 2d ships)
32
+ # rather than work around with extra runtime deps.
33
+
34
+ [ -n "${BROWSER_SKILL_SECRET_KEYCHAIN_LOADED:-}" ] && return 0
35
+ readonly BROWSER_SKILL_SECRET_KEYCHAIN_LOADED=1
36
+
37
+ readonly _KEYCHAIN_SERVICE="${BROWSER_SKILL_KEYCHAIN_SERVICE:-browser-skill}"
38
+ readonly _KEYCHAIN_SECURITY_BIN="${KEYCHAIN_SECURITY_BIN:-security}"
39
+
40
+ # secret_set NAME — stdin → keychain via `security add-generic-password -w`.
41
+ # The -U flag makes the call idempotent: if an entry already exists for
42
+ # (-s SERVICE -a NAME), it is updated rather than rejected with an error.
43
+ secret_set() {
44
+ local name="$1"
45
+ assert_safe_name "${name}" "credential-name"
46
+ local secret
47
+ secret="$(cat)"
48
+ "${_KEYCHAIN_SECURITY_BIN}" add-generic-password \
49
+ -s "${_KEYCHAIN_SERVICE}" \
50
+ -a "${name}" \
51
+ -w "${secret}" \
52
+ -U \
53
+ >/dev/null
54
+ }
55
+
56
+ # secret_get NAME — echoes the password to stdout via `security find-generic-
57
+ # password -w`. Exits non-zero if entry is not in the keychain (security's
58
+ # native exit-44, "item could not be found").
59
+ secret_get() {
60
+ local name="$1"
61
+ assert_safe_name "${name}" "credential-name"
62
+ "${_KEYCHAIN_SECURITY_BIN}" find-generic-password \
63
+ -s "${_KEYCHAIN_SERVICE}" \
64
+ -a "${name}" \
65
+ -w \
66
+ 2>/dev/null
67
+ }
68
+
69
+ # secret_delete NAME — idempotent. `security delete-generic-password` exits
70
+ # non-zero on missing items; the `|| true` swallow makes the contract match
71
+ # the plaintext backend (and what callers expect).
72
+ secret_delete() {
73
+ local name="$1"
74
+ assert_safe_name "${name}" "credential-name"
75
+ "${_KEYCHAIN_SECURITY_BIN}" delete-generic-password \
76
+ -s "${_KEYCHAIN_SERVICE}" \
77
+ -a "${name}" \
78
+ >/dev/null 2>&1 || true
79
+ }
80
+
81
+ # secret_exists NAME — returns 0 if entry present in keychain, non-zero if not.
82
+ # Probes via `security find-generic-password` without -w (no payload echo,
83
+ # just existence check).
84
+ secret_exists() {
85
+ local name="$1"
86
+ assert_safe_name "${name}" "credential-name"
87
+ "${_KEYCHAIN_SECURITY_BIN}" find-generic-password \
88
+ -s "${_KEYCHAIN_SERVICE}" \
89
+ -a "${name}" \
90
+ >/dev/null 2>&1
91
+ }
@@ -0,0 +1,74 @@
1
+ # scripts/lib/secret/libsecret.sh — Linux libsecret credentials backend.
2
+ #
3
+ # Implements the 4-fn secret backend contract used by lib/credential.sh:
4
+ # secret_set NAME (stdin → libsecret via `secret-tool store`)
5
+ # secret_get NAME (libsecret → stdout via `secret-tool lookup`)
6
+ # secret_delete NAME (idempotent rm via `secret-tool clear`)
7
+ # secret_exists NAME (probe via `secret-tool lookup` to /dev/null)
8
+ #
9
+ # All entries share a single service attribute:
10
+ # ${BROWSER_SKILL_LIBSECRET_SERVICE:-browser-skill}
11
+ # Per-credential entries use account = NAME.
12
+ #
13
+ # AP-7 status: CLEAN — no documented exception. The upstream `secret-tool`
14
+ # CLI reads the password from stdin natively (via the `store` subcommand).
15
+ # `secret_set` reads stdin and pipes directly into `secret-tool store`;
16
+ # the password never appears in argv. Contrast with the macOS keychain
17
+ # backend (`scripts/lib/secret/keychain.sh`) which has a documented AP-7
18
+ # exception because the upstream `security` CLI is argv-only.
19
+ #
20
+ # Stdin verbatim: `secret-tool store` reads the password from stdin without
21
+ # trailing-newline strip — what you pipe in is what gets stored. Tests
22
+ # assert byte-exact roundtrip. If you `printf 'pw\n' | secret_set foo`,
23
+ # the stored value is `pw\n` (with newline). For most callers,
24
+ # `printf 'pw' | secret_set foo` (no trailing newline) is the right idiom.
25
+
26
+ [ -n "${BROWSER_SKILL_SECRET_LIBSECRET_LOADED:-}" ] && return 0
27
+ readonly BROWSER_SKILL_SECRET_LIBSECRET_LOADED=1
28
+
29
+ readonly _LIBSECRET_SERVICE="${BROWSER_SKILL_LIBSECRET_SERVICE:-browser-skill}"
30
+ readonly _LIBSECRET_TOOL_BIN="${LIBSECRET_TOOL_BIN:-secret-tool}"
31
+
32
+ # secret_set NAME — stdin → libsecret via `secret-tool store`. AP-7 clean.
33
+ # Idempotency: clear-then-store. `clear` exits non-zero on missing item;
34
+ # the swallow keeps the contract.
35
+ secret_set() {
36
+ local name="$1"
37
+ assert_safe_name "${name}" "credential-name"
38
+ "${_LIBSECRET_TOOL_BIN}" clear \
39
+ service "${_LIBSECRET_SERVICE}" account "${name}" \
40
+ >/dev/null 2>&1 || true
41
+ "${_LIBSECRET_TOOL_BIN}" store \
42
+ --label "browser-skill: ${name}" \
43
+ service "${_LIBSECRET_SERVICE}" account "${name}"
44
+ }
45
+
46
+ # secret_get NAME — echoes the password to stdout. Exits non-zero (1) if
47
+ # entry not in libsecret.
48
+ secret_get() {
49
+ local name="$1"
50
+ assert_safe_name "${name}" "credential-name"
51
+ "${_LIBSECRET_TOOL_BIN}" lookup \
52
+ service "${_LIBSECRET_SERVICE}" account "${name}"
53
+ }
54
+
55
+ # secret_delete NAME — idempotent. `secret-tool clear` exits non-zero on
56
+ # missing items; the `|| true` swallow makes the contract match the
57
+ # plaintext + keychain backends.
58
+ secret_delete() {
59
+ local name="$1"
60
+ assert_safe_name "${name}" "credential-name"
61
+ "${_LIBSECRET_TOOL_BIN}" clear \
62
+ service "${_LIBSECRET_SERVICE}" account "${name}" \
63
+ >/dev/null 2>&1 || true
64
+ }
65
+
66
+ # secret_exists NAME — returns 0 if entry present in libsecret, non-zero
67
+ # if not. Probes via `secret-tool lookup` discarding the password.
68
+ secret_exists() {
69
+ local name="$1"
70
+ assert_safe_name "${name}" "credential-name"
71
+ "${_LIBSECRET_TOOL_BIN}" lookup \
72
+ service "${_LIBSECRET_SERVICE}" account "${name}" \
73
+ >/dev/null 2>&1
74
+ }
@@ -0,0 +1,75 @@
1
+ # scripts/lib/secret/plaintext.sh — plaintext credentials backend.
2
+ #
3
+ # Implements the 4-fn secret backend contract used by lib/credential.sh:
4
+ # secret_set NAME (stdin → ${CREDENTIALS_DIR}/<name>.secret mode 0600)
5
+ # secret_get NAME (file → stdout)
6
+ # secret_delete NAME (rm -f, idempotent)
7
+ # secret_exists NAME (returns 0 if present, 1 if not)
8
+ #
9
+ # Backends are dumb I/O — they DO NOT enforce any flow logic (typed-phrase
10
+ # confirmation, --reveal masking, etc). All flow concerns live in
11
+ # lib/credential.sh and the verb scripts (Phase 5 part 2d).
12
+ #
13
+ # AP-7: secrets MUST flow via stdin pipes only. NEVER as positional argv.
14
+ # tests/secret_plaintext.bats greps this file for the anti-pattern.
15
+ #
16
+ # Sibling backends (deferred):
17
+ # - scripts/lib/secret/keychain.sh (macOS Security framework) — phase-05 part 2b
18
+ # - scripts/lib/secret/libsecret.sh (Linux Secret Service) — phase-05 part 2c
19
+ #
20
+ # Plaintext threat model: file mode 0600 + ${BROWSER_SKILL_HOME} mode 0700 +
21
+ # disk encryption (FileVault on macOS / LUKS on Linux — doctor advises). A
22
+ # user without disk encryption is warned by `doctor` (Phase 1). The verb
23
+ # layer (part 2d's `creds add`) requires a typed-phrase confirmation on
24
+ # first plaintext use. None of that policy lives here.
25
+
26
+ [ -n "${BROWSER_SKILL_SECRET_PLAINTEXT_LOADED:-}" ] && return 0
27
+ readonly BROWSER_SKILL_SECRET_PLAINTEXT_LOADED=1
28
+
29
+ _secret_plaintext_path() {
30
+ printf '%s/%s.secret' "${CREDENTIALS_DIR}" "$1"
31
+ }
32
+
33
+ # secret_set NAME — reads stdin, writes ${CREDENTIALS_DIR}/<name>.secret 0600.
34
+ # Atomically: writes to a tmp file then renames. Overwrites existing payload.
35
+ secret_set() {
36
+ local name="$1"
37
+ assert_safe_name "${name}" "credential-name"
38
+
39
+ mkdir -p "${CREDENTIALS_DIR}"
40
+ chmod 700 "${CREDENTIALS_DIR}"
41
+
42
+ local path tmp
43
+ path="$(_secret_plaintext_path "${name}")"
44
+ tmp="${path}.tmp.$$"
45
+
46
+ ( umask 077; cat > "${tmp}" )
47
+ chmod 600 "${tmp}"
48
+ mv "${tmp}" "${path}"
49
+ }
50
+
51
+ # secret_get NAME — echoes the payload to stdout. Exits non-zero on missing.
52
+ secret_get() {
53
+ local name="$1"
54
+ assert_safe_name "${name}" "credential-name"
55
+ local path
56
+ path="$(_secret_plaintext_path "${name}")"
57
+ if [ ! -f "${path}" ]; then
58
+ die "${EXIT_USAGE_ERROR}" "secret not found: ${name}"
59
+ fi
60
+ cat "${path}"
61
+ }
62
+
63
+ # secret_delete NAME — idempotent rm -f.
64
+ secret_delete() {
65
+ local name="$1"
66
+ assert_safe_name "${name}" "credential-name"
67
+ rm -f "$(_secret_plaintext_path "${name}")"
68
+ }
69
+
70
+ # secret_exists NAME — returns 0 if present, 1 if not.
71
+ secret_exists() {
72
+ local name="$1"
73
+ assert_safe_name "${name}" "credential-name"
74
+ [ -f "$(_secret_plaintext_path "${name}")" ]
75
+ }