Merge branch 'master' into codex/task-workspace-ownership

This commit is contained in:
Dotta authored and GitHub committed 2026-10-12 17:28:20 -05:00
commit eecbdb67e2
57 files changed
+10936 -8719

No files matched your search

@@ -249,6 +249,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 () => {
vi.resetModules();
@@ -305,22 +316,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 = {
@@ -463,61 +460,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(),
@@ -540,14 +487,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(),
@@ -588,14 +528,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")
@@ -816,14 +749,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(),
+26 -120
View File
@@ -507,6 +507,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}`);
@@ -690,14 +701,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:");
@@ -779,14 +783,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");
@@ -1763,14 +1760,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: {
@@ -3306,14 +3296,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:");
@@ -3350,14 +3333,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([]);
@@ -3430,14 +3406,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",
@@ -3503,14 +3472,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([]);
@@ -3570,14 +3532,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([]);
@@ -3622,14 +3577,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([]);
@@ -3720,14 +3668,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([]);
@@ -3802,14 +3743,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([]);
@@ -5500,14 +5434,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");
@@ -5559,14 +5486,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([]);
@@ -5743,14 +5663,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({
@@ -5819,14 +5732,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);
+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
@@ -294,6 +294,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;
@@ -2204,21 +2302,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",
@@ -2311,21 +2395,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",
@@ -2392,21 +2462,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",
@@ -2465,21 +2521,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",
@@ -2529,17 +2571,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,
@@ -2565,37 +2597,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",
@@ -2719,21 +2722,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",
@@ -2877,37 +2866,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",
@@ -2988,17 +2948,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,
@@ -3161,14 +3111,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,
@@ -3246,14 +3189,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,
@@ -3331,14 +3267,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,
@@ -3435,14 +3364,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,
@@ -3458,21 +3380,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",
@@ -3542,14 +3450,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,
@@ -3671,14 +3572,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,
@@ -3872,14 +3766,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,
@@ -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" },
+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();
@@ -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" });