mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-20 06:02:18 +02:00
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 <noreply@paperclip.ing>
This commit is contained in:
1 parent
8556539364
commit
43ffe8ce95
2 files changed
+50
-8
No files matched your search
@@ -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.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in new issue
Block a user