feat(identity): Profile → Connectors — connect a personal connector from a curated set#642
Merged
Merged
Conversation
…rom a curated set The Profile → Connectors page gains an "Add a connector" affordance backed by a config-driven, curated set of personal-connectable connectors, plus a Connect flow that installs the connector on the caller's identity and runs the OAuth handshake. - `personal?: boolean` on the connector meta (`NimbleBrainConnectorMeta`), carried through both catalog projections to `DirectoryEntry` — the config marker for "offerable as a personal connection," authored in the curated catalog alongside `auth` / `tags`. - `manage_connectors list_personal_catalog` returns the offered set: entries that are `personal && remote-oauth && auth:"dcr"` and not already installed on the caller's identity. The DCR predicate is kept in lockstep with `handleInstallIdentity`'s accept rule, so the offered set is exactly the installable one. - `list_personal_connectors` reports honest same-pod connection state (`running` when the source is registered in the user's registry, else `not_authenticated`) via a non-starting `isIdentityConnectorRunning` probe. - Web: `listPersonalCatalog` / `installPersonalConnector` (scope "identity") / `initiateIdentityConnect` helpers, and the `ProfileConnectorsTab` UI — the installed list (with a Connect action for a not-yet-authenticated connector and a Connected indicator when running) plus the curated "Add a connector" picker, each Connect redirecting through the identity OAuth flow. DCR-only, matching what the identity plane accepts today; composio / static / provider personal connectors remain a separate slice.
…screen A personal connector's OAuth client registered as `NimbleBrain (user:<id>)` — the raw user id — because the identity (user) provider arm passed no owner display name and fell back to it, unlike the workspace arm which resolves the workspace name. Resolve the user's `displayName` (else `email`) via a `resolveUserDisplayName` helper mirroring `resolveWorkspaceDisplayName`, and pass it as `ownerDisplayName` so the vendor consent screen reads `NimbleBrain (<name>)`. Best-effort — an unreadable profile still falls back to the id.
…onnect - Load the installed list and the curated picker with Promise.allSettled so a `list_personal_catalog` failure hides "Add a connector" but doesn't block the installed list (the page's primary content) behind a load error. - On a Connect that installs but fails to start the OAuth flow, refresh() so the connector moves from "Add a connector" to "Your connectors" instead of showing stale. The action-error banner now renders above the loading branch so it survives the re-sync.
… profile Each connector row on Profile → Connectors now expands (via its grant-count label) into a per-workspace access panel: the caller's workspaces, each with a Grant/Revoke toggle. A personal connector is identity-bound and must be granted into every workspace it's used in — the personal workspace included, listed like any other (marked, not special-cased). Grant/revoke call the existing `grant_connector` / `revoke_connector` actions and re-sync silently so the panel stays open. Adds `grantConnector` / `revokeConnector` web helpers and consumes `useWorkspaceContext` for the workspace list.
…ning/not_authenticated Closes the handler coverage gap: the isIdentityConnectorRunning probe was tested in isolation, but not its composition through handleListPersonalConnectors. Adds a connectorRunning harness knob and asserts state=running (probe true) and state=not_authenticated (probe false).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
The Profile → Connectors page becomes actionable: alongside the list of connected personal connectors, it now offers a config-driven, curated set of connectors you can add for a personal (identity-plane) connection, each with a Connect button that installs on your identity and runs the OAuth handshake — and grant/revoke each connector into your workspaces. This is the user-facing surface for the identity-connector arc (#634/#639/#640/#641).
The curated set (config-driven)
personal?: booleanis added to the connector_meta(NimbleBrainConnectorMeta) and carried through both catalog projections toDirectoryEntry— the same per-connector config mechanism asauth/tags. A connector is offered for personal connection when it's flaggedpersonal: true. Scoped to Granola by flagging only Granola in the curated catalog; adding another later is a one-line config edit.manage_connectors list_personal_catalogreturns the offered set —personal && install.kind === "remote-oauth" && install.auth === "dcr", minus anything already on the caller's identity. The DCR predicate is held in lockstep withhandleInstallIdentity's accept rule (commented), so the offered set is exactly the installable one.Honest connection state
list_personal_connectorsno longer hard-codesnot_authenticated. A non-startingisIdentityConnectorRunningprobe (reads the user's registry, never starts a source) reportsrunningfor a connected connector, elsenot_authenticated— so a just-connected Granola shows Connected, not "Connect." Live cross-pod state remains the deferred reauth slice.UI (
ProfileConnectorsTab)not_authenticatedpill.)installPersonalConnector(scope:"identity") →initiateIdentityConnect→ the vendor's OAuth URL; the callback lands back on/profile/connectors.grant_connector/revoke_connector), re-syncing silently so the panel stays open. A personal connector is identity-bound and must be granted into every workspace it's used in — the personal workspace included, listed like any other (no free-at-home, no special-casing).listPersonalCatalog/installPersonalConnector/initiateIdentityConnect/grantConnector/revokeConnector.Promise.allSettled) so a catalog failure doesn't blank the installed list; a Connect that installs then fails to start re-syncs the row into "Your connectors."Scope
DCR-only, matching what the identity plane accepts today. Composio / static / provider personal connectors are a separate slice (their credential-location design).
Config (curated set)
Production offers the curated personal set from the connectors config; a companion deployments change flags Granola
personal: true. This PR'stest/fixtures/connectors/curated.yamlcarries the flag for tests + local dev (NB_CURATED_CATALOG_DIR).Testing
list_personal_catalog(offered = personal+DCR, non-DCR-personal excluded, installed deduped);isIdentityConnectorRunning(false → true) + the list handler mapping it torunning.ProfileConnectorsTab— installed list, honest Connected state, the curated Connect picker, catalog-read-failure decoupling, and the grant/revoke panel (expands, lists workspaces, callsgrantConnector).verify:static,test:unit,test:integration, and the web tests all green — see CI.client_name.Also: human-readable OAuth consent name
A personal connector's OAuth client was registering as
NimbleBrain (user:<id>)— the raw user id on the vendor's consent screen — because the identity (user) provider arm passed no owner display name, unlike the workspace arm (which resolves the workspace name). Now resolved viaresolveUserDisplayName(displayName→email→ fallback), mirroringresolveWorkspaceDisplayName, so the consent screen readsNimbleBrain (<name>). Verified live: a fresh DCR registersclient_name: "NimbleBrain (Developer)"for the dev user (wasuser:usr_default).