Skip to content

Commit 0c3540a

Browse files
fix(mothership): grant gated entitlements to organization Build chats (#8998)
* fix(mothership): grant gated entitlements to organization Build chats Organization chats in Build or Plan work in the organization's workspaces, but the dashboards, tests and changelog entitlements only declared a workspace evaluator, so an organization owner was never granted them and the worker hid those commands (for example, the agent could not create a native dashboard from org home). Each now also grants on the organization's rollout flag; the gated operations still check the workspace they run against. Assistant turns are unchanged (no entitlements). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lrd1NLD9qx2KBLVnw32c3E * test(mothership): cover each organization entitlement rollout Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QP1Q5MoymJi3ufphqFZfVE --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 59ecfc1 commit 0c3540a

2 files changed

Lines changed: 40 additions & 16 deletions

File tree

‎apps/sim/lib/mothership/chat/payload.test.ts‎

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
billingSubscriptionMock,
66
billingSubscriptionMockFns,
77
} from '@sim/testing/mocks/billing-subscription.mock'
8+
import { featureFlagsMock, featureFlagsMockFns } from '@sim/testing/mocks/feature-flags.mock'
89
import {
910
integrationsAvailabilityMock,
1011
integrationsAvailabilityMockFns,
@@ -70,6 +71,7 @@ vi.mock('@/lib/mothership/chat/workspace-inventory', () => ({
7071
truncated: [],
7172
})),
7273
}))
74+
vi.mock('@/lib/core/config/feature-flags', () => featureFlagsMock)
7375
vi.mock('@/lib/dashboards/application/availability', () => ({
7476
readDashboardAvailability: { execute: mockDashboardAvailability },
7577
}))
@@ -396,19 +398,34 @@ describe('buildCopilotRequestPayload', () => {
396398
}
397399
)
398400

399-
it('never grants the workspace-only dashboards entitlement to an organization chat', async () => {
400-
mockDashboardAvailability.mockResolvedValue(true)
401-
const payload = await buildCopilotRequestPayload({
402-
message: 'Show my dashboard',
403-
userId: 'actor',
404-
userMessageId: 'message-1',
405-
organizationId: 'org-1',
406-
principal: { kind: 'session' as const, userId: 'actor' },
407-
mode: 'agent',
408-
model: '',
409-
})
410-
expect(payload.entitlements).toEqual([])
411-
})
401+
it.each([
402+
['dashboards', 'dashboards', true],
403+
['dashboards', 'dashboards', false],
404+
['tests', 'workflow-tests', true],
405+
['tests', 'workflow-tests', false],
406+
['changelog', 'changelog', true],
407+
['changelog', 'changelog', false],
408+
] as const)(
409+
'grants an organization Build chat the %s entitlement from the %s rollout: %s',
410+
async (entitlement, rolloutFlag, enabled) => {
411+
featureFlagsMockFns.mockIsFeatureEnabled.mockImplementation(
412+
async (flag) => enabled && flag === rolloutFlag
413+
)
414+
const payload = await buildCopilotRequestPayload({
415+
message: 'Build it',
416+
userId: 'actor',
417+
userMessageId: 'message-1',
418+
organizationId: 'org-1',
419+
principal: { kind: 'session' as const, userId: 'actor' },
420+
mode: 'agent',
421+
model: '',
422+
})
423+
expect(payload.entitlements).toEqual(enabled ? [entitlement] : [])
424+
expect(featureFlagsMockFns.mockIsFeatureEnabled).toHaveBeenCalledWith(rolloutFlag, {
425+
orgId: 'org-1',
426+
})
427+
}
428+
)
412429

413430
beforeEach(() => {
414431
mockTrackChatUpload.mockResolvedValue({ displayName: 'payroll.xlsx' })

‎apps/sim/lib/mothership/entitlements.ts‎

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
import type { Principal } from '@sim/auth/principal'
22
import { readChangelogAvailability } from '@/lib/changelog/application/availability'
3+
import { isChangelogEnabled } from '@/lib/changelog/feature-flag'
34
import { readDashboardAvailability } from '@/lib/dashboards/application/availability'
5+
import { isDashboardsEnabled } from '@/lib/dashboards/feature-flag'
46
import { ENTITLEMENTS, type Entitlement } from '@/lib/mothership/generated/protocol'
57
import { readWorkflowTestAvailability } from '@/lib/workflow-tests/application/availability'
8+
import { isWorkflowTestsEnabled } from '@/lib/workflow-tests/feature-flag'
69

710
/** The owner of one chat turn: exactly one of a workspace or an organization. */
811
export interface EntitlementOwner {
@@ -22,9 +25,10 @@ interface OrganizationOwner {
2225
}
2326

2427
/**
25-
* Each entitlement declares the chat scopes it exists in. A scope it does not
26-
* declare is never granted, so a workspace-only capability cannot leak into an
27-
* organization chat that has no workspace to run it against.
28+
* Each entitlement declares the chat scopes it exists in; a scope it does not declare is never
29+
* granted. An organization chat in Build or Plan works in the organization's workspaces, so a
30+
* workspace capability exists there whenever the organization has it rolled out. The operation
31+
* behind it still checks the workspace it runs against.
2832
*/
2933
interface EntitlementEvaluator {
3034
workspace?: (owner: WorkspaceOwner) => Promise<boolean>
@@ -49,14 +53,17 @@ const EVALUATORS: Record<Entitlement, EntitlementEvaluator> = {
4953
[ENTITLEMENTS.dashboards]: {
5054
workspace: ({ principal, workspaceId }) =>
5155
readDashboardAvailability.execute({ principal, input: { workspaceId } }),
56+
organization: ({ organizationId }) => isDashboardsEnabled(organizationId),
5257
},
5358
[ENTITLEMENTS.tests]: {
5459
workspace: ({ principal, workspaceId }) =>
5560
readWorkflowTestAvailability.execute({ principal, input: { workspaceId } }),
61+
organization: ({ organizationId }) => isWorkflowTestsEnabled(organizationId),
5662
},
5763
[ENTITLEMENTS.changelog]: {
5864
workspace: ({ principal, workspaceId }) =>
5965
readChangelogAvailability.execute({ principal, input: { workspaceId } }),
66+
organization: ({ organizationId }) => isChangelogEnabled(organizationId),
6067
},
6168
}
6269

0 commit comments

Comments
 (0)