mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-24 08:57:25 +00:00
feat: pilot agent outbound-mode for remote nodes (#667)
* feat: pilot agent outbound-mode for remote nodes Adds a second mode for managing remote nodes: the agent dials an outbound WebSocket tunnel to the primary, so the remote host no longer needs an inbound port, a reachable URL, or its own TLS certificate. Works behind NAT, residential routers, and corporate firewalls. The primary multiplexes HTTP and WebSocket requests over a single tunnel via a hybrid JSON + binary frame protocol, bridged through a per-tunnel loopback server so existing proxy and upgrade handlers route pilot-mode nodes identically to proxy-mode ones. Enrollment uses a single-use 15-minute pilot_enroll JWT exchanged for a long-lived pilot_tunnel credential on first connect. Proxy mode continues to work unchanged and both modes are supported side-by-side. * test(e2e): switch to proxy mode before asserting api_url field Remote nodes default to Pilot Agent mode, which hides the api_url input. The SSRF-validation tests need proxy mode, so the helper now selects Distributed API Proxy after picking Remote type before asserting the field is visible. * fix(e2e): wire Combobox id prop so node-mode selector resolves The Combobox trigger button had no id, leaving its Label orphaned and making getByRole name-based lookups fail. Adding id to the primitive, passing id="node-mode" from NodeManager, and updating the E2E helper to use #node-mode fixes both the a11y regression and the CI timeout.
This commit is contained in:
@@ -3,6 +3,7 @@ import axios from 'axios';
|
||||
import { EventEmitter } from 'events';
|
||||
import { DatabaseService, Node } from './DatabaseService';
|
||||
import { fetchRemoteMeta } from './CapabilityRegistry';
|
||||
import { PilotTunnelManager } from './PilotTunnelManager';
|
||||
|
||||
/**
|
||||
* NodeRegistry: Manages connections for multiple nodes.
|
||||
@@ -99,12 +100,22 @@ export class NodeRegistry extends EventEmitter {
|
||||
/**
|
||||
* Get the HTTP proxy target for a remote node.
|
||||
* Returns { apiUrl, apiToken } for use by the HTTP proxy middleware.
|
||||
*
|
||||
* Pilot-agent nodes resolve to the loopback URL of their active tunnel
|
||||
* bridge; the bridge strips the bearer token and re-authenticates
|
||||
* implicitly via the pre-verified tunnel socket.
|
||||
*/
|
||||
public getProxyTarget(nodeId: number): { apiUrl: string; apiToken: string } | null {
|
||||
const node = DatabaseService.getInstance().getNode(nodeId);
|
||||
if (!node || node.type !== 'remote' || !node.api_url || !node.api_token) {
|
||||
return null;
|
||||
if (!node || node.type !== 'remote') return null;
|
||||
|
||||
if (node.mode === 'pilot_agent') {
|
||||
const loopbackUrl = PilotTunnelManager.getInstance().getLoopbackUrl(nodeId);
|
||||
if (!loopbackUrl) return null;
|
||||
return { apiUrl: loopbackUrl, apiToken: '' };
|
||||
}
|
||||
|
||||
if (!node.api_url || !node.api_token) return null;
|
||||
return { apiUrl: node.api_url, apiToken: node.api_token };
|
||||
}
|
||||
|
||||
@@ -122,12 +133,48 @@ export class NodeRegistry extends EventEmitter {
|
||||
}
|
||||
|
||||
if (node.type === 'remote') {
|
||||
if (node.mode === 'pilot_agent') {
|
||||
return this.testPilotConnection(node);
|
||||
}
|
||||
return this.testRemoteConnection(node);
|
||||
}
|
||||
|
||||
return this.testLocalConnection(nodeId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a pilot-agent node has an active tunnel. Does not call
|
||||
* any endpoint; tunnel liveness is tracked in-process by
|
||||
* PilotTunnelManager and via the JWT handshake that originally accepted
|
||||
* the agent.
|
||||
*/
|
||||
private async testPilotConnection(node: Node): Promise<{ success: boolean; error?: string; info?: any }> {
|
||||
const db = DatabaseService.getInstance();
|
||||
const active = PilotTunnelManager.getInstance().hasActiveTunnel(node.id);
|
||||
if (!active) {
|
||||
db.updateNodeStatus(node.id, 'offline');
|
||||
return { success: false, error: 'Pilot agent is not connected. Start the agent container or regenerate the enrollment token.' };
|
||||
}
|
||||
db.updateNodeStatus(node.id, 'online');
|
||||
return {
|
||||
success: true,
|
||||
info: {
|
||||
name: node.name,
|
||||
serverVersion: 'Pilot Agent',
|
||||
senchoVersion: node.pilot_agent_version ?? null,
|
||||
capabilities: [],
|
||||
os: 'Remote (tunnel)',
|
||||
architecture: 'Remote',
|
||||
containers: '-',
|
||||
containersRunning: '-',
|
||||
images: '-',
|
||||
memTotal: 0,
|
||||
cpus: '-',
|
||||
pilotLastSeen: node.pilot_last_seen ?? null,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private async testLocalConnection(nodeId: number): Promise<{ success: boolean; error?: string; info?: any }> {
|
||||
const db = DatabaseService.getInstance();
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user