import { randomUUID } from 'node:crypto'; import type { Command } from 'commander'; import { registerFleetAgentCommands, type FleetCommandDeps } from './fleet.js'; import { withAuth } from './with-auth.js'; import { selectItem } from './select-dialog.js'; import { fetchAgentConfigs, createAgentConfig, updateAgentConfig, deleteAgentConfig, fetchProjects, fetchProviders, enrollAgent, fetchEnrollment, } from '../tui/gateway-api.js'; import type { AgentConfigInfo, EnrolledAgentInfo } from '../tui/gateway-api.js'; function formatAgent(a: AgentConfigInfo): string { const sys = a.isSystem ? ' [system]' : ''; return `${a.name}${sys} — ${a.provider}/${a.model} (${a.status})`; } function showAgentDetail(a: AgentConfigInfo) { console.log(` ID: ${a.id}`); console.log(` Name: ${a.name}`); console.log(` Provider: ${a.provider}`); console.log(` Model: ${a.model}`); console.log(` Status: ${a.status}`); console.log(` System: ${a.isSystem ? 'yes' : 'no'}`); console.log(` Project: ${a.projectId ?? '—'}`); console.log(` System Prompt: ${a.systemPrompt ? `${a.systemPrompt.slice(0, 80)}...` : '—'}`); console.log(` Tools: ${a.allowedTools ? a.allowedTools.join(', ') : 'all'}`); console.log(` Skills: ${a.skills ? a.skills.join(', ') : '—'}`); console.log(` Created: ${new Date(a.createdAt).toLocaleString()}`); } export function registerAgentCommand(program: Command, fleetDeps: FleetCommandDeps = {}) { const cmd = program .command('agent') .description('Manage agent configurations and local fleet agents') .option('-g, --gateway ', 'Gateway URL', 'http://localhost:14242') .option('--mosaic-home ', 'Mosaic home directory') .option('--roster ', 'Local fleet roster path') .option('--list', 'List all agents') .option('--new', 'Create a new agent') .option('--show ', 'Show agent details') .option('--update ', 'Update an agent') .option('--delete ', 'Delete an agent') .action( async (opts: { gateway: string; list?: boolean; new?: boolean; show?: string; update?: string; delete?: string; }) => { const auth = await withAuth(opts.gateway); if (opts.list) { return listAgents(auth.gateway, auth.cookie); } if (opts.new) { return createAgentWizard(auth.gateway, auth.cookie); } if (opts.show) { return showAgent(auth.gateway, auth.cookie, opts.show); } if (opts.update) { return updateAgentWizard(auth.gateway, auth.cookie, opts.update); } if (opts.delete) { return deleteAgent(auth.gateway, auth.cookie, opts.delete); } // Default: interactive select return interactiveSelect(auth.gateway, auth.cookie); }, ); registerEnrollmentCommands(cmd); registerFleetAgentCommands(cmd, fleetDeps); return cmd; } // ── Agent enrollment (design docs/plans/2026-08-29-agent-enrollment-command-design.md §3; // CLI parity bound by contract 5 §4.5) ── export interface EnrollCommandOptions { gateway: string; harness: string; name: string; model: string; provider: string; persona?: string; credential: string; idempotencyKey?: string; correlationId?: string; replayMode?: string; } /** * Read an intake credential value from stdin. Never accepted via argv — a * process argument is world-readable in `ps` for the process lifetime. */ export async function readCredentialFromStdin(): Promise { if (process.stdin.isTTY) { console.error('Enter API key, then press Enter and Ctrl-D:'); } const chunks: Buffer[] = []; for await (const chunk of process.stdin) { chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); } return Buffer.concat(chunks) .toString('utf8') .replace(/\r?\n$/, ''); } function showEnrollment(correlationId: string, agent: EnrolledAgentInfo): void { console.log(` ID: ${agent.id}`); console.log(` Name: ${agent.name}`); console.log(` Harness: ${agent.harness ?? '—'}`); console.log(` Provider: ${agent.provider}`); console.log(` Model: ${agent.model}`); console.log(` Status: ${agent.status}`); console.log(` Owner: ${agent.ownerId ?? '—'}`); console.log(` Enrolled: ${agent.enrolledAt ?? '—'}`); console.log(` Correlation: ${correlationId}`); } export async function runEnroll( auth: { gateway: string; cookie: string }, opts: EnrollCommandOptions, readSecret: () => Promise = readCredentialFromStdin, ): Promise { if (opts.credential !== 'reference' && opts.credential !== 'intake') { console.error(`Unknown credential mode "${opts.credential}" (use reference or intake).`); process.exitCode = 1; return; } let credential: { mode: 'reference' } | { mode: 'intake'; type: 'api_key'; value: string }; if (opts.credential === 'intake') { const value = await readSecret(); if (!value) { console.error('Intake credential requires a non-empty API key on stdin.'); process.exitCode = 1; return; } credential = { mode: 'intake', type: 'api_key', value }; } else { credential = { mode: 'reference' }; } const result = await enrollAgent(auth.gateway, auth.cookie, { harness: opts.harness, name: opts.name, model: opts.model, provider: opts.provider, ...(opts.persona !== undefined ? { persona: opts.persona } : {}), credential, idempotencyKey: opts.idempotencyKey ?? randomUUID(), ...(opts.correlationId !== undefined ? { correlationId: opts.correlationId } : {}), ...(opts.replayMode !== undefined ? { replayMode: opts.replayMode } : {}), }); console.log(`Agent "${result.agent.name}" enrolled.\n`); showEnrollment(result.correlationId, result.agent); } export async function runGetEnrollment( auth: { gateway: string; cookie: string }, agentId: string, correlationId?: string, ): Promise { const result = await fetchEnrollment(auth.gateway, auth.cookie, agentId, correlationId); showEnrollment(result.correlationId, result.agent); } export function registerEnrollmentCommands(cmd: Command): void { cmd .command('enroll') .description('Enroll an agent through the gateway enrollment command (agent.enroll)') .requiredOption('--harness ', 'Harness the agent runs on (must be registered)') .requiredOption('--name ', 'Agent display name') .requiredOption('--model ', 'Model identifier') .requiredOption('--provider ', 'Provider the credential belongs to') .option('--persona ', 'Agent persona / system prompt') .option( '--credential ', 'Credential mode: "reference" (already stored) or "intake" (API key read from stdin, never argv)', 'reference', ) .option('--idempotency-key ', 'Idempotency key (generated when omitted)') .option('--correlation-id ', 'Correlation id to carry through the audit trail') .option('--replay-mode ', 'Idempotency replay mode (actor-bound)') .action(async (opts: Omit) => { const parent = cmd.opts<{ gateway: string }>(); const auth = await withAuth(parent.gateway); await runEnroll(auth, { ...opts, gateway: parent.gateway }); }); cmd .command('enrollment ') .description('Read one enrolled agent (agent.enrollment.get; owner or admin)') .option('--correlation-id ', 'Correlation id to carry through the read') .action(async (agentId: string, opts: { correlationId?: string }) => { const parent = cmd.opts<{ gateway: string }>(); const auth = await withAuth(parent.gateway); await runGetEnrollment(auth, agentId, opts.correlationId); }); } async function resolveAgent( gateway: string, cookie: string, idOrName: string, ): Promise { const agents = await fetchAgentConfigs(gateway, cookie); return agents.find((a) => a.id === idOrName || a.name === idOrName); } async function listAgents(gateway: string, cookie: string) { const agents = await fetchAgentConfigs(gateway, cookie); if (agents.length === 0) { console.log('No agents found.'); return; } console.log(`Agents (${agents.length}):\n`); for (const a of agents) { const sys = a.isSystem ? ' [system]' : ''; const project = a.projectId ? ` project=${a.projectId.slice(0, 8)}` : ''; console.log(` ${a.name}${sys} ${a.provider}/${a.model} ${a.status}${project}`); } } async function showAgent(gateway: string, cookie: string, idOrName: string) { const agent = await resolveAgent(gateway, cookie, idOrName); if (!agent) { console.error(`Agent "${idOrName}" not found.`); process.exit(1); } showAgentDetail(agent); } async function interactiveSelect(gateway: string, cookie: string) { const agents = await fetchAgentConfigs(gateway, cookie); const selected = await selectItem(agents, { message: 'Select an agent:', render: formatAgent, emptyMessage: 'No agents found. Create one with `mosaic agent --new`.', }); if (selected) { showAgentDetail(selected); } } async function createAgentWizard(gateway: string, cookie: string) { const readline = await import('node:readline'); const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); const ask = (q: string): Promise => new Promise((resolve) => rl.question(q, resolve)); try { const name = await ask('Agent name: '); if (!name.trim()) { console.error('Name is required.'); return; } // Project selection const projects = await fetchProjects(gateway, cookie); let projectId: string | undefined; if (projects.length > 0) { const selected = await selectItem(projects, { message: 'Assign to project (optional):', render: (p) => `${p.name} (${p.status})`, }); if (selected) projectId = selected.id; } // Provider / model selection const providers = await fetchProviders(gateway, cookie); let provider = 'default'; let model = 'default'; if (providers.length > 0) { const allModels = providers.flatMap((p) => p.models.map((m) => ({ provider: p.name, model: m.id, label: `${p.name}/${m.id}` })), ); if (allModels.length > 0) { const selected = await selectItem(allModels, { message: 'Select model:', render: (m) => m.label, }); if (selected) { provider = selected.provider; model = selected.model; } } } const systemPrompt = await ask('System prompt (optional, press Enter to skip): '); const agent = await createAgentConfig(gateway, cookie, { name: name.trim(), provider, model, projectId, systemPrompt: systemPrompt.trim() || undefined, }); console.log(`\nAgent "${agent.name}" created (${agent.id}).`); } finally { rl.close(); } } async function updateAgentWizard(gateway: string, cookie: string, idOrName: string) { const agent = await resolveAgent(gateway, cookie, idOrName); if (!agent) { console.error(`Agent "${idOrName}" not found.`); process.exit(1); } const readline = await import('node:readline'); const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); const ask = (q: string): Promise => new Promise((resolve) => rl.question(q, resolve)); try { console.log(`Updating agent: ${agent.name}\n`); const name = await ask(`Name [${agent.name}]: `); const systemPrompt = await ask(`System prompt [${agent.systemPrompt ? 'set' : 'none'}]: `); const updates: Record = {}; if (name.trim()) updates['name'] = name.trim(); if (systemPrompt.trim()) updates['systemPrompt'] = systemPrompt.trim(); if (Object.keys(updates).length === 0) { console.log('No changes.'); return; } const updated = await updateAgentConfig(gateway, cookie, agent.id, updates); console.log(`\nAgent "${updated.name}" updated.`); } finally { rl.close(); } } async function deleteAgent(gateway: string, cookie: string, idOrName: string) { const agent = await resolveAgent(gateway, cookie, idOrName); if (!agent) { console.error(`Agent "${idOrName}" not found.`); process.exit(1); } if (agent.isSystem) { console.error('Cannot delete system agents.'); process.exit(1); } const readline = await import('node:readline'); const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); const answer = await new Promise((resolve) => rl.question(`Delete agent "${agent.name}"? (y/N): `, resolve), ); rl.close(); if (answer.toLowerCase() !== 'y') { console.log('Cancelled.'); return; } await deleteAgentConfig(gateway, cookie, agent.id); console.log(`Agent "${agent.name}" deleted.`); }