From 9c82ba8d47e93faf0c5ef03e77e87587d98a8abd Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Mon, 28 Sep 2026 19:33:58 -0700 Subject: [PATCH 1/2] fix(selectors): rebind Copilot principals for nested domain use cases Selectors backed by another domain (knowledge documents, table columns, workflows, sandboxes, MCP tools) forwarded the Copilot principal admitted under the selector audience into use cases that accept only their own audience. The refusal is a DelegatedWorkspaceAuthorizationError, which the v2 surface conceals as 404, so Chat saw "Workspace not found" on workspaces the user administers. Fork sync previews hit it whenever a saved dependent value (a table conflict column, a knowledge document) needed validation. Selectors now rebind through bindCopilotWorkspaceOperation, which can also project the single resource a nested call reaches, as the executor and Chat MCP paths do when they mint. A grant already narrowed to one resource is never moved to another. Managed MCP connections now reach their credential-group rule and return its 403 instead of the concealed 404. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../copilot-workspace-invocation.test.ts | 25 +++- .../copilot-workspace-invocation.ts | 43 ++++++- .../copilot-nested-selectors.integration.ts | 114 ++++++++++++++++++ .../selectors/application/execute-selector.ts | 9 +- .../lib/selectors/application/operations.ts | 2 + apps/sim/lib/selectors/server/internal.ts | 37 ++++-- .../sim/lib/selectors/server/providers/mcp.ts | 15 ++- apps/sim/lib/selectors/server/types.ts | 28 +++++ .../__integration__/fork-sync.integration.ts | 96 +++++++++++++++ 9 files changed, 350 insertions(+), 19 deletions(-) create mode 100644 apps/sim/lib/selectors/__integration__/copilot-nested-selectors.integration.ts diff --git a/apps/sim/lib/core/application/copilot-workspace-invocation.test.ts b/apps/sim/lib/core/application/copilot-workspace-invocation.test.ts index e67ebd571a2..81c615dd918 100644 --- a/apps/sim/lib/core/application/copilot-workspace-invocation.test.ts +++ b/apps/sim/lib/core/application/copilot-workspace-invocation.test.ts @@ -1,5 +1,9 @@ import { describe, expect, it } from 'vitest' -import { markCopilotWorkspaceInvocation } from '@/lib/core/application/copilot-workspace-invocation' +import { + bindCopilotWorkspaceOperation, + markCopilotWorkspaceInvocation, +} from '@/lib/core/application/copilot-workspace-invocation' +import { MANAGED_MCP_DELEGATION_AUDIENCE } from '@/lib/credentials/application/authorization' import { createCopilotChatPrincipal } from '@/lib/mothership/auth/application-delegation' import { tableDelegationPolicy } from '@/lib/table/application/authorization' @@ -45,3 +49,22 @@ describe('private table workspace invocation', () => { ) }) }) +describe('nested workspace operation binding', () => { + it('never moves a grant already narrowed to one resource onto another', () => { + const caller = createCopilotChatPrincipal( + { userId: 'actor', workspaceId: 'workspace', chatId: 'chat' }, + 'sim:selectors', + { credentialId: 'mcp-cg-granted' } + ) + markCopilotWorkspaceInvocation(caller) + expect(() => + bindCopilotWorkspaceOperation( + caller, + 'workspace', + ['sim:selectors'], + { delegationAudience: MANAGED_MCP_DELEGATION_AUDIENCE }, + { credentialId: 'mcp-cg-other' } + ) + ).toThrow(/resource scope/) + }) +}) diff --git a/apps/sim/lib/core/application/copilot-workspace-invocation.ts b/apps/sim/lib/core/application/copilot-workspace-invocation.ts index 3fb3a1fcd07..8d023187323 100644 --- a/apps/sim/lib/core/application/copilot-workspace-invocation.ts +++ b/apps/sim/lib/core/application/copilot-workspace-invocation.ts @@ -25,12 +25,43 @@ export function isCopilotWorkspaceInvocation(principal: DelegatedPrincipal): boo ) } -/** Nested domain reads keep the admitted actor, resource restrictions, workspace, and expiry. */ +const NESTED_RESOURCE_SCOPE_KEYS = ['fileId', 'tableId', 'credentialId', 'mcpServerId'] as const + +export type NestedResourceScope = Partial< + Record<(typeof NESTED_RESOURCE_SCOPE_KEYS)[number], string> +> + +/** Adds the nested target to the admitted scope; a key already granted never moves to another resource. */ +function narrowResourceScope( + granted: DelegatedPrincipal['resourceScope'], + target: NestedResourceScope +): NonNullable { + const scope = { ...granted } + for (const key of NESTED_RESOURCE_SCOPE_KEYS) { + const value = target[key] + if (value === undefined) continue + if (!value.trim() || (scope[key] !== undefined && scope[key] !== value)) { + throw new OrchestrationError( + 'forbidden', + 'Nested operation cannot move its delegated resource scope' + ) + } + scope[key] = value + } + return Object.freeze(scope) +} + +/** + * Nested domain reads keep the admitted actor, resource restrictions, workspace, and expiry. + * A nested operation bound to one resource names it in `resourceScope`, as the executor and + * Chat tool paths do when they mint a principal for that resource. + */ export function bindCopilotWorkspaceOperation

