History

Add hosted outbound HTTP policy

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.

Rhys Sullivan committed 6 months ago

Commit 5fc67cd1 parent 745deac19 files · +559 −36Browse files

Showing the changes to packages/plugins (4 of 19 files). Show all

Unified · Split

4 files
  1. packages/plugins/graphql/src/sdk/plugin.ts +6 −5
  2. packages/plugins/mcp/src/sdk/plugin.ts +17 −2
  3. packages/plugins/mcp/src/sdk/probe-shape.ts +3 −2
  4. packages/plugins/openapi/src/sdk/plugin.ts +6 −4
packages/plugins/graphql/src/sdk/plugin.ts+6−5
@@ -1,6 +1,6 @@
11 import { Effect, Option, Schema } from "effect";
22 import type { Layer } from "effect";
3−import { FetchHttpClient, HttpClient } from "effect/unstable/http";
3+import { HttpClient } from "effect/unstable/http";
44
55 import { GraphqlGroup } from "../api/group";
66 import { GraphqlExtensionService, GraphqlHandlers } from "../api/handlers";
@@ -916,15 +916,14 @@ const makeGraphqlExtension = ( · 909 unmodified lines
916916 export type GraphqlPluginExtension = ReturnType<typeof makeGraphqlExtension>;
917917
918918 export const graphqlPlugin = definePlugin((options?: GraphqlPluginOptions) => {
919− const httpClientLayer = options?.httpClientLayer ?? FetchHttpClient.layer;
920−
921919 return {
922920 id: "graphql" as const,
923921 packageName: "@executor-js/plugin-graphql",
924922 schema: graphqlSchema,
925923 storage: (deps): GraphqlStore => makeDefaultGraphqlStore(deps),
926924
927− extension: (ctx) => makeGraphqlExtension(ctx, httpClientLayer, options?.configFile),
925+ extension: (ctx) =>
926+ makeGraphqlExtension(ctx, options?.httpClientLayer ?? ctx.httpClientLayer, options?.configFile),
928927
929928 staticSources: (self) => [
930929 {
@@ -969,6 +968,7 @@ export const graphqlPlugin = definePlugin((options?: GraphqlPluginOptions) => { · 38 unmodified lines
969968
970969 invokeTool: ({ ctx, toolRow, args }) =>
971970 Effect.gen(function* () {
971+ const httpClientLayer = options?.httpClientLayer ?? ctx.httpClientLayer;
972972 // toolRow.scope_id is the resolved owning scope of the tool
973973 // (innermost-wins from the executor's stack). The matching
974974 // graphql_operation + graphql_source rows live at the same
@@ -1081,8 +1081,9 @@ export const graphqlPlugin = definePlugin((options?: GraphqlPluginOptions) => { · 106 unmodified lines
10811081
10821082 usagesForConnection: () => Effect.succeed([]),
10831083
1084− detect: ({ url }) =>
1084+ detect: ({ ctx, url }) =>
10851085 Effect.gen(function* () {
1086+ const httpClientLayer = options?.httpClientLayer ?? ctx.httpClientLayer;
10861087 const trimmed = url.trim();
10871088 if (!trimmed) return null;
10881089 const parsed = yield* Effect.try({
packages/plugins/mcp/src/sdk/plugin.ts+17−2
@@ -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";
213
314 import type { OAuthClientProvider } from "@modelcontextprotocol/sdk/client/auth.js";
415
@@ -848,6 +859,7 @@ export interface McpPluginOptions { · 843 unmodified lines
848859 * `process.env`. Only enable for trusted single-user contexts.
849860 */
850861 readonly dangerouslyAllowStdioMCP?: boolean;
862+ readonly httpClientLayer?: Layer.Layer<HttpClient.HttpClient>;
851863 /** If provided, source add/remove is mirrored to executor.jsonc
852864 * (best-effort — file errors are logged, not raised). */
853865 readonly configFile?: ConfigFileSink;
@@ -937,6 +949,7 @@ export const mcpPlugin = definePlugin((options?: McpPluginOptions) => { · 83 unmodified lines
937949 storage: (deps): McpBindingStore => makeMcpStore(deps),
938950
939951 extension: (ctx) => {
952+ const httpClientLayer = options?.httpClientLayer ?? ctx.httpClientLayer;
940953 const probeEndpoint = (input: string | McpProbeEndpointInput) =>
941954 Effect.gen(function* () {
942955 const endpoint = typeof input === "string" ? input : input.endpoint;
@@ -994,6 +1007,7 @@ export const mcpPlugin = definePlugin((options?: McpPluginOptions) => { · 51 unmodified lines
9941007 // probe and be misclassified as MCP. The shape probe rejects
9951008 // anything whose initialize response isn't 2xx or 401+Bearer.
9961009 const shape = yield* probeMcpEndpointShape(trimmed, {
1010+ httpClientLayer,
9971011 headers: probeHeaders,
9981012 queryParams: probeQueryParams,
9991013 });
@@ -1575,6 +1589,7 @@ export const mcpPlugin = definePlugin((options?: McpPluginOptions) => { · 575 unmodified lines
15751589
15761590 detect: ({ ctx, url }) =>
15771591 Effect.gen(function* () {
1592+ const httpClientLayer = options?.httpClientLayer ?? ctx.httpClientLayer;
15781593 const trimmed = url.trim();
15791594 if (!trimmed) return null;
15801595
@@ -1610,7 +1625,7 @@ export const mcpPlugin = definePlugin((options?: McpPluginOptions) => { · 29 unmodified lines
16101625
16111626 // host publishes RFC 9728 + 8414 metadata) would be classified
16121627 // as MCP whenever the cross-plugin detector fans out to us.
1613− const shape = yield* probeMcpEndpointShape(trimmed);
1628+ const shape = yield* probeMcpEndpointShape(trimmed, { httpClientLayer });
16141629 if (shape.kind !== "mcp") return null;
16151630
16161631 // Confirm OAuth metadata is actually reachable. The shape
packages/plugins/mcp/src/sdk/probe-shape.ts+3−2
@@ -30,7 +30,7 @@ · 29 unmodified lines
3030 // round-trip, no DCR — every non-MCP endpoint exits here.
3131 // ---------------------------------------------------------------------------
3232
33−import { Data, Duration, Effect, Option, Schema } from "effect";
33+import { Data, Duration, Effect, Layer, Option, Schema } from "effect";
3434 import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/unstable/http";
3535
3636 /** MCP initialize request body used as the shape probe. Any real MCP
@@ -97,6 +97,7 @@ export type McpShapeProbeResult = · 60 unmodified lines
9797 export interface ProbeOptions {
9898 /** Abort the request after this many ms. Default 8000. */
9999 readonly timeoutMs?: number;
100+ readonly httpClientLayer?: Layer.Layer<HttpClient.HttpClient>;
100101 readonly headers?: Record<string, string>;
101102 readonly queryParams?: Record<string, string>;
102103 }
@@ -191,7 +192,7 @@ export const probeMcpEndpointShape = ( · 88 unmodified lines
191192 reason: `unexpected status ${postResponse.status} for initialize`,
192193 } as const;
193194 }).pipe(
194− Effect.provide(FetchHttpClient.layer),
195+ Effect.provide(options.httpClientLayer ?? FetchHttpClient.layer),
195196 Effect.mapError(
196197 (cause) =>
197198 new ProbeTransportError({
packages/plugins/openapi/src/sdk/plugin.ts+6−4
@@ -1,6 +1,6 @@
11 import { Effect, Option, Predicate, Schema } from "effect";
22 import type { Layer } from "effect";
3−import { FetchHttpClient, HttpClient } from "effect/unstable/http";
3+import { HttpClient } from "effect/unstable/http";
44
55 import { OpenApiGroup } from "../api/group";
66 import { OpenApiExtensionService, OpenApiHandlers } from "../api/handlers";
@@ -754,8 +754,6 @@ const toOpenApiSourceConfig = ( · 747 unmodified lines
754754 const isHttpUrl = (s: string): boolean => s.startsWith("http://") || s.startsWith("https://");
755755
756756 export const openApiPlugin = definePlugin((options?: OpenApiPluginOptions) => {
757− const httpClientLayer = options?.httpClientLayer ?? FetchHttpClient.layer;
758−
759757 type RebuildInput = {
760758 readonly specText: string;
761759 readonly scope: string;
@@ -902,6 +900,7 @@ export const openApiPlugin = definePlugin((options?: OpenApiPluginOptions) => { · 140 unmodified lines
902900 // than surface a 500 through the unwhitelisted error channel.
903901 const refreshSourceInternal = (ctx: PluginCtx<OpenapiStore>, sourceId: string, scope: string) =>
904902 Effect.gen(function* () {
903+ const httpClientLayer = options?.httpClientLayer ?? ctx.httpClientLayer;
905904 const existing = yield* ctx.storage.getSource(sourceId, scope);
906905 if (!existing) return;
907906 const effective = yield* resolveEffectiveSourceConfig(ctx, existing);
@@ -938,6 +937,7 @@ export const openApiPlugin = definePlugin((options?: OpenApiPluginOptions) => { · 30 unmodified lines
938937 storage: (deps): OpenapiStore => makeDefaultOpenapiStore(deps),
939938
940939 extension: (ctx) => {
940+ const httpClientLayer = options?.httpClientLayer ?? ctx.httpClientLayer;
941941 const addSpecInternal = (config: OpenApiSpecConfig) =>
942942 Effect.gen(function* () {
943943 // Resolve URL → text and parse BEFORE opening a transaction.
@@ -1173,6 +1173,7 @@ export const openApiPlugin = definePlugin((options?: OpenApiPluginOptions) => { · 229 unmodified lines
11731173
11741174 invokeTool: ({ ctx, toolRow, args }) =>
11751175 Effect.gen(function* () {
1176+ const httpClientLayer = options?.httpClientLayer ?? ctx.httpClientLayer;
11761177 // toolRow.scope_id is the resolved owning scope of the tool
11771178 // (innermost-wins from the executor's stack). The matching
11781179 // openapi_operation + openapi_source rows live at the same
@@ -1313,8 +1314,9 @@ export const openApiPlugin = definePlugin((options?: OpenApiPluginOptions) => { · 134 unmodified lines
13131314 // means stale UI state, which is worth surfacing to the caller.
13141315 refreshSource: ({ ctx, sourceId, scope }) => refreshSourceInternal(ctx, sourceId, scope),
13151316
1316− detect: ({ url }) =>
1317+ detect: ({ ctx, url }) =>
13171318 Effect.gen(function* () {
1319+ const httpClientLayer = options?.httpClientLayer ?? ctx.httpClientLayer;
13181320 const trimmed = url.trim();
13191321 if (!trimmed) return null;
13201322 const parsed = yield* Effect.try({