From cf1ccd1e1424b5229ca609cb65c0bbcc561e7e03 Mon Sep 17 00:00:00 2001 From: Dotta Date: Fri, 6 Mar 2026 11:22:24 -0600 Subject: [PATCH] Assign invite-joined agents to company CEO --- .../src/__tests__/invite-join-manager.test.ts | 33 +++++++++++++++++++ server/src/routes/access.ts | 22 ++++++++++++- 2 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 server/src/__tests__/invite-join-manager.test.ts diff --git a/server/src/__tests__/invite-join-manager.test.ts b/server/src/__tests__/invite-join-manager.test.ts new file mode 100644 index 00000000..92770a25 --- /dev/null +++ b/server/src/__tests__/invite-join-manager.test.ts @@ -0,0 +1,33 @@ +import { describe, expect, it } from "vitest"; +import { resolveJoinRequestAgentManagerId } from "../routes/access.js"; + +describe("resolveJoinRequestAgentManagerId", () => { + it("returns null when no CEO exists in the company agent list", () => { + const managerId = resolveJoinRequestAgentManagerId([ + { id: "a1", role: "cto", reportsTo: null }, + { id: "a2", role: "engineer", reportsTo: "a1" }, + ]); + + expect(managerId).toBeNull(); + }); + + it("selects the root CEO when available", () => { + const managerId = resolveJoinRequestAgentManagerId([ + { id: "ceo-child", role: "ceo", reportsTo: "manager-1" }, + { id: "manager-1", role: "cto", reportsTo: null }, + { id: "ceo-root", role: "ceo", reportsTo: null }, + ]); + + expect(managerId).toBe("ceo-root"); + }); + + it("falls back to the first CEO when no root CEO is present", () => { + const managerId = resolveJoinRequestAgentManagerId([ + { id: "ceo-1", role: "ceo", reportsTo: "mgr" }, + { id: "ceo-2", role: "ceo", reportsTo: "mgr" }, + { id: "mgr", role: "cto", reportsTo: null }, + ]); + + expect(managerId).toBe("ceo-1"); + }); +}); diff --git a/server/src/routes/access.ts b/server/src/routes/access.ts index ebf86ee1..fade54a4 100644 --- a/server/src/routes/access.ts +++ b/server/src/routes/access.ts @@ -965,6 +965,21 @@ function grantsFromDefaults( return result; } +type JoinRequestManagerCandidate = { + id: string; + role: string; + reportsTo: string | null; +}; + +export function resolveJoinRequestAgentManagerId( + candidates: JoinRequestManagerCandidate[], +): string | null { + const ceoCandidates = candidates.filter((candidate) => candidate.role === "ceo"); + if (ceoCandidates.length === 0) return null; + const rootCeo = ceoCandidates.find((candidate) => candidate.reportsTo === null); + return (rootCeo ?? ceoCandidates[0] ?? null)?.id ?? null; +} + function isInviteTokenHashCollisionError(error: unknown) { const candidates = [ error, @@ -1604,12 +1619,17 @@ export function accessRoutes( req.actor.userId ?? null, ); } else { + const managerId = resolveJoinRequestAgentManagerId(await agents.list(companyId)); + if (!managerId) { + throw conflict("Join request cannot be approved because this company has no active CEO"); + } + const created = await agents.create(companyId, { name: existing.agentName ?? "New Agent", role: "general", title: null, status: "idle", - reportsTo: null, + reportsTo: managerId, capabilities: existing.capabilities ?? null, adapterType: existing.adapterType ?? "process", adapterConfig: