fix(security): harden ACP prompt size guardrails

This commit is contained in:
Peter Steinberger
2026-02-19 15:40:46 +01:00
parent ebcf19746f
commit 63e39d7f57
5 changed files with 89 additions and 10 deletions

View File

@@ -2,6 +2,7 @@ import type {
AgentSideConnection,
LoadSessionRequest,
NewSessionRequest,
PromptRequest,
} from "@agentclientprotocol/sdk";
import { describe, expect, it, vi } from "vitest";
import type { GatewayClient } from "../gateway/client.js";
@@ -14,9 +15,11 @@ function createConnection(): AgentSideConnection {
} as unknown as AgentSideConnection;
}
function createGateway(): GatewayClient {
function createGateway(
request: GatewayClient["request"] = vi.fn(async () => ({ ok: true })) as GatewayClient["request"],
): GatewayClient {
return {
request: vi.fn(async () => ({ ok: true })),
request,
} as unknown as GatewayClient;
}
@@ -37,6 +40,18 @@ function createLoadSessionRequest(sessionId: string, cwd = "/tmp"): LoadSessionR
} as unknown as LoadSessionRequest;
}
function createPromptRequest(
sessionId: string,
text: string,
meta: Record<string, unknown> = {},
): PromptRequest {
return {
sessionId,
prompt: [{ type: "text", text }],
_meta: meta,
} as unknown as PromptRequest;
}
describe("acp session creation rate limit", () => {
it("rate limits excessive newSession bursts", async () => {
const sessionStore = createInMemorySessionStore();
@@ -76,3 +91,45 @@ describe("acp session creation rate limit", () => {
sessionStore.clearAllSessionsForTest();
});
});
describe("acp prompt size hardening", () => {
it("rejects oversized prompt blocks without leaking active runs", async () => {
const request = vi.fn(async () => ({ ok: true }));
const sessionStore = createInMemorySessionStore();
const agent = new AcpGatewayAgent(createConnection(), createGateway(request), {
sessionStore,
});
const sessionId = "prompt-limit-oversize";
await agent.loadSession(createLoadSessionRequest(sessionId));
await expect(
agent.prompt(createPromptRequest(sessionId, "a".repeat(2 * 1024 * 1024 + 1))),
).rejects.toThrow(/maximum allowed size/i);
expect(request).not.toHaveBeenCalledWith("chat.send", expect.anything(), expect.anything());
const session = sessionStore.getSession(sessionId);
expect(session?.activeRunId).toBeNull();
expect(session?.abortController).toBeNull();
sessionStore.clearAllSessionsForTest();
});
it("rejects oversize final messages from cwd prefix without leaking active runs", async () => {
const request = vi.fn(async () => ({ ok: true }));
const sessionStore = createInMemorySessionStore();
const agent = new AcpGatewayAgent(createConnection(), createGateway(request), {
sessionStore,
});
const sessionId = "prompt-limit-prefix";
await agent.loadSession(createLoadSessionRequest(sessionId));
await expect(
agent.prompt(createPromptRequest(sessionId, "a".repeat(2 * 1024 * 1024))),
).rejects.toThrow(/maximum allowed size/i);
expect(request).not.toHaveBeenCalledWith("chat.send", expect.anything(), expect.anything());
const session = sessionStore.getSession(sessionId);
expect(session?.activeRunId).toBeNull();
expect(session?.abortController).toBeNull();
sessionStore.clearAllSessionsForTest();
});
});