Skip to content

feat(extensions): connect, reconnect, and disconnect org connections from the desktop - #3258

Merged
benjaminshafii merged 3 commits into
devfrom
feat/org-connection-lifecycle-desktop
Jul 29, 2026
Merged

feat(extensions): connect, reconnect, and disconnect org connections from the desktop#3258
benjaminshafii merged 3 commits into
devfrom
feat/org-connection-lifecycle-desktop

Conversation

@benjaminshafii

Copy link
Copy Markdown
Member

What

Per-member org MCP connections (e.g. Granola, shared by your organization) previously showed a hardcoded Connected status in Settings → Extensions and offered no lifecycle actions — members could not connect, disconnect, or re-authorize an external connection from the desktop at all (the working connect() in useOrgMcpConnections was never wired to any UI, and disconnect was restricted to native Google Workspace / Microsoft 365 providers).

The Extensions connection detail page now:

  • shows an honest status (Not connected / Connected) derived from real per-member readiness,
  • offers Connect your account (browser OAuth handoff + 2s polling until Den records the member credential),
  • offers Reconnect when connected (forces a fresh authorization: member-scoped disconnect, then a new connect/start round trip — required because connect/start short-circuits to "already connected" while the stored token is valid),
  • offers Disconnect (calls the existing Den POST /v1/mcp-connections/:id/disconnect-my-account; native providers keep their oauth-providers disconnect route),
  • keeps shared org-account connections admin-managed (no member actions), and hides member actions when reconnect is owned by an organization admin (reconnectActionOwner === "organization_admin").

Also includes:

  • fix(den-db): MariaDB aliases JSON to LONGTEXT so mysql2 returns strings where MySQL returns objects; the external-MCP credential columns (oauth_configuration, credential_health, scopes, extra) now parse string driver values. Without this, every per-member OAuth callback fails with MCP_OAUTH_CONFIGURATION_REQUIRED on MariaDB-backed Den deployments (verified: identical curl dance passes on MySQL 8, failed on MariaDB 11.8 until this fix).
  • New voiceover + coded eval flow org-connection-lifecycle-desktop proving the full lifecycle end-to-end.

How it was verified (Daytona, real e2e)

Two Daytona sandboxes: a Den server sandbox (test-server-on-daytona.sh, MariaDB + den-api/den-web/worker-proxy, seeded demo org) and an Electron sandbox (test-on-daytona.sh --den-base-url ... --den-api-base-url ...), plus the mock OAuth MCP server (scripts/mock-oauth-mcp-server.mjs) hosted on the Electron sandbox behind a public preview URL so den-api performs real RFC 9728 discovery + DCR + token exchange against it, and the sandbox's real Chromium completes the browser round trip via xdg-open.

OPENWORK_EVAL_DAYTONA_SANDBOX=openwork-test-… \
OPENWORK_EVAL_DEN_API_URL=https://8788-….daytonaproxy01.net \
OPENWORK_EVAL_DEN_WEB_URL=https://3005-….daytonaproxy01.net \
OPENWORK_EVAL_MEMBER_EMAIL=alex@acme.test \
OPENWORK_EVAL_LIFECYCLE_MOCK_PUBLIC_URL=https://3979-….daytonaproxy01.net \
pnpm fraimz --flow org-connection-lifecycle-desktop --cdp-url https://9825-….daytonaproxy01.net

Result: PASSED — all 6 frames + voice-over coverage (frame-by-frame proof posted below). Server-side witnesses per frame: mock IdP request log (GET /authorize with signed state + DCR client), Den connectedForMe / fresh connectedAt after reconnect, connectedForMe: false after disconnect.

Also ran:

  • pnpm --filter @openwork/app typecheck — clean
  • apps/app: bun test tests/ — 493 pass, 0 fail
  • apps/app: bun test src/react-app/domains/connections/ — 23 pass, 0 fail (new lifecycle-helper coverage)
  • pnpm --filter @openwork-ee/den-db build, pnpm --filter @openwork-ee/den-api build — clean

