From 43ffe8ce95d11aca98319da50461b96c63fd8919 Mon Sep 17 00:00:00 2001 From: Dotta <34892728+cryppadotta@users.noreply.github.com> Date: Fri, 16 Oct 2026 05:59:23 -0500 Subject: [PATCH] perf: skip conditions for unselected tool profile entries (#16556) Check exact tool profile selectors before evaluating conditions. Matched entries retain fresh policy condition checks and existing approval, deny, binding, and decision-phase behavior. Validation: 70 affected cases pass with zero skips. Full repository typecheck and build pass. A balanced same-database comparison over 47,931 entries preserves all decision JSON and reduces 200 decisions from about 1.05 seconds to 0.40 seconds. Fresh exact-head full CI, E2E, Superagent, legacy security, and Apex 5/5 passed with no unresolved findings. Co-Authored-By: Paperclip --- .../tool-gateway-listing-memory.test.ts | 44 ++++++++++++++++++- server/src/services/tool-access-policy.ts | 14 +++--- 2 files changed, 50 insertions(+), 8 deletions(-) diff --git a/server/src/__tests__/tool-gateway-listing-memory.test.ts b/server/src/__tests__/tool-gateway-listing-memory.test.ts index 4708aaf0c47..24d20cdc9fc 100644 --- a/server/src/__tests__/tool-gateway-listing-memory.test.ts +++ b/server/src/__tests__/tool-gateway-listing-memory.test.ts @@ -18,7 +18,7 @@ import { toolProfiles, toolMcpGatewayTokens, } from "@paperclipai/db"; -import { eq } from "drizzle-orm"; +import { and, eq } from "drizzle-orm"; import type { ToolAccessDecisionInput } from "@paperclipai/shared"; import { createToolAccessDecisionCache, @@ -178,6 +178,48 @@ describeEmbeddedPostgres("tool gateway listing memory", () => { expect(largeListing.statements.length).toBeLessThan(100); }); + it("does not construct a condition clock for unmatched entries in a decision batch", async () => { + const fixture = await createListingFixture(db, 6); + const [binding] = await db.select().from(toolProfileBindings) + .where(and(eq(toolProfileBindings.companyId, fixture.company.id), eq(toolProfileBindings.targetId, fixture.namedGateway.id))); + expect(binding).toBeDefined(); + for (let start = 0; start < 47_930; start += 1000) { + await db.insert(toolProfileEntries).values(Array.from({ length: Math.min(1000, 47_930 - start) }, (_, offset) => ({ + companyId: fixture.company.id, profileId: binding!.profileId, + selectorType: "tool_name" as const, toolName: `not_selected_${start + offset}`, effect: "exclude" as const, + }))); + } + const policy = toolAccessPolicyService(db); + const cache = createToolAccessDecisionCache(); + const originalDate = Date; + let clocks = 0; + vi.stubGlobal("Date", new Proxy(originalDate, { + construct(target, args, constructor) { + if (args.length === 0) clocks++; + return Reflect.construct(target, args, constructor); + }, + })); + const decisions = []; + try { + for (let index = 0; index < 10; index++) { + const entry = fixture.entries[4 + index % 2]!; + decisions.push(await policy.decide({ + companyId: fixture.company.id, + actor: { actorType: "agent", actorId: fixture.agent.id, agentId: fixture.agent.id }, + runContext: { heartbeatRunId: fixture.run.id, issueId: fixture.issue.id, projectId: fixture.project.id, gatewayId: fixture.namedGateway.id }, + request: { toolName: entry.toolName!, applicationId: fixture.application.id, connectionId: fixture.connection.id, + catalogEntryId: entry.id, upstreamToolName: entry.toolName!, riskLevel: entry.riskLevel, arguments: {} }, + consumeRateLimit: false, + }, { cache })); + } + } finally { + vi.unstubAllGlobals(); + } + expect(decisions).toHaveLength(10); + expect(decisions.every(decision => decision.allowed && decision.reasonCode === "allow_profile")).toBe(true); + expect(clocks).toBeLessThan(5000); + }, 30_000); + it("reads the catalog without repeating the connection row for each tool", async () => { const fixture = await createListingFixture(db, 20); // An eligible connection without catalog tools adds nothing to a listing. diff --git a/server/src/services/tool-access-policy.ts b/server/src/services/tool-access-policy.ts index 60db597245e..6357c9b5e87 100644 --- a/server/src/services/tool-access-policy.ts +++ b/server/src/services/tool-access-policy.ts @@ -591,14 +591,14 @@ function evaluatePolicyConditions( } function profileEntryMatches(entry: typeof toolProfileEntries.$inferSelect, ctx: ToolAccessContext): boolean { + const selected = entry.selectorType === "application" ? entry.applicationId === ctx.applicationId + : entry.selectorType === "connection" ? entry.connectionId === ctx.connectionId + : entry.selectorType === "catalog_entry" ? entry.catalogEntryId === ctx.catalogEntryId + : entry.selectorType === "tool_name" ? entry.toolName === ctx.toolName + : entry.selectorType === "risk_level" ? entry.riskLevel === ctx.riskLevel : false; + if (!selected) return false; const conditions = isRecord(entry.conditions) ? entry.conditions as ToolPolicyConditions : null; - if (!evaluatePolicyConditions(conditions, ctx).matched) return false; - if (entry.selectorType === "application") return entry.applicationId === ctx.applicationId; - if (entry.selectorType === "connection") return entry.connectionId === ctx.connectionId; - if (entry.selectorType === "catalog_entry") return entry.catalogEntryId === ctx.catalogEntryId; - if (entry.selectorType === "tool_name") return entry.toolName === ctx.toolName; - if (entry.selectorType === "risk_level") return entry.riskLevel === ctx.riskLevel; - return false; + return evaluatePolicyConditions(conditions, ctx).matched; } function targetMatches(binding: typeof toolProfileBindings.$inferSelect, ctx: ToolAccessContext): boolean {