Route hosted plugin outbound requests through a shared Effect HttpClient layer that blocks local/private/metadata targets and re-checks redirects. Thread the executor-provided HTTP client through OpenAPI, GraphQL, MCP probing, and OAuth discovery, keep local harnesses on the normal fetch client, and split the restored security coverage into the relevant existing suites. Fix cloud migration scripts to invoke the local Drizzle binary under op run.
Showing the changes to packages/plugins (4 of 19 files). Show all
Unified · Split
| @@ -1,6 +1,6 @@ | |||
| 1 | import { Effect, Option, Schema } from "effect"; | 1 | import { Effect, Option, Schema } from "effect"; |
| 2 | import type { Layer } from "effect"; | 2 | import type { Layer } from "effect"; |
| 3 | import { FetchHttpClient, HttpClient } from "effect/unstable/http"; | 3 | import { HttpClient } from "effect/unstable/http"; |
| 4 | 4 | ||
| 5 | import { GraphqlGroup } from "../api/group"; | 5 | import { GraphqlGroup } from "../api/group"; |
| 6 | import { GraphqlExtensionService, GraphqlHandlers } from "../api/handlers"; | 6 | import { GraphqlExtensionService, GraphqlHandlers } from "../api/handlers"; |
| @@ -916,15 +916,14 @@ const makeGraphqlExtension = ( · 909 unmodified lines | |||
| 916 | export type GraphqlPluginExtension = ReturnType<typeof makeGraphqlExtension>; | 916 | export type GraphqlPluginExtension = ReturnType<typeof makeGraphqlExtension>; |
| 917 | 917 | ||
| 918 | export const graphqlPlugin = definePlugin((options?: GraphqlPluginOptions) => { | 918 | export const graphqlPlugin = definePlugin((options?: GraphqlPluginOptions) => { |
| 919 | const httpClientLayer = options?.httpClientLayer ?? FetchHttpClient.layer; | ||
| 920 | |||
| 921 | return { | 919 | return { |
| 922 | id: "graphql" as const, | 920 | id: "graphql" as const, |
| 923 | packageName: "@executor-js/plugin-graphql", | 921 | packageName: "@executor-js/plugin-graphql", |
| 924 | schema: graphqlSchema, | 922 | schema: graphqlSchema, |
| 925 | storage: (deps): GraphqlStore => makeDefaultGraphqlStore(deps), | 923 | storage: (deps): GraphqlStore => makeDefaultGraphqlStore(deps), |
| 926 | 924 | ||
| 927 | extension: (ctx) => makeGraphqlExtension(ctx, httpClientLayer, options?.configFile), | 925 | extension: (ctx) => |
| 926 | makeGraphqlExtension(ctx, options?.httpClientLayer ?? ctx.httpClientLayer, options?.configFile), | ||
| 928 | 927 | ||
| 929 | staticSources: (self) => [ | 928 | staticSources: (self) => [ |
| 930 | { | 929 | { |
| @@ -969,6 +968,7 @@ export const graphqlPlugin = definePlugin((options?: GraphqlPluginOptions) => { · 38 unmodified lines | |||
| 969 | 968 | ||
| 970 | invokeTool: ({ ctx, toolRow, args }) => | 969 | invokeTool: ({ ctx, toolRow, args }) => |
| 971 | Effect.gen(function* () { | 970 | Effect.gen(function* () { |
| 971 | const httpClientLayer = options?.httpClientLayer ?? ctx.httpClientLayer; | ||
| 972 | // toolRow.scope_id is the resolved owning scope of the tool | 972 | // toolRow.scope_id is the resolved owning scope of the tool |
| 973 | // (innermost-wins from the executor's stack). The matching | 973 | // (innermost-wins from the executor's stack). The matching |
| 974 | // graphql_operation + graphql_source rows live at the same | 974 | // graphql_operation + graphql_source rows live at the same |
| @@ -1081,8 +1081,9 @@ export const graphqlPlugin = definePlugin((options?: GraphqlPluginOptions) => { · 106 unmodified lines | |||
| 1081 | 1081 | ||
| 1082 | usagesForConnection: () => Effect.succeed([]), | 1082 | usagesForConnection: () => Effect.succeed([]), |
| 1083 | 1083 | ||
| 1084 | detect: ({ url }) => | 1084 | detect: ({ ctx, url }) => |
| 1085 | Effect.gen(function* () { | 1085 | Effect.gen(function* () { |
| 1086 | const httpClientLayer = options?.httpClientLayer ?? ctx.httpClientLayer; | ||
| 1086 | const trimmed = url.trim(); | 1087 | const trimmed = url.trim(); |
| 1087 | if (!trimmed) return null; | 1088 | if (!trimmed) return null; |
| 1088 | const parsed = yield* Effect.try({ | 1089 | const parsed = yield* Effect.try({ |
| @@ -1,4 +1,15 @@ | |||
| 1 | import { Duration, Effect, Exit, Option, Predicate, Result, Scope, ScopedCache } from "effect"; | 1 | import { |
| 2 | Duration, | ||
| 3 | Effect, | ||
| 4 | Exit, | ||
| 5 | Layer, | ||
| 6 | Option, | ||
| 7 | Predicate, | ||
| 8 | Result, | ||
| 9 | Scope, | ||
| 10 | ScopedCache, | ||
| 11 | } from "effect"; | ||
| 12 | import type { HttpClient } from "effect/unstable/http"; | ||
| 2 | 13 | ||
| 3 | import type { OAuthClientProvider } from "@modelcontextprotocol/sdk/client/auth.js"; | 14 | import type { OAuthClientProvider } from "@modelcontextprotocol/sdk/client/auth.js"; |
| 4 | 15 | ||
| @@ -848,6 +859,7 @@ export interface McpPluginOptions { · 843 unmodified lines | |||
| 848 | * `process.env`. Only enable for trusted single-user contexts. | 859 | * `process.env`. Only enable for trusted single-user contexts. |
| 849 | */ | 860 | */ |
| 850 | readonly dangerouslyAllowStdioMCP?: boolean; | 861 | readonly dangerouslyAllowStdioMCP?: boolean; |
| 862 | readonly httpClientLayer?: Layer.Layer<HttpClient.HttpClient>; | ||
| 851 | /** If provided, source add/remove is mirrored to executor.jsonc | 863 | /** If provided, source add/remove is mirrored to executor.jsonc |
| 852 | * (best-effort — file errors are logged, not raised). */ | 864 | * (best-effort — file errors are logged, not raised). */ |
| 853 | readonly configFile?: ConfigFileSink; | 865 | readonly configFile?: ConfigFileSink; |
| @@ -937,6 +949,7 @@ export const mcpPlugin = definePlugin((options?: McpPluginOptions) => { · 83 unmodified lines | |||
| 937 | storage: (deps): McpBindingStore => makeMcpStore(deps), | 949 | storage: (deps): McpBindingStore => makeMcpStore(deps), |
| 938 | 950 | ||
| 939 | extension: (ctx) => { | 951 | extension: (ctx) => { |
| 952 | const httpClientLayer = options?.httpClientLayer ?? ctx.httpClientLayer; | ||
| 940 | const probeEndpoint = (input: string | McpProbeEndpointInput) => | 953 | const probeEndpoint = (input: string | McpProbeEndpointInput) => |
| 941 | Effect.gen(function* () { | 954 | Effect.gen(function* () { |
| 942 | const endpoint = typeof input === "string" ? input : input.endpoint; | 955 | const endpoint = typeof input === "string" ? input : input.endpoint; |
| @@ -994,6 +1007,7 @@ export const mcpPlugin = definePlugin((options?: McpPluginOptions) => { · 51 unmodified lines | |||
| 994 | // probe and be misclassified as MCP. The shape probe rejects | 1007 | // probe and be misclassified as MCP. The shape probe rejects |
| 995 | // anything whose initialize response isn't 2xx or 401+Bearer. | 1008 | // anything whose initialize response isn't 2xx or 401+Bearer. |
| 996 | const shape = yield* probeMcpEndpointShape(trimmed, { | 1009 | const shape = yield* probeMcpEndpointShape(trimmed, { |
| 1010 | httpClientLayer, | ||
| 997 | headers: probeHeaders, | 1011 | headers: probeHeaders, |
| 998 | queryParams: probeQueryParams, | 1012 | queryParams: probeQueryParams, |
| 999 | }); | 1013 | }); |
| @@ -1575,6 +1589,7 @@ export const mcpPlugin = definePlugin((options?: McpPluginOptions) => { · 575 unmodified lines | |||
| 1575 | 1589 | ||
| 1576 | detect: ({ ctx, url }) => | 1590 | detect: ({ ctx, url }) => |
| 1577 | Effect.gen(function* () { | 1591 | Effect.gen(function* () { |
| 1592 | const httpClientLayer = options?.httpClientLayer ?? ctx.httpClientLayer; | ||
| 1578 | const trimmed = url.trim(); | 1593 | const trimmed = url.trim(); |
| 1579 | if (!trimmed) return null; | 1594 | if (!trimmed) return null; |
| 1580 | 1595 | ||
| @@ -1610,7 +1625,7 @@ export const mcpPlugin = definePlugin((options?: McpPluginOptions) => { · 29 unmodified lines | |||
| 1610 | 1625 | ||
| 1611 | // host publishes RFC 9728 + 8414 metadata) would be classified | 1626 | // host publishes RFC 9728 + 8414 metadata) would be classified |
| 1612 | // as MCP whenever the cross-plugin detector fans out to us. | 1627 | // as MCP whenever the cross-plugin detector fans out to us. |
| 1613 | const shape = yield* probeMcpEndpointShape(trimmed); | 1628 | const shape = yield* probeMcpEndpointShape(trimmed, { httpClientLayer }); |
| 1614 | if (shape.kind !== "mcp") return null; | 1629 | if (shape.kind !== "mcp") return null; |
| 1615 | 1630 | ||
| 1616 | // Confirm OAuth metadata is actually reachable. The shape | 1631 | // Confirm OAuth metadata is actually reachable. The shape |
| @@ -30,7 +30,7 @@ · 29 unmodified lines | |||
| 30 | // round-trip, no DCR — every non-MCP endpoint exits here. | 30 | // round-trip, no DCR — every non-MCP endpoint exits here. |
| 31 | // --------------------------------------------------------------------------- | 31 | // --------------------------------------------------------------------------- |
| 32 | 32 | ||
| 33 | import { Data, Duration, Effect, Option, Schema } from "effect"; | 33 | import { Data, Duration, Effect, Layer, Option, Schema } from "effect"; |
| 34 | import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/unstable/http"; | 34 | import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/unstable/http"; |
| 35 | 35 | ||
| 36 | /** MCP initialize request body used as the shape probe. Any real MCP | 36 | /** MCP initialize request body used as the shape probe. Any real MCP |
| @@ -97,6 +97,7 @@ export type McpShapeProbeResult = · 60 unmodified lines | |||
| 97 | export interface ProbeOptions { | 97 | export interface ProbeOptions { |
| 98 | /** Abort the request after this many ms. Default 8000. */ | 98 | /** Abort the request after this many ms. Default 8000. */ |
| 99 | readonly timeoutMs?: number; | 99 | readonly timeoutMs?: number; |
| 100 | readonly httpClientLayer?: Layer.Layer<HttpClient.HttpClient>; | ||
| 100 | readonly headers?: Record<string, string>; | 101 | readonly headers?: Record<string, string>; |
| 101 | readonly queryParams?: Record<string, string>; | 102 | readonly queryParams?: Record<string, string>; |
| 102 | } | 103 | } |
| @@ -191,7 +192,7 @@ export const probeMcpEndpointShape = ( · 88 unmodified lines | |||
| 191 | reason: `unexpected status ${postResponse.status} for initialize`, | 192 | reason: `unexpected status ${postResponse.status} for initialize`, |
| 192 | } as const; | 193 | } as const; |
| 193 | }).pipe( | 194 | }).pipe( |
| 194 | Effect.provide(FetchHttpClient.layer), | 195 | Effect.provide(options.httpClientLayer ?? FetchHttpClient.layer), |
| 195 | Effect.mapError( | 196 | Effect.mapError( |
| 196 | (cause) => | 197 | (cause) => |
| 197 | new ProbeTransportError({ | 198 | new ProbeTransportError({ |
| @@ -1,6 +1,6 @@ | |||
| 1 | import { Effect, Option, Predicate, Schema } from "effect"; | 1 | import { Effect, Option, Predicate, Schema } from "effect"; |
| 2 | import type { Layer } from "effect"; | 2 | import type { Layer } from "effect"; |
| 3 | import { FetchHttpClient, HttpClient } from "effect/unstable/http"; | 3 | import { HttpClient } from "effect/unstable/http"; |
| 4 | 4 | ||
| 5 | import { OpenApiGroup } from "../api/group"; | 5 | import { OpenApiGroup } from "../api/group"; |
| 6 | import { OpenApiExtensionService, OpenApiHandlers } from "../api/handlers"; | 6 | import { OpenApiExtensionService, OpenApiHandlers } from "../api/handlers"; |
| @@ -754,8 +754,6 @@ const toOpenApiSourceConfig = ( · 747 unmodified lines | |||
| 754 | const isHttpUrl = (s: string): boolean => s.startsWith("http://") || s.startsWith("https://"); | 754 | const isHttpUrl = (s: string): boolean => s.startsWith("http://") || s.startsWith("https://"); |
| 755 | 755 | ||
| 756 | export const openApiPlugin = definePlugin((options?: OpenApiPluginOptions) => { | 756 | export const openApiPlugin = definePlugin((options?: OpenApiPluginOptions) => { |
| 757 | const httpClientLayer = options?.httpClientLayer ?? FetchHttpClient.layer; | ||
| 758 | |||
| 759 | type RebuildInput = { | 757 | type RebuildInput = { |
| 760 | readonly specText: string; | 758 | readonly specText: string; |
| 761 | readonly scope: string; | 759 | readonly scope: string; |
| @@ -902,6 +900,7 @@ export const openApiPlugin = definePlugin((options?: OpenApiPluginOptions) => { · 140 unmodified lines | |||
| 902 | // than surface a 500 through the unwhitelisted error channel. | 900 | // than surface a 500 through the unwhitelisted error channel. |
| 903 | const refreshSourceInternal = (ctx: PluginCtx<OpenapiStore>, sourceId: string, scope: string) => | 901 | const refreshSourceInternal = (ctx: PluginCtx<OpenapiStore>, sourceId: string, scope: string) => |
| 904 | Effect.gen(function* () { | 902 | Effect.gen(function* () { |
| 903 | const httpClientLayer = options?.httpClientLayer ?? ctx.httpClientLayer; | ||
| 905 | const existing = yield* ctx.storage.getSource(sourceId, scope); | 904 | const existing = yield* ctx.storage.getSource(sourceId, scope); |
| 906 | if (!existing) return; | 905 | if (!existing) return; |
| 907 | const effective = yield* resolveEffectiveSourceConfig(ctx, existing); | 906 | const effective = yield* resolveEffectiveSourceConfig(ctx, existing); |
| @@ -938,6 +937,7 @@ export const openApiPlugin = definePlugin((options?: OpenApiPluginOptions) => { · 30 unmodified lines | |||
| 938 | storage: (deps): OpenapiStore => makeDefaultOpenapiStore(deps), | 937 | storage: (deps): OpenapiStore => makeDefaultOpenapiStore(deps), |
| 939 | 938 | ||
| 940 | extension: (ctx) => { | 939 | extension: (ctx) => { |
| 940 | const httpClientLayer = options?.httpClientLayer ?? ctx.httpClientLayer; | ||
| 941 | const addSpecInternal = (config: OpenApiSpecConfig) => | 941 | const addSpecInternal = (config: OpenApiSpecConfig) => |
| 942 | Effect.gen(function* () { | 942 | Effect.gen(function* () { |
| 943 | // Resolve URL → text and parse BEFORE opening a transaction. | 943 | // Resolve URL → text and parse BEFORE opening a transaction. |
| @@ -1173,6 +1173,7 @@ export const openApiPlugin = definePlugin((options?: OpenApiPluginOptions) => { · 229 unmodified lines | |||
| 1173 | 1173 | ||
| 1174 | invokeTool: ({ ctx, toolRow, args }) => | 1174 | invokeTool: ({ ctx, toolRow, args }) => |
| 1175 | Effect.gen(function* () { | 1175 | Effect.gen(function* () { |
| 1176 | const httpClientLayer = options?.httpClientLayer ?? ctx.httpClientLayer; | ||
| 1176 | // toolRow.scope_id is the resolved owning scope of the tool | 1177 | // toolRow.scope_id is the resolved owning scope of the tool |
| 1177 | // (innermost-wins from the executor's stack). The matching | 1178 | // (innermost-wins from the executor's stack). The matching |
| 1178 | // openapi_operation + openapi_source rows live at the same | 1179 | // openapi_operation + openapi_source rows live at the same |
| @@ -1313,8 +1314,9 @@ export const openApiPlugin = definePlugin((options?: OpenApiPluginOptions) => { · 134 unmodified lines | |||
| 1313 | // means stale UI state, which is worth surfacing to the caller. | 1314 | // means stale UI state, which is worth surfacing to the caller. |
| 1314 | refreshSource: ({ ctx, sourceId, scope }) => refreshSourceInternal(ctx, sourceId, scope), | 1315 | refreshSource: ({ ctx, sourceId, scope }) => refreshSourceInternal(ctx, sourceId, scope), |
| 1315 | 1316 | ||
| 1316 | detect: ({ url }) => | 1317 | detect: ({ ctx, url }) => |
| 1317 | Effect.gen(function* () { | 1318 | Effect.gen(function* () { |
| 1319 | const httpClientLayer = options?.httpClientLayer ?? ctx.httpClientLayer; | ||
| 1318 | const trimmed = url.trim(); | 1320 | const trimmed = url.trim(); |
| 1319 | if (!trimmed) return null; | 1321 | if (!trimmed) return null; |
| 1320 | const parsed = yield* Effect.try({ | 1322 | const parsed = yield* Effect.try({ |