Skip to content

feat(credentials): agent-initiated oauth credential reconnect#5488

Merged
j15z merged 3 commits into
devfrom
feat/agent-reconnect-credential
Jul 8, 2026
Merged

feat(credentials): agent-initiated oauth credential reconnect#5488
j15z merged 3 commits into
devfrom
feat/agent-reconnect-credential

Conversation

@j15z

@j15z j15z commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • The agent can now generate a Reconnect link for an existing OAuth credential: oauth_get_auth_link accepts an optional credentialId (tool param + prompts land in the companion mothership PR)
  • /api/auth/oauth2/authorize accepts credentialId: requires credential-admin access (same check as the draft POST route), rejects cross-workspace / non-oauth / provider-mismatched ids, and threads the id into the pending credential draft so the existing handleReconnectCredential path rebinds the same credential.id to the fresh account row
  • Draft upsert writes credentialId (or null) on both the insert and conflict paths — a plain connect can never inherit a stale reconnect draft and silently rebind an existing credential
  • Tool handler validates the credential at link-generation time so failures surface in the tool result (agent-visible) instead of a silent browser redirect
  • Trello/Shopify reconnect is rejected on BOTH the tool and the authorize route (their custom authorize flows bypass this endpoint, so a reconnect draft written here would linger and could be consumed by their token-store callbacks) — users are pointed to the integrations page, where reconnect for them works as before
  • Reconnect drafts carry the credential's actual display name so audit records stay accurate (page parity)
  • Extracted the connect modal's display-name collision numbering into lib/credentials/display-name.ts and applied it to copilot-path connects — agent-created credentials get "Justin's Gmail 2" instead of identical duplicate names; name-lookup failures degrade to the un-numbered default with a logger.warn
  • Chat credential chip renders "Reconnect {credential display name}" when the link URL carries a credentialId (derived from the URL, not from model-emitted tag fields)
  • ensureWorkspaceAccess now returns the resolved WorkspaceAccess (previously discarded) so the tool handler reuses it for the credential-admin lookup instead of re-querying

Merge order: this PR merges before the mothership companion — the new tool param crosses the tool-catalog codegen boundary.

Type of Change

  • Bug fix
  • New feature
  • Breaking change
  • Documentation
  • Other: ___________

Testing

  • 37 new unit tests: authorize route (reconnect authz, provider mismatch, custom-flow provider rejection, draft cross-contamination, audit display name), tool handler validation, display-name behavior pinning, chip label; neighboring suites green (780 tests)
  • bun run lint clean, bun run check:api-validation:strict passed, tsc clean
  • Manual E2E: reconnect keeps credential.id, swaps the account row, deletes the orphaned account, dependent workflow still runs

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)

Screenshots/Videos

🤖 Generated with Claude Code

Companion PRs

  • simstudioai/mothership#344

@vercel

vercel Bot commented Jul 7, 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 8, 2026 10:04pm

Request Review

@cursor

cursor Bot commented Jul 7, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Changes OAuth authorize and pending-draft semantics (token rebind + draft credentialId clearing); authz is tightened with credential-admin checks and explicit guards against cross-contamination.

Overview
Adds OAuth reconnect end-to-end: optional credentialId on /api/auth/oauth2/authorize and on copilot oauth_get_auth_link, so completing OAuth rebinds an existing credential instead of always creating a new one.

The authorize route enforces credential-admin access (not just workspace write), validates workspace/type/provider match, blocks Trello/Shopify, and writes credentialId on pending draft insert/upsert—including null on plain connect so a stale reconnect draft cannot silently rebind. Connect-path drafts also get collision-numbered display names via shared defaultCredentialDisplayName (modal + server).

The copilot handler validates reconnect up front (agent-visible errors), threads credentialId into the authorize URL, and ensureWorkspaceAccess now returns WorkspaceAccess for reuse. Chat credential link chips show “Reconnect {name}” when the URL includes credentialId. Broad unit test coverage for authorize, tool handler, display names, and chips.

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

@j15z

j15z commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator Author

@greptile

@j15z

j15z commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator Author

@cursor review

@github-actions github-actions Bot added the requires-mothership-merge Has a companion PR on the mothership/copilot side — merge in lockstep label Jul 7, 2026
@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown

⚠️ Cross-repo companion check

One or more companion PRs aren't merged into staging yet. Merging this without them will leave copilot and sim out of sync — merge them in lockstep.

  • simstudioai/mothership#344OPEN, not merged (targets dev) — feat(tools): reconnect support for oauth_get_auth_link via credentialId

@greptile-apps

greptile-apps Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds agent-initiated OAuth credential reconnect: the oauth_get_auth_link tool and /api/auth/oauth2/authorize route now accept an optional credentialId that, when present, rebinds an existing credential to a freshly authorized account rather than creating a new one.

  • Route handler: /api/auth/oauth2/authorize validates credentialId (admin access, workspace membership, OAuth type, provider match) and threads it into the draft so the existing handleReconnectCredential path picks it up; the draft upsert explicitly writes credentialId: null on both insert and conflict paths for plain connects, preventing stale reconnect drafts from leaking.
  • Tool handler: oauth.ts now returns the WorkspaceAccess from ensureWorkspaceAccess and reuses it in getCredentialActorContext to avoid a double fetch; validation in the tool path surfaces errors agent-side before the user ever opens a URL.
  • Display-name logic extracted from the connect modal into lib/credentials/display-name.ts and reused across the route handler and modal; reconnect drafts skip collision numbering and use the credential's actual name for audit accuracy.

