feat(gateway): add node capability negotiation foundation
This commit is contained in:
+49
-2
@@ -8,7 +8,7 @@ import type { SessionBridgeConfig } from './session-bridge.js';
|
||||
import { LaneQueue } from './lane-queue.js';
|
||||
import type { LaneQueueConfig } from './lane-queue.js';
|
||||
import { MetricsCollector } from './metrics.js';
|
||||
import { authenticateRequest } from './auth.js';
|
||||
import { authenticateRequest, authorizeNodeMethod } from './auth.js';
|
||||
import type { AuthConfig } from './auth.js';
|
||||
import { startGatewayDiscovery, type GatewayDiscoveryHandle } from './discovery.js';
|
||||
import {
|
||||
@@ -28,9 +28,11 @@ import {
|
||||
createIntentHandlers,
|
||||
createRoutingHandlers,
|
||||
createHistoryHandlers,
|
||||
createNodeHandlers,
|
||||
} from './handlers/index.js';
|
||||
import { discoverServices } from './handlers/services.js';
|
||||
import type { TokenUsageEntry } from './handlers/system.js';
|
||||
import type { NodeConnectionState } from './handlers/node.js';
|
||||
import type { SessionManager } from '../session/manager.js';
|
||||
import type { Config } from '../config/index.js';
|
||||
import type { ToolRegistry } from '../tools/registry.js';
|
||||
@@ -91,6 +93,11 @@ export interface GatewayServerConfig {
|
||||
sessions?: Record<string, Partial<LaneQueueConfig>>;
|
||||
};
|
||||
};
|
||||
nodes?: {
|
||||
enabled: boolean;
|
||||
allowedRoles: string[];
|
||||
featureGates: Record<string, boolean>;
|
||||
};
|
||||
/** Optional pairing manager for DM pairing code management via gateway. */
|
||||
pairingManager?: PairingManager;
|
||||
memoryStore?: MemoryStore;
|
||||
@@ -134,6 +141,7 @@ export class GatewayServer {
|
||||
violations: number;
|
||||
windowStartMs: number;
|
||||
}> = new Map();
|
||||
private connectionStateMap: Map<string, NodeConnectionState> = new Map();
|
||||
private config: GatewayServerConfig;
|
||||
private startTime: number = Date.now();
|
||||
|
||||
@@ -269,6 +277,23 @@ export class GatewayServer {
|
||||
routingPolicy: this.config.routingPolicy,
|
||||
});
|
||||
|
||||
const nodeHandlers = createNodeHandlers({
|
||||
enabled: this.config.nodes?.enabled ?? false,
|
||||
allowedRoles: this.config.nodes?.allowedRoles ?? [],
|
||||
featureGates: this.config.nodes?.featureGates ?? {},
|
||||
getConnectionState: (connectionId) => this.connectionStateMap.get(connectionId),
|
||||
setNodeRegistration: (connectionId, registration) => {
|
||||
const existing = this.connectionStateMap.get(connectionId);
|
||||
if (!existing) {
|
||||
return;
|
||||
}
|
||||
this.connectionStateMap.set(connectionId, {
|
||||
...existing,
|
||||
node: registration,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
// Config handlers (only if config object is provided)
|
||||
if (this.config.config) {
|
||||
const configHandlers = createConfigHandlers({
|
||||
@@ -310,6 +335,9 @@ export class GatewayServer {
|
||||
for (const [method, handler] of Object.entries(routingHandlers)) {
|
||||
this.router.register(method, handler);
|
||||
}
|
||||
for (const [method, handler] of Object.entries(nodeHandlers)) {
|
||||
this.router.register(method, handler);
|
||||
}
|
||||
}
|
||||
|
||||
async start(): Promise<void> {
|
||||
@@ -370,6 +398,7 @@ export class GatewayServer {
|
||||
ws.close(1001, 'Server shutting down');
|
||||
}
|
||||
this.connectionMap.clear();
|
||||
this.connectionStateMap.clear();
|
||||
|
||||
// Close WSS first, then the underlying HTTP server
|
||||
await new Promise<void>((resolve) => {
|
||||
@@ -395,7 +424,7 @@ export class GatewayServer {
|
||||
});
|
||||
}
|
||||
|
||||
private handleConnection(ws: WebSocket, _identity?: string): void {
|
||||
private handleConnection(ws: WebSocket, identity?: string): void {
|
||||
// Gateway lock — reject if another client is already connected
|
||||
if (this.config.lock && this.connectionMap.size > 0) {
|
||||
ws.close(4003, 'Gateway locked — another client is already connected');
|
||||
@@ -405,6 +434,7 @@ export class GatewayServer {
|
||||
const connectionId = randomUUID();
|
||||
this.sessionBridge.connect(connectionId);
|
||||
this.connectionMap.set(ws, connectionId);
|
||||
this.connectionStateMap.set(connectionId, { identity });
|
||||
this.connectionRateMap.set(connectionId, {
|
||||
tokens: this.getWsRateConfig().capacity,
|
||||
lastRefillMs: Date.now(),
|
||||
@@ -429,6 +459,7 @@ export class GatewayServer {
|
||||
this.sessionBridge.disconnect(connectionId);
|
||||
this.connectionMap.delete(ws);
|
||||
this.connectionRateMap.delete(connectionId);
|
||||
this.connectionStateMap.delete(connectionId);
|
||||
});
|
||||
|
||||
ws.on('error', (err) => {
|
||||
@@ -598,6 +629,22 @@ export class GatewayServer {
|
||||
if (!request.params) {request.params = {};}
|
||||
request.params.connectionId = connectionId;
|
||||
|
||||
const nodeAuth = authorizeNodeMethod({
|
||||
enabled: this.config.nodes?.enabled ?? false,
|
||||
method: request.method,
|
||||
nodeRole: this.connectionStateMap.get(connectionId)?.node?.role,
|
||||
allowedRoles: this.config.nodes?.allowedRoles ?? [],
|
||||
roleScopes: {
|
||||
companion: ['node.capabilities.get'],
|
||||
observer: ['node.capabilities.get'],
|
||||
automation: ['node.capabilities.get'],
|
||||
},
|
||||
});
|
||||
if (!nodeAuth.authenticated) {
|
||||
this.send(ws, makeError(request.id, ErrorCode.AuthFailed, nodeAuth.error ?? 'Node authorization failed'));
|
||||
return;
|
||||
}
|
||||
|
||||
const send = (msg: OutboundMessage) => this.send(ws, msg);
|
||||
const response = await this.router.dispatch(request, send);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user