Skip to content

feat(copilot): service account setup & reconnect in chat#5786

Merged
TheodoreSpeaks merged 12 commits into
stagingfrom
feat/mothership-service-account
Jul 22, 2026
Merged

feat(copilot): service account setup & reconnect in chat#5786
TheodoreSpeaks merged 12 commits into
stagingfrom
feat/mothership-service-account

Conversation

@TheodoreSpeaks

@TheodoreSpeaks TheodoreSpeaks commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator

Summary

Lets the agent set up and reconnect service accounts (shared, app-level workspace credentials) directly in chat, alongside the existing OAuth and API-key flows.

  • The agent emits a service_account credential tag — <credential>{"type":"service_account","provider":"slack"}</credential> — directly from intent, like secret_input. No tool call: the tag carries only a provider name (nothing secret), so the button streams in with the message.
  • The tag renders an in-chat connect button that opens ConnectServiceAccountModal over the conversation (Google JSON key, Slack custom bot, Notion token, …). The user enters the secret in Sim's own form — it never touches the transcript — and stays in the chat that asked for it.
  • Discovery lives in the VFS: a serviceAccount field on each oauth-integrations.json entry (and each op's auth.serviceAccount) tells the agent which services support it, plus the connectNoun for what to prepare. GA-only, so preview-gated flows aren't proactively offered.
  • Provider resolver maps a slug / OAuth value / service-account id / display name (space- and underscore-normalized) to the integration whose modal to open; family names (google, atlassian) resolve to a canonical slug.
  • Reconnect: the tag takes an optional credentialId to rotate an existing service account's secret in place (id preserved). credentials.json now carries each credential's type so the agent branches — service accounts reconnect via the tag, OAuth via oauth_get_auth_link.
  • Per-viewer preview gate in the renderer: a gated provider (a custom Slack bot needs slack_v2) renders nothing — no fall-back to a personal OAuth connect, since a shared credential and a personal one are different intents.
  • oauth_get_auth_link rejects a service-account id (normalized) and steers the agent to the tag, so a custom bot can't silently degrade into a personal OAuth link.

Pairs with mothership (Go) PR #361, which teaches the auth agent the tag flow. The renderer here is inert until that ships.

Type of Change

  • New feature

Testing

Tested manually; bun run lint, check:api-validation:strict, mship-tools:check, and tsc --noEmit clean. Unit tests for the provider resolver (incl. normalized/readable forms and canonical families), the VFS serviceAccount discovery field, the preview gate (preview → omit, GA → include, missing → fail-closed), the service_account chat tag (validation, reconnect credentialId, blank rejection), and the oauth_get_auth_link service-account-id guard.

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)

@vercel

vercel Bot commented Jul 21, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
docs Ready Ready Preview, Comment Jul 22, 2026 9:05pm

Request Review

@cursor

cursor Bot commented Jul 21, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Touches credential/OAuth routing and workspace credential serialization used by the agent; mistakes could mis-link auth or expose preview-gated flows, though extensive tests and fail-closed gating mitigate this.

Overview
Enables the copilot to drive shared service account setup and reconnect without leaving chat. The agent emits a service_account <credential> tag (provider only, optional credentialId for rotate-in-place); chat parses/validates it and renders a button that lazily opens ConnectServiceAccountModal, with secrets entered in Sim’s form—not the transcript.

Agent discovery & branching: VFS OAuth auth and oauth-integrations.json gain optional serviceAccount metadata (connectNoun), gated for preview flows (e.g. custom Slack bot ↔ slack_v2). credentials.json includes credential type (oauth vs service_account). oauth_get_auth_link rejects service-account provider ids (normalized) and directs the agent to the tag instead of fuzzy-matching into a personal OAuth URL.

Shared UI logic: New service-account-provider-ids helpers and useServiceAccountConnectTarget centralize connect labels, nouns, and preview hiding across integrations detail, workflow credential pickers, and chat. resolveServiceAccountIntegration maps slugs/provider ids/display names to the right integration modal.

Docs: add-block skills/commands document oauth-input credentialKind and service-account picker behavior.

