From 5a18088249165f9c9c1d745b7b23f11cc3564469 Mon Sep 17 00:00:00 2001 From: root Date: Mon, 20 Jul 2026 21:25:30 +0200 Subject: [PATCH] fix: authenticate Workbench SSE inspection --- internal/workbench/workbench.test.ts | 30 ++++++++++++++++++++++++++-- tools/src/workbench-cli.ts | 7 +++++-- 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/internal/workbench/workbench.test.ts b/internal/workbench/workbench.test.ts index 2ba0d097..1c83462d 100644 --- a/internal/workbench/workbench.test.ts +++ b/internal/workbench/workbench.test.ts @@ -177,7 +177,14 @@ describe("Workbench native HTTP adapter", () => { async dispatch() { return { ok: true }; }, async snapshot() { return { sessions: {}, turns: {} }; } }); - const server = Bun.serve({ port: 0, fetch: (request) => app.fetch(request) }); + let authorization: string | null = null; + const server = Bun.serve({ + port: 0, + fetch(request) { + authorization = request.headers.get("authorization"); + return app.fetch(request); + } + }); try { const pending = runWorkbenchCli([ "events", "inspect", @@ -186,7 +193,7 @@ describe("Workbench native HTTP adapter", () => { "--over-api", "--api-url", `http://127.0.0.1:${server.port}`, "--timeout-ms", "1000" - ], {}); + ], { HWLAB_API_KEY: "l0-sse-api-key" }); for (let index = 0; index < 20 && subscribers.size === 0; index += 1) await Bun.sleep(5); for (const listener of subscribers) listener({ schema: "hwlab.event.v1", sessionId: "ses_l0_sse", traceId: "trc_l0_sse", event: { type: "backend", eventType: "backend", label: "agentrun:backend:command-created" } }); expect(await pending).toMatchObject({ @@ -203,12 +210,31 @@ describe("Workbench native HTTP adapter", () => { eventCount: 1, eventTypes: ["backend"] }); + expect(authorization).toBe("Bearer l0-sse-api-key"); } finally { server.stop(true); await app.close(); } }); + test("L0 CLI reports the Workbench SSE HTTP status when authentication fails", async () => { + const server = Bun.serve({ port: 0, fetch: () => new Response("unauthorized", { status: 401 }) }); + try { + await expect(runWorkbenchCli([ + "events", "inspect", + "--trace-id", "trc_l0_auth_failure", + "--over-api", + "--api-url", `http://127.0.0.1:${server.port}`, + "--timeout-ms", "1000" + ], {})).rejects.toMatchObject({ + code: "workbench_sse_read_failed", + message: expect.stringContaining("Workbench SSE returned HTTP 401") + }); + } finally { + server.stop(true); + } + }); + test("L0 CLI replays retained Kafka events before the same live SSE stream", async () => { const sessionId = "ses_l0_refresh"; const traceId = "trc_l0_refresh"; diff --git a/tools/src/workbench-cli.ts b/tools/src/workbench-cli.ts index 9d44ef64..49aa34c0 100644 --- a/tools/src/workbench-cli.ts +++ b/tools/src/workbench-cli.ts @@ -55,10 +55,13 @@ async function inspectEvents(parsed: Parsed, env: Record controller.abort(), timeoutMs); + const headers: Record = {}; + const apiKey = env.HWLAB_API_KEY; + if (apiKey) headers.authorization = `Bearer ${apiKey}`; const frames: SseFrame[] = []; let buffer = ""; try { - const response = await fetch(`${baseUrl.replace(/\/$/u, "")}${path}`, { signal: controller.signal }); + const response = await fetch(`${baseUrl.replace(/\/$/u, "")}${path}`, { signal: controller.signal, headers }); if (!response.ok || !response.body) throw codedError("workbench_sse_open_failed", `Workbench SSE returned HTTP ${response.status}`); const reader = response.body.getReader(); const decoder = new TextDecoder(); @@ -83,7 +86,7 @@ async function inspectEvents(parsed: Parsed, env: Record entry.name === "workbench.connected")}`); + throw codedError("workbench_sse_read_failed", `Workbench SSE read failed: ${error?.message ?? error?.code ?? error?.name ?? "unknown"}; frames=${frames.length}; businessEvents=${business.length}; observed=${observed.observedSemantics.join(",")}; connected=${frames.some((entry) => entry.name === "workbench.connected")}`); } } finally { clearTimeout(timer);