Files
PaperClipAI/doc/plans/2026-10-27-connection-grants-capability-batch.md
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

14 KiB

Connection-grants capability fan-out: design and safety proof

Date: 2026-10-27. This records the selected design before implementation. See implementation verification for measured results and completed acceptance checks.

Recommendation

Add one narrow, service-owned batch operation for describing which agents may receive a connection installation. Factor the common installed-agent checks out of the per-candidate loop. Keep authorization policy in its existing owner and keep actual mutations on the existing fresh authorization path.

Start with board actors describing a direct, saved, non-AI connection. Preserve the ordinary evaluator for delegated agents and complex resources until their additional obligations have executable parity evidence. This is a bounded first fix, not a general authorization cache or a rewrite of connection permissions.

The endpoint currently asks, for each candidate agent, whether the actor may change the connection's installations. Each answer checks all retained installed agents, followed by the candidate if it is not already installed. A company installation expands to the company roster. For 100 candidates with 100 retained targets, this means 10,000 target evaluations before the other route work. The proposed eligible path needs 100 retained-target evaluations, with extra evaluations only for candidates outside that set.

Those retained checks are real policy obligations. The fix removes their repeated execution; it must not remove their authority requirements.

What the evidence establishes

The prior observation window contains two successful grants GETs lasting 56.506 and 66.388 seconds. There is no matching request trace attributing their duration to this loop. See the measurement record. The source and local probes establish amplification, not the fraction of hosted latency it causes or the eventual improvement.

The production path is:

  • server/src/routes/tool-access.ts, describeConnectionCapabilities: selects the company roster and calls connections:profiles once per agent, with mutation authority and readOnly: false.
  • server/src/services/connection-management.ts, authorize: resolves the saved connection, expands its retained installations, then checks the ordered target set. capability converts only errors with status 403 into denials; other faults escape.
  • server/src/services/authorization.ts: owns membership and scoped permission decisions. Some subtree grants can read the whole company hierarchy for each target, so linear target evaluations do not guarantee linear rows read.

Three independent designs were compared: structural factoring, general lazy request caching, and a narrow guarded batch. A separate judge selected the narrow batch. The final design incorporates the structural candidate's preparation continuation and differential test approach, plus the cache candidate's careful first-visit ordering and separate work counters.

Local experiment Observed result Limit
Existing exported service, all-installed fixtures 4 agents caused 16 target reads; 8 caused 64 Synthetic database transport, not PostgreSQL
Handwritten factored terminal phase compared with the current service Six static output/fault cases matched; target reads fell 16 to 4 and 64 to 8 Treatment omits preparation and is not the proposed production implementation
General read-reuse experiment, 32 agents Physical transport reads fell 3,168 to 36, but 1,024 target invocations remained Demonstrates why caching alone can retain quadratic work
Existing policy with a grant scoped to one agent and no installations Named candidate allowed; targetless request denied Rules out an extra targetless authorization preflight
Existing service with an unavailable retained target Normal capability denied; mutation: false shortcut allowed Rules out bypassing retained installation checks
Existing continuation after membership removal Fresh reauthorize denied Does not prove a future batch's freshness

All three local proof scripts were rerun successfully by the primary agent and the independent judge. They execute unchanged exported services over synthetic Drizzle transport. They are level-4 evidence for the exercised current-code facts, not proof of the unimplemented batch, assembled HTTP equivalence, PostgreSQL performance, or delegated-actor parity.

Owning API and eligibility

The new operation belongs in connectionManagementService, beside capability. Its only initial consumer replaces the editableAgentIds loop. Use a fixed connections:profiles action and mutation-mode semantics rather than exposing general action or read-only knobs. Accept the original actor, base connection resource, ordered candidate IDs, and existing read context. Return positional capability descriptions; the route derives the existing editable-ID list.

Do not return an authorization context, audit authority, reusable session, cache handle, or continuation. Do not change the wire schema. Keep manager checks, configure/use capabilities, membership gates, grant identity filtering, and audience disclosure on their existing paths.

The initial optimized shape is deliberately explicit:

  • Board actor; one direct saved connection whose actual saved purpose is not AI.
  • No non-null profile, gateway, routine, project, or issue selector.
  • No own requestedProvider or requestedInstallCompanyIds property, and no selfServiceAgentInstalls: true request.
  • Validated, nonempty roster IDs in their original spelling and input order. Retained target IDs must also be canonical and nonempty. Unusual identifiers use the ordinary path rather than normalization: a saved empty target is a candidate-dependent selector in the legacy evaluator.

Use the saved connection's provider, purpose, and creator derivation, including its ordinary personal-grant handling. Caller labels cannot establish eligibility. Unsupported shapes retain the existing per-candidate evaluator. In particular, AI pools, profile bindings and selectors, current/proposed gateway resources, and delegated actor contexts keep all recursive checks.

Evaluation algorithm

