import { spawn } from 'node:child_process'; import { randomBytes } from 'node:crypto'; import { constants } from 'node:fs'; import { mkdir, open, rename } from 'node:fs/promises'; import { join } from 'node:path'; import type { Command } from 'commander'; import { TmuxPromotionTransport, type PromotionTarget, type PromotionTransport, } from '../fleet/promotion-transport.js'; export type { PromotionTransport } from '../fleet/promotion-transport.js'; import { resolveFleetPaths, type CommandRunner } from './fleet.js'; const ATTEMPT_ID_PATTERN = /^[a-f0-9]{64}$/; const DEFAULT_POLL_INTERVAL_MS = 250; const DEFAULT_TIMEOUT_MS = 30_000; const SUBPROCESS_TIMEOUT_MS = 4_500; const PENDING_DIRECTORY = 'mosaic-lease'; const AUTHORIZATION_DIRECTORY = 'authorizations'; const AUTHORIZATION_TTL_SECONDS = 60; const RESULT_FILE = 'last-result.json'; export interface PromotionBreadcrumb { attempt_id: string; expires_at_wallclock: number | null; reason: string | null; session_id: string; ts: number; verified: boolean; } export interface PromotionBreadcrumbStore { readAttemptId(sessionId: string): Promise; readResult(): Promise; } export interface PromotionResult { bundle: string; expiresAtWallclock: number | null; reason: string | null; seat: string; sessionId: string; status: 'VERIFIED' | 'UNVERIFIED'; } export interface PromoteSeatOptions { clock?: () => number; pollIntervalMs?: number; sleep?: (milliseconds: number) => Promise; store: PromotionBreadcrumbStore; target?: PromotionTarget; timeoutMs?: number; transport: PromotionTransport; } export interface PromoteCommandDeps { mintAuthorization?: (target: PromotionTarget) => Promise; mosaicHome?: string; runner?: CommandRunner; store?: PromotionBreadcrumbStore; transport?: PromotionTransport; } /** Private, local result store shared with the in-seat completion hook. */ export class FilePromotionBreadcrumbStore implements PromotionBreadcrumbStore { constructor(private readonly runtimeDirectory = defaultRuntimeDirectory()) {} async readAttemptId(sessionId: string): Promise { if (!ATTEMPT_ID_PATTERN.test(sessionId)) return null; const content = await readPrivateFile( join(this.runtimeDirectory, PENDING_DIRECTORY, `pending-${sessionId}`), ); const attemptId = content?.trim(); return attemptId !== undefined && ATTEMPT_ID_PATTERN.test(attemptId) ? attemptId : null; } async readResult(): Promise { const content = await readPrivateFile( join(this.runtimeDirectory, PENDING_DIRECTORY, RESULT_FILE), ); if (content === null) return null; try { return parseBreadcrumb(JSON.parse(content) as unknown); } catch { return null; } } } /** Drives one bounded, non-interactive in-seat promotion attempt. */ export async function promoteSeat( seat: string, options: PromoteSeatOptions, ): Promise { const clock = options.clock ?? wallClockSeconds; const sleep = options.sleep ?? defaultSleep; const timeoutMs = normalizeTimeout(options.timeoutMs); const pollIntervalMs = normalizePollInterval(options.pollIntervalMs); let target: PromotionTarget; try { target = options.target ?? (await options.transport.resolve(seat)); } catch (error: unknown) { return unverifiedUnresolvedSeat(seat, `RESOLVE_FAILED: ${errorMessage(error)}`); } const previousAttemptId = await options.store.readAttemptId(target.sessionId); const preSendTimestamp = clock(); try { await options.transport.sendPromotion(target); } catch (error: unknown) { return unverified(target, `DELIVERY_FAILED: ${errorMessage(error)}`); } const deadline = preSendTimestamp + timeoutMs / 1_000; let attemptId: string | null = null; while (true) { const currentAttemptId = await options.store.readAttemptId(target.sessionId); if (currentAttemptId !== null && currentAttemptId !== previousAttemptId) { attemptId = currentAttemptId; } if (attemptId !== null || previousAttemptId === null) { // Completion can consume a first attempt's nonce before this poll observes it. // In that branch, correlation degrades to session_id + fresh timestamp, which // is acceptable for this 0600, same-UID local trust boundary. const breadcrumb = await options.store.readResult(); if ( breadcrumb !== null && breadcrumb.session_id === target.sessionId && (attemptId === null || breadcrumb.attempt_id === attemptId) && breadcrumb.ts > preSendTimestamp ) { return { bundle: target.bundle, expiresAtWallclock: breadcrumb.expires_at_wallclock, reason: breadcrumb.reason, seat: target.seat, sessionId: target.sessionId, status: breadcrumb.verified ? 'VERIFIED' : 'UNVERIFIED', }; } } if (clock() >= deadline) return unverified(target, 'PROMOTION_TIMEOUT'); await sleep(Math.min(pollIntervalMs, Math.max(0, deadline - clock()) * 1_000)); } } export function registerPromoteCommand(program: Command, deps: PromoteCommandDeps = {}): void { const mosaicHome = deps.mosaicHome ?? resolveFleetPaths().mosaicHome; const transport = deps.transport ?? new TmuxPromotionTransport({ mosaicHome, runner: deps.runner ?? runCommand }); const store = deps.store ?? new FilePromotionBreadcrumbStore(); program .command('promote ') .description('Promote a Claude fleet seat and report the correlated lease result') .option( '--timeout ', `Bounded result wait in milliseconds (default: ${DEFAULT_TIMEOUT_MS})`, ) .action(async (seat: string, opts: { timeout?: string }) => { if ( process.env['MOSAIC_LEASE_SESSION_ID'] !== undefined && deps.mintAuthorization === undefined ) { console.error('mosaic promote must run outside a lease-gated seat.'); process.exitCode = 1; return; } let target: PromotionTarget; try { target = await transport.resolve(seat); await (deps.mintAuthorization ?? mintAuthorization)(target); } catch (error: unknown) { console.error(`mosaic promote authorization failed: ${errorMessage(error)}`); process.exitCode = 1; return; } const result = await promoteSeat(seat, { store, target, timeoutMs: parseOptionTimeout(opts.timeout), transport, }); const expiry = result.expiresAtWallclock === null ? 'none' : new Date(result.expiresAtWallclock * 1_000).toISOString(); const reason = result.reason === null ? '' : ` reason=${result.reason}`; console.log( `${result.status} seat=${result.seat} session=${result.sessionId} bundle=${result.bundle} expiry=${expiry}${reason}`, ); if (result.status === 'UNVERIFIED') process.exitCode = 1; }); } async function mintAuthorization(target: PromotionTarget): Promise { const directory = join(defaultRuntimeDirectory(), PENDING_DIRECTORY, AUTHORIZATION_DIRECTORY); await mkdir(directory, { mode: 0o700, recursive: true }); const token = { expires_at: wallClockSeconds() + AUTHORIZATION_TTL_SECONDS, nonce: randomBytes(32).toString('hex'), seat: target.seat, session_id: target.sessionId, ts: wallClockSeconds(), }; const destination = join(directory, `${target.sessionId}.auth`); const temporary = join(directory, `.${target.sessionId}.${randomBytes(8).toString('hex')}.tmp`); const handle = await open( temporary, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL, 0o600, ); try { await handle.chmod(0o600); await handle.writeFile(JSON.stringify(token)); await handle.sync(); } finally { await handle.close(); } await rename(temporary, destination); } function defaultRuntimeDirectory(): string { const configured = process.env['XDG_RUNTIME_DIR']; if (configured) return configured; const uid = typeof process.getuid === 'function' ? process.getuid() : 0; return `/run/user/${uid}`; } async function readPrivateFile(path: string): Promise { let handle: Awaited>; try { handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW); } catch { return null; } try { const metadata = await handle.stat(); if ( !metadata.isFile() || metadata.uid !== (typeof process.getuid === 'function' ? process.getuid() : 0) || (metadata.mode & 0o077) !== 0 ) { return null; } return handle.readFile({ encoding: 'utf8' }); } catch { return null; } finally { await handle.close(); } } function parseBreadcrumb(value: unknown): PromotionBreadcrumb | null { if (!isRecord(value) || Object.keys(value).length !== 6) return null; const { attempt_id, expires_at_wallclock, reason, session_id, ts, verified } = value; if ( typeof attempt_id !== 'string' || !ATTEMPT_ID_PATTERN.test(attempt_id) || typeof verified !== 'boolean' || typeof session_id !== 'string' || !ATTEMPT_ID_PATTERN.test(session_id) || typeof ts !== 'number' || !Number.isFinite(ts) || (expires_at_wallclock !== null && (typeof expires_at_wallclock !== 'number' || !Number.isFinite(expires_at_wallclock))) || (reason !== null && typeof reason !== 'string') ) { return null; } return { attempt_id, expires_at_wallclock, reason, session_id, ts, verified }; } function isRecord(value: unknown): value is Record { return typeof value === 'object' && value !== null && !Array.isArray(value); } function normalizeTimeout(value: number | undefined): number { return value !== undefined && Number.isFinite(value) ? Math.max(0, value) : DEFAULT_TIMEOUT_MS; } function normalizePollInterval(value: number | undefined): number { return value !== undefined && Number.isFinite(value) ? Math.max(1, value) : DEFAULT_POLL_INTERVAL_MS; } function parseOptionTimeout(value: string | undefined): number | undefined { if (value === undefined) return undefined; const parsed = Number.parseInt(value, 10); return Number.isFinite(parsed) ? parsed : undefined; } function unverifiedUnresolvedSeat(seat: string, reason: string): PromotionResult { return { bundle: 'unresolved', expiresAtWallclock: null, reason, seat, sessionId: 'unresolved', status: 'UNVERIFIED', }; } function unverified(target: PromotionTarget, reason: string): PromotionResult { return { bundle: target.bundle, expiresAtWallclock: null, reason, seat: target.seat, sessionId: target.sessionId, status: 'UNVERIFIED', }; } function errorMessage(error: unknown): string { return error instanceof Error ? error.message : String(error); } function wallClockSeconds(): number { return Date.now() / 1_000; } function defaultSleep(milliseconds: number): Promise { return new Promise((resolve) => setTimeout(resolve, milliseconds)); } function runCommand( command: string, args: string[], ): Promise<{ exitCode: number; stderr: string; stdout: string; }> { return new Promise((resolve) => { const child = spawn(command, args, { stdio: ['ignore', 'pipe', 'pipe'] }); let stdout = ''; let stderr = ''; let settled = false; const finish = (result: { exitCode: number; stderr: string; stdout: string }): void => { if (settled) return; settled = true; clearTimeout(timeout); resolve(result); }; const timeout = setTimeout(() => { child.kill('SIGKILL'); finish({ exitCode: 124, stderr: `Promotion transport subprocess timed out after ${SUBPROCESS_TIMEOUT_MS}ms.`, stdout, }); }, SUBPROCESS_TIMEOUT_MS); child.stdout.on('data', (chunk: Buffer) => { stdout += chunk.toString('utf8'); }); child.stderr.on('data', (chunk: Buffer) => { stderr += chunk.toString('utf8'); }); child.on('error', (error: Error) => { finish({ exitCode: 127, stderr: error.message, stdout }); }); child.on('close', (code: number | null) => { finish({ exitCode: code ?? 1, stderr, stdout }); }); }); }