fix(credentials): move auth-required remediation message into provider
Adds a paired `authRequiredMessage()` method to AgentProvider so per-provider auth-failure remediation can differ. Claude returns the Anthropic/`claude` instruction; future providers (Codex, OpenCode, …) can return their own remediation text. The poll-loop calls `provider.authRequiredMessage?.()` and falls back to a generic message if a provider implements `isAuthRequired` without supplying its own remediation. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -21,10 +21,15 @@ function generateId(): string {
|
|||||||
return `msg-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
return `msg-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const AUTH_REQUIRED_USER_TEXT =
|
// Generic fallback for providers that classify auth failures via
|
||||||
"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.";
|
// `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');
|
log('Auth-required detected — substituting host-aware message for the user');
|
||||||
writeMessageOut({
|
writeMessageOut({
|
||||||
id: generateId(),
|
id: generateId(),
|
||||||
@@ -32,7 +37,7 @@ function writeAuthRequiredMessage(routing: RoutingContext): void {
|
|||||||
platform_id: routing.platformId,
|
platform_id: routing.platformId,
|
||||||
channel_type: routing.channelType,
|
channel_type: routing.channelType,
|
||||||
thread_id: routing.threadId,
|
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<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (config.provider.isAuthRequired?.(errMsg)) {
|
if (config.provider.isAuthRequired?.(errMsg)) {
|
||||||
writeAuthRequiredMessage(routing);
|
writeAuthRequiredMessage(config.provider, routing);
|
||||||
} else {
|
} else {
|
||||||
writeMessageOut({
|
writeMessageOut({
|
||||||
id: generateId(),
|
id: generateId(),
|
||||||
@@ -330,7 +335,7 @@ async function processQuery(
|
|||||||
markCompleted(initialBatchIds);
|
markCompleted(initialBatchIds);
|
||||||
if (event.text) {
|
if (event.text) {
|
||||||
if (provider.isAuthRequired?.(event.text)) {
|
if (provider.isAuthRequired?.(event.text)) {
|
||||||
writeAuthRequiredMessage(routing);
|
writeAuthRequiredMessage(provider, routing);
|
||||||
} else {
|
} else {
|
||||||
dispatchResultText(event.text, routing);
|
dispatchResultText(event.text, routing);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,3 +35,13 @@ describe('ClaudeProvider.isAuthRequired', () => {
|
|||||||
expect(provider.isAuthRequired('Tool execution failed: timeout')).toBe(false);
|
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');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -275,6 +275,10 @@ export class ClaudeProvider implements AgentProvider {
|
|||||||
return AUTH_REQUIRED_RE.test(text);
|
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 {
|
query(input: QueryInput): AgentQuery {
|
||||||
const stream = new MessageStream();
|
const stream = new MessageStream();
|
||||||
stream.push(input.prompt);
|
stream.push(input.prompt);
|
||||||
|
|||||||
@@ -17,11 +17,24 @@ export interface AgentProvider {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* True if the given text/error indicates the underlying SDK or CLI has no
|
* 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
|
* /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;
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user