11import type { OrganizationDelegatedPrincipal , SubjectDelegatedPrincipal } from '@sim/auth/principal'
22import { db } from '@sim/db'
3- import { copilotChats , workspace } from '@sim/db/schema'
3+ import { copilotChats , project , workspace } from '@sim/db/schema'
4+ import { permissionSatisfies } from '@sim/platform-authz/workspace'
45import { and , eq , isNull } from 'drizzle-orm'
56import { defineWorkspaceOperation } from '@/lib/core/application'
67import { authorizeOrganizationOperation } from '@/lib/core/application/organization-authorization'
78import { defineOrganizationOperation } from '@/lib/core/application/organization-operation'
8- import {
9- InsufficientWorkspacePermissionsError ,
10- requireCurrentHumanRole ,
11- } from '@/lib/core/application/workspace-authorization'
12- import { isFeatureEnabled } from '@/lib/core/config/feature-flags'
9+ import { requireCurrentHumanRole } from '@/lib/core/application/workspace-authorization'
1310import { OrchestrationError } from '@/lib/core/orchestration/types'
1411import type { DbTransaction } from '@/lib/db/types'
1512import { defineAuthorizedChatUseCase } from '@/lib/mothership/chat/application/authorized-chat-use-case'
@@ -21,15 +18,13 @@ import type {
2118} from '@/lib/mothership/generated/memory-notes-access'
2219import { assertWorkspaceCapability } from '@/lib/permission-groups/capability-assertions'
2320import { PermissionGroupCapabilityError } from '@/lib/permission-groups/capability-error'
24- import { acquirePermissionGroupOrgReadLock } from '@/lib/permission-groups/locks'
2521import { loadProjectAccess } from '@/lib/projects/application/authorization'
2622import {
2723 requireCurrentCopilotProjectInvocation ,
2824 requireProjectFileOwnerCapabilities ,
2925 requireProjectFileOwnerRole ,
3026} from '@/lib/projects/files/application/authorization'
31- import { lockWorkspaceProject } from '@/lib/projects/membership'
32- import { requireProjectApiEnabled } from '@/lib/projects/rollout.server'
27+ import { isProjectsEnabled , requireProjectApiEnabled } from '@/lib/projects/rollout.server'
3328import { requireCurrentFileSubject } from '@/lib/workspace-files/application/subject'
3429
3530export const MEMORY_NOTES_ACCESS_AUDIENCE = 'sim:memory-access'
@@ -52,6 +47,18 @@ const organizationOperation = defineOrganizationOperation({
5247 delegationAudience : MEMORY_NOTES_ACCESS_AUDIENCE ,
5348} )
5449type MemoryPrincipal = SubjectDelegatedPrincipal | OrganizationDelegatedPrincipal
50+ /** The worker acts on the answer after commit, so a consistent unlocked snapshot is enough. */
51+ const READ_SNAPSHOT = { isolationLevel : 'repeatable read' , accessMode : 'read only' } as const
52+
53+ async function workspaceProject ( tx : DbTransaction , workspaceId : string ) {
54+ const [ row ] = await tx
55+ . select ( { id : project . id , organizationId : project . organizationId } )
56+ . from ( workspace )
57+ . innerJoin ( project , eq ( project . id , workspace . projectId ) )
58+ . where ( eq ( workspace . id , workspaceId ) )
59+ . limit ( 1 )
60+ return row ?? null
61+ }
5562
5663async function workspaceAccess ( tx : DbTransaction , userId : string , workspaceId : string ) {
5764 const [ target ] = await tx
@@ -63,25 +70,17 @@ async function workspaceAccess(tx: DbTransaction, userId: string, workspaceId: s
6370 } )
6471 . from ( workspace )
6572 . where ( and ( eq ( workspace . id , workspaceId ) , isNull ( workspace . archivedAt ) ) )
66- . for ( 'share' )
6773 . limit ( 1 )
6874 if ( ! target ) throw new OrchestrationError ( 'not_found' , 'Workspace not found' )
69- await requireCurrentHumanRole ( userId , target , 'read' , { executor : tx } )
75+ const permission = await requireCurrentHumanRole ( userId , target , 'read' , { executor : tx } )
7076 await assertWorkspaceCapability (
7177 userId ,
7278 workspaceId ,
7379 'copilot.use' ,
7480 target . workspaceOrganizationId ,
7581 tx
7682 )
77- let sharedMutation = true
78- try {
79- await requireCurrentHumanRole ( userId , target , 'write' , { executor : tx } )
80- } catch ( error ) {
81- if ( ! ( error instanceof InsufficientWorkspacePermissionsError ) ) throw error
82- sharedMutation = false
83- }
84- return { target, sharedMutation }
83+ return { target, sharedMutation : permissionSatisfies ( permission , 'write' ) }
8584}
8685
8786async function projectAccess (
@@ -99,22 +98,21 @@ async function projectAccess(
9998 ...( scope . organizationId ? { organizationId : scope . organizationId } : { } ) ,
10099 ...( scope . workspaceId ? { workspaceId : scope . workspaceId } : { } ) ,
101100 } ,
102- 'shared '
101+ 'snapshot '
103102 )
104103 if ( scope . organizationId !== access . record . organizationId )
105104 throw new OrchestrationError ( 'not_found' , 'Project not found in this conversation' )
106- if ( access . record . organizationId )
107- await acquirePermissionGroupOrgReadLock ( tx , access . record . organizationId )
108105 await requireCurrentCopilotProjectInvocation (
109106 tx ,
110107 {
111108 subjectUserId : principal . subjectUserId ,
112109 invocation : { kind : 'chat' , chatId } ,
113110 } ,
114111 access ,
115- scope
112+ scope ,
113+ 'snapshot'
116114 )
117- const owner = await requireProjectFileOwnerRole ( tx , principal , access , 'read' )
115+ const owner = await requireProjectFileOwnerRole ( tx , principal , access , 'read' , 'snapshot' )
118116 let sharedMutation = owner . canWrite
119117 if ( sharedMutation ) {
120118 try {
@@ -167,8 +165,9 @@ const resolveAccess = defineAuthorizedChatUseCase({
167165 } ,
168166 } ,
169167 async execute ( { principal, context, input } ) : Promise < MemoryNotesAccessResult > {
168+ const projectsEnabled = input . target . scope !== 'personal' && ( await isProjectsEnabled ( ) )
170169 return db . transaction ( async ( tx ) => {
171- await requireCurrentFileSubject ( tx , principal )
170+ await requireCurrentFileSubject ( tx , principal , 'snapshot' )
172171 const [ chat ] = await tx
173172 . select ( {
174173 userId : copilotChats . userId ,
@@ -179,7 +178,6 @@ const resolveAccess = defineAuthorizedChatUseCase({
179178 } )
180179 . from ( copilotChats )
181180 . where ( and ( eq ( copilotChats . id , context . chatId ) , isNull ( copilotChats . deletedAt ) ) )
182- . for ( 'share' )
183181 . limit ( 1 )
184182 if (
185183 ! chat ||
@@ -204,9 +202,11 @@ const resolveAccess = defineAuthorizedChatUseCase({
204202 }
205203 const origin =
206204 chat . workspaceId && input . target . scope !== 'personal'
207- ? await lockWorkspaceProject ( tx , chat . workspaceId )
205+ ? await workspaceProject ( tx , chat . workspaceId )
208206 : null
209- if ( chat . workspaceId ) await workspaceAccess ( tx , principal . subjectUserId , chat . workspaceId )
207+ const originAccess = chat . workspaceId
208+ ? await workspaceAccess ( tx , principal . subjectUserId , chat . workspaceId )
209+ : null
210210 const scope = {
211211 organizationId : chat . organizationId ?? origin ?. organizationId ?? null ,
212212 ...( chat . workspaceId ? { workspaceId : chat . workspaceId } : { } ) ,
@@ -218,7 +218,7 @@ const resolveAccess = defineAuthorizedChatUseCase({
218218 permissions : { read : true , privateMutation : true , sharedMutation : false } ,
219219 }
220220 } else if ( input . target . scope === 'project' ) {
221- await requireProjectApiEnabled ( )
221+ if ( ! projectsEnabled ) await requireProjectApiEnabled ( )
222222 if ( chat . workspaceId && origin ?. id !== input . target . projectId )
223223 throw new OrchestrationError ( 'not_found' , 'Project not found in this conversation' )
224224 target = await projectAccess ( tx , principal , context . chatId , input . target . projectId , scope )
@@ -227,13 +227,15 @@ const resolveAccess = defineAuthorizedChatUseCase({
227227 throw new OrchestrationError ( 'not_found' , 'Workspace not found in this conversation' )
228228 const membership = chat . workspaceId
229229 ? origin
230- : await lockWorkspaceProject ( tx , input . target . workspaceId )
230+ : await workspaceProject ( tx , input . target . workspaceId )
231231 if ( ! membership || membership . organizationId !== scope . organizationId )
232232 throw new OrchestrationError ( 'not_found' , 'Workspace not found in this conversation' )
233- const access = await workspaceAccess ( tx , principal . subjectUserId , input . target . workspaceId )
233+ const access =
234+ originAccess ??
235+ ( await workspaceAccess ( tx , principal . subjectUserId , input . target . workspaceId ) )
234236 if ( access . target . workspaceOrganizationId !== scope . organizationId )
235237 throw new OrchestrationError ( 'not_found' , 'Workspace not found in this conversation' )
236- const currentProject = ( await isFeatureEnabled ( 'projects' ) )
238+ const currentProject = projectsEnabled
237239 ? await projectAccess ( tx , principal , context . chatId , membership . id , scope )
238240 : null
239241 target = {
@@ -252,7 +254,7 @@ const resolveAccess = defineAuthorizedChatUseCase({
252254 actor : { userId : principal . subjectUserId , chatId : context . chatId } ,
253255 target,
254256 }
255- } )
257+ } , READ_SNAPSHOT )
256258 } ,
257259} )
258260
0 commit comments