diff --git a/apps/sim/lib/mothership/agent-cli/file-provenance.test.ts b/apps/sim/lib/mothership/agent-cli/file-provenance.test.ts index 509d3239e9b..893d05af5e5 100644 --- a/apps/sim/lib/mothership/agent-cli/file-provenance.test.ts +++ b/apps/sim/lib/mothership/agent-cli/file-provenance.test.ts @@ -4,6 +4,7 @@ import { queueTableRows, resetDbChainMock } from '@sim/testing' import { createPersonalApiKeyPrincipal } from '@sim/testing/factories/principal.factory' import { encryptionMock, encryptionMockFns } from '@sim/testing/mocks/encryption.mock' import { fileParsersMock, fileParsersMockFns } from '@sim/testing/mocks/file-parsers.mock' +import { getMockLogger } from '@sim/testing/mocks/logger.mock' import { mothershipWorkspaceTargetMock, mothershipWorkspaceTargetMockFns, @@ -53,6 +54,7 @@ vi.mock('@/lib/mothership/agent-cli/scoped-transport', () => ({ import { createFileReadTransport } from '@/lib/mothership/agent-cli/file-read-transport' import { inspectToolResultForCopilot } from '@/lib/mothership/request/tools/resolved-secret-result' import { executeSimCli } from '@/lib/mothership/tools/handlers/sim-cli' +import { reportWorkspaceFileDelivery } from '@/lib/workspace-files/application/file-delivery-observer' import { readWorkspaceFileArtifact } from '@/lib/workspace-files/application/read-workspace-file-artifact' import { ResolvedSecretTraceRegistry } from '@/executor/utils/resolved-secret-trace-registry' @@ -98,17 +100,48 @@ function registry() { return new ResolvedSecretTraceRegistry([], { userId: 'reader', workspaceId: 'workspace' }) } -function classify(status: 'exact' | 'unknown', fileRevision = revision) { +type SidecarStatus = 'exact' | 'unknown' + +function sidecarRow(status: SidecarStatus, fileRevision = revision, entries: unknown[] = []) { + return { + fileContentUpdatedAt: fileRevision, + secretProvenanceVersion: 1, + provenanceContentUpdatedAt: fileRevision, + status, + entries, + } +} + +/** Queues one sidecar per status, in the order the reads consume them. */ +function classify(...statuses: [SidecarStatus, ...SidecarStatus[]]) { resetDbChainMock() - queueTableRows(workspaceFiles, [ + for (const status of statuses) queueTableRows(workspaceFiles, [sidecarRow(status)]) +} + +type CliInvocation = Parameters[0]['request']['invocation'] + +const readFile: CliInvocation = { + kind: 'augmentation', + name: 'files read', + positionals: ['file'], + flags: {}, +} + +function grepFiles(pattern: string): CliInvocation { + return { kind: 'augmentation', name: 'grep', positionals: [pattern], flags: { scope: 'files' } } +} + +function runCli(invocation: CliInvocation, trace = registry()) { + return executeSimCli( + { request: { invocation } }, { - fileContentUpdatedAt: fileRevision, - secretProvenanceVersion: 1, - provenanceContentUpdatedAt: fileRevision, - status, - entries: [], - }, - ]) + userId: 'reader', + workspaceId: 'workspace', + workflowId: '', + chatId: 'chat', + resolvedSecretTraceRegistry: trace, + } + ) } function readTransport(trace = registry()) { @@ -278,14 +311,53 @@ describe('file provenance at the actual CLI and model-result boundary', () => { expect(mocks.stream).toHaveBeenCalledTimes(1) }) - it('withholds extracted text whose canonical revision changed', async () => { - classify('exact', new Date(revision.getTime() + 1)) + it('refuses a forwarded response naming its unknown file, without blanking the invocation', async () => { + const refusals = getMockLogger('DurableSecretProvenancePersistence').error + refusals.mockClear() + const trace = registry() + const transport = createFileReadTransport({ + endpoint: 'https://sim.test', + userId: 'reader', + registry: trace, + invocation: { userId: 'reader', workspaceId: 'workspace', chatId: 'chat' }, + transport: async () => { + await reportWorkspaceFileDelivery({ status: 'exact', entries: [] }, 'clean') + await reportWorkspaceFileDelivery({ status: 'unknown' }, 'tainted') + return new Response(content) + }, + }) + const response = await transport( + new Request('https://sim.test/api/v2/files/bulk-download?workspaceId=workspace') + ) + expect(response.status).toBe(403) + expect(await response.text()).not.toContain(content) + expect(trace.isPermanentlyIncomplete()).toBe(false) + expect(refusals).toHaveBeenCalledTimes(1) + expect(refusals).toHaveBeenCalledWith( + 'Refusing unavailable durable secret provenance', + expect.objectContaining({ resourceId: 'tainted' }) + ) + }) + + it('refuses extracted text whose canonical revision changed before loading its bytes', async () => { + resetDbChainMock() + queueTableRows(workspaceFiles, [sidecarRow('exact', new Date(revision.getTime() + 1))]) const trace = registry() const response = await readTransport(trace)(fileRequest('/text')) + expect(response.status).toBe(403) + expect(await response.text()).not.toContain(content) + expect(mocks.buffer).not.toHaveBeenCalled() + expect(trace.isPermanentlyIncomplete()).toBe(false) + }) + + it('still withholds a delivered text read once the registry is incomplete for another reason', async () => { + classify('exact') + const trace = registry() + trace.markIncomplete('provenance-capacity-exceeded') + const response = await readTransport(trace)(fileRequest('/text')) expect(response.status).toBe(200) const output = await response.text() expect(output).toContain(content) - expect(trace.isPermanentlyIncomplete()).toBe(true) expect( JSON.stringify( inspectToolResultForCopilot({ success: true, output }, trace, 'sim_cli').result @@ -366,62 +438,83 @@ describe('file provenance at the actual CLI and model-result boundary', () => { classify('unknown') const grep = async () => { const trace = registry() - const result = await executeSimCli( - { - request: { - invocation: { - kind: 'augmentation', - name: 'grep', - positionals: [content], - flags: { scope: 'files' }, - }, - }, - }, - { - userId: 'reader', - workspaceId: 'workspace', - workflowId: '', - chatId: 'chat', - resolvedSecretTraceRegistry: trace, - } + const result = await runCli(grepFiles(content), trace) + expect(result.error).toContain('Search incomplete') + expect(JSON.stringify(result.output)).toContain( + 'files/private.txt (file): could not read text' ) - expect(result.success).toBe(true) - expect(JSON.stringify(result.output)).toContain(content) - expect( - JSON.stringify(inspectToolResultForCopilot(result, trace, 'sim_cli').result) - ).not.toContain(content) + expect(JSON.stringify(result.output)).toContain('No matches') + expect(trace.isPermanentlyIncomplete()).toBe(false) } if (mode === 'parallel') await Promise.all([grep(), grep()]) else { await grep() await grep() } - expect(mocks.buffer).toHaveBeenCalledTimes(2) + expect(mocks.buffer).not.toHaveBeenCalled() } ) - it('carries unified text reads through the actual CLI transport and secret projection', async () => { - classify('unknown') + it('returns clean grep matches while refusing only the file whose provenance is unknown', async () => { + const tainted = { ...file, id: 'tainted', key: 'workspace/workspace/tainted-key' } + const taintedText = 'TAINTED_FILE_CANARY_FOR_LOCAL_TEST' + const byId = (id: string) => (id === tainted.id ? tainted : file) + mocks.file.mockImplementation(async (_workspaceId: string, id: string) => byId(id)) + mocks.reference.mockImplementation(async (_workspaceId: string, id: string) => byId(id)) + mocks.context.mockImplementation(async (id: string) => ({ + workspaceId: 'workspace', + fileId: id, + workspaceOrganizationId: null, + allowPersonalApiKeys: true, + billedAccountUserId: 'owner', + })) + mocks.buffer.mockImplementation(async (record: { id: string }) => + Buffer.from(record.id === tainted.id ? taintedText : content) + ) + vi.stubGlobal('fetch', async () => + Response.json({ + data: [file, tainted].map((record) => ({ ...record, updatedAt: revision.toISOString() })), + nextCursor: null, + }) + ) + classify('exact', 'unknown') + const refusals = getMockLogger('DurableSecretProvenancePersistence').error + refusals.mockClear() const trace = registry() - const result = await executeSimCli( - { - request: { - invocation: { - kind: 'augmentation', - name: 'files read', - positionals: ['file'], - flags: {}, - }, - }, - }, - { - userId: 'reader', - workspaceId: 'workspace', - workflowId: '', - chatId: 'chat', - resolvedSecretTraceRegistry: trace, - } + const result = await runCli(grepFiles('CANARY_FOR_LOCAL_TEST'), trace) + const projected = JSON.stringify(inspectToolResultForCopilot(result, trace, 'sim_cli').result) + expect(trace.isPermanentlyIncomplete()).toBe(false) + expect(projected).not.toContain(taintedText) + expect(projected).toContain(content) + expect(mocks.buffer).toHaveBeenCalledTimes(1) + expect(refusals).toHaveBeenCalledTimes(1) + expect(refusals).toHaveBeenCalledWith( + 'Refusing unavailable durable secret provenance', + expect.objectContaining({ + cause: 'workspace-file-provenance-unavailable', + resourceId: tainted.id, + }) ) + }) + + it('refuses an unknown file through the actual CLI text read without loading its bytes', async () => { + classify('unknown') + const result = await runCli(readFile) + expect(result.success).toBe(false) + expect(JSON.stringify(result.output)).not.toContain(content) + expect(JSON.stringify(result)).toContain('secret provenance is unavailable') + expect(mocks.buffer).not.toHaveBeenCalled() + }) + + it('carries unified text reads through the actual CLI transport and secret projection', async () => { + resetDbChainMock() + queueTableRows(workspaceFiles, [ + sidecarRow('exact', revision, [ + { name: 'FILE_SECRET', encryptedValue: 'fixture-ciphertext', sourceUserId: 'reader' }, + ]), + ]) + const trace = registry() + const result = await runCli(readFile, trace) expect(result.success).toBe(true) expect(JSON.stringify(result.output)).toContain(content) expect(JSON.stringify(result.output)).toContain('representation') diff --git a/apps/sim/lib/mothership/agent-cli/file-read-transport.ts b/apps/sim/lib/mothership/agent-cli/file-read-transport.ts index cfccc827b0e..436ec74380d 100644 --- a/apps/sim/lib/mothership/agent-cli/file-read-transport.ts +++ b/apps/sim/lib/mothership/agent-cli/file-read-transport.ts @@ -13,12 +13,14 @@ import { import { readV2CredentialHeaders } from '@/lib/api/server/routes/v2-credential-headers' import { V2_PARSE_DEFAULTS } from '@/lib/api/server/routes/v2-json-route' import { parseRequest } from '@/lib/api/server/validation' +import { OrchestrationError } from '@/lib/core/orchestration/types' import { type CopilotChatDelegationContext, createCopilotChatPrincipal, } from '@/lib/mothership/auth/application-delegation' import { importWorkspaceFileSnapshotProvenance, + MODEL_UNSAFE_WORKSPACE_FILE_ERROR_MESSAGE, mergeWorkspaceFileSecretProvenance, type WorkspaceFileSecretProvenance, } from '@/lib/uploads/contexts/workspace/workspace-file-secret-provenance' @@ -52,18 +54,33 @@ export function createFileReadTransport(context: { `${basePath}${v2ListFileFoldersContract.path}`, `${basePath}${v2BulkDownloadFilesContract.path}`, ]) - const observe = async (workspaceId: string, provenance?: WorkspaceFileSecretProvenance) => { - const imported = - provenance && - (await importWorkspaceFileSnapshotProvenance({ - workspaceId, - provenance, - registry: context.registry, - actorUserId: context.userId, - })) - if (!imported) context.registry?.markIncomplete('workspace-file-provenance-unknown') - return imported - } + /** + * Admits one delivery's classification into the invocation registry. A delivery it refuses is + * either withheld (text and forwarded responses reach only the model, so refusing them is enough) + * or, for a direct download whose bytes can be persisted, delivered with the whole invocation + * marked incomplete so anything derived from the copy keeps the unknown classification. + */ + const admit = async ( + workspaceId: string, + provenance: WorkspaceFileSecretProvenance | undefined, + resourceId?: string + ): Promise => + provenance !== undefined && + (await importWorkspaceFileSnapshotProvenance({ + workspaceId, + provenance, + registry: context.registry, + actorUserId: context.userId, + resourceId, + })) + const refused = () => + v2FileErrorPolicies.concealResourceAuthorization.render( + new OrchestrationError('forbidden', MODEL_UNSAFE_WORKSPACE_FILE_ERROR_MESSAGE) + ) ?? + Response.json( + { error: { message: MODEL_UNSAFE_WORKSPACE_FILE_ERROR_MESSAGE } }, + { status: 403 } + ) const forward: typeof fetch = async (input, init) => { const url = new URL(input instanceof Request ? input.url : input) @@ -72,8 +89,10 @@ export function createFileReadTransport(context: { return (context.transport ?? fetch)(input, init) } let provenance: WorkspaceFileSecretProvenance | undefined + const deliveries: { source: WorkspaceFileSecretProvenance | undefined; fileId: string }[] = [] const response = await observeWorkspaceFileDelivery( - async (source) => { + async (source, fileId) => { + deliveries.push({ source, fileId }) provenance = mergeWorkspaceFileSecretProvenance( provenance ?? { status: 'exact', entries: [] }, source ?? { status: 'unknown' } @@ -83,13 +102,20 @@ export function createFileReadTransport(context: { ) if (!provenance) return response try { - if (!context.registry || !(await observe(context.invocation.workspaceId, provenance))) { + if (!context.registry) { await response.body?.cancel().catch(() => {}) return Response.json( { error: { message: 'File read provenance is unavailable. Retry the read.' } }, { status: 503 } ) } + /** Each delivery is admitted under its own file id, so a refusal names the file it refused. */ + for (const { source, fileId } of deliveries) { + if (!(await admit(context.invocation.workspaceId, source, fileId))) { + await response.body?.cancel().catch(() => {}) + return refused() + } + } if (response.body) context.trackDownload?.(response.body, provenance) return response } catch (error) { @@ -143,8 +169,10 @@ export function createFileReadTransport(context: { if (!parsed.success) return parsed.response request.signal.throwIfAborted() const result = await observeWorkspaceFileDelivery( - async (source) => { - await observe(parsed.data.query.workspaceId, source) + async (source, fileId) => { + if (!(await admit(parsed.data.query.workspaceId, source, fileId))) { + throw new OrchestrationError('forbidden', MODEL_UNSAFE_WORKSPACE_FILE_ERROR_MESSAGE) + } }, () => readWorkspaceFileText.execute({ @@ -173,8 +201,10 @@ export function createFileReadTransport(context: { if (!parsed.success) return parsed.response request.signal.throwIfAborted() const result = await observeWorkspaceFileDelivery( - async (source) => { - await observe(parsed.data.query.workspaceId, source) + async (source, fileId) => { + if (!(await admit(parsed.data.query.workspaceId, source, fileId))) { + context.registry?.markIncomplete('workspace-file-provenance-unknown') + } }, () => downloadWorkspaceFileStream.execute({ diff --git a/apps/sim/lib/mothership/tools/handlers/workbench-confidentiality.live.test.ts b/apps/sim/lib/mothership/tools/handlers/workbench-confidentiality.live.test.ts index 4e0f61e889a..eba6b76d179 100644 --- a/apps/sim/lib/mothership/tools/handlers/workbench-confidentiality.live.test.ts +++ b/apps/sim/lib/mothership/tools/handlers/workbench-confidentiality.live.test.ts @@ -560,17 +560,20 @@ describe('sandbox API provenance admission', () => { `/api/v2/${source === 'file' ? 'files/fixture' : 'tables/fixture/rows'}`, async () => { if (source === 'file') { - await reportWorkspaceFileDelivery({ - status: 'exact', - entries: [ - { - name: 'TOKEN', - encryptedValue: catalog[0].encryptedValue, - sourceUserId: scope.userId, - sourceWorkspaceId: scope.workspaceId, - }, - ], - }) + await reportWorkspaceFileDelivery( + { + status: 'exact', + entries: [ + { + name: 'TOKEN', + encryptedValue: catalog[0].encryptedValue, + sourceUserId: scope.userId, + sourceWorkspaceId: scope.workspaceId, + }, + ], + }, + 'fixture' + ) } else { await reportTableRowDelivery( { @@ -598,7 +601,7 @@ describe('sandbox API provenance admission', () => { it('accepts explicitly unrecorded file delivery and retains earlier secret history', async () => { await run('printf "%s" "$TOKEN" > saved.txt', ['TOKEN']) const response = await sandboxApi('/api/v2/files/fixture/download', async () => { - await reportWorkspaceFileDelivery({ status: 'unrecorded' }) + await reportWorkspaceFileDelivery({ status: 'unrecorded' }, 'fixture') return new Response('ordinary user input') }) await machine.writeFile('/home/user/input.txt', await response.text()) @@ -654,7 +657,7 @@ describe('sandbox API provenance admission', () => { await sandboxApi( `/api/v2/${source === 'file' ? 'files/fixture' : 'tables/fixture/rows'}`, async () => { - if (source === 'file') await reportWorkspaceFileDelivery({ status: 'unknown' }) + if (source === 'file') await reportWorkspaceFileDelivery({ status: 'unknown' }, 'fixture') else await reportTableRowDelivery({ version: 1, complete: false, entries: [] }, [ { value: 'unknown' }, diff --git a/apps/sim/lib/mothership/tools/sandbox-resource-transport.test.ts b/apps/sim/lib/mothership/tools/sandbox-resource-transport.test.ts index caf7742b5d5..abded34fef0 100644 --- a/apps/sim/lib/mothership/tools/sandbox-resource-transport.test.ts +++ b/apps/sim/lib/mothership/tools/sandbox-resource-transport.test.ts @@ -288,7 +288,7 @@ it('dispatches a workbench file read under a delivery observer the Copilot guard routeMatcher.mockReturnValue({ params: { fileId: 'file' }, load: async () => ({ GET: fetcher }) }) fetcher.mockImplementation(async () => { requireCopilotWorkspaceFileDeliveryObserver(createDelegatedPrincipal({ serviceId: 'copilot' })) - await reportWorkspaceFileDelivery({ status: 'exact', entries: [] }) + await reportWorkspaceFileDelivery({ status: 'exact', entries: [] }, 'file') return new Response('file text') }) recordInput.mockResolvedValueOnce(undefined) diff --git a/apps/sim/lib/workspace-files/__integration__/copilot-file-versions.integration.ts b/apps/sim/lib/workspace-files/__integration__/copilot-file-versions.integration.ts index f791f4c7c7b..e8062c59a93 100644 --- a/apps/sim/lib/workspace-files/__integration__/copilot-file-versions.integration.ts +++ b/apps/sim/lib/workspace-files/__integration__/copilot-file-versions.integration.ts @@ -376,15 +376,17 @@ describe('chat-delegated file version history', () => { it('withholds a historical version whose secret provenance is unknown', async () => { const fixture = await seedVersionedFile({ status: 'unknown' }) + const registry = registryFor(fixture, fixture.bobId) const response = await chatTransport( fixture, fixture.bobId, - registryFor(fixture, fixture.bobId) + registry )(`/api/v2/files/${fixture.fileId}/versions/2/text?workspaceId=${fixture.workspaceId}`) - expect(response.status).toBe(503) + expect(response.status).toBe(403) expect(await response.text()).not.toContain(SECRET) + expect(registry.isPermanentlyIncomplete()).toBe(false) }) it('keeps a secret redacted after Chat reverts the file to the version holding it', async () => { diff --git a/apps/sim/lib/workspace-files/application/download-workspace-file.test.ts b/apps/sim/lib/workspace-files/application/download-workspace-file.test.ts index 2874b81eb65..00f1b45f8d2 100644 --- a/apps/sim/lib/workspace-files/application/download-workspace-file.test.ts +++ b/apps/sim/lib/workspace-files/application/download-workspace-file.test.ts @@ -126,7 +126,7 @@ describe('workspace file downloads', () => { mocks.provenance.mockResolvedValue(provenance) const observe = vi.fn(async () => {}) const result = await observeWorkspaceFileDelivery(observe, () => downloadStream('file-2')) - expect(observe).toHaveBeenCalledWith(provenance) + expect(observe).toHaveBeenCalledWith(provenance, file.id) expect(result.secretProvenance).toBeUndefined() }) it('does not return content if the private delivery observer refuses', async () => { diff --git a/apps/sim/lib/workspace-files/application/download-workspace-file.ts b/apps/sim/lib/workspace-files/application/download-workspace-file.ts index c6bc8fa8f5e..d787ea0164d 100644 --- a/apps/sim/lib/workspace-files/application/download-workspace-file.ts +++ b/apps/sim/lib/workspace-files/application/download-workspace-file.ts @@ -115,7 +115,7 @@ async function executeDownloadWorkspaceFileStream({ contentUpdatedAt: file.contentUpdatedAt ?? undefined, }) : undefined - await reportWorkspaceFileDelivery(secretProvenance) + await reportWorkspaceFileDelivery(secretProvenance, file.id) return streamWorkspaceFileRecord( file, principal, diff --git a/apps/sim/lib/workspace-files/application/file-delivery-observer.ts b/apps/sim/lib/workspace-files/application/file-delivery-observer.ts index edc304b1915..aae029b5e71 100644 --- a/apps/sim/lib/workspace-files/application/file-delivery-observer.ts +++ b/apps/sim/lib/workspace-files/application/file-delivery-observer.ts @@ -2,12 +2,16 @@ import { AsyncLocalStorage } from 'node:async_hooks' import type { Principal } from '@sim/auth/principal' import type { WorkspaceFileSecretProvenance } from '@/lib/uploads/contexts/workspace/workspace-file-secret-provenance' -const observer = new AsyncLocalStorage< - (provenance?: WorkspaceFileSecretProvenance) => Promise ->() +/** `fileId` names the canonical file the classification belongs to, for attribution only. */ +type WorkspaceFileDeliveryObserver = ( + provenance: WorkspaceFileSecretProvenance | undefined, + fileId: string +) => Promise + +const observer = new AsyncLocalStorage() /** Internal transport evidence observes authorized bytes without changing public API admission or output. */ export function observeWorkspaceFileDelivery( - observe: (provenance?: WorkspaceFileSecretProvenance) => Promise, + observe: WorkspaceFileDeliveryObserver, execute: () => T ): T { return observer.run(observe, execute) @@ -15,11 +19,15 @@ export function observeWorkspaceFileDelivery( export function hasWorkspaceFileDeliveryObserver(): boolean { return observer.getStore() !== undefined } -/** Runs before the canonical use case returns bytes to its transport. */ +/** + * Runs before the canonical use case returns bytes to its transport. An observer that refuses the + * file throws, so its bytes are never read. + */ export async function reportWorkspaceFileDelivery( - provenance?: WorkspaceFileSecretProvenance + provenance: WorkspaceFileSecretProvenance | undefined, + fileId: string ): Promise { - await observer.getStore()?.(provenance) + await observer.getStore()?.(provenance, fileId) } /** A Chat read reached stored bytes with no observer to record their secret provenance. */ diff --git a/apps/sim/lib/workspace-files/application/file-versions.ts b/apps/sim/lib/workspace-files/application/file-versions.ts index 166efc7c8eb..3c7825eb58c 100644 --- a/apps/sim/lib/workspace-files/application/file-versions.ts +++ b/apps/sim/lib/workspace-files/application/file-versions.ts @@ -245,7 +245,7 @@ export const downloadWorkspaceFileVersion = defineAuthorizedWorkspaceFileUseCase async execute({ input, context, principal }): Promise { const file = await loadActiveFile(context) const version = await loadVersion(file, input.version) - await reportWorkspaceFileDelivery(await readVersionSecretProvenance(file, version)) + await reportWorkspaceFileDelivery(await readVersionSecretProvenance(file, version), file.id) const result = await readVersionObject(version.version, () => streamWorkspaceFileRecord(recordAtVersion(file, version), principal) ) diff --git a/apps/sim/lib/workspace-files/application/read-workspace-file-text.test.ts b/apps/sim/lib/workspace-files/application/read-workspace-file-text.test.ts index aa1ee02530a..1fa9e922d85 100644 --- a/apps/sim/lib/workspace-files/application/read-workspace-file-text.test.ts +++ b/apps/sim/lib/workspace-files/application/read-workspace-file-text.test.ts @@ -110,7 +110,7 @@ describe('readWorkspaceFileText', () => { const result = await observeWorkspaceFileDelivery(observe, () => readWorkspaceFileText.execute({ principal: principals[0], input: input() }) ) - expect(observe).toHaveBeenCalledWith(provenance) + expect(observe).toHaveBeenCalledWith(provenance, FILE_ID) expect(result.secretProvenance).toBeUndefined() }) it('does not return content if the private delivery observer refuses', async () => { diff --git a/apps/sim/lib/workspace-files/application/read-workspace-file-text.ts b/apps/sim/lib/workspace-files/application/read-workspace-file-text.ts index d6d0cda68e9..82ad8f03edd 100644 --- a/apps/sim/lib/workspace-files/application/read-workspace-file-text.ts +++ b/apps/sim/lib/workspace-files/application/read-workspace-file-text.ts @@ -143,7 +143,7 @@ export async function extractWorkspaceFileRecordText( contentUpdatedAt: file.contentUpdatedAt ?? undefined, }) : undefined) - await reportWorkspaceFileDelivery(secretProvenance) + await reportWorkspaceFileDelivery(secretProvenance, file.id) const extension = input.allowPlainText ? (workspaceFileTextFormat(file) ?? getFileExtension(file.name))