mirror of
https://github.com/qwibitai/nanoclaw.git
synced 2026-06-04 10:14:47 +08:00
setup: drop redundant agent ping; harden auth detection and OAuth paste
- 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) <noreply@anthropic.com>
This commit is contained in:
+13
-12
@@ -491,14 +491,6 @@ async function main(): Promise<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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',
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
+19
-32
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
+9
-29
@@ -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<void> {
|
||||
|
||||
// 1. Check service status + detect checkout mismatch.
|
||||
//
|
||||
// Why the mismatch matters: the host binds `<DATA_DIR>/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 `<projectRoot>/data/v2.db` and
|
||||
// binds `<DATA_DIR>/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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user