revert(credentials): drop auth-required login-message handling
Removing the "Not logged in · Please run /login" detection and substitution from this PR — narrowing scope to just the OneCLI gateway transient-retry change. The login-message handling will be addressed separately. Reverts: - AgentProvider.isAuthRequired / authRequiredMessage - ClaudeProvider auth-required regex, classifier, and remediation text - poll-loop writeAuthRequiredMessage helper + call sites - claude.test.ts (auth-only test file) OneCLI/wakeContainer changes (the remaining content of the PR) are unaffected. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -21,26 +21,6 @@ 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)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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(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(),
|
|
||||||
kind: 'chat',
|
|
||||||
platform_id: routing.platformId,
|
|
||||||
channel_type: routing.channelType,
|
|
||||||
thread_id: routing.threadId,
|
|
||||||
content: JSON.stringify({ text }),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PollLoopConfig {
|
export interface PollLoopConfig {
|
||||||
provider: AgentProvider;
|
provider: AgentProvider;
|
||||||
/**
|
/**
|
||||||
@@ -191,7 +171,7 @@ export async function runPollLoop(config: PollLoopConfig): Promise<void> {
|
|||||||
const skippedSet = new Set(skipped);
|
const skippedSet = new Set(skipped);
|
||||||
const processingIds = ids.filter((id) => !commandIds.includes(id) && !skippedSet.has(id));
|
const processingIds = ids.filter((id) => !commandIds.includes(id) && !skippedSet.has(id));
|
||||||
try {
|
try {
|
||||||
const result = await processQuery(query, routing, processingIds, config.provider, config.providerName);
|
const result = await processQuery(query, routing, processingIds, config.providerName);
|
||||||
if (result.continuation && result.continuation !== continuation) {
|
if (result.continuation && result.continuation !== continuation) {
|
||||||
continuation = result.continuation;
|
continuation = result.continuation;
|
||||||
setContinuation(config.providerName, continuation);
|
setContinuation(config.providerName, continuation);
|
||||||
@@ -209,18 +189,15 @@ export async function runPollLoop(config: PollLoopConfig): Promise<void> {
|
|||||||
clearContinuation(config.providerName);
|
clearContinuation(config.providerName);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.provider.isAuthRequired?.(errMsg)) {
|
// Write error response so the user knows something went wrong
|
||||||
writeAuthRequiredMessage(config.provider, routing);
|
writeMessageOut({
|
||||||
} else {
|
id: generateId(),
|
||||||
writeMessageOut({
|
kind: 'chat',
|
||||||
id: generateId(),
|
platform_id: routing.platformId,
|
||||||
kind: 'chat',
|
channel_type: routing.channelType,
|
||||||
platform_id: routing.platformId,
|
thread_id: routing.threadId,
|
||||||
channel_type: routing.channelType,
|
content: JSON.stringify({ text: `Error: ${errMsg}` }),
|
||||||
thread_id: routing.threadId,
|
});
|
||||||
content: JSON.stringify({ text: `Error: ${errMsg}` }),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure completed even if processQuery ended without a result event
|
// Ensure completed even if processQuery ended without a result event
|
||||||
@@ -272,7 +249,6 @@ async function processQuery(
|
|||||||
query: AgentQuery,
|
query: AgentQuery,
|
||||||
routing: RoutingContext,
|
routing: RoutingContext,
|
||||||
initialBatchIds: string[],
|
initialBatchIds: string[],
|
||||||
provider: AgentProvider,
|
|
||||||
providerName: string,
|
providerName: string,
|
||||||
): Promise<QueryResult> {
|
): Promise<QueryResult> {
|
||||||
let queryContinuation: string | undefined;
|
let queryContinuation: string | undefined;
|
||||||
@@ -334,11 +310,7 @@ async function processQuery(
|
|||||||
// at all — either way the turn is finished.
|
// at all — either way the turn is finished.
|
||||||
markCompleted(initialBatchIds);
|
markCompleted(initialBatchIds);
|
||||||
if (event.text) {
|
if (event.text) {
|
||||||
if (provider.isAuthRequired?.(event.text)) {
|
dispatchResultText(event.text, routing);
|
||||||
writeAuthRequiredMessage(provider, routing);
|
|
||||||
} else {
|
|
||||||
dispatchResultText(event.text, routing);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,47 +0,0 @@
|
|||||||
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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
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');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -236,18 +236,6 @@ const CLAUDE_CODE_AUTO_COMPACT_WINDOW = '165000';
|
|||||||
*/
|
*/
|
||||||
const STALE_SESSION_RE = /no conversation found|ENOENT.*\.jsonl|session.*not found/i;
|
const STALE_SESSION_RE = /no conversation found|ENOENT.*\.jsonl|session.*not found/i;
|
||||||
|
|
||||||
/**
|
|
||||||
* 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/;
|
|
||||||
|
|
||||||
export class ClaudeProvider implements AgentProvider {
|
export class ClaudeProvider implements AgentProvider {
|
||||||
readonly supportsNativeSlashCommands = true;
|
readonly supportsNativeSlashCommands = true;
|
||||||
|
|
||||||
@@ -271,14 +259,6 @@ export class ClaudeProvider implements AgentProvider {
|
|||||||
return STALE_SESSION_RE.test(msg);
|
return STALE_SESSION_RE.test(msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
isAuthRequired(text: string): boolean {
|
|
||||||
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);
|
||||||
|
|||||||
@@ -14,27 +14,6 @@ export interface AgentProvider {
|
|||||||
* (missing transcript, unknown session, etc.) and should be cleared.
|
* (missing transcript, unknown session, etc.) and should be cleared.
|
||||||
*/
|
*/
|
||||||
isSessionInvalid(err: unknown): boolean;
|
isSessionInvalid(err: unknown): boolean;
|
||||||
|
|
||||||
/**
|
|
||||||
* True if the given text/error indicates the underlying SDK or CLI has no
|
|
||||||
* 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 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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user