Confidence Score: 5/5

Safe to merge — all three layers of the reconnect flow (tool handler, authorize route, draft upsert) are correctly validated and the previously identified issues have been addressed.

The reconnect path is validated at two independent checkpoints (tool handler before URL generation, route handler before draft creation). The draft upsert explicitly clears credentialId on the conflict path for plain connects, closing the stale-draft cross-contamination window. The audit log display-name issue noted in previous review threads is fixed by using the credential's actual displayName directly for reconnect drafts, bypassing collision numbering. Thirty-one new unit tests cover authz edge cases, provider mismatches, and display-name behavior. No logic gaps found.

No files require special attention.

Important Files Changed

Filename Overview
apps/sim/app/api/auth/oauth2/authorize/route.ts Core reconnect logic: adds credentialId validation (admin, workspace, type, provider), threads reconnect display name through to audit, and explicitly nulls credentialId in the upsert conflict set. Well-structured and correct.
apps/sim/lib/copilot/tools/handlers/oauth.ts Tool handler now returns WorkspaceAccess from ensureWorkspaceAccess and threads it to getCredentialActorContext, eliminating the previously-flagged double-fetch. Reconnect validation mirrors the route handler, surfacing errors agent-side before click.
apps/sim/lib/copilot/tools/handlers/access.ts Return type changed from void to WorkspaceAccess; all existing branches updated. Minimal, correct change.
apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx CredentialDisplay split into CredentialLinkDisplay (link type) + remaining; reconnect credentialId derived from URL searchParams and used to show 'Reconnect {displayName}' label. Hook ordering is correct.
apps/sim/lib/credentials/display-name.ts New shared utility extracted from the connect modal. Logic is unchanged; exports DISPLAY_NAME_MAX_LENGTH for tests. Clean extraction.
apps/sim/lib/api/contracts/oauth-connections.ts Adds optional credentialId (min length 1) to authorizeOAuth2QuerySchema. Correct and minimal.
apps/sim/app/workspace/[workspaceId]/components/connect-oauth-modal/connect-oauth-modal.tsx Removes the inlined defaultDisplayName and replaces it with the shared defaultCredentialDisplayName import. Behavior is identical.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Agent as Copilot Agent
    participant Tool as oauth.ts (Tool Handler)
    participant Route as /api/auth/oauth2/authorize
    participant DB as DB (pendingCredentialDraft)
    participant Provider as OAuth Provider

    Agent->>Tool: "oauth_get_auth_link({ providerName, credentialId })"
    Tool->>Tool: ensureWorkspaceAccess(workspaceId, userId, 'write')
    Tool->>Tool: "getCredentialActorContext(credentialId, userId, { workspaceAccess })"
    Note over Tool: Validate: admin, workspace, type, provider match
    Tool-->>Agent: "{ oauth_url: /authorize?...&credentialId=cred-1 }"

    Agent-->>User: Renders "Reconnect Justin's Gmail" chip
    User->>Route: "GET /api/auth/oauth2/authorize?credentialId=cred-1"
    Route->>Route: checkWorkspaceAccess
    Route->>Route: getCredentialActorContext(credentialId) → isAdmin check
    Route->>Route: validate workspace / type / provider
    Route->>DB: "INSERT draft { credentialId: 'cred-1', displayName: credential.displayName }"
    Note over DB: onConflict → SET credentialId explicitly (nulls on plain connect)
    Route->>Provider: redirect to OAuth provider
    Provider-->>Route: OAuth callback
    Route->>DB: handleReconnectCredential → rebind existing credential.id to new account
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant Agent as Copilot Agent
    participant Tool as oauth.ts (Tool Handler)
    participant Route as /api/auth/oauth2/authorize
    participant DB as DB (pendingCredentialDraft)
    participant Provider as OAuth Provider

    Agent->>Tool: "oauth_get_auth_link({ providerName, credentialId })"
    Tool->>Tool: ensureWorkspaceAccess(workspaceId, userId, 'write')
    Tool->>Tool: "getCredentialActorContext(credentialId, userId, { workspaceAccess })"
    Note over Tool: Validate: admin, workspace, type, provider match
    Tool-->>Agent: "{ oauth_url: /authorize?...&credentialId=cred-1 }"

    Agent-->>User: Renders "Reconnect Justin's Gmail" chip
    User->>Route: "GET /api/auth/oauth2/authorize?credentialId=cred-1"
    Route->>Route: checkWorkspaceAccess
    Route->>Route: getCredentialActorContext(credentialId) → isAdmin check
    Route->>Route: validate workspace / type / provider
    Route->>DB: "INSERT draft { credentialId: 'cred-1', displayName: credential.displayName }"
    Note over DB: onConflict → SET credentialId explicitly (nulls on plain connect)
    Route->>Provider: redirect to OAuth provider
    Provider-->>Route: OAuth callback
    Route->>DB: handleReconnectCredential → rebind existing credential.id to new account
