feat(copilot): service account setup via a mothership tool#5786
feat(copilot): service account setup via a mothership tool#5786TheodoreSpeaks wants to merge 6 commits into
Conversation
Resolves a loosely-specified integration name to the catalog slug whose
detail page mounts ConnectServiceAccountModal, and returns
`/integrations/{slug}?connect=service-account`. The agent surfaces it via
the existing <credential type="link"> tag, so the user gets a Connect
button and supplies the key material in Sim's own form — the agent never
handles the secret.
Exact matches beat fuzzy ones so a caller naming a specific service lands
on it (gmail stays Gmail rather than collapsing to Drive), and family
names resolve through an explicit canonical map rather than to whichever
member sorts first.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Phx1MLjf8Ui3M3VpwisZds
The fuzzy provider match falls back to substring containment, so `slack-custom-bot` contains `slack` and resolved to the Slack OAuth service. The tool then returned a personal-OAuth authorize URL and reported success — a user who asked for a shared custom bot got a Connect button that linked their own account instead. Every service account id degraded this way (notion-, salesforce-, zoom-, linear-), always silently. Guard runs before the fuzzy pass and points at service_account_get_setup_link. Keys off the id being a service-account id, not off the integration offering one, so `slack` and `notion` still resolve for OAuth. Moves the narrowing predicate out of the integration catalog module so callers that need only the predicate skip the integrations.json load and the OAUTH_PROVIDERS walk. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Phx1MLjf8Ui3M3VpwisZds
…ng out
The tool handed back a /integrations/{slug}?connect=service-account URL,
so accepting the agent's offer navigated away from the conversation that
asked for the credential. Adds a `service_account` credential tag that
mounts ConnectServiceAccountModal over the chat; setup_url stays as the
headless/MCP fallback.
The tag carries a provider and no value — the secret is typed into Sim's
own form and never enters the transcript — so the validator gets a branch
alongside secret_input/sim_key rather than falling through to the
value-required check.
Extracts useServiceAccountConnectTarget so the chat and the integrations
page share one source of truth for the connect label and the preview
gate. Custom Slack bots ride the slack_v2 flag; without the shared gate
the chat would have surfaced a setup form the integrations page hides.
Modal is lazy-loaded off the deep path (not the barrel) to keep three
provider-specific setup forms out of the chat's initial chunk.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Phx1MLjf8Ui3M3VpwisZds
…he UI The in-chat connect button hides itself when the provider's gating block is preview-hidden (a custom Slack bot needs slack_v2). The tool didn't check this, so it returned success for slack-custom-bot even when slack_v2 was preview-gated off — the agent said "here's the setup form" and the button silently rendered nothing, leaving the user with no form at all. Adds getServiceAccountGatingBlockType as the single source for the provider→gating-block mapping, consumed by both the tool (server-side, via getBlockVisibilityForCopilot) and the connect hook (client overlay). When the gating block is hidden the tool now fails with a fall-back-to-OAuth message instead of promising an invisible form. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Phx1MLjf8Ui3M3VpwisZds
Removes the VFS auth-metadata exposure and returns connectNoun from the service_account_get_setup_link result instead. The VFS aggregate was a second, viewer-independent source of truth that couldn't agree with the per-viewer preview gate (it always hid slack-custom-bot, even for viewers with slack_v2 revealed, while the tool accepts it for them). The tool now resolves the provider, applies the per-viewer gate, and returns either the in-chat button + connectNoun or a fall-back-to-oauth error — one source of truth. connectNoun stays DRY via getServiceAccountConnectNoun, shared with the connect-button label.
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
PR SummaryMedium Risk Overview Shared connect logic moves into
Reviewed by Cursor Bugbot for commit 89b2d62. Bugbot is set up for automated code reviews on this repo. Configure here. |
Greptile SummaryThis PR adds service-account setup through the Copilot tool flow. The main changes are:
Confidence Score: 4/5The provider normalization and service-specific tag identity need fixes before merging.
apps/sim/lib/copilot/tools/handlers/oauth.ts; apps/sim/lib/copilot/tools/handlers/service-account.ts Important Files Changed
Reviews (1): Last reviewed commit: "feat(copilot): make the tool own service..." | Re-trigger Greptile |
| if (isServiceAccountProviderId(normalizedInput)) { | ||
| throw new Error( | ||
| `"${providerName}" is a service account, not an OAuth provider. ` + | ||
| `Call service_account_get_setup_link with this provider instead — ` + | ||
| `it returns a link that opens the service account setup form.` | ||
| ) | ||
| } |
There was a problem hiding this comment.
Readable Service-Account Names Bypass Guard
Inputs such as google service account or slack custom bot are not recognized because the predicate only accepts canonical hyphenated IDs. They then reach the fuzzy OAuth resolver and can produce a personal OAuth link for a service-account request, preserving the wrong-account behavior this guard is meant to prevent.
| if (isServiceAccountProviderId(normalizedInput)) { | |
| throw new Error( | |
| `"${providerName}" is a service account, not an OAuth provider. ` + | |
| `Call service_account_get_setup_link with this provider instead — ` + | |
| `it returns a link that opens the service account setup form.` | |
| ) | |
| } | |
| const normalizedServiceAccountInput = normalizedInput.replace(/[\s_]+/g, '-') | |
| if (isServiceAccountProviderId(normalizedServiceAccountInput)) { | |
| throw new Error( | |
| `"${providerName}" is a service account, not an OAuth provider. ` + | |
| `Call service_account_get_setup_link with this provider instead — ` + | |
| `it returns a link that opens the service account setup form.` | |
| ) | |
| } |
| output: { | ||
| message: `Service account setup available for ${match.serviceName}.`, | ||
| instructions: | ||
| `Emit <credential>{"type":"service_account","provider":"${match.providerId}"}</credential> ` + |
There was a problem hiding this comment.
Shared Provider ID Loses Service Identity
The tag embeds match.providerId, but several integrations share an OAuth provider ID. For example, a Google Slides request emits google-drive; the chat resolves that tag as Google Drive and labels the setup button for the wrong service even though the fallback URL correctly targets Slides. Embedding the resolved integration slug preserves the service the user requested.
| `Emit <credential>{"type":"service_account","provider":"${match.providerId}"}</credential> ` + | |
| `Emit <credential>{"type":"service_account","provider":"${match.slug}"}</credential> ` + |
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
The agent now emits the service_account credential tag directly from intent — like secret_input — instead of round-tripping through a tool. Removes service_account_get_setup_link (handler, registration, display title, Go tool def) and restores auth.serviceAccount as the VFS discovery field so the agent knows which providers support a service account. The link-vs-tag distinction was the wrong axis: only oauth needs a tool, because its button carries a minted URL that can't be reconstructed. The service_account tag carries just a provider name the agent already knows, so it needs no tool — discovery lives in the VFS (auth.serviceAccount, GA-only, so slack's preview-gated custom bot is never proactively offered), and the per-viewer gate lives in the renderer, which renders nothing when a provider isn't available for the viewer (no OAuth fallback — a shared credential and a personal one are different intents). oauth_get_auth_link's service-account-id guard now points at the tag.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 3 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 89b2d62. Configure here.
| */ | ||
| export function listServiceAccountIntegrationNames(): string[] { | ||
| return SERVICE_ACCOUNT_INTEGRATIONS.map((entry) => entry.serviceName) | ||
| } |
There was a problem hiding this comment.
Missing service-account setup tool handler
High Severity
The PR centers on service_account_get_setup_link, but no Sim-side handler or registration exists. listServiceAccountIntegrationNames and the gating helper are written for that tool’s discovery/error path, yet nothing calls them, so the mothership tool has no executor to resolve providers, apply the preview gate, or return the in-chat button.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 89b2d62. Configure here.
| `"${providerName}" is a service account, not an OAuth provider. ` + | ||
| `Emit a service_account credential tag with the service's OAuth provider ` + | ||
| `value instead (e.g. "slack") — it opens the service account setup form in chat.` | ||
| ) |
There was a problem hiding this comment.
OAuth rejection skips preview gating
Medium Severity
Rejecting a service-account id tells the agent to emit a service_account credential tag directly. That bypasses the intended gated setup tool, so for preview-gated providers like slack-custom-bot the chat control can render as nothing for viewers without slack_v2, with no fall-back-to-oauth guidance.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 89b2d62. Configure here.
| */ | ||
| export function listServiceAccountIntegrationNames(): string[] { | ||
| return SERVICE_ACCOUNT_INTEGRATIONS.map((entry) => entry.serviceName) | ||
| } |
There was a problem hiding this comment.
Unused service-account name helper
Low Severity
listServiceAccountIntegrationNames is exported for agent-facing error copy but has no production callers—only its definition and unit tests reference it—so it is dead code that will drift from the real handler path.
Reviewed by Cursor Bugbot for commit 89b2d62. Configure here.


Summary
service_account_get_setup_link— the agent hands the user an in-chat button that opens Sim's service-account setup form (Google JSON key, Slack bot, Notion token, …); the user enters the secret in the form, never in chatoauth_get_auth_linkso a custom bot no longer silently degrades to a personal OAuth connectslack_v2), so it never offers a form the UI would hideconnectNoun(what secret to prepare) or a fall-back-to-oauth errorDepends on the mothership (Go) PR that adds the tool to the catalog and agent — the handler here is inert until that ships.
Type of Change
Testing
Tested manually;
bun run lint,check:api-validation:strict, andtsc --noEmitclean; unit tests for the resolver, the tool handler (incl. preview gate), the oauth-id guard, and theservice_accountchat tagChecklist