Note: frame 1's screenshot shows an unrelated transient toast (opencode_unconfigured) from the freshly created eval workspace booting without a runtime — unrelated to this change.

…from the desktop detail page

Per-member org MCP connections (e.g. Granola) previously showed a misleading Connected status with no lifecycle actions in Settings > Extensions. The detail page now reflects real readiness, offers Connect your account (browser OAuth + poll), Reconnect (fresh authorization via disconnect-my-account + connect/start), and Disconnect (member-scoped credential removal) for per-member connections. Shared org-account connections stay admin-managed.
…iaDB

MariaDB aliases JSON to LONGTEXT, so mysql2 returns raw strings where MySQL returns parsed objects. The external MCP OAuth path read oauthConfiguration as a string, dropping the bound issuer and failing every per-member connect callback with MCP_OAUTH_CONFIGURATION_REQUIRED on MariaDB-backed Den deployments. Also harden the lifecycle eval flow for split web/api origins and an externally hosted mock provider.
Accept the shared-v1 OAuth callback shape, wait for lifecycle buttons to re-enable between actions, and capture the sandbox desktop for the actions frame so it cannot duplicate the connected frame.
@vercel

vercel Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

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

Project Deployment Actions Updated (UTC)
openwork-app Ready Ready Preview, Comment Jul 28, 2026 11:31pm
openwork-den Ready Ready Preview, Comment Jul 28, 2026 11:31pm
openwork-den-worker-proxy Ready Ready Preview, Comment Jul 28, 2026 11:31pm
openwork-diagnostics Ready Ready Preview, Comment Jul 28, 2026 11:31pm
openwork-landing Ready Ready Preview, Comment, Open in v0 Jul 28, 2026 11:31pm

@benjaminshafii

Copy link
Copy Markdown
Member Author

fraimz — ✅ PASSED

1 passed · 0 failed · 0 skipped — run 2026-07-28T23-31-02-378Z

Full frame proof with validated screenshots: evals/results/2026-07-28T23-31-02-378Z/fraimz.html (re-run: pnpm fraimz --flow org-connection-lifecycle-desktop)

✅ org-connection-lifecycle-desktop — Desktop Extensions: connect, reconnect, and disconnect a per-member org connection

