Skip to content
Draft
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
22 changes: 5 additions & 17 deletions ee/apps/den-api/src/mcp/marketplace-capabilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,17 +226,6 @@ function normalizeMarketplaceIds(input: { configObjectId: string; pluginId: stri
}
}

function roleIncludes(roleValue: string, role: string): boolean {
return roleValue
.split(",")
.map((entry) => entry.trim())
.includes(role)
}

function isOrgAdmin(member: MemberRow): boolean {
return roleIncludes(member.role, "owner") || roleIncludes(member.role, "admin")
}

function unique<T>(values: T[]): T[] {
return [...new Set(values)]
}
Expand Down Expand Up @@ -481,12 +470,9 @@ async function listMarketplaceGrants(organizationId: OrganizationId, marketplace

async function filterVisibleRows(input: {
member: McpMemberIdentity
memberRow: MemberRow
organizationId: OrganizationId
rows: MarketplaceCapabilityRow[]
}): Promise<MarketplaceCapabilityRow[]> {
if (isOrgAdmin(input.memberRow)) return input.rows

const configObjectGrantRows = await listConfigObjectGrants(
input.organizationId,
unique(input.rows.map((row) => row.configObject.id)),
Expand All @@ -504,6 +490,10 @@ async function filterVisibleRows(input: {
const marketplaceGrants = groupGrants(marketplaceGrantRows)

return input.rows.filter((row) => {
// Administrative visibility in Den must not silently publish every
// capability to that administrator's personal desktop catalog. Desktop
// discovery follows the same explicit member, team, and org-wide grants
// for every role so admins can curate what OpenWork exposes to them.
if (grantRole(input.member, configObjectGrants.get(row.configObject.id) ?? [])) return true
if (grantRole(input.member, pluginGrants.get(row.plugin.id) ?? [])) return true
return Boolean(grantRole(input.member, marketplaceGrants.get(row.marketplace.id) ?? []))
Expand All @@ -524,7 +514,6 @@ export async function listAccessibleMarketplaceSkillDescriptors(input: {
const rows = await filterVisibleRows({
organizationId,
member: input.member,
memberRow,
rows: (await listActiveMarketplaceRows(organizationId))
.filter((row) => row.configObject.objectType === "skill"),
})
Expand Down Expand Up @@ -1207,7 +1196,6 @@ export async function searchMarketplaceCapabilities(input: {
const rows = await filterVisibleRows({
organizationId,
member: input.member,
memberRow,
rows: await listActiveMarketplaceRows(organizationId),
})
const requirementStatusesByPluginId = await marketplacePluginMcpRequirementStatuses({
Expand Down Expand Up @@ -1295,7 +1283,7 @@ export async function executeMarketplaceCapability(input: {
if (!memberRow) {
return { ok: false, error: "forbidden", message: "No active org membership for this token." }
}
const visibleRows = await filterVisibleRows({ organizationId, member: input.member, memberRow, rows })
const visibleRows = await filterVisibleRows({ organizationId, member: input.member, rows })
const row = visibleRows[0]
if (!row) {
return { ok: false, error: "forbidden", message: "You have not been granted access to this marketplace plugin capability." }
Expand Down
26 changes: 26 additions & 0 deletions ee/apps/den-api/test/marketplace-capabilities.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -513,6 +513,32 @@ describe("marketplace capabilities source", () => {
expect(descriptors.some((descriptor) => descriptor.capability === unassigned.name)).toBe(false)
})

test("admins only discover desktop skills explicitly assigned to them", async () => {
const admin = await seedMember({ role: "admin" })
const assigned = await seedCapability({
owner: admin,
objectType: "skill",
title: "Approved Admin Playbook",
rawSourceText: "# Approved Admin Playbook",
})
const denOnly = await seedCapability({
owner: admin,
objectType: "skill",
title: "Den Only Admin Playbook",
grant: "none",
rawSourceText: "# Den Only Admin Playbook",
})

const descriptors = await marketplaceCapabilities.listAccessibleMarketplaceSkillDescriptors({
organizationId: admin.organizationId,
member: admin.member,
enabled: true,
})

expect(descriptors.some((descriptor) => descriptor.capability === assigned.name)).toBe(true)
expect(descriptors.some((descriptor) => descriptor.capability === denOnly.name)).toBe(false)
})

test("search finds a published plugin skill and execute returns provenance-framed raw content", async () => {
const owner = await seedMember()
const seeded = await seedCapability({
Expand Down
Loading