Add organization-managed rules with team targeting#2805
Add organization-managed rules with team targeting#2805elie222 wants to merge 1 commit intomainelie222/inbox-zero:mainfrom claude/enterprise-role-based-rules-kwx5kmelie222/inbox-zero:claude/enterprise-role-based-rules-kwx5kmCopy head branch name to clipboard
Conversation
Organization admins can now define rules at the organization level that are automatically provisioned into every targeted member's email account as locked, managed copies of the rule. The existing rule execution pipeline runs them unchanged. - New OrganizationRule + OrganizationRuleAction models, synced into member accounts via Rule.organizationRuleId (cascade delete) - New OrganizationTeam model: members can be assigned a team (e.g. Engineering, Marketing) and rules can target specific teams; rules with no teams apply to every member - Sync engine provisions/updates/removes managed rules on rule changes, member joins, team changes, and member removal - Members cannot edit, delete, or toggle managed rules: guards in the core rule utilities cover server actions, chat tools, and the v1 API - Org page gets a Rules tab (admin only) for managing rules and teams; members see a "Managed by org" badge on their rules list https://claude.ai/code/session_018psqcCuWEVCr6JVYiw4PBY
|
|
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
There was a problem hiding this comment.
9 issues found across 24 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="apps/web/app/(app)/[emailAccountId]/assistant/Rules.tsx">
<violation number="1" location="apps/web/app/(app)/[emailAccountId]/assistant/Rules.tsx:168">
P2: Organization-managed rules are still openable via row click into an editable RuleDialog, leaving an edit/delete path despite the new menu/toggle locks.</violation>
</file>
<file name="apps/web/prisma/migrations/20260609120000_add_organization_rules/migration.sql">
<violation number="1" location="apps/web/prisma/migrations/20260609120000_add_organization_rules/migration.sql:93">
P2: `Rule.organizationRuleId` FK lacks organization scoping, allowing cross-organization references.</violation>
</file>
<file name="apps/web/prisma/schema.prisma">
<violation number="1" location="apps/web/prisma/schema.prisma:249">
P1: Member.team relation is missing same-organization enforcement, so cross-org team assignments are possible at the DB level.</violation>
<violation number="2" location="apps/web/prisma/schema.prisma:298">
P1: OrganizationRule↔OrganizationTeam relation does not enforce same-organization links, allowing cross-tenant associations.</violation>
</file>
<file name="apps/web/utils/actions/organization.ts">
<violation number="1" location="apps/web/utils/actions/organization.ts:384">
P2: This introduces a non-atomic remove flow: the member is deleted before managed-rule cleanup, so cleanup failure throws after irreversible deletion. Wrap both operations in a transaction or make cleanup best-effort in a deferred/background path to avoid partial-success errors.</violation>
</file>
<file name="apps/web/utils/actions/organization-rule.validation.ts">
<violation number="1" location="apps/web/utils/actions/organization-rule.validation.ts:118">
P2: `teamId` accepts empty strings, which bypasses team existence checks and can trigger DB foreign-key errors instead of clean validation failures.</violation>
</file>
<file name="apps/web/utils/actions/organization-rule.ts">
<violation number="1" location="apps/web/utils/actions/organization-rule.ts:85">
P2: Toggle updates parent and managed copies in separate non-transactional writes, which can leave inconsistent enabled state if the second write fails.</violation>
</file>
<file name="apps/web/utils/organizations/organization-rules.ts">
<violation number="1" location="apps/web/utils/organizations/organization-rules.ts:185">
P2: Member-level org-rule sync drops the apply result, so skipped/failed rule applications are silently ignored and the completion log can falsely imply successful sync.</violation>
<violation number="2" location="apps/web/utils/organizations/organization-rules.ts:262">
P1: Duplicate-name conflicts are treated as a permanent skip instead of retrying and applying a deterministic fallback name, so targeted members can remain without a managed org rule indefinitely.
(Based on your team's feedback about org-rule duplicate-name handling with deterministic fallback.) [FEEDBACK_USED].</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| actions OrganizationRuleAction[] | ||
|
|
||
| // teams this rule applies to; empty means it applies to all members | ||
| teams OrganizationTeam[] |
There was a problem hiding this comment.
P1: OrganizationRule↔OrganizationTeam relation does not enforce same-organization links, allowing cross-tenant associations.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/web/prisma/schema.prisma, line 298:
<comment>OrganizationRule↔OrganizationTeam relation does not enforce same-organization links, allowing cross-tenant associations.</comment>
<file context>
@@ -219,35 +219,113 @@ model EmailAccount {
+ actions OrganizationRuleAction[]
+
+ // teams this rule applies to; empty means it applies to all members
+ teams OrganizationTeam[]
+
+ managedRules Rule[]
</file context>
| organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) | ||
| emailAccount EmailAccount @relation(fields: [emailAccountId], references: [id], onDelete: Cascade) | ||
| teamId String? | ||
| team OrganizationTeam? @relation(fields: [teamId], references: [id], onDelete: SetNull) |
There was a problem hiding this comment.
P1: Member.team relation is missing same-organization enforcement, so cross-org team assignments are possible at the DB level.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/web/prisma/schema.prisma, line 249:
<comment>Member.team relation is missing same-organization enforcement, so cross-org team assignments are possible at the DB level.</comment>
<file context>
@@ -219,35 +219,113 @@ model EmailAccount {
+ organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
+ emailAccount EmailAccount @relation(fields: [emailAccountId], references: [id], onDelete: Cascade)
+ teamId String?
+ team OrganizationTeam? @relation(fields: [teamId], references: [id], onDelete: SetNull)
@@unique([organizationId, emailAccountId])
</file context>
| }); | ||
| return { status: "created" }; | ||
| } catch (error) { | ||
| if (isDuplicateError(error, "name")) { |
There was a problem hiding this comment.
P1: Duplicate-name conflicts are treated as a permanent skip instead of retrying and applying a deterministic fallback name, so targeted members can remain without a managed org rule indefinitely.
(Based on your team's feedback about org-rule duplicate-name handling with deterministic fallback.) .
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/web/utils/organizations/organization-rules.ts, line 262:
<comment>Duplicate-name conflicts are treated as a permanent skip instead of retrying and applying a deterministic fallback name, so targeted members can remain without a managed org rule indefinitely.
(Based on your team's feedback about org-rule duplicate-name handling with deterministic fallback.) .</comment>
<file context>
@@ -0,0 +1,302 @@
+ });
+ return { status: "created" };
+ } catch (error) {
+ if (isDuplicateError(error, "name")) {
+ return {
+ status: "skipped",
</file context>
| <TableBody> | ||
| {rules.map((rule) => { | ||
| const isPlaceholder = rule.id.startsWith("placeholder-"); | ||
| const isOrganizationManaged = !!rule.organizationRuleId; |
There was a problem hiding this comment.
P2: Organization-managed rules are still openable via row click into an editable RuleDialog, leaving an edit/delete path despite the new menu/toggle locks.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/web/app/(app)/[emailAccountId]/assistant/Rules.tsx, line 168:
<comment>Organization-managed rules are still openable via row click into an editable RuleDialog, leaving an edit/delete path despite the new menu/toggle locks.</comment>
<file context>
@@ -164,6 +165,7 @@ export function Rules({
<TableBody>
{rules.map((rule) => {
const isPlaceholder = rule.id.startsWith("placeholder-");
+ const isOrganizationManaged = !!rule.organizationRuleId;
return (
</file context>
| ALTER TABLE "OrganizationRuleAction" ADD CONSTRAINT "OrganizationRuleAction_organizationRuleId_fkey" FOREIGN KEY ("organizationRuleId") REFERENCES "OrganizationRule"("id") ON DELETE CASCADE ON UPDATE CASCADE; | ||
|
|
||
| -- AddForeignKey | ||
| ALTER TABLE "Rule" ADD CONSTRAINT "Rule_organizationRuleId_fkey" FOREIGN KEY ("organizationRuleId") REFERENCES "OrganizationRule"("id") ON DELETE CASCADE ON UPDATE CASCADE; |
There was a problem hiding this comment.
P2: Rule.organizationRuleId FK lacks organization scoping, allowing cross-organization references.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/web/prisma/migrations/20260609120000_add_organization_rules/migration.sql, line 93:
<comment>`Rule.organizationRuleId` FK lacks organization scoping, allowing cross-organization references.</comment>
<file context>
@@ -0,0 +1,100 @@
+ALTER TABLE "OrganizationRuleAction" ADD CONSTRAINT "OrganizationRuleAction_organizationRuleId_fkey" FOREIGN KEY ("organizationRuleId") REFERENCES "OrganizationRule"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "Rule" ADD CONSTRAINT "Rule_organizationRuleId_fkey" FOREIGN KEY ("organizationRuleId") REFERENCES "OrganizationRule"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
</file context>
|
|
||
| await prisma.member.delete({ where: { id: memberId } }); | ||
|
|
||
| await removeOrganizationRulesForMember({ |
There was a problem hiding this comment.
P2: This introduces a non-atomic remove flow: the member is deleted before managed-rule cleanup, so cleanup failure throws after irreversible deletion. Wrap both operations in a transaction or make cleanup best-effort in a deferred/background path to avoid partial-success errors.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/web/utils/actions/organization.ts, line 384:
<comment>This introduces a non-atomic remove flow: the member is deleted before managed-rule cleanup, so cleanup failure throws after irreversible deletion. Wrap both operations in a transaction or make cleanup best-effort in a deferred/background path to avoid partial-success errors.</comment>
<file context>
@@ -358,6 +380,11 @@ export const removeMemberAction = actionClientUser
await prisma.member.delete({ where: { id: memberId } });
+
+ await removeOrganizationRulesForMember({
+ emailAccountId: targetMember.emailAccountId,
+ organizationId: targetMember.organizationId,
</file context>
|
|
||
| export const updateMemberTeamBody = z.object({ | ||
| memberId: z.string().min(1, "Member ID is required"), | ||
| teamId: z.string().nullable(), |
There was a problem hiding this comment.
P2: teamId accepts empty strings, which bypasses team existence checks and can trigger DB foreign-key errors instead of clean validation failures.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/web/utils/actions/organization-rule.validation.ts, line 118:
<comment>`teamId` accepts empty strings, which bypasses team existence checks and can trigger DB foreign-key errors instead of clean validation failures.</comment>
<file context>
@@ -0,0 +1,120 @@
+
+export const updateMemberTeamBody = z.object({
+ memberId: z.string().min(1, "Member ID is required"),
+ teamId: z.string().nullable(),
+});
+export type UpdateMemberTeamBody = z.infer<typeof updateMemberTeamBody>;
</file context>
| await assertTeamsBelongToOrganization({ organizationId, teamIds }); | ||
|
|
||
| try { | ||
| await prisma.organizationRule.update({ |
There was a problem hiding this comment.
P2: Toggle updates parent and managed copies in separate non-transactional writes, which can leave inconsistent enabled state if the second write fails.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/web/utils/actions/organization-rule.ts, line 85:
<comment>Toggle updates parent and managed copies in separate non-transactional writes, which can leave inconsistent enabled state if the second write fails.</comment>
<file context>
@@ -0,0 +1,320 @@
+ await assertTeamsBelongToOrganization({ organizationId, teamIds });
+
+ try {
+ await prisma.organizationRule.update({
+ where: { id },
+ data: {
</file context>
| } | ||
|
|
||
| for (const organizationRule of applicableRules) { | ||
| await applyOrganizationRuleToAccount({ |
There was a problem hiding this comment.
P2: Member-level org-rule sync drops the apply result, so skipped/failed rule applications are silently ignored and the completion log can falsely imply successful sync.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/web/utils/organizations/organization-rules.ts, line 185:
<comment>Member-level org-rule sync drops the apply result, so skipped/failed rule applications are silently ignored and the completion log can falsely imply successful sync.</comment>
<file context>
@@ -0,0 +1,302 @@
+ }
+
+ for (const organizationRule of applicableRules) {
+ await applyOrganizationRuleToAccount({
+ organizationRule,
+ emailAccountId,
</file context>
Organization admins can now define rules at the organization level that are automatically provisioned into every targeted member's email account as locked, managed copies of the rule. The existing rule execution pipeline runs them unchanged.