225 lines · 6.9 KB · TypeScript
RawDownload
// Boot-time replay of MCP auth from `executor.jsonc`. The plugin already// resolves `secret-public-ref:` strings at connect time via// `ctx.secrets.get` — the regression class here is auth being silently// dropped at the config boundary. `mcp.addSource` persists the source// row even when the remote is unreachable, so unreachable endpoints are// enough to assert on the stored auth shape without running an MCP// server. The new credential-binding flow does require referenced// secrets/connections to exist at the target scope, hence the seeds// before each test. import { afterEach, beforeEach, describe, expect, it } from "@effect/vitest";import { Effect } from "effect";import { mkdtempSync, rmSync, writeFileSync } from "node:fs";import { tmpdir } from "node:os";import { join } from "node:path"; import { SECRET_REF_PREFIX, type ExecutorFileConfig } from "@executor-js/config";import {  ConnectionId,  CreateConnectionInput,  OAUTH2_PROVIDER_KEY,  ScopeId,  SecretId,  TokenMaterial,  createExecutor,  definePlugin,  makeTestConfig,  type SecretProvider,} from "@executor-js/sdk";import { mcpPlugin } from "@executor-js/plugin-mcp"; import { syncFromConfig } from "./config-sync";import type { LocalExecutor } from "./executor"; const UNREACHABLE = "http://127.0.0.1:1/mcp";const TEST_SCOPE = ScopeId.make("test-scope"); const makeMemorySecretsPlugin = () => {  const store = new Map<string, string>();  const provider: SecretProvider = {    key: "memory",    writable: true,    get: (id, scope) => Effect.sync(() => store.get(`${scope} ${id}`) ?? null),    set: (id, value, scope) =>      Effect.sync(() => {        store.set(`${scope} ${id}`, value);      }),    delete: (id, scope) => Effect.sync(() => store.delete(`${scope} ${id}`)),    list: () =>      Effect.sync(() =>        Array.from(store.keys()).map((k) => {          const id = k.split(" ", 2)[1] ?? k;          return { id, name: id };        }),      ),  };  return definePlugin(() => ({    id: "memory-secrets" as const,    storage: () => ({}),    secretProviders: [provider],  }));}; let workDir: string; beforeEach(() => {  workDir = mkdtempSync(join(tmpdir(), "exec-config-sync-"));}); afterEach(() => {  rmSync(workDir, { recursive: true, force: true });}); const writeConfig = (config: ExecutorFileConfig): string => {  const path = join(workDir, "executor.jsonc");  writeFileSync(path, JSON.stringify(config, null, 2));  return path;}; const makeExecutor = () =>  createExecutor(makeTestConfig({ plugins: [makeMemorySecretsPlugin()(), mcpPlugin()] as const })); describe("syncFromConfig — MCP auth replay", () => {  it.effect("strips secret-public-ref prefix from header auth", () =>    Effect.gen(function* () {      const configPath = writeConfig({        sources: [          {            kind: "mcp",            transport: "remote",            name: "PostHog",            endpoint: UNREACHABLE,            namespace: "posthog",            auth: {              kind: "header",              headerName: "Authorization",              secret: `${SECRET_REF_PREFIX}posthog-api-key`,              prefix: "Bearer ",            },          },        ],      });      const executor = yield* makeExecutor();      yield* executor.secrets.set({        id: SecretId.make("posthog-api-key"),        scope: TEST_SCOPE,        name: "PostHog API Key",        value: "phx_test_token",        provider: "memory",      });       yield* syncFromConfig({        // The test executor uses a narrower plugin tuple than LocalExecutor (no        // openapi/graphql), but Match in addSourceFromConfig only dispatches to        // the mcp branch for these fixtures, so the missing methods are never        // touched at runtime.        // oxlint-disable-next-line executor/no-double-cast        executor: executor as unknown as LocalExecutor,        configPath,        targetScope: TEST_SCOPE,      });       const stored = yield* executor.mcp.getSource("posthog", TEST_SCOPE);      expect(stored).not.toBeNull();      expect(stored!.config).toMatchObject({        transport: "remote",        endpoint: UNREACHABLE,        auth: {          kind: "header",          headerName: "Authorization",          secretSlot: "auth:header",          prefix: "Bearer ",        },      });    }),  );   it.effect("passes oauth2 auth through unchanged", () =>    Effect.gen(function* () {      const configPath = writeConfig({        sources: [          {            kind: "mcp",            transport: "remote",            name: "Linear",            endpoint: UNREACHABLE,            namespace: "linear",            auth: { kind: "oauth2", connectionId: "mcp-oauth2-linear" },          },        ],      });      const executor = yield* makeExecutor();      const connectionId = ConnectionId.make("mcp-oauth2-linear");      yield* executor.connections.create(        CreateConnectionInput.make({          id: connectionId,          scope: TEST_SCOPE,          provider: OAUTH2_PROVIDER_KEY,          identityLabel: "user@example.com",          accessToken: TokenMaterial.make({            secretId: SecretId.make(`${connectionId}.access_token`),            name: "MCP Access Token",            value: "access-token-value",          }),          refreshToken: null,          expiresAt: null,          oauthScope: null,          providerState: {            endpoint: UNREACHABLE,            tokenType: "Bearer",            clientInformation: { client_id: "fake" },            authorizationServerUrl: null,            authorizationServerMetadata: null,            resourceMetadataUrl: null,            resourceMetadata: null,          },        }),      );       yield* syncFromConfig({        // oxlint-disable-next-line executor/no-double-cast        executor: executor as unknown as LocalExecutor,        configPath,        targetScope: TEST_SCOPE,      });       const stored = yield* executor.mcp.getSource("linear", TEST_SCOPE);      expect(stored!.config).toMatchObject({        transport: "remote",        auth: { kind: "oauth2", connectionSlot: "auth:oauth2:connection" },      });    }),  );   it.effect("preserves kind:none auth on replay", () =>    Effect.gen(function* () {      const configPath = writeConfig({        sources: [          {            kind: "mcp",            transport: "remote",            name: "DeepWiki",            endpoint: UNREACHABLE,            namespace: "devin",            auth: { kind: "none" },          },        ],      });      const executor = yield* makeExecutor();       yield* syncFromConfig({        // oxlint-disable-next-line executor/no-double-cast        executor: executor as unknown as LocalExecutor,        configPath,        targetScope: TEST_SCOPE,      });       const stored = yield* executor.mcp.getSource("devin", TEST_SCOPE);      expect(stored!.config).toMatchObject({        transport: "remote",        auth: { kind: "none" },      });    }),  );});