Files
PaperClipAI/server/src/__tests__/connection-management.integration.test.ts
T
DottaandClaude Opus 5.5 353945c86d fix(tool-gateway): let agents create gateway tokens without receiving the value (#20613)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Tool gateways let external MCP clients use a company's tools with a
gateway client token
> - Since delegated connection management (#17737), an agent can create
and revoke gateway client tokens for its responsible person
> - #20581 stopped agents from getting the one-time token value, but it
did this by making token creation person-only
> - The product decision is that agents keep their delegated authority
to create and revoke tokens
> - This pull request restores agent token creation, but the agent never
receives the token value: the person reveals the value once on the
gateway's Tokens page
> - The benefit is that an agent can do the rotation work for the
person, and the secret still never enters chat, run events, logs or tool
output

## Linked Issues or Issue Description

Refs #17737
Refs #20581

**What happened**
After #20581, `POST /api/tool-gateway/gateways/{gatewayId}/tokens`
returns 403 `gateway_token_requires_person` to every agent caller.
Agents lost the delegated authority to create gateway client tokens that
#17737 gave them. Before #20581, an agent that created a token received
the plaintext value in its tool result and could repeat it in chat.

**Expected behavior**
An agent with delegated connection management can create and revoke
gateway client tokens. The agent never receives a token value. The
responsible person gets the value once, on the gateway's Tokens page.
Person callers keep today's behavior.

**Steps to reproduce**
1. Give an agent delegated connection management for a responsible
person with `tools:admin`.
2. As the agent, call `POST
/api/tool-gateway/gateways/{gatewayId}/tokens` (for example through
`call_api`).
3. See 403 `gateway_token_requires_person`.

## What Changed

- Agent create: `POST /api/tool-gateway/gateways/:gatewayId/tokens` from
an agent with delegated authority returns 201 with token metadata only
(`id`, `tokenPrefix`, `expiresAt`, `pendingReveal: true`), plus
`tokensPath`, a `reveal_credential` `requiredUserAction` for the
responsible person, and a `nextStep` instruction. The saved row has no
usable value: its stored hash is of a value that is generated and then
discarded. Gateway authentication also rejects `pendingReveal` rows.
- Person reveal: new person-only `POST
/api/tool-gateway/gateway-tokens/:tokenId/reveal`. It generates the
value, stores its hash, clears `pendingReveal` and returns the value
once. A conditional update makes a second or concurrent reveal return
409. Only the token's responsible person can reveal it (403
`gateway_token_reveal_requires_responsible_person`). Agent callers get
403 `gateway_token_requires_person`. A token revoked before reveal
cannot be revealed.
- Why a reveal that generates the value: no stored plaintext existed to
reveal, and storing one (even encrypted) for later display adds a secret
at rest. Generating the value at reveal time keeps the existing one-time
display and stores only a hash.
- Schema: `tool_mcp_gateway_tokens.pending_reveal boolean not null
default false` (migration `0460_gateway_token_pending_reveal`). Shared
`ToolMcpGatewayToken.pendingReveal`, and `ConnectionRequiredUserAction`
gets `reveal_credential` / `gateway_token`.
- UI: the Tokens page shows a "Waiting for you to reveal" section for
pending tokens with a "Reveal token" button. The value appears in the
existing "New token — copy now" one-time block. Pending tokens show the
status "Not revealed".
- Agent gateway create: the 201 for agent callers also includes
`gatewayPath`, `tokensPath` and a `nextStep` that tells the agent to
link the saved gateway.
- The agent create-token `nextStep` and the catalog say not to revoke
the old token in the same turn: its client keeps working on it until the
person installs the new value.
- The reveal update and its `tool_gateway.token_revealed` activity entry
commit in one transaction. The Tokens page offers Reveal only for tokens
that act for the signed-in person.
- API reference: new `docs/api/tool-gateway-tokens.md` (create, reveal,
revoke) in the docs nav.
- Revoke stays allowed for agents with delegated authority (unchanged
route).
- API catalog: create-gateway, create-token, reveal and revoke
descriptions now say who acts, that agents never receive values, the
page paths, and that the old token is revoked only after the client
works with the new one.
- Agent access map: create-token policy note and a new reveal entry.
- Evals `gateways-profiles.create-gateway` and
`gateways-profiles.rotate-token`:
- Remove the keyword grader (`human|operator|admin|manage`) and the
handoff text checks. Grade saved state, rendered links and secret
absence. A plain reply or a question card is accepted.
- Remove the setup probes that called the create routes as the agent.
Under delegated authority, those probes created a real gateway
("Forbidden agent gateway"), which failed create-gateway before the
agent ran.
- create-gateway: exactly one gateway for the intended profile and
company, created by the agent (or by the person after a handoff); the
person opens it from a rendered link; attribution matches the creator;
any agent-created token is still pending reveal; no token value in chat,
cards or run events.
- rotate-token: after the agent's turn, both setup tokens still work and
any replacement the agent created is pending the person's reveal; the
person follows the rendered Tokens page link and reveals (or issues) the
value in a private browser; the new value works before the old token is
revoked; the unrelated client keeps access; no token value appears
anywhere, including a regex scan for any full `pcgw_<id>.<secret>`
value.
- The changed scenario context and outcomes are recorded in
`chat-full-catalog-testops-manifest.json` `changedFields` and pinned in
the manifest test. The inventory digest, descriptors (`oracleVersion`
v2, `sourceFiles`) and SDK public input pins are updated. The unused
`external-gateway-handoff.ts` module is removed.
- Fixture realism: the create-gateway profile now allows a real read
tool (an empty profile made the agent ask which apps to add instead of
finishing), and the rotation tokens get a neutral owner note instead of
"Disposable rotation fixture" (the agent read that note as an eval
hint).
- Shared private-browser helpers (`external-private-browser.ts`,
`external-private-browser-worker.ts`) get one optional `pendingTokenId`
input and one `revealedPending` receipt field.

## Verification

- Red: on `master` route code, the new integration tests `lets agents
create and revoke gateway client tokens without ever receiving a token
value` and `reveals an agent-created gateway token value only to its
responsible person` fail (403 `gateway_token_requires_person`).
- Green: `cd server && npx vitest run
src/__tests__/connection-management.integration.test.ts -t gateway` (25
passed). The test checks that the agent `call_api` result has no `token`
field and no token-shaped value, that the agent cannot reveal, that the
person reveals once (second reveal 409) and the value authenticates,
that agent revoke works and stops the value, that a token revoked before
reveal never gets a value, and that person create still returns the
value.
- `npx vitest run src/__tests__/tool-gateway.test.ts
src/__tests__/openapi-routes.test.ts
src/services/native-runtime/runner-api.test.ts` (2247 passed).
- `cd ui && npx vitest run
src/pages/apps/gateways/panels/TokensPanel.test.tsx
src/pages/apps/gateways/gateway-helpers.test.ts` (16 passed); `pnpm
check:token-gates` clean.
- Eval graders and catalog: `pnpm exec vitest run --config
tests/runner-e2e/vitest.config.ts` on the gateway oracle tests,
`external.test.ts`, catalog consistency/contract, TestOps
export/retained, phase/clock callers and SDK input/type tests (all
pass); `node --test` on the TestOps manifest, adapter, contract,
streaming and reconcile tests (all pass); `node
tests/runner-e2e/source-sdk-public-inputs.mjs <HEAD> --check`
(unchanged).
- `node scripts/check-maps.mjs` passes. `tsc --noEmit` for `server`,
`ui` and `tests/runner-e2e`: no errors in changed files.
- Paid end-to-end run of both eval cases, run
[42872921890](https://github.com/paperclipai/paperclip/actions/runs/42872921890)
on 39f38a7f6c (before the last three commits): both FAILED, for
agent-behavior reasons the later commits address. Not rerun yet.
- rotate-token: the agent created the replacement (pending reveal, no
value), then revoked the old token in the same turn, despite the create
response saying to wait. Its reasoning cited the eval-looking fixture
note. The grader rejected the premature revocation. The reply linked the
Tokens page correctly.
- create-gateway: the agent created the gateway and one pending token,
then asked a question card about which apps to add to the empty profile,
with no link to the gateway. The grader rejected the missing link.
- Token values: a scan of all evidence files of both cells found no full
`pcgw_<id>.<secret>` value and no `pcgw_<uuid>` form. Agent `call_api`
payloads in run events are redacted; the integration test proves the
agent tool result has no value.

## Risks

- Behavior change for agents: token create returns metadata and a Tokens
page path, not a value. Agents that expect `token` in the response must
send the person to the Tokens page.
- Migration adds one boolean column with a default. Existing tokens get
`false` and keep working.
- A pending token that is never revealed stays as an unusable row until
someone revokes it. It cannot authenticate.

## Model Used

- Anthropic Claude Opus 5.5 (`claude-opus-5-5`), tool use and code
execution in Claude Code.

## 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
- [ ] All Paperclip CI gates are green
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 15:13:34 -06:00

2070 lines
193 KiB
TypeScript

import { createToolGatewayService } from "../services/tool-gateway.js";
import { commitToolActionReview } from "../services/tool-action-review.js";
import { googleWorkspaceSelectedScopes, defaultGitHubReviewPolicy, SLACK_CHAT_BOT_SCOPES, SLACK_BOT_TOOL_SCOPES } from "@paperclipai/shared";
import { createHash, randomBytes, randomUUID, generateKeyPairSync } from "node:crypto";
import { join } from "node:path";
import { createServer } from "node:http";
import { and, eq, sql } from "drizzle-orm";
import { beforeAll, afterAll, describe, expect, it, vi } from "vitest";
import { createDb, authUsers, activityLog, agentApiKeys, companies, companySecrets, companySecretVersions, chatConversations, chatDeliveries, chatEndpoints, chatExternalPrincipals, chatIdentityLinks, toolPolicies, toolRateLimitCounters, connectionGrants, connectionGrantMembers, connectionGrantDelegations, toolAccessAuditEvents, toolOauthStates, issueThreadInteractions, connectionIntentDeliveries, agents, companyMemberships, heartbeatRuns, issues, principalPermissionGrants, projects, toolApplications, toolConnections, toolConnectionInstalls, aiConnectionPools, toolMcpGateways, toolMcpGatewayTokens, toolActionRequests, toolCatalogEntries, toolInvocations, toolCallEvents, toolProfileEntries, toolProfiles, toolProfileBindings, plugins, adapterAuthSessions, environments, routines, chatGitHubReviews } from "@paperclipai/db";
import { findSatisfiedToolConnection, satisfiedConnectionIntentService } from "../services/satisfied-connection-intents.js";
import { connectionManagementService, continuingConnectionManagement } from "../services/connection-management.js";
import { aiConnectionService } from "../services/ai-connections.js";
import { redactRunnerApiPayloads } from "../services/native-runtime/runner-api-log-redaction.js";
import * as remoteEndpointGuard from "../services/remote-http-endpoint-guard.js";
import * as remoteHttpFetch from "../services/remote-http-fetch.js";
import * as runIdentity from "../services/run-identity.js";
import * as connectionManagementModule from "../services/connection-management.js";
import * as localBrowserLogin from "../services/local-ai-browser-login.js";
import { createLocalAgentJwt } from "../agent-auth-jwt.js";
import { startRunnerApiTestServer } from "./helpers/runner-api-server.js";
import { getServerAdapter, registerServerAdapter, unregisterServerAdapter } from "../adapters/index.js";
import { configureAgentLifecycle } from "../services/agent-lifecycle.js";
import { agentService } from "../services/agents.js";
import { accessService } from "../services/access.js";
import { secretService } from "../services/secrets.js";
import { localEncryptedProvider } from "../secrets/local-encrypted-provider.js";
import { instanceSettingsService } from "../services/instance-settings.js";
import { PaperclipRunnerToolAuthority } from "../services/native-runtime/paperclip-runner-tool-authority.js";
import type { AuthorizationActor } from "../services/authorization.js";
import type { ConnectionManagementResource } from "../services/connection-management-policy.js";
vi.mock("../services/agent-lifecycle-driver.js", () => ({
createLifecycleDriver: () => ({ requiredPluginIds: async () => [], runPlugin: async () => "complete", runHost: async () => "complete" }),
}));
vi.mock("../services/chat-channels/github/webhook-recovery-work.js", async importOriginal => {
const original = await importOriginal<typeof import("../services/chat-channels/github/webhook-recovery-work.js")>();
return { ...original, registerGitHubWebhookRecoveryWork: (...args: Parameters<typeof original.registerGitHubWebhookRecoveryWork>) =>
original.registerGitHubWebhookRecoveryWork(args[0], args[1], () => false) };
});
// All requests traverse the assembled server and production bearer middleware.
// No req.actor injection or special permission grants establish access.
describe("connection management with production default agents", () => {
let server: Awaited<ReturnType<typeof startRunnerApiTestServer>>;
let lifecycle: ReturnType<typeof configureAgentLifecycle>;
let provider: ((url: string, init?: RequestInit) => Promise<Response>) | null = null;
let providerFetch: ReturnType<typeof vi.spyOn>;
beforeAll(async () => {
vi.stubEnv("PAPERCLIP_AGENT_JWT_SECRET", randomUUID());
vi.stubEnv("PAPERCLIP_RESPONSIBLE_USER_AUTHZ_MODE", "shadow");
vi.stubEnv("PAPERCLIP_AUTH_RATE_LIMIT_ENABLED", "false");
const realFetch = globalThis.fetch;
providerFetch = vi.spyOn(globalThis, "fetch").mockImplementation((input, init) => {
const url = String(input);
if (new URL(url).hostname === "127.0.0.1") return realFetch(input, init);
if (provider) return provider(url, init);
throw new Error("Unexpected external request in provider fixture");
});
server = await startRunnerApiTestServer({ authPublicBaseUrl: "https://paperclip.fixture.invalid", humanSessions: true, databaseUrl: process.env.PAPERCLIP_CONNECTION_MANAGEMENT_TEST_DATABASE_URL });
lifecycle = configureAgentLifecycle(server.db, { requiredPluginIds: async () => [], runPlugin: async () => "complete", runHost: async () => "complete" });
vi.stubEnv("PAPERCLIP_SECRETS_MASTER_KEY_FILE", join(server.root, "master.key"));
await instanceSettingsService(server.db).updateExperimental({ enableChatConnectors: true, enableGitHubReviewBots: true, enableGoogleWorkspaceConnector: true });
}, 90_000);
afterAll(async () => { await lifecycle?.stop(); await server?.close(); providerFetch?.mockRestore(); vi.unstubAllEnvs(); vi.unstubAllGlobals(); });
async function fixture() {
const base = await server.fixture({ connectionScenario: "fresh" });
const userId = base.responsibleUserId!;
const access = accessService(server.db);
await access.ensureMembership(base.companyId, "user", userId, "operator", "active");
await access.ensureRoleDefaultGrants(base.companyId, userId, "operator", userId);
await server.db.update(companies).set({ requireBoardApprovalForNewAgents: false }).where(eq(companies.id, base.companyId));
const cookie = await server.humanSession(userId);
const created = await fetch(`${server.apiUrl}/api/companies/${base.companyId}/agents`, {
method: "POST", headers: { cookie, "Content-Type": "application/json", origin: server.apiUrl },
body: JSON.stringify({ name: "Fresh standard agent", role: "engineer", adapterType: "paperclip_runner", adapterConfig: { provider: "codex" },
runtimeConfig: { heartbeat: { enabled: false, wakeOnDemand: false } }, permissions: {} }),
});
const agent = await created.json();
expect(agent, JSON.stringify(agent)).toHaveProperty("id");
expect(created.status).toBe(201);
await lifecycle.process(agent.id);
await vi.waitFor(async () => expect((await agentService(server.db).getById(agent.id))?.status).toBe("idle"), { timeout: 10_000 });
await server.db.update(heartbeatRuns).set({ agentId: agent.id }).where(eq(heartbeatRuns.id, base.runId));
await server.db.update(issues).set({ assigneeAgentId: agent.id, responsibleUserId: userId }).where(eq(issues.id, base.issueId));
const key = await agentService(server.db).createApiKey(agent.id, "integration", { kind: "standard" }, { responsibleUserId: userId });
const jwt = createLocalAgentJwt(agent.id, base.companyId, "paperclip_runner", base.runId, userId)!;
const request = async (method: string, path: string, body?: unknown, token = key.token) => {
const response = await fetch(server.apiUrl + (path.startsWith("/mcp/") ? "" : "/api") + path, { method, headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" }, ...(body === undefined ? {} : { body: JSON.stringify(body) }) });
return { status: response.status, body: await response.json().catch(() => null) };
};
return { ...base, agentId: agent.id, userId, key, jwt, request,
authority: new PaperclipRunnerToolAuthority(server.db, { companyId: base.companyId, agentId: agent.id, issueId: base.issueId, runId: base.runId, apiUrl: server.apiUrl, storage: server.storage }) };
}
it.each(["tool", "ai"] as const)("matches ordinary %s install capabilities on real PostgreSQL for scoped, retained and fallback resources", async (connectionPurpose) => {
const f = await server.fixture({ connectionScenario: "fresh" });
const candidateIds = [f.agentId, randomUUID(), randomUUID()];
await server.db.insert(agents).values(candidateIds.slice(1).map((id, index) => ({ id, companyId: f.companyId, name: `Capability target ${index}`, adapterType: "process" })));
const [application] = await server.db.insert(toolApplications).values({ companyId: f.companyId, name: "Capability differential", type: "mcp_http" }).returning();
const [connection] = await server.db.insert(toolConnections).values({ companyId: f.companyId, applicationId: application.id,
uid: randomUUID(), name: "Capability differential", connectionPurpose,
transport: connectionPurpose === "ai" ? "runtime_auth" : "mcp_remote", authKind: "none", createdByUserId: "different-creator" }).returning();
await server.db.insert(principalPermissionGrants).values({ companyId: f.companyId, principalType: "user", principalId: f.responsibleUserId!,
permissionKey: "tools:manage_profiles", scope: { targetAgentIds: candidateIds.slice(0, 2) } });
const service = connectionManagementService(server.db);
const actor: AuthorizationActor = { type: "board", userId: f.responsibleUserId, source: "cloud_tenant" };
const resource = { type: "connection_management" as const, companyId: f.companyId, connectionId: connection.id, operation: "configure", readOnly: false };
const compare = async (base: ConnectionManagementResource = resource, candidates: readonly string[] = candidateIds, principal = actor) => {
const baseline = [];
for (const targetAgentId of candidates) baseline.push(await service.capability(principal, "connections:profiles", { ...base, targetAgentId }));
const batch = await service.describeAgentInstallCapabilities(principal, base, candidates);
expect(batch).toEqual(baseline);
return batch;
};
expect((await compare()).map(result => result.allowed)).toEqual([true, true, false]);
expect(await service.capability(actor, "connections:profiles", resource)).toMatchObject({ allowed: false });
expect(await compare(resource, [])).toEqual([]);
expect((await compare(resource, [candidateIds[1], candidateIds[0], candidateIds[1]])).map(result => result.allowed)).toEqual([true, true, true]);
// A saved empty text target leaves the candidate ID in the ordinary
// evaluator's resolved policy input, so it is not a shared target prefix.
await server.db.insert(toolConnectionInstalls).values({ companyId: f.companyId, connectionId: connection.id, targetType: "agent", targetId: "" });
const emptyTargetCandidates = [candidateIds[2], candidateIds[0]];
const emptyTargetBaseline = [];
for (const targetAgentId of emptyTargetCandidates) {
emptyTargetBaseline.push(await service.capability(actor, "connections:profiles", { ...resource, targetAgentId }));
}
expect(emptyTargetBaseline.map(capability => capability.allowed)).toEqual([false, true]);
expect(await service.describeAgentInstallCapabilities(actor, resource, emptyTargetCandidates)).toEqual(emptyTargetBaseline);
await server.db.delete(toolConnectionInstalls).where(eq(toolConnectionInstalls.connectionId, connection.id));
await server.db.insert(toolConnectionInstalls).values({ companyId: f.companyId, connectionId: connection.id, targetType: "agent", targetId: candidateIds[0] });
expect((await compare()).map(result => result.allowed)).toEqual([true, true, false]);
await server.db.insert(toolConnectionInstalls).values({ companyId: f.companyId, connectionId: connection.id, targetType: "agent", targetId: candidateIds[2] });
expect((await compare()).every(result => !result.allowed)).toBe(true);
await server.db.delete(toolConnectionInstalls).where(eq(toolConnectionInstalls.connectionId, connection.id));
await server.db.insert(toolConnectionInstalls).values({ companyId: f.companyId, connectionId: connection.id, targetType: "company", targetId: f.companyId });
expect((await compare()).every(result => !result.allowed)).toBe(true);
// JavaScript target sets distinguish UUID spelling; PostgreSQL's uuid
// lookup does not. Noncanonical candidates must retain ordinary evaluation.
for (const spellings of [candidateIds.map(id => id.toUpperCase()),
[candidateIds[0].toUpperCase(), candidateIds[1], candidateIds[0], candidateIds[1].toUpperCase(), candidateIds[0].toUpperCase()]]) {
expect((await compare(resource, spellings, { type: "board", source: "local_implicit" })).map(result => result.allowed))
.toEqual(spellings.map(() => true));
}
await server.db.delete(toolConnectionInstalls).where(eq(toolConnectionInstalls.connectionId, connection.id));
await server.db.insert(toolConnectionInstalls).values({ companyId: f.companyId, connectionId: connection.id, targetType: "agent", targetId: randomUUID() });
expect((await compare(resource, candidateIds, { type: "board", source: "local_implicit" })).map(result => result.reason))
.toEqual(candidateIds.map(() => "Target agent is outside this company"));
await server.db.delete(toolConnectionInstalls).where(eq(toolConnectionInstalls.connectionId, connection.id));
const [foreign] = await server.db.insert(agents).values({ companyId: f.foreignCompanyId, name: "Foreign capability target", adapterType: "process" }).returning();
expect((await compare(resource, [candidateIds[0], foreign.id], { type: "board", source: "local_implicit" })).map(result => result.allowed))
.toEqual([true, false]);
const [profile] = await server.db.insert(toolProfiles).values({ companyId: f.companyId, name: "Fallback profile", profileKey: randomUUID(), defaultAction: "deny" }).returning();
await compare({ ...resource, profileId: profile.id });
// Proposed changes also stay on the full evaluator, including own-property presence.
await compare({ ...resource, requestedProvider: null });
const faultingDb = new Proxy(server.db, { get(target, key, receiver) {
if (key !== "select") return Reflect.get(target, key, receiver);
return (...fields: Parameters<typeof target.select>) => {
const builder = target.select(...fields), 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 compiled = query.toSQL();
if (compiled.sql.includes('"agents"."id" =') && compiled.params[0] === candidateIds[1]) {
// A real database failure at the visited target boundary, rather
// than a fake permission denial or a mocked query result.
await server.db.$client.unsafe("SELECT 1 / 0");
}
return execute(...executeArgs);
}) as typeof execute;
return query;
}) as typeof builder.from;
return builder;
};
} });
const faulting = connectionManagementService(faultingDb);
const localActor: AuthorizationActor = { type: "board", source: "local_implicit" };
await expect(faulting.capability(localActor, "connections:profiles", { ...resource, targetAgentId: candidateIds[1] })).rejects.toThrow(/division by zero/);
await expect(faulting.describeAgentInstallCapabilities(localActor, resource, [candidateIds[1]])).rejects.toThrow(/division by zero/);
await server.db.insert(toolConnectionInstalls).values({ companyId: f.companyId, connectionId: connection.id, targetType: "agent", targetId: randomUUID() });
const denied = await faulting.capability(localActor, "connections:profiles", { ...resource, targetAgentId: candidateIds[1] });
expect(denied).toMatchObject({ allowed: false, reason: "Target agent is outside this company" });
expect(await faulting.describeAgentInstallCapabilities(localActor, resource, [candidateIds[1]])).toEqual([denied]);
});
it.each(["tool", "ai"] as const)("preserves the complete %s grants GET across human roles and denies a real install mutation after revocation", async (connectionPurpose) => {
const f = await server.fixture({ connectionScenario: "fresh" });
const candidateIds = [f.agentId, randomUUID(), randomUUID()];
await server.db.insert(agents).values(candidateIds.slice(1).map((id, index) => ({ id, companyId: f.companyId, name: `Grant target ${index}`, adapterType: "process" })));
const users = { creator: f.responsibleUserId!, manager: randomUUID(), member: randomUUID(), viewer: randomUUID(), inactive: randomUUID(),
admin: randomUUID(), owner: randomUUID(), operator: randomUUID() };
for (const [name, userId] of Object.entries(users)) {
if (name !== "creator") {
await server.db.insert(authUsers).values({ id: userId, name, email: `${userId}@fixture.invalid`, emailVerified: true, createdAt: new Date(), updatedAt: new Date() });
await server.db.insert(companyMemberships).values({ companyId: f.companyId, principalType: "user", principalId: userId,
membershipRole: ["viewer", "admin", "owner", "operator"].includes(name) ? name : "member", status: name === "inactive" ? "inactive" : "active" });
}
}
for (const permissionKey of ["tools:manage_connections", "tools:manage_profiles", "tools:use", "agents:configure"]) {
await server.db.insert(principalPermissionGrants).values([users.manager, users.admin].map(principalId => ({ companyId: f.companyId, principalType: "user", principalId, permissionKey })));
}
await server.db.insert(principalPermissionGrants).values(["tools:manage_profiles", "agents:configure"].map(permissionKey => ({ companyId: f.companyId,
principalType: "user", principalId: users.member, permissionKey,
scope: permissionKey === "tools:manage_profiles" ? { targetAgentIds: candidateIds.slice(0, 2) } : null })));
const [application] = await server.db.insert(toolApplications).values({ companyId: f.companyId, name: "Grants response parity", type: "mcp_http" }).returning();
const [connection] = await server.db.insert(toolConnections).values({ companyId: f.companyId, applicationId: application.id,
uid: randomUUID(), name: "Grants response parity", connectionPurpose,
transport: connectionPurpose === "ai" ? "runtime_auth" : "mcp_remote", authKind: "none", createdByUserId: users.creator }).returning();
const grants = await server.db.insert(connectionGrants).values([
{ companyId: f.companyId, connectionId: connection.id, kind: "organization", createdByUserId: users.creator, isDefault: true },
...[users.creator, users.manager, users.member].map((subjectUserId, index) => ({ companyId: f.companyId, connectionId: connection.id, kind: "user" as const,
subjectUserId, createdByUserId: subjectUserId, updatedAt: new Date(`2026-10-2${index + 1}T12:00:00.000Z`) })),
]).returning();
await server.db.insert(connectionGrantMembers).values({ companyId: f.companyId, grantId: grants[0].id, subjectType: "user", subjectId: users.member });
await server.db.insert(toolConnectionInstalls).values({ companyId: f.companyId, connectionId: connection.id, targetType: "agent", targetId: candidateIds[0] });
const cookies = Object.fromEntries(await Promise.all(Object.entries(users).map(async ([name, userId]) => [name, await server.humanSession(userId)])));
const request = async (name: keyof typeof users, method = "GET", body?: unknown) => {
const response = await fetch(`${server.apiUrl}/api/tool-connections/${connection.id}/${method === "GET" ? "grants" : "installs"}`, {
method, headers: { cookie: cookies[name], origin: server.apiUrl, "Content-Type": "application/json" },
...(body === undefined ? {} : { body: JSON.stringify(body) }),
});
return { status: response.status, body: await response.json() };
};
const original = connectionManagementModule.connectionManagementService;
const legacy = vi.spyOn(connectionManagementModule, "connectionManagementService").mockImplementation(db => {
const service = original(db);
return { ...service, describeAgentInstallCapabilities: async (actor, base, candidates, options) => {
const result = [];
for (const targetAgentId of candidates) result.push(await service.capability(actor, "connections:profiles", { ...base, targetAgentId, readOnly: false }, options));
return result;
} };
});
const baseline = new Map<string, Awaited<ReturnType<typeof request>>>();
try {
for (const name of Object.keys(users) as Array<keyof typeof users>) baseline.set(name, await request(name));
} finally { legacy.mockRestore(); }
const actual = new Map<string, Awaited<ReturnType<typeof request>>>();
for (const name of Object.keys(users) as Array<keyof typeof users>) {
actual.set(name, await request(name));
expect(actual.get(name), name).toEqual(baseline.get(name));
}
expect(actual.get("manager")).toMatchObject({ status: 200, body: { capabilities: { canConfigure: true, canViewOtherPersonalIdentities: true, editableAgentIds: expect.arrayContaining(candidateIds) } } });
expect(actual.get("manager")!.body.grants).toHaveLength(4);
expect(actual.get("creator")!.body.capabilities).toMatchObject({ canConfigure: true, canViewOtherPersonalIdentities: false });
expect(actual.get("creator")!.body.members.length).toBeGreaterThan(0);
expect(actual.get("member")!.body.capabilities).toMatchObject({ canConfigure: false, canViewOtherPersonalIdentities: false, editableAgentIds: expect.arrayContaining(candidateIds.slice(0, 2)) });
expect(actual.get("member")!.body.capabilities.editableAgentIds).toHaveLength(2);
expect(actual.get("member")!.body.grants.map((grant: { id: string }) => grant.id)).toEqual([grants[0].id, grants[3].id]);
expect(actual.get("member")!.body.members).toEqual([]);
expect(actual.get("viewer")!.body.capabilities).toMatchObject({ canConfigure: false, canManageAgentInstalls: false, editableAgentIds: [] });
expect(actual.get("inactive")!.status).toBe(404);
const installs = [{ targetType: "agent", targetId: candidateIds[0] }, { targetType: "agent", targetId: candidateIds[1] }];
const beforeRevoke = await request("member", "PUT", { installs });
expect(beforeRevoke, JSON.stringify(beforeRevoke)).toMatchObject({ status: 200 });
await server.db.delete(principalPermissionGrants).where(and(eq(principalPermissionGrants.companyId, f.companyId), eq(principalPermissionGrants.principalId, users.member)));
expect(await request("member")).toMatchObject({ status: 200, body: { capabilities: { editableAgentIds: [], canManageAgentInstalls: false } } });
expect(await request("member", "PUT", { installs: [{ targetType: "agent", targetId: candidateIds[0] }] })).toMatchObject({ status: 403 });
const savedInstalls = await server.db.select().from(toolConnectionInstalls).where(eq(toolConnectionInstalls.connectionId, connection.id));
expect(savedInstalls.map(install => install.targetId)).toEqual(expect.arrayContaining(candidateIds.slice(0, 2)));
expect(savedInstalls).toHaveLength(2);
});
it("checks company-wide personal AI installs once per target and preserves pool member authorization", async () => {
const f = await server.fixture({ connectionScenario: "fresh" });
const candidateIds = [f.agentId, ...Array.from({ length: 19 }, () => randomUUID())];
await server.db.insert(agents).values(candidateIds.slice(1).map((id, index) => ({ id, companyId: f.companyId, name: `AI target ${index}`, adapterType: "process" })));
const [application] = await server.db.insert(toolApplications).values({ companyId: f.companyId, name: "AI capability checks", type: "mcp_http" }).returning();
const [connection] = await server.db.insert(toolConnections).values({ companyId: f.companyId, applicationId: application.id,
uid: randomUUID(), name: "Personal AI account", transport: "runtime_auth", connectionPurpose: "ai", authKind: "api_key",
credentialPolicy: "per_user", createdByUserId: f.responsibleUserId }).returning();
const [grant] = await server.db.insert(connectionGrants).values({ companyId: f.companyId, connectionId: connection.id,
kind: "user", subjectUserId: f.responsibleUserId, createdByUserId: f.responsibleUserId }).returning();
await server.db.insert(toolConnectionInstalls).values({ companyId: f.companyId, connectionId: connection.id, targetType: "company", targetId: f.companyId });
await server.db.insert(principalPermissionGrants).values({ companyId: f.companyId, principalType: "user", principalId: f.responsibleUserId!,
permissionKey: "tools:manage_profiles", scope: { targetAgentIds: candidateIds } });
let targetReads = 0;
const observedDb = new Proxy(server.db, { get(target, key, receiver) {
if (key !== "select") return Reflect.get(target, key, receiver);
return (...fields: Parameters<typeof target.select>) => {
const builder = target.select(...fields), 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>) => {
if (query.toSQL().sql.includes('"agents"."id" =')) targetReads++;
return execute(...executeArgs);
}) as typeof execute;
return query;
}) as typeof builder.from;
return builder;
};
} });
const service = connectionManagementService(observedDb);
const actor: AuthorizationActor = { type: "board", userId: f.responsibleUserId, source: "cloud_tenant" };
const resource = { type: "connection_management" as const, companyId: f.companyId, connectionId: connection.id, operation: "configure", readOnly: false };
const baseline = [];
for (const targetAgentId of candidateIds) baseline.push(await service.capability(actor, "connections:profiles", { ...resource, targetAgentId }));
expect(baseline.every(result => result.allowed)).toBe(true);
expect(targetReads).toBe(candidateIds.length ** 2);
targetReads = 0;
expect(await service.describeAgentInstallCapabilities(actor, resource, candidateIds)).toEqual(baseline);
expect(targetReads).toBe(candidateIds.length);
const [pool] = await server.db.insert(toolConnections).values({ companyId: f.companyId, applicationId: application.id,
uid: randomUUID(), name: "Pool with private member", transport: "runtime_auth", connectionPurpose: "ai", authKind: "none" }).returning();
await server.db.insert(aiConnectionPools).values({ id: pool.id, companyId: f.companyId, pluginKey: "fixture-pool", config: {
name: "Pool", enabled: true, mode: "round_robin", thresholdPercent: 90,
members: [{ id: randomUUID(), binding: { mode: "delegated", provider: "openai", method: "api_key", connectionId: connection.id, grantId: grant.id }, profile: { provider: "codex", model: "fixture-model" } }],
} });
await server.db.delete(principalPermissionGrants).where(and(eq(principalPermissionGrants.companyId, f.companyId), eq(principalPermissionGrants.principalId, f.responsibleUserId!)));
await server.db.insert(principalPermissionGrants).values({ companyId: f.companyId, principalType: "user", principalId: f.responsibleUserId!,
permissionKey: "tools:manage_profiles", scope: { connectionIds: [pool.id] } });
const poolResource = { ...resource, connectionId: pool.id };
expect((await service.describeAgentInstallCapabilities(actor, poolResource, candidateIds.slice(0, 2))).every(result => result.allowed)).toBe(true);
// The pool grant cannot replace the member account's own authority.
await server.db.update(connectionGrants).set({ status: "revoked" }).where(eq(connectionGrants.id, grant.id));
await server.db.update(toolConnections).set({ createdByUserId: "different-owner" }).where(eq(toolConnections.id, connection.id));
const ordinary = await service.capability(actor, "connections:profiles", { ...poolResource, targetAgentId: candidateIds[0] });
expect(ordinary.allowed).toBe(false);
expect(await service.describeAgentInstallCapabilities(actor, poolResource, candidateIds.slice(0, 2))).toEqual([ordinary, ordinary]);
});
it("creates and configures tool connections and profiles with fresh grants and actual key/JWT authentication", async () => {
const f = await fixture();
const grants = await server.db.select().from(principalPermissionGrants).where(eq(principalPermissionGrants.principalId, f.agentId));
expect(grants.some(g => g.permissionKey === "tools:admin")).toBe(false);
expect(await f.request("GET", `/companies/${f.companyId}/tools/stdio-templates`)).toMatchObject({ status: 403 });
const created = await f.request("POST", `/companies/${f.companyId}/tools/connections`, { name: "Fixture tool", transport: "mcp_remote", authKind: "none", transportConfig: { url: "https://1.1.1.1/mcp" } });
expect(created, JSON.stringify(created)).toMatchObject({ status: 201 });
expect(await f.request("PATCH", `/tool-connections/${created.body.id}`, { name: "Updated tool" }, f.jwt)).toMatchObject({ status: 200, body: { name: "Updated tool" } });
const [saved] = await server.db.select().from(toolConnections).where(eq(toolConnections.id, created.body.id));
expect(saved.createdByUserId).toBe(f.userId);
const audit = await server.db.select().from(activityLog).where(eq(activityLog.entityId, saved.id));
expect(audit).toEqual(expect.arrayContaining([expect.objectContaining({ actorType: "agent", actorId: f.agentId, responsibleUserId: f.userId,
details: expect.objectContaining({ permissionDecision: expect.objectContaining({ action: "connections:configure", reason: expect.stringMatching(/^allow_/) }) }),
})]));
const profile = await f.request("POST", `/companies/${f.companyId}/tools/profiles`, { profileKey: "fixture-profile", name: "Fixture profile", defaultAction: "deny", entries: [] });
expect(profile, JSON.stringify(profile)).toMatchObject({ status: 201 });
});
it("checks effective-profile connection access once for hundreds of tools on the same account", async () => {
const f = await fixture();
const connection = await f.request("POST", `/companies/${f.companyId}/tools/connections`, { name: "Large catalog", transport: "mcp_remote", authKind: "none", config: { url: "https://1.1.1.1/mcp" } });
expect(connection).toMatchObject({ status: 201 });
await server.db.insert(toolCatalogEntries).values(Array.from({ length: 200 }, (_, index) => ({ companyId: f.companyId, applicationId: connection.body.applicationId, connectionId: connection.body.id, name: `tool_${index}`, toolName: `tool_${index}`, versionHash: `fixture_${index}` })));
const profile = await f.request("POST", `/companies/${f.companyId}/tools/profiles`, { profileKey: "large-catalog", name: "Large catalog", entries: [{ selectorType: "connection", connectionId: connection.body.id }] });
expect(profile).toMatchObject({ status: 201 });
expect(await f.request("POST", `/companies/${f.companyId}/tools/profiles/${profile.body.id}/bind`, { targetType: "agent", targetId: f.agentId })).toMatchObject({ status: 201 });
expect(await f.request("PUT", `/tool-connections/${connection.body.id}/installs`, { installs: [{ targetType: "agent", targetId: f.agentId }] })).toMatchObject({ status: 200 });
const module = await import("../services/connection-management.js");
const original = connectionManagementService;
let connectionChecks = 0;
const factory = vi.spyOn(module, "connectionManagementService").mockImplementation((...args) => {
const service = original(...args);
return { ...service, capability: async (...input: Parameters<typeof service.capability>) => {
if (input[2].connectionId === connection.body.id) connectionChecks++;
return service.capability(...input);
} };
});
try {
const effective = await f.request("GET", `/companies/${f.companyId}/tools/profiles/effective/agents/${f.agentId}`);
expect(effective, JSON.stringify(effective)).toMatchObject({ status: 200 });
expect(effective.body.allowedTools).toHaveLength(200);
expect(effective.body.installedConnections).toHaveLength(1);
expect(connectionChecks).toBe(1);
} finally { factory.mockRestore(); }
});
it("checks every saved profile selector and effective projection against personal account visibility", async () => {
const f = await fixture();
const first = await f.request("POST", `/companies/${f.companyId}/tools/connections`, { name: "Owned profile account", transport: "mcp_remote", authKind: "none", config: { url: "https://1.1.1.1/mcp" } });
expect(first).toMatchObject({ status: 201 });
const second = await f.request("POST", `/companies/${f.companyId}/tools/connections`, { applicationId: first.body.applicationId, name: "Private profile account", transport: "mcp_remote", authKind: "none", config: { url: "https://1.1.1.1/mcp" } });
expect(second, JSON.stringify(second)).toMatchObject({ status: 201, body: { applicationId: first.body.applicationId } });
const [known] = await server.db.insert(toolCatalogEntries).values({ companyId: f.companyId, applicationId: first.body.applicationId, connectionId: first.body.id, name: "known", toolName: "known", versionHash: "known" }).returning();
const profileIds: string[] = [];
const selectors = [
{ selectorType: "application", applicationId: first.body.applicationId },
{ selectorType: "catalog_entry", catalogEntryId: known.id },
{ selectorType: "tool_name", toolName: "private_tool" },
{ selectorType: "risk_level", riskLevel: "read" },
];
for (const [index, entry] of selectors.entries()) {
const profile = await f.request("POST", `/companies/${f.companyId}/tools/profiles`, { profileKey: `privacy-${index}`, name: `Privacy ${index}`, entries: [entry] });
expect(profile, JSON.stringify(profile)).toMatchObject({ status: 201 });
profileIds.push(profile.body.id);
}
const all = await f.request("POST", `/companies/${f.companyId}/tools/profiles`, { profileKey: "privacy-all", name: "All tools", defaultAction: "allow", entries: [] });
expect(all).toMatchObject({ status: 201 });
profileIds.push(all.body.id);
expect(await f.request("POST", `/companies/${f.companyId}/tools/profiles/${profileIds[0]}/bind`, { targetType: "agent", targetId: f.agentId })).toMatchObject({ status: 201 });
await server.db.insert(toolCatalogEntries).values({ companyId: f.companyId, applicationId: first.body.applicationId, connectionId: second.body.id, name: "private_tool", toolName: "private_tool", description: "Private tool metadata", versionHash: "private", firstSeenAt: new Date(Date.now() + 60_000) });
expect(await f.request("GET", `/tool-profiles/${profileIds[1]}/new-tools`)).toMatchObject({ status: 200, body: { tools: [expect.objectContaining({ connectionName: "Private profile account", description: "Private tool metadata" })] } });
const effectivePath = `/companies/${f.companyId}/tools/profiles/effective/agents/${f.agentId}`;
expect((await f.request("GET", effectivePath)).body.allowedTools.some((tool: { toolName: string }) => tool.toolName === "private_tool")).toBe(true);
expect(await f.request("PUT", `/tool-connections/${second.body.id}/installs`, { installs: [{ targetType: "agent", targetId: f.agentId }] })).toMatchObject({ status: 200 });
await server.db.update(toolConnections).set({ credentialPolicy: "per_user", createdByUserId: randomUUID() }).where(eq(toolConnections.id, second.body.id));
for (const token of [f.key.token, f.jwt]) {
expect(await f.request("GET", `/tool-profiles/${profileIds[1]}/new-tools`, undefined, token)).toMatchObject({ status: 403 });
for (const id of profileIds) expect(await f.request("GET", `/tool-profiles/${id}/new-tools`, undefined, token)).toMatchObject({ status: 403 });
expect((await f.request("GET", `/companies/${f.companyId}/tools/profiles`, undefined, token)).body.profiles).toEqual([]);
expect(await f.request("GET", effectivePath, undefined, token)).toMatchObject({ status: 403 });
}
for (const [index, entry] of selectors.entries()) expect(await f.request("POST", `/companies/${f.companyId}/tools/profiles`, { profileKey: `private-attempt-${index}`, name: `Private attempt ${index}`, entries: [entry] })).toMatchObject({ status: 403 });
expect(await f.request("POST", `/companies/${f.companyId}/tools/profiles`, { profileKey: "private-attempt-all", name: "Private all", defaultAction: "allow", entries: [] })).toMatchObject({ status: 403 });
await server.db.delete(toolProfileBindings).where(and(eq(toolProfileBindings.companyId, f.companyId), eq(toolProfileBindings.targetType, "agent"), eq(toolProfileBindings.targetId, f.agentId)));
expect(await f.request("GET", effectivePath)).toMatchObject({ status: 403 });
await server.db.update(toolConnections).set({ createdByUserId: f.userId }).where(eq(toolConnections.id, second.body.id));
expect(await f.request("GET", effectivePath)).toMatchObject({ status: 200 });
await server.db.update(principalPermissionGrants).set({ scope: { connectionIds: [first.body.id] } }).where(and(eq(principalPermissionGrants.principalId, f.agentId), eq(principalPermissionGrants.permissionKey, "tools:manage_profiles")));
for (const id of profileIds) expect(await f.request("GET", `/tool-profiles/${id}/new-tools?connectionId=${first.body.id}`)).toMatchObject({ status: 403 });
await server.db.update(principalPermissionGrants).set({ scope: { connectionIds: [first.body.id, second.body.id] } }).where(and(eq(principalPermissionGrants.principalId, f.agentId), eq(principalPermissionGrants.permissionKey, "tools:manage_profiles")));
for (const id of profileIds) expect(await f.request("GET", `/tool-profiles/${id}/new-tools?connectionId=${first.body.id}`)).toMatchObject({ status: 200 });
});
it("keeps connection profile configuration scoped and rechecks each profile against current personal access", async () => {
const f = await fixture();
const create = async (name: string) => {
const response = await f.request("POST", `/companies/${f.companyId}/tools/connections`, {
name, transport: "mcp_remote", authKind: "none", config: { url: "https://1.1.1.1/mcp" },
});
expect(response).toMatchObject({ status: 201 });
return response.body;
};
const owned = await create("Configuration owned"), privateConnection = await create("Configuration private");
const [app, intent] = await server.db.insert(toolProfiles).values([
{ companyId: f.companyId, profileKey: `app:${owned.id}`, name: "App" },
{ companyId: f.companyId, profileKey: `connection-intent:${owned.id}:${f.agentId}`, name: "Intent",
metadata: { source: "connection_intent", connectionId: owned.id, agentId: f.agentId } },
]).returning();
await server.db.insert(toolProfileBindings).values({ companyId: f.companyId, profileId: intent.id, targetType: "agent", targetId: f.agentId });
await server.db.insert(toolProfileEntries).values([
{ companyId: f.companyId, profileId: app.id, selectorType: "connection", connectionId: privateConnection.id },
{ companyId: f.companyId, profileId: intent.id, selectorType: "connection", connectionId: owned.id },
]);
const path = `/tool-connections/${owned.id}/profile-configuration`;
const uidResult = await f.request("GET", `/tool-connections/${encodeURIComponent(owned.uid)}/profile-configuration`);
expect(uidResult).toMatchObject({ status: 200 });
expect(uidResult.body.profiles.map((p: { id: string }) => p.id).sort()).toEqual([app.id, intent.id].sort());
const foreign = await fixture();
expect(await foreign.request("GET", `/tool-connections/${owned.id}/profile-configuration`)).toMatchObject({ status: 404 });
for (const token of [f.key.token, f.jwt]) {
const result = await f.request("GET", path, undefined, token);
expect(result).toMatchObject({ status: 200 });
expect(result.body.profiles.map((p: { id: string }) => p.id).sort()).toEqual([app.id, intent.id].sort());
expect(result.body.profiles.every((p: object) => !("summary" in p) && !("newToolsPendingCount" in p))).toBe(true);
}
// Seeing the displayed connection must not expose a profile spanning a
// second account after that account becomes private to another person.
await server.db.update(toolConnections).set({ credentialPolicy: "per_user", createdByUserId: randomUUID() }).where(eq(toolConnections.id, privateConnection.id));
for (const token of [f.key.token, f.jwt]) {
expect(await f.request("GET", path, undefined, token)).toMatchObject({ status: 200, body: { profiles: [expect.objectContaining({ id: intent.id })] } });
expect(await f.request("GET", `/tool-connections/${privateConnection.id}/profile-configuration`, undefined, token)).toMatchObject({ status: 403 });
expect(await f.request("GET", `/tool-connections/${randomUUID()}/profile-configuration`, undefined, token)).toMatchObject({ status: 404 });
}
await server.db.update(toolConnections).set({ createdByUserId: f.userId }).where(eq(toolConnections.id, privateConnection.id));
expect((await f.request("GET", path)).body.profiles).toHaveLength(2);
await server.db.delete(principalPermissionGrants).where(and(eq(principalPermissionGrants.principalId, f.agentId), eq(principalPermissionGrants.permissionKey, "tools:manage_profiles")));
expect(await f.request("GET", path)).toMatchObject({ status: 403 });
expect(await f.request("GET", path, undefined, f.jwt)).toMatchObject({ status: 403 });
});
it("rechecks authority after remote endpoint validation before committing a connection update", async () => {
const f = await fixture();
const created = await f.request("POST", `/companies/${f.companyId}/tools/connections`, { name: "Before validation", transport: "mcp_remote", authKind: "none", transportConfig: { url: "https://1.1.1.1/mcp" } });
expect(created).toMatchObject({ status: 201 });
const original = remoteEndpointGuard.assertPublicRemoteHttpEndpoint;
const validation = vi.spyOn(remoteEndpointGuard, "assertPublicRemoteHttpEndpoint").mockImplementation(async (...args) => {
const result = await original(...args);
await server.db.delete(principalPermissionGrants).where(and(eq(principalPermissionGrants.principalId, f.agentId), eq(principalPermissionGrants.permissionKey, "tools:manage_connections")));
return result;
});
try {
expect(await f.request("PATCH", `/tool-connections/${created.body.id}`, { name: "After validation" })).toMatchObject({ status: 403 });
expect(validation).toHaveBeenCalled();
expect((await server.db.select().from(toolConnections).where(eq(toolConnections.id, created.body.id)))[0].name).toBe("Before validation");
} finally { validation.mockRestore(); }
});
it("reaches sandbox device-login management as the responsible user and protects another owner's saved session", async () => {
const f = await fixture();
registerServerAdapter({ ...getServerAdapter("paperclip_runner"), type: "codex_local", loginCapability: {
panelMode: "displayed_code", timeoutPolicy: "caller_bounded", getCommand: () => "codex login --device-auth", parsePrompt: () => null,
} });
try {
const [environment] = await server.db.insert(environments).values({ name: `Login fixture ${f.agentId}`, driver: "sandbox" }).returning();
const [session] = await server.db.insert(adapterAuthSessions).values({ companyId: f.companyId, environmentId: environment.id, adapterType: "codex_local", startedByUserId: f.userId,
publicSessionId: randomUUID(), status: "waiting_for_user", expiresAt: new Date(Date.now() + 60_000), aiConnection: { provider: "openai", method: "subscription", ownership: "personal", name: "Saved login", allAgents: false, agentIds: [] } }).returning();
const path = `/companies/${f.companyId}/adapters/codex_local/login-sessions`;
expect(await f.request("GET", `${path}/active`)).toMatchObject({ status: 200, body: { sessionId: session.publicSessionId } });
expect(await f.request("GET", `${path}/${session.publicSessionId}`, undefined, f.jwt)).toMatchObject({ status: 200, body: { sessionId: session.publicSessionId } });
expect(await f.request("POST", path, {})).toMatchObject({ status: 400 });
expect(await f.request("POST", `${path}/${session.publicSessionId}/cancel`, {})).toMatchObject({ status: 200 });
await server.db.update(adapterAuthSessions).set({ startedByUserId: randomUUID() }).where(eq(adapterAuthSessions.id, session.id));
expect(await f.request("GET", `${path}/${session.publicSessionId}`)).toMatchObject({ status: 404 });
expect(await f.request("POST", `${path}/${session.publicSessionId}/cancel`, {})).toMatchObject({ status: 404 });
} finally { unregisterServerAdapter("codex_local"); }
});
it.each(["add", "remove"] as const)("requires a connection creator to edit agent targets when finish will %s installations", async (change) => {
const f = await server.fixture({ connectionScenario: "fresh" });
const userId = f.responsibleUserId!;
const cookie = await server.humanSession(userId);
const [application] = await server.db.insert(toolApplications).values({ companyId: f.companyId, name: "Creator install authority", type: "mcp_http" }).returning();
const [connection] = await server.db.insert(toolConnections).values({ companyId: f.companyId, applicationId: application.id,
uid: randomUUID(), name: "Creator-owned connection", transport: "mcp_remote", authKind: "none", status: "active", createdByUserId: userId }).returning();
if (change === "remove") await server.db.insert(toolConnectionInstalls).values({ companyId: f.companyId, connectionId: connection.id, targetType: "agent", targetId: f.agentId });
const before = await server.db.select().from(toolConnectionInstalls).where(eq(toolConnectionInstalls.connectionId, connection.id));
const finish = async () => fetch(`${server.apiUrl}/api/companies/${f.companyId}/tools/apps/${connection.id}/finish`, {
method: "POST", headers: { cookie, origin: server.apiUrl, "Content-Type": "application/json" },
body: JSON.stringify({ enabledCatalogEntryIds: [], askFirstCatalogEntryIds: [], access: { agentIds: change === "add" ? [f.agentId] : [] } }),
});
const denied = await finish();
expect(await denied.json()).toMatchObject({ error: expect.any(String) });
expect(denied.status).toBe(403);
expect(await server.db.select().from(toolConnectionInstalls).where(eq(toolConnectionInstalls.connectionId, connection.id))).toEqual(before);
expect(await server.db.select().from(toolProfiles).where(eq(toolProfiles.profileKey, `app:${connection.id}`))).toEqual([]);
await server.db.insert(principalPermissionGrants).values({ companyId: f.companyId, principalType: "user", principalId: userId,
permissionKey: "agents:configure", scope: { targetAgentIds: [f.agentId] } });
const allowed = await finish();
expect(allowed.status, JSON.stringify(await allowed.json())).toBe(200);
expect(await server.db.select().from(toolConnectionInstalls).where(eq(toolConnectionInstalls.connectionId, connection.id))).toHaveLength(change === "add" ? 1 : 0);
const [peer] = await server.db.insert(agents).values({ companyId: f.companyId, name: "Outside edit scope", adapterType: "process" }).returning();
const outOfScope = await fetch(`${server.apiUrl}/api/companies/${f.companyId}/tools/apps/${connection.id}/finish`, {
method: "POST", headers: { cookie, origin: server.apiUrl, "Content-Type": "application/json" },
body: JSON.stringify({ enabledCatalogEntryIds: [], askFirstCatalogEntryIds: [], access: { agentIds: [peer.id] } }),
});
expect(outOfScope.status).toBe(403);
expect(await server.db.select().from(toolConnectionInstalls).where(eq(toolConnectionInstalls.connectionId, connection.id))).toHaveLength(change === "add" ? 1 : 0);
});
it("checks finish access, all-agent sharing, and retained targets against both management grants", async () => {
const f = await fixture();
const connection = await f.request("POST", `/companies/${f.companyId}/tools/connections`, { name: "Finish authority", transport: "mcp_remote", authKind: "none", transportConfig: { url: "https://1.1.1.1/mcp" } });
const path = `/companies/${f.companyId}/tools/apps/${connection.body.id}/finish`;
const body = { enabledCatalogEntryIds: [], askFirstCatalogEntryIds: [], access: { agentIds: [f.agentId] } };
expect(await f.request("POST", path, body)).toMatchObject({ status: 200 });
await server.db.delete(principalPermissionGrants).where(and(eq(principalPermissionGrants.principalId, f.agentId), eq(principalPermissionGrants.permissionKey, "tools:manage_profiles")));
for (const access of [body.access, { agentIds: [] }, "all_agents"]) expect(await f.request("POST", path, { ...body, access })).toMatchObject({ status: 403 });
await server.db.insert(principalPermissionGrants).values({ companyId: f.companyId, principalType: "agent", principalId: f.agentId, permissionKey: "tools:manage_profiles", scope: { targetAgentIds: [f.agentId] } });
expect(await f.request("POST", path, { ...body, access: "all_agents" })).toMatchObject({ status: 403 });
const [other] = await server.db.select({ id: agents.id }).from(agents).where(and(eq(agents.companyId, f.companyId), eq(agents.name, "Eval agent")));
const targets = await server.db.select({ id: agents.id }).from(agents).where(eq(agents.companyId, f.companyId));
const otherId = other?.id ?? targets.find(agent => agent.id !== f.agentId)!.id;
expect(await f.request("POST", path, { ...body, access: { agentIds: [otherId] } })).toMatchObject({ status: 403 });
await server.db.update(principalPermissionGrants).set({ scope: null }).where(and(eq(principalPermissionGrants.principalId, f.agentId), eq(principalPermissionGrants.permissionKey, "tools:manage_profiles")));
await server.db.update(principalPermissionGrants).set({ scope: { targetAgentIds: [f.agentId] } }).where(and(eq(principalPermissionGrants.principalId, f.agentId), eq(principalPermissionGrants.permissionKey, "agents:configure")));
expect(await f.request("POST", path, { ...body, access: { agentIds: [otherId] } })).toMatchObject({ status: 403 });
const bindings = await server.db.select().from(toolProfileBindings).where(eq(toolProfileBindings.companyId, f.companyId));
expect(bindings.map(binding => binding.targetId)).toEqual([f.agentId]);
});
it.each(["none", "other-agent", "other-agent-no-access", "other-agent-allow-policy", "other-agent-explicit-grant", "other-agent-caller-ask", "other-agent-new-rate", "other-agent-new-shared-rate", "other-agent-new-rate-target-block", "other-agent-caller-rate", "other-agent-shared-rate", "other-agent-credential", "other-agent-credential-none", "other-agent-caller-revoked", "private-status", "agent", "user", "cancelled", "project-block", "rate-limit", "agent-rate-approval", "authorized-policy-edit"] as const)("keeps delegated test policy and approval continuation bounded (%s)", async revocation => {
const f = await fixture();
let targetAgentId = f.agentId;
if (revocation.startsWith("other-agent")) {
const cookie = await server.humanSession(f.userId);
const created = await fetch(`${server.apiUrl}/api/companies/${f.companyId}/agents`, { method: "POST", headers: { cookie, origin: server.apiUrl, "Content-Type": "application/json" },
body: JSON.stringify({ name: "Test target", role: "engineer", adapterType: "paperclip_runner", adapterConfig: { provider: "codex" }, permissions: {}, runtimeConfig: { heartbeat: { enabled: false, wakeOnDemand: false } } }) });
const target = await created.json();
expect(created.status, JSON.stringify(target)).toBe(201);
targetAgentId = target.id;
await lifecycle.process(targetAgentId);
await vi.waitFor(async () => expect((await agentService(server.db).getById(targetAgentId))?.status).toBe("idle"));
}
const allowed = revocation === "none" || revocation === "other-agent" || revocation === "other-agent-caller-ask" || ["other-agent-new-rate", "other-agent-new-shared-rate"].includes(revocation);
let calls = 0;
const remote = createServer(async (req, res) => {
const chunks: Buffer[] = [];
for await (const chunk of req) chunks.push(Buffer.from(chunk));
const request = JSON.parse(Buffer.concat(chunks).toString() || "{}");
let result: unknown;
if (request.method === "initialize") result = { protocolVersion: "2025-03-26", capabilities: { tools: {} }, serverInfo: { name: "Fixture", version: "1" } };
else if (request.method === "tools/call") { calls++; result = { content: [{ type: "text", text: "Fixture result" }] }; }
else { res.writeHead(req.method === "DELETE" ? 204 : request.method === "notifications/initialized" ? 202 : 405); res.end(); return; }
res.writeHead(200, { "content-type": "application/json" });
res.end(JSON.stringify({ jsonrpc: "2.0", id: request.id, result }));
});
await new Promise<void>(resolve => remote.listen(0, "127.0.0.1", resolve));
const address = remote.address();
if (!address || typeof address === "string") throw new Error("Fixture did not listen");
const mcpUrl = `http://127.0.0.1:${address.port}/mcp`;
const freezeRateClock = revocation.startsWith("other-agent-new-") || ["other-agent-caller-rate", "other-agent-shared-rate", "rate-limit", "agent-rate-approval"].includes(revocation);
if (freezeRateClock) vi.useFakeTimers({ toFake: ["Date"] });
try {
const created = await f.request("POST", `/companies/${f.companyId}/tools/connections`, { name: "Governed test", transport: "mcp_remote", authKind: "none", transportConfig: { url: mcpUrl } });
await server.db.update(toolConnections).set({ healthStatus: "ok" }).where(eq(toolConnections.id, created.body.id));
const [entry] = await server.db.insert(toolCatalogEntries).values({ companyId: f.companyId, applicationId: created.body.applicationId, connectionId: created.body.id,
entryKind: "tool", name: "Fixture probe", toolName: "fixture_probe", inputSchema: { type: "object", properties: {} }, riskLevel: "read", isReadOnly: true, status: "active", versionHash: "fixture-v1" }).returning();
expect(await f.request("POST", `/companies/${f.companyId}/tools/apps/${created.body.id}/finish`, { enabledCatalogEntryIds: [entry.id], access: { agentIds: revocation.startsWith("other-agent") && !["other-agent-no-access", "other-agent-allow-policy", "other-agent-explicit-grant"].includes(revocation) ? [f.agentId, targetAgentId] : [targetAgentId] } })).toMatchObject({ status: 200 });
const path = `/tool-connections/${created.body.id}/test-calls`;
const body = { agentId: targetAgentId, runContext: { heartbeatRunId: randomUUID(), issueId: randomUUID(), projectId: randomUUID() }, toolName: "fixture_probe", parameters: {} };
if (revocation === "other-agent-no-access") {
await server.db.insert(toolPolicies).values({ companyId: f.companyId, name: "Caller cannot use this tool", policyType: "block", selectors: { connectionId: created.body.id, agentId: f.agentId }, priority: 0 });
for (const token of [f.key.token, f.jwt]) expect(await f.request("POST", path, body, token)).toMatchObject({ status: 200, body: { decision: "off", error: { reasonCode: "deny_policy_block" } } });
expect(calls).toBe(0);
return;
}
if (revocation === "other-agent-allow-policy" || revocation === "other-agent-explicit-grant") {
if (revocation === "other-agent-allow-policy") await server.db.insert(toolPolicies).values({ companyId: f.companyId, name: "Caller authorized by policy", policyType: "allow", selectors: { connectionId: created.body.id, agentId: f.agentId }, priority: 0 });
else await server.db.update(principalPermissionGrants).set({ scope: { connectionId: created.body.id } }).where(and(eq(principalPermissionGrants.principalId, f.agentId), eq(principalPermissionGrants.permissionKey, "tools:use")));
const result = await f.request("POST", path, body, f.jwt);
expect(result, JSON.stringify(result)).toMatchObject({ status: 200, body: { decision: "allowed", result: { content: "Fixture result" } } });
expect(calls).toBe(1);
return;
}
if (revocation === "other-agent-credential" || revocation === "other-agent-credential-none") {
await server.db.update(toolConnections).set({ authKind: revocation === "other-agent-credential-none" ? "none" : "api_key", credentialPolicy: "per_agent" }).where(eq(toolConnections.id, created.body.id));
await server.db.insert(connectionGrants).values([f.agentId, targetAgentId].map(subjectAgentId => ({ companyId: f.companyId, connectionId: created.body.id, kind: "agent" as const, subjectAgentId, status: "active" as const })));
expect(await f.request("POST", path, body, f.jwt)).toMatchObject({ status: 403, body: { reasonCode: "test_credential_identity_mismatch" } });
expect(calls).toBe(0);
return;
}
if (revocation === "other-agent-caller-rate" || revocation === "other-agent-shared-rate") {
await server.db.insert(toolPolicies).values({ companyId: f.companyId, name: "Caller rate", policyType: "rate_limit", selectors: { connectionId: created.body.id, ...(revocation === "other-agent-caller-rate" ? { agentId: f.agentId } : {}) }, config: { limit: 1, windowSeconds: 60, keyBy: revocation === "other-agent-caller-rate" ? ["agent", "tool"] : ["company", "tool"] } });
const admitted = await f.request("POST", path, body, f.jwt);
expect(admitted, JSON.stringify(admitted)).toMatchObject({ status: 200, body: { decision: "allowed", result: { content: "Fixture result" } } });
expect(calls).toBe(1);
expect(await f.request("POST", path, body, f.jwt)).toMatchObject({ status: 200, body: { decision: "off", error: { reasonCode: "rate_limited" } } });
expect(calls).toBe(1);
return;
}
for (const selectors of [{ connectionId: created.body.id, projectId: f.projectId }, { connectionId: created.body.id, issueId: f.issueId }]) {
const [block] = await server.db.insert(toolPolicies).values({ companyId: f.companyId, name: "Scoped test block", policyType: "block", selectors, priority: 1 }).returning();
expect(await f.request("POST", path, body, f.jwt)).toMatchObject({ status: 200, body: { decision: "off" } });
expect(calls).toBe(0);
await server.db.delete(toolPolicies).where(eq(toolPolicies.id, block.id));
}
if (revocation === "rate-limit") {
await server.db.insert(toolPolicies).values({ companyId: f.companyId, name: "One test per minute", policyType: "rate_limit", selectors: { connectionId: created.body.id }, config: { limit: 1, windowSeconds: 60, keyBy: ["agent", "tool"] } });
expect(await f.request("POST", path, body, f.jwt)).toMatchObject({ status: 200, body: { decision: "allowed", result: { content: "Fixture result" } } });
expect(calls).toBe(1);
expect(await f.request("POST", path, body, f.jwt)).toMatchObject({ status: 200, body: { decision: "off", error: { reasonCode: "rate_limited" } } });
expect(calls).toBe(1);
return;
}
if (revocation === "agent-rate-approval") {
await server.db.insert(toolPolicies).values([
{ companyId: f.companyId, name: "One admitted call", policyType: "rate_limit", selectors: { connectionId: created.body.id }, priority: 0, config: { limit: 1, windowSeconds: 60, keyBy: ["agent", "tool"] } },
{ companyId: f.companyId, name: "Approve admitted call", policyType: "require_approval", selectors: { connectionId: created.body.id }, priority: 1 },
]);
const session = await f.request("POST", "/tool-gateway/sessions", {}, f.jwt);
expect(session, JSON.stringify(session)).toMatchObject({ status: 201 });
const gatewayRequest = async (path: string, parameters?: unknown) => {
const response = await fetch(server.apiUrl + "/api" + path, { method: parameters === undefined ? "GET" : "POST", headers: { "x-paperclip-tool-gateway-token": session.body.token, "Content-Type": "application/json" }, ...(parameters === undefined ? {} : { body: JSON.stringify(parameters) }) });
return { status: response.status, body: await response.json() };
};
const listed = await gatewayRequest("/tool-gateway/tools");
expect(listed).toMatchObject({ status: 200 });
const tool = listed.body.find((item: { catalogEntryId: string }) => item.catalogEntryId === entry.id);
expect(tool, JSON.stringify(listed.body)).toBeTruthy();
const requested = await gatewayRequest("/tool-gateway/tools/call", { tool: tool.name, parameters: {}, idempotencyKey: "rate-approval" });
expect(requested, JSON.stringify(requested)).toMatchObject({ status: 409, body: { reasonCode: "approval_required" } });
expect(calls).toBe(0);
expect(await f.request("POST", `/tool-gateway/action-requests/${requested.body.actionRequestId}/approve`, { companyId: f.companyId }, f.jwt)).toMatchObject({ status: 403 });
const cookie = await server.humanSession(f.userId);
const response = await fetch(`${server.apiUrl}/api/tool-gateway/action-requests/${requested.body.actionRequestId}/approve`, { method: "POST", headers: { cookie, origin: server.apiUrl, "Content-Type": "application/json" }, body: JSON.stringify({ companyId: f.companyId }) });
expect(response.status, JSON.stringify(await response.json())).toBe(200);
expect((await server.db.select().from(toolInvocations).where(eq(toolInvocations.id, requested.body.invocationId)))[0].status).toBe("succeeded");
expect(calls).toBe(1);
expect(await gatewayRequest("/tool-gateway/tools/call", { tool: tool.name, parameters: { anotherCall: true }, idempotencyKey: "next-rate-approval" })).toMatchObject({ status: 429, body: { reasonCode: "rate_limited" } });
expect(calls).toBe(1);
return;
}
await server.db.insert(toolPolicies).values({ companyId: f.companyId, name: "Scoped test approval", policyType: "require_approval", selectors: { connectionId: created.body.id, issueId: f.issueId, ...(revocation === "other-agent-caller-ask" ? { agentId: f.agentId } : {}) }, priority: 1 });
const pending = await f.request("POST", path, body, f.jwt);
expect(pending, JSON.stringify(pending)).toMatchObject({ status: 200, body: { decision: "ask_first" } });
const [saved] = await server.db.select().from(toolInvocations).where(eq(toolInvocations.id, pending.body.invocationId));
expect(saved).toMatchObject({ runId: f.runId, issueId: f.issueId, actorType: "agent", actorId: f.agentId, agentId: targetAgentId });
expect(calls).toBe(0);
if (revocation === "private-status") {
const statusPath = `${path}/${pending.body.actionRequestId}`;
expect(await f.request("GET", statusPath)).toMatchObject({ status: 200 });
const [hidden] = await server.db.insert(issues).values({ companyId: f.companyId, title: "Private test task", visibility: "private", status: "in_progress", responsibleUserId: randomUUID() }).returning();
await server.db.update(toolInvocations).set({ issueId: hidden.id }).where(eq(toolInvocations.id, saved.id));
for (const token of [f.key.token, f.jwt]) expect(await f.request("GET", statusPath, undefined, token)).toMatchObject({ status: 403 });
expect(calls).toBe(0);
return;
}
const approvalPath = `/tool-gateway/action-requests/${pending.body.actionRequestId}/approve`;
expect(await f.request("POST", approvalPath, { companyId: f.companyId }, f.jwt)).toMatchObject({ status: 403 });
if (revocation === "authorized-policy-edit") {
const policiesPath = `/companies/${f.companyId}/tools/policies`;
const policy = await f.request("POST", policiesPath, { name: "Requested self-targeted access", policyType: "allow", priority: 0,
selectors: { agentId: f.agentId, connectionId: created.body.id } }, f.jwt);
expect(policy, JSON.stringify(policy)).toMatchObject({ status: 201 });
expect(await f.request("POST", path, body, f.jwt)).toMatchObject({ status: 200, body: { decision: "allowed" } });
expect(calls).toBe(1);
expect(await f.request("POST", approvalPath, { companyId: f.companyId }, f.jwt)).toMatchObject({ status: 403 });
await server.db.delete(principalPermissionGrants).where(and(eq(principalPermissionGrants.principalId, f.agentId), eq(principalPermissionGrants.permissionKey, "tools:manage_profiles")));
expect(await f.request("PATCH", `${policiesPath}/${policy.body.id}`, { enabled: false }, f.jwt)).toMatchObject({ status: 403 });
return;
}
await server.db.update(heartbeatRuns).set({ status: "succeeded" }).where(eq(heartbeatRuns.id, f.runId));
if (revocation === "agent") await server.db.delete(principalPermissionGrants).where(and(eq(principalPermissionGrants.principalId, f.agentId), eq(principalPermissionGrants.permissionKey, "tools:use")));
if (revocation === "user") for (const permission of ["tools:use", "tools:manage_connections"]) await server.db.delete(principalPermissionGrants).where(and(eq(principalPermissionGrants.companyId, f.companyId), eq(principalPermissionGrants.principalId, f.userId), eq(principalPermissionGrants.permissionKey, permission)));
if (revocation === "cancelled") await server.db.update(issues).set({ status: "cancelled" }).where(eq(issues.id, f.issueId));
if (revocation === "other-agent-caller-revoked") await server.db.insert(toolPolicies).values({ companyId: f.companyId, name: "Caller access revoked", policyType: "block", selectors: { connectionId: created.body.id, agentId: f.agentId }, priority: 0 });
if (revocation === "project-block") await server.db.insert(toolPolicies).values({ companyId: f.companyId, name: "New project block", policyType: "block", selectors: { connectionId: created.body.id, projectId: f.projectId }, priority: 0 });
if (revocation.startsWith("other-agent-new-")) {
await server.db.insert(toolPolicies).values({ companyId: f.companyId, name: "New delegated test admission", policyType: "rate_limit",
selectors: { connectionId: created.body.id, ...(revocation === "other-agent-new-shared-rate" ? {} : { agentId: f.agentId }) },
priority: -2, config: { limit: 1, windowSeconds: 60, keyBy: revocation === "other-agent-new-shared-rate" ? ["company", "tool"] : ["agent", "tool"] } });
if (revocation === "other-agent-new-rate") await server.db.insert(toolPolicies).values({ companyId: f.companyId, name: "New test target admission", policyType: "rate_limit",
selectors: { connectionId: created.body.id, agentId: targetAgentId }, priority: -2,
config: { limit: 1, windowSeconds: 60, keyBy: ["agent", "tool"] } });
if (revocation === "other-agent-new-rate-target-block") await server.db.insert(toolPolicies).values({ companyId: f.companyId, name: "New test target block", policyType: "block",
selectors: { connectionId: created.body.id, agentId: targetAgentId }, priority: -1 });
}
const cookie = await server.humanSession(f.userId);
const approve = () => fetch(server.apiUrl + "/api" + approvalPath, { method: "POST", headers: { cookie, origin: server.apiUrl, "Content-Type": "application/json" }, body: JSON.stringify({ companyId: f.companyId }) });
const response = await approve();
expect(response.status, JSON.stringify(await response.json())).toBe(revocation === "cancelled" ? 409 : 200);
const [settled] = await server.db.select().from(toolInvocations).where(eq(toolInvocations.id, saved.id));
if (revocation === "cancelled") expect(settled.status).not.toBe("succeeded");
else expect(settled, JSON.stringify(settled)).toMatchObject({ status: allowed ? "succeeded" : "failed" });
expect(calls).toBe(allowed ? 1 : 0);
if (allowed) { expect((await approve()).status).toBe(200); expect(calls).toBe(1); }
if (revocation.startsWith("other-agent-new-")) {
const counters = await server.db.select().from(toolRateLimitCounters).where(eq(toolRateLimitCounters.companyId, f.companyId));
expect(counters).toHaveLength(allowed ? revocation === "other-agent-new-shared-rate" ? 1 : 2 : 0);
expect(counters.every(counter => counter.remaining === 0)).toBe(true);
if (allowed) {
expect(settled.policyExplanation?.consumedRateAdmissions).toHaveLength(1);
expect(settled.policyExplanation?.delegatedCallerConsumedRateAdmissions).toHaveLength(1);
const [nextRun] = await server.db.insert(heartbeatRuns).values({ companyId: f.companyId, agentId: f.agentId,
status: "running", responsibleUserId: f.userId, contextSnapshot: { issueId: f.issueId } }).returning();
const nextJwt = createLocalAgentJwt(f.agentId, f.companyId, "paperclip_runner", nextRun.id, f.userId)!;
expect(await f.request("POST", path, body, nextJwt)).toMatchObject({ status: 200, body: { decision: "off", error: { reasonCode: "rate_limited" } } });
expect(calls).toBe(1);
}
}
} finally { if (freezeRateClock) vi.useRealTimers(); remote.closeAllConnections(); await new Promise<void>((resolve, reject) => remote.close(error => error ? reject(error) : resolve())); }
});
it("completes concurrent run-authenticated profile finishing and policy writes without lock-order failures", async () => {
const f = await fixture();
const connection = await f.request("POST", `/companies/${f.companyId}/tools/connections`, { name: "Concurrent management", transport: "mcp_remote", authKind: "none", transportConfig: { url: "https://1.1.1.1/mcp" } });
const policy = await f.request("POST", `/companies/${f.companyId}/tools/policies`, { name: "Concurrent policy", policyType: "allow", selectors: { agentId: f.agentId } });
expect(policy).toMatchObject({ status: 201 });
const finishPath = `/companies/${f.companyId}/tools/apps/${connection.body.id}/finish`;
const finish = { enabledCatalogEntryIds: [], access: { agentIds: [f.agentId] } };
expect(await f.request("POST", finishPath, finish, f.jwt)).toMatchObject({ status: 200 });
for (let round = 0; round < 3; round++) {
const rows = await server.db.select({ id: toolPolicies.id }).from(toolPolicies).where(eq(toolPolicies.companyId, f.companyId));
const results = await Promise.all([
f.request("POST", `/companies/${f.companyId}/tools/policies/reorder`, { policyIds: rows.map(row => row.id) }, f.jwt),
f.request("PATCH", `/companies/${f.companyId}/tools/policies/${policy.body.id}`, { description: `Round ${round}` }, f.jwt),
f.request("POST", finishPath, finish, f.jwt),
]);
expect([200, 422]).toContain(results[0].status);
expect(results.slice(1), JSON.stringify(results)).toEqual([expect.objectContaining({ status: 200 }), expect.objectContaining({ status: 200 })]);
}
expect((await server.db.select().from(toolPolicies).where(eq(toolPolicies.id, policy.body.id)))[0].description).toBe("Round 2");
});
it("checks issue, routine, and gateway profile targets including retained bindings", async () => {
const f = await fixture();
const other = (await server.db.select({ id: agents.id }).from(agents).where(eq(agents.companyId, f.companyId))).find(agent => agent.id !== f.agentId)!;
const profile = await f.request("POST", `/companies/${f.companyId}/tools/profiles`, { profileKey: "target-checks", name: "Target checks", entries: [] });
const gatewayProfile = await f.request("POST", `/companies/${f.companyId}/tools/profiles`, { profileKey: "gateway-target", name: "Gateway target", entries: [] });
const gateway = await f.request("POST", `/companies/${f.companyId}/tools/gateways`, { name: "Target gateway", slug: "binding-target", profileId: gatewayProfile.body.id, agentId: other.id });
expect(gateway, JSON.stringify(gateway)).toMatchObject({ status: 201 });
const [issue] = await server.db.insert(issues).values({ companyId: f.companyId, title: "Other agent task", assigneeAgentId: other.id, status: "in_progress", responsibleUserId: f.userId }).returning();
const [routine] = await server.db.insert(routines).values({ companyId: f.companyId, title: "Other agent routine", assigneeAgentId: other.id, responsibleUserId: f.userId }).returning();
const [configurationGrant] = await server.db.select().from(principalPermissionGrants).where(and(eq(principalPermissionGrants.principalId, f.agentId), eq(principalPermissionGrants.permissionKey, "agents:configure")));
await server.db.update(principalPermissionGrants).set({ scope: { ...configurationGrant.scope, targetAgentIds: [f.agentId] } }).where(and(eq(principalPermissionGrants.principalId, f.agentId), eq(principalPermissionGrants.permissionKey, "agents:configure")));
const path = `/companies/${f.companyId}/tools/profiles/${profile.body.id}`;
const ownBinding = await f.request("POST", `${path}/bind`, { targetType: "issue", targetId: f.issueId });
expect(ownBinding, JSON.stringify(ownBinding)).toMatchObject({ status: 201 });
expect(await f.request("POST", `${path}/unbind`, { targetType: "issue", targetId: f.issueId })).toMatchObject({ status: 200 });
for (const target of [{ targetType: "project" as const, targetId: f.projectId }, { targetType: "issue" as const, targetId: issue.id }, { targetType: "routine" as const, targetId: routine.id }, { targetType: "gateway" as const, targetId: gateway.body.id }]) {
expect(await f.request("POST", `${path}/bind`, target), JSON.stringify(target)).toMatchObject({ status: 403 });
const [saved] = await server.db.insert(toolProfileBindings).values({ ...target, companyId: f.companyId, profileId: profile.body.id }).returning();
expect(await f.request("PATCH", `/tool-profiles/${profile.body.id}`, { description: "Cannot edit retained target" })).toMatchObject({ status: 403 });
expect(await f.request("POST", `${path}/unbind`, target)).toMatchObject({ status: 403 });
await server.db.delete(toolProfileBindings).where(eq(toolProfileBindings.id, saved.id));
}
await server.db.update(toolMcpGateways).set({ profileId: profile.body.id }).where(eq(toolMcpGateways.id, gateway.body.id));
expect(await f.request("PATCH", `/tool-profiles/${profile.body.id}`, { description: "Cannot edit a gateway's retained profile" })).toMatchObject({ status: 403 });
});
it("filters GitHub review history and detail by the linked task's private visibility", async () => {
const f = await fixture();
const endpoint = await f.request("POST", `/companies/${f.companyId}/chat-endpoints`, { provider: "github", assignedAgentId: f.agentId });
const [privateIssue] = await server.db.insert(issues).values({ companyId: f.companyId, title: "Private review task", visibility: "private", status: "in_progress", responsibleUserId: randomUUID() }).returning();
const [visible, hidden] = await server.db.insert(chatGitHubReviews).values([f.issueId, privateIssue.id].map((issueId, index) => ({ companyId: f.companyId, endpointId: endpoint.body.id,
issueId, repositoryId: "42", repository: "fixture/repo", pullNumber: index + 1, headSha: "b".repeat(40), deliveryId: randomUUID(), configurationRevision: 1,
policySnapshot: defaultGitHubReviewPolicy(), event: { event: "opened" as const, deliveryId: randomUUID(), repositoryId: "42", repository: "fixture/repo", pullNumber: index + 1,
title: "Review", body: index ? "Private provider context" : "Public context", baseSha: "a".repeat(40), headSha: "b".repeat(40), baseBranch: "master", author: { id: "1", login: "fixture", isBot: false }, sender: { id: "1", login: "fixture" }, draft: false, labels: [] },
}))).returning();
const path = `/chat-endpoints/${endpoint.body.id}/github/reviews`;
expect((await f.request("GET", path)).body.map((review: { id: string }) => review.id)).toEqual([visible.id]);
expect(await f.request("GET", `${path}/${visible.id}`)).toMatchObject({ status: 200 });
expect(await f.request("GET", `${path}/${hidden.id}`)).toMatchObject({ status: 404 });
});
it("keeps narrow policy create, update, reorder, and delete within their actual audiences", async () => {
const f = await fixture();
const other = (await server.db.select({ id: agents.id }).from(agents).where(eq(agents.companyId, f.companyId))).find(agent => agent.id !== f.agentId)!;
await server.db.update(principalPermissionGrants).set({ scope: { targetAgentIds: [f.agentId] } }).where(and(eq(principalPermissionGrants.principalId, f.agentId), eq(principalPermissionGrants.permissionKey, "agents:configure")));
const path = `/companies/${f.companyId}/tools/policies`;
const created = await f.request("POST", path, { name: "Own target rule", policyType: "allow", selectors: { agentId: f.agentId } });
expect(created, JSON.stringify(created)).toMatchObject({ status: 201 });
expect(await f.request("PATCH", `${path}/${created.body.id}`, { description: "Retain the target" })).toMatchObject({ status: 200 });
expect(await f.request("POST", `${path}/reorder`, { policyIds: [created.body.id] })).toMatchObject({ status: 200 });
expect(await f.request("PATCH", `${path}/${created.body.id}`, { selectors: {} })).toMatchObject({ status: 403 });
expect(await f.request("PATCH", `${path}/${created.body.id}`, { selectors: { agentId: other.id } })).toMatchObject({ status: 403 });
expect(await f.request("POST", path, { name: "Company rule", policyType: "allow", selectors: {} })).toMatchObject({ status: 403 });
expect(await f.request("DELETE", `${path}/${created.body.id}`)).toMatchObject({ status: 200 });
});
it("bounds and deduplicates resource selectors before per-resource database work", async () => {
const f = await fixture();
const created = await f.request("POST", `/companies/${f.companyId}/tools/connections`, { name: "Bounded selector", transport: "mcp_remote", authKind: "none", transportConfig: { url: "https://1.1.1.1/mcp" } });
let resourceReads = 0;
const originalSelect = server.db.select.bind(server.db);
const select = vi.spyOn(server.db, "select").mockImplementation(((selection: never) => {
const builder = originalSelect(selection);
const originalFrom = builder.from.bind(builder);
builder.from = ((table: never) => {
if (table === toolConnections || table === toolPolicies || table === toolCatalogEntries) resourceReads++;
return originalFrom(table);
}) as typeof builder.from;
return builder;
}) as unknown as typeof server.db.select);
try {
const path = `/companies/${f.companyId}/tools/policies`;
const body = { name: "Bounded policy", policyType: "allow", selectors: { agentId: f.agentId, connectionIds: Array(5000).fill(created.body.id) } };
expect(await f.request("POST", path, body)).toMatchObject({ status: 400 });
expect(resourceReads).toBe(0);
expect(await f.request("POST", path, { ...body, selectors: { ...body.selectors, connectionIds: Array(100).fill(created.body.id) } })).toMatchObject({ status: 201 });
expect(resourceReads).toBeLessThan(20);
resourceReads = 0;
await accessService(server.db).ensureMembership(f.companyId, "user", f.userId, "operator", "suspended");
expect(await f.request("POST", path, { ...body, selectors: { ...body.selectors, connectionIds: Array(100).fill(created.body.id) } })).toMatchObject({ status: 403 });
expect(resourceReads).toBe(0);
} finally { select.mockRestore(); }
});
it("shares one run capture across a management read without caching authority across requests", async () => {
const f = await fixture();
for (let index = 0; index < 5; index++) expect(await f.request("POST", `/companies/${f.companyId}/tools/connections`, { name: `Read snapshot ${index}`, transport: "mcp_remote", authKind: "none", transportConfig: { url: "https://1.1.1.1/mcp" } })).toMatchObject({ status: 201 });
const capture = vi.spyOn(runIdentity, "captureRunIdentity");
try {
const listed = await f.request("GET", `/companies/${f.companyId}/tools/connections`, undefined, f.jwt);
expect(listed).toMatchObject({ status: 200 });
expect(listed.body.connections).toHaveLength(5);
expect(capture.mock.calls.length).toBeGreaterThan(0);
expect(capture.mock.calls.length).toBeLessThanOrEqual(2);
await server.db.delete(principalPermissionGrants).where(and(eq(principalPermissionGrants.principalId, f.agentId), eq(principalPermissionGrants.permissionKey, "tools:manage_connections")));
expect(await f.request("GET", `/companies/${f.companyId}/tools/connections`, undefined, f.jwt)).toMatchObject({ status: 403 });
} finally { capture.mockRestore(); }
});
it("returns the same missing-resource response for foreign and absent connection IDs", async () => {
const f = await fixture(), other = await fixture();
const foreign = await other.request("POST", `/companies/${other.companyId}/tools/connections`, { name: "Other company", transport: "mcp_remote", authKind: "none", transportConfig: { url: "https://1.1.1.1/mcp" } });
const hidden = await f.request("GET", `/tool-connections/${foreign.body.id}`);
const absent = await f.request("GET", `/tool-connections/${randomUUID()}`);
expect(hidden).toMatchObject({ status: 404 });
expect(absent).toEqual(hidden);
});
it("checks existing and proposed provider scope without treating generic metadata as provider authority", async () => {
const f = await fixture();
const payload = { name: "Scoped provider", transport: "mcp_remote", authKind: "none", config: { url: "https://1.1.1.1/mcp", provider: "fixture-one" } };
const created = await f.request("POST", `/companies/${f.companyId}/tools/connections`, payload);
expect(created.status).toBe(201);
const untagged = await f.request("POST", `/companies/${f.companyId}/tools/connections`, { ...payload, name: "Untagged", config: { url: "https://1.1.1.1/mcp" } });
const application = await f.request("POST", `/companies/${f.companyId}/tools/applications`, { name: "Provider app", type: "rest_api", metadata: { sourceTemplateKey: "fixture-one" } });
expect(application).toMatchObject({ status: 201 });
await server.db.update(principalPermissionGrants).set({ scope: { providers: ["fixture-one"] } }).where(and(eq(principalPermissionGrants.principalId, f.agentId), eq(principalPermissionGrants.permissionKey, "tools:manage_connections")));
expect(await f.request("PATCH", `/tool-connections/${untagged.body.id}`, { name: "Spoofed label", provider: "fixture-one" })).toMatchObject({ status: 403 });
expect(await f.request("POST", `/companies/${f.companyId}/tools/applications`, { name: "Spoofed application", type: "rest_api", provider: "fixture-one", metadata: { sourceTemplateKey: "fixture-two" } })).toMatchObject({ status: 403 });
expect(await f.request("PATCH", `/tool-applications/${application.body.id}`, { name: "Same app provider" })).toMatchObject({ status: 200 });
for (const metadata of [{ sourceTemplateKey: "fixture-two" }, {}]) expect(await f.request("PATCH", `/tool-applications/${application.body.id}`, { provider: "fixture-one", metadata })).toMatchObject({ status: 403 });
expect(await f.request("PATCH", `/tool-connections/${created.body.id}`, { name: "Still the same provider" })).toMatchObject({ status: 200 });
for (const config of [{ url: "https://1.1.1.1/mcp", provider: "fixture-two" }, { url: "https://1.1.1.1/mcp" }]) expect(await f.request("PATCH", `/tool-connections/${created.body.id}`, { config })).toMatchObject({ status: 403 });
expect(await f.request("POST", `/companies/${f.companyId}/tools/connections`, { ...payload, name: "Caller-labelled provider" })).toMatchObject({ status: 403 });
await server.db.update(principalPermissionGrants).set({ scope: { purposes: ["tool"] } }).where(and(eq(principalPermissionGrants.principalId, f.agentId), eq(principalPermissionGrants.permissionKey, "tools:manage_connections")));
expect(await f.request("POST", `/companies/${f.companyId}/ai-connections`, { provider: "openai", method: "api_key", name: "Wrong purpose", apiKey: "fixture", ownership: "personal", agentIds: [] })).toMatchObject({ status: 403 });
});
it("hands generic app authentication to the saved setup without creating agent-bound OAuth state", async () => {
const f = await fixture();
const created = await f.request("POST", `/companies/${f.companyId}/tools/connections`, {
name: "OAuth handoff fixture", transport: "mcp_remote", authKind: "oauth", credentialPolicy: "per_user", status: "draft",
transportConfig: { url: "https://1.1.1.1/mcp" },
});
expect(created, JSON.stringify(created)).toMatchObject({ status: 201 });
const path = `/tools/oauth/${created.body.id}/start`;
const started = await f.request("POST", path, { asCurrentUser: true });
expect(started, JSON.stringify(started)).toMatchObject({ status: 200, body: {
expiresAt: null, requiredUserAction: { userId: f.userId, resource: { id: created.body.id } },
} });
expect(new URL(started.body.authorizationUrl).searchParams.get("resume")).toBe(created.body.id);
expect(await f.request("POST", path, { asCurrentUser: true })).toMatchObject({ status: 200, body: { authorizationUrl: started.body.authorizationUrl } });
expect(await server.db.select().from(toolOauthStates).where(eq(toolOauthStates.connectionId, created.body.id))).toHaveLength(0);
});
it("delegates and revokes an owned credential with actual agent attribution", async () => {
const f = await fixture();
const created = await f.request("POST", `/companies/${f.companyId}/tools/connections`, {
name: "Personal delegation fixture", transport: "mcp_remote", authKind: "none", credentialPolicy: "per_user",
transportConfig: { url: "https://1.1.1.1/mcp" },
});
expect(created.status).toBe(201);
// Provider fixture: a completed personal credential, not principal permissions.
const [grant] = await server.db.insert(connectionGrants).values({ companyId: f.companyId,
connectionId: created.body.id, kind: "user", subjectUserId: f.userId, status: "active",
credentialSecretRefs: [], createdByUserId: f.userId,
}).returning();
const path = `/tool-connections/${created.body.id}/grants/${grant.id}/delegations`;
const delegated = await f.request("POST", path, { agentId: f.agentId });
expect(delegated, JSON.stringify(delegated)).toMatchObject({ status: 201 });
expect(await f.request("POST", path, { agentId: f.agentId })).toMatchObject({ status: 201, body: { id: delegated.body.id } });
expect(await f.request("DELETE", `${path}/${delegated.body.id}`)).toMatchObject({ status: 200 });
expect(await server.db.select().from(connectionGrantDelegations).where(eq(connectionGrantDelegations.grantId, grant.id))).toHaveLength(0);
const events = await server.db.select().from(toolAccessAuditEvents).where(eq(toolAccessAuditEvents.connectionId, created.body.id));
expect(events).toEqual(expect.arrayContaining([
expect.objectContaining({ action: "connection_grant.delegated", actorType: "agent", actorId: f.agentId }),
expect.objectContaining({ action: "connection_grant.delegation_revoked", actorType: "agent", actorId: f.agentId }),
]));
const activity = await server.db.select().from(activityLog).where(eq(activityLog.entityId, grant.id));
expect(activity).toEqual(expect.arrayContaining([expect.objectContaining({ actorType: "agent", actorId: f.agentId, responsibleUserId: f.userId })]));
});
it("saves the addressed user's Google service choices before handing off browser consent", async () => {
const f = await fixture();
const created = await f.request("POST", `/companies/${f.companyId}/tools/connections`, { name: "Selected Google services", transport: "mcp_remote", authKind: "oauth", credentialPolicy: "per_user", status: "draft",
config: { url: "https://1.1.1.1/mcp", sourceTemplateKey: "google-workspace", connectionMethodKey: "paperclip-workspace" } });
expect(created, JSON.stringify(created)).toMatchObject({ status: 201 });
const handoff = await f.request("POST", `/tools/oauth/${created.body.id}/start`, { asCurrentUser: true });
expect(handoff, JSON.stringify(handoff)).toMatchObject({ status: 200, body: { expiresAt: null, requiredUserAction: { userId: f.userId } } });
const [unselected] = await server.db.select().from(toolConnections).where(eq(toolConnections.id, created.body.id));
expect(unselected.config.workspaceSetup).toBeUndefined();
const services = { gmail: "read" as const };
const started = await f.request("POST", `/tools/oauth/${created.body.id}/start`, { asCurrentUser: true, workspaceServices: services });
expect(started, JSON.stringify(started)).toMatchObject({ status: 200, body: { expiresAt: null, requiredUserAction: { userId: f.userId } } });
const [saved] = await server.db.select().from(toolConnections).where(eq(toolConnections.id, created.body.id));
expect((saved.config.oauth as { pendingWorkspaceServiceRequests: Record<string, unknown> }).pendingWorkspaceServiceRequests).toEqual({
[f.userId]: { requestedScopes: googleWorkspaceSelectedScopes(services), requestedAt: expect.any(String) },
});
const restored = await f.request("GET", `/tool-connections/${saved.id}`);
expect(restored).toMatchObject({ status: 200, body: { config: { oauth: { pendingWorkspaceServiceRequests: {
[f.userId]: { requestedScopes: googleWorkspaceSelectedScopes(services) },
} } } } });
expect(await server.db.select().from(toolOauthStates).where(eq(toolOauthStates.connectionId, saved.id))).toHaveLength(0);
expect(await f.request("POST", `/tools/oauth/${saved.id}/start`, { asCurrentUser: true })).toMatchObject({ status: 200, body: { authorizationUrl: started.body.authorizationUrl } });
const [handedOff] = await server.db.select().from(toolConnections).where(eq(toolConnections.id, saved.id));
expect(handedOff.config.workspaceSetup).toBeUndefined();
expect(handedOff.config.oauth).toEqual(saved.config.oauth);
});
it("discovers and invokes connection APIs through default production native bindings", async () => {
const f = await fixture();
const definitions = await f.authority.definitions();
const names = definitions.map(d => d.name);
const context = await f.authority.execute({ tool: "get_task_context", callId: "configuration-guidance", arguments: {} });
expect(JSON.stringify(context)).toContain("Standard agents can manage connections under their responsible user's authority");
expect(JSON.stringify(definitions.find(d => d.name === "connections_search"))).toContain("search_api/call_api");
expect(names).toEqual(expect.arrayContaining(["search_api", "call_api"]));
const search = await f.authority.execute({ tool: "search_api", callId: "find-profile", arguments: { query: "create tools profiles", limit: 10 } });
expect(JSON.stringify(search)).toContain("POST /api/companies/{companyId}/tools/profiles");
const result = await f.authority.execute({ tool: "call_api", callId: "create-profile", arguments: { operationId: "POST /api/companies/{companyId}/tools/profiles", body: { profileKey: "native-profile", name: "Native profile", entries: [] } } });
expect(result, JSON.stringify(result)).toMatchObject({ status: 201, ok: true });
});
it("persists meaningful Slack and GitHub endpoint configuration for the responsible user", async () => {
const f = await fixture();
for (const provider of ["slack", "github"]) {
const result = await f.request("POST", `/companies/${f.companyId}/chat-endpoints`, { provider, assignedAgentId: f.agentId, name: `${provider} fixture`, ...(provider === "slack" ? { slackSetupMethod: "manual" } : {}) });
expect(result, JSON.stringify(result)).toMatchObject({ status: 201 });
if (provider === "slack") {
const patch = { requireAtMention: true, allowDirectMessages: false };
const updated = await f.request("PATCH", `/chat-endpoints/${result.body.id}`, patch);
expect(updated, JSON.stringify(updated)).toMatchObject({ status: 200, body: patch });
} else {
const current = await f.request("GET", `/chat-endpoints/${result.body.id}/github/configuration`);
expect(current, JSON.stringify(current)).toMatchObject({ status: 200 });
const body = { expectedRevision: current.body.revision, configuration: { ...current.body.configuration, memberAccess: "selected" } };
const updated = await f.request("PUT", `/chat-endpoints/${result.body.id}/github/configuration`, body);
expect(updated, JSON.stringify(updated)).toMatchObject({ status: 200, body: { revision: 1, configuration: { memberAccess: "selected" } } });
expect(await f.request("PUT", `/chat-endpoints/${result.body.id}/github/configuration`, body)).toMatchObject({ status: 409 });
const handoff = await f.request("POST", `/chat-endpoints/${result.body.id}/github/identity/start`, {});
expect(handoff).toMatchObject({ status: 200, body: { requiredUserAction: { userId: f.userId, resource: { id: result.body.id } } } });
}
expect((await f.request("GET", `/chat-endpoints/${result.body.id}`)).body.sponsorUserId).toBe(f.userId);
}
});
it("configures Slack against provider fixtures and subjects test messages to the normal action policy", async () => {
const f = await fixture();
const botId = `U${f.agentId.replaceAll("-", "").toUpperCase()}`;
const scopes = [...SLACK_CHAT_BOT_SCOPES, ...SLACK_BOT_TOOL_SCOPES];
let sent = 0;
let revokeDuringAuth = false;
provider = async (url, init) => {
if (url.endsWith("auth.test")) {
if (revokeDuringAuth) await accessService(server.db).ensureMembership(f.companyId, "user", f.userId, "operator", "suspended");
return Response.json({ ok: true, team_id: "TFIXTURE", team: "Fixture workspace", user_id: botId, user: "fixture-bot" }, { headers: { "x-oauth-scopes": scopes.join(",") } });
}
if (url.includes("conversations.list")) return Response.json({ ok: true, channels: [], response_metadata: { next_cursor: "" } });
if (url.endsWith("users.info")) return Response.json({ ok: true, user: { id: new URLSearchParams(String(init?.body)).get("user") ?? "UHUMAN", team_id: "TFIXTURE", name: "Fixture user", is_bot: false, deleted: false } });
if (url.endsWith("chat.postMessage")) { sent++; return Response.json({ ok: true, channel: "DTEST", ts: "1.000001" }); }
if (url.endsWith("agents.sessions.setStatus")) return Response.json({ ok: true });
throw new Error(`Unexpected Slack fixture operation: ${new URL(url).pathname}`);
};
try {
const created = await f.request("POST", `/companies/${f.companyId}/chat-endpoints`, { provider: "slack", assignedAgentId: f.agentId, slackSetupMethod: "manual" });
const setupPath = `/chat-endpoints/${created.body.id}/setup`;
const cookie = await server.humanSession(f.userId);
const configuredResponse = await fetch(server.apiUrl + "/api" + setupPath, { method: "POST", headers: { cookie, origin: server.apiUrl, "Content-Type": "application/json" },
body: JSON.stringify({ action: "configure", credentials: { botToken: "xoxb-fixture-token", signingSecret: "fixture-signing-secret" } }) });
const configured = { status: configuredResponse.status, body: await configuredResponse.json() };
expect(configured, JSON.stringify(configured)).toMatchObject({ status: 200 });
expect(JSON.stringify(configured.body)).not.toContain("xoxb-fixture-token");
const [configuredEndpoint] = await server.db.select().from(chatEndpoints).where(eq(chatEndpoints.id, created.body.id));
await server.db.update(chatEndpoints).set({ status: "active", setup: { ...configuredEndpoint.setup, step: "complete", webhookVerifiedAt: new Date().toISOString() } }).where(eq(chatEndpoints.id, created.body.id));
const [before] = await server.db.select().from(toolConnections).where(eq(toolConnections.id, created.body.connectionId));
const botTokenRef = before.credentialSecretRefs.find(ref => ref.configPath === "credentials.botToken")!;
const rebound = before.credentialSecretRefs.map(ref => ref.configPath === "credentials.signingSecret" ? { ...ref, secretId: botTokenRef.secretId } : ref);
expect(await f.request("PATCH", `/tool-connections/${created.body.connectionId}`, { credentialSecretRefs: rebound }, f.jwt))
.toMatchObject({ status: 403, body: { code: "chat_endpoint_credentials_require_setup" } });
let credentialIntentId: string | undefined;
for (const action of ["reconnect", "configure"] as const) for (const token of [f.key.token, f.jwt]) {
const chosen = "agent-chosen-webhook-key";
const handoff = await f.request("POST", setupPath, { action, credentials: { signingSecret: chosen } }, token);
expect(handoff, JSON.stringify(handoff)).toMatchObject({ status: 200, body: { status: "needs_user_action", endpointId: created.body.id,
requiredUserAction: { kind: "enter_credentials", userId: f.userId, resource: { id: created.body.id } } } });
expect(JSON.stringify(handoff.body)).not.toContain(chosen);
expect(handoff.body.requiredUserAction.setupPath).toContain("stage=credentials&reconnect=1");
const entryLink = new URL(handoff.body.requiredUserAction.setupPath, server.apiUrl);
expect.soft(entryLink.searchParams.get("credentialFields")).toBe("signingSecret");
expect.soft(entryLink.searchParams.get("credentialMode")).toBe("all");
if (token === f.jwt) {
if (credentialIntentId) expect(handoff.body.interactionId).toBe(credentialIntentId);
credentialIntentId = handoff.body.interactionId;
}
const [after] = await server.db.select().from(toolConnections).where(eq(toolConnections.id, created.body.connectionId));
expect(after.credentialSecretRefs).toEqual(before.credentialSecretRefs);
const signing = after.credentialSecretRefs.find(ref => ref.configPath === "credentials.signingSecret")!;
expect(await secretService(server.db).resolveSecretValue(f.companyId, signing.secretId, "latest", {
accessContext: { consumerType: "system", consumerId: "integration-fixture", actorType: "system" },
})).toBe("fixture-signing-secret");
}
const [credentialIntent] = await server.db.select().from(issueThreadInteractions).where(eq(issueThreadInteractions.id, credentialIntentId!));
const [task] = await server.db.select().from(issues).where(eq(issues.id, f.issueId));
const entryProof = (credentialIntent.payload as { savedSetup: { credentialEntry: { previousGenerations: Record<string, string>; requireAll: boolean } } }).savedSetup.credentialEntry;
expect(entryProof).toEqual({ previousGenerations: { signingSecret: expect.any(String) }, requireAll: true });
expect(await findSatisfiedToolConnection(server.db, task, credentialIntent)).toBeNull();
await satisfiedConnectionIntentService(server.db).sweepPending();
expect((await server.db.select().from(issueThreadInteractions).where(eq(issueThreadInteractions.id, credentialIntentId!)))[0].status).toBe("pending");
// Ordinary write-only bot-token repair remains delegated under current authority.
expect(await f.request("POST", setupPath, { action: "reconnect", credentials: { botToken: "xoxb-fixture-token" } }, f.jwt)).toMatchObject({ status: 200, body: { id: created.body.id } });
const healthy = async () => {
await server.db.update(chatEndpoints).set({ status: "active" }).where(eq(chatEndpoints.id, created.body.id));
await server.db.update(toolConnections).set({ status: "active", enabled: true, healthStatus: "healthy" }).where(eq(toolConnections.id, created.body.connectionId));
};
await healthy();
expect(await findSatisfiedToolConnection(server.db, task, credentialIntent)).toBeNull();
const humanReconnect = async (signingSecret: string) => {
const response = await fetch(server.apiUrl + "/api" + setupPath, { method: "POST", headers: { cookie, origin: server.apiUrl, "Content-Type": "application/json" },
body: JSON.stringify({ action: "reconnect", credentials: { signingSecret, botToken: "xoxb-fixture-token" } }) });
expect({ status: response.status, body: await response.json() }).toMatchObject({ status: 200 });
};
await humanReconnect("fixture-signing-secret");
await healthy();
expect(await findSatisfiedToolConnection(server.db, task, credentialIntent)).toBeNull();
await humanReconnect("human-replacement-signing-secret");
expect(await findSatisfiedToolConnection(server.db, task, credentialIntent)).toBeNull();
const resumedRequest = await f.request("POST", setupPath, { action: "reconnect", credentials: { signingSecret: "agent-chosen-webhook-key" } }, f.jwt);
expect(resumedRequest.body.interactionId).toBe(credentialIntentId);
expect((await server.db.select().from(issueThreadInteractions).where(eq(issueThreadInteractions.id, credentialIntentId!)))[0].payload)
.toMatchObject({ savedSetup: { credentialEntry: entryProof } });
await healthy();
expect(await findSatisfiedToolConnection(server.db, task, credentialIntent)).toBeNull();
const [repaired] = await server.db.select().from(chatEndpoints).where(eq(chatEndpoints.id, created.body.id));
await server.db.update(chatEndpoints).set({ setup: { ...repaired.setup, webhookVerifiedAt: new Date().toISOString() } }).where(eq(chatEndpoints.id, created.body.id));
expect((await findSatisfiedToolConnection(server.db, task, credentialIntent))?.id).toBe(created.body.connectionId);
expect(await findSatisfiedToolConnection(server.db, { ...task, status: "cancelled" }, credentialIntent)).toBeNull();
await accessService(server.db).ensureMembership(f.companyId, "user", f.userId, "operator", "suspended");
expect(await findSatisfiedToolConnection(server.db, task, credentialIntent)).toBeNull();
await accessService(server.db).ensureMembership(f.companyId, "user", f.userId, "operator", "active");
await satisfiedConnectionIntentService(server.db).sweepPending();
expect((await server.db.select().from(issueThreadInteractions).where(eq(issueThreadInteractions.id, credentialIntentId!)))[0].status).toBe("expired");
// Replacing complete credentials repairs unavailable old vault material.
for (const state of ["disabled", "deleted", "unreadable"] as const) {
const [connection] = await server.db.select().from(toolConnections).where(eq(toolConnections.id, created.body.connectionId));
const signing = connection.credentialSecretRefs.find(ref => ref.configPath === "credentials.signingSecret")!;
if (state === "unreadable") {
await server.db.update(companySecrets).set({ status: "active" }).where(eq(companySecrets.id, signing.secretId));
await server.db.update(companySecretVersions).set({ material: {} }).where(eq(companySecretVersions.secretId, signing.secretId));
} else {
await server.db.update(companySecrets).set({ status: state }).where(eq(companySecrets.id, signing.secretId));
}
const response = await fetch(server.apiUrl + "/api" + setupPath, { method: "POST", headers: { cookie, origin: server.apiUrl, "Content-Type": "application/json" },
body: JSON.stringify({ action: "reconnect", credentials: { signingSecret: `human-repair-${state}`, botToken: "xoxb-fixture-token" } }) });
expect.soft({ status: response.status, body: await response.json() }, `repair ${state} signing secret`).toMatchObject({ status: 200 });
if (response.status === 200) {
const [current] = await server.db.select().from(toolConnections).where(eq(toolConnections.id, created.body.connectionId));
const replacement = current.credentialSecretRefs.find(ref => ref.configPath === "credentials.signingSecret")!;
expect(replacement.secretId).not.toBe(signing.secretId);
expect(await secretService(server.db).resolveSecretValue(f.companyId, replacement.secretId, "latest", {
accessContext: { consumerType: "system", consumerId: "integration-fixture", actorType: "system" },
})).toBe(`human-repair-${state}`);
}
}
// Unexpected resolver failures must abort before writing new references.
const [beforeFault] = await server.db.select().from(toolConnections).where(eq(toolConnections.id, created.body.connectionId));
const resolverFault = vi.spyOn(localEncryptedProvider, "resolveVersion").mockRejectedValue(new Error("Unexpected secret resolution failure"));
try {
const response = await fetch(server.apiUrl + "/api" + setupPath, { method: "POST", headers: { cookie, origin: server.apiUrl, "Content-Type": "application/json" },
body: JSON.stringify({ action: "reconnect", credentials: { signingSecret: "human-repair-unexpected", botToken: "xoxb-fixture-token" } }) });
expect(response.status).toBe(500);
expect((await server.db.select().from(toolConnections).where(eq(toolConnections.id, created.body.connectionId)))[0].credentialSecretRefs).toEqual(beforeFault.credentialSecretRefs);
} finally { resolverFault.mockRestore(); }
// Identity is a provider fixture; production HTTP admission and policy remain real.
const [principal] = await server.db.insert(chatExternalPrincipals).values({ companyId: f.companyId, provider: "slack", providerAccountId: "TFIXTURE", externalId: "UHUMAN", isBot: false }).returning();
await server.db.insert(chatIdentityLinks).values({ companyId: f.companyId, endpointId: created.body.id, principalId: principal.id, paperclipUserId: f.userId, status: "linked", confirmedAt: new Date() });
await server.db.update(chatEndpoints).set({ status: "active", allowDirectMessages: true }).where(eq(chatEndpoints.id, created.body.id));
const [deny] = await server.db.insert(toolPolicies).values({ companyId: f.companyId, name: "Test messages off", policyType: "block", selectors: { connectionId: created.body.connectionId }, priority: 1 }).returning();
const testPath = `/chat-endpoints/${created.body.id}/slack/test-message`;
const denied = await f.request("POST", testPath, { requestId: randomUUID() });
expect(denied, JSON.stringify(denied)).toMatchObject({ status: 403 });
expect(sent).toBe(0);
await server.db.update(toolPolicies).set({ policyType: "allow" }).where(eq(toolPolicies.id, deny.id));
vi.useFakeTimers({ toFake: ["Date"] });
await server.db.insert(toolPolicies).values({ companyId: f.companyId, name: "One Slack setup send", policyType: "rate_limit", selectors: { connectionId: created.body.connectionId }, priority: 0, config: { limit: 1, windowSeconds: 60, keyBy: ["agent", "tool"] } });
const requestId = randomUUID();
const tested = await f.request("POST", testPath, { requestId });
expect(tested, JSON.stringify(tested)).toMatchObject({ status: 200, body: { status: "sent" } });
expect(await f.request("POST", testPath, { requestId })).toMatchObject({ status: 200 });
expect(await f.request("POST", testPath, { requestId: randomUUID() })).toMatchObject({ status: 403, body: { details: { reasonCode: "rate_limited" } } });
expect(await f.request("POST", testPath, { requestId: randomUUID(), slackUserId: "UOTHER" })).toMatchObject({ status: 403 });
expect(sent).toBe(1);
vi.useRealTimers();
const [beforeRevocation] = await server.db.select().from(toolConnections).where(eq(toolConnections.id, created.body.connectionId));
revokeDuringAuth = true;
expect(await f.request("POST", setupPath, { action: "reconnect", credentials: { botToken: "xoxb-fixture-token" } }, f.jwt)).toMatchObject({ status: 403 });
expect((await server.db.select().from(toolConnections).where(eq(toolConnections.id, created.body.connectionId)))[0].credentialSecretRefs)
.toEqual(beforeRevocation.credentialSecretRefs);
} finally { vi.useRealTimers(); provider = null; }
});
it.each(["linked", "invitation"] as const)("checks caller and target policy for delegated Slack %s sends", async recipient => {
for (const mode of ["caller-block", "caller-ask", "caller-rate", "target-block", "target-rate", "shared-rate"] as const) {
const f = await fixture();
const cookie = await server.humanSession(f.userId);
const targetResponse = await fetch(`${server.apiUrl}/api/companies/${f.companyId}/agents`, {
method: "POST", headers: { cookie, origin: server.apiUrl, "Content-Type": "application/json" },
body: JSON.stringify({ name: "Slack target", role: "engineer", adapterType: "paperclip_runner", adapterConfig: { provider: "codex" },
permissions: {}, runtimeConfig: { heartbeat: { enabled: false, wakeOnDemand: false } } }),
});
const target = await targetResponse.json();
expect(targetResponse.status, JSON.stringify(target)).toBe(201);
await lifecycle.process(target.id);
await vi.waitFor(async () => expect((await agentService(server.db).getById(target.id))?.status).toBe("idle"));
let sends = 0;
const workspaceId = `T${f.companyId.replaceAll("-", "").toUpperCase()}`;
const scopes = [...SLACK_CHAT_BOT_SCOPES, ...SLACK_BOT_TOOL_SCOPES];
provider = async (url, init) => {
if (url.endsWith("auth.test")) return Response.json({ ok: true, team_id: workspaceId, team: "Delegated fixture", user_id: `U${target.id.replaceAll("-", "").toUpperCase()}`, user: "fixture-bot" }, { headers: { "x-oauth-scopes": scopes.join(",") } });
if (url.includes("conversations.list")) return Response.json({ ok: true, channels: [], response_metadata: { next_cursor: "" } });
if (url.endsWith("users.info")) return Response.json({ ok: true, user: { id: new URLSearchParams(String(init?.body)).get("user"), team_id: workspaceId, name: "Fixture user", is_bot: false, deleted: false } });
if (url.endsWith("chat.postMessage")) { sends++; return Response.json({ ok: true, channel: "DTEST", ts: "1.000001" }); }
if (url.endsWith("agents.sessions.setStatus")) return Response.json({ ok: true });
throw new Error(`Unexpected Slack fixture operation: ${new URL(url).pathname}`);
};
try {
const created = await f.request("POST", `/companies/${f.companyId}/chat-endpoints`, { provider: "slack", assignedAgentId: target.id, slackSetupMethod: "manual" });
expect(created, JSON.stringify(created)).toMatchObject({ status: 201 });
const configured = await fetch(`${server.apiUrl}/api/chat-endpoints/${created.body.id}/setup`, {
method: "POST", headers: { cookie, origin: server.apiUrl, "Content-Type": "application/json" },
body: JSON.stringify({ action: "configure", credentials: { botToken: `xoxb-${target.id}`, signingSecret: "fixture-signing-secret" } }),
});
expect({ status: configured.status, body: await configured.json() }).toMatchObject({ status: 200 });
await server.db.update(chatEndpoints).set({ status: "active", allowDirectMessages: true }).where(eq(chatEndpoints.id, created.body.id));
await server.db.update(toolConnections).set({ status: "active", enabled: true, healthStatus: "healthy" }).where(eq(toolConnections.id, created.body.connectionId));
if (recipient === "linked") {
const [principal] = await server.db.insert(chatExternalPrincipals).values({ companyId: f.companyId, provider: "slack", providerAccountId: workspaceId, externalId: "UHUMAN", isBot: false }).returning();
await server.db.insert(chatIdentityLinks).values({ companyId: f.companyId, endpointId: created.body.id, principalId: principal.id, paperclipUserId: f.userId, status: "linked", confirmedAt: new Date() });
}
await server.db.insert(toolPolicies).values({ companyId: f.companyId, name: "Slack fixture access", policyType: "allow", selectors: { connectionId: created.body.connectionId }, priority: 2 });
const rate = mode.endsWith("rate");
await server.db.insert(toolPolicies).values({ companyId: f.companyId, name: mode, policyType: rate ? "rate_limit" : mode === "caller-ask" ? "require_approval" : "block",
selectors: { connectionId: created.body.connectionId, ...(mode === "shared-rate" ? {} : { agentId: mode.startsWith("caller") ? f.agentId : target.id }) }, priority: 0,
...(rate ? { config: { limit: 1, windowSeconds: 60, keyBy: mode === "shared-rate" ? ["company", "tool"] : ["agent", "tool"] } } : {}),
});
const path = `/chat-endpoints/${created.body.id}/slack/test-message`;
if (rate) {
// Keep both calls in one rate window even when CI crosses a minute boundary.
vi.useFakeTimers({ toFake: ["Date"] });
// Shared buckets admit one actual send, while each principal's own
// limit also prevents the next send. Neither may consume twice.
expect(await f.request("POST", path, { requestId: randomUUID(), slackUserId: "UHUMAN" }, f.jwt), mode)
.toMatchObject({ status: 200, body: { status: recipient === "linked" ? "sent" : "connection_pending" } });
expect(sends, mode).toBe(1);
}
for (const token of [f.key.token, f.jwt]) {
const denied = await f.request("POST", path, { requestId: randomUUID(), slackUserId: "UHUMAN" }, token);
expect(denied, `${mode}: ${JSON.stringify(denied)}`).toMatchObject({ status: 403, body: { details: { reasonCode: rate ? "rate_limited" : mode === "caller-ask" ? "requires_approval_policy" : "deny_policy_block" } } });
if (mode === "caller-ask") expect(denied.body.details.requiredUserAction).toMatchObject({ kind: "approve" });
}
expect(sends, mode).toBe(rate ? 1 : 0);
const audit = await server.db.select().from(toolAccessAuditEvents).where(eq(toolAccessAuditEvents.companyId, f.companyId));
expect(audit.some(event => event.details.agentId === f.agentId && event.actorId === f.agentId), mode).toBe(true);
} finally { vi.useRealTimers(); provider = null; }
}
}, 90_000);
it("imports a GitHub App and discovers repositories against provider fixtures", async () => {
const f = await fixture();
const permissions = { contents: "read", issues: "write", metadata: "read", pull_requests: "write" };
const appId = 100000 + Number.parseInt(f.agentId.slice(0, 6), 16);
const privateKey = generateKeyPairSync("rsa", { modulusLength: 2048 }).privateKey.export({ type: "pkcs8", format: "pem" }).toString();
provider = async (url, init) => {
const path = new URL(url).pathname;
if (path === "/login/oauth/access_token") return Response.json({ access_token: "fixture-personal-token" });
if (path === "/user") return Response.json({ id: 1357, login: "fixture-person" });
if (path === "/user/installations") return Response.json({ installations: [{ id: 2468, app_id: appId }] });
if (path === "/app") return Response.json({ id: appId, slug: `fixture-${f.agentId.slice(0, 8)}`, name: "Fixture App", owner: { login: "fixture" }, permissions,
events: ["pull_request", "github_app_authorization", "installation", "installation_repositories", "issue_comment", "pull_request_review_comment"] });
if (path === "/app/hook/config") return Response.json(init?.body ? JSON.parse(String(init.body)) : { url: "https://paperclip.fixture.invalid", content_type: "json", insecure_ssl: "0" });
if (path === "/app/installations") return Response.json([{ id: 2468, account: { id: 1357, login: "fixture", type: "Organization" }, permissions, suspended_at: null }]);
if (path === "/app/installations/2468") return Response.json({ permissions, suspended_at: null });
if (path === "/app/installations/2468/access_tokens") return Response.json({ token: "fixture-installation-token" }, { status: 201 });
if (path === "/installation/repositories") return Response.json({ repositories: [{ id: 42, name: "repo", full_name: "fixture/repo", private: true, owner: { login: "fixture" }, html_url: "https://github.com/fixture/repo" }] });
throw new Error(`Unexpected GitHub fixture operation: ${path}`);
};
try {
const created = await f.request("POST", `/companies/${f.companyId}/chat-endpoints`, { provider: "github", assignedAgentId: f.agentId });
expect(await f.request("POST", `/chat-endpoints/${created.body.id}/github/registration`, { name: `fixture-${f.agentId.slice(0, 8)}` })).toMatchObject({ status: 200 });
const cookie = await server.humanSession(f.userId);
const humanRequest = (path: string, method = "GET", body?: unknown, session = cookie) => fetch(server.apiUrl + path, {
method, redirect: "manual", headers: { cookie: session, "Content-Type": "application/json", origin: server.apiUrl },
...(body === undefined ? {} : { body: JSON.stringify(body) }),
});
const appCredentials = { appId: String(appId), privateKey, webhookSecret: "fixture-webhook-secret", clientId: "fixture-client", clientSecret: "fixture-secret" };
expect(await f.request("POST", `/chat-endpoints/${created.body.id}/github/app`, appCredentials)).toMatchObject({ status: 200, body: { status: "needs_user_action" } });
const importHandoff = await f.request("POST", `/chat-endpoints/${created.body.id}/github/app`, {}, f.jwt);
expect(importHandoff).toMatchObject({ status: 200, body: { requiredUserAction: { kind: "enter_credentials", userId: f.userId } } });
expect.soft(importHandoff.body.requiredUserAction.setupPath).toContain("stage=credentials");
expect.soft(importHandoff.body.requiredUserAction.setupPath).not.toContain("reconnect=1");
const privateKeyHandoff = await f.request("POST", `/chat-endpoints/${created.body.id}/setup`, { action: "configure", credentials: { privateKey } }, f.jwt);
expect.soft(privateKeyHandoff, JSON.stringify(privateKeyHandoff)).toMatchObject({ status: 200, body: { requiredUserAction: { kind: "enter_credentials" } } });
if (privateKeyHandoff.status === 200) {
const entryLink = new URL(privateKeyHandoff.body.requiredUserAction.setupPath, server.apiUrl);
expect.soft(entryLink.searchParams.get("credentialFields")).toBe("privateKey");
expect.soft(entryLink.searchParams.get("credentialMode")).toBe("all");
expect(privateKeyHandoff.body.interactionId).not.toBe(importHandoff.body.interactionId);
const retry = await f.request("POST", `/chat-endpoints/${created.body.id}/setup`, { action: "configure", credentials: { privateKey } }, f.jwt);
expect(retry.body.interactionId).toBe(privateKeyHandoff.body.interactionId);
}
expect((await server.db.select().from(toolConnections).where(eq(toolConnections.id, created.body.connectionId)))[0].credentialSecretRefs).toEqual([]);
const imported = await humanRequest(`/api/chat-endpoints/${created.body.id}/github/app`, "POST", appCredentials);
expect(imported.status).toBe(200);
expect(JSON.stringify(await imported.json())).not.toContain("PRIVATE KEY");
const refreshed = await f.request("POST", `/chat-endpoints/${created.body.id}/github/repositories/refresh`, {});
expect(refreshed, JSON.stringify(refreshed)).toMatchObject({ status: 200 });
expect(JSON.stringify(refreshed.body)).toContain("fixture/repo");
expect(await f.request("POST", `/chat-endpoints/${created.body.id}/github/verify`, {})).toMatchObject({ status: 200 });
await server.db.update(chatEndpoints).set({ status: "active" }).where(eq(chatEndpoints.id, created.body.id));
await server.db.update(toolConnections).set({ status: "active", enabled: true, healthStatus: "healthy" }).where(eq(toolConnections.id, created.body.connectionId));
const identityHandoff = await f.request("POST", `/chat-endpoints/${created.body.id}/github/identity/start`, {}, f.jwt);
expect(identityHandoff).toMatchObject({ status: 200, body: { requiredUserAction: { kind: "confirm_identity" } } });
const [identityIntent] = await server.db.select().from(issueThreadInteractions).where(eq(issueThreadInteractions.id, identityHandoff.body.interactionId));
const [identityTask] = await server.db.select().from(issues).where(eq(issues.id, f.issueId));
expect(await findSatisfiedToolConnection(server.db, identityTask, identityIntent)).toBeNull();
const start = await humanRequest(`/api/chat-endpoints/${created.body.id}/github/identity/start`, "POST", {});
const started = await start.json();
expect(started, JSON.stringify(started)).toHaveProperty("authorizationUrl");
const state = new URL(started.authorizationUrl).searchParams.get("state")!;
const callback = `/api/chat-github/identity/callback?state=${encodeURIComponent(state)}&code=fixture-code`;
const other = await fixture();
const otherCookie = await server.humanSession(other.userId);
expect((await humanRequest(callback, "GET", undefined, otherCookie)).status).toBe(403);
expect((await humanRequest(callback)).status).toBe(303);
expect((await humanRequest(callback)).status).toBe(403);
expect(await findSatisfiedToolConnection(server.db, identityTask, identityIntent)).toBeNull();
const confirmed = await humanRequest(`/api/chat-endpoints/${created.body.id}/github/identity/confirm`, "POST", { githubUserId: "1357" });
expect(confirmed.status, JSON.stringify(await confirmed.json())).toBe(200);
const [endpoint] = await server.db.select().from(chatEndpoints).where(eq(chatEndpoints.id, created.body.id));
await server.db.update(chatEndpoints).set({ status: "active", setup: { ...endpoint.setup, webhookVerifiedAt: new Date().toISOString() } }).where(eq(chatEndpoints.id, endpoint.id));
await server.db.update(toolConnections).set({ status: "active", enabled: true, healthStatus: "healthy" }).where(eq(toolConnections.id, created.body.connectionId));
expect((await findSatisfiedToolConnection(server.db, identityTask, identityIntent))?.id).toBe(created.body.connectionId);
const credentialHandoff = await f.request("POST", `/chat-endpoints/${created.body.id}/setup`, { action: "reconnect", credentials: { webhookSecret: "agent-chosen-key" } }, f.jwt);
expect(credentialHandoff).toMatchObject({ status: 200, body: { requiredUserAction: { kind: "enter_credentials" } } });
const entryLink = new URL(credentialHandoff.body.requiredUserAction.setupPath, server.apiUrl);
expect(entryLink.searchParams.get("reconnect")).toBe("1");
expect.soft(entryLink.searchParams.get("credentialFields")).toBe("webhookSecret");
expect.soft(entryLink.searchParams.get("credentialMode")).toBe("all");
const entryOptionsResponse = await humanRequest(`/api/connection-intents/${credentialHandoff.body.interactionId}/setup-options`);
const entryOptions = await entryOptionsResponse.json();
expect.soft(entryOptions.chatSetup.path).toBe(credentialHandoff.body.requiredUserAction.setupPath);
const [entryIntent] = await server.db.select().from(issueThreadInteractions).where(eq(issueThreadInteractions.id, credentialHandoff.body.interactionId));
const [entryTask] = await server.db.select().from(issues).where(eq(issues.id, f.issueId));
expect(await findSatisfiedToolConnection(server.db, entryTask, entryIntent)).toBeNull();
const repairedResponse = await humanRequest(`/api/chat-endpoints/${created.body.id}/setup`, "POST", { action: "reconnect", credentials: { webhookSecret: "human-replacement-webhook" } });
expect(repairedResponse.status).toBe(200);
const [repairedConnection] = await server.db.select().from(toolConnections).where(eq(toolConnections.id, created.body.connectionId));
for (const [field, value] of [["privateKey", privateKey], ["clientSecret", "fixture-secret"], ["webhookSecret", "human-replacement-webhook"]]) {
const ref = repairedConnection.credentialSecretRefs.find(ref => ref.configPath === `credentials.${field}`)!;
expect(await secretService(server.db).resolveSecretValue(f.companyId, ref.secretId, "latest", {
accessContext: { consumerType: "system", consumerId: "integration-fixture", actorType: "system" },
})).toBe(value);
}
expect(await findSatisfiedToolConnection(server.db, entryTask, entryIntent)).toBeNull();
const [repairedEndpoint] = await server.db.select().from(chatEndpoints).where(eq(chatEndpoints.id, created.body.id));
expect(repairedEndpoint.setup.webhookVerifiedAt).toBeNull();
await server.db.update(chatEndpoints).set({ status: "active" }).where(eq(chatEndpoints.id, created.body.id));
await server.db.update(toolConnections).set({ status: "active", enabled: true, healthStatus: "healthy" }).where(eq(toolConnections.id, created.body.connectionId));
expect(await findSatisfiedToolConnection(server.db, entryTask, entryIntent)).toBeNull();
await server.db.update(chatEndpoints).set({ setup: { ...repairedEndpoint.setup, webhookVerifiedAt: new Date().toISOString() } }).where(eq(chatEndpoints.id, created.body.id));
expect((await findSatisfiedToolConnection(server.db, entryTask, entryIntent))?.id).toBe(created.body.connectionId);
const [beforeRotation] = await server.db.select().from(toolConnections).where(eq(toolConnections.id, created.body.connectionId));
let rotationIntentId = "";
for (const token of [f.key.token, f.jwt, f.jwt]) {
const rotation = await f.request("POST", `/chat-endpoints/${created.body.id}/setup-secret`, {}, token);
expect(rotation, JSON.stringify(rotation)).toMatchObject({ status: 200, body: { status: "needs_user_action", endpointId: created.body.id,
requiredUserAction: { kind: "enter_credentials", userId: f.userId } } });
expect(rotation.body).not.toHaveProperty("webhookSecret");
expect(rotation.body.requiredUserAction.setupPath).toContain("stage=credentials&reconnect=1&rotateSecret=1");
if (token === f.jwt) {
if (rotationIntentId) expect(rotation.body.interactionId).toBe(rotationIntentId);
rotationIntentId = rotation.body.interactionId;
}
}
expect(rotationIntentId).not.toBe(importHandoff.body.interactionId);
const [afterRotation] = await server.db.select().from(toolConnections).where(eq(toolConnections.id, created.body.connectionId));
expect(afterRotation.credentialSecretRefs).toEqual(beforeRotation.credentialSecretRefs);
const [rotationIntent] = await server.db.select().from(issueThreadInteractions).where(eq(issueThreadInteractions.id, rotationIntentId));
const [task] = await server.db.select().from(issues).where(eq(issues.id, f.issueId));
const setupOptions = await humanRequest(`/api/connection-intents/${rotationIntentId}/setup-options`);
expect(setupOptions.status).toBe(200);
expect((await setupOptions.json()).chatSetup.path).toContain("stage=credentials&reconnect=1&rotateSecret=1");
expect(await findSatisfiedToolConnection(server.db, task, rotationIntent)).toBeNull();
await satisfiedConnectionIntentService(server.db).sweepPending();
expect((await server.db.select().from(issueThreadInteractions).where(eq(issueThreadInteractions.id, rotationIntentId)))[0].status).toBe("pending");
const humanRotation = await humanRequest(`/api/chat-endpoints/${created.body.id}/setup-secret`, "POST", {});
expect(humanRotation.status).toBe(201);
const generatedRotation = await humanRotation.json();
expect(generatedRotation).toMatchObject({ webhookSecret: expect.stringMatching(/^[a-f0-9]{64}$/), credentialRotationId: expect.any(String) });
const [humanRotated] = await server.db.select().from(toolConnections).where(eq(toolConnections.id, created.body.connectionId));
expect(humanRotated.credentialSecretRefs).not.toEqual(beforeRotation.credentialSecretRefs);
const rotationState = await f.request("GET", `/chat-endpoints/${endpoint.id}`);
expect(rotationState.body.setup).toMatchObject({ credentialRotationId: expect.any(String), webhookVerifiedAt: null });
expect(rotationState.body.setup.credentialRotationId).toBe(generatedRotation.credentialRotationId);
const nextRotation = await f.request("POST", `/chat-endpoints/${endpoint.id}/setup-secret`, {});
expect(nextRotation.body.requiredUserAction.setupPath).toContain(`afterRotationId=${rotationState.body.setup.credentialRotationId}`);
expect(await findSatisfiedToolConnection(server.db, task, rotationIntent)).toBeNull();
// Provider-fixture states separate restored health from new webhook verification.
const [rotatedEndpoint] = await server.db.select().from(chatEndpoints).where(eq(chatEndpoints.id, endpoint.id));
await server.db.update(chatEndpoints).set({ status: "active" }).where(eq(chatEndpoints.id, endpoint.id));
await server.db.update(toolConnections).set({ status: "active", enabled: true, healthStatus: "healthy" }).where(eq(toolConnections.id, created.body.connectionId));
expect(await findSatisfiedToolConnection(server.db, task, rotationIntent)).toBeNull();
await server.db.update(chatEndpoints).set({ setup: { ...rotatedEndpoint.setup, webhookVerifiedAt: new Date().toISOString() } }).where(eq(chatEndpoints.id, endpoint.id));
expect((await findSatisfiedToolConnection(server.db, task, rotationIntent))?.id).toBe(created.body.connectionId);
} finally { provider = null; }
});
it("creates an email account through the API without exposing its write-only credential and reuses retries", async () => {
const f = await fixture();
provider = async url => {
if (new URL(url).pathname === "/v0/auth/me") return Response.json({ scope_type: "organization", organization_id: "fixture-org" });
throw new Error(`Unexpected AgentMail fixture operation: ${new URL(url).pathname}`);
};
try {
const body = { apiKey: "fixture-agentmail-secret", grantKind: "user", agentIds: [f.agentId], allAgents: false, idempotencyKey: randomUUID() };
const created = await f.request("POST", `/companies/${f.companyId}/email/connections`, body);
expect(created, JSON.stringify(created)).toMatchObject({ status: 201 });
expect(JSON.stringify(created.body)).not.toContain(body.apiKey);
expect(await f.request("POST", `/companies/${f.companyId}/email/connections`, body)).toMatchObject({ status: 201, body: { id: created.body.id } });
expect((await f.request("GET", `/companies/${f.companyId}/email/connections`)).body).toEqual(expect.arrayContaining([expect.objectContaining({ id: created.body.id })]));
const [saved] = await server.db.select().from(toolConnections).where(eq(toolConnections.id, created.body.id));
expect(saved.createdByUserId).toBe(f.userId);
const other = await f.request("POST", `/companies/${f.companyId}/email/connections`, { ...body, apiKey: "other-agentmail-secret", grantKind: "organization", idempotencyKey: randomUUID() });
expect(other).toMatchObject({ status: 201 });
await server.db.update(principalPermissionGrants).set({ scope: { connectionIds: [created.body.id] } }).where(and(eq(principalPermissionGrants.principalId, f.agentId), eq(principalPermissionGrants.permissionKey, "tools:manage_connections")));
for (const token of [f.key.token, f.jwt]) {
const listed = await f.request("GET", `/companies/${f.companyId}/email/connections?connectionId=${created.body.id}`, undefined, token);
expect(listed, JSON.stringify(listed)).toMatchObject({ status: 200 });
expect(listed.body.map((account: { id: string }) => account.id)).toEqual([created.body.id]);
}
} finally { provider = null; }
});
it("shares tool-activity authorization by task and reuses run identity within one page", async () => {
const f = await fixture();
const connection = await f.request("POST", `/companies/${f.companyId}/tools/connections`, { name: "Activity page", transport: "mcp_remote", authKind: "none", transportConfig: { url: "https://1.1.1.1/mcp" } });
expect(connection).toMatchObject({ status: 201 });
const [hidden] = await server.db.insert(issues).values({ companyId: f.companyId, title: "Private activity task", visibility: "private", status: "in_progress", responsibleUserId: randomUUID() }).returning();
await server.db.insert(toolCallEvents).values(Array.from({ length: 100 }, (_, index) => ({ companyId: f.companyId, connectionId: connection.body.id,
eventType: "policy_decision" as const, actorType: "agent", actorId: f.agentId, issueId: index % 2 ? f.issueId : hidden.id, toolName: index % 2 ? "visible" : "private_activity" })));
const original = connectionManagementModule.connectionManagementService;
const checked: string[] = [];
const authorization = vi.spyOn(connectionManagementModule, "connectionManagementService").mockImplementation((db) => {
const service = original(db);
return { ...service, capability: async (...args: Parameters<typeof service.capability>) => {
if (args[1] === "connections:audit") checked.push(args[2].issueId ?? "unbound");
return service.capability(...args);
} };
});
const capture = vi.spyOn(runIdentity, "captureRunIdentity");
try {
const path = `/tool-connections/${connection.body.id}/activity?limit=100`;
const result = await f.request("GET", path, undefined, f.jwt);
expect(result, JSON.stringify(result)).toMatchObject({ status: 200 });
expect(result.body.events).toHaveLength(50);
expect(JSON.stringify(result.body)).not.toContain("private_activity");
expect(checked, JSON.stringify({ checks: checked.length, captures: capture.mock.calls.length })).toHaveLength(2);
expect(capture.mock.calls.length).toBeGreaterThan(0);
expect(capture.mock.calls.length).toBeLessThanOrEqual(2);
await server.db.delete(principalPermissionGrants).where(and(eq(principalPermissionGrants.principalId, f.agentId), eq(principalPermissionGrants.permissionKey, "tools:view_audit")));
expect(await f.request("GET", path, undefined, f.jwt)).toMatchObject({ status: 403 });
} finally { authorization.mockRestore(); capture.mockRestore(); }
});
it("bounds activity authorization to the requested page rather than endpoint history", async () => {
const f = await fixture();
const endpoint = await f.request("POST", `/companies/${f.companyId}/chat-endpoints`, { provider: "github", assignedAgentId: f.agentId });
expect(endpoint).toMatchObject({ status: 201 });
const conversations = await server.db.insert(chatConversations).values(Array.from({ length: 200 }, (_, index) => ({
companyId: f.companyId, endpointId: endpoint.body.id, issueId: f.issueId,
externalConversationId: `history-${index}`, externalLabel: `History ${index}`,
}))).returning();
const [delivery] = await server.db.insert(chatDeliveries).values({ companyId: f.companyId, endpointId: endpoint.body.id,
conversationId: conversations[0].id, providerEventId: randomUUID(), deduplicationKey: randomUUID(), eventKind: "mention", state: "processed", normalizedEvent: {} }).returning();
const original = connectionManagementModule.connectionManagementService;
const checked: string[] = [];
const authorization = vi.spyOn(connectionManagementModule, "connectionManagementService").mockImplementation((db) => {
const service = original(db);
return { ...service, capability: async (...args: Parameters<typeof service.capability>) => {
if (args[1] === "connections:audit") checked.push(args[2].issueId ?? "unbound");
return service.capability(...args);
} };
});
try {
const result = await f.request("GET", `/chat-endpoints/${endpoint.body.id}/activity?limit=1`, undefined, f.jwt);
expect(result, JSON.stringify(result)).toMatchObject({ status: 200, body: { items: [expect.objectContaining({ id: delivery.id })] } });
expect(checked).toEqual([f.issueId]);
} finally { authorization.mockRestore(); }
});
it("continues activity pagination through private-only windows", async () => {
const f = await fixture();
const endpoint = await f.request("POST", `/companies/${f.companyId}/chat-endpoints`, { provider: "github", assignedAgentId: f.agentId });
const [hidden] = await server.db.insert(issues).values({ companyId: f.companyId, title: "Private history", visibility: "private", status: "in_progress", responsibleUserId: randomUUID() }).returning();
const conversations = await server.db.insert(chatConversations).values([f.issueId, hidden.id].map(issueId => ({ companyId: f.companyId, endpointId: endpoint.body.id, issueId,
externalConversationId: randomUUID(), externalLabel: "History" }))).returning();
const rows = Array.from({ length: 30 }, (_, index) => ({ id: `${f.agentId.slice(0, 24)}${index.toString(16).padStart(12, "0")}`,
companyId: f.companyId, endpointId: endpoint.body.id, conversationId: conversations[index < 3 ? 0 : 1].id,
providerEventId: randomUUID(), deduplicationKey: randomUUID(), eventKind: "message" as const, state: "processed" as const, redactedError: index < 3 ? null : "private_history", normalizedEvent: {}, createdAt: new Date("2026-10-25T12:00:00.000Z") }));
await server.db.insert(chatDeliveries).values(rows);
const path = `/chat-endpoints/${endpoint.body.id}/activity?limit=25`;
const first = await f.request("GET", path, undefined, f.jwt);
expect(first).toMatchObject({ status: 200, body: { items: [], nextCursor: expect.any(String) } });
expect(JSON.stringify(first.body)).not.toContain("private_history");
const second = await f.request("GET", `${path}&cursor=${encodeURIComponent(first.body.nextCursor)}`, undefined, f.jwt);
expect(second).toMatchObject({ status: 200, body: { items: rows.slice(0, 3).reverse().map(row => expect.objectContaining({ id: row.id })), nextCursor: null } });
expect(JSON.stringify(second.body)).not.toContain("private_history");
});
it("saves delegated Arcade discovery for the responsible user and shares its inventory with that user", async () => {
const f = await fixture();
const transport = vi.spyOn(remoteHttpFetch, "guardedRemoteHttpFetch").mockImplementation(async (url, init) => provider!(String(url), init));
provider = async (url, init) => {
const path = new URL(url).pathname;
const rpc = init?.body ? JSON.parse(String(init.body)) : null;
if (rpc?.method === "initialize") return Response.json({ jsonrpc: "2.0", id: rpc.id, result: { protocolVersion: "2025-03-26", capabilities: { tools: {} }, serverInfo: { name: "fixture", version: "1" } } });
if (rpc?.method === "notifications/initialized") return new Response(null, { status: 202 });
if (rpc?.method === "tools/list") return Response.json({ jsonrpc: "2.0", id: rpc.id, result: { tools: [{ name: "Notion.ListPages", inputSchema: { type: "object" } }] } });
expect(new Headers(init?.headers).get("Authorization")).toBe("Bearer fixture-arcade-discovery");
if (path === "/v1/tools") return Response.json({ items: [{ qualified_name: "Notion.ListPages", toolkit: { name: "Notion" }, requirements: { met: true, authorization: { provider_id: "notion", token_status: "completed" } } }] });
if (path === "/v1/admin/user_connections") return Response.json({ items: [{ id: "arcade-work", provider_id: "notion", user_id: "arcade-user", connection_status: "active" }] });
throw new Error(`Unexpected Arcade fixture operation: ${path}`);
};
try {
const created = await f.request("POST", `/companies/${f.companyId}/tools/connections`, { name: "Arcade fixture", transport: "mcp_remote", authKind: "none", credentialPolicy: "shared", status: "active", enabled: true, config: { sourceTemplateKey: "arcade", url: "https://api.arcade.dev/mcp/test" } });
expect(created, JSON.stringify(created)).toMatchObject({ status: 201 });
const configured = await f.request("PUT", `/tool-connections/${created.body.id}/aggregator/discovery`, { apiKey: "fixture-arcade-discovery", userId: "arcade-user" }, f.jwt);
expect(configured, JSON.stringify(configured)).toMatchObject({ status: 200 });
const [saved] = await server.db.select().from(toolConnections).where(eq(toolConnections.id, created.body.id));
const discovery = saved.config.aggregatorDiscovery as Record<string, { secretId: string }>;
expect(discovery[f.agentId]).toBeUndefined();
expect(discovery[f.userId]).toMatchObject({ userId: "arcade-user", secretId: expect.any(String) });
const [secret] = await server.db.select().from(companySecrets).where(eq(companySecrets.id, discovery[f.userId].secretId));
expect(secret.ownerUserId).toBe(f.userId);
const events = await server.db.select().from(activityLog).where(eq(activityLog.entityId, saved.id));
expect(events).toEqual(expect.arrayContaining([expect.objectContaining({ action: "tool_connection.account_sync_configured", actorType: "agent", actorId: f.agentId, responsibleUserId: f.userId })]));
const cookie = await server.humanSession(f.userId);
await vi.waitFor(async () => {
const response = await fetch(`${server.apiUrl}/api/tool-connections/${saved.id}/aggregator/apps`, { headers: { cookie } });
const result = await response.json();
expect(result.discovery).toMatchObject({ availability: "available" });
expect(result.apps).toEqual(expect.arrayContaining([expect.objectContaining({ provider: "arcade", appSlug: "notion" })]));
}, { timeout: 10_000 });
} finally { transport.mockRestore(); provider = null; }
});
it("encrypts native local-login checks and replays without exposing device codes in receipts or assets", async () => {
const f = await fixture();
const priorHome = process.env.PAPERCLIP_HOME;
vi.stubEnv("PAPERCLIP_HOME", join(server.root, "local-login-receipt"));
const authorizationUrl = "https://fixture.invalid/device?private_handoff=disposable-login";
const code = "DISPOSABLE-DEVICE-CODE";
const browser = vi.spyOn(localBrowserLogin, "startLocalBrowserLogin").mockReturnValue({ authorizationUrl, code, abort: vi.fn() });
const intent = { provider: "openai", method: "subscription", ownership: "personal", name: "Local receipt fixture", agentIds: [], allAgents: false };
let sessionId: string | undefined;
try {
const started = await f.request("POST", `/companies/${f.companyId}/ai-connections/local/attempts`, intent, f.jwt);
expect(started, JSON.stringify(started)).toMatchObject({ status: 201, body: { sessionId: expect.any(String) } });
sessionId = started.body.sessionId;
const call = { tool: "call_api", callId: "local-login-private-check", arguments: {
operationId: "POST /api/companies/{companyId}/ai-connections/local/check", body: { ...intent, localSessionId: sessionId },
} };
const result = await f.authority.execute(call);
expect(result).toMatchObject({ ok: true, status: 200, data: { status: "sign_in_required", authorizationUrl, code } });
expect(result).not.toHaveProperty("artifact");
expect(await f.authority.execute(call)).toEqual(result);
const [run] = await server.db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, f.runId));
const receipts = (run.resultJson as { apiToolReceipts: Record<string, { operationId: string }> }).apiToolReceipts;
expect(Object.values(receipts).find(receipt => receipt.operationId === call.arguments.operationId)).toHaveProperty("encryptedResult");
for (const secret of [authorizationUrl, code]) {
expect(JSON.stringify(run.resultJson)).not.toContain(secret);
expect(JSON.stringify(redactRunnerApiPayloads(result))).not.toContain(secret);
}
expect((await f.snapshot()).assets.filter(asset => asset.objectKey.includes("/runner-api/"))).toEqual([]);
} finally {
if (sessionId) await f.request("DELETE", `/companies/${f.companyId}/ai-connections/local/attempts/${sessionId}`, undefined, f.jwt);
browser.mockRestore();
if (priorHome === undefined) delete process.env.PAPERCLIP_HOME;
else process.env.PAPERCLIP_HOME = priorHome;
}
});
// Tokens issued by agents before client-token minting became person-only
// remain valid until revoked, so runtime enforcement still covers them.
async function legacyAgentIssuedGatewayToken(f: Awaited<ReturnType<typeof fixture>>, gatewayId: string, name: string) {
const id = randomUUID(), token = `pcgw_${id}.${randomBytes(32).toString("base64url")}`;
const [row] = await server.db.insert(toolMcpGatewayTokens).values({ id, companyId: f.companyId, gatewayId, name, tokenHash: createHash("sha256").update(token).digest("hex"),
tokenPrefix: `pcgw_${id.slice(0, 8)}`, subjectType: "gateway_client", clientLabel: "fixture", allowedActions: ["tools/list", "tools/call", "resources/list", "resources/read", "prompts/list", "prompts/get"],
responsibleUserId: f.userId, createdByAgentId: f.agentId }).returning();
return { status: 201, body: { ...row, token } };
}
it("lets agents create and revoke gateway client tokens without ever receiving a token value", async () => {
const f = await fixture();
const profile = await f.request("POST", `/companies/${f.companyId}/tools/profiles`, { profileKey: "gateway-profile", name: "Gateway profile", entries: [] });
expect(profile, JSON.stringify(profile)).toMatchObject({ status: 201 });
const gateway = await f.request("POST", `/companies/${f.companyId}/tools/gateways`, { name: "Delegated gateway", slug: "delegated", profileId: profile.body.id });
expect(gateway, JSON.stringify(gateway)).toMatchObject({ status: 201 });
const body = { companyId: f.companyId, name: "Fixture client", clientLabel: "fixture", expiresAt: new Date(Date.now() + 3600000).toISOString() };
const call = (callId: string, operationId: string, pathParams: Record<string, string>, payload: unknown) =>
f.authority.execute({ tool: "call_api", callId, arguments: { operationId, pathParams, body: payload } }) as Promise<{ status: number; data: Record<string, unknown> }>;
const tokenValue = /pcgw_[0-9a-f-]{36}\.[A-Za-z0-9_-]+/;
const [company] = await server.db.select({ issuePrefix: companies.issuePrefix }).from(companies).where(eq(companies.id, f.companyId));
const tokensPath = `/${company.issuePrefix}/apps/gateways/${gateway.body.id}/tokens`;
expect(gateway.body).toMatchObject({ tokensPath, gatewayPath: `/${company.issuePrefix}/apps/gateways/${gateway.body.id}/overview` });
const created = await call("gateway-client-create", "POST /api/tool-gateway/gateways/{gatewayId}/tokens", { gatewayId: gateway.body.id }, body);
expect(created, JSON.stringify(created)).toMatchObject({ status: 201, data: { name: "Fixture client", pendingReveal: true, tokensPath,
tokenPrefix: expect.stringMatching(/^pcgw_[0-9a-f]{8}$/), createdByAgentId: f.agentId, responsibleUserId: f.userId,
requiredUserAction: { kind: "reveal_credential", userId: f.userId, setupPath: tokensPath, resource: { type: "gateway_token", id: created.data.id, companyId: f.companyId } } } });
expect(created.data).not.toHaveProperty("token");
expect(JSON.stringify(created)).not.toMatch(tokenValue);
const [pending] = await server.db.select().from(toolMcpGatewayTokens).where(eq(toolMcpGatewayTokens.id, created.data.id as string));
expect(pending).toMatchObject({ pendingReveal: true, createdByAgentId: f.agentId, responsibleUserId: f.userId, subjectType: "gateway_client", revokedAt: null });
// Agents cannot reveal any value.
const agentReveal = await call("gateway-client-reveal", "POST /api/tool-gateway/gateway-tokens/{tokenId}/reveal", { tokenId: created.data.id as string }, { companyId: f.companyId });
expect(agentReveal, JSON.stringify(agentReveal)).toMatchObject({ status: 403, data: { reasonCode: "gateway_token_requires_person" } });
expect(JSON.stringify(agentReveal)).not.toMatch(tokenValue);
expect(await f.request("POST", `/tool-gateway/gateway-tokens/${created.data.id}/reveal`, { companyId: f.companyId })).toMatchObject({ status: 403 });
// The responsible person reveals the value exactly once, and it works.
const cookie = await server.humanSession(f.userId);
const reveal = () => fetch(`${server.apiUrl}/api/tool-gateway/gateway-tokens/${created.data.id}/reveal`, { method: "POST", headers: { cookie, origin: server.apiUrl, "Content-Type": "application/json" }, body: JSON.stringify({ companyId: f.companyId }) });
const revealed = await reveal();
expect(revealed.status).toBe(200);
const value = await revealed.json() as { token: string; pendingReveal: boolean; id: string };
expect(value).toMatchObject({ id: created.data.id, pendingReveal: false, token: expect.stringMatching(new RegExp(`^pcgw_${created.data.id}\\.`)) });
expect(await reveal()).toMatchObject({ status: 409 });
expect(await server.db.select().from(activityLog).where(and(eq(activityLog.companyId, f.companyId), eq(activityLog.action, "tool_gateway.token_revealed"))))
.toEqual([expect.objectContaining({ actorType: "user", actorId: f.userId, entityId: gateway.body.id, details: { tokenId: created.data.id } })]);
const use = () => f.request("POST", `/mcp/gateways/${gateway.body.gatewayPublicId}`, { jsonrpc: "2.0", id: 1, method: "tools/list" }, value.token);
expect(await use()).toMatchObject({ status: 200 });
// Agents keep delegated revoke.
const revoked = await call("gateway-client-revoke", "POST /api/tool-gateway/gateway-tokens/{tokenId}/revoke", { tokenId: created.data.id as string }, { companyId: f.companyId });
expect(revoked, JSON.stringify(revoked)).toMatchObject({ status: 200, data: { id: created.data.id, revokedAt: expect.any(String) } });
expect((await use()).status).toBeGreaterThanOrEqual(400);
// A token revoked before reveal never gets a value.
const unrevealed = await call("gateway-client-create-2", "POST /api/tool-gateway/gateways/{gatewayId}/tokens", { gatewayId: gateway.body.id }, { ...body, name: "Never revealed" });
expect(unrevealed).toMatchObject({ status: 201 });
expect(await call("gateway-client-revoke-2", "POST /api/tool-gateway/gateway-tokens/{tokenId}/revoke", { tokenId: unrevealed.data.id as string }, { companyId: f.companyId })).toMatchObject({ status: 200 });
expect(await fetch(`${server.apiUrl}/api/tool-gateway/gateway-tokens/${unrevealed.data.id}/reveal`, { method: "POST", headers: { cookie, origin: server.apiUrl, "Content-Type": "application/json" }, body: JSON.stringify({ companyId: f.companyId }) }))
.toMatchObject({ status: 409 });
// A token that expired before reveal never gets a value.
const expiring = await call("gateway-client-create-3", "POST /api/tool-gateway/gateways/{gatewayId}/tokens", { gatewayId: gateway.body.id }, { ...body, name: "Expired before reveal" });
expect(expiring).toMatchObject({ status: 201 });
await server.db.update(toolMcpGatewayTokens).set({ expiresAt: new Date(Date.now() - 1000) }).where(eq(toolMcpGatewayTokens.id, expiring.data.id as string));
const expiredReveal = await fetch(`${server.apiUrl}/api/tool-gateway/gateway-tokens/${expiring.data.id}/reveal`, { method: "POST", headers: { cookie, origin: server.apiUrl, "Content-Type": "application/json" }, body: JSON.stringify({ companyId: f.companyId }) });
expect(expiredReveal.status).toBe(409);
expect(await expiredReveal.json()).toMatchObject({ reasonCode: "gateway_token_expired" });
const [stillPending] = await server.db.select().from(toolMcpGatewayTokens).where(eq(toolMcpGatewayTokens.id, expiring.data.id as string));
expect(stillPending).toMatchObject({ pendingReveal: true });
// Person callers keep receiving the value from create.
const personal = await fetch(`${server.apiUrl}/api/tool-gateway/gateways/${gateway.body.id}/tokens`, { method: "POST", headers: { cookie, origin: server.apiUrl, "Content-Type": "application/json" }, body: JSON.stringify(body) });
expect(personal.status).toBe(201);
expect(await personal.json()).toMatchObject({ token: expect.stringMatching(/^pcgw_/), createdByUserId: f.userId, createdByAgentId: null, pendingReveal: false });
});
it("reveals an agent-created gateway token value only to its responsible person", async () => {
const f = await fixture();
const profile = await f.request("POST", `/companies/${f.companyId}/tools/profiles`, { profileKey: "gateway-profile", name: "Gateway profile", entries: [] });
const gateway = await f.request("POST", `/companies/${f.companyId}/tools/gateways`, { name: "Delegated gateway", slug: "delegated", profileId: profile.body.id });
const created = await f.request("POST", `/tool-gateway/gateways/${gateway.body.id}/tokens`, { companyId: f.companyId, name: "Fixture client", clientLabel: "fixture" });
expect(created, JSON.stringify(created)).toMatchObject({ status: 201, body: { pendingReveal: true } });
const otherId = `other-admin-${randomUUID()}`;
await server.db.insert(authUsers).values({ id: otherId, name: "Other admin", email: `${otherId}@fixture.invalid`, emailVerified: true, createdAt: new Date(), updatedAt: new Date() });
const access = accessService(server.db);
await access.ensureMembership(f.companyId, "user", otherId, "owner", "active");
await access.ensureRoleDefaultGrants(f.companyId, otherId, "owner", otherId);
const cookie = await server.humanSession(otherId);
const reveal = await fetch(`${server.apiUrl}/api/tool-gateway/gateway-tokens/${created.body.id}/reveal`, { method: "POST", headers: { cookie, origin: server.apiUrl, "Content-Type": "application/json" }, body: JSON.stringify({ companyId: f.companyId }) });
expect(reveal.status).toBe(403);
expect(await reveal.json()).toMatchObject({ reasonCode: "gateway_token_reveal_requires_responsible_person" });
const [still] = await server.db.select().from(toolMcpGatewayTokens).where(eq(toolMcpGatewayTokens.id, created.body.id));
expect(still).toMatchObject({ pendingReveal: true });
});
it("keeps durable agent-issued gateway client tokens bound to the issuer and responsible user", async () => {
const f = await fixture();
const profile = await f.request("POST", `/companies/${f.companyId}/tools/profiles`, { profileKey: "gateway-profile", name: "Gateway profile", entries: [] });
expect(profile, JSON.stringify(profile)).toMatchObject({ status: 201 });
const gateway = await f.request("POST", `/companies/${f.companyId}/tools/gateways`, { name: "Delegated gateway", slug: "delegated", profileId: profile.body.id });
expect(gateway, JSON.stringify(gateway)).toMatchObject({ status: 201 });
const token = await legacyAgentIssuedGatewayToken(f, gateway.body.id, "Fixture client");
const [saved] = await server.db.select().from(toolMcpGatewayTokens).where(eq(toolMcpGatewayTokens.id, token.body.id));
expect(saved).toMatchObject({ createdByAgentId: f.agentId, responsibleUserId: f.userId, subjectType: "gateway_client" });
const use = () => f.request("POST", `/mcp/gateways/${gateway.body.gatewayPublicId}`, { jsonrpc: "2.0", id: 1, method: "tools/list" }, token.body.token);
expect(await use()).toMatchObject({ status: 200 });
await server.db.update(principalPermissionGrants).set({ scope: { purpose: "tool" } }).where(and(eq(principalPermissionGrants.principalId, f.agentId), eq(principalPermissionGrants.permissionKey, "tools:manage_connections")));
expect(await use()).toMatchObject({ status: 200 });
await server.db.update(heartbeatRuns).set({ status: "succeeded" }).where(eq(heartbeatRuns.id, f.runId));
expect(await use()).toMatchObject({ status: 200 });
await server.db.update(companyMemberships).set({ status: "suspended" }).where(and(eq(companyMemberships.companyId, f.companyId), eq(companyMemberships.principalId, f.userId)));
expect((await use()).status).toBeGreaterThanOrEqual(400);
await server.db.update(companyMemberships).set({ status: "active" }).where(and(eq(companyMemberships.companyId, f.companyId), eq(companyMemberships.principalId, f.userId)));
expect(await use()).toMatchObject({ status: 200 });
await server.db.delete(principalPermissionGrants).where(and(eq(principalPermissionGrants.principalId, f.agentId), eq(principalPermissionGrants.permissionKey, "tools:manage_connections")));
expect((await use()).status).toBeGreaterThanOrEqual(400);
// Model the token FK's ON DELETE SET NULL state without deleting the
// fixture's protected historical activity references to the agent.
await server.db.update(toolMcpGatewayTokens).set({ createdByAgentId: null }).where(eq(toolMcpGatewayTokens.id, token.body.id));
const [orphaned] = await server.db.select().from(toolMcpGatewayTokens).where(eq(toolMcpGatewayTokens.id, token.body.id));
expect(orphaned).toMatchObject({ createdByAgentId: null, responsibleUserId: f.userId });
expect((await use()).status).toBeGreaterThanOrEqual(400);
});
it.each(["block", "ask", "ask-board", "ask-revoked", "ask-rate", "ask-new-rate", "ask-new-shared-rate", "ask-new-rate-target-block", "ask-token-revoked", "ask-token-deleted", "ask-token-scope", "ask-grant-revoked", "ask-one-connection", "ask-one-connection-recovery", "ask-one-connection-revoked", "rate", "shared-rate", "target-block", "credential"] as const)("intersects gateway issuer and target rules (%s)", async mode => {
const f = await fixture();
const cookie = await server.humanSession(f.userId);
const createdTarget = await fetch(`${server.apiUrl}/api/companies/${f.companyId}/agents`, { method: "POST", headers: { cookie, origin: server.apiUrl, "Content-Type": "application/json" },
body: JSON.stringify({ name: "Gateway target", role: "engineer", adapterType: "paperclip_runner", adapterConfig: { provider: "codex" }, permissions: {}, runtimeConfig: { heartbeat: { enabled: false, wakeOnDemand: false } } }) });
const target = await createdTarget.json();
expect(createdTarget.status).toBe(201);
await lifecycle.process(target.id);
await vi.waitFor(async () => expect((await agentService(server.db).getById(target.id))?.status).toBe("idle"));
let calls = 0;
const remote = createServer(async (req, res) => {
let body = "";
for await (const chunk of req) body += chunk;
const rpc = JSON.parse(body || "{}");
let result: unknown;
if (rpc.method === "initialize") result = { protocolVersion: "2025-03-26", capabilities: { tools: {} }, serverInfo: { name: "Fixture", version: "1" } };
else if (rpc.method === "tools/list") result = { tools: [{ name: "fixture_probe", inputSchema: { type: "object" } }] };
else if (rpc.method === "tools/call") { calls++; result = { content: [{ type: "text", text: "Fixture gateway result" }] }; }
else { res.writeHead(202); res.end(); return; }
res.writeHead(200, { "content-type": "application/json" });
res.end(JSON.stringify({ jsonrpc: "2.0", id: rpc.id, result }));
});
await new Promise<void>(resolve => remote.listen(0, "127.0.0.1", resolve));
const address = remote.address();
if (!address || typeof address === "string") throw new Error("No fixture address");
try {
const connection = await f.request("POST", `/companies/${f.companyId}/tools/connections`, { name: "Gateway policy fixture", transport: "mcp_remote", authKind: "none", transportConfig: { url: `http://127.0.0.1:${address.port}/mcp` } });
expect(connection.status).toBe(201);
await server.db.update(toolConnections).set({ healthStatus: "ok" }).where(eq(toolConnections.id, connection.body.id));
const [entry] = await server.db.insert(toolCatalogEntries).values({ companyId: f.companyId, applicationId: connection.body.applicationId, connectionId: connection.body.id,
entryKind: "tool", name: "Fixture probe", toolName: "fixture_probe", inputSchema: { type: "object" }, riskLevel: "read", isReadOnly: true, status: "active", versionHash: "fixture-v1" }).returning();
expect(await f.request("POST", `/companies/${f.companyId}/tools/apps/${connection.body.id}/finish`, { enabledCatalogEntryIds: [entry.id], access: { agentIds: [f.agentId, target.id] } })).toMatchObject({ status: 200 });
const profile = await f.request("POST", `/companies/${f.companyId}/tools/profiles`, { profileKey: "gateway-rules", name: "Gateway rules", entries: [{ selectorType: "connection", connectionId: connection.body.id }] });
const gateway = await f.request("POST", `/companies/${f.companyId}/tools/gateways`, { name: "Cross-agent gateway", slug: "cross-agent", profileId: profile.body.id, agentId: target.id, issueId: f.issueId, projectId: f.projectId });
expect(gateway, JSON.stringify(gateway)).toMatchObject({ status: 201 });
const tokenBody = { companyId: f.companyId, name: "Issuer client", clientLabel: "fixture" };
const token = mode === "ask-board" ? await (async () => {
const response = await fetch(`${server.apiUrl}/api/tool-gateway/gateways/${gateway.body.id}/tokens`, { method: "POST", headers: { cookie, origin: server.apiUrl, "Content-Type": "application/json" }, body: JSON.stringify(tokenBody) });
return { status: response.status, body: await response.json() };
})() : await legacyAgentIssuedGatewayToken(f, gateway.body.id, tokenBody.name);
expect(token, JSON.stringify(token)).toMatchObject({ status: 201 });
const path = `/mcp/gateways/${gateway.body.gatewayPublicId}`;
const rpc = (method: string, params?: unknown) => f.request("POST", path, { jsonrpc: "2.0", id: randomUUID(), method, ...(params ? { params } : {}) }, token.body.token);
const listed = await rpc("tools/list");
const tool = listed.body.result?.tools?.find((tool: { name: string }) => tool.name.includes("fixture-probe"));
expect(tool, JSON.stringify(listed)).toBeTruthy();
vi.useFakeTimers({ toFake: ["Date"] });
const isRate = mode === "rate" || mode === "shared-rate";
await server.db.insert(toolPolicies).values({ companyId: f.companyId, name: "Gateway caller control", policyType: isRate ? "rate_limit" : mode.startsWith("ask") ? "require_approval" : mode === "credential" ? "allow" : "block",
selectors: { connectionId: connection.body.id, ...(mode === "shared-rate" ? {} : { agentId: mode === "target-block" || mode === "ask-board" ? target.id : f.agentId }) }, priority: 0,
...(isRate ? { config: { limit: 1, windowSeconds: 60, keyBy: mode === "shared-rate" ? ["company", "tool"] : ["agent", "tool"] } } : {}) });
if (mode === "ask-rate") await server.db.insert(toolPolicies).values({ companyId: f.companyId, name: "One issuer admission", policyType: "rate_limit", selectors: { connectionId: connection.body.id, agentId: f.agentId }, priority: -1, config: { limit: 1, windowSeconds: 60, keyBy: ["agent", "tool"] } });
if (mode === "credential") {
await server.db.update(toolConnections).set({ credentialPolicy: "per_agent" }).where(eq(toolConnections.id, connection.body.id));
await server.db.insert(connectionGrants).values([f.agentId, target.id].map(subjectAgentId => ({ companyId: f.companyId, connectionId: connection.body.id, kind: "agent" as const, subjectAgentId, status: "active" as const })));
}
const called = await rpc("tools/call", { name: tool.name, arguments: {} });
if (isRate) {
expect(calls, JSON.stringify(called)).toBe(1);
await rpc("tools/call", { name: tool.name, arguments: { another: true } });
expect(calls).toBe(1);
return;
}
expect(calls, JSON.stringify(called)).toBe(0);
if (mode === "credential") {
expect(JSON.stringify(called.body)).toContain("gateway_credential_identity_mismatch");
return;
}
const [invocation] = await server.db.select().from(toolInvocations).where(eq(toolInvocations.gatewayId, gateway.body.id));
expect(invocation).toMatchObject({ gatewayTokenId: token.body.id, actorId: mode === "ask-board" ? token.body.id : f.agentId, agentId: target.id, policyDecision: mode.startsWith("ask") ? "require_approval" : "deny" });
if (!mode.startsWith("ask")) return;
const [action] = await server.db.select().from(toolActionRequests).where(eq(toolActionRequests.invocationId, invocation.id));
expect(action.requestedByAgentId).toBe(mode === "ask-board" ? null : f.agentId);
if (mode.startsWith("ask-new-")) {
await server.db.insert(toolPolicies).values({ companyId: f.companyId, name: "New admission after review", policyType: "rate_limit",
selectors: { connectionId: connection.body.id, ...(mode === "ask-new-shared-rate" ? {} : { agentId: f.agentId }) },
priority: -2, config: { limit: 1, windowSeconds: 60, keyBy: mode === "ask-new-shared-rate" ? ["company", "tool"] : ["agent", "tool"] } });
if (mode === "ask-new-rate") await server.db.insert(toolPolicies).values({ companyId: f.companyId, name: "New target admission", policyType: "rate_limit",
selectors: { connectionId: connection.body.id, agentId: target.id }, priority: -1,
config: { limit: 1, windowSeconds: 60, keyBy: ["agent", "tool"] } });
if (mode === "ask-new-rate-target-block") await server.db.insert(toolPolicies).values({ companyId: f.companyId, name: "New target block", policyType: "block",
selectors: { connectionId: connection.body.id, agentId: target.id }, priority: -1 });
}
if (mode === "ask-revoked") await server.db.insert(toolPolicies).values({ companyId: f.companyId, name: "Issuer blocked after review", policyType: "block", selectors: { connectionId: connection.body.id, agentId: f.agentId }, priority: -1 });
if (mode === "ask-token-revoked") await server.db.update(toolMcpGatewayTokens).set({ revokedAt: new Date() }).where(eq(toolMcpGatewayTokens.id, token.body.id));
if (mode === "ask-token-deleted") await server.db.delete(toolMcpGatewayTokens).where(eq(toolMcpGatewayTokens.id, token.body.id));
if (mode === "ask-token-scope") await server.db.update(toolMcpGatewayTokens).set({ allowedActions: ["tools/list"] }).where(eq(toolMcpGatewayTokens.id, token.body.id));
if (mode === "ask-grant-revoked" || mode === "ask-one-connection-revoked") await server.db.delete(principalPermissionGrants).where(and(eq(principalPermissionGrants.principalId, f.agentId), eq(principalPermissionGrants.permissionKey, "tools:manage_connections")));
const approvalDenied = ["ask-revoked", "ask-new-rate-target-block", "ask-token-revoked", "ask-token-deleted", "ask-token-scope", "ask-grant-revoked", "ask-one-connection-revoked"].includes(mode);
if (mode.startsWith("ask-one-connection")) {
const limited = createDb(server.databaseUrl, { maxConnections: 1, idleTimeoutSeconds: 0, maxLifetimeSeconds: 0 });
const service = createToolGatewayService(limited);
let timer: ReturnType<typeof setTimeout> | undefined;
let attempt: Promise<unknown> | undefined;
try {
if (mode === "ask-one-connection-recovery") await commitToolActionReview(server.db, {
companyId: f.companyId, actionRequestId: action.id, decision: "approved", actor: { userId: f.userId },
});
attempt = mode === "ask-one-connection-recovery" ? service.recoverApprovedActionReviews(f.companyId)
: service.approveActionRequest({ companyId: f.companyId, actionRequestId: action.id, actor: { userId: f.userId } });
void attempt.catch(() => undefined);
const completed = Promise.race([attempt, new Promise<never>((_, reject) => {
timer = setTimeout(() => reject(new Error("Single-connection gateway approval did not settle")), 5000);
})]);
if (approvalDenied) await expect(completed).rejects.toMatchObject({ status: 403 });
else await completed;
expect((await limited.execute(sql`select 1 as available`))[0].available).toBe(1);
} finally {
clearTimeout(timer); await limited.$client.end({ timeout: 1 }); await attempt?.catch(() => undefined);
}
} else {
const response = await fetch(`${server.apiUrl}/api/tool-gateway/action-requests/${action.id}/approve`, { method: "POST", headers: { cookie, origin: server.apiUrl, "Content-Type": "application/json" }, body: JSON.stringify({ companyId: f.companyId }) });
expect(response.status, JSON.stringify(await response.json())).toBe(approvalDenied ? 403 : 200);
}
expect(calls).toBe(approvalDenied ? 0 : 1);
if (mode.startsWith("ask-new-")) {
const counters = await server.db.select().from(toolRateLimitCounters).where(eq(toolRateLimitCounters.companyId, f.companyId));
expect(counters).toHaveLength(approvalDenied ? 0 : mode === "ask-new-shared-rate" ? 1 : 2);
expect(counters.every(counter => counter.remaining === 0)).toBe(true);
if (!approvalDenied) {
const [settled] = await server.db.select().from(toolInvocations).where(eq(toolInvocations.id, invocation.id));
expect(settled.policyExplanation?.consumedRateAdmissions).toHaveLength(1);
expect(settled.policyExplanation?.delegatedCallerConsumedRateAdmissions).toHaveLength(1);
}
}
if (["ask-rate", "ask-new-rate", "ask-new-shared-rate"].includes(mode)) {
await rpc("tools/call", { name: tool.name, arguments: { another: true } });
expect(calls).toBe(1);
}
} finally { vi.useRealTimers(); await new Promise<void>((resolve, reject) => remote.close(error => error ? reject(error) : resolve())); }
});
it("requires retained profile authority to clear gateway bindings and connection audiences", async () => {
const f = await fixture();
const profile = await f.request("POST", `/companies/${f.companyId}/tools/profiles`, { profileKey: "bound-profile", name: "Bound profile", entries: [] });
expect(profile, JSON.stringify(profile)).toMatchObject({ status: 201 });
const gateway = await f.request("POST", `/companies/${f.companyId}/tools/gateways`, { name: "Bound gateway", slug: "bound", profileId: profile.body.id, agentId: f.agentId });
expect(gateway, JSON.stringify(gateway)).toMatchObject({ status: 201, body: { agentId: f.agentId } });
await server.db.delete(principalPermissionGrants).where(and(eq(principalPermissionGrants.principalId, f.agentId), eq(principalPermissionGrants.permissionKey, "tools:manage_profiles")));
expect(await f.request("PATCH", `/tool-gateway/gateways/${gateway.body.id}`, { companyId: f.companyId, agentId: null })).toMatchObject({ status: 403 });
expect(await f.request("GET", `/companies/${f.companyId}/tools/gateways`)).toMatchObject({ status: 200, body: { gateways: expect.arrayContaining([expect.objectContaining({ id: gateway.body.id, agentId: f.agentId })]) } });
for (const audience of [{ allAgents: false }, { agentIds: [] }]) {
expect(await f.request("POST", `/companies/${f.companyId}/ai-connections`, {
provider: "openai", method: "api_key", name: "No audience authority", apiKey: "unused-fixture-secret", ...audience,
})).toMatchObject({ status: 403 });
}
});
it("requires access to saved and replacement gateway profiles and project bindings", async () => {
const f = await fixture();
const saved = await f.request("POST", `/companies/${f.companyId}/tools/profiles`, { profileKey: "saved-scope", name: "Saved scope", entries: [] });
const replacement = await f.request("POST", `/companies/${f.companyId}/tools/profiles`, { profileKey: "replacement-scope", name: "Replacement scope", entries: [] });
expect(saved.status).toBe(201);
expect(replacement.status).toBe(201);
expect(await f.request("POST", `/companies/${f.companyId}/tools/profiles/${saved.body.id}/bind`, { targetType: "project", targetId: f.projectId })).toMatchObject({ status: 201 });
const gateway = await f.request("POST", `/companies/${f.companyId}/tools/gateways`, { name: "Scoped gateway", slug: "scoped", profileId: saved.body.id });
expect(gateway.status).toBe(201);
await server.db.update(principalPermissionGrants).set({ scope: { profileIds: [replacement.body.id] } }).where(and(eq(principalPermissionGrants.principalId, f.agentId), eq(principalPermissionGrants.permissionKey, "tools:manage_connections")));
expect(await f.request("PATCH", `/tool-gateway/gateways/${gateway.body.id}`, { companyId: f.companyId, profileId: replacement.body.id })).toMatchObject({ status: 403 });
// A grant covering both sides permits the same requested change.
await server.db.update(principalPermissionGrants).set({ scope: { profileIds: [saved.body.id, replacement.body.id] } }).where(and(eq(principalPermissionGrants.principalId, f.agentId), eq(principalPermissionGrants.permissionKey, "tools:manage_connections")));
expect(await f.request("PATCH", `/tool-gateway/gateways/${gateway.body.id}`, { companyId: f.companyId, profileId: replacement.body.id })).toMatchObject({ status: 200, body: { profileId: replacement.body.id } });
expect(await f.request("PATCH", `/tool-gateway/gateways/${gateway.body.id}`, { companyId: f.companyId, projectId: f.projectId })).toMatchObject({ status: 200 });
const nextProjectId = randomUUID();
await server.db.insert(projects).values({ id: nextProjectId, companyId: f.companyId, name: "Replacement project" });
await server.db.update(principalPermissionGrants).set({ scope: { projectIds: [f.projectId] } }).where(and(eq(principalPermissionGrants.principalId, f.agentId), eq(principalPermissionGrants.permissionKey, "tools:manage_profiles")));
expect(await f.request("PATCH", `/tool-gateway/gateways/${gateway.body.id}`, { companyId: f.companyId, projectId: nextProjectId })).toMatchObject({ status: 403 });
expect(await f.request("PATCH", `/tool-gateway/gateways/${gateway.body.id}`, { companyId: f.companyId, projectId: null })).toMatchObject({ status: 403 });
await server.db.update(principalPermissionGrants).set({ scope: { projectIds: [f.projectId, nextProjectId] } }).where(and(eq(principalPermissionGrants.principalId, f.agentId), eq(principalPermissionGrants.permissionKey, "tools:manage_profiles")));
expect(await f.request("PATCH", `/tool-gateway/gateways/${gateway.body.id}`, { companyId: f.companyId, projectId: nextProjectId })).toMatchObject({ status: 200, body: { projectId: nextProjectId } });
await server.db.update(principalPermissionGrants).set({ scope: null }).where(and(eq(principalPermissionGrants.principalId, f.agentId), eq(principalPermissionGrants.permissionKey, "tools:manage_profiles")));
expect(await f.request("PATCH", `/tool-gateway/gateways/${gateway.body.id}`, { companyId: f.companyId, projectId: null })).toMatchObject({ status: 200, body: { projectId: null } });
});
it("saves an AI account with fixture verification under the user's ownership and agent attribution", async () => {
const f = await fixture();
provider = async url => {
if (url === "https://api.openai.com/v1/models") return Response.json({ data: [] });
throw new Error("Unexpected AI fixture request");
};
registerServerAdapter({ ...getServerAdapter("paperclip_runner"), testEnvironment: async () => ({ adapterType: "paperclip_runner", status: "pass", checks: [], testedAt: new Date().toISOString() }) });
try {
const account = await f.request("POST", `/companies/${f.companyId}/ai-connections`, { provider: "openai", method: "api_key", name: "Fixture OpenAI", ownership: "personal", apiKey: "fixture-secret-do-not-return", agentIds: [f.agentId] });
expect(account, JSON.stringify(account)).toMatchObject({ status: 201 });
expect(JSON.stringify(account)).not.toContain("fixture-secret-do-not-return");
const list = await f.request("GET", `/companies/${f.companyId}/ai-connections`);
expect(list).toMatchObject({ status: 200, body: { currentUserId: f.userId } });
const [row] = await server.db.select().from(toolConnections).where(eq(toolConnections.id, account.body.connectionId));
expect(row.createdByUserId).toBe(f.userId);
const binding = { mode: "delegated", provider: "openai", method: "api_key", ...account.body };
const bound = await f.request("PATCH", `/agents/${f.agentId}`, { runtimeConfig: { heartbeat: { enabled: false, wakeOnDemand: false }, aiConnection: binding } });
expect(bound, JSON.stringify(bound)).toMatchObject({ status: 200 });
await instanceSettingsService(server.db).updateExperimental({ enableAiConnectionRouters: true });
const pluginKey = `fixture.router.${f.agentId}`;
await server.db.insert(plugins).values({ pluginKey, packageName: pluginKey, version: "0.1.0", status: "ready", manifestJson: {
id: pluginKey, apiVersion: 1, version: "0.1.0", displayName: "Fixture router", description: "Fixture", author: "Tests",
categories: ["connector"], capabilities: ["ai.connections.route"], aiConnectionRouter: { name: "Fixture pool", description: "Use saved accounts" }, entrypoints: { worker: "worker.js" },
} });
const config = { name: "Fixture pool", enabled: true, mode: "round_robin", thresholdPercent: 90,
members: [{ id: randomUUID(), binding, profile: { provider: "codex", model: "gpt-5.6-sol", effort: "low" } }] };
const pool = await f.request("POST", `/companies/${f.companyId}/ai-connection-pools`, { pluginKey, config });
expect(pool, JSON.stringify(pool)).toMatchObject({ status: 200, body: { revision: 1 } });
const changed = await f.request("POST", `/companies/${f.companyId}/ai-connection-pools`, { pluginKey, id: pool.body.id, expectedRevision: 1, config: { ...config, name: "Changed pool" } });
expect(changed, JSON.stringify(changed)).toMatchObject({ status: 200, body: { revision: 2 } });
expect(await f.request("DELETE", `/companies/${f.companyId}/ai-connection-pools/${pool.body.id}`, { expectedRevision: 1 })).toMatchObject({ status: 409 });
expect(await f.request("DELETE", `/companies/${f.companyId}/ai-connection-pools/${pool.body.id}`, { expectedRevision: 2 })).toMatchObject({ status: 200 });
await server.db.delete(principalPermissionGrants).where(and(eq(principalPermissionGrants.principalId, f.agentId), eq(principalPermissionGrants.permissionKey, "tools:manage_profiles")));
expect(await f.request("POST", `/companies/${f.companyId}/ai-connections`, { provider: "openai", method: "api_key", name: "No sharing authority", ownership: "personal", apiKey: "fixture-secret", agentIds: [f.agentId] })).toMatchObject({ status: 403 });
} finally { provider = null; unregisterServerAdapter("paperclip_runner"); }
});
it("reads only the authorized Anthropic account identity through actual agent authentication", async () => {
const f = await fixture();
const account = await aiConnectionService(server.db).save(f.companyId, f.userId, {
provider: "anthropic", method: "subscription", ownership: "personal", name: "Account identity fixture",
loginSessionId: "fixture", agentIds: [f.agentId], allAgents: false,
}, "selected-identity-credential");
let calls = 0;
provider = async (url, init) => {
calls++;
expect(url).toBe("https://api.anthropic.com/api/oauth/profile");
expect(new Headers(init?.headers).get("authorization")).toBe("Bearer selected-identity-credential");
return Response.json({ account: { uuid: "private-seat", email: "avery@example.test" }, organization: { uuid: "private-org", name: "Example team" } });
};
const path = `/companies/${f.companyId}/ai-connections/${account.connectionId}/identity?grantId=${account.grantId}`;
try {
for (const token of [f.key.token, f.jwt]) {
const response = await f.request("GET", path, undefined, token);
expect(response).toMatchObject({ status: 200, body: { ...account, status: "available", accountLabel: "avery@example.test", organizationLabel: "Example team" } });
for (const secret of ["selected-identity-credential", "private-seat", "private-org"]) expect(JSON.stringify(response)).not.toContain(secret);
}
expect(calls).toBe(2);
expect((await f.request("GET", path.replace(f.companyId, f.foreignCompanyId))).status).toBe(403);
expect((await f.request("GET", path.replace(account.grantId, randomUUID()))).status).toBe(404);
expect(calls).toBe(2);
await server.db.update(connectionGrants).set({ status: "revoked" }).where(eq(connectionGrants.id, account.grantId));
expect(await f.request("GET", path)).toMatchObject({ status: 200, body: { status: "unavailable", errorCode: "connection_unavailable", accountLabel: null } });
expect(calls).toBe(2);
} finally { provider = null; }
});
it("filters AI account and pool listings by the agent's resolved connection scope", async () => {
const f = await fixture();
provider = async url => {
if (url === "https://api.openai.com/v1/models") return Response.json({ data: [] });
throw new Error("Unexpected AI fixture request");
};
try {
const accounts: { connectionId: string; grantId: string }[] = [];
for (const name of ["Allowed account", "Outside agent scope"]) {
const account = await f.request("POST", `/companies/${f.companyId}/ai-connections`, { provider: "openai", method: "api_key", name, ownership: "personal", apiKey: `fixture-${name}`, agentIds: [] });
expect(account, JSON.stringify(account)).toMatchObject({ status: 201 });
accounts.push(account.body);
}
await instanceSettingsService(server.db).updateExperimental({ enableAiConnectionRouters: true });
const pluginKey = `fixture.scope.${f.agentId}`;
await server.db.insert(plugins).values({ pluginKey, packageName: pluginKey, version: "0.1.0", status: "ready", manifestJson: {
id: pluginKey, apiVersion: 1, version: "0.1.0", displayName: "Fixture router", description: "Fixture", author: "Tests",
categories: ["connector"], capabilities: ["ai.connections.route"], aiConnectionRouter: { name: "Fixture pool", description: "Use saved accounts" }, entrypoints: { worker: "worker.js" },
} });
const savedPools: { id: string }[] = [];
for (const account of accounts) {
const pool = await f.request("POST", `/companies/${f.companyId}/ai-connection-pools`, { pluginKey, config: { name: `Pool ${account.connectionId}`, enabled: true, mode: "round_robin", thresholdPercent: 90,
members: [{ id: randomUUID(), binding: { mode: "delegated", provider: "openai", method: "api_key", ...account }, profile: { provider: "codex", model: "gpt-5.6-sol", effort: "low" } }] } });
expect(pool, JSON.stringify(pool)).toMatchObject({ status: 200 });
savedPools.push(pool.body);
}
const path = `/companies/${f.companyId}/ai-connections?connectionId=${accounts[0].connectionId}`;
expect((await f.request("GET", path)).body.connections).toHaveLength(2);
expect((await f.request("GET", path)).body.pools).toHaveLength(2);
for (const connectionIds of [[accounts[0].connectionId], [accounts[0].connectionId, ...savedPools.map(pool => pool.id)]]) {
await server.db.update(principalPermissionGrants).set({ scope: { connectionIds } }).where(and(eq(principalPermissionGrants.principalId, f.agentId), eq(principalPermissionGrants.permissionKey, "tools:manage_connections")));
for (const token of [f.key.token, f.jwt]) {
const listed = await f.request("GET", path, undefined, token);
expect(listed, JSON.stringify(listed)).toMatchObject({ status: 200 });
expect(listed.body.connections.map((account: { id: string }) => account.id)).toEqual([accounts[0].connectionId]);
expect(listed.body.pools.map((pool: { id: string }) => pool.id)).toEqual(connectionIds.length === 1 ? [] : [savedPools[0].id]);
}
}
} finally { provider = null; }
});
it("reuses a saved chat authentication request and resumes only while its authority remains valid", async () => {
const f = await fixture();
const created = await f.request("POST", `/companies/${f.companyId}/chat-endpoints`, { provider: "github", assignedAgentId: f.agentId });
expect(created.status).toBe(201);
const path = `/chat-endpoints/${created.body.id}/github/identity/start`;
const first = await f.request("POST", path, {}, f.jwt);
expect(first, JSON.stringify(first)).toMatchObject({ status: 200, body: { interactionId: expect.any(String) } });
const retry = await f.request("POST", path, {}, f.jwt);
expect(retry.body.interactionId).toBe(first.body.interactionId);
const [intent] = await server.db.select().from(issueThreadInteractions).where(eq(issueThreadInteractions.id, first.body.interactionId));
const [task] = await server.db.select().from(issues).where(eq(issues.id, f.issueId));
expect(intent).toMatchObject({ addresseeUserId: f.userId, payload: { savedSetup: { endpointId: created.body.id, connectionId: created.body.connectionId } } });
expect(await findSatisfiedToolConnection(server.db, task, intent)).toBeNull();
// Healthy App credentials alone do not prove the addressee's identity.
await server.db.update(heartbeatRuns).set({ status: "succeeded" }).where(eq(heartbeatRuns.id, f.runId));
await server.db.update(chatEndpoints).set({ status: "active", providerAccountId: "identity-fixture" }).where(eq(chatEndpoints.id, created.body.id));
await server.db.update(toolConnections).set({ status: "active", enabled: true, healthStatus: "healthy" }).where(eq(toolConnections.id, created.body.connectionId));
expect(await findSatisfiedToolConnection(server.db, task, intent)).toBeNull();
await satisfiedConnectionIntentService(server.db).sweepPending();
expect((await server.db.select().from(issueThreadInteractions).where(eq(issueThreadInteractions.id, intent.id)))[0].status).toBe("pending");
expect(await server.db.select().from(connectionIntentDeliveries).where(eq(connectionIntentDeliveries.interactionId, intent.id))).toEqual([]);
const [principal] = await server.db.insert(chatExternalPrincipals).values({ companyId: f.companyId, provider: "github", providerAccountId: "identity-fixture", externalId: "1357" }).returning();
const [link] = await server.db.insert(chatIdentityLinks).values({ companyId: f.companyId, endpointId: created.body.id, principalId: principal.id, paperclipUserId: f.userId, status: "pending" }).returning();
for (const status of ["pending", "revoked", "expired"] as const) {
await server.db.update(chatIdentityLinks).set({ status }).where(eq(chatIdentityLinks.id, link.id));
expect(await findSatisfiedToolConnection(server.db, task, intent), status).toBeNull();
}
await server.db.update(chatIdentityLinks).set({ status: "linked", confirmedAt: new Date() }).where(eq(chatIdentityLinks.id, link.id));
await server.db.update(chatIdentityLinks).set({ paperclipUserId: randomUUID() }).where(eq(chatIdentityLinks.id, link.id));
expect(await findSatisfiedToolConnection(server.db, task, intent)).toBeNull();
await server.db.update(chatIdentityLinks).set({ paperclipUserId: f.userId }).where(eq(chatIdentityLinks.id, link.id));
await server.db.update(chatExternalPrincipals).set({ providerAccountId: "different-installation" }).where(eq(chatExternalPrincipals.id, principal.id));
expect(await findSatisfiedToolConnection(server.db, task, intent)).toBeNull();
await server.db.update(chatExternalPrincipals).set({ providerAccountId: "identity-fixture" }).where(eq(chatExternalPrincipals.id, principal.id));
expect((await findSatisfiedToolConnection(server.db, task, intent))?.id).toBe(created.body.connectionId);
expect(await findSatisfiedToolConnection(server.db, { ...task, status: "cancelled" }, intent)).toBeNull();
await accessService(server.db).ensureMembership(f.companyId, "user", f.userId, "operator", "suspended");
expect(await findSatisfiedToolConnection(server.db, task, intent)).toBeNull();
await accessService(server.db).ensureMembership(f.companyId, "user", f.userId, "operator", "active");
await satisfiedConnectionIntentService(server.db).sweepPending();
const [delivery] = await server.db.select().from(connectionIntentDeliveries).where(eq(connectionIntentDeliveries.interactionId, intent.id));
expect(delivery).toBeDefined();
});
it("retains a restricted user's personal self-service without company management or spoofed ownership", async () => {
const f = await fixture();
await server.db.delete(principalPermissionGrants).where(and(eq(principalPermissionGrants.principalType, "user"), eq(principalPermissionGrants.principalId, f.userId)));
provider = async url => {
if (url === "https://api.openai.com/v1/models") return Response.json({ data: [] });
throw new Error("Unexpected AI fixture request");
};
const payload = { provider: "openai", method: "api_key", name: "Personal", ownership: "personal", apiKey: "personal-fixture", agentIds: [], allAgents: false };
try {
expect(await f.request("POST", `/companies/${f.companyId}/ai-connections`, { ...payload, ownership: "shared" })).toMatchObject({ status: 403 });
expect(await f.request("POST", `/companies/${f.companyId}/ai-connections`, { ...payload, responsibleUserId: randomUUID() })).toMatchObject({ status: 403 });
expect(await f.request("POST", `/companies/${f.companyId}/ai-connections`, payload)).toMatchObject({ status: 201 });
} finally { provider = null; }
});
it("rejects foreign personal metadata, missing responsible users and task-bridge keys", async () => {
const f = await fixture();
const created = await f.request("POST", `/companies/${f.companyId}/tools/connections`, { name: "Foreign personal fixture", transport: "mcp_remote", authKind: "none", transportConfig: { url: "https://1.1.1.1/mcp" } });
expect(created.status).toBe(201);
await server.db.update(toolConnections).set({ credentialPolicy: "per_user", createdByUserId: randomUUID() }).where(eq(toolConnections.id, created.body.id));
expect(await f.request("GET", `/tool-connections/${created.body.id}`)).toMatchObject({ status: 403 });
const listed = await f.request("GET", `/companies/${f.companyId}/tools/connections`);
expect(JSON.stringify(listed.body)).not.toContain("Foreign personal fixture");
const [policy] = await server.db.insert(toolPolicies).values({ companyId: f.companyId, name: "Private account rule", policyType: "block", selectors: { connectionIds: [created.body.id] } }).returning();
expect(JSON.stringify((await f.request("GET", `/companies/${f.companyId}/tools/policies`)).body)).not.toContain(policy.id);
expect(await f.request("POST", `/companies/${f.companyId}/tools/policies`, { name: "Cannot edit foreign access", policyType: "allow", selectors: { agentId: f.agentId, connectionIds: [created.body.id] } })).toMatchObject({ status: 403 });
await server.db.update(agentApiKeys).set({ responsibleUserId: null }).where(eq(agentApiKeys.id, f.key.id));
expect((await f.request("GET", `/companies/${f.companyId}/tools/gallery`)).status).toBeGreaterThanOrEqual(400);
const g = await fixture();
await server.db.update(agentApiKeys).set({ scopeConfig: { kind: "task_bridge", parentIssueId: g.issueId } }).where(eq(agentApiKeys.id, g.key.id));
expect((await g.request("GET", `/companies/${g.companyId}/tools/gallery`)).status).toBeGreaterThanOrEqual(400);
});
it("revalidates pending credential promotion after yielding without reviving cancelled work", async () => {
const f = await fixture();
const context = await connectionManagementService(server.db).authorize({ type: "agent", companyId: f.companyId,
agentId: f.agentId, runId: f.runId, onBehalfOfUserId: f.userId }, "connections:configure",
{ type: "connection_management", companyId: f.companyId, purpose: "ai", operation: "login" });
const continuation = continuingConnectionManagement(context)!;
await server.db.update(heartbeatRuns).set({ status: "succeeded" }).where(eq(heartbeatRuns.id, f.runId));
await expect(context.reauthorize()).rejects.toThrow();
await expect(continuation.reauthorize()).resolves.toBeUndefined();
await server.db.update(issues).set({ status: "cancelled" }).where(eq(issues.id, f.issueId));
await expect(continuation.reauthorize()).rejects.toThrow();
});
it("denies revoked agent grants and user authority, including shadow mode, without backfilling", async () => {
const f = await fixture();
await server.db.delete(principalPermissionGrants).where(and(eq(principalPermissionGrants.principalId, f.agentId), eq(principalPermissionGrants.permissionKey, "tools:manage_connections")));
expect(await f.request("GET", `/companies/${f.companyId}/tools/gallery`)).toMatchObject({ status: 403 });
const g = await fixture();
await accessService(server.db).ensureMembership(g.companyId, "user", g.userId, "operator", "suspended");
expect(await g.request("GET", `/companies/${g.companyId}/tools/gallery`)).toMatchObject({ status: 403 });
});
it("enforces grant scopes, company isolation, effective trust, and Ask/Plan work modes", async () => {
const f = await fixture();
expect(await f.request("GET", `/companies/${f.foreignCompanyId}/tools/gallery`)).toMatchObject({ status: 403 });
const owned = await f.request("POST", `/companies/${f.companyId}/tools/connections`, { name: "Scoped fixture", transport: "mcp_remote", authKind: "none", transportConfig: { url: "https://1.1.1.1/mcp" } });
expect(owned.status).toBe(201);
await server.db.update(principalPermissionGrants).set({ scope: { connectionIds: [owned.body.id], purposes: ["tool"], operations: ["GET /tool-connections/:connectionId"] } }).where(and(eq(principalPermissionGrants.principalId, f.agentId), eq(principalPermissionGrants.permissionKey, "tools:manage_connections")));
expect(await f.request("GET", `/tool-connections/${owned.body.id}`)).toMatchObject({ status: 200 });
expect(await f.request("PATCH", `/tool-connections/${owned.body.id}`, { name: "Beyond operation scope" })).toMatchObject({ status: 403 });
await server.db.update(principalPermissionGrants).set({ scope: { provider: "slack" } }).where(and(eq(principalPermissionGrants.principalId, f.agentId), eq(principalPermissionGrants.permissionKey, "tools:manage_connections")));
expect(await f.request("POST", `/companies/${f.companyId}/chat-endpoints`, { provider: "github", assignedAgentId: f.agentId })).toMatchObject({ status: 403 });
const g = await fixture();
for (const workMode of ["ask", "planning", "skill_test"]) {
await server.db.update(issues).set({ workMode }).where(eq(issues.id, g.issueId));
expect(await g.request("POST", `/companies/${g.companyId}/tools/profiles`, { name: "Forbidden" }, g.jwt)).toMatchObject({ status: 403 });
}
await server.db.update(issues).set({ workMode: "standard" }).where(eq(issues.id, g.issueId));
await server.db.update(projects).set({ executionWorkspacePolicy: { authorizationPolicy: { trustPreset: "low_trust_review", lowTrustBoundary: { projectIds: [g.projectId] } } } }).where(eq(projects.id, g.projectId));
expect(await g.request("POST", `/companies/${g.companyId}/tools/profiles`, { name: "Forbidden trust" }, g.jwt)).toMatchObject({ status: 403 });
await server.db.update(agents).set({ permissions: { trustPreset: "low_trust_review", authorizationPolicy: { trustBoundary: { mode: "low_trust_review", projectIds: [g.projectId], rootIssueId: g.issueId } } } }).where(eq(agents.id, g.agentId));
expect(await g.request("POST", `/companies/${g.companyId}/tools/profiles`, { name: "Forbidden agent trust" })).toMatchObject({ status: 403 });
});
});