From eb1920bf918decd8e49939cf51021e644b2705a1 Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> Date: Tue, 23 Jun 2026 20:01:40 -0300 Subject: [PATCH] refactor(chatCore): extrai runPluginOnRequestHook (#3501) (#4827) Integrated into release/v3.8.36 (#3501 chatCore extraction stack 5/13) --- open-sse/handlers/chatCore.ts | 61 +++++----------- open-sse/handlers/chatCore/pluginOnRequest.ts | 65 +++++++++++++++++ tests/unit/chatcore-plugin-onrequest.test.ts | 73 +++++++++++++++++++ 3 files changed, 156 insertions(+), 43 deletions(-) create mode 100644 open-sse/handlers/chatCore/pluginOnRequest.ts create mode 100644 tests/unit/chatcore-plugin-onrequest.test.ts diff --git a/open-sse/handlers/chatCore.ts b/open-sse/handlers/chatCore.ts index 9a88f30124..8331cd5780 100644 --- a/open-sse/handlers/chatCore.ts +++ b/open-sse/handlers/chatCore.ts @@ -173,6 +173,7 @@ import { } from "./chatCore/compressionComboPredicates.ts"; import { emitOutputStyleTelemetry } from "./chatCore/outputStyleTelemetry.ts"; import { writeCompressionAnalytics } from "./chatCore/compressionAnalyticsWrite.ts"; +import { runPluginOnRequestHook } from "./chatCore/pluginOnRequest.ts"; import { recordContextEditingTelemetryHook } from "./chatCore/contextEditingTelemetry.ts"; import { recordCompressionCacheStats } from "./chatCore/compressionCacheStats.ts"; import { writeCavemanOutputAnalytics } from "./chatCore/cavemanOutputAnalytics.ts"; @@ -385,50 +386,24 @@ export async function handleChatCore({ body = injectSystemPrompt(body); // ── Plugin onRequest hook ── // Dynamic import cached by Node.js after first call — minimal overhead - try { - const { runOnRequest } = await import("@/lib/plugins/hooks"); - const pluginCtx = { - requestId: traceId, - body, - model, - provider, - apiKeyInfo, - metadata: {}, + const pluginGate = await runPluginOnRequestHook({ + requestId: traceId, + body, + model, + provider, + apiKeyInfo, + log, + }); + if (pluginGate.blocked) { + return { + success: false, + status: 403, + error: "Request blocked by plugin", + response: pluginGate.response, }; - const pluginResult = await runOnRequest(pluginCtx); - if (pluginResult?.blocked) { - log?.info?.("PLUGIN", `Request blocked by plugin`); - return { - success: false, - status: 403, - error: "Request blocked by plugin", - response: pluginResult.response - ? new Response(JSON.stringify(pluginResult.response), { - status: 403, - headers: { "Content-Type": "application/json" }, - }) - : new Response( - JSON.stringify({ - error: { message: "Request blocked by plugin", type: "plugin_block" }, - }), - { - status: 403, - headers: { "Content-Type": "application/json" }, - } - ), - }; - } - if (pluginResult?.body) { - body = pluginResult.body; - } - if (pluginResult?.metadata) { - Object.assign(pluginCtx.metadata, pluginResult.metadata); - } - } catch (pluginErr) { - log?.debug?.( - "PLUGIN", - `onRequest hook error (non-fatal): ${pluginErr instanceof Error ? pluginErr.message : String(pluginErr)}` - ); + } + if (pluginGate.body) { + body = pluginGate.body; } let effectiveServiceTier: EffectiveServiceTier = "standard"; diff --git a/open-sse/handlers/chatCore/pluginOnRequest.ts b/open-sse/handlers/chatCore/pluginOnRequest.ts new file mode 100644 index 0000000000..170c276c5e --- /dev/null +++ b/open-sse/handlers/chatCore/pluginOnRequest.ts @@ -0,0 +1,65 @@ +/** + * chatCore plugin onRequest hook (Quality Gate v2 / Fase 9 — chatCore god-file decomposition, + * #3501). + * + * Extracted from handleChatCore's request entry: run the registered plugin `onRequest` hooks. The + * hook may block the request (→ the handler returns a 403), rewrite the body (→ the handler + * reassigns `body`), or do nothing. Fail-open — a misbehaving plugin is logged and ignored. Returns + * a discriminated result so the early-return + body reassignment stay in the handler; behaviour is + * byte-identical to the previous inline block. + */ + +type LoggerLike = + | { info?: (...args: unknown[]) => void; debug?: (...args: unknown[]) => void } + | null + | undefined; + +export type PluginOnRequestGate = + | { blocked: true; response: Response } + | { blocked: false; body?: unknown }; + +const JSON_HEADERS = { status: 403, headers: { "Content-Type": "application/json" } } as const; + +export async function runPluginOnRequestHook(args: { + requestId: string; + body: unknown; + model: string | null | undefined; + provider: string | null | undefined; + apiKeyInfo: unknown; + log?: LoggerLike; +}): Promise { + try { + const { runOnRequest } = await import("@/lib/plugins/hooks"); + const pluginCtx = { + requestId: args.requestId, + body: args.body, + model: args.model, + provider: args.provider, + apiKeyInfo: args.apiKeyInfo, + metadata: {}, + }; + const pluginResult = await runOnRequest(pluginCtx); + if (pluginResult?.blocked) { + args.log?.info?.("PLUGIN", `Request blocked by plugin`); + const response = pluginResult.response + ? new Response(JSON.stringify(pluginResult.response), JSON_HEADERS) + : new Response( + JSON.stringify({ + error: { message: "Request blocked by plugin", type: "plugin_block" }, + }), + JSON_HEADERS + ); + return { blocked: true, response }; + } + if (pluginResult?.metadata) { + Object.assign(pluginCtx.metadata, pluginResult.metadata); + } + return { blocked: false, body: pluginResult?.body }; + } catch (pluginErr) { + args.log?.debug?.( + "PLUGIN", + `onRequest hook error (non-fatal): ${pluginErr instanceof Error ? pluginErr.message : String(pluginErr)}` + ); + return { blocked: false }; + } +} diff --git a/tests/unit/chatcore-plugin-onrequest.test.ts b/tests/unit/chatcore-plugin-onrequest.test.ts new file mode 100644 index 0000000000..ed343e006a --- /dev/null +++ b/tests/unit/chatcore-plugin-onrequest.test.ts @@ -0,0 +1,73 @@ +// Characterization of runPluginOnRequestHook — the plugin onRequest gate extracted from +// handleChatCore's request entry (chatCore god-file decomposition, #3501). Hooks are in-memory. +// Locks: the discriminated result — blocked (403 Response) vs body-rewrite vs pass-through — and +// fail-open on a throwing hook. +import { test, afterEach } from "node:test"; +import assert from "node:assert/strict"; + +const { registerHook, unregisterHook } = await import("../../src/lib/plugins/hooks.ts"); +const { runPluginOnRequestHook } = await import( + "../../open-sse/handlers/chatCore/pluginOnRequest.ts" +); + +const PLUGIN = "test-onrequest-plugin"; + +afterEach(() => { + unregisterHook("onRequest", PLUGIN); +}); + +function baseArgs(overrides: Record = {}) { + return { + requestId: "req-1", + body: { messages: [{ role: "user", content: "hi" }] }, + model: "gpt-x", + provider: "openai", + apiKeyInfo: null, + ...overrides, + } as Parameters[0]; +} + +test("no registered hooks → pass-through (blocked:false, no body)", async () => { + const gate = await runPluginOnRequestHook(baseArgs()); + assert.equal(gate.blocked, false); +}); + +test("a blocking hook → blocked:true with a 403 JSON Response", async () => { + registerHook("onRequest", PLUGIN, async () => ({ + blocked: true, + response: { error: "nope" }, + })); + const gate = await runPluginOnRequestHook(baseArgs()); + assert.equal(gate.blocked, true); + if (!gate.blocked) return; + assert.equal(gate.response.status, 403); + const payload = await gate.response.json(); + assert.deepEqual(payload, { error: "nope" }); +}); + +test("a blocking hook without a response → generic plugin_block 403", async () => { + registerHook("onRequest", PLUGIN, async () => ({ blocked: true })); + const gate = await runPluginOnRequestHook(baseArgs()); + assert.equal(gate.blocked, true); + if (!gate.blocked) return; + assert.equal(gate.response.status, 403); + const payload = (await gate.response.json()) as { error?: { type?: string } }; + assert.equal(payload.error?.type, "plugin_block"); +}); + +test("a body-rewriting hook → blocked:false with the new body", async () => { + const rewritten = { messages: [{ role: "user", content: "rewritten" }] }; + registerHook("onRequest", PLUGIN, async () => ({ body: rewritten })); + const gate = await runPluginOnRequestHook(baseArgs()); + assert.equal(gate.blocked, false); + if (gate.blocked) return; + assert.deepEqual(gate.body, rewritten); +}); + +test("a throwing hook → fail-open pass-through (blocked:false)", async () => { + registerHook("onRequest", PLUGIN, async () => { + throw new Error("boom"); + }); + const gate = await runPluginOnRequestHook(baseArgs()); + assert.equal(gate.blocked, false); +});