( principal: P, workspaceId: string, sourceAudiences: readonly string[], - useCase: Pick, 'delegationAudience'> + useCase: Pick, 'delegationAudience'>, + resourceScope?: NestedResourceScope ): P { if (principal.kind !== 'delegated' || principal.serviceId !== 'copilot') return principal if ( @@ -44,7 +75,13 @@ export function bindCopilotWorkspaceOperation

( 'Nested operation requires the current workspace invocation' ) } - const derived = { ...principal, audience: useCase.delegationAudience } + const derived = { + ...principal, + audience: useCase.delegationAudience, + ...(resourceScope + ? { resourceScope: narrowResourceScope(principal.resourceScope, resourceScope) } + : {}), + } if (derived.kind === 'delegated') markCopilotWorkspaceInvocation(derived) return derived } diff --git a/apps/sim/lib/selectors/__integration__/copilot-nested-selectors.integration.ts b/apps/sim/lib/selectors/__integration__/copilot-nested-selectors.integration.ts new file mode 100644 index 00000000000..8a8251b4165 --- /dev/null +++ b/apps/sim/lib/selectors/__integration__/copilot-nested-selectors.integration.ts @@ -0,0 +1,114 @@ +/** Chat's in-process CLI reaching selectors whose options are owned by another domain. */ + +import { db } from '@sim/db' +import { + credential, + credentialGroupEnrollment, + mcpServers, + permissions, + user, + workspace, +} from '@sim/db/schema' +import { sha256Hex } from '@sim/security/hash' +import { generateId } from '@sim/utils/id' +import { eq } from 'drizzle-orm' +import { afterAll, beforeAll, describe, expect, it } from 'vitest' +import { withWorkspaceInvocationScope } from '@/lib/core/application/workspace-invocation-scope' +import { ensureWorkspaceAccountsGroup } from '@/lib/credential-groups/service' +import { encryptManagedMcpTokens } from '@/lib/credentials/managed-mcp' +import { generateManagedMcpConnectionId } from '@/lib/mcp/utils' +import { createScopedCliTransport } from '@/lib/mothership/agent-cli/scoped-transport' + +const ORIGIN = 'http://localhost:3000' +const userId = generateId() +const workspaceId = generateId() +const connectionId = generateManagedMcpConnectionId() + +function listSelector(selectorKey: string, context: Record) { + const transport = createScopedCliTransport(ORIGIN, { userId, workspaceId, chatId: generateId() }) + return withWorkspaceInvocationScope({ workspaceId }, () => + transport(`${ORIGIN}/api/v2/selectors/list`, { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ workspaceId, selectorKey, context }), + }) + ) +} + +describe('Copilot selectors backed by another domain', () => { + beforeAll(async () => { + const now = new Date() + await db.insert(user).values({ + id: userId, + name: 'Selector fixture', + email: `${userId}@selector.test`, + emailVerified: true, + createdAt: now, + updatedAt: now, + }) + await db.insert(workspace).values({ + id: workspaceId, + name: 'Selector fixture', + ownerId: userId, + billedAccountUserId: userId, + }) + await db.insert(permissions).values({ + id: generateId(), + userId, + entityType: 'workspace', + entityId: workspaceId, + permissionType: 'admin', + }) + const group = await ensureWorkspaceAccountsGroup(workspaceId, userId) + const serverId = generateId() + await db.insert(mcpServers).values({ + id: serverId, + workspaceId, + credentialGroupId: group.id, + managedConnectorId: 'notion', + name: 'Notion', + transport: 'streamable-http', + url: 'https://mcp.notion.com/mcp', + authType: 'oauth', + createdBy: userId, + }) + const enrollmentId = generateId() + await db.insert(credentialGroupEnrollment).values({ + id: enrollmentId, + credentialGroupId: group.id, + userId, + email: `${userId}@selector.test`, + status: 'completed', + invitationTokenHash: sha256Hex(generateId()), + invitationExpiresAt: new Date(Date.now() + 60_000), + invitedAt: now, + }) + await db.insert(credential).values({ + id: connectionId, + workspaceId, + type: 'managed_mcp', + displayName: 'Notion', + grantedAt: now, + mcpTools: [], + credentialGroupEnrollmentId: enrollmentId, + mcpServerId: serverId, + managedOauthStatus: 'active', + encryptedOauthTokenSet: await encryptManagedMcpTokens({ + access_token: 'fixture-access', + token_type: 'Bearer', + }), + }) + }) + afterAll(async () => { + await db.delete(workspace).where(eq(workspace.id, workspaceId)) + await db.delete(user).where(eq(user.id, userId)) + await db.$client.end() + }) + + it('decides a managed MCP connection by its credential-group rule instead of concealing it', async () => { + const response = await listSelector('mcp.tools', { mcpServerId: connectionId }) + const body = await response.json() + expect(response.status, JSON.stringify(body)).toBe(403) + expect(body.error.message).toBe('Credential Group credential access denied') + }) +}) diff --git a/apps/sim/lib/selectors/application/execute-selector.ts b/apps/sim/lib/selectors/application/execute-selector.ts index d2d7a29eeb9..df881f217fb 100644 --- a/apps/sim/lib/selectors/application/execute-selector.ts +++ b/apps/sim/lib/selectors/application/execute-selector.ts @@ -12,7 +12,10 @@ import { withResourceOutboundScope } from '@/lib/core/network/resource-scope.ser import { OrchestrationError } from '@/lib/core/orchestration/types' import { authorizePersonalSearchSetup } from '@/lib/knowledge/application/personal-search-account' import { type CredentialAuditRequest, recordCredentialAccess } from '@/lib/oauth/token-resolution' -import { selectorOperations } from '@/lib/selectors/application/operations' +import { + SELECTOR_DELEGATION_AUDIENCE, + selectorOperations, +} from '@/lib/selectors/application/operations' import { resolveSelectorApplicationContext, type SelectorApplicationContext, @@ -327,7 +330,9 @@ const executeWorkspaceSelector = defineAuthorizedWorkspaceUseCase< if (context.workspaceId === undefined) throw new SelectorContextUnavailableError() return context }, - authorizationOptions: { delegation: { audience: 'sim:selectors', isWithinScope: () => true } }, + authorizationOptions: { + delegation: { audience: SELECTOR_DELEGATION_AUDIENCE, isWithinScope: () => true }, + }, authorizeResource: ({ input, context }) => validateAuthorizedInput(input, context), execute: executeAuthorizedSelector, }) diff --git a/apps/sim/lib/selectors/application/operations.ts b/apps/sim/lib/selectors/application/operations.ts index 8377443d14a..8594705d022 100644 --- a/apps/sim/lib/selectors/application/operations.ts +++ b/apps/sim/lib/selectors/application/operations.ts @@ -1,5 +1,7 @@ import { defineWorkspaceOperation } from '@/lib/core/application/workspace-operation' +export const SELECTOR_DELEGATION_AUDIENCE = 'sim:selectors' + export const selectorOperations = { // permission-group-exempt: no static capability names selector browsing — credential access is authorized per credential, and per-integration denial is the parameterized allowedIntegrations key, which the funnel cannot apply because it never sees which integration a selector reaches. That decision is enforced from the use case by assertSelectorIntegrationAllowed, against the selector's own resource, ahead of the provider call. execute: defineWorkspaceOperation({ diff --git a/apps/sim/lib/selectors/server/internal.ts b/apps/sim/lib/selectors/server/internal.ts index 446d1bdf236..bd541b270fb 100644 --- a/apps/sim/lib/selectors/server/internal.ts +++ b/apps/sim/lib/selectors/server/internal.ts @@ -22,6 +22,8 @@ import { detailSelectorResult, type ExecuteServerSelectorArgs, listSelectorResult, + nestedSelectorPrincipal, + type SelectorPrincipal, type ServerSelectorAttachmentMap, } from '@/lib/selectors/server/types' import { readTableUseCase } from '@/lib/table/application/tables' @@ -45,17 +47,14 @@ function labelWorkflow( return `${base} (${folder})` } -async function loadWorkflows( - args: Parameters<(typeof listWorkflows)['execute']>[0]['principal'], - workspaceId: string -) { +async function loadWorkflows(principal: SelectorPrincipal, workspaceId: string) { const workflows: Array< Awaited>['workflows'][number] > = [] let cursorKeys: Awaited>['nextCursorKeys'] = null for (let page = 0; page < MAX_WORKFLOW_PAGES; page += 1) { const result = await listWorkflows.execute({ - principal: args, + principal: nestedSelectorPrincipal(principal, workspaceId, listWorkflows), input: { workspaceId, scope: 'active', @@ -82,7 +81,11 @@ export const internalSelectorAttachments = { const knowledgeBaseId = args.context.knowledgeBaseId! if (args.request.kind === 'detail') { const result = await readKnowledgeDocument.execute({ - principal: args.principal, + principal: nestedSelectorPrincipal( + args.principal, + args.workspaceId, + readKnowledgeDocument + ), input: { knowledgeBaseId, documentId: args.request.id, @@ -98,7 +101,11 @@ export const internalSelectorAttachments = { const offset = args.request.cursor ? Number(args.request.cursor) : 0 if (!Number.isSafeInteger(offset) || offset < 0) throw new Error('Invalid selector cursor') const result = await listKnowledgeDocuments.execute({ - principal: args.principal, + principal: nestedSelectorPrincipal( + args.principal, + args.workspaceId, + listKnowledgeDocuments + ), input: { knowledgeBaseId, assertedWorkspaceId: args.workspaceId, @@ -151,7 +158,7 @@ export const internalSelectorAttachments = { async execute(args: ExecuteServerSelectorArgs) { if (!args.workspaceId) throw new SelectorContextUnavailableError() const { table } = await readTableUseCase.execute({ - principal: args.principal, + principal: nestedSelectorPrincipal(args.principal, args.workspaceId, readTableUseCase), input: { tableId: args.context.tableId!, workspaceId: args.workspaceId }, }) const options = (table.schema?.columns ?? []) @@ -169,7 +176,7 @@ export const internalSelectorAttachments = { async execute(args: ExecuteServerSelectorArgs) { if (!args.workspaceId) throw new SelectorContextUnavailableError() const { table } = await readTableUseCase.execute({ - principal: args.principal, + principal: nestedSelectorPrincipal(args.principal, args.workspaceId, readTableUseCase), input: { tableId: args.context.tableId!, workspaceId: args.workspaceId }, }) const options = (table.schema?.columns ?? []).map((column) => ({ @@ -291,7 +298,11 @@ export const internalSelectorAttachments = { if (args.request.kind === 'detail') { const result = await getWorkspaceSandboxUseCase .execute({ - principal: args.principal, + principal: nestedSelectorPrincipal( + args.principal, + args.workspaceId, + getWorkspaceSandboxUseCase + ), input: { workspaceId: args.workspaceId, sandboxId: args.request.id }, }) .catch((error: unknown) => { @@ -308,7 +319,11 @@ export const internalSelectorAttachments = { }) } const { sandboxes, nextCursorKeys } = await listWorkspaceSandboxesUseCase.execute({ - principal: args.principal, + principal: nestedSelectorPrincipal( + args.principal, + args.workspaceId, + listWorkspaceSandboxesUseCase + ), input: { workspaceId: args.workspaceId, limit: 1000 }, }) if (nextCursorKeys) throw new SelectorOptionsUnavailableError() diff --git a/apps/sim/lib/selectors/server/providers/mcp.ts b/apps/sim/lib/selectors/server/providers/mcp.ts index 7852d84372c..9d9a7392d3b 100644 --- a/apps/sim/lib/selectors/server/providers/mcp.ts +++ b/apps/sim/lib/selectors/server/providers/mcp.ts @@ -6,6 +6,7 @@ import { definePreparedSelectorAttachment, detailSelectorResult, listSelectorResult, + nestedSelectorPrincipal, } from '@/lib/selectors/server/types' /** The existing MCP use case binds the destination and credentials to an authorized server. */ @@ -25,7 +26,12 @@ export const mcpSelectorAttachments = { if (!isManagedMcpConnectionId(serverId)) throw new OrchestrationError('validation', 'Invalid managed MCP connection ID') return discoverManagedMcpToolsUseCase.execute({ - principal: args.principal, + principal: nestedSelectorPrincipal( + args.principal, + args.workspaceId, + discoverManagedMcpToolsUseCase, + { credentialId: serverId } + ), input: { workspaceId: args.workspaceId, credentialId: serverId, @@ -34,7 +40,12 @@ export const mcpSelectorAttachments = { }) } return discoverMcpServerToolsUseCase.execute({ - principal: args.principal, + principal: nestedSelectorPrincipal( + args.principal, + args.workspaceId, + discoverMcpServerToolsUseCase, + { mcpServerId: serverId } + ), input: { workspaceId: args.workspaceId, serverId, diff --git a/apps/sim/lib/selectors/server/types.ts b/apps/sim/lib/selectors/server/types.ts index 0ac2d246512..113ac2e2174 100644 --- a/apps/sim/lib/selectors/server/types.ts +++ b/apps/sim/lib/selectors/server/types.ts @@ -1,5 +1,11 @@ import type { Principal, SessionPrincipal } from '@sim/auth/principal' import type { CredentialAccessResult } from '@/lib/auth/credential-access' +import { + bindCopilotWorkspaceOperation, + type NestedResourceScope, +} from '@/lib/core/application/copilot-workspace-invocation' +import type { ApplicationOperation, OperationUseCase } from '@/lib/core/application/operation' +import { SELECTOR_DELEGATION_AUDIENCE } from '@/lib/selectors/application/operations' import { MAX_SELECTOR_OPTIONS } from '@/lib/selectors/limits' import type { SelectorKey, ServerSelectorKey } from '@/lib/selectors/manifest' import type { @@ -14,6 +20,28 @@ export type SelectorPrincipal = | Extract | (Extract & { serviceId: 'copilot' }) +/** + * The principal a selector hands to another domain's use case. A Copilot + * invocation is admitted under the selector audience, which no other domain + * accepts, so it is re-bound to the nested use case's own audience for the same + * workspace, naming the one resource it reaches when that domain scopes its + * delegations. Every other principal passes through unchanged. + */ +export function nestedSelectorPrincipal

( + principal: P, + workspaceId: string, + useCase: Pick, 'delegationAudience'>, + resourceScope?: NestedResourceScope +): P { + return bindCopilotWorkspaceOperation( + principal, + workspaceId, + [SELECTOR_DELEGATION_AUDIENCE], + useCase, + resourceScope + ) +} + export type SelectorDestinationPolicy = 'fixed' | 'credential-bound' | 'user-controlled' export type SelectorProtectedValueKind = 'secret' | 'reference' diff --git a/apps/sim/lib/workspaces/__integration__/fork-sync.integration.ts b/apps/sim/lib/workspaces/__integration__/fork-sync.integration.ts index 7768e5148e2..13118cd4e0b 100644 --- a/apps/sim/lib/workspaces/__integration__/fork-sync.integration.ts +++ b/apps/sim/lib/workspaces/__integration__/fork-sync.integration.ts @@ -4,6 +4,7 @@ import { outboxEvent, permissions, user, + userTableDefinitions, workflow, workflowBlocks, workflowDeploymentOperation, @@ -16,7 +17,9 @@ import { import { generateId } from '@sim/utils/id' import { and, eq } from 'drizzle-orm' import { afterAll, beforeAll, describe, expect, it } from 'vitest' +import { withWorkspaceInvocationScope } from '@/lib/core/application/workspace-invocation-scope' import { processOutboxEventById } from '@/lib/core/outbox/service' +import { createScopedCliTransport } from '@/lib/mothership/agent-cli/scoped-transport' import { workflowDeploymentOutboxHandlers } from '@/lib/workflows/deployment-outbox' import { admitWorkflowState, saveAdmittedWorkflowState } from '@/lib/workflows/persistence/utils' import { getWorkspaceOperation } from '@/lib/workspaces/operations/application' @@ -517,6 +520,99 @@ describe('authorized fork and sync against PostgreSQL', () => { } }) + it('validates a mapped table dependent through a Copilot CLI push preview', async () => { + const childId = await createChild() + const [sourceVersion] = await db + .select() + .from(workflowDeploymentVersion) + .where( + and( + eq(workflowDeploymentVersion.workflowId, sourceWorkflowId), + eq(workflowDeploymentVersion.isActive, true) + ) + ) + const sourceTableId = generateId() + const childTableId = generateId() + const schema = { columns: [{ id: 'col_key', name: 'key', type: 'string', unique: true }] } + await db.insert(userTableDefinitions).values([ + { + id: sourceTableId, + workspaceId: sourceWorkspaceId, + name: `Source table ${sourceTableId}`, + schema, + createdBy: userId, + }, + { id: childTableId, workspaceId: childId, name: 'Child table', schema, createdBy: userId }, + ]) + const sourceState = structuredClone(sourceVersion.state) as WorkflowState + sourceState.blocks.upsert = { + id: 'upsert', + type: 'table', + name: 'Upsert', + enabled: true, + position: { x: 400, y: 0 }, + subBlocks: { + operation: { id: 'operation', type: 'dropdown', value: 'upsert_row' }, + tableSelector: { id: 'tableSelector', type: 'table-selector', value: sourceTableId }, + conflictColumnSelector: { + id: 'conflictColumnSelector', + type: 'column-selector', + value: 'col_key', + }, + }, + outputs: {}, + } + const transport = createScopedCliTransport('http://localhost:3000', { + userId, + workspaceId: sourceWorkspaceId, + chatId: generateId(), + }) + try { + await db + .update(workflowDeploymentVersion) + .set({ state: sourceState }) + .where(eq(workflowDeploymentVersion.id, sourceVersion.id)) + const response = await withWorkspaceInvocationScope({ workspaceId: sourceWorkspaceId }, () => + transport( + `http://localhost:3000/api/v2/workspaces/${sourceWorkspaceId}/fork/push/preview`, + { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ + otherWorkspaceId: childId, + mappings: [ + { resourceType: 'table', sourceId: sourceTableId, targetId: childTableId }, + ], + dependentValues: [ + { + sourceWorkflowId, + sourceBlockId: 'upsert', + subBlockKey: 'conflictColumnSelector', + value: 'col_key', + }, + ], + }), + } + ) + ) + const body = await response.json() + expect(response.status, JSON.stringify(body)).toBe(200) + expect(body.data.configuration).toContainEqual( + expect.objectContaining({ + sourceBlockId: 'upsert', + subBlockKey: 'conflictColumnSelector', + currentValue: 'col_key', + discoveryWorkspaceId: childId, + }) + ) + } finally { + await db + .update(workflowDeploymentVersion) + .set({ state: sourceVersion.state }) + .where(eq(workflowDeploymentVersion.id, sourceVersion.id)) + } + }) + it('refuses inherited folder locks with no committed mutation', async () => { const childId = await createChild() const [target] = await db.select().from(workflow).where(eq(workflow.workspaceId, childId)) From a0ba913452a106e3e2df25660ed51ff66700f1b7 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Mon, 28 Sep 2026 19:44:03 -0700 Subject: [PATCH 2/2] fix(selectors): scope table selector reads to the selected table Tables honor resourceScope.tableId, so the nested read names the table it reads, as the MCP selector names its server or connection. Co-Authored-By: Claude Opus 5.5 (1M context) --- apps/sim/lib/selectors/server/internal.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/apps/sim/lib/selectors/server/internal.ts b/apps/sim/lib/selectors/server/internal.ts index bd541b270fb..887e8019195 100644 --- a/apps/sim/lib/selectors/server/internal.ts +++ b/apps/sim/lib/selectors/server/internal.ts @@ -157,9 +157,12 @@ export const internalSelectorAttachments = { destination: 'fixed', async execute(args: ExecuteServerSelectorArgs) { if (!args.workspaceId) throw new SelectorContextUnavailableError() + const tableId = args.context.tableId! const { table } = await readTableUseCase.execute({ - principal: nestedSelectorPrincipal(args.principal, args.workspaceId, readTableUseCase), - input: { tableId: args.context.tableId!, workspaceId: args.workspaceId }, + principal: nestedSelectorPrincipal(args.principal, args.workspaceId, readTableUseCase, { + tableId, + }), + input: { tableId, workspaceId: args.workspaceId }, }) const options = (table.schema?.columns ?? []) .filter((column) => column.unique) @@ -175,9 +178,12 @@ export const internalSelectorAttachments = { destination: 'fixed', async execute(args: ExecuteServerSelectorArgs) { if (!args.workspaceId) throw new SelectorContextUnavailableError() + const tableId = args.context.tableId! const { table } = await readTableUseCase.execute({ - principal: nestedSelectorPrincipal(args.principal, args.workspaceId, readTableUseCase), - input: { tableId: args.context.tableId!, workspaceId: args.workspaceId }, + principal: nestedSelectorPrincipal(args.principal, args.workspaceId, readTableUseCase, { + tableId, + }), + input: { tableId, workspaceId: args.workspaceId }, }) const options = (table.schema?.columns ?? []).map((column) => ({ id: getColumnId(column),