@enricai/barnacle 1.12.32 → 1.12.34
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/dist/lib/llm/schemas.d.ts +30 -0
- package/dist/lib/llm/schemas.d.ts.map +1 -1
- package/dist/lib/llm/schemas.js +28 -0
- package/dist/lib/llm/schemas.js.map +1 -1
- package/dist/plugins/config-plugin.d.ts +15 -0
- package/dist/plugins/config-plugin.d.ts.map +1 -1
- package/dist/plugins/config-plugin.js +16 -0
- package/dist/plugins/config-plugin.js.map +1 -1
- package/dist/scraper/errors.d.ts +23 -1
- package/dist/scraper/errors.d.ts.map +1 -1
- package/dist/scraper/errors.js +40 -2
- package/dist/scraper/errors.js.map +1 -1
- package/dist/scraper/flow-runner.d.ts +64 -0
- package/dist/scraper/flow-runner.d.ts.map +1 -1
- package/dist/scraper/flow-runner.js +218 -18
- package/dist/scraper/flow-runner.js.map +1 -1
- package/dist/scripts/recon-browser.d.ts +50 -1
- package/dist/scripts/recon-browser.d.ts.map +1 -1
- package/dist/scripts/recon-browser.js +14 -3
- package/dist/scripts/recon-browser.js.map +1 -1
- package/dist/scripts/recon-generate.d.ts +72 -0
- package/dist/scripts/recon-generate.d.ts.map +1 -1
- package/dist/scripts/recon-generate.js +243 -35
- package/dist/scripts/recon-generate.js.map +1 -1
- package/dist/testmail/client.d.ts +7 -0
- package/dist/testmail/client.d.ts.map +1 -1
- package/dist/testmail/client.js +21 -0
- package/dist/testmail/client.js.map +1 -1
- package/package.json +1 -1
|
@@ -85,6 +85,8 @@ exports.verifyDomEffect = verifyDomEffect;
|
|
|
85
85
|
exports.narrowInvalidFormControl = narrowInvalidFormControl;
|
|
86
86
|
exports.formatStepPrefix = formatStepPrefix;
|
|
87
87
|
exports.probeStepBeforeAttempts = probeStepBeforeAttempts;
|
|
88
|
+
exports.extractLinkFromMessage = extractLinkFromMessage;
|
|
89
|
+
exports.extractCodeFromMessage = extractCodeFromMessage;
|
|
88
90
|
exports.executeStepWithHealing = executeStepWithHealing;
|
|
89
91
|
exports.waitForSpaReady = waitForSpaReady;
|
|
90
92
|
exports.runHealingFlow = runHealingFlow;
|
|
@@ -105,6 +107,7 @@ const schemas_1 = require("../lib/llm/schemas");
|
|
|
105
107
|
const logging_1 = require("../lib/logging");
|
|
106
108
|
const call_capture_1 = require("../lib/telemetry/call-capture");
|
|
107
109
|
const call_types_1 = require("../lib/telemetry/call-types");
|
|
110
|
+
const capture_filters_1 = require("../recon/capture-filters");
|
|
108
111
|
const browser_click_expr_1 = require("../scraper/browser-click-expr");
|
|
109
112
|
const captcha_solver_1 = require("../scraper/captcha-solver");
|
|
110
113
|
const deep_locator_actuate_1 = require("../scraper/deep-locator-actuate");
|
|
@@ -119,6 +122,7 @@ const stagehand_guard_1 = require("../scraper/stagehand-guard");
|
|
|
119
122
|
const submit_control_1 = require("../scraper/submit-control");
|
|
120
123
|
const watchdog_1 = require("../scraper/watchdog");
|
|
121
124
|
const recon_shared_1 = require("../scripts/recon-shared");
|
|
125
|
+
const client_1 = require("../testmail/client");
|
|
122
126
|
const logger = (0, logging_1.getLogger)({ name: "scraper/flow-runner" });
|
|
123
127
|
/** Cap on the rolling capture-filename window held in memory for failure dumps. */
|
|
124
128
|
const RECENT_CAPTURES_WINDOW = 20;
|
|
@@ -4449,12 +4453,12 @@ async function applySelectValue(page, target, selIdx, value) {
|
|
|
4449
4453
|
* step's stable `targetId` for cross-run convergence.
|
|
4450
4454
|
*/
|
|
4451
4455
|
async function trySelectPrimitive(params) {
|
|
4452
|
-
const { page, target, instruction, logger, anthropic, captureFn } = params;
|
|
4456
|
+
const { page, target, instruction, logger, anthropic, captureFn, redactValue } = params;
|
|
4453
4457
|
const parsed = parseSelectStep(instruction);
|
|
4454
4458
|
if (!parsed)
|
|
4455
4459
|
return null;
|
|
4456
4460
|
const { option, questionLabel } = parsed;
|
|
4457
|
-
const optLabel = `option "${option.slice(0, 40)}"${questionLabel ? `, question "${questionLabel.slice(0, 40)}"` : ""}`;
|
|
4461
|
+
const optLabel = `option "${redactIfSensitive(option, redactValue).slice(0, 40)}"${questionLabel ? `, question "${questionLabel.slice(0, 40)}"` : ""}`;
|
|
4458
4462
|
// Phase 1 (browser, no mutation): find the target select — the one whose
|
|
4459
4463
|
// nearby text matches the question label (or, when no label, any select).
|
|
4460
4464
|
// Try a deterministic option match first; if it hits, apply immediately (no
|
|
@@ -4783,12 +4787,12 @@ async function tryFillRequiredSelectsPrimitive(params) {
|
|
|
4783
4787
|
* for the id-as-targetId rationale.
|
|
4784
4788
|
*/
|
|
4785
4789
|
async function tryCheckboxPrimitive(params) {
|
|
4786
|
-
const { page, target, instruction, logger, anthropic, captureFn } = params;
|
|
4790
|
+
const { page, target, instruction, logger, anthropic, captureFn, redactValue } = params;
|
|
4787
4791
|
const parsed = parseSelectStep(instruction);
|
|
4788
4792
|
if (!parsed)
|
|
4789
4793
|
return null;
|
|
4790
4794
|
const { option, questionLabel } = parsed;
|
|
4791
|
-
const optLabel = `option "${option.slice(0, 40)}"${questionLabel ? `, question "${questionLabel.slice(0, 40)}"` : ""}`;
|
|
4795
|
+
const optLabel = `option "${redactIfSensitive(option, redactValue).slice(0, 40)}"${questionLabel ? `, question "${questionLabel.slice(0, 40)}"` : ""}`;
|
|
4792
4796
|
// Phase 1 (browser, no mutation): find checkbox GROUPS and their options.
|
|
4793
4797
|
// A group is a `c-MultiCheckboxInput` container or a `<fieldset>` containing
|
|
4794
4798
|
// checkboxes. Question label = the group's legend / associated label; each
|
|
@@ -5110,12 +5114,12 @@ function selectRadioGroupOption(params) {
|
|
|
5110
5114
|
* (checkbox/select/absent) falls through to the cascade.
|
|
5111
5115
|
*/
|
|
5112
5116
|
async function tryRadioPrimitive(params) {
|
|
5113
|
-
const { page, target, instruction, logger, anthropic, captureFn } = params;
|
|
5117
|
+
const { page, target, instruction, logger, anthropic, captureFn, redactValue } = params;
|
|
5114
5118
|
const parsed = parseRadioStep(instruction);
|
|
5115
5119
|
if (!parsed)
|
|
5116
5120
|
return null;
|
|
5117
5121
|
const { option, questionLabel } = parsed;
|
|
5118
|
-
const optLabel = `option "${option.slice(0, 40)}"${questionLabel ? `, question "${questionLabel.slice(0, 40)}"` : ""}`;
|
|
5122
|
+
const optLabel = `option "${redactIfSensitive(option, redactValue).slice(0, 40)}"${questionLabel ? `, question "${questionLabel.slice(0, 40)}"` : ""}`;
|
|
5119
5123
|
// Phase 1 (browser): find radio GROUPS and their options. A group is a
|
|
5120
5124
|
// `<fieldset>` / `[role=radiogroup]` / `[role=group]` / `[class*='RadioGroup']`
|
|
5121
5125
|
// container with radios. Question label = the group's legend / associated label; each option
|
|
@@ -5399,7 +5403,7 @@ async function applyRadioSelection(target, gi, ri, hint) {
|
|
|
5399
5403
|
* `trySelectPrimitive`/`tryRadioPrimitive`'s null-fallthrough contract.
|
|
5400
5404
|
*/
|
|
5401
5405
|
async function tryPromptSelectorPrimitive(params) {
|
|
5402
|
-
const { page, target, instruction, logger, anthropic, captureFn } = params;
|
|
5406
|
+
const { page, target, instruction, logger, anthropic, captureFn, redactValue } = params;
|
|
5403
5407
|
// A prompt-selector widget's search box renders a real <input>, so flow/
|
|
5404
5408
|
// replan generation routinely describes filling it as a FILL step ("Fill in
|
|
5405
5409
|
// the 'How Did You Hear About Us?' field with 'Internet/Online'") rather
|
|
@@ -5432,7 +5436,7 @@ async function tryPromptSelectorPrimitive(params) {
|
|
|
5432
5436
|
if (!parsed)
|
|
5433
5437
|
return null;
|
|
5434
5438
|
const { option, questionLabel } = parsed;
|
|
5435
|
-
const optLabel = `option "${option.slice(0, 40)}"${questionLabel ? `, question "${questionLabel.slice(0, 40)}"` : ""}`;
|
|
5439
|
+
const optLabel = `option "${redactIfSensitive(option, redactValue).slice(0, 40)}"${questionLabel ? `, question "${questionLabel.slice(0, 40)}"` : ""}`;
|
|
5436
5440
|
const norm = (s) => s.replace(/\s+/g, " ").trim().toLowerCase();
|
|
5437
5441
|
// Phase 1 (browser, read-only except for the marker attribute stamped for
|
|
5438
5442
|
// Phase 2's click addressing): find candidate widgets — popup-dropdown
|
|
@@ -6795,8 +6799,92 @@ function assertProbeSessionAlive(page, stepIndex, totalSteps) {
|
|
|
6795
6799
|
throw new errors_2.SessionTimeoutError(`${formatStepPrefix(stepIndex, totalSteps)} session appears closed/dead (page.url() threw: ${(0, errors_1.toErrorMessage)(err)}) during probe`);
|
|
6796
6800
|
}
|
|
6797
6801
|
}
|
|
6802
|
+
/** Matches `re` against `text`, falling back to `html`; returns the first capture group, or the full match when the pattern has none. Shared by {@link extractLinkFromMessage}'s pattern branch and {@link extractCodeFromMessage}. */
|
|
6803
|
+
function firstRegexMatch(text, html, re) {
|
|
6804
|
+
const match = text?.match(re) ?? html?.match(re) ?? null;
|
|
6805
|
+
if (!match)
|
|
6806
|
+
return null;
|
|
6807
|
+
return match[1] ?? match[0] ?? null;
|
|
6808
|
+
}
|
|
6809
|
+
/**
|
|
6810
|
+
* Extracts the verification URL from a testmail message. Why this exists: the
|
|
6811
|
+
* `emailStep` hook must resolve a link out of arbitrary inbox HTML/text
|
|
6812
|
+
* without a flow author having to hand-write a regex for every ATS's email
|
|
6813
|
+
* template — a caller-supplied `linkPattern` covers the templates that need
|
|
6814
|
+
* precision, and the default (first http(s) URL sharing the current page's
|
|
6815
|
+
* registrable domain) covers the rest. Returns `null` — never a guess — when
|
|
6816
|
+
* nothing matches, so the caller fails loudly instead of navigating blind.
|
|
6817
|
+
*/
|
|
6818
|
+
function extractLinkFromMessage(msg, pattern, currentPageUrl) {
|
|
6819
|
+
if (pattern) {
|
|
6820
|
+
return firstRegexMatch(msg.text, msg.html, new RegExp(pattern));
|
|
6821
|
+
}
|
|
6822
|
+
const fallbackDomain = (() => {
|
|
6823
|
+
try {
|
|
6824
|
+
return (0, capture_filters_1.registrableDomain)(new URL(currentPageUrl).hostname);
|
|
6825
|
+
}
|
|
6826
|
+
catch {
|
|
6827
|
+
return null;
|
|
6828
|
+
}
|
|
6829
|
+
})();
|
|
6830
|
+
if (!fallbackDomain)
|
|
6831
|
+
return null;
|
|
6832
|
+
const candidateUrls = [msg.text, msg.html]
|
|
6833
|
+
.filter((body) => typeof body === "string")
|
|
6834
|
+
.flatMap((body) => body.match(/https?:\/\/\S+/g) ?? []);
|
|
6835
|
+
return (candidateUrls.find((url) => {
|
|
6836
|
+
try {
|
|
6837
|
+
return (0, capture_filters_1.registrableDomain)(new URL(url).hostname) === fallbackDomain;
|
|
6838
|
+
}
|
|
6839
|
+
catch {
|
|
6840
|
+
return false;
|
|
6841
|
+
}
|
|
6842
|
+
}) ?? null);
|
|
6843
|
+
}
|
|
6844
|
+
/**
|
|
6845
|
+
* Extracts the OTP/code from a testmail message, defaulting to the first
|
|
6846
|
+
* 4-8 digit run (the common OTP shape) so most flows need no `codePattern`.
|
|
6847
|
+
* Sibling of {@link extractLinkFromMessage} — see that function's docblock
|
|
6848
|
+
* for why the default-vs-pattern split exists.
|
|
6849
|
+
*/
|
|
6850
|
+
function extractCodeFromMessage(msg, pattern) {
|
|
6851
|
+
return firstRegexMatch(msg.text, msg.html, pattern ? new RegExp(pattern) : /\b\d{4,8}\b/);
|
|
6852
|
+
}
|
|
6853
|
+
/**
|
|
6854
|
+
* Splices an `emailStep` code-extraction result into a fill step's quoted
|
|
6855
|
+
* value so the pre-existing fill cascade (`parseFillStep`/`parseFillValueIntent`,
|
|
6856
|
+
* both of which read the value out of the instruction's own quoted text)
|
|
6857
|
+
* picks it up as the value to type without any of those parsers needing to
|
|
6858
|
+
* know the value came from an inbox rather than the flow's payload.
|
|
6859
|
+
*/
|
|
6860
|
+
function spliceEmailStepFillValue(step, code) {
|
|
6861
|
+
return /with\s+'[^']*'/i.test(step)
|
|
6862
|
+
? step.replace(/with\s+'[^']*'/i, `with '${code}'`)
|
|
6863
|
+
: `${step} with '${code}'`;
|
|
6864
|
+
}
|
|
6865
|
+
/**
|
|
6866
|
+
* Masks `text` when it equals `sensitiveValue` (the emailStep-extracted code
|
|
6867
|
+
* spliced into the step's fill value, see {@link spliceEmailStepFillValue}).
|
|
6868
|
+
* The select/checkbox/radio/prompt-selector primitives all re-parse the
|
|
6869
|
+
* (possibly code-bearing) instruction independently of the fill primitive,
|
|
6870
|
+
* so each one's own "option"/diagnostic logging must redact it too — the
|
|
6871
|
+
* code is a single-use credential and must never reach the logs verbatim.
|
|
6872
|
+
*/
|
|
6873
|
+
function redactIfSensitive(text, sensitiveValue) {
|
|
6874
|
+
return sensitiveValue && text === sensitiveValue ? "[redacted]" : text;
|
|
6875
|
+
}
|
|
6798
6876
|
async function executeStepWithHealing(params) {
|
|
6799
|
-
const { stagehand, page,
|
|
6877
|
+
const { stagehand, page, optional, upload, submitStep, captchaGated = false, emailStep = false, emailStepConfig, allocatedInbox = null, flowHasSubmitSemantics: flowHasSubmitSemanticsFlag, stepIndex, totalSteps, phase, signalCounter, recentCaptures, recentCaptureMeta, anthropic, rephraseModel, logger, captureFn, uploadFixture, isFinalStep, submitEndpointPattern, submittedStateSelectors, requireSubmitEndpointMatch, advanceTransitionBodyPattern, successUrlFragments, successPageTitleHints, ownBackendHostnames, knownErrorClassPrefixes, wizardExitButtonLabels, getSuppressedAisdkElementIdErrorCount, trajectory, onStepFailure, } = params;
|
|
6878
|
+
// Mutable: the `emailStep` code-extract path splices the extracted code
|
|
6879
|
+
// into this instruction (see the emailStep hook block below) so the
|
|
6880
|
+
// pre-existing fill cascade downstream picks it up as the value to type
|
|
6881
|
+
// without re-running Stagehand's act/observe resolution a second time.
|
|
6882
|
+
let step = params.step;
|
|
6883
|
+
// Set only by the emailStep code-extract path below. Threaded into every
|
|
6884
|
+
// select/checkbox/radio/prompt-selector primitive call further down so
|
|
6885
|
+
// each one's own re-parse of `step` (now code-bearing) redacts the code
|
|
6886
|
+
// out of its diagnostic logging too, not just the fill primitive's.
|
|
6887
|
+
let emailStepCode;
|
|
6800
6888
|
// Mutable (not the destructured const above) so a lost frame-attach race
|
|
6801
6889
|
// can be upgraded in place once the OOPIF attaches later in the cascade —
|
|
6802
6890
|
// see reresolveFrameTargetIfLost below. Every existing reference in this
|
|
@@ -6804,6 +6892,111 @@ async function executeStepWithHealing(params) {
|
|
|
6804
6892
|
// moved so it can be reassigned.
|
|
6805
6893
|
let frameTarget = params.frameTarget;
|
|
6806
6894
|
let frameReresolveAttempted = false;
|
|
6895
|
+
// Emailed-verification hook. Runs FIRST — before upload/select/checkbox/
|
|
6896
|
+
// radio/prompt-selector/captcha or any other cascade primitive — so an
|
|
6897
|
+
// `emailStep: true` step with no allocated inbox fails loud instead of
|
|
6898
|
+
// being silently claimed by an unrelated primitive that happens to match
|
|
6899
|
+
// the step's instruction text. Polls the run's allocated testmail inbox,
|
|
6900
|
+
// extracts a link or code from the matched message, and either navigates
|
|
6901
|
+
// to the link (gating "completed" on the same transition poll the captcha
|
|
6902
|
+
// hook reuses) or splices the code into this step's fill value and falls
|
|
6903
|
+
// through to the normal cascade below. Never logs the extracted link/code
|
|
6904
|
+
// — either can be a single-use credential — and never navigates off the
|
|
6905
|
+
// current page's registrable domain, so a poisoned inbox message can't
|
|
6906
|
+
// redirect the session.
|
|
6907
|
+
if (emailStep) {
|
|
6908
|
+
if (!allocatedInbox) {
|
|
6909
|
+
// No inbox to poll — fail loud, exactly like the captcha no-key path.
|
|
6910
|
+
throw new errors_2.EmailStepInboxUnavailableError("emailStep set but no testmail inbox allocated (pass --allocate-email)");
|
|
6911
|
+
}
|
|
6912
|
+
const cfg = emailStepConfig ?? {};
|
|
6913
|
+
logger.info(`${formatStepPrefix(stepIndex, totalSteps)} emailStep: polling inbox ${allocatedInbox.address} (subjectContains=${cfg.subjectContains ?? "*"})`);
|
|
6914
|
+
const msg = await (0, client_1.pollTestmailInbox)({
|
|
6915
|
+
inbox: allocatedInbox,
|
|
6916
|
+
subjectContains: cfg.subjectContains,
|
|
6917
|
+
timeoutMs: cfg.timeoutMs ?? 120_000,
|
|
6918
|
+
}).catch((err) => {
|
|
6919
|
+
logger.error(`${formatStepPrefix(stepIndex, totalSteps)} emailStep: no matching email within budget (${(0, errors_1.toErrorMessage)(err)}); failing the step`);
|
|
6920
|
+
throw err;
|
|
6921
|
+
});
|
|
6922
|
+
if ((cfg.extract ?? "link") === "link") {
|
|
6923
|
+
const emailStepTarget = frameTarget ?? (0, frame_target_1.mainFrameTarget)(page);
|
|
6924
|
+
const currentPageUrl = await emailStepTarget.url();
|
|
6925
|
+
const url = extractLinkFromMessage(msg, cfg.linkPattern, currentPageUrl);
|
|
6926
|
+
if (!url) {
|
|
6927
|
+
throw new errors_2.EmailStepExtractError("no link matched in the verification email");
|
|
6928
|
+
}
|
|
6929
|
+
// Allowlist gate: the link's host must share the current page origin's
|
|
6930
|
+
// registrable domain (or be a declared `ownBackendHostnames` entry) —
|
|
6931
|
+
// never follow a link from untrusted inbox content off-domain.
|
|
6932
|
+
const fallbackDomain = (() => {
|
|
6933
|
+
try {
|
|
6934
|
+
return (0, capture_filters_1.registrableDomain)(new URL(currentPageUrl).hostname);
|
|
6935
|
+
}
|
|
6936
|
+
catch {
|
|
6937
|
+
return null;
|
|
6938
|
+
}
|
|
6939
|
+
})();
|
|
6940
|
+
const linkHost = (() => {
|
|
6941
|
+
try {
|
|
6942
|
+
return new URL(url).hostname;
|
|
6943
|
+
}
|
|
6944
|
+
catch {
|
|
6945
|
+
return null;
|
|
6946
|
+
}
|
|
6947
|
+
})();
|
|
6948
|
+
if (!linkHost || !(0, capture_filters_1.isAllowedFixtureHost)(linkHost, ownBackendHostnames, fallbackDomain)) {
|
|
6949
|
+
throw new errors_2.EmailStepExtractError("extracted link's host is outside the current page's registrable domain; refusing to navigate");
|
|
6950
|
+
}
|
|
6951
|
+
if (cfg.action === "fill") {
|
|
6952
|
+
// extract:"link" + action:"fill" — splice the link into this step's
|
|
6953
|
+
// fill value instead of navigating, same as the extract:"code" path
|
|
6954
|
+
// below, for flows whose verification step is a paste-the-link field
|
|
6955
|
+
// rather than a page transition.
|
|
6956
|
+
logger.info(`${formatStepPrefix(stepIndex, totalSteps)} emailStep: link extracted`);
|
|
6957
|
+
emailStepCode = url;
|
|
6958
|
+
step = spliceEmailStepFillValue(step, url);
|
|
6959
|
+
}
|
|
6960
|
+
else {
|
|
6961
|
+
logger.info(`${formatStepPrefix(stepIndex, totalSteps)} emailStep: navigating to extracted link`);
|
|
6962
|
+
const preIdx = latestCaptureIndex(recentCaptures);
|
|
6963
|
+
await page.goto(url); // NEVER log the URL body — it can be a single-use credential
|
|
6964
|
+
// Gate advance on the same transition poll the captcha hook reuses.
|
|
6965
|
+
if (advanceTransitionBodyPattern) {
|
|
6966
|
+
const confirmed = await waitForTransitionBody({
|
|
6967
|
+
page,
|
|
6968
|
+
preIdx,
|
|
6969
|
+
advanceTransitionBodyPattern,
|
|
6970
|
+
timeoutMs: CAPTCHA_TRANSITION_POLL_MS,
|
|
6971
|
+
intervalMs: ADVANCE_TRANSITION_POLL_INTERVAL_MS,
|
|
6972
|
+
});
|
|
6973
|
+
logger.info(`${formatStepPrefix(stepIndex, totalSteps)} emailStep: post-navigate transition poll confirmed=${confirmed}`);
|
|
6974
|
+
if (confirmed) {
|
|
6975
|
+
trajectory?.push({ stepIndex, verifiedBy: "network" });
|
|
6976
|
+
}
|
|
6977
|
+
}
|
|
6978
|
+
return "completed";
|
|
6979
|
+
}
|
|
6980
|
+
}
|
|
6981
|
+
if (cfg.extract === "link") {
|
|
6982
|
+
// extract:"link" + action:"fill" already spliced `url` into `step`
|
|
6983
|
+
// above; fall through to the normal fill cascade below, same as the
|
|
6984
|
+
// extract:"code" path.
|
|
6985
|
+
}
|
|
6986
|
+
else {
|
|
6987
|
+
// extract:"code" — splice the extracted code into this step's fill
|
|
6988
|
+
// value and fall through to the pre-existing fill primitive below,
|
|
6989
|
+
// rather than returning early or re-running Stagehand's act/observe
|
|
6990
|
+
// resolution against the code.
|
|
6991
|
+
const code = extractCodeFromMessage(msg, cfg.codePattern);
|
|
6992
|
+
if (!code) {
|
|
6993
|
+
throw new errors_2.EmailStepExtractError("no code matched in the verification email");
|
|
6994
|
+
}
|
|
6995
|
+
logger.info(`${formatStepPrefix(stepIndex, totalSteps)} emailStep: code extracted (len ${code.length})`);
|
|
6996
|
+
emailStepCode = code;
|
|
6997
|
+
step = spliceEmailStepFillValue(step, code);
|
|
6998
|
+
}
|
|
6999
|
+
}
|
|
6807
7000
|
/**
|
|
6808
7001
|
* Re-resolves a frame that lost the attach race at step entry —
|
|
6809
7002
|
* `resolveFrameTarget`'s per-step poll (called once in the runner's step
|
|
@@ -6907,6 +7100,7 @@ async function executeStepWithHealing(params) {
|
|
|
6907
7100
|
logger,
|
|
6908
7101
|
anthropic,
|
|
6909
7102
|
captureFn,
|
|
7103
|
+
redactValue: emailStepCode,
|
|
6910
7104
|
});
|
|
6911
7105
|
if (selectTargetId !== null) {
|
|
6912
7106
|
logger.info(`${formatStepPrefix(stepIndex, totalSteps)} resolved by select primitive`);
|
|
@@ -6925,6 +7119,7 @@ async function executeStepWithHealing(params) {
|
|
|
6925
7119
|
logger,
|
|
6926
7120
|
anthropic,
|
|
6927
7121
|
captureFn,
|
|
7122
|
+
redactValue: emailStepCode,
|
|
6928
7123
|
});
|
|
6929
7124
|
if (checkboxTargetId !== null) {
|
|
6930
7125
|
logger.info(`${formatStepPrefix(stepIndex, totalSteps)} resolved by checkbox primitive`);
|
|
@@ -6944,6 +7139,7 @@ async function executeStepWithHealing(params) {
|
|
|
6944
7139
|
logger,
|
|
6945
7140
|
anthropic,
|
|
6946
7141
|
captureFn,
|
|
7142
|
+
redactValue: emailStepCode,
|
|
6947
7143
|
});
|
|
6948
7144
|
if (radioTargetId !== null) {
|
|
6949
7145
|
logger.info(`${formatStepPrefix(stepIndex, totalSteps)} resolved by radio primitive`);
|
|
@@ -6968,6 +7164,7 @@ async function executeStepWithHealing(params) {
|
|
|
6968
7164
|
logger,
|
|
6969
7165
|
anthropic,
|
|
6970
7166
|
captureFn,
|
|
7167
|
+
redactValue: emailStepCode,
|
|
6971
7168
|
});
|
|
6972
7169
|
if (promptSelectorTargetId !== null) {
|
|
6973
7170
|
logger.info(`${formatStepPrefix(stepIndex, totalSteps)} resolved by prompt-selector primitive`);
|
|
@@ -7974,8 +8171,8 @@ async function executeStepWithHealing(params) {
|
|
|
7974
8171
|
const dateFill = await fillHtml5DateTimeInput(dateFillTarget, overriddenTarget.selector, fillValue);
|
|
7975
8172
|
if (dateFill !== null) {
|
|
7976
8173
|
record.errorMessage = dateFill.filled
|
|
7977
|
-
? `html5-date-fallback: filled ${dateFill.inputType}="${dateFill.postValue}"`
|
|
7978
|
-
: `html5-date-fallback: failed to fill ${dateFill.inputType} (post=${dateFill.postValue})`;
|
|
8174
|
+
? `html5-date-fallback: filled ${dateFill.inputType}="${redactIfSensitive(dateFill.postValue, emailStepCode)}"`
|
|
8175
|
+
: `html5-date-fallback: failed to fill ${dateFill.inputType} (post=${redactIfSensitive(dateFill.postValue, emailStepCode)})`;
|
|
7979
8176
|
// Override the act result based on the deterministic fill
|
|
7980
8177
|
// outcome — the helper bypasses Stagehand's schema-error
|
|
7981
8178
|
// failure mode by writing directly via the native setter.
|
|
@@ -7995,8 +8192,8 @@ async function executeStepWithHealing(params) {
|
|
|
7995
8192
|
const datepickerFill = await fillTextDatepickerInput(dateFillTarget, overriddenTarget.selector, fillValue);
|
|
7996
8193
|
if (datepickerFill !== null) {
|
|
7997
8194
|
record.errorMessage = datepickerFill.filled
|
|
7998
|
-
? `text-datepicker-fill: filled via ${datepickerFill.strategy} "${datepickerFill.postValue}"`
|
|
7999
|
-
: `text-datepicker-fill: failed to commit "${fillValue.slice(0, 60)}" (post=${datepickerFill.postValue})`;
|
|
8195
|
+
? `text-datepicker-fill: filled via ${datepickerFill.strategy} "${redactIfSensitive(datepickerFill.postValue, emailStepCode)}"`
|
|
8196
|
+
: `text-datepicker-fill: failed to commit "${redactIfSensitive(fillValue.slice(0, 60), emailStepCode)}" (post=${redactIfSensitive(datepickerFill.postValue, emailStepCode)})`;
|
|
8000
8197
|
if (datepickerFill.filled) {
|
|
8001
8198
|
record.actResultSuccess = true;
|
|
8002
8199
|
resolvedAction = overriddenTarget;
|
|
@@ -8015,11 +8212,11 @@ async function executeStepWithHealing(params) {
|
|
|
8015
8212
|
const readback = await verifyFillReadback(dateFillTarget, overriddenTarget.selector, fillValue);
|
|
8016
8213
|
if (readback !== null) {
|
|
8017
8214
|
if (readback.outcome === "rejected") {
|
|
8018
|
-
record.errorMessage = `fill-value-rejected: tried "${fillValue.slice(0, 60)}" on <${readback.tag}>; element value remains empty (silent rejection — HTML5 type validation, framework controlled-component, or masked-input library)`;
|
|
8215
|
+
record.errorMessage = `fill-value-rejected: tried "${redactIfSensitive(fillValue.slice(0, 60), emailStepCode)}" on <${readback.tag}>; element value remains empty (silent rejection — HTML5 type validation, framework controlled-component, or masked-input library)`;
|
|
8019
8216
|
record.actResultSuccess = false;
|
|
8020
8217
|
}
|
|
8021
8218
|
else if (readback.outcome === "differs") {
|
|
8022
|
-
logger.info(`${formatStepPrefix(stepIndex, totalSteps)} fill-value-differs: tried "${fillValue.slice(0, 60)}" got "${readback.postValue.slice(0, 60)}" (framework reformatted)`);
|
|
8219
|
+
logger.info(`${formatStepPrefix(stepIndex, totalSteps)} fill-value-differs: tried "${redactIfSensitive(fillValue.slice(0, 60), emailStepCode)}" got "${redactIfSensitive(readback.postValue.slice(0, 60), emailStepCode)}" (framework reformatted)`);
|
|
8023
8220
|
}
|
|
8024
8221
|
}
|
|
8025
8222
|
}
|
|
@@ -8428,7 +8625,7 @@ async function executeStepWithHealing(params) {
|
|
|
8428
8625
|
if (datepickerFill !== null) {
|
|
8429
8626
|
if (datepickerFill.filled) {
|
|
8430
8627
|
datepickerCommitted = true;
|
|
8431
|
-
record.errorMessage = `text-datepicker-fill: filled via ${datepickerFill.strategy} "${datepickerFill.postValue}"`;
|
|
8628
|
+
record.errorMessage = `text-datepicker-fill: filled via ${datepickerFill.strategy} "${redactIfSensitive(datepickerFill.postValue, emailStepCode)}"`;
|
|
8432
8629
|
}
|
|
8433
8630
|
else {
|
|
8434
8631
|
// A gated-in datepicker that neither gesture commits is a real fill
|
|
@@ -8436,7 +8633,7 @@ async function executeStepWithHealing(params) {
|
|
|
8436
8633
|
// so suppress the weak view-swap/form-value acceptance and escalate.
|
|
8437
8634
|
datepickerRejected = true;
|
|
8438
8635
|
record.actResultSuccess = false;
|
|
8439
|
-
record.errorMessage = `text-datepicker-fill: failed to commit "${fillIntent.value.slice(0, 60)}" (post=${datepickerFill.postValue})`;
|
|
8636
|
+
record.errorMessage = `text-datepicker-fill: failed to commit "${redactIfSensitive(fillIntent.value.slice(0, 60), emailStepCode)}" (post=${redactIfSensitive(datepickerFill.postValue, emailStepCode)})`;
|
|
8440
8637
|
}
|
|
8441
8638
|
}
|
|
8442
8639
|
else {
|
|
@@ -8445,7 +8642,7 @@ async function executeStepWithHealing(params) {
|
|
|
8445
8642
|
// failure dump reads identically regardless of which path caught it.
|
|
8446
8643
|
datepickerRejected = true;
|
|
8447
8644
|
record.actResultSuccess = false;
|
|
8448
|
-
record.errorMessage = `fill-value-rejected: tried "${fillIntent.value.slice(0, 60)}" on <${readback.tag}>; element value remains empty (silent rejection — HTML5 type validation, framework controlled-component, or masked-input library)`;
|
|
8645
|
+
record.errorMessage = `fill-value-rejected: tried "${redactIfSensitive(fillIntent.value.slice(0, 60), emailStepCode)}" on <${readback.tag}>; element value remains empty (silent rejection — HTML5 type validation, framework controlled-component, or masked-input library)`;
|
|
8449
8646
|
}
|
|
8450
8647
|
}
|
|
8451
8648
|
}
|
|
@@ -9274,6 +9471,9 @@ async function runHealingFlow(deps) {
|
|
|
9274
9471
|
upload: s.upload,
|
|
9275
9472
|
submitStep: s.submitStep,
|
|
9276
9473
|
captchaGated: s.captchaGated === true,
|
|
9474
|
+
emailStep: s.emailStep === true,
|
|
9475
|
+
emailStepConfig: s.emailStepConfig,
|
|
9476
|
+
allocatedInbox: deps.allocatedInbox ?? null,
|
|
9277
9477
|
flowHasSubmitSemantics: flowHasSubmitSemanticsFlag,
|
|
9278
9478
|
stepIndex: i,
|
|
9279
9479
|
totalSteps: () => steps.length,
|