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,
|
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) {
|
if (!res.terminal?.fields.CONFIGURED_CHANNELS) {
|
||||||
notes.push(
|
notes.push(
|
||||||
@@ -518,7 +510,6 @@ async function main(): Promise<void> {
|
|||||||
unresolved_count: notes.length,
|
unresolved_count: notes.length,
|
||||||
service_running: res.terminal?.fields.SERVICE === 'running',
|
service_running: res.terminal?.fields.SERVICE === 'running',
|
||||||
has_credentials: res.terminal?.fields.CREDENTIALS === 'configured',
|
has_credentials: res.terminal?.fields.CREDENTIALS === 'configured',
|
||||||
agent_responds: res.terminal?.fields.AGENT_PING === 'ok',
|
|
||||||
});
|
});
|
||||||
await offerClaudeAssist({
|
await offerClaudeAssist({
|
||||||
stepName: 'verify',
|
stepName: 'verify',
|
||||||
@@ -777,15 +768,25 @@ async function runPasteAuth(method: 'oauth' | 'api'): Promise<void> {
|
|||||||
message: `Paste your ${label}`,
|
message: `Paste your ${label}`,
|
||||||
clearOnError: true,
|
clearOnError: true,
|
||||||
validate: (v) => {
|
validate: (v) => {
|
||||||
if (!v || !v.trim()) return 'Required';
|
// Strip any internal whitespace so a line-wrapped paste that did
|
||||||
if (!v.trim().startsWith(prefix)) {
|
// 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}…`;
|
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;
|
return undefined;
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
const token = (answer as string).trim();
|
const token = (answer as string).replace(/\s+/g, '');
|
||||||
|
|
||||||
const res = await runQuietChild(
|
const res = await runQuietChild(
|
||||||
'auth',
|
'auth',
|
||||||
|
|||||||
@@ -20,6 +20,15 @@ describe('classifyPingResult', () => {
|
|||||||
expect(classifyPingResult(1, '', 'Authentication error')).toBe('auth_error');
|
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', () => {
|
it('preserves socket errors', () => {
|
||||||
expect(classifyPingResult(2, '')).toBe('socket_error');
|
expect(classifyPingResult(2, '')).toBe('socket_error');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -20,7 +20,10 @@ export function classifyPingResult(exitCode: number | null, stdout: string, stde
|
|||||||
if (
|
if (
|
||||||
/Invalid bearer token/i.test(output) ||
|
/Invalid bearer token/i.test(output) ||
|
||||||
/authentication[_ ]error/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';
|
return 'auth_error';
|
||||||
}
|
}
|
||||||
|
|||||||
+19
-32
@@ -5,45 +5,14 @@ import { determineVerifyStatus } from './verify.js';
|
|||||||
const healthyBase = {
|
const healthyBase = {
|
||||||
service: 'running' as const,
|
service: 'running' as const,
|
||||||
credentials: 'configured',
|
credentials: 'configured',
|
||||||
anyChannelConfigured: false,
|
|
||||||
registeredGroups: 1,
|
registeredGroups: 1,
|
||||||
agentPing: 'ok' as const,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('determineVerifyStatus', () => {
|
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');
|
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', () => {
|
it('fails when no agent groups are registered', () => {
|
||||||
expect(
|
expect(
|
||||||
determineVerifyStatus({
|
determineVerifyStatus({
|
||||||
@@ -52,4 +21,22 @@ describe('determineVerifyStatus', () => {
|
|||||||
}),
|
}),
|
||||||
).toBe('failed');
|
).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 { DATA_DIR } from '../src/config.js';
|
||||||
import { readEnvFile } from '../src/env.js';
|
import { readEnvFile } from '../src/env.js';
|
||||||
import { log } from '../src/log.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 { getLaunchdLabel, getSystemdUnit } from '../src/install-slug.js';
|
||||||
import {
|
import {
|
||||||
getPlatform,
|
getPlatform,
|
||||||
@@ -33,11 +32,12 @@ export async function run(_args: string[]): Promise<void> {
|
|||||||
|
|
||||||
// 1. Check service status + detect checkout mismatch.
|
// 1. Check service status + detect checkout mismatch.
|
||||||
//
|
//
|
||||||
// Why the mismatch matters: the host binds `<DATA_DIR>/cli.sock` relative
|
// Why the mismatch matters: the host reads `<projectRoot>/data/v2.db` and
|
||||||
// to the project root it was started from. If the running service is from
|
// binds `<DATA_DIR>/cli.sock` relative to the project root it was started
|
||||||
// a sibling checkout (common for developers with multiple clones), this
|
// from. If the running service is from a sibling checkout (common for
|
||||||
// repo's `data/cli.sock` won't exist — AGENT_PING would return a
|
// developers with multiple clones), nothing in this checkout is actually
|
||||||
// misleading `socket_error`. Surface the mismatch directly instead.
|
// wired up. Surface the mismatch directly so the user knows to point the
|
||||||
|
// service at the right folder.
|
||||||
let service:
|
let service:
|
||||||
| 'not_found'
|
| 'not_found'
|
||||||
| 'stopped'
|
| 'stopped'
|
||||||
@@ -186,7 +186,6 @@ export async function run(_args: string[]): Promise<void> {
|
|||||||
if (has('IMESSAGE_ENABLED')) channelAuth.imessage = 'configured';
|
if (has('IMESSAGE_ENABLED')) channelAuth.imessage = 'configured';
|
||||||
|
|
||||||
const configuredChannels = Object.keys(channelAuth);
|
const configuredChannels = Object.keys(channelAuth);
|
||||||
const anyChannelConfigured = configuredChannels.length > 0;
|
|
||||||
|
|
||||||
// 5. Check registered groups in v2 central DB (agent_groups + messaging_group_agents)
|
// 5. Check registered groups in v2 central DB (agent_groups + messaging_group_agents)
|
||||||
let registeredGroups = 0;
|
let registeredGroups = 0;
|
||||||
@@ -218,23 +217,12 @@ export async function run(_args: string[]): Promise<void> {
|
|||||||
mountAllowlist = 'configured';
|
mountAllowlist = 'configured';
|
||||||
}
|
}
|
||||||
|
|
||||||
// 7. End-to-end: ping the CLI agent and confirm it replies. Only run if
|
// Determine overall status. The cli-agent step earlier in setup already
|
||||||
// everything upstream looks healthy, since a broken socket would just hang.
|
// proved the agent round-trip works; verify is a static health check.
|
||||||
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.
|
|
||||||
const status = determineVerifyStatus({
|
const status = determineVerifyStatus({
|
||||||
service,
|
service,
|
||||||
credentials,
|
credentials,
|
||||||
anyChannelConfigured,
|
|
||||||
registeredGroups,
|
registeredGroups,
|
||||||
agentPing,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
log.info('Verification complete', { status, channelAuth });
|
log.info('Verification complete', { status, channelAuth });
|
||||||
@@ -247,7 +235,6 @@ export async function run(_args: string[]): Promise<void> {
|
|||||||
CHANNEL_AUTH: JSON.stringify(channelAuth),
|
CHANNEL_AUTH: JSON.stringify(channelAuth),
|
||||||
REGISTERED_GROUPS: registeredGroups,
|
REGISTERED_GROUPS: registeredGroups,
|
||||||
MOUNT_ALLOWLIST: mountAllowlist,
|
MOUNT_ALLOWLIST: mountAllowlist,
|
||||||
AGENT_PING: agentPing,
|
|
||||||
STATUS: status,
|
STATUS: status,
|
||||||
LOG: 'logs/setup.log',
|
LOG: 'logs/setup.log',
|
||||||
});
|
});
|
||||||
@@ -258,18 +245,11 @@ export async function run(_args: string[]): Promise<void> {
|
|||||||
export function determineVerifyStatus(input: {
|
export function determineVerifyStatus(input: {
|
||||||
service: 'not_found' | 'stopped' | 'running' | 'running_other_checkout';
|
service: 'not_found' | 'stopped' | 'running' | 'running_other_checkout';
|
||||||
credentials: string;
|
credentials: string;
|
||||||
anyChannelConfigured: boolean;
|
|
||||||
registeredGroups: number;
|
registeredGroups: number;
|
||||||
agentPing: PingResult | 'skipped';
|
|
||||||
}): 'success' | 'failed' {
|
}): 'success' | 'failed' {
|
||||||
const cliAgentResponds = input.agentPing === 'ok';
|
|
||||||
const hasUsableChannel = input.anyChannelConfigured || cliAgentResponds;
|
|
||||||
|
|
||||||
return input.service === 'running' &&
|
return input.service === 'running' &&
|
||||||
input.credentials !== 'missing' &&
|
input.credentials !== 'missing' &&
|
||||||
hasUsableChannel &&
|
input.registeredGroups > 0
|
||||||
input.registeredGroups > 0 &&
|
|
||||||
(cliAgentResponds || input.agentPing === 'skipped')
|
|
||||||
? 'success'
|
? 'success'
|
||||||
: 'failed';
|
: 'failed';
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user