From 13a220538226f6caece7470538cbeeb769fb663e Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 8 Oct 2026 22:27:56 -0700 Subject: [PATCH 1/3] fix(mothership): let Chat share credentials and request access as the delegating member --- apps/sim/app/api/v2/access-requests.test.ts | 6 + .../[requestId]/cancel/route.ts | 4 +- .../access-requests/discovery/route.ts | 4 +- .../[workspaceId]/access-requests/route.ts | 9 +- .../lib/application/authorization.test.ts | 13 + .../lib/application/authorization.ts | 21 +- .../application/authorized-use-case.test.ts | 22 +- .../lib/application/authorized-use-case.ts | 38 ++- .../copilot-requests.integration.ts | 222 +++++++++++++++++ .../lib/application/operations.ts | 51 ++-- .../lib/application/requests.ts | 68 +++--- .../access-requests/lib/application/review.ts | 8 +- .../copilot-credential-members.integration.ts | 230 ++++++++++++++++++ .../application/credential-members.ts | 3 +- .../lib/credentials/application/operations.ts | 6 +- 15 files changed, 625 insertions(+), 80 deletions(-) create mode 100644 apps/sim/ee/access-requests/lib/application/copilot-requests.integration.ts create mode 100644 apps/sim/lib/credentials/__integration__/copilot-credential-members.integration.ts diff --git a/apps/sim/app/api/v2/access-requests.test.ts b/apps/sim/app/api/v2/access-requests.test.ts index f45fc837a77..3f339f51054 100644 --- a/apps/sim/app/api/v2/access-requests.test.ts +++ b/apps/sim/app/api/v2/access-requests.test.ts @@ -30,6 +30,12 @@ vi.mock('@/ee/access-requests/lib/application/requests', () => ({ }, createAccessRequest: { operation: { id: 'access_requests.create' }, execute: mocks.create }, cancelAccessRequest: { operation: { id: 'access_requests.cancel' }, execute: mocks.cancel }, + workspaceAccessRequestUseCases: { + discover: { operation: { id: 'access_requests.discover' }, execute: mocks.discover }, + listMine: { operation: { id: 'access_requests.list_mine' }, execute: mocks.listMine }, + create: { operation: { id: 'access_requests.create' }, execute: mocks.create }, + cancel: { operation: { id: 'access_requests.cancel' }, execute: mocks.cancel }, + }, listOrganizationAccessRequests: { operation: { id: 'access_requests.list_organization' }, execute: mocks.listOrganization, diff --git a/apps/sim/app/api/v2/workspaces/[workspaceId]/access-requests/[requestId]/cancel/route.ts b/apps/sim/app/api/v2/workspaces/[workspaceId]/access-requests/[requestId]/cancel/route.ts index e120d96eeb0..463eed65c64 100644 --- a/apps/sim/app/api/v2/workspaces/[workspaceId]/access-requests/[requestId]/cancel/route.ts +++ b/apps/sim/app/api/v2/workspaces/[workspaceId]/access-requests/[requestId]/cancel/route.ts @@ -2,7 +2,7 @@ import { v2CancelWorkspaceAccessRequestContract } from '@/lib/api/contracts/v2/a import { defineV2JsonRoute, v2ApiKeyAuth, v2RateLimits } from '@/lib/api/server/routes' import { v2AccessRequestErrorPolicy } from '@/lib/api/server/routes/access-requests' import { accessRequestOperations } from '@/ee/access-requests/lib/application/operations' -import { cancelAccessRequest } from '@/ee/access-requests/lib/application/requests' +import { workspaceAccessRequestUseCases } from '@/ee/access-requests/lib/application/requests' export const POST = defineV2JsonRoute({ contract: v2CancelWorkspaceAccessRequestContract, @@ -14,6 +14,6 @@ export const POST = defineV2JsonRoute({ requestId: params.requestId, scope: { kind: 'workspace' as const, workspaceId: params.workspaceId }, }), - useCase: cancelAccessRequest, + useCase: workspaceAccessRequestUseCases.cancel, present: ({ request }) => ({ data: request }), }) diff --git a/apps/sim/app/api/v2/workspaces/[workspaceId]/access-requests/discovery/route.ts b/apps/sim/app/api/v2/workspaces/[workspaceId]/access-requests/discovery/route.ts index 835bb1fa1d0..1730da9a585 100644 --- a/apps/sim/app/api/v2/workspaces/[workspaceId]/access-requests/discovery/route.ts +++ b/apps/sim/app/api/v2/workspaces/[workspaceId]/access-requests/discovery/route.ts @@ -4,7 +4,7 @@ import { defineV2JsonRoute, v2ApiKeyAuth, v2RateLimits } from '@/lib/api/server/ import { v2AccessRequestErrorPolicy } from '@/lib/api/server/routes/access-requests' import { cursorSortKey, decodeOffsetCursor, encodeOffsetCursor } from '@/app/api/v2/lib/response' import { accessRequestOperations } from '@/ee/access-requests/lib/application/operations' -import { discoverAccessRequests } from '@/ee/access-requests/lib/application/requests' +import { workspaceAccessRequestUseCases } from '@/ee/access-requests/lib/application/requests' function cursorFilters( params: { workspaceId: string }, @@ -33,7 +33,7 @@ export const GET = defineV2JsonRoute({ cursorFilters(params, query) ), }), - useCase: discoverAccessRequests, + useCase: workspaceAccessRequestUseCases.discover, present: ({ entries, hasMore }, { params, query }) => ({ data: entries, nextCursor: hasMore diff --git a/apps/sim/app/api/v2/workspaces/[workspaceId]/access-requests/route.ts b/apps/sim/app/api/v2/workspaces/[workspaceId]/access-requests/route.ts index 54df6252af1..d975bfdd0a4 100644 --- a/apps/sim/app/api/v2/workspaces/[workspaceId]/access-requests/route.ts +++ b/apps/sim/app/api/v2/workspaces/[workspaceId]/access-requests/route.ts @@ -7,10 +7,7 @@ import { defineV2JsonRoute, v2ApiKeyAuth, v2RateLimits } from '@/lib/api/server/ import { v2AccessRequestErrorPolicy } from '@/lib/api/server/routes/access-requests' import { readSortedCursor, writeSortedCursor } from '@/app/api/v2/lib/response' import { accessRequestOperations } from '@/ee/access-requests/lib/application/operations' -import { - createAccessRequest, - listMyAccessRequests, -} from '@/ee/access-requests/lib/application/requests' +import { workspaceAccessRequestUseCases } from '@/ee/access-requests/lib/application/requests' function cursorFilters(params: { workspaceId: string }, query: { status?: string }) { return cursorScopeKey(cursorRoute(v2ListMyWorkspaceAccessRequestsContract, params), { @@ -40,7 +37,7 @@ export const GET = defineV2JsonRoute({ ), }, }), - useCase: listMyAccessRequests, + useCase: workspaceAccessRequestUseCases.listMine, present: ({ requests, nextCursorKeys }, { params, query }) => ({ data: requests, nextCursor: writeSortedCursor( @@ -62,6 +59,6 @@ export const POST = defineV2JsonRoute({ ...body, scope: { kind: 'workspace' as const, workspaceId: params.workspaceId }, }), - useCase: createAccessRequest, + useCase: workspaceAccessRequestUseCases.create, present: ({ request }) => ({ data: request }), }) diff --git a/apps/sim/ee/access-requests/lib/application/authorization.test.ts b/apps/sim/ee/access-requests/lib/application/authorization.test.ts index 06f0d7e56d3..7289620024d 100644 --- a/apps/sim/ee/access-requests/lib/application/authorization.test.ts +++ b/apps/sim/ee/access-requests/lib/application/authorization.test.ts @@ -2,6 +2,7 @@ import { db } from '@sim/db' import { member, permissions, user, workspace } from '@sim/db/schema' import { dbChainMockFns, queueTableRows, resetDbChainMock } from '@sim/testing' import { + createDelegatedPrincipal, createPersonalApiKeyPrincipal, createSessionPrincipal, } from '@sim/testing/factories/principal.factory' @@ -185,6 +186,18 @@ describe('access request scope authorization', () => { expect(dbChainMockFns.select).not.toHaveBeenCalled() }) + it("refuses Chat on a member's organization-scoped request before any lookup", async () => { + const chat = createDelegatedPrincipal({ + subjectUserId: 'person', + workspaceId: 'workspace', + audience: 'sim:settings', + }) + await expect( + authorizeAccessRequestScope(chat, accessRequestOperations.create, organizationScope) + ).rejects.toMatchObject({ detailCode: 'PRINCIPAL_KIND_NOT_PERMITTED' }) + expect(dbChainMockFns.select).not.toHaveBeenCalled() + }) + it('conceals an archived or removed workspace before membership lookup', async () => { queueTableRows(workspace, []) await expect( diff --git a/apps/sim/ee/access-requests/lib/application/authorization.ts b/apps/sim/ee/access-requests/lib/application/authorization.ts index 7321f3a4f41..f2f78ed1f14 100644 --- a/apps/sim/ee/access-requests/lib/application/authorization.ts +++ b/apps/sim/ee/access-requests/lib/application/authorization.ts @@ -1,3 +1,4 @@ +import { requirePrincipalSubjectUserId } from '@sim/auth/principal' import { db } from '@sim/db' import { member, permissions, user, workspace } from '@sim/db/schema' import { @@ -10,13 +11,15 @@ import { isAccountBlocked } from '@/lib/auth/ban' import { authorizeOrganizationOperation } from '@/lib/core/application/organization-authorization' import { authorizeWorkspaceOperation, + PrincipalKindAuthorizationError, requireAllowedWorkspacePrincipal, } from '@/lib/core/application/workspace-authorization' import { OrchestrationError } from '@/lib/core/orchestration/types' import type { DbOrTx } from '@/lib/db/types' -import type { - AccessRequestOperation, - AccessRequestPrincipal, +import { + ACCESS_REQUEST_DELEGATION_AUDIENCE, + type AccessRequestOperation, + type AccessRequestPrincipal, } from '@/ee/access-requests/lib/application/operations' import type { AccessRequestScope } from '@/ee/access-requests/lib/targets' @@ -95,6 +98,10 @@ export async function authorizeAccessRequestScope( if (operation.admin && scope.kind !== 'organization') { throw new OrchestrationError('forbidden', 'Organization administrator access is required') } + if (principal.kind === 'delegated' && scope.kind !== 'workspace') { + throw new PrincipalKindAuthorizationError(principal.kind, operation.id) + } + const actorUserId = requirePrincipalSubjectUserId(principal) let canonicalWorkspace: | Pick | undefined @@ -119,7 +126,7 @@ export async function authorizeAccessRequestScope( } const membership = await loadAccessRequestMembership( executor, - principal.userId, + actorUserId, scope, organizationId, forUpdate @@ -136,7 +143,11 @@ export async function authorizeAccessRequestScope( workspaceOrganizationId: canonicalWorkspace.organizationId, allowPersonalApiKeys: canonicalWorkspace.allowPersonalApiKeys, }, - { executor, forUpdate } + { + executor, + forUpdate, + delegation: { audience: ACCESS_REQUEST_DELEGATION_AUDIENCE, isWithinScope: () => true }, + } ) } else if (scope.kind === 'organization') { await authorizeOrganizationOperation( diff --git a/apps/sim/ee/access-requests/lib/application/authorized-use-case.test.ts b/apps/sim/ee/access-requests/lib/application/authorized-use-case.test.ts index f03e13a6765..6c67c11e253 100644 --- a/apps/sim/ee/access-requests/lib/application/authorized-use-case.test.ts +++ b/apps/sim/ee/access-requests/lib/application/authorized-use-case.test.ts @@ -1,6 +1,7 @@ import type { Principal } from '@sim/auth/principal' import { db } from '@sim/db' import { + createDelegatedPrincipal, createPersonalApiKeyPrincipal, createSessionPrincipal, createWorkspaceApiKeyPrincipal, @@ -95,6 +96,11 @@ describe('authorized access request execution', () => { scopes: ['api:write'], expiresAt: new Date('2099-01-01'), }, + createDelegatedPrincipal({ + subjectUserId: 'requester', + workspaceId: 'workspace', + audience: 'sim:settings', + }), ])( 'preserves the $kind actor through preparation, transactional reauthorization and audit', async (caller) => { @@ -110,7 +116,12 @@ describe('authorized access request execution', () => { projectAudit: () => audit, }) await expect(useCase.execute({ principal: caller, input })).resolves.toBe('result') - expect(prepare).toHaveBeenCalledWith({ principal: caller, input, context }) + expect(prepare).toHaveBeenCalledWith({ + principal: caller, + actorUserId: 'requester', + input, + context, + }) expect(mocks.authorize).toHaveBeenNthCalledWith( 2, caller, @@ -122,6 +133,7 @@ describe('authorized access request execution', () => { ) expect(execute).toHaveBeenCalledWith({ principal: caller, + actorUserId: 'requester', input, context, executor: transaction, @@ -194,7 +206,12 @@ describe('authorized access request execution', () => { execute, }) await expect(useCase.execute({ principal, input })).resolves.toEqual({ id: 'request' }) - expect(prepare).toHaveBeenCalledExactlyOnceWith({ principal, input, context }) + expect(prepare).toHaveBeenCalledExactlyOnceWith({ + principal, + actorUserId: 'requester', + input, + context, + }) expect(prepare.mock.invocationCallOrder[0]).toBeLessThan( vi.mocked(db.transaction).mock.invocationCallOrder[0] ) @@ -213,6 +230,7 @@ describe('authorized access request execution', () => { ) expect(execute).toHaveBeenCalledExactlyOnceWith({ principal, + actorUserId: 'requester', input, context, executor: transaction, diff --git a/apps/sim/ee/access-requests/lib/application/authorized-use-case.ts b/apps/sim/ee/access-requests/lib/application/authorized-use-case.ts index 2780b5748d7..26cc0b4d2f3 100644 --- a/apps/sim/ee/access-requests/lib/application/authorized-use-case.ts +++ b/apps/sim/ee/access-requests/lib/application/authorized-use-case.ts @@ -1,4 +1,4 @@ -import type { Principal } from '@sim/auth/principal' +import { type Principal, requirePrincipalSubjectUserId } from '@sim/auth/principal' import { db } from '@sim/db' import { acquireOrganizationMutationLock } from '@/lib/billing/organizations/membership' import { @@ -13,14 +13,17 @@ import { type AccessRequestContext, authorizeAccessRequestScope, } from '@/ee/access-requests/lib/application/authorization' -import type { - AccessRequestOperation, - AccessRequestPrincipal, +import { + ACCESS_REQUEST_DELEGATION_AUDIENCE, + type AccessRequestOperation, + type AccessRequestPrincipal, } from '@/ee/access-requests/lib/application/operations' import type { AccessRequestScope } from '@/ee/access-requests/lib/targets' interface AccessRequestPreparationArgs { principal: AccessRequestPrincipal + /** The person acting: the caller, or the member Chat acts for. */ + actorUserId: string input: I context: AccessRequestContext } @@ -107,7 +110,8 @@ export function defineAuthorizedAccessRequestUseCase( const scope = definition.scope(input) const initial = await authorizeAccessRequestScope(principal, definition.operation, scope) return runWithOutboundOrganization(initial.organizationId, async () => { - const preparation = { principal, input, context: initial } + const actorUserId = requirePrincipalSubjectUserId(principal) + const preparation = { principal, actorUserId, input, context: initial } let context = initial let result: R if (definition.mutation) { @@ -124,11 +128,11 @@ export function defineAuthorizedAccessRequestUseCase( true, initial ) - return execute({ principal, input, context, executor }) + return execute({ principal, actorUserId, input, context, executor }) }) } else { const execute = await prepareExecution(definition, preparation) - result = await execute({ principal, input, context, executor: db }) + result = await execute({ principal, actorUserId, input, context, executor: db }) } if (definition.projectAudit) { recordProjectedUseCaseAuditEntries( @@ -136,7 +140,14 @@ export function defineAuthorizedAccessRequestUseCase( context.workspaceId, principal, request, - definition.projectAudit({ principal, input, context, executor: db, result }), + definition.projectAudit({ + principal, + actorUserId, + input, + context, + executor: db, + result, + }), context.organizationId ?? undefined ) } @@ -145,3 +156,14 @@ export function defineAuthorizedAccessRequestUseCase( }, } } + +/** + * The workspace routes' copy of a member use case. Declaring the audience admits Chat acting for + * the requester, pinned to its own workspace; the organization routes keep the shared use case, + * so Chat never reaches them. + */ +export function admitWorkspaceDelegation( + useCase: OperationUseCase +): OperationUseCase { + return Object.freeze({ ...useCase, delegationAudience: ACCESS_REQUEST_DELEGATION_AUDIENCE }) +} diff --git a/apps/sim/ee/access-requests/lib/application/copilot-requests.integration.ts b/apps/sim/ee/access-requests/lib/application/copilot-requests.integration.ts new file mode 100644 index 00000000000..fc524b01e0c --- /dev/null +++ b/apps/sim/ee/access-requests/lib/application/copilot-requests.integration.ts @@ -0,0 +1,222 @@ +/** Chat's in-process CLI requesting withheld access for the member it acts for. */ + +import { db } from '@sim/db' +import { + auditLog, + member, + organization, + permissionAccessRequest, + permissionGroup, + permissionGroupWorkspace, + permissions, + user, + workspace, +} from '@sim/db/schema' +import { envFlagsMock } from '@sim/testing/mocks/env-flags.mock' +import { generateId } from '@sim/utils/id' +import { and, eq, inArray } from 'drizzle-orm' +import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest' + +vi.mock('@/lib/core/config/env-flags', () => ({ ...envFlagsMock, isAccessControlEnabled: true })) + +import { withWorkspaceInvocationScope } from '@/lib/core/application/workspace-invocation-scope' +import { createScopedCliTransport } from '@/lib/mothership/agent-cli/scoped-transport' +import '@/app/api/v2/workspaces/[workspaceId]/access-requests/route' +import '@/app/api/v2/workspaces/[workspaceId]/access-requests/discovery/route' +import '@/app/api/v2/workspaces/[workspaceId]/access-requests/[requestId]/cancel/route' +import '@/app/api/v2/organizations/[organizationId]/access-requests/route' + +const ORIGIN = 'http://localhost:3000' +const organizationId = generateId() +const workspaceId = generateId() +const otherWorkspaceId = generateId() +const requesterId = generateId() +const peerId = generateId() +const groupId = generateId() +const target = { kind: 'feature', configKey: 'hideTablesTab' } as const + +function chat(userId: string, chatWorkspaceId = workspaceId) { + const transport = createScopedCliTransport(ORIGIN, { + userId, + workspaceId: chatWorkspaceId, + chatId: generateId(), + }) + return (path: string, body?: unknown) => + withWorkspaceInvocationScope({ workspaceId: chatWorkspaceId, organizationId }, () => + transport( + `${ORIGIN}${path}`, + body === undefined + ? undefined + : { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify(body), + } + ) + ) +} + +function requestsOf(userId: string) { + return db + .select({ id: permissionAccessRequest.id, status: permissionAccessRequest.status }) + .from(permissionAccessRequest) + .where(eq(permissionAccessRequest.requesterId, userId)) +} + +describe('chat-delegated workspace access requests', () => { + beforeAll(async () => { + const now = new Date() + await db.insert(user).values( + [requesterId, peerId].map((id) => ({ + id, + name: 'Access request fixture', + email: `${id}@access-request.test`, + emailVerified: true, + createdAt: now, + updatedAt: now, + })) + ) + await db.insert(organization).values({ + id: organizationId, + name: 'Access request fixture', + slug: organizationId, + createdAt: now, + updatedAt: now, + }) + await db.insert(member).values( + [requesterId, peerId].map((userId) => ({ + id: generateId(), + userId, + organizationId, + role: 'member', + createdAt: now, + })) + ) + await db.insert(workspace).values( + [workspaceId, otherWorkspaceId].map((id) => ({ + id, + name: 'Access request fixture', + ownerId: peerId, + billedAccountUserId: peerId, + organizationId, + workspaceMode: 'organization' as const, + })) + ) + await db.insert(permissions).values([ + ...[requesterId, peerId].map((userId) => ({ + id: generateId(), + userId, + entityType: 'workspace', + entityId: workspaceId, + permissionType: 'write' as const, + })), + { + id: generateId(), + userId: peerId, + entityType: 'workspace', + entityId: otherWorkspaceId, + permissionType: 'write' as const, + }, + ]) + await db.insert(permissionGroup).values({ + id: groupId, + organizationId, + name: 'Tables withheld', + createdBy: peerId, + config: { hideTablesTab: true }, + }) + await db.insert(permissionGroupWorkspace).values({ + id: generateId(), + permissionGroupId: groupId, + workspaceId, + organizationId, + }) + }) + + afterAll(async () => { + await db + .delete(permissionAccessRequest) + .where(eq(permissionAccessRequest.organizationId, organizationId)) + await db.delete(auditLog).where(eq(auditLog.workspaceId, workspaceId)) + await db.delete(workspace).where(inArray(workspace.id, [workspaceId, otherWorkspaceId])) + await db.delete(organization).where(eq(organization.id, organizationId)) + await db.delete(user).where(inArray(user.id, [requesterId, peerId])) + }) + + it('discovers, requests, lists and cancels a withheld feature as the delegating member', async () => { + const asRequester = chat(requesterId) + const base = `/api/v2/workspaces/${workspaceId}/access-requests` + + const discovery = await asRequester(`${base}/discovery?targetKind=feature&state=requestable`) + expect(discovery.status).toBe(200) + expect((await discovery.json()).data).toContainEqual( + expect.objectContaining({ target, state: 'requestable', pendingRequestId: null }) + ) + + const created = await asRequester(base, { target, reason: 'Need tables for a report' }) + expect(created.status).toBe(200) + const { data: request } = await created.json() + expect(request).toMatchObject({ status: 'pending', requester: { id: requesterId } }) + await vi.waitFor(async () => { + const [audit] = await db + .select({ actorId: auditLog.actorId, metadata: auditLog.metadata }) + .from(auditLog) + .where(and(eq(auditLog.workspaceId, workspaceId), eq(auditLog.resourceId, request.id))) + expect(audit).toMatchObject({ + actorId: requesterId, + metadata: { + operation: 'access_requests.create', + actor: { kind: 'delegated', serviceId: 'copilot' }, + }, + }) + }) + + const mine = await asRequester(base) + expect(mine.status).toBe(200) + expect((await mine.json()).data.map((entry: { id: string }) => entry.id)).toEqual([request.id]) + + const cancelled = await asRequester(`${base}/${request.id}/cancel`, {}) + expect(cancelled.status).toBe(200) + expect(await cancelled.json()).toMatchObject({ data: { id: request.id, status: 'cancelled' } }) + }) + + it("refuses cancelling another member's request", async () => { + const base = `/api/v2/workspaces/${workspaceId}/access-requests` + const created = await chat(peerId)(base, { target }) + expect(created.status).toBe(200) + const { data: request } = await created.json() + + const response = await chat(requesterId)(`${base}/${request.id}/cancel`, {}) + + expect(response.status).toBe(404) + expect(await response.json()).toMatchObject({ + error: { code: 'NOT_FOUND', message: 'Access request scope not found' }, + }) + expect(await requestsOf(peerId)).toEqual([{ id: request.id, status: 'pending' }]) + }) + + it('refuses a chat pinned to another workspace', async () => { + const response = await chat(peerId, otherWorkspaceId)( + `/api/v2/workspaces/${workspaceId}/access-requests`, + { target: { kind: 'feature', configKey: 'hideFilesTab' } } + ) + + expect(response.status).toBe(404) + expect(await response.json()).toMatchObject({ + error: { code: 'NOT_FOUND', message: 'Access request scope not found' }, + }) + expect(await requestsOf(peerId)).toHaveLength(1) + }) + + it('keeps organization access requests direct-only', async () => { + const response = await chat(requesterId)( + `/api/v2/organizations/${organizationId}/access-requests`, + { target } + ) + + expect(response.status).toBe(403) + expect(await response.json()).toMatchObject({ + error: { code: 'FORBIDDEN', message: 'This operation is unavailable through Mothership.' }, + }) + }) +}) diff --git a/apps/sim/ee/access-requests/lib/application/operations.ts b/apps/sim/ee/access-requests/lib/application/operations.ts index c2a8e8a2e4c..0af98a38bd9 100644 --- a/apps/sim/ee/access-requests/lib/application/operations.ts +++ b/apps/sim/ee/access-requests/lib/application/operations.ts @@ -1,11 +1,13 @@ -import type { Principal } from '@sim/auth/principal' +import type { DelegatedPrincipal, Principal } from '@sim/auth/principal' import { defineOrganizationOperation } from '@/lib/core/application/organization-operation' import { defineWorkspaceOperation } from '@/lib/core/application/workspace-operation' -export type AccessRequestPrincipal = Extract< - Principal, - { kind: 'session' | 'personal_api_key' | 'oauth_access_token' } -> +/** Chat acting for the requester joins the direct callers on the workspace routes only. */ +export type AccessRequestPrincipal = + | Extract + | (DelegatedPrincipal & { serviceId: 'copilot' }) + +export const ACCESS_REQUEST_DELEGATION_AUDIENCE = 'sim:settings' function defineAccessRequestOperation( id: string, @@ -22,17 +24,34 @@ function defineAccessRequestOperation( */ capability: 'none', }) - const workspaceOperation = defineWorkspaceOperation({ - id, - oauthScope, - minimumRole: 'read', - workspaceApiKey: 'deny', - principalKinds: ['session', 'personal_api_key', 'oauth_access_token'], - /** - * permission-group-exempt: requests never grant a withheld capability without administrator review. - */ - capability: 'none', - }) + /** + * A member's own requests admit Chat delegated by that member. Review stays with direct + * callers: it is organization-scoped and never reached through a workspace. + */ + const workspaceOperation = admin + ? defineWorkspaceOperation({ + id, + oauthScope, + minimumRole: 'read', + workspaceApiKey: 'deny', + principalKinds: ['session', 'personal_api_key', 'oauth_access_token'], + /** + * permission-group-exempt: requests never grant a withheld capability without administrator review. + */ + capability: 'none', + }) + : defineWorkspaceOperation({ + id, + oauthScope, + minimumRole: 'read', + workspaceApiKey: 'deny', + principalKinds: ['session', 'personal_api_key', 'oauth_access_token', 'delegated'], + delegatedServices: ['copilot'], + /** + * permission-group-exempt: requests never grant a withheld capability without administrator review. + */ + capability: 'none', + }) return Object.freeze({ ...workspaceOperation, organizationOperation, admin }) } diff --git a/apps/sim/ee/access-requests/lib/application/requests.ts b/apps/sim/ee/access-requests/lib/application/requests.ts index 10c9a4ac83f..2af23adb248 100644 --- a/apps/sim/ee/access-requests/lib/application/requests.ts +++ b/apps/sim/ee/access-requests/lib/application/requests.ts @@ -14,7 +14,10 @@ import type { DbOrTx } from '@/lib/db/types' import { acquirePermissionGroupOrgLock } from '@/lib/permission-groups/locks' import type { AccessRequestContext } from '@/ee/access-requests/lib/application/authorization' import { loadAccessRequestMembership } from '@/ee/access-requests/lib/application/authorization' -import { defineAuthorizedAccessRequestUseCase } from '@/ee/access-requests/lib/application/authorized-use-case' +import { + admitWorkspaceDelegation, + defineAuthorizedAccessRequestUseCase, +} from '@/ee/access-requests/lib/application/authorized-use-case' import { accessRequestOperations } from '@/ee/access-requests/lib/application/operations' import { prepareAccessRequestPolicy } from '@/ee/access-requests/lib/application/prepare' import { @@ -115,13 +118,13 @@ async function hasCurrentMemberLimitMembership( export const discoverAccessRequests = defineAuthorizedAccessRequestUseCase({ operation: accessRequestOperations.discover, scope: (input: DiscoverAccessRequestsInput) => input, - async execute({ input, principal, context, executor }): Promise { + async execute({ input, actorUserId, context, executor }): Promise { const organizationId = context.organizationId if (!organizationId || !(await isAccessRequestEnabled(organizationId, executor))) return { enabled: false, organizationId, entries: [], total: 0, hasMore: false } const catalog = await loadAccessRequestCatalog( { - userId: principal.userId, + userId: actorUserId, organizationId, workspaceId: context.workspaceId, }, @@ -139,7 +142,7 @@ export const discoverAccessRequests = defineAuthorizedAccessRequestUseCase({ ) }) const offset = input.offset ?? 0 - const policy = await loadAccessRequestPolicy(executor, context, principal.userId) + const policy = await loadAccessRequestPolicy(executor, context, actorUserId) const pending = await executor .select({ id: permissionAccessRequest.id, @@ -152,7 +155,7 @@ export const discoverAccessRequests = defineAuthorizedAccessRequestUseCase({ .where( and( eq(permissionAccessRequest.organizationId, organizationId), - eq(permissionAccessRequest.requesterId, principal.userId), + eq(permissionAccessRequest.requesterId, actorUserId), or( eq(permissionAccessRequest.scopeKey, accessRequestScopeKey(input)), eq(permissionAccessRequest.scopeKey, memberLimitScopeKey(organizationId)) @@ -164,12 +167,7 @@ export const discoverAccessRequests = defineAuthorizedAccessRequestUseCase({ .limit(ACCESS_REQUEST_MAX_PENDING) const pendingMemberLimit = pending.find((row) => row.targetKey === 'usage_limit:member') const memberLimitMembershipMatches = pendingMemberLimit - ? await hasCurrentMemberLimitMembership( - executor, - context, - principal.userId, - pendingMemberLimit - ) + ? await hasCurrentMemberLimitMembership(executor, context, actorUserId, pendingMemberLimit) : false const pendingByTarget = new Map( pending @@ -186,7 +184,7 @@ export const discoverAccessRequests = defineAuthorizedAccessRequestUseCase({ const result = await evaluateAccessRequestTarget( executor, context, - principal.userId, + actorUserId, input, target, catalog, @@ -233,12 +231,12 @@ interface MutationResult { export const createAccessRequest = defineAuthorizedAccessRequestUseCase({ operation: accessRequestOperations.create, scope: (input: CreateAccessRequestInput) => input.scope, - prepare: ({ principal, context, input }) => - prepareAccessRequestPolicy(context, principal.userId, input.target.kind), + prepare: ({ actorUserId, context, input }) => + prepareAccessRequestPolicy(context, actorUserId, input.target.kind), mutation: true, async execute({ - principal, input, + actorUserId, context, executor, prepared, @@ -261,7 +259,7 @@ export const createAccessRequest = defineAuthorizedAccessRequestUseCase({ target.kind === 'usage_limit' ? await loadAccessRequestMembership( executor, - principal.userId, + actorUserId, { kind: 'organization', organizationId }, organizationId ) @@ -275,13 +273,13 @@ export const createAccessRequest = defineAuthorizedAccessRequestUseCase({ const currentPolicy = await loadAccessRequestPolicy( executor, context, - principal.userId, + actorUserId, prepared.entitled ) const policy = await evaluateAccessRequestTarget( executor, context, - principal.userId, + actorUserId, input.scope, target, catalog, @@ -293,7 +291,7 @@ export const createAccessRequest = defineAuthorizedAccessRequestUseCase({ .where( and( eq(permissionAccessRequest.organizationId, organizationId), - eq(permissionAccessRequest.requesterId, principal.userId), + eq(permissionAccessRequest.requesterId, actorUserId), eq(permissionAccessRequest.scopeKey, scopeKey), eq(permissionAccessRequest.targetKey, targetKey), eq(permissionAccessRequest.status, 'pending') @@ -322,7 +320,7 @@ export const createAccessRequest = defineAuthorizedAccessRequestUseCase({ const membership = origin ? await loadAccessRequestMembership( executor, - principal.userId, + actorUserId, { kind: 'workspace', workspaceId: pending.workspaceId }, organizationId ) @@ -369,7 +367,7 @@ export const createAccessRequest = defineAuthorizedAccessRequestUseCase({ .where( and( eq(permissionAccessRequest.organizationId, organizationId), - eq(permissionAccessRequest.requesterId, principal.userId), + eq(permissionAccessRequest.requesterId, actorUserId), eq(permissionAccessRequest.status, 'pending') ) ) @@ -384,7 +382,7 @@ export const createAccessRequest = defineAuthorizedAccessRequestUseCase({ .where( and( eq(permissionAccessRequest.organizationId, organizationId), - eq(permissionAccessRequest.requesterId, principal.userId), + eq(permissionAccessRequest.requesterId, actorUserId), gte( permissionAccessRequest.createdAt, new Date(Date.now() - ACCESS_REQUEST_SUBMISSION_WINDOW_MS) @@ -401,7 +399,7 @@ export const createAccessRequest = defineAuthorizedAccessRequestUseCase({ .values({ id: generateId(), organizationId, - requesterId: principal.userId, + requesterId: actorUserId, workspaceId: requestWorkspaceId, scopeKey, targetKey, @@ -457,13 +455,13 @@ interface ListMineInput { export const listMyAccessRequests = defineAuthorizedAccessRequestUseCase({ operation: accessRequestOperations.listMine, scope: (input: ListMineInput) => input.scope, - async execute({ principal, input, context, executor }) { + async execute({ input, actorUserId, context, executor }) { if (!context.organizationId) return { requests: [], total: 0, hasMore: false } return listAccessRequestRecords( executor, and( eq(permissionAccessRequest.organizationId, context.organizationId), - eq(permissionAccessRequest.requesterId, principal.userId), + eq(permissionAccessRequest.requesterId, actorUserId), input.requestId ? eq(permissionAccessRequest.id, input.requestId) : undefined, input.status ? eq(permissionAccessRequest.status, input.status) : undefined, or( @@ -487,7 +485,7 @@ export const cancelAccessRequest = defineAuthorizedAccessRequestUseCase({ operation: accessRequestOperations.cancel, scope: (input: CancelInput) => input.scope, mutation: true, - async execute({ principal, input, context, executor }): Promise { + async execute({ input, actorUserId, executor, context }): Promise { const row = await loadStoredAccessRequest( executor, requireOrganization(context.organizationId), @@ -495,7 +493,7 @@ export const cancelAccessRequest = defineAuthorizedAccessRequestUseCase({ true ) if ( - row.requesterId !== principal.userId || + row.requesterId !== actorUserId || (row.scopeKey !== accessRequestScopeKey(input.scope) && row.scopeKey !== memberLimitScopeKey(row.organizationId)) ) @@ -506,7 +504,7 @@ export const cancelAccessRequest = defineAuthorizedAccessRequestUseCase({ .update(permissionAccessRequest) .set({ status: 'cancelled', - decidedBy: principal.userId, + decidedBy: actorUserId, decidedAt: new Date(), updatedAt: new Date(), }) @@ -530,6 +528,14 @@ export const cancelAccessRequest = defineAuthorizedAccessRequestUseCase({ : [], }) +/** A member's own requests, served to the workspace routes and to Chat acting for that member. */ +export const workspaceAccessRequestUseCases = { + discover: admitWorkspaceDelegation(discoverAccessRequests), + create: admitWorkspaceDelegation(createAccessRequest), + listMine: admitWorkspaceDelegation(listMyAccessRequests), + cancel: admitWorkspaceDelegation(cancelAccessRequest), +} as const + interface OrganizationInput { organizationId: string } @@ -573,19 +579,19 @@ export const updateAccessRequestSettings = defineAuthorizedAccessRequestUseCase( operation: accessRequestOperations.updateSettings, scope: (input: UpdateSettingsInput) => organizationScope(input), mutation: true, - async execute({ principal, input, executor }): Promise { + async execute({ input, actorUserId, executor }): Promise { await executor .insert(organizationAccessRequestSettings) .values({ organizationId: input.organizationId, allowRequests: input.allowRequests, - updatedBy: principal.userId, + updatedBy: actorUserId, }) .onConflictDoUpdate({ target: organizationAccessRequestSettings.organizationId, set: { allowRequests: input.allowRequests, - updatedBy: principal.userId, + updatedBy: actorUserId, updatedAt: new Date(), }, }) diff --git a/apps/sim/ee/access-requests/lib/application/review.ts b/apps/sim/ee/access-requests/lib/application/review.ts index 19d68dabe3a..7174de98e5d 100644 --- a/apps/sim/ee/access-requests/lib/application/review.ts +++ b/apps/sim/ee/access-requests/lib/application/review.ts @@ -255,7 +255,7 @@ export const resolveAccessRequest = defineAuthorizedAccessRequestUseCase({ storedAccessRequestTargetSchema.parse(row.target).kind ) }, - async execute({ principal, input, executor, prepared }) { + async execute({ input, actorUserId, executor, prepared }) { const row = await loadStoredAccessRequest(executor, input.organizationId, input.requestId, true) if (row.status !== 'pending') return { request: await presentAccessRequest(executor, row), changed: false } @@ -266,7 +266,7 @@ export const resolveAccessRequest = defineAuthorizedAccessRequestUseCase({ .set({ status: 'declined', decisionReason: input.decision.reason, - decidedBy: principal.userId, + decidedBy: actorUserId, decidedAt: now, updatedAt: now, }) @@ -306,7 +306,7 @@ export const resolveAccessRequest = defineAuthorizedAccessRequestUseCase({ row.organizationId, row.requesterId, creditsToDollars(newLimitCredits), - principal.userId, + actorUserId, executor ) } else { @@ -344,7 +344,7 @@ export const resolveAccessRequest = defineAuthorizedAccessRequestUseCase({ .update(permissionAccessRequest) .set({ status: 'fulfilled', - decidedBy: principal.userId, + decidedBy: actorUserId, decidedAt: now, updatedAt: now, decision, diff --git a/apps/sim/lib/credentials/__integration__/copilot-credential-members.integration.ts b/apps/sim/lib/credentials/__integration__/copilot-credential-members.integration.ts new file mode 100644 index 00000000000..46bf4e3e54b --- /dev/null +++ b/apps/sim/lib/credentials/__integration__/copilot-credential-members.integration.ts @@ -0,0 +1,230 @@ +/** Chat's in-process CLI sharing a workspace OAuth credential as the delegating credential admin. */ + +import { db } from '@sim/db' +import { + account, + auditLog, + credential, + credentialMember, + member, + organization, + permissions, + user, + workspace, +} from '@sim/db/schema' +import { generateId } from '@sim/utils/id' +import { and, eq, inArray } from 'drizzle-orm' +import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest' +import { withWorkspaceInvocationScope } from '@/lib/core/application/workspace-invocation-scope' +import { createScopedCliTransport } from '@/lib/mothership/agent-cli/scoped-transport' +import '@/app/api/v2/credentials/[credentialId]/members/route' +import '@/app/api/v2/credentials/[credentialId]/members/[userId]/route' + +const ORIGIN = 'http://localhost:3000' +const organizationId = generateId() +const workspaceId = generateId() +const otherWorkspaceId = generateId() +const adminId = generateId() +const teammateId = generateId() +const writerId = generateId() +const outsiderId = generateId() +const credentialId = generateId() +const userIds = [adminId, teammateId, writerId, outsiderId] + +function chat(userId: string, chatWorkspaceId = workspaceId) { + const transport = createScopedCliTransport(ORIGIN, { + userId, + workspaceId: chatWorkspaceId, + chatId: generateId(), + }) + return (path: string, init?: { method: string; body?: unknown }) => + withWorkspaceInvocationScope({ workspaceId: chatWorkspaceId, organizationId }, () => + transport(`${ORIGIN}/api/v2/credentials/${credentialId}/members${path}`, { + method: init?.method ?? 'GET', + ...(init?.body === undefined + ? {} + : { + headers: { 'content-type': 'application/json' }, + body: JSON.stringify(init.body), + }), + }) + ) +} + +const query = `?workspaceId=${workspaceId}` + +function activeGrantsFor(userId: string) { + return db + .select({ role: credentialMember.role }) + .from(credentialMember) + .where( + and( + eq(credentialMember.credentialId, credentialId), + eq(credentialMember.userId, userId), + eq(credentialMember.status, 'active') + ) + ) +} + +describe('chat-delegated credential sharing', () => { + beforeAll(async () => { + const now = new Date() + await db.insert(user).values( + userIds.map((id) => ({ + id, + name: 'Credential sharing fixture', + email: `${id}@credential-sharing.test`, + emailVerified: true, + createdAt: now, + updatedAt: now, + })) + ) + await db.insert(organization).values({ + id: organizationId, + name: 'Credential sharing fixture', + slug: organizationId, + createdAt: now, + updatedAt: now, + }) + await db.insert(member).values( + userIds.map((userId) => ({ + id: generateId(), + userId, + organizationId, + role: 'member', + createdAt: now, + })) + ) + await db.insert(workspace).values( + [workspaceId, otherWorkspaceId].map((id) => ({ + id, + name: 'Credential sharing fixture', + ownerId: adminId, + billedAccountUserId: adminId, + organizationId, + workspaceMode: 'organization' as const, + })) + ) + await db.insert(permissions).values( + ( + [ + [adminId, workspaceId, 'admin'], + [adminId, otherWorkspaceId, 'admin'], + [teammateId, workspaceId, 'write'], + [writerId, workspaceId, 'write'], + [outsiderId, otherWorkspaceId, 'write'], + ] as const + ).map(([userId, entityId, permissionType]) => ({ + id: generateId(), + userId, + entityType: 'workspace', + entityId, + permissionType, + })) + ) + const accountId = generateId() + await db.insert(account).values({ + id: accountId, + userId: adminId, + providerId: 'slack', + accountId: generateId(), + accessToken: 'fixture-access', + createdAt: now, + updatedAt: now, + }) + await db.insert(credential).values({ + id: credentialId, + type: 'oauth', + workspaceId, + accountId, + providerId: 'slack', + displayName: 'Slack fixture', + createdBy: adminId, + }) + }) + + afterAll(async () => { + await db.delete(auditLog).where(eq(auditLog.workspaceId, workspaceId)) + await db.delete(workspace).where(inArray(workspace.id, [workspaceId, otherWorkspaceId])) + await db.delete(organization).where(eq(organization.id, organizationId)) + await db.delete(user).where(inArray(user.id, userIds)) + }) + + it('shares, lists and unshares the credential as its admin', async () => { + const asAdmin = chat(adminId) + + const shared = await asAdmin(query, { + method: 'POST', + body: { userId: teammateId, role: 'member' }, + }) + expect(shared.status).toBe(201) + expect(await shared.json()).toMatchObject({ + data: { userId: teammateId, role: 'member', created: true }, + }) + expect(await activeGrantsFor(teammateId)).toEqual([{ role: 'member' }]) + await vi.waitFor(async () => { + const [audit] = await db + .select({ actorId: auditLog.actorId, metadata: auditLog.metadata }) + .from(auditLog) + .where(and(eq(auditLog.workspaceId, workspaceId), eq(auditLog.resourceId, credentialId))) + expect(audit).toMatchObject({ + actorId: adminId, + metadata: { + operation: 'credentials.members.upsert', + actor: { kind: 'delegated', serviceId: 'copilot' }, + targetUserId: teammateId, + }, + }) + }) + + const listed = await asAdmin(query) + expect(listed.status).toBe(200) + expect((await listed.json()).data).toContainEqual( + expect.objectContaining({ userId: teammateId, role: 'member' }) + ) + + const removed = await asAdmin(`/${teammateId}${query}`, { method: 'DELETE' }) + expect(removed.status).toBe(200) + expect(await removed.json()).toMatchObject({ data: { userId: teammateId, revoked: true } }) + expect(await activeGrantsFor(teammateId)).toEqual([]) + }) + + it('refuses sharing from a workspace member who is not a credential admin', async () => { + const response = await chat(writerId)(query, { + method: 'POST', + body: { userId: teammateId, role: 'admin' }, + }) + + expect(response.status).toBe(403) + expect(await response.json()).toMatchObject({ + error: { code: 'FORBIDDEN', details: { code: 'CREDENTIAL_ADMIN_ACCESS_REQUIRED' } }, + }) + expect(await activeGrantsFor(teammateId)).toEqual([]) + }) + + it('refuses sharing with someone outside the credential workspace', async () => { + const response = await chat(adminId)(query, { + method: 'POST', + body: { userId: outsiderId, role: 'member' }, + }) + + expect(response.status).toBe(400) + expect(await response.json()).toMatchObject({ + error: { message: 'Target user must belong to the credential workspace' }, + }) + expect(await activeGrantsFor(outsiderId)).toEqual([]) + }) + + it('refuses a chat pinned to another workspace', async () => { + const response = await chat(adminId, otherWorkspaceId)(query, { + method: 'POST', + body: { userId: teammateId, role: 'member' }, + }) + + expect(response.status).toBe(404) + expect(await response.json()).toMatchObject({ + error: { code: 'NOT_FOUND', message: 'Resource not found in the selected workspace' }, + }) + expect(await activeGrantsFor(teammateId)).toEqual([]) + }) +}) diff --git a/apps/sim/lib/credentials/application/credential-members.ts b/apps/sim/lib/credentials/application/credential-members.ts index e217d98bdea..691a7276642 100644 --- a/apps/sim/lib/credentials/application/credential-members.ts +++ b/apps/sim/lib/credentials/application/credential-members.ts @@ -3,6 +3,7 @@ import { requirePrincipalSubjectUserId, type SessionPrincipal } from '@sim/auth/ import { defineAuthorizedWorkspaceUseCase } from '@/lib/core/application' import { OrchestrationError } from '@/lib/core/orchestration/types' import { requireOrdinaryCredentialType } from '@/lib/credentials/access' +import { credentialDelegationPolicy } from '@/lib/credentials/application/authorization' import { defineAuthorizedCredentialUseCase, requireManageableCredentialType, @@ -37,7 +38,7 @@ export const listCredentialMembersUseCase = defineAuthorizedWorkspaceUseCase({ }: { input: CredentialMemberResourceInput & Partial }) => resolveCredentialApplicationContext(input), - authorizationOptions: {}, + authorizationOptions: { delegation: credentialDelegationPolicy }, async execute({ principal, input, context }) { requireManageableCredentialType(principal, context.credential) if (context.credential.type === 'personal_token') { diff --git a/apps/sim/lib/credentials/application/operations.ts b/apps/sim/lib/credentials/application/operations.ts index 3d45db277f4..566e5534e1e 100644 --- a/apps/sim/lib/credentials/application/operations.ts +++ b/apps/sim/lib/credentials/application/operations.ts @@ -196,7 +196,7 @@ export const credentialOperations = { minimumRole: 'read', workspaceApiKey: 'deny', capability: 'integrations.manage', - principalKinds: ['session', 'personal_api_key', 'oauth_access_token'], + ...HUMAN_AND_COPILOT_PRINCIPALS, }), upsertMember: defineCredentialOperation( defineWorkspaceOperation({ @@ -205,7 +205,7 @@ export const credentialOperations = { minimumRole: 'read', workspaceApiKey: 'deny', capability: 'integrations.manage', - principalKinds: ['session', 'personal_api_key', 'oauth_access_token'], + ...HUMAN_AND_COPILOT_PRINCIPALS, }), 'admin' ), @@ -216,7 +216,7 @@ export const credentialOperations = { minimumRole: 'read', workspaceApiKey: 'deny', capability: 'integrations.manage', - principalKinds: ['session', 'personal_api_key', 'oauth_access_token'], + ...HUMAN_AND_COPILOT_PRINCIPALS, }), 'admin' ), From 60624ef5fae7c35e3615158511217fd3fddc2394 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 8 Oct 2026 22:57:25 -0700 Subject: [PATCH 2/3] test(access-requests): leave Chat's actor proof to the integration suite --- .../lib/application/authorized-use-case.test.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/apps/sim/ee/access-requests/lib/application/authorized-use-case.test.ts b/apps/sim/ee/access-requests/lib/application/authorized-use-case.test.ts index 6c67c11e253..0324b2d991d 100644 --- a/apps/sim/ee/access-requests/lib/application/authorized-use-case.test.ts +++ b/apps/sim/ee/access-requests/lib/application/authorized-use-case.test.ts @@ -1,7 +1,6 @@ import type { Principal } from '@sim/auth/principal' import { db } from '@sim/db' import { - createDelegatedPrincipal, createPersonalApiKeyPrincipal, createSessionPrincipal, createWorkspaceApiKeyPrincipal, @@ -96,11 +95,6 @@ describe('authorized access request execution', () => { scopes: ['api:write'], expiresAt: new Date('2099-01-01'), }, - createDelegatedPrincipal({ - subjectUserId: 'requester', - workspaceId: 'workspace', - audience: 'sim:settings', - }), ])( 'preserves the $kind actor through preparation, transactional reauthorization and audit', async (caller) => { From 8d1ea34f353979c679482c575ffbd2fc73c78118 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 8 Oct 2026 23:09:59 -0700 Subject: [PATCH 3/3] chore(mothership): refresh the refused-routes inventory for credential sharing and access requests --- .../server/routes/copilot-refused-routes.json | 35 ------------------- 1 file changed, 35 deletions(-) diff --git a/apps/sim/lib/api/server/routes/copilot-refused-routes.json b/apps/sim/lib/api/server/routes/copilot-refused-routes.json index 79c4856280f..eacd2e4df6b 100644 --- a/apps/sim/lib/api/server/routes/copilot-refused-routes.json +++ b/apps/sim/lib/api/server/routes/copilot-refused-routes.json @@ -4,21 +4,6 @@ "path": "/api/v2/chat", "operation": "chat.send" }, - { - "method": "GET", - "path": "/api/v2/credentials/[credentialId]/members", - "operation": "credentials.members.list" - }, - { - "method": "POST", - "path": "/api/v2/credentials/[credentialId]/members", - "operation": "credentials.members.upsert" - }, - { - "method": "DELETE", - "path": "/api/v2/credentials/[credentialId]/members/[userId]", - "operation": "credentials.members.remove" - }, { "method": "DELETE", "path": "/api/v2/files/[fileId]/versions/[version]", @@ -248,25 +233,5 @@ "method": "GET", "path": "/api/v2/workspaces", "operation": "workspaces.list_public" - }, - { - "method": "GET", - "path": "/api/v2/workspaces/[workspaceId]/access-requests", - "operation": "access_requests.list_mine" - }, - { - "method": "POST", - "path": "/api/v2/workspaces/[workspaceId]/access-requests", - "operation": "access_requests.create" - }, - { - "method": "POST", - "path": "/api/v2/workspaces/[workspaceId]/access-requests/[requestId]/cancel", - "operation": "access_requests.cancel" - }, - { - "method": "GET", - "path": "/api/v2/workspaces/[workspaceId]/access-requests/discovery", - "operation": "access_requests.discover" } ]