From 796140e546ac7528220b7a96ff6e2a5ddf383c1f Mon Sep 17 00:00:00 2001 From: R-M-Naveen Date: Thu, 12 Feb 2026 10:10:52 -0600 Subject: [PATCH] feat: Add agent self-registration CLI command Adds `npx atxp agent register` for AI agents to self-register without requiring an existing login. The command handles the 2-step flow: fetches a base64-encoded captcha challenge, decodes and displays it, prompts for the answer, then verifies and creates the account. Supports --server flag for custom server URL and --answer for non-interactive use. Co-Authored-By: Claude Opus 4.6 --- packages/atxp/src/commands/agent.ts | 143 +++++++++++++++++++++++++++- packages/atxp/src/help.ts | 5 +- 2 files changed, 144 insertions(+), 4 deletions(-) diff --git a/packages/atxp/src/commands/agent.ts b/packages/atxp/src/commands/agent.ts index 0bfc1aa..8ee3a5f 100644 --- a/packages/atxp/src/commands/agent.ts +++ b/packages/atxp/src/commands/agent.ts @@ -1,6 +1,9 @@ import chalk from 'chalk'; +import { createInterface } from 'readline'; import { getConnection } from '../config.js'; +const DEFAULT_ACCOUNTS_URL = 'https://accounts.atxp.ai'; + function getAccountsAuth(): { baseUrl: string; token: string } { const connection = getConnection(); if (!connection) { @@ -17,11 +20,25 @@ function getAccountsAuth(): { baseUrl: string; token: string } { return { baseUrl: `${url.protocol}//${url.host}`, token }; } +function getBaseUrl(): string { + const connection = getConnection(); + if (connection) { + try { + const url = new URL(connection); + return `${url.protocol}//${url.host}`; + } catch { + // Fall through to default + } + } + return DEFAULT_ACCOUNTS_URL; +} + function showAgentHelp(): void { console.log(chalk.bold('Agent Commands:')); console.log(); - console.log(' ' + chalk.cyan('npx atxp agent create') + ' ' + 'Create a new agent account'); - console.log(' ' + chalk.cyan('npx atxp agent list') + ' ' + 'List your agents'); + console.log(' ' + chalk.cyan('npx atxp agent create') + ' ' + 'Create a new agent account (requires login)'); + console.log(' ' + chalk.cyan('npx atxp agent list') + ' ' + 'List your agents (requires login)'); + console.log(' ' + chalk.cyan('npx atxp agent register') + ' ' + 'Self-register as an agent (no login required)'); console.log(); console.log(chalk.bold('Details:')); console.log(' Each agent gets:'); @@ -30,9 +47,15 @@ function showAgentHelp(): void { console.log(' - 10 IOU tokens to start'); console.log(' - A connection token for SDK/CLI access'); console.log(); + console.log(chalk.bold('Register Options:')); + console.log(' ' + chalk.yellow('--server') + ' ' + 'Accounts server URL (default: https://accounts.atxp.ai)'); + console.log(' ' + chalk.yellow('--answer') + ' ' + 'Provide the challenge answer non-interactively'); + console.log(); console.log(chalk.bold('Examples:')); console.log(' npx atxp agent create'); console.log(' npx atxp agent list'); + console.log(' npx atxp agent register'); + console.log(' npx atxp agent register --server http://localhost:8016'); console.log(' CONNECTION_TOKEN= npx atxp email inbox'); } @@ -51,6 +74,10 @@ export async function agentCommand(subCommand: string): Promise { await listAgents(); break; + case 'register': + await registerAgent(); + break; + default: console.error(chalk.red(`Unknown agent command: ${subCommand}`)); console.log(); @@ -105,6 +132,118 @@ async function createAgent(): Promise { console.log(' ' + chalk.yellow(`CONNECTION_TOKEN=${data.connectionToken} npx atxp email inbox`)); } +function getArgValue(flag: string): string | undefined { + const index = process.argv.findIndex((arg) => arg === flag); + return index !== -1 ? process.argv[index + 1] : undefined; +} + +function promptForInput(prompt: string): Promise { + const rl = createInterface({ input: process.stdin, output: process.stdout }); + return new Promise((resolve) => { + rl.question(prompt, (answer) => { + rl.close(); + resolve(answer.trim()); + }); + }); +} + +async function registerAgent(): Promise { + const baseUrl = getArgValue('--server') || getBaseUrl(); + const presetAnswer = getArgValue('--answer'); + + // Step 1: Get challenge + console.log(chalk.gray(`Requesting challenge from ${baseUrl}...`)); + + const challengeRes = await fetch(`${baseUrl}/agents/register`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + }); + + if (!challengeRes.ok) { + const body = await challengeRes.json().catch(() => ({})) as Record; + console.error(chalk.red(`Error: ${body.error_description || body.error || challengeRes.statusText}`)); + process.exit(1); + } + + const challenge = await challengeRes.json() as { + registration_id: string; + challenge: string; + instructions: string; + expires_at: string; + }; + + // Decode base64 challenge and instructions + const decodedChallenge = Buffer.from(challenge.challenge, 'base64').toString('utf-8'); + const decodedInstructions = Buffer.from(challenge.instructions, 'base64').toString('utf-8'); + + console.log(); + console.log(chalk.bold('Challenge:')); + console.log(' ' + chalk.yellow(decodedChallenge)); + console.log(); + console.log(chalk.bold('Instructions:')); + console.log(' ' + decodedInstructions); + console.log(); + console.log(chalk.gray(`Registration ID: ${challenge.registration_id}`)); + console.log(chalk.gray(`Expires at: ${challenge.expires_at}`)); + console.log(); + + // Step 2: Get answer + let answer: string; + if (presetAnswer) { + answer = presetAnswer; + console.log(chalk.gray(`Using provided answer: ${answer}`)); + } else { + answer = await promptForInput(chalk.bold('Your answer: ')); + if (!answer) { + console.error(chalk.red('No answer provided.')); + process.exit(1); + } + } + + // Step 3: Verify and create account + console.log(); + console.log(chalk.gray('Verifying answer and creating account...')); + + const verifyRes = await fetch(`${baseUrl}/agents/register/verify`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + registration_id: challenge.registration_id, + answer, + }), + }); + + if (!verifyRes.ok) { + const body = await verifyRes.json().catch(() => ({})) as Record; + console.error(chalk.red(`Error: ${body.error_description || body.error || verifyRes.statusText}`)); + process.exit(1); + } + + const data = await verifyRes.json() as { + agentId: string; + connectionToken: string; + connectionString: string; + email: string; + walletAddress: string; + fundedAmount: string; + }; + + console.log(); + console.log(chalk.green.bold('Agent self-registered successfully!')); + console.log(); + console.log(' ' + chalk.bold('Agent ID:') + ' ' + data.agentId); + console.log(' ' + chalk.bold('Email:') + ' ' + chalk.cyan(data.email)); + console.log(' ' + chalk.bold('Connection Token:') + ' ' + data.connectionToken); + console.log(' ' + chalk.bold('Wallet:') + ' ' + data.walletAddress); + console.log(' ' + chalk.bold('Funded:') + ' ' + data.fundedAmount + ' IOU'); + console.log(); + console.log(chalk.bold('Connection String:')); + console.log(' ' + chalk.cyan(data.connectionString)); + console.log(); + console.log(chalk.bold('Use this to authenticate as the agent:')); + console.log(' ' + chalk.yellow(`npx atxp login --token "${data.connectionString}"`)); +} + async function listAgents(): Promise { const { baseUrl, token } = getAccountsAuth(); diff --git a/packages/atxp/src/help.ts b/packages/atxp/src/help.ts index 009c2c4..b932d17 100644 --- a/packages/atxp/src/help.ts +++ b/packages/atxp/src/help.ts @@ -86,8 +86,9 @@ export function showHelp(): void { console.log(); console.log(chalk.bold('Agent Examples:')); - console.log(' npx atxp agent create # Create a new agent'); - console.log(' npx atxp agent list # List your agents'); + console.log(' npx atxp agent create # Create a new agent (requires login)'); + console.log(' npx atxp agent list # List your agents (requires login)'); + console.log(' npx atxp agent register # Self-register as an agent (no login)'); console.log(); console.log(chalk.bold('PAAS Examples:'));