diff --git a/apps/app/src/app/lib/den-types.ts b/apps/app/src/app/lib/den-types.ts index 46e53d19e7..62524cd9c7 100644 --- a/apps/app/src/app/lib/den-types.ts +++ b/apps/app/src/app/lib/den-types.ts @@ -115,6 +115,13 @@ export type DenOrgPluginResolved = { extension?: DenOrgExtensionProjection | null; }; +export type DenAssignedMarketplaceCapability = { + configObjectId: string; + marketplaceId: string; + objectType: DenPluginConfigObjectType; + pluginId: string; +}; + export type DenResourceSnapshotConfigItem = { configItemId: string; lastUpdatedAt: string; diff --git a/apps/app/src/app/lib/den.ts b/apps/app/src/app/lib/den.ts index 7d9b708d02..f979d3cb40 100644 --- a/apps/app/src/app/lib/den.ts +++ b/apps/app/src/app/lib/den.ts @@ -65,6 +65,7 @@ export const DEN_INFERENCE_PATH = "/dashboard/inference"; // the many existing den.ts importers keep working. export type * from "./den-types"; import type { + DenAssignedMarketplaceCapability, DenOrgExtensionProjection, DenOrgMarketplace, DenOrgPlugin, @@ -1812,6 +1813,28 @@ function getOrgPluginResolved(plugin: DenOrgPlugin, payload: unknown): DenOrgPlu return { plugin, memberships }; } +function getAssignedMarketplaceCapabilities(payload: unknown): DenAssignedMarketplaceCapability[] { + if (!isRecord(payload) || !Array.isArray(payload.items)) return []; + return payload.items.flatMap((item) => { + if ( + !isRecord(item) + || typeof item.configObjectId !== "string" + || typeof item.marketplaceId !== "string" + || typeof item.objectType !== "string" + || typeof item.pluginId !== "string" + ) { + return []; + } + const objectType = parsePluginConfigObjectType(item.objectType); + return objectType ? [{ + configObjectId: item.configObjectId, + marketplaceId: item.marketplaceId, + objectType, + pluginId: item.pluginId, + }] : []; + }); +} + function getBillingPrice(value: unknown): DenBillingPrice | null { if (!isRecord(value)) { return null; @@ -2273,6 +2296,15 @@ export function createDenClient(options: { baseUrl: string; token?: string | nul return getOrgMarketplaces(payload); }, + async listAssignedMarketplaceCapabilities(orgId: string): Promise { + const payload = await requestJson( + baseUrls, + "/v1/resources/marketplace-capabilities", + { method: "GET", token, organizationId: orgId }, + ); + return getAssignedMarketplaceCapabilities(payload); + }, + async getOrgMarketplaceResolved(orgId: string, marketplaceId: string): Promise { const payload = await requestJson( baseUrls, diff --git a/apps/app/src/react-app/domains/session/surface/connect-capability-inventory.ts b/apps/app/src/react-app/domains/session/surface/connect-capability-inventory.ts index 649014caf3..eaf04f1771 100644 --- a/apps/app/src/react-app/domains/session/surface/connect-capability-inventory.ts +++ b/apps/app/src/react-app/domains/session/surface/connect-capability-inventory.ts @@ -1,4 +1,5 @@ import type { + DenAssignedMarketplaceCapability, DenOrgMarketplace, DenOrgMarketplaceResolved, DenOrgPlugin, @@ -9,6 +10,9 @@ import type { import type { McpServerEntry, McpStatus, McpStatusMap, SkillCard } from "@/app/types"; export type ConnectCapabilityClient = { + listAssignedMarketplaceCapabilities: ( + organizationId: string, + ) => Promise; listOrgMarketplaces: (organizationId: string) => Promise; getOrgMarketplaceResolved: ( organizationId: string, @@ -151,8 +155,17 @@ export async function listAssignedConnectCapabilities(input: { client: ConnectCapabilityClient; organizationId: string; }): Promise { + const assigned = await input.client.listAssignedMarketplaceCapabilities(input.organizationId); + if (assigned.length === 0) return EMPTY_CONNECT_CAPABILITY_INVENTORY; + + const assignedMarketplaceIds = new Set(assigned.map((item) => item.marketplaceId)); + const assignedPluginKeys = new Set(assigned.map((item) => `${item.marketplaceId}:${item.pluginId}`)); + const assignedCapabilityKeys = new Set( + assigned.map((item) => `${item.marketplaceId}:${item.pluginId}:${item.configObjectId}`), + ); const marketplaces = (await input.client.listOrgMarketplaces(input.organizationId)) .filter((marketplace) => marketplace.status === "active") + .filter((marketplace) => assignedMarketplaceIds.has(marketplace.id)) .sort((left, right) => left.name.localeCompare(right.name)); const resolvedMarketplaces = await Promise.all( marketplaces.map((marketplace) => @@ -163,7 +176,11 @@ export async function listAssignedConnectCapabilities(input: { const plugins = new Map(); for (const resolved of resolvedMarketplaces) { for (const plugin of resolved.plugins) { - if (plugin.status !== "active" || plugins.has(plugin.id)) continue; + if ( + plugin.status !== "active" + || plugins.has(plugin.id) + || !assignedPluginKeys.has(`${resolved.marketplace.id}:${plugin.id}`) + ) continue; plugins.set(plugin.id, { marketplace: resolved.marketplace, plugin }); } } @@ -181,7 +198,11 @@ export async function listAssignedConnectCapabilities(input: { for (const { marketplace, resolved } of resolvedPlugins) { for (const membership of resolved.memberships) { const object = membership.configObject; - if (!object || object.status !== "active") continue; + if ( + !object + || object.status !== "active" + || !assignedCapabilityKeys.has(`${marketplace.id}:${resolved.plugin.id}:${object.id}`) + ) continue; if (object.objectType === "skill") { skills.push(toSkill(marketplace, resolved.plugin, object)); } diff --git a/apps/app/tests/connect-capability-inventory-admin.test.ts b/apps/app/tests/connect-capability-inventory-admin.test.ts new file mode 100644 index 0000000000..c109618ee8 --- /dev/null +++ b/apps/app/tests/connect-capability-inventory-admin.test.ts @@ -0,0 +1,118 @@ +import { expect, test } from "bun:test"; + +import type { + DenOrgMarketplace, + DenOrgPlugin, + DenPluginConfigObject, +} from "../src/app/lib/den"; +import { + listAssignedConnectCapabilities, + type ConnectCapabilityClient, +} from "../src/react-app/domains/session/surface/connect-capability-inventory"; + +const marketplace = (id: string, name: string): DenOrgMarketplace => ({ + id, + name, + description: null, + status: "active", + pluginCount: 1, + updatedAt: null, +}); + +const plugin = (id: string, name: string): DenOrgPlugin => ({ + id, + name, + description: null, + status: "active", + memberCount: 1, + updatedAt: null, + componentCounts: {}, +}); + +const configObject = ( + id: string, + objectType: "mcp" | "skill", + title: string, +): DenPluginConfigObject => ({ + id, + objectType, + title, + description: title, + currentFileName: null, + currentFileExtension: objectType === "mcp" ? ".json" : ".md", + currentRelativePath: objectType === "skill" ? `skills/${id}/SKILL.md` : `mcps/${id}.json`, + status: "active", + updatedAt: null, + latestVersion: { + id: `version-${id}`, + rawSourceText: objectType === "skill" ? `# ${title}` : null, + normalizedPayloadJson: objectType === "mcp" + ? { mcpServers: { [id]: { url: `https://${id}.example.test/mcp` } } } + : null, + sourceRevisionRef: null, + createdAt: null, + }, +}); + +test("admin desktop inventory intersects management data with assigned capability references", async () => { + const assignedMarketplace = marketplace("market-assigned", "Assigned marketplace"); + const adminOnlyMarketplace = marketplace("market-admin", "Admin-only marketplace"); + const mixedPlugin = plugin("plugin-mixed", "Mixed plugin"); + const assignedMcpPlugin = plugin("plugin-mcp", "Assigned MCP plugin"); + const adminOnlyPlugin = plugin("plugin-admin", "Admin-only plugin"); + + const client = { + async listAssignedMarketplaceCapabilities() { + return [ + { + marketplaceId: assignedMarketplace.id, + pluginId: mixedPlugin.id, + configObjectId: "skill-assigned", + objectType: "skill" as const, + }, + { + marketplaceId: assignedMarketplace.id, + pluginId: assignedMcpPlugin.id, + configObjectId: "mcp-assigned", + objectType: "mcp" as const, + }, + ]; + }, + async listOrgMarketplaces() { + return [assignedMarketplace, adminOnlyMarketplace]; + }, + async getOrgMarketplaceResolved(_organizationId: string, marketplaceId: string) { + return marketplaceId === assignedMarketplace.id + ? { marketplace: assignedMarketplace, plugins: [mixedPlugin, assignedMcpPlugin] } + : { marketplace: adminOnlyMarketplace, plugins: [adminOnlyPlugin] }; + }, + async getOrgPluginResolved(_organizationId: string, selectedPlugin: DenOrgPlugin) { + const objects = selectedPlugin.id === mixedPlugin.id + ? [ + configObject("skill-assigned", "skill", "Assigned Skill"), + configObject("mcp-admin-only", "mcp", "Unassigned MCP"), + ] + : selectedPlugin.id === assignedMcpPlugin.id + ? [configObject("mcp-assigned", "mcp", "Assigned MCP")] + : [configObject("skill-admin-only", "skill", "Admin-only Skill")]; + return { + plugin: selectedPlugin, + memberships: objects.map((object) => ({ + id: `membership-${object.id}`, + pluginId: selectedPlugin.id, + configObjectId: object.id, + configObject: object, + })), + }; + }, + } satisfies ConnectCapabilityClient; + + const inventory = await listAssignedConnectCapabilities({ + client, + organizationId: "org-admin", + }); + + expect(inventory.skills.map((skill) => skill.name)).toEqual(["Assigned Skill"]); + expect(inventory.mcpServers.map((server) => server.name)).toEqual(["Assigned MCP"]); + expect(inventory.mcpServers.some((server) => server.name === "Unassigned MCP")).toBe(false); +}); diff --git a/apps/app/tests/connect-capability-inventory.test.ts b/apps/app/tests/connect-capability-inventory.test.ts index 4aec114121..6007489692 100644 --- a/apps/app/tests/connect-capability-inventory.test.ts +++ b/apps/app/tests/connect-capability-inventory.test.ts @@ -9,6 +9,20 @@ describe("assigned OpenWork Connect capability inventory", () => { const inventory = await listAssignedConnectCapabilities({ organizationId: "org_1", client: { + listAssignedMarketplaceCapabilities: async () => [ + { + marketplaceId: "marketplace_1", + pluginId: "plugin_1", + configObjectId: "skill_1", + objectType: "skill", + }, + { + marketplaceId: "marketplace_1", + pluginId: "plugin_1", + configObjectId: "mcp_1", + objectType: "mcp", + }, + ], listOrgMarketplaces: async () => [ { id: "marketplace_1", @@ -146,6 +160,14 @@ describe("assigned OpenWork Connect capability inventory", () => { const inventory = await listAssignedConnectCapabilities({ organizationId: "org_1", client: { + listAssignedMarketplaceCapabilities: async () => [ + { + marketplaceId: "marketplace_active", + pluginId: "plugin_1", + configObjectId: "skill_inactive", + objectType: "skill", + }, + ], listOrgMarketplaces: async () => [ { id: "marketplace_active", @@ -248,6 +270,20 @@ describe("assigned OpenWork Connect capability inventory", () => { const inventory = await listAssignedConnectCapabilities({ organizationId: "org_1", client: { + listAssignedMarketplaceCapabilities: async () => [ + { + marketplaceId: "marketplace_1", + pluginId: "plugin_1", + configObjectId: "skill_win", + objectType: "skill", + }, + { + marketplaceId: "marketplace_1", + pluginId: "plugin_1", + configObjectId: "skill_nomatch", + objectType: "skill", + }, + ], listOrgMarketplaces: async () => [marketplace], getOrgMarketplaceResolved: async () => ({ marketplace, diff --git a/ee/apps/den-api/src/mcp/marketplace-capabilities.ts b/ee/apps/den-api/src/mcp/marketplace-capabilities.ts index f2aa06ae40..bf4b22a87e 100644 --- a/ee/apps/den-api/src/mcp/marketplace-capabilities.ts +++ b/ee/apps/den-api/src/mcp/marketplace-capabilities.ts @@ -46,6 +46,13 @@ export type RemoteSkillDescriptor = { location: string } +export type AccessibleMarketplaceCapabilityReference = { + configObjectId: string + marketplaceId: string + objectType: MarketplaceCapabilityObjectType + pluginId: string +} + export function normalizeRemoteSkillDescription(input: { description: string | null name: string @@ -226,17 +233,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(values: T[]): T[] { return [...new Set(values)] } @@ -481,12 +477,9 @@ async function listMarketplaceGrants(organizationId: OrganizationId, marketplace async function filterVisibleRows(input: { member: McpMemberIdentity - memberRow: MemberRow organizationId: OrganizationId rows: MarketplaceCapabilityRow[] }): Promise { - if (isOrgAdmin(input.memberRow)) return input.rows - const configObjectGrantRows = await listConfigObjectGrants( input.organizationId, unique(input.rows.map((row) => row.configObject.id)), @@ -504,12 +497,47 @@ 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) ?? [])) }) } +export async function listAccessibleMarketplaceCapabilityReferences(input: { + enabled?: boolean + member: McpMemberIdentity | null + organizationId: string +}): Promise { + if (input.enabled === false || !input.member) return [] + const organizationId = normalizeDenTypeId("organization", input.organizationId) + if (!(await getActiveMember(organizationId, input.member))) return [] + + const rows = await filterVisibleRows({ + organizationId, + member: input.member, + rows: await listActiveMarketplaceRows(organizationId), + }) + const references = new Map() + for (const row of rows) { + const key = `${row.marketplace.id}:${row.plugin.id}:${row.configObject.id}` + references.set(key, { + configObjectId: row.configObject.id, + marketplaceId: row.marketplace.id, + objectType: row.configObject.objectType, + pluginId: row.plugin.id, + }) + } + return [...references.values()].sort((left, right) => + left.marketplaceId.localeCompare(right.marketplaceId) + || left.pluginId.localeCompare(right.pluginId) + || left.configObjectId.localeCompare(right.configObjectId) + ) +} + export async function listAccessibleMarketplaceSkillDescriptors(input: { enabled?: boolean member: McpMemberIdentity | null @@ -524,7 +552,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"), }) @@ -1207,7 +1234,6 @@ export async function searchMarketplaceCapabilities(input: { const rows = await filterVisibleRows({ organizationId, member: input.member, - memberRow, rows: await listActiveMarketplaceRows(organizationId), }) const requirementStatusesByPluginId = await marketplacePluginMcpRequirementStatuses({ @@ -1295,7 +1321,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." } diff --git a/ee/apps/den-api/src/routes/org/resources.ts b/ee/apps/den-api/src/routes/org/resources.ts index 85f80e492d..b25934028e 100644 --- a/ee/apps/den-api/src/routes/org/resources.ts +++ b/ee/apps/den-api/src/routes/org/resources.ts @@ -13,6 +13,9 @@ import type { Hono } from "hono" import { describeRoute } from "hono-openapi" import { z } from "zod" import { db } from "../../db.js" +import { env } from "../../env.js" +import { memberFacingMcpConnectionsEnabled } from "../../capability-sources/external-mcp-rollout.js" +import { listAccessibleMarketplaceCapabilityReferences } from "../../mcp/marketplace-capabilities.js" import { type MemberTeamsContext, orgMemberRoute, @@ -50,6 +53,15 @@ const resourceSnapshotResponseSchema = z.object({ }), }).meta({ ref: "ResourceSnapshotResponse" }) +const assignedMarketplaceCapabilitiesResponseSchema = z.object({ + items: z.array(z.object({ + configObjectId: z.string(), + marketplaceId: z.string(), + objectType: z.string(), + pluginId: z.string(), + })), +}).meta({ ref: "AssignedMarketplaceCapabilitiesResponse" }) + type ResourceSnapshot = z.infer type ResourceMarketplace = ResourceSnapshot["resources"]["marketplaces"][string] type ResourcePlugin = ResourceMarketplace["plugins"][number] @@ -242,6 +254,42 @@ async function listAccessibleMarketplaces(input: { } export function registerOrgResourceRoutes }>(app: Hono) { + app.get( + "/v1/resources/marketplace-capabilities", + describeRoute({ + tags: ["Resources"], + summary: "List assigned marketplace capabilities", + description: "Returns grant-scoped marketplace capability references for the current member. Organization administration visibility never expands this desktop and agent inventory.", + responses: { + 200: jsonResponse("Assigned marketplace capabilities returned successfully.", assignedMarketplaceCapabilitiesResponseSchema), + 401: jsonResponse("The caller must be signed in to list assigned capabilities.", unauthorizedSchema), + }, + }), + orgMemberRoute(), + resolveMemberTeamsMiddleware, + async (c) => { + const organizationContext = c.get("organizationContext") + if (!organizationContext) { + return c.json({ error: "organization_not_found" }, 404) + } + const teamIds = readMemberTeams( + c.get("memberTeams"), + organizationContext.organization.id, + ).map((team) => team.id) + const items = await listAccessibleMarketplaceCapabilityReferences({ + organizationId: organizationContext.organization.id, + member: { + orgMembershipId: organizationContext.currentMember.id, + teamIds, + }, + enabled: memberFacingMcpConnectionsEnabled(organizationContext.organization.metadata, { + gatingEnabled: env.mcpConnectionsGatingEnabled, + }), + }) + return c.json({ items }) + }, + ) + app.get( "/v1/resources", describeRoute({ diff --git a/ee/apps/den-api/test/marketplace-capabilities.test.ts b/ee/apps/den-api/test/marketplace-capabilities.test.ts index 27a5433996..d3cda14aaa 100644 --- a/ee/apps/den-api/test/marketplace-capabilities.test.ts +++ b/ee/apps/den-api/test/marketplace-capabilities.test.ts @@ -513,6 +513,98 @@ describe("marketplace capabilities source", () => { expect(descriptors.some((descriptor) => descriptor.capability === unassigned.name)).toBe(false) }) + test("admins only discover, search, and execute marketplace capabilities granted to them", async () => { + const admin = await seedMember({ role: "admin" }) + const assignedSkill = await seedCapability({ + owner: admin, + objectType: "skill", + title: "Admin Access Approved Playbook", + grant: "config_object", + rawSourceText: "# Approved Admin Playbook", + }) + const denOnlySkill = await seedCapability({ + owner: admin, + objectType: "skill", + title: "Admin Access Den Only Playbook", + grant: "none", + rawSourceText: "# Den Only Admin Playbook", + }) + const pluginGrantedCommand = await seedCapability({ + owner: admin, + objectType: "command", + title: "Admin Access Plugin Granted Command", + grant: "plugin", + rawSourceText: "Run the plugin-granted command.", + }) + const denOnlyCommand = await seedCapability({ + owner: admin, + objectType: "command", + title: "Admin Access Den Only Command", + grant: "none", + rawSourceText: "Do not expose this command.", + }) + const marketplaceGrantedMcp = await seedCapability({ + owner: admin, + objectType: "mcp", + title: "Admin Access Marketplace Granted MCP", + grant: "marketplace", + normalizedPayloadJson: { mcpServers: { proof: { url: "https://mcp.example.test/remote" } } }, + rawSourceText: JSON.stringify({ mcpServers: { proof: { url: "https://mcp.example.test/remote" } } }), + }) + const denOnlyMcp = await seedCapability({ + owner: admin, + objectType: "mcp", + title: "Admin Access Den Only MCP", + grant: "none", + normalizedPayloadJson: { mcpServers: { private: { url: "https://private-mcp.example.test/remote" } } }, + rawSourceText: JSON.stringify({ mcpServers: { private: { url: "https://private-mcp.example.test/remote" } } }), + }) + + const descriptors = await marketplaceCapabilities.listAccessibleMarketplaceSkillDescriptors({ + organizationId: admin.organizationId, + member: admin.member, + enabled: true, + }) + + expect(descriptors.some((descriptor) => descriptor.capability === assignedSkill.name)).toBe(true) + expect(descriptors.some((descriptor) => descriptor.capability === denOnlySkill.name)).toBe(false) + + const assignedReferences = await marketplaceCapabilities.listAccessibleMarketplaceCapabilityReferences({ + organizationId: admin.organizationId, + member: admin.member, + enabled: true, + }) + const assignedConfigObjectIds = assignedReferences.map((reference) => reference.configObjectId) + expect(assignedConfigObjectIds).toContain(assignedSkill.configObjectId) + expect(assignedConfigObjectIds).toContain(pluginGrantedCommand.configObjectId) + expect(assignedConfigObjectIds).toContain(marketplaceGrantedMcp.configObjectId) + expect(assignedConfigObjectIds).not.toContain(denOnlySkill.configObjectId) + expect(assignedConfigObjectIds).not.toContain(denOnlyCommand.configObjectId) + expect(assignedConfigObjectIds).not.toContain(denOnlyMcp.configObjectId) + + const matches = await marketplaceCapabilities.searchMarketplaceCapabilities({ + organizationId: admin.organizationId, + member: admin.member, + query: "admin access", + limit: 20, + enabled: true, + }) + + expect(matches.find((match) => match.name === assignedSkill.name)?.kind).toBe("skill") + expect(matches.find((match) => match.name === pluginGrantedCommand.name)?.kind).toBe("command") + expect(matches.find((match) => match.name === marketplaceGrantedMcp.name)?.kind).toBe("mcp") + expect(matches.some((match) => match.name === denOnlySkill.name)).toBe(false) + expect(matches.some((match) => match.name === denOnlyCommand.name)).toBe(false) + expect(matches.some((match) => match.name === denOnlyMcp.name)).toBe(false) + + expect((await execute(admin, assignedSkill)).ok).toBe(true) + expect((await execute(admin, pluginGrantedCommand)).ok).toBe(true) + expect((await execute(admin, marketplaceGrantedMcp)).ok).toBe(true) + expect(await execute(admin, denOnlySkill)).toMatchObject({ ok: false, error: "forbidden" }) + expect(await execute(admin, denOnlyCommand)).toMatchObject({ ok: false, error: "forbidden" }) + expect(await execute(admin, denOnlyMcp)).toMatchObject({ ok: false, error: "forbidden" }) + }) + test("search finds a published plugin skill and execute returns provenance-framed raw content", async () => { const owner = await seedMember() const seeded = await seedCapability({ diff --git a/evals/flows/admin-desktop-skill-grants.flow.mjs b/evals/flows/admin-desktop-skill-grants.flow.mjs new file mode 100644 index 0000000000..9e9f267411 --- /dev/null +++ b/evals/flows/admin-desktop-skill-grants.flow.mjs @@ -0,0 +1,611 @@ +import { mkdirSync } from "node:fs"; +import { loadVoiceoverParagraphs } from "../runner/voiceover.mjs"; + +const FLOW_ID = "admin-desktop-skill-grants"; +const DEN_API_URL = cleanBaseUrl(process.env.OPENWORK_EVAL_DEN_API_URL); +const DEN_WEB_URL = cleanBaseUrl(process.env.OPENWORK_EVAL_DEN_WEB_URL); +const ADMIN_EMAIL = process.env.OPENWORK_EVAL_DEMO_EMAIL?.trim() || "alex@acme.test"; +const ADMIN_PASSWORD = process.env.OPENWORK_EVAL_DEMO_PASSWORD?.trim() || "OpenWorkDemo123!"; +const WORKSPACE_PATH = process.env.OPENWORK_EVAL_WORKSPACE_PATH?.trim() || "/tmp/openwork-admin-desktop-skill-grants"; +const RUN_TAG = Date.now(); +const APPROVED_TITLE = `Admin Access Approved Skill ${RUN_TAG}`; +const DEN_ONLY_TITLE = `Admin Access Den Only Skill ${RUN_TAG}`; +const PLUGIN_GRANTED_TITLE = `Admin Access Plugin Granted Command ${RUN_TAG}`; +const PLUGIN_DEN_ONLY_TITLE = `Admin Access Den Only Command ${RUN_TAG}`; +const MCP_GRANTED_TITLE = `Admin Access Marketplace Granted MCP ${RUN_TAG}`; +const MCP_DEN_ONLY_TITLE = `Admin Access Den Only MCP ${RUN_TAG}`; +const ALL_MANAGED_TITLES = [ + APPROVED_TITLE, + DEN_ONLY_TITLE, + PLUGIN_GRANTED_TITLE, + PLUGIN_DEN_ONLY_TITLE, + MCP_GRANTED_TITLE, + MCP_DEN_ONLY_TITLE, +]; +const vo = await loadVoiceoverParagraphs(FLOW_ID); + +const state = { + adminToken: null, + organizationId: null, + memberId: null, + approvedCapability: null, + denOnlyCapability: null, + pluginGrantedCapability: null, + pluginDenOnlyCapability: null, + mcpGrantedCapability: null, + mcpDenOnlyCapability: null, + denManagedTitles: [], + assignedConfigObjectIds: [], + discoveredTitles: [], + searchedCapabilityNames: [], + allowedPayloads: {}, + deniedPayloads: {}, +}; + +export default { + id: FLOW_ID, + title: "Administrators receive only explicitly granted desktop Connect capabilities", + kind: "user-facing", + requiredEnv: ["OPENWORK_EVAL_DEN_API_URL", "OPENWORK_EVAL_DEN_WEB_URL", "OPENWORK_EVAL_WORKSPACE_PATH"], + steps: [ + { + name: "Den administration remains complete", + run: async (ctx) => { + await ctx.prove("The administrator can still manage every Skill, plugin capability, and MCP declaration in Den", { + voiceover: vo[0], + action: async () => { + await prepareScenario(ctx); + await signDesktopIntoDen(ctx); + await ctx.control("settings.panel.open", { panel: "cloud-account" }); + await ctx.waitForText(ADMIN_EMAIL, { timeoutMs: 30_000 }); + }, + assert: async () => { + for (const title of ALL_MANAGED_TITLES) { + witness( + ctx, + state.denManagedTitles.includes(title), + `Den management lists ${title}`, + state.denManagedTitles, + ); + } + await ctx.expectText(ADMIN_EMAIL); + await ctx.expectText("OpenWork Cloud"); + }, + screenshot: { + name: "admin-den-management-context", + requireText: ["OpenWork Cloud", ADMIN_EMAIL, "Connected"], + rejectText: ["Something went wrong"], + }, + }); + }, + }, + { + name: "Desktop discovery and execution honor grants", + run: async (ctx) => { + await ctx.prove("The administrator agent can discover, search, and execute only granted Skills, plugin capabilities, and MCP declarations", { + voiceover: vo[1], + action: async () => { + await verifyAgentMcpBoundary(ctx); + await ctx.control("settings.panel.open", { panel: "extensions" }); + await ctx.waitForText("Extensions", { timeoutMs: 30_000 }); + await ctx.clickText("Skills", { selector: "button", timeoutMs: 30_000 }); + }, + assert: async () => { + witness( + ctx, + state.discoveredTitles.includes(APPROVED_TITLE), + "The live desktop MCP Skill index includes the explicitly granted Skill", + state.discoveredTitles, + ); + witness( + ctx, + !state.discoveredTitles.includes(DEN_ONLY_TITLE), + "The live desktop MCP Skill index excludes the unassigned Den-only Skill", + state.discoveredTitles, + ); + for (const [label, capability] of [ + ["Skill", state.approvedCapability], + ["plugin-granted command", state.pluginGrantedCapability], + ["marketplace-granted MCP declaration", state.mcpGrantedCapability], + ]) { + witness( + ctx, + state.assignedConfigObjectIds.includes(capabilityConfigObjectId(capability)), + `The desktop inventory endpoint returns the granted ${label}`, + state.assignedConfigObjectIds, + ); + } + for (const [label, capability] of [ + ["Skill", state.denOnlyCapability], + ["command", state.pluginDenOnlyCapability], + ["MCP declaration", state.mcpDenOnlyCapability], + ]) { + witness( + ctx, + !state.assignedConfigObjectIds.includes(capabilityConfigObjectId(capability)), + `The desktop inventory endpoint excludes the unassigned ${label}`, + state.assignedConfigObjectIds, + ); + } + witness( + ctx, + state.searchedCapabilityNames.includes(state.approvedCapability), + "search_capabilities returns the explicitly granted Skill", + state.searchedCapabilityNames, + ); + witness( + ctx, + !state.searchedCapabilityNames.includes(state.denOnlyCapability), + "search_capabilities excludes the unassigned Den-only Skill", + state.searchedCapabilityNames, + ); + for (const [label, capability] of [ + ["plugin-granted command", state.pluginGrantedCapability], + ["marketplace-granted MCP declaration", state.mcpGrantedCapability], + ]) { + witness( + ctx, + state.searchedCapabilityNames.includes(capability), + `search_capabilities returns the ${label}`, + state.searchedCapabilityNames, + ); + } + for (const [label, capability] of [ + ["unassigned command", state.pluginDenOnlyCapability], + ["unassigned MCP declaration", state.mcpDenOnlyCapability], + ]) { + witness( + ctx, + !state.searchedCapabilityNames.includes(capability), + `search_capabilities excludes the ${label}`, + state.searchedCapabilityNames, + ); + } + for (const [label, payload] of Object.entries(state.allowedPayloads)) { + witness(ctx, payload?.error === undefined, `Direct execution allows the granted ${label}`, payload); + } + for (const [label, payload] of Object.entries(state.deniedPayloads)) { + witness(ctx, payload?.error === "forbidden", `Direct execution rejects the unassigned ${label}`, payload); + } + ctx.output("Admin capability grant boundary", JSON.stringify({ + approvedSkill: APPROVED_TITLE, + denOnlySkill: DEN_ONLY_TITLE, + pluginGrantedCommand: PLUGIN_GRANTED_TITLE, + denOnlyCommand: PLUGIN_DEN_ONLY_TITLE, + marketplaceGrantedMcp: MCP_GRANTED_TITLE, + denOnlyMcp: MCP_DEN_ONLY_TITLE, + discoveredTitles: state.discoveredTitles, + searchedCapabilityNames: state.searchedCapabilityNames, + allowedExecution: state.allowedPayloads, + deniedExecution: state.deniedPayloads, + }, null, 2)); + await ctx.expectText("Extensions"); + await ctx.expectText("Skills"); + await ctx.expectText(APPROVED_TITLE); + await ctx.expectNoText(DEN_ONLY_TITLE); + }, + screenshot: { + name: "admin-desktop-cloud-connection", + requireText: ["Extensions", "Skills", APPROVED_TITLE], + rejectText: ["Something went wrong", DEN_ONLY_TITLE], + hashIncludes: "/settings/extensions", + }, + }); + }, + }, + { + name: "Desktop MCP connection context honors grants", + run: async (ctx) => { + await ctx.prove("The administrator desktop shows the granted MCP declaration without exposing the unassigned MCP", { + voiceover: vo[2], + action: async () => { + await ctx.control("settings.panel.open", { panel: "extensions" }); + await ctx.waitForText("Extensions", { timeoutMs: 30_000 }); + await ctx.clickText("Connections", { selector: "button", timeoutMs: 30_000 }); + await ctx.waitForText(MCP_GRANTED_TITLE, { timeoutMs: 30_000 }); + }, + assert: async () => { + await ctx.expectText(MCP_GRANTED_TITLE); + await ctx.expectNoText(MCP_DEN_ONLY_TITLE); + }, + screenshot: { + name: "admin-desktop-assigned-mcp-context", + requireText: ["Extensions", "Connections", MCP_GRANTED_TITLE], + rejectText: ["Something went wrong", MCP_DEN_ONLY_TITLE], + hashIncludes: "/settings/extensions", + }, + }); + }, + }, + ], +}; + +function cleanBaseUrl(value) { + return (value ?? "").trim().replace(/\/+$/, ""); +} + +function witness(ctx, condition, assertion, actual) { + ctx.recordEvidence({ type: "assertion", status: condition ? "passed" : "failed", assertion, actual }); + ctx.assert(condition, `${assertion}: ${JSON.stringify(actual)}`); +} + +function capabilityConfigObjectId(capability) { + return capability?.split(":")[2] ?? ""; +} + +async function denFetch(pathname, options = {}) { + const response = await fetch(`${DEN_API_URL}${pathname}`, { + ...options, + headers: { + "content-type": "application/json", + origin: DEN_WEB_URL, + ...(options.headers ?? {}), + }, + }); + const text = await response.text(); + let body = text; + try { + body = text ? JSON.parse(text) : null; + } catch { + body = text; + } + return { response, body, text }; +} + +async function prepareScenario(ctx) { + const signedIn = await denFetch("/api/auth/sign-in/email", { + method: "POST", + body: JSON.stringify({ email: ADMIN_EMAIL, password: ADMIN_PASSWORD }), + }); + ctx.assert( + signedIn.response.ok && typeof signedIn.body?.token === "string", + `Admin sign-in failed: ${signedIn.response.status} ${signedIn.text.slice(0, 300)}`, + ); + state.adminToken = signedIn.body.token; + + const orgs = await denFetch("/v1/me/orgs", { + headers: { authorization: `Bearer ${state.adminToken}` }, + }); + ctx.assert(orgs.response.ok && Array.isArray(orgs.body?.orgs), `Organization lookup failed: ${orgs.text.slice(0, 300)}`); + const active = orgs.body.orgs.find((org) => org.id === orgs.body.activeOrgId) ?? orgs.body.orgs[0]; + ctx.assert(typeof active?.id === "string", "No active organization resolved for the administrator."); + state.organizationId = active.id; + + const activated = await denFetch("/v1/me/active-organization", { + method: "POST", + headers: { authorization: `Bearer ${state.adminToken}` }, + body: JSON.stringify({ organizationId: state.organizationId }), + }); + ctx.assert(activated.response.ok, `Active organization update failed: ${activated.text.slice(0, 300)}`); + + await seedCapabilityMatrix(ctx); + + const managed = await denFetch(`/v1/plugins?limit=100&q=${encodeURIComponent(String(RUN_TAG))}`, { + headers: { authorization: `Bearer ${state.adminToken}` }, + }); + ctx.assert(managed.response.ok && Array.isArray(managed.body?.items), `Den plugin list failed: ${managed.text.slice(0, 300)}`); + state.denManagedTitles = managed.body.items.flatMap((plugin) => + Array.isArray(plugin.configObjects) ? plugin.configObjects.map((item) => item.title) : [] + ); + if (state.denManagedTitles.length === 0) { + state.denManagedTitles = ALL_MANAGED_TITLES.filter((title) => managed.text.includes(title)); + } +} + +async function seedCapabilityMatrix(ctx) { + const { createDenDb } = await import("../../ee/packages/den-db/dist/index.js"); + const { + ConfigObjectAccessGrantTable, + ConfigObjectTable, + ConfigObjectVersionTable, + MarketplaceAccessGrantTable, + MarketplacePluginTable, + MarketplaceTable, + MemberTable, + PluginAccessGrantTable, + PluginConfigObjectTable, + PluginTable, + } = await import("../../ee/packages/den-db/dist/schema.js"); + const { and, eq, isNull } = await import("../../ee/packages/den-db/dist/drizzle.js"); + const { createDenTypeId, normalizeDenTypeId } = await import("../../ee/packages/utils/dist/typeid.js"); + const databaseUrl = process.env.DATABASE_URL; + ctx.assert(typeof databaseUrl === "string" && databaseUrl.length > 0, "DATABASE_URL is required for the isolated proof seed."); + const denDb = createDenDb({ databaseUrl, mode: "mysql" }); + const database = denDb.db; + const organizationId = normalizeDenTypeId("organization", state.organizationId); + const members = await database + .select({ id: MemberTable.id, role: MemberTable.role }) + .from(MemberTable) + .where(and( + eq(MemberTable.organizationId, organizationId), + isNull(MemberTable.removedAt), + )); + const administrator = members.find((member) => member.role.split(",").some((role) => ["owner", "admin"].includes(role.trim()))); + ctx.assert(Boolean(administrator), "The demo organization has no active owner or administrator membership."); + state.memberId = administrator.id; + + const seedOne = async ({ grant, objectType, title }) => { + const now = new Date(); + const marketplaceId = createDenTypeId("marketplace"); + const pluginId = createDenTypeId("plugin"); + const configObjectId = createDenTypeId("configObject"); + const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, "-"); + const isMcp = objectType === "mcp"; + const normalizedPayloadJson = isMcp + ? { mcpServers: { proof: { url: `https://${slug}.example.test/remote` } } } + : null; + const rawSourceText = isMcp + ? JSON.stringify(normalizedPayloadJson) + : objectType === "command" + ? `Run ${title}.` + : `---\nname: ${slug}\ndescription: ${title}\n---\n\n# ${title}`; + await database.insert(MarketplaceTable).values({ + id: marketplaceId, + organizationId, + name: `${title} Marketplace`, + description: "PR #3159 end-to-end proof", + logoUrl: null, + status: "active", + createdByOrgMembershipId: state.memberId, + createdAt: now, + updatedAt: now, + deletedAt: null, + }); + await database.insert(PluginTable).values({ + id: pluginId, + organizationId, + name: `${title} Plugin`, + description: "PR #3159 end-to-end proof", + status: "active", + createdByOrgMembershipId: state.memberId, + createdAt: now, + updatedAt: now, + deletedAt: null, + }); + await database.insert(ConfigObjectTable).values({ + id: configObjectId, + organizationId, + objectType, + sourceMode: "cloud", + title, + description: `Proof ${objectType} ${title}`, + searchText: title, + currentFileName: `${slug}${isMcp ? ".json" : ".md"}`, + currentFileExtension: isMcp ? ".json" : ".md", + currentRelativePath: `${objectType}s/${slug}${objectType === "skill" ? "/SKILL.md" : isMcp ? ".json" : ".md"}`, + status: "active", + createdByOrgMembershipId: state.memberId, + connectorInstanceId: null, + createdAt: now, + updatedAt: now, + deletedAt: null, + }); + await database.insert(MarketplacePluginTable).values({ + id: createDenTypeId("marketplacePlugin"), + organizationId, + marketplaceId, + pluginId, + membershipSource: "manual", + createdByOrgMembershipId: state.memberId, + createdAt: now, + removedAt: null, + }); + await database.insert(PluginConfigObjectTable).values({ + id: createDenTypeId("pluginConfigObject"), + organizationId, + pluginId, + configObjectId, + membershipSource: "manual", + connectorMappingId: null, + createdByOrgMembershipId: state.memberId, + createdAt: now, + removedAt: null, + }); + await database.insert(ConfigObjectVersionTable).values({ + id: createDenTypeId("configObjectVersion"), + organizationId, + configObjectId, + normalizedPayloadJson, + rawSourceText, + schemaVersion: null, + createdVia: "cloud", + createdByOrgMembershipId: state.memberId, + connectorSyncEventId: null, + sourceRevisionRef: null, + isDeletedVersion: false, + createdAt: now, + }); + if (grant === "config_object") { + await database.insert(ConfigObjectAccessGrantTable).values({ + id: createDenTypeId("configObjectAccessGrant"), + organizationId, + configObjectId, + orgMembershipId: state.memberId, + teamId: null, + orgWide: false, + role: "viewer", + createdByOrgMembershipId: state.memberId, + createdAt: now, + removedAt: null, + }); + } + if (grant === "plugin") { + await database.insert(PluginAccessGrantTable).values({ + id: createDenTypeId("pluginAccessGrant"), + organizationId, + pluginId, + orgMembershipId: state.memberId, + teamId: null, + orgWide: false, + role: "viewer", + createdByOrgMembershipId: state.memberId, + createdAt: now, + removedAt: null, + }); + } + if (grant === "marketplace") { + await database.insert(MarketplaceAccessGrantTable).values({ + id: createDenTypeId("marketplaceAccessGrant"), + organizationId, + marketplaceId, + orgMembershipId: state.memberId, + teamId: null, + orgWide: false, + role: "viewer", + createdByOrgMembershipId: state.memberId, + createdAt: now, + removedAt: null, + }); + } + return `plugin:${pluginId}:${configObjectId}`; + }; + + state.approvedCapability = await seedOne({ grant: "config_object", objectType: "skill", title: APPROVED_TITLE }); + state.denOnlyCapability = await seedOne({ grant: "none", objectType: "skill", title: DEN_ONLY_TITLE }); + state.pluginGrantedCapability = await seedOne({ grant: "plugin", objectType: "command", title: PLUGIN_GRANTED_TITLE }); + state.pluginDenOnlyCapability = await seedOne({ grant: "none", objectType: "command", title: PLUGIN_DEN_ONLY_TITLE }); + state.mcpGrantedCapability = await seedOne({ grant: "marketplace", objectType: "mcp", title: MCP_GRANTED_TITLE }); + state.mcpDenOnlyCapability = await seedOne({ grant: "none", objectType: "mcp", title: MCP_DEN_ONLY_TITLE }); + await denDb.client.end(); +} + +async function signDesktopIntoDen(ctx) { + mkdirSync(WORKSPACE_PATH, { recursive: true }); + await ctx.waitFor("Boolean(window.__openworkControl)", { timeoutMs: 120_000, label: "desktop control API" }); + await ctx.waitFor("Boolean(window.__OPENWORK_ELECTRON__?.invokeDesktop)", { timeoutMs: 30_000, label: "desktop bridge" }); + const bootstrap = { baseUrl: DEN_WEB_URL, apiBaseUrl: DEN_API_URL, requireSignin: false, handoff: null }; + const configured = await ctx.eval(`(async () => { + const bridge = window.__OPENWORK_ELECTRON__?.invokeDesktop; + if (!bridge) return false; + await bridge("setDesktopBootstrapConfig", ${JSON.stringify(bootstrap)}); + localStorage.setItem("openwork.den.baseUrl", ${JSON.stringify(DEN_WEB_URL)}); + localStorage.setItem("openwork.den.apiBaseUrl", ${JSON.stringify(DEN_API_URL)}); + for (const key of ["openwork.den.authToken", "openwork.den.activeOrgId", "openwork.den.activeOrgSlug", "openwork.den.activeOrgName"]) { + localStorage.removeItem(key); + } + return true; + })()`, { awaitPromise: true }); + ctx.assert(configured === true, "Desktop bootstrap configuration failed."); + await ctx.eval("location.reload()"); + await ctx.waitFor("Boolean(window.__openworkControl)", { timeoutMs: 60_000, label: "control API after reload" }); + + const handoff = await denFetch("/v1/auth/desktop-handoff", { + method: "POST", + headers: { authorization: `Bearer ${state.adminToken}` }, + body: JSON.stringify({ desktopScheme: "openwork" }), + }); + ctx.assert(handoff.response.ok && typeof handoff.body?.grant === "string", `Desktop handoff failed: ${handoff.text.slice(0, 300)}`); + await ctx.waitFor( + "window.__openworkControl?.listActions().some((action) => action.id === 'auth.exchange-grant' && !action.disabled)", + { timeoutMs: 30_000, label: "auth.exchange-grant action" }, + ); + await ctx.control("auth.exchange-grant", { grant: handoff.body.grant, baseUrl: DEN_WEB_URL }); + await ctx.waitFor("Boolean((localStorage.getItem('openwork.den.authToken') ?? '').trim())", { + timeoutMs: 45_000, + label: "desktop Den token", + }); + + await ctx.clickText("Continue with organization", { timeoutMs: 5_000 }).catch(() => {}); + await ctx.clickText("Continue to workspace", { timeoutMs: 8_000 }).catch(() => {}); + const folderInput = await ctx.eval("Boolean(document.querySelector('input[placeholder=\"/workspace/my-project\"]'))").catch(() => false); + if (folderInput) { + await ctx.fill('input[placeholder="/workspace/my-project"]', WORKSPACE_PATH); + await ctx.clickText("Use this folder", { timeoutMs: 10_000 }); + } + + const selectedWorkspace = await ctx.eval( + "Boolean((localStorage.getItem('openwork.react.activeWorkspace') ?? '').trim())", + ); + if (!selectedWorkspace) { + await ctx.waitFor( + "window.__openworkControl?.listActions().some((action) => action.id === 'workspace.create' && !action.disabled)", + { timeoutMs: 60_000, label: "workspace.create action" }, + ); + await ctx.control("workspace.create", { path: WORKSPACE_PATH }); + await ctx.waitFor( + "Boolean((localStorage.getItem('openwork.react.activeWorkspace') ?? '').trim())", + { timeoutMs: 60_000, label: "selected eval workspace" }, + ); + } +} + +async function mcpCall(ctx, token, method, params = {}) { + const response = await fetch(`${DEN_API_URL}/mcp/agent`, { + method: "POST", + headers: { + accept: "application/json, text/event-stream", + authorization: `Bearer ${token}`, + "content-type": "application/json", + }, + body: JSON.stringify({ jsonrpc: "2.0", id: Date.now(), method, params }), + }); + const raw = await response.text(); + ctx.assert(response.ok, `MCP ${method} failed: ${response.status} ${raw.slice(0, 300)}`); + const dataLine = raw.split("\n").find((line) => line.startsWith("data:")); + ctx.assert(Boolean(dataLine), `MCP ${method} returned no data frame: ${raw.slice(0, 300)}`); + const payload = JSON.parse(dataLine.slice(5)); + ctx.assert(!payload.error, `MCP ${method} returned ${JSON.stringify(payload.error)}`); + return payload.result; +} + +async function verifyAgentMcpBoundary(ctx) { + const minted = await denFetch("/v1/mcp/token", { + method: "POST", + headers: { authorization: `Bearer ${state.adminToken}` }, + body: JSON.stringify({}), + }); + ctx.assert(minted.response.ok && typeof minted.body?.token === "string", `MCP token mint failed: ${minted.text.slice(0, 300)}`); + + const resources = await mcpCall(ctx, minted.body.token, "resources/list"); + state.discoveredTitles = (resources.resources ?? []).map((resource) => resource.title).filter(Boolean); + + const assigned = await denFetch("/v1/resources/marketplace-capabilities", { + headers: { authorization: `Bearer ${state.adminToken}` }, + }); + ctx.assert( + assigned.response.ok && Array.isArray(assigned.body?.items), + `Assigned desktop capability inventory failed: ${assigned.response.status} ${assigned.text.slice(0, 300)}`, + ); + state.assignedConfigObjectIds = assigned.body.items.map((item) => item.configObjectId).filter(Boolean); + + const searched = await mcpCall(ctx, minted.body.token, "tools/call", { + name: "search_capabilities", + arguments: { query: `Admin Access ${RUN_TAG}`, type: "marketplace", limit: 20 }, + }); + const searchedText = searched.content?.[0]?.text ?? "{}"; + let searchedPayload; + try { + searchedPayload = JSON.parse(searchedText); + } catch { + searchedPayload = { raw: searchedText }; + } + ctx.assert(Array.isArray(searchedPayload?.matches), `Unexpected search_capabilities payload: ${searchedText.slice(0, 300)}`); + state.searchedCapabilityNames = searchedPayload.matches.map((match) => match.name).filter(Boolean); + + const execute = async (name) => { + const result = await mcpCall(ctx, minted.body.token, "tools/call", { + name: "execute_capability", + arguments: { name }, + }); + const text = result.content?.[0]?.text ?? "{}"; + try { + return JSON.parse(text); + } catch { + return { raw: text }; + } + }; + for (const [label, capability] of [ + ["Skill", state.approvedCapability], + ["plugin-granted command", state.pluginGrantedCapability], + ["marketplace-granted MCP declaration", state.mcpGrantedCapability], + ]) { + state.allowedPayloads[label] = await execute(capability); + } + for (const [label, capability] of [ + ["Skill", state.denOnlyCapability], + ["command", state.pluginDenOnlyCapability], + ["MCP declaration", state.mcpDenOnlyCapability], + ]) { + state.deniedPayloads[label] = await execute(capability); + } +} diff --git a/evals/voiceovers/admin-desktop-skill-grants.md b/evals/voiceovers/admin-desktop-skill-grants.md new file mode 100644 index 0000000000..f0c1a421b8 --- /dev/null +++ b/evals/voiceovers/admin-desktop-skill-grants.md @@ -0,0 +1,7 @@ +# Admin desktop Skill grants + +1. I am signed in as an Acme administrator. Den still lets me manage the complete catalog—including assigned and unassigned Skills, plugin capabilities, and MCP declarations—so administration has not been reduced. + +2. For the same administrator’s working context, OpenWork Cloud applies grants consistently. The desktop inventory, agent search, and execution allow granted capabilities while excluding or rejecting every unassigned counterpart. + +3. In the desktop MCP context, the administrator sees the MCP declaration intentionally granted through its marketplace. The unassigned MCP remains manageable in Den but is not exposed here.