From e4a114331ef42cd5fb93e7675267bec7e61ac436 Mon Sep 17 00:00:00 2001 From: dotta Date: Tue, 24 Mar 2026 08:52:36 -0500 Subject: [PATCH] Preserve task assignment grants for joined agents --- .../src/__tests__/invite-join-grants.test.ts | 57 +++++++++++++++++++ server/src/routes/access.ts | 32 +++++++---- 2 files changed, 78 insertions(+), 11 deletions(-) create mode 100644 server/src/__tests__/invite-join-grants.test.ts diff --git a/server/src/__tests__/invite-join-grants.test.ts b/server/src/__tests__/invite-join-grants.test.ts new file mode 100644 index 00000000..7dd34267 --- /dev/null +++ b/server/src/__tests__/invite-join-grants.test.ts @@ -0,0 +1,57 @@ +import { describe, expect, it } from "vitest"; +import { agentJoinGrantsFromDefaults } from "../routes/access.js"; + +describe("agentJoinGrantsFromDefaults", () => { + it("adds tasks:assign when invite defaults do not specify agent grants", () => { + expect(agentJoinGrantsFromDefaults(null)).toEqual([ + { + permissionKey: "tasks:assign", + scope: null, + }, + ]); + }); + + it("preserves invite agent grants and appends tasks:assign", () => { + expect( + agentJoinGrantsFromDefaults({ + agent: { + grants: [ + { + permissionKey: "agents:create", + scope: null, + }, + ], + }, + }), + ).toEqual([ + { + permissionKey: "agents:create", + scope: null, + }, + { + permissionKey: "tasks:assign", + scope: null, + }, + ]); + }); + + it("does not duplicate tasks:assign when invite defaults already include it", () => { + expect( + agentJoinGrantsFromDefaults({ + agent: { + grants: [ + { + permissionKey: "tasks:assign", + scope: { projectId: "project-1" }, + }, + ], + }, + }), + ).toEqual([ + { + permissionKey: "tasks:assign", + scope: { projectId: "project-1" }, + }, + ]); + }); +}); diff --git a/server/src/routes/access.ts b/server/src/routes/access.ts index 6d8c5af2..7d7dfe2b 100644 --- a/server/src/routes/access.ts +++ b/server/src/routes/access.ts @@ -1411,6 +1411,25 @@ function grantsFromDefaults( return result; } +export function agentJoinGrantsFromDefaults( + defaultsPayload: Record | null | undefined +): Array<{ + permissionKey: (typeof PERMISSION_KEYS)[number]; + scope: Record | null; +}> { + const grants = grantsFromDefaults(defaultsPayload, "agent"); + if (grants.some((grant) => grant.permissionKey === "tasks:assign")) { + return grants; + } + return [ + ...grants, + { + permissionKey: "tasks:assign", + scope: null + } + ]; +} + type JoinRequestManagerCandidate = { id: string; role: string; @@ -2618,17 +2637,8 @@ export function accessRoutes( "member", "active" ); - await access.setPrincipalPermission( - companyId, - "agent", - created.id, - "tasks:assign", - true, - req.actor.userId ?? null - ); - const grants = grantsFromDefaults( - invite.defaultsPayload as Record | null, - "agent" + const grants = agentJoinGrantsFromDefaults( + invite.defaultsPayload as Record | null ); await access.setPrincipalGrants( companyId,