Rhys Sullivanfix naming and release7b942677 months agoHistory
603 lines · 18.0 KB · TypeScript
RawDownload
import {  FetchHttpClient,  HttpClient,  HttpClientRequest,} from "@effect/platform";import * as Data from "effect/Data";import * as Effect from "effect/Effect";import * as Layer from "effect/Layer"; import {  applyCookiePlacementsToHeaders,  applyHttpQueryPlacementsToUrl,  applyJsonBodyPlacements,  standardSchemaFromJsonSchema,  toTool,  type HttpRequestPlacements,  type ToolMap,  unknownInputSchema,} from "@executor/codemode-core"; import {  compileOpenApiToolDefinitions,  type OpenApiToolDefinition,} from "./openapi-definitions";import {  serializeOpenApiParameterValue,  serializeOpenApiRequestBody,  withSerializedQueryEntries,  type SerializedOpenApiQueryEntry,} from "./openapi-http-serialization";import { buildOpenApiToolPresentation } from "./openapi-tool-presentation";import { resolveSchemaWithRefHints } from "./openapi-schema-refs";import {  extractOpenApiManifest,  type OpenApiExtractionError,} from "./openapi-extraction";import {  type OpenApiRefHintTable,  type OpenApiInvocationPayload,  type OpenApiSpecInput,  type OpenApiToolManifest,} from "./openapi-types"; type OpenApiToolArgs = Record<string, unknown>;type OpenApiToolParameter = OpenApiInvocationPayload["parameters"][number]; export class OpenApiToolInvocationError extends Data.TaggedError(  "OpenApiToolInvocationError",)<{  operation: string;  message: string;  details: string | null;}> {} const isRecord = (value: unknown): value is Record<string, unknown> =>  typeof value === "object" && value !== null && !Array.isArray(value); const asToolArgs = (value: unknown): OpenApiToolArgs => {  if (!isRecord(value)) {    return {};  }   return value;}; const parameterContainerKeys: Record<  OpenApiToolParameter["location"],  Array<string>> = {  path: ["path", "pathParams", "params"],  query: ["query", "queryParams", "params"],  header: ["headers", "header"],  cookie: ["cookies", "cookie"],}; const readParameterValue = (  args: OpenApiToolArgs,  parameter: OpenApiToolParameter,): unknown => {  const directValue = args[parameter.name];  if (directValue !== undefined) {    return directValue;  }   for (const key of parameterContainerKeys[parameter.location]) {    const container = args[key];    if (!isRecord(container)) {      continue;    }     const nestedValue = container[parameter.name];    if (nestedValue !== undefined) {      return nestedValue;    }  }   return undefined;}; const hasRequestBody = (  args: OpenApiToolArgs,): args is OpenApiToolArgs & { body: unknown } =>  Object.prototype.hasOwnProperty.call(args, "body") && args.body !== undefined; const replacePathTemplate = (  pathTemplate: string,  args: OpenApiToolArgs,  payload: OpenApiInvocationPayload,): string => {  let resolvedPath = pathTemplate;   for (const parameter of payload.parameters) {    if (parameter.location !== "path") {      continue;    }     const parameterValue = readParameterValue(args, parameter);    if (parameterValue === undefined || parameterValue === null) {      if (parameter.required) {        throw new OpenApiToolInvocationError({          operation: "resolve_path",          message: `Missing required path parameter: ${parameter.name}`,          details: pathTemplate,        });      }      continue;    }     const serialized = serializeOpenApiParameterValue(parameter, parameterValue);    resolvedPath = resolvedPath.replaceAll(      `{${parameter.name}}`,      serialized.kind === "path"        ? serialized.value        : encodeURIComponent(String(parameterValue)),    );  }   const unresolvedPathParameters = [...resolvedPath.matchAll(/\{([^{}]+)\}/g)]    .map((match) => match[1])    .filter((value): value is string => typeof value === "string" && value.length > 0);   for (const parameterName of unresolvedPathParameters) {    const parameterValue = args[parameterName]      ?? (isRecord(args.path) ? args.path[parameterName] : undefined)      ?? (isRecord(args.pathParams) ? args.pathParams[parameterName] : undefined)      ?? (isRecord(args.params) ? args.params[parameterName] : undefined);     if (parameterValue === undefined || parameterValue === null) {      continue;    }     resolvedPath = resolvedPath.replaceAll(      `{${parameterName}}`,      encodeURIComponent(String(parameterValue)),    );  }   const stillUnresolvedPathParameters = [...resolvedPath.matchAll(/\{([^{}]+)\}/g)]    .map((match) => match[1])    .filter((value): value is string => typeof value === "string" && value.length > 0);   if (stillUnresolvedPathParameters.length > 0) {    const names = [...new Set(stillUnresolvedPathParameters)].sort().join(", ");    throw new OpenApiToolInvocationError({      operation: "resolve_path",      message: `Unresolved path parameters after substitution: ${names}`,      details: resolvedPath,    });  }   return resolvedPath;}; const normalizeHttpUrl = (value: string): string => {  const trimmed = value.trim();  if (trimmed.length === 0) {    throw new OpenApiToolInvocationError({      operation: "validate_base_url",      message: "OpenAPI baseUrl is empty",      details: null,    });  }   try {    const parsed = new URL(trimmed);    if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {      throw new OpenApiToolInvocationError({        operation: "validate_base_url",        message: "OpenAPI baseUrl must be http or https",        details: parsed.toString(),      });    }     return parsed.toString();  } catch (cause) {    if (cause instanceof OpenApiToolInvocationError) {      throw cause;    }     throw new OpenApiToolInvocationError({      operation: "validate_base_url",      message: "OpenAPI baseUrl is invalid",      details: cause instanceof Error ? cause.message : String(cause),    });  }};   const decodeHttpClientResponseBody = (  response: Awaited<ReturnType<HttpClient.HttpClient["execute"]>> extends Effect.Effect<    infer Value,    infer _Error,    infer _Requirements  >    ? Value    : never,): Effect.Effect<unknown, Error, never> => {  const contentType = response.headers["content-type"]?.toLowerCase() ?? "";   if (contentType.includes("application/json")) {    return response.json.pipe(      Effect.mapError((cause) =>        cause instanceof Error ? cause : new Error(String(cause)),      ),    );  }   return response.text.pipe(    Effect.mapError((cause) =>      cause instanceof Error ? cause : new Error(String(cause)),    ),  );}; const summarizeHttpResponseBody = (body: unknown): string | null => {  if (body === null || body === undefined) {    return null;  }   if (typeof body === "string") {    const trimmed = body.trim();    if (trimmed.length === 0) {      return null;    }     return trimmed.length > 500 ? `${trimmed.slice(0, 500)}...` : trimmed;  }   try {    const serialized = JSON.stringify(body);    return serialized.length > 500 ? `${serialized.slice(0, 500)}...` : serialized;  } catch {    return String(body);  }}; const inputSchemaFromTyping = (input: {  inputSchema: unknown;  refHintTable?: Readonly<OpenApiRefHintTable>;}) => {  const resolvedSchema = resolveSchemaWithRefHints(    input.inputSchema,    input.refHintTable,  ) ?? input.inputSchema;   if (resolvedSchema === undefined || resolvedSchema === null) {    return unknownInputSchema;  }   try {    return standardSchemaFromJsonSchema(resolvedSchema, {      vendor: "openapi",      fallback: unknownInputSchema,    });  } catch {    return unknownInputSchema;  }}; export type CreateOpenApiToolsFromManifestInput = {  manifest: OpenApiToolManifest;  baseUrl: string;  namespace?: string;  sourceKey?: string;  defaultHeaders?: Readonly<Record<string, string>>;  credentialHeaders?: Readonly<Record<string, string>>;  credentialPlacements?: HttpRequestPlacements;  httpClientLayer?: Layer.Layer<HttpClient.HttpClient, never, never>;}; const resolveRequestUrl = (baseUrl: string, resolvedPath: string): URL => {  try {    return new URL(resolvedPath);  } catch {    const base = new URL(baseUrl);    const basePath =      base.pathname === "/"        ? ""        : base.pathname.endsWith("/")          ? base.pathname.slice(0, -1)          : base.pathname;    const pathPart = resolvedPath.startsWith("/") ? resolvedPath : `/${resolvedPath}`;     base.pathname = `${basePath}${pathPart}`.replace(/\/{2,}/g, "/");    base.search = "";    base.hash = "";     return base;  }}; const buildFetchRequest = (input: {  payload: OpenApiInvocationPayload;  args: OpenApiToolArgs;  baseUrl: string;  defaultHeaders: Readonly<Record<string, string>>;  credentialPlacements: HttpRequestPlacements;}): {  url: string;  method: string;  headers: Record<string, string>;  body?: string;} => {  const resolvedPath = replacePathTemplate(    input.payload.pathTemplate,    input.args,    input.payload,  );  const url = resolveRequestUrl(input.baseUrl, resolvedPath);   const headers: Record<string, string> = {    ...input.defaultHeaders,  };  const cookieParts: Array<string> = [];  const queryEntries: SerializedOpenApiQueryEntry[] = [];   for (const parameter of input.payload.parameters) {    if (parameter.location === "path") {      continue;    }     const parameterValue = readParameterValue(input.args, parameter);    if (parameterValue === undefined || parameterValue === null) {      if (parameter.required) {        throw new OpenApiToolInvocationError({          operation: "validate_args",          message: `Missing required ${parameter.location} parameter: ${parameter.name}`,          details: input.payload.pathTemplate,        });      }      continue;    }     const serialized = serializeOpenApiParameterValue(parameter, parameterValue);     if (serialized.kind === "query") {      queryEntries.push(...serialized.entries);    } else if (serialized.kind === "header") {      headers[parameter.name] = serialized.value;    } else if (serialized.kind === "cookie") {      cookieParts.push(        ...serialized.pairs.map((pair) =>          `${pair.name}=${encodeURIComponent(pair.value)}`        ),      );    }  }   if (cookieParts.length > 0) {    headers.cookie = cookieParts.join("; ");  }   let body: string | undefined;  const bodyValues = input.credentialPlacements.bodyValues ?? {};  const hasCredentialBodyValues = Object.keys(bodyValues).length > 0;   if (input.payload.requestBody !== null) {    if (!hasRequestBody(input.args) && !hasCredentialBodyValues) {      if (input.payload.requestBody.required) {        throw new OpenApiToolInvocationError({          operation: "validate_args",          message: "Missing required request body at args.body",          details: input.payload.pathTemplate,        });      }    } else {      const serializedBody = serializeOpenApiRequestBody({        requestBody: input.payload.requestBody,        body: applyJsonBodyPlacements({          body: hasRequestBody(input.args) ? input.args.body : {},          bodyValues,          label: `${input.payload.method.toUpperCase()} ${input.payload.pathTemplate}`,        }),      });       body = serializedBody.bodyText;      headers["content-type"] = serializedBody.contentType;    }  }   const urlWithAuth = applyHttpQueryPlacementsToUrl({    url,    queryParams: input.credentialPlacements.queryParams,  });  const urlWithQueryParams = withSerializedQueryEntries(urlWithAuth, queryEntries);  const headersWithAuthCookies = applyCookiePlacementsToHeaders({    headers,    cookies: input.credentialPlacements.cookies,  });   for (const [key, value] of Object.entries(input.credentialPlacements.headers ?? {})) {    headersWithAuthCookies[key] = value;  }   return {    url: urlWithQueryParams,    method: input.payload.method.toUpperCase(),    headers: headersWithAuthCookies,    body,  };}; const createToolPath = (namespace: string | undefined, definition: OpenApiToolDefinition): string =>  namespace ? `${namespace}.${definition.toolId}` : definition.toolId; export type CreateOpenApiToolFromDefinitionInput = {  definition: OpenApiToolDefinition;  path: string;  sourceKey: string;  baseUrl: string;  defaultHeaders?: Readonly<Record<string, string>>;  credentialHeaders?: Readonly<Record<string, string>>;  credentialPlacements?: HttpRequestPlacements;  refHintTable?: Readonly<OpenApiRefHintTable>;  httpClientLayer?: Layer.Layer<HttpClient.HttpClient, never, never>;}; const normalizeCredentialPlacements = (input: {  credentialHeaders?: Readonly<Record<string, string>>;  credentialPlacements?: HttpRequestPlacements;}): HttpRequestPlacements => ({  headers: {    ...(input.credentialHeaders ?? {}),    ...(input.credentialPlacements?.headers ?? {}),  },  queryParams: input.credentialPlacements?.queryParams,  cookies: input.credentialPlacements?.cookies,  bodyValues: input.credentialPlacements?.bodyValues,}); export const createOpenApiToolFromDefinition = (  input: CreateOpenApiToolFromDefinitionInput,) => {  const defaultHeaders = input.defaultHeaders ?? {};  const credentialPlacements = normalizeCredentialPlacements({    credentialHeaders: input.credentialHeaders,    credentialPlacements: input.credentialPlacements,  });  const httpClientLayer = input.httpClientLayer ?? FetchHttpClient.layer;  const presentation = buildOpenApiToolPresentation({    definition: input.definition,    refHintTable: input.refHintTable,  });   return toTool({    tool: {      description: input.definition.description,      inputSchema: inputSchemaFromTyping({        inputSchema: presentation.inputSchema ?? input.definition.typing?.inputSchema,        refHintTable: input.refHintTable,      }),      execute: async (args: unknown) => {        const decodedArgs = asToolArgs(args);        const request = buildFetchRequest({          payload: input.definition.invocation,          args: decodedArgs,          baseUrl: input.baseUrl,          defaultHeaders,          credentialPlacements,        });         return Effect.runPromise(          Effect.gen(function* () {            const client = yield* HttpClient.HttpClient;            let clientRequest = HttpClientRequest.make(              request.method as Parameters<typeof HttpClientRequest.make>[0],            )(request.url, {              headers: request.headers,            });             if (request.body !== undefined) {              clientRequest = HttpClientRequest.bodyText(                clientRequest,                request.body,                request.headers["content-type"],              );            }             const response = yield* client.execute(clientRequest).pipe(

Lines 1–500 of 603.