Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .github/scripts/http-e2e.sh
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,6 @@ case "$group" in
export COPILOT_TOOL_PERMISSIONS_ENABLED=true
export KNOWLEDGE_MEMBER_ACCESS=true CREDENTIAL_GROUPS=true ACCESS_CONTROL_ENABLED=true
export NEXT_PUBLIC_ACCESS_CONTROL_ENABLED=true NEXT_PUBLIC_ORGANIZATIONS_ENABLED=true
export MSHIP_SEARCH_INTEGRATION_TOOLS=true
bunx --no-install playwright install --with-deps chromium
export INTERNAL_API_SECRET=desktop-inbox-http-ci-local-secret-at-least-32-characters
export DB_TX_TRIPWIRE=throw
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,6 @@ Plain JavaScript without imports runs in isolated-vm within Node child workers i
| Variable | Description |
|----------|-------------|
| `COPILOT_API_KEY` | API key for the default Sim service. Required for Chat, the Sim Chat workflow block, and Inbox operations that use that service |
| `MSHIP_SEARCH_INTEGRATION_TOOLS` | Opt in to personal-account reads and reviewed actions in Search Assistant. Off by default. Applies when AppConfig is disabled or no feature-flags document is available; an available AppConfig document takes precedence. Requires organization account setup and configured provider credentials. Tool permissions and source restrictions still apply. See [Connect your account](/search/connect-your-account) |
| `NEXT_PUBLIC_CHAT_DISABLED` | Set to `true` to hide the Chat module: the workspace lands on your first workflow, with no chats list or editor Chat panel. Chat is shown when unset; `bunx sim-setup` sets it for you if you skip the chat key |
| `PII_URL` | Base URL of the Presidio service backing PII detection and redaction. The Helm chart wires it to its own `pii` Service when `pii.enabled`; on Compose point it at the PII service on your network. Local development and the Helm fallback use `http://localhost:5001`; that address works only when the service is reachable there. Point production at a running service to use redaction |
| `ADMIN_API_KEY` | Admin API key for GitOps operations and organization provisioning |
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/content/docs/search/ask-questions.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ Copy the reviewed draft into another app, or ask Assistant to create it using yo

## Review actions

When integration actions are enabled, Assistant can answer questions and take supported actions in the same conversation. For example, ask it to draft an email in Gmail or create a Jira issue using your own connected account.
Assistant can answer questions and take supported actions in the same conversation. For example, ask it to draft an email in Gmail or create a Jira issue using your own connected account.

Reads run without a prompt by default. Writes wait for review by default. Each proposal appears inline in the conversation with its app, account, destination, and proposed content. Select **Edit** to change supported fields, then choose **Allow** or **Cancel** for each action. After several proposals, **Allow N shown** or **Cancel N shown** applies your choice to that displayed batch. You can approve individual actions first, then cancel the remaining batch.

Expand Down
2 changes: 1 addition & 1 deletion apps/docs/content/docs/search/connect-your-account.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ Creating Jira sprints requires the Jira sprint permission. If your account was c

## Tool permissions

When Assistant integration actions are enabled, open the app in **Integrations** and select your account. Tool permissions appear in its details when the app has supported Assistant tools available to you. Set a preference for each operation or use the **Read-only tools** and **Write tools** group controls:
Open the app in **Integrations** and select your account. Tool permissions appear in its details when the app has supported Assistant tools available to you. Set a preference for each operation or use the **Read-only tools** and **Write tools** group controls:

