refactor(gateway): unify control-ui and plugin webhook routing

This commit is contained in:
Peter Steinberger
2026-03-02 16:17:31 +00:00
parent 21708f58ce
commit b13d48987c
17 changed files with 870 additions and 425 deletions

View File

@@ -146,6 +146,22 @@ function writeUpgradeAuthFailure(
export type HooksRequestHandler = (req: IncomingMessage, res: ServerResponse) => Promise<boolean>;
type GatewayHttpRequestStage = {
name: string;
run: () => Promise<boolean> | boolean;
};
async function runGatewayHttpRequestStages(
stages: readonly GatewayHttpRequestStage[],
): Promise<boolean> {
for (const stage of stages) {
if (await stage.run()) {
return true;
}
}
return false;
}
export function createHooksRequestHandler(
opts: {
getHooksConfig: () => HooksConfigResolved | null;
@@ -429,113 +445,144 @@ export function createGatewayHttpServer(opts: {
req.url = scopedCanvas.rewrittenUrl;
}
const requestPath = new URL(req.url ?? "/", "http://localhost").pathname;
if (await handleHooksRequest(req, res)) {
return;
}
if (
await handleToolsInvokeHttpRequest(req, res, {
auth: resolvedAuth,
trustedProxies,
allowRealIpFallback,
rateLimiter,
})
) {
return;
}
if (await handleSlackHttpRequest(req, res)) {
return;
}
const requestStages: GatewayHttpRequestStage[] = [
{
name: "hooks",
run: () => handleHooksRequest(req, res),
},
{
name: "tools-invoke",
run: () =>
handleToolsInvokeHttpRequest(req, res, {
auth: resolvedAuth,
trustedProxies,
allowRealIpFallback,
rateLimiter,
}),
},
{
name: "slack",
run: () => handleSlackHttpRequest(req, res),
},
];
if (openResponsesEnabled) {
if (
await handleOpenResponsesHttpRequest(req, res, {
auth: resolvedAuth,
config: openResponsesConfig,
trustedProxies,
allowRealIpFallback,
rateLimiter,
})
) {
return;
}
requestStages.push({
name: "openresponses",
run: () =>
handleOpenResponsesHttpRequest(req, res, {
auth: resolvedAuth,
config: openResponsesConfig,
trustedProxies,
allowRealIpFallback,
rateLimiter,
}),
});
}
if (openAiChatCompletionsEnabled) {
if (
await handleOpenAiHttpRequest(req, res, {
auth: resolvedAuth,
trustedProxies,
allowRealIpFallback,
rateLimiter,
})
) {
return;
}
requestStages.push({
name: "openai",
run: () =>
handleOpenAiHttpRequest(req, res, {
auth: resolvedAuth,
trustedProxies,
allowRealIpFallback,
rateLimiter,
}),
});
}
if (canvasHost) {
if (isCanvasPath(requestPath)) {
const ok = await authorizeCanvasRequest({
req,
auth: resolvedAuth,
trustedProxies,
allowRealIpFallback,
clients,
canvasCapability: scopedCanvas.capability,
malformedScopedPath: scopedCanvas.malformedScopedPath,
rateLimiter,
});
if (!ok.ok) {
sendGatewayAuthFailure(res, ok);
return;
}
}
if (await handleA2uiHttpRequest(req, res)) {
return;
}
if (await canvasHost.handleHttpRequest(req, res)) {
return;
}
requestStages.push({
name: "canvas-auth",
run: async () => {
if (!isCanvasPath(requestPath)) {
return false;
}
const ok = await authorizeCanvasRequest({
req,
auth: resolvedAuth,
trustedProxies,
allowRealIpFallback,
clients,
canvasCapability: scopedCanvas.capability,
malformedScopedPath: scopedCanvas.malformedScopedPath,
rateLimiter,
});
if (!ok.ok) {
sendGatewayAuthFailure(res, ok);
return true;
}
return false;
},
});
requestStages.push({
name: "a2ui",
run: () => handleA2uiHttpRequest(req, res),
});
requestStages.push({
name: "canvas-http",
run: () => canvasHost.handleHttpRequest(req, res),
});
}
if (controlUiEnabled) {
if (
handleControlUiAvatarRequest(req, res, {
basePath: controlUiBasePath,
resolveAvatar: (agentId) => resolveAgentAvatar(configSnapshot, agentId),
})
) {
return;
}
if (
handleControlUiHttpRequest(req, res, {
basePath: controlUiBasePath,
config: configSnapshot,
root: controlUiRoot,
})
) {
return;
}
requestStages.push({
name: "control-ui-avatar",
run: () =>
handleControlUiAvatarRequest(req, res, {
basePath: controlUiBasePath,
resolveAvatar: (agentId) => resolveAgentAvatar(configSnapshot, agentId),
}),
});
requestStages.push({
name: "control-ui-http",
run: () =>
handleControlUiHttpRequest(req, res, {
basePath: controlUiBasePath,
config: configSnapshot,
root: controlUiRoot,
}),
});
}
// Plugins run after built-in gateway routes so core surfaces keep
// precedence on overlapping paths.
if (handlePluginRequest) {
if (
(shouldEnforcePluginGatewayAuth ?? shouldEnforceDefaultPluginGatewayAuth)(requestPath)
) {
const pluginAuthOk = await enforcePluginRouteGatewayAuth({
req,
res,
auth: resolvedAuth,
trustedProxies,
allowRealIpFallback,
rateLimiter,
});
if (!pluginAuthOk) {
return;
}
}
if (await handlePluginRequest(req, res)) {
return;
}
requestStages.push({
name: "plugin-auth",
run: async () => {
if (
!(shouldEnforcePluginGatewayAuth ?? shouldEnforceDefaultPluginGatewayAuth)(
requestPath,
)
) {
return false;
}
const pluginAuthOk = await enforcePluginRouteGatewayAuth({
req,
res,
auth: resolvedAuth,
trustedProxies,
allowRealIpFallback,
rateLimiter,
});
if (!pluginAuthOk) {
return true;
}
return false;
},
});
requestStages.push({
name: "plugin-http",
run: () => handlePluginRequest(req, res),
});
}
if (handleGatewayProbeRequest(req, res, requestPath)) {
requestStages.push({
name: "gateway-probes",
run: () => handleGatewayProbeRequest(req, res, requestPath),
});
if (await runGatewayHttpRequestStages(requestStages)) {
return;
}