@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.
@@ -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, step, optional, upload, submitStep, captchaGated = false, 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;
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,