User-facing flow demo

  1. Extensions lists the org connection under Needs your sign-in, and its detail page shows an honest Not connected status with a Connect your account button

    🎙 Jordan opens Extensions and filters to Connections — the org-shared connection is sitting under "Needs your sign-in", and opening it shows an honest status: Not connected, OAuth required, with a Connect your account button right in the details.

    • ✅ Visible text includes "NEEDS YOUR SIGN-IN"
    • ✅ Visible text includes "Meeting Notes 1785281462349"
    • ✅ Visible text includes "Not connected"
    • ✅ Visible text includes "OAuth required"
    • ✅ Visible text includes "Connect your account"
  2. The org connection is listed as needing sign-in and its detail page is honestly not connected.

    🎙 Jordan opens Extensions and filters to Connections — the org-shared connection is sitting under "Needs your sign-in", and opening it shows an honest status: Not connected, OAuth required, with a Connect your account button right in the details.

    • 📸 org-connection-lifecycle-desktop-01-lifecycle-1-needs-signin.png — 7 validations passed
    org-connection-lifecycle-desktop-01-lifecycle-1-needs-signin.png
  3. Clicking Connect hands off to the browser: the mock identity provider receives the authorization request started by the desktop

    🎙 Jordan clicks Connect — the desktop hands off to the browser for the real OAuth sign-in and waits, no config files, no dashboard round-trip.

  4. The mock OAuth provider saw the desktop-started authorization request.

    🎙 Jordan clicks Connect — the desktop hands off to the browser for the real OAuth sign-in and waits, no config files, no dashboard round-trip.

    • 📸 org-connection-lifecycle-desktop-02-lifecycle-2-browser-handoff.png — 5 validations passed
    org-connection-lifecycle-desktop-02-lifecycle-2-browser-handoff.png
  5. The detail page flips to Connected on its own after the browser sign-in — no reload — and shows it is the member's own account

    🎙 The moment the sign-in completes, the detail page flips to Connected on its own — no reload — and the connection now shows exactly who it's connected as: Jordan's own account.

  6. The detail page updated itself to the member-owned connected state.

    🎙 The moment the sign-in completes, the detail page flips to Connected on its own — no reload — and the connection now shows exactly who it's connected as: Jordan's own account.

    • 📸 org-connection-lifecycle-desktop-03-lifecycle-3-connected.png — 7 validations passed
    org-connection-lifecycle-desktop-03-lifecycle-3-connected.png
  7. The connected detail page now offers both Reconnect and Disconnect

    🎙 Because things go stale in the real world, the connected view now offers both Reconnect and Disconnect, so Jordan controls the whole lifecycle from the desktop.

    • ✅ Visible text includes "Connected"
  8. The connected detail page exposes Reconnect and Disconnect lifecycle actions.

    🎙 Because things go stale in the real world, the connected view now offers both Reconnect and Disconnect, so Jordan controls the whole lifecycle from the desktop.

    • 📸 org-connection-lifecycle-desktop-04-lifecycle-4-lifecycle-actions.png — 6 validations passed
    org-connection-lifecycle-desktop-04-lifecycle-4-lifecycle-actions.png
  9. Reconnect runs a fresh OAuth round trip and Den records a new authorization timestamp

    🎙 Jordan hits Reconnect — one more browser round trip and the connection is re-authorized fresh, which is the fix for expired tokens or newly requested permissions.

  10. Reconnect completed a fresh OAuth round trip and left the detail page connected again.

🎙 Jordan hits Reconnect — one more browser round trip and the connection is re-authorized fresh, which is the fix for expired tokens or newly requested permissions.

  • 📸 org-connection-lifecycle-desktop-05-lifecycle-5-reconnected.png — 6 validations passed

org-connection-lifecycle-desktop-05-lifecycle-5-reconnected.png

  1. Disconnect removes the member's credential: Den reports it gone and the page returns to Connect your account

🎙 Finally Jordan clicks Disconnect — the connection drops back to "Connect your account", the cloud confirms Jordan's credential is gone, and Jordan can sign back in whenever they want. Full connect, reconnect, disconnect — without ever leaving the app.

  1. Disconnect removed the member credential and returned the detail page to Connect your account.

🎙 Finally Jordan clicks Disconnect — the connection drops back to "Connect your account", the cloud confirms Jordan's credential is gone, and Jordan can sign back in whenever they want. Full connect, reconnect, disconnect — without ever leaving the app.

  • 📸 org-connection-lifecycle-desktop-06-lifecycle-6-disconnected.png — 7 validations passed

org-connection-lifecycle-desktop-06-lifecycle-6-disconnected.png

  • ✅ Script frame 1 narrated: "Jordan opens Extensions and filters to Connections — the org-shared connection is sittin"
  • ✅ Script frame 2 narrated: "Jordan clicks Connect — the desktop hands off to the browser for the real OAuth sign-in "
  • ✅ Script frame 3 narrated: "The moment the sign-in completes, the detail page flips to Connected on its own — no rel"
  • ✅ Script frame 4 narrated: "Because things go stale in the real world, the connected view now offers both Reconnect "
  • ✅ Script frame 5 narrated: "Jordan hits Reconnect — one more browser round trip and the connection is re-authorized "
  • ✅ Script frame 6 narrated: "Finally Jordan clicks Disconnect — the connection drops back to "Connect your account", "

@benjaminshafii
benjaminshafii merged commit f1e1cb5 into dev Jul 29, 2026
21 checks passed
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