592 lines · 18.9 KB · TypeScript
RawDownload
import { createHash } from "node:crypto";import { readFile, realpath } from "node:fs/promises";import path from "node:path";import { Effect, Schema } from "effect";import { closePooledConnection, probeConnector } from "./connector-pool";import { listConnectors, upsertConnectors, type ConnectorConfig } from "./connectors-service";import { getGoogleAccount, type GoogleAccountView } from "./google-account";import {  googleWorkspaceConnector,  trustedGoogleWorkspacePlugin,  type GoogleWorkspacePluginId,} from "./google-workspace-adapter";import { discoverPluginBundles, type PluginBundle, type PluginSource } from "./plugin-discovery";import {  type PluginActivationResult,  type PluginRuntimeView,  type PluginToolsView,  type PluginToolState,} from "./plugin-runtime-contract"; export {  type PluginActivationResult,  type PluginRuntimeView,  type PluginToolsView,  type PluginToolState,} from "./plugin-runtime-contract"; const StringRecord = Schema.Record(Schema.String, Schema.String); const StdioServerSchema = Schema.Struct({  type: Schema.optional(Schema.Literal("stdio")),  command: Schema.String,  args: Schema.optional(Schema.Array(Schema.String)),  env: Schema.optional(StringRecord),  cwd: Schema.optional(Schema.String),}); const HttpServerSchema = Schema.Struct({  type: Schema.Literal("http"),  url: Schema.String,  headers: Schema.optional(StringRecord),  bearer_token_env_var: Schema.optional(Schema.String),  oauth_resource: Schema.optional(Schema.String),}); const McpServerSchema = Schema.Union([StdioServerSchema, HttpServerSchema]);const McpManifestSchema = Schema.Struct({  mcpServers: Schema.Record(Schema.String, Schema.Unknown),}); type ResolvedServer = {  connector: ConnectorConfig | null;  blocker?: string;}; export class PluginRuntimeError extends Error {  constructor(    readonly status: number,    message: string,  ) {    super(message);  }} function isContained(root: string, candidate: string): boolean {  const relative = path.relative(root, candidate);  return (    relative === "" ||    (!relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative))  );} async function containedRealPath(root: string, value: string): Promise<string> {  const canonicalRoot = await realpath(root);  const canonicalCandidate = await realpath(path.resolve(canonicalRoot, value));  if (!isContained(canonicalRoot, canonicalCandidate)) {    throw new PluginRuntimeError(422, `Plugin path escapes its bundle: ${value}`);  }  return canonicalCandidate;} async function resolvedCommand(root: string, command: string): Promise<string> {  if (path.isAbsolute(command)) return command;  if (command.startsWith(".") || command.includes(path.sep)) {    return containedRealPath(root, command);  }  return command;} async function resolvedArg(root: string, value: string): Promise<string> {  return value.startsWith(".") ? containedRealPath(root, value) : value;} function connectorId(pluginId: string, serverId: string): string {  const base = `plugin-${pluginId}-${serverId}`.toLowerCase().replace(/[^a-z0-9-_]+/g, "-");  if (base.length <= 64) return base;  const digest = createHash("sha256").update(base).digest("hex").slice(0, 8);  return `${base.slice(0, 55)}-${digest}`;} async function resolvedServer(  bundle: PluginBundle,  serverId: string,  input: unknown,): Promise<ResolvedServer> {  const server = Schema.decodeUnknownSync(McpServerSchema)(input);  const origin = {    kind: "plugin",    id: bundle.plugin.id,    version: bundle.plugin.version,    binding: serverId,  };  const id = connectorId(bundle.plugin.id, serverId);  const name =    serverId === bundle.plugin.id      ? bundle.plugin.displayName      : `${bundle.plugin.displayName}: ${serverId}`;   if ("command" in server) {    const root = await realpath(bundle.rootDir);    const args = await Promise.all((server.args ?? []).map((value) => resolvedArg(root, value)));    return {      connector: {        id,        name,        transport: "stdio",        command: await resolvedCommand(root, server.command),        args,        env: { ...(server.env ?? {}) },        cwd: await containedRealPath(root, server.cwd ?? "."),        origin,        enabled: false,      },    };  }   if (server.oauth_resource) return { connector: null, blocker: "OAuth connection required" };  const bearerEnv = server.bearer_token_env_var;  const bearerToken = bearerEnv ? process.env[bearerEnv]?.trim() : undefined;  if (bearerEnv && !bearerToken) return { connector: null, blocker: `Set ${bearerEnv}` };  return {    connector: {      id,      name,      transport: "http",      url: server.url,      headers: {        ...(server.headers ?? {}),        ...(bearerToken ? { Authorization: `Bearer ${bearerToken}` } : {}),      },      origin,      enabled: false,    },  };} function loadPluginServers(  bundle: PluginBundle,): Effect.Effect<ResolvedServer[], PluginRuntimeError> {  if (!bundle.manifest.mcpServers) return Effect.succeed([]);  return Effect.tryPromise({    try: async () => {      const manifestPath = await containedRealPath(        bundle.rootDir,        bundle.manifest.mcpServers ?? "",      );      const manifest = Schema.decodeUnknownSync(McpManifestSchema)(        JSON.parse(await readFile(manifestPath, "utf8")),      );      return Promise.all(        Object.entries(manifest.mcpServers).map(([serverId, server]) =>          resolvedServer(bundle, serverId, server),        ),      );    },    catch: (error) =>      error instanceof PluginRuntimeError        ? error        : new PluginRuntimeError(            422,            `Invalid MCP manifest for ${bundle.plugin.displayName}: ${error}`,          ),  });} function pluginToolsView(  bundle: PluginBundle,  connectors: ConnectorConfig[],  servers: ResolvedServer[],  reconciliationError?: string,): PluginToolsView {  if (!bundle.manifest.mcpServers) {    return { state: "none", serverCount: 0, allowedToolCount: 0, mode: null };  }  const current = connectors.filter(    (connector) =>      connector.origin?.kind === "plugin" &&      connector.origin.id === bundle.plugin.id &&      connector.origin.version === bundle.plugin.version,  );  const enabled = current.filter((connector) => connector.enabled);  const blockers = [    ...new Set(servers.flatMap((server) => (server.blocker ? [server.blocker] : []))),  ];  const installable = servers.filter((server) => server.connector !== null);  const allowedToolCount = enabled.reduce(    (count, connector) => count + (connector.allowTools?.length ?? 0),    0,  );  if (enabled.length > 0) {    return {      state: "enabled",      serverCount: servers.length,      allowedToolCount,      mode: "observe",    };  }  if (reconciliationError) {    return {      state: "invalid",      serverCount: servers.length,      allowedToolCount: 0,      mode: null,      reason: reconciliationError,    };  }  if (current.length > 0) {    return {      state: "disabled",      serverCount: servers.length,      allowedToolCount: 0,      mode: "observe",    };  }  if (installable.length > 0) {    return {      state: "available",      serverCount: servers.length,      allowedToolCount: 0,      mode: "observe",      ...(blockers.length ? { reason: blockers.join(" · ") } : {}),    };  }  return {    state: "configuration_required",    serverCount: servers.length,    allowedToolCount: 0,    mode: null,    reason: blockers.join(" · ") || "No executable MCP server",  };} function googleWorkspaceRuntimeView(  bundle: PluginBundle,  id: GoogleWorkspacePluginId,  connectors: ConnectorConfig[],  account: GoogleAccountView,): PluginRuntimeView {  const connection = account.connections[id];  const current = connectors.filter(    (connector) =>      connector.origin?.kind === "account-adapter" &&      connector.origin.id === id &&      connector.origin.binding === "google-workspace",  );  const enabled = current.filter((connector) => connector.enabled);  const allowedToolCount = enabled.reduce(    (count, connector) => count + (connector.allowTools?.length ?? 0),    0,  );  const state: PluginToolState =    !account.configured || !connection.connected      ? "configuration_required"      : enabled.length        ? "enabled"        : current.length          ? "disabled"          : "available";  const reason = !account.configured    ? "Add a Google Desktop OAuth client"    : !connection.connected      ? "Finish Google sign-in"      : undefined;  return {    ...bundle.plugin,    account: {      provider: "google",      id,      configured: account.configured,      connected: connection.connected,      email: connection.email,    },    tools: {      state,      serverCount: 1,      allowedToolCount,      mode: connection.connected ? "observe" : null,      ...(reason ? { reason } : {}),    },  };} function runtimeView(  bundle: PluginBundle,  connectors: ConnectorConfig[],  account: GoogleAccountView,  reconciliationError?: string,): Effect.Effect<PluginRuntimeView, PluginRuntimeError> {  return Effect.gen(function* () {    const googleWorkspace = yield* trustedGoogleWorkspacePlugin(bundle);    if (googleWorkspace) {      const view = googleWorkspaceRuntimeView(bundle, googleWorkspace, connectors, account);      const current = connectors.filter(        (connector) =>          connector.origin?.kind === "account-adapter" &&          connector.origin.id === googleWorkspace &&          connector.origin.binding === "google-workspace",      );      return yield* runtimeHealthView(view, current);    }    return yield* Effect.matchEffect(loadPluginServers(bundle), {      onFailure: (error) =>        Effect.succeed({          ...bundle.plugin,          tools: {            state: "invalid" as const,            serverCount: 0,            allowedToolCount: 0,            mode: null,            reason: error.message,          },        }),      onSuccess: (servers) => {        const current = connectors.filter(          (connector) =>            connector.origin?.kind === "plugin" &&            connector.origin.id === bundle.plugin.id &&            connector.origin.version === bundle.plugin.version,        );        return runtimeHealthView(          {            ...bundle.plugin,            tools: pluginToolsView(bundle, connectors, servers, reconciliationError),          },          current,        );      },    });  });} function runtimeHealthView(  view: PluginRuntimeView,  connectors: ConnectorConfig[],): Effect.Effect<PluginRuntimeView> {  if (view.tools.state !== "enabled") return Effect.succeed(view);  return Effect.promise(() =>    Promise.all(connectors.map((connector) => probeConnector(connector))),  ).pipe(    Effect.map((probes) => {      const failures = probes.flatMap((probe, index) =>        probe.ok ? [] : [`${connectors[index]?.name}: ${probe.error ?? "MCP probe failed"}`],      );      return failures.length        ? {            ...view,            tools: { ...view.tools, state: "invalid" as const, reason: failures.join(" · ") },          }        : view;    }),  );} type ConnectorReconciliation = {  connectors: ConnectorConfig[];  errors: Map<string, string>;}; async function reconcileEnabledPluginConnectors(  bundles: PluginBundle[],  initial: ConnectorConfig[],): Promise<ConnectorReconciliation> {  let connectors = initial;  const errors = new Map<string, string>();  for (const bundle of bundles) {    const stale = connectors.filter(      (connector) =>        connector.enabled &&        connector.origin?.kind === "plugin" &&        connector.origin.id === bundle.plugin.id &&        connector.origin.version !== bundle.plugin.version,    );    if (stale.length === 0) continue;    try {      const servers = await Effect.runPromise(loadPluginServers(bundle));      const replacements = stale.map((connector) => {        const replacement = servers.find(          (server) => server.connector?.origin?.binding === connector.origin?.binding,        )?.connector;        if (!replacement || !connector.allowTools?.length) {          throw new PluginRuntimeError(409, "Reconnect to approve the updated plugin");        }        return { ...replacement, allowTools: connector.allowTools, enabled: true };      });      const updated = await Effect.runPromise(enabledObserveConnectors(replacements));      connectors = await upsertConnectors(updated);      updated.forEach((connector) => closePooledConnection(connector.id));    } catch (error) {      errors.set(bundle.plugin.id, error instanceof Error ? error.message : "Plugin update failed");    }  }  return { connectors, errors };} function connectorReconciliationEffect(  bundles: PluginBundle[],): Effect.Effect<ConnectorReconciliation, PluginRuntimeError> {  return Effect.gen(function* () {    const initial = yield* connectorsEffect();    return yield* Effect.tryPromise({      try: () => reconcileEnabledPluginConnectors(bundles, initial),      catch: (error) => new PluginRuntimeError(500, `Plugin reconciliation failed: ${error}`),    });  });} export function refreshEnabledPluginConnectors(  sources?: PluginSource[],): Effect.Effect<void, PluginRuntimeError> {  return Effect.gen(function* () {    const bundles = yield* discoverPluginBundles(sources).pipe(      Effect.mapError((error) => new PluginRuntimeError(500, error.message)),    );    yield* connectorReconciliationEffect(bundles);  });} function connectorsEffect(): Effect.Effect<ConnectorConfig[], PluginRuntimeError> {  return Effect.tryPromise({    try: listConnectors,    catch: (error) => new PluginRuntimeError(500, `Failed to read connector state: ${error}`),  });} function googleAccountEffect(): Effect.Effect<GoogleAccountView, PluginRuntimeError> {  return getGoogleAccount().pipe(    Effect.mapError(      (error) => new PluginRuntimeError(error.status, `Google account failed: ${error.message}`),    ),  );} export function listPluginRuntimeViews(  sources?: PluginSource[],): Effect.Effect<PluginRuntimeView[], PluginRuntimeError> {  return Effect.gen(function* () {    const bundles = yield* discoverPluginBundles(sources).pipe(      Effect.mapError((error) => new PluginRuntimeError(500, error.message)),    );    const reconciliation = yield* connectorReconciliationEffect(bundles);    const account = yield* googleAccountEffect();    return yield* Effect.all(      bundles.map((bundle) =>        runtimeView(          bundle,          reconciliation.connectors,          account,          reconciliation.errors.get(bundle.plugin.id),        ),      ),    );  });} function enabledObserveConnectors(  connectors: ConnectorConfig[],): Effect.Effect<ConnectorConfig[], PluginRuntimeError> {  return Effect.tryPromise({    try: async () => {      const probed = await Promise.all(        connectors.map(async (connector) => ({          connector,          probe: await probeConnector(connector),        })),      );      return probed.map(({ connector, probe }) => {        if (!probe.ok) {          throw new PluginRuntimeError(            502,            `${connector.name} failed to start: ${probe.error ?? "MCP probe failed"}`,          );        }        const requested = connector.allowTools ? new Set(connector.allowTools) : null;        const allowTools = probe.tools          .filter(            (tool) =>              tool.annotations?.readOnlyHint === true && (!requested || requested.has(tool.name)),          )          .map((tool) => tool.name);        if (allowTools.length === 0) {

Lines 1–500 of 592.