Skip to content

Commit b11fc5c

Browse files
committed
fix(mothership): preserve catalog curation and embedded CLI access
1 parent 6271431 commit b11fc5c

4 files changed

Lines changed: 64 additions & 28 deletions

File tree

apps/sim/lib/mothership/agent-cli/curation.test.ts

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { beforeEach, describe, expect, it, vi } from 'vitest'
2+
import type { V2BlockDetail } from '@/lib/api/contracts/v2/catalog'
23
import { curateBlockDetail } from '@/lib/mothership/agent-cli/curation'
34

45
const { permissionConfig, denied } = vi.hoisted(() => ({
@@ -21,14 +22,39 @@ vi.mock('@/lib/mothership/integration-tool-projection', () => ({
2122

2223
const viewer = { workspaceId: 'ws', userId: 'user' }
2324

24-
function blockDetail() {
25+
function blockDetail(): V2BlockDetail {
2526
return {
26-
type: 'slack',
27+
id: 'slack',
28+
name: 'Slack',
29+
description: 'Messaging',
30+
category: 'tools',
31+
source: 'builtin',
32+
triggerAllowed: false,
33+
triggerCapable: false,
34+
triggerIds: [],
35+
triggers: [],
36+
tags: [],
37+
preview: false,
38+
operationIds: ['send', 'canvas'],
39+
toolIds: ['slack_send', 'slack_canvas'],
40+
inputSchema: [
41+
{ id: 'operation', type: 'dropdown', options: [{ id: 'send' }, { id: 'canvas' }] },
42+
],
43+
operationInputSchema: { send: [], canvas: [] },
44+
inputDefinitions: {},
45+
outputs: {},
2746
operations: {
28-
send: { toolId: 'slack_send' },
29-
canvas: { toolId: 'slack_canvas' },
47+
send: { toolId: 'slack_send', inputs: {}, outputs: {}, inputSchema: [] },
48+
canvas: { toolId: 'slack_canvas', inputs: {}, outputs: {}, inputSchema: [] },
3049
},
31-
tools: [{ id: 'slack_send' }, { id: 'slack_canvas' }],
50+
tools: ['slack_send', 'slack_canvas'].map((id) => ({
51+
id,
52+
name: id,
53+
description: '',
54+
hostedApiKey: 'none',
55+
params: {},
56+
outputs: {},
57+
})),
3258
}
3359
}
3460

@@ -63,7 +89,11 @@ describe('curateBlockDetail', () => {
6389
expect(result.exitCode).toBe(0)
6490
const curated = JSON.parse(result.stdout)
6591
expect(Object.keys(curated.operations)).toEqual(['send'])
66-
expect(curated.tools).toEqual([{ id: 'slack_send' }])
92+
expect(curated.tools.map((tool: { id: string }) => tool.id)).toEqual(['slack_send'])
93+
expect(curated.operationIds).toEqual(['send'])
94+
expect(curated.operationInputSchema).toEqual({ send: [] })
95+
expect(curated.inputSchema[0].options).toEqual([{ id: 'send' }])
96+
expect(curated.toolIds).toEqual(['slack_send'])
6797
})
6898

6999
it('refuses a fully denied block', async () => {

apps/sim/lib/mothership/agent-cli/curation.ts

Lines changed: 26 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
* so a partially-denied block is trimmed to the operations this viewer may configure.
66
*/
77

8+
import { omit } from '@sim/utils/object'
9+
import { type V2BlockDetail, v2BlockDetailSchema } from '@/lib/api/contracts/v2/catalog'
810
import { agentCliFail } from '@/lib/mothership/agent-cli/types'
911
import type { AgentCliRawResult } from '@/lib/mothership/generated/agent-cli'
1012
import { resolveDeniedBlockOperations } from '@/lib/mothership/integration-tool-projection'
@@ -16,22 +18,13 @@ export interface CurationViewer {
1618
userId: string
1719
}
1820

19-
interface BlockDetailShape {
20-
type: string
21-
operations?: Record<string, unknown>
22-
tools?: Array<{ id?: unknown }>
23-
}
24-
25-
function parseBlockDetail(stdout: string): BlockDetailShape | null {
26-
let parsed: unknown
21+
function parseBlockDetail(stdout: string): V2BlockDetail | null {
2722
try {
28-
parsed = JSON.parse(stdout)
23+
const parsed = v2BlockDetailSchema.safeParse(JSON.parse(stdout))
24+
return parsed.success ? parsed.data : null
2925
} catch {
3026
return null
3127
}
32-
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return null
33-
const candidate = parsed as { type?: unknown }
34-
return typeof candidate.type === 'string' ? (parsed as BlockDetailShape) : null
3528
}
3629

3730
export async function curateBlockDetail(
@@ -45,16 +38,28 @@ export async function curateBlockDetail(
4538
if (!deniedTools?.length) return result
4639
const isToolAllowed = createToolAccessGate(deniedTools)
4740
const denied = resolveDeniedBlockOperations(deniedTools, isToolAllowed)
48-
if (denied.fullyDenied.has(detail.type)) {
49-
return agentCliFail(`Block "${detail.type}" is not available to you in this workspace.`)
41+
if (denied.fullyDenied.has(detail.id)) {
42+
return agentCliFail(`Block "${detail.id}" is not available to you in this workspace.`)
5043
}
51-
const deniedOperations = denied.needsProjection.get(detail.type)
44+
const deniedOperations = denied.needsProjection.get(detail.id)
5245
if (!deniedOperations) return result
53-
const operations = Object.fromEntries(
54-
Object.entries(detail.operations ?? {}).filter(([id]) => !deniedOperations.has(id))
46+
const operations = omit(detail.operations, [...deniedOperations])
47+
const tools = detail.tools.filter((tool) => isToolAllowed(tool.id))
48+
const inputSchema = detail.inputSchema.map((field) =>
49+
field.id === 'operation'
50+
? { ...field, options: field.options?.filter((option) => !deniedOperations.has(option.id)) }
51+
: field
5552
)
56-
const tools = (detail.tools ?? []).filter(
57-
(tool) => typeof tool.id !== 'string' || isToolAllowed(tool.id)
58-
)
59-
return { ...result, stdout: JSON.stringify({ ...detail, operations, tools }, null, 2) }
53+
return {
54+
...result,
55+
stdout: JSON.stringify({
56+
...detail,
57+
operations,
58+
operationIds: detail.operationIds.filter((id) => !deniedOperations.has(id)),
59+
operationInputSchema: omit(detail.operationInputSchema, [...deniedOperations]),
60+
inputSchema,
61+
tools,
62+
toolIds: detail.toolIds.filter(isToolAllowed),
63+
}),
64+
}
6065
}

apps/sim/lib/mothership/generated/workbench.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,6 @@ import { z } from "zod";
66
/** Executable bootstrap is served only on the authenticated Sim → worker connection. */
77
export const WorkbenchBootstrap = z.strictObject({
88
version: z.literal(1),
9-
entrypoint: z.string().min(1).max(65_536),
9+
entrypoint: z.string().min(1).max(1_048_576),
1010
});
1111
export type WorkbenchBootstrap = z.infer<typeof WorkbenchBootstrap>;

packages/sim-cli/src/runtime.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export { Command } from 'commander'
2+
export { runEmbeddedCli } from './embed'
23
export { SimApiError } from './http/client'
34
export { buildProgram } from './program'
45
export { runTerminalCli } from './terminal'

0 commit comments

Comments
 (0)