feat(identity): install a personal connector on the identity plane (DCR)#641
Merged
Conversation
`manage_connectors install` gains `scope: "identity"` — installs a remote MCP connection as a PERSONAL connector on the caller's own identity (`users/<id>/connectors.json`) instead of into a workspace. The write-path sibling of the interactive Connect flow (#640): install persists the record, and `POST /v1/mcp-auth/initiate-identity` → `startIdentityAuth` authenticates and starts the source into the user registry when the user returns. This is the write path that lights the personal-connector feature up for standard OAuth connectors (it was dark — nothing wrote `connectors.json` outside tests). DCR-only for now. `composio` keys the Composio-side identity on a workspace (`composioUserId(wsId)`), `static` reads an operator client secret from the workspace credential store, and `provider` is a fleet auth class eager-started outside the interactive Connect flow — all workspace/platform-bound. Only `dcr` (standard dynamic-client-registration OAuth) authenticates cleanly with no workspace binding; the identity variants of the others are a separate slice and are rejected here with a clear message. Forbid the same-name collision at both install points: an identity install is rejected when the serverName already exists as a shared-workspace install in a workspace the caller belongs to, and a shared-workspace install is rejected when the caller already has a personal connector of that name. A connector that is both would be unaddressable by `get/set_permissions`, which resolve a personal connector first. A collision formed later (the caller joins a workspace that already installs the name) is the documented residual — personal wins. `list_personal_connectors` reads the identity plane (`IdentityConnectorStore`), not the personal-workspace registry.
The DCR auth-type gate already narrows the action; validateRemoteOAuthInstall only checks composio/static/provider, so it's a pass-through for dcr. Use the narrowed entry.install directly.
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
manage_connectors installgainsscope: "identity"— installs a remote MCP connection as a personal connector on the caller's own identity (users/<id>/connectors.json) instead of into a workspace. It's the write-path sibling of the interactive Connect flow (#640): install persists the record, and the existingPOST /v1/mcp-auth/initiate-identity→startIdentityAuthauthenticates and starts the source into the user registry when the user returns.This is the write path that lights the personal-connector feature up for standard OAuth connectors — until now nothing wrote
connectors.jsonoutside tests, so the identity-plane read paths (#634/#639) resolved to nothing.Scope — DCR-only
Only
dcr(standard dynamic-client-registration OAuth, e.g. Granola/Gmail-style) installs on the identity plane here. The other remote-oauth auth types are workspace/platform-bound and rejected with a clear message:composiokeys the Composio-side identity on a workspace (composioUserId(wsId))staticreads an operator client secret from the workspace credential storeprovideris a fleet auth class, eager-started outside the interactive Connect flowTheir identity variants (a user-owned Composio connection, an identity composio-auth route, a user credential home) are a separate slice.
Forbid the same-name collision (both directions)
A serverName can't be both a personal connector and a shared-workspace install —
get/set_permissionsresolve a personal connector first, so the workspace copy's policy would be unaddressable. Enforced at both install points:workspace.jsonbundles, so it's pod-independent; the caller's own personal workspace is skipped — it's the legacy personal home, not a shared collision)A collision that forms later (the caller joins a workspace that already installs the name) is the documented residual —
resolvePermissionOwnerresolves it personal-first by a stated rule.Also
list_personal_connectorsreads the identity plane (IdentityConnectorStore.list) rather than the personal-workspace registry. Display name/description are enriched from the catalog; connectionstatereports the static resting value (not_authenticated→ "Connect") — live per-user state lands with the deferred reauth slice.Not in scope
scope: "identity". Until it lands, this path is reachable only by a direct tool call, so the feature stays effectively dark end-to-end.Testing
test/integration/identity-target-install.test.ts(6): identity install writesconnectors.json+scope:"identity";list_personal_connectorsreads the identity plane; non-remote-oauth rejected; composio rejected; collision rejected in both directions (with a no-partial-write assertion).verify:static✅ (exit 0) ·test:unit4048 pass ·test:integration623 pass / 0 fail.