Unified · Split
| @@ -27,7 +27,7 @@ import { · 26 unmodified lines | |||
| 27 | type ResolveExecutionEnvironment, | 27 | type ResolveExecutionEnvironment, |
| 28 | resolveLocalWorkspaceContext, | 28 | resolveLocalWorkspaceContext, |
| 29 | SourceIdSchema, | 29 | SourceIdSchema, |
| 30 | SourceRecipeRevisionIdSchema, | 30 | SourceCatalogRevisionIdSchema, |
| 31 | writeLocalSourceArtifact, | 31 | writeLocalSourceArtifact, |
| 32 | writeProjectLocalExecutorConfig, | 32 | writeProjectLocalExecutorConfig, |
| 33 | } from "@executor/control-plane"; | 33 | } from "@executor/control-plane"; |
| @@ -144,7 +144,7 @@ const writeConfiguredLocalMcpSource = (input: { · 110 unmodified lines | |||
| 144 | }; | 144 | }; |
| 145 | 145 | ||
| 146 | const materialization = materializationFromMcpManifestEntries({ | 146 | const materialization = materializationFromMcpManifestEntries({ |
| 147 | recipeRevisionId: SourceRecipeRevisionIdSchema.make("src_recipe_rev_materialization"), | 147 | catalogRevisionId: SourceCatalogRevisionIdSchema.make("src_catalog_rev_materialization"), |
| 148 | endpoint: input.endpoint, | 148 | endpoint: input.endpoint, |
| 149 | manifestEntries: [{ | 149 | manifestEntries: [{ |
| 150 | toolId: "gated_echo", | 150 | toolId: "gated_echo", |
| @@ -35,9 +35,6 @@ const catalog: ToolCatalog = { · 34 unmodified lines | |||
| 35 | outputType: "object", | 35 | outputType: "object", |
| 36 | }); | 36 | }); |
| 37 | }, | 37 | }, |
| 38 | getSchemaBundle() { | ||
| 39 | return Effect.succeed(null); | ||
| 40 | }, | ||
| 41 | searchTools({ limit }) { | 38 | searchTools({ limit }) { |
| 42 | return Effect.succeed( | 39 | return Effect.succeed( |
| 43 | [ | 40 | [ |
| @@ -13,7 +13,6 @@ import type { · 12 unmodified lines | |||
| 13 | ToolMap, | 13 | ToolMap, |
| 14 | ToolNamespace, | 14 | ToolNamespace, |
| 15 | ToolPath, | 15 | ToolPath, |
| 16 | ToolSchemaBundle, | ||
| 17 | } from "./types"; | 16 | } from "./types"; |
| 18 | 17 | ||
| 19 | const tokenize = (value: string): string[] => | 18 | const tokenize = (value: string): string[] => |
| @@ -46,8 +45,8 @@ const projectDescriptor = (input: { · 26 unmodified lines | |||
| 46 | 45 | ||
| 47 | return { | 46 | return { |
| 48 | ...descriptor, | 47 | ...descriptor, |
| 49 | inputSchemaJson: undefined, | 48 | inputSchema: undefined, |
| 50 | outputSchemaJson: undefined, | 49 | outputSchema: undefined, |
| 51 | }; | 50 | }; |
| 52 | }; | 51 | }; |
| 53 | 52 | ||
| @@ -162,7 +161,6 @@ export function createToolCatalogFromTools(input: { · 108 unmodified lines | |||
| 162 | 161 | ||
| 163 | export function createToolCatalogFromEntries(input: { | 162 | export function createToolCatalogFromEntries(input: { |
| 164 | entries: ReadonlyArray<ToolCatalogEntry>; | 163 | entries: ReadonlyArray<ToolCatalogEntry>; |
| 165 | getSchemaBundle?: (input: { id: string }) => Effect.Effect<ToolSchemaBundle | null, unknown>; | ||
| 166 | }): ToolCatalog { | 164 | }): ToolCatalog { |
| 167 | const entries = [...input.entries]; | 165 | const entries = [...input.entries]; |
| 168 | const byPath = new Map( | 166 | const byPath = new Map( |
| @@ -213,10 +211,6 @@ export function createToolCatalogFromEntries(input: { · 44 unmodified lines | |||
| 213 | }) | 211 | }) |
| 214 | : null, | 212 | : null, |
| 215 | ), | 213 | ), |
| 216 | getSchemaBundle: ({ id }) => | ||
| 217 | input.getSchemaBundle | ||
| 218 | ? input.getSchemaBundle({ id }) | ||
| 219 | : Effect.succeed(null), | ||
| 220 | searchTools: ({ query, namespace, limit }) => { | 214 | searchTools: ({ query, namespace, limit }) => { |
| 221 | const queryTokens = tokenize(query); | 215 | const queryTokens = tokenize(query); |
| 222 | 216 | ||
| @@ -283,18 +277,6 @@ export function mergeToolCatalogs(input: { · 60 unmodified lines | |||
| 283 | return null; | 277 | return null; |
| 284 | }), | 278 | }), |
| 285 | 279 | ||
| 286 | getSchemaBundle: ({ id }) => | ||
| 287 | Effect.gen(function* () { | ||
| 288 | for (const catalog of catalogs) { | ||
| 289 | const bundle = yield* catalog.getSchemaBundle({ id }); | ||
| 290 | if (bundle) { | ||
| 291 | return bundle; | ||
| 292 | } | ||
| 293 | } | ||
| 294 | |||
| 295 | return null; | ||
| 296 | }), | ||
| 297 | |||
| 298 | searchTools: ({ query, namespace, limit }) => | 280 | searchTools: ({ query, namespace, limit }) => |
| 299 | Effect.gen(function* () { | 281 | Effect.gen(function* () { |
| 300 | const groups = yield* Effect.forEach( | 282 | const groups = yield* Effect.forEach( |
| @@ -357,7 +339,6 @@ export function createDiscoveryPrimitivesFromToolCatalog(input: { · 56 unmodified lines | |||
| 357 | const describe: DescribePrimitive = { | 339 | const describe: DescribePrimitive = { |
| 358 | tool: ({ path, includeSchemas = false }) => | 340 | tool: ({ path, includeSchemas = false }) => |
| 359 | catalog.getToolByPath({ path, includeSchemas }), | 341 | catalog.getToolByPath({ path, includeSchemas }), |
| 360 | schemaBundle: ({ id }) => catalog.getSchemaBundle({ id }), | ||
| 361 | }; | 342 | }; |
| 362 | 343 | ||
| 363 | const discover: DiscoverPrimitive = ({ | 344 | const discover: DiscoverPrimitive = ({ |
| @@ -406,9 +387,8 @@ export function createDiscoveryPrimitivesFromToolCatalog(input: { · 42 unmodified lines | |||
| 406 | outputType: descriptor.outputType, | 387 | outputType: descriptor.outputType, |
| 407 | ...(includeSchemas | 388 | ...(includeSchemas |
| 408 | ? { | 389 | ? { |
| 409 | inputSchemaJson: descriptor.inputSchemaJson, | 390 | inputSchema: descriptor.inputSchema, |
| 410 | outputSchemaJson: descriptor.outputSchemaJson, | 391 | outputSchema: descriptor.outputSchema, |
| 411 | schemaBundleId: descriptor.schemaBundleId, | ||
| 412 | } | 392 | } |
| 413 | : {}), | 393 | : {}), |
| 414 | }; | 394 | }; |
| @@ -471,7 +451,7 @@ export function buildDynamicExecuteDescription(input: { · 56 unmodified lines | |||
| 471 | "Workflow:", | 451 | "Workflow:", |
| 472 | '1) const matches = await tools.discover({ query: "<intent>", limit: 12 });', | 452 | '1) const matches = await tools.discover({ query: "<intent>", limit: 12 });', |
| 473 | "2) const details = await tools.describe.tool({ path, includeSchemas: true });", | 453 | "2) const details = await tools.describe.tool({ path, includeSchemas: true });", |
| 474 | "3) If details.schemaBundleId is present, fetch it once with tools.describe.schemaBundle({ id }).", | 454 | "3) Read details.inputSchema/details.outputSchema when you need the projected shape.", |
| 475 | "4) Call selected tools.<path>(input).", | 455 | "4) Call selected tools.<path>(input).", |
| 476 | "Do not use fetch; use tools.* only.", | 456 | "Do not use fetch; use tools.* only.", |
| 477 | ].join("\n"); | 457 | ].join("\n"); |
| @@ -326,13 +326,12 @@ describe("codemode-core", () => { · 325 unmodified lines | |||
| 326 | .slice(0, limit) | 326 | .slice(0, limit) |
| 327 | .map((descriptor) => ({ | 327 | .map((descriptor) => ({ |
| 328 | ...descriptor, | 328 | ...descriptor, |
| 329 | inputSchemaJson: includeSchemas ? descriptor.inputSchemaJson : undefined, | 329 | inputSchema: includeSchemas ? descriptor.inputSchema : undefined, |
| 330 | outputSchemaJson: includeSchemas ? descriptor.outputSchemaJson : undefined, | 330 | outputSchema: includeSchemas ? descriptor.outputSchema : undefined, |
| 331 | })), | 331 | })), |
| 332 | ), | 332 | ), |
| 333 | getToolByPath: ({ path }) => | 333 | getToolByPath: ({ path }) => |
| 334 | Effect.succeed(descriptors[path] ?? null), | 334 | Effect.succeed(descriptors[path] ?? null), |
| 335 | getSchemaBundle: () => Effect.succeed(null), | ||
| 336 | searchTools: () => | 335 | searchTools: () => |
| 337 | Effect.succeed([ | 336 | Effect.succeed([ |
| 338 | { path: asToolPath("source.issues.create"), score: 0.93 }, | 337 | { path: asToolPath("source.issues.create"), score: 0.93 }, |
| @@ -369,7 +368,7 @@ describe("codemode-core", () => { · 30 unmodified lines | |||
| 369 | description: "Get repository details", | 368 | description: "Get repository details", |
| 370 | inputType: "{ owner: string; repo: string }", | 369 | inputType: "{ owner: string; repo: string }", |
| 371 | outputType: "Repository", | 370 | outputType: "Repository", |
| 372 | inputSchemaJson: '{"type":"object"}', | 371 | inputSchema: { type: "object" }, |
| 373 | }, | 372 | }, |
| 374 | }, | 373 | }, |
| 375 | { | 374 | { |
| @@ -388,17 +387,6 @@ describe("codemode-core", () => { · 12 unmodified lines | |||
| 388 | }, | 387 | }, |
| 389 | }, | 388 | }, |
| 390 | ], | 389 | ], |
| 391 | getSchemaBundle: ({ id }) => | ||
| 392 | Effect.succeed( | ||
| 393 | id === "bundle_1" | ||
| 394 | ? { | ||
| 395 | id, | ||
| 396 | kind: "json_schema_ref_map", | ||
| 397 | hash: "hash_1", | ||
| 398 | refsJson: "{}", | ||
| 399 | } | ||
| 400 | : null, | ||
| 401 | ), | ||
| 402 | }); | 390 | }); |
| 403 | 391 | ||
| 404 | const namespaces = yield* catalog.listNamespaces({ limit: 10 }); | 392 | const namespaces = yield* catalog.listNamespaces({ limit: 10 }); |
| @@ -414,7 +402,7 @@ describe("codemode-core", () => { · 9 unmodified lines | |||
| 414 | includeSchemas: false, | 402 | includeSchemas: false, |
| 415 | }); | 403 | }); |
| 416 | expect(listed[0]?.path).toBe("github.repos.getRepo"); | 404 | expect(listed[0]?.path).toBe("github.repos.getRepo"); |
| 417 | expect(listed[0]?.inputSchemaJson).toBeUndefined(); | 405 | expect(listed[0]?.inputSchema).toBeUndefined(); |
| 418 | 406 | ||
| 419 | const discovered = yield* catalog.searchTools({ | 407 | const discovered = yield* catalog.searchTools({ |
| 420 | query: "repository issues", | 408 | query: "repository issues", |
| @@ -426,10 +414,7 @@ describe("codemode-core", () => { · 5 unmodified lines | |||
| 426 | path: asToolPath("github.repos.getRepo"), | 414 | path: asToolPath("github.repos.getRepo"), |
| 427 | includeSchemas: true, | 415 | includeSchemas: true, |
| 428 | }); | 416 | }); |
| 429 | expect(described?.inputSchemaJson).toContain('"type"'); | 417 | expect(described?.inputSchema).toEqual({ type: "object" }); |
| 430 | |||
| 431 | const bundle = yield* catalog.getSchemaBundle({ id: "bundle_1" }); | ||
| 432 | expect(bundle?.id).toBe("bundle_1"); | ||
| 433 | }), | 418 | }), |
| 434 | ); | 419 | ); |
| 435 | 420 | ||
| @@ -123,3 +123,19 @@ export const typeSignatureFromSchemaJson = ( · 122 unmodified lines | |||
| 123 | return fallback; | 123 | return fallback; |
| 124 | } | 124 | } |
| 125 | }; | 125 | }; |
| 126 | |||
| 127 | export const typeSignatureFromSchema = ( | ||
| 128 | schema: unknown, | ||
| 129 | fallback: string, | ||
| 130 | maxLength: number = 220, | ||
| 131 | ): string => { | ||
| 132 | if (schema === undefined) { | ||
| 133 | return fallback; | ||
| 134 | } | ||
| 135 | |||
| 136 | try { | ||
| 137 | return schemaToTypeSignature(schema, maxLength); | ||
| 138 | } catch { | ||
| 139 | return fallback; | ||
| 140 | } | ||
| 141 | }; | ||
| @@ -6,7 +6,6 @@ import { toTool } from "./tool-map"; · 5 unmodified lines | |||
| 6 | import { | 6 | import { |
| 7 | ToolDescriptorSchema, | 7 | ToolDescriptorSchema, |
| 8 | ToolNamespaceSchema, | 8 | ToolNamespaceSchema, |
| 9 | ToolSchemaBundleSchema, | ||
| 10 | type ToolCatalog, | 9 | type ToolCatalog, |
| 11 | type ToolMap, | 10 | type ToolMap, |
| 12 | type ToolPath, | 11 | type ToolPath, |
| @@ -52,16 +51,6 @@ const describeToolOutputSchema = Schema.standardSchemaV1( · 39 unmodified lines | |||
| 52 | Schema.NullOr(ToolDescriptorSchema), | 51 | Schema.NullOr(ToolDescriptorSchema), |
| 53 | ); | 52 | ); |
| 54 | 53 | ||
| 55 | const describeSchemaBundleInputSchema = Schema.standardSchemaV1( | ||
| 56 | Schema.Struct({ | ||
| 57 | id: Schema.String, | ||
| 58 | }), | ||
| 59 | ); | ||
| 60 | |||
| 61 | const describeSchemaBundleOutputSchema = Schema.standardSchemaV1( | ||
| 62 | Schema.NullOr(ToolSchemaBundleSchema), | ||
| 63 | ); | ||
| 64 | |||
| 65 | const discoverInputSchema = Schema.standardSchemaV1( | 54 | const discoverInputSchema = Schema.standardSchemaV1( |
| 66 | Schema.Struct({ | 55 | Schema.Struct({ |
| 67 | query: Schema.String, | 56 | query: Schema.String, |
| @@ -181,24 +170,6 @@ export const createSystemToolMap = ( · 113 unmodified lines | |||
| 181 | }, | 170 | }, |
| 182 | }); | 171 | }); |
| 183 | 172 | ||
| 184 | tools["describe.schemaBundle"] = toTool({ | ||
| 185 | tool: { | ||
| 186 | description: "Get a shared schema bundle by id", | ||
| 187 | inputSchema: describeSchemaBundleInputSchema, | ||
| 188 | outputSchema: describeSchemaBundleOutputSchema, | ||
| 189 | execute: ({ id }: { id: string }) => | ||
| 190 | Effect.runPromise( | ||
| 191 | getPrimitives().describe.schemaBundle({ | ||
| 192 | id, | ||
| 193 | }), | ||
| 194 | ), | ||
| 195 | }, | ||
| 196 | metadata: { | ||
| 197 | sourceKey, | ||
| 198 | interaction: "auto", | ||
| 199 | }, | ||
| 200 | }); | ||
| 201 | |||
| 202 | tools.discover = toTool({ | 173 | tools.discover = toTool({ |
| 203 | tool: { | 174 | tool: { |
| 204 | description: "Search tools by intent and return ranked matches", | 175 | description: "Search tools by intent and return ranked matches", |
| @@ -2,7 +2,7 @@ import type { StandardSchemaV1 } from "@standard-schema/spec"; · 1 unmodified line | |||
| 2 | import * as Data from "effect/Data"; | 2 | import * as Data from "effect/Data"; |
| 3 | import * as Effect from "effect/Effect"; | 3 | import * as Effect from "effect/Effect"; |
| 4 | import * as JSONSchema from "effect/JSONSchema"; | 4 | import * as JSONSchema from "effect/JSONSchema"; |
| 5 | import { typeSignatureFromSchemaJson } from "./schema-types"; | 5 | import { typeSignatureFromSchema } from "./schema-types"; |
| 6 | 6 | ||
| 7 | import type { | 7 | import type { |
| 8 | ElicitationRequest, | 8 | ElicitationRequest, |
| @@ -336,7 +336,7 @@ export const toExecutorTool = wrapTool; · 327 unmodified lines | |||
| 336 | const isToolDefinition = (value: ToolInput): value is ToolDefinition => | 336 | const isToolDefinition = (value: ToolInput): value is ToolDefinition => |
| 337 | typeof value === "object" && value !== null && "tool" in value; | 337 | typeof value === "object" && value !== null && "tool" in value; |
| 338 | 338 | ||
| 339 | const stringifySchema = (value: unknown): string | undefined => { | 339 | const deriveSchemaValue = (value: unknown): unknown | undefined => { |
| 340 | if (value === undefined || value === null) { | 340 | if (value === undefined || value === null) { |
| 341 | return undefined; | 341 | return undefined; |
| 342 | } | 342 | } |
| @@ -347,20 +347,20 @@ const stringifySchema = (value: unknown): string | undefined => { · 4 unmodified lines | |||
| 347 | && value !== null | 347 | && value !== null |
| 348 | && "~standard" in value | 348 | && "~standard" in value |
| 349 | ) { | 349 | ) { |
| 350 | return JSON.stringify(JSONSchema.make(value as any)); | 350 | return JSONSchema.make(value as any); |
| 351 | } | 351 | } |
| 352 | 352 | ||
| 353 | return JSON.stringify(value); | 353 | return value; |
| 354 | } catch { | 354 | } catch { |
| 355 | return undefined; | 355 | return undefined; |
| 356 | } | 356 | } |
| 357 | }; | 357 | }; |
| 358 | 358 | ||
| 359 | const inferTypeFromSchemaJson = ( | 359 | const inferTypeFromSchemaValue = ( |
| 360 | schemaJson: string | undefined, | 360 | schema: unknown, |
| 361 | fallback: string, | 361 | fallback: string, |
| 362 | maxLength: number = 240, | 362 | maxLength: number = 240, |
| 363 | ): string => typeSignatureFromSchemaJson(schemaJson, fallback, maxLength); | 363 | ): string => typeSignatureFromSchema(schema, fallback, maxLength); |
| 364 | 364 | ||
| 365 | export function createToolsFromRecord(input: { | 365 | export function createToolsFromRecord(input: { |
| 366 | tools: Record<string, ExecutableTool>; | 366 | tools: Record<string, ExecutableTool>; |
| @@ -418,13 +418,13 @@ export function toolDescriptorsFromTools(input: { · 51 unmodified lines | |||
| 418 | return resolvedTools.map((entry) => { | 418 | return resolvedTools.map((entry) => { |
| 419 | const metadata = entry.metadata; | 419 | const metadata = entry.metadata; |
| 420 | const definition = entry.tool; | 420 | const definition = entry.tool; |
| 421 | const inputSchemaJson = | 421 | const inputSchema = |
| 422 | metadata?.inputSchemaJson | 422 | metadata?.inputSchema |
| 423 | ?? stringifySchema(definition.inputSchema) | 423 | ?? deriveSchemaValue(definition.inputSchema) |
| 424 | ?? stringifySchema(definition.parameters); | 424 | ?? deriveSchemaValue(definition.parameters); |
| 425 | const outputSchemaJson = | 425 | const outputSchema = |
| 426 | metadata?.outputSchemaJson | 426 | metadata?.outputSchema |
| 427 | ?? stringifySchema(definition.outputSchema); | 427 | ?? deriveSchemaValue(definition.outputSchema); |
| 428 | 428 | ||
| 429 | return { | 429 | return { |
| 430 | path: entry.path, | 430 | path: entry.path, |
| @@ -433,21 +433,20 @@ export function toolDescriptorsFromTools(input: { · 2 unmodified lines | |||
| 433 | interaction: metadata?.interaction, | 433 | interaction: metadata?.interaction, |
| 434 | elicitation: metadata?.elicitation, | 434 | elicitation: metadata?.elicitation, |
| 435 | inputType: | 435 | inputType: |
| 436 | metadata?.inputType ?? inferTypeFromSchemaJson(inputSchemaJson, "unknown"), | 436 | metadata?.inputType ?? inferTypeFromSchemaValue(inputSchema, "unknown"), |
| 437 | outputType: | 437 | outputType: |
| 438 | metadata?.outputType ?? inferTypeFromSchemaJson(outputSchemaJson, "unknown"), | 438 | metadata?.outputType ?? inferTypeFromSchemaValue(outputSchema, "unknown"), |
| 439 | inputSchemaJson, | 439 | ...(inputSchema !== undefined ? { inputSchema } : {}), |
| 440 | outputSchemaJson, | 440 | ...(outputSchema !== undefined ? { outputSchema } : {}), |
| 441 | ...(metadata?.schemaBundleId ? { schemaBundleId: metadata.schemaBundleId } : {}), | 441 | ...(metadata?.exampleInput !== undefined |
| 442 | ...(metadata?.exampleInputJson | 442 | ? { exampleInput: metadata.exampleInput } |
| 443 | ? { exampleInputJson: metadata.exampleInputJson } | ||
| 444 | : {}), | 443 | : {}), |
| 445 | ...(metadata?.exampleOutputJson | 444 | ...(metadata?.exampleOutput !== undefined |
| 446 | ? { exampleOutputJson: metadata.exampleOutputJson } | 445 | ? { exampleOutput: metadata.exampleOutput } |
| 447 | : {}), | 446 | : {}), |
| 448 | ...(metadata?.providerKind ? { providerKind: metadata.providerKind } : {}), | 447 | ...(metadata?.providerKind ? { providerKind: metadata.providerKind } : {}), |
| 449 | ...(metadata?.providerDataJson | 448 | ...(metadata?.providerData !== undefined |
| 450 | ? { providerDataJson: metadata.providerDataJson } | 449 | ? { providerData: metadata.providerData } |
| 451 | : {}), | 450 | : {}), |
| 452 | } satisfies ToolDescriptor; | 451 | } satisfies ToolDescriptor; |
| 453 | }); | 452 | }); |
| @@ -80,14 +80,13 @@ export type ToolMetadata = { · 79 unmodified lines | |||
| 80 | elicitation?: ElicitationRequest; | 80 | elicitation?: ElicitationRequest; |
| 81 | inputType?: string; | 81 | inputType?: string; |
| 82 | outputType?: string; | 82 | outputType?: string; |
| 83 | inputSchemaJson?: string; | 83 | inputSchema?: unknown; |
| 84 | outputSchemaJson?: string; | 84 | outputSchema?: unknown; |
| 85 | schemaBundleId?: string; | 85 | exampleInput?: unknown; |
| 86 | exampleInputJson?: string; | 86 | exampleOutput?: unknown; |
| 87 | exampleOutputJson?: string; | ||
| 88 | sourceKey?: string; | 87 | sourceKey?: string; |
| 89 | providerKind?: string; | 88 | providerKind?: string; |
| 90 | providerDataJson?: string; | 89 | providerData?: unknown; |
| 91 | }; | 90 | }; |
| 92 | 91 | ||
| 93 | export type ToolExecutionContext = { | 92 | export type ToolExecutionContext = { |
| @@ -134,13 +133,12 @@ export type ToolDescriptor = { · 40 unmodified lines | |||
| 134 | elicitation?: ElicitationRequest; | 133 | elicitation?: ElicitationRequest; |
| 135 | inputType?: string; | 134 | inputType?: string; |
| 136 | outputType?: string; | 135 | outputType?: string; |
| 137 | inputSchemaJson?: string; | 136 | inputSchema?: unknown; |
| 138 | outputSchemaJson?: string; | 137 | outputSchema?: unknown; |
| 139 | schemaBundleId?: string; | 138 | exampleInput?: unknown; |
| 140 | exampleInputJson?: string; | 139 | exampleOutput?: unknown; |
| 141 | exampleOutputJson?: string; | ||
| 142 | providerKind?: string; | 140 | providerKind?: string; |
| 143 | providerDataJson?: string; | 141 | providerData?: unknown; |
| 144 | }; | 142 | }; |
| 145 | 143 | ||
| 146 | export const ToolDescriptorSchema = Schema.Struct({ | 144 | export const ToolDescriptorSchema = Schema.Struct({ |
| @@ -153,27 +151,12 @@ export const ToolDescriptorSchema = Schema.Struct({ · 6 unmodified lines | |||
| 153 | elicitation: Schema.optional(Schema.Unknown), | 151 | elicitation: Schema.optional(Schema.Unknown), |
| 154 | inputType: Schema.optional(Schema.String), | 152 | inputType: Schema.optional(Schema.String), |
| 155 | outputType: Schema.optional(Schema.String), | 153 | outputType: Schema.optional(Schema.String), |
| 156 | inputSchemaJson: Schema.optional(Schema.String), | 154 | inputSchema: Schema.optional(Schema.Unknown), |
| 157 | outputSchemaJson: Schema.optional(Schema.String), | 155 | outputSchema: Schema.optional(Schema.Unknown), |
| 158 | schemaBundleId: Schema.optional(Schema.String), | 156 | exampleInput: Schema.optional(Schema.Unknown), |
| 159 | exampleInputJson: Schema.optional(Schema.String), | 157 | exampleOutput: Schema.optional(Schema.Unknown), |
| 160 | exampleOutputJson: Schema.optional(Schema.String), | ||
| 161 | providerKind: Schema.optional(Schema.String), | 158 | providerKind: Schema.optional(Schema.String), |
| 162 | providerDataJson: Schema.optional(Schema.String), | 159 | providerData: Schema.optional(Schema.Unknown), |
| 163 | }); | ||
| 164 | |||
| 165 | export type ToolSchemaBundle = { | ||
| 166 | id: string; | ||
| 167 | kind: string; | ||
| 168 | hash: string; | ||
| 169 | refsJson: string; | ||
| 170 | }; | ||
| 171 | |||
| 172 | export const ToolSchemaBundleSchema = Schema.Struct({ | ||
| 173 | id: Schema.String, | ||
| 174 | kind: Schema.String, | ||
| 175 | hash: Schema.String, | ||
| 176 | refsJson: Schema.String, | ||
| 177 | }); | 160 | }); |
| 178 | 161 | ||
| 179 | export type ToolNamespace = { | 162 | export type ToolNamespace = { |
| @@ -217,10 +200,6 @@ export interface ToolCatalog { · 37 unmodified lines | |||
| 217 | includeSchemas: boolean; | 200 | includeSchemas: boolean; |
| 218 | }): Effect.Effect<ToolDescriptor | null, unknown>; | 201 | }): Effect.Effect<ToolDescriptor | null, unknown>; |
| 219 | 202 | ||
| 220 | getSchemaBundle(input: { | ||
| 221 | id: string; | ||
| 222 | }): Effect.Effect<ToolSchemaBundle | null, unknown>; | ||
| 223 | |||
| 224 | searchTools(input: { | 203 | searchTools(input: { |
| 225 | query: string; | 204 | query: string; |
| 226 | namespace?: string; | 205 | namespace?: string; |
| @@ -249,9 +228,6 @@ export type DescribePrimitive = { · 22 unmodified lines | |||
| 249 | path: ToolPath; | 228 | path: ToolPath; |
| 250 | includeSchemas?: boolean; | 229 | includeSchemas?: boolean; |
| 251 | }): Effect.Effect<ToolDescriptor | null, unknown>; | 230 | }): Effect.Effect<ToolDescriptor | null, unknown>; |
| 252 | schemaBundle(input: { | ||
| 253 | id: string; | ||
| 254 | }): Effect.Effect<ToolSchemaBundle | null, unknown>; | ||
| 255 | }; | 231 | }; |
| 256 | 232 | ||
| 257 | export type DiscoverPrimitive = (input: { | 233 | export type DiscoverPrimitive = (input: { |
| @@ -90,6 +90,18 @@ const asStringArray = (value: unknown): ReadonlyArray<string> => · 89 unmodified lines | |||
| 90 | }) | 90 | }) |
| 91 | : []; | 91 | : []; |
| 92 | 92 | ||
| 93 | const parseJson = (value: string | undefined): unknown | undefined => { | ||
| 94 | if (!value) { | ||
| 95 | return undefined; | ||
| 96 | } | ||
| 97 | |||
| 98 | try { | ||
| 99 | return JSON.parse(value) as unknown; | ||
| 100 | } catch { | ||
| 101 | return undefined; | ||
| 102 | } | ||
| 103 | }; | ||
| 104 | |||
| 93 | const contentHash = (value: string): string => | 105 | const contentHash = (value: string): string => |
| 94 | sha256Hex(value); | 106 | sha256Hex(value); |
| 95 | 107 | ||
| @@ -384,17 +396,29 @@ const manifestMethodFromDiscoveryMethod = (input: { · 288 unmodified lines | |||
| 384 | ...(methodRequestSchemaJson({ | 396 | ...(methodRequestSchemaJson({ |
| 385 | method: input.method, | 397 | method: input.method, |
| 386 | topLevelSchemas: input.topLevelSchemas, | 398 | topLevelSchemas: input.topLevelSchemas, |
| 387 | }) ? { inputSchemaJson: methodRequestSchemaJson({ | 399 | }) |
| 388 | method: input.method, | 400 | ? { |
| 389 | topLevelSchemas: input.topLevelSchemas, | 401 | inputSchema: parseJson( |
| 390 | }) } : {}), | 402 | methodRequestSchemaJson({ |
| 403 | method: input.method, | ||
| 404 | topLevelSchemas: input.topLevelSchemas, | ||
| 405 | }), | ||
| 406 | ), | ||
| 407 | } | ||
| 408 | : {}), | ||
| 391 | ...(methodResponseSchemaJson({ | 409 | ...(methodResponseSchemaJson({ |
| 392 | method: input.method, | 410 | method: input.method, |
| 393 | topLevelSchemas: input.topLevelSchemas, | 411 | topLevelSchemas: input.topLevelSchemas, |
| 394 | }) ? { outputSchemaJson: methodResponseSchemaJson({ | 412 | }) |
| 395 | method: input.method, | 413 | ? { |
| 396 | topLevelSchemas: input.topLevelSchemas, | 414 | outputSchema: parseJson( |
| 397 | }) } : {}), | 415 | methodResponseSchemaJson({ |
| 416 | method: input.method, | ||
| 417 | topLevelSchemas: input.topLevelSchemas, | ||
| 418 | }), | ||
| 419 | ), | ||
| 420 | } | ||
| 421 | : {}), | ||
| 398 | }; | 422 | }; |
| 399 | }; | 423 | }; |
| 400 | 424 | ||
| @@ -117,8 +117,8 @@ describe("google discovery tools", () => { · 116 unmodified lines | |||
| 117 | expect(definitions.length).toBeGreaterThan(10); | 117 | expect(definitions.length).toBeGreaterThan(10); |
| 118 | expect(valuesGet).toBeDefined(); | 118 | expect(valuesGet).toBeDefined(); |
| 119 | expect(valuesGet?.method).toBe("get"); | 119 | expect(valuesGet?.method).toBe("get"); |
| 120 | expect(valuesGet?.inputSchemaJson).toContain("\"spreadsheetId\""); | 120 | expect(JSON.stringify(valuesGet?.inputSchema)).toContain("\"spreadsheetId\""); |
| 121 | expect(valuesGet?.outputSchemaJson).toContain("\"values\""); | 121 | expect(JSON.stringify(valuesGet?.outputSchema)).toContain("\"values\""); |
| 122 | expect(Object.keys(manifest.schemaRefTable ?? {})).toContain( | 122 | expect(Object.keys(manifest.schemaRefTable ?? {})).toContain( |
| 123 | "#/$defs/google/ValueRange", | 123 | "#/$defs/google/ValueRange", |
| 124 | ); | 124 | ); |
| @@ -27,10 +27,10 @@ import { · 26 unmodified lines | |||
| 27 | } from "./google-discovery-types"; | 27 | } from "./google-discovery-types"; |
| 28 | 28 | ||
| 29 | const decodeGoogleDiscoveryToolProviderDataJson = Schema.decodeUnknownEither( | 29 | const decodeGoogleDiscoveryToolProviderDataJson = Schema.decodeUnknownEither( |
| 30 | Schema.parseJson(GoogleDiscoveryToolProviderDataSchema), | 30 | GoogleDiscoveryToolProviderDataSchema, |
| 31 | ); | 31 | ); |
| 32 | 32 | ||
| 33 | export const googleDiscoveryProviderDataJsonFromDefinition = ( | 33 | export const googleDiscoveryProviderDataFromDefinition = ( |
| 34 | input: { | 34 | input: { |
| 35 | service: string; | 35 | service: string; |
| 36 | version: string; | 36 | version: string; |
| @@ -38,8 +38,7 @@ export const googleDiscoveryProviderDataJsonFromDefinition = ( · 1 unmodified line | |||
| 38 | servicePath: string; | 38 | servicePath: string; |
| 39 | definition: GoogleDiscoveryManifestMethod; | 39 | definition: GoogleDiscoveryManifestMethod; |
| 40 | }, | 40 | }, |
| 41 | ): string => | 41 | ): typeof GoogleDiscoveryToolProviderDataSchema.Type => ({ |
| 42 | JSON.stringify({ | ||
| 43 | kind: "google_discovery", | 42 | kind: "google_discovery", |
| 44 | service: input.service, | 43 | service: input.service, |
| 45 | version: input.version, | 44 | version: input.version, |
| @@ -115,19 +114,14 @@ const setNestedSchemaProperty = ( · 69 unmodified lines | |||
| 115 | }; | 114 | }; |
| 116 | 115 | ||
| 117 | const materializeSchemaWithRefDefinitions = (input: { | 116 | const materializeSchemaWithRefDefinitions = (input: { |
| 118 | schemaJson: string | undefined; | 117 | schema: unknown; |
| 119 | refTable?: Readonly<Record<string, unknown>>; | 118 | refTable?: Readonly<Record<string, unknown>>; |
| 120 | }): Record<string, unknown> => { | 119 | }): Record<string, unknown> => { |
| 121 | if (!input.schemaJson) { | 120 | if (input.schema === undefined || input.schema === null) { |
| 122 | return {}; | 121 | return {}; |
| 123 | } | 122 | } |
| 124 | 123 | ||
| 125 | let rootSchema: Record<string, unknown>; | 124 | const rootSchema = asRecord(input.schema); |
| 126 | try { | ||
| 127 | rootSchema = asRecord(JSON.parse(input.schemaJson) as unknown); | ||
| 128 | } catch { | ||
| 129 | return {}; | ||
| 130 | } | ||
| 131 | 125 | ||
| 132 | if (!input.refTable || Object.keys(input.refTable).length === 0) { | 126 | if (!input.refTable || Object.keys(input.refTable).length === 0) { |
| 133 | return rootSchema; | 127 | return rootSchema; |
| @@ -250,14 +244,14 @@ const buildGoogleDiscoveryRequest = (input: { · 116 unmodified lines | |||
| 250 | defaultHeaders: Readonly<Record<string, string>>; | 244 | defaultHeaders: Readonly<Record<string, string>>; |
| 251 | credentialPlacements: HttpRequestPlacements; | 245 | credentialPlacements: HttpRequestPlacements; |
| 252 | baseUrl?: string | undefined; | 246 | baseUrl?: string | undefined; |
| 253 | providerDataJson: string; | 247 | providerData: typeof GoogleDiscoveryToolProviderDataSchema.Type; |
| 254 | }): { | 248 | }): { |
| 255 | method: string; | 249 | method: string; |
| 256 | url: URL; | 250 | url: URL; |
| 257 | headers: Record<string, string>; | 251 | headers: Record<string, string>; |
| 258 | body?: string; | 252 | body?: string; |
| 259 | } => { | 253 | } => { |
| 260 | const providerData = decodeGoogleDiscoveryToolProviderDataJson(input.providerDataJson); | 254 | const providerData = decodeGoogleDiscoveryToolProviderDataJson(input.providerData); |
| 261 | if (providerData._tag === "Left") { | 255 | if (providerData._tag === "Left") { |
| 262 | throw new Error("Invalid Google Discovery provider data"); | 256 | throw new Error("Invalid Google Discovery provider data"); |
| 263 | } | 257 | } |
| @@ -365,28 +359,12 @@ export type CreateGoogleDiscoveryToolFromDefinitionInput = { · 101 unmodified lines | |||
| 365 | export const createGoogleDiscoveryToolFromDefinition = ( | 359 | export const createGoogleDiscoveryToolFromDefinition = ( |
| 366 | input: CreateGoogleDiscoveryToolFromDefinitionInput, | 360 | input: CreateGoogleDiscoveryToolFromDefinitionInput, |
| 367 | ) => { | 361 | ) => { |
| 368 | const providerDataJson = JSON.stringify({ | 362 | const providerData = googleDiscoveryProviderDataFromDefinition({ |
| 369 | kind: "google_discovery", | ||
| 370 | service: input.service, | 363 | service: input.service, |
| 371 | version: input.version, | 364 | version: input.version, |
| 372 | toolId: input.definition.toolId, | 365 | rootUrl: input.rootUrl, |
| 373 | rawToolId: input.definition.rawToolId, | 366 | servicePath: input.servicePath, |
| 374 | methodId: input.definition.methodId, | 367 | definition: input.definition, |
| 375 | group: input.definition.group, | ||
| 376 | leaf: input.definition.leaf, | ||
| 377 | invocation: { | ||
| 378 | method: input.definition.method, | ||
| 379 | path: input.definition.path, | ||
| 380 | flatPath: input.definition.flatPath, | ||
| 381 | rootUrl: input.rootUrl, | ||
| 382 | servicePath: input.servicePath, | ||
| 383 | parameters: input.definition.parameters, | ||
| 384 | requestSchemaId: input.definition.requestSchemaId, | ||
| 385 | responseSchemaId: input.definition.responseSchemaId, | ||
| 386 | scopes: input.definition.scopes, | ||
| 387 | supportsMediaUpload: input.definition.supportsMediaUpload, | ||
| 388 | supportsMediaDownload: input.definition.supportsMediaDownload, | ||
| 389 | }, | ||
| 390 | }); | 368 | }); |
| 391 | const credentialPlacements = normalizeCredentialPlacements({ | 369 | const credentialPlacements = normalizeCredentialPlacements({ |
| 392 | credentialHeaders: input.credentialHeaders, | 370 | credentialHeaders: input.credentialHeaders, |
| @@ -403,7 +381,7 @@ export const createGoogleDiscoveryToolFromDefinition = ( · 10 unmodified lines | |||
| 403 | }), | 381 | }), |
| 404 | ); | 382 | ); |
| 405 | const inputSchema = materializeSchemaWithRefDefinitions({ | 383 | const inputSchema = materializeSchemaWithRefDefinitions({ |
| 406 | schemaJson: input.definition.inputSchemaJson, | 384 | schema: input.definition.inputSchema, |
| 407 | refTable, | 385 | refTable, |
| 408 | }); | 386 | }); |
| 409 | 387 | ||
| @@ -411,15 +389,15 @@ export const createGoogleDiscoveryToolFromDefinition = ( · 1 unmodified line | |||
| 411 | interaction: input.definition.method === "get" || input.definition.method === "head" | 389 | interaction: input.definition.method === "get" || input.definition.method === "head" |
| 412 | ? "auto" | 390 | ? "auto" |
| 413 | : "required", | 391 | : "required", |
| 414 | ...(input.definition.inputSchemaJson | 392 | ...(input.definition.inputSchema !== undefined |
| 415 | ? { inputSchemaJson: input.definition.inputSchemaJson } | 393 | ? { inputSchema: input.definition.inputSchema } |
| 416 | : {}), | 394 | : {}), |
| 417 | ...(input.definition.outputSchemaJson | 395 | ...(input.definition.outputSchema !== undefined |
| 418 | ? { outputSchemaJson: input.definition.outputSchemaJson } | 396 | ? { outputSchema: input.definition.outputSchema } |
| 419 | : {}), | 397 | : {}), |
| 420 | sourceKey: input.sourceKey, | 398 | sourceKey: input.sourceKey, |
| 421 | providerKind: "google_discovery", | 399 | providerKind: "google_discovery", |
| 422 | providerDataJson, | 400 | providerData, |
| 423 | }; | 401 | }; |
| 424 | 402 | ||
| 425 | return toTool({ | 403 | return toTool({ |
| @@ -435,7 +413,7 @@ export const createGoogleDiscoveryToolFromDefinition = ( · 9 unmodified lines | |||
| 435 | defaultHeaders: input.defaultHeaders ?? {}, | 413 | defaultHeaders: input.defaultHeaders ?? {}, |
| 436 | credentialPlacements, | 414 | credentialPlacements, |
| 437 | baseUrl: input.baseUrl, | 415 | baseUrl: input.baseUrl, |
| 438 | providerDataJson, | 416 | providerData, |
| 439 | }); | 417 | }); |
| 440 | let clientRequest = HttpClientRequest.make( | 418 | let clientRequest = HttpClientRequest.make( |
| 441 | request.method as Parameters<typeof HttpClientRequest.make>[0], | 419 | request.method as Parameters<typeof HttpClientRequest.make>[0], |
| @@ -74,8 +74,8 @@ export const GoogleDiscoveryManifestMethodSchema = Schema.Struct({ · 73 unmodified lines | |||
| 74 | scopes: Schema.Array(Schema.String), | 74 | scopes: Schema.Array(Schema.String), |
| 75 | supportsMediaUpload: Schema.Boolean, | 75 | supportsMediaUpload: Schema.Boolean, |
| 76 | supportsMediaDownload: Schema.Boolean, | 76 | supportsMediaDownload: Schema.Boolean, |
| 77 | inputSchemaJson: Schema.optional(Schema.String), | 77 | inputSchema: Schema.optional(Schema.Unknown), |
| 78 | outputSchemaJson: Schema.optional(Schema.String), | 78 | outputSchema: Schema.optional(Schema.Unknown), |
| 79 | }); | 79 | }); |
| 80 | 80 | ||
| 81 | export const GoogleDiscoverySchemaRefTableSchema = Schema.Record({ | 81 | export const GoogleDiscoverySchemaRefTableSchema = Schema.Record({ |
| @@ -5,7 +5,7 @@ export { · 4 unmodified lines | |||
| 5 | export { | 5 | export { |
| 6 | createGoogleDiscoveryToolFromDefinition, | 6 | createGoogleDiscoveryToolFromDefinition, |
| 7 | decodeGoogleDiscoverySchemaRefTableJson, | 7 | decodeGoogleDiscoverySchemaRefTableJson, |
| 8 | googleDiscoveryProviderDataJsonFromDefinition, | 8 | googleDiscoveryProviderDataFromDefinition, |
| 9 | type CreateGoogleDiscoveryToolFromDefinitionInput, | 9 | type CreateGoogleDiscoveryToolFromDefinitionInput, |
| 10 | } from "./google-discovery-tools"; | 10 | } from "./google-discovery-tools"; |
| 11 | export { | 11 | export { |
| @@ -7,8 +7,8 @@ export type McpToolManifestEntry = { · 6 unmodified lines | |||
| 7 | toolId: string; | 7 | toolId: string; |
| 8 | toolName: string; | 8 | toolName: string; |
| 9 | description: string | null; | 9 | description: string | null; |
| 10 | inputSchemaJson?: string; | 10 | inputSchema?: unknown; |
| 11 | outputSchemaJson?: string; | 11 | outputSchema?: unknown; |
| 12 | }; | 12 | }; |
| 13 | 13 | ||
| 14 | export type McpToolManifest = { | 14 | export type McpToolManifest = { |
| @@ -34,18 +34,6 @@ const uniqueToolId = (value: string, byBase: Map<string, number>): string => { · 19 unmodified lines | |||
| 34 | return count === 1 ? base : `${base}_${count}`; | 34 | return count === 1 ? base : `${base}_${count}`; |
| 35 | }; | 35 | }; |
| 36 | 36 | ||
| 37 | const stringifyJson = (value: unknown): string | undefined => { | ||
| 38 | if (value === undefined || value === null) { | ||
| 39 | return undefined; | ||
| 40 | } | ||
| 41 | |||
| 42 | try { | ||
| 43 | return JSON.stringify(value); | ||
| 44 | } catch { | ||
| 45 | return undefined; | ||
| 46 | } | ||
| 47 | }; | ||
| 48 | |||
| 49 | const ListedMcpToolSchema = Schema.Struct({ | 37 | const ListedMcpToolSchema = Schema.Struct({ |
| 50 | name: Schema.String, | 38 | name: Schema.String, |
| 51 | description: Schema.optional(Schema.NullOr(Schema.String)), | 39 | description: Schema.optional(Schema.NullOr(Schema.String)), |
| @@ -85,10 +73,8 @@ export const extractMcpToolManifestFromListToolsResult = ( · 33 unmodified lines | |||
| 85 | toolId: uniqueToolId(toolName, byBase), | 73 | toolId: uniqueToolId(toolName, byBase), |
| 86 | toolName, | 74 | toolName, |
| 87 | description: tool.description ?? null, | 75 | description: tool.description ?? null, |
| 88 | inputSchemaJson: | 76 | inputSchema: tool.inputSchema ?? tool.parameters, |
| 89 | stringifyJson(tool.inputSchema) | 77 | outputSchema: tool.outputSchema, |
| 90 | ?? stringifyJson(tool.parameters), | ||
| 91 | outputSchemaJson: stringifyJson(tool.outputSchema), | ||
| 92 | }; | 78 | }; |
| 93 | }) | 79 | }) |
| 94 | .filter((tool): tool is McpToolManifestEntry => tool !== null); | 80 | .filter((tool): tool is McpToolManifestEntry => tool !== null); |
| @@ -230,7 +230,7 @@ describe("codemode-mcp", () => { · 229 unmodified lines | |||
| 230 | 230 | ||
| 231 | const toolDefinition = resolveToolDefinition(discovered.tools["source.mcp.echo"]!); | 231 | const toolDefinition = resolveToolDefinition(discovered.tools["source.mcp.echo"]!); |
| 232 | expect(toolDefinition.metadata?.sourceKey).toBe("mcp.demo"); | 232 | expect(toolDefinition.metadata?.sourceKey).toBe("mcp.demo"); |
| 233 | expect(toolDefinition.metadata?.inputSchemaJson).toContain("value"); | 233 | expect(JSON.stringify(toolDefinition.metadata?.inputSchema)).toContain("value"); |
| 234 | 234 | ||
| 235 | const invoke = resolveToolExecutor(discovered.tools, "source.mcp.echo"); | 235 | const invoke = resolveToolExecutor(discovered.tools, "source.mcp.echo"); |
| 236 | const invocationResult = yield* Effect.promise(() => invoke({ value: "hello" })); | 236 | const invocationResult = yield* Effect.promise(() => invoke({ value: "hello" })); |
| @@ -93,13 +93,13 @@ const isExecutionSuspendedCause = (cause: unknown): boolean => · 92 unmodified lines | |||
| 93 | const toDetails = (cause: unknown): string => | 93 | const toDetails = (cause: unknown): string => |
| 94 | cause instanceof Error ? cause.message : String(cause); | 94 | cause instanceof Error ? cause.message : String(cause); |
| 95 | 95 | ||
| 96 | const inputSchemaFromManifest = (inputSchemaJson: string | undefined) => { | 96 | const inputSchemaFromManifest = (inputSchema: unknown) => { |
| 97 | if (!inputSchemaJson) { | 97 | if (inputSchema === undefined || inputSchema === null) { |
| 98 | return unknownInputSchema; | 98 | return unknownInputSchema; |
| 99 | } | 99 | } |
| 100 | 100 | ||
| 101 | try { | 101 | try { |
| 102 | return standardSchemaFromJsonSchema(JSON.parse(inputSchemaJson), { | 102 | return standardSchemaFromJsonSchema(inputSchema, { |
| 103 | vendor: "mcp", | 103 | vendor: "mcp", |
| 104 | fallback: unknownInputSchema, | 104 | fallback: unknownInputSchema, |
| 105 | }); | 105 | }); |
| @@ -470,7 +470,7 @@ export const createMcpToolsFromManifest = (input: { · 364 unmodified lines | |||
| 470 | toTool({ | 470 | toTool({ |
| 471 | tool: { | 471 | tool: { |
| 472 | description: entry.description ?? `MCP tool: ${entry.toolName}`, | 472 | description: entry.description ?? `MCP tool: ${entry.toolName}`, |
| 473 | inputSchema: inputSchemaFromManifest(entry.inputSchemaJson), | 473 | inputSchema: inputSchemaFromManifest(entry.inputSchema), |
| 474 | execute: async (args: unknown, executionContext?: ToolExecutionContext) => { | 474 | execute: async (args: unknown, executionContext?: ToolExecutionContext) => { |
| 475 | const exit = await Effect.runPromiseExit( | 475 | const exit = await Effect.runPromiseExit( |
| 476 | withConnectionEffect({ | 476 | withConnectionEffect({ |
| @@ -504,8 +504,12 @@ export const createMcpToolsFromManifest = (input: { · 27 unmodified lines | |||
| 504 | }, | 504 | }, |
| 505 | metadata: { | 505 | metadata: { |
| 506 | sourceKey, | 506 | sourceKey, |
| 507 | inputSchemaJson: entry.inputSchemaJson, | 507 | ...(entry.inputSchema !== undefined |
| 508 | outputSchemaJson: entry.outputSchemaJson, | 508 | ? { inputSchema: entry.inputSchema } |
| 509 | : {}), | ||
| 510 | ...(entry.outputSchema !== undefined | ||
| 511 | ? { outputSchema: entry.outputSchema } | ||
| 512 | : {}), | ||
| 509 | }, | 513 | }, |
| 510 | }), | 514 | }), |
| 511 | ] as const; | 515 | ] as const; |
| @@ -8,13 +8,13 @@ export { · 7 unmodified lines | |||
| 8 | } from "./openapi-extraction"; | 8 | } from "./openapi-extraction"; |
| 9 | export { | 9 | export { |
| 10 | compileOpenApiToolDefinitions, | 10 | compileOpenApiToolDefinitions, |
| 11 | openApiProviderDataJsonFromDefinition, | 11 | openApiProviderDataFromDefinition, |
| 12 | OpenApiToolDefinitionSchema, | 12 | OpenApiToolDefinitionSchema, |
| 13 | type OpenApiToolDefinition, | 13 | type OpenApiToolDefinition, |
| 14 | } from "./openapi-definitions"; | 14 | } from "./openapi-definitions"; |
| 15 | export { | 15 | export { |
| 16 | buildOpenApiToolPresentation, | 16 | buildOpenApiToolPresentation, |
| 17 | openApiOutputTypeSignatureFromSchemaJson, | 17 | openApiOutputTypeSignatureFromSchema, |
| 18 | type OpenApiToolPresentation, | 18 | type OpenApiToolPresentation, |
| 19 | } from "./openapi-tool-presentation"; | 19 | } from "./openapi-tool-presentation"; |
| 20 | export { | 20 | export { |
| @@ -23,7 +23,7 @@ export { · 2 unmodified lines | |||
| 23 | createOpenApiToolsFromManifest, | 23 | createOpenApiToolsFromManifest, |
| 24 | createOpenApiToolsFromSpec, | 24 | createOpenApiToolsFromSpec, |
| 25 | } from "./openapi-tools"; | 25 | } from "./openapi-tools"; |
| 26 | export { resolveSchemaJsonWithRefHints, resolveTypingSchemasWithRefHints } from "./openapi-schema-refs"; | 26 | export { resolveSchemaWithRefHints, resolveTypingSchemasWithRefHints } from "./openapi-schema-refs"; |
| 27 | export { | 27 | export { |
| 28 | OPEN_API_HTTP_METHODS, | 28 | OPEN_API_HTTP_METHODS, |
| 29 | OPEN_API_PARAMETER_LOCATIONS, | 29 | OPEN_API_PARAMETER_LOCATIONS, |
| @@ -236,10 +236,9 @@ export const compileOpenApiToolDefinitions = ( · 235 unmodified lines | |||
| 236 | ); | 236 | ); |
| 237 | }; | 237 | }; |
| 238 | 238 | ||
| 239 | export const openApiProviderDataJsonFromDefinition = ( | 239 | export const openApiProviderDataFromDefinition = ( |
| 240 | definition: OpenApiToolDefinition, | 240 | definition: OpenApiToolDefinition, |
| 241 | ): string => | 241 | ): typeof OpenApiToolProviderDataSchema.Type => ({ |
| 242 | JSON.stringify({ | ||
| 243 | kind: "openapi", | 242 | kind: "openapi", |
| 244 | toolId: definition.toolId, | 243 | toolId: definition.toolId, |
| 245 | rawToolId: definition.rawToolId, | 244 | rawToolId: definition.rawToolId, |
| @@ -26,8 +26,8 @@ type ParsedInputSchema = { · 25 unmodified lines | |||
| 26 | required?: Array<string>; | 26 | required?: Array<string>; |
| 27 | }; | 27 | }; |
| 28 | 28 | ||
| 29 | const parseInputSchema = (inputSchemaJson: string | undefined): ParsedInputSchema | null => | 29 | const parseInputSchema = (inputSchema: unknown): ParsedInputSchema | null => |
| 30 | inputSchemaJson ? (JSON.parse(inputSchemaJson) as ParsedInputSchema) : null; | 30 | inputSchema ? (inputSchema as ParsedInputSchema) : null; |
| 31 | 31 | ||
| 32 | const expectedTools = [ | 32 | const expectedTools = [ |
| 33 | { | 33 | { |
| @@ -89,7 +89,7 @@ describe("openapi-extraction real specs", () => { · 55 unmodified lines | |||
| 89 | expect(tool?.path).toBe(expectedTool.path); | 89 | expect(tool?.path).toBe(expectedTool.path); |
| 90 | expect(tool?.tags).toEqual(expectedTool.tags); | 90 | expect(tool?.tags).toEqual(expectedTool.tags); |
| 91 | 91 | ||
| 92 | const inputSchema = parseInputSchema(tool?.typing?.inputSchemaJson); | 92 | const inputSchema = parseInputSchema(tool?.typing?.inputSchema); |
| 93 | expect(inputSchema?.type).toBe("object"); | 93 | expect(inputSchema?.type).toBe("object"); |
| 94 | expect(Object.keys(inputSchema?.properties ?? {})).toEqual( | 94 | expect(Object.keys(inputSchema?.properties ?? {})).toEqual( |
| 95 | expect.arrayContaining([...expectedTool.requiredInputs]), | 95 | expect.arrayContaining([...expectedTool.requiredInputs]), |
| @@ -2,8 +2,8 @@ type JsonObject = Record<string, unknown>; · 1 unmodified line | |||
| 2 | type RefHintValue = string | JsonObject; | 2 | type RefHintValue = string | JsonObject; |
| 3 | 3 | ||
| 4 | type DiscoveryTypingLike = { | 4 | type DiscoveryTypingLike = { |
| 5 | inputSchemaJson?: string; | 5 | inputSchema?: unknown; |
| 6 | outputSchemaJson?: string; | 6 | outputSchema?: unknown; |
| 7 | }; | 7 | }; |
| 8 | 8 | ||
| 9 | const isJsonObject = (value: unknown): value is JsonObject => | 9 | const isJsonObject = (value: unknown): value is JsonObject => |
| @@ -92,50 +92,40 @@ const resolveSchemaNode = ( · 82 unmodified lines | |||
| 92 | return next; | 92 | return next; |
| 93 | }; | 93 | }; |
| 94 | 94 | ||
| 95 | export const resolveSchemaJsonWithRefHints = ( | 95 | export const resolveSchemaWithRefHints = ( |
| 96 | schemaJson: string | undefined, | 96 | schema: unknown, |
| 97 | refHintTable: Readonly<Record<string, RefHintValue>> | undefined, | 97 | refHintTable: Readonly<Record<string, RefHintValue>> | undefined, |
| 98 | ): string | null => { | 98 | ): unknown | null => { |
| 99 | if (!schemaJson) { | 99 | if (schema === undefined || schema === null) { |
| 100 | return null; | 100 | return null; |
| 101 | } | 101 | } |
| 102 | 102 | ||
| 103 | const parsedSchema = parseJson(schemaJson); | ||
| 104 | if (parsedSchema === null) { | ||
| 105 | return schemaJson; | ||
| 106 | } | ||
| 107 | |||
| 108 | if (!refHintTable || Object.keys(refHintTable).length === 0) { | 103 | if (!refHintTable || Object.keys(refHintTable).length === 0) { |
| 109 | return schemaJson; | 104 | return schema; |
| 110 | } | 105 | } |
| 111 | 106 | ||
| 112 | const resolved = resolveSchemaNode( | 107 | const resolved = resolveSchemaNode( |
| 113 | parsedSchema, | 108 | schema, |
| 114 | refHintTable, | 109 | refHintTable, |
| 115 | new Map<string, unknown>(), | 110 | new Map<string, unknown>(), |
| 116 | new Set<string>(), | 111 | new Set<string>(), |
| 117 | ); | 112 | ); |
| 118 | 113 | return resolved; | |
| 119 | try { | ||
| 120 | return JSON.stringify(resolved); | ||
| 121 | } catch { | ||
| 122 | return schemaJson; | ||
| 123 | } | ||
| 124 | }; | 114 | }; |
| 125 | 115 | ||
| 126 | export const resolveTypingSchemasWithRefHints = ( | 116 | export const resolveTypingSchemasWithRefHints = ( |
| 127 | typing: DiscoveryTypingLike | undefined, | 117 | typing: DiscoveryTypingLike | undefined, |
| 128 | refHintTable: Readonly<Record<string, RefHintValue>> | undefined, | 118 | refHintTable: Readonly<Record<string, RefHintValue>> | undefined, |
| 129 | ): { | 119 | ): { |
| 130 | inputSchemaJson: string | null; | 120 | inputSchema: unknown | null; |
| 131 | outputSchemaJson: string | null; | 121 | outputSchema: unknown | null; |
| 132 | } => ({ | 122 | } => ({ |
| 133 | inputSchemaJson: resolveSchemaJsonWithRefHints( | 123 | inputSchema: resolveSchemaWithRefHints( |
| 134 | typing?.inputSchemaJson, | 124 | typing?.inputSchema, |
| 135 | refHintTable, | 125 | refHintTable, |
| 136 | ), | 126 | ), |
| 137 | outputSchemaJson: resolveSchemaJsonWithRefHints( | 127 | outputSchema: resolveSchemaWithRefHints( |
| 138 | typing?.outputSchemaJson, | 128 | typing?.outputSchema, |
| 139 | refHintTable, | 129 | refHintTable, |
| 140 | ), | 130 | ), |
| 141 | }); | 131 | }); |
| @@ -1,22 +1,16 @@ | |||
| 1 | import { typeSignatureFromSchemaJson } from "@executor/codemode-core"; | 1 | import { typeSignatureFromSchema } from "@executor/codemode-core"; |
| 2 | 2 | ||
| 3 | import type { | 3 | import type { |
| 4 | OpenApiExample, | 4 | OpenApiExample, |
| 5 | OpenApiInvocationPayload, | ||
| 5 | OpenApiToolDocumentation, | 6 | OpenApiToolDocumentation, |
| 7 | OpenApiToolProviderData, | ||
| 6 | } from "./openapi-types"; | 8 | } from "./openapi-types"; |
| 7 | import { | 9 | import { |
| 8 | openApiProviderDataJsonFromDefinition, | 10 | openApiProviderDataFromDefinition, |
| 9 | type OpenApiToolDefinition, | 11 | type OpenApiToolDefinition, |
| 10 | } from "./openapi-definitions"; | 12 | } from "./openapi-definitions"; |
| 11 | 13 | ||
| 12 | const parseJson = (value: string): unknown | undefined => { | ||
| 13 | try { | ||
| 14 | return JSON.parse(value) as unknown; | ||
| 15 | } catch { | ||
| 16 | return undefined; | ||
| 17 | } | ||
| 18 | }; | ||
| 19 | |||
| 20 | const asRecord = (value: unknown): Record<string, unknown> => | 14 | const asRecord = (value: unknown): Record<string, unknown> => |
| 21 | typeof value === "object" && value !== null && !Array.isArray(value) | 15 | typeof value === "object" && value !== null && !Array.isArray(value) |
| 22 | ? (value as Record<string, unknown>) | 16 | ? (value as Record<string, unknown>) |
| @@ -32,33 +26,62 @@ const isStrictEmptyObjectSchema = (value: unknown): boolean => { · 9 unmodified lines | |||
| 32 | return Object.keys(properties).length === 0 && schema.additionalProperties === false; | 26 | return Object.keys(properties).length === 0 && schema.additionalProperties === false; |
| 33 | }; | 27 | }; |
| 34 | 28 | ||
| 35 | export const openApiOutputTypeSignatureFromSchemaJson = ( | 29 | export const openApiOutputTypeSignatureFromSchema = ( |
| 36 | schemaJson: string | undefined, | 30 | schema: unknown, |
| 37 | maxLength: number = 320, | 31 | maxLength: number = 320, |
| 38 | ): string => { | 32 | ): string => { |
| 39 | if (!schemaJson) { | 33 | if (schema === undefined || schema === null) { |
| 40 | return "void"; | 34 | return "void"; |
| 41 | } | 35 | } |
| 42 | 36 | ||
| 43 | const parsed = parseJson(schemaJson); | 37 | if (isStrictEmptyObjectSchema(schema)) { |
| 44 | if (parsed === undefined) { | ||
| 45 | return "unknown"; | ||
| 46 | } | ||
| 47 | |||
| 48 | if (isStrictEmptyObjectSchema(parsed)) { | ||
| 49 | return "{}"; | 38 | return "{}"; |
| 50 | } | 39 | } |
| 51 | 40 | ||
| 52 | return typeSignatureFromSchemaJson(schemaJson, "unknown", maxLength); | 41 | return typeSignatureFromSchema(schema, "unknown", maxLength); |
| 53 | }; | 42 | }; |
| 54 | 43 | ||
| 55 | const firstExample = ( | 44 | const firstExample = ( |
| 56 | examples: ReadonlyArray<OpenApiExample> | undefined, | 45 | examples: ReadonlyArray<OpenApiExample> | undefined, |
| 57 | ): OpenApiExample | undefined => examples?.[0]; | 46 | ): OpenApiExample | undefined => examples?.[0]; |
| 58 | 47 | ||
| 59 | const buildExampleInputJson = ( | 48 | const fallbackInputSchemaFromInvocation = ( |
| 49 | invocation: OpenApiInvocationPayload, | ||
| 50 | ): Record<string, unknown> | undefined => { | ||
| 51 | const properties: Record<string, unknown> = {}; | ||
| 52 | const required: string[] = []; | ||
| 53 | |||
| 54 | for (const parameter of invocation.parameters) { | ||
| 55 | properties[parameter.name] = { | ||
| 56 | type: "string", | ||
| 57 | }; | ||
| 58 | if (parameter.required) { | ||
| 59 | required.push(parameter.name); | ||
| 60 | } | ||
| 61 | } | ||
| 62 | |||
| 63 | if (invocation.requestBody) { | ||
| 64 | properties.body = { | ||
| 65 | type: "object", | ||
| 66 | }; | ||
| 67 | if (invocation.requestBody.required) { | ||
| 68 | required.push("body"); | ||
| 69 | } | ||
| 70 | } | ||
| 71 | |||
| 72 | return Object.keys(properties).length > 0 | ||
| 73 | ? { | ||
| 74 | type: "object", | ||
| 75 | properties, | ||
| 76 | ...(required.length > 0 ? { required } : {}), | ||
| 77 | additionalProperties: false, | ||
| 78 | } | ||
| 79 | : undefined; | ||
| 80 | }; | ||
| 81 | |||
| 82 | const buildExampleInput = ( | ||
| 60 | documentation: OpenApiToolDocumentation | undefined, | 83 | documentation: OpenApiToolDocumentation | undefined, |
| 61 | ): string | undefined => { | 84 | ): Record<string, unknown> | undefined => { |
| 62 | if (!documentation) { | 85 | if (!documentation) { |
| 63 | return undefined; | 86 | return undefined; |
| 64 | } | 87 | } |
| @@ -71,52 +94,51 @@ const buildExampleInputJson = ( · 6 unmodified lines | |||
| 71 | continue; | 94 | continue; |
| 72 | } | 95 | } |
| 73 | 96 | ||
| 74 | const parsedValue = parseJson(example.valueJson); | 97 | input[parameter.name] = JSON.parse(example.valueJson) as unknown; |
| 75 | if (parsedValue !== undefined) { | ||
| 76 | input[parameter.name] = parsedValue; | ||
| 77 | } | ||
| 78 | } | 98 | } |
| 79 | 99 | ||
| 80 | const requestBodyExample = firstExample(documentation.requestBody?.examples); | 100 | const requestBodyExample = firstExample(documentation.requestBody?.examples); |
| 81 | if (requestBodyExample) { | 101 | if (requestBodyExample) { |
| 82 | const parsedValue = parseJson(requestBodyExample.valueJson); | 102 | input.body = JSON.parse(requestBodyExample.valueJson) as unknown; |
| 83 | if (parsedValue !== undefined) { | ||
| 84 | input.body = parsedValue; | ||
| 85 | } | ||
| 86 | } | 103 | } |
| 87 | 104 | ||
| 88 | return Object.keys(input).length > 0 ? JSON.stringify(input) : undefined; | 105 | return Object.keys(input).length > 0 ? input : undefined; |
| 89 | }; | 106 | }; |
| 90 | 107 | ||
| 91 | const buildExampleOutputJson = ( | 108 | const buildExampleOutput = ( |
| 92 | documentation: OpenApiToolDocumentation | undefined, | 109 | documentation: OpenApiToolDocumentation | undefined, |
| 93 | ): string | undefined => firstExample(documentation?.response?.examples)?.valueJson; | 110 | ): unknown | undefined => { |
| 111 | const example = firstExample(documentation?.response?.examples)?.valueJson; | ||
| 112 | return example ? JSON.parse(example) as unknown : undefined; | ||
| 113 | }; | ||
| 94 | 114 | ||
| 95 | export type OpenApiToolPresentation = { | 115 | export type OpenApiToolPresentation = { |
| 96 | inputType: string; | 116 | inputType: string; |
| 97 | outputType: string; | 117 | outputType: string; |
| 98 | inputSchemaJson?: string; | 118 | inputSchema?: unknown; |
| 99 | outputSchemaJson?: string; | 119 | outputSchema?: unknown; |
| 100 | exampleInputJson?: string; | 120 | exampleInput?: unknown; |
| 101 | exampleOutputJson?: string; | 121 | exampleOutput?: unknown; |
| 102 | providerDataJson: string; | 122 | providerData: OpenApiToolProviderData; |
| 103 | }; | 123 | }; |
| 104 | 124 | ||
| 105 | export const buildOpenApiToolPresentation = (input: { | 125 | export const buildOpenApiToolPresentation = (input: { |
| 106 | definition: OpenApiToolDefinition; | 126 | definition: OpenApiToolDefinition; |
| 107 | }): OpenApiToolPresentation => { | 127 | }): OpenApiToolPresentation => { |
| 108 | const inputSchemaJson = input.definition.typing?.inputSchemaJson; | 128 | const inputSchema = |
| 109 | const outputSchemaJson = input.definition.typing?.outputSchemaJson; | 129 | input.definition.typing?.inputSchema |
| 110 | const exampleInputJson = buildExampleInputJson(input.definition.documentation); | 130 | ?? fallbackInputSchemaFromInvocation(input.definition.invocation); |
| 111 | const exampleOutputJson = buildExampleOutputJson(input.definition.documentation); | 131 | const outputSchema = input.definition.typing?.outputSchema; |
| 132 | const exampleInput = buildExampleInput(input.definition.documentation); | ||
| 133 | const exampleOutput = buildExampleOutput(input.definition.documentation); | ||
| 112 | 134 | ||
| 113 | return { | 135 | return { |
| 114 | inputType: typeSignatureFromSchemaJson(inputSchemaJson, "unknown", Infinity), | 136 | inputType: typeSignatureFromSchema(inputSchema, "unknown", Infinity), |
| 115 | outputType: openApiOutputTypeSignatureFromSchemaJson(outputSchemaJson, Infinity), | 137 | outputType: openApiOutputTypeSignatureFromSchema(outputSchema, Infinity), |
| 116 | ...(inputSchemaJson ? { inputSchemaJson } : {}), | 138 | ...(inputSchema !== undefined ? { inputSchema } : {}), |
| 117 | ...(outputSchemaJson ? { outputSchemaJson } : {}), | 139 | ...(outputSchema !== undefined ? { outputSchema } : {}), |
| 118 | ...(exampleInputJson ? { exampleInputJson } : {}), | 140 | ...(exampleInput !== undefined ? { exampleInput } : {}), |
| 119 | ...(exampleOutputJson ? { exampleOutputJson } : {}), | 141 | ...(exampleOutput !== undefined ? { exampleOutput } : {}), |
| 120 | providerDataJson: openApiProviderDataJsonFromDefinition(input.definition), | 142 | providerData: openApiProviderDataFromDefinition(input.definition), |
| 121 | }; | 143 | }; |
| 122 | }; | 144 | }; |
| @@ -291,8 +291,8 @@ describe("openapi-tools", () => { · 290 unmodified lines | |||
| 291 | const resolvedGet = resolveToolDefinition(tools["source.demo.repos.getRepo"]!); | 291 | const resolvedGet = resolveToolDefinition(tools["source.demo.repos.getRepo"]!); |
| 292 | expect(resolvedGet.metadata?.sourceKey).toBe("api.demo"); | 292 | expect(resolvedGet.metadata?.sourceKey).toBe("api.demo"); |
| 293 | expect(resolvedGet.metadata?.providerKind).toBe("openapi"); | 293 | expect(resolvedGet.metadata?.providerKind).toBe("openapi"); |
| 294 | expect(resolvedGet.metadata?.providerDataJson).toContain('"group":"repos"'); | 294 | expect(resolvedGet.metadata?.providerData).toMatchObject({ group: "repos" }); |
| 295 | expect(resolvedGet.metadata?.inputSchemaJson).toBeDefined(); | 295 | expect(resolvedGet.metadata?.inputSchema).toBeDefined(); |
| 296 | }), | 296 | }), |
| 297 | ); | 297 | ); |
| 298 | 298 | ||
| @@ -23,7 +23,7 @@ import { · 22 unmodified lines | |||
| 23 | type OpenApiToolDefinition, | 23 | type OpenApiToolDefinition, |
| 24 | } from "./openapi-definitions"; | 24 | } from "./openapi-definitions"; |
| 25 | import { buildOpenApiToolPresentation } from "./openapi-tool-presentation"; | 25 | import { buildOpenApiToolPresentation } from "./openapi-tool-presentation"; |
| 26 | import { resolveSchemaJsonWithRefHints } from "./openapi-schema-refs"; | 26 | import { resolveSchemaWithRefHints } from "./openapi-schema-refs"; |
| 27 | import { | 27 | import { |
| 28 | extractOpenApiManifest, | 28 | extractOpenApiManifest, |
| 29 | type OpenApiExtractionError, | 29 | type OpenApiExtractionError, |
| @@ -262,21 +262,21 @@ const summarizeHttpResponseBody = (body: unknown): string | null => { · 232 unmodified lines | |||
| 262 | } | 262 | } |
| 263 | }; | 263 | }; |
| 264 | 264 | ||
| 265 | const inputSchemaFromTypingJson = (input: { | 265 | const inputSchemaFromTyping = (input: { |
| 266 | inputSchemaJson: string | undefined; | 266 | inputSchema: unknown; |
| 267 | refHintTable?: Readonly<OpenApiRefHintTable>; | 267 | refHintTable?: Readonly<OpenApiRefHintTable>; |
| 268 | }) => { | 268 | }) => { |
| 269 | const resolvedSchemaJson = resolveSchemaJsonWithRefHints( | 269 | const resolvedSchema = resolveSchemaWithRefHints( |
| 270 | input.inputSchemaJson, | 270 | input.inputSchema, |
| 271 | input.refHintTable, | 271 | input.refHintTable, |
| 272 | ) ?? input.inputSchemaJson; | 272 | ) ?? input.inputSchema; |
| 273 | 273 | ||
| 274 | if (!resolvedSchemaJson) { | 274 | if (resolvedSchema === undefined || resolvedSchema === null) { |
| 275 | return unknownInputSchema; | 275 | return unknownInputSchema; |
| 276 | } | 276 | } |
| 277 | 277 | ||
| 278 | try { | 278 | try { |
| 279 | return standardSchemaFromJsonSchema(JSON.parse(resolvedSchemaJson), { | 279 | return standardSchemaFromJsonSchema(resolvedSchema, { |
| 280 | vendor: "openapi", | 280 | vendor: "openapi", |
| 281 | fallback: unknownInputSchema, | 281 | fallback: unknownInputSchema, |
| 282 | }); | 282 | }); |
| @@ -469,8 +469,8 @@ export const createOpenApiToolFromDefinition = ( · 186 unmodified lines | |||
| 469 | return toTool({ | 469 | return toTool({ |
| 470 | tool: { | 470 | tool: { |
| 471 | description: input.definition.description, | 471 | description: input.definition.description, |
| 472 | inputSchema: inputSchemaFromTypingJson({ | 472 | inputSchema: inputSchemaFromTyping({ |
| 473 | inputSchemaJson: presentation.inputSchemaJson ?? input.definition.typing?.inputSchemaJson, | 473 | inputSchema: presentation.inputSchema ?? input.definition.typing?.inputSchema, |
| 474 | refHintTable: input.refHintTable, | 474 | refHintTable: input.refHintTable, |
| 475 | }), | 475 | }), |
| 476 | execute: async (args: unknown) => { | 476 | execute: async (args: unknown) => { |
| @@ -526,12 +526,20 @@ export const createOpenApiToolFromDefinition = ( · 49 unmodified lines | |||
| 526 | sourceKey: input.sourceKey, | 526 | sourceKey: input.sourceKey, |
| 527 | inputType: presentation.inputType, | 527 | inputType: presentation.inputType, |
| 528 | outputType: presentation.outputType, | 528 | outputType: presentation.outputType, |
| 529 | inputSchemaJson: presentation.inputSchemaJson, | 529 | ...((presentation.inputSchema ?? input.definition.typing?.inputSchema) !== undefined |
| 530 | outputSchemaJson: presentation.outputSchemaJson, | 530 | ? { inputSchema: presentation.inputSchema ?? input.definition.typing?.inputSchema } |
| 531 | exampleInputJson: presentation.exampleInputJson, | 531 | : {}), |
| 532 | exampleOutputJson: presentation.exampleOutputJson, | 532 | ...((presentation.outputSchema ?? input.definition.typing?.outputSchema) !== undefined |
| 533 | ? { outputSchema: presentation.outputSchema ?? input.definition.typing?.outputSchema } | ||
| 534 | : {}), | ||
| 535 | ...(presentation.exampleInput !== undefined | ||
| 536 | ? { exampleInput: presentation.exampleInput } | ||
| 537 | : {}), | ||
| 538 | ...(presentation.exampleOutput !== undefined | ||
| 539 | ? { exampleOutput: presentation.exampleOutput } | ||
| 540 | : {}), | ||
| 533 | providerKind: "openapi", | 541 | providerKind: "openapi", |
| 534 | providerDataJson: presentation.providerDataJson, | 542 | providerData: presentation.providerData, |
| 535 | }, | 543 | }, |
| 536 | }); | 544 | }); |
| 537 | }; | 545 | }; |
| @@ -58,8 +58,8 @@ export const OpenApiInvocationPayloadSchema = Schema.Struct({ · 57 unmodified lines | |||
| 58 | }); | 58 | }); |
| 59 | 59 | ||
| 60 | export const DiscoveryTypingPayloadSchema = Schema.Struct({ | 60 | export const DiscoveryTypingPayloadSchema = Schema.Struct({ |
| 61 | inputSchemaJson: Schema.optional(Schema.String), | 61 | inputSchema: Schema.optional(Schema.Unknown), |
| 62 | outputSchemaJson: Schema.optional(Schema.String), | 62 | outputSchema: Schema.optional(Schema.Unknown), |
| 63 | refHintKeys: Schema.optional(Schema.Array(Schema.String)), | 63 | refHintKeys: Schema.optional(Schema.Array(Schema.String)), |
| 64 | }); | 64 | }); |
| 65 | 65 | ||
| @@ -5,6 +5,7 @@ · 4 unmodified lines | |||
| 5 | "exports": { | 5 | "exports": { |
| 6 | ".": "./src/index.ts", | 6 | ".": "./src/index.ts", |
| 7 | "./client": "./src/client.ts", | 7 | "./client": "./src/client.ts", |
| 8 | "./ir": "./src/ir/index.ts", | ||
| 8 | "./schema": "./src/schema/index.ts" | 9 | "./schema": "./src/schema/index.ts" |
| 9 | }, | 10 | }, |
| 10 | "imports": { | 11 | "imports": { |
| @@ -8,7 +8,6 @@ import { · 7 unmodified lines | |||
| 8 | SourceIdSchema, | 8 | SourceIdSchema, |
| 9 | SourceInspectionDiscoverPayloadSchema, | 9 | SourceInspectionDiscoverPayloadSchema, |
| 10 | SourceInspectionDiscoverResultSchema, | 10 | SourceInspectionDiscoverResultSchema, |
| 11 | SourceInspectionSchemaBundleSchema, | ||
| 12 | SourceInspectionSchema, | 11 | SourceInspectionSchema, |
| 13 | SourceInspectionToolDetailSchema, | 12 | SourceInspectionToolDetailSchema, |
| 14 | SourceImportAuthPolicySchema, | 13 | SourceImportAuthPolicySchema, |
| @@ -80,7 +79,6 @@ export type UpdateSourcePayload = typeof UpdateSourcePayloadSchema.Type; · 65 unmodified lines | |||
| 80 | const workspaceIdParam = HttpApiSchema.param("workspaceId", WorkspaceIdSchema); | 79 | const workspaceIdParam = HttpApiSchema.param("workspaceId", WorkspaceIdSchema); |
| 81 | const sourceIdParam = HttpApiSchema.param("sourceId", SourceIdSchema); | 80 | const sourceIdParam = HttpApiSchema.param("sourceId", SourceIdSchema); |
| 82 | const toolPathParam = HttpApiSchema.param("toolPath", Schema.String); | 81 | const toolPathParam = HttpApiSchema.param("toolPath", Schema.String); |
| 83 | const schemaBundleIdParam = HttpApiSchema.param("schemaBundleId", Schema.String); | ||
| 84 | 82 | ||
| 85 | const CredentialPageUrlParamsSchema = Schema.Struct({ | 83 | const CredentialPageUrlParamsSchema = Schema.Struct({ |
| 86 | interactionId: ExecutionInteractionIdSchema, | 84 | interactionId: ExecutionInteractionIdSchema, |
| @@ -242,15 +240,6 @@ export class SourcesApi extends HttpApiGroup.make("sources") · 155 unmodified lines | |||
| 242 | .addError(ControlPlaneNotFoundError) | 240 | .addError(ControlPlaneNotFoundError) |
| 243 | .addError(ControlPlaneStorageError), | 241 | .addError(ControlPlaneStorageError), |
| 244 | ) | 242 | ) |
| 245 | .add( | ||
| 246 | HttpApiEndpoint.get("inspectionSchemaBundle")`/workspaces/${workspaceIdParam}/sources/${sourceIdParam}/inspection/schema-bundles/${schemaBundleIdParam}` | ||
| 247 | .addSuccess(SourceInspectionSchemaBundleSchema) | ||
| 248 | .addError(ControlPlaneBadRequestError) | ||
| 249 | .addError(ControlPlaneUnauthorizedError) | ||
| 250 | .addError(ControlPlaneForbiddenError) | ||
| 251 | .addError(ControlPlaneNotFoundError) | ||
| 252 | .addError(ControlPlaneStorageError), | ||
| 253 | ) | ||
| 254 | .add( | 243 | .add( |
| 255 | HttpApiEndpoint.post("inspectionDiscover")`/workspaces/${workspaceIdParam}/sources/${sourceIdParam}/inspection/discover` | 244 | HttpApiEndpoint.post("inspectionDiscover")`/workspaces/${workspaceIdParam}/sources/${sourceIdParam}/inspection/discover` |
| 256 | .setPayload(SourceInspectionDiscoverPayloadSchema) | 245 | .setPayload(SourceInspectionDiscoverPayloadSchema) |
| @@ -16,7 +16,6 @@ import { · 15 unmodified lines | |||
| 16 | import { | 16 | import { |
| 17 | discoverSourceInspectionTools, | 17 | discoverSourceInspectionTools, |
| 18 | getSourceInspection, | 18 | getSourceInspection, |
| 19 | getSourceInspectionSchemaBundle, | ||
| 20 | getSourceInspectionToolDetail, | 19 | getSourceInspectionToolDetail, |
| 21 | } from "../../runtime/source-inspection"; | 20 | } from "../../runtime/source-inspection"; |
| 22 | import { | 21 | import { |
| @@ -750,17 +749,6 @@ export const ControlPlaneSourcesLive = HttpApiBuilder.group( · 727 unmodified lines | |||
| 750 | ), | 749 | ), |
| 751 | ), | 750 | ), |
| 752 | ) | 751 | ) |
| 753 | .handle("inspectionSchemaBundle", ({ path }) => | ||
| 754 | resolveRequestedLocalWorkspace("sources.inspection_schema_bundle", path.workspaceId).pipe( | ||
| 755 | Effect.zipRight( | ||
| 756 | getSourceInspectionSchemaBundle({ | ||
| 757 | workspaceId: path.workspaceId, | ||
| 758 | sourceId: path.sourceId, | ||
| 759 | schemaBundleId: path.schemaBundleId, | ||
| 760 | }), | ||
| 761 | ), | ||
| 762 | ), | ||
| 763 | ) | ||
| 764 | .handle("inspectionDiscover", ({ path, payload }) => | 752 | .handle("inspectionDiscover", ({ path, payload }) => |
| 765 | resolveRequestedLocalWorkspace("sources.inspection_discover", path.workspaceId).pipe( | 753 | resolveRequestedLocalWorkspace("sources.inspection_discover", path.workspaceId).pipe( |
| 766 | Effect.zipRight( | 754 | Effect.zipRight( |
| @@ -2,3 +2,4 @@ export * from "./schema/index"; · 1 unmodified line | |||
| 2 | export * from "./api/index"; | 2 | export * from "./api/index"; |
| 3 | export * from "./runtime/index"; | 3 | export * from "./runtime/index"; |
| 4 | export * from "./client"; | 4 | export * from "./client"; |
| 5 | export * as ir from "./ir/index"; | ||
| @@ -0,0 +1,33 @@ | |||
| 1 | # IR v1 | ||
| 2 | |||
| 3 | `@executor/control-plane/ir` is the greenfield semantic center for imported sources. | ||
| 4 | |||
| 5 | ## Boundary | ||
| 6 | |||
| 7 | - `CatalogSnapshotV1` is the canonical persisted import artifact. | ||
| 8 | - `CatalogV1` is the normalized semantic graph. | ||
| 9 | - `CatalogFragmentV1` is the adapter/importer output contract. | ||
| 10 | - `ToolDescriptor`, search docs, inspect views, and `InvocationPlan` are projections. | ||
| 11 | |||
| 12 | ## Direction | ||
| 13 | |||
| 14 | Importers should move toward: | ||
| 15 | |||
| 16 | `source -> CatalogFragmentV1 -> merge -> CatalogV1 -> projectors -> InvocationPlan` | ||
| 17 | |||
| 18 | The existing runtime-specific compiled payload blobs are no longer the target semantic center. | ||
| 19 | |||
| 20 | ## Package Surface | ||
| 21 | |||
| 22 | Use either: | ||
| 23 | |||
| 24 | - `@executor/control-plane/ir` | ||
| 25 | - `import { ir } from "@executor/control-plane"` | ||
| 26 | |||
| 27 | ## Guarantees | ||
| 28 | |||
| 29 | - provenance is required on entities | ||
| 30 | - synthetic entities are marked explicitly | ||
| 31 | - lossy normalization is represented by diagnostics | ||
| 32 | - inheritance is preserved in scopes and resolved at projection/planning time | ||
| 33 | - invocation plans remain connection-bound runtime artifacts, not canonical IR | ||
| @@ -0,0 +1,711 @@ | |||
| 1 | import { describe, expect, it } from "@effect/vitest"; | ||
| 2 | |||
| 3 | import { | ||
| 4 | buildInvocationPlan, | ||
| 5 | createCatalogSnapshotV1, | ||
| 6 | createEmptyCatalogV1, | ||
| 7 | decodeCatalogSnapshotV1, | ||
| 8 | mergeCatalogFragments, | ||
| 9 | projectCatalogForAgentSdk, | ||
| 10 | projectSymbolExpandedView, | ||
| 11 | projectSymbolShallowView, | ||
| 12 | validateCatalogInvariants, | ||
| 13 | } from "./catalog"; | ||
| 14 | import { | ||
| 15 | CapabilityIdSchema, | ||
| 16 | DiagnosticIdSchema, | ||
| 17 | DocumentIdSchema, | ||
| 18 | ExecutableIdSchema, | ||
| 19 | ExampleSymbolIdSchema, | ||
| 20 | HeaderSymbolIdSchema, | ||
| 21 | ParameterSymbolIdSchema, | ||
| 22 | RequestBodySymbolIdSchema, | ||
| 23 | ResourceIdSchema, | ||
| 24 | ResponseSetIdSchema, | ||
| 25 | ResponseSymbolIdSchema, | ||
| 26 | ScopeIdSchema, | ||
| 27 | SecuritySchemeSymbolIdSchema, | ||
| 28 | ShapeSymbolIdSchema, | ||
| 29 | } from "./ids"; | ||
| 30 | import type { CatalogV1, CatalogFragmentV1, ProvenanceRef } from "./model"; | ||
| 31 | |||
| 32 | const put = <K extends string, V>(record: Record<K, V>, key: K, value: V) => { | ||
| 33 | record[key] = value; | ||
| 34 | }; | ||
| 35 | |||
| 36 | const docId = DocumentIdSchema.make("doc_primary"); | ||
| 37 | const baseProvenance = (pointer: string): ProvenanceRef[] => [{ | ||
| 38 | relation: "declared", | ||
| 39 | documentId: docId, | ||
| 40 | pointer, | ||
| 41 | }]; | ||
| 42 | |||
| 43 | const createHttpCatalog = (input: { | ||
| 44 | collideIds?: boolean; | ||
| 45 | method?: string; | ||
| 46 | } = {}): CatalogV1 => { | ||
| 47 | const catalog = createEmptyCatalogV1(); | ||
| 48 | |||
| 49 | const resourceId = ResourceIdSchema.make("res_primary"); | ||
| 50 | const scopeId = ScopeIdSchema.make("scope_service"); | ||
| 51 | const authSchemeId = SecuritySchemeSymbolIdSchema.make("sym_security_oauth"); | ||
| 52 | const bodyShapeId = ShapeSymbolIdSchema.make("shape_event_body"); | ||
| 53 | const resultShapeId = ShapeSymbolIdSchema.make("shape_event_result"); | ||
| 54 | const stringShapeId = ShapeSymbolIdSchema.make("shape_string"); | ||
| 55 | const idPathParamId = ParameterSymbolIdSchema.make("sym_param_path_event_id"); | ||
| 56 | const calendarParamId = ParameterSymbolIdSchema.make("sym_param_path_calendar_id"); | ||
| 57 | const queryParamId = ParameterSymbolIdSchema.make( | ||
| 58 | input.collideIds ? "sym_param_query_id" : "sym_param_query_send_updates", | ||
| 59 | ); | ||
| 60 | const requestBodyId = RequestBodySymbolIdSchema.make("sym_request_body_event"); | ||
| 61 | const responseId = ResponseSymbolIdSchema.make("sym_response_200"); | ||
| 62 | const responseSetId = ResponseSetIdSchema.make("response_set_events_update"); | ||
| 63 | const executableId = ExecutableIdSchema.make("exec_http_events_update"); | ||
| 64 | const capabilityId = CapabilityIdSchema.make("cap_events_update"); | ||
| 65 | const exampleId = ExampleSymbolIdSchema.make("sym_example_event_body"); | ||
| 66 | |||
| 67 | put(catalog.documents as Record<typeof docId, CatalogV1["documents"][typeof docId]>, docId, { | ||
| 68 | id: docId, | ||
| 69 | kind: "openapi", | ||
| 70 | title: "Calendar API", | ||
| 71 | fetchedAt: "2026-03-14T00:00:00.000Z", | ||
| 72 | rawRef: "memory://calendar/openapi.json", | ||
| 73 | }); | ||
| 74 | |||
| 75 | put(catalog.resources as Record<typeof resourceId, CatalogV1["resources"][typeof resourceId]>, resourceId, { | ||
| 76 | id: resourceId, | ||
| 77 | documentId: docId, | ||
| 78 | canonicalUri: "https://example.test/openapi.json", | ||
| 79 | baseUri: "https://example.test/openapi.json", | ||
| 80 | anchors: {}, | ||
| 81 | dynamicAnchors: {}, | ||
| 82 | synthetic: false, | ||
| 83 | provenance: baseProvenance("#"), | ||
| 84 | }); | ||
| 85 | |||
| 86 | put(catalog.scopes as Record<typeof scopeId, CatalogV1["scopes"][typeof scopeId]>, scopeId, { | ||
| 87 | id: scopeId, | ||
| 88 | kind: "service", | ||
| 89 | name: "Calendar", | ||
| 90 | namespace: "google.calendar", | ||
| 91 | docs: { | ||
| 92 | summary: "Calendar service", | ||
| 93 | description: "Manage calendar events.", | ||
| 94 | }, | ||
| 95 | synthetic: false, | ||
| 96 | provenance: baseProvenance("#/servers/0"), | ||
| 97 | }); | ||
| 98 | |||
| 99 | put(catalog.symbols as Record<typeof authSchemeId, CatalogV1["symbols"][typeof authSchemeId]>, authSchemeId, { | ||
| 100 | id: authSchemeId, | ||
| 101 | kind: "securityScheme", | ||
| 102 | schemeType: "oauth2", | ||
| 103 | docs: { | ||
| 104 | summary: "OAuth 2.0", | ||
| 105 | }, | ||
| 106 | oauth: { | ||
| 107 | scopes: { | ||
| 108 | "calendar.events": "Manage calendar events", | ||
| 109 | }, | ||
| 110 | }, | ||
| 111 | synthetic: false, | ||
| 112 | provenance: baseProvenance("#/components/securitySchemes/oauth2"), | ||
| 113 | }); | ||
| 114 | |||
| 115 | put(catalog.symbols as Record<typeof stringShapeId, CatalogV1["symbols"][typeof stringShapeId]>, stringShapeId, { | ||
| 116 | id: stringShapeId, | ||
| 117 | kind: "shape", | ||
| 118 | resourceId, | ||
| 119 | title: "String", | ||
| 120 | node: { | ||
| 121 | type: "scalar", | ||
| 122 | scalar: "string", | ||
| 123 | }, | ||
| 124 | synthetic: false, | ||
| 125 | provenance: baseProvenance("#/components/schemas/String"), | ||
| 126 | }); | ||
| 127 | |||
| 128 | put(catalog.symbols as Record<typeof bodyShapeId, CatalogV1["symbols"][typeof bodyShapeId]>, bodyShapeId, { | ||
| 129 | id: bodyShapeId, | ||
| 130 | kind: "shape", | ||
| 131 | resourceId, | ||
| 132 | title: "EventUpdateBody", | ||
| 133 | node: { | ||
| 134 | type: "object", | ||
| 135 | fields: { | ||
| 136 | summary: { | ||
| 137 | shapeId: stringShapeId, | ||
| 138 | }, | ||
| 139 | }, | ||
| 140 | additionalProperties: false, | ||
| 141 | }, | ||
| 142 | synthetic: false, | ||
| 143 | provenance: baseProvenance("#/components/schemas/EventUpdateBody"), | ||
| 144 | }); | ||
| 145 | |||
| 146 | put(catalog.symbols as Record<typeof resultShapeId, CatalogV1["symbols"][typeof resultShapeId]>, resultShapeId, { | ||
| 147 | id: resultShapeId, | ||
| 148 | kind: "shape", | ||
| 149 | resourceId, | ||
| 150 | title: "Event", | ||
| 151 | node: { | ||
| 152 | type: "object", | ||
| 153 | fields: { | ||
| 154 | id: { | ||
| 155 | shapeId: stringShapeId, | ||
| 156 | }, | ||
| 157 | summary: { | ||
| 158 | shapeId: stringShapeId, | ||
| 159 | }, | ||
| 160 | }, | ||
| 161 | required: ["id"], | ||
| 162 | additionalProperties: false, | ||
| 163 | }, | ||
| 164 | synthetic: false, | ||
| 165 | provenance: baseProvenance("#/components/schemas/Event"), | ||
| 166 | }); | ||
| 167 | |||
| 168 | put(catalog.symbols as Record<typeof exampleId, CatalogV1["symbols"][typeof exampleId]>, exampleId, { | ||
| 169 | id: exampleId, | ||
| 170 | kind: "example", | ||
| 171 | exampleKind: "value", | ||
| 172 | name: "Event body example", | ||
| 173 | value: { | ||
| 174 | summary: "Team sync", | ||
| 175 | }, | ||
| 176 | synthetic: false, | ||
| 177 | provenance: baseProvenance("#/components/examples/EventBody"), | ||
| 178 | }); | ||
| 179 | |||
| 180 | put(catalog.symbols as Record<typeof calendarParamId, CatalogV1["symbols"][typeof calendarParamId]>, calendarParamId, { | ||
| 181 | id: calendarParamId, | ||
| 182 | kind: "parameter", | ||
| 183 | name: input.collideIds ? "id" : "calendarId", | ||
| 184 | location: "path", | ||
| 185 | required: true, | ||
| 186 | docs: { | ||
| 187 | summary: "Calendar identifier", | ||
| 188 | description: "The target calendar.", | ||
| 189 | }, | ||
| 190 | schemaShapeId: stringShapeId, | ||
| 191 | synthetic: false, | ||
| 192 | provenance: baseProvenance("#/paths/~1events~1{calendarId}/parameters/0"), | ||
| 193 | }); | ||
| 194 | |||
| 195 | put(catalog.symbols as Record<typeof idPathParamId, CatalogV1["symbols"][typeof idPathParamId]>, idPathParamId, { | ||
| 196 | id: idPathParamId, | ||
| 197 | kind: "parameter", | ||
| 198 | name: "eventId", | ||
| 199 | location: "path", | ||
| 200 | required: true, | ||
| 201 | docs: { | ||
| 202 | summary: "Event identifier", | ||
| 203 | }, | ||
| 204 | schemaShapeId: stringShapeId, | ||
| 205 | synthetic: false, | ||
| 206 | provenance: baseProvenance("#/paths/~1events~1{eventId}/parameters/0"), | ||
| 207 | }); | ||
| 208 | |||
| 209 | put(catalog.symbols as Record<typeof queryParamId, CatalogV1["symbols"][typeof queryParamId]>, queryParamId, { | ||
| 210 | id: queryParamId, | ||
| 211 | kind: "parameter", | ||
| 212 | name: input.collideIds ? "id" : "sendUpdates", | ||
| 213 | location: "query", | ||
| 214 | docs: { | ||
| 215 | summary: "Notification mode", | ||
| 216 | }, | ||
| 217 | schemaShapeId: stringShapeId, | ||
| 218 | synthetic: false, | ||
| 219 | provenance: baseProvenance("#/paths/~1events/parameters/0"), | ||
| 220 | }); | ||
| 221 | |||
| 222 | put(catalog.symbols as Record<typeof requestBodyId, CatalogV1["symbols"][typeof requestBodyId]>, requestBodyId, { | ||
| 223 | id: requestBodyId, | ||
| 224 | kind: "requestBody", | ||
| 225 | required: true, | ||
| 226 | docs: { | ||
| 227 | summary: "Event payload", | ||
| 228 | }, | ||
| 229 | contents: [ | ||
| 230 | { | ||
| 231 | mediaType: "application/json", | ||
| 232 | shapeId: bodyShapeId, | ||
| 233 | exampleIds: [exampleId], | ||
| 234 | }, | ||
| 235 | ], | ||
| 236 | synthetic: false, | ||
| 237 | provenance: baseProvenance("#/paths/~1events/requestBody"), | ||
| 238 | }); | ||
| 239 | |||
| 240 | put(catalog.symbols as Record<typeof responseId, CatalogV1["symbols"][typeof responseId]>, responseId, { | ||
| 241 | id: responseId, | ||
| 242 | kind: "response", | ||
| 243 | docs: { | ||
| 244 | summary: "Updated event", | ||
| 245 | }, | ||
| 246 | contents: [ | ||
| 247 | { | ||
| 248 | mediaType: "application/json", | ||
| 249 | shapeId: resultShapeId, | ||
| 250 | }, | ||
| 251 | ], | ||
| 252 | synthetic: false, | ||
| 253 | provenance: baseProvenance("#/paths/~1events/responses/200"), | ||
| 254 | }); | ||
| 255 | |||
| 256 | put(catalog.responseSets as Record<typeof responseSetId, CatalogV1["responseSets"][typeof responseSetId]>, responseSetId, { | ||
| 257 | id: responseSetId, | ||
| 258 | variants: [ | ||
| 259 | { | ||
| 260 | match: { | ||
| 261 | kind: "exact", | ||
| 262 | status: 200, | ||
| 263 | }, | ||
| 264 | responseId, | ||
| 265 | traits: ["success"], | ||
| 266 | }, | ||
| 267 | ], | ||
| 268 | synthetic: false, | ||
| 269 | provenance: baseProvenance("#/paths/~1events/responses"), | ||
| 270 | }); | ||
| 271 | |||
| 272 | put(catalog.executables as Record<typeof executableId, CatalogV1["executables"][typeof executableId]>, executableId, { | ||
| 273 | id: executableId, | ||
| 274 | protocol: "http", | ||
| 275 | capabilityId, | ||
| 276 | scopeId, | ||
| 277 | method: input.method ?? "PATCH", | ||
| 278 | pathTemplate: "/calendars/{calendarId}/events/{eventId}", | ||
| 279 | pathParameterIds: [calendarParamId, idPathParamId], | ||
| 280 | queryParameterIds: [queryParamId], | ||
| 281 | requestBodyId, | ||
| 282 | responseSetId, | ||
| 283 | synthetic: false, | ||
| 284 | provenance: baseProvenance("#/paths/~1events/patch"), | ||
| 285 | }); | ||
| 286 | |||
| 287 | put(catalog.capabilities as Record<typeof capabilityId, CatalogV1["capabilities"][typeof capabilityId]>, capabilityId, { | ||
| 288 | id: capabilityId, | ||
| 289 | serviceScopeId: scopeId, | ||
| 290 | surface: { | ||
| 291 | toolPath: ["google", "calendar", "events", "update"], | ||
| 292 | title: "Update event", | ||
| 293 | summary: "Update a calendar event.", | ||
| 294 | description: "Updates an existing event by ID.", | ||
| 295 | tags: ["calendar", "events"], | ||
| 296 | }, | ||
| 297 | semantics: { | ||
| 298 | effect: "write", | ||
| 299 | safe: false, | ||
| 300 | idempotent: false, | ||
| 301 | destructive: false, | ||
| 302 | }, | ||
| 303 | auth: { | ||
| 304 | kind: "scheme", | ||
| 305 | schemeId: authSchemeId, | ||
| 306 | scopes: ["calendar.events"], | ||
| 307 | }, | ||
| 308 | interaction: { | ||
| 309 | approval: { | ||
| 310 | mayRequire: true, | ||
| 311 | reasons: ["write"], | ||
| 312 | }, | ||
| 313 | elicitation: { | ||
| 314 | mayRequest: false, | ||
| 315 | }, | ||
| 316 | resume: { | ||
| 317 | supported: true, | ||
| 318 | }, | ||
| 319 | }, | ||
| 320 | executableIds: [executableId], | ||
| 321 | preferredExecutableId: executableId, | ||
| 322 | synthetic: false, | ||
| 323 | provenance: baseProvenance("#/paths/~1events/patch"), | ||
| 324 | }); | ||
| 325 | |||
| 326 | return catalog; | ||
| 327 | }; | ||
| 328 | |||
| 329 | const createGraphqlCatalog = (): CatalogV1 => { | ||
| 330 | const catalog = createEmptyCatalogV1(); | ||
| 331 | |||
| 332 | const resourceId = ResourceIdSchema.make("res_graphql"); | ||
| 333 | const scopeId = ScopeIdSchema.make("scope_graphql_service"); | ||
| 334 | const argShapeId = ShapeSymbolIdSchema.make("shape_graphql_args"); | ||
| 335 | const inputShapeId = ShapeSymbolIdSchema.make("shape_graphql_input"); | ||
| 336 | const selectShapeId = ShapeSymbolIdSchema.make("shape_graphql_select"); | ||
| 337 | const resultShapeId = ShapeSymbolIdSchema.make("shape_graphql_result"); | ||
| 338 | const stringShapeId = ShapeSymbolIdSchema.make("shape_graphql_string"); | ||
| 339 | const responseId = ResponseSymbolIdSchema.make("sym_graphql_response"); | ||
| 340 | const responseSetId = ResponseSetIdSchema.make("response_set_graphql"); | ||
| 341 | const executableId = ExecutableIdSchema.make("exec_graphql_update"); | ||
| 342 | const capabilityId = CapabilityIdSchema.make("cap_graphql_update"); | ||
| 343 | |||
| 344 | put(catalog.documents as Record<typeof docId, CatalogV1["documents"][typeof docId]>, docId, { | ||
| 345 | id: docId, | ||
| 346 | kind: "graphql-schema", | ||
| 347 | title: "GraphQL schema", | ||
| 348 | fetchedAt: "2026-03-14T00:00:00.000Z", | ||
| 349 | rawRef: "memory://graphql/schema.json", | ||
| 350 | }); | ||
| 351 | |||
| 352 | put(catalog.resources as Record<typeof resourceId, CatalogV1["resources"][typeof resourceId]>, resourceId, { | ||
| 353 | id: resourceId, | ||
| 354 | documentId: docId, | ||
| 355 | canonicalUri: "https://example.test/graphql", | ||
| 356 | baseUri: "https://example.test/graphql", | ||
| 357 | anchors: {}, | ||
| 358 | dynamicAnchors: {}, | ||
| 359 | synthetic: false, | ||
| 360 | provenance: baseProvenance("#"), | ||
| 361 | }); | ||
| 362 | |||
| 363 | put(catalog.scopes as Record<typeof scopeId, CatalogV1["scopes"][typeof scopeId]>, scopeId, { | ||
| 364 | id: scopeId, | ||
| 365 | kind: "service", | ||
| 366 | name: "GraphQL", | ||
| 367 | synthetic: false, | ||
| 368 | provenance: baseProvenance("#/schema"), | ||
| 369 | }); | ||
| 370 | |||
| 371 | put(catalog.symbols as Record<typeof stringShapeId, CatalogV1["symbols"][typeof stringShapeId]>, stringShapeId, { | ||
| 372 | id: stringShapeId, | ||
| 373 | kind: "shape", | ||
| 374 | resourceId, | ||
| 375 | title: "String", | ||
| 376 | node: { | ||
| 377 | type: "scalar", | ||
| 378 | scalar: "string", | ||
| 379 | }, | ||
| 380 | synthetic: false, | ||
| 381 | provenance: baseProvenance("#/$defs/String"), | ||
| 382 | }); | ||
| 383 | |||
| 384 | put(catalog.symbols as Record<typeof inputShapeId, CatalogV1["symbols"][typeof inputShapeId]>, inputShapeId, { | ||
| 385 | id: inputShapeId, | ||
| 386 | kind: "shape", | ||
| 387 | resourceId, | ||
| 388 | title: "UserInput", | ||
| 389 | node: { | ||
| 390 | type: "object", | ||
| 391 | fields: { | ||
| 392 | name: { | ||
| 393 | shapeId: stringShapeId, | ||
| 394 | }, | ||
| 395 | }, | ||
| 396 | additionalProperties: false, | ||
| 397 | }, | ||
| 398 | synthetic: false, | ||
| 399 | provenance: baseProvenance("#/$defs/UserInput"), | ||
| 400 | }); | ||
| 401 | |||
| 402 | put(catalog.symbols as Record<typeof argShapeId, CatalogV1["symbols"][typeof argShapeId]>, argShapeId, { | ||
| 403 | id: argShapeId, | ||
| 404 | kind: "shape", | ||
| 405 | resourceId, | ||
| 406 | title: "UpdateUserArgs", | ||
| 407 | node: { | ||
| 408 | type: "object", | ||
| 409 | fields: { | ||
| 410 | id: { | ||
| 411 | shapeId: stringShapeId, | ||
| 412 | }, | ||
| 413 | input: { | ||
| 414 | shapeId: inputShapeId, | ||
| 415 | }, | ||
| 416 | }, | ||
| 417 | required: ["id", "input"], | ||
| 418 | additionalProperties: false, | ||
| 419 | }, | ||
| 420 | synthetic: false, | ||
| 421 | provenance: baseProvenance("#/$defs/UpdateUserArgs"), | ||
| 422 | }); | ||
| 423 | |||
| 424 | put(catalog.symbols as Record<typeof selectShapeId, CatalogV1["symbols"][typeof selectShapeId]>, selectShapeId, { | ||
| 425 | id: selectShapeId, | ||
| 426 | kind: "shape", | ||
| 427 | resourceId, | ||
| 428 | title: "UserSelection", | ||
| 429 | node: { | ||
| 430 | type: "object", | ||
| 431 | fields: { | ||
| 432 | id: { | ||
| 433 | shapeId: stringShapeId, | ||
| 434 | }, | ||
| 435 | name: { | ||
| 436 | shapeId: stringShapeId, | ||
| 437 | }, | ||
| 438 | }, | ||
| 439 | additionalProperties: false, | ||
| 440 | }, | ||
| 441 | synthetic: false, | ||
| 442 | provenance: baseProvenance("#/$defs/UserSelection"), | ||
| 443 | }); | ||
| 444 | |||
| 445 | put(catalog.symbols as Record<typeof resultShapeId, CatalogV1["symbols"][typeof resultShapeId]>, resultShapeId, { | ||
| 446 | id: resultShapeId, | ||
| 447 | kind: "shape", | ||
| 448 | resourceId, | ||
| 449 | title: "User", | ||
| 450 | node: { | ||
| 451 | type: "object", | ||
| 452 | fields: { | ||
| 453 | id: { | ||
| 454 | shapeId: stringShapeId, | ||
| 455 | }, | ||
| 456 | name: { | ||
| 457 | shapeId: stringShapeId, | ||
| 458 | }, | ||
| 459 | }, | ||
| 460 | required: ["id"], | ||
| 461 | additionalProperties: false, | ||
| 462 | }, | ||
| 463 | synthetic: false, | ||
| 464 | provenance: baseProvenance("#/$defs/User"), | ||
| 465 | }); | ||
| 466 | |||
| 467 | put(catalog.symbols as Record<typeof responseId, CatalogV1["symbols"][typeof responseId]>, responseId, { | ||
| 468 | id: responseId, | ||
| 469 | kind: "response", | ||
| 470 | contents: [ | ||
| 471 | { | ||
| 472 | mediaType: "application/json", | ||
| 473 | shapeId: resultShapeId, | ||
| 474 | }, | ||
| 475 | ], | ||
| 476 | synthetic: false, | ||
| 477 | provenance: baseProvenance("#/responses/default"), | ||
| 478 | }); | ||
| 479 | |||
| 480 | put(catalog.responseSets as Record<typeof responseSetId, CatalogV1["responseSets"][typeof responseSetId]>, responseSetId, { | ||
| 481 | id: responseSetId, | ||
| 482 | variants: [ | ||
| 483 | { | ||
| 484 | match: { | ||
| 485 | kind: "default", | ||
| 486 | }, | ||
| 487 | responseId, | ||
| 488 | traits: ["success"], | ||
| 489 | }, | ||
| 490 | ], | ||
| 491 | synthetic: false, | ||
| 492 | provenance: baseProvenance("#/responses"), | ||
| 493 | }); | ||
| 494 | |||
| 495 | put(catalog.executables as Record<typeof executableId, CatalogV1["executables"][typeof executableId]>, executableId, { | ||
| 496 | id: executableId, | ||
| 497 | protocol: "graphql", | ||
| 498 | capabilityId, | ||
| 499 | scopeId, | ||
| 500 | operationType: "mutation", | ||
| 501 | rootField: "updateUser", | ||
| 502 | argumentShapeId: argShapeId, | ||
| 503 | resultShapeId, | ||
| 504 | selectionShapeId: selectShapeId, | ||
| 505 | selectionMode: "caller", | ||
| 506 | responseSetId, | ||
| 507 | synthetic: false, | ||
| 508 | provenance: baseProvenance("#/mutations/updateUser"), | ||
| 509 | }); | ||
| 510 | |||
| 511 | put(catalog.capabilities as Record<typeof capabilityId, CatalogV1["capabilities"][typeof capabilityId]>, capabilityId, { | ||
| 512 | id: capabilityId, | ||
| 513 | serviceScopeId: scopeId, | ||
| 514 | surface: { | ||
| 515 | toolPath: ["graphql", "users", "update"], | ||
| 516 | title: "Update user", | ||
| 517 | summary: "Update a user.", | ||
| 518 | }, | ||
| 519 | semantics: { | ||
| 520 | effect: "write", | ||
| 521 | }, | ||
| 522 | auth: { | ||
| 523 | kind: "none", | ||
| 524 | }, | ||
| 525 | interaction: { | ||
| 526 | approval: { | ||
| 527 | mayRequire: true, | ||
| 528 | reasons: ["write"], | ||
| 529 | }, | ||
| 530 | elicitation: { | ||
| 531 | mayRequest: false, | ||
| 532 | }, | ||
| 533 | resume: { | ||
| 534 | supported: false, | ||
| 535 | }, | ||
| 536 | }, | ||
| 537 | executableIds: [executableId], | ||
| 538 | preferredExecutableId: executableId, | ||
| 539 | synthetic: false, | ||
| 540 | provenance: baseProvenance("#/mutations/updateUser"), | ||
| 541 | }); | ||
| 542 | |||
| 543 | return catalog; | ||
| 544 | }; | ||
| 545 | |||
| 546 | describe("IR catalog", () => { | ||
| 547 | it("decodes a snapshot through the canonical schema", () => { | ||
| 548 | const catalog = createHttpCatalog(); | ||
| 549 | const snapshot = createCatalogSnapshotV1({ | ||
| 550 | import: { | ||
| 551 | sourceKind: "openapi", | ||
| 552 | adapterKey: "openapi", | ||
| 553 | importerVersion: "1.0.0", | ||
| 554 | importedAt: "2026-03-14T00:00:00.000Z", | ||
| 555 | sourceConfigHash: "hash_source", | ||
| 556 | }, | ||
| 557 | catalog, | ||
| 558 | }); | ||
| 559 | |||
| 560 | const decoded = decodeCatalogSnapshotV1(snapshot); | ||
| 561 | expect(decoded.version).toBe("ir.v1.snapshot"); | ||
| 562 | expect(decoded.catalog.capabilities[CapabilityIdSchema.make("cap_events_update")]?.surface.title).toBe( | ||
| 563 | "Update event", | ||
| 564 | ); | ||
| 565 | }); | ||
| 566 | |||
| 567 | it("merges fragments and emits diagnostics on conflicting IDs", () => { | ||
| 568 | const fragmentA: CatalogFragmentV1 = { | ||
| 569 | version: "ir.v1.fragment", | ||
| 570 | documents: { | ||
| 571 | [docId]: { | ||
| 572 | id: docId, | ||
| 573 | kind: "openapi", | ||
| 574 | title: "A", | ||
| 575 | fetchedAt: "2026-03-14T00:00:00.000Z", | ||
| 576 | rawRef: "memory://a", | ||
| 577 | }, | ||
| 578 | }, | ||
| 579 | }; | ||
| 580 | const fragmentB: CatalogFragmentV1 = { | ||
| 581 | version: "ir.v1.fragment", | ||
| 582 | documents: { | ||
| 583 | [docId]: { | ||
| 584 | id: docId, | ||
| 585 | kind: "openapi", | ||
| 586 | title: "B", | ||
| 587 | fetchedAt: "2026-03-14T00:00:00.000Z", | ||
| 588 | rawRef: "memory://b", | ||
| 589 | }, | ||
| 590 | }, | ||
| 591 | }; | ||
| 592 | |||
| 593 | const merged = mergeCatalogFragments([fragmentA, fragmentB]); | ||
| 594 | expect(merged.documents[docId]?.title).toBe("A"); | ||
| 595 | expect(Object.values(merged.diagnostics).some((diagnostic) => diagnostic.code === "merge_conflict_preserved_first")).toBe(true); | ||
| 596 | }); | ||
| 597 | |||
| 598 | it("reports invariant violations for unresolved shapes without diagnostics", () => { | ||
| 599 | const catalog = createEmptyCatalogV1(); | ||
| 600 | const shapeId = ShapeSymbolIdSchema.make("shape_unresolved"); | ||
| 601 | |||
| 602 | put(catalog.documents as Record<typeof docId, CatalogV1["documents"][typeof docId]>, docId, { | ||
| 603 | id: docId, | ||
| 604 | kind: "custom", | ||
| 605 | fetchedAt: "2026-03-14T00:00:00.000Z", | ||
| 606 | rawRef: "memory://custom", | ||
| 607 | }); | ||
| 608 | |||
| 609 | put(catalog.symbols as Record<typeof shapeId, CatalogV1["symbols"][typeof shapeId]>, shapeId, { | ||
| 610 | id: shapeId, | ||
| 611 | kind: "shape", | ||
| 612 | title: "Unresolved", | ||
| 613 | node: { | ||
| 614 | type: "unknown", | ||
| 615 | reason: "unresolved reference #/components/schemas/Missing", | ||
| 616 | }, | ||
| 617 | synthetic: false, | ||
| 618 | provenance: baseProvenance("#/components/schemas/Missing"), | ||
| 619 | }); | ||
| 620 | |||
| 621 | const violations = validateCatalogInvariants(catalog); | ||
| 622 | expect(violations.map((violation) => violation.code)).toContain("missing_resource_context"); | ||
| 623 | expect(violations.map((violation) => violation.code)).toContain("missing_unresolved_ref_diagnostic"); | ||
| 624 | }); | ||
| 625 | |||
| 626 | it("projects HTTP capabilities into ergonomic call and result shapes", () => { | ||
| 627 | const projected = projectCatalogForAgentSdk({ | ||
| 628 | catalog: createHttpCatalog(), | ||
| 629 | }); | ||
| 630 | |||
| 631 | const descriptor = projected.toolDescriptors[CapabilityIdSchema.make("cap_events_update")]; | ||
| 632 | expect(descriptor.toolPath).toEqual(["google", "calendar", "events", "update"]); | ||
| 633 | expect(descriptor.resultShapeId).toBe(ShapeSymbolIdSchema.make("shape_event_result")); | ||
| 634 | |||
| 635 | const callShape = projected.catalog.symbols[descriptor.callShapeId]; | ||
| 636 | expect(callShape?.kind).toBe("shape"); | ||
| 637 | if (callShape?.kind !== "shape" || callShape.node.type !== "object") { | ||
| 638 | throw new Error("Expected projected call shape object"); | ||
| 639 | } | ||
| 640 | |||
| 641 | expect(Object.keys(callShape.node.fields)).toEqual([ | ||
| 642 | "calendarId", | ||
| 643 | "eventId", | ||
| 644 | "sendUpdates", | ||
| 645 | "body", | ||
| 646 | ]); | ||
| 647 | expect(callShape.node.required).toEqual(["calendarId", "eventId", "body"]); | ||
| 648 | |||
| 649 | const searchDoc = projected.searchDocs[CapabilityIdSchema.make("cap_events_update")]; | ||
| 650 | expect(searchDoc.authHints).toContain("oauth2"); | ||
| 651 | expect(searchDoc.tags).toEqual(["calendar", "events"]); | ||
| 652 | }); | ||
| 653 | |||
| 654 | it("groups colliding HTTP parameter names into path/query containers", () => { | ||
| 655 | const projected = projectCatalogForAgentSdk({ | ||
| 656 | catalog: createHttpCatalog({ collideIds: true }), | ||
| 657 | }); | ||
| 658 | |||
| 659 | const descriptor = projected.toolDescriptors[CapabilityIdSchema.make("cap_events_update")]; | ||
| 660 | const callShape = projected.catalog.symbols[descriptor.callShapeId]; | ||
| 661 | if (callShape?.kind !== "shape" || callShape.node.type !== "object") { | ||
| 662 | throw new Error("Expected projected call shape object"); | ||
| 663 | } | ||
| 664 | |||
| 665 | expect(Object.keys(callShape.node.fields)).toContain("path"); | ||
| 666 | expect(Object.keys(callShape.node.fields)).toContain("query"); | ||
| 667 | expect(Object.keys(callShape.node.fields)).not.toContain("id"); | ||
| 668 | expect(Object.values(projected.catalog.diagnostics).some((diagnostic) => diagnostic.code === "projection_collision_grouped_fields")).toBe(true); | ||
| 669 | }); | ||
| 670 | |||
| 671 | it("keeps GraphQL selection explicit for caller-driven outputs", () => { | ||
| 672 | const projected = projectCatalogForAgentSdk({ | ||
| 673 | catalog: createGraphqlCatalog(), | ||
| 674 | }); | ||
| 675 | |||
| 676 | const descriptor = projected.toolDescriptors[CapabilityIdSchema.make("cap_graphql_update")]; | ||
| 677 | const callShape = projected.catalog.symbols[descriptor.callShapeId]; | ||
| 678 | if (callShape?.kind !== "shape" || callShape.node.type !== "object") { | ||
| 679 | throw new Error("Expected projected GraphQL call shape object"); | ||
| 680 | } | ||
| 681 | |||
| 682 | expect(Object.keys(callShape.node.fields)).toEqual(["id", "input", "select"]); | ||
| 683 | expect(descriptor.resultShapeId).toBe(ShapeSymbolIdSchema.make("shape_graphql_result")); | ||
| 684 | }); | ||
| 685 | |||
| 686 | it("builds inspect views and invocation plans from the projected graph", () => { | ||
| 687 | const projected = projectCatalogForAgentSdk({ | ||
| 688 | catalog: createHttpCatalog(), | ||
| 689 | }); | ||
| 690 | const capabilityId = CapabilityIdSchema.make("cap_events_update"); | ||
| 691 | const descriptor = projected.toolDescriptors[capabilityId]; | ||
| 692 | const shallow = projectSymbolShallowView(projected.catalog, descriptor.callShapeId); | ||
| 693 | const expanded = projectSymbolExpandedView(projected.catalog, descriptor.callShapeId); | ||
| 694 | const plan = buildInvocationPlan({ | ||
| 695 | catalog: projected.catalog, | ||
| 696 | capabilityId, | ||
| 697 | connectionBindingId: "binding_primary", | ||
| 698 | resolvedEndpoint: "https://example.test", | ||
| 699 | requestPlan: { | ||
| 700 | method: "PATCH", | ||
| 701 | path: "/calendars/primary/events/abc", | ||
| 702 | }, | ||
| 703 | }); | ||
| 704 | |||
| 705 | expect(shallow.edges.some((edge) => edge.label === "field:body")).toBe(true); | ||
| 706 | expect(expanded.symbol.id).toBe(descriptor.callShapeId); | ||
| 707 | expect(plan.connectionBindingId).toBe("binding_primary"); | ||
| 708 | expect(plan.resolvedScopeChain).toEqual([ScopeIdSchema.make("scope_service")]); | ||
| 709 | expect(plan.resolvedInteraction?.requiresApproval).toBe(true); | ||
| 710 | }); | ||
| 711 | }); | ||
| @@ -0,0 +1,1628 @@ | |||
| 1 | import { sha256Hex } from "@executor/codemode-core"; | ||
| 2 | import * as ParseResult from "effect/ParseResult"; | ||
| 3 | import { Schema } from "effect"; | ||
| 4 | |||
| 5 | import { | ||
| 6 | CapabilityIdSchema, | ||
| 7 | type CapabilityId, | ||
| 8 | DiagnosticIdSchema, | ||
| 9 | type DiagnosticId, | ||
| 10 | DocumentIdSchema, | ||
| 11 | type DocumentId, | ||
| 12 | ExecutableIdSchema, | ||
| 13 | type ExecutableId, | ||
| 14 | ExampleSymbolIdSchema, | ||
| 15 | type ExampleSymbolId, | ||
| 16 | HeaderSymbolIdSchema, | ||
| 17 | type HeaderSymbolId, | ||
| 18 | ParameterSymbolIdSchema, | ||
| 19 | type ParameterSymbolId, | ||
| 20 | type RequestBodySymbolId, | ||
| 21 | ResourceIdSchema, | ||
| 22 | type ResourceId, | ||
| 23 | type ResponseSymbolId, | ||
| 24 | type ResponseSetId, | ||
| 25 | ScopeIdSchema, | ||
| 26 | type ScopeId, | ||
| 27 | type ShapeSymbolId, | ||
| 28 | ShapeSymbolIdSchema, | ||
| 29 | type SymbolId, | ||
| 30 | } from "./ids"; | ||
| 31 | import { | ||
| 32 | CatalogFragmentV1Schema, | ||
| 33 | type CatalogFragmentV1, | ||
| 34 | CatalogSnapshotV1Schema, | ||
| 35 | CatalogV1Schema, | ||
| 36 | type CatalogV1, | ||
| 37 | type Capability, | ||
| 38 | type ContentSpec, | ||
| 39 | type DocumentationBlock, | ||
| 40 | type Executable, | ||
| 41 | type GraphQLExecutable, | ||
| 42 | type HttpExecutable, | ||
| 43 | type ImportDiagnostic, | ||
| 44 | type ImportMetadata, | ||
| 45 | type InteractionSpec, | ||
| 46 | type McpExecutable, | ||
| 47 | type ParameterSymbol, | ||
| 48 | type HeaderSymbol, | ||
| 49 | type ResponseSet, | ||
| 50 | type ResponseSymbol, | ||
| 51 | type Scope, | ||
| 52 | type ScopeDefaults, | ||
| 53 | type ShapeSymbol, | ||
| 54 | type StatusMatch, | ||
| 55 | type Symbol as IrSymbol, | ||
| 56 | } from "./model"; | ||
| 57 | |||
| 58 | export interface ToolDescriptor { | ||
| 59 | toolPath: string[]; | ||
| 60 | capabilityId: CapabilityId; | ||
| 61 | title?: string; | ||
| 62 | summary?: string; | ||
| 63 | effect: Capability["semantics"]["effect"]; | ||
| 64 | interaction: { | ||
| 65 | mayRequireApproval: boolean; | ||
| 66 | mayElicit: boolean; | ||
| 67 | }; | ||
| 68 | callShapeId: ShapeSymbolId; | ||
| 69 | resultShapeId?: ShapeSymbolId; | ||
| 70 | responseSetId: ResponseSetId; | ||
| 71 | diagnosticCounts: { | ||
| 72 | warning: number; | ||
| 73 | error: number; | ||
| 74 | }; | ||
| 75 | } | ||
| 76 | |||
| 77 | export interface ToolSearchDoc { | ||
| 78 | capabilityId: CapabilityId; | ||
| 79 | toolPath: string[]; | ||
| 80 | title?: string; | ||
| 81 | summary?: string; | ||
| 82 | tags?: string[]; | ||
| 83 | protocolHints: string[]; | ||
| 84 | authHints: string[]; | ||
| 85 | effect: Capability["semantics"]["effect"]; | ||
| 86 | } | ||
| 87 | |||
| 88 | export interface CapabilitySummaryView { | ||
| 89 | capabilityId: CapabilityId; | ||
| 90 | toolPath: string[]; | ||
| 91 | summary?: string; | ||
| 92 | executableIds: ExecutableId[]; | ||
| 93 | auth: Capability["auth"]; | ||
| 94 | interaction: InteractionSpec; | ||
| 95 | callShapeId: ShapeSymbolId; | ||
| 96 | resultShapeId?: ShapeSymbolId; | ||
| 97 | responseSetId: ResponseSetId; | ||
| 98 | diagnosticIds?: DiagnosticId[]; | ||
| 99 | } | ||
| 100 | |||
| 101 | export interface SymbolShallowView { | ||
| 102 | symbolId: SymbolId; | ||
| 103 | kind: IrSymbol["kind"]; | ||
| 104 | title?: string; | ||
| 105 | summary?: string; | ||
| 106 | edges: Array<{ label: string; targetId: SymbolId }>; | ||
| 107 | } | ||
| 108 | |||
| 109 | export interface SymbolExpandedView { | ||
| 110 | symbolId: SymbolId; | ||
| 111 | symbol: IrSymbol; | ||
| 112 | diagnostics?: ImportDiagnostic[]; | ||
| 113 | } | ||
| 114 | |||
| 115 | export interface InvocationPlan { | ||
| 116 | capabilityId: CapabilityId; | ||
| 117 | executableId: ExecutableId; | ||
| 118 | connectionBindingId: string; | ||
| 119 | resolvedScopeChain: ScopeId[]; | ||
| 120 | resolvedEndpoint?: string; | ||
| 121 | resolvedAuth?: unknown; | ||
| 122 | resolvedInteraction?: { | ||
| 123 | requiresApproval: boolean; | ||
| 124 | mayPauseForElicitation: boolean; | ||
| 125 | resumable: boolean; | ||
| 126 | }; | ||
| 127 | requestPlan: unknown; | ||
| 128 | provenance: CatalogV1["capabilities"][CapabilityId]["provenance"]; | ||
| 129 | } | ||
| 130 | |||
| 131 | export interface ProjectedCatalog { | ||
| 132 | catalog: CatalogV1; | ||
| 133 | toolDescriptors: Record<CapabilityId, ToolDescriptor>; | ||
| 134 | searchDocs: Record<CapabilityId, ToolSearchDoc>; | ||
| 135 | capabilityViews: Record<CapabilityId, CapabilitySummaryView>; | ||
| 136 | } | ||
| 137 | |||
| 138 | export interface CatalogInvariantViolation { | ||
| 139 | code: | ||
| 140 | | "missing_symbol_provenance" | ||
| 141 | | "missing_entity_provenance" | ||
| 142 | | "missing_resource_context" | ||
| 143 | | "missing_reference_target" | ||
| 144 | | "missing_unresolved_ref_diagnostic" | ||
| 145 | | "missing_executable" | ||
| 146 | | "missing_response_set" | ||
| 147 | | "missing_scope" | ||
| 148 | | "missing_service_scope" | ||
| 149 | | "invalid_preferred_executable" | ||
| 150 | | "missing_projection_synthetic_marker"; | ||
| 151 | message: string; | ||
| 152 | entityId?: string; | ||
| 153 | } | ||
| 154 | |||
| 155 | export type DecodeCatalogError = Error; | ||
| 156 | |||
| 157 | type MutableScopeDefaults = { | ||
| 158 | servers?: ScopeDefaults["servers"]; | ||
| 159 | auth?: ScopeDefaults["auth"]; | ||
| 160 | parameterIds?: ParameterSymbolId[]; | ||
| 161 | headerIds?: HeaderSymbolId[]; | ||
| 162 | variables?: Record<string, string>; | ||
| 163 | }; | ||
| 164 | |||
| 165 | type CatalogSnapshotV1 = { | ||
| 166 | version: "ir.v1.snapshot"; | ||
| 167 | import: ImportMetadata; | ||
| 168 | catalog: CatalogV1; | ||
| 169 | }; | ||
| 170 | |||
| 171 | const decodeCatalogSync = Schema.decodeUnknownSync( | ||
| 172 | CatalogV1Schema as Schema.Schema<CatalogV1, unknown, never>, | ||
| 173 | ); | ||
| 174 | const decodeCatalogSnapshotSync = Schema.decodeUnknownSync( | ||
| 175 | CatalogSnapshotV1Schema as Schema.Schema<CatalogSnapshotV1, unknown, never>, | ||
| 176 | ); | ||
| 177 | const decodeCatalogFragmentSync = Schema.decodeUnknownSync( | ||
| 178 | CatalogFragmentV1Schema as Schema.Schema<CatalogFragmentV1, unknown, never>, | ||
| 179 | ); | ||
| 180 | |||
| 181 | const stableStringify = (value: unknown): string => { | ||
| 182 | if (value === null || typeof value !== "object") { | ||
| 183 | return JSON.stringify(value); | ||
| 184 | } | ||
| 185 | |||
| 186 | if (Array.isArray(value)) { | ||
| 187 | return `[${value.map((entry) => stableStringify(entry)).join(",")}]`; | ||
| 188 | } | ||
| 189 | |||
| 190 | const entries = Object.entries(value as Record<string, unknown>) | ||
| 191 | .sort(([left], [right]) => left.localeCompare(right)) | ||
| 192 | .map(([key, entry]) => `${JSON.stringify(key)}:${stableStringify(entry)}`); | ||
| 193 | return `{${entries.join(",")}}`; | ||
| 194 | }; | ||
| 195 | |||
| 196 | const stableHash = (value: unknown): string => sha256Hex(stableStringify(value)).slice(0, 16); | ||
| 197 | |||
| 198 | const unique = <T>(values: readonly T[]): T[] => [...new Set(values)]; | ||
| 199 | |||
| 200 | const cloneCatalog = (catalog: CatalogV1): CatalogV1 => ({ | ||
| 201 | version: catalog.version, | ||
| 202 | documents: { ...catalog.documents }, | ||
| 203 | resources: { ...catalog.resources }, | ||
| 204 | scopes: { ...catalog.scopes }, | ||
| 205 | symbols: { ...catalog.symbols }, | ||
| 206 | capabilities: { ...catalog.capabilities }, | ||
| 207 | executables: { ...catalog.executables }, | ||
| 208 | responseSets: { ...catalog.responseSets }, | ||
| 209 | diagnostics: { ...catalog.diagnostics }, | ||
| 210 | }); | ||
| 211 | |||
| 212 | const mutableRecord = <K extends string, V>(value: Readonly<Record<K, V>>): Record<K, V> => | ||
| 213 | value as unknown as Record<K, V>; | ||
| 214 | |||
| 215 | const createStableEntityId = <A extends string>( | ||
| 216 | make: (value: string) => A, | ||
| 217 | prefix: string, | ||
| 218 | value: unknown, | ||
| 219 | ): A => make(`${prefix}_${stableHash(value)}`); | ||
| 220 | |||
| 221 | const emptyCatalog = (): CatalogV1 => ({ | ||
| 222 | version: "ir.v1", | ||
| 223 | documents: {}, | ||
| 224 | resources: {}, | ||
| 225 | scopes: {}, | ||
| 226 | symbols: {}, | ||
| 227 | capabilities: {}, | ||
| 228 | executables: {}, | ||
| 229 | responseSets: {}, | ||
| 230 | diagnostics: {}, | ||
| 231 | }); | ||
| 232 | |||
| 233 | const authHintStrings = (catalog: CatalogV1, auth: Capability["auth"]): string[] => { | ||
| 234 | switch (auth.kind) { | ||
| 235 | case "none": | ||
| 236 | return ["none"]; | ||
| 237 | case "scheme": { | ||
| 238 | const scheme = catalog.symbols[auth.schemeId]; | ||
| 239 | if (!scheme || scheme.kind !== "securityScheme") { | ||
| 240 | return ["unknown"]; | ||
| 241 | } | ||
| 242 | return [ | ||
| 243 | scheme.schemeType, | ||
| 244 | ...((auth.scopes ?? []).map((scope: string) => `scope:${scope}`)), | ||
| 245 | ]; | ||
| 246 | } | ||
| 247 | case "allOf": | ||
| 248 | case "anyOf": | ||
| 249 | return unique(auth.items.flatMap((item: Capability["auth"]) => authHintStrings(catalog, item))); | ||
| 250 | } | ||
| 251 | }; | ||
| 252 | |||
| 253 | const docsSummary = (docs: DocumentationBlock | undefined): string | undefined => | ||
| 254 | docs?.summary ?? docs?.description; | ||
| 255 | |||
| 256 | const getShape = (catalog: CatalogV1, shapeId: ShapeSymbolId): ShapeSymbol | undefined => { | ||
| 257 | const symbol = catalog.symbols[shapeId]; | ||
| 258 | return symbol && symbol.kind === "shape" ? symbol : undefined; | ||
| 259 | }; | ||
| 260 | |||
| 261 | const getParameter = ( | ||
| 262 | catalog: CatalogV1, | ||
| 263 | parameterId: ParameterSymbolId, | ||
| 264 | ): ParameterSymbol | undefined => { | ||
| 265 | const symbol = catalog.symbols[parameterId]; | ||
| 266 | return symbol && symbol.kind === "parameter" ? symbol : undefined; | ||
| 267 | }; | ||
| 268 | |||
| 269 | const getResponseSymbol = ( | ||
| 270 | catalog: CatalogV1, | ||
| 271 | responseId: ResponseSymbolId, | ||
| 272 | ): ResponseSymbol | undefined => { | ||
| 273 | const symbol = catalog.symbols[responseId]; | ||
| 274 | return symbol && symbol.kind === "response" ? symbol : undefined; | ||
| 275 | }; | ||
| 276 | |||
| 277 | const getHeader = ( | ||
| 278 | catalog: CatalogV1, | ||
| 279 | headerId: HeaderSymbolId, | ||
| 280 | ): HeaderSymbol | undefined => { | ||
| 281 | const symbol = catalog.symbols[headerId]; | ||
| 282 | return symbol && symbol.kind === "header" ? symbol : undefined; | ||
| 283 | }; | ||
| 284 | |||
| 285 | const scopeChain = (catalog: CatalogV1, scopeId: ScopeId): Scope[] => { | ||
| 286 | const chain: Scope[] = []; | ||
| 287 | let currentId: ScopeId | undefined = scopeId; | ||
| 288 | |||
| 289 | while (currentId) { | ||
| 290 | const scope: Scope | undefined = catalog.scopes[currentId]; | ||
| 291 | if (!scope) { | ||
| 292 | break; | ||
| 293 | } | ||
| 294 | chain.unshift(scope); | ||
| 295 | currentId = scope.parentId; | ||
| 296 | } | ||
| 297 | |||
| 298 | return chain; | ||
| 299 | }; | ||
| 300 | |||
| 301 | const mergeScopeDefaults = (chain: readonly Scope[]): ScopeDefaults => { | ||
| 302 | const merged: MutableScopeDefaults = {}; | ||
| 303 | |||
| 304 | for (const scope of chain) { | ||
| 305 | const defaults = scope.defaults; | ||
| 306 | if (!defaults) { | ||
| 307 | continue; | ||
| 308 | } | ||
| 309 | |||
| 310 | if (defaults.servers) { | ||
| 311 | merged.servers = [...defaults.servers]; | ||
| 312 | } | ||
| 313 | |||
| 314 | if (defaults.auth) { | ||
| 315 | merged.auth = defaults.auth; | ||
| 316 | } | ||
| 317 | |||
| 318 | if (defaults.parameterIds) { | ||
| 319 | merged.parameterIds = unique([ | ||
| 320 | ...(merged.parameterIds ?? []), | ||
| 321 | ...defaults.parameterIds, | ||
| 322 | ]); | ||
| 323 | } | ||
| 324 | |||
| 325 | if (defaults.headerIds) { | ||
| 326 | merged.headerIds = unique([ | ||
| 327 | ...(merged.headerIds ?? []), | ||
| 328 | ...defaults.headerIds, | ||
| 329 | ]); | ||
| 330 | } | ||
| 331 | |||
| 332 | if (defaults.variables) { | ||
| 333 | merged.variables = { | ||
| 334 | ...(merged.variables ?? {}), | ||
| 335 | ...defaults.variables, | ||
| 336 | }; | ||
| 337 | } | ||
| 338 | } | ||
| 339 | |||
| 340 | return merged as ScopeDefaults; | ||
| 341 | }; | ||
| 342 | |||
| 343 | const isJsonMediaType = (mediaType: string): boolean => { | ||
| 344 | const normalized = mediaType.trim().toLowerCase(); | ||
| 345 | return normalized === "application/json" | ||
| 346 | || normalized.endsWith("+json") | ||
| 347 | || normalized === "text/json"; | ||
| 348 | }; | ||
| 349 | |||
| 350 | const createDiagnostic = ( | ||
| 351 | catalog: CatalogV1, | ||
| 352 | input: Omit<ImportDiagnostic, "id"> & { idSeed: unknown }, | ||
| 353 | ): DiagnosticId => { | ||
| 354 | const diagnosticId = createStableEntityId( | ||
| 355 | DiagnosticIdSchema.make, | ||
| 356 | "diag", | ||
| 357 | input.idSeed, | ||
| 358 | ); | ||
| 359 | |||
| 360 | mutableRecord(catalog.diagnostics)[diagnosticId] = { | ||
| 361 | id: diagnosticId, | ||
| 362 | level: input.level, | ||
| 363 | code: input.code, | ||
| 364 | message: input.message, | ||
| 365 | ...(input.relatedSymbolIds ? { relatedSymbolIds: input.relatedSymbolIds } : {}), | ||
| 366 | provenance: input.provenance, | ||
| 367 | }; | ||
| 368 | |||
| 369 | return diagnosticId; | ||
| 370 | }; | ||
| 371 | |||
| 372 | const ensureProjectionResource = ( | ||
| 373 | catalog: CatalogV1, | ||
| 374 | capability: Capability, | ||
| 375 | ): ResourceId => { | ||
| 376 | const resourceId = createStableEntityId( | ||
| 377 | ResourceIdSchema.make, | ||
| 378 | "res", | ||
| 379 | { | ||
| 380 | kind: "projection", | ||
| 381 | capabilityId: capability.id, | ||
| 382 | }, | ||
| 383 | ); | ||
| 384 | |||
| 385 | if (!catalog.resources[resourceId]) { | ||
| 386 | const serviceScope = catalog.scopes[capability.serviceScopeId]; | ||
| 387 | const documentId = serviceScope?.provenance[0]?.documentId | ||
| 388 | ?? DocumentIdSchema.make(`doc_projection_${stableHash(capability.id)}`); | ||
| 389 | |||
| 390 | if (!catalog.documents[documentId]) { | ||
| 391 | mutableRecord(catalog.documents)[documentId] = { | ||
| 392 | id: documentId, | ||
| 393 | kind: "custom", | ||
| 394 | title: `Projection resource for ${capability.id}`, | ||
| 395 | fetchedAt: new Date(0).toISOString(), | ||
| 396 | rawRef: `synthetic://projection/${capability.id}`, | ||
| 397 | }; | ||
| 398 | } | ||
| 399 | |||
| 400 | mutableRecord(catalog.resources)[resourceId] = { | ||
| 401 | id: resourceId, | ||
| 402 | documentId, | ||
| 403 | canonicalUri: `synthetic://projection/${capability.id}`, | ||
| 404 | baseUri: `synthetic://projection/${capability.id}`, | ||
| 405 | anchors: {}, | ||
| 406 | dynamicAnchors: {}, | ||
| 407 | synthetic: true, | ||
| 408 | provenance: capability.provenance, | ||
| 409 | }; | ||
| 410 | |||
| 411 | createDiagnostic(catalog, { | ||
| 412 | idSeed: { | ||
| 413 | code: "synthetic_resource_context_created", | ||
| 414 | capabilityId: capability.id, | ||
| 415 | }, | ||
| 416 | level: "info", | ||
| 417 | code: "synthetic_resource_context_created", | ||
| 418 | message: `Created synthetic projection resource for ${capability.id}`, | ||
| 419 | provenance: capability.provenance, | ||
| 420 | }); | ||
| 421 | } | ||
| 422 | |||
| 423 | return resourceId; | ||
| 424 | }; | ||
| 425 | |||
| 426 | const createSyntheticShape = ( | ||
| 427 | catalog: CatalogV1, | ||
| 428 | input: { | ||
| 429 | capability: Capability; | ||
| 430 | label: string; | ||
| 431 | node: ShapeSymbol["node"]; | ||
| 432 | title?: string; | ||
| 433 | docs?: DocumentationBlock; | ||
| 434 | diagnostic?: | ||
| 435 | | { | ||
| 436 | level: ImportDiagnostic["level"]; | ||
| 437 | code: ImportDiagnostic["code"]; | ||
| 438 | message: string; | ||
| 439 | relatedSymbolIds?: SymbolId[]; | ||
| 440 | } | ||
| 441 | | undefined; | ||
| 442 | }, | ||
| 443 | ): ShapeSymbolId => { | ||
| 444 | const resourceId = ensureProjectionResource(catalog, input.capability); | ||
| 445 | const shapeId = createStableEntityId( | ||
| 446 | ShapeSymbolIdSchema.make, | ||
| 447 | "shape", | ||
| 448 | { | ||
| 449 | capabilityId: input.capability.id, | ||
| 450 | label: input.label, | ||
| 451 | node: input.node, | ||
| 452 | }, | ||
| 453 | ); | ||
| 454 | |||
| 455 | if (!catalog.symbols[shapeId]) { | ||
| 456 | const diagnostics = input.diagnostic | ||
| 457 | ? [ | ||
| 458 | createDiagnostic(catalog, { | ||
| 459 | idSeed: { | ||
| 460 | code: input.diagnostic.code, | ||
| 461 | shapeId, | ||
| 462 | }, | ||
| 463 | level: input.diagnostic.level, | ||
| 464 | code: input.diagnostic.code, | ||
| 465 | message: input.diagnostic.message, | ||
| 466 | ...(input.diagnostic.relatedSymbolIds | ||
| 467 | ? { relatedSymbolIds: input.diagnostic.relatedSymbolIds } | ||
| 468 | : {}), | ||
| 469 | provenance: input.capability.provenance, | ||
| 470 | }), | ||
| 471 | ] | ||
| 472 | : undefined; | ||
| 473 | |||
| 474 | mutableRecord(catalog.symbols)[shapeId] = { | ||
| 475 | id: shapeId, | ||
| 476 | kind: "shape", | ||
| 477 | resourceId, | ||
| 478 | ...(input.title ? { title: input.title } : {}), | ||
| 479 | ...(input.docs ? { docs: input.docs } : {}), | ||
| 480 | node: input.node, | ||
| 481 | synthetic: true, | ||
| 482 | provenance: input.capability.provenance, | ||
| 483 | ...(diagnostics ? { diagnosticIds: diagnostics } : {}), | ||
| 484 | }; | ||
| 485 | } | ||
| 486 | |||
| 487 | return shapeId; | ||
| 488 | }; | ||
| 489 | |||
| 490 | const contentShapeId = ( | ||
| 491 | catalog: CatalogV1, | ||
| 492 | capability: Capability, | ||
| 493 | contents: readonly ContentSpec[] | undefined, | ||
| 494 | label: string, | ||
| 495 | ): ShapeSymbolId | undefined => { | ||
| 496 | const shapeIds = unique( | ||
| 497 | (contents ?? []) | ||
| 498 | .map((content) => content.shapeId) | ||
| 499 | .filter((shapeId): shapeId is ShapeSymbolId => shapeId !== undefined), | ||
| 500 | ); | ||
| 501 | |||
| 502 | if (shapeIds.length === 0) { | ||
| 503 | return undefined; | ||
| 504 | } | ||
| 505 | |||
| 506 | if (shapeIds.length === 1) { | ||
| 507 | return shapeIds[0]; | ||
| 508 | } | ||
| 509 | |||
| 510 | return createSyntheticShape(catalog, { | ||
| 511 | capability, | ||
| 512 | label, | ||
| 513 | title: `${capability.surface.title ?? capability.id} content union`, | ||
| 514 | node: { | ||
| 515 | type: "anyOf", | ||
| 516 | items: shapeIds, | ||
| 517 | }, | ||
| 518 | diagnostic: { | ||
| 519 | level: "warning", | ||
| 520 | code: "projection_result_shape_synthesized", | ||
| 521 | message: `Synthesized content union for ${capability.id}`, | ||
| 522 | relatedSymbolIds: shapeIds, | ||
| 523 | }, | ||
| 524 | }); | ||
| 525 | }; | ||
| 526 | |||
| 527 | const parameterShapeId = ( | ||
| 528 | catalog: CatalogV1, | ||
| 529 | capability: Capability, | ||
| 530 | parameter: ParameterSymbol, | ||
| 531 | ): ShapeSymbolId => { | ||
| 532 | if (parameter.schemaShapeId) { | ||
| 533 | return parameter.schemaShapeId; | ||
| 534 | } | ||
| 535 | |||
| 536 | const contentShape = contentShapeId( | ||
| 537 | catalog, | ||
| 538 | capability, | ||
| 539 | parameter.content, | ||
| 540 | `parameter:${parameter.id}`, | ||
| 541 | ); | ||
| 542 | if (contentShape) { | ||
| 543 | return contentShape; | ||
| 544 | } | ||
| 545 | |||
| 546 | return createSyntheticShape(catalog, { | ||
| 547 | capability, | ||
| 548 | label: `parameter:${parameter.id}:unknown`, | ||
| 549 | title: parameter.name, | ||
| 550 | docs: parameter.docs, | ||
| 551 | node: { | ||
| 552 | type: "unknown", | ||
| 553 | reason: `Missing shape for parameter ${parameter.id}`, | ||
| 554 | }, | ||
| 555 | diagnostic: { | ||
| 556 | level: "warning", | ||
| 557 | code: "projection_call_shape_synthesized", | ||
| 558 | message: `Missing parameter shape for ${parameter.name}; using unknown`, | ||
| 559 | relatedSymbolIds: [parameter.id], | ||
| 560 | }, | ||
| 561 | }); | ||
| 562 | }; | ||
| 563 | |||
| 564 | const requestBodyShapeId = ( | ||
| 565 | catalog: CatalogV1, | ||
| 566 | capability: Capability, | ||
| 567 | requestBodyId: RequestBodySymbolId | undefined, | ||
| 568 | ): ShapeSymbolId | undefined => { | ||
| 569 | if (!requestBodyId) { | ||
| 570 | return undefined; | ||
| 571 | } | ||
| 572 | |||
| 573 | const requestBody = catalog.symbols[requestBodyId]; | ||
| 574 | if (!requestBody || requestBody.kind !== "requestBody") { | ||
| 575 | return undefined; | ||
| 576 | } | ||
| 577 | |||
| 578 | return contentShapeId( | ||
| 579 | catalog, | ||
| 580 | capability, | ||
| 581 | requestBody.contents, | ||
| 582 | `requestBody:${requestBody.id}`, | ||
| 583 | ); | ||
| 584 | }; | ||
| 585 | |||
| 586 | const chooseExecutable = (catalog: CatalogV1, capability: Capability): Executable => { | ||
| 587 | const preferredId = capability.preferredExecutableId; | ||
| 588 | if (preferredId) { | ||
| 589 | const preferred = catalog.executables[preferredId]; | ||
| 590 | if (preferred) { | ||
| 591 | return preferred; | ||
| 592 | } | ||
| 593 | } | ||
| 594 | |||
| 595 | const executables = capability.executableIds | ||
| 596 | .map((executableId) => catalog.executables[executableId]) | ||
| 597 | .filter((executable): executable is Executable => executable !== undefined); | ||
| 598 | |||
| 599 | if (executables.length === 0) { | ||
| 600 | throw new Error(`Capability ${capability.id} has no executables`); | ||
| 601 | } | ||
| 602 | |||
| 603 | return executables[0]; | ||
| 604 | }; | ||
| 605 | |||
| 606 | const scoreStatusMatch = (match: StatusMatch): number => { | ||
| 607 | switch (match.kind) { | ||
| 608 | case "exact": | ||
| 609 | return match.status === 200 ? 100 : match.status >= 200 && match.status < 300 ? 80 : 10; | ||
| 610 | case "range": | ||
| 611 | return match.value === "2XX" ? 60 : 5; | ||
| 612 | case "default": | ||
| 613 | return 40; | ||
| 614 | } | ||
| 615 | }; | ||
| 616 | |||
| 617 | const responseContentCandidates = ( | ||
| 618 | response: ResponseSymbol, | ||
| 619 | ): Array<{ mediaType: string; shapeId: ShapeSymbolId }> => | ||
| 620 | (response.contents ?? []) | ||
| 621 | .flatMap((content) => | ||
| 622 | content.shapeId ? [{ mediaType: content.mediaType, shapeId: content.shapeId }] : [], | ||
| 623 | ); | ||
| 624 | |||
| 625 | const projectResultShapeFromResponses = ( | ||
| 626 | catalog: CatalogV1, | ||
| 627 | capability: Capability, | ||
| 628 | responseSet: ResponseSet, | ||
| 629 | ): ShapeSymbolId | undefined => { | ||
| 630 | const ranked = [...responseSet.variants] | ||
| 631 | .map((variant) => { | ||
| 632 | const response = getResponseSymbol(catalog, variant.responseId); | ||
| 633 | return response | ||
| 634 | ? { | ||
| 635 | variant, | ||
| 636 | response, | ||
| 637 | score: scoreStatusMatch(variant.match), | ||
| 638 | } | ||
| 639 | : null; | ||
| 640 | }) | ||
| 641 | .filter((entry): entry is { variant: ResponseSet["variants"][number]; response: ResponseSymbol; score: number } => entry !== null) | ||
| 642 | .sort((left, right) => right.score - left.score); | ||
| 643 | |||
| 644 | const jsonCandidates = ranked.flatMap(({ response }) => | ||
| 645 | responseContentCandidates(response).filter((candidate) => isJsonMediaType(candidate.mediaType)), | ||
| 646 | ); | ||
| 647 | |||
| 648 | if (jsonCandidates.length > 0) { | ||
| 649 | return contentShapeId( | ||
| 650 | catalog, | ||
| 651 | capability, | ||
| 652 | jsonCandidates.map((candidate) => ({ | ||
| 653 | mediaType: candidate.mediaType, | ||
| 654 | shapeId: candidate.shapeId, | ||
| 655 | })), | ||
| 656 | `responseSet:${responseSet.id}:json`, | ||
| 657 | ); | ||
| 658 | } | ||
| 659 | |||
| 660 | const fallbackShapeIds = unique( | ||
| 661 | ranked.flatMap(({ response }) => | ||
| 662 | responseContentCandidates(response).map((candidate) => candidate.shapeId), | ||
| 663 | ), | ||
| 664 | ); | ||
| 665 | |||
| 666 | if (fallbackShapeIds.length === 0) { | ||
| 667 | return undefined; | ||
| 668 | } | ||
| 669 | |||
| 670 | if (fallbackShapeIds.length === 1) { | ||
| 671 | return fallbackShapeIds[0]; | ||
| 672 | } | ||
| 673 | |||
| 674 | return createSyntheticShape(catalog, { | ||
| 675 | capability, | ||
| 676 | label: `responseSet:${responseSet.id}:union`, | ||
| 677 | title: `${capability.surface.title ?? capability.id} result`, | ||
| 678 | node: { | ||
| 679 | type: "anyOf", | ||
| 680 | items: fallbackShapeIds, | ||
| 681 | }, | ||
| 682 | diagnostic: { | ||
| 683 | level: "warning", | ||
| 684 | code: "multi_response_union_synthesized", | ||
| 685 | message: `Synthesized response union for ${capability.id}`, | ||
| 686 | relatedSymbolIds: fallbackShapeIds, | ||
| 687 | }, | ||
| 688 | }); | ||
| 689 | }; | ||
| 690 | |||
| 691 | const groupFieldShape = ( | ||
| 692 | catalog: CatalogV1, | ||
| 693 | capability: Capability, | ||
| 694 | location: "path" | "query" | "headers" | "cookies", | ||
| 695 | fields: Record<string, { shapeId: ShapeSymbolId; docs?: DocumentationBlock }>, | ||
| 696 | required: string[], | ||
| 697 | ): ShapeSymbolId => | ||
| 698 | createSyntheticShape(catalog, { | ||
| 699 | capability, | ||
| 700 | label: `group:${capability.id}:${location}`, | ||
| 701 | title: `${location} parameters`, | ||
| 702 | node: { | ||
| 703 | type: "object", | ||
| 704 | fields: Object.fromEntries( | ||
| 705 | Object.entries(fields).map(([name, field]) => [ | ||
| 706 | name, | ||
| 707 | { | ||
| 708 | shapeId: field.shapeId, | ||
| 709 | ...(field.docs ? { docs: field.docs } : {}), | ||
| 710 | }, | ||
| 711 | ]), | ||
| 712 | ), | ||
| 713 | ...(required.length > 0 ? { required } : {}), | ||
| 714 | additionalProperties: false, | ||
| 715 | }, | ||
| 716 | }); | ||
| 717 | |||
| 718 | const projectHttpCallShape = ( | ||
| 719 | catalog: CatalogV1, | ||
| 720 | capability: Capability, | ||
| 721 | executable: HttpExecutable, | ||
| 722 | ): ShapeSymbolId => { | ||
| 723 | const chain = scopeChain(catalog, executable.scopeId); | ||
| 724 | const defaults = mergeScopeDefaults(chain); | ||
| 725 | |||
| 726 | const byLocation: Record< | ||
| 727 | "path" | "query" | "header" | "cookie", | ||
| 728 | ParameterSymbol[] | ||
| 729 | > = { | ||
| 730 | path: [], | ||
| 731 | query: [], | ||
| 732 | header: [], | ||
| 733 | cookie: [], | ||
| 734 | }; | ||
| 735 | |||
| 736 | const pushParameter = (parameterId: ParameterSymbolId) => { | ||
| 737 | const parameter = getParameter(catalog, parameterId); | ||
| 738 | if (parameter) { | ||
| 739 | byLocation[parameter.location].push(parameter); | ||
| 740 | } | ||
| 741 | }; | ||
| 742 | |||
| 743 | for (const parameterId of defaults.parameterIds ?? []) { | ||
| 744 | pushParameter(parameterId); | ||
| 745 | } | ||
| 746 | for (const parameterId of executable.pathParameterIds ?? []) { | ||
| 747 | pushParameter(parameterId); | ||
| 748 | } | ||
| 749 | for (const parameterId of executable.queryParameterIds ?? []) { | ||
| 750 | pushParameter(parameterId); | ||
| 751 | } | ||
| 752 | for (const parameterId of executable.headerParameterIds ?? []) { | ||
| 753 | pushParameter(parameterId); | ||
| 754 | } | ||
| 755 | for (const parameterId of executable.cookieParameterIds ?? []) { | ||
| 756 | pushParameter(parameterId); | ||
| 757 | } | ||
| 758 | |||
| 759 | const explicitHeaders = unique([ | ||
| 760 | ...(defaults.headerIds ?? []), | ||
| 761 | ]) | ||
| 762 | .map((headerId: HeaderSymbolId) => getHeader(catalog, headerId)) | ||
| 763 | .filter((symbol): symbol is HeaderSymbol => symbol !== undefined); | ||
| 764 | |||
| 765 | const reservedTopLevel = new Set(["body", "args", "select", "path", "query", "headers", "cookies"]); | ||
| 766 | const allNames = [ | ||
| 767 | ...byLocation.path.map((parameter) => parameter.name), | ||
| 768 | ...byLocation.query.map((parameter) => parameter.name), | ||
| 769 | ...byLocation.header.map((parameter) => parameter.name), | ||
| 770 | ...byLocation.cookie.map((parameter) => parameter.name), | ||
| 771 | ]; | ||
| 772 | |||
| 773 | const hasCollision = allNames.some((name, index) => | ||
| 774 | reservedTopLevel.has(name) || allNames.indexOf(name) !== index | ||
| 775 | ); | ||
| 776 | |||
| 777 | const topLevelFields: Record<string, { shapeId: ShapeSymbolId; docs?: DocumentationBlock }> = {}; | ||
| 778 | const topLevelRequired: string[] = []; | ||
| 779 | |||
| 780 | const groupedFieldNames: Array<keyof typeof byLocation> = hasCollision | ||
| 781 | ? ["path", "query", "header", "cookie"] | ||
| 782 | : []; | ||
| 783 | |||
| 784 | if (hasCollision) { | ||
| 785 | createDiagnostic(catalog, { | ||
| 786 | idSeed: { | ||
| 787 | code: "projection_collision_grouped_fields", | ||
| 788 | capabilityId: capability.id, | ||
| 789 | }, | ||
| 790 | level: "info", | ||
| 791 | code: "projection_collision_grouped_fields", | ||
| 792 | message: `Grouped parameter fields for ${capability.id} to avoid collisions`, | ||
| 793 | provenance: capability.provenance, | ||
| 794 | }); | ||
| 795 | } | ||
| 796 | |||
| 797 | for (const location of Object.keys(byLocation) as Array<keyof typeof byLocation>) { | ||
| 798 | const parameters = byLocation[location]; | ||
| 799 | if (parameters.length === 0) { | ||
| 800 | continue; | ||
| 801 | } | ||
| 802 | |||
| 803 | if (groupedFieldNames.includes(location)) { | ||
| 804 | const groupFields: Record<string, { shapeId: ShapeSymbolId; docs?: DocumentationBlock }> = {}; | ||
| 805 | const groupRequired: string[] = []; | ||
| 806 | for (const parameter of parameters) { | ||
| 807 | groupFields[parameter.name] = { | ||
| 808 | shapeId: parameterShapeId(catalog, capability, parameter), | ||
| 809 | docs: parameter.docs, | ||
| 810 | }; | ||
| 811 | if (parameter.required) { | ||
| 812 | groupRequired.push(parameter.name); | ||
| 813 | } | ||
| 814 | } | ||
| 815 | |||
| 816 | const groupKey = location === "header" ? "headers" : location === "cookie" ? "cookies" : location; | ||
| 817 | topLevelFields[groupKey] = { | ||
| 818 | shapeId: groupFieldShape(catalog, capability, groupKey, groupFields, groupRequired), | ||
| 819 | }; | ||
| 820 | if (groupRequired.length > 0) { | ||
| 821 | topLevelRequired.push(groupKey); | ||
| 822 | } | ||
| 823 | continue; | ||
| 824 | } | ||
| 825 | |||
| 826 | for (const parameter of parameters) { | ||
| 827 | topLevelFields[parameter.name] = { | ||
| 828 | shapeId: parameterShapeId(catalog, capability, parameter), | ||
| 829 | docs: parameter.docs, | ||
| 830 | }; | ||
| 831 | if (parameter.required) { | ||
| 832 | topLevelRequired.push(parameter.name); | ||
| 833 | } | ||
| 834 | } | ||
| 835 | } | ||
| 836 | |||
| 837 | for (const header of explicitHeaders) { | ||
| 838 | if (hasCollision) { | ||
| 839 | const existing = topLevelFields.headers | ||
| 840 | ? getShape(catalog, topLevelFields.headers.shapeId) | ||
| 841 | : undefined; | ||
| 842 | const fields = existing && existing.node.type === "object" | ||
| 843 | ? ({ ...existing.node.fields } as Record<string, { shapeId: ShapeSymbolId; docs?: DocumentationBlock }>) | ||
| 844 | : {}; | ||
| 845 | fields[header.name] = { | ||
| 846 | shapeId: header.schemaShapeId | ||
| 847 | ?? createSyntheticShape(catalog, { | ||
| 848 | capability, | ||
| 849 | label: `header:${header.id}:unknown`, | ||
| 850 | title: header.name, | ||
| 851 | docs: header.docs, | ||
| 852 | node: { | ||
| 853 | type: "unknown", | ||
| 854 | reason: `Missing shape for header ${header.id}`, | ||
| 855 | }, | ||
| 856 | }), | ||
| 857 | ...(header.docs ? { docs: header.docs } : {}), | ||
| 858 | }; | ||
| 859 | topLevelFields.headers = { | ||
| 860 | shapeId: groupFieldShape( | ||
| 861 | catalog, | ||
| 862 | capability, | ||
| 863 | "headers", | ||
| 864 | Object.fromEntries( | ||
| 865 | Object.entries(fields).map(([name, field]) => [name, { shapeId: field.shapeId, docs: field.docs }]), | ||
| 866 | ), | ||
| 867 | [], | ||
| 868 | ), | ||
| 869 | }; | ||
| 870 | continue; | ||
| 871 | } | ||
| 872 | |||
| 873 | topLevelFields[header.name] = { | ||
| 874 | shapeId: header.schemaShapeId | ||
| 875 | ?? createSyntheticShape(catalog, { | ||
| 876 | capability, | ||
| 877 | label: `header:${header.id}:unknown`, | ||
| 878 | title: header.name, | ||
| 879 | docs: header.docs, | ||
| 880 | node: { | ||
| 881 | type: "unknown", | ||
| 882 | reason: `Missing shape for header ${header.id}`, | ||
| 883 | }, | ||
| 884 | }), | ||
| 885 | docs: header.docs, | ||
| 886 | }; | ||
| 887 | } | ||
| 888 | |||
| 889 | const bodyShapeId = requestBodyShapeId(catalog, capability, executable.requestBodyId); | ||
| 890 | if (bodyShapeId) { | ||
| 891 | topLevelFields.body = { shapeId: bodyShapeId }; | ||
| 892 | const requestBody = executable.requestBodyId | ||
| 893 | ? catalog.symbols[executable.requestBodyId] | ||
| 894 | : undefined; | ||
| 895 | if (requestBody && requestBody.kind === "requestBody" && requestBody.required) { | ||
| 896 | topLevelRequired.push("body"); | ||
| 897 | } | ||
| 898 | } | ||
| 899 | |||
| 900 | return createSyntheticShape(catalog, { | ||
| 901 | capability, | ||
| 902 | label: `call:${capability.id}`, | ||
| 903 | title: `${capability.surface.title ?? capability.id} call`, | ||
| 904 | node: { | ||
| 905 | type: "object", | ||
| 906 | fields: Object.fromEntries( | ||
| 907 | Object.entries(topLevelFields).map(([name, field]) => [ | ||
| 908 | name, | ||
| 909 | { | ||
| 910 | shapeId: field.shapeId, | ||
| 911 | ...(field.docs ? { docs: field.docs } : {}), | ||
| 912 | }, | ||
| 913 | ]), | ||
| 914 | ), | ||
| 915 | ...(topLevelRequired.length > 0 ? { required: unique(topLevelRequired) } : {}), | ||
| 916 | additionalProperties: false, | ||
| 917 | }, | ||
| 918 | diagnostic: { | ||
| 919 | level: "info", | ||
| 920 | code: "projection_call_shape_synthesized", | ||
| 921 | message: `Synthesized HTTP call shape for ${capability.id}`, | ||
| 922 | }, | ||
| 923 | }); | ||
| 924 | }; | ||
| 925 | |||
| 926 | const projectGraphqlCallShape = ( | ||
| 927 | catalog: CatalogV1, | ||
| 928 | capability: Capability, | ||
| 929 | executable: GraphQLExecutable, | ||
| 930 | ): ShapeSymbolId => { | ||
| 931 | const argumentShape = getShape(catalog, executable.argumentShapeId); | ||
| 932 | const reserved = new Set(["body", "args", "select", "path", "query", "headers", "cookies"]); | ||
| 933 | |||
| 934 | const canFlatten = | ||
| 935 | argumentShape?.node.type === "object" | ||
| 936 | && Object.keys(argumentShape.node.fields).every((name) => !reserved.has(name)); | ||
| 937 | |||
| 938 | const fields: Record<string, { shapeId: ShapeSymbolId; docs?: DocumentationBlock }> = {}; | ||
| 939 | const required: string[] = []; | ||
| 940 | |||
| 941 | if (canFlatten && argumentShape.node.type === "object") { | ||
| 942 | for (const [name, field] of Object.entries(argumentShape.node.fields)) { | ||
| 943 | fields[name] = { | ||
| 944 | shapeId: field.shapeId, | ||
| 945 | docs: field.docs, | ||
| 946 | }; | ||
| 947 | } | ||
| 948 | required.push(...(argumentShape.node.required ?? [])); | ||
| 949 | } else { | ||
| 950 | fields.args = { | ||
| 951 | shapeId: executable.argumentShapeId, | ||
| 952 | docs: argumentShape?.docs, | ||
| 953 | }; | ||
| 954 | required.push("args"); | ||
| 955 | } | ||
| 956 | |||
| 957 | if (executable.selectionMode === "caller") { | ||
| 958 | fields.select = { | ||
| 959 | shapeId: | ||
| 960 | executable.selectionShapeId | ||
| 961 | ?? createSyntheticShape(catalog, { | ||
| 962 | capability, | ||
| 963 | label: `select:${capability.id}:unknown`, | ||
| 964 | title: `${capability.surface.title ?? capability.id} selection`, | ||
| 965 | node: { | ||
| 966 | type: "unknown", | ||
| 967 | reason: `Missing selection shape for ${capability.id}`, | ||
| 968 | }, | ||
| 969 | diagnostic: { | ||
| 970 | level: "warning", | ||
| 971 | code: "selection_shape_missing", | ||
| 972 | message: `Missing selection shape for ${capability.id}`, | ||
| 973 | }, | ||
| 974 | }), | ||
| 975 | }; | ||
| 976 | required.push("select"); | ||
| 977 | } | ||
| 978 | |||
| 979 | return createSyntheticShape(catalog, { | ||
| 980 | capability, | ||
| 981 | label: `call:${capability.id}`, | ||
| 982 | title: `${capability.surface.title ?? capability.id} call`, | ||
| 983 | node: { | ||
| 984 | type: "object", | ||
| 985 | fields: Object.fromEntries( | ||
| 986 | Object.entries(fields).map(([name, field]) => [ | ||
| 987 | name, | ||
| 988 | { | ||
| 989 | shapeId: field.shapeId, | ||
| 990 | ...(field.docs ? { docs: field.docs } : {}), | ||
| 991 | }, | ||
| 992 | ]), | ||
| 993 | ), | ||
| 994 | ...(required.length > 0 ? { required: unique(required) } : {}), | ||
| 995 | additionalProperties: false, | ||
| 996 | }, | ||
| 997 | diagnostic: { | ||
| 998 | level: "info", | ||
| 999 | code: "projection_call_shape_synthesized", | ||
| 1000 | message: `Synthesized GraphQL call shape for ${capability.id}`, | ||
| 1001 | }, | ||
| 1002 | }); | ||
| 1003 | }; | ||
| 1004 | |||
| 1005 | const projectMcpCallShape = ( | ||
| 1006 | catalog: CatalogV1, | ||
| 1007 | capability: Capability, | ||
| 1008 | executable: McpExecutable, | ||
| 1009 | ): ShapeSymbolId => { | ||
| 1010 | if (!executable.inputShapeId) { | ||
| 1011 | return createSyntheticShape(catalog, { | ||
| 1012 | capability, | ||
| 1013 | label: `call:${capability.id}:empty`, | ||
| 1014 | title: `${capability.surface.title ?? capability.id} call`, | ||
| 1015 | node: { | ||
| 1016 | type: "object", | ||
| 1017 | fields: {}, | ||
| 1018 | additionalProperties: false, | ||
| 1019 | }, | ||
| 1020 | diagnostic: { | ||
| 1021 | level: "info", | ||
| 1022 | code: "projection_call_shape_synthesized", | ||
| 1023 | message: `Synthesized empty MCP call shape for ${capability.id}`, | ||
| 1024 | }, | ||
| 1025 | }); | ||
| 1026 | } | ||
| 1027 | |||
| 1028 | const inputShape = getShape(catalog, executable.inputShapeId); | ||
| 1029 | if (inputShape?.node.type === "object") { | ||
| 1030 | return executable.inputShapeId; | ||
| 1031 | } | ||
| 1032 | |||
| 1033 | return createSyntheticShape(catalog, { | ||
| 1034 | capability, | ||
| 1035 | label: `call:${capability.id}`, | ||
| 1036 | title: `${capability.surface.title ?? capability.id} call`, | ||
| 1037 | node: { | ||
| 1038 | type: "object", | ||
| 1039 | fields: { | ||
| 1040 | input: { | ||
| 1041 | shapeId: executable.inputShapeId, | ||
| 1042 | }, | ||
| 1043 | }, | ||
| 1044 | required: ["input"], | ||
| 1045 | additionalProperties: false, | ||
| 1046 | }, | ||
| 1047 | diagnostic: { | ||
| 1048 | level: "info", | ||
| 1049 | code: "projection_call_shape_synthesized", | ||
| 1050 | message: `Wrapped non-object MCP input shape for ${capability.id}`, | ||
| 1051 | }, | ||
| 1052 | }); | ||
| 1053 | }; | ||
| 1054 | |||
| 1055 | const projectCapability = ( | ||
| 1056 | catalog: CatalogV1, | ||
| 1057 | capability: Capability, | ||
| 1058 | ): ToolDescriptor => { | ||
| 1059 | const executable = chooseExecutable(catalog, capability); | ||
| 1060 | const responseSet = catalog.responseSets[executable.responseSetId]; | ||
| 1061 | |||
| 1062 | if (!responseSet) { | ||
| 1063 | throw new Error(`Missing response set ${executable.responseSetId} for ${capability.id}`); | ||
| 1064 | } | ||
| 1065 | |||
| 1066 | let callShapeId: ShapeSymbolId; | ||
| 1067 | let resultShapeId: ShapeSymbolId | undefined; | ||
| 1068 | |||
| 1069 | switch (executable.protocol) { | ||
| 1070 | case "http": | ||
| 1071 | callShapeId = projectHttpCallShape(catalog, capability, executable); | ||
| 1072 | resultShapeId = projectResultShapeFromResponses(catalog, capability, responseSet); | ||
| 1073 | break; | ||
| 1074 | case "graphql": | ||
| 1075 | callShapeId = projectGraphqlCallShape(catalog, capability, executable); | ||
| 1076 | resultShapeId = executable.resultShapeId; | ||
| 1077 | break; | ||
| 1078 | case "mcp": | ||
| 1079 | callShapeId = projectMcpCallShape(catalog, capability, executable); | ||
| 1080 | resultShapeId = executable.outputShapeId; | ||
| 1081 | break; | ||
| 1082 | } | ||
| 1083 | |||
| 1084 | const diagnosticIds = unique([ | ||
| 1085 | ...(capability.diagnosticIds ?? []), | ||
| 1086 | ...(executable.diagnosticIds ?? []), | ||
| 1087 | ...(responseSet.diagnosticIds ?? []), | ||
| 1088 | ...(callShapeId ? catalog.symbols[callShapeId]?.diagnosticIds ?? [] : []), | ||
| 1089 | ...(resultShapeId ? catalog.symbols[resultShapeId]?.diagnosticIds ?? [] : []), | ||
| 1090 | ]); | ||
| 1091 | const diagnostics = diagnosticIds | ||
| 1092 | .map((diagnosticId) => catalog.diagnostics[diagnosticId]) | ||
| 1093 | .filter((diagnostic): diagnostic is ImportDiagnostic => diagnostic !== undefined); | ||
| 1094 | |||
| 1095 | return { | ||
| 1096 | toolPath: [...capability.surface.toolPath], | ||
| 1097 | capabilityId: capability.id, | ||
| 1098 | ...(capability.surface.title ? { title: capability.surface.title } : {}), | ||
| 1099 | ...(capability.surface.summary ? { summary: capability.surface.summary } : {}), | ||
| 1100 | effect: capability.semantics.effect, | ||
| 1101 | interaction: { | ||
| 1102 | mayRequireApproval: capability.interaction.approval.mayRequire, | ||
| 1103 | mayElicit: capability.interaction.elicitation.mayRequest, | ||
| 1104 | }, | ||
| 1105 | callShapeId, | ||
| 1106 | ...(resultShapeId ? { resultShapeId } : {}), | ||
| 1107 | responseSetId: executable.responseSetId, | ||
| 1108 | diagnosticCounts: { | ||
| 1109 | warning: diagnostics.filter((diagnostic) => diagnostic.level === "warning").length, | ||
| 1110 | error: diagnostics.filter((diagnostic) => diagnostic.level === "error").length, | ||
| 1111 | }, | ||
| 1112 | }; | ||
| 1113 | }; | ||
| 1114 | |||
| 1115 | const summaryView = ( | ||
| 1116 | catalog: CatalogV1, | ||
| 1117 | capability: Capability, | ||
| 1118 | descriptor: ToolDescriptor, | ||
| 1119 | ): CapabilitySummaryView => ({ | ||
| 1120 | capabilityId: capability.id, | ||
| 1121 | toolPath: [...capability.surface.toolPath], | ||
| 1122 | ...(capability.surface.summary ? { summary: capability.surface.summary } : {}), | ||
| 1123 | executableIds: [...capability.executableIds], | ||
| 1124 | auth: capability.auth, | ||
| 1125 | interaction: capability.interaction, | ||
| 1126 | callShapeId: descriptor.callShapeId, | ||
| 1127 | ...(descriptor.resultShapeId ? { resultShapeId: descriptor.resultShapeId } : {}), | ||
| 1128 | responseSetId: descriptor.responseSetId, | ||
| 1129 | ...(capability.diagnosticIds ? { diagnosticIds: [...capability.diagnosticIds] } : {}), | ||
| 1130 | }); | ||
| 1131 | |||
| 1132 | const searchDoc = ( | ||
| 1133 | catalog: CatalogV1, | ||
| 1134 | capability: Capability, | ||
| 1135 | ): ToolSearchDoc => ({ | ||
| 1136 | capabilityId: capability.id, | ||
| 1137 | toolPath: [...capability.surface.toolPath], | ||
| 1138 | ...(capability.surface.title ? { title: capability.surface.title } : {}), | ||
| 1139 | ...(capability.surface.summary ? { summary: capability.surface.summary } : {}), | ||
| 1140 | ...(capability.surface.tags ? { tags: [...capability.surface.tags] } : {}), | ||
| 1141 | protocolHints: unique( | ||
| 1142 | capability.executableIds | ||
| 1143 | .map((executableId: ExecutableId) => catalog.executables[executableId]?.protocol) | ||
| 1144 | .filter((protocol): protocol is Executable["protocol"] => protocol !== undefined), | ||
| 1145 | ), | ||
| 1146 | authHints: authHintStrings(catalog, capability.auth), | ||
| 1147 | effect: capability.semantics.effect, | ||
| 1148 | }); | ||
| 1149 | |||
| 1150 | const symbolTitle = (symbol: IrSymbol): string | undefined => { | ||
| 1151 | switch (symbol.kind) { | ||
| 1152 | case "shape": | ||
| 1153 | return symbol.title ?? symbol.docs?.summary; | ||
| 1154 | case "parameter": | ||
| 1155 | case "header": | ||
| 1156 | return symbol.name; | ||
| 1157 | case "example": | ||
| 1158 | return symbol.name ?? symbol.docs?.summary; | ||
| 1159 | case "requestBody": | ||
| 1160 | case "response": | ||
| 1161 | case "securityScheme": | ||
| 1162 | return symbol.docs?.summary; | ||
| 1163 | } | ||
| 1164 | }; | ||
| 1165 | |||
| 1166 | const symbolSummary = (symbol: IrSymbol): string | undefined => | ||
| 1167 | symbol.docs?.summary ?? docsSummary(symbol.docs); | ||
| 1168 | |||
| 1169 | const symbolEdges = (symbol: IrSymbol): Array<{ label: string; targetId: SymbolId }> => { | ||
| 1170 | switch (symbol.kind) { | ||
| 1171 | case "shape": { | ||
| 1172 | const edges: Array<{ label: string; targetId: SymbolId }> = []; | ||
| 1173 | switch (symbol.node.type) { | ||
| 1174 | case "object": | ||
| 1175 | for (const [name, field] of Object.entries(symbol.node.fields)) { | ||
| 1176 | edges.push({ label: `field:${name}`, targetId: field.shapeId }); | ||
| 1177 | } | ||
| 1178 | if (typeof symbol.node.additionalProperties !== "boolean" && symbol.node.additionalProperties) { | ||
| 1179 | edges.push({ | ||
| 1180 | label: "additionalProperties", | ||
| 1181 | targetId: symbol.node.additionalProperties, | ||
| 1182 | }); | ||
| 1183 | } | ||
| 1184 | for (const [pattern, targetId] of Object.entries(symbol.node.patternProperties ?? {})) { | ||
| 1185 | edges.push({ label: `pattern:${pattern}`, targetId }); | ||
| 1186 | } | ||
| 1187 | break; | ||
| 1188 | case "array": | ||
| 1189 | edges.push({ label: "item", targetId: symbol.node.itemShapeId }); | ||
| 1190 | break; | ||
| 1191 | case "tuple": | ||
| 1192 | symbol.node.itemShapeIds.forEach((targetId, index) => { | ||
| 1193 | edges.push({ label: `item:${String(index)}`, targetId }); | ||
| 1194 | }); | ||
| 1195 | if (typeof symbol.node.additionalItems !== "boolean" && symbol.node.additionalItems) { | ||
| 1196 | edges.push({ label: "additionalItems", targetId: symbol.node.additionalItems }); | ||
| 1197 | } | ||
| 1198 | break; | ||
| 1199 | case "map": | ||
| 1200 | edges.push({ label: "value", targetId: symbol.node.valueShapeId }); | ||
| 1201 | break; | ||
| 1202 | case "allOf": | ||
| 1203 | case "anyOf": | ||
| 1204 | case "oneOf": | ||
| 1205 | symbol.node.items.forEach((targetId, index) => { | ||
| 1206 | edges.push({ label: `${symbol.node.type}:${String(index)}`, targetId }); | ||
| 1207 | }); | ||
| 1208 | if (symbol.node.type === "oneOf") { | ||
| 1209 | for (const [key, targetId] of Object.entries(symbol.node.discriminator?.mapping ?? {})) { | ||
| 1210 | edges.push({ label: `discriminator:${key}`, targetId }); | ||
| 1211 | } | ||
| 1212 | } | ||
| 1213 | break; | ||
| 1214 | case "nullable": | ||
| 1215 | edges.push({ label: "item", targetId: symbol.node.itemShapeId }); | ||
| 1216 | break; | ||
| 1217 | case "ref": | ||
| 1218 | edges.push({ label: "target", targetId: symbol.node.target }); | ||
| 1219 | break; | ||
| 1220 | case "not": | ||
| 1221 | edges.push({ label: "not", targetId: symbol.node.itemShapeId }); | ||
| 1222 | break; | ||
| 1223 | case "conditional": | ||
| 1224 | edges.push({ label: "if", targetId: symbol.node.ifShapeId }); | ||
| 1225 | if (symbol.node.thenShapeId) { | ||
| 1226 | edges.push({ label: "then", targetId: symbol.node.thenShapeId }); | ||
| 1227 | } | ||
| 1228 | if (symbol.node.elseShapeId) { | ||
| 1229 | edges.push({ label: "else", targetId: symbol.node.elseShapeId }); | ||
| 1230 | } | ||
| 1231 | break; | ||
| 1232 | case "graphqlInterface": | ||
| 1233 | for (const [name, field] of Object.entries(symbol.node.fields)) { | ||
| 1234 | edges.push({ label: `field:${name}`, targetId: field.shapeId }); | ||
| 1235 | } | ||
| 1236 | symbol.node.possibleTypeIds.forEach((targetId) => { | ||
| 1237 | edges.push({ label: "possibleType", targetId }); | ||
| 1238 | }); | ||
| 1239 | break; | ||
| 1240 | case "graphqlUnion": | ||
| 1241 | symbol.node.memberTypeIds.forEach((targetId) => { | ||
| 1242 | edges.push({ label: "member", targetId }); | ||
| 1243 | }); | ||
| 1244 | break; | ||
| 1245 | default: | ||
| 1246 | break; | ||
| 1247 | } | ||
| 1248 | return edges; | ||
| 1249 | } | ||
| 1250 | case "parameter": | ||
| 1251 | case "header": | ||
| 1252 | return unique([ | ||
| 1253 | ...(symbol.schemaShapeId ? [{ label: "schema", targetId: symbol.schemaShapeId as SymbolId }] : []), | ||
| 1254 | ...((symbol.content ?? []).flatMap((content, index) => [ | ||
| 1255 | ...(content.shapeId ? [{ label: `content:${String(index)}`, targetId: content.shapeId as SymbolId }] : []), | ||
| 1256 | ...((content.exampleIds ?? []).map((targetId) => ({ | ||
| 1257 | label: `example:${String(index)}`, | ||
| 1258 | targetId: targetId as SymbolId, | ||
| 1259 | }))), | ||
| 1260 | ])), | ||
| 1261 | ...((symbol.exampleIds ?? []).map((targetId) => ({ | ||
| 1262 | label: "example", | ||
| 1263 | targetId: targetId as SymbolId, | ||
| 1264 | }))), | ||
| 1265 | ]); | ||
| 1266 | case "requestBody": | ||
| 1267 | return (symbol.contents ?? []).flatMap((content, index) => [ | ||
| 1268 | ...(content.shapeId ? [{ label: `content:${String(index)}`, targetId: content.shapeId as SymbolId }] : []), | ||
| 1269 | ...((content.exampleIds ?? []).map((targetId) => ({ | ||
| 1270 | label: `example:${String(index)}`, | ||
| 1271 | targetId: targetId as SymbolId, | ||
| 1272 | }))), | ||
| 1273 | ]); | ||
| 1274 | case "response": | ||
| 1275 | return [ | ||
| 1276 | ...((symbol.headerIds ?? []).map((targetId) => ({ | ||
| 1277 | label: "header", | ||
| 1278 | targetId: targetId as SymbolId, | ||
| 1279 | }))), | ||
| 1280 | ...((symbol.contents ?? []).flatMap((content, index) => [ | ||
| 1281 | ...(content.shapeId ? [{ label: `content:${String(index)}`, targetId: content.shapeId as SymbolId }] : []), | ||
| 1282 | ...((content.exampleIds ?? []).map((targetId) => ({ | ||
| 1283 | label: `example:${String(index)}`, | ||
| 1284 | targetId: targetId as SymbolId, | ||
| 1285 | }))), | ||
| 1286 | ])), | ||
| 1287 | ]; | ||
| 1288 | case "example": | ||
| 1289 | case "securityScheme": | ||
| 1290 | return []; | ||
| 1291 | } | ||
| 1292 | }; | ||
| 1293 | |||
| 1294 | export const decodeCatalogV1 = (input: unknown): CatalogV1 => { | ||
| 1295 | try { | ||
| 1296 | return decodeCatalogSync(input); | ||
| 1297 | } catch (cause) { | ||
| 1298 | throw new Error(ParseResult.TreeFormatter.formatErrorSync(cause as never)); | ||
| 1299 | } | ||
| 1300 | }; | ||
| 1301 | |||
| 1302 | export const decodeCatalogFragmentV1 = (input: unknown): CatalogFragmentV1 => { | ||
| 1303 | try { | ||
| 1304 | return decodeCatalogFragmentSync(input); | ||
| 1305 | } catch (cause) { | ||
| 1306 | throw new Error(ParseResult.TreeFormatter.formatErrorSync(cause as never)); | ||
| 1307 | } | ||
| 1308 | }; | ||
| 1309 | |||
| 1310 | export const decodeCatalogSnapshotV1 = (input: unknown) => { | ||
| 1311 | try { | ||
| 1312 | return decodeCatalogSnapshotSync(input); | ||
| 1313 | } catch (cause) { | ||
| 1314 | throw new Error(ParseResult.TreeFormatter.formatErrorSync(cause as never)); | ||
| 1315 | } | ||
| 1316 | }; | ||
| 1317 | |||
| 1318 | export const createEmptyCatalogV1 = (): CatalogV1 => emptyCatalog(); | ||
| 1319 | |||
| 1320 | export const createCatalogSnapshotV1 = (input: { | ||
| 1321 | import: ImportMetadata; | ||
| 1322 | catalog: CatalogV1; | ||
| 1323 | }) => ({ | ||
| 1324 | version: "ir.v1.snapshot" as const, | ||
| 1325 | import: input.import, | ||
| 1326 | catalog: decodeCatalogV1(input.catalog), | ||
| 1327 | }); | ||
| 1328 | |||
| 1329 | export const mergeCatalogFragments = (fragments: readonly CatalogFragmentV1[]): CatalogV1 => { | ||
| 1330 | const catalog = emptyCatalog(); | ||
| 1331 | const seenByCollection = new Map<string, string>(); | ||
| 1332 | |||
| 1333 | const mergeRecord = <T extends { id: string }>( | ||
| 1334 | collectionName: keyof CatalogV1, | ||
| 1335 | entries: Record<string, T> | undefined, | ||
| 1336 | ) => { | ||
| 1337 | if (!entries) { | ||
| 1338 | return; | ||
| 1339 | } | ||
| 1340 | |||
| 1341 | const target = catalog[collectionName] as Record<string, T>; | ||
| 1342 | for (const [id, entry] of Object.entries(entries)) { | ||
| 1343 | const existing = target[id]; | ||
| 1344 | if (!existing) { | ||
| 1345 | target[id] = entry; | ||
| 1346 | seenByCollection.set(`${String(collectionName)}:${id}`, stableStringify(entry)); | ||
| 1347 | continue; | ||
| 1348 | } | ||
| 1349 | |||
| 1350 | const existingHash = seenByCollection.get(`${String(collectionName)}:${id}`) ?? stableStringify(existing); | ||
| 1351 | const nextHash = stableStringify(entry); | ||
| 1352 | if (existingHash === nextHash) { | ||
| 1353 | continue; | ||
| 1354 | } | ||
| 1355 | |||
| 1356 | if (collectionName !== "diagnostics") { | ||
| 1357 | createDiagnostic(catalog, { | ||
| 1358 | idSeed: { | ||
| 1359 | collectionName, | ||
| 1360 | id, | ||
| 1361 | existingHash, | ||
| 1362 | nextHash, | ||
| 1363 | }, | ||
| 1364 | level: "error", | ||
| 1365 | code: "merge_conflict_preserved_first", | ||
| 1366 | message: `Conflicting ${String(collectionName)} entry for ${id}; preserved first value`, | ||
| 1367 | provenance: "provenance" in existing | ||
| 1368 | ? (existing as { provenance: ImportDiagnostic["provenance"] }).provenance | ||
| 1369 | : [], | ||
| 1370 | }); | ||
| 1371 | } | ||
| 1372 | } | ||
| 1373 | }; | ||
| 1374 | |||
| 1375 | for (const fragment of fragments) { | ||
| 1376 | mergeRecord("documents", fragment.documents); | ||
| 1377 | mergeRecord("resources", fragment.resources); | ||
| 1378 | mergeRecord("scopes", fragment.scopes); | ||
| 1379 | mergeRecord("symbols", fragment.symbols); | ||
| 1380 | mergeRecord("capabilities", fragment.capabilities); | ||
| 1381 | mergeRecord("executables", fragment.executables); | ||
| 1382 | mergeRecord("responseSets", fragment.responseSets); | ||
| 1383 | mergeRecord("diagnostics", fragment.diagnostics); | ||
| 1384 | } | ||
| 1385 | |||
| 1386 | return catalog; | ||
| 1387 | }; | ||
| 1388 | |||
| 1389 | export const validateCatalogInvariants = ( | ||
| 1390 | catalog: CatalogV1, | ||
| 1391 | ): CatalogInvariantViolation[] => { | ||
| 1392 | const violations: CatalogInvariantViolation[] = []; | ||
| 1393 | |||
| 1394 | for (const symbol of Object.values(catalog.symbols)) { | ||
| 1395 | if (symbol.provenance.length === 0) { | ||
| 1396 | violations.push({ | ||
| 1397 | code: "missing_symbol_provenance", | ||
| 1398 | entityId: symbol.id, | ||
| 1399 | message: `Symbol ${symbol.id} is missing provenance`, | ||
| 1400 | }); | ||
| 1401 | } | ||
| 1402 | |||
| 1403 | if (symbol.kind === "shape") { | ||
| 1404 | if (!symbol.resourceId && !symbol.synthetic) { | ||
| 1405 | violations.push({ | ||
| 1406 | code: "missing_resource_context", | ||
| 1407 | entityId: symbol.id, | ||
| 1408 | message: `Shape ${symbol.id} must belong to a resource or be synthetic`, | ||
| 1409 | }); | ||
| 1410 | } | ||
| 1411 | |||
| 1412 | if (symbol.node.type === "ref" && !catalog.symbols[symbol.node.target]) { | ||
| 1413 | violations.push({ | ||
| 1414 | code: "missing_reference_target", | ||
| 1415 | entityId: symbol.id, | ||
| 1416 | message: `Shape ${symbol.id} references missing shape ${symbol.node.target}`, | ||
| 1417 | }); | ||
| 1418 | } | ||
| 1419 | |||
| 1420 | if (symbol.node.type === "unknown" && symbol.node.reason?.includes("unresolved")) { | ||
| 1421 | const hasDiagnostic = (symbol.diagnosticIds ?? []) | ||
| 1422 | .map((diagnosticId) => catalog.diagnostics[diagnosticId]) | ||
| 1423 | .some((diagnostic) => diagnostic?.code === "unresolved_ref"); | ||
| 1424 | |||
| 1425 | if (!hasDiagnostic) { | ||
| 1426 | violations.push({ | ||
| 1427 | code: "missing_unresolved_ref_diagnostic", | ||
| 1428 | entityId: symbol.id, | ||
| 1429 | message: `Shape ${symbol.id} is unresolved but has no unresolved_ref diagnostic`, | ||
| 1430 | }); | ||
| 1431 | } | ||
| 1432 | } | ||
| 1433 | |||
| 1434 | if (symbol.synthetic && symbol.resourceId && !catalog.resources[symbol.resourceId]) { | ||
| 1435 | violations.push({ | ||
| 1436 | code: "missing_resource_context", | ||
| 1437 | entityId: symbol.id, | ||
| 1438 | message: `Synthetic shape ${symbol.id} references missing resource ${symbol.resourceId}`, | ||
| 1439 | }); | ||
| 1440 | } | ||
| 1441 | } | ||
| 1442 | } | ||
| 1443 | |||
| 1444 | for (const collection of [ | ||
| 1445 | ...Object.values(catalog.resources), | ||
| 1446 | ...Object.values(catalog.scopes), | ||
| 1447 | ...Object.values(catalog.capabilities), | ||
| 1448 | ...Object.values(catalog.executables), | ||
| 1449 | ...Object.values(catalog.responseSets), | ||
| 1450 | ]) { | ||
| 1451 | if ("provenance" in collection && collection.provenance.length === 0) { | ||
| 1452 | violations.push({ | ||
| 1453 | code: "missing_entity_provenance", | ||
| 1454 | entityId: "id" in collection ? collection.id : undefined, | ||
| 1455 | message: `Entity ${"id" in collection ? collection.id : "unknown"} is missing provenance`, | ||
| 1456 | }); | ||
| 1457 | } | ||
| 1458 | } | ||
| 1459 | |||
| 1460 | for (const capability of Object.values(catalog.capabilities)) { | ||
| 1461 | if (!catalog.scopes[capability.serviceScopeId]) { | ||
| 1462 | violations.push({ | ||
| 1463 | code: "missing_service_scope", | ||
| 1464 | entityId: capability.id, | ||
| 1465 | message: `Capability ${capability.id} references missing scope ${capability.serviceScopeId}`, | ||
| 1466 | }); | ||
| 1467 | } | ||
| 1468 | |||
| 1469 | if ( | ||
| 1470 | capability.preferredExecutableId | ||
| 1471 | && !capability.executableIds.includes(capability.preferredExecutableId) | ||
| 1472 | ) { | ||
| 1473 | violations.push({ | ||
| 1474 | code: "invalid_preferred_executable", | ||
| 1475 | entityId: capability.id, | ||
| 1476 | message: `Capability ${capability.id} preferred executable is not in executableIds`, | ||
| 1477 | }); | ||
| 1478 | } | ||
| 1479 | |||
| 1480 | for (const executableId of capability.executableIds) { | ||
| 1481 | if (!catalog.executables[executableId]) { | ||
| 1482 | violations.push({ | ||
| 1483 | code: "missing_executable", | ||
| 1484 | entityId: capability.id, | ||
| 1485 | message: `Capability ${capability.id} references missing executable ${executableId}`, | ||
| 1486 | }); | ||
| 1487 | } | ||
| 1488 | } | ||
| 1489 | } | ||
| 1490 | |||
| 1491 | for (const executable of Object.values(catalog.executables)) { | ||
| 1492 | if (!catalog.capabilities[executable.capabilityId]) { | ||
| 1493 | violations.push({ | ||
| 1494 | code: "missing_executable", | ||
| 1495 | entityId: executable.id, | ||
| 1496 | message: `Executable ${executable.id} references missing capability ${executable.capabilityId}`, | ||
| 1497 | }); | ||
| 1498 | } | ||
| 1499 | |||
| 1500 | if (!catalog.scopes[executable.scopeId]) { | ||
| 1501 | violations.push({ | ||
| 1502 | code: "missing_scope", | ||
| 1503 | entityId: executable.id, | ||
| 1504 | message: `Executable ${executable.id} references missing scope ${executable.scopeId}`, | ||
| 1505 | }); | ||
| 1506 | } | ||
| 1507 | |||
| 1508 | if (!catalog.responseSets[executable.responseSetId]) { | ||
| 1509 | violations.push({ | ||
| 1510 | code: "missing_response_set", | ||
| 1511 | entityId: executable.id, | ||
| 1512 | message: `Executable ${executable.id} references missing response set ${executable.responseSetId}`, | ||
| 1513 | }); | ||
| 1514 | } | ||
| 1515 | } | ||
| 1516 | |||
| 1517 | return violations; | ||
| 1518 | }; | ||
| 1519 | |||
| 1520 | export const projectCatalogForAgentSdk = (input: { | ||
| 1521 | catalog: CatalogV1; | ||
| 1522 | }): ProjectedCatalog => { | ||
| 1523 | const workingCatalog = cloneCatalog(input.catalog); | ||
| 1524 | const toolDescriptors = {} as Record<CapabilityId, ToolDescriptor>; | ||
| 1525 | const searchDocs = {} as Record<CapabilityId, ToolSearchDoc>; | ||
| 1526 | const capabilityViews = {} as Record<CapabilityId, CapabilitySummaryView>; | ||
| 1527 | |||
| 1528 | const capabilities = Object.values(workingCatalog.capabilities) | ||
| 1529 | .sort((left, right) => left.id.localeCompare(right.id)); | ||
| 1530 | |||
| 1531 | for (const capability of capabilities) { | ||
| 1532 | const descriptor = projectCapability(workingCatalog, capability); | ||
| 1533 | toolDescriptors[capability.id] = descriptor; | ||
| 1534 | searchDocs[capability.id] = searchDoc(workingCatalog, capability); | ||
| 1535 | capabilityViews[capability.id] = summaryView(workingCatalog, capability, descriptor); | ||
| 1536 | } | ||
| 1537 | |||
| 1538 | return { | ||
| 1539 | catalog: workingCatalog, | ||
| 1540 | toolDescriptors, | ||
| 1541 | searchDocs, | ||
| 1542 | capabilityViews, | ||
| 1543 | }; | ||
| 1544 | }; | ||
| 1545 | |||
| 1546 | export const projectSymbolShallowView = ( | ||
| 1547 | catalog: CatalogV1, | ||
| 1548 | symbolId: SymbolId, | ||
| 1549 | ): SymbolShallowView => { | ||
| 1550 | const symbol = catalog.symbols[symbolId]; | ||
| 1551 | if (!symbol) { | ||
| 1552 | throw new Error(`Unknown symbol ${symbolId}`); | ||
| 1553 | } | ||
| 1554 | |||
| 1555 | return { | ||
| 1556 | symbolId, | ||
| 1557 | kind: symbol.kind, | ||
| 1558 | ...(symbolTitle(symbol) ? { title: symbolTitle(symbol) } : {}), | ||
| 1559 | ...(symbolSummary(symbol) ? { summary: symbolSummary(symbol) } : {}), | ||
| 1560 | edges: symbolEdges(symbol), | ||
| 1561 | }; | ||
| 1562 | }; | ||
| 1563 | |||
| 1564 | export const projectSymbolExpandedView = ( | ||
| 1565 | catalog: CatalogV1, | ||
| 1566 | symbolId: SymbolId, | ||
| 1567 | ): SymbolExpandedView => { | ||
| 1568 | const symbol = catalog.symbols[symbolId]; | ||
| 1569 | if (!symbol) { | ||
| 1570 | throw new Error(`Unknown symbol ${symbolId}`); | ||
| 1571 | } | ||
| 1572 | |||
| 1573 | return { | ||
| 1574 | symbolId, | ||
| 1575 | symbol, | ||
| 1576 | ...(symbol.diagnosticIds | ||
| 1577 | ? { | ||
| 1578 | diagnostics: symbol.diagnosticIds | ||
| 1579 | .map((diagnosticId) => catalog.diagnostics[diagnosticId]) | ||
| 1580 | .filter((diagnostic): diagnostic is ImportDiagnostic => diagnostic !== undefined), | ||
| 1581 | } | ||
| 1582 | : {}), | ||
| 1583 | }; | ||
| 1584 | }; | ||
| 1585 | |||
| 1586 | export const buildInvocationPlan = (input: { | ||
| 1587 | catalog: CatalogV1; | ||
| 1588 | capabilityId: CapabilityId; | ||
| 1589 | executableId?: ExecutableId; | ||
| 1590 | connectionBindingId: string; | ||
| 1591 | resolvedEndpoint?: string; | ||
| 1592 | resolvedAuth?: unknown; | ||
| 1593 | resolvedInteraction?: Partial<InvocationPlan["resolvedInteraction"]>; | ||
| 1594 | requestPlan: unknown; | ||
| 1595 | }): InvocationPlan => { | ||
| 1596 | const capability = input.catalog.capabilities[input.capabilityId]; | ||
| 1597 | if (!capability) { | ||
| 1598 | throw new Error(`Unknown capability ${input.capabilityId}`); | ||
| 1599 | } | ||
| 1600 | |||
| 1601 | const executable = input.executableId | ||
| 1602 | ? input.catalog.executables[input.executableId] | ||
| 1603 | : chooseExecutable(input.catalog, capability); | ||
| 1604 | if (!executable) { | ||
| 1605 | throw new Error(`Unknown executable for capability ${input.capabilityId}`); | ||
| 1606 | } | ||
| 1607 | |||
| 1608 | const scopes = scopeChain(input.catalog, executable.scopeId).map((scope) => scope.id); | ||
| 1609 | |||
| 1610 | return { | ||
| 1611 | capabilityId: capability.id, | ||
| 1612 | executableId: executable.id, | ||
| 1613 | connectionBindingId: input.connectionBindingId, | ||
| 1614 | resolvedScopeChain: scopes, | ||
| 1615 | ...(input.resolvedEndpoint ? { resolvedEndpoint: input.resolvedEndpoint } : {}), | ||
| 1616 | ...(input.resolvedAuth !== undefined ? { resolvedAuth: input.resolvedAuth } : {}), | ||
| 1617 | resolvedInteraction: { | ||
| 1618 | requiresApproval: input.resolvedInteraction?.requiresApproval | ||
| 1619 | ?? capability.interaction.approval.mayRequire, | ||
| 1620 | mayPauseForElicitation: input.resolvedInteraction?.mayPauseForElicitation | ||
| 1621 | ?? capability.interaction.elicitation.mayRequest, | ||
| 1622 | resumable: input.resolvedInteraction?.resumable | ||
| 1623 | ?? capability.interaction.resume.supported, | ||
| 1624 | }, | ||
| 1625 | requestPlan: input.requestPlan, | ||
| 1626 | provenance: capability.provenance, | ||
| 1627 | }; | ||
| 1628 | }; | ||
| @@ -0,0 +1,58 @@ | |||
| 1 | import { Schema } from "effect"; | ||
| 2 | |||
| 3 | export const DocumentIdSchema = Schema.String.pipe(Schema.brand("DocumentId")); | ||
| 4 | export const ResourceIdSchema = Schema.String.pipe(Schema.brand("ResourceId")); | ||
| 5 | export const ScopeIdSchema = Schema.String.pipe(Schema.brand("ScopeId")); | ||
| 6 | export const CapabilityIdSchema = Schema.String.pipe(Schema.brand("CapabilityId")); | ||
| 7 | export const ExecutableIdSchema = Schema.String.pipe(Schema.brand("ExecutableId")); | ||
| 8 | export const ResponseSetIdSchema = Schema.String.pipe(Schema.brand("ResponseSetId")); | ||
| 9 | export const DiagnosticIdSchema = Schema.String.pipe(Schema.brand("DiagnosticId")); | ||
| 10 | |||
| 11 | export const ShapeSymbolIdSchema = Schema.String.pipe(Schema.brand("ShapeSymbolId")); | ||
| 12 | export const ParameterSymbolIdSchema = Schema.String.pipe(Schema.brand("ParameterSymbolId")); | ||
| 13 | export const RequestBodySymbolIdSchema = Schema.String.pipe( | ||
| 14 | Schema.brand("RequestBodySymbolId"), | ||
| 15 | ); | ||
| 16 | export const ResponseSymbolIdSchema = Schema.String.pipe( | ||
| 17 | Schema.brand("ResponseSymbolId"), | ||
| 18 | ); | ||
| 19 | export const HeaderSymbolIdSchema = Schema.String.pipe(Schema.brand("HeaderSymbolId")); | ||
| 20 | export const ExampleSymbolIdSchema = Schema.String.pipe(Schema.brand("ExampleSymbolId")); | ||
| 21 | export const SecuritySchemeSymbolIdSchema = Schema.String.pipe( | ||
| 22 | Schema.brand("SecuritySchemeSymbolId"), | ||
| 23 | ); | ||
| 24 | |||
| 25 | export type DocumentId = typeof DocumentIdSchema.Type; | ||
| 26 | export type ResourceId = typeof ResourceIdSchema.Type; | ||
| 27 | export type ScopeId = typeof ScopeIdSchema.Type; | ||
| 28 | export type CapabilityId = typeof CapabilityIdSchema.Type; | ||
| 29 | export type ExecutableId = typeof ExecutableIdSchema.Type; | ||
| 30 | export type ResponseSetId = typeof ResponseSetIdSchema.Type; | ||
| 31 | export type DiagnosticId = typeof DiagnosticIdSchema.Type; | ||
| 32 | |||
| 33 | export type ShapeSymbolId = typeof ShapeSymbolIdSchema.Type; | ||
| 34 | export type ParameterSymbolId = typeof ParameterSymbolIdSchema.Type; | ||
| 35 | export type RequestBodySymbolId = typeof RequestBodySymbolIdSchema.Type; | ||
| 36 | export type ResponseSymbolId = typeof ResponseSymbolIdSchema.Type; | ||
| 37 | export type HeaderSymbolId = typeof HeaderSymbolIdSchema.Type; | ||
| 38 | export type ExampleSymbolId = typeof ExampleSymbolIdSchema.Type; | ||
| 39 | export type SecuritySchemeSymbolId = typeof SecuritySchemeSymbolIdSchema.Type; | ||
| 40 | |||
| 41 | export type SymbolId = | ||
| 42 | | ShapeSymbolId | ||
| 43 | | ParameterSymbolId | ||
| 44 | | RequestBodySymbolId | ||
| 45 | | ResponseSymbolId | ||
| 46 | | HeaderSymbolId | ||
| 47 | | ExampleSymbolId | ||
| 48 | | SecuritySchemeSymbolId; | ||
| 49 | |||
| 50 | export const SymbolIdSchema = Schema.Union( | ||
| 51 | ShapeSymbolIdSchema, | ||
| 52 | ParameterSymbolIdSchema, | ||
| 53 | RequestBodySymbolIdSchema, | ||
| 54 | ResponseSymbolIdSchema, | ||
| 55 | HeaderSymbolIdSchema, | ||
| 56 | ExampleSymbolIdSchema, | ||
| 57 | SecuritySchemeSymbolIdSchema, | ||
| 58 | ); | ||
| @@ -0,0 +1,3 @@ | |||
| 1 | export * from "./ids"; | ||
| 2 | export * from "./model"; | ||
| 3 | export * from "./catalog"; | ||
| @@ -0,0 +1,962 @@ | |||
| 1 | import { Schema } from "effect"; | ||
| 2 | |||
| 3 | import { | ||
| 4 | CapabilityIdSchema, | ||
| 5 | type CapabilityId, | ||
| 6 | DiagnosticIdSchema, | ||
| 7 | type DiagnosticId, | ||
| 8 | DocumentIdSchema, | ||
| 9 | type DocumentId, | ||
| 10 | ExecutableIdSchema, | ||
| 11 | type ExecutableId, | ||
| 12 | ExampleSymbolIdSchema, | ||
| 13 | type ExampleSymbolId, | ||
| 14 | HeaderSymbolIdSchema, | ||
| 15 | type HeaderSymbolId, | ||
| 16 | ParameterSymbolIdSchema, | ||
| 17 | type ParameterSymbolId, | ||
| 18 | RequestBodySymbolIdSchema, | ||
| 19 | type RequestBodySymbolId, | ||
| 20 | ResourceIdSchema, | ||
| 21 | type ResourceId, | ||
| 22 | ResponseSetIdSchema, | ||
| 23 | type ResponseSetId, | ||
| 24 | ResponseSymbolIdSchema, | ||
| 25 | type ResponseSymbolId, | ||
| 26 | ScopeIdSchema, | ||
| 27 | type ScopeId, | ||
| 28 | SecuritySchemeSymbolIdSchema, | ||
| 29 | type SecuritySchemeSymbolId, | ||
| 30 | ShapeSymbolIdSchema, | ||
| 31 | type ShapeSymbolId, | ||
| 32 | SymbolIdSchema, | ||
| 33 | type SymbolId, | ||
| 34 | } from "./ids"; | ||
| 35 | |||
| 36 | export const SourceKindSchema = Schema.Literal( | ||
| 37 | "openapi", | ||
| 38 | "graphql-schema", | ||
| 39 | "google-discovery", | ||
| 40 | "mcp", | ||
| 41 | "custom", | ||
| 42 | ); | ||
| 43 | |||
| 44 | export const ScopeKindSchema = Schema.Literal( | ||
| 45 | "service", | ||
| 46 | "document", | ||
| 47 | "resource", | ||
| 48 | "pathItem", | ||
| 49 | "operation", | ||
| 50 | "folder", | ||
| 51 | ); | ||
| 52 | |||
| 53 | export const EffectKindSchema = Schema.Literal( | ||
| 54 | "read", | ||
| 55 | "write", | ||
| 56 | "delete", | ||
| 57 | "action", | ||
| 58 | "subscribe", | ||
| 59 | ); | ||
| 60 | |||
| 61 | export const ParameterLocationSchema = Schema.Literal( | ||
| 62 | "path", | ||
| 63 | "query", | ||
| 64 | "header", | ||
| 65 | "cookie", | ||
| 66 | ); | ||
| 67 | |||
| 68 | export const ResponseTraitSchema = Schema.Literal( | ||
| 69 | "success", | ||
| 70 | "redirect", | ||
| 71 | "stream", | ||
| 72 | "download", | ||
| 73 | "upload", | ||
| 74 | "longRunning", | ||
| 75 | ); | ||
| 76 | |||
| 77 | export const SecuritySchemeTypeSchema = Schema.Literal( | ||
| 78 | "oauth2", | ||
| 79 | "http", | ||
| 80 | "apiKey", | ||
| 81 | "basic", | ||
| 82 | "bearer", | ||
| 83 | "custom", | ||
| 84 | ); | ||
| 85 | |||
| 86 | export const SelectionModeSchema = Schema.Literal( | ||
| 87 | "caller", | ||
| 88 | "fixed", | ||
| 89 | "persisted", | ||
| 90 | ); | ||
| 91 | |||
| 92 | export const StatusRangeSchema = Schema.Literal( | ||
| 93 | "1XX", | ||
| 94 | "2XX", | ||
| 95 | "3XX", | ||
| 96 | "4XX", | ||
| 97 | "5XX", | ||
| 98 | ); | ||
| 99 | |||
| 100 | export const PaginationHintKindSchema = Schema.Literal( | ||
| 101 | "cursor", | ||
| 102 | "offset", | ||
| 103 | "token", | ||
| 104 | "unknown", | ||
| 105 | ); | ||
| 106 | |||
| 107 | export const DiagnosticLevelSchema = Schema.Literal( | ||
| 108 | "info", | ||
| 109 | "warning", | ||
| 110 | "error", | ||
| 111 | ); | ||
| 112 | |||
| 113 | export const DiagnosticCodeSchema = Schema.Literal( | ||
| 114 | "external_ref_bundled", | ||
| 115 | "relative_ref_rebased", | ||
| 116 | "schema_hoisted", | ||
| 117 | "selection_shape_synthesized", | ||
| 118 | "opaque_hook_imported", | ||
| 119 | "discriminator_lost", | ||
| 120 | "multi_response_union_synthesized", | ||
| 121 | "unsupported_link_preserved_native", | ||
| 122 | "unsupported_callback_preserved_native", | ||
| 123 | "unresolved_ref", | ||
| 124 | "merge_conflict_preserved_first", | ||
| 125 | "projection_call_shape_synthesized", | ||
| 126 | "projection_result_shape_synthesized", | ||
| 127 | "projection_collision_grouped_fields", | ||
| 128 | "synthetic_resource_context_created", | ||
| 129 | "selection_shape_missing", | ||
| 130 | ); | ||
| 131 | |||
| 132 | export const NativeEncodingSchema = Schema.Literal( | ||
| 133 | "json", | ||
| 134 | "yaml", | ||
| 135 | "graphql", | ||
| 136 | "text", | ||
| 137 | "unknown", | ||
| 138 | ); | ||
| 139 | |||
| 140 | export const ProvenanceRelationSchema = Schema.Literal( | ||
| 141 | "declared", | ||
| 142 | "hoisted", | ||
| 143 | "derived", | ||
| 144 | "merged", | ||
| 145 | "projected", | ||
| 146 | ); | ||
| 147 | |||
| 148 | export const DocumentationBlockSchema = Schema.Struct({ | ||
| 149 | summary: Schema.optional(Schema.String), | ||
| 150 | description: Schema.optional(Schema.String), | ||
| 151 | externalDocsUrl: Schema.optional(Schema.String), | ||
| 152 | }); | ||
| 153 | |||
| 154 | export const ProvenanceRefSchema = Schema.Struct({ | ||
| 155 | relation: ProvenanceRelationSchema, | ||
| 156 | documentId: DocumentIdSchema, | ||
| 157 | resourceId: Schema.optional(ResourceIdSchema), | ||
| 158 | pointer: Schema.optional(Schema.String), | ||
| 159 | line: Schema.optional(Schema.Number), | ||
| 160 | column: Schema.optional(Schema.Number), | ||
| 161 | }); | ||
| 162 | |||
| 163 | export const NativeBlobSchema = Schema.Struct({ | ||
| 164 | sourceKind: SourceKindSchema, | ||
| 165 | kind: Schema.String, | ||
| 166 | pointer: Schema.optional(Schema.String), | ||
| 167 | encoding: Schema.optional(NativeEncodingSchema), | ||
| 168 | summary: Schema.optional(Schema.String), | ||
| 169 | value: Schema.optional(Schema.Unknown), | ||
| 170 | }); | ||
| 171 | |||
| 172 | export const EntityBaseSchema = Schema.Struct({ | ||
| 173 | synthetic: Schema.Boolean, | ||
| 174 | provenance: Schema.Array(ProvenanceRefSchema), | ||
| 175 | diagnosticIds: Schema.optional(Schema.Array(DiagnosticIdSchema)), | ||
| 176 | native: Schema.optional(Schema.Array(NativeBlobSchema)), | ||
| 177 | }); | ||
| 178 | |||
| 179 | export const ImportMetadataSchema = Schema.Struct({ | ||
| 180 | sourceKind: SourceKindSchema, | ||
| 181 | adapterKey: Schema.String, | ||
| 182 | importerVersion: Schema.String, | ||
| 183 | importedAt: Schema.String, | ||
| 184 | sourceConfigHash: Schema.String, | ||
| 185 | }); | ||
| 186 | |||
| 187 | export const SourceDocumentSchema = Schema.Struct({ | ||
| 188 | id: DocumentIdSchema, | ||
| 189 | kind: SourceKindSchema, | ||
| 190 | title: Schema.optional(Schema.String), | ||
| 191 | versionHint: Schema.optional(Schema.String), | ||
| 192 | fetchedAt: Schema.String, | ||
| 193 | rawRef: Schema.String, | ||
| 194 | entryUri: Schema.optional(Schema.String), | ||
| 195 | native: Schema.optional(Schema.Array(NativeBlobSchema)), | ||
| 196 | }); | ||
| 197 | |||
| 198 | export const FieldSpecSchema = Schema.Struct({ | ||
| 199 | shapeId: ShapeSymbolIdSchema, | ||
| 200 | docs: Schema.optional(DocumentationBlockSchema), | ||
| 201 | deprecated: Schema.optional(Schema.Boolean), | ||
| 202 | exampleIds: Schema.optional(Schema.Array(ExampleSymbolIdSchema)), | ||
| 203 | }); | ||
| 204 | |||
| 205 | export const DiscriminatorSpecSchema = Schema.Struct({ | ||
| 206 | propertyName: Schema.String, | ||
| 207 | mapping: Schema.optional( | ||
| 208 | Schema.Record({ | ||
| 209 | key: Schema.String, | ||
| 210 | value: ShapeSymbolIdSchema, | ||
| 211 | }), | ||
| 212 | ), | ||
| 213 | }); | ||
| 214 | |||
| 215 | export const ScalarShapeSchema = Schema.Struct({ | ||
| 216 | type: Schema.Literal("scalar"), | ||
| 217 | scalar: Schema.Literal("string", "number", "integer", "boolean", "null", "bytes"), | ||
| 218 | format: Schema.optional(Schema.String), | ||
| 219 | constraints: Schema.optional( | ||
| 220 | Schema.Record({ | ||
| 221 | key: Schema.String, | ||
| 222 | value: Schema.Unknown, | ||
| 223 | }), | ||
| 224 | ), | ||
| 225 | }); | ||
| 226 | |||
| 227 | export const UnknownShapeSchema = Schema.Struct({ | ||
| 228 | type: Schema.Literal("unknown"), | ||
| 229 | reason: Schema.optional(Schema.String), | ||
| 230 | }); | ||
| 231 | |||
| 232 | export const ConstShapeSchema = Schema.Struct({ | ||
| 233 | type: Schema.Literal("const"), | ||
| 234 | value: Schema.Unknown, | ||
| 235 | }); | ||
| 236 | |||
| 237 | export const EnumShapeSchema = Schema.Struct({ | ||
| 238 | type: Schema.Literal("enum"), | ||
| 239 | values: Schema.Array(Schema.Unknown), | ||
| 240 | }); | ||
| 241 | |||
| 242 | export const ObjectShapeSchema = Schema.Struct({ | ||
| 243 | type: Schema.Literal("object"), | ||
| 244 | fields: Schema.Record({ | ||
| 245 | key: Schema.String, | ||
| 246 | value: FieldSpecSchema, | ||
| 247 | }), | ||
| 248 | required: Schema.optional(Schema.Array(Schema.String)), | ||
| 249 | additionalProperties: Schema.optional( | ||
| 250 | Schema.Union(Schema.Boolean, ShapeSymbolIdSchema), | ||
| 251 | ), | ||
| 252 | patternProperties: Schema.optional( | ||
| 253 | Schema.Record({ | ||
| 254 | key: Schema.String, | ||
| 255 | value: ShapeSymbolIdSchema, | ||
| 256 | }), | ||
| 257 | ), | ||
| 258 | }); | ||
| 259 | |||
| 260 | export const ArrayShapeSchema = Schema.Struct({ | ||
| 261 | type: Schema.Literal("array"), | ||
| 262 | itemShapeId: ShapeSymbolIdSchema, | ||
| 263 | minItems: Schema.optional(Schema.Number), | ||
| 264 | maxItems: Schema.optional(Schema.Number), | ||
| 265 | }); | ||
| 266 | |||
| 267 | export const TupleShapeSchema = Schema.Struct({ | ||
| 268 | type: Schema.Literal("tuple"), | ||
| 269 | itemShapeIds: Schema.Array(ShapeSymbolIdSchema), | ||
| 270 | additionalItems: Schema.optional( | ||
| 271 | Schema.Union(Schema.Boolean, ShapeSymbolIdSchema), | ||
| 272 | ), | ||
| 273 | }); | ||
| 274 | |||
| 275 | export const MapShapeSchema = Schema.Struct({ | ||
| 276 | type: Schema.Literal("map"), | ||
| 277 | valueShapeId: ShapeSymbolIdSchema, | ||
| 278 | }); | ||
| 279 | |||
| 280 | export const AllOfShapeSchema = Schema.Struct({ | ||
| 281 | type: Schema.Literal("allOf"), | ||
| 282 | items: Schema.Array(ShapeSymbolIdSchema), | ||
| 283 | }); | ||
| 284 | |||
| 285 | export const AnyOfShapeSchema = Schema.Struct({ | ||
| 286 | type: Schema.Literal("anyOf"), | ||
| 287 | items: Schema.Array(ShapeSymbolIdSchema), | ||
| 288 | }); | ||
| 289 | |||
| 290 | export const OneOfShapeSchema = Schema.Struct({ | ||
| 291 | type: Schema.Literal("oneOf"), | ||
| 292 | items: Schema.Array(ShapeSymbolIdSchema), | ||
| 293 | discriminator: Schema.optional(DiscriminatorSpecSchema), | ||
| 294 | }); | ||
| 295 | |||
| 296 | export const NullableShapeSchema = Schema.Struct({ | ||
| 297 | type: Schema.Literal("nullable"), | ||
| 298 | itemShapeId: ShapeSymbolIdSchema, | ||
| 299 | }); | ||
| 300 | |||
| 301 | export const RefShapeSchema = Schema.Struct({ | ||
| 302 | type: Schema.Literal("ref"), | ||
| 303 | target: ShapeSymbolIdSchema, | ||
| 304 | }); | ||
| 305 | |||
| 306 | export const NotShapeSchema = Schema.Struct({ | ||
| 307 | type: Schema.Literal("not"), | ||
| 308 | itemShapeId: ShapeSymbolIdSchema, | ||
| 309 | }); | ||
| 310 | |||
| 311 | export const ConditionalShapeSchema = Schema.Struct({ | ||
| 312 | type: Schema.Literal("conditional"), | ||
| 313 | ifShapeId: ShapeSymbolIdSchema, | ||
| 314 | thenShapeId: Schema.optional(ShapeSymbolIdSchema), | ||
| 315 | elseShapeId: Schema.optional(ShapeSymbolIdSchema), | ||
| 316 | }); | ||
| 317 | |||
| 318 | export const GraphQLInterfaceShapeSchema = Schema.Struct({ | ||
| 319 | type: Schema.Literal("graphqlInterface"), | ||
| 320 | fields: Schema.Record({ | ||
| 321 | key: Schema.String, | ||
| 322 | value: FieldSpecSchema, | ||
| 323 | }), | ||
| 324 | possibleTypeIds: Schema.Array(ShapeSymbolIdSchema), | ||
| 325 | }); | ||
| 326 | |||
| 327 | export const GraphQLUnionShapeSchema = Schema.Struct({ | ||
| 328 | type: Schema.Literal("graphqlUnion"), | ||
| 329 | memberTypeIds: Schema.Array(ShapeSymbolIdSchema), | ||
| 330 | }); | ||
| 331 | |||
| 332 | export const ShapeNodeSchema = Schema.Union( | ||
| 333 | UnknownShapeSchema, | ||
| 334 | ScalarShapeSchema, | ||
| 335 | ConstShapeSchema, | ||
| 336 | EnumShapeSchema, | ||
| 337 | ObjectShapeSchema, | ||
| 338 | ArrayShapeSchema, | ||
| 339 | TupleShapeSchema, | ||
| 340 | MapShapeSchema, | ||
| 341 | AllOfShapeSchema, | ||
| 342 | AnyOfShapeSchema, | ||
| 343 | OneOfShapeSchema, | ||
| 344 | NullableShapeSchema, | ||
| 345 | RefShapeSchema, | ||
| 346 | NotShapeSchema, | ||
| 347 | ConditionalShapeSchema, | ||
| 348 | GraphQLInterfaceShapeSchema, | ||
| 349 | GraphQLUnionShapeSchema, | ||
| 350 | ); | ||
| 351 | |||
| 352 | export const AnchorTargetSchema = Schema.Struct({ | ||
| 353 | shapeId: ShapeSymbolIdSchema, | ||
| 354 | pointer: Schema.optional(Schema.String), | ||
| 355 | }); | ||
| 356 | |||
| 357 | export const SchemaResourceSchema = Schema.extend( | ||
| 358 | Schema.Struct({ | ||
| 359 | id: ResourceIdSchema, | ||
| 360 | documentId: DocumentIdSchema, | ||
| 361 | canonicalUri: Schema.String, | ||
| 362 | baseUri: Schema.String, | ||
| 363 | dialectUri: Schema.optional(Schema.String), | ||
| 364 | rootShapeId: Schema.optional(ShapeSymbolIdSchema), | ||
| 365 | anchors: Schema.Record({ | ||
| 366 | key: Schema.String, | ||
| 367 | value: AnchorTargetSchema, | ||
| 368 | }), | ||
| 369 | dynamicAnchors: Schema.Record({ | ||
| 370 | key: Schema.String, | ||
| 371 | value: AnchorTargetSchema, | ||
| 372 | }), | ||
| 373 | }), | ||
| 374 | EntityBaseSchema, | ||
| 375 | ); | ||
| 376 | |||
| 377 | export const RequestPlacementHintSchema = Schema.Union( | ||
| 378 | Schema.Struct({ | ||
| 379 | location: Schema.Literal("header", "query", "cookie"), | ||
| 380 | name: Schema.String, | ||
| 381 | }), | ||
| 382 | Schema.Struct({ | ||
| 383 | location: Schema.Literal("body"), | ||
| 384 | path: Schema.String, | ||
| 385 | }), | ||
| 386 | ); | ||
| 387 | |||
| 388 | export const OAuthFlowSchema = Schema.Struct({ | ||
| 389 | authorizationUrl: Schema.optional(Schema.String), | ||
| 390 | tokenUrl: Schema.optional(Schema.String), | ||
| 391 | refreshUrl: Schema.optional(Schema.String), | ||
| 392 | scopes: Schema.optional( | ||
| 393 | Schema.Record({ | ||
| 394 | key: Schema.String, | ||
| 395 | value: Schema.String, | ||
| 396 | }), | ||
| 397 | ), | ||
| 398 | }); | ||
| 399 | |||
| 400 | export const SecurityPlacementSchema = Schema.Struct({ | ||
| 401 | in: Schema.optional(Schema.Literal("header", "query", "cookie")), | ||
| 402 | name: Schema.optional(Schema.String), | ||
| 403 | }); | ||
| 404 | |||
| 405 | export const SecuritySchemeSymbolSchema = Schema.extend( | ||
| 406 | Schema.Struct({ | ||
| 407 | id: SecuritySchemeSymbolIdSchema, | ||
| 408 | kind: Schema.Literal("securityScheme"), | ||
| 409 | schemeType: SecuritySchemeTypeSchema, | ||
| 410 | docs: Schema.optional(DocumentationBlockSchema), | ||
| 411 | placement: Schema.optional(SecurityPlacementSchema), | ||
| 412 | http: Schema.optional( | ||
| 413 | Schema.Struct({ | ||
| 414 | scheme: Schema.String, | ||
| 415 | bearerFormat: Schema.optional(Schema.String), | ||
| 416 | }), | ||
| 417 | ), | ||
| 418 | apiKey: Schema.optional( | ||
| 419 | Schema.Struct({ | ||
| 420 | in: Schema.Literal("header", "query", "cookie"), | ||
| 421 | name: Schema.String, | ||
| 422 | }), | ||
| 423 | ), | ||
| 424 | oauth: Schema.optional( | ||
| 425 | Schema.Struct({ | ||
| 426 | flows: Schema.optional( | ||
| 427 | Schema.Record({ | ||
| 428 | key: Schema.String, | ||
| 429 | value: OAuthFlowSchema, | ||
| 430 | }), | ||
| 431 | ), | ||
| 432 | scopes: Schema.optional( | ||
| 433 | Schema.Record({ | ||
| 434 | key: Schema.String, | ||
| 435 | value: Schema.String, | ||
| 436 | }), | ||
| 437 | ), | ||
| 438 | }), | ||
| 439 | ), | ||
| 440 | custom: Schema.optional( | ||
| 441 | Schema.Struct({ | ||
| 442 | placementHints: Schema.optional(Schema.Array(RequestPlacementHintSchema)), | ||
| 443 | }), | ||
| 444 | ), | ||
| 445 | }), | ||
| 446 | EntityBaseSchema, | ||
| 447 | ); | ||
| 448 | |||
| 449 | export const EncodingSpecSchema = Schema.Struct({ | ||
| 450 | contentType: Schema.optional(Schema.String), | ||
| 451 | style: Schema.optional(Schema.String), | ||
| 452 | explode: Schema.optional(Schema.Boolean), | ||
| 453 | allowReserved: Schema.optional(Schema.Boolean), | ||
| 454 | headers: Schema.optional(Schema.Array(HeaderSymbolIdSchema)), | ||
| 455 | }); | ||
| 456 | |||
| 457 | export const ContentSpecSchema = Schema.Struct({ | ||
| 458 | mediaType: Schema.String, | ||
| 459 | shapeId: Schema.optional(ShapeSymbolIdSchema), | ||
| 460 | exampleIds: Schema.optional(Schema.Array(ExampleSymbolIdSchema)), | ||
| 461 | encoding: Schema.optional( | ||
| 462 | Schema.Record({ | ||
| 463 | key: Schema.String, | ||
| 464 | value: EncodingSpecSchema, | ||
| 465 | }), | ||
| 466 | ), | ||
| 467 | }); | ||
| 468 | |||
| 469 | export const ShapeSymbolSchema = Schema.extend( | ||
| 470 | Schema.Struct({ | ||
| 471 | id: ShapeSymbolIdSchema, | ||
| 472 | kind: Schema.Literal("shape"), | ||
| 473 | resourceId: Schema.optional(ResourceIdSchema), | ||
| 474 | title: Schema.optional(Schema.String), | ||
| 475 | docs: Schema.optional(DocumentationBlockSchema), | ||
| 476 | deprecated: Schema.optional(Schema.Boolean), | ||
| 477 | node: ShapeNodeSchema, | ||
| 478 | }), | ||
| 479 | EntityBaseSchema, | ||
| 480 | ); | ||
| 481 | |||
| 482 | export const ParameterSymbolSchema = Schema.extend( | ||
| 483 | Schema.Struct({ | ||
| 484 | id: ParameterSymbolIdSchema, | ||
| 485 | kind: Schema.Literal("parameter"), | ||
| 486 | name: Schema.String, | ||
| 487 | location: ParameterLocationSchema, | ||
| 488 | required: Schema.optional(Schema.Boolean), | ||
| 489 | docs: Schema.optional(DocumentationBlockSchema), | ||
| 490 | deprecated: Schema.optional(Schema.Boolean), | ||
| 491 | exampleIds: Schema.optional(Schema.Array(ExampleSymbolIdSchema)), | ||
| 492 | schemaShapeId: Schema.optional(ShapeSymbolIdSchema), | ||
| 493 | content: Schema.optional(Schema.Array(ContentSpecSchema)), | ||
| 494 | style: Schema.optional(Schema.String), | ||
| 495 | explode: Schema.optional(Schema.Boolean), | ||
| 496 | allowReserved: Schema.optional(Schema.Boolean), | ||
| 497 | }), | ||
| 498 | EntityBaseSchema, | ||
| 499 | ); | ||
| 500 | |||
| 501 | export const HeaderSymbolSchema = Schema.extend( | ||
| 502 | Schema.Struct({ | ||
| 503 | id: HeaderSymbolIdSchema, | ||
| 504 | kind: Schema.Literal("header"), | ||
| 505 | name: Schema.String, | ||
| 506 | docs: Schema.optional(DocumentationBlockSchema), | ||
| 507 | deprecated: Schema.optional(Schema.Boolean), | ||
| 508 | exampleIds: Schema.optional(Schema.Array(ExampleSymbolIdSchema)), | ||
| 509 | schemaShapeId: Schema.optional(ShapeSymbolIdSchema), | ||
| 510 | content: Schema.optional(Schema.Array(ContentSpecSchema)), | ||
| 511 | style: Schema.optional(Schema.String), | ||
| 512 | explode: Schema.optional(Schema.Boolean), | ||
| 513 | }), | ||
| 514 | EntityBaseSchema, | ||
| 515 | ); | ||
| 516 | |||
| 517 | export const RequestBodySymbolSchema = Schema.extend( | ||
| 518 | Schema.Struct({ | ||
| 519 | id: RequestBodySymbolIdSchema, | ||
| 520 | kind: Schema.Literal("requestBody"), | ||
| 521 | docs: Schema.optional(DocumentationBlockSchema), | ||
| 522 | required: Schema.optional(Schema.Boolean), | ||
| 523 | contents: Schema.Array(ContentSpecSchema), | ||
| 524 | }), | ||
| 525 | EntityBaseSchema, | ||
| 526 | ); | ||
| 527 | |||
| 528 | export const ResponseSymbolSchema = Schema.extend( | ||
| 529 | Schema.Struct({ | ||
| 530 | id: ResponseSymbolIdSchema, | ||
| 531 | kind: Schema.Literal("response"), | ||
| 532 | docs: Schema.optional(DocumentationBlockSchema), | ||
| 533 | headerIds: Schema.optional(Schema.Array(HeaderSymbolIdSchema)), | ||
| 534 | contents: Schema.optional(Schema.Array(ContentSpecSchema)), | ||
| 535 | }), | ||
| 536 | EntityBaseSchema, | ||
| 537 | ); | ||
| 538 | |||
| 539 | export const ExampleSymbolSchema = Schema.extend( | ||
| 540 | Schema.Struct({ | ||
| 541 | id: ExampleSymbolIdSchema, | ||
| 542 | kind: Schema.Literal("example"), | ||
| 543 | name: Schema.optional(Schema.String), | ||
| 544 | docs: Schema.optional(DocumentationBlockSchema), | ||
| 545 | exampleKind: Schema.Literal("value", "call"), | ||
| 546 | value: Schema.optional(Schema.Unknown), | ||
| 547 | externalValue: Schema.optional(Schema.String), | ||
| 548 | call: Schema.optional( | ||
| 549 | Schema.Struct({ | ||
| 550 | args: Schema.Record({ | ||
| 551 | key: Schema.String, | ||
| 552 | value: Schema.Unknown, | ||
| 553 | }), | ||
| 554 | result: Schema.optional(Schema.Unknown), | ||
| 555 | }), | ||
| 556 | ), | ||
| 557 | }), | ||
| 558 | EntityBaseSchema, | ||
| 559 | ); | ||
| 560 | |||
| 561 | export const SymbolSchema = Schema.Union( | ||
| 562 | ShapeSymbolSchema, | ||
| 563 | ParameterSymbolSchema, | ||
| 564 | RequestBodySymbolSchema, | ||
| 565 | ResponseSymbolSchema, | ||
| 566 | HeaderSymbolSchema, | ||
| 567 | ExampleSymbolSchema, | ||
| 568 | SecuritySchemeSymbolSchema, | ||
| 569 | ); | ||
| 570 | |||
| 571 | export type AuthRequirement = | ||
| 572 | | { | ||
| 573 | readonly kind: "none"; | ||
| 574 | } | ||
| 575 | | { | ||
| 576 | readonly kind: "scheme"; | ||
| 577 | readonly schemeId: SecuritySchemeSymbolId; | ||
| 578 | readonly scopes?: readonly string[]; | ||
| 579 | } | ||
| 580 | | { | ||
| 581 | readonly kind: "allOf"; | ||
| 582 | readonly items: readonly AuthRequirement[]; | ||
| 583 | } | ||
| 584 | | { | ||
| 585 | readonly kind: "anyOf"; | ||
| 586 | readonly items: readonly AuthRequirement[]; | ||
| 587 | }; | ||
| 588 | |||
| 589 | export const AuthRequirementSchema = Schema.suspend(() => | ||
| 590 | Schema.Union( | ||
| 591 | Schema.Struct({ | ||
| 592 | kind: Schema.Literal("none"), | ||
| 593 | }), | ||
| 594 | Schema.Struct({ | ||
| 595 | kind: Schema.Literal("scheme"), | ||
| 596 | schemeId: SecuritySchemeSymbolIdSchema, | ||
| 597 | scopes: Schema.optional(Schema.Array(Schema.String)), | ||
| 598 | }), | ||
| 599 | Schema.Struct({ | ||
| 600 | kind: Schema.Literal("allOf"), | ||
| 601 | items: Schema.Array(AuthRequirementSchema), | ||
| 602 | }), | ||
| 603 | Schema.Struct({ | ||
| 604 | kind: Schema.Literal("anyOf"), | ||
| 605 | items: Schema.Array(AuthRequirementSchema), | ||
| 606 | }), | ||
| 607 | ) | ||
| 608 | ) as Schema.Schema<AuthRequirement, AuthRequirement, never>; | ||
| 609 | |||
| 610 | export const ServerSpecSchema = Schema.Struct({ | ||
| 611 | url: Schema.String, | ||
| 612 | description: Schema.optional(Schema.String), | ||
| 613 | variables: Schema.optional( | ||
| 614 | Schema.Record({ | ||
| 615 | key: Schema.String, | ||
| 616 | value: Schema.String, | ||
| 617 | }), | ||
| 618 | ), | ||
| 619 | }); | ||
| 620 | |||
| 621 | export const ScopeDefaultsSchema = Schema.Struct({ | ||
| 622 | servers: Schema.optional(Schema.Array(ServerSpecSchema)), | ||
| 623 | auth: Schema.optional(AuthRequirementSchema), | ||
| 624 | parameterIds: Schema.optional(Schema.Array(ParameterSymbolIdSchema)), | ||
| 625 | headerIds: Schema.optional(Schema.Array(HeaderSymbolIdSchema)), | ||
| 626 | variables: Schema.optional( | ||
| 627 | Schema.Record({ | ||
| 628 | key: Schema.String, | ||
| 629 | value: Schema.String, | ||
| 630 | }), | ||
| 631 | ), | ||
| 632 | }); | ||
| 633 | |||
| 634 | export const ScopeSchema = Schema.extend( | ||
| 635 | Schema.Struct({ | ||
| 636 | id: ScopeIdSchema, | ||
| 637 | kind: ScopeKindSchema, | ||
| 638 | parentId: Schema.optional(ScopeIdSchema), | ||
| 639 | name: Schema.optional(Schema.String), | ||
| 640 | namespace: Schema.optional(Schema.String), | ||
| 641 | docs: Schema.optional(DocumentationBlockSchema), | ||
| 642 | defaults: Schema.optional(ScopeDefaultsSchema), | ||
| 643 | }), | ||
| 644 | EntityBaseSchema, | ||
| 645 | ); | ||
| 646 | |||
| 647 | export const InteractionSpecSchema = Schema.Struct({ | ||
| 648 | approval: Schema.Struct({ | ||
| 649 | mayRequire: Schema.Boolean, | ||
| 650 | reasons: Schema.optional( | ||
| 651 | Schema.Array( | ||
| 652 | Schema.Literal("write", "delete", "sensitive", "externalSideEffect"), | ||
| 653 | ), | ||
| 654 | ), | ||
| 655 | }), | ||
| 656 | elicitation: Schema.Struct({ | ||
| 657 | mayRequest: Schema.Boolean, | ||
| 658 | shapeId: Schema.optional(ShapeSymbolIdSchema), | ||
| 659 | }), | ||
| 660 | resume: Schema.Struct({ | ||
| 661 | supported: Schema.Boolean, | ||
| 662 | }), | ||
| 663 | }); | ||
| 664 | |||
| 665 | export const CapabilitySchema = Schema.extend( | ||
| 666 | Schema.Struct({ | ||
| 667 | id: CapabilityIdSchema, | ||
| 668 | serviceScopeId: ScopeIdSchema, | ||
| 669 | surface: Schema.Struct({ | ||
| 670 | toolPath: Schema.Array(Schema.String), | ||
| 671 | title: Schema.optional(Schema.String), | ||
| 672 | summary: Schema.optional(Schema.String), | ||
| 673 | description: Schema.optional(Schema.String), | ||
| 674 | aliases: Schema.optional(Schema.Array(Schema.String)), | ||
| 675 | tags: Schema.optional(Schema.Array(Schema.String)), | ||
| 676 | }), | ||
| 677 | semantics: Schema.Struct({ | ||
| 678 | effect: EffectKindSchema, | ||
| 679 | safe: Schema.optional(Schema.Boolean), | ||
| 680 | idempotent: Schema.optional(Schema.Boolean), | ||
| 681 | destructive: Schema.optional(Schema.Boolean), | ||
| 682 | }), | ||
| 683 | docs: Schema.optional(DocumentationBlockSchema), | ||
| 684 | auth: AuthRequirementSchema, | ||
| 685 | interaction: InteractionSpecSchema, | ||
| 686 | executableIds: Schema.Array(ExecutableIdSchema), | ||
| 687 | preferredExecutableId: Schema.optional(ExecutableIdSchema), | ||
| 688 | exampleIds: Schema.optional(Schema.Array(ExampleSymbolIdSchema)), | ||
| 689 | }), | ||
| 690 | EntityBaseSchema, | ||
| 691 | ); | ||
| 692 | |||
| 693 | export const HttpExecutableSchema = Schema.extend( | ||
| 694 | Schema.Struct({ | ||
| 695 | id: ExecutableIdSchema, | ||
| 696 | protocol: Schema.Literal("http"), | ||
| 697 | capabilityId: CapabilityIdSchema, | ||
| 698 | scopeId: ScopeIdSchema, | ||
| 699 | method: Schema.String, | ||
| 700 | pathTemplate: Schema.String, | ||
| 701 | pathParameterIds: Schema.optional(Schema.Array(ParameterSymbolIdSchema)), | ||
| 702 | queryParameterIds: Schema.optional(Schema.Array(ParameterSymbolIdSchema)), | ||
| 703 | headerParameterIds: Schema.optional(Schema.Array(ParameterSymbolIdSchema)), | ||
| 704 | cookieParameterIds: Schema.optional(Schema.Array(ParameterSymbolIdSchema)), | ||
| 705 | requestBodyId: Schema.optional(RequestBodySymbolIdSchema), | ||
| 706 | responseSetId: ResponseSetIdSchema, | ||
| 707 | }), | ||
| 708 | EntityBaseSchema, | ||
| 709 | ); | ||
| 710 | |||
| 711 | export const GraphQLExecutableSchema = Schema.extend( | ||
| 712 | Schema.Struct({ | ||
| 713 | id: ExecutableIdSchema, | ||
| 714 | protocol: Schema.Literal("graphql"), | ||
| 715 | capabilityId: CapabilityIdSchema, | ||
| 716 | scopeId: ScopeIdSchema, | ||
| 717 | operationType: Schema.Literal("query", "mutation", "subscription"), | ||
| 718 | rootField: Schema.String, | ||
| 719 | argumentShapeId: ShapeSymbolIdSchema, | ||
| 720 | resultShapeId: ShapeSymbolIdSchema, | ||
| 721 | selectionShapeId: Schema.optional(ShapeSymbolIdSchema), | ||
| 722 | selectionMode: SelectionModeSchema, | ||
| 723 | responseSetId: ResponseSetIdSchema, | ||
| 724 | }), | ||
| 725 | EntityBaseSchema, | ||
| 726 | ); | ||
| 727 | |||
| 728 | export const McpExecutableSchema = Schema.extend( | ||
| 729 | Schema.Struct({ | ||
| 730 | id: ExecutableIdSchema, | ||
| 731 | protocol: Schema.Literal("mcp"), | ||
| 732 | capabilityId: CapabilityIdSchema, | ||
| 733 | scopeId: ScopeIdSchema, | ||
| 734 | serverRef: Schema.String, | ||
| 735 | toolName: Schema.String, | ||
| 736 | inputShapeId: Schema.optional(ShapeSymbolIdSchema), | ||
| 737 | outputShapeId: Schema.optional(ShapeSymbolIdSchema), | ||
| 738 | responseSetId: ResponseSetIdSchema, | ||
| 739 | }), | ||
| 740 | EntityBaseSchema, | ||
| 741 | ); | ||
| 742 | |||
| 743 | export const ExecutableSchema = Schema.Union( | ||
| 744 | HttpExecutableSchema, | ||
| 745 | GraphQLExecutableSchema, | ||
| 746 | McpExecutableSchema, | ||
| 747 | ); | ||
| 748 | |||
| 749 | export const PaginationHintSchema = Schema.Struct({ | ||
| 750 | kind: PaginationHintKindSchema, | ||
| 751 | tokenParamName: Schema.optional(Schema.String), | ||
| 752 | nextFieldPath: Schema.optional(Schema.String), | ||
| 753 | }); | ||
| 754 | |||
| 755 | export const StatusMatchSchema = Schema.Union( | ||
| 756 | Schema.Struct({ | ||
| 757 | kind: Schema.Literal("exact"), | ||
| 758 | status: Schema.Number, | ||
| 759 | }), | ||
| 760 | Schema.Struct({ | ||
| 761 | kind: Schema.Literal("range"), | ||
| 762 | value: StatusRangeSchema, | ||
| 763 | }), | ||
| 764 | Schema.Struct({ | ||
| 765 | kind: Schema.Literal("default"), | ||
| 766 | }), | ||
| 767 | ); | ||
| 768 | |||
| 769 | export const ResponseVariantSchema = Schema.Struct({ | ||
| 770 | match: StatusMatchSchema, | ||
| 771 | responseId: ResponseSymbolIdSchema, | ||
| 772 | traits: Schema.optional(Schema.Array(ResponseTraitSchema)), | ||
| 773 | pagination: Schema.optional(PaginationHintSchema), | ||
| 774 | }); | ||
| 775 | |||
| 776 | export const ResponseSetSchema = Schema.extend( | ||
| 777 | Schema.Struct({ | ||
| 778 | id: ResponseSetIdSchema, | ||
| 779 | variants: Schema.Array(ResponseVariantSchema), | ||
| 780 | }), | ||
| 781 | EntityBaseSchema, | ||
| 782 | ); | ||
| 783 | |||
| 784 | export const ImportDiagnosticSchema = Schema.Struct({ | ||
| 785 | id: DiagnosticIdSchema, | ||
| 786 | level: DiagnosticLevelSchema, | ||
| 787 | code: DiagnosticCodeSchema, | ||
| 788 | message: Schema.String, | ||
| 789 | relatedSymbolIds: Schema.optional(Schema.Array(SymbolIdSchema)), | ||
| 790 | provenance: Schema.Array(ProvenanceRefSchema), | ||
| 791 | }); | ||
| 792 | |||
| 793 | export const CatalogV1Schema = Schema.Struct({ | ||
| 794 | version: Schema.Literal("ir.v1"), | ||
| 795 | documents: Schema.Record({ | ||
| 796 | key: DocumentIdSchema, | ||
| 797 | value: SourceDocumentSchema, | ||
| 798 | }), | ||
| 799 | resources: Schema.Record({ | ||
| 800 | key: ResourceIdSchema, | ||
| 801 | value: SchemaResourceSchema, | ||
| 802 | }), | ||
| 803 | scopes: Schema.Record({ | ||
| 804 | key: ScopeIdSchema, | ||
| 805 | value: ScopeSchema, | ||
| 806 | }), | ||
| 807 | symbols: Schema.Record({ | ||
| 808 | key: Schema.String, | ||
| 809 | value: SymbolSchema, | ||
| 810 | }), | ||
| 811 | capabilities: Schema.Record({ | ||
| 812 | key: CapabilityIdSchema, | ||
| 813 | value: CapabilitySchema, | ||
| 814 | }), | ||
| 815 | executables: Schema.Record({ | ||
| 816 | key: ExecutableIdSchema, | ||
| 817 | value: ExecutableSchema, | ||
| 818 | }), | ||
| 819 | responseSets: Schema.Record({ | ||
| 820 | key: ResponseSetIdSchema, | ||
| 821 | value: ResponseSetSchema, | ||
| 822 | }), | ||
| 823 | diagnostics: Schema.Record({ | ||
| 824 | key: DiagnosticIdSchema, | ||
| 825 | value: ImportDiagnosticSchema, | ||
| 826 | }), | ||
| 827 | }); | ||
| 828 | |||
| 829 | export const CatalogFragmentV1Schema = Schema.Struct({ | ||
| 830 | version: Schema.Literal("ir.v1.fragment"), | ||
| 831 | documents: Schema.optional( | ||
| 832 | Schema.Record({ | ||
| 833 | key: DocumentIdSchema, | ||
| 834 | value: SourceDocumentSchema, | ||
| 835 | }), | ||
| 836 | ), | ||
| 837 | resources: Schema.optional( | ||
| 838 | Schema.Record({ | ||
| 839 | key: ResourceIdSchema, | ||
| 840 | value: SchemaResourceSchema, | ||
| 841 | }), | ||
| 842 | ), | ||
| 843 | scopes: Schema.optional( | ||
| 844 | Schema.Record({ | ||
| 845 | key: ScopeIdSchema, | ||
| 846 | value: ScopeSchema, | ||
| 847 | }), | ||
| 848 | ), | ||
| 849 | symbols: Schema.optional( | ||
| 850 | Schema.Record({ | ||
| 851 | key: Schema.String, | ||
| 852 | value: SymbolSchema, | ||
| 853 | }), | ||
| 854 | ), | ||
| 855 | capabilities: Schema.optional( | ||
| 856 | Schema.Record({ | ||
| 857 | key: CapabilityIdSchema, | ||
| 858 | value: CapabilitySchema, | ||
| 859 | }), | ||
| 860 | ), | ||
| 861 | executables: Schema.optional( | ||
| 862 | Schema.Record({ | ||
| 863 | key: ExecutableIdSchema, | ||
| 864 | value: ExecutableSchema, | ||
| 865 | }), | ||
| 866 | ), | ||
| 867 | responseSets: Schema.optional( | ||
| 868 | Schema.Record({ | ||
| 869 | key: ResponseSetIdSchema, | ||
| 870 | value: ResponseSetSchema, | ||
| 871 | }), | ||
| 872 | ), | ||
| 873 | diagnostics: Schema.optional( | ||
| 874 | Schema.Record({ | ||
| 875 | key: DiagnosticIdSchema, | ||
| 876 | value: ImportDiagnosticSchema, | ||
| 877 | }), | ||
| 878 | ), | ||
| 879 | }); | ||
| 880 | |||
| 881 | export const CatalogSnapshotV1Schema = Schema.Struct({ | ||
| 882 | version: Schema.Literal("ir.v1.snapshot"), | ||
| 883 | import: ImportMetadataSchema, | ||
| 884 | catalog: CatalogV1Schema, | ||
| 885 | }); | ||
| 886 | |||
| 887 | export type SourceKind = typeof SourceKindSchema.Type; | ||
| 888 | export type ScopeKind = typeof ScopeKindSchema.Type; | ||
| 889 | export type EffectKind = typeof EffectKindSchema.Type; | ||
| 890 | export type ParameterLocation = typeof ParameterLocationSchema.Type; | ||
| 891 | export type ResponseTrait = typeof ResponseTraitSchema.Type; | ||
| 892 | export type SecuritySchemeType = typeof SecuritySchemeTypeSchema.Type; | ||
| 893 | export type SelectionMode = typeof SelectionModeSchema.Type; | ||
| 894 | export type PaginationHintKind = typeof PaginationHintKindSchema.Type; | ||
| 895 | export type DiagnosticCode = typeof DiagnosticCodeSchema.Type; | ||
| 896 | export type DiagnosticLevel = typeof DiagnosticLevelSchema.Type; | ||
| 897 | |||
| 898 | export type DocumentationBlock = typeof DocumentationBlockSchema.Type; | ||
| 899 | export type ProvenanceRef = typeof ProvenanceRefSchema.Type; | ||
| 900 | export type NativeBlob = typeof NativeBlobSchema.Type; | ||
| 901 | export type EntityBase = typeof EntityBaseSchema.Type; | ||
| 902 | export type ImportMetadata = typeof ImportMetadataSchema.Type; | ||
| 903 | export type SourceDocument = typeof SourceDocumentSchema.Type; | ||
| 904 | export type FieldSpec = typeof FieldSpecSchema.Type; | ||
| 905 | export type DiscriminatorSpec = typeof DiscriminatorSpecSchema.Type; | ||
| 906 | export type ScalarShape = typeof ScalarShapeSchema.Type; | ||
| 907 | export type UnknownShape = typeof UnknownShapeSchema.Type; | ||
| 908 | export type ConstShape = typeof ConstShapeSchema.Type; | ||
| 909 | export type EnumShape = typeof EnumShapeSchema.Type; | ||
| 910 | export type ObjectShape = typeof ObjectShapeSchema.Type; | ||
| 911 | export type ArrayShape = typeof ArrayShapeSchema.Type; | ||
| 912 | export type TupleShape = typeof TupleShapeSchema.Type; | ||
| 913 | export type MapShape = typeof MapShapeSchema.Type; | ||
| 914 | export type AllOfShape = typeof AllOfShapeSchema.Type; | ||
| 915 | export type AnyOfShape = typeof AnyOfShapeSchema.Type; | ||
| 916 | export type OneOfShape = typeof OneOfShapeSchema.Type; | ||
| 917 | export type NullableShape = typeof NullableShapeSchema.Type; | ||
| 918 | export type RefShape = typeof RefShapeSchema.Type; | ||
| 919 | export type NotShape = typeof NotShapeSchema.Type; | ||
| 920 | export type ConditionalShape = typeof ConditionalShapeSchema.Type; | ||
| 921 | export type GraphQLInterfaceShape = typeof GraphQLInterfaceShapeSchema.Type; | ||
| 922 | export type GraphQLUnionShape = typeof GraphQLUnionShapeSchema.Type; | ||
| 923 | export type ShapeNode = typeof ShapeNodeSchema.Type; | ||
| 924 | export type AnchorTarget = typeof AnchorTargetSchema.Type; | ||
| 925 | export type SchemaResource = typeof SchemaResourceSchema.Type; | ||
| 926 | export type RequestPlacementHint = typeof RequestPlacementHintSchema.Type; | ||
| 927 | export type OAuthFlow = typeof OAuthFlowSchema.Type; | ||
| 928 | export type SecurityPlacement = typeof SecurityPlacementSchema.Type; | ||
| 929 | export type SecuritySchemeSymbol = typeof SecuritySchemeSymbolSchema.Type; | ||
| 930 | export type EncodingSpec = typeof EncodingSpecSchema.Type; | ||
| 931 | export type ContentSpec = typeof ContentSpecSchema.Type; | ||
| 932 | export type ShapeSymbol = typeof ShapeSymbolSchema.Type; | ||
| 933 | export type ParameterSymbol = typeof ParameterSymbolSchema.Type; | ||
| 934 | export type HeaderSymbol = typeof HeaderSymbolSchema.Type; | ||
| 935 | export type RequestBodySymbol = typeof RequestBodySymbolSchema.Type; | ||
| 936 | export type ResponseSymbol = typeof ResponseSymbolSchema.Type; | ||
| 937 | export type ExampleSymbol = typeof ExampleSymbolSchema.Type; | ||
| 938 | export type Symbol = | ||
| 939 | | ShapeSymbol | ||
| 940 | | ParameterSymbol | ||
| 941 | | RequestBodySymbol | ||
| 942 | | ResponseSymbol | ||
| 943 | | HeaderSymbol | ||
| 944 | | ExampleSymbol | ||
| 945 | | SecuritySchemeSymbol; | ||
| 946 | export type ServerSpec = typeof ServerSpecSchema.Type; | ||
| 947 | export type ScopeDefaults = typeof ScopeDefaultsSchema.Type; | ||
| 948 | export type Scope = typeof ScopeSchema.Type; | ||
| 949 | export type InteractionSpec = typeof InteractionSpecSchema.Type; | ||
| 950 | export type Capability = typeof CapabilitySchema.Type; | ||
| 951 | export type HttpExecutable = typeof HttpExecutableSchema.Type; | ||
| 952 | export type GraphQLExecutable = typeof GraphQLExecutableSchema.Type; | ||
| 953 | export type McpExecutable = typeof McpExecutableSchema.Type; | ||
| 954 | export type Executable = typeof ExecutableSchema.Type; | ||
| 955 | export type PaginationHint = typeof PaginationHintSchema.Type; | ||
| 956 | export type StatusMatch = typeof StatusMatchSchema.Type; | ||
| 957 | export type ResponseVariant = typeof ResponseVariantSchema.Type; | ||
| 958 | export type ResponseSet = typeof ResponseSetSchema.Type; | ||
| 959 | export type ImportDiagnostic = typeof ImportDiagnosticSchema.Type; | ||
| 960 | export type CatalogV1 = typeof CatalogV1Schema.Type; | ||
| 961 | export type CatalogFragmentV1 = typeof CatalogFragmentV1Schema.Type; | ||
| 962 | export type CatalogSnapshotV1 = typeof CatalogSnapshotV1Schema.Type; | ||
| @@ -82,13 +82,13 @@ export const EXECUTOR_SOURCES_ADD_OUTPUT_SIGNATURE = deriveSchemaTypeSignature( · 81 unmodified lines | |||
| 82 | 260, | 82 | 260, |
| 83 | ); | 83 | ); |
| 84 | 84 | ||
| 85 | export const EXECUTOR_SOURCES_ADD_INPUT_SCHEMA_JSON = JSON.stringify( | 85 | export const EXECUTOR_SOURCES_ADD_INPUT_SCHEMA = deriveSchemaJson( |
| 86 | deriveSchemaJson(ExecutorAddSourceInputSchema) ?? {}, | 86 | ExecutorAddSourceInputSchema, |
| 87 | ); | 87 | ) ?? {}; |
| 88 | 88 | ||
| 89 | export const EXECUTOR_SOURCES_ADD_OUTPUT_SCHEMA_JSON = JSON.stringify( | 89 | export const EXECUTOR_SOURCES_ADD_OUTPUT_SCHEMA = deriveSchemaJson( |
| 90 | deriveSchemaJson(SourceSchema) ?? {}, | 90 | SourceSchema, |
| 91 | ); | 91 | ) ?? {}; |
| 92 | 92 | ||
| 93 | export const EXECUTOR_SOURCES_ADD_HELP_LINES = [ | 93 | export const EXECUTOR_SOURCES_ADD_HELP_LINES = [ |
| 94 | "Source add input shapes:", | 94 | "Source add input shapes:", |
| @@ -437,8 +437,8 @@ export const createExecutorToolMap = (input: { · 342 unmodified lines | |||
| 437 | metadata: { | 437 | metadata: { |
| 438 | inputType: EXECUTOR_SOURCES_ADD_INPUT_HINT, | 438 | inputType: EXECUTOR_SOURCES_ADD_INPUT_HINT, |
| 439 | outputType: EXECUTOR_SOURCES_ADD_OUTPUT_SIGNATURE, | 439 | outputType: EXECUTOR_SOURCES_ADD_OUTPUT_SIGNATURE, |
| 440 | inputSchemaJson: EXECUTOR_SOURCES_ADD_INPUT_SCHEMA_JSON, | 440 | inputSchema: EXECUTOR_SOURCES_ADD_INPUT_SCHEMA, |
| 441 | outputSchemaJson: EXECUTOR_SOURCES_ADD_OUTPUT_SCHEMA_JSON, | 441 | outputSchema: EXECUTOR_SOURCES_ADD_OUTPUT_SCHEMA, |
| 442 | sourceKey: "executor", | 442 | sourceKey: "executor", |
| 443 | interaction: "auto", | 443 | interaction: "auto", |
| 444 | }, | 444 | }, |
| @@ -1,6 +1,6 @@ | |||
| 1 | import { createServer, type IncomingMessage } from "node:http"; | 1 | import { createServer, type IncomingMessage } from "node:http"; |
| 2 | 2 | ||
| 3 | import { afterEach, describe, expect, it } from "vitest"; | 3 | import { afterEach, describe, expect, it } from "@effect/vitest"; |
| 4 | 4 | ||
| 5 | import { createGraphqlToolFromPersistedOperation } from "./graphql-tools"; | 5 | import { createGraphqlToolFromPersistedOperation } from "./graphql-tools"; |
| 6 | 6 | ||
| @@ -91,7 +91,7 @@ describe("graphql-tools", () => { · 84 unmodified lines | |||
| 91 | path: "source.graphql.raw", | 91 | path: "source.graphql.raw", |
| 92 | sourceKey: "src_graphql", | 92 | sourceKey: "src_graphql", |
| 93 | endpoint: server.baseUrl, | 93 | endpoint: server.baseUrl, |
| 94 | providerDataJson: JSON.stringify({ | 94 | providerData: { |
| 95 | kind: "graphql", | 95 | kind: "graphql", |
| 96 | toolKind: "request", | 96 | toolKind: "request", |
| 97 | toolId: "raw", | 97 | toolId: "raw", |
| @@ -105,7 +105,7 @@ describe("graphql-tools", () => { · 7 unmodified lines | |||
| 105 | queryTypeName: "Query", | 105 | queryTypeName: "Query", |
| 106 | mutationTypeName: "Mutation", | 106 | mutationTypeName: "Mutation", |
| 107 | subscriptionTypeName: null, | 107 | subscriptionTypeName: null, |
| 108 | }), | 108 | }, |
| 109 | credentialPlacements: { | 109 | credentialPlacements: { |
| 110 | headers: { | 110 | headers: { |
| 111 | authorization: "Token top-secret", | 111 | authorization: "Token top-secret", |
| @@ -10,7 +10,7 @@ import { · 9 unmodified lines | |||
| 10 | type ToolMap, | 10 | type ToolMap, |
| 11 | type ToolMetadata, | 11 | type ToolMetadata, |
| 12 | type ToolPath, | 12 | type ToolPath, |
| 13 | typeSignatureFromSchemaJson, | 13 | typeSignatureFromSchema, |
| 14 | } from "@executor/codemode-core"; | 14 | } from "@executor/codemode-core"; |
| 15 | import * as Effect from "effect/Effect"; | 15 | import * as Effect from "effect/Effect"; |
| 16 | import * as Schema from "effect/Schema"; | 16 | import * as Schema from "effect/Schema"; |
| @@ -51,9 +51,9 @@ type GraphqlManifestToolBase = { · 34 unmodified lines | |||
| 51 | rawToolId: string | null; | 51 | rawToolId: string | null; |
| 52 | toolName: string; | 52 | toolName: string; |
| 53 | description: string | null; | 53 | description: string | null; |
| 54 | inputSchemaJson?: string; | 54 | inputSchema?: unknown; |
| 55 | outputSchemaJson?: string; | 55 | outputSchema?: unknown; |
| 56 | exampleInputJson?: string; | 56 | exampleInput?: unknown; |
| 57 | }; | 57 | }; |
| 58 | 58 | ||
| 59 | export type GraphqlRequestToolManifestEntry = GraphqlManifestToolBase & { | 59 | export type GraphqlRequestToolManifestEntry = GraphqlManifestToolBase & { |
| @@ -102,10 +102,10 @@ export type GraphqlToolDefinition = { · 42 unmodified lines | |||
| 102 | export type GraphqlToolPresentation = { | 102 | export type GraphqlToolPresentation = { |
| 103 | inputType: string; | 103 | inputType: string; |
| 104 | outputType: string; | 104 | outputType: string; |
| 105 | inputSchemaJson?: string; | 105 | inputSchema?: unknown; |
| 106 | outputSchemaJson?: string; | 106 | outputSchema?: unknown; |
| 107 | exampleInputJson?: string; | 107 | exampleInput?: unknown; |
| 108 | providerDataJson: string; | 108 | providerData: GraphqlToolProviderData; |
| 109 | }; | 109 | }; |
| 110 | 110 | ||
| 111 | const GraphqlToolKindSchema = Schema.Literal("request", "field"); | 111 | const GraphqlToolKindSchema = Schema.Literal("request", "field"); |
| @@ -883,11 +883,11 @@ const outputEnvelopeSchemaForField = ( · 771 unmodified lines | |||
| 883 | }; | 883 | }; |
| 884 | }; | 884 | }; |
| 885 | 885 | ||
| 886 | const exampleInputJsonForField = ( | 886 | const exampleInputForField = ( |
| 887 | args: readonly GraphQLArgument[], | 887 | args: readonly GraphQLArgument[], |
| 888 | ): string | undefined => { | 888 | ): Record<string, unknown> | undefined => { |
| 889 | if (args.length === 0) { | 889 | if (args.length === 0) { |
| 890 | return JSON.stringify({}); | 890 | return {}; |
| 891 | } | 891 | } |
| 892 | 892 | ||
| 893 | const requiredArgs = args.filter( | 893 | const requiredArgs = args.filter( |
| @@ -902,7 +902,7 @@ const exampleInputJsonForField = ( · 8 unmodified lines | |||
| 902 | ]), | 902 | ]), |
| 903 | ); | 903 | ); |
| 904 | 904 | ||
| 905 | return JSON.stringify(example); | 905 | return example; |
| 906 | }; | 906 | }; |
| 907 | 907 | ||
| 908 | const buildGraphqlFieldOperationDocument = (input: { | 908 | const buildGraphqlFieldOperationDocument = (input: { |
| @@ -958,11 +958,11 @@ const requestToolManifestEntry = ( · 49 unmodified lines | |||
| 958 | rawToolId: "request", | 958 | rawToolId: "request", |
| 959 | toolName: "GraphQL request", | 959 | toolName: "GraphQL request", |
| 960 | description: `Execute a raw GraphQL request against ${sourceName}.`, | 960 | description: `Execute a raw GraphQL request against ${sourceName}.`, |
| 961 | inputSchemaJson: JSON.stringify(GRAPHQL_REQUEST_INPUT_SCHEMA), | 961 | inputSchema: GRAPHQL_REQUEST_INPUT_SCHEMA, |
| 962 | outputSchemaJson: JSON.stringify(GRAPHQL_REQUEST_OUTPUT_SCHEMA), | 962 | outputSchema: GRAPHQL_REQUEST_OUTPUT_SCHEMA, |
| 963 | exampleInputJson: JSON.stringify({ | 963 | exampleInput: { |
| 964 | query: "query { __typename }", | 964 | query: "query { __typename }", |
| 965 | }), | 965 | }, |
| 966 | }); | 966 | }); |
| 967 | 967 | ||
| 968 | type GraphqlFieldToolDraft = Omit<GraphqlFieldToolManifestEntry, "toolId"> & { | 968 | type GraphqlFieldToolDraft = Omit<GraphqlFieldToolManifestEntry, "toolId"> & { |
| @@ -1030,11 +1030,13 @@ const fieldToolDraftsFromRootType = (input: { · 61 unmodified lines | |||
| 1030 | .filter((field) => !field.name.startsWith("__")) | 1030 | .filter((field) => !field.name.startsWith("__")) |
| 1031 | .map((field) => { | 1031 | .map((field) => { |
| 1032 | const leaf = toCamelCase(field.name); | 1032 | const leaf = toCamelCase(field.name); |
| 1033 | const outputSchemaJson = JSON.stringify( | 1033 | const outputSchema = outputEnvelopeSchemaForField( |
| 1034 | outputEnvelopeSchemaForField(field.type, input.bundleBuilder), | 1034 | field.type, |
| 1035 | input.bundleBuilder, | ||
| 1035 | ); | 1036 | ); |
| 1036 | const inputSchemaJson = JSON.stringify( | 1037 | const inputSchema = inputSchemaForFieldArguments( |
| 1037 | inputSchemaForFieldArguments(field.args, input.bundleBuilder), | 1038 | field.args, |
| 1039 | input.bundleBuilder, | ||
| 1038 | ); | 1040 | ); |
| 1039 | const { operationName, operationDocument } = | 1041 | const { operationName, operationDocument } = |
| 1040 | buildGraphqlFieldOperationDocument({ | 1042 | buildGraphqlFieldOperationDocument({ |
| @@ -1061,9 +1063,9 @@ const fieldToolDraftsFromRootType = (input: { · 20 unmodified lines | |||
| 1061 | operationType: input.operationType, | 1063 | operationType: input.operationType, |
| 1062 | operationName, | 1064 | operationName, |
| 1063 | operationDocument, | 1065 | operationDocument, |
| 1064 | inputSchemaJson, | 1066 | inputSchema, |
| 1065 | outputSchemaJson, | 1067 | outputSchema, |
| 1066 | exampleInputJson: exampleInputJsonForField(field.args), | 1068 | exampleInput: exampleInputForField(field.args), |
| 1067 | searchTerms: [ | 1069 | searchTerms: [ |
| 1068 | input.operationType, | 1070 | input.operationType, |
| 1069 | field.name, | 1071 | field.name, |
| @@ -1143,26 +1145,26 @@ export const buildGraphqlToolPresentation = (input: { · 73 unmodified lines | |||
| 1143 | const entry = input.manifest.tools.find( | 1145 | const entry = input.manifest.tools.find( |
| 1144 | (tool) => tool.toolId === input.definition.toolId, | 1146 | (tool) => tool.toolId === input.definition.toolId, |
| 1145 | ); | 1147 | ); |
| 1146 | const inputSchemaJson = entry?.inputSchemaJson; | 1148 | const inputSchema = entry?.inputSchema; |
| 1147 | const outputSchemaJson = entry?.outputSchemaJson; | 1149 | const outputSchema = entry?.outputSchema; |
| 1148 | 1150 | ||
| 1149 | return { | 1151 | return { |
| 1150 | inputType: typeSignatureFromSchemaJson( | 1152 | inputType: typeSignatureFromSchema( |
| 1151 | inputSchemaJson, | 1153 | inputSchema, |
| 1152 | "unknown", | 1154 | "unknown", |
| 1153 | Infinity, | 1155 | Infinity, |
| 1154 | ), | 1156 | ), |
| 1155 | outputType: typeSignatureFromSchemaJson( | 1157 | outputType: typeSignatureFromSchema( |
| 1156 | outputSchemaJson, | 1158 | outputSchema, |
| 1157 | "unknown", | 1159 | "unknown", |
| 1158 | Infinity, | 1160 | Infinity, |
| 1159 | ), | 1161 | ), |
| 1160 | ...(inputSchemaJson ? { inputSchemaJson } : {}), | 1162 | ...(inputSchema !== undefined ? { inputSchema } : {}), |
| 1161 | ...(outputSchemaJson ? { outputSchemaJson } : {}), | 1163 | ...(outputSchema !== undefined ? { outputSchema } : {}), |
| 1162 | ...(entry?.exampleInputJson | 1164 | ...(entry?.exampleInput !== undefined |
| 1163 | ? { exampleInputJson: entry.exampleInputJson } | 1165 | ? { exampleInput: entry.exampleInput } |
| 1164 | : {}), | 1166 | : {}), |
| 1165 | providerDataJson: JSON.stringify({ | 1167 | providerData: { |
| 1166 | kind: "graphql", | 1168 | kind: "graphql", |
| 1167 | toolKind: entry?.kind ?? "request", | 1169 | toolKind: entry?.kind ?? "request", |
| 1168 | toolId: input.definition.toolId, | 1170 | toolId: input.definition.toolId, |
| @@ -1176,12 +1178,12 @@ export const buildGraphqlToolPresentation = (input: { · 7 unmodified lines | |||
| 1176 | queryTypeName: input.manifest.queryTypeName, | 1178 | queryTypeName: input.manifest.queryTypeName, |
| 1177 | mutationTypeName: input.manifest.mutationTypeName, | 1179 | mutationTypeName: input.manifest.mutationTypeName, |
| 1178 | subscriptionTypeName: input.manifest.subscriptionTypeName, | 1180 | subscriptionTypeName: input.manifest.subscriptionTypeName, |
| 1179 | } satisfies GraphqlToolProviderData), | 1181 | } satisfies GraphqlToolProviderData, |
| 1180 | }; | 1182 | }; |
| 1181 | }; | 1183 | }; |
| 1182 | 1184 | ||
| 1183 | const decodeGraphqlToolProviderDataJson = Schema.decodeUnknownEither( | 1185 | const decodeGraphqlToolProviderData = Schema.decodeUnknownEither( |
| 1184 | Schema.parseJson(GraphqlToolProviderDataSchema), | 1186 | GraphqlToolProviderDataSchema, |
| 1185 | ); | 1187 | ); |
| 1186 | 1188 | ||
| 1187 | export const decodeGraphqlSchemaRefTableJson = Schema.decodeUnknownEither( | 1189 | export const decodeGraphqlSchemaRefTableJson = Schema.decodeUnknownEither( |
| @@ -1218,19 +1220,14 @@ const setNestedSchemaProperty = ( · 30 unmodified lines | |||
| 1218 | }; | 1220 | }; |
| 1219 | 1221 | ||
| 1220 | const materializeSchemaWithRefDefinitions = (input: { | 1222 | const materializeSchemaWithRefDefinitions = (input: { |
| 1221 | schemaJson: string | undefined; | 1223 | schema: unknown; |
| 1222 | refTable?: Readonly<Record<string, unknown>>; | 1224 | refTable?: Readonly<Record<string, unknown>>; |
| 1223 | }): Record<string, unknown> => { | 1225 | }): Record<string, unknown> => { |
| 1224 | if (!input.schemaJson) { | 1226 | if (input.schema === undefined || input.schema === null) { |
| 1225 | return {}; | 1227 | return {}; |
| 1226 | } | 1228 | } |
| 1227 | 1229 | ||
| 1228 | let rootSchema: Record<string, unknown>; | 1230 | const rootSchema = asRecord(input.schema); |
| 1229 | try { | ||
| 1230 | rootSchema = asRecord(JSON.parse(input.schemaJson) as unknown); | ||
| 1231 | } catch { | ||
| 1232 | return {}; | ||
| 1233 | } | ||
| 1234 | 1231 | ||
| 1235 | if (!input.refTable || Object.keys(input.refTable).length === 0) { | 1232 | if (!input.refTable || Object.keys(input.refTable).length === 0) { |
| 1236 | return rootSchema; | 1233 | return rootSchema; |
| @@ -1269,17 +1266,17 @@ export const createGraphqlToolFromPersistedOperation = (input: { · 32 unmodified lines | |||
| 1269 | sourceKey: string; | 1266 | sourceKey: string; |
| 1270 | endpoint: string; | 1267 | endpoint: string; |
| 1271 | description?: string; | 1268 | description?: string; |
| 1272 | inputSchemaJson?: string; | 1269 | inputSchema?: unknown; |
| 1273 | outputSchemaJson?: string; | 1270 | outputSchema?: unknown; |
| 1274 | exampleInputJson?: string; | 1271 | exampleInput?: unknown; |
| 1275 | providerDataJson: string; | 1272 | providerData: unknown; |
| 1276 | schemaRefTable?: Readonly<Record<string, unknown>>; | 1273 | schemaRefTable?: Readonly<Record<string, unknown>>; |
| 1277 | defaultHeaders?: Readonly<Record<string, string>>; | 1274 | defaultHeaders?: Readonly<Record<string, string>>; |
| 1278 | credentialHeaders?: Readonly<Record<string, string>>; | 1275 | credentialHeaders?: Readonly<Record<string, string>>; |
| 1279 | credentialPlacements?: HttpRequestPlacements; | 1276 | credentialPlacements?: HttpRequestPlacements; |
| 1280 | }) => { | 1277 | }) => { |
| 1281 | const decodedProviderData = decodeGraphqlToolProviderDataJson( | 1278 | const decodedProviderData = decodeGraphqlToolProviderData( |
| 1282 | input.providerDataJson, | 1279 | input.providerData, |
| 1283 | ); | 1280 | ); |
| 1284 | if (decodedProviderData._tag === "Left") { | 1281 | if (decodedProviderData._tag === "Left") { |
| 1285 | throw new Error("Invalid GraphQL provider data"); | 1282 | throw new Error("Invalid GraphQL provider data"); |
| @@ -1287,7 +1284,7 @@ export const createGraphqlToolFromPersistedOperation = (input: { · 1 unmodified line | |||
| 1287 | 1284 | ||
| 1288 | const providerData = decodedProviderData.right; | 1285 | const providerData = decodedProviderData.right; |
| 1289 | const inputSchema = materializeSchemaWithRefDefinitions({ | 1286 | const inputSchema = materializeSchemaWithRefDefinitions({ |
| 1290 | schemaJson: input.inputSchemaJson, | 1287 | schema: input.inputSchema, |
| 1291 | refTable: input.schemaRefTable, | 1288 | refTable: input.schemaRefTable, |
| 1292 | }); | 1289 | }); |
| 1293 | 1290 | ||
| @@ -1298,28 +1295,28 @@ export const createGraphqlToolFromPersistedOperation = (input: { · 4 unmodified lines | |||
| 1298 | : providerData.operationType === "query" | 1295 | : providerData.operationType === "query" |
| 1299 | ? "auto" | 1296 | ? "auto" |
| 1300 | : "required", | 1297 | : "required", |
| 1301 | inputType: typeSignatureFromSchemaJson( | 1298 | inputType: typeSignatureFromSchema( |
| 1302 | input.inputSchemaJson, | 1299 | input.inputSchema, |
| 1303 | "unknown", | 1300 | "unknown", |
| 1304 | Infinity, | 1301 | Infinity, |
| 1305 | ), | 1302 | ), |
| 1306 | outputType: typeSignatureFromSchemaJson( | 1303 | outputType: typeSignatureFromSchema( |
| 1307 | input.outputSchemaJson, | 1304 | input.outputSchema, |
| 1308 | "unknown", | 1305 | "unknown", |
| 1309 | Infinity, | 1306 | Infinity, |
| 1310 | ), | 1307 | ), |
| 1311 | ...(input.inputSchemaJson | 1308 | ...(input.inputSchema !== undefined |
| 1312 | ? { inputSchemaJson: input.inputSchemaJson } | 1309 | ? { inputSchema: input.inputSchema } |
| 1313 | : {}), | 1310 | : {}), |
| 1314 | ...(input.outputSchemaJson | 1311 | ...(input.outputSchema !== undefined |
| 1315 | ? { outputSchemaJson: input.outputSchemaJson } | 1312 | ? { outputSchema: input.outputSchema } |
| 1316 | : {}), | 1313 | : {}), |
| 1317 | ...(input.exampleInputJson | 1314 | ...(input.exampleInput !== undefined |
| 1318 | ? { exampleInputJson: input.exampleInputJson } | 1315 | ? { exampleInput: input.exampleInput } |
| 1319 | : {}), | 1316 | : {}), |
| 1320 | sourceKey: input.sourceKey, | 1317 | sourceKey: input.sourceKey, |
| 1321 | providerKind: "graphql", | 1318 | providerKind: "graphql", |
| 1322 | providerDataJson: input.providerDataJson, | 1319 | providerData, |
| 1323 | }; | 1320 | }; |
| 1324 | 1321 | ||
| 1325 | return toTool({ | 1322 | return toTool({ |
| @@ -1387,17 +1384,17 @@ export const graphqlToolDescriptorFromDefinition = (input: { · 61 unmodified lines | |||
| 1387 | input.definition.operationType === "query" ? "auto" : "required", | 1384 | input.definition.operationType === "query" ? "auto" : "required", |
| 1388 | inputType: presentation.inputType, | 1385 | inputType: presentation.inputType, |
| 1389 | outputType: presentation.outputType, | 1386 | outputType: presentation.outputType, |
| 1390 | ...(input.includeSchemas && presentation.inputSchemaJson | 1387 | ...(input.includeSchemas && presentation.inputSchema !== undefined |
| 1391 | ? { inputSchemaJson: presentation.inputSchemaJson } | 1388 | ? { inputSchema: presentation.inputSchema } |
| 1392 | : {}), | 1389 | : {}), |
| 1393 | ...(input.includeSchemas && presentation.outputSchemaJson | 1390 | ...(input.includeSchemas && presentation.outputSchema !== undefined |
| 1394 | ? { outputSchemaJson: presentation.outputSchemaJson } | 1391 | ? { outputSchema: presentation.outputSchema } |
| 1395 | : {}), | 1392 | : {}), |
| 1396 | ...(presentation.exampleInputJson | 1393 | ...(presentation.exampleInput !== undefined |
| 1397 | ? { exampleInputJson: presentation.exampleInputJson } | 1394 | ? { exampleInput: presentation.exampleInput } |
| 1398 | : {}), | 1395 | : {}), |
| 1399 | providerKind: "graphql", | 1396 | providerKind: "graphql", |
| 1400 | providerDataJson: presentation.providerDataJson, | 1397 | providerData: presentation.providerData, |
| 1401 | }; | 1398 | }; |
| 1402 | }; | 1399 | }; |
| 1403 | 1400 | ||
| @@ -1572,10 +1569,10 @@ export const createGraphqlToolsFromManifest = (input: { · 168 unmodified lines | |||
| 1572 | sourceKey: input.sourceKey, | 1569 | sourceKey: input.sourceKey, |
| 1573 | endpoint, | 1570 | endpoint, |
| 1574 | description: definition.description, | 1571 | description: definition.description, |
| 1575 | inputSchemaJson: presentation.inputSchemaJson, | 1572 | inputSchema: presentation.inputSchema, |
| 1576 | outputSchemaJson: presentation.outputSchemaJson, | 1573 | outputSchema: presentation.outputSchema, |
| 1577 | exampleInputJson: presentation.exampleInputJson, | 1574 | exampleInput: presentation.exampleInput, |
| 1578 | providerDataJson: presentation.providerDataJson, | 1575 | providerData: presentation.providerData, |
| 1579 | schemaRefTable: input.manifest.schemaRefTable, | 1576 | schemaRefTable: input.manifest.schemaRefTable, |
| 1580 | defaultHeaders: input.defaultHeaders, | 1577 | defaultHeaders: input.defaultHeaders, |
| 1581 | credentialHeaders: input.credentialHeaders, | 1578 | credentialHeaders: input.credentialHeaders, |
| @@ -33,9 +33,9 @@ import { LocalToolRuntimeLoaderLive } from "./local-tools"; · 32 unmodified lines | |||
| 33 | import { synchronizeLocalWorkspaceState } from "./local-workspace-sync"; | 33 | import { synchronizeLocalWorkspaceState } from "./local-workspace-sync"; |
| 34 | import { ControlPlaneStore, type ControlPlaneStoreShape } from "./store"; | 34 | import { ControlPlaneStore, type ControlPlaneStoreShape } from "./store"; |
| 35 | import { RuntimeSourceStoreLive } from "./source-store"; | 35 | import { RuntimeSourceStoreLive } from "./source-store"; |
| 36 | import { RuntimeSourceRecipeStoreLive } from "./source-recipes-runtime"; | 36 | import { RuntimeSourceCatalogStoreLive } from "./source-catalog-runtime"; |
| 37 | import { RuntimeSourceAuthMaterialLive } from "./source-auth-material"; | 37 | import { RuntimeSourceAuthMaterialLive } from "./source-auth-material"; |
| 38 | import { RuntimeSourceMaterializationLive } from "./source-materialization"; | 38 | import { RuntimeSourceCatalogSyncLive } from "./source-catalog-sync"; |
| 39 | import { | 39 | import { |
| 40 | RuntimeSourceAuthServiceLive, | 40 | RuntimeSourceAuthServiceLive, |
| 41 | } from "./source-auth-service"; | 41 | } from "./source-auth-service"; |
| @@ -121,7 +121,7 @@ export const createRuntimeControlPlaneLayer = ( · 79 unmodified lines | |||
| 121 | Layer.provide(baseLayer), | 121 | Layer.provide(baseLayer), |
| 122 | ); | 122 | ); |
| 123 | 123 | ||
| 124 | const sourceRecipeStoreLayer = RuntimeSourceRecipeStoreLive.pipe( | 124 | const sourceCatalogStoreLayer = RuntimeSourceCatalogStoreLive.pipe( |
| 125 | Layer.provide(Layer.mergeAll(baseLayer, sourceStoreLayer)), | 125 | Layer.provide(Layer.mergeAll(baseLayer, sourceStoreLayer)), |
| 126 | ); | 126 | ); |
| 127 | 127 | ||
| @@ -129,7 +129,7 @@ export const createRuntimeControlPlaneLayer = ( · 1 unmodified line | |||
| 129 | Layer.provide(Layer.mergeAll(baseLayer, secretMaterialLayer)), | 129 | Layer.provide(Layer.mergeAll(baseLayer, secretMaterialLayer)), |
| 130 | ); | 130 | ); |
| 131 | 131 | ||
| 132 | const sourceMaterializationLayer = RuntimeSourceMaterializationLive.pipe( | 132 | const sourceCatalogSyncLayer = RuntimeSourceCatalogSyncLive.pipe( |
| 133 | Layer.provide( | 133 | Layer.provide( |
| 134 | Layer.mergeAll(baseLayer, secretMaterialLayer, sourceAuthMaterialLayer), | 134 | Layer.mergeAll(baseLayer, secretMaterialLayer, sourceAuthMaterialLayer), |
| 135 | ), | 135 | ), |
| @@ -143,7 +143,7 @@ export const createRuntimeControlPlaneLayer = ( · 7 unmodified lines | |||
| 143 | baseLayer, | 143 | baseLayer, |
| 144 | secretMaterialLayer, | 144 | secretMaterialLayer, |
| 145 | sourceStoreLayer, | 145 | sourceStoreLayer, |
| 146 | sourceMaterializationLayer, | 146 | sourceCatalogSyncLayer, |
| 147 | ), | 147 | ), |
| 148 | ), | 148 | ), |
| 149 | ); | 149 | ); |
| @@ -156,9 +156,9 @@ export const createRuntimeControlPlaneLayer = ( · 6 unmodified lines | |||
| 156 | baseLayer, | 156 | baseLayer, |
| 157 | secretMaterialLayer, | 157 | secretMaterialLayer, |
| 158 | sourceAuthMaterialLayer, | 158 | sourceAuthMaterialLayer, |
| 159 | sourceMaterializationLayer, | 159 | sourceCatalogSyncLayer, |
| 160 | sourceAuthLayer, | 160 | sourceAuthLayer, |
| 161 | sourceRecipeStoreLayer, | 161 | sourceCatalogStoreLayer, |
| 162 | localToolRuntimeLayer, | 162 | localToolRuntimeLayer, |
| 163 | ), | 163 | ), |
| 164 | ), | 164 | ), |
| @@ -169,8 +169,8 @@ export const createRuntimeControlPlaneLayer = ( · 4 unmodified lines | |||
| 169 | secretMaterialLayer, | 169 | secretMaterialLayer, |
| 170 | sourceStoreLayer, | 170 | sourceStoreLayer, |
| 171 | sourceAuthMaterialLayer, | 171 | sourceAuthMaterialLayer, |
| 172 | sourceMaterializationLayer, | 172 | sourceCatalogSyncLayer, |
| 173 | sourceRecipeStoreLayer, | 173 | sourceCatalogStoreLayer, |
| 174 | localToolRuntimeLayer, | 174 | localToolRuntimeLayer, |
| 175 | sourceAuthLayer, | 175 | sourceAuthLayer, |
| 176 | executionResolverLayer, | 176 | executionResolverLayer, |
| @@ -0,0 +1,494 @@ | |||
| 1 | import { | ||
| 2 | applyCookiePlacementsToHeaders, | ||
| 3 | applyHttpQueryPlacementsToUrl, | ||
| 4 | applyJsonBodyPlacements, | ||
| 5 | } from "@executor/codemode-core"; | ||
| 6 | import { createSdkMcpConnector } from "@executor/codemode-mcp"; | ||
| 7 | import type { AccountId, Source } from "#schema"; | ||
| 8 | import * as Effect from "effect/Effect"; | ||
| 9 | |||
| 10 | import type { CatalogV1, Capability, Executable, GraphQLExecutable, HttpExecutable, McpExecutable, ParameterSymbol } from "../ir/model"; | ||
| 11 | import type { LoadedSourceCatalogToolIndexEntry } from "./source-catalog-runtime"; | ||
| 12 | import type { ResolvedSourceAuthMaterial } from "./source-auth-material"; | ||
| 13 | |||
| 14 | const asObject = (value: unknown): Record<string, unknown> => | ||
| 15 | value !== null && typeof value === "object" && !Array.isArray(value) | ||
| 16 | ? (value as Record<string, unknown>) | ||
| 17 | : {}; | ||
| 18 | |||
| 19 | const asString = (value: unknown): string | null => | ||
| 20 | typeof value === "string" && value.trim().length > 0 ? value : null; | ||
| 21 | |||
| 22 | const asStringRecord = (value: unknown): Record<string, string> => | ||
| 23 | Object.fromEntries( | ||
| 24 | Object.entries(asObject(value)).flatMap(([key, entry]) => | ||
| 25 | typeof entry === "string" ? [[key, entry]] : []), | ||
| 26 | ); | ||
| 27 | |||
| 28 | const readSourceHeaders = (source: Source): Record<string, string> => { | ||
| 29 | const binding = asObject(source.binding); | ||
| 30 | const defaultHeaders = binding.defaultHeaders ?? binding.headers; | ||
| 31 | return asStringRecord(defaultHeaders); | ||
| 32 | }; | ||
| 33 | |||
| 34 | const readSourceQueryParams = (source: Source): Record<string, string> => | ||
| 35 | asStringRecord(asObject(source.binding).queryParams); | ||
| 36 | |||
| 37 | const readSourceTransport = (source: Source): "auto" | "streamable-http" | "sse" | undefined => { | ||
| 38 | const transport = asString(asObject(source.binding).transport); | ||
| 39 | if (transport === "streamable-http" || transport === "sse" || transport === "auto") { | ||
| 40 | return transport; | ||
| 41 | } | ||
| 42 | return undefined; | ||
| 43 | }; | ||
| 44 | |||
| 45 | const summarizeHttpBody = (body: unknown): string => { | ||
| 46 | if (typeof body === "string") { | ||
| 47 | return body.slice(0, 400); | ||
| 48 | } | ||
| 49 | try { | ||
| 50 | return JSON.stringify(body).slice(0, 400); | ||
| 51 | } catch { | ||
| 52 | return String(body); | ||
| 53 | } | ||
| 54 | }; | ||
| 55 | |||
| 56 | const decodeFetchBody = async (response: Response): Promise<unknown> => { | ||
| 57 | const contentType = response.headers.get("content-type")?.toLowerCase() ?? ""; | ||
| 58 | if (contentType.includes("application/json")) { | ||
| 59 | return response.json(); | ||
| 60 | } | ||
| 61 | if (response.status === 204) { | ||
| 62 | return null; | ||
| 63 | } | ||
| 64 | return response.text(); | ||
| 65 | }; | ||
| 66 | |||
| 67 | const primitiveString = (value: unknown): string => { | ||
| 68 | if (value === null || value === undefined) { | ||
| 69 | return ""; | ||
| 70 | } | ||
| 71 | if (typeof value === "string") { | ||
| 72 | return value; | ||
| 73 | } | ||
| 74 | if (typeof value === "number" || typeof value === "boolean") { | ||
| 75 | return String(value); | ||
| 76 | } | ||
| 77 | return JSON.stringify(value); | ||
| 78 | }; | ||
| 79 | |||
| 80 | const parameterById = (catalog: CatalogV1, parameterId: string): ParameterSymbol | undefined => { | ||
| 81 | const symbol = catalog.symbols[parameterId]; | ||
| 82 | return symbol?.kind === "parameter" ? symbol : undefined; | ||
| 83 | }; | ||
| 84 | |||
| 85 | const httpParameterLookupMode = (catalog: CatalogV1, executable: HttpExecutable): "flat" | "grouped" => { | ||
| 86 | const reserved = new Set(["body", "args", "select", "path", "query", "headers", "cookies"]); | ||
| 87 | const allParameters = [ | ||
| 88 | ...(executable.pathParameterIds ?? []), | ||
| 89 | ...(executable.queryParameterIds ?? []), | ||
| 90 | ...(executable.headerParameterIds ?? []), | ||
| 91 | ...(executable.cookieParameterIds ?? []), | ||
| 92 | ] | ||
| 93 | .map((parameterId) => parameterById(catalog, parameterId)) | ||
| 94 | .filter((parameter): parameter is ParameterSymbol => parameter !== undefined); | ||
| 95 | |||
| 96 | const names = allParameters.map((parameter) => parameter.name); | ||
| 97 | const hasCollision = names.some((name, index) => | ||
| 98 | reserved.has(name) || names.findIndex((candidate) => candidate === name) !== index | ||
| 99 | ); | ||
| 100 | |||
| 101 | return hasCollision ? "grouped" : "flat"; | ||
| 102 | }; | ||
| 103 | |||
| 104 | const readHttpParameterValue = (input: { | ||
| 105 | catalog: CatalogV1; | ||
| 106 | executable: HttpExecutable; | ||
| 107 | parameter: ParameterSymbol; | ||
| 108 | args: Record<string, unknown>; | ||
| 109 | }): unknown => { | ||
| 110 | const mode = httpParameterLookupMode(input.catalog, input.executable); | ||
| 111 | if (mode === "flat") { | ||
| 112 | return input.args[input.parameter.name]; | ||
| 113 | } | ||
| 114 | |||
| 115 | const groupKey = | ||
| 116 | input.parameter.location === "header" | ||
| 117 | ? "headers" | ||
| 118 | : input.parameter.location === "cookie" | ||
| 119 | ? "cookies" | ||
| 120 | : input.parameter.location; | ||
| 121 | return asObject(input.args[groupKey])[input.parameter.name]; | ||
| 122 | }; | ||
| 123 | |||
| 124 | const executeHttp = (input: { | ||
| 125 | source: Source; | ||
| 126 | catalog: CatalogV1; | ||
| 127 | executable: HttpExecutable; | ||
| 128 | auth: ResolvedSourceAuthMaterial; | ||
| 129 | args: unknown; | ||
| 130 | }) => | ||
| 131 | Effect.tryPromise({ | ||
| 132 | try: async () => { | ||
| 133 | const argsRecord = asObject(input.args); | ||
| 134 | let resolvedPath = input.executable.pathTemplate; | ||
| 135 | const url = applyHttpQueryPlacementsToUrl({ | ||
| 136 | url: new URL(input.source.endpoint), | ||
| 137 | queryParams: readSourceQueryParams(input.source), | ||
| 138 | }); | ||
| 139 | const headers: Record<string, string> = { | ||
| 140 | ...readSourceHeaders(input.source), | ||
| 141 | }; | ||
| 142 | const cookies: Record<string, string> = {}; | ||
| 143 | |||
| 144 | const allParameterIds = [ | ||
| 145 | ...(input.executable.pathParameterIds ?? []), | ||
| 146 | ...(input.executable.queryParameterIds ?? []), | ||
| 147 | ...(input.executable.headerParameterIds ?? []), | ||
| 148 | ...(input.executable.cookieParameterIds ?? []), | ||
| 149 | ]; | ||
| 150 | |||
| 151 | for (const parameterId of allParameterIds) { | ||
| 152 | const parameter = parameterById(input.catalog, parameterId); | ||
| 153 | if (!parameter) { | ||
| 154 | continue; | ||
| 155 | } | ||
| 156 | |||
| 157 | const value = readHttpParameterValue({ | ||
| 158 | catalog: input.catalog, | ||
| 159 | executable: input.executable, | ||
| 160 | parameter, | ||
| 161 | args: argsRecord, | ||
| 162 | }); | ||
| 163 | if (value === undefined || value === null) { | ||
| 164 | if (parameter.required) { | ||
| 165 | throw new Error(`Missing required ${parameter.location} parameter ${parameter.name}`); | ||
| 166 | } | ||
| 167 | continue; | ||
| 168 | } | ||
| 169 | |||
| 170 | if (parameter.location === "path") { | ||
| 171 | resolvedPath = resolvedPath.replace( | ||
| 172 | new RegExp(`{${parameter.name}}`, "g"), | ||
| 173 | encodeURIComponent(primitiveString(value)), | ||
| 174 | ); | ||
| 175 | continue; | ||
| 176 | } | ||
| 177 | |||
| 178 | if (parameter.location === "query") { | ||
| 179 | if (Array.isArray(value)) { | ||
| 180 | for (const entry of value) { | ||
| 181 | url.searchParams.append(parameter.name, primitiveString(entry)); | ||
| 182 | } | ||
| 183 | } else { | ||
| 184 | url.searchParams.set(parameter.name, primitiveString(value)); | ||
| 185 | } | ||
| 186 | continue; | ||
| 187 | } | ||
| 188 | |||
| 189 | if (parameter.location === "header") { | ||
| 190 | headers[parameter.name] = primitiveString(value); | ||
| 191 | continue; | ||
| 192 | } | ||
| 193 | |||
| 194 | if (parameter.location === "cookie") { | ||
| 195 | cookies[parameter.name] = primitiveString(value); | ||
| 196 | } | ||
| 197 | } | ||
| 198 | |||
| 199 | const bodySymbol = input.executable.requestBodyId | ||
| 200 | ? input.catalog.symbols[input.executable.requestBodyId] | ||
| 201 | : undefined; | ||
| 202 | let body: string | undefined; | ||
| 203 | if (bodySymbol?.kind === "requestBody") { | ||
| 204 | const bodyValue = argsRecord.body ?? argsRecord.input; | ||
| 205 | if (bodyValue !== undefined) { | ||
| 206 | const contentType = bodySymbol.contents[0]?.mediaType ?? "application/json"; | ||
| 207 | headers["content-type"] = contentType; | ||
| 208 | body = JSON.stringify( | ||
| 209 | applyJsonBodyPlacements({ | ||
| 210 | body: bodyValue, | ||
| 211 | bodyValues: input.auth.bodyValues, | ||
| 212 | label: `${input.executable.method} ${input.executable.pathTemplate}`, | ||
| 213 | }), | ||
| 214 | ); | ||
| 215 | } | ||
| 216 | } | ||
| 217 | |||
| 218 | const requestUrl = new URL(resolvedPath, url); | ||
| 219 | const urlWithAuth = applyHttpQueryPlacementsToUrl({ | ||
| 220 | url: requestUrl, | ||
| 221 | queryParams: input.auth.queryParams, | ||
| 222 | }); | ||
| 223 | const headersWithCookies = applyCookiePlacementsToHeaders({ | ||
| 224 | headers: { | ||
| 225 | ...headers, | ||
| 226 | ...input.auth.headers, | ||
| 227 | }, | ||
| 228 | cookies: { | ||
| 229 | ...cookies, | ||
| 230 | ...input.auth.cookies, | ||
| 231 | }, | ||
| 232 | }); | ||
| 233 | |||
| 234 | const response = await fetch(urlWithAuth, { | ||
| 235 | method: input.executable.method, | ||
| 236 | headers: headersWithCookies, | ||
| 237 | ...(body !== undefined ? { body } : {}), | ||
| 238 | }); | ||
| 239 | const responseBody = await decodeFetchBody(response); | ||
| 240 | if (!response.ok) { | ||
| 241 | throw new Error(`HTTP ${response.status}: ${summarizeHttpBody(responseBody)}`); | ||
| 242 | } | ||
| 243 | |||
| 244 | return responseBody; | ||
| 245 | }, | ||
| 246 | catch: (cause) => cause instanceof Error ? cause : new Error(String(cause)), | ||
| 247 | }); | ||
| 248 | |||
| 249 | const graphqlSelectionFromArgs = (value: unknown): string => { | ||
| 250 | const record = asObject(value); | ||
| 251 | const fields = Object.entries(record).flatMap(([key, entry]) => { | ||
| 252 | if (entry === true) { | ||
| 253 | return [key]; | ||
| 254 | } | ||
| 255 | if (entry && typeof entry === "object") { | ||
| 256 | const child = graphqlSelectionFromArgs(entry); | ||
| 257 | return child.length > 0 ? [`${key} { ${child} }`] : []; | ||
| 258 | } | ||
| 259 | return []; | ||
| 260 | }); | ||
| 261 | return fields.join(" "); | ||
| 262 | }; | ||
| 263 | |||
| 264 | const graphqlArgsPayload = (input: { | ||
| 265 | catalog: CatalogV1; | ||
| 266 | executable: GraphQLExecutable; | ||
| 267 | args: Record<string, unknown>; | ||
| 268 | }): Record<string, unknown> => { | ||
| 269 | const argumentShape = input.catalog.symbols[input.executable.argumentShapeId]; | ||
| 270 | const reserved = new Set(["body", "args", "select", "path", "query", "headers", "cookies"]); | ||
| 271 | if ( | ||
| 272 | argumentShape?.kind === "shape" | ||
| 273 | && argumentShape.node.type === "object" | ||
| 274 | && Object.keys(argumentShape.node.fields).every((name) => !reserved.has(name)) | ||
| 275 | ) { | ||
| 276 | return Object.fromEntries( | ||
| 277 | Object.entries(input.args).filter(([key]) => key !== "select" && key !== "headers"), | ||
| 278 | ); | ||
| 279 | } | ||
| 280 | |||
| 281 | return asObject(input.args.args); | ||
| 282 | }; | ||
| 283 | |||
| 284 | const executeGraphql = (input: { | ||
| 285 | source: Source; | ||
| 286 | catalog: CatalogV1; | ||
| 287 | executable: GraphQLExecutable; | ||
| 288 | auth: ResolvedSourceAuthMaterial; | ||
| 289 | args: unknown; | ||
| 290 | }) => | ||
| 291 | Effect.tryPromise({ | ||
| 292 | try: async () => { | ||
| 293 | const argsRecord = asObject(input.args); | ||
| 294 | const native = input.executable.native?.[0]?.value as Record<string, unknown> | undefined; | ||
| 295 | const requestHeaders = { | ||
| 296 | ...readSourceHeaders(input.source), | ||
| 297 | ...input.auth.headers, | ||
| 298 | ...asStringRecord(argsRecord.headers), | ||
| 299 | "content-type": "application/json", | ||
| 300 | }; | ||
| 301 | const queryParams = { | ||
| 302 | ...readSourceQueryParams(input.source), | ||
| 303 | ...input.auth.queryParams, | ||
| 304 | }; | ||
| 305 | const endpoint = applyHttpQueryPlacementsToUrl({ | ||
| 306 | url: input.source.endpoint, | ||
| 307 | queryParams, | ||
| 308 | }).toString(); | ||
| 309 | |||
| 310 | const toolKind = asString(native?.toolKind); | ||
| 311 | if (toolKind === "request" || !native || !asString(native.operationDocument)) { | ||
| 312 | const query = asString(argsRecord.query); | ||
| 313 | if (query === null) { | ||
| 314 | throw new Error(`GraphQL request tools require args.query`); | ||
| 315 | } | ||
| 316 | |||
| 317 | const response = await fetch(endpoint, { | ||
| 318 | method: "POST", | ||
| 319 | headers: applyCookiePlacementsToHeaders({ | ||
| 320 | headers: requestHeaders, | ||
| 321 | cookies: input.auth.cookies, | ||
| 322 | }), | ||
| 323 | body: JSON.stringify({ | ||
| 324 | query, | ||
| 325 | ...(argsRecord.variables ? { variables: asObject(argsRecord.variables) } : {}), | ||
| 326 | ...(asString(argsRecord.operationName) ? { operationName: asString(argsRecord.operationName)! } : {}), | ||
| 327 | }), | ||
| 328 | }); | ||
| 329 | const body = await decodeFetchBody(response); | ||
| 330 | if (!response.ok) { | ||
| 331 | throw new Error(`GraphQL HTTP ${response.status}: ${summarizeHttpBody(body)}`); | ||
| 332 | } | ||
| 333 | return body; | ||
| 334 | } | ||
| 335 | |||
| 336 | const operationDocument = asString(native.operationDocument)!; | ||
| 337 | const operationName = asString(native.operationName) ?? undefined; | ||
| 338 | const variables = graphqlArgsPayload({ | ||
| 339 | catalog: input.catalog, | ||
| 340 | executable: input.executable, | ||
| 341 | args: argsRecord, | ||
| 342 | }); | ||
| 343 | |||
| 344 | const query = | ||
| 345 | input.executable.selectionMode === "caller" | ||
| 346 | ? (() => { | ||
| 347 | const selection = graphqlSelectionFromArgs(argsRecord.select); | ||
| 348 | if (selection.length === 0) { | ||
| 349 | throw new Error(`GraphQL caller selection tools require args.select`); | ||
| 350 | } | ||
| 351 | return `${input.executable.operationType} ${input.executable.rootField} { ${input.executable.rootField} ${selection ? `{ ${selection} }` : ""} }`; | ||
| 352 | })() | ||
| 353 | : operationDocument; | ||
| 354 | |||
| 355 | const response = await fetch(endpoint, { | ||
| 356 | method: "POST", | ||
| 357 | headers: applyCookiePlacementsToHeaders({ | ||
| 358 | headers: requestHeaders, | ||
| 359 | cookies: input.auth.cookies, | ||
| 360 | }), | ||
| 361 | body: JSON.stringify({ | ||
| 362 | query, | ||
| 363 | variables, | ||
| 364 | ...(operationName ? { operationName } : {}), | ||
| 365 | }), | ||
| 366 | }); | ||
| 367 | const body = asObject(await decodeFetchBody(response)); | ||
| 368 | if (!response.ok) { | ||
| 369 | throw new Error(`GraphQL HTTP ${response.status}: ${summarizeHttpBody(body)}`); | ||
| 370 | } | ||
| 371 | |||
| 372 | const errors = Array.isArray(body.errors) ? body.errors : []; | ||
| 373 | const data = asObject(body.data); | ||
| 374 | const fieldName = asString(native.fieldName) ?? input.executable.rootField; | ||
| 375 | return { | ||
| 376 | data: data[fieldName] ?? null, | ||
| 377 | errors, | ||
| 378 | isError: errors.length > 0, | ||
| 379 | }; | ||
| 380 | }, | ||
| 381 | catch: (cause) => cause instanceof Error ? cause : new Error(String(cause)), | ||
| 382 | }); | ||
| 383 | |||
| 384 | const executeMcp = (input: { | ||
| 385 | source: Source; | ||
| 386 | catalog: CatalogV1; | ||
| 387 | executable: McpExecutable; | ||
| 388 | auth: ResolvedSourceAuthMaterial; | ||
| 389 | args: unknown; | ||
| 390 | }) => | ||
| 391 | Effect.tryPromise({ | ||
| 392 | try: async () => { | ||
| 393 | const inputShape = input.executable.inputShapeId | ||
| 394 | ? input.catalog.symbols[input.executable.inputShapeId] | ||
| 395 | : undefined; | ||
| 396 | const payload = | ||
| 397 | inputShape?.kind === "shape" && inputShape.node.type !== "object" | ||
| 398 | ? asObject(input.args).input | ||
| 399 | : input.args; | ||
| 400 | const connector = createSdkMcpConnector({ | ||
| 401 | endpoint: input.source.endpoint, | ||
| 402 | transport: readSourceTransport(input.source), | ||
| 403 | queryParams: { | ||
| 404 | ...readSourceQueryParams(input.source), | ||
| 405 | ...input.auth.queryParams, | ||
| 406 | }, | ||
| 407 | headers: applyCookiePlacementsToHeaders({ | ||
| 408 | headers: { | ||
| 409 | ...readSourceHeaders(input.source), | ||
| 410 | ...input.auth.headers, | ||
| 411 | }, | ||
| 412 | cookies: input.auth.cookies, | ||
| 413 | }), | ||
| 414 | }); | ||
| 415 | const connection = await connector(); | ||
| 416 | try { | ||
| 417 | return await connection.client.callTool({ | ||
| 418 | name: input.executable.toolName, | ||
| 419 | arguments: asObject(payload), | ||
| 420 | }); | ||
| 421 | } finally { | ||
| 422 | await connection.close?.(); | ||
| 423 | } | ||
| 424 | }, | ||
| 425 | catch: (cause) => cause instanceof Error ? cause : new Error(String(cause)), | ||
| 426 | }); | ||
| 427 | |||
| 428 | export const invocationDescriptorFromTool = (input: { | ||
| 429 | tool: LoadedSourceCatalogToolIndexEntry; | ||
| 430 | }): { | ||
| 431 | toolPath: string; | ||
| 432 | sourceId: Source["id"]; | ||
| 433 | sourceName: Source["name"]; | ||
| 434 | sourceKind: Source["kind"]; | ||
| 435 | sourceNamespace: string | null; | ||
| 436 | operationKind: "read" | "write" | "delete" | "execute" | "unknown"; | ||
| 437 | interaction: "auto" | "required"; | ||
| 438 | approvalLabel: string | null; | ||
| 439 | } => ({ | ||
| 440 | toolPath: input.tool.path, | ||
| 441 | sourceId: input.tool.source.id, | ||
| 442 | sourceName: input.tool.source.name, | ||
| 443 | sourceKind: input.tool.source.kind, | ||
| 444 | sourceNamespace: | ||
| 445 | input.tool.source.namespace ?? null, | ||
| 446 | operationKind: | ||
| 447 | input.tool.capability.semantics.effect === "read" | ||
| 448 | ? "read" | ||
| 449 | : input.tool.capability.semantics.effect === "write" | ||
| 450 | ? "write" | ||
| 451 | : input.tool.capability.semantics.effect === "delete" | ||
| 452 | ? "delete" | ||
| 453 | : input.tool.capability.semantics.effect === "action" | ||
| 454 | ? "execute" | ||
| 455 | : "unknown", | ||
| 456 | interaction: input.tool.descriptor.interaction ?? "auto", | ||
| 457 | approvalLabel: input.tool.capability.surface.title ?? null, | ||
| 458 | }); | ||
| 459 | |||
| 460 | export const invokeIrTool = (input: { | ||
| 461 | workspaceId: Source["workspaceId"]; | ||
| 462 | accountId: AccountId; | ||
| 463 | tool: LoadedSourceCatalogToolIndexEntry; | ||
| 464 | auth: ResolvedSourceAuthMaterial; | ||
| 465 | args: unknown; | ||
| 466 | context?: Record<string, unknown>; | ||
| 467 | }) => { | ||
| 468 | switch (input.tool.executable.protocol) { | ||
| 469 | case "http": | ||
| 470 | return executeHttp({ | ||
| 471 | source: input.tool.source, | ||
| 472 | catalog: input.tool.projectedCatalog, | ||
| 473 | executable: input.tool.executable, | ||
| 474 | auth: input.auth, | ||
| 475 | args: input.args, | ||
| 476 | }); | ||
| 477 | case "graphql": | ||
| 478 | return executeGraphql({ | ||
| 479 | source: input.tool.source, | ||
| 480 | catalog: input.tool.projectedCatalog, | ||
| 481 | executable: input.tool.executable, | ||
| 482 | auth: input.auth, | ||
| 483 | args: input.args, | ||
| 484 | }); | ||
| 485 | case "mcp": | ||
| 486 | return executeMcp({ | ||
| 487 | source: input.tool.source, | ||
| 488 | catalog: input.tool.projectedCatalog, | ||
| 489 | executable: input.tool.executable, | ||
| 490 | auth: input.auth, | ||
| 491 | args: input.args, | ||
| 492 | }); | ||
| 493 | } | ||
| 494 | }; | ||
| @@ -1,4 +1,4 @@ | |||
| 1 | import { describe, expect, it } from "vitest"; | 1 | import { describe, expect, it } from "@effect/vitest"; |
| 2 | 2 | ||
| 3 | import { sanitizePersistedElicitationResponse } from "./live-execution"; | 3 | import { sanitizePersistedElicitationResponse } from "./live-execution"; |
| 4 | 4 | ||
| @@ -3,18 +3,17 @@ import { FileSystem } from "@effect/platform"; · 2 unmodified lines | |||
| 3 | 3 | ||
| 4 | import { | 4 | import { |
| 5 | SourceIdSchema, | 5 | SourceIdSchema, |
| 6 | SourceRecipeIdSchema, | 6 | SourceCatalogIdSchema, |
| 7 | StoredSourceRecipeDocumentRecordSchema, | 7 | StoredSourceCatalogRevisionRecordSchema, |
| 8 | StoredSourceRecipeOperationRecordSchema, | ||
| 9 | StoredSourceRecipeRevisionRecordSchema, | ||
| 10 | StoredSourceRecipeSchemaBundleRecordSchema, | ||
| 11 | TimestampMsSchema, | 8 | TimestampMsSchema, |
| 12 | type Source, | 9 | type Source, |
| 13 | type SourceRecipeId, | 10 | type SourceCatalogId, |
| 14 | } from "#schema"; | 11 | } from "#schema"; |
| 15 | import * as Effect from "effect/Effect"; | 12 | import * as Effect from "effect/Effect"; |
| 16 | import * as Schema from "effect/Schema"; | 13 | import * as Schema from "effect/Schema"; |
| 17 | 14 | ||
| 15 | import { CatalogSnapshotV1Schema } from "../ir/model"; | ||
| 16 | import type { SourceCatalogSyncResult } from "./source-catalog-support"; | ||
| 18 | import type { ResolvedLocalWorkspaceContext } from "./local-config"; | 17 | import type { ResolvedLocalWorkspaceContext } from "./local-config"; |
| 19 | import { | 18 | import { |
| 20 | LocalFileSystemError, | 19 | LocalFileSystemError, |
| @@ -22,25 +21,20 @@ import { · 1 unmodified line | |||
| 22 | unknownLocalErrorDetails, | 21 | unknownLocalErrorDetails, |
| 23 | } from "./local-errors"; | 22 | } from "./local-errors"; |
| 24 | import { | 23 | import { |
| 25 | createSourceRecipeRevisionRecord, | 24 | createSourceCatalogRevisionRecord, |
| 26 | stableSourceRecipeId, | 25 | stableSourceCatalogId, |
| 27 | } from "./source-definitions"; | 26 | } from "./source-definitions"; |
| 28 | import { | 27 | import { contentHash } from "./source-catalog-support"; |
| 29 | contentHash, | ||
| 30 | type SourceRecipeMaterialization, | ||
| 31 | } from "./source-recipe-support"; | ||
| 32 | 28 | ||
| 33 | const LOCAL_SOURCE_ARTIFACT_VERSION = 1 as const; | 29 | const LOCAL_SOURCE_ARTIFACT_VERSION = 2 as const; |
| 34 | 30 | ||
| 35 | export const LocalSourceArtifactSchema = Schema.Struct({ | 31 | export const LocalSourceArtifactSchema = Schema.Struct({ |
| 36 | version: Schema.Literal(LOCAL_SOURCE_ARTIFACT_VERSION), | 32 | version: Schema.Literal(LOCAL_SOURCE_ARTIFACT_VERSION), |
| 37 | sourceId: SourceIdSchema, | 33 | sourceId: SourceIdSchema, |
| 38 | recipeId: SourceRecipeIdSchema, | 34 | catalogId: SourceCatalogIdSchema, |
| 39 | generatedAt: TimestampMsSchema, | 35 | generatedAt: TimestampMsSchema, |
| 40 | revision: StoredSourceRecipeRevisionRecordSchema, | 36 | revision: StoredSourceCatalogRevisionRecordSchema, |
| 41 | documents: Schema.Array(StoredSourceRecipeDocumentRecordSchema), | 37 | snapshot: CatalogSnapshotV1Schema, |
| 42 | schemaBundles: Schema.Array(StoredSourceRecipeSchemaBundleRecordSchema), | ||
| 43 | operations: Schema.Array(StoredSourceRecipeOperationRecordSchema), | ||
| 44 | }); | 38 | }); |
| 45 | 39 | ||
| 46 | export type LocalSourceArtifact = typeof LocalSourceArtifactSchema.Type; | 40 | export type LocalSourceArtifact = typeof LocalSourceArtifactSchema.Type; |
| @@ -65,90 +59,32 @@ const localSourceArtifactPath = (input: { · 18 unmodified lines | |||
| 65 | `${input.sourceId}.json`, | 59 | `${input.sourceId}.json`, |
| 66 | ); | 60 | ); |
| 67 | 61 | ||
| 68 | const canonicalMaterializationHash = (input: { | 62 | const snapshotHash = (snapshot: SourceCatalogSyncResult["snapshot"]): string => |
| 69 | materialization: SourceRecipeMaterialization; | 63 | contentHash(JSON.stringify(snapshot)); |
| 70 | }): string => { | ||
| 71 | const documents = [...input.materialization.documents] | ||
| 72 | .map((document) => ({ | ||
| 73 | documentKind: document.documentKind, | ||
| 74 | documentKey: document.documentKey, | ||
| 75 | contentHash: document.contentHash, | ||
| 76 | })) | ||
| 77 | .sort((left, right) => | ||
| 78 | left.documentKind.localeCompare(right.documentKind) | ||
| 79 | || left.documentKey.localeCompare(right.documentKey) | ||
| 80 | || left.contentHash.localeCompare(right.contentHash) | ||
| 81 | ); | ||
| 82 | const schemaBundles = [...input.materialization.schemaBundles] | ||
| 83 | .map((bundle) => ({ | ||
| 84 | bundleKind: bundle.bundleKind, | ||
| 85 | contentHash: bundle.contentHash, | ||
| 86 | })) | ||
| 87 | .sort((left, right) => | ||
| 88 | left.bundleKind.localeCompare(right.bundleKind) | ||
| 89 | || left.contentHash.localeCompare(right.contentHash) | ||
| 90 | ); | ||
| 91 | const operations = [...input.materialization.operations] | ||
| 92 | .map((operation) => ({ | ||
| 93 | operationKey: operation.operationKey, | ||
| 94 | transportKind: operation.transportKind, | ||
| 95 | toolId: operation.toolId, | ||
| 96 | title: operation.title, | ||
| 97 | description: operation.description, | ||
| 98 | operationKind: operation.operationKind, | ||
| 99 | searchText: operation.searchText, | ||
| 100 | inputSchemaJson: operation.inputSchemaJson, | ||
| 101 | outputSchemaJson: operation.outputSchemaJson, | ||
| 102 | providerKind: operation.providerKind, | ||
| 103 | providerDataJson: operation.providerDataJson, | ||
| 104 | })) | ||
| 105 | .sort((left, right) => left.operationKey.localeCompare(right.operationKey)); | ||
| 106 | |||
| 107 | return contentHash(JSON.stringify({ | ||
| 108 | schemaVersion: 1, | ||
| 109 | manifestHash: input.materialization.manifestHash, | ||
| 110 | manifestJson: input.materialization.manifestJson, | ||
| 111 | documents, | ||
| 112 | schemaBundles, | ||
| 113 | operations, | ||
| 114 | })); | ||
| 115 | }; | ||
| 116 | |||
| 117 | const bindRevisionId = <T extends { recipeRevisionId: string }>( | ||
| 118 | items: readonly T[], | ||
| 119 | recipeRevisionId: string, | ||
| 120 | ): T[] => | ||
| 121 | items.map((item) => ({ | ||
| 122 | ...item, | ||
| 123 | recipeRevisionId, | ||
| 124 | })); | ||
| 125 | 64 | ||
| 126 | export const buildLocalSourceArtifact = (input: { | 65 | export const buildLocalSourceArtifact = (input: { |
| 127 | source: Source; | 66 | source: Source; |
| 128 | materialization: SourceRecipeMaterialization; | 67 | syncResult: SourceCatalogSyncResult; |
| 129 | }): LocalSourceArtifact => { | 68 | }): LocalSourceArtifact => { |
| 130 | const recipeId: SourceRecipeId = stableSourceRecipeId(input.source); | 69 | const catalogId: SourceCatalogId = stableSourceCatalogId(input.source); |
| 131 | const now = Date.now(); | 70 | const generatedAt = Date.now(); |
| 132 | const revision = createSourceRecipeRevisionRecord({ | 71 | const hash = snapshotHash(input.syncResult.snapshot); |
| 72 | const revision = createSourceCatalogRevisionRecord({ | ||
| 133 | source: input.source, | 73 | source: input.source, |
| 134 | recipeId, | 74 | catalogId, |
| 135 | revisionNumber: 1, | 75 | revisionNumber: 1, |
| 136 | manifestJson: input.materialization.manifestJson, | 76 | importMetadataJson: JSON.stringify(input.syncResult.snapshot.import), |
| 137 | manifestHash: input.materialization.manifestHash, | 77 | importMetadataHash: hash, |
| 138 | materializationHash: canonicalMaterializationHash({ | 78 | snapshotHash: hash, |
| 139 | materialization: input.materialization, | ||
| 140 | }), | ||
| 141 | }); | 79 | }); |
| 142 | 80 | ||
| 143 | return { | 81 | return { |
| 144 | version: LOCAL_SOURCE_ARTIFACT_VERSION, | 82 | version: LOCAL_SOURCE_ARTIFACT_VERSION, |
| 145 | sourceId: input.source.id, | 83 | sourceId: input.source.id, |
| 146 | recipeId, | 84 | catalogId, |
| 147 | generatedAt: now, | 85 | generatedAt, |
| 148 | revision, | 86 | revision, |
| 149 | documents: bindRevisionId(input.materialization.documents, revision.id), | 87 | snapshot: input.syncResult.snapshot, |
| 150 | schemaBundles: bindRevisionId(input.materialization.schemaBundles, revision.id), | ||
| 151 | operations: bindRevisionId(input.materialization.operations, revision.id), | ||
| 152 | }; | 88 | }; |
| 153 | }; | 89 | }; |
| 154 | 90 | ||
| @@ -175,13 +111,12 @@ export const readLocalSourceArtifact = (input: { · 20 unmodified lines | |||
| 175 | ); | 111 | ); |
| 176 | return yield* Effect.try({ | 112 | return yield* Effect.try({ |
| 177 | try: () => decodeLocalSourceArtifact(JSON.parse(content) as unknown), | 113 | try: () => decodeLocalSourceArtifact(JSON.parse(content) as unknown), |
| 178 | catch: (cause) => { | 114 | catch: (cause) => |
| 179 | return new LocalSourceArtifactDecodeError({ | 115 | new LocalSourceArtifactDecodeError({ |
| 180 | message: `Invalid local source artifact at ${path}: ${unknownLocalErrorDetails(cause)}`, | 116 | message: `Invalid local source artifact at ${path}: ${unknownLocalErrorDetails(cause)}`, |
| 181 | path, | 117 | path, |
| 182 | details: unknownLocalErrorDetails(cause), | 118 | details: unknownLocalErrorDetails(cause), |
| 183 | }); | 119 | }), |
| 184 | }, | ||
| 185 | }); | 120 | }); |
| 186 | }); | 121 | }); |
| 187 | 122 | ||
| @@ -32,7 +32,7 @@ import { · 31 unmodified lines | |||
| 32 | loadLocalWorkspaceState, | 32 | loadLocalWorkspaceState, |
| 33 | writeLocalWorkspaceState, | 33 | writeLocalWorkspaceState, |
| 34 | } from "./local-workspace-state"; | 34 | } from "./local-workspace-state"; |
| 35 | import type { SourceRecipeMaterialization } from "./source-recipe-support"; | 35 | import type { SourceCatalogSyncResult } from "./source-catalog-support"; |
| 36 | import type { Source } from "#schema"; | 36 | import type { Source } from "#schema"; |
| 37 | 37 | ||
| 38 | export type InstallationStoreShape = { | 38 | export type InstallationStoreShape = { |
| @@ -80,7 +80,7 @@ export class WorkspaceStateStore extends Context.Tag( · 41 unmodified lines | |||
| 80 | export type SourceArtifactStoreShape = { | 80 | export type SourceArtifactStoreShape = { |
| 81 | build: (input: { | 81 | build: (input: { |
| 82 | source: Source; | 82 | source: Source; |
| 83 | materialization: SourceRecipeMaterialization; | 83 | syncResult: SourceCatalogSyncResult; |
| 84 | }) => LocalSourceArtifact; | 84 | }) => LocalSourceArtifact; |
| 85 | read: (input: { | 85 | read: (input: { |
| 86 | context: ResolvedLocalWorkspaceContext; | 86 | context: ResolvedLocalWorkspaceContext; |
| @@ -1,6 +1,6 @@ | |||
| 1 | import { createServer } from "node:http"; | 1 | import { createServer } from "node:http"; |
| 2 | 2 | ||
| 3 | import { describe, expect, it } from "vitest"; | 3 | import { describe, expect, it } from "@effect/vitest"; |
| 4 | import * as Effect from "effect/Effect"; | 4 | import * as Effect from "effect/Effect"; |
| 5 | 5 | ||
| 6 | import { startOauthLoopbackRedirectServer } from "./oauth-loopback"; | 6 | import { startOauthLoopbackRedirectServer } from "./oauth-loopback"; |
| @@ -56,7 +56,7 @@ const withStaticServer = async <T>(content: string, handler: (baseUrl: string) = · 55 unmodified lines | |||
| 56 | }; | 56 | }; |
| 57 | 57 | ||
| 58 | describe("google discovery source adapter", () => { | 58 | describe("google discovery source adapter", () => { |
| 59 | it("materializes a real Google Sheets discovery doc into recipe content", () => | 59 | it("syncs a real Google Sheets discovery doc into snapshot content", () => |
| 60 | Effect.tryPromise({ | 60 | Effect.tryPromise({ |
| 61 | try: async () => { | 61 | try: async () => { |
| 62 | const discoveryDocument = await fetchLiveDiscoveryDocument(); | 62 | const discoveryDocument = await fetchLiveDiscoveryDocument(); |
| @@ -86,8 +86,8 @@ describe("google discovery source adapter", () => { · 23 unmodified lines | |||
| 86 | }), | 86 | }), |
| 87 | ); | 87 | ); |
| 88 | 88 | ||
| 89 | const materialization = await Effect.runPromise( | 89 | const syncResult = await Effect.runPromise( |
| 90 | googleDiscoverySourceAdapter.materializeSource({ | 90 | googleDiscoverySourceAdapter.syncCatalog({ |
| 91 | source, | 91 | source, |
| 92 | resolveSecretMaterial: () => Effect.fail(new Error("unexpected secret lookup")), | 92 | resolveSecretMaterial: () => Effect.fail(new Error("unexpected secret lookup")), |
| 93 | resolveAuthMaterialForSlot: () => | 93 | resolveAuthMaterialForSlot: () => |
| @@ -103,12 +103,12 @@ describe("google discovery source adapter", () => { · 9 unmodified lines | |||
| 103 | }), | 103 | }), |
| 104 | ); | 104 | ); |
| 105 | 105 | ||
| 106 | expect(materialization.documents[0]?.documentKind).toBe("google_discovery"); | 106 | expect(Object.values(syncResult.snapshot.catalog.documents)[0]?.kind).toBe("google-discovery"); |
| 107 | expect(materialization.schemaBundles.length).toBeGreaterThan(0); | 107 | expect(Object.keys(syncResult.snapshot.catalog.resources)).not.toHaveLength(0); |
| 108 | expect(materialization.operations.length).toBeGreaterThan(50); | 108 | expect(Object.keys(syncResult.snapshot.catalog.capabilities).length).toBeGreaterThan(50); |
| 109 | expect( | 109 | expect( |
| 110 | materialization.operations.some( | 110 | Object.values(syncResult.snapshot.catalog.capabilities).some((capability) => |
| 111 | (operation) => operation.toolId === "spreadsheets.values.get", | 111 | capability.surface.toolPath.join(".") === "google.sheets.spreadsheets.values.get" |
| 112 | ), | 112 | ), |
| 113 | ).toBe(true); | 113 | ).toBe(true); |
| 114 | }); | 114 | }); |
| @@ -3,55 +3,32 @@ import { · 2 unmodified lines | |||
| 3 | HttpClient, | 3 | HttpClient, |
| 4 | HttpClientRequest, | 4 | HttpClientRequest, |
| 5 | } from "@effect/platform"; | 5 | } from "@effect/platform"; |
| 6 | import { | ||
| 7 | allowAllToolInteractions, | ||
| 8 | makeToolInvokerFromTools, | ||
| 9 | typeSignatureFromSchemaJson, | ||
| 10 | } from "@executor/codemode-core"; | ||
| 11 | import { | 6 | import { |
| 12 | compileGoogleDiscoveryToolDefinitions, | 7 | compileGoogleDiscoveryToolDefinitions, |
| 13 | createGoogleDiscoveryToolFromDefinition, | ||
| 14 | decodeGoogleDiscoverySchemaRefTableJson, | ||
| 15 | extractGoogleDiscoveryManifest, | 8 | extractGoogleDiscoveryManifest, |
| 16 | googleDiscoveryProviderDataJsonFromDefinition, | 9 | type GoogleDiscoveryToolProviderData, |
| 17 | GoogleDiscoveryToolProviderDataSchema, | 10 | googleDiscoveryProviderDataFromDefinition, |
| 18 | type GoogleDiscoveryToolManifest, | ||
| 19 | } from "@executor/codemode-google-discovery"; | 11 | } from "@executor/codemode-google-discovery"; |
| 20 | import type { | 12 | import type { Source } from "#schema"; |
| 21 | Source, | 13 | import { StringMapSchema } from "#schema"; |
| 22 | SourceRecipeRevisionId, | ||
| 23 | StoredSourceRecipeDocumentRecord, | ||
| 24 | StoredSourceRecipeOperationRecord, | ||
| 25 | StoredSourceRecipeSchemaBundleRecord, | ||
| 26 | } from "#schema"; | ||
| 27 | import { | ||
| 28 | SourceRecipeSchemaBundleIdSchema, | ||
| 29 | StringMapSchema, | ||
| 30 | } from "#schema"; | ||
| 31 | import * as Either from "effect/Either"; | ||
| 32 | import * as Effect from "effect/Effect"; | 14 | import * as Effect from "effect/Effect"; |
| 33 | import * as Schema from "effect/Schema"; | 15 | import * as Schema from "effect/Schema"; |
| 34 | 16 | ||
| 35 | import { | 17 | import { |
| 36 | contentHash, | 18 | createGoogleDiscoveryCatalogSnapshot, |
| 37 | normalizeSearchText, | 19 | type GoogleDiscoveryCatalogOperationInput, |
| 38 | } from "../source-recipe-support"; | 20 | } from "../source-catalog-snapshot"; |
| 39 | import type { | 21 | import type { SourceAdapter } from "./types"; |
| 40 | SourceAdapter, | ||
| 41 | SourceAdapterMaterialization, | ||
| 42 | } from "./types"; | ||
| 43 | import { | 22 | import { |
| 44 | ConnectHttpAuthSchema, | 23 | ConnectHttpAuthSchema, |
| 45 | ConnectHttpImportAuthSchema, | 24 | ConnectHttpImportAuthSchema, |
| 46 | ConnectOauthClientSchema, | 25 | ConnectOauthClientSchema, |
| 47 | createStandardToolDescriptor, | ||
| 48 | decodeBindingConfig, | 26 | decodeBindingConfig, |
| 49 | decodeSourceBindingPayload, | 27 | decodeSourceBindingPayload, |
| 50 | emptySourceBindingState, | 28 | emptySourceBindingState, |
| 51 | encodeBindingConfig, | 29 | encodeBindingConfig, |
| 52 | isSourceCredentialRequiredError, | 30 | isSourceCredentialRequiredError, |
| 53 | OptionalNullableStringSchema, | 31 | OptionalNullableStringSchema, |
| 54 | parseJsonValue, | ||
| 55 | SourceCredentialRequiredError, | 32 | SourceCredentialRequiredError, |
| 56 | } from "./shared"; | 33 | } from "./shared"; |
| 57 | 34 | ||
| @@ -97,13 +74,6 @@ const GoogleDiscoverySourceBindingPayloadSchema = Schema.Struct({ · 39 unmodified lines | |||
| 97 | type GoogleDiscoveryBindingConfig = typeof GoogleDiscoveryBindingConfigSchema.Type; | 74 | type GoogleDiscoveryBindingConfig = typeof GoogleDiscoveryBindingConfigSchema.Type; |
| 98 | 75 | ||
| 99 | const GOOGLE_DISCOVERY_BINDING_CONFIG_VERSION = 1; | 76 | const GOOGLE_DISCOVERY_BINDING_CONFIG_VERSION = 1; |
| 100 | const GOOGLE_DISCOVERY_MATERIALIZATION_REVISION_ID = | ||
| 101 | "src_recipe_rev_materialization" as SourceRecipeRevisionId; | ||
| 102 | |||
| 103 | const decodeGoogleDiscoveryProviderDataJson = Schema.decodeUnknownEither( | ||
| 104 | Schema.parseJson(GoogleDiscoveryToolProviderDataSchema), | ||
| 105 | ); | ||
| 106 | |||
| 107 | const bindingHasAnyField = ( | 77 | const bindingHasAnyField = ( |
| 108 | value: unknown, | 78 | value: unknown, |
| 109 | fields: readonly string[], | 79 | fields: readonly string[], |
| @@ -205,129 +175,30 @@ const fetchGoogleDiscoveryDocumentWithHeaders = (input: { · 95 unmodified lines | |||
| 205 | ); | 175 | ); |
| 206 | }).pipe(Effect.provide(FetchHttpClient.layer)); | 176 | }).pipe(Effect.provide(FetchHttpClient.layer)); |
| 207 | 177 | ||
| 208 | const toGoogleDiscoverySchemaBundleRecord = (input: { | 178 | const googleDiscoveryCatalogOperationFromDefinition = (input: { |
| 209 | recipeRevisionId: SourceRecipeRevisionId; | 179 | manifest: Parameters<typeof compileGoogleDiscoveryToolDefinitions>[0]; |
| 210 | refTable: Readonly<Record<string, string>> | undefined; | ||
| 211 | now: number; | ||
| 212 | }): StoredSourceRecipeSchemaBundleRecord | null => { | ||
| 213 | const refEntries = Object.entries(input.refTable ?? {}); | ||
| 214 | if (refEntries.length === 0) { | ||
| 215 | return null; | ||
| 216 | } | ||
| 217 | |||
| 218 | const refs = Object.fromEntries( | ||
| 219 | refEntries.map(([ref, rawValue]) => { | ||
| 220 | try { | ||
| 221 | return [ref, JSON.parse(rawValue) as unknown]; | ||
| 222 | } catch { | ||
| 223 | return [ref, rawValue]; | ||
| 224 | } | ||
| 225 | }), | ||
| 226 | ); | ||
| 227 | const refsJson = JSON.stringify(refs); | ||
| 228 | |||
| 229 | return { | ||
| 230 | id: SourceRecipeSchemaBundleIdSchema.make( | ||
| 231 | `src_recipe_bundle_${crypto.randomUUID()}`, | ||
| 232 | ), | ||
| 233 | recipeRevisionId: input.recipeRevisionId, | ||
| 234 | bundleKind: "json_schema_ref_map", | ||
| 235 | refsJson, | ||
| 236 | contentHash: contentHash(refsJson), | ||
| 237 | createdAt: input.now, | ||
| 238 | updatedAt: input.now, | ||
| 239 | }; | ||
| 240 | }; | ||
| 241 | |||
| 242 | const toGoogleDiscoveryRecipeOperationRecord = (input: { | ||
| 243 | recipeRevisionId: SourceRecipeRevisionId; | ||
| 244 | manifest: GoogleDiscoveryToolManifest; | ||
| 245 | definition: ReturnType<typeof compileGoogleDiscoveryToolDefinitions>[number]; | 180 | definition: ReturnType<typeof compileGoogleDiscoveryToolDefinitions>[number]; |
| 246 | now: number; | 181 | }): GoogleDiscoveryCatalogOperationInput => ({ |
| 247 | }): StoredSourceRecipeOperationRecord => ({ | ||
| 248 | id: `src_recipe_op_${crypto.randomUUID()}`, | ||
| 249 | recipeRevisionId: input.recipeRevisionId, | ||
| 250 | operationKey: input.definition.toolId, | ||
| 251 | transportKind: "http", | ||
| 252 | toolId: input.definition.toolId, | 182 | toolId: input.definition.toolId, |
| 253 | title: input.definition.name, | 183 | title: input.definition.name, |
| 254 | description: input.definition.description, | 184 | description: input.definition.description, |
| 255 | operationKind: | 185 | effect: |
| 256 | input.definition.method === "get" || input.definition.method === "head" | 186 | input.definition.method === "get" || input.definition.method === "head" |
| 257 | ? "read" | 187 | ? "read" |
| 258 | : input.definition.method === "delete" | 188 | : input.definition.method === "delete" |
| 259 | ? "delete" | 189 | ? "delete" |
| 260 | : "write", | 190 | : "write", |
| 261 | searchText: normalizeSearchText( | 191 | inputSchema: input.definition.inputSchema, |
| 262 | input.definition.toolId, | 192 | outputSchema: input.definition.outputSchema, |
| 263 | input.definition.rawToolId, | 193 | providerData: googleDiscoveryProviderDataFromDefinition({ |
| 264 | input.definition.methodId, | ||
| 265 | input.definition.name, | ||
| 266 | input.definition.description ?? undefined, | ||
| 267 | input.definition.method, | ||
| 268 | input.definition.path, | ||
| 269 | input.definition.group ?? undefined, | ||
| 270 | input.definition.leaf, | ||
| 271 | input.manifest.service, | ||
| 272 | input.manifest.versionName, | ||
| 273 | input.definition.scopes.join(" "), | ||
| 274 | ), | ||
| 275 | inputSchemaJson: input.definition.inputSchemaJson ?? null, | ||
| 276 | outputSchemaJson: input.definition.outputSchemaJson ?? null, | ||
| 277 | providerKind: "google_discovery", | ||
| 278 | providerDataJson: googleDiscoveryProviderDataJsonFromDefinition({ | ||
| 279 | service: input.manifest.service, | 194 | service: input.manifest.service, |
| 280 | version: input.manifest.versionName, | 195 | version: input.manifest.versionName, |
| 281 | rootUrl: input.manifest.rootUrl, | 196 | rootUrl: input.manifest.rootUrl, |
| 282 | servicePath: input.manifest.servicePath, | 197 | servicePath: input.manifest.servicePath, |
| 283 | definition: input.definition, | 198 | definition: input.definition, |
| 284 | }), | 199 | }) as GoogleDiscoveryToolProviderData, |
| 285 | createdAt: input.now, | ||
| 286 | updatedAt: input.now, | ||
| 287 | }); | 200 | }); |
| 288 | 201 | ||
| 289 | const googleDiscoveryDefinitionFromPersistedOperation = (input: { | ||
| 290 | path: string; | ||
| 291 | operation: StoredSourceRecipeOperationRecord; | ||
| 292 | }) => { | ||
| 293 | const decoded = input.operation.providerDataJson | ||
| 294 | ? decodeGoogleDiscoveryProviderDataJson(input.operation.providerDataJson) | ||
| 295 | : Either.left(new Error("Missing providerDataJson")); | ||
| 296 | |||
| 297 | if (Either.isLeft(decoded)) { | ||
| 298 | throw new Error(`Invalid Google Discovery provider data for ${input.path}`); | ||
| 299 | } | ||
| 300 | |||
| 301 | const providerData = decoded.right; | ||
| 302 | return { | ||
| 303 | definition: { | ||
| 304 | toolId: input.operation.toolId, | ||
| 305 | rawToolId: providerData.rawToolId, | ||
| 306 | methodId: providerData.methodId, | ||
| 307 | name: input.operation.title ?? input.operation.toolId, | ||
| 308 | description: input.operation.description, | ||
| 309 | group: providerData.group, | ||
| 310 | leaf: providerData.leaf, | ||
| 311 | method: providerData.invocation.method, | ||
| 312 | path: providerData.invocation.path, | ||
| 313 | flatPath: providerData.invocation.flatPath, | ||
| 314 | parameters: providerData.invocation.parameters, | ||
| 315 | requestSchemaId: providerData.invocation.requestSchemaId, | ||
| 316 | responseSchemaId: providerData.invocation.responseSchemaId, | ||
| 317 | scopes: providerData.invocation.scopes, | ||
| 318 | supportsMediaUpload: providerData.invocation.supportsMediaUpload, | ||
| 319 | supportsMediaDownload: providerData.invocation.supportsMediaDownload, | ||
| 320 | ...(input.operation.inputSchemaJson | ||
| 321 | ? { inputSchemaJson: input.operation.inputSchemaJson } | ||
| 322 | : {}), | ||
| 323 | ...(input.operation.outputSchemaJson | ||
| 324 | ? { outputSchemaJson: input.operation.outputSchemaJson } | ||
| 325 | : {}), | ||
| 326 | }, | ||
| 327 | providerData, | ||
| 328 | }; | ||
| 329 | }; | ||
| 330 | |||
| 331 | const googleDiscoveryOauth2SetupConfig = (source: Source) => | 202 | const googleDiscoveryOauth2SetupConfig = (source: Source) => |
| 332 | Effect.gen(function* () { | 203 | Effect.gen(function* () { |
| 333 | const bindingConfig = yield* googleDiscoveryBindingConfigFromSource(source); | 204 | const bindingConfig = yield* googleDiscoveryBindingConfigFromSource(source); |
| @@ -369,8 +240,6 @@ export const googleDiscoverySourceAdapter: SourceAdapter = { · 35 unmodified lines | |||
| 369 | bindingConfigVersion: GOOGLE_DISCOVERY_BINDING_CONFIG_VERSION, | 240 | bindingConfigVersion: GOOGLE_DISCOVERY_BINDING_CONFIG_VERSION, |
| 370 | providerKey: "google_workspace", | 241 | providerKey: "google_workspace", |
| 371 | defaultImportAuthPolicy: "reuse_runtime", | 242 | defaultImportAuthPolicy: "reuse_runtime", |
| 372 | primaryDocumentKind: "google_discovery", | ||
| 373 | primarySchemaBundleKind: "json_schema_ref_map", | ||
| 374 | connectPayloadSchema: GoogleDiscoveryConnectPayloadSchema, | 243 | connectPayloadSchema: GoogleDiscoveryConnectPayloadSchema, |
| 375 | executorAddInputSchema: GoogleDiscoveryExecutorAddInputSchema, | 244 | executorAddInputSchema: GoogleDiscoveryExecutorAddInputSchema, |
| 376 | executorAddHelpText: [ | 245 | executorAddHelpText: [ |
| @@ -438,71 +307,7 @@ export const googleDiscoverySourceAdapter: SourceAdapter = { · 61 unmodified lines | |||
| 438 | }), | 307 | }), |
| 439 | shouldAutoProbe: (source) => | 308 | shouldAutoProbe: (source) => |
| 440 | source.enabled && (source.status === "draft" || source.status === "probing"), | 309 | source.enabled && (source.status === "draft" || source.status === "probing"), |
| 441 | parseManifest: ({ source, manifestJson }) => | 310 | syncCatalog: ({ source, resolveAuthMaterialForSlot }) => |
| 442 | parseJsonValue<GoogleDiscoveryToolManifest>({ | ||
| 443 | label: `Google Discovery manifest for ${source.id}`, | ||
| 444 | value: manifestJson, | ||
| 445 | }), | ||
| 446 | describePersistedOperation: ({ path, operation }) => | ||
| 447 | Effect.gen(function* () { | ||
| 448 | const decoded = operation.providerDataJson | ||
| 449 | ? decodeGoogleDiscoveryProviderDataJson(operation.providerDataJson) | ||
| 450 | : Either.left(new Error("Missing providerDataJson")); | ||
| 451 | if (Either.isLeft(decoded)) { | ||
| 452 | return yield* Effect.fail( | ||
| 453 | new Error(`Invalid Google Discovery provider data for ${path}`), | ||
| 454 | ); | ||
| 455 | } | ||
| 456 | |||
| 457 | const providerData = decoded.right; | ||
| 458 | const method = providerData.invocation.method.toUpperCase(); | ||
| 459 | return { | ||
| 460 | method: providerData.invocation.method, | ||
| 461 | pathTemplate: providerData.invocation.path, | ||
| 462 | rawToolId: providerData.rawToolId, | ||
| 463 | operationId: providerData.methodId, | ||
| 464 | group: providerData.group, | ||
| 465 | leaf: providerData.leaf, | ||
| 466 | tags: [], | ||
| 467 | searchText: normalizeSearchText( | ||
| 468 | path, | ||
| 469 | operation.toolId, | ||
| 470 | operation.title ?? undefined, | ||
| 471 | operation.description ?? undefined, | ||
| 472 | providerData.rawToolId, | ||
| 473 | providerData.methodId, | ||
| 474 | method, | ||
| 475 | providerData.invocation.path, | ||
| 476 | providerData.group ?? undefined, | ||
| 477 | providerData.leaf, | ||
| 478 | providerData.service, | ||
| 479 | providerData.version, | ||
| 480 | providerData.invocation.scopes.join(" "), | ||
| 481 | operation.searchText, | ||
| 482 | ), | ||
| 483 | interaction: method === "GET" || method === "HEAD" ? "auto" : "required", | ||
| 484 | approvalLabel: `${method} ${providerData.invocation.path}`, | ||
| 485 | } as const; | ||
| 486 | }), | ||
| 487 | createToolDescriptor: ({ source, operation, path, includeSchemas, schemaBundleId }) => | ||
| 488 | createStandardToolDescriptor({ | ||
| 489 | source, | ||
| 490 | operation, | ||
| 491 | path, | ||
| 492 | includeSchemas, | ||
| 493 | interaction: operation.operationKind === "read" ? "auto" : "required", | ||
| 494 | outputType: typeSignatureFromSchemaJson( | ||
| 495 | operation.outputSchemaJson ?? undefined, | ||
| 496 | "unknown", | ||
| 497 | 320, | ||
| 498 | ), | ||
| 499 | schemaBundleId, | ||
| 500 | }), | ||
| 501 | materializeSource: ({ source, resolveAuthMaterialForSlot }): Effect.Effect< | ||
| 502 | SourceAdapterMaterialization, | ||
| 503 | Error, | ||
| 504 | never | ||
| 505 | > => | ||
| 506 | Effect.gen(function* () { | 311 | Effect.gen(function* () { |
| 507 | const bindingConfig = yield* googleDiscoveryBindingConfigFromSource(source); | 312 | const bindingConfig = yield* googleDiscoveryBindingConfigFromSource(source); |
| 508 | const auth = yield* resolveAuthMaterialForSlot("import"); | 313 | const auth = yield* resolveAuthMaterialForSlot("import"); |
| @@ -529,37 +334,24 @@ export const googleDiscoverySourceAdapter: SourceAdapter = { · 20 unmodified lines | |||
| 529 | ); | 334 | ); |
| 530 | const definitions = compileGoogleDiscoveryToolDefinitions(manifest); | 335 | const definitions = compileGoogleDiscoveryToolDefinitions(manifest); |
| 531 | const now = Date.now(); | 336 | const now = Date.now(); |
| 532 | const schemaBundle = toGoogleDiscoverySchemaBundleRecord({ | ||
| 533 | recipeRevisionId: GOOGLE_DISCOVERY_MATERIALIZATION_REVISION_ID, | ||
| 534 | refTable: manifest.schemaRefTable, | ||
| 535 | now, | ||
| 536 | }); | ||
| 537 | 337 | ||
| 538 | return { | 338 | return { |
| 539 | manifestJson: JSON.stringify(manifest), | 339 | snapshot: createGoogleDiscoveryCatalogSnapshot({ |
| 540 | manifestHash: manifest.sourceHash, | 340 | source, |
| 541 | sourceHash: manifest.sourceHash, | 341 | documents: [{ |
| 542 | documents: [ | ||
| 543 | { | ||
| 544 | id: `src_recipe_doc_${crypto.randomUUID()}`, | ||
| 545 | recipeRevisionId: GOOGLE_DISCOVERY_MATERIALIZATION_REVISION_ID, | ||
| 546 | documentKind: "google_discovery", | 342 | documentKind: "google_discovery", |
| 547 | documentKey: bindingConfig.discoveryUrl, | 343 | documentKey: bindingConfig.discoveryUrl, |
| 548 | contentText: discoveryDocument, | 344 | contentText: discoveryDocument, |
| 549 | contentHash: contentHash(discoveryDocument), | ||
| 550 | fetchedAt: now, | 345 | fetchedAt: now, |
| 551 | createdAt: now, | 346 | }], |
| 552 | updatedAt: now, | 347 | operations: definitions.map((definition) => |
| 553 | }, | 348 | googleDiscoveryCatalogOperationFromDefinition({ |
| 554 | ], | 349 | manifest, |
| 555 | schemaBundles: schemaBundle ? [schemaBundle] : [], | 350 | definition, |
| 556 | operations: definitions.map((definition) => | 351 | }) |
| 557 | toGoogleDiscoveryRecipeOperationRecord({ | 352 | ), |
| 558 | recipeRevisionId: GOOGLE_DISCOVERY_MATERIALIZATION_REVISION_ID, | 353 | }), |
| 559 | manifest, | 354 | sourceHash: manifest.sourceHash, |
| 560 | definition, | ||
| 561 | now, | ||
| 562 | })), | ||
| 563 | }; | 355 | }; |
| 564 | }), | 356 | }), |
| 565 | getOauth2SetupConfig: ({ source }) => googleDiscoveryOauth2SetupConfig(source), | 357 | getOauth2SetupConfig: ({ source }) => googleDiscoveryOauth2SetupConfig(source), |
| @@ -568,71 +360,4 @@ export const googleDiscoverySourceAdapter: SourceAdapter = { · 2 unmodified lines | |||
| 568 | ...input, | 360 | ...input, |
| 569 | redirectMode: input.redirectMode ?? "loopback", | 361 | redirectMode: input.redirectMode ?? "loopback", |
| 570 | }), | 362 | }), |
| 571 | invokePersistedTool: ({ | ||
| 572 | source, | ||
| 573 | path, | ||
| 574 | operation, | ||
| 575 | schemaBundle, | ||
| 576 | auth, | ||
| 577 | args, | ||
| 578 | context, | ||
| 579 | onElicitation, | ||
| 580 | }) => | ||
| 581 | Effect.gen(function* () { | ||
| 582 | const bindingConfig = yield* googleDiscoveryBindingConfigFromSource(source); | ||
| 583 | const schemaRefTable = schemaBundle | ||
| 584 | ? decodeGoogleDiscoverySchemaRefTableJson(schemaBundle.refsJson) | ||
| 585 | : Either.right({}); | ||
| 586 | if (Either.isLeft(schemaRefTable)) { | ||
| 587 | return yield* Effect.fail( | ||
| 588 | new Error(`Invalid Google Discovery schema bundle for ${path}`), | ||
| 589 | ); | ||
| 590 | } | ||
| 591 | |||
| 592 | const { definition, providerData } = googleDiscoveryDefinitionFromPersistedOperation({ | ||
| 593 | path, | ||
| 594 | operation, | ||
| 595 | }); | ||
| 596 | const stringRefTable = Object.fromEntries( | ||
| 597 | Object.entries(schemaRefTable.right).map(([ref, value]) => [ | ||
| 598 | ref, | ||
| 599 | typeof value === "string" ? value : JSON.stringify(value), | ||
| 600 | ]), | ||
| 601 | ); | ||
| 602 | |||
| 603 | const tool = createGoogleDiscoveryToolFromDefinition({ | ||
| 604 | definition, | ||
| 605 | service: providerData.service, | ||
| 606 | version: providerData.version, | ||
| 607 | rootUrl: providerData.invocation.rootUrl, | ||
| 608 | servicePath: providerData.invocation.servicePath, | ||
| 609 | path, | ||
| 610 | sourceKey: source.id, | ||
| 611 | defaultHeaders: bindingConfig.defaultHeaders ?? {}, | ||
| 612 | credentialPlacements: auth, | ||
| 613 | schemaRefTable: stringRefTable, | ||
| 614 | }); | ||
| 615 | |||
| 616 | return yield* makeToolInvokerFromTools({ | ||
| 617 | tools: { | ||
| 618 | [path]: tool, | ||
| 619 | }, | ||
| 620 | // Authorization was already handled by authorizePersistedToolInvocation | ||
| 621 | // at the workspace level, so skip the redundant tool-level interaction | ||
| 622 | // gate. Without this, the default "required" interaction on write tools | ||
| 623 | // would re-elicit approval and then merge the approval form content | ||
| 624 | // (e.g. { approve: true }) into the tool args, breaking schemas that | ||
| 625 | // use additionalProperties: false. | ||
| 626 | onToolInteraction: allowAllToolInteractions, | ||
| 627 | onElicitation, | ||
| 628 | }).invoke({ | ||
| 629 | path, | ||
| 630 | args, | ||
| 631 | context, | ||
| 632 | }).pipe( | ||
| 633 | Effect.mapError((cause) => | ||
| 634 | cause instanceof Error ? cause : new Error(String(cause)), | ||
| 635 | ), | ||
| 636 | ); | ||
| 637 | }), | ||
| 638 | }; | 363 | }; |
| @@ -1,58 +1,34 @@ | |||
| 1 | import { | 1 | import { |
| 2 | allowAllToolInteractions, | ||
| 3 | applyCookiePlacementsToHeaders, | 2 | applyCookiePlacementsToHeaders, |
| 4 | applyHttpQueryPlacementsToUrl, | 3 | applyHttpQueryPlacementsToUrl, |
| 5 | applyJsonBodyPlacements, | 4 | applyJsonBodyPlacements, |
| 6 | makeToolInvokerFromTools, | ||
| 7 | typeSignatureFromSchemaJson, | ||
| 8 | } from "@executor/codemode-core"; | 5 | } from "@executor/codemode-core"; |
| 9 | import type { | ||
| 10 | GraphqlToolManifest, | ||
| 11 | } from "../graphql-tools"; | ||
| 12 | import { | 6 | import { |
| 13 | buildGraphqlToolPresentation, | 7 | buildGraphqlToolPresentation, |
| 14 | compileGraphqlToolDefinitions, | 8 | compileGraphqlToolDefinitions, |
| 15 | createGraphqlToolFromPersistedOperation, | ||
| 16 | decodeGraphqlSchemaRefTableJson, | ||
| 17 | extractGraphqlManifest, | 9 | extractGraphqlManifest, |
| 18 | GraphqlToolProviderDataSchema, | ||
| 19 | GRAPHQL_INTROSPECTION_QUERY, | 10 | GRAPHQL_INTROSPECTION_QUERY, |
| 11 | type GraphqlToolProviderData, | ||
| 20 | } from "../graphql-tools"; | 12 | } from "../graphql-tools"; |
| 21 | import type { | 13 | import type { Source } from "#schema"; |
| 22 | Source, | 14 | import { StringMapSchema } from "#schema"; |
| 23 | SourceRecipeRevisionId, | ||
| 24 | StoredSourceRecipeDocumentRecord, | ||
| 25 | StoredSourceRecipeOperationRecord, | ||
| 26 | StoredSourceRecipeSchemaBundleRecord, | ||
| 27 | } from "#schema"; | ||
| 28 | import { | ||
| 29 | StringMapSchema, | ||
| 30 | SourceRecipeSchemaBundleIdSchema, | ||
| 31 | } from "#schema"; | ||
| 32 | import * as Either from "effect/Either"; | ||
| 33 | import * as Effect from "effect/Effect"; | 15 | import * as Effect from "effect/Effect"; |
| 34 | import * as Schema from "effect/Schema"; | 16 | import * as Schema from "effect/Schema"; |
| 35 | 17 | ||
| 36 | import { namespaceFromSourceName } from "../source-names"; | ||
| 37 | import { | 18 | import { |
| 38 | contentHash, | 19 | createGraphqlCatalogSnapshot, |
| 39 | normalizeSearchText, | 20 | type GraphqlCatalogOperationInput, |
| 40 | } from "../source-recipe-support"; | 21 | } from "../source-catalog-snapshot"; |
| 41 | import type { | 22 | import type { SourceAdapter } from "./types"; |
| 42 | SourceAdapter, | ||
| 43 | SourceAdapterMaterialization, | ||
| 44 | } from "./types"; | ||
| 45 | import { | 23 | import { |
| 46 | ConnectHttpAuthSchema, | 24 | ConnectHttpAuthSchema, |
| 47 | ConnectHttpImportAuthSchema, | 25 | ConnectHttpImportAuthSchema, |
| 48 | createStandardToolDescriptor, | ||
| 49 | decodeBindingConfig, | 26 | decodeBindingConfig, |
| 50 | decodeSourceBindingPayload, | 27 | decodeSourceBindingPayload, |
| 51 | emptySourceBindingState, | 28 | emptySourceBindingState, |
| 52 | encodeBindingConfig, | 29 | encodeBindingConfig, |
| 53 | isSourceCredentialRequiredError, | 30 | isSourceCredentialRequiredError, |
| 54 | OptionalNullableStringSchema, | 31 | OptionalNullableStringSchema, |
| 55 | parseJsonValue, | ||
| 56 | SourceCredentialRequiredError, | 32 | SourceCredentialRequiredError, |
| 57 | SourceConnectCommonFieldsSchema, | 33 | SourceConnectCommonFieldsSchema, |
| 58 | } from "./shared"; | 34 | } from "./shared"; |
| @@ -91,12 +67,6 @@ const GraphqlSourceBindingPayloadSchema = Schema.Struct({ · 32 unmodified lines | |||
| 91 | 67 | ||
| 92 | const GRAPHQL_BINDING_CONFIG_VERSION = 1; | 68 | const GRAPHQL_BINDING_CONFIG_VERSION = 1; |
| 93 | 69 | ||
| 94 | const GRAPHQL_MATERIALIZATION_REVISION_ID = "src_recipe_rev_materialization" as SourceRecipeRevisionId; | ||
| 95 | |||
| 96 | const decodeGraphqlToolProviderDataJson = Schema.decodeUnknownEither( | ||
| 97 | Schema.parseJson(GraphqlToolProviderDataSchema), | ||
| 98 | ); | ||
| 99 | |||
| 100 | const bindingHasAnyField = ( | 70 | const bindingHasAnyField = ( |
| 101 | value: unknown, | 71 | value: unknown, |
| 102 | fields: readonly string[], | 72 | fields: readonly string[], |
| @@ -198,92 +168,29 @@ const fetchGraphqlIntrospectionDocumentWithHeaders = (input: { · 95 unmodified lines | |||
| 198 | cause instanceof Error ? cause : new Error(String(cause)), | 168 | cause instanceof Error ? cause : new Error(String(cause)), |
| 199 | }); | 169 | }); |
| 200 | 170 | ||
| 201 | const toGraphqlSchemaBundleRecord = (input: { | 171 | const graphqlCatalogOperationFromDefinition = (input: { |
| 202 | recipeRevisionId: SourceRecipeRevisionId; | ||
| 203 | manifest: Parameters<typeof buildGraphqlToolPresentation>[0]["manifest"]; | ||
| 204 | now: number; | ||
| 205 | }): StoredSourceRecipeSchemaBundleRecord | null => { | ||
| 206 | const refEntries = Object.entries(input.manifest.schemaRefTable ?? {}); | ||
| 207 | if (refEntries.length === 0) { | ||
| 208 | return null; | ||
| 209 | } | ||
| 210 | |||
| 211 | const refs = Object.fromEntries( | ||
| 212 | refEntries.map(([ref, rawValue]) => { | ||
| 213 | try { | ||
| 214 | return [ref, JSON.parse(rawValue) as unknown]; | ||
| 215 | } catch { | ||
| 216 | return [ref, rawValue]; | ||
| 217 | } | ||
| 218 | }), | ||
| 219 | ); | ||
| 220 | const refsJson = JSON.stringify(refs); | ||
| 221 | |||
| 222 | return { | ||
| 223 | id: SourceRecipeSchemaBundleIdSchema.make( | ||
| 224 | `src_recipe_bundle_${crypto.randomUUID()}`, | ||
| 225 | ), | ||
| 226 | recipeRevisionId: input.recipeRevisionId, | ||
| 227 | bundleKind: "json_schema_ref_map", | ||
| 228 | refsJson, | ||
| 229 | contentHash: contentHash(refsJson), | ||
| 230 | createdAt: input.now, | ||
| 231 | updatedAt: input.now, | ||
| 232 | }; | ||
| 233 | }; | ||
| 234 | |||
| 235 | const toGraphqlRecipeOperationRecord = (input: { | ||
| 236 | recipeRevisionId: SourceRecipeRevisionId; | ||
| 237 | definition: ReturnType<typeof compileGraphqlToolDefinitions>[number]; | 172 | definition: ReturnType<typeof compileGraphqlToolDefinitions>[number]; |
| 238 | manifest: Parameters<typeof buildGraphqlToolPresentation>[0]["manifest"]; | 173 | manifest: Parameters<typeof buildGraphqlToolPresentation>[0]["manifest"]; |
| 239 | now: number; | 174 | }): GraphqlCatalogOperationInput => { |
| 240 | }): StoredSourceRecipeOperationRecord => { | ||
| 241 | const presentation = buildGraphqlToolPresentation({ | 175 | const presentation = buildGraphqlToolPresentation({ |
| 242 | manifest: input.manifest, | 176 | manifest: input.manifest, |
| 243 | definition: input.definition, | 177 | definition: input.definition, |
| 244 | }); | 178 | }); |
| 245 | 179 | ||
| 246 | return { | 180 | return { |
| 247 | id: `src_recipe_op_${crypto.randomUUID()}`, | ||
| 248 | recipeRevisionId: input.recipeRevisionId, | ||
| 249 | operationKey: input.definition.toolId, | ||
| 250 | transportKind: "graphql", | ||
| 251 | toolId: input.definition.toolId, | 181 | toolId: input.definition.toolId, |
| 252 | title: input.definition.name, | 182 | title: input.definition.name, |
| 253 | description: input.definition.description, | 183 | description: input.definition.description, |
| 254 | operationKind: | 184 | effect: |
| 255 | input.definition.operationType === "query" | 185 | input.definition.operationType === "query" |
| 256 | ? "read" | 186 | ? "read" |
| 257 | : input.definition.operationType === "mutation" | 187 | : "write", |
| 258 | ? "write" | 188 | inputSchema: presentation.inputSchema, |
| 259 | : "unknown", | 189 | outputSchema: presentation.outputSchema, |
| 260 | searchText: normalizeSearchText( | 190 | providerData: presentation.providerData as GraphqlToolProviderData, |
| 261 | input.definition.toolId, | ||
| 262 | input.definition.name, | ||
| 263 | input.definition.description, | ||
| 264 | input.definition.rawToolId, | ||
| 265 | input.definition.group, | ||
| 266 | input.definition.leaf, | ||
| 267 | input.definition.fieldName, | ||
| 268 | input.definition.operationType, | ||
| 269 | input.definition.operationName, | ||
| 270 | input.definition.searchTerms.join(" "), | ||
| 271 | ), | ||
| 272 | inputSchemaJson: presentation.inputSchemaJson ?? null, | ||
| 273 | outputSchemaJson: presentation.outputSchemaJson ?? null, | ||
| 274 | providerKind: "graphql", | ||
| 275 | providerDataJson: presentation.providerDataJson, | ||
| 276 | createdAt: input.now, | ||
| 277 | updatedAt: input.now, | ||
| 278 | }; | 191 | }; |
| 279 | }; | 192 | }; |
| 280 | 193 | ||
| 281 | const primaryDocument = ( | ||
| 282 | documents: readonly StoredSourceRecipeDocumentRecord[], | ||
| 283 | ): StoredSourceRecipeDocumentRecord | null => | ||
| 284 | documents.find((document) => document.documentKind === "graphql_introspection") | ||
| 285 | ?? null; | ||
| 286 | |||
| 287 | export const graphqlSourceAdapter: SourceAdapter = { | 194 | export const graphqlSourceAdapter: SourceAdapter = { |
| 288 | key: "graphql", | 195 | key: "graphql", |
| 289 | displayName: "GraphQL", | 196 | displayName: "GraphQL", |
| @@ -291,8 +198,6 @@ export const graphqlSourceAdapter: SourceAdapter = { · 1 unmodified line | |||
| 291 | bindingConfigVersion: GRAPHQL_BINDING_CONFIG_VERSION, | 198 | bindingConfigVersion: GRAPHQL_BINDING_CONFIG_VERSION, |
| 292 | providerKey: "generic_graphql", | 199 | providerKey: "generic_graphql", |
| 293 | defaultImportAuthPolicy: "reuse_runtime", | 200 | defaultImportAuthPolicy: "reuse_runtime", |
| 294 | primaryDocumentKind: "graphql_introspection", | ||
| 295 | primarySchemaBundleKind: "json_schema_ref_map", | ||
| 296 | connectPayloadSchema: GraphqlConnectPayloadSchema, | 201 | connectPayloadSchema: GraphqlConnectPayloadSchema, |
| 297 | executorAddInputSchema: GraphqlExecutorAddInputSchema, | 202 | executorAddInputSchema: GraphqlExecutorAddInputSchema, |
| 298 | executorAddHelpText: [ | 203 | executorAddHelpText: [ |
| @@ -349,68 +254,7 @@ export const graphqlSourceAdapter: SourceAdapter = { · 50 unmodified lines | |||
| 349 | }), | 254 | }), |
| 350 | shouldAutoProbe: (source) => | 255 | shouldAutoProbe: (source) => |
| 351 | source.enabled && (source.status === "draft" || source.status === "probing"), | 256 | source.enabled && (source.status === "draft" || source.status === "probing"), |
| 352 | parseManifest: ({ source, manifestJson }) => | 257 | syncCatalog: ({ source, resolveAuthMaterialForSlot }) => |
| 353 | parseJsonValue<GraphqlToolManifest>({ | ||
| 354 | label: `GraphQL manifest for ${source.id}`, | ||
| 355 | value: manifestJson, | ||
| 356 | }), | ||
| 357 | describePersistedOperation: ({ operation, path }) => | ||
| 358 | Effect.gen(function* () { | ||
| 359 | const decoded = operation.providerDataJson | ||
| 360 | ? decodeGraphqlToolProviderDataJson(operation.providerDataJson) | ||
| 361 | : Either.left(new Error("Missing providerDataJson")); | ||
| 362 | if (Either.isLeft(decoded)) { | ||
| 363 | return yield* Effect.fail( | ||
| 364 | new Error(`Invalid GraphQL provider data for ${path}`), | ||
| 365 | ); | ||
| 366 | } | ||
| 367 | |||
| 368 | const providerData = decoded.right; | ||
| 369 | |||
| 370 | return { | ||
| 371 | method: null, | ||
| 372 | pathTemplate: null, | ||
| 373 | rawToolId: providerData.rawToolId, | ||
| 374 | operationId: providerData.operationName, | ||
| 375 | group: providerData.group, | ||
| 376 | leaf: providerData.leaf, | ||
| 377 | tags: [], | ||
| 378 | searchText: normalizeSearchText( | ||
| 379 | path, | ||
| 380 | operation.toolId, | ||
| 381 | operation.title ?? undefined, | ||
| 382 | operation.description ?? undefined, | ||
| 383 | providerData.rawToolId ?? undefined, | ||
| 384 | providerData.group ?? undefined, | ||
| 385 | providerData.leaf ?? undefined, | ||
| 386 | providerData.fieldName ?? undefined, | ||
| 387 | providerData.operationType ?? undefined, | ||
| 388 | providerData.operationName ?? undefined, | ||
| 389 | operation.searchText, | ||
| 390 | ), | ||
| 391 | interaction: providerData.operationType === "query" ? "auto" : "required", | ||
| 392 | approvalLabel: providerData.operationType | ||
| 393 | ? `GraphQL ${providerData.operationType} ${path}` | ||
| 394 | : `GraphQL ${path}`, | ||
| 395 | } as const; | ||
| 396 | }), | ||
| 397 | searchNamespace: ({ source }) => | ||
| 398 | source.namespace ?? namespaceFromSourceName(source.name), | ||
| 399 | createToolDescriptor: ({ source, operation, path, includeSchemas, schemaBundleId }) => | ||
| 400 | createStandardToolDescriptor({ | ||
| 401 | source, | ||
| 402 | operation, | ||
| 403 | path, | ||
| 404 | includeSchemas, | ||
| 405 | interaction: operation.operationKind === "read" ? "auto" : "required", | ||
| 406 | outputType: typeSignatureFromSchemaJson( | ||
| 407 | operation.outputSchemaJson ?? undefined, | ||
| 408 | "unknown", | ||
| 409 | 320, | ||
| 410 | ), | ||
| 411 | schemaBundleId, | ||
| 412 | }), | ||
| 413 | materializeSource: ({ source, resolveAuthMaterialForSlot }) => | ||
| 414 | Effect.gen(function* () { | 258 | Effect.gen(function* () { |
| 415 | const bindingConfig = yield* graphqlBindingConfigFromSource(source); | 259 | const bindingConfig = yield* graphqlBindingConfigFromSource(source); |
| 416 | const auth = yield* resolveAuthMaterialForSlot("import"); | 260 | const auth = yield* resolveAuthMaterialForSlot("import"); |
| @@ -447,94 +291,24 @@ export const graphqlSourceAdapter: SourceAdapter = { · 30 unmodified lines | |||
| 447 | 291 | ||
| 448 | const definitions = compileGraphqlToolDefinitions(manifest); | 292 | const definitions = compileGraphqlToolDefinitions(manifest); |
| 449 | const now = Date.now(); | 293 | const now = Date.now(); |
| 450 | const schemaBundle = toGraphqlSchemaBundleRecord({ | ||
| 451 | recipeRevisionId: GRAPHQL_MATERIALIZATION_REVISION_ID, | ||
| 452 | manifest, | ||
| 453 | now, | ||
| 454 | }); | ||
| 455 | 294 | ||
| 456 | return { | 295 | return { |
| 457 | manifestJson: JSON.stringify(manifest), | 296 | snapshot: createGraphqlCatalogSnapshot({ |
| 458 | manifestHash: manifest.sourceHash, | 297 | source, |
| 459 | sourceHash: manifest.sourceHash, | 298 | documents: [{ |
| 460 | documents: [ | ||
| 461 | { | ||
| 462 | id: `src_recipe_doc_${crypto.randomUUID()}`, | ||
| 463 | recipeRevisionId: GRAPHQL_MATERIALIZATION_REVISION_ID, | ||
| 464 | documentKind: "graphql_introspection", | 299 | documentKind: "graphql_introspection", |
| 465 | documentKey: source.endpoint, | 300 | documentKey: source.endpoint, |
| 466 | contentText: graphqlDocument, | 301 | contentText: graphqlDocument, |
| 467 | contentHash: contentHash(graphqlDocument), | ||
| 468 | fetchedAt: now, | 302 | fetchedAt: now, |
| 469 | createdAt: now, | 303 | }], |
| 470 | updatedAt: now, | 304 | operations: definitions.map((definition) => |
| 471 | }, | 305 | graphqlCatalogOperationFromDefinition({ |
| 472 | ], | 306 | definition, |
| 473 | schemaBundles: schemaBundle ? [schemaBundle] : [], | 307 | manifest, |
| 474 | operations: definitions.map((definition) => | 308 | }) |
| 475 | toGraphqlRecipeOperationRecord({ | 309 | ), |
| 476 | recipeRevisionId: GRAPHQL_MATERIALIZATION_REVISION_ID, | 310 | }), |
| 477 | definition, | 311 | sourceHash: manifest.sourceHash, |
| 478 | manifest, | 312 | }; |
| 479 | now, | ||
| 480 | }), | ||
| 481 | ), | ||
| 482 | } satisfies SourceAdapterMaterialization; | ||
| 483 | }), | ||
| 484 | invokePersistedTool: ({ | ||
| 485 | source, | ||
| 486 | path, | ||
| 487 | operation, | ||
| 488 | schemaBundle, | ||
| 489 | auth, | ||
| 490 | args, | ||
| 491 | context, | ||
| 492 | onElicitation, | ||
| 493 | }) => | ||
| 494 | Effect.gen(function* () { | ||
| 495 | const bindingConfig = yield* graphqlBindingConfigFromSource(source); | ||
| 496 | const schemaRefTable = schemaBundle | ||
| 497 | ? decodeGraphqlSchemaRefTableJson(schemaBundle.refsJson) | ||
| 498 | : Either.right({}); | ||
| 499 | |||
| 500 | if (Either.isLeft(schemaRefTable)) { | ||
| 501 | return yield* Effect.fail( | ||
| 502 | new Error(`Invalid GraphQL schema bundle for ${path}`), | ||
| 503 | ); | ||
| 504 | } | ||
| 505 | if (!operation.providerDataJson) { | ||
| 506 | return yield* Effect.fail( | ||
| 507 | new Error(`Missing GraphQL provider data for ${path}`), | ||
| 508 | ); | ||
| 509 | } | ||
| 510 | |||
| 511 | const tool = createGraphqlToolFromPersistedOperation({ | ||
| 512 | path, | ||
| 513 | sourceKey: source.id, | ||
| 514 | endpoint: source.endpoint, | ||
| 515 | description: operation.description ?? operation.title ?? undefined, | ||
| 516 | inputSchemaJson: operation.inputSchemaJson ?? undefined, | ||
| 517 | outputSchemaJson: operation.outputSchemaJson ?? undefined, | ||
| 518 | providerDataJson: operation.providerDataJson, | ||
| 519 | schemaRefTable: schemaRefTable.right, | ||
| 520 | defaultHeaders: bindingConfig.defaultHeaders ?? {}, | ||
| 521 | credentialPlacements: auth, | ||
| 522 | }); | ||
| 523 | |||
| 524 | return yield* makeToolInvokerFromTools({ | ||
| 525 | tools: { | ||
| 526 | [path]: tool, | ||
| 527 | }, | ||
| 528 | onToolInteraction: allowAllToolInteractions, | ||
| 529 | onElicitation, | ||
| 530 | }).invoke({ | ||
| 531 | path, | ||
| 532 | args, | ||
| 533 | context, | ||
| 534 | }).pipe( | ||
| 535 | Effect.mapError((cause) => | ||
| 536 | cause instanceof Error ? cause : new Error(String(cause)), | ||
| 537 | ), | ||
| 538 | ); | ||
| 539 | }), | 313 | }), |
| 540 | }; | 314 | }; |
| @@ -1,4 +1,4 @@ | |||
| 1 | import type { Source, StoredSourceRecipeOperationRecord } from "#schema"; | 1 | import type { Source } from "#schema"; |
| 2 | import * as Schema from "effect/Schema"; | 2 | import * as Schema from "effect/Schema"; |
| 3 | 3 | ||
| 4 | import { graphqlSourceAdapter } from "./graphql"; | 4 | import { graphqlSourceAdapter } from "./graphql"; |
| @@ -67,10 +67,6 @@ export const getSourceAdapterForSource = (source: Pick<Source, "kind">): SourceA · 62 unmodified lines | |||
| 67 | export const sourceBindingStateFromSource = (source: Source) => | 67 | export const sourceBindingStateFromSource = (source: Source) => |
| 68 | getSourceAdapterForSource(source).bindingStateFromSource(source); | 68 | getSourceAdapterForSource(source).bindingStateFromSource(source); |
| 69 | 69 | ||
| 70 | export const getSourceAdapterForOperation = ( | ||
| 71 | operation: Pick<StoredSourceRecipeOperationRecord, "providerKind">, | ||
| 72 | ): SourceAdapter => getSourceAdapter(operation.providerKind); | ||
| 73 | |||
| 74 | export const hasSourceAdapterFamily = ( | 70 | export const hasSourceAdapterFamily = ( |
| 75 | key: string, | 71 | key: string, |
| 76 | family: SourceAdapter["family"], | 72 | family: SourceAdapter["family"], |
| @@ -4,7 +4,6 @@ import type { Source } from "#schema"; · 3 unmodified lines | |||
| 4 | 4 | ||
| 5 | import type { SourceAdapter } from "./types"; | 5 | import type { SourceAdapter } from "./types"; |
| 6 | import { | 6 | import { |
| 7 | createStandardToolDescriptor, | ||
| 8 | decodeBindingConfig, | 7 | decodeBindingConfig, |
| 9 | decodeSourceBindingPayload, | 8 | decodeSourceBindingPayload, |
| 10 | emptySourceBindingState, | 9 | emptySourceBindingState, |
| @@ -58,8 +57,6 @@ export const internalSourceAdapter: SourceAdapter = { · 47 unmodified lines | |||
| 58 | bindingConfigVersion: INTERNAL_BINDING_CONFIG_VERSION, | 57 | bindingConfigVersion: INTERNAL_BINDING_CONFIG_VERSION, |
| 59 | providerKey: "generic_internal", | 58 | providerKey: "generic_internal", |
| 60 | defaultImportAuthPolicy: "none", | 59 | defaultImportAuthPolicy: "none", |
| 61 | primaryDocumentKind: null, | ||
| 62 | primarySchemaBundleKind: null, | ||
| 63 | connectPayloadSchema: null, | 60 | connectPayloadSchema: null, |
| 64 | executorAddInputSchema: null, | 61 | executorAddInputSchema: null, |
| 65 | executorAddHelpText: null, | 62 | executorAddHelpText: null, |
| @@ -102,40 +99,28 @@ export const internalSourceAdapter: SourceAdapter = { · 36 unmodified lines | |||
| 102 | }; | 99 | }; |
| 103 | }), | 100 | }), |
| 104 | shouldAutoProbe: () => false, | 101 | shouldAutoProbe: () => false, |
| 105 | parseManifest: () => Effect.succeed(null), | 102 | syncCatalog: () => Effect.succeed({ |
| 106 | describePersistedOperation: ({ operation, path }) => | 103 | snapshot: { |
| 107 | Effect.succeed({ | 104 | version: "ir.v1.snapshot", |
| 108 | method: null, | 105 | import: { |
| 109 | pathTemplate: null, | 106 | sourceKind: "custom", |
| 110 | rawToolId: null, | 107 | adapterKey: "internal", |
| 111 | operationId: null, | 108 | importerVersion: "ir.v1.internal", |
| 112 | group: null, | 109 | importedAt: new Date().toISOString(), |
| 113 | leaf: null, | 110 | sourceConfigHash: "internal", |
| 114 | tags: [], | 111 | }, |
| 115 | searchText: [path, operation.toolId, operation.title ?? "", operation.description ?? "", operation.searchText] | 112 | catalog: { |
| 116 | .filter((part) => part.length > 0) | 113 | version: "ir.v1", |
| 117 | .join(" ") | 114 | documents: {}, |
| 118 | .toLowerCase(), | 115 | resources: {}, |
| 119 | interaction: "auto", | 116 | scopes: {}, |
| 120 | approvalLabel: null, | 117 | symbols: {}, |
| 121 | } as const), | 118 | capabilities: {}, |
| 122 | createToolDescriptor: ({ source, operation, path, includeSchemas, schemaBundleId }) => | 119 | executables: {}, |
| 123 | createStandardToolDescriptor({ | 120 | responseSets: {}, |
| 124 | source, | 121 | diagnostics: {}, |
| 125 | operation, | 122 | }, |
| 126 | path, | 123 | }, |
| 127 | includeSchemas, | ||
| 128 | interaction: "auto", | ||
| 129 | schemaBundleId, | ||
| 130 | }), | ||
| 131 | materializeSource: () => Effect.succeed({ | ||
| 132 | manifestJson: null, | ||
| 133 | manifestHash: null, | ||
| 134 | sourceHash: null, | 124 | sourceHash: null, |
| 135 | documents: [], | ||
| 136 | schemaBundles: [], | ||
| 137 | operations: [], | ||
| 138 | }), | 125 | }), |
| 139 | invokePersistedTool: ({ path }) => | ||
| 140 | Effect.fail(new Error(`Unsupported stored tool provider for ${path}`)), | ||
| 141 | }; | 126 | }; |
| @@ -1,10 +1,7 @@ | |||
| 1 | import { | 1 | import { |
| 2 | allowAllToolInteractions, | ||
| 3 | applyCookiePlacementsToHeaders, | 2 | applyCookiePlacementsToHeaders, |
| 4 | makeToolInvokerFromTools, | ||
| 5 | } from "@executor/codemode-core"; | 3 | } from "@executor/codemode-core"; |
| 6 | import { | 4 | import { |
| 7 | createMcpToolsFromManifest, | ||
| 8 | createSdkMcpConnector, | 5 | createSdkMcpConnector, |
| 9 | discoverMcpToolsFromConnector, | 6 | discoverMcpToolsFromConnector, |
| 10 | type McpToolManifest, | 7 | type McpToolManifest, |
| @@ -12,8 +9,6 @@ import { · 1 unmodified line | |||
| 12 | } from "@executor/codemode-mcp"; | 9 | } from "@executor/codemode-mcp"; |
| 13 | import type { | 10 | import type { |
| 14 | Source, | 11 | Source, |
| 15 | SourceRecipeRevisionId, | ||
| 16 | StoredSourceRecipeOperationRecord, | ||
| 17 | } from "#schema"; | 12 | } from "#schema"; |
| 18 | import { | 13 | import { |
| 19 | SourceTransportSchema, | 14 | SourceTransportSchema, |
| @@ -22,22 +17,23 @@ import { · 2 unmodified lines | |||
| 22 | import * as Effect from "effect/Effect"; | 17 | import * as Effect from "effect/Effect"; |
| 23 | import * as Schema from "effect/Schema"; | 18 | import * as Schema from "effect/Schema"; |
| 24 | 19 | ||
| 20 | import { | ||
| 21 | createMcpCatalogSnapshot, | ||
| 22 | type McpCatalogOperationInput, | ||
| 23 | } from "../source-catalog-snapshot"; | ||
| 25 | import { | 24 | import { |
| 26 | contentHash, | 25 | contentHash, |
| 27 | normalizeSearchText, | 26 | type SourceCatalogSyncResult, |
| 28 | type SourceRecipeMaterialization, | 27 | } from "../source-catalog-support"; |
| 29 | } from "../source-recipe-support"; | ||
| 30 | import { namespaceFromSourceName } from "../source-names"; | 28 | import { namespaceFromSourceName } from "../source-names"; |
| 31 | import type { SourceAdapter, SourceAdapterMaterialization } from "./types"; | 29 | import type { SourceAdapter } from "./types"; |
| 32 | import { | 30 | import { |
| 33 | createStandardToolDescriptor, | ||
| 34 | decodeBindingConfig, | 31 | decodeBindingConfig, |
| 35 | decodeSourceBindingPayload, | 32 | decodeSourceBindingPayload, |
| 36 | emptySourceBindingState, | 33 | emptySourceBindingState, |
| 37 | encodeBindingConfig, | 34 | encodeBindingConfig, |
| 38 | McpConnectFieldsSchema, | 35 | McpConnectFieldsSchema, |
| 39 | OptionalNullableStringSchema, | 36 | OptionalNullableStringSchema, |
| 40 | parseJsonValue, | ||
| 41 | SourceConnectCommonFieldsSchema, | 37 | SourceConnectCommonFieldsSchema, |
| 42 | } from "./shared"; | 38 | } from "./shared"; |
| 43 | 39 | ||
| @@ -87,17 +83,6 @@ const McpSourceBindingPayloadSchema = Schema.Struct({ · 43 unmodified lines | |||
| 87 | 83 | ||
| 88 | const MCP_BINDING_CONFIG_VERSION = 1; | 84 | const MCP_BINDING_CONFIG_VERSION = 1; |
| 89 | 85 | ||
| 90 | const McpToolProviderDataSchema = Schema.Struct({ | ||
| 91 | kind: Schema.Literal("mcp"), | ||
| 92 | toolId: Schema.String, | ||
| 93 | toolName: Schema.String, | ||
| 94 | description: Schema.NullOr(Schema.String), | ||
| 95 | }); | ||
| 96 | |||
| 97 | const decodeMcpToolProviderDataJson = Schema.decodeUnknownEither( | ||
| 98 | Schema.parseJson(McpToolProviderDataSchema), | ||
| 99 | ); | ||
| 100 | |||
| 101 | const bindingHasAnyField = ( | 86 | const bindingHasAnyField = ( |
| 102 | value: unknown, | 87 | value: unknown, |
| 103 | fields: readonly string[], | 88 | fields: readonly string[], |
| @@ -137,43 +122,25 @@ const mcpBindingConfigFromSource = ( · 33 unmodified lines | |||
| 137 | } satisfies McpBindingConfig; | 122 | } satisfies McpBindingConfig; |
| 138 | }); | 123 | }); |
| 139 | 124 | ||
| 140 | const toMcpRecipeOperationRecord = (input: { | 125 | const mcpCatalogOperationFromManifestEntry = (entry: McpToolManifestEntry): McpCatalogOperationInput => ({ |
| 141 | recipeRevisionId: SourceRecipeRevisionId; | 126 | toolId: entry.toolId, |
| 142 | entry: McpToolManifestEntry; | 127 | title: entry.toolName, |
| 143 | now: number; | 128 | description: entry.description ?? null, |
| 144 | }): StoredSourceRecipeOperationRecord => ({ | 129 | effect: "action", |
| 145 | id: `src_recipe_op_${crypto.randomUUID()}`, | 130 | inputSchema: entry.inputSchema, |
| 146 | recipeRevisionId: input.recipeRevisionId, | 131 | outputSchema: entry.outputSchema, |
| 147 | operationKey: input.entry.toolId, | 132 | providerData: { |
| 148 | transportKind: "mcp", | 133 | toolId: entry.toolId, |
| 149 | toolId: input.entry.toolId, | 134 | toolName: entry.toolName, |
| 150 | title: input.entry.toolName, | 135 | description: entry.description ?? null, |
| 151 | description: input.entry.description ?? null, | 136 | }, |
| 152 | operationKind: "unknown", | ||
| 153 | searchText: normalizeSearchText( | ||
| 154 | input.entry.toolId, | ||
| 155 | input.entry.toolName, | ||
| 156 | input.entry.description ?? undefined, | ||
| 157 | "mcp", | ||
| 158 | ), | ||
| 159 | inputSchemaJson: input.entry.inputSchemaJson ?? null, | ||
| 160 | outputSchemaJson: input.entry.outputSchemaJson ?? null, | ||
| 161 | providerKind: "mcp", | ||
| 162 | providerDataJson: JSON.stringify({ | ||
| 163 | kind: "mcp", | ||
| 164 | toolId: input.entry.toolId, | ||
| 165 | toolName: input.entry.toolName, | ||
| 166 | description: input.entry.description ?? null, | ||
| 167 | }), | ||
| 168 | createdAt: input.now, | ||
| 169 | updatedAt: input.now, | ||
| 170 | }); | 137 | }); |
| 171 | 138 | ||
| 172 | export const materializationFromMcpManifestEntries = (input: { | 139 | export const catalogSyncResultFromMcpManifestEntries = (input: { |
| 173 | recipeRevisionId: SourceRecipeRevisionId; | 140 | source: Source; |
| 174 | endpoint: string; | 141 | endpoint: string; |
| 175 | manifestEntries: readonly McpToolManifestEntry[]; | 142 | manifestEntries: readonly McpToolManifestEntry[]; |
| 176 | }): SourceRecipeMaterialization => { | 143 | }): SourceCatalogSyncResult => { |
| 177 | const now = Date.now(); | 144 | const now = Date.now(); |
| 178 | const manifest: McpToolManifest = { | 145 | const manifest: McpToolManifest = { |
| 179 | version: 1, | 146 | version: 1, |
| @@ -183,30 +150,17 @@ export const materializationFromMcpManifestEntries = (input: { · 3 unmodified lines | |||
| 183 | const manifestHash = contentHash(manifestJson); | 150 | const manifestHash = contentHash(manifestJson); |
| 184 | 151 | ||
| 185 | return { | 152 | return { |
| 186 | manifestJson, | 153 | snapshot: createMcpCatalogSnapshot({ |
| 187 | manifestHash, | 154 | source: input.source, |
| 188 | sourceHash: manifestHash, | 155 | documents: [{ |
| 189 | documents: [ | ||
| 190 | { | ||
| 191 | id: `src_recipe_doc_${crypto.randomUUID()}`, | ||
| 192 | recipeRevisionId: input.recipeRevisionId, | ||
| 193 | documentKind: "mcp_manifest", | 156 | documentKind: "mcp_manifest", |
| 194 | documentKey: input.endpoint, | 157 | documentKey: input.endpoint, |
| 195 | contentText: manifestJson, | 158 | contentText: manifestJson, |
| 196 | contentHash: manifestHash, | ||
| 197 | fetchedAt: now, | 159 | fetchedAt: now, |
| 198 | createdAt: now, | 160 | }], |
| 199 | updatedAt: now, | 161 | operations: input.manifestEntries.map(mcpCatalogOperationFromManifestEntry), |
| 200 | }, | 162 | }), |
| 201 | ], | 163 | sourceHash: manifestHash, |
| 202 | schemaBundles: [], | ||
| 203 | operations: input.manifestEntries.map((entry) => | ||
| 204 | toMcpRecipeOperationRecord({ | ||
| 205 | recipeRevisionId: input.recipeRevisionId, | ||
| 206 | entry, | ||
| 207 | now, | ||
| 208 | }) | ||
| 209 | ), | ||
| 210 | }; | 164 | }; |
| 211 | }; | 165 | }; |
| 212 | 166 | ||
| @@ -217,8 +171,6 @@ export const mcpSourceAdapter: SourceAdapter = { · 4 unmodified lines | |||
| 217 | bindingConfigVersion: MCP_BINDING_CONFIG_VERSION, | 171 | bindingConfigVersion: MCP_BINDING_CONFIG_VERSION, |
| 218 | providerKey: "generic_mcp", | 172 | providerKey: "generic_mcp", |
| 219 | defaultImportAuthPolicy: "reuse_runtime", | 173 | defaultImportAuthPolicy: "reuse_runtime", |
| 220 | primaryDocumentKind: "mcp_manifest", | ||
| 221 | primarySchemaBundleKind: null, | ||
| 222 | connectPayloadSchema: McpConnectPayloadSchema, | 174 | connectPayloadSchema: McpConnectPayloadSchema, |
| 223 | executorAddInputSchema: McpExecutorAddInputSchema, | 175 | executorAddInputSchema: McpExecutorAddInputSchema, |
| 224 | executorAddHelpText: [ | 176 | executorAddHelpText: [ |
| @@ -280,53 +232,7 @@ export const mcpSourceAdapter: SourceAdapter = { · 55 unmodified lines | |||
| 280 | }; | 232 | }; |
| 281 | }), | 233 | }), |
| 282 | shouldAutoProbe: () => false, | 234 | shouldAutoProbe: () => false, |
| 283 | parseManifest: ({ source, manifestJson }) => | 235 | syncCatalog: ({ source, resolveAuthMaterialForSlot }) => |
| 284 | parseJsonValue<McpToolManifest>({ | ||
| 285 | label: `MCP manifest for ${source.id}`, | ||
| 286 | value: manifestJson, | ||
| 287 | }), | ||
| 288 | describePersistedOperation: ({ operation, path }) => | ||
| 289 | Effect.gen(function* () { | ||
| 290 | const decoded = operation.providerDataJson | ||
| 291 | ? decodeMcpToolProviderDataJson(operation.providerDataJson) | ||
| 292 | : null; | ||
| 293 | if (decoded && decoded._tag === "Left") { | ||
| 294 | return yield* Effect.fail( | ||
| 295 | new Error(`Invalid MCP provider data for ${path}`), | ||
| 296 | ); | ||
| 297 | } | ||
| 298 | |||
| 299 | const providerData = decoded?._tag === "Right" ? decoded.right : null; | ||
| 300 | |||
| 301 | return { | ||
| 302 | method: null, | ||
| 303 | pathTemplate: null, | ||
| 304 | rawToolId: providerData?.toolId ?? null, | ||
| 305 | operationId: null, | ||
| 306 | group: null, | ||
| 307 | leaf: null, | ||
| 308 | tags: [], | ||
| 309 | searchText: normalizeSearchText( | ||
| 310 | path, | ||
| 311 | operation.toolId, | ||
| 312 | providerData?.toolName ?? operation.title ?? undefined, | ||
| 313 | providerData?.description ?? operation.description ?? undefined, | ||
| 314 | operation.searchText, | ||
| 315 | ), | ||
| 316 | interaction: "auto", | ||
| 317 | approvalLabel: null, | ||
| 318 | } as const; | ||
| 319 | }), | ||
| 320 | createToolDescriptor: ({ source, operation, path, includeSchemas, schemaBundleId }) => | ||
| 321 | createStandardToolDescriptor({ | ||
| 322 | source, | ||
| 323 | operation, | ||
| 324 | path, | ||
| 325 | includeSchemas, | ||
| 326 | interaction: "auto", | ||
| 327 | schemaBundleId, | ||
| 328 | }), | ||
| 329 | materializeSource: ({ source, resolveAuthMaterialForSlot }) => | ||
| 330 | Effect.gen(function* () { | 236 | Effect.gen(function* () { |
| 331 | const bindingConfig = yield* mcpBindingConfigFromSource(source); | 237 | const bindingConfig = yield* mcpBindingConfigFromSource(source); |
| 332 | const auth = yield* resolveAuthMaterialForSlot("import"); | 238 | const auth = yield* resolveAuthMaterialForSlot("import"); |
| @@ -362,64 +268,10 @@ export const mcpSourceAdapter: SourceAdapter = { · 29 unmodified lines | |||
| 362 | ), | 268 | ), |
| 363 | ); | 269 | ); |
| 364 | 270 | ||
| 365 | return materializationFromMcpManifestEntries({ | 271 | return catalogSyncResultFromMcpManifestEntries({ |
| 366 | recipeRevisionId: "src_recipe_rev_materialization" as SourceRecipeRevisionId, | 272 | source, |
| 367 | endpoint: source.endpoint, | 273 | endpoint: source.endpoint, |
| 368 | manifestEntries: discovered.manifest.tools, | 274 | manifestEntries: discovered.manifest.tools, |
| 369 | }) satisfies SourceAdapterMaterialization; | ||
| 370 | }), | ||
| 371 | invokePersistedTool: ({ | ||
| 372 | source, | ||
| 373 | path, | ||
| 374 | manifestJson, | ||
| 375 | auth, | ||
| 376 | args, | ||
| 377 | context, | ||
| 378 | onElicitation, | ||
| 379 | }) => | ||
| 380 | Effect.gen(function* () { | ||
| 381 | const bindingConfig = yield* mcpBindingConfigFromSource(source); | ||
| 382 | const manifest = yield* parseJsonValue<McpToolManifest>({ | ||
| 383 | label: `MCP manifest for ${source.id}`, | ||
| 384 | value: manifestJson, | ||
| 385 | }); | 275 | }); |
| 386 | if (manifest === null) { | ||
| 387 | return yield* Effect.fail( | ||
| 388 | new Error(`Missing MCP manifest for ${source.id}`), | ||
| 389 | ); | ||
| 390 | } | ||
| 391 | |||
| 392 | const tools = createMcpToolsFromManifest({ | ||
| 393 | manifest, | ||
| 394 | connect: createSdkMcpConnector({ | ||
| 395 | endpoint: source.endpoint, | ||
| 396 | transport: bindingConfig.transport ?? undefined, | ||
| 397 | queryParams: { | ||
| 398 | ...(bindingConfig.queryParams ?? {}), | ||
| 399 | ...auth.queryParams, | ||
| 400 | }, | ||
| 401 | headers: headersWithAuthCookies({ | ||
| 402 | headers: bindingConfig.headers ?? {}, | ||
| 403 | authHeaders: auth.headers, | ||
| 404 | authCookies: auth.cookies, | ||
| 405 | }), | ||
| 406 | }), | ||
| 407 | namespace: source.namespace ?? namespaceFromSourceName(source.name), | ||
| 408 | sourceKey: source.id, | ||
| 409 | }); | ||
| 410 | |||
| 411 | return yield* makeToolInvokerFromTools({ | ||
| 412 | tools, | ||
| 413 | onToolInteraction: allowAllToolInteractions, | ||
| 414 | onElicitation, | ||
| 415 | }).invoke({ | ||
| 416 | path, | ||
| 417 | args, | ||
| 418 | context, | ||
| 419 | }).pipe( | ||
| 420 | Effect.mapError((cause) => | ||
| 421 | cause instanceof Error ? cause : new Error(String(cause)), | ||
| 422 | ), | ||
| 423 | ); | ||
| 424 | }), | 276 | }), |
| 425 | }; | 277 | }; |
| @@ -4,55 +4,34 @@ import { · 3 unmodified lines | |||
| 4 | HttpClientRequest, | 4 | HttpClientRequest, |
| 5 | } from "@effect/platform"; | 5 | } from "@effect/platform"; |
| 6 | import { | 6 | import { |
| 7 | allowAllToolInteractions, | ||
| 8 | applyCookiePlacementsToHeaders, | 7 | applyCookiePlacementsToHeaders, |
| 9 | applyHttpQueryPlacementsToUrl, | 8 | applyHttpQueryPlacementsToUrl, |
| 10 | makeToolInvokerFromTools, | ||
| 11 | } from "@executor/codemode-core"; | 9 | } from "@executor/codemode-core"; |
| 12 | import { | 10 | import { |
| 13 | buildOpenApiToolPresentation, | 11 | buildOpenApiToolPresentation, |
| 14 | compileOpenApiToolDefinitions, | 12 | compileOpenApiToolDefinitions, |
| 15 | createOpenApiToolFromDefinition, | 13 | type OpenApiToolProviderData, |
| 16 | extractOpenApiManifest, | 14 | extractOpenApiManifest, |
| 17 | openApiOutputTypeSignatureFromSchemaJson, | ||
| 18 | OpenApiRefHintTableSchema, | ||
| 19 | OpenApiToolProviderDataSchema, | ||
| 20 | type OpenApiToolManifest, | ||
| 21 | } from "@executor/codemode-openapi"; | 15 | } from "@executor/codemode-openapi"; |
| 22 | import type { | 16 | import type { Source } from "#schema"; |
| 23 | Source, | 17 | import { StringMapSchema } from "#schema"; |
| 24 | SourceRecipeRevisionId, | ||
| 25 | StoredSourceRecipeDocumentRecord, | ||
| 26 | StoredSourceRecipeOperationRecord, | ||
| 27 | StoredSourceRecipeSchemaBundleRecord, | ||
| 28 | } from "#schema"; | ||
| 29 | import { | ||
| 30 | StringMapSchema, | ||
| 31 | SourceRecipeSchemaBundleIdSchema, | ||
| 32 | } from "#schema"; | ||
| 33 | import * as Either from "effect/Either"; | ||
| 34 | import * as Effect from "effect/Effect"; | 18 | import * as Effect from "effect/Effect"; |
| 35 | import * as Schema from "effect/Schema"; | 19 | import * as Schema from "effect/Schema"; |
| 36 | 20 | ||
| 37 | import { | 21 | import { |
| 38 | contentHash, | 22 | createOpenApiCatalogSnapshot, |
| 39 | normalizeSearchText, | 23 | type OpenApiCatalogOperationInput, |
| 40 | } from "../source-recipe-support"; | 24 | } from "../source-catalog-snapshot"; |
| 41 | import type { | 25 | import type { SourceAdapter } from "./types"; |
| 42 | SourceAdapter, | ||
| 43 | SourceAdapterMaterialization, | ||
| 44 | } from "./types"; | ||
| 45 | import { | 26 | import { |
| 46 | ConnectHttpAuthSchema, | 27 | ConnectHttpAuthSchema, |
| 47 | ConnectHttpImportAuthSchema, | 28 | ConnectHttpImportAuthSchema, |
| 48 | createStandardToolDescriptor, | ||
| 49 | decodeBindingConfig, | 29 | decodeBindingConfig, |
| 50 | decodeSourceBindingPayload, | 30 | decodeSourceBindingPayload, |
| 51 | emptySourceBindingState, | 31 | emptySourceBindingState, |
| 52 | encodeBindingConfig, | 32 | encodeBindingConfig, |
| 53 | isSourceCredentialRequiredError, | 33 | isSourceCredentialRequiredError, |
| 54 | OptionalNullableStringSchema, | 34 | OptionalNullableStringSchema, |
| 55 | parseJsonValue, | ||
| 56 | SourceCredentialRequiredError, | 35 | SourceCredentialRequiredError, |
| 57 | SourceConnectCommonFieldsSchema, | 36 | SourceConnectCommonFieldsSchema, |
| 58 | } from "./shared"; | 37 | } from "./shared"; |
| @@ -98,16 +77,6 @@ type OpenApiBindingConfig = { · 39 unmodified lines | |||
| 98 | 77 | ||
| 99 | const OPENAPI_BINDING_CONFIG_VERSION = 1; | 78 | const OPENAPI_BINDING_CONFIG_VERSION = 1; |
| 100 | 79 | ||
| 101 | const OPENAPI_MATERIALIZATION_REVISION_ID = "src_recipe_rev_materialization" as SourceRecipeRevisionId; | ||
| 102 | |||
| 103 | const decodeOpenApiProviderDataJson = Schema.decodeUnknownEither( | ||
| 104 | Schema.parseJson(OpenApiToolProviderDataSchema), | ||
| 105 | ); | ||
| 106 | |||
| 107 | const decodeOpenApiRefHintTableJson = Schema.decodeUnknownEither( | ||
| 108 | Schema.parseJson(OpenApiRefHintTableSchema), | ||
| 109 | ); | ||
| 110 | |||
| 111 | const bindingHasAnyField = ( | 80 | const bindingHasAnyField = ( |
| 112 | value: unknown, | 81 | value: unknown, |
| 113 | fields: readonly string[], | 82 | fields: readonly string[], |
| @@ -198,137 +167,30 @@ const fetchOpenApiDocumentWithHeaders = (input: { · 84 unmodified lines | |||
| 198 | ); | 167 | ); |
| 199 | }).pipe(Effect.provide(FetchHttpClient.layer)); | 168 | }).pipe(Effect.provide(FetchHttpClient.layer)); |
| 200 | 169 | ||
| 201 | const toOpenApiRecipeOperationRecord = (input: { | 170 | const openApiCatalogOperationFromDefinition = ( |
| 202 | recipeRevisionId: SourceRecipeRevisionId; | 171 | definition: ReturnType<typeof compileOpenApiToolDefinitions>[number], |
| 203 | definition: ReturnType<typeof compileOpenApiToolDefinitions>[number]; | 172 | ): OpenApiCatalogOperationInput => { |
| 204 | now: number; | ||
| 205 | }): StoredSourceRecipeOperationRecord => { | ||
| 206 | const presentation = buildOpenApiToolPresentation({ | 173 | const presentation = buildOpenApiToolPresentation({ |
| 207 | definition: input.definition, | 174 | definition, |
| 208 | }); | 175 | }); |
| 209 | const method = input.definition.method.toUpperCase(); | 176 | const method = definition.method.toUpperCase(); |
| 210 | 177 | ||
| 211 | return { | 178 | return { |
| 212 | id: `src_recipe_op_${crypto.randomUUID()}`, | 179 | toolId: definition.toolId, |
| 213 | recipeRevisionId: input.recipeRevisionId, | 180 | title: definition.name, |
| 214 | operationKey: input.definition.toolId, | 181 | description: definition.description, |
| 215 | transportKind: "http", | 182 | effect: |
| 216 | toolId: input.definition.toolId, | ||
| 217 | title: input.definition.name, | ||
| 218 | description: input.definition.description, | ||
| 219 | operationKind: | ||
| 220 | method === "GET" || method === "HEAD" | 183 | method === "GET" || method === "HEAD" |
| 221 | ? "read" | 184 | ? "read" |
| 222 | : method === "DELETE" | 185 | : method === "DELETE" |
| 223 | ? "delete" | 186 | ? "delete" |
| 224 | : "write", | 187 | : "write", |
| 225 | searchText: normalizeSearchText( | 188 | inputSchema: presentation.inputSchema, |
| 226 | input.definition.toolId, | 189 | outputSchema: presentation.outputSchema, |
| 227 | input.definition.name, | 190 | providerData: presentation.providerData as OpenApiToolProviderData, |
| 228 | input.definition.description, | ||
| 229 | input.definition.rawToolId, | ||
| 230 | input.definition.operationId ?? undefined, | ||
| 231 | input.definition.method, | ||
| 232 | input.definition.path, | ||
| 233 | input.definition.group, | ||
| 234 | input.definition.leaf, | ||
| 235 | input.definition.tags.join(" "), | ||
| 236 | ), | ||
| 237 | inputSchemaJson: presentation.inputSchemaJson ?? null, | ||
| 238 | outputSchemaJson: presentation.outputSchemaJson ?? null, | ||
| 239 | providerKind: "openapi", | ||
| 240 | providerDataJson: presentation.providerDataJson, | ||
| 241 | createdAt: input.now, | ||
| 242 | updatedAt: input.now, | ||
| 243 | }; | ||
| 244 | }; | ||
| 245 | |||
| 246 | const toOpenApiSchemaBundleRecord = (input: { | ||
| 247 | recipeRevisionId: SourceRecipeRevisionId; | ||
| 248 | refTable: Readonly<Record<string, string>> | undefined; | ||
| 249 | now: number; | ||
| 250 | }): StoredSourceRecipeSchemaBundleRecord | null => { | ||
| 251 | const refEntries = Object.entries(input.refTable ?? {}); | ||
| 252 | if (refEntries.length === 0) { | ||
| 253 | return null; | ||
| 254 | } | ||
| 255 | |||
| 256 | const refs = Object.fromEntries( | ||
| 257 | refEntries.map(([ref, rawValue]) => { | ||
| 258 | try { | ||
| 259 | return [ref, JSON.parse(rawValue) as unknown]; | ||
| 260 | } catch { | ||
| 261 | return [ref, rawValue]; | ||
| 262 | } | ||
| 263 | }), | ||
| 264 | ); | ||
| 265 | const refsJson = JSON.stringify(refs); | ||
| 266 | |||
| 267 | return { | ||
| 268 | id: SourceRecipeSchemaBundleIdSchema.make( | ||
| 269 | `src_recipe_bundle_${crypto.randomUUID()}`, | ||
| 270 | ), | ||
| 271 | recipeRevisionId: input.recipeRevisionId, | ||
| 272 | bundleKind: "json_schema_ref_map", | ||
| 273 | refsJson, | ||
| 274 | contentHash: contentHash(refsJson), | ||
| 275 | createdAt: input.now, | ||
| 276 | updatedAt: input.now, | ||
| 277 | }; | ||
| 278 | }; | ||
| 279 | |||
| 280 | const openApiDefinitionFromPersistedOperation = (input: { | ||
| 281 | source: Source; | ||
| 282 | path: string; | ||
| 283 | operation: StoredSourceRecipeOperationRecord; | ||
| 284 | }) => { | ||
| 285 | const decoded = input.operation.providerDataJson | ||
| 286 | ? decodeOpenApiProviderDataJson(input.operation.providerDataJson) | ||
| 287 | : Either.left(new Error("Missing providerDataJson")); | ||
| 288 | |||
| 289 | if (Either.isLeft(decoded)) { | ||
| 290 | throw new Error(`Invalid OpenAPI provider data for ${input.path}`); | ||
| 291 | } | ||
| 292 | |||
| 293 | return { | ||
| 294 | toolId: input.operation.toolId, | ||
| 295 | rawToolId: decoded.right.rawToolId, | ||
| 296 | ...(decoded.right.operationId | ||
| 297 | ? { operationId: decoded.right.operationId } | ||
| 298 | : {}), | ||
| 299 | name: input.operation.title ?? input.operation.toolId, | ||
| 300 | description: | ||
| 301 | input.operation.description ?? | ||
| 302 | `${decoded.right.method.toUpperCase()} ${decoded.right.path}`, | ||
| 303 | group: decoded.right.group, | ||
| 304 | leaf: decoded.right.leaf, | ||
| 305 | tags: decoded.right.tags, | ||
| 306 | ...(decoded.right.versionSegment | ||
| 307 | ? { versionSegment: decoded.right.versionSegment } | ||
| 308 | : {}), | ||
| 309 | method: decoded.right.method, | ||
| 310 | path: decoded.right.path, | ||
| 311 | invocation: decoded.right.invocation, | ||
| 312 | operationHash: decoded.right.operationHash, | ||
| 313 | typing: { | ||
| 314 | ...(input.operation.inputSchemaJson | ||
| 315 | ? { inputSchemaJson: input.operation.inputSchemaJson } | ||
| 316 | : {}), | ||
| 317 | ...(input.operation.outputSchemaJson | ||
| 318 | ? { outputSchemaJson: input.operation.outputSchemaJson } | ||
| 319 | : {}), | ||
| 320 | }, | ||
| 321 | ...(decoded.right.documentation | ||
| 322 | ? { documentation: decoded.right.documentation } | ||
| 323 | : {}), | ||
| 324 | }; | 191 | }; |
| 325 | }; | 192 | }; |
| 326 | 193 | ||
| 327 | const primaryDocument = ( | ||
| 328 | documents: readonly StoredSourceRecipeDocumentRecord[], | ||
| 329 | ): StoredSourceRecipeDocumentRecord | null => | ||
| 330 | documents.find((document) => document.documentKind === "openapi") ?? null; | ||
| 331 | |||
| 332 | export const openApiSourceAdapter: SourceAdapter = { | 194 | export const openApiSourceAdapter: SourceAdapter = { |
| 333 | key: "openapi", | 195 | key: "openapi", |
| 334 | displayName: "OpenAPI", | 196 | displayName: "OpenAPI", |
| @@ -336,8 +198,6 @@ export const openApiSourceAdapter: SourceAdapter = { · 1 unmodified line | |||
| 336 | bindingConfigVersion: OPENAPI_BINDING_CONFIG_VERSION, | 198 | bindingConfigVersion: OPENAPI_BINDING_CONFIG_VERSION, |
| 337 | providerKey: "generic_http", | 199 | providerKey: "generic_http", |
| 338 | defaultImportAuthPolicy: "reuse_runtime", | 200 | defaultImportAuthPolicy: "reuse_runtime", |
| 339 | primaryDocumentKind: "openapi", | ||
| 340 | primarySchemaBundleKind: "json_schema_ref_map", | ||
| 341 | connectPayloadSchema: OpenApiConnectPayloadSchema, | 201 | connectPayloadSchema: OpenApiConnectPayloadSchema, |
| 342 | executorAddInputSchema: OpenApiExecutorAddInputSchema, | 202 | executorAddInputSchema: OpenApiExecutorAddInputSchema, |
| 343 | executorAddHelpText: [ | 203 | executorAddHelpText: [ |
| @@ -398,65 +258,7 @@ export const openApiSourceAdapter: SourceAdapter = { · 54 unmodified lines | |||
| 398 | }), | 258 | }), |
| 399 | shouldAutoProbe: (source) => | 259 | shouldAutoProbe: (source) => |
| 400 | source.enabled && (source.status === "draft" || source.status === "probing"), | 260 | source.enabled && (source.status === "draft" || source.status === "probing"), |
| 401 | parseManifest: ({ source, manifestJson }) => | 261 | syncCatalog: ({ source, resolveAuthMaterialForSlot }) => |
| 402 | parseJsonValue<OpenApiToolManifest>({ | ||
| 403 | label: `OpenAPI manifest for ${source.id}`, | ||
| 404 | value: manifestJson, | ||
| 405 | }), | ||
| 406 | describePersistedOperation: ({ operation, path }) => | ||
| 407 | Effect.gen(function* () { | ||
| 408 | const decoded = operation.providerDataJson | ||
| 409 | ? decodeOpenApiProviderDataJson(operation.providerDataJson) | ||
| 410 | : Either.left(new Error("Missing providerDataJson")); | ||
| 411 | if (Either.isLeft(decoded)) { | ||
| 412 | return yield* Effect.fail( | ||
| 413 | new Error(`Invalid OpenAPI provider data for ${path}`), | ||
| 414 | ); | ||
| 415 | } | ||
| 416 | |||
| 417 | const providerData = decoded.right; | ||
| 418 | const method = providerData.method.toUpperCase(); | ||
| 419 | |||
| 420 | return { | ||
| 421 | method: providerData.method, | ||
| 422 | pathTemplate: providerData.path, | ||
| 423 | rawToolId: providerData.rawToolId, | ||
| 424 | operationId: providerData.operationId ?? null, | ||
| 425 | group: providerData.group, | ||
| 426 | leaf: providerData.leaf, | ||
| 427 | tags: providerData.tags, | ||
| 428 | searchText: normalizeSearchText( | ||
| 429 | path, | ||
| 430 | operation.toolId, | ||
| 431 | operation.title ?? undefined, | ||
| 432 | operation.description ?? undefined, | ||
| 433 | providerData.rawToolId, | ||
| 434 | providerData.operationId ?? undefined, | ||
| 435 | method, | ||
| 436 | providerData.path, | ||
| 437 | providerData.group, | ||
| 438 | providerData.leaf, | ||
| 439 | providerData.tags.join(" "), | ||
| 440 | operation.searchText, | ||
| 441 | ), | ||
| 442 | interaction: method === "GET" || method === "HEAD" ? "auto" : "required", | ||
| 443 | approvalLabel: `${method} ${providerData.path}`, | ||
| 444 | } as const; | ||
| 445 | }), | ||
| 446 | createToolDescriptor: ({ source, operation, path, includeSchemas, schemaBundleId }) => | ||
| 447 | createStandardToolDescriptor({ | ||
| 448 | source, | ||
| 449 | operation, | ||
| 450 | path, | ||
| 451 | includeSchemas, | ||
| 452 | interaction: operation.operationKind === "read" ? "auto" : "required", | ||
| 453 | outputType: openApiOutputTypeSignatureFromSchemaJson( | ||
| 454 | operation.outputSchemaJson ?? undefined, | ||
| 455 | 320, | ||
| 456 | ), | ||
| 457 | schemaBundleId, | ||
| 458 | }), | ||
| 459 | materializeSource: ({ source, resolveAuthMaterialForSlot }) => | ||
| 460 | Effect.gen(function* () { | 262 | Effect.gen(function* () { |
| 461 | const bindingConfig = yield* openApiBindingConfigFromSource(source); | 263 | const bindingConfig = yield* openApiBindingConfigFromSource(source); |
| 462 | 264 | ||
| @@ -491,89 +293,19 @@ export const openApiSourceAdapter: SourceAdapter = { · 28 unmodified lines | |||
| 491 | 293 | ||
| 492 | const definitions = compileOpenApiToolDefinitions(manifest); | 294 | const definitions = compileOpenApiToolDefinitions(manifest); |
| 493 | const now = Date.now(); | 295 | const now = Date.now(); |
| 494 | const schemaBundle = toOpenApiSchemaBundleRecord({ | ||
| 495 | recipeRevisionId: OPENAPI_MATERIALIZATION_REVISION_ID, | ||
| 496 | refTable: manifest.refHintTable, | ||
| 497 | now, | ||
| 498 | }); | ||
| 499 | 296 | ||
| 500 | return { | 297 | return { |
| 501 | manifestJson: JSON.stringify(manifest), | 298 | snapshot: createOpenApiCatalogSnapshot({ |
| 502 | manifestHash: manifest.sourceHash, | 299 | source, |
| 503 | sourceHash: manifest.sourceHash, | 300 | documents: [{ |
| 504 | documents: [ | ||
| 505 | { | ||
| 506 | id: `src_recipe_doc_${crypto.randomUUID()}`, | ||
| 507 | recipeRevisionId: OPENAPI_MATERIALIZATION_REVISION_ID, | ||
| 508 | documentKind: "openapi", | 301 | documentKind: "openapi", |
| 509 | documentKey: bindingConfig.specUrl, | 302 | documentKey: bindingConfig.specUrl, |
| 510 | contentText: openApiDocument, | 303 | contentText: openApiDocument, |
| 511 | contentHash: contentHash(openApiDocument), | ||
| 512 | fetchedAt: now, | 304 | fetchedAt: now, |
| 513 | createdAt: now, | 305 | }], |
| 514 | updatedAt: now, | 306 | operations: definitions.map(openApiCatalogOperationFromDefinition), |
| 515 | }, | ||
| 516 | ], | ||
| 517 | schemaBundles: schemaBundle ? [schemaBundle] : [], | ||
| 518 | operations: definitions.map((definition) => | ||
| 519 | toOpenApiRecipeOperationRecord({ | ||
| 520 | recipeRevisionId: OPENAPI_MATERIALIZATION_REVISION_ID, | ||
| 521 | definition, | ||
| 522 | now, | ||
| 523 | }), | ||
| 524 | ), | ||
| 525 | } satisfies SourceAdapterMaterialization; | ||
| 526 | }), | ||
| 527 | invokePersistedTool: ({ | ||
| 528 | source, | ||
| 529 | path, | ||
| 530 | operation, | ||
| 531 | schemaBundle, | ||
| 532 | auth, | ||
| 533 | args, | ||
| 534 | context, | ||
| 535 | onElicitation, | ||
| 536 | }) => | ||
| 537 | Effect.gen(function* () { | ||
| 538 | const bindingConfig = yield* openApiBindingConfigFromSource(source); | ||
| 539 | const refHintTable = schemaBundle | ||
| 540 | ? decodeOpenApiRefHintTableJson(schemaBundle.refsJson) | ||
| 541 | : Either.right({}); | ||
| 542 | |||
| 543 | if (Either.isLeft(refHintTable)) { | ||
| 544 | return yield* Effect.fail( | ||
| 545 | new Error(`Invalid OpenAPI schema bundle for ${path}`), | ||
| 546 | ); | ||
| 547 | } | ||
| 548 | |||
| 549 | const tool = createOpenApiToolFromDefinition({ | ||
| 550 | definition: openApiDefinitionFromPersistedOperation({ | ||
| 551 | source, | ||
| 552 | path, | ||
| 553 | operation, | ||
| 554 | }), | 307 | }), |
| 555 | path, | 308 | sourceHash: manifest.sourceHash, |
| 556 | sourceKey: source.id, | 309 | }; |
| 557 | baseUrl: source.endpoint, | ||
| 558 | defaultHeaders: bindingConfig.defaultHeaders ?? {}, | ||
| 559 | credentialPlacements: auth, | ||
| 560 | refHintTable: refHintTable.right, | ||
| 561 | }); | ||
| 562 | |||
| 563 | return yield* makeToolInvokerFromTools({ | ||
| 564 | tools: { | ||
| 565 | [path]: tool, | ||
| 566 | }, | ||
| 567 | onToolInteraction: allowAllToolInteractions, | ||
| 568 | onElicitation, | ||
| 569 | }).invoke({ | ||
| 570 | path, | ||
| 571 | args, | ||
| 572 | context, | ||
| 573 | }).pipe( | ||
| 574 | Effect.mapError((cause) => | ||
| 575 | cause instanceof Error ? cause : new Error(String(cause)), | ||
| 576 | ), | ||
| 577 | ); | ||
| 578 | }), | 310 | }), |
| 579 | }; | 311 | }; |
| @@ -1,9 +1,3 @@ | |||
| 1 | import { | ||
| 2 | type ToolDescriptor, | ||
| 3 | type ToolPath, | ||
| 4 | type ToolSchemaBundle, | ||
| 5 | typeSignatureFromSchemaJson, | ||
| 6 | } from "@executor/codemode-core"; | ||
| 7 | import { | 1 | import { |
| 8 | SourceOauthClientInputSchema, | 2 | SourceOauthClientInputSchema, |
| 9 | type CredentialSlot, | 3 | type CredentialSlot, |
| @@ -15,10 +9,8 @@ import { · 5 unmodified lines | |||
| 15 | } from "#schema"; | 9 | } from "#schema"; |
| 16 | import type { | 10 | import type { |
| 17 | SecretRef, | 11 | SecretRef, |
| 18 | Source, | ||
| 19 | SourceBinding, | 12 | SourceBinding, |
| 20 | SourceTransport, | 13 | SourceTransport, |
| 21 | StoredSourceRecipeOperationRecord, | ||
| 22 | StringMap, | 14 | StringMap, |
| 23 | } from "#schema"; | 15 | } from "#schema"; |
| 24 | import * as Effect from "effect/Effect"; | 16 | import * as Effect from "effect/Effect"; |
| @@ -28,8 +20,6 @@ import { · 3 unmodified lines | |||
| 28 | TrimmedNonEmptyStringSchema, | 20 | TrimmedNonEmptyStringSchema, |
| 29 | } from "../../api/string-schemas"; | 21 | } from "../../api/string-schemas"; |
| 30 | 22 | ||
| 31 | const asToolPath = (value: string): ToolPath => value as ToolPath; | ||
| 32 | |||
| 33 | export const OptionalNullableStringSchema = Schema.optional( | 23 | export const OptionalNullableStringSchema = Schema.optional( |
| 34 | Schema.NullOr(Schema.String), | 24 | Schema.NullOr(Schema.String), |
| 35 | ); | 25 | ); |
| @@ -236,43 +226,3 @@ export const decodeSourceBindingPayload = <A>(input: { · 200 unmodified lines | |||
| 236 | ); | 226 | ); |
| 237 | }, | 227 | }, |
| 238 | }); | 228 | }); |
| 239 | |||
| 240 | export const firstSchemaBundle = (input: { | ||
| 241 | schemaBundles: readonly ToolSchemaBundle[]; | ||
| 242 | preferredKind: string | null; | ||
| 243 | }): ToolSchemaBundle | null => | ||
| 244 | (input.preferredKind | ||
| 245 | ? input.schemaBundles.find((schemaBundle) => schemaBundle.kind === input.preferredKind) | ||
| 246 | : null) | ||
| 247 | ?? input.schemaBundles[0] | ||
| 248 | ?? null; | ||
| 249 | |||
| 250 | export const createStandardToolDescriptor = (input: { | ||
| 251 | source: Source; | ||
| 252 | operation: StoredSourceRecipeOperationRecord; | ||
| 253 | path: string; | ||
| 254 | includeSchemas: boolean; | ||
| 255 | interaction: "auto" | "required"; | ||
| 256 | outputType?: string | undefined; | ||
| 257 | schemaBundleId?: string | null; | ||
| 258 | }): ToolDescriptor => ({ | ||
| 259 | path: asToolPath(input.path), | ||
| 260 | sourceKey: input.source.id, | ||
| 261 | description: input.operation.description ?? input.operation.title ?? undefined, | ||
| 262 | interaction: input.interaction, | ||
| 263 | inputType: typeSignatureFromSchemaJson( | ||
| 264 | input.operation.inputSchemaJson ?? undefined, | ||
| 265 | "unknown", | ||
| 266 | 320, | ||
| 267 | ), | ||
| 268 | ...(input.outputType ? { outputType: input.outputType } : {}), | ||
| 269 | inputSchemaJson: input.includeSchemas ? input.operation.inputSchemaJson ?? undefined : undefined, | ||
| 270 | outputSchemaJson: input.includeSchemas ? input.operation.outputSchemaJson ?? undefined : undefined, | ||
| 271 | ...(input.schemaBundleId ? { schemaBundleId: input.schemaBundleId } : {}), | ||
| 272 | ...(input.operation.providerKind | ||
| 273 | ? { providerKind: input.operation.providerKind } | ||
| 274 | : {}), | ||
| 275 | ...(input.operation.providerDataJson | ||
| 276 | ? { providerDataJson: input.operation.providerDataJson } | ||
| 277 | : {}), | ||
| 278 | }); | ||
| @@ -1,10 +1,4 @@ | |||
| 1 | import type { | 1 | import type { |
| 2 | OnElicitation, | ||
| 3 | ToolDescriptor, | ||
| 4 | ToolSchemaBundle, | ||
| 5 | } from "@executor/codemode-core"; | ||
| 6 | import type { | ||
| 7 | AccountId, | ||
| 8 | CredentialSlot, | 2 | CredentialSlot, |
| 9 | OAuth2ClientAuthenticationMethod, | 3 | OAuth2ClientAuthenticationMethod, |
| 10 | SourceOauthClientInput, | 4 | SourceOauthClientInput, |
| @@ -12,10 +6,6 @@ import type { · 1 unmodified line | |||
| 12 | SourceBinding, | 6 | SourceBinding, |
| 13 | SourceImportAuthPolicy, | 7 | SourceImportAuthPolicy, |
| 14 | SourceTransport, | 8 | SourceTransport, |
| 15 | StoredSourceRecipeDocumentRecord, | ||
| 16 | StoredSourceRecipeOperationRecord, | ||
| 17 | StoredSourceRecipeSchemaBundleRecord, | ||
| 18 | StoredSourceRecipeRevisionRecord, | ||
| 19 | StoredSourceRecord, | 9 | StoredSourceRecord, |
| 20 | StringMap, | 10 | StringMap, |
| 21 | } from "#schema"; | 11 | } from "#schema"; |
| @@ -24,6 +14,7 @@ import * as Schema from "effect/Schema"; · 2 unmodified lines | |||
| 24 | 14 | ||
| 25 | import type { ResolvedSourceAuthMaterial } from "../source-auth-material"; | 15 | import type { ResolvedSourceAuthMaterial } from "../source-auth-material"; |
| 26 | import type { ResolveSecretMaterial } from "../secret-material-providers"; | 16 | import type { ResolveSecretMaterial } from "../secret-material-providers"; |
| 17 | import type { SourceCatalogSyncResult } from "../source-catalog-support"; | ||
| 27 | 18 | ||
| 28 | export type SourceAdapterFamily = "http_api" | "mcp" | "internal"; | 19 | export type SourceAdapterFamily = "http_api" | "mcp" | "internal"; |
| 29 | export type SourceAdapterInputSchema = Schema.Schema<any, any, never>; | 20 | export type SourceAdapterInputSchema = Schema.Schema<any, any, never>; |
| @@ -47,28 +38,6 @@ export type SourceAdapterSyncInput = { · 17 unmodified lines | |||
| 47 | >; | 38 | >; |
| 48 | }; | 39 | }; |
| 49 | 40 | ||
| 50 | export type SourceAdapterMaterialization = { | ||
| 51 | manifestJson: string | null; | ||
| 52 | manifestHash: string | null; | ||
| 53 | sourceHash: string | null; | ||
| 54 | documents: readonly StoredSourceRecipeDocumentRecord[]; | ||
| 55 | schemaBundles: readonly StoredSourceRecipeSchemaBundleRecord[]; | ||
| 56 | operations: readonly StoredSourceRecipeOperationRecord[]; | ||
| 57 | }; | ||
| 58 | |||
| 59 | export type SourceAdapterPersistedOperationMetadata = { | ||
| 60 | method: string | null; | ||
| 61 | pathTemplate: string | null; | ||
| 62 | rawToolId: string | null; | ||
| 63 | operationId: string | null; | ||
| 64 | group: string | null; | ||
| 65 | leaf: string | null; | ||
| 66 | tags: readonly string[]; | ||
| 67 | searchText: string; | ||
| 68 | interaction: "auto" | "required"; | ||
| 69 | approvalLabel: string | null; | ||
| 70 | }; | ||
| 71 | |||
| 72 | export type SourceAdapterOauth2SetupConfig = { | 41 | export type SourceAdapterOauth2SetupConfig = { |
| 73 | providerKey: string; | 42 | providerKey: string; |
| 74 | authorizationEndpoint: string; | 43 | authorizationEndpoint: string; |
| @@ -80,20 +49,6 @@ export type SourceAdapterOauth2SetupConfig = { · 5 unmodified lines | |||
| 80 | authorizationParams?: Readonly<Record<string, string>>; | 49 | authorizationParams?: Readonly<Record<string, string>>; |
| 81 | }; | 50 | }; |
| 82 | 51 | ||
| 83 | export type SourceAdapterInvokePersistedToolInput = { | ||
| 84 | workspaceId: Source["workspaceId"]; | ||
| 85 | accountId: AccountId; | ||
| 86 | source: Source; | ||
| 87 | path: string; | ||
| 88 | operation: StoredSourceRecipeOperationRecord; | ||
| 89 | schemaBundle: ToolSchemaBundle | null; | ||
| 90 | manifestJson: string | null; | ||
| 91 | auth: ResolvedSourceAuthMaterial; | ||
| 92 | args: unknown; | ||
| 93 | context?: Record<string, unknown>; | ||
| 94 | onElicitation?: OnElicitation; | ||
| 95 | }; | ||
| 96 | |||
| 97 | export type SourceAdapter = { | 52 | export type SourceAdapter = { |
| 98 | key: string; | 53 | key: string; |
| 99 | displayName: string; | 54 | displayName: string; |
| @@ -101,8 +56,6 @@ export type SourceAdapter = { · 1 unmodified line | |||
| 101 | bindingConfigVersion: number; | 56 | bindingConfigVersion: number; |
| 102 | providerKey: string; | 57 | providerKey: string; |
| 103 | defaultImportAuthPolicy: SourceImportAuthPolicy; | 58 | defaultImportAuthPolicy: SourceImportAuthPolicy; |
| 104 | primaryDocumentKind: string | null; | ||
| 105 | primarySchemaBundleKind: string | null; | ||
| 106 | connectPayloadSchema: SourceAdapterInputSchema | null; | 59 | connectPayloadSchema: SourceAdapterInputSchema | null; |
| 107 | executorAddInputSchema: SourceAdapterInputSchema | null; | 60 | executorAddInputSchema: SourceAdapterInputSchema | null; |
| 108 | executorAddHelpText: readonly string[] | null; | 61 | executorAddHelpText: readonly string[] | null; |
| @@ -115,30 +68,9 @@ export type SourceAdapter = { · 6 unmodified lines | |||
| 115 | sourceConfigFromSource: (source: Source) => Record<string, unknown>; | 68 | sourceConfigFromSource: (source: Source) => Record<string, unknown>; |
| 116 | validateSource: (source: Source) => Effect.Effect<Source, Error, never>; | 69 | validateSource: (source: Source) => Effect.Effect<Source, Error, never>; |
| 117 | shouldAutoProbe: (source: Source) => boolean; | 70 | shouldAutoProbe: (source: Source) => boolean; |
| 118 | parseManifest: (input: { | 71 | syncCatalog: ( |
| 119 | source: Pick<Source, "id" | "kind">; | ||
| 120 | manifestJson: string | null; | ||
| 121 | }) => Effect.Effect<unknown | null, Error, never>; | ||
| 122 | describePersistedOperation: (input: { | ||
| 123 | source: Source; | ||
| 124 | path: string; | ||
| 125 | operation: StoredSourceRecipeOperationRecord; | ||
| 126 | }) => Effect.Effect<SourceAdapterPersistedOperationMetadata, Error, never>; | ||
| 127 | searchNamespace?: (input: { | ||
| 128 | source: Source; | ||
| 129 | path: string; | ||
| 130 | operation: StoredSourceRecipeOperationRecord; | ||
| 131 | }) => string; | ||
| 132 | createToolDescriptor: (input: { | ||
| 133 | source: Source; | ||
| 134 | operation: StoredSourceRecipeOperationRecord; | ||
| 135 | path: string; | ||
| 136 | schemaBundleId?: string | null; | ||
| 137 | includeSchemas: boolean; | ||
| 138 | }) => ToolDescriptor; | ||
| 139 | materializeSource: ( | ||
| 140 | input: SourceAdapterSyncInput, | 72 | input: SourceAdapterSyncInput, |
| 141 | ) => Effect.Effect<SourceAdapterMaterialization, Error, never>; | 73 | ) => Effect.Effect<SourceCatalogSyncResult, Error, never>; |
| 142 | getOauth2SetupConfig?: (input: { | 74 | getOauth2SetupConfig?: (input: { |
| 143 | source: Source; | 75 | source: Source; |
| 144 | slot: CredentialSlot; | 76 | slot: CredentialSlot; |
| @@ -146,7 +78,4 @@ export type SourceAdapter = { · 1 unmodified line | |||
| 146 | normalizeOauthClientInput?: ( | 78 | normalizeOauthClientInput?: ( |
| 147 | input: SourceOauthClientInput, | 79 | input: SourceOauthClientInput, |
| 148 | ) => Effect.Effect<SourceOauthClientInput, Error, never>; | 80 | ) => Effect.Effect<SourceOauthClientInput, Error, never>; |
| 149 | invokePersistedTool: ( | ||
| 150 | input: SourceAdapterInvokePersistedToolInput, | ||
| 151 | ) => Effect.Effect<unknown, Error, never>; | ||
| 152 | }; | 81 | }; |
| @@ -1,4 +1,4 @@ | |||
| 1 | import { describe, expect, it } from "vitest"; | 1 | import { describe, expect, it } from "@effect/vitest"; |
| 2 | import * as Effect from "effect/Effect"; | 2 | import * as Effect from "effect/Effect"; |
| 3 | 3 | ||
| 4 | import { | 4 | import { |
| @@ -68,9 +68,9 @@ import { · 67 unmodified lines | |||
| 68 | } from "./secret-material-providers"; | 68 | } from "./secret-material-providers"; |
| 69 | import { upsertOauth2AuthorizedUserLeaseFromTokenResponse } from "./auth-leases"; | 69 | import { upsertOauth2AuthorizedUserLeaseFromTokenResponse } from "./auth-leases"; |
| 70 | import { | 70 | import { |
| 71 | RuntimeSourceMaterializationService, | 71 | RuntimeSourceCatalogSyncService, |
| 72 | type RuntimeSourceMaterializationShape, | 72 | type RuntimeSourceCatalogSyncShape, |
| 73 | } from "./source-materialization"; | 73 | } from "./source-catalog-sync"; |
| 74 | import { | 74 | import { |
| 75 | buildOAuth2AuthorizationUrl, | 75 | buildOAuth2AuthorizationUrl, |
| 76 | createPkceCodeVerifier, | 76 | createPkceCodeVerifier, |
| @@ -978,7 +978,7 @@ const startOauth2PkceSourceCredentialSetup = (input: { · 901 unmodified lines | |||
| 978 | const connectMcpSourceInternal = (input: { | 978 | const connectMcpSourceInternal = (input: { |
| 979 | rows: ControlPlaneStoreShape; | 979 | rows: ControlPlaneStoreShape; |
| 980 | sourceStore: RuntimeSourceStore; | 980 | sourceStore: RuntimeSourceStore; |
| 981 | sourceMaterialization: RuntimeSourceMaterializationShape; | 981 | sourceCatalogSync: RuntimeSourceCatalogSyncShape; |
| 982 | getLocalServerBaseUrl?: () => string | undefined; | 982 | getLocalServerBaseUrl?: () => string | undefined; |
| 983 | baseUrl?: string | null; | 983 | baseUrl?: string | null; |
| 984 | workspaceId: WorkspaceId; | 984 | workspaceId: WorkspaceId; |
| @@ -1087,7 +1087,7 @@ const connectMcpSourceInternal = (input: { · 102 unmodified lines | |||
| 1087 | const persistedDraft = yield* input.sourceStore.persistSource(draftSource, { | 1087 | const persistedDraft = yield* input.sourceStore.persistSource(draftSource, { |
| 1088 | actorAccountId: input.actorAccountId, | 1088 | actorAccountId: input.actorAccountId, |
| 1089 | }); | 1089 | }); |
| 1090 | yield* input.sourceMaterialization.sync({ | 1090 | yield* input.sourceCatalogSync.sync({ |
| 1091 | source: persistedDraft, | 1091 | source: persistedDraft, |
| 1092 | actorAccountId: input.actorAccountId, | 1092 | actorAccountId: input.actorAccountId, |
| 1093 | }); | 1093 | }); |
| @@ -1110,7 +1110,7 @@ const connectMcpSourceInternal = (input: { · 16 unmodified lines | |||
| 1110 | auth: { kind: "none" }, | 1110 | auth: { kind: "none" }, |
| 1111 | }); | 1111 | }); |
| 1112 | const indexed = yield* Effect.either( | 1112 | const indexed = yield* Effect.either( |
| 1113 | input.sourceMaterialization.persistMcpRecipeMaterializationFromManifest({ | 1113 | input.sourceCatalogSync.persistMcpCatalogSnapshotFromManifest({ |
| 1114 | source: connected, | 1114 | source: connected, |
| 1115 | manifestEntries: result.manifest.tools, | 1115 | manifestEntries: result.manifest.tools, |
| 1116 | }), | 1116 | }), |
| @@ -1206,7 +1206,7 @@ const connectMcpSourceInternal = (input: { · 89 unmodified lines | |||
| 1206 | const addExecutorHttpSource = (input: { | 1206 | const addExecutorHttpSource = (input: { |
| 1207 | rows: ControlPlaneStoreShape; | 1207 | rows: ControlPlaneStoreShape; |
| 1208 | sourceStore: RuntimeSourceStore; | 1208 | sourceStore: RuntimeSourceStore; |
| 1209 | sourceMaterialization: RuntimeSourceMaterializationShape; | 1209 | sourceCatalogSync: RuntimeSourceCatalogSyncShape; |
| 1210 | sourceInput: Extract<ExecutorAddSourceInput, { kind: "openapi" | "graphql" }>; | 1210 | sourceInput: Extract<ExecutorAddSourceInput, { kind: "openapi" | "graphql" }>; |
| 1211 | storeSecretMaterial: StoreSecretMaterial; | 1211 | storeSecretMaterial: StoreSecretMaterial; |
| 1212 | resolveSecretMaterial: ResolveSecretMaterial; | 1212 | resolveSecretMaterial: ResolveSecretMaterial; |
| @@ -1358,7 +1358,7 @@ const addExecutorHttpSource = (input: { · 145 unmodified lines | |||
| 1358 | } | 1358 | } |
| 1359 | 1359 | ||
| 1360 | const synced = yield* Effect.either( | 1360 | const synced = yield* Effect.either( |
| 1361 | input.sourceMaterialization.sync({ | 1361 | input.sourceCatalogSync.sync({ |
| 1362 | source: { | 1362 | source: { |
| 1363 | ...persistedDraft, | 1363 | ...persistedDraft, |
| 1364 | status: "connected", | 1364 | status: "connected", |
| @@ -1409,7 +1409,7 @@ const addExecutorHttpSource = (input: { · 44 unmodified lines | |||
| 1409 | const addExecutorGoogleDiscoverySource = (input: { | 1409 | const addExecutorGoogleDiscoverySource = (input: { |
| 1410 | rows: ControlPlaneStoreShape; | 1410 | rows: ControlPlaneStoreShape; |
| 1411 | sourceStore: RuntimeSourceStore; | 1411 | sourceStore: RuntimeSourceStore; |
| 1412 | sourceMaterialization: RuntimeSourceMaterializationShape; | 1412 | sourceCatalogSync: RuntimeSourceCatalogSyncShape; |
| 1413 | sourceInput: Extract<ExecutorAddSourceInput, { kind: "google_discovery" }>; | 1413 | sourceInput: Extract<ExecutorAddSourceInput, { kind: "google_discovery" }>; |
| 1414 | storeSecretMaterial: StoreSecretMaterial; | 1414 | storeSecretMaterial: StoreSecretMaterial; |
| 1415 | resolveSecretMaterial: ResolveSecretMaterial; | 1415 | resolveSecretMaterial: ResolveSecretMaterial; |
| @@ -1578,7 +1578,7 @@ const addExecutorGoogleDiscoverySource = (input: { · 162 unmodified lines | |||
| 1578 | } | 1578 | } |
| 1579 | 1579 | ||
| 1580 | const synced = yield* Effect.either( | 1580 | const synced = yield* Effect.either( |
| 1581 | input.sourceMaterialization.sync({ | 1581 | input.sourceCatalogSync.sync({ |
| 1582 | source: { | 1582 | source: { |
| 1583 | ...persistedDraft, | 1583 | ...persistedDraft, |
| 1584 | status: "connected", | 1584 | status: "connected", |
| @@ -1671,7 +1671,7 @@ type RuntimeSourceAuthDependencies = { · 86 unmodified lines | |||
| 1671 | rows: ControlPlaneStoreShape; | 1671 | rows: ControlPlaneStoreShape; |
| 1672 | liveExecutionManager: LiveExecutionManager; | 1672 | liveExecutionManager: LiveExecutionManager; |
| 1673 | sourceStore: RuntimeSourceStore; | 1673 | sourceStore: RuntimeSourceStore; |
| 1674 | sourceMaterialization: RuntimeSourceMaterializationShape; | 1674 | sourceCatalogSync: RuntimeSourceCatalogSyncShape; |
| 1675 | resolveSecretMaterial: ResolveSecretMaterial; | 1675 | resolveSecretMaterial: ResolveSecretMaterial; |
| 1676 | storeSecretMaterial: StoreSecretMaterial; | 1676 | storeSecretMaterial: StoreSecretMaterial; |
| 1677 | getLocalServerBaseUrl?: () => string | undefined; | 1677 | getLocalServerBaseUrl?: () => string | undefined; |
| @@ -1721,7 +1721,7 @@ const createRuntimeSourceConnectionService = ( · 43 unmodified lines | |||
| 1721 | ? addExecutorGoogleDiscoverySource({ | 1721 | ? addExecutorGoogleDiscoverySource({ |
| 1722 | rows: input.rows, | 1722 | rows: input.rows, |
| 1723 | sourceStore: input.sourceStore, | 1723 | sourceStore: input.sourceStore, |
| 1724 | sourceMaterialization: input.sourceMaterialization, | 1724 | sourceCatalogSync: input.sourceCatalogSync, |
| 1725 | sourceInput, | 1725 | sourceInput, |
| 1726 | storeSecretMaterial: input.storeSecretMaterial, | 1726 | storeSecretMaterial: input.storeSecretMaterial, |
| 1727 | resolveSecretMaterial: input.resolveSecretMaterial, | 1727 | resolveSecretMaterial: input.resolveSecretMaterial, |
| @@ -1732,7 +1732,7 @@ const createRuntimeSourceConnectionService = ( · 4 unmodified lines | |||
| 1732 | ? addExecutorHttpSource({ | 1732 | ? addExecutorHttpSource({ |
| 1733 | rows: input.rows, | 1733 | rows: input.rows, |
| 1734 | sourceStore: input.sourceStore, | 1734 | sourceStore: input.sourceStore, |
| 1735 | sourceMaterialization: input.sourceMaterialization, | 1735 | sourceCatalogSync: input.sourceCatalogSync, |
| 1736 | sourceInput: sourceInput as Extract< | 1736 | sourceInput: sourceInput as Extract< |
| 1737 | ExecutorAddSourceInput, | 1737 | ExecutorAddSourceInput, |
| 1738 | { kind: "openapi" | "graphql" } | 1738 | { kind: "openapi" | "graphql" } |
| @@ -1745,7 +1745,7 @@ const createRuntimeSourceConnectionService = ( · 6 unmodified lines | |||
| 1745 | : connectMcpSourceInternal({ | 1745 | : connectMcpSourceInternal({ |
| 1746 | rows: input.rows, | 1746 | rows: input.rows, |
| 1747 | sourceStore: input.sourceStore, | 1747 | sourceStore: input.sourceStore, |
| 1748 | sourceMaterialization: input.sourceMaterialization, | 1748 | sourceCatalogSync: input.sourceCatalogSync, |
| 1749 | getLocalServerBaseUrl: input.getLocalServerBaseUrl, | 1749 | getLocalServerBaseUrl: input.getLocalServerBaseUrl, |
| 1750 | workspaceId: sourceInput.workspaceId, | 1750 | workspaceId: sourceInput.workspaceId, |
| 1751 | actorAccountId: sourceInput.actorAccountId, | 1751 | actorAccountId: sourceInput.actorAccountId, |
| @@ -1767,7 +1767,7 @@ const createRuntimeSourceConnectionService = ( · 15 unmodified lines | |||
| 1767 | connectMcpSourceInternal({ | 1767 | connectMcpSourceInternal({ |
| 1768 | rows: input.rows, | 1768 | rows: input.rows, |
| 1769 | sourceStore: input.sourceStore, | 1769 | sourceStore: input.sourceStore, |
| 1770 | sourceMaterialization: input.sourceMaterialization, | 1770 | sourceCatalogSync: input.sourceCatalogSync, |
| 1771 | getLocalServerBaseUrl: input.getLocalServerBaseUrl, | 1771 | getLocalServerBaseUrl: input.getLocalServerBaseUrl, |
| 1772 | workspaceId: sourceInput.workspaceId, | 1772 | workspaceId: sourceInput.workspaceId, |
| 1773 | actorAccountId: sourceInput.actorAccountId, | 1773 | actorAccountId: sourceInput.actorAccountId, |
| @@ -2028,7 +2028,7 @@ const createRuntimeSourceOAuthSessionService = ( · 254 unmodified lines | |||
| 2028 | status: "error", | 2028 | status: "error", |
| 2029 | lastError: reason, | 2029 | lastError: reason, |
| 2030 | }); | 2030 | }); |
| 2031 | yield* input.sourceMaterialization.sync({ | 2031 | yield* input.sourceCatalogSync.sync({ |
| 2032 | source: failedSource, | 2032 | source: failedSource, |
| 2033 | actorAccountId: session.actorAccountId, | 2033 | actorAccountId: session.actorAccountId, |
| 2034 | }); | 2034 | }); |
| @@ -2201,7 +2201,7 @@ const createRuntimeSourceOAuthSessionService = ( · 166 unmodified lines | |||
| 2201 | ); | 2201 | ); |
| 2202 | } | 2202 | } |
| 2203 | const indexed = yield* Effect.either( | 2203 | const indexed = yield* Effect.either( |
| 2204 | input.sourceMaterialization.sync({ | 2204 | input.sourceCatalogSync.sync({ |
| 2205 | source: connectedSource, | 2205 | source: connectedSource, |
| 2206 | actorAccountId: session.actorAccountId, | 2206 | actorAccountId: session.actorAccountId, |
| 2207 | }), | 2207 | }), |
| @@ -2272,7 +2272,7 @@ export const RuntimeSourceAuthServiceLive = (input: { · 64 unmodified lines | |||
| 2272 | const rows = yield* ControlPlaneStore; | 2272 | const rows = yield* ControlPlaneStore; |
| 2273 | const liveExecutionManager = yield* LiveExecutionManagerService; | 2273 | const liveExecutionManager = yield* LiveExecutionManagerService; |
| 2274 | const sourceStore = yield* RuntimeSourceStoreService; | 2274 | const sourceStore = yield* RuntimeSourceStoreService; |
| 2275 | const sourceMaterialization = yield* RuntimeSourceMaterializationService; | 2275 | const sourceCatalogSync = yield* RuntimeSourceCatalogSyncService; |
| 2276 | const resolveSecretMaterial = yield* SecretMaterialResolverService; | 2276 | const resolveSecretMaterial = yield* SecretMaterialResolverService; |
| 2277 | const storeSecretMaterial = yield* SecretMaterialStorerService; | 2277 | const storeSecretMaterial = yield* SecretMaterialStorerService; |
| 2278 | const runtimeLocalWorkspace = yield* getRuntimeLocalWorkspaceOption(); | 2278 | const runtimeLocalWorkspace = yield* getRuntimeLocalWorkspaceOption(); |
| @@ -2281,7 +2281,7 @@ export const RuntimeSourceAuthServiceLive = (input: { · 2 unmodified lines | |||
| 2281 | rows, | 2281 | rows, |
| 2282 | liveExecutionManager, | 2282 | liveExecutionManager, |
| 2283 | sourceStore, | 2283 | sourceStore, |
| 2284 | sourceMaterialization, | 2284 | sourceCatalogSync, |
| 2285 | resolveSecretMaterial, | 2285 | resolveSecretMaterial, |
| 2286 | storeSecretMaterial, | 2286 | storeSecretMaterial, |
| 2287 | getLocalServerBaseUrl: input.getLocalServerBaseUrl, | 2287 | getLocalServerBaseUrl: input.getLocalServerBaseUrl, |
| @@ -0,0 +1,743 @@ | |||
| 1 | import { | ||
| 2 | type ToolCatalogEntry, | ||
| 3 | type ToolDescriptor as CatalogToolDescriptor, | ||
| 4 | typeSignatureFromSchema, | ||
| 5 | } from "@executor/codemode-core"; | ||
| 6 | import type { | ||
| 7 | AccountId, | ||
| 8 | Source, | ||
| 9 | StoredSourceRecord, | ||
| 10 | StoredSourceCatalogRevisionRecord, | ||
| 11 | WorkspaceId, | ||
| 12 | } from "#schema"; | ||
| 13 | import * as Context from "effect/Context"; | ||
| 14 | import * as Effect from "effect/Effect"; | ||
| 15 | import * as Layer from "effect/Layer"; | ||
| 16 | |||
| 17 | import { | ||
| 18 | projectCatalogForAgentSdk, | ||
| 19 | type ProjectedCatalog, | ||
| 20 | } from "../ir/catalog"; | ||
| 21 | import type { | ||
| 22 | Capability, | ||
| 23 | CatalogSnapshotV1, | ||
| 24 | CatalogV1, | ||
| 25 | Executable, | ||
| 26 | GraphQLExecutable, | ||
| 27 | HttpExecutable, | ||
| 28 | McpExecutable, | ||
| 29 | ShapeSymbol, | ||
| 30 | Symbol as IrSymbol, | ||
| 31 | } from "../ir/model"; | ||
| 32 | import { LocalSourceArtifactMissingError } from "./local-errors"; | ||
| 33 | import { | ||
| 34 | RuntimeLocalWorkspaceService, | ||
| 35 | type RuntimeLocalWorkspaceState, | ||
| 36 | } from "./local-runtime-context"; | ||
| 37 | import type { LocalSourceArtifact } from "./local-source-artifacts"; | ||
| 38 | import { | ||
| 39 | SourceArtifactStore, | ||
| 40 | type SourceArtifactStoreShape, | ||
| 41 | } from "./local-storage"; | ||
| 42 | import { namespaceFromSourceName } from "./source-names"; | ||
| 43 | import { | ||
| 44 | RuntimeSourceStoreService, | ||
| 45 | type RuntimeSourceStore, | ||
| 46 | } from "./source-store"; | ||
| 47 | |||
| 48 | type CatalogImportMetadata = CatalogSnapshotV1["import"]; | ||
| 49 | |||
| 50 | export type LoadedSourceCatalog = { | ||
| 51 | source: Source; | ||
| 52 | sourceRecord: StoredSourceRecord; | ||
| 53 | revision: StoredSourceCatalogRevisionRecord; | ||
| 54 | snapshot: CatalogSnapshotV1; | ||
| 55 | catalog: CatalogV1; | ||
| 56 | projected: ProjectedCatalog; | ||
| 57 | importMetadata: CatalogImportMetadata; | ||
| 58 | }; | ||
| 59 | |||
| 60 | export type LoadedSourceCatalogTool = { | ||
| 61 | path: string; | ||
| 62 | searchNamespace: string; | ||
| 63 | searchText: string; | ||
| 64 | source: Source; | ||
| 65 | sourceRecord: StoredSourceRecord; | ||
| 66 | revision: StoredSourceCatalogRevisionRecord; | ||
| 67 | capabilityId: keyof CatalogV1["capabilities"]; | ||
| 68 | executableId: keyof CatalogV1["executables"]; | ||
| 69 | capability: Capability; | ||
| 70 | executable: Executable; | ||
| 71 | descriptor: CatalogToolDescriptor; | ||
| 72 | projectedCatalog: CatalogV1; | ||
| 73 | }; | ||
| 74 | |||
| 75 | export type LoadedSourceCatalogToolIndexEntry = Omit<LoadedSourceCatalogTool, "revision">; | ||
| 76 | |||
| 77 | export const catalogToolCatalogEntry = (input: { | ||
| 78 | tool: LoadedSourceCatalogToolIndexEntry; | ||
| 79 | score: (queryTokens: readonly string[]) => number; | ||
| 80 | }): ToolCatalogEntry => ({ | ||
| 81 | descriptor: input.tool.descriptor, | ||
| 82 | namespace: input.tool.searchNamespace, | ||
| 83 | searchText: input.tool.searchText, | ||
| 84 | score: input.score, | ||
| 85 | }); | ||
| 86 | |||
| 87 | const catalogNamespaceFromPath = (path: string): string => { | ||
| 88 | const [first, second] = path.split("."); | ||
| 89 | return second ? `${first}.${second}` : first; | ||
| 90 | }; | ||
| 91 | |||
| 92 | const chooseExecutable = (catalog: CatalogV1, capability: Capability): Executable => { | ||
| 93 | const preferred = | ||
| 94 | capability.preferredExecutableId !== undefined | ||
| 95 | ? catalog.executables[capability.preferredExecutableId] | ||
| 96 | : undefined; | ||
| 97 | if (preferred) { | ||
| 98 | return preferred; | ||
| 99 | } | ||
| 100 | |||
| 101 | const first = capability.executableIds | ||
| 102 | .map((id) => catalog.executables[id]) | ||
| 103 | .find((entry): entry is Executable => entry !== undefined); | ||
| 104 | if (!first) { | ||
| 105 | throw new Error(`Capability ${capability.id} has no executable`); | ||
| 106 | } | ||
| 107 | return first; | ||
| 108 | }; | ||
| 109 | |||
| 110 | const asShape = (catalog: CatalogV1, shapeId: string | undefined): ShapeSymbol | undefined => { | ||
| 111 | if (!shapeId) { | ||
| 112 | return undefined; | ||
| 113 | } | ||
| 114 | |||
| 115 | const symbol = catalog.symbols[shapeId]; | ||
| 116 | return symbol?.kind === "shape" ? symbol : undefined; | ||
| 117 | }; | ||
| 118 | |||
| 119 | const symbolDocsSummary = (symbol: IrSymbol | undefined): string | undefined => { | ||
| 120 | if (!symbol || !("docs" in symbol)) { | ||
| 121 | return undefined; | ||
| 122 | } | ||
| 123 | |||
| 124 | return symbol.docs?.summary ?? symbol.docs?.description; | ||
| 125 | }; | ||
| 126 | |||
| 127 | const shapeToJsonSchema = (catalog: CatalogV1, rootShapeId: string): unknown => { | ||
| 128 | const defs: Record<string, unknown> = {}; | ||
| 129 | const visiting = new Set<string>(); | ||
| 130 | const built = new Set<string>(); | ||
| 131 | |||
| 132 | const defNameFor = (shapeId: string) => shapeId.replace(/[^A-Za-z0-9_]/g, "_"); | ||
| 133 | |||
| 134 | const visit = (shapeId: string): { $ref: string } => { | ||
| 135 | const defName = defNameFor(shapeId); | ||
| 136 | if (built.has(shapeId)) { | ||
| 137 | return { $ref: `#/$defs/${defName}` }; | ||
| 138 | } | ||
| 139 | if (visiting.has(shapeId)) { | ||
| 140 | return { $ref: `#/$defs/${defName}` }; | ||
| 141 | } | ||
| 142 | |||
| 143 | const shape = asShape(catalog, shapeId); | ||
| 144 | visiting.add(shapeId); | ||
| 145 | |||
| 146 | const schema = (() => { | ||
| 147 | if (!shape) { | ||
| 148 | return {} as Record<string, unknown>; | ||
| 149 | } | ||
| 150 | |||
| 151 | const withDocs = (schemaValue: Record<string, unknown>): Record<string, unknown> => ({ | ||
| 152 | ...(shape.title ? { title: shape.title } : {}), | ||
| 153 | ...(shape.docs?.description ? { description: shape.docs.description } : {}), | ||
| 154 | ...schemaValue, | ||
| 155 | }); | ||
| 156 | |||
| 157 | switch (shape.node.type) { | ||
| 158 | case "unknown": | ||
| 159 | return withDocs({}); | ||
| 160 | case "const": | ||
| 161 | return withDocs({ const: shape.node.value }); | ||
| 162 | case "enum": | ||
| 163 | return withDocs({ enum: shape.node.values }); | ||
| 164 | case "scalar": | ||
| 165 | return withDocs({ | ||
| 166 | type: | ||
| 167 | shape.node.scalar === "bytes" | ||
| 168 | ? "string" | ||
| 169 | : shape.node.scalar, | ||
| 170 | ...(shape.node.scalar === "bytes" ? { format: "binary" } : {}), | ||
| 171 | ...(shape.node.format ? { format: shape.node.format } : {}), | ||
| 172 | ...(shape.node.constraints ?? {}), | ||
| 173 | }); | ||
| 174 | case "ref": | ||
| 175 | return visit(shape.node.target); | ||
| 176 | case "nullable": | ||
| 177 | return withDocs({ | ||
| 178 | anyOf: [ | ||
| 179 | visit(shape.node.itemShapeId), | ||
| 180 | { type: "null" }, | ||
| 181 | ], | ||
| 182 | }); | ||
| 183 | case "allOf": | ||
| 184 | return withDocs({ | ||
| 185 | allOf: shape.node.items.map((entry) => visit(entry)), | ||
| 186 | }); | ||
| 187 | case "anyOf": | ||
| 188 | return withDocs({ | ||
| 189 | anyOf: shape.node.items.map((entry) => visit(entry)), | ||
| 190 | }); | ||
| 191 | case "oneOf": | ||
| 192 | return withDocs({ | ||
| 193 | oneOf: shape.node.items.map((entry) => visit(entry)), | ||
| 194 | ...(shape.node.discriminator | ||
| 195 | ? { | ||
| 196 | discriminator: { | ||
| 197 | propertyName: shape.node.discriminator.propertyName, | ||
| 198 | ...(shape.node.discriminator.mapping | ||
| 199 | ? { | ||
| 200 | mapping: Object.fromEntries( | ||
| 201 | Object.entries(shape.node.discriminator.mapping).map(([key, value]) => [ | ||
| 202 | key, | ||
| 203 | `#/$defs/${defNameFor(value)}`, | ||
| 204 | ]), | ||
| 205 | ), | ||
| 206 | } | ||
| 207 | : {}), | ||
| 208 | }, | ||
| 209 | } | ||
| 210 | : {}), | ||
| 211 | }); | ||
| 212 | case "not": | ||
| 213 | return withDocs({ | ||
| 214 | not: visit(shape.node.itemShapeId), | ||
| 215 | }); | ||
| 216 | case "conditional": | ||
| 217 | return withDocs({ | ||
| 218 | if: visit(shape.node.ifShapeId), | ||
| 219 | ...(shape.node.thenShapeId ? { then: visit(shape.node.thenShapeId) } : {}), | ||
| 220 | ...(shape.node.elseShapeId ? { else: visit(shape.node.elseShapeId) } : {}), | ||
| 221 | }); | ||
| 222 | case "array": | ||
| 223 | return withDocs({ | ||
| 224 | type: "array", | ||
| 225 | items: visit(shape.node.itemShapeId), | ||
| 226 | ...(shape.node.minItems !== undefined ? { minItems: shape.node.minItems } : {}), | ||
| 227 | ...(shape.node.maxItems !== undefined ? { maxItems: shape.node.maxItems } : {}), | ||
| 228 | }); | ||
| 229 | case "tuple": | ||
| 230 | return withDocs({ | ||
| 231 | type: "array", | ||
| 232 | prefixItems: shape.node.itemShapeIds.map((entry) => visit(entry)), | ||
| 233 | ...(shape.node.additionalItems !== undefined | ||
| 234 | ? { | ||
| 235 | items: | ||
| 236 | typeof shape.node.additionalItems === "boolean" | ||
| 237 | ? shape.node.additionalItems | ||
| 238 | : visit(shape.node.additionalItems), | ||
| 239 | } | ||
| 240 | : {}), | ||
| 241 | }); | ||
| 242 | case "map": | ||
| 243 | return withDocs({ | ||
| 244 | type: "object", | ||
| 245 | additionalProperties: visit(shape.node.valueShapeId), | ||
| 246 | }); | ||
| 247 | case "object": | ||
| 248 | return withDocs({ | ||
| 249 | type: "object", | ||
| 250 | properties: Object.fromEntries( | ||
| 251 | Object.entries(shape.node.fields).map(([key, field]) => [ | ||
| 252 | key, | ||
| 253 | { | ||
| 254 | ...(visit(field.shapeId)), | ||
| 255 | ...(field.docs?.description ? { description: field.docs.description } : {}), | ||
| 256 | }, | ||
| 257 | ]), | ||
| 258 | ), | ||
| 259 | ...(shape.node.required && shape.node.required.length > 0 | ||
| 260 | ? { required: shape.node.required } | ||
| 261 | : {}), | ||
| 262 | ...(shape.node.additionalProperties !== undefined | ||
| 263 | ? { | ||
| 264 | additionalProperties: | ||
| 265 | typeof shape.node.additionalProperties === "boolean" | ||
| 266 | ? shape.node.additionalProperties | ||
| 267 | : visit(shape.node.additionalProperties), | ||
| 268 | } | ||
| 269 | : {}), | ||
| 270 | ...(shape.node.patternProperties | ||
| 271 | ? { | ||
| 272 | patternProperties: Object.fromEntries( | ||
| 273 | Object.entries(shape.node.patternProperties).map(([key, value]) => [ | ||
| 274 | key, | ||
| 275 | visit(value), | ||
| 276 | ]), | ||
| 277 | ), | ||
| 278 | } | ||
| 279 | : {}), | ||
| 280 | }); | ||
| 281 | case "graphqlInterface": | ||
| 282 | return withDocs({ | ||
| 283 | type: "object", | ||
| 284 | properties: Object.fromEntries( | ||
| 285 | Object.entries(shape.node.fields).map(([key, field]) => [ | ||
| 286 | key, | ||
| 287 | visit(field.shapeId), | ||
| 288 | ]), | ||
| 289 | ), | ||
| 290 | }); | ||
| 291 | case "graphqlUnion": | ||
| 292 | return withDocs({ | ||
| 293 | oneOf: shape.node.memberTypeIds.map((entry) => visit(entry)), | ||
| 294 | }); | ||
| 295 | } | ||
| 296 | })(); | ||
| 297 | |||
| 298 | visiting.delete(shapeId); | ||
| 299 | built.add(shapeId); | ||
| 300 | defs[defName] = schema; | ||
| 301 | return { $ref: `#/$defs/${defName}` }; | ||
| 302 | }; | ||
| 303 | |||
| 304 | const rootRef = visit(rootShapeId); | ||
| 305 | return { | ||
| 306 | ...rootRef, | ||
| 307 | $defs: defs, | ||
| 308 | }; | ||
| 309 | }; | ||
| 310 | |||
| 311 | const codemodeDescriptorFromCapability = (input: { | ||
| 312 | source: Source; | ||
| 313 | projected: ProjectedCatalog; | ||
| 314 | capability: Capability; | ||
| 315 | executable: Executable; | ||
| 316 | includeSchemas: boolean; | ||
| 317 | }): CatalogToolDescriptor => { | ||
| 318 | const descriptor = input.projected.toolDescriptors[input.capability.id]; | ||
| 319 | const path = descriptor.toolPath.join("."); | ||
| 320 | const interaction = | ||
| 321 | descriptor.interaction.mayRequireApproval || descriptor.interaction.mayElicit | ||
| 322 | ? "required" | ||
| 323 | : "auto"; | ||
| 324 | const inputSchema = input.includeSchemas | ||
| 325 | ? shapeToJsonSchema(input.projected.catalog, descriptor.callShapeId) | ||
| 326 | : undefined; | ||
| 327 | const outputSchema = | ||
| 328 | input.includeSchemas && descriptor.resultShapeId | ||
| 329 | ? shapeToJsonSchema(input.projected.catalog, descriptor.resultShapeId) | ||
| 330 | : undefined; | ||
| 331 | |||
| 332 | return { | ||
| 333 | path: path as CatalogToolDescriptor["path"], | ||
| 334 | sourceKey: input.source.id, | ||
| 335 | description: input.capability.surface.summary ?? input.capability.surface.description, | ||
| 336 | interaction, | ||
| 337 | ...(inputSchema !== undefined ? { inputSchema } : {}), | ||
| 338 | ...(outputSchema !== undefined ? { outputSchema } : {}), | ||
| 339 | ...(inputSchema !== undefined | ||
| 340 | ? { | ||
| 341 | inputType: typeSignatureFromSchema(inputSchema, "unknown"), | ||
| 342 | } | ||
| 343 | : {}), | ||
| 344 | ...(outputSchema !== undefined | ||
| 345 | ? { | ||
| 346 | outputType: typeSignatureFromSchema(outputSchema, "unknown"), | ||
| 347 | } | ||
| 348 | : {}), | ||
| 349 | providerKind: input.executable.protocol, | ||
| 350 | providerData: { | ||
| 351 | capabilityId: input.capability.id, | ||
| 352 | executableId: input.executable.id, | ||
| 353 | protocol: input.executable.protocol, | ||
| 354 | }, | ||
| 355 | }; | ||
| 356 | }; | ||
| 357 | |||
| 358 | const sourceRecordFromCatalogArtifact = (input: { | ||
| 359 | source: Source; | ||
| 360 | artifact: { | ||
| 361 | catalogId: StoredSourceRecord["catalogId"]; | ||
| 362 | revision: StoredSourceCatalogRevisionRecord; | ||
| 363 | }; | ||
| 364 | }): StoredSourceRecord => ({ | ||
| 365 | id: input.source.id, | ||
| 366 | workspaceId: input.source.workspaceId, | ||
| 367 | catalogId: input.artifact.catalogId, | ||
| 368 | catalogRevisionId: input.artifact.revision.id, | ||
| 369 | name: input.source.name, | ||
| 370 | kind: input.source.kind, | ||
| 371 | endpoint: input.source.endpoint, | ||
| 372 | status: input.source.status, | ||
| 373 | enabled: input.source.enabled, | ||
| 374 | namespace: input.source.namespace, | ||
| 375 | importAuthPolicy: input.source.importAuthPolicy, | ||
| 376 | bindingConfigJson: JSON.stringify(input.source.binding), | ||
| 377 | sourceHash: input.source.sourceHash, | ||
| 378 | lastError: input.source.lastError, | ||
| 379 | createdAt: input.source.createdAt, | ||
| 380 | updatedAt: input.source.updatedAt, | ||
| 381 | }); | ||
| 382 | |||
| 383 | type RuntimeSourceCatalogStoreShape = { | ||
| 384 | loadWorkspaceSourceCatalogs: (input: { | ||
| 385 | workspaceId: WorkspaceId; | ||
| 386 | actorAccountId?: AccountId | null; | ||
| 387 | }) => Effect.Effect<readonly LoadedSourceCatalog[], Error, never>; | ||
| 388 | loadSourceWithCatalog: (input: { | ||
| 389 | workspaceId: WorkspaceId; | ||
| 390 | sourceId: Source["id"]; | ||
| 391 | actorAccountId?: AccountId | null; | ||
| 392 | }) => Effect.Effect<LoadedSourceCatalog, Error | LocalSourceArtifactMissingError, never>; | ||
| 393 | loadWorkspaceSourceCatalogToolIndex: (input: { | ||
| 394 | workspaceId: WorkspaceId; | ||
| 395 | actorAccountId?: AccountId | null; | ||
| 396 | includeSchemas: boolean; | ||
| 397 | }) => Effect.Effect<readonly LoadedSourceCatalogToolIndexEntry[], Error, never>; | ||
| 398 | loadWorkspaceSourceCatalogToolByPath: (input: { | ||
| 399 | workspaceId: WorkspaceId; | ||
| 400 | path: string; | ||
| 401 | actorAccountId?: AccountId | null; | ||
| 402 | includeSchemas: boolean; | ||
| 403 | }) => Effect.Effect<LoadedSourceCatalogToolIndexEntry | null, Error, never>; | ||
| 404 | }; | ||
| 405 | |||
| 406 | export type RuntimeSourceCatalogStore = RuntimeSourceCatalogStoreShape; | ||
| 407 | |||
| 408 | export class RuntimeSourceCatalogStoreService extends Context.Tag( | ||
| 409 | "#runtime/RuntimeSourceCatalogStoreService", | ||
| 410 | )<RuntimeSourceCatalogStoreService, RuntimeSourceCatalogStoreShape>() {} | ||
| 411 | |||
| 412 | type RuntimeSourceCatalogStoreDeps = { | ||
| 413 | runtimeLocalWorkspace: RuntimeLocalWorkspaceState; | ||
| 414 | sourceStore: RuntimeSourceStore; | ||
| 415 | sourceArtifactStore: SourceArtifactStoreShape; | ||
| 416 | }; | ||
| 417 | |||
| 418 | type SourceCatalogRuntimeServices = | ||
| 419 | | RuntimeLocalWorkspaceService | ||
| 420 | | RuntimeSourceStoreService | ||
| 421 | | SourceArtifactStore; | ||
| 422 | |||
| 423 | const ensureRuntimeCatalogWorkspace = ( | ||
| 424 | deps: RuntimeSourceCatalogStoreDeps, | ||
| 425 | workspaceId: WorkspaceId, | ||
| 426 | ) => { | ||
| 427 | if (deps.runtimeLocalWorkspace.installation.workspaceId !== workspaceId) { | ||
| 428 | return Effect.fail( | ||
| 429 | new Error( | ||
| 430 | `Runtime local workspace mismatch: expected ${workspaceId}, got ${deps.runtimeLocalWorkspace.installation.workspaceId}`, | ||
| 431 | ), | ||
| 432 | ); | ||
| 433 | } | ||
| 434 | |||
| 435 | return Effect.succeed(deps.runtimeLocalWorkspace.context); | ||
| 436 | }; | ||
| 437 | |||
| 438 | const buildSnapshotFromArtifact = (input: { | ||
| 439 | source: Source; | ||
| 440 | artifact: LocalSourceArtifact; | ||
| 441 | }): CatalogSnapshotV1 => { | ||
| 442 | return input.artifact.snapshot; | ||
| 443 | }; | ||
| 444 | |||
| 445 | const loadWorkspaceSourceCatalogsWithDeps = (deps: RuntimeSourceCatalogStoreDeps, input: { | ||
| 446 | workspaceId: WorkspaceId; | ||
| 447 | actorAccountId?: AccountId | null; | ||
| 448 | }): Effect.Effect<readonly LoadedSourceCatalog[], Error, never> => | ||
| 449 | Effect.gen(function* () { | ||
| 450 | const workspaceContext = yield* ensureRuntimeCatalogWorkspace( | ||
| 451 | deps, | ||
| 452 | input.workspaceId, | ||
| 453 | ); | ||
| 454 | const sources = yield* deps.sourceStore.loadSourcesInWorkspace( | ||
| 455 | input.workspaceId, | ||
| 456 | { | ||
| 457 | actorAccountId: input.actorAccountId, | ||
| 458 | }, | ||
| 459 | ); | ||
| 460 | |||
| 461 | const localCatalogs = yield* Effect.forEach(sources, (source) => | ||
| 462 | Effect.gen(function* () { | ||
| 463 | const artifact = yield* deps.sourceArtifactStore.read({ | ||
| 464 | context: workspaceContext, | ||
| 465 | sourceId: source.id, | ||
| 466 | }); | ||
| 467 | if (artifact === null) { | ||
| 468 | return null; | ||
| 469 | } | ||
| 470 | |||
| 471 | const snapshot = buildSnapshotFromArtifact({ | ||
| 472 | source, | ||
| 473 | artifact, | ||
| 474 | }); | ||
| 475 | const projected = projectCatalogForAgentSdk({ | ||
| 476 | catalog: snapshot.catalog, | ||
| 477 | }); | ||
| 478 | |||
| 479 | return { | ||
| 480 | source, | ||
| 481 | sourceRecord: sourceRecordFromCatalogArtifact({ | ||
| 482 | source, | ||
| 483 | artifact, | ||
| 484 | }), | ||
| 485 | revision: artifact.revision, | ||
| 486 | snapshot, | ||
| 487 | catalog: snapshot.catalog, | ||
| 488 | projected, | ||
| 489 | importMetadata: snapshot.import, | ||
| 490 | } satisfies LoadedSourceCatalog; | ||
| 491 | }), | ||
| 492 | ); | ||
| 493 | |||
| 494 | return localCatalogs.filter((catalogEntry): catalogEntry is LoadedSourceCatalog => catalogEntry !== null); | ||
| 495 | }); | ||
| 496 | |||
| 497 | const loadSourceWithCatalogWithDeps = (deps: RuntimeSourceCatalogStoreDeps, input: { | ||
| 498 | workspaceId: WorkspaceId; | ||
| 499 | sourceId: Source["id"]; | ||
| 500 | actorAccountId?: AccountId | null; | ||
| 501 | }): Effect.Effect<LoadedSourceCatalog, Error | LocalSourceArtifactMissingError, never> => | ||
| 502 | Effect.gen(function* () { | ||
| 503 | const workspaceContext = yield* ensureRuntimeCatalogWorkspace( | ||
| 504 | deps, | ||
| 505 | input.workspaceId, | ||
| 506 | ); | ||
| 507 | const source = yield* deps.sourceStore.loadSourceById({ | ||
| 508 | workspaceId: input.workspaceId, | ||
| 509 | sourceId: input.sourceId, | ||
| 510 | actorAccountId: input.actorAccountId, | ||
| 511 | }); | ||
| 512 | const artifact = yield* deps.sourceArtifactStore.read({ | ||
| 513 | context: workspaceContext, | ||
| 514 | sourceId: source.id, | ||
| 515 | }); | ||
| 516 | if (artifact === null) { | ||
| 517 | return yield* Effect.fail( | ||
| 518 | new LocalSourceArtifactMissingError({ | ||
| 519 | message: `Catalog artifact missing for source ${input.sourceId}`, | ||
| 520 | sourceId: input.sourceId, | ||
| 521 | }), | ||
| 522 | ); | ||
| 523 | } | ||
| 524 | |||
| 525 | const snapshot = buildSnapshotFromArtifact({ | ||
| 526 | source, | ||
| 527 | artifact, | ||
| 528 | }); | ||
| 529 | const projected = projectCatalogForAgentSdk({ | ||
| 530 | catalog: snapshot.catalog, | ||
| 531 | }); | ||
| 532 | |||
| 533 | return { | ||
| 534 | source, | ||
| 535 | sourceRecord: sourceRecordFromCatalogArtifact({ | ||
| 536 | source, | ||
| 537 | artifact, | ||
| 538 | }), | ||
| 539 | revision: artifact.revision, | ||
| 540 | snapshot, | ||
| 541 | catalog: snapshot.catalog, | ||
| 542 | projected, | ||
| 543 | importMetadata: snapshot.import, | ||
| 544 | } satisfies LoadedSourceCatalog; | ||
| 545 | }); | ||
| 546 | |||
| 547 | export const loadWorkspaceSourceCatalogs = (input: { | ||
| 548 | workspaceId: WorkspaceId; | ||
| 549 | actorAccountId?: AccountId | null; | ||
| 550 | }): Effect.Effect<readonly LoadedSourceCatalog[], Error, SourceCatalogRuntimeServices> => | ||
| 551 | Effect.gen(function* () { | ||
| 552 | const runtimeLocalWorkspace = yield* RuntimeLocalWorkspaceService; | ||
| 553 | const sourceStore = yield* RuntimeSourceStoreService; | ||
| 554 | const sourceArtifactStore = yield* SourceArtifactStore; | ||
| 555 | |||
| 556 | return yield* loadWorkspaceSourceCatalogsWithDeps( | ||
| 557 | { | ||
| 558 | runtimeLocalWorkspace, | ||
| 559 | sourceStore, | ||
| 560 | sourceArtifactStore, | ||
| 561 | }, | ||
| 562 | input, | ||
| 563 | ); | ||
| 564 | }); | ||
| 565 | |||
| 566 | export const loadSourceWithCatalog = (input: { | ||
| 567 | workspaceId: WorkspaceId; | ||
| 568 | sourceId: Source["id"]; | ||
| 569 | actorAccountId?: AccountId | null; | ||
| 570 | }): Effect.Effect< | ||
| 571 | LoadedSourceCatalog, | ||
| 572 | Error | LocalSourceArtifactMissingError, | ||
| 573 | SourceCatalogRuntimeServices | ||
| 574 | > => | ||
| 575 | Effect.gen(function* () { | ||
| 576 | const runtimeLocalWorkspace = yield* RuntimeLocalWorkspaceService; | ||
| 577 | const sourceStore = yield* RuntimeSourceStoreService; | ||
| 578 | const sourceArtifactStore = yield* SourceArtifactStore; | ||
| 579 | |||
| 580 | return yield* loadSourceWithCatalogWithDeps( | ||
| 581 | { | ||
| 582 | runtimeLocalWorkspace, | ||
| 583 | sourceStore, | ||
| 584 | sourceArtifactStore, | ||
| 585 | }, | ||
| 586 | input, | ||
| 587 | ); | ||
| 588 | }); | ||
| 589 | |||
| 590 | export const expandCatalogTools = (input: { | ||
| 591 | catalogs: readonly LoadedSourceCatalog[]; | ||
| 592 | includeSchemas: boolean; | ||
| 593 | }): Effect.Effect<readonly LoadedSourceCatalogTool[], Error, never> => | ||
| 594 | Effect.succeed( | ||
| 595 | input.catalogs.flatMap((catalogEntry) => | ||
| 596 | Object.values(catalogEntry.catalog.capabilities).map((capability) => { | ||
| 597 | const executable = chooseExecutable(catalogEntry.projected.catalog, capability); | ||
| 598 | const descriptor = codemodeDescriptorFromCapability({ | ||
| 599 | source: catalogEntry.source, | ||
| 600 | projected: catalogEntry.projected, | ||
| 601 | capability, | ||
| 602 | executable, | ||
| 603 | includeSchemas: input.includeSchemas, | ||
| 604 | }); | ||
| 605 | const path = descriptor.path; | ||
| 606 | const searchDoc = catalogEntry.projected.searchDocs[capability.id]; | ||
| 607 | const searchNamespace = catalogNamespaceFromPath(path); | ||
| 608 | const searchText = [ | ||
| 609 | path, | ||
| 610 | searchNamespace, | ||
| 611 | catalogEntry.source.name, | ||
| 612 | capability.surface.title, | ||
| 613 | capability.surface.summary, | ||
| 614 | capability.surface.description, | ||
| 615 | ...(searchDoc?.tags ?? []), | ||
| 616 | ...(searchDoc?.protocolHints ?? []), | ||
| 617 | ...(searchDoc?.authHints ?? []), | ||
| 618 | ] | ||
| 619 | .filter((part): part is string => typeof part === "string" && part.length > 0) | ||
| 620 | .join(" ") | ||
| 621 | .toLowerCase(); | ||
| 622 | |||
| 623 | return { | ||
| 624 | path, | ||
| 625 | searchNamespace, | ||
| 626 | searchText, | ||
| 627 | source: catalogEntry.source, | ||
| 628 | sourceRecord: catalogEntry.sourceRecord, | ||
| 629 | revision: catalogEntry.revision, | ||
| 630 | capabilityId: capability.id, | ||
| 631 | executableId: executable.id, | ||
| 632 | capability, | ||
| 633 | executable, | ||
| 634 | descriptor, | ||
| 635 | projectedCatalog: catalogEntry.projected.catalog, | ||
| 636 | } satisfies LoadedSourceCatalogTool; | ||
| 637 | }), | ||
| 638 | ), | ||
| 639 | ); | ||
| 640 | |||
| 641 | export const loadWorkspaceSourceCatalogToolIndex = (input: { | ||
| 642 | workspaceId: WorkspaceId; | ||
| 643 | actorAccountId?: AccountId | null; | ||
| 644 | includeSchemas: boolean; | ||
| 645 | }): Effect.Effect< | ||
| 646 | readonly LoadedSourceCatalogToolIndexEntry[], | ||
| 647 | Error, | ||
| 648 | SourceCatalogRuntimeServices | ||
| 649 | > => | ||
| 650 | Effect.gen(function* () { | ||
| 651 | const catalogs = yield* loadWorkspaceSourceCatalogs({ | ||
| 652 | workspaceId: input.workspaceId, | ||
| 653 | actorAccountId: input.actorAccountId, | ||
| 654 | }); | ||
| 655 | const tools = yield* expandCatalogTools({ | ||
| 656 | catalogs, | ||
| 657 | includeSchemas: input.includeSchemas, | ||
| 658 | }); | ||
| 659 | return tools.map((tool) => ({ | ||
| 660 | path: tool.path, | ||
| 661 | searchNamespace: tool.searchNamespace, | ||
| 662 | searchText: tool.searchText, | ||
| 663 | source: tool.source, | ||
| 664 | sourceRecord: tool.sourceRecord, | ||
| 665 | capabilityId: tool.capabilityId, | ||
| 666 | executableId: tool.executableId, | ||
| 667 | capability: tool.capability, | ||
| 668 | executable: tool.executable, | ||
| 669 | descriptor: tool.descriptor, | ||
| 670 | projectedCatalog: tool.projectedCatalog, | ||
| 671 | })); | ||
| 672 | }); | ||
| 673 | |||
| 674 | export const loadWorkspaceSourceCatalogToolByPath = (input: { | ||
| 675 | workspaceId: WorkspaceId; | ||
| 676 | path: string; | ||
| 677 | actorAccountId?: AccountId | null; | ||
| 678 | includeSchemas: boolean; | ||
| 679 | }): Effect.Effect< | ||
| 680 | LoadedSourceCatalogToolIndexEntry | null, | ||
| 681 | Error, | ||
| 682 | SourceCatalogRuntimeServices | ||
| 683 | > => | ||
| 684 | Effect.gen(function* () { | ||
| 685 | const catalogs = yield* loadWorkspaceSourceCatalogs({ | ||
| 686 | workspaceId: input.workspaceId, | ||
| 687 | actorAccountId: input.actorAccountId, | ||
| 688 | }); | ||
| 689 | const tools = yield* expandCatalogTools({ | ||
| 690 | catalogs, | ||
| 691 | includeSchemas: input.includeSchemas, | ||
| 692 | }); | ||
| 693 | const tool = tools.find((entry) => entry.path === input.path) ?? null; | ||
| 694 | return tool | ||
| 695 | ? { | ||
| 696 | path: tool.path, | ||
| 697 | searchNamespace: tool.searchNamespace, | ||
| 698 | searchText: tool.searchText, | ||
| 699 | source: tool.source, | ||
| 700 | sourceRecord: tool.sourceRecord, | ||
| 701 | capabilityId: tool.capabilityId, | ||
| 702 | executableId: tool.executableId, | ||
| 703 | capability: tool.capability, | ||
| 704 | executable: tool.executable, | ||
| 705 | descriptor: tool.descriptor, | ||
| 706 | projectedCatalog: tool.projectedCatalog, | ||
| 707 | } | ||
| 708 | : null; | ||
| 709 | }); | ||
| 710 | |||
| 711 | export const RuntimeSourceCatalogStoreLive = Layer.effect( | ||
| 712 | RuntimeSourceCatalogStoreService, | ||
| 713 | Effect.gen(function* () { | ||
| 714 | const runtimeLocalWorkspace = yield* RuntimeLocalWorkspaceService; | ||
| 715 | const sourceStore = yield* RuntimeSourceStoreService; | ||
| 716 | const sourceArtifactStore = yield* SourceArtifactStore; | ||
| 717 | |||
| 718 | const deps: RuntimeSourceCatalogStoreDeps = { | ||
| 719 | runtimeLocalWorkspace, | ||
| 720 | sourceStore, | ||
| 721 | sourceArtifactStore, | ||
| 722 | }; | ||
| 723 | |||
| 724 | return RuntimeSourceCatalogStoreService.of({ | ||
| 725 | loadWorkspaceSourceCatalogs: (input) => | ||
| 726 | loadWorkspaceSourceCatalogsWithDeps(deps, input), | ||
| 727 | loadSourceWithCatalog: (input) => | ||
| 728 | loadSourceWithCatalogWithDeps(deps, input), | ||
| 729 | loadWorkspaceSourceCatalogToolIndex: (input) => | ||
| 730 | loadWorkspaceSourceCatalogToolIndex(input).pipe( | ||
| 731 | Effect.provideService(RuntimeLocalWorkspaceService, runtimeLocalWorkspace), | ||
| 732 | Effect.provideService(RuntimeSourceStoreService, sourceStore), | ||
| 733 | Effect.provideService(SourceArtifactStore, sourceArtifactStore), | ||
| 734 | ), | ||
| 735 | loadWorkspaceSourceCatalogToolByPath: (input) => | ||
| 736 | loadWorkspaceSourceCatalogToolByPath(input).pipe( | ||
| 737 | Effect.provideService(RuntimeLocalWorkspaceService, runtimeLocalWorkspace), | ||
| 738 | Effect.provideService(RuntimeSourceStoreService, sourceStore), | ||
| 739 | Effect.provideService(SourceArtifactStore, sourceArtifactStore), | ||
| 740 | ), | ||
| 741 | }); | ||
| 742 | }), | ||
| 743 | ); | ||
| @@ -0,0 +1,221 @@ | |||
| 1 | import { describe, expect, it } from "@effect/vitest"; | ||
| 2 | |||
| 3 | import type { Source } from "#schema"; | ||
| 4 | |||
| 5 | import { | ||
| 6 | createGoogleDiscoveryCatalogSnapshot, | ||
| 7 | createGraphqlCatalogSnapshot, | ||
| 8 | createOpenApiCatalogSnapshot, | ||
| 9 | } from "./source-catalog-snapshot"; | ||
| 10 | |||
| 11 | const baseSource: Source = { | ||
| 12 | id: "src_calendar" as Source["id"], | ||
| 13 | workspaceId: "ws_test" as Source["workspaceId"], | ||
| 14 | name: "Calendar", | ||
| 15 | kind: "openapi", | ||
| 16 | endpoint: "https://api.example.test", | ||
| 17 | status: "connected", | ||
| 18 | enabled: true, | ||
| 19 | namespace: "google.calendar", | ||
| 20 | bindingVersion: 1, | ||
| 21 | binding: { | ||
| 22 | specUrl: "https://api.example.test/openapi.json", | ||
| 23 | defaultHeaders: null, | ||
| 24 | }, | ||
| 25 | importAuthPolicy: "none", | ||
| 26 | importAuth: { kind: "none" }, | ||
| 27 | auth: { kind: "none" }, | ||
| 28 | sourceHash: "hash_source", | ||
| 29 | lastError: null, | ||
| 30 | createdAt: 1, | ||
| 31 | updatedAt: 1, | ||
| 32 | }; | ||
| 33 | |||
| 34 | describe("source-catalog-snapshot", () => { | ||
| 35 | it("builds an HTTP capability graph from OpenAPI operation inputs", () => { | ||
| 36 | const snapshot = createOpenApiCatalogSnapshot({ | ||
| 37 | source: baseSource, | ||
| 38 | documents: [{ | ||
| 39 | documentKind: "openapi", | ||
| 40 | documentKey: "https://api.example.test/openapi.json", | ||
| 41 | contentText: "{}", | ||
| 42 | fetchedAt: 1, | ||
| 43 | }], | ||
| 44 | operations: [{ | ||
| 45 | toolId: "events.update", | ||
| 46 | title: "Update event", | ||
| 47 | description: "Update a calendar event", | ||
| 48 | effect: "write", | ||
| 49 | inputSchema: { | ||
| 50 | type: "object", | ||
| 51 | properties: { | ||
| 52 | calendarId: { type: "string", description: "Calendar ID" }, | ||
| 53 | eventId: { type: "string" }, | ||
| 54 | sendUpdates: { type: "string", enum: ["all", "none"] }, | ||
| 55 | body: { | ||
| 56 | type: "object", | ||
| 57 | properties: { | ||
| 58 | summary: { type: "string" }, | ||
| 59 | }, | ||
| 60 | }, | ||
| 61 | }, | ||
| 62 | required: ["calendarId", "eventId", "body"], | ||
| 63 | }, | ||
| 64 | outputSchema: { | ||
| 65 | type: "object", | ||
| 66 | properties: { | ||
| 67 | id: { type: "string" }, | ||
| 68 | summary: { type: "string" }, | ||
| 69 | }, | ||
| 70 | required: ["id"], | ||
| 71 | }, | ||
| 72 | providerData: { | ||
| 73 | kind: "openapi", | ||
| 74 | toolId: "events.update", | ||
| 75 | rawToolId: "events.update", | ||
| 76 | group: "events", | ||
| 77 | leaf: "update", | ||
| 78 | tags: ["events"], | ||
| 79 | method: "patch", | ||
| 80 | path: "/calendars/{calendarId}/events/{eventId}", | ||
| 81 | operationHash: "op_hash", | ||
| 82 | invocation: { | ||
| 83 | method: "patch", | ||
| 84 | pathTemplate: "/calendars/{calendarId}/events/{eventId}", | ||
| 85 | parameters: [ | ||
| 86 | { name: "calendarId", location: "path", required: true }, | ||
| 87 | { name: "eventId", location: "path", required: true }, | ||
| 88 | { name: "sendUpdates", location: "query", required: false }, | ||
| 89 | ], | ||
| 90 | requestBody: { | ||
| 91 | required: true, | ||
| 92 | contentTypes: ["application/json"], | ||
| 93 | }, | ||
| 94 | }, | ||
| 95 | documentation: { | ||
| 96 | summary: "Update event", | ||
| 97 | parameters: [ | ||
| 98 | { | ||
| 99 | name: "calendarId", | ||
| 100 | location: "path", | ||
| 101 | required: true, | ||
| 102 | description: "Calendar identifier", | ||
| 103 | }, | ||
| 104 | ], | ||
| 105 | requestBody: { | ||
| 106 | description: "Event patch body", | ||
| 107 | }, | ||
| 108 | response: { | ||
| 109 | statusCode: "200", | ||
| 110 | description: "Updated event", | ||
| 111 | contentTypes: ["application/json"], | ||
| 112 | }, | ||
| 113 | }, | ||
| 114 | }, | ||
| 115 | }], | ||
| 116 | }); | ||
| 117 | |||
| 118 | expect(snapshot.version).toBe("ir.v1.snapshot"); | ||
| 119 | expect(Object.keys(snapshot.catalog.capabilities)).toHaveLength(1); | ||
| 120 | |||
| 121 | const capability = Object.values(snapshot.catalog.capabilities)[0]!; | ||
| 122 | const executable = Object.values(snapshot.catalog.executables)[0]!; | ||
| 123 | const responseSet = Object.values(snapshot.catalog.responseSets)[0]!; | ||
| 124 | |||
| 125 | expect(capability.surface.toolPath).toEqual(["google", "calendar", "events", "update"]); | ||
| 126 | expect(capability.semantics.effect).toBe("write"); | ||
| 127 | expect(executable.protocol).toBe("http"); | ||
| 128 | expect(executable.method).toBe("PATCH"); | ||
| 129 | expect(executable.pathTemplate).toBe("/calendars/{calendarId}/events/{eventId}"); | ||
| 130 | expect(responseSet.variants).toHaveLength(1); | ||
| 131 | }); | ||
| 132 | |||
| 133 | it("imports Google Discovery scopes as auth requirements", () => { | ||
| 134 | const snapshot = createGoogleDiscoveryCatalogSnapshot({ | ||
| 135 | source: { | ||
| 136 | ...baseSource, | ||
| 137 | kind: "google_discovery", | ||
| 138 | namespace: "google.drive", | ||
| 139 | }, | ||
| 140 | documents: [], | ||
| 141 | operations: [{ | ||
| 142 | toolId: "files.list", | ||
| 143 | title: "List files", | ||
| 144 | description: "List drive files", | ||
| 145 | effect: "read", | ||
| 146 | inputSchema: { type: "object", properties: { pageSize: { type: "integer" } } }, | ||
| 147 | outputSchema: { type: "object", properties: { files: { type: "array", items: { type: "string" } } } }, | ||
| 148 | providerData: { | ||
| 149 | kind: "google_discovery", | ||
| 150 | service: "drive", | ||
| 151 | version: "v3", | ||
| 152 | toolId: "files.list", | ||
| 153 | rawToolId: "files.list", | ||
| 154 | methodId: "drive.files.list", | ||
| 155 | group: "files", | ||
| 156 | leaf: "list", | ||
| 157 | invocation: { | ||
| 158 | method: "get", | ||
| 159 | path: "/drive/v3/files", | ||
| 160 | flatPath: null, | ||
| 161 | rootUrl: "https://www.googleapis.com/", | ||
| 162 | servicePath: "drive/v3/", | ||
| 163 | parameters: [], | ||
| 164 | requestSchemaId: null, | ||
| 165 | responseSchemaId: "FileList", | ||
| 166 | scopes: ["https://www.googleapis.com/auth/drive.readonly"], | ||
| 167 | supportsMediaUpload: false, | ||
| 168 | supportsMediaDownload: false, | ||
| 169 | }, | ||
| 170 | }, | ||
| 171 | }], | ||
| 172 | }); | ||
| 173 | |||
| 174 | const capability = Object.values(snapshot.catalog.capabilities)[0]!; | ||
| 175 | const securityScheme = Object.values(snapshot.catalog.symbols).find((symbol) => symbol.kind === "securityScheme"); | ||
| 176 | |||
| 177 | expect(capability.auth.kind).toBe("scheme"); | ||
| 178 | expect(securityScheme?.kind).toBe("securityScheme"); | ||
| 179 | }); | ||
| 180 | |||
| 181 | it("converts GraphQL field operations into GraphQL executables", () => { | ||
| 182 | const snapshot = createGraphqlCatalogSnapshot({ | ||
| 183 | source: { | ||
| 184 | ...baseSource, | ||
| 185 | kind: "graphql", | ||
| 186 | namespace: "github", | ||
| 187 | }, | ||
| 188 | documents: [], | ||
| 189 | operations: [{ | ||
| 190 | toolId: "viewer", | ||
| 191 | title: "Viewer", | ||
| 192 | description: "Load the current viewer", | ||
| 193 | effect: "read", | ||
| 194 | inputSchema: { type: "object", properties: {} }, | ||
| 195 | outputSchema: { type: "object", properties: { login: { type: "string" } } }, | ||
| 196 | providerData: { | ||
| 197 | kind: "graphql", | ||
| 198 | toolKind: "field", | ||
| 199 | toolId: "viewer", | ||
| 200 | rawToolId: "viewer", | ||
| 201 | group: "query", | ||
| 202 | leaf: "viewer", | ||
| 203 | fieldName: "viewer", | ||
| 204 | operationType: "query", | ||
| 205 | operationName: "ViewerQuery", | ||
| 206 | operationDocument: "query ViewerQuery { viewer { login } }", | ||
| 207 | queryTypeName: "Query", | ||
| 208 | mutationTypeName: null, | ||
| 209 | subscriptionTypeName: null, | ||
| 210 | }, | ||
| 211 | }], | ||
| 212 | }); | ||
| 213 | |||
| 214 | const executable = Object.values(snapshot.catalog.executables)[0]!; | ||
| 215 | |||
| 216 | expect(executable.protocol).toBe("graphql"); | ||
| 217 | expect(executable.operationType).toBe("query"); | ||
| 218 | expect(executable.rootField).toBe("viewer"); | ||
| 219 | expect(executable.selectionMode).toBe("fixed"); | ||
| 220 | }); | ||
| 221 | }); | ||
| @@ -0,0 +1,1688 @@ | |||
| 1 | import { sha256Hex } from "@executor/codemode-core"; | ||
| 2 | import { | ||
| 3 | type OpenApiToolProviderData, | ||
| 4 | } from "@executor/codemode-openapi"; | ||
| 5 | import { | ||
| 6 | type GoogleDiscoveryToolProviderData, | ||
| 7 | } from "@executor/codemode-google-discovery"; | ||
| 8 | |||
| 9 | import type { Source } from "#schema"; | ||
| 10 | |||
| 11 | import { | ||
| 12 | createCatalogSnapshotV1, | ||
| 13 | createEmptyCatalogV1, | ||
| 14 | } from "../ir/catalog"; | ||
| 15 | import { | ||
| 16 | CapabilityIdSchema, | ||
| 17 | DiagnosticIdSchema, | ||
| 18 | DocumentIdSchema, | ||
| 19 | ExampleSymbolIdSchema, | ||
| 20 | ExecutableIdSchema, | ||
| 21 | HeaderSymbolIdSchema, | ||
| 22 | ParameterSymbolIdSchema, | ||
| 23 | RequestBodySymbolIdSchema, | ||
| 24 | ResourceIdSchema, | ||
| 25 | ResponseSetIdSchema, | ||
| 26 | ResponseSymbolIdSchema, | ||
| 27 | ScopeIdSchema, | ||
| 28 | SecuritySchemeSymbolIdSchema, | ||
| 29 | ShapeSymbolIdSchema, | ||
| 30 | } from "../ir/ids"; | ||
| 31 | import type { | ||
| 32 | AuthRequirement, | ||
| 33 | Capability, | ||
| 34 | CatalogSnapshotV1, | ||
| 35 | CatalogV1, | ||
| 36 | ContentSpec, | ||
| 37 | DocumentationBlock, | ||
| 38 | EffectKind, | ||
| 39 | ExampleSymbol, | ||
| 40 | GraphQLExecutable, | ||
| 41 | HttpExecutable, | ||
| 42 | ImportDiagnostic, | ||
| 43 | McpExecutable, | ||
| 44 | NativeBlob, | ||
| 45 | ParameterSymbol, | ||
| 46 | ProvenanceRef, | ||
| 47 | ResponseSet, | ||
| 48 | ResponseSymbol, | ||
| 49 | Scope, | ||
| 50 | SecuritySchemeSymbol, | ||
| 51 | ShapeNode, | ||
| 52 | ShapeSymbol, | ||
| 53 | SourceKind, | ||
| 54 | SourceDocument, | ||
| 55 | } from "../ir/model"; | ||
| 56 | import type { GraphqlToolProviderData } from "./graphql-tools"; | ||
| 57 | import { namespaceFromSourceName } from "./source-names"; | ||
| 58 | |||
| 59 | type JsonSchema = boolean | Record<string, unknown>; | ||
| 60 | |||
| 61 | export type CatalogSourceDocumentInput = { | ||
| 62 | documentKind: string; | ||
| 63 | documentKey: string; | ||
| 64 | contentText: string; | ||
| 65 | fetchedAt?: number | null; | ||
| 66 | }; | ||
| 67 | |||
| 68 | type CatalogOperationInput = { | ||
| 69 | toolId: string; | ||
| 70 | title?: string | null; | ||
| 71 | description?: string | null; | ||
| 72 | effect: EffectKind; | ||
| 73 | inputSchema?: unknown; | ||
| 74 | outputSchema?: unknown; | ||
| 75 | }; | ||
| 76 | |||
| 77 | export type OpenApiCatalogOperationInput = CatalogOperationInput & { | ||
| 78 | providerData: OpenApiToolProviderData; | ||
| 79 | }; | ||
| 80 | |||
| 81 | export type GoogleDiscoveryCatalogOperationInput = CatalogOperationInput & { | ||
| 82 | providerData: GoogleDiscoveryToolProviderData; | ||
| 83 | }; | ||
| 84 | |||
| 85 | export type GraphqlCatalogOperationInput = CatalogOperationInput & { | ||
| 86 | providerData: GraphqlToolProviderData; | ||
| 87 | }; | ||
| 88 | |||
| 89 | export type McpCatalogOperationInput = CatalogOperationInput & { | ||
| 90 | providerData: { | ||
| 91 | toolId: string; | ||
| 92 | toolName: string; | ||
| 93 | description: string | null; | ||
| 94 | }; | ||
| 95 | }; | ||
| 96 | |||
| 97 | type JsonSchemaImporter = { | ||
| 98 | importSchema: (schema: unknown, key: string) => ReturnType<typeof ShapeSymbolIdSchema.make>; | ||
| 99 | }; | ||
| 100 | |||
| 101 | const stableStringify = (value: unknown): string => { | ||
| 102 | if (value === null || typeof value !== "object") { | ||
| 103 | return JSON.stringify(value); | ||
| 104 | } | ||
| 105 | |||
| 106 | if (Array.isArray(value)) { | ||
| 107 | return `[${value.map((entry) => stableStringify(entry)).join(",")}]`; | ||
| 108 | } | ||
| 109 | |||
| 110 | const entries = Object.entries(value as Record<string, unknown>) | ||
| 111 | .sort(([left], [right]) => left.localeCompare(right)) | ||
| 112 | .map(([key, entry]) => `${JSON.stringify(key)}:${stableStringify(entry)}`); | ||
| 113 | return `{${entries.join(",")}}`; | ||
| 114 | }; | ||
| 115 | |||
| 116 | const stableHash = (value: unknown): string => | ||
| 117 | sha256Hex(stableStringify(value)).slice(0, 16); | ||
| 118 | |||
| 119 | const mutableRecord = <K extends string, V>(value: Readonly<Record<K, V>>): Record<K, V> => | ||
| 120 | value as unknown as Record<K, V>; | ||
| 121 | |||
| 122 | const asObject = (value: unknown): Record<string, unknown> => | ||
| 123 | value !== null && typeof value === "object" && !Array.isArray(value) | ||
| 124 | ? (value as Record<string, unknown>) | ||
| 125 | : {}; | ||
| 126 | |||
| 127 | const asString = (value: unknown): string | null => | ||
| 128 | typeof value === "string" && value.trim().length > 0 ? value : null; | ||
| 129 | |||
| 130 | const asBoolean = (value: unknown): boolean | null => | ||
| 131 | typeof value === "boolean" ? value : null; | ||
| 132 | |||
| 133 | const asNumber = (value: unknown): number | null => | ||
| 134 | typeof value === "number" && Number.isFinite(value) ? value : null; | ||
| 135 | |||
| 136 | const asArray = (value: unknown): unknown[] => | ||
| 137 | Array.isArray(value) ? value : []; | ||
| 138 | |||
| 139 | const asStringArray = (value: unknown): string[] => | ||
| 140 | asArray(value).flatMap((entry) => { | ||
| 141 | const stringValue = asString(entry); | ||
| 142 | return stringValue === null ? [] : [stringValue]; | ||
| 143 | }); | ||
| 144 | |||
| 145 | const sourceKindFromSource = (source: Source): SourceKind => { | ||
| 146 | switch (source.kind) { | ||
| 147 | case "openapi": | ||
| 148 | return "openapi"; | ||
| 149 | case "graphql": | ||
| 150 | return "graphql-schema"; | ||
| 151 | case "google_discovery": | ||
| 152 | return "google-discovery"; | ||
| 153 | case "mcp": | ||
| 154 | return "mcp"; | ||
| 155 | default: | ||
| 156 | return "custom"; | ||
| 157 | } | ||
| 158 | }; | ||
| 159 | |||
| 160 | const toolPathSegments = (source: Source, toolId: string): string[] => { | ||
| 161 | const namespace = source.namespace ?? namespaceFromSourceName(source.name); | ||
| 162 | const fullPath = namespace ? `${namespace}.${toolId}` : toolId; | ||
| 163 | return fullPath.split(".").filter((segment) => segment.length > 0); | ||
| 164 | }; | ||
| 165 | |||
| 166 | const serviceScopeIdForSource = (source: Source) => | ||
| 167 | ScopeIdSchema.make(`scope_service_${stableHash({ sourceId: source.id })}`); | ||
| 168 | |||
| 169 | const documentIdFor = (source: Source, key: string) => | ||
| 170 | DocumentIdSchema.make(`doc_${stableHash({ sourceId: source.id, key })}`); | ||
| 171 | |||
| 172 | const resourceIdForSource = (source: Source) => | ||
| 173 | ResourceIdSchema.make(`res_${stableHash({ sourceId: source.id })}`); | ||
| 174 | |||
| 175 | const provenanceFor = (documentId: ReturnType<typeof DocumentIdSchema.make>, pointer: string): ProvenanceRef[] => [{ | ||
| 176 | relation: "declared", | ||
| 177 | documentId, | ||
| 178 | pointer, | ||
| 179 | }]; | ||
| 180 | |||
| 181 | const effectFromOperationKind = (operationKind: string): EffectKind => { | ||
| 182 | switch (operationKind) { | ||
| 183 | case "read": | ||
| 184 | return "read"; | ||
| 185 | case "delete": | ||
| 186 | return "delete"; | ||
| 187 | case "write": | ||
| 188 | return "write"; | ||
| 189 | default: | ||
| 190 | return "action"; | ||
| 191 | } | ||
| 192 | }; | ||
| 193 | |||
| 194 | const interactionForEffect = (effect: EffectKind): Capability["interaction"] => ({ | ||
| 195 | approval: { | ||
| 196 | mayRequire: effect !== "read", | ||
| 197 | reasons: | ||
| 198 | effect === "delete" | ||
| 199 | ? ["delete"] | ||
| 200 | : effect === "write" || effect === "action" | ||
| 201 | ? ["write"] | ||
| 202 | : [], | ||
| 203 | }, | ||
| 204 | elicitation: { | ||
| 205 | mayRequest: false, | ||
| 206 | }, | ||
| 207 | resume: { | ||
| 208 | supported: false, | ||
| 209 | }, | ||
| 210 | }); | ||
| 211 | |||
| 212 | const importMetadataFor = (input: { | ||
| 213 | source: Source; | ||
| 214 | adapterKey: string; | ||
| 215 | }) => ({ | ||
| 216 | sourceKind: sourceKindFromSource(input.source), | ||
| 217 | adapterKey: input.adapterKey, | ||
| 218 | importerVersion: "ir.v1.snapshot_builder", | ||
| 219 | importedAt: new Date().toISOString(), | ||
| 220 | sourceConfigHash: | ||
| 221 | input.source.sourceHash | ||
| 222 | ?? stableHash({ | ||
| 223 | endpoint: input.source.endpoint, | ||
| 224 | binding: input.source.binding, | ||
| 225 | auth: input.source.auth.kind, | ||
| 226 | }), | ||
| 227 | }); | ||
| 228 | |||
| 229 | const addDiagnostic = ( | ||
| 230 | catalog: CatalogV1, | ||
| 231 | input: Omit<ImportDiagnostic, "id">, | ||
| 232 | ) => { | ||
| 233 | const id = DiagnosticIdSchema.make(`diag_${stableHash(input)}`); | ||
| 234 | mutableRecord(catalog.diagnostics)[id] = { | ||
| 235 | id, | ||
| 236 | ...input, | ||
| 237 | }; | ||
| 238 | return id; | ||
| 239 | }; | ||
| 240 | |||
| 241 | const nativeBlob = (input: { | ||
| 242 | source: Source; | ||
| 243 | kind: string; | ||
| 244 | pointer: string; | ||
| 245 | value: unknown; | ||
| 246 | summary?: string; | ||
| 247 | }): NativeBlob => ({ | ||
| 248 | sourceKind: sourceKindFromSource(input.source), | ||
| 249 | kind: input.kind, | ||
| 250 | pointer: input.pointer, | ||
| 251 | encoding: "json", | ||
| 252 | ...(input.summary ? { summary: input.summary } : {}), | ||
| 253 | value: input.value, | ||
| 254 | }); | ||
| 255 | |||
| 256 | const docsFrom = (input: { | ||
| 257 | summary?: string | null; | ||
| 258 | description?: string | null; | ||
| 259 | externalDocsUrl?: string | null; | ||
| 260 | }): DocumentationBlock | undefined => { | ||
| 261 | const summary = input.summary ?? undefined; | ||
| 262 | const description = input.description ?? undefined; | ||
| 263 | const externalDocsUrl = input.externalDocsUrl ?? undefined; | ||
| 264 | |||
| 265 | if (!summary && !description && !externalDocsUrl) { | ||
| 266 | return undefined; | ||
| 267 | } | ||
| 268 | |||
| 269 | return { | ||
| 270 | ...(summary ? { summary } : {}), | ||
| 271 | ...(description ? { description } : {}), | ||
| 272 | ...(externalDocsUrl ? { externalDocsUrl } : {}), | ||
| 273 | }; | ||
| 274 | }; | ||
| 275 | |||
| 276 | const createJsonSchemaImporter = (input: { | ||
| 277 | catalog: CatalogV1; | ||
| 278 | source: Source; | ||
| 279 | resourceId: ReturnType<typeof ResourceIdSchema.make>; | ||
| 280 | documentId: ReturnType<typeof DocumentIdSchema.make>; | ||
| 281 | }) : JsonSchemaImporter => { | ||
| 282 | const cache = new Map<string, ReturnType<typeof ShapeSymbolIdSchema.make>>(); | ||
| 283 | |||
| 284 | const resolvePointer = (root: unknown, pointer: string): unknown => { | ||
| 285 | if (pointer === "#" || pointer.length === 0) { | ||
| 286 | return root; | ||
| 287 | } | ||
| 288 | |||
| 289 | const segments = pointer | ||
| 290 | .replace(/^#\//, "") | ||
| 291 | .split("/") | ||
| 292 | .map((segment) => segment.replace(/~1/g, "/").replace(/~0/g, "~")); | ||
| 293 | let current: unknown = root; | ||
| 294 | for (const segment of segments) { | ||
| 295 | if (Array.isArray(current)) { | ||
| 296 | const index = Number(segment); | ||
| 297 | current = Number.isInteger(index) ? current[index] : undefined; | ||
| 298 | continue; | ||
| 299 | } | ||
| 300 | |||
| 301 | current = asObject(current)[segment]; | ||
| 302 | } | ||
| 303 | |||
| 304 | return current; | ||
| 305 | }; | ||
| 306 | |||
| 307 | const importSchema = (schema: unknown, key: string, rootSchema?: unknown): ReturnType<typeof ShapeSymbolIdSchema.make> => { | ||
| 308 | const stableKey = `${input.resourceId}:${key}`; | ||
| 309 | const cached = cache.get(stableKey); | ||
| 310 | if (cached) { | ||
| 311 | return cached; | ||
| 312 | } | ||
| 313 | |||
| 314 | const shapeId = ShapeSymbolIdSchema.make(`shape_${stableHash({ resourceId: input.resourceId, key })}`); | ||
| 315 | cache.set(stableKey, shapeId); | ||
| 316 | |||
| 317 | const objectSchema = asObject(schema); | ||
| 318 | const title = asString(objectSchema.title) ?? undefined; | ||
| 319 | const docs = docsFrom({ | ||
| 320 | description: asString(objectSchema.description), | ||
| 321 | }); | ||
| 322 | const deprecated = asBoolean(objectSchema.deprecated) ?? undefined; | ||
| 323 | |||
| 324 | const register = (node: ShapeNode, extras: { | ||
| 325 | native?: NativeBlob[]; | ||
| 326 | diagnosticIds?: ReturnType<typeof DiagnosticIdSchema.make>[]; | ||
| 327 | } = {}) => { | ||
| 328 | mutableRecord(input.catalog.symbols)[shapeId] = { | ||
| 329 | id: shapeId, | ||
| 330 | kind: "shape", | ||
| 331 | resourceId: input.resourceId, | ||
| 332 | ...(title ? { title } : {}), | ||
| 333 | ...(docs ? { docs } : {}), | ||
| 334 | ...(deprecated !== undefined ? { deprecated } : {}), | ||
| 335 | node, | ||
| 336 | synthetic: false, | ||
| 337 | provenance: provenanceFor(input.documentId, key), | ||
| 338 | ...(extras.diagnosticIds && extras.diagnosticIds.length > 0 | ||
| 339 | ? { diagnosticIds: extras.diagnosticIds } | ||
| 340 | : {}), | ||
| 341 | ...(extras.native && extras.native.length > 0 ? { native: extras.native } : {}), | ||
| 342 | } satisfies ShapeSymbol; | ||
| 343 | }; | ||
| 344 | |||
| 345 | if (typeof schema === "boolean") { | ||
| 346 | register({ | ||
| 347 | type: "unknown", | ||
| 348 | reason: schema ? "schema_true" : "schema_false", | ||
| 349 | }); | ||
| 350 | return shapeId; | ||
| 351 | } | ||
| 352 | |||
| 353 | const ref = asString(objectSchema.$ref); | ||
| 354 | if (ref !== null) { | ||
| 355 | const resolved = | ||
| 356 | ref.startsWith("#") | ||
| 357 | ? resolvePointer(rootSchema ?? schema, ref) | ||
| 358 | : undefined; | ||
| 359 | |||
| 360 | if (resolved === undefined) { | ||
| 361 | const diagnosticId = addDiagnostic(input.catalog, { | ||
| 362 | level: "warning", | ||
| 363 | code: "unresolved_ref", | ||
| 364 | message: `Unresolved JSON schema ref ${ref}`, | ||
| 365 | provenance: provenanceFor(input.documentId, key), | ||
| 366 | relatedSymbolIds: [shapeId], | ||
| 367 | }); | ||
| 368 | register( | ||
| 369 | { | ||
| 370 | type: "unknown", | ||
| 371 | reason: `unresolved_ref:${ref}`, | ||
| 372 | }, | ||
| 373 | { | ||
| 374 | diagnosticIds: [diagnosticId], | ||
| 375 | }, | ||
| 376 | ); | ||
| 377 | return shapeId; | ||
| 378 | } | ||
| 379 | |||
| 380 | const target = importSchema(resolved, ref, rootSchema ?? schema); | ||
| 381 | register({ | ||
| 382 | type: "ref", | ||
| 383 | target, | ||
| 384 | }); | ||
| 385 | return shapeId; | ||
| 386 | } | ||
| 387 | |||
| 388 | const enumValues = asArray(objectSchema.enum); | ||
| 389 | if (enumValues.length === 1) { | ||
| 390 | register({ | ||
| 391 | type: "const", | ||
| 392 | value: enumValues[0], | ||
| 393 | }); | ||
| 394 | return shapeId; | ||
| 395 | } | ||
| 396 | |||
| 397 | if (enumValues.length > 1) { | ||
| 398 | register({ | ||
| 399 | type: "enum", | ||
| 400 | values: enumValues, | ||
| 401 | }); | ||
| 402 | return shapeId; | ||
| 403 | } | ||
| 404 | |||
| 405 | if ("const" in objectSchema) { | ||
| 406 | register({ | ||
| 407 | type: "const", | ||
| 408 | value: objectSchema.const, | ||
| 409 | }); | ||
| 410 | return shapeId; | ||
| 411 | } | ||
| 412 | |||
| 413 | const anyOf = asArray(objectSchema.anyOf); | ||
| 414 | if (anyOf.length > 0) { | ||
| 415 | register({ | ||
| 416 | type: "anyOf", | ||
| 417 | items: anyOf.map((entry, index) => importSchema(entry, `${key}/anyOf/${index}`, rootSchema ?? schema)), | ||
| 418 | }); | ||
| 419 | return shapeId; | ||
| 420 | } | ||
| 421 | |||
| 422 | const allOf = asArray(objectSchema.allOf); | ||
| 423 | if (allOf.length > 0) { | ||
| 424 | register({ | ||
| 425 | type: "allOf", | ||
| 426 | items: allOf.map((entry, index) => importSchema(entry, `${key}/allOf/${index}`, rootSchema ?? schema)), | ||
| 427 | }); | ||
| 428 | return shapeId; | ||
| 429 | } | ||
| 430 | |||
| 431 | const oneOf = asArray(objectSchema.oneOf); | ||
| 432 | if (oneOf.length > 0) { | ||
| 433 | register({ | ||
| 434 | type: "oneOf", | ||
| 435 | items: oneOf.map((entry, index) => importSchema(entry, `${key}/oneOf/${index}`, rootSchema ?? schema)), | ||
| 436 | }); | ||
| 437 | return shapeId; | ||
| 438 | } | ||
| 439 | |||
| 440 | if ("if" in objectSchema || "then" in objectSchema || "else" in objectSchema) { | ||
| 441 | register({ | ||
| 442 | type: "conditional", | ||
| 443 | ifShapeId: importSchema(objectSchema.if ?? {}, `${key}/if`, rootSchema ?? schema), | ||
| 444 | thenShapeId: importSchema(objectSchema.then ?? {}, `${key}/then`, rootSchema ?? schema), | ||
| 445 | ...(objectSchema.else !== undefined | ||
| 446 | ? { | ||
| 447 | elseShapeId: importSchema(objectSchema.else, `${key}/else`, rootSchema ?? schema), | ||
| 448 | } | ||
| 449 | : {}), | ||
| 450 | }); | ||
| 451 | return shapeId; | ||
| 452 | } | ||
| 453 | |||
| 454 | if ("not" in objectSchema) { | ||
| 455 | register({ | ||
| 456 | type: "not", | ||
| 457 | itemShapeId: importSchema(objectSchema.not, `${key}/not`, rootSchema ?? schema), | ||
| 458 | }); | ||
| 459 | return shapeId; | ||
| 460 | } | ||
| 461 | |||
| 462 | const declaredType = objectSchema.type; | ||
| 463 | const typeArray = Array.isArray(declaredType) | ||
| 464 | ? declaredType.flatMap((entry) => { | ||
| 465 | const value = asString(entry); | ||
| 466 | return value === null ? [] : [value]; | ||
| 467 | }) | ||
| 468 | : []; | ||
| 469 | const nullable = asBoolean(objectSchema.nullable) === true || typeArray.includes("null"); | ||
| 470 | const effectiveType = Array.isArray(declaredType) | ||
| 471 | ? typeArray.find((entry) => entry !== "null") ?? null | ||
| 472 | : asString(declaredType); | ||
| 473 | |||
| 474 | const registerNullable = (itemShapeId: ReturnType<typeof ShapeSymbolIdSchema.make>) => { | ||
| 475 | register({ | ||
| 476 | type: "nullable", | ||
| 477 | itemShapeId, | ||
| 478 | }); | ||
| 479 | return shapeId; | ||
| 480 | }; | ||
| 481 | |||
| 482 | const constraints: Record<string, unknown> = {}; | ||
| 483 | for (const constraintKey of [ | ||
| 484 | "format", | ||
| 485 | "minLength", | ||
| 486 | "maxLength", | ||
| 487 | "pattern", | ||
| 488 | "minimum", | ||
| 489 | "maximum", | ||
| 490 | "exclusiveMinimum", | ||
| 491 | "exclusiveMaximum", | ||
| 492 | "multipleOf", | ||
| 493 | "default", | ||
| 494 | "examples", | ||
| 495 | ]) { | ||
| 496 | if (objectSchema[constraintKey] !== undefined) { | ||
| 497 | constraints[constraintKey] = objectSchema[constraintKey]; | ||
| 498 | } | ||
| 499 | } | ||
| 500 | |||
| 501 | if ( | ||
| 502 | effectiveType === "object" | ||
| 503 | || "properties" in objectSchema | ||
| 504 | || "additionalProperties" in objectSchema | ||
| 505 | || "patternProperties" in objectSchema | ||
| 506 | ) { | ||
| 507 | const fields = Object.fromEntries( | ||
| 508 | Object.entries(asObject(objectSchema.properties)).map(([fieldName, fieldSchema]) => [ | ||
| 509 | fieldName, | ||
| 510 | { | ||
| 511 | shapeId: importSchema( | ||
| 512 | fieldSchema, | ||
| 513 | `${key}/properties/${fieldName}`, | ||
| 514 | rootSchema ?? schema, | ||
| 515 | ), | ||
| 516 | ...(docsFrom({ | ||
| 517 | description: asString(asObject(fieldSchema).description), | ||
| 518 | }) | ||
| 519 | ? { | ||
| 520 | docs: docsFrom({ | ||
| 521 | description: asString(asObject(fieldSchema).description), | ||
| 522 | })!, | ||
| 523 | } | ||
| 524 | : {}), | ||
| 525 | }, | ||
| 526 | ]), | ||
| 527 | ); | ||
| 528 | |||
| 529 | const patternProperties = Object.fromEntries( | ||
| 530 | Object.entries(asObject(objectSchema.patternProperties)).map(([pattern, value]) => [ | ||
| 531 | pattern, | ||
| 532 | importSchema(value, `${key}/patternProperties/${pattern}`, rootSchema ?? schema), | ||
| 533 | ]), | ||
| 534 | ); | ||
| 535 | |||
| 536 | const additionalPropertiesValue = objectSchema.additionalProperties; | ||
| 537 | const additionalProperties = | ||
| 538 | typeof additionalPropertiesValue === "boolean" | ||
| 539 | ? additionalPropertiesValue | ||
| 540 | : additionalPropertiesValue !== undefined | ||
| 541 | ? importSchema(additionalPropertiesValue, `${key}/additionalProperties`, rootSchema ?? schema) | ||
| 542 | : undefined; | ||
| 543 | |||
| 544 | const objectNode: ShapeNode = { | ||
| 545 | type: "object", | ||
| 546 | fields, | ||
| 547 | ...(asStringArray(objectSchema.required).length > 0 | ||
| 548 | ? { required: asStringArray(objectSchema.required) } | ||
| 549 | : {}), | ||
| 550 | ...(additionalProperties !== undefined ? { additionalProperties } : {}), | ||
| 551 | ...(Object.keys(patternProperties).length > 0 ? { patternProperties } : {}), | ||
| 552 | }; | ||
| 553 | |||
| 554 | if (nullable) { | ||
| 555 | const innerId = importSchema( | ||
| 556 | { | ||
| 557 | ...objectSchema, | ||
| 558 | nullable: false, | ||
| 559 | type: "object", | ||
| 560 | }, | ||
| 561 | `${key}:nonnull`, | ||
| 562 | rootSchema ?? schema, | ||
| 563 | ); | ||
| 564 | return registerNullable(innerId); | ||
| 565 | } | ||
| 566 | |||
| 567 | register(objectNode); | ||
| 568 | return shapeId; | ||
| 569 | } | ||
| 570 | |||
| 571 | if (effectiveType === "array" || "items" in objectSchema || "prefixItems" in objectSchema) { | ||
| 572 | if (Array.isArray(objectSchema.prefixItems) && objectSchema.prefixItems.length > 0) { | ||
| 573 | const tupleNode: ShapeNode = { | ||
| 574 | type: "tuple", | ||
| 575 | itemShapeIds: objectSchema.prefixItems.map((entry, index) => | ||
| 576 | importSchema(entry, `${key}/prefixItems/${index}`, rootSchema ?? schema) | ||
| 577 | ), | ||
| 578 | ...(objectSchema.items !== undefined | ||
| 579 | ? { | ||
| 580 | additionalItems: | ||
| 581 | typeof objectSchema.items === "boolean" | ||
| 582 | ? objectSchema.items | ||
| 583 | : importSchema(objectSchema.items, `${key}/items`, rootSchema ?? schema), | ||
| 584 | } | ||
| 585 | : {}), | ||
| 586 | }; | ||
| 587 | |||
| 588 | if (nullable) { | ||
| 589 | const innerId = importSchema( | ||
| 590 | { | ||
| 591 | ...objectSchema, | ||
| 592 | nullable: false, | ||
| 593 | type: "array", | ||
| 594 | }, | ||
| 595 | `${key}:nonnull`, | ||
| 596 | rootSchema ?? schema, | ||
| 597 | ); | ||
| 598 | return registerNullable(innerId); | ||
| 599 | } | ||
| 600 | |||
| 601 | register(tupleNode); | ||
| 602 | return shapeId; | ||
| 603 | } | ||
| 604 | |||
| 605 | const items = objectSchema.items ?? {}; | ||
| 606 | const arrayNode: ShapeNode = { | ||
| 607 | type: "array", | ||
| 608 | itemShapeId: importSchema(items, `${key}/items`, rootSchema ?? schema), | ||
| 609 | ...(asNumber(objectSchema.minItems) !== null ? { minItems: asNumber(objectSchema.minItems)! } : {}), | ||
| 610 | ...(asNumber(objectSchema.maxItems) !== null ? { maxItems: asNumber(objectSchema.maxItems)! } : {}), | ||
| 611 | }; | ||
| 612 | |||
| 613 | if (nullable) { | ||
| 614 | const innerId = importSchema( | ||
| 615 | { | ||
| 616 | ...objectSchema, | ||
| 617 | nullable: false, | ||
| 618 | type: "array", | ||
| 619 | }, | ||
| 620 | `${key}:nonnull`, | ||
| 621 | rootSchema ?? schema, | ||
| 622 | ); | ||
| 623 | return registerNullable(innerId); | ||
| 624 | } | ||
| 625 | |||
| 626 | register(arrayNode); | ||
| 627 | return shapeId; | ||
| 628 | } | ||
| 629 | |||
| 630 | if ( | ||
| 631 | effectiveType === "string" | ||
| 632 | || effectiveType === "number" | ||
| 633 | || effectiveType === "integer" | ||
| 634 | || effectiveType === "boolean" | ||
| 635 | || effectiveType === "null" | ||
| 636 | ) { | ||
| 637 | const scalar = | ||
| 638 | effectiveType === "null" | ||
| 639 | ? "null" | ||
| 640 | : effectiveType === "integer" | ||
| 641 | ? "integer" | ||
| 642 | : effectiveType === "number" | ||
| 643 | ? "number" | ||
| 644 | : effectiveType === "boolean" | ||
| 645 | ? "boolean" | ||
| 646 | : asString(objectSchema.format) === "binary" | ||
| 647 | ? "bytes" | ||
| 648 | : "string"; | ||
| 649 | |||
| 650 | const scalarNode: ShapeNode = { | ||
| 651 | type: "scalar", | ||
| 652 | scalar, | ||
| 653 | ...(asString(objectSchema.format) ? { format: asString(objectSchema.format)! } : {}), | ||
| 654 | ...(Object.keys(constraints).length > 0 ? { constraints } : {}), | ||
| 655 | }; | ||
| 656 | |||
| 657 | if (nullable && scalar !== "null") { | ||
| 658 | const innerId = importSchema( | ||
| 659 | { | ||
| 660 | ...objectSchema, | ||
| 661 | nullable: false, | ||
| 662 | type: effectiveType, | ||
| 663 | }, | ||
| 664 | `${key}:nonnull`, | ||
| 665 | rootSchema ?? schema, | ||
| 666 | ); | ||
| 667 | return registerNullable(innerId); | ||
| 668 | } | ||
| 669 | |||
| 670 | register(scalarNode); | ||
| 671 | return shapeId; | ||
| 672 | } | ||
| 673 | |||
| 674 | register({ | ||
| 675 | type: "unknown", | ||
| 676 | reason: `unsupported_schema:${key}`, | ||
| 677 | }, { | ||
| 678 | native: [nativeBlob({ | ||
| 679 | source: input.source, | ||
| 680 | kind: "json_schema", | ||
| 681 | pointer: key, | ||
| 682 | value: schema, | ||
| 683 | summary: "Unsupported JSON schema preserved natively", | ||
| 684 | })], | ||
| 685 | }); | ||
| 686 | return shapeId; | ||
| 687 | }; | ||
| 688 | |||
| 689 | return { | ||
| 690 | importSchema: (schema, key) => importSchema(schema, key, schema), | ||
| 691 | }; | ||
| 692 | }; | ||
| 693 | |||
| 694 | const exampleSymbolFromValue = (input: { | ||
| 695 | catalog: CatalogV1; | ||
| 696 | source: Source; | ||
| 697 | documentId: ReturnType<typeof DocumentIdSchema.make>; | ||
| 698 | pointer: string; | ||
| 699 | name?: string; | ||
| 700 | value: unknown; | ||
| 701 | summary?: string; | ||
| 702 | description?: string; | ||
| 703 | }): ReturnType<typeof ExampleSymbolIdSchema.make> => { | ||
| 704 | const exampleId = ExampleSymbolIdSchema.make(`example_${stableHash({ | ||
| 705 | pointer: input.pointer, | ||
| 706 | value: input.value, | ||
| 707 | })}`); | ||
| 708 | mutableRecord(input.catalog.symbols)[exampleId] = { | ||
| 709 | id: exampleId, | ||
| 710 | kind: "example", | ||
| 711 | exampleKind: "value", | ||
| 712 | ...(input.name ? { name: input.name } : {}), | ||
| 713 | ...(docsFrom({ | ||
| 714 | summary: input.summary ?? null, | ||
| 715 | description: input.description ?? null, | ||
| 716 | }) | ||
| 717 | ? { | ||
| 718 | docs: docsFrom({ | ||
| 719 | summary: input.summary ?? null, | ||
| 720 | description: input.description ?? null, | ||
| 721 | })!, | ||
| 722 | } | ||
| 723 | : {}), | ||
| 724 | value: input.value, | ||
| 725 | synthetic: false, | ||
| 726 | provenance: provenanceFor(input.documentId, input.pointer), | ||
| 727 | } satisfies ExampleSymbol; | ||
| 728 | return exampleId; | ||
| 729 | }; | ||
| 730 | |||
| 731 | const schemaFromField = (schema: unknown, fieldName: string): unknown => { | ||
| 732 | const record = asObject(schema); | ||
| 733 | const properties = asObject(record.properties); | ||
| 734 | if (properties[fieldName] !== undefined) { | ||
| 735 | return properties[fieldName]; | ||
| 736 | } | ||
| 737 | return undefined; | ||
| 738 | }; | ||
| 739 | |||
| 740 | const groupedSchemaForParameter = (schema: unknown, location: string, name: string): unknown => { | ||
| 741 | const direct = schemaFromField(schema, name); | ||
| 742 | if (direct !== undefined) { | ||
| 743 | return direct; | ||
| 744 | } | ||
| 745 | |||
| 746 | const groupKey = | ||
| 747 | location === "header" | ||
| 748 | ? "headers" | ||
| 749 | : location === "cookie" | ||
| 750 | ? "cookies" | ||
| 751 | : location; | ||
| 752 | const groupSchema = schemaFromField(schema, groupKey); | ||
| 753 | return groupSchema === undefined ? undefined : schemaFromField(groupSchema, name); | ||
| 754 | }; | ||
| 755 | |||
| 756 | const requestBodySchemaFromInput = (schema: unknown): unknown => | ||
| 757 | schemaFromField(schema, "body") ?? schemaFromField(schema, "input"); | ||
| 758 | |||
| 759 | const preferredResponseContentTypes = ( | ||
| 760 | mediaTypes: readonly string[] | undefined, | ||
| 761 | ): string[] => | ||
| 762 | mediaTypes && mediaTypes.length > 0 ? [...mediaTypes] : ["application/json"]; | ||
| 763 | |||
| 764 | const createServiceScope = (input: { | ||
| 765 | catalog: CatalogV1; | ||
| 766 | source: Source; | ||
| 767 | documentId: ReturnType<typeof DocumentIdSchema.make>; | ||
| 768 | }) => { | ||
| 769 | const scopeId = serviceScopeIdForSource(input.source); | ||
| 770 | mutableRecord(input.catalog.scopes)[scopeId] = { | ||
| 771 | id: scopeId, | ||
| 772 | kind: "service", | ||
| 773 | name: input.source.name, | ||
| 774 | namespace: input.source.namespace ?? namespaceFromSourceName(input.source.name), | ||
| 775 | docs: docsFrom({ | ||
| 776 | summary: input.source.name, | ||
| 777 | }), | ||
| 778 | synthetic: false, | ||
| 779 | provenance: provenanceFor(input.documentId, "#/service"), | ||
| 780 | } satisfies Scope; | ||
| 781 | return scopeId; | ||
| 782 | }; | ||
| 783 | |||
| 784 | const responseSetFromSingleResponse = (input: { | ||
| 785 | catalog: CatalogV1; | ||
| 786 | responseId: ReturnType<typeof ResponseSymbolIdSchema.make>; | ||
| 787 | provenance: ProvenanceRef[]; | ||
| 788 | traits?: ResponseSet["variants"][number]["traits"]; | ||
| 789 | }) => { | ||
| 790 | const responseSetId = ResponseSetIdSchema.make(`response_set_${stableHash({ | ||
| 791 | responseId: input.responseId, | ||
| 792 | traits: input.traits, | ||
| 793 | })}`); | ||
| 794 | mutableRecord(input.catalog.responseSets)[responseSetId] = { | ||
| 795 | id: responseSetId, | ||
| 796 | variants: [{ | ||
| 797 | match: { | ||
| 798 | kind: "range", | ||
| 799 | value: "2XX", | ||
| 800 | }, | ||
| 801 | responseId: input.responseId, | ||
| 802 | ...(input.traits && input.traits.length > 0 ? { traits: input.traits } : {}), | ||
| 803 | }], | ||
| 804 | synthetic: false, | ||
| 805 | provenance: input.provenance, | ||
| 806 | } satisfies ResponseSet; | ||
| 807 | return responseSetId; | ||
| 808 | }; | ||
| 809 | |||
| 810 | const createHttpCapabilityFromOpenApi = (input: { | ||
| 811 | catalog: CatalogV1; | ||
| 812 | source: Source; | ||
| 813 | documentId: ReturnType<typeof DocumentIdSchema.make>; | ||
| 814 | serviceScopeId: ReturnType<typeof ScopeIdSchema.make>; | ||
| 815 | operation: OpenApiCatalogOperationInput; | ||
| 816 | importer: JsonSchemaImporter; | ||
| 817 | }) => { | ||
| 818 | const toolPath = toolPathSegments(input.source, input.operation.providerData.toolId); | ||
| 819 | const capabilityId = CapabilityIdSchema.make(`cap_${stableHash({ | ||
| 820 | sourceId: input.source.id, | ||
| 821 | toolId: input.operation.providerData.toolId, | ||
| 822 | })}`); | ||
| 823 | const executableId = ExecutableIdSchema.make(`exec_${stableHash({ | ||
| 824 | sourceId: input.source.id, | ||
| 825 | toolId: input.operation.providerData.toolId, | ||
| 826 | protocol: "http", | ||
| 827 | })}`); | ||
| 828 | const inputSchema = input.operation.inputSchema ?? {}; | ||
| 829 | const outputSchema = input.operation.outputSchema ?? {}; | ||
| 830 | const exampleIds: Array<ReturnType<typeof ExampleSymbolIdSchema.make>> = []; | ||
| 831 | |||
| 832 | const parameterIds = input.operation.providerData.invocation.parameters.map((parameter) => { | ||
| 833 | const parameterId = ParameterSymbolIdSchema.make(`param_${stableHash({ | ||
| 834 | capabilityId, | ||
| 835 | location: parameter.location, | ||
| 836 | name: parameter.name, | ||
| 837 | })}`); | ||
| 838 | const parameterSchema = groupedSchemaForParameter(inputSchema, parameter.location, parameter.name); | ||
| 839 | const matchingDocs = input.operation.providerData.documentation?.parameters.find((candidate) => | ||
| 840 | candidate.name === parameter.name && candidate.location === parameter.location | ||
| 841 | ); | ||
| 842 | const parameterExampleIds = (matchingDocs?.examples ?? []).map((example, index) => { | ||
| 843 | const parsed = JSON.parse(example.valueJson) as unknown; | ||
| 844 | return exampleSymbolFromValue({ | ||
| 845 | catalog: input.catalog, | ||
| 846 | source: input.source, | ||
| 847 | documentId: input.documentId, | ||
| 848 | pointer: `#/openapi/${input.operation.providerData.toolId}/parameter/${parameter.location}/${parameter.name}/example/${index}`, | ||
| 849 | name: example.label, | ||
| 850 | summary: example.label, | ||
| 851 | value: parsed, | ||
| 852 | }); | ||
| 853 | }); | ||
| 854 | exampleIds.push(...parameterExampleIds); | ||
| 855 | mutableRecord(input.catalog.symbols)[parameterId] = { | ||
| 856 | id: parameterId, | ||
| 857 | kind: "parameter", | ||
| 858 | name: parameter.name, | ||
| 859 | location: parameter.location, | ||
| 860 | required: parameter.required, | ||
| 861 | ...(docsFrom({ | ||
| 862 | description: matchingDocs?.description ?? null, | ||
| 863 | }) | ||
| 864 | ? { | ||
| 865 | docs: docsFrom({ | ||
| 866 | description: matchingDocs?.description ?? null, | ||
| 867 | })!, | ||
| 868 | } | ||
| 869 | : {}), | ||
| 870 | ...(parameterSchema !== undefined | ||
| 871 | ? { | ||
| 872 | schemaShapeId: input.importer.importSchema( | ||
| 873 | parameterSchema, | ||
| 874 | `#/openapi/${input.operation.providerData.toolId}/parameter/${parameter.location}/${parameter.name}`, | ||
| 875 | ), | ||
| 876 | } | ||
| 877 | : {}), | ||
| 878 | ...(parameterExampleIds.length > 0 ? { exampleIds: parameterExampleIds } : {}), | ||
| 879 | synthetic: false, | ||
| 880 | provenance: provenanceFor( | ||
| 881 | input.documentId, | ||
| 882 | `#/openapi/${input.operation.providerData.toolId}/parameter/${parameter.location}/${parameter.name}`, | ||
| 883 | ), | ||
| 884 | } satisfies ParameterSymbol; | ||
| 885 | return parameterId; | ||
| 886 | }); | ||
| 887 | |||
| 888 | const requestBodyId = input.operation.providerData.invocation.requestBody | ||
| 889 | ? RequestBodySymbolIdSchema.make(`request_body_${stableHash({ capabilityId })}`) | ||
| 890 | : undefined; | ||
| 891 | |||
| 892 | if (requestBodyId) { | ||
| 893 | const requestBodySchema = requestBodySchemaFromInput(inputSchema); | ||
| 894 | const requestBodyExampleIds = (input.operation.providerData.documentation?.requestBody?.examples ?? []).map( | ||
| 895 | (example, index) => | ||
| 896 | exampleSymbolFromValue({ | ||
| 897 | catalog: input.catalog, | ||
| 898 | source: input.source, | ||
| 899 | documentId: input.documentId, | ||
| 900 | pointer: `#/openapi/${input.operation.providerData.toolId}/requestBody/example/${index}`, | ||
| 901 | name: example.label, | ||
| 902 | summary: example.label, | ||
| 903 | value: JSON.parse(example.valueJson) as unknown, | ||
| 904 | }), | ||
| 905 | ); | ||
| 906 | exampleIds.push(...requestBodyExampleIds); | ||
| 907 | const contents: ContentSpec[] = preferredResponseContentTypes( | ||
| 908 | input.operation.providerData.invocation.requestBody?.contentTypes, | ||
| 909 | ).map((mediaType) => ({ | ||
| 910 | mediaType, | ||
| 911 | ...(requestBodySchema !== undefined | ||
| 912 | ? { | ||
| 913 | shapeId: input.importer.importSchema( | ||
| 914 | requestBodySchema, | ||
| 915 | `#/openapi/${input.operation.providerData.toolId}/requestBody`, | ||
| 916 | ), | ||
| 917 | } | ||
| 918 | : {}), | ||
| 919 | ...(requestBodyExampleIds.length > 0 ? { exampleIds: requestBodyExampleIds } : {}), | ||
| 920 | })); | ||
| 921 | |||
| 922 | mutableRecord(input.catalog.symbols)[requestBodyId] = { | ||
| 923 | id: requestBodyId, | ||
| 924 | kind: "requestBody", | ||
| 925 | ...(docsFrom({ | ||
| 926 | description: input.operation.providerData.documentation?.requestBody?.description ?? null, | ||
| 927 | }) | ||
| 928 | ? { | ||
| 929 | docs: docsFrom({ | ||
| 930 | description: input.operation.providerData.documentation?.requestBody?.description ?? null, | ||
| 931 | })!, | ||
| 932 | } | ||
| 933 | : {}), | ||
| 934 | required: input.operation.providerData.invocation.requestBody?.required ?? false, | ||
| 935 | contents, | ||
| 936 | synthetic: false, | ||
| 937 | provenance: provenanceFor(input.documentId, `#/openapi/${input.operation.providerData.toolId}/requestBody`), | ||
| 938 | }; | ||
| 939 | } | ||
| 940 | |||
| 941 | const responseId = ResponseSymbolIdSchema.make(`response_${stableHash({ capabilityId })}`); | ||
| 942 | const responseExampleIds = (input.operation.providerData.documentation?.response?.examples ?? []).map((example, index) => | ||
| 943 | exampleSymbolFromValue({ | ||
| 944 | catalog: input.catalog, | ||
| 945 | source: input.source, | ||
| 946 | documentId: input.documentId, | ||
| 947 | pointer: `#/openapi/${input.operation.providerData.toolId}/response/example/${index}`, | ||
| 948 | name: example.label, | ||
| 949 | summary: example.label, | ||
| 950 | value: JSON.parse(example.valueJson) as unknown, | ||
| 951 | }) | ||
| 952 | ); | ||
| 953 | exampleIds.push(...responseExampleIds); | ||
| 954 | mutableRecord(input.catalog.symbols)[responseId] = { | ||
| 955 | id: responseId, | ||
| 956 | kind: "response", | ||
| 957 | ...(docsFrom({ | ||
| 958 | description: | ||
| 959 | input.operation.providerData.documentation?.response?.description | ||
| 960 | ?? input.operation.description, | ||
| 961 | }) | ||
| 962 | ? { | ||
| 963 | docs: docsFrom({ | ||
| 964 | description: | ||
| 965 | input.operation.providerData.documentation?.response?.description | ||
| 966 | ?? input.operation.description, | ||
| 967 | })!, | ||
| 968 | } | ||
| 969 | : {}), | ||
| 970 | contents: [{ | ||
| 971 | mediaType: preferredResponseContentTypes(input.operation.providerData.documentation?.response?.contentTypes)[0] ?? "application/json", | ||
| 972 | ...(input.operation.outputSchema !== undefined | ||
| 973 | ? { | ||
| 974 | shapeId: input.importer.importSchema( | ||
| 975 | outputSchema, | ||
| 976 | `#/openapi/${input.operation.providerData.toolId}/response`, | ||
| 977 | ), | ||
| 978 | } | ||
| 979 | : {}), | ||
| 980 | ...(responseExampleIds.length > 0 ? { exampleIds: responseExampleIds } : {}), | ||
| 981 | }], | ||
| 982 | synthetic: false, | ||
| 983 | provenance: provenanceFor(input.documentId, `#/openapi/${input.operation.providerData.toolId}/response`), | ||
| 984 | } satisfies ResponseSymbol; | ||
| 985 | |||
| 986 | const responseSetId = responseSetFromSingleResponse({ | ||
| 987 | catalog: input.catalog, | ||
| 988 | responseId, | ||
| 989 | provenance: provenanceFor(input.documentId, `#/openapi/${input.operation.providerData.toolId}/responseSet`), | ||
| 990 | }); | ||
| 991 | |||
| 992 | const executable: HttpExecutable = { | ||
| 993 | id: executableId, | ||
| 994 | protocol: "http", | ||
| 995 | capabilityId, | ||
| 996 | scopeId: input.serviceScopeId, | ||
| 997 | method: input.operation.providerData.invocation.method.toUpperCase(), | ||
| 998 | pathTemplate: input.operation.providerData.invocation.pathTemplate, | ||
| 999 | pathParameterIds: parameterIds.filter((parameterId) => { | ||
| 1000 | const parameter = input.catalog.symbols[parameterId] as ParameterSymbol | undefined; | ||
| 1001 | return parameter?.location === "path"; | ||
| 1002 | }), | ||
| 1003 | queryParameterIds: parameterIds.filter((parameterId) => { | ||
| 1004 | const parameter = input.catalog.symbols[parameterId] as ParameterSymbol | undefined; | ||
| 1005 | return parameter?.location === "query"; | ||
| 1006 | }), | ||
| 1007 | headerParameterIds: parameterIds.filter((parameterId) => { | ||
| 1008 | const parameter = input.catalog.symbols[parameterId] as ParameterSymbol | undefined; | ||
| 1009 | return parameter?.location === "header"; | ||
| 1010 | }), | ||
| 1011 | cookieParameterIds: parameterIds.filter((parameterId) => { | ||
| 1012 | const parameter = input.catalog.symbols[parameterId] as ParameterSymbol | undefined; | ||
| 1013 | return parameter?.location === "cookie"; | ||
| 1014 | }), | ||
| 1015 | ...(requestBodyId ? { requestBodyId } : {}), | ||
| 1016 | responseSetId, | ||
| 1017 | synthetic: false, | ||
| 1018 | provenance: provenanceFor(input.documentId, `#/openapi/${input.operation.providerData.toolId}/executable`), | ||
| 1019 | native: [nativeBlob({ | ||
| 1020 | source: input.source, | ||
| 1021 | kind: "openapi_provider_data", | ||
| 1022 | pointer: `#/openapi/${input.operation.providerData.toolId}/providerData`, | ||
| 1023 | value: input.operation.providerData, | ||
| 1024 | })], | ||
| 1025 | }; | ||
| 1026 | mutableRecord(input.catalog.executables)[executableId] = executable; | ||
| 1027 | |||
| 1028 | const effect = input.operation.effect; | ||
| 1029 | mutableRecord(input.catalog.capabilities)[capabilityId] = { | ||
| 1030 | id: capabilityId, | ||
| 1031 | serviceScopeId: input.serviceScopeId, | ||
| 1032 | surface: { | ||
| 1033 | toolPath, | ||
| 1034 | ...(input.operation.title ? { title: input.operation.title } : {}), | ||
| 1035 | ...(input.operation.description ? { summary: input.operation.description } : {}), | ||
| 1036 | ...(input.operation.providerData.tags.length > 0 ? { tags: input.operation.providerData.tags } : {}), | ||
| 1037 | }, | ||
| 1038 | semantics: { | ||
| 1039 | effect, | ||
| 1040 | safe: effect === "read", | ||
| 1041 | idempotent: effect === "read" || effect === "delete", | ||
| 1042 | destructive: effect === "delete", | ||
| 1043 | }, | ||
| 1044 | auth: { kind: "none" }, | ||
| 1045 | interaction: interactionForEffect(effect), | ||
| 1046 | executableIds: [executableId], | ||
| 1047 | ...(exampleIds.length > 0 ? { exampleIds } : {}), | ||
| 1048 | synthetic: false, | ||
| 1049 | provenance: provenanceFor(input.documentId, `#/openapi/${input.operation.providerData.toolId}/capability`), | ||
| 1050 | native: [nativeBlob({ | ||
| 1051 | source: input.source, | ||
| 1052 | kind: "openapi_documentation", | ||
| 1053 | pointer: `#/openapi/${input.operation.providerData.toolId}/documentation`, | ||
| 1054 | value: input.operation.providerData.documentation ?? null, | ||
| 1055 | })], | ||
| 1056 | } satisfies Capability; | ||
| 1057 | }; | ||
| 1058 | |||
| 1059 | const createHttpCapabilityFromGoogleDiscovery = (input: { | ||
| 1060 | catalog: CatalogV1; | ||
| 1061 | source: Source; | ||
| 1062 | documentId: ReturnType<typeof DocumentIdSchema.make>; | ||
| 1063 | serviceScopeId: ReturnType<typeof ScopeIdSchema.make>; | ||
| 1064 | operation: GoogleDiscoveryCatalogOperationInput; | ||
| 1065 | importer: JsonSchemaImporter; | ||
| 1066 | }) => { | ||
| 1067 | const toolPath = toolPathSegments(input.source, input.operation.providerData.toolId); | ||
| 1068 | const capabilityId = CapabilityIdSchema.make(`cap_${stableHash({ | ||
| 1069 | sourceId: input.source.id, | ||
| 1070 | toolId: input.operation.providerData.toolId, | ||
| 1071 | })}`); | ||
| 1072 | const executableId = ExecutableIdSchema.make(`exec_${stableHash({ | ||
| 1073 | sourceId: input.source.id, | ||
| 1074 | toolId: input.operation.providerData.toolId, | ||
| 1075 | protocol: "http", | ||
| 1076 | })}`); | ||
| 1077 | const inputSchema = input.operation.inputSchema ?? {}; | ||
| 1078 | const outputSchema = input.operation.outputSchema ?? {}; | ||
| 1079 | |||
| 1080 | const authSchemeId = input.operation.providerData.invocation.scopes.length > 0 | ||
| 1081 | ? SecuritySchemeSymbolIdSchema.make(`security_${stableHash({ | ||
| 1082 | sourceId: input.source.id, | ||
| 1083 | scopes: input.operation.providerData.invocation.scopes, | ||
| 1084 | })}`) | ||
| 1085 | : undefined; | ||
| 1086 | |||
| 1087 | if (authSchemeId && !input.catalog.symbols[authSchemeId]) { | ||
| 1088 | mutableRecord(input.catalog.symbols)[authSchemeId] = { | ||
| 1089 | id: authSchemeId, | ||
| 1090 | kind: "securityScheme", | ||
| 1091 | schemeType: "oauth2", | ||
| 1092 | docs: docsFrom({ | ||
| 1093 | summary: "OAuth 2.0", | ||
| 1094 | description: "Imported from Google Discovery scopes.", | ||
| 1095 | }), | ||
| 1096 | oauth: { | ||
| 1097 | flows: {}, | ||
| 1098 | scopes: Object.fromEntries(input.operation.providerData.invocation.scopes.map((scope) => [scope, scope])), | ||
| 1099 | }, | ||
| 1100 | synthetic: false, | ||
| 1101 | provenance: provenanceFor(input.documentId, "#/googleDiscovery/security"), | ||
| 1102 | } satisfies SecuritySchemeSymbol; | ||
| 1103 | } | ||
| 1104 | |||
| 1105 | const parameterIds = input.operation.providerData.invocation.parameters.map((parameter) => { | ||
| 1106 | const parameterId = ParameterSymbolIdSchema.make(`param_${stableHash({ | ||
| 1107 | capabilityId, | ||
| 1108 | location: parameter.location, | ||
| 1109 | name: parameter.name, | ||
| 1110 | })}`); | ||
| 1111 | const parameterSchema = groupedSchemaForParameter(inputSchema, parameter.location, parameter.name) | ||
| 1112 | ?? (parameter.repeated | ||
| 1113 | ? { | ||
| 1114 | type: "array", | ||
| 1115 | items: { | ||
| 1116 | type: parameter.type === "integer" ? "integer" : "string", | ||
| 1117 | ...(parameter.enum ? { enum: parameter.enum } : {}), | ||
| 1118 | }, | ||
| 1119 | } | ||
| 1120 | : { | ||
| 1121 | type: parameter.type === "integer" ? "integer" : "string", | ||
| 1122 | ...(parameter.enum ? { enum: parameter.enum } : {}), | ||
| 1123 | }); | ||
| 1124 | |||
| 1125 | mutableRecord(input.catalog.symbols)[parameterId] = { | ||
| 1126 | id: parameterId, | ||
| 1127 | kind: "parameter", | ||
| 1128 | name: parameter.name, | ||
| 1129 | location: parameter.location, | ||
| 1130 | required: parameter.required, | ||
| 1131 | ...(docsFrom({ | ||
| 1132 | description: parameter.description, | ||
| 1133 | }) | ||
| 1134 | ? { | ||
| 1135 | docs: docsFrom({ | ||
| 1136 | description: parameter.description, | ||
| 1137 | })!, | ||
| 1138 | } | ||
| 1139 | : {}), | ||
| 1140 | schemaShapeId: input.importer.importSchema( | ||
| 1141 | parameterSchema, | ||
| 1142 | `#/googleDiscovery/${input.operation.providerData.toolId}/parameter/${parameter.location}/${parameter.name}`, | ||
| 1143 | ), | ||
| 1144 | synthetic: false, | ||
| 1145 | provenance: provenanceFor( | ||
| 1146 | input.documentId, | ||
| 1147 | `#/googleDiscovery/${input.operation.providerData.toolId}/parameter/${parameter.location}/${parameter.name}`, | ||
| 1148 | ), | ||
| 1149 | } satisfies ParameterSymbol; | ||
| 1150 | return parameterId; | ||
| 1151 | }); | ||
| 1152 | |||
| 1153 | const requestBodyId = input.operation.providerData.invocation.requestSchemaId || requestBodySchemaFromInput(inputSchema) !== undefined | ||
| 1154 | ? RequestBodySymbolIdSchema.make(`request_body_${stableHash({ capabilityId })}`) | ||
| 1155 | : undefined; | ||
| 1156 | |||
| 1157 | if (requestBodyId) { | ||
| 1158 | const requestBodySchema = requestBodySchemaFromInput(inputSchema) ?? inputSchema; | ||
| 1159 | mutableRecord(input.catalog.symbols)[requestBodyId] = { | ||
| 1160 | id: requestBodyId, | ||
| 1161 | kind: "requestBody", | ||
| 1162 | contents: [{ | ||
| 1163 | mediaType: "application/json", | ||
| 1164 | shapeId: input.importer.importSchema( | ||
| 1165 | requestBodySchema, | ||
| 1166 | `#/googleDiscovery/${input.operation.providerData.toolId}/requestBody`, | ||
| 1167 | ), | ||
| 1168 | }], | ||
| 1169 | synthetic: false, | ||
| 1170 | provenance: provenanceFor(input.documentId, `#/googleDiscovery/${input.operation.providerData.toolId}/requestBody`), | ||
| 1171 | }; | ||
| 1172 | } | ||
| 1173 | |||
| 1174 | const responseId = ResponseSymbolIdSchema.make(`response_${stableHash({ capabilityId })}`); | ||
| 1175 | mutableRecord(input.catalog.symbols)[responseId] = { | ||
| 1176 | id: responseId, | ||
| 1177 | kind: "response", | ||
| 1178 | ...(docsFrom({ | ||
| 1179 | description: input.operation.description, | ||
| 1180 | }) | ||
| 1181 | ? { | ||
| 1182 | docs: docsFrom({ | ||
| 1183 | description: input.operation.description, | ||
| 1184 | })!, | ||
| 1185 | } | ||
| 1186 | : {}), | ||
| 1187 | ...(input.operation.outputSchema !== undefined | ||
| 1188 | ? { | ||
| 1189 | contents: [{ | ||
| 1190 | mediaType: "application/json", | ||
| 1191 | shapeId: input.importer.importSchema( | ||
| 1192 | outputSchema, | ||
| 1193 | `#/googleDiscovery/${input.operation.providerData.toolId}/response`, | ||
| 1194 | ), | ||
| 1195 | }], | ||
| 1196 | } | ||
| 1197 | : {}), | ||
| 1198 | synthetic: false, | ||
| 1199 | provenance: provenanceFor(input.documentId, `#/googleDiscovery/${input.operation.providerData.toolId}/response`), | ||
| 1200 | } satisfies ResponseSymbol; | ||
| 1201 | |||
| 1202 | const traits: Array<"upload" | "download"> = []; | ||
| 1203 | if (input.operation.providerData.invocation.supportsMediaUpload) { | ||
| 1204 | traits.push("upload"); | ||
| 1205 | } | ||
| 1206 | if (input.operation.providerData.invocation.supportsMediaDownload) { | ||
| 1207 | traits.push("download"); | ||
| 1208 | } | ||
| 1209 | |||
| 1210 | const responseSetId = responseSetFromSingleResponse({ | ||
| 1211 | catalog: input.catalog, | ||
| 1212 | responseId, | ||
| 1213 | provenance: provenanceFor(input.documentId, `#/googleDiscovery/${input.operation.providerData.toolId}/responseSet`), | ||
| 1214 | traits, | ||
| 1215 | }); | ||
| 1216 | |||
| 1217 | mutableRecord(input.catalog.executables)[executableId] = { | ||
| 1218 | id: executableId, | ||
| 1219 | protocol: "http", | ||
| 1220 | capabilityId, | ||
| 1221 | scopeId: input.serviceScopeId, | ||
| 1222 | method: input.operation.providerData.invocation.method.toUpperCase(), | ||
| 1223 | pathTemplate: input.operation.providerData.invocation.path, | ||
| 1224 | pathParameterIds: parameterIds.filter((parameterId) => (input.catalog.symbols[parameterId] as ParameterSymbol | undefined)?.location === "path"), | ||
| 1225 | queryParameterIds: parameterIds.filter((parameterId) => (input.catalog.symbols[parameterId] as ParameterSymbol | undefined)?.location === "query"), | ||
| 1226 | headerParameterIds: parameterIds.filter((parameterId) => (input.catalog.symbols[parameterId] as ParameterSymbol | undefined)?.location === "header"), | ||
| 1227 | ...(requestBodyId ? { requestBodyId } : {}), | ||
| 1228 | responseSetId, | ||
| 1229 | synthetic: false, | ||
| 1230 | provenance: provenanceFor(input.documentId, `#/googleDiscovery/${input.operation.providerData.toolId}/executable`), | ||
| 1231 | native: [nativeBlob({ | ||
| 1232 | source: input.source, | ||
| 1233 | kind: "google_discovery_provider_data", | ||
| 1234 | pointer: `#/googleDiscovery/${input.operation.providerData.toolId}/providerData`, | ||
| 1235 | value: input.operation.providerData, | ||
| 1236 | })], | ||
| 1237 | } satisfies HttpExecutable; | ||
| 1238 | |||
| 1239 | const effect = input.operation.effect; | ||
| 1240 | const auth: AuthRequirement = | ||
| 1241 | authSchemeId | ||
| 1242 | ? { | ||
| 1243 | kind: "scheme", | ||
| 1244 | schemeId: authSchemeId, | ||
| 1245 | scopes: input.operation.providerData.invocation.scopes, | ||
| 1246 | } | ||
| 1247 | : { kind: "none" }; | ||
| 1248 | |||
| 1249 | mutableRecord(input.catalog.capabilities)[capabilityId] = { | ||
| 1250 | id: capabilityId, | ||
| 1251 | serviceScopeId: input.serviceScopeId, | ||
| 1252 | surface: { | ||
| 1253 | toolPath, | ||
| 1254 | ...(input.operation.title ? { title: input.operation.title } : {}), | ||
| 1255 | ...(input.operation.description ? { summary: input.operation.description } : {}), | ||
| 1256 | tags: ["google", input.operation.providerData.service, input.operation.providerData.version], | ||
| 1257 | }, | ||
| 1258 | semantics: { | ||
| 1259 | effect, | ||
| 1260 | safe: effect === "read", | ||
| 1261 | idempotent: effect === "read" || effect === "delete", | ||
| 1262 | destructive: effect === "delete", | ||
| 1263 | }, | ||
| 1264 | auth, | ||
| 1265 | interaction: interactionForEffect(effect), | ||
| 1266 | executableIds: [executableId], | ||
| 1267 | synthetic: false, | ||
| 1268 | provenance: provenanceFor(input.documentId, `#/googleDiscovery/${input.operation.providerData.toolId}/capability`), | ||
| 1269 | native: [nativeBlob({ | ||
| 1270 | source: input.source, | ||
| 1271 | kind: "google_discovery_provider_data", | ||
| 1272 | pointer: `#/googleDiscovery/${input.operation.providerData.toolId}/providerData`, | ||
| 1273 | value: input.operation.providerData, | ||
| 1274 | })], | ||
| 1275 | } satisfies Capability; | ||
| 1276 | }; | ||
| 1277 | |||
| 1278 | const createGraphqlCapability = (input: { | ||
| 1279 | catalog: CatalogV1; | ||
| 1280 | source: Source; | ||
| 1281 | documentId: ReturnType<typeof DocumentIdSchema.make>; | ||
| 1282 | serviceScopeId: ReturnType<typeof ScopeIdSchema.make>; | ||
| 1283 | operation: GraphqlCatalogOperationInput; | ||
| 1284 | importer: JsonSchemaImporter; | ||
| 1285 | }) => { | ||
| 1286 | const toolPath = toolPathSegments(input.source, input.operation.providerData.toolId); | ||
| 1287 | const capabilityId = CapabilityIdSchema.make(`cap_${stableHash({ | ||
| 1288 | sourceId: input.source.id, | ||
| 1289 | toolId: input.operation.providerData.toolId, | ||
| 1290 | })}`); | ||
| 1291 | const executableId = ExecutableIdSchema.make(`exec_${stableHash({ | ||
| 1292 | sourceId: input.source.id, | ||
| 1293 | toolId: input.operation.providerData.toolId, | ||
| 1294 | protocol: "graphql", | ||
| 1295 | })}`); | ||
| 1296 | const argumentShapeId = input.operation.inputSchema !== undefined | ||
| 1297 | ? input.importer.importSchema( | ||
| 1298 | input.operation.inputSchema, | ||
| 1299 | `#/graphql/${input.operation.providerData.toolId}/input`, | ||
| 1300 | ) | ||
| 1301 | : input.importer.importSchema( | ||
| 1302 | { | ||
| 1303 | type: "object", | ||
| 1304 | additionalProperties: true, | ||
| 1305 | }, | ||
| 1306 | `#/graphql/${input.operation.providerData.toolId}/input`, | ||
| 1307 | ); | ||
| 1308 | const resultShapeId = input.operation.outputSchema !== undefined | ||
| 1309 | ? input.importer.importSchema( | ||
| 1310 | input.operation.outputSchema, | ||
| 1311 | `#/graphql/${input.operation.providerData.toolId}/output`, | ||
| 1312 | ) | ||
| 1313 | : input.importer.importSchema( | ||
| 1314 | { | ||
| 1315 | type: "object", | ||
| 1316 | additionalProperties: true, | ||
| 1317 | }, | ||
| 1318 | `#/graphql/${input.operation.providerData.toolId}/output`, | ||
| 1319 | ); | ||
| 1320 | |||
| 1321 | const responseId = ResponseSymbolIdSchema.make(`response_${stableHash({ capabilityId })}`); | ||
| 1322 | mutableRecord(input.catalog.symbols)[responseId] = { | ||
| 1323 | id: responseId, | ||
| 1324 | kind: "response", | ||
| 1325 | ...(docsFrom({ | ||
| 1326 | description: input.operation.description, | ||
| 1327 | }) | ||
| 1328 | ? { | ||
| 1329 | docs: docsFrom({ | ||
| 1330 | description: input.operation.description, | ||
| 1331 | })!, | ||
| 1332 | } | ||
| 1333 | : {}), | ||
| 1334 | contents: [{ | ||
| 1335 | mediaType: "application/json", | ||
| 1336 | shapeId: resultShapeId, | ||
| 1337 | }], | ||
| 1338 | synthetic: false, | ||
| 1339 | provenance: provenanceFor(input.documentId, `#/graphql/${input.operation.providerData.toolId}/response`), | ||
| 1340 | } satisfies ResponseSymbol; | ||
| 1341 | |||
| 1342 | const responseSetId = responseSetFromSingleResponse({ | ||
| 1343 | catalog: input.catalog, | ||
| 1344 | responseId, | ||
| 1345 | provenance: provenanceFor(input.documentId, `#/graphql/${input.operation.providerData.toolId}/responseSet`), | ||
| 1346 | }); | ||
| 1347 | |||
| 1348 | mutableRecord(input.catalog.executables)[executableId] = { | ||
| 1349 | id: executableId, | ||
| 1350 | protocol: "graphql", | ||
| 1351 | capabilityId, | ||
| 1352 | scopeId: input.serviceScopeId, | ||
| 1353 | operationType: input.operation.providerData.operationType ?? "query", | ||
| 1354 | rootField: | ||
| 1355 | input.operation.providerData.fieldName | ||
| 1356 | ?? input.operation.providerData.leaf | ||
| 1357 | ?? input.operation.providerData.toolId, | ||
| 1358 | argumentShapeId, | ||
| 1359 | resultShapeId, | ||
| 1360 | selectionMode: "fixed", | ||
| 1361 | responseSetId, | ||
| 1362 | synthetic: false, | ||
| 1363 | provenance: provenanceFor(input.documentId, `#/graphql/${input.operation.providerData.toolId}/executable`), | ||
| 1364 | native: [nativeBlob({ | ||
| 1365 | source: input.source, | ||
| 1366 | kind: "graphql_provider_data", | ||
| 1367 | pointer: `#/graphql/${input.operation.providerData.toolId}/providerData`, | ||
| 1368 | value: input.operation.providerData, | ||
| 1369 | })], | ||
| 1370 | } satisfies GraphQLExecutable; | ||
| 1371 | |||
| 1372 | const effect = input.operation.effect; | ||
| 1373 | |||
| 1374 | mutableRecord(input.catalog.capabilities)[capabilityId] = { | ||
| 1375 | id: capabilityId, | ||
| 1376 | serviceScopeId: input.serviceScopeId, | ||
| 1377 | surface: { | ||
| 1378 | toolPath, | ||
| 1379 | ...(input.operation.title ? { title: input.operation.title } : {}), | ||
| 1380 | ...(input.operation.description ? { summary: input.operation.description } : {}), | ||
| 1381 | ...(input.operation.providerData.group ? { tags: [input.operation.providerData.group] } : {}), | ||
| 1382 | }, | ||
| 1383 | semantics: { | ||
| 1384 | effect, | ||
| 1385 | safe: effect === "read", | ||
| 1386 | idempotent: effect === "read", | ||
| 1387 | destructive: false, | ||
| 1388 | }, | ||
| 1389 | auth: { kind: "none" }, | ||
| 1390 | interaction: interactionForEffect(effect), | ||
| 1391 | executableIds: [executableId], | ||
| 1392 | synthetic: false, | ||
| 1393 | provenance: provenanceFor(input.documentId, `#/graphql/${input.operation.providerData.toolId}/capability`), | ||
| 1394 | native: [nativeBlob({ | ||
| 1395 | source: input.source, | ||
| 1396 | kind: "graphql_provider_data", | ||
| 1397 | pointer: `#/graphql/${input.operation.providerData.toolId}/providerData`, | ||
| 1398 | value: input.operation.providerData, | ||
| 1399 | })], | ||
| 1400 | } satisfies Capability; | ||
| 1401 | }; | ||
| 1402 | |||
| 1403 | const createMcpCapability = (input: { | ||
| 1404 | catalog: CatalogV1; | ||
| 1405 | source: Source; | ||
| 1406 | documentId: ReturnType<typeof DocumentIdSchema.make>; | ||
| 1407 | serviceScopeId: ReturnType<typeof ScopeIdSchema.make>; | ||
| 1408 | operation: McpCatalogOperationInput; | ||
| 1409 | importer: JsonSchemaImporter; | ||
| 1410 | }) => { | ||
| 1411 | const toolPath = toolPathSegments(input.source, input.operation.providerData.toolId); | ||
| 1412 | const capabilityId = CapabilityIdSchema.make(`cap_${stableHash({ | ||
| 1413 | sourceId: input.source.id, | ||
| 1414 | toolId: input.operation.providerData.toolId, | ||
| 1415 | })}`); | ||
| 1416 | const executableId = ExecutableIdSchema.make(`exec_${stableHash({ | ||
| 1417 | sourceId: input.source.id, | ||
| 1418 | toolId: input.operation.providerData.toolId, | ||
| 1419 | protocol: "mcp", | ||
| 1420 | })}`); | ||
| 1421 | const inputShapeId = input.operation.inputSchema !== undefined | ||
| 1422 | ? input.importer.importSchema( | ||
| 1423 | input.operation.inputSchema, | ||
| 1424 | `#/mcp/${input.operation.providerData.toolId}/input`, | ||
| 1425 | ) | ||
| 1426 | : undefined; | ||
| 1427 | const outputShapeId = input.operation.outputSchema !== undefined | ||
| 1428 | ? input.importer.importSchema( | ||
| 1429 | input.operation.outputSchema, | ||
| 1430 | `#/mcp/${input.operation.providerData.toolId}/output`, | ||
| 1431 | ) | ||
| 1432 | : undefined; | ||
| 1433 | |||
| 1434 | const responseId = ResponseSymbolIdSchema.make(`response_${stableHash({ capabilityId })}`); | ||
| 1435 | mutableRecord(input.catalog.symbols)[responseId] = { | ||
| 1436 | id: responseId, | ||
| 1437 | kind: "response", | ||
| 1438 | ...(docsFrom({ | ||
| 1439 | description: input.operation.providerData.description ?? input.operation.description, | ||
| 1440 | }) | ||
| 1441 | ? { | ||
| 1442 | docs: docsFrom({ | ||
| 1443 | description: input.operation.providerData.description ?? input.operation.description, | ||
| 1444 | })!, | ||
| 1445 | } | ||
| 1446 | : {}), | ||
| 1447 | ...(outputShapeId | ||
| 1448 | ? { | ||
| 1449 | contents: [{ | ||
| 1450 | mediaType: "application/json", | ||
| 1451 | shapeId: outputShapeId, | ||
| 1452 | }], | ||
| 1453 | } | ||
| 1454 | : {}), | ||
| 1455 | synthetic: false, | ||
| 1456 | provenance: provenanceFor(input.documentId, `#/mcp/${input.operation.providerData.toolId}/response`), | ||
| 1457 | } satisfies ResponseSymbol; | ||
| 1458 | const responseSetId = responseSetFromSingleResponse({ | ||
| 1459 | catalog: input.catalog, | ||
| 1460 | responseId, | ||
| 1461 | provenance: provenanceFor(input.documentId, `#/mcp/${input.operation.providerData.toolId}/responseSet`), | ||
| 1462 | }); | ||
| 1463 | |||
| 1464 | mutableRecord(input.catalog.executables)[executableId] = { | ||
| 1465 | id: executableId, | ||
| 1466 | protocol: "mcp", | ||
| 1467 | capabilityId, | ||
| 1468 | scopeId: input.serviceScopeId, | ||
| 1469 | serverRef: input.source.endpoint, | ||
| 1470 | toolName: input.operation.providerData.toolName, | ||
| 1471 | ...(inputShapeId ? { inputShapeId } : {}), | ||
| 1472 | ...(outputShapeId ? { outputShapeId } : {}), | ||
| 1473 | responseSetId, | ||
| 1474 | synthetic: false, | ||
| 1475 | provenance: provenanceFor(input.documentId, `#/mcp/${input.operation.providerData.toolId}/executable`), | ||
| 1476 | native: [nativeBlob({ | ||
| 1477 | source: input.source, | ||
| 1478 | kind: "mcp_provider_data", | ||
| 1479 | pointer: `#/mcp/${input.operation.providerData.toolId}/providerData`, | ||
| 1480 | value: input.operation.providerData, | ||
| 1481 | })], | ||
| 1482 | } satisfies McpExecutable; | ||
| 1483 | |||
| 1484 | mutableRecord(input.catalog.capabilities)[capabilityId] = { | ||
| 1485 | id: capabilityId, | ||
| 1486 | serviceScopeId: input.serviceScopeId, | ||
| 1487 | surface: { | ||
| 1488 | toolPath, | ||
| 1489 | title: input.operation.providerData.toolName, | ||
| 1490 | ...(input.operation.providerData.description ? { summary: input.operation.providerData.description } : {}), | ||
| 1491 | }, | ||
| 1492 | semantics: { | ||
| 1493 | effect: input.operation.effect, | ||
| 1494 | safe: input.operation.effect === "read", | ||
| 1495 | idempotent: input.operation.effect === "read", | ||
| 1496 | destructive: false, | ||
| 1497 | }, | ||
| 1498 | auth: { kind: "none" }, | ||
| 1499 | interaction: interactionForEffect(input.operation.effect), | ||
| 1500 | executableIds: [executableId], | ||
| 1501 | synthetic: false, | ||
| 1502 | provenance: provenanceFor(input.documentId, `#/mcp/${input.operation.providerData.toolId}/capability`), | ||
| 1503 | } satisfies Capability; | ||
| 1504 | }; | ||
| 1505 | |||
| 1506 | const buildCatalogSnapshot = (input: { | ||
| 1507 | source: Source; | ||
| 1508 | adapterKey: string; | ||
| 1509 | documents: readonly CatalogSourceDocumentInput[]; | ||
| 1510 | registerOperations: (context: { | ||
| 1511 | catalog: CatalogV1; | ||
| 1512 | documentId: ReturnType<typeof DocumentIdSchema.make>; | ||
| 1513 | serviceScopeId: ReturnType<typeof ScopeIdSchema.make>; | ||
| 1514 | importer: JsonSchemaImporter; | ||
| 1515 | }) => void; | ||
| 1516 | }): CatalogSnapshotV1 => { | ||
| 1517 | const catalog = createEmptyCatalogV1(); | ||
| 1518 | const primaryDocumentKey = | ||
| 1519 | input.documents[0]?.documentKey | ||
| 1520 | ?? input.source.endpoint | ||
| 1521 | ?? input.source.id; | ||
| 1522 | const primaryDocumentId = documentIdFor(input.source, primaryDocumentKey); | ||
| 1523 | const primaryResourceId = resourceIdForSource(input.source); | ||
| 1524 | |||
| 1525 | const documents = input.documents.length > 0 | ||
| 1526 | ? input.documents | ||
| 1527 | : [{ | ||
| 1528 | documentKind: "synthetic", | ||
| 1529 | documentKey: input.source.endpoint, | ||
| 1530 | fetchedAt: Date.now(), | ||
| 1531 | contentText: "{}", | ||
| 1532 | }]; | ||
| 1533 | |||
| 1534 | for (const document of documents) { | ||
| 1535 | const documentId = documentIdFor(input.source, `${document.documentKind}:${document.documentKey}`); | ||
| 1536 | mutableRecord(catalog.documents)[documentId] = { | ||
| 1537 | id: documentId, | ||
| 1538 | kind: sourceKindFromSource(input.source), | ||
| 1539 | title: input.source.name, | ||
| 1540 | fetchedAt: new Date(document.fetchedAt ?? Date.now()).toISOString(), | ||
| 1541 | rawRef: document.documentKey, | ||
| 1542 | entryUri: document.documentKey.startsWith("http") ? document.documentKey : undefined, | ||
| 1543 | native: [nativeBlob({ | ||
| 1544 | source: input.source, | ||
| 1545 | kind: "source_document", | ||
| 1546 | pointer: `#/${document.documentKind}`, | ||
| 1547 | value: document.contentText, | ||
| 1548 | summary: document.documentKind, | ||
| 1549 | })], | ||
| 1550 | } satisfies SourceDocument; | ||
| 1551 | } | ||
| 1552 | |||
| 1553 | mutableRecord(catalog.resources)[primaryResourceId] = { | ||
| 1554 | id: primaryResourceId, | ||
| 1555 | documentId: primaryDocumentId, | ||
| 1556 | canonicalUri: primaryDocumentKey, | ||
| 1557 | baseUri: primaryDocumentKey, | ||
| 1558 | ...(input.source.kind === "openapi" || input.source.kind === "graphql" | ||
| 1559 | ? { | ||
| 1560 | dialectUri: | ||
| 1561 | input.source.kind === "openapi" | ||
| 1562 | ? "https://json-schema.org/draft/2020-12/schema" | ||
| 1563 | : "https://spec.graphql.org/", | ||
| 1564 | } | ||
| 1565 | : {}), | ||
| 1566 | anchors: {}, | ||
| 1567 | dynamicAnchors: {}, | ||
| 1568 | synthetic: false, | ||
| 1569 | provenance: provenanceFor(primaryDocumentId, "#"), | ||
| 1570 | }; | ||
| 1571 | |||
| 1572 | const serviceScopeId = createServiceScope({ | ||
| 1573 | catalog, | ||
| 1574 | source: input.source, | ||
| 1575 | documentId: primaryDocumentId, | ||
| 1576 | }); | ||
| 1577 | const importer = createJsonSchemaImporter({ | ||
| 1578 | catalog, | ||
| 1579 | source: input.source, | ||
| 1580 | resourceId: primaryResourceId, | ||
| 1581 | documentId: primaryDocumentId, | ||
| 1582 | }); | ||
| 1583 | input.registerOperations({ | ||
| 1584 | catalog, | ||
| 1585 | documentId: primaryDocumentId, | ||
| 1586 | serviceScopeId, | ||
| 1587 | importer, | ||
| 1588 | }); | ||
| 1589 | |||
| 1590 | const snapshot = createCatalogSnapshotV1({ | ||
| 1591 | import: importMetadataFor(input), | ||
| 1592 | catalog, | ||
| 1593 | }); | ||
| 1594 | |||
| 1595 | return snapshot; | ||
| 1596 | }; | ||
| 1597 | |||
| 1598 | export const createOpenApiCatalogSnapshot = (input: { | ||
| 1599 | source: Source; | ||
| 1600 | documents: readonly CatalogSourceDocumentInput[]; | ||
| 1601 | operations: readonly OpenApiCatalogOperationInput[]; | ||
| 1602 | }): CatalogSnapshotV1 => | ||
| 1603 | buildCatalogSnapshot({ | ||
| 1604 | source: input.source, | ||
| 1605 | adapterKey: "openapi", | ||
| 1606 | documents: input.documents, | ||
| 1607 | registerOperations: ({ catalog, documentId, serviceScopeId, importer }) => { | ||
| 1608 | for (const operation of input.operations) { | ||
| 1609 | createHttpCapabilityFromOpenApi({ | ||
| 1610 | catalog, | ||
| 1611 | source: input.source, | ||
| 1612 | documentId, | ||
| 1613 | serviceScopeId, | ||
| 1614 | operation, | ||
| 1615 | importer, | ||
| 1616 | }); | ||
| 1617 | } | ||
| 1618 | }, | ||
| 1619 | }); | ||
| 1620 | |||
| 1621 | export const createGoogleDiscoveryCatalogSnapshot = (input: { | ||
| 1622 | source: Source; | ||
| 1623 | documents: readonly CatalogSourceDocumentInput[]; | ||
| 1624 | operations: readonly GoogleDiscoveryCatalogOperationInput[]; | ||
| 1625 | }): CatalogSnapshotV1 => | ||
| 1626 | buildCatalogSnapshot({ | ||
| 1627 | source: input.source, | ||
| 1628 | adapterKey: "google_discovery", | ||
| 1629 | documents: input.documents, | ||
| 1630 | registerOperations: ({ catalog, documentId, serviceScopeId, importer }) => { | ||
| 1631 | for (const operation of input.operations) { | ||
| 1632 | createHttpCapabilityFromGoogleDiscovery({ | ||
| 1633 | catalog, | ||
| 1634 | source: input.source, | ||
| 1635 | documentId, | ||
| 1636 | serviceScopeId, | ||
| 1637 | operation, | ||
| 1638 | importer, | ||
| 1639 | }); | ||
| 1640 | } | ||
| 1641 | }, | ||
| 1642 | }); | ||
| 1643 | |||
| 1644 | export const createGraphqlCatalogSnapshot = (input: { | ||
| 1645 | source: Source; | ||
| 1646 | documents: readonly CatalogSourceDocumentInput[]; | ||
| 1647 | operations: readonly GraphqlCatalogOperationInput[]; | ||
| 1648 | }): CatalogSnapshotV1 => | ||
| 1649 | buildCatalogSnapshot({ | ||
| 1650 | source: input.source, | ||
| 1651 | adapterKey: "graphql", | ||
| 1652 | documents: input.documents, | ||
| 1653 | registerOperations: ({ catalog, documentId, serviceScopeId, importer }) => { | ||
| 1654 | for (const operation of input.operations) { | ||
| 1655 | createGraphqlCapability({ | ||
| 1656 | catalog, | ||
| 1657 | source: input.source, | ||
| 1658 | documentId, | ||
| 1659 | serviceScopeId, | ||
| 1660 | operation, | ||
| 1661 | importer, | ||
| 1662 | }); | ||
| 1663 | } | ||
| 1664 | }, | ||
| 1665 | }); | ||
| 1666 | |||
| 1667 | export const createMcpCatalogSnapshot = (input: { | ||
| 1668 | source: Source; | ||
| 1669 | documents: readonly CatalogSourceDocumentInput[]; | ||
| 1670 | operations: readonly McpCatalogOperationInput[]; | ||
| 1671 | }): CatalogSnapshotV1 => | ||
| 1672 | buildCatalogSnapshot({ | ||
| 1673 | source: input.source, | ||
| 1674 | adapterKey: "mcp", | ||
| 1675 | documents: input.documents, | ||
| 1676 | registerOperations: ({ catalog, documentId, serviceScopeId, importer }) => { | ||
| 1677 | for (const operation of input.operations) { | ||
| 1678 | createMcpCapability({ | ||
| 1679 | catalog, | ||
| 1680 | source: input.source, | ||
| 1681 | documentId, | ||
| 1682 | serviceScopeId, | ||
| 1683 | operation, | ||
| 1684 | importer, | ||
| 1685 | }); | ||
| 1686 | } | ||
| 1687 | }, | ||
| 1688 | }); | ||
| @@ -0,0 +1,18 @@ | |||
| 1 | import { sha256Hex } from "@executor/codemode-core"; | ||
| 2 | import type { CatalogSnapshotV1 } from "../ir/model"; | ||
| 3 | |||
| 4 | export const normalizeSearchText = ( | ||
| 5 | ...parts: ReadonlyArray<string | null | undefined> | ||
| 6 | ): string => | ||
| 7 | parts | ||
| 8 | .flatMap((part) => (part ? [part.trim()] : [])) | ||
| 9 | .filter(Boolean) | ||
| 10 | .join(" ") | ||
| 11 | .toLowerCase(); | ||
| 12 | |||
| 13 | export const contentHash = (value: string): string => sha256Hex(value); | ||
| 14 | |||
| 15 | export type SourceCatalogSyncResult = { | ||
| 16 | snapshot: CatalogSnapshotV1; | ||
| 17 | sourceHash: string | null; | ||
| 18 | }; | ||
| @@ -27,7 +27,7 @@ import { · 26 unmodified lines | |||
| 27 | getSourceAdapterForSource, | 27 | getSourceAdapterForSource, |
| 28 | } from "./source-adapters"; | 28 | } from "./source-adapters"; |
| 29 | import { | 29 | import { |
| 30 | materializationFromMcpManifestEntries, | 30 | catalogSyncResultFromMcpManifestEntries, |
| 31 | } from "./source-adapters/mcp"; | 31 | } from "./source-adapters/mcp"; |
| 32 | import { SecretMaterialResolverService } from "./secret-material-providers"; | 32 | import { SecretMaterialResolverService } from "./secret-material-providers"; |
| 33 | 33 | ||
| @@ -36,7 +36,7 @@ const shouldIndexSource = (source: Source): boolean => · 2 unmodified lines | |||
| 36 | && source.status === "connected" | 36 | && source.status === "connected" |
| 37 | && getSourceAdapterForSource(source).family !== "internal"; | 37 | && getSourceAdapterForSource(source).family !== "internal"; |
| 38 | 38 | ||
| 39 | type RuntimeSourceMaterializationDeps = { | 39 | type RuntimeSourceCatalogSyncDeps = { |
| 40 | runtimeLocalWorkspace: RuntimeLocalWorkspaceState; | 40 | runtimeLocalWorkspace: RuntimeLocalWorkspaceState; |
| 41 | workspaceStateStore: WorkspaceStateStoreShape; | 41 | workspaceStateStore: WorkspaceStateStoreShape; |
| 42 | sourceArtifactStore: SourceArtifactStoreShape; | 42 | sourceArtifactStore: SourceArtifactStoreShape; |
| @@ -44,30 +44,30 @@ type RuntimeSourceMaterializationDeps = { · 1 unmodified line | |||
| 44 | sourceAuthMaterialService: Effect.Effect.Success<typeof RuntimeSourceAuthMaterialService>; | 44 | sourceAuthMaterialService: Effect.Effect.Success<typeof RuntimeSourceAuthMaterialService>; |
| 45 | }; | 45 | }; |
| 46 | 46 | ||
| 47 | type SourceMaterializationServices = | 47 | type SourceCatalogSyncServices = |
| 48 | | RuntimeLocalWorkspaceService | 48 | | RuntimeLocalWorkspaceService |
| 49 | | WorkspaceStateStore | 49 | | WorkspaceStateStore |
| 50 | | SourceArtifactStore | 50 | | SourceArtifactStore |
| 51 | | RuntimeSourceAuthMaterialService | 51 | | RuntimeSourceAuthMaterialService |
| 52 | | SecretMaterialResolverService; | 52 | | SecretMaterialResolverService; |
| 53 | 53 | ||
| 54 | export type RuntimeSourceMaterializationShape = { | 54 | export type RuntimeSourceCatalogSyncShape = { |
| 55 | sync: (input: { | 55 | sync: (input: { |
| 56 | source: Source; | 56 | source: Source; |
| 57 | actorAccountId?: AccountId | null; | 57 | actorAccountId?: AccountId | null; |
| 58 | }) => Effect.Effect<void, Error, never>; | 58 | }) => Effect.Effect<void, Error, never>; |
| 59 | persistMcpRecipeMaterializationFromManifest: (input: { | 59 | persistMcpCatalogSnapshotFromManifest: (input: { |
| 60 | source: Source; | 60 | source: Source; |
| 61 | manifestEntries: Parameters<typeof materializationFromMcpManifestEntries>[0]["manifestEntries"]; | 61 | manifestEntries: Parameters<typeof catalogSyncResultFromMcpManifestEntries>[0]["manifestEntries"]; |
| 62 | }) => Effect.Effect<void, Error, never>; | 62 | }) => Effect.Effect<void, Error, never>; |
| 63 | }; | 63 | }; |
| 64 | 64 | ||
| 65 | export class RuntimeSourceMaterializationService extends Context.Tag( | 65 | export class RuntimeSourceCatalogSyncService extends Context.Tag( |
| 66 | "#runtime/RuntimeSourceMaterializationService", | 66 | "#runtime/RuntimeSourceCatalogSyncService", |
| 67 | )<RuntimeSourceMaterializationService, RuntimeSourceMaterializationShape>() {} | 67 | )<RuntimeSourceCatalogSyncService, RuntimeSourceCatalogSyncShape>() {} |
| 68 | 68 | ||
| 69 | const ensureRuntimeMaterializationWorkspace = ( | 69 | const ensureRuntimeCatalogSyncWorkspace = ( |
| 70 | deps: RuntimeSourceMaterializationDeps, | 70 | deps: RuntimeSourceCatalogSyncDeps, |
| 71 | workspaceId: Source["workspaceId"], | 71 | workspaceId: Source["workspaceId"], |
| 72 | ) => { | 72 | ) => { |
| 73 | if (deps.runtimeLocalWorkspace.installation.workspaceId !== workspaceId) { | 73 | if (deps.runtimeLocalWorkspace.installation.workspaceId !== workspaceId) { |
| @@ -81,15 +81,15 @@ const ensureRuntimeMaterializationWorkspace = ( · 7 unmodified lines | |||
| 81 | return Effect.succeed(deps.runtimeLocalWorkspace.context); | 81 | return Effect.succeed(deps.runtimeLocalWorkspace.context); |
| 82 | }; | 82 | }; |
| 83 | 83 | ||
| 84 | const syncSourceMaterializationWithDeps = ( | 84 | const syncSourceCatalogWithDeps = ( |
| 85 | deps: RuntimeSourceMaterializationDeps, | 85 | deps: RuntimeSourceCatalogSyncDeps, |
| 86 | input: { | 86 | input: { |
| 87 | source: Source; | 87 | source: Source; |
| 88 | actorAccountId?: AccountId | null; | 88 | actorAccountId?: AccountId | null; |
| 89 | }, | 89 | }, |
| 90 | ): Effect.Effect<void, Error, never> => | 90 | ): Effect.Effect<void, Error, never> => |
| 91 | Effect.gen(function* () { | 91 | Effect.gen(function* () { |
| 92 | const workspaceContext = yield* ensureRuntimeMaterializationWorkspace( | 92 | const workspaceContext = yield* ensureRuntimeCatalogSyncWorkspace( |
| 93 | deps, | 93 | deps, |
| 94 | input.source.workspaceId, | 94 | input.source.workspaceId, |
| 95 | ); | 95 | ); |
| @@ -118,7 +118,7 @@ const syncSourceMaterializationWithDeps = ( · 22 unmodified lines | |||
| 118 | } | 118 | } |
| 119 | 119 | ||
| 120 | const adapter = getSourceAdapterForSource(input.source); | 120 | const adapter = getSourceAdapterForSource(input.source); |
| 121 | const materialization = yield* adapter.materializeSource({ | 121 | const syncResult = yield* adapter.syncCatalog({ |
| 122 | source: input.source, | 122 | source: input.source, |
| 123 | resolveSecretMaterial: deps.resolveSecretMaterial, | 123 | resolveSecretMaterial: deps.resolveSecretMaterial, |
| 124 | resolveAuthMaterialForSlot: (slot) => | 124 | resolveAuthMaterialForSlot: (slot) => |
| @@ -133,7 +133,7 @@ const syncSourceMaterializationWithDeps = ( · 8 unmodified lines | |||
| 133 | sourceId: input.source.id, | 133 | sourceId: input.source.id, |
| 134 | artifact: deps.sourceArtifactStore.build({ | 134 | artifact: deps.sourceArtifactStore.build({ |
| 135 | source: input.source, | 135 | source: input.source, |
| 136 | materialization, | 136 | syncResult, |
| 137 | }), | 137 | }), |
| 138 | }); | 138 | }); |
| 139 | 139 | ||
| @@ -146,7 +146,7 @@ const syncSourceMaterializationWithDeps = ( · 6 unmodified lines | |||
| 146 | [input.source.id]: { | 146 | [input.source.id]: { |
| 147 | status: "connected", | 147 | status: "connected", |
| 148 | lastError: null, | 148 | lastError: null, |
| 149 | sourceHash: materialization.sourceHash, | 149 | sourceHash: syncResult.sourceHash, |
| 150 | createdAt: existingSourceState?.createdAt ?? input.source.createdAt, | 150 | createdAt: existingSourceState?.createdAt ?? input.source.createdAt, |
| 151 | updatedAt: Date.now(), | 151 | updatedAt: Date.now(), |
| 152 | }, | 152 | }, |
| @@ -158,20 +158,20 @@ const syncSourceMaterializationWithDeps = ( · 5 unmodified lines | |||
| 158 | }); | 158 | }); |
| 159 | }); | 159 | }); |
| 160 | 160 | ||
| 161 | const persistMcpRecipeMaterializationFromManifestWithDeps = ( | 161 | const persistMcpCatalogSnapshotFromManifestWithDeps = ( |
| 162 | deps: RuntimeSourceMaterializationDeps, | 162 | deps: RuntimeSourceCatalogSyncDeps, |
| 163 | input: { | 163 | input: { |
| 164 | source: Source; | 164 | source: Source; |
| 165 | manifestEntries: Parameters<typeof materializationFromMcpManifestEntries>[0]["manifestEntries"]; | 165 | manifestEntries: Parameters<typeof catalogSyncResultFromMcpManifestEntries>[0]["manifestEntries"]; |
| 166 | }, | 166 | }, |
| 167 | ): Effect.Effect<void, Error, never> => | 167 | ): Effect.Effect<void, Error, never> => |
| 168 | Effect.gen(function* () { | 168 | Effect.gen(function* () { |
| 169 | const workspaceContext = yield* ensureRuntimeMaterializationWorkspace( | 169 | const workspaceContext = yield* ensureRuntimeCatalogSyncWorkspace( |
| 170 | deps, | 170 | deps, |
| 171 | input.source.workspaceId, | 171 | input.source.workspaceId, |
| 172 | ); | 172 | ); |
| 173 | const materialization = materializationFromMcpManifestEntries({ | 173 | const syncResult = catalogSyncResultFromMcpManifestEntries({ |
| 174 | recipeRevisionId: "src_recipe_rev_materialization" as never, | 174 | source: input.source, |
| 175 | endpoint: input.source.endpoint, | 175 | endpoint: input.source.endpoint, |
| 176 | manifestEntries: input.manifestEntries, | 176 | manifestEntries: input.manifestEntries, |
| 177 | }); | 177 | }); |
| @@ -181,15 +181,15 @@ const persistMcpRecipeMaterializationFromManifestWithDeps = ( · 3 unmodified lines | |||
| 181 | sourceId: input.source.id, | 181 | sourceId: input.source.id, |
| 182 | artifact: deps.sourceArtifactStore.build({ | 182 | artifact: deps.sourceArtifactStore.build({ |
| 183 | source: input.source, | 183 | source: input.source, |
| 184 | materialization, | 184 | syncResult, |
| 185 | }), | 185 | }), |
| 186 | }); | 186 | }); |
| 187 | }); | 187 | }); |
| 188 | 188 | ||
| 189 | export const syncSourceMaterialization = (input: { | 189 | export const syncSourceCatalog = (input: { |
| 190 | source: Source; | 190 | source: Source; |
| 191 | actorAccountId?: AccountId | null; | 191 | actorAccountId?: AccountId | null; |
| 192 | }): Effect.Effect<void, Error, SourceMaterializationServices> => | 192 | }): Effect.Effect<void, Error, SourceCatalogSyncServices> => |
| 193 | Effect.gen(function* () { | 193 | Effect.gen(function* () { |
| 194 | const runtimeLocalWorkspace = yield* RuntimeLocalWorkspaceService; | 194 | const runtimeLocalWorkspace = yield* RuntimeLocalWorkspaceService; |
| 195 | const workspaceStateStore = yield* WorkspaceStateStore; | 195 | const workspaceStateStore = yield* WorkspaceStateStore; |
| @@ -197,7 +197,7 @@ export const syncSourceMaterialization = (input: { · 1 unmodified line | |||
| 197 | const resolveSecretMaterial = yield* SecretMaterialResolverService; | 197 | const resolveSecretMaterial = yield* SecretMaterialResolverService; |
| 198 | const sourceAuthMaterialService = yield* RuntimeSourceAuthMaterialService; | 198 | const sourceAuthMaterialService = yield* RuntimeSourceAuthMaterialService; |
| 199 | 199 | ||
| 200 | return yield* syncSourceMaterializationWithDeps( | 200 | return yield* syncSourceCatalogWithDeps( |
| 201 | { | 201 | { |
| 202 | runtimeLocalWorkspace, | 202 | runtimeLocalWorkspace, |
| 203 | workspaceStateStore, | 203 | workspaceStateStore, |
| @@ -212,10 +212,10 @@ export const syncSourceMaterialization = (input: { · 8 unmodified lines | |||
| 212 | ); | 212 | ); |
| 213 | }); | 213 | }); |
| 214 | 214 | ||
| 215 | export const persistMcpRecipeMaterializationFromManifest = (input: { | 215 | export const persistMcpCatalogSnapshotFromManifest = (input: { |
| 216 | source: Source; | 216 | source: Source; |
| 217 | manifestEntries: Parameters<typeof materializationFromMcpManifestEntries>[0]["manifestEntries"]; | 217 | manifestEntries: Parameters<typeof catalogSyncResultFromMcpManifestEntries>[0]["manifestEntries"]; |
| 218 | }): Effect.Effect<void, Error, SourceMaterializationServices> => | 218 | }): Effect.Effect<void, Error, SourceCatalogSyncServices> => |
| 219 | Effect.gen(function* () { | 219 | Effect.gen(function* () { |
| 220 | const runtimeLocalWorkspace = yield* RuntimeLocalWorkspaceService; | 220 | const runtimeLocalWorkspace = yield* RuntimeLocalWorkspaceService; |
| 221 | const workspaceStateStore = yield* WorkspaceStateStore; | 221 | const workspaceStateStore = yield* WorkspaceStateStore; |
| @@ -223,7 +223,7 @@ export const persistMcpRecipeMaterializationFromManifest = (input: { · 1 unmodified line | |||
| 223 | const resolveSecretMaterial = yield* SecretMaterialResolverService; | 223 | const resolveSecretMaterial = yield* SecretMaterialResolverService; |
| 224 | const sourceAuthMaterialService = yield* RuntimeSourceAuthMaterialService; | 224 | const sourceAuthMaterialService = yield* RuntimeSourceAuthMaterialService; |
| 225 | 225 | ||
| 226 | return yield* persistMcpRecipeMaterializationFromManifestWithDeps( | 226 | return yield* persistMcpCatalogSnapshotFromManifestWithDeps( |
| 227 | { | 227 | { |
| 228 | runtimeLocalWorkspace, | 228 | runtimeLocalWorkspace, |
| 229 | workspaceStateStore, | 229 | workspaceStateStore, |
| @@ -235,8 +235,8 @@ export const persistMcpRecipeMaterializationFromManifest = (input: { · 5 unmodified lines | |||
| 235 | ); | 235 | ); |
| 236 | }); | 236 | }); |
| 237 | 237 | ||
| 238 | export const RuntimeSourceMaterializationLive = Layer.effect( | 238 | export const RuntimeSourceCatalogSyncLive = Layer.effect( |
| 239 | RuntimeSourceMaterializationService, | 239 | RuntimeSourceCatalogSyncService, |
| 240 | Effect.gen(function* () { | 240 | Effect.gen(function* () { |
| 241 | const runtimeLocalWorkspace = yield* RuntimeLocalWorkspaceService; | 241 | const runtimeLocalWorkspace = yield* RuntimeLocalWorkspaceService; |
| 242 | const workspaceStateStore = yield* WorkspaceStateStore; | 242 | const workspaceStateStore = yield* WorkspaceStateStore; |
| @@ -244,7 +244,7 @@ export const RuntimeSourceMaterializationLive = Layer.effect( · 1 unmodified line | |||
| 244 | const resolveSecretMaterial = yield* SecretMaterialResolverService; | 244 | const resolveSecretMaterial = yield* SecretMaterialResolverService; |
| 245 | const sourceAuthMaterialService = yield* RuntimeSourceAuthMaterialService; | 245 | const sourceAuthMaterialService = yield* RuntimeSourceAuthMaterialService; |
| 246 | 246 | ||
| 247 | const deps: RuntimeSourceMaterializationDeps = { | 247 | const deps: RuntimeSourceCatalogSyncDeps = { |
| 248 | runtimeLocalWorkspace, | 248 | runtimeLocalWorkspace, |
| 249 | workspaceStateStore, | 249 | workspaceStateStore, |
| 250 | sourceArtifactStore, | 250 | sourceArtifactStore, |
| @@ -252,10 +252,10 @@ export const RuntimeSourceMaterializationLive = Layer.effect( · 1 unmodified line | |||
| 252 | sourceAuthMaterialService, | 252 | sourceAuthMaterialService, |
| 253 | }; | 253 | }; |
| 254 | 254 | ||
| 255 | return RuntimeSourceMaterializationService.of({ | 255 | return RuntimeSourceCatalogSyncService.of({ |
| 256 | sync: (input) => syncSourceMaterializationWithDeps(deps, input), | 256 | sync: (input) => syncSourceCatalogWithDeps(deps, input), |
| 257 | persistMcpRecipeMaterializationFromManifest: (input) => | 257 | persistMcpCatalogSnapshotFromManifest: (input) => |
| 258 | persistMcpRecipeMaterializationFromManifestWithDeps(deps, input), | 258 | persistMcpCatalogSnapshotFromManifestWithDeps(deps, input), |
| 259 | }); | 259 | }); |
| 260 | }), | 260 | }), |
| 261 | ); | 261 | ); |
| @@ -1,4 +1,4 @@ | |||
| 1 | import { describe, expect, it } from "vitest"; | 1 | import { describe, expect, it } from "@effect/vitest"; |
| 2 | import * as Effect from "effect/Effect"; | 2 | import * as Effect from "effect/Effect"; |
| 3 | 3 | ||
| 4 | import { | 4 | import { |
| @@ -13,8 +13,8 @@ import { · 8 unmodified lines | |||
| 13 | createSourceFromPayload, | 13 | createSourceFromPayload, |
| 14 | projectSourceFromStorage, | 14 | projectSourceFromStorage, |
| 15 | splitSourceForStorage, | 15 | splitSourceForStorage, |
| 16 | stableSourceRecipeId, | 16 | stableSourceCatalogId, |
| 17 | stableSourceRecipeRevisionId, | 17 | stableSourceCatalogRevisionId, |
| 18 | updateSourceFromPayload, | 18 | updateSourceFromPayload, |
| 19 | } from "./source-definitions"; | 19 | } from "./source-definitions"; |
| 20 | import { namespaceFromSourceName } from "./source-names"; | 20 | import { namespaceFromSourceName } from "./source-names"; |
| @@ -63,7 +63,7 @@ const makeSource = (overrides: Partial<Source> = {}): Source => ({ · 42 unmodified lines | |||
| 63 | }); | 63 | }); |
| 64 | 64 | ||
| 65 | describe("source-definitions", () => { | 65 | describe("source-definitions", () => { |
| 66 | describe("stable recipe ids", () => { | 66 | describe("stable catalog ids", () => { |
| 67 | it("is deterministic across calls and ignores source name/workspace", () => { | 67 | it("is deterministic across calls and ignores source name/workspace", () => { |
| 68 | const source = makeSource(); | 68 | const source = makeSource(); |
| 69 | const renamed = makeSource({ | 69 | const renamed = makeSource({ |
| @@ -73,26 +73,26 @@ describe("source-definitions", () => { · 3 unmodified lines | |||
| 73 | workspaceId: WorkspaceIdSchema.make("ws_source_definitions_other"), | 73 | workspaceId: WorkspaceIdSchema.make("ws_source_definitions_other"), |
| 74 | }); | 74 | }); |
| 75 | 75 | ||
| 76 | expect(stableSourceRecipeId(source)).toBe(stableSourceRecipeId(source)); | 76 | expect(stableSourceCatalogId(source)).toBe(stableSourceCatalogId(source)); |
| 77 | expect(stableSourceRecipeRevisionId(source)).toBe(stableSourceRecipeRevisionId(source)); | 77 | expect(stableSourceCatalogRevisionId(source)).toBe(stableSourceCatalogRevisionId(source)); |
| 78 | expect(stableSourceRecipeId(renamed)).toBe(stableSourceRecipeId(source)); | 78 | expect(stableSourceCatalogId(renamed)).toBe(stableSourceCatalogId(source)); |
| 79 | expect(stableSourceRecipeRevisionId(renamed)).toBe(stableSourceRecipeRevisionId(source)); | 79 | expect(stableSourceCatalogRevisionId(renamed)).toBe(stableSourceCatalogRevisionId(source)); |
| 80 | expect(stableSourceRecipeId(differentWorkspace)).toBe(stableSourceRecipeId(source)); | 80 | expect(stableSourceCatalogId(differentWorkspace)).toBe(stableSourceCatalogId(source)); |
| 81 | expect(stableSourceRecipeRevisionId(differentWorkspace)).toBe( | 81 | expect(stableSourceCatalogRevisionId(differentWorkspace)).toBe( |
| 82 | stableSourceRecipeRevisionId(source), | 82 | stableSourceCatalogRevisionId(source), |
| 83 | ); | 83 | ); |
| 84 | }); | 84 | }); |
| 85 | 85 | ||
| 86 | it("changes recipe and revision ids when the source config changes", () => { | 86 | it("changes catalog and revision ids when the source config changes", () => { |
| 87 | const source = makeSource(); | 87 | const source = makeSource(); |
| 88 | const changedEndpoint = makeSource({ | 88 | const changedEndpoint = makeSource({ |
| 89 | endpoint: "https://example.com", | 89 | endpoint: "https://example.com", |
| 90 | binding: openApiBinding("https://example.com/openapi.json"), | 90 | binding: openApiBinding("https://example.com/openapi.json"), |
| 91 | }); | 91 | }); |
| 92 | 92 | ||
| 93 | expect(stableSourceRecipeId(changedEndpoint)).not.toBe(stableSourceRecipeId(source)); | 93 | expect(stableSourceCatalogId(changedEndpoint)).not.toBe(stableSourceCatalogId(source)); |
| 94 | expect(stableSourceRecipeRevisionId(changedEndpoint)).not.toBe( | 94 | expect(stableSourceCatalogRevisionId(changedEndpoint)).not.toBe( |
| 95 | stableSourceRecipeRevisionId(source), | 95 | stableSourceCatalogRevisionId(source), |
| 96 | ); | 96 | ); |
| 97 | }); | 97 | }); |
| 98 | }); | 98 | }); |
| @@ -358,14 +358,14 @@ describe("source-definitions", () => { · 259 unmodified lines | |||
| 358 | }, | 358 | }, |
| 359 | }, | 359 | }, |
| 360 | }); | 360 | }); |
| 361 | const recipeId = stableSourceRecipeId(source); | 361 | const catalogId = stableSourceCatalogId(source); |
| 362 | const recipeRevisionId = stableSourceRecipeRevisionId(source); | 362 | const catalogRevisionId = stableSourceCatalogRevisionId(source); |
| 363 | const existingCredentialId = CredentialIdSchema.make("cred_existing"); | 363 | const existingCredentialId = CredentialIdSchema.make("cred_existing"); |
| 364 | 364 | ||
| 365 | const { sourceRecord, runtimeAuthArtifact } = splitSourceForStorage({ | 365 | const { sourceRecord, runtimeAuthArtifact } = splitSourceForStorage({ |
| 366 | source, | 366 | source, |
| 367 | recipeId, | 367 | catalogId, |
| 368 | recipeRevisionId, | 368 | catalogRevisionId, |
| 369 | existingRuntimeAuthArtifactId: existingCredentialId, | 369 | existingRuntimeAuthArtifactId: existingCredentialId, |
| 370 | }); | 370 | }); |
| 371 | 371 | ||
| @@ -426,8 +426,8 @@ describe("source-definitions", () => { · 54 unmodified lines | |||
| 426 | for (const source of [withRefresh, withoutRefresh]) { | 426 | for (const source of [withRefresh, withoutRefresh]) { |
| 427 | const { sourceRecord, runtimeAuthArtifact } = splitSourceForStorage({ | 427 | const { sourceRecord, runtimeAuthArtifact } = splitSourceForStorage({ |
| 428 | source, | 428 | source, |
| 429 | recipeId: stableSourceRecipeId(source), | 429 | catalogId: stableSourceCatalogId(source), |
| 430 | recipeRevisionId: stableSourceRecipeRevisionId(source), | 430 | catalogRevisionId: stableSourceCatalogRevisionId(source), |
| 431 | }); | 431 | }); |
| 432 | const projected = await Effect.runPromise(projectSourceFromStorage({ | 432 | const projected = await Effect.runPromise(projectSourceFromStorage({ |
| 433 | sourceRecord, | 433 | sourceRecord, |
| @@ -445,8 +445,8 @@ describe("source-definitions", () => { · 11 unmodified lines | |||
| 445 | }); | 445 | }); |
| 446 | const { sourceRecord, runtimeAuthArtifact } = splitSourceForStorage({ | 446 | const { sourceRecord, runtimeAuthArtifact } = splitSourceForStorage({ |
| 447 | source, | 447 | source, |
| 448 | recipeId: stableSourceRecipeId(source), | 448 | catalogId: stableSourceCatalogId(source), |
| 449 | recipeRevisionId: stableSourceRecipeRevisionId(source), | 449 | catalogRevisionId: stableSourceCatalogRevisionId(source), |
| 450 | }); | 450 | }); |
| 451 | 451 | ||
| 452 | expect(runtimeAuthArtifact).toBeNull(); | 452 | expect(runtimeAuthArtifact).toBeNull(); |
| @@ -11,19 +11,19 @@ import type { · 10 unmodified lines | |||
| 11 | Source, | 11 | Source, |
| 12 | SourceAuth, | 12 | SourceAuth, |
| 13 | SourceImportAuthPolicy, | 13 | SourceImportAuthPolicy, |
| 14 | SourceRecipeAdapterKey, | 14 | SourceCatalogAdapterKey, |
| 15 | SourceRecipeId, | 15 | SourceCatalogId, |
| 16 | SourceRecipeKind, | 16 | SourceCatalogKind, |
| 17 | SourceRecipeRevisionId, | 17 | SourceCatalogRevisionId, |
| 18 | StoredSourceRecord, | 18 | StoredSourceRecord, |
| 19 | StoredSourceRecipeRecord, | 19 | StoredSourceCatalogRecord, |
| 20 | StoredSourceRecipeRevisionRecord, | 20 | StoredSourceCatalogRevisionRecord, |
| 21 | WorkspaceId, | 21 | WorkspaceId, |
| 22 | } from "#schema"; | 22 | } from "#schema"; |
| 23 | import { | 23 | import { |
| 24 | AuthArtifactIdSchema, | 24 | AuthArtifactIdSchema, |
| 25 | SourceRecipeIdSchema, | 25 | SourceCatalogIdSchema, |
| 26 | SourceRecipeRevisionIdSchema, | 26 | SourceCatalogRevisionIdSchema, |
| 27 | } from "#schema"; | 27 | } from "#schema"; |
| 28 | import * as Effect from "effect/Effect"; | 28 | import * as Effect from "effect/Effect"; |
| 29 | 29 | ||
| @@ -42,45 +42,45 @@ const trimOrNull = (value: string | null | undefined): string | null => { · 12 unmodified lines | |||
| 42 | return trimmed.length > 0 ? trimmed : null; | 42 | return trimmed.length > 0 ? trimmed : null; |
| 43 | }; | 43 | }; |
| 44 | 44 | ||
| 45 | type SourceRecipeSourceConfig = Record<string, unknown>; | 45 | type SourceCatalogSourceConfig = Record<string, unknown>; |
| 46 | 46 | ||
| 47 | const sourceConfigFromSource = (source: Source): SourceRecipeSourceConfig => | 47 | const sourceConfigFromSource = (source: Source): SourceCatalogSourceConfig => |
| 48 | getSourceAdapterForSource(source).sourceConfigFromSource(source); | 48 | getSourceAdapterForSource(source).sourceConfigFromSource(source); |
| 49 | 49 | ||
| 50 | const sourceRecipeKindFromSource = (source: Source): SourceRecipeKind => { | 50 | const sourceCatalogKindFromSource = (source: Source): SourceCatalogKind => { |
| 51 | const adapter = getSourceAdapterForSource(source); | 51 | const adapter = getSourceAdapterForSource(source); |
| 52 | return adapter.family; | 52 | return adapter.family; |
| 53 | }; | 53 | }; |
| 54 | 54 | ||
| 55 | const sourceRecipeAdapterKeyFromSource = (source: Source): SourceRecipeAdapterKey => { | 55 | const sourceCatalogAdapterKeyFromSource = (source: Source): SourceCatalogAdapterKey => { |
| 56 | return getSourceAdapterForSource(source).key; | 56 | return getSourceAdapterForSource(source).key; |
| 57 | }; | 57 | }; |
| 58 | 58 | ||
| 59 | const sourceRecipeProviderKeyFromSource = (source: Source): string => { | 59 | const sourceCatalogProviderKeyFromSource = (source: Source): string => { |
| 60 | return getSourceAdapterForSource(source).providerKey; | 60 | return getSourceAdapterForSource(source).providerKey; |
| 61 | }; | 61 | }; |
| 62 | 62 | ||
| 63 | const stableHash = (value: string): string => | 63 | const stableHash = (value: string): string => |
| 64 | sha256Hex(value).slice(0, 24); | 64 | sha256Hex(value).slice(0, 24); |
| 65 | 65 | ||
| 66 | const sourceRecipeSignature = (source: Source): string => | 66 | const sourceCatalogSignature = (source: Source): string => |
| 67 | JSON.stringify({ | 67 | JSON.stringify({ |
| 68 | recipeKind: sourceRecipeKindFromSource(source), | 68 | catalogKind: sourceCatalogKindFromSource(source), |
| 69 | adapterKey: sourceRecipeAdapterKeyFromSource(source), | 69 | adapterKey: sourceCatalogAdapterKeyFromSource(source), |
| 70 | providerKey: sourceRecipeProviderKeyFromSource(source), | 70 | providerKey: sourceCatalogProviderKeyFromSource(source), |
| 71 | sourceConfig: sourceConfigFromSource(source), | 71 | sourceConfig: sourceConfigFromSource(source), |
| 72 | }); | 72 | }); |
| 73 | 73 | ||
| 74 | export const sourceConfigSignature = (source: Source): string => | 74 | export const sourceConfigSignature = (source: Source): string => |
| 75 | JSON.stringify(sourceConfigFromSource(source)); | 75 | JSON.stringify(sourceConfigFromSource(source)); |
| 76 | 76 | ||
| 77 | export const stableSourceRecipeId = (source: Source): SourceRecipeId => | 77 | export const stableSourceCatalogId = (source: Source): SourceCatalogId => |
| 78 | SourceRecipeIdSchema.make(`src_recipe_${stableHash(sourceRecipeSignature(source))}`); | 78 | SourceCatalogIdSchema.make(`src_catalog_${stableHash(sourceCatalogSignature(source))}`); |
| 79 | 79 | ||
| 80 | export const stableSourceRecipeRevisionId = ( | 80 | export const stableSourceCatalogRevisionId = ( |
| 81 | source: Source, | 81 | source: Source, |
| 82 | ): SourceRecipeRevisionId => | 82 | ): SourceCatalogRevisionId => |
| 83 | SourceRecipeRevisionIdSchema.make(`src_recipe_rev_${stableHash(sourceConfigSignature(source))}`); | 83 | SourceCatalogRevisionIdSchema.make(`src_catalog_rev_${stableHash(sourceConfigSignature(source))}`); |
| 84 | 84 | ||
| 85 | const normalizeAuth = ( | 85 | const normalizeAuth = ( |
| 86 | auth: SourceAuth | undefined, | 86 | auth: SourceAuth | undefined, |
| @@ -298,15 +298,15 @@ export const updateSourceFromPayload = (input: { · 211 unmodified lines | |||
| 298 | }); | 298 | }); |
| 299 | }); | 299 | }); |
| 300 | 300 | ||
| 301 | export const createSourceRecipeRecord = (input: { | 301 | export const createSourceCatalogRecord = (input: { |
| 302 | source: Source; | 302 | source: Source; |
| 303 | recipeId?: SourceRecipeId | null; | 303 | catalogId?: SourceCatalogId | null; |
| 304 | latestRevisionId: SourceRecipeRevisionId; | 304 | latestRevisionId: SourceCatalogRevisionId; |
| 305 | }): StoredSourceRecipeRecord => ({ | 305 | }): StoredSourceCatalogRecord => ({ |
| 306 | id: input.recipeId ?? stableSourceRecipeId(input.source), | 306 | id: input.catalogId ?? stableSourceCatalogId(input.source), |
| 307 | kind: sourceRecipeKindFromSource(input.source), | 307 | kind: sourceCatalogKindFromSource(input.source), |
| 308 | adapterKey: sourceRecipeAdapterKeyFromSource(input.source), | 308 | adapterKey: sourceCatalogAdapterKeyFromSource(input.source), |
| 309 | providerKey: sourceRecipeProviderKeyFromSource(input.source), | 309 | providerKey: sourceCatalogProviderKeyFromSource(input.source), |
| 310 | name: input.source.name, | 310 | name: input.source.name, |
| 311 | summary: null, | 311 | summary: null, |
| 312 | visibility: "workspace", | 312 | visibility: "workspace", |
| @@ -315,32 +315,32 @@ export const createSourceRecipeRecord = (input: { · 2 unmodified lines | |||
| 315 | updatedAt: input.source.updatedAt, | 315 | updatedAt: input.source.updatedAt, |
| 316 | }); | 316 | }); |
| 317 | 317 | ||
| 318 | export const createSourceRecipeRevisionRecord = (input: { | 318 | export const createSourceCatalogRevisionRecord = (input: { |
| 319 | source: Source; | 319 | source: Source; |
| 320 | recipeId: SourceRecipeId; | 320 | catalogId: SourceCatalogId; |
| 321 | recipeRevisionId?: SourceRecipeRevisionId | null; | 321 | catalogRevisionId?: SourceCatalogRevisionId | null; |
| 322 | revisionNumber: number; | 322 | revisionNumber: number; |
| 323 | manifestJson?: string | null; | 323 | importMetadataJson?: string | null; |
| 324 | manifestHash?: string | null; | 324 | importMetadataHash?: string | null; |
| 325 | materializationHash?: string | null; | 325 | snapshotHash?: string | null; |
| 326 | }): StoredSourceRecipeRevisionRecord => ({ | 326 | }): StoredSourceCatalogRevisionRecord => ({ |
| 327 | id: | 327 | id: |
| 328 | input.recipeRevisionId | 328 | input.catalogRevisionId |
| 329 | ?? stableSourceRecipeRevisionId(input.source), | 329 | ?? stableSourceCatalogRevisionId(input.source), |
| 330 | recipeId: input.recipeId, | 330 | catalogId: input.catalogId, |
| 331 | revisionNumber: input.revisionNumber, | 331 | revisionNumber: input.revisionNumber, |
| 332 | sourceConfigJson: sourceConfigSignature(input.source), | 332 | sourceConfigJson: sourceConfigSignature(input.source), |
| 333 | manifestJson: input.manifestJson ?? null, | 333 | importMetadataJson: input.importMetadataJson ?? null, |
| 334 | manifestHash: input.manifestHash ?? null, | 334 | importMetadataHash: input.importMetadataHash ?? null, |
| 335 | materializationHash: input.materializationHash ?? null, | 335 | snapshotHash: input.snapshotHash ?? null, |
| 336 | createdAt: input.source.createdAt, | 336 | createdAt: input.source.createdAt, |
| 337 | updatedAt: input.source.updatedAt, | 337 | updatedAt: input.source.updatedAt, |
| 338 | }); | 338 | }); |
| 339 | 339 | ||
| 340 | export const splitSourceForStorage = (input: { | 340 | export const splitSourceForStorage = (input: { |
| 341 | source: Source; | 341 | source: Source; |
| 342 | recipeId: SourceRecipeId; | 342 | catalogId: SourceCatalogId; |
| 343 | recipeRevisionId: SourceRecipeRevisionId; | 343 | catalogRevisionId: SourceCatalogRevisionId; |
| 344 | actorAccountId?: AccountId | null; | 344 | actorAccountId?: AccountId | null; |
| 345 | existingRuntimeAuthArtifactId?: AuthArtifact["id"] | null; | 345 | existingRuntimeAuthArtifactId?: AuthArtifact["id"] | null; |
| 346 | existingImportAuthArtifactId?: AuthArtifact["id"] | null; | 346 | existingImportAuthArtifactId?: AuthArtifact["id"] | null; |
| @@ -352,8 +352,8 @@ export const splitSourceForStorage = (input: { · 5 unmodified lines | |||
| 352 | const sourceRecord: StoredSourceRecord = { | 352 | const sourceRecord: StoredSourceRecord = { |
| 353 | id: input.source.id, | 353 | id: input.source.id, |
| 354 | workspaceId: input.source.workspaceId, | 354 | workspaceId: input.source.workspaceId, |
| 355 | recipeId: input.recipeId, | 355 | catalogId: input.catalogId, |
| 356 | recipeRevisionId: input.recipeRevisionId, | 356 | catalogRevisionId: input.catalogRevisionId, |
| 357 | name: input.source.name, | 357 | name: input.source.name, |
| 358 | kind: input.source.kind, | 358 | kind: input.source.kind, |
| 359 | endpoint: input.source.endpoint, | 359 | endpoint: input.source.endpoint, |
| @@ -4,7 +4,7 @@ import { · 3 unmodified lines | |||
| 4 | type ServerResponse, | 4 | type ServerResponse, |
| 5 | } from "node:http"; | 5 | } from "node:http"; |
| 6 | 6 | ||
| 7 | import { describe, expect, it } from "vitest"; | 7 | import { describe, expect, it } from "@effect/vitest"; |
| 8 | import * as Effect from "effect/Effect"; | 8 | import * as Effect from "effect/Effect"; |
| 9 | 9 | ||
| 10 | import { discoverSource } from "./source-discovery"; | 10 | import { discoverSource } from "./source-discovery"; |
| @@ -1,37 +1,27 @@ | |||
| 1 | import type { | 1 | import type { |
| 2 | Source, | ||
| 3 | SourceId, | 2 | SourceId, |
| 4 | SourceInspection, | 3 | SourceInspection, |
| 5 | SourceInspectionDiscoverPayload, | 4 | SourceInspectionDiscoverPayload, |
| 6 | SourceInspectionDiscoverResult, | 5 | SourceInspectionDiscoverResult, |
| 7 | SourceInspectionDiscoverResultItem, | 6 | SourceInspectionDiscoverResultItem, |
| 8 | SourceInspectionSchemaBundle, | ||
| 9 | SourceInspectionToolListItem, | ||
| 10 | SourceInspectionToolDetail, | 7 | SourceInspectionToolDetail, |
| 8 | SourceInspectionToolListItem, | ||
| 11 | SourceInspectionToolSummary, | 9 | SourceInspectionToolSummary, |
| 12 | StoredSourceRecipeOperationRecord, | ||
| 13 | WorkspaceId, | 10 | WorkspaceId, |
| 14 | } from "#schema"; | 11 | } from "#schema"; |
| 15 | import { SourceRecipeSchemaBundleIdSchema } from "#schema"; | ||
| 16 | import { | 12 | import { |
| 17 | ControlPlaneNotFoundError, | 13 | ControlPlaneNotFoundError, |
| 18 | ControlPlaneStorageError, | 14 | ControlPlaneStorageError, |
| 19 | } from "../api/errors"; | 15 | } from "../api/errors"; |
| 20 | import * as Effect from "effect/Effect"; | 16 | import * as Effect from "effect/Effect"; |
| 21 | import * as Option from "effect/Option"; | ||
| 22 | 17 | ||
| 23 | import { operationErrors } from "./operation-errors"; | 18 | import { operationErrors } from "./operation-errors"; |
| 24 | import { formatWithPrettier } from "./prettier-format"; | 19 | import { formatWithPrettier } from "./prettier-format"; |
| 25 | import { | 20 | import { |
| 26 | getSourceAdapterForOperation, | 21 | expandCatalogTools, |
| 27 | } from "./source-adapters"; | 22 | loadSourceWithCatalog, |
| 28 | import { | 23 | type LoadedSourceCatalogTool, |
| 29 | RuntimeSourceRecipeStoreService, | 24 | } from "./source-catalog-runtime"; |
| 30 | recipeToolMetadata, | ||
| 31 | recipeToolPath, | ||
| 32 | } from "./source-recipes-runtime"; | ||
| 33 | import type { SourceAdapterPersistedOperationMetadata } from "./source-adapters/types"; | ||
| 34 | import { namespaceFromSourceName } from "./source-names"; | ||
| 35 | 25 | ||
| 36 | const sourceInspectOps = { | 26 | const sourceInspectOps = { |
| 37 | bundle: operationErrors("sources.inspect.bundle"), | 27 | bundle: operationErrors("sources.inspect.bundle"), |
| @@ -46,21 +36,6 @@ const tokenize = (value: string): Array<string> => · 8 unmodified lines | |||
| 46 | .split(/[^a-z0-9]+/) | 36 | .split(/[^a-z0-9]+/) |
| 47 | .filter(Boolean); | 37 | .filter(Boolean); |
| 48 | 38 | ||
| 49 | type InspectionToolRecord = { | ||
| 50 | operation: StoredSourceRecipeOperationRecord; | ||
| 51 | metadata: SourceAdapterPersistedOperationMetadata; | ||
| 52 | listItem: SourceInspectionToolListItem; | ||
| 53 | searchText: string; | ||
| 54 | }; | ||
| 55 | |||
| 56 | type ResolvedSourceInspection = { | ||
| 57 | source: Source; | ||
| 58 | namespace: string; | ||
| 59 | pipelineKind: SourceInspection["pipelineKind"]; | ||
| 60 | schemaBundleId: string | null; | ||
| 61 | tools: ReadonlyArray<InspectionToolRecord>; | ||
| 62 | }; | ||
| 63 | |||
| 64 | const formatOptionalJson = (value: string | null) => | 39 | const formatOptionalJson = (value: string | null) => |
| 65 | value === null | 40 | value === null |
| 66 | ? Effect.succeed<string | null>(null) | 41 | ? Effect.succeed<string | null>(null) |
| @@ -71,198 +46,163 @@ const formatOptionalTypeScript = (value: string | undefined) => · 4 unmodified lines | |||
| 71 | ? Effect.succeed<string | undefined>(undefined) | 46 | ? Effect.succeed<string | undefined>(undefined) |
| 72 | : Effect.promise(() => formatWithPrettier(value, "typescript")); | 47 | : Effect.promise(() => formatWithPrettier(value, "typescript")); |
| 73 | 48 | ||
| 74 | const formatToolSummary = (summary: SourceInspectionToolSummary) => | ||
| 75 | Effect.all({ | ||
| 76 | inputType: formatOptionalTypeScript(summary.inputType), | ||
| 77 | outputType: formatOptionalTypeScript(summary.outputType), | ||
| 78 | }).pipe( | ||
| 79 | Effect.map(({ inputType, outputType }) => ({ | ||
| 80 | ...summary, | ||
| 81 | ...(inputType ? { inputType } : {}), | ||
| 82 | ...(outputType ? { outputType } : {}), | ||
| 83 | } satisfies SourceInspectionToolSummary)), | ||
| 84 | ); | ||
| 85 | |||
| 86 | const formatInspectionToolDetail = (detail: SourceInspectionToolDetail) => | 49 | const formatInspectionToolDetail = (detail: SourceInspectionToolDetail) => |
| 87 | Effect.gen(function* () { | 50 | Effect.gen(function* () { |
| 88 | const summary = yield* formatToolSummary(detail.summary); | 51 | const summary = yield* Effect.all({ |
| 52 | inputType: formatOptionalTypeScript(detail.summary.inputType), | ||
| 53 | outputType: formatOptionalTypeScript(detail.summary.outputType), | ||
| 54 | }).pipe( | ||
| 55 | Effect.map(({ inputType, outputType }) => ({ | ||
| 56 | ...detail.summary, | ||
| 57 | ...(inputType ? { inputType } : {}), | ||
| 58 | ...(outputType ? { outputType } : {}), | ||
| 59 | })), | ||
| 60 | ); | ||
| 89 | const detailFields = yield* Effect.all({ | 61 | const detailFields = yield* Effect.all({ |
| 90 | definitionJson: formatOptionalJson(detail.definitionJson), | 62 | definitionJson: formatOptionalJson(detail.definitionJson), |
| 91 | documentationJson: formatOptionalJson(detail.documentationJson), | 63 | documentationJson: formatOptionalJson(detail.documentationJson), |
| 92 | providerDataJson: formatOptionalJson(detail.providerDataJson), | 64 | nativeJson: formatOptionalJson(detail.nativeJson), |
| 93 | inputSchemaJson: formatOptionalJson(detail.inputSchemaJson), | 65 | callSchemaJson: formatOptionalJson(detail.callSchemaJson), |
| 94 | outputSchemaJson: formatOptionalJson(detail.outputSchemaJson), | 66 | resultSchemaJson: formatOptionalJson(detail.resultSchemaJson), |
| 95 | exampleInputJson: formatOptionalJson(detail.exampleInputJson), | 67 | exampleCallJson: formatOptionalJson(detail.exampleCallJson), |
| 96 | exampleOutputJson: formatOptionalJson(detail.exampleOutputJson), | 68 | exampleResultJson: formatOptionalJson(detail.exampleResultJson), |
| 97 | }); | 69 | }); |
| 98 | 70 | ||
| 99 | return { | 71 | return { |
| 100 | ...detail, | 72 | ...detail, |
| 101 | ...detailFields, | ||
| 102 | summary, | 73 | summary, |
| 74 | ...detailFields, | ||
| 103 | } satisfies SourceInspectionToolDetail; | 75 | } satisfies SourceInspectionToolDetail; |
| 104 | }); | 76 | }); |
| 105 | 77 | ||
| 106 | const loadSourceRecipeRecord = (input: { | 78 | const executableDetails = (tool: LoadedSourceCatalogTool) => { |
| 107 | workspaceId: WorkspaceId; | 79 | switch (tool.executable.protocol) { |
| 108 | sourceId: SourceId; | 80 | case "http": |
| 109 | }) => | ||
| 110 | Effect.flatMap(RuntimeSourceRecipeStoreService, (sourceRecipeStore) => | ||
| 111 | Effect.gen(function* () { | ||
| 112 | const recipe = yield* sourceRecipeStore.loadSourceWithRecipe({ | ||
| 113 | workspaceId: input.workspaceId, | ||
| 114 | sourceId: input.sourceId, | ||
| 115 | }).pipe( | ||
| 116 | Effect.mapError((cause) => | ||
| 117 | cause instanceof Error && cause.message.startsWith("Source not found:") | ||
| 118 | ? sourceInspectOps.bundle.notFound( | ||
| 119 | "Source not found", | ||
| 120 | `workspaceId=${input.workspaceId} sourceId=${input.sourceId}`, | ||
| 121 | ) | ||
| 122 | : sourceInspectOps.bundle.unknownStorage( | ||
| 123 | cause, | ||
| 124 | "Failed loading source recipe", | ||
| 125 | ), | ||
| 126 | ), | ||
| 127 | ); | ||
| 128 | |||
| 129 | return { | 81 | return { |
| 130 | source: recipe.source, | 82 | method: tool.executable.method, |
| 131 | operations: recipe.operations, | 83 | pathTemplate: tool.executable.pathTemplate, |
| 132 | schemaBundleId: recipe.schemaBundles.find((bundle) => bundle.bundleKind === "json_schema_ref_map")?.id | 84 | rawToolId: tool.path.split(".").at(-1) ?? null, |
| 133 | ?? recipe.schemaBundles[0]?.id | 85 | operationId: null, |
| 134 | ?? null, | 86 | group: null, |
| 87 | leaf: tool.path.split(".").at(-1) ?? null, | ||
| 88 | tags: tool.capability.surface.tags ?? [], | ||
| 135 | }; | 89 | }; |
| 136 | }), | 90 | case "graphql": |
| 137 | ); | 91 | return { |
| 92 | method: tool.executable.operationType, | ||
| 93 | pathTemplate: tool.executable.rootField, | ||
| 94 | rawToolId: tool.path.split(".").at(-1) ?? null, | ||
| 95 | operationId: tool.executable.rootField, | ||
| 96 | group: null, | ||
| 97 | leaf: tool.executable.rootField, | ||
| 98 | tags: tool.capability.surface.tags ?? [], | ||
| 99 | }; | ||
| 100 | case "mcp": | ||
| 101 | return { | ||
| 102 | method: null, | ||
| 103 | pathTemplate: null, | ||
| 104 | rawToolId: tool.path.split(".").at(-1) ?? null, | ||
| 105 | operationId: tool.executable.toolName, | ||
| 106 | group: null, | ||
| 107 | leaf: tool.executable.toolName, | ||
| 108 | tags: tool.capability.surface.tags ?? [], | ||
| 109 | }; | ||
| 110 | } | ||
| 111 | }; | ||
| 138 | 112 | ||
| 139 | const inspectionToolListItemFromOperation = (input: { | 113 | const inspectionToolListItemFromTool = (tool: LoadedSourceCatalogTool): SourceInspectionToolListItem => ({ |
| 140 | source: Source; | 114 | path: tool.path, |
| 141 | operation: StoredSourceRecipeOperationRecord; | 115 | method: |
| 142 | metadata: SourceAdapterPersistedOperationMetadata; | 116 | tool.executable.protocol === "http" |
| 143 | }): SourceInspectionToolListItem => ({ | 117 | ? tool.executable.method |
| 144 | path: recipeToolPath({ | 118 | : tool.executable.protocol === "graphql" |
| 145 | source: input.source, | 119 | ? tool.executable.operationType |
| 146 | operation: input.operation, | 120 | : null, |
| 147 | }), | ||
| 148 | method: input.metadata.method, | ||
| 149 | }); | 121 | }); |
| 150 | 122 | ||
| 151 | const persistedToolSummaryFromRecipeOperation = (input: { | 123 | const persistedToolSummaryFromTool = (tool: LoadedSourceCatalogTool): SourceInspectionToolSummary => { |
| 152 | source: Source; | 124 | const details = executableDetails(tool); |
| 153 | operation: StoredSourceRecipeOperationRecord; | ||
| 154 | metadata: SourceAdapterPersistedOperationMetadata; | ||
| 155 | includeTypes: boolean; | ||
| 156 | }): SourceInspectionToolSummary => { | ||
| 157 | const path = inspectionToolListItemFromOperation(input).path; | ||
| 158 | const descriptor = input.includeTypes | ||
| 159 | ? getSourceAdapterForOperation(input.operation).createToolDescriptor({ | ||
| 160 | source: input.source, | ||
| 161 | operation: input.operation, | ||
| 162 | path, | ||
| 163 | includeSchemas: false, | ||
| 164 | }) | ||
| 165 | : null; | ||
| 166 | 125 | ||
| 167 | return { | 126 | return { |
| 168 | path, | 127 | path: tool.path, |
| 169 | sourceKey: input.source.id, | 128 | sourceKey: tool.source.id, |
| 170 | ...(input.operation.title ? { title: input.operation.title } : {}), | 129 | ...(tool.capability.surface.title ? { title: tool.capability.surface.title } : {}), |
| 171 | ...(input.operation.description ? { description: input.operation.description } : {}), | 130 | ...(tool.capability.surface.summary || tool.capability.surface.description |
| 172 | providerKind: input.operation.providerKind, | 131 | ? { description: tool.capability.surface.summary ?? tool.capability.surface.description! } |
| 173 | toolId: input.operation.toolId, | 132 | : {}), |
| 174 | rawToolId: input.metadata.rawToolId, | 133 | protocol: tool.executable.protocol, |
| 175 | operationId: input.metadata.operationId, | 134 | toolId: tool.path.split(".").at(-1) ?? tool.path, |
| 176 | group: input.metadata.group, | 135 | rawToolId: details.rawToolId, |
| 177 | leaf: input.metadata.leaf, | 136 | operationId: details.operationId, |
| 178 | tags: [...input.metadata.tags], | 137 | group: details.group, |
| 179 | method: input.metadata.method, | 138 | leaf: details.leaf, |
| 180 | pathTemplate: input.metadata.pathTemplate, | 139 | tags: [...details.tags], |
| 181 | ...(descriptor?.inputType ? { inputType: descriptor.inputType } : {}), | 140 | method: details.method, |
| 182 | ...(descriptor?.outputType ? { outputType: descriptor.outputType } : {}), | 141 | pathTemplate: details.pathTemplate, |
| 142 | ...(tool.descriptor.inputType ? { inputType: tool.descriptor.inputType } : {}), | ||
| 143 | ...(tool.descriptor.outputType ? { outputType: tool.descriptor.outputType } : {}), | ||
| 183 | }; | 144 | }; |
| 184 | }; | 145 | }; |
| 185 | 146 | ||
| 186 | const inspectionToolDetailFromOperation = (input: { | 147 | const inspectionToolDetailFromTool = (tool: LoadedSourceCatalogTool): SourceInspectionToolDetail => ({ |
| 187 | source: Source; | 148 | summary: persistedToolSummaryFromTool(tool), |
| 188 | operation: StoredSourceRecipeOperationRecord; | 149 | definitionJson: JSON.stringify({ |
| 189 | metadata: SourceAdapterPersistedOperationMetadata; | 150 | capability: tool.capability, |
| 190 | schemaBundleId: string | null; | 151 | executable: tool.executable, |
| 191 | }): SourceInspectionToolDetail => { | 152 | }), |
| 192 | const summary = persistedToolSummaryFromRecipeOperation({ | 153 | documentationJson: JSON.stringify({ |
| 193 | source: input.source, | 154 | capabilityDocs: { |
| 194 | operation: input.operation, | 155 | summary: tool.capability.surface.summary, |
| 195 | metadata: input.metadata, | 156 | description: tool.capability.surface.description, |
| 196 | includeTypes: true, | 157 | }, |
| 197 | }); | 158 | }), |
| 198 | 159 | nativeJson: tool.executable.native?.[0]?.value | |
| 199 | return { | 160 | ? JSON.stringify(tool.executable.native[0]!.value) |
| 200 | summary, | 161 | : null, |
| 201 | definitionJson: null, | 162 | callSchemaJson: tool.descriptor.inputSchema |
| 202 | documentationJson: null, | 163 | ? JSON.stringify(tool.descriptor.inputSchema) |
| 203 | providerDataJson: input.operation.providerDataJson, | 164 | : null, |
| 204 | inputSchemaJson: input.operation.inputSchemaJson, | 165 | resultSchemaJson: tool.descriptor.outputSchema |
| 205 | outputSchemaJson: input.operation.outputSchemaJson, | 166 | ? JSON.stringify(tool.descriptor.outputSchema) |
| 206 | schemaBundleId: input.schemaBundleId, | 167 | : null, |
| 207 | exampleInputJson: null, | 168 | exampleCallJson: null, |
| 208 | exampleOutputJson: null, | 169 | exampleResultJson: null, |
| 209 | } satisfies SourceInspectionToolDetail; | 170 | }); |
| 210 | }; | ||
| 211 | 171 | ||
| 212 | const resolveSourceInspection = (input: { | 172 | const resolveSourceInspection = (input: { |
| 213 | workspaceId: WorkspaceId; | 173 | workspaceId: WorkspaceId; |
| 214 | sourceId: SourceId; | 174 | sourceId: SourceId; |
| 215 | }) => | 175 | }) => |
| 216 | Effect.gen(function* () { | 176 | Effect.gen(function* () { |
| 217 | const { source, operations, schemaBundleId } = yield* loadSourceRecipeRecord(input); | 177 | const catalogEntry = yield* loadSourceWithCatalog({ |
| 218 | const namespace = source.namespace ?? namespaceFromSourceName(source.name); | 178 | workspaceId: input.workspaceId, |
| 179 | sourceId: input.sourceId, | ||
| 180 | }); | ||
| 181 | const tools = yield* expandCatalogTools({ | ||
| 182 | catalogs: [catalogEntry], | ||
| 183 | includeSchemas: true, | ||
| 184 | }); | ||
| 219 | 185 | ||
| 220 | return { | 186 | return { |
| 221 | source, | 187 | source: catalogEntry.source, |
| 222 | namespace, | 188 | namespace: catalogEntry.source.namespace ?? "", |
| 223 | pipelineKind: "persisted", | 189 | pipelineKind: "ir" as const, |
| 224 | schemaBundleId, | 190 | tools, |
| 225 | tools: yield* Effect.forEach(operations, (operation) => | 191 | }; |
| 226 | Effect.gen(function* () { | ||
| 227 | const path = recipeToolPath({ | ||
| 228 | source, | ||
| 229 | operation, | ||
| 230 | }); | ||
| 231 | const metadata = yield* recipeToolMetadata({ | ||
| 232 | source, | ||
| 233 | operation, | ||
| 234 | path, | ||
| 235 | }); | ||
| 236 | const listItem = inspectionToolListItemFromOperation({ | ||
| 237 | source, | ||
| 238 | operation, | ||
| 239 | metadata, | ||
| 240 | }); | ||
| 241 | |||
| 242 | return { | ||
| 243 | operation, | ||
| 244 | metadata, | ||
| 245 | listItem, | ||
| 246 | searchText: metadata.searchText, | ||
| 247 | } satisfies InspectionToolRecord; | ||
| 248 | }) | ||
| 249 | ), | ||
| 250 | } satisfies ResolvedSourceInspection; | ||
| 251 | }); | 192 | }); |
| 252 | 193 | ||
| 253 | const scoreTool = (input: { | 194 | const scoreTool = (input: { |
| 254 | queryTokens: ReadonlyArray<string>; | 195 | queryTokens: ReadonlyArray<string>; |
| 255 | tool: InspectionToolRecord; | 196 | tool: LoadedSourceCatalogTool; |
| 256 | }): SourceInspectionDiscoverResultItem | null => { | 197 | }): SourceInspectionDiscoverResultItem | null => { |
| 257 | let score = 0; | 198 | let score = 0; |
| 258 | const reasons: Array<string> = []; | 199 | const reasons: Array<string> = []; |
| 259 | const pathTokens = tokenize(input.tool.listItem.path); | 200 | const summary = persistedToolSummaryFromTool(input.tool); |
| 260 | const titleTokens = tokenize(input.tool.operation.title ?? ""); | 201 | const pathTokens = tokenize(summary.path); |
| 261 | const descriptionTokens = tokenize(input.tool.operation.description ?? ""); | 202 | const titleTokens = tokenize(summary.title ?? ""); |
| 262 | const tagTokens = input.tool.metadata.tags.flatMap(tokenize); | 203 | const descriptionTokens = tokenize(summary.description ?? ""); |
| 263 | const methodPathTokens = tokenize( | 204 | const tagTokens = summary.tags.flatMap(tokenize); |
| 264 | `${input.tool.listItem.method ?? ""} ${input.tool.metadata.pathTemplate ?? ""}`, | 205 | const methodPathTokens = tokenize(`${summary.method ?? ""} ${summary.pathTemplate ?? ""}`); |
| 265 | ); | ||
| 266 | 206 | ||
| 267 | for (const token of input.queryTokens) { | 207 | for (const token of input.queryTokens) { |
| 268 | if (pathTokens.includes(token)) { | 208 | if (pathTokens.includes(token)) { |
| @@ -296,13 +236,13 @@ const scoreTool = (input: { · 27 unmodified lines | |||
| 296 | } | 236 | } |
| 297 | 237 | ||
| 298 | return { | 238 | return { |
| 299 | path: input.tool.listItem.path, | 239 | path: input.tool.path, |
| 300 | score, | 240 | score, |
| 301 | ...(input.tool.operation.description | 241 | ...(summary.description ? { description: summary.description } : {}), |
| 302 | ? { description: input.tool.operation.description } | 242 | ...(summary.inputType ? { inputType: summary.inputType } : {}), |
| 303 | : {}), | 243 | ...(summary.outputType ? { outputType: summary.outputType } : {}), |
| 304 | reasons, | 244 | reasons, |
| 305 | } satisfies SourceInspectionDiscoverResultItem; | 245 | }; |
| 306 | }; | 246 | }; |
| 307 | 247 | ||
| 308 | const mapInspectionError = ( | 248 | const mapInspectionError = ( |
| @@ -316,6 +256,7 @@ const mapInspectionError = ( · 7 unmodified lines | |||
| 316 | if (cause instanceof ControlPlaneNotFoundError) { | 256 | if (cause instanceof ControlPlaneNotFoundError) { |
| 317 | return cause; | 257 | return cause; |
| 318 | } | 258 | } |
| 259 | |||
| 319 | if (cause instanceof ControlPlaneStorageError) { | 260 | if (cause instanceof ControlPlaneStorageError) { |
| 320 | return cause; | 261 | return cause; |
| 321 | } | 262 | } |
| @@ -335,7 +276,7 @@ export const getSourceInspection = (input: { · 13 unmodified lines | |||
| 335 | namespace: inspection.namespace, | 276 | namespace: inspection.namespace, |
| 336 | pipelineKind: inspection.pipelineKind, | 277 | pipelineKind: inspection.pipelineKind, |
| 337 | toolCount: inspection.tools.length, | 278 | toolCount: inspection.tools.length, |
| 338 | tools: inspection.tools.map((tool) => tool.listItem), | 279 | tools: inspection.tools.map((tool) => inspectionToolListItemFromTool(tool)), |
| 339 | } satisfies SourceInspection; | 280 | } satisfies SourceInspection; |
| 340 | }).pipe( | 281 | }).pipe( |
| 341 | Effect.mapError((cause) => | 282 | Effect.mapError((cause) => |
| @@ -356,7 +297,7 @@ export const getSourceInspectionToolDetail = (input: { · 14 unmodified lines | |||
| 356 | workspaceId: input.workspaceId, | 297 | workspaceId: input.workspaceId, |
| 357 | sourceId: input.sourceId, | 298 | sourceId: input.sourceId, |
| 358 | }); | 299 | }); |
| 359 | const tool = inspection.tools.find((candidate) => candidate.listItem.path === input.toolPath); | 300 | const tool = inspection.tools.find((candidate) => candidate.path === input.toolPath); |
| 360 | 301 | ||
| 361 | if (!tool) { | 302 | if (!tool) { |
| 362 | return yield* Effect.fail( | 303 | return yield* Effect.fail( |
| @@ -367,12 +308,7 @@ export const getSourceInspectionToolDetail = (input: { · 4 unmodified lines | |||
| 367 | ); | 308 | ); |
| 368 | } | 309 | } |
| 369 | 310 | ||
| 370 | return yield* formatInspectionToolDetail(inspectionToolDetailFromOperation({ | 311 | return yield* formatInspectionToolDetail(inspectionToolDetailFromTool(tool)); |
| 371 | source: inspection.source, | ||
| 372 | operation: tool.operation, | ||
| 373 | metadata: tool.metadata, | ||
| 374 | schemaBundleId: inspection.schemaBundleId, | ||
| 375 | })); | ||
| 376 | }).pipe( | 312 | }).pipe( |
| 377 | Effect.mapError((cause) => | 313 | Effect.mapError((cause) => |
| 378 | mapInspectionError( | 314 | mapInspectionError( |
| @@ -382,57 +318,6 @@ export const getSourceInspectionToolDetail = (input: { · 3 unmodified lines | |||
| 382 | )), | 318 | )), |
| 383 | ); | 319 | ); |
| 384 | 320 | ||
| 385 | export const getSourceInspectionSchemaBundle = (input: { | ||
| 386 | workspaceId: WorkspaceId; | ||
| 387 | sourceId: SourceId; | ||
| 388 | schemaBundleId: string; | ||
| 389 | }) => | ||
| 390 | Effect.flatMap(RuntimeSourceRecipeStoreService, (sourceRecipeStore) => | ||
| 391 | Effect.gen(function* () { | ||
| 392 | const recipe = yield* sourceRecipeStore.loadSourceWithRecipe({ | ||
| 393 | workspaceId: input.workspaceId, | ||
| 394 | sourceId: input.sourceId, | ||
| 395 | }).pipe( | ||
| 396 | Effect.mapError((cause) => | ||
| 397 | cause instanceof Error && cause.message.startsWith("Source not found:") | ||
| 398 | ? sourceInspectOps.tool.notFound( | ||
| 399 | "Source not found", | ||
| 400 | `workspaceId=${input.workspaceId} sourceId=${input.sourceId}`, | ||
| 401 | ) | ||
| 402 | : sourceInspectOps.tool.unknownStorage( | ||
| 403 | cause, | ||
| 404 | "Failed loading source recipe", | ||
| 405 | ), | ||
| 406 | ), | ||
| 407 | ); | ||
| 408 | const schemaBundle = recipe.schemaBundles.find( | ||
| 409 | (candidate) => candidate.id === SourceRecipeSchemaBundleIdSchema.make(input.schemaBundleId), | ||
| 410 | ); | ||
| 411 | if (!schemaBundle) { | ||
| 412 | return yield* Effect.fail( | ||
| 413 | sourceInspectOps.tool.notFound( | ||
| 414 | "Schema bundle not found", | ||
| 415 | `workspaceId=${input.workspaceId} sourceId=${input.sourceId} schemaBundleId=${input.schemaBundleId}`, | ||
| 416 | ), | ||
| 417 | ); | ||
| 418 | } | ||
| 419 | |||
| 420 | return { | ||
| 421 | id: schemaBundle.id, | ||
| 422 | kind: schemaBundle.bundleKind, | ||
| 423 | hash: schemaBundle.contentHash, | ||
| 424 | refsJson: schemaBundle.refsJson, | ||
| 425 | } satisfies SourceInspectionSchemaBundle; | ||
| 426 | }), | ||
| 427 | ).pipe( | ||
| 428 | Effect.mapError((cause) => | ||
| 429 | mapInspectionError( | ||
| 430 | sourceInspectOps.tool, | ||
| 431 | cause, | ||
| 432 | "Failed loading source inspection schema bundle", | ||
| 433 | )), | ||
| 434 | ); | ||
| 435 | |||
| 436 | export const discoverSourceInspectionTools = (input: { | 321 | export const discoverSourceInspectionTools = (input: { |
| 437 | workspaceId: WorkspaceId; | 322 | workspaceId: WorkspaceId; |
| 438 | sourceId: SourceId; | 323 | sourceId: SourceId; |
| @@ -1,27 +0,0 @@ | |||
| 1 | import { sha256Hex } from "@executor/codemode-core"; | ||
| 2 | |||
| 3 | import type { | ||
| 4 | StoredSourceRecipeDocumentRecord, | ||
| 5 | StoredSourceRecipeOperationRecord, | ||
| 6 | StoredSourceRecipeSchemaBundleRecord, | ||
| 7 | } from "#schema"; | ||
| 8 | |||
| 9 | export const normalizeSearchText = ( | ||
| 10 | ...parts: ReadonlyArray<string | null | undefined> | ||
| 11 | ): string => | ||
| 12 | parts | ||
| 13 | .flatMap((part) => (part ? [part.trim()] : [])) | ||
| 14 | .filter(Boolean) | ||
| 15 | .join(" ") | ||
| 16 | .toLowerCase(); | ||
| 17 | |||
| 18 | export const contentHash = (value: string): string => sha256Hex(value); | ||
| 19 | |||
| 20 | export type SourceRecipeMaterialization = { | ||
| 21 | manifestJson: string | null; | ||
| 22 | manifestHash: string | null; | ||
| 23 | sourceHash: string | null; | ||
| 24 | documents: readonly StoredSourceRecipeDocumentRecord[]; | ||
| 25 | schemaBundles: readonly StoredSourceRecipeSchemaBundleRecord[]; | ||
| 26 | operations: readonly StoredSourceRecipeOperationRecord[]; | ||
| 27 | }; | ||
| @@ -1,598 +0,0 @@ | |||
| 1 | import { | ||
| 2 | type ToolCatalogEntry, | ||
| 3 | type ToolDescriptor, | ||
| 4 | } from "@executor/codemode-core"; | ||
| 5 | import type { | ||
| 6 | AccountId, | ||
| 7 | Source, | ||
| 8 | StoredSourceRecord, | ||
| 9 | StoredSourceRecipeDocumentRecord, | ||
| 10 | StoredSourceRecipeOperationRecord, | ||
| 11 | StoredSourceRecipeSchemaBundleRecord, | ||
| 12 | StoredSourceRecipeRevisionRecord, | ||
| 13 | WorkspaceId, | ||
| 14 | } from "#schema"; | ||
| 15 | import * as Context from "effect/Context"; | ||
| 16 | import * as Effect from "effect/Effect"; | ||
| 17 | import * as Layer from "effect/Layer"; | ||
| 18 | import { LocalSourceArtifactMissingError } from "./local-errors"; | ||
| 19 | import { | ||
| 20 | RuntimeLocalWorkspaceService, | ||
| 21 | type RuntimeLocalWorkspaceState, | ||
| 22 | } from "./local-runtime-context"; | ||
| 23 | import { | ||
| 24 | SourceArtifactStore, | ||
| 25 | type SourceArtifactStoreShape, | ||
| 26 | } from "./local-storage"; | ||
| 27 | import { namespaceFromSourceName } from "./source-names"; | ||
| 28 | import { | ||
| 29 | getSourceAdapterForOperation, | ||
| 30 | getSourceAdapterForSource, | ||
| 31 | } from "./source-adapters"; | ||
| 32 | import type { SourceAdapterPersistedOperationMetadata } from "./source-adapters/types"; | ||
| 33 | import { firstSchemaBundle } from "./source-adapters/shared"; | ||
| 34 | import { | ||
| 35 | RuntimeSourceStoreService, | ||
| 36 | type RuntimeSourceStore, | ||
| 37 | } from "./source-store"; | ||
| 38 | |||
| 39 | type RecipeManifest = unknown | null; | ||
| 40 | |||
| 41 | export type LoadedSourceRecipe = { | ||
| 42 | source: Source; | ||
| 43 | sourceRecord: StoredSourceRecord; | ||
| 44 | revision: StoredSourceRecipeRevisionRecord; | ||
| 45 | documents: readonly StoredSourceRecipeDocumentRecord[]; | ||
| 46 | schemaBundles: readonly StoredSourceRecipeSchemaBundleRecord[]; | ||
| 47 | operations: readonly StoredSourceRecipeOperationRecord[]; | ||
| 48 | manifest: RecipeManifest; | ||
| 49 | }; | ||
| 50 | |||
| 51 | export type LoadedSourceRecipeTool = { | ||
| 52 | path: string; | ||
| 53 | searchNamespace: string; | ||
| 54 | searchText: string; | ||
| 55 | source: Source; | ||
| 56 | sourceRecord: StoredSourceRecord; | ||
| 57 | revision: StoredSourceRecipeRevisionRecord; | ||
| 58 | operation: StoredSourceRecipeOperationRecord; | ||
| 59 | metadata: SourceAdapterPersistedOperationMetadata; | ||
| 60 | schemaBundleId: string | null; | ||
| 61 | manifest: RecipeManifest; | ||
| 62 | descriptor: ToolDescriptor; | ||
| 63 | }; | ||
| 64 | |||
| 65 | export type LoadedSourceRecipeToolIndexEntry = { | ||
| 66 | path: string; | ||
| 67 | searchNamespace: string; | ||
| 68 | searchText: string; | ||
| 69 | source: Source; | ||
| 70 | sourceRecord: StoredSourceRecord; | ||
| 71 | operation: StoredSourceRecipeOperationRecord; | ||
| 72 | metadata: SourceAdapterPersistedOperationMetadata; | ||
| 73 | schemaBundleId: string | null; | ||
| 74 | descriptor: ToolDescriptor; | ||
| 75 | }; | ||
| 76 | |||
| 77 | export const recipeToolCatalogEntry = (input: { | ||
| 78 | tool: LoadedSourceRecipeToolIndexEntry; | ||
| 79 | score: (queryTokens: readonly string[]) => number; | ||
| 80 | }): ToolCatalogEntry => ({ | ||
| 81 | descriptor: input.tool.descriptor, | ||
| 82 | namespace: input.tool.searchNamespace, | ||
| 83 | searchText: input.tool.searchText, | ||
| 84 | score: input.score, | ||
| 85 | }); | ||
| 86 | |||
| 87 | const parseManifestForRecipe = (input: { | ||
| 88 | source: Source; | ||
| 89 | revision: StoredSourceRecipeRevisionRecord; | ||
| 90 | }): Effect.Effect<RecipeManifest, Error, never> => | ||
| 91 | getSourceAdapterForSource(input.source).parseManifest({ | ||
| 92 | source: input.source, | ||
| 93 | manifestJson: input.revision.manifestJson, | ||
| 94 | }); | ||
| 95 | |||
| 96 | const catalogNamespaceFromPath = (path: string): string => { | ||
| 97 | const [first, second] = path.split("."); | ||
| 98 | return second ? `${first}.${second}` : first; | ||
| 99 | }; | ||
| 100 | |||
| 101 | export const recipeToolPath = (input: { | ||
| 102 | source: Source; | ||
| 103 | operation: StoredSourceRecipeOperationRecord; | ||
| 104 | }): string => { | ||
| 105 | const namespace = input.source.namespace ?? namespaceFromSourceName(input.source.name); | ||
| 106 | return namespace ? `${namespace}.${input.operation.toolId}` : input.operation.toolId; | ||
| 107 | }; | ||
| 108 | |||
| 109 | export const recipeToolSearchNamespace = (input: { | ||
| 110 | source: Source; | ||
| 111 | path: string; | ||
| 112 | operation: StoredSourceRecipeOperationRecord; | ||
| 113 | }): string => | ||
| 114 | getSourceAdapterForOperation(input.operation).searchNamespace?.({ | ||
| 115 | source: input.source, | ||
| 116 | path: input.path, | ||
| 117 | operation: input.operation, | ||
| 118 | }) | ||
| 119 | ?? catalogNamespaceFromPath(input.path); | ||
| 120 | |||
| 121 | export const recipeToolDescriptor = (input: { | ||
| 122 | source: Source; | ||
| 123 | operation: StoredSourceRecipeOperationRecord; | ||
| 124 | path: string; | ||
| 125 | schemaBundleId?: string | null; | ||
| 126 | includeSchemas: boolean; | ||
| 127 | }): ToolDescriptor => | ||
| 128 | getSourceAdapterForOperation(input.operation).createToolDescriptor(input); | ||
| 129 | |||
| 130 | export const recipeToolMetadata = (input: { | ||
| 131 | source: Source; | ||
| 132 | operation: StoredSourceRecipeOperationRecord; | ||
| 133 | path: string; | ||
| 134 | }): Effect.Effect<SourceAdapterPersistedOperationMetadata, Error, never> => | ||
| 135 | getSourceAdapterForOperation(input.operation).describePersistedOperation(input); | ||
| 136 | |||
| 137 | const sourceRecipeDocumentForSource = (input: { | ||
| 138 | source: Source; | ||
| 139 | documents: readonly StoredSourceRecipeDocumentRecord[]; | ||
| 140 | }): StoredSourceRecipeDocumentRecord | null => { | ||
| 141 | const preferredKind = getSourceAdapterForSource(input.source).primaryDocumentKind; | ||
| 142 | |||
| 143 | if (preferredKind === null) { | ||
| 144 | return null; | ||
| 145 | } | ||
| 146 | |||
| 147 | return input.documents.find((document) => document.documentKind === preferredKind) ?? null; | ||
| 148 | }; | ||
| 149 | |||
| 150 | export const recipePrimaryDocumentText = (input: { | ||
| 151 | source: Source; | ||
| 152 | documents: readonly StoredSourceRecipeDocumentRecord[]; | ||
| 153 | }): string | null => | ||
| 154 | sourceRecipeDocumentForSource(input)?.contentText ?? null; | ||
| 155 | |||
| 156 | const primarySchemaBundleForRevision = (input: { | ||
| 157 | source: Source; | ||
| 158 | schemaBundles: readonly StoredSourceRecipeSchemaBundleRecord[]; | ||
| 159 | }): StoredSourceRecipeSchemaBundleRecord | null => { | ||
| 160 | const selected = firstSchemaBundle({ | ||
| 161 | schemaBundles: input.schemaBundles.map((schemaBundle) => ({ | ||
| 162 | id: schemaBundle.id, | ||
| 163 | kind: schemaBundle.bundleKind, | ||
| 164 | hash: schemaBundle.contentHash, | ||
| 165 | refsJson: schemaBundle.refsJson, | ||
| 166 | })), | ||
| 167 | preferredKind: getSourceAdapterForSource(input.source).primarySchemaBundleKind, | ||
| 168 | }); | ||
| 169 | |||
| 170 | return selected | ||
| 171 | ? input.schemaBundles.find((schemaBundle) => schemaBundle.id === selected.id) ?? null | ||
| 172 | : null; | ||
| 173 | }; | ||
| 174 | |||
| 175 | const sourceRecordFromRecipeArtifact = (input: { | ||
| 176 | source: Source; | ||
| 177 | artifact: { | ||
| 178 | recipeId: StoredSourceRecord["recipeId"]; | ||
| 179 | revision: StoredSourceRecipeRevisionRecord; | ||
| 180 | }; | ||
| 181 | }): StoredSourceRecord => ({ | ||
| 182 | id: input.source.id, | ||
| 183 | workspaceId: input.source.workspaceId, | ||
| 184 | recipeId: input.artifact.recipeId, | ||
| 185 | recipeRevisionId: input.artifact.revision.id, | ||
| 186 | name: input.source.name, | ||
| 187 | kind: input.source.kind, | ||
| 188 | endpoint: input.source.endpoint, | ||
| 189 | status: input.source.status, | ||
| 190 | enabled: input.source.enabled, | ||
| 191 | namespace: input.source.namespace, | ||
| 192 | importAuthPolicy: input.source.importAuthPolicy, | ||
| 193 | bindingConfigJson: getSourceAdapterForSource(input.source).serializeBindingConfig(input.source), | ||
| 194 | sourceHash: input.source.sourceHash, | ||
| 195 | lastError: input.source.lastError, | ||
| 196 | createdAt: input.source.createdAt, | ||
| 197 | updatedAt: input.source.updatedAt, | ||
| 198 | }); | ||
| 199 | |||
| 200 | type RuntimeSourceRecipeStoreShape = { | ||
| 201 | loadWorkspaceSourceRecipes: (input: { | ||
| 202 | workspaceId: WorkspaceId; | ||
| 203 | actorAccountId?: AccountId | null; | ||
| 204 | }) => Effect.Effect<readonly LoadedSourceRecipe[], Error, never>; | ||
| 205 | loadSourceWithRecipe: (input: { | ||
| 206 | workspaceId: WorkspaceId; | ||
| 207 | sourceId: Source["id"]; | ||
| 208 | actorAccountId?: AccountId | null; | ||
| 209 | }) => Effect.Effect<LoadedSourceRecipe, Error | LocalSourceArtifactMissingError, never>; | ||
| 210 | loadWorkspaceSourceRecipeToolIndex: (input: { | ||
| 211 | workspaceId: WorkspaceId; | ||
| 212 | actorAccountId?: AccountId | null; | ||
| 213 | includeSchemas: boolean; | ||
| 214 | }) => Effect.Effect<readonly LoadedSourceRecipeToolIndexEntry[], Error, never>; | ||
| 215 | loadWorkspaceSourceRecipeToolByPath: (input: { | ||
| 216 | workspaceId: WorkspaceId; | ||
| 217 | path: string; | ||
| 218 | actorAccountId?: AccountId | null; | ||
| 219 | includeSchemas: boolean; | ||
| 220 | }) => Effect.Effect<LoadedSourceRecipeToolIndexEntry | null, Error, never>; | ||
| 221 | loadWorkspaceSchemaBundle: (input: { | ||
| 222 | workspaceId: WorkspaceId; | ||
| 223 | id: string; | ||
| 224 | }) => Effect.Effect< | ||
| 225 | { id: string; kind: string; hash: string; refsJson: string } | null, | ||
| 226 | Error, | ||
| 227 | never | ||
| 228 | >; | ||
| 229 | }; | ||
| 230 | |||
| 231 | export type RuntimeSourceRecipeStore = RuntimeSourceRecipeStoreShape; | ||
| 232 | |||
| 233 | export class RuntimeSourceRecipeStoreService extends Context.Tag( | ||
| 234 | "#runtime/RuntimeSourceRecipeStoreService", | ||
| 235 | )<RuntimeSourceRecipeStoreService, RuntimeSourceRecipeStoreShape>() {} | ||
| 236 | |||
| 237 | type RuntimeSourceRecipeStoreDeps = { | ||
| 238 | runtimeLocalWorkspace: RuntimeLocalWorkspaceState; | ||
| 239 | sourceStore: RuntimeSourceStore; | ||
| 240 | sourceArtifactStore: SourceArtifactStoreShape; | ||
| 241 | }; | ||
| 242 | |||
| 243 | type SourceRecipeRuntimeServices = | ||
| 244 | | RuntimeLocalWorkspaceService | ||
| 245 | | RuntimeSourceStoreService | ||
| 246 | | SourceArtifactStore; | ||
| 247 | |||
| 248 | const ensureRuntimeRecipeWorkspace = ( | ||
| 249 | deps: RuntimeSourceRecipeStoreDeps, | ||
| 250 | workspaceId: WorkspaceId, | ||
| 251 | ) => { | ||
| 252 | if (deps.runtimeLocalWorkspace.installation.workspaceId !== workspaceId) { | ||
| 253 | return Effect.fail( | ||
| 254 | new Error( | ||
| 255 | `Runtime local workspace mismatch: expected ${workspaceId}, got ${deps.runtimeLocalWorkspace.installation.workspaceId}`, | ||
| 256 | ), | ||
| 257 | ); | ||
| 258 | } | ||
| 259 | |||
| 260 | return Effect.succeed(deps.runtimeLocalWorkspace.context); | ||
| 261 | }; | ||
| 262 | |||
| 263 | const loadWorkspaceSourceRecipesWithDeps = (deps: RuntimeSourceRecipeStoreDeps, input: { | ||
| 264 | workspaceId: WorkspaceId; | ||
| 265 | actorAccountId?: AccountId | null; | ||
| 266 | }): Effect.Effect<readonly LoadedSourceRecipe[], Error, never> => | ||
| 267 | Effect.gen(function* () { | ||
| 268 | const workspaceContext = yield* ensureRuntimeRecipeWorkspace( | ||
| 269 | deps, | ||
| 270 | input.workspaceId, | ||
| 271 | ); | ||
| 272 | const sources = yield* deps.sourceStore.loadSourcesInWorkspace( | ||
| 273 | input.workspaceId, | ||
| 274 | { | ||
| 275 | actorAccountId: input.actorAccountId, | ||
| 276 | }, | ||
| 277 | ); | ||
| 278 | const localRecipes = yield* Effect.forEach(sources, (source) => | ||
| 279 | Effect.gen(function* () { | ||
| 280 | const artifact = yield* deps.sourceArtifactStore.read({ | ||
| 281 | context: workspaceContext, | ||
| 282 | sourceId: source.id, | ||
| 283 | }); | ||
| 284 | if (artifact === null) { | ||
| 285 | return null; | ||
| 286 | } | ||
| 287 | |||
| 288 | const manifest = yield* parseManifestForRecipe({ | ||
| 289 | source, | ||
| 290 | revision: artifact.revision, | ||
| 291 | }); | ||
| 292 | |||
| 293 | return { | ||
| 294 | source, | ||
| 295 | sourceRecord: sourceRecordFromRecipeArtifact({ source, artifact }), | ||
| 296 | revision: artifact.revision, | ||
| 297 | documents: artifact.documents, | ||
| 298 | schemaBundles: artifact.schemaBundles, | ||
| 299 | operations: artifact.operations, | ||
| 300 | manifest, | ||
| 301 | } satisfies LoadedSourceRecipe; | ||
| 302 | }), | ||
| 303 | ); | ||
| 304 | return localRecipes.filter((recipe): recipe is LoadedSourceRecipe => recipe !== null); | ||
| 305 | }); | ||
| 306 | |||
| 307 | const loadSourceWithRecipeWithDeps = (deps: RuntimeSourceRecipeStoreDeps, input: { | ||
| 308 | workspaceId: WorkspaceId; | ||
| 309 | sourceId: Source["id"]; | ||
| 310 | actorAccountId?: AccountId | null; | ||
| 311 | }): Effect.Effect<LoadedSourceRecipe, Error | LocalSourceArtifactMissingError, never> => | ||
| 312 | Effect.gen(function* () { | ||
| 313 | const workspaceContext = yield* ensureRuntimeRecipeWorkspace( | ||
| 314 | deps, | ||
| 315 | input.workspaceId, | ||
| 316 | ); | ||
| 317 | const source = yield* deps.sourceStore.loadSourceById({ | ||
| 318 | workspaceId: input.workspaceId, | ||
| 319 | sourceId: input.sourceId, | ||
| 320 | actorAccountId: input.actorAccountId, | ||
| 321 | }); | ||
| 322 | const artifact = yield* deps.sourceArtifactStore.read({ | ||
| 323 | context: workspaceContext, | ||
| 324 | sourceId: source.id, | ||
| 325 | }); | ||
| 326 | if (artifact === null) { | ||
| 327 | return yield* Effect.fail( | ||
| 328 | new LocalSourceArtifactMissingError({ | ||
| 329 | message: `Recipe artifact missing for source ${input.sourceId}`, | ||
| 330 | sourceId: input.sourceId, | ||
| 331 | }), | ||
| 332 | ); | ||
| 333 | } | ||
| 334 | |||
| 335 | const manifest = yield* parseManifestForRecipe({ | ||
| 336 | source, | ||
| 337 | revision: artifact.revision, | ||
| 338 | }); | ||
| 339 | |||
| 340 | return { | ||
| 341 | source, | ||
| 342 | sourceRecord: sourceRecordFromRecipeArtifact({ source, artifact }), | ||
| 343 | revision: artifact.revision, | ||
| 344 | documents: artifact.documents, | ||
| 345 | schemaBundles: artifact.schemaBundles, | ||
| 346 | operations: artifact.operations, | ||
| 347 | manifest, | ||
| 348 | } satisfies LoadedSourceRecipe; | ||
| 349 | }); | ||
| 350 | |||
| 351 | export const loadWorkspaceSourceRecipes = (input: { | ||
| 352 | workspaceId: WorkspaceId; | ||
| 353 | actorAccountId?: AccountId | null; | ||
| 354 | }): Effect.Effect<readonly LoadedSourceRecipe[], Error, SourceRecipeRuntimeServices> => | ||
| 355 | Effect.gen(function* () { | ||
| 356 | const runtimeLocalWorkspace = yield* RuntimeLocalWorkspaceService; | ||
| 357 | const sourceStore = yield* RuntimeSourceStoreService; | ||
| 358 | const sourceArtifactStore = yield* SourceArtifactStore; | ||
| 359 | |||
| 360 | return yield* loadWorkspaceSourceRecipesWithDeps( | ||
| 361 | { | ||
| 362 | runtimeLocalWorkspace, | ||
| 363 | sourceStore, | ||
| 364 | sourceArtifactStore, | ||
| 365 | }, | ||
| 366 | input, | ||
| 367 | ); | ||
| 368 | }); | ||
| 369 | |||
| 370 | export const loadSourceWithRecipe = (input: { | ||
| 371 | workspaceId: WorkspaceId; | ||
| 372 | sourceId: Source["id"]; | ||
| 373 | actorAccountId?: AccountId | null; | ||
| 374 | }): Effect.Effect< | ||
| 375 | LoadedSourceRecipe, | ||
| 376 | Error | LocalSourceArtifactMissingError, | ||
| 377 | SourceRecipeRuntimeServices | ||
| 378 | > => | ||
| 379 | Effect.gen(function* () { | ||
| 380 | const runtimeLocalWorkspace = yield* RuntimeLocalWorkspaceService; | ||
| 381 | const sourceStore = yield* RuntimeSourceStoreService; | ||
| 382 | const sourceArtifactStore = yield* SourceArtifactStore; | ||
| 383 | |||
| 384 | return yield* loadSourceWithRecipeWithDeps( | ||
| 385 | { | ||
| 386 | runtimeLocalWorkspace, | ||
| 387 | sourceStore, | ||
| 388 | sourceArtifactStore, | ||
| 389 | }, | ||
| 390 | input, | ||
| 391 | ); | ||
| 392 | }); | ||
| 393 | |||
| 394 | export const expandRecipeTools = (input: { | ||
| 395 | recipes: readonly LoadedSourceRecipe[]; | ||
| 396 | includeSchemas: boolean; | ||
| 397 | }): Effect.Effect<readonly LoadedSourceRecipeTool[], Error, never> => | ||
| 398 | Effect.map( | ||
| 399 | Effect.forEach(input.recipes, (recipe) => | ||
| 400 | Effect.forEach(recipe.operations, (operation) => | ||
| 401 | Effect.gen(function* () { | ||
| 402 | const path = recipeToolPath({ | ||
| 403 | source: recipe.source, | ||
| 404 | operation, | ||
| 405 | }); | ||
| 406 | const searchNamespace = recipeToolSearchNamespace({ | ||
| 407 | source: recipe.source, | ||
| 408 | path, | ||
| 409 | operation, | ||
| 410 | }); | ||
| 411 | const schemaBundleId = primarySchemaBundleForRevision({ | ||
| 412 | source: recipe.source, | ||
| 413 | schemaBundles: recipe.schemaBundles, | ||
| 414 | })?.id ?? null; | ||
| 415 | const metadata = yield* recipeToolMetadata({ | ||
| 416 | source: recipe.source, | ||
| 417 | operation, | ||
| 418 | path, | ||
| 419 | }); | ||
| 420 | |||
| 421 | return { | ||
| 422 | path, | ||
| 423 | searchNamespace, | ||
| 424 | searchText: [ | ||
| 425 | path, | ||
| 426 | searchNamespace, | ||
| 427 | recipe.source.name, | ||
| 428 | metadata.searchText, | ||
| 429 | ] | ||
| 430 | .filter((part) => part.length > 0) | ||
| 431 | .join(" ") | ||
| 432 | .toLowerCase(), | ||
| 433 | source: recipe.source, | ||
| 434 | sourceRecord: recipe.sourceRecord, | ||
| 435 | revision: recipe.revision, | ||
| 436 | operation, | ||
| 437 | metadata, | ||
| 438 | schemaBundleId, | ||
| 439 | manifest: recipe.manifest, | ||
| 440 | descriptor: recipeToolDescriptor({ | ||
| 441 | source: recipe.source, | ||
| 442 | operation, | ||
| 443 | path, | ||
| 444 | schemaBundleId, | ||
| 445 | includeSchemas: input.includeSchemas, | ||
| 446 | }), | ||
| 447 | } satisfies LoadedSourceRecipeTool; | ||
| 448 | }) | ||
| 449 | ), | ||
| 450 | ), | ||
| 451 | (recipes) => recipes.flat(), | ||
| 452 | ); | ||
| 453 | |||
| 454 | export const loadWorkspaceSourceRecipeToolIndex = (input: { | ||
| 455 | workspaceId: WorkspaceId; | ||
| 456 | actorAccountId?: AccountId | null; | ||
| 457 | includeSchemas: boolean; | ||
| 458 | }): Effect.Effect< | ||
| 459 | readonly LoadedSourceRecipeToolIndexEntry[], | ||
| 460 | Error, | ||
| 461 | SourceRecipeRuntimeServices | ||
| 462 | > => | ||
| 463 | Effect.gen(function* () { | ||
| 464 | const recipes = yield* loadWorkspaceSourceRecipes({ | ||
| 465 | workspaceId: input.workspaceId, | ||
| 466 | actorAccountId: input.actorAccountId, | ||
| 467 | }); | ||
| 468 | const tools = yield* expandRecipeTools({ | ||
| 469 | recipes, | ||
| 470 | includeSchemas: input.includeSchemas, | ||
| 471 | }); | ||
| 472 | return tools.map((tool) => ({ | ||
| 473 | path: tool.path, | ||
| 474 | searchNamespace: tool.searchNamespace, | ||
| 475 | searchText: tool.searchText, | ||
| 476 | source: tool.source, | ||
| 477 | sourceRecord: tool.sourceRecord, | ||
| 478 | operation: tool.operation, | ||
| 479 | metadata: tool.metadata, | ||
| 480 | schemaBundleId: tool.schemaBundleId, | ||
| 481 | descriptor: tool.descriptor, | ||
| 482 | })); | ||
| 483 | }); | ||
| 484 | |||
| 485 | export const loadWorkspaceSourceRecipeToolByPath = (input: { | ||
| 486 | workspaceId: WorkspaceId; | ||
| 487 | path: string; | ||
| 488 | actorAccountId?: AccountId | null; | ||
| 489 | includeSchemas: boolean; | ||
| 490 | }): Effect.Effect< | ||
| 491 | LoadedSourceRecipeToolIndexEntry | null, | ||
| 492 | Error, | ||
| 493 | SourceRecipeRuntimeServices | ||
| 494 | > => | ||
| 495 | Effect.gen(function* () { | ||
| 496 | const recipes = yield* loadWorkspaceSourceRecipes({ | ||
| 497 | workspaceId: input.workspaceId, | ||
| 498 | actorAccountId: input.actorAccountId, | ||
| 499 | }); | ||
| 500 | const tools = yield* expandRecipeTools({ | ||
| 501 | recipes, | ||
| 502 | includeSchemas: input.includeSchemas, | ||
| 503 | }); | ||
| 504 | const tool = tools.find((entry) => entry.path === input.path) ?? null; | ||
| 505 | return tool | ||
| 506 | ? { | ||
| 507 | path: tool.path, | ||
| 508 | searchNamespace: tool.searchNamespace, | ||
| 509 | searchText: tool.searchText, | ||
| 510 | source: tool.source, | ||
| 511 | sourceRecord: tool.sourceRecord, | ||
| 512 | operation: tool.operation, | ||
| 513 | metadata: tool.metadata, | ||
| 514 | schemaBundleId: tool.schemaBundleId, | ||
| 515 | descriptor: tool.descriptor, | ||
| 516 | } | ||
| 517 | : null; | ||
| 518 | }); | ||
| 519 | |||
| 520 | export const RuntimeSourceRecipeStoreLive = Layer.effect( | ||
| 521 | RuntimeSourceRecipeStoreService, | ||
| 522 | Effect.gen(function* () { | ||
| 523 | const runtimeLocalWorkspace = yield* RuntimeLocalWorkspaceService; | ||
| 524 | const sourceStore = yield* RuntimeSourceStoreService; | ||
| 525 | const sourceArtifactStore = yield* SourceArtifactStore; | ||
| 526 | |||
| 527 | const deps: RuntimeSourceRecipeStoreDeps = { | ||
| 528 | runtimeLocalWorkspace, | ||
| 529 | sourceStore, | ||
| 530 | sourceArtifactStore, | ||
| 531 | }; | ||
| 532 | |||
| 533 | return RuntimeSourceRecipeStoreService.of({ | ||
| 534 | loadWorkspaceSourceRecipes: (input) => | ||
| 535 | loadWorkspaceSourceRecipesWithDeps(deps, input), | ||
| 536 | loadSourceWithRecipe: (input) => | ||
| 537 | loadSourceWithRecipeWithDeps(deps, input), | ||
| 538 | loadWorkspaceSourceRecipeToolIndex: (input) => | ||
| 539 | Effect.gen(function* () { | ||
| 540 | const recipes = yield* loadWorkspaceSourceRecipesWithDeps(deps, input); | ||
| 541 | const tools = yield* expandRecipeTools({ | ||
| 542 | recipes, | ||
| 543 | includeSchemas: input.includeSchemas, | ||
| 544 | }); | ||
| 545 | return tools.map((tool) => ({ | ||
| 546 | path: tool.path, | ||
| 547 | searchNamespace: tool.searchNamespace, | ||
| 548 | searchText: tool.searchText, | ||
| 549 | source: tool.source, | ||
| 550 | sourceRecord: tool.sourceRecord, | ||
| 551 | operation: tool.operation, | ||
| 552 | metadata: tool.metadata, | ||
| 553 | schemaBundleId: tool.schemaBundleId, | ||
| 554 | descriptor: tool.descriptor, | ||
| 555 | })); | ||
| 556 | }), | ||
| 557 | loadWorkspaceSourceRecipeToolByPath: (input) => | ||
| 558 | Effect.gen(function* () { | ||
| 559 | const recipes = yield* loadWorkspaceSourceRecipesWithDeps(deps, input); | ||
| 560 | const tools = yield* expandRecipeTools({ | ||
| 561 | recipes, | ||
| 562 | includeSchemas: input.includeSchemas, | ||
| 563 | }); | ||
| 564 | const tool = tools.find((entry) => entry.path === input.path) ?? null; | ||
| 565 | return tool | ||
| 566 | ? { | ||
| 567 | path: tool.path, | ||
| 568 | searchNamespace: tool.searchNamespace, | ||
| 569 | searchText: tool.searchText, | ||
| 570 | source: tool.source, | ||
| 571 | sourceRecord: tool.sourceRecord, | ||
| 572 | operation: tool.operation, | ||
| 573 | metadata: tool.metadata, | ||
| 574 | schemaBundleId: tool.schemaBundleId, | ||
| 575 | descriptor: tool.descriptor, | ||
| 576 | } | ||
| 577 | : null; | ||
| 578 | }), | ||
| 579 | loadWorkspaceSchemaBundle: (input) => | ||
| 580 | Effect.gen(function* () { | ||
| 581 | const recipes = yield* loadWorkspaceSourceRecipesWithDeps(deps, { | ||
| 582 | workspaceId: input.workspaceId, | ||
| 583 | }); | ||
| 584 | const localBundle = recipes | ||
| 585 | .flatMap((recipe) => recipe.schemaBundles) | ||
| 586 | .find((schemaBundle) => schemaBundle.id === input.id); | ||
| 587 | return localBundle | ||
| 588 | ? { | ||
| 589 | id: localBundle.id, | ||
| 590 | kind: localBundle.bundleKind, | ||
| 591 | hash: localBundle.contentHash, | ||
| 592 | refsJson: localBundle.refsJson, | ||
| 593 | } | ||
| 594 | : null; | ||
| 595 | }), | ||
| 596 | }); | ||
| 597 | }), | ||
| 598 | ); | ||
| @@ -14,8 +14,8 @@ import * as Effect from "effect/Effect"; · 13 unmodified lines | |||
| 14 | import * as Layer from "effect/Layer"; | 14 | import * as Layer from "effect/Layer"; |
| 15 | 15 | ||
| 16 | import { | 16 | import { |
| 17 | stableSourceRecipeId, | 17 | stableSourceCatalogId, |
| 18 | stableSourceRecipeRevisionId, | 18 | stableSourceCatalogRevisionId, |
| 19 | splitSourceForStorage, | 19 | splitSourceForStorage, |
| 20 | } from "./source-definitions"; | 20 | } from "./source-definitions"; |
| 21 | import { | 21 | import { |
| @@ -407,8 +407,8 @@ const buildLocalSourceRecord = (input: { · 385 unmodified lines | |||
| 407 | const sourceRecord = { | 407 | const sourceRecord = { |
| 408 | id: baseSource.id, | 408 | id: baseSource.id, |
| 409 | workspaceId: baseSource.workspaceId, | 409 | workspaceId: baseSource.workspaceId, |
| 410 | recipeId: artifact?.recipeId ?? stableSourceRecipeId(baseSource), | 410 | catalogId: artifact?.catalogId ?? stableSourceCatalogId(baseSource), |
| 411 | recipeRevisionId: artifact?.revision.id ?? stableSourceRecipeRevisionId(baseSource), | 411 | catalogRevisionId: artifact?.revision.id ?? stableSourceCatalogRevisionId(baseSource), |
| 412 | name: baseSource.name, | 412 | name: baseSource.name, |
| 413 | kind: baseSource.kind, | 413 | kind: baseSource.kind, |
| 414 | endpoint: baseSource.endpoint, | 414 | endpoint: baseSource.endpoint, |
| @@ -855,8 +855,8 @@ const persistSourceWithDeps = ( · 440 unmodified lines | |||
| 855 | 855 | ||
| 856 | const { runtimeAuthArtifact, importAuthArtifact } = splitSourceForStorage({ | 856 | const { runtimeAuthArtifact, importAuthArtifact } = splitSourceForStorage({ |
| 857 | source: nextSource, | 857 | source: nextSource, |
| 858 | recipeId: stableSourceRecipeId(nextSource), | 858 | catalogId: stableSourceCatalogId(nextSource), |
| 859 | recipeRevisionId: stableSourceRecipeRevisionId(nextSource), | 859 | catalogRevisionId: stableSourceCatalogRevisionId(nextSource), |
| 860 | actorAccountId: options.actorAccountId, | 860 | actorAccountId: options.actorAccountId, |
| 861 | existingRuntimeAuthArtifactId: existingRuntimeAuthArtifact?.id ?? null, | 861 | existingRuntimeAuthArtifactId: existingRuntimeAuthArtifact?.id ?? null, |
| 862 | existingImportAuthArtifactId: existingImportAuthArtifact?.id ?? null, | 862 | existingImportAuthArtifactId: existingImportAuthArtifact?.id ?? null, |
| @@ -25,7 +25,7 @@ import { · 24 unmodified lines | |||
| 25 | operationErrors, | 25 | operationErrors, |
| 26 | } from "./operation-errors"; | 26 | } from "./operation-errors"; |
| 27 | import { ControlPlaneStore, type ControlPlaneStoreShape } from "./store"; | 27 | import { ControlPlaneStore, type ControlPlaneStoreShape } from "./store"; |
| 28 | import { RuntimeSourceMaterializationService } from "./source-materialization"; | 28 | import { RuntimeSourceCatalogSyncService } from "./source-catalog-sync"; |
| 29 | import { | 29 | import { |
| 30 | RuntimeSourceStoreService, | 30 | RuntimeSourceStoreService, |
| 31 | } from "./source-store"; | 31 | } from "./source-store"; |
| @@ -51,7 +51,7 @@ const syncArtifactsForSource = (input: { · 19 unmodified lines | |||
| 51 | | typeof sourceOps.update; | 51 | | typeof sourceOps.update; |
| 52 | }) => | 52 | }) => |
| 53 | Effect.gen(function* () { | 53 | Effect.gen(function* () { |
| 54 | const materializationService = yield* RuntimeSourceMaterializationService; | 54 | const catalogSyncService = yield* RuntimeSourceCatalogSyncService; |
| 55 | 55 | ||
| 56 | // For HTTP-backed source kinds that can validate themselves from a remote | 56 | // For HTTP-backed source kinds that can validate themselves from a remote |
| 57 | // document, automatically attempt to probe and connect. This mirrors the | 57 | // document, automatically attempt to probe and connect. This mirrors the |
| @@ -63,7 +63,7 @@ const syncArtifactsForSource = (input: { · 5 unmodified lines | |||
| 63 | : input.source; | 63 | : input.source; |
| 64 | 64 | ||
| 65 | const synced = yield* Effect.either( | 65 | const synced = yield* Effect.either( |
| 66 | materializationService.sync({ | 66 | catalogSyncService.sync({ |
| 67 | source: sourceForSync, | 67 | source: sourceForSync, |
| 68 | actorAccountId: input.actorAccountId, | 68 | actorAccountId: input.actorAccountId, |
| 69 | }), | 69 | }), |
| @@ -8,11 +8,9 @@ import { · 7 unmodified lines | |||
| 8 | type ToolCatalog, | 8 | type ToolCatalog, |
| 9 | type ToolInvoker, | 9 | type ToolInvoker, |
| 10 | type ToolPath, | 10 | type ToolPath, |
| 11 | type ToolSchemaBundle, | ||
| 12 | } from "@executor/codemode-core"; | 11 | } from "@executor/codemode-core"; |
| 13 | import { makeQuickJsExecutor } from "@executor/runtime-quickjs"; | 12 | import { makeQuickJsExecutor } from "@executor/runtime-quickjs"; |
| 14 | import type { AccountId, Source, SourceRecipeSchemaBundleId } from "#schema"; | 13 | import type { AccountId, Source } from "#schema"; |
| 15 | import { SourceRecipeSchemaBundleIdSchema } from "#schema"; | ||
| 16 | import * as Context from "effect/Context"; | 14 | import * as Context from "effect/Context"; |
| 17 | import * as Either from "effect/Either"; | 15 | import * as Either from "effect/Either"; |
| 18 | import * as Effect from "effect/Effect"; | 16 | import * as Effect from "effect/Effect"; |
| @@ -29,13 +27,15 @@ import { · 10 unmodified lines | |||
| 29 | type RuntimeSourceAuthService, | 27 | type RuntimeSourceAuthService, |
| 30 | } from "./source-auth-service"; | 28 | } from "./source-auth-service"; |
| 31 | import { | 29 | import { |
| 32 | RuntimeSourceRecipeStoreService, | 30 | RuntimeSourceCatalogStoreService, |
| 33 | type LoadedSourceRecipeToolIndexEntry, | 31 | type LoadedSourceCatalogToolIndexEntry, |
| 34 | recipeToolCatalogEntry, | 32 | catalogToolCatalogEntry, |
| 35 | } from "./source-recipes-runtime"; | 33 | } from "./source-catalog-runtime"; |
| 34 | import { | ||
| 35 | invocationDescriptorFromTool, | ||
| 36 | invokeIrTool, | ||
| 37 | } from "./ir-execution"; | ||
| 36 | import { RuntimeSourceAuthMaterialService } from "./source-auth-material"; | 38 | import { RuntimeSourceAuthMaterialService } from "./source-auth-material"; |
| 37 | import { namespaceFromSourceName } from "./source-names"; | ||
| 38 | import { getSourceAdapterForOperation } from "./source-adapters"; | ||
| 39 | import { | 39 | import { |
| 40 | type SecretMaterialResolveContext, | 40 | type SecretMaterialResolveContext, |
| 41 | } from "./secret-material-providers"; | 41 | } from "./secret-material-providers"; |
| @@ -145,14 +145,14 @@ const toSecretResolutionContext = ( · 103 unmodified lines | |||
| 145 | const queryTokenWeight = (token: string): number => | 145 | const queryTokenWeight = (token: string): number => |
| 146 | LOW_SIGNAL_QUERY_TOKENS.has(token) ? 0.25 : 1; | 146 | LOW_SIGNAL_QUERY_TOKENS.has(token) ? 0.25 : 1; |
| 147 | 147 | ||
| 148 | const loadWorkspaceRecipeTools = (input: { | 148 | const loadWorkspaceCatalogTools = (input: { |
| 149 | workspaceId: Source["workspaceId"]; | 149 | workspaceId: Source["workspaceId"]; |
| 150 | accountId: AccountId; | 150 | accountId: AccountId; |
| 151 | sourceRecipeStore: Effect.Effect.Success<typeof RuntimeSourceRecipeStoreService>; | 151 | sourceCatalogStore: Effect.Effect.Success<typeof RuntimeSourceCatalogStoreService>; |
| 152 | includeSchemas: boolean; | 152 | includeSchemas: boolean; |
| 153 | }): Effect.Effect<readonly LoadedSourceRecipeToolIndexEntry[], Error, WorkspaceStorageServices> => | 153 | }): Effect.Effect<readonly LoadedSourceCatalogToolIndexEntry[], Error, WorkspaceStorageServices> => |
| 154 | Effect.map( | 154 | Effect.map( |
| 155 | input.sourceRecipeStore.loadWorkspaceSourceRecipeToolIndex({ | 155 | input.sourceCatalogStore.loadWorkspaceSourceCatalogToolIndex({ |
| 156 | workspaceId: input.workspaceId, | 156 | workspaceId: input.workspaceId, |
| 157 | actorAccountId: input.accountId, | 157 | actorAccountId: input.accountId, |
| 158 | includeSchemas: input.includeSchemas, | 158 | includeSchemas: input.includeSchemas, |
| @@ -163,14 +163,14 @@ const loadWorkspaceRecipeTools = (input: { · 4 unmodified lines | |||
| 163 | ), | 163 | ), |
| 164 | ); | 164 | ); |
| 165 | 165 | ||
| 166 | const loadWorkspaceRecipeToolByPath = (input: { | 166 | const loadWorkspaceCatalogToolByPath = (input: { |
| 167 | workspaceId: Source["workspaceId"]; | 167 | workspaceId: Source["workspaceId"]; |
| 168 | accountId: AccountId; | 168 | accountId: AccountId; |
| 169 | sourceRecipeStore: Effect.Effect.Success<typeof RuntimeSourceRecipeStoreService>; | 169 | sourceCatalogStore: Effect.Effect.Success<typeof RuntimeSourceCatalogStoreService>; |
| 170 | path: string; | 170 | path: string; |
| 171 | includeSchemas: boolean; | 171 | includeSchemas: boolean; |
| 172 | }): Effect.Effect<LoadedSourceRecipeToolIndexEntry | null, Error, WorkspaceStorageServices> => | 172 | }): Effect.Effect<LoadedSourceCatalogToolIndexEntry | null, Error, WorkspaceStorageServices> => |
| 173 | input.sourceRecipeStore.loadWorkspaceSourceRecipeToolByPath({ | 173 | input.sourceCatalogStore.loadWorkspaceSourceCatalogToolByPath({ |
| 174 | workspaceId: input.workspaceId, | 174 | workspaceId: input.workspaceId, |
| 175 | path: input.path, | 175 | path: input.path, |
| 176 | actorAccountId: input.accountId, | 176 | actorAccountId: input.accountId, |
| @@ -183,42 +183,29 @@ const loadWorkspaceRecipeToolByPath = (input: { · 6 unmodified lines | |||
| 183 | ), | 183 | ), |
| 184 | ); | 184 | ); |
| 185 | 185 | ||
| 186 | const loadWorkspaceSchemaBundle = (input: { | 186 | const scoreCatalogTool = ( |
| 187 | workspaceId: Source["workspaceId"]; | ||
| 188 | sourceRecipeStore: Effect.Effect.Success<typeof RuntimeSourceRecipeStoreService>; | ||
| 189 | id: SourceRecipeSchemaBundleId; | ||
| 190 | }): Effect.Effect<ToolSchemaBundle | null, Error, WorkspaceStorageServices> => | ||
| 191 | Effect.map( | ||
| 192 | input.sourceRecipeStore.loadWorkspaceSchemaBundle({ | ||
| 193 | workspaceId: input.workspaceId, | ||
| 194 | id: input.id, | ||
| 195 | }), | ||
| 196 | (bundle) => | ||
| 197 | bundle | ||
| 198 | ? { | ||
| 199 | id: bundle.id, | ||
| 200 | kind: bundle.kind as ToolSchemaBundle["kind"], | ||
| 201 | hash: bundle.hash, | ||
| 202 | refsJson: bundle.refsJson, | ||
| 203 | } | ||
| 204 | : null, | ||
| 205 | ); | ||
| 206 | |||
| 207 | const scoreRecipeTool = ( | ||
| 208 | queryTokens: readonly string[], | 187 | queryTokens: readonly string[], |
| 209 | tool: LoadedSourceRecipeToolIndexEntry, | 188 | tool: LoadedSourceCatalogToolIndexEntry, |
| 210 | ): number => { | 189 | ): number => { |
| 211 | const pathText = tool.path.toLowerCase(); | 190 | const pathText = tool.path.toLowerCase(); |
| 212 | const namespaceText = tool.searchNamespace.toLowerCase(); | 191 | const namespaceText = tool.searchNamespace.toLowerCase(); |
| 213 | const toolIdText = tool.operation.toolId.toLowerCase(); | 192 | const toolIdText = tool.path.split(".").at(-1)?.toLowerCase() ?? ""; |
| 214 | const titleText = tool.operation.title?.toLowerCase() ?? ""; | 193 | const titleText = tool.capability.surface.title?.toLowerCase() ?? ""; |
| 215 | const descriptionText = tool.operation.description?.toLowerCase() ?? ""; | 194 | const descriptionText = |
| 216 | const templateText = tool.metadata.pathTemplate?.toLowerCase() ?? ""; | 195 | tool.capability.surface.summary?.toLowerCase() |
| 217 | 196 | ?? tool.capability.surface.description?.toLowerCase() | |
| 218 | const pathTokens = tokenize(`${tool.path} ${tool.operation.toolId}`); | 197 | ?? ""; |
| 198 | const templateText = | ||
| 199 | tool.executable.protocol === "http" | ||
| 200 | ? tool.executable.pathTemplate.toLowerCase() | ||
| 201 | : tool.executable.protocol === "graphql" | ||
| 202 | ? tool.executable.rootField.toLowerCase() | ||
| 203 | : tool.executable.toolName.toLowerCase(); | ||
| 204 | |||
| 205 | const pathTokens = tokenize(`${tool.path} ${toolIdText}`); | ||
| 219 | const namespaceTokens = tokenize(tool.searchNamespace); | 206 | const namespaceTokens = tokenize(tool.searchNamespace); |
| 220 | const titleTokens = tokenize(tool.operation.title ?? ""); | 207 | const titleTokens = tokenize(tool.capability.surface.title ?? ""); |
| 221 | const templateTokens = tokenize(tool.metadata.pathTemplate ?? ""); | 208 | const templateTokens = tokenize(templateText); |
| 222 | 209 | ||
| 223 | let score = 0; | 210 | let score = 0; |
| 224 | let structuralHits = 0; | 211 | let structuralHits = 0; |
| @@ -342,20 +329,9 @@ const approvalMessageForInvocation = ( · 117 unmodified lines | |||
| 342 | return `Allow tool call: ${descriptor.toolPath}?`; | 329 | return `Allow tool call: ${descriptor.toolPath}?`; |
| 343 | }; | 330 | }; |
| 344 | 331 | ||
| 345 | const toInvocationDescriptorFromRecipeTool = (input: { | 332 | const toInvocationDescriptorFromCatalogTool = (input: { |
| 346 | tool: LoadedSourceRecipeToolIndexEntry; | 333 | tool: LoadedSourceCatalogToolIndexEntry; |
| 347 | }): InvocationDescriptor => ({ | 334 | }): InvocationDescriptor => invocationDescriptorFromTool(input) satisfies InvocationDescriptor; |
| 348 | toolPath: input.tool.path, | ||
| 349 | sourceId: input.tool.source.id, | ||
| 350 | sourceName: input.tool.source.name, | ||
| 351 | sourceKind: input.tool.source.kind, | ||
| 352 | sourceNamespace: | ||
| 353 | input.tool.source.namespace ?? | ||
| 354 | namespaceFromSourceName(input.tool.source.name), | ||
| 355 | operationKind: input.tool.operation.operationKind, | ||
| 356 | interaction: input.tool.metadata.interaction, | ||
| 357 | approvalLabel: input.tool.metadata.approvalLabel, | ||
| 358 | }); | ||
| 359 | 335 | ||
| 360 | const authorizePersistedToolInvocation = (input: { | 336 | const authorizePersistedToolInvocation = (input: { |
| 361 | workspaceId: Source["workspaceId"]; | 337 | workspaceId: Source["workspaceId"]; |
| @@ -452,10 +428,10 @@ const provideRuntimeLocalWorkspace = <A, E, R>( · 90 unmodified lines | |||
| 452 | ): Effect.Effect<A, E, R> => | 428 | ): Effect.Effect<A, E, R> => |
| 453 | provideOptionalRuntimeLocalWorkspace(effect, runtimeLocalWorkspace); | 429 | provideOptionalRuntimeLocalWorkspace(effect, runtimeLocalWorkspace); |
| 454 | 430 | ||
| 455 | const createWorkspaceRecipeCatalog = (input: { | 431 | const createWorkspaceSourceCatalog = (input: { |
| 456 | workspaceId: Source["workspaceId"]; | 432 | workspaceId: Source["workspaceId"]; |
| 457 | accountId: AccountId; | 433 | accountId: AccountId; |
| 458 | sourceRecipeStore: Effect.Effect.Success<typeof RuntimeSourceRecipeStoreService>; | 434 | sourceCatalogStore: Effect.Effect.Success<typeof RuntimeSourceCatalogStoreService>; |
| 459 | workspaceConfigStore: WorkspaceConfigStoreShape; | 435 | workspaceConfigStore: WorkspaceConfigStoreShape; |
| 460 | workspaceStateStore: WorkspaceStateStoreShape; | 436 | workspaceStateStore: WorkspaceStateStoreShape; |
| 461 | sourceArtifactStore: SourceArtifactStoreShape; | 437 | sourceArtifactStore: SourceArtifactStoreShape; |
| @@ -471,26 +447,20 @@ const createWorkspaceRecipeCatalog = (input: { · 9 unmodified lines | |||
| 471 | 447 | ||
| 472 | const createSharedCatalog = (includeSchemas: boolean): Effect.Effect<ToolCatalog, Error, never> => | 448 | const createSharedCatalog = (includeSchemas: boolean): Effect.Effect<ToolCatalog, Error, never> => |
| 473 | provideWorkspaceStorage(Effect.gen(function* () { | 449 | provideWorkspaceStorage(Effect.gen(function* () { |
| 474 | const recipeTools = yield* loadWorkspaceRecipeTools({ | 450 | const catalogTools = yield* loadWorkspaceCatalogTools({ |
| 475 | workspaceId: input.workspaceId, | 451 | workspaceId: input.workspaceId, |
| 476 | accountId: input.accountId, | 452 | accountId: input.accountId, |
| 477 | sourceRecipeStore: input.sourceRecipeStore, | 453 | sourceCatalogStore: input.sourceCatalogStore, |
| 478 | includeSchemas, | 454 | includeSchemas, |
| 479 | }); | 455 | }); |
| 480 | 456 | ||
| 481 | return createToolCatalogFromEntries({ | 457 | return createToolCatalogFromEntries({ |
| 482 | entries: recipeTools.map((tool) => | 458 | entries: catalogTools.map((tool) => |
| 483 | recipeToolCatalogEntry({ | 459 | catalogToolCatalogEntry({ |
| 484 | tool, | 460 | tool, |
| 485 | score: (queryTokens) => scoreRecipeTool(queryTokens, tool), | 461 | score: (queryTokens) => scoreCatalogTool(queryTokens, tool), |
| 486 | }), | 462 | }), |
| 487 | ), | 463 | ), |
| 488 | getSchemaBundle: ({ id }) => | ||
| 489 | provideWorkspaceStorage(loadWorkspaceSchemaBundle({ | ||
| 490 | workspaceId: input.workspaceId, | ||
| 491 | sourceRecipeStore: input.sourceRecipeStore, | ||
| 492 | id: SourceRecipeSchemaBundleIdSchema.make(id), | ||
| 493 | })), | ||
| 494 | }); | 464 | }); |
| 495 | })); | 465 | })); |
| 496 | 466 | ||
| @@ -524,14 +494,6 @@ const createWorkspaceRecipeCatalog = (input: { · 27 unmodified lines | |||
| 524 | input.runtimeLocalWorkspace, | 494 | input.runtimeLocalWorkspace, |
| 525 | ), | 495 | ), |
| 526 | 496 | ||
| 527 | getSchemaBundle: ({ id }) => | ||
| 528 | provideRuntimeLocalWorkspace( | ||
| 529 | Effect.flatMap(createSharedCatalog(false), (catalog) => | ||
| 530 | catalog.getSchemaBundle({ id }), | ||
| 531 | ), | ||
| 532 | input.runtimeLocalWorkspace, | ||
| 533 | ), | ||
| 534 | |||
| 535 | searchTools: ({ query, namespace, limit }) => | 497 | searchTools: ({ query, namespace, limit }) => |
| 536 | provideRuntimeLocalWorkspace( | 498 | provideRuntimeLocalWorkspace( |
| 537 | Effect.flatMap(createSharedCatalog(false), (catalog) => | 499 | Effect.flatMap(createSharedCatalog(false), (catalog) => |
| @@ -549,7 +511,7 @@ const createWorkspaceRecipeCatalog = (input: { · 11 unmodified lines | |||
| 549 | const createWorkspaceToolInvoker = (input: { | 511 | const createWorkspaceToolInvoker = (input: { |
| 550 | workspaceId: Source["workspaceId"]; | 512 | workspaceId: Source["workspaceId"]; |
| 551 | accountId: AccountId; | 513 | accountId: AccountId; |
| 552 | sourceRecipeStore: Effect.Effect.Success<typeof RuntimeSourceRecipeStoreService>; | 514 | sourceCatalogStore: Effect.Effect.Success<typeof RuntimeSourceCatalogStoreService>; |
| 553 | workspaceConfigStore: WorkspaceConfigStoreShape; | 515 | workspaceConfigStore: WorkspaceConfigStoreShape; |
| 554 | workspaceStateStore: WorkspaceStateStoreShape; | 516 | workspaceStateStore: WorkspaceStateStoreShape; |
| 555 | sourceArtifactStore: SourceArtifactStoreShape; | 517 | sourceArtifactStore: SourceArtifactStoreShape; |
| @@ -578,10 +540,10 @@ const createWorkspaceToolInvoker = (input: { · 22 unmodified lines | |||
| 578 | sourceAuthService: input.sourceAuthService, | 540 | sourceAuthService: input.sourceAuthService, |
| 579 | runtimeLocalWorkspace: input.runtimeLocalWorkspace, | 541 | runtimeLocalWorkspace: input.runtimeLocalWorkspace, |
| 580 | }); | 542 | }); |
| 581 | const recipeCatalog = createWorkspaceRecipeCatalog({ | 543 | const sourceCatalog = createWorkspaceSourceCatalog({ |
| 582 | workspaceId: input.workspaceId, | 544 | workspaceId: input.workspaceId, |
| 583 | accountId: input.accountId, | 545 | accountId: input.accountId, |
| 584 | sourceRecipeStore: input.sourceRecipeStore, | 546 | sourceCatalogStore: input.sourceCatalogStore, |
| 585 | workspaceConfigStore: input.workspaceConfigStore, | 547 | workspaceConfigStore: input.workspaceConfigStore, |
| 586 | workspaceStateStore: input.workspaceStateStore, | 548 | workspaceStateStore: input.workspaceStateStore, |
| 587 | sourceArtifactStore: input.sourceArtifactStore, | 549 | sourceArtifactStore: input.sourceArtifactStore, |
| @@ -606,7 +568,7 @@ const createWorkspaceToolInvoker = (input: { · 18 unmodified lines | |||
| 606 | tools: authoredTools, | 568 | tools: authoredTools, |
| 607 | }); | 569 | }); |
| 608 | catalog = mergeToolCatalogs({ | 570 | catalog = mergeToolCatalogs({ |
| 609 | catalogs: [authoredCatalog, recipeCatalog], | 571 | catalogs: [authoredCatalog, sourceCatalog], |
| 610 | }); | 572 | }); |
| 611 | const authoredToolPaths = new Set(Object.keys(authoredTools)); | 573 | const authoredToolPaths = new Set(Object.keys(authoredTools)); |
| 612 | const authoredInvoker = makeToolInvokerFromTools({ | 574 | const authoredInvoker = makeToolInvokerFromTools({ |
| @@ -621,14 +583,14 @@ const createWorkspaceToolInvoker = (input: { · 8 unmodified lines | |||
| 621 | }) => | 583 | }) => |
| 622 | provideRuntimeLocalWorkspace( | 584 | provideRuntimeLocalWorkspace( |
| 623 | provideWorkspaceStorage(Effect.gen(function* () { | 585 | provideWorkspaceStorage(Effect.gen(function* () { |
| 624 | const recipeTool = yield* loadWorkspaceRecipeToolByPath({ | 586 | const catalogTool = yield* loadWorkspaceCatalogToolByPath({ |
| 625 | workspaceId: input.workspaceId, | 587 | workspaceId: input.workspaceId, |
| 626 | accountId: input.accountId, | 588 | accountId: input.accountId, |
| 627 | sourceRecipeStore: input.sourceRecipeStore, | 589 | sourceCatalogStore: input.sourceCatalogStore, |
| 628 | path: invocation.path, | 590 | path: invocation.path, |
| 629 | includeSchemas: false, | 591 | includeSchemas: false, |
| 630 | }); | 592 | }); |
| 631 | if (!recipeTool) { | 593 | if (!catalogTool) { |
| 632 | return yield* Effect.fail( | 594 | return yield* Effect.fail( |
| 633 | new Error(`Unknown tool path: ${invocation.path}`), | 595 | new Error(`Unknown tool path: ${invocation.path}`), |
| 634 | ); | 596 | ); |
| @@ -637,49 +599,27 @@ const createWorkspaceToolInvoker = (input: { · 2 unmodified lines | |||
| 637 | yield* authorizePersistedToolInvocation({ | 599 | yield* authorizePersistedToolInvocation({ |
| 638 | workspaceId: input.workspaceId, | 600 | workspaceId: input.workspaceId, |
| 639 | accountId: input.accountId, | 601 | accountId: input.accountId, |
| 640 | descriptor: toInvocationDescriptorFromRecipeTool({ | 602 | descriptor: toInvocationDescriptorFromCatalogTool({ |
| 641 | tool: recipeTool, | 603 | tool: catalogTool, |
| 642 | }), | 604 | }), |
| 643 | args: invocation.args, | 605 | args: invocation.args, |
| 644 | source: recipeTool.source, | 606 | source: catalogTool.source, |
| 645 | context: invocation.context, | 607 | context: invocation.context, |
| 646 | onElicitation: input.onElicitation, | 608 | onElicitation: input.onElicitation, |
| 647 | }); | 609 | }); |
| 648 | 610 | ||
| 649 | const auth = yield* input.sourceAuthMaterialService.resolve({ | 611 | const auth = yield* input.sourceAuthMaterialService.resolve({ |
| 650 | source: recipeTool.source, | 612 | source: catalogTool.source, |
| 651 | actorAccountId: input.accountId, | 613 | actorAccountId: input.accountId, |
| 652 | context: toSecretResolutionContext(invocation.context), | 614 | context: toSecretResolutionContext(invocation.context), |
| 653 | }); | 615 | }); |
| 654 | const schemaBundle = recipeTool.schemaBundleId | 616 | return yield* invokeIrTool({ |
| 655 | ? yield* loadWorkspaceSchemaBundle({ | ||
| 656 | workspaceId: input.workspaceId, | ||
| 657 | sourceRecipeStore: input.sourceRecipeStore, | ||
| 658 | id: SourceRecipeSchemaBundleIdSchema.make( | ||
| 659 | recipeTool.schemaBundleId, | ||
| 660 | ), | ||
| 661 | }) | ||
| 662 | : null; | ||
| 663 | const recipe = yield* input.sourceRecipeStore.loadSourceWithRecipe({ | ||
| 664 | workspaceId: input.workspaceId, | ||
| 665 | sourceId: recipeTool.source.id, | ||
| 666 | actorAccountId: input.accountId, | ||
| 667 | }); | ||
| 668 | |||
| 669 | return yield* getSourceAdapterForOperation( | ||
| 670 | recipeTool.operation, | ||
| 671 | ).invokePersistedTool({ | ||
| 672 | workspaceId: input.workspaceId, | 617 | workspaceId: input.workspaceId, |
| 673 | accountId: input.accountId, | 618 | accountId: input.accountId, |
| 674 | source: recipeTool.source, | 619 | tool: catalogTool, |
| 675 | path: invocation.path, | ||
| 676 | operation: recipeTool.operation, | ||
| 677 | schemaBundle, | ||
| 678 | manifestJson: recipe.revision.manifestJson, | ||
| 679 | auth, | 620 | auth, |
| 680 | args: invocation.args, | 621 | args: invocation.args, |
| 681 | context: invocation.context, | 622 | context: invocation.context, |
| 682 | onElicitation: input.onElicitation, | ||
| 683 | }); | 623 | }); |
| 684 | })), | 624 | })), |
| 685 | input.runtimeLocalWorkspace, | 625 | input.runtimeLocalWorkspace, |
| @@ -702,7 +642,7 @@ const createWorkspaceToolInvoker = (input: { · 16 unmodified lines | |||
| 702 | export const createWorkspaceExecutionEnvironmentResolver = (input: { | 642 | export const createWorkspaceExecutionEnvironmentResolver = (input: { |
| 703 | sourceAuthMaterialService: Effect.Effect.Success<typeof RuntimeSourceAuthMaterialService>; | 643 | sourceAuthMaterialService: Effect.Effect.Success<typeof RuntimeSourceAuthMaterialService>; |
| 704 | sourceAuthService: RuntimeSourceAuthService; | 644 | sourceAuthService: RuntimeSourceAuthService; |
| 705 | sourceRecipeStore: Effect.Effect.Success<typeof RuntimeSourceRecipeStoreService>; | 645 | sourceCatalogStore: Effect.Effect.Success<typeof RuntimeSourceCatalogStoreService>; |
| 706 | localToolRuntimeLoader: LocalToolRuntimeLoaderShape; | 646 | localToolRuntimeLoader: LocalToolRuntimeLoaderShape; |
| 707 | workspaceConfigStore: WorkspaceConfigStoreShape; | 647 | workspaceConfigStore: WorkspaceConfigStoreShape; |
| 708 | workspaceStateStore: WorkspaceStateStoreShape; | 648 | workspaceStateStore: WorkspaceStateStoreShape; |
| @@ -723,7 +663,7 @@ export const createWorkspaceExecutionEnvironmentResolver = (input: { · 14 unmodified lines | |||
| 723 | const { catalog, toolInvoker } = createWorkspaceToolInvoker({ | 663 | const { catalog, toolInvoker } = createWorkspaceToolInvoker({ |
| 724 | workspaceId, | 664 | workspaceId, |
| 725 | accountId, | 665 | accountId, |
| 726 | sourceRecipeStore: input.sourceRecipeStore, | 666 | sourceCatalogStore: input.sourceCatalogStore, |
| 727 | workspaceConfigStore: input.workspaceConfigStore, | 667 | workspaceConfigStore: input.workspaceConfigStore, |
| 728 | workspaceStateStore: input.workspaceStateStore, | 668 | workspaceStateStore: input.workspaceStateStore, |
| 729 | sourceArtifactStore: input.sourceArtifactStore, | 669 | sourceArtifactStore: input.sourceArtifactStore, |
| @@ -762,7 +702,7 @@ export const RuntimeExecutionResolverLive = ( · 32 unmodified lines | |||
| 762 | Effect.gen(function* () { | 702 | Effect.gen(function* () { |
| 763 | const sourceAuthMaterialService = yield* RuntimeSourceAuthMaterialService; | 703 | const sourceAuthMaterialService = yield* RuntimeSourceAuthMaterialService; |
| 764 | const sourceAuthService = yield* RuntimeSourceAuthServiceTag; | 704 | const sourceAuthService = yield* RuntimeSourceAuthServiceTag; |
| 765 | const sourceRecipeStore = yield* RuntimeSourceRecipeStoreService; | 705 | const sourceCatalogStore = yield* RuntimeSourceCatalogStoreService; |
| 766 | const localToolRuntimeLoader = yield* LocalToolRuntimeLoaderService; | 706 | const localToolRuntimeLoader = yield* LocalToolRuntimeLoaderService; |
| 767 | const workspaceConfigStore = yield* WorkspaceConfigStore; | 707 | const workspaceConfigStore = yield* WorkspaceConfigStore; |
| 768 | const workspaceStateStore = yield* WorkspaceStateStore; | 708 | const workspaceStateStore = yield* WorkspaceStateStore; |
| @@ -771,7 +711,7 @@ export const RuntimeExecutionResolverLive = ( · 2 unmodified lines | |||
| 771 | return createWorkspaceExecutionEnvironmentResolver({ | 711 | return createWorkspaceExecutionEnvironmentResolver({ |
| 772 | sourceAuthService, | 712 | sourceAuthService, |
| 773 | sourceAuthMaterialService, | 713 | sourceAuthMaterialService, |
| 774 | sourceRecipeStore, | 714 | sourceCatalogStore, |
| 775 | localToolRuntimeLoader, | 715 | localToolRuntimeLoader, |
| 776 | workspaceConfigStore, | 716 | workspaceConfigStore, |
| 777 | workspaceStateStore, | 717 | workspaceStateStore, |
| @@ -16,21 +16,13 @@ export { · 15 unmodified lines | |||
| 16 | type SecretRef, | 16 | type SecretRef, |
| 17 | } from "./models/auth-artifact"; | 17 | } from "./models/auth-artifact"; |
| 18 | export { | 18 | export { |
| 19 | SourceRecipeAdapterKeySchema, | 19 | SourceCatalogAdapterKeySchema, |
| 20 | SourceRecipeDocumentKindSchema, | 20 | SourceCatalogKindSchema, |
| 21 | SourceRecipeKindSchema, | 21 | SourceCatalogVisibilitySchema, |
| 22 | SourceRecipeOperationKindSchema, | 22 | type SourceCatalogAdapterKey, |
| 23 | SourceRecipeOperationProviderKindSchema, | 23 | type SourceCatalogKind, |
| 24 | SourceRecipeTransportKindSchema, | 24 | type SourceCatalogVisibility, |
| 25 | SourceRecipeVisibilitySchema, | 25 | } from "./models/source-catalog"; |
| 26 | type SourceRecipeAdapterKey, | ||
| 27 | type SourceRecipeDocumentKind, | ||
| 28 | type SourceRecipeKind, | ||
| 29 | type SourceRecipeOperationKind, | ||
| 30 | type SourceRecipeOperationProviderKind, | ||
| 31 | type SourceRecipeTransportKind, | ||
| 32 | type SourceRecipeVisibility, | ||
| 33 | } from "./models/source-recipe"; | ||
| 34 | export { | 26 | export { |
| 35 | SourceAuthInferenceSchema, | 27 | SourceAuthInferenceSchema, |
| 36 | SourceDiscoveryAuthKindSchema, | 28 | SourceDiscoveryAuthKindSchema, |
| @@ -3,12 +3,9 @@ import { Schema } from "effect"; · 2 unmodified lines | |||
| 3 | export const AccountIdSchema = Schema.String.pipe(Schema.brand("AccountId")); | 3 | export const AccountIdSchema = Schema.String.pipe(Schema.brand("AccountId")); |
| 4 | export const WorkspaceIdSchema = Schema.String.pipe(Schema.brand("WorkspaceId")); | 4 | export const WorkspaceIdSchema = Schema.String.pipe(Schema.brand("WorkspaceId")); |
| 5 | export const SourceIdSchema = Schema.String.pipe(Schema.brand("SourceId")); | 5 | export const SourceIdSchema = Schema.String.pipe(Schema.brand("SourceId")); |
| 6 | export const SourceRecipeIdSchema = Schema.String.pipe(Schema.brand("SourceRecipeId")); | 6 | export const SourceCatalogIdSchema = Schema.String.pipe(Schema.brand("SourceCatalogId")); |
| 7 | export const SourceRecipeRevisionIdSchema = Schema.String.pipe( | 7 | export const SourceCatalogRevisionIdSchema = Schema.String.pipe( |
| 8 | Schema.brand("SourceRecipeRevisionId"), | 8 | Schema.brand("SourceCatalogRevisionId"), |
| 9 | ); | ||
| 10 | export const SourceRecipeSchemaBundleIdSchema = Schema.String.pipe( | ||
| 11 | Schema.brand("SourceRecipeSchemaBundleId"), | ||
| 12 | ); | 9 | ); |
| 13 | export const SourceAuthSessionIdSchema = Schema.String.pipe( | 10 | export const SourceAuthSessionIdSchema = Schema.String.pipe( |
| 14 | Schema.brand("SourceAuthSessionId"), | 11 | Schema.brand("SourceAuthSessionId"), |
| @@ -38,9 +35,8 @@ export const ExecutionStepIdSchema = Schema.String.pipe( · 23 unmodified lines | |||
| 38 | export type AccountId = typeof AccountIdSchema.Type; | 35 | export type AccountId = typeof AccountIdSchema.Type; |
| 39 | export type WorkspaceId = typeof WorkspaceIdSchema.Type; | 36 | export type WorkspaceId = typeof WorkspaceIdSchema.Type; |
| 40 | export type SourceId = typeof SourceIdSchema.Type; | 37 | export type SourceId = typeof SourceIdSchema.Type; |
| 41 | export type SourceRecipeId = typeof SourceRecipeIdSchema.Type; | 38 | export type SourceCatalogId = typeof SourceCatalogIdSchema.Type; |
| 42 | export type SourceRecipeRevisionId = typeof SourceRecipeRevisionIdSchema.Type; | 39 | export type SourceCatalogRevisionId = typeof SourceCatalogRevisionIdSchema.Type; |
| 43 | export type SourceRecipeSchemaBundleId = typeof SourceRecipeSchemaBundleIdSchema.Type; | ||
| 44 | export type SourceAuthSessionId = typeof SourceAuthSessionIdSchema.Type; | 40 | export type SourceAuthSessionId = typeof SourceAuthSessionIdSchema.Type; |
| 45 | export type AuthArtifactId = typeof AuthArtifactIdSchema.Type; | 41 | export type AuthArtifactId = typeof AuthArtifactIdSchema.Type; |
| 46 | export type AuthLeaseId = typeof AuthLeaseIdSchema.Type; | 42 | export type AuthLeaseId = typeof AuthLeaseIdSchema.Type; |
| @@ -4,7 +4,7 @@ export * from "./ids"; · 3 unmodified lines | |||
| 4 | 4 | ||
| 5 | export * from "./models/auth"; | 5 | export * from "./models/auth"; |
| 6 | export * from "./models/source"; | 6 | export * from "./models/source"; |
| 7 | export * from "./models/source-recipe"; | 7 | export * from "./models/source-catalog"; |
| 8 | export * from "./models/auth-artifact"; | 8 | export * from "./models/auth-artifact"; |
| 9 | export * from "./models/auth-lease"; | 9 | export * from "./models/auth-lease"; |
| 10 | export * from "./models/source-oauth-client"; | 10 | export * from "./models/source-oauth-client"; |
| @@ -0,0 +1,53 @@ | |||
| 1 | import { Schema } from "effect"; | ||
| 2 | |||
| 3 | import { TimestampMsSchema } from "../common"; | ||
| 4 | import { | ||
| 5 | SourceCatalogIdSchema, | ||
| 6 | SourceCatalogRevisionIdSchema, | ||
| 7 | } from "../ids"; | ||
| 8 | |||
| 9 | export const SourceCatalogKindSchema = Schema.Literal( | ||
| 10 | "http_api", | ||
| 11 | "mcp", | ||
| 12 | "internal", | ||
| 13 | ); | ||
| 14 | |||
| 15 | export const SourceCatalogAdapterKeySchema = Schema.String; | ||
| 16 | |||
| 17 | export const SourceCatalogVisibilitySchema = Schema.Literal( | ||
| 18 | "private", | ||
| 19 | "workspace", | ||
| 20 | "organization", | ||
| 21 | "public", | ||
| 22 | ); | ||
| 23 | |||
| 24 | export const StoredSourceCatalogRecordSchema = Schema.Struct({ | ||
| 25 | id: SourceCatalogIdSchema, | ||
| 26 | kind: SourceCatalogKindSchema, | ||
| 27 | adapterKey: SourceCatalogAdapterKeySchema, | ||
| 28 | providerKey: Schema.String, | ||
| 29 | name: Schema.String, | ||
| 30 | summary: Schema.NullOr(Schema.String), | ||
| 31 | visibility: SourceCatalogVisibilitySchema, | ||
| 32 | latestRevisionId: SourceCatalogRevisionIdSchema, | ||
| 33 | createdAt: TimestampMsSchema, | ||
| 34 | updatedAt: TimestampMsSchema, | ||
| 35 | }); | ||
| 36 | |||
| 37 | export const StoredSourceCatalogRevisionRecordSchema = Schema.Struct({ | ||
| 38 | id: SourceCatalogRevisionIdSchema, | ||
| 39 | catalogId: SourceCatalogIdSchema, | ||
| 40 | revisionNumber: Schema.Number, | ||
| 41 | sourceConfigJson: Schema.String, | ||
| 42 | importMetadataJson: Schema.NullOr(Schema.String), | ||
| 43 | importMetadataHash: Schema.NullOr(Schema.String), | ||
| 44 | snapshotHash: Schema.NullOr(Schema.String), | ||
| 45 | createdAt: TimestampMsSchema, | ||
| 46 | updatedAt: TimestampMsSchema, | ||
| 47 | }); | ||
| 48 | |||
| 49 | export type SourceCatalogKind = typeof SourceCatalogKindSchema.Type; | ||
| 50 | export type SourceCatalogAdapterKey = typeof SourceCatalogAdapterKeySchema.Type; | ||
| 51 | export type SourceCatalogVisibility = typeof SourceCatalogVisibilitySchema.Type; | ||
| 52 | export type StoredSourceCatalogRecord = typeof StoredSourceCatalogRecordSchema.Type; | ||
| 53 | export type StoredSourceCatalogRevisionRecord = typeof StoredSourceCatalogRevisionRecordSchema.Type; | ||
| @@ -2,18 +2,14 @@ import { Schema } from "effect"; · 1 unmodified line | |||
| 2 | 2 | ||
| 3 | import { SourceSchema } from "./source"; | 3 | import { SourceSchema } from "./source"; |
| 4 | 4 | ||
| 5 | export const SourceInspectionPipelineKindSchema = Schema.Literal( | 5 | export const SourceInspectionPipelineKindSchema = Schema.Literal("ir"); |
| 6 | "openapi", | ||
| 7 | "graphql", | ||
| 8 | "persisted", | ||
| 9 | ); | ||
| 10 | 6 | ||
| 11 | export const SourceInspectionToolSummarySchema = Schema.Struct({ | 7 | export const SourceInspectionToolSummarySchema = Schema.Struct({ |
| 12 | path: Schema.String, | 8 | path: Schema.String, |
| 13 | sourceKey: Schema.String, | 9 | sourceKey: Schema.String, |
| 14 | title: Schema.optional(Schema.String), | 10 | title: Schema.optional(Schema.String), |
| 15 | description: Schema.optional(Schema.String), | 11 | description: Schema.optional(Schema.String), |
| 16 | providerKind: Schema.String, | 12 | protocol: Schema.String, |
| 17 | toolId: Schema.String, | 13 | toolId: Schema.String, |
| 18 | rawToolId: Schema.NullOr(Schema.String), | 14 | rawToolId: Schema.NullOr(Schema.String), |
| 19 | operationId: Schema.NullOr(Schema.String), | 15 | operationId: Schema.NullOr(Schema.String), |
| @@ -43,19 +39,11 @@ export const SourceInspectionToolDetailSchema = Schema.Struct({ · 23 unmodified lines | |||
| 43 | summary: SourceInspectionToolSummarySchema, | 39 | summary: SourceInspectionToolSummarySchema, |
| 44 | definitionJson: Schema.NullOr(Schema.String), | 40 | definitionJson: Schema.NullOr(Schema.String), |
| 45 | documentationJson: Schema.NullOr(Schema.String), | 41 | documentationJson: Schema.NullOr(Schema.String), |
| 46 | providerDataJson: Schema.NullOr(Schema.String), | 42 | nativeJson: Schema.NullOr(Schema.String), |
| 47 | inputSchemaJson: Schema.NullOr(Schema.String), | 43 | callSchemaJson: Schema.NullOr(Schema.String), |
| 48 | outputSchemaJson: Schema.NullOr(Schema.String), | 44 | resultSchemaJson: Schema.NullOr(Schema.String), |
| 49 | schemaBundleId: Schema.NullOr(Schema.String), | 45 | exampleCallJson: Schema.NullOr(Schema.String), |
| 50 | exampleInputJson: Schema.NullOr(Schema.String), | 46 | exampleResultJson: Schema.NullOr(Schema.String), |
| 51 | exampleOutputJson: Schema.NullOr(Schema.String), | ||
| 52 | }); | ||
| 53 | |||
| 54 | export const SourceInspectionSchemaBundleSchema = Schema.Struct({ | ||
| 55 | id: Schema.String, | ||
| 56 | kind: Schema.String, | ||
| 57 | hash: Schema.String, | ||
| 58 | refsJson: Schema.String, | ||
| 59 | }); | 47 | }); |
| 60 | 48 | ||
| 61 | export const SourceInspectionDiscoverPayloadSchema = Schema.Struct({ | 49 | export const SourceInspectionDiscoverPayloadSchema = Schema.Struct({ |
| @@ -89,8 +77,6 @@ export type SourceInspectionToolListItem = · 27 unmodified lines | |||
| 89 | export type SourceInspection = typeof SourceInspectionSchema.Type; | 77 | export type SourceInspection = typeof SourceInspectionSchema.Type; |
| 90 | export type SourceInspectionToolDetail = | 78 | export type SourceInspectionToolDetail = |
| 91 | typeof SourceInspectionToolDetailSchema.Type; | 79 | typeof SourceInspectionToolDetailSchema.Type; |
| 92 | export type SourceInspectionSchemaBundle = | ||
| 93 | typeof SourceInspectionSchemaBundleSchema.Type; | ||
| 94 | export type SourceInspectionDiscoverPayload = | 80 | export type SourceInspectionDiscoverPayload = |
| 95 | typeof SourceInspectionDiscoverPayloadSchema.Type; | 81 | typeof SourceInspectionDiscoverPayloadSchema.Type; |
| 96 | export type SourceInspectionDiscoverResultItem = | 82 | export type SourceInspectionDiscoverResultItem = |
| @@ -1,131 +0,0 @@ | |||
| 1 | import { Schema } from "effect"; | ||
| 2 | |||
| 3 | import { TimestampMsSchema } from "../common"; | ||
| 4 | import { | ||
| 5 | SourceRecipeIdSchema, | ||
| 6 | SourceRecipeRevisionIdSchema, | ||
| 7 | SourceRecipeSchemaBundleIdSchema, | ||
| 8 | } from "../ids"; | ||
| 9 | |||
| 10 | export const SourceRecipeKindSchema = Schema.Literal( | ||
| 11 | "http_api", | ||
| 12 | "mcp", | ||
| 13 | "internal", | ||
| 14 | ); | ||
| 15 | |||
| 16 | export const SourceRecipeAdapterKeySchema = Schema.String; | ||
| 17 | |||
| 18 | export const SourceRecipeVisibilitySchema = Schema.Literal( | ||
| 19 | "private", | ||
| 20 | "workspace", | ||
| 21 | "organization", | ||
| 22 | "public", | ||
| 23 | ); | ||
| 24 | |||
| 25 | export const SourceRecipeDocumentKindSchema = Schema.String; | ||
| 26 | |||
| 27 | export const SourceRecipeSchemaBundleKindSchema = Schema.String; | ||
| 28 | |||
| 29 | export const SourceRecipeTransportKindSchema = Schema.Literal( | ||
| 30 | "http", | ||
| 31 | "graphql", | ||
| 32 | "mcp", | ||
| 33 | "internal", | ||
| 34 | ); | ||
| 35 | |||
| 36 | export const SourceRecipeOperationKindSchema = Schema.Literal( | ||
| 37 | "read", | ||
| 38 | "write", | ||
| 39 | "delete", | ||
| 40 | "unknown", | ||
| 41 | ); | ||
| 42 | |||
| 43 | export const SourceRecipeOperationProviderKindSchema = Schema.String; | ||
| 44 | |||
| 45 | export const StoredSourceRecipeRecordSchema = Schema.Struct({ | ||
| 46 | id: SourceRecipeIdSchema, | ||
| 47 | kind: SourceRecipeKindSchema, | ||
| 48 | adapterKey: SourceRecipeAdapterKeySchema, | ||
| 49 | providerKey: Schema.String, | ||
| 50 | name: Schema.String, | ||
| 51 | summary: Schema.NullOr(Schema.String), | ||
| 52 | visibility: SourceRecipeVisibilitySchema, | ||
| 53 | latestRevisionId: SourceRecipeRevisionIdSchema, | ||
| 54 | createdAt: TimestampMsSchema, | ||
| 55 | updatedAt: TimestampMsSchema, | ||
| 56 | }); | ||
| 57 | |||
| 58 | export const StoredSourceRecipeRevisionRecordSchema = Schema.Struct({ | ||
| 59 | id: SourceRecipeRevisionIdSchema, | ||
| 60 | recipeId: SourceRecipeIdSchema, | ||
| 61 | revisionNumber: Schema.Number, | ||
| 62 | sourceConfigJson: Schema.String, | ||
| 63 | manifestJson: Schema.NullOr(Schema.String), | ||
| 64 | manifestHash: Schema.NullOr(Schema.String), | ||
| 65 | materializationHash: Schema.NullOr(Schema.String), | ||
| 66 | createdAt: TimestampMsSchema, | ||
| 67 | updatedAt: TimestampMsSchema, | ||
| 68 | }); | ||
| 69 | |||
| 70 | export const StoredSourceRecipeDocumentRecordSchema = Schema.Struct({ | ||
| 71 | id: Schema.String, | ||
| 72 | recipeRevisionId: SourceRecipeRevisionIdSchema, | ||
| 73 | documentKind: SourceRecipeDocumentKindSchema, | ||
| 74 | documentKey: Schema.String, | ||
| 75 | contentText: Schema.String, | ||
| 76 | contentHash: Schema.String, | ||
| 77 | fetchedAt: Schema.NullOr(TimestampMsSchema), | ||
| 78 | createdAt: TimestampMsSchema, | ||
| 79 | updatedAt: TimestampMsSchema, | ||
| 80 | }); | ||
| 81 | |||
| 82 | export const StoredSourceRecipeSchemaBundleRecordSchema = Schema.Struct({ | ||
| 83 | id: SourceRecipeSchemaBundleIdSchema, | ||
| 84 | recipeRevisionId: SourceRecipeRevisionIdSchema, | ||
| 85 | bundleKind: SourceRecipeSchemaBundleKindSchema, | ||
| 86 | refsJson: Schema.String, | ||
| 87 | contentHash: Schema.String, | ||
| 88 | createdAt: TimestampMsSchema, | ||
| 89 | updatedAt: TimestampMsSchema, | ||
| 90 | }); | ||
| 91 | |||
| 92 | export const StoredSourceRecipeOperationRowSchema = Schema.Struct({ | ||
| 93 | id: Schema.String, | ||
| 94 | recipeRevisionId: SourceRecipeRevisionIdSchema, | ||
| 95 | operationKey: Schema.String, | ||
| 96 | transportKind: SourceRecipeTransportKindSchema, | ||
| 97 | toolId: Schema.String, | ||
| 98 | title: Schema.NullOr(Schema.String), | ||
| 99 | description: Schema.NullOr(Schema.String), | ||
| 100 | operationKind: SourceRecipeOperationKindSchema, | ||
| 101 | searchText: Schema.String, | ||
| 102 | inputSchemaJson: Schema.NullOr(Schema.String), | ||
| 103 | outputSchemaJson: Schema.NullOr(Schema.String), | ||
| 104 | providerKind: SourceRecipeOperationProviderKindSchema, | ||
| 105 | providerDataJson: Schema.NullOr(Schema.String), | ||
| 106 | createdAt: TimestampMsSchema, | ||
| 107 | updatedAt: TimestampMsSchema, | ||
| 108 | }); | ||
| 109 | export const StoredSourceRecipeOperationRecordSchema = | ||
| 110 | StoredSourceRecipeOperationRowSchema.annotations({ | ||
| 111 | identifier: "StoredSourceRecipeOperationRecord", | ||
| 112 | }); | ||
| 113 | |||
| 114 | export type SourceRecipeKind = typeof SourceRecipeKindSchema.Type; | ||
| 115 | export type SourceRecipeAdapterKey = typeof SourceRecipeAdapterKeySchema.Type; | ||
| 116 | export type SourceRecipeVisibility = typeof SourceRecipeVisibilitySchema.Type; | ||
| 117 | export type SourceRecipeDocumentKind = typeof SourceRecipeDocumentKindSchema.Type; | ||
| 118 | export type SourceRecipeSchemaBundleKind = | ||
| 119 | typeof SourceRecipeSchemaBundleKindSchema.Type; | ||
| 120 | export type SourceRecipeTransportKind = typeof SourceRecipeTransportKindSchema.Type; | ||
| 121 | export type SourceRecipeOperationKind = typeof SourceRecipeOperationKindSchema.Type; | ||
| 122 | export type SourceRecipeOperationProviderKind = | ||
| 123 | typeof SourceRecipeOperationProviderKindSchema.Type; | ||
| 124 | export type StoredSourceRecipeRecord = typeof StoredSourceRecipeRecordSchema.Type; | ||
| 125 | export type StoredSourceRecipeRevisionRecord = typeof StoredSourceRecipeRevisionRecordSchema.Type; | ||
| 126 | export type StoredSourceRecipeDocumentRecord = typeof StoredSourceRecipeDocumentRecordSchema.Type; | ||
| 127 | export type StoredSourceRecipeSchemaBundleRecord = | ||
| 128 | typeof StoredSourceRecipeSchemaBundleRecordSchema.Type; | ||
| 129 | export type StoredSourceRecipeOperationRow = typeof StoredSourceRecipeOperationRowSchema.Type; | ||
| 130 | export type StoredSourceRecipeOperationRecord = | ||
| 131 | typeof StoredSourceRecipeOperationRecordSchema.Type; | ||
| @@ -3,8 +3,8 @@ import { Schema } from "effect"; · 2 unmodified lines | |||
| 3 | import { TimestampMsSchema } from "../common"; | 3 | import { TimestampMsSchema } from "../common"; |
| 4 | import { | 4 | import { |
| 5 | SourceIdSchema, | 5 | SourceIdSchema, |
| 6 | SourceRecipeIdSchema, | 6 | SourceCatalogIdSchema, |
| 7 | SourceRecipeRevisionIdSchema, | 7 | SourceCatalogRevisionIdSchema, |
| 8 | WorkspaceIdSchema, | 8 | WorkspaceIdSchema, |
| 9 | } from "../ids"; | 9 | } from "../ids"; |
| 10 | import { SecretRefSchema } from "./auth-artifact"; | 10 | import { SecretRefSchema } from "./auth-artifact"; |
| @@ -77,8 +77,8 @@ export const SourceBindingSchema = Schema.Struct({ · 66 unmodified lines | |||
| 77 | const SourceStorageRowSchema = Schema.Struct({ | 77 | const SourceStorageRowSchema = Schema.Struct({ |
| 78 | workspaceId: WorkspaceIdSchema, | 78 | workspaceId: WorkspaceIdSchema, |
| 79 | sourceId: SourceIdSchema, | 79 | sourceId: SourceIdSchema, |
| 80 | recipeId: SourceRecipeIdSchema, | 80 | catalogId: SourceCatalogIdSchema, |
| 81 | recipeRevisionId: SourceRecipeRevisionIdSchema, | 81 | catalogRevisionId: SourceCatalogRevisionIdSchema, |
| 82 | name: Schema.String, | 82 | name: Schema.String, |
| 83 | kind: SourceKindSchema, | 83 | kind: SourceKindSchema, |
| 84 | endpoint: Schema.String, | 84 | endpoint: Schema.String, |
| @@ -98,8 +98,8 @@ export const StoredSourceRecordSchema = Schema.transform( · 13 unmodified lines | |||
| 98 | Schema.Struct({ | 98 | Schema.Struct({ |
| 99 | id: SourceIdSchema, | 99 | id: SourceIdSchema, |
| 100 | workspaceId: WorkspaceIdSchema, | 100 | workspaceId: WorkspaceIdSchema, |
| 101 | recipeId: SourceRecipeIdSchema, | 101 | catalogId: SourceCatalogIdSchema, |
| 102 | recipeRevisionId: SourceRecipeRevisionIdSchema, | 102 | catalogRevisionId: SourceCatalogRevisionIdSchema, |
| 103 | name: Schema.String, | 103 | name: Schema.String, |
| 104 | kind: SourceKindSchema, | 104 | kind: SourceKindSchema, |
| 105 | endpoint: Schema.String, | 105 | endpoint: Schema.String, |
| @@ -118,8 +118,8 @@ export const StoredSourceRecordSchema = Schema.transform( · 12 unmodified lines | |||
| 118 | decode: (row) => ({ | 118 | decode: (row) => ({ |
| 119 | id: row.sourceId, | 119 | id: row.sourceId, |
| 120 | workspaceId: row.workspaceId, | 120 | workspaceId: row.workspaceId, |
| 121 | recipeId: row.recipeId, | 121 | catalogId: row.catalogId, |
| 122 | recipeRevisionId: row.recipeRevisionId, | 122 | catalogRevisionId: row.catalogRevisionId, |
| 123 | name: row.name, | 123 | name: row.name, |
| 124 | kind: row.kind, | 124 | kind: row.kind, |
| 125 | endpoint: row.endpoint, | 125 | endpoint: row.endpoint, |
| @@ -136,8 +136,8 @@ export const StoredSourceRecordSchema = Schema.transform( · 10 unmodified lines | |||
| 136 | encode: (source) => ({ | 136 | encode: (source) => ({ |
| 137 | workspaceId: source.workspaceId, | 137 | workspaceId: source.workspaceId, |
| 138 | sourceId: source.id, | 138 | sourceId: source.id, |
| 139 | recipeId: source.recipeId, | 139 | catalogId: source.catalogId, |
| 140 | recipeRevisionId: source.recipeRevisionId, | 140 | catalogRevisionId: source.catalogRevisionId, |
| 141 | name: source.name, | 141 | name: source.name, |
| 142 | kind: source.kind, | 142 | kind: source.kind, |
| 143 | endpoint: source.endpoint, | 143 | endpoint: source.endpoint, |
| @@ -3,55 +3,55 @@ import * as Either from "effect/Either"; · 2 unmodified lines | |||
| 3 | import * as Schema from "effect/Schema"; | 3 | import * as Schema from "effect/Schema"; |
| 4 | 4 | ||
| 5 | import { | 5 | import { |
| 6 | StoredSourceRecipeOperationRecordSchema, | 6 | SourceInspectionToolDetailSchema, |
| 7 | StoredSourceCatalogRevisionRecordSchema, | ||
| 7 | } from "./index"; | 8 | } from "./index"; |
| 8 | 9 | ||
| 9 | describe("control-plane-schema", () => { | 10 | describe("control-plane-schema", () => { |
| 10 | it("accepts canonical source recipe operation rows and rejects invalid transport kinds", () => { | 11 | it("accepts snapshot-native inspection detail rows", () => { |
| 11 | const decode = Schema.decodeUnknownEither(StoredSourceRecipeOperationRecordSchema); | 12 | const decode = Schema.decodeUnknownEither(SourceInspectionToolDetailSchema); |
| 12 | 13 | ||
| 13 | const operationRecord = decode({ | 14 | const detail = decode({ |
| 14 | id: "src_recipe_op_1", | 15 | summary: { |
| 15 | recipeRevisionId: "src_recipe_rev_1", | 16 | path: "github.issues.list", |
| 16 | operationKey: "getRepo", | 17 | sourceKey: "src_1", |
| 17 | transportKind: "http", | 18 | protocol: "http", |
| 18 | toolId: "getRepo", | 19 | toolId: "list", |
| 19 | title: "Get Repo", | 20 | rawToolId: "issues.list", |
| 20 | description: "Read a repository", | 21 | operationId: "listIssues", |
| 21 | operationKind: "read", | 22 | group: "issues", |
| 22 | searchText: "get repo", | 23 | leaf: "list", |
| 23 | inputSchemaJson: null, | 24 | tags: ["issues"], |
| 24 | outputSchemaJson: null, | 25 | method: "GET", |
| 25 | providerKind: "openapi", | 26 | pathTemplate: "/issues", |
| 26 | providerDataJson: JSON.stringify({ | 27 | }, |
| 27 | kind: "openapi", | 28 | definitionJson: "{}", |
| 28 | }), | 29 | documentationJson: "{}", |
| 29 | createdAt: 1, | 30 | nativeJson: "{}", |
| 30 | updatedAt: 1, | 31 | callSchemaJson: null, |
| 32 | resultSchemaJson: null, | ||
| 33 | exampleCallJson: null, | ||
| 34 | exampleResultJson: null, | ||
| 31 | }); | 35 | }); |
| 32 | 36 | ||
| 33 | expect(Either.isRight(operationRecord)).toBe(true); | 37 | expect(Either.isRight(detail)).toBe(true); |
| 38 | }); | ||
| 39 | |||
| 40 | it("rejects malformed catalog revisions", () => { | ||
| 41 | const decode = Schema.decodeUnknownEither(StoredSourceCatalogRevisionRecordSchema); | ||
| 34 | 42 | ||
| 35 | const invalidRecord = decode({ | 43 | const invalidRevision = decode({ |
| 36 | id: "src_recipe_op_2", | 44 | id: "src_catalog_rev_1", |
| 37 | recipeRevisionId: "src_recipe_rev_1", | 45 | catalogId: "src_catalog_1", |
| 38 | operationKey: "viewer", | 46 | revisionNumber: "1", |
| 39 | transportKind: "ftp", | 47 | sourceConfigJson: "{}", |
| 40 | toolId: "viewer", | 48 | importMetadataJson: null, |
| 41 | title: "Viewer", | 49 | importMetadataHash: null, |
| 42 | description: null, | 50 | snapshotHash: null, |
| 43 | operationKind: "read", | ||
| 44 | searchText: "viewer", | ||
| 45 | inputSchemaJson: null, | ||
| 46 | outputSchemaJson: null, | ||
| 47 | providerKind: "graphql", | ||
| 48 | providerDataJson: JSON.stringify({ | ||
| 49 | kind: "graphql", | ||
| 50 | }), | ||
| 51 | createdAt: 1, | 51 | createdAt: 1, |
| 52 | updatedAt: 1, | 52 | updatedAt: 1, |
| 53 | }); | 53 | }); |
| 54 | 54 | ||
| 55 | expect(Either.isLeft(invalidRecord)).toBe(true); | 55 | expect(Either.isLeft(invalidRevision)).toBe(true); |
| 56 | }); | 56 | }); |
| 57 | }); | 57 | }); |
| @@ -67,8 +67,8 @@ type StoredToolArtifactRecord = { · 66 unmodified lines | |||
| 67 | description: string | null; | 67 | description: string | null; |
| 68 | searchNamespace: string; | 68 | searchNamespace: string; |
| 69 | searchText: string; | 69 | searchText: string; |
| 70 | inputSchemaJson: string | null; | 70 | inputSchema: unknown | null; |
| 71 | outputSchemaJson: string | null; | 71 | outputSchema: unknown | null; |
| 72 | providerKind: "openapi"; | 72 | providerKind: "openapi"; |
| 73 | openApiMethod: "get" | "put" | "post" | "delete" | "patch" | "head" | "options" | "trace"; | 73 | openApiMethod: "get" | "put" | "post" | "delete" | "patch" | "head" | "options" | "trace"; |
| 74 | openApiPathTemplate: string | null; | 74 | openApiPathTemplate: string | null; |
| @@ -89,10 +89,10 @@ const toDescriptor = ( · 14 unmodified lines | |||
| 89 | sourceKey: artifact.sourceId, | 89 | sourceKey: artifact.sourceId, |
| 90 | description: artifact.description ?? artifact.title ?? undefined, | 90 | description: artifact.description ?? artifact.title ?? undefined, |
| 91 | interaction: "auto" as const, | 91 | interaction: "auto" as const, |
| 92 | inputType: includeSchemas && artifact.inputSchemaJson ? "object" : undefined, | 92 | inputType: includeSchemas && artifact.inputSchema ? "object" : undefined, |
| 93 | outputType: includeSchemas && artifact.outputSchemaJson ? "output" : undefined, | 93 | outputType: includeSchemas && artifact.outputSchema ? "output" : undefined, |
| 94 | inputSchemaJson: includeSchemas ? artifact.inputSchemaJson ?? undefined : undefined, | 94 | inputSchema: includeSchemas ? artifact.inputSchema ?? undefined : undefined, |
| 95 | outputSchemaJson: includeSchemas ? artifact.outputSchemaJson ?? undefined : undefined, | 95 | outputSchema: includeSchemas ? artifact.outputSchema ?? undefined : undefined, |
| 96 | }); | 96 | }); |
| 97 | 97 | ||
| 98 | type SourceCallContext = { | 98 | type SourceCallContext = { |
| @@ -359,7 +359,6 @@ const createWorkspaceToolCatalog = (input: { · 260 unmodified lines | |||
| 359 | path, | 359 | path, |
| 360 | }).then((artifact) => (artifact ? toDescriptor(artifact, includeSchemas) : null)) | 360 | }).then((artifact) => (artifact ? toDescriptor(artifact, includeSchemas) : null)) |
| 361 | ), | 361 | ), |
| 362 | getSchemaBundle: () => Effect.succeed(null), | ||
| 363 | searchTools: ({ query, namespace, limit }) => | 362 | searchTools: ({ query, namespace, limit }) => |
| 364 | Effect.promise(() => | 363 | Effect.promise(() => |
| 365 | input.toolStore.list({ | 364 | input.toolStore.list({ |
| @@ -522,8 +521,8 @@ const openApiArtifact = (input: { · 156 unmodified lines | |||
| 522 | method: NonNullable<StoredToolArtifactRecord["openApiMethod"]>; | 521 | method: NonNullable<StoredToolArtifactRecord["openApiMethod"]>; |
| 523 | pathTemplate: string; | 522 | pathTemplate: string; |
| 524 | operationHash?: string; | 523 | operationHash?: string; |
| 525 | inputSchemaJson?: string | null; | 524 | inputSchema?: unknown | null; |
| 526 | outputSchemaJson?: string | null; | 525 | outputSchema?: unknown | null; |
| 527 | }): StoredToolArtifactRecord => { | 526 | }): StoredToolArtifactRecord => { |
| 528 | const path = input.path as string; | 527 | const path = input.path as string; |
| 529 | const description = | 528 | const description = |
| @@ -545,8 +544,8 @@ const openApiArtifact = (input: { · 15 unmodified lines | |||
| 545 | input.method.toUpperCase(), | 544 | input.method.toUpperCase(), |
| 546 | input.pathTemplate, | 545 | input.pathTemplate, |
| 547 | ), | 546 | ), |
| 548 | inputSchemaJson: input.inputSchemaJson ?? null, | 547 | inputSchema: input.inputSchema ?? null, |
| 549 | outputSchemaJson: input.outputSchemaJson ?? null, | 548 | outputSchema: input.outputSchema ?? null, |
| 550 | providerKind: "openapi", | 549 | providerKind: "openapi", |
| 551 | openApiMethod: input.method, | 550 | openApiMethod: input.method, |
| 552 | openApiPathTemplate: input.pathTemplate, | 551 | openApiPathTemplate: input.pathTemplate, |
| @@ -783,7 +782,7 @@ describe("source runtime", () => { · 230 unmodified lines | |||
| 783 | "Workflow:", | 782 | "Workflow:", |
| 784 | '1) const matches = await tools.discover({ query: "<intent>", limit: 12 });', | 783 | '1) const matches = await tools.discover({ query: "<intent>", limit: 12 });', |
| 785 | "2) const details = await tools.describe.tool({ path, includeSchemas: true });", | 784 | "2) const details = await tools.describe.tool({ path, includeSchemas: true });", |
| 786 | "3) If details.schemaBundleId is present, fetch it once with tools.describe.schemaBundle({ id }).", | 785 | "3) Read details.inputSchema/details.outputSchema when you need the projected shape.", |
| 787 | "4) Call selected tools.<path>(input).", | 786 | "4) Call selected tools.<path>(input).", |
| 788 | "Do not use fetch; use tools.* only.", | 787 | "Do not use fetch; use tools.* only.", |
| 789 | ].join("\n"), | 788 | ].join("\n"), |
| @@ -834,7 +833,7 @@ describe("source runtime", () => { · 44 unmodified lines | |||
| 834 | description: "GET /repos/{owner}/{repo}", | 833 | description: "GET /repos/{owner}/{repo}", |
| 835 | interaction: "auto", | 834 | interaction: "auto", |
| 836 | inputType: "{ owner: string, repo: string }", | 835 | inputType: "{ owner: string, repo: string }", |
| 837 | outputType: "unknown", | 836 | outputType: "void", |
| 838 | }, | 837 | }, |
| 839 | ], | 838 | ], |
| 840 | total: 1, | 839 | total: 1, |
| @@ -853,7 +852,7 @@ describe("source runtime", () => { · 12 unmodified lines | |||
| 853 | "Workflow:", | 852 | "Workflow:", |
| 854 | '1) const matches = await tools.discover({ query: "<intent>", limit: 12 });', | 853 | '1) const matches = await tools.discover({ query: "<intent>", limit: 12 });', |
| 855 | "2) const details = await tools.describe.tool({ path, includeSchemas: true });", | 854 | "2) const details = await tools.describe.tool({ path, includeSchemas: true });", |
| 856 | "3) If details.schemaBundleId is present, fetch it once with tools.describe.schemaBundle({ id }).", | 855 | "3) Read details.inputSchema/details.outputSchema when you need the projected shape.", |
| 857 | "4) Call selected tools.<path>(input).", | 856 | "4) Call selected tools.<path>(input).", |
| 858 | "Do not use fetch; use tools.* only.", | 857 | "Do not use fetch; use tools.* only.", |
| 859 | ].join("\n"), | 858 | ].join("\n"), |
| @@ -19,7 +19,6 @@ import type { · 18 unmodified lines | |||
| 19 | SourceDiscoveryResult, | 19 | SourceDiscoveryResult, |
| 20 | SourceInspection, | 20 | SourceInspection, |
| 21 | SourceInspectionDiscoverResult, | 21 | SourceInspectionDiscoverResult, |
| 22 | SourceInspectionSchemaBundle, | ||
| 23 | SourceInspectionToolDetail, | 22 | SourceInspectionToolDetail, |
| 24 | StartSourceOAuthPayload, | 23 | StartSourceOAuthPayload, |
| 25 | StartSourceOAuthResult, | 24 | StartSourceOAuthResult, |
| @@ -59,13 +58,6 @@ type SourceToolDetailKeyParts = readonly [ · 33 unmodified lines | |||
| 59 | Source["id"], | 58 | Source["id"], |
| 60 | string | null, | 59 | string | null, |
| 61 | ]; | 60 | ]; |
| 62 | type SourceSchemaBundleKeyParts = readonly [ | ||
| 63 | boolean, | ||
| 64 | Source["workspaceId"], | ||
| 65 | string, | ||
| 66 | Source["id"], | ||
| 67 | string | null, | ||
| 68 | ]; | ||
| 69 | type SourceDiscoveryKeyParts = readonly [ | 61 | type SourceDiscoveryKeyParts = readonly [ |
| 70 | boolean, | 62 | boolean, |
| 71 | Source["workspaceId"], | 63 | Source["workspaceId"], |
| @@ -86,7 +78,6 @@ type ActiveQueryCollections = { · 14 unmodified lines | |||
| 86 | sources: Set<string>; | 78 | sources: Set<string>; |
| 87 | inspections: Set<string>; | 79 | inspections: Set<string>; |
| 88 | toolDetails: Set<string>; | 80 | toolDetails: Set<string>; |
| 89 | schemaBundles: Set<string>; | ||
| 90 | discoveries: Set<string>; | 81 | discoveries: Set<string>; |
| 91 | }; | 82 | }; |
| 92 | 83 | ||
| @@ -162,17 +153,6 @@ const encodeToolDetailKey = ( · 69 unmodified lines | |||
| 162 | ): string => | 153 | ): string => |
| 163 | encodeAtomKey([enabled, workspaceId, accountId, sourceId, toolPath] satisfies SourceToolDetailKeyParts); | 154 | encodeAtomKey([enabled, workspaceId, accountId, sourceId, toolPath] satisfies SourceToolDetailKeyParts); |
| 164 | 155 | ||
| 165 | const encodeSchemaBundleKey = ( | ||
| 166 | enabled: boolean, | ||
| 167 | workspaceId: Source["workspaceId"], | ||
| 168 | accountId: string, | ||
| 169 | sourceId: Source["id"], | ||
| 170 | schemaBundleId: string | null, | ||
| 171 | ): string => | ||
| 172 | encodeAtomKey( | ||
| 173 | [enabled, workspaceId, accountId, sourceId, schemaBundleId] satisfies SourceSchemaBundleKeyParts, | ||
| 174 | ); | ||
| 175 | |||
| 176 | const encodeDiscoveryKey = ( | 156 | const encodeDiscoveryKey = ( |
| 177 | enabled: boolean, | 157 | enabled: boolean, |
| 178 | workspaceId: Source["workspaceId"], | 158 | workspaceId: Source["workspaceId"], |
| @@ -367,25 +347,6 @@ const sourceInspectionToolAtom = Atom.family((key: string) => { · 188 unmodified lines | |||
| 367 | ).pipe(Atom.keepAlive); | 347 | ).pipe(Atom.keepAlive); |
| 368 | }); | 348 | }); |
| 369 | 349 | ||
| 370 | const sourceInspectionSchemaBundleAtom = Atom.family((key: string) => { | ||
| 371 | const [enabled, workspaceId, accountId, sourceId, schemaBundleId] = decodeAtomKey<SourceSchemaBundleKeyParts>(key); | ||
| 372 | |||
| 373 | return Atom.make( | ||
| 374 | enabled && schemaBundleId | ||
| 375 | ? controlPlaneRequest({ | ||
| 376 | accountId, | ||
| 377 | execute: (client) => client.sources.inspectionSchemaBundle({ | ||
| 378 | path: { | ||
| 379 | workspaceId, | ||
| 380 | sourceId, | ||
| 381 | schemaBundleId, | ||
| 382 | }, | ||
| 383 | }), | ||
| 384 | }) | ||
| 385 | : Effect.succeed<SourceInspectionSchemaBundle | null>(null), | ||
| 386 | ).pipe(Atom.keepAlive); | ||
| 387 | }); | ||
| 388 | |||
| 389 | const sourceDiscoveryAtom = Atom.family((key: string) => { | 350 | const sourceDiscoveryAtom = Atom.family((key: string) => { |
| 390 | const [enabled, workspaceId, accountId, sourceId, query, limit] = decodeAtomKey<SourceDiscoveryKeyParts>(key); | 351 | const [enabled, workspaceId, accountId, sourceId, query, limit] = decodeAtomKey<SourceDiscoveryKeyParts>(key); |
| 391 | 352 | ||
| @@ -547,7 +508,6 @@ const createActiveQueryCollections = (): ActiveQueryCollections => ({ · 155 unmodified lines | |||
| 547 | sources: new Set(), | 508 | sources: new Set(), |
| 548 | inspections: new Set(), | 509 | inspections: new Set(), |
| 549 | toolDetails: new Set(), | 510 | toolDetails: new Set(), |
| 550 | schemaBundles: new Set(), | ||
| 551 | discoveries: new Set(), | 511 | discoveries: new Set(), |
| 552 | }); | 512 | }); |
| 553 | 513 | ||
| @@ -602,13 +562,6 @@ const invalidateTrackedQueries = ( · 48 unmodified lines | |||
| 602 | } | 562 | } |
| 603 | }); | 563 | }); |
| 604 | 564 | ||
| 605 | activeQueries.schemaBundles.forEach((key) => { | ||
| 606 | const [enabled, workspaceId, accountId, sourceId] = decodeAtomKey<SourceSchemaBundleKeyParts>(key); | ||
| 607 | if (enabled && targetMatches(target, workspaceId, accountId, sourceId)) { | ||
| 608 | registry.refresh(sourceInspectionSchemaBundleAtom(key)); | ||
| 609 | } | ||
| 610 | }); | ||
| 611 | |||
| 612 | activeQueries.discoveries.forEach((key) => { | 565 | activeQueries.discoveries.forEach((key) => { |
| 613 | const [enabled, workspaceId, accountId, sourceId] = decodeAtomKey<SourceDiscoveryKeyParts>(key); | 566 | const [enabled, workspaceId, accountId, sourceId] = decodeAtomKey<SourceDiscoveryKeyParts>(key); |
| 614 | if (enabled && targetMatches(target, workspaceId, accountId, sourceId)) { | 567 | if (enabled && targetMatches(target, workspaceId, accountId, sourceId)) { |
| @@ -990,27 +943,6 @@ export const useSourceToolDetail = ( · 375 unmodified lines | |||
| 990 | return workspace.enabled ? detail : pendingLoadable(workspace.workspace); | 943 | return workspace.enabled ? detail : pendingLoadable(workspace.workspace); |
| 991 | }; | 944 | }; |
| 992 | 945 | ||
| 993 | export const useSourceSchemaBundle = ( | ||
| 994 | sourceId: string, | ||
| 995 | schemaBundleId: string | null, | ||
| 996 | ): Loadable<SourceInspectionSchemaBundle | null> => { | ||
| 997 | const workspace = useWorkspaceRequestContext(); | ||
| 998 | const requestedSourceId = workspace.enabled | ||
| 999 | ? (sourceId as Source["id"]) | ||
| 1000 | : PLACEHOLDER_SOURCE_ID; | ||
| 1001 | const key = encodeSchemaBundleKey( | ||
| 1002 | workspace.enabled, | ||
| 1003 | workspace.workspaceId, | ||
| 1004 | workspace.accountId, | ||
| 1005 | requestedSourceId, | ||
| 1006 | schemaBundleId, | ||
| 1007 | ); | ||
| 1008 | useTrackActiveKey("schemaBundles", key, workspace.enabled && schemaBundleId !== null); | ||
| 1009 | const schemaBundle = useLoadableAtom(sourceInspectionSchemaBundleAtom(key)); | ||
| 1010 | |||
| 1011 | return workspace.enabled ? schemaBundle : pendingLoadable(workspace.workspace); | ||
| 1012 | }; | ||
| 1013 | |||
| 1014 | export const useSourceDiscovery = (input: { | 946 | export const useSourceDiscovery = (input: { |
| 1015 | sourceId: string; | 947 | sourceId: string; |
| 1016 | query: string; | 948 | query: string; |