From 5c37b310602ccbd44c3ab9e4d99897eba95645fc Mon Sep 17 00:00:00 2001 From: Aditya kumar singh <143548997+Adityakk9031@users.noreply.github.com> Date: Mon, 10 Aug 2026 17:27:38 +0530 Subject: [PATCH] fix(selfhost): refresh stale connection tools concurrently and expose EXECUTOR_TOOLS_SYNC_TTL_MS --- apps/host-selfhost/src/config.ts | 11 ++++++ apps/host-selfhost/src/execution.ts | 1 + .../host-selfhost/src/executor-config.test.ts | 25 +++++++++++++ .../core/api/src/server/scoped-executor.ts | 6 ++++ packages/core/sdk/src/executor.ts | 36 +++++++++++-------- 5 files changed, 64 insertions(+), 15 deletions(-) diff --git a/apps/host-selfhost/src/config.ts b/apps/host-selfhost/src/config.ts index e0cd282d52..866981516d 100644 --- a/apps/host-selfhost/src/config.ts +++ b/apps/host-selfhost/src/config.ts @@ -43,6 +43,8 @@ export interface SelfHostConfig { readonly organizationName: string; /** URL slug for org-prefixed console paths (`//policies`). */ readonly orgSlug: string; + /** Freshness TTL (in ms) for remote tool catalogs, or `null` to disable. */ + readonly toolsSyncTtlMs?: number | null; } export const resolveDataDir = (): string => @@ -148,6 +150,7 @@ export const loadConfig = (): SelfHostConfig => { bootstrapAdminName: process.env.EXECUTOR_BOOTSTRAP_ADMIN_NAME ?? "Admin", organizationName: process.env.EXECUTOR_ORG_NAME ?? "Default", orgSlug: resolveOrgSlug(), + toolsSyncTtlMs: resolveToolsSyncTtlMs(), }; }; @@ -165,3 +168,11 @@ const resolveOrgSlug = (): string => { } return slug; }; + +const resolveToolsSyncTtlMs = (): number | null | undefined => { + const raw = process.env.EXECUTOR_TOOLS_SYNC_TTL_MS?.trim(); + if (!raw) return undefined; + if (raw === "null" || raw === "false" || raw === "0") return null; + const parsed = Number.parseInt(raw, 10); + return Number.isNaN(parsed) ? undefined : parsed; +}; diff --git a/apps/host-selfhost/src/execution.ts b/apps/host-selfhost/src/execution.ts index 270ffc4f8e..d196dd215d 100644 --- a/apps/host-selfhost/src/execution.ts +++ b/apps/host-selfhost/src/execution.ts @@ -55,6 +55,7 @@ export const SelfHostHostConfig: Layer.Layer = Layer.sync(HostConfig allowLocalNetwork: config.allowLocalNetwork, webBaseUrl: config.webBaseUrl, oauthCallbackPath: "/api/oauth/callback", + toolsSyncTtlMs: config.toolsSyncTtlMs, onIntegrationChange: (event) => selfHostAnalytics.record( event.kind === "added" ? "integration_added" : "integration_removed", diff --git a/apps/host-selfhost/src/executor-config.test.ts b/apps/host-selfhost/src/executor-config.test.ts index 0d56bc32f2..7598aade82 100644 --- a/apps/host-selfhost/src/executor-config.test.ts +++ b/apps/host-selfhost/src/executor-config.test.ts @@ -1,11 +1,14 @@ import { afterEach, beforeEach, expect, test } from "@effect/vitest"; +import { loadConfig } from "./config"; import executorConfig from "../executor.config"; const ENV_NAME = "EXECUTOR_ALLOW_STDIO_MCP"; const SECRET_ENV_NAME = "EXECUTOR_SECRET_KEY"; +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]; beforeEach(() => { process.env[SECRET_ENV_NAME] = originalSecret ?? "executor-config-test-secret"; @@ -22,6 +25,11 @@ afterEach(() => { } else { process.env[SECRET_ENV_NAME] = originalSecret; } + if (originalTtl === undefined) { + delete process.env[TTL_ENV_NAME]; + } else { + process.env[TTL_ENV_NAME] = originalTtl; + } }); const allowStdio = (): boolean => { @@ -57,3 +65,20 @@ test("stdio MCP is enabled when the opt-in is exactly true", () => { process.env[ENV_NAME] = "true"; expect(allowStdio()).toBe(true); }); + +test("toolsSyncTtlMs parses integer, null/false/0 disable values, and undefined fallback", () => { + delete process.env[TTL_ENV_NAME]; + expect(loadConfig().toolsSyncTtlMs).toBeUndefined(); + + process.env[TTL_ENV_NAME] = "60000"; + expect(loadConfig().toolsSyncTtlMs).toBe(60000); + + process.env[TTL_ENV_NAME] = "null"; + expect(loadConfig().toolsSyncTtlMs).toBeNull(); + + process.env[TTL_ENV_NAME] = "false"; + expect(loadConfig().toolsSyncTtlMs).toBeNull(); + + process.env[TTL_ENV_NAME] = "0"; + expect(loadConfig().toolsSyncTtlMs).toBeNull(); +}); diff --git a/packages/core/api/src/server/scoped-executor.ts b/packages/core/api/src/server/scoped-executor.ts index ea0e33ce61..25aae932a8 100644 --- a/packages/core/api/src/server/scoped-executor.ts +++ b/packages/core/api/src/server/scoped-executor.ts @@ -97,6 +97,11 @@ export interface HostConfigShape { * Hosts that record product analytics supply it; omitted -> no observation. */ readonly onIntegrationChange?: ExecutorConfig["onIntegrationChange"]; + /** + * Freshness TTL (in ms) for remote tool catalogs before an explicit re-sync is + * attempted. Omit for default (15 mins), or set `null` to disable time-based re-sync. + */ + readonly toolsSyncTtlMs?: number | null; } export class HostConfig extends Context.Service()( @@ -284,6 +289,7 @@ export const makeScopedExecutor = < httpClientLayer, fetch: hostedFetch, onIntegrationChange: config.onIntegrationChange, + ...(config.toolsSyncTtlMs !== undefined ? { toolsSyncTtlMs: config.toolsSyncTtlMs } : {}), onElicitation: "accept-all", redirectUri, oauthCallbackStateOrgSlug: orgSlug, diff --git a/packages/core/sdk/src/executor.ts b/packages/core/sdk/src/executor.ts index 56cb2e2997..66f005d0ff 100644 --- a/packages/core/sdk/src/executor.ts +++ b/packages/core/sdk/src/executor.ts @@ -3631,6 +3631,7 @@ export const createExecutor = Effect.succeed([] as readonly Tool[])), - Effect.withSpan("executor.tools.sync_stale", { - attributes: { - "executor.integration": connection.integration, - "executor.connection": connection.name, + tasks.push( + produceConnectionTools( + integrationRow, + { + owner: connection.owner as Owner, + integration: IntegrationSlug.make(connection.integration), + name: ConnectionName.make(connection.name), }, - }), + "background", + ).pipe( + Effect.catch(() => Effect.succeed([] as readonly Tool[])), + Effect.withSpan("executor.tools.sync_stale", { + attributes: { + "executor.integration": connection.integration, + "executor.connection": connection.name, + }, + }), + ), ); } + if (tasks.length > 0) { + yield* Effect.all(tasks, { concurrency: 10 }); + } }); const toolsList = (filter?: ToolListFilter): Effect.Effect =>