Skip to content

Enhance sign-in password handling and memory login validation - #3337

Merged
esokullu merged 11 commits into
webbrain-one:mainfrom
esokullu:main
Nov 8, 2026
Merged

esokullu merged 11 commits into
webbrain-one:mainfrom
esokullu:main

Conversation

@esokullu

@esokullu esokullu commented Nov 8, 2026

Copy link
Copy Markdown
Collaborator

No description provided.

esokullu and others added 11 commits November 8, 2026 17:41
Two gaps in the credential-not-provided guard from #593, both confirmed
by a probe against the merged code:

- A login the user saved in WebBrain's local memory ("QA portal login:
  qa-tester / value"; memory refuses text that says "password") was
  refused at the sign-in field, while the same value in the WebBrain
  profile was typed. Memory was not a source.
- Any lone password field whose form did not create an account counted
  as a sign-in, so "Fill out this form with random info" was refused on
  a form's lone "Password" field with "Submit", and "generate a strong
  Wi-Fi password" on a "Wi-Fi password" field with "Apply".

Memory:
- Active local memory notes are a source when memory is enabled, and,
  like the profile, not in a Cloud bot-memory session.
- Only notes the user wrote count (user-memory.js
  userAuthoredMemoryRecords, source "manual": /memory --add, an edit in
  Settings, or an import or Profile Sync that carries that source).
  Every auto-capture path (chat turns, clarification answers, completed
  forms) saves source "auto" with text an extraction model wrote from
  the turn, whose assistant reply and clarify questions can quote the
  page, so those notes are excluded.
- So that source keeps naming who wrote the text: an auto-capture
  rewrite of a manual note now makes it "auto"; a Settings edit that
  changes the text makes it "manual" (update_user_memory passes it, and
  a save that only changes the kind leaves the source); re-saving the
  same words up to case keeps the newer author's text with its source,
  since a password in it is case-sensitive.
- The refusal now names "a WebBrain memory note they saved".

Field rule (credential-fields.js, the same file in both builds):
- A lone password field is a sign-in's only with sign-in evidence: a
  username, email or login field in its form (outside a form, among the
  root's form-less fields), a submit control that signs in or goes to
  the next step ("Sign in", "Log in", "Next", "Continue", "Enter", and
  those words in twelve other languages), or a name or label that says
  sign-in, login or account. autocomplete current-password and a field
  naming the current password stay guarded, and every existing
  exemption still comes first.
- content.js _fieldMeta (both builds) and the Chrome CDP selector probe
  report the new usernameFields fact with the same code. Field facts
  without it (a route that did not read the form) keep the old rule.
- newPasswordValue now takes only fields marked as new (autocomplete
  new-password, a new or repeated password label, a confirmation beside
  it, an account-creating submit), so a value made up for a lone
  password field never becomes a source for a later sign-in.

Docs: security-model.md, guards.md, architecture.md (memory source), and
the guard registry summary. Tests: test/credential-not-provided.mjs and
test/credential-not-provided-dom.mjs; the new cases fail on the old code.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… the user's

Review of the follow-up found real sign-ins the first cut of the lone
password field rule left unguarded, all guarded on main, and two ways a
memory note's provenance could be wrong.

Field facts:
- A password input in a web component's shadow root has no form owner, and
  usernameFields counted only that shadow root, so a light-DOM form of
  Email/Username and Password components with "Sign in" read as a lone
  field. A field's form is now its form owner or the <form> around the
  component across shadow boundaries; outside a form, the form-less part of
  its document across open shadow roots.
- Outside a form no button was read, and the username test missed
  aria-labelledby labels, phone fields and customer or member numbers. Every
  visible button of the field's form (button, input submit/button/image,
  role=button, a button component whose label is slotted in) is now read
  into buttonTexts, nearest first, and any of them saying sign in, log in,
  next, continue, enter, verify, unlock or authenticate (more languages
  now) is evidence. A form with no button of its own (a dialog footer
  submits it) reads the document's form-less buttons.
- An icon-only submit lost its aria-label to whitespace text, and a "Show"
  toggle before "Log in" hid the sign-in. Text is trimmed before falling
  back to aria-label, aria-labelledby or title, and any button counts.
  formSubmitText stays the first submit control, for "Create account".
- A password step's hidden username or identifier now counts (not generic
  ids such as user_id or flow), so #hu-pass (hidden username + "Submit")
  is guarded, as the second step of a sign-in.
- The facts are computed by one function, passwordFieldFacts in the
  shared rich-text-toolbar-heuristic.js, which content.js (both builds)
  and the Chrome CDP selector probe already load, instead of two copies of
  the code. Form-less passwordFields now counts only form-less fields (it
  counted every password field of the page, 7 on the DOM fixture, so a
  form-less sign-in beside any other password field read as new).

Memory:
- The dedupe let auto-capture repeating a note the user saved (on by
  default) take it over: the note became "auto" with the model's casing,
  and stopped counting. Now auto-capture repeating a manual note leaves its
  text, source and confidence; the user's own save, or a capture over a
  captured note, takes the new text and source.
- Before the follow-up, an auto-capture rewrite of a manual note kept
  "manual" with the model's text and confidence. A note counts now only
  with confidence 1, which /memory --add saves and a Settings edit that
  changes the text sets; an auto-capture that keeps the user's words does
  not lower it. A legacy rewrite whose model reported exactly 1.0 cannot be
  told apart, and is documented.

Docs: security-model.md, guards.md, architecture.md and the guard summary.
Tests: four unit tests (dedupe, legacy confidence, any-button evidence,
mirror check of the heuristic and user-memory files) and five DOM fixture
pages (components, buttons, a form-less tel login, a dialog footer) through
both builds' content.js and the CDP probe; they fail on the previous commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
credential-not-provided refused outright when text the model chose for a
sign-in password field was not in the places it reads (messages, clarify
answers, attached files, profile, the user's memory notes, MemCode, a
password WebBrain set). Those are not all the places a user can give a
password: a test form's documented login, or one kept where WebBrain does
not look, left the run stuck with no way through. The owner chose a card:
a wrong refusal should cost one click, while a guessed password must still
never be typed unasked.

The tool batch now pauses such a call before anything is typed and asks on
WebBrain's hard unverified-step card, after the call's action pipeline
returns, so the card waits outside the action deadline. It names the site
and the field ("Type this into a sign-in password field on <host>?"), says
the text is not a password the user gave in this conversation, their
profile or their saved notes, and shows the text masked (first character
and a dot per other character, only dots under four), the way the submit
card shows a password field as [redacted]; with strict secret handling it
shows none of it. Buttons: "Type it" / "Don't type it".

"Type it" runs the call again through the same typing-target preflight with
the approval, and it types only while the re-resolved target is still the
sign-in password field and site the card named (credential_field_changed
otherwise). That text then counts as given on that host for the rest of the
run. "Don't type it", a timeout answer, an unattended scheduled run or a
caller without a card returns the existing refusal (credential_not_provided,
noDispatch, retryable false), and the same text there is refused again
without a card. Cloud and MCP relay the card as they do the other hard
cards; it never times out. Lean mode and Strict checks keep it, saved
workflow replays are still unchecked, and page text is still no source.

The registry entry asks on both outcomes, and test/guard-policy.mjs lists it
as the one hard guard that asks when it knows. Both builds; all 23 locales
translate the new card strings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The sign-in password card docs compared its masking to the submit
confirmation card and quoted that card's placeholder as "[redacted]". The
submit probe (fieldValue in agent.js) writes "[password redacted]", so a
reader checking the claim against the code would not find it. The guards
doc now also says that a Cloud run's update log, which replaces typed text
with "[redacted typed text]", still holds the masked preview of a relayed
card, so nobody assumes Cloud keeps none of the text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review of the sign-in password card found two problems.

The card, and the answers kept for it, named the top-level tab's host
even for a field in a frame from another origin. On a shop page with a
bank's sign-in iframe, a guess approved for the shop's own sign-in field
was then typed into the bank's field with no card. _credentialTargetHost
now takes the site from the field's frame (webNavigation.getAllFrames; a
frame at about:blank or about:srcdoc takes the site of the frame above
it), or, when the preflight named no frame, from the document that
answered the guard's own field probe. That site goes on the card, into
the decision key and into the approval match. A field whose site cannot
be read (a data: frame, a frame that is gone) is refused without a card
instead of being named by the page's host.