Reviewed by Cursor Bugbot for commit a14dae0. 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 and reconnect flows to Copilot chat. The main changes are:

  • In-chat service-account credential tags and connection modal.
  • Provider resolution with service-specific and canonical family names.
  • Preview gating across chat, integrations, and credential selectors.
  • Service-account discovery and credential types in the workspace VFS.
  • OAuth guardrails that redirect service-account requests to the new flow.

Confidence Score: 5/5

This looks safe to merge.

  • The readable service-account names now reach the intended rejection path.
  • Specific service tags retain their integration identity before shared-provider fallback.
  • No blocking issue remains in the reviewed updates.

Important Files Changed

Filename Overview
apps/sim/lib/copilot/tools/handlers/oauth.ts Rejects normalized service-account identifiers before OAuth provider resolution.
apps/sim/lib/integrations/oauth-service.ts Resolves service-account tags while preserving specific integration identities.
apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx Renders validated service-account controls and opens the connection modal in chat.
apps/sim/lib/copilot/vfs/serializers.ts Adds gated service-account discovery and credential-type metadata to VFS output.

Reviews (5): Last reviewed commit: "refactor(credentials): route the editor ..." | Re-trigger Greptile

Comment thread apps/sim/lib/copilot/tools/handlers/oauth.ts Outdated
Comment thread apps/sim/lib/copilot/tools/handlers/service-account.ts Outdated
Comment thread apps/sim/lib/integrations/oauth-service.ts Outdated
Comment thread apps/sim/lib/copilot/tools/handlers/oauth.ts Outdated
Comment thread apps/sim/lib/integrations/oauth-service.ts Outdated
Comment thread apps/sim/lib/copilot/tools/handlers/oauth.ts
TheodoreSpeaks added a commit that referenced this pull request Jul 21, 2026
Review round on #5786:
- The service-account-id guard threw into the generic catch, which
  overwrote its recovery hint with a "connect manually" message and a
  workspace oauth_url — contradictory signals. It now returns a coherent
  failure directly, before the try, with no oauth_url.
- Normalize spaces/underscores before the check so a readable form
  ("slack custom bot", "google service account") is caught too, not
  passed to the fuzzy OAuth resolver.
- Remove listServiceAccountIntegrationNames — dead after the tool was
  removed (its only caller was the deleted handler's error copy).
@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator Author

@greptile review

@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator Author

@cursor review

Comment thread apps/sim/lib/copilot/vfs/serializers.ts Outdated
TheodoreSpeaks added a commit that referenced this pull request Jul 21, 2026
Review round on #5786: describeServiceAccountForOAuthProvider used
`getBlock(...)?.preview ?? true`, which treats a GA'd gating block — one
that dropped its `preview` flag, exactly slack_v2's documented migration —
as still gated, so the custom bot would stay omitted from VFS discovery
forever after GA even though the UI shows it. Reuse the canonical
isHiddenUnder(null, block) predicate instead, so a non-preview block is
visible. Adds service-account-gate.test.ts covering preview → omit, GA →
include, and missing → fail-closed with a mocked getBlock (the block
registry is globally stubbed, so the real slack_v2 preview flag isn't
observable through serializeIntegrationSchema).
@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator Author

@greptile review

@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator Author

@cursor review

Comment thread apps/sim/lib/integrations/oauth-service.ts
TheodoreSpeaks added a commit that referenced this pull request Jul 21, 2026
…tialId

Review round on #5786:
- resolveServiceAccountIntegration only lowercased/trimmed, but the
  oauth_get_auth_link guard normalizes spaces/underscores to hyphens
  before rejecting a service-account id and steering the agent to a
  service_account tag. The chat renderer then couldn't resolve those same
  readable forms ("slack custom bot", "notion_service_account") and
  rendered nothing. Apply the same normalization to the id lookups (raw
  query still used for display-name matches).
- service_account tag validation rejected a blank provider but allowed a
  whitespace-only credentialId, which is truthy — the renderer took the
  reconnect path and tried to rotate a non-existent credential. Reject a
  blank/whitespace credentialId.
@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator Author

@greptile review

@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator Author

@cursor review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 523c4e9. Configure here.

@TheodoreSpeaks TheodoreSpeaks changed the title feat(copilot): service account setup via a mothership tool feat(copilot): service account setup & reconnect in chat Jul 22, 2026
TheodoreSpeaks and others added 6 commits July 22, 2026 12:10
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 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.
Reconnect had no service-account path — it required oauth_get_auth_link
and a link tag for every repair, so rotating a workspace service account
either errored or pushed the user through OAuth.

The service_account tag now takes an optional credentialId; when present
the renderer opens the modal in reconnect mode (rotates the secret on that
credential in place, id preserved) and labels the button "Reconnect X".
credentials.json now carries each credential's type (oauth vs
service_account) so the agent can branch: service accounts reconnect via
the tag + credentialId, oauth via oauth_get_auth_link as before.
Review round on #5786:
- The service-account-id guard threw into the generic catch, which
  overwrote its recovery hint with a "connect manually" message and a
  workspace oauth_url — contradictory signals. It now returns a coherent
  failure directly, before the try, with no oauth_url.
