From 004024b453e9ba07317d2893f050a0d6dae6a67b Mon Sep 17 00:00:00 2001
From: Rhys Sullivan <39114868+RhysSullivan@users.noreply.github.com>
Date: Fri, 18 Sep 2026 10:02:36 -0700
Subject: [PATCH 01/11] Add a self-host opt-out for Better Auth rate limiting
(#2044)
---
.changeset/selfhost-auth-rate-limit.md | 6 ++++++
apps/docs/hosted/docker.mdx | 1 +
apps/host-selfhost/.env.example | 6 ++++++
apps/host-selfhost/src/auth/better-auth.ts | 4 ++++
apps/host-selfhost/src/config.ts | 10 ++++++++++
.../host-selfhost/src/executor-config.test.ts | 19 +++++++++++++++++++
e2e/setup/selfhost-docker.boot.ts | 5 +++++
7 files changed, 51 insertions(+)
create mode 100644 .changeset/selfhost-auth-rate-limit.md
diff --git a/.changeset/selfhost-auth-rate-limit.md b/.changeset/selfhost-auth-rate-limit.md
new file mode 100644
index 0000000000..1324e53436
--- /dev/null
+++ b/.changeset/selfhost-auth-rate-limit.md
@@ -0,0 +1,6 @@
+---
+"@executor-js/host-selfhost": patch
+"executor": patch
+---
+
+Add `EXECUTOR_DISABLE_AUTH_RATE_LIMIT` to the self-host. Better Auth 1.6.17 and later enforce sign-in rate limits strictly in production, and with no trusted proxy header every caller shares one bucket of three sign-ins per ten seconds. The Docker release gate signs in from many test files at once and tripped it. The flag is off by default; the e2e harness sets it for the image it tests.
diff --git a/apps/docs/hosted/docker.mdx b/apps/docs/hosted/docker.mdx
index 8fc30f0e85..807951b07f 100644
--- a/apps/docs/hosted/docker.mdx
+++ b/apps/docs/hosted/docker.mdx
@@ -69,6 +69,7 @@ the container defaults.
| `EXECUTOR_ORG_NAME` | `Default` | Display name of the single org every user joins. |
| `EXECUTOR_ORG_SLUG` | `default` | URL slug for that org. |
| `EXECUTOR_ALLOW_LOCAL_NETWORK` | `false` | Allow sandboxed code to reach loopback / private addresses. Keep off unless you trust the code. |
+| `EXECUTOR_DISABLE_AUTH_RATE_LIMIT` | `false` | Turn off sign-in rate limiting. Only when a proxy or WAF in front of Executor limits instead. |
Tracing is configured separately, and off unless you turn it on — see
[Tracing](/hosted/tracing).
diff --git a/apps/host-selfhost/.env.example b/apps/host-selfhost/.env.example
index 1eb13376a6..3e3296c884 100644
--- a/apps/host-selfhost/.env.example
+++ b/apps/host-selfhost/.env.example
@@ -36,6 +36,12 @@
# default — adversarial generated code should not reach your internal network.
# EXECUTOR_ALLOW_LOCAL_NETWORK=false
+# --- Auth rate limiting -------------------------------------------------------
+# Sign-in attempts are rate-limited per client IP. Without a trusted proxy
+# header every caller shares one bucket. Set the exact string "true" only when
+# something in front of Executor rate-limits instead.
+# EXECUTOR_DISABLE_AUTH_RATE_LIMIT=false
+
# --- Local stdio MCP (trusted deployments only) -------------------------------
# Stdio MCP is disabled unless this is explicitly set to the exact string
# "true". Enabling it lets users configure MCP servers whose commands execute
diff --git a/apps/host-selfhost/src/auth/better-auth.ts b/apps/host-selfhost/src/auth/better-auth.ts
index 1d714312fd..4031fdf273 100644
--- a/apps/host-selfhost/src/auth/better-auth.ts
+++ b/apps/host-selfhost/src/auth/better-auth.ts
@@ -130,6 +130,10 @@ const makeAuthOptions = (client: Client, getOrganizationId: () => string, gate?:
baseURL: config.webBaseUrl,
trustedOrigins: [...config.trustedOrigins],
advanced: { useSecureCookies: !hasInsecureTrustedOrigin },
+ // Better Auth's own limiter is on in production and off in development.
+ // Only an explicit opt-out is passed through, so that environment default
+ // stays in charge everywhere else.
+ ...(config.authRateLimit ? {} : { rateLimit: { enabled: false } }),
emailAndPassword: { enabled: true },
// `apiKey` issues long-lived personal keys (the API-keys page). With
// `enableSessionForAPIKeys`, presenting a key resolves to its owner's
diff --git a/apps/host-selfhost/src/config.ts b/apps/host-selfhost/src/config.ts
index ab443f3bf3..07dcc56d14 100644
--- a/apps/host-selfhost/src/config.ts
+++ b/apps/host-selfhost/src/config.ts
@@ -54,6 +54,15 @@ export interface SelfHostConfig {
* internal network unless an operator opts in.
*/
readonly allowLocalNetwork: boolean;
+ /**
+ * Whether Better Auth rate-limits its own endpoints (sign-in and friends).
+ * Better Auth turns this on in production and keys the limit on the client
+ * IP it reads from a trusted proxy header. With no such header every caller
+ * shares one bucket, so an operator who rate-limits upstream, or an
+ * automated suite that signs in far faster than a person, turns it off with
+ * `EXECUTOR_DISABLE_AUTH_RATE_LIMIT=true`.
+ */
+ readonly authRateLimit: boolean;
// Better Auth session secret. Always resolved (env, else generated + persisted
// under the data dir) so a single-container deploy boots with no env; the auth
// layer still validates an explicitly-set env secret is long enough.
@@ -187,6 +196,7 @@ export const loadConfig = (): SelfHostConfig => {
webBaseUrl,
trustedOrigins: resolveTrustedOrigins(webBaseUrl),
allowLocalNetwork: process.env.EXECUTOR_ALLOW_LOCAL_NETWORK === "true",
+ authRateLimit: process.env.EXECUTOR_DISABLE_AUTH_RATE_LIMIT !== "true",
authSecret: resolveAuthSecret(),
bootstrapAdminEmail: process.env.EXECUTOR_BOOTSTRAP_ADMIN_EMAIL,
bootstrapAdminPassword: process.env.EXECUTOR_BOOTSTRAP_ADMIN_PASSWORD,
diff --git a/apps/host-selfhost/src/executor-config.test.ts b/apps/host-selfhost/src/executor-config.test.ts
index 313d097b85..576b93820c 100644
--- a/apps/host-selfhost/src/executor-config.test.ts
+++ b/apps/host-selfhost/src/executor-config.test.ts
@@ -9,6 +9,8 @@ const TTL_ENV_NAME = "EXECUTOR_TOOLS_SYNC_TTL_MS";
const originalValue = process.env[ENV_NAME];
const originalSecret = process.env[SECRET_ENV_NAME];
const originalTtl = process.env[TTL_ENV_NAME];
+const RATE_LIMIT_ENV_NAME = "EXECUTOR_DISABLE_AUTH_RATE_LIMIT";
+const originalRateLimit = process.env[RATE_LIMIT_ENV_NAME];
beforeEach(() => {
process.env[SECRET_ENV_NAME] = originalSecret ?? "executor-config-test-secret";
@@ -30,6 +32,11 @@ afterEach(() => {
} else {
process.env[TTL_ENV_NAME] = originalTtl;
}
+ if (originalRateLimit === undefined) {
+ delete process.env[RATE_LIMIT_ENV_NAME];
+ } else {
+ process.env[RATE_LIMIT_ENV_NAME] = originalRateLimit;
+ }
});
const allowStdio = (): boolean => {
@@ -112,3 +119,15 @@ test("a negative tools-sync TTL refuses to boot", () => {
process.env[TTL_ENV_NAME] = "-1";
expect(() => loadConfig()).toThrow(/must not be negative/);
});
+
+test("auth rate limiting stays on unless the opt-out is exactly true", () => {
+ delete process.env[RATE_LIMIT_ENV_NAME];
+ expect(loadConfig().authRateLimit).toBe(true);
+ process.env[RATE_LIMIT_ENV_NAME] = "TRUE";
+ expect(loadConfig().authRateLimit).toBe(true);
+});
+
+test("auth rate limiting is off when the opt-out is exactly true", () => {
+ process.env[RATE_LIMIT_ENV_NAME] = "true";
+ expect(loadConfig().authRateLimit).toBe(false);
+});
diff --git a/e2e/setup/selfhost-docker.boot.ts b/e2e/setup/selfhost-docker.boot.ts
index 67f49d0fbf..d8bcc27f92 100644
--- a/e2e/setup/selfhost-docker.boot.ts
+++ b/e2e/setup/selfhost-docker.boot.ts
@@ -108,6 +108,11 @@ export const runSelfhostContainer = async (options: RunContainerOptions): Promis
// test servers and points the instance at them.
"-e",
"EXECUTOR_ALLOW_LOCAL_NETWORK=true",
+ // The production image runs Better Auth's rate limiter. It sees no proxy
+ // header here, so it pools every caller into one bucket of three sign-ins
+ // per ten seconds, and this suite signs in from 100+ files at once.
+ "-e",
+ "EXECUTOR_DISABLE_AUTH_RATE_LIMIT=true",
options.image,
];
log(options.logFile, `docker ${args.join(" ")}`);
From dc0808d5ca9716d73e4def9365f49d4c1afd4af9 Mon Sep 17 00:00:00 2001
From: Rhys Sullivan <39114868+RhysSullivan@users.noreply.github.com>
Date: Fri, 18 Sep 2026 10:10:42 -0700
Subject: [PATCH 02/11] Fix macOS auto-update: preserve framework symlinks in
the update zip (#2049)
---
.changeset/mac-updater-zip-symlinks.md | 11 ++
.github/workflows/publish-desktop.yml | 31 +++++
apps/desktop/package.json | 2 +-
apps/desktop/src/main/index.ts | 37 ++---
apps/desktop/src/main/updater-state.test.ts | 9 ++
apps/desktop/src/main/updater-state.ts | 11 +-
bun.lock | 144 ++++----------------
7 files changed, 105 insertions(+), 140 deletions(-)
create mode 100644 .changeset/mac-updater-zip-symlinks.md
diff --git a/.changeset/mac-updater-zip-symlinks.md b/.changeset/mac-updater-zip-symlinks.md
new file mode 100644
index 0000000000..8482912362
--- /dev/null
+++ b/.changeset/mac-updater-zip-symlinks.md
@@ -0,0 +1,11 @@
+---
+"@executor-js/desktop": patch
+---
+
+Fix macOS auto-update. The 1.6.9 update zip was built with a 7-Zip that
+expanded the framework symlinks into copies, so the extracted app failed code
+signing and Squirrel.Mac silently refused to install it; "Restart to update"
+appeared to do nothing. electron-builder is bumped to a release that preserves
+symlinks, the publish job now verifies the zip's signature before uploading,
+and a rejected install surfaces as "Update failed" instead of leaving the card
+untouched.
diff --git a/.github/workflows/publish-desktop.yml b/.github/workflows/publish-desktop.yml
index e42832181c..4c9a998383 100644
--- a/.github/workflows/publish-desktop.yml
+++ b/.github/workflows/publish-desktop.yml
@@ -206,6 +206,37 @@ jobs:
run: bunx --bun electron-builder --${{ matrix.platform }} --${{ matrix.arch }} --publish never --config electron-builder.config.ts
working-directory: apps/desktop
+ # electron-updater installs from the zip, not the DMG, and Squirrel.Mac
+ # rejects it unless the extracted app passes codesign. 1.6.9 shipped a
+ # zip whose framework symlinks (Versions/Current -> A) had been expanded
+ # into copies by a 7-Zip upgrade inside electron-builder; the DMG was
+ # fine, every auto-update silently failed. Extract the zip the way
+ # Squirrel does and verify it before anything is uploaded.
+ - name: Verify mac update zip
+ if: matrix.platform == 'mac'
+ shell: bash
+ env:
+ CSC_LINK: ${{ secrets.CSC_LINK }}
+ run: |
+ set -euo pipefail
+ zip="apps/desktop/dist/executor-desktop-mac-${{ matrix.arch }}.zip"
+ links=$(unzip -Z "$zip" | grep -c '^l' || true)
+ echo "symlink entries in $zip: $links"
+ if [ "$links" -eq 0 ]; then
+ echo "::error::$zip has no symlink entries; framework bundles were flattened and Squirrel.Mac will reject the update"
+ exit 1
+ fi
+ # Unsigned builds (forks, no CSC_LINK) cannot pass codesign; the
+ # symlink check above still catches the flattening on its own.
+ if [ -z "${CSC_LINK:-}" ]; then
+ echo "no signing certificate configured; skipping codesign verification"
+ exit 0
+ fi
+ tmp=$(mktemp -d)
+ ditto -x -k "$zip" "$tmp"
+ codesign --verify --deep --strict --verbose=1 "$tmp/Executor.app"
+ rm -rf "$tmp"
+
# The two mac legs each emit a latest-mac.yml listing only their own
# arch. Rename per-arch here; the release job merges them back into the
# single latest-mac.yml electron-updater clients fetch. Without this,
diff --git a/apps/desktop/package.json b/apps/desktop/package.json
index 47faabf240..771ea9103f 100644
--- a/apps/desktop/package.json
+++ b/apps/desktop/package.json
@@ -48,7 +48,7 @@
"@zip.js/zip.js": "^2.8.26",
"bun-types": "catalog:",
"electron": "41.10.3",
- "electron-builder": "^26",
+ "electron-builder": "26.16.1",
"electron-vite": "^5",
"quickjs-emscripten": "catalog:",
"typescript": "catalog:",
diff --git a/apps/desktop/src/main/index.ts b/apps/desktop/src/main/index.ts
index 98f9ec7b88..0168684676 100644
--- a/apps/desktop/src/main/index.ts
+++ b/apps/desktop/src/main/index.ts
@@ -4,6 +4,7 @@ import { join } from "node:path";
import { fileURLToPath } from "node:url";
import {
app,
+ autoUpdater as nativeAutoUpdater,
BrowserWindow,
dialog,
ipcMain,
@@ -806,17 +807,11 @@ const registerIpcHandlers = () => {
// Outside a packaged build there is no real bundle to swap, and quitting
// would tear down the e2e harness — reflect "installing" so the renderer
// can prove the wiring instead.
- if (!app.isPackaged) {
- setUpdateStatus({ state: "installing", version });
- return;
- }
- // Stop the sidecar cleanly before Squirrel.Mac swaps the bundle, matching
- // the native dialog's restart path.
- stopSupervisedMonitor();
- if (connection) {
- await stopConnection(connection);
- connection = null;
- }
+ setUpdateStatus({ state: "installing", version });
+ if (!app.isPackaged) return;
+ // Squirrel.Mac only validates the staged bundle now; the sidecar is torn
+ // down in 'before-quit-for-update' once it has accepted the update, so a
+ // rejected zip leaves the app usable and surfaces via the 'error' handler.
autoUpdater.quitAndInstall(false, true);
});
// Crash-screen last resort for damaged state: confirm, move the data dir
@@ -937,13 +932,7 @@ const promptInstallUpdate = async (version: string) => {
cancelId: 1,
});
if (response.response === 0) {
- // Stop the sidecar cleanly before Squirrel.Mac swaps the bundle. A
- // supervised daemon is left running — it's independent of this bundle.
- stopSupervisedMonitor();
- if (connection) {
- await stopConnection(connection);
- connection = null;
- }
+ setUpdateStatus({ state: "installing", version });
autoUpdater.quitAndInstall(false, true);
return;
}
@@ -963,6 +952,18 @@ const setupAutoUpdater = () => {
autoUpdater.logger = log;
autoUpdater.autoDownload = true;
autoUpdater.autoInstallOnAppQuit = false;
+ // Fired by Electron's native updater once Squirrel.Mac has downloaded and
+ // validated the bundle and is about to quit for the swap. Stop a spawned
+ // sidecar here rather than before quitAndInstall: if Squirrel rejects the
+ // update (bad signature, corrupt zip) nothing has been torn down. A
+ // supervised daemon is left running — it's independent of this bundle.
+ nativeAutoUpdater.on("before-quit-for-update", () => {
+ stopSupervisedMonitor();
+ if (connection) {
+ void stopConnection(connection);
+ connection = null;
+ }
+ });
autoUpdater.on("update-available", (info: UpdateInfo) => {
pendingUpdateVersion = info.version;
diff --git a/apps/desktop/src/main/updater-state.test.ts b/apps/desktop/src/main/updater-state.test.ts
index 9809f759cb..388413966d 100644
--- a/apps/desktop/src/main/updater-state.test.ts
+++ b/apps/desktop/src/main/updater-state.test.ts
@@ -79,6 +79,15 @@ describe("updater state decisions", () => {
});
});
+ it("moves a staged or installing update to error when Squirrel rejects it", () => {
+ expect(
+ statusAfterUpdateError({ state: "downloaded", version: "1.6.9" }, "Update failed"),
+ ).toEqual({ state: "error", version: "1.6.9", message: "Update failed" });
+ expect(
+ statusAfterUpdateError({ state: "installing", version: "1.6.9" }, "Update failed"),
+ ).toEqual({ state: "error", version: "1.6.9", message: "Update failed" });
+ });
+
it("restores autoInstallOnAppQuit only when the fatal path recovers", () => {
expect(
planFatalAutoInstallOnQuit({
diff --git a/apps/desktop/src/main/updater-state.ts b/apps/desktop/src/main/updater-state.ts
index 1739337006..9f3c209a0c 100644
--- a/apps/desktop/src/main/updater-state.ts
+++ b/apps/desktop/src/main/updater-state.ts
@@ -89,14 +89,17 @@ export const planDownloadedUpdate = (input: DownloadedUpdateInput): DownloadedUp
};
};
+// Any state that names a version is an update in flight, including a staged
+// ("downloaded") or installing one: Squirrel.Mac validates the bundle only when
+// the install starts, so a rejected zip surfaces as an error *after* the card
+// already offered "Restart to update". Dropping that error left the card
+// unchanged and the click looked like a no-op.
export const statusAfterUpdateError = (
status: DesktopUpdateStatus,
message: string,
): DesktopUpdateStatus => {
- if (status.state === "available" || status.state === "downloading" || status.state === "error") {
- return { state: "error", version: status.version, message };
- }
- return status;
+ if (status.state === "idle") return status;
+ return { state: "error", version: status.version, message };
};
export const planFatalAutoInstallOnQuit = (input: {
diff --git a/bun.lock b/bun.lock
index 02fa988dae..e00404fe07 100644
--- a/bun.lock
+++ b/bun.lock
@@ -161,7 +161,7 @@
"@zip.js/zip.js": "^2.8.26",
"bun-types": "catalog:",
"electron": "41.10.3",
- "electron-builder": "^26",
+ "electron-builder": "26.16.1",
"electron-vite": "^5",
"quickjs-emscripten": "catalog:",
"typescript": "catalog:",
@@ -1687,7 +1687,7 @@
"@electron/osx-sign": ["@electron/osx-sign@1.3.3", "", { "dependencies": { "compare-version": "^0.1.2", "debug": "^4.3.4", "fs-extra": "^10.0.0", "isbinaryfile": "^4.0.8", "minimist": "^1.2.6", "plist": "^3.0.5" }, "bin": { "electron-osx-flat": "bin/electron-osx-flat.js", "electron-osx-sign": "bin/electron-osx-sign.js" } }, "sha512-KZ8mhXvWv2rIEgMbWZ4y33bDHyUKMXnx4M0sTyPNK/vcB81ImdeY9Ggdqy0SWbMDgmbqyQ+phgejh6V3R2QuSg=="],
- "@electron/rebuild": ["@electron/rebuild@4.0.3", "", { "dependencies": { "@malept/cross-spawn-promise": "^2.0.0", "debug": "^4.1.1", "detect-libc": "^2.0.1", "got": "^11.7.0", "graceful-fs": "^4.2.11", "node-abi": "^4.2.0", "node-api-version": "^0.2.1", "node-gyp": "^11.2.0", "ora": "^5.1.0", "read-binary-file-arch": "^1.0.6", "semver": "^7.3.5", "tar": "^7.5.6", "yargs": "^17.0.1" }, "bin": { "electron-rebuild": "lib/cli.js" } }, "sha512-u9vpTHRMkOYCs/1FLiSVAFZ7FbjsXK+bQuzviJZa+lG7BHZl1nz52/IcGvwa3sk80/fc3llutBkbCq10Vh8WQA=="],
+ "@electron/rebuild": ["@electron/rebuild@4.0.4", "", { "dependencies": { "@malept/cross-spawn-promise": "^2.0.0", "debug": "^4.1.1", "node-abi": "^4.2.0", "node-api-version": "^0.2.1", "node-gyp": "^12.2.0", "read-binary-file-arch": "^1.0.6" }, "bin": { "electron-rebuild": "lib/cli.js" } }, "sha512-Rzc39XPdk/+/wBG8MfwAHohXflep0ITUfulb6Rgz3R0NeSB1noE+E9/M/cb8ftCAiyDD9PPhLuuWgE1GaInbKg=="],
"@electron/universal": ["@electron/universal@2.0.3", "", { "dependencies": { "@electron/asar": "^3.3.1", "@malept/cross-spawn-promise": "^2.0.0", "debug": "^4.3.1", "dir-compare": "^4.2.0", "fs-extra": "^11.1.1", "minimatch": "^9.0.3", "plist": "^3.1.0" } }, "sha512-Wn9sPYIVFRFl5HmwMJkARCCf7rqK/EurkfQ/rJZ14mHP3iYTjZSIOSVonEAnhWeAXwtw7zOekGRlc6yTtZ0t+g=="],
@@ -2229,10 +2229,6 @@
"@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="],
- "@npmcli/agent": ["@npmcli/agent@3.0.0", "", { "dependencies": { "agent-base": "^7.1.0", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.1", "lru-cache": "^10.0.1", "socks-proxy-agent": "^8.0.3" } }, "sha512-S79NdEgDQd/NGCay6TCoVzXSj74skRZIKJcpJjC5lOq34SZzyI6MqtiiWoiVWoVrTcGjNeC4ipbh1VIHlpfF5Q=="],
-
- "@npmcli/fs": ["@npmcli/fs@4.0.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-/xGlezI6xfGO9NwuJlnwz/K14qD1kCSAGtacBHnGzeAIuJGazcp45KP5NuyARXoKb7cwulAGWVsbeSxdG/cb0Q=="],
-
"@octokit/auth-token": ["@octokit/auth-token@6.0.0", "", {}, "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w=="],
"@octokit/core": ["@octokit/core@7.0.6", "", { "dependencies": { "@octokit/auth-token": "^6.0.0", "@octokit/graphql": "^9.0.3", "@octokit/request": "^10.0.6", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "before-after-hook": "^4.0.0", "universal-user-agent": "^7.0.0" } }, "sha512-DhGl4xMVFGVIyMwswXeyzdL4uXD5OGILGX5N8Y+f6W7LhC1Ze2poSNrkF/fedpVDHEEZ+PHFW0vL14I+mm8K3Q=="],
@@ -3309,7 +3305,7 @@
"@zip.js/zip.js": ["@zip.js/zip.js@2.8.26", "", {}, "sha512-RQ4h9F6DOiHxpdocUDrOl6xBM+yOtz+LkUol47AVWcfebGBDpZ7w7Xvz9PS24JgXvLGiXXzSAfdCdVy1tPlaFA=="],
- "abbrev": ["abbrev@3.0.1", "", {}, "sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg=="],
+ "abbrev": ["abbrev@4.0.0", "", {}, "sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA=="],
"abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="],
@@ -3357,7 +3353,7 @@
"app-builder-bin": ["app-builder-bin@5.0.0-alpha.12", "", {}, "sha512-j87o0j6LqPL3QRr8yid6c+Tt5gC7xNfYo6uQIQkorAC6MpeayVMZrEDzKmJJ/Hlv7EnOQpaRm53k6ktDYZyB6w=="],
- "app-builder-lib": ["app-builder-lib@26.15.0", "", { "dependencies": { "@electron/asar": "3.4.1", "@electron/fuses": "^1.8.0", "@electron/get": "^3.0.0", "@electron/notarize": "2.5.0", "@electron/osx-sign": "1.3.3", "@electron/rebuild": "4.0.3", "@electron/universal": "2.0.3", "@malept/flatpak-bundler": "^0.4.0", "@noble/hashes": "^2.2.0", "@peculiar/webcrypto": "^1.7.1", "@types/fs-extra": "9.0.13", "ajv": "^8.18.0", "asn1js": "^3.0.10", "async-exit-hook": "^2.0.1", "builder-util": "26.15.0", "builder-util-runtime": "9.7.0", "chromium-pickle-js": "^0.2.0", "ci-info": "4.3.1", "debug": "^4.3.4", "dotenv": "^16.4.5", "dotenv-expand": "^11.0.6", "ejs": "^3.1.8", "electron-publish": "26.15.0", "fs-extra": "^10.1.0", "hosted-git-info": "^4.1.0", "isbinaryfile": "^5.0.0", "jiti": "^2.4.2", "js-yaml": "^4.1.0", "json5": "^2.2.3", "lazy-val": "^1.0.5", "minimatch": "^10.2.5", "pkijs": "^3.4.0", "plist": "3.1.0", "proper-lockfile": "^4.1.2", "resedit": "^1.7.0", "semver": "~7.7.3", "tar": "^7.5.7", "temp-file": "^3.4.0", "tiny-async-pool": "1.3.0", "unzipper": "^0.12.3", "which": "^5.0.0" }, "peerDependencies": { "dmg-builder": "26.15.0", "electron-builder-squirrel-windows": "26.15.0" } }, "sha512-j2+P6Lh+l/VuWfXZWSs7u+OAPqYJQGnZZO30M833XQQaRuyohm4RZk7Gw4nQXfeyQH9GqXaTwR16Y0LaVTlS+g=="],
+ "app-builder-lib": ["app-builder-lib@26.16.1", "", { "dependencies": { "@electron/asar": "3.4.1", "@electron/fuses": "^1.8.0", "@electron/get": "^3.0.0", "@electron/notarize": "2.5.0", "@electron/osx-sign": "1.3.3", "@electron/rebuild": "^4.0.4", "@electron/universal": "2.0.3", "@malept/flatpak-bundler": "^0.4.0", "@noble/hashes": "^1.8.0", "@peculiar/webcrypto": "^1.7.1", "@types/fs-extra": "9.0.13", "ajv": "^8.18.0", "asn1js": "^3.0.10", "async-exit-hook": "^2.0.1", "builder-util": "26.16.0", "builder-util-runtime": "9.7.0", "chromium-pickle-js": "^0.2.0", "ci-info": "4.3.1", "debug": "^4.3.4", "dotenv": "^16.4.5", "dotenv-expand": "^11.0.6", "ejs": "^3.1.8", "electron-publish": "26.16.0", "fs-extra": "^10.1.0", "hosted-git-info": "^4.1.0", "isbinaryfile": "^5.0.0", "jiti": "^2.4.2", "js-yaml": "^4.1.0", "json5": "^2.2.3", "lazy-val": "^1.0.5", "minimatch": "^10.2.5", "pkijs": "^3.4.0", "plist": "3.1.0", "proper-lockfile": "^4.1.2", "resedit": "^1.7.0", "semver": "~7.7.3", "tar": "^7.5.7", "temp-file": "^3.4.0", "tiny-async-pool": "1.3.0", "unzipper": "^0.12.3", "which": "^5.0.0" }, "peerDependencies": { "dmg-builder": "26.16.1", "electron-builder-squirrel-windows": "26.16.1" } }, "sha512-FhaO6YOup01ZfQW0Z6gt3AyukJjv1gW4uFK47jTgwcHZKqyN/fSlK2LqPf9tAeZYLP2bRJLDzeOkRImsw2X4Pg=="],
"app-root-path": ["app-root-path@3.1.0", "", {}, "sha512-biN3PwB2gUtjaYy/isrU3aNWI5w+fAfvHkSvCKeQGxhmYpwKFUxudR3Yya+KqVRHBmEDYh+/lTozYCFbmzX4nA=="],
@@ -3485,7 +3481,7 @@
"buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
- "builder-util": ["builder-util@26.15.0", "", { "dependencies": { "@types/debug": "^4.1.6", "builder-util-runtime": "9.7.0", "chalk": "^4.1.2", "cross-spawn": "^7.0.6", "debug": "^4.3.4", "fs-extra": "^10.1.0", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.0", "js-yaml": "^4.1.0", "sanitize-filename": "^1.6.3", "source-map-support": "^0.5.19", "stat-mode": "^1.0.0", "temp-file": "^3.4.0", "tiny-async-pool": "1.3.0" } }, "sha512-dUx+HxVbiNsNQ4mGe1PyoC/tBmsHwBNDLdBuqWCj+rhHFE9lHgrXiGYKAM1uNlznhAaUSyMlms84VeSSr3gOBA=="],
+ "builder-util": ["builder-util@26.16.0", "", { "dependencies": { "@types/debug": "^4.1.6", "builder-util-runtime": "9.7.0", "chalk": "^4.1.2", "cross-spawn": "^7.0.6", "debug": "^4.3.4", "fs-extra": "^10.1.0", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.0", "js-yaml": "^4.1.0", "sanitize-filename": "^1.6.3", "source-map-support": "^0.5.19", "stat-mode": "^1.0.0", "temp-file": "^3.4.0", "tiny-async-pool": "1.3.0" } }, "sha512-RLyJhB7Si3YkzKR9ubQslWuXW3Vhs3CGe1i+SeixBZ0qTd1mk3XBmssvY22TlB6CS5blyko8Gu1JzpYk8UkYAg=="],
"builder-util-runtime": ["builder-util-runtime@9.7.0", "", { "dependencies": { "debug": "^4.3.4", "sax": "^1.2.4" } }, "sha512-g/kR520giAFYkSXTzcmF3kqQq7wi8F6N6SzeDgZrqTBN+VHdmgWOyTdD1yD7AATDId/yXLvuP34CxW46/BwCdw=="],
@@ -3513,8 +3509,6 @@
"cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="],
- "cacache": ["cacache@19.0.1", "", { "dependencies": { "@npmcli/fs": "^4.0.0", "fs-minipass": "^3.0.0", "glob": "^10.2.2", "lru-cache": "^10.0.1", "minipass": "^7.0.3", "minipass-collect": "^2.0.1", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "p-map": "^7.0.2", "ssri": "^12.0.0", "tar": "^7.4.3", "unique-filename": "^4.0.0" } }, "sha512-hdsUxulXCi5STId78vRVYEtDAjq99ICAUktLTeTYsLoTE6Z8dS0c8pWNCxwdrk9YfJeobDZc2Y186hD/5ZQgFQ=="],
-
"cacheable-lookup": ["cacheable-lookup@5.0.4", "", {}, "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA=="],
"cacheable-request": ["cacheable-request@7.0.4", "", { "dependencies": { "clone-response": "^1.0.2", "get-stream": "^5.1.0", "http-cache-semantics": "^4.0.0", "keyv": "^4.0.0", "lowercase-keys": "^2.0.0", "normalize-url": "^6.0.1", "responselike": "^2.0.0" } }, "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg=="],
@@ -3787,8 +3781,6 @@
"default-browser-id": ["default-browser-id@5.0.1", "", {}, "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q=="],
- "defaults": ["defaults@1.0.4", "", { "dependencies": { "clone": "^1.0.2" } }, "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A=="],
-
"defer-to-connect": ["defer-to-connect@2.0.1", "", {}, "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg=="],
"define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="],
@@ -3831,7 +3823,7 @@
"dir-glob": ["dir-glob@3.0.1", "", { "dependencies": { "path-type": "^4.0.0" } }, "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA=="],
- "dmg-builder": ["dmg-builder@26.15.0", "", { "dependencies": { "app-builder-lib": "26.15.0", "builder-util": "26.15.0", "fs-extra": "^10.1.0", "js-yaml": "^4.1.0" } }, "sha512-oS8MWttbpIUF/2v8LOEY+f4ayL84ipMOarZvdRMl/pxlhLxAYjYMklTXHEXIl37Ig+qJv/bVF7HgyIoOoZyMWA=="],
+ "dmg-builder": ["dmg-builder@26.16.1", "", { "dependencies": { "app-builder-lib": "26.16.1", "builder-util": "26.16.0", "fs-extra": "^10.1.0", "js-yaml": "^4.1.0" } }, "sha512-pnI/3Qb24Uk+rMTgIUrsVUKosVgwmBUdF8Zeb8TexOSbpq8MWc7v6l+n+FrEqVkjNZwzBN+XpDS9ENgZ/rkWAw=="],
"dmg-license": ["dmg-license@1.0.11", "", { "dependencies": { "@types/plist": "^3.0.1", "@types/verror": "^1.10.3", "ajv": "^6.10.0", "crc": "^3.8.0", "iconv-corefoundation": "^1.1.7", "plist": "^3.0.4", "smart-buffer": "^4.0.2", "verror": "^1.10.0" }, "os": "darwin", "bin": { "dmg-license": "bin/dmg-license.js" } }, "sha512-ZdzmqwKmECOWJpqefloC5OJy1+WZBBse5+MR88z9g9Zn4VY+WYUkAyojmhzJckH5YbbZGcYIuGAkY5/Ys5OM2Q=="],
@@ -3875,13 +3867,13 @@
"electron": ["electron@41.10.3", "", { "dependencies": { "@electron-internal/extract-zip": "^1.0.1", "@electron/get": "^5.0.0", "@types/node": "^24.9.0" }, "bin": { "electron": "cli.js" } }, "sha512-MJuSODPw8siv/I8JjhctW/cS/XNldwI4gLRyyWZx6QkoZJUDgbEvitp7IVOnGrHENTQb6Udo+zMpKhFnhlIhdg=="],
- "electron-builder": ["electron-builder@26.15.0", "", { "dependencies": { "app-builder-lib": "26.15.0", "builder-util": "26.15.0", "builder-util-runtime": "9.7.0", "chalk": "^4.1.2", "ci-info": "^4.2.0", "dmg-builder": "26.15.0", "fs-extra": "^10.1.0", "lazy-val": "^1.0.5", "simple-update-notifier": "2.0.0", "yargs": "^17.6.2" }, "bin": { "electron-builder": "./cli.js", "install-app-deps": "./install-app-deps.js" } }, "sha512-zd4cfvjHmtyGqMaDudg5rAjNUkwIJDz8ICaCsz77hFKcjMQHcZNNNCs/C4phwN9+gEVwmhvpKMzNFum6fs/n6A=="],
+ "electron-builder": ["electron-builder@26.16.1", "", { "dependencies": { "app-builder-lib": "26.16.1", "builder-util": "26.16.0", "builder-util-runtime": "9.7.0", "chalk": "^4.1.2", "ci-info": "^4.2.0", "dmg-builder": "26.16.1", "fs-extra": "^10.1.0", "lazy-val": "^1.0.5", "simple-update-notifier": "2.0.0", "yargs": "^17.6.2" }, "bin": { "electron-builder": "./cli.js", "install-app-deps": "./install-app-deps.js" } }, "sha512-LrLK65QX5PUYYODXqp23FKrV7CILTtVY7mrJckNknO9jLNSMiqFkKbSMiDRw4CjOADMPVDdWLxY4mezOZWswxg=="],
"electron-builder-squirrel-windows": ["electron-builder-squirrel-windows@26.8.1", "", { "dependencies": { "app-builder-lib": "26.8.1", "builder-util": "26.8.1", "electron-winstaller": "5.4.0" } }, "sha512-o288fIdgPLHA76eDrFADHPoo7VyGkDCYbLV1GzndaMSAVBoZrGvM9m2IehdcVMzdAZJ2eV9bgyissQXHv5tGzA=="],
"electron-log": ["electron-log@5.4.3", "", {}, "sha512-sOUsM3LjZdugatazSQ/XTyNcw8dfvH1SYhXWiJyfYodAAKOZdHs0txPiLDXFzOZbhXgAgshQkshH2ccq0feyLQ=="],
- "electron-publish": ["electron-publish@26.15.0", "", { "dependencies": { "@types/fs-extra": "^9.0.11", "aws4": "^1.13.2", "builder-util": "26.15.0", "builder-util-runtime": "9.7.0", "chalk": "^4.1.2", "form-data": "^4.0.5", "fs-extra": "^10.1.0", "lazy-val": "^1.0.5", "mime": "^2.5.2" } }, "sha512-pt6K3ol/a+o3HbqmYkL2NYlVH5pd34tL4FPRcgX8E88xQAqQyIsseXe4vWy7Pq2BaYy+iFGJrtInZe11FFAQwQ=="],
+ "electron-publish": ["electron-publish@26.16.0", "", { "dependencies": { "@types/fs-extra": "^9.0.11", "aws4": "^1.13.2", "builder-util": "26.16.0", "builder-util-runtime": "9.7.0", "chalk": "^4.1.2", "form-data": "^4.0.5", "fs-extra": "^10.1.0", "lazy-val": "^1.0.5", "mime": "^2.5.2" } }, "sha512-Vt3KzQIiw9BImvNOYtndg9Mjki+tl4+1sQiC/+G5j8khWaENOJFWodiB+sUl6yyHwtd37avehskdtPw7f8y/+Q=="],
"electron-store": ["electron-store@10.1.0", "", { "dependencies": { "conf": "^14.0.0", "type-fest": "^4.41.0" } }, "sha512-oL8bRy7pVCLpwhmXy05Rh/L6O93+k9t6dqSw0+MckIc3OmCTZm6Mp04Q4f/J0rtu84Ky6ywkR8ivtGOmrq+16w=="],
@@ -4113,8 +4105,6 @@
"fs-extra": ["fs-extra@7.0.1", "", { "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw=="],
- "fs-minipass": ["fs-minipass@3.0.3", "", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw=="],
-
"fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="],
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
@@ -4285,8 +4275,6 @@
"import-in-the-middle": ["import-in-the-middle@3.0.2", "", { "dependencies": { "acorn": "^8.15.0", "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^2.2.0", "module-details-from-path": "^1.0.4" } }, "sha512-LGLYRl0A2gtyUJb2WDliBHmk6TtlHwdDjxonacZ8QrEs/ZW+YDgNv2QAfjRQWpS8HqvNcq6GGnN6jrOa5FysDQ=="],
- "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
-
"indent-string": ["indent-string@5.0.0", "", {}, "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg=="],
"inflight": ["inflight@1.0.6", "", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA=="],
@@ -4593,8 +4581,6 @@
"magicast": ["magicast@0.5.2", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "source-map-js": "^1.2.1" } }, "sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ=="],
- "make-fetch-happen": ["make-fetch-happen@14.0.3", "", { "dependencies": { "@npmcli/agent": "^3.0.0", "cacache": "^19.0.1", "http-cache-semantics": "^4.1.1", "minipass": "^7.0.2", "minipass-fetch": "^4.0.0", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "negotiator": "^1.0.0", "proc-log": "^5.0.0", "promise-retry": "^2.0.1", "ssri": "^12.0.0" } }, "sha512-QMjGbFTP0blj97EeidG5hk/QhKQ3T4ICckQGLgz38QF7Vgbk6e6FTARN8KhKxyBbWn8R0HU+bnw8aSoFPD4qtQ=="],
-
"markdown-extensions": ["markdown-extensions@2.0.0", "", {}, "sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q=="],
"markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="],
@@ -4757,16 +4743,6 @@
"minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="],
- "minipass-collect": ["minipass-collect@2.0.1", "", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw=="],
-
- "minipass-fetch": ["minipass-fetch@4.0.1", "", { "dependencies": { "minipass": "^7.0.3", "minipass-sized": "^1.0.3", "minizlib": "^3.0.1" }, "optionalDependencies": { "encoding": "^0.1.13" } }, "sha512-j7U11C5HXigVuutxebFadoYBbd7VSdZWggSe64NVdvWNBqGAiXPL2QVCehjmw7lY1oF9gOllYbORh+hiNgfPgQ=="],
-
- "minipass-flush": ["minipass-flush@1.0.7", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-TbqTz9cUwWyHS2Dy89P3ocAGUGxKjjLuR9z8w4WUTGAVgEj17/4nhgo2Du56i0Fm3Pm30g4iA8Lcqctc76jCzA=="],
-
- "minipass-pipeline": ["minipass-pipeline@1.2.4", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A=="],
-
- "minipass-sized": ["minipass-sized@1.0.3", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g=="],
-
"minizlib": ["minizlib@3.1.0", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw=="],
"mixin-deep": ["mixin-deep@1.3.2", "", { "dependencies": { "for-in": "^1.0.2", "is-extendable": "^1.0.1" } }, "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA=="],
@@ -4831,7 +4807,7 @@
"node-fetch-native": ["node-fetch-native@1.6.7", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="],
- "node-gyp": ["node-gyp@11.5.0", "", { "dependencies": { "env-paths": "^2.2.0", "exponential-backoff": "^3.1.1", "graceful-fs": "^4.2.6", "make-fetch-happen": "^14.0.3", "nopt": "^8.0.0", "proc-log": "^5.0.0", "semver": "^7.3.5", "tar": "^7.4.3", "tinyglobby": "^0.2.12", "which": "^5.0.0" }, "bin": { "node-gyp": "bin/node-gyp.js" } }, "sha512-ra7Kvlhxn5V9Slyus0ygMa2h+UqExPqUIkfk7Pc8QTLT956JLSy51uWFwHtIYy0vI8cB4BDhc/S03+880My/LQ=="],
+ "node-gyp": ["node-gyp@12.3.0", "", { "dependencies": { "env-paths": "^2.2.0", "exponential-backoff": "^3.1.1", "graceful-fs": "^4.2.6", "nopt": "^9.0.0", "proc-log": "^6.0.0", "semver": "^7.3.5", "tar": "^7.5.4", "tinyglobby": "^0.2.12", "undici": "^6.25.0", "which": "^6.0.0" }, "bin": { "node-gyp": "bin/node-gyp.js" } }, "sha512-QNcUWM+HgJplcPzBvFBZ9VXacyGZ4+VTOb80PwWR+TlVzoHbRKULNEzpRsnaoxG3Wzr7Qh7BYxGDU3CbKib2Yg=="],
"node-gyp-build-optional-packages": ["node-gyp-build-optional-packages@5.2.2", "", { "dependencies": { "detect-libc": "^2.0.1" }, "bin": { "node-gyp-build-optional-packages": "bin.js", "node-gyp-build-optional-packages-optional": "optional.js", "node-gyp-build-optional-packages-test": "build-test.js" } }, "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw=="],
@@ -4841,7 +4817,7 @@
"node-releases": ["node-releases@2.0.55", "", {}, "sha512-mIrE/Cw9y+9Au6dS5vDKDhQza9YvG6w+ZrS6X+ZzA7yFW/soAeaups4Qzn1bL6g5FVy8WtP79+0j82oPIbqRjQ=="],
- "nopt": ["nopt@8.1.0", "", { "dependencies": { "abbrev": "^3.0.0" }, "bin": { "nopt": "bin/nopt.js" } }, "sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A=="],
+ "nopt": ["nopt@9.0.0", "", { "dependencies": { "abbrev": "^4.0.0" }, "bin": { "nopt": "bin/nopt.js" } }, "sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw=="],
"normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="],
@@ -5061,7 +5037,7 @@
"prismjs": ["prismjs@1.30.0", "", {}, "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw=="],
- "proc-log": ["proc-log@5.0.0", "", {}, "sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ=="],
+ "proc-log": ["proc-log@6.1.0", "", {}, "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ=="],
"process": ["process@0.11.10", "", {}, "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A=="],
@@ -5417,10 +5393,6 @@
"smol-toml": ["smol-toml@1.7.1", "", {}, "sha512-PPlsspAZ4jbMBu5DMFhfUGDQLu/vrL4SyBROVS37x8ynnVmFIs1VPBz1Co8Xks3TvpIaZXmU85y4DrQ+UyVFoQ=="],
- "socks": ["socks@2.8.10", "", { "dependencies": { "ip-address": "^10.1.1", "smart-buffer": "^4.2.0" } }, "sha512-e0VyvkVTwVYViNovRkZ9aodhxVlyoMn7eJhVUPxZ+eK9P/7CBkxvvsBOHqFPEH416726W8tLXXXjKwqgTErrCQ=="],
-
- "socks-proxy-agent": ["socks-proxy-agent@8.0.5", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "^4.3.4", "socks": "^2.8.3" } }, "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw=="],
-
"solid-js": ["solid-js@1.9.13", "", { "dependencies": { "csstype": "^3.1.0", "seroval": "~1.5.0", "seroval-plugins": "~1.5.0" } }, "sha512-6hJeJMOcEX8ktqjpDoJZEmld3ijvcvWBDtiXBm7f4332SiFN66QeAQI1REQshvyUoISsSeJ4PHDauKYbwao9JQ=="],
"solid-transition-group": ["solid-transition-group@0.2.3", "", { "dependencies": { "@solid-primitives/refs": "^1.0.5", "@solid-primitives/transition-group": "^1.0.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-iB72c9N5Kz9ykRqIXl0lQohOau4t0dhel9kjwFvx81UZJbVwaChMuBuyhiZmK24b8aKEK0w3uFM96ZxzcyZGdg=="],
@@ -5453,8 +5425,6 @@
"sshpk": ["sshpk@1.18.0", "", { "dependencies": { "asn1": "~0.2.3", "assert-plus": "^1.0.0", "bcrypt-pbkdf": "^1.0.0", "dashdash": "^1.12.0", "ecc-jsbn": "~0.1.1", "getpass": "^0.1.1", "jsbn": "~0.1.0", "safer-buffer": "^2.0.2", "tweetnacl": "~0.14.0" }, "bin": { "sshpk-conv": "bin/sshpk-conv", "sshpk-sign": "bin/sshpk-sign", "sshpk-verify": "bin/sshpk-verify" } }, "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ=="],
- "ssri": ["ssri@12.0.0", "", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-S7iGNosepx9RadX82oimUkvr0Ct7IjJbEbs4mJcTxst8um95J3sDYU1RBEOvdu6oL1Wek2ODI5i4MAw+dZ6cAQ=="],
-
"stack-utils": ["stack-utils@2.0.6", "", { "dependencies": { "escape-string-regexp": "^2.0.0" } }, "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ=="],
"stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="],
@@ -5675,10 +5645,6 @@
"unifont": ["unifont@0.7.5", "", { "dependencies": { "css-tree": "^3.1.0", "ohash": "^2.0.11", "undici": "^8.0.0" } }, "sha512-ULe/Cs+ZIsq+dcFofNkhqielCrUJnb5mr+Yc4EBM2VlL+6OZR6+cjtI2mT1bJvRBrVncqHAbLURxmPLcCXzWMg=="],
- "unique-filename": ["unique-filename@4.0.0", "", { "dependencies": { "unique-slug": "^5.0.0" } }, "sha512-XSnEewXmQ+veP7xX2dS5Q4yZAvO40cBN2MWkJ7D/6sW4Dg6wYBNwM1Vrnz1FhH5AdeLIlUXRI9e28z1YZi71NQ=="],
-
- "unique-slug": ["unique-slug@5.0.0", "", { "dependencies": { "imurmurhash": "^0.1.4" } }, "sha512-9OdaqO5kwqR+1kVgHAhsp5vPNU0hnxRa26rBFNfNgM7M6pNtgzeBn3s/xbyCQL3dcjzOatcef6UUHpB/6MaETg=="],
-
"unique-string": ["unique-string@3.0.0", "", { "dependencies": { "crypto-random-string": "^4.0.0" } }, "sha512-VGXBUVwxKMBUznyffQweQABPRRW1vHZAbadFZud4pLFAqRGvv/96vafgjWFqzourzr8YonlQiPgH0YCJfawoGQ=="],
"unist-util-find-after": ["unist-util-find-after@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ=="],
@@ -5765,8 +5731,6 @@
"walk-up-path": ["walk-up-path@4.0.0", "", {}, "sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A=="],
- "wcwidth": ["wcwidth@1.0.1", "", { "dependencies": { "defaults": "^1.0.3" } }, "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg=="],
-
"web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="],
"web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="],
@@ -5967,11 +5931,7 @@
"@electron/osx-sign/isbinaryfile": ["isbinaryfile@4.0.10", "", {}, "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw=="],
- "@electron/rebuild/node-abi": ["node-abi@4.35.0", "", { "dependencies": { "semver": "^7.6.3" } }, "sha512-ymk4aIzxdPopw2giv8Fs1Ec6vybGkjmyxUwVqhkI4MCy2tVfXdkOGGWieWVjL0THgH+7a8lRdevyupoYj3Js/Q=="],
-
- "@electron/rebuild/ora": ["ora@5.4.1", "", { "dependencies": { "bl": "^4.1.0", "chalk": "^4.1.0", "cli-cursor": "^3.1.0", "cli-spinners": "^2.5.0", "is-interactive": "^1.0.0", "is-unicode-supported": "^0.1.0", "log-symbols": "^4.1.0", "strip-ansi": "^6.0.0", "wcwidth": "^1.0.1" } }, "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ=="],
-
- "@electron/rebuild/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
+ "@electron/rebuild/node-abi": ["node-abi@4.31.0", "", { "dependencies": { "semver": "^7.6.3" } }, "sha512-Erq5w/t3syw3s4sDsUaX4QttIdBPsGKTT1DTRsCkTonGggczhlDKm/wDX3o+HPJpQ41EjXCbcmXf0tgr5YZJXw=="],
"@electron/universal/fs-extra": ["fs-extra@11.3.5", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg=="],
@@ -6119,12 +6079,6 @@
"@modelcontextprotocol/server/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="],
- "@npmcli/agent/agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
-
- "@npmcli/agent/https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
-
- "@npmcli/fs/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
-
"@octokit/request/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="],
"@opentelemetry/configuration/@opentelemetry/core": ["@opentelemetry/core@2.6.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-8xHSGWpJP9wBxgBpnqGL0R3PbdWQndL1Qp50qrg71+B28zK5OQmUgcDKLJgzyAAV38t4tOyLMGDD60LneR5W8g=="],
@@ -6483,6 +6437,8 @@
"app-builder-lib/@electron/get": ["@electron/get@3.1.0", "", { "dependencies": { "debug": "^4.1.1", "env-paths": "^2.2.0", "fs-extra": "^8.1.0", "got": "^11.8.5", "progress": "^2.0.3", "semver": "^6.2.0", "sumchecker": "^3.0.1" }, "optionalDependencies": { "global-agent": "^3.0.0" } }, "sha512-F+nKc0xW+kVbBRhFzaMgPy3KwmuNTYX1fx6+FxxoSnNgwYX6LD7AKBTWkU0MQ6IBoe7dz069CNkR673sPAgkCQ=="],
+ "app-builder-lib/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="],
+
"app-builder-lib/ci-info": ["ci-info@4.3.1", "", {}, "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA=="],
"app-builder-lib/dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="],
@@ -6541,8 +6497,6 @@
"bun-types/@types/node": ["@types/node@25.6.0", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ=="],
- "cacache/p-map": ["p-map@7.0.8", "", {}, "sha512-MitaVsCuCFIvOLLPIU7NnfrZvS9H9h7kwMUkDo+T2pEISaJD48IV9S8iIdXB7PsvvdxyYcsSTTrr90XKsbulNw=="],
-
"cacheable-request/get-stream": ["get-stream@5.2.0", "", { "dependencies": { "pump": "^3.0.0" } }, "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA=="],
"cheerio/undici": ["undici@7.29.0", "", {}, "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw=="],
@@ -6579,8 +6533,6 @@
"d3-sankey/d3-shape": ["d3-shape@1.3.7", "", { "dependencies": { "d3-path": "1" } }, "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw=="],
- "defaults/clone": ["clone@1.0.4", "", {}, "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg=="],
-
"dir-compare/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="],
"dir-compare/p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],
@@ -6601,6 +6553,8 @@
"electron-builder/fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="],
+ "electron-builder/yargs": ["yargs@17.7.3", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g=="],
+
"electron-builder-squirrel-windows/app-builder-lib": ["app-builder-lib@26.8.1", "", { "dependencies": { "@develar/schema-utils": "~2.6.5", "@electron/asar": "3.4.1", "@electron/fuses": "^1.8.0", "@electron/get": "^3.0.0", "@electron/notarize": "2.5.0", "@electron/osx-sign": "1.3.3", "@electron/rebuild": "^4.0.3", "@electron/universal": "2.0.3", "@malept/flatpak-bundler": "^0.4.0", "@types/fs-extra": "9.0.13", "async-exit-hook": "^2.0.1", "builder-util": "26.8.1", "builder-util-runtime": "9.5.1", "chromium-pickle-js": "^0.2.0", "ci-info": "4.3.1", "debug": "^4.3.4", "dotenv": "^16.4.5", "dotenv-expand": "^11.0.6", "ejs": "^3.1.8", "electron-publish": "26.8.1", "fs-extra": "^10.1.0", "hosted-git-info": "^4.1.0", "isbinaryfile": "^5.0.0", "jiti": "^2.4.2", "js-yaml": "^4.1.0", "json5": "^2.2.3", "lazy-val": "^1.0.5", "minimatch": "^10.0.3", "plist": "3.1.0", "proper-lockfile": "^4.1.2", "resedit": "^1.7.0", "semver": "~7.7.3", "tar": "^7.5.7", "temp-file": "^3.4.0", "tiny-async-pool": "1.3.0", "which": "^5.0.0" }, "peerDependencies": { "dmg-builder": "26.8.1", "electron-builder-squirrel-windows": "26.8.1" } }, "sha512-p0Im/Dx5C4tmz8QEE1Yn4MkuPC8PrnlRneMhWJj7BBXQfNTJUshM/bp3lusdEsDbvvfJZpXWnYesgSLvwtM2Zw=="],
"electron-builder-squirrel-windows/builder-util": ["builder-util@26.8.1", "", { "dependencies": { "7zip-bin": "~5.2.0", "@types/debug": "^4.1.6", "app-builder-bin": "5.0.0-alpha.12", "builder-util-runtime": "9.5.1", "chalk": "^4.1.2", "cross-spawn": "^7.0.6", "debug": "^4.3.4", "fs-extra": "^10.1.0", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.0", "js-yaml": "^4.1.0", "sanitize-filename": "^1.6.3", "source-map-support": "^0.5.19", "stat-mode": "^1.0.0", "temp-file": "^3.4.0", "tiny-async-pool": "1.3.0" } }, "sha512-pm1lTYbGyc90DHgCDO7eo8Rl4EqKLciayNbZqGziqnH9jrlKe8ZANGdityLZU+pJh16dfzjAx2xQq9McuIPEtw=="],
@@ -6737,12 +6691,6 @@
"miniflare/ws": ["ws@8.18.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw=="],
- "minipass-flush/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
-
- "minipass-pipeline/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
-
- "minipass-sized/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
-
"monaco-editor/dompurify": ["dompurify@3.2.7", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw=="],
"morgan/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
@@ -6755,7 +6703,9 @@
"node-gyp/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
- "node-gyp/which": ["which@5.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ=="],
+ "node-gyp/undici": ["undici@6.28.0", "", {}, "sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA=="],
+
+ "node-gyp/which": ["which@6.0.1", "", { "dependencies": { "isexe": "^4.0.0" }, "bin": { "node-which": "bin/which.js" } }, "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg=="],
"npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="],
@@ -6843,10 +6793,6 @@
"simple-update-notifier/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
- "socks/ip-address": ["ip-address@10.7.1", "", {}, "sha512-4OUAqU9Z1i3vCnS05hzGiFnEMDpQ+62pAD/MVQOp83fYyNC8GleCqaS0QikQBmcWCrKFiUs/B8ztRRiYOAXuCA=="],
-
- "socks-proxy-agent/agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
-
"solid-js/seroval-plugins": ["seroval-plugins@1.5.2", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-qpY0Cl+fKYFn4GOf3cMiq6l72CpuVaawb6ILjubOQ+diJ54LfOWaSSPsaswN8DRPIPW4Yq+tE1k5aKd7ILyaFg=="],
"source-map-support/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
@@ -7105,15 +7051,7 @@
"@electron/osx-sign/fs-extra/universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="],
- "@electron/rebuild/ora/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
-
- "@electron/rebuild/ora/cli-cursor": ["cli-cursor@3.1.0", "", { "dependencies": { "restore-cursor": "^3.1.0" } }, "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw=="],
-
- "@electron/rebuild/ora/is-interactive": ["is-interactive@1.0.0", "", {}, "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w=="],
-
- "@electron/rebuild/ora/is-unicode-supported": ["is-unicode-supported@0.1.0", "", {}, "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw=="],
-
- "@electron/rebuild/ora/log-symbols": ["log-symbols@4.1.0", "", { "dependencies": { "chalk": "^4.1.0", "is-unicode-supported": "^0.1.0" } }, "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg=="],
+ "@electron/rebuild/node-abi/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
"@electron/universal/fs-extra/jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="],
@@ -7619,8 +7557,6 @@
"electron-builder-squirrel-windows/app-builder-lib/@electron/get": ["@electron/get@3.1.0", "", { "dependencies": { "debug": "^4.1.1", "env-paths": "^2.2.0", "fs-extra": "^8.1.0", "got": "^11.8.5", "progress": "^2.0.3", "semver": "^6.2.0", "sumchecker": "^3.0.1" }, "optionalDependencies": { "global-agent": "^3.0.0" } }, "sha512-F+nKc0xW+kVbBRhFzaMgPy3KwmuNTYX1fx6+FxxoSnNgwYX6LD7AKBTWkU0MQ6IBoe7dz069CNkR673sPAgkCQ=="],
- "electron-builder-squirrel-windows/app-builder-lib/@electron/rebuild": ["@electron/rebuild@4.0.4", "", { "dependencies": { "@malept/cross-spawn-promise": "^2.0.0", "debug": "^4.1.1", "node-abi": "^4.2.0", "node-api-version": "^0.2.1", "node-gyp": "^12.2.0", "read-binary-file-arch": "^1.0.6" }, "bin": { "electron-rebuild": "lib/cli.js" } }, "sha512-Rzc39XPdk/+/wBG8MfwAHohXflep0ITUfulb6Rgz3R0NeSB1noE+E9/M/cb8ftCAiyDD9PPhLuuWgE1GaInbKg=="],
-
"electron-builder-squirrel-windows/app-builder-lib/builder-util-runtime": ["builder-util-runtime@9.5.1", "", { "dependencies": { "debug": "^4.3.4", "sax": "^1.2.4" } }, "sha512-qt41tMfgHTllhResqM5DcnHyDIWNgzHvuY2jDcYP9iaGpkWxTUzV6GQjDeLnlR1/DtdlcsWQbA7sByMpmJFTLQ=="],
"electron-builder-squirrel-windows/app-builder-lib/ci-info": ["ci-info@4.3.1", "", {}, "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA=="],
@@ -7655,6 +7591,10 @@
"electron-builder/fs-extra/universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="],
+ "electron-builder/yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
+
+ "electron-builder/yargs/yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
+
"electron-publish/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
"electron-publish/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
@@ -7809,15 +7749,9 @@
"miniflare/workerd/@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20260424.1", "", { "os": "win32", "cpu": "x64" }, "sha512-tZ7Z9qmYNAP6z1/+8r/zKbk8F8DZmpmwNzMeN+zkde2Wnhfr3FBqOkJXT/5zmli8HPoWrIXxSiyqcNDMy8V2Zg=="],
- "minipass-flush/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
-
- "minipass-pipeline/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
-
- "minipass-sized/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
-
"morgan/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
- "node-gyp/which/isexe": ["isexe@3.1.5", "", {}, "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w=="],
+ "node-gyp/which/isexe": ["isexe@4.0.0", "", {}, "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw=="],
"ora/cli-cursor/restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="],
@@ -7967,12 +7901,6 @@
"@electron/asar/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
- "@electron/rebuild/ora/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
-
- "@electron/rebuild/ora/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
-
- "@electron/rebuild/ora/cli-cursor/restore-cursor": ["restore-cursor@3.1.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA=="],
-
"@electron/universal/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
"@executor-js/motel/@opentelemetry/exporter-logs-otlp-http/@opentelemetry/otlp-transformer/@opentelemetry/resources": ["@opentelemetry/resources@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA=="],
@@ -8031,10 +7959,6 @@
"electron-builder-squirrel-windows/app-builder-lib/@electron/get/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
- "electron-builder-squirrel-windows/app-builder-lib/@electron/rebuild/node-abi": ["node-abi@4.31.0", "", { "dependencies": { "semver": "^7.6.3" } }, "sha512-Erq5w/t3syw3s4sDsUaX4QttIdBPsGKTT1DTRsCkTonGggczhlDKm/wDX3o+HPJpQ41EjXCbcmXf0tgr5YZJXw=="],
-
- "electron-builder-squirrel-windows/app-builder-lib/@electron/rebuild/node-gyp": ["node-gyp@12.3.0", "", { "dependencies": { "env-paths": "^2.2.0", "exponential-backoff": "^3.1.1", "graceful-fs": "^4.2.6", "nopt": "^9.0.0", "proc-log": "^6.0.0", "semver": "^7.3.5", "tar": "^7.5.4", "tinyglobby": "^0.2.12", "undici": "^6.25.0", "which": "^6.0.0" }, "bin": { "node-gyp": "bin/node-gyp.js" } }, "sha512-QNcUWM+HgJplcPzBvFBZ9VXacyGZ4+VTOb80PwWR+TlVzoHbRKULNEzpRsnaoxG3Wzr7Qh7BYxGDU3CbKib2Yg=="],
-
"electron-builder-squirrel-windows/app-builder-lib/dmg-builder/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
"electron-builder-squirrel-windows/app-builder-lib/electron-publish/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
@@ -8057,6 +7981,8 @@
"electron-builder-squirrel-windows/builder-util/https-proxy-agent/agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
+ "electron-builder/yargs/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
+
"filelist/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
"glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
@@ -8085,8 +8011,6 @@
"typeorm/yargs/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
- "@electron/rebuild/ora/cli-cursor/restore-cursor/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
-
"@executor-js/motel/@opentelemetry/exporter-trace-otlp-http/@opentelemetry/otlp-transformer/protobufjs/@protobufjs/codegen": ["@protobufjs/codegen@2.0.4", "", {}, "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg=="],
"@executor-js/motel/@opentelemetry/exporter-trace-otlp-http/@opentelemetry/otlp-transformer/protobufjs/@protobufjs/eventemitter": ["@protobufjs/eventemitter@1.1.0", "", {}, "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q=="],
@@ -8103,16 +8027,6 @@
"agents/yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
- "electron-builder-squirrel-windows/app-builder-lib/@electron/rebuild/node-gyp/env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="],
-
- "electron-builder-squirrel-windows/app-builder-lib/@electron/rebuild/node-gyp/nopt": ["nopt@9.0.0", "", { "dependencies": { "abbrev": "^4.0.0" }, "bin": { "nopt": "bin/nopt.js" } }, "sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw=="],
-
- "electron-builder-squirrel-windows/app-builder-lib/@electron/rebuild/node-gyp/proc-log": ["proc-log@6.1.0", "", {}, "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ=="],
-
- "electron-builder-squirrel-windows/app-builder-lib/@electron/rebuild/node-gyp/undici": ["undici@6.28.0", "", {}, "sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA=="],
-
- "electron-builder-squirrel-windows/app-builder-lib/@electron/rebuild/node-gyp/which": ["which@6.0.1", "", { "dependencies": { "isexe": "^4.0.0" }, "bin": { "node-which": "bin/which.js" } }, "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg=="],
-
"electron-builder-squirrel-windows/app-builder-lib/electron-publish/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
"electron-builder-squirrel-windows/app-builder-lib/electron-publish/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
@@ -8123,10 +8037,6 @@
"@executor-js/motel/@opentelemetry/exporter-trace-otlp-http/@opentelemetry/otlp-transformer/protobufjs/@types/node/undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="],
- "electron-builder-squirrel-windows/app-builder-lib/@electron/rebuild/node-gyp/nopt/abbrev": ["abbrev@4.0.0", "", {}, "sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA=="],
-
- "electron-builder-squirrel-windows/app-builder-lib/@electron/rebuild/node-gyp/which/isexe": ["isexe@4.0.0", "", {}, "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw=="],
-
"temp/rimraf/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
}
}
From 3890d6f5e5efd1530f0dba0fe23ada95a39caf86 Mon Sep 17 00:00:00 2001
From: Rhys Sullivan <39114868+RhysSullivan@users.noreply.github.com>
Date: Fri, 18 Sep 2026 10:12:10 -0700
Subject: [PATCH 03/11] Version Packages (#2048)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
---
.changeset/mac-updater-zip-symlinks.md | 11 ----
.changeset/selfhost-auth-rate-limit.md | 6 --
apps/cli/CHANGELOG.md | 12 ++++
apps/cli/package.json | 2 +-
apps/cloud/CHANGELOG.md | 21 ++++++
apps/cloud/package.json | 2 +-
apps/desktop/CHANGELOG.md | 12 ++++
apps/desktop/package.json | 2 +-
apps/host-selfhost/CHANGELOG.md | 23 +++++++
apps/host-selfhost/package.json | 2 +-
apps/local/CHANGELOG.md | 27 ++++++++
apps/local/package.json | 2 +-
bun.lock | 66 +++++++++----------
e2e/CHANGELOG.md | 12 ++++
e2e/package.json | 2 +-
examples/all-plugins/CHANGELOG.md | 14 ++++
examples/all-plugins/package.json | 2 +-
examples/docs-sdk-quickstart/CHANGELOG.md | 8 +++
examples/docs-sdk-quickstart/package.json | 2 +-
packages/core/analytics/CHANGELOG.md | 7 ++
packages/core/analytics/package.json | 2 +-
packages/core/api/CHANGELOG.md | 9 +++
packages/core/api/package.json | 2 +-
packages/core/cli/CHANGELOG.md | 7 ++
packages/core/cli/package.json | 2 +-
packages/core/config/CHANGELOG.md | 7 ++
packages/core/config/package.json | 2 +-
packages/core/execution/CHANGELOG.md | 8 +++
packages/core/execution/package.json | 2 +-
packages/core/sdk/CHANGELOG.md | 2 +
packages/core/sdk/package.json | 2 +-
packages/core/vite-plugin/CHANGELOG.md | 7 ++
packages/core/vite-plugin/package.json | 2 +-
packages/hosts/cloudflare/CHANGELOG.md | 10 +++
packages/hosts/cloudflare/package.json | 2 +-
packages/hosts/mcp-apps-shell/CHANGELOG.md | 8 +++
packages/hosts/mcp-apps-shell/package.json | 2 +-
packages/kernel/core/CHANGELOG.md | 2 +
packages/kernel/core/package.json | 2 +-
packages/kernel/runtime-quickjs/CHANGELOG.md | 7 ++
packages/kernel/runtime-quickjs/package.json | 2 +-
.../runtime-workerd-subprocess/CHANGELOG.md | 7 ++
.../runtime-workerd-subprocess/package.json | 2 +-
packages/onboarding-demo/CHANGELOG.md | 10 +++
packages/onboarding-demo/package.json | 2 +-
.../plugins/desktop-settings/CHANGELOG.md | 7 ++
.../plugins/desktop-settings/package.json | 2 +-
.../plugins/encrypted-secrets/CHANGELOG.md | 7 ++
.../plugins/encrypted-secrets/package.json | 2 +-
packages/plugins/example/CHANGELOG.md | 7 ++
packages/plugins/example/package.json | 2 +-
packages/plugins/file-secrets/CHANGELOG.md | 7 ++
packages/plugins/file-secrets/package.json | 2 +-
packages/plugins/graphql/CHANGELOG.md | 10 +++
packages/plugins/graphql/package.json | 2 +-
packages/plugins/keychain/CHANGELOG.md | 7 ++
packages/plugins/keychain/package.json | 2 +-
packages/plugins/mcp/CHANGELOG.md | 10 +++
packages/plugins/mcp/package.json | 2 +-
packages/plugins/onepassword/CHANGELOG.md | 9 +++
packages/plugins/onepassword/package.json | 2 +-
packages/plugins/openapi/CHANGELOG.md | 10 +++
packages/plugins/openapi/package.json | 2 +-
.../provider-service-split/CHANGELOG.md | 8 +++
.../provider-service-split/package.json | 2 +-
packages/plugins/toolkits/CHANGELOG.md | 9 +++
packages/plugins/toolkits/package.json | 2 +-
packages/react/CHANGELOG.md | 8 +++
packages/react/package.json | 2 +-
69 files changed, 385 insertions(+), 83 deletions(-)
delete mode 100644 .changeset/mac-updater-zip-symlinks.md
delete mode 100644 .changeset/selfhost-auth-rate-limit.md
diff --git a/.changeset/mac-updater-zip-symlinks.md b/.changeset/mac-updater-zip-symlinks.md
deleted file mode 100644
index 8482912362..0000000000
--- a/.changeset/mac-updater-zip-symlinks.md
+++ /dev/null
@@ -1,11 +0,0 @@
----
-"@executor-js/desktop": patch
----
-
-Fix macOS auto-update. The 1.6.9 update zip was built with a 7-Zip that
-expanded the framework symlinks into copies, so the extracted app failed code
-signing and Squirrel.Mac silently refused to install it; "Restart to update"
-appeared to do nothing. electron-builder is bumped to a release that preserves
-symlinks, the publish job now verifies the zip's signature before uploading,
-and a rejected install surfaces as "Update failed" instead of leaving the card
-untouched.
diff --git a/.changeset/selfhost-auth-rate-limit.md b/.changeset/selfhost-auth-rate-limit.md
deleted file mode 100644
index 1324e53436..0000000000
--- a/.changeset/selfhost-auth-rate-limit.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-"@executor-js/host-selfhost": patch
-"executor": patch
----
-
-Add `EXECUTOR_DISABLE_AUTH_RATE_LIMIT` to the self-host. Better Auth 1.6.17 and later enforce sign-in rate limits strictly in production, and with no trusted proxy header every caller shares one bucket of three sign-ins per ten seconds. The Docker release gate signs in from many test files at once and tripped it. The flag is off by default; the e2e harness sets it for the image it tests.
diff --git a/apps/cli/CHANGELOG.md b/apps/cli/CHANGELOG.md
index 1d6806e6aa..7d64d39efb 100644
--- a/apps/cli/CHANGELOG.md
+++ b/apps/cli/CHANGELOG.md
@@ -1,5 +1,17 @@
# executor
+## 1.6.10
+
+### Patch Changes
+
+- [#2044](https://github.com/UsefulSoftwareCo/executor/pull/2044) [`004024b`](https://github.com/UsefulSoftwareCo/executor/commit/004024b453e9ba07317d2893f050a0d6dae6a67b) Thanks [@RhysSullivan](https://github.com/RhysSullivan)! - Add `EXECUTOR_DISABLE_AUTH_RATE_LIMIT` to the self-host. Better Auth 1.6.17 and later enforce sign-in rate limits strictly in production, and with no trusted proxy header every caller shares one bucket of three sign-ins per ten seconds. The Docker release gate signs in from many test files at once and tripped it. The flag is off by default; the e2e harness sets it for the image it tests.
+
+- Updated dependencies []:
+ - @executor-js/sdk@1.6.10
+ - @executor-js/runtime-quickjs@1.6.10
+ - @executor-js/local@1.6.10
+ - @executor-js/api@1.4.73
+
## 1.6.9
### Patch Changes
diff --git a/apps/cli/package.json b/apps/cli/package.json
index 8f96399565..f298ce7c4c 100644
--- a/apps/cli/package.json
+++ b/apps/cli/package.json
@@ -1,6 +1,6 @@
{
"name": "executor",
- "version": "1.6.9",
+ "version": "1.6.10",
"private": true,
"bin": {
"executor": "./bin/executor.ts"
diff --git a/apps/cloud/CHANGELOG.md b/apps/cloud/CHANGELOG.md
index 407785932d..691c2e786b 100644
--- a/apps/cloud/CHANGELOG.md
+++ b/apps/cloud/CHANGELOG.md
@@ -1,5 +1,26 @@
# @executor-js/cloud
+## 1.4.71
+
+### Patch Changes
+
+- Updated dependencies []:
+ - @executor-js/sdk@1.6.10
+ - @executor-js/runtime-quickjs@1.6.10
+ - @executor-js/execution@1.6.10
+ - @executor-js/plugin-graphql@1.6.10
+ - @executor-js/plugin-mcp@1.6.10
+ - @executor-js/plugin-openapi@1.6.10
+ - @executor-js/api@1.4.73
+ - @executor-js/vite-plugin@0.0.70
+ - @executor-js/cloudflare@0.0.52
+ - @executor-js/host-mcp@1.4.4
+ - @executor-js/mcp-apps-shell@1.4.21
+ - @executor-js/runtime-dynamic-worker@1.4.4
+ - @executor-js/plugin-toolkits@1.5.45
+ - @executor-js/plugin-workos-vault@0.0.2
+ - @executor-js/react@1.4.73
+
## 1.4.70
### Patch Changes
diff --git a/apps/cloud/package.json b/apps/cloud/package.json
index 3c1dde605f..6bf5a8c91a 100644
--- a/apps/cloud/package.json
+++ b/apps/cloud/package.json
@@ -1,6 +1,6 @@
{
"name": "@executor-js/cloud",
- "version": "1.4.70",
+ "version": "1.4.71",
"private": true,
"type": "module",
"scripts": {
diff --git a/apps/desktop/CHANGELOG.md b/apps/desktop/CHANGELOG.md
index 01d54c3530..28c1dd5bcf 100644
--- a/apps/desktop/CHANGELOG.md
+++ b/apps/desktop/CHANGELOG.md
@@ -1,5 +1,17 @@
# @executor-js/desktop
+## 1.6.10
+
+### Patch Changes
+
+- [#2049](https://github.com/UsefulSoftwareCo/executor/pull/2049) [`dc0808d`](https://github.com/UsefulSoftwareCo/executor/commit/dc0808d5ca9716d73e4def9365f49d4c1afd4af9) Thanks [@RhysSullivan](https://github.com/RhysSullivan)! - Fix macOS auto-update. The 1.6.9 update zip was built with a 7-Zip that
+ expanded the framework symlinks into copies, so the extracted app failed code
+ signing and Squirrel.Mac silently refused to install it; "Restart to update"
+ appeared to do nothing. electron-builder is bumped to a release that preserves
+ symlinks, the publish job now verifies the zip's signature before uploading,
+ and a rejected install surfaces as "Update failed" instead of leaving the card
+ untouched.
+
## 1.6.9
## 1.6.8
diff --git a/apps/desktop/package.json b/apps/desktop/package.json
index 771ea9103f..0c17148dd4 100644
--- a/apps/desktop/package.json
+++ b/apps/desktop/package.json
@@ -1,6 +1,6 @@
{
"name": "@executor-js/desktop",
- "version": "1.6.9",
+ "version": "1.6.10",
"private": true,
"homepage": "https://github.com/UsefulSoftwareCo/executor",
"license": "MIT",
diff --git a/apps/host-selfhost/CHANGELOG.md b/apps/host-selfhost/CHANGELOG.md
index 222a2029f8..e4d5e8b9ba 100644
--- a/apps/host-selfhost/CHANGELOG.md
+++ b/apps/host-selfhost/CHANGELOG.md
@@ -1,5 +1,28 @@
# @executor-js/host-selfhost
+## 0.0.52
+
+### Patch Changes
+
+- [#2044](https://github.com/UsefulSoftwareCo/executor/pull/2044) [`004024b`](https://github.com/UsefulSoftwareCo/executor/commit/004024b453e9ba07317d2893f050a0d6dae6a67b) Thanks [@RhysSullivan](https://github.com/RhysSullivan)! - Add `EXECUTOR_DISABLE_AUTH_RATE_LIMIT` to the self-host. Better Auth 1.6.17 and later enforce sign-in rate limits strictly in production, and with no trusted proxy header every caller shares one bucket of three sign-ins per ten seconds. The Docker release gate signs in from many test files at once and tripped it. The flag is off by default; the e2e harness sets it for the image it tests.
+
+- Updated dependencies []:
+ - @executor-js/sdk@1.6.10
+ - @executor-js/runtime-quickjs@1.6.10
+ - @executor-js/execution@1.6.10
+ - @executor-js/plugin-graphql@1.6.10
+ - @executor-js/plugin-mcp@1.6.10
+ - @executor-js/plugin-openapi@1.6.10
+ - @executor-js/app@1.4.4
+ - @executor-js/analytics@0.1.17
+ - @executor-js/api@1.4.73
+ - @executor-js/host-mcp@1.4.4
+ - @executor-js/mcp-apps-shell@1.4.21
+ - @executor-js/plugin-encrypted-secrets@0.0.52
+ - @executor-js/plugin-provider-service-split@0.0.24
+ - @executor-js/plugin-toolkits@1.5.45
+ - @executor-js/react@1.4.73
+
## 0.0.51
### Patch Changes
diff --git a/apps/host-selfhost/package.json b/apps/host-selfhost/package.json
index d91eaae8bf..0f9c804248 100644
--- a/apps/host-selfhost/package.json
+++ b/apps/host-selfhost/package.json
@@ -1,6 +1,6 @@
{
"name": "@executor-js/host-selfhost",
- "version": "0.0.51",
+ "version": "0.0.52",
"private": true,
"type": "module",
"exports": {
diff --git a/apps/local/CHANGELOG.md b/apps/local/CHANGELOG.md
index a13966fc3a..54955cdca3 100644
--- a/apps/local/CHANGELOG.md
+++ b/apps/local/CHANGELOG.md
@@ -1,5 +1,32 @@
# @executor-js/local
+## 1.6.10
+
+### Patch Changes
+
+- Updated dependencies []:
+ - @executor-js/sdk@1.6.10
+ - @executor-js/runtime-quickjs@1.6.10
+ - @executor-js/execution@1.6.10
+ - @executor-js/config@1.6.10
+ - @executor-js/plugin-file-secrets@1.6.10
+ - @executor-js/plugin-graphql@1.6.10
+ - @executor-js/plugin-keychain@1.6.10
+ - @executor-js/plugin-mcp@1.6.10
+ - @executor-js/plugin-onepassword@1.6.10
+ - @executor-js/plugin-openapi@1.6.10
+ - @executor-js/plugin-example@1.6.10
+ - @executor-js/plugin-desktop-settings@1.6.10
+ - @executor-js/app@1.4.4
+ - @executor-js/analytics@0.1.17
+ - @executor-js/api@1.4.73
+ - @executor-js/vite-plugin@0.0.70
+ - @executor-js/host-mcp@1.4.4
+ - @executor-js/mcp-apps-shell@1.4.21
+ - @executor-js/plugin-provider-service-split@0.0.24
+ - @executor-js/plugin-toolkits@1.5.45
+ - @executor-js/react@1.4.73
+
## 1.6.9
### Patch Changes
diff --git a/apps/local/package.json b/apps/local/package.json
index c1df3307be..138f4cc37f 100644
--- a/apps/local/package.json
+++ b/apps/local/package.json
@@ -1,6 +1,6 @@
{
"name": "@executor-js/local",
- "version": "1.6.9",
+ "version": "1.6.10",
"private": true,
"type": "module",
"exports": {
diff --git a/bun.lock b/bun.lock
index e00404fe07..87589fabdd 100644
--- a/bun.lock
+++ b/bun.lock
@@ -31,7 +31,7 @@
},
"apps/cli": {
"name": "executor",
- "version": "1.6.9",
+ "version": "1.6.10",
"bin": {
"executor": "./bin/executor.ts",
},
@@ -60,7 +60,7 @@
},
"apps/cloud": {
"name": "@executor-js/cloud",
- "version": "1.4.70",
+ "version": "1.4.71",
"dependencies": {
"@cloudflare/vite-plugin": "^1.31.1",
"@effect/atom-react": "catalog:",
@@ -133,7 +133,7 @@
},
"apps/desktop": {
"name": "@executor-js/desktop",
- "version": "1.6.9",
+ "version": "1.6.10",
"dependencies": {
"@sentry/bun": "^10.57.0",
"@sentry/electron": "7.19.0",
@@ -220,7 +220,7 @@
},
"apps/host-selfhost": {
"name": "@executor-js/host-selfhost",
- "version": "0.0.51",
+ "version": "0.0.52",
"dependencies": {
"@better-auth/api-key": "^1.6.11",
"@cloudflare/worker-bundler": "0.2.1",
@@ -273,7 +273,7 @@
},
"apps/local": {
"name": "@executor-js/local",
- "version": "1.6.9",
+ "version": "1.6.10",
"dependencies": {
"@effect/atom-react": "catalog:",
"@effect/platform-node": "catalog:",
@@ -353,7 +353,7 @@
},
"e2e": {
"name": "@executor-js/e2e",
- "version": "0.0.49",
+ "version": "0.0.50",
"dependencies": {
"@executor-js/api": "workspace:*",
"@executor-js/emulate": "^0.14.2",
@@ -388,7 +388,7 @@
},
"examples/all-plugins": {
"name": "@executor-js/example-all-plugins",
- "version": "0.0.70",
+ "version": "0.0.71",
"dependencies": {
"@executor-js/plugin-file-secrets": "workspace:*",
"@executor-js/plugin-graphql": "workspace:*",
@@ -407,7 +407,7 @@
},
"examples/docs-sdk-quickstart": {
"name": "@executor-js/example-docs-sdk-quickstart",
- "version": "0.0.55",
+ "version": "0.0.56",
"dependencies": {
"@executor-js/plugin-openapi": "workspace:*",
"@executor-js/sdk": "workspace:*",
@@ -464,7 +464,7 @@
},
"packages/core/analytics": {
"name": "@executor-js/analytics",
- "version": "0.1.16",
+ "version": "0.1.17",
"dependencies": {
"@effect/platform-node": "catalog:",
"@executor-js/execution": "workspace:*",
@@ -480,7 +480,7 @@
},
"packages/core/api": {
"name": "@executor-js/api",
- "version": "1.4.72",
+ "version": "1.4.73",
"dependencies": {
"@executor-js/execution": "workspace:*",
"@executor-js/host-mcp": "workspace:*",
@@ -497,7 +497,7 @@
},
"packages/core/cli": {
"name": "@executor-js/cli",
- "version": "0.2.59",
+ "version": "0.2.60",
"bin": {
"executor-sdk": "./dist/index.js",
},
@@ -518,7 +518,7 @@
},
"packages/core/config": {
"name": "@executor-js/config",
- "version": "1.6.9",
+ "version": "1.6.10",
"dependencies": {
"@executor-js/sdk": "workspace:*",
"jiti": "^2.6.1",
@@ -539,7 +539,7 @@
},
"packages/core/execution": {
"name": "@executor-js/execution",
- "version": "1.6.9",
+ "version": "1.6.10",
"dependencies": {
"@executor-js/codemode-core": "workspace:*",
"@executor-js/sdk": "workspace:*",
@@ -605,7 +605,7 @@
},
"packages/core/sdk": {
"name": "@executor-js/sdk",
- "version": "1.6.9",
+ "version": "1.6.10",
"dependencies": {
"@executor-js/fumadb": "workspace:*",
"@standard-schema/spec": "^1.1.0",
@@ -658,7 +658,7 @@
},
"packages/core/vite-plugin": {
"name": "@executor-js/vite-plugin",
- "version": "0.0.69",
+ "version": "0.0.70",
"dependencies": {
"@executor-js/sdk": "workspace:*",
"jiti": "^2.6.1",
@@ -678,7 +678,7 @@
},
"packages/hosts/cloudflare": {
"name": "@executor-js/cloudflare",
- "version": "0.0.51",
+ "version": "0.0.52",
"dependencies": {
"@executor-js/api": "workspace:*",
"@executor-js/execution": "workspace:*",
@@ -719,7 +719,7 @@
},
"packages/hosts/mcp-apps-shell": {
"name": "@executor-js/mcp-apps-shell",
- "version": "1.4.20",
+ "version": "1.4.21",
"dependencies": {
"@executor-js/react": "workspace:*",
"@executor-js/runtime-quickjs": "workspace:*",
@@ -757,7 +757,7 @@
},
"packages/kernel/core": {
"name": "@executor-js/codemode-core",
- "version": "1.6.9",
+ "version": "1.6.10",
"dependencies": {
"@babel/parser": "^7.29.2",
"@standard-schema/spec": "^1.0.0",
@@ -830,7 +830,7 @@
},
"packages/kernel/runtime-quickjs": {
"name": "@executor-js/runtime-quickjs",
- "version": "1.6.9",
+ "version": "1.6.10",
"dependencies": {
"@executor-js/codemode-core": "workspace:*",
"quickjs-emscripten": "catalog:",
@@ -850,7 +850,7 @@
},
"packages/kernel/runtime-workerd-subprocess": {
"name": "@executor-js/runtime-workerd-subprocess",
- "version": "0.0.24",
+ "version": "0.0.25",
"dependencies": {
"@executor-js/codemode-core": "workspace:*",
"effect": "catalog:",
@@ -865,7 +865,7 @@
},
"packages/onboarding-demo": {
"name": "@executor-js/onboarding-demo",
- "version": "0.0.4",
+ "version": "0.0.5",
"dependencies": {
"@executor-js/plugin-mcp": "workspace:*",
"@executor-js/plugin-openapi": "workspace:*",
@@ -889,7 +889,7 @@
},
"packages/plugins/desktop-settings": {
"name": "@executor-js/plugin-desktop-settings",
- "version": "1.6.9",
+ "version": "1.6.10",
"dependencies": {
"@executor-js/sdk": "workspace:*",
"react": "catalog:",
@@ -902,7 +902,7 @@
},
"packages/plugins/encrypted-secrets": {
"name": "@executor-js/plugin-encrypted-secrets",
- "version": "0.0.51",
+ "version": "0.0.52",
"dependencies": {
"@executor-js/sdk": "workspace:*",
"effect": "catalog:",
@@ -917,7 +917,7 @@
},
"packages/plugins/example": {
"name": "@executor-js/plugin-example",
- "version": "1.6.9",
+ "version": "1.6.10",
"dependencies": {
"@executor-js/sdk": "workspace:*",
},
@@ -940,7 +940,7 @@
},
"packages/plugins/file-secrets": {
"name": "@executor-js/plugin-file-secrets",
- "version": "1.6.9",
+ "version": "1.6.10",
"dependencies": {
"@executor-js/sdk": "workspace:*",
},
@@ -957,7 +957,7 @@
},
"packages/plugins/graphql": {
"name": "@executor-js/plugin-graphql",
- "version": "1.6.9",
+ "version": "1.6.10",
"dependencies": {
"@effect/platform-node": "catalog:",
"@executor-js/config": "workspace:*",
@@ -996,7 +996,7 @@
},
"packages/plugins/keychain": {
"name": "@executor-js/plugin-keychain",
- "version": "1.6.9",
+ "version": "1.6.10",
"dependencies": {
"@executor-js/sdk": "workspace:*",
"@napi-rs/keyring": "^1.2.0",
@@ -1015,7 +1015,7 @@
},
"packages/plugins/mcp": {
"name": "@executor-js/plugin-mcp",
- "version": "1.6.9",
+ "version": "1.6.10",
"dependencies": {
"@cfworker/json-schema": "^4.1.1",
"@effect/platform-node": "catalog:",
@@ -1059,7 +1059,7 @@
},
"packages/plugins/onepassword": {
"name": "@executor-js/plugin-onepassword",
- "version": "1.6.9",
+ "version": "1.6.10",
"dependencies": {
"@1password/sdk": "^0.4.1-beta.1",
"@effect/atom-react": "catalog:",
@@ -1092,7 +1092,7 @@
},
"packages/plugins/openapi": {
"name": "@executor-js/plugin-openapi",
- "version": "1.6.9",
+ "version": "1.6.10",
"dependencies": {
"@effect/platform-node": "catalog:",
"@executor-js/config": "workspace:*",
@@ -1133,7 +1133,7 @@
},
"packages/plugins/provider-service-split": {
"name": "@executor-js/plugin-provider-service-split",
- "version": "0.0.23",
+ "version": "0.0.24",
"dependencies": {
"@executor-js/plugin-openapi": "workspace:*",
"@executor-js/sdk": "workspace:*",
@@ -1150,7 +1150,7 @@
},
"packages/plugins/toolkits": {
"name": "@executor-js/plugin-toolkits",
- "version": "1.5.44",
+ "version": "1.5.45",
"dependencies": {
"@executor-js/sdk": "workspace:*",
},
@@ -1219,7 +1219,7 @@
},
"packages/react": {
"name": "@executor-js/react",
- "version": "1.4.72",
+ "version": "1.4.73",
"dependencies": {
"@base-ui/react": "^1.3.0",
"@effect/atom-react": "catalog:",
diff --git a/e2e/CHANGELOG.md b/e2e/CHANGELOG.md
index 625d796924..aeb68f0a64 100644
--- a/e2e/CHANGELOG.md
+++ b/e2e/CHANGELOG.md
@@ -1,5 +1,17 @@
# @executor-js/e2e
+## 0.0.50
+
+### Patch Changes
+
+- Updated dependencies []:
+ - @executor-js/sdk@1.6.10
+ - @executor-js/plugin-graphql@1.6.10
+ - @executor-js/plugin-mcp@1.6.10
+ - @executor-js/plugin-openapi@1.6.10
+ - @executor-js/api@1.4.73
+ - @executor-js/plugin-toolkits@1.5.45
+
## 0.0.49
### Patch Changes
diff --git a/e2e/package.json b/e2e/package.json
index 2671745d08..10f9a8e40f 100644
--- a/e2e/package.json
+++ b/e2e/package.json
@@ -1,6 +1,6 @@
{
"name": "@executor-js/e2e",
- "version": "0.0.49",
+ "version": "0.0.50",
"private": true,
"type": "module",
"scripts": {
diff --git a/examples/all-plugins/CHANGELOG.md b/examples/all-plugins/CHANGELOG.md
index e980998560..cf669b4ade 100644
--- a/examples/all-plugins/CHANGELOG.md
+++ b/examples/all-plugins/CHANGELOG.md
@@ -1,5 +1,19 @@
# @executor-js/example-all-plugins
+## 0.0.71
+
+### Patch Changes
+
+- Updated dependencies []:
+ - @executor-js/sdk@1.6.10
+ - @executor-js/plugin-file-secrets@1.6.10
+ - @executor-js/plugin-graphql@1.6.10
+ - @executor-js/plugin-keychain@1.6.10
+ - @executor-js/plugin-mcp@1.6.10
+ - @executor-js/plugin-onepassword@1.6.10
+ - @executor-js/plugin-openapi@1.6.10
+ - @executor-js/plugin-workos-vault@0.0.2
+
## 0.0.70
### Patch Changes
diff --git a/examples/all-plugins/package.json b/examples/all-plugins/package.json
index 4022ceddd2..6adceba1ff 100644
--- a/examples/all-plugins/package.json
+++ b/examples/all-plugins/package.json
@@ -1,6 +1,6 @@
{
"name": "@executor-js/example-all-plugins",
- "version": "0.0.70",
+ "version": "0.0.71",
"private": true,
"type": "module",
"scripts": {
diff --git a/examples/docs-sdk-quickstart/CHANGELOG.md b/examples/docs-sdk-quickstart/CHANGELOG.md
index 058f39683f..a7d9111d44 100644
--- a/examples/docs-sdk-quickstart/CHANGELOG.md
+++ b/examples/docs-sdk-quickstart/CHANGELOG.md
@@ -1,5 +1,13 @@
# @executor-js/example-docs-sdk-quickstart
+## 0.0.56
+
+### Patch Changes
+
+- Updated dependencies []:
+ - @executor-js/sdk@1.6.10
+ - @executor-js/plugin-openapi@1.6.10
+
## 0.0.55
### Patch Changes
diff --git a/examples/docs-sdk-quickstart/package.json b/examples/docs-sdk-quickstart/package.json
index da8f8f480d..5828cfdb82 100644
--- a/examples/docs-sdk-quickstart/package.json
+++ b/examples/docs-sdk-quickstart/package.json
@@ -1,6 +1,6 @@
{
"name": "@executor-js/example-docs-sdk-quickstart",
- "version": "0.0.55",
+ "version": "0.0.56",
"private": true,
"type": "module",
"scripts": {
diff --git a/packages/core/analytics/CHANGELOG.md b/packages/core/analytics/CHANGELOG.md
index 4827f67b1f..ee97c48fab 100644
--- a/packages/core/analytics/CHANGELOG.md
+++ b/packages/core/analytics/CHANGELOG.md
@@ -1,5 +1,12 @@
# @executor-js/analytics
+## 0.1.17
+
+### Patch Changes
+
+- Updated dependencies []:
+ - @executor-js/execution@1.6.10
+
## 0.1.16
### Patch Changes
diff --git a/packages/core/analytics/package.json b/packages/core/analytics/package.json
index 9b8baf18eb..1862e0851d 100644
--- a/packages/core/analytics/package.json
+++ b/packages/core/analytics/package.json
@@ -1,6 +1,6 @@
{
"name": "@executor-js/analytics",
- "version": "0.1.16",
+ "version": "0.1.17",
"private": true,
"homepage": "https://github.com/UsefulSoftwareCo/executor/tree/main/packages/core/analytics",
"bugs": {
diff --git a/packages/core/api/CHANGELOG.md b/packages/core/api/CHANGELOG.md
index 7910d9a3cf..ea1036bfd3 100644
--- a/packages/core/api/CHANGELOG.md
+++ b/packages/core/api/CHANGELOG.md
@@ -1,5 +1,14 @@
# @executor-js/api
+## 1.4.73
+
+### Patch Changes
+
+- Updated dependencies []:
+ - @executor-js/sdk@1.6.10
+ - @executor-js/execution@1.6.10
+ - @executor-js/host-mcp@1.4.4
+
## 1.4.72
### Patch Changes
diff --git a/packages/core/api/package.json b/packages/core/api/package.json
index 8205bb886e..31c1beca31 100644
--- a/packages/core/api/package.json
+++ b/packages/core/api/package.json
@@ -1,6 +1,6 @@
{
"name": "@executor-js/api",
- "version": "1.4.72",
+ "version": "1.4.73",
"private": true,
"type": "module",
"exports": {
diff --git a/packages/core/cli/CHANGELOG.md b/packages/core/cli/CHANGELOG.md
index adb1299ba0..e1812914a5 100644
--- a/packages/core/cli/CHANGELOG.md
+++ b/packages/core/cli/CHANGELOG.md
@@ -1,5 +1,12 @@
# @executor-js/cli
+## 0.2.60
+
+### Patch Changes
+
+- Updated dependencies []:
+ - @executor-js/sdk@1.6.10
+
## 0.2.59
### Patch Changes
diff --git a/packages/core/cli/package.json b/packages/core/cli/package.json
index f5477f210b..ab83e21e04 100644
--- a/packages/core/cli/package.json
+++ b/packages/core/cli/package.json
@@ -1,6 +1,6 @@
{
"name": "@executor-js/cli",
- "version": "0.2.59",
+ "version": "0.2.60",
"description": "CLI for the executor SDK — schema generation, migrations",
"homepage": "https://github.com/UsefulSoftwareCo/executor/tree/main/packages/core/cli",
"bugs": {
diff --git a/packages/core/config/CHANGELOG.md b/packages/core/config/CHANGELOG.md
index 20701aaeb9..46b8e59a35 100644
--- a/packages/core/config/CHANGELOG.md
+++ b/packages/core/config/CHANGELOG.md
@@ -1,5 +1,12 @@
# @executor-js/config
+## 1.6.10
+
+### Patch Changes
+
+- Updated dependencies []:
+ - @executor-js/sdk@1.6.10
+
## 1.6.9
### Patch Changes
diff --git a/packages/core/config/package.json b/packages/core/config/package.json
index dfd10bd38a..b128eebddf 100644
--- a/packages/core/config/package.json
+++ b/packages/core/config/package.json
@@ -1,6 +1,6 @@
{
"name": "@executor-js/config",
- "version": "1.6.9",
+ "version": "1.6.10",
"homepage": "https://github.com/UsefulSoftwareCo/executor/tree/main/packages/core/config",
"bugs": {
"url": "https://github.com/UsefulSoftwareCo/executor/issues"
diff --git a/packages/core/execution/CHANGELOG.md b/packages/core/execution/CHANGELOG.md
index cbfa89cdb7..cd3eee3c46 100644
--- a/packages/core/execution/CHANGELOG.md
+++ b/packages/core/execution/CHANGELOG.md
@@ -1,5 +1,13 @@
# @executor-js/execution
+## 1.6.10
+
+### Patch Changes
+
+- Updated dependencies []:
+ - @executor-js/sdk@1.6.10
+ - @executor-js/codemode-core@1.6.10
+
## 1.6.9
### Patch Changes
diff --git a/packages/core/execution/package.json b/packages/core/execution/package.json
index 0891004b15..edefd98597 100644
--- a/packages/core/execution/package.json
+++ b/packages/core/execution/package.json
@@ -1,6 +1,6 @@
{
"name": "@executor-js/execution",
- "version": "1.6.9",
+ "version": "1.6.10",
"homepage": "https://github.com/UsefulSoftwareCo/executor/tree/main/packages/core/execution",
"bugs": {
"url": "https://github.com/UsefulSoftwareCo/executor/issues"
diff --git a/packages/core/sdk/CHANGELOG.md b/packages/core/sdk/CHANGELOG.md
index 4a1f1d2334..6e05d81e28 100644
--- a/packages/core/sdk/CHANGELOG.md
+++ b/packages/core/sdk/CHANGELOG.md
@@ -1,5 +1,7 @@
# @executor-js/sdk
+## 1.6.10
+
## 1.6.9
### Patch Changes
diff --git a/packages/core/sdk/package.json b/packages/core/sdk/package.json
index 9412d9d4da..bf4374d6e7 100644
--- a/packages/core/sdk/package.json
+++ b/packages/core/sdk/package.json
@@ -1,6 +1,6 @@
{
"name": "@executor-js/sdk",
- "version": "1.6.9",
+ "version": "1.6.10",
"homepage": "https://github.com/UsefulSoftwareCo/executor/tree/main/packages/core/sdk",
"bugs": {
"url": "https://github.com/UsefulSoftwareCo/executor/issues"
diff --git a/packages/core/vite-plugin/CHANGELOG.md b/packages/core/vite-plugin/CHANGELOG.md
index ab73ab229f..dafa62eb8b 100644
--- a/packages/core/vite-plugin/CHANGELOG.md
+++ b/packages/core/vite-plugin/CHANGELOG.md
@@ -1,5 +1,12 @@
# @executor-js/vite-plugin
+## 0.0.70
+
+### Patch Changes
+
+- Updated dependencies []:
+ - @executor-js/sdk@1.6.10
+
## 0.0.69
### Patch Changes
diff --git a/packages/core/vite-plugin/package.json b/packages/core/vite-plugin/package.json
index 8d013f6dd6..4c9203e8d6 100644
--- a/packages/core/vite-plugin/package.json
+++ b/packages/core/vite-plugin/package.json
@@ -1,6 +1,6 @@
{
"name": "@executor-js/vite-plugin",
- "version": "0.0.69",
+ "version": "0.0.70",
"homepage": "https://github.com/UsefulSoftwareCo/executor/tree/main/packages/core/vite-plugin",
"bugs": {
"url": "https://github.com/UsefulSoftwareCo/executor/issues"
diff --git a/packages/hosts/cloudflare/CHANGELOG.md b/packages/hosts/cloudflare/CHANGELOG.md
index 8d548affc0..5e54606529 100644
--- a/packages/hosts/cloudflare/CHANGELOG.md
+++ b/packages/hosts/cloudflare/CHANGELOG.md
@@ -1,5 +1,15 @@
# @executor-js/cloudflare
+## 0.0.52
+
+### Patch Changes
+
+- Updated dependencies []:
+ - @executor-js/sdk@1.6.10
+ - @executor-js/execution@1.6.10
+ - @executor-js/api@1.4.73
+ - @executor-js/host-mcp@1.4.4
+
## 0.0.51
### Patch Changes
diff --git a/packages/hosts/cloudflare/package.json b/packages/hosts/cloudflare/package.json
index d18022c193..a04546dbd4 100644
--- a/packages/hosts/cloudflare/package.json
+++ b/packages/hosts/cloudflare/package.json
@@ -1,6 +1,6 @@
{
"name": "@executor-js/cloudflare",
- "version": "0.0.51",
+ "version": "0.0.52",
"private": true,
"type": "module",
"exports": {
diff --git a/packages/hosts/mcp-apps-shell/CHANGELOG.md b/packages/hosts/mcp-apps-shell/CHANGELOG.md
index 4c7426080c..dbeb773c76 100644
--- a/packages/hosts/mcp-apps-shell/CHANGELOG.md
+++ b/packages/hosts/mcp-apps-shell/CHANGELOG.md
@@ -1,5 +1,13 @@
# @executor-js/mcp-apps-shell
+## 1.4.21
+
+### Patch Changes
+
+- Updated dependencies []:
+ - @executor-js/runtime-quickjs@1.6.10
+ - @executor-js/react@1.4.73
+
## 1.4.20
### Patch Changes
diff --git a/packages/hosts/mcp-apps-shell/package.json b/packages/hosts/mcp-apps-shell/package.json
index abd6616b7f..e1615bdcc2 100644
--- a/packages/hosts/mcp-apps-shell/package.json
+++ b/packages/hosts/mcp-apps-shell/package.json
@@ -1,6 +1,6 @@
{
"name": "@executor-js/mcp-apps-shell",
- "version": "1.4.20",
+ "version": "1.4.21",
"private": true,
"type": "module",
"exports": {
diff --git a/packages/kernel/core/CHANGELOG.md b/packages/kernel/core/CHANGELOG.md
index ff5850ac6a..eed11fadab 100644
--- a/packages/kernel/core/CHANGELOG.md
+++ b/packages/kernel/core/CHANGELOG.md
@@ -1,5 +1,7 @@
# @executor-js/codemode-core
+## 1.6.10
+
## 1.6.9
## 1.6.8
diff --git a/packages/kernel/core/package.json b/packages/kernel/core/package.json
index d2952db08a..a1d24ebbe7 100644
--- a/packages/kernel/core/package.json
+++ b/packages/kernel/core/package.json
@@ -1,6 +1,6 @@
{
"name": "@executor-js/codemode-core",
- "version": "1.6.9",
+ "version": "1.6.10",
"homepage": "https://github.com/UsefulSoftwareCo/executor/tree/main/packages/kernel/core",
"bugs": {
"url": "https://github.com/UsefulSoftwareCo/executor/issues"
diff --git a/packages/kernel/runtime-quickjs/CHANGELOG.md b/packages/kernel/runtime-quickjs/CHANGELOG.md
index 19a9cde4ea..5425da04d1 100644
--- a/packages/kernel/runtime-quickjs/CHANGELOG.md
+++ b/packages/kernel/runtime-quickjs/CHANGELOG.md
@@ -1,5 +1,12 @@
# @executor-js/runtime-quickjs
+## 1.6.10
+
+### Patch Changes
+
+- Updated dependencies []:
+ - @executor-js/codemode-core@1.6.10
+
## 1.6.9
### Patch Changes
diff --git a/packages/kernel/runtime-quickjs/package.json b/packages/kernel/runtime-quickjs/package.json
index 082895092f..029a937e2e 100644
--- a/packages/kernel/runtime-quickjs/package.json
+++ b/packages/kernel/runtime-quickjs/package.json
@@ -1,6 +1,6 @@
{
"name": "@executor-js/runtime-quickjs",
- "version": "1.6.9",
+ "version": "1.6.10",
"homepage": "https://github.com/UsefulSoftwareCo/executor/tree/main/packages/kernel/runtime-quickjs",
"bugs": {
"url": "https://github.com/UsefulSoftwareCo/executor/issues"
diff --git a/packages/kernel/runtime-workerd-subprocess/CHANGELOG.md b/packages/kernel/runtime-workerd-subprocess/CHANGELOG.md
index 0c0aa1bcab..bc94e1566c 100644
--- a/packages/kernel/runtime-workerd-subprocess/CHANGELOG.md
+++ b/packages/kernel/runtime-workerd-subprocess/CHANGELOG.md
@@ -1,5 +1,12 @@
# @executor-js/runtime-workerd-subprocess
+## 0.0.25
+
+### Patch Changes
+
+- Updated dependencies []:
+ - @executor-js/codemode-core@1.6.10
+
## 0.0.24
### Patch Changes
diff --git a/packages/kernel/runtime-workerd-subprocess/package.json b/packages/kernel/runtime-workerd-subprocess/package.json
index cb98709333..be832a7e18 100644
--- a/packages/kernel/runtime-workerd-subprocess/package.json
+++ b/packages/kernel/runtime-workerd-subprocess/package.json
@@ -1,6 +1,6 @@
{
"name": "@executor-js/runtime-workerd-subprocess",
- "version": "0.0.24",
+ "version": "0.0.25",
"private": true,
"type": "module",
"exports": {
diff --git a/packages/onboarding-demo/CHANGELOG.md b/packages/onboarding-demo/CHANGELOG.md
index 886ee25d63..5af39e4d7a 100644
--- a/packages/onboarding-demo/CHANGELOG.md
+++ b/packages/onboarding-demo/CHANGELOG.md
@@ -1,5 +1,15 @@
# @executor-js/onboarding-demo
+## 0.0.5
+
+### Patch Changes
+
+- Updated dependencies []:
+ - @executor-js/sdk@1.6.10
+ - @executor-js/plugin-mcp@1.6.10
+ - @executor-js/plugin-openapi@1.6.10
+ - @executor-js/react@1.4.73
+
## 0.0.4
### Patch Changes
diff --git a/packages/onboarding-demo/package.json b/packages/onboarding-demo/package.json
index 5e6fbf4b00..0ec0fc60da 100644
--- a/packages/onboarding-demo/package.json
+++ b/packages/onboarding-demo/package.json
@@ -1,6 +1,6 @@
{
"name": "@executor-js/onboarding-demo",
- "version": "0.0.4",
+ "version": "0.0.5",
"private": true,
"type": "module",
"scripts": {
diff --git a/packages/plugins/desktop-settings/CHANGELOG.md b/packages/plugins/desktop-settings/CHANGELOG.md
index 38975d5802..95d45049af 100644
--- a/packages/plugins/desktop-settings/CHANGELOG.md
+++ b/packages/plugins/desktop-settings/CHANGELOG.md
@@ -1,5 +1,12 @@
# @executor-js/plugin-desktop-settings
+## 1.6.10
+
+### Patch Changes
+
+- Updated dependencies []:
+ - @executor-js/sdk@1.6.10
+
## 1.6.9
### Patch Changes
diff --git a/packages/plugins/desktop-settings/package.json b/packages/plugins/desktop-settings/package.json
index aba4e2a3f5..3f927dc2c0 100644
--- a/packages/plugins/desktop-settings/package.json
+++ b/packages/plugins/desktop-settings/package.json
@@ -1,6 +1,6 @@
{
"name": "@executor-js/plugin-desktop-settings",
- "version": "1.6.9",
+ "version": "1.6.10",
"homepage": "https://github.com/UsefulSoftwareCo/executor/tree/main/packages/plugins/desktop-settings",
"bugs": {
"url": "https://github.com/UsefulSoftwareCo/executor/issues"
diff --git a/packages/plugins/encrypted-secrets/CHANGELOG.md b/packages/plugins/encrypted-secrets/CHANGELOG.md
index 4fc7c36721..db01af7f82 100644
--- a/packages/plugins/encrypted-secrets/CHANGELOG.md
+++ b/packages/plugins/encrypted-secrets/CHANGELOG.md
@@ -1,5 +1,12 @@
# @executor-js/plugin-encrypted-secrets
+## 0.0.52
+
+### Patch Changes
+
+- Updated dependencies []:
+ - @executor-js/sdk@1.6.10
+
## 0.0.51
### Patch Changes
diff --git a/packages/plugins/encrypted-secrets/package.json b/packages/plugins/encrypted-secrets/package.json
index d47049b036..0499dd9136 100644
--- a/packages/plugins/encrypted-secrets/package.json
+++ b/packages/plugins/encrypted-secrets/package.json
@@ -1,6 +1,6 @@
{
"name": "@executor-js/plugin-encrypted-secrets",
- "version": "0.0.51",
+ "version": "0.0.52",
"private": true,
"type": "module",
"exports": {
diff --git a/packages/plugins/example/CHANGELOG.md b/packages/plugins/example/CHANGELOG.md
index 84188a7a3e..ebef693e0e 100644
--- a/packages/plugins/example/CHANGELOG.md
+++ b/packages/plugins/example/CHANGELOG.md
@@ -1,5 +1,12 @@
# @executor-js/plugin-example
+## 1.6.10
+
+### Patch Changes
+
+- Updated dependencies []:
+ - @executor-js/sdk@1.6.10
+
## 1.6.9
### Patch Changes
diff --git a/packages/plugins/example/package.json b/packages/plugins/example/package.json
index 61e2cc5f18..87b1e2a62a 100644
--- a/packages/plugins/example/package.json
+++ b/packages/plugins/example/package.json
@@ -1,6 +1,6 @@
{
"name": "@executor-js/plugin-example",
- "version": "1.6.9",
+ "version": "1.6.10",
"homepage": "https://github.com/UsefulSoftwareCo/executor/tree/main/packages/plugins/example",
"bugs": {
"url": "https://github.com/UsefulSoftwareCo/executor/issues"
diff --git a/packages/plugins/file-secrets/CHANGELOG.md b/packages/plugins/file-secrets/CHANGELOG.md
index 4fb028a45f..5aa71cc0af 100644
--- a/packages/plugins/file-secrets/CHANGELOG.md
+++ b/packages/plugins/file-secrets/CHANGELOG.md
@@ -1,5 +1,12 @@
# @executor-js/plugin-file-secrets
+## 1.6.10
+
+### Patch Changes
+
+- Updated dependencies []:
+ - @executor-js/sdk@1.6.10
+
## 1.6.9
### Patch Changes
diff --git a/packages/plugins/file-secrets/package.json b/packages/plugins/file-secrets/package.json
index dc783a98bd..2575cad448 100644
--- a/packages/plugins/file-secrets/package.json
+++ b/packages/plugins/file-secrets/package.json
@@ -1,6 +1,6 @@
{
"name": "@executor-js/plugin-file-secrets",
- "version": "1.6.9",
+ "version": "1.6.10",
"homepage": "https://github.com/UsefulSoftwareCo/executor/tree/main/packages/plugins/file-secrets",
"bugs": {
"url": "https://github.com/UsefulSoftwareCo/executor/issues"
diff --git a/packages/plugins/graphql/CHANGELOG.md b/packages/plugins/graphql/CHANGELOG.md
index b063c836e2..22f48273fb 100644
--- a/packages/plugins/graphql/CHANGELOG.md
+++ b/packages/plugins/graphql/CHANGELOG.md
@@ -1,5 +1,15 @@
# @executor-js/plugin-graphql
+## 1.6.10
+
+### Patch Changes
+
+- Updated dependencies []:
+ - @executor-js/sdk@1.6.10
+ - @executor-js/config@1.6.10
+ - @executor-js/api@1.4.73
+ - @executor-js/react@1.4.73
+
## 1.6.9
### Patch Changes
diff --git a/packages/plugins/graphql/package.json b/packages/plugins/graphql/package.json
index c1de04eaa7..3aa148b426 100644
--- a/packages/plugins/graphql/package.json
+++ b/packages/plugins/graphql/package.json
@@ -1,6 +1,6 @@
{
"name": "@executor-js/plugin-graphql",
- "version": "1.6.9",
+ "version": "1.6.10",
"homepage": "https://github.com/UsefulSoftwareCo/executor/tree/main/packages/plugins/graphql",
"bugs": {
"url": "https://github.com/UsefulSoftwareCo/executor/issues"
diff --git a/packages/plugins/keychain/CHANGELOG.md b/packages/plugins/keychain/CHANGELOG.md
index 68c8f47d50..e0c3dbd480 100644
--- a/packages/plugins/keychain/CHANGELOG.md
+++ b/packages/plugins/keychain/CHANGELOG.md
@@ -1,5 +1,12 @@
# @executor-js/plugin-keychain
+## 1.6.10
+
+### Patch Changes
+
+- Updated dependencies []:
+ - @executor-js/sdk@1.6.10
+
## 1.6.9
### Patch Changes
diff --git a/packages/plugins/keychain/package.json b/packages/plugins/keychain/package.json
index d28235f142..c679b44b8d 100644
--- a/packages/plugins/keychain/package.json
+++ b/packages/plugins/keychain/package.json
@@ -1,6 +1,6 @@
{
"name": "@executor-js/plugin-keychain",
- "version": "1.6.9",
+ "version": "1.6.10",
"homepage": "https://github.com/UsefulSoftwareCo/executor/tree/main/packages/plugins/keychain",
"bugs": {
"url": "https://github.com/UsefulSoftwareCo/executor/issues"
diff --git a/packages/plugins/mcp/CHANGELOG.md b/packages/plugins/mcp/CHANGELOG.md
index 54cc8691f5..956176cf99 100644
--- a/packages/plugins/mcp/CHANGELOG.md
+++ b/packages/plugins/mcp/CHANGELOG.md
@@ -1,5 +1,15 @@
# @executor-js/plugin-mcp
+## 1.6.10
+
+### Patch Changes
+
+- Updated dependencies []:
+ - @executor-js/sdk@1.6.10
+ - @executor-js/config@1.6.10
+ - @executor-js/api@1.4.73
+ - @executor-js/react@1.4.73
+
## 1.6.9
### Patch Changes
diff --git a/packages/plugins/mcp/package.json b/packages/plugins/mcp/package.json
index b10e9909d2..b070d53b60 100644
--- a/packages/plugins/mcp/package.json
+++ b/packages/plugins/mcp/package.json
@@ -1,6 +1,6 @@
{
"name": "@executor-js/plugin-mcp",
- "version": "1.6.9",
+ "version": "1.6.10",
"homepage": "https://github.com/UsefulSoftwareCo/executor/tree/main/packages/plugins/mcp",
"bugs": {
"url": "https://github.com/UsefulSoftwareCo/executor/issues"
diff --git a/packages/plugins/onepassword/CHANGELOG.md b/packages/plugins/onepassword/CHANGELOG.md
index 5af3bed5d1..ddbeefa517 100644
--- a/packages/plugins/onepassword/CHANGELOG.md
+++ b/packages/plugins/onepassword/CHANGELOG.md
@@ -1,5 +1,14 @@
# @executor-js/plugin-onepassword
+## 1.6.10
+
+### Patch Changes
+
+- Updated dependencies []:
+ - @executor-js/sdk@1.6.10
+ - @executor-js/api@1.4.73
+ - @executor-js/react@1.4.73
+
## 1.6.9
### Patch Changes
diff --git a/packages/plugins/onepassword/package.json b/packages/plugins/onepassword/package.json
index a52acd00df..7fcecff5e9 100644
--- a/packages/plugins/onepassword/package.json
+++ b/packages/plugins/onepassword/package.json
@@ -1,6 +1,6 @@
{
"name": "@executor-js/plugin-onepassword",
- "version": "1.6.9",
+ "version": "1.6.10",
"homepage": "https://github.com/UsefulSoftwareCo/executor/tree/main/packages/plugins/onepassword",
"bugs": {
"url": "https://github.com/UsefulSoftwareCo/executor/issues"
diff --git a/packages/plugins/openapi/CHANGELOG.md b/packages/plugins/openapi/CHANGELOG.md
index 29dcdb2f4f..e16ed52c78 100644
--- a/packages/plugins/openapi/CHANGELOG.md
+++ b/packages/plugins/openapi/CHANGELOG.md
@@ -1,5 +1,15 @@
# @executor-js/plugin-openapi
+## 1.6.10
+
+### Patch Changes
+
+- Updated dependencies []:
+ - @executor-js/sdk@1.6.10
+ - @executor-js/config@1.6.10
+ - @executor-js/api@1.4.73
+ - @executor-js/react@1.4.73
+
## 1.6.9
### Patch Changes
diff --git a/packages/plugins/openapi/package.json b/packages/plugins/openapi/package.json
index 57e8d4320b..f9f527f0c9 100644
--- a/packages/plugins/openapi/package.json
+++ b/packages/plugins/openapi/package.json
@@ -1,6 +1,6 @@
{
"name": "@executor-js/plugin-openapi",
- "version": "1.6.9",
+ "version": "1.6.10",
"homepage": "https://github.com/UsefulSoftwareCo/executor/tree/main/packages/plugins/openapi",
"bugs": {
"url": "https://github.com/UsefulSoftwareCo/executor/issues"
diff --git a/packages/plugins/provider-service-split/CHANGELOG.md b/packages/plugins/provider-service-split/CHANGELOG.md
index 0cefad1526..346f7495a7 100644
--- a/packages/plugins/provider-service-split/CHANGELOG.md
+++ b/packages/plugins/provider-service-split/CHANGELOG.md
@@ -1,5 +1,13 @@
# @executor-js/plugin-provider-service-split
+## 0.0.24
+
+### Patch Changes
+
+- Updated dependencies []:
+ - @executor-js/sdk@1.6.10
+ - @executor-js/plugin-openapi@1.6.10
+
## 0.0.23
### Patch Changes
diff --git a/packages/plugins/provider-service-split/package.json b/packages/plugins/provider-service-split/package.json
index a790fdea11..385f3cf685 100644
--- a/packages/plugins/provider-service-split/package.json
+++ b/packages/plugins/provider-service-split/package.json
@@ -1,6 +1,6 @@
{
"name": "@executor-js/plugin-provider-service-split",
- "version": "0.0.23",
+ "version": "0.0.24",
"private": true,
"type": "module",
"exports": {
diff --git a/packages/plugins/toolkits/CHANGELOG.md b/packages/plugins/toolkits/CHANGELOG.md
index a4c8172a01..2df0df288c 100644
--- a/packages/plugins/toolkits/CHANGELOG.md
+++ b/packages/plugins/toolkits/CHANGELOG.md
@@ -1,5 +1,14 @@
# @executor-js/plugin-toolkits
+## 1.5.45
+
+### Patch Changes
+
+- Updated dependencies []:
+ - @executor-js/sdk@1.6.10
+ - @executor-js/api@1.4.73
+ - @executor-js/react@1.4.73
+
## 1.5.44
### Patch Changes
diff --git a/packages/plugins/toolkits/package.json b/packages/plugins/toolkits/package.json
index 275f8f3fcd..9de56ae469 100644
--- a/packages/plugins/toolkits/package.json
+++ b/packages/plugins/toolkits/package.json
@@ -1,6 +1,6 @@
{
"name": "@executor-js/plugin-toolkits",
- "version": "1.5.44",
+ "version": "1.5.45",
"homepage": "https://github.com/UsefulSoftwareCo/executor/tree/main/packages/plugins/toolkits",
"bugs": {
"url": "https://github.com/UsefulSoftwareCo/executor/issues"
diff --git a/packages/react/CHANGELOG.md b/packages/react/CHANGELOG.md
index 6239d80c2c..4c3a7df603 100644
--- a/packages/react/CHANGELOG.md
+++ b/packages/react/CHANGELOG.md
@@ -1,5 +1,13 @@
# @executor-js/react
+## 1.4.73
+
+### Patch Changes
+
+- Updated dependencies []:
+ - @executor-js/sdk@1.6.10
+ - @executor-js/api@1.4.73
+
## 1.4.72
### Patch Changes
diff --git a/packages/react/package.json b/packages/react/package.json
index bc7e72a21a..62e41b5c4b 100644
--- a/packages/react/package.json
+++ b/packages/react/package.json
@@ -1,6 +1,6 @@
{
"name": "@executor-js/react",
- "version": "1.4.72",
+ "version": "1.4.73",
"private": true,
"type": "module",
"exports": {
From 31b24b8564899b137106baf28f66fb3f15c33633 Mon Sep 17 00:00:00 2001
From: Rhys Sullivan <39114868+RhysSullivan@users.noreply.github.com>
Date: Fri, 18 Sep 2026 11:48:00 -0700
Subject: [PATCH 04/11] Show disabled integration actions for members (#2051)
* Align integration creation UI with admin permissions
* Show disabled integration actions for members
---
.changeset/fair-admin-integrations.md | 5 +
.../integration-creation-permissions.test.ts | 17 ++
.../integration-creation-permissions.test.ts | 21 +++
e2e/src/integration-creation-permissions.ts | 164 ++++++++++++++++++
.../react/src/components/command-palette.tsx | 9 +-
.../components/integration-creation-gate.tsx | 36 ++++
.../src/components/workspace-admin-hint.tsx | 29 ++++
packages/react/src/multiplayer/shell.tsx | 28 +--
packages/react/src/pages/integration-add.tsx | 16 +-
.../react/src/pages/integration-browse.tsx | 20 ++-
.../react/src/pages/integration-detail.tsx | 41 +++--
packages/react/src/pages/integrations.tsx | 65 ++++---
12 files changed, 395 insertions(+), 56 deletions(-)
create mode 100644 .changeset/fair-admin-integrations.md
create mode 100644 e2e/cloud/integration-creation-permissions.test.ts
create mode 100644 e2e/selfhost/integration-creation-permissions.test.ts
create mode 100644 e2e/src/integration-creation-permissions.ts
create mode 100644 packages/react/src/components/integration-creation-gate.tsx
create mode 100644 packages/react/src/components/workspace-admin-hint.tsx
diff --git a/.changeset/fair-admin-integrations.md b/.changeset/fair-admin-integrations.md
new file mode 100644
index 0000000000..a8e83972b8
--- /dev/null
+++ b/.changeset/fair-admin-integrations.md
@@ -0,0 +1,5 @@
+---
+"@executor-js/react": patch
+---
+
+Show restricted integration actions as disabled controls with an admin explanation. Members can browse the catalog and add personal connections to existing integrations.
diff --git a/e2e/cloud/integration-creation-permissions.test.ts b/e2e/cloud/integration-creation-permissions.test.ts
new file mode 100644
index 0000000000..44f7b91bb5
--- /dev/null
+++ b/e2e/cloud/integration-creation-permissions.test.ts
@@ -0,0 +1,17 @@
+import { Effect } from "effect";
+import { scenario } from "../src/scenario";
+import { Target } from "../src/services";
+import { integrationCreationPermissions } from "../src/integration-creation-permissions";
+import { forBrowser, joinOrg } from "./support/session";
+
+scenario(
+ "Integration creation · cloud members see admin guidance and admins can add",
+ { timeout: 180_000 },
+ Effect.gen(function* () {
+ const target = yield* Target;
+ const admin = yield* target.newIdentity();
+ const invitee = yield* target.newIdentity({ org: false });
+ const member = yield* joinOrg(target, admin, invitee);
+ yield* integrationCreationPermissions(forBrowser(admin), forBrowser(member));
+ }),
+);
diff --git a/e2e/selfhost/integration-creation-permissions.test.ts b/e2e/selfhost/integration-creation-permissions.test.ts
new file mode 100644
index 0000000000..0ddd87ebe8
--- /dev/null
+++ b/e2e/selfhost/integration-creation-permissions.test.ts
@@ -0,0 +1,21 @@
+import { Effect } from "effect";
+import { scenario } from "../src/scenario";
+import { Target } from "../src/services";
+import { integrationCreationPermissions } from "../src/integration-creation-permissions";
+import { createInvitedIdentity } from "../targets/selfhost";
+
+scenario(
+ "Integration creation · self-host members see admin guidance and owners can add",
+ { timeout: 180_000 },
+ Effect.gen(function* () {
+ const target = yield* Target;
+ const admin = yield* target.newIdentity();
+ const member = yield* Effect.promise(() =>
+ createInvitedIdentity(target.baseUrl, admin, {
+ role: "member",
+ emailPrefix: "integration-permissions",
+ }),
+ );
+ yield* integrationCreationPermissions(admin, member);
+ }),
+);
diff --git a/e2e/src/integration-creation-permissions.ts b/e2e/src/integration-creation-permissions.ts
new file mode 100644
index 0000000000..c54309a569
--- /dev/null
+++ b/e2e/src/integration-creation-permissions.ts
@@ -0,0 +1,164 @@
+import { randomBytes } from "node:crypto";
+import { expect } from "@effect/vitest";
+import { Effect } from "effect";
+import { composePluginApi } from "@executor-js/api/server";
+import { openApiHttpPlugin } from "@executor-js/plugin-openapi/api";
+import { IntegrationSlug } from "@executor-js/sdk/shared";
+
+import { Api, Browser } from "./services";
+import type { Identity } from "./target";
+import { visit } from "./surfaces/browser";
+
+const api = composePluginApi([openApiHttpPlugin()] as const);
+
+/** Exercise integration creation and member restrictions through the shared console. */
+export const integrationCreationPermissions = (admin: Identity, member: Identity) =>
+ Effect.gen(function* () {
+ const browser = yield* Browser;
+ const { client } = yield* Api;
+ const adminClient = yield* client(api, admin);
+ const title = `Permissions API ${randomBytes(4).toString("hex")}`;
+ const slug = IntegrationSlug.make(title.toLowerCase().replaceAll(" ", "_"));
+ const spec = JSON.stringify({
+ openapi: "3.0.3",
+ info: { title, version: "1.0.0" },
+ servers: [{ url: "https://api.example.com" }],
+ paths: {},
+ components: {
+ securitySchemes: { apiKey: { type: "apiKey", in: "header", name: "X-API-Key" } },
+ },
+ security: [{ apiKey: [] }],
+ });
+
+ yield* Effect.ensuring(
+ Effect.gen(function* () {
+ yield* browser.session(admin, async ({ page, step }) => {
+ await step("Admin opens the integration catalog", async () => {
+ await visit(page, "/");
+ await page.getByRole("button", { name: "Browse integrations", exact: true }).waitFor();
+ await page.keyboard.press("ControlOrMeta+k");
+ await page.getByRole("option", { name: /^Add OpenAPI/ }).waitFor();
+ await page.keyboard.press("Escape");
+ await page.getByRole("link", { name: "Add integration", exact: true }).click();
+ await page.getByRole("heading", { name: "Add an integration", exact: true }).waitFor();
+ await page
+ .getByRole("textbox", { name: "Search integrations, or paste a URL" })
+ .waitFor();
+ });
+ await step("Admin creates an integration from the setup form", async () => {
+ await visit(page, "/integrations/add/openapi");
+ await page.getByPlaceholder("https://api.example.com/openapi.json").fill(spec);
+ await page.getByRole("button", { name: "Add integration", exact: true }).click();
+ await page.waitForURL((url) => url.pathname.endsWith(`/integrations/${slug}`), {
+ timeout: 30_000,
+ });
+ await page.getByRole("button", { name: "Edit", exact: true }).waitFor();
+ await page.getByRole("button", { name: "Delete", exact: true }).waitFor();
+ });
+ });
+ expect(yield* adminClient.integrations.get({ params: { slug } })).toMatchObject({
+ name: title,
+ });
+
+ yield* browser.session(member, async ({ page, step }) => {
+ await step(
+ "Member sees disabled creation controls with an admin explanation",
+ async () => {
+ await visit(page, "/");
+ await page.getByRole("heading", { name: "Integrations", exact: true }).waitFor();
+ await page.getByTestId(`integration-entry-${slug}`).waitFor();
+ const add = page.getByRole("button", { name: "Add integration", exact: true });
+ await add.waitFor();
+ expect(await add.isDisabled()).toBe(true);
+ expect(
+ await page
+ .getByRole("button", { name: "Browse integrations", exact: true })
+ .isDisabled(),
+ ).toBe(true);
+ const hint = page
+ .getByRole("group", { name: "Requires a workspace admin" })
+ .filter({ has: add });
+ await hint.hover();
+ await page.getByRole("tooltip", { name: "Requires a workspace admin" }).waitFor();
+ await hint.focus();
+ const before = page.url();
+ await page.keyboard.press("Enter");
+ expect(page.url()).toBe(before);
+ },
+ );
+ await step(
+ "Member sees disabled add commands and can still find existing integrations",
+ async () => {
+ await page.keyboard.press("ControlOrMeta+k");
+ const palette = page.getByRole("dialog");
+ await palette.getByRole("option", { name: new RegExp(title) }).waitFor();
+ const addCommand = palette.getByRole("option", { name: /^Add OpenAPI/ });
+ await addCommand.waitFor();
+ expect(await addCommand.getAttribute("aria-disabled")).toBe("true");
+ expect(await addCommand.textContent()).toContain("Admin only");
+ await page.keyboard.press("Escape");
+ },
+ );
+ await step("Member sees disabled Edit and Delete actions", async () => {
+ await page.getByTestId(`integration-entry-${slug}`).click();
+ await page.getByRole("button", { name: "Add connection", exact: true }).waitFor();
+ for (const name of ["Edit", "Delete"]) {
+ const action = page.getByRole("button", { name, exact: true });
+ await action.waitFor();
+ expect(await action.isDisabled()).toBe(true);
+ }
+ });
+ await step("Member can still add a personal connection", async () => {
+ await page.getByRole("button", { name: "Add connection", exact: true }).click();
+ const dialog = page.getByRole("dialog");
+ await dialog.waitFor();
+ expect(await dialog.getByText("Workspace", { exact: true }).count()).toBe(0);
+ });
+ await step("Member browses the catalog with disabled Add buttons", async () => {
+ await visit(page, "/integrations/browse");
+ await page.getByRole("heading", { name: "Add an integration", exact: true }).waitFor();
+ await page
+ .getByText("Requires a workspace admin to add integrations.", { exact: true })
+ .waitFor();
+ const addButtons = page.getByRole("button", { name: /^Add / });
+ await addButtons.first().waitFor();
+ for (const button of await addButtons.all())
+ expect(await button.isDisabled()).toBe(true);
+ const scratch = page.getByRole("button", {
+ name: "New OpenAPI integration from scratch",
+ exact: true,
+ });
+ expect(await scratch.isDisabled()).toBe(true);
+ const view = page.getByRole("link", { name: `View ${title}`, exact: true });
+ await view.waitFor();
+ expect(await view.isEnabled()).toBe(true);
+ });
+ await step("Member cannot add a URL with the button or Enter key", async () => {
+ const input = page.getByRole("textbox", {
+ name: "Search integrations, or paste a URL",
+ });
+ await input.fill("https://api.example.com/openapi.json");
+ expect(
+ await page.getByRole("button", { name: "Add this URL", exact: true }).isDisabled(),
+ ).toBe(true);
+ const before = page.url();
+ await input.press("Enter");
+ expect(page.url()).toBe(before);
+ });
+ for (const path of ["/integrations/add/openapi", "/integrations/add/mcp"]) {
+ await step(`Member follows ${path} and sees the admin explanation`, async () => {
+ await visit(page, path);
+ await page.getByRole("heading", { name: "An admin must add integrations" }).waitFor();
+ expect(await page.getByRole("textbox").count()).toBe(0);
+ expect(await page.getByRole("button", { name: /^Add/ }).count()).toBe(0);
+ });
+ }
+ await step("Member returns to their existing integrations", async () => {
+ await page.getByRole("link", { name: "Back to integrations" }).click();
+ await page.getByRole("heading", { name: "Integrations", exact: true }).waitFor();
+ });
+ });
+ }),
+ adminClient.openapi.removeSpec({ params: { slug } }).pipe(Effect.ignore),
+ );
+ });
diff --git a/packages/react/src/components/command-palette.tsx b/packages/react/src/components/command-palette.tsx
index 585ceed1f2..ecd148dcb2 100644
--- a/packages/react/src/components/command-palette.tsx
+++ b/packages/react/src/components/command-palette.tsx
@@ -9,6 +9,7 @@ import { IntegrationFavicon, integrationPresetIconUrl } from "./integration-favi
import { PresetIcon } from "./preset-icon";
import { integrationsOptimisticAtom } from "../api/atoms";
import { useIntegrationPlugins } from "@executor-js/sdk/client";
+import { useCanCreateWorkspaceConnections } from "../multiplayer/use-admin-nav";
import {
CommandDialog,
CommandEmpty,
@@ -34,6 +35,7 @@ export function CommandPalette(props: { open: boolean; onOpenChange: (open: bool
const integrationPlugins = useIntegrationPlugins();
const navigate = useNavigate();
const integrationsResult = useAtomValue(integrationsOptimisticAtom);
+ const canCreateIntegration = useCanCreateWorkspaceConnections();
// Toggle with ⌘K / Ctrl+K
useEffect(() => {
@@ -176,11 +178,13 @@ export function CommandPalette(props: { open: boolean; onOpenChange: (open: bool
{integrationPlugins.map((plugin) => (
goToAdd(plugin.key)}
>
Add {plugin.label}
+ {!canCreateIntegration && Admin only }
))}
@@ -193,6 +197,7 @@ export function CommandPalette(props: { open: boolean; onOpenChange: (open: bool
{presetEntries.map((e) => (
goToPreset(e.pluginKey, e.presetId, e.presetUrl)}
>
@@ -208,7 +213,9 @@ export function CommandPalette(props: { open: boolean; onOpenChange: (open: bool
}
/>
{e.presetName}
- {e.pluginLabel}
+
+ {canCreateIntegration ? e.pluginLabel : "Admin only"}
+
))}
diff --git a/packages/react/src/components/integration-creation-gate.tsx b/packages/react/src/components/integration-creation-gate.tsx
new file mode 100644
index 0000000000..afcdee7025
--- /dev/null
+++ b/packages/react/src/components/integration-creation-gate.tsx
@@ -0,0 +1,36 @@
+import type { ReactNode } from "react";
+import { Link } from "@tanstack/react-router";
+import { useAtomValue } from "@effect/atom-react";
+
+import { orgMembersAtom } from "../api/account-atoms";
+import { isAsyncResultLoading } from "../lib/async-result";
+import { useCanCreateWorkspaceConnections } from "../multiplayer/use-admin-nav";
+import { Button } from "./button";
+import { PageContainer, PageHeader } from "./page";
+import { Skeleton } from "./skeleton";
+
+/** Keep integration creation flows behind the same role gate as edit and delete. */
+export function IntegrationCreationGate({ children }: { readonly children: ReactNode }) {
+ const canCreate = useCanCreateWorkspaceConnections();
+ const members = useAtomValue(orgMembersAtom);
+ if (canCreate) return children;
+ if (isAsyncResultLoading(members)) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ Back to integrations
+
+
+ );
+}
diff --git a/packages/react/src/components/workspace-admin-hint.tsx b/packages/react/src/components/workspace-admin-hint.tsx
new file mode 100644
index 0000000000..ea497615f0
--- /dev/null
+++ b/packages/react/src/components/workspace-admin-hint.tsx
@@ -0,0 +1,29 @@
+import type { ReactNode } from "react";
+import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./tooltip";
+
+/** Explain a disabled workspace action on hover or keyboard focus. */
+export function WorkspaceAdminHint(props: {
+ readonly allowed: boolean;
+ readonly children: ReactNode;
+}) {
+ if (props.allowed) return props.children;
+ return (
+
+
+
+
+ {props.children}
+
+
+
+ Requires a workspace admin
+
+
+
+ );
+}
diff --git a/packages/react/src/multiplayer/shell.tsx b/packages/react/src/multiplayer/shell.tsx
index 1bd109ece1..2ee203b25e 100644
--- a/packages/react/src/multiplayer/shell.tsx
+++ b/packages/react/src/multiplayer/shell.tsx
@@ -6,6 +6,7 @@ import { BookOpen, Command, ExternalLink, PlusIcon } from "lucide-react";
import type { Integration } from "@executor-js/sdk/shared";
import { integrationsOptimisticAtom } from "../api/atoms";
import { trackEvent } from "../api/analytics";
+import { WorkspaceAdminHint } from "../components/workspace-admin-hint";
import { Button } from "../components/button";
import { Skeleton } from "../components/skeleton";
import { SidebarUpdateCard } from "../components/update-card";
@@ -25,6 +26,7 @@ import { CommandPalette } from "../components/command-palette";
import { Wordmark } from "../components/wordmark";
import { useClientPlugins, useIntegrationPlugins } from "@executor-js/sdk/client";
import { useAuth } from "./auth-context";
+import { useCanCreateWorkspaceConnections } from "./use-admin-nav";
// ---------------------------------------------------------------------------
// Shared multiplayer shell (cloud + self-host).
@@ -351,6 +353,7 @@ function SidebarContent(
},
) {
const plugins = useClientPlugins();
+ const canCreateIntegration = useCanCreateWorkspaceConnections();
const pluginNavItems = plugins.flatMap((plugin) =>
(plugin.pages ?? []).flatMap((page) =>
page.nav
@@ -385,17 +388,20 @@ function SidebarContent(
Integrations
-
-
-
+
+
+
+
+
diff --git a/packages/react/src/pages/integration-add.tsx b/packages/react/src/pages/integration-add.tsx
index 9691ca4cd2..f6b1f32a72 100644
--- a/packages/react/src/pages/integration-add.tsx
+++ b/packages/react/src/pages/integration-add.tsx
@@ -1,16 +1,27 @@
-import { Suspense } from "react";
+import { Suspense, type ComponentProps } from "react";
import { useAtomRefresh } from "@effect/atom-react";
import { Link, useNavigate } from "@tanstack/react-router";
import { useIntegrationPlugins } from "@executor-js/sdk/client";
import { integrationsOptimisticAtom } from "../api/atoms";
import { trackEvent } from "../api/analytics";
import { useExecutorDocumentTitle } from "../lib/document-title";
+import { IntegrationCreationGate } from "../components/integration-creation-gate";
// ---------------------------------------------------------------------------
// Page
// ---------------------------------------------------------------------------
-export function AddIntegrationPage(props: {
+/** Render an integration setup flow only when the workspace role permits creation. */
+export function AddIntegrationPage(props: ComponentProps) {
+ useExecutorDocumentTitle("Add integration");
+ return (
+
+
+
+ );
+}
+
+function AddIntegrationContent(props: {
pluginKey: string;
url?: string;
preset?: string;
@@ -20,7 +31,6 @@ export function AddIntegrationPage(props: {
authKind?: string;
specOverrides?: string;
}) {
- useExecutorDocumentTitle("Add integration");
const { pluginKey, url, preset, namespace, authHeader, authNote, authKind, specOverrides } =
props;
const navigate = useNavigate();
diff --git a/packages/react/src/pages/integration-browse.tsx b/packages/react/src/pages/integration-browse.tsx
index 83b133e28f..c63789d3ef 100644
--- a/packages/react/src/pages/integration-browse.tsx
+++ b/packages/react/src/pages/integration-browse.tsx
@@ -27,6 +27,7 @@ import {
} from "../components/integration-favicon";
import { Skeleton } from "../components/skeleton";
import { useExecutorDocumentTitle } from "../lib/document-title";
+import { useCanCreateWorkspaceConnections } from "../multiplayer/use-admin-nav";
import {
availableCatalogKinds,
catalogLogoUrl,
@@ -244,7 +245,7 @@ function RowIcon(props: { readonly src?: string; readonly alt: string }) {
);
}
-function ResultCard(props: { readonly row: Row }) {
+function ResultCard(props: { readonly row: Row; readonly canCreate: boolean }) {
const { row } = props;
return (
+ {!canCreate && (
+
+ Requires a workspace admin to add integrations.
+
+ )}
{
- if (event.key === "Enter" && isUrl) void handleDetect();
+ if (event.key === "Enter" && isUrl && canCreate) void handleDetect();
}}
placeholder="Search integrations, or paste a URL…"
aria-label="Search integrations, or paste a URL"
@@ -951,7 +960,7 @@ export function IntegrationBrowsePage() {
void handleDetect()}
- disabled={detecting || query.trim().length === 0}
+ disabled={!canCreate || detecting || query.trim().length === 0}
loading={detecting}
>
Add this URL
@@ -973,6 +982,7 @@ export function IntegrationBrowsePage() {
key={plugin.key}
type="button"
aria-label={`New ${plugin.label} integration from scratch`}
+ disabled={!canCreate}
onClick={() => {
trackEvent("integration_add_started", {
plugin_key: plugin.key,
@@ -983,7 +993,7 @@ export function IntegrationBrowsePage() {
params: { pluginKey: plugin.key },
});
}}
- className="inline-flex items-center gap-1 rounded-full border border-border px-2.5 py-1 text-xs font-medium text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
+ className="disabled:opacity-50 disabled:pointer-events-none inline-flex items-center gap-1 rounded-full border border-border px-2.5 py-1 text-xs font-medium text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
>
{plugin.label}
@@ -1020,7 +1030,7 @@ export function IntegrationBrowsePage() {
) : (
{results.map((row) => (
-
+
))}
{catalog.loadingMore
? Array.from({ length: 3 }, (_, index) => (
diff --git a/packages/react/src/pages/integration-detail.tsx b/packages/react/src/pages/integration-detail.tsx
index 5f90c2efb6..f88412b3e3 100644
--- a/packages/react/src/pages/integration-detail.tsx
+++ b/packages/react/src/pages/integration-detail.tsx
@@ -37,6 +37,7 @@ import { IntegrationEditSheet } from "../components/metadata-edit-sheet";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "../components/tabs";
import { authMethodsFromDescriptors, type AuthMethod } from "../lib/auth-placements";
import { usePolicyActions } from "../hooks/use-policy-actions";
+import { WorkspaceAdminHint } from "../components/workspace-admin-hint";
import { useCanCreateWorkspaceConnections } from "../multiplayer/use-admin-nav";
import { useIntegrationPlugins, type IntegrationAccountHandoff } from "@executor-js/sdk/client";
import { Button } from "../components/button";
@@ -139,11 +140,11 @@ export function IntegrationDetailPage(props: {
const isBuiltInIntegration = namespace === "executor" || integrationData?.kind === "built-in";
const currentTab = isBuiltInIntegration ? "tools" : activeTab;
// Integrations are workspace-owned; the server refuses catalog mutations
- // (update/remove) from non-admin members, so hide the controls for them.
+ // (update/remove) from non-admin members, so disable the controls for them.
const canMutateIntegration = useCanCreateWorkspaceConnections();
- const canEdit = canMutateIntegration && !isBuiltInIntegration && integrationData !== null;
+ const canEdit = !isBuiltInIntegration && integrationData !== null;
const canRefresh = integrationData?.canRefresh ?? false;
- const canRemove = canMutateIntegration && (integrationData?.canRemove ?? false);
+ const canRemove = integrationData?.canRemove ?? false;
const urlAccountHandoff = useMemo
(() => {
const search = new URLSearchParams(locationSearch);
// The route-validated flag and the raw `addAccount=1` are the same request;
@@ -499,9 +500,16 @@ export function IntegrationDetailPage(props: {
{!confirmDelete && canEdit && (
- setEditSheetOpen(true)}>
- Edit
-
+
+ setEditSheetOpen(true)}
+ >
+ Edit
+
+
)}
{canRefresh && (
@@ -530,20 +538,23 @@ export function IntegrationDetailPage(props: {
variant="destructive"
size="sm"
onClick={() => void handleDelete()}
- disabled={deleting}
+ disabled={deleting || !canMutateIntegration}
>
{deleting ? "Deleting..." : "Confirm Delete"}
) : (
- setConfirmDelete(true)}
- className="border-destructive/30 text-destructive hover:bg-destructive/10"
- >
- Delete
-
+
+ setConfirmDelete(true)}
+ className="border-destructive/30 text-destructive hover:bg-destructive/10"
+ >
+ Delete
+
+
))}
diff --git a/packages/react/src/pages/integrations.tsx b/packages/react/src/pages/integrations.tsx
index a08faeaa67..b12c8fc558 100644
--- a/packages/react/src/pages/integrations.tsx
+++ b/packages/react/src/pages/integrations.tsx
@@ -8,6 +8,7 @@ import { useIntegrationPlugins, type IntegrationPlugin } from "@executor-js/sdk/
import { integrationsOptimisticAtom } from "../api/atoms";
import { trackEvent } from "../api/analytics";
import { McpInstallCard } from "../components/mcp-install-card";
+import { WorkspaceAdminHint } from "../components/workspace-admin-hint";
import { Button } from "../components/button";
import { PageContainer, PageHeader } from "../components/page";
import {
@@ -32,6 +33,7 @@ import { Skeleton } from "../components/skeleton";
import { useExecutorDocumentTitle } from "../lib/document-title";
import { ErrorState } from "../components/error-state";
import { isAsyncResultLoading } from "../lib/async-result";
+import { useCanCreateWorkspaceConnections } from "../multiplayer/use-admin-nav";
const KIND_TO_PLUGIN_KEY: Record
= {
openapi: "openapi",
@@ -48,6 +50,7 @@ export function IntegrationsPage() {
useExecutorDocumentTitle("Integrations");
const integrations = useAtomValue(integrationsOptimisticAtom);
const refreshIntegrations = useAtomRefresh(integrationsOptimisticAtom);
+ const canCreate = useCanCreateWorkspaceConnections();
return (
@@ -55,15 +58,24 @@ export function IntegrationsPage() {
title="Integrations"
description="Tool providers available in this workspace."
actions={
-
- trackEvent("integration_browse_opened", { via: "header" })}
- >
-
- Add integration
-
-
+ canCreate ? (
+
+ trackEvent("integration_browse_opened", { via: "header" })}
+ >
+
+ Add integration
+
+
+ ) : (
+
+
+
+ Add integration
+
+
+ )
}
/>
@@ -83,7 +95,7 @@ export function IntegrationsPage() {
),
onSuccess: ({ value }) => {
if (value.length === 0) {
- return ;
+ return ;
}
return (
@@ -102,7 +114,7 @@ export function IntegrationsPage() {
// Empty state
// ---------------------------------------------------------------------------
-function EmptyIntegrations() {
+function EmptyIntegrations({ canCreate }: { readonly canCreate: boolean }) {
return (
@@ -110,17 +122,28 @@ function EmptyIntegrations() {
No integrations yet
- Connect an integration to start curating tools.
+ {canCreate
+ ? "Connect an integration to start curating tools."
+ : "Ask a workspace admin to add an integration."}
-
- trackEvent("integration_browse_opened", { via: "empty-state" })}
- >
-
- Add an integration
-
-
+ {canCreate ? (
+
+ trackEvent("integration_browse_opened", { via: "empty-state" })}
+ >
+
+ Add an integration
+
+
+ ) : (
+
+
+
+ Add an integration
+
+
+ )}
);
}
From 2e5aa16bedb4ba74448b3f9338754764b40519a0 Mon Sep 17 00:00:00 2001
From: Rhys Sullivan <39114868+RhysSullivan@users.noreply.github.com>
Date: Fri, 18 Sep 2026 11:52:27 -0700
Subject: [PATCH 05/11] fix(oauth): request all advertised scopes, keep sync
verdicts visible (#2056)
Scope discovery capped the request at 100 scopes. A resource that
advertises more (PostHog lists 150) got a token missing the scopes its
MCP server needs, so every new connection synced zero tools. Bound the
request by scope-string length (8 KiB) instead.
A credential-only health check then reported healthy over the
sync-stamped rejection, hiding the failure. Sync-supplied verdicts now
carry the tool_sync_failed reason and are served until a sync succeeds.
Co-authored-by: Claude Fable 5.1
---
.changeset/oauth-discovered-scope-budget.md | 5 ++
packages/core/sdk/src/executor.ts | 27 +++++++-
packages/core/sdk/src/health-check.ts | 1 +
packages/core/sdk/src/oauth-flow.test.ts | 67 +++++++++++++++++++
.../core/sdk/src/oauth-scope-union.test.ts | 45 +++++++++++--
packages/core/sdk/src/oauth-service.ts | 23 ++++++-
6 files changed, 156 insertions(+), 12 deletions(-)
create mode 100644 .changeset/oauth-discovered-scope-budget.md
diff --git a/.changeset/oauth-discovered-scope-budget.md b/.changeset/oauth-discovered-scope-budget.md
new file mode 100644
index 0000000000..c71d10c41e
--- /dev/null
+++ b/.changeset/oauth-discovered-scope-budget.md
@@ -0,0 +1,5 @@
+---
+"@executor-js/sdk": patch
+---
+
+Request every scope a resource advertises during OAuth scope discovery, bounded by an 8 KiB scope-string budget instead of a 100-scope count. Resources with many fine-grained scopes previously received a token missing the ones it needed. Health checks without a probe no longer replace a tool-sync failure verdict with "healthy".
diff --git a/packages/core/sdk/src/executor.ts b/packages/core/sdk/src/executor.ts
index cd89ab9075..551032d8da 100644
--- a/packages/core/sdk/src/executor.ts
+++ b/packages/core/sdk/src/executor.ts
@@ -3605,7 +3605,15 @@ export const createExecutor = =>
findConnectionRow(ref).pipe(
Effect.flatMap((fresh) =>
- fresh === null || oauthReauthRequiredFromProviderState(fresh.provider_state) !== null
+ fresh === null ||
+ oauthReauthRequiredFromProviderState(fresh.provider_state) !== null ||
+ // A credential verdict cannot refute a failed tool sync; only a
+ // successful sync clears that record (see `isToolSyncHealth`).
+ isToolSyncHealth(Option.getOrNull(decodeLastHealth(fresh.last_health)))
? Effect.void
: persistHealthResult(ref, fresh, result),
),
@@ -5241,6 +5253,17 @@ export const createExecutor =
+ result.status === "healthy" && previous !== null && isToolSyncHealth(previous)
+ ? previous
+ : result,
+ ),
Effect.tap((result) => persistProbeHealthResult(ref, result)),
Effect.map((result) => ({
source: "credential_only" as const,
diff --git a/packages/core/sdk/src/health-check.ts b/packages/core/sdk/src/health-check.ts
index a4d6f1cb20..9e39047d55 100644
--- a/packages/core/sdk/src/health-check.ts
+++ b/packages/core/sdk/src/health-check.ts
@@ -145,6 +145,7 @@ export type HealthCheckResult = typeof HealthCheckResult.Type;
export const toolSyncHealthDetailPrefix = "Tool sync failing";
export const isToolSyncHealth = (result: HealthCheckResult | null | undefined): boolean =>
+ result?.reason === "tool_sync_failed" ||
result?.detail?.startsWith(toolSyncHealthDetailPrefix) === true;
// ---------------------------------------------------------------------------
diff --git a/packages/core/sdk/src/oauth-flow.test.ts b/packages/core/sdk/src/oauth-flow.test.ts
index 7496ce1efd..b98d500716 100644
--- a/packages/core/sdk/src/oauth-flow.test.ts
+++ b/packages/core/sdk/src/oauth-flow.test.ts
@@ -2033,6 +2033,73 @@ describe("oauth token refresh in resolveConnectionValue", () => {
),
);
+ it.effect(
+ "checkHealth without a probe serves a sync-stamped verdict instead of burying it under healthy",
+ () =>
+ Effect.scoped(
+ Effect.gen(function* () {
+ const server = yield* serveOAuthTestServer({ scopes: ["read"] });
+ const { executor, config } = yield* makeTestWorkspaceHarness({ plugins });
+ yield* executor.acme.seed();
+
+ yield* executor.oauth.createClient({
+ owner: "org",
+ slug: CLIENT,
+ authorizationUrl: server.authorizationEndpoint,
+ tokenUrl: server.tokenEndpoint,
+ grant: "authorization_code",
+ clientId: "test-client",
+ clientSecret: "test-secret",
+ resource: server.mcpResourceUrl,
+ });
+
+ const started = yield* executor.oauth.start({
+ owner: "org",
+ client: CLIENT,
+ clientOwner: "org",
+ name: ConnectionName.make("main"),
+ integration: INTEG,
+ template: TEMPLATE,
+ });
+ expect(started.status).toBe("redirect");
+ if (started.status !== "redirect") return;
+ const callback = yield* server.completeAuthorizationCodeFlow({
+ authorizationUrl: started.authorizationUrl,
+ });
+ yield* executor.oauth.complete({ state: started.state, code: callback.code });
+
+ // Tool sync found the upstream rejecting the freshly minted token
+ // (e.g. an MCP discovery handshake answering 401) and stamped it.
+ // The token itself still resolves, so a credential-only check would
+ // otherwise report healthy and hide a connection that has no tools.
+ const stamped = {
+ status: "expired",
+ checkedAt: Date.now(),
+ detail: "MCP OAuth reauthorization required",
+ reason: "tool_sync_failed",
+ };
+ yield* Effect.promise(() =>
+ config.db.updateMany("connection", {
+ where: (b) => b("name", "=", "main"),
+ set: { last_health: stamped },
+ }),
+ );
+
+ const result = yield* executor.connections.checkHealth({
+ owner: "org",
+ integration: INTEG,
+ name: ConnectionName.make("main"),
+ });
+ expect(result).toMatchObject(stamped);
+
+ const row = yield* Effect.promise(() =>
+ config.db.findFirst("connection", { where: (b) => b("name", "=", "main") }),
+ );
+ expect(row?.last_health).toMatchObject(stamped);
+ }),
+ ),
+ );
+
it.effect("records missing authorization-code scopes without blocking the connection", () =>
Effect.scoped(
Effect.gen(function* () {
diff --git a/packages/core/sdk/src/oauth-scope-union.test.ts b/packages/core/sdk/src/oauth-scope-union.test.ts
index 47b92d16e0..97dbd3dce5 100644
--- a/packages/core/sdk/src/oauth-scope-union.test.ts
+++ b/packages/core/sdk/src/oauth-scope-union.test.ts
@@ -713,13 +713,42 @@ describe("oauth.start integration-driven scopes", () => {
),
);
- it.effect("(j) caps server-advertised resource scopes so the authorize URL stays bounded", () =>
+ it.effect("(j) requests every advertised scope of a large but realistic resource list", () =>
Effect.scoped(
Effect.gen(function* () {
- // A hostile/buggy server advertises far more scopes than any real
- // template. Discovery caps the request at 100 so the authorize URL
- // cannot be blown up.
- const manyScopes = Array.from({ length: 200 }, (_, i) => `scope:${i}`);
+ // A fine-grained resource can legitimately advertise well over a
+ // hundred scopes (PostHog lists 150). Dropping any of them mints a
+ // token the resource rejects, so the whole list must be requested.
+ const manyScopes = Array.from(
+ { length: 150 },
+ (_, i) => `resource_${i}:${i % 2 === 0 ? "read" : "write"}`,
+ );
+ const server = yield* serveMetadataServer({ prm: { scopesSupported: manyScopes } });
+ const executor = yield* setupMcpScopeClient(server);
+
+ const started = yield* executor.oauth.start({
+ owner: "org",
+ client: CLIENT,
+ clientOwner: "org",
+ name: ConnectionName.make("main"),
+ integration: INTEG,
+ template: TEMPLATE,
+ });
+ expect(started.status).toBe("redirect");
+ if (started.status !== "redirect") return;
+
+ expect(scopesFromAuthorizeUrl(started.authorizationUrl)).toEqual(manyScopes);
+ }),
+ ),
+ );
+
+ it.effect("(j2) caps server-advertised resource scopes so the authorize URL stays bounded", () =>
+ Effect.scoped(
+ Effect.gen(function* () {
+ // A hostile/buggy server advertises an absurd list. Discovery keeps
+ // the longest leading prefix whose joined `scope` value fits the
+ // 8 KiB budget so the authorize URL cannot be blown up.
+ const manyScopes = Array.from({ length: 2000 }, (_, i) => `scope:${i}`);
const server = yield* serveMetadataServer({ prm: { scopesSupported: manyScopes } });
const executor = yield* setupMcpScopeClient(server);
@@ -735,8 +764,10 @@ describe("oauth.start integration-driven scopes", () => {
if (started.status !== "redirect") return;
const requested = scopesFromAuthorizeUrl(started.authorizationUrl);
- expect(requested.length).toBe(100);
- expect(requested).toEqual(manyScopes.slice(0, 100));
+ expect(requested.length).toBeLessThan(manyScopes.length);
+ expect(requested).toEqual(manyScopes.slice(0, requested.length));
+ expect(requested.join(" ").length).toBeLessThanOrEqual(8192);
+ expect([...requested, manyScopes[requested.length]].join(" ").length).toBeGreaterThan(8192);
}),
),
);
diff --git a/packages/core/sdk/src/oauth-service.ts b/packages/core/sdk/src/oauth-service.ts
index a98eeda039..7d8d7d46ea 100644
--- a/packages/core/sdk/src/oauth-service.ts
+++ b/packages/core/sdk/src/oauth-service.ts
@@ -782,9 +782,26 @@ export const makeOAuthService = (deps: OAuthServiceDeps): OAuthService => {
// Caps on server-controlled discovery input — a hostile or buggy server must
// not be able to hang `oauth.start` or overflow the authorize URL.
const MAX_DISCOVERY_AUTH_SERVERS = 3; // AS-failover lists are tiny in practice
- const MAX_DISCOVERED_SCOPES = 100; // far beyond any realistic authorization template
- const capScopes = (scopes: readonly string[]): readonly string[] =>
- dedupeScopes(scopes).slice(0, MAX_DISCOVERED_SCOPES);
+ // The cap is on the encoded `scope` parameter's length, not the scope
+ // count: the URL is what overflows, and a real resource can legitimately
+ // advertise well over a hundred fine-grained scopes (PostHog lists 150).
+ // Dropping any advertised scope silently mints a token the resource then
+ // rejects, so the budget is generous — 8 KiB leaves room for the rest of the
+ // authorize URL under the common 8-16 KiB request-line limits — and only an
+ // absurd list is truncated.
+ const MAX_DISCOVERED_SCOPE_CHARS = 8192;
+ const capScopes = (scopes: readonly string[]): readonly string[] => {
+ const unique = dedupeScopes(scopes);
+ let length = 0;
+ let count = 0;
+ for (const scope of unique) {
+ const next = length + scope.length + (count > 0 ? 1 : 0);
+ if (next > MAX_DISCOVERED_SCOPE_CHARS) break;
+ length = next;
+ count += 1;
+ }
+ return unique.slice(0, count);
+ };
// Bound a whole discovery sequence (PRM + up to MAX_DISCOVERY_AUTH_SERVERS AS
// fetches, each with its own request timeout). 30s is larger than a single
From 25a490d3dc715c1d3f26e53cfe65b96e7b6070ae Mon Sep 17 00:00:00 2001
From: Rhys Sullivan <39114868+RhysSullivan@users.noreply.github.com>
Date: Fri, 18 Sep 2026 12:09:15 -0700
Subject: [PATCH 06/11] Protect deployment database connections (#2057)
---
.github/scripts/check-database-capacity.py | 59 ++++++++++++
.github/workflows/database-capacity.yml | 27 ++++++
apps/cloud/docs/database-connections.md | 43 +++++++++
apps/cloud/scripts/database-connection.ts | 56 +++++++++++
.../scripts/ensure-workos-mirror-ready.ts | 5 +-
apps/cloud/scripts/migrate.ts | 5 +-
.../src/db/deployment-connection.test.ts | 93 +++++++++++++++++++
7 files changed, 286 insertions(+), 2 deletions(-)
create mode 100644 .github/scripts/check-database-capacity.py
create mode 100644 .github/workflows/database-capacity.yml
create mode 100644 apps/cloud/docs/database-connections.md
create mode 100644 apps/cloud/scripts/database-connection.ts
create mode 100644 apps/cloud/src/db/deployment-connection.test.ts
diff --git a/.github/scripts/check-database-capacity.py b/.github/scripts/check-database-capacity.py
new file mode 100644
index 0000000000..08ea5b6fef
--- /dev/null
+++ b/.github/scripts/check-database-capacity.py
@@ -0,0 +1,59 @@
+"""Read aggregate connection capacity with libpq; never print credentials or SQL data."""
+
+import json
+import os
+import subprocess
+import sys
+from urllib.parse import unquote, urlparse
+
+
+def main() -> int:
+ try:
+ url = urlparse(os.environ["DATABASE_URL"])
+ if url.scheme not in ("postgres", "postgresql") or not url.hostname:
+ raise ValueError("Invalid database URL")
+ if url.hostname.endswith(".psdb.cloud") and url.port not in (None, 5432):
+ raise ValueError("Capacity checks require the direct endpoint")
+ env = {
+ **os.environ,
+ "PGHOST": url.hostname,
+ "PGPORT": str(url.port or 5432),
+ "PGUSER": unquote(url.username or ""),
+ "PGPASSWORD": unquote(url.password or ""),
+ "PGDATABASE": unquote(url.path.removeprefix("/")),
+ "PGSSLMODE": "require",
+ "PGCONNECT_TIMEOUT": "10",
+ "PGAPPNAME": "database-capacity-check",
+ "PGOPTIONS": "-c default_transaction_read_only=on -c statement_timeout=10000",
+ }
+ result = subprocess.run(
+ ["psql", "-X", "-A", "-t", "-v", "ON_ERROR_STOP=1", "-c", """
+ SELECT json_build_object(
+ 'limit', current_setting('max_connections')::int,
+ 'reserved', current_setting('superuser_reserved_connections')::int
+ + current_setting('reserved_connections')::int,
+ 'used', count(*)::int
+ ) FROM pg_stat_activity WHERE backend_type = 'client backend'
+ """],
+ env=env,
+ capture_output=True,
+ text=True,
+ timeout=25,
+ check=True,
+ )
+ capacity = json.loads(result.stdout)
+ if any(type(capacity[key]) is not int for key in ("limit", "reserved", "used")):
+ raise ValueError("Invalid capacity response")
+ free = capacity["limit"] - capacity["reserved"] - capacity["used"]
+ print(json.dumps({**capacity, "ordinary_free": free, "minimum_free": 10}))
+ if free < 10:
+ print("::error::Database connection headroom is below 10 slots. Inspect direct clients and the PgBouncer budget.")
+ return 1
+ return 0
+ except (KeyError, ValueError, OSError, subprocess.SubprocessError):
+ print("::error::Database capacity check failed. Check direct endpoint access and provider health.")
+ return 1
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/.github/workflows/database-capacity.yml b/.github/workflows/database-capacity.yml
new file mode 100644
index 0000000000..d0610eff43
--- /dev/null
+++ b/.github/workflows/database-capacity.yml
@@ -0,0 +1,27 @@
+name: Database capacity
+
+on:
+ schedule:
+ - cron: "2-57/5 * * * *"
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+concurrency:
+ group: database-capacity
+ cancel-in-progress: false
+
+jobs:
+ check:
+ runs-on: ubuntu-24.04
+ timeout-minutes: 2
+ environment: production
+ steps:
+ - uses: actions/checkout@v4
+ # psql is supplied by the Ubuntu runner image. A failed check uses the
+ # repository's Actions failure notifications; no customer data is logged.
+ - name: Check ordinary connection headroom
+ run: python3 .github/scripts/check-database-capacity.py
+ env:
+ DATABASE_URL: ${{ secrets.DATABASE_URL }}
diff --git a/apps/cloud/docs/database-connections.md b/apps/cloud/docs/database-connections.md
new file mode 100644
index 0000000000..12c677b775
--- /dev/null
+++ b/apps/cloud/docs/database-connections.md
@@ -0,0 +1,43 @@
+# Production database connections
+
+Application traffic uses Hyperdrive, then PlanetScale's local transaction-mode
+PgBouncer on port 6432. Deployment scripts use the direct endpoint on port 5432.
+Code migrations hold session advisory locks, so they must bypass transaction pooling.
+
+The connection budget is:
+
+| Setting | Value |
+| ----------------------------------------- | --------------- |
+| PostgreSQL max_connections | 50 |
+| PostgreSQL superuser_reserved_connections | 3 |
+| Local PgBouncer processes | 1 |
+| PgBouncer default_pool_size | 12 |
+| PgBouncer max_db_connections | 12 |
+| PgBouncer max_client_conn | 400 |
+| PgBouncer max_prepared_statements | 200 |
+| Hyperdrive origin connection limit | 12 (soft limit) |
+
+Hyperdrive's origin limit is advisory. PgBouncer's database limit enforces the
+backend budget across users of one database. The cap is per PgBouncer process:
+adding processes, databases, direct clients, or other poolers requires a new
+aggregate budget. Keep capacity for provider sessions, deploys and administration.
+Prepared statements require protocol-level support to remain enabled in PgBouncer.
+
+The migration and membership-readiness scripts retry only the initial `SELECT 1`
+when PostgreSQL returns SQLSTATE `53300`. They make at most seven attempts, with
+ten seconds between attempts and a ten-second connection timeout. They never
+retry migration bodies or readiness mutations. Other errors fail immediately.
+
+The Database capacity workflow checks direct access and aggregate connection
+headroom every five minutes. It fails when fewer than ten ordinary slots remain.
+Counts include the monitor and conservatively count privileged client sessions
+against ordinary capacity. GitHub schedule delays and notification preferences
+apply; this is not a real-time paging service. Check PlanetScale CPU and PgBouncer
+waiting clients alongside Cloudflare query errors and latency during load spikes.
+
+For a routing change, first account for overlapping old and new pools. Verify
+the active PostgreSQL limit and applied pool settings before changing Hyperdrive.
+Afterward, check an authenticated application page, direct database access,
+backend counts, and provider errors. Roll back by restoring the prior Hyperdrive
+origin port only while there is capacity for both pools. Do not kill idle sessions
+as routine maintenance: clients can reconnect and consume the slots again.
diff --git a/apps/cloud/scripts/database-connection.ts b/apps/cloud/scripts/database-connection.ts
new file mode 100644
index 0000000000..d3f725b842
--- /dev/null
+++ b/apps/cloud/scripts/database-connection.ts
@@ -0,0 +1,56 @@
+/* oxlint-disable executor/no-error-constructor, executor/no-try-catch-or-throw -- boundary: deployment CLI connection acquisition */
+
+import { setTimeout } from "node:timers/promises";
+
+const MAX_ATTEMPTS = 7;
+const RETRY_DELAY_MS = 10_000;
+
+/**
+ * Validate the deploy transport without logging credentials. PlanetScale schema
+ * migrations use the direct endpoint because code migrations hold session locks.
+ */
+export const directDatabaseUrl = (value: string): string => {
+ let url: URL;
+ try {
+ url = new URL(value);
+ } catch {
+ throw new Error("DATABASE_URL must be a valid PostgreSQL URL");
+ }
+ if (url.protocol !== "postgres:" && url.protocol !== "postgresql:") {
+ throw new Error("DATABASE_URL must use the postgres or postgresql protocol");
+ }
+ if (url.hostname.endsWith(".psdb.cloud") && url.port !== "" && url.port !== "5432") {
+ throw new Error("PlanetScale deploy scripts require the direct endpoint on port 5432");
+ }
+ return value;
+};
+
+/**
+ * Open the CLI's single connection before starting work. Retry only PostgreSQL
+ * admission failures (53300), at most six times with ten seconds between tries.
+ * The caller must set connect_timeout and close the client on every exit.
+ * Migration and readiness mutations remain outside this retry boundary.
+ */
+export const waitForDatabaseConnection = async (
+ sql: { readonly unsafe: (query: string) => PromiseLike },
+ options: {
+ readonly log: (message: string) => void;
+ readonly sleep?: (milliseconds: number) => Promise;
+ },
+): Promise => {
+ const sleep = options.sleep ?? ((milliseconds: number) => setTimeout(milliseconds));
+ for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt += 1) {
+ try {
+ await sql.unsafe("SELECT 1");
+ return;
+ } catch (cause) {
+ const isCapacityError =
+ typeof cause === "object" && cause !== null && "code" in cause && cause.code === "53300";
+ if (!isCapacityError || attempt === MAX_ATTEMPTS) throw cause;
+ options.log(
+ `Database connection capacity is full (53300). Retrying connection ${attempt}/${MAX_ATTEMPTS - 1} in 10s; no work has started.`,
+ );
+ await sleep(RETRY_DELAY_MS);
+ }
+ }
+};
diff --git a/apps/cloud/scripts/ensure-workos-mirror-ready.ts b/apps/cloud/scripts/ensure-workos-mirror-ready.ts
index d10b826d3e..30794097e0 100644
--- a/apps/cloud/scripts/ensure-workos-mirror-ready.ts
+++ b/apps/cloud/scripts/ensure-workos-mirror-ready.ts
@@ -36,6 +36,7 @@ import { fileURLToPath } from "node:url";
import { drizzle } from "drizzle-orm/postgres-js";
import postgres from "postgres";
+import { directDatabaseUrl, waitForDatabaseConnection } from "./database-connection";
import {
MirrorReadinessState,
@@ -56,9 +57,10 @@ if (!connectionString) {
const usesLocalDatabase =
connectionString.includes("127.0.0.1") || connectionString.includes("localhost");
-const sql = postgres(connectionString, {
+const sql = postgres(directDatabaseUrl(connectionString), {
max: 1,
prepare: false,
+ connect_timeout: 10,
...(usesLocalDatabase ? {} : { ssl: "require" as const }),
});
const db = drizzle(sql);
@@ -84,6 +86,7 @@ const runScript = (what: string, script: string) => {
};
try {
+ await waitForDatabaseConnection(sql, { log });
let state = await readiness();
log(describeMirrorReadiness(state));
diff --git a/apps/cloud/scripts/migrate.ts b/apps/cloud/scripts/migrate.ts
index 9466a049b5..f610f3df9c 100644
--- a/apps/cloud/scripts/migrate.ts
+++ b/apps/cloud/scripts/migrate.ts
@@ -8,6 +8,7 @@ import { migrate as migrateDrizzle } from "drizzle-orm/postgres-js/migrator";
import postgres from "postgres";
import { cloudCodeMigrations, runCodeMigrations } from "./code-migrations/index";
+import { directDatabaseUrl, waitForDatabaseConnection } from "./database-connection";
const __dirname = dirname(fileURLToPath(import.meta.url));
const MIGRATIONS_FOLDER = resolve(__dirname, "../drizzle");
@@ -41,13 +42,15 @@ if (!connectionString) {
const usesLocalDatabase =
connectionString.includes("127.0.0.1") || connectionString.includes("localhost");
-const sql = postgres(connectionString, {
+const sql = postgres(directDatabaseUrl(connectionString), {
max: 1,
prepare: false,
+ connect_timeout: 10,
...(usesLocalDatabase ? {} : { ssl: "require" as const }),
});
try {
+ await waitForDatabaseConnection(sql, { log: console.log });
if (!codeOnly) {
if (dryRun) {
console.log("[schema-migrate] dry run: Drizzle SQL migrations are not applied");
diff --git a/apps/cloud/src/db/deployment-connection.test.ts b/apps/cloud/src/db/deployment-connection.test.ts
new file mode 100644
index 0000000000..015b88db81
--- /dev/null
+++ b/apps/cloud/src/db/deployment-connection.test.ts
@@ -0,0 +1,93 @@
+/* oxlint-disable executor/no-promise-reject -- boundary: simulate the Postgres.js driver's rejected promises */
+
+import { describe, expect, it } from "@effect/vitest";
+import { Effect } from "effect";
+
+import { directDatabaseUrl, waitForDatabaseConnection } from "../../scripts/database-connection";
+
+describe("deployment database connection", () => {
+ it.effect("waits for admission before allowing deployment work", () =>
+ Effect.promise(async () => {
+ let remainingFailures = 2;
+ const waits: number[] = [];
+ const logs: string[] = [];
+ const queries: string[] = [];
+ await waitForDatabaseConnection(
+ {
+ unsafe: (query) => {
+ queries.push(query);
+ return remainingFailures-- > 0
+ ? Promise.reject({ code: "53300", detail: "private connection data" })
+ : Promise.resolve([]);
+ },
+ },
+ {
+ log: (line) => logs.push(line),
+ sleep: async (ms) => {
+ waits.push(ms);
+ },
+ },
+ );
+ expect(queries).toEqual(["SELECT 1", "SELECT 1", "SELECT 1"]);
+ expect(waits).toEqual([10_000, 10_000]);
+ expect(logs).toHaveLength(2);
+ expect(logs.join()).not.toContain("private connection data");
+ }),
+ );
+
+ it.effect("fails after the bounded admission budget", () =>
+ Effect.promise(async () => {
+ const failure = { code: "53300" };
+ let attempts = 0;
+ const waits: number[] = [];
+ await expect(
+ waitForDatabaseConnection(
+ {
+ unsafe: () => {
+ attempts += 1;
+ return Promise.reject(failure);
+ },
+ },
+ {
+ log: () => {},
+ sleep: async (ms) => {
+ waits.push(ms);
+ },
+ },
+ ),
+ ).rejects.toBe(failure);
+ expect(attempts).toBe(7);
+ expect(waits).toEqual(Array(6).fill(10_000));
+ }),
+ );
+
+ it.effect("fails immediately for authentication, transport and SQL errors", () =>
+ Effect.promise(async () => {
+ for (const code of ["28P01", "CONNECT_TIMEOUT", "CONNECTION_CLOSED", "42601", "40001"]) {
+ const failure = { code };
+ const waits: number[] = [];
+ await expect(
+ waitForDatabaseConnection(
+ { unsafe: () => Promise.reject(failure) },
+ {
+ log: () => {},
+ sleep: async (ms) => {
+ waits.push(ms);
+ },
+ },
+ ),
+ ).rejects.toBe(failure);
+ expect(waits).toEqual([]);
+ }
+ }),
+ );
+
+ it("keeps PlanetScale deployment traffic on the direct endpoint", () => {
+ const direct = "postgres://example:secret@region.pg.psdb.cloud:5432/database";
+ expect(directDatabaseUrl(direct)).toBe(direct);
+ expect(directDatabaseUrl("postgres://localhost:25432/postgres")).toContain(":25432");
+ expect(() => directDatabaseUrl(direct.replace(":5432", ":6432"))).toThrow("direct endpoint");
+ expect(() => directDatabaseUrl("invalid-secret")).toThrow("valid PostgreSQL URL");
+ expect(() => directDatabaseUrl("https://localhost/database")).toThrow("protocol");
+ });
+});
From 4a08d8db6f74612e07f17494e222ff038d8af944 Mon Sep 17 00:00:00 2001
From: Rhys Sullivan <39114868+RhysSullivan@users.noreply.github.com>
Date: Fri, 18 Sep 2026 12:15:28 -0700
Subject: [PATCH 07/11] Scope toolkit tool reads and stop gating reads on
TTL-expired catalogs (#2061)
---
.changeset/toolkit-list-scope.md | 6 +
packages/core/sdk/src/connections.test.ts | 130 ++++++
packages/core/sdk/src/executor.ts | 371 +++++++++++-------
packages/core/sdk/src/index.ts | 4 +
packages/core/sdk/src/plugin.ts | 27 +-
packages/core/sdk/src/policies.test.ts | 145 +++++++
packages/core/sdk/src/policies.ts | 43 ++
.../plugins/mcp/src/sdk/catalog-sync.test.ts | 20 +-
packages/plugins/toolkits/src/server.test.ts | 44 +++
packages/plugins/toolkits/src/server.ts | 105 +++--
10 files changed, 706 insertions(+), 189 deletions(-)
create mode 100644 .changeset/toolkit-list-scope.md
diff --git a/.changeset/toolkit-list-scope.md b/.changeset/toolkit-list-scope.md
new file mode 100644
index 0000000000..2af6981f88
--- /dev/null
+++ b/.changeset/toolkit-list-scope.md
@@ -0,0 +1,6 @@
+---
+"@executor-js/sdk": patch
+"@executor-js/plugin-toolkits": patch
+---
+
+Toolkit sessions no longer walk the whole workspace catalog on connect, search, or describe: the toolkit's access patterns narrow the tool rows core reads. Tools reads no longer wait on re-listing catalogs that are only older than the freshness TTL; those rebuild in the background while the read answers from the persisted rows. Stale-marked and config-revised catalogs still gate the read within the grace budget.
diff --git a/packages/core/sdk/src/connections.test.ts b/packages/core/sdk/src/connections.test.ts
index 51c7f8d110..1fd653c093 100644
--- a/packages/core/sdk/src/connections.test.ts
+++ b/packages/core/sdk/src/connections.test.ts
@@ -10,6 +10,7 @@ import {
Option,
Predicate,
Result,
+ Schedule,
Schema,
Tracer,
} from "effect";
@@ -1912,6 +1913,135 @@ describe("tool catalog sync safety", () => {
),
);
+ // Live clock: the poll below waits on a detached rebuild fiber, not on the
+ // test clock.
+ it.live("a time-expired catalog answers from persisted rows and rebuilds in the background", () =>
+ Effect.scoped(
+ Effect.gen(function* () {
+ const listingStarted = yield* Deferred.make();
+ const releaseListing = yield* Deferred.make();
+ let resolutions = 0;
+ const remotePlugin = definePlugin(() => ({
+ id: "remote" as const,
+ credentialProviders: [memoryProvider()],
+ storage: () => ({}),
+ remoteToolCatalog: true,
+ resolveTools: () =>
+ Effect.gen(function* () {
+ resolutions += 1;
+ if (resolutions === 1) {
+ return { tools: [{ name: ToolName.make("deploy"), description: "deploy" }] };
+ }
+ yield* Deferred.succeed(listingStarted, undefined);
+ yield* Deferred.await(releaseListing);
+ return {
+ tools: [
+ { name: ToolName.make("deploy"), description: "deploy" },
+ { name: ToolName.make("list"), description: "list" },
+ ],
+ };
+ }),
+ invokeTool: ({ toolRow }) => Effect.succeed({ ran: toolRow.name }),
+ extension: (ctx) => ({
+ seed: () =>
+ ctx.core.integrations.register({ slug: INTEG, description: "Vercel", config: {} }),
+ }),
+ }))();
+ // TTL 0: every catalog is time-expired on every read.
+ const config = {
+ ...makeTestConfig({ plugins: [remotePlugin] as const }),
+ toolsSyncTtlMs: 0,
+ };
+ const executor = yield* createExecutor(config);
+ yield* executor.remote.seed();
+ yield* executor.connections.create({
+ owner: "org",
+ name: ConnectionName.make("main"),
+ integration: INTEG,
+ template: TEMPLATE,
+ value: "secret-token",
+ });
+
+ // Let the clock move past the stamp `create` wrote, so the catalog is
+ // older than the zero TTL on the read below.
+ yield* Effect.sleep("5 millis");
+
+ // The upstream listing is held open. A read that waited on it would
+ // pay the full grace budget; this one must answer at once from the
+ // persisted catalog.
+ const startedAt = Date.now();
+ const stale = yield* executor.tools.list({ integration: INTEG });
+ expect(Date.now() - startedAt).toBeLessThan(1000);
+ expect(stale.map((tool) => String(tool.name))).toEqual(["deploy"]);
+ yield* Deferred.await(listingStarted);
+
+ // Once the background rebuild lands, a later read observes it.
+ yield* Deferred.succeed(releaseListing, undefined);
+ const converged = yield* executor.tools.list({ integration: INTEG }).pipe(
+ Effect.map((tools) => tools.map((tool) => String(tool.name)).sort()),
+ Effect.repeat({
+ until: (names) => names.length === 2,
+ schedule: Schedule.spaced("10 millis"),
+ }),
+ Effect.timeout("5 seconds"),
+ );
+ expect(converged).toEqual(["deploy", "list"]);
+ }),
+ ),
+ );
+
+ it.effect("a stale-marked catalog still gates the read within the grace budget", () =>
+ Effect.scoped(
+ Effect.gen(function* () {
+ let resolutions = 0;
+ const remotePlugin = definePlugin(() => ({
+ id: "remote" as const,
+ credentialProviders: [memoryProvider()],
+ storage: () => ({}),
+ remoteToolCatalog: true,
+ resolveTools: () =>
+ Effect.sync(() => {
+ resolutions += 1;
+ return {
+ tools:
+ resolutions === 1
+ ? [{ name: ToolName.make("deploy"), description: "deploy" }]
+ : [
+ { name: ToolName.make("deploy"), description: "deploy" },
+ { name: ToolName.make("list"), description: "list" },
+ ],
+ };
+ }),
+ invokeTool: ({ toolRow }) => Effect.succeed({ ran: toolRow.name }),
+ extension: (ctx) => ({
+ seed: () =>
+ ctx.core.integrations.register({ slug: INTEG, description: "Vercel", config: {} }),
+ }),
+ }))();
+ const config = makeTestConfig({ plugins: [remotePlugin] as const });
+ const executor = yield* createExecutor(config);
+ yield* executor.remote.seed();
+ yield* executor.connections.create({
+ owner: "org",
+ name: ConnectionName.make("main"),
+ integration: INTEG,
+ template: TEMPLATE,
+ value: "secret-token",
+ });
+ // Stale-marked (an upstream said the catalog changed): the very next
+ // read reflects the rebuild.
+ yield* Effect.promise(() =>
+ config.db.updateMany("connection", {
+ where: (b) => b.and(b("integration", "=", String(INTEG)), b("name", "=", "main")),
+ set: { tools_synced_at: null },
+ }),
+ );
+ const tools = yield* executor.tools.list({ integration: INTEG });
+ expect(tools.map((tool) => String(tool.name)).sort()).toEqual(["deploy", "list"]);
+ }),
+ ),
+ );
+
it.effect(
"background sync preserves a nonzero remote catalog when a plugin returns authoritative empty",
() =>
diff --git a/packages/core/sdk/src/executor.ts b/packages/core/sdk/src/executor.ts
index 551032d8da..fdbc9b7671 100644
--- a/packages/core/sdk/src/executor.ts
+++ b/packages/core/sdk/src/executor.ts
@@ -154,6 +154,7 @@ import {
import type { FirstPartyOAuthClientConfig } from "./oauth-client";
import {
comparePolicyRow,
+ isUnboundedDynamicToolScope,
isValidPattern,
matchPattern,
positionForNewPattern,
@@ -180,6 +181,7 @@ import type {
StaticIntegrationDecl,
StaticToolDecl,
StorageDeps,
+ PreparedToolPolicy,
ToolPolicyProvider,
ToolPolicyProviderRule,
ToolInvocationCredential,
@@ -3222,7 +3224,10 @@ export const createExecutor = !tool.static).map((tool) => String(tool.integration)),
);
@@ -4767,7 +4772,10 @@ export const createExecutor = !tool.static)
@@ -5422,10 +5430,8 @@ export const createExecutor = EffectivePolicy;
+ readonly resolve: PreparedToolPolicy["resolve"];
+ readonly dynamicScope: PreparedToolPolicy["dynamicScope"];
};
const compareProviderPolicyRule = (
@@ -5466,9 +5472,10 @@ export const createExecutor = ({
+ Effect.map((prepared) => ({
kind: "prepared" as const,
- resolve,
+ resolve: prepared.resolve,
+ dynamicScope: prepared.dynamicScope,
})),
)
: activeToolPolicyProvider.resolve
@@ -5544,116 +5551,137 @@ export const createExecutor = [row.slug, row] as const));
- // The TTL only matters when a loaded plugin actually lists a live remote
- // catalog; otherwise skip it so age alone never widens the stale query.
- const anyRemoteCatalog = Array.from(runtimes.values()).some(
- (runtime) => runtime.plugin.remoteToolCatalog === true,
- );
- const cutoff =
- toolsSyncTtlMs == null || !anyRemoteCatalog ? null : Date.now() - toolsSyncTtlMs;
-
- // Bound the scan to potentially-stale rows: stale-marked (NULL stamp) or
- // synced before the latest instant any trigger could fire at (the TTL
- // cutoff / the newest config revision). Per-row trigger checks below
- // re-verify against each row's own integration; in steady state this
- // query returns nothing and the read pays one indexed lookup.
- const latestRevision = integrations.reduce(
- (max, row) =>
- row.config_revised_at == null
- ? max
- : Math.max(max ?? Number(row.config_revised_at), Number(row.config_revised_at)),
- null,
- );
- const staleBefore =
- cutoff === null && latestRevision === null
- ? null
- : Math.max(cutoff ?? Number.MIN_SAFE_INTEGER, latestRevision ?? Number.MIN_SAFE_INTEGER);
-
- const connections = yield* core.findMany("connection", {
- where: (b: AnyCb) =>
- staleBefore === null
- ? b.isNull("tools_synced_at")
- : b.or(b.isNull("tools_synced_at"), b("tools_synced_at", "<", staleBefore)),
- });
- // Each rebuild is an independent upstream listing, so they run together
- // rather than one after another: a host with many stale remote-catalog
- // connections otherwise pays the sum of every server's latency on the
- // read that trips the TTL. Only the listings overlap — `persistCatalog`
- // keeps the catalog writes in a single-file queue, so this fan-out never
- // opens two transactions on a one-connection database.
- const rebuilds: Effect.Effect[] = [];
- for (const connection of connections) {
- const integrationRow = integrationBySlug.get(connection.integration);
- if (!integrationRow) continue;
- const runtime = runtimes.get(integrationRow.plugin_id);
- // Only re-produce catalogs this executor can actually re-list —
- // rebuilding under an unloaded plugin would clear a working catalog.
- // (A loaded plugin without `resolveTools` still flows through:
- // `produceConnectionTools` runs its clear-and-stamp cleanup path.)
- if (!runtime) continue;
-
- const syncedAt =
- connection.tools_synced_at == null ? null : Number(connection.tools_synced_at);
- const revisedTime =
- integrationRow.config_revised_at == null
+ const syncStaleConnectionTools = (mode: "converge" | "bounded") =>
+ Effect.gen(function* () {
+ // The platform view can never persist a rebuilt catalog (writes are
+ // denied at the storage boundary), so attempting the sync would only
+ // fire upstream `resolveTools` calls whose results are thrown away —
+ // network side effects on a read-only credential. Skip it entirely:
+ // read-only-ness of the platform read path is a stated invariant here,
+ // not an accident of the best-effort catch below.
+ if (config.platformView === true) return;
+ const integrations = yield* core.findMany("integration", {});
+ if (integrations.length === 0) return;
+ const integrationBySlug = new Map(integrations.map((row) => [row.slug, row] as const));
+ // The TTL only matters when a loaded plugin actually lists a live remote
+ // catalog; otherwise skip it so age alone never widens the stale query.
+ const anyRemoteCatalog = Array.from(runtimes.values()).some(
+ (runtime) => runtime.plugin.remoteToolCatalog === true,
+ );
+ const cutoff =
+ toolsSyncTtlMs == null || !anyRemoteCatalog ? null : Date.now() - toolsSyncTtlMs;
+
+ // Bound the scan to potentially-stale rows: stale-marked (NULL stamp) or
+ // synced before the latest instant any trigger could fire at (the TTL
+ // cutoff / the newest config revision). Per-row trigger checks below
+ // re-verify against each row's own integration; in steady state this
+ // query returns nothing and the read pays one indexed lookup.
+ const latestRevision = integrations.reduce(
+ (max, row) =>
+ row.config_revised_at == null
+ ? max
+ : Math.max(max ?? Number(row.config_revised_at), Number(row.config_revised_at)),
+ null,
+ );
+ const staleBefore =
+ cutoff === null && latestRevision === null
? null
- : Number(integrationRow.config_revised_at);
-
- const staleMarked = syncedAt === null;
- const configRevised = revisedTime !== null && (syncedAt ?? 0) < revisedTime;
- const expired =
- cutoff !== null &&
- runtime.plugin.remoteToolCatalog === true &&
- syncedAt !== null &&
- syncedAt < cutoff;
- if (!staleMarked && !configRevised && !expired) continue;
+ : Math.max(
+ cutoff ?? Number.MIN_SAFE_INTEGER,
+ latestRevision ?? Number.MIN_SAFE_INTEGER,
+ );
- rebuilds.push(
- produceConnectionTools(
- integrationRow,
- {
- owner: connection.owner as Owner,
- integration: IntegrationSlug.make(connection.integration),
- name: ConnectionName.make(connection.name),
- },
- "background",
- ).pipe(
- // Best-effort, but never silent: the read still succeeds on the
- // stale-but-working catalog and the peer rebuilds still finish,
- // while the operator gets the connection that failed and why.
- // Without this a connection whose upstream is permanently broken
- // re-fails on every read and leaves no trace anywhere.
- Effect.catch((error) =>
- Effect.logWarning("executor stale tool sync failed", {
- integration: connection.integration,
- connection: connection.name,
- error: describeSyncFailure(error),
- }).pipe(Effect.as([] as readonly Tool[])),
- ),
- Effect.withSpan("executor.tools.sync_stale", {
- attributes: {
- "executor.integration": connection.integration,
- "executor.connection": connection.name,
+ const connections = yield* core.findMany("connection", {
+ where: (b: AnyCb) =>
+ staleBefore === null
+ ? b.isNull("tools_synced_at")
+ : b.or(b.isNull("tools_synced_at"), b("tools_synced_at", "<", staleBefore)),
+ });
+ // Each rebuild is an independent upstream listing, so they run together
+ // rather than one after another: a host with many stale remote-catalog
+ // connections otherwise pays the sum of every server's latency on the
+ // read that trips the TTL. Only the listings overlap — `persistCatalog`
+ // keeps the catalog writes in a single-file queue, so this fan-out never
+ // opens two transactions on a one-connection database.
+ //
+ // Two urgency classes. A stale-MARKED or config-revised catalog is known
+ // wrong (the upstream said so, or the integration's config changed), so
+ // the read waits for it within the grace budget. A catalog that is only
+ // older than the TTL is stale-but-working: in bounded mode its rebuild
+ // runs entirely in the background and the read answers from the
+ // persisted rows at once. Without that split every read after the TTL
+ // paid the grace budget for MCP listings it had no reason to wait on.
+ const urgent: Effect.Effect[] = [];
+ const deferred: Effect.Effect[] = [];
+ for (const connection of connections) {
+ const integrationRow = integrationBySlug.get(connection.integration);
+ if (!integrationRow) continue;
+ const runtime = runtimes.get(integrationRow.plugin_id);
+ // Only re-produce catalogs this executor can actually re-list —
+ // rebuilding under an unloaded plugin would clear a working catalog.
+ // (A loaded plugin without `resolveTools` still flows through:
+ // `produceConnectionTools` runs its clear-and-stamp cleanup path.)
+ if (!runtime) continue;
+
+ const syncedAt =
+ connection.tools_synced_at == null ? null : Number(connection.tools_synced_at);
+ const revisedTime =
+ integrationRow.config_revised_at == null
+ ? null
+ : Number(integrationRow.config_revised_at);
+
+ const staleMarked = syncedAt === null;
+ const configRevised = revisedTime !== null && (syncedAt ?? 0) < revisedTime;
+ const expired =
+ cutoff !== null &&
+ runtime.plugin.remoteToolCatalog === true &&
+ syncedAt !== null &&
+ syncedAt < cutoff;
+ if (!staleMarked && !configRevised && !expired) continue;
+
+ (staleMarked || configRevised || mode === "converge" ? urgent : deferred).push(
+ produceConnectionTools(
+ integrationRow,
+ {
+ owner: connection.owner as Owner,
+ integration: IntegrationSlug.make(connection.integration),
+ name: ConnectionName.make(connection.name),
},
- }),
- ),
- );
- }
- yield* Effect.all(rebuilds, {
- concurrency: STALE_TOOLS_SYNC_CONCURRENCY,
+ "background",
+ ).pipe(
+ // Best-effort, but never silent: the read still succeeds on the
+ // stale-but-working catalog and the peer rebuilds still finish,
+ // while the operator gets the connection that failed and why.
+ // Without this a connection whose upstream is permanently broken
+ // re-fails on every read and leaves no trace anywhere.
+ Effect.catch((error) =>
+ Effect.logWarning("executor stale tool sync failed", {
+ integration: connection.integration,
+ connection: connection.name,
+ error: describeSyncFailure(error),
+ }).pipe(Effect.as([] as readonly Tool[])),
+ ),
+ Effect.withSpan("executor.tools.sync_stale", {
+ attributes: {
+ "executor.integration": connection.integration,
+ "executor.connection": connection.name,
+ },
+ }),
+ ),
+ );
+ }
+ if (deferred.length > 0) {
+ const background = yield* Effect.forkDetach(
+ Effect.all(deferred, { concurrency: STALE_TOOLS_SYNC_CONCURRENCY }),
+ );
+ config.waitUntil?.(
+ new Promise((resolve) => background.addObserver(() => resolve(undefined))),
+ );
+ }
+ yield* Effect.all(urgent, {
+ concurrency: STALE_TOOLS_SYNC_CONCURRENCY,
+ });
});
- });
// How long a tools read waits for the stale sync before answering from
// the persisted rows (`ExecutorConfig.toolsSyncGraceMs`; `null` blocks
@@ -5671,51 +5699,97 @@ export const createExecutor =
- Effect.gen(function* () {
- const fiber = yield* Effect.forkDetach(
- syncStaleConnectionTools.pipe(
- Effect.catch((error) =>
- Effect.logWarning("executor stale tool sync scan failed", {
- error: describeSyncFailure(error),
- }),
+ const startStaleSync = Effect.gen(function* () {
+ const fiber = yield* Effect.forkDetach(
+ syncStaleConnectionTools("bounded").pipe(
+ Effect.catch((error) =>
+ Effect.logWarning("executor stale tool sync scan failed", {
+ error: describeSyncFailure(error),
+ }),
+ ),
+ ),
+ );
+ // On hosts that cancel request-scoped I/O once the response settles
+ // (Cloudflare Workers), hand the host the rebuilds' completion so the
+ // catalog still converges after the read stops waiting.
+ config.waitUntil?.(
+ new Promise((resolve) => fiber.addObserver(() => resolve(undefined))),
+ );
+ return fiber;
+ });
+
+ // Restrict a tool-row read to the prefixes an allowlist policy source can
+ // reach. `null` = no restriction; `false` = nothing reachable, skip the
+ // read. Any unbounded prefix (a bare `*`, or wildcards in every position)
+ // makes the whole scope unrestricted.
+ const dynamicScopeCondition = (
+ ruleSet: ActivePolicyRuleSet,
+ ): ((b: AnyCb) => Condition | boolean) | null | false => {
+ if (ruleSet.kind !== "prepared" || ruleSet.dynamicScope === undefined) return null;
+ const scopes = ruleSet.dynamicScope;
+ if (scopes.length === 0) return false;
+ if (scopes.some(isUnboundedDynamicToolScope)) return null;
+ return (b: AnyCb) =>
+ b.or(
+ ...scopes.map((scope) =>
+ b.and(
+ scope.integration === null ? true : b("integration", "=", scope.integration),
+ scope.owner === null ? true : b("owner", "=", scope.owner),
+ scope.connection === null ? true : b("connection", "=", scope.connection),
),
),
);
- // On hosts that cancel request-scoped I/O once the response settles
- // (Cloudflare Workers), hand the host the rebuilds' completion so the
- // catalog still converges after the read stops waiting.
- config.waitUntil?.(
- new Promise((resolve) => fiber.addObserver(() => resolve(undefined))),
- );
- yield* Fiber.await(fiber).pipe(Effect.timeoutOption(graceMs), Effect.asVoid);
- });
+ };
- const toolsList = (filter?: ToolListFilter): Effect.Effect =>
+ // `awaitStaleSync: false` still starts the bounded background sync (so
+ // catalogs converge for sessions that only ever list connections) but
+ // answers without waiting on it. Visibility-only readers (which
+ // connections and integrations exist under the active policy) use it: a
+ // stale catalog does not change which connection a tool belongs to, so
+ // gating those reads on upstream MCP listings only added latency to every
+ // session start. In strict (`null` grace) mode the wait is unconditional.
+ const readTools = (
+ filter: ToolListFilter | undefined,
+ options: { readonly awaitStaleSync: boolean },
+ ): Effect.Effect =>
Effect.gen(function* () {
+ let syncFiber: Fiber.Fiber | null = null;
if (toolsSyncGraceMs === null) {
- yield* syncStaleConnectionTools;
+ yield* syncStaleConnectionTools("converge");
} else {
- yield* awaitStaleSyncWithinGrace(toolsSyncGraceMs);
+ syncFiber = yield* startStaleSync;
}
+ // Fetch the policy snapshot while the sync runs: the scope it carries
+ // decides which rows to read at all.
+ const policyRules = yield* listActivePolicyRuleSet();
+ if (syncFiber && options.awaitStaleSync) {
+ yield* Fiber.await(syncFiber).pipe(
+ Effect.timeoutOption(toolsSyncGraceMs ?? 0),
+ Effect.asVoid,
+ );
+ }
+ const scopeCondition = dynamicScopeCondition(policyRules);
// Projected: the list surface is metadata (address, description,
// annotations) — loading every tool's input/output schema JSON made
// an unbounded list scale with schema bytes, not tool count.
- const rows = yield* core.findMany("tool", {
- where: (b: AnyCb) =>
- b.and(
- filter?.integration === undefined
- ? true
- : b("integration", "=", String(filter.integration)),
- filter?.owner === undefined ? true : b("owner", "=", filter.owner),
- filter?.connection === undefined
- ? true
- : b("connection", "=", String(filter.connection)),
- ),
- select: TOOL_INVOCATION_COLUMNS,
- });
+ const rows =
+ scopeCondition === false
+ ? []
+ : yield* core.findMany("tool", {
+ where: (b: AnyCb) =>
+ b.and(
+ filter?.integration === undefined
+ ? true
+ : b("integration", "=", String(filter.integration)),
+ filter?.owner === undefined ? true : b("owner", "=", filter.owner),
+ filter?.connection === undefined
+ ? true
+ : b("connection", "=", String(filter.connection)),
+ scopeCondition === null ? true : scopeCondition(b),
+ ),
+ select: TOOL_INVOCATION_COLUMNS,
+ });
const includeBlocked = filter?.includeBlocked ?? false;
- const policyRules = yield* listActivePolicyRuleSet();
// Only tools whose integration is still in the catalog. A tool row
// whose integration was removed is an orphan (a removal that could
// not reach this subject's rows): listing it invites an invoke that
@@ -5752,6 +5826,9 @@ export const createExecutor = =>
+ readTools(filter, { awaitStaleSync: true });
+
const toolSchema = (
address: ToolAddress,
): Effect.Effect =>
diff --git a/packages/core/sdk/src/index.ts b/packages/core/sdk/src/index.ts
index 56d7d5b777..627d3de1de 100644
--- a/packages/core/sdk/src/index.ts
+++ b/packages/core/sdk/src/index.ts
@@ -192,7 +192,10 @@ export {
export {
matchPattern,
isValidPattern,
+ dynamicToolScopeForPattern,
+ isUnboundedDynamicToolScope,
effectivePolicyFromSorted,
+ type DynamicToolScope,
ToolPolicyActionSchema,
type ToolPolicy,
type CreateToolPolicyInput,
@@ -388,6 +391,7 @@ export {
type AnyPlugin,
type StorageDeps,
type OwnerBinding,
+ type PreparedToolPolicy,
type ToolPolicyProvider,
type ToolPolicyProviderRule,
type IntegrationRecord,
diff --git a/packages/core/sdk/src/plugin.ts b/packages/core/sdk/src/plugin.ts
index 2ace32f891..e079f5ab8a 100644
--- a/packages/core/sdk/src/plugin.ts
+++ b/packages/core/sdk/src/plugin.ts
@@ -55,6 +55,7 @@ import type { CredentialProvider, ProviderEntry } from "./provider";
import type { PluginStorageConfig, PluginStorageFacade } from "./plugin-storage";
import type {
CreateToolPolicyInput,
+ DynamicToolScope,
EffectivePolicy,
RemoveToolPolicyInput,
ToolPolicy,
@@ -131,13 +132,25 @@ export interface ToolPolicyProvider {
* requests), so caching on it would serve stale policy state. Each operation
* gets a fresh snapshot.
*/
- readonly prepare?: () => Effect.Effect<
- (input: {
- readonly toolId: string;
- readonly defaultRequiresApproval?: boolean;
- }) => EffectivePolicy,
- StorageFailure
- >;
+ readonly prepare?: () => Effect.Effect;
+}
+
+/** What `ToolPolicyProvider.prepare` hands core for one operation. */
+export interface PreparedToolPolicy {
+ /** Pure resolver over the snapshot `prepare` fetched. */
+ readonly resolve: (input: {
+ readonly toolId: string;
+ readonly defaultRequiresApproval?: boolean;
+ }) => EffectivePolicy;
+ /**
+ * The dynamic-tool prefixes this policy source can ever approve. When set,
+ * core restricts the tool rows it loads on a list to these prefixes instead
+ * of reading the whole catalog and blocking most of it in memory — the read
+ * then scales with the allowlist, not the workspace. An empty array means no
+ * dynamic tool is reachable. Omit when the source is not an allowlist (any
+ * row may be approved) so core keeps the unrestricted read.
+ */
+ readonly dynamicScope?: readonly DynamicToolScope[];
}
// ---------------------------------------------------------------------------
diff --git a/packages/core/sdk/src/policies.test.ts b/packages/core/sdk/src/policies.test.ts
index 18c5d29a72..98af2a65e7 100644
--- a/packages/core/sdk/src/policies.test.ts
+++ b/packages/core/sdk/src/policies.test.ts
@@ -16,6 +16,7 @@ import { ElicitationResponse, type ElicitationHandler } from "./elicitation";
import { createExecutor } from "./executor";
import type { FumaDb } from "./fuma-runtime";
import {
+ dynamicToolScopeForPattern,
effectivePolicyFromSorted,
isValidPattern,
matchPattern,
@@ -108,6 +109,56 @@ describe("isValidPattern", () => {
});
});
+describe("dynamicToolScopeForPattern", () => {
+ it("reads the connection prefix out of subtree patterns", () => {
+ expect(dynamicToolScopeForPattern("github.org.main.*")).toEqual({
+ integration: "github",
+ owner: "org",
+ connection: "main",
+ });
+ expect(dynamicToolScopeForPattern("github.org.*")).toEqual({
+ integration: "github",
+ owner: "org",
+ connection: null,
+ });
+ expect(dynamicToolScopeForPattern("github.*")).toEqual({
+ integration: "github",
+ owner: null,
+ connection: null,
+ });
+ });
+
+ it("treats a mid-segment wildcard as any value for that position", () => {
+ expect(dynamicToolScopeForPattern("github.*.*.repos.list")).toEqual({
+ integration: "github",
+ owner: null,
+ connection: null,
+ });
+ expect(dynamicToolScopeForPattern("github.user.*.repos.*")).toEqual({
+ integration: "github",
+ owner: "user",
+ connection: null,
+ });
+ });
+
+ it("is unbounded for the universal pattern", () => {
+ expect(dynamicToolScopeForPattern("*")).toEqual({
+ integration: null,
+ owner: null,
+ connection: null,
+ });
+ });
+
+ it("yields no scope for patterns that can only reach static tools", () => {
+ // Exact ids shorter than a dynamic address.
+ expect(dynamicToolScopeForPattern("github")).toBeNull();
+ expect(dynamicToolScopeForPattern("github.org.main")).toBeNull();
+ // A literal owner that is neither org nor user is a static namespace.
+ expect(dynamicToolScopeForPattern("executor.coreTools.*")).toBeNull();
+ expect(dynamicToolScopeForPattern("executor.coreTools.connections.list")).toBeNull();
+ });
+});
+
describe("resolveToolPolicy", () => {
// v2: policy rows carry `owner` (org|user) instead of a scope id.
const ROW = (
@@ -714,6 +765,100 @@ describe("active tool-policy provider", () => {
);
});
+describe("prepared tool policy provider with a dynamic scope", () => {
+ const scopedProviderPlugin = (
+ dynamicScope: readonly {
+ integration: string | null;
+ owner: string | null;
+ connection: string | null;
+ }[],
+ ) =>
+ definePlugin(() => ({
+ id: "scoped-policy-provider" as const,
+ storage: () => ({}),
+ toolPolicyProvider: () => ({
+ list: () => Effect.succeed([]),
+ // Approves everything it is asked about: only the scope decides what
+ // core reads, so anything missing from the list was never loaded.
+ prepare: () =>
+ Effect.succeed({
+ resolve: () => ({ action: "approve" as const, source: "user" as const, pattern: "*" }),
+ dynamicScope,
+ }),
+ }),
+ }))();
+
+ const setupScoped = (
+ dynamicScope: readonly {
+ integration: string | null;
+ owner: string | null;
+ connection: string | null;
+ }[],
+ ) =>
+ makeTestExecutor({
+ plugins: [policyTestPlugin(), scopedProviderPlugin(dynamicScope)] as const,
+ }).pipe(
+ Effect.tap((executor) =>
+ Effect.gen(function* () {
+ yield* executor.ptest.seed();
+ for (const integration of [VERCEL, GITHUB]) {
+ yield* executor.connections.create({
+ owner: "org",
+ name: CONN,
+ integration,
+ template: TEMPLATE,
+ value: "v",
+ });
+ }
+ }),
+ ),
+ );
+
+ const dynamicAddresses = (tools: readonly { address: unknown; static?: boolean }[]) =>
+ tools
+ .filter((tool) => !tool.static)
+ .map((tool) => String(tool.address))
+ .sort();
+
+ it.effect("restricts the list to the scoped connection", () =>
+ Effect.gen(function* () {
+ const executor = yield* setupScoped([
+ { integration: String(VERCEL), owner: "org", connection: String(CONN) },
+ ]);
+ const tools = yield* executor.tools.list();
+ expect(dynamicAddresses(tools)).toEqual([
+ String(addr(VERCEL, "delete")),
+ String(addr(VERCEL, "deploy")),
+ ]);
+ const connections = yield* executor.connections.list();
+ expect(connections.map((connection) => String(connection.integration))).toEqual([
+ String(VERCEL),
+ ]);
+ }),
+ );
+
+ it.effect("a wildcard position widens the scope to every value", () =>
+ Effect.gen(function* () {
+ const executor = yield* setupScoped([{ integration: null, owner: "org", connection: null }]);
+ const tools = yield* executor.tools.list();
+ expect(dynamicAddresses(tools)).toEqual([
+ String(addr(GITHUB, "list")),
+ String(addr(VERCEL, "delete")),
+ String(addr(VERCEL, "deploy")),
+ ]);
+ }),
+ );
+
+ it.effect("an empty scope reads no dynamic rows", () =>
+ Effect.gen(function* () {
+ const executor = yield* setupScoped([]);
+ const tools = yield* executor.tools.list();
+ expect(dynamicAddresses(tools)).toEqual([]);
+ expect(yield* executor.connections.list()).toEqual([]);
+ }),
+ );
+});
+
describe("approve / require_approval interaction with annotations", () => {
it.effect("approve skips the elicitation prompt even when plugin requires approval", () =>
Effect.gen(function* () {
diff --git a/packages/core/sdk/src/policies.ts b/packages/core/sdk/src/policies.ts
index 8620d9c6d3..b9e1f1ecad 100644
--- a/packages/core/sdk/src/policies.ts
+++ b/packages/core/sdk/src/policies.ts
@@ -120,6 +120,49 @@ export const isValidPattern = (pattern: string): boolean => {
return true;
};
+// ---------------------------------------------------------------------------
+// Dynamic-tool scope — the (integration, owner, connection) prefix a pattern
+// can reach. Lets a policy source that is an allowlist (a toolkit) narrow the
+// tool rows core loads to the connections the allowlist names, instead of
+// walking the whole catalog and blocking almost all of it in memory.
+// ---------------------------------------------------------------------------
+
+/** One reachable prefix of a dynamic tool id `integration.owner.connection.tool`.
+ * `null` in a position means any value. All three `null` = unbounded. */
+export interface DynamicToolScope {
+ readonly integration: string | null;
+ readonly owner: string | null;
+ readonly connection: string | null;
+}
+
+export const isUnboundedDynamicToolScope = (scope: DynamicToolScope): boolean =>
+ scope.integration === null && scope.owner === null && scope.connection === null;
+
+/**
+ * The prefix of dynamic tool ids a pattern can match, or `null` when it can
+ * match none. A dynamic tool id has at least four segments, so an exact
+ * pattern shorter than that reaches only static tools. A trailing `*` covers
+ * every deeper segment; a mid-pattern `*` covers exactly that segment.
+ */
+export const dynamicToolScopeForPattern = (pattern: string): DynamicToolScope | null => {
+ if (pattern === "*") return { integration: null, owner: null, connection: null };
+ const segments = pattern.split(".");
+ const subtree = segments.at(-1) === "*";
+ if (!subtree && segments.length < 4) return null;
+ const at = (index: number): string | null => {
+ const segment = segments[index];
+ if (segment === undefined) return null;
+ // Only the trailing `*` reaches past its own position; a mid `*` is one
+ // segment, which is also "any value" for that position.
+ return segment === "*" ? null : segment;
+ };
+ const owner = at(1);
+ // A dynamic tool id's owner segment is always `org` or `user`; any other
+ // literal there names a static namespace (`executor.coreTools.*`).
+ if (owner !== null && owner !== "org" && owner !== "user") return null;
+ return { integration: at(0), owner, connection: at(2) };
+};
+
// ---------------------------------------------------------------------------
// Resolution — each owner contributes its first matching rule by local
// position; the most restrictive matched action across owners wins. Caller
diff --git a/packages/plugins/mcp/src/sdk/catalog-sync.test.ts b/packages/plugins/mcp/src/sdk/catalog-sync.test.ts
index f3328876b3..34ec9f24f8 100644
--- a/packages/plugins/mcp/src/sdk/catalog-sync.test.ts
+++ b/packages/plugins/mcp/src/sdk/catalog-sync.test.ts
@@ -14,7 +14,7 @@
// ---------------------------------------------------------------------------
import { describe, expect, it } from "@effect/vitest";
-import { Deferred, Effect, Fiber, Option, Ref, Schema } from "effect";
+import { Deferred, Effect, Fiber, Option, Ref, Schedule, Schema } from "effect";
import { HttpServerResponse } from "effect/unstable/http";
import {
@@ -133,11 +133,13 @@ describe("MCP tool-catalog sync (end-to-end)", () => {
}),
);
- it.effect("expired catalogs re-list on read once older than the freshness TTL", () =>
+ // Live clock: the rebuild is real I/O against the test server, so the poll
+ // must advance on wall time rather than the test clock.
+ it.live("expired catalogs re-list in the background once older than the freshness TTL", () =>
Effect.gen(function* () {
const mutable = makeMutableCatalogMcpServer();
const server = yield* serveMcpServer(mutable.factory);
- // Everything is instantly stale — every tools read re-lists.
+ // Everything is instantly stale — every tools read starts a re-list.
const executor = yield* makeCatalogTestExecutor(server.url, { toolsSyncTtlMs: 0 });
expect(toolNames(yield* executor.tools.list())).toContain(mutable.initialToolName);
@@ -145,7 +147,17 @@ describe("MCP tool-catalog sync (end-to-end)", () => {
// Server-side change with no notification and no executor signal at all.
mutable.renameTool();
- const refreshed = toolNames(yield* executor.tools.list());
+ // A time-expired catalog is stale-but-working: the read answers from the
+ // persisted rows without waiting on the upstream listing, and a later
+ // read observes the rebuilt catalog.
+ const refreshed = yield* executor.tools.list().pipe(
+ Effect.map(toolNames),
+ Effect.repeat({
+ until: (names) => names.includes(mutable.renamedToolName),
+ schedule: Schedule.spaced("20 millis"),
+ }),
+ Effect.timeout("5 seconds"),
+ );
expect(refreshed).toContain(mutable.renamedToolName);
expect(refreshed).not.toContain(mutable.initialToolName);
}),
diff --git a/packages/plugins/toolkits/src/server.test.ts b/packages/plugins/toolkits/src/server.test.ts
index bab67eb0e9..3db165f6e6 100644
--- a/packages/plugins/toolkits/src/server.test.ts
+++ b/packages/plugins/toolkits/src/server.test.ts
@@ -132,6 +132,50 @@ describe("toolkitsPlugin", () => {
}),
);
+ it.effect("prepares a dynamic scope from the toolkit's access patterns", () =>
+ Effect.gen(function* () {
+ const executor = yield* makeTestExecutor({
+ plugins: [toolkitsPlugin()] as const,
+ });
+
+ const orgKit = yield* executor.toolkits.create({ owner: "org", name: "Org Kit" });
+ for (const pattern of [
+ "github.org.main.*",
+ "slack.*",
+ "linear.*.*.issues.list",
+ "github.user.alice.*",
+ "executor.coreTools.*",
+ ]) {
+ yield* executor.toolkits.createConnection(orgKit.id, { pattern });
+ }
+ const prepared = yield* executor.toolkits.preparePolicyResolverForSlug(orgKit.slug);
+ // An org toolkit never reaches personal rows: unowned prefixes pin to
+ // org, user-only prefixes drop, and static-only patterns contribute none.
+ const byIntegration = (
+ a: { integration: string | null },
+ b: { integration: string | null },
+ ) => String(a.integration).localeCompare(String(b.integration));
+ expect([...(prepared.dynamicScope ?? [])].sort(byIntegration)).toEqual([
+ { integration: "github", owner: "org", connection: "main" },
+ { integration: "linear", owner: "org", connection: null },
+ { integration: "slack", owner: "org", connection: null },
+ ]);
+ expect(prepared.resolve({ toolId: "github.org.main.repos.list" }).action).toBe("approve");
+ expect(prepared.resolve({ toolId: "github.user.alice.repos.list" }).action).toBe("block");
+
+ const personalKit = yield* executor.toolkits.create({ owner: "user", name: "Me Kit" });
+ yield* executor.toolkits.createConnection(personalKit.id, { pattern: "github.user.alice.*" });
+ const personal = yield* executor.toolkits.preparePolicyResolverForSlug(personalKit.slug);
+ expect(personal.dynamicScope).toEqual([
+ { integration: "github", owner: "user", connection: "alice" },
+ ]);
+
+ const missing = yield* executor.toolkits.preparePolicyResolverForSlug("no-such-kit");
+ expect(missing.dynamicScope).toEqual([]);
+ expect(missing.resolve({ toolId: "github.org.main.repos.list" }).action).toBe("block");
+ }),
+ );
+
it.effect("treats a persisted connection-root approve as an access policy", () =>
Effect.gen(function* () {
const executor = yield* makeTestExecutor({
diff --git a/packages/plugins/toolkits/src/server.ts b/packages/plugins/toolkits/src/server.ts
index 7dacc3e456..eef15de797 100644
--- a/packages/plugins/toolkits/src/server.ts
+++ b/packages/plugins/toolkits/src/server.ts
@@ -2,16 +2,19 @@ import {
Context,
definePlugin,
definePluginStorageCollection,
+ dynamicToolScopeForPattern,
Effect,
HttpApiBuilder,
isValidPattern,
matchPattern,
Schema,
+ type DynamicToolScope,
type EffectivePolicy,
type Owner,
type PluginCtx,
type PluginStorageFacade,
type PluginStorageCollectionFacade,
+ type PreparedToolPolicy,
type StorageFailure,
type ToolPolicyAction,
type ToolPolicyProvider,
@@ -150,22 +153,42 @@ const isLegacyConnectionPolicy = (policy: ToolkitPolicyRecord): boolean => {
return parts.at(-1) === "*" && (parts.length === 3 || parts.length === 4);
};
-const resolveToolkitPolicy = (
- toolId: string,
+// The toolkit's rules, digested once so resolving a tool is a scan over
+// already-sorted patterns. A tools list resolves every candidate row against
+// the same snapshot, so the legacy split and the sort must not be redone per
+// tool.
+interface ToolkitRuleSnapshot {
+ /** Patterns granting access: connection records plus legacy approve rows. */
+ readonly accessPatterns: readonly string[];
+ /** Non-legacy policies in precedence order. */
+ readonly orderedPolicies: readonly ToolkitPolicyRecord[];
+}
+
+const digestToolkitRules = (
connections: readonly ToolkitConnectionRecord[],
policies: readonly ToolkitPolicyRecord[],
+): ToolkitRuleSnapshot => {
+ const legacyPolicyIds = legacyConnectionPolicyIds(policies, connections);
+ return {
+ accessPatterns: [
+ ...connections.map((connection) => connection.pattern),
+ ...policies.filter((policy) => legacyPolicyIds.has(policy.id)).map((p) => p.pattern),
+ ],
+ orderedPolicies: policies
+ .filter((policy) => !legacyPolicyIds.has(policy.id))
+ .sort(comparePositioned),
+ };
+};
+
+const resolveToolkitPolicy = (
+ toolId: string,
+ rules: ToolkitRuleSnapshot,
defaultRequiresApproval?: boolean,
): EffectivePolicy => {
- const legacyPolicyIds = legacyConnectionPolicyIds(policies, connections);
- const connected =
- connections.some((connection) => matchPattern(connection.pattern, toolId)) ||
- policies.some(
- (policy) => legacyPolicyIds.has(policy.id) && matchPattern(policy.pattern, toolId),
- );
+ const connected = rules.accessPatterns.some((pattern) => matchPattern(pattern, toolId));
if (!connected) return blockedPolicy();
- for (const policy of [...policies].sort(comparePositioned)) {
- if (legacyPolicyIds.has(policy.id)) continue;
+ for (const policy of rules.orderedPolicies) {
if (!matchPattern(policy.pattern, toolId)) continue;
return {
action: policy.action,
@@ -177,6 +200,28 @@ const resolveToolkitPolicy = (
return pluginDefaultPolicy(defaultRequiresApproval);
};
+// The dynamic-tool prefixes the access patterns can reach. An org toolkit
+// never grants personal tools, so its prefixes are pinned to org rows and
+// user-only prefixes drop out; the per-tool check still enforces the same
+// rule for anything the prefix cannot express.
+const toolkitDynamicScope = (
+ rules: ToolkitRuleSnapshot,
+ isOrg: boolean,
+): readonly DynamicToolScope[] => {
+ const scopes: DynamicToolScope[] = [];
+ for (const pattern of rules.accessPatterns) {
+ const scope = dynamicToolScopeForPattern(pattern);
+ if (!scope) continue;
+ if (!isOrg) {
+ scopes.push(scope);
+ continue;
+ }
+ if (scope.owner === "user") continue;
+ scopes.push(scope.owner === null ? { ...scope, owner: "org" } : scope);
+ }
+ return scopes;
+};
+
const legacyConnectionPolicyIds = (
policies: readonly ToolkitPolicyRecord[],
connections: readonly ToolkitConnectionRecord[],
@@ -507,38 +552,36 @@ const makeToolkitsExtension = (ctx: PluginCtx) => {
if (toolkit.owner === "org" && isPersonalDynamicToolId(toolId)) return blockedPolicy();
const policies = yield* listPoliciesForRecord(toolkit.data.id);
const connections = yield* listConnectionsForRecord(toolkit.data.id);
- return resolveToolkitPolicy(toolId, connections, policies, defaultRequiresApproval);
+ return resolveToolkitPolicy(
+ toolId,
+ digestToolkitRules(connections, policies),
+ defaultRequiresApproval,
+ );
});
// Batched form of `resolvePolicyForSlug`: fetch the toolkit, its policies, and
// its connections ONCE, then hand back a pure resolver core can run for every
- // tool in a single tools/list or tools/call. `resolvePolicyForSlug` re-fetches
- // policies + connections on every tool, which is the per-tool N+1 that scales
- // with the whole catalog on the list surface. This is byte-for-byte the same
- // resolution, just hoisted out of the loop.
+ // tool in a single tools/list or tools/call, plus the prefixes those rules
+ // can reach so core reads only the toolkit's rows instead of the whole
+ // catalog. `resolvePolicyForSlug` re-fetches policies + connections on every
+ // tool, which is the per-tool N+1 that scales with the whole catalog on the
+ // list surface. This is the same resolution, hoisted out of the loop.
const preparePolicyResolverForSlug = (
slug: string,
- ): Effect.Effect<
- (input: {
- readonly toolId: string;
- readonly defaultRequiresApproval?: boolean;
- }) => EffectivePolicy,
- StorageFailure
- > =>
+ ): Effect.Effect =>
Effect.gen(function* () {
const toolkit = yield* getBySlugEntry(slug);
- if (!toolkit) return () => blockedPolicy();
+ if (!toolkit) return { resolve: () => blockedPolicy(), dynamicScope: [] };
const isOrg = toolkit.owner === "org";
const policies = yield* listPoliciesForRecord(toolkit.data.id);
const connections = yield* listConnectionsForRecord(toolkit.data.id);
- return (input: { readonly toolId: string; readonly defaultRequiresApproval?: boolean }) => {
- if (isOrg && isPersonalDynamicToolId(input.toolId)) return blockedPolicy();
- return resolveToolkitPolicy(
- input.toolId,
- connections,
- policies,
- input.defaultRequiresApproval,
- );
+ const rules = digestToolkitRules(connections, policies);
+ return {
+ resolve: (input) => {
+ if (isOrg && isPersonalDynamicToolId(input.toolId)) return blockedPolicy();
+ return resolveToolkitPolicy(input.toolId, rules, input.defaultRequiresApproval);
+ },
+ dynamicScope: toolkitDynamicScope(rules, isOrg),
};
});
From 29413d8dafaeabfd9b9f10e5fddd1f5b735ffa30 Mon Sep 17 00:00:00 2001
From: Rhys Sullivan <39114868+RhysSullivan@users.noreply.github.com>
Date: Fri, 18 Sep 2026 12:18:43 -0700
Subject: [PATCH 08/11] Scope tool policies to the account they are set under
(#2062)
---
.changeset/account-scoped-tool-policies.md | 5 ++
e2e/scenarios/policies-ui.test.ts | 89 ++++++++++++++-----
e2e/src/integration-creation-permissions.ts | 12 +++
.../react/src/api/error-reporting.test.ts | 14 +++
packages/react/src/api/error-reporting.tsx | 13 ++-
packages/react/src/components/tool-tree.tsx | 75 +++++++++++-----
packages/react/src/lib/integration-add.tsx | 12 +--
packages/react/src/lib/policy-pattern.test.ts | 21 +++++
packages/react/src/lib/policy-pattern.ts | 28 +++++-
.../react/src/pages/integration-detail.tsx | 29 ++++--
packages/react/src/pages/tools.tsx | 26 ++++--
11 files changed, 257 insertions(+), 67 deletions(-)
create mode 100644 .changeset/account-scoped-tool-policies.md
create mode 100644 packages/react/src/lib/policy-pattern.test.ts
diff --git a/.changeset/account-scoped-tool-policies.md b/.changeset/account-scoped-tool-policies.md
new file mode 100644
index 0000000000..a258479d0a
--- /dev/null
+++ b/.changeset/account-scoped-tool-policies.md
@@ -0,0 +1,5 @@
+---
+"@executor-js/react": patch
+---
+
+Tool policies set from an account section of the integration Tools tab now apply to that connection only, and each account header gets a menu to set a policy for the whole connection. Members no longer see policy controls they cannot use, and a refused policy write shows the server's reason.
diff --git a/e2e/scenarios/policies-ui.test.ts b/e2e/scenarios/policies-ui.test.ts
index ad45864429..00509252f5 100644
--- a/e2e/scenarios/policies-ui.test.ts
+++ b/e2e/scenarios/policies-ui.test.ts
@@ -4,18 +4,23 @@
// the category (group) row menu writes a subtree rule. The product promises
// under test:
//
-// 1. Both menus surface the REAL stored pattern (connection-wildcarded
-// `integration.*.*.tool`) before anything is written.
+// 1. Both menus surface the REAL stored pattern (pinned to the account the
+// row sits under, `integration...tool`) before
+// anything is written.
// 2. A leaf rule and a category rule coexist: the more specific leaf rule
// keeps precedence over the later category rule, which covers the rest
// of its group.
-// 3. Rules are connection-agnostic: set from one account's section, they
-// govern the other account's rows too, and the menu there shows the
-// active rule with a Clear option.
-// 4. The tool detail header's policy badge is the same authoring surface:
+// 3. Rules are account-scoped: set from one account's section, they leave
+// the other account's rows untouched. Two connections of one
+// integration are different credentials (a bot token and a user token),
+// so blocking a tool on one must not block it on the other.
+// 4. The account header has its own menu that rules the whole connection
+// (`integration...*`), recognizes its rule, and
+// clears it.
+// 5. The tool detail header's policy badge is the same authoring surface:
// it writes the same stored pattern, recognizes its own rule afterward
// (the Clear affordance), and Clear really removes the rule.
-// 5. The rules materialize as manageable rows on /policies and persist
+// 6. The rules materialize as manageable rows on /policies and persist
// server-side with exactly the owner/pattern/action the UI promised.
import { randomBytes } from "node:crypto";
@@ -83,11 +88,12 @@ scenario(
const beta = ConnectionName.make(`beta${suffix}`);
const accounts = [alpha, beta] as const;
- // The UI hides owner/connection segments; a rule authored on a node is
- // stored connection-wildcarded so it spans every account.
- const leafPattern = `${integration}.*.*.records.create`;
- const categoryPattern = `${integration}.*.*.records.*`;
- const listLeafPattern = `${integration}.*.*.records.list`;
+ // The UI hides owner/connection segments in the row labels, but a rule
+ // authored under an account section is stored pinned to that account.
+ const leafPattern = `${integration}.org.${alpha}.records.create`;
+ const categoryPattern = `${integration}.org.${alpha}.records.*`;
+ const listLeafPattern = `${integration}.org.${alpha}.records.list`;
+ const betaAccountPattern = `${integration}.org.${beta}.*`;
// Selfhost scenarios share one workspace — remove everything this one
// made (policies, connections, the integration) even on failure.
@@ -159,6 +165,14 @@ scenario(
.getByRole("button")
.filter({ hasText: leaf })
.getByLabel(label, { exact: true });
+ // Wait until a leaf's indicator with this label is gone (after a clear).
+ const expectNoIndicator = async (connection: string, leaf: string, label: string) => {
+ await expect
+ .poll(() => leafIndicator(connection, leaf, label).count(), {
+ message: `${connection} ${leaf} still shows "${label}"`,
+ })
+ .toBe(0);
+ };
const internalError = JSON.stringify({ _tag: "InternalError", traceId: "policy-write" });
await step("Open the integration's Tools tab", async () => {
@@ -254,25 +268,58 @@ scenario(
},
);
- await step("The same rules govern the second account's rows", async () => {
+ await step("The second account's rows are untouched", async () => {
await closedGroup(beta, integration).click();
await closedGroup(beta, "records").click();
- await leafIndicator(beta, "create", `Blocked (matched ${leafPattern})`).waitFor();
- await leafIndicator(
- beta,
- "list",
- `Require approval (matched ${categoryPattern})`,
- ).waitFor();
+ await sectionFor(beta).getByRole("button").filter({ hasText: "create" }).waitFor();
+ expect(
+ await leafIndicator(beta, "create", `Blocked (matched ${leafPattern})`).count(),
+ "a rule set under one account does not block the same tool on another",
+ ).toBe(0);
+ expect(
+ await leafIndicator(
+ beta,
+ "list",
+ `Require approval (matched ${categoryPattern})`,
+ ).count(),
+ "a category rule set under one account does not reach another account",
+ ).toBe(0);
});
await step("Reopening the menu offers to clear the active rule", async () => {
- await policyMenuFor(beta, `${integration}.records.create`).click();
+ await policyMenuFor(alpha, `${integration}.records.create`).click();
await page.getByRole("menuitem", { name: "Clear" }).waitFor();
await page.keyboard.press("Escape");
});
+ await step("The account header blocks the whole second connection", async () => {
+ const headerMenu = sectionFor(beta).getByRole("button", {
+ name: `Set policy for ${integration} / ${beta}`,
+ exact: true,
+ });
+ await headerMenu.click();
+ // The header menu is headed by the whole-account pattern it will store.
+ await page.getByText(betaAccountPattern, { exact: true }).waitFor();
+ await page.getByRole("menuitem", { name: "Block" }).click();
+ await leafIndicator(beta, "create", `Blocked (matched ${betaAccountPattern})`).waitFor();
+ await leafIndicator(beta, "list", `Blocked (matched ${betaAccountPattern})`).waitFor();
+ // The first account is not affected by the second account's rule.
+ await leafIndicator(alpha, "create", `Blocked (matched ${leafPattern})`).waitFor();
+ });
+
+ await step("The account header recognizes its rule and Clear removes it", async () => {
+ const headerMenu = sectionFor(beta).getByRole("button", {
+ name: `Set policy for ${integration} / ${beta}`,
+ exact: true,
+ });
+ await headerMenu.click();
+ await page.getByRole("menuitem", { name: "Clear" }).click();
+ await sectionFor(beta).getByRole("button").filter({ hasText: "create" }).waitFor();
+ await expectNoIndicator(beta, "create", `Blocked (matched ${betaAccountPattern})`);
+ });
+
await step("Open the tool detail for records.list", async () => {
- await sectionFor(beta).getByRole("button").filter({ hasText: "list" }).click();
+ await sectionFor(alpha).getByRole("button").filter({ hasText: "list" }).click();
// The header badge reflects the inherited category rule.
await page.getByRole("button", { name: `Matched policy: ${categoryPattern}` }).waitFor();
});
diff --git a/e2e/src/integration-creation-permissions.ts b/e2e/src/integration-creation-permissions.ts
index c54309a569..cce298ac9f 100644
--- a/e2e/src/integration-creation-permissions.ts
+++ b/e2e/src/integration-creation-permissions.ts
@@ -108,6 +108,18 @@ export const integrationCreationPermissions = (admin: Identity, member: Identity
expect(await action.isDisabled()).toBe(true);
}
});
+ await step("Member sees tool policies without a way to change them", async () => {
+ // Policies on the Tools page are workspace rules the server refuses
+ // for members, so the row menus and the detail badge menu stay off.
+ await visit(page, "/tools");
+ await page.getByRole("button").filter({ hasText: "executor" }).first().waitFor();
+ expect(
+ await page.getByRole("button", { name: /^Set policy/ }).count(),
+ "members get no policy menus on tool rows",
+ ).toBe(0);
+ await visit(page, `/integrations/${slug}`);
+ await page.getByRole("button", { name: "Add connection", exact: true }).waitFor();
+ });
await step("Member can still add a personal connection", async () => {
await page.getByRole("button", { name: "Add connection", exact: true }).click();
const dialog = page.getByRole("dialog");
diff --git a/packages/react/src/api/error-reporting.test.ts b/packages/react/src/api/error-reporting.test.ts
index 655f792ba7..c1c1d09e97 100644
--- a/packages/react/src/api/error-reporting.test.ts
+++ b/packages/react/src/api/error-reporting.test.ts
@@ -53,6 +53,20 @@ describe("frontend error reporting", () => {
expect(messageFromExit(Exit.fail({ reason: "unknown" }), "Fallback")).toBe("Fallback");
});
+ it("reads a message the error exposes as a prototype getter", () => {
+ // Schema-tagged API errors (e.g. OrgWriteDeniedError) declare no `message`
+ // field; the sentence lives on a class getter, which a struct decode misses.
+ class GetterError extends Data.TaggedError("GetterError")<{}> {
+ override get message(): string {
+ return "Requires a workspace admin.";
+ }
+ }
+ const exit = Exit.fail(new GetterError());
+
+ expect(messageFromExit(exit, "Fallback")).toBe("Requires a workspace admin.");
+ expect(messageFromUnknown(new GetterError(), "Fallback")).toBe("Requires a workspace admin.");
+ });
+
it("reports failed exits with the provided context", () => {
const exit = Exit.fail({ message: "Could not update integration" });
const { calls, report } = captureReports();
diff --git a/packages/react/src/api/error-reporting.tsx b/packages/react/src/api/error-reporting.tsx
index 8a9eab0d03..2c19b8ce39 100644
--- a/packages/react/src/api/error-reporting.tsx
+++ b/packages/react/src/api/error-reporting.tsx
@@ -21,8 +21,17 @@ class FrontendHandledError extends Data.TaggedError("FrontendHandledError")<{
readonly context: FrontendErrorContext;
}> {}
-const ErrorMessage = Schema.Struct({ message: Schema.String });
-const decodeErrorMessage = Schema.decodeUnknownOption(ErrorMessage);
+// Effect's tagged error classes (`Schema.TaggedErrorClass`) often declare no
+// `message` field and expose it as a prototype getter instead, so a struct
+// decode — which only sees own properties — would miss the very sentence the
+// server wrote for the user. Read the property directly.
+const decodeErrorMessage = (value: unknown): Option.Option<{ readonly message: string }> => {
+ if (typeof value !== "object" || value === null) return Option.none();
+ const message: unknown = Reflect.get(value, "message");
+ return typeof message === "string" && message.length > 0
+ ? Option.some({ message })
+ : Option.none();
+};
const TaggedValue = Schema.Struct({ _tag: Schema.String });
const decodeTaggedValue = Schema.decodeUnknownOption(TaggedValue);
diff --git a/packages/react/src/components/tool-tree.tsx b/packages/react/src/components/tool-tree.tsx
index f0fa20e6f2..d3b8c94908 100644
--- a/packages/react/src/components/tool-tree.tsx
+++ b/packages/react/src/components/tool-tree.tsx
@@ -3,7 +3,7 @@ import { ChevronRightIcon, MoreHorizontalIcon, SearchIcon, XIcon } from "lucide-
import type { EffectivePolicy, Owner, ToolPolicyAction } from "@executor-js/sdk/shared";
import { ownerLabel, useOwnerDisplay } from "../api/owner-display";
import { trackEvent } from "../api/analytics";
-import { toPolicyPattern } from "../lib/policy-pattern";
+import { accountPolicyPattern, toPolicyPattern } from "../lib/policy-pattern";
import { Badge } from "./badge";
import { Button } from "./button";
import { Input } from "./input";
@@ -297,7 +297,9 @@ export function ToolTree(props: {
* emit the tool's full dotted id; group rows emit `prefix.*`. */
onSetPolicy?: (pattern: string, action: ToolPolicyAction) => void;
onClearPolicy?: (pattern: string) => void;
- /** Maps the displayed row path into the persisted policy pattern. */
+ /** Maps the displayed row path into the persisted policy pattern for the
+ * flat tree. Ignored in the account-grouped view, where every row writes a
+ * pattern pinned to its own account (`integration...`). */
patternForDisplay?: (displayPattern: string) => string;
/** Sorted user-authored policies (most-precedent first). Used to
* decide whether a node has its own exact-pattern user rule today
@@ -429,26 +431,55 @@ export function ToolTree(props: {
: (props.emptyLabel ?? "No tools available")}
) : groupByConnection ? (
- accountGroups.map((group) => (
-
-
- {ownerDisplay.showOwnerLabels ? (
-
- {ownerLabel(group.owner)}
-
- ) : null}
-
- {group.integration && group.connection
- ? `${group.integration} / ${group.connection}`
- : group.connection || ownerDisplay.label(group.owner)}
-
-
- {group.tools.length}
-
-
-
-
- ))
+ accountGroups.map((group) => {
+ // Rows inside an account section author rules for THAT account
+ // only: two connections of one integration are different
+ // credentials, so a rule set under one must not govern the other.
+ const accountPattern = accountPolicyPattern(group.owner, group.connection);
+ const wholeAccountPattern = accountPattern(`${group.integration}.*`);
+ const wholeAccountRule = exactPatterns.get(wholeAccountPattern);
+ const accountLabel =
+ group.integration && group.connection
+ ? `${group.integration} / ${group.connection}`
+ : group.connection || ownerDisplay.label(group.owner);
+ return (
+
+
+ {ownerDisplay.showOwnerLabels ? (
+
+ {ownerLabel(group.owner)}
+
+ ) : null}
+
+ {accountLabel}
+
+
+ {group.tools.length}
+
+ {onSetPolicy && group.integration && group.connection ? (
+
+ ) : null}
+
+
+
+ );
+ })
) : (
)}
diff --git a/packages/react/src/lib/integration-add.tsx b/packages/react/src/lib/integration-add.tsx
index 1b1ddbac96..968bc0f3f9 100644
--- a/packages/react/src/lib/integration-add.tsx
+++ b/packages/react/src/lib/integration-add.tsx
@@ -11,20 +11,14 @@ import { Link } from "@tanstack/react-router";
import * as Exit from "effect/Exit";
import * as Option from "effect/Option";
import * as Predicate from "effect/Predicate";
-import * as Schema from "effect/Schema";
import * as AsyncResult from "effect/unstable/reactivity/AsyncResult";
import { integrationsOptimisticAtom } from "../api/atoms";
-
-const ErrorMessage = Schema.Struct({ message: Schema.String });
-const decodeErrorMessage = Schema.decodeUnknownOption(ErrorMessage);
+import { messageFromExit } from "../api/error-reporting";
/** The failed Exit's `message`, or `fallback` when the error carries none. */
-export const errorMessageFromExit = (exit: Exit.Exit, fallback: string): string =>
- Option.match(Option.flatMap(Exit.findErrorOption(exit), decodeErrorMessage), {
- onNone: () => fallback,
- onSome: ({ message }) => message,
- });
+export const errorMessageFromExit: (exit: Exit.Exit, fallback: string) => string =
+ messageFromExit;
export const isIntegrationAlreadyExistsExit = (exit: Exit.Exit): boolean =>
Option.match(Exit.findErrorOption(exit), {
diff --git a/packages/react/src/lib/policy-pattern.test.ts b/packages/react/src/lib/policy-pattern.test.ts
new file mode 100644
index 0000000000..0c3604eb0a
--- /dev/null
+++ b/packages/react/src/lib/policy-pattern.test.ts
@@ -0,0 +1,21 @@
+import { describe, expect, it } from "@effect/vitest";
+
+import { accountPolicyPattern, toPolicyPattern } from "./policy-pattern";
+
+describe("policy pattern bridges", () => {
+ it("wildcards owner and connection for the connection-agnostic tree", () => {
+ expect(toPolicyPattern("slack.conversations.history")).toBe("slack.*.*.conversations.history");
+ expect(toPolicyPattern("slack.conversations.*")).toBe("slack.*.*.conversations.*");
+ expect(toPolicyPattern("slack.*")).toBe("slack.*");
+ expect(toPolicyPattern("*")).toBe("*");
+ });
+
+ it("pins owner and connection for a row inside an account section", () => {
+ const forBot = accountPolicyPattern("org", "bot");
+ expect(forBot("slack.conversations.history")).toBe("slack.org.bot.conversations.history");
+ expect(forBot("slack.conversations.*")).toBe("slack.org.bot.conversations.*");
+ expect(forBot("slack.*")).toBe("slack.org.bot.*");
+ expect(forBot("slack")).toBe("slack.org.bot.*");
+ expect(forBot("*")).toBe("*");
+ });
+});
diff --git a/packages/react/src/lib/policy-pattern.ts b/packages/react/src/lib/policy-pattern.ts
index 7a274f4296..c2727f38b4 100644
--- a/packages/react/src/lib/policy-pattern.ts
+++ b/packages/react/src/lib/policy-pattern.ts
@@ -1,4 +1,4 @@
-import { matchPattern } from "@executor-js/sdk/shared";
+import { matchPattern, type Owner } from "@executor-js/sdk/shared";
// ---------------------------------------------------------------------------
// Policy pattern bridge.
@@ -29,3 +29,29 @@ export const toPolicyPattern = (displayPattern: string): string => {
};
export { matchPattern };
+
+// ---------------------------------------------------------------------------
+// Account-scoped bridge.
+//
+// The account-grouped Tools tab shows the same tool once per connection. A
+// rule authored from a row inside one account's section must govern THAT
+// account only — a Slack bot connection and a Slack user connection are
+// different credentials with different capabilities, and blocking a tool on
+// one must not silently block it on the other. So instead of wildcarding the
+// owner + connection segments, fill them in: `integration...`.
+// `integration.*` becomes the whole-account subtree
+// `integration...*`. Apply the returned mapper at both the site
+// that BUILDS a pattern and the site that LOOKS UP the exact rule, exactly as
+// with `toPolicyPattern`.
+// ---------------------------------------------------------------------------
+
+export const accountPolicyPattern =
+ (owner: Owner, connection: string) =>
+ (displayPattern: string): string => {
+ if (displayPattern === "*") return "*";
+ const firstDot = displayPattern.indexOf(".");
+ if (firstDot === -1) return `${displayPattern}.${owner}.${connection}.*`;
+ const integration = displayPattern.slice(0, firstDot);
+ const rest = displayPattern.slice(firstDot + 1);
+ return `${integration}.${owner}.${connection}.${rest}`;
+ };
diff --git a/packages/react/src/pages/integration-detail.tsx b/packages/react/src/pages/integration-detail.tsx
index f88412b3e3..a30d8da9d6 100644
--- a/packages/react/src/pages/integration-detail.tsx
+++ b/packages/react/src/pages/integration-detail.tsx
@@ -13,6 +13,7 @@ import {
effectivePolicyFromSorted,
type Connection,
type Owner,
+ type ToolPolicyAction,
} from "@executor-js/sdk/shared";
import {
checkConnectionHealth,
@@ -46,6 +47,7 @@ import { useExecutorDocumentTitle } from "../lib/document-title";
import { ErrorState } from "../components/error-state";
import { isAsyncResultLoading } from "../lib/async-result";
import { useConnectionsHealth } from "../lib/use-connection-health";
+import { accountPolicyPattern } from "../lib/policy-pattern";
import {
integrationDetailInternalTabFromSearch,
type IntegrationDetailInternalTab,
@@ -142,6 +144,16 @@ export function IntegrationDetailPage(props: {
// Integrations are workspace-owned; the server refuses catalog mutations
// (update/remove) from non-admin members, so disable the controls for them.
const canMutateIntegration = useCanCreateWorkspaceConnections();
+ // Tool policies on this tab are workspace rules (`usePolicyActions("org")`),
+ // which the server refuses for non-admin members. Offer the menus only to
+ // those who can actually write them.
+ const canSetPolicy = canMutateIntegration;
+ const onSetPolicy = canSetPolicy
+ ? (pattern: string, action: ToolPolicyAction) => void policyActions.set(pattern, action)
+ : undefined;
+ const onClearPolicy = canSetPolicy
+ ? (pattern: string, policyId?: string) => void policyActions.clear(pattern, policyId)
+ : undefined;
const canEdit = !isBuiltInIntegration && integrationData !== null;
const canRefresh = integrationData?.canRefresh ?? false;
const canRemove = integrationData?.canRemove ?? false;
@@ -301,6 +313,7 @@ export function IntegrationDetailPage(props: {
policy: effectivePolicyFromSorted(matchId, policyList, t.requiresApproval),
owner: t.owner,
connection: t.connection,
+ integration: t.integration,
};
});
}, [tools, policyList]);
@@ -620,8 +633,8 @@ export function IntegrationDetailPage(props: {
tools={integrationTools}
selectedToolId={selectedToolId}
onSelect={setSelectedToolId}
- onSetPolicy={(pattern, action) => void policyActions.set(pattern, action)}
- onClearPolicy={(pattern) => void policyActions.clear(pattern)}
+ onSetPolicy={onSetPolicy}
+ onClearPolicy={onClearPolicy}
policies={sortedPolicies}
groupByConnection={!isBuiltInIntegration}
emptyLabel={hasToolSyncIssue ? emptyToolsTitle : undefined}
@@ -636,9 +649,15 @@ export function IntegrationDetailPage(props: {
toolName={selectedTool.name}
staticTool={selection?.static}
policy={selectedTool.policy}
- onSetPolicy={(pattern, action) => void policyActions.set(pattern, action)}
- onClearPolicy={(pattern, policyId) =>
- void policyActions.clear(pattern, policyId)
+ onSetPolicy={onSetPolicy}
+ onClearPolicy={onClearPolicy}
+ // The header badge must write and look up the SAME
+ // account-pinned pattern the tree row under this
+ // account uses, or it cannot recognize its own rule.
+ patternForDisplay={
+ selection && !selection.static
+ ? accountPolicyPattern(selection.owner, selection.connection)
+ : undefined
}
{...(!selection?.static && selectedBareName
? {
diff --git a/packages/react/src/pages/tools.tsx b/packages/react/src/pages/tools.tsx
index 60e6e0fa9a..cdb05d3e38 100644
--- a/packages/react/src/pages/tools.tsx
+++ b/packages/react/src/pages/tools.tsx
@@ -2,7 +2,11 @@ import { useMemo, useState } from "react";
import { Link } from "@tanstack/react-router";
import { useAtomRefresh, useAtomValue } from "@effect/atom-react";
import * as AsyncResult from "effect/unstable/reactivity/AsyncResult";
-import { ToolAddress, effectivePolicyFromSorted } from "@executor-js/sdk/shared";
+import {
+ ToolAddress,
+ effectivePolicyFromSorted,
+ type ToolPolicyAction,
+} from "@executor-js/sdk/shared";
import { policiesOptimisticAtom, toolsAllAtom } from "../api/atoms";
import { usePolicyActions } from "../hooks/use-policy-actions";
@@ -13,6 +17,7 @@ import { Skeleton } from "../components/skeleton";
import { useExecutorDocumentTitle } from "../lib/document-title";
import { ErrorState } from "../components/error-state";
import { isAsyncResultLoading } from "../lib/async-result";
+import { useCanCreateWorkspaceConnections } from "../multiplayer/use-admin-nav";
// Dynamic tool policy patterns are derived from the connection-aware address.
// Static tools (for example Executor's own tools) use their address directly.
@@ -38,6 +43,15 @@ export function ToolsPage() {
const refreshTools = useAtomRefresh(toolsAllAtom);
const policies = useAtomValue(policiesOptimisticAtom);
const policyActions = usePolicyActions("org");
+ // Policies here are workspace rules, which the server refuses for non-admin
+ // members. Offer the menus only to those who can actually write them.
+ const canSetPolicy = useCanCreateWorkspaceConnections();
+ const onSetPolicy = canSetPolicy
+ ? (pattern: string, action: ToolPolicyAction) => void policyActions.set(pattern, action)
+ : undefined;
+ const onClearPolicy = canSetPolicy
+ ? (pattern: string, policyId?: string) => void policyActions.clear(pattern, policyId)
+ : undefined;
const [selectedToolId, setSelectedToolId] = useState(null);
@@ -144,8 +158,8 @@ export function ToolsPage() {
tools={summaries}
selectedToolId={selectedToolId}
onSelect={setSelectedToolId}
- onSetPolicy={(pattern, action) => void policyActions.set(pattern, action)}
- onClearPolicy={(pattern) => void policyActions.clear(pattern)}
+ onSetPolicy={onSetPolicy}
+ onClearPolicy={onClearPolicy}
policies={sortedPolicies}
/>
@@ -156,10 +170,8 @@ export function ToolsPage() {
toolName={selectedTool.name}
staticTool={selection?.static}
policy={selectedTool.policy}
- onSetPolicy={(pattern, action) => void policyActions.set(pattern, action)}
- onClearPolicy={(pattern, policyId) =>
- void policyActions.clear(pattern, policyId)
- }
+ onSetPolicy={onSetPolicy}
+ onClearPolicy={onClearPolicy}
/>
) : (
0} />
From 24adb127f304e2b145b462c3805a9e11b2a524e0 Mon Sep 17 00:00:00 2001
From: Rhys Sullivan <39114868+RhysSullivan@users.noreply.github.com>
Date: Fri, 18 Sep 2026 12:24:09 -0700
Subject: [PATCH 09/11] Accept Slack bot and user OAuth grants (#2050)
* Accept Slack bot and user OAuth grants
* Preserve standard bearer response metadata
---
.changeset/quiet-connection-setup.md | 5 +
e2e/scenarios/connection-setup-ux.test.ts | 137 ++++++++++++++++++
packages/core/sdk/src/oauth-helpers.test.ts | 153 ++++++++++++++++++++
packages/core/sdk/src/oauth-helpers.ts | 64 +++++++-
4 files changed, 356 insertions(+), 3 deletions(-)
create mode 100644 .changeset/quiet-connection-setup.md
create mode 100644 e2e/scenarios/connection-setup-ux.test.ts
diff --git a/.changeset/quiet-connection-setup.md b/.changeset/quiet-connection-setup.md
new file mode 100644
index 0000000000..9d75373928
--- /dev/null
+++ b/.changeset/quiet-connection-setup.md
@@ -0,0 +1,5 @@
+---
+"executor": patch
+---
+
+Accept Slack bot and user OAuth token envelopes during sign-in and token refresh.
diff --git a/e2e/scenarios/connection-setup-ux.test.ts b/e2e/scenarios/connection-setup-ux.test.ts
new file mode 100644
index 0000000000..8158a9edbd
--- /dev/null
+++ b/e2e/scenarios/connection-setup-ux.test.ts
@@ -0,0 +1,137 @@
+import { randomBytes } from "node:crypto";
+import { expect } from "@effect/vitest";
+import { Effect } from "effect";
+import { composePluginApi } from "@executor-js/api/server";
+import { connectEmulator } from "@executor-js/emulate";
+import { openApiHttpPlugin } from "@executor-js/plugin-openapi/api";
+import { IntegrationSlug, OAuthClientSlug } from "@executor-js/sdk/shared";
+import { variable } from "@executor-js/sdk/http-auth";
+import { createEmulatorInstance } from "../src/emulator-instance";
+import { scenario } from "../src/scenario";
+import { Api, Browser, Target } from "../src/services";
+import { visit } from "../src/surfaces/browser";
+
+const api = composePluginApi([openApiHttpPlugin()] as const);
+// Each journey has its own real provider state, OAuth app, user and integration.
+const fixture = Effect.gen(function* () {
+ const target = yield* Target;
+ const browser = yield* Browser;
+ const { client: makeClient } = yield* Api;
+ const identity = yield* target.newIdentity();
+ const client = yield* makeClient(api, identity);
+ const slug = IntegrationSlug.make(`setup-${randomBytes(4).toString("hex")}`);
+ const app = OAuthClientSlug.make(`${slug}-app`);
+ const baseUrl = yield* createEmulatorInstance("slack", "connection-setup");
+ const emulator = yield* Effect.promise(() => connectEmulator({ baseUrl }));
+ const credential = yield* Effect.promise(() =>
+ emulator.credentials.mint({
+ type: "oauth-authorization-code",
+ redirect_uris: [new URL("/api/oauth/callback", target.baseUrl).toString()],
+ }),
+ );
+ const {
+ client_id: clientId,
+ client_secret: clientSecret,
+ authorization_url: authorizationUrl,
+ token_url: tokenUrl,
+ } = credential;
+ if (!clientId || !clientSecret || !authorizationUrl || !tokenUrl) {
+ return yield* Effect.die("Slack emulator did not mint an OAuth app");
+ }
+ yield* Effect.addFinalizer(() =>
+ Effect.gen(function* () {
+ const connections = yield* client.connections.list({ query: { integration: slug } });
+ for (const connection of connections) {
+ yield* client.connections
+ .remove({
+ params: {
+ owner: connection.owner,
+ integration: slug,
+ name: connection.name,
+ },
+ })
+ .pipe(Effect.ignore);
+ }
+ yield* client.oauth
+ .removeClient({ params: { slug: app }, payload: { owner: "org" } })
+ .pipe(Effect.ignore);
+ yield* client.openapi.removeSpec({ params: { slug } }).pipe(Effect.ignore);
+ }).pipe(Effect.ignore),
+ );
+ yield* client.openapi.addSpec({
+ payload: {
+ slug,
+ name: "Team chat",
+ baseUrl: "https://slack.com",
+ displayDomain: "slack.com",
+ spec: {
+ kind: "blob",
+ value: JSON.stringify({
+ openapi: "3.0.3",
+ info: { title: "Team chat", version: "1" },
+ // No API operations: only the isolated emulator receives OAuth traffic.
+ servers: [{ url: "https://slack.com" }],
+ paths: {},
+ }),
+ },
+ authenticationTemplate: [
+ {
+ slug: "token",
+ type: "apiKey",
+ headers: { Authorization: ["Bearer ", variable("token")] },
+ },
+ { slug: "oauth", kind: "oauth2", authorizationUrl, tokenUrl, scopes: ["users:read"] },
+ ],
+ },
+ });
+ yield* client.oauth.createClient({
+ payload: {
+ slug: app,
+ owner: "org",
+ grant: "authorization_code",
+ clientId,
+ clientSecret,
+ authorizationUrl,
+ tokenUrl,
+ originIntegration: slug,
+ },
+ });
+ return { target, browser, identity, client, slug, emulator };
+});
+
+scenario(
+ "Slack OAuth · provider consent saves a connection",
+ {},
+ Effect.scoped(
+ Effect.gen(function* () {
+ const { browser, identity, slug, client, emulator } = yield* fixture;
+ yield* browser.session(identity, async ({ page, step }) => {
+ await step("Sign in with a provider account without naming the connection", async () => {
+ await visit(page, `/integrations/${slug}?addAccount=1`);
+ await page.getByRole("tab", { name: "OAuth2", exact: true }).click();
+ const opened = page.waitForEvent("popup");
+ await page.getByRole("button", { name: "Connect with OAuth", exact: true }).click();
+ const popup = await opened;
+ await popup.waitForURL(/oauth\/v2\/authorize/);
+ // The hosted emulator renders a root-relative form action. Rebase only
+ // that provider transport onto this run's isolated instance.
+ await popup.route("https://emulators.dev/oauth/v2/authorize/callback", (route) =>
+ route.continue({ url: `${emulator.baseUrl}/oauth/v2/authorize/callback` }),
+ );
+ await popup.getByRole("button", { name: /admin/ }).click();
+ await page
+ .getByRole("heading", { name: /Add connection/ })
+ .waitFor({ state: "hidden", timeout: 30_000 });
+ });
+ });
+ const connections = yield* client.connections.list({ query: { integration: slug } });
+ expect(connections, "the completed callback persists the new account").toHaveLength(1);
+ expect(connections[0]?.name).toBeTruthy();
+ const ledger = yield* Effect.promise(() => emulator.ledger.list());
+ expect(
+ ledger.some((entry) => entry.method === "POST" && entry.path.includes("oauth.v2.access")),
+ "the real provider exchanged an authorization code",
+ ).toBe(true);
+ }),
+ ),
+);
diff --git a/packages/core/sdk/src/oauth-helpers.test.ts b/packages/core/sdk/src/oauth-helpers.test.ts
index d94e8c34ad..92bc623d06 100644
--- a/packages/core/sdk/src/oauth-helpers.test.ts
+++ b/packages/core/sdk/src/oauth-helpers.test.ts
@@ -621,6 +621,7 @@ describe("exchangeAuthorizationCode", () => {
it.effect("uses nested granted scopes for Slack-style user token responses", () =>
withTokenEndpoint(
tokenResponse({
+ ok: true,
access_token: "xoxp-user-token",
token_type: "Bearer",
scope: "",
@@ -1838,3 +1839,155 @@ describe("OAuth2Error tagging", () => {
});
});
});
+
+// Slack labels bearer credentials by actor type. The same envelope is used
+// during authorization-code exchange and refresh-token rotation.
+describe("Provider token envelopes", () => {
+ const grants = [
+ {
+ label: "standard bearer response with ok metadata",
+ body: {
+ ok: true,
+ access_token: "provider-token",
+ token_type: "Bearer",
+ scope: "scope,with-comma other.scope",
+ },
+ expected: {
+ access_token: "provider-token",
+ token_type: "bearer",
+ scope: "scope,with-comma other.scope",
+ },
+ },
+ {
+ // https://docs.slack.dev/reference/methods/oauth.v2.access/
+ // A single response can contain two distinct accounts and refresh tokens.
+ label: "Slack bot and user response",
+ body: {
+ ok: true,
+ access_token: "bot-token",
+ token_type: "bot",
+ scope: "commands,incoming-webhook",
+ expires_in: 43200,
+ refresh_token: "bot-refresh",
+ authed_user: {
+ access_token: "user-token",
+ token_type: "user",
+ scope: "chat:write",
+ expires_in: 43200,
+ refresh_token: "user-refresh",
+ },
+ },
+ expected: {
+ access_token: "bot-token",
+ token_type: "bearer",
+ scope: "commands incoming-webhook",
+ expires_in: 43200,
+ refresh_token: "bot-refresh",
+ },
+ },
+ {
+ label: "bot",
+ body: {
+ ok: true,
+ access_token: "bot-token",
+ token_type: "bot",
+ scope: "channels:read,chat:write",
+ refresh_token: "bot-refresh",
+ expires_in: 3600,
+ },
+ expected: {
+ access_token: "bot-token",
+ token_type: "bearer",
+ scope: "channels:read chat:write",
+ refresh_token: "bot-refresh",
+ expires_in: 3600,
+ },
+ },
+ {
+ label: "user",
+ body: {
+ ok: true,
+ access_token: "user-token",
+ token_type: "user",
+ scope: "users:read,users:read.email",
+ refresh_token: "user-refresh",
+ expires_in: 3600,
+ },
+ expected: {
+ access_token: "user-token",
+ token_type: "bearer",
+ scope: "users:read users:read.email",
+ refresh_token: "user-refresh",
+ expires_in: 3600,
+ },
+ },
+ {
+ label: "nested user",
+ body: {
+ ok: true,
+ authed_user: {
+ access_token: "nested-user-token",
+ token_type: "user",
+ scope: "users:read,chat:write",
+ refresh_token: "nested-refresh",
+ expires_in: 3600,
+ },
+ },
+ expected: {
+ access_token: "nested-user-token",
+ token_type: "bearer",
+ scope: "users:read chat:write",
+ refresh_token: "nested-refresh",
+ expires_in: 3600,
+ },
+ },
+ ];
+ for (const grant of grants) {
+ it.effect(`exchanges a ${grant.label} grant`, () =>
+ withTokenEndpoint(tokenResponse(grant.body), ({ tokenUrl }) =>
+ Effect.gen(function* () {
+ const result = yield* exchangeAuthorizationCode({
+ tokenUrl,
+ clientId: "cid",
+ clientSecret: "secret",
+ redirectUrl: "https://app.example/callback",
+ codeVerifier: "verifier",
+ code: "code",
+ });
+ expect(result).toMatchObject(grant.expected);
+ }),
+ ),
+ );
+ it.effect(`refreshes a ${grant.label} grant`, () =>
+ withTokenEndpoint(tokenResponse(grant.body), ({ tokenUrl }) =>
+ Effect.gen(function* () {
+ const result = yield* refreshAccessToken({
+ tokenUrl,
+ clientId: "cid",
+ clientSecret: "secret",
+ refreshToken: "old-refresh",
+ });
+ expect(result).toMatchObject(grant.expected);
+ }),
+ ),
+ );
+ }
+ it.effect("still rejects unsupported token types in an otherwise successful envelope", () =>
+ withTokenEndpoint(
+ tokenResponse({ ok: true, access_token: "token", token_type: "mac" }),
+ ({ tokenUrl }) =>
+ Effect.gen(function* () {
+ const exit = yield* Effect.exit(
+ exchangeAuthorizationCode({
+ tokenUrl,
+ clientId: "cid",
+ redirectUrl: "https://app.example/callback",
+ codeVerifier: "verifier",
+ code: "code",
+ }),
+ );
+ expect(Exit.isFailure(exit)).toBe(true);
+ }),
+ ),
+ );
+});
diff --git a/packages/core/sdk/src/oauth-helpers.ts b/packages/core/sdk/src/oauth-helpers.ts
index c6e3209fe7..b8b11714c2 100644
--- a/packages/core/sdk/src/oauth-helpers.ts
+++ b/packages/core/sdk/src/oauth-helpers.ts
@@ -1117,16 +1117,74 @@ const stripIdToken = async (response: Response): Promise
};
};
+const SlackGrant = Schema.Struct({
+ access_token: Schema.optional(Schema.String),
+ token_type: Schema.optional(Schema.Literals(["bot", "user", "Bearer", "bearer"])),
+ refresh_token: Schema.optional(Schema.String),
+ expires_in: Schema.optional(Schema.Number),
+ scope: Schema.optional(Schema.String),
+});
+const decodeSlackEnvelope = Schema.decodeUnknownOption(
+ Schema.Struct({
+ ...SlackGrant.fields,
+ ok: Schema.Literal(true),
+ authed_user: Schema.optional(SlackGrant),
+ }),
+);
+
+/** Slack's `bot` and `user` values identify the account, not an HTTP auth
+ * scheme. Project its successful envelope to an RFC 6749 bearer grant before
+ * oauth4webapi validates it. User-only grants may live entirely in authed_user;
+ * never replace a populated top-level grant with another account's grant. */
+const normalizeSlackTokenEnvelope = async (response: Response): Promise => {
+ const decoded = decodeSlackEnvelope(await safeJsonFromResponse(response));
+ if (Option.isNone(decoded)) return response;
+ const envelope = decoded.value;
+ const user = envelope.authed_user;
+ const grant =
+ user?.access_token !== undefined &&
+ (envelope.access_token === undefined || !envelope.scope?.trim())
+ ? user
+ : envelope;
+ // Standard bearer responses may also contain `ok: true`. Preserve their
+ // scopes and provider metadata; only Slack's actor token types need adapting.
+ if (
+ grant.access_token === undefined ||
+ (grant.token_type !== "bot" && grant.token_type !== "user")
+ ) {
+ return response;
+ }
+ const scope = grant.scope
+ ?.split(/[\s,]+/)
+ .filter(Boolean)
+ .join(" ");
+ return new Response(
+ JSON.stringify({
+ access_token: grant.access_token,
+ token_type: "Bearer",
+ refresh_token: grant.refresh_token,
+ expires_in: grant.expires_in,
+ ...(scope ? { scope } : {}),
+ }),
+ {
+ status: response.status,
+ statusText: response.statusText,
+ headers: response.headers,
+ },
+ );
+};
+
const processTokenEndpointResponse = async (
as: oauth.AuthorizationServer,
client: oauth.Client,
response: Response,
): Promise => {
const stripped = await stripIdToken(response);
- const providerUserGrant = await nestedAuthedUserGrant(stripped.response);
+ const normalizedResponse = await normalizeSlackTokenEnvelope(stripped.response);
+ const providerUserGrant = await nestedAuthedUserGrant(normalizedResponse);
const parsed = tokenResponseFrom(
as,
- await oauth.processGenericTokenEndpointResponse(as, client, stripped.response),
+ await oauth.processGenericTokenEndpointResponse(as, client, normalizedResponse),
);
const token =
parsed.scope === undefined && providerUserGrant !== undefined
@@ -1442,7 +1500,7 @@ export const refreshAccessToken = (
const result = await oauth.processRefreshTokenResponse(
as,
client,
- (await stripIdToken(response)).response,
+ await normalizeSlackTokenEnvelope((await stripIdToken(response)).response),
);
return tokenResponseFrom(as, result);
},
From b8c29700bdefd453d3010882889be87d54c24509 Mon Sep 17 00:00:00 2001
From: Rhys Sullivan <39114868+RhysSullivan@users.noreply.github.com>
Date: Fri, 18 Sep 2026 12:38:57 -0700
Subject: [PATCH 10/11] Document the 20-connection application pool (#2064)
---
apps/cloud/docs/database-connections.md | 9 ++++++---
1 file changed, 6 insertions(+), 3 deletions(-)
diff --git a/apps/cloud/docs/database-connections.md b/apps/cloud/docs/database-connections.md
index 12c677b775..e0304c89e6 100644
--- a/apps/cloud/docs/database-connections.md
+++ b/apps/cloud/docs/database-connections.md
@@ -11,16 +11,19 @@ The connection budget is:
| PostgreSQL max_connections | 50 |
| PostgreSQL superuser_reserved_connections | 3 |
| Local PgBouncer processes | 1 |
-| PgBouncer default_pool_size | 12 |
-| PgBouncer max_db_connections | 12 |
+| PgBouncer default_pool_size | 20 |
+| PgBouncer max_db_connections | 20 |
| PgBouncer max_client_conn | 400 |
| PgBouncer max_prepared_statements | 200 |
-| Hyperdrive origin connection limit | 12 (soft limit) |
+| Hyperdrive origin connection limit | 20 (soft limit) |
Hyperdrive's origin limit is advisory. PgBouncer's database limit enforces the
backend budget across users of one database. The cap is per PgBouncer process:
adding processes, databases, direct clients, or other poolers requires a new
aggregate budget. Keep capacity for provider sessions, deploys and administration.
+The 20-connection application budget leaves 27 ordinary slots for those clients
+after the three superuser-reserved slots. This is a concurrency ceiling, not a
+target for active queries; check CPU, queue waits and latency before raising it.
Prepared statements require protocol-level support to remain enabled in PgBouncer.
The migration and membership-readiness scripts retry only the initial `SELECT 1`
From c6d10838f741ece700922fb2469782f79753b7f7 Mon Sep 17 00:00:00 2001
From: Rhys Sullivan <39114868+RhysSullivan@users.noreply.github.com>
Date: Fri, 18 Sep 2026 13:09:17 -0700
Subject: [PATCH 11/11] Prefer OAuth when a matching client is available
(#2066)
* Prefer OAuth when adding connections
* Verify authentication method selection stays usable
* Check client availability before preferring OAuth
---
.changeset/connection-oauth-default.md | 5 +
e2e/scenarios/connection-setup-ux.test.ts | 317 +++++++++++++-----
.../src/components/add-account-modal.test.ts | 67 ++++
.../src/components/add-account-modal.tsx | 76 +++--
4 files changed, 354 insertions(+), 111 deletions(-)
create mode 100644 .changeset/connection-oauth-default.md
diff --git a/.changeset/connection-oauth-default.md b/.changeset/connection-oauth-default.md
new file mode 100644
index 0000000000..35cfce58c2
--- /dev/null
+++ b/.changeset/connection-oauth-default.md
@@ -0,0 +1,5 @@
+---
+"executor": patch
+---
+
+Prefer browser sign-in when a matching OAuth client is available, while preserving a user’s chosen method when clients finish loading.
diff --git a/e2e/scenarios/connection-setup-ux.test.ts b/e2e/scenarios/connection-setup-ux.test.ts
index 8158a9edbd..fa87ea93d6 100644
--- a/e2e/scenarios/connection-setup-ux.test.ts
+++ b/e2e/scenarios/connection-setup-ux.test.ts
@@ -9,95 +9,108 @@ import { variable } from "@executor-js/sdk/http-auth";
import { createEmulatorInstance } from "../src/emulator-instance";
import { scenario } from "../src/scenario";
import { Api, Browser, Target } from "../src/services";
-import { visit } from "../src/surfaces/browser";
+import { hydrated, visit } from "../src/surfaces/browser";
const api = composePluginApi([openApiHttpPlugin()] as const);
// Each journey has its own real provider state, OAuth app, user and integration.
-const fixture = Effect.gen(function* () {
- const target = yield* Target;
- const browser = yield* Browser;
- const { client: makeClient } = yield* Api;
- const identity = yield* target.newIdentity();
- const client = yield* makeClient(api, identity);
- const slug = IntegrationSlug.make(`setup-${randomBytes(4).toString("hex")}`);
- const app = OAuthClientSlug.make(`${slug}-app`);
- const baseUrl = yield* createEmulatorInstance("slack", "connection-setup");
- const emulator = yield* Effect.promise(() => connectEmulator({ baseUrl }));
- const credential = yield* Effect.promise(() =>
- emulator.credentials.mint({
- type: "oauth-authorization-code",
- redirect_uris: [new URL("/api/oauth/callback", target.baseUrl).toString()],
- }),
- );
- const {
- client_id: clientId,
- client_secret: clientSecret,
- authorization_url: authorizationUrl,
- token_url: tokenUrl,
- } = credential;
- if (!clientId || !clientSecret || !authorizationUrl || !tokenUrl) {
- return yield* Effect.die("Slack emulator did not mint an OAuth app");
- }
- yield* Effect.addFinalizer(() =>
- Effect.gen(function* () {
- const connections = yield* client.connections.list({ query: { integration: slug } });
- for (const connection of connections) {
- yield* client.connections
- .remove({
- params: {
- owner: connection.owner,
- integration: slug,
- name: connection.name,
- },
- })
+const connectionFixture = (registerClient: boolean) =>
+ Effect.gen(function* () {
+ const target = yield* Target;
+ const browser = yield* Browser;
+ const { client: makeClient } = yield* Api;
+ const identity = yield* target.newIdentity();
+ const client = yield* makeClient(api, identity);
+ const slug = IntegrationSlug.make(`setup-${randomBytes(4).toString("hex")}`);
+ const app = OAuthClientSlug.make(`${slug}-app`);
+ const baseUrl = yield* createEmulatorInstance("slack", "connection-setup");
+ const emulator = yield* Effect.promise(() => connectEmulator({ baseUrl }));
+ const credential = yield* Effect.promise(() =>
+ emulator.credentials.mint({
+ type: "oauth-authorization-code",
+ redirect_uris: [new URL("/api/oauth/callback", target.baseUrl).toString()],
+ }),
+ );
+ const {
+ client_id: clientId,
+ client_secret: clientSecret,
+ authorization_url: authorizationUrl,
+ token_url: tokenUrl,
+ } = credential;
+ if (!clientId || !clientSecret || !authorizationUrl || !tokenUrl) {
+ return yield* Effect.die("Slack emulator did not mint an OAuth app");
+ }
+ yield* Effect.addFinalizer(() =>
+ Effect.gen(function* () {
+ const connections = yield* client.connections.list({ query: { integration: slug } });
+ for (const connection of connections) {
+ yield* client.connections
+ .remove({
+ params: {
+ owner: connection.owner,
+ integration: slug,
+ name: connection.name,
+ },
+ })
+ .pipe(Effect.ignore);
+ }
+ yield* client.oauth
+ .removeClient({ params: { slug: app }, payload: { owner: "org" } })
.pipe(Effect.ignore);
- }
- yield* client.oauth
- .removeClient({ params: { slug: app }, payload: { owner: "org" } })
- .pipe(Effect.ignore);
- yield* client.openapi.removeSpec({ params: { slug } }).pipe(Effect.ignore);
- }).pipe(Effect.ignore),
- );
- yield* client.openapi.addSpec({
- payload: {
- slug,
- name: "Team chat",
- baseUrl: "https://slack.com",
- displayDomain: "slack.com",
- spec: {
- kind: "blob",
- value: JSON.stringify({
- openapi: "3.0.3",
- info: { title: "Team chat", version: "1" },
- // No API operations: only the isolated emulator receives OAuth traffic.
- servers: [{ url: "https://slack.com" }],
- paths: {},
- }),
+ yield* client.openapi.removeSpec({ params: { slug } }).pipe(Effect.ignore);
+ }).pipe(Effect.ignore),
+ );
+ yield* client.openapi.addSpec({
+ payload: {
+ slug,
+ name: "Team chat",
+ baseUrl: "https://slack.com",
+ displayDomain: "slack.com",
+ spec: {
+ kind: "blob",
+ value: JSON.stringify({
+ openapi: "3.0.3",
+ info: { title: "Team chat", version: "1" },
+ // No API operations: only the isolated emulator receives OAuth traffic.
+ servers: [{ url: "https://slack.com" }],
+ paths: {},
+ }),
+ },
+ authenticationTemplate: [
+ {
+ slug: "token",
+ type: "apiKey",
+ headers: { Authorization: ["Bearer ", variable("token")] },
+ },
+ {
+ slug: "oauth",
+ kind: "oauth2",
+ // The unconfigured case tests metadata only, without contacting a
+ // provider. A unique reserved host cannot match another test's app.
+ authorizationUrl: registerClient
+ ? authorizationUrl
+ : `https://${slug}.invalid/authorize`,
+ tokenUrl: registerClient ? tokenUrl : `https://${slug}.invalid/token`,
+ scopes: ["users:read"],
+ },
+ ],
},
- authenticationTemplate: [
- {
- slug: "token",
- type: "apiKey",
- headers: { Authorization: ["Bearer ", variable("token")] },
+ });
+ if (registerClient)
+ yield* client.oauth.createClient({
+ payload: {
+ slug: app,
+ owner: "org",
+ grant: "authorization_code",
+ clientId,
+ clientSecret,
+ authorizationUrl,
+ tokenUrl,
+ originIntegration: slug,
},
- { slug: "oauth", kind: "oauth2", authorizationUrl, tokenUrl, scopes: ["users:read"] },
- ],
- },
- });
- yield* client.oauth.createClient({
- payload: {
- slug: app,
- owner: "org",
- grant: "authorization_code",
- clientId,
- clientSecret,
- authorizationUrl,
- tokenUrl,
- originIntegration: slug,
- },
+ });
+ return { target, browser, identity, client, slug, emulator };
});
- return { target, browser, identity, client, slug, emulator };
-});
+const fixture = connectionFixture(true);
scenario(
"Slack OAuth · provider consent saves a connection",
@@ -135,3 +148,143 @@ scenario(
}),
),
);
+
+scenario(
+ "Connection setup · without a matching client the API key stays the default",
+ {},
+ Effect.scoped(
+ Effect.gen(function* () {
+ const { browser, identity, slug } = yield* connectionFixture(false);
+ yield* browser.session(identity, async ({ page, step }) => {
+ await step("Open an integration whose OAuth app has not been configured", async () => {
+ await visit(page, `/integrations/${slug}?addAccount=1`);
+ expect(
+ await page
+ .getByRole("tab", { name: "API key (Authorization)", exact: true })
+ .getAttribute("aria-selected"),
+ "declaring OAuth without a usable client must not replace the key form",
+ ).toBe("true");
+ await page.getByRole("tab", { name: "OAuth2", exact: true }).click();
+ await page.getByRole("button", { name: "Register app", exact: true }).waitFor();
+ });
+ });
+ }),
+ ),
+);
+
+for (const { interaction, expectedOAuth, expectedKeys } of [
+ { interaction: "untouched", expectedOAuth: "true", expectedKeys: [] },
+ { interaction: "select API key", expectedOAuth: "false", expectedKeys: [""] },
+ {
+ interaction: "enter API key",
+ expectedOAuth: "false",
+ expectedKeys: ["synthetic-key-in-progress"],
+ },
+] as const) {
+ scenario(
+ `Connection setup · client list arrives with the form ${interaction}`,
+ {},
+ Effect.scoped(
+ Effect.gen(function* () {
+ const { browser, identity, slug } = yield* fixture;
+ yield* browser.session(identity, async ({ page, step }) => {
+ const started = Promise.withResolvers();
+ const released = Promise.withResolvers();
+ const completed = Promise.withResolvers();
+ await page.route(/\/api\/oauth\/clients(?:\?|$)/, async (route) => {
+ const response = await route.fetch();
+ started.resolve();
+ await released.promise;
+ await route.fulfill({ response });
+ completed.resolve();
+ });
+ try {
+ await step("Open the dialog while the real OAuth client list is held", async () => {
+ await page.goto(`/integrations/${slug}?addAccount=1`, {
+ waitUntil: "domcontentloaded",
+ });
+ await hydrated(page);
+ await started.promise;
+ const keyTab = page.getByRole("tab", {
+ name: "API key (Authorization)",
+ exact: true,
+ });
+ await keyTab.waitFor();
+ expect(await keyTab.getAttribute("aria-selected")).toBe("true");
+ });
+ await step(
+ "Resolve client availability without replacing a user's choice",
+ async () => {
+ const keyTab = page.getByRole("tab", {
+ name: "API key (Authorization)",
+ exact: true,
+ });
+ const keyInput = page.getByRole("textbox", { name: "Authorization", exact: true });
+ if (interaction === "select API key") await keyTab.click();
+ if (interaction === "enter API key")
+ await keyInput.fill("synthetic-key-in-progress");
+ released.resolve();
+ await completed.promise;
+ await page.waitForLoadState("networkidle");
+ expect(
+ await page
+ .getByRole("tab", { name: "OAuth2", exact: true })
+ .getAttribute("aria-selected"),
+ "only an untouched form should adopt the available OAuth client",
+ ).toBe(expectedOAuth);
+ expect(
+ await keyTab.getAttribute("aria-selected"),
+ "late data must preserve the chosen key form",
+ ).toBe(String(expectedOAuth === "false"));
+ expect(
+ await Promise.all((await keyInput.all()).map((input) => input.inputValue())),
+ "any key already entered must remain unchanged",
+ ).toEqual(expectedKeys);
+ },
+ );
+ } finally {
+ released.resolve();
+ await page.unrouteAll({ behavior: "wait" });
+ }
+ });
+ }),
+ ),
+ );
+}
+
+scenario(
+ "Connection setup · a ready OAuth app is the default",
+ {},
+ Effect.scoped(
+ Effect.gen(function* () {
+ const { browser, identity, slug } = yield* fixture;
+ yield* browser.session(identity, async ({ page, step }) => {
+ await step("Add a connection with both a token and a registered sign-in app", async () => {
+ await visit(page, `/integrations/${slug}?addAccount=1`);
+ await page.getByRole("tab", { name: "OAuth2", exact: true }).waitFor();
+ expect(
+ await page
+ .getByRole("tab", { name: "OAuth2", exact: true })
+ .getAttribute("aria-selected"),
+ "sign-in is selected without first switching away from API token",
+ ).toBe("true");
+ });
+ await step("Choose an API key instead of browser sign-in", async () => {
+ const tokenTab = page.getByRole("tab", { name: "API key (Authorization)", exact: true });
+ await tokenTab.click();
+ expect(await tokenTab.getAttribute("aria-selected")).toBe("true");
+ await page.getByRole("button", { name: "Cancel", exact: true }).click();
+ await page.getByRole("heading", { name: /Add connection/ }).waitFor({ state: "hidden" });
+ });
+ await step("Open a new connection on browser sign-in again", async () => {
+ await page.getByRole("button", { name: "Add connection", exact: true }).click();
+ expect(
+ await page
+ .getByRole("tab", { name: "OAuth2", exact: true })
+ .getAttribute("aria-selected"),
+ ).toBe("true");
+ });
+ });
+ }),
+ ),
+);
diff --git a/packages/react/src/components/add-account-modal.test.ts b/packages/react/src/components/add-account-modal.test.ts
index 0fdedf4983..90558590e6 100644
--- a/packages/react/src/components/add-account-modal.test.ts
+++ b/packages/react/src/components/add-account-modal.test.ts
@@ -11,6 +11,7 @@ import {
import type { AuthMethod } from "../lib/auth-placements";
import type { OAuthPopupReservation } from "../plugins/oauth-sign-in";
+import type { OAuthClientOption } from "../plugins/use-effective-oauth-client";
import {
connectionNameFrom,
connectionLabel,
@@ -19,6 +20,7 @@ import {
DEFAULT_CONNECTION_OWNER,
hasDcr,
mergeCustomMethods,
+ preferredMethodId,
oauthIdentityLabelFromHealth,
runAutomaticOAuthConnect,
runCimdConnect,
@@ -1438,3 +1440,68 @@ describe("runDcrConnect", () => {
expect(String(registerArgs!.slug)).toBe("dcr-auth-example-com");
});
});
+
+describe("preferredMethodId", () => {
+ const integration = IntegrationSlug.make("team-chat");
+ const token = apiKeyMethod("token", "spec");
+ const oauth: AuthMethod = {
+ id: "oauth",
+ label: "OAuth",
+ kind: "oauth",
+ source: "spec",
+ template: AuthTemplateSlug.make("oauth"),
+ placements: [],
+ oauth: { tokenUrl: "https://auth.example.com/token", scopes: ["read"] },
+ };
+ const client: OAuthClientOption = {
+ owner: "org",
+ slug: OAuthClientSlug.make("team-chat-app"),
+ grant: "authorization_code",
+ authorizationUrl: "https://auth.example.com/authorize",
+ tokenUrl: "https://auth.example.com/token",
+ clientId: "synthetic-client",
+ origin: { kind: "manual", integration: null },
+ };
+
+ it("prefers OAuth only with a client matched to that method", () => {
+ expect(preferredMethodId([token, oauth], [client], integration)).toBe("oauth");
+ expect(preferredMethodId([oauth, token], [], integration)).toBe("token");
+ expect(
+ preferredMethodId(
+ [token, oauth],
+ [{ ...client, tokenUrl: "https://other.example.com/token" }],
+ integration,
+ ),
+ ).toBe("token");
+ });
+
+ it("selects the OAuth method with a client rather than the first OAuth method", () => {
+ const unconfigured = {
+ ...oauth,
+ id: "other-oauth",
+ oauth: { tokenUrl: "https://unregistered.example.net/token" },
+ };
+ expect(preferredMethodId([token, unconfigured, oauth], [client], integration)).toBe("oauth");
+ });
+
+ it("uses matching built-in clients only when they allow the requested scopes", () => {
+ const builtIn: OAuthClientOption = {
+ ...client,
+ origin: { kind: "first_party", allowedScopes: ["read"] },
+ };
+ expect(preferredMethodId([token, oauth], [builtIn], integration)).toBe("oauth");
+ expect(
+ preferredMethodId(
+ [token, { ...oauth, oauth: { ...oauth.oauth, scopes: ["write"] } }],
+ [builtIn],
+ integration,
+ ),
+ ).toBe("token");
+ });
+
+ it("keeps single-method and empty integrations usable", () => {
+ expect(preferredMethodId([token], [], integration)).toBe("token");
+ expect(preferredMethodId([oauth], [], integration)).toBe("oauth");
+ expect(preferredMethodId([], [], integration)).toBe("");
+ });
+});
diff --git a/packages/react/src/components/add-account-modal.tsx b/packages/react/src/components/add-account-modal.tsx
index 5e10fd509f..222dd0f682 100644
--- a/packages/react/src/components/add-account-modal.tsx
+++ b/packages/react/src/components/add-account-modal.tsx
@@ -73,6 +73,7 @@ import {
clientDisplayName,
clientHost,
optimisticDcrClientSlug,
+ selectClientsForEndpoints,
selectDcrClientsForIntegration,
uniqueClientSlug,
useOAuthClientsForIntegration,
@@ -628,13 +629,29 @@ export const connectionExistsMessage = (label: string): string =>
* explicit choice. Personal: a connection is most often a personal credential. */
export const DEFAULT_CONNECTION_OWNER: Owner = "user";
-/** The method the modal opens on. OAuth needs a registered app (or a DCR
- * round-trip) before "Connect" does anything; a key is one paste. When an
- * integration declares both, starting on OAuth greets most users with
- * "Register app" — a dead end — while the working method sits one tab over.
- * Prefer the first non-OAuth method; OAuth stays one click away. */
-export const preferredMethodId = (methods: readonly AuthMethod[]): string =>
- (methods.find((method) => method.kind !== "oauth") ?? methods[0])?.id ?? "";
+/** Prefer OAuth only when its picker has a matching, usable client. Otherwise
+ * prefer a credential method; OAuth-only integrations still expose setup. */
+export const preferredMethodId = (
+ methods: readonly AuthMethod[],
+ clients: readonly OAuthClientOption[],
+ integration: IntegrationSlug,
+): string =>
+ (
+ methods.find(
+ (method) =>
+ method.kind === "oauth" &&
+ selectClientsForEndpoints(clients, {
+ integration,
+ tokenUrl: method.oauth?.tokenUrl,
+ authorizationUrl: method.oauth?.authorizationUrl,
+ scopes: method.oauth?.scopes,
+ discoversScopes: hasDcr(method),
+ requireEndpointMatch: true,
+ }).matched.length > 0,
+ ) ??
+ methods.find((method) => method.kind !== "oauth") ??
+ methods[0]
+ )?.id ?? "";
const authMethodKey = (method: AuthMethod): string =>
method.source === "custom" ? `custom:${String(method.template)}` : `declared:${method.id}`;
@@ -1443,7 +1460,9 @@ function AddAccountModalView(props: AddAccountModalProps) {
);
const [addingMethod, setAddingMethod] = useState(false);
- const [methodId, setMethodId] = useState(preferredMethodId(methods));
+ // An untouched form follows client availability. User interaction or a
+ // handoff pins a method so a late clients response cannot replace their form.
+ const [selectedMethodId, setMethodId] = useState(null);
// One value per distinct credential input (`variable → pasted value`). A
// single-secret method has just `{ token }`; a method with two distinct inputs
// (e.g. Datadog's two keys) collects one value per variable.
@@ -1548,6 +1567,23 @@ function AddAccountModalView(props: AddAccountModalProps) {
() => (AsyncResult.isSuccess(allClientsResult) ? allClientsResult.value : []),
[allClientsResult],
);
+ const defaultMethodId = useMemo(
+ () =>
+ preferredMethodId(
+ allMethods,
+ clientSummaries.flatMap((client) =>
+ client.grant === "authorization_code" || client.grant === "client_credentials"
+ ? [{ ...client, grant: client.grant }]
+ : [],
+ ),
+ integration,
+ ),
+ [allMethods, clientSummaries, integration],
+ );
+ const methodId =
+ selectedMethodId !== null && allMethods.some((method) => method.id === selectedMethodId)
+ ? selectedMethodId
+ : defaultMethodId;
const usage = useMemo(
() => buildUsageMap(AsyncResult.isSuccess(connectionsResult) ? connectionsResult.value : []),
[connectionsResult],
@@ -1582,15 +1618,6 @@ function AddAccountModalView(props: AddAccountModalProps) {
[allMethods, methodId],
);
- useEffect(() => {
- if (allMethods.length === 0) {
- if (methodId !== "") setMethodId("");
- return;
- }
- if (allMethods.some((m: AuthMethod) => m.id === methodId)) return;
- setMethodId(allMethods[0]!.id);
- }, [allMethods, methodId]);
-
// Apply the handoff prefill ONCE per handoff key (tracked by ref). The
// effect's deps include `allMethods`, which gets a new identity whenever the
// integration refetches — and the wizard itself triggers a refetch mid-flow
@@ -1622,18 +1649,6 @@ function AddAccountModalView(props: AddAccountModalProps) {
setDcrFallbackMessage(null);
}, [initialState, allMethods, defaultOwner, ownerOptions]);
- useEffect(() => {
- if (allMethods.length === 0) return;
- if (allMethods.some((m: AuthMethod) => m.id === methodId)) return;
- const initialMethod = initialState?.template
- ? allMethods.find(
- (m: AuthMethod) =>
- m.id === initialState.template || String(m.template) === initialState.template,
- )
- : undefined;
- setMethodId(initialMethod?.id ?? preferredMethodId(allMethods));
- }, [allMethods, initialState?.template, methodId]);
-
// Non-secret prefill carried by an `oauth.clients.createHandoff` deep link.
// The agent fills in the endpoints/grant/client id it discovered; the client
// secret is deliberately absent and is typed by the human in the form below.
@@ -2695,6 +2710,9 @@ function AddAccountModalView(props: AddAccountModalProps) {
setMethodId(methodId)}
+ onKeyDownCapture={() => setMethodId(methodId)}
+ onInput={() => setMethodId(methodId)}
className={cn(
"max-h-[85vh] overflow-x-hidden overflow-y-auto",
(addingMethod && createCustomMethod) || oauthRegistering || oauthEditing