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 @@
1import { Effect, Option, Schema } from "effect";1import { Effect, Option, Schema } from "effect";
2import type { Layer } from "effect";2import type { Layer } from "effect";
3import { FetchHttpClient, HttpClient } from "effect/unstable/http";3import { HttpClient } from "effect/unstable/http";
4 4
5import { GraphqlGroup } from "../api/group";5import { GraphqlGroup } from "../api/group";
6import { GraphqlExtensionService, GraphqlHandlers } from "../api/handlers";6import { GraphqlExtensionService, GraphqlHandlers } from "../api/handlers";
@@ -916,15 +916,14 @@ const makeGraphqlExtension = ( · 909 unmodified lines
916export type GraphqlPluginExtension = ReturnType<typeof makeGraphqlExtension>;916export type GraphqlPluginExtension = ReturnType<typeof makeGraphqlExtension>;
917 917
918export const graphqlPlugin = definePlugin((options?: GraphqlPluginOptions) => {918export 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 tool972 // toolRow.scope_id is the resolved owning scope of the tool
973 // (innermost-wins from the executor's stack). The matching973 // (innermost-wins from the executor's stack). The matching
974 // graphql_operation + graphql_source rows live at the same974 // 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({
packages/plugins/mcp/src/sdk/plugin.ts+17−2
@@ -1,4 +1,15 @@
1import { Duration, Effect, Exit, Option, Predicate, Result, Scope, ScopedCache } from "effect";1import {
2 Duration,
3 Effect,
4 Exit,
5 Layer,
6 Option,
7 Predicate,
8 Result,
9 Scope,
10 ScopedCache,
11} from "effect";
12import type { HttpClient } from "effect/unstable/http";
2 13
3import type { OAuthClientProvider } from "@modelcontextprotocol/sdk/client/auth.js";14import 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.jsonc863 /** 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 rejects1007 // 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 classified1626 // 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 shape1631 // 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
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
33import { Data, Duration, Effect, Option, Schema } from "effect";33import { Data, Duration, Effect, Layer, Option, Schema } from "effect";
34import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/unstable/http";34import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/unstable/http";
35 35
36/** MCP initialize request body used as the shape probe. Any real MCP36/** MCP initialize request body used as the shape probe. Any real MCP
@@ -97,6 +97,7 @@ export type McpShapeProbeResult = · 60 unmodified lines
97export interface ProbeOptions {97export 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({
packages/plugins/openapi/src/sdk/plugin.ts+6−4
@@ -1,6 +1,6 @@
1import { Effect, Option, Predicate, Schema } from "effect";1import { Effect, Option, Predicate, Schema } from "effect";
2import type { Layer } from "effect";2import type { Layer } from "effect";
3import { FetchHttpClient, HttpClient } from "effect/unstable/http";3import { HttpClient } from "effect/unstable/http";
4 4
5import { OpenApiGroup } from "../api/group";5import { OpenApiGroup } from "../api/group";
6import { OpenApiExtensionService, OpenApiHandlers } from "../api/handlers";6import { OpenApiExtensionService, OpenApiHandlers } from "../api/handlers";
@@ -754,8 +754,6 @@ const toOpenApiSourceConfig = ( · 747 unmodified lines
754const isHttpUrl = (s: string): boolean => s.startsWith("http://") || s.startsWith("https://");754const isHttpUrl = (s: string): boolean => s.startsWith("http://") || s.startsWith("https://");
755 755
756export const openApiPlugin = definePlugin((options?: OpenApiPluginOptions) => {756export 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 tool1177 // toolRow.scope_id is the resolved owning scope of the tool
1177 // (innermost-wins from the executor's stack). The matching1178 // (innermost-wins from the executor's stack). The matching
1178 // openapi_operation + openapi_source rows live at the same1179 // 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({