- Normalize spaces/underscores before the check so a readable form
  ("slack custom bot", "google service account") is caught too, not
  passed to the fuzzy OAuth resolver.
- Remove listServiceAccountIntegrationNames — dead after the tool was
  removed (its only caller was the deleted handler's error copy).
Review round on #5786: describeServiceAccountForOAuthProvider used
`getBlock(...)?.preview ?? true`, which treats a GA'd gating block — one
that dropped its `preview` flag, exactly slack_v2's documented migration —
as still gated, so the custom bot would stay omitted from VFS discovery
forever after GA even though the UI shows it. Reuse the canonical
isHiddenUnder(null, block) predicate instead, so a non-preview block is
visible. Adds service-account-gate.test.ts covering preview → omit, GA →
include, and missing → fail-closed with a mocked getBlock (the block
registry is globally stubbed, so the real slack_v2 preview flag isn't
observable through serializeIntegrationSchema).
…tialId

Review round on #5786:
- resolveServiceAccountIntegration only lowercased/trimmed, but the
  oauth_get_auth_link guard normalizes spaces/underscores to hyphens
  before rejecting a service-account id and steering the agent to a
  service_account tag. The chat renderer then couldn't resolve those same
  readable forms ("slack custom bot", "notion_service_account") and
  rendered nothing. Apply the same normalization to the id lookups (raw
  query still used for display-name matches).
- service_account tag validation rejected a blank provider but allowed a
  whitespace-only credentialId, which is truthy — the renderer took the
  reconnect path and tried to rotate a non-existent credential. Reject a
  blank/whitespace credentialId.
@TheodoreSpeaks
TheodoreSpeaks force-pushed the feat/mothership-service-account branch from 523c4e9 to 8350013 Compare July 22, 2026 19:17
…al connect hook

The workflow-editor credential selector (from #5800's merged picker) resolved
its service-account setup surface inline and mounted the modal with NO preview
gate — so a `credentialKind: 'service-account'` picker would offer a custom-bot
setup even when slack_v2 is preview-gated off, the leak the integrations page
and chat already guard against.

Route it through the shared useServiceAccountConnectTarget hook (the same
resolver chat and the integrations page use): suppress the setup action when
`hidden`, and use the hook's vendor-accurate label ("Add private app token",
"Set up a custom bot") as the default connect-row copy. Existing service
accounts stay selectable; the per-block `credentialLabels.serviceAccountConnect`
override still wins. One resolver now backs all three SA connect surfaces.
@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator Author

@greptile review

@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator Author

@cursor review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit d1c8c87. Configure here.

The add-block skill had no mention of credentialKind — the mechanism (#5800)
that controls whether an oauth-input offers OAuth, service-account, or a merged
picker — and its example was a plain oauth-input mislabeled "Service Account".
Documents the three credentialKind modes, that a default oauth-input already
lets users select an existing service account (they fold in), and the
credentialLabels / allowServiceAccounts companions. Regenerates the .claude and
.cursor projections.
@TheodoreSpeaks
TheodoreSpeaks merged commit ae7b5ef into staging Jul 22, 2026
19 checks passed
@TheodoreSpeaks
TheodoreSpeaks deleted the feat/mothership-service-account branch July 22, 2026 21:06
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