fix(plugins): validate checkbox response input

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Michael NguyenandPaperclip committed 2026-07-24 01:07:42 -07:00
1 parent 1603a25a23
commit 942a2fe768
2 files changed
+41 -4

No files matched your search

@@ -1036,6 +1036,42 @@ describeEmbeddedPostgres("plugin orchestration APIs", () => {
});
});
it("respondInteraction rejects duplicate checkbox option ids at the plugin boundary", async () => {
const { companyId } = await seedCompanyAndAgent();
const operatorUserId = randomUUID();
await db.insert(companyMemberships).values({
companyId, principalType: "user", principalId: operatorUserId, status: "active", membershipRole: "operator",
});
const issueId = randomUUID();
await db.insert(issues).values({
id: issueId, companyId, title: "Choose campaigns", status: "in_review", priority: "medium",
});
const interactionId = await seedInteraction(companyId, issueId, {
kind: "request_checkbox_confirmation",
payload: {
version: 1,
prompt: "Which spikes should become campaigns?",
options: [{ id: "spike-a", label: "Spike A" }],
defaultSelectedOptionIds: [],
minSelected: 0,
maxSelected: 1,
} as never,
});
const services = buildHostServices(db, "plugin-record-id", "paperclip.gateway", createEventBusStub());
await expect(services.issues.respondInteraction({
issueId,
interactionId,
companyId,
action: "accept",
actorUserId: operatorUserId,
selectedOptionIds: ["spike-a", "spike-a"],
})).rejects.toThrow("selectedOptionIds must be unique");
const [row] = await db.select().from(issueThreadInteractions).where(eq(issueThreadInteractions.id, interactionId));
expect(row?.status).toBe("pending");
});
it("respondInteraction converges (applied:false) when the interaction is already resolved", async () => {
const { companyId, agentId } = await seedCompanyAndAgent();
const humanUserId = randomUUID();
+5 -4
View File
@@ -28,7 +28,7 @@ import type {
PluginExecutionWorkspaceMetadata,
} from "@paperclipai/plugin-sdk";
import type { CreateIssueThreadInteraction, InviteJoinType, IssueDocumentSummary, PermissionKey, PrincipalType } from "@paperclipai/shared";
import { pluginOperationIssueOriginKind } from "@paperclipai/shared";
import { acceptIssueThreadInteractionSchema, pluginOperationIssueOriginKind } from "@paperclipai/shared";
import { companyService } from "./companies.js";
import { agentService } from "./agents.js";
import { projectService } from "./projects.js";
@@ -2286,12 +2286,13 @@ export function buildHostServices(
status: issue.status,
};
if (params.action === "accept") {
const acceptInput = acceptIssueThreadInteractionSchema.parse({
selectedOptionIds: params.selectedOptionIds,
});
const result = await interactions.acceptInteraction(
{ id: issue.id, companyId, projectId: issue.projectId ?? null, goalId: issue.goalId ?? null },
params.interactionId,
params.selectedOptionIds === undefined
? {}
: { selectedOptionIds: params.selectedOptionIds },
acceptInput,
actor,
);
resolved = result.interaction as typeof current;