From 708d2020d86cbeb577d4e79b5fe31d6312b44c2d Mon Sep 17 00:00:00 2001 From: Wynne668 Date: Fri, 17 Jul 2026 17:19:21 +0800 Subject: [PATCH] fix(reef): prevent blank guard keys from denying every message (#109667) * fix(reef): reject blank guard credentials * fix(reef): type fetch mock in guard credential test Co-authored-by: Cursor * refactor: normalize Reef guard credentials canonically Co-authored-by: ZengWen-DT --------- Co-authored-by: Cursor Co-authored-by: Peter Steinberger --- extensions/reef/src/flow.test.ts | 34 ++++++++++++++++++++++++++++++-- extensions/reef/src/flow.ts | 7 ++++--- 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/extensions/reef/src/flow.test.ts b/extensions/reef/src/flow.test.ts index 82ce3e2232da..968a58dda40e 100644 --- a/extensions/reef/src/flow.test.ts +++ b/extensions/reef/src/flow.test.ts @@ -9,7 +9,7 @@ import { verifyReceipt, type Verdict, } from "../protocol/index.js"; -import { ReefMessageFlow } from "./flow.js"; +import { createConfiguredGuard, ReefMessageFlow } from "./flow.js"; import { allow, config, @@ -26,7 +26,37 @@ import type { ReefTransportClient } from "./transport.js"; import type { InboxEntry } from "./types.js"; beforeEach(resetFlowStoresForTests); -afterEach(resetFlowStoresForTests); +afterEach(() => { + vi.unstubAllEnvs(); + resetFlowStoresForTests(); +}); + +describe("createConfiguredGuard", () => { + it("rejects a whitespace-only guard credential", () => { + vi.stubEnv("REEF_TEST_KEY", " "); + + expect(() => createConfiguredGuard(config())).toThrow( + "Reef guard credential environment variable REEF_TEST_KEY is unset", + ); + }); + + it("trims a configured guard credential before requests", async () => { + vi.stubEnv("REEF_TEST_KEY", " guard-key "); + const fetcher = vi.fn(async () => new Response("", { status: 401 })); + const classifier = createConfiguredGuard(config(), fetcher); + + await classifier.classify({ + direction: "outbound", + source: "alice#1", + destination: "bob#1", + text: "hello", + policyVersion: "v1", + }); + + const init = fetcher.mock.calls[0]?.[1]; + expect(new Headers(init?.headers).get("authorization")).toBe("Bearer guard-key"); + }); +}); describe("ReefMessageFlow inbound", () => { it("delivers and persists before ack, then acks duplicate redelivery without delivering twice", async () => { diff --git a/extensions/reef/src/flow.ts b/extensions/reef/src/flow.ts index aa0f9e53e2e4..07f26722b902 100644 --- a/extensions/reef/src/flow.ts +++ b/extensions/reef/src/flow.ts @@ -1,3 +1,4 @@ +import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime"; import { appendAudit, appendInboxRead, @@ -449,14 +450,14 @@ export function createConfiguredGuard( if (!config.guard) { throw new Error("Reef guard is not configured"); } - const apiKey = process.env[config.guard.apiKeyEnv]; - if (!apiKey) { + const guardCredential = normalizeOptionalString(process.env[config.guard.apiKeyEnv]); + if (!guardCredential) { throw new Error( `Reef guard credential environment variable ${config.guard.apiKeyEnv} is unset`, ); } const options = { - apiKey, + apiKey: guardCredential, pinnedModel: config.guard.pinnedModel, timeoutMs: config.guard.timeoutMs, fetch: fetcher,