mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-02 16:58:52 +01:00
## 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>
418 lines
21 KiB
TypeScript
418 lines
21 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { PgDialect } from "drizzle-orm/pg-core";
|
|
import type { SQL } from "drizzle-orm";
|
|
import {
|
|
agents,
|
|
aiConnectionPools,
|
|
companyMemberships,
|
|
connectionGrants,
|
|
principalPermissionGrants,
|
|
routines,
|
|
toolConnections,
|
|
toolConnectionInstalls,
|
|
toolMcpGateways,
|
|
toolProfiles,
|
|
type Db,
|
|
} from "@paperclipai/db";
|
|
import { connectionManagementService } from "../services/connection-management.js";
|
|
import type { AuthorizationActor } from "../services/authorization.js";
|
|
import type { ConnectionManagementResource } from "../services/connection-management-policy.js";
|
|
|
|
const companyId = "00000000-0000-4000-8000-000000000001";
|
|
const connectionId = "00000000-0000-4000-8000-000000000002";
|
|
const userId = "capability-test-user";
|
|
const agentId = (index: number) => `00000000-0000-4000-8000-${String(index + 10).padStart(12, "0")}`;
|
|
const actor: AuthorizationActor = { type: "board", source: "cloud_tenant", userId };
|
|
const localActor: AuthorizationActor = { type: "board", source: "local_implicit" };
|
|
const resource: ConnectionManagementResource = {
|
|
type: "connection_management", companyId, connectionId, operation: "configure", readOnly: false,
|
|
};
|
|
const dialect = new PgDialect();
|
|
|
|
/** Only the query transport is synthetic. Both paths use the real services and policy evaluator. */
|
|
function fixture(count = 3) {
|
|
const agentRows = Array.from({ length: count }, (_, index) => ({
|
|
id: agentId(index), companyId, status: "idle", role: "engineer", permissions: {}, reportsTo: null,
|
|
}));
|
|
const state = {
|
|
activeMembership: true,
|
|
membershipRole: "member",
|
|
creatorId: userId,
|
|
connectionPurpose: "tool",
|
|
provider: null as string | null,
|
|
connectionExists: true,
|
|
missingConnections: new Set<string>(),
|
|
agentStatus: "idle",
|
|
installs: [] as Array<{ targetType: string; targetId: string }>,
|
|
missingAgents: new Set<string>(),
|
|
targetErrors: new Map<string, Error>(),
|
|
grants: new Map<string, Record<string, unknown> | null>(),
|
|
poolMembers: [] as Array<{ binding: { connectionId: string } }>,
|
|
afterTargetRead: undefined as ((targetId: string) => void) | undefined,
|
|
};
|
|
const reads = { total: 0, targetIds: [] as string[], connections: 0, pools: 0, profiles: 0, gateways: 0 };
|
|
const db = {
|
|
select() {
|
|
return {
|
|
from(table: unknown) {
|
|
return {
|
|
async where(condition: SQL) {
|
|
reads.total++;
|
|
const query = dialect.sqlToQuery(condition);
|
|
if (table === toolConnections) {
|
|
reads.connections++;
|
|
const selectedId = String(query.params[0]);
|
|
return state.connectionExists && query.params[1] === companyId && !state.missingConnections.has(selectedId) ? [{
|
|
id: selectedId, companyId, createdByUserId: state.creatorId,
|
|
connectionPurpose: selectedId === connectionId ? state.connectionPurpose : "tool",
|
|
credentialPolicy: "shared", config: state.provider ? { provider: state.provider } : {}, transportConfig: {},
|
|
}] : [];
|
|
}
|
|
if (table === toolConnectionInstalls) return state.installs;
|
|
if (table === companyMemberships) return state.activeMembership && query.params[0] === companyId
|
|
&& query.params[2] === userId ? [{
|
|
companyId, principalType: "user", principalId: userId,
|
|
status: "active", membershipRole: state.membershipRole,
|
|
}] : [];
|
|
if (table === principalPermissionGrants) {
|
|
const permissionKey = String(query.params[3]);
|
|
return state.grants.has(permissionKey) ? [{
|
|
companyId, principalType: "user", principalId: userId,
|
|
permissionKey, scope: state.grants.get(permissionKey),
|
|
}] : [];
|
|
}
|
|
if (table === connectionGrants) return [];
|
|
if (table === aiConnectionPools) {
|
|
reads.pools++;
|
|
return [{ id: connectionId, config: { members: state.poolMembers } }];
|
|
}
|
|
if (table === toolProfiles) { reads.profiles++; return []; }
|
|
if (table === toolMcpGateways) { reads.gateways++; return []; }
|
|
if (table === routines) return [];
|
|
if (table === agents) {
|
|
if (!query.sql.includes('"agents"."id" =')) return agentRows;
|
|
const targetId = String(query.params[0]);
|
|
reads.targetIds.push(targetId);
|
|
const error = state.targetErrors.get(targetId);
|
|
if (error) throw error;
|
|
const result = state.missingAgents.has(targetId) ? [] : agentRows
|
|
.filter(row => row.id === targetId)
|
|
.map(row => ({ ...row, status: state.agentStatus }));
|
|
state.afterTargetRead?.(targetId);
|
|
return result;
|
|
}
|
|
throw new Error(`Unexpected capability fixture query: ${query.sql}`);
|
|
},
|
|
};
|
|
},
|
|
};
|
|
},
|
|
} as unknown as Db;
|
|
const management = connectionManagementService(db);
|
|
const candidates = agentRows.map(row => row.id);
|
|
const baseline = async (
|
|
ids = candidates,
|
|
probeResource = resource,
|
|
probeActor = actor,
|
|
) => {
|
|
const results = [];
|
|
for (const targetAgentId of ids) {
|
|
results.push(await management.capability(probeActor, "connections:profiles", {
|
|
...probeResource, readOnly: false, targetAgentId,
|
|
}));
|
|
}
|
|
return results;
|
|
};
|
|
return { state, reads, management, candidates, baseline };
|
|
}
|
|
|
|
describe("connection agent-install capability batches", () => {
|
|
it("does no database work for an empty candidate list", async () => {
|
|
const f = fixture();
|
|
expect(await f.management.describeAgentInstallCapabilities(actor, resource, [])).toEqual([]);
|
|
expect(f.reads.total).toBe(0);
|
|
});
|
|
|
|
it("preserves candidate order, duplicate results and target-scoped grants without retained installs", async () => {
|
|
const f = fixture();
|
|
f.state.creatorId = "another-user";
|
|
f.state.grants.set("tools:manage_profiles", { targetAgentIds: [agentId(1)] });
|
|
const candidates = [agentId(1), agentId(0), agentId(1), agentId(2)];
|
|
const expected = await f.baseline(candidates);
|
|
expect(expected.map(result => result.allowed)).toEqual([true, false, true, false]);
|
|
expect(await f.management.describeAgentInstallCapabilities(actor, resource, candidates)).toEqual(expected);
|
|
});
|
|
|
|
it("preserves a mixture of existing and missing candidates", async () => {
|
|
const f = fixture();
|
|
const candidates = [agentId(1), agentId(99), agentId(0)];
|
|
const expected = await f.baseline(candidates);
|
|
expect(expected.map(result => result.allowed)).toEqual([true, false, true]);
|
|
expect(await f.management.describeAgentInstallCapabilities(actor, resource, candidates)).toEqual(expected);
|
|
});
|
|
|
|
it("denies every candidate when a retained installed target is unavailable", async () => {
|
|
const f = fixture();
|
|
f.state.installs = [{ targetType: "agent", targetId: agentId(99) }];
|
|
const expected = await f.baseline();
|
|
expect(expected.every(result => !result.allowed)).toBe(true);
|
|
expect(await f.management.describeAgentInstallCapabilities(actor, resource, f.candidates)).toEqual(expected);
|
|
});
|
|
|
|
it("does not hide a retained-target policy denial behind candidate-specific permission", async () => {
|
|
const f = fixture();
|
|
f.state.creatorId = "another-user";
|
|
f.state.grants.set("tools:manage_profiles", { targetAgentIds: [agentId(0)] });
|
|
f.state.installs = [{ targetType: "agent", targetId: agentId(1) }];
|
|
const expected = await f.baseline([agentId(0)]);
|
|
expect(expected[0]).toMatchObject({ allowed: false });
|
|
expect(await f.management.describeAgentInstallCapabilities(actor, resource, [agentId(0)])).toEqual(expected);
|
|
});
|
|
|
|
it("preserves candidate-dependent policy checks for an empty retained agent selector", async () => {
|
|
const f = fixture();
|
|
f.state.creatorId = "another-user";
|
|
f.state.grants.set("tools:manage_profiles", { targetAgentIds: [agentId(1)] });
|
|
f.state.installs = [{ targetType: "agent", targetId: "" }];
|
|
const expected = await f.baseline();
|
|
expect(expected.map(result => result.allowed)).toEqual([false, true, false]);
|
|
expect(await f.management.describeAgentInstallCapabilities(actor, resource, f.candidates)).toEqual(expected);
|
|
const laterFault = new Error("later allowed candidate database fault");
|
|
f.state.targetErrors.set(agentId(1), laterFault);
|
|
await expect(f.baseline()).rejects.toBe(laterFault);
|
|
await expect(f.management.describeAgentInstallCapabilities(actor, resource, f.candidates)).rejects.toBe(laterFault);
|
|
});
|
|
|
|
it("preserves output for retained candidates, new candidates and duplicates in the same batch", async () => {
|
|
const f = fixture();
|
|
f.state.installs = [{ targetType: "agent", targetId: agentId(0) }];
|
|
const candidates = [agentId(0), agentId(2), agentId(0), agentId(99), agentId(1)];
|
|
const expected = await f.baseline(candidates);
|
|
expect(expected.map(result => result.allowed)).toEqual([true, true, true, false, true]);
|
|
expect(await f.management.describeAgentInstallCapabilities(actor, resource, candidates)).toEqual(expected);
|
|
});
|
|
|
|
it("stops at an earlier retained denial without visiting a later infrastructure fault", async () => {
|
|
const f = fixture();
|
|
f.state.installs = [
|
|
{ targetType: "agent", targetId: agentId(99) },
|
|
{ targetType: "agent", targetId: agentId(1) },
|
|
];
|
|
f.state.targetErrors.set(agentId(1), new Error("later database fault"));
|
|
const expected = await f.baseline();
|
|
f.reads.targetIds.length = 0;
|
|
expect(await f.management.describeAgentInstallCapabilities(actor, resource, f.candidates)).toEqual(expected);
|
|
expect(f.reads.targetIds).not.toContain(agentId(1));
|
|
});
|
|
|
|
it("propagates an earlier retained infrastructure fault before any later denial", async () => {
|
|
const f = fixture();
|
|
f.state.installs = [
|
|
{ targetType: "agent", targetId: agentId(0) },
|
|
{ targetType: "agent", targetId: agentId(99) },
|
|
];
|
|
const fault = new Error("first database fault");
|
|
f.state.targetErrors.set(agentId(0), fault);
|
|
await expect(f.baseline()).rejects.toBe(fault);
|
|
f.reads.targetIds.length = 0;
|
|
await expect(f.management.describeAgentInstallCapabilities(actor, resource, f.candidates)).rejects.toBe(fault);
|
|
expect(f.reads.targetIds).toEqual([agentId(0)]);
|
|
});
|
|
|
|
it("converts only 403 faults to denied descriptions and continues in candidate order", async () => {
|
|
const f = fixture();
|
|
f.state.targetErrors.set(agentId(1), Object.assign(new Error("synthetic forbidden target"), { status: 403 }));
|
|
const expected = await f.baseline();
|
|
expect(expected.map(result => result.allowed)).toEqual([true, false, true]);
|
|
expect(await f.management.describeAgentInstallCapabilities(actor, resource, f.candidates)).toEqual(expected);
|
|
});
|
|
|
|
it.each([409, 500])("propagates a %s fault without evaluating later candidates", async status => {
|
|
const f = fixture();
|
|
const fault = Object.assign(new Error("synthetic infrastructure failure"), { status });
|
|
f.state.targetErrors.set(agentId(1), fault);
|
|
await expect(f.management.describeAgentInstallCapabilities(actor, resource, f.candidates)).rejects.toBe(fault);
|
|
expect(f.reads.targetIds).not.toContain(agentId(2));
|
|
});
|
|
|
|
it("always probes mutation authority even when a caller supplies readOnly or mutation:false", async () => {
|
|
const f = fixture();
|
|
f.state.installs = [{ targetType: "agent", targetId: agentId(99) }];
|
|
expect(await f.management.capability(localActor, "connections:profiles", {
|
|
...resource, targetAgentId: agentId(0),
|
|
}, { mutation: false })).toMatchObject({ allowed: true });
|
|
const extraCallerOptions = { mutation: false, readContext: undefined };
|
|
expect(await f.management.describeAgentInstallCapabilities(localActor, { ...resource, readOnly: true },
|
|
[agentId(0)], extraCallerOptions)).toEqual(await f.baseline([agentId(0)], resource, localActor));
|
|
});
|
|
|
|
it("ignores a caller's targetAgentId when evaluating each supplied candidate", async () => {
|
|
const f = fixture();
|
|
f.state.creatorId = "another-user";
|
|
f.state.grants.set("tools:manage_profiles", { targetAgentIds: [agentId(0)] });
|
|
expect(await f.management.describeAgentInstallCapabilities(actor, { ...resource, targetAgentId: agentId(0) },
|
|
[agentId(1)])).toEqual(await f.baseline([agentId(1)]));
|
|
});
|
|
|
|
it("keeps later batches and actual authorization fresh after membership revocation", async () => {
|
|
const f = fixture();
|
|
const description = await f.management.describeAgentInstallCapabilities(actor, resource, [agentId(0)]);
|
|
expect(description[0]).toMatchObject({ allowed: true });
|
|
expect(description[0]).not.toHaveProperty("reauthorize");
|
|
const context = await f.management.authorize(actor, "connections:profiles", { ...resource, targetAgentId: agentId(0) });
|
|
f.state.activeMembership = false;
|
|
expect(await f.management.describeAgentInstallCapabilities(actor, resource, [agentId(0)]))
|
|
.toEqual(await f.baseline([agentId(0)]));
|
|
expect((await f.baseline([agentId(0)]))[0]).toMatchObject({ allowed: false });
|
|
await expect(f.management.authorize(actor, "connections:profiles", { ...resource, targetAgentId: agentId(0) }))
|
|
.rejects.toMatchObject({ status: 403 });
|
|
await expect(context.reauthorize()).rejects.toMatchObject({ status: 403 });
|
|
});
|
|
|
|
it("reloads target-scoped grants on the next batch", async () => {
|
|
const f = fixture();
|
|
f.state.creatorId = "another-user";
|
|
f.state.grants.set("tools:manage_profiles", { targetAgentIds: [agentId(0)] });
|
|
expect((await f.management.describeAgentInstallCapabilities(actor, resource, [agentId(0)]))[0].allowed).toBe(true);
|
|
f.state.grants.delete("tools:manage_profiles");
|
|
expect((await f.management.describeAgentInstallCapabilities(actor, resource, [agentId(0)]))[0].allowed).toBe(false);
|
|
});
|
|
|
|
it("reloads a deleted connection on the next batch", async () => {
|
|
const f = fixture();
|
|
expect((await f.management.describeAgentInstallCapabilities(actor, resource, [agentId(0)]))[0].allowed).toBe(true);
|
|
f.state.connectionExists = false;
|
|
const result = await f.management.describeAgentInstallCapabilities(actor, resource, [agentId(0)]);
|
|
expect(result).toEqual(await f.baseline([agentId(0)]));
|
|
expect(result[0]).toMatchObject({ allowed: false, reason: "Connection is unavailable in this company" });
|
|
});
|
|
|
|
it("does not reuse a prior actor's or company's descriptions", async () => {
|
|
const f = fixture();
|
|
expect((await f.management.describeAgentInstallCapabilities(actor, resource, [agentId(0)]))[0].allowed).toBe(true);
|
|
const otherActor: AuthorizationActor = { ...actor, userId: "another-user" };
|
|
const otherResource = { ...resource, companyId: agentId(98) };
|
|
const actorResult = await f.management.describeAgentInstallCapabilities(otherActor, resource, [agentId(0)]);
|
|
const companyResult = await f.management.describeAgentInstallCapabilities(actor, otherResource, [agentId(0)]);
|
|
expect(actorResult).toEqual(await f.baseline([agentId(0)], resource, otherActor));
|
|
expect(companyResult).toEqual(await f.baseline([agentId(0)], otherResource));
|
|
expect(actorResult[0].allowed).toBe(false);
|
|
expect(companyResult[0].allowed).toBe(false);
|
|
});
|
|
|
|
it("allows descriptive staleness after a retained check while fresh authorization observes deletion", async () => {
|
|
const f = fixture(2);
|
|
f.state.installs = [{ targetType: "agent", targetId: agentId(0) }];
|
|
const targetResource = { ...resource, targetAgentId: agentId(1) };
|
|
const context = await f.management.authorize(actor, "connections:profiles", targetResource);
|
|
f.state.afterTargetRead = targetId => {
|
|
if (targetId === agentId(0)) f.state.missingAgents.add(targetId);
|
|
};
|
|
const described = await f.management.describeAgentInstallCapabilities(actor, resource, f.candidates);
|
|
expect(described.map(result => result.allowed)).toEqual([true, true]);
|
|
await expect(f.management.authorize(actor, "connections:profiles", targetResource))
|
|
.rejects.toMatchObject({ status: 403 });
|
|
await expect(context.reauthorize()).rejects.toMatchObject({ status: 403 });
|
|
const freshDescriptions = await f.management.describeAgentInstallCapabilities(actor, resource, f.candidates);
|
|
expect(freshDescriptions.every(result => !result.allowed)).toBe(true);
|
|
});
|
|
|
|
it.each([4, 8])("checks company-installed targets once instead of N squared for N=%s", async count => {
|
|
const f = fixture(count);
|
|
f.state.installs = [{ targetType: "company", targetId: companyId }];
|
|
const expected = await f.baseline();
|
|
expect(f.reads.targetIds).toHaveLength(count * count);
|
|
f.reads.targetIds.length = 0;
|
|
expect(await f.management.describeAgentInstallCapabilities(actor, resource, f.candidates)).toEqual(expected);
|
|
expect(f.reads.targetIds).toHaveLength(count);
|
|
expect(new Set(f.reads.targetIds)).toEqual(new Set(f.candidates));
|
|
});
|
|
|
|
it.each([
|
|
"AAAAAAAA-0000-4000-8000-000000000001",
|
|
"",
|
|
"not-a-uuid",
|
|
])("uses ordinary evaluation without repeating preparation for candidate %j", async candidate => {
|
|
const f = fixture(2);
|
|
f.state.installs = [{ targetType: "company", targetId: companyId }];
|
|
const candidates = [agentId(0), candidate, agentId(1)];
|
|
const expected = await f.baseline(candidates);
|
|
// The ordinary evaluator treats an empty selector as omitted.
|
|
expect(expected.map(result => result.allowed)).toEqual([true, candidate === "", true]);
|
|
f.reads.connections = 0;
|
|
expect(await f.management.describeAgentInstallCapabilities(actor, resource, candidates)).toEqual(expected);
|
|
expect(f.reads.connections).toBe(candidates.length);
|
|
});
|
|
|
|
it.each([
|
|
"AAAAAAAA-0000-4000-8000-000000000001",
|
|
"not-a-uuid",
|
|
])("uses ordinary evaluation without repeating preparation for retained selector %j", async targetId => {
|
|
const f = fixture();
|
|
f.state.installs = [{ targetType: "agent", targetId }];
|
|
const expected = await f.baseline();
|
|
expect(expected.every(result => !result.allowed)).toBe(true);
|
|
f.reads.connections = 0;
|
|
expect(await f.management.describeAgentInstallCapabilities(actor, resource, f.candidates)).toEqual(expected);
|
|
expect(f.reads.connections).toBe(f.candidates.length);
|
|
});
|
|
|
|
it.each(["profileId", "gatewayId", "routineId"] as const)("retains ordinary denial behavior for a missing %s selector", async selector => {
|
|
const f = fixture();
|
|
const selected = { ...resource, [selector]: agentId(99) };
|
|
const expected = await f.baseline(f.candidates, selected);
|
|
expect(expected.every(result => !result.allowed)).toBe(true);
|
|
expect(await f.management.describeAgentInstallCapabilities(actor, selected, f.candidates)).toEqual(expected);
|
|
});
|
|
|
|
it("retains recursive AI-pool checks through the ordinary path", async () => {
|
|
const f = fixture(2);
|
|
f.state.connectionPurpose = "ai";
|
|
f.state.poolMembers = [{ binding: { connectionId: agentId(99) } }];
|
|
const expected = await f.baseline();
|
|
expect(await f.management.describeAgentInstallCapabilities(actor, resource, f.candidates)).toEqual(expected);
|
|
expect(f.reads.pools).toBeGreaterThan(0);
|
|
f.state.missingConnections.add(agentId(99));
|
|
const denied = await f.baseline();
|
|
expect(denied.every(result => !result.allowed)).toBe(true);
|
|
expect(await f.management.describeAgentInstallCapabilities(actor, resource, f.candidates)).toEqual(denied);
|
|
});
|
|
|
|
it("retains proposed-provider scope checks through the ordinary path", async () => {
|
|
const f = fixture();
|
|
f.state.creatorId = "another-user";
|
|
f.state.provider = "current-provider";
|
|
f.state.grants.set("tools:manage_profiles", { providers: ["current-provider"] });
|
|
const selected = { ...resource, requestedProvider: "replacement-provider" };
|
|
const expected = await f.baseline(f.candidates, selected);
|
|
expect(expected.every(result => !result.allowed)).toBe(true);
|
|
expect(await f.management.describeAgentInstallCapabilities(actor, selected, f.candidates)).toEqual(expected);
|
|
f.state.grants.set("tools:manage_profiles", { providers: ["current-provider", "replacement-provider"] });
|
|
const allowed = await f.baseline(f.candidates, selected);
|
|
expect(allowed.every(result => result.allowed)).toBe(true);
|
|
expect(await f.management.describeAgentInstallCapabilities(actor, selected, f.candidates)).toEqual(allowed);
|
|
});
|
|
|
|
it("retains configure escalation for a proposed company installation", async () => {
|
|
const f = fixture();
|
|
f.state.creatorId = "another-user";
|
|
f.state.grants.set("tools:manage_profiles", null);
|
|
expect((await f.baseline())[0].allowed).toBe(true);
|
|
const selected = { ...resource, requestedInstallCompanyIds: [companyId] };
|
|
const expected = await f.baseline(f.candidates, selected);
|
|
expect(expected.every(result => !result.allowed)).toBe(true);
|
|
expect(await f.management.describeAgentInstallCapabilities(actor, selected, f.candidates)).toEqual(expected);
|
|
});
|
|
|
|
it("retains inactive-agent rejection through the ordinary path", async () => {
|
|
const f = fixture();
|
|
f.state.agentStatus = "paused";
|
|
const agentActor: AuthorizationActor = {
|
|
type: "agent", source: "agent_key", companyId, agentId: agentId(0), onBehalfOfUserId: userId,
|
|
};
|
|
const expected = await f.baseline(f.candidates, resource, agentActor);
|
|
expect(expected.every(result => !result.allowed)).toBe(true);
|
|
expect(await f.management.describeAgentInstallCapabilities(agentActor, resource, f.candidates)).toEqual(expected);
|
|
});
|
|
});
|