Files
PaperClipAI/scripts/benchmark-connection-install-capabilities.ts
T
DottaandPaperclip 2cd747d1b6 perf(server): batch connection install capability checks (#17945)
## Thinking Path

> - Paperclip manages AI agents and their access to external
connections.
> - The connection grants screen describes which agents can receive an
installation.
> - Each candidate currently repeats authorization for every retained
installation.
> - A company installation makes this work grow with the square of the
roster size.
> - This pull request checks retained targets once inside the existing
service owner.
> - The benefit is fewer authorization calls with the same scoped
capability results.

## Linked Issues or Issue Description

**What happened?**

The grants GET repeats connection preparation and retained-target checks
for each candidate agent. A local 100-agent company-install fixture made
50,300 SQL calls and 10,000 target lookups in one capability-description
operation. Open PR searches found no matching capability fan-out fix.

**Expected behavior**

The description must preserve retained-installation obligations and each
candidate's permission. It should evaluate retained targets once per
description. Writes must still authorize fresh.

**Steps to reproduce**

Run the committed benchmark with
`--baseline-ref=f6bc653f7f745adaafa26393852fb2f5a99256a4`. It creates
disposable synthetic PostgreSQL fixtures and loads the actual pre-fix
service from Git. It compares every positional capability, reason, and
operation.

**Paperclip version or commit**

Baseline `f6bc653f7f745adaafa26393852fb2f5a99256a4`.

**Deployment mode**

Built from source. Local real PostgreSQL proof. No hosted rollout.

## What Changed

- Add `describeAgentInstallCapabilities` to connection management.
Ordinary authorization and the batch share preparation, target checks,
and fallback completion.
- Use the batch only for direct saved non-AI connections and board
actors. Complex resources, delegated actors, and unusual saved target
IDs keep the ordinary evaluator. Fallback consumes the first preparation
once.
- Replace only the grants route's editable-agent loop. Keep
configure/use checks, identity filtering, audience disclosure, and write
authority on their existing paths.
- Add scoped-grant, retained-target, error-order, freshness, fallback,
full HTTP response, and real mutation regression coverage. Add a
reproducible benchmark and evidence notes.

## Verification

Real PostgreSQL 18.6 Linux Docker, Node 26.4.0. Two warmups and five
measured samples per arm, in alternating order. These are
service-component measurements with the same SQL-count instrumentation
in both arms. They do not measure the whole hosted endpoint.

| Fixture | Old median | Batch median | SQL calls old → batch | Target
lookups old → batch |
| --- | ---: | ---: | ---: | ---: |
| 100 agents, company install | 24.753 s | 246.39 ms | 50,300 → 503 |
10,000 → 100 |
| 100 agents, partial install | 6.339 s | 236.37 ms | 13,075 → 502 |
2,575 → 100 |
| 1 agent, company install | 5.13 ms | 4.82 ms | 8 → 8 | 1 → 1 |

All eight fixture shapes and every measured iteration had complete
positional equality. One-agent timing ranges overlap. Full ranges,
database settings, and limits are in
`doc/plans/2026-10-27-connection-grants-batch-verification.md`.

- **Passed:** `pnpm -r typecheck` and `pnpm build`, using pinned pnpm
9.15.4.
- **Passed:** 116 focused tests: all 83 connection-management
integration cases use real PostgreSQL and HTTP; 33 batch regressions
execute real policy over synthetic query transport. The integration
cases keep existing database creation, migrations, clients, services,
and assembled HTTP server, with disposable Docker PostgreSQL.
- **Passed:** complete grants GET parity across six human-role fixtures;
real install PUT succeeds before revocation, then fresh GET and actual
PUT deny after revocation without changing saved installs.
- **Passed:** a real PostgreSQL red/green regression for empty saved
target selectors, including candidate-specific decisions and later
reached infrastructure faults.
- **Passed:** independent old/new evaluator comparisons, including
recursive and delegated fallback, run-context reuse, continuation,
failure order, and principal/company isolation.
- **Local full-suite limit:** `pnpm test:run` was attempted and stopped
after native PostgreSQL fixtures could not initialize. An isolated
failed native case reproduced that startup error. The host cannot
allocate PostgreSQL SysV semaphores, including outside the sandbox. The
focused suites use an existing constructor seam to start disposable
Docker PostgreSQL. This is not a complete local full-suite pass.
- **Passed:** all 16 native-runtime cases that failed to start
PostgreSQL on the host, rerun with the disposable Docker provider. The
full local workflow remains incomplete.
- `git diff --check` passes. Greptile is 5/5 with no actionable findings
on final head `41dcf3ae029b6c60999ca6208136399b0af663d0`. All final-head
CI gates passed, including the full PR workflow. The two Storybook
checks are expected skips because this change has no UI changes.

Normal reproduction:

```sh
node --import ./server/node_modules/tsx/dist/esm/index.mjs scripts/benchmark-connection-install-capabilities.ts --baseline-ref=f6bc653f7f745adaafa26393852fb2f5a99256a4
cd server
pnpm exec vitest run src/__tests__/connection-install-capabilities.test.ts src/__tests__/connection-management.integration.test.ts
```

## Risks

Retained facts are observed once during one descriptive call. A mid-call
revoke or deletion can leave a stale displayed choice. The next
description and every mutation or human continuation read fresh
authority. The batch returns no reusable authorization context.

The bound applies to target evaluations in the eligible shape. Scoped
grant payloads and hierarchy reads can still grow inside each policy
decision. Complex resources and agent callers keep existing costs.
Hosted latency improvement remains unverified. There is no migration,
provider protocol change, new worker, persistent cache, or UI contract
change.

## Model Used

OpenAI Codex with code editing, execution, and test tools. Design, test,
and independent review agents used exact model IDs `gpt-6.1-sol` and
`gpt-6-astra` with xhigh reasoning. The host did not expose the primary
session's exact model ID or context-window size.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-10-27 20:59:01 -05:00

182 lines
12 KiB
TypeScript

import assert from "node:assert/strict";
import { execFileSync } from "node:child_process";
import { createHash, randomUUID } from "node:crypto";
import { readFile, mkdtemp, rm, writeFile } from "node:fs/promises";
import { createRequire } from "node:module";
import { tmpdir } from "node:os";
import path from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import {
agents, companies, companyMemberships, createDb, principalPermissionGrants,
startEmbeddedPostgresTestDatabase, toolApplications, toolConnections, toolConnectionInstalls, type Db,
} from "../packages/db/src/index.js";
import { connectionManagementService } from "../server/src/services/connection-management.js";
import type { AuthorizationActor } from "../server/src/services/authorization.js";
// Run explicitly; this benchmark never reads DATABASE_URL or contacts a hosted DB.
// node --import ./server/node_modules/tsx/dist/esm/index.mjs scripts/benchmark-connection-install-capabilities.ts --baseline-ref=<pre-fix SHA>
const baselineRef = process.argv.find(arg => arg.startsWith("--baseline-ref="))?.slice(15);
assert.ok(baselineRef, "Supply an explicit pre-fix --baseline-ref");
const repository = fileURLToPath(new URL("../", import.meta.url));
const git = (...args: string[]) => execFileSync("git", args, { cwd: repository, encoding: "utf8" }).trim();
const baselineRevision = git("rev-parse", "--verify", `${baselineRef}^{commit}`);
const treatmentRevision = git("rev-parse", "HEAD");
const servicePath = "server/src/services/connection-management.ts";
const routePath = "server/src/routes/tool-access.ts";
const runtimeChanges = git("diff", baselineRevision, "--name-only", "--", "server/src", "packages/db/src", "packages/shared/src")
.split("\n").filter(Boolean).filter(name => !name.includes("/__tests__/") && !/\.test\.[cm]?ts$/.test(name));
assert.deepEqual(runtimeChanges.filter(name => name !== servicePath && name !== routePath), [],
"Baseline imports current dependencies: isolate any additional changed runtime dependencies first");
assert.equal(git("diff", baselineRevision, "--", "pnpm-lock.yaml"), "", "Dependency lock must match the pinned baseline");
const oldSource = execFileSync("git", ["show", `${baselineRevision}:${servicePath}`], { cwd: repository, encoding: "utf8" });
const requireFromServer = createRequire(new URL("../server/package.json", import.meta.url));
const serviceUrl = new URL(`../${servicePath}`, import.meta.url);
const rewritten = oldSource.replace(/from "([^"]+)"/g, (_match, specifier: string) => {
const resolved = specifier.startsWith(".")
? fileURLToPath(new URL(specifier, serviceUrl))
: requireFromServer.resolve(specifier).replace(/\.cjs$/, ".js");
return `from ${JSON.stringify(resolved)}`;
});
const baselineDirectory = await mkdtemp(path.join(tmpdir(), "connection-install-baseline-"));
const baselineFile = path.join(baselineDirectory, "connection-management.mts");
await writeFile(baselineFile, rewritten);
const baselineModule = await import(pathToFileURL(baselineFile).href) as typeof import("../server/src/services/connection-management.js");
type Statement = { sql: string; params: unknown[]; calls: number; rows: number };
type Counts = { calls: number; rows: number; statements: Map<string, Statement> };
let activeCounts: Counts | undefined;
function instrument(db: Db): Db {
return new Proxy(db, { get(target, key, receiver) {
if (key !== "select") return Reflect.get(target, key, receiver);
return (...fields: Parameters<Db["select"]>) => {
const builder = target.select(...fields);
const from = builder.from.bind(builder);
builder.from = ((...args: Parameters<typeof from>) => {
const query = from(...args), execute = query.execute.bind(query);
query.execute = (async (...executeArgs: Parameters<typeof execute>) => {
const counts = activeCounts;
const compiled = counts ? query.toSQL() : undefined;
const result = await execute(...executeArgs);
if (counts && compiled) {
counts.calls++;
const rows = Array.isArray(result) ? result.length : 0;
counts.rows += rows;
const statement = counts.statements.get(compiled.sql) ?? { ...compiled, calls: 0, rows: 0 };
statement.calls++;
statement.rows += rows;
counts.statements.set(compiled.sql, statement);
}
return result;
}) as typeof execute;
return query;
}) as typeof builder.from;
return builder;
};
} });
}
function summary(samples: number[]) {
const sorted = [...samples].sort((a, b) => a - b);
return { samplesMs: samples, medianMs: sorted[Math.floor(sorted.length / 2)], minMs: sorted[0], maxMs: sorted.at(-1) };
}
const keepAlive = setInterval(() => {}, 1_000);
let database: Awaited<ReturnType<typeof startEmbeddedPostgresTestDatabase>> | undefined;
let db: Db | undefined;
try {
database = await startEmbeddedPostgresTestDatabase("connection-install-benchmark-");
db = createDb(database.connectionString);
const observedDb = instrument(db);
const old = baselineModule.connectionManagementService(observedDb);
const treatment = connectionManagementService(observedDb);
assert.equal(typeof treatment.describeAgentInstallCapabilities, "function", "Implement the production batch first");
const results = [];
for (const n of [1, 8, 32, 100]) {
for (const installs of ["company", "partial"] as const) {
const companyId = randomUUID(), connectionId = randomUUID(), applicationId = randomUUID(), userId = randomUUID();
const candidateIds = Array.from({ length: n }, () => randomUUID());
await db.insert(companies).values({ id: companyId, name: `Capability benchmark ${n} ${installs}`, issuePrefix: `B${results.length}` });
await db.insert(agents).values(candidateIds.map((id, index) => ({ id, companyId, name: `Agent ${index}`, adapterType: "process" })));
await db.insert(companyMemberships).values({ companyId, principalType: "user", principalId: userId, status: "active", membershipRole: "member" });
await db.insert(principalPermissionGrants).values({ companyId, principalType: "user", principalId: userId,
permissionKey: "tools:manage_profiles", scope: { targetAgentIds: candidateIds } });
await db.insert(toolApplications).values({ id: applicationId, companyId, name: "Synthetic benchmark", type: "mcp_http" });
await db.insert(toolConnections).values({ id: connectionId, applicationId, companyId, uid: connectionId,
name: "Synthetic benchmark", transport: "mcp_remote", authKind: "none", config: { provider: "fixture" } });
await db.insert(toolConnectionInstalls).values(installs === "company"
? [{ companyId, connectionId, targetType: "company", targetId: companyId }]
: candidateIds.slice(0, Math.max(1, Math.floor(n / 4))).map(targetId => ({ companyId, connectionId, targetType: "agent" as const, targetId })));
await db.$client.unsafe("ANALYZE");
const actor: AuthorizationActor = { type: "board", source: "cloud_tenant", userId };
const resource = { type: "connection_management" as const, companyId, connectionId, operation: "configure", readOnly: false };
const baseline = async () => {
const capabilities = [];
for (const targetAgentId of candidateIds) capabilities.push(await old.capability(actor, "connections:profiles", { ...resource, targetAgentId }));
return capabilities;
};
const batch = () => treatment.describeAgentInstallCapabilities(actor, resource, candidateIds);
const samples = { baseline: [] as number[], treatment: [] as number[] };
const counters: Record<string, { calls: number; rows: number }> = {};
let captured: Record<string, Statement[]> = {};
for (let iteration = 0; iteration < 7; iteration++) {
const order = iteration % 2 ? ["treatment", "baseline"] as const : ["baseline", "treatment"] as const;
const outputs: Record<string, unknown> = {};
for (const name of order) {
activeCounts = { calls: 0, rows: 0, statements: new Map() };
const start = performance.now();
outputs[name] = await (name === "baseline" ? baseline() : batch());
const elapsed = performance.now() - start;
const counts = activeCounts;
activeCounts = undefined;
if (iteration >= 2) samples[name].push(elapsed);
const previous = counters[name];
const current = { calls: counts.calls, rows: counts.rows };
if (previous) assert.deepEqual(current, previous, "Stable fixture must generate stable work counts");
counters[name] = current;
if (iteration === 6) captured[name] = [...counts.statements.values()];
}
assert.deepEqual(outputs.treatment, outputs.baseline, `${installs} N=${n}: full positional capabilities`);
assert.equal((outputs.baseline as Array<{ allowed: boolean }>).length, n);
assert.ok((outputs.baseline as Array<{ allowed: boolean }>).every(capability => capability.allowed), "The benchmark must exercise every authorized retained target");
}
assert.ok(counters.treatment.calls < counters.baseline.calls || n === 1, "Batch must remove repeated SQL");
// This is a call-fan-out change. Individual statement plans are controls,
// not the explanation of the gain; capture them outside measured samples.
const plans = [];
for (const statement of captured.treatment) {
const [plan] = await db.$client.unsafe(`EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) ${statement.sql}`, statement.params as never[]);
plans.push({ ...statement, plan: plan["QUERY PLAN"] });
}
const before = summary(samples.baseline), after = summary(samples.treatment);
const targetLookups = (statements: Statement[]) => statements.filter(statement => statement.sql.includes('"agents"."id" =')).reduce((sum, statement) => sum + statement.calls, 0);
const expectedRetained = installs === "company" ? n : Math.max(1, Math.floor(n / 4));
assert.equal(targetLookups(captured.baseline), n * expectedRetained + n - expectedRetained);
assert.equal(targetLookups(captured.treatment), n);
const result = { n, installs, principal: "cloud noncreator member with target-scoped grant", completePositionalEquality: true,
baseline: { ...before, ...counters.baseline, targetLookups: targetLookups(captured.baseline) },
treatment: { ...after, ...counters.treatment, targetLookups: targetLookups(captured.treatment) },
speedup: before.medianMs! / after.medianMs!, statements: captured, treatmentPlans: plans };
results.push(result);
console.error(JSON.stringify({ n, installs, baselineMs: before.medianMs, treatmentMs: after.medianMs, counts: counters }));
}
}
const [version] = await db.$client.unsafe("SELECT version() AS version");
const settings = await db.$client.unsafe("SELECT name, setting, unit FROM pg_settings WHERE name IN ('shared_buffers','work_mem','effective_cache_size','jit') ORDER BY name");
console.log(JSON.stringify({ baselineRevision, treatmentRevision,
benchmarkSourceSha256: createHash("sha256").update(await readFile(fileURLToPath(import.meta.url))).digest("hex"),
treatmentSourceSha256: createHash("sha256").update(await readFile(new URL(`../${servicePath}`, import.meta.url))).digest("hex"),
uncommittedDiffSha256: createHash("sha256").update(git("diff", "HEAD")).digest("hex"),
dependencyLockfileSha256: createHash("sha256").update(await readFile(new URL("../pnpm-lock.yaml", import.meta.url))).digest("hex"),
baselineIsolation: { copiedModule: servicePath, imports: "Absolute current paths; all other server/shared/db runtime sources and lockfile verified unchanged from baseline", runtimeChanges },
node: process.version, postgres: version.version, settings, warmups: 2, samples: 5, alternatingOrder: true,
measurement: "Real service evaluation with real local PostgreSQL; select statement executions and returned Drizzle rows; no simulated transport latency",
limits: "Descriptive service batch only; no hosted latency attribution, agent fast path, route timing, concurrent revocation, or subtree hierarchy scaling claim",
results }, null, 2));
} finally {
activeCounts = undefined;
try {
await db?.$client.end({ timeout: 0 });
await database?.cleanup();
await rm(baselineDirectory, { recursive: true, force: true });
} finally { clearInterval(keepAlive); }
}