Skip to content

Commit 55a2463

Browse files
authored
Move Google and Microsoft onto the OpenAPI plugin (#1366)
* Add openapi provider catalog design notes * Add OpenAPI spec format adapter support * Add Google and Microsoft spec format adapters * Preserve streaming spec format refresh * Move provider add surfaces into catalog presets * Fix provider catalog health checks * Fix Google Directory health check args * Format design notes * Add openapi provider service split migration * Fix service split blob migration * Read all provider operation rows for the migration plan * Assign provider operations by fresh-add spec ownership * Fix Microsoft service split ownership * Map singular mail folder and users profile segments * Seed add form identity from the catalog preset name * Use OIDC id token for OAuth identity labels * Handle OAuth health without probes * Canonicalize Microsoft scopes and decode JWT claims as UTF-8 * Update lockfile for provider package dependency changes * Package provider service split migration * Retarget orphan guardrail policies as dormant rows in the cloud runner * Retarget orphan approve policies too * Fold provider catalogs into openapi
1 parent 93b000a commit 55a2463

148 files changed

Lines changed: 8259 additions & 6034 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/cloud/executor.config.ts

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
import { defineExecutorConfig } from "@executor-js/sdk";
22
import { openApiHttpPlugin } from "@executor-js/plugin-openapi/api";
3-
import { googleHttpPlugin } from "@executor-js/plugin-google/api";
4-
import { microsoftHttpPlugin } from "@executor-js/plugin-microsoft/api";
3+
import {
4+
googleCatalog,
5+
googleDiscoveryAdapter,
6+
} from "@executor-js/plugin-openapi/providers/google";
7+
import {
8+
microsoftCatalog,
9+
microsoftGraphAdapter,
10+
} from "@executor-js/plugin-openapi/providers/microsoft";
511
import { mcpHttpPlugin } from "@executor-js/plugin-mcp/api";
612
import { graphqlHttpPlugin } from "@executor-js/plugin-graphql/api";
713
import { workosVaultPlugin, type WorkOSVaultClient } from "@executor-js/plugin-workos-vault";
@@ -42,23 +48,15 @@ interface CloudPluginDeps {
4248
* falls back to the credential-driven default. */
4349
readonly workosVaultClient?: WorkOSVaultClient;
4450
readonly activeToolkitSlug?: string;
45-
/** Mirrors `HostConfig.allowLocalNetwork` (`ALLOW_LOCAL_NETWORK`): lets
46-
* `microsoft.addGraph` point at a loopback emulator instead of the pinned
47-
* Microsoft Graph URLs. Off by default; production leaves it unset. */
48-
readonly allowLocalNetwork?: boolean;
4951
}
5052

5153
export default defineExecutorConfig({
52-
plugins: ({
53-
workosCredentials,
54-
workosVaultClient,
55-
activeToolkitSlug,
56-
allowLocalNetwork,
57-
}: CloudPluginDeps = {}) =>
54+
plugins: ({ workosCredentials, workosVaultClient, activeToolkitSlug }: CloudPluginDeps = {}) =>
5855
[
59-
openApiHttpPlugin(),
60-
googleHttpPlugin(),
61-
microsoftHttpPlugin({ allowUnsafeUrlOverrides: allowLocalNetwork === true }),
56+
openApiHttpPlugin({
57+
presets: [...googleCatalog, ...microsoftCatalog],
58+
specFormats: [googleDiscoveryAdapter, microsoftGraphAdapter],
59+
}),
6260
mcpHttpPlugin({
6361
dangerouslyAllowStdioMCP: false,
6462
}),

apps/cloud/package.json

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,8 @@
4141
"@executor-js/execution": "workspace:*",
4242
"@executor-js/fumadb": "workspace:*",
4343
"@executor-js/host-mcp": "workspace:*",
44-
"@executor-js/plugin-google": "workspace:*",
4544
"@executor-js/plugin-graphql": "workspace:*",
4645
"@executor-js/plugin-mcp": "workspace:*",
47-
"@executor-js/plugin-microsoft": "workspace:*",
4846
"@executor-js/plugin-openapi": "workspace:*",
4947
"@executor-js/plugin-toolkits": "workspace:*",
5048
"@executor-js/plugin-workos-vault": "workspace:*",

apps/cloud/src/engine/execution-stack.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,6 @@ export const CloudPluginsProvider: Layer.Layer<PluginsProvider> = Layer.succeed(
6666
},
6767
activeToolkitSlug:
6868
context?.mcpResource?.kind === "toolkit" ? context.mcpResource.slug : undefined,
69-
allowLocalNetwork: env.ALLOW_LOCAL_NETWORK === "true",
7069
}),
7170
});
7271

