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.
- package/LICENSE +21 -0
- package/README.md +144 -0
- package/SECURITY.md +39 -0
- package/SKILL.md +206 -0
- package/bin/cli.mjs +55 -0
- package/install.sh +143 -0
- package/package.json +54 -0
- package/references/adapter-candidates.md +40 -0
- package/references/browser-mcp-cheatsheet.md +132 -0
- package/references/browser-stats-cheatsheet.md +155 -0
- package/references/chrome-devtools-mcp-cheatsheet.md +232 -0
- package/references/midscene-integration.md +359 -0
- package/references/obscura-cheatsheet.md +103 -0
- package/references/playwright-cli-cheatsheet.md +64 -0
- package/references/playwright-lib-cheatsheet.md +90 -0
- package/references/recipes/add-a-tool-adapter.md +134 -0
- package/references/recipes/agent-workflows/README.md +37 -0
- package/references/recipes/agent-workflows/cache-driven-bulk-operation.md +110 -0
- package/references/recipes/agent-workflows/flow-record-and-replay.md +102 -0
- package/references/recipes/agent-workflows/incremental-pattern-discovery.md +125 -0
- package/references/recipes/agent-workflows/login-then-scrape.md +100 -0
- package/references/recipes/anti-patterns-tool-extension.md +182 -0
- package/references/recipes/body-bytes-not-body.md +139 -0
- package/references/recipes/cache-write-security.md +210 -0
- package/references/recipes/fingerprint-rescue.md +154 -0
- package/references/recipes/model-routing.md +143 -0
- package/references/recipes/path-security.md +138 -0
- package/references/recipes/privacy-canary.md +96 -0
- package/references/recipes/visual-rescue-hook.md +182 -0
- package/references/stats-prices.json +42 -0
- package/references/stats-schema.json +77 -0
- package/references/tool-versions.md +8 -0
- package/scripts/browser-add-site.sh +113 -0
- package/scripts/browser-assert.sh +106 -0
- package/scripts/browser-audit.sh +68 -0
- package/scripts/browser-baseline.sh +135 -0
- package/scripts/browser-click.sh +100 -0
- package/scripts/browser-creds-add.sh +254 -0
- package/scripts/browser-creds-list.sh +67 -0
- package/scripts/browser-creds-migrate.sh +122 -0
- package/scripts/browser-creds-remove.sh +69 -0
- package/scripts/browser-creds-rotate-totp.sh +109 -0
- package/scripts/browser-creds-show.sh +82 -0
- package/scripts/browser-creds-totp.sh +94 -0
- package/scripts/browser-do.sh +630 -0
- package/scripts/browser-doctor.sh +365 -0
- package/scripts/browser-drag.sh +90 -0
- package/scripts/browser-extract.sh +192 -0
- package/scripts/browser-fill.sh +142 -0
- package/scripts/browser-flow.sh +316 -0
- package/scripts/browser-history.sh +187 -0
- package/scripts/browser-hover.sh +92 -0
- package/scripts/browser-inspect.sh +188 -0
- package/scripts/browser-list-sessions.sh +78 -0
- package/scripts/browser-list-sites.sh +42 -0
- package/scripts/browser-login.sh +279 -0
- package/scripts/browser-mcp.sh +65 -0
- package/scripts/browser-migrate.sh +195 -0
- package/scripts/browser-open.sh +134 -0
- package/scripts/browser-press.sh +80 -0
- package/scripts/browser-remove-session.sh +72 -0
- package/scripts/browser-remove-site.sh +68 -0
- package/scripts/browser-replay.sh +206 -0
- package/scripts/browser-route.sh +174 -0
- package/scripts/browser-select.sh +122 -0
- package/scripts/browser-show-session.sh +57 -0
- package/scripts/browser-show-site.sh +37 -0
- package/scripts/browser-snapshot.sh +176 -0
- package/scripts/browser-stats.sh +522 -0
- package/scripts/browser-tab-close.sh +112 -0
- package/scripts/browser-tab-list.sh +70 -0
- package/scripts/browser-tab-switch.sh +111 -0
- package/scripts/browser-upload.sh +132 -0
- package/scripts/browser-use.sh +60 -0
- package/scripts/browser-vlm.sh +707 -0
- package/scripts/browser-wait.sh +97 -0
- package/scripts/install-git-hooks.sh +16 -0
- package/scripts/lib/capture.sh +356 -0
- package/scripts/lib/common.sh +262 -0
- package/scripts/lib/credential.sh +237 -0
- package/scripts/lib/fingerprint-rescue.js +123 -0
- package/scripts/lib/flow.sh +448 -0
- package/scripts/lib/flow_record.sh +210 -0
- package/scripts/lib/mask.sh +49 -0
- package/scripts/lib/memory.sh +427 -0
- package/scripts/lib/migrate.sh +390 -0
- package/scripts/lib/migrators/README.md +23 -0
- package/scripts/lib/migrators/memory/v1_to_v2.sh +15 -0
- package/scripts/lib/migrators/recent_urls/README.md +13 -0
- package/scripts/lib/migrators/stats/README.md +24 -0
- package/scripts/lib/node/chrome-devtools-bridge.mjs +1812 -0
- package/scripts/lib/node/mcp-server.mjs +531 -0
- package/scripts/lib/node/mcp-tools.json +68 -0
- package/scripts/lib/node/playwright-driver.mjs +1104 -0
- package/scripts/lib/node/totp-core.mjs +52 -0
- package/scripts/lib/node/totp.mjs +52 -0
- package/scripts/lib/node/url-pattern-cluster.mjs +102 -0
- package/scripts/lib/node/url-pattern-resolver.mjs +77 -0
- package/scripts/lib/output.sh +79 -0
- package/scripts/lib/router.sh +342 -0
- package/scripts/lib/sanitize.sh +107 -0
- package/scripts/lib/secret/keychain.sh +91 -0
- package/scripts/lib/secret/libsecret.sh +74 -0
- package/scripts/lib/secret/plaintext.sh +75 -0
- package/scripts/lib/secret_backend_select.sh +57 -0
- package/scripts/lib/session.sh +153 -0
- package/scripts/lib/site.sh +126 -0
- package/scripts/lib/stats.sh +419 -0
- package/scripts/lib/tool/.gitkeep +0 -0
- package/scripts/lib/tool/chrome-devtools-mcp.sh +349 -0
- package/scripts/lib/tool/obscura.sh +249 -0
- package/scripts/lib/tool/playwright-cli.sh +155 -0
- package/scripts/lib/tool/playwright-lib.sh +106 -0
- package/scripts/lib/verb_helpers.sh +222 -0
- package/scripts/lib/visual-rescue-default.sh +145 -0
- package/scripts/regenerate-docs.sh +99 -0
- 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
|
+
}
|