Skip to content

feat(copilot): service account setup via a mothership tool#5786

Open
TheodoreSpeaks wants to merge 5 commits into
stagingfrom
feat/mothership-service-account
Open

feat(copilot): service account setup via a mothership tool#5786
TheodoreSpeaks wants to merge 5 commits into
stagingfrom
feat/mothership-service-account

Conversation

@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator

Summary

  • Add 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 chat
  • Fuzzy provider resolver (slug / OAuth value / SA id / display name → the integration whose modal to open), with family names resolving to a canonical slug
  • Reject service-account ids in oauth_get_auth_link so a custom bot no longer silently degrades to a personal OAuth connect
  • Gate the tool on the same per-viewer preview flag as the UI (a custom Slack bot needs slack_v2), so it never offers a form the UI would hide
  • The tool owns discovery: resolves the provider, applies the gate, returns the in-chat button + connectNoun (what secret to prepare) or a fall-back-to-oauth error

Depends 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

  • New feature

Testing

Tested manually; bun run lint, check:api-validation:strict, and tsc --noEmit clean; unit tests for the resolver, the tool handler (incl. preview gate), the oauth-id guard, and the service_account chat tag

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

TheodoreSpeaks and others added 5 commits July 20, 2026 11:32
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.
@vercel

vercel Bot commented Jul 21, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped Jul 21, 2026 12:31am

Request Review

@cursor

cursor Bot commented Jul 21, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Touches credential/OAuth routing and copilot tool behavior where mis-resolution could send users to the wrong connect flow; mitigated by explicit rejection, preview gating aligned with UI, and broad unit tests.

Overview
Adds service_account_get_setup_link so the copilot can steer users through service-account setup (Google JSON key, Slack custom bot, Notion token, etc.) without pasting secrets into chat. The handler resolves a loose providerName, returns connectNoun plus instructions to emit <credential>{"type":"service_account","provider":"…"}</credential>, and falls back to an integrations URL only for headless/MCP.

Chat gains a service_account credential tag that lazy-loads ConnectServiceAccountModal in-thread; parsing/validation and streaming suppression mirror other credential tags. useServiceAccountConnectTarget centralizes connect labels and slack_v2 preview gating for both the integrations detail page and chat.

oauth_get_auth_link now rejects service-account provider ids before fuzzy matching (fixing slack-custom-bot degrading to personal OAuth). Shared helpers in service-account-provider-ids and resolveServiceAccountIntegration support resolver canonical slugs and aligned tool/UI gates.

Reviewed by Cursor Bugbot for commit ed3ffad. Bugbot is set up for automated code reviews on this repo. Configure here.

@greptile-apps

greptile-apps Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds service-account setup through the Copilot tool flow. The main changes are:

  • Provider discovery with canonical and fuzzy matching.
  • Workspace permission and preview-flag checks.
  • An inline chat control that opens the service-account modal.
  • OAuth rejection for service-account provider IDs.
  • Generated tool registration and unit coverage.

Confidence Score: 4/5

The provider normalization and service-specific tag identity need fixes before merging.

  • Human-readable service-account names can still fall through to personal OAuth.
  • Shared OAuth provider IDs can label the chat setup control for the wrong integration.
  • Workspace access and preview gating are applied consistently.

apps/sim/lib/copilot/tools/handlers/oauth.ts; apps/sim/lib/copilot/tools/handlers/service-account.ts

Important Files Changed

Filename Overview
apps/sim/lib/copilot/tools/handlers/service-account.ts Adds the setup-link handler, access checks, preview gating, chat instructions, and fallback URL.
apps/sim/lib/integrations/oauth-service.ts Adds service-account integration resolution with exact, canonical-family, and fuzzy matching.
apps/sim/lib/copilot/tools/handlers/oauth.ts Rejects canonical service-account IDs before OAuth matching, but readable variants can bypass the check.
apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx Adds service-account tag validation and a permission-aware, lazily loaded setup modal.
apps/sim/app/workspace/[workspaceId]/integrations/components/connect-service-account-modal/use-service-account-connect.ts Centralizes connect labels and preview visibility for integration and chat surfaces.

Reviews (1): Last reviewed commit: "feat(copilot): make the tool own service..." | Re-trigger Greptile

Comment on lines +115 to +121
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.`
)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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.

Suggested change
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> ` +

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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.

Suggested change
`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!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant