Skip to content

Commit 67d03d9

Browse files
committed
fix(selfhost): refresh stale connection tools concurrently and expose EXECUTOR_TOOLS_SYNC_TTL_MS
1 parent 903126a commit 67d03d9

5 files changed

Lines changed: 64 additions & 15 deletions

File tree

apps/host-selfhost/src/config.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ export interface SelfHostConfig {
4343
readonly organizationName: string;
4444
/** URL slug for org-prefixed console paths (`/<slug>/policies`). */
4545
readonly orgSlug: string;
46+
/** Freshness TTL (in ms) for remote tool catalogs, or `null` to disable. */
47+
readonly toolsSyncTtlMs?: number | null;
4648
}
4749

4850
export const resolveDataDir = (): string =>
@@ -148,6 +150,7 @@ export const loadConfig = (): SelfHostConfig => {
148150
bootstrapAdminName: process.env.EXECUTOR_BOOTSTRAP_ADMIN_NAME ?? "Admin",
149151
organizationName: process.env.EXECUTOR_ORG_NAME ?? "Default",
150152
orgSlug: resolveOrgSlug(),
153+
toolsSyncTtlMs: resolveToolsSyncTtlMs(),
151154
};
152155
};
153156

@@ -165,3 +168,11 @@ const resolveOrgSlug = (): string => {
165168
}
166169
return slug;
167170
};
171+
172+
const resolveToolsSyncTtlMs = (): number | null | undefined => {
173+
const raw = process.env.EXECUTOR_TOOLS_SYNC_TTL_MS?.trim();
174+
if (!raw) return undefined;
175+
if (raw === "null" || raw === "false" || raw === "0") return null;
176+
const parsed = Number.parseInt(raw, 10);
177+
return Number.isNaN(parsed) ? undefined : parsed;
178+
};

