fix(security): sanitize session bootstrap failures
This commit is contained in:
@@ -6,6 +6,9 @@ import type { Settings } from "../settings/settings-store.js";
|
||||
import { getUser } from "../auth/auth.js";
|
||||
import type { ReadinessManager } from "../runtime/readiness-manager.js";
|
||||
|
||||
const BOOTSTRAP_FAILURE_MESSAGE =
|
||||
"Session startup failed. Check configuration and connectivity, then Resume the session.";
|
||||
|
||||
export function sessionRoutes(
|
||||
app: FastifyInstance,
|
||||
d: { mgr: PiProcessManager; tht: ThtRunner; hub: SseHub; getSettings: () => Settings; readiness: ReadinessManager },
|
||||
@@ -34,11 +37,10 @@ export function sessionRoutes(
|
||||
await Promise.all([configure, retrieval]);
|
||||
info(id, "Starting model");
|
||||
start();
|
||||
} catch (error) {
|
||||
} catch {
|
||||
d.mgr.teardown(id);
|
||||
const text = error instanceof Error ? error.message : String(error);
|
||||
void d.tht.failSession(id, d.getSettings().workspace).catch(() => undefined);
|
||||
rt.bridge.emitClientEvent({ type: "info", level: "error", text: `Session bootstrap failed: ${text}` });
|
||||
rt.bridge.emitClientEvent({ type: "info", level: "error", text: BOOTSTRAP_FAILURE_MESSAGE });
|
||||
rt.bridge.emitClientEvent({ type: "system_event", event: "session_failed" });
|
||||
rt.bridge.emitClientEvent({ type: "system_event", event: "agent_end" });
|
||||
}
|
||||
|
||||
@@ -471,3 +471,51 @@ test("POST /sessions returns after bridge attachment but starts only after retri
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
expect(started).toBe(true);
|
||||
});
|
||||
|
||||
test("POST /sessions bootstrap failure emits only a fixed recovery message", async () => {
|
||||
const published: Array<{ event: string; data: any }> = [];
|
||||
let listener: ((event: any) => void) | undefined;
|
||||
const bridge = {
|
||||
onClientEvent: (callback: (event: any) => void) => { listener = callback; },
|
||||
emitClientEvent: (event: any) => listener?.(event),
|
||||
};
|
||||
const runtime = { bridge } as any;
|
||||
const rawFailure =
|
||||
"connect https://secret.invalid/bootstrap?token=DO_NOT_LEAK using /srv/private/model-key";
|
||||
const app = buildApp(loadConfig({ THT_HARNESS_DIR: "../harness" }), {
|
||||
mgr: {
|
||||
createFor: () => runtime,
|
||||
configure: async () => { throw new Error(rawFailure); },
|
||||
start: () => {},
|
||||
teardown: () => {},
|
||||
} as any,
|
||||
hub: {
|
||||
publish: (_id: string, event: string, data: any) => published.push({ event, data }),
|
||||
} as any,
|
||||
thtRunner: {
|
||||
ollamaEnsure: async () => ({ ok: true }),
|
||||
sessionNew: async () => ({ id: "s-bootstrap" }),
|
||||
searchPack: async () => {},
|
||||
failSession: async () => {},
|
||||
} as any,
|
||||
getSettings: () => ({ workspace: "psd" }) as any,
|
||||
});
|
||||
|
||||
const response = await app.inject({
|
||||
method: "POST",
|
||||
url: "/sessions",
|
||||
payload: { question: "q" },
|
||||
});
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
|
||||
expect(response.json()).toEqual({ id: "s-bootstrap" });
|
||||
expect(published.map(({ data }) => data)).toContainEqual({
|
||||
type: "info",
|
||||
level: "error",
|
||||
text: "Session startup failed. Check configuration and connectivity, then Resume the session.",
|
||||
});
|
||||
const clientOutput = JSON.stringify(published);
|
||||
expect(clientOutput).not.toContain("secret.invalid");
|
||||
expect(clientOutput).not.toContain("DO_NOT_LEAK");
|
||||
expect(clientOutput).not.toContain("/srv/private/model-key");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user