Files
openclaw/extensions/reef/protocol/guard.test.ts
sunlit-deng 7aa1aacb8e fix(reef): prevent oversized guard responses from consuming memory (#106439)
* fix(reef): bound guard provider response reads

* test(reef): strengthen guard response bound proof

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-07-13 18:02:11 -07:00

354 lines
12 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { createAnthropicGuard, createOpenAiGuard, type FetchLike } from "./guard-adapters.js";
import { admitGuardAdapter, type GuardRequest, type Verdict } from "./guard.js";
const model = "guard-model-2026-07-12";
const request: GuardRequest = {
direction: "outbound",
source: "alice#1",
destination: "bob#1",
text: "meeting at ten",
policyVersion: "v1",
};
const allow: Verdict = {
decision: "allow",
category: "coordination",
reason: "Routine coordination.",
model,
policyVersion: "v1",
};
const modelAllow = {
decision: "allow",
category: "coordination",
reason: "Routine coordination.",
policyVersion: "v1",
};
describe("guard admission", () => {
it.each([
[
"throws",
async () => {
throw new Error("network");
},
],
["malformed", async () => "not an object"],
["extra fields", async () => ({ ...allow, extra: true })],
["wrong model", async () => ({ ...allow, model: "other-model-2026-07-12" })],
])("fails closed when raw adapter %s", async (_name, classifyRaw) => {
const guard = admitGuardAdapter({ providerId: "fake", pinnedModel: model, classifyRaw });
await expect(guard.classify(request)).resolves.toMatchObject({
decision: "deny",
category: "guard_failure",
model,
});
});
it("fails closed on timeout", async () => {
const guard = admitGuardAdapter(
{ providerId: "fake", pinnedModel: model, classifyRaw: () => new Promise(() => {}) },
5,
);
await expect(guard.classify(request)).resolves.toMatchObject({
decision: "deny",
category: "guard_failure",
});
});
it("rejects floating model aliases at construction", () => {
expect(() =>
admitGuardAdapter({
providerId: "fake",
pinnedModel: "guard-latest",
async classifyRaw() {
return allow;
},
}),
).toThrow("dated snapshot");
});
it("rejects bare family aliases without a documented immutable id", () => {
expect(() =>
admitGuardAdapter({
providerId: "fake",
pinnedModel: "gpt-5.6",
async classifyRaw() {
return allow;
},
}),
).toThrow("dated snapshot");
});
it.each(["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"])(
"accepts documented immutable undated id %s",
(pinnedModel) => {
expect(() =>
admitGuardAdapter({
providerId: "fake",
pinnedModel,
async classifyRaw() {
return allow;
},
}),
).not.toThrow();
},
);
});
describe("provider adapters", () => {
it("uses OpenAI Responses strict JSON schema and accepts a recorded response", async () => {
let captured: RequestInit | undefined;
const fetch: FetchLike = async (_url, init) => {
captured = init;
return jsonResponse({
model,
status: "completed",
output: [
{ type: "message", content: [{ type: "output_text", text: JSON.stringify(modelAllow) }] },
],
});
};
const guard = createOpenAiGuard({ apiKey: "test", pinnedModel: model, fetch });
await expect(guard.classify(request)).resolves.toEqual(allow);
const body = JSON.parse(captured!.body as string) as Record<string, any>;
expect(body).toMatchObject({ model, store: false, background: false, tools: [] });
expect(body.text.format).toMatchObject({ type: "json_schema", strict: true });
expect(body.text.format.schema.properties).not.toHaveProperty("model");
expect(body.text.format.schema.required).not.toContain("model");
expect(body.instructions).toContain("outbound DLP");
expect(body.instructions).toContain('Set policyVersion to exactly "v1".');
});
it("accepts a recorded Anthropic response with distinct inbound instructions", async () => {
let captured: RequestInit | undefined;
const fetch: FetchLike = async (_url, init) => {
captured = init;
return jsonResponse({
model,
stop_reason: "end_turn",
content: [{ type: "text", text: JSON.stringify({ ...modelAllow, category: "safe" }) }],
});
};
const guard = createAnthropicGuard({ apiKey: "test", pinnedModel: model, fetch });
const inbound = { ...request, direction: "inbound" as const };
await expect(guard.classify(inbound)).resolves.toMatchObject({
decision: "allow",
category: "safe",
});
const body = JSON.parse(captured!.body as string) as Record<string, any>;
expect(body.system).toContain("inbound prompt-injection");
expect(body.system).toContain('Set policyVersion to exactly "v1".');
expect(body.system).not.toContain('"model"');
expect(body.output_config.format.type).toBe("json_schema");
expect(body.output_config.format.schema).toMatchObject({
additionalProperties: false,
required: ["decision", "category", "reason", "policyVersion"],
});
expect(body.output_config.format.schema.properties).not.toHaveProperty("model");
expect((captured!.headers as Record<string, string>)["anthropic-version"]).toBe("2023-06-01");
});
it("fails closed on Anthropic prose despite provider schema enforcement", async () => {
const guard = createAnthropicGuard({
apiKey: "test",
pinnedModel: model,
fetch: async () =>
jsonResponse({
model,
stop_reason: "end_turn",
content: [{ type: "text", text: `Verdict: ${JSON.stringify(modelAllow)}` }],
}),
});
await expect(guard.classify(request)).resolves.toMatchObject({
decision: "deny",
category: "guard_failure",
});
});
it("fails closed on non-200 provider responses", async () => {
const guard = createOpenAiGuard({
apiKey: "test",
pinnedModel: model,
fetch: async () => jsonResponse({ error: "no" }, 500),
});
await expect(guard.classify(request)).resolves.toMatchObject({
decision: "deny",
category: "guard_failure",
});
});
it("cancels oversized provider response streams before buffering them fully", async () => {
const maxBytes = 256 * 1024;
const chunk = new TextEncoder().encode("x".repeat(64 * 1024));
const totalChunks = 64;
let emittedChunks = 0;
let cancelled = false;
const response = new Response(
new ReadableStream<Uint8Array>({
pull(controller) {
if (emittedChunks >= totalChunks) {
controller.close();
return;
}
controller.enqueue(chunk);
emittedChunks += 1;
},
cancel() {
cancelled = true;
},
}),
{ headers: { "content-type": "application/json" } },
);
const guard = createOpenAiGuard({
apiKey: "test",
pinnedModel: model,
fetch: async () => response,
});
await expect(guard.classify(request)).resolves.toMatchObject({
decision: "deny",
category: "guard_failure",
});
expect(cancelled).toBe(true);
// Allow the overflow chunk plus one chunk queued by the stream implementation.
expect(emittedChunks * chunk.byteLength).toBeLessThanOrEqual(maxBytes + chunk.byteLength * 2);
});
it("accepts valid provider responses close to the body limit", async () => {
const body = JSON.stringify({
model,
status: "completed",
output: [
{ type: "message", content: [{ type: "output_text", text: JSON.stringify(modelAllow) }] },
],
provider_metadata: "x".repeat(240 * 1024),
});
const bodyBytes = new TextEncoder().encode(body).byteLength;
expect(bodyBytes).toBeGreaterThan(240 * 1024);
expect(bodyBytes).toBeLessThan(256 * 1024);
const guard = createOpenAiGuard({
apiKey: "test",
pinnedModel: model,
fetch: async () => new Response(body, { headers: { "content-type": "application/json" } }),
});
await expect(guard.classify(request)).resolves.toEqual(allow);
});
it("fails closed on malformed JSON and provider model mismatch", async () => {
const malformed = createOpenAiGuard({
apiKey: "test",
pinnedModel: model,
fetch: async () => new Response("not json"),
});
await expect(malformed.classify(request)).resolves.toMatchObject({ category: "guard_failure" });
const mismatch = createOpenAiGuard({
apiKey: "test",
pinnedModel: model,
fetch: async () =>
jsonResponse({ model: "other-model-2026-07-12", status: "completed", output: [] }),
});
await expect(mismatch.classify(request)).resolves.toMatchObject({ category: "guard_failure" });
const duplicate = createOpenAiGuard({
apiKey: "test",
pinnedModel: model,
fetch: async () =>
jsonResponse({
model,
status: "completed",
output: [
{
type: "message",
content: [
{
type: "output_text",
text: '{"decision":"allow","decision":"deny","category":"safe","reason":"No.","policyVersion":"v1"}',
},
],
},
],
}),
});
await expect(duplicate.classify(request)).resolves.toMatchObject({ category: "guard_failure" });
});
it("takes model evidence from the provider and requires the model policy echo", async () => {
const responseFor =
(modelJson: unknown): FetchLike =>
async () =>
jsonResponse({
model,
status: "completed",
output: [
{
type: "message",
content: [{ type: "output_text", text: JSON.stringify(modelJson) }],
},
],
});
const happy = createOpenAiGuard({
apiKey: "test",
pinnedModel: model,
fetch: responseFor(modelAllow),
});
await expect(happy.classify(request)).resolves.toEqual(allow);
const wrongPolicy = createOpenAiGuard({
apiKey: "test",
pinnedModel: model,
fetch: responseFor({ ...modelAllow, policyVersion: "v2" }),
});
await expect(wrongPolicy.classify(request)).resolves.toMatchObject({
decision: "deny",
category: "guard_failure",
});
const { policyVersion: _policyVersion, ...missingPolicyJson } = modelAllow;
const missingPolicy = createOpenAiGuard({
apiKey: "test",
pinnedModel: model,
fetch: responseFor(missingPolicyJson),
});
await expect(missingPolicy.classify(request)).resolves.toMatchObject({
decision: "deny",
category: "guard_failure",
});
const unexpectedModel = createOpenAiGuard({
apiKey: "test",
pinnedModel: model,
fetch: responseFor({ ...modelAllow, model: "invented-2026-01-01" }),
});
await expect(unexpectedModel.classify(request)).resolves.toMatchObject({
decision: "deny",
category: "guard_failure",
});
});
});
describe.skipIf(process.env.REEF_LIVE_GUARD !== "1")("live guard smoke", () => {
it("calls OpenAI only when explicitly enabled", async () => {
const liveModel = process.env.REEF_OPENAI_MODEL;
const apiKey = process.env.OPENAI_API_KEY;
if (!liveModel || !apiKey) {
return;
}
const guard = createOpenAiGuard({ apiKey, pinnedModel: liveModel, fetch });
expect((await guard.classify(request)).model).toBe(liveModel);
});
it("calls Anthropic only when explicitly enabled", async () => {
const liveModel = process.env.REEF_ANTHROPIC_MODEL;
const apiKey = process.env.ANTHROPIC_API_KEY;
if (!liveModel || !apiKey) {
return;
}
const guard = createAnthropicGuard({ apiKey, pinnedModel: liveModel, fetch });
expect((await guard.classify(request)).model).toBe(liveModel);
});
});
function jsonResponse(value: unknown, status = 200): Response {
return new Response(JSON.stringify(value), {
status,
headers: { "content-type": "application/json" },
});
}