From 1b08b58fcd80a2019b5cf012904798901b087fc8 Mon Sep 17 00:00:00 2001 From: "exe.dev user" Date: Fri, 1 May 2026 17:03:02 +0000 Subject: [PATCH] setup: drop redundant agent ping; harden auth detection and OAuth paste MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - verify: remove the CLI ping; cli-agent step earlier in setup already proved the round-trip works, and the test agent gets cleaned up before verify runs — so the ping was guaranteed to fail on installs that wired a messaging app instead of staying CLI-only. Status now collapses to service-running ∧ credentials ∧ ≥1 wired group. - agent-ping: catch Claude Code's "Please run /login" / "Not logged in" / "Invalid API key" banners so a successfully-spawned agent that has no credentials no longer reports as 'ok'. - auth paste: validate the full sk-ant-oat…AA shape; when the cleaned input is under 90 chars, surface a truncation-specific hint pointing at terminal wrap as the likely cause. Strip internal whitespace at both validate and assignment so multi-line pastes that survive clack also go through cleanly. Co-Authored-By: Claude Opus 4.7 (1M context) --- setup/auto.ts | 25 +++++++++--------- setup/lib/agent-ping.test.ts | 9 +++++++ setup/lib/agent-ping.ts | 5 +++- setup/verify.test.ts | 51 ++++++++++++++---------------------- setup/verify.ts | 38 +++++++-------------------- 5 files changed, 54 insertions(+), 74 deletions(-) diff --git a/setup/auto.ts b/setup/auto.ts index f97757119..c468abc14 100644 --- a/setup/auto.ts +++ b/setup/auto.ts @@ -491,14 +491,6 @@ async function main(): Promise { 6, ), ); - } else { - const agentPing = res.terminal?.fields.AGENT_PING; - if (agentPing && agentPing !== 'ok' && agentPing !== 'skipped') { - notes.push( - "• Your assistant didn't reply to a test message. " + - 'Check `logs/nanoclaw.log` for clues, then try `pnpm run chat hi`.', - ); - } } if (!res.terminal?.fields.CONFIGURED_CHANNELS) { notes.push( @@ -518,7 +510,6 @@ async function main(): Promise { unresolved_count: notes.length, service_running: res.terminal?.fields.SERVICE === 'running', has_credentials: res.terminal?.fields.CREDENTIALS === 'configured', - agent_responds: res.terminal?.fields.AGENT_PING === 'ok', }); await offerClaudeAssist({ stepName: 'verify', @@ -777,15 +768,25 @@ async function runPasteAuth(method: 'oauth' | 'api'): Promise { message: `Paste your ${label}`, clearOnError: true, validate: (v) => { - if (!v || !v.trim()) return 'Required'; - if (!v.trim().startsWith(prefix)) { + // Strip any internal whitespace so a line-wrapped paste that did + // survive into clack can still validate. The mid-token-newline + // case where clack only sees the first line is caught by the + // shape check below. + const cleaned = (v ?? '').replace(/\s+/g, ''); + if (!cleaned) return 'Required'; + if (!cleaned.startsWith(prefix)) { return `Should start with ${prefix}…`; } + if (method === 'oauth' && !/^sk-ant-oat[A-Za-z0-9_-]{80,500}AA$/.test(cleaned)) { + return cleaned.length < 90 + ? 'Token looks truncated — line breaks in the paste can cut it off. Widen your terminal so the token fits on one line, then paste again.' + : "Token shape doesn't look right (expected sk-ant-oat…AA)."; + } return undefined; }, }), ); - const token = (answer as string).trim(); + const token = (answer as string).replace(/\s+/g, ''); const res = await runQuietChild( 'auth', diff --git a/setup/lib/agent-ping.test.ts b/setup/lib/agent-ping.test.ts index 5f2be2cd9..3578ec16b 100644 --- a/setup/lib/agent-ping.test.ts +++ b/setup/lib/agent-ping.test.ts @@ -20,6 +20,15 @@ describe('classifyPingResult', () => { expect(classifyPingResult(1, '', 'Authentication error')).toBe('auth_error'); }); + it('detects Claude Code login banners printed as a chat reply', () => { + expect( + classifyPingResult(0, 'Invalid API key · Please run /login'), + ).toBe('auth_error'); + expect( + classifyPingResult(0, 'Not logged in · Please run /login'), + ).toBe('auth_error'); + }); + it('preserves socket errors', () => { expect(classifyPingResult(2, '')).toBe('socket_error'); }); diff --git a/setup/lib/agent-ping.ts b/setup/lib/agent-ping.ts index 49c5fe294..5682c2f99 100644 --- a/setup/lib/agent-ping.ts +++ b/setup/lib/agent-ping.ts @@ -20,7 +20,10 @@ export function classifyPingResult(exitCode: number | null, stdout: string, stde if ( /Invalid bearer token/i.test(output) || /authentication[_ ]error/i.test(output) || - /Failed to authenticate/i.test(output) + /Failed to authenticate/i.test(output) || + /Please run \/login/i.test(output) || + /Not logged in/i.test(output) || + /Invalid API key/i.test(output) ) { return 'auth_error'; } diff --git a/setup/verify.test.ts b/setup/verify.test.ts index 1e09acd51..444b2cdff 100644 --- a/setup/verify.test.ts +++ b/setup/verify.test.ts @@ -5,45 +5,14 @@ import { determineVerifyStatus } from './verify.js'; const healthyBase = { service: 'running' as const, credentials: 'configured', - anyChannelConfigured: false, registeredGroups: 1, - agentPing: 'ok' as const, }; describe('determineVerifyStatus', () => { - it('accepts a working CLI-only install', () => { + it('accepts a healthy install with at least one wired agent group', () => { expect(determineVerifyStatus(healthyBase)).toBe('success'); }); - it('accepts a messaging-channel install when CLI ping is skipped', () => { - expect( - determineVerifyStatus({ - ...healthyBase, - anyChannelConfigured: true, - agentPing: 'skipped', - }), - ).toBe('success'); - }); - - it('fails when neither CLI nor messaging channels are usable', () => { - expect( - determineVerifyStatus({ - ...healthyBase, - agentPing: 'skipped', - }), - ).toBe('failed'); - }); - - it('fails when the CLI agent does not respond', () => { - expect( - determineVerifyStatus({ - ...healthyBase, - anyChannelConfigured: true, - agentPing: 'no_reply', - }), - ).toBe('failed'); - }); - it('fails when no agent groups are registered', () => { expect( determineVerifyStatus({ @@ -52,4 +21,22 @@ describe('determineVerifyStatus', () => { }), ).toBe('failed'); }); + + it('fails when the service is not running', () => { + expect( + determineVerifyStatus({ + ...healthyBase, + service: 'stopped', + }), + ).toBe('failed'); + }); + + it('fails when credentials are missing', () => { + expect( + determineVerifyStatus({ + ...healthyBase, + credentials: 'missing', + }), + ).toBe('failed'); + }); }); diff --git a/setup/verify.ts b/setup/verify.ts index 30a54084d..de1160c76 100644 --- a/setup/verify.ts +++ b/setup/verify.ts @@ -14,7 +14,6 @@ import Database from 'better-sqlite3'; import { DATA_DIR } from '../src/config.js'; import { readEnvFile } from '../src/env.js'; import { log } from '../src/log.js'; -import { pingCliAgent, type PingResult } from './lib/agent-ping.js'; import { getLaunchdLabel, getSystemdUnit } from '../src/install-slug.js'; import { getPlatform, @@ -33,11 +32,12 @@ export async function run(_args: string[]): Promise { // 1. Check service status + detect checkout mismatch. // - // Why the mismatch matters: the host binds `/cli.sock` relative - // to the project root it was started from. If the running service is from - // a sibling checkout (common for developers with multiple clones), this - // repo's `data/cli.sock` won't exist — AGENT_PING would return a - // misleading `socket_error`. Surface the mismatch directly instead. + // Why the mismatch matters: the host reads `/data/v2.db` and + // binds `/cli.sock` relative to the project root it was started + // from. If the running service is from a sibling checkout (common for + // developers with multiple clones), nothing in this checkout is actually + // wired up. Surface the mismatch directly so the user knows to point the + // service at the right folder. let service: | 'not_found' | 'stopped' @@ -186,7 +186,6 @@ export async function run(_args: string[]): Promise { if (has('IMESSAGE_ENABLED')) channelAuth.imessage = 'configured'; const configuredChannels = Object.keys(channelAuth); - const anyChannelConfigured = configuredChannels.length > 0; // 5. Check registered groups in v2 central DB (agent_groups + messaging_group_agents) let registeredGroups = 0; @@ -218,23 +217,12 @@ export async function run(_args: string[]): Promise { mountAllowlist = 'configured'; } - // 7. End-to-end: ping the CLI agent and confirm it replies. Only run if - // everything upstream looks healthy, since a broken socket would just hang. - let agentPing: 'ok' | 'no_reply' | 'socket_error' | 'auth_error' | 'skipped' = 'skipped'; - if (service === 'running' && registeredGroups > 0) { - log.info('Pinging CLI agent'); - agentPing = await pingCliAgent(); - log.info('Agent ping result', { agentPing }); - } - - // Determine overall status. A CLI-only install is valid when the local - // agent round-trip succeeds; messaging app credentials are optional. + // Determine overall status. The cli-agent step earlier in setup already + // proved the agent round-trip works; verify is a static health check. const status = determineVerifyStatus({ service, credentials, - anyChannelConfigured, registeredGroups, - agentPing, }); log.info('Verification complete', { status, channelAuth }); @@ -247,7 +235,6 @@ export async function run(_args: string[]): Promise { CHANNEL_AUTH: JSON.stringify(channelAuth), REGISTERED_GROUPS: registeredGroups, MOUNT_ALLOWLIST: mountAllowlist, - AGENT_PING: agentPing, STATUS: status, LOG: 'logs/setup.log', }); @@ -258,18 +245,11 @@ export async function run(_args: string[]): Promise { export function determineVerifyStatus(input: { service: 'not_found' | 'stopped' | 'running' | 'running_other_checkout'; credentials: string; - anyChannelConfigured: boolean; registeredGroups: number; - agentPing: PingResult | 'skipped'; }): 'success' | 'failed' { - const cliAgentResponds = input.agentPing === 'ok'; - const hasUsableChannel = input.anyChannelConfigured || cliAgentResponds; - return input.service === 'running' && input.credentials !== 'missing' && - hasUsableChannel && - input.registeredGroups > 0 && - (cliAgentResponds || input.agentPing === 'skipped') + input.registeredGroups > 0 ? 'success' : 'failed'; }