Skip to content

Commit a6eed4e

Browse files
committed
feat(mothership): create organization workspaces through CLI
1 parent b83d21b commit a6eed4e

15 files changed

Lines changed: 573 additions & 59 deletions

‎apps/sim/lib/api/contracts/mothership-management-tools.ts‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,22 @@
11
import { z } from 'zod'
2+
import { createWorkspaceInputSchema } from '@/lib/workspaces/create-input'
23
import { organizationSearchSourcesInputSchema } from './mothership-search-sources'
34
import { mothershipSettingsInputSchema } from './mothership-settings'
45

6+
export const mothershipWorkspacesInputSchema = z.discriminatedUnion('action', [
7+
createWorkspaceInputSchema.extend({ action: z.literal('create') }).strict(),
8+
])
9+
510
/** Shared input contracts and availability; permission decisions remain in the domain use cases. */
611
export const managementToolContracts = [
12+
{
13+
id: 'workspaces',
14+
route: 'sim',
15+
scope: 'organization',
16+
description:
17+
'Create a workspace in the conversation’s organization under the current user’s workspace-creation policy. Returns its ID for subsequent explicitly workspace-scoped commands. Includes a starter workflow unless skipDefaultWorkflow is true.',
18+
inputSchema: mothershipWorkspacesInputSchema,
19+
},
720
{
821
id: 'settings',
922
route: 'sim',

‎apps/sim/lib/api/contracts/workspaces.ts‎

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { z } from 'zod'
22
import { nonEmptyIdSchema, organizationRoleSchema } from '@/lib/api/contracts/primitives'
33
import { type ContractJsonResponse, defineRouteContract } from '@/lib/api/contracts/types'
4+
import { createWorkspaceInputSchema } from '@/lib/workspaces/create-input'
45
import { workspacePermissionUpdatesSchema } from '@/lib/workspaces/permissions/input'
56

67
export const workspaceScopeSchema = z.enum(['active', 'archived', 'all'])
@@ -61,10 +62,7 @@ export const listWorkspacesQuerySchema = z.object({
6162

6263
export type WorkspaceQueryScope = NonNullable<z.input<typeof listWorkspacesQuerySchema>['scope']>
6364

64-
export const createWorkspaceBodySchema = z.object({
65-
name: z.string().trim().min(1, 'Name is required'),
66-
skipDefaultWorkflow: z.boolean().optional().default(false),
67-
})
65+
export const createWorkspaceBodySchema = createWorkspaceInputSchema
6866

6967
export const workspaceParamsSchema = z.object({
7068
id: z.string().min(1),

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

Lines changed: 27 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -96,30 +96,33 @@ describe('scoped CLI service adapter', () => {
9696
})
9797
})
9898

99-
it.each(['list_workspaces', 'search_workspace', 'read_document', 'search_sources'] as const)(
100-
'%s keeps org chat authority without workspace resolution',
101-
async (name) => {
102-
const result = await executeAgentCliService(service(name), organization)
103-
expect(result.exitCode).toBe(0)
104-
expect(JSON.parse(result.stdout).documents[0].citation).toBe('<doc id="1" />')
105-
expect(boundary.workspace).not.toHaveBeenCalled()
106-
expect(boundary.organization).toHaveBeenCalledWith({
107-
principal: expect.objectContaining({
108-
subjectUserId: 'actor',
109-
organizationId: 'org',
110-
resourceScope: { chatId: 'chat' },
111-
}),
112-
})
113-
expect(boundary.route).toHaveBeenCalledWith(
114-
name,
115-
{},
116-
expect.objectContaining({ organizationId: 'org', workspaceId: undefined })
117-
)
118-
await expect(executeAgentCliService(service(name), workspace)).rejects.toThrow(
119-
'organization conversation'
120-
)
121-
}
122-
)
99+
it.each([
100+
'list_workspaces',
101+
'search_workspace',
102+
'read_document',
103+
'search_sources',
104+
'workspaces',
105+
] as const)('%s keeps org chat authority without workspace resolution', async (name) => {
106+
const result = await executeAgentCliService(service(name), organization)
107+
expect(result.exitCode).toBe(0)
108+
expect(JSON.parse(result.stdout).documents[0].citation).toBe('<doc id="1" />')
109+
expect(boundary.workspace).not.toHaveBeenCalled()
110+
expect(boundary.organization).toHaveBeenCalledWith({
111+
principal: expect.objectContaining({
112+
subjectUserId: 'actor',
113+
organizationId: 'org',
114+
resourceScope: { chatId: 'chat' },
115+
}),
116+
})
117+
expect(boundary.route).toHaveBeenCalledWith(
118+
name,
119+
{},
120+
expect.objectContaining({ organizationId: 'org', workspaceId: undefined })
121+
)
122+
await expect(executeAgentCliService(service(name), workspace)).rejects.toThrow(
123+
'organization conversation'
124+
)
125+
})
123126
it('passes trusted execution metadata through the real sim_cli handler and index branch', async () => {
124127
const signal = new AbortController().signal
125128
const result = await executeSimCli(
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import type { OperationUseCase } from '@/lib/core/application/operation'
2+
import { OrchestrationError } from '@/lib/core/orchestration/types'
3+
import {
4+
COPILOT_APPLICATION_DELEGATION_TTL_MS,
5+
type CopilotExecutionContext,
6+
createTrustedOrganizationCopilotPrincipal,
7+
requireTrustedOrganizationCopilotContext,
8+
} from '@/lib/mothership/auth/application-delegation'
9+
import { authorizeOrganizationChatDelegation } from '@/lib/mothership/chat/organization-chats'
10+
import {
11+
type OrganizationWorkspaceOperation,
12+
organizationWorkspaceOperations,
13+
} from '@/lib/workspaces/application/organization-operations'
14+
15+
/** Organization operations bind to the private chat, without inventing a workspace authority. */
16+
export async function executeOrganizationWorkspaceUseCase<
17+
O extends OrganizationWorkspaceOperation,
18+
I,
19+
R,
20+
>(
21+
context: CopilotExecutionContext | undefined,
22+
useCase: OperationUseCase<O, I & { organizationId: string }, R>,
23+
input: I
24+
): Promise<R> {
25+
if (
26+
!Object.values(organizationWorkspaceOperations).some(
27+
(operation) => operation === useCase.operation
28+
)
29+
)
30+
throw new Error('Unregistered organization workspace operation')
31+
const trusted = requireTrustedOrganizationCopilotContext(context)
32+
if (context?.requestMode !== 'agent')
33+
throw new OrchestrationError(
34+
'forbidden',
35+
'Workspace management requires organization agent mode'
36+
)
37+
const principal = createTrustedOrganizationCopilotPrincipal(
38+
{ ...trusted, delegationId: trusted.toolCallId },
39+
{ audience: useCase.operation.delegationAudience, ttlMs: COPILOT_APPLICATION_DELEGATION_TTL_MS }
40+
)
41+
await authorizeOrganizationChatDelegation.execute({ principal })
42+
return useCase.execute({ principal, input: { ...input, organizationId: trusted.organizationId } })
43+
}

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,14 @@ export type AgentCliStdoutInvocation = z.infer<typeof AgentCliStdoutInvocation>;
4646
/** Internal product operations reuse Sim's authorized handlers; public CLI operations stay native. */
4747
export const AgentCliServiceInvocation = z.object({
4848
kind: z.literal("service"),
49-
name: z.enum(["list_workspaces", "search_workspace", "read_document", "settings", "search_sources"]),
49+
name: z.enum([
50+
"list_workspaces",
51+
"search_workspace",
52+
"read_document",
53+
"settings",
54+
"search_sources",
55+
"workspaces",
56+
]),
5057
input: z.record(z.string(), z.json()),
5158
inputFiles: z.partialRecord(z.enum(["input", "changes"]), z.string().min(1).max(1000)).optional(),
5259
});

‎apps/sim/lib/mothership/tools/server/router.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import { organizationSearchSourcesServerTool } from '@/lib/mothership/tools/serv
3232
import { settingsServerTool } from '@/lib/mothership/tools/server/settings'
3333
import { getCredentialsServerTool } from '@/lib/mothership/tools/server/user/get-credentials'
3434
import { listWorkspacesServerTool } from '@/lib/mothership/tools/server/workspace-list'
35+
import { workspacesServerTool } from '@/lib/mothership/tools/server/workspaces'
3536

3637
export type ExecuteResponseSuccess = z.output<typeof ExecuteResponseSuccessSchema>
3738

@@ -62,6 +63,7 @@ const baseServerToolRegistry: Record<string, BaseServerTool> = {
6263
[searchDocsServerTool.name]: searchDocsServerTool,
6364
[searchWorkspaceServerTool.name]: searchWorkspaceServerTool,
6465
[listWorkspacesServerTool.name]: listWorkspacesServerTool,
66+
[workspacesServerTool.name]: workspacesServerTool,
6567
[organizationSearchSourcesServerTool.name]: organizationSearchSourcesServerTool,
6668
[settingsServerTool.name]: settingsServerTool,
6769
[openResourceServerTool.name]: openResourceServerTool,

‎apps/sim/lib/mothership/tools/server/workspace-list.ts‎

Lines changed: 5 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,6 @@
11
import { listWorkspacesInputSchema } from '@/lib/api/contracts/mothership-assistant-tools'
22
import { messageForCopilotApplicationError } from '@/lib/mothership/application/error'
3-
import {
4-
COPILOT_APPLICATION_DELEGATION_TTL_MS,
5-
createTrustedOrganizationCopilotPrincipal,
6-
requireTrustedOrganizationCopilotContext,
7-
} from '@/lib/mothership/auth/application-delegation'
8-
import { authorizeOrganizationChatDelegation } from '@/lib/mothership/chat/organization-chats'
3+
import { executeOrganizationWorkspaceUseCase } from '@/lib/mothership/application/execute-organization-workspace-use-case'
94
import type { BaseServerTool } from '@/lib/mothership/tools/server/base-tool'
105
import { listOrganizationWorkspaces } from '@/lib/workspaces/application/list-organization-workspaces'
116

@@ -14,18 +9,11 @@ export const listWorkspacesServerTool: BaseServerTool = {
149
inputSchema: listWorkspacesInputSchema,
1510
async execute(raw, context) {
1611
try {
17-
const trusted = requireTrustedOrganizationCopilotContext(context)
18-
if (context?.requestMode !== 'agent')
19-
throw new Error('Workspace discovery requires organization agent mode')
20-
const principal = createTrustedOrganizationCopilotPrincipal(
21-
{ ...trusted, delegationId: trusted.toolCallId },
22-
{ audience: 'sim:workspaces', ttlMs: COPILOT_APPLICATION_DELEGATION_TTL_MS }
12+
const result = await executeOrganizationWorkspaceUseCase(
13+
context,
14+
listOrganizationWorkspaces,
15+
listWorkspacesInputSchema.parse(raw)
2316
)
24-
await authorizeOrganizationChatDelegation.execute({ principal })
25-
const result = await listOrganizationWorkspaces.execute({
26-
principal,
27-
input: { ...listWorkspacesInputSchema.parse(raw), organizationId: trusted.organizationId },
28-
})
2917
return { success: true, ...result }
3018
} catch (error) {
3119
return {

0 commit comments

Comments
 (0)