Repository navigation
Add exact per-account Google toolkit bundles #10
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "@executor-js/plugin-toolkits": patch | ||
| --- | ||
|
|
||
| Add per-account Email read-only, Drive read-only, and Photos picker tool bundles. Selection saves exact current tool addresses, preserves account permissions and approval policies, and does not grant future operations automatically. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| import { expect, it } from "@effect/vitest"; | ||
| import { googleToolkitBundles } from "./google-bundles"; | ||
|
|
||
| it("selects exact read operations and excludes send, modify, delete and permission writes", () => { | ||
| const names = [ | ||
| "gmail.users.messages.list", | ||
| "gmail.users.messages.get", | ||
| "gmail.users.messages.send", | ||
| "gmail.users.messages.modify", | ||
| "gmail.users.messages.delete", | ||
| "drive.files.list", | ||
| "drive.files.export", | ||
| "drive.files.delete", | ||
| "drive.permissions.create", | ||
| ]; | ||
| const bundles = googleToolkitBundles( | ||
| names.map((name) => ({ name, pattern: `service.org.account.${name}` })), | ||
| ); | ||
| expect(bundles.map((bundle) => [bundle.name, bundle.patterns])).toEqual([ | ||
| [ | ||
| "Email read-only", | ||
| [ | ||
| "service.org.account.gmail.users.messages.get", | ||
| "service.org.account.gmail.users.messages.list", | ||
| ], | ||
| ], | ||
| [ | ||
| "Drive read-only", | ||
| ["service.org.account.drive.files.export", "service.org.account.drive.files.list"], | ||
| ], | ||
| ]); | ||
| }); | ||
|
|
||
| it("includes picker session management but excludes Library uploads", () => { | ||
| const names = [ | ||
| "photospicker.sessions.create", | ||
| "photospicker.sessions.delete", | ||
| "photospicker.mediaItems.list", | ||
| "photoslibrary.mediaItems.batchCreate", | ||
| "photoslibrary.uploads.upload", | ||
| ]; | ||
| const bundles = googleToolkitBundles( | ||
| names.map((name) => ({ name, pattern: `photos.org.main.${name}` })), | ||
| ); | ||
| expect(bundles.map((bundle) => bundle.patterns)).toEqual([ | ||
| [ | ||
| "photos.org.main.photospicker.mediaItems.list", | ||
| "photos.org.main.photospicker.sessions.create", | ||
| "photos.org.main.photospicker.sessions.delete", | ||
| ], | ||
| ]); | ||
| }); | ||
|
|
||
| it("deduplicates current addresses without widening account or operation selection", () => { | ||
| const tools = [ | ||
| { name: "gmail.users.messages.list", pattern: "gmail.user.mine.gmail.users.messages.list" }, | ||
| ]; | ||
| expect(googleToolkitBundles([...tools, ...tools])[0]?.patterns).toEqual([tools[0].pattern]); | ||
| expect(googleToolkitBundles([])).toEqual([]); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| const readOperation = (name: string): boolean => | ||
| ["get", "list", "export"].includes(name.split(".").at(-1) ?? ""); | ||
|
|
||
| const templates = [ | ||
| { | ||
| id: "google-email-readonly", | ||
| name: "Email read-only", | ||
| summary: "Current Gmail get and list tools.", | ||
| matches: (name: string) => name.startsWith("gmail.") && readOperation(name), | ||
| }, | ||
| { | ||
| id: "google-photos-picker", | ||
| name: "Photos picker", | ||
| summary: "Picker sessions and explicitly selected media. No Library uploads.", | ||
| matches: (name: string) => name.startsWith("photospicker."), | ||
| }, | ||
| { | ||
| id: "google-drive-readonly", | ||
| name: "Drive read-only", | ||
| summary: "Current Drive get, list, and export tools.", | ||
| matches: (name: string) => name.startsWith("drive.") && readOperation(name), | ||
| }, | ||
| ] as const; | ||
|
|
||
| /** Snapshot exact tool addresses. New operations are never granted implicitly. */ | ||
| export const googleToolkitBundles = ( | ||
| tools: readonly { readonly name: string; readonly pattern: string }[], | ||
| ) => | ||
| templates.flatMap(({ matches, ...template }) => { | ||
| const patterns = [ | ||
| ...new Set(tools.filter((tool) => matches(tool.name)).map((tool) => tool.pattern)), | ||
| ].sort((a, b) => a.localeCompare(b)); | ||
| return patterns.length > 0 ? [{ ...template, patterns }] : []; | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,5 @@ | ||
| import { useId, useMemo, useState } from "react"; | ||
| import { googleToolkitBundles } from "./google-bundles"; | ||
| import { Link, useNavigate } from "@tanstack/react-router"; | ||
| import { useAtomSet, useAtomValue } from "@effect/atom-react"; | ||
| import * as Atom from "effect/unstable/reactivity/Atom"; | ||
|
|
@@ -385,8 +386,10 @@ const configuredConnectionViews = ( | |
| showOwnerLabels: boolean, | ||
| ): readonly ConfiguredConnectionView[] => | ||
| connections.map((connection) => { | ||
| const group = connectionGroups.find((candidate) => | ||
| candidate.patterns.includes(connection.pattern), | ||
| const group = connectionGroups.find( | ||
| (candidate) => | ||
| candidate.patterns.includes(connection.pattern) || | ||
| candidate.tools.some((tool) => toolMatchId(tool) === connection.pattern), | ||
| ); | ||
| if (!group) { | ||
| return { | ||
|
|
@@ -812,6 +815,37 @@ function AddConnectionDialog(props: { | |
| }); | ||
| }, [props.groups, trimmedQuery]); | ||
|
|
||
| const bundleRows = useMemo( | ||
| () => | ||
| filteredGroups.flatMap((group) => { | ||
| const integration = props.integrations.find( | ||
| (row) => String(row.slug) === group.integration, | ||
| ); | ||
| if (integration?.family !== "google") return []; | ||
| return googleToolkitBundles( | ||
| group.tools | ||
| .filter((tool) => !tool.static) | ||
| .map((tool) => ({ | ||
| name: tool.name, | ||
| pattern: toolMatchId(tool), | ||
| })), | ||
| ).map((bundle) => ({ | ||
| ...bundle, | ||
| group, | ||
| exactConnectionIds: props.configuredConnections | ||
| .filter((connection) => bundle.patterns.includes(connection.pattern)) | ||
| .map((connection) => connection.id), | ||
| missingPatterns: bundle.patterns.filter( | ||
| (pattern) => | ||
| !props.configuredConnections.some((connection) => | ||
| matchPattern(connection.pattern, pattern), | ||
| ), | ||
|
Comment on lines
+838
to
+842
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
For a migrated toolkit whose account access is still represented by a legacy broad Useful? React with 👍 / 👎. |
||
| ), | ||
| })); | ||
| }), | ||
| [filteredGroups, props.integrations, props.configuredConnections], | ||
| ); | ||
|
|
||
| const addConnection = async (patterns: readonly string[]) => { | ||
| await props.onAddPatterns(patterns); | ||
| }; | ||
|
|
@@ -877,6 +911,62 @@ function AddConnectionDialog(props: { | |
| ) : null} | ||
| </div> | ||
| <div className="min-h-0 overflow-y-auto p-3" style={{ maxHeight: "32rem" }}> | ||
| {bundleRows.length > 0 ? ( | ||
| <div className="mb-4 space-y-2"> | ||
| <div className="px-1 text-[10px] font-semibold uppercase tracking-widest text-muted-foreground"> | ||
| Tool bundles | ||
| </div> | ||
| <p className="px-1 text-xs text-muted-foreground"> | ||
| Select current tools only. Account permissions stay the same. | ||
| </p> | ||
| <div className="overflow-hidden rounded-md border border-border/70 bg-background/30"> | ||
| {bundleRows.map((bundle) => ( | ||
| <div | ||
| key={`${bundle.group.id}:${bundle.id}`} | ||
| className="flex min-w-0 items-center gap-3 border-b border-border/50 px-3 py-2.5 last:border-b-0" | ||
| > | ||
| <BoxIcon className="size-4 shrink-0 text-muted-foreground" /> | ||
| <div className="min-w-0 flex-1 space-y-1"> | ||
| <div className="text-xs font-medium">{bundle.name}</div> | ||
| <div className="text-[11px] text-muted-foreground"> | ||
| {connectionTitleForHost(bundle.group, props.showOwnerLabels)} ·{" "} | ||
| {bundle.patterns.length} tools | ||
| </div> | ||
| <p className="text-[11px] text-muted-foreground">{bundle.summary}</p> | ||
| </div> | ||
| <Button | ||
| type="button" | ||
| size="sm" | ||
| variant="outline" | ||
| disabled={bundle.missingPatterns.length === 0} | ||
| aria-label={`Add bundle ${bundle.name} from ${connectionTitleForHost(bundle.group, props.showOwnerLabels)}`} | ||
| onClick={() => void addConnection(bundle.missingPatterns)} | ||
|
Comment on lines
+941
to
+943
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Adding a bundle creates one toolkit connection record for every exact tool pattern, but Useful? React with 👍 / 👎. |
||
| > | ||
| {bundle.missingPatterns.length === 0 | ||
| ? "Added" | ||
| : `Add ${bundle.missingPatterns.length}`} | ||
| </Button> | ||
| {bundle.exactConnectionIds.length > 0 ? ( | ||
| <Button | ||
| type="button" | ||
| size="sm" | ||
| variant="outline" | ||
| aria-label={`Remove bundle ${bundle.name} from ${connectionTitleForHost(bundle.group, props.showOwnerLabels)}`} | ||
| onClick={() => | ||
| void (async () => { | ||
| for (const id of bundle.exactConnectionIds) | ||
| await removeConnection(id); | ||
| })() | ||
| } | ||
| > | ||
| Remove | ||
| </Button> | ||
| ) : null} | ||
| </div> | ||
| ))} | ||
| </div> | ||
| </div> | ||
| ) : null} | ||
| <div className="mb-2 flex items-center justify-between px-1"> | ||
| <span className="text-[10px] font-semibold uppercase tracking-widest text-muted-foreground"> | ||
| Connections | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The reviewed commit records
Codex <codex@openai.com>as its author, so publishing it would put explicit AI attribution into a public commit. Rewrite the commit metadata to use the responsible human or approved project identity before this change is published.AGENTS.md reference: AGENTS.md:L41-L42
Useful? React with 👍 / 👎.