mirror of
https://github.com/stablyai/orca.git
synced 2026-10-06 16:02:25 +00:00
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:
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user