feat(mosaic): mechanically authorize lease promotion

This commit is contained in:
Jason Woltje
2026-08-11 20:51:03 -05:00
parent 239a2a93f1
commit 709a23d08c
6 changed files with 216 additions and 41 deletions
+10 -2
View File
@@ -152,7 +152,11 @@ describe('mosaic promote', () => {
};
const output = vi.spyOn(console, 'log').mockImplementation(() => {});
const program = new Command().exitOverride();
registerPromoteCommand(program, { store, transport: promotionTransport });
registerPromoteCommand(program, {
mintAuthorization: async () => {},
store,
transport: promotionTransport,
});
try {
await program.parseAsync(['node', 'mosaic', 'promote', 'claude-seat']);
@@ -179,7 +183,11 @@ describe('mosaic promote', () => {
};
const output = vi.spyOn(console, 'log').mockImplementation(() => {});
const program = new Command().exitOverride();
registerPromoteCommand(program, { store, transport: promotionTransport });
registerPromoteCommand(program, {
mintAuthorization: async () => {},
store,
transport: promotionTransport,
});
try {
process.exitCode = undefined;
+52 -2
View File
@@ -1,6 +1,7 @@
import { spawn } from 'node:child_process';
import { randomBytes } from 'node:crypto';
import { constants } from 'node:fs';
import { open } from 'node:fs/promises';
import { mkdir, open, rename } from 'node:fs/promises';
import { join } from 'node:path';
import type { Command } from 'commander';
import {
@@ -17,6 +18,8 @@ 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 {
@@ -47,11 +50,13 @@ export interface PromoteSeatOptions {
pollIntervalMs?: number;
sleep?: (milliseconds: number) => Promise<void>;
store: PromotionBreadcrumbStore;
target?: PromotionTarget;
timeoutMs?: number;
transport: PromotionTransport;
}
export interface PromoteCommandDeps {
mintAuthorization?: (target: PromotionTarget) => Promise<void>;
mosaicHome?: string;
runner?: CommandRunner;
store?: PromotionBreadcrumbStore;
@@ -95,7 +100,7 @@ export async function promoteSeat(
const pollIntervalMs = normalizePollInterval(options.pollIntervalMs);
let target: PromotionTarget;
try {
target = await options.transport.resolve(seat);
target = options.target ?? (await options.transport.resolve(seat));
} catch (error: unknown) {
return unverifiedUnresolvedSeat(seat, `RESOLVE_FAILED: ${errorMessage(error)}`);
}
@@ -154,8 +159,26 @@ export function registerPromoteCommand(program: Command, deps: PromoteCommandDep
`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,
});
@@ -171,6 +194,33 @@ export function registerPromoteCommand(program: Command, deps: PromoteCommandDep
});
}
async function mintAuthorization(target: PromotionTarget): Promise<void> {
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;