Files
nanoclaw/setup/auth.ts
Koshkoshinsk 01389ff8fc feat(new-setup): add onecli, auth, and cli-agent dispatcher steps
Aggregates the loose OneCLI install, secret registration, and first-agent
wiring commands from /setup into three new dispatcher steps. Adds
--cli-only mode to init-first-agent so /new-setup can reach a working
2-way CLI chat with the bare minimum.

- setup/onecli.ts: idempotent install + PATH + api-host + .env, polls /health
- setup/auth.ts: --check verifies secret; --create --value registers it
- setup/cli-agent.ts: wraps init-first-agent --cli-only
- scripts/init-first-agent.ts: --cli-only mode; DM mode unchanged

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 10:43:35 +00:00

187 lines
4.8 KiB
TypeScript

/**
* Step: auth — Verify or register an Anthropic credential in OneCLI.
*
* Modes:
* --check (default) Verify an Anthropic secret exists.
* --create --value <token> Create an Anthropic secret. Errors if one
* already exists unless --force is passed.
*
* The actual user-facing prompt (subscription vs API key, paste the token)
* stays in the /new-setup SKILL.md. This step is just the machine side:
* it calls `onecli secrets list` / `onecli secrets create` and emits a
* structured status block. The token value is never logged.
*/
import { execFileSync } from 'child_process';
import os from 'os';
import path from 'path';
import { log } from '../src/log.js';
import { emitStatus } from './status.js';
const LOCAL_BIN = path.join(os.homedir(), '.local', 'bin');
interface Args {
mode: 'check' | 'create';
value?: string;
force: boolean;
}
function childEnv(): NodeJS.ProcessEnv {
const parts = [LOCAL_BIN];
if (process.env.PATH) parts.push(process.env.PATH);
return { ...process.env, PATH: parts.join(path.delimiter) };
}
function parseArgs(args: string[]): Args {
let mode: 'check' | 'create' = 'check';
let value: string | undefined;
let force = false;
for (let i = 0; i < args.length; i++) {
const key = args[i];
const val = args[i + 1];
switch (key) {
case '--check':
mode = 'check';
break;
case '--create':
mode = 'create';
break;
case '--value':
value = val;
i++;
break;
case '--force':
force = true;
break;
}
}
if (mode === 'create' && !value) {
emitStatus('AUTH', {
STATUS: 'failed',
ERROR: 'missing_value_for_create',
LOG: 'logs/setup.log',
});
process.exit(2);
}
return { mode, value, force };
}
interface OnecliSecret {
id: string;
name: string;
type: string;
hostPattern: string | null;
}
function listSecrets(): OnecliSecret[] {
const out = execFileSync('onecli', ['secrets', 'list'], {
encoding: 'utf-8',
env: childEnv(),
stdio: ['ignore', 'pipe', 'ignore'],
});
const parsed = JSON.parse(out) as { data?: unknown };
return Array.isArray(parsed.data) ? (parsed.data as OnecliSecret[]) : [];
}
function findAnthropicSecret(secrets: OnecliSecret[]): OnecliSecret | undefined {
return secrets.find((s) => s.type === 'anthropic');
}
function createAnthropicSecret(value: string): void {
// `value` is a credential — do not log it, do not echo, do not pass through a shell.
execFileSync(
'onecli',
[
'secrets',
'create',
'--name',
'Anthropic',
'--type',
'anthropic',
'--value',
value,
'--host-pattern',
'api.anthropic.com',
],
{
env: childEnv(),
stdio: ['ignore', 'ignore', 'pipe'],
},
);
}
export async function run(args: string[]): Promise<void> {
const { mode, value, force } = parseArgs(args);
let secrets: OnecliSecret[];
try {
secrets = listSecrets();
} catch (err) {
log.error('onecli secrets list failed', { err });
emitStatus('AUTH', {
STATUS: 'failed',
ERROR: 'onecli_list_failed',
HINT: 'Is OneCLI running? Run `/new-setup` from the onecli step.',
LOG: 'logs/setup.log',
});
process.exit(1);
}
const existing = findAnthropicSecret(secrets);
if (mode === 'check') {
emitStatus('AUTH', {
SECRET_PRESENT: !!existing,
ANTHROPIC_OK: !!existing,
STATUS: existing ? 'success' : 'missing',
...(existing ? { SECRET_NAME: existing.name, SECRET_ID: existing.id } : {}),
LOG: 'logs/setup.log',
});
return;
}
// mode === 'create'
if (existing && !force) {
emitStatus('AUTH', {
SECRET_PRESENT: true,
STATUS: 'skipped',
REASON: 'anthropic_secret_already_exists',
SECRET_NAME: existing.name,
SECRET_ID: existing.id,
HINT: 'Re-run with --force to replace, or delete the existing secret first.',
LOG: 'logs/setup.log',
});
return;
}
try {
createAnthropicSecret(value!);
} catch (err) {
const e = err as { stderr?: string | Buffer; status?: number };
const stderr = typeof e.stderr === 'string' ? e.stderr : e.stderr?.toString('utf-8') ?? '';
log.error('onecli secrets create failed', { status: e.status, stderr });
emitStatus('AUTH', {
STATUS: 'failed',
ERROR: 'onecli_create_failed',
EXIT_CODE: e.status ?? -1,
LOG: 'logs/setup.log',
});
process.exit(1);
}
// Re-verify
const updated = findAnthropicSecret(listSecrets());
emitStatus('AUTH', {
SECRET_PRESENT: !!updated,
ANTHROPIC_OK: !!updated,
CREATED: true,
STATUS: updated ? 'success' : 'failed',
...(updated ? { SECRET_NAME: updated.name, SECRET_ID: updated.id } : {}),
LOG: 'logs/setup.log',
});
}