diff --git a/apps/docs/content/docs/search/confluence.mdx b/apps/docs/content/docs/search/confluence.mdx index 70276ed311f..e2480e8a74d 100644 --- a/apps/docs/content/docs/search/confluence.mdx +++ b/apps/docs/content/docs/search/confluence.mdx @@ -17,10 +17,10 @@ Admin setup uses your organization's **Settings → Sources** page. Teammates co | Method | Who supplies the content? | What teammates do | | --- | --- | --- | -| **Central account** | One account syncs content, space permissions, page restrictions, and group membership. | Connect their own Confluence account so Sim can match their Atlassian identity to those permissions. | +| **Service account** | One service account syncs content, space permissions, page restrictions, and group membership. | Connect their own Confluence account so Sim can match their Atlassian identity to those permissions. | | **Member accounts** | Sim syncs content separately through connected members' accounts. | Connect their own Confluence account to establish which pages they can access. | -Selecting **Add source** on Confluence's integration page opens central account setup. Use this when one account can read the intended spaces and their permissions. For a member source, start **Connect account** from **Integrations** in the main sidebar after an admin allows Confluence. Available methods depend on your organization's enabled features. +Selecting **Add source** on Confluence's integration page opens service-account setup. Use this when a service account can read the intended spaces and their permissions. For a member source, start **Connect** from **Integrations** in the main sidebar after an admin allows Confluence. Available methods depend on your organization's enabled features. **Everyone still connects in both methods.** With a central account, teammates supply their identity; they do not configure another central crawl or choose spaces again. @@ -40,14 +40,14 @@ On hosted Sim, personal connections authorize the existing Sim app. Teammates do ### Choose Confluence -Open **Settings → Sources** and turn on **Confluence** under **Allowed in Sim Search**. Select **Set up** (or **Manage** if sources already exist), then **Add source**. **Setup guide** opens this guide from the source form. +Open **Settings → Sources** and turn on **Confluence**. Select **Set up** (or **Manage** if sources already exist), then **Add source**. **Setup guide** opens this guide from the source form. -### Select an account +### Select a service account -Under **Indexing account**, select an existing account, choose **Connect Confluence account** for OAuth, or add a service account using the [steps below](#using-a-service-account). The account must be able to read the content and its permissions. +Under **Indexing account**, select an existing service account or add one using the [steps below](#using-a-service-account). It must be able to read the content and its permissions. Personal OAuth accounts are used in the member connection flow. @@ -60,9 +60,9 @@ Open **More options** to change **Content Type**, **Filter by Label**, or **Meta Confluence central source setup with an indexing account, domain, and spaces @@ -70,7 +70,7 @@ Open **More options** to change **Content Type**, **Filter by Label**, or **Meta ### Save and connect your identity -Click **Connect & Sync**. Then open **Integrations** in the main sidebar, click **Connect account** on the Confluence source, and finish the connection in the new tab. Sign in using the Atlassian email that matches your verified Sim email, and authorize the configured site. +Click **Connect & Sync**. Then open **Integrations** in the main sidebar, click **Connect** on the Confluence source, and finish the connection in the new tab. Sign in using the Atlassian email that matches your verified Sim email, and authorize the configured site. Each teammate completes this last step. A previously authorized account may already be connected. Return to Integrations to see indexing status and your searchable document count. @@ -79,7 +79,7 @@ Each teammate completes this last step. A previously authorized account may alre ## Connect member accounts -After an admin allows Confluence, open **Integrations** in the main sidebar and select **Connect account**. If there is no source yet, enter **Confluence Domain** and **Space Keys**, then select **Connect** and authorize your account. For another site or space scope, use **Add source** beside **Add another Confluence source**. +After an admin allows Confluence, open **Integrations** in the main sidebar and select **Connect**. If there is no source yet, enter **Confluence Domain** and **Space Keys**, then select **Connect** and authorize your account. For another site or space scope, select **Connect** beside the Confluence row labeled **Connect a different site or content scope**. An admin can open **Settings → Sources**, select **Manage** beside **Confluence**, and open the source's **Settings** tab to adjust its filters. **Account for browsing** helps populate the space picker; it does not connect that account for Search. Manual space keys work without a browsing account. @@ -147,7 +147,7 @@ Under **Indexing account**, choose the service-account connection action. Paste Scopes do not grant access to spaces or pages by themselves. Keep the account's Confluence permissions and its token scopes aligned. When a token expires or needs different scopes, create a replacement in Atlassian. Add the replacement service account in the source's **Settings**, then use **Change indexing account** to apply it. -Personal OAuth uses Sim's shared Confluence integration and requests a broader set of permissions, including writes. Search reads content and permissions; it does not edit your Confluence pages. Older OAuth connections need to reconnect to grant the group-read permission used by central permission syncing. +Personal OAuth uses Sim's shared Confluence integration and requests a broader set of permissions, including writes. Search reads content and permissions; it does not edit your Confluence pages. ## Configuration @@ -164,7 +164,7 @@ Search manages the schedule and hides item limits. Published/current content is ## Teammates and ongoing sync -Existing organization members see the configured Confluence source and their own **Connect account** or **Reconnect** action. Add new teammates through your Sim organization invitation or SSO onboarding, then have them connect Confluence from Integrations. Connecting a Confluence account does not add someone to the Sim organization. +Existing organization members see the configured Confluence source and their own **Connect** or **Reconnect** action. Add new teammates through your Sim organization invitation or SSO onboarding, then have them connect Confluence from Integrations. Connecting a Confluence account does not add someone to the Sim organization. With a central account, Sim applies space access together with the page's restrictions and inherited ancestor restrictions. Group membership is refreshed in the background. With member accounts, each person's provider listing determines the pages available to them. A Sim organization admin does not automatically receive access to every Confluence document. @@ -174,7 +174,7 @@ New content and permission changes require a sync and processing before Search r | What you see | What to check | | --- | --- | -| **Connect & Sync** is disabled | Select a central account, enter the domain, and choose at least one space. | +| **Connect & Sync** is disabled | Select a service account, enter its site domain, and choose at least one space. | | Space picker is empty | Connect an account, enter the correct domain, and verify its space access. You can also switch to manual space keys. | | Service-account validation fails | Check the token's expiry, site, Confluence app access, and scopes. Use a scoped API token from an Atlassian service account. | | Content syncs but central search returns nothing | Connect your personal Confluence identity. Ask the admin to check directory/permission sync errors and group-read scopes. | @@ -190,12 +190,12 @@ On Confluence Premium, **Inspect permissions** can show where a user's access is ## Self-hosted operator setup -Configure one shared Confluence OAuth integration for your deployment. This powers personal identity connections in both Search methods and the optional central OAuth account. +Configure one shared Confluence OAuth integration for your deployment. This powers personal connections in both Search methods. Central indexing uses a service-account token. 1. In the [Atlassian developer console](https://developer.atlassian.com/console/myapps/), select or create your deployment's **OAuth 2.0 integration**. 2. Under **Authorization → OAuth 2.0 (3LO)**, add `https:///api/auth/oauth2/callback/confluence` to **Callback URLs**, keep existing callbacks used by the deployment, and save. 3. Under **Permissions**, add the Confluence API and configure the full `confluence` scope list for your release in [Sim's OAuth configuration](https://github.com/simstudioai/sim/blob/staging/apps/sim/lib/oauth/oauth.ts), including `read:group:confluence`. Also add **User Identity API** with `read:me`. Sim requests `offline_access` for refresh tokens. The service-account read scopes above do not replace the broader shared OAuth scope set. 4. Enable sharing under **Distribution**. Set `CONFLUENCE_CLIENT_ID` and `CONFLUENCE_CLIENT_SECRET` from the app's **Settings**, verify `NEXT_PUBLIC_APP_URL`, and restart Sim. -5. Start authorization from **Integrations** and select the configured site. Reconnect old accounts after adding scopes so the new permission grant takes effect. +5. Start authorization from **Integrations** and select the configured site. After changing the deployment's OAuth client or requested scopes, an organization admin selects **Settings → Sources → Update configurations**, then affected teammates reconnect. A callback mismatch needs a corrected callback URL; a connection that works only for the app owner needs sharing enabled. See Atlassian's [OAuth configuration guide](https://developer.atlassian.com/cloud/confluence/oauth-2-3lo-apps/) and Sim's [deployment reference](/platform/self-hosting/integrations-oauth). diff --git a/apps/docs/content/docs/search/connect-your-account.mdx b/apps/docs/content/docs/search/connect-your-account.mdx index 025f7414568..25e02ec92fe 100644 --- a/apps/docs/content/docs/search/connect-your-account.mdx +++ b/apps/docs/content/docs/search/connect-your-account.mdx @@ -21,23 +21,20 @@ Accept your Sim organization invitation or sign in through your organization's S ## Open Integrations -Open **Integrations** in the main sidebar, find the provider or source, and select **Connect account**. Your first connection may ask for a GitHub repository, Confluence domain and space keys, or Jira domain and project keys. Enter the required fields and select **Connect**. If the provider is missing, ask an organization admin to turn it on under **Settings → Sources → Allowed in Sim Search**. +Open **Integrations** in the main sidebar, find the provider or source, and select **Connect**. Your first connection may ask for a GitHub repository, Confluence domain and space keys, or Jira domain and project keys. Enter the required fields and select **Connect**. If the provider is missing, ask an organization admin to turn it on under **Settings → Sources**. -Use **Add source** beside **Add another [provider] source** when you need another supported repository, site, or project scope. Connecting an existing source does not ask you to configure it again. - -Organization Integrations showing approved providers and Connect account actions +To connect another supported repository, site, or project scope, find the provider row labeled **Connect a different site or content scope** and select **Connect**. Connecting an existing source does not ask you to configure it again. +Organization Integrations with search, personal connections, and Connect actions ## Authorize your account -In the new tab, select **Connect** and complete the provider's authorization. Choose the account associated with your verified Sim email. The provider may require your organization's SSO or app approval. - -Return to Integrations when the connection completes, or select **Return to Search** to open your organization’s Search page. Your account is saved when authorization completes; there is no separate submit step. If the popup was blocked or closed, allow popups and select **Connect account** again. While authorization is pending, use **Open again**. +Complete the provider's authorization in the new tab. Choose the account associated with your verified Sim email. The provider may require your organization's SSO or app approval. -Gmail account connection with Connect and Return to Search actions +The authorization tab closes when the connection completes and Integrations updates. If the tab stays open, return to Integrations. Your account is saved when authorization completes; there is no separate submit step. If the popup was blocked or closed, allow popups and select **Connect** again. While authorization is pending, use **Open again**. @@ -49,7 +46,7 @@ The source row shows indexing status and how many documents are available to you -For a source configured inside a workspace, join that workspace and use its **Search** page instead. Organization and workspace sources are separate. +For a source configured inside a workspace, join that workspace and connect through its **Search** page. To find documents, open **Home** and select **Search** in the composer. Organization and workspace sources are separate. ## Do I always need to connect? @@ -57,36 +54,38 @@ For a source configured inside a workspace, join that workspace and use its **Se | --- | --- | | Member accounts | Connect your own account, including when you are the admin. | | GitHub App installation | Connect GitHub once for this Sim organization. The App handles indexing; your account establishes which repositories you may search. | -| Confluence admin/service account | Connect Confluence to verify your identity; the administrator's account handles the crawl. | +| Confluence service account | Connect Confluence to verify your identity; the service account handles the crawl. | | Google Drive delegated service account | No personal connection is needed for that source. Your verified Sim email is matched to Drive permissions. | -| GitLab instance administrator | No personal connection is needed. Your verified Sim email must match a confirmed GitLab email. | +| GitLab instance administrator | No personal connection is needed. Your verified Sim email must match the confirmed primary GitLab email. | Connecting one Google service does not connect all of them. Gmail, Calendar, and Drive each have their own Search connection. ## If you received a connection request -Open the link from your admin and sign in to Sim with the invited email. A provider-specific request opens that provider's connection directly. Verify your Sim email if prompted, then reopen the original link and authorize the account. +Open the link from your admin and sign in to Sim with the invited email. A provider-specific request opens a Sim connection page for that provider; select **Connect** to start authorization. Verify your Sim email if prompted. Sim returns you to the connection page to authorize your account; if you are not redirected, reopen the original link. -An account connection request does not invite you into the Sim organization. You can contribute an account without organization membership, but you need membership and enabled Search access to search the organization's documents. The connection page offers **Return to Search** when you have that access; otherwise it offers **Your connected accounts**. +An account connection request does not invite you into the Sim organization. You can contribute an account without organization membership, but you need membership and enabled Search access to search the organization's documents. A provider-specific request offers **Return to Search** when you have that access; otherwise it offers **Open Sim**. A request covering several providers lists their connection options and a **Submit** button. Each account is saved as soon as its authorization completes. + +Gmail account connection with Connect and Return to Search actions ## Manage your connected accounts -Select **Your accounts** on the main Integrations page to open your personal **Connected accounts** settings. Use **Reconnect** to renew an organization account connection or **Disconnect** to withdraw it. Disconnecting stops that account from being used for organization indexing and workflows, and removes Search access that depends on it. +On the main **Integrations** page, use **Reconnect** beside an expired connection to renew it. To withdraw an account, open its row's actions menu and select **Disconnect**, then confirm. If several accounts are connected, choose the account to disconnect. Disconnecting stops that account from being used for organization indexing and workflows, and removes Search access that depends on it. -Admins can select **Manage sources** to open organization setup, then select **Manage** beside an integration to open its **Sources** and **Accounts** tabs. This does not grant the admin access to every document. +Admins manage setup from **Settings → Sources**. Select **Manage** beside the integration; Google providers have **Accounts** and **Advanced** tabs, while other providers with personal connections have **Sources** and **Accounts**. This does not grant the admin access to every document. ## If you get stuck | Status | What to do | | --- | --- | -| **Connect account** | Complete the connection in the new tab. | +| **Connect** | Complete the connection in the new tab. | | **Reconnect** | Authorize the same source account again. | | **Finish connecting in the other tab** | Finish authorization, or use **Open again**. Allow popups for Sim. | | No results | Check the source's filters and sync status with your admin. Confirm you can open the document at the source. | -| **Verify email** | Verify your Sim email, then reopen the connection link. | +| **Verify email** | Verify your Sim email to return to the connection page. Reopen the original link if you are not redirected. | | Expired or cancelled authorization | Return to the original connection page and start again. If the invitation itself expired, ask the admin for a new request. | | Access revoked | Ask the organization admin to restore your account contribution access before reconnecting. | -| Needs admin attention | Ask your admin to open **Settings → Sources**, select **Manage** beside the integration, and open the source to inspect its error. | +| Needs admin attention | Ask your admin to open **Settings → Sources**, select **Manage** beside the integration, and open the source from **Advanced** (Google providers) or **Sources** to inspect its error. | Your Sim role does not override document access at the source. Connecting a different account or receiving a Search link does not share someone else's mailbox, private calendar, or restricted documents with you. diff --git a/apps/docs/content/docs/search/github.mdx b/apps/docs/content/docs/search/github.mdx index c4e661e1c7f..cf3f2858cc9 100644 --- a/apps/docs/content/docs/search/github.mdx +++ b/apps/docs/content/docs/search/github.mdx @@ -25,7 +25,7 @@ To connect an installation for central indexing, you must be a Sim organization ### Open GitHub setup -Open **Settings → Sources** and turn on **GitHub** under **Allowed in Sim Search**. Select **Set up** (or **Manage** if sources already exist), then **Add source**. +Open **Settings → Sources** and turn on **GitHub**. Select **Set up** (or **Manage** if sources already exist), then **Add source**. @@ -65,13 +65,13 @@ Open **More options** if you need a different branch, path or extension filters, ### Connect your account -Open **Integrations** in the main sidebar, select **Connect account** on the GitHub source, and authorize the App. Teammates repeat this step after joining the Sim organization. For private repositories, both the person's account and the App installation must have access. GitHub also permits App user tokens to read public repositories without an installation; see [GitHub's permission rules](https://docs.github.com/en/apps/creating-github-apps/registering-a-github-app/choosing-permissions-for-a-github-app). +Open **Integrations** in the main sidebar, select **Connect** on the GitHub source, and authorize the App. Teammates repeat this step after joining the Sim organization. For private repositories, both the person's account and the App installation must have access. GitHub also permits App user tokens to read public repositories without an installation; see [GitHub's permission rules](https://docs.github.com/en/apps/creating-github-apps/registering-a-github-app/choosing-permissions-for-a-github-app). An App installation or dedicated indexing account can start syncing after the source is saved. With **Connected members**, indexing begins after someone connects. Each teammate still connects before searching. An existing GitHub connection in the same Sim organization is reused across its GitHub sources. Open **Settings → Sources**, select **Manage** beside **GitHub**, then open the source to inspect **Documents**, edit **Settings**, or review **Sync history**. -Use **GitHub → Accounts → Request connections** to send provider-specific connection requests. These requests do not grant organization membership. For another repository, add another source; members can also use **Add another GitHub source** in the main Integrations page. +Use **GitHub → Accounts → Request connections** to send provider-specific connection requests. These requests do not grant organization membership. For another repository, add another source; members can also select **Connect** beside the GitHub row labeled **Connect a different site or content scope** in Integrations. @@ -94,7 +94,7 @@ This is an installation plus personal authorization flow. GitHub Search does not | No eligible installations found | Finish connecting your own GitHub account, install the configured App on your account or an organization you own, then select **Refresh**. An installation of a different App or one you only have repository access to cannot be selected. | | Repository is not accepted for an installation | Check `owner/repo`, the installation's account and repository selection, and your own access. Update the source's Repository field after a rename. After a transfer, add a source using an installation for the new owner. | | Identity verification fails | Verify the email used by your Sim account in GitHub's email settings, then reconnect. A public profile email alone is insufficient. | -| Authorization fails after installation | Return to Sim and start **Connect account** there. Do not enable authorization during installation. | +| Authorization fails after installation | Return to Sim and start **Connect** there. Do not enable authorization during installation. | | Account authorization did not complete | Start the connection again from Sim. If it repeats, contact your organization admin or Sim support. For self-hosted Sim, check the [App callback and credentials](/platform/self-hosting/integrations-oauth#github-search). | | Update GitHub using Update configurations in organization settings before connecting this source | An organization admin must select **Settings → Sources → Update configurations**, then reconnect GitHub. | | Indexed files no longer appear | Confirm your own repository access, App repository selection, and connection status. Installation-indexed content is also withheld when GitHub cannot verify current access; retry once GitHub is available. | diff --git a/apps/docs/content/docs/search/gitlab.mdx b/apps/docs/content/docs/search/gitlab.mdx index 69789c73e36..39fb464de65 100644 --- a/apps/docs/content/docs/search/gitlab.mdx +++ b/apps/docs/content/docs/search/gitlab.mdx @@ -7,7 +7,7 @@ import { Callout } from 'fumadocs-ui/components/callout' import { Step, Steps } from 'fumadocs-ui/components/steps' import { Image } from '@/components/ui/image' -GitLab Search uses an administrator connection to sync a project's content and permissions. Teammates do not connect individual GitLab accounts. They sign in to the Sim organization with a verified email matching their confirmed GitLab email. +GitLab Search uses an administrator connection to sync a project's content and permissions. Teammates do not connect individual GitLab accounts. They sign in to the Sim organization with a verified email matching their confirmed primary GitLab email. GitLab source setup in Sim Search @@ -49,7 +49,7 @@ The token must read the project, users, inherited project membership, instance s ### Configure the source in Sim -Open **Settings → Sources** and turn on **GitLab** under **Allowed in Sim Search**. Select **Set up** (or **Manage** if sources already exist), then **Add source**. Paste the **Personal Access Token**, enter your **Host** and **Project**, and choose the content to index. +Open **Settings → Sources** and turn on **GitLab**. Select **Set up** (or **Manage** if sources already exist), then **Add source**. Paste the **Personal Access Token**, enter your **Host** and **Project**, and choose the content to index. | Field | What to enter | |---|---| @@ -59,7 +59,7 @@ Open **Settings → Sources** and turn on **GitLab** under **Allowed in Sim Sear | Branch | Optional branch or tag for repository files; blank uses the project's default branch. | | Path Filter / File Extensions | Optional limits for repository files. | | Issue State / Labels / Milestone | Optional filters for issues. | -| Max Items | Optional positive limit. Leave blank for all matching items. | +| Max Items | Optional positive whole-number limit. Leave blank for all matching items. | **More options** contains the repository and issue filters, **Max Items**, and **Metadata tags**. Unlike member-account sources, GitLab retains this optional item limit. @@ -72,7 +72,7 @@ Select **Connect & Sync**. Sim validates the token and source policy, then start ### Let teammates search -Invite teammates to the Sim organization using their verified work email. Sim matches that email against the GitLab directory and applies project, feature, and confidential-issue permissions. No GitLab **Connect account** step is required. +Invite teammates to the Sim organization using their verified work email. Sim matches that email against their confirmed primary GitLab email and applies project, feature, and confidential-issue permissions. Confirmed secondary addresses are not matched. No GitLab **Connect** step is required. Admins open **Settings → Sources**, select **Manage** beside **GitLab**, then open the source to inspect **Documents**, edit **Settings**, or review **Sync history**. GitLab has no personal **Accounts** tab. Permission and membership changes are picked up during background refreshes. @@ -90,7 +90,7 @@ The connector supports text repository files, wiki pages, issues, merge requests | Administrator token required | Use an active instance administrator's PAT with `read_api`, plus `admin_mode` when required. A project or group token cannot replace it. | | Source permissions cannot be mirrored | Read the reported policy. Sim rejects unsupported external authorization, IP restrictions, download-ban policies, or session-specific step-up requirements. | | Project not found | Check the host, project path or ID, and token access. | -| A teammate sees no results | Confirm both accounts' verified/confirmed email addresses match and the user has the required GitLab project or feature access. | +| A teammate sees no results | Confirm their verified Sim email matches their confirmed primary GitLab email and the user has the required GitLab project or feature access. | | Token expired | Remove and add the source again with a new token. This connector does not support replacing its token in place or refreshing PATs automatically. | Custom GitLab roles may grant more access than Sim's conservative role mapping recognizes. A source requiring unsupported policies must remain unavailable until its access model can be represented accurately. diff --git a/apps/docs/content/docs/search/gmail.mdx b/apps/docs/content/docs/search/gmail.mdx index 1af4d9a514d..94fd942d937 100644 --- a/apps/docs/content/docs/search/gmail.mdx +++ b/apps/docs/content/docs/search/gmail.mdx @@ -11,30 +11,34 @@ Search email threads from your own Gmail account. An organization admin enables Admin setup uses your organization's **Settings → Sources** page. Teammates connect from **Integrations** in the main sidebar. For workspace Search, use **Search → Add source** instead; **Create & Invite** is the workspace equivalent of **Add source**. -## Set up the source - -These steps require a Sim organization admin. +## Set up Gmail -### Set up Gmail +### Allow Gmail -Open **Settings → Sources** and turn on **Gmail** under **Allowed in Sim Search**. Select **Set up** (or **Manage** if sources already exist), then **Add source**. Gmail uses member accounts; there is no domain-wide or service-account crawl in Search. +Open **Settings → Sources** and turn on **Gmail**. Gmail uses member accounts; there is no domain-wide or service-account crawl in Search. -### Choose what to include +### Connect your account -Keep the defaults to search all dates and labels, excluding Promotions, Social, Spam, and Trash. **Labels** and **Date Range** are shown first. Open **More options** for category exclusions, **Search Filter**, and **Metadata tags**. +Open **Integrations** and select **Connect** beside Gmail. Authorize the Google account matching your verified Sim email. The first connection creates the default sync configuration: all dates and labels, excluding Promotions, Social, Spam, and Trash. -### Create the source +### Adjust filters if needed + +An admin selects **Manage** beside Gmail in **Settings → Sources**, opens **Advanced**, then selects the configuration's **Settings** tab. Change **Labels**, **Date Range**, or other filters and save. + +To create a separate configuration, use **Add sync configuration** on **Advanced**. Its form shows **Labels** and **Date Range** first; **More options** contains category exclusions, **Search Filter**, and **Metadata tags**. Select **Add source** to save. This does not connect accounts or invite people. + +One configuration is usually enough. Adding another creates a separate source; editing **Settings** updates the selected one. Every configuration applies to all active Gmail connections, including accounts connected later. It does not assign different filters to selected people or let teammates search each other's mail. -Click **Add source**. Gmail appears in the provider's **Sources** list. Each person, including the admin, then connects their own account from **Integrations** in the main sidebar. To send a Gmail connection request, open **Gmail → Accounts → Request connections**; this does not invite the recipient to the Sim organization. +Configurations are additive: a narrower one does not restrict an existing broader one, and overlapping configurations can index the same thread more than once. For one organization-wide policy, edit the existing configuration. @@ -43,15 +47,15 @@ Click **Add source**. Gmail appears in the provider's **Sources** list. Each per ## Connect your account -1. Join the Sim organization and verify your Sim email address. Open **Integrations** and click **Connect account** beside Gmail. +1. Join the Sim organization and verify your Sim email address. Open **Integrations** and click **Connect** beside Gmail. 2. Complete the connection in the tab that opens. Choose the Google account whose verified email matches your Sim email, and grant the requested permissions. 3. Return to Integrations. The source shows its indexing status and the number of documents you can search. -Teammates follow these same steps after joining the organization. Once an admin approves Gmail, the first connection can create its source with default filters. Admins can configure shared filters beforehand. +Teammates follow these same steps after joining the organization. Once an admin approves Gmail, the first connection can create its source with default filters. Admins can configure shared filters beforehand or request connections from **Manage → Accounts → Request connections**. A connection request does not invite the recipient to the Sim organization. ## Source options -An admin opens **Settings → Sources**, selects **Manage** beside **Gmail**, and opens the source's **Settings** tab to change these options. Filters apply separately to each connected mailbox. **Documents** shows indexed threads and **Sync history** shows recent runs. +An admin opens **Settings → Sources**, selects **Manage** beside **Gmail**, opens **Advanced**, and selects the configuration's **Settings** tab to change these options. Filters apply separately to each connected mailbox. **Documents** shows indexed threads and **Sync history** shows recent runs. | Option | Behavior | | --- | --- | @@ -70,24 +74,30 @@ File attachments and image contents are not indexed. Thread discovery uses Gmail Search schedules syncs hourly. The first sync and large mailboxes can take longer; results appear as documents are indexed. Updates and removals are reconciled during background sync, rather than fetched live for each search. +An empty mailbox or filters with no matching threads complete normally with zero documents. + +Threads that exceed indexing size limits are skipped and reconsidered when the thread changes. + ## Troubleshooting | What you see | What to do | | --- | --- | | A different email is requested | Use the Google account matching your verified Sim email. A separate personal account or alias does not satisfy the match. | | No searchable documents | Check the source's labels, date range, category exclusions, and search filter. Allow the first sync to finish. | -| Finish connecting in the other tab | Complete the Google flow, or use **Open again** while authorization is pending. If the popup was blocked or closed, allow popups and select **Connect account** again. | +| Finish connecting in the other tab | Complete the Google flow, or use **Open again** while authorization is pending. If the popup was blocked or closed, allow popups and select **Connect** again. | | Reconnect | Click **Reconnect** and authorize the same account again. | | Unavailable or needs admin attention | Ask your Sim admin to check source status and the deployment's Google OAuth configuration. | ## Self-hosted operator setup +For an External app in **Testing**, Google refresh tokens for these scopes expire after seven days. Before production use, configure the appropriate publishing status and complete any required verification; adding test users alone does not make a durable production connection. See [Google’s token expiration rules](https://developers.google.com/identity/protocols/oauth2#expiration). + Users do not need to create Google Cloud credentials. The deployment operator configures one Google OAuth client for the instance: 1. In [Google Cloud Console](https://console.cloud.google.com/), select your project. Open **APIs & Services → Library**, find **Gmail API**, and enable it. 2. Open **Google Auth platform → Branding**. Select **Get started** if needed, then enter the app name, support email, and contact email. Under **Audience**, use **Internal** only for an app limited to your Google Workspace organization; otherwise use **External** and add test users while testing. Review the app's permissions under **Data Access → Add or remove scopes**, using the current Sim scopes below. Follow Google's [consent and verification guidance](https://developers.google.com/workspace/guides/configure-oauth-consent) for your audience. 3. Open **Google Auth platform → Clients → Create client**. Choose **Web application**, give the client a name, and add the URI below under **Authorized redirect URIs**. If this instance already has a Google client, add this URI to that client instead. See [Google's credential setup](https://developers.google.com/workspace/guides/create-credentials#web-application). -4. Save the client ID and secret as `GOOGLE_CLIENT_ID` and `GOOGLE_CLIENT_SECRET`. Set `NEXT_PUBLIC_APP_URL` to the same Sim origin used in the callback, then restart Sim. See [Integrations & OAuth](/platform/self-hosting/integrations-oauth). +4. Save the client ID and secret as `GOOGLE_CLIENT_ID` and `GOOGLE_CLIENT_SECRET`. Set `NEXT_PUBLIC_APP_URL` to the same Sim origin used in the callback, then restart Sim. See [Integrations & OAuth](/platform/self-hosting/integrations-oauth). If you change an existing deployment's OAuth client or scopes, an organization admin selects **Settings → Sources → Update configurations**, then affected teammates reconnect. ```text https:///api/auth/oauth2/callback/google-email diff --git a/apps/docs/content/docs/search/google-calendar.mdx b/apps/docs/content/docs/search/google-calendar.mdx index fecad2266eb..60a676b3f45 100644 --- a/apps/docs/content/docs/search/google-calendar.mdx +++ b/apps/docs/content/docs/search/google-calendar.mdx @@ -11,32 +11,30 @@ Search meetings and event details available to your Google account. An organizat Admin setup uses your organization's **Settings → Sources** page. Teammates connect from **Integrations** in the main sidebar. For workspace Search, use **Search → Add source** instead; **Create & Invite** is the workspace equivalent of **Add source**. -## Set up the source - -These steps require a Sim organization admin. +## Set up Google Calendar -### Set up Google Calendar +### Allow and connect Google Calendar -Open **Settings → Sources** and turn on **Google Calendar** under **Allowed in Sim Search**. Select **Set up** (or **Manage** if sources already exist), then **Add source**. Search uses member accounts; an admin or service account cannot connect on behalf of everyone. +An admin opens **Settings → Sources** and turns on **Google Calendar**. Then each person opens **Integrations**, selects **Connect** beside Google Calendar, and authorizes their matching Google account. The first connection creates the default sync configuration. Search uses member accounts; an admin or service account cannot connect on behalf of everyone. -### Choose the calendars +### Choose calendars if needed -Leave **Calendars** empty to search each person's primary calendar. To include specific shared calendars, select an **Account for browsing** and choose calendars, or switch to **Calendar IDs** and enter their IDs. +An admin selects **Manage** beside Google Calendar in **Settings → Sources**, opens **Advanced**, and selects the configuration's **Settings** tab. Leave **Calendars** empty to search each person's primary calendar. To include specific shared calendars, select an **Account for browsing** and choose calendars, or switch to **Calendar IDs** and enter their IDs. **Account for browsing** only helps you choose calendars. It does not connect your account for Search or grant teammates access. -### Create the source +### Save or add a configuration -Keep the default date range for the previous and next 30 days. **More options** contains **Search Query**, **Include Attendees**, and **Metadata tags**. Click **Add source**, then connect your own account from **Integrations** in the main sidebar. +The default date range covers the previous and next 30 days. Save any changes to the existing configuration. To create a separate one, select **Add sync configuration** on **Advanced**. In that form, **More options** contains **Search Query**, **Include Attendees**, and **Metadata tags**. Select **Add source** to save it; teammates connect their own accounts from Integrations. @@ -49,7 +47,7 @@ Keep the default date range for the previous and next 30 days. **More options** ## Connect your account -1. Join the Sim organization and verify your Sim email. Open **Integrations** and click **Connect account** beside Google Calendar. +1. Join the Sim organization and verify your Sim email. Open **Integrations** and click **Connect** beside Google Calendar. 2. In the connection tab, choose the Google account whose verified email matches your Sim email. Grant the requested permissions. 3. Return to Integrations to see indexing status and your searchable document count. @@ -57,7 +55,7 @@ Teammates repeat only these connection steps after joining the organization. The ## Source options -An admin opens **Settings → Sources**, selects **Manage** beside **Google Calendar**, and opens the source's **Settings** tab to change these options. **Documents** shows indexed events and **Sync history** shows recent runs. +An admin opens **Settings → Sources**, selects **Manage** beside **Google Calendar**, opens **Advanced**, and selects the configuration's **Settings** tab to change these options. **Documents** shows indexed events and **Sync history** shows recent runs. | Option | Behavior | | --- | --- | @@ -89,12 +87,14 @@ Search schedules syncs hourly. Event edits, cancellations, access changes, and e ## Self-hosted operator setup +For an External app in **Testing**, Google refresh tokens for these scopes expire after seven days. Before production use, configure the appropriate publishing status and complete any required verification; adding test users alone does not make a durable production connection. See [Google’s token expiration rules](https://developers.google.com/identity/protocols/oauth2#expiration). + The deployment operator configures Google OAuth once; teammates then use the normal connection flow. 1. In [Google Cloud Console](https://console.cloud.google.com/), select your project. Open **APIs & Services → Library**, find **Google Calendar API**, and enable it. 2. Open **Google Auth platform → Branding** and configure the app name and contact details. Under **Audience**, choose **Internal** for your Google Workspace organization only, or **External** for other users. Add test users while an external app is testing. Review **Data Access → Add or remove scopes** using the current Sim scopes below. See Google's [consent and verification guidance](https://developers.google.com/workspace/guides/configure-oauth-consent). 3. Open **Google Auth platform → Clients → Create client**, choose **Web application**, and add the URI below under **Authorized redirect URIs**. Add it to the existing Google client if the instance already uses one. See [Google's credential setup](https://developers.google.com/workspace/guides/create-credentials#web-application). -4. Save the client ID and secret as `GOOGLE_CLIENT_ID` and `GOOGLE_CLIENT_SECRET`. Set `NEXT_PUBLIC_APP_URL` to the same Sim origin used in the callback, then restart Sim. See [Integrations & OAuth](/platform/self-hosting/integrations-oauth). +4. Save the client ID and secret as `GOOGLE_CLIENT_ID` and `GOOGLE_CLIENT_SECRET`. Set `NEXT_PUBLIC_APP_URL` to the same Sim origin used in the callback, then restart Sim. See [Integrations & OAuth](/platform/self-hosting/integrations-oauth). If you change an existing deployment's OAuth client or scopes, an organization admin selects **Settings → Sources → Update configurations**, then affected teammates reconnect. ```text https:///api/auth/oauth2/callback/google-calendar diff --git a/apps/docs/content/docs/search/google-drive.mdx b/apps/docs/content/docs/search/google-drive.mdx index 3f68cebe7c7..d502865f1d4 100644 --- a/apps/docs/content/docs/search/google-drive.mdx +++ b/apps/docs/content/docs/search/google-drive.mdx @@ -15,7 +15,7 @@ Admin setup uses your organization's **Settings → Sources** page. Teammates co | Method | Use it when | What teammates do | | --- | --- | --- | -| **Member accounts** | Each person should connect their own Drive access. No Google Workspace administrator setup is needed. | Connect their own Google Drive accounts after the source is created. | +| **Member accounts** | Each person should connect their own Drive access. No Google Workspace administrator setup is needed. | Connect their own Google Drive accounts. | | **Service account** | A Google Workspace administrator can configure delegation and directory access for a central crawl. | Sign in to Sim with matching verified email addresses; no personal Drive connection is needed for this source. | @@ -29,21 +29,21 @@ Admin setup uses your organization's **Settings → Sources** page. Teammates co ### Allow Google Drive -An organization admin opens **Settings → Sources** and turns on **Google Drive** under **Allowed in Sim Search**. This allows personal connections; it does not create a source or connect anyone's account. +An organization admin opens **Settings → Sources** and turns on **Google Drive**. This allows personal connections; it does not create a source or connect anyone's account. ### Connect your account -Open **Integrations** in the main sidebar and select **Connect account** beside Google Drive. Use the Google account matching your verified Sim email. The first personal connection can create a source with default filters. Teammates follow the same [connection steps](/search/connect-your-account). +Open **Integrations** in the main sidebar and select **Connect** beside Google Drive. Use the Google account matching your verified Sim email. The first personal connection can create a source with default filters. Teammates follow the same [connection steps](/search/connect-your-account). ### Adjust filters if needed -An admin opens **Settings → Sources**, selects **Manage** beside **Google Drive**, and opens the source's **Settings** tab. Leave **Folders** empty to include supported files each member can access, or narrow the source to folders. **Account for browsing** helps select folders; manual **Folder IDs** work without it. Browsing does not connect that account to Search. +An admin opens **Settings → Sources**, selects **Manage** beside **Google Drive**, opens **Advanced**, and selects the sync configuration's **Settings** tab. Leave **Folders** empty to include supported files each member can access, or narrow the source to folders. **Account for browsing** helps select folders; manual **Folder IDs** work without it. Browsing does not connect that account to Search. Keep **Sync documents with → Connected members** unless a dedicated account should fetch content. Members still connect to establish access. If the dedicated account is a delegated service account, **Crawl as** selects the Google Workspace user whose files it fetches. Save the source settings when finished. @@ -52,7 +52,7 @@ Keep **Sync documents with → Connected members** unless a dedicated account sh ## Set up a central service account -Open **Settings → Sources** and turn on **Google Drive** under **Allowed in Sim Search**. Select **Set up** (or **Manage** if sources already exist), then **Add source**. This opens central service-account setup. Teammates do not need a personal Drive connection for this source. +Open **Settings → Sources** and turn on **Google Drive**. Select **Manage → Advanced → Add sync configuration**. This opens central service-account setup. If your organization has only central indexing enabled, select **Add source** from the provider's **Sources** tab instead. Teammates do not need a personal Drive connection for this source. This requires a Google Workspace domain and a Workspace super administrator to authorize domain-wide delegation. Consumer Gmail accounts cannot use this path. @@ -90,6 +90,8 @@ Select **Authorize**, then **View details** to confirm all three scopes were sav These are Search's central crawl scopes. The general [Google service account guide](/integrations/google-service-account) includes broader scopes for workflow actions; do not copy those into this Search setup. +Group permissions require groups and memberships that the indexing administrator can read in this Google Workspace customer. External groups and unresolvable nested groups are not supported. Google Drive target-audience shares are not mapped; use explicit user, supported group, or domain permissions instead. + @@ -121,14 +123,13 @@ Set **Crawl as** to a Google Workspace administrator who can read groups, member Sim exports Docs and Slides as text and Sheets as XLSX spreadsheets. Supported uploaded files use the knowledge-base document pipeline, including PDF and Office formats. Unsupported files and oversized exports cannot be indexed; Google limits Workspace exports to 10 MB. See [Drive export formats](https://developers.google.com/workspace/drive/api/guides/ref-export-formats) and [download limits](https://developers.google.com/workspace/drive/api/guides/manage-downloads). -Search schedules syncs hourly. Content, deletions, and permissions refresh in the background; results are not a live read from Drive. Open **Settings → Sources**, select **Manage** beside **Google Drive**, then open the source to inspect **Documents**, edit **Settings**, or review **Sync history**. **Accounts** on the provider page shows personal account connections where configured; it does not list the central service-account credential. +Search schedules syncs hourly. Content, deletions, and permissions refresh in the background; results are not a live read from Drive. Open **Settings → Sources**, select **Manage** beside **Google Drive**, then open its configuration under **Advanced** to inspect **Documents**, edit **Settings**, or review **Sync history**. **Accounts** on the provider page shows personal account connections where configured; it does not list the central service-account credential. ## Troubleshooting | Problem | Next step | | --- | --- | | Directory access failed | Check the delegated scopes and the **Crawl as** user's administrator privileges. A normal Google OAuth credential cannot supply this central Search path. | -| An existing central source uses a normal Google OAuth account | Open the source's **Settings** tab, select or add a delegated service account, and choose **Change indexing account**. If the source is **Paused** or **Disabled**, choose **Resume** after updating the account. | | Missing files in a central crawl | Open them as the **Crawl as** user. Delegation does not grant that user access to all domain files. Check folder and file-type filters. | | A teammate sees no results | Confirm their verified Sim email matches the Drive permission or group membership. For member accounts, finish their personal Drive connection too. | | A public or shared-link file is missing | Check **Openly shared files**. Link-only sharing does not grant Search access. A named user or group permission can still make the file searchable. | @@ -150,7 +151,7 @@ https:///api/auth/oauth2/callback/google-drive This Google Cloud example uses one client for all three services. Replace `https://sim.example.com` with your Sim origin and add only the callbacks for services you enable. -Save the client ID and secret as `GOOGLE_CLIENT_ID` and `GOOGLE_CLIENT_SECRET`. Set `NEXT_PUBLIC_APP_URL` to the same Sim origin used in the callback, then restart Sim. See [Integrations & OAuth](/platform/self-hosting/integrations-oauth). +Save the client ID and secret as `GOOGLE_CLIENT_ID` and `GOOGLE_CLIENT_SECRET`. Set `NEXT_PUBLIC_APP_URL` to the same Sim origin used in the callback, then restart Sim. See [Integrations & OAuth](/platform/self-hosting/integrations-oauth). If you change an existing deployment's OAuth client or scopes, an organization admin selects **Settings → Sources → Update configurations**, then affected teammates reconnect. The current Sim Drive OAuth connection uses these scopes: diff --git a/apps/docs/content/docs/search/index.mdx b/apps/docs/content/docs/search/index.mdx index cfbd2812709..6119f99440b 100644 --- a/apps/docs/content/docs/search/index.mdx +++ b/apps/docs/content/docs/search/index.mdx @@ -16,45 +16,47 @@ Search brings your connected sources into one place. An organization admin allow ### Allow an integration -As an organization admin, open **Settings → Sources** and turn on the integration under **Allowed in Sim Search**. It stays in the list; no setup page opens automatically. +As an organization admin, open **Settings → Sources** and turn on the integration. It stays in the list; no setup page opens automatically. The switch permits the integration in your organization. It does not connect an account, grant document access, or start indexing. Teammates connect from **Integrations** in the main sidebar; only admins manage these switches. -### Configure it once +### Connect or configure the source -Select **Set up** beside the integration, or **Manage** if it already has sources. On its page, select **Add source**. For Slack, complete **Set up Slack app** first. Follow its **Setup guide** to connect the indexing account or choose the folders, repositories, calendars, spaces, or channels to include. **More options** contains secondary filters and **Metadata tags**. +For personal Gmail, Calendar, or Drive, teammates can connect immediately from **Integrations**. The first account creates the default sync configuration. Admins can adjust it later under **Manage → Advanced** on the provider page. -Select **Connect & Sync** for an administrator connection, or **Add source** for member accounts. Creating a source does not invite people or authorize their accounts. +For other sources, select **Set up** or **Manage**, then **Add source**. For a central Drive service account, use **Manage → Advanced → Add sync configuration**. For Slack, complete **Set up Slack app** first. Follow the provider's **Setup guide** to choose the indexing account and content. **More options** contains secondary filters and **Metadata tags**. + +In the source form, select **Connect & Sync** for a central account or **Add source** for member accounts. Creating a source does not invite people or authorize their accounts. ### Connect and search -Open **Integrations** in the main sidebar and select **Connect account** if prompted—even if you created the source. Complete authorization in the new tab. Open **Search** to find documents, or **Home** to ask the assistant about them. Documents become available as background indexing progresses. +Open **Integrations** in the main sidebar and select **Connect** if prompted—even if you created the source. Complete authorization in the new tab. Open **Search** to find documents, or **Home** to ask the assistant about them. Documents become available as background indexing progresses. -Organization Sources settings with Allowed in Sim Search switches and separate Set up and Manage actions +Organization Sources settings with provider switches, Set up or Manage actions, and Update configurations -Source availability depends on the deployment and organization policy. An unavailable source needs operator configuration before setup can continue. +Source availability depends on the deployment and organization policy. An unavailable source needs operator configuration before setup can continue. If an existing provider asks for a configuration update, an admin selects **Update configurations** on this page, then affected teammates reconnect. ## Choose the right connection method -Most sources use member accounts. GitHub supports a central App installation with a personal connection for each reader. Google Drive and Confluence also support a central administrator connection; GitLab requires an administrator token for a self-managed instance. +Most sources use member accounts. GitHub supports a central App installation with a personal connection for each reader. Google Drive and Confluence also support a central service-account connection; GitLab requires an administrator token for a self-managed instance. | Method | What the admin does | What teammates do | | --- | --- | --- | -| **Member accounts** | Sets the source's filters once. | Connect their own accounts. Sim lists documents using each member's access. | +| **Member accounts** | Allows the provider and adjusts shared filters when needed. | Connect their own accounts. Sim lists documents using each member's access. | | **GitHub App installation** | Installs the App, selects it under **Sync documents with**, and adds repository sources. | Connect GitHub once. Sim checks each reader's current repository access before returning installation-indexed content. | -| **Service account** (Drive) / **Admin or service account** (Confluence) | Connects an account that can read the content and the source's permissions or directory. | Join the organization with a matching verified identity. Confluence also requires each person to connect their account. | +| **Service account** (Drive and Confluence) | Connects an account that can read the content and the source's permissions or directory. | Join the organization with a matching verified identity. Confluence also requires each person to connect their account. | | **Administrator token** (GitLab) | Connects a self-managed instance administrator token and selects projects to index. | Join the organization with a verified Sim email matching GitLab. No personal connection is needed. | -Adding a Google Drive or Confluence source from the admin page starts central setup. For personal connections, use **Integrations → Connect account** in the main sidebar. An approved provider can create its first member source there; required repository, site, or project fields are collected before authorization. Admins can edit that source's filters afterward in its **Settings** tab. +Adding a Google Drive or Confluence source from the admin page starts central setup. Drive uses **Manage → Advanced → Add sync configuration**; Confluence uses **Set up/Manage → Add source**. For personal connections, use **Integrations → Connect** in the main sidebar. An approved provider can create its first member source there; required repository, site, or project fields are collected before authorization. Admins can edit that source's filters afterward in its **Settings** tab. When personal connections are disabled for the organization, central Drive uses **Sources → Add source** instead of **Advanced**. Some member sources offer **Sync documents with**, either directly in setup or under **More options**. **Connected members** uses members' accounts for both content and access checks. Selecting a dedicated account uses it to fetch content; members still connect to establish which documents they may search. For GitHub organization sources, choose **Connect GitHub App** in this field to [connect an installation](/search/github#add-a-repository). **Account for browsing** only helps an admin pick source options—it does not enroll that account for Search. @@ -66,7 +68,7 @@ Some member sources offer **Sync documents with**, either directly in setup or u | Source | Content | Connection in Search | | --- | --- | --- | -| [Confluence](/search/confluence) | Pages and blog posts | Admin/service account or member accounts; each teammate connects | +| [Confluence](/search/confluence) | Pages and blog posts | Service account or member accounts; each teammate connects | | [GitHub](/search/github) | Repository text files | App installation or member indexing; each teammate connects | | [GitLab](/search/gitlab) | Repository files, wikis, issues, merge requests | Self-managed instance administrator token; no member connection | | [Gmail](/search/gmail) | Email thread text | Each member's Gmail account | @@ -89,39 +91,31 @@ These requests are separate from organization invitations. They let recipients c ## Manage sources and documents -Open **Settings → Sources**, select **Manage** beside the integration, then select a source. Providers with personal connections have **Sources** and **Accounts** tabs; providers such as GitLab open directly to the source list. Multiple sources can have different folder, repository, space, or project scopes. +Open **Settings → Sources** and select **Manage** beside the integration. Gmail, Calendar, and Drive list their sync configurations under **Advanced** and personal connections under **Accounts**. Other providers with personal connections use **Sources** and **Accounts**; GitLab opens directly to the source list. Select a source or sync configuration to manage it. Multiple sources can have different folder, repository, space, or project scopes. + +For Gmail, one configuration is usually enough. **Add sync configuration** creates another source; editing **Settings** updates the selected source. Each configuration applies to all connected Gmail accounts, including accounts connected later, using each person's own mailbox permissions. Configurations are not assigned to individual people. -Integration detail with Sources and Accounts tabs and a nested source list +Gmail Advanced tab with a sync configuration, search, and Add sync configuration action | Source tab | What you can do | | --- | --- | | **Documents** | Find indexed documents, inspect processing status, retry failed indexing, or exclude and restore documents. | -| **Settings** | Edit the source's scope, filters, and supported connection settings. Save your changes before leaving. | -| **Sync history** | Review recent sync runs and errors. | +| **Settings** | Edit the source's scope, filters, and supported indexing credentials. Save your changes before leaving. | +| **Sync history** | Review run dates, document changes, and any sync or account errors. | -Use the source header to sync, pause, resume, or remove that source. The back link returns to its integration. To disable an entire integration, turn off its **Allowed in Sim Search** switch. If it has sources, confirm **Deactivate**. Its content becomes unavailable in Search, Assistant, and MCP; sources and connected accounts are preserved. Turn the switch back on to allow it again. +**Sync using** shows the method selected when the source was created. Create a new source to change that method. To replace a supported indexing credential, select its replacement and use **Change indexing account**. -Gmail source Settings tab with label, date range, and search filters +Use the source header to sync, pause, resume, or remove that source. The back link returns to its integration. To disable an entire integration, turn off its switch in **Settings → Sources**. If it has sources, confirm **Deactivate**. Its content becomes unavailable in Search, Assistant, and MCP; sources and connected accounts are preserved. Turn the switch back on to allow it again. -Source Sync history with no connected accounts or synced members +Gmail Settings with the read-only Sync using method and editable label, date range, and search filters + +Gmail Sync history showing run dates and document additions, deletions, or no changes ## Search, Assistant, and MCP **Search** in the organization sidebar finds documents directly. The assistant on **Home** can search and read the same sources to answer questions with citations. Conversations are private to their author, including when another organization member is an admin. -To search from Claude, Codex, Claude Code, or Cursor, open **Settings → Search MCP**, choose your app, and copy its URL, command, or configuration. Connect in that app, sign in to Sim, and approve read-only Search access. No API key is needed. In Claude Team or Enterprise, an owner adds the custom connector before members connect. - -For another client, choose **Other** and use the server URL with Streamable HTTP and OAuth. The client must support remote MCP authentication. When adding configuration to an existing file, keep your other MCP servers. - -Each person signs in with their own Sim account. MCP applies their current organization membership and document access; connecting an app does not add sources or grant new document permissions. To disconnect an app, open **Settings → General → Authorized apps** and revoke it. - -MCP provides three tools for your organization: - -- **search** finds indexed passages. Narrow results by source, modification date, or document. -- **read_document** opens an indexed document by ID or its original URL. Read around a matching passage or page through longer documents. Results include a citation link. -- **chat** asks the Sim Assistant for an answer with citations. Each call starts a new private conversation and can use the same search filters. - -Search MCP is available in organization settings. All three tools use the caller’s current document permissions. They do not browse the web or change connected sources. +To use these sources from Claude, Codex, Claude Code, Cursor, or another compatible app, open **Settings → Search MCP**. Each person signs in with their own Sim account. The server provides `search`, `read_document`, and `chat`; `chat` starts a new private Sim conversation. See [Search MCP](/search/mcp) for app setup, permissions, and limits. ## Existing workspace Search @@ -138,9 +132,9 @@ Search runs background syncs on an hourly schedule. Large sources, provider limi ## If indexing needs attention -A completed sync means the source was checked; some documents may still be indexing. In the main **Integrations** page, each source row shows how many documents you can search and whether indexing failed for any documents you can access. +A completed sync means the source was checked; some documents may still be indexing. Integrations lists personal connections; a central source that needs no personal account, such as delegated Drive, can be searchable without appearing there. In the main **Integrations** page, each connected source row shows how many documents you can search and whether indexing failed for any documents you can access. -As an admin, open **Settings → Sources**, select **Manage** beside the integration, then open the source. In **Documents**, select **Failed** from the status dropdown to inspect those files. Use the search field to find a document by name. Select **Retry indexing** beside a file to try again. **Exclude** removes a file from search; select **Excluded** and then **Restore** to include it again. Fix a disconnected account or source configuration before retrying a sync that needs attention. +As an admin, open **Settings → Sources**, select **Manage** beside the integration, then open the source from **Advanced** (Google providers) or **Sources**. In **Documents**, select **Failed** from the status dropdown to inspect those files. Use the search field to find a document by name. Select **Retry indexing** beside a file to try again. **Exclude** removes a file from search; select **Excluded** and then **Restore** to include it again. Fix a disconnected account or source configuration before retrying a sync that needs attention. Empty source Documents tab with search and an Included status filter diff --git a/apps/docs/content/docs/search/jira.mdx b/apps/docs/content/docs/search/jira.mdx index 4c0b5fa4df6..ea3e1e960c1 100644 --- a/apps/docs/content/docs/search/jira.mdx +++ b/apps/docs/content/docs/search/jira.mdx @@ -32,7 +32,7 @@ Sim uses its existing Jira OAuth integration. Search uses `read:jira-work` to re ### Choose Jira -Open **Settings → Sources** and turn on **Jira** under **Allowed in Sim Search**. Select **Set up** (or **Manage** if sources already exist), then **Add source**. Jira sources use member accounts. +Open **Settings → Sources** and turn on **Jira**. Select **Set up** (or **Manage** if sources already exist), then **Add source**. Jira sources use member accounts. @@ -66,7 +66,7 @@ Click **Add source**. The source appears on Jira's **Sources** tab. Creating it ### Connect your search account -Open **Integrations** in the main sidebar and click **Connect account** on the Jira source. Complete the connection in the new tab using the Atlassian email that matches your verified Sim email. Select the configured Atlassian site when asked and grant the requested permissions. +Open **Integrations** in the main sidebar and click **Connect** on the Jira source. Complete the connection in the new tab using the Atlassian email that matches your verified Sim email. Select the configured Atlassian site when asked and grant the requested permissions. Return to Integrations to see connection and indexing status. Each teammate follows this same step. A previously authorized account may already be connected. @@ -86,11 +86,11 @@ Search manages the sync schedule. Item limits and sync frequency are not setup d ## Teammates and ongoing sync -Existing organization members see the same source configuration and their own **Connect account**, **Reconnect**, or indexing status. They do not choose projects again. Invite new teammates to the Sim organization through its Members settings or SSO onboarding, then have them open Integrations and connect Jira. A Jira authorization does not grant Sim organization membership. +Existing organization members see the same source configuration and their own **Connect**, **Reconnect**, or indexing status. They do not choose projects again. Invite new teammates to the Sim organization through its Members settings or SSO onboarding, then have them open Integrations and connect Jira. A Jira authorization does not grant Sim organization membership. Sim checks Jira separately using each connected person's account. Issue content and tags become searchable as processing finishes; changes and lost issue access are picked up by later syncs. The main Integrations page reports documents searchable by the current viewer. Admins open **Settings → Sources**, select **Manage** beside **Jira**, then open the source for **Documents**, **Settings**, and **Sync history**. -Use **Jira → Accounts → Request connections** to send Jira connection requests. These requests do not invite people into the Sim organization. For a different site or project scope, an admin can add another source; members can also use **Add another Jira source** in the main Integrations page. +Use **Jira → Accounts → Request connections** to send Jira connection requests. These requests do not invite people into the Sim organization. For a different site or project scope, an admin can add another source; members can also select **Connect** beside the Jira row labeled **Connect a different site or content scope** in Integrations. ## Troubleshooting @@ -99,10 +99,10 @@ Use **Jira → Accounts → Request connections** to send Jira connection reques | No provider setup controls | Ask a Sim organization admin to approve and set up Jira. | | Projects are empty or disabled | Enter the domain and connect a browsing account, or switch to manual project keys. Check that the account can browse those projects. | | Connected, but no issues | Confirm the authorized site matches the configured domain. Check project access, issue security, and the JQL filter. An admin's Jira access does not grant access to other members. | -| Email mismatch | Sign in to Atlassian with the email shown by Sim's connection flow. | +| Email mismatch | Sign in to Atlassian with the same email as your verified Sim account. If switching accounts in Jira does not help, log out of Atlassian and sign in again before retrying **Connect**. | | Atlassian says the callback URL is invalid | Ask the deployment operator to check the OAuth app identified by `JIRA_CLIENT_ID`. Its saved callback must exactly match the authorization request's `redirect_uri`, including scheme, hostname, port, and `/api/auth/oauth2/callback/jira` path. | | **Reconnect** | Reauthorize the Jira account and grant all requested permissions. This is needed after a grant is revoked or its required permissions change. | -| Connection tab does not open | Allow pop-ups for Sim, then click **Connect account** again. | +| Connection tab does not open | Allow pop-ups for Sim, then click **Connect** again. | ### Check access in Jira @@ -122,7 +122,7 @@ Atlassian illustration from its [permissions tutorial](https://www.atlassian.com ## Self-hosted operator setup -The deployment operator configures one shared Jira OAuth integration. Teammates continue to start **Connect account** from Sim. +The deployment operator configures one shared Jira OAuth integration. Teammates continue to start **Connect** from Sim. 1. Open the [Atlassian developer console](https://developer.atlassian.com/console/myapps/) and select your deployment's **OAuth 2.0 integration**, or create one for the deployment. 2. Under **Authorization**, configure **OAuth 2.0 (3LO)**. Add `https:///api/auth/oauth2/callback/jira` to **Callback URLs**, keeping any callbacks already used by your deployment, then save. @@ -138,6 +138,6 @@ The deployment operator configures one shared Jira OAuth integration. Teammates 3. Under **Permissions**, add **Jira API**, then **Configure** its classic and granular scopes for Jira, Jira Service Management, and Assets. Separately add **User Identity API** with `read:me`. Sim requests `offline_access` in the authorization URL for refresh tokens. Configure the full `jira` scope list for your release in [Sim's OAuth configuration](https://github.com/simstudioai/sim/blob/staging/apps/sim/lib/oauth/oauth.ts); the Search read scopes above are only a subset of this shared integration's permissions. 4. Under **Distribution**, enable sharing so teammates can authorize the app. Copy the client ID and secret from **Settings** into `JIRA_CLIENT_ID` and `JIRA_CLIENT_SECRET`, set the correct `NEXT_PUBLIC_APP_URL`, and restart Sim. -5. Start a connection from **Integrations**. Confirm that Atlassian lists the intended site, then return to Sim. After changing requested scopes, reconnect previously authorized accounts. +5. Start a connection from **Integrations**. Confirm that Atlassian lists the intended site, then return to Sim. After changing the deployment's OAuth client or requested scopes, an organization admin selects **Settings → Sources → Update configurations**, then affected teammates reconnect. For a local instance using `NEXT_PUBLIC_APP_URL=http://localhost:3000`, register `http://localhost:3000/api/auth/oauth2/callback/jira`. Use a separate development OAuth app when production callbacks must remain unchanged. After updating local client credentials or the app URL, restart Sim and begin a new connection from **Integrations**. If only the app owner can connect, check **Distribution**. See Atlassian's [OAuth configuration and sharing guide](https://developer.atlassian.com/cloud/jira/platform/oauth-2-3lo-apps/) and Sim's [deployment reference](/platform/self-hosting/integrations-oauth). diff --git a/apps/docs/content/docs/search/mcp.mdx b/apps/docs/content/docs/search/mcp.mdx new file mode 100644 index 00000000000..bd2d3de0638 --- /dev/null +++ b/apps/docs/content/docs/search/mcp.mdx @@ -0,0 +1,52 @@ +--- +title: Search MCP +description: Search your organization's sources from Claude, Codex, Cursor, and other MCP apps +--- + +Use Sim Search from another app to find and read your organization's indexed documents, or ask the Sim Assistant for cited answers. Your Sim permissions apply. + +## Connect an app + +1. Confirm you can find a document in the organization's **Search**. [Connect your source account](/search/connect-your-account) first if required. +2. Open **Settings → Search MCP** in the organization view. Select your **App** and copy the displayed URL, command, or configuration. +3. Follow the steps for your app below, sign in to Sim, and approve Search access. Each teammate signs in separately; no API key is needed for this flow. + +| App | Finish setup | +| --- | --- | +| **Claude** | Add the copied **Server URL** as a custom connector, then connect it. For Team or Enterprise, an owner first adds it under **Organization settings → Connectors**; members then connect individually. See [Claude's custom connector instructions](https://support.claude.com/en/articles/11175166-get-started-with-custom-connectors-using-remote-mcp). | +| **Codex** | Run the copied **Terminal command** and complete browser sign-in. If authentication is needed again, run `codex mcp login sim-search`. | +| **Claude Code** | Run the copied **Terminal command**, then open `/mcp` in Claude Code and authenticate. See [Claude Code's MCP instructions](https://code.claude.com/docs/en/mcp). | +| **Cursor** | Add the copied `sim-search` entry to `mcpServers` in `~/.cursor/mcp.json`, preserving your other entries. Enable the server in Cursor and sign in to Sim. See [Cursor's MCP instructions](https://cursor.com/docs/mcp). | +| **Other** | Add the **Server URL** to a client that supports remote MCP with OAuth. Choose **Streamable HTTP** if asked. | + +For Claude's hosted custom connectors, your Sim deployment must be reachable from Claude's servers; a localhost URL will not work. See [Claude's network requirements](https://support.claude.com/en/articles/11175166-get-started-with-custom-connectors-using-remote-mcp). + +## Use the tools + +Ask your app: “Use Sim Search to find our launch checklist, read the relevant document, and cite the source.” The app can use three tools: + +| Tool | Behavior | +| --- | --- | +| `search` | Finds matching passages. Filter by `source` (such as `jira`), `modifiedAfter` (an ISO timestamp), or `documentIds`. Use the returned `citationUrl` when citing a result. | +| `read_document` | Reads an indexed document using either its returned `documentId` or original URL. Use `aroundChunkIndex` for context around a search hit, or `offset` to page through it. It does not fetch arbitrary web pages. | +| `chat` | Asks the Sim Assistant a question using your accessible sources and returns an answer with citations. Each call creates a new private Sim conversation. It accepts the same filters as `search`. | + +The tools do not edit source content. Assistant policy and usage limits apply to `chat`. + +## Access and limits + +The connection applies to the organization whose URL you copied. Sim checks your current membership and source access on each request. Connecting MCP does not add sources, connect provider accounts, or grant additional document permissions. Source edits and permission changes follow the same sync behavior as regular Search. + +Search returns 10 passages by default, with `topK` up to 50. Document reads return 20 chunks by default, with `limit` up to 50. When `pagination.hasMore` is true, continue at `pagination.offset + pagination.limit`; use either `offset` or `aroundChunkIndex`, not both. Documents still indexing return metadata only. + +Queries allow up to 8,192 characters; `documentIds` accepts up to 20 IDs. Responses are limited to 1 MiB. Request fewer passages or smaller document pages if a result is too large. API rate limits also apply; follow the retry delay returned by the tool. + +## Reconnect or revoke access + +If sign-in expires or access is revoked, reconnect in your app. To withdraw its Sim authorization, open **Settings → General → Authorized apps**, find the app, and revoke it. This stops future requests; it does not remove content already returned to that app. + +If Search MCP is unavailable, ask an organization admin to check the organization's Search availability and MCP policy. If results are missing, check the same query in Sim Search, your provider connection, and source sync status first. + +## Workspace MCP is separate + +Organization Search MCP searches organization sources. [Workspace MCP tools](/agents/mcp) connect external servers to Sim agents; [MCP deployment](/workflows/deployment/mcp) exposes workflows as tools. Neither setup automatically adds workspace content to organization Search. diff --git a/apps/docs/content/docs/search/meta.json b/apps/docs/content/docs/search/meta.json index f3f4dffbd2c..473704f2d45 100644 --- a/apps/docs/content/docs/search/meta.json +++ b/apps/docs/content/docs/search/meta.json @@ -2,6 +2,7 @@ "title": "Search", "pages": [ "connect-your-account", + "mcp", "confluence", "github", "gitlab", diff --git a/apps/docs/content/docs/search/slack.mdx b/apps/docs/content/docs/search/slack.mdx index 81d8c0cb91f..42917bc7438 100644 --- a/apps/docs/content/docs/search/slack.mdx +++ b/apps/docs/content/docs/search/slack.mdx @@ -6,15 +6,13 @@ description: Set up a workspace Slack app and connect members for channel search import { Step, Steps } from 'fumadocs-ui/components/steps' import { Image } from '@/components/ui/image' -Slack Search indexes channel messages and threads. A Sim organization admin configures your Slack app once, then each teammate authorizes their own Slack account. Their results are limited to the selected public channels and private channels they can access. DMs and group DMs are not indexed. - -Slack app setup for Sim Search with application and client credential fields +Slack Search indexes messages and threads each connected member can access. Public and private channels are included by default; one-to-one and group DMs are opt-in. A Sim organization admin installs the organization's Slack app, then each teammate authorizes their own account for indexing. ## Before you start You need a Sim organization admin and permission to create and install an app in the target Slack workspace. Ask a Slack workspace admin for approval when app installation is restricted. Use the same email address for Slack and your verified Sim account. -This guide covers **indexing Slack messages for Search and MCP**. It does not install a Sim assistant that answers inside Slack. +The same app supports **Sim Search in Slack** and **indexing Slack messages**. Installing the bot lets it answer questions about connected sources. Indexing Slack content additionally requires a source and each member’s authorization. ## Set up the organization's Slack app @@ -25,54 +23,57 @@ Skip to **Connect the source** if the organization already has a verified Slack ### Open provider settings -Open **Settings → Sources** and turn on **Slack** under **Allowed in Sim Search**. Select **Set up** (or **Manage** if sources already exist), then **Set up Slack app**. Allowing Slack does not configure the app or start indexing. +Open **Settings → Sources** and turn on **Slack**. Select **Set up** (or **Manage** if sources already exist), then **Set up Slack app**. Allowing Slack does not configure the app or start indexing. ### Configure an app in Slack -On the [Slack Apps page](https://api.slack.com/apps), create an app for the target workspace, or use an app dedicated to your Sim organization. Under **OAuth & Permissions**, add the user scopes and both redirect URLs listed below. Keep **Token Rotation** disabled. +Select **Install Sim Search** to open the three-step setup. In step 1, select **Create app in Slack** and choose the target workspace. Sim supplies a manifest with the required scopes, redirects, events, and interactivity URL. Keep **Token Rotation** disabled. + +You can also open this wizard from **Settings → Sim Search in Slack → Set up**. Slack app settings showing Basic Information and App Credentials *Official Slack example: [Basic Information](https://docs.slack.dev/tools/bolt-python/creating-an-app/#create-a-new-app). Use your own app's credentials.* -In Sim, enter these four fields: +Return to Sim and select **Continue**. In step 2, copy **Client ID**, **Client Secret**, and **Signing Secret** from the new app’s **Basic Information → App Credentials**: -| Sim field | Where to find it | -|---|---| -| Slack App ID | Slack app **Basic Information → App Credentials** (`A…`). | -| Slack workspace ID | The workspace segment of the Slack web URL, `app.slack.com/client/T…/…`. | -| Client ID | The same app's **Basic Information → App Credentials**. | -| Client Secret | The same app's **Basic Information → App Credentials**. | +Sim Search in Slack setup asking for Client ID, Client Secret, and Signing Secret -Organization setup uses personal user authorization. It does not ask for a bot token or signing secret. +Select **Continue**, then **Install in Slack** in step 3. Approve the installation in Slack. Sim saves the bot connection and opens **Settings → Sim Search in Slack**. Complete any required Slack administrator approval before continuing. ### Verify and continue -Select **Verify and add**, then authorize the app in the Slack popup. Sim verifies the app, workspace, client credentials, and required scopes. Allow popups if the window does not open. +Return to **Settings → Sources → Slack → Set up Slack app**. Select **Verify and add** and authorize member access for the installed app. Allow popups if the authorization window does not open. -After verification, Sim returns to the provider page. Open **Sources → Add source** to choose what to index. If you opened app setup from an unfinished source form, Sim returns to that form instead. If Slack requires administrator approval, complete that approval before continuing. +Member connections use one configured Slack app and workspace per organization. Installing another bot does not change that configuration. Changing the verified app requires members to reconnect. + +After verification, open **Sources → Add source** on Slack’s provider page to choose what to index. A connected bot alone does not mean Slack messages have been indexed. ## Connect the source -Open **Sources → Add source** on Slack's provider page. **Channels** and **Earliest Message Date** appear first. Open **More options** for exclusions, archived channels, metadata tags, and **Sync documents with**. Keep **Connected members** for the usual setup. +Open **Sources → Add source** on Slack's provider page. Choose **Channel Messages**, **Direct Messages**, **Channels**, and **Earliest Message Date**. Open **More options** for exclusions, archived channels, metadata tags, and **Sync documents with**. Keep **Connected members** for the usual setup. | Field | Behavior | |---|---| +| Channel Messages | Included by default. Turn off for a DM-only source. | +| Direct Messages | Excluded by default. Include to index one-to-one and group DMs the connected member can access. | | Channels | Leave blank for all accessible public and private channels, or choose channel names/IDs. | | Excluded Channels | Names or IDs to omit; exclusions override included channels. | -| Archived Channels | Included by default. | +| Archived Channels | Included by default. The picker lists active channels; use manual names/IDs to select archived channels. | | Earliest Message Date | Optional UTC date (`YYYY-MM-DD`). Applies to the thread's first message; replies are included with that thread. | -Select **Add source**. Each person opens **Integrations** in the main sidebar, selects **Connect account** on the Slack source, and approves the configured app. Creating the source or verifying the Slack app does not authorize teammates automatically. +Select **Add source**. Each person opens **Integrations** in the main sidebar, selects **Connect** on the Slack source, and approves the configured app. Creating the source or verifying the Slack app does not authorize teammates automatically. + +The Slack app's **Home → Connect sources** opens this same Integrations page. To send a Slack connection request, open **Slack → Accounts → Request connections**. This requests an external account connection; it does not invite the recipient to the Sim organization. @@ -82,27 +83,28 @@ Admins open **Settings → Sources**, select **Manage** beside **Slack**, then o ## Permissions reference -The organization account pool supports Search and workspace workflow tools. Its current authorization requests the following **User Token Scopes**, including write permissions. Search itself only indexes channel messages and threads; workspace use is controlled separately in the organization account settings. +New Search member connections request these read-only **User Token Scopes**. They are separate from the bot scopes used to answer messages in Slack. | Purpose | User scopes | |---|---| -| Public channels | `channels:read`, `channels:history`, `channels:write` | -| Private channels | `groups:read`, `groups:history`, `groups:write` | -| Messages and conversations | `chat:write`, `im:read`, `im:history`, `im:write`, `mpim:read`, `mpim:history`, `mpim:write` | -| Files and canvases | `files:read`, `files:write`, `canvases:read`, `canvases:write` | -| Reactions | `reactions:read`, `reactions:write` | -| Identity and profile | `users:read`, `users:read.email`, `users.profile:read`, `users.profile:write` | +| Public channels | `channels:read`, `channels:history` | +| Private channels | `groups:read`, `groups:history` | +| Direct messages | `im:read`, `im:history`, `mpim:read`, `mpim:history` | +| Identity | `users:read`, `users:read.email` | -Add both redirect URLs under **OAuth & Permissions → Redirect URLs**, using your Sim origin: +The generated manifest includes these redirect URLs under **OAuth & Permissions → Redirect URLs**, using your Sim origin: ```text +https:///api/knowledge/slack/oauth/callback https:///api/credential-groups/slack-managed-users/callback https:///api/credential-groups/oauth/slack/callback ``` Compare **OAuth & Permissions → Scopes → User Token Scopes** with the table above. If scopes change, update the Slack app and have members reconnect. Do not change a shared production app's credentials to configure a separate test installation. -For existing **workspace** Search, use **Search → Add source → Slack**. That flow uses the custom-bot wizard and **Connected accounts → Access → Search documents**, which requests the six read-only channel and identity scopes instead. Its bot installation is separate from the organization setup described here. +Existing workflow account pools retain their configured permissions; preserve those scopes when updating the shared app. + +For existing **workspace** Search, use **Search → Add source → Slack**. That flow uses the custom-bot wizard and **Connected accounts → Access → Search documents**, which requests read-only channel, DM, and identity scopes. Its bot installation is separate from the organization setup described here. See Slack's [app manifest reference](https://docs.slack.dev/reference/app-manifest/) and [user token access model](https://docs.slack.dev/authentication/tokens/). @@ -111,7 +113,7 @@ See Slack's [app manifest reference](https://docs.slack.dev/reference/app-manife | Problem | Next step | |---|---| | Setup keeps asking for a Slack app | Finish **Verify and add** in the Slack setup; approval alone is insufficient. | -| Redirect mismatch | Check both redirect URLs above against your Sim origin. | +| Redirect mismatch | Check all three redirect URLs above against your Sim origin. | | App or workspace mismatch | Use the App ID and client credentials from the same app, and the ID of the workspace being authorized. | | Missing scopes | Compare User Token Scopes with the table above, update the Slack app, reinstall as Slack requires, and reconnect. In workspace setup, select **Search documents** in both setup screens. | | Missing private-channel results | Confirm the member is in the channel and it is within the source filters. With an indexing account, confirm that account can read it too. | diff --git a/apps/docs/public/static/search/confluence-setup.jpg b/apps/docs/public/static/search/confluence-setup.jpg index 8ad1f1aed92..8efc63e456d 100644 Binary files a/apps/docs/public/static/search/confluence-setup.jpg and b/apps/docs/public/static/search/confluence-setup.jpg differ diff --git a/apps/docs/public/static/search/connect-account.png b/apps/docs/public/static/search/connect-account.png index 163bdd34312..d4ad8a4044a 100644 Binary files a/apps/docs/public/static/search/connect-account.png and b/apps/docs/public/static/search/connect-account.png differ diff --git a/apps/docs/public/static/search/integration-provider.jpg b/apps/docs/public/static/search/integration-provider.jpg index 96341327613..bf655343f51 100644 Binary files a/apps/docs/public/static/search/integration-provider.jpg and b/apps/docs/public/static/search/integration-provider.jpg differ diff --git a/apps/docs/public/static/search/integration-settings.jpg b/apps/docs/public/static/search/integration-settings.jpg index ab46bb0e984..05d4e20bebe 100644 Binary files a/apps/docs/public/static/search/integration-settings.jpg and b/apps/docs/public/static/search/integration-settings.jpg differ diff --git a/apps/docs/public/static/search/slack-setup.jpg b/apps/docs/public/static/search/slack-setup.jpg index 92e06d28802..c9ee55a27df 100644 Binary files a/apps/docs/public/static/search/slack-setup.jpg and b/apps/docs/public/static/search/slack-setup.jpg differ diff --git a/apps/docs/public/static/search/source-settings.jpg b/apps/docs/public/static/search/source-settings.jpg index 1a1cb850924..d89c8062380 100644 Binary files a/apps/docs/public/static/search/source-settings.jpg and b/apps/docs/public/static/search/source-settings.jpg differ diff --git a/apps/docs/public/static/search/source-sync-history.jpg b/apps/docs/public/static/search/source-sync-history.jpg index 84151025f0d..59b7ef8abd6 100644 Binary files a/apps/docs/public/static/search/source-sync-history.jpg and b/apps/docs/public/static/search/source-sync-history.jpg differ diff --git a/apps/sim/app/(auth)/verify/use-verification.test.tsx b/apps/sim/app/(auth)/verify/use-verification.test.tsx new file mode 100644 index 00000000000..bb09a6d84ea --- /dev/null +++ b/apps/sim/app/(auth)/verify/use-verification.test.tsx @@ -0,0 +1,102 @@ +/** @vitest-environment jsdom */ +import { act } from 'react' +import { createRoot, type Root } from 'react-dom/client' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +const mocks = vi.hoisted(() => ({ + session: vi.fn(), + refetch: vi.fn(), + verify: vi.fn(), + resend: vi.fn(), +})) + +vi.mock('@/lib/auth/auth-client', () => ({ + useSession: () => ({ data: mocks.session(), refetch: mocks.refetch }), + client: { emailOtp: { verifyEmail: mocks.verify, sendVerificationOtp: mocks.resend } }, +})) +vi.mock('next/navigation', () => ({ useSearchParams: () => new URLSearchParams() })) + +import { useVerification } from '@/app/(auth)/verify/use-verification' + +function useTestVerification() { + return useVerification({ + hasEmailService: true, + isProduction: true, + isEmailVerificationEnabled: true, + }) +} + +let root: Root +function renderVerification() { + const result = { current: undefined as ReturnType | undefined } + function Harness() { + result.current = useTestVerification() + return null + } + act(() => root.render()) + return { + get current() { + return result.current! + }, + } +} + +beforeEach(() => { + vi.clearAllMocks() + vi.useFakeTimers() + vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true) + const container = document.createElement('div') + document.body.appendChild(container) + root = createRoot(container) + sessionStorage.clear() + mocks.session.mockReturnValue({ user: { email: 'member@example.com', emailVerified: false } }) + mocks.verify.mockResolvedValue({}) + mocks.resend.mockResolvedValue({}) +}) + +afterEach(() => { + act(() => root.unmount()) + document.body.innerHTML = '' + vi.clearAllTimers() + vi.useRealTimers() + vi.unstubAllGlobals() + sessionStorage.clear() +}) + +describe('verification after opening an enrollment in a new tab', () => { + it('resends and verifies for the signed-in user without signup storage', async () => { + const result = renderVerification() + expect(result.current.email).toBe('member@example.com') + await act(async () => result.current.resendCode()) + expect(mocks.resend).toHaveBeenCalledWith({ + email: 'member@example.com', + type: 'email-verification', + }) + act(() => result.current.handleOtpChange('123456')) + await act(async () => result.current.verifyCode()) + expect(mocks.verify).toHaveBeenCalledWith({ email: 'member@example.com', otp: '123456' }) + expect(result.current.status).toBe('verified') + expect(mocks.refetch).toHaveBeenCalled() + }) + + it('uses the current account over a previous signup address in the tab', async () => { + sessionStorage.setItem('verificationEmail', 'previous@example.com') + const result = renderVerification() + await act(async () => result.current.resendCode()) + expect(mocks.resend).toHaveBeenCalledWith({ + email: 'member@example.com', + type: 'email-verification', + }) + }) + + it('preserves signup verification before a session exists', async () => { + mocks.session.mockReturnValue(null) + sessionStorage.setItem('verificationEmail', 'signup@example.com') + const result = renderVerification() + await act(async () => result.current.resendCode()) + expect(mocks.resend).toHaveBeenCalledWith({ + email: 'signup@example.com', + type: 'email-verification', + }) + }) +}) diff --git a/apps/sim/app/(auth)/verify/use-verification.ts b/apps/sim/app/(auth)/verify/use-verification.ts index 5927438998b..b2009abdbbc 100644 --- a/apps/sim/app/(auth)/verify/use-verification.ts +++ b/apps/sim/app/(auth)/verify/use-verification.ts @@ -75,18 +75,19 @@ export function useVerification({ isEmailVerificationEnabled, }: UseVerificationParams): UseVerificationReturn { const searchParams = useSearchParams() - const { refetch: refetchSession } = useSession() + const { data: session, refetch: refetchSession } = useSession() const [otp, setOtp] = useState('') - const [email, setEmail] = useState('') + const [storedEmail, setStoredEmail] = useState('') const [status, setStatus] = useState('idle') const [isResending, setIsResending] = useState(false) const [errorMessage, setErrorMessage] = useState('') useEffect(() => { const storedEmail = sessionStorage.getItem('verificationEmail') - if (storedEmail) setEmail(storedEmail) + if (storedEmail) setStoredEmail(storedEmail) }, []) + const email = session?.user?.email || storedEmail const isOtpComplete = otp.length === 6 async function verifyCode() { diff --git a/apps/sim/app/api/organization-credentials/oauth/route.test.ts b/apps/sim/app/api/organization-credentials/oauth/route.test.ts index a4f0499298a..f666a71e7fa 100644 --- a/apps/sim/app/api/organization-credentials/oauth/route.test.ts +++ b/apps/sim/app/api/organization-credentials/oauth/route.test.ts @@ -15,7 +15,7 @@ vi.mock('@/lib/credentials/application/organization-credentials', () => { } as const return { organizationCredentialOperations: { list: operation }, - listOrganizationCredentials: { operation, execute: mocks.execute }, + listOrganizationOAuthCredentials: { operation, execute: mocks.execute }, } }) @@ -45,23 +45,26 @@ describe('GET /api/organization-credentials/oauth', () => { credentials: [ { id: 'full-credential', - displayName: 'Full access', - providerId: 'google-drive', + type: 'oauth', + name: 'Full access', + provider: 'google-drive', scopes: [DRIVE_SCOPE, METADATA_SCOPE], accountId: 'private-account-full', encryptedValue: 'private-secret', }, { id: 'limited-credential', - displayName: 'Limited access', - providerId: 'google-drive', + type: 'oauth', + name: 'Limited access', + provider: 'google-drive', scopes: [METADATA_SCOPE], accountId: 'private-account-limited', }, { id: 'unknown-credential', - displayName: 'Unknown access', - providerId: 'google-drive', + type: 'oauth', + name: 'Unknown access', + provider: 'google-drive', scopes: [], }, ], @@ -110,6 +113,60 @@ describe('GET /api/organization-credentials/oauth', () => { ) }) + it('forwards browsing intent with the acting session and projects a managed choice safely', async () => { + mocks.execute.mockResolvedValue({ + credentials: [ + { + id: 'managed-1', + name: 'My Jira', + provider: 'jira', + type: 'managed_oauth', + scopes: ['read:jira-work'], + encryptedAccessToken: 'never-public', + }, + ], + }) + const response = await GET( + createMockRequest( + 'GET', + undefined, + {}, + 'http://localhost:3000/api/organization-credentials/oauth?organizationId=org-1&providerId=jira&purpose=browsing' + ) + ) + expect(response.status).toBe(200) + expect(await response.json()).toEqual({ + credentials: [ + { + id: 'managed-1', + name: 'My Jira', + provider: 'jira', + type: 'managed_oauth', + scopes: ['read:jira-work'], + }, + ], + }) + expect(mocks.execute).toHaveBeenCalledWith( + expect.objectContaining({ + principal: { kind: 'session', userId: 'admin-1', sessionId: 'session-1' }, + input: { organizationId: 'org-1', providerId: 'jira', type: 'oauth', purpose: 'browsing' }, + }) + ) + }) + + it('rejects an unsupported listing purpose before the use case', async () => { + const response = await GET( + createMockRequest( + 'GET', + undefined, + {}, + 'http://localhost:3000/api/organization-credentials/oauth?organizationId=org-1&purpose=all-members' + ) + ) + expect(response.status).toBe(400) + expect(mocks.execute).not.toHaveBeenCalled() + }) + it('still returns an empty authorized list without fabricating a credential', async () => { mocks.execute.mockResolvedValue({ credentials: [] }) diff --git a/apps/sim/app/api/organization-credentials/oauth/route.ts b/apps/sim/app/api/organization-credentials/oauth/route.ts index e6e784b9a45..f354d397279 100644 --- a/apps/sim/app/api/organization-credentials/oauth/route.ts +++ b/apps/sim/app/api/organization-credentials/oauth/route.ts @@ -6,10 +6,9 @@ import { } from '@/lib/api/server/routes' import { internalCredentialErrorPolicy } from '@/lib/credentials/api/route-policies' import { - listOrganizationCredentials, + listOrganizationOAuthCredentials, organizationCredentialOperations, } from '@/lib/credentials/application/organization-credentials' -import type { OAuthProvider } from '@/lib/oauth/types' export const GET = defineInternalJsonRoute({ contract: listOrganizationOAuthCredentialsContract, @@ -18,14 +17,5 @@ export const GET = defineInternalJsonRoute({ rateLimit: internalRateLimits.none({ reason: 'Preserve OAuth credential listing behavior' }), errorPolicy: internalCredentialErrorPolicy, mapInput: ({ query }) => ({ ...query, type: 'oauth' as const }), - useCase: listOrganizationCredentials, - present: ({ credentials }) => ({ - credentials: credentials.map((row) => ({ - id: row.id, - name: row.displayName, - provider: row.providerId as OAuthProvider, - type: 'oauth' as const, - scopes: row.scopes, - })), - }), + useCase: listOrganizationOAuthCredentials, }) diff --git a/apps/sim/app/api/organizations/[id]/connected-accounts/[groupId]/slack-managed-users/route.test.ts b/apps/sim/app/api/organizations/[id]/connected-accounts/[groupId]/slack-managed-users/route.test.ts index 146a97011f1..087273cd537 100644 --- a/apps/sim/app/api/organizations/[id]/connected-accounts/[groupId]/slack-managed-users/route.test.ts +++ b/apps/sim/app/api/organizations/[id]/connected-accounts/[groupId]/slack-managed-users/route.test.ts @@ -20,8 +20,6 @@ import { POST } from '@/app/api/organizations/[id]/connected-accounts/[groupId]/ const body = { appId: 'A123', teamId: 'T123', - clientId: 'fixture-client-id', - clientSecret: 'fixture-client-secret', } const context = { params: Promise.resolve({ id: 'org-a', groupId: 'group-a' }) } function request(input: unknown = body) { @@ -43,7 +41,7 @@ beforeEach(() => { }) describe('organization Slack setup route', () => { - it('authenticates before parsing setup secrets', async () => { + it('authenticates before parsing setup input', async () => { mocks.session.mockResolvedValue(null) const response = await POST(request({}), context) expect(response.status).toBe(401) @@ -67,6 +65,12 @@ describe('organization Slack setup route', () => { expect(mocks.execute).not.toHaveBeenCalled() }) + it.each(['clientId', 'clientSecret'])('rejects a client-supplied OAuth %s', async (field) => { + const response = await POST(request({ ...body, [field]: 'client-supplied-value' }), context) + expect(response.status).toBe(400) + expect(mocks.execute).not.toHaveBeenCalled() + }) + it('preserves refusal when current organization authority is insufficient', async () => { mocks.execute.mockRejectedValue( new OrchestrationError('forbidden', 'Organization admin required') diff --git a/apps/sim/app/credential-groups/enroll/[token]/page.test.tsx b/apps/sim/app/credential-groups/enroll/[token]/page.test.tsx index 3b01f425c50..970dcd47e6b 100644 --- a/apps/sim/app/credential-groups/enroll/[token]/page.test.tsx +++ b/apps/sim/app/credential-groups/enroll/[token]/page.test.tsx @@ -204,7 +204,11 @@ describe('focused Search enrollment', () => { session: { id: 'session-1' }, }) await render({ returnTo: 'search', optionId: 'site-two' }) - expect(document.querySelector('a')?.getAttribute('href')).toBe('/verify') + const recovery = new URL(document.querySelector('a')!.getAttribute('href')!, 'https://sim.test') + expect(recovery.pathname).toBe('/verify') + expect(recovery.searchParams.get('redirectAfter')).toBe( + '/credential-groups/enroll/invitation?returnTo=search&optionId=site-two' + ) expect(mocks.read).not.toHaveBeenCalled() }) diff --git a/apps/sim/app/credential-groups/enroll/[token]/page.tsx b/apps/sim/app/credential-groups/enroll/[token]/page.tsx index f5504cef26a..63423f76409 100644 --- a/apps/sim/app/credential-groups/enroll/[token]/page.tsx +++ b/apps/sim/app/credential-groups/enroll/[token]/page.tsx @@ -138,21 +138,21 @@ export default async function CredentialGroupEnrollmentPage({ const { token } = await params if (!token || token.length > 128) return const resolvedSearchParams = await searchParams + const callback = new URLSearchParams() + for (const key of ['returnTo', 'optionId']) { + const value = getSearchParam(resolvedSearchParams, key) + if (value) callback.set(key, value) + } + const callbackUrl = `/credential-groups/enroll/${encodeURIComponent(token)}${callback.size ? `?${callback}` : ''}` const session = await getSession() if (!session?.user) { - const callback = new URLSearchParams() - for (const key of ['returnTo', 'optionId']) { - const value = getSearchParam(resolvedSearchParams, key) - if (value) callback.set(key, value) - } - const callbackUrl = `/credential-groups/enroll/${encodeURIComponent(token)}${callback.size ? `?${callback}` : ''}` redirect(`/login?callbackUrl=${encodeURIComponent(callbackUrl)}`) } if (!session.user.emailVerified) return ( ) diff --git a/apps/sim/app/o/[organizationId]/home/components/get-started/get-started.tsx b/apps/sim/app/o/[organizationId]/home/components/get-started/get-started.tsx index 9dd3e3fb56f..5d343df275e 100644 --- a/apps/sim/app/o/[organizationId]/home/components/get-started/get-started.tsx +++ b/apps/sim/app/o/[organizationId]/home/components/get-started/get-started.tsx @@ -1,14 +1,15 @@ 'use client' -import { useState } from 'react' +import { useEffect, useState } from 'react' import { cn, Expandable, ExpandableContent } from '@sim/emcn' import { ArrowRight, ChevronDown } from '@sim/emcn/icons' import Link from 'next/link' +import { OAUTH_SEARCH_READ_SCOPE, oauthScopeSatisfies } from '@/lib/auth/oauth-provider' import type { ResourceScope } from '@/lib/core/resource-scope' import { organizationRoutes } from '@/lib/navigation/paths' import { useOrganizationContext } from '@/app/o/[organizationId]/providers/organization-provider' -import { useApiKeys } from '@/hooks/queries/api-keys' import { useSearchSourceOverview } from '@/hooks/queries/kb/connectors' +import { useAuthorizedApps } from '@/hooks/queries/oauth-provider' type StepId = 'connect-integration' | 'connect-sim-search' @@ -66,14 +67,24 @@ function StepMark({ complete }: { complete: boolean }) { * the workspace home's suggested actions: a hover-revealed disclosure header * over hairline-separated rows. Each step leads to the page that completes it, * and reads as done from the organization's real state: a source the viewer can - * search and a personal API key for the MCP server. + * search and an OAuth app authorized to use Search. */ export function GetStarted() { const { organization, viewer } = useOrganizationContext() const routes = organizationRoutes(organization.id) const scope: ResourceScope = { kind: 'organization', organizationId: organization.id } const { data: overview } = useSearchSourceOverview(scope) - const { data: apiKeys } = useApiKeys('', 'personal') + const { + data: authorizedApps, + fetchNextPage, + hasNextPage, + isFetching, + isError, + } = useAuthorizedApps('', { enabled: viewer.canUseSearchMcp }) + const hasSearchAuthorization = + authorizedApps?.pages.some((page) => + page.apps.some((app) => oauthScopeSatisfies(app.scopes, OAUTH_SEARCH_READ_SCOPE)) + ) ?? false const hrefs: Record = { 'connect-integration': viewer.isAdmin @@ -83,8 +94,9 @@ export function GetStarted() { } const completed: Record = { 'connect-integration': overview?.hasSearchableDocuments === true, - 'connect-sim-search': (apiKeys?.personalKeys.length ?? 0) > 0, + 'connect-sim-search': hasSearchAuthorization, } + const steps = STEPS.filter((step) => step.id !== 'connect-sim-search' || viewer.canUseSearchMcp) const [expanded, setExpanded] = useState(true) /** @@ -95,6 +107,25 @@ export function GetStarted() { */ const [animationsEnabled, setAnimationsEnabled] = useState(false) + useEffect(() => { + if ( + viewer.canUseSearchMcp && + !hasSearchAuthorization && + hasNextPage && + !isFetching && + !isError + ) { + void fetchNextPage() + } + }, [ + viewer.canUseSearchMcp, + hasSearchAuthorization, + hasNextPage, + isFetching, + isError, + fetchNextPage, + ]) + const handleToggleExpanded = () => { setAnimationsEnabled(true) setExpanded((prev) => !prev) @@ -135,7 +166,7 @@ export function GetStarted() { would hold its full value through the close and then vanish on unmount, snapping the content below up. */}
- {STEPS.map((step, i) => { + {steps.map((step, i) => { const complete = completed[step.id] return ( ({ context: vi.fn(), @@ -13,6 +14,8 @@ const mocks = vi.hoisted(() => ({ consume: vi.fn(), sources: vi.fn(), apiKeys: vi.fn(), + authorizedApps: vi.fn(), + fetchNextPage: vi.fn(), })) vi.mock('@/lib/auth/auth-client', () => ({ useSession: () => ({ data: { user: { id: 'reader' } } }), @@ -30,6 +33,7 @@ vi.mock('@/hooks/queries/mothership-chats', () => ({ vi.mock('@/app/o/[organizationId]/home/components/composer', () => ({ Composer: mocks.composer })) vi.mock('@/hooks/queries/kb/connectors', () => ({ useSearchSourceOverview: mocks.sources })) vi.mock('@/hooks/queries/api-keys', () => ({ useApiKeys: mocks.apiKeys })) +vi.mock('@/hooks/queries/oauth-provider', () => ({ useAuthorizedApps: mocks.authorizedApps })) vi.mock('@/app/workspace/[workspaceId]/home/components/mothership-chat', () => ({ MothershipChat: mocks.renderer, })) @@ -45,10 +49,11 @@ beforeEach(() => { mocks.context.mockReturnValue({ organization: { id: 'organization-a' }, searchAccess: { memberScoped: true }, - viewer: { isAdmin: false }, + viewer: { isAdmin: false, canUseSearchMcp: true }, }) mocks.sources.mockReturnValue({ data: { providers: [], hasSearchableDocuments: false } }) mocks.apiKeys.mockReturnValue({ data: { personalKeys: [] } }) + mockAuthorizedApps([{ apps: [], nextCursor: null }]) mocks.chat.mockReturnValue({ messages: [], isChatHistoryPending: true, sendMessage: mocks.send }) mocks.composer.mockReturnValue(
Question composer
) mocks.renderer.mockReturnValue(
Chat history
) @@ -65,6 +70,30 @@ function composerProps(): ComponentProps { return mocks.composer.mock.lastCall![0] } +function hasCompletedMcpStep() { + const link = container.querySelector('a[href="/o/organization-a/settings/search-mcp"]') + expect(link).not.toBeNull() + return link!.querySelector('span[aria-hidden="true"] svg') !== null +} + +function authorizedApp(scopes: string[], clientId = 'search-client'): AuthorizedApp { + return { clientId, name: clientId, scopes, authorizedAt: '2026-09-01T00:00:00.000Z' } +} + +function mockAuthorizedApps( + pages: AuthorizedAppsPage[], + state: { isFetching?: boolean; isError?: boolean } = {} +) { + mocks.authorizedApps.mockReturnValue({ + data: { pages }, + fetchNextPage: mocks.fetchNextPage, + hasNextPage: Boolean(pages.at(-1)?.nextCursor), + isFetching: false, + isError: false, + ...state, + }) +} + describe('organization home', () => { it.each([undefined, 'chat-a'])( 'does not mount Home or chat %s when Search is disabled', @@ -151,7 +180,7 @@ describe('organization home', () => { mocks.context.mockReturnValue({ organization: { id: 'organization-a' }, searchAccess: { memberScoped: true }, - viewer: { isAdmin }, + viewer: { isAdmin, canUseSearchMcp: true }, }) await act(async () => root.render()) expect( @@ -170,6 +199,79 @@ describe('organization home', () => { }) } ) + it('does not complete MCP onboarding for an unrelated personal API key', async () => { + mocks.apiKeys.mockReturnValue({ data: { personalKeys: [{ id: 'workflow-api-key' }] } }) + await act(async () => root.render()) + expect(hasCompletedMcpStep()).toBe(false) + expect(mocks.apiKeys).not.toHaveBeenCalled() + }) + + it('hides MCP onboarding and stops authorization paging when organization policy blocks access', async () => { + mocks.context.mockReturnValue({ + organization: { id: 'organization-a' }, + searchAccess: { memberScoped: true }, + viewer: { isAdmin: false, canUseSearchMcp: false }, + }) + mockAuthorizedApps([{ apps: [], nextCursor: 'older-apps' }]) + await act(async () => root.render()) + expect(container.textContent).not.toContain('Connect Sim Search MCP') + expect(container.textContent).toContain('Connect an integration') + expect(mocks.authorizedApps).toHaveBeenCalledWith('', { enabled: false }) + expect(mocks.fetchNextPage).not.toHaveBeenCalled() + }) + + it.each([ + { scopes: ['search:read'], completed: true }, + { scopes: ['api:read'], completed: true }, + { scopes: ['api:write'], completed: true }, + { scopes: ['offline_access'], completed: false }, + { scopes: [], completed: false }, + { scopes: ['unrecognized:read'], completed: false }, + ])('derives MCP completion from OAuth scopes $scopes', async ({ scopes, completed }) => { + mockAuthorizedApps([{ apps: [authorizedApp(scopes)], nextCursor: null }]) + await act(async () => root.render()) + expect(hasCompletedMcpStep()).toBe(completed) + }) + + it('finds a Search authorization after the first page and stops paging once found', async () => { + const firstPage = { + apps: [authorizedApp(['offline_access'], 'other-client')], + nextCursor: 'older-apps', + } + mockAuthorizedApps([firstPage]) + await act(async () => root.render()) + expect(hasCompletedMcpStep()).toBe(false) + expect(mocks.fetchNextPage).toHaveBeenCalledTimes(1) + + mockAuthorizedApps([ + firstPage, + { apps: [authorizedApp(['search:read'])], nextCursor: 'even-older-apps' }, + ]) + await act(async () => root.render()) + expect(hasCompletedMcpStep()).toBe(true) + expect(mocks.fetchNextPage).toHaveBeenCalledTimes(1) + }) + + it.each([{ isFetching: true }, { isError: true }])( + 'does not start another authorization page request while %j', + async (state) => { + mockAuthorizedApps([{ apps: [], nextCursor: 'older-apps' }], state) + await act(async () => root.render()) + expect(mocks.fetchNextPage).not.toHaveBeenCalled() + expect(hasCompletedMcpStep()).toBe(false) + } + ) + + it('clears MCP completion when the Search authorization is revoked', async () => { + mockAuthorizedApps([{ apps: [authorizedApp(['search:read'])], nextCursor: null }]) + await act(async () => root.render()) + expect(hasCompletedMcpStep()).toBe(true) + + mockAuthorizedApps([{ apps: [], nextCursor: null }]) + await act(async () => root.render()) + expect(hasCompletedMcpStep()).toBe(false) + }) + it('sends the member question as an assistant turn and clears the draft', async () => { await act(async () => root.render()) await act(async () => composerProps().onChange('Find our launch plan')) diff --git a/apps/sim/app/o/[organizationId]/settings/components/organization-search-mcp.test.tsx b/apps/sim/app/o/[organizationId]/settings/components/organization-search-mcp.test.tsx index de88160942d..636a3fc5080 100644 --- a/apps/sim/app/o/[organizationId]/settings/components/organization-search-mcp.test.tsx +++ b/apps/sim/app/o/[organizationId]/settings/components/organization-search-mcp.test.tsx @@ -3,13 +3,13 @@ import { act } from 'react' import { createRoot, type Root } from 'react-dom/client' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -const mocks = vi.hoisted(() => ({ organizationId: 'org-1' })) +const mocks = vi.hoisted(() => ({ organizationId: 'org-1', canUseSearchMcp: true })) vi.mock('@/lib/core/utils/urls', () => ({ getBaseUrl: () => 'https://sim.fixture.test' })) vi.mock('@/app/o/[organizationId]/providers/organization-provider', () => ({ useOrganizationContext: () => ({ organization: { id: mocks.organizationId }, - viewer: { canUsePersonalApiKeys: false }, + viewer: { canUsePersonalApiKeys: false, canUseSearchMcp: mocks.canUseSearchMcp }, }), })) @@ -22,6 +22,7 @@ describe('Organization Search MCP', () => { beforeEach(() => { vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true) mocks.organizationId = 'org-1' + mocks.canUseSearchMcp = true container = document.createElement('div') document.body.appendChild(container) root = createRoot(container) @@ -33,7 +34,7 @@ describe('Organization Search MCP', () => { vi.unstubAllGlobals() }) - it('offers organization OAuth setup even when personal API keys are disabled', async () => { + it('offers OAuth setup when Search MCP is allowed without API key management', async () => { await act(async () => root.render()) expect(container.querySelector('input')?.value).toBe( 'https://sim.fixture.test/api/mcp/search/organizations/org-1' @@ -44,6 +45,16 @@ describe('Organization Search MCP', () => { expect(container.textContent).not.toContain('Authorization header') }) + it('explains the organization policy restriction without offering OAuth setup', async () => { + mocks.canUseSearchMcp = false + await act(async () => root.render()) + expect(container.textContent).toContain('organization’s policy disables Sim Search MCP access') + expect(container.textContent).toContain('Contact an organization admin') + expect(container.querySelector('input')).toBeNull() + expect(container.querySelector('[aria-label^="MCP app: "]')).toBeNull() + expect(container.textContent).not.toContain('sign in to Sim') + }) + it('replaces the connection scope when the organization changes', async () => { await act(async () => root.render()) mocks.organizationId = 'org-2' diff --git a/apps/sim/app/o/[organizationId]/settings/components/organization-search-mcp.tsx b/apps/sim/app/o/[organizationId]/settings/components/organization-search-mcp.tsx index a0235437d6d..7607f4c93ed 100644 --- a/apps/sim/app/o/[organizationId]/settings/components/organization-search-mcp.tsx +++ b/apps/sim/app/o/[organizationId]/settings/components/organization-search-mcp.tsx @@ -5,7 +5,15 @@ import { useOrganizationContext } from '@/app/o/[organizationId]/providers/organ import { SearchMcpConnection } from '@/app/o/[organizationId]/settings/components/search-mcp-connection' export function OrganizationSearchMcp() { - const { organization } = useOrganizationContext() + const { organization, viewer } = useOrganizationContext() + if (!viewer.canUseSearchMcp) { + return ( +

+ Your organization’s policy disables Sim Search MCP access. Contact an organization admin to + enable it. +

+ ) + } const endpoint = getSearchMcpUrl(organization.id) return ( diff --git a/apps/sim/app/o/[organizationId]/settings/components/search-mcp-connection.tsx b/apps/sim/app/o/[organizationId]/settings/components/search-mcp-connection.tsx index c19fa601a15..1bcec157d2b 100644 --- a/apps/sim/app/o/[organizationId]/settings/components/search-mcp-connection.tsx +++ b/apps/sim/app/o/[organizationId]/settings/components/search-mcp-connection.tsx @@ -3,8 +3,8 @@ import { useState } from 'react' import { Chip, - ChipDropdown, ChipModalField, + ChipSelect, Code, chipFieldSurfaceClass, useCopyToClipboard, @@ -38,18 +38,20 @@ export function SearchMcpConnection({ endpoint }: SearchMcpConnectionProps) { return ( <> - { - const option = CLIENTS.find((item) => item.value === value) - if (option) setClient(option.value) - }} - options={CLIENTS} - aria-label={`MCP app: ${CLIENTS.find((option) => option.value === client)?.label}`} - align='start' - matchTriggerWidth={false} - className='self-start' - /> +
+ { + const option = CLIENTS.find((item) => item.value === value) + if (option) setClient(option.value) + }} + options={[...CLIENTS]} + aria-label={`MCP app: ${CLIENTS.find((option) => option.value === client)?.label}`} + align='start' + fullWidth + dropdownWidth='trigger' + /> +
{client !== 'cursor' ? ( { ) await click('Advanced') expect(container.textContent).toContain( - 'A sync configuration controls what gets indexed and how often.' + 'No accounts connected yet. A sync configuration will be created when someone connects.' ) expect(container.textContent).toContain('Add sync configuration') await click('Add sync configuration') diff --git a/apps/sim/app/o/[organizationId]/settings/integrations/providers/[connectorType]/provider-detail.tsx b/apps/sim/app/o/[organizationId]/settings/integrations/providers/[connectorType]/provider-detail.tsx index d1002eb31b2..eda0203eca3 100644 --- a/apps/sim/app/o/[organizationId]/settings/integrations/providers/[connectorType]/provider-detail.tsx +++ b/apps/sim/app/o/[organizationId]/settings/integrations/providers/[connectorType]/provider-detail.tsx @@ -221,12 +221,6 @@ export function OrganizationProviderDetail({ connectorType }: OrganizationProvid const renderSources = () => ( - {automaticSetup && ( - - A sync configuration controls what gets indexed and how often. Connecting the first - account creates the default configuration automatically. - - )} {approval.error && ( {approval.error.message} diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/source-card/source-card.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/source-card/source-card.tsx index 97c9c683b3e..b339c1a91fc 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/source-card/source-card.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/source-card/source-card.tsx @@ -16,6 +16,7 @@ import { import { BRAND_ICON_BY_BASE_TYPE, sourceLabel, + sourceSiteName, } from '@/app/workspace/[workspaceId]/home/components/message-content/components/source-chip' import type { SourceTagData } from '@/app/workspace/[workspaceId]/home/components/message-content/components/special-tags' import { BrandIcon } from '@/blocks/brand-icon' @@ -133,7 +134,7 @@ export function SourceCard({ source, query, onSummarize, dense = false }: Source : undefined const updatedAt = parseUpdatedAt(source.updatedAt) const meta = [ - sourceLabel(source), + sourceSiteName(source), source.author?.trim() || null, updatedAt ? formatDate(updatedAt) : null, ].filter((part): part is string => Boolean(part)) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/source-chip/index.ts b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/source-chip/index.ts index 329fa2848eb..612c5e37cc5 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/source-chip/index.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/source-chip/index.ts @@ -1 +1,6 @@ -export { BRAND_ICON_BY_BASE_TYPE, SourceChip, sourceLabel } from './source-chip' +export { + BRAND_ICON_BY_BASE_TYPE, + SourceChip, + sourceLabel, + sourceSiteName, +} from '@/app/workspace/[workspaceId]/home/components/message-content/components/source-chip/source-chip' diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/source-chip/source-chip.test.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/source-chip/source-chip.test.tsx new file mode 100644 index 00000000000..fbba6a94034 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/source-chip/source-chip.test.tsx @@ -0,0 +1,81 @@ +/** @vitest-environment jsdom */ +import { act } from 'react' +import { createRoot, type Root } from 'react-dom/client' +import { afterEach, describe, expect, it, vi } from 'vitest' + +vi.mock('@/lib/browser-agent/open-in-panel', () => ({ + shouldOpenInBrowserPanel: () => false, + openInBrowserPanel: vi.fn(), +})) +vi.mock('@/lib/integrations', () => ({ blockTypeToIconMap: {} })) + +import { SourceCard } from '@/app/workspace/[workspaceId]/home/components/message-content/components/source-card' +import { SourceChip } from '@/app/workspace/[workspaceId]/home/components/message-content/components/source-chip' +import type { SourceTagData } from '@/app/workspace/[workspaceId]/home/components/message-content/components/special-tags' + +let root: Root | null = null +let container: HTMLDivElement | null = null + +function mount(ui: React.ReactNode) { + ;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true + container = document.createElement('div') + document.body.appendChild(container) + root = createRoot(container) + act(() => root?.render(ui)) + return container +} + +afterEach(() => { + if (root) act(() => root?.unmount()) + container?.remove() + root = null + container = null +}) + +describe('citation labels', () => { + it.each([ + { + url: 'https://mail.google.com/mail/u/0/#all/thread', + title: 'Launch checklist', + siteName: 'Sim Search', + connectorType: 'gmail', + }, + { + url: 'https://example.slack.com/archives/channel/message', + title: '#engineering — release handoff', + siteName: 'Slack', + connectorType: 'slack', + }, + { + url: 'https://docs.github.com/page', + title: 'Managing repositories', + siteName: 'GitHub Docs', + }, + ])('uses the retrieved title for $url', (source: SourceTagData) => { + const view = mount() + expect(view.querySelector('a')?.textContent).toBe(source.title) + expect(view.querySelector('a')?.getAttribute('href')).toBe(source.url) + }) + + it.each([ + [{ url: 'https://mail.google.com/thread', title: ' ', siteName: 'Gmail' }, 'Gmail'], + [{ url: 'https://www.example.com/page' }, 'example.com'], + ] as const)('keeps a readable fallback without a title', (source, expected) => { + expect(mount().querySelector('a')?.textContent).toBe(expected) + }) + + it('keeps the provider separate from the source card title', () => { + const view = mount( + + ) + expect(view.querySelector('[data-source-link]')?.textContent).toBe('Launch checklist') + expect(view.textContent?.match(/Launch checklist/g)).toHaveLength(1) + expect(view.textContent).toContain('Gmail') + }) +}) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/source-chip/source-chip.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/source-chip/source-chip.tsx index 75a276e46a2..8e3ae251d43 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/source-chip/source-chip.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/source-chip/source-chip.tsx @@ -23,20 +23,25 @@ export const BRAND_ICON_BY_BASE_TYPE: ReadonlyMap = new M Object.entries(blockTypeToIconMap).map(([type, icon]) => [stripVersionSuffix(type), icon]) ) -/** Chip label: the site name the model supplied, else the URL's hostname without a `www.` prefix. */ -export function sourceLabel(source: SourceTagData): string { +/** The source's site or provider, separate from its document title. */ +export function sourceSiteName(source: SourceTagData): string { const siteName = source.siteName?.trim() if (siteName) return siteName return (externalLinkHostname(source.url) ?? source.url).replace(/^www\./, '') } +/** Citations identify the document; source metadata is the fallback when its title is unavailable. */ +export function sourceLabel(source: SourceTagData): string { + return source.title?.trim() || sourceSiteName(source) +} + interface SourceChipProps { source: SourceTagData } /** * A cited document as a small round pill — the connector's brand mark or the - * site favicon, then the site name — used inline at the citation point and + * site favicon, then the document title — used inline at the citation point and * again in the footer strip. Built on the chip fill and hover tokens at a 20px * height so it sits inside a line of prose; the 30px `Chip` is the wrong scale * for a citation. Opens the document like any external link in the reply. diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx index 0330603d98c..9130d67c55e 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx @@ -327,11 +327,11 @@ export interface WorkspaceResourceTagData { export interface SourceTagData { /** Canonical http(s) link to the referenced document. */ url: string - /** Document title, shown on hover. */ + /** Document title, used as the citation label and shown in full on hover. */ title?: string /** - * Short chip label — the site or product the document lives in ("GitHub - * Docs", "Confluence"). Falls back to the URL's hostname. + * The site or product the document lives in ("GitHub Docs", "Confluence"), + * used when its title is missing and as secondary metadata in source cards. */ siteName?: string /** diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/resolve-citations.test.ts b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/resolve-citations.test.ts index f9cb6f66bbe..dc297b1df74 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/resolve-citations.test.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/resolve-citations.test.ts @@ -67,6 +67,37 @@ describe('evidence-linked citations', () => { ).blocks[1].content ).toEqual(resolveMessageCitations(blocks(), '', true).blocks[1].content) }) + it('retains a retrieved provider label after persistence instead of the internal index name', () => { + const providerOutput = structuredClone(output) + Object.assign(providerOutput.data.results[0], { + knowledgeBaseName: 'Sim Search', + siteName: 'Gmail', + connectorType: 'gmail', + }) + for (const result of [ + providerOutput, + compactRetrievalCitations('search_workspace', providerOutput), + ]) { + const resolved = resolveMessageCitations(blocks(result), '', true).blocks[1].content + expect(resolved).toContain('"title":"Actual title"') + expect(resolved).toContain('"siteName":"Gmail"') + expect(resolved).not.toContain('Sim Search') + } + }) + + it('keeps the document title when a follow-up uses only read_document evidence', () => { + const readBlocks = blocks({ + success: true, + data: { + ...output.data.results[0], + chunks: [{ content: 'Retrieved passage', chunkIndex: 0 }], + }, + }) + readBlocks[0].toolCall!.name = 'read_document' + expect(resolveMessageCitations(readBlocks, '', true).blocks[1].content).toContain( + '"title":"Actual title"' + ) + }) it('resolves source tags split across streamed text chunks before rendering', () => { const split = blocks().slice(0, 1) split.push( diff --git a/apps/sim/app/workspace/[workspaceId]/integrations/components/connect-service-account-modal/connect-service-account-modal.test.tsx b/apps/sim/app/workspace/[workspaceId]/integrations/components/connect-service-account-modal/connect-service-account-modal.test.tsx new file mode 100644 index 00000000000..2f33ace6fd8 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/integrations/components/connect-service-account-modal/connect-service-account-modal.test.tsx @@ -0,0 +1,133 @@ +/** @vitest-environment jsdom */ +import { act, type ReactNode } from 'react' +import { createRoot, type Root } from 'react-dom/client' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +const mocks = vi.hoisted(() => ({ create: vi.fn(), update: vi.fn() })) + +vi.mock('@sim/emcn', () => ({ + ChipModal: ({ children }: { children: ReactNode }) =>
{children}
, + ChipModalBody: ({ children }: { children: ReactNode }) =>
{children}
, + ChipModalHeader: ({ children }: { children: ReactNode }) =>
{children}
, + ChipModalError: ({ children }: { children: ReactNode }) =>
{children}
, + ChipModalField: ({ + type, + title, + value, + onChange, + }: { + type: string + title: string + value?: string + onChange: (value: string) => void + }) => + type === 'file' ? null : ( +