Compare commits

..

5 Commits

9 changed files with 200 additions and 311 deletions

View File

@@ -97,10 +97,6 @@ _Living section — cycle opened at the v3.8.50 freeze (parallel-cycle model). B
### 🐛 Bug Fixes
- **security(streaming):** sanitize generic mid-stream error messages before emitting OpenAI,
Responses, or Claude SSE failure frames and before diagnostic logging, while preserving raw
failures for internal classification and keeping client disconnects out of provider failure state.
### 📝 Maintenance
---

View File

@@ -5,6 +5,7 @@ import { sanitizeErrorMessage } from "../utils/error.ts";
const ADAPTA_APP_URL = "https://agent.adapta.one";
const ADAPTA_CLERK_URL = "https://clerk.agent.adapta.one";
const ADAPTA_STREAM_URL = `${ADAPTA_APP_URL}/api/chat/stream/v1`;
const ADAPTA_PUBLIC_STREAM_ERROR = `\n\n[Erro: ${sanitizeErrorMessage("Adapta upstream error")}]`;
// Default model ID in Adapta's internal system (corresponds to "ONE" / auto-select)
const DEFAULT_AI_MODEL_ID = 14;
@@ -308,10 +309,9 @@ function transformStream(adaptaStream: ReadableStream, model: string): ReadableS
if (event.id === "quick-response") continue;
// Real text ended — stream will send more events or close
} else if (type === "error") {
const errText = String(event.errorText ?? "Adapta upstream error");
ensureRole();
// Emit the error as content so the user sees it
chunk({ content: `\n\n[Erro: ${errText}]` });
// Keep upstream diagnostics private: the transformed SSE is a public HTTP 200 body.
chunk({ content: ADAPTA_PUBLIC_STREAM_ERROR });
finalize();
return;
} else if (type === "done" || type === "end") {

View File

@@ -1,7 +1,6 @@
import { trackPendingRequest } from "@/lib/usageDb";
import { STREAM_IDLE_TIMEOUT_MS } from "../config/constants.ts";
import { FORMATS } from "../translator/formats.ts";
import { buildErrorBody } from "./error.ts";
import { PENDING_REQUEST_CLEARED_MARKER } from "./stream.ts";
import { createCompletedResponsesToolHandoffWatcher } from "./responsesToolHandoff.ts";
import { createStreamContentWatcher, type StreamContentWatcher } from "./streamReadiness.ts";
@@ -188,10 +187,6 @@ function getErrorStatusCode(error: unknown): number {
return 502;
}
function getPublicErrorMessage(errorMsg: string, statusCode: number): string {
return buildErrorBody(statusCode, errorMsg).error.message;
}
function isDeadlineAbortReason(reason: unknown): reason is Error {
return (
reason instanceof Error &&
@@ -411,7 +406,7 @@ export function createStreamController({
}
if (error instanceof Error) {
logStream(`error: ${getPublicErrorMessage(error.message, getErrorStatusCode(error))}`);
logStream(`error: ${error.message}`);
return;
}
logStream("error: unknown");
@@ -457,7 +452,6 @@ export function buildStreamErrorChunks(
clientResponseFormat?: string | null
) {
const statusMapping = getStreamErrorStatusMapping(statusCode);
const publicErrorMessage = getPublicErrorMessage(errorMsg, statusCode);
if (isResponsesClientFormat(clientResponseFormat)) {
const errorEvent = {
@@ -466,7 +460,7 @@ export function buildStreamErrorChunks(
id: null,
status: "failed",
error: {
message: publicErrorMessage,
message: errorMsg,
type: statusMapping.responses.type,
code: statusMapping.responses.code,
},
@@ -481,7 +475,7 @@ export function buildStreamErrorChunks(
type: "error",
error: {
type: statusMapping.claude.type,
message: publicErrorMessage,
message: errorMsg,
},
};
@@ -504,7 +498,7 @@ export function buildStreamErrorChunks(
},
],
error: {
message: publicErrorMessage,
message: errorMsg,
type: statusMapping.responses.type,
code: statusMapping.responses.code,
},

View File

@@ -0,0 +1,69 @@
import assert from "node:assert/strict";
import test from "node:test";
assert.ok(process.env.DATA_DIR, "the subprocess fixture requires an isolated DATA_DIR");
assert.ok(
process.env.OMNIROUTE_PLUGINS_DIR,
"the subprocess fixture requires an isolated OMNIROUTE_PLUGINS_DIR"
);
assert.equal(process.env.HOME, undefined, "the subprocess must not inherit HOME");
assert.equal(process.env.CODEX_HOME, undefined, "the subprocess must not inherit CODEX_HOME");
const { AdaptaWebExecutor } = await import("../../open-sse/executors/adapta-web.ts");
const originalFetch = globalThis.fetch;
test.afterEach(() => {
globalThis.fetch = originalFetch;
});
test("terminates an upstream error event without exposing its text in the public SSE", async () => {
const hostileError =
"SQLSTATE 42P01 at /srv/omniroute/private.ts:91 — Authorization: Bearer secret-token";
const requestedUrls: string[] = [];
const logMessages: string[] = [];
globalThis.fetch = (async (input: RequestInfo | URL) => {
const url = String(input);
requestedUrls.push(url);
if (url.endsWith("/v1/client")) {
return Response.json({
response: { sessions: [{ id: "session-stream-error", status: "active" }] },
});
}
if (url.includes("/tokens")) {
return Response.json({ jwt: "eyJ.test-session.jwt" });
}
return new Response(`data: ${JSON.stringify({ type: "error", errorText: hostileError })}\n\n`, {
status: 200,
headers: { "Content-Type": "text/event-stream" },
});
}) as typeof fetch;
const executor = new AdaptaWebExecutor();
const result = await executor.execute({
model: "adapta-one",
body: { messages: [{ role: "user", content: "hello" }] },
stream: true,
credentials: { apiKey: "__client=unique-stream-error-cookie" },
signal: null,
log: {
info: (_tag, message) => logMessages.push(message),
warn: (_tag, message) => logMessages.push(message),
},
});
assert.equal(result.response.status, 200);
assert.equal(result.response.headers.get("content-type"), "text/event-stream");
const publicSse = await result.response.text();
assert.equal(requestedUrls.length, 3);
assert.match(publicSse, /"content":"\\n\\n\[Erro: Adapta upstream error\]"/);
assert.match(publicSse, /"finish_reason":"stop"/);
assert.match(publicSse, /data: \[DONE\]/);
assert.doesNotMatch(publicSse, /SQLSTATE|\/srv\/omniroute|secret-token/);
assert.doesNotMatch(logMessages.join("\n"), /SQLSTATE|\/srv\/omniroute|secret-token/);
});

View File

@@ -1,211 +0,0 @@
// This suite owns process-wide DATA_DIR, plugin, logger, and DB state. It must run only inside
// the subprocess launched by tests/unit/stream-handler-public-error-boundary.test.ts.
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import test from "node:test";
const originalDataDir = process.env.DATA_DIR;
const originalPluginsDir = process.env.OMNIROUTE_PLUGINS_DIR;
const testRoot = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-stream-public-error-"));
const TEST_DATA_DIR = path.join(testRoot, "data");
const TEST_PLUGINS_DIR = path.join(testRoot, "plugins");
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
fs.mkdirSync(TEST_PLUGINS_DIR, { recursive: true });
process.env.DATA_DIR = TEST_DATA_DIR;
process.env.OMNIROUTE_PLUGINS_DIR = TEST_PLUGINS_DIR;
const [core, callLogs, artifactWriter, loggerResource, streamHandler, { FORMATS }] =
await Promise.all([
import("../../src/lib/db/core.ts"),
import("../../src/lib/usage/callLogs.ts"),
import("../../src/lib/usage/callLogArtifactWriter.ts"),
import("../../src/shared/utils/loggerResource.ts"),
import("../../open-sse/utils/streamHandler.ts"),
import("../../open-sse/translator/formats.ts"),
]);
const { createStreamController, pipeWithDisconnect } = streamHandler;
const SECRET = "sk-live-streamhandler-secret-123456";
const API_KEY = "provider-key-streamhandler-654321";
const PRIVATE_PATH = "/srv/omniroute/private/provider.ts:42:9";
const RAW_MESSAGE =
`Upstream failed at ${PRIVATE_PATH} Authorization: Bearer ${SECRET} api_key=${API_KEY}` +
`\n at dispatch (/srv/omniroute/private/dispatcher.ts:88:3)`;
test.after(async () => {
assert.equal(await callLogs.waitForCallLogSaves(3_000), true);
await artifactWriter.closeCallLogArtifactWriter();
core.resetDbInstance();
await loggerResource.closeSharedLoggerResource();
if (originalDataDir === undefined) delete process.env.DATA_DIR;
else process.env.DATA_DIR = originalDataDir;
if (originalPluginsDir === undefined) delete process.env.OMNIROUTE_PLUGINS_DIR;
else process.env.OMNIROUTE_PLUGINS_DIR = originalPluginsDir;
fs.rmSync(testRoot, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
});
test("fixture binds all persistent state to its process-owned directories", () => {
assert.equal(core.DATA_DIR, TEST_DATA_DIR);
assert.equal(core.SQLITE_FILE, path.join(TEST_DATA_DIR, "storage.sqlite"));
assert.equal(process.env.DATA_DIR, TEST_DATA_DIR);
assert.equal(process.env.OMNIROUTE_PLUGINS_DIR, TEST_PLUGINS_DIR);
assert.equal(fs.existsSync(TEST_DATA_DIR), true);
assert.equal(fs.existsSync(TEST_PLUGINS_DIR), true);
});
test("OpenAI stream failures keep raw diagnostics internal and sanitize the public wire", async () => {
const upstreamError = Object.assign(new Error(RAW_MESSAGE), { statusCode: 502 });
const source = new ReadableStream<Uint8Array>({
start(controller) {
controller.error(upstreamError);
},
});
let internalMessage = "";
const stream = pipeWithDisconnect(
new Response(source),
new TransformStream<Uint8Array, Uint8Array>(),
createStreamController({
clientResponseFormat: FORMATS.OPENAI,
onError(event) {
internalMessage = event.message;
return true;
},
}),
{ stallTimeoutMs: 0 }
);
const publicWire = await new Response(stream).text();
assert.equal(internalMessage, RAW_MESSAGE, "failure classification must retain the raw message");
assert.match(publicWire, /"finish_reason":"error"/);
assert.match(publicWire, /"code":"server_error"/);
assert.match(publicWire, /\[DONE\]/);
assert.doesNotMatch(publicWire, new RegExp(SECRET));
assert.doesNotMatch(publicWire, new RegExp(API_KEY));
assert.doesNotMatch(publicWire, /\/srv\/omniroute\/private/);
assert.doesNotMatch(publicWire, /dispatcher\.ts/);
assert.match(publicWire, /Authorization: \[REDACTED\]/);
assert.match(publicWire, /<path>/);
});
test("Responses stream failures preserve the failure event shape without leaking diagnostics", async () => {
const upstreamError = Object.assign(new Error(RAW_MESSAGE), { statusCode: 429 });
const source = new ReadableStream<Uint8Array>({
start(controller) {
controller.error(upstreamError);
},
});
let internalError: unknown;
const stream = pipeWithDisconnect(
new Response(source),
new TransformStream<Uint8Array, Uint8Array>(),
createStreamController({
clientResponseFormat: FORMATS.OPENAI_RESPONSES,
onError(event) {
internalError = event.error;
return true;
},
}),
{ stallTimeoutMs: 0 }
);
const publicWire = await new Response(stream).text();
assert.equal(internalError, upstreamError, "the original error object must reach classification");
assert.match(publicWire, /event: response\.failed/);
assert.match(publicWire, /"type":"response\.failed"/);
assert.match(publicWire, /"type":"rate_limit_error"/);
assert.match(publicWire, /"code":"rate_limit_exceeded"/);
assert.doesNotMatch(publicWire, new RegExp(SECRET));
assert.doesNotMatch(publicWire, new RegExp(API_KEY));
assert.doesNotMatch(publicWire, /\/srv\/omniroute\/private/);
assert.doesNotMatch(publicWire, /dispatcher\.ts/);
assert.match(publicWire, /Authorization: \[REDACTED\]/);
assert.match(publicWire, /<path>/);
});
test("Claude stream failures preserve error and stop events without leaking diagnostics", async () => {
const upstreamError = Object.assign(new Error(RAW_MESSAGE), { statusCode: 403 });
const source = new ReadableStream<Uint8Array>({
start(controller) {
controller.error(upstreamError);
},
});
let internalStatusCode = 0;
const stream = pipeWithDisconnect(
new Response(source),
new TransformStream<Uint8Array, Uint8Array>(),
createStreamController({
clientResponseFormat: FORMATS.CLAUDE,
onError(event) {
internalStatusCode = event.statusCode;
return true;
},
}),
{ stallTimeoutMs: 0 }
);
const publicWire = await new Response(stream).text();
assert.equal(internalStatusCode, 403);
assert.match(publicWire, /event: error/);
assert.match(publicWire, /"type":"permission_error"/);
assert.match(publicWire, /event: message_stop/);
assert.doesNotMatch(publicWire, new RegExp(SECRET));
assert.doesNotMatch(publicWire, new RegExp(API_KEY));
assert.doesNotMatch(publicWire, /\/srv\/omniroute\/private/);
assert.doesNotMatch(publicWire, /dispatcher\.ts/);
assert.match(publicWire, /Authorization: \[REDACTED\]/);
assert.match(publicWire, /<path>/);
});
test("stream diagnostics sanitize logs while callbacks retain the original failure", () => {
const upstreamError = Object.assign(new Error(RAW_MESSAGE), { statusCode: 502 });
const originalLog = console.log;
const logLines: string[] = [];
let internalError: unknown;
console.log = (...args: unknown[]) => {
logLines.push(args.map(String).join(" "));
};
try {
createStreamController({
provider: "test-provider",
model: "test-model",
onError(event) {
internalError = event.error;
return true;
},
}).handleError(upstreamError);
} finally {
console.log = originalLog;
}
const logs = logLines.join("\n");
assert.equal(internalError, upstreamError);
assert.match(logs, /error: Upstream failed at <path>/);
assert.match(logs, /Authorization: \[REDACTED\]/);
assert.doesNotMatch(logs, new RegExp(SECRET));
assert.doesNotMatch(logs, new RegExp(API_KEY));
assert.doesNotMatch(logs, /\/srv\/omniroute\/private/);
assert.doesNotMatch(logs, /dispatcher\.ts/);
});
test("client disconnects stay outside the provider-failure callback", () => {
let providerFailureRecorded = false;
const controller = createStreamController({
onError() {
providerFailureRecorded = true;
return true;
},
});
controller.handleError(new DOMException("request_signal_aborted", "AbortError"));
assert.equal(providerFailureRecorded, false);
assert.equal(controller.signal.aborted, false);
});

View File

@@ -0,0 +1,97 @@
import assert from "node:assert/strict";
import { spawn } from "node:child_process";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";
import test from "node:test";
const FIXTURE = fileURLToPath(
new URL("../fixtures/adapta-web-stream-error-boundary.fixture.ts", import.meta.url)
);
const REPO_ROOT = fileURLToPath(new URL("../..", import.meta.url));
const SYNTHETIC_API_KEY_SECRET =
"adapta-stream-boundary-test-secret-00000000000000000000000000000000";
type FixtureResult = {
code: number | null;
signal: NodeJS.Signals | null;
stdout: string;
stderr: string;
};
function runIsolatedFixture(testRoot: string): Promise<FixtureResult> {
const dataDir = path.join(testRoot, "data");
const pluginsDir = path.join(testRoot, "plugins");
fs.mkdirSync(dataDir, { recursive: true });
fs.mkdirSync(pluginsDir, { recursive: true });
const childEnv: NodeJS.ProcessEnv = {
PATH: process.env.PATH,
NODE_PATH: process.env.NODE_PATH,
LANG: process.env.LANG ?? "C.UTF-8",
LC_ALL: process.env.LC_ALL,
TZ: process.env.TZ ?? "UTC",
TMPDIR: process.env.TMPDIR ?? os.tmpdir(),
NODE_ENV: "test",
APP_LOG_TO_FILE: "false",
API_KEY_SECRET: SYNTHETIC_API_KEY_SECRET,
DATA_DIR: dataDir,
OMNIROUTE_PLUGINS_DIR: pluginsDir,
};
delete childEnv.NODE_TEST_CONTEXT;
return new Promise((resolve, reject) => {
const child = spawn(process.execPath, ["--import", "tsx/esm", "--test", FIXTURE], {
cwd: REPO_ROOT,
env: childEnv,
stdio: ["ignore", "pipe", "pipe"],
timeout: 90_000,
});
let stdout = "";
let stderr = "";
child.stdout.setEncoding("utf8").on("data", (chunk: string) => {
stdout += chunk;
});
child.stderr.setEncoding("utf8").on("data", (chunk: string) => {
stderr += chunk;
});
child.once("error", reject);
child.once("close", (code, signal) => resolve({ code, signal, stdout, stderr }));
});
}
test("Adapta stream errors are sanitized in an isolated executor fixture", async () => {
const testRoot = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-adapta-boundary-parent-"));
const originalDataDir = process.env.DATA_DIR;
const originalPluginsDir = process.env.OMNIROUTE_PLUGINS_DIR;
const originalFetch = globalThis.fetch;
const eventBusOwner = globalThis as { __omnirouteEventBus?: unknown };
const originalEventBus = eventBusOwner.__omnirouteEventBus;
try {
const result = await runIsolatedFixture(testRoot);
assert.equal(
result.code,
0,
`isolated Adapta fixture failed (signal=${result.signal ?? "none"})\n` +
`stdout:\n${result.stdout}\nstderr:\n${result.stderr}`
);
assert.equal(result.signal, null);
assert.match(result.stdout, / tests 1/);
assert.match(result.stdout, / pass 1/);
assert.match(result.stdout, / fail 0/);
assert.doesNotMatch(result.stdout + result.stderr, new RegExp(SYNTHETIC_API_KEY_SECRET));
assert.equal(process.env.DATA_DIR, originalDataDir);
assert.equal(process.env.OMNIROUTE_PLUGINS_DIR, originalPluginsDir);
assert.equal(globalThis.fetch, originalFetch);
assert.equal(
eventBusOwner.__omnirouteEventBus,
originalEventBus,
"the subprocess fixture must not replace the parent event bus singleton"
);
} finally {
fs.rmSync(testRoot, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
}
});

View File

@@ -36,18 +36,25 @@ describe("AdaptaWebExecutor", () => {
});
it("execute returns proper result shape on auth failure", async () => {
const executor = new mod.AdaptaWebExecutor();
const result = await executor.execute({
model: "adapta-one",
body: { messages: [{ role: "user", content: "hi" }] },
stream: false,
credentials: { apiKey: "invalid-jwt" },
signal: null,
});
assert.ok(result.response instanceof Response);
assert.ok(typeof result.url === "string");
assert.ok(typeof result.headers === "object");
assert.ok(result.transformedBody !== undefined);
const originalFetch = globalThis.fetch;
globalThis.fetch = (async () => new Response(null, { status: 401 })) as typeof fetch;
try {
const executor = new mod.AdaptaWebExecutor();
const result = await executor.execute({
model: "adapta-one",
body: { messages: [{ role: "user", content: "hi" }] },
stream: false,
credentials: { apiKey: "invalid-jwt" },
signal: null,
});
assert.ok(result.response instanceof Response);
assert.ok(typeof result.url === "string");
assert.ok(typeof result.headers === "object");
assert.ok(result.transformedBody !== undefined);
} finally {
globalThis.fetch = originalFetch;
}
});
it("testConnection returns false for invalid credentials", async () => {

View File

@@ -1,62 +0,0 @@
import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import { fileURLToPath } from "node:url";
import test from "node:test";
const REPO_ROOT = fileURLToPath(new URL("../..", import.meta.url));
const FIXTURE = fileURLToPath(
new URL("../fixtures/stream-handler-public-error-boundary.fixture.ts", import.meta.url)
);
const CHILD_RUNTIME_ENV_KEYS = [
"PATH",
"TMPDIR",
"TMP",
"TEMP",
"SystemRoot",
"ComSpec",
"PATHEXT",
"LANG",
"LC_ALL",
"TZ",
] as const;
function buildFixtureEnv(): NodeJS.ProcessEnv {
const env: NodeJS.ProcessEnv = {
NODE_ENV: "test",
APP_LOG_TO_FILE: "false",
API_KEY_SECRET: "stream-handler-boundary-fixture-secret-20260902",
DISABLE_SQLITE_AUTO_BACKUP: "true",
NO_COLOR: "1",
};
for (const key of CHILD_RUNTIME_ENV_KEYS) {
const value = process.env[key];
if (value !== undefined) env[key] = value;
}
// Nested test runners must not inherit the parent runner's recursion marker.
delete env.NODE_TEST_CONTEXT;
return env;
}
test("generic stream public error boundaries pass in an isolated process", () => {
const result = spawnSync(
process.execPath,
["--import", "tsx/esm", "--import", "./open-sse/utils/setupPolyfill.ts", "--test", FIXTURE],
{
cwd: REPO_ROOT,
encoding: "utf8",
env: buildFixtureEnv(),
timeout: 120_000,
}
);
const output = `${result.stdout}\n${result.stderr}`;
assert.ifError(result.error);
assert.equal(result.signal, null, output.slice(-12_000));
assert.equal(result.status, 0, output.slice(-12_000));
assert.match(output, /(?:^|\s)tests\s+6(?:\s|$)/m);
assert.match(output, /(?:^|\s)pass\s+6(?:\s|$)/m);
assert.match(output, /(?:^|\s)fail\s+0(?:\s|$)/m);
});

View File

@@ -256,8 +256,7 @@ test("createDisconnectAwareStream emits Responses API failure events for Respons
assert.match(text, /event: response\.failed/);
assert.match(text, /"type":"response\.failed"/);
assert.match(text, /"message":"responses stream"/);
assert.doesNotMatch(text, /died/);
assert.match(text, /"message":"responses stream\\ndied"/);
assert.match(text, /"type":"server_error"/);
assert.match(text, /"code":"server_error"/);
assert.doesNotMatch(text, /chat\.completion\.chunk/);
@@ -265,7 +264,7 @@ test("createDisconnectAwareStream emits Responses API failure events for Respons
assert.doesNotMatch(text, /\[DONE\]/);
});
test("createDisconnectAwareStream strips multiline diagnostic tails from Responses errors", async () => {
test("createDisconnectAwareStream keeps newlines escaped inside SSE data fields", async () => {
const upstreamError = Object.assign(new Error("line one\nline two\rline three"), {
statusCode: 400,
});
@@ -291,9 +290,9 @@ test("createDisconnectAwareStream strips multiline diagnostic tails from Respons
const text = await readStreamText(stream);
assert.match(text, /^event: response\.failed\ndata: \{"type":"response\.failed"/);
assert.match(text, /"message":"line one"/);
assert.doesNotMatch(text, /line two/);
assert.doesNotMatch(text, /line three/);
assert.match(text, /"message":"line one\\nline two\\rline three"/);
assert.doesNotMatch(text, /^line two/m);
assert.doesNotMatch(text, /^line three/m);
});
test("createDisconnectAwareStream treats legacy OpenAI response format alias as Responses", async () => {
@@ -361,7 +360,7 @@ test("createDisconnectAwareStream emits Claude SSE errors for Claude clients", a
assert.doesNotMatch(text, /\[DONE\]/);
});
test("createDisconnectAwareStream strips multiline diagnostic tails from Claude errors", async () => {
test("createDisconnectAwareStream keeps newlines escaped for Claude SSE errors", async () => {
const upstreamError = Object.assign(new Error("claude line one\nclaude line two"), {
statusCode: 502,
});
@@ -387,8 +386,8 @@ test("createDisconnectAwareStream strips multiline diagnostic tails from Claude
const text = await readStreamText(stream);
assert.match(text, /^event: error\ndata: \{"type":"error"/);
assert.match(text, /"message":"claude line one"/);
assert.doesNotMatch(text, /claude line two/);
assert.match(text, /"message":"claude line one\\nclaude line two"/);
assert.doesNotMatch(text, /^claude line two/m);
});
// #7699/#7816 — heuristic is scoped to FORMATS.CLAUDE (/v1/messages); a