diff --git a/container/agent-runner/src/poll-loop.ts b/container/agent-runner/src/poll-loop.ts index 43c9cf1..fb54378 100644 --- a/container/agent-runner/src/poll-loop.ts +++ b/container/agent-runner/src/poll-loop.ts @@ -21,10 +21,15 @@ 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."; +// Generic fallback for providers that classify auth failures via +// `isAuthRequired` but don't supply their own remediation text. Concrete +// providers (Claude, Codex, …) override this with a provider-specific +// message via `authRequiredMessage()`. +const GENERIC_AUTH_REQUIRED_MESSAGE = + "I can't reach my credentials right now. The operator running NanoClaw needs to re-authenticate on the host machine."; -function writeAuthRequiredMessage(routing: RoutingContext): void { +function writeAuthRequiredMessage(provider: AgentProvider, routing: RoutingContext): void { + const text = provider.authRequiredMessage?.() ?? GENERIC_AUTH_REQUIRED_MESSAGE; log('Auth-required detected — substituting host-aware message for the user'); writeMessageOut({ id: generateId(), @@ -32,7 +37,7 @@ function writeAuthRequiredMessage(routing: RoutingContext): void { platform_id: routing.platformId, channel_type: routing.channelType, thread_id: routing.threadId, - content: JSON.stringify({ text: AUTH_REQUIRED_USER_TEXT }), + content: JSON.stringify({ text }), }); } @@ -205,7 +210,7 @@ export async function runPollLoop(config: PollLoopConfig): Promise { } if (config.provider.isAuthRequired?.(errMsg)) { - writeAuthRequiredMessage(routing); + writeAuthRequiredMessage(config.provider, routing); } else { writeMessageOut({ id: generateId(), @@ -330,7 +335,7 @@ async function processQuery( markCompleted(initialBatchIds); if (event.text) { if (provider.isAuthRequired?.(event.text)) { - writeAuthRequiredMessage(routing); + writeAuthRequiredMessage(provider, routing); } else { dispatchResultText(event.text, routing); } diff --git a/container/agent-runner/src/providers/claude.test.ts b/container/agent-runner/src/providers/claude.test.ts index d906280..91836e4 100644 --- a/container/agent-runner/src/providers/claude.test.ts +++ b/container/agent-runner/src/providers/claude.test.ts @@ -35,3 +35,13 @@ describe('ClaudeProvider.isAuthRequired', () => { expect(provider.isAuthRequired('Tool execution failed: timeout')).toBe(false); }); }); + +describe('ClaudeProvider.authRequiredMessage', () => { + const provider = new ClaudeProvider(); + + it('returns the Anthropic-specific remediation', () => { + const msg = provider.authRequiredMessage(); + expect(msg).toContain('Anthropic credentials'); + expect(msg).toContain('claude'); + }); +}); diff --git a/container/agent-runner/src/providers/claude.ts b/container/agent-runner/src/providers/claude.ts index 11ea4b0..89dd5cd 100644 --- a/container/agent-runner/src/providers/claude.ts +++ b/container/agent-runner/src/providers/claude.ts @@ -275,6 +275,10 @@ export class ClaudeProvider implements AgentProvider { return AUTH_REQUIRED_RE.test(text); } + authRequiredMessage(): string { + return "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."; + } + query(input: QueryInput): AgentQuery { const stream = new MessageStream(); stream.push(input.prompt); diff --git a/container/agent-runner/src/providers/types.ts b/container/agent-runner/src/providers/types.ts index 99833a7..3124c07 100644 --- a/container/agent-runner/src/providers/types.ts +++ b/container/agent-runner/src/providers/types.ts @@ -17,11 +17,24 @@ export interface AgentProvider { /** * 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 + * usable credentials (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. + * since the user can't authenticate from chat. + * + * Paired with `authRequiredMessage()` — providers that implement one + * should implement both. The matcher is provider-specific because each + * SDK/CLI has its own auth-failure banner format. */ isAuthRequired?(text: string): boolean; + + /** + * User-facing remediation message returned when `isAuthRequired` matches. + * Provider-specific because the actionable instruction differs across + * providers (e.g. Claude vs Codex vs OpenCode each direct the operator + * to a different auth flow). Falls back to a generic message in the + * poll-loop if a provider implements `isAuthRequired` but not this. + */ + authRequiredMessage?(): string; } /**