@trusty-squire/mcp 0.9.18 → 0.9.19-rc.10
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/README.md +4 -4
- package/dist/api-client.d.ts +5 -0
- package/dist/api-client.d.ts.map +1 -1
- package/dist/api-client.js.map +1 -1
- package/dist/bot/agent.d.ts +12 -0
- package/dist/bot/agent.d.ts.map +1 -1
- package/dist/bot/agent.js +469 -6
- package/dist/bot/agent.js.map +1 -1
- package/dist/bot/browser.d.ts +9 -0
- package/dist/bot/browser.d.ts.map +1 -1
- package/dist/bot/browser.js +258 -4
- package/dist/bot/browser.js.map +1 -1
- package/dist/bot/credential-extraction-flow.d.ts.map +1 -1
- package/dist/bot/credential-extraction-flow.js +6 -0
- package/dist/bot/credential-extraction-flow.js.map +1 -1
- package/dist/bot/form-fill.d.ts +1 -0
- package/dist/bot/form-fill.d.ts.map +1 -1
- package/dist/bot/form-fill.js +18 -6
- package/dist/bot/form-fill.js.map +1 -1
- package/dist/bot/index.d.ts +1 -0
- package/dist/bot/index.d.ts.map +1 -1
- package/dist/bot/index.js +1 -0
- package/dist/bot/index.js.map +1 -1
- package/dist/bot/login-state.d.ts +2 -1
- package/dist/bot/login-state.d.ts.map +1 -1
- package/dist/bot/login-state.js +22 -5
- package/dist/bot/login-state.js.map +1 -1
- package/dist/bot/onboarding-capture.d.ts +3 -0
- package/dist/bot/onboarding-capture.d.ts.map +1 -1
- package/dist/bot/onboarding-capture.js +34 -0
- package/dist/bot/onboarding-capture.js.map +1 -1
- package/dist/bot/post-signup-recovery-state.d.ts +5 -0
- package/dist/bot/post-signup-recovery-state.d.ts.map +1 -1
- package/dist/bot/post-signup-recovery-state.js +14 -0
- package/dist/bot/post-signup-recovery-state.js.map +1 -1
- package/dist/bot/promote-to-skill.d.ts.map +1 -1
- package/dist/bot/promote-to-skill.js +34 -7
- package/dist/bot/promote-to-skill.js.map +1 -1
- package/dist/bot/provision-session.d.ts +149 -0
- package/dist/bot/provision-session.d.ts.map +1 -0
- package/dist/bot/provision-session.js +954 -0
- package/dist/bot/provision-session.js.map +1 -0
- package/dist/bot/redact.d.ts.map +1 -1
- package/dist/bot/redact.js +1 -0
- package/dist/bot/redact.js.map +1 -1
- package/dist/bot/replay-skill.d.ts +1 -0
- package/dist/bot/replay-skill.d.ts.map +1 -1
- package/dist/bot/replay-skill.js +141 -6
- package/dist/bot/replay-skill.js.map +1 -1
- package/dist/bot/skill-hint.d.ts +7 -0
- package/dist/bot/skill-hint.d.ts.map +1 -0
- package/dist/bot/skill-hint.js +105 -0
- package/dist/bot/skill-hint.js.map +1 -0
- package/dist/bot/unwinnable-services.d.ts.map +1 -1
- package/dist/bot/unwinnable-services.js +5 -5
- package/dist/bot/unwinnable-services.js.map +1 -1
- package/dist/install/cli.d.ts +9 -1
- package/dist/install/cli.d.ts.map +1 -1
- package/dist/install/cli.js +207 -78
- package/dist/install/cli.js.map +1 -1
- package/dist/install/interactive.d.ts +9 -3
- package/dist/install/interactive.d.ts.map +1 -1
- package/dist/install/interactive.js +80 -140
- package/dist/install/interactive.js.map +1 -1
- package/dist/install/proxy-url.d.ts +2 -0
- package/dist/install/proxy-url.d.ts.map +1 -0
- package/dist/install/proxy-url.js +20 -0
- package/dist/install/proxy-url.js.map +1 -0
- package/dist/session.d.ts +3 -0
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js.map +1 -1
- package/dist/skill-registry-client.d.ts +8 -0
- package/dist/skill-registry-client.d.ts.map +1 -1
- package/dist/skill-registry-client.js +70 -53
- package/dist/skill-registry-client.js.map +1 -1
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +5 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/provision-any.d.ts.map +1 -1
- package/dist/tools/provision-any.js +17 -5
- package/dist/tools/provision-any.js.map +1 -1
- package/dist/tools/provision-drive.d.ts +83 -0
- package/dist/tools/provision-drive.d.ts.map +1 -0
- package/dist/tools/provision-drive.js +245 -0
- package/dist/tools/provision-drive.js.map +1 -0
- package/package.json +2 -2
package/dist/bot/agent.js
CHANGED
|
@@ -32,7 +32,7 @@ import { MAX_POST_VERIFY_NAVIGATES, MAX_PREMATURE_DONE_FALLBACKS, MAX_UPSTREAM_B
|
|
|
32
32
|
import { PostSignupSyntheticCapture } from "./post-signup-synthetic-capture.js";
|
|
33
33
|
import { CredentialExtractionFlow, DOM_LABEL_TO_KEY, NON_CREDENTIAL_KEYS, extractAllLabeledTokensFromReason, hasAnyExtractedCredential, hasUsableCredentialBundle, } from "./credential-extraction-flow.js";
|
|
34
34
|
export { extractAllLabeledTokensFromReason, hasAnyExtractedCredential, isMultiCredBundle, } from "./credential-extraction-flow.js";
|
|
35
|
-
import { captureOnboardingRound, updateCapturedRoundSemantic, } from "./onboarding-capture.js";
|
|
35
|
+
import { captureOnboardingRound, hasCapturedAnyRound, hasCapturedExtractRound, nextCaptureRound, updateCapturedRoundSemantic, } from "./onboarding-capture.js";
|
|
36
36
|
import { evaluateSemanticTransition, inferSemanticTransition, } from "./semantic-transition.js";
|
|
37
37
|
import { KEYS_DESTINATION_URL, runNavSearch, } from "./nav-search.js";
|
|
38
38
|
import { wasRecentlyPrewarmed, recordPrewarmSuccess } from "./prewarm-cache.js";
|
|
@@ -401,6 +401,16 @@ const SERVICE_KEYS_PATHS = {
|
|
|
401
401
|
// /settings/* and /api-keys guesses miss it and the planner can get stuck
|
|
402
402
|
// in Business Account > Team Members.
|
|
403
403
|
paddle: ["/authentication-v2"],
|
|
404
|
+
// Together AI's project-scoped keys route is discoverable in the DOM, but the
|
|
405
|
+
// planner can loop between Create key and payment-skip affordances after it
|
|
406
|
+
// lands there. Pin the route so recovery starts at the credential surface.
|
|
407
|
+
togetherai: [
|
|
408
|
+
"https://api.together.ai/settings/projects/~current/api-keys",
|
|
409
|
+
"/settings/projects/~current/api-keys",
|
|
410
|
+
],
|
|
411
|
+
// Cartesia's authenticated root/dashboard can expose key rows with Reveal
|
|
412
|
+
// controls; generic /settings/* guesses hit 404/error shells first.
|
|
413
|
+
cartesia: ["/", "/dashboard", "/start", "/api-keys"],
|
|
404
414
|
};
|
|
405
415
|
// Normalize a service name to the slug used as a SERVICE_KEYS_PATHS key:
|
|
406
416
|
// lowercased, alphanumerics only. Mirrors guessSignupUrl's slug rule so
|
|
@@ -409,6 +419,80 @@ const SERVICE_KEYS_PATHS = {
|
|
|
409
419
|
export function serviceSlug(service) {
|
|
410
420
|
return service.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
411
421
|
}
|
|
422
|
+
export function hasCuratedServiceKeyPath(service) {
|
|
423
|
+
return SERVICE_KEYS_PATHS[serviceSlug(service)] !== undefined;
|
|
424
|
+
}
|
|
425
|
+
function elementLabel(el) {
|
|
426
|
+
return [
|
|
427
|
+
el.visibleText,
|
|
428
|
+
el.ariaLabel,
|
|
429
|
+
el.title,
|
|
430
|
+
el.labelText,
|
|
431
|
+
el.iconLabel,
|
|
432
|
+
el.href,
|
|
433
|
+
el.testId,
|
|
434
|
+
]
|
|
435
|
+
.filter((s) => s !== null && s !== undefined)
|
|
436
|
+
.join(" ")
|
|
437
|
+
.replace(/\s+/g, " ")
|
|
438
|
+
.trim();
|
|
439
|
+
}
|
|
440
|
+
export function shouldRevealBeforeCredentialSweep(input) {
|
|
441
|
+
const haystack = `${input.url}\n${input.pageText}`;
|
|
442
|
+
return (/\b(?:api[\s_-]*(?:keys?|tokens?)|access[\s_-]*tokens?|personal[\s_-]*access[\s_-]*tokens?|secret[\s_-]*keys?|auth[\s_-]*tokens?|credentials?|reveal|show\s+(?:key|token|secret)|copy\s+(?:key|token|secret))\b/i.test(haystack) &&
|
|
443
|
+
/(?:•{3,}|\*{3,}|[A-Za-z0-9]{2,4}[•*]{4,}|\b(?:reveal|show|unmask|view)\b)/i.test(haystack));
|
|
444
|
+
}
|
|
445
|
+
export function credentialActionSignature(step, inventory) {
|
|
446
|
+
if (!("selector" in step) || step.selector === undefined)
|
|
447
|
+
return null;
|
|
448
|
+
const target = inventory.find((e) => e.selector === step.selector);
|
|
449
|
+
const label = [
|
|
450
|
+
target?.visibleText,
|
|
451
|
+
target?.ariaLabel,
|
|
452
|
+
target?.title,
|
|
453
|
+
target?.labelText,
|
|
454
|
+
target?.iconLabel,
|
|
455
|
+
step.reason,
|
|
456
|
+
]
|
|
457
|
+
.filter((s) => s !== null && s !== undefined)
|
|
458
|
+
.join(" ")
|
|
459
|
+
.replace(/\s+/g, " ")
|
|
460
|
+
.trim();
|
|
461
|
+
if (label.length === 0)
|
|
462
|
+
return null;
|
|
463
|
+
const lower = label.toLowerCase();
|
|
464
|
+
if (/\b(?:create|generate|new|add|issue|mint)\b.*\b(?:api\s*)?(?:key|token|secret|credential)s?\b/.test(lower) ||
|
|
465
|
+
/\b(?:api\s*)?(?:key|token|secret|credential)s?\b.*\b(?:create|generate|new|add|issue|mint)\b/.test(lower)) {
|
|
466
|
+
return `${step.kind}:create-key`;
|
|
467
|
+
}
|
|
468
|
+
if (/\b(?:skip|maybe later|not now|without)\b.*\b(?:payment|billing|card|deposit|credits?)\b/.test(lower) ||
|
|
469
|
+
/\b(?:payment|billing|card|deposit|credits?)\b.*\b(?:skip|maybe later|not now|without)\b/.test(lower)) {
|
|
470
|
+
return `${step.kind}:skip-payment`;
|
|
471
|
+
}
|
|
472
|
+
if (/\b(?:add|make|initial)\b.*\b(?:payment|deposit|credits?)\b/.test(lower)) {
|
|
473
|
+
return `${step.kind}:payment`;
|
|
474
|
+
}
|
|
475
|
+
return null;
|
|
476
|
+
}
|
|
477
|
+
export function isRepeatingCredentialActionCycle(signatures, pageText = "") {
|
|
478
|
+
const interesting = signatures.some((s) => /(?:create-key|payment|deposit)/.test(s)) ||
|
|
479
|
+
/\b(?:api\s*keys?|tokens?|payment|billing|deposit|credits?)\b/i.test(pageText);
|
|
480
|
+
if (!interesting)
|
|
481
|
+
return false;
|
|
482
|
+
const last3 = signatures.slice(-3);
|
|
483
|
+
if (last3.length === 3 &&
|
|
484
|
+
last3.every((s) => s === last3[0]) &&
|
|
485
|
+
/(?:create-key|skip-payment|payment)/.test(last3[0] ?? "")) {
|
|
486
|
+
return true;
|
|
487
|
+
}
|
|
488
|
+
const last5 = signatures.slice(-5);
|
|
489
|
+
return (last5.length === 5 &&
|
|
490
|
+
last5[0] === last5[2] &&
|
|
491
|
+
last5[2] === last5[4] &&
|
|
492
|
+
last5[1] === last5[3] &&
|
|
493
|
+
last5[0] !== last5[1] &&
|
|
494
|
+
last5.some((s) => /(?:create-key|skip-payment|payment)/.test(s)));
|
|
495
|
+
}
|
|
412
496
|
// 0.8.2-rc.10 — heuristic for "this account already exists on the
|
|
413
497
|
// service and its API keys are masked, with no path to reveal them."
|
|
414
498
|
// The test identity (methoxine@gmail.com) accumulates state across
|
|
@@ -613,6 +697,51 @@ export function findKeyNameInput(inventory) {
|
|
|
613
697
|
// should the bot. Exported, pure (operates on the inventory shape only).
|
|
614
698
|
const API_KEYS_HREF = /\/(?:api[-_]?keys?|api[-_]?tokens?|access[-_]?tokens?|auth[-_]?tokens?|secret[-_]?keys?|personal[-_]?access[-_]?tokens?|developers?|keys?|tokens?)(?:[/?#]|$)/i;
|
|
615
699
|
const API_KEYS_TEXT = /\b(?:api|access|secret|auth|personal\s+access)\s*(?:keys?|tokens?)\b/i;
|
|
700
|
+
// Find a settings / API-keys / dashboard nav link to ESCAPE a stuck onboarding
|
|
701
|
+
// wizard. MEASURED 2026-06-24 (cloudinary /app/welcome): the welcome survey's
|
|
702
|
+
// clicks don't register (STALLED), but it's an OVERLAY on the real product
|
|
703
|
+
// dashboard whose sidebar — Home → /app/home, Settings → /app/settings — is
|
|
704
|
+
// already in the inventory. Navigating to settings/keys/home reaches the
|
|
705
|
+
// credential surface behind the wizard. Prefers settings/account/keys (where
|
|
706
|
+
// API credentials live) over a bare home/dashboard link. Anchor href is
|
|
707
|
+
// authoritative; text is the backstop (cloudinary's Settings link is icon-only,
|
|
708
|
+
// no visible text — only the href identifies it).
|
|
709
|
+
const WIZARD_ESCAPE_HREF = /\/(?:settings|account|api[-_]?keys?|developers?|dashboard|home|console)(?:\/|$|\?)/i;
|
|
710
|
+
const WIZARD_ESCAPE_TEXT = /\b(?:settings|account|dashboard|api\s*keys?|developers?|console)\b/i;
|
|
711
|
+
export function findWizardEscapeNavLink(inventory, alreadyClicked = new Set()) {
|
|
712
|
+
let best = null;
|
|
713
|
+
for (const el of inventory) {
|
|
714
|
+
// Escape via a NAV LINK (anchor), not a wizard survey button — a role
|
|
715
|
+
// option like "Back End Developer" is a button and must never be mistaken
|
|
716
|
+
// for a "Developers" settings link.
|
|
717
|
+
const isNavLink = el.tag === "a" || el.role === "link";
|
|
718
|
+
if (!isNavLink || el.visible === false)
|
|
719
|
+
continue;
|
|
720
|
+
if (alreadyClicked.has(el.selector))
|
|
721
|
+
continue;
|
|
722
|
+
const href = el.href ?? "";
|
|
723
|
+
const text = [el.visibleText, el.ariaLabel, el.title, el.labelText, el.iconLabel]
|
|
724
|
+
.filter((s) => s !== null && s !== undefined)
|
|
725
|
+
.join(" ");
|
|
726
|
+
const hrefHit = href.length > 0 && WIZARD_ESCAPE_HREF.test(href);
|
|
727
|
+
const textHit = WIZARD_ESCAPE_TEXT.test(text);
|
|
728
|
+
if (!hrefHit && !textHit)
|
|
729
|
+
continue;
|
|
730
|
+
let score = 0;
|
|
731
|
+
if (hrefHit)
|
|
732
|
+
score += 4; // a navigable target beats a text guess
|
|
733
|
+
// settings/account/keys are where API credentials live — rank above home.
|
|
734
|
+
if (/\/(?:settings|account|api[-_]?keys?|developers?)(?:\/|$|\?)/i.test(href))
|
|
735
|
+
score += 3;
|
|
736
|
+
if (/\b(?:settings|account|api\s*keys?|developers?)\b/i.test(text))
|
|
737
|
+
score += 2;
|
|
738
|
+
if (el.tag === "a" && href.length > 0)
|
|
739
|
+
score += 1;
|
|
740
|
+
if (best === null || score > best.score)
|
|
741
|
+
best = { el, score };
|
|
742
|
+
}
|
|
743
|
+
return best?.el ?? null;
|
|
744
|
+
}
|
|
616
745
|
export function findApiKeysNavLink(inventory, alreadyClicked = new Set()) {
|
|
617
746
|
const candidates = [];
|
|
618
747
|
for (const el of inventory) {
|
|
@@ -661,6 +790,58 @@ export function findApiKeysNavLink(inventory, alreadyClicked = new Set()) {
|
|
|
661
790
|
candidates.sort((a, b) => b.score - a.score);
|
|
662
791
|
return candidates[0].el;
|
|
663
792
|
}
|
|
793
|
+
export function findRenderAccountSettingsLink(inventory, alreadyClicked = new Set()) {
|
|
794
|
+
for (const el of inventory) {
|
|
795
|
+
if (el.visible === false)
|
|
796
|
+
continue;
|
|
797
|
+
if (alreadyClicked.has(el.selector))
|
|
798
|
+
continue;
|
|
799
|
+
const clickable = el.tag === "a" ||
|
|
800
|
+
el.tag === "button" ||
|
|
801
|
+
el.role === "link" ||
|
|
802
|
+
el.role === "button";
|
|
803
|
+
if (!clickable)
|
|
804
|
+
continue;
|
|
805
|
+
const label = elementLabel(el);
|
|
806
|
+
if (/\baccount settings\b/i.test(label) || /\/u\/[^/\s]+\/settings(?:[#?\s]|$)/i.test(label)) {
|
|
807
|
+
return el;
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
return null;
|
|
811
|
+
}
|
|
812
|
+
export function findRenderAccountMenuTrigger(inventory) {
|
|
813
|
+
const candidates = [];
|
|
814
|
+
for (const el of inventory) {
|
|
815
|
+
if (el.visible === false)
|
|
816
|
+
continue;
|
|
817
|
+
const clickable = el.tag === "button" || el.role === "button";
|
|
818
|
+
if (!clickable)
|
|
819
|
+
continue;
|
|
820
|
+
const label = elementLabel(el);
|
|
821
|
+
if (label.length === 0)
|
|
822
|
+
continue;
|
|
823
|
+
const normalized = label.toLowerCase().trim();
|
|
824
|
+
if (/\b(?:new|upgrade|search|settings|billing|projects?|blueprints?|notifications?|contact support|create|add)\b/i.test(normalized)) {
|
|
825
|
+
continue;
|
|
826
|
+
}
|
|
827
|
+
let score = 0;
|
|
828
|
+
if (el.landmark === "header")
|
|
829
|
+
score += 4;
|
|
830
|
+
if (/^[a-z]{1,3}$/i.test(normalized))
|
|
831
|
+
score += 3;
|
|
832
|
+
if (/\b(?:profile|account|user|avatar)\b/i.test(label))
|
|
833
|
+
score += 2;
|
|
834
|
+
if (el.inViewport === true)
|
|
835
|
+
score += 1;
|
|
836
|
+
if (score <= 0)
|
|
837
|
+
continue;
|
|
838
|
+
candidates.push({ el, score });
|
|
839
|
+
}
|
|
840
|
+
if (candidates.length === 0)
|
|
841
|
+
return null;
|
|
842
|
+
candidates.sort((a, b) => b.score - a.score);
|
|
843
|
+
return candidates[0].el;
|
|
844
|
+
}
|
|
664
845
|
const RELOCATED_PAGE_TEXT = /\b(?:404|page not found|not found|does not exist|no longer exists|has moved|moved|migrated|now (?:lives|exists)|part of .* instead|teams? list|organisations? list|organizations? list)\b/i;
|
|
665
846
|
const CREDENTIAL_CONTEXT_TEXT = /\b(?:api[\s_-]*(?:keys?|tokens?)|access[\s_-]*tokens?|personal[\s_-]*access[\s_-]*tokens?|secret[\s_-]*keys?|auth[\s_-]*tokens?|credentials?|developers?)\b/i;
|
|
666
847
|
const RELOCATION_RECOVERY_TEXT = /\b(?:teams?|organisations?|organizations?|accounts?|settings|profile|dashboard|developers?|api|keys?|tokens?|credentials?)\b/i;
|
|
@@ -3728,6 +3909,16 @@ export function extractApiKeyFromText(text) {
|
|
|
3728
3909
|
// pattern only fires on Zeabur-style keys. Surfaced from the
|
|
3729
3910
|
// rc.23 snapshot review.
|
|
3730
3911
|
/\bsk-[a-z0-9]{28,38}\b/, // Zeabur
|
|
3912
|
+
// PostHog PERSONAL API key — `phx_<43+ alnum>`. MEASURED 2026-06-24: the
|
|
3913
|
+
// post-OAuth create-personal-api-key flow mints a `phx_`-prefixed secret —
|
|
3914
|
+
// the planner SAW it (and even OCR-transcribed it with errors), but no regex
|
|
3915
|
+
// matched so the DOM extractor stored nothing and the run bailed
|
|
3916
|
+
// oauth_onboarding_failed while a real key sat on the page. The {40,60}
|
|
3917
|
+
// bound keeps it clear of short prefixed noise. Deliberately NOT matching
|
|
3918
|
+
// the `phc_` PROJECT key —
|
|
3919
|
+
// that's a PUBLIC analytics key embedded in client JS on every PostHog site
|
|
3920
|
+
// (see extract-credentials.test "ignores a PostHog project key").
|
|
3921
|
+
/\bphx_[A-Za-z0-9]{40,60}\b/, // PostHog personal API key
|
|
3731
3922
|
// OpenRouter, Anthropic, OpenAI — these are the dominant
|
|
3732
3923
|
// OAuth-completed-then-copy-needed services. Specific-prefix
|
|
3733
3924
|
// patterns first so a labeled-pattern fallback isn't load-
|
|
@@ -5841,7 +6032,7 @@ export class SignupAgent {
|
|
|
5841
6032
|
// branch will actually consult it (candidates present, NOT committed, and
|
|
5842
6033
|
// no provider button hit yet). Computing it unconditionally would fire a
|
|
5843
6034
|
// spurious extractText() every round and diverge from the inline path.
|
|
5844
|
-
const needScanShell = oauthCandidates.length > 0 && !state.
|
|
6035
|
+
const needScanShell = oauthCandidates.length > 0 && !state.oauthProvenUnavailable && oauthButtonHitRaw === null;
|
|
5845
6036
|
const oauthScanShell = needScanShell &&
|
|
5846
6037
|
(inventory.length <= 1 ||
|
|
5847
6038
|
!hasCredentialInput ||
|
|
@@ -7568,6 +7759,7 @@ export class SignupAgent {
|
|
|
7568
7759
|
...(task.scopeHint !== undefined ? { scopeHint: task.scopeHint } : {}),
|
|
7569
7760
|
...(task.machineToken !== undefined ? { machineToken: task.machineToken } : {}),
|
|
7570
7761
|
...(task.apiBase !== undefined ? { apiBase: task.apiBase } : {}),
|
|
7762
|
+
allowOperatorInboxOtp: task.allowOperatorInboxOtp === true,
|
|
7571
7763
|
});
|
|
7572
7764
|
}
|
|
7573
7765
|
if (hasUsableCredentialBundle(credentials)) {
|
|
@@ -7839,6 +8031,7 @@ export class SignupAgent {
|
|
|
7839
8031
|
...(task.scopeHint !== undefined ? { scopeHint: task.scopeHint } : {}),
|
|
7840
8032
|
...(task.machineToken !== undefined ? { machineToken: task.machineToken } : {}),
|
|
7841
8033
|
...(task.apiBase !== undefined ? { apiBase: task.apiBase } : {}),
|
|
8034
|
+
allowOperatorInboxOtp: task.allowOperatorInboxOtp === true,
|
|
7842
8035
|
});
|
|
7843
8036
|
}
|
|
7844
8037
|
}
|
|
@@ -7887,6 +8080,9 @@ export class SignupAgent {
|
|
|
7887
8080
|
}
|
|
7888
8081
|
}
|
|
7889
8082
|
if (credentials.api_key !== undefined || credentials.username !== undefined) {
|
|
8083
|
+
// Form-fill + email success returns here WITHOUT entering the post-verify
|
|
8084
|
+
// loop, so its extract-round salvage never ran — write one now (mailjet).
|
|
8085
|
+
await this.salvageExtractCaptureIfNeeded(task.service, credentials, false);
|
|
7890
8086
|
return {
|
|
7891
8087
|
success: true,
|
|
7892
8088
|
credentials: { ...credentials, password, email: task.email },
|
|
@@ -8451,7 +8647,8 @@ export class SignupAgent {
|
|
|
8451
8647
|
// degrades to the phone-tap path below.
|
|
8452
8648
|
if (provider.id === "github" &&
|
|
8453
8649
|
task.machineToken !== undefined &&
|
|
8454
|
-
task.machineToken.length > 0
|
|
8650
|
+
task.machineToken.length > 0 &&
|
|
8651
|
+
task.allowOperatorInboxOtp === true) {
|
|
8455
8652
|
steps.push("GitHub: verify-it's-you challenge — polling operator inbox for a device-confirmation link (up to 60s)");
|
|
8456
8653
|
try {
|
|
8457
8654
|
const { readGitHubChallengeLink } = await import("./read-otp.js");
|
|
@@ -9069,6 +9266,9 @@ export class SignupAgent {
|
|
|
9069
9266
|
machineToken.length === 0) {
|
|
9070
9267
|
otpResult = { code: null, reason: "no_machine_token" };
|
|
9071
9268
|
}
|
|
9269
|
+
else if (task.allowOperatorInboxOtp !== true) {
|
|
9270
|
+
otpResult = { code: null, reason: "operator_inbox_consent_missing" };
|
|
9271
|
+
}
|
|
9072
9272
|
else {
|
|
9073
9273
|
steps.push(`Email-OTP gate detected (${pathOf(gateState.url)}) — polling operator inbox for the code` +
|
|
9074
9274
|
(domain !== null ? ` (from_domain=${domain})` : ""));
|
|
@@ -9222,6 +9422,7 @@ export class SignupAgent {
|
|
|
9222
9422
|
...(task.scopeHint !== undefined ? { scopeHint: task.scopeHint } : {}),
|
|
9223
9423
|
...(task.machineToken !== undefined ? { machineToken: task.machineToken } : {}),
|
|
9224
9424
|
...(task.apiBase !== undefined ? { apiBase: task.apiBase } : {}),
|
|
9425
|
+
allowOperatorInboxOtp: task.allowOperatorInboxOtp === true,
|
|
9225
9426
|
});
|
|
9226
9427
|
}
|
|
9227
9428
|
catch (err) {
|
|
@@ -9692,6 +9893,7 @@ ${formatInventory(input.inventory)}`,
|
|
|
9692
9893
|
...(task.scopeHint !== undefined ? { scopeHint: task.scopeHint } : {}),
|
|
9693
9894
|
...(task.machineToken !== undefined ? { machineToken: task.machineToken } : {}),
|
|
9694
9895
|
...(task.apiBase !== undefined ? { apiBase: task.apiBase } : {}),
|
|
9896
|
+
allowOperatorInboxOtp: task.allowOperatorInboxOtp === true,
|
|
9695
9897
|
});
|
|
9696
9898
|
}
|
|
9697
9899
|
// Drive the browser toward the API key after the account exists —
|
|
@@ -10351,6 +10553,38 @@ Prefer items naming keys / tokens / API / developer / secrets; then credentials
|
|
|
10351
10553
|
// form-fill phase already captured (captureSignupFormRounds); 0 on the
|
|
10352
10554
|
// OAuth path, so this is unchanged for OAuth skills.
|
|
10353
10555
|
let capturedRound = this.captureChainRound;
|
|
10556
|
+
if (hasCuratedServiceKeyPath(args.service)) {
|
|
10557
|
+
const fallback = pickStuckLoopFallbackUrl(this.browser.currentUrl(), recovery.triedFallbackUrls, args.service, this.resolvedSignupUrl);
|
|
10558
|
+
if (fallback !== null) {
|
|
10559
|
+
recovery.triedFallbackUrls.add(fallback);
|
|
10560
|
+
args.steps.push(`Post-verify: nav-search exhausted but ${args.service} has a curated credential route — trying ${fallback} before greedy planning.`);
|
|
10561
|
+
try {
|
|
10562
|
+
await this.browser.goto(fallback);
|
|
10563
|
+
await this.browser.waitForInteractiveDom(5, 15_000).catch(() => undefined);
|
|
10564
|
+
const direct = await this.harvestVisibleCredentials();
|
|
10565
|
+
for (const [k, v] of Object.entries(direct)) {
|
|
10566
|
+
if (credentials[k] === undefined)
|
|
10567
|
+
credentials[k] = v;
|
|
10568
|
+
}
|
|
10569
|
+
if (hasAnyExtractedCredential(credentials)) {
|
|
10570
|
+
await this.writeFastPathSyntheticCapture(args.service, capturedRound, oauth, "curated credential route synthetic extract — nav-search exhausted, fallback route exposed credentials");
|
|
10571
|
+
return credentials;
|
|
10572
|
+
}
|
|
10573
|
+
const minted = await this.attemptMintNewKey(args.steps, args.service);
|
|
10574
|
+
if (minted !== null && hasAnyExtractedCredential(minted)) {
|
|
10575
|
+
for (const [k, v] of Object.entries(minted)) {
|
|
10576
|
+
if (credentials[k] === undefined)
|
|
10577
|
+
credentials[k] = v;
|
|
10578
|
+
}
|
|
10579
|
+
await this.writeFastPathSyntheticCapture(args.service, capturedRound, oauth, "curated credential route synthetic extract — minted/extracted after nav-search fallback");
|
|
10580
|
+
return credentials;
|
|
10581
|
+
}
|
|
10582
|
+
}
|
|
10583
|
+
catch (err) {
|
|
10584
|
+
args.steps.push(`Post-verify: curated credential route fallback errored (${err instanceof Error ? err.message : String(err)}) — continuing with greedy planner.`);
|
|
10585
|
+
}
|
|
10586
|
+
}
|
|
10587
|
+
}
|
|
10354
10588
|
const credentialTracker = new PostSignupCredentialTracker(credentials);
|
|
10355
10589
|
// Gate URLs we've already polled the operator's gmail for, so a
|
|
10356
10590
|
// multi-round wait on the same email-OTP page doesn't re-poll.
|
|
@@ -10390,6 +10624,23 @@ Prefer items naming keys / tokens / API / developer / secrets; then credentials
|
|
|
10390
10624
|
// the planner choosing the right click, so an on-screen key is never
|
|
10391
10625
|
// missed into a maxRounds bail. Merge-only (never overwrites a prior
|
|
10392
10626
|
// capture); both extractors are best-effort.
|
|
10627
|
+
try {
|
|
10628
|
+
const currentUrl = this.browser.currentUrl();
|
|
10629
|
+
if (!recovery.revealSweepUrls.has(currentUrl)) {
|
|
10630
|
+
const sweepText = await this.browser.extractVisibleText().catch(() => "");
|
|
10631
|
+
if (shouldRevealBeforeCredentialSweep({ url: currentUrl, pageText: sweepText })) {
|
|
10632
|
+
recovery.revealSweepUrls.add(currentUrl);
|
|
10633
|
+
const reveal = await this.browser.revealMaskedCredentials();
|
|
10634
|
+
if (reveal.clicked > 0) {
|
|
10635
|
+
args.steps.push(`Post-verify round ${round}: credential surface has reveal controls — clicked ${reveal.clicked} before sweeping.`);
|
|
10636
|
+
await this.browser.wait(1);
|
|
10637
|
+
}
|
|
10638
|
+
}
|
|
10639
|
+
}
|
|
10640
|
+
}
|
|
10641
|
+
catch {
|
|
10642
|
+
// reveal is opportunistic; normal extraction still runs below
|
|
10643
|
+
}
|
|
10393
10644
|
try {
|
|
10394
10645
|
const sweep = await this.extractCredentials();
|
|
10395
10646
|
for (const [k, v] of Object.entries(sweep)) {
|
|
@@ -10449,10 +10700,17 @@ Prefer items naming keys / tokens / API / developer / secrets; then credentials
|
|
|
10449
10700
|
try {
|
|
10450
10701
|
if (await this.browser.hasDisabledSubmit()) {
|
|
10451
10702
|
const picked = await this.browser.fillRequiredComboboxes();
|
|
10452
|
-
|
|
10703
|
+
// The scope/preset pattern (API-key creation forms) gates submit
|
|
10704
|
+
// behind a segmented "All access" + a LemonSelect preset the
|
|
10705
|
+
// combobox filler can't see — satisfy those too before giving up.
|
|
10706
|
+
const scoped = picked.length > 0
|
|
10707
|
+
? []
|
|
10708
|
+
: await this.browser.satisfyScopePresets().catch(() => []);
|
|
10709
|
+
const satisfied = [...picked, ...scoped];
|
|
10710
|
+
if (satisfied.length > 0) {
|
|
10453
10711
|
comboboxFilled = true;
|
|
10454
|
-
args.steps.push(`Post-verify: satisfied ${
|
|
10455
|
-
`to clear a disabled submit — ${
|
|
10712
|
+
args.steps.push(`Post-verify: satisfied ${satisfied.length} required onboarding select(s) ` +
|
|
10713
|
+
`to clear a disabled submit — ${satisfied.join(", ")}`);
|
|
10456
10714
|
await this.browser.waitForFormReady().catch(() => undefined);
|
|
10457
10715
|
}
|
|
10458
10716
|
else {
|
|
@@ -10706,6 +10964,77 @@ Prefer items naming keys / tokens / API / developer / secrets; then credentials
|
|
|
10706
10964
|
else {
|
|
10707
10965
|
lastReachablePostVerifyUrl = state.url;
|
|
10708
10966
|
}
|
|
10967
|
+
if (serviceSlug(args.service) === "render" &&
|
|
10968
|
+
!hasAnyExtractedCredential(credentials) &&
|
|
10969
|
+
/dashboard\.render\.com/i.test(state.url)) {
|
|
10970
|
+
const onRenderAccountSettings = /\/u\/[^/]+\/settings\b/i.test(state.url);
|
|
10971
|
+
if (!onRenderAccountSettings) {
|
|
10972
|
+
const accountSettingsLink = findRenderAccountSettingsLink(inventory, recovery.clickedKeysLinks);
|
|
10973
|
+
if (accountSettingsLink !== null) {
|
|
10974
|
+
recovery.clickedKeysLinks.add(accountSettingsLink.selector);
|
|
10975
|
+
const label = accountSettingsLink.visibleText ??
|
|
10976
|
+
accountSettingsLink.ariaLabel ??
|
|
10977
|
+
accountSettingsLink.href ??
|
|
10978
|
+
accountSettingsLink.selector;
|
|
10979
|
+
args.steps.push(`Post-verify round ${round}: Render account menu exposes "${label.slice(0, 60)}" — entering account settings before API-key navigation.`);
|
|
10980
|
+
try {
|
|
10981
|
+
await this.browser.click(accountSettingsLink.selector);
|
|
10982
|
+
await this.browser.waitForInteractiveDom(5, 15_000).catch(() => undefined);
|
|
10983
|
+
hint = undefined;
|
|
10984
|
+
recovery.prevSignature = null;
|
|
10985
|
+
recovery.prevInventorySize = -1;
|
|
10986
|
+
continue;
|
|
10987
|
+
}
|
|
10988
|
+
catch (err) {
|
|
10989
|
+
args.steps.push(`Post-verify round ${round}: Render account-settings click failed (${err instanceof Error ? err.message : String(err)}) — continuing.`);
|
|
10990
|
+
}
|
|
10991
|
+
}
|
|
10992
|
+
else if (!recovery.triedRenderAccountMenu) {
|
|
10993
|
+
const accountMenu = findRenderAccountMenuTrigger(inventory);
|
|
10994
|
+
if (accountMenu !== null) {
|
|
10995
|
+
recovery.triedRenderAccountMenu = true;
|
|
10996
|
+
const label = accountMenu.visibleText ??
|
|
10997
|
+
accountMenu.ariaLabel ??
|
|
10998
|
+
accountMenu.title ??
|
|
10999
|
+
accountMenu.selector;
|
|
11000
|
+
args.steps.push(`Post-verify round ${round}: Render dashboard has no API-key link visible — opening account menu "${label.slice(0, 40)}".`);
|
|
11001
|
+
try {
|
|
11002
|
+
await this.browser.click(accountMenu.selector);
|
|
11003
|
+
await this.browser.waitForInteractiveDom(5, 15_000).catch(() => undefined);
|
|
11004
|
+
hint = undefined;
|
|
11005
|
+
recovery.prevSignature = null;
|
|
11006
|
+
recovery.prevInventorySize = -1;
|
|
11007
|
+
continue;
|
|
11008
|
+
}
|
|
11009
|
+
catch (err) {
|
|
11010
|
+
args.steps.push(`Post-verify round ${round}: Render account-menu click failed (${err instanceof Error ? err.message : String(err)}) — continuing.`);
|
|
11011
|
+
}
|
|
11012
|
+
}
|
|
11013
|
+
}
|
|
11014
|
+
}
|
|
11015
|
+
else if (!/#api-keys\b/i.test(state.url)) {
|
|
11016
|
+
const keysLink = findApiKeysNavLink(inventory, recovery.clickedKeysLinks);
|
|
11017
|
+
if (keysLink !== null) {
|
|
11018
|
+
recovery.clickedKeysLinks.add(keysLink.selector);
|
|
11019
|
+
const label = keysLink.visibleText ??
|
|
11020
|
+
keysLink.ariaLabel ??
|
|
11021
|
+
keysLink.href ??
|
|
11022
|
+
keysLink.selector;
|
|
11023
|
+
args.steps.push(`Post-verify round ${round}: Render account settings reached — clicking API-key anchor "${label.slice(0, 60)}".`);
|
|
11024
|
+
try {
|
|
11025
|
+
await this.browser.click(keysLink.selector);
|
|
11026
|
+
await this.browser.waitForInteractiveDom(5, 15_000).catch(() => undefined);
|
|
11027
|
+
hint = undefined;
|
|
11028
|
+
recovery.prevSignature = null;
|
|
11029
|
+
recovery.prevInventorySize = -1;
|
|
11030
|
+
continue;
|
|
11031
|
+
}
|
|
11032
|
+
catch (err) {
|
|
11033
|
+
args.steps.push(`Post-verify round ${round}: Render API-key anchor click failed (${err instanceof Error ? err.message : String(err)}) — continuing.`);
|
|
11034
|
+
}
|
|
11035
|
+
}
|
|
11036
|
+
}
|
|
11037
|
+
}
|
|
10709
11038
|
const emailAlias = args.verificationEmailAlias ??
|
|
10710
11039
|
args.credentials?.email ??
|
|
10711
11040
|
args.oauthAccountEmail;
|
|
@@ -10978,6 +11307,72 @@ Prefer items naming keys / tokens / API / developer / secrets; then credentials
|
|
|
10978
11307
|
}
|
|
10979
11308
|
}
|
|
10980
11309
|
}
|
|
11310
|
+
// JS-click recovery FIRST. The dominant wizard-stall cause is a custom
|
|
11311
|
+
// radio-card selection whose React onClick a normal (coordinate/CDP)
|
|
11312
|
+
// click doesn't fire, so the page never changes and "Next" stays
|
|
11313
|
+
// inert — MEASURED 2026-06-24 (auth0 Account-Type Personal/Company,
|
|
11314
|
+
// cloudinary survey, posthog role-picker class). Re-dispatch the recent
|
|
11315
|
+
// page-mutating clicks via page.evaluate(el.click()) (clickViaJs), which
|
|
11316
|
+
// fires the handler; the selection commits and the next forward click
|
|
11317
|
+
// advances. One attempt, then fall through to the escape/stall.
|
|
11318
|
+
if (!recovery.triedWizardJsClick && typeof this.browser.clickViaJs === "function") {
|
|
11319
|
+
recovery.triedWizardJsClick = true;
|
|
11320
|
+
const recentClickSelectors = recovery.actionEffects
|
|
11321
|
+
.filter((e) => e.kind === "click" && e.selector !== null)
|
|
11322
|
+
.map((e) => e.selector)
|
|
11323
|
+
.filter((s, i, a) => a.indexOf(s) === i);
|
|
11324
|
+
if (recentClickSelectors.length > 0) {
|
|
11325
|
+
args.steps.push(`Post-verify: STALLED in a wizard — re-dispatching ${recentClickSelectors.length} recent click(s) via JS ` +
|
|
11326
|
+
`(custom radio-card onClick that the normal click didn't fire).`);
|
|
11327
|
+
for (const sel of recentClickSelectors) {
|
|
11328
|
+
await this.browser.clickViaJs(sel).catch(() => undefined);
|
|
11329
|
+
await this.browser.wait(1);
|
|
11330
|
+
}
|
|
11331
|
+
// Then JS-click a forward/submit button so a now-enabled Next advances.
|
|
11332
|
+
const fwd = pickOnboardingSubmit(inventory);
|
|
11333
|
+
if (fwd !== null) {
|
|
11334
|
+
await this.browser.clickViaJs(fwd.selector).catch(() => undefined);
|
|
11335
|
+
}
|
|
11336
|
+
await this.browser.waitForInteractiveDom(5, 15_000).catch(() => undefined);
|
|
11337
|
+
recovery.actionEffects.length = 0;
|
|
11338
|
+
recovery.prevContentSig = null;
|
|
11339
|
+
hint = undefined;
|
|
11340
|
+
continue;
|
|
11341
|
+
}
|
|
11342
|
+
}
|
|
11343
|
+
// Wizard-escape before giving up. A re-presenting onboarding wizard
|
|
11344
|
+
// whose clicks don't register is an OVERLAY on the real product
|
|
11345
|
+
// dashboard — its sidebar (Home/Settings/API Keys) is already in the
|
|
11346
|
+
// inventory. Navigate to a settings/keys/home nav link to reach the
|
|
11347
|
+
// credential surface behind it (MEASURED 2026-06-24, cloudinary
|
|
11348
|
+
// /app/welcome: the survey no-ops but Settings → /app/settings is right
|
|
11349
|
+
// there). One attempt, then honor the stall.
|
|
11350
|
+
if (!recovery.triedWizardEscape) {
|
|
11351
|
+
recovery.triedWizardEscape = true;
|
|
11352
|
+
const escapeLink = findWizardEscapeNavLink(inventory, recovery.clickedKeysLinks);
|
|
11353
|
+
if (escapeLink !== null) {
|
|
11354
|
+
recovery.clickedKeysLinks.add(escapeLink.selector);
|
|
11355
|
+
const label = escapeLink.visibleText ?? escapeLink.ariaLabel ?? escapeLink.href ?? "the dashboard";
|
|
11356
|
+
args.steps.push(`Post-verify: STALLED in an onboarding wizard — escaping to ${JSON.stringify(label.slice(0, 40))} ` +
|
|
11357
|
+
`(the wizard overlays the product dashboard; the key surface is behind it).`);
|
|
11358
|
+
try {
|
|
11359
|
+
if (escapeLink.href !== undefined && escapeLink.href !== null && escapeLink.href.length > 0) {
|
|
11360
|
+
await this.browser.goto(new URL(escapeLink.href, state.url).toString());
|
|
11361
|
+
}
|
|
11362
|
+
else {
|
|
11363
|
+
await this.browser.click(escapeLink.selector);
|
|
11364
|
+
}
|
|
11365
|
+
await this.browser.waitForInteractiveDom(5, 15_000).catch(() => undefined);
|
|
11366
|
+
recovery.actionEffects.length = 0;
|
|
11367
|
+
recovery.prevContentSig = null;
|
|
11368
|
+
hint = undefined;
|
|
11369
|
+
continue;
|
|
11370
|
+
}
|
|
11371
|
+
catch (err) {
|
|
11372
|
+
args.steps.push(`Post-verify: wizard-escape navigate failed (${err instanceof Error ? err.message : String(err)}) — honoring the stall.`);
|
|
11373
|
+
}
|
|
11374
|
+
}
|
|
11375
|
+
}
|
|
10981
11376
|
// Capture the exact page that defeated the wizard so the N1
|
|
10982
11377
|
// onboarding-wizard class is debuggable post-hoc (post-verify pages
|
|
10983
11378
|
// weren't being snapshotted — imagekit's step-1/3 role-select stall
|
|
@@ -11037,6 +11432,7 @@ Prefer items naming keys / tokens / API / developer / secrets; then credentials
|
|
|
11037
11432
|
if (hint === undefined &&
|
|
11038
11433
|
args.machineToken !== undefined &&
|
|
11039
11434
|
args.machineToken.length > 0 &&
|
|
11435
|
+
args.allowOperatorInboxOtp === true &&
|
|
11040
11436
|
!otpPolledUrls.has(state.url) &&
|
|
11041
11437
|
detectEmailOtpGate(state.url, state.title, await this.browser.extractText().catch(() => ""), inventory)) {
|
|
11042
11438
|
otpPolledUrls.add(state.url);
|
|
@@ -11604,6 +12000,46 @@ Prefer items naming keys / tokens / API / developer / secrets; then credentials
|
|
|
11604
12000
|
recovery.prevSignature = null;
|
|
11605
12001
|
recovery.prevInventorySize = inventory.length;
|
|
11606
12002
|
}
|
|
12003
|
+
const actionCycleSignature = credentialActionSignature(nextStep, inventory);
|
|
12004
|
+
if (actionCycleSignature !== null) {
|
|
12005
|
+
recovery.recentCredentialActionSignatures.push(actionCycleSignature);
|
|
12006
|
+
if (recovery.recentCredentialActionSignatures.length > 8) {
|
|
12007
|
+
recovery.recentCredentialActionSignatures =
|
|
12008
|
+
recovery.recentCredentialActionSignatures.slice(-8);
|
|
12009
|
+
}
|
|
12010
|
+
if (isRepeatingCredentialActionCycle(recovery.recentCredentialActionSignatures, visiblePageText)) {
|
|
12011
|
+
args.steps.push(`Post-verify: detected repeating credential-action cycle (${recovery.recentCredentialActionSignatures.slice(-5).join(" → ")}) — trying extraction/fallback instead of repeating it.`);
|
|
12012
|
+
const harvested = await this.harvestVisibleCredentials().catch(() => ({}));
|
|
12013
|
+
for (const [k, v] of Object.entries(harvested)) {
|
|
12014
|
+
if (credentials[k] === undefined)
|
|
12015
|
+
credentials[k] = v;
|
|
12016
|
+
}
|
|
12017
|
+
if (hasAnyExtractedCredential(credentials)) {
|
|
12018
|
+
await this.writeFastPathSyntheticCapture(args.service, capturedRound, oauth, "repeating credential-action cycle synthetic extract — credentials were already visible");
|
|
12019
|
+
return credentials;
|
|
12020
|
+
}
|
|
12021
|
+
const fallback = pickStuckLoopFallbackUrl(state.url, recovery.triedFallbackUrls, args.service, this.resolvedSignupUrl);
|
|
12022
|
+
if (fallback !== null) {
|
|
12023
|
+
recovery.triedFallbackUrls.add(fallback);
|
|
12024
|
+
args.steps.push(`Post-verify: credential-action cycle fallback — navigating to ${fallback}`);
|
|
12025
|
+
try {
|
|
12026
|
+
await this.browser.goto(fallback);
|
|
12027
|
+
await this.browser.waitForInteractiveDom(5, 15_000).catch(() => undefined);
|
|
12028
|
+
}
|
|
12029
|
+
catch (err) {
|
|
12030
|
+
args.steps.push(`Post-verify: credential-action cycle fallback navigate failed (${err instanceof Error ? err.message : String(err)}) — continuing.`);
|
|
12031
|
+
}
|
|
12032
|
+
recovery.prevSignature = null;
|
|
12033
|
+
recovery.prevInventorySize = -1;
|
|
12034
|
+
hint = undefined;
|
|
12035
|
+
continue;
|
|
12036
|
+
}
|
|
12037
|
+
this.lastPostVerifyDoneReason =
|
|
12038
|
+
`[stuck_loop] planner repeated credential/payment actions (${recovery.recentCredentialActionSignatures.slice(-5).join(" → ")}) with no usable credential and no fallback URL remaining.`;
|
|
12039
|
+
args.steps.push(`Post-verify: credential-action cycle unresolvable — breaking out with planner_stuck.`);
|
|
12040
|
+
break;
|
|
12041
|
+
}
|
|
12042
|
+
}
|
|
11607
12043
|
// Record the kind of the step we're ABOUT to execute (all re-plan
|
|
11608
12044
|
// `continue` guards are behind us here) so next round can judge
|
|
11609
12045
|
// whether it changed the page — the stalled-wizard breaker above.
|
|
@@ -11983,6 +12419,16 @@ Prefer items naming keys / tokens / API / developer / secrets; then credentials
|
|
|
11983
12419
|
}
|
|
11984
12420
|
}
|
|
11985
12421
|
}
|
|
12422
|
+
// Salvage the synthesis path. The bot's credential-tracker / background
|
|
12423
|
+
// extraction can populate `credentials` without a planner `extract` action
|
|
12424
|
+
// ever running — the loop then ends on a `done`/`click`, so the on-disk
|
|
12425
|
+
// capture has no extract round and the synthesizer REJECTS it
|
|
12426
|
+
// (no_extract_step) even though the run SUCCEEDED. MEASURED 2026-06-24
|
|
12427
|
+
// (serpapi/galileo/mailjet: extracted api_key, last round was `done`, skill
|
|
12428
|
+
// never promoted). When we're about to return a real credential but no
|
|
12429
|
+
// extract round was captured this run, write one for the final page so the
|
|
12430
|
+
// success becomes a replayable skill instead of an OF#2-only win.
|
|
12431
|
+
await this.salvageExtractCaptureIfNeeded(args.service, credentials, oauth);
|
|
11986
12432
|
return credentials;
|
|
11987
12433
|
}
|
|
11988
12434
|
// 0.8.3-rc.1 — write a single synthetic extract round when the
|
|
@@ -12000,6 +12446,23 @@ Prefer items naming keys / tokens / API / developer / secrets; then credentials
|
|
|
12000
12446
|
//
|
|
12001
12447
|
// Best-effort: a capture failure here must NEVER block returning
|
|
12002
12448
|
// the credential we already have.
|
|
12449
|
+
// Salvage the synthesis path for ANY successful exit (not just the post-verify
|
|
12450
|
+
// loop). When a run returns a real credential but no `extract` round was
|
|
12451
|
+
// captured — the form-fill+email path (mailjet) returns before the post-verify
|
|
12452
|
+
// loop ever runs, so its salvage never fires — write a synthetic extract round
|
|
12453
|
+
// for the final page so the success becomes a replayable skill. Gated on
|
|
12454
|
+
// hasCapturedAnyRound: a LONE extract round (no captured nav/OAuth steps — the
|
|
12455
|
+
// no_rounds class, e.g. galileo's OAuth-fast-path) can't be replayed and would
|
|
12456
|
+
// synth-reject anyway, so don't write one. Idempotent via hasCapturedExtractRound.
|
|
12457
|
+
async salvageExtractCaptureIfNeeded(service, credentials, oauth) {
|
|
12458
|
+
if (!hasAnyExtractedCredential(credentials))
|
|
12459
|
+
return;
|
|
12460
|
+
if (hasCapturedExtractRound(service))
|
|
12461
|
+
return;
|
|
12462
|
+
if (!hasCapturedAnyRound(service))
|
|
12463
|
+
return;
|
|
12464
|
+
await this.writeFastPathSyntheticCapture(service, nextCaptureRound(service), oauth, "salvage synthetic extract — credential captured on a non-post-verify exit; no planner extract round ran");
|
|
12465
|
+
}
|
|
12003
12466
|
async writeFastPathSyntheticCapture(service, capturedRound, oauth, reason = "fast-path synthetic extract — credentials were already on the page before any planner round ran") {
|
|
12004
12467
|
try {
|
|
12005
12468
|
const [state, inventory] = await Promise.all([
|