201 lines
6.6 KiB
TypeScript
201 lines
6.6 KiB
TypeScript
import {
|
|
buildAgentSendCommand,
|
|
buildTmuxListPanesCommand,
|
|
getRosterAgent,
|
|
loadFleetRoster,
|
|
parseTmuxListPanes,
|
|
resolveFleetPaths,
|
|
RUNTIME_ACCEPTABLE_COMMANDS,
|
|
socketArgs,
|
|
type CommandResult,
|
|
type CommandRunner,
|
|
type FleetRoster,
|
|
} from '../commands/fleet.js';
|
|
|
|
export type FleetRuntimeTransportErrorCode =
|
|
| 'forbidden'
|
|
| 'invalid_request'
|
|
| 'not_found'
|
|
| 'unavailable';
|
|
|
|
/** A roster-bound, verified tmux target. It never accepts a caller-selected socket. */
|
|
export interface FleetRuntimeTarget {
|
|
id: string;
|
|
runtimeId: string;
|
|
socketName: string;
|
|
}
|
|
|
|
/** Narrow transport boundary consumed by the runtime provider. */
|
|
export interface FleetRuntimeTransport {
|
|
verifySession(sessionId: string): Promise<FleetRuntimeTarget>;
|
|
listSessions(): Promise<FleetRuntimeTarget[]>;
|
|
sendMessage(sessionId: string, message: string, sourceLabel: string): Promise<void>;
|
|
terminate(sessionId: string): Promise<void>;
|
|
}
|
|
|
|
export interface FleetTmuxRuntimeTransportOptions {
|
|
mosaicHome: string;
|
|
rosterPath?: string;
|
|
rosterLoader?: () => Promise<FleetRoster>;
|
|
runner: CommandRunner;
|
|
}
|
|
|
|
/** A typed, fail-closed error for roster, target, identity, and tmux failures. */
|
|
export class FleetRuntimeTransportError extends Error {
|
|
constructor(
|
|
readonly code: FleetRuntimeTransportErrorCode,
|
|
message: string,
|
|
) {
|
|
super(message);
|
|
this.name = FleetRuntimeTransportError.name;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Roster-bound transport for the local fleet tmux server. Every side-effecting
|
|
* operation verifies an exact roster name, the roster socket, and the runtime
|
|
* command in the exact pane before it invokes the maintained sender or tmux.
|
|
*/
|
|
export class FleetTmuxRuntimeTransport implements FleetRuntimeTransport {
|
|
private readonly rosterLoader: () => Promise<FleetRoster>;
|
|
|
|
constructor(private readonly options: FleetTmuxRuntimeTransportOptions) {
|
|
this.rosterLoader =
|
|
options.rosterLoader ??
|
|
(() =>
|
|
loadFleetRoster(options.rosterPath ?? resolveFleetPaths(options.mosaicHome).rosterPath));
|
|
}
|
|
|
|
async verifySession(sessionId: string): Promise<FleetRuntimeTarget> {
|
|
const roster = await this.loadRoster();
|
|
return this.verifyRosterSession(roster, sessionId);
|
|
}
|
|
|
|
async listSessions(): Promise<FleetRuntimeTarget[]> {
|
|
const roster = await this.loadRoster();
|
|
const targets = await Promise.all(
|
|
roster.agents.map(async (agent): Promise<FleetRuntimeTarget | undefined> => {
|
|
try {
|
|
return await this.verifyRosterSession(roster, agent.name);
|
|
} catch (error: unknown) {
|
|
if (error instanceof FleetRuntimeTransportError && error.code === 'unavailable') {
|
|
return undefined;
|
|
}
|
|
throw error;
|
|
}
|
|
}),
|
|
);
|
|
return targets.filter((target): target is FleetRuntimeTarget => target !== undefined);
|
|
}
|
|
|
|
async sendMessage(sessionId: string, message: string, sourceLabel: string): Promise<void> {
|
|
if (message.length === 0) {
|
|
throw new FleetRuntimeTransportError('invalid_request', 'Fleet message content is required');
|
|
}
|
|
if (sourceLabel.length === 0) {
|
|
throw new FleetRuntimeTransportError(
|
|
'invalid_request',
|
|
'Fleet message source label is required',
|
|
);
|
|
}
|
|
const target = await this.verifySession(sessionId);
|
|
const command = buildAgentSendCommand(
|
|
resolveFleetPaths(this.options.mosaicHome),
|
|
target.id,
|
|
message,
|
|
target.socketName,
|
|
sourceLabel,
|
|
);
|
|
await this.runChecked(command, 'Fleet message delivery failed');
|
|
}
|
|
|
|
async terminate(sessionId: string): Promise<void> {
|
|
const target = await this.verifySession(sessionId);
|
|
await this.runChecked(
|
|
['tmux', ...socketArgs(target.socketName), 'kill-session', '-t', `=${target.id}`],
|
|
'Fleet session termination failed',
|
|
);
|
|
}
|
|
|
|
private async loadRoster(): Promise<FleetRoster> {
|
|
try {
|
|
return await this.rosterLoader();
|
|
} catch (error: unknown) {
|
|
const message = error instanceof Error ? error.message : 'Fleet roster is unavailable';
|
|
throw new FleetRuntimeTransportError(
|
|
'unavailable',
|
|
`Fleet roster is unavailable: ${message}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
private async verifyRosterSession(
|
|
roster: FleetRoster,
|
|
sessionId: string,
|
|
): Promise<FleetRuntimeTarget> {
|
|
const agent = this.exactRosterAgent(roster, sessionId);
|
|
const socketName = roster.tmux.socketName;
|
|
const result = await this.run(buildTmuxListPanesCommand(agent.name, socketName));
|
|
if (result.exitCode !== 0) {
|
|
throw new FleetRuntimeTransportError(
|
|
'unavailable',
|
|
`Fleet session is unavailable: ${agent.name}`,
|
|
);
|
|
}
|
|
const pane = parseTmuxListPanes(result.stdout);
|
|
if (pane.dead || pane.command === null) {
|
|
throw new FleetRuntimeTransportError(
|
|
'unavailable',
|
|
`Fleet session is unavailable: ${agent.name}`,
|
|
);
|
|
}
|
|
if (!hasExactRuntimeIdentity(agent.runtime, pane.command)) {
|
|
throw new FleetRuntimeTransportError(
|
|
'forbidden',
|
|
`Fleet runtime identity mismatch: ${agent.name}`,
|
|
);
|
|
}
|
|
return { id: agent.name, runtimeId: agent.runtime, socketName };
|
|
}
|
|
|
|
private exactRosterAgent(roster: FleetRoster, sessionId: string): FleetRoster['agents'][number] {
|
|
try {
|
|
const agent = getRosterAgent(roster, sessionId);
|
|
if (agent.name !== sessionId) {
|
|
throw new FleetRuntimeTransportError('not_found', 'Fleet session target must be exact');
|
|
}
|
|
return agent;
|
|
} catch (error: unknown) {
|
|
if (error instanceof FleetRuntimeTransportError) {
|
|
throw error;
|
|
}
|
|
throw new FleetRuntimeTransportError('not_found', 'Fleet session target is not roster-bound');
|
|
}
|
|
}
|
|
|
|
private async run(command: string[]): Promise<CommandResult> {
|
|
const [executable, ...args] = command;
|
|
if (executable === undefined) {
|
|
throw new FleetRuntimeTransportError('invalid_request', 'Fleet command is required');
|
|
}
|
|
try {
|
|
return await this.options.runner(executable, args);
|
|
} catch (error: unknown) {
|
|
const message = error instanceof Error ? error.message : 'Fleet command failed';
|
|
throw new FleetRuntimeTransportError('unavailable', message);
|
|
}
|
|
}
|
|
|
|
private async runChecked(command: string[], failureMessage: string): Promise<void> {
|
|
const result = await this.run(command);
|
|
if (result.exitCode !== 0) {
|
|
throw new FleetRuntimeTransportError('unavailable', failureMessage);
|
|
}
|
|
}
|
|
}
|
|
|
|
function hasExactRuntimeIdentity(runtimeId: string, paneCommand: string): boolean {
|
|
const expectedCommands = RUNTIME_ACCEPTABLE_COMMANDS[runtimeId];
|
|
return expectedCommands !== undefined && expectedCommands.includes(paneCommand);
|
|
}
|