merge: reconcile published native prerequisite and current master

Preserve canonical startup ownership, scoped evidence, current Storybook IDs, and the Codex form controls while carrying Claude effort and immutable source bindings.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-10-12 20:40:11 -05:00
commit 48ffca9438
415 files changed
+22501 -15340

No files matched your search

@@ -261,6 +261,17 @@ function makeAgent(adapterType: string) {
};
}
function skillSnapshot(adapterType = "claude_local", mode = "ephemeral") {
return {
adapterType: adapterType,
supported: true,
mode: mode,
desiredSkills: ["paperclipai/paperclip/paperclip"],
entries: [],
warnings: [],
};
}
describe("agent skill routes", () => {
beforeEach(async () => {
runnerTarget.platform = "linux";
@@ -320,22 +331,8 @@ describe("agent skill routes", () => {
unresolved: [],
}),
);
mockAdapter.listSkills.mockResolvedValue({
adapterType: "claude_local",
supported: true,
mode: "ephemeral",
desiredSkills: ["paperclipai/paperclip/paperclip"],
entries: [],
warnings: [],
});
mockAdapter.syncSkills.mockResolvedValue({
adapterType: "claude_local",
supported: true,
mode: "ephemeral",
desiredSkills: ["paperclipai/paperclip/paperclip"],
entries: [],
warnings: [],
});
mockAdapter.listSkills.mockResolvedValue(skillSnapshot());
mockAdapter.syncSkills.mockResolvedValue(skillSnapshot());
mockAgentService.update.mockImplementation(async (_id: string, patch: Record<string, unknown>) => {
const previousAgent = persistedAgent ?? makeAgent("claude_local");
persistedAgent = {
@@ -478,61 +475,11 @@ describe("agent skill routes", () => {
);
});
it("threads a non-undefined actor secret context into resolveAdapterConfigForRuntime on both skills routes (audit fidelity, skipUserSecrets preserved)", async () => {
const expectedContext = {
consumerType: "agent",
consumerId: "11111111-1111-4111-8111-111111111111",
actorType: "user",
actorId: "local-board",
actorSource: "local_implicit",
responsibleUserId: "local-board",
};
// GET /agents/:id/skills
mockAgentService.getById.mockResolvedValue(makeAgent("claude_local"));
const listRes = await requestApp(
await createApp(),
(baseUrl) => request(baseUrl)
.get("/api/agents/11111111-1111-4111-8111-111111111111/skills?companyId=company-1"),
);
expect(listRes.status, JSON.stringify(listRes.body)).toBe(200);
const listCall = mockSecretService.resolveAdapterConfigForRuntime.mock.calls.at(-1);
expect(listCall?.[2]).toBeDefined();
expect(listCall?.[2]).toEqual(expectedContext);
expect(listCall?.[3]).toEqual({ adapterType: "claude_local", skipUserSecrets: true });
// POST /agents/:id/skills/sync
mockAdapter.syncSkills.mockResolvedValue({
adapterType: "claude_local",
supported: true,
mode: "ephemeral",
desiredSkills: ["paperclipai/paperclip/paperclip"],
entries: [],
warnings: [],
});
const syncRes = await requestApp(
await createApp(),
(baseUrl) => request(baseUrl)
.post("/api/agents/11111111-1111-4111-8111-111111111111/skills/sync?companyId=company-1")
.send({ desiredSkills: ["paperclip"], mode: "replace" }),
);
expect(syncRes.status, JSON.stringify(syncRes.body)).toBe(200);
const syncCall = mockSecretService.resolveAdapterConfigForRuntime.mock.calls.at(-1);
expect(syncCall?.[2]).toBeDefined();
expect(syncCall?.[2]).toEqual(expectedContext);
expect(syncCall?.[3]).toEqual({ adapterType: "claude_local", skipUserSecrets: true });
});
it("skips runtime materialization when listing Codex skills", async () => {
mockAgentService.getById.mockResolvedValue(makeAgent("codex_local"));
mockAdapter.listSkills.mockResolvedValue({
adapterType: "codex_local",
supported: true,
mode: "ephemeral",
desiredSkills: ["paperclipai/paperclip/paperclip"],
entries: [],
warnings: [],
});
mockAdapter.listSkills.mockResolvedValue(skillSnapshot("codex_local"));
const res = await requestApp(
await createApp(),
@@ -555,14 +502,7 @@ describe("agent skill routes", () => {
mockSecretService.resolveAdapterConfigForRuntime.mockResolvedValueOnce({
config: { agent: "claude" },
});
mockAdapter.listSkills.mockResolvedValue({
adapterType: "acpx_local",
supported: true,
mode: "ephemeral",
desiredSkills: ["paperclipai/paperclip/paperclip"],
entries: [],
warnings: [],
});
mockAdapter.listSkills.mockResolvedValue(skillSnapshot("acpx_local"));
const res = await requestApp(
await createApp(),
@@ -603,14 +543,7 @@ describe("agent skill routes", () => {
},
},
});
mockAdapter.syncSkills.mockResolvedValue({
adapterType: "acpx_local",
supported: true,
mode: "ephemeral",
desiredSkills: ["paperclipai/paperclip/paperclip"],
entries: [],
warnings: [],
});
mockAdapter.syncSkills.mockResolvedValue(skillSnapshot("acpx_local"));
const res = await requestApp(await createApp(), (baseUrl) => request(baseUrl)
.post("/api/agents/11111111-1111-4111-8111-111111111111/skills/sync?companyId=company-1")
@@ -831,14 +764,7 @@ describe("agent skill routes", () => {
it("skips runtime materialization when listing persistent skill adapters", async () => {
mockAgentService.getById.mockResolvedValue(makeAgent("cursor"));
mockAdapter.listSkills.mockResolvedValue({
adapterType: "cursor",
supported: true,
mode: "persistent",
desiredSkills: ["paperclipai/paperclip/paperclip"],
entries: [],
warnings: [],
});
mockAdapter.listSkills.mockResolvedValue(skillSnapshot("cursor", "persistent"));
const res = await requestApp(
await createApp(),
@@ -1,3 +1,7 @@
import * as activityLogModule from "../services/activity-log.js";
import { chatSlackManagerGrants, chatSlackRegistrations, toolOauthStates } from "@paperclipai/db";
import type { PaperclipCloudConnector, SealedConnectorCredentials } from "../services/paperclip-cloud-connector.js";
import { buildSlackAppManifest, SLACK_MANAGER_SCOPES } from "@paperclipai/shared";
import { registerChatActionWork, dueChatAction } from "../services/chat-action-work.js";
import { createDeliveryWorkCoordinator } from "../services/delivery-work-coordinator.js";
import { registerChatDeliveryWork } from "../services/chat-delivery-work.js";
@@ -7,14 +11,11 @@ import { aiConnectionService } from "../services/ai-connections.js";
import { fastResponseService } from "../services/fast-responses.js";
import { fastResponseReceipt } from "../services/fast-response-provider.js";
import { fastResponseRequests } from "@paperclipai/db";
import { chatSlackRegistrations, toolOauthStates } from "@paperclipai/db";
import { buildSlackAppManifest } from "@paperclipai/shared";
import { toolAccessService } from "../services/tool-access.js";
import { toolActionRequests, toolInvocations } from "@paperclipai/db";
import { GitHubPublicationLeaseLost, withGitHubPublicationLease } from "../services/chat-github-publication-lease.js";
import { secretService } from "../services/secrets.js";
import { githubChatWizardService } from "../services/chat-github-wizard.js";
import type { PaperclipCloudConnector } from "../services/paperclip-cloud-connector.js";
import { dispatchGitHubBotCloudEvent } from "../services/chat-github-cloud-ingress.js";
import { githubChatManagementService } from "../services/chat-github-management.js";
import { githubChatReviewService } from "../services/chat-github-reviews.js";
@@ -6262,7 +6263,9 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => {
const token = `xoxb-bot-token-canary-${id}`;
const scopes = buildSlackAppManifest({ app: appDetails, agentName: "Maya", webhookUrl: "https://paperclip.example/hook" }).oauth_config.scopes.bot;
const state = {
failCreate: false, failValidate: false, failInventory: false, failAuth: false, failExchange: false,
managedAccessToken: token, rejectOldBotToken: false,
userListCursor: null as string | null, userListNextCursor: "",
managedBotAppId: appId, grantedScopes: scopes, managedError: null as string | null, failCreate: false, failValidate: false, failInventory: false, failAuth: false, failExchange: false,
failManifestUpdate: false, timeoutManifestUpdate: false,
beforeManifestUpdate: null as null | (() => Promise<void>),
failIcon: false, timeoutIcon: false, failUser: false, failWelcome: false, timeoutWelcome: false,
@@ -6270,20 +6273,23 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => {
user: { id: installerId, team_id: "TAUTO", name: "installer", real_name: "Installing Person", is_bot: false, deleted: false },
beforeIcon: null as null | (() => Promise<void>),
beforeAuth: null as null | (() => Promise<void>),
beforeExchange: null as null | (() => Promise<void>),
oauth: { ok: true, app_id: appId, token_type: "bot", access_token: token, team: { id: "TAUTO" }, bot_user_id: botId, authed_user: { id: installerId }, scope: scopes.join(",") },
};
const fallback = fakeSlackFetch(botId);
const provider = vi.fn(async (input: string | URL | Request, init?: RequestInit) => {
const url = String(input);
if (url.endsWith("bots.info")) return Response.json({ ok: true, bot: { id: "BMANAGED", app_id: state.managedBotAppId, user_id: botId, deleted: false } });
if (url.endsWith("apps.managedInstall")) return Response.json(state.managedError ? { ok: false, error: state.managedError } : { ok: true, app_id: appId, team_id: "TAUTO", api_access_tokens: { bot_access_token: state.managedAccessToken } });
if (url.endsWith("apps.manifest.validate")) return Response.json(state.failValidate ? { ok: false, error: "invalid_auth", detail: configToken } : { ok: true });
if (url.endsWith("apps.manifest.create")) {
if (state.failCreate) throw new Error(`Timeout ${configToken} ${clientSecret}`);
return Response.json({ ok: true, app_id: appId, credentials: { client_id: "123.456", client_secret: clientSecret, signing_secret: signingSecret } });
return Response.json({ ok: true, app_id: appId, team_id: "TAUTO", credentials: { client_id: "123.456", client_secret: clientSecret, signing_secret: signingSecret } });
}
if (url.endsWith("apps.manifest.update")) {
await state.beforeManifestUpdate?.();
if (state.timeoutManifestUpdate) throw new Error(`Manifest timeout ${configToken} ${clientSecret}`);
return Response.json(state.failManifestUpdate ? { ok: false, error: "invalid_auth", detail: configToken } : { ok: true, app_id: appId });
return Response.json(state.failManifestUpdate ? { ok: false, error: "invalid_auth", detail: configToken } : { ok: true, app_id: appId, permissions_updated: true });
}
if (url.endsWith("apps.icon.set")) {
await state.beforeIcon?.();
@@ -6291,13 +6297,19 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => {
return Response.json(state.failIcon ? { ok: false, error: "missing_scope", detail: `${configToken} ${signingSecret}` } : { ok: true });
}
if (url.endsWith("oauth.v2.access")) {
await state.beforeExchange?.();
if (state.failExchange) throw new Error(`Timeout ${clientSecret} ${token}`);
return Response.json(state.oauth);
}
if (url.endsWith("auth.test")) {
await state.beforeAuth?.();
if (state.rejectOldBotToken && new Headers(init?.headers).get("authorization") !== `Bearer ${state.managedAccessToken}`) return Response.json({ ok: false, error: "token_revoked" });
if (state.failAuth) throw new Error(`Temporary failure ${token}`);
return Response.json({ ok: true, team_id: "TAUTO", user_id: botId, user: `maya-${id}`, team: "Test workspace" }, { headers: { "x-oauth-scopes": scopes.join(",") } });
return Response.json({ ok: true, team_id: "TAUTO", user_id: botId, bot_id: "BMANAGED", user: `maya-${id}`, team: "Test workspace" }, { headers: { "x-oauth-scopes": state.grantedScopes.join(",") } });
}
if (url.endsWith("users.list")) {
state.userListCursor = new URLSearchParams(String(init?.body)).get("cursor");
return Response.json({ ok: true, members: [state.user], response_metadata: { next_cursor: state.userListNextCursor } });
}
if (url.endsWith("users.info")) {
await state.beforeUser?.();
@@ -6323,6 +6335,408 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => {
const install = async () => new URL((await service.slackRegistration.install(endpoint.id, actor)).authorizationUrl).searchParams.get("state")!;
return { ...company, endpoint, service, sdk, app, state, provider, create, install, createInput, token, appId, botId, installerId, avatarBytes, renderAvatar };
}
async function managedFixture() {
const broker = {
getCapabilities: vi.fn(async () => ["slack.manager"]),
getSlackManagerAppId: vi.fn(async () => "AMANAGER"),
startAuthorization: vi.fn(async (_input: { returnState: string }) => ({ authorizationUrl: "https://slack.com/oauth/v2/authorize?state=broker", expiresAt: new Date(Date.now() + 600_000).toISOString() })),
claim: vi.fn(), refresh: vi.fn(), revoke: vi.fn(async () => undefined),
};
const f = await fixture({ slackManaged: { enabled: true, broker: broker as unknown as PaperclipCloudConnector } });
const credentials: SealedConnectorCredentials = { v: 1, provider: "slack", profile: "slack.manager", instanceId: "test", environment: "staging", subject: actor.userId, companyId: f.companyId,
accessToken: "xoxe.xoxp-manager-access-canary", refreshToken: "xoxe-manager-refresh-canary", tokenType: "user", accessTokenExpiresAt: new Date(Date.now() + 43_200_000).toISOString(), refreshTokenExpiresAt: null,
scopes: [...SLACK_MANAGER_SCOPES], slackManager: { appId: "AMANAGER", workspaceId: "TAUTO", workspaceName: "Test workspace", userId: f.installerId } };
broker.claim.mockResolvedValue(credentials);
broker.refresh.mockResolvedValue({ ...credentials, accessToken: "xoxe.rotated-canary", refreshToken: "xoxe.rotated-refresh-canary" });
await f.service.update(f.endpoint.id, { slackSetupMethod: "managed" }, actor.userId);
const grants = f.service.slackRegistration.managerGrants;
const authorize = async () => { await grants.authorize(f.endpoint.id, actor); return broker.startAuthorization.mock.calls.at(-1)![0].returnState; };
const authorizeState = await authorize();
await grants.complete(authorizeState, "claim-canary", null, actor);
const choices = await grants.choices(f.companyId, actor);
expect(choices.workspaces).toHaveLength(1);
const grantId = choices.workspaces[0]!.grantId;
const input = { requestId: randomUUID(), grantId };
const provision = () => f.service.slackRegistration.managed.provision(f.endpoint.id, actor, input);
return { ...f, broker, credentials, grants, authorize, authorizeState, grantId, input, provision };
}
describe("Slack test messages", () => {
const posts = (f: Awaited<ReturnType<typeof managedFixture>>) => f.provider.mock.calls.filter(([url]) => String(url).endsWith("chat.postMessage"));
it("sends to the current linked user and reuses a durable result on duplicate requests", async () => {
const f = await managedFixture(); await f.provision(); f.provider.mockClear();
const input = { requestId: randomUUID() };
const result = await request(f.app).post(`/api/chat-endpoints/${f.endpoint.id}/slack/test-message`).send(input).expect(200);
expect(result.body).toEqual({ status: "sent", slackUrl: "https://app.slack.com/client/TAUTO/DWELCOME" });
await f.service.sendSlackTestMessage(f.endpoint.id, actor, input);
expect(posts(f)).toHaveLength(1);
expect(new URLSearchParams(String(posts(f)[0]![1]!.body)).get("channel")).toBe(f.installerId);
});
it("links the actor's own manager identity without requiring a connect command", async () => {
const f = await managedFixture(); await f.provision();
await db.delete(chatIdentityLinks).where(eq(chatIdentityLinks.endpointId, f.endpoint.id));
expect(await f.service.sendSlackTestMessage(f.endpoint.id, actor, { requestId: randomUUID() })).toMatchObject({ status: "sent" });
expect(await db.select().from(chatIdentityLinks).where(eq(chatIdentityLinks.endpointId, f.endpoint.id))).toEqual([expect.objectContaining({ paperclipUserId: actor.userId, status: "linked" })]);
});
it("requires a private Slack challenge for an unknown account before linking and sending the test", async () => {
const f = await managedFixture(); await f.provision();
await db.delete(chatIdentityLinks).where(eq(chatIdentityLinks.endpointId, f.endpoint.id));
await db.update(chatSlackManagerGrants).set({ userId: "another-user" }).where(eq(chatSlackManagerGrants.id, f.grantId));
f.provider.mockClear(); const input = { requestId: randomUUID() };
expect(await f.service.sendSlackTestMessage(f.endpoint.id, actor, input)).toEqual({ status: "connection_required", accounts: [{ id: f.installerId, name: "Installing Person" }], nextCursor: null });
expect(posts(f)).toHaveLength(0);
const pending = await f.service.sendSlackTestMessage(f.endpoint.id, actor, { ...input, slackUserId: f.installerId });
expect(pending.status).toBe("connection_pending");
const links = await db.select().from(chatIdentityLinks).where(eq(chatIdentityLinks.endpointId, f.endpoint.id));
expect(links).toEqual([expect.objectContaining({ status: "pending", paperclipUserId: null })]);
const fields = new URLSearchParams(String(posts(f)[0]![1]!.body));
expect(fields.get("text")).not.toContain("This is your test message");
const blocks = JSON.parse(fields.get("blocks")!);
const invitationUrl = /<([^|]+)\|/.exec(blocks[1].text.text)![1]!;
const token = new URL(invitationUrl).searchParams.get("token")!;
expect(JSON.stringify(pending)).not.toContain(token);
expect(await f.service.sendSlackTestMessage(f.endpoint.id, actor, input)).toEqual(pending);
expect(posts(f)).toHaveLength(1);
await f.service.confirmIdentityLink(token, actor.userId);
expect(await f.service.sendSlackTestMessage(f.endpoint.id, actor, input)).toMatchObject({ status: "sent" });
expect(new URLSearchParams(String(posts(f).at(-1)![1]!.body)).get("text")).toContain("This is your test message");
});
it("loads another workspace page instead of truncating account discovery", async () => {
const f = await managedFixture(); await f.provision();
await db.delete(chatIdentityLinks).where(eq(chatIdentityLinks.endpointId, f.endpoint.id));
await db.update(chatSlackManagerGrants).set({ userId: "another-user" }).where(eq(chatSlackManagerGrants.id, f.grantId));
f.provider.mockClear(); f.state.userListNextCursor = "next-page";
const input = { requestId: randomUUID() };
expect(await f.service.sendSlackTestMessage(f.endpoint.id, actor, input)).toMatchObject({ status: "connection_required", nextCursor: "next-page" });
f.state.userListNextCursor = "";
expect(await f.service.sendSlackTestMessage(f.endpoint.id, actor, { ...input, accountCursor: "next-page" })).toMatchObject({ status: "connection_required", nextCursor: null });
expect(f.state.userListCursor).toBe("next-page");
expect(posts(f)).toHaveLength(0);
});
it("does not downgrade confirmed delivery when activity logging fails", async () => {
const f = await managedFixture(); await f.provision(); f.provider.mockClear();
const original = activityLogModule.logActivity;
const audit = vi.spyOn(activityLogModule, "logActivity").mockImplementation(async (database, entry) => {
if (entry.action === "chat_slack.test_message_sent") throw new Error("Synthetic activity failure");
return original(database, entry);
});
onTestFinished(() => audit.mockRestore());
const input = { requestId: randomUUID() };
expect(await f.service.sendSlackTestMessage(f.endpoint.id, actor, input)).toMatchObject({ status: "sent" });
expect(await f.service.sendSlackTestMessage(f.endpoint.id, actor, input)).toMatchObject({ status: "sent" });
expect(posts(f)).toHaveLength(1);
});
it("does not restore revoked access or take another user's identity", async () => {
const f = await managedFixture(); await f.provision(); f.provider.mockClear();
await db.update(chatIdentityLinks).set({ status: "revoked" }).where(eq(chatIdentityLinks.endpointId, f.endpoint.id));
await expect(f.service.sendSlackTestMessage(f.endpoint.id, actor, { requestId: randomUUID() })).rejects.toThrow("existing link");
await db.update(chatIdentityLinks).set({ status: "linked", paperclipUserId: "another-user" }).where(eq(chatIdentityLinks.endpointId, f.endpoint.id));
await expect(f.service.sendSlackTestMessage(f.endpoint.id, actor, { requestId: randomUUID(), slackUserId: f.installerId })).rejects.toThrow("existing link");
expect(posts(f)).toHaveLength(0);
});
it("rejects wrong-workspace users, disabled DMs, and membership revocation", async () => {
const f = await managedFixture(); await f.provision(); f.provider.mockClear();
f.state.user.team_id = "TOTHER";
await expect(f.service.sendSlackTestMessage(f.endpoint.id, actor, { requestId: randomUUID() })).rejects.toThrow("personal Slack account");
f.state.user.team_id = "TAUTO";
await db.update(chatEndpoints).set({ allowDirectMessages: false }).where(eq(chatEndpoints.id, f.endpoint.id));
await expect(f.service.sendSlackTestMessage(f.endpoint.id, actor, { requestId: randomUUID() })).rejects.toThrow("Enable direct messages");
await db.update(chatEndpoints).set({ allowDirectMessages: true }).where(eq(chatEndpoints.id, f.endpoint.id));
await db.update(companyMemberships).set({ status: "inactive" }).where(and(eq(companyMemberships.companyId, f.companyId), eq(companyMemberships.principalId, actor.userId)));
await expect(f.service.sendSlackTestMessage(f.endpoint.id, actor, { requestId: randomUUID() })).rejects.toThrow("active Paperclip account");
expect(posts(f)).toHaveLength(0);
});
it("redacts provider failures and never retries an ambiguous send", async () => {
const f = await managedFixture(); await f.provision(); f.provider.mockClear(); f.state.timeoutWelcome = true;
const input = { requestId: randomUUID() };
const result = await request(f.app).post(`/api/chat-endpoints/${f.endpoint.id}/slack/test-message`).send(input).expect(422);
expect(JSON.stringify(result.body)).not.toContain("canary");
await expect(f.service.sendSlackTestMessage(f.endpoint.id, actor, input)).rejects.toThrow("may already have been sent");
expect(posts(f)).toHaveLength(1);
const actions = await db.select().from(chatActions).where(and(eq(chatActions.endpointId, f.endpoint.id), eq(chatActions.kind, "slack_test_message")));
expect(JSON.stringify(actions)).not.toContain("canary");
expect(actions[0]!.status).toBe("uncertain");
});
});
describe("managed Slack", () => {
it("keeps open-source setup independent of the broker", async () => {
const f = await fixture();
expect(await f.service.slackRegistration.managerGrants.choices(f.companyId, actor)).toEqual({ managedAvailable: false, defaultMethod: "automatic", workspaces: [] });
await f.create();
expect((await f.service.get(f.endpoint.id)).setup.slackSetupMethod).toBe("automatic");
await expect(f.service.update(f.endpoint.id, { slackSetupMethod: "managed" }, actor.userId)).rejects.toThrow();
});
it("keeps configured Cloud drafts managed when the broker profile disappears", async () => {
const f = await managedFixture();
f.broker.getSlackManagerAppId.mockResolvedValue(null);
expect(f.grants.defaultMethod()).toBe("managed");
expect(await f.grants.choices(f.companyId, actor)).toEqual({ managedAvailable: false, defaultMethod: "managed", workspaces: [] });
const next = await f.service.create(f.companyId, { provider: "slack", assignedAgentId: f.assignedAgentId }, actor.userId);
expect(next.setup.slackSetupMethod).toBe("managed");
await expect(f.provision()).rejects.toThrow();
});
it("creates and installs once, uploads the avatar and links only the manager grant owner", async () => {
const f = await managedFixture();
await Promise.all([f.provision(), f.provision()]);
const saved = await f.service.get(f.endpoint.id);
expect(saved.setup).toMatchObject({ slackSetupMethod: "managed", slackRegistration: { status: "configured", appId: f.appId }, slackAvatar: { status: "uploaded" }, slackAccount: { status: "linked", paperclipUserId: actor.userId, externalUserId: f.installerId } });
expect(f.provider.mock.calls.filter(([url]) => String(url).endsWith("apps.manifest.create"))).toHaveLength(1);
expect(f.provider.mock.calls.filter(([url]) => String(url).endsWith("apps.managedInstall"))).toHaveLength(1);
const activities = await db.select().from(activityLog).where(eq(activityLog.companyId, f.companyId));
for (const secret of [f.credentials.accessToken, f.credentials.refreshToken!, f.token, clientSecret, signingSecret, "claim-canary"]) expect(JSON.stringify({ saved, activities, choices: await f.grants.choices(f.companyId, actor) })).not.toContain(secret);
await expect(f.service.update(f.endpoint.id, { slackSetupMethod: "automatic" }, actor.userId)).rejects.toThrow();
});
it("reuses the same personal workspace grant for another draft without another authorization", async () => {
const f = await managedFixture();
const draft = await f.service.create(f.companyId, { provider: "slack", assignedAgentId: f.assignedAgentId, slackSetupMethod: "managed" }, actor.userId);
expect((await f.grants.choices(f.companyId, actor)).workspaces[0]!.grantId).toBe(f.grantId);
const otherActor = { ...actor, userId: "another-user", bypassPermissionCheck: true };
expect((await f.grants.choices(f.companyId, otherActor)).workspaces).toEqual([]);
await expect(f.service.slackRegistration.managed.provision(draft.id, otherActor, { ...f.input, requestId: randomUUID() })).rejects.toThrow();
expect(f.broker.startAuthorization).toHaveBeenCalledTimes(1);
});
it("installs a second bot using the same grant without new manager consent", async () => {
const f = await managedFixture(); await f.provision();
const secondBot = `${f.botId}SECOND`;
const secondApp = `${f.appId}SECOND`;
const scopes = buildSlackAppManifest({ app: appDetails, agentName: "Maya", webhookUrl: "https://paperclip.example/hook" }).oauth_config.scopes.bot;
const provider: typeof fetch = async (input, init) => {
const url = String(input);
if (url.endsWith("apps.manifest.create")) return Response.json({ ok: true, app_id: secondApp, team_id: "TAUTO", credentials: { client_id: "222.333", client_secret: clientSecret, signing_secret: signingSecret } });
if (url.endsWith("apps.managedInstall")) return Response.json({ ok: true, app_id: secondApp, team_id: "TAUTO", api_access_tokens: { bot_access_token: "xoxb-second-managed" } });
if (url.endsWith("auth.test")) return Response.json({ ok: true, team_id: "TAUTO", user_id: secondBot, bot_id: "BSECOND", team: "Test workspace" }, { headers: { "x-oauth-scopes": scopes.join(",") } });
if (url.endsWith("bots.info")) return Response.json({ ok: true, bot: { id: "BSECOND", app_id: secondApp, user_id: secondBot, deleted: false } });
return f.provider(input, init);
};
const { service } = createService(new FakeChatSdkRuntime(), provider, { renderSlackAvatar: f.renderAvatar, slackManaged: { enabled: true, broker: f.broker as unknown as PaperclipCloudConnector } });
const second = await service.create(f.companyId, { provider: "slack", assignedAgentId: f.assignedAgentId, slackSetupMethod: "managed", slackApp: appDetails }, actor.userId);
await service.slackRegistration.managed.provision(second.id, actor, { grantId: f.grantId, requestId: randomUUID() });
expect((await service.get(second.id)).setup.slackRegistration).toMatchObject({ status: "configured", appId: secondApp });
expect(f.broker.startAuthorization).toHaveBeenCalledTimes(1);
expect(f.broker.claim).toHaveBeenCalledTimes(1);
});
it("rejects missing tool scopes before activating a managed bot", async () => {
const f = await managedFixture(); f.state.grantedScopes = [];
await f.provision();
const saved = await f.service.get(f.endpoint.id);
expect(saved.providerAccountId).toBeNull();
expect(saved.setup.slackRegistration).toMatchObject({ status: "credentials_saved", errorCode: "slack_install_scopes_missing" });
});
it.each(["revoked", "attention", "active"] as const)("reinstalls the configured app on an explicit %s reconnect", async status => {
const f = await managedFixture(); await f.provision(); f.provider.mockClear();
await db.update(chatEndpoints).set({ status }).where(eq(chatEndpoints.id, f.endpoint.id));
f.state.managedAccessToken = `xoxb-reconnected-${f.botId}`; f.state.rejectOldBotToken = true;
const reconnectInput = { ...f.input, requestId: randomUUID(), reconnect: true };
await f.service.slackRegistration.managed.provision(f.endpoint.id, actor, reconnectInput);
expect(f.provider.mock.calls.filter(([url]) => String(url).endsWith("apps.managedInstall"))).toHaveLength(1);
expect(f.provider.mock.calls.filter(([url]) => String(url).endsWith("apps.manifest.create"))).toHaveLength(0);
expect((await f.service.get(f.endpoint.id)).status).not.toBe("revoked");
expect((await f.service.get(f.endpoint.id)).setup.slackRegistration).toMatchObject({ status: "configured", appId: f.appId });
await f.service.slackRegistration.managed.provision(f.endpoint.id, actor, reconnectInput);
await f.provision();
expect(f.provider.mock.calls.filter(([url]) => String(url).endsWith("apps.managedInstall"))).toHaveLength(1);
});
it("preserves configured-app reconnect intent when managed installation fails", async () => {
const f = await managedFixture(); await f.provision(); f.provider.mockClear();
await db.update(chatEndpoints).set({ status: "revoked" }).where(eq(chatEndpoints.id, f.endpoint.id));
f.state.managedError = "temporary_error";
await f.provision();
expect((await f.service.get(f.endpoint.id)).setup.slackRegistration).toMatchObject({ status: "install", appId: f.appId });
f.state.managedError = null;
await f.provision();
expect((await f.service.get(f.endpoint.id)).setup.slackRegistration?.status).toBe("configured");
expect(f.provider.mock.calls.filter(([url]) => String(url).endsWith("apps.manifest.create"))).toHaveLength(0);
});
it("reinstalls the saved app after missing bot scopes without creating another app", async () => {
const f = await managedFixture(); const scopes = f.state.grantedScopes;
f.state.grantedScopes = []; await f.provision();
f.state.grantedScopes = scopes; await f.provision();
expect((await f.service.get(f.endpoint.id)).setup.slackRegistration?.status).toBe("configured");
expect(f.provider.mock.calls.filter(([url]) => String(url).endsWith("apps.managedInstall"))).toHaveLength(2);
expect(f.provider.mock.calls.filter(([url]) => String(url).endsWith("apps.manifest.create"))).toHaveLength(1);
});
it("retains reinstall intent after a provider failure instead of retrying rejected credentials", async () => {
const f = await managedFixture(); const scopes = f.state.grantedScopes;
f.state.grantedScopes = []; await f.provision();
f.state.managedError = "internal_error"; await f.provision();
expect((await f.service.get(f.endpoint.id)).setup.slackRegistration).toMatchObject({ status: "install", errorCode: "slack_provider_failure" });
f.state.managedError = null; f.state.grantedScopes = scopes; await f.provision();
expect((await f.service.get(f.endpoint.id)).setup.slackRegistration?.status).toBe("configured");
expect(f.provider.mock.calls.filter(([url]) => String(url).endsWith("apps.managedInstall"))).toHaveLength(3);
expect(f.provider.mock.calls.filter(([url]) => String(url).endsWith("apps.manifest.create"))).toHaveLength(1);
});
it("clears a manager reconnect error after renewing the bound grant", async () => {
const f = await managedFixture(); f.state.managedError = "invalid_auth"; await f.provision();
expect((await f.service.get(f.endpoint.id)).setup.slackRegistration?.errorCode).toBe("slack_manager_reauthorize");
await f.grants.complete(await f.authorize(), "new-claim", null, actor);
expect((await f.service.get(f.endpoint.id)).setup.slackRegistration?.errorCode).toBeNull();
f.state.managedError = null; await f.provision();
expect((await f.service.get(f.endpoint.id)).setup.slackRegistration?.status).toBe("configured");
expect(f.provider.mock.calls.filter(([url]) => String(url).endsWith("apps.manifest.create"))).toHaveLength(1);
});
it("reconnects a rejected manager token without losing pending manifest configuration", async () => {
const f = await managedFixture(); f.state.failManifestUpdate = true; await f.provision();
expect((await f.service.get(f.endpoint.id)).setup.slackRegistration?.errorCode).toBe("slack_manifest_update_pending");
expect((await f.grants.choices(f.companyId, actor)).workspaces).toEqual([]);
expect((await f.grants.get(f.grantId, f.companyId, actor)).status).toBe("reauthorize");
await f.grants.complete(await f.authorize(), "new-claim", null, actor);
f.state.failManifestUpdate = false; await f.provision();
expect((await f.service.get(f.endpoint.id)).setup.slackRegistration?.status).toBe("configured");
expect((await f.service.get(f.endpoint.id)).setup.slackAvatar?.status).toBe("uploaded");
expect(f.provider.mock.calls.filter(([url]) => String(url).endsWith("apps.icon.set"))).toHaveLength(1);
expect(f.provider.mock.calls.filter(([url]) => String(url).endsWith("apps.manifest.create"))).toHaveLength(1);
});
it("does not restore a revoked grant when an in-flight refresh returns", async () => {
const f = await managedFixture();
await db.update(chatSlackManagerGrants).set({ expiresAt: new Date(0) }).where(eq(chatSlackManagerGrants.id, f.grantId));
f.broker.refresh.mockImplementation(async () => {
await f.grants.revoke(f.grantId, f.companyId, actor);
return f.credentials;
});
await expect(f.grants.token(f.grantId, f.companyId, actor)).rejects.toThrow();
expect((await f.grants.get(f.grantId, f.companyId, actor)).status).toBe("revoked");
});
it("claims concurrent manager callbacks once", async () => {
const f = await managedFixture(); const state = await f.authorize();
const results = await Promise.allSettled([1, 2].map(() => f.grants.complete(state, "claim", null, actor)));
expect(results.filter(result => result.status === "fulfilled")).toHaveLength(1);
expect(f.broker.claim).toHaveBeenCalledTimes(2);
});
it("atomically consumes manager state and rejects wrong actors and expired attempts", async () => {
const f = await managedFixture();
await expect(f.grants.complete(f.authorizeState, "replay", null, actor)).rejects.toThrow();
const state = await f.authorize();
await expect(f.grants.complete(state, "claim", null, { ...actor, userId: "another-user", bypassPermissionCheck: true })).rejects.toThrow();
await db.update(toolOauthStates).set({ expiresAt: new Date(0) }).where(eq(toolOauthStates.state, state));
await expect(f.grants.complete(state, "claim", null, actor)).rejects.toThrow();
expect(f.broker.claim).toHaveBeenCalledTimes(1);
});
it("preserves declined manager authorization as a safe recoverable outcome", async () => {
const f = await managedFixture();
await f.grants.complete(await f.authorize(), null, "access_denied", actor);
expect((await f.service.get(f.endpoint.id)).setup.slackManagerError).toBe("slack_install_declined");
});
it("resumes saved installation credentials after inventory failure without creating or reinstalling", async () => {
const f = await managedFixture(); f.state.failInventory = true;
await f.provision();
expect((await f.service.slackRegistration.registration(f.endpoint.id))?.status).toBe("credentials_saved");
f.state.failInventory = false; await f.provision();
expect((await f.service.get(f.endpoint.id)).setup.slackRegistration?.status).toBe("configured");
expect(f.provider.mock.calls.filter(([url]) => String(url).endsWith("apps.managedInstall"))).toHaveLength(1);
expect(f.provider.mock.calls.filter(([url]) => String(url).endsWith("apps.manifest.create"))).toHaveLength(1);
});
it("never recreates an uncertain app", async () => {
const f = await managedFixture(); f.state.failCreate = true;
await f.provision(); await f.provision();
expect((await f.service.get(f.endpoint.id)).setup.slackRegistration?.status).toBe("uncertain");
expect(f.provider.mock.calls.filter(([url]) => String(url).endsWith("apps.manifest.create"))).toHaveLength(1);
});
it("uses bound OAuth for approval fallback and rejects a different installer", async () => {
const f = await managedFixture(); f.state.managedError = "app_approval_request_eligible";
const result = await f.provision();
const state = new URL(result.authorization!.authorizationUrl).searchParams.get("state")!;
f.state.oauth.authed_user.id = "UOTHER";
await f.service.slackRegistration.complete(state, "code-canary", null, actor);
expect((await f.service.get(f.endpoint.id)).setup.slackRegistration?.errorCode).toBe("slack_install_identity_mismatch");
expect((await f.service.get(f.endpoint.id)).providerAccountId).toBeNull();
});
it("does not activate a bot when the manager grant is revoked during child OAuth", async () => {
const f = await managedFixture(); f.state.managedError = "app_approval_request_eligible";
const result = await f.provision();
const state = new URL(result.authorization!.authorizationUrl).searchParams.get("state")!;
f.state.beforeExchange = () => f.grants.revoke(f.grantId, f.companyId, actor);
await f.service.slackRegistration.complete(state, "code-canary", null, actor);
const saved = await f.service.get(f.endpoint.id);
expect(saved.providerAccountId).toBeNull();
expect(saved.setup.slackRegistration?.errorCode).toBe("slack_install_identity_mismatch");
});
it("uses the held transaction for grant queries without acquiring another pool connection", async () => {
const f = await managedFixture();
const transactionContext = new AsyncLocalStorage<boolean>();
const originalTransaction = db.transaction.bind(db);
const transactionSpy = vi.spyOn(db, "transaction").mockImplementation((work, ...args) =>
originalTransaction(tx => transactionContext.run(true, () => work(tx)), ...args));
const querySpies = (["select", "insert", "update", "delete", "execute"] as const).map(method => {
const original = db[method].bind(db) as (...args: unknown[]) => unknown;
return vi.spyOn(db, method).mockImplementation(((...args: unknown[]) => {
if (transactionContext.getStore()) throw new Error("Acquired outer pool connection from held transaction");
return original(...args);
}) as never);
});
try {
await f.grants.token(f.grantId, f.companyId, actor);
await db.update(chatSlackManagerGrants).set({ expiresAt: new Date(0) }).where(eq(chatSlackManagerGrants.id, f.grantId));
await f.grants.token(f.grantId, f.companyId, actor);
expect(f.broker.refresh).toHaveBeenCalledTimes(1);
await f.grants.request(f.grantId, f.companyId, actor, "apps.manifest.validate", { manifest: "{}" });
await f.grants.complete(await f.authorize(), "renewed-claim", null, actor);
expect((await f.service.get(f.endpoint.id)).setup.slackManagerError).toBeUndefined();
await f.grants.revoke(f.grantId, f.companyId, actor);
expect(f.broker.revoke).toHaveBeenCalledTimes(2);
} finally {
transactionSpy.mockRestore();
for (const spy of querySpies) spy.mockRestore();
}
});
it("serializes refreshes across endpoints and never replays an ambiguous refresh", async () => {
const f = await managedFixture();
await db.update(chatSlackManagerGrants).set({ expiresAt: new Date(0) }).where(eq(chatSlackManagerGrants.id, f.grantId));
const tokens = await Promise.all([1, 2].map(() => f.grants.token(f.grantId, f.companyId, actor)));
expect(tokens.every(result => result.token === "xoxe.rotated-canary")).toBe(true);
expect(f.broker.refresh).toHaveBeenCalledTimes(1);
await db.update(chatSlackManagerGrants).set({ expiresAt: new Date(0) }).where(eq(chatSlackManagerGrants.id, f.grantId));
f.broker.refresh.mockRejectedValue(new Error("provider echoed manager-canary"));
await expect(f.grants.token(f.grantId, f.companyId, actor)).rejects.toThrow();
await expect(f.grants.token(f.grantId, f.companyId, actor)).rejects.toThrow();
expect(f.broker.refresh).toHaveBeenCalledTimes(2);
});
it("rejects a bot token belonging to a different Slack app before activation", async () => {
const f = await managedFixture(); f.state.managedBotAppId = "ADIFFERENT";
await f.provision();
const saved = await f.service.get(f.endpoint.id);
expect(saved.providerAccountId).toBeNull();
expect(saved.setup.slackRegistration).toMatchObject({ status: "credentials_saved", errorCode: "slack_install_identity_mismatch" });
});
it("rejects manager identity changes and both callback-origin and ingress-origin drift", async () => {
const f = await managedFixture(); const state = await f.authorize();
for (const change of [{ publicBaseUrl: "https://changed.example" }, { webhookPublicBaseUrl: "https://changed.example" }]) {
const changed = createService(new FakeChatSdkRuntime(), f.provider, { ...change, slackManaged: { enabled: true, broker: f.broker as unknown as PaperclipCloudConnector } }).service;
await expect(changed.slackRegistration.managerGrants.complete(state, "claim", null, actor)).rejects.toThrow();
}
f.broker.getSlackManagerAppId.mockResolvedValue("ANEWMANAGER");
await expect(f.provision()).rejects.toThrow();
expect((await f.grants.choices(f.companyId, actor)).workspaces).toEqual([]);
});
it("preserves provider rate-limit backoff across subsequent attempts", async () => {
const f = await managedFixture(); f.state.managedError = "ratelimited";
await f.provision();
f.state.managedError = null; await f.provision();
expect(f.provider.mock.calls.filter(([url]) => String(url).endsWith("apps.managedInstall"))).toHaveLength(1);
expect((await f.service.get(f.endpoint.id)).setup.slackRegistration?.errorCode).toBe("slack_setup_rate_limited");
});
it("routes the broker claim_id callback back to the saved wizard without exposing the handoff", async () => {
const f = await managedFixture(); const state = await f.authorize();
const response = await request(f.app).get(`/api/chat-slack/managed/oauth/callback?state=${state}&claim_id=sealed-claim-canary`).set("accept", "application/json").expect(303);
expect(f.broker.claim.mock.calls.at(-1)![0].claimId).toBe("sealed-claim-canary");
expect(response.headers.location).toContain(f.endpoint.id);
expect(response.text).not.toContain("sealed-claim-canary");
expect(response.headers["cache-control"]).toBe("no-store");
});
it("does not accept another company's grant or keep authorizations after removal", async () => {
const f = await managedFixture(); const other = await seedCompany();
await expect(f.grants.token(f.grantId, other.companyId, { ...actor, bypassPermissionCheck: true })).rejects.toThrow();
const state = await f.authorize();
await f.service.configure(f.endpoint.id, { action: "remove" }, actor.userId);
await expect(f.grants.complete(state, "claim", null, actor)).rejects.toThrow();
expect((await f.grants.get(f.grantId, f.companyId, actor)).status).toBe("active");
});
it("revocation invalidates pending authorization and prevents future provisioning", async () => {
const f = await managedFixture(); const state = await f.authorize();
await f.grants.revoke(f.grantId, f.companyId, actor);
await expect(f.grants.complete(state, "claim", null, actor)).rejects.toThrow();
await expect(f.provision()).rejects.toThrow();
expect((await f.grants.choices(f.companyId, actor)).workspaces).toEqual([]);
});
});
function setupEvent(f: Awaited<ReturnType<typeof fixture>>, patch: Record<string, unknown> = {}, url = f.endpoint.setup.webhookUrl!, secret = signingSecret, timestamp?: string) {
return signedSlackWebhookRequest({ url, contentType: "application/json", signingSecret: secret, ...(timestamp ? { timestamp } : {}),
body: JSON.stringify({ type: "event_callback", api_app_id: f.appId, team_id: "TAUTO", event_id: "EvSETUP", event: {
+26 -120
View File
@@ -522,6 +522,17 @@ describe("company portability", () => {
}));
});
function exportCompanyAndAgents(portability: ReturnType<typeof companyPortabilityService>) {
return portability.exportBundle("company-1", {
include: {
company: true,
agents: true,
projects: false,
issues: false,
},
});
}
it("renders high-volume YAML blocks without overflowing the call stack", () => {
const tasks = Array.from({ length: 130_000 }, (_, index) => `issue-${index}`);
@@ -705,14 +716,7 @@ describe("company portability", () => {
};
const portability = companyPortabilityService(db as any);
const exported = await portability.exportBundle("company-1", {
include: {
company: true,
agents: true,
projects: false,
issues: false,
},
});
const exported = await exportCompanyAndAgents(portability);
const extension = asTextFile(exported.files[".paperclip.yaml"]);
expect(extension).toContain("permissionGrants:");
@@ -794,14 +798,7 @@ describe("company portability", () => {
},
]);
const exported = await portability.exportBundle("company-1", {
include: {
company: true,
agents: true,
projects: false,
issues: false,
},
});
const exported = await exportCompanyAndAgents(portability);
const serialized = JSON.stringify(exported);
expect(serialized).not.toContain("sk-inline-secret-value");
@@ -1778,14 +1775,7 @@ describe("company portability", () => {
it("reads env inputs back from .paperclip.yaml during preview import", async () => {
const portability = companyPortabilityService({} as any);
const exported = await portability.exportBundle("company-1", {
include: {
company: true,
agents: true,
projects: false,
issues: false,
},
});
const exported = await exportCompanyAndAgents(portability);
const preview = await portability.previewImport({
source: {
@@ -3321,14 +3311,7 @@ describe("company portability", () => {
},
]);
const exported = await portability.exportBundle("company-1", {
include: {
company: true,
agents: true,
projects: false,
issues: false,
},
});
const exported = await exportCompanyAndAgents(portability);
const extension = asTextFile(exported.files[".paperclip.yaml"]);
expect(extension).toContain("APIKEY:");
@@ -3365,14 +3348,7 @@ describe("company portability", () => {
reason: "Existing skill matched; renamed to paperclip-2.",
}]);
const exported = await portability.exportBundle("company-1", {
include: {
company: true,
agents: true,
projects: false,
issues: false,
},
});
const exported = await exportCompanyAndAgents(portability);
agentSvc.list.mockResolvedValue([]);
@@ -3445,14 +3421,7 @@ describe("company portability", () => {
});
const portability = companyPortabilityService({} as any, storage as any);
const exported = await portability.exportBundle("company-1", {
include: {
company: true,
agents: true,
projects: false,
issues: false,
},
});
const exported = await exportCompanyAndAgents(portability);
exported.files["images/company-logo.png"] = {
encoding: "base64",
@@ -3518,14 +3487,7 @@ describe("company portability", () => {
name: "ClaudeCoder",
});
const exported = await portability.exportBundle("company-1", {
include: {
company: true,
agents: true,
projects: false,
issues: false,
},
});
const exported = await exportCompanyAndAgents(portability);
agentSvc.list.mockResolvedValue([]);
@@ -3585,14 +3547,7 @@ describe("company portability", () => {
})));
agentSvc.list.mockClear();
const exported = await portability.exportBundle("company-1", {
include: {
company: true,
agents: true,
projects: false,
issues: false,
},
});
const exported = await exportCompanyAndAgents(portability);
agentSvc.list.mockResolvedValue([]);
@@ -3637,14 +3592,7 @@ describe("company portability", () => {
it("imports only selected files and leaves unchecked company metadata alone", async () => {
const portability = companyPortabilityService({} as any);
const exported = await portability.exportBundle("company-1", {
include: {
company: true,
agents: true,
projects: false,
issues: false,
},
});
const exported = await exportCompanyAndAgents(portability);
agentSvc.list.mockResolvedValue([]);
projectSvc.list.mockResolvedValue([]);
@@ -3735,14 +3683,7 @@ describe("company portability", () => {
name: "ClaudeCoder",
});
const exported = await portability.exportBundle("company-1", {
include: {
company: true,
agents: true,
projects: false,
issues: false,
},
});
const exported = await exportCompanyAndAgents(portability);
agentSvc.list.mockResolvedValue([]);
@@ -3818,14 +3759,7 @@ describe("company portability", () => {
adapterConfig: input.adapterConfig,
}));
const exported = await portability.exportBundle("company-1", {
include: {
company: true,
agents: true,
projects: false,
issues: false,
},
});
const exported = await exportCompanyAndAgents(portability);
agentSvc.list.mockResolvedValue([]);
@@ -5517,14 +5451,7 @@ describe("company portability", () => {
name: "ClaudeCoder",
});
const exported = await portability.exportBundle("company-1", {
include: {
company: true,
agents: true,
projects: false,
issues: false,
},
});
const exported = await exportCompanyAndAgents(portability);
const originalAgentsMarkdown = exported.files["agents/claudecoder/AGENTS.md"];
expect(typeof originalAgentsMarkdown).toBe("string");
@@ -5576,14 +5503,7 @@ describe("company portability", () => {
it("rejects dangerous adapter types on agent-safe imports", async () => {
const portability = companyPortabilityService({} as any);
const exported = await portability.exportBundle("company-1", {
include: {
company: true,
agents: true,
projects: false,
issues: false,
},
});
const exported = await exportCompanyAndAgents(portability);
agentSvc.list.mockResolvedValue([]);
@@ -5760,14 +5680,7 @@ describe("company portability", () => {
it("imports new agents as active while preserving future hire approval settings", async () => {
const portability = companyPortabilityService({} as any);
const exported = await portability.exportBundle("company-1", {
include: {
company: true,
agents: true,
projects: false,
issues: false,
},
});
const exported = await exportCompanyAndAgents(portability);
agentSvc.list.mockResolvedValue([]);
secretSvc.normalizeAdapterConfigForPersistence.mockResolvedValueOnce({
@@ -5836,14 +5749,7 @@ describe("company portability", () => {
},
})));
agentSvc.list.mockClear();
const exported = await portability.exportBundle("company-1", {
include: {
company: true,
agents: true,
projects: false,
issues: false,
},
});
const exported = await exportCompanyAndAgents(portability);
secretSvc.normalizeAdapterConfigForPersistence.mockResolvedValueOnce({
normalized: "updated",
+109 -403
View File
@@ -201,6 +201,75 @@ function registerModuleMocks() {
}));
}
function boardActor(userId = "local-board", source = "local_implicit") {
return {
type: "board",
userId: userId,
companyIds: ["company-1"],
source: source,
isInstanceAdmin: false,
};
}
function assignedAgentActor() {
return {
type: "agent",
agentId: "55555555-5555-4555-8555-555555555555",
companyId: "company-1",
runId: "run-1",
};
}
function createCatalogSkill() {
return {
id: "paperclipai:bundled:software-development:review",
key: "paperclipai/bundled/software-development/review",
kind: "bundled",
category: "software-development",
slug: "review",
name: "review",
description: "Review code",
path: "catalog/bundled/software-development/review",
entrypoint: "SKILL.md",
trustLevel: "markdown_only",
compatibility: "compatible",
defaultInstall: false,
recommendedForRoles: ["engineer"],
requires: [],
tags: ["review"],
files: [{ path: "SKILL.md", kind: "skill", sizeBytes: 8, sha256: "abc" }],
contentHash: "sha256:abc",
};
}
function createSkillTestRunDetails(overrides: Record<string, unknown> = {}) {
return {
id: "22222222-2222-4222-8222-222222222222",
companyId: "company-1",
skillId: "skill-1",
inputId: "11111111-1111-4111-8111-111111111111",
inputSnapshot: "Try the skill",
skillVersionId: "33333333-3333-4333-8333-333333333333",
agentId: "55555555-5555-4555-8555-555555555555",
agentConfigSnapshot: { adapterType: "codex_local" },
issueId: "44444444-4444-4444-8444-444444444444",
templateId: "built-in:default-test-template",
templateName: "Default test template",
templateBody: "Default {{skillName}}",
renderedTemplateBody: "Default Review",
harnessIssueDescription: "Try the skill\n\n---\n\nDefault Review",
status: "queued",
outputDocumentKey: "output",
outputSnapshot: "",
error: null,
deletedAt: null,
supersededAt: null,
harnessIssueExpiresAt: null,
harnessIssueDeletedAt: null,
...overrides,
};
}
describe("company skill mutation permissions", () => {
const routeModules = hoistModuleGraph(registerModuleMocks, async () => {
const [{ companySkillRoutes }, { errorHandler }] = await Promise.all([
@@ -399,25 +468,7 @@ describe("company skill mutation permissions", () => {
createdAt: new Date("2026-05-26T00:00:00.000Z"),
updatedAt: new Date("2026-05-26T00:00:00.000Z"),
},
catalogSkill: {
id: "paperclipai:bundled:software-development:review",
key: "paperclipai/bundled/software-development/review",
kind: "bundled",
category: "software-development",
slug: "review",
name: "review",
description: "Review code",
path: "catalog/bundled/software-development/review",
entrypoint: "SKILL.md",
trustLevel: "markdown_only",
compatibility: "compatible",
defaultInstall: false,
recommendedForRoles: ["engineer"],
requires: [],
tags: ["review"],
files: [{ path: "SKILL.md", kind: "skill", sizeBytes: 8, sha256: "abc" }],
contentHash: "sha256:abc",
},
catalogSkill: createCatalogSkill(),
warnings: [],
});
mockCompanySkillService.createLocalSkill.mockResolvedValue({
@@ -590,56 +641,14 @@ describe("company skill mutation permissions", () => {
mockCompanySkillService.listTestRuns.mockResolvedValue([]);
mockCompanySkillService.getTestRunDetail.mockResolvedValue(null);
mockCompanySkillService.createTestRun.mockResolvedValue({
id: "22222222-2222-4222-8222-222222222222",
companyId: "company-1",
skillId: "skill-1",
inputId: "11111111-1111-4111-8111-111111111111",
inputSnapshot: "Try the skill",
skillVersionId: "33333333-3333-4333-8333-333333333333",
agentId: "55555555-5555-4555-8555-555555555555",
agentConfigSnapshot: { adapterType: "codex_local" },
issueId: "44444444-4444-4444-8444-444444444444",
templateId: "built-in:default-test-template",
templateName: "Default test template",
templateBody: "Default {{skillName}}",
renderedTemplateBody: "Default Review",
harnessIssueDescription: "Try the skill\n\n---\n\nDefault Review",
status: "queued",
outputDocumentKey: "output",
outputSnapshot: "",
error: null,
deletedAt: null,
supersededAt: null,
harnessIssueExpiresAt: null,
harnessIssueDeletedAt: null,
...createSkillTestRunDetails(),
createdAt: new Date("2026-05-26T00:00:00.000Z"),
updatedAt: new Date("2026-05-26T00:00:00.000Z"),
cost: { costCents: 0, inputTokens: 0, cachedInputTokens: 0, outputTokens: 0 },
taskExpired: false,
});
mockCompanySkillService.cancelTestRun.mockResolvedValue({
id: "22222222-2222-4222-8222-222222222222",
companyId: "company-1",
skillId: "skill-1",
inputId: "11111111-1111-4111-8111-111111111111",
inputSnapshot: "Try the skill",
skillVersionId: "33333333-3333-4333-8333-333333333333",
agentId: "55555555-5555-4555-8555-555555555555",
agentConfigSnapshot: { adapterType: "codex_local" },
issueId: "44444444-4444-4444-8444-444444444444",
templateId: "built-in:default-test-template",
templateName: "Default test template",
templateBody: "Default {{skillName}}",
renderedTemplateBody: "Default Review",
harnessIssueDescription: "Try the skill\n\n---\n\nDefault Review",
status: "cancelled",
outputDocumentKey: "output",
outputSnapshot: "",
error: "Cancelled by operator",
deletedAt: null,
supersededAt: null,
harnessIssueExpiresAt: null,
harnessIssueDeletedAt: null,
...createSkillTestRunDetails({ status: "cancelled", error: "Cancelled by operator" }),
createdAt: new Date("2026-05-26T00:00:00.000Z"),
updatedAt: new Date("2026-05-26T00:01:00.000Z"),
cost: { costCents: 0, inputTokens: 0, cachedInputTokens: 0, outputTokens: 0 },
@@ -661,25 +670,7 @@ describe("company skill mutation permissions", () => {
mockHeartbeatService.wakeup.mockResolvedValue({});
mockHeartbeatService.cancelRun.mockResolvedValue({});
mockCatalogService.listCatalogSkillsOrEmpty.mockReturnValue([]);
mockCatalogService.getCatalogSkillOrThrow.mockReturnValue({
id: "paperclipai:bundled:software-development:review",
key: "paperclipai/bundled/software-development/review",
kind: "bundled",
category: "software-development",
slug: "review",
name: "review",
description: "Review code",
path: "catalog/bundled/software-development/review",
entrypoint: "SKILL.md",
trustLevel: "markdown_only",
compatibility: "compatible",
defaultInstall: false,
recommendedForRoles: ["engineer"],
requires: [],
tags: ["review"],
files: [{ path: "SKILL.md", kind: "skill", sizeBytes: 8, sha256: "abc" }],
contentHash: "sha256:abc",
});
mockCatalogService.getCatalogSkillOrThrow.mockReturnValue(createCatalogSkill());
mockCatalogService.readCatalogSkillFile.mockResolvedValue({
catalogSkillId: "paperclipai:bundled:software-development:review",
path: "SKILL.md",
@@ -709,13 +700,7 @@ describe("company skill mutation permissions", () => {
});
it("allows local board operators to mutate company skills", async () => {
const res = await request(await createApp({
type: "board",
userId: "local-board",
companyIds: ["company-1"],
source: "local_implicit",
isInstanceAdmin: false,
}))
const res = await request(await createApp(boardActor()))
.post("/api/companies/company-1/skills/import")
.send({ source: "https://github.com/vercel-labs/agent-browser" });
@@ -750,13 +735,7 @@ describe("company skill mutation permissions", () => {
}],
warnings: [],
});
const app = await createApp({
type: "board",
userId: "local-board",
companyIds: ["company-1"],
source: "local_implicit",
isInstanceAdmin: false,
});
const app = await createApp(boardActor());
const preview = await request(app)
.post("/api/companies/company-1/skills/scan-projects")
@@ -798,13 +777,7 @@ describe("company skill mutation permissions", () => {
"Actor is restricted from changing skill configuration.",
));
const res = await request(await createApp({
type: "board",
userId: "board-user",
companyIds: ["company-1"],
source: "session",
isInstanceAdmin: false,
}))
const res = await request(await createApp(boardActor("board-user", "session")))
.post("/api/companies/company-1/skills/scan-projects")
.send({ mode: "preview", workspaceIds: [workspaceId] });
@@ -814,13 +787,7 @@ describe("company skill mutation permissions", () => {
});
it("allows board users with skills:create to create, import, install, update, delete, audit, and reset company skills", async () => {
const app = await createApp({
type: "board",
userId: "board-user",
companyIds: ["company-1"],
source: "session",
isInstanceAdmin: false,
});
const app = await createApp(boardActor("board-user", "session"));
await request(app)
.post("/api/companies/company-1/skills")
@@ -867,13 +834,7 @@ describe("company skill mutation permissions", () => {
it("allows board users without skills:create when no explicit skill policy exists", async () => {
mockAccessService.decide.mockResolvedValue(denySkillChangeDecision());
const res = await request(await createApp({
type: "board",
userId: "board-user",
companyIds: ["company-1"],
source: "session",
isInstanceAdmin: false,
}))
const res = await request(await createApp(boardActor("board-user", "session")))
.post("/api/companies/company-1/skills/import")
.send({ source: "https://github.com/vercel-labs/agent-browser" });
@@ -897,12 +858,7 @@ describe("company skill mutation permissions", () => {
remediation: "Contact a company administrator to change the skill policy.",
});
const res = await request(await createApp({
type: "agent",
agentId: "55555555-5555-4555-8555-555555555555",
companyId: "company-1",
runId: "run-1",
}))
const res = await request(await createApp(assignedAgentActor()))
.post("/api/companies/company-1/skills/import")
.send({ source: "https://packages.example.com/skill.tgz" });
@@ -921,13 +877,7 @@ describe("company skill mutation permissions", () => {
it("rejects secret-bearing remote import URLs without echoing the secret", async () => {
const source = "https://github.com/acme/private-skill?token=secret#token=secret";
const res = await request(await createApp({
type: "board",
userId: "local-board",
companyIds: ["company-1"],
source: "local_implicit",
isInstanceAdmin: false,
}))
const res = await request(await createApp(boardActor()))
.post("/api/companies/company-1/skills/import")
.send({ source });
@@ -941,13 +891,7 @@ describe("company skill mutation permissions", () => {
});
it("rejects malformed remote import URLs before policy evaluation", async () => {
const res = await request(await createApp({
type: "board",
userId: "local-board",
companyIds: ["company-1"],
source: "local_implicit",
isInstanceAdmin: false,
}))
const res = await request(await createApp(boardActor()))
.post("/api/companies/company-1/skills/import")
.send({ source: "https://" });
@@ -965,12 +909,7 @@ describe("company skill mutation permissions", () => {
"Low-trust agents cannot use company-wide skill APIs.",
));
const res = await request(await createApp({
type: "agent",
agentId: "55555555-5555-4555-8555-555555555555",
companyId: "company-1",
runId: "run-1",
}))
const res = await request(await createApp(assignedAgentActor()))
.post("/api/companies/company-1/skills/import")
.send({ source: "https://github.com/vercel-labs/agent-browser" });
@@ -998,13 +937,7 @@ describe("company skill mutation permissions", () => {
};
});
const res = await request(await createApp({
type: "board",
userId: "board-user",
companyIds: ["company-1"],
source: "session",
isInstanceAdmin: false,
}))
const res = await request(await createApp(boardActor("board-user", "session")))
.post("/api/companies/company-1/skills/import")
.send({ source: "vercel-labs/agent-browser" });
@@ -1042,13 +975,7 @@ describe("company skill mutation permissions", () => {
}
));
const res = await request(await createApp({
type: "board",
userId: "board-user",
companyIds: ["company-1"],
source: "session",
isInstanceAdmin: false,
}))
const res = await request(await createApp(boardActor("board-user", "session")))
.patch("/api/companies/company-1/skills/skill-1")
.send({ name: "Updated review" });
@@ -1073,13 +1000,7 @@ describe("company skill mutation permissions", () => {
});
mockCompanySkillPolicyService.evaluate.mockResolvedValue(denySkillPolicy("skills.create"));
const res = await request(await createApp({
type: "board",
userId: "board-user",
companyIds: ["company-1"],
source: "session",
isInstanceAdmin: false,
}))
const res = await request(await createApp(boardActor("board-user", "session")))
.post("/api/companies/company-1/skills/skill-1/versions")
.send({ label: "v1" });
@@ -1110,13 +1031,7 @@ describe("company skill mutation permissions", () => {
};
});
const res = await request(await createApp({
type: "board",
userId: "board-user",
companyIds: ["company-1"],
source: "session",
isInstanceAdmin: false,
}))
const res = await request(await createApp(boardActor("board-user", "session")))
.post("/api/companies/company-1/skills/import")
.send({ source: "npx skills add Vercel-Labs/Agent-Browser --skill agent-browser -g" });
@@ -1160,13 +1075,7 @@ describe("company skill mutation permissions", () => {
};
});
const res = await request(await createApp({
type: "board",
userId: "board-user",
companyIds: ["company-1"],
source: "session",
isInstanceAdmin: false,
}))
const res = await request(await createApp(boardActor("board-user", "session")))
.post("/api/companies/company-1/skills/import")
.send({ source });
@@ -1185,34 +1094,10 @@ describe("company skill mutation permissions", () => {
it("serves catalog listing without mutating company skills", async () => {
mockCatalogService.listCatalogSkillsOrEmpty.mockReturnValue([
{
id: "paperclipai:bundled:software-development:review",
key: "paperclipai/bundled/software-development/review",
kind: "bundled",
category: "software-development",
slug: "review",
name: "review",
description: "Review code",
path: "catalog/bundled/software-development/review",
entrypoint: "SKILL.md",
trustLevel: "markdown_only",
compatibility: "compatible",
defaultInstall: false,
recommendedForRoles: ["engineer"],
requires: [],
tags: ["review"],
files: [{ path: "SKILL.md", kind: "skill", sizeBytes: 8, sha256: "abc" }],
contentHash: "sha256:abc",
},
createCatalogSkill(),
]);
const res = await request(await createApp({
type: "board",
userId: "local-board",
companyIds: ["company-1"],
source: "local_implicit",
isInstanceAdmin: false,
}))
const res = await request(await createApp(boardActor()))
.get("/api/skills/catalog?kind=bundled&q=review");
expect(res.status, JSON.stringify(res.body)).toBe(200);
@@ -1267,13 +1152,7 @@ describe("company skill mutation permissions", () => {
});
it("serves catalog detail and files by catalog reference", async () => {
const app = await createApp({
type: "board",
userId: "local-board",
companyIds: ["company-1"],
source: "local_implicit",
isInstanceAdmin: false,
});
const app = await createApp(boardActor());
const detail = await request(app)
.get("/api/skills/catalog/review");
@@ -1288,13 +1167,7 @@ describe("company skill mutation permissions", () => {
});
it("installs catalog skills with mutation permissions and logs provenance", async () => {
const res = await request(await createApp({
type: "board",
userId: "local-board",
companyIds: ["company-1"],
source: "local_implicit",
isInstanceAdmin: false,
}))
const res = await request(await createApp(boardActor()))
.post("/api/companies/company-1/skills/install-catalog")
.send({
catalogSkillId: "paperclipai:bundled:software-development:review",
@@ -1348,13 +1221,7 @@ describe("company skill mutation permissions", () => {
warnings: [],
});
const res = await request(await createApp({
type: "board",
userId: "local-board",
companyIds: ["company-1"],
source: "local_implicit",
isInstanceAdmin: false,
}))
const res = await request(await createApp(boardActor()))
.post("/api/companies/company-1/skills/import")
.send({ source: "https://github.com/vercel-labs/agent-browser" });
@@ -1486,13 +1353,7 @@ describe("company skill mutation permissions", () => {
warnings: [],
});
const res = await request(await createApp({
type: "board",
userId: "local-board",
companyIds: ["company-1"],
source: "local_implicit",
isInstanceAdmin: false,
}))
const res = await request(await createApp(boardActor()))
.post("/api/companies/company-1/skills/import")
.send({ source: "https://ghe.example.com/acme/private-skill" });
@@ -1528,13 +1389,7 @@ describe("company skill mutation permissions", () => {
warnings: [],
});
const res = await request(await createApp({
type: "board",
userId: "local-board",
companyIds: ["company-1"],
source: "local_implicit",
isInstanceAdmin: false,
}))
const res = await request(await createApp(boardActor()))
.post("/api/companies/company-1/skills/import")
.send({ source: "https://github.com/acme/private-skill" });
@@ -1553,12 +1408,7 @@ describe("company skill mutation permissions", () => {
permissions: { canCreateSkills: false },
});
const res = await request(await createApp({
type: "agent",
agentId: "55555555-5555-4555-8555-555555555555",
companyId: "company-1",
runId: "run-1",
}))
const res = await request(await createApp(assignedAgentActor()))
.post("/api/companies/company-1/skills/import")
.send({ source: "https://github.com/vercel-labs/agent-browser" });
@@ -1578,12 +1428,7 @@ describe("company skill mutation permissions", () => {
permissions: { canCreateSkills: true },
});
const res = await request(await createApp({
type: "agent",
agentId: "55555555-5555-4555-8555-555555555555",
companyId: "company-1",
runId: "run-1",
}))
const res = await request(await createApp(assignedAgentActor()))
.post("/api/companies/company-2/skills/install-catalog")
.send({ catalogSkillId: "paperclipai:bundled:software-development:review" });
@@ -1861,12 +1706,7 @@ describe("company skill mutation permissions", () => {
permissions: { canCreateSkills: false },
});
const res = await request(await createApp({
type: "agent",
agentId: "55555555-5555-4555-8555-555555555555",
companyId: "company-1",
runId: "run-1",
}))
const res = await request(await createApp(assignedAgentActor()))
.post("/api/companies/company-1/skills/import")
.send({ source: "https://github.com/vercel-labs/agent-browser" });
@@ -1906,26 +1746,7 @@ describe("company skill mutation permissions", () => {
expect(mockCompanySkillService.createLocalSkill).toHaveBeenCalled();
});
it("does not invoke the legacy consent gate under the open default", async () => {
mockAccessService.decide.mockResolvedValue(denySkillChangeDecision(
"deny_missing_consent",
"Permission skills:suggest-changes requires accepted change consent before applying this mutation.",
));
mockReflectionCoachMutationGate.assertConsented.mockRejectedValue(new Error("database unavailable"));
const res = await request(await createApp({
type: "agent",
agentId: "reflection-coach",
companyId: "company-1",
runId: "run-apply",
}))
.post("/api/companies/company-1/skills")
.send({ name: "Reflection Draft", slug: "reflection-draft", markdown: "# Draft" });
expect(res.status, JSON.stringify(res.body)).toBe(201);
expect(mockReflectionCoachMutationGate.assertConsented).not.toHaveBeenCalled();
expect(mockCompanySkillService.createLocalSkill).toHaveBeenCalled();
});
it("allows suggest-tier skill mutations without a second consent decision", async () => {
mockAccessService.decide
@@ -1940,12 +1761,7 @@ describe("company skill mutation permissions", () => {
permissions: {},
});
const res = await request(await createApp({
type: "agent",
agentId: "55555555-5555-4555-8555-555555555555",
companyId: "company-1",
runId: "run-1",
}))
const res = await request(await createApp(assignedAgentActor()))
.post("/api/companies/company-1/skills/import")
.send({ source: "https://github.com/vercel-labs/agent-browser" });
@@ -1966,12 +1782,7 @@ describe("company skill mutation permissions", () => {
permissions: {},
});
const res = await request(await createApp({
type: "agent",
agentId: "55555555-5555-4555-8555-555555555555",
companyId: "company-1",
runId: "run-1",
}))
const res = await request(await createApp(assignedAgentActor()))
.post("/api/companies/company-1/skills/import")
.send({ source: "https://github.com/vercel-labs/agent-browser" });
@@ -2016,13 +1827,7 @@ describe("company skill mutation permissions", () => {
});
it("routes skill test input CRUD through skills mutation permissions", async () => {
const app = await createApp({
type: "board",
userId: "local-board",
companyIds: ["company-1"],
source: "local_implicit",
isInstanceAdmin: false,
});
const app = await createApp(boardActor());
const created = await request(app)
.post("/api/companies/company-1/skills/skill-1/test-inputs")
@@ -2057,13 +1862,7 @@ describe("company skill mutation permissions", () => {
});
it("routes skill test run template CRUD through skills mutation permissions", async () => {
const app = await createApp({
type: "board",
userId: "local-board",
companyIds: ["company-1"],
source: "local_implicit",
isInstanceAdmin: false,
});
const app = await createApp(boardActor());
const listed = await request(app).get("/api/companies/company-1/skill-test-run-templates");
expect(listed.status, JSON.stringify(listed.body)).toBe(200);
@@ -2124,28 +1923,7 @@ describe("company skill mutation permissions", () => {
});
await deps.wakeHarnessIssue("44444444-4444-4444-8444-444444444444", "55555555-5555-4555-8555-555555555555");
return {
id: "22222222-2222-4222-8222-222222222222",
companyId: "company-1",
skillId: "skill-1",
inputId: "11111111-1111-4111-8111-111111111111",
inputSnapshot: "Try the skill",
skillVersionId: "33333333-3333-4333-8333-333333333333",
agentId: "55555555-5555-4555-8555-555555555555",
agentConfigSnapshot: { adapterType: "codex_local" },
issueId: "44444444-4444-4444-8444-444444444444",
templateId: "built-in:default-test-template",
templateName: "Default test template",
templateBody: "Default {{skillName}}",
renderedTemplateBody: "Default Review",
harnessIssueDescription: "Try the skill\n\n---\n\nDefault Review",
status: "queued",
outputDocumentKey: "output",
outputSnapshot: "",
error: null,
deletedAt: null,
supersededAt: null,
harnessIssueExpiresAt: null,
harnessIssueDeletedAt: null,
...createSkillTestRunDetails(),
createdAt: new Date("2026-05-26T00:00:00.000Z"),
updatedAt: new Date("2026-05-26T00:00:00.000Z"),
cost: { costCents: 0, inputTokens: 0, cachedInputTokens: 0, outputTokens: 0 },
@@ -2160,28 +1938,7 @@ describe("company skill mutation permissions", () => {
) => {
await deps.cancelHarnessIssue("44444444-4444-4444-8444-444444444444");
return {
id: "22222222-2222-4222-8222-222222222222",
companyId: "company-1",
skillId: "skill-1",
inputId: "11111111-1111-4111-8111-111111111111",
inputSnapshot: "Try the skill",
skillVersionId: "33333333-3333-4333-8333-333333333333",
agentId: "55555555-5555-4555-8555-555555555555",
agentConfigSnapshot: { adapterType: "codex_local" },
issueId: "44444444-4444-4444-8444-444444444444",
templateId: "built-in:default-test-template",
templateName: "Default test template",
templateBody: "Default {{skillName}}",
renderedTemplateBody: "Default Review",
harnessIssueDescription: "Try the skill\n\n---\n\nDefault Review",
status: "cancelled",
outputDocumentKey: "output",
outputSnapshot: "",
error: "Cancelled by operator",
deletedAt: null,
supersededAt: null,
harnessIssueExpiresAt: null,
harnessIssueDeletedAt: null,
...createSkillTestRunDetails({ status: "cancelled", error: "Cancelled by operator" }),
createdAt: new Date("2026-05-26T00:00:00.000Z"),
updatedAt: new Date("2026-05-26T00:01:00.000Z"),
cost: { costCents: 0, inputTokens: 0, cachedInputTokens: 0, outputTokens: 0 },
@@ -2189,13 +1946,7 @@ describe("company skill mutation permissions", () => {
};
});
const app = await createApp({
type: "board",
userId: "local-board",
companyIds: ["company-1"],
source: "local_implicit",
isInstanceAdmin: false,
});
const app = await createApp(boardActor());
const created = await request(app)
.post("/api/companies/company-1/skills/skill-1/test-runs")
@@ -2282,13 +2033,7 @@ describe("company skill mutation permissions", () => {
harnessContent: { available: false, unavailableReason: "expired", documents: [], attachments: [], workProducts: [] },
});
const app = await createApp({
type: "board",
userId: "local-board",
companyIds: ["company-1"],
source: "local_implicit",
isInstanceAdmin: false,
});
const app = await createApp(boardActor());
const listed = await request(app)
.get("/api/companies/company-1/skills/skill-1/test-runs");
@@ -2322,28 +2067,7 @@ describe("company skill mutation permissions", () => {
) => {
await deps.hideHarnessIssue("44444444-4444-4444-8444-444444444444");
return {
id: "22222222-2222-4222-8222-222222222222",
companyId: "company-1",
skillId: "skill-1",
inputId: null,
inputSnapshot: "Try the skill",
skillVersionId: "33333333-3333-4333-8333-333333333333",
agentId: "55555555-5555-4555-8555-555555555555",
agentConfigSnapshot: { adapterType: "codex_local" },
issueId: "44444444-4444-4444-8444-444444444444",
templateId: "built-in:default-test-template",
templateName: "Default test template",
templateBody: "Default {{skillName}}",
renderedTemplateBody: "Default Review",
harnessIssueDescription: "Try the skill\n\n---\n\nDefault Review",
status: "succeeded",
outputDocumentKey: "output",
outputSnapshot: "",
error: null,
deletedAt: new Date("2026-05-26T00:02:00.000Z"),
supersededAt: null,
harnessIssueExpiresAt: null,
harnessIssueDeletedAt: null,
...createSkillTestRunDetails({ inputId: null, status: "succeeded", deletedAt: new Date("2026-05-26T00:02:00.000Z") }),
createdAt: new Date("2026-05-26T00:00:00.000Z"),
updatedAt: new Date("2026-05-26T00:02:00.000Z"),
cost: { costCents: 0, inputTokens: 0, cachedInputTokens: 0, outputTokens: 0 },
@@ -2351,13 +2075,7 @@ describe("company skill mutation permissions", () => {
};
});
const app = await createApp({
type: "board",
userId: "local-board",
companyIds: ["company-1"],
source: "local_implicit",
isInstanceAdmin: false,
});
const app = await createApp(boardActor());
const deleted = await request(app)
.delete("/api/companies/company-1/skills/skill-1/test-runs/22222222-2222-4222-8222-222222222222");
@@ -2371,13 +2089,7 @@ describe("company skill mutation permissions", () => {
it("returns 404 when deleting a missing test run", async () => {
mockCompanySkillService.deleteTestRun.mockResolvedValueOnce(null);
const app = await createApp({
type: "board",
userId: "local-board",
companyIds: ["company-1"],
source: "local_implicit",
isInstanceAdmin: false,
});
const app = await createApp(boardActor());
const res = await request(app)
.delete("/api/companies/company-1/skills/skill-1/test-runs/22222222-2222-4222-8222-222222222222");
expect(res.status).toBe(404);
@@ -2391,13 +2103,7 @@ describe("company skill mutation permissions", () => {
);
});
const res = await request(await createApp({
type: "board",
userId: "local-board",
companyIds: ["company-1"],
source: "local_implicit",
isInstanceAdmin: false,
}))
const res = await request(await createApp(boardActor()))
.delete("/api/companies/company-1/skills/skill-1");
expect(res.status, JSON.stringify(res.body)).toBe(422);
@@ -149,7 +149,6 @@ describe("paperclip runner native dev prerequisite", () => {
fs.utimesSync(target, current, current);
const input = { repoRoot: checkout.root, nativeRunnerRequired: true, platform: "darwin", architecture };
expect(paperclipRunnerBinaryNeedsBuild(input)).toBe(false);
// A newer Linux build-host output cannot mask the remote Mac artifact.
fs.writeFileSync(checkout.binary, nativeHeader("linux", "x64"));
const wrongHostNewer = new Date(Date.now() + 4000);
fs.utimesSync(checkout.binary, wrongHostNewer, wrongHostNewer);
+113 -387
View File
@@ -223,11 +223,7 @@ function createDeleteBlastRadius(overrides: Partial<{
}
let server: Server | null = null;
let currentActor: Record<string, unknown> = {
type: "board",
userId: "user-1",
source: "local_implicit",
};
let currentActor: Record<string, unknown> = localBoardActor();
const routeOptions: Record<string, unknown> = {};
const originalSecretsProviderEnv = process.env.PAPERCLIP_SECRETS_PROVIDER;
@@ -260,6 +256,67 @@ function createApp(actor: Record<string, unknown>, options: Record<string, unkno
return server;
}
function localBoardActor(userId = "user-1") {
return {
type: "board",
userId: userId,
source: "local_implicit",
};
}
function createSecurePluginDriver() {
return {
driverKey: "secure-plugin",
kind: "sandbox_provider",
displayName: "Secure Sandbox",
configSchema: {
type: "object",
properties: {
template: { type: "string" },
apiKey: { type: "string", format: "secret-ref" },
timeoutMs: { type: "number" },
reuseLease: { type: "boolean" },
},
},
};
}
function createSecretBackedSshEnvironment() {
return {
...createEnvironment(),
id: "env-ssh",
name: "SSH Fixture",
driver: "ssh" as const,
config: {
host: "ssh.example.test",
port: 22,
username: "ssh-user",
remoteWorkspacePath: "/srv/paperclip/workspace",
privateKey: null,
privateKeySecretRef: {
type: "secret_ref",
secretId: "11111111-1111-1111-1111-111111111111",
version: "latest",
},
knownHosts: null,
strictHostKeyChecking: true,
},
};
}
function createSshConfig() {
return {
host: "ssh.example.test",
port: 22,
username: "ssh-user",
remoteWorkspacePath: "/srv/paperclip/workspace",
privateKey: null,
privateKeySecretRef: null,
knownHosts: null,
strictHostKeyChecking: true,
};
}
describe("environment routes", () => {
afterAll(async () => {
if (originalSecretsProviderEnv === undefined) {
@@ -369,20 +426,7 @@ describe("environment routes", () => {
? {
pluginId: "plugin-secure",
pluginKey: "acme.secure-sandbox-provider",
driver: {
driverKey: "secure-plugin",
kind: "sandbox_provider",
displayName: "Secure Sandbox",
configSchema: {
type: "object",
properties: {
template: { type: "string" },
apiKey: { type: "string", format: "secret-ref" },
timeoutMs: { type: "number" },
reuseLease: { type: "boolean" },
},
},
},
driver: createSecurePluginDriver(),
}
: null
));
@@ -401,11 +445,7 @@ describe("environment routes", () => {
it("lists instance-scoped environments through the company route alias", async () => {
mockEnvironmentService.list.mockResolvedValue([createEnvironment()]);
const app = createApp({
type: "board",
userId: "user-1",
source: "local_implicit",
});
const app = createApp(localBoardActor());
const res = await request(app).get("/api/companies/company-1/environments?driver=local");
@@ -718,15 +758,7 @@ describe("environment routes", () => {
expect(mockEnvironmentService.update).not.toHaveBeenCalled();
});
it("still rejects deletion of the managed sandbox row", async () => {
mockEnvironmentService.getById.mockResolvedValue(createPlatformSandboxEnvironment());
const app = createApp(ownerAdminActor);
const res = await request(app).delete("/api/environments/env-managed-1");
expect(res.status).toBe(403);
expect(res.body.details).toMatchObject({ code: "environment_platform_managed" });
});
it("hides the local environment from every read surface under managed-sandbox-only", async () => {
mockInstanceSettingsService.getExperimental.mockResolvedValue({ enableManagedSandboxOnly: true });
@@ -1173,11 +1205,7 @@ describe("environment routes", () => {
});
it("returns provider capabilities for the company", async () => {
const app = createApp({
type: "board",
userId: "user-1",
source: "local_implicit",
});
const app = createApp(localBoardActor());
const res = await request(app).get("/api/companies/company-1/environments/capabilities");
@@ -1213,11 +1241,7 @@ describe("environment routes", () => {
},
},
]);
const app = createApp({
type: "board",
userId: "user-1",
source: "local_implicit",
});
const app = createApp(localBoardActor());
const res = await request(app).get("/api/companies/company-1/environments/capabilities");
@@ -1271,11 +1295,7 @@ describe("environment routes", () => {
configSchema: { type: "object", properties: {} },
},
]);
const app = createApp({
type: "board",
userId: "user-1",
source: "local_implicit",
});
const app = createApp(localBoardActor());
const res = await request(app).get("/api/companies/company-1/environments/capabilities");
@@ -1295,11 +1315,7 @@ describe("environment routes", () => {
configSchema: { type: "object", properties: {} },
},
]);
const app = createApp({
type: "board",
userId: "user-1",
source: "local_implicit",
});
const app = createApp(localBoardActor());
const res = await request(app).get("/api/companies/company-1/environments/capabilities");
@@ -1322,11 +1338,7 @@ describe("environment routes", () => {
configSchema: { type: "object", properties: {} },
},
]);
const app = createApp({
type: "board",
userId: "user-1",
source: "local_implicit",
});
const app = createApp(localBoardActor());
const res = await request(app).get("/api/companies/company-1/environments/capabilities");
@@ -1384,11 +1396,7 @@ describe("environment routes", () => {
it("creates an environment and logs activity", async () => {
const environment = createEnvironment();
mockEnvironmentService.create.mockResolvedValue(environment);
const app = createApp({
type: "board",
userId: "user-1",
source: "local_implicit",
});
const app = createApp(localBoardActor());
const res = await request(app)
.post("/api/companies/company-1/environments")
@@ -1425,11 +1433,7 @@ describe("environment routes", () => {
it("returns conflict when creating a second local environment", async () => {
mockEnvironmentService.list.mockResolvedValue([createEnvironment()]);
const app = createApp({
type: "board",
userId: "user-1",
source: "local_implicit",
});
const app = createApp(localBoardActor());
const res = await request(app)
.post("/api/companies/company-1/environments")
@@ -1529,11 +1533,7 @@ describe("environment routes", () => {
mockEnvironmentService.getDeleteBlastRadius.mockResolvedValue(createDeleteBlastRadius({
isManagedLocal: true,
}));
const app = createApp({
type: "board",
userId: "admin-1",
source: "local_implicit",
});
const app = createApp(localBoardActor("admin-1"));
const res = await request(app).delete("/api/environments/env-1");
@@ -1549,26 +1549,13 @@ describe("environment routes", () => {
...createEnvironment(),
driver: "ssh" as const,
name: "SSH Fixture",
config: {
host: "ssh.example.test",
port: 22,
username: "ssh-user",
remoteWorkspacePath: "/srv/paperclip/workspace",
privateKey: null,
privateKeySecretRef: null,
knownHosts: null,
strictHostKeyChecking: true,
},
config: createSshConfig(),
};
mockEnvironmentService.getById.mockResolvedValue(environment);
mockEnvironmentService.getDeleteBlastRadius.mockResolvedValue(createDeleteBlastRadius({
isInstanceDefault: true,
}));
const app = createApp({
type: "board",
userId: "admin-1",
source: "local_implicit",
});
const app = createApp(localBoardActor("admin-1"));
const res = await request(app).delete("/api/environments/env-1");
@@ -1585,26 +1572,13 @@ describe("environment routes", () => {
...createEnvironment(),
driver: "ssh" as const,
name: "SSH Fixture",
config: {
host: "ssh.example.test",
port: 22,
username: "ssh-user",
remoteWorkspacePath: "/srv/paperclip/workspace",
privateKey: null,
privateKeySecretRef: null,
knownHosts: null,
strictHostKeyChecking: true,
},
config: createSshConfig(),
};
mockEnvironmentService.getById.mockResolvedValue(environment);
mockEnvironmentService.getDeleteBlastRadius.mockResolvedValue(createDeleteBlastRadius({
pendingCleanupLeaseCount: 1,
}));
const app = createApp({
type: "board",
userId: "admin-1",
source: "local_implicit",
});
const app = createApp(localBoardActor("admin-1"));
const res = await request(app).delete("/api/environments/env-1");
@@ -1632,11 +1606,7 @@ describe("environment routes", () => {
activeLeaseCount: 1,
reusableSandboxLeaseCount: 1,
}));
const app = createApp({
type: "board",
userId: "admin-1",
source: "local_implicit",
});
const app = createApp(localBoardActor("admin-1"));
const res = await request(app).delete("/api/environments/env-1");
@@ -1662,11 +1632,7 @@ describe("environment routes", () => {
});
mockEnvironmentService.removeIfDeletable.mockResolvedValue(environment);
mockInstanceSettingsService.listCompanyIds.mockResolvedValue(["company-1"]);
const app = createApp({
type: "board",
userId: "admin-1",
source: "local_implicit",
});
const app = createApp(localBoardActor("admin-1"));
const res = await request(app).delete("/api/environments/env-1?destroyReusableSandboxLeases=true");
@@ -1696,11 +1662,7 @@ describe("environment routes", () => {
isInstanceDefault: true,
reusableSandboxLeaseCount: 1,
}));
const app = createApp({
type: "board",
userId: "admin-1",
source: "local_implicit",
});
const app = createApp(localBoardActor("admin-1"));
const res = await request(app).delete("/api/environments/env-1?destroyReusableSandboxLeases=true");
@@ -1722,11 +1684,7 @@ describe("environment routes", () => {
failed: 1,
skippedLiveRun: 0,
});
const app = createApp({
type: "board",
userId: "admin-1",
source: "local_implicit",
});
const app = createApp(localBoardActor("admin-1"));
const res = await request(app).delete("/api/environments/env-1?destroyReusableSandboxLeases=true");
@@ -1746,24 +1704,11 @@ describe("environment routes", () => {
id: "env-ssh",
driver: "ssh" as const,
name: "SSH Fixture",
config: {
host: "ssh.example.test",
port: 22,
username: "ssh-user",
remoteWorkspacePath: "/srv/paperclip/workspace",
privateKey: null,
privateKeySecretRef: null,
knownHosts: null,
strictHostKeyChecking: true,
},
config: createSshConfig(),
};
mockEnvironmentService.getById.mockResolvedValue(environment);
mockEnvironmentService.hasUnresolvedPendingCleanupLeases.mockResolvedValue(true);
const app = createApp({
type: "board",
userId: "admin-1",
source: "local_implicit",
});
const app = createApp(localBoardActor("admin-1"));
const res = await request(app)
.patch("/api/environments/env-ssh")
@@ -1790,25 +1735,12 @@ describe("environment routes", () => {
id: "env-ssh",
driver: "ssh" as const,
name: "SSH Fixture",
config: {
host: "ssh.example.test",
port: 22,
username: "ssh-user",
remoteWorkspacePath: "/srv/paperclip/workspace",
privateKey: null,
privateKeySecretRef: null,
knownHosts: null,
strictHostKeyChecking: true,
},
config: createSshConfig(),
};
mockEnvironmentService.getById.mockResolvedValue(environment);
mockEnvironmentService.hasUnresolvedPendingCleanupLeases.mockResolvedValue(true);
mockEnvironmentService.update.mockResolvedValue({ ...environment, description: "Updated" });
const app = createApp({
type: "board",
userId: "admin-1",
source: "local_implicit",
});
const app = createApp(localBoardActor("admin-1"));
const res = await request(app)
.patch("/api/environments/env-ssh")
@@ -1844,11 +1776,7 @@ describe("environment routes", () => {
companyName: "Other Team",
},
]);
const app = createApp({
type: "board",
userId: "admin-1",
source: "local_implicit",
});
const app = createApp(localBoardActor("admin-1"));
const res = await request(app).get("/api/environments/env-sandbox/secret-refs");
@@ -1890,35 +1818,12 @@ describe("environment routes", () => {
});
it("clears environment selections and secret bindings across all companies when deleting an environment", async () => {
const environment = {
...createEnvironment(),
id: "env-ssh",
name: "SSH Fixture",
driver: "ssh" as const,
config: {
host: "ssh.example.test",
port: 22,
username: "ssh-user",
remoteWorkspacePath: "/srv/paperclip/workspace",
privateKey: null,
privateKeySecretRef: {
type: "secret_ref",
secretId: "11111111-1111-1111-1111-111111111111",
version: "latest",
},
knownHosts: null,
strictHostKeyChecking: true,
},
};
const environment = createSecretBackedSshEnvironment();
mockEnvironmentService.getById.mockResolvedValue(environment);
mockEnvironmentService.getDeleteBlastRadius.mockResolvedValue(createDeleteBlastRadius());
mockEnvironmentService.removeIfDeletable.mockResolvedValue(environment);
mockInstanceSettingsService.listCompanyIds.mockResolvedValue(["company-1", "company-2"]);
const app = createApp({
type: "board",
userId: "admin-1",
source: "local_implicit",
});
const app = createApp(localBoardActor("admin-1"));
const res = await request(app).delete("/api/environments/env-ssh");
@@ -1956,11 +1861,7 @@ describe("environment routes", () => {
});
it("rejects invalid SSH config on create", async () => {
const app = createApp({
type: "board",
userId: "user-1",
source: "local_implicit",
});
const app = createApp(localBoardActor());
const res = await request(app)
.post("/api/companies/company-1/environments")
@@ -1979,33 +1880,10 @@ describe("environment routes", () => {
});
it("normalizes SSH private keys into secret refs before persistence", async () => {
const environment = {
...createEnvironment(),
id: "env-ssh",
name: "SSH Fixture",
driver: "ssh" as const,
config: {
host: "ssh.example.test",
port: 22,
username: "ssh-user",
remoteWorkspacePath: "/srv/paperclip/workspace",
privateKey: null,
privateKeySecretRef: {
type: "secret_ref",
secretId: "11111111-1111-1111-1111-111111111111",
version: "latest",
},
knownHosts: null,
strictHostKeyChecking: true,
},
};
const environment = createSecretBackedSshEnvironment();
mockEnvironmentService.create.mockResolvedValue(environment);
const pluginWorkerManager = {};
const app = createApp({
type: "board",
userId: "user-1",
source: "local_implicit",
}, { pluginWorkerManager });
const app = createApp(localBoardActor(), { pluginWorkerManager });
const res = await request(app)
.post("/api/companies/company-1/environments")
@@ -2045,32 +1923,9 @@ describe("environment routes", () => {
it("uses the configured provider for SSH private key secret materialization", async () => {
process.env.PAPERCLIP_SECRETS_PROVIDER = "aws_secrets_manager";
const environment = {
...createEnvironment(),
id: "env-ssh",
name: "SSH Fixture",
driver: "ssh" as const,
config: {
host: "ssh.example.test",
port: 22,
username: "ssh-user",
remoteWorkspacePath: "/srv/paperclip/workspace",
privateKey: null,
privateKeySecretRef: {
type: "secret_ref",
secretId: "11111111-1111-1111-1111-111111111111",
version: "latest",
},
knownHosts: null,
strictHostKeyChecking: true,
},
};
const environment = createSecretBackedSshEnvironment();
mockEnvironmentService.create.mockResolvedValue(environment);
const app = createApp({
type: "board",
userId: "user-1",
source: "local_implicit",
});
const app = createApp(localBoardActor());
const res = await request(app)
.post("/api/companies/company-1/environments")
@@ -2097,11 +1952,7 @@ describe("environment routes", () => {
});
it("rejects persisted fake sandbox environments", async () => {
const app = createApp({
type: "board",
userId: "user-1",
source: "local_implicit",
});
const app = createApp(localBoardActor());
const res = await request(app)
.post("/api/companies/company-1/environments")
@@ -2134,11 +1985,7 @@ describe("environment routes", () => {
};
mockEnvironmentService.create.mockResolvedValue(environment);
const pluginWorkerManager = {};
const app = createApp({
type: "board",
userId: "user-1",
source: "local_implicit",
}, { pluginWorkerManager });
const app = createApp(localBoardActor(), { pluginWorkerManager });
const res = await request(app)
.post("/api/companies/company-1/environments")
@@ -2223,11 +2070,7 @@ describe("environment routes", () => {
},
);
const pluginWorkerManager = {};
const app = createApp({
type: "board",
userId: "user-1",
source: "local_implicit",
}, { pluginWorkerManager });
const app = createApp(localBoardActor(), { pluginWorkerManager });
const res = await request(app)
.post("/api/companies/company-1/environments")
@@ -2277,27 +2120,10 @@ describe("environment routes", () => {
},
pluginId: "plugin-secure",
pluginKey: "acme.secure-sandbox-provider",
driver: {
driverKey: "secure-plugin",
kind: "sandbox_provider",
displayName: "Secure Sandbox",
configSchema: {
type: "object",
properties: {
template: { type: "string" },
apiKey: { type: "string", format: "secret-ref" },
timeoutMs: { type: "number" },
reuseLease: { type: "boolean" },
},
},
},
driver: createSecurePluginDriver(),
});
const pluginWorkerManager = {};
const app = createApp({
type: "board",
userId: "user-1",
source: "local_implicit",
}, { pluginWorkerManager });
const app = createApp(localBoardActor(), { pluginWorkerManager });
const res = await request(app)
.post("/api/companies/company-1/environments")
@@ -2369,27 +2195,10 @@ describe("environment routes", () => {
normalizedConfig: { ...config },
pluginId: "plugin-secure",
pluginKey: "acme.secure-sandbox-provider",
driver: {
driverKey: "secure-plugin",
kind: "sandbox_provider",
displayName: "Secure Sandbox",
configSchema: {
type: "object",
properties: {
template: { type: "string" },
apiKey: { type: "string", format: "secret-ref" },
timeoutMs: { type: "number" },
reuseLease: { type: "boolean" },
},
},
},
driver: createSecurePluginDriver(),
}));
const pluginWorkerManager = {};
const app = createApp({
type: "board",
userId: "user-1",
source: "local_implicit",
}, { pluginWorkerManager });
const app = createApp(localBoardActor(), { pluginWorkerManager });
const res = await request(app)
.post("/api/companies/company-1/environments")
@@ -2452,27 +2261,10 @@ describe("environment routes", () => {
},
pluginId: "plugin-secure",
pluginKey: "acme.secure-sandbox-provider",
driver: {
driverKey: "secure-plugin",
kind: "sandbox_provider",
displayName: "Secure Sandbox",
configSchema: {
type: "object",
properties: {
template: { type: "string" },
apiKey: { type: "string", format: "secret-ref" },
timeoutMs: { type: "number" },
reuseLease: { type: "boolean" },
},
},
},
driver: createSecurePluginDriver(),
});
const pluginWorkerManager = {};
const app = createApp({
type: "board",
userId: "user-1",
source: "local_implicit",
}, { pluginWorkerManager });
const app = createApp(localBoardActor(), { pluginWorkerManager });
const res = await request(app)
.post("/api/companies/company-1/environments")
@@ -2516,11 +2308,7 @@ describe("environment routes", () => {
mockValidatePluginEnvironmentDriverConfig.mockResolvedValue(environment.config);
mockEnvironmentService.create.mockResolvedValue(environment);
const pluginWorkerManager = {};
const app = createApp({
type: "board",
userId: "user-1",
source: "local_implicit",
}, { pluginWorkerManager });
const app = createApp(localBoardActor(), { pluginWorkerManager });
const res = await request(app)
.post("/api/companies/company-1/environments")
@@ -2604,11 +2392,7 @@ describe("environment routes", () => {
updatedAt: new Date("2026-04-16T05:05:00.000Z"),
},
]);
const app = createApp({
type: "board",
userId: "user-1",
source: "local_implicit",
});
const app = createApp(localBoardActor());
const res = await request(app).get(`/api/environments/${environment.id}/leases?status=active`);
@@ -2640,11 +2424,7 @@ describe("environment routes", () => {
createdAt: new Date("2026-04-16T05:00:00.000Z"),
updatedAt: new Date("2026-04-16T05:05:00.000Z"),
});
const app = createApp({
type: "board",
userId: "user-1",
source: "local_implicit",
});
const app = createApp(localBoardActor());
const res = await request(app).get("/api/environment-leases/lease-1");
@@ -2677,11 +2457,7 @@ describe("environment routes", () => {
...environment,
status: "archived",
});
const app = createApp({
type: "board",
userId: "user-1",
source: "local_implicit",
});
const app = createApp(localBoardActor());
const res = await request(app)
.patch(`/api/environments/${environment.id}?companyId=company-1`)
@@ -2736,11 +2512,7 @@ describe("environment routes", () => {
driver: "local" as const,
config: {},
});
const app = createApp({
type: "board",
userId: "user-1",
source: "local_implicit",
});
const app = createApp(localBoardActor());
const res = await request(app)
.patch(`/api/environments/${environment.id}?companyId=company-1`)
@@ -2783,30 +2555,13 @@ describe("environment routes", () => {
normalizedConfig: { ...config },
pluginId: "plugin-secure",
pluginKey: "acme.secure-sandbox-provider",
driver: {
driverKey: "secure-plugin",
kind: "sandbox_provider",
displayName: "Secure Sandbox",
configSchema: {
type: "object",
properties: {
template: { type: "string" },
apiKey: { type: "string", format: "secret-ref" },
timeoutMs: { type: "number" },
reuseLease: { type: "boolean" },
},
},
},
driver: createSecurePluginDriver(),
}));
// The environment's only binding still lives in another company. Before
// bindings moved with the referenced secret, this state made every save
// from the caller's company fail, with no route-level way out.
mockSecretService.listBindingCompanyIdsForTarget.mockResolvedValue(["company-old"]);
const app = createApp({
type: "board",
userId: "user-1",
source: "local_implicit",
}, { pluginWorkerManager: {} });
const app = createApp(localBoardActor(), { pluginWorkerManager: {} });
const res = await request(app)
.patch("/api/environments/env-sandbox?companyId=company-new")
@@ -2856,11 +2611,7 @@ describe("environment routes", () => {
mockSecretService.replaceSecretRefsForInstanceTarget.mockRejectedValue(
unprocessable("Secret referenced at apiKey was not found", { code: "secret_missing" }),
);
const app = createApp({
type: "board",
userId: "user-1",
source: "local_implicit",
}, { pluginWorkerManager: {} });
const app = createApp(localBoardActor(), { pluginWorkerManager: {} });
const res = await request(app)
.patch("/api/environments/env-sandbox?companyId=company-1")
@@ -2876,11 +2627,7 @@ describe("environment routes", () => {
it("requires explicit SSH config when switching from local to SSH", async () => {
mockEnvironmentService.getById.mockResolvedValue(createEnvironment());
const app = createApp({
type: "board",
userId: "user-1",
source: "local_implicit",
});
const app = createApp(localBoardActor());
const res = await request(app)
.patch("/api/environments/env-1?companyId=company-1")
@@ -2895,11 +2642,7 @@ describe("environment routes", () => {
it("rejects switching an environment to the built-in fake sandbox provider", async () => {
mockEnvironmentService.getById.mockResolvedValue(createEnvironment());
const app = createApp({
type: "board",
userId: "user-1",
source: "local_implicit",
});
const app = createApp(localBoardActor());
const res = await request(app)
.patch("/api/environments/env-1?companyId=company-1")
@@ -2918,11 +2661,7 @@ describe("environment routes", () => {
it("returns 404 when patching a missing environment", async () => {
mockEnvironmentService.getById.mockResolvedValue(null);
const app = createApp({
type: "board",
userId: "user-1",
source: "local_implicit",
});
const app = createApp(localBoardActor());
const res = await request(app)
.patch("/api/environments/missing-env")
@@ -3200,20 +2939,7 @@ describe("environment routes", () => {
},
pluginId: "plugin-secure",
pluginKey: "acme.secure-sandbox-provider",
driver: {
driverKey: "secure-plugin",
kind: "sandbox_provider",
displayName: "Secure Sandbox",
configSchema: {
type: "object",
properties: {
template: { type: "string" },
apiKey: { type: "string", format: "secret-ref" },
timeoutMs: { type: "number" },
reuseLease: { type: "boolean" },
},
},
},
driver: createSecurePluginDriver(),
});
mockProbeEnvironment.mockResolvedValue({
ok: true,
File diff suppressed because it is too large. Load diff
@@ -154,8 +154,6 @@ describeEmbeddedPostgres("environmentService leases", () => {
config: { config: { ...config, image: `registry.example.test/runtime@sha256:${"b".repeat(64)}` } },
driver: { driver: "ssh", config: { host: "fixture.example.test", username: "fixture" } },
};
// Interleave the real probe UPDATE after the save is prepared but before its
// real UPDATE executes. This reproduces the stale route-read/write window.
const delayedWriteDb = {
update(table: typeof environments) {
const prepared = db.update(table);
@@ -292,6 +292,104 @@ describeEmbeddedPostgres("executionWorkspaceService.getCloseReadiness", () => {
await tempDb?.cleanup();
});
function insertBlockedSourceTask(issueId: string, companyId: string, projectId: string) {
return db.insert(issues).values({
id: issueId,
companyId,
projectId,
title: "Source task",
status: "blocked",
priority: "medium",
});
}
function insertBoardWorkspaceRecovery(companyId: string, issueId: string, fingerprint: string) {
return db.insert(issueRecoveryActions).values({
companyId,
sourceIssueId: issueId,
kind: "workspace_validation",
status: "active",
ownerType: "board",
cause: "workspace_validation_failed",
fingerprint,
evidence: {
workspaceValidation: {
fingerprint,
},
},
nextAction: "Repair the source issue workspace link.",
});
}
function insertMissingRecordedBranchWorkspace(executionWorkspaceId: string, companyId: string, projectId: string, issueId: string, worktreePath: string) {
return db.insert(executionWorkspaces).values({
id: executionWorkspaceId,
companyId,
projectId,
sourceIssueId: issueId,
mode: "isolated_workspace",
strategyType: "git_worktree",
name: "feature/never-created",
status: "idle",
providerType: "git_worktree",
cwd: worktreePath,
providerRef: worktreePath,
branchName: "feature/never-created",
baseRef: "main",
});
}
function insertRecordedBranchWorkspace(executionWorkspaceId: string, companyId: string, projectId: string, projectWorkspaceId: string, issueId: string, worktreePath: string) {
return db.insert(executionWorkspaces).values({
id: executionWorkspaceId,
companyId,
projectId,
projectWorkspaceId,
sourceIssueId: issueId,
mode: "isolated_workspace",
strategyType: "git_worktree",
name: "feature/recorded",
status: "active",
providerType: "git_worktree",
cwd: worktreePath,
providerRef: worktreePath,
branchName: "feature/recorded",
baseRef: "main",
});
}
function insertRunningWorkspaceService(runtimeServiceId: string, companyId: string, projectId: string, executionWorkspaceId: string, issueId: string, worktreePath: string) {
return db.insert(workspaceRuntimeServices).values({
id: runtimeServiceId,
companyId,
projectId,
executionWorkspaceId,
issueId,
scopeType: "execution_workspace",
serviceName: "web",
status: "running",
lifecycle: "shared",
command: "pnpm dev",
cwd: worktreePath,
provider: "local_process",
healthStatus: "healthy",
});
}
function insertActiveCodexCoder(agentId: string, companyId: string) {
return db.insert(agents).values({
id: agentId,
companyId,
name: "Codex Coder",
role: "engineer",
status: "active",
adapterType: "codex_local",
adapterConfig: {},
runtimeConfig: {},
permissions: {},
});
}
async function seedTerminalWorkspace(options: {
mergedPr?: boolean;
activeRun?: boolean;
@@ -2202,21 +2300,7 @@ describeEmbeddedPostgres("executionWorkspaceService.getCloseReadiness", () => {
branchName: "feature/recorded",
baseRef: "main",
});
await db.insert(issueRecoveryActions).values({
companyId,
sourceIssueId: issueId,
kind: "workspace_validation",
status: "active",
ownerType: "board",
cause: "workspace_validation_failed",
fingerprint,
evidence: {
workspaceValidation: {
fingerprint,
},
},
nextAction: "Repair the source issue workspace link.",
});
await insertBoardWorkspaceRecovery(companyId, issueId, fingerprint);
const result = await svc.reconcileExecutionWorkspaceBranch(executionWorkspaceId, {
mode: "forward",
@@ -2309,21 +2393,7 @@ describeEmbeddedPostgres("executionWorkspaceService.getCloseReadiness", () => {
status: "blocked",
priority: "medium",
});
await db.insert(executionWorkspaces).values({
id: executionWorkspaceId,
companyId,
projectId,
sourceIssueId: issueId,
mode: "isolated_workspace",
strategyType: "git_worktree",
name: "feature/never-created",
status: "idle",
providerType: "git_worktree",
cwd: worktreePath,
providerRef: worktreePath,
branchName: "feature/never-created",
baseRef: "main",
});
await insertMissingRecordedBranchWorkspace(executionWorkspaceId, companyId, projectId, issueId, worktreePath);
const result = await svc.reconcileExecutionWorkspaceBranch(executionWorkspaceId, {
mode: "forward",
@@ -2390,21 +2460,7 @@ describeEmbeddedPostgres("executionWorkspaceService.getCloseReadiness", () => {
status: "blocked",
priority: "medium",
});
await db.insert(executionWorkspaces).values({
id: executionWorkspaceId,
companyId,
projectId,
sourceIssueId: issueId,
mode: "isolated_workspace",
strategyType: "git_worktree",
name: "feature/never-created",
status: "idle",
providerType: "git_worktree",
cwd: worktreePath,
providerRef: worktreePath,
branchName: "feature/never-created",
baseRef: "main",
});
await insertMissingRecordedBranchWorkspace(executionWorkspaceId, companyId, projectId, issueId, worktreePath);
await expect(svc.reconcileExecutionWorkspaceBranch(executionWorkspaceId, {
mode: "forward",
@@ -2463,21 +2519,7 @@ describeEmbeddedPostgres("executionWorkspaceService.getCloseReadiness", () => {
status: "blocked",
priority: "medium",
});
await db.insert(executionWorkspaces).values({
id: executionWorkspaceId,
companyId,
projectId,
sourceIssueId: issueId,
mode: "isolated_workspace",
strategyType: "git_worktree",
name: "feature/never-created",
status: "idle",
providerType: "git_worktree",
cwd: worktreePath,
providerRef: worktreePath,
branchName: "feature/never-created",
baseRef: "main",
});
await insertMissingRecordedBranchWorkspace(executionWorkspaceId, companyId, projectId, issueId, worktreePath);
await expect(svc.reconcileExecutionWorkspaceBranch(executionWorkspaceId, {
mode: "forward",
@@ -2527,17 +2569,7 @@ describeEmbeddedPostgres("executionWorkspaceService.getCloseReadiness", () => {
issuePrefix: "PAP",
requireBoardApprovalForNewAgents: false,
});
await db.insert(agents).values({
id: agentId,
companyId,
name: "Codex Coder",
role: "engineer",
status: "active",
adapterType: "codex_local",
adapterConfig: {},
runtimeConfig: {},
permissions: {},
});
await insertActiveCodexCoder(agentId, companyId);
await db.insert(projects).values({
id: projectId,
companyId,
@@ -2563,37 +2595,8 @@ describeEmbeddedPostgres("executionWorkspaceService.getCloseReadiness", () => {
priority: "medium",
assigneeAgentId: agentId,
});
await db.insert(executionWorkspaces).values({
id: executionWorkspaceId,
companyId,
projectId,
projectWorkspaceId,
sourceIssueId: issueId,
mode: "isolated_workspace",
strategyType: "git_worktree",
name: "feature/recorded",
status: "active",
providerType: "git_worktree",
cwd: worktreePath,
providerRef: worktreePath,
branchName: "feature/recorded",
baseRef: "main",
});
await db.insert(issueRecoveryActions).values({
companyId,
sourceIssueId: issueId,
kind: "workspace_validation",
status: "active",
ownerType: "board",
cause: "workspace_validation_failed",
fingerprint,
evidence: {
workspaceValidation: {
fingerprint,
},
},
nextAction: "Repair the source issue workspace link.",
});
await insertRecordedBranchWorkspace(executionWorkspaceId, companyId, projectId, projectWorkspaceId, issueId, worktreePath);
await insertBoardWorkspaceRecovery(companyId, issueId, fingerprint);
const result = await svc.reconcileExecutionWorkspaceBranch(executionWorkspaceId, {
mode: "quarantine_restore",
@@ -2717,21 +2720,7 @@ describeEmbeddedPostgres("executionWorkspaceService.getCloseReadiness", () => {
branchName: "feature/recorded",
baseRef: "main",
});
await db.insert(workspaceRuntimeServices).values({
id: runtimeServiceId,
companyId,
projectId,
executionWorkspaceId,
issueId,
scopeType: "execution_workspace",
serviceName: "web",
status: "running",
lifecycle: "shared",
command: "pnpm dev",
cwd: worktreePath,
provider: "local_process",
healthStatus: "healthy",
});
await insertRunningWorkspaceService(runtimeServiceId, companyId, projectId, executionWorkspaceId, issueId, worktreePath);
await expect(svc.reconcileExecutionWorkspaceBranch(executionWorkspaceId, {
mode: "quarantine_restore",
@@ -2875,37 +2864,8 @@ describeEmbeddedPostgres("executionWorkspaceService.getCloseReadiness", () => {
lastDecisionOutcome: null,
},
});
await db.insert(executionWorkspaces).values({
id: executionWorkspaceId,
companyId,
projectId,
projectWorkspaceId,
sourceIssueId: issueId,
mode: "isolated_workspace",
strategyType: "git_worktree",
name: "feature/recorded",
status: "active",
providerType: "git_worktree",
cwd: worktreePath,
providerRef: worktreePath,
branchName: "feature/recorded",
baseRef: "main",
});
await db.insert(issueRecoveryActions).values({
companyId,
sourceIssueId: issueId,
kind: "workspace_validation",
status: "active",
ownerType: "board",
cause: "workspace_validation_failed",
fingerprint,
evidence: {
workspaceValidation: {
fingerprint,
},
},
nextAction: "Repair the source issue workspace link.",
});
await insertRecordedBranchWorkspace(executionWorkspaceId, companyId, projectId, projectWorkspaceId, issueId, worktreePath);
await insertBoardWorkspaceRecovery(companyId, issueId, fingerprint);
const result = await svc.reconcileExecutionWorkspaceBranch(executionWorkspaceId, {
mode: "quarantine_restore",
@@ -2986,17 +2946,7 @@ describeEmbeddedPostgres("executionWorkspaceService.getCloseReadiness", () => {
issuePrefix: "PAP",
requireBoardApprovalForNewAgents: false,
});
await db.insert(agents).values({
id: agentId,
companyId,
name: "Codex Coder",
role: "engineer",
status: "active",
adapterType: "codex_local",
adapterConfig: {},
runtimeConfig: {},
permissions: {},
});
await insertActiveCodexCoder(agentId, companyId);
await db.insert(projects).values({
id: projectId,
companyId,
@@ -3159,14 +3109,7 @@ describeEmbeddedPostgres("executionWorkspaceService.getCloseReadiness", () => {
name: "Branch reconcile",
status: "in_progress",
});
await db.insert(issues).values({
id: issueId,
companyId,
projectId,
title: "Source task",
status: "blocked",
priority: "medium",
});
await insertBlockedSourceTask(issueId, companyId, projectId);
await db.insert(executionWorkspaces).values({
id: executionWorkspaceId,
companyId,
@@ -3244,14 +3187,7 @@ describeEmbeddedPostgres("executionWorkspaceService.getCloseReadiness", () => {
name: "Branch reconcile",
status: "in_progress",
});
await db.insert(issues).values({
id: issueId,
companyId,
projectId,
title: "Source task",
status: "blocked",
priority: "medium",
});
await insertBlockedSourceTask(issueId, companyId, projectId);
await db.insert(executionWorkspaces).values({
id: executionWorkspaceId,
companyId,
@@ -3329,14 +3265,7 @@ describeEmbeddedPostgres("executionWorkspaceService.getCloseReadiness", () => {
name: "Branch reconcile",
status: "in_progress",
});
await db.insert(issues).values({
id: issueId,
companyId,
projectId,
title: "Source task",
status: "blocked",
priority: "medium",
});
await insertBlockedSourceTask(issueId, companyId, projectId);
await db.insert(executionWorkspaces).values({
id: executionWorkspaceId,
companyId,
@@ -3433,14 +3362,7 @@ describeEmbeddedPostgres("executionWorkspaceService.getCloseReadiness", () => {
name: "Branch reconcile",
status: "in_progress",
});
await db.insert(issues).values({
id: issueId,
companyId,
projectId,
title: "Source task",
status: "blocked",
priority: "medium",
});
await insertBlockedSourceTask(issueId, companyId, projectId);
await db.insert(executionWorkspaces).values({
id: executionWorkspaceId,
companyId,
@@ -3456,21 +3378,7 @@ describeEmbeddedPostgres("executionWorkspaceService.getCloseReadiness", () => {
branchName: "feature/recorded",
baseRef: "main",
});
await db.insert(workspaceRuntimeServices).values({
id: runtimeServiceId,
companyId,
projectId,
executionWorkspaceId,
issueId,
scopeType: "execution_workspace",
serviceName: "web",
status: "running",
lifecycle: "shared",
command: "pnpm dev",
cwd: worktreePath,
provider: "local_process",
healthStatus: "healthy",
});
await insertRunningWorkspaceService(runtimeServiceId, companyId, projectId, executionWorkspaceId, issueId, worktreePath);
await expect(svc.reconcileExecutionWorkspaceBranch(executionWorkspaceId, {
mode: "override",
@@ -3540,14 +3448,7 @@ describeEmbeddedPostgres("executionWorkspaceService.getCloseReadiness", () => {
name: "Branch reconcile",
status: "in_progress",
});
await db.insert(issues).values({
id: issueId,
companyId,
projectId,
title: "Source task",
status: "blocked",
priority: "medium",
});
await insertBlockedSourceTask(issueId, companyId, projectId);
await db.insert(executionWorkspaces).values({
id: executionWorkspaceId,
companyId,
@@ -3669,14 +3570,7 @@ describeEmbeddedPostgres("executionWorkspaceService.getCloseReadiness", () => {
name: "Branch reconcile",
status: "in_progress",
});
await db.insert(issues).values({
id: issueId,
companyId,
projectId,
title: "Source task",
status: "blocked",
priority: "medium",
});
await insertBlockedSourceTask(issueId, companyId, projectId);
await db.insert(executionWorkspaces).values({
id: executionWorkspaceId,
companyId,
@@ -3852,14 +3746,7 @@ describeEmbeddedPostgres("executionWorkspaceService.getCloseReadiness", () => {
name: "Branch reconcile",
status: "in_progress",
});
await db.insert(issues).values({
id: issueId,
companyId,
projectId,
title: "Source task",
status: "blocked",
priority: "medium",
});
await insertBlockedSourceTask(issueId, companyId, projectId);
await db.insert(executionWorkspaces).values({
id: executionWorkspaceId,
companyId,
+1 -1
View File
@@ -65,7 +65,7 @@ describe("isPrivateWebhookHttpRequest", () => {
describe("isSecretSensitiveHttpRequest", () => {
it("protects Slack registration, installation, recovery, and all callback methods", () => {
for (const suffix of ["registration", "install", "resume"]) expect(isSecretSensitiveHttpRequest("POST", `/api/chat-endpoints/id/slack/${suffix}`)).toBe(true);
for (const method of ["GET", "POST", "DELETE"]) for (const path of ["/api/chat-slack/oauth/callback?code=canary", "https://paperclip.test/API/CHAT-SLACK/OAUTH/callback?code=canary"])
for (const method of ["GET", "POST", "DELETE"]) for (const path of ["/api/chat-slack/oauth/callback?code=canary", "/api/chat-slack/managed/oauth/callback?claim=canary", "/api/chat-slack/managed/oauth/callback?code=canary", "/api/chat-endpoints/endpoint/slack/managed/authorize", "/api/chat-endpoints/endpoint/slack/managed/provision", "https://paperclip.test/API/CHAT-SLACK/OAUTH/callback?code=canary"])
expect(isSecretSensitiveHttpRequest(method, path)).toBe(true);
});
it("identifies credential-bearing chat setup mutations", () => {
@@ -68,6 +68,15 @@ const mockDb = {
select: vi.fn(() => ({ from: () => Promise.resolve(mockCompanyRows) })),
};
function localAdminActor() {
return {
type: "board",
userId: "local-board",
source: "local_implicit",
isInstanceAdmin: true,
};
}
describe("instance settings routes", () => {
const routeModules = hoistModuleGraph(registerModuleMocks, async () => {
const [{ errorHandler }, { instanceSettingsRoutes }] = await Promise.all([
@@ -234,12 +243,7 @@ describe("instance settings routes", () => {
});
it("allows local board users to read and update experimental settings", async () => {
const app = await createApp({
type: "board",
userId: "local-board",
source: "local_implicit",
isInstanceAdmin: true,
});
const app = await createApp(localAdminActor());
const getRes = await request(app).get("/api/instance/settings/experimental");
expect(getRes.status).toBe(200);
@@ -273,12 +277,7 @@ describe("instance settings routes", () => {
}, 10_000);
it("does not expose the retired liveness auto-recovery endpoints", async () => {
const app = await createApp({
type: "board",
userId: "local-board",
source: "local_implicit",
isInstanceAdmin: true,
});
const app = await createApp(localAdminActor());
await request(app)
.post("/api/instance/settings/experimental/issue-graph-liveness-auto-recovery/preview")
@@ -303,12 +302,7 @@ describe("instance settings routes", () => {
});
it("accepts the instance-wide Streamlined UI preference", async () => {
const app = await createApp({
type: "board",
userId: "local-board",
source: "local_implicit",
isInstanceAdmin: true,
});
const app = await createApp(localAdminActor());
await request(app)
.patch("/api/instance/settings/experimental")
@@ -321,12 +315,7 @@ describe("instance settings routes", () => {
});
it("strips server-managed worktree run execution fields before updating experimental settings", async () => {
const app = await createApp({
type: "board",
userId: "local-board",
source: "local_implicit",
isInstanceAdmin: true,
});
const app = await createApp(localAdminActor());
await request(app)
.patch("/api/instance/settings/experimental")
@@ -343,12 +332,7 @@ describe("instance settings routes", () => {
});
it("allows local board users to read and update the instance default environment", async () => {
const app = await createApp({
type: "board",
userId: "local-board",
source: "local_implicit",
isInstanceAdmin: true,
});
const app = await createApp(localAdminActor());
const getRes = await request(app).get("/api/instance/settings");
expect(getRes.status).toBe(200);
@@ -379,12 +363,7 @@ describe("instance settings routes", () => {
envVars: {},
metadata: { managedByPaperclip: true, managedDefaultStamped: true },
});
const app = await createApp({
type: "board",
userId: "local-board",
source: "local_implicit",
isInstanceAdmin: true,
});
const app = await createApp(localAdminActor());
const patchRes = await request(app)
.patch("/api/instance/settings")
@@ -419,12 +398,7 @@ describe("instance settings routes", () => {
metadata: { managedByPaperclip: true, managedDefaultStamped: true },
});
mockEnvironmentService.update.mockRejectedValue(new Error("metadata write failed"));
const app = await createApp({
type: "board",
userId: "local-board",
source: "local_implicit",
isInstanceAdmin: true,
});
const app = await createApp(localAdminActor());
const patchRes = await request(app)
.patch("/api/instance/settings")
@@ -436,12 +410,7 @@ describe("instance settings routes", () => {
it("rejects unknown defaultEnvironmentId values with 422", async () => {
mockEnvironmentService.getById.mockResolvedValue(null);
const app = await createApp({
type: "board",
userId: "local-board",
source: "local_implicit",
isInstanceAdmin: true,
});
const app = await createApp(localAdminActor());
const res = await request(app)
.patch("/api/instance/settings")
@@ -453,12 +422,7 @@ describe("instance settings routes", () => {
});
it("allows local board users to update guarded dev-server auto-restart", async () => {
const app = await createApp({
type: "board",
userId: "local-board",
source: "local_implicit",
isInstanceAdmin: true,
});
const app = await createApp(localAdminActor());
await request(app)
.patch("/api/instance/settings/experimental")
@@ -473,12 +437,7 @@ describe("instance settings routes", () => {
});
it("allows local board users to update external object detection", async () => {
const app = await createApp({
type: "board",
userId: "local-board",
source: "local_implicit",
isInstanceAdmin: true,
});
const app = await createApp(localAdminActor());
await request(app)
.patch("/api/instance/settings/experimental")
@@ -491,12 +450,7 @@ describe("instance settings routes", () => {
});
it("allows local board users to update built-in agents", async () => {
const app = await createApp({
type: "board",
userId: "local-board",
source: "local_implicit",
isInstanceAdmin: true,
});
const app = await createApp(localAdminActor());
await request(app)
.patch("/api/instance/settings/experimental")
@@ -509,12 +463,7 @@ describe("instance settings routes", () => {
});
it("allows local board users to update the goals sidebar link", async () => {
const app = await createApp({
type: "board",
userId: "local-board",
source: "local_implicit",
isInstanceAdmin: true,
});
const app = await createApp(localAdminActor());
await request(app)
.patch("/api/instance/settings/experimental")
@@ -527,12 +476,7 @@ describe("instance settings routes", () => {
});
it("allows local board users to update the server info debug view", async () => {
const app = await createApp({
type: "board",
userId: "local-board",
source: "local_implicit",
isInstanceAdmin: true,
});
const app = await createApp(localAdminActor());
await request(app)
.patch("/api/instance/settings/experimental")
@@ -545,12 +489,7 @@ describe("instance settings routes", () => {
});
it("allows local board users to update environment controls", async () => {
const app = await createApp({
type: "board",
userId: "local-board",
source: "local_implicit",
isInstanceAdmin: true,
});
const app = await createApp(localAdminActor());
await request(app)
.patch("/api/instance/settings/experimental")
@@ -582,12 +521,7 @@ describe("instance settings routes", () => {
});
it("allows local board users to read and update general settings", async () => {
const app = await createApp({
type: "board",
userId: "local-board",
source: "local_implicit",
isInstanceAdmin: true,
});
const app = await createApp(localAdminActor());
const getRes = await request(app).get("/api/instance/settings/general");
expect(getRes.status).toBe(200);
@@ -321,6 +321,56 @@ async function waitForWakeup(assertion: () => void) {
await vi.waitFor(assertion);
}
function createAgentReviewPolicy(reviewerAgentId: string) {
return normalizePolicy({
stages: [
{
id: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
type: "review",
participants: [{ type: "agent", agentId: reviewerAgentId }],
},
],
});
}
function createReviewIssue(policy: NonNullable<Awaited<ReturnType<typeof normalizePolicy>>>, reviewerAgentId: string) {
return {
...makeIssue("todo"),
status: "in_review",
assigneeAgentId: reviewerAgentId,
executionPolicy: policy,
executionState: {
status: "pending",
currentStageId: policy.stages[0].id,
currentStageIndex: 0,
currentStageType: "review",
currentParticipant: { type: "agent", agentId: reviewerAgentId },
returnAssignee: {
type: "agent",
agentId: "22222222-2222-4222-8222-222222222222",
},
completedStageIds: [],
lastDecisionId: null,
lastDecisionOutcome: null,
},
};
}
function createScheduledRetry() {
return {
runId: "retry-run-1",
status: "scheduled_retry",
agentId: "22222222-2222-4222-8222-222222222222",
agentName: "CodexCoder",
retryOfRunId: "source-run-1",
scheduledRetryAt: new Date("2026-05-18T14:00:00.000Z"),
scheduledRetryAttempt: 1,
scheduledRetryReason: "transient_failure",
error: null,
errorCode: null,
};
}
describe("issue comment reopen routes", () => {
beforeEach(() => {
vi.clearAllMocks();
@@ -956,7 +1006,7 @@ describe("issue comment reopen routes", () => {
// Same guard on cancelled status — explicit resume must use `resume: true`,
// a log-class self-comment with `reopen: true` is not a reopen signal.
it("does not reopen via POST comment+reopen when the assignee agent is the actor on a cancelled issue", async () => {
it("rejects bare agent reopen requests on cancelled issues before writing a comment", async () => {
const assigneeAgentId = "22222222-2222-4222-8222-222222222222";
mockIssueService.getById.mockResolvedValue(makeIssue("cancelled"));
mockIssueService.update.mockImplementation(
@@ -985,14 +1035,13 @@ describe("issue comment reopen routes", () => {
}),
)
.post("/api/issues/11111111-1111-4111-8111-111111111111/comments")
// Cancelled issues reject explicit resume entirely, so only reopen=true
// is observable here — the guard is what keeps it from flipping back.
.send({ body: "log line", reopen: true });
// Cancelled issues are rejected at assertExplicitResumeIntentAllowed for
// agent actors with reopen=true (409). The guard runs after that, but
// either way no reopen wakeup must fire and no status update to todo.
expect([200, 201, 409]).toContain(res.status);
expect(res.status).toBe(409);
expect(res.body.error).toBe(
"Cancelled issues must be restored through the dedicated restore flow",
);
expect(mockIssueService.addComment).not.toHaveBeenCalled();
expect(mockIssueService.update).not.toHaveBeenCalledWith(
"11111111-1111-4111-8111-111111111111",
expect.objectContaining({ status: "todo" }),
@@ -1142,18 +1191,7 @@ describe("issue comment reopen routes", () => {
executionRunId: "retry-run-1",
};
mockIssueService.getById.mockResolvedValue(issue);
mockIssueService.getCurrentScheduledRetry.mockResolvedValue({
runId: "retry-run-1",
status: "scheduled_retry",
agentId: "22222222-2222-4222-8222-222222222222",
agentName: "CodexCoder",
retryOfRunId: "source-run-1",
scheduledRetryAt: new Date("2026-05-18T14:00:00.000Z"),
scheduledRetryAttempt: 1,
scheduledRetryReason: "transient_failure",
error: null,
errorCode: null,
});
mockIssueService.getCurrentScheduledRetry.mockResolvedValue(createScheduledRetry());
mockIssueService.update.mockImplementation(
async (_id: string, patch: Record<string, unknown>) => ({
...issue,
@@ -1214,18 +1252,7 @@ describe("issue comment reopen routes", () => {
executionRunId: "retry-run-1",
};
mockIssueService.getById.mockResolvedValue(issue);
mockIssueService.getCurrentScheduledRetry.mockResolvedValue({
runId: "retry-run-1",
status: "scheduled_retry",
agentId: "22222222-2222-4222-8222-222222222222",
agentName: "CodexCoder",
retryOfRunId: "source-run-1",
scheduledRetryAt: new Date("2026-05-18T14:00:00.000Z"),
scheduledRetryAttempt: 1,
scheduledRetryReason: "transient_failure",
error: null,
errorCode: null,
});
mockIssueService.getCurrentScheduledRetry.mockResolvedValue(createScheduledRetry());
mockHeartbeatService.cancelRun.mockRejectedValue(
new Error("cancel failed"),
);
@@ -1890,18 +1917,7 @@ describe("issue comment reopen routes", () => {
executionRunId: "retry-run-1",
};
mockIssueService.getById.mockResolvedValue(issue);
mockIssueService.getCurrentScheduledRetry.mockResolvedValue({
runId: "retry-run-1",
status: "scheduled_retry",
agentId: "22222222-2222-4222-8222-222222222222",
agentName: "CodexCoder",
retryOfRunId: "source-run-1",
scheduledRetryAt: new Date("2026-05-18T14:00:00.000Z"),
scheduledRetryAttempt: 1,
scheduledRetryReason: "transient_failure",
error: null,
errorCode: null,
});
mockIssueService.getCurrentScheduledRetry.mockResolvedValue(createScheduledRetry());
mockIssueService.update.mockImplementation(
async (_id: string, patch: Record<string, unknown>) => ({
...issue,
@@ -1950,18 +1966,7 @@ describe("issue comment reopen routes", () => {
executionRunId: "retry-run-1",
};
mockIssueService.getById.mockResolvedValue(issue);
mockIssueService.getCurrentScheduledRetry.mockResolvedValue({
runId: "retry-run-1",
status: "scheduled_retry",
agentId: "22222222-2222-4222-8222-222222222222",
agentName: "CodexCoder",
retryOfRunId: "source-run-1",
scheduledRetryAt: new Date("2026-05-18T14:00:00.000Z"),
scheduledRetryAttempt: 1,
scheduledRetryReason: "transient_failure",
error: null,
errorCode: null,
});
mockIssueService.getCurrentScheduledRetry.mockResolvedValue(createScheduledRetry());
mockHeartbeatService.cancelRun.mockRejectedValue(
new Error("cancel failed"),
);
@@ -2635,8 +2640,8 @@ describe("issue comment reopen routes", () => {
},
);
it.each(["invalid", "wrong agent", "wrong company"])(
"rejects comment and PATCH writes with a %s run",
it(
"rejects comment and PATCH writes when influence validation rejects the run",
async () => {
mockIssueService.getById.mockResolvedValue(makeIssue("todo"));
mockObserveCrossIssueInfluence.mockRejectedValue(
@@ -2940,35 +2945,8 @@ describe("issue comment reopen routes", () => {
it("auto-approves a reviewer comment with the APPROVED review marker", async () => {
const reviewerAgentId = "33333333-3333-4333-8333-333333333333";
const policy = await normalizePolicy({
stages: [
{
id: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
type: "review",
participants: [{ type: "agent", agentId: reviewerAgentId }],
},
],
})!;
const issue = {
...makeIssue("todo"),
status: "in_review",
assigneeAgentId: reviewerAgentId,
executionPolicy: policy,
executionState: {
status: "pending",
currentStageId: policy.stages[0].id,
currentStageIndex: 0,
currentStageType: "review",
currentParticipant: { type: "agent", agentId: reviewerAgentId },
returnAssignee: {
type: "agent",
agentId: "22222222-2222-4222-8222-222222222222",
},
completedStageIds: [],
lastDecisionId: null,
lastDecisionOutcome: null,
},
};
const policy = await createAgentReviewPolicy(reviewerAgentId)!;
const issue = createReviewIssue(policy, reviewerAgentId);
const reviewBody = "## Review: PAP-580 - APPROVED\n\nLooks good.";
mockIssueService.getById.mockResolvedValue(issue);
mockIssueService.addComment.mockResolvedValue({
@@ -3032,35 +3010,8 @@ describe("issue comment reopen routes", () => {
it("auto-approves a reviewer comment with structured review metadata", async () => {
const reviewerAgentId = "33333333-3333-4333-8333-333333333333";
const policy = await normalizePolicy({
stages: [
{
id: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
type: "review",
participants: [{ type: "agent", agentId: reviewerAgentId }],
},
],
})!;
const issue = {
...makeIssue("todo"),
status: "in_review",
assigneeAgentId: reviewerAgentId,
executionPolicy: policy,
executionState: {
status: "pending",
currentStageId: policy.stages[0].id,
currentStageIndex: 0,
currentStageType: "review",
currentParticipant: { type: "agent", agentId: reviewerAgentId },
returnAssignee: {
type: "agent",
agentId: "22222222-2222-4222-8222-222222222222",
},
completedStageIds: [],
lastDecisionId: null,
lastDecisionOutcome: null,
},
};
const policy = await createAgentReviewPolicy(reviewerAgentId)!;
const issue = createReviewIssue(policy, reviewerAgentId);
const reviewBody = "kind: review\ndecision: approved\nsummary: ship it";
mockIssueService.getById.mockResolvedValue(issue);
mockIssueService.addComment.mockResolvedValue({
@@ -3125,15 +3076,7 @@ describe("issue comment reopen routes", () => {
it("auto-approves a reviewer comment and wakes dependents when the final blocker resolves", async () => {
const reviewerAgentId = "33333333-3333-4333-8333-333333333333";
const dependentAgentId = "44444444-4444-4444-8444-444444444444";
const policy = await normalizePolicy({
stages: [
{
id: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
type: "review",
participants: [{ type: "agent", agentId: reviewerAgentId }],
},
],
})!;
const policy = await createAgentReviewPolicy(reviewerAgentId)!;
const issue = {
...makeIssue("todo"),
status: "in_review",
@@ -3220,15 +3163,7 @@ describe("issue comment reopen routes", () => {
it("does not wake the returnAssignee with issue_commented when auto-approval reassigns the issue", async () => {
const reviewerAgentId = "33333333-3333-4333-8333-333333333333";
const returnAssigneeAgentId = "22222222-2222-4222-8222-222222222222";
const policy = await normalizePolicy({
stages: [
{
id: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
type: "review",
participants: [{ type: "agent", agentId: reviewerAgentId }],
},
],
})!;
const policy = await createAgentReviewPolicy(reviewerAgentId)!;
const issue = {
...makeIssue("todo"),
status: "in_review",
@@ -3298,35 +3233,8 @@ describe("issue comment reopen routes", () => {
it("does not auto-approve APPROVED comments from a non-review participant", async () => {
const reviewerAgentId = "33333333-3333-4333-8333-333333333333";
const policy = await normalizePolicy({
stages: [
{
id: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
type: "review",
participants: [{ type: "agent", agentId: reviewerAgentId }],
},
],
})!;
const issue = {
...makeIssue("todo"),
status: "in_review",
assigneeAgentId: reviewerAgentId,
executionPolicy: policy,
executionState: {
status: "pending",
currentStageId: policy.stages[0].id,
currentStageIndex: 0,
currentStageType: "review",
currentParticipant: { type: "agent", agentId: reviewerAgentId },
returnAssignee: {
type: "agent",
agentId: "22222222-2222-4222-8222-222222222222",
},
completedStageIds: [],
lastDecisionId: null,
lastDecisionOutcome: null,
},
};
const policy = await createAgentReviewPolicy(reviewerAgentId)!;
const issue = createReviewIssue(policy, reviewerAgentId);
const reviewBody = "## Review: PAP-580 - APPROVED\n\nLooks good.";
mockIssueService.getById.mockResolvedValue(issue);
mockIssueService.addComment.mockResolvedValue({
@@ -3422,35 +3330,8 @@ describe("issue comment reopen routes", () => {
// Even though both `kind: review` and `decision: approved` appear, a blank line between
// them means they are not on truly consecutive lines, so the strict regex must reject it.
const reviewerAgentId = "33333333-3333-4333-8333-333333333333";
const policy = await normalizePolicy({
stages: [
{
id: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
type: "review",
participants: [{ type: "agent", agentId: reviewerAgentId }],
},
],
})!;
const issue = {
...makeIssue("todo"),
status: "in_review",
assigneeAgentId: reviewerAgentId,
executionPolicy: policy,
executionState: {
status: "pending",
currentStageId: policy.stages[0].id,
currentStageIndex: 0,
currentStageType: "review",
currentParticipant: { type: "agent", agentId: reviewerAgentId },
returnAssignee: {
type: "agent",
agentId: "22222222-2222-4222-8222-222222222222",
},
completedStageIds: [],
lastDecisionId: null,
lastDecisionOutcome: null,
},
};
const policy = await createAgentReviewPolicy(reviewerAgentId)!;
const issue = createReviewIssue(policy, reviewerAgentId);
const reviewBody = "kind: review\n\ndecision: approved";
mockIssueService.getById.mockResolvedValue(issue);
mockIssueService.addComment.mockResolvedValue({
@@ -3483,35 +3364,8 @@ describe("issue comment reopen routes", () => {
it("does not auto-approve reviewer comments without an approval marker", async () => {
const reviewerAgentId = "33333333-3333-4333-8333-333333333333";
const policy = await normalizePolicy({
stages: [
{
id: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
type: "review",
participants: [{ type: "agent", agentId: reviewerAgentId }],
},
],
})!;
const issue = {
...makeIssue("todo"),
status: "in_review",
assigneeAgentId: reviewerAgentId,
executionPolicy: policy,
executionState: {
status: "pending",
currentStageId: policy.stages[0].id,
currentStageIndex: 0,
currentStageType: "review",
currentParticipant: { type: "agent", agentId: reviewerAgentId },
returnAssignee: {
type: "agent",
agentId: "22222222-2222-4222-8222-222222222222",
},
completedStageIds: [],
lastDecisionId: null,
lastDecisionOutcome: null,
},
};
const policy = await createAgentReviewPolicy(reviewerAgentId)!;
const issue = createReviewIssue(policy, reviewerAgentId);
const reviewBody = "Looks good.";
mockIssueService.getById.mockResolvedValue(issue);
mockIssueService.addComment.mockResolvedValue({
@@ -3545,15 +3399,7 @@ describe("issue comment reopen routes", () => {
it("does not auto-approve approval comments outside the in_review status", async () => {
const reviewerAgentId = "33333333-3333-4333-8333-333333333333";
const policy = await normalizePolicy({
stages: [
{
id: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
type: "review",
participants: [{ type: "agent", agentId: reviewerAgentId }],
},
],
})!;
const policy = await createAgentReviewPolicy(reviewerAgentId)!;
const issue = {
...makeIssue("todo"),
status: "in_progress",
@@ -3633,35 +3479,8 @@ describe("issue comment reopen routes", () => {
])("does not auto-approve negated approval phrasings ($name)", ({ body }) => {
it("rejects the auto-approval transition and keeps the comment as a regular comment", async () => {
const reviewerAgentId = "33333333-3333-4333-8333-333333333333";
const policy = await normalizePolicy({
stages: [
{
id: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
type: "review",
participants: [{ type: "agent", agentId: reviewerAgentId }],
},
],
})!;
const issue = {
...makeIssue("todo"),
status: "in_review",
assigneeAgentId: reviewerAgentId,
executionPolicy: policy,
executionState: {
status: "pending",
currentStageId: policy.stages[0].id,
currentStageIndex: 0,
currentStageType: "review",
currentParticipant: { type: "agent", agentId: reviewerAgentId },
returnAssignee: {
type: "agent",
agentId: "22222222-2222-4222-8222-222222222222",
},
completedStageIds: [],
lastDecisionId: null,
lastDecisionOutcome: null,
},
};
const policy = await createAgentReviewPolicy(reviewerAgentId)!;
const issue = createReviewIssue(policy, reviewerAgentId);
mockIssueService.getById.mockResolvedValue(issue);
mockIssueService.addComment.mockResolvedValue({
id: "comment-review-negated",
@@ -3705,35 +3524,8 @@ describe("issue comment reopen routes", () => {
])("auto-approves positive approval phrasings ($name)", ({ body }) => {
it("triggers the auto-approval transition", async () => {
const reviewerAgentId = "33333333-3333-4333-8333-333333333333";
const policy = await normalizePolicy({
stages: [
{
id: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
type: "review",
participants: [{ type: "agent", agentId: reviewerAgentId }],
},
],
})!;
const issue = {
...makeIssue("todo"),
status: "in_review",
assigneeAgentId: reviewerAgentId,
executionPolicy: policy,
executionState: {
status: "pending",
currentStageId: policy.stages[0].id,
currentStageIndex: 0,
currentStageType: "review",
currentParticipant: { type: "agent", agentId: reviewerAgentId },
returnAssignee: {
type: "agent",
agentId: "22222222-2222-4222-8222-222222222222",
},
completedStageIds: [],
lastDecisionId: null,
lastDecisionOutcome: null,
},
};
const policy = await createAgentReviewPolicy(reviewerAgentId)!;
const issue = createReviewIssue(policy, reviewerAgentId);
mockIssueService.getById.mockResolvedValue(issue);
mockIssueService.addComment.mockResolvedValue({
id: "comment-review-positive",
@@ -3794,35 +3586,8 @@ describe("issue comment reopen routes", () => {
it("rolls back the comment when the auto-approval status transition fails", async () => {
const reviewerAgentId = "33333333-3333-4333-8333-333333333333";
const policy = await normalizePolicy({
stages: [
{
id: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
type: "review",
participants: [{ type: "agent", agentId: reviewerAgentId }],
},
],
})!;
const issue = {
...makeIssue("todo"),
status: "in_review",
assigneeAgentId: reviewerAgentId,
executionPolicy: policy,
executionState: {
status: "pending",
currentStageId: policy.stages[0].id,
currentStageIndex: 0,
currentStageType: "review",
currentParticipant: { type: "agent", agentId: reviewerAgentId },
returnAssignee: {
type: "agent",
agentId: "22222222-2222-4222-8222-222222222222",
},
completedStageIds: [],
lastDecisionId: null,
lastDecisionOutcome: null,
},
};
const policy = await createAgentReviewPolicy(reviewerAgentId)!;
const issue = createReviewIssue(policy, reviewerAgentId);
const reviewBody = "## Review: PAP-580 - APPROVED\n\nLooks good.";
mockIssueService.getById.mockResolvedValue(issue);
mockIssueService.addComment.mockResolvedValue({
@@ -3869,35 +3634,8 @@ describe("issue comment reopen routes", () => {
it("rolls back the auto-approval comment when the issue is concurrently deleted", async () => {
const reviewerAgentId = "33333333-3333-4333-8333-333333333333";
const policy = await normalizePolicy({
stages: [
{
id: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
type: "review",
participants: [{ type: "agent", agentId: reviewerAgentId }],
},
],
})!;
const issue = {
...makeIssue("todo"),
status: "in_review",
assigneeAgentId: reviewerAgentId,
executionPolicy: policy,
executionState: {
status: "pending",
currentStageId: policy.stages[0].id,
currentStageIndex: 0,
currentStageType: "review",
currentParticipant: { type: "agent", agentId: reviewerAgentId },
returnAssignee: {
type: "agent",
agentId: "22222222-2222-4222-8222-222222222222",
},
completedStageIds: [],
lastDecisionId: null,
lastDecisionOutcome: null,
},
};
const policy = await createAgentReviewPolicy(reviewerAgentId)!;
const issue = createReviewIssue(policy, reviewerAgentId);
const reviewBody = "## Review: PAP-580 - APPROVED\n\nLooks good.";
mockIssueService.getById.mockResolvedValue(issue);
mockIssueService.addComment.mockResolvedValue({
@@ -81,17 +81,55 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
await tempDb?.cleanup();
});
function interactionCompanyRow(companyId: string): typeof companies.$inferInsert {
return {
id: companyId,
name: "Paperclip",
issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
requireBoardApprovalForNewAgents: false,
};
}
function parentIssueRow(issueId: string, companyId: string, goalId: string): typeof issues.$inferInsert {
return {
id: issueId,
companyId,
goalId,
title: "Parent issue",
status: "in_progress",
priority: "medium",
};
}
function acceptIssueScope(issueId: string, companyId: string, goalId: string) {
return {
id: issueId,
companyId,
goalId,
projectId: null,
};
}
function activeCodexAgentRow(agentId: string, companyId: string, name: string, role: string): typeof agents.$inferInsert {
return {
id: agentId,
companyId,
name,
role,
status: "active",
adapterType: "codex_local",
adapterConfig: {},
runtimeConfig: {},
permissions: {},
};
}
async function seedConfirmationIssue(title = "Comment supersede") {
const companyId = randomUUID();
const goalId = randomUUID();
const issueId = randomUUID();
await db.insert(companies).values({
id: companyId,
name: "Paperclip",
issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
requireBoardApprovalForNewAgents: false,
});
await db.insert(companies).values(interactionCompanyRow(companyId));
await instanceSettingsService(db).updateExperimental({ enableIsolatedWorkspaces: false });
await db.insert(goals).values({
id: goalId,
@@ -100,14 +138,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
level: "task",
status: "active",
});
await db.insert(issues).values({
id: issueId,
companyId,
goalId,
title: "Parent issue",
status: "in_progress",
priority: "medium",
});
await db.insert(issues).values(parentIssueRow(issueId, companyId, goalId));
return { companyId, goalId, issueId };
}
@@ -174,17 +205,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
const { companyId, issueId } = await seedConfirmationIssue("Source question race");
const agentId = randomUUID();
const runId = randomUUID();
await db.insert(agents).values({
id: agentId,
companyId,
name: "Questioner",
role: "engineer",
status: "active",
adapterType: "codex_local",
adapterConfig: {},
runtimeConfig: {},
permissions: {},
});
await db.insert(agents).values(activeCodexAgentRow(agentId, companyId, "Questioner", "engineer"));
await db.insert(heartbeatRuns).values({
id: runId,
companyId,
@@ -596,17 +617,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
it("rejects an explicitly mismatched source-run issue or agent", async () => {
const fixture = await seedSourceQuestionFixture({ paperclipWake: { comments: [] } });
const otherAgentId = randomUUID();
await db.insert(agents).values({
id: otherAgentId,
companyId: fixture.companyId,
name: "Other questioner",
role: "engineer",
status: "active",
adapterType: "codex_local",
adapterConfig: {},
runtimeConfig: {},
permissions: {},
});
await db.insert(agents).values(activeCodexAgentRow(otherAgentId, fixture.companyId, "Other questioner", "engineer"));
// Simulate historical corruption to retain the service-level defense test.
await db.transaction(async (tx) => {
@@ -764,17 +775,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
const agentId = randomUUID();
const firstRunId = randomUUID();
const secondRunId = randomUUID();
await db.insert(agents).values({
id: agentId,
companyId,
name: "Researcher",
role: "researcher",
status: "active",
adapterType: "codex_local",
adapterConfig: {},
runtimeConfig: {},
permissions: {},
});
await db.insert(agents).values(activeCodexAgentRow(agentId, companyId, "Researcher", "researcher"));
await db.update(issues).set({ assigneeAgentId: agentId, status: "in_progress" }).where(eq(issues.id, issueId));
await db.insert(heartbeatRuns).values([
{
@@ -1168,12 +1169,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
const assigneeAgentId = randomUUID();
const responsibleUserId = randomUUID();
await db.insert(companies).values({
id: companyId,
name: "Paperclip",
issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
requireBoardApprovalForNewAgents: false,
});
await db.insert(companies).values(interactionCompanyRow(companyId));
await instanceSettingsService(db).updateExperimental({ enableIsolatedWorkspaces: false });
await db.insert(goals).values({
@@ -1183,17 +1179,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
level: "task",
status: "active",
});
await db.insert(agents).values({
id: assigneeAgentId,
companyId,
name: "CodexCoder",
role: "engineer",
status: "active",
adapterType: "codex_local",
adapterConfig: {},
runtimeConfig: {},
permissions: {},
});
await db.insert(agents).values(activeCodexAgentRow(assigneeAgentId, companyId, "CodexCoder", "engineer"));
await db.insert(issues).values({
id: issueId,
companyId,
@@ -1233,12 +1219,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
expect(created.status).toBe("pending");
const accepted = await interactionsSvc.acceptSuggestedTasks({
id: issueId,
companyId,
goalId,
projectId: null,
}, created.id, {}, {
const accepted = await interactionsSvc.acceptSuggestedTasks(acceptIssueScope(issueId, companyId, goalId), created.id, {}, {
userId: "local-board",
});
@@ -1295,12 +1276,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
expect(listed).toHaveLength(1);
expect(listed[0]?.status).toBe("accepted");
await expect(interactionsSvc.acceptSuggestedTasks({
id: issueId,
companyId,
goalId,
projectId: null,
}, created.id, {}, {
await expect(interactionsSvc.acceptSuggestedTasks(acceptIssueScope(issueId, companyId, goalId), created.id, {}, {
userId: "local-board",
})).rejects.toThrow("Interaction has already been resolved");
@@ -1313,12 +1289,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
const goalId = randomUUID();
const issueId = randomUUID();
await db.insert(companies).values({
id: companyId,
name: "Paperclip",
issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
requireBoardApprovalForNewAgents: false,
});
await db.insert(companies).values(interactionCompanyRow(companyId));
await instanceSettingsService(db).updateExperimental({ enableIsolatedWorkspaces: false });
await db.insert(goals).values({
@@ -1366,12 +1337,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
userId: "local-board",
});
const accepted = await interactionsSvc.acceptSuggestedTasks({
id: issueId,
companyId,
goalId,
projectId: null,
}, created.id, {
const accepted = await interactionsSvc.acceptSuggestedTasks(acceptIssueScope(issueId, companyId, goalId), created.id, {
selectedClientKeys: ["root"],
}, {
userId: "local-board",
@@ -1395,12 +1361,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
const goalId = randomUUID();
const issueId = randomUUID();
await db.insert(companies).values({
id: companyId,
name: "Paperclip",
issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
requireBoardApprovalForNewAgents: false,
});
await db.insert(companies).values(interactionCompanyRow(companyId));
await instanceSettingsService(db).updateExperimental({ enableIsolatedWorkspaces: false });
await db.insert(goals).values({
@@ -1410,14 +1371,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
level: "task",
status: "active",
});
await db.insert(issues).values({
id: issueId,
companyId,
goalId,
title: "Parent issue",
status: "in_progress",
priority: "medium",
});
await db.insert(issues).values(parentIssueRow(issueId, companyId, goalId));
const created = await interactionsSvc.create({
id: issueId,
@@ -1444,12 +1398,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
});
await expect(
interactionsSvc.acceptSuggestedTasks({
id: issueId,
companyId,
goalId,
projectId: null,
}, created.id, {
interactionsSvc.acceptSuggestedTasks(acceptIssueScope(issueId, companyId, goalId), created.id, {
selectedClientKeys: ["child"],
}, {
userId: "local-board",
@@ -1462,12 +1411,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
const goalId = randomUUID();
const issueId = randomUUID();
await db.insert(companies).values({
id: companyId,
name: "Paperclip",
issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
requireBoardApprovalForNewAgents: false,
});
await db.insert(companies).values(interactionCompanyRow(companyId));
await instanceSettingsService(db).updateExperimental({ enableIsolatedWorkspaces: false });
await db.insert(goals).values({
@@ -1564,12 +1508,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
const goalId = randomUUID();
const issueId = randomUUID();
await db.insert(companies).values({
id: companyId,
name: "Paperclip",
issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
requireBoardApprovalForNewAgents: false,
});
await db.insert(companies).values(interactionCompanyRow(companyId));
await instanceSettingsService(db).updateExperimental({ enableIsolatedWorkspaces: false });
await db.insert(goals).values({
id: goalId,
@@ -1951,12 +1890,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
const agentId = randomUUID();
const runId = randomUUID();
await db.insert(companies).values({
id: companyId,
name: "Paperclip",
issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
requireBoardApprovalForNewAgents: false,
});
await db.insert(companies).values(interactionCompanyRow(companyId));
await instanceSettingsService(db).updateExperimental({ enableIsolatedWorkspaces: false });
await db.insert(goals).values({
@@ -1966,25 +1900,8 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
level: "task",
status: "active",
});
await db.insert(agents).values({
id: agentId,
companyId,
name: "CodexCoder",
role: "engineer",
status: "active",
adapterType: "codex_local",
adapterConfig: {},
runtimeConfig: {},
permissions: {},
});
await db.insert(issues).values({
id: issueId,
companyId,
goalId,
title: "Parent issue",
status: "in_progress",
priority: "medium",
});
await db.insert(agents).values(activeCodexAgentRow(agentId, companyId, "CodexCoder", "engineer"));
await db.insert(issues).values(parentIssueRow(issueId, companyId, goalId));
await db.insert(heartbeatRuns).values({
id: runId,
companyId,
@@ -2039,28 +1956,8 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
const firstAgentId = randomUUID();
const secondAgentId = randomUUID();
await db.insert(agents).values([
{
id: firstAgentId,
companyId,
name: "First agent",
role: "engineer",
status: "active",
adapterType: "codex_local",
adapterConfig: {},
runtimeConfig: {},
permissions: {},
},
{
id: secondAgentId,
companyId,
name: "Second agent",
role: "engineer",
status: "active",
adapterType: "codex_local",
adapterConfig: {},
runtimeConfig: {},
permissions: {},
},
activeCodexAgentRow(firstAgentId, companyId, "First agent", "engineer"),
activeCodexAgentRow(secondAgentId, companyId, "Second agent", "engineer"),
]);
const older = await interactionsSvc.create({ id: issueId, companyId }, {
@@ -2117,28 +2014,8 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
const probingAgentId = randomUUID();
const otherAgentId = randomUUID();
await db.insert(agents).values([
{
id: probingAgentId,
companyId,
name: "Probing agent",
role: "engineer",
status: "active",
adapterType: "codex_local",
adapterConfig: {},
runtimeConfig: {},
permissions: {},
},
{
id: otherAgentId,
companyId,
name: "Other agent",
role: "engineer",
status: "active",
adapterType: "codex_local",
adapterConfig: {},
runtimeConfig: {},
permissions: {},
},
activeCodexAgentRow(probingAgentId, companyId, "Probing agent", "engineer"),
activeCodexAgentRow(otherAgentId, companyId, "Other agent", "engineer"),
]);
const question = (prompt: string) => ({
@@ -2198,23 +2075,8 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
const goalId = randomUUID();
const issueId = randomUUID();
const agentId = randomUUID();
await db.insert(companies).values({
id: companyId,
name: "Paperclip",
issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
requireBoardApprovalForNewAgents: false,
});
await db.insert(agents).values({
id: agentId,
companyId,
name: "Chief of staff",
role: "chief_of_staff",
status: "active",
adapterType: "codex_local",
adapterConfig: {},
runtimeConfig: {},
permissions: {},
});
await db.insert(companies).values(interactionCompanyRow(companyId));
await db.insert(agents).values(activeCodexAgentRow(agentId, companyId, "Chief of staff", "chief_of_staff"));
await db.insert(goals).values({
id: goalId,
companyId,
@@ -2291,28 +2153,8 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
const firstAgentId = randomUUID();
const secondAgentId = randomUUID();
await db.insert(agents).values([
{
id: firstAgentId,
companyId,
name: "First agent",
role: "engineer",
status: "active",
adapterType: "codex_local",
adapterConfig: {},
runtimeConfig: {},
permissions: {},
},
{
id: secondAgentId,
companyId,
name: "Second agent",
role: "engineer",
status: "active",
adapterType: "codex_local",
adapterConfig: {},
runtimeConfig: {},
permissions: {},
},
activeCodexAgentRow(firstAgentId, companyId, "First agent", "engineer"),
activeCodexAgentRow(secondAgentId, companyId, "Second agent", "engineer"),
]);
const firstAgentIds = [randomUUID(), randomUUID(), randomUUID()];
@@ -2411,12 +2253,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
const goalId = randomUUID();
const issueId = randomUUID();
await db.insert(companies).values({
id: companyId,
name: "Paperclip",
issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
requireBoardApprovalForNewAgents: false,
});
await db.insert(companies).values(interactionCompanyRow(companyId));
await instanceSettingsService(db).updateExperimental({ enableIsolatedWorkspaces: false });
await db.insert(goals).values({
id: goalId,
@@ -2425,14 +2262,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
level: "task",
status: "active",
});
await db.insert(issues).values({
id: issueId,
companyId,
goalId,
title: "Parent issue",
status: "in_progress",
priority: "medium",
});
await db.insert(issues).values(parentIssueRow(issueId, companyId, goalId));
const created = await interactionsSvc.create({
id: issueId,
@@ -2454,12 +2284,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
expect(created.kind).toBe("request_confirmation");
expect(created.status).toBe("pending");
const accepted = await interactionsSvc.acceptInteraction({
id: issueId,
companyId,
goalId,
projectId: null,
}, created.id, {}, {
const accepted = await interactionsSvc.acceptInteraction(acceptIssueScope(issueId, companyId, goalId), created.id, {}, {
userId: "local-board",
});
@@ -2551,17 +2376,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
const resolverAgentId = randomUUID();
const resolverRunId = randomUUID();
await db.update(issues).set({ status: "in_review" }).where(eq(issues.id, issueId));
await db.insert(agents).values({
id: resolverAgentId,
companyId,
name: "Review agent",
role: "reviewer",
status: "active",
adapterType: "codex_local",
adapterConfig: {},
runtimeConfig: {},
permissions: {},
});
await db.insert(agents).values(activeCodexAgentRow(resolverAgentId, companyId, "Review agent", "reviewer"));
await db.insert(heartbeatRuns).values({
id: resolverRunId,
companyId,
@@ -2579,12 +2394,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
});
await recordReviewTransition({ companyId, issueId, interactionId: created.id });
const accepted = await interactionsSvc.acceptInteraction({
id: issueId,
companyId,
goalId,
projectId: null,
}, created.id, {}, {
const accepted = await interactionsSvc.acceptInteraction(acceptIssueScope(issueId, companyId, goalId), created.id, {}, {
agentId: resolverAgentId,
runId: resolverRunId,
resolverPolicyRestriction: "anyone",
@@ -2604,17 +2414,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
const { companyId, goalId, issueId } = await seedConfirmationIssue(`Locked ${action} policy`);
const resolverAgentId = randomUUID();
const resolverRunId = randomUUID();
await db.insert(agents).values({
id: resolverAgentId,
companyId,
name: "Review agent",
role: "reviewer",
status: "active",
adapterType: "codex_local",
adapterConfig: {},
runtimeConfig: {},
permissions: {},
});
await db.insert(agents).values(activeCodexAgentRow(resolverAgentId, companyId, "Review agent", "reviewer"));
await db.insert(heartbeatRuns).values({
id: resolverRunId,
companyId,
@@ -2705,17 +2505,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
const resolverAgentId = randomUUID();
const resolverRunId = randomUUID();
await db.update(issues).set({ status: "in_review" }).where(eq(issues.id, issueId));
await db.insert(agents).values({
id: resolverAgentId,
companyId,
name: "Review agent",
role: "reviewer",
status: "active",
adapterType: "codex_local",
adapterConfig: {},
runtimeConfig: {},
permissions: {},
});
await db.insert(agents).values(activeCodexAgentRow(resolverAgentId, companyId, "Review agent", "reviewer"));
await db.insert(heartbeatRuns).values({
id: resolverRunId,
companyId,
@@ -2806,12 +2596,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
},
});
const accepted = await interactionsSvc.acceptInteraction({
id: issueId,
companyId,
goalId,
projectId: null,
}, created.id, {
const accepted = await interactionsSvc.acceptInteraction(acceptIssueScope(issueId, companyId, goalId), created.id, {
selectedOptionIds: ["file-c", "file-a"],
}, {
userId: "local-board",
@@ -2854,34 +2639,19 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
userId: "local-board",
});
await expect(interactionsSvc.acceptInteraction({
id: issueId,
companyId,
goalId,
projectId: null,
}, created.id, {
await expect(interactionsSvc.acceptInteraction(acceptIssueScope(issueId, companyId, goalId), created.id, {
selectedOptionIds: [],
}, {
userId: "local-board",
})).rejects.toThrow("Select at least 1 checkbox confirmation option(s)");
await expect(interactionsSvc.acceptInteraction({
id: issueId,
companyId,
goalId,
projectId: null,
}, created.id, {
await expect(interactionsSvc.acceptInteraction(acceptIssueScope(issueId, companyId, goalId), created.id, {
selectedOptionIds: ["missing"],
}, {
userId: "local-board",
})).rejects.toThrow("Unknown checkbox confirmation optionId: missing");
await expect(interactionsSvc.acceptInteraction({
id: issueId,
companyId,
goalId,
projectId: null,
}, created.id, {
await expect(interactionsSvc.acceptInteraction(acceptIssueScope(issueId, companyId, goalId), created.id, {
selectedOptionIds: ["one", "two", "three"],
}, {
userId: "local-board",
@@ -3217,12 +2987,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
const issueId = randomUUID();
const agentId = randomUUID();
await db.insert(companies).values({
id: companyId,
name: "Paperclip",
issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
requireBoardApprovalForNewAgents: false,
});
await db.insert(companies).values(interactionCompanyRow(companyId));
await instanceSettingsService(db).updateExperimental({ enableIsolatedWorkspaces: false });
await db.insert(goals).values({
id: goalId,
@@ -3231,17 +2996,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
level: "task",
status: "active",
});
await db.insert(agents).values({
id: agentId,
companyId,
name: "Senior Product Engineer",
role: "engineer",
status: "active",
adapterType: "codex_local",
adapterConfig: {},
runtimeConfig: {},
permissions: {},
});
await db.insert(agents).values(activeCodexAgentRow(agentId, companyId, "Senior Product Engineer", "engineer"));
await db.insert(issues).values({
id: issueId,
companyId,
@@ -3268,12 +3023,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
agentId,
});
const accepted = await interactionsSvc.acceptInteraction({
id: issueId,
companyId,
goalId,
projectId: null,
}, created.id, {}, {
const accepted = await interactionsSvc.acceptInteraction(acceptIssueScope(issueId, companyId, goalId), created.id, {}, {
userId: "local-board",
});
@@ -3315,12 +3065,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
agentId,
});
const resumed = await interactionsSvc.acceptInteraction({
id: issueId,
companyId,
goalId,
projectId: null,
}, agentOwnedConfirmation.id, {}, {
const resumed = await interactionsSvc.acceptInteraction(acceptIssueScope(issueId, companyId, goalId), agentOwnedConfirmation.id, {}, {
userId: "local-board",
});
@@ -3358,12 +3103,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
agentId,
});
const acceptedWhileActive = await interactionsSvc.acceptInteraction({
id: issueId,
companyId,
goalId,
projectId: null,
}, activeConfirmation.id, {}, {
const acceptedWhileActive = await interactionsSvc.acceptInteraction(acceptIssueScope(issueId, companyId, goalId), activeConfirmation.id, {}, {
userId: "local-board",
});
@@ -3380,17 +3120,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
it("atomically returns an accepted Plan-mode issue to its agent in Auto mode", async () => {
const { companyId, goalId, issueId } = await seedConfirmationIssue("Accept a plan into Auto mode");
const agentId = randomUUID();
await db.insert(agents).values({
id: agentId,
companyId,
name: "Plan owner",
role: "engineer",
status: "active",
adapterType: "codex_local",
adapterConfig: {},
runtimeConfig: {},
permissions: {},
});
await db.insert(agents).values(activeCodexAgentRow(agentId, companyId, "Plan owner", "engineer"));
await db.update(issues).set({
status: "in_review",
workMode: "planning",
@@ -3403,12 +3133,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
payload: { version: 1, prompt: "Accept this plan?", target },
}, { agentId });
const accepted = await interactionsSvc.acceptInteraction({
id: issueId,
companyId,
goalId,
projectId: null,
}, created.id, {}, { userId: "local-board" });
const accepted = await interactionsSvc.acceptInteraction(acceptIssueScope(issueId, companyId, goalId), created.id, {}, { userId: "local-board" });
expect(accepted.interaction).toMatchObject({
id: created.id,
@@ -3433,17 +3158,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
it("keeps Plan mode for non-plan and checkbox confirmations", async () => {
const { companyId, goalId, issueId } = await seedConfirmationIssue("Do not auto-transition other confirmations");
const agentId = randomUUID();
await db.insert(agents).values({
id: agentId,
companyId,
name: "Plan owner",
role: "engineer",
status: "active",
adapterType: "codex_local",
adapterConfig: {},
runtimeConfig: {},
permissions: {},
});
await db.insert(agents).values(activeCodexAgentRow(agentId, companyId, "Plan owner", "engineer"));
await db.update(issues).set({
status: "in_review",
workMode: "planning",
@@ -3454,7 +3169,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
kind: "request_confirmation",
payload: { version: 1, prompt: "Accept this unrelated decision?" },
}, { agentId });
await interactionsSvc.acceptInteraction({ id: issueId, companyId, goalId, projectId: null }, nonPlan.id, {}, {
await interactionsSvc.acceptInteraction(acceptIssueScope(issueId, companyId, goalId), nonPlan.id, {}, {
userId: "local-board",
});
await expect(db.select().from(issues).where(eq(issues.id, issueId)).then((rows) => rows[0]?.workMode))
@@ -3471,7 +3186,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
target,
},
}, { agentId });
await interactionsSvc.acceptInteraction({ id: issueId, companyId, goalId, projectId: null }, checkbox.id, {
await interactionsSvc.acceptInteraction(acceptIssueScope(issueId, companyId, goalId), checkbox.id, {
selectedOptionIds: ["phase-1"],
}, { userId: "local-board" });
await expect(db.select().from(issues).where(eq(issues.id, issueId)).then((rows) => rows[0]?.workMode))
@@ -3487,7 +3202,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
payload: { version: 1, prompt: "Accept this plan?", target },
}, { userId: "local-board" });
await interactionsSvc.acceptInteraction({ id: issueId, companyId, goalId, projectId: null }, created.id, {}, {
await interactionsSvc.acceptInteraction(acceptIssueScope(issueId, companyId, goalId), created.id, {}, {
userId: "local-board",
});
@@ -3929,12 +3644,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
const revisionId = randomUUID();
const nextRevisionId = randomUUID();
await db.insert(companies).values({
id: companyId,
name: "Paperclip",
issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
requireBoardApprovalForNewAgents: false,
});
await db.insert(companies).values(interactionCompanyRow(companyId));
await instanceSettingsService(db).updateExperimental({ enableIsolatedWorkspaces: false });
await db.insert(goals).values({
id: goalId,
@@ -3943,14 +3653,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
level: "task",
status: "active",
});
await db.insert(issues).values({
id: issueId,
companyId,
goalId,
title: "Parent issue",
status: "in_progress",
priority: "medium",
});
await db.insert(issues).values(parentIssueRow(issueId, companyId, goalId));
await db.insert(documents).values({
id: documentId,
companyId,
@@ -4013,12 +3716,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
latestRevisionNumber: 2,
});
await expect(interactionsSvc.acceptInteraction({
id: issueId,
companyId,
goalId,
projectId: null,
}, created.id, {}, {
await expect(interactionsSvc.acceptInteraction(acceptIssueScope(issueId, companyId, goalId), created.id, {}, {
userId: "local-board",
})).rejects.toMatchObject({
status: 409,
@@ -4057,12 +3755,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
const revisionId = randomUUID();
const nextRevisionId = randomUUID();
await db.insert(companies).values({
id: companyId,
name: "Paperclip",
issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
requireBoardApprovalForNewAgents: false,
});
await db.insert(companies).values(interactionCompanyRow(companyId));
await instanceSettingsService(db).updateExperimental({ enableIsolatedWorkspaces: false });
await db.insert(goals).values({
id: goalId,
@@ -4071,14 +3764,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
level: "task",
status: "active",
});
await db.insert(issues).values({
id: issueId,
companyId,
goalId,
title: "Parent issue",
status: "in_progress",
priority: "medium",
});
await db.insert(issues).values(parentIssueRow(issueId, companyId, goalId));
await db.update(issues).set({ workMode: "planning" }).where(eq(issues.id, issueId));
// Document is already at revision 2 — revision 1 is stale.
await db.insert(documents).values({
@@ -4172,12 +3858,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
userId: "local-board",
});
expect(created).toMatchObject({ status: "pending", kind: "request_confirmation" });
await expect(interactionsSvc.acceptInteraction({
id: issueId,
companyId,
goalId,
projectId: null,
}, created.id, {}, {
await expect(interactionsSvc.acceptInteraction(acceptIssueScope(issueId, companyId, goalId), created.id, {}, {
userId: "local-board",
})).resolves.toMatchObject({
interaction: { status: "accepted" },
@@ -4196,12 +3877,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
const revisionId = randomUUID();
const nextRevisionId = randomUUID();
await db.insert(companies).values({
id: companyId,
name: "Paperclip",
issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
requireBoardApprovalForNewAgents: false,
});
await db.insert(companies).values(interactionCompanyRow(companyId));
await instanceSettingsService(db).updateExperimental({ enableIsolatedWorkspaces: false });
await db.insert(goals).values({
id: goalId,
@@ -4210,14 +3886,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
level: "task",
status: "active",
});
await db.insert(issues).values({
id: issueId,
companyId,
goalId,
title: "Parent issue",
status: "in_progress",
priority: "medium",
});
await db.insert(issues).values(parentIssueRow(issueId, companyId, goalId));
await db.insert(documents).values({
id: documentId,
companyId,
@@ -4357,12 +4026,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
const foreignRunId = randomUUID();
const kind = options?.kind ?? "request_confirmation";
await db.insert(companies).values({
id: companyId,
name: "Paperclip",
issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
requireBoardApprovalForNewAgents: false,
});
await db.insert(companies).values(interactionCompanyRow(companyId));
await instanceSettingsService(db).updateExperimental({ enableIsolatedWorkspaces: false });
await db.insert(projects).values({
id: projectId,
@@ -4379,17 +4043,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
visibility: "default",
isPrimary: true,
});
await db.insert(agents).values({
id: agentId,
companyId,
name: "CodexCoder",
role: "engineer",
status: "active",
adapterType: "codex_local",
adapterConfig: {},
runtimeConfig: {},
permissions: {},
});
await db.insert(agents).values(activeCodexAgentRow(agentId, companyId, "CodexCoder", "engineer"));
const sourceRunStatus = options?.sourceRunStatus ?? "succeeded";
const sourceRunTerminal = sourceRunStatus !== "running";
await db.insert(heartbeatRuns).values([
@@ -4596,7 +4250,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
});
const accepted = await interactionsSvc.acceptInteraction(
{ id: issueId, companyId, goalId, projectId: null },
acceptIssueScope(issueId, companyId, goalId),
interactionId,
{},
{ userId: "local-board" },
@@ -4624,7 +4278,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
await expect(
interactionsSvc.acceptInteraction(
{ id: issueId, companyId, goalId, projectId: null },
acceptIssueScope(issueId, companyId, goalId),
interactionId,
{},
{ userId: "local-board" },
@@ -4654,7 +4308,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
});
const accepted = await interactionsSvc.acceptInteraction(
{ id: issueId, companyId, goalId, projectId: null },
acceptIssueScope(issueId, companyId, goalId),
interactionId,
{},
{ userId: "local-board" },
@@ -4692,7 +4346,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
});
const accepted = await interactionsSvc.acceptInteraction(
{ id: issueId, companyId, goalId, projectId: null },
acceptIssueScope(issueId, companyId, goalId),
interactionId,
{},
{ userId: "local-board" },
@@ -4722,7 +4376,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
});
const accepted = await interactionsSvc.acceptInteraction(
{ id: issueId, companyId, goalId, projectId: null },
acceptIssueScope(issueId, companyId, goalId),
interactionId,
{},
{ userId: "local-board" },
@@ -4752,7 +4406,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
await expect(
interactionsSvc.acceptInteraction(
{ id: issueId, companyId, goalId, projectId: null },
acceptIssueScope(issueId, companyId, goalId),
interactionId,
{},
{ userId: "local-board" },
@@ -4780,7 +4434,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
});
const accepted = await interactionsSvc.acceptInteraction(
{ id: issueId, companyId, goalId, projectId: null },
acceptIssueScope(issueId, companyId, goalId),
interactionId,
{},
{ userId: "local-board" },
@@ -4815,7 +4469,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
});
const accepted = await interactionsSvc.acceptInteraction(
{ id: issueId, companyId, goalId, projectId: null },
acceptIssueScope(issueId, companyId, goalId),
interactionId,
{ selectedOptionIds: ["file-b"] },
{ userId: "local-board" },
@@ -4846,7 +4500,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
await expect(
interactionsSvc.acceptInteraction(
{ id: issueId, companyId, goalId, projectId: null },
acceptIssueScope(issueId, companyId, goalId),
interactionId,
{ selectedOptionIds: ["file-a"] },
{ userId: "local-board" },
@@ -4869,7 +4523,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
});
const accepted = await interactionsSvc.acceptInteraction(
{ id: issueId, companyId, goalId, projectId: null },
acceptIssueScope(issueId, companyId, goalId),
interactionId,
{ selectedOptionIds: ["file-a"] },
{ userId: "local-board" },
@@ -4896,7 +4550,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
});
const accepted = await interactionsSvc.acceptInteraction(
{ id: issueId, companyId, goalId, projectId: null },
acceptIssueScope(issueId, companyId, goalId),
interactionId,
{ selectedOptionIds: ["file-a"] },
{ userId: "local-board" },
@@ -4946,7 +4600,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
});
const accepted = await interactionsSvc.acceptInteraction(
{ id: issueId, companyId, goalId, projectId: null },
acceptIssueScope(issueId, companyId, goalId),
created.id,
{},
{ userId: "local-board" },
@@ -6,6 +6,11 @@ import {
stripSecretBearingUrlParts,
} from "../middleware/redact-sensitive.js";
it("redacts Slack manager grants and sealed OAuth handoffs", () => {
const result = redactSensitive({ managerToken: "manager-canary", refreshToken: "refresh-canary", sealed: { ct: "sealed-canary" }, handoff: { token: "handoff-canary" }, claim_id: "claim-canary" });
expect(JSON.stringify(result)).not.toContain("canary");
});
describe("redactSensitive", () => {
it("redacts a plaintext password field on a sign-in body", () => {
const body = {
@@ -63,7 +63,6 @@ describe("removeTestHome", () => {
const home = makeHome();
fs.mkdirSync(path.join(home, "instances", "default"), { recursive: true });
fs.writeFileSync(path.join(home, "instances", "default", "paperclip.json"), "{}\n");
// An unrelated ENOTEMPTY must not cause permission changes or be swallowed.
const error = Object.assign(new Error("unrelated nonempty directory"), { code: "ENOTEMPTY" });
const removal = vi.spyOn(fs, "rmSync").mockImplementation(() => { throw error; });
const permissionChange = vi.spyOn(fs, "chmodSync");
+1 -5
View File
@@ -9,8 +9,7 @@ import path from "node:path";
* in them read-only (`makeDirectoriesReadOnly` in
* services/native-runtime/runtime-context.ts), and a run that is still
* finishing when the worker exits can leave such a bundle under the worker's
* home. Removing a file inside a read-only directory fails with `EACCES`
* or, on macOS, the recursive removal can report `ENOTEMPTY` instead.
* home. Removing a file inside a read-only directory fails with `EACCES`,
* Vitest reports it as an unhandled error, and the shard fails after every
* test passed. So on a permission error every directory in the tree is made
* writable first and the removal is tried once more.
@@ -23,8 +22,6 @@ export function removeTestHome(home: string): void {
const permissionError = isPermissionError(error);
if (!permissionError && (error as NodeJS.ErrnoException | null)?.code !== "ENOTEMPTY") throw error;
const restored = makeDirectoriesWritable(home);
// ENOTEMPTY also has unrelated causes; retry only after restoring an
// actual read-only directory, and still surface a failed second removal.
if (!permissionError && !restored) throw error;
fs.rmSync(home, options);
}
@@ -51,7 +48,6 @@ function makeDirectoriesWritable(directory: string): boolean {
fs.chmodSync(directory, stat.mode | 0o700);
restored = true;
} catch {
// A directory that cannot be changed still fails the removal below, loudly.
}
}
let entries: fs.Dirent[];
+171 -436
View File
@@ -184,6 +184,46 @@ describeEmbeddedPostgres("secretService", () => {
rmSync(secretsTmpDir, { recursive: true, force: true });
});
function awsVaultInput(): Parameters<ReturnType<typeof secretService>["createProviderConfig"]>[1] {
return {
provider: "aws_secrets_manager",
displayName: "AWS production",
config: { region: "us-east-1", namespace: "prod-use1" },
};
}
function preparedAwsVersion(externalRef: string, versionId: string, valueSha256: string, fingerprintSha256: string): Awaited<ReturnType<typeof awsSecretsManagerProvider.createSecret>> {
return {
material: {
scheme: "aws_secrets_manager_v1",
secretId: externalRef,
versionId,
source: "managed",
},
valueSha256,
fingerprintSha256,
externalRef,
providerVersionRef: versionId,
};
}
function managedAwsVersionRow(secretId: string, externalRef: string): typeof companySecretVersions.$inferInsert {
return {
secretId,
version: 1,
material: {
scheme: "aws_secrets_manager_v1",
secretId: externalRef,
versionId: "aws-version-1",
source: "managed",
},
valueSha256: "value-sha-1",
fingerprintSha256: "fingerprint-sha-1",
providerVersionRef: "aws-version-1",
status: "current",
};
}
async function seedCompany(name = "Acme") {
const companyId = randomUUID();
await db.insert(companies).values({
@@ -925,52 +965,62 @@ describeEmbeddedPostgres("secretService", () => {
// provider is `local_encrypted`.
const companyId = await seedCompany();
const svc = secretService(db);
const awsVault = await svc.createProviderConfig(companyId, {
provider: "aws_secrets_manager",
displayName: "AWS production",
config: { region: "us-east-1", namespace: "prod-use1" },
});
vi.spyOn(awsSecretsManagerProvider, "createSecret").mockResolvedValue({
material: {
scheme: "aws_secrets_manager_v1",
secretId: "arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/company/aws-secret",
versionId: "aws-version-1",
source: "managed",
},
valueSha256: "value-sha-1",
fingerprintSha256: "fingerprint-sha-1",
externalRef: "arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/company/aws-secret",
providerVersionRef: "aws-version-1",
});
const awsVault = await svc.createProviderConfig(companyId, awsVaultInput());
const createSecretSpy = vi.spyOn(awsSecretsManagerProvider, "createSecret").mockResolvedValue(preparedAwsVersion("arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/company/aws-secret", "aws-version-1", "value-sha-1", "fingerprint-sha-1"));
const originalProviderWrite = createSecretSpy.getMockImplementation()!;
const uncontendedEntryDurationMs = await measureUncontendedEntryDurationMs(
createSecretSpy,
originalProviderWrite,
() => svc.create(companyId, {
name: `aws-baseline-${randomUUID()}`,
provider: "aws_secrets_manager",
providerConfigId: awsVault.id,
value: "baseline-runtime-secret",
}),
);
const events: string[] = [];
const cleanupEntered = deferred<void>();
const providerEntered = deferred<void>();
createSecretSpy.mockImplementationOnce(async (input) => {
events.push("aws-provider-enter");
providerEntered.resolve();
return originalProviderWrite(input);
});
let releaseCleanup!: () => void;
const cleanupGate = new Promise<void>((resolve) => {
releaseCleanup = resolve;
});
const cleanupCall = withAccountHomeSecretMutationLock(undefined, companyId, async () => {
events.push("cleanup-enter");
cleanupEntered.resolve();
await cleanupGate;
events.push("cleanup-exit");
});
// Give the cleanup a chance to acquire the lock before the AWS create
// starts racing for the same lock.
await new Promise((resolve) => setTimeout(resolve, 20));
const createCall = svc.create(companyId, {
name: `aws-secret-${randomUUID()}`,
provider: "aws_secrets_manager",
providerConfigId: awsVault.id,
value: "runtime-secret",
});
// The AWS create must stay queued behind the held lock, the same way a
// `local_encrypted` create does.
await new Promise((resolve) => setTimeout(resolve, 20));
expect(events).toEqual(["cleanup-enter"]);
releaseCleanup();
await cleanupCall;
const created = await createCall;
expect(events).toEqual(["cleanup-enter", "cleanup-exit"]);
let createCall: ReturnType<typeof svc.create> | undefined;
let outcomes: PromiseSettledResult<unknown>[] = [];
try {
await awaitEntryOrOperationFailure(cleanupEntered.promise, cleanupCall, "cleanupCall");
createCall = svc.create(companyId, {
name: `aws-secret-${randomUUID()}`,
provider: "aws_secrets_manager",
providerConfigId: awsVault.id,
value: "runtime-secret",
});
void createCall.catch(() => {});
await waitEntryDetectionWindow(uncontendedEntryDurationMs, providerEntered.promise);
expect(events).toEqual(["cleanup-enter"]);
expect(createSecretSpy).toHaveBeenCalledTimes(ENTRY_DETECTION_BASELINE_SAMPLE_COUNT);
} finally {
releaseCleanup();
outcomes = await Promise.allSettled([cleanupCall, createCall]);
}
for (const outcome of outcomes) {
if (outcome.status === "rejected") throw outcome.reason;
}
const created = await createCall!;
expect(events).toEqual(["cleanup-enter", "cleanup-exit", "aws-provider-enter"]);
expect(created.status).toBe("active");
});
@@ -980,35 +1030,9 @@ describeEmbeddedPostgres("secretService", () => {
// `local_encrypted` rotate.
const companyId = await seedCompany();
const svc = secretService(db);
const awsVault = await svc.createProviderConfig(companyId, {
provider: "aws_secrets_manager",
displayName: "AWS production",
config: { region: "us-east-1", namespace: "prod-use1" },
});
vi.spyOn(awsSecretsManagerProvider, "createSecret").mockResolvedValue({
material: {
scheme: "aws_secrets_manager_v1",
secretId: "arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/company/aws-secret-rotate",
versionId: "aws-version-1",
source: "managed",
},
valueSha256: "value-sha-1",
fingerprintSha256: "fingerprint-sha-1",
externalRef: "arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/company/aws-secret-rotate",
providerVersionRef: "aws-version-1",
});
vi.spyOn(awsSecretsManagerProvider, "createVersion").mockResolvedValue({
material: {
scheme: "aws_secrets_manager_v1",
secretId: "arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/company/aws-secret-rotate",
versionId: "aws-version-2",
source: "managed",
},
valueSha256: "value-sha-2",
fingerprintSha256: "fingerprint-sha-2",
externalRef: "arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/company/aws-secret-rotate",
providerVersionRef: "aws-version-2",
});
const awsVault = await svc.createProviderConfig(companyId, awsVaultInput());
vi.spyOn(awsSecretsManagerProvider, "createSecret").mockResolvedValue(preparedAwsVersion("arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/company/aws-secret-rotate", "aws-version-1", "value-sha-1", "fingerprint-sha-1"));
const createVersionSpy = vi.spyOn(awsSecretsManagerProvider, "createVersion").mockResolvedValue(preparedAwsVersion("arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/company/aws-secret-rotate", "aws-version-2", "value-sha-2", "fingerprint-sha-2"));
const existing = await svc.create(companyId, {
name: `aws-secret-rotate-${randomUUID()}`,
provider: "aws_secrets_manager",
@@ -1016,25 +1040,55 @@ describeEmbeddedPostgres("secretService", () => {
value: "runtime-secret",
});
const baselineSecret = await svc.create(companyId, {
name: `aws-baseline-${randomUUID()}`,
provider: "aws_secrets_manager",
providerConfigId: awsVault.id,
value: "baseline-runtime-secret",
});
const originalProviderWrite = createVersionSpy.getMockImplementation()!;
const uncontendedEntryDurationMs = await measureUncontendedEntryDurationMs(
createVersionSpy,
originalProviderWrite,
() => svc.rotate(baselineSecret.id, { value: "baseline-rotated-runtime-secret" }),
);
const events: string[] = [];
const cleanupEntered = deferred<void>();
const providerEntered = deferred<void>();
createVersionSpy.mockImplementationOnce(async (input) => {
events.push("aws-provider-enter");
providerEntered.resolve();
return originalProviderWrite(input);
});
let releaseCleanup!: () => void;
const cleanupGate = new Promise<void>((resolve) => {
releaseCleanup = resolve;
});
const cleanupCall = withAccountHomeSecretMutationLock(undefined, companyId, async () => {
events.push("cleanup-enter");
cleanupEntered.resolve();
await cleanupGate;
events.push("cleanup-exit");
});
await new Promise((resolve) => setTimeout(resolve, 20));
const rotateCall = svc.rotate(existing.id, { value: "rotated-runtime-secret" });
await new Promise((resolve) => setTimeout(resolve, 20));
expect(events).toEqual(["cleanup-enter"]);
releaseCleanup();
await cleanupCall;
const rotated = await rotateCall;
expect(events).toEqual(["cleanup-enter", "cleanup-exit"]);
let rotateCall: ReturnType<typeof svc.rotate> | undefined;
let outcomes: PromiseSettledResult<unknown>[] = [];
try {
await awaitEntryOrOperationFailure(cleanupEntered.promise, cleanupCall, "cleanupCall");
rotateCall = svc.rotate(existing.id, { value: "rotated-runtime-secret" });
void rotateCall.catch(() => {});
await waitEntryDetectionWindow(uncontendedEntryDurationMs, providerEntered.promise);
expect(events).toEqual(["cleanup-enter"]);
expect(createVersionSpy).toHaveBeenCalledTimes(ENTRY_DETECTION_BASELINE_SAMPLE_COUNT);
} finally {
releaseCleanup();
outcomes = await Promise.allSettled([cleanupCall, rotateCall]);
}
for (const outcome of outcomes) {
if (outcome.status === "rejected") throw outcome.reason;
}
const rotated = await rotateCall!;
expect(events).toEqual(["cleanup-enter", "cleanup-exit", "aws-provider-enter"]);
expect(rotated.latestVersion).toBe(2);
});
@@ -2008,11 +2062,7 @@ describeEmbeddedPostgres("secretService", () => {
const companyId = await seedCompany();
await seedCompanyMember(companyId, "user-1", "owner");
const svc = secretService(db);
const awsVault = await svc.createProviderConfig(companyId, {
provider: "aws_secrets_manager",
displayName: "AWS production",
config: { region: "us-east-1", namespace: "prod-use1" },
});
const awsVault = await svc.createProviderConfig(companyId, awsVaultInput());
const definition = await svc.createUserSecretDefinition(companyId, {
key: "github_token",
name: "GitHub token",
@@ -2070,11 +2120,7 @@ describeEmbeddedPostgres("secretService", () => {
const companyId = await seedCompany();
await seedCompanyMember(companyId, "user-1", "owner");
const svc = secretService(db);
const awsVault = await svc.createProviderConfig(companyId, {
provider: "aws_secrets_manager",
displayName: "AWS production",
config: { region: "us-east-1", namespace: "prod-use1" },
});
const awsVault = await svc.createProviderConfig(companyId, awsVaultInput());
const definition = await svc.createUserSecretDefinition(companyId, {
key: "github_token",
name: "GitHub token",
@@ -2083,18 +2129,7 @@ describeEmbeddedPostgres("secretService", () => {
});
const externalRef =
"arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/user/github-token";
vi.spyOn(awsSecretsManagerProvider, "createSecret").mockResolvedValue({
material: {
scheme: "aws_secrets_manager_v1",
secretId: externalRef,
versionId: "aws-version-1",
source: "managed",
},
valueSha256: "value-sha-1",
fingerprintSha256: "fingerprint-sha-1",
externalRef,
providerVersionRef: "aws-version-1",
});
vi.spyOn(awsSecretsManagerProvider, "createSecret").mockResolvedValue(preparedAwsVersion(externalRef, "aws-version-1", "value-sha-1", "fingerprint-sha-1"));
vi.spyOn(awsSecretsManagerProvider, "deleteOrArchive").mockResolvedValue();
vi.spyOn(db, "transaction").mockRejectedValueOnce(new Error("db activate failed"));
vi.spyOn(db, "delete").mockImplementationOnce(() => {
@@ -2164,11 +2199,7 @@ describeEmbeddedPostgres("secretService", () => {
await seedCompanyMember(companyId, "user-1", "owner");
await seedCompanyMember(companyId, "user-2", "member");
const svc = secretService(db);
const awsVault = await svc.createProviderConfig(companyId, {
provider: "aws_secrets_manager",
displayName: "AWS production",
config: { region: "us-east-1", namespace: "prod-use1" },
});
const awsVault = await svc.createProviderConfig(companyId, awsVaultInput());
const definition = await svc.createUserSecretDefinition(companyId, {
key: "github_token",
name: "GitHub token",
@@ -2180,18 +2211,7 @@ describeEmbeddedPostgres("secretService", () => {
nextVersion += 1;
const externalRef =
`arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/${input.context.secretKey}`;
return {
material: {
scheme: "aws_secrets_manager_v1",
secretId: externalRef,
versionId: `aws-version-${nextVersion}`,
source: "managed",
},
valueSha256: `value-sha-${nextVersion}`,
fingerprintSha256: `fingerprint-sha-${nextVersion}`,
externalRef,
providerVersionRef: `aws-version-${nextVersion}`,
};
return preparedAwsVersion(externalRef, `aws-version-${nextVersion}`, `value-sha-${nextVersion}`, `fingerprint-sha-${nextVersion}`);
});
const deleteSpy = vi.spyOn(awsSecretsManagerProvider, "deleteOrArchive").mockResolvedValue();
const userOneSecret = await svc.createCurrentUserSecretValue(companyId, "user-1", {
@@ -2245,29 +2265,14 @@ describeEmbeddedPostgres("secretService", () => {
const companyId = await seedCompany();
await seedCompanyMember(companyId, "user-1", "owner");
const svc = secretService(db);
const awsVault = await svc.createProviderConfig(companyId, {
provider: "aws_secrets_manager",
displayName: "AWS production",
config: { region: "us-east-1", namespace: "prod-use1" },
});
const awsVault = await svc.createProviderConfig(companyId, awsVaultInput());
const definition = await svc.createUserSecretDefinition(companyId, {
key: "github_token",
name: "GitHub token",
provider: "aws_secrets_manager",
providerConfigId: awsVault.id,
});
vi.spyOn(awsSecretsManagerProvider, "createSecret").mockResolvedValue({
material: {
scheme: "aws_secrets_manager_v1",
secretId: "arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/user-secret",
versionId: "aws-version-1",
source: "managed",
},
valueSha256: "value-sha-1",
fingerprintSha256: "fingerprint-sha-1",
externalRef: "arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/user-secret",
providerVersionRef: "aws-version-1",
});
vi.spyOn(awsSecretsManagerProvider, "createSecret").mockResolvedValue(preparedAwsVersion("arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/user-secret", "aws-version-1", "value-sha-1", "fingerprint-sha-1"));
const deleteSpy = vi.spyOn(awsSecretsManagerProvider, "deleteOrArchive").mockResolvedValue();
const userSecret = await svc.createCurrentUserSecretValue(companyId, "user-1", {
definitionId: definition.id,
@@ -2622,11 +2627,7 @@ describeEmbeddedPostgres("secretService", () => {
it("stores external references without requiring or persisting secret values", async () => {
const companyId = await seedCompany();
const svc = secretService(db);
const awsVault = await svc.createProviderConfig(companyId, {
provider: "aws_secrets_manager",
displayName: "AWS production",
config: { region: "us-east-1", namespace: "prod-use1" },
});
const awsVault = await svc.createProviderConfig(companyId, awsVaultInput());
const secret = await svc.create(companyId, {
name: `external-${randomUUID()}`,
@@ -2727,11 +2728,7 @@ describeEmbeddedPostgres("secretService", () => {
it("removes provider vault config locally without deleting remote AWS secrets", async () => {
const companyId = await seedCompany();
const svc = secretService(db);
const vault = await svc.createProviderConfig(companyId, {
provider: "aws_secrets_manager",
displayName: "AWS production",
config: { region: "us-east-1", namespace: "prod-use1" },
});
const vault = await svc.createProviderConfig(companyId, awsVaultInput());
const secret = await svc.create(companyId, {
name: `external-${randomUUID()}`,
provider: "aws_secrets_manager",
@@ -2782,11 +2779,7 @@ describeEmbeddedPostgres("secretService", () => {
it("rejects bindings and env refs to soft-deleted external reference secrets", async () => {
const companyId = await seedCompany();
const svc = secretService(db);
const awsVault = await svc.createProviderConfig(companyId, {
provider: "aws_secrets_manager",
displayName: "AWS production",
config: { region: "us-east-1", namespace: "prod-use1" },
});
const awsVault = await svc.createProviderConfig(companyId, awsVaultInput());
const deleted = await svc.create(companyId, {
name: "Deleted external",
key: "deleted-external",
@@ -2816,11 +2809,7 @@ describeEmbeddedPostgres("secretService", () => {
it("rejects updates to already soft-deleted external reference secrets", async () => {
const companyId = await seedCompany();
const svc = secretService(db);
const awsVault = await svc.createProviderConfig(companyId, {
provider: "aws_secrets_manager",
displayName: "AWS production",
config: { region: "us-east-1", namespace: "prod-use1" },
});
const awsVault = await svc.createProviderConfig(companyId, awsVaultInput());
const deleted = await svc.create(companyId, {
name: "Deleted patch target",
key: "deleted-patch-target",
@@ -2839,11 +2828,7 @@ describeEmbeddedPostgres("secretService", () => {
it("allows re-importing a remote secret after the prior external reference is soft-deleted", async () => {
const companyId = await seedCompany();
const svc = secretService(db);
const awsVault = await svc.createProviderConfig(companyId, {
provider: "aws_secrets_manager",
displayName: "AWS production",
config: { region: "us-east-1", namespace: "prod-use1" },
});
const awsVault = await svc.createProviderConfig(companyId, awsVaultInput());
const externalRef =
"arn:aws:secretsmanager:us-east-1:123456789012:secret:prod/reimportable";
const deleted = await svc.create(companyId, {
@@ -2892,11 +2877,7 @@ describeEmbeddedPostgres("secretService", () => {
it("ignores soft-deleted name and key conflicts during remote import", async () => {
const companyId = await seedCompany();
const svc = secretService(db);
const awsVault = await svc.createProviderConfig(companyId, {
provider: "aws_secrets_manager",
displayName: "AWS production",
config: { region: "us-east-1", namespace: "prod-use1" },
});
const awsVault = await svc.createProviderConfig(companyId, awsVaultInput());
const deleted = await svc.create(companyId, {
name: "Deleted external",
key: "deleted-external",
@@ -2992,30 +2973,8 @@ describeEmbeddedPostgres("secretService", () => {
},
});
const createSpy = vi.spyOn(awsSecretsManagerProvider, "createSecret").mockResolvedValue({
material: {
scheme: "aws_secrets_manager_v1",
secretId: "arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/company/openai-api-key",
versionId: "aws-version-1",
source: "managed",
},
valueSha256: "value-sha-1",
fingerprintSha256: "fingerprint-sha-1",
externalRef: "arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/company/openai-api-key",
providerVersionRef: "aws-version-1",
});
const createVersionSpy = vi.spyOn(awsSecretsManagerProvider, "createVersion").mockResolvedValue({
material: {
scheme: "aws_secrets_manager_v1",
secretId: "arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/company/openai-api-key",
versionId: "aws-version-2",
source: "managed",
},
valueSha256: "value-sha-2",
fingerprintSha256: "fingerprint-sha-2",
externalRef: "arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/company/openai-api-key",
providerVersionRef: "aws-version-2",
});
const createSpy = vi.spyOn(awsSecretsManagerProvider, "createSecret").mockResolvedValue(preparedAwsVersion("arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/company/openai-api-key", "aws-version-1", "value-sha-1", "fingerprint-sha-1"));
const createVersionSpy = vi.spyOn(awsSecretsManagerProvider, "createVersion").mockResolvedValue(preparedAwsVersion("arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/company/openai-api-key", "aws-version-2", "value-sha-2", "fingerprint-sha-2"));
const resolveSpy = vi.spyOn(awsSecretsManagerProvider, "resolveVersion").mockResolvedValue("resolved-secret");
const secret = await svc.create(companyId, {
@@ -3048,25 +3007,8 @@ describeEmbeddedPostgres("secretService", () => {
it("cleans up managed provider secrets when create persistence fails", async () => {
const companyId = await seedCompany();
const svc = secretService(db);
const awsVault = await svc.createProviderConfig(companyId, {
provider: "aws_secrets_manager",
displayName: "AWS production",
config: { region: "us-east-1", namespace: "prod-use1" },
});
const prepared = {
material: {
scheme: "aws_secrets_manager_v1",
secretId:
"arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/company/create-rollback",
versionId: "aws-version-1",
source: "managed",
},
valueSha256: "value-sha-1",
fingerprintSha256: "fingerprint-sha-1",
externalRef:
"arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/company/create-rollback",
providerVersionRef: "aws-version-1",
};
const awsVault = await svc.createProviderConfig(companyId, awsVaultInput());
const prepared = preparedAwsVersion("arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/company/create-rollback", "aws-version-1", "value-sha-1", "fingerprint-sha-1");
vi.spyOn(awsSecretsManagerProvider, "createSecret").mockResolvedValue(prepared);
const deleteSpy = vi.spyOn(awsSecretsManagerProvider, "deleteOrArchive").mockResolvedValue();
vi.spyOn(db, "transaction").mockRejectedValueOnce(new Error("db insert failed"));
@@ -3103,25 +3045,8 @@ describeEmbeddedPostgres("secretService", () => {
it("keeps a local cleanup handle when create rollback cleanup fails", async () => {
const companyId = await seedCompany();
const svc = secretService(db);
const awsVault = await svc.createProviderConfig(companyId, {
provider: "aws_secrets_manager",
displayName: "AWS production",
config: { region: "us-east-1", namespace: "prod-use1" },
});
const prepared = {
material: {
scheme: "aws_secrets_manager_v1",
secretId:
"arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/company/create-cleanup-handle",
versionId: "aws-version-1",
source: "managed",
},
valueSha256: "value-sha-1",
fingerprintSha256: "fingerprint-sha-1",
externalRef:
"arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/company/create-cleanup-handle",
providerVersionRef: "aws-version-1",
};
const awsVault = await svc.createProviderConfig(companyId, awsVaultInput());
const prepared = preparedAwsVersion("arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/company/create-cleanup-handle", "aws-version-1", "value-sha-1", "fingerprint-sha-1");
vi.spyOn(awsSecretsManagerProvider, "createSecret").mockResolvedValue(prepared);
vi.spyOn(awsSecretsManagerProvider, "deleteOrArchive").mockRejectedValue(
new Error("cleanup failed"),
@@ -3171,25 +3096,8 @@ describeEmbeddedPostgres("secretService", () => {
it("reports managed secret persistence rollback failures when local cleanup cannot remove the reserved row", async () => {
const companyId = await seedCompany();
const svc = secretService(db);
const awsVault = await svc.createProviderConfig(companyId, {
provider: "aws_secrets_manager",
displayName: "AWS production",
config: { region: "us-east-1", namespace: "prod-use1" },
});
const prepared = {
material: {
scheme: "aws_secrets_manager_v1",
secretId:
"arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/company/create-local-cleanup",
versionId: "aws-version-1",
source: "managed",
},
valueSha256: "value-sha-1",
fingerprintSha256: "fingerprint-sha-1",
externalRef:
"arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/company/create-local-cleanup",
providerVersionRef: "aws-version-1",
};
const awsVault = await svc.createProviderConfig(companyId, awsVaultInput());
const prepared = preparedAwsVersion("arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/company/create-local-cleanup", "aws-version-1", "value-sha-1", "fingerprint-sha-1");
vi.spyOn(awsSecretsManagerProvider, "createSecret").mockResolvedValue(prepared);
vi.spyOn(awsSecretsManagerProvider, "deleteOrArchive").mockResolvedValue();
vi.spyOn(db, "transaction").mockRejectedValueOnce(new Error("db activate failed"));
@@ -3227,25 +3135,8 @@ describeEmbeddedPostgres("secretService", () => {
it("archives managed provider versions when rotate persistence fails", async () => {
const companyId = await seedCompany();
const svc = secretService(db);
const awsVault = await svc.createProviderConfig(companyId, {
provider: "aws_secrets_manager",
displayName: "AWS production",
config: { region: "us-east-1", namespace: "prod-use1" },
});
vi.spyOn(awsSecretsManagerProvider, "createSecret").mockResolvedValue({
material: {
scheme: "aws_secrets_manager_v1",
secretId:
"arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/company/rotate-rollback",
versionId: "aws-version-1",
source: "managed",
},
valueSha256: "value-sha-1",
fingerprintSha256: "fingerprint-sha-1",
externalRef:
"arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/company/rotate-rollback",
providerVersionRef: "aws-version-1",
});
const awsVault = await svc.createProviderConfig(companyId, awsVaultInput());
vi.spyOn(awsSecretsManagerProvider, "createSecret").mockResolvedValue(preparedAwsVersion("arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/company/rotate-rollback", "aws-version-1", "value-sha-1", "fingerprint-sha-1"));
const secret = await svc.create(companyId, {
name: "Rotate Rollback",
key: "rotate-rollback",
@@ -3253,20 +3144,7 @@ describeEmbeddedPostgres("secretService", () => {
providerConfigId: awsVault.id,
value: "runtime-secret",
});
const prepared = {
material: {
scheme: "aws_secrets_manager_v1",
secretId:
"arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/company/rotate-rollback",
versionId: "aws-version-2",
source: "managed",
},
valueSha256: "value-sha-2",
fingerprintSha256: "fingerprint-sha-2",
externalRef:
"arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/company/rotate-rollback",
providerVersionRef: "aws-version-2",
};
const prepared = preparedAwsVersion("arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/company/rotate-rollback", "aws-version-2", "value-sha-2", "fingerprint-sha-2");
vi.spyOn(awsSecretsManagerProvider, "createVersion").mockResolvedValue(prepared);
const deleteSpy = vi.spyOn(awsSecretsManagerProvider, "deleteOrArchive").mockResolvedValue();
vi.spyOn(db, "transaction").mockRejectedValueOnce(new Error("db rotate failed"));
@@ -3292,25 +3170,8 @@ describeEmbeddedPostgres("secretService", () => {
it("keeps a disabled version cleanup handle when rotate rollback cleanup fails", async () => {
const companyId = await seedCompany();
const svc = secretService(db);
const awsVault = await svc.createProviderConfig(companyId, {
provider: "aws_secrets_manager",
displayName: "AWS production",
config: { region: "us-east-1", namespace: "prod-use1" },
});
vi.spyOn(awsSecretsManagerProvider, "createSecret").mockResolvedValue({
material: {
scheme: "aws_secrets_manager_v1",
secretId:
"arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/company/rotate-cleanup-handle",
versionId: "aws-version-1",
source: "managed",
},
valueSha256: "value-sha-1",
fingerprintSha256: "fingerprint-sha-1",
externalRef:
"arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/company/rotate-cleanup-handle",
providerVersionRef: "aws-version-1",
});
const awsVault = await svc.createProviderConfig(companyId, awsVaultInput());
vi.spyOn(awsSecretsManagerProvider, "createSecret").mockResolvedValue(preparedAwsVersion("arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/company/rotate-cleanup-handle", "aws-version-1", "value-sha-1", "fingerprint-sha-1"));
const secret = await svc.create(companyId, {
name: "Rotate Cleanup Handle",
key: "rotate-cleanup-handle",
@@ -3318,20 +3179,7 @@ describeEmbeddedPostgres("secretService", () => {
providerConfigId: awsVault.id,
value: "runtime-secret",
});
const prepared = {
material: {
scheme: "aws_secrets_manager_v1",
secretId:
"arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/company/rotate-cleanup-handle",
versionId: "aws-version-2",
source: "managed",
},
valueSha256: "value-sha-2",
fingerprintSha256: "fingerprint-sha-2",
externalRef:
"arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/company/rotate-cleanup-handle",
providerVersionRef: "aws-version-2",
};
const prepared = preparedAwsVersion("arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/company/rotate-cleanup-handle", "aws-version-2", "value-sha-2", "fingerprint-sha-2");
vi.spyOn(awsSecretsManagerProvider, "createVersion").mockResolvedValue(prepared);
vi.spyOn(awsSecretsManagerProvider, "deleteOrArchive").mockRejectedValue(
new Error("cleanup failed"),
@@ -3372,20 +3220,7 @@ describeEmbeddedPostgres("secretService", () => {
displayName: "AWS secondary",
config: { region: "us-west-2", namespace: "prod-usw2" },
});
vi.spyOn(awsSecretsManagerProvider, "createSecret").mockResolvedValue({
material: {
scheme: "aws_secrets_manager_v1",
secretId:
"arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/company/vault-reassign",
versionId: "aws-version-1",
source: "managed",
},
valueSha256: "value-sha-1",
fingerprintSha256: "fingerprint-sha-1",
externalRef:
"arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/company/vault-reassign",
providerVersionRef: "aws-version-1",
});
vi.spyOn(awsSecretsManagerProvider, "createSecret").mockResolvedValue(preparedAwsVersion("arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/company/vault-reassign", "aws-version-1", "value-sha-1", "fingerprint-sha-1"));
const secret = await svc.create(companyId, {
name: "Vault Reassign",
key: "vault-reassign",
@@ -3426,11 +3261,7 @@ describeEmbeddedPostgres("secretService", () => {
it("previews AWS remote import candidates with duplicate and collision enrichment", async () => {
const companyId = await seedCompany();
const svc = secretService(db);
const awsVault = await svc.createProviderConfig(companyId, {
provider: "aws_secrets_manager",
displayName: "AWS production",
config: { region: "us-east-1", namespace: "prod-use1" },
});
const awsVault = await svc.createProviderConfig(companyId, awsVaultInput());
const duplicate = await svc.create(companyId, {
name: "Existing duplicate",
provider: "aws_secrets_manager",
@@ -3504,11 +3335,7 @@ describeEmbeddedPostgres("secretService", () => {
it("sanitizes AWS remote import preview provider errors before crossing the service boundary", async () => {
const companyId = await seedCompany();
const svc = secretService(db);
const awsVault = await svc.createProviderConfig(companyId, {
provider: "aws_secrets_manager",
displayName: "AWS production",
config: { region: "us-east-1", namespace: "prod-use1" },
});
const awsVault = await svc.createProviderConfig(companyId, awsVaultInput());
const rawProviderMessage =
"AccessDeniedException: User: arn:aws:sts::123456789012:assumed-role/prod/Paperclip is not authorized to perform secretsmanager:ListSecrets";
@@ -3542,11 +3369,7 @@ describeEmbeddedPostgres("secretService", () => {
it("sanitizes AWS managed secret create failures and removes the reserved row", async () => {
const companyId = await seedCompany();
const svc = secretService(db);
const awsVault = await svc.createProviderConfig(companyId, {
provider: "aws_secrets_manager",
displayName: "AWS production",
config: { region: "us-east-1", namespace: "prod-use1" },
});
const awsVault = await svc.createProviderConfig(companyId, awsVaultInput());
const rawProviderMessage =
"AccessDeniedException: User: arn:aws:sts::123456789012:assumed-role/prod/Paperclip is not authorized to perform secretsmanager:CreateSecret on arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1";
@@ -3600,11 +3423,7 @@ describeEmbeddedPostgres("secretService", () => {
it("reports rollback failures when AWS managed secret create cleanup cannot remove the reserved row", async () => {
const companyId = await seedCompany();
const svc = secretService(db);
const awsVault = await svc.createProviderConfig(companyId, {
provider: "aws_secrets_manager",
displayName: "AWS production",
config: { region: "us-east-1", namespace: "prod-use1" },
});
const awsVault = await svc.createProviderConfig(companyId, awsVaultInput());
vi.spyOn(awsSecretsManagerProvider, "createSecret").mockRejectedValueOnce(
new SecretProviderClientError({
@@ -3777,11 +3596,7 @@ describeEmbeddedPostgres("secretService", () => {
it("writes external reference rotations through the provider when a value is given", async () => {
const companyId = await seedCompany();
const svc = secretService(db);
const awsVault = await svc.createProviderConfig(companyId, {
provider: "aws_secrets_manager",
displayName: "AWS production",
config: { region: "us-east-1", namespace: "prod-use1" },
});
const awsVault = await svc.createProviderConfig(companyId, awsVaultInput());
const externalRef = "arn:aws:secretsmanager:us-east-1:123456789012:secret:shared/neon-admin";
const secret = await svc.create(companyId, {
name: `external-${randomUUID()}`,
@@ -3832,11 +3647,7 @@ describeEmbeddedPostgres("secretService", () => {
it("restores the provider current version when external value rotation persistence fails", async () => {
const companyId = await seedCompany();
const svc = secretService(db);
const awsVault = await svc.createProviderConfig(companyId, {
provider: "aws_secrets_manager",
displayName: "AWS production",
config: { region: "us-east-1", namespace: "prod-use1" },
});
const awsVault = await svc.createProviderConfig(companyId, awsVaultInput());
const externalRef = "arn:aws:secretsmanager:us-east-1:123456789012:secret:shared/rollback";
const secret = await svc.create(companyId, {
name: "External rollback",
@@ -3885,11 +3696,7 @@ describeEmbeddedPostgres("secretService", () => {
it("rejects external value rotations that also retarget or pin versions", async () => {
const companyId = await seedCompany();
const svc = secretService(db);
const awsVault = await svc.createProviderConfig(companyId, {
provider: "aws_secrets_manager",
displayName: "AWS production",
config: { region: "us-east-1", namespace: "prod-use1" },
});
const awsVault = await svc.createProviderConfig(companyId, awsVaultInput());
const secret = await svc.create(companyId, {
name: `external-${randomUUID()}`,
provider: "aws_secrets_manager",
@@ -3927,11 +3734,7 @@ describeEmbeddedPostgres("secretService", () => {
it("imports AWS remote references row-by-row without fetching plaintext", async () => {
const companyId = await seedCompany();
const svc = secretService(db);
const awsVault = await svc.createProviderConfig(companyId, {
provider: "aws_secrets_manager",
displayName: "AWS production",
config: { region: "us-east-1", namespace: "prod-use1" },
});
const awsVault = await svc.createProviderConfig(companyId, awsVaultInput());
const duplicate = await svc.create(companyId, {
name: "Existing duplicate",
provider: "aws_secrets_manager",
@@ -4000,11 +3803,7 @@ describeEmbeddedPostgres("secretService", () => {
it("sanitizes AWS remote import row provider errors", async () => {
const companyId = await seedCompany();
const svc = secretService(db);
const awsVault = await svc.createProviderConfig(companyId, {
provider: "aws_secrets_manager",
displayName: "AWS production",
config: { region: "us-east-1", namespace: "prod-use1" },
});
const awsVault = await svc.createProviderConfig(companyId, awsVaultInput());
const rawProviderMessage =
"AccessDeniedException: User: arn:aws:sts::123456789012:assumed-role/prod/Paperclip is not authorized to perform secretsmanager:DescribeSecret on arn:aws:secretsmanager:us-east-1:123456789012:secret:prod/openai";
vi.spyOn(awsSecretsManagerProvider, "linkExternalSecret").mockRejectedValueOnce(
@@ -4047,11 +3846,7 @@ describeEmbeddedPostgres("secretService", () => {
it("rejects Paperclip-managed AWS namespace refs during preview and import commit", async () => {
const companyId = await seedCompany();
const svc = secretService(db);
const awsVault = await svc.createProviderConfig(companyId, {
provider: "aws_secrets_manager",
displayName: "AWS production",
config: { region: "us-east-1", namespace: "prod-use1" },
});
const awsVault = await svc.createProviderConfig(companyId, awsVaultInput());
vi.spyOn(awsSecretsManagerProvider, "listRemoteSecrets").mockResolvedValue({
secrets: [
@@ -4112,11 +3907,7 @@ describeEmbeddedPostgres("secretService", () => {
it("skips duplicate AWS remote imports for the same provider vault and canonical ref", async () => {
const companyId = await seedCompany();
const svc = secretService(db);
const awsVault = await svc.createProviderConfig(companyId, {
provider: "aws_secrets_manager",
displayName: "AWS production",
config: { region: "us-east-1", namespace: "prod-use1" },
});
const awsVault = await svc.createProviderConfig(companyId, awsVaultInput());
const first = await svc.importRemoteSecrets(companyId, {
providerConfigId: awsVault.id,
@@ -4200,11 +3991,7 @@ describeEmbeddedPostgres("secretService", () => {
it("rejects generic update retargeting for external reference secrets", async () => {
const companyId = await seedCompany();
const svc = secretService(db);
const awsVault = await svc.createProviderConfig(companyId, {
provider: "aws_secrets_manager",
displayName: "AWS production",
config: { region: "us-east-1", namespace: "prod-use1" },
});
const awsVault = await svc.createProviderConfig(companyId, awsVaultInput());
const secret = await svc.create(companyId, {
name: `external-${randomUUID()}`,
provider: "aws_secrets_manager",
@@ -4263,20 +4050,7 @@ describeEmbeddedPostgres("secretService", () => {
.returning()
.then((rows) => rows[0]);
await db.insert(companySecretVersions).values({
secretId: secret.id,
version: 1,
material: {
scheme: "aws_secrets_manager_v1",
secretId: externalRef,
versionId: "aws-version-1",
source: "managed",
},
valueSha256: "value-sha-1",
fingerprintSha256: "fingerprint-sha-1",
providerVersionRef: "aws-version-1",
status: "current",
});
await db.insert(companySecretVersions).values(managedAwsVersionRow(secret.id, externalRef));
const deleteSpy = vi.spyOn(awsSecretsManagerProvider, "deleteOrArchive").mockResolvedValue();
@@ -4329,20 +4103,7 @@ describeEmbeddedPostgres("secretService", () => {
.returning()
.then((rows) => rows[0]);
await db.insert(companySecretVersions).values({
secretId: secret.id,
version: 1,
material: {
scheme: "aws_secrets_manager_v1",
secretId: externalRef,
versionId: "aws-version-1",
source: "managed",
},
valueSha256: "value-sha-1",
fingerprintSha256: "fingerprint-sha-1",
providerVersionRef: "aws-version-1",
status: "current",
});
await db.insert(companySecretVersions).values(managedAwsVersionRow(secret.id, externalRef));
vi.spyOn(awsSecretsManagerProvider, "deleteOrArchive").mockRejectedValueOnce(
new Error("provider delete failed"),
);
@@ -4386,20 +4147,7 @@ describeEmbeddedPostgres("secretService", () => {
status: "deleted",
deletedAt: new Date(),
});
await db.insert(companySecretVersions).values({
secretId,
version: 1,
material: {
scheme: "aws_secrets_manager_v1",
secretId: externalRef,
versionId: "aws-version-1",
source: "managed",
},
valueSha256: "value-sha-1",
fingerprintSha256: "fingerprint-sha-1",
providerVersionRef: "aws-version-1",
status: "current",
});
await db.insert(companySecretVersions).values(managedAwsVersionRow(secretId, externalRef));
const deleteSpy = vi.spyOn(awsSecretsManagerProvider, "deleteOrArchive").mockRejectedValueOnce(
new SecretProviderClientError({
code: "not_found",
@@ -4449,20 +4197,7 @@ describeEmbeddedPostgres("secretService", () => {
.returning()
.then((rows) => rows[0]);
await db.insert(companySecretVersions).values({
secretId: secret.id,
version: 1,
material: {
scheme: "aws_secrets_manager_v1",
secretId: externalRef,
versionId: "aws-version-1",
source: "managed",
},
valueSha256: "value-sha-1",
fingerprintSha256: "fingerprint-sha-1",
providerVersionRef: "aws-version-1",
status: "current",
});
await db.insert(companySecretVersions).values(managedAwsVersionRow(secret.id, externalRef));
await svc.disableProviderConfig(vault.id);
const deleteSpy = vi.spyOn(awsSecretsManagerProvider, "deleteOrArchive").mockResolvedValue();
@@ -0,0 +1,73 @@
import { chmodSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest";
import { SLACK_MANAGER_SCOPES } from "@paperclipai/shared";
import { developmentSlackManager } from "../services/connectors/slack/setup/development-manager.js";
const roots: string[] = [];
afterEach(() => roots.splice(0).forEach(path => rmSync(path, { recursive: true, force: true })));
function fixture() {
const root = mkdtempSync(join(tmpdir(), "slack-development-")); roots.push(root);
const path = join(root, "manager.json");
writeFileSync(path, JSON.stringify({ appId: "A123", clientId: "123.456", clientSecret: "client-secret-canary" }), { mode: 0o600 });
return { path, env: { NODE_ENV: "development", PAPERCLIP_SLACK_MANAGED_SETUP_ENABLED: "true", PAPERCLIP_SLACK_MANAGER_DEVELOPMENT_CONFIG: path } };
}
const origin = () => "https://local-test.trycloudflare.com";
const identity = { subject: "actor", companyId: "company", profile: "slack.manager" as const };
const user = { id: "U123", token_type: "user", access_token: "access-canary", refresh_token: "refresh-canary", expires_in: 43200, scope: SLACK_MANAGER_SCOPES.join(",") };
const response = (value: unknown) => new Response(JSON.stringify(value), { headers: { "content-type": "application/json" } });
describe("local managed Slack qualification", () => {
it("requires explicit development opt-in and refuses production", () => {
const { env } = fixture();
expect(developmentSlackManager(origin, {})).toBeNull();
expect(developmentSlackManager(origin, { ...env, NODE_ENV: "production" })).toBeNull();
expect(developmentSlackManager(origin, { ...env, PAPERCLIP_SLACK_MANAGED_SETUP_ENABLED: "false" })).toBeNull();
});
it("rejects readable secrets and invalid HTTPS origins", async () => {
const { env, path } = fixture(); chmodSync(path, 0o644);
expect(() => developmentSlackManager(origin, env)).toThrow();
chmodSync(path, 0o600);
await expect(developmentSlackManager(() => "http://local.test", env)!.getSlackManagerAppId()).rejects.toThrow();
});
it("builds a scoped authorization URL without exposing the client secret", async () => {
const { env } = fixture();
const client = developmentSlackManager(origin, env)!;
const result = await client.startAuthorization({ ...identity, returnUri: `${origin()}/api/chat-slack/managed/oauth/callback`, returnState: "bound-state" });
const url = new URL(result.authorizationUrl);
expect(url.origin).toBe("https://slack.com");
expect(url.searchParams.get("state")).toBe("bound-state");
expect(url.searchParams.get("user_scope")).toBe(SLACK_MANAGER_SCOPES.join(","));
expect(JSON.stringify(result)).not.toContain("client-secret-canary");
await expect(client.startAuthorization({ ...identity, returnUri: "https://other.test/callback", returnState: "bound-state" })).rejects.toThrow();
});
it("exchanges the code against the exact callback and preserves actor/workspace identity", async () => {
const { env } = fixture();
const fetcher = vi.fn().mockResolvedValue(response({ ok: true, app_id: "A123", team: { id: "T123", name: "Paperclip" }, authed_user: user }));
const result = await developmentSlackManager(origin, env, fetcher)!.claim({ ...identity, claimId: "code-canary", redemptionId: "once" });
expect(result.slackManager).toEqual({ appId: "A123", workspaceId: "T123", workspaceName: "Paperclip", userId: "U123" });
expect(result.subject).toBe("actor"); expect(result.companyId).toBe("company");
const [url, request] = fetcher.mock.calls[0];
expect(url).toBe("https://slack.com/api/oauth.v2.access");
expect(request.redirect).toBe("error");
expect(request.body.get("redirect_uri")).toBe(`${origin()}/api/chat-slack/managed/oauth/callback`);
expect(request.body.get("code")).toBe("code-canary");
});
it.each(["wrong-app", "missing-scope", "provider-echo"])("rejects %s without echoing secrets", async reason => {
const { env } = fixture();
const value = reason === "provider-echo" ? { ok: false, error: "client-secret-canary code-canary" } : { ok: true, app_id: reason === "wrong-app" ? "AOTHER" : "A123", team: { id: "T123", name: "Paperclip" }, authed_user: { ...user, scope: reason === "missing-scope" ? "" : user.scope } };
const client = developmentSlackManager(origin, env, vi.fn().mockResolvedValue(response(value)))!;
const error = await client.claim({ ...identity, claimId: "code-canary", redemptionId: "once" }).catch(error => error);
expect(error).toBeInstanceOf(Error);
expect(String(error)).not.toMatch(/canary/);
});
it("rotates refresh credentials and verifies revocation", async () => {
const { env } = fixture();
const fetcher = vi.fn().mockResolvedValueOnce(response({ ok: true, ...user })).mockResolvedValueOnce(response({ ok: false, error: "invalid_auth" }));
const client = developmentSlackManager(origin, env, fetcher)!;
expect((await client.refresh({ ...identity, refreshToken: "old-refresh" })).refreshToken).toBe("refresh-canary");
expect(fetcher.mock.calls[0][1].body.get("grant_type")).toBe("refresh_token");
await expect(client.revoke({ ...identity, token: "access-canary" })).rejects.toThrow();
});
});
@@ -149,6 +149,56 @@ async function createApprovedToolAction(input: {
return { decisionInput, invocation: recorded.invocation, actionRequest: updated };
}
type IdRef = { id: string };
function sendEmailGrantRow(company: IdRef, agent: IdRef): typeof principalPermissionGrants.$inferInsert {
return {
companyId: company.id,
principalType: "agent",
principalId: agent.id,
permissionKey: "tools:use",
scope: { toolName: "send_email" },
};
}
function agentProfileBindingRow(company: IdRef, profile: IdRef, agent: IdRef): typeof toolProfileBindings.$inferInsert {
return {
companyId: company.id,
profileId: profile.id,
targetType: "agent",
targetId: agent.id,
};
}
function sendEmailDecisionInput(company: IdRef, agent: IdRef, connection: IdRef, catalogEntry: IdRef): Parameters<ReturnType<typeof toolAccessPolicyService>["decide"]>[0] {
return {
companyId: company.id,
actor: { actorType: "agent", actorId: agent.id, agentId: agent.id },
request: { connectionId: connection.id, catalogEntryId: catalogEntry.id, toolName: "send_email" },
};
}
function reviewSendPolicyRow(company: IdRef, name: string): typeof toolPolicies.$inferInsert {
return {
companyId: company.id,
name: name,
policyType: "require_approval",
selectors: { toolName: "send_email" },
};
}
function executedActionInput(db: ReturnType<typeof createDb>, company: IdRef, agent: IdRef, connection: IdRef, catalogEntry: IdRef, args: Record<string, unknown>): Parameters<typeof createApprovedToolAction>[0] {
return {
db,
companyId: company.id,
agentId: agent.id,
connectionId: connection.id,
catalogEntryId: catalogEntry.id,
argumentsValue: args,
status: "executed",
};
}
describeEmbeddedPostgres("tool access policy service", () => {
let db!: ReturnType<typeof createDb>;
let tempDb: Awaited<ReturnType<typeof startEmbeddedPostgresTestDatabase>> | null = null;
@@ -190,11 +240,7 @@ describeEmbeddedPostgres("tool access policy service", () => {
const agent = await createAgent(db, company.id);
const { connection, catalogEntry } = await createTool(db, company.id);
const result = await toolAccessPolicyService(db).decide({
companyId: company.id,
actor: { actorType: "agent", actorId: agent.id, agentId: agent.id },
request: { connectionId: connection.id, catalogEntryId: catalogEntry.id, toolName: "send_email" },
});
const result = await toolAccessPolicyService(db).decide(sendEmailDecisionInput(company, agent, connection, catalogEntry));
expect(result).toMatchObject({
allowed: false,
@@ -213,12 +259,7 @@ describeEmbeddedPostgres("tool access policy service", () => {
name: "Write tools",
defaultAction: "deny",
}).returning().then((rows) => rows[0]!);
await db.insert(toolProfileBindings).values({
companyId: company.id,
profileId: profile.id,
targetType: "agent",
targetId: agent.id,
});
await db.insert(toolProfileBindings).values(agentProfileBindingRow(company, profile, agent));
await db.insert(toolProfileEntries).values({
companyId: company.id,
profileId: profile.id,
@@ -227,11 +268,7 @@ describeEmbeddedPostgres("tool access policy service", () => {
toolName: "send_email",
});
const result = await toolAccessPolicyService(db).decide({
companyId: company.id,
actor: { actorType: "agent", actorId: agent.id, agentId: agent.id },
request: { connectionId: connection.id, catalogEntryId: catalogEntry.id, toolName: "send_email" },
});
const result = await toolAccessPolicyService(db).decide(sendEmailDecisionInput(company, agent, connection, catalogEntry));
expect(result).toMatchObject({
allowed: true,
@@ -468,11 +505,7 @@ describeEmbeddedPostgres("tool access policy service", () => {
]);
for (const agent of [draftAgent, archivedAgent]) {
const result = await toolAccessPolicyService(db).decide({
companyId: company.id,
actor: { actorType: "agent", actorId: agent.id, agentId: agent.id },
request: { connectionId: connection.id, catalogEntryId: catalogEntry.id, toolName: "send_email" },
});
const result = await toolAccessPolicyService(db).decide(sendEmailDecisionInput(company, agent, connection, catalogEntry));
expect(result).toMatchObject({
allowed: false,
decision: "deny",
@@ -486,13 +519,7 @@ describeEmbeddedPostgres("tool access policy service", () => {
const company = await createCompany(db);
const agent = await createAgent(db, company.id);
const { application, connection, catalogEntry } = await createTool(db, company.id);
await db.insert(principalPermissionGrants).values({
companyId: company.id,
principalType: "agent",
principalId: agent.id,
permissionKey: "tools:use",
scope: { toolName: "send_email" },
});
await db.insert(principalPermissionGrants).values(sendEmailGrantRow(company, agent));
const input = {
companyId: company.id,
actor: { actorType: "agent" as const, actorId: agent.id, agentId: agent.id },
@@ -615,12 +642,7 @@ describeEmbeddedPostgres("tool access policy service", () => {
name: "Write tools",
defaultAction: "allow",
}).returning().then((rows) => rows[0]!);
await db.insert(toolProfileBindings).values({
companyId: company.id,
profileId: profile.id,
targetType: "agent",
targetId: agent.id,
});
await db.insert(toolProfileBindings).values(agentProfileBindingRow(company, profile, agent));
const wildcardPolicy = await db.insert(toolPolicies).values({
companyId: company.id,
name: "Review wildcard-looking sends",
@@ -825,13 +847,7 @@ describeEmbeddedPostgres("tool access policy service", () => {
const company = await createCompany(db);
const agent = await createAgent(db, company.id);
const { connection } = await createTool(db, company.id);
await db.insert(principalPermissionGrants).values({
companyId: company.id,
principalType: "agent",
principalId: agent.id,
permissionKey: "tools:use",
scope: { toolName: "send_email" },
});
await db.insert(principalPermissionGrants).values(sendEmailGrantRow(company, agent));
const input = {
companyId: company.id,
actor: { actorType: "agent" as const, actorId: agent.id, agentId: agent.id },
@@ -857,13 +873,7 @@ describeEmbeddedPostgres("tool access policy service", () => {
const company = await createCompany(db);
const agent = await createAgent(db, company.id);
const { connection } = await createTool(db, company.id);
await db.insert(principalPermissionGrants).values({
companyId: company.id,
principalType: "agent",
principalId: agent.id,
permissionKey: "tools:use",
scope: { toolName: "send_email" },
});
await db.insert(principalPermissionGrants).values(sendEmailGrantRow(company, agent));
const input = {
companyId: company.id,
actor: { actorType: "agent" as const, actorId: agent.id, agentId: agent.id },
@@ -889,13 +899,7 @@ describeEmbeddedPostgres("tool access policy service", () => {
const company = await createCompany(db);
const agent = await createAgent(db, company.id);
const { connection } = await createTool(db, company.id);
await db.insert(principalPermissionGrants).values({
companyId: company.id,
principalType: "agent",
principalId: agent.id,
permissionKey: "tools:use",
scope: { toolName: "send_email" },
});
await db.insert(principalPermissionGrants).values(sendEmailGrantRow(company, agent));
await db.insert(toolPolicies).values({
companyId: company.id,
name: "One send per minute",
@@ -925,13 +929,7 @@ describeEmbeddedPostgres("tool access policy service", () => {
const company = await createCompany(db);
const agent = await createAgent(db, company.id);
const { connection } = await createTool(db, company.id);
await db.insert(principalPermissionGrants).values({
companyId: company.id,
principalType: "agent",
principalId: agent.id,
permissionKey: "tools:use",
scope: { toolName: "send_email" },
});
await db.insert(principalPermissionGrants).values(sendEmailGrantRow(company, agent));
await db.insert(toolPolicies).values({
companyId: company.id,
name: "One concurrent send per minute",
@@ -1105,12 +1103,7 @@ describeEmbeddedPostgres("tool access policy service", () => {
name: "Fallback allow",
defaultAction: "allow",
}).returning().then((rows) => rows[0]!);
await db.insert(toolProfileBindings).values({
companyId: company.id,
profileId: profile.id,
targetType: "agent",
targetId: agent.id,
});
await db.insert(toolProfileBindings).values(agentProfileBindingRow(company, profile, agent));
const [redactPolicy] = await db.insert(toolPolicies).values({
companyId: company.id,
name: "Legacy redact",
@@ -1128,17 +1121,9 @@ describeEmbeddedPostgres("tool access policy service", () => {
priority: 2,
}).returning();
const redactDecision = await toolAccessPolicyService(db).decide({
companyId: company.id,
actor: { actorType: "agent", actorId: agent.id, agentId: agent.id },
request: { connectionId: connection.id, catalogEntryId: catalogEntry.id, toolName: "send_email" },
});
const redactDecision = await toolAccessPolicyService(db).decide(sendEmailDecisionInput(company, agent, connection, catalogEntry));
await db.update(toolPolicies).set({ enabled: false }).where(eq(toolPolicies.id, redactPolicy!.id));
const validateDecision = await toolAccessPolicyService(db).decide({
companyId: company.id,
actor: { actorType: "agent", actorId: agent.id, agentId: agent.id },
request: { connectionId: connection.id, catalogEntryId: catalogEntry.id, toolName: "send_email" },
});
const validateDecision = await toolAccessPolicyService(db).decide(sendEmailDecisionInput(company, agent, connection, catalogEntry));
expect(redactDecision).toMatchObject({
allowed: false,
@@ -1164,12 +1149,7 @@ describeEmbeddedPostgres("tool access policy service", () => {
name: "Fallback allow",
defaultAction: "allow",
}).returning().then((rows) => rows[0]!);
await db.insert(toolProfileBindings).values({
companyId: company.id,
profileId: profile.id,
targetType: "agent",
targetId: agent.id,
});
await db.insert(toolProfileBindings).values(agentProfileBindingRow(company, profile, agent));
const [policy] = await db.insert(toolPolicies).values({
companyId: company.id,
name: "Legacy conditional allow",
@@ -1179,11 +1159,7 @@ describeEmbeddedPostgres("tool access policy service", () => {
priority: 1,
}).returning();
const result = await toolAccessPolicyService(db).decide({
companyId: company.id,
actor: { actorType: "agent", actorId: agent.id, agentId: agent.id },
request: { connectionId: connection.id, catalogEntryId: catalogEntry.id, toolName: "send_email" },
});
const result = await toolAccessPolicyService(db).decide(sendEmailDecisionInput(company, agent, connection, catalogEntry));
expect(result).toMatchObject({
allowed: false,
@@ -1203,12 +1179,7 @@ describeEmbeddedPostgres("tool access policy service", () => {
name: "Fallback allow",
defaultAction: "allow",
}).returning().then((rows) => rows[0]!);
await db.insert(toolProfileBindings).values({
companyId: company.id,
profileId: profile.id,
targetType: "agent",
targetId: agent.id,
});
await db.insert(toolProfileBindings).values(agentProfileBindingRow(company, profile, agent));
const [policy] = await db.insert(toolPolicies).values({
companyId: company.id,
name: "Broken rate limit",
@@ -1218,11 +1189,7 @@ describeEmbeddedPostgres("tool access policy service", () => {
priority: 1,
}).returning();
const result = await toolAccessPolicyService(db).decide({
companyId: company.id,
actor: { actorType: "agent", actorId: agent.id, agentId: agent.id },
request: { connectionId: connection.id, catalogEntryId: catalogEntry.id, toolName: "send_email" },
});
const result = await toolAccessPolicyService(db).decide(sendEmailDecisionInput(company, agent, connection, catalogEntry));
expect(result).toMatchObject({
allowed: false,
@@ -1311,12 +1278,7 @@ describeEmbeddedPostgres("tool access policy service", () => {
const agentA = await createAgent(db, company.id);
const agentB = await createAgent(db, company.id);
const { connection, catalogEntry } = await createTool(db, company.id);
await db.insert(toolPolicies).values({
companyId: company.id,
name: "Review scoped sends",
policyType: "require_approval",
selectors: { toolName: "send_email" },
});
await db.insert(toolPolicies).values(reviewSendPolicyRow(company, "Review scoped sends"));
const args = { to: "ops@example.com", body: "same reviewed payload" };
await createApprovedToolAction({
db,
@@ -1348,35 +1310,14 @@ describeEmbeddedPostgres("tool access policy service", () => {
const company = await createCompany(db);
const agent = await createAgent(db, company.id);
const { connection, catalogEntry } = await createTool(db, company.id);
await db.insert(toolPolicies).values({
companyId: company.id,
name: "Review versioned sends",
policyType: "require_approval",
selectors: { toolName: "send_email" },
});
await db.insert(toolPolicies).values(reviewSendPolicyRow(company, "Review versioned sends"));
const args = { to: "ops@example.com", body: "same payload after tool change" };
await createApprovedToolAction({
db,
companyId: company.id,
agentId: agent.id,
connectionId: connection.id,
catalogEntryId: catalogEntry.id,
argumentsValue: args,
status: "executed",
});
await createApprovedToolAction(executedActionInput(db, company, agent, connection, catalogEntry, args));
await db
.update(toolCatalogEntries)
.set({ versionHash: randomUUID(), schemaHash: randomUUID(), updatedAt: new Date() })
.where(eq(toolCatalogEntries.id, catalogEntry.id));
const currentVersionAction = await createApprovedToolAction({
db,
companyId: company.id,
agentId: agent.id,
connectionId: connection.id,
catalogEntryId: catalogEntry.id,
argumentsValue: args,
status: "executed",
});
const currentVersionAction = await createApprovedToolAction(executedActionInput(db, company, agent, connection, catalogEntry, args));
await expect(toolAccessPolicyService(db).createTrustRuleFromActionRequest({
companyId: company.id,
@@ -1389,31 +1330,10 @@ describeEmbeddedPostgres("tool access policy service", () => {
const company = await createCompany(db);
const agent = await createAgent(db, company.id);
const { connection, catalogEntry } = await createTool(db, company.id);
await db.insert(toolPolicies).values({
companyId: company.id,
name: "Review scoped sends",
policyType: "require_approval",
selectors: { toolName: "send_email" },
});
await db.insert(toolPolicies).values(reviewSendPolicyRow(company, "Review scoped sends"));
const args = { to: "ops@example.com", body: "same reviewed payload" };
const first = await createApprovedToolAction({
db,
companyId: company.id,
agentId: agent.id,
connectionId: connection.id,
catalogEntryId: catalogEntry.id,
argumentsValue: args,
status: "executed",
});
await createApprovedToolAction({
db,
companyId: company.id,
agentId: agent.id,
connectionId: connection.id,
catalogEntryId: catalogEntry.id,
argumentsValue: args,
status: "executed",
});
const first = await createApprovedToolAction(executedActionInput(db, company, agent, connection, catalogEntry, args));
await createApprovedToolAction(executedActionInput(db, company, agent, connection, catalogEntry, args));
await expect(toolAccessPolicyService(db).createTrustRuleFromActionRequest({
companyId: company.id,
@@ -1435,31 +1355,10 @@ describeEmbeddedPostgres("tool access policy service", () => {
const company = await createCompany(db);
const agent = await createAgent(db, company.id);
const { connection, catalogEntry } = await createTool(db, company.id);
await db.insert(toolPolicies).values({
companyId: company.id,
name: "Review exact payload sends",
policyType: "require_approval",
selectors: { toolName: "send_email" },
});
await db.insert(toolPolicies).values(reviewSendPolicyRow(company, "Review exact payload sends"));
const args = { to: "ops@example.com", body: "same reviewed payload" };
const first = await createApprovedToolAction({
db,
companyId: company.id,
agentId: agent.id,
connectionId: connection.id,
catalogEntryId: catalogEntry.id,
argumentsValue: args,
status: "executed",
});
await createApprovedToolAction({
db,
companyId: company.id,
agentId: agent.id,
connectionId: connection.id,
catalogEntryId: catalogEntry.id,
argumentsValue: args,
status: "executed",
});
const first = await createApprovedToolAction(executedActionInput(db, company, agent, connection, catalogEntry, args));
await createApprovedToolAction(executedActionInput(db, company, agent, connection, catalogEntry, args));
await expect(toolAccessPolicyService(db).createTrustRuleFromActionRequest({
companyId: company.id,
@@ -1475,22 +1374,9 @@ describeEmbeddedPostgres("tool access policy service", () => {
const company = await createCompany(db);
const agent = await createAgent(db, company.id);
const { connection, catalogEntry } = await createTool(db, company.id);
await db.insert(toolPolicies).values({
companyId: company.id,
name: "Review changed sends",
policyType: "require_approval",
selectors: { toolName: "send_email" },
});
await db.insert(toolPolicies).values(reviewSendPolicyRow(company, "Review changed sends"));
const args = { to: "ops@example.com", body: "repeatable" };
const first = await createApprovedToolAction({
db,
companyId: company.id,
agentId: agent.id,
connectionId: connection.id,
catalogEntryId: catalogEntry.id,
argumentsValue: args,
status: "executed",
});
const first = await createApprovedToolAction(executedActionInput(db, company, agent, connection, catalogEntry, args));
await createApprovedToolAction({
db,
companyId: company.id,
@@ -1523,22 +1409,9 @@ describeEmbeddedPostgres("tool access policy service", () => {
const company = await createCompany(db);
const agent = await createAgent(db, company.id);
const { connection, catalogEntry } = await createTool(db, company.id);
await db.insert(toolPolicies).values({
companyId: company.id,
name: "Review revocable sends",
policyType: "require_approval",
selectors: { toolName: "send_email" },
});
await db.insert(toolPolicies).values(reviewSendPolicyRow(company, "Review revocable sends"));
const args = { to: "ops@example.com", body: "revocable" };
const first = await createApprovedToolAction({
db,
companyId: company.id,
agentId: agent.id,
connectionId: connection.id,
catalogEntryId: catalogEntry.id,
argumentsValue: args,
status: "executed",
});
const first = await createApprovedToolAction(executedActionInput(db, company, agent, connection, catalogEntry, args));
await createApprovedToolAction({
db,
companyId: company.id,
File diff suppressed because it is too large. Load diff
+86 -211
View File
@@ -169,6 +169,34 @@ function fakePluginDispatcher(): PluginToolDispatcher {
};
}
type IdRef = { id: string };
function reviewNotePolicyRow(company: IdRef): typeof toolPolicies.$inferInsert {
return {
companyId: company.id,
name: "Review note writes",
policyType: "require_approval",
selectors: { toolName: "mcp-remote-fixture:update_note" },
};
}
function readPolicyRow(company: IdRef, name: string): typeof toolPolicies.$inferInsert {
return {
companyId: company.id,
name: name,
policyType: "allow",
selectors: { riskLevel: "read" },
};
}
function runSessionInput(company: IdRef, agent: IdRef, run: IdRef): Parameters<ReturnType<typeof createToolGatewayService>["createSession"]>[0] {
return {
companyId: company.id,
agentId: agent.id,
runId: run.id,
};
}
describeEmbeddedPostgres("tool gateway service", () => {
let db!: ReturnType<typeof createDb>;
let tempDb: Awaited<ReturnType<typeof startEmbeddedPostgresTestDatabase>> | null = null;
@@ -208,18 +236,9 @@ describeEmbeddedPostgres("tool gateway service", () => {
it("gates write tools with an action request and executes only stored reviewed arguments once", async () => {
const { company, agent, run } = await createRunFixture(db);
await db.insert(toolPolicies).values({
companyId: company.id,
name: "Review note writes",
policyType: "require_approval",
selectors: { toolName: "mcp-remote-fixture:update_note" },
});
await db.insert(toolPolicies).values(reviewNotePolicyRow(company));
const gateway = createTestToolGatewayService(db);
const session = await gateway.createSession({
companyId: company.id,
agentId: agent.id,
runId: run.id,
});
const session = await gateway.createSession(runSessionInput(company, agent, run));
const notified = vi.fn();
const unsubscribe = subscribeDeliveryWork(db, DELIVERY_QUEUES.toolAction, notified);
@@ -313,18 +332,9 @@ describeEmbeddedPostgres("tool gateway service", () => {
companyId: company.id, runId: run.id, issueId: issue.id,
responsibleUserId: null, cause: "company_default",
});
await db.insert(toolPolicies).values({
companyId: company.id,
name: "Review note writes",
policyType: "require_approval",
selectors: { toolName: "mcp-remote-fixture:update_note" },
});
await db.insert(toolPolicies).values(reviewNotePolicyRow(company));
const gateway = createTestToolGatewayService(db);
const session = await gateway.createSession({
companyId: company.id,
agentId: agent.id,
runId: run.id,
});
const session = await gateway.createSession(runSessionInput(company, agent, run));
await expect(gateway.executeTool({
sessionToken: session.token,
@@ -379,7 +389,7 @@ describeEmbeddedPostgres("tool gateway service", () => {
});
const deliveries = toolActionDeliveryService(db, { wakeup });
const gateway = createTestToolGatewayService(db, { onToolActionSettled: deliveries.deliver });
const session = await gateway.createSession({ companyId: company.id, agentId: agent.id, runId: run.id });
const session = await gateway.createSession(runSessionInput(company, agent, run));
await expect(gateway.executeTool({ sessionToken: session.token, tool: "mcp-remote-fixture:update_note", parameters: { noteId: "n1", body: "receipt" } })).rejects.toMatchObject({ reasonCode: "approval_required" });
const [request] = await db.select().from(toolActionRequests);
await gateway.approveActionRequest({ companyId: company.id, actionRequestId: request.id, actor: { userId: "reviewer" } });
@@ -419,7 +429,7 @@ describeEmbeddedPostgres("tool gateway service", () => {
const wakeup = vi.fn(async (agentId: string, input: any) => (await db.insert(agentWakeupRequests).values({ companyId: company.id, agentId, source: input.source, idempotencyKey: input.idempotencyKey, payload: input.payload }).returning())[0] as any);
const delivery = toolActionDeliveryService(db, { wakeup });
const gateway = createTestToolGatewayService(db, { onToolActionSettled: delivery.deliver });
const session = await gateway.createSession({ companyId: company.id, agentId: agent.id, runId: run.id });
const session = await gateway.createSession(runSessionInput(company, agent, run));
for (const noteId of ["one", "two"]) await expect(gateway.executeTool({ sessionToken: session.token, tool: "mcp-remote-fixture:update_note", parameters: { noteId, body: "reviewed body" } })).rejects.toMatchObject({ reasonCode: "approval_required" });
const requests = await db.select().from(toolActionRequests);
expect(requests).toHaveLength(2);
@@ -470,7 +480,7 @@ describeEmbeddedPostgres("tool gateway service", () => {
const { company, agent, run } = await createRunFixture(db);
await db.insert(toolPolicies).values({ companyId: company.id, name: "Ask first", policyType: "require_approval", selectors: { toolName: "mcp-remote-fixture:update_note" } });
const gateway = createTestToolGatewayService(db);
const session = await gateway.createSession({ companyId: company.id, agentId: agent.id, runId: run.id });
const session = await gateway.createSession(runSessionInput(company, agent, run));
for (let n = 0; n < 12; n++) {
await expect(gateway.executeTool({ sessionToken: session.token, tool: "mcp-remote-fixture:update_note", parameters: { noteId: `note-${n}`, body: "review me" } })).rejects.toMatchObject({ reasonCode: "approval_required" });
}
@@ -500,7 +510,7 @@ describeEmbeddedPostgres("tool gateway service", () => {
const { company, agent, issue, run } = await createRunFixture(db);
await db.insert(toolPolicies).values({ companyId: company.id, name: "Ask first", policyType: "require_approval", selectors: { toolName: "mcp-remote-fixture:update_note" } });
const gateway = createTestToolGatewayService(db);
const session = await gateway.createSession({ companyId: company.id, agentId: agent.id, runId: run.id });
const session = await gateway.createSession(runSessionInput(company, agent, run));
await expect(gateway.executeTool({ sessionToken: session.token, tool: "mcp-remote-fixture:update_note", parameters: { noteId: "reassigned" } })).rejects.toMatchObject({ reasonCode: "approval_required" });
const [request] = await db.select().from(toolActionRequests);
await gateway.declineActionRequest({ companyId: company.id, actionRequestId: request.id, actor: { userId: "reviewer" } });
@@ -518,7 +528,7 @@ describeEmbeddedPostgres("tool gateway service", () => {
const { company, agent, run } = await createRunFixture(db);
await db.insert(toolPolicies).values({ companyId: company.id, name: "Ask first", policyType: "require_approval", selectors: { toolName: "mcp-remote-fixture:update_note" } });
const gateway = createTestToolGatewayService(db);
const session = await gateway.createSession({ companyId: company.id, agentId: agent.id, runId: run.id });
const session = await gateway.createSession(runSessionInput(company, agent, run));
await expect(gateway.executeTool({ sessionToken: session.token, tool: "mcp-remote-fixture:update_note", parameters: { noteId: "expire" } })).rejects.toMatchObject({ reasonCode: "approval_required" });
const [request] = await db.select().from(toolActionRequests);
await db.update(toolActionRequests).set({ expiresAt: new Date(Date.now() - 1000) }).where(eq(toolActionRequests.id, request.id));
@@ -537,7 +547,7 @@ describeEmbeddedPostgres("tool gateway service", () => {
const { company, agent, issue, run } = await createRunFixture(db);
await db.insert(toolPolicies).values({ companyId: company.id, name: "Ask first", policyType: "require_approval", selectors: { toolName: "mcp-remote-fixture:update_note" } });
const gateway = createTestToolGatewayService(db);
const session = await gateway.createSession({ companyId: company.id, agentId: agent.id, runId: run.id });
const session = await gateway.createSession(runSessionInput(company, agent, run));
await expect(gateway.executeTool({ sessionToken: session.token, tool: "mcp-remote-fixture:update_note", parameters: { noteId: "expire" } })).rejects.toMatchObject({ reasonCode: "approval_required" });
const [expiring] = await db.select().from(toolActionRequests);
await db.update(toolActionRequests).set({ expiresAt: new Date(Date.now() - 1000) }).where(eq(toolActionRequests.id, expiring.id));
@@ -572,7 +582,7 @@ describeEmbeddedPostgres("tool gateway service", () => {
if (body.method === "tools/call") calls++;
return new Response(JSON.stringify({ jsonrpc: "2.0", id: body.id, result: { content: [{ type: "text", text: "Roadmap and meeting notes" }] } }), { headers: { "content-type": "application/json" } });
} });
const session = await gateway.createSession({ companyId: company.id, agentId: agent.id, runId: run.id });
const session = await gateway.createSession(runSessionInput(company, agent, run));
const tool = (await gateway.listToolsForSession(session.token)).find(t => t.connectionId === connection.id)!;
const call = (limit: number) => gateway.executeTool({ sessionToken: session.token, tool: tool.name, parameters: { limit } });
await expect(call(10)).rejects.toMatchObject({ reasonCode: "approval_required" });
@@ -611,7 +621,7 @@ describeEmbeddedPostgres("tool gateway service", () => {
const { company, agent, run } = await createRunFixture(db);
await db.insert(toolPolicies).values({ companyId: company.id, name: "Ask first", policyType: "require_approval", selectors: { toolName: "mcp-remote-fixture:update_note" } });
const gateway = createTestToolGatewayService(db);
const session = await gateway.createSession({ companyId: company.id, agentId: agent.id, runId: run.id });
const session = await gateway.createSession(runSessionInput(company, agent, run));
await expect(gateway.executeTool({ sessionToken: session.token, tool: "mcp-remote-fixture:update_note", parameters: { noteId: "n1" } })).rejects.toMatchObject({ reasonCode: "approval_required" });
const [request] = await db.select().from(toolActionRequests);
// This internal fixture has no connection and cannot grant action-wide access.
@@ -624,14 +634,9 @@ describeEmbeddedPostgres("tool gateway service", () => {
it("refuses to approve an action request through a different interaction", async () => {
const { company, agent, issue, run } = await createRunFixture(db);
await db.insert(toolPolicies).values({
companyId: company.id,
name: "Review note writes",
policyType: "require_approval",
selectors: { toolName: "mcp-remote-fixture:update_note" },
});
await db.insert(toolPolicies).values(reviewNotePolicyRow(company));
const gateway = createTestToolGatewayService(db);
const session = await gateway.createSession({ companyId: company.id, agentId: agent.id, runId: run.id });
const session = await gateway.createSession(runSessionInput(company, agent, run));
await expect(gateway.executeTool({
sessionToken: session.token,
@@ -654,14 +659,9 @@ describeEmbeddedPostgres("tool gateway service", () => {
it("prevents another run from consuming an approved action request by id", async () => {
const { company, agent, issue, run } = await createRunFixture(db);
await db.insert(toolPolicies).values({
companyId: company.id,
name: "Review note writes",
policyType: "require_approval",
selectors: { toolName: "mcp-remote-fixture:update_note" },
});
await db.insert(toolPolicies).values(reviewNotePolicyRow(company));
const gateway = createTestToolGatewayService(db);
const originatingSession = await gateway.createSession({ companyId: company.id, agentId: agent.id, runId: run.id });
const originatingSession = await gateway.createSession(runSessionInput(company, agent, run));
await expect(gateway.executeTool({
sessionToken: originatingSession.token,
@@ -702,14 +702,9 @@ describeEmbeddedPostgres("tool gateway service", () => {
it("executes an approved identical-args race once and returns the winner result", async () => {
const { company, agent, run } = await createRunFixture(db);
await db.insert(toolPolicies).values({
companyId: company.id,
name: "Review note writes",
policyType: "require_approval",
selectors: { toolName: "mcp-remote-fixture:update_note" },
});
await db.insert(toolPolicies).values(reviewNotePolicyRow(company));
const gateway = createTestToolGatewayService(db);
const session = await gateway.createSession({ companyId: company.id, agentId: agent.id, runId: run.id });
const session = await gateway.createSession(runSessionInput(company, agent, run));
const parameters = { noteId: "n1", body: "race body" };
await expect(gateway.executeTool({
@@ -737,14 +732,9 @@ describeEmbeddedPostgres("tool gateway service", () => {
it("refuses to execute an approved action after its issue closes", async () => {
const { company, agent, issue, run } = await createRunFixture(db);
await db.insert(toolPolicies).values({
companyId: company.id,
name: "Review note writes",
policyType: "require_approval",
selectors: { toolName: "mcp-remote-fixture:update_note" },
});
await db.insert(toolPolicies).values(reviewNotePolicyRow(company));
const gateway = createTestToolGatewayService(db);
const session = await gateway.createSession({ companyId: company.id, agentId: agent.id, runId: run.id });
const session = await gateway.createSession(runSessionInput(company, agent, run));
const parameters = { noteId: "n1", body: "post-close body" };
await expect(gateway.executeTool({
@@ -774,14 +764,9 @@ describeEmbeddedPostgres("tool gateway service", () => {
it("keeps pre-execute-on-approve approved requests inert", async () => {
const { company, agent, run } = await createRunFixture(db);
await db.insert(toolPolicies).values({
companyId: company.id,
name: "Review note writes",
policyType: "require_approval",
selectors: { toolName: "mcp-remote-fixture:update_note" },
});
await db.insert(toolPolicies).values(reviewNotePolicyRow(company));
const gateway = createTestToolGatewayService(db);
const session = await gateway.createSession({ companyId: company.id, agentId: agent.id, runId: run.id });
const session = await gateway.createSession(runSessionInput(company, agent, run));
const parameters = { noteId: "n1", body: "legacy" };
await expect(gateway.executeTool({ sessionToken: session.token, tool: "mcp-remote-fixture:update_note", parameters }))
.rejects.toMatchObject({ reasonCode: "approval_required" });
@@ -833,12 +818,7 @@ describeEmbeddedPostgres("tool gateway service", () => {
it("does not let a stale legacy consumer overwrite the winning approved execution", async () => {
const { company, agent, run } = await createRunFixture(db);
await db.insert(toolPolicies).values({
companyId: company.id,
name: "Review note writes",
policyType: "require_approval",
selectors: { toolName: "mcp-remote-fixture:update_note" },
});
await db.insert(toolPolicies).values(reviewNotePolicyRow(company));
let observeLegacyClaim!: () => void;
const legacyClaimObserved = new Promise<void>((resolve) => {
@@ -854,11 +834,7 @@ describeEmbeddedPostgres("tool gateway service", () => {
await legacyClaimBlocked;
},
});
const session = await gateway.createSession({
companyId: company.id,
agentId: agent.id,
runId: run.id,
});
const session = await gateway.createSession(runSessionInput(company, agent, run));
const parameters = { noteId: "n1", body: "legacy race" };
await expect(gateway.executeTool({
@@ -941,18 +917,9 @@ describeEmbeddedPostgres("tool gateway service", () => {
it("does not leave unsigned action requests pending when signing is unavailable", async () => {
vi.stubEnv("PAPERCLIP_TOOL_ACTION_SIGNING_SECRET", "");
const { company, agent, run } = await createRunFixture(db);
await db.insert(toolPolicies).values({
companyId: company.id,
name: "Review note writes",
policyType: "require_approval",
selectors: { toolName: "mcp-remote-fixture:update_note" },
});
await db.insert(toolPolicies).values(reviewNotePolicyRow(company));
const gateway = createTestToolGatewayService(db, { toolActionSigningSecret: " " });
const session = await gateway.createSession({
companyId: company.id,
agentId: agent.id,
runId: run.id,
});
const session = await gateway.createSession(runSessionInput(company, agent, run));
await expect(gateway.executeTool({
sessionToken: session.token,
@@ -993,14 +960,9 @@ describeEmbeddedPostgres("tool gateway service", () => {
status: "running",
contextSnapshot: {},
}).returning().then((rows) => rows[0]!);
await db.insert(toolPolicies).values({
companyId: company.id,
name: "Review note writes",
policyType: "require_approval",
selectors: { toolName: "mcp-remote-fixture:update_note" },
});
await db.insert(toolPolicies).values(reviewNotePolicyRow(company));
const gateway = createTestToolGatewayService(db);
const session = await gateway.createSession({ companyId: company.id, agentId: agent.id, runId: run.id });
const session = await gateway.createSession(runSessionInput(company, agent, run));
await expect(gateway.executeTool({
sessionToken: session.token,
@@ -1016,18 +978,9 @@ describeEmbeddedPostgres("tool gateway service", () => {
it("cancels a stale pending action request when direct approval sees an invalid signature", async () => {
const { company, agent, run } = await createRunFixture(db);
await db.insert(toolPolicies).values({
companyId: company.id,
name: "Review note writes",
policyType: "require_approval",
selectors: { toolName: "mcp-remote-fixture:update_note" },
});
await db.insert(toolPolicies).values(reviewNotePolicyRow(company));
const gateway = createTestToolGatewayService(db);
const session = await gateway.createSession({
companyId: company.id,
agentId: agent.id,
runId: run.id,
});
const session = await gateway.createSession(runSessionInput(company, agent, run));
await expect(gateway.executeTool({
sessionToken: session.token,
tool: "mcp-remote-fixture:update_note",
@@ -1053,18 +1006,9 @@ describeEmbeddedPostgres("tool gateway service", () => {
it("declines a pending action request and rejects the invocation (PAP-10859)", async () => {
const { company, agent, run } = await createRunFixture(db);
await db.insert(toolPolicies).values({
companyId: company.id,
name: "Review note writes",
policyType: "require_approval",
selectors: { toolName: "mcp-remote-fixture:update_note" },
});
await db.insert(toolPolicies).values(reviewNotePolicyRow(company));
const gateway = createTestToolGatewayService(db);
const session = await gateway.createSession({
companyId: company.id,
agentId: agent.id,
runId: run.id,
});
const session = await gateway.createSession(runSessionInput(company, agent, run));
await expect(gateway.executeTool({
sessionToken: session.token,
@@ -1107,14 +1051,9 @@ describeEmbeddedPostgres("tool gateway service", () => {
it("expires a stale identical request and creates a fresh approval", async () => {
const { company, agent, run } = await createRunFixture(db);
await db.insert(toolPolicies).values({
companyId: company.id,
name: "Review note writes",
policyType: "require_approval",
selectors: { toolName: "mcp-remote-fixture:update_note" },
});
await db.insert(toolPolicies).values(reviewNotePolicyRow(company));
const gateway = createTestToolGatewayService(db);
const session = await gateway.createSession({ companyId: company.id, agentId: agent.id, runId: run.id });
const session = await gateway.createSession(runSessionInput(company, agent, run));
const parameters = { noteId: "n1", body: "expires" };
await expect(gateway.executeTool({ sessionToken: session.token, tool: "mcp-remote-fixture:update_note", parameters }))
.rejects.toMatchObject({ reasonCode: "approval_required" });
@@ -1138,11 +1077,7 @@ describeEmbeddedPostgres("tool gateway service", () => {
selectors: { toolName: "fixture:delete_everything" },
});
const gateway = createTestToolGatewayService(db, { pluginToolDispatcher: fakePluginDispatcher() });
const session = await gateway.createSession({
companyId: company.id,
agentId: agent.id,
runId: run.id,
});
const session = await gateway.createSession(runSessionInput(company, agent, run));
let approvalRequired: ToolGatewayHttpError | null = null;
try {
@@ -1204,12 +1139,7 @@ describeEmbeddedPostgres("tool gateway service", () => {
it("maps remote MCP elicitation to a durable issue interaction", async () => {
const { company, agent, run } = await createRunFixture(db);
await createRemoteMcpToolFixture(db, company.id);
await db.insert(toolPolicies).values({
companyId: company.id,
name: "Allow read tools",
policyType: "allow",
selectors: { riskLevel: "read" },
});
await db.insert(toolPolicies).values(readPolicyRow(company, "Allow read tools"));
const originalFetch = globalThis.fetch;
globalThis.fetch = async (_url, init) => new Response(JSON.stringify({
jsonrpc: "2.0",
@@ -1235,11 +1165,7 @@ describeEmbeddedPostgres("tool gateway service", () => {
}), { status: 200, headers: { "content-type": "application/json" } });
try {
const gateway = createTestToolGatewayService(db);
const session = await gateway.createSession({
companyId: company.id,
agentId: agent.id,
runId: run.id,
});
const session = await gateway.createSession(runSessionInput(company, agent, run));
const tool = (await gateway.listToolsForSession(session.token))
.find((candidate) => candidate.providerType === "mcp_remote_http");
expect(tool).toBeTruthy();
@@ -1296,12 +1222,7 @@ describeEmbeddedPostgres("tool gateway service", () => {
...(persistPreference ? { mcpSessionRequired: true } : {}),
},
}).where(eq(toolConnections.id, connection.id));
await db.insert(toolPolicies).values({
companyId: company.id,
name: "Allow read tools",
policyType: "allow",
selectors: { riskLevel: "read" },
});
await db.insert(toolPolicies).values(readPolicyRow(company, "Allow read tools"));
const requests: Array<{ method: string; sessionId: string | null; protocolVersion: string | null }> = [];
const gateway = createTestToolGatewayService(db, {
remoteHttpRequest: async (_url, init) => {
@@ -1334,11 +1255,7 @@ describeEmbeddedPostgres("tool gateway service", () => {
}), { status: 200, headers: { "content-type": "application/json" } });
},
});
const session = await gateway.createSession({
companyId: company.id,
agentId: agent.id,
runId: run.id,
});
const session = await gateway.createSession(runSessionInput(company, agent, run));
const tool = (await gateway.listToolsForSession(session.token))
.find((candidate) => candidate.providerType === "mcp_remote_http");
@@ -1359,12 +1276,7 @@ describeEmbeddedPostgres("tool gateway service", () => {
it("keeps direct tools/call for generic connections without the curated session preference", async () => {
const { company, agent, run } = await createRunFixture(db);
const { connection } = await createRemoteMcpToolFixture(db, company.id);
await db.insert(toolPolicies).values({
companyId: company.id,
name: "Allow read tools",
policyType: "allow",
selectors: { riskLevel: "read" },
});
await db.insert(toolPolicies).values(readPolicyRow(company, "Allow read tools"));
const requests: string[] = [];
const gateway = createTestToolGatewayService(db, {
remoteHttpRequest: async (_url, init) => {
@@ -1377,7 +1289,7 @@ describeEmbeddedPostgres("tool gateway service", () => {
});
},
});
const session = await gateway.createSession({ companyId: company.id, agentId: agent.id, runId: run.id });
const session = await gateway.createSession(runSessionInput(company, agent, run));
const tool = (await gateway.listToolsForSession(session.token))
.find((candidate) => candidate.providerType === "mcp_remote_http");
@@ -1406,9 +1318,7 @@ describeEmbeddedPostgres("tool gateway service", () => {
} },
} },
}).where(eq(toolCatalogEntries.id, catalogEntry.id));
await db.insert(toolPolicies).values({
companyId: company.id, name: "Allow Chat reads", policyType: "allow", selectors: { riskLevel: "read" },
});
await db.insert(toolPolicies).values(readPolicyRow(company, "Allow Chat reads"));
const provider = vi.fn(async (_url, init) => {
const payload = JSON.parse(String(init.body));
if (payload.method === "notifications/initialized") return new Response(null, { status: 202 });
@@ -1417,7 +1327,7 @@ describeEmbeddedPostgres("tool gateway service", () => {
: { content: [{ type: "text", text: "Found matching messages." }] } });
});
const gateway = createTestToolGatewayService(db, { remoteHttpRequest: provider });
const session = await gateway.createSession({ companyId: company.id, agentId: agent.id, runId: run.id });
const session = await gateway.createSession(runSessionInput(company, agent, run));
const tool = (await gateway.listToolsForSession(session.token))
.find((candidate) => candidate.upstreamToolName === "search_messages")!;
expect(tool).toBeTruthy();
@@ -1457,12 +1367,7 @@ describeEmbeddedPostgres("tool gateway service", () => {
sourceTemplateKey: "google-drive",
},
}).where(eq(toolConnections.id, connection.id));
await db.insert(toolPolicies).values({
companyId: company.id,
name: "Allow Drive reads",
policyType: "allow",
selectors: { riskLevel: "read" },
});
await db.insert(toolPolicies).values(readPolicyRow(company, "Allow Drive reads"));
const gateway = createTestToolGatewayService(db, {
remoteHttpRequest: async (_url, init) => {
const requestBody = JSON.parse(String(init.body)) as { id: string };
@@ -1476,11 +1381,7 @@ describeEmbeddedPostgres("tool gateway service", () => {
}), { status: 200, headers: { "content-type": "application/json" } });
},
});
const session = await gateway.createSession({
companyId: company.id,
agentId: agent.id,
runId: run.id,
});
const session = await gateway.createSession(runSessionInput(company, agent, run));
const tool = (await gateway.listToolsForSession(session.token))
.find((candidate) => candidate.providerType === "mcp_remote_http");
@@ -1517,12 +1418,7 @@ describeEmbeddedPostgres("tool gateway service", () => {
externalCredential: { provider: "vercel_connect", subjectType: "app" },
credentialSecretRefs: [],
}).where(eq(connectionGrants.connectionId, connection.id));
await db.insert(toolPolicies).values({
companyId: company.id,
name: "Allow Vercel-backed reads",
policyType: "allow",
selectors: { riskLevel: "read" },
});
await db.insert(toolPolicies).values(readPolicyRow(company, "Allow Vercel-backed reads"));
const getToken = vi.fn<VercelConnectClient["getToken"]>(async (_request, options) => ({
token: options?.forceRefresh ? "fresh-provider-bearer" : "stale-provider-bearer",
@@ -1552,7 +1448,7 @@ describeEmbeddedPostgres("tool gateway service", () => {
}), { status: 200, headers: { "content-type": "application/json" } });
},
});
const session = await gateway.createSession({ companyId: company.id, agentId: agent.id, runId: run.id });
const session = await gateway.createSession(runSessionInput(company, agent, run));
const tool = (await gateway.listToolsForSession(session.token))
.find((candidate) => candidate.providerType === "mcp_remote_http");
@@ -1596,8 +1492,7 @@ describeEmbeddedPostgres("tool gateway service", () => {
}))).returning();
await initializeRunIdentity(db, { companyId: company.id, runId: run.id, issueId: issue.id,
responsibleUserId: "A", cause: "instruction" });
await db.insert(toolPolicies).values({ companyId: company.id, name: "Allow reads",
policyType: "allow", selectors: { riskLevel: "read" } });
await db.insert(toolPolicies).values(readPolicyRow(company, "Allow reads"));
const resolvedGrants: string[] = [];
const gateway = createTestToolGatewayService(db, {
oauthGrantRefresher: async ({ grantId }) => {
@@ -1608,7 +1503,7 @@ describeEmbeddedPostgres("tool gateway service", () => {
id: JSON.parse(String(init.body)).id, result: { content: [{ type: "text", text: "ok" }] },
}), { status: 200, headers: { "content-type": "application/json" } }),
});
const session = await gateway.createSession({ companyId: company.id, agentId: agent.id, runId: run.id });
const session = await gateway.createSession(runSessionInput(company, agent, run));
const tool = (await gateway.listToolsForSession(session.token)).find(t => t.providerType === "mcp_remote_http")!;
for (const [index, user] of ["A", "B", "A"].entries()) {
if (index > 0) {
@@ -1655,8 +1550,7 @@ describeEmbeddedPostgres("tool gateway service", () => {
}
await initializeRunIdentity(db, { companyId: company.id, runId: run.id, issueId: issue.id,
responsibleUserId: "A", cause: "instruction" });
await db.insert(toolPolicies).values({ companyId: company.id, name: "Allow reads",
policyType: "allow", selectors: { riskLevel: "read" } });
await db.insert(toolPolicies).values(readPolicyRow(company, "Allow reads"));
const refreshed: string[] = [];
const dispatched = vi.fn(async (_url: string, init: RequestInit) => {
expect(new Headers(init.headers).get("authorization")).toBe(`Bearer token-${upstreamFailure ? 1 : 0}`);
@@ -1674,7 +1568,7 @@ describeEmbeddedPostgres("tool gateway service", () => {
return grants.find(grant => grant.id === grantId)!;
}, remoteHttpRequest: dispatched,
});
const session = await gateway.createSession({ companyId: company.id, agentId: agent.id, runId: run.id });
const session = await gateway.createSession(runSessionInput(company, agent, run));
const tool = (await gateway.listToolsForSession(session.token)).find(t => t.providerType === "mcp_remote_http")!;
const execution = gateway.executeTool({ sessionToken: session.token, tool: tool.name, parameters: {} });
if (upstreamFailure) await expect(execution).rejects.toMatchObject({ status: 502 });
@@ -1732,12 +1626,7 @@ describeEmbeddedPostgres("tool gateway service", () => {
},
},
}).where(eq(connectionGrants.id, grant.id));
await db.insert(toolPolicies).values({
companyId: company.id,
name: "Allow customer OAuth reads",
policyType: "allow",
selectors: { riskLevel: "read" },
});
await db.insert(toolPolicies).values(readPolicyRow(company, "Allow customer OAuth reads"));
const oauthGrantRefresher: NonNullable<ToolGatewayServiceOptions["oauthGrantRefresher"]> = vi.fn(async (input) => {
if (input.forceRefresh) {
@@ -1784,7 +1673,7 @@ describeEmbeddedPostgres("tool gateway service", () => {
}), { status: 200, headers: { "content-type": "application/json" } });
},
});
const session = await gateway.createSession({ companyId: company.id, agentId: agent.id, runId: run.id });
const session = await gateway.createSession(runSessionInput(company, agent, run));
const tool = (await gateway.listToolsForSession(session.token))
.find((candidate) => candidate.providerType === "mcp_remote_http");
@@ -1865,12 +1754,7 @@ describeEmbeddedPostgres("tool gateway service", () => {
},
},
}).where(eq(connectionGrants.id, grant.id));
await db.insert(toolPolicies).values({
companyId: company.id,
name: "Allow managed OAuth reads",
policyType: "allow",
selectors: { riskLevel: "read" },
});
await db.insert(toolPolicies).values(readPolicyRow(company, "Allow managed OAuth reads"));
const oauthGrantRefresher: NonNullable<ToolGatewayServiceOptions["oauthGrantRefresher"]> = vi.fn(async (input) => {
if (input.forceRefresh) {
@@ -1887,7 +1771,7 @@ describeEmbeddedPostgres("tool gateway service", () => {
return new Response(null, { status: 401 });
},
});
const session = await gateway.createSession({ companyId: company.id, agentId: agent.id, runId: run.id });
const session = await gateway.createSession(runSessionInput(company, agent, run));
const tool = (await gateway.listToolsForSession(session.token))
.find((candidate) => candidate.providerType === "mcp_remote_http");
@@ -1926,12 +1810,7 @@ describeEmbeddedPostgres("tool gateway service", () => {
contextSnapshot: {},
}).returning().then((rows) => rows[0]!);
await createRemoteMcpToolFixture(db, company.id);
await db.insert(toolPolicies).values({
companyId: company.id,
name: "Allow read tools",
policyType: "allow",
selectors: { riskLevel: "read" },
});
await db.insert(toolPolicies).values(readPolicyRow(company, "Allow read tools"));
const originalFetch = globalThis.fetch;
globalThis.fetch = async (_url, init) => new Response(JSON.stringify({
jsonrpc: "2.0",
@@ -1940,11 +1819,7 @@ describeEmbeddedPostgres("tool gateway service", () => {
}), { status: 200, headers: { "content-type": "application/json" } });
try {
const gateway = createTestToolGatewayService(db);
const session = await gateway.createSession({
companyId: company.id,
agentId: agent.id,
runId: run.id,
});
const session = await gateway.createSession(runSessionInput(company, agent, run));
const tool = (await gateway.listToolsForSession(session.token))
.find((candidate) => candidate.providerType === "mcp_remote_http");
expect(tool).toBeTruthy();
@@ -2073,7 +1948,7 @@ describeEmbeddedPostgres("tool gateway service", () => {
actor: { type: "agent", companyId: company.id, agentId: agent.id, runId: run.id },
tool: "fixture:read_status",
parameters: { query: "ok", apiKey: "sk-secret-value" },
runContext: { companyId: company.id, agentId: agent.id, runId: run.id },
runContext: runSessionInput(company, agent, run),
});
expect(executedParameters).toEqual({ query: "ok", apiKey: "sk-secret-value" });