fix(credentials): translate auth errors and require OneCLI for spawn

Two related fixes for the case where credentials aren't usable:

1. Replace Claude Code's "Not logged in / Invalid API key · Please run
   /login" output with a host-aware message. The user can't run /login
   from chat, so the raw text is unhelpful. Provider gains an optional
   isAuthRequired() classifier; the poll-loop substitutes the message
   on both result-text and error paths.

2. Treat OneCLI gateway failure as a transient hard error instead of
   spawning a credential-less container. The catch in container-runner
   now propagates; router and host-sweep wrap wakeContainer to log and
   leave the inbound row pending so the next 60s sweep tick retries.
   Router also stops the typing indicator on failure.

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

View File

@@ -21,6 +21,20 @@ function generateId(): string {
return `msg-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
}
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 {
writeMessageOut({
id: generateId(),
kind: 'chat',
platform_id: routing.platformId,
channel_type: routing.channelType,
thread_id: routing.threadId,
content: JSON.stringify({ text: AUTH_REQUIRED_USER_TEXT }),
});
}
export interface PollLoopConfig {
provider: AgentProvider;
/**
@@ -171,7 +185,7 @@ export async function runPollLoop(config: PollLoopConfig): Promise<void> {
const skippedSet = new Set(skipped);
const processingIds = ids.filter((id) => !commandIds.includes(id) && !skippedSet.has(id));
try {
const result = await processQuery(query, routing, processingIds, config.providerName);
const result = await processQuery(query, routing, processingIds, config.provider, config.providerName);
if (result.continuation && result.continuation !== continuation) {
continuation = result.continuation;
setContinuation(config.providerName, continuation);
@@ -189,15 +203,18 @@ export async function runPollLoop(config: PollLoopConfig): Promise<void> {
clearContinuation(config.providerName);
}
// Write error response so the user knows something went wrong
writeMessageOut({
id: generateId(),
kind: 'chat',
platform_id: routing.platformId,
channel_type: routing.channelType,
thread_id: routing.threadId,
content: JSON.stringify({ text: `Error: ${errMsg}` }),
});
if (config.provider.isAuthRequired?.(errMsg)) {
writeAuthRequiredMessage(routing);
} else {
writeMessageOut({
id: generateId(),
kind: 'chat',
platform_id: routing.platformId,
channel_type: routing.channelType,
thread_id: routing.threadId,
content: JSON.stringify({ text: `Error: ${errMsg}` }),
});
}
}
// Ensure completed even if processQuery ended without a result event
@@ -249,6 +266,7 @@ async function processQuery(
query: AgentQuery,
routing: RoutingContext,
initialBatchIds: string[],
provider: AgentProvider,
providerName: string,
): Promise<QueryResult> {
let queryContinuation: string | undefined;
@@ -310,7 +328,11 @@ async function processQuery(
// at all — either way the turn is finished.
markCompleted(initialBatchIds);
if (event.text) {
dispatchResultText(event.text, routing);
if (provider.isAuthRequired?.(event.text)) {
writeAuthRequiredMessage(routing);
} else {
dispatchResultText(event.text, routing);
}
}
}
}

View File

@@ -236,6 +236,14 @@ 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
* 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.
*/
const AUTH_REQUIRED_RE = /(Not logged in|Invalid API key)[\s\S]*?Please run \/login/i;
export class ClaudeProvider implements AgentProvider {
readonly supportsNativeSlashCommands = true;
@@ -259,6 +267,10 @@ export class ClaudeProvider implements AgentProvider {
return STALE_SESSION_RE.test(msg);
}
isAuthRequired(text: string): boolean {
return AUTH_REQUIRED_RE.test(text);
}
query(input: QueryInput): AgentQuery {
const stream = new MessageStream();
stream.push(input.prompt);

View File

@@ -14,6 +14,14 @@ export interface AgentProvider {
* (missing transcript, unknown session, etc.) and should be cleared.
*/
isSessionInvalid(err: unknown): boolean;
/**
* True if the given text/error indicates the underlying SDK or CLI has no
* usable Anthropic auth (e.g. Claude Code's "Not logged in · Please run
* /login"). The poll-loop swaps the raw output for a host-aware message
* since the user can't run /login from chat.
*/
isAuthRequired?(text: string): boolean;
}
/**