Skip to content

Commit b26d9b1

Browse files
committed
test(files): run Chat file reads through the production CLI transport composition
1 parent 58132a3 commit b26d9b1

4 files changed

Lines changed: 134 additions & 48 deletions

File tree

‎apps/sim/lib/mothership/agent-cli/index.ts‎

Lines changed: 9 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,11 @@ import { withWorkspaceInvocationScope } from '@/lib/core/application/workspace-i
33
import { getInternalApiBaseUrl } from '@/lib/core/utils/urls'
44
import { curateBlockDetail } from '@/lib/mothership/agent-cli/curation'
55
import { AUGMENTATION_ENGINES, runEngine } from '@/lib/mothership/agent-cli/engines'
6-
import { createFileReadTransport } from '@/lib/mothership/agent-cli/file-read-transport'
7-
import { createFileUploadTransport } from '@/lib/mothership/agent-cli/file-upload-transport'
86
import { curateKnowledgeDocuments } from '@/lib/mothership/agent-cli/knowledge-curation'
9-
import { createResourceEffectTransport } from '@/lib/mothership/agent-cli/resource-effects'
107
import { runCli } from '@/lib/mothership/agent-cli/run-cli'
11-
import { createScopedCliTransport } from '@/lib/mothership/agent-cli/scoped-transport'
128
import { executeAgentCliService } from '@/lib/mothership/agent-cli/services'
139
import { applySink } from '@/lib/mothership/agent-cli/sink'
14-
import { createTableReadTransport } from '@/lib/mothership/agent-cli/table-read-transport'
15-
import { createTracedCliTransport } from '@/lib/mothership/agent-cli/traced-transport'
10+
import { createAgentCliTransport } from '@/lib/mothership/agent-cli/transport'
1611
import { createWorkbenchFileProvenance } from '@/lib/mothership/agent-cli/workbench-file-provenance'
1712
import { resolveInvocationWorkspace } from '@/lib/mothership/application/workspace-target'
1813
import {
@@ -79,44 +74,22 @@ async function executeBoundAgentCliRequest(
7974
const endpoint = getInternalApiBaseUrl()
8075
const sessionKey = context.chatId ? chatSandboxSessionKey(context.chatId) : null
8176
const files = sessionKey ? createWorkbenchFileProvenance({ ...context, sessionKey }) : undefined
82-
const reads = createFileReadTransport({
83-
endpoint,
84-
transport: createTableReadTransport({
85-
endpoint,
86-
transport: createTracedCliTransport(
87-
endpoint,
88-
createScopedCliTransport(endpoint, invocationIdentity)
89-
),
90-
registry: context.resolvedSecretTraceRegistry,
91-
}),
92-
userId: context.userId,
93-
invocation: invocationIdentity,
94-
registry: context.resolvedSecretTraceRegistry,
95-
...(context.chatId !== undefined ? { chatId: context.chatId } : {}),
96-
...(files ? { trackDownload: files.trackDownload } : {}),
97-
})
9877
const resources: ResourceChange[] = []
9978
const identity: EmbeddedCliIdentity = {
10079
endpoint,
10180
apiKey,
10281
workspaceId: context.workspaceId,
103-
transport: createResourceEffectTransport(
82+
transport: createAgentCliTransport({
10483
endpoint,
105-
files
106-
? createFileUploadTransport({
107-
endpoint,
108-
workspaceId: context.workspaceId,
109-
userId: context.userId,
110-
invocation: invocationIdentity,
111-
fallback: reads,
112-
uploadProvenance: files.uploadProvenance,
113-
})
114-
: reads,
84+
invocation: invocationIdentity,
85+
registry: context.resolvedSecretTraceRegistry,
86+
...(files ? { files } : {}),
11587
resources,
116-
request.invocation.kind === 'cli' ||
88+
observeReads:
89+
request.invocation.kind === 'cli' ||
11790
(request.invocation.kind === 'augmentation' &&
118-
AUGMENTATION_ENGINES[request.invocation.name]?.openReadResources === true)
119-
),
91+
AUGMENTATION_ENGINES[request.invocation.name]?.openReadResources === true),
92+
}),
12093
...(context.signal ? { signal: context.signal } : {}),
12194
}
12295

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { createFileReadTransport } from '@/lib/mothership/agent-cli/file-read-transport'
2+
import { createFileUploadTransport } from '@/lib/mothership/agent-cli/file-upload-transport'
3+
import { createResourceEffectTransport } from '@/lib/mothership/agent-cli/resource-effects'
4+
import { createScopedCliTransport } from '@/lib/mothership/agent-cli/scoped-transport'
5+
import { createTableReadTransport } from '@/lib/mothership/agent-cli/table-read-transport'
6+
import { createTracedCliTransport } from '@/lib/mothership/agent-cli/traced-transport'
7+
import type { createWorkbenchFileProvenance } from '@/lib/mothership/agent-cli/workbench-file-provenance'
8+
import type { CopilotChatDelegationContext } from '@/lib/mothership/auth/application-delegation'
9+
import type { ResourceChange } from '@/lib/mothership/generated/resources'
10+
import type { ResolvedSecretTraceRegistry } from '@/executor/utils/resolved-secret-trace-registry'
11+
12+
/**
13+
* The transport every Chat CLI invocation and augmentation engine reaches Sim through.
14+
*
15+
* Order is load-bearing: the file read layer sits outside in-process admission so every file
16+
* read and every other `GET` runs under a delivery observer that records the secret provenance
17+
* of the bytes, and resource effects observe what the composed stack returns.
18+
*/
19+
export function createAgentCliTransport(context: {
20+
endpoint: string
21+
invocation: CopilotChatDelegationContext & { workspaceId: string }
22+
registry?: ResolvedSecretTraceRegistry
23+
files?: ReturnType<typeof createWorkbenchFileProvenance>
24+
resources: ResourceChange[]
25+
observeReads: boolean
26+
}): typeof fetch {
27+
const { endpoint, invocation, registry, files } = context
28+
const reads = createFileReadTransport({
29+
endpoint,
30+
transport: createTableReadTransport({
31+
endpoint,
32+
transport: createTracedCliTransport(endpoint, createScopedCliTransport(endpoint, invocation)),
33+
registry,
34+
}),
35+
userId: invocation.userId,
36+
invocation,
37+
registry,
38+
...(invocation.chatId !== undefined ? { chatId: invocation.chatId } : {}),
39+
...(files ? { trackDownload: files.trackDownload } : {}),
40+
})
41+
return createResourceEffectTransport(
42+
endpoint,
43+
files
44+
? createFileUploadTransport({
45+
endpoint,
46+
workspaceId: invocation.workspaceId,
47+
userId: invocation.userId,
48+
invocation,
49+
fallback: reads,
50+
uploadProvenance: files.uploadProvenance,
51+
})
52+
: reads,
53+
context.resources,
54+
context.observeReads
55+
)
56+
}

‎apps/sim/lib/mothership/tools/sandbox-resource-transport.test.ts‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { createDelegatedPrincipal } from '@sim/testing/factories/principal.factory'
12
import {
23
mothershipWorkspaceTargetMock,
34
mothershipWorkspaceTargetMockFns,
@@ -7,6 +8,10 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
78
import { isCopilotRequest } from '@/lib/api/server/routes/copilot-request'
89
import { assertWorkspaceInvocationScope } from '@/lib/core/application/workspace-invocation-scope'
910
import { OrchestrationError } from '@/lib/core/orchestration/types'
11+
import {
12+
reportWorkspaceFileDelivery,
13+
requireCopilotWorkspaceFileDeliveryObserver,
14+
} from '@/lib/workspace-files/application/file-delivery-observer'
1015

1116
const { readScope, recordEffects, fetcher, routeMatcher, recordInput, mint } = vi.hoisted(() => ({
1217
readScope: vi.fn(),
@@ -275,6 +280,30 @@ it('cannot deliver a successful unclassified file response when recording its un
275280
expect(fetcher).toHaveBeenCalledOnce()
276281
})
277282

283+
/**
284+
* The file read and download use cases refuse a Chat principal that no delivery observer records,
285+
* so the workbench's file reads depend on this proxy installing one around the route handler.
286+
*/
287+
it('dispatches a workbench file read under a delivery observer the Copilot guard admits', async () => {
288+
const provenance = { status: 'exact', entries: [] } as const
289+
routeMatcher.mockReturnValue({ params: { fileId: 'file' }, load: async () => ({ GET: fetcher }) })
290+
fetcher.mockImplementation(async () => {
291+
requireCopilotWorkspaceFileDeliveryObserver(createDelegatedPrincipal({ serviceId: 'copilot' }))
292+
await reportWorkspaceFileDelivery(provenance)
293+
return new Response('file text')
294+
})
295+
recordInput.mockResolvedValueOnce(undefined)
296+
297+
const response = await proxySandboxResourceRequest(
298+
request('/api/v2/files/file/text?workspaceId=workspace'),
299+
token
300+
)
301+
302+
expect(response.status).toBe(200)
303+
expect(await response.text()).toBe('file text')
304+
expect(recordInput).toHaveBeenCalledWith('mothership-chat:chat', provenance)
305+
})
306+
278307
vi.mock('@/lib/mothership/chat/delegation', () => ({ mintDelegationToken: mint }))
279308

280309
it('never resolves a real credential for an invalid callback scope', async () => {

‎apps/sim/lib/workspace-files/__integration__/copilot-file-versions.integration.ts‎

Lines changed: 40 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,9 @@ import {
3535
createKnowledgeAclFixtureIds,
3636
seedKnowledgeAclFixture,
3737
} from '@/lib/knowledge/__integration__/seed-source-access-fixture'
38-
import { createFileReadTransport } from '@/lib/mothership/agent-cli/file-read-transport'
38+
import { runCli } from '@/lib/mothership/agent-cli/run-cli'
3939
import { createScopedCliTransport } from '@/lib/mothership/agent-cli/scoped-transport'
40+
import { createAgentCliTransport } from '@/lib/mothership/agent-cli/transport'
4041
import { createCopilotChatPrincipal } from '@/lib/mothership/auth/application-delegation'
4142
import {
4243
updateWorkspaceFileContent,
@@ -126,36 +127,40 @@ describe('chat-delegated file version history', () => {
126127
}
127128

128129
/**
129-
* Chat's composed CLI transport: the provenance-observing read layer over in-process admission.
130+
* Chat's composed CLI transport, the same stack every Chat CLI invocation and engine runs on.
130131
* `layers` drops the outer layers to prove the inner ones hold on their own.
131132
*/
132-
function chatTransport(
133+
function chatFetch(
133134
fixture: { workspaceId: string; organizationId: string },
134135
userId: string,
135136
registry?: ResolvedSecretTraceRegistry,
136137
layers: { observer?: boolean; invocationScope?: boolean } = {}
137-
) {
138+
): typeof fetch {
138139
const invocation = { userId, workspaceId: fixture.workspaceId, chatId: generateId() }
139-
const scoped = createScopedCliTransport(ORIGIN, invocation)
140140
const transport =
141141
layers.observer === false
142-
? scoped
143-
: createFileReadTransport({
142+
? createScopedCliTransport(ORIGIN, invocation)
143+
: createAgentCliTransport({
144144
endpoint: ORIGIN,
145-
transport: scoped,
146-
userId,
147145
invocation,
148146
...(registry ? { registry } : {}),
147+
resources: [],
148+
observeReads: true,
149149
})
150-
return (url: string, init?: RequestInit) =>
150+
return (input, init) =>
151151
layers.invocationScope === false
152-
? transport(`${ORIGIN}${url}`, init)
152+
? transport(input, init)
153153
: withWorkspaceInvocationScope(
154154
{ workspaceId: fixture.workspaceId, organizationId: fixture.organizationId },
155-
() => transport(`${ORIGIN}${url}`, init)
155+
() => transport(input, init)
156156
)
157157
}
158158

159+
function chatTransport(...args: Parameters<typeof chatFetch>) {
160+
const transport = chatFetch(...args)
161+
return (url: string, init?: RequestInit) => transport(`${ORIGIN}${url}`, init)
162+
}
163+
159164
function registryFor(fixture: { workspaceId: string }, userId: string) {
160165
return new ResolvedSecretTraceRegistry([], { userId, workspaceId: fixture.workspaceId })
161166
}
@@ -492,6 +497,29 @@ describe('chat-delegated file version history', () => {
492497
expect(await new Response(result.stream).text()).toBe(`token=${SECRET}`)
493498
})
494499

500+
it('serves a Chat `files read` through the composed CLI', async () => {
501+
const fixture = await seedVersionedFile()
502+
await revertToSecretVersion(fixture)
503+
const registry = registryFor(fixture, fixture.bobId)
504+
505+
const result = await runCli(
506+
['files', 'read', fixture.fileId],
507+
{
508+
endpoint: ORIGIN,
509+
apiKey: 'mothership-in-process',
510+
workspaceId: fixture.workspaceId,
511+
transport: chatFetch(fixture, fixture.bobId, registry),
512+
},
513+
null
514+
)
515+
516+
expect(result.exitCode, result.stderr).toBe(0)
517+
const projected = projectResolvedSecretModelContent(result.stdout, registry)
518+
if (!projected.safe) throw new Error('File text was withheld')
519+
expect(projected.value).not.toContain(SECRET)
520+
expect(projected.value).toContain('token=[REDACTED_SECRET]')
521+
})
522+
495523
it('serves Chat through the transport that records provenance', async () => {
496524
const fixture = await seedVersionedFile()
497525
await revertToSecretVersion(fixture)

0 commit comments

Comments
 (0)