apps/host-selfhost/src/execution.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ export const SelfHostHostConfig: Layer.Layer<HostConfig> = Layer.sync(HostConfig
5555
allowLocalNetwork: config.allowLocalNetwork,
5656
webBaseUrl: config.webBaseUrl,
5757
oauthCallbackPath: "/api/oauth/callback",
58+
toolsSyncTtlMs: config.toolsSyncTtlMs,
5859
onIntegrationChange: (event) =>
5960
selfHostAnalytics.record(
6061
event.kind === "added" ? "integration_added" : "integration_removed",

apps/host-selfhost/src/executor-config.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
import { afterEach, beforeEach, expect, test } from "@effect/vitest";
22

3+
import { loadConfig } from "./config";
34
import executorConfig from "../executor.config";
45

56
const ENV_NAME = "EXECUTOR_ALLOW_STDIO_MCP";
67
const SECRET_ENV_NAME = "EXECUTOR_SECRET_KEY";
8+
const TTL_ENV_NAME = "EXECUTOR_TOOLS_SYNC_TTL_MS";
79
const originalValue = process.env[ENV_NAME];
810
const originalSecret = process.env[SECRET_ENV_NAME];
11+
const originalTtl = process.env[TTL_ENV_NAME];
912

1013
beforeEach(() => {
1114
process.env[SECRET_ENV_NAME] = originalSecret ?? "executor-config-test-secret";
@@ -22,6 +25,11 @@ afterEach(() => {
2225
} else {
2326
process.env[SECRET_ENV_NAME] = originalSecret;
2427
}
28+
if (originalTtl === undefined) {
29+
delete process.env[TTL_ENV_NAME];
30+
} else {
31+
process.env[TTL_ENV_NAME] = originalTtl;
32+
}
2533
});
2634

2735
const allowStdio = (): boolean => {
@@ -57,3 +65,20 @@ test("stdio MCP is enabled when the opt-in is exactly true", () => {
5765
process.env[ENV_NAME] = "true";
5866
expect(allowStdio()).toBe(true);
5967
});
68+
69+
test("toolsSyncTtlMs parses integer, null/false/0 disable values, and undefined fallback", () => {
70+
delete process.env[TTL_ENV_NAME];
71+
expect(loadConfig().toolsSyncTtlMs).toBeUndefined();
72+
73+
process.env[TTL_ENV_NAME] = "60000";
74+
expect(loadConfig().toolsSyncTtlMs).toBe(60000);
75+
76+
process.env[TTL_ENV_NAME] = "null";
77+
expect(loadConfig().toolsSyncTtlMs).toBeNull();
78+
79+
process.env[TTL_ENV_NAME] = "false";
80+
expect(loadConfig().toolsSyncTtlMs).toBeNull();
81+
82+
process.env[TTL_ENV_NAME] = "0";
83+
expect(loadConfig().toolsSyncTtlMs).toBeNull();
84+
});

packages/core/api/src/server/scoped-executor.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,11 @@ export interface HostConfigShape {
9797
* Hosts that record product analytics supply it; omitted -> no observation.
9898
*/
9999
readonly onIntegrationChange?: ExecutorConfig["onIntegrationChange"];
100+
/**
101+
* Freshness TTL (in ms) for remote tool catalogs before an explicit re-sync is
102+
* attempted. Omit for default (15 mins), or set `null` to disable time-based re-sync.
103+
*/
104+
readonly toolsSyncTtlMs?: number | null;
100105
}
101106

102107
export class HostConfig extends Context.Service<HostConfig, HostConfigShape>()(
@@ -284,6 +289,7 @@ export const makeScopedExecutor = <
284289
httpClientLayer,
285290
fetch: hostedFetch,
286291
onIntegrationChange: config.onIntegrationChange,
292+
...(config.toolsSyncTtlMs !== undefined ? { toolsSyncTtlMs: config.toolsSyncTtlMs } : {}),
287293
onElicitation: "accept-all",
288294
redirectUri,
289295
oauthCallbackStateOrgSlug: orgSlug,

packages/core/sdk/src/executor.ts

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3631,6 +3631,7 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea
36313631
? b.isNull("tools_synced_at")
36323632
: b.or(b.isNull("tools_synced_at"), b("tools_synced_at", "<", staleBefore)),
36333633
});
3634+
const tasks = [];
36343635
for (const connection of connections) {
36353636
const integrationRow = integrationBySlug.get(connection.integration);
36363637
if (!integrationRow) continue;
@@ -3657,24 +3658,29 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea
36573658
syncedAt < cutoff;
36583659
if (!staleMarked && !configRevised && !expired) continue;
36593660

3660-
yield* produceConnectionTools(
3661-
integrationRow,
3662-
{
3663-
owner: connection.owner as Owner,
3664-
integration: IntegrationSlug.make(connection.integration),
3665-
name: ConnectionName.make(connection.name),
3666-
},
3667-
"background",
3668-
).pipe(
3669-
Effect.catch(() => Effect.succeed([] as readonly Tool[])),
3670-
Effect.withSpan("executor.tools.sync_stale", {
3671-
attributes: {
3672-
"executor.integration": connection.integration,
3673-
"executor.connection": connection.name,
3661+
tasks.push(
3662+
produceConnectionTools(
3663+
integrationRow,
3664+
{
3665+
owner: connection.owner as Owner,
3666+
integration: IntegrationSlug.make(connection.integration),
3667+
name: ConnectionName.make(connection.name),
36743668
},
3675-
}),
3669+
"background",
3670+
).pipe(
3671+
Effect.catch(() => Effect.succeed([] as readonly Tool[])),
3672+
Effect.withSpan("executor.tools.sync_stale", {
3673+
attributes: {
3674+
"executor.integration": connection.integration,
3675+
"executor.connection": connection.name,
3676+
},
3677+
}),
3678+
),
36763679
);
36773680
}
3681+
if (tasks.length > 0) {
3682+
yield* Effect.all(tasks, { concurrency: 10 });
3683+
}
36783684
});
36793685

36803686
const toolsList = (filter?: ToolListFilter): Effect.Effect<readonly Tool[], StorageFailure> =>

0 commit comments

Comments
 (0)