Skip to content

Commit 29111a9

Browse files
committed
fix(mothership): preserve sharing and search semantics
1 parent a5d2d82 commit 29111a9

12 files changed

Lines changed: 286 additions & 17 deletions

File tree

‎apps/sim/lib/benchmarks/application/operations.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ export const benchmarkOperations = {
102102

103103
export const benchmarkSourceOperation = defineWorkspaceOperation({
104104
id: 'benchmarks.source.read',
105+
contentDelivery: 'none',
105106
minimumRole: 'read',
106107
workspaceApiKey: 'deny',
107108
principalKinds: ['delegated'],

‎apps/sim/lib/mothership/__integration__/content-delivery.integration.ts‎

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { mkdtempSync } from 'node:fs'
88
import { rm } from 'node:fs/promises'
99
import { tmpdir } from 'node:os'
1010
import path from 'node:path'
11+
import { createSessionPrincipal } from '@sim/testing/factories/principal.factory'
1112
import { featureFlagsMock, featureFlagsMockFns } from '@sim/testing/mocks/feature-flags.mock'
1213
import { rateLimiterMock } from '@sim/testing/mocks/rate-limiter.mock'
1314
import { generateId } from '@sim/utils/id'
@@ -108,13 +109,22 @@ import { createCopilotChatPrincipal } from '@/lib/mothership/auth/application-de
108109
import { createRegistryContentGate } from '@/lib/mothership/content-delivery/registry-gate'
109110
import { proxySandboxResourceRequest } from '@/lib/mothership/tools/sandbox-resource-transport'
110111
import { chatSandboxSessionKey } from '@/lib/mothership/tools/sandbox-session-key'
112+
import { resolveServableDoc } from '@/lib/mothership/tools/server/files/doc-compile'
113+
import { getShareForResource } from '@/lib/public-shares/share-manager'
111114
import {
112115
createOwnedWorkspaceFile,
116+
getWorkspaceFile,
117+
updateWorkspaceFileContent as replaceFixtureFileContent,
113118
uploadWorkspaceFile,
114119
} from '@/lib/uploads/contexts/workspace/workspace-file-manager'
115120
import type { WorkspaceFileSecretProvenance } from '@/lib/uploads/contexts/workspace/workspace-file-secret-provenance'
116121
import { WORKSPACE_FILES_DELEGATION_AUDIENCE } from '@/lib/workspace-files/application/authorization'
117122
import { readWorkspaceFileContent } from '@/lib/workspace-files/application/read-workspace-file-content'
123+
import {
124+
getWorkspaceFileShares,
125+
updateWorkspaceFileShare,
126+
} from '@/lib/workspace-files/application/share-workspace-file'
127+
import { storeCompiledDoc } from '@/lib/workspace-files/compiled-document-store'
118128
import { indexWorkspaceFileForSearch } from '@/lib/workspace-files/search/indexing'
119129
import { projectResolvedSecretModelContent } from '@/executor/utils/resolved-secret-content-projection'
120130
import { ResolvedSecretTraceRegistry } from '@/executor/utils/resolved-secret-trace-registry'
@@ -176,6 +186,8 @@ describe('content reaching Chat is admitted per item', () => {
176186

177187
beforeAll(async () => {
178188
fixtureStorage.root = mkdtempSync(path.join(tmpdir(), 'sim-content-delivery-'))
189+
// Compile the lazy tool graph before request deadlines and per-case provider fixtures exist.
190+
await matchV2Route('/api/v2/tools/file_get_content/execute')?.load()
179191
fixture = await seedContentFixture()
180192
connector = await seedKnowledgeConnector(fixture)
181193
}, 120_000)
@@ -578,6 +590,147 @@ describe('content reaching Chat is admitted per item', () => {
578590
expect((await share(fixture.files.entries)).status).toBe(403)
579591
expect((await share(fixture.files.clean)).status).toBe(200)
580592
})
593+
it.each([
594+
'clean',
595+
'unrecorded',
596+
'secret',
597+
'unknown',
598+
'old-secret',
599+
'malformed',
600+
'invalid',
601+
'mislabeled',
602+
'uploaded',
603+
'legacy',
604+
] as const)(
605+
'checks the published document inputs before exposing a share (%s)',
606+
async (kind) => {
607+
const entries = [
608+
{
609+
name: 'COMPILED_INPUT',
610+
encryptedValue: (await encryptSecret(CANARY.secret)).encrypted,
611+
sourceUserId: fixture.ids.aliceId,
612+
sourceWorkspaceId: ws(),
613+
},
614+
]
615+
const inputContent = kind === 'clean' ? CANARY.clean : CANARY.secret
616+
const input = await uploadWorkspaceFile(
617+
ws(),
618+
fixture.ids.aliceId,
619+
Buffer.from(inputContent),
620+
`compiled-input-${generateId()}.txt`,
621+
'text/plain',
622+
{
623+
notifyWorkspaceChange: false,
624+
secretProvenance:
625+
kind === 'unknown'
626+
? { status: 'unknown' }
627+
: kind === 'unrecorded'
628+
? { status: 'unrecorded' }
629+
: { status: 'exact', entries: kind === 'clean' ? [] : entries },
630+
}
631+
)
632+
const record = await getWorkspaceFile(ws(), input.id, { throwOnError: true })
633+
if (!record) throw new Error('Missing compiled input fixture')
634+
const source =
635+
kind === 'uploaded'
636+
? '%PDF-1.7\nordinary uploaded binary'
637+
: `data = open(input_path("${input.id}"), "rb").read()`
638+
const document = await uploadWorkspaceFile(
639+
ws(),
640+
fixture.ids.aliceId,
641+
Buffer.from(source),
642+
`compiled-share-${generateId()}.pdf`,
643+
kind === 'mislabeled' || kind === 'uploaded' ? 'application/pdf' : 'text/x-python-pdf',
644+
{ notifyWorkspaceChange: false, secretProvenance: { status: 'exact', entries: [] } }
645+
)
646+
const identity = JSON.stringify({
647+
version: 1,
648+
inputs: [
649+
{
650+
fileId: input.id,
651+
key: record.key,
652+
context: record.storageContext ?? 'workspace',
653+
contentVersion: (record.contentUpdatedAt ?? record.updatedAt).toISOString(),
654+
size: record.size,
655+
},
656+
],
657+
})
658+
const artifact = Buffer.from(`%PDF-1.7\n${inputContent}`)
659+
await storeCompiledDoc(
660+
ws(),
661+
source,
662+
'pdf',
663+
'application/pdf',
664+
artifact,
665+
kind === 'legacy'
666+
? undefined
667+
: kind === 'malformed'
668+
? '{invalid'
669+
: kind === 'invalid'
670+
? JSON.stringify({ version: 1, inputs: [{}] })
671+
: identity
672+
)
673+
const published = await resolveServableDoc(ws(), Buffer.from(source), document.name)
674+
expect(published.kind).toBe(kind === 'uploaded' ? 'passthrough' : 'artifact')
675+
if (published.kind === 'artifact') expect(published.buffer).toEqual(artifact)
676+
if (kind === 'old-secret') {
677+
await replaceFixtureFileContent(
678+
ws(),
679+
input.id,
680+
fixture.ids.aliceId,
681+
Buffer.from('now clean'),
682+
undefined,
683+
{
684+
version: { source: 'api', authorUserId: fixture.ids.aliceId },
685+
secretProvenancePolicy: {
686+
mode: 'replace',
687+
provenance: { status: 'exact', entries: [] },
688+
},
689+
}
690+
)
691+
}
692+
const allowed = ['clean', 'unrecorded', 'legacy', 'uploaded'].includes(kind)
693+
const invalid = kind === 'malformed' || kind === 'invalid'
694+
const activation = await cli('PATCH', `/api/v2/files/${document.id}/share`, {
695+
workspaceId: ws(),
696+
isActive: true,
697+
authType: 'public',
698+
})
699+
expect.soft(activation.status).toBe(allowed ? 200 : invalid ? 500 : 403)
700+
if (!allowed) expect.soft(await getShareForResource('file', document.id)).toBeNull()
701+
702+
const humanShare = await updateWorkspaceFileShare.execute({
703+
principal: createSessionPrincipal({ userId: fixture.ids.aliceId }),
704+
input: { fileId: document.id, isActive: true, authType: 'public' },
705+
})
706+
expect(humanShare.share.isActive).toBe(true)
707+
for (const read of [cli, sandbox]) {
708+
const link = await read('GET', `/api/v2/files/${document.id}/share?workspaceId=${ws()}`)
709+
expect.soft(link.status).toBe(allowed ? 200 : invalid ? 500 : 403)
710+
if (!allowed) expect.soft(link.body).not.toContain(humanShare.share.token)
711+
}
712+
if (!invalid) {
713+
const result = await observeContentDelivery(
714+
createRegistryContentGate({
715+
registry: new ResolvedSecretTraceRegistry([]),
716+
actorUserId: fixture.ids.aliceId,
717+
workspaceId: ws(),
718+
}),
719+
() =>
720+
getWorkspaceFileShares.execute({
721+
principal: createCopilotChatPrincipal(
722+
{ userId: fixture.ids.aliceId, workspaceId: ws() },
723+
WORKSPACE_FILES_DELEGATION_AUDIENCE
724+
),
725+
input: { workspaceId: ws(), fileIds: [document.id, fixture.files.clean] },
726+
})
727+
)
728+
expect(result.shares.has(document.id)).toBe(allowed)
729+
expect(result.shares.has(fixture.files.clean)).toBe(true)
730+
}
731+
}
732+
)
733+
581734
it('reads one file directly: clean as is, secrets redacted, unknown withheld whole (#1, D7)', async () => {
582735
const download = (fileId: string) => cli('GET', `/api/v2/files/${fileId}?workspaceId=${ws()}`)
583736
const clean = await download(fixture.files.clean)

‎apps/sim/lib/mothership/agent-cli/engines/universal-grep.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -461,7 +461,9 @@ function clip(line: string): string {
461461
*/
462462
function redactForModel(text: string, egress: ResolvedSecretTraceRegistry | undefined) {
463463
if (!egress) return text
464-
const projection = projectResolvedSecretModelContent(text, egress)
464+
const projection = projectResolvedSecretModelContent(text, egress, undefined, {
465+
preserveLineBreaks: true,
466+
})
465467
return projection.safe && typeof projection.value === 'string' ? projection.value : null
466468
}
467469

‎apps/sim/lib/mothership/agent-cli/file-provenance.test.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -623,6 +623,8 @@ describe('file provenance at the actual CLI and model-result boundary', () => {
623623
'FIXTURE KEY|KEYLINE'
624624
)
625625
for (const fragment of fragments(keyBlock)) expect(projected).not.toContain(fragment)
626+
const footer = await grepSecretFile(`config start\n${keyBlock}\nconfig end`, 'config end')
627+
expect(footer).toContain(':6: config end')
626628
})
627629
})
628630

‎apps/sim/lib/mothership/memory/application/read-scope.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { requireOwnedMemorySpace } from '@/lib/mothership/memory/spaces'
1111
export const MEMORY_SCOPE_AUDIENCE = 'sim:copilot-memory'
1212
export const readMemoryScopeOperation = defineWorkspaceOperation({
1313
id: 'mothership.memory.read_scope',
14+
contentDelivery: 'none',
1415
minimumRole: 'read',
1516
workspaceApiKey: 'deny',
1617
capability: 'copilot.use',
@@ -23,6 +24,7 @@ export const readMemoryScope = defineAuthorizedChatUseCase({
2324
operation: readMemoryScopeOperation,
2425
organizationOperation: defineOrganizationOperation({
2526
id: readMemoryScopeOperation.id,
27+
contentDelivery: readMemoryScopeOperation.contentDelivery,
2628
minimumRole: 'member',
2729
capability: readMemoryScopeOperation.capability,
2830
principalKinds: ['organization_delegated'],

‎apps/sim/lib/mothership/tools/server/files/doc-compile.test.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ vi.mock('@/lib/workspace-files/application/read-workspace-file-metadata', () =>
2929
readWorkspaceFileMetadata: { execute: readWorkspaceFileMetadataMock },
3030
}))
3131
vi.mock('@/lib/execution/sandbox/run-task', () => ({ runSandboxTask: runSandboxTaskMock }))
32-
vi.mock('./doc-compiled-store', () => ({
32+
vi.mock('@/lib/workspace-files/compiled-document-store', async (importOriginal) => ({
33+
...(await importOriginal<typeof import('@/lib/workspace-files/compiled-document-store')>()),
3334
loadCompiledDoc: loadCompiledDocMock,
3435
loadPublishedCompiledDoc: vi.fn(),
3536
publishCompiledDocArtifact: publishCompiledDocArtifactMock,

‎apps/sim/lib/mothership/tools/server/files/doc-compile.ts‎

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,20 @@ import {
1111
} from '@/lib/execution/remote-sandbox'
1212
import { runSandboxTask } from '@/lib/execution/sandbox/run-task'
1313
import { DocCompileUserError } from '@/lib/mothership/tools/server/files/doc-compile-error'
14+
import { PPTX_SHIM_JS } from '@/lib/mothership/tools/server/files/pptx-shim'
15+
import type { WorkspaceFileSecretProvenanceIdentity } from '@/lib/uploads/contexts/workspace/workspace-file-secret-provenance'
16+
import { admitWorkspaceFiles } from '@/lib/workspace-files/application/admit-file-content'
17+
import { readWorkspaceFileContent } from '@/lib/workspace-files/application/read-workspace-file-content'
18+
import { readWorkspaceFileMetadata } from '@/lib/workspace-files/application/read-workspace-file-metadata'
1419
import {
1520
type CompiledDocReadOptions,
1621
loadCompiledDoc,
1722
loadPublishedCompiledDoc,
23+
COMPILED_PDF_MAGIC as PDF_MAGIC,
1824
publishCompiledDocArtifact,
1925
storeCompiledDoc,
20-
} from '@/lib/mothership/tools/server/files/doc-compiled-store'
21-
import { PPTX_SHIM_JS } from '@/lib/mothership/tools/server/files/pptx-shim'
22-
import type { WorkspaceFileSecretProvenanceIdentity } from '@/lib/uploads/contexts/workspace/workspace-file-secret-provenance'
23-
import { admitWorkspaceFiles } from '@/lib/workspace-files/application/admit-file-content'
24-
import { readWorkspaceFileContent } from '@/lib/workspace-files/application/read-workspace-file-content'
25-
import { readWorkspaceFileMetadata } from '@/lib/workspace-files/application/read-workspace-file-metadata'
26+
COMPILED_ZIP_MAGIC as ZIP_MAGIC,
27+
} from '@/lib/workspace-files/compiled-document-store'
2628
import { getContentType } from '@/app/api/files/utils'
2729
import type { SandboxTaskId } from '@/sandbox-tasks/registry'
2830

@@ -664,9 +666,6 @@ interface CompilableFormat {
664666
contentType: string
665667
}
666668

667-
const ZIP_MAGIC = Buffer.from([0x50, 0x4b, 0x03, 0x04])
668-
const PDF_MAGIC = Buffer.from([0x25, 0x50, 0x44, 0x46, 0x2d])
669-
670669
const COMPILABLE_FORMATS: Record<string, CompilableFormat> = {
671670
'.pptx': { magic: ZIP_MAGIC, taskId: 'pptx-generate', contentType: PPTX_MIME },
672671
'.docx': { magic: ZIP_MAGIC, taskId: 'docx-generate', contentType: DOCX_MIME },

‎apps/sim/lib/mothership/tools/server/files/doc-servable.test.ts‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ vi.mock('@/lib/workspace-files/application/read-workspace-file-content', () => (
3333
vi.mock('@/lib/workspace-files/application/read-workspace-file-metadata', () => ({
3434
readWorkspaceFileMetadata: { execute: mockReadWorkspaceFileMetadata },
3535
}))
36-
vi.mock('./doc-compiled-store', () => ({
36+
vi.mock('@/lib/workspace-files/compiled-document-store', async (importOriginal) => ({
37+
...(await importOriginal<typeof import('@/lib/workspace-files/compiled-document-store')>()),
3738
loadCompiledDoc: mockLoadCompiledDoc,
3839
loadPublishedCompiledDoc: mockLoadPublishedCompiledDoc,
3940
publishCompiledDocArtifact: mockPublishCompiledDocArtifact,
@@ -48,8 +49,12 @@ vi.mock('@/app/api/files/utils', () => ({
4849
: 'application/octet-stream',
4950
}))
5051

52+
import {
53+
compileDoc,
54+
resolveServableDoc,
55+
resolveServableDocBytes,
56+
} from '@/lib/mothership/tools/server/files/doc-compile'
5157
import { DocCompileUserError } from '@/lib/mothership/tools/server/files/doc-compile-error'
52-
import { compileDoc, resolveServableDoc, resolveServableDocBytes } from './doc-compile'
5358

5459
const { mockExecuteInSandbox } = remoteSandboxMockFns
5560

‎apps/sim/lib/workspace-files/application/share-workspace-file.ts‎

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,22 @@ import {
2121
upsertFileShare,
2222
} from '@/lib/public-shares/share-manager'
2323
import {
24+
findListedWorkspaceFiles,
2425
getWorkspaceFile,
2526
loadActiveWorkspaceContext,
2627
} from '@/lib/uploads/contexts/workspace/workspace-file-manager'
2728
import {
29+
getBoundWorkspaceFileSecretProvenanceByIdentity,
2830
getCurrentWorkspaceFileSecretProvenance,
2931
type WorkspaceFileSecretProvenance,
3032
} from '@/lib/uploads/contexts/workspace/workspace-file-secret-provenance'
33+
import { downloadFileStream } from '@/lib/uploads/core/storage-service'
34+
import { getFileExtension, isRenderableDocumentName } from '@/lib/uploads/utils/file-utils'
35+
import { workspaceFileProvenanceIdentity } from '@/lib/workspace-files/application/admit-file-content'
3136
import { defineAuthorizedWorkspaceFileUseCase } from '@/lib/workspace-files/application/authorized-workspace-file-use-case'
3237
import { fileOperations } from '@/lib/workspace-files/application/operations'
3338
import { resolveActiveWorkspaceFileContext } from '@/lib/workspace-files/application/workspace-file-context'
39+
import { loadPublishedCompiledDocInputs } from '@/lib/workspace-files/compiled-document-store'
3440
import { MAX_WORKSPACE_FILE_BULK_AFFECTED_ITEMS } from '@/lib/workspace-files/limits'
3541
import { validatePublicFileSharing } from '@/ee/access-control/utils/permission-check'
3642

@@ -97,7 +103,28 @@ async function admitSharedFiles(
97103
await admitContent(principal, [])
98104
if (!isCopilotPrincipal(principal) || fileIds.length === 0) return new Set(fileIds)
99105
const provenance = await getCurrentWorkspaceFileSecretProvenance(workspaceId, fileIds)
100-
return new Set(fileIds.filter((id) => isShareableByChat(provenance.get(id))))
106+
const shareable = new Set(fileIds.filter((id) => isShareableByChat(provenance.get(id))))
107+
if (shareable.size === 0) return shareable
108+
const files = await findListedWorkspaceFiles(workspaceId, [...shareable])
109+
for (const id of shareable) {
110+
const file = files.get(id)
111+
if (!file || file.id !== id || !isRenderableDocumentName(file.name)) continue
112+
const source = await downloadFileStream({
113+
key: file.key,
114+
context: file.storageContext ?? 'workspace',
115+
})
116+
const contributors = await loadPublishedCompiledDocInputs(
117+
workspaceId,
118+
source,
119+
getFileExtension(file.name)
120+
)
121+
const bound = await getBoundWorkspaceFileSecretProvenanceByIdentity(workspaceId, [
122+
workspaceFileProvenanceIdentity(file),
123+
...contributors,
124+
])
125+
if ([...bound.values()].some((value) => !isShareableByChat(value))) shareable.delete(id)
126+
}
127+
return shareable
101128
}
102129

103130
export const getWorkspaceFileShare = defineAuthorizedWorkspaceFileUseCase({

apps/sim/lib/mothership/tools/server/files/doc-compiled-store.test.ts renamed to apps/sim/lib/workspace-files/compiled-document-store.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
44
vi.mock('@/lib/uploads/core/storage-service', () => storageServiceMock)
55

66
import { PayloadSizeLimitError } from '@/lib/core/utils/stream-limits'
7+
import { MAX_BUFFERED_TRANSFER_BYTES } from '@/lib/uploads/shared/types'
78
import {
89
loadCompiledDoc,
910
loadPublishedCompiledDoc,
1011
storeCompiledDoc,
11-
} from '@/lib/mothership/tools/server/files/doc-compiled-store'
12-
import { MAX_BUFFERED_TRANSFER_BYTES } from '@/lib/uploads/shared/types'
12+
} from '@/lib/workspace-files/compiled-document-store'
1313

1414
const { mockDownloadFile, mockUploadFile } = storageServiceMockFns
1515

0 commit comments

Comments
 (0)