Extract private preparation, target evaluation, and completion primitives from the existing evaluator. Both the ordinary and batch paths must consume these same primitives; do not copy policy into the route or implement a second policy engine. Ordinary authorization always starts with fresh private state.

  1. An empty candidate list returns an empty result without authorization, as the current loop does.
  2. Begin preparation for the first actual candidate in the current order. Discover eligibility during the normal saved-connection lookup. The private preparation frame is consumed exactly once. If ineligible, finish that same evaluation through the ordinary path, then evaluate the remaining candidates normally. Do not restart preparation to fall back.
  3. For eligible inputs, form the same ordered set of retained installation targets, including the existing company-install expansion. Evaluate each retained target once, using the original check order: connection policy, applicable proposed-provider/resource trust checks, target existence, and principal-specific target authority. The shared primitive preserves these obligations even where the initial eligibility guard excludes some branches.
  4. A retained-target 403 denies every candidate with the same current denial. A reached non-403 fault aborts the operation. Do not inspect later targets to discover a fault the original evaluator would not reach.
  5. After the retained prefix succeeds, visit candidates in input order. A candidate already in the retained set uses the final retained decision, matching the current ordered-set behavior and success reason. An additional candidate uses the shared target primitive. Its 403 denies that candidate; a reached non-403 fault aborts. Return one result per input position.
  6. With no retained installations, the candidate is the policy target. Do not add a targetless preflight: a grant scoped to that candidate may correctly allow it while denying an unspecified target.

For N candidate positions and S distinct retained targets, the target phase is bounded by S + N evaluations, and often fewer when candidates are retained. This is not a promise of S + N SQL statements. Authorization may still read membership, grants, and hierarchy for each distinct target; complex fallback keeps its present cost. Measure those separately before widening the change.

Avoid eager prefetch and Promise.all here. A denied retained target currently prevents later target lookups. Loading those targets first changes which faults are reached. Parallelizing the old loop also retains its total work.

Two safety facts and the blast radius

1. A displayed permission must retain every existing installation obligation and its own target scope. The batch cannot turn a denied or absent retained target into editable siblings, substitute a targetless decision, or reuse an allowed candidate's decision for a different candidate. Preserve positional results, reason, operation, ordered deduplication, and the 403/fatal-fault boundary. Existing-code counterexamples prove these shortcuts are unsafe. Actual old/new service and route comparisons remain required to prove the proposed change.

2. A description must never become reusable mutation authority. Batch state exists only inside one call and cannot cross actors, companies, connections, requests, operations, or human continuations. Actual writes and reauthorize start fresh. The batch returns descriptions only; identity disclosure does not consume them as authority.

There is a deliberate consistency change: reused facts and retained decisions are observed once during the description rather than reread for every candidate. This is neither a database snapshot nor an atomic revocation boundary. A revoke or target deletion during the call can leave a briefly stale displayed choice; a second GET and an actual mutation must observe fresh authority. Sequential first visits preserve static-fact failure order, not every possible failure or revocation between removed duplicate reads. Document and test this honestly.

Delegated agents remain on the ordinary path initially. captureRunIdentity locks and can reconcile pending steering identity; it is not a pure read. A future extension must prove run ownership, work mode, API-key status, responsible human and external-human ceilings, and pending-identity behavior. Preserve the original actor reference because existing read-context reuse checks identity by reference. Do not memoize capture or attach batch state to that context.

The intended change touches the connection-management owner, its route consumer, and focused tests. Existing connection policy is the authority. There is no schema migration, provider protocol change, background worker, persistent cache, UI contract change, or blanket creator/manager allow rule in this design.

Implementation and acceptance gates

  1. Shared-helper refactor: preserve ordinary evaluation before enabling the batch. Prove preparation runs exactly once for the first candidate on both eligible and fallback paths. Keep its continuation private and consumed.
  2. Actual service differential: compare the new production batch against the old full evaluator with a disposable PostgreSQL fixture. Compare every positional allowed, reason, and operation, not just editable counts. Cover empty/duplicate candidates; no, partial, and company installations; scoped grants with no installations; missing/foreign retained and candidate targets; local board and Cloud creator, owner, administrator, operator, ordinary member, viewer, and inactive membership.
  3. Failures and fallback: retained denial before an unreachable fault; candidate denial followed by a reached later fault; preparation/target 500 and relevant reconciliation 409; only 403 converted. Exercise every excluded shape, including empty AI pools, explicit null selectors, proposed providers, company-install changes, profile selectors, and linked gateway obligations.
  4. Full grants GET: compare complete assembled responses, including order, editable IDs, configure/use flags, personal identity filtering, manager visibility, audience members, and route access gates. Exercise the existing connection Permissions and agent installation UI consumers.
  5. Freshness and isolation: controlled mid-batch revoke/delete; fresh second GET; GET then revoke then real mutation; delayed human/OAuth continuation; concurrent actors, companies, and connections. Agent fast-path enablement additionally requires real run/key/steering and lock-contention fixtures.
  6. Performance: at roster sizes 1, 8, 32, and 100 or larger, count preparation, target evaluations, policy decisions, SQL calls, returned rows, and wall time separately. Test subtree grants and unrelated-request responsiveness. Measure assembled route phases and database/pool delay before attributing savings.

After implementation, run the relevant regression suite and the repository's required typecheck, tests, and build before PR hand-off. Require passing final-head CI and review before hosted rollout. Repeat acceptance on the exact serving revision, verify both response behavior and latency with enough samples, and retain a way to restore the ordinary evaluator if parity fails.

This design pass changed no application behavior and performed no hosted mutation. It establishes a preferred implementation and executable current-code safety constraints. Complete batch parity and production latency improvement remain unproved until the implementation and acceptance gates above pass.