apps/desktop/package.json

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,9 @@
3535
"@executor-js/local": "workspace:*",
3636
"@executor-js/plugin-desktop-settings": "workspace:*",
3737
"@executor-js/plugin-file-secrets": "workspace:*",
38-
"@executor-js/plugin-google": "workspace:*",
3938
"@executor-js/plugin-graphql": "workspace:*",
4039
"@executor-js/plugin-keychain": "workspace:*",
4140
"@executor-js/plugin-mcp": "workspace:*",
42-
"@executor-js/plugin-microsoft": "workspace:*",
4341
"@executor-js/plugin-onepassword": "workspace:*",
4442
"@executor-js/plugin-openapi": "workspace:*",
4543
"@executor-js/runtime-quickjs": "workspace:*",

apps/host-cloudflare/executor.config.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
import { defineExecutorConfig } from "@executor-js/sdk";
22
import { openApiHttpPlugin } from "@executor-js/plugin-openapi/api";
3-
import { googleHttpPlugin } from "@executor-js/plugin-google/api";
4-
import { microsoftHttpPlugin } from "@executor-js/plugin-microsoft/api";
3+
import {
4+
googleCatalog,
5+
googleDiscoveryAdapter,
6+
} from "@executor-js/plugin-openapi/providers/google";
7+
import {
8+
microsoftCatalog,
9+
microsoftGraphAdapter,
10+
} from "@executor-js/plugin-openapi/providers/microsoft";
511
import { mcpHttpPlugin } from "@executor-js/plugin-mcp/api";
612
import { graphqlHttpPlugin } from "@executor-js/plugin-graphql/api";
713
import { encryptedSecretsPlugin } from "@executor-js/plugin-encrypted-secrets";
@@ -19,9 +25,10 @@ import { toolkitsPlugin } from "@executor-js/plugin-toolkits/server";
1925
export default defineExecutorConfig({
2026
plugins: ({ activeToolkitSlug }: { readonly activeToolkitSlug?: string } = {}) =>
2127
[
22-
openApiHttpPlugin(),
23-
googleHttpPlugin(),
24-
microsoftHttpPlugin(),
28+
openApiHttpPlugin({
29+
presets: [...googleCatalog, ...microsoftCatalog],
30+
specFormats: [googleDiscoveryAdapter, microsoftGraphAdapter],
31+
}),
2532
mcpHttpPlugin({ dangerouslyAllowStdioMCP: false }),
2633
graphqlHttpPlugin(),
2734
toolkitsPlugin({ activeToolkitSlug }),

apps/host-cloudflare/package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,10 @@
2424
"@executor-js/fumadb": "workspace:*",
2525
"@executor-js/host-mcp": "workspace:*",
2626
"@executor-js/plugin-encrypted-secrets": "workspace:*",
27-
"@executor-js/plugin-google": "workspace:*",
2827
"@executor-js/plugin-graphql": "workspace:*",
2928
"@executor-js/plugin-mcp": "workspace:*",
30-
"@executor-js/plugin-microsoft": "workspace:*",
3129
"@executor-js/plugin-openapi": "workspace:*",
30+
"@executor-js/plugin-provider-service-split": "workspace:*",
3231
"@executor-js/plugin-toolkits": "workspace:*",
3332
"@executor-js/react": "workspace:*",
3433
"@executor-js/runtime-quickjs": "workspace:*",

apps/host-cloudflare/src/db/data-migrations.test.ts

Lines changed: 54 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -110,11 +110,11 @@ const insertOperationStorage = (
110110
args: [
111111
row.tenant,
112112
row.pluginId,
113-
`${row.integration}.items.list`,
113+
`${row.integration}.calendar.events.list`,
114114
JSON.stringify({
115115
integration: row.integration,
116-
toolName: "items.list",
117-
binding: { method: "get", pathTemplate: "/items" },
116+
toolName: "calendar.events.list",
117+
binding: { method: "get", pathTemplate: "/calendar/events" },
118118
}),
119119
now,
120120
now,
@@ -239,6 +239,7 @@ describe("runCloudflareDataMigrations", () => {
239239
const d1 = makeFakeD1(db.client);
240240
expect(yield* Effect.promise(() => runCloudflareDataMigrations(d1, bucket))).toEqual([
241241
"2026-06-20-google-openapi-ownership",
242+
"2026-07-08-provider-service-split",
242243
]);
243244
expect(yield* Effect.promise(() => runCloudflareDataMigrations(d1, bucket))).toEqual([]);
244245

@@ -276,10 +277,12 @@ describe("runCloudflareDataMigrations", () => {
276277
}),
277278
);
278279
objects.set("o:org_1/openapi/spec/googlehash", "google spec");
280+
objects.set("o:org_1/google/defs/googlehash", "google defs");
279281

280282
const d1 = makeFakeD1(db.client);
281283
expect(yield* Effect.promise(() => runCloudflareDataMigrations(d1, bucket))).toEqual([
282284
"2026-06-20-google-openapi-ownership",
285+
"2026-07-08-provider-service-split",
283286
]);
284287
expect(yield* Effect.promise(() => runCloudflareDataMigrations(d1, bucket))).toEqual([]);
285288

@@ -288,12 +291,58 @@ describe("runCloudflareDataMigrations", () => {
288291
const integrations = yield* Effect.promise(() =>
289292
db.client.execute("SELECT slug, plugin_id FROM integration ORDER BY slug"),
290293
);
291-
expect(integrations.rows).toEqual([{ slug: "google", plugin_id: "google" }]);
294+
expect(integrations.rows).toEqual([{ slug: "google_calendar", plugin_id: "openapi" }]);
292295

293296
const storage = yield* Effect.promise(() =>
294297
db.client.execute("SELECT plugin_id, key FROM plugin_storage ORDER BY plugin_id, key"),
295298
);
296-
expect(storage.rows).toEqual([{ plugin_id: "google", key: "google.items.list" }]);
299+
expect(storage.rows).toEqual([{ plugin_id: "openapi", key: expect.stringMatching(/^op\./) }]);
300+
301+
yield* Effect.promise(() => db.close());
302+
}),
303+
);
304+
305+
it.effect("splits Google monolith ownership and copies R2 spec and defs objects", () =>
306+
Effect.gen(function* () {
307+
const db = yield* Effect.promise(() => createSqliteTestFumaDb({ tables: collectTables() }));
308+
const { bucket, objects } = makeFakeR2();
309+
310+
yield* Effect.promise(() =>
311+
insertIntegration(db.client, {
312+
rowId: "google-monolith-row",
313+
tenant: "org_1",
314+
slug: "google",
315+
pluginId: "google",
316+
config: {
317+
specHash: "mono-hash",
318+
googleDiscoveryUrls: ["https://www.googleapis.com/discovery/v1/apis/calendar/v3/rest"],
319+
},
320+
}),
321+
);
322+
yield* Effect.promise(() =>
323+
insertOperationStorage(db.client, {
324+
tenant: "org_1",
325+
pluginId: "google",
326+
integration: "google",
327+
}),
328+
);
329+
objects.set("o:org_1/google/spec/mono-hash", "google spec");
330+
objects.set("o:org_1/google/defs/mono-hash", "google defs");
331+
332+
const d1 = makeFakeD1(db.client);
333+
expect(yield* Effect.promise(() => runCloudflareDataMigrations(d1, bucket))).toEqual([
334+
"2026-06-20-google-openapi-ownership",
335+
"2026-07-08-provider-service-split",
336+
]);
337+
expect(yield* Effect.promise(() => runCloudflareDataMigrations(d1, bucket))).toEqual([]);
338+
339+
expect(objects.get("o:org_1/openapi/spec/mono-hash")).toBe("google spec");
340+
expect(objects.get("o:org_1/openapi/defs/mono-hash")).toBe("google defs");
341+
342+
const integrations = yield* Effect.promise(() =>
343+
db.client.execute("SELECT slug, plugin_id FROM integration ORDER BY slug"),
344+
);
345+
expect(integrations.rows).toEqual([{ slug: "google_calendar", plugin_id: "openapi" }]);
297346

298347
yield* Effect.promise(() => db.close());
299348
}),

apps/host-cloudflare/src/db/data-migrations.ts

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,13 @@ import {
77
type SqliteDataMigration,
88
type SqliteDataMigrationClient,
99
} from "@executor-js/sdk";
10-
import { googleOpenApiOwnershipDataMigration } from "@executor-js/plugin-google";
10+
import { googleOpenApiOwnershipDataMigration } from "@executor-js/plugin-openapi/providers/google";
11+
12+
import {
13+
providerServiceSplitDataMigration,
14+
runSqliteProviderServiceSplitMigration,
15+
type BlobRow,
16+
} from "@executor-js/plugin-provider-service-split";
1117

1218
const TX_CONTROL = new Set(["BEGIN", "BEGIN TRANSACTION", "COMMIT", "ROLLBACK"]);
1319

@@ -161,6 +167,61 @@ const copyGoogleOpenApiSpecBlobsToR2 = (
161167
}),
162168
});
163169

170+
const copyProviderServiceSplitBlobsToR2 = (
171+
client: SqliteDataMigrationClient,
172+
bucket: R2Bucket,
173+
): Effect.Effect<readonly BlobRow[], DataMigrationError> =>
174+
Effect.tryPromise({
175+
try: async () => {
176+
const present: BlobRow[] = [];
177+
const result = await client.execute(
178+
`SELECT tenant, plugin_id, json_extract(config, '$.specHash') AS spec_hash
179+
FROM integration
180+
WHERE ((plugin_id = 'google' AND slug = 'google')
181+
OR (plugin_id = 'microsoft' AND slug = 'microsoft'))
182+
AND config IS NOT NULL
183+
AND json_valid(config)
184+
AND json_extract(config, '$.specHash') IS NOT NULL
185+
AND json_extract(config, '$.specHash') <> ''`,
186+
);
187+
const seen = new Set<string>();
188+
for (const row of result.rows) {
189+
if (
190+
typeof row.tenant !== "string" ||
191+
typeof row.plugin_id !== "string" ||
192+
typeof row.spec_hash !== "string"
193+
) {
194+
continue;
195+
}
196+
for (const key of [`spec/${row.spec_hash}`, `defs/${row.spec_hash}`]) {
197+
const sourceNamespace = `o:${row.tenant}/${row.plugin_id}`;
198+
const sourceKey = r2ObjectName(row.tenant, row.plugin_id, key);
199+
const targetKey = r2ObjectName(row.tenant, "openapi", key);
200+
const source = await bucket.get(sourceKey);
201+
if (source == null) continue;
202+
const presenceKey = `${sourceNamespace}/${key}`;
203+
if (!seen.has(presenceKey)) {
204+
seen.add(presenceKey);
205+
present.push({
206+
id: JSON.stringify([sourceNamespace, key]),
207+
namespace: sourceNamespace,
208+
key,
209+
});
210+
}
211+
if ((await bucket.head(targetKey)) == null) {
212+
await bucket.put(targetKey, await source.text());
213+
}
214+
}
215+
}
216+
return present;
217+
},
218+
catch: (cause) =>
219+
new DataMigrationError({
220+
migration: providerServiceSplitDataMigration.name,
221+
cause,
222+
}),
223+
});
224+
164225
const cloudflareDataMigrations = (bucket: R2Bucket | undefined): readonly SqliteDataMigration[] => [
165226
{
166227
name: googleOpenApiOwnershipDataMigration.name,
@@ -170,6 +231,17 @@ const cloudflareDataMigrations = (bucket: R2Bucket | undefined): readonly Sqlite
170231
yield* googleOpenApiOwnershipDataMigration.run(client);
171232
}),
172233
},
234+
{
235+
name: providerServiceSplitDataMigration.name,
236+
run: (client) =>
237+
Effect.gen(function* () {
238+
const blobs = bucket ? yield* copyProviderServiceSplitBlobsToR2(client, bucket) : [];
239+
yield* runSqliteProviderServiceSplitMigration(client, {
240+
blobBackend: bucket ? "external" : "database",
241+
blobs,
242+
}).pipe(Effect.asVoid);
243+
}),
244+
},
173245
];
174246

175247
export const runCloudflareDataMigrations = (

apps/host-cloudflare/src/plugins.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
import { openApiHttpPlugin } from "@executor-js/plugin-openapi/api";
2-
import { googleHttpPlugin } from "@executor-js/plugin-google/api";
3-
import { microsoftHttpPlugin } from "@executor-js/plugin-microsoft/api";
2+
import {
3+
googleCatalog,
4+
googleDiscoveryAdapter,
5+
} from "@executor-js/plugin-openapi/providers/google";
6+
import {
7+
microsoftCatalog,
8+
microsoftGraphAdapter,
9+
} from "@executor-js/plugin-openapi/providers/microsoft";
410
import { mcpHttpPlugin } from "@executor-js/plugin-mcp/api";
511
import { graphqlHttpPlugin } from "@executor-js/plugin-graphql/api";
612
import { encryptedSecretsPlugin } from "@executor-js/plugin-encrypted-secrets";
@@ -22,9 +28,10 @@ export const makeCloudflarePlugins = (
2228
options: { readonly activeToolkitSlug?: string; readonly allowLocalNetwork?: boolean } = {},
2329
) =>
2430
[
25-
openApiHttpPlugin(),
26-
googleHttpPlugin(),
27-
microsoftHttpPlugin({ allowUnsafeUrlOverrides: options.allowLocalNetwork === true }),
31+
openApiHttpPlugin({
32+
presets: [...googleCatalog, ...microsoftCatalog],
33+
specFormats: [googleDiscoveryAdapter, microsoftGraphAdapter],
34+
}),
2835
mcpHttpPlugin({ dangerouslyAllowStdioMCP: false }),
2936
graphqlHttpPlugin(),
3037
toolkitsPlugin({ activeToolkitSlug: options.activeToolkitSlug }),

apps/host-cloudflare/src/worker.e2e.node.test.ts

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { fileURLToPath } from "node:url";
66
import { afterAll, beforeAll, describe, expect, it } from "@effect/vitest";
77
import { Schema } from "effect";
88
import { unstable_dev, type Unstable_DevWorker } from "wrangler";
9+
import { microsoftCatalog } from "@executor-js/plugin-openapi/providers/microsoft";
910

1011
// ---------------------------------------------------------------------------
1112
// End-to-end test for the Cloudflare host: boots the REAL worker on workerd via
@@ -187,13 +188,48 @@ describe("cloudflare host e2e (workerd/miniflare)", () => {
187188
expect(integration?.slug).toBe(slug);
188189
}, 60_000);
189190

190-
it("registers the Microsoft Graph API route in the runtime plugin set", async () => {
191-
const res = await worker.fetch("/api/microsoft/graph", {
191+
it("exposes Microsoft catalog presets through the OpenAPI integration catalog", async () => {
192+
const microsoftFilesPreset = microsoftCatalog.find(
193+
(preset) => preset.defaultSlug === "microsoft_files",
194+
);
195+
expect(microsoftFilesPreset).toBeTruthy();
196+
197+
const removedProviderRoute = await worker.fetch("/api/microsoft/graph", {
192198
method: "POST",
193199
headers: { "content-type": "application/json" },
194200
body: JSON.stringify({}),
195201
});
196-
expect(res.status).toBe(400);
202+
expect(removedProviderRoute.status).toBe(404);
203+
204+
const slug = `microsoft-files-${runId}`;
205+
const add = await worker.fetch("/api/openapi/specs", {
206+
method: "POST",
207+
headers: { "content-type": "application/json" },
208+
body: JSON.stringify({
209+
spec: { kind: "blob", value: SPEC },
210+
slug,
211+
name: microsoftFilesPreset?.name,
212+
description: microsoftFilesPreset?.summary,
213+
family: microsoftFilesPreset?.family,
214+
authenticationTemplate: microsoftFilesPreset?.authTemplate,
215+
}),
216+
});
217+
expect(add.status).toBe(200);
218+
219+
const res = await worker.fetch(`/api/integrations/${slug}`);
220+
expect(res.status).toBe(200);
221+
const integration = (await res.json()) as {
222+
readonly slug: string;
223+
readonly kind: string;
224+
readonly family?: string;
225+
readonly authMethods: readonly { readonly template: string; readonly kind: string }[];
226+
};
227+
expect(integration.slug).toBe(slug);
228+
expect(integration.kind).toBe("openapi");
229+
expect(integration.family).toBe("microsoft");
230+
expect(integration.authMethods.some((method) => method.template === "azureAdDelegated")).toBe(
231+
true,
232+
);
197233
});
198234

199235
it("gates the API when dev-auth is on but treats the request as the dev admin", async () => {

0 commit comments

Comments
 (0)