Skip to content
Open
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
5 changes: 5 additions & 0 deletions .changeset/desktop-toolkits-personal-connections.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@executor-js/plugin-toolkits": patch
---

Allow toolkits on single-player/desktop hosts to display and manage personal connections without multi-tenant workspace isolation restrictions.
53 changes: 53 additions & 0 deletions packages/plugins/toolkits/src/page.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { describe, expect, it } from "@effect/vitest";
import type { ToolAddress } from "@executor-js/sdk/shared";

import { toolCanAppearInToolkit } from "./page";
import type { ToolkitResponse } from "./shared";

describe("toolCanAppearInToolkit", () => {
const sampleOrgToolkit: ToolkitResponse = {
id: "tk_1",
owner: "org",
slug: "org-kit",
name: "Org Kit",
createdAt: 1,
updatedAt: 1,
};

const sampleUserToolkit: ToolkitResponse = {
id: "tk_2",
owner: "user",
slug: "user-kit",
name: "User Kit",
createdAt: 1,
updatedAt: 1,
};

const userTool = {
address: "tools.github.user.main.repos.list" as ToolAddress,
integration: "github",
owner: "user" as const,
name: "repos.list",
};

const orgTool = {
address: "tools.github.org.main.repos.list" as ToolAddress,
integration: "github",
owner: "org" as const,
name: "repos.list",
};

it("allows all tools when showOwnerLabels is false (single-player / desktop host)", () => {
expect(toolCanAppearInToolkit(sampleOrgToolkit, userTool, false)).toBe(true);
expect(toolCanAppearInToolkit(sampleOrgToolkit, orgTool, false)).toBe(true);
expect(toolCanAppearInToolkit(sampleUserToolkit, userTool, false)).toBe(true);
expect(toolCanAppearInToolkit(sampleUserToolkit, orgTool, false)).toBe(true);
});

it("hides personal tools in org toolkits when showOwnerLabels is true (multiplayer / cloud host)", () => {
expect(toolCanAppearInToolkit(sampleOrgToolkit, userTool, true)).toBe(false);
expect(toolCanAppearInToolkit(sampleOrgToolkit, orgTool, true)).toBe(true);
expect(toolCanAppearInToolkit(sampleUserToolkit, userTool, true)).toBe(true);
expect(toolCanAppearInToolkit(sampleUserToolkit, orgTool, true)).toBe(true);
});
});
25 changes: 16 additions & 9 deletions packages/plugins/toolkits/src/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -156,8 +156,12 @@ const resolveToolkitPolicy = (
const toolMatchId = (tool: ToolRow): string =>
tool.static ? String(tool.address) : String(tool.address).replace(/^tools\./, "");

const toolCanAppearInToolkit = (toolkit: ToolkitResponse, tool: ToolRow): boolean =>
toolkit.owner === "user" || tool.static === true || tool.owner !== "user";
export const toolCanAppearInToolkit = (
toolkit: ToolkitResponse,
tool: ToolRow,
showOwnerLabels = true,
): boolean =>
!showOwnerLabels || toolkit.owner === "user" || tool.static === true || tool.owner !== "user";

const toolkitUrlFor = (orgSlug: string | undefined, slug: string): string => {
const path = orgSlug ? `/${orgSlug}/mcp/toolkits/${slug}` : `/mcp/toolkits/${slug}`;
Expand Down Expand Up @@ -461,10 +465,10 @@ function ToolkitTile(props: { showOwnerLabels: boolean; toolkit: ToolkitResponse
() =>
AsyncResult.isSuccess(tools)
? (tools.value as readonly ToolRow[]).filter((tool) =>
toolCanAppearInToolkit(toolkit, tool),
toolCanAppearInToolkit(toolkit, tool, props.showOwnerLabels),
)
: [],
[toolkit, tools],
[props.showOwnerLabels, toolkit, tools],
);
const connectionGroups = useMemo(() => buildConnectionGroups(visibleTools), [visibleTools]);
const connectionRows = AsyncResult.isSuccess(connections) ? connections.value.connections : [];
Expand Down Expand Up @@ -674,7 +678,7 @@ function ToolkitGrid(props: {
<div className="min-h-0 flex-1 overflow-y-auto">
<div className="w-full space-y-7 px-4 py-4" style={toolkitGridContainerStyle}>
<ToolkitSection
owner="org"
owner="user"
showOwnerLabels={false}
toolkits={props.toolkits}
onCreate={props.onCreate}
Expand Down Expand Up @@ -1050,14 +1054,17 @@ function ToolkitWorkspace(props: {
const [addOpen, setAddOpen] = useState(false);
const [selectedToolId, setSelectedToolId] = useState<string | null>(null);
const visibleTools = useMemo(
() => props.tools.filter((tool) => toolCanAppearInToolkit(props.toolkit, tool)),
[props.toolkit, props.tools],
() =>
props.tools.filter((tool) =>
toolCanAppearInToolkit(props.toolkit, tool, props.showOwnerLabels),
),
[props.showOwnerLabels, props.toolkit, props.tools],
);
const connectionGroups = useMemo(() => buildConnectionGroups(visibleTools), [visibleTools]);
const hiddenPersonalConnectionCount = useMemo(() => {
if (props.toolkit.owner !== "org") return 0;
if (!props.showOwnerLabels || props.toolkit.owner !== "org") return 0;
return buildConnectionGroups(props.tools.filter((tool) => toolOwner(tool) === "user")).length;
}, [props.toolkit.owner, props.tools]);
}, [props.showOwnerLabels, props.toolkit.owner, props.tools]);
const configuredConnections = useMemo(
() =>
configuredConnectionViews(
Expand Down
37 changes: 37 additions & 0 deletions packages/plugins/toolkits/src/server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,4 +199,41 @@ describe("toolkitsPlugin", () => {
).toContain("google_docs.org.* approve");
}),
);

it.effect("allows personal toolkits to connect and resolve personal connections", () =>
Effect.gen(function* () {
const executor = yield* makeTestExecutor({
plugins: [toolkitsPlugin()] as const,
});

const toolkit = yield* executor.toolkits.create({
owner: "user",
name: "Desktop Kit",
});
yield* executor.toolkits.createConnection(toolkit.id, {
pattern: "github.user.main.*",
});

const personalResult = yield* executor.toolkits.resolvePolicyForSlug(
toolkit.slug,
"github.user.main.repos.list",
false,
);
expect(personalResult.action).toBe("approve");

const personalApprovalResult = yield* executor.toolkits.resolvePolicyForSlug(
toolkit.slug,
"github.user.main.repos.list",
true,
);
expect(personalApprovalResult.action).toBe("require_approval");

const preparedResolver = yield* executor.toolkits.preparePolicyResolverForSlug(toolkit.slug);
const preparedResult = preparedResolver({
toolId: "github.user.main.repos.list",
defaultRequiresApproval: false,
});
expect(preparedResult.action).toBe("approve");
}),
);
});
Loading