feat(gateway): add node capability negotiation foundation

This commit is contained in:
William Valentin
2026-02-16 12:14:25 -08:00
parent de0c1f41b3
commit d9f7807ab2
17 changed files with 675 additions and 7 deletions
+129
View File
@@ -0,0 +1,129 @@
import type { GatewayRequest, OutboundMessage } from '../protocol.js';
import { makeError, makeResponse, ErrorCode, GATEWAY_PROTOCOL_VERSION, parseNodeRegisterParams } from '../protocol.js';
export interface NodeRegistration {
nodeId: string;
role: string;
protocolVersion: number;
capabilities: string[];
registeredAt: number;
}
export interface NodeConnectionState {
identity?: string;
node?: NodeRegistration;
}
export interface NodeHandlerDeps {
enabled: boolean;
allowedRoles: string[];
featureGates: Record<string, boolean>;
getConnectionState: (connectionId: string) => NodeConnectionState | undefined;
setNodeRegistration: (connectionId: string, registration: NodeRegistration) => void;
}
export function createNodeHandlers(deps: NodeHandlerDeps) {
return {
'node.register': async (request: GatewayRequest): Promise<OutboundMessage> => {
if (!deps.enabled) {
return makeError(request.id, ErrorCode.AuthFailed, 'Node RPC is disabled');
}
const parsed = parseNodeRegisterParams(request.params);
if (!parsed) {
return makeError(request.id, ErrorCode.InvalidRequest, 'Invalid node.register params');
}
if (deps.allowedRoles.length > 0 && !deps.allowedRoles.includes(parsed.role)) {
return makeError(request.id, ErrorCode.AuthFailed, `Node role '${parsed.role}' is not allowed`);
}
const negotiatedVersion = Math.min(GATEWAY_PROTOCOL_VERSION, parsed.protocolVersion);
if (negotiatedVersion < 1) {
return makeError(request.id, ErrorCode.InvalidRequest, 'Unsupported protocolVersion');
}
const dedupedCapabilities = Array.from(new Set(parsed.capabilities.map((entry) => entry.trim()).filter(Boolean)));
deps.setNodeRegistration(parsed.connectionId, {
nodeId: parsed.nodeId,
role: parsed.role,
protocolVersion: parsed.protocolVersion,
capabilities: dedupedCapabilities,
registeredAt: Date.now(),
});
const enabledCapabilities = dedupedCapabilities.filter((capability) => deps.featureGates[capability] !== false);
return makeResponse(request.id, {
registered: true,
node: {
id: parsed.nodeId,
role: parsed.role,
},
protocol: {
serverVersion: GATEWAY_PROTOCOL_VERSION,
clientVersion: parsed.protocolVersion,
negotiatedVersion,
},
capabilities: {
declared: dedupedCapabilities,
enabled: enabledCapabilities,
},
});
},
'node.capabilities.get': async (request: GatewayRequest): Promise<OutboundMessage> => {
if (!deps.enabled) {
return makeError(request.id, ErrorCode.AuthFailed, 'Node RPC is disabled');
}
const params = request.params as { connectionId?: string } | undefined;
const connectionId = params?.connectionId;
if (!connectionId) {
return makeError(request.id, ErrorCode.InvalidRequest, 'connectionId is required');
}
const state = deps.getConnectionState(connectionId);
if (!state?.node) {
return makeError(request.id, ErrorCode.AuthFailed, 'Node is not registered for this connection');
}
const enabledCapabilities = state.node.capabilities.filter((capability) => deps.featureGates[capability] !== false);
return makeResponse(request.id, {
protocol: {
serverVersion: GATEWAY_PROTOCOL_VERSION,
nodeVersion: state.node.protocolVersion,
negotiatedVersion: Math.min(GATEWAY_PROTOCOL_VERSION, state.node.protocolVersion),
},
node: {
id: state.node.nodeId,
role: state.node.role,
registeredAt: state.node.registeredAt,
},
capabilities: {
declared: state.node.capabilities,
enabled: enabledCapabilities,
featureGates: deps.featureGates,
},
});
},
'system.capabilities': async (request: GatewayRequest): Promise<OutboundMessage> => {
const params = request.params as { connectionId?: string } | undefined;
const connectionId = params?.connectionId;
const state = connectionId ? deps.getConnectionState(connectionId) : undefined;
return makeResponse(request.id, {
protocol: {
version: GATEWAY_PROTOCOL_VERSION,
},
nodes: {
enabled: deps.enabled,
allowedRoles: deps.allowedRoles,
registered: Boolean(state?.node),
role: state?.node?.role,
nodeId: state?.node?.nodeId,
},
featureGates: deps.featureGates,
});
},
};
}