From 47ff7fd2cf37e90b2f2684236bdc8005e1df055a Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Sun, 1 Nov 2026 07:01:22 -0800 Subject: [PATCH 1/2] feat(connectors): a hosted connection says how it differs from its siblings A hosted entry can hold several of the caller's rows: an OAuth sign-in and operator keys bound per agent. The catalog listed each as a bare id, so the Tools panel could not tell them apart and showed no Disconnect at all. A hosted row now carries connectedAt, and for a key its masked hint and the agent it is bound to. Refs and the provider subject stay off the card. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015AKgpQ2pr6xRMhN5mcfoHF --- .../installableCatalogService.test.js | 35 +++++++++++++++++++ .../installable/installableCatalogService.ts | 10 ++++++ 2 files changed, 45 insertions(+) diff --git a/backend/__tests__/unit/services/installableCatalogService.test.js b/backend/__tests__/unit/services/installableCatalogService.test.js index a850a004f2..8874081cf8 100644 --- a/backend/__tests__/unit/services/installableCatalogService.test.js +++ b/backend/__tests__/unit/services/installableCatalogService.test.js @@ -314,6 +314,41 @@ it("a hosted entry's tool row carries its connect descriptor and only its own co } }); +it("a hosted entry's connections each say how they differ: when, and for a key its hint and agent", async () => { + const shipped = HOSTED_MCP_ENTRIES.slice(); + const linearEntry = shipped.find((entry) => entry.id === 'linear'); + try { + mockInstallables([], [buildHostedMcpToolInstallable(linearEntry)]); + InstallableInstallation.find.mockReturnValue(lean([])); + Integration.find.mockReturnValue(lean([ + { + _id: 'conn-oauth', type: 'hosted-mcp', status: 'connected', createdBy: userId, + createdAt: new Date('2026-10-30T10:00:00Z'), + config: { entryId: 'linear', intake: 'oauth', credentialRef: 'ref-oauth', providerSubject: 'subject-1' }, + }, + { + _id: 'conn-key', type: 'hosted-mcp', status: 'connected', createdBy: userId, + createdAt: new Date('2026-10-31T11:00:00Z'), + config: { entryId: 'linear', agentId: 'agent-7', credentialHint: 'lin…9f', credentialRef: 'ref-key' }, + }, + ])); + + const catalog = await catalogFor(userId); + const linear = catalog.installables.find((entry) => entry.list === 'tools' && entry.installableId === 'linear'); + + expect(linear.connections).toEqual([ + { connectionId: 'conn-oauth', owner: '', repo: '', connectedAt: '2026-10-30T10:00:00.000Z' }, + { + connectionId: 'conn-key', owner: '', repo: '', connectedAt: '2026-10-31T11:00:00.000Z', + agentId: 'agent-7', credentialHint: 'lin…9f', + }, + ]); + expect(JSON.stringify(linear.connections)).not.toMatch(/ref-oauth|ref-key|subject-1/); + } finally { + HOSTED_MCP_ENTRIES.splice(0, HOSTED_MCP_ENTRIES.length, ...shipped); + } +}); + it('hides an instance-listed hosted entry from offerings while keeping its connection removable', async () => { const prior = process.env.HOSTED_MCP_HIDDEN_ENTRIES; process.env.HOSTED_MCP_HIDDEN_ENTRIES = '["airtable"]'; diff --git a/backend/services/installable/installableCatalogService.ts b/backend/services/installable/installableCatalogService.ts index 904b3a6524..1349c1ab90 100644 --- a/backend/services/installable/installableCatalogService.ts +++ b/backend/services/installable/installableCatalogService.ts @@ -110,10 +110,20 @@ const publicInstallation = (installation: any): unknown => { // A Connection the caller may grant from: the mint requires the connection's // owner, so only the caller's own rows are offered. `connectionId` is the key // the mint takes; owner/repo are the card's evidence of what the grant acts on. +// +// A hosted entry can hold several of the caller's rows, an OAuth sign-in and +// operator keys bound per agent (TASK-330), so a hosted row also carries what +// tells them apart: when it was connected, and for a key its masked hint and +// the agent it is bound to. const publicConnection = (integration: any) => ({ connectionId: String(integration.installationId || integration._id || ''), owner: String(integration.config?.owner || ''), repo: String(integration.config?.repo || ''), + ...(integration.type === 'hosted-mcp' ? { + ...(integration.createdAt ? { connectedAt: new Date(integration.createdAt).toISOString() } : {}), + ...(integration.config?.agentId ? { agentId: String(integration.config.agentId) } : {}), + ...(integration.config?.credentialHint ? { credentialHint: String(integration.config.credentialHint) } : {}), + } : {}), }); /** From 4a66cad37d87ce0caa840a425b88e9dbc9949f69 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Sun, 1 Nov 2026 07:05:44 -0800 Subject: [PATCH 2/2] fix(connectors): label a hosted connection by when and agent only; leak-matrix hosted rows credentialHint is never written by the paste route, so the card cannot promise a masked hint; it is left off. The installables leak matrix now seeds an OAuth row and an agent-bound key row for a hosted entry with sentinel refs, subject, hint and pending authorization, and a projection that leaks a ref fails it. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015AKgpQ2pr6xRMhN5mcfoHF --- .../installableCatalogService.test.js | 8 +++---- backend/__tests__/utils/leakMatrixSuites.js | 24 +++++++++++++++++++ .../installable/installableCatalogService.ts | 5 ++-- 3 files changed, 30 insertions(+), 7 deletions(-) diff --git a/backend/__tests__/unit/services/installableCatalogService.test.js b/backend/__tests__/unit/services/installableCatalogService.test.js index 8874081cf8..05d8524bb9 100644 --- a/backend/__tests__/unit/services/installableCatalogService.test.js +++ b/backend/__tests__/unit/services/installableCatalogService.test.js @@ -314,7 +314,7 @@ it("a hosted entry's tool row carries its connect descriptor and only its own co } }); -it("a hosted entry's connections each say how they differ: when, and for a key its hint and agent", async () => { +it("a hosted entry's connections each say how they differ: when, and for a key its agent", async () => { const shipped = HOSTED_MCP_ENTRIES.slice(); const linearEntry = shipped.find((entry) => entry.id === 'linear'); try { @@ -329,7 +329,7 @@ it("a hosted entry's connections each say how they differ: when, and for a key i { _id: 'conn-key', type: 'hosted-mcp', status: 'connected', createdBy: userId, createdAt: new Date('2026-10-31T11:00:00Z'), - config: { entryId: 'linear', agentId: 'agent-7', credentialHint: 'lin…9f', credentialRef: 'ref-key' }, + config: { entryId: 'linear', agentId: '6a0000000000000000000007', credentialHint: 'lin…9f', credentialRef: 'ref-key' }, }, ])); @@ -340,10 +340,10 @@ it("a hosted entry's connections each say how they differ: when, and for a key i { connectionId: 'conn-oauth', owner: '', repo: '', connectedAt: '2026-10-30T10:00:00.000Z' }, { connectionId: 'conn-key', owner: '', repo: '', connectedAt: '2026-10-31T11:00:00.000Z', - agentId: 'agent-7', credentialHint: 'lin…9f', + agentId: '6a0000000000000000000007', }, ]); - expect(JSON.stringify(linear.connections)).not.toMatch(/ref-oauth|ref-key|subject-1/); + expect(JSON.stringify(linear.connections)).not.toMatch(/ref-oauth|ref-key|subject-1|lin…9f/); } finally { HOSTED_MCP_ENTRIES.splice(0, HOSTED_MCP_ENTRIES.length, ...shipped); } diff --git a/backend/__tests__/utils/leakMatrixSuites.js b/backend/__tests__/utils/leakMatrixSuites.js index 3fbbf4e62a..a4188f7a33 100644 --- a/backend/__tests__/utils/leakMatrixSuites.js +++ b/backend/__tests__/utils/leakMatrixSuites.js @@ -261,6 +261,30 @@ const installables = { installationId: 'install-leak-1', scope: 'user', type: 'github-app', status: 'connected', createdBy: owner, isActive: true, ...withConfig(integrationSecrets(s, 'INST_GITHUBAPP'), { installationId: 'install-leak-1', owner: 'octo', repo: 'demo' }), }); + // A hosted entry with two of the owner's rows, an OAuth sign-in and an + // operator key bound to an agent: the catalog labels each row, and no ref, + // subject, hint or pending authorization reaches any caller. + // eslint-disable-next-line global-require + const { HOSTED_MCP_ENTRIES } = require('../../integrations/hostedMcp/entries'); + // eslint-disable-next-line global-require + const { buildHostedMcpToolInstallable } = require('../../services/installable/toolInstallables'); + await model.Installable().create(buildHostedMcpToolInstallable(HOSTED_MCP_ENTRIES.find((entry) => entry.id === 'linear'))); + await Integration.create({ + scope: 'user', type: 'hosted-mcp', status: 'connected', createdBy: owner, isActive: true, + config: { + entryId: 'linear', intake: 'oauth', + credentialRef: s('INST_HOSTED_OAUTH_CREDENTIALREF'), refreshTokenRef: s('INST_HOSTED_OAUTH_REFRESHTOKENREF'), + providerSubject: s('INST_HOSTED_OAUTH_PROVIDERSUBJECT'), + pendingAuth: { state: s('INST_HOSTED_PENDINGAUTH_STATE'), codeVerifier: s('INST_HOSTED_PENDINGAUTH_CODEVERIFIER') }, + }, + }); + await Integration.create({ + scope: 'user', type: 'hosted-mcp', status: 'connected', createdBy: owner, isActive: true, + config: { + entryId: 'linear', agentId: String(new mongoose.Types.ObjectId()), + credentialRef: s('INST_HOSTED_KEY_CREDENTIALREF'), credentialHint: s('INST_HOSTED_KEY_CREDENTIALHINT'), + }, + }); return { sentinels: s.all, identities: { owner: { user: owner }, stranger: { user: stranger }, admin: { user: admin } }, diff --git a/backend/services/installable/installableCatalogService.ts b/backend/services/installable/installableCatalogService.ts index 1349c1ab90..7f11802a4b 100644 --- a/backend/services/installable/installableCatalogService.ts +++ b/backend/services/installable/installableCatalogService.ts @@ -113,8 +113,8 @@ const publicInstallation = (installation: any): unknown => { // // A hosted entry can hold several of the caller's rows, an OAuth sign-in and // operator keys bound per agent (TASK-330), so a hosted row also carries what -// tells them apart: when it was connected, and for a key its masked hint and -// the agent it is bound to. +// tells them apart: when it was connected, and for a key the agent (bot user +// id) it is bound to. const publicConnection = (integration: any) => ({ connectionId: String(integration.installationId || integration._id || ''), owner: String(integration.config?.owner || ''), @@ -122,7 +122,6 @@ const publicConnection = (integration: any) => ({ ...(integration.type === 'hosted-mcp' ? { ...(integration.createdAt ? { connectedAt: new Date(integration.createdAt).toISOString() } : {}), ...(integration.config?.agentId ? { agentId: String(integration.config.agentId) } : {}), - ...(integration.config?.credentialHint ? { credentialHint: String(integration.config.credentialHint) } : {}), } : {}), });