Skip to content

Never guess credentials — ask the user, or read them from knowledge - #128

Open
DavertMik wants to merge 3 commits into
mainfrom
navigator-never-guesses-credentials
Open

Never guess credentials — ask the user, or read them from knowledge#128
DavertMik wants to merge 3 commits into
mainfrom
navigator-never-guesses-credentials

Conversation

@DavertMik

Copy link
Copy Markdown
Contributor

What happened

A page that redirected to a login form was treated as a form to fill. The model invented an email and a password, submitted them, and spent the rest of its attempts on a login that was never going to succeed:

AI Navigator resolving state at /users/sign_in?info=You+must+be+logged+in...
  > I.fillField({"role":"textbox","text":"name@email.com"}, "Enrique.Schinner@gmail.com")
  > I.fillField({"role":"textbox","text":"********"}, "RV6kXHNcheHiUIz")
  > I.click({"role":"button","text":"Sign In"})
URL verification failed: expected /projects/Testomat/defects, got /users/sign_in

The system prompt already said to ask the user for credentials — but resolveState exposed only stop. The instruction had no mechanism behind it, so guessing was the only move on the table.

Changes

Credentials have two sources, and neither is invention. The <credentials> block in the Navigator prompt rules out made-up values for anything that identifies an account. A scenario that explicitly creates something, such as registering a new user, still makes up the values it needs.

The Navigator can ask. askUser is now among the tools in resolveState, so credentials are requested at the moment they are needed and the run carries on with the answer. Where nobody can answer, it stops and names what is missing — the failure path already points at explorbot learn "<path>" for recording them once.

The loop survives an ask-only turn. A turn that called a tool and returned no code used to end the loop, so the user's answer arrived after the Navigator had given up and was discarded (src/ai/navigator.ts, one line).

An answer can become knowledge. askUser takes persistent: a lasting fact about the page is appended to that page's knowledge file, and later runs read it instead of asking again. It is filed under the page path with the query string dropped — normalizeUrl only trims, and matchesUrl keeps a query in the compared path whenever the pattern has one, so filing under /users/sign_in?info=You+must+be+logged+in... would have written a file no later visit could match.

The tool is offered only where somebody can answer it. The Pilot's inline copy became the shared createAskUserTool, and both call sites gate on isInteractive() rather than handing the model a tool whose only possible reply was "user input not available".

Tests

Two paths that could silently not work are covered: the ask → fill round trip in navigator-resolve-state.test.ts (verified to fail without the loop fix — the first version of it passed for the wrong reason, via the end-of-loop fallback), and persistence in tools.test.ts, where the fake state carries a query string so the path stripping stays locked in.

Unit 1026 pass / 3 fail, integration 80 pass / 0 fail. The 3 failures are pre-existing and untracked on mainnavigator-user-redirect.test.ts and execution-controller-resume.test.ts test a <user_redirect> feature that does not exist in navigator.ts; confirmed by stashing this branch's changes.

Known, not fixed

  • Answering the same persistent question in a later run appends a second block under a --- separator. addKnowledge has always appended rather than deduped.
  • askUser answers are not passed to registerSecret, so a password typed at the prompt is written verbatim into the experience file when the successful login flow is recorded.
  • LearnCommand passes state.url to addKnowledge unstripped and has the same query-string issue; left alone as out of scope.

🤖 Generated with Claude Code

A redirect to a login form was being treated as a form to fill: the model
invented an email and a password and spent its remaining attempts on a login
that could not succeed. The system prompt already told it to ask instead, but
resolveState exposed no way to ask, so guessing was the only move available.

The Navigator now gets askUser, and the resolution loop survives a turn that
only asks — previously an answer arrived after the loop had already given up
and discarded it.

askUser also takes `persistent`: a lasting fact about the page (how to
authorize, what its data means) is appended to the knowledge of that page, so
later runs read it instead of asking again. It is filed under the page path
with the query string dropped, so it still matches when the redirect carries a
different message.

The tool is only offered where somebody can answer it; the Pilot's inline copy
is now the same shared one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Comment thread src/ai/tools.ts Outdated
const path = extractStatePath(state.url || state.fullUrl || '/')
.split('?')[0]
.split('#')[0];
const { filePath } = stateManager.getKnowledgeTracker().addKnowledge(path, `${question}\n\n${userInput}`);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could silently save login credentials to knowledge/*.md. The model decides whether persistent is true, while the user isn’t told that their answer will be written to a file. I’d either ask for explicit permission before saving it or avoid automatically persisting credential-related answers

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, we already have --session so we can avoid persistent storage

DavertMik and others added 2 commits August 22, 2026 03:15
askUser wrote knowledge on its own: a `persistent` flag appended the question and the
answer to the knowledge file of the page, decided by the model mid-run, with nobody to
check whether the answer had been any use.

The tool now writes nothing. It records the answer against the page it was asked on and
starts capturing the log, so what the answer produced is on record. Once the work that
needed it has finished — a test, or the command that was running — the Captain reads that
log and judges one thing: was the answer worth more than this run? When it was, the
question and the answer are kept verbatim in the experience of that page, under a how-to
heading the Captain supplies; the recipe itself is already written by the Navigator, so
nothing is retyped by a model. Nothing is written when the log does not show the answer
being used, when what followed it failed, or when the page already carries the fact.

Experience is filed per page state, but a test only ever saw the experience of the page it
started on, and the Pilot reviewing a new page had neither its table of contents nor
learnExperience. Both now arrive with the page — without which an answer saved for a login
page would never reach the run that lands there.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…es-credentials

# Conflicts:
#	CHANGELOG.md
#	tests/unit/tools.test.ts
@DenysKuchma

Copy link
Copy Markdown
Collaborator

Credentials are still persisted, just in experience now. Prompt answers aren't registered as secrets, so redactSecrets() won't hide passwords. Credential answers should stay session-only or be redacted before writeFlow()

@DavertMik DavertMik added the regression Run the LLM regression suite (one run per label add) label Aug 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

regression Run the LLM regression suite (one run per label add)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants