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,419 @@
|
|
|
1
|
+
# shellcheck shell=bash
|
|
2
|
+
# scripts/lib/stats.sh — Phase 12 part 1: per-action telemetry / "balance triangle" audit.
|
|
3
|
+
#
|
|
4
|
+
# Emits one JSONL event per adapter invocation to
|
|
5
|
+
# ${BROWSER_SKILL_HOME}/memory/stats.jsonl (mode 0600, parent 0700).
|
|
6
|
+
# Each event captures: route, verb, selector kind/hit, retries, duration,
|
|
7
|
+
# stdout/stderr byte sizes (token proxies for the bash skill), post-condition
|
|
8
|
+
# result, and a 13-value failure-mode enum.
|
|
9
|
+
#
|
|
10
|
+
# Optional gen_ai.* token fields are populated only when Claude Code injects
|
|
11
|
+
# them via env (CLAUDE_USAGE_INPUT_TOKENS / _OUTPUT_TOKENS / _CACHE_READ_TOKENS
|
|
12
|
+
# / _CACHE_CREATE_TOKENS / _MODEL / _SERVICE_TIER) — left null otherwise.
|
|
13
|
+
# Field naming follows OpenInference + OTel GenAI v1.40 conventions so the log
|
|
14
|
+
# is forward-compatible with Langfuse/Phoenix/Jaeger via an OTLP exporter.
|
|
15
|
+
#
|
|
16
|
+
# Best-effort writer — failure NEVER taints the verb's exit code. Mirrors the
|
|
17
|
+
# contract of scripts/browser-do.sh::_record_event and lib/memory.sh::
|
|
18
|
+
# memory_record_recent_url (Phase 11 v2 prior art).
|
|
19
|
+
#
|
|
20
|
+
# Public API:
|
|
21
|
+
# stats_init_dir — lazy-create memory dir mode 0700
|
|
22
|
+
# stats_random_id — 16-hex-char id (fork-free $RANDOM
|
|
23
|
+
# unless STATS_USE_CRYPTO_ID=1)
|
|
24
|
+
# stats_now_iso_ms — ISO 8601 with ms precision; uses
|
|
25
|
+
# $EPOCHREALTIME (bash 5.0+) — no fork
|
|
26
|
+
# stats_classify_failure RC OUT ERR — echo one failure_mode enum value
|
|
27
|
+
# stats_postcond_check T M EXP OBS — return 0/1; sets STATS_POSTCOND_HIT
|
|
28
|
+
# stats_extract_selector_meta ARGS... — sets STATS_SEL_KIND, STATS_SEL_VALUE
|
|
29
|
+
# stats_emit_event JSON_OBJECT — append one JSONL line (best-effort)
|
|
30
|
+
# stats_run_adapter_emit VERB ROUTE T0 RC STDOUT STDERR -- ARGS...
|
|
31
|
+
# — convenience helper for verb scripts.
|
|
32
|
+
# Post-condition contract comes via env:
|
|
33
|
+
# STATS_EXPECT_TYPE ∈ url|element_path|element_value
|
|
34
|
+
# STATS_EXPECT_MATCH ∈ exact|include|semantic (default: include)
|
|
35
|
+
# STATS_EXPECT_VALUE (string; "" disables check)
|
|
36
|
+
# STATS_OBSERVED (string the verb measured)
|
|
37
|
+
#
|
|
38
|
+
# Performance notes (Phase 12 part 2 — audit improvements):
|
|
39
|
+
# - Bash 5.0+ required. macOS users need Homebrew bash (already the case for
|
|
40
|
+
# this skill's other bash-isms). $EPOCHREALTIME + LC_ALL=C ${#var} + $RANDOM
|
|
41
|
+
# replace ~9 forks per emit; one jq invocation remains.
|
|
42
|
+
# - chmod 600 runs only on file creation, not every emit.
|
|
43
|
+
#
|
|
44
|
+
# Schema: see references/stats-schema.json. Schema version: 1.
|
|
45
|
+
|
|
46
|
+
[ -n "${BROWSER_SKILL_STATS_LOADED:-}" ] && return 0
|
|
47
|
+
readonly BROWSER_SKILL_STATS_LOADED=1
|
|
48
|
+
|
|
49
|
+
readonly STATS_SCHEMA_VERSION=1
|
|
50
|
+
|
|
51
|
+
# Failure-mode enum (synced with WAREX + Agent-E + WebVoyager taxonomies).
|
|
52
|
+
# Update references/stats-schema.json::failure_mode.enum in lockstep.
|
|
53
|
+
# Phase 14 (Bundle #3) added `unknown_failure` as the catch-all so rc!=0 events
|
|
54
|
+
# never silently drop out of the histogram. Real telemetry (35-event sample)
|
|
55
|
+
# had 100% of rc!=0 events with failure_mode=null — invisible to `stats tune`.
|
|
56
|
+
# shellcheck disable=SC2034 # documentation constant; consumers grep this for the canonical list
|
|
57
|
+
readonly STATS_FAILURE_MODES="element_not_found element_ambiguous wrong_element_acted stale_ref action_timeout navigation_mismatch js_not_ready network_error captcha_blocked auth_required popup_intercept extraction_mismatch oblivious_success unknown_failure"
|
|
58
|
+
|
|
59
|
+
# stats_init_dir — idempotent mkdir + chmod for ${BROWSER_SKILL_HOME}/memory/.
|
|
60
|
+
# Same lazy-create pattern as memory_init_dir in lib/memory.sh; duplicated here
|
|
61
|
+
# so callers that don't source memory.sh can still emit (e.g. extract verb).
|
|
62
|
+
stats_init_dir() {
|
|
63
|
+
local dir
|
|
64
|
+
dir="${BROWSER_SKILL_HOME}/memory"
|
|
65
|
+
mkdir -p "${dir}" 2>/dev/null || return 1
|
|
66
|
+
chmod 700 "${dir}" 2>/dev/null || true
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
# stats_random_id — 16 hex chars. Fork-free $RANDOM-based by default (~60 bits
|
|
70
|
+
# effective entropy; adequate for in-session correlation IDs, NOT cryptography).
|
|
71
|
+
# Set STATS_USE_CRYPTO_ID=1 to fall back to `openssl rand -hex 8` when crypto-
|
|
72
|
+
# strength uniqueness is needed (e.g. cross-session log export).
|
|
73
|
+
stats_random_id() {
|
|
74
|
+
if [ "${STATS_USE_CRYPTO_ID:-0}" = "1" ] && command -v openssl >/dev/null 2>&1; then
|
|
75
|
+
openssl rand -hex 8
|
|
76
|
+
else
|
|
77
|
+
# 4 × $RANDOM (15 bits each, %04x pads to 4 hex chars). printf -v keeps
|
|
78
|
+
# the result in a var without forking; piping to stdout is one printf call.
|
|
79
|
+
local _hex
|
|
80
|
+
printf -v _hex '%04x%04x%04x%04x' "${RANDOM}" "${RANDOM}" "${RANDOM}" "${RANDOM}"
|
|
81
|
+
printf '%s\n' "${_hex}"
|
|
82
|
+
fi
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
# stats_now_iso_ms — ISO 8601 with ms precision, UTC. Uses bash 5.0+ $EPOCHREALTIME
|
|
86
|
+
# (no fork) + the bash-4.2+ `printf -v %()T` builtin date formatter (no fork).
|
|
87
|
+
# Fallback for bash <5.0 is a single `date -u` fork at second precision — no
|
|
88
|
+
# python3 dependency, no millisecond gymnastics.
|
|
89
|
+
stats_now_iso_ms() {
|
|
90
|
+
if [ -n "${EPOCHREALTIME:-}" ]; then
|
|
91
|
+
local secs=${EPOCHREALTIME%.*}
|
|
92
|
+
local frac=${EPOCHREALTIME#*.}
|
|
93
|
+
local ms=${frac:0:3}
|
|
94
|
+
local _ts
|
|
95
|
+
# %()T strftime reads TZ at call time; env-prefix sets it for this builtin.
|
|
96
|
+
TZ=UTC printf -v _ts '%(%Y-%m-%dT%H:%M:%S)T.%sZ' "${secs}" "${ms}"
|
|
97
|
+
printf '%s\n' "${_ts}"
|
|
98
|
+
else
|
|
99
|
+
# Legacy fallback (bash <5.0). Second precision only.
|
|
100
|
+
date -u +%Y-%m-%dT%H:%M:%SZ
|
|
101
|
+
fi
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
# stats_classify_failure RC STDOUT STDERR
|
|
105
|
+
# Echo one failure_mode enum value (or empty when outcome is success).
|
|
106
|
+
# Heuristic — looks for adapter-specific markers in stdout/stderr.
|
|
107
|
+
# RC == 0 → empty (no failure).
|
|
108
|
+
# Otherwise pattern-match exit codes + error text. Phase 14 (Bundle #3): when
|
|
109
|
+
# no pattern matches but rc!=0, fall back to `unknown_failure` (was empty).
|
|
110
|
+
# Empty silently dropped events from the histogram so `stats tune` couldn't
|
|
111
|
+
# surface them. The `unknown_failure` bucket is the explicit "we know it failed
|
|
112
|
+
# but couldn't classify" signal — actionable for adding new patterns later.
|
|
113
|
+
stats_classify_failure() {
|
|
114
|
+
local rc="$1" out="$2" err="$3"
|
|
115
|
+
[ "${rc}" = "0" ] && return 0
|
|
116
|
+
|
|
117
|
+
# Exit code first (common.sh constants).
|
|
118
|
+
case "${rc}" in
|
|
119
|
+
13) printf 'extraction_mismatch\n'; return 0 ;; # EXIT_ASSERTION_FAILED
|
|
120
|
+
22) printf 'auth_required\n'; return 0 ;; # EXIT_SESSION_EXPIRED
|
|
121
|
+
25) printf 'auth_required\n'; return 0 ;; # EXIT_AUTH_INTERACTIVE_REQUIRED
|
|
122
|
+
30) printf 'network_error\n'; return 0 ;; # EXIT_NETWORK_ERROR
|
|
123
|
+
43) printf 'action_timeout\n'; return 0 ;; # EXIT_TOOL_TIMEOUT
|
|
124
|
+
esac
|
|
125
|
+
|
|
126
|
+
local combined="${out}${err}"
|
|
127
|
+
# Order matters — earlier patterns win on overlap. Each branch lists
|
|
128
|
+
# NON-OVERLAPPING substrings: e.g. *"captcha"* already matches "hcaptcha"
|
|
129
|
+
# and "recaptcha", so listing those separately would be dead code.
|
|
130
|
+
case "${combined}" in
|
|
131
|
+
*"captcha"*|*"Cloudflare"*)
|
|
132
|
+
printf 'captcha_blocked\n'; return 0 ;;
|
|
133
|
+
*"login required"*|*"unauthorized"*|*" 401 "*|*" 403 "*)
|
|
134
|
+
printf 'auth_required\n'; return 0 ;;
|
|
135
|
+
*"strict mode"*|*"ambiguous"*)
|
|
136
|
+
printf 'element_ambiguous\n'; return 0 ;;
|
|
137
|
+
*"not found"*|*"no element"*|*"Target closed"*|*"detached"*)
|
|
138
|
+
printf 'element_not_found\n'; return 0 ;;
|
|
139
|
+
*"stale "*|*"snapshot is outdated"*|*"ref expired"*|*"invalid ref"*)
|
|
140
|
+
printf 'stale_ref\n'; return 0 ;;
|
|
141
|
+
*"timeout"*|*"timed out"*|*"exceeded"*)
|
|
142
|
+
printf 'action_timeout\n'; return 0 ;;
|
|
143
|
+
*"net::ERR"*|*"ECONNREFUSED"*|*"ENOTFOUND"*|*"ETIMEDOUT"*)
|
|
144
|
+
printf 'network_error\n'; return 0 ;;
|
|
145
|
+
*"navigation"*|*"redirect"*|*"URL did not match"*)
|
|
146
|
+
printf 'navigation_mismatch\n'; return 0 ;;
|
|
147
|
+
*"modal"*|*"dialog"*|*"consent"*|*"popup"*)
|
|
148
|
+
printf 'popup_intercept\n'; return 0 ;;
|
|
149
|
+
*"script error"*|*"ReferenceError"*|*"TypeError"*)
|
|
150
|
+
printf 'js_not_ready\n'; return 0 ;;
|
|
151
|
+
esac
|
|
152
|
+
# No pattern matched but rc!=0 — explicit catch-all so the event surfaces in
|
|
153
|
+
# `stats tune` histograms instead of being silently dropped as null.
|
|
154
|
+
printf 'unknown_failure\n'
|
|
155
|
+
return 0
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
# stats_postcond_check TYPE MATCHER EXPECTED OBSERVED
|
|
159
|
+
# Verify a post-condition. TYPE ∈ {url, element_path, element_value};
|
|
160
|
+
# MATCHER ∈ {exact, include, semantic}. Returns 0 on hit, 1 on miss.
|
|
161
|
+
# Sets STATS_POSTCOND_HIT="true"/"false" so callers can serialize.
|
|
162
|
+
# Semantic matcher v1 = case-insensitive substring (placeholder for LLM-judge).
|
|
163
|
+
stats_postcond_check() {
|
|
164
|
+
local type="$1" matcher="$2" expected="$3" observed="$4"
|
|
165
|
+
STATS_POSTCOND_HIT="false"
|
|
166
|
+
[ -z "${type}" ] && return 1
|
|
167
|
+
[ -z "${expected}" ] && return 1
|
|
168
|
+
case "${matcher}" in
|
|
169
|
+
exact)
|
|
170
|
+
[ "${expected}" = "${observed}" ] && STATS_POSTCOND_HIT="true"
|
|
171
|
+
;;
|
|
172
|
+
include)
|
|
173
|
+
case "${observed}" in
|
|
174
|
+
*"${expected}"*) STATS_POSTCOND_HIT="true" ;;
|
|
175
|
+
esac
|
|
176
|
+
;;
|
|
177
|
+
semantic)
|
|
178
|
+
# v1 placeholder: case-insensitive substring. Upgrade path = LLM-judge.
|
|
179
|
+
local exp_lc="${expected,,}" obs_lc="${observed,,}"
|
|
180
|
+
case "${obs_lc}" in
|
|
181
|
+
*"${exp_lc}"*) STATS_POSTCOND_HIT="true" ;;
|
|
182
|
+
esac
|
|
183
|
+
;;
|
|
184
|
+
*)
|
|
185
|
+
return 1
|
|
186
|
+
;;
|
|
187
|
+
esac
|
|
188
|
+
[ "${STATS_POSTCOND_HIT}" = "true" ]
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
# stats_extract_selector_meta ARGS...
|
|
192
|
+
# Walks an argv looking for --ref / --selector / --src-ref / --dst-ref and
|
|
193
|
+
# sets STATS_SEL_KIND ∈ {a11y_ref, css, role, text, none} + STATS_SEL_VALUE.
|
|
194
|
+
# Read-only on argv — does not modify positional params.
|
|
195
|
+
stats_extract_selector_meta() {
|
|
196
|
+
STATS_SEL_KIND="none"
|
|
197
|
+
STATS_SEL_VALUE=""
|
|
198
|
+
local prev=""
|
|
199
|
+
local a
|
|
200
|
+
for a in "$@"; do
|
|
201
|
+
case "${prev}" in
|
|
202
|
+
--ref|--src-ref|--dst-ref)
|
|
203
|
+
STATS_SEL_KIND="a11y_ref"
|
|
204
|
+
STATS_SEL_VALUE="${a}"
|
|
205
|
+
return 0
|
|
206
|
+
;;
|
|
207
|
+
--selector)
|
|
208
|
+
STATS_SEL_VALUE="${a}"
|
|
209
|
+
case "${a}" in
|
|
210
|
+
'role='*|"[role="*) STATS_SEL_KIND="role" ;;
|
|
211
|
+
'text='*|*':has-text('*) STATS_SEL_KIND="text" ;;
|
|
212
|
+
*) STATS_SEL_KIND="css" ;;
|
|
213
|
+
esac
|
|
214
|
+
return 0
|
|
215
|
+
;;
|
|
216
|
+
esac
|
|
217
|
+
prev="${a}"
|
|
218
|
+
done
|
|
219
|
+
return 0
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
# stats_emit_event JSON_OBJECT
|
|
223
|
+
# Append one canonicalised JSONL line to memory/stats.jsonl. Best-effort —
|
|
224
|
+
# emits warn: on failure but never taints exit code.
|
|
225
|
+
# Phase 12 part 2: chmod runs only on file creation, not every emit (eliminates
|
|
226
|
+
# one fork per write on the hot path).
|
|
227
|
+
stats_emit_event() {
|
|
228
|
+
local payload="$1"
|
|
229
|
+
if ! stats_init_dir; then
|
|
230
|
+
warn "stats: could not create memory dir; event skipped"
|
|
231
|
+
return 0
|
|
232
|
+
fi
|
|
233
|
+
local file="${BROWSER_SKILL_HOME}/memory/stats.jsonl"
|
|
234
|
+
local needs_chmod=0
|
|
235
|
+
[ -f "${file}" ] || needs_chmod=1
|
|
236
|
+
local line
|
|
237
|
+
if ! line="$(printf '%s' "${payload}" | jq -c . 2>/dev/null)"; then
|
|
238
|
+
warn "stats: jq encode failed (best-effort; action exit unchanged)"
|
|
239
|
+
return 0
|
|
240
|
+
fi
|
|
241
|
+
if ! printf '%s\n' "${line}" >> "${file}" 2>/dev/null; then
|
|
242
|
+
warn "stats: append failed (best-effort; action exit unchanged)"
|
|
243
|
+
return 0
|
|
244
|
+
fi
|
|
245
|
+
[ "${needs_chmod}" = "1" ] && chmod 600 "${file}" 2>/dev/null
|
|
246
|
+
return 0
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
# stats_run_adapter_emit VERB ROUTE T0_MS RC STDOUT STDERR -- ARGS...
|
|
250
|
+
# Convenience wrapper that builds + emits a complete event from raw bash vars.
|
|
251
|
+
# Caller passes already-captured stdout/stderr (as strings); we compute byte
|
|
252
|
+
# sizes from them. ARGS... is the verb's own argv (used by selector extractor).
|
|
253
|
+
#
|
|
254
|
+
# Post-condition contract comes via env (kept out of positional args to keep
|
|
255
|
+
# the call-site readable — see Phase 12 part 2 audit):
|
|
256
|
+
# STATS_EXPECT_TYPE ∈ url | element_path | element_value
|
|
257
|
+
# STATS_EXPECT_MATCH ∈ exact | include | semantic (default: include)
|
|
258
|
+
# STATS_EXPECT_VALUE string ("" disables check)
|
|
259
|
+
# STATS_OBSERVED string the verb measured
|
|
260
|
+
# Verb scripts conventionally export BROWSER_STATS_EXPECT_* / BROWSER_STATS_OBSERVED
|
|
261
|
+
# and this helper reads either prefix (BROWSER_STATS_* preferred, STATS_* legacy).
|
|
262
|
+
stats_run_adapter_emit() {
|
|
263
|
+
local verb="$1" route="$2" t0_ms="$3" rc="$4"
|
|
264
|
+
local stdout="$5" stderr="$6"
|
|
265
|
+
shift 6
|
|
266
|
+
[ "${1:-}" = "--" ] && shift
|
|
267
|
+
|
|
268
|
+
# Resolve post-condition fields from env (BROWSER_STATS_* wins; STATS_* legacy fallback).
|
|
269
|
+
local exp_type="${BROWSER_STATS_EXPECT_TYPE:-${STATS_EXPECT_TYPE:-}}"
|
|
270
|
+
local exp_match="${BROWSER_STATS_EXPECT_MATCH:-${STATS_EXPECT_MATCH:-include}}"
|
|
271
|
+
local exp_value="${BROWSER_STATS_EXPECT_VALUE:-${STATS_EXPECT_VALUE:-}}"
|
|
272
|
+
local observed="${BROWSER_STATS_OBSERVED:-${STATS_OBSERVED:-}}"
|
|
273
|
+
|
|
274
|
+
local t1_ms duration_ms
|
|
275
|
+
t1_ms="$(now_ms)"
|
|
276
|
+
duration_ms=$(( t1_ms - t0_ms ))
|
|
277
|
+
|
|
278
|
+
stats_extract_selector_meta "$@"
|
|
279
|
+
|
|
280
|
+
# Phase 14 (Bundle #3): span_id generated up-front so the unknown_failure
|
|
281
|
+
# self-healing hint can quote the exact event the operator will grep for.
|
|
282
|
+
local span_id trace_id ts
|
|
283
|
+
span_id="$(stats_random_id)"
|
|
284
|
+
trace_id="${BROWSER_SKILL_TRACE_ID:-${span_id}}"
|
|
285
|
+
ts="$(stats_now_iso_ms)"
|
|
286
|
+
|
|
287
|
+
local failure_mode outcome
|
|
288
|
+
if [ "${rc}" = "0" ]; then
|
|
289
|
+
outcome="success"
|
|
290
|
+
failure_mode=""
|
|
291
|
+
else
|
|
292
|
+
outcome="fail"
|
|
293
|
+
failure_mode="$(stats_classify_failure "${rc}" "${stdout}" "${stderr}")"
|
|
294
|
+
# Phase 14 (Bundle #3): self-healing hint per spec §2.5 when classifier
|
|
295
|
+
# bucketed the failure as unknown. Tells operator exactly where to look
|
|
296
|
+
# to extend the pattern table — span_id lets them grep stats.jsonl for
|
|
297
|
+
# the full event including argv/stderr.
|
|
298
|
+
if [ "${failure_mode}" = "unknown_failure" ]; then
|
|
299
|
+
warn "${verb} exited rc=${rc}; no diagnosable signal — span_id=${span_id} in memory/stats.jsonl"
|
|
300
|
+
fi
|
|
301
|
+
fi
|
|
302
|
+
|
|
303
|
+
STATS_POSTCOND_HIT=""
|
|
304
|
+
local postcond_hit_json="null"
|
|
305
|
+
if [ -n "${exp_type}" ] && [ -n "${exp_value}" ]; then
|
|
306
|
+
if stats_postcond_check "${exp_type}" "${exp_match:-include}" "${exp_value}" "${observed}"; then
|
|
307
|
+
postcond_hit_json="true"
|
|
308
|
+
else
|
|
309
|
+
postcond_hit_json="false"
|
|
310
|
+
# Oblivious-success detection: adapter said OK but post-condition fails.
|
|
311
|
+
if [ "${outcome}" = "success" ]; then
|
|
312
|
+
outcome="partial"
|
|
313
|
+
failure_mode="oblivious_success"
|
|
314
|
+
fi
|
|
315
|
+
fi
|
|
316
|
+
fi
|
|
317
|
+
|
|
318
|
+
local site
|
|
319
|
+
# span_id, trace_id, ts already populated above so the unknown_failure hint
|
|
320
|
+
# can reference span_id without forward-declaring vars.
|
|
321
|
+
site="${ARG_SITE:-}"
|
|
322
|
+
if [ -z "${site}" ] && command -v current_get >/dev/null 2>&1; then
|
|
323
|
+
site="$(current_get 2>/dev/null || true)"
|
|
324
|
+
fi
|
|
325
|
+
|
|
326
|
+
# Byte counts via LC_ALL=C ${#var} — bash builtin, fork-free.
|
|
327
|
+
# `${#var}` returns CHARS in UTF-8 locale, BYTES under LC_ALL=C. Save+restore
|
|
328
|
+
# the prior LC_ALL so the rest of the function (jq) sees its original locale.
|
|
329
|
+
local argv_bytes stdout_bytes stderr_bytes _argv_str _saved_lc="${LC_ALL-}"
|
|
330
|
+
LC_ALL=C
|
|
331
|
+
_argv_str="$*"
|
|
332
|
+
argv_bytes=${#_argv_str}
|
|
333
|
+
stdout_bytes=${#stdout}
|
|
334
|
+
stderr_bytes=${#stderr}
|
|
335
|
+
if [ -z "${_saved_lc}" ]; then
|
|
336
|
+
unset LC_ALL
|
|
337
|
+
else
|
|
338
|
+
LC_ALL="${_saved_lc}"
|
|
339
|
+
fi
|
|
340
|
+
|
|
341
|
+
# Token fields from env (Claude Code injects when available; null otherwise).
|
|
342
|
+
local model="${CLAUDE_MODEL:-${ANTHROPIC_MODEL:-}}"
|
|
343
|
+
local input_tokens="${CLAUDE_USAGE_INPUT_TOKENS:-}"
|
|
344
|
+
local output_tokens="${CLAUDE_USAGE_OUTPUT_TOKENS:-}"
|
|
345
|
+
local cache_read="${CLAUDE_USAGE_CACHE_READ_TOKENS:-}"
|
|
346
|
+
local cache_create="${CLAUDE_USAGE_CACHE_CREATE_TOKENS:-}"
|
|
347
|
+
local service_tier="${CLAUDE_SERVICE_TIER:-}"
|
|
348
|
+
local session_id="${CLAUDE_SESSION_ID:-${BROWSER_SKILL_SESSION_ID:-}}"
|
|
349
|
+
|
|
350
|
+
# Build event JSON via jq for safe escaping (never bash-interpolate JSON strings).
|
|
351
|
+
local event
|
|
352
|
+
event=$(jq -nc \
|
|
353
|
+
--argjson schema_version "${STATS_SCHEMA_VERSION}" \
|
|
354
|
+
--arg ts "${ts}" \
|
|
355
|
+
--arg span_id "${span_id}" \
|
|
356
|
+
--arg trace_id "${trace_id}" \
|
|
357
|
+
--arg parent_span_id "${BROWSER_SKILL_PARENT_SPAN_ID:-}" \
|
|
358
|
+
--arg session_id "${session_id}" \
|
|
359
|
+
--arg verb "${verb}" \
|
|
360
|
+
--arg adapter_route "${route}" \
|
|
361
|
+
--arg gen_ai_tool_name "${route}.${verb}" \
|
|
362
|
+
--arg site "${site}" \
|
|
363
|
+
--arg selector_kind "${STATS_SEL_KIND}" \
|
|
364
|
+
--arg selector_value "${STATS_SEL_VALUE}" \
|
|
365
|
+
--argjson duration_ms "${duration_ms}" \
|
|
366
|
+
--argjson argv_bytes "${argv_bytes:-0}" \
|
|
367
|
+
--argjson stdout_bytes "${stdout_bytes:-0}" \
|
|
368
|
+
--argjson stderr_bytes "${stderr_bytes:-0}" \
|
|
369
|
+
--argjson rc "${rc}" \
|
|
370
|
+
--arg outcome "${outcome}" \
|
|
371
|
+
--arg failure_mode "${failure_mode}" \
|
|
372
|
+
--arg model "${model}" \
|
|
373
|
+
--arg service_tier "${service_tier}" \
|
|
374
|
+
--arg input_tokens "${input_tokens}" \
|
|
375
|
+
--arg output_tokens "${output_tokens}" \
|
|
376
|
+
--arg cache_read "${cache_read}" \
|
|
377
|
+
--arg cache_create "${cache_create}" \
|
|
378
|
+
--arg exp_type "${exp_type}" \
|
|
379
|
+
--arg exp_match "${exp_match}" \
|
|
380
|
+
--arg exp_value "${exp_value}" \
|
|
381
|
+
--arg observed "${observed}" \
|
|
382
|
+
--argjson postcond_hit "${postcond_hit_json}" '
|
|
383
|
+
{
|
|
384
|
+
schema_version: $schema_version,
|
|
385
|
+
ts: $ts,
|
|
386
|
+
span_id: $span_id,
|
|
387
|
+
trace_id: $trace_id,
|
|
388
|
+
parent_span_id: ($parent_span_id | select(. != "") // null),
|
|
389
|
+
session_id: ($session_id | select(. != "") // null),
|
|
390
|
+
gen_ai_operation_name: "execute_tool",
|
|
391
|
+
gen_ai_tool_name: $gen_ai_tool_name,
|
|
392
|
+
gen_ai_tool_type: "function",
|
|
393
|
+
verb: $verb,
|
|
394
|
+
adapter_route: $adapter_route,
|
|
395
|
+
site: ($site | select(. != "") // null),
|
|
396
|
+
selector_kind: $selector_kind,
|
|
397
|
+
selector_value: ($selector_value | select(. != "") // null),
|
|
398
|
+
duration_ms: $duration_ms,
|
|
399
|
+
argv_bytes: $argv_bytes,
|
|
400
|
+
stdout_bytes: $stdout_bytes,
|
|
401
|
+
stderr_bytes: $stderr_bytes,
|
|
402
|
+
rc: $rc,
|
|
403
|
+
outcome: $outcome,
|
|
404
|
+
failure_mode: ($failure_mode | select(. != "") // null),
|
|
405
|
+
model: ($model | select(. != "") // null),
|
|
406
|
+
service_tier: ($service_tier | select(. != "") // null),
|
|
407
|
+
gen_ai_usage_input_tokens: (if $input_tokens == "" then null else ($input_tokens | tonumber) end),
|
|
408
|
+
gen_ai_usage_output_tokens: (if $output_tokens == "" then null else ($output_tokens | tonumber) end),
|
|
409
|
+
gen_ai_usage_cache_read_input_tokens: (if $cache_read == "" then null else ($cache_read | tonumber) end),
|
|
410
|
+
gen_ai_usage_cache_creation_input_tokens: (if $cache_create == "" then null else ($cache_create | tonumber) end),
|
|
411
|
+
post_condition_target_type: ($exp_type | select(. != "") // null),
|
|
412
|
+
post_condition_matcher: ($exp_match | select(. != "") // null),
|
|
413
|
+
post_condition_expected: ($exp_value | select(. != "") // null),
|
|
414
|
+
post_condition_observed: ($observed | select(. != "") // null),
|
|
415
|
+
post_condition_hit: $postcond_hit
|
|
416
|
+
}')
|
|
417
|
+
|
|
418
|
+
stats_emit_event "${event}"
|
|
419
|
+
}
|
|
File without changes
|