Skip to content

feat(identity): Profile → Connectors — connect a personal connector from a curated set#642

Merged
mgoldsborough merged 5 commits into
mainfrom
feat/profile-connectors-ui
Jul 9, 2026
Merged

feat(identity): Profile → Connectors — connect a personal connector from a curated set#642
mgoldsborough merged 5 commits into
mainfrom
feat/profile-connectors-ui

Conversation

@mgoldsborough

@mgoldsborough mgoldsborough commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

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?: boolean is added to the connector _meta (NimbleBrainConnectorMeta) and carried through both catalog projections to DirectoryEntry — the same per-connector config mechanism as auth / tags. A connector is offered for personal connection when it's flagged personal: 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_catalog returns 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 with handleInstallIdentity's accept rule (commented), so the offered set is exactly the installable one.

Honest connection state

list_personal_connectors no longer hard-codes not_authenticated. A non-starting isIdentityConnectorRunning probe (reads the user's registry, never starts a source) reports running for a connected connector, else not_authenticated — so a just-connected Granola shows Connected, not "Connect." Live cross-pod state remains the deferred reauth slice.

UI (ProfileConnectorsTab)

  • Your connectors — the installed list; a Connect action on a not-yet-authenticated connector (e.g. a cancelled flow), a subtle Connected indicator when running. (Replaces the raw not_authenticated pill.)
  • Add a connector — the curated picker (Granola), each row a ConnectinstallPersonalConnector (scope:"identity") → initiateIdentityConnect → the vendor's OAuth URL; the callback lands back on /profile/connectors.
  • Grant / revoke — each connector row expands (via its grant-count label) into a per-workspace access panel: the caller's workspaces, each with a Grant/Revoke toggle (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).
  • New web helpers: listPersonalCatalog / installPersonalConnector / initiateIdentityConnect / grantConnector / revokeConnector.
  • Robustness: the installed and catalog reads are decoupled (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's test/fixtures/connectors/curated.yaml carries the flag for tests + local dev (NB_CURATED_CATALOG_DIR).

Testing

  • Backend: list_personal_catalog (offered = personal+DCR, non-DCR-personal excluded, installed deduped); isIdentityConnectorRunning (false → true) + the list handler mapping it to running.
  • Web: ProfileConnectorsTab — installed list, honest Connected state, the curated Connect picker, catalog-read-failure decoupling, and the grant/revoke panel (expands, lists workspaces, calls grantConnector).
  • verify:static, test:unit, test:integration, and the web tests all green — see CI.
  • Verified live: a dev instance pointed at the flagged catalog offers only Granola; the Profile UI renders the picker, Connect, and grant/revoke; and a fresh DCR registers a human-readable 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 via resolveUserDisplayName (displayNameemail → fallback), mirroring resolveWorkspaceDisplayName, so the consent screen reads NimbleBrain (<name>). Verified live: a fresh DCR registers client_name: "NimbleBrain (Developer)" for the dev user (was user:usr_default).

…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.
@mgoldsborough mgoldsborough added the qa-reviewed QA review completed with no critical issues label Jul 9, 2026
… 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).
@mgoldsborough mgoldsborough merged commit d7618c5 into main Jul 9, 2026
4 checks passed
@mgoldsborough mgoldsborough deleted the feat/profile-connectors-ui branch July 9, 2026 23:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

qa-reviewed QA review completed with no critical issues

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant