diff --git a/apps/sim/lib/api/contracts/mothership-resource-tools.ts b/apps/sim/lib/api/contracts/mothership-resource-tools.ts index 08de9dba462..c7187963192 100644 --- a/apps/sim/lib/api/contracts/mothership-resource-tools.ts +++ b/apps/sim/lib/api/contracts/mothership-resource-tools.ts @@ -29,7 +29,6 @@ export const openResourceOutputSchema = z.object({ type: z.enum(['workflow', 'table', 'knowledgebase', 'file', 'dashboard', 'log']), id: z.string(), title: z.string(), - workspaceId: z.string(), viewId: z.string().optional(), executionId: z.string().optional(), }) diff --git a/apps/sim/lib/mothership/agent-cli/index.ts b/apps/sim/lib/mothership/agent-cli/index.ts index 7f8b904b0b3..324a36dd401 100644 --- a/apps/sim/lib/mothership/agent-cli/index.ts +++ b/apps/sim/lib/mothership/agent-cli/index.ts @@ -177,7 +177,8 @@ async function executeBoundAgentCliRequest( } else throw new Error('Service invocation must use the service bridge') if (resources.length) result = { ...result, resources: [...resources, ...(result.resources ?? [])] } - if (result.resources?.length) + // Only organization chats address a workspace; a workspace chat's resources leave it implicit. + if (context.chatOrganizationId && result.resources?.length) result = { ...result, resources: result.resources.map((effect) => { diff --git a/apps/sim/lib/mothership/agent-cli/resource-owner.test.ts b/apps/sim/lib/mothership/agent-cli/resource-owner.test.ts index 2f836fa2041..7b6965d0ad7 100644 --- a/apps/sim/lib/mothership/agent-cli/resource-owner.test.ts +++ b/apps/sim/lib/mothership/agent-cli/resource-owner.test.ts @@ -85,7 +85,7 @@ beforeEach(() => { }) describe('scoped CLI resource ownership', () => { - it('gives a workspace upload completion and explicit open the same resource identity', async () => { + it('gives a workspace upload completion, explicit open, and an open tab one identity', async () => { const upload = await executeAgentCliRequest( { invocation: { kind: 'cli', argv: ['files', 'upload', '@/tmp/upload-acceptance.txt'] } }, context @@ -102,9 +102,10 @@ describe('scoped CLI resource ownership', () => { id: fileId, title: file.name, path: 'files/upload-acceptance.txt', - workspaceId: first, }) - expect(getChatResourceKey(resource)).toBe(getChatResourceKey(opened.resources[0])) + const openTab = getChatResourceKey({ type: 'file', id: fileId }) + expect(getChatResourceKey(resource)).toBe(openTab) + expect(getChatResourceKey(opened.resources[0])).toBe(openTab) expect(mocks.transport).toHaveBeenCalledTimes(1) }) diff --git a/apps/sim/lib/mothership/request/tools/executor.ts b/apps/sim/lib/mothership/request/tools/executor.ts index fabe535218e..4262b930af0 100644 --- a/apps/sim/lib/mothership/request/tools/executor.ts +++ b/apps/sim/lib/mothership/request/tools/executor.ts @@ -1040,7 +1040,10 @@ async function executeToolAndReportInner( execContext.chatId, options?.onEvent, () => abortRequested(context, execContext, options), - toolCall.targetWorkspaceId ?? execContext.workspaceId, + { + organizationId: execContext.organizationId, + workspaceId: toolCall.targetWorkspaceId ?? execContext.workspaceId, + }, execContext.userId ) } diff --git a/apps/sim/lib/mothership/request/tools/resources.test.ts b/apps/sim/lib/mothership/request/tools/resources.test.ts index a7a700f16a2..659f9572895 100644 --- a/apps/sim/lib/mothership/request/tools/resources.test.ts +++ b/apps/sim/lib/mothership/request/tools/resources.test.ts @@ -24,6 +24,7 @@ vi.mock('@/lib/mothership/resources/persistence', () => ({ import { MothershipStreamV1EventType } from '@/lib/mothership/generated/mothership-stream-v1' import { handleResourceSideEffects } from '@/lib/mothership/request/tools/resources' +import type { StreamEvent } from '@/lib/mothership/request/types' import type { MothershipResource } from '@/lib/mothership/resources/types' describe('handleResourceSideEffects', () => { @@ -97,31 +98,35 @@ describe('handleResourceSideEffects', () => { }) } ) - it.each(['workspace-a'])('addresses extracted exports to admitted %s', async (workspaceId) => { - const resource = { type: 'file' as const, id: 'export', title: 'decisions.csv' } - mocks.extractResourcesFromToolResult.mockReturnValue([resource]) - const onEvent = vi.fn() - await handleResourceSideEffects( - 'run_function', - undefined, - { success: true, output: {} }, - { success: true, output: {} }, - 'org-chat', - onEvent, - () => false, - workspaceId - ) - expect(mocks.persistChatResources).toHaveBeenCalledWith('org-chat', [ - { ...resource, workspaceId }, - ]) - expect(onEvent).toHaveBeenCalledWith({ - type: 'resource', - payload: { - op: 'upsert', - resource: { ...resource, workspaceId }, - }, - }) - }) + it.each([ + { chat: 'organization', organizationId: 'org', expected: { workspaceId: 'workspace-a' } }, + { chat: 'workspace', organizationId: undefined, expected: {} }, + ])( + 'addresses extracted exports in a $chat chat like its open tabs', + async ({ organizationId, expected }) => { + const resource = { type: 'file' as const, id: 'export', title: 'decisions.csv' } + mocks.extractResourcesFromToolResult.mockReturnValue([resource]) + const events: StreamEvent[] = [] + await handleResourceSideEffects( + 'run_function', + undefined, + { success: true, output: {} }, + { success: true, output: {} }, + 'chat', + (event) => { + events.push(event) + }, + () => false, + { organizationId, workspaceId: 'workspace-a' } + ) + expect(events).toEqual([ + { + type: 'resource', + payload: { op: 'upsert', resource: { ...resource, ...expected } }, + }, + ]) + } + ) }) it('emits authorized Search results beside the persisted address, never inside it', async () => { diff --git a/apps/sim/lib/mothership/request/tools/resources.ts b/apps/sim/lib/mothership/request/tools/resources.ts index a7c980ecad4..c74853cfda4 100644 --- a/apps/sim/lib/mothership/request/tools/resources.ts +++ b/apps/sim/lib/mothership/request/tools/resources.ts @@ -36,9 +36,11 @@ export async function handleResourceSideEffects( chatId: string, onEvent: ((event: StreamEvent) => void | Promise) | undefined, isAborted: () => boolean, - workspaceId?: string, + owner?: { organizationId?: string; workspaceId?: string }, actorUserId?: string ): Promise { + // Only organization chats address a workspace; a workspace chat's resources leave it implicit. + const workspaceId = owner?.organizationId ? owner.workspaceId : undefined // Cheap early exit so we don't emit a span for tools that can never // produce resources (most of them). The span only shows up for tools // that might actually do resource work. diff --git a/apps/sim/lib/mothership/tools/registry/server-tool-adapter.test.ts b/apps/sim/lib/mothership/tools/registry/server-tool-adapter.test.ts index 162ff99678c..428853aa89f 100644 --- a/apps/sim/lib/mothership/tools/registry/server-tool-adapter.test.ts +++ b/apps/sim/lib/mothership/tools/registry/server-tool-adapter.test.ts @@ -120,9 +120,7 @@ describe('server tool adapter authority boundary', () => { }) it('forwards canonical open-resource effects without replacing target assertions or injecting a workflow', async () => { - const resources = [ - { type: 'workflow', id: 'flow', title: 'Canonical', workspaceId: 'workspace-1' }, - ] + const resources = [{ type: 'workflow', id: 'flow', title: 'Canonical' }] mocks.routeExecution.mockResolvedValue({ resources }) const params = { workspaceId: 'asserted-workspace', diff --git a/apps/sim/lib/mothership/tools/server/open-resource.test.ts b/apps/sim/lib/mothership/tools/server/open-resource.test.ts index baae77a31d9..67610aab989 100644 --- a/apps/sim/lib/mothership/tools/server/open-resource.test.ts +++ b/apps/sim/lib/mothership/tools/server/open-resource.test.ts @@ -87,7 +87,6 @@ describe('resource opening authorization boundary', () => { 'Canonical knowledge', 'Workflow run', ]) - expect(result.resources.every((resource) => resource.workspaceId === 'ws-a')).toBe(true) for (const read of Object.values(mocks)) expect(read).toHaveBeenCalledWith( expect.objectContaining({ diff --git a/apps/sim/lib/mothership/tools/server/open-resource.ts b/apps/sim/lib/mothership/tools/server/open-resource.ts index c84e8543c3b..ad622bc329b 100644 --- a/apps/sim/lib/mothership/tools/server/open-resource.ts +++ b/apps/sim/lib/mothership/tools/server/open-resource.ts @@ -56,7 +56,7 @@ export const openResourceServerTool: BaseServerTool