The rule that a lone password field is a sign-in's only with sign-in
evidence typed guesses into real sign-ins unasked: a re-authentication
with only "Password" and "Confirm", a "NetID" and "Password" form with
"Submit", a PIN step whose account is a generic hidden id with "OK".
main guarded all three. Now that the guard asks instead of refusing, a
wrong "sign-in" costs the user one click, while a wrong "no sign-in"
types a guess silently, so a lone password field with no evidence either
way counts as a sign-in's and asks. Only a field that names a network's
key (Wi-Fi, WLAN, wireless, WPA, WEP, SSID, hotspot, network key) is left
out, so a generated Wi-Fi password is still typed unasked. "Confirm" and
its translations join the re-authentication button words, which give the
reason the model reads. "Fill out this form with random info" on a lone
"Password" with "Submit" now asks once before typing.

Outside a form, passwordFields counts every form-less password field,
so a form-less sign-in shown beside a form-less sign-up read as a new
password and was typed unasked (also on main). The password facts now
report formless, and there a button that signs in keeps such a field a
lone one; the sign-up's confirmation stays a new password.

Tests: four new tests per build in test/credential-not-provided.mjs (the
form-less sign-in, random info through the batch, the field's frame's
site, the probe's document), the field-rule tests rewritten, and DOM
fixtures for the re-authentication, NetID, PIN and side-by-side pages
through both builds' content.js and the Chrome CDP probe. They fail on
the previous commit. Docs: security-model.md, guards.md and the registry
summary.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review of c0496edc5 found four ways a guessed sign-in password was still
typed with no card, or named by the wrong site.

The network-key exemption matched "Network password", which is how many
companies name the directory (AD or SSO) account's password, and "ssid"
inside "sessid" or "passid". A lock page's lone "Network password" with
"OK" was typed unasked; main guarded it. Only wireless words leave a field
out now (Wi-Fi, WLAN, wireless, WPA, WEP, SSID, hotspot, network key,
network security key), and SSID, WEP and WPA must not continue a longer
word, though a separator may join them (ssid_pass, router_wpa_key).

The top frame's site was the tab's address as it stands: every
about:blank popup was "about", data: pages "data", blob: pages
"blob:https:" and an unreadable tab "", so a "Type it" given on one such
page covered another site's sign-in shown the same way. The top frame now
takes its site by the frame rule: about:blank, about:srcdoc, data: and an
unreadable address have none and are refused without a card, and a blob:
page has its creator's site.

Without a frame from the preflight, the guard's probe of the field was
sent to every frame, and so was the preflight's own probe. Once an iframe
tool had injected the content script into a subframe, that frame's quick
"stale or unavailable" answered first, so the field read as unknown and
the guess was typed unasked by the write, which also goes to every frame.
The guard now asks each frame from webNavigation on its own and takes the
site from the frame that holds the field (the document that answered must
be of that frame's site); a selector naming a sign-in field in two frames
is refused without a card. The preflight's probe of a ref, selector or
index asks only the top frame, whose answer it is bound to.

A second visible password field made any form a sign-up's, before the
account field or sign-in button was read (also on main). A bank's
username, "Password" and "PIN" with "Log in", or a sign-in with an
off-screen autofill decoy, typed the guess unasked and then counted it as
a password WebBrain set, for every later sign-in in the tab, since those
were kept per tab with no site. Such a field now reads as a new password
only when the form creates an account or shows no sign-in (no account
field, no "Log in" or "Sign in" button); otherwise the lone-field rules
decide, and a sign-up whose "Email" nothing else marks, with "Submit",
asks for its "Password" while "Confirm password" stays new. A password
WebBrain set counts only on the site of its field, read before the write
goes out; the result records it as newPasswordSite for a restored
conversation, and a result without one counts nowhere.

Tests: seven new or rewritten tests per build in
test/credential-not-provided.mjs (the network words, the second password
field, the top page's site, every frame asked, the preflight's top frame,
new passwords by site, and the form-less "Next" case), and DOM fixtures
for the bank, decoy, sign-up and "Network password" pages through both
builds' content.js and the Chrome CDP probe. On c0496edc5's sources 14 of
73 unit tests and 3 of 6 DOM tests fail. Docs: security-model.md,
guards.md and the registry summary.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-ins

Review of 892338c8d found two ways a guessed sign-in password was still
typed with no card.

The site a password WebBrain set counts on was read from the preflight's
frame, and since 892338c8d the preflight probes only the top frame. A write
to a ref in a subframe arrived with no frame, so its password was kept for
the top page's site: a guess typed into a cross-site sign-up frame's
new-password field was then typed unasked into the page's own sign-in. The
site now comes from the frame that holds the field: the preflight's when it
read the field, or else the one the guard's own per-frame probe found for
this call (kept per call, so the page is asked once). The site read sends no
probe of its own, so a write whose field no frame answered for, or one the
guard did not probe (text the user gave, a replay, WebBrain's own writes),
keeps its password for no site.

autocomplete="new-password" made a field a new password before its form
was read (also on main). Many sign-in forms set it on their password input
to stop the browser's autofill, so a guess there was typed unasked and then
counted as a password WebBrain set for the site's next sign-in. A form's
only password field marked new-password is now a sign-in's when its submit
control says "Log in" or "Sign in" (or a translation), or another of its
buttons does while no control creates an account. A field whose own name
or label says new, and one beside another password field, stay new.

Tests: two new tests per build in test/credential-not-provided.mjs (a
cross-site sign-up frame, then the page's and the frame's sign-ins, through
the tool batch; the new-password rule and the review's sequence through
executeTool), and a DOM fixture for an anti-autofill login and a
new-password sign-up through both builds' content.js and the Chrome CDP
probe. On 892338c8d's sources 4 of 77 unit tests and 3 of 6 DOM tests fail.
Docs: security-model.md, guards.md and the registry summary.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…another

Review of 1b0a9f1b0 found that the guard checked the field the
typing-target preflight resolved, and the dispatch could then type into
another one (also on main). By default (typing-target-identity advises),
Chrome's focused type_text whose bound field lost focus types into the
field focused now, and a type_text selector the preflight did not pin
types into the selector's current match. Firefox binds such a selector's
current match, or sends the write unbound to every frame. A two-step
sign-in that focuses its password field after the username step's Next
took the guess there with no card.

Where the dispatch types into a field other than the one checked, it now
runs the guard against that field first (_retargetedCredentialBlock): the
element it pinned or bound, as its probe read it, or else the guard's own
probe of the field focused now or of the selector's match (in the top
frame for Chrome, where it types; in each frame for Firefox's unbound
write). Strict checks still refuse first, and the typing-target-identity
note is left only when the text goes through. An approved password does
not follow focus to another field (credential_field_changed).

Such a refusal comes from inside the dispatch. It is returned without the
call's result finalization, as the guard's own is: after a "Type it" the
batch runs the call again with the same dispatch state, and the finalized
first refusal would have stayed the result of the rerun that typed.

Tests: in test/credential-not-provided.mjs, three Chrome tests (focus
moved to a sign-in password field, through executeTool and through the
tool batch with the real dispatch, "Don't type it" and "Type it"; a
selector without a pin) and one Firefox test (a selector without a
binding, matching in a frame from another site). On 1b0a9f1b0's sources
4 of 81 fail. Docs: security-model.md and guards.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review of 8e6e6e5cc found that Firefox typed a guess into a sign-in
password field with no card (on main too). The content `type` handler
types a type_text with no selector into the field the last click focused
when focus has since gone to the body or to that field's shadow host
(typeText's recent-target fallback, kept for 30 s). The guard's probe of
the focused field (field_value_digest {focused: true}) read only the
active element, so it found no field in any frame, and a target that
cannot be read is typed as before. A virtual-keypad sign-in that blurs
its password field on focus took the guess after click({selector}).

The guard's probe (_credentialTargetFieldMeta, both builds) now asks for
the field a selectorless `type` writes to (`typeTarget: true`). Firefox's
field_value_digest answers it with the same fallbacks as typeText: the
recent editable target when focus is on the body, or on that target's
shadow host. Other callers of the focused digest are unchanged, and
Chrome's content script, whose typing has no fallback, ignores the flag.

Tests: in test/credential-not-provided-dom.mjs, a Firefox test runs the
real content.js and agent.js on a keypad sign-in, for focus lost to the
body and to a component: type_text({text}) returns the card's question
and sends no `type`, and a selectorless `type` then writes to that
field. A Chrome test shows its `type` types nothing there. Both Firefox
cases fail on 8e6e6e5cc's sources. The focused-probe assertion in
test/credential-not-provided.mjs checks the flag. Docs: guards.md and
security-model.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Also name the field's own component in the two docs that describe
Firefox's selectorless type_text fallback, as the last review noted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Ask before typing a sign-in password the user never gave, and count saved logins
@vercel

vercel Bot commented Nov 8, 2026

Copy link
Copy Markdown

@esokullu is attempting to deploy a commit to the esokullu's projects Team on Vercel.

A member of the Team first needs to authorize it.

@esokullu
esokullu merged commit b9e90f0 into webbrain-one:main Nov 8, 2026
1 of 3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant