Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions backend/__tests__/unit/services/installableCatalogService.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 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: '6a0000000000000000000007', 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: '6a0000000000000000000007',
},
]);
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);
}
});

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"]';
Expand Down
24 changes: 24 additions & 0 deletions backend/__tests__/utils/leakMatrixSuites.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 } },
Expand Down
9 changes: 9 additions & 0 deletions backend/services/installable/installableCatalogService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,10 +110,19 @@ 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 the agent (bot user
// id) 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) } : {}),
} : {}),
});

/**
Expand Down
Loading