Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/account-scoped-tool-policies.md
Original file line number Diff line number Diff line change
@@ -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.
5 changes: 5 additions & 0 deletions .changeset/connection-oauth-default.md
Original file line number Diff line number Diff line change
@@ -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.
5 changes: 5 additions & 0 deletions .changeset/fair-admin-integrations.md
Original file line number Diff line number Diff line change
@@ -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.
5 changes: 5 additions & 0 deletions .changeset/oauth-discovered-scope-budget.md
Original file line number Diff line number Diff line change
@@ -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".
5 changes: 5 additions & 0 deletions .changeset/quiet-connection-setup.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"executor": patch
---

Accept Slack bot and user OAuth token envelopes during sign-in and token refresh.
6 changes: 6 additions & 0 deletions .changeset/toolkit-list-scope.md
Original file line number Diff line number Diff line change
@@ -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.
59 changes: 59 additions & 0 deletions .github/scripts/check-database-capacity.py
Original file line number Diff line number Diff line change
@@ -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())
27 changes: 27 additions & 0 deletions .github/workflows/database-capacity.yml
Original file line number Diff line number Diff line change
@@ -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 }}
31 changes: 31 additions & 0 deletions .github/workflows/publish-desktop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
12 changes: 12 additions & 0 deletions apps/cli/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion apps/cli/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "executor",
"version": "1.6.9",
"version": "1.6.10",
"private": true,
"bin": {
"executor": "./bin/executor.ts"
Expand Down
21 changes: 21 additions & 0 deletions apps/cloud/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
46 changes: 46 additions & 0 deletions apps/cloud/docs/database-connections.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# 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 | 20 |
| PgBouncer max_db_connections | 20 |
| PgBouncer max_client_conn | 400 |
| PgBouncer max_prepared_statements | 200 |
| 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`
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.
2 changes: 1 addition & 1 deletion apps/cloud/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@executor-js/cloud",
"version": "1.4.70",
"version": "1.4.71",
"private": true,
"type": "module",
"scripts": {
Expand Down
56 changes: 56 additions & 0 deletions apps/cloud/scripts/database-connection.ts
Original file line number Diff line number Diff line change
@@ -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<unknown> },
options: {
readonly log: (message: string) => void;
readonly sleep?: (milliseconds: number) => Promise<void>;
},
): Promise<void> => {
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);
}
}
};
5 changes: 4 additions & 1 deletion apps/cloud/scripts/ensure-workos-mirror-ready.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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);
Expand All @@ -84,6 +86,7 @@ const runScript = (what: string, script: string) => {
};

try {
await waitForDatabaseConnection(sql, { log });
let state = await readiness();
log(describeMirrorReadiness(state));

Expand Down
5 changes: 4 additions & 1 deletion apps/cloud/scripts/migrate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -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");
Expand Down
Loading
Loading