feat(copilot): service account setup & reconnect in chat#5786
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
PR SummaryMedium Risk Overview Agent discovery & branching: VFS OAuth auth and Shared UI logic: New Docs: add-block skills/commands document Reviewed by Cursor Bugbot for commit a14dae0. Bugbot is set up for automated code reviews on this repo. Configure here. |
Greptile SummaryThis PR adds service-account setup and reconnect flows to Copilot chat. The main changes are:
Confidence Score: 5/5This looks safe to merge.
Important Files Changed
Reviews (5): Last reviewed commit: "refactor(credentials): route the editor ..." | Re-trigger Greptile |
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).
|
@greptile review |
|
@cursor review |
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).
|
@greptile review |
|
@cursor review |
…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.
|
@greptile review |
|
@cursor review |
There was a problem hiding this comment.
✅ 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.
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.
523c4e9 to
8350013
Compare
…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.
|
@greptile review |
|
@cursor review |
There was a problem hiding this comment.
✅ 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.
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.
service_accountcredential tag —<credential>{"type":"service_account","provider":"slack"}</credential>— directly from intent, likesecret_input. No tool call: the tag carries only a provider name (nothing secret), so the button streams in with the message.ConnectServiceAccountModalover 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.serviceAccountfield on eachoauth-integrations.jsonentry (and each op'sauth.serviceAccount) tells the agent which services support it, plus theconnectNounfor what to prepare. GA-only, so preview-gated flows aren't proactively offered.google,atlassian) resolve to a canonical slug.credentialIdto rotate an existing service account's secret in place (id preserved).credentials.jsonnow carries each credential'stypeso the agent branches — service accounts reconnect via the tag, OAuth viaoauth_get_auth_link.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_linkrejects 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
Testing
Tested manually;
bun run lint,check:api-validation:strict,mship-tools:check, andtsc --noEmitclean. Unit tests for the provider resolver (incl. normalized/readable forms and canonical families), the VFSserviceAccountdiscovery field, the preview gate (preview → omit, GA → include, missing → fail-closed), theservice_accountchat tag (validation, reconnectcredentialId, blank rejection), and theoauth_get_auth_linkservice-account-id guard.Checklist