Loading

Reviews (3): Last reviewed commit: "fix(credentials): address reconnect revi..." | Re-trigger Greptile

Comment thread apps/sim/app/api/auth/oauth2/authorize/route.ts Outdated
@greptile-apps

greptile-apps Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds agent-initiated OAuth credential reconnect: the oauth_get_auth_link tool and the /api/auth/oauth2/authorize route both accept an optional credentialId that threads through the pending credential draft so the existing handleReconnectCredential path rebinds the credential in place. It also extracts display-name collision-numbering into a shared lib/credentials/display-name.ts and updates the chat credential chip to show "Reconnect {name}" when the link URL carries a credentialId.

  • Authorization route (route.ts): validates credential ownership, workspace membership, OAuth type, provider match, and admin role before creating a reconnect draft; upsert explicitly sets credentialId: null on the conflict path so a stale reconnect draft cannot leak into a plain connect.
  • Tool handler (oauth.ts): validates the same constraints at link-generation time so failures surface in the tool result; Trello/Shopify reconnect is blocked with a pointer to the integrations page.
  • Chat chip (special-tags.tsx): derives reconnectCredentialId from the URL rather than model-emitted fields and resolves the display name via useWorkspaceCredential.

Confidence Score: 4/5

The reconnect authorization chain is well-guarded with redundant credential-admin checks at both the tool-handler and route layers. The main imprecision is in the audit log for reconnects, where the draft display name gets a collision suffix because the credential being reconnected is counted in takenNames.

Cross-workspace attacks, non-admin reconnects, type and provider mismatches, and draft cross-contamination are all explicitly checked and tested. The two issues found are the misleading audit log name during reconnects and a redundant workspace-access DB round-trip in the tool handler — neither affects credential data or user-facing behavior.

apps/sim/app/api/auth/oauth2/authorize/route.ts — the createConnectDraft call in the reconnect path should skip or narrow collision detection so the draft's displayName matches the existing credential name for correct audit logs.

Important Files Changed

Filename Overview
apps/sim/app/api/auth/oauth2/authorize/route.ts Adds credentialId query param support to the authorize GET route; validates credential ownership, workspace membership, and provider match before creating a reconnect draft. The draft's displayName is incorrectly collision-checked against all credentials (including the one being reconnected), producing a suffixed name in the audit log while leaving the actual credential name unchanged.
apps/sim/lib/copilot/tools/handlers/oauth.ts Extends the OAuth tool handler with reconnect validation (credential existence, workspace, type, provider, admin checks) before embedding credentialId in the authorize URL. Calls getCredentialActorContext without the pre-fetched workspace access, causing a redundant DB round-trip per reconnect link generation.
apps/sim/lib/credentials/display-name.ts New module extracting the defaultCredentialDisplayName helper from the connect modal into a shared library; clean extraction with unchanged logic and full unit test coverage.
apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx Extracts link rendering into CredentialLinkDisplay; derives reconnect label from the URL's credentialId param via useWorkspaceCredential rather than trusting model-emitted fields, with safe URL parsing and fallback to provider ID while loading.
apps/sim/lib/api/contracts/oauth-connections.ts Adds optional credentialId field (min-length 1) to authorizeOAuth2QuerySchema; straightforward contract extension.

Reviews (2): Last reviewed commit: "feat(credentials): agent-initiated oauth..." | Re-trigger Greptile

Comment thread apps/sim/app/api/auth/oauth2/authorize/route.ts Outdated
Comment thread apps/sim/lib/copilot/tools/handlers/oauth.ts Outdated
Comment thread apps/sim/app/api/auth/oauth2/authorize/route.ts Outdated
@j15z j15z force-pushed the feat/agent-reconnect-credential branch from 91cec7f to 24ef2cf Compare July 7, 2026 21:39
@j15z j15z changed the base branch from staging to dev July 7, 2026 21:39
@j15z

j15z commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator Author

@greptile

@j15z

j15z commented Jul 7, 2026

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 24ef2cf. Configure here.

@j15z j15z force-pushed the feat/agent-reconnect-credential branch from d1dd5c0 to 1aad91b Compare July 8, 2026 22:04
@j15z j15z merged commit 040dea9 into dev Jul 8, 2026
16 checks passed
@j15z j15z deleted the feat/agent-reconnect-credential branch July 8, 2026 22:12
Sg312 pushed a commit that referenced this pull request Jul 8, 2026
* feat(credentials): agent-initiated oauth credential reconnect

* fix(credentials): address reconnect review findings

* improvement(credentials): log when connect draft name lookups degrade
Sg312 pushed a commit that referenced this pull request Jul 9, 2026
* feat(credentials): agent-initiated oauth credential reconnect

* fix(credentials): address reconnect review findings

* improvement(credentials): log when connect draft name lookups degrade
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

requires-mothership-merge Has a companion PR on the mothership/copilot side — merge in lockstep

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant