fix(credentials): address review feedback

- wakeContainer now never throws — returns Promise<boolean>, catches
  internally. Closes the regression risk for the 5 awaited callers in
  agent-to-agent, interactive, and approvals/response-handler that the
  previous version left unwrapped. Router uses the boolean to stop the
  typing indicator on transient failure; host-sweep just awaits.
- Tighten AUTH_REQUIRED_RE: anchor to start-of-string with the specific
  `·` (U+00B7) separator the CLI uses, so an agent that quotes the
  banner mid-sentence in a normal reply doesn't trip the classifier.
- Log a one-line note from writeAuthRequiredMessage so substitutions
  are visible when debugging "user got the credentials message but I
  don't see why."
- Add unit tests for ClaudeProvider.isAuthRequired covering both banner
  variants, trailing content, mid-sentence quoting, leading-prose
  quoting, alternate separators, and unrelated text.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gavrielc
2026-04-29 17:51:32 +03:00
parent 5f34e26240
commit d5b48e4742
6 changed files with 70 additions and 25 deletions

View File

@@ -25,6 +25,7 @@ const AUTH_REQUIRED_USER_TEXT =
"I can't reach my Anthropic credentials right now. The operator running NanoClaw needs to re-run setup, or run `claude` in the project directory on the machine I'm running on.";
function writeAuthRequiredMessage(routing: RoutingContext): void {
log('Auth-required detected — substituting host-aware message for the user');
writeMessageOut({
id: generateId(),
kind: 'chat',

View File

@@ -0,0 +1,37 @@
import { describe, it, expect } from 'bun:test';
import { ClaudeProvider } from './claude.js';
describe('ClaudeProvider.isAuthRequired', () => {
const provider = new ClaudeProvider();
it('matches the "Not logged in" banner', () => {
expect(provider.isAuthRequired('Not logged in · Please run /login')).toBe(true);
});
it('matches the "Invalid API key" banner', () => {
expect(provider.isAuthRequired('Invalid API key · Please run /login')).toBe(true);
});
it('matches with trailing content after the banner', () => {
expect(provider.isAuthRequired('Not logged in · Please run /login\n\nstack trace …')).toBe(true);
});
it('does not match when the agent quotes the phrase mid-sentence', () => {
const quoted = "The error 'Invalid API key · Please run /login' means your auth has expired.";
expect(provider.isAuthRequired(quoted)).toBe(false);
});
it('does not match when the agent leads its reply with the phrase in prose', () => {
const prose = '"Not logged in · Please run /login" is a Claude Code error.';
expect(provider.isAuthRequired(prose)).toBe(false);
});
it('does not match a different separator (defensive against typos in agent output)', () => {
expect(provider.isAuthRequired('Not logged in - Please run /login')).toBe(false);
});
it('does not match unrelated text', () => {
expect(provider.isAuthRequired('Tool execution failed: timeout')).toBe(false);
});
});

View File

@@ -237,12 +237,16 @@ const CLAUDE_CODE_AUTO_COMPACT_WINDOW = '165000';
const STALE_SESSION_RE = /no conversation found|ENOENT.*\.jsonl|session.*not found/i;
/**
* Auth-required detection. Matches Claude Code's output when no usable
* Auth-required detection. Matches Claude Code's banner when no usable
* credential is available — "Not logged in · Please run /login" or
* "Invalid API key · Please run /login". The user can't run /login from
* chat, so the poll-loop substitutes a host-aware message.
*
* Anchored to start-of-string with the specific `·` separator (U+00B7)
* the CLI uses, so an agent that quotes the phrase verbatim mid-sentence
* in a normal reply doesn't trip the classifier.
*/
const AUTH_REQUIRED_RE = /(Not logged in|Invalid API key)[\s\S]*?Please run \/login/i;
const AUTH_REQUIRED_RE = /^(Not logged in|Invalid API key)\s*·\s*Please run \/login/;
export class ClaudeProvider implements AgentProvider {
readonly supportsNativeSlashCommands = true;