fix(runtime): stop formatted source code reading as a credential prompt

Mining 16,206 distinct credential-vocabulary lines out of this repo's own tracked
files and placing each one row above a real agent composer caret produced **100
refusals across 25 distinct lines**, on Codex, Claude Code and OpenCode. The
corpus had `rg` hits and diffs, which carry their own chrome, but no plain
formatted source -- and that is the category that leaked.

24 of the 25 are one shape. `AUTH_QUESTION_ROW_RE` rests on "prose never starts
with a bare `?`", which is true and irrelevant: oxfmt puts a ternary's consequent
on its own row as `? someValue`, and **5,666 tracked files have one**.
`sawAuthQuestion` then returns unconditionally, with no position requirement, so a
composer caret directly below could not clear it. Nine matched only because
`\b(?:log[ -]?in)\b` reads `user.login` as the auth verb.

The 25th is the other rule: `CREDENTIAL_ASK_PREFIX_RE` was unanchored, so an ask
verb anywhere in a row made a row-final credential noun a prompt -- `// Why: a
merely missing or expired bundle must not enter the credential` is a real wrapped
comment in this repo, and terminal wrapping makes ending on a noun routine.

Three fixes, the same position discipline the flow rule already uses:

- the ask verb must LEAD its row, modulo decoration and a menu number, so
  `2. Paste an API key` still reads as a prompt and 60 characters of prose before
  `enter the` does not;
- `.login` is a property access, not an auth verb;
- a known composer caret on the bottom row suppresses the question-row rule,
  because an agent sitting at its own prompt is not asking anyone anything.

The caret set deliberately includes a bare `>`. It suppresses only the
question-row rule, and the #19749 sign-in dialog -- whose own bottom row is `>` --
matches through `isCredentialPromptLine`, which returns first. Verified: that
dialog and the auth-method menu both still refuse.

Mined-corpus refusals 100 -> 0, with the mined lines added as a corpus category so
the next widening has to refuse them. Sentinel budget unchanged.
This commit is contained in:
Neil
2026-09-11 02:26:26 -07:00
parent 4348824ccf
commit 4cf09424c5
2 changed files with 136 additions and 5 deletions
@@ -500,6 +500,115 @@ const LEGITIMATE_AGENT_SCREENS: readonly (readonly [string, string[]])[] = [
[
'dotenv example printed by the agent',
['$ cat .env.example', 'DATABASE_URL=', 'API_KEY=', 'SESSION_SECRET=']
],
// Plain source code printed into the pane -- the category this corpus lacked. It had `rg` hits
// and diffs, which carry their own chrome, but not bare formatted source. Every line below was
// mined from this repo's tracked files and refused a prompt above a real composer caret: oxfmt
// renders a ternary consequent as a bare `?` row (5,666 tracked files have one), `.login` reads
// as the auth verb "log in", and prose that merely ENDS on a credential noun read as an ask.
[
'ternary consequent with a sign-in string',
[
" ? 'Update desktop Orca and sign in to connect from anywhere'",
'› Ask Codex to do anything'
]
],
[
'ternary consequent with an authentication template literal',
[' ? `replacement session authentication timed out (${stage})`', '> ']
],
[
'ternary consequent with a login error',
[' ? `Codex login failed: ${trimmedOutput}`', '❯ ']
],
[
'ternary consequent calling a login spawn builder',
[
" ? buildWindowsHostInteractiveLoginSpawn(codexCommand, ['login'])",
'› Ask Codex to do anything'
]
],
[
'ternary consequent with a sign-in status string',
[" ? 'Timed out while checking Codex sign-in status'", '> ']
],
[
'ternary consequent reading an authorization basis',
[' ? this.originPool.controlForBasis(authorization.basisConnId)', '❯ ']
],
[
'ternary consequent building a gh auth command',
[' ? `gh auth login --hostname ${host}`', '› Ask Codex to do anything']
],
[
'ternary consequent with a translated sign-in-again label',
[" ? translate('settings.signInAgain', 'Sign in again')", '❯ ']
],
[
'ternary consequent with a pat docs url',
[
" ? 'https://learn.microsoft.com/azure/devops/accounts/use-pat-to-authenticate'",
'› Ask Codex to do anything'
]
],
[
'ternary filtering assignees by login',
[' ? prevAssignees.filter((l) => l !== login)', '› Ask Codex to do anything']
],
[
'ternary narrowing a login to a string',
[" ? overrides.filter((login): login is string => typeof login === 'string')", '> ']
],
[
'ternary removing assignees by login',
[' ? { removeAssignees: [user.login] }', '❯ ']
],
[
'ternary mapping project assignees',
[
' ? projectRowDetail.assignees.map((login) => ({',
'› Ask Codex to do anything'
]
],
[
'ternary removing a reviewer by login',
[' ? handleRemoveReviewers([reviewer.login])', '> ']
],
[
'ternary editing assignees by login',
[' ? onEditAssignees?.([], [user.login])', '❯ ']
],
[
'ternary lowercasing an assignee login',
[
' ? prevAssignees.filter((user) => user.login.toLowerCase() !== lowerLogin)',
'› Ask Codex to do anything'
]
],
[
'ternary building an assignee removal patch',
[" ? { family: 'assignees', kind: 'remove', logins: [login] }", '❯ ']
],
[
'wrapped comment ending on a credential noun',
[' // Why: a merely missing or expired bundle must not enter the credential', '❯ ']
],
[
'wrapped comment ending on a password noun',
[' // The caller must provide the current password', '› Ask Codex to do anything']
],
[
'wrapped jsdoc ending on an api key noun',
[' * Callers are expected to paste their API key', '> ']
],
// No caret, and the bottom row DOES lead with an action phrase, so the only thing keeping this
// from reading as a live prompt is that `candidate.login` is a property access.
[
'source rows where a .login access is the only would-be auth verb',
[
' const owner = candidate.login',
' // Enter the code below to finish linking the account'
]
]
]
@@ -48,8 +48,14 @@ const CREDENTIAL_NOUN_ANYWHERE_RE = new RegExp(CREDENTIAL_NOUN_SOURCE, 'i')
// Why a vendor slot: real prompts read "enter your Anthropic API key" and
// "paste your personal access token", not just "enter your API key".
//
// Why anchored: unanchored, the ask verb could sit anywhere, so any prose that happened to end on
// a credential noun read as a prompt — `// Why: a merely missing or expired bundle must not enter
// the credential` is a real wrapped comment in this repo, and terminal wrapping makes ending on a
// noun routine. A dialog addresses the user, so its ask verb leads the row, modulo decoration and
// a menu number (`2. Paste an API key`).
const CREDENTIAL_ASK_PREFIX_RE =
/(?:^|[^a-z])(?:enter|re-?enter|type|paste|input|provide|confirm)(?:\s+(?:your|the|a|an|my|new|current|old))?(?:\s+[a-z][a-z0-9.'-]{0,20}){0,2}[\s_]+$/i
/^[^a-z0-9]{0,8}(?:\d{1,2}[.)]\s*)?(?:please\s+)?(?:enter|re-?enter|type|paste|input|provide|confirm)(?:\s+(?:your|the|a|an|my|new|current|old))?(?:\s+[a-z][a-z0-9.'-]{0,20}){0,2}[\s_]+$/i
// A bare label prompt: decoration, an optional qualifier, an optional env-var vendor segment
// (`ANTHROPIC_API_KEY:`), then the noun.
@@ -72,8 +78,11 @@ const CLAUSE_TERMINATED_RE = /[:›❯»>_]\s*$/
const SUDO_PASSWORD_RE = /^[^a-z0-9]{0,8}\[sudo\]\s/i
const GIT_CREDENTIAL_RE = /^[^a-z0-9]{0,8}(?:username|password) for ['"]?[a-z][a-z0-9+.-]*:\/\//i
// Why the lookbehind: `user.login`, `candidate.login` and `overrides.filter((login) =>` are
// property accesses, not auth wording, and `\b` treats `.` as a boundary. Agents print this
// repo's own source constantly.
const AUTH_VERB_RE =
/\b(?:sign[ -]?in|signin|log[ -]?in|authenticate|authorized?|authorization|authentication)\b/i
/(?<![.\w])(?:sign[ -]?in|signin|log[ -]?in|authenticate|authorized?|authorization|authentication)\b/i
// Wording only a dialog addressing the user uses.
// Why `waiting for you to` carries an auth continuation: bare "waiting for you to …" is how
@@ -104,10 +113,22 @@ const AUTH_ACTION_FLOW_LEADS_ROW_RE = new RegExp(
'i'
)
// An inquirer-style question row. Prose never starts with a bare `?`, so a `?`
// row asking about auth is a dialog header even when its options wrap below it.
// An inquirer-style question row. Prose never starts with a bare `?` — but FORMATTED CODE does:
// oxfmt puts a ternary's consequent on its own row as `? someValue`, and 5,666 tracked files in
// this repo have one. So the row rule alone is not enough; see `COMPOSER_CARET_ROW_RE`.
const AUTH_QUESTION_ROW_RE = /^\?\s+\S/
/**
* A row that proves the agent is sitting at its own composer, so nothing is asking the user
* anything and a `?` row above it is output, not a dialog header.
*
* Why a bare `>` is safe to include here: this suppresses only the question-row rule, which is
* the one rule with no position requirement. A sign-in dialog drawn OVER ready chrome — the
* #19749 shape, whose own bottom row is `>` — matches through `isCredentialPromptLine` instead,
* and that returns before this is consulted.
*/
const COMPOSER_CARET_ROW_RE = /^(?:›\s*ask\b.*|✳\s*claude code\b.*|[>❯›◇»$])$/i
// A finished sentence, i.e. narration. A lone `.`/`!`/`?` ends a clause; `...`
// and `…` are progress wording ("opening browser...") and are not sentences.
// Why CJK punctuation counts: an agent narrating auth work in Chinese or Japanese writes
@@ -216,5 +237,6 @@ export function findCredentialPromptIndex(normalized: string): number | null {
bottomRowAsks &&
!NARRATION_ROW_RE.test(bottomRow) &&
(sawAuthVerb || sawCredentialNoun)
return authFlowOwnsBottom || sawAuthQuestion ? windowStart : null
const bottomRowIsComposerCaret = COMPOSER_CARET_ROW_RE.test(bottomRow)
return authFlowOwnsBottom || (sawAuthQuestion && !bottomRowIsComposerCaret) ? windowStart : null
}