| Preference | Behavior |
| --- | --- |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { InputToolbar } from '@/app/workspace/[workspaceId]/home/components/user
interface SearchInputBarProps {
inputRef: RefObject<HTMLTextAreaElement | null>
value: string
disabled?: boolean
onChange: (value: string) => void
onSubmit: () => void
onPaste?: ClipboardEventHandler<HTMLTextAreaElement>
Expand All @@ -36,6 +37,7 @@ export function SearchInputBar({
attachments,
inputRef,
value,
disabled = false,
onChange,
onSubmit,
onPaste,
Expand Down Expand Up @@ -106,6 +108,7 @@ export function SearchInputBar({
inputRef={inputRef}
scrollerRef={scrollerRef}
value={value}
disabled={disabled}
compact={!hasAttachments && !expanded}
className={hasAttachments ? 'min-h-[56px]' : undefined}
maxHeight={200}
Expand Down
9 changes: 7 additions & 2 deletions apps/sim/app/o/[organizationId]/search/search.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { MicButton } from '@/app/workspace/[workspaceId]/home/components/user-in
import { MicrophonePermissionHelp } from '@/app/workspace/[workspaceId]/home/components/user-input/components/microphone-permission-help/microphone-permission-help'
import { HomeFallback } from '@/app/workspace/[workspaceId]/home/home-fallback'
import { knowledgeKeys } from '@/hooks/queries/utils/knowledge-keys'
import { useHydrated } from '@/hooks/use-hydrated'
import { useVoiceInput } from '@/hooks/use-voice-input'
import { useMothershipDraftsStore } from '@/stores/mothership-drafts/store'

Expand Down Expand Up @@ -54,6 +55,7 @@ interface SearchFieldProps {
/** Search commits a query on submit while retaining an independent editable draft. */
function SearchField({ userId, initialValue, onSubmit }: SearchFieldProps) {
const inputRef = useRef<HTMLTextAreaElement>(null)
const hydrated = useHydrated()
const { organization } = useOrganizationContext()
const queryClient = useQueryClient()
const queryKey = knowledgeKeys.searchQuery(
Expand Down Expand Up @@ -90,14 +92,17 @@ function SearchField({ userId, initialValue, onSubmit }: SearchFieldProps) {
getValue: () => value,
onChange: setValue,
})
const canSubmit = value.trim().length > 0
const canSubmit = hydrated && value.trim().length > 0

useEffect(() => inputRef.current?.focus(), [])
useEffect(() => {
if (hydrated) inputRef.current?.focus()
}, [hydrated])

const field = (
<SearchInputBar
inputRef={inputRef}
value={value}
disabled={!hydrated}
onChange={setValue}
onSubmit={() => submit()}
floating={!initialValue.trim()}
Expand Down
6 changes: 2 additions & 4 deletions apps/sim/lib/assistant-actions/catalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import { projectIntegrationToolsForViewer } from '@/lib/integrations/tool-projec
import { requireOrganizationSearchApproval } from '@/lib/knowledge/search/integration-policy'
import { projectAssistantConnectedAccountTool } from '@/lib/mothership/assistant/connected-account-tool'
import { isAssistantIntegrationTool } from '@/lib/mothership/assistant/tool-policy'
import { isSearchIntegrationToolsEnabled } from '@/lib/mothership/feature-flags'
import { getUserPermissionConfigForOrganization } from '@/lib/permission-groups/resolve.server'
import { requiresScopedRetrieval } from '@/lib/sim-search/live/policy-schema'
import { livePolicyFor, loadLiveSearchPolicies } from '@/lib/sim-search/live/policy-store'
Expand All @@ -17,17 +16,16 @@ export async function loadAssistantIntegrationTools(
scope: { organizationId: string; userId: string },
executor?: DbOrTx
) {
const [visibility, config, enabled] = await Promise.all([
const [visibility, config] = await Promise.all([
getBlockVisibility({ userId: scope.userId, orgId: scope.organizationId }),
getUserPermissionConfigForOrganization(scope.organizationId, executor),
isSearchIntegrationToolsEnabled(),
])
const tools = projectIntegrationToolsForViewer(visibility, config)
.tools.map((entry) => ({
...entry,
config: projectAssistantConnectedAccountTool(entry.config),
}))
.filter(({ config: tool }) => enabled && isAssistantIntegrationTool(tool) && tool.oauth)
.filter(({ config: tool }) => isAssistantIntegrationTool(tool) && tool.oauth)
const providers = [
...new Set(
tools.flatMap(({ config: tool }) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,6 @@ import { getToolMetadata } from '@/tools/metadata'

vi.hoisted(() => {
Object.assign(process.env, {
MSHIP_SEARCH_INTEGRATION_TOOLS: 'true',
JIRA_CLIENT_ID: 'assistant-fixture-client',
JIRA_CLIENT_SECRET: 'assistant-fixture-client-secret',
GOOGLE_CLIENT_ID: 'assistant-google-fixture-client',
Expand Down
1 change: 0 additions & 1 deletion apps/sim/lib/core/config/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -606,7 +606,6 @@ export const env = createEnv({
DASHBOARDS: z.boolean().optional(),
PROJECTS: z.boolean().optional(), // Fallback for the `projects` feature flag off AppConfig
MSHIP_COMPUTER_USE: z.boolean().optional(),
MSHIP_SEARCH_INTEGRATION_TOOLS: z.boolean().optional(),
WORKFLOW_TESTS: z.boolean().optional(), // Fallback for the `workflow-tests` feature flag off AppConfig
CHANGELOG: z.boolean().optional(), // Fallback for the `changelog` feature flag off AppConfig
MSHIP_MODEL_SELECTOR: z.boolean().optional(),
Expand Down
5 changes: 0 additions & 5 deletions apps/sim/lib/core/config/feature-flags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,6 @@ const FEATURE_FLAGS = {
'personal workspaces follow the global switch. Each device must also opt in.',
fallback: 'MSHIP_COMPUTER_USE',
},
'mothership-search-integration-tools': {
description:
'Enable personal-account integration reads and reviewed actions in Search Assistant. Global rollout with MSHIP_SEARCH_INTEGRATION_TOOLS fallback when AppConfig is unavailable; disabled by default.',
fallback: 'MSHIP_SEARCH_INTEGRATION_TOOLS',
},
'workflow-tests': {
description:
'Enable workflow tests: the workspace Tests page, tests/<name>.test.js files, and the Sim tests tool. Supports global and organization rollout; disabled by default.',
Expand Down
79 changes: 27 additions & 52 deletions apps/sim/lib/mothership/chat/payload.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,25 +44,17 @@ const {
mockDashboardAvailability,
mockSecretNames,
mockComputerUseAvailable,
mockSearchIntegrationToolsEnabled,
} = vi.hoisted(() => ({
mockDashboardAvailability: vi.fn(async () => false),
mockComputerUseAvailable: vi.fn(async () => false),
mockCreateUserToolSchema: vi.fn(() => ({ type: 'object', properties: {} })),
mockSecretNames: vi.fn(async () => ({ names: [] as string[] })),
mockSearchIntegrationToolsEnabled: vi.fn(async () => true),
}))

vi.mock('@/lib/computer-use/application/availability', () => ({
isComputerUseAvailableFor: mockComputerUseAvailable,
}))

vi.mock('@/lib/mothership/feature-flags', () => ({
isSearchIntegrationToolsEnabled: mockSearchIntegrationToolsEnabled,
}))

beforeEach(() => mockSearchIntegrationToolsEnabled.mockResolvedValue(true))

// The inventory reads nine application worlds; these suites exercise the request shape, not the reads.
vi.mock('@/lib/mothership/application/execute-organization-secret-use-case', () => ({
executeOrganizationSecretUseCase: mockSecretNames,
Expand Down Expand Up @@ -948,49 +940,32 @@ describe('desktop request capabilities', () => {
})
})

it('carries only enabled MCP IDs without eager catalog discovery while preserving desktop capabilities', async () => {
const { getBlockVisibilityForCopilot } = await import('@/lib/mothership/block-visibility')
vi.mocked(getBlockVisibilityForCopilot).mockClear()
const principal = createSessionPrincipal({ userId: 'user-org' })
const payload = await buildCopilotRequestPayload({
message: 'Use my tools',
userId: 'user-org',
userMessageId: 'message-org',
organizationId: 'org-catalog',
chatId: 'chat-org',
principal,
mode: 'agent',
model: '',
mcpServerIds: ['server-a', 'server-b'],
browser: true,
terminalCapable: true,
})
expect(getBlockVisibilityForCopilot).not.toHaveBeenCalled()
expect(payload.integrationCatalog).toEqual({ mcpServerIds: ['server-a', 'server-b'] })
expect(payload).not.toHaveProperty('integrationTools')
expect(payload).not.toHaveProperty('mothershipTools')
expect(payload.desktop).toMatchObject({ browser: true, terminal: true })
})

/** The worker derives both its gateway tools and prompt instructions from this capability. */
it.each([{ organizationId: 'org-1' }, { workspaceId: 'ws-1' }])(
'switches Search integration capability per turn while preserving Build for %j',
async (scope) => {
for (const enabled of [true, false, true]) {
mockSearchIntegrationToolsEnabled.mockResolvedValue(enabled)
for (const mode of ['assistant', 'agent', 'plan']) {
const payload = await buildCopilotRequestPayload({
message: 'Find a person',
userId: 'person',
userMessageId: 'message',
mode,
model: '',
...scope,
})
expect(payload.integrationCatalog).toEqual(
mode === 'assistant' && !enabled ? undefined : { mcpServerIds: [] }
)
}
}
it.each(['assistant', 'agent', 'plan'] as const)(
'advertises the integration gateway while isolating MCP and desktop capabilities in %s mode',
async (mode) => {
const { getBlockVisibilityForCopilot } = await import('@/lib/mothership/block-visibility')
vi.mocked(getBlockVisibilityForCopilot).mockClear()
const principal = createSessionPrincipal({ userId: 'user-org' })
const payload = await buildCopilotRequestPayload({
message: 'Use my tools',
userId: 'user-org',
userMessageId: 'message-org',
organizationId: 'org-catalog',
chatId: 'chat-org',
principal,
mode,
model: '',
mcpServerIds: ['server-a', 'server-b'],
browser: true,
terminalCapable: true,
})
expect(getBlockVisibilityForCopilot).not.toHaveBeenCalled()
expect(payload.integrationCatalog).toEqual({
mcpServerIds: mode === 'assistant' ? [] : ['server-a', 'server-b'],
})
expect(payload).not.toHaveProperty('integrationTools')
expect(payload).not.toHaveProperty('mothershipTools')
if (mode === 'assistant') expect(payload.desktop).toBeUndefined()
else expect(payload.desktop).toMatchObject({ browser: true, terminal: true })
}
)
12 changes: 3 additions & 9 deletions apps/sim/lib/mothership/chat/payload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ import type { AssistantImageContent } from '@/lib/mothership/chat/assistant-imag
import { buildUploadedFileContext } from '@/lib/mothership/chat/upload-context'
import { buildWorkspaceInventory } from '@/lib/mothership/chat/workspace-inventory'
import { computeEntitlements } from '@/lib/mothership/entitlements'
import { isSearchIntegrationToolsEnabled } from '@/lib/mothership/feature-flags'
import type { AssistantSearchLevel } from '@/lib/mothership/generated/assistant'
import {
type ChatRequest,
Expand Down Expand Up @@ -388,7 +387,6 @@ export async function buildCopilotRequestPayload(params: BuildPayloadParams): Pr
params.principal !== undefined &&
computerUseOwner !== null &&
(await isComputerUseAvailableFor(params.principal, computerUseOwner))
const integrationGateway = !isAssistant || (await isSearchIntegrationToolsEnabled())

// Track uploaded files in the DB and build context tags instead of base64 inlining.
// Tracking writes `workspace_files` rows, so it needs the same write grant the
Expand Down Expand Up @@ -539,13 +537,9 @@ export async function buildCopilotRequestPayload(params: BuildPayloadParams): Pr
messageId: userMessageId,
...(chatId ? { chatId } : {}),
...(allContexts.length > 0 ? { context: allContexts } : {}),
...(integrationGateway
? {
integrationCatalog: {
mcpServerIds: isAssistant ? [] : [...new Set(params.mcpServerIds ?? [])],
},
}
: {}),
integrationCatalog: {
mcpServerIds: isAssistant ? [] : [...new Set(params.mcpServerIds ?? [])],
},
...(params.userTimezone ? { userTimezone: params.userTimezone } : {}),
...(params.effort ? { effort: params.effort } : {}),
...(params.modelSelection ? { modelSelection: params.modelSelection } : {}),
Expand Down
5 changes: 0 additions & 5 deletions apps/sim/lib/mothership/feature-flags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,6 @@ export function isPlanModeEnabled(userId: string): Promise<boolean> {
return canUseBenchmarks(userId)
}

/** One deployment gate controls Search integration discovery, execution, and prompt capability. */
export function isSearchIntegrationToolsEnabled(): Promise<boolean> {
return isFeatureEnabled('mothership-search-integration-tools')
}

/** Graph reads, writes and management use the same super-user gate as Plan and benchmarks. */
export function isMemorySpacesEnabled(userId: string, executor?: DbOrTx): Promise<boolean> {
return canUseBenchmarks(userId, executor)
Expand Down
15 changes: 0 additions & 15 deletions apps/sim/lib/mothership/integrations/application/catalog.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,7 @@ import {
readIntegrationCatalog,
} from '@/lib/mothership/integrations/application/catalog'

const hoisted = vi.hoisted(() => ({ flag: vi.fn(async () => true) }))
const mocks = {
...hoisted,
discover: mcpUseCasesMockFns.mockDiscoverMcpServerToolsUseCase,
mcpPermission: permissionCheckMockFns.mockAssertPermissionsAllowed,
build: mothershipChatPayloadMockFns.mockBuildIntegrationToolSchemas,
Expand All @@ -63,7 +61,6 @@ vi.mock('@/lib/mothership/application/workspace-target', () => mothershipWorkspa
vi.mock('@/lib/workspaces/application/context', () => workspaceContextMock)
vi.mock('@/lib/auth/ban', () => authBanMock)
vi.mock('@/lib/permission-groups/resolve.server', () => permissionGroupsResolveMock)
vi.mock('@/lib/mothership/feature-flags', () => ({ isSearchIntegrationToolsEnabled: hoisted.flag }))

const input = { mode: 'assistant' as const, mcpServerIds: [], limit: 20 }
const tools = [
Expand Down Expand Up @@ -100,7 +97,6 @@ function queueChat(mode = 'assistant', role = 'member') {
}
beforeEach(() => {
resetDbChainMock()
mocks.flag.mockResolvedValue(true)
mocks.banned.mockResolvedValue([])
mocks.config.mockResolvedValue(null)
mocks.build.mockResolvedValue([...tools])
Expand Down Expand Up @@ -808,17 +804,6 @@ it('filters organization enabled servers to the authorized target before broad M
expect(discoveredServerIds()).toEqual(['mcp-abc'])
})

it('removes previously discoverable Search operations when the runtime flag turns off', async () => {
for (const enabled of [true, false, true]) {
mocks.flag.mockResolvedValue(enabled)
queueChat()
const result = await readIntegrationCatalog.execute({ principal: principal(), input })
expect(result.operations.map((operation) => operation.toolId)).toEqual(
enabled ? ['gmail_send', 'slack_send'] : []
)
}
})

describe('tagged MCP servers that cannot be discovered', () => {
const failures: Record<string, () => Promise<never> | Promise<{ tools: never[] }>> = {
'mcp-deleted': async () => {
Expand Down
3 changes: 0 additions & 3 deletions apps/sim/lib/mothership/integrations/application/catalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ import { createCopilotChatPrincipal } from '@/lib/mothership/auth/application-de
import { defineAuthorizedChatUseCase } from '@/lib/mothership/chat/application/authorized-chat-use-case'
import { resolveOwnedChatContext } from '@/lib/mothership/chat/application/context'
import { buildIntegrationToolSchemas, type ToolSchema } from '@/lib/mothership/chat/payload'
import { isSearchIntegrationToolsEnabled } from '@/lib/mothership/feature-flags'
import type {
IntegrationCatalogRequest,
IntegrationCatalogResponse,
Expand Down Expand Up @@ -326,8 +325,6 @@ const catalogUseCase = defineAuthorizedChatUseCase({
delegation: { audience: INTEGRATION_CATALOG_AUDIENCE, isWithinScope: () => true },
},
async execute({ input, context }) {
if (context.mode === 'assistant' && !(await isSearchIntegrationToolsEnabled()))
return { total: 0, truncated: false, operations: [] }
if (input.mcpExecution && context.organizationId)
throw new OrchestrationError('forbidden', 'Executor catalogs require workspace agent scope')
let workspaceId = context.workspaceId
Expand Down
Loading
Loading