| @@ -7,10 +7,7 @@ import { · 6 unmodified lines |
| 7 | 7 | IntegrationDetectionResult, |
| 8 | 8 | IntegrationNotFoundError, |
| 9 | 9 | IntegrationSlug, |
| 10 | | − ToolName, |
| 11 | | − ToolFileJsonSchema, |
| 12 | 10 | ToolResult, |
| 13 | | − authToolFailure, |
| 14 | 11 | definePlugin, |
| 15 | 12 | mergeAuthTemplates, |
| 16 | 13 | sha256Hex, |
| @@ -20,18 +17,10 @@ import { · 3 unmodified lines |
| 20 | 17 | type IntegrationConfig, |
| 21 | 18 | type IntegrationRecord, |
| 22 | 19 | type PluginCtx, |
| 23 | | − type ResolveToolsResult, |
| 24 | 20 | type StorageFailure, |
| 25 | | − type ToolDef, |
| 26 | | − type ToolInvocationCredential, |
| 27 | 21 | } from "@executor-js/sdk/core"; |
| 28 | 22 | |
| 29 | | −import { |
| 30 | | − decodeOpenApiIntegrationConfig, |
| 31 | | − renderAuthTemplate, |
| 32 | | − requiredTemplateVariables, |
| 33 | | − type OpenApiIntegrationConfig, |
| 34 | | −} from "./config"; |
| 23 | +import { decodeOpenApiIntegrationConfig, type OpenApiIntegrationConfig } from "./config"; |
| 35 | 24 | import { OpenApiExtractionError, OpenApiOAuthError, OpenApiParseError } from "./errors"; |
| 36 | 25 | import { parse, resolveSpecText } from "./parse"; |
| 37 | 26 | import { |
| @@ -41,98 +30,20 @@ import { · 3 unmodified lines |
| 41 | 30 | isGoogleDiscoveryUrl, |
| 42 | 31 | } from "./google-discovery"; |
| 43 | 32 | import { extract } from "./extract"; |
| 44 | | −import { compileToolDefinitions, type ToolDefinition } from "./definitions"; |
| 45 | | −import { annotationsForOperation, invokeWithLayer } from "./invoke"; |
| 46 | 33 | import { previewSpec, previewSpecText, type SpecPreview } from "./preview"; |
| 47 | 34 | import { deriveAuthenticationTemplateFromPreview, firstBaseUrlForPreview } from "./derive-auth"; |
| 48 | 35 | import { openApiPresets } from "./presets"; |
| 49 | | −import { makeDefaultOpenapiStore, type OpenapiStore, type StoredOperation } from "./store"; |
| 36 | +import { makeDefaultOpenapiStore, type OpenapiStore } from "./store"; |
| 50 | 37 | import type { Authentication } from "./types"; |
| 51 | | −import { OperationBinding, normalizeOpenApiAuthInputs, type AuthenticationInput } from "./types"; |
| 38 | +import { normalizeOpenApiAuthInputs, type AuthenticationInput } from "./types"; |
| 52 | 39 | import { ApiKeyAuthTemplate, describeApiKeyAuthMethod } from "@executor-js/sdk/http-auth"; |
| 53 | | − |
| 54 | | −// --------------------------------------------------------------------------- |
| 55 | | −// Plugin config |
| 56 | | −// --------------------------------------------------------------------------- |
| 57 | | − |
| 58 | | −const STRINGIFIED_BODY_CAP = 1024; |
| 59 | | −const UpstreamMessageBody = Schema.Struct({ message: Schema.String }); |
| 60 | | −const UpstreamErrorMessageBody = Schema.Struct({ errorMessage: Schema.String }); |
| 61 | | −const UpstreamNestedErrorBody = Schema.Struct({ error: UpstreamMessageBody }); |
| 62 | | −const UpstreamErrorsArrayBody = Schema.Struct({ |
| 63 | | − errors: Schema.Array( |
| 64 | | − Schema.Struct({ |
| 65 | | − detail: Schema.optional(Schema.String), |
| 66 | | − message: Schema.optional(Schema.String), |
| 67 | | − title: Schema.optional(Schema.String), |
| 68 | | − }), |
| 69 | | − ), |
| 70 | | −}); |
| 71 | | −const UpstreamDescriptionBody = Schema.Struct({ |
| 72 | | − detail: Schema.optional(Schema.String), |
| 73 | | − title: Schema.optional(Schema.String), |
| 74 | | − description: Schema.optional(Schema.String), |
| 75 | | −}); |
| 76 | | − |
| 77 | | −const decodeUpstreamMessageBody = Schema.decodeUnknownOption(UpstreamMessageBody); |
| 78 | | −const decodeUpstreamErrorMessageBody = Schema.decodeUnknownOption(UpstreamErrorMessageBody); |
| 79 | | −const decodeUpstreamNestedErrorBody = Schema.decodeUnknownOption(UpstreamNestedErrorBody); |
| 80 | | −const decodeUpstreamErrorsArrayBody = Schema.decodeUnknownOption(UpstreamErrorsArrayBody); |
| 81 | | −const decodeUpstreamDescriptionBody = Schema.decodeUnknownOption(UpstreamDescriptionBody); |
| 82 | | − |
| 83 | | −const clampedStringify = (value: unknown): string => { |
| 84 | | − let s: string; |
| 85 | | − // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: JSON.stringify may throw on cycles; fall back to String() so the upstream body can still be surfaced as ToolError.details fallback text |
| 86 | | − try { |
| 87 | | − s = JSON.stringify(value); |
| 88 | | − } catch { |
| 89 | | − s = String(value); |
| 90 | | − } |
| 91 | | − return s.length > STRINGIFIED_BODY_CAP ? `${s.slice(0, STRINGIFIED_BODY_CAP)}…` : s; |
| 92 | | −}; |
| 93 | | − |
| 94 | | −const firstNonEmpty = (...values: readonly (string | undefined)[]): string | undefined => |
| 95 | | − values.find((value) => value !== undefined && value.length > 0); |
| 96 | | − |
| 97 | | −// Walk known upstream error-body shapes so ToolError.message stays concise |
| 98 | | −// while ToolError.details preserves the original body. |
| 99 | | −const extractUpstreamMessage = (body: unknown, status: number): string => { |
| 100 | | − if (typeof body === "string") { |
| 101 | | − return body.length > 0 ? body : `Upstream returned HTTP ${status}`; |
| 102 | | − } |
| 103 | | − const nested = Option.getOrUndefined(decodeUpstreamNestedErrorBody(body)); |
| 104 | | − const messageBody = Option.getOrUndefined(decodeUpstreamMessageBody(body)); |
| 105 | | − const errorMessageBody = Option.getOrUndefined(decodeUpstreamErrorMessageBody(body)); |
| 106 | | − const errorsBody = Option.getOrUndefined(decodeUpstreamErrorsArrayBody(body)); |
| 107 | | − const descriptionBody = Option.getOrUndefined(decodeUpstreamDescriptionBody(body)); |
| 108 | | − const arrayMessage = errorsBody?.errors |
| 109 | | − .map( |
| 110 | | − ({ |
| 111 | | − detail, |
| 112 | | − message: upstreamMessage, |
| 113 | | − title, |
| 114 | | − }: { |
| 115 | | − detail?: string; |
| 116 | | − message?: string; |
| 117 | | − title?: string; |
| 118 | | − }) => firstNonEmpty(detail, upstreamMessage, title), |
| 119 | | − ) |
| 120 | | − .find((message: string | undefined) => message !== undefined); |
| 121 | | − const message = firstNonEmpty( |
| 122 | | − nested?.error.message, |
| 123 | | − messageBody?.message, |
| 124 | | − errorMessageBody?.errorMessage, |
| 125 | | − arrayMessage, |
| 126 | | − descriptionBody?.detail, |
| 127 | | − descriptionBody?.title, |
| 128 | | − descriptionBody?.description, |
| 129 | | − ); |
| 130 | | − if (message !== undefined) return message; |
| 131 | | − if (body !== null && typeof body === "object") { |
| 132 | | − return clampedStringify(body); |
| 133 | | − } |
| 134 | | − return `Upstream returned HTTP ${status}`; |
| 135 | | −}; |
| 40 | +import { |
| 41 | + compileOpenApiSpec, |
| 42 | + invokeOpenApiBackedTool, |
| 43 | + openApiStoredOperationsFromCompiled, |
| 44 | + resolveOpenApiBackedAnnotations, |
| 45 | + resolveOpenApiBackedTools, |
| 46 | +} from "./backing"; |
| 136 | 47 | |
| 137 | 48 | // --------------------------------------------------------------------------- |
| 138 | 49 | // Extension input shapes |
| @@ -381,38 +292,6 @@ const openApiToolFailure = (code: string, message: string, details?: unknown) => · 242 unmodified lines |
| 381 | 292 | ...(details === undefined ? {} : { details }), |
| 382 | 293 | }); |
| 383 | 294 | |
| 384 | | −const openApiAuthToolFailure = (failure: { |
| 385 | | − readonly code: string; |
| 386 | | − readonly message: string; |
| 387 | | − readonly owner: "org" | "user"; |
| 388 | | − readonly integration: string; |
| 389 | | − readonly connection: string; |
| 390 | | − readonly credentialKind: "secret" | "oauth" | "upstream"; |
| 391 | | − readonly credentialLabel?: string; |
| 392 | | − readonly status?: number; |
| 393 | | − readonly details?: unknown; |
| 394 | | −}) => |
| 395 | | − authToolFailure({ |
| 396 | | − // The auth-tool-failure helper's code set is shared with v1; keep the |
| 397 | | − // string but reference the connection rather than v1's source/slot. |
| 398 | | − code: failure.code as Parameters<typeof authToolFailure>[0]["code"], |
| 399 | | − message: failure.message, |
| 400 | | − source: { id: failure.integration, scope: failure.owner }, |
| 401 | | − credential: { |
| 402 | | − kind: failure.credentialKind, |
| 403 | | − ...(failure.credentialLabel ? { label: failure.credentialLabel } : {}), |
| 404 | | − }, |
| 405 | | − ...(failure.status !== undefined ? { status: failure.status } : {}), |
| 406 | | − ...(failure.details !== undefined |
| 407 | | − ? { |
| 408 | | − upstream: { |
| 409 | | − ...(failure.status !== undefined ? { status: failure.status } : {}), |
| 410 | | − details: failure.details, |
| 411 | | − }, |
| 412 | | − } |
| 413 | | − : {}), |
| 414 | | − }); |
| 415 | | − |
| 416 | 295 | const staticPreviewOutput = (preview: SpecPreview): StaticPreviewSpecOutput => ({ |
| 417 | 296 | title: Option.getOrNull(preview.title), |
| 418 | 297 | version: Option.getOrNull(preview.version), |
| @@ -477,58 +356,6 @@ const staticPreviewOutput = (preview: SpecPreview): StaticPreviewSpecOutput => ( · 58 unmodified lines |
| 477 | 356 | })), |
| 478 | 357 | }); |
| 479 | 358 | |
| 480 | | −// --------------------------------------------------------------------------- |
| 481 | | −// Helpers |
| 482 | | −// --------------------------------------------------------------------------- |
| 483 | | − |
| 484 | | −/** Rewrite OpenAPI `#/components/schemas/X` refs to standard `#/$defs/X`. */ |
| 485 | | −const normalizeOpenApiRefs = (node: unknown): unknown => { |
| 486 | | − if (node == null || typeof node !== "object") return node; |
| 487 | | − if (Array.isArray(node)) { |
| 488 | | − let changed = false; |
| 489 | | − const out = node.map((item) => { |
| 490 | | − const n = normalizeOpenApiRefs(item); |
| 491 | | − if (n !== item) changed = true; |
| 492 | | − return n; |
| 493 | | − }); |
| 494 | | − return changed ? out : node; |
| 495 | | − } |
| 496 | | − |
| 497 | | − const obj = node as Record<string, unknown>; |
| 498 | | − |
| 499 | | − if (typeof obj.$ref === "string") { |
| 500 | | − const match = obj.$ref.match(/^#\/components\/schemas\/(.+)$/); |
| 501 | | − if (match) return { ...obj, $ref: `#/$defs/${match[1]}` }; |
| 502 | | − return obj; |
| 503 | | − } |
| 504 | | − |
| 505 | | − let changed = false; |
| 506 | | − const result: Record<string, unknown> = {}; |
| 507 | | − for (const [k, v] of Object.entries(obj)) { |
| 508 | | − const n = normalizeOpenApiRefs(v); |
| 509 | | − if (n !== v) changed = true; |
| 510 | | − result[k] = n; |
| 511 | | − } |
| 512 | | − return changed ? result : obj; |
| 513 | | −}; |
| 514 | | − |
| 515 | | −const toBinding = (def: ToolDefinition): OperationBinding => |
| 516 | | − OperationBinding.make({ |
| 517 | | − method: def.operation.method, |
| 518 | | − servers: def.operation.servers, |
| 519 | | − pathTemplate: def.operation.pathTemplate, |
| 520 | | − parameters: [...def.operation.parameters], |
| 521 | | − requestBody: def.operation.requestBody, |
| 522 | | − responseBody: def.operation.responseBody, |
| 523 | | − }); |
| 524 | | − |
| 525 | | −const descriptionFor = (def: ToolDefinition): string => { |
| 526 | | − const op = def.operation; |
| 527 | | − return Option.getOrElse(op.description, () => |
| 528 | | − Option.getOrElse(op.summary, () => `${op.method.toUpperCase()} ${op.pathTemplate}`), |
| 529 | | − ); |
| 530 | | −}; |
| 531 | | − |
| 532 | 359 | const specInputToSourceUrl = (spec: OpenApiSpecInput): string | undefined => |
| 533 | 360 | spec.kind === "url" || spec.kind === "googleDiscovery" ? spec.url : undefined; |
| 534 | 361 | |
| @@ -575,88 +402,6 @@ export const describeOpenApiIntegrationDisplay = ( · 40 unmodified lines |
| 575 | 402 | return { url: config?.baseUrl ?? config?.sourceUrl }; |
| 576 | 403 | }; |
| 577 | 404 | |
| 578 | | −// --------------------------------------------------------------------------- |
| 579 | | −// Spec text resolution — the stored config carries the spec's content hash |
| 580 | | −// (`specHash` → blob `spec/<hash>`). Pre-blob rows that inlined the text are |
| 581 | | −// rewritten by the spec-to-blob migrations before this code reads them. |
| 582 | | −// --------------------------------------------------------------------------- |
| 583 | | − |
| 584 | | −const loadSpecText = ( |
| 585 | | − storage: OpenapiStore, |
| 586 | | − config: OpenApiIntegrationConfig, |
| 587 | | −): Effect.Effect<string | null, StorageFailure> => |
| 588 | | − config.specHash != null ? storage.getSpec(config.specHash) : Effect.succeed(null); |
| 589 | | − |
| 590 | | −// --------------------------------------------------------------------------- |
| 591 | | −// Spec → tool definitions (shared by addSpec, resolveTools, and detect) |
| 592 | | −// --------------------------------------------------------------------------- |
| 593 | | − |
| 594 | | −interface CompiledSpec { |
| 595 | | − readonly definitions: readonly ToolDefinition[]; |
| 596 | | − readonly hoistedDefs: Record<string, unknown>; |
| 597 | | − readonly title: string | undefined; |
| 598 | | − /** The spec's `info.description`. */ |
| 599 | | − readonly description: string | undefined; |
| 600 | | −} |
| 601 | | − |
| 602 | | −const compileSpec = ( |
| 603 | | − specText: string, |
| 604 | | −): Effect.Effect<CompiledSpec, OpenApiParseError | OpenApiExtractionError> => |
| 605 | | − Effect.gen(function* () { |
| 606 | | − const doc = yield* parse(specText); |
| 607 | | − const result = yield* extract(doc); |
| 608 | | − const hoistedDefs: Record<string, unknown> = {}; |
| 609 | | − if (doc.components?.schemas) { |
| 610 | | − for (const [k, v] of Object.entries(doc.components.schemas)) { |
| 611 | | − hoistedDefs[k] = normalizeOpenApiRefs(v); |
| 612 | | − } |
| 613 | | − } |
| 614 | | − return { |
| 615 | | − definitions: compileToolDefinitions(result.operations), |
| 616 | | − hoistedDefs, |
| 617 | | − title: Option.getOrUndefined(result.title), |
| 618 | | − description: Option.getOrUndefined(result.description), |
| 619 | | − }; |
| 620 | | − }); |
| 621 | | − |
| 622 | | −// A tool's name carries its structured `group.leaf` path verbatim (e.g. |
| 623 | | −// `aliases.deleteAlias`). The address grammar |
| 624 | | −// `tools.<integration>.<owner>.<connection>.<tool>` treats `<tool>` as the |
| 625 | | −// trailing remainder (see parseToolAddress), so the dotted path needs no |
| 626 | | −// flattening — it nests naturally as |
| 627 | | −// `tools.<integration>.<owner>.<connection>.aliases.deleteAlias`, matching how |
| 628 | | −// the sandbox `tools` proxy joins property access. |
| 629 | | − |
| 630 | | −const toolDefsFromCompiled = (compiled: CompiledSpec): readonly ToolDef[] => |
| 631 | | − compiled.definitions.map( |
| 632 | | − (def): ToolDef => ({ |
| 633 | | − name: ToolName.make(def.toolPath), |
| 634 | | − description: descriptionFor(def), |
| 635 | | − inputSchema: normalizeOpenApiRefs(Option.getOrUndefined(def.operation.inputSchema)), |
| 636 | | − // The output schema is the upstream response body only — transport |
| 637 | | − // status/headers live in the ToolResult `http` side channel, not the |
| 638 | | − // payload (see the invoke handler). |
| 639 | | − outputSchema: Option.match(def.operation.responseBody, { |
| 640 | | − onNone: () => normalizeOpenApiRefs(Option.getOrUndefined(def.operation.outputSchema)), |
| 641 | | − onSome: (responseBody) => |
| 642 | | − Option.isSome(responseBody.fileHint) |
| 643 | | − ? ToolFileJsonSchema |
| 644 | | − : normalizeOpenApiRefs(Option.getOrUndefined(def.operation.outputSchema)), |
| 645 | | − }), |
| 646 | | − annotations: annotationsForOperation(def.operation.method, def.operation.pathTemplate), |
| 647 | | − }), |
| 648 | | − ); |
| 649 | | − |
| 650 | | −const storedOperationsFromCompiled = ( |
| 651 | | − integration: string, |
| 652 | | − compiled: CompiledSpec, |
| 653 | | −): readonly StoredOperation[] => |
| 654 | | − compiled.definitions.map((def) => ({ |
| 655 | | − integration, |
| 656 | | − toolName: def.toolPath, |
| 657 | | − binding: toBinding(def), |
| 658 | | − })); |
| 659 | | − |
| 660 | 405 | // --------------------------------------------------------------------------- |
| 661 | 406 | // Plugin factory |
| 662 | 407 | // --------------------------------------------------------------------------- |
| @@ -752,7 +497,7 @@ export const openApiPlugin = definePlugin((options?: OpenApiPluginOptions) => { · 89 unmodified lines |
| 752 | 497 | // Resolve URL → text and parse BEFORE opening a transaction. Holding |
| 753 | 498 | // `BEGIN` across a network fetch is the Hyperdrive deadlock path. |
| 754 | 499 | const resolved = yield* resolveSpecForInput(config.spec, httpClientLayer); |
| 755 | | − const compiled = yield* compileSpec(resolved.specText); |
| 500 | + const compiled = yield* compileOpenApiSpec(resolved.specText); |
| 756 | 501 | |
| 757 | 502 | // Defaults the add page derives from its preview, applied here so |
| 758 | 503 | // headless callers (MCP, API) get the same integration the UI's |
| @@ -846,7 +591,7 @@ export const openApiPlugin = definePlugin((options?: OpenApiPluginOptions) => { · 87 unmodified lines |
| 846 | 591 | }); |
| 847 | 592 | yield* ctx.storage.putOperations( |
| 848 | 593 | config.slug, |
| 849 | | − storedOperationsFromCompiled(config.slug, compiled), |
| 594 | + openApiStoredOperationsFromCompiled(config.slug, compiled), |
| 850 | 595 | ); |
| 851 | 596 | }), |
| 852 | 597 | ); |
| @@ -890,7 +635,7 @@ export const openApiPlugin = definePlugin((options?: OpenApiPluginOptions) => { · 37 unmodified lines |
| 890 | 635 | // Resolve + compile BEFORE the transaction (same Hyperdrive-deadlock |
| 891 | 636 | // rule as addSpec: never hold BEGIN across a network fetch). |
| 892 | 637 | const resolved = yield* resolveSpecForInput(specInput, httpClientLayer); |
| 893 | | − const compiled = yield* compileSpec(resolved.specText); |
| 638 | + const compiled = yield* compileOpenApiSpec(resolved.specText); |
| 894 | 639 | |
| 895 | 640 | const previousOperations = yield* ctx.storage.listOperations(rawSlug); |
| 896 | 641 | const previousNames = new Set(previousOperations.map((op) => op.toolName)); |
| @@ -921,7 +666,7 @@ export const openApiPlugin = definePlugin((options?: OpenApiPluginOptions) => { · 24 unmodified lines |
| 921 | 666 | }); |
| 922 | 667 | yield* ctx.storage.putOperations( |
| 923 | 668 | rawSlug, |
| 924 | | − storedOperationsFromCompiled(rawSlug, compiled), |
| 669 | + openApiStoredOperationsFromCompiled(rawSlug, compiled), |
| 925 | 670 | ); |
| 926 | 671 | }), |
| 927 | 672 | ); |
| @@ -1133,173 +878,24 @@ export const openApiPlugin = definePlugin((options?: OpenApiPluginOptions) => { · 205 unmodified lines |
| 1133 | 878 | // operation bindings invokeTool needs are persisted at addSpec time; this |
| 1134 | 879 | // hook only shapes the per-connection ToolDefs from the spec blob the |
| 1135 | 880 | // catalog config points at. |
| 1136 | | − resolveTools: ({ |
| 1137 | | − config, |
| 1138 | | − storage, |
| 1139 | | − }: { |
| 1140 | | − readonly integration: Integration; |
| 1141 | | − readonly config: IntegrationConfig; |
| 1142 | | − readonly storage: OpenapiStore; |
| 1143 | | − }): Effect.Effect<ResolveToolsResult, StorageFailure> => |
| 1144 | | − Effect.gen(function* () { |
| 1145 | | − const openApiConfig = decodeOpenApiIntegrationConfig(config); |
| 1146 | | − if (!openApiConfig) return { tools: [], definitions: {} }; |
| 1147 | | − const specText = yield* loadSpecText(storage, openApiConfig); |
| 1148 | | − if (specText == null) return { tools: [], definitions: {} }; |
| 1149 | | − const compiled = yield* compileSpec(specText).pipe( |
| 1150 | | − Effect.catch(() => Effect.succeed(null)), |
| 1151 | | − ); |
| 1152 | | − if (!compiled) return { tools: [], definitions: {} }; |
| 1153 | | − return { |
| 1154 | | − tools: toolDefsFromCompiled(compiled), |
| 1155 | | − definitions: compiled.hoistedDefs, |
| 1156 | | − }; |
| 1157 | | − }), |
| 1158 | | − |
| 1159 | | − invokeTool: ({ |
| 1160 | | − ctx: invokeCtx, |
| 1161 | | − toolRow, |
| 1162 | | − credential, |
| 1163 | | − args, |
| 1164 | | − }: { |
| 1165 | | − readonly ctx: PluginCtx<OpenapiStore>; |
| 1166 | | − readonly toolRow: { readonly integration: string; readonly name: string }; |
| 1167 | | − readonly credential: ToolInvocationCredential; |
| 1168 | | − readonly args: unknown; |
| 1169 | | − }) => |
| 1170 | | − Effect.gen(function* () { |
| 1171 | | − const httpClientLayer = options?.httpClientLayer ?? invokeCtx.httpClientLayer; |
| 1172 | | − const integration = toolRow.integration; |
| 1173 | | − const config = decodeOpenApiIntegrationConfig(credential.config); |
| 1174 | | − |
| 1175 | | − // Resolve the operation binding from the plugin store; fall back to |
| 1176 | | − // re-deriving it from the spec blob when the store has no row (e.g. an |
| 1177 | | − // integration registered without going through addSpec). The fallback |
| 1178 | | − // is the ONLY invoke-path spec read — the hot path never loads it. |
| 1179 | | − const bindingFromSpec = config |
| 1180 | | − ? Effect.gen(function* () { |
| 1181 | | − const specText = yield* loadSpecText(invokeCtx.storage, config).pipe( |
| 1182 | | − Effect.catch(() => Effect.succeed(null)), |
| 1183 | | − ); |
| 1184 | | − const compiled = |
| 1185 | | − specText == null |
| 1186 | | − ? null |
| 1187 | | − : yield* compileSpec(specText).pipe(Effect.catch(() => Effect.succeed(null))); |
| 1188 | | − return compiled |
| 1189 | | − ? storedOperationsFromCompiled(integration, compiled).find( |
| 1190 | | − (op) => op.toolName === toolRow.name, |
| 1191 | | − )?.binding |
| 1192 | | − : undefined; |
| 1193 | | − }) |
| 1194 | | − : Effect.succeed(undefined); |
| 1195 | | − |
| 1196 | | − let binding = (yield* invokeCtx.storage.getOperation(integration, toolRow.name))?.binding; |
| 1197 | | − if ((!binding || Option.isNone(binding.responseBody)) && config) { |
| 1198 | | − binding = (yield* bindingFromSpec) ?? binding; |
| 1199 | | − } |
| 1200 | | − if (!binding) { |
| 1201 | | − return yield* new OpenApiExtractionError({ |
| 1202 | | − message: `No OpenAPI operation found for tool "${toolRow.name}" on "${integration}"`, |
| 1203 | | − }); |
| 1204 | | − } |
| 1205 | | − |
| 1206 | | − const headers: Record<string, string> = { ...(config?.headers ?? {}) }; |
| 1207 | | − const queryParams: Record<string, string> = { |
| 1208 | | − ...(config?.queryParams ?? {}), |
| 1209 | | − }; |
| 1210 | | − |
| 1211 | | − // Apply the auth template (D11): render the connection's resolved inputs |
| 1212 | | − // into the matching template's header/query slots (apiKey) or as a bearer |
| 1213 | | − // Authorization header (oauth). A method with two distinct inputs (e.g. |
| 1214 | | − // Datadog) renders each from its own value. |
| 1215 | | − const template = (config?.authenticationTemplate ?? []).find( |
| 1216 | | − (entry) => String(entry.slug) === String(credential.template), |
| 1217 | | − ); |
| 1218 | | − if (template) { |
| 1219 | | − // Fail if ANY input the template requires is unresolved — not just the |
| 1220 | | − // primary `token` (a Datadog connection has no `token`, only its keys). |
| 1221 | | − const missing = requiredTemplateVariables(template).filter((name) => { |
| 1222 | | − const value = credential.values[name]; |
| 1223 | | − return value == null || value === ""; |
| 1224 | | − }); |
| 1225 | | − if (missing.length > 0) { |
| 1226 | | − return openApiAuthToolFailure({ |
| 1227 | | − code: |
| 1228 | | − template.kind === "oauth2" |
| 1229 | | − ? "oauth_connection_missing" |
| 1230 | | − : "connection_value_missing", |
| 1231 | | − message: `Connection "${credential.connection}" for "${integration}" has no resolvable credential value. Re-authenticate or update the connection.`, |
| 1232 | | − owner: credential.owner, |
| 1233 | | − integration, |
| 1234 | | − connection: String(credential.connection), |
| 1235 | | − credentialKind: template.kind === "oauth2" ? "oauth" : "secret", |
| 1236 | | − }); |
| 1237 | | − } |
| 1238 | | − const rendered = renderAuthTemplate(template, credential.values); |
| 1239 | | − Object.assign(headers, rendered.headers); |
| 1240 | | − Object.assign(queryParams, rendered.queryParams); |
| 1241 | | − } |
| 1242 | | − |
| 1243 | | − const result = yield* invokeWithLayer( |
| 1244 | | − binding, |
| 1245 | | − (args ?? {}) as Record<string, unknown>, |
| 1246 | | − config?.baseUrl ?? "", |
| 1247 | | − headers, |
| 1248 | | − queryParams, |
| 1249 | | − httpClientLayer, |
| 1250 | | − ); |
| 1251 | | − |
| 1252 | | − const ok = result.status >= 200 && result.status < 300; |
| 1253 | | − if (!ok) { |
| 1254 | | − if (result.status === 401 || result.status === 403) { |
| 1255 | | − return openApiAuthToolFailure({ |
| 1256 | | − code: "connection_rejected", |
| 1257 | | − status: result.status, |
| 1258 | | − message: `Upstream rejected credentials for "${integration}" with HTTP ${result.status}. Re-authenticate or update the connection "${credential.connection}" before retrying this tool.`, |
| 1259 | | − owner: credential.owner, |
| 1260 | | − integration, |
| 1261 | | − connection: String(credential.connection), |
| 1262 | | − credentialKind: "upstream", |
| 1263 | | − credentialLabel: "Upstream authorization", |
| 1264 | | − details: result.error, |
| 1265 | | − }); |
| 1266 | | − } |
| 1267 | | − return ToolResult.fail({ |
| 1268 | | − code: "upstream_http_error", |
| 1269 | | − status: result.status, |
| 1270 | | − message: extractUpstreamMessage(result.error, result.status), |
| 1271 | | − details: result.error, |
| 1272 | | − }); |
| 1273 | | − } |
| 1274 | | − // Payload-first: `data` is the upstream response body (matching the |
| 1275 | | − // graphql/mcp plugins); transport facts ride in the `http` side |
| 1276 | | − // channel so pagination/rate-limit headers stay reachable. |
| 1277 | | − return ToolResult.ok(result.data, { |
| 1278 | | − http: { status: result.status, headers: result.headers }, |
| 1279 | | − }); |
| 1280 | | − }), |
| 881 | + resolveTools: ({ config, storage }) => resolveOpenApiBackedTools({ config, storage }), |
| 882 | + |
| 883 | + invokeTool: ({ ctx: invokeCtx, toolRow, credential, args }) => { |
| 884 | + const httpClientLayer = options?.httpClientLayer ?? invokeCtx.httpClientLayer; |
| 885 | + return invokeOpenApiBackedTool({ |
| 886 | + ctx: invokeCtx, |
| 887 | + toolRow, |
| 888 | + credential, |
| 889 | + args, |
| 890 | + httpClientLayer, |
| 891 | + }); |
| 892 | + }, |
| 1281 | 893 | |
| 1282 | | − resolveAnnotations: ({ |
| 1283 | | − ctx: annotationsCtx, |
| 1284 | | − integration, |
| 1285 | | − toolRows, |
| 1286 | | − }: { |
| 1287 | | − readonly ctx: PluginCtx<OpenapiStore>; |
| 1288 | | − readonly integration: string; |
| 1289 | | − readonly toolRows: readonly { readonly name: string }[]; |
| 1290 | | − }) => |
| 1291 | | − Effect.gen(function* () { |
| 1292 | | − const ops = yield* annotationsCtx.storage.listOperations(String(integration)); |
| 1293 | | − const byName = new Map<string, OperationBinding>(); |
| 1294 | | − for (const op of ops) byName.set(op.toolName, op.binding); |
| 1295 | | − const out: Record<string, ReturnType<typeof annotationsForOperation>> = {}; |
| 1296 | | − for (const row of toolRows) { |
| 1297 | | − const binding = byName.get(row.name); |
| 1298 | | − if (binding) { |
| 1299 | | − out[row.name] = annotationsForOperation(binding.method, binding.pathTemplate); |
| 1300 | | − } |
| 1301 | | − } |
| 1302 | | − return out; |
| 894 | + resolveAnnotations: ({ ctx: annotationsCtx, integration, toolRows }) => |
| 895 | + resolveOpenApiBackedAnnotations({ |
| 896 | + ctx: annotationsCtx, |
| 897 | + integration: String(integration), |
| 898 | + toolRows, |
| 1303 | 899 | }), |
| 1304 | 900 | |
| 1305 | 901 | removeConnection: () => Effect.void, |