Unified · Split
| @@ -27,7 +27,7 @@ import { · 26 unmodified lines | ||
| 27 | 27 | type ResolveExecutionEnvironment, |
| 28 | 28 | resolveLocalWorkspaceContext, |
| 29 | 29 | SourceIdSchema, |
| 30 | − SourceRecipeRevisionIdSchema, | |
| 30 | + SourceCatalogRevisionIdSchema, | |
| 31 | 31 | writeLocalSourceArtifact, |
| 32 | 32 | writeProjectLocalExecutorConfig, |
| 33 | 33 | } from "@executor/control-plane"; |
| @@ -144,7 +144,7 @@ const writeConfiguredLocalMcpSource = (input: { · 110 unmodified lines | ||
| 144 | 144 | }; |
| 145 | 145 | |
| 146 | 146 | const materialization = materializationFromMcpManifestEntries({ |
| 147 | − recipeRevisionId: SourceRecipeRevisionIdSchema.make("src_recipe_rev_materialization"), | |
| 147 | + catalogRevisionId: SourceCatalogRevisionIdSchema.make("src_catalog_rev_materialization"), | |
| 148 | 148 | endpoint: input.endpoint, |
| 149 | 149 | manifestEntries: [{ |
| 150 | 150 | toolId: "gated_echo", |
| @@ -35,9 +35,6 @@ const catalog: ToolCatalog = { · 34 unmodified lines | ||
| 35 | 35 | outputType: "object", |
| 36 | 36 | }); |
| 37 | 37 | }, |
| 38 | − getSchemaBundle() { | |
| 39 | − return Effect.succeed(null); | |
| 40 | − }, | |
| 41 | 38 | searchTools({ limit }) { |
| 42 | 39 | return Effect.succeed( |
| 43 | 40 | [ |
| @@ -13,7 +13,6 @@ import type { · 12 unmodified lines | ||
| 13 | 13 | ToolMap, |
| 14 | 14 | ToolNamespace, |
| 15 | 15 | ToolPath, |
| 16 | − ToolSchemaBundle, | |
| 17 | 16 | } from "./types"; |
| 18 | 17 | |
| 19 | 18 | const tokenize = (value: string): string[] => |
| @@ -46,8 +45,8 @@ const projectDescriptor = (input: { · 26 unmodified lines | ||
| 46 | 45 | |
| 47 | 46 | return { |
| 48 | 47 | ...descriptor, |
| 49 | − inputSchemaJson: undefined, | |
| 50 | − outputSchemaJson: undefined, | |
| 48 | + inputSchema: undefined, | |
| 49 | + outputSchema: undefined, | |
| 51 | 50 | }; |
| 52 | 51 | }; |
| 53 | 52 | |
| @@ -162,7 +161,6 @@ export function createToolCatalogFromTools(input: { · 108 unmodified lines | ||
| 162 | 161 | |
| 163 | 162 | export function createToolCatalogFromEntries(input: { |
| 164 | 163 | entries: ReadonlyArray<ToolCatalogEntry>; |
| 165 | − getSchemaBundle?: (input: { id: string }) => Effect.Effect<ToolSchemaBundle | null, unknown>; | |
| 166 | 164 | }): ToolCatalog { |
| 167 | 165 | const entries = [...input.entries]; |
| 168 | 166 | const byPath = new Map( |
| @@ -213,10 +211,6 @@ export function createToolCatalogFromEntries(input: { · 44 unmodified lines | ||
| 213 | 211 | }) |
| 214 | 212 | : null, |
| 215 | 213 | ), |
| 216 | − getSchemaBundle: ({ id }) => | |
| 217 | − input.getSchemaBundle | |
| 218 | − ? input.getSchemaBundle({ id }) | |
| 219 | − : Effect.succeed(null), | |
| 220 | 214 | searchTools: ({ query, namespace, limit }) => { |
| 221 | 215 | const queryTokens = tokenize(query); |
| 222 | 216 | |
| @@ -283,18 +277,6 @@ export function mergeToolCatalogs(input: { · 60 unmodified lines | ||
| 283 | 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 | 280 | searchTools: ({ query, namespace, limit }) => |
| 299 | 281 | Effect.gen(function* () { |
| 300 | 282 | const groups = yield* Effect.forEach( |
| @@ -357,7 +339,6 @@ export function createDiscoveryPrimitivesFromToolCatalog(input: { · 56 unmodified lines | ||
| 357 | 339 | const describe: DescribePrimitive = { |
| 358 | 340 | tool: ({ path, includeSchemas = false }) => |
| 359 | 341 | catalog.getToolByPath({ path, includeSchemas }), |
| 360 | − schemaBundle: ({ id }) => catalog.getSchemaBundle({ id }), | |
| 361 | 342 | }; |
| 362 | 343 | |
| 363 | 344 | const discover: DiscoverPrimitive = ({ |
| @@ -406,9 +387,8 @@ export function createDiscoveryPrimitivesFromToolCatalog(input: { · 42 unmodified lines | ||
| 406 | 387 | outputType: descriptor.outputType, |
| 407 | 388 | ...(includeSchemas |
| 408 | 389 | ? { |
| 409 | − inputSchemaJson: descriptor.inputSchemaJson, | |
| 410 | − outputSchemaJson: descriptor.outputSchemaJson, | |
| 411 | − schemaBundleId: descriptor.schemaBundleId, | |
| 390 | + inputSchema: descriptor.inputSchema, | |
| 391 | + outputSchema: descriptor.outputSchema, | |
| 412 | 392 | } |
| 413 | 393 | : {}), |
| 414 | 394 | }; |
| @@ -471,7 +451,7 @@ export function buildDynamicExecuteDescription(input: { · 56 unmodified lines | ||
| 471 | 451 | "Workflow:", |
| 472 | 452 | '1) const matches = await tools.discover({ query: "<intent>", limit: 12 });', |
| 473 | 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 | 455 | "4) Call selected tools.<path>(input).", |
| 476 | 456 | "Do not use fetch; use tools.* only.", |
| 477 | 457 | ].join("\n"); |
| @@ -326,13 +326,12 @@ describe("codemode-core", () => { · 325 unmodified lines | ||
| 326 | 326 | .slice(0, limit) |
| 327 | 327 | .map((descriptor) => ({ |
| 328 | 328 | ...descriptor, |
| 329 | − inputSchemaJson: includeSchemas ? descriptor.inputSchemaJson : undefined, | |
| 330 | − outputSchemaJson: includeSchemas ? descriptor.outputSchemaJson : undefined, | |
| 329 | + inputSchema: includeSchemas ? descriptor.inputSchema : undefined, | |
| 330 | + outputSchema: includeSchemas ? descriptor.outputSchema : undefined, | |
| 331 | 331 | })), |
| 332 | 332 | ), |
| 333 | 333 | getToolByPath: ({ path }) => |
| 334 | 334 | Effect.succeed(descriptors[path] ?? null), |
| 335 | − getSchemaBundle: () => Effect.succeed(null), | |
| 336 | 335 | searchTools: () => |
| 337 | 336 | Effect.succeed([ |
| 338 | 337 | { path: asToolPath("source.issues.create"), score: 0.93 }, |
| @@ -369,7 +368,7 @@ describe("codemode-core", () => { · 30 unmodified lines | ||
| 369 | 368 | description: "Get repository details", |
| 370 | 369 | inputType: "{ owner: string; repo: string }", |
| 371 | 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 | 392 | const namespaces = yield* catalog.listNamespaces({ limit: 10 }); |
| @@ -414,7 +402,7 @@ describe("codemode-core", () => { · 9 unmodified lines | ||
| 414 | 402 | includeSchemas: false, |
| 415 | 403 | }); |
| 416 | 404 | expect(listed[0]?.path).toBe("github.repos.getRepo"); |
| 417 | − expect(listed[0]?.inputSchemaJson).toBeUndefined(); | |
| 405 | + expect(listed[0]?.inputSchema).toBeUndefined(); | |
| 418 | 406 | |
| 419 | 407 | const discovered = yield* catalog.searchTools({ |
| 420 | 408 | query: "repository issues", |
| @@ -426,10 +414,7 @@ describe("codemode-core", () => { · 5 unmodified lines | ||
| 426 | 414 | path: asToolPath("github.repos.getRepo"), |
| 427 | 415 | includeSchemas: true, |
| 428 | 416 | }); |
| 429 | − expect(described?.inputSchemaJson).toContain('"type"'); | |
| 430 | − | |
| 431 | − const bundle = yield* catalog.getSchemaBundle({ id: "bundle_1" }); | |
| 432 | − expect(bundle?.id).toBe("bundle_1"); | |
| 417 | + expect(described?.inputSchema).toEqual({ type: "object" }); | |
| 433 | 418 | }), |
| 434 | 419 | ); |
| 435 | 420 | |
| @@ -123,3 +123,19 @@ export const typeSignatureFromSchemaJson = ( · 122 unmodified lines | ||
| 123 | 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 | 6 | import { |
| 7 | 7 | ToolDescriptorSchema, |
| 8 | 8 | ToolNamespaceSchema, |
| 9 | − ToolSchemaBundleSchema, | |
| 10 | 9 | type ToolCatalog, |
| 11 | 10 | type ToolMap, |
| 12 | 11 | type ToolPath, |
| @@ -52,16 +51,6 @@ const describeToolOutputSchema = Schema.standardSchemaV1( · 39 unmodified lines | ||
| 52 | 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 | 54 | const discoverInputSchema = Schema.standardSchemaV1( |
| 66 | 55 | Schema.Struct({ |
| 67 | 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 | 173 | tools.discover = toTool({ |
| 203 | 174 | tool: { |
| 204 | 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 | 2 | import * as Data from "effect/Data"; |
| 3 | 3 | import * as Effect from "effect/Effect"; |
| 4 | 4 | import * as JSONSchema from "effect/JSONSchema"; |
| 5 | −import { typeSignatureFromSchemaJson } from "./schema-types"; | |
| 5 | +import { typeSignatureFromSchema } from "./schema-types"; | |
| 6 | 6 | |
| 7 | 7 | import type { |
| 8 | 8 | ElicitationRequest, |
| @@ -336,7 +336,7 @@ export const toExecutorTool = wrapTool; · 327 unmodified lines | ||
| 336 | 336 | const isToolDefinition = (value: ToolInput): value is ToolDefinition => |
| 337 | 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 | 340 | if (value === undefined || value === null) { |
| 341 | 341 | return undefined; |
| 342 | 342 | } |
| @@ -347,20 +347,20 @@ const stringifySchema = (value: unknown): string | undefined => { · 4 unmodified lines | ||
| 347 | 347 | && value !== null |
| 348 | 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 | 354 | } catch { |
| 355 | 355 | return undefined; |
| 356 | 356 | } |
| 357 | 357 | }; |
| 358 | 358 | |
| 359 | −const inferTypeFromSchemaJson = ( | |
| 360 | − schemaJson: string | undefined, | |
| 359 | +const inferTypeFromSchemaValue = ( | |
| 360 | + schema: unknown, | |
| 361 | 361 | fallback: string, |
| 362 | 362 | maxLength: number = 240, |
| 363 | −): string => typeSignatureFromSchemaJson(schemaJson, fallback, maxLength); | |
| 363 | +): string => typeSignatureFromSchema(schema, fallback, maxLength); | |
| 364 | 364 | |
| 365 | 365 | export function createToolsFromRecord(input: { |
| 366 | 366 | tools: Record<string, ExecutableTool>; |
| @@ -418,13 +418,13 @@ export function toolDescriptorsFromTools(input: { · 51 unmodified lines | ||
| 418 | 418 | return resolvedTools.map((entry) => { |
| 419 | 419 | const metadata = entry.metadata; |
| 420 | 420 | const definition = entry.tool; |
| 421 | − const inputSchemaJson = | |
| 422 | − metadata?.inputSchemaJson | |
| 423 | − ?? stringifySchema(definition.inputSchema) | |
| 424 | − ?? stringifySchema(definition.parameters); | |
| 425 | − const outputSchemaJson = | |
| 426 | − metadata?.outputSchemaJson | |
| 427 | − ?? stringifySchema(definition.outputSchema); | |
| 421 | + const inputSchema = | |
| 422 | + metadata?.inputSchema | |
| 423 | + ?? deriveSchemaValue(definition.inputSchema) | |
| 424 | + ?? deriveSchemaValue(definition.parameters); | |
| 425 | + const outputSchema = | |
| 426 | + metadata?.outputSchema | |
| 427 | + ?? deriveSchemaValue(definition.outputSchema); | |
| 428 | 428 | |
| 429 | 429 | return { |
| 430 | 430 | path: entry.path, |
| @@ -433,21 +433,20 @@ export function toolDescriptorsFromTools(input: { · 2 unmodified lines | ||
| 433 | 433 | interaction: metadata?.interaction, |
| 434 | 434 | elicitation: metadata?.elicitation, |
| 435 | 435 | inputType: |
| 436 | − metadata?.inputType ?? inferTypeFromSchemaJson(inputSchemaJson, "unknown"), | |
| 436 | + metadata?.inputType ?? inferTypeFromSchemaValue(inputSchema, "unknown"), | |
| 437 | 437 | outputType: |
| 438 | − metadata?.outputType ?? inferTypeFromSchemaJson(outputSchemaJson, "unknown"), | |
| 439 | − inputSchemaJson, | |
| 440 | − outputSchemaJson, | |
| 441 | − ...(metadata?.schemaBundleId ? { schemaBundleId: metadata.schemaBundleId } : {}), | |
| 442 | − ...(metadata?.exampleInputJson | |
| 443 | − ? { exampleInputJson: metadata.exampleInputJson } | |
| 438 | + metadata?.outputType ?? inferTypeFromSchemaValue(outputSchema, "unknown"), | |
| 439 | + ...(inputSchema !== undefined ? { inputSchema } : {}), | |
| 440 | + ...(outputSchema !== undefined ? { outputSchema } : {}), | |
| 441 | + ...(metadata?.exampleInput !== undefined | |
| 442 | + ? { exampleInput: metadata.exampleInput } | |
| 444 | 443 | : {}), |
| 445 | − ...(metadata?.exampleOutputJson | |
| 446 | − ? { exampleOutputJson: metadata.exampleOutputJson } | |
| 444 | + ...(metadata?.exampleOutput !== undefined | |
| 445 | + ? { exampleOutput: metadata.exampleOutput } | |
| 447 | 446 | : {}), |
| 448 | 447 | ...(metadata?.providerKind ? { providerKind: metadata.providerKind } : {}), |
| 449 | − ...(metadata?.providerDataJson | |
| 450 | − ? { providerDataJson: metadata.providerDataJson } | |
| 448 | + ...(metadata?.providerData !== undefined | |
| 449 | + ? { providerData: metadata.providerData } | |
| 451 | 450 | : {}), |
| 452 | 451 | } satisfies ToolDescriptor; |
| 453 | 452 | }); |
| @@ -80,14 +80,13 @@ export type ToolMetadata = { · 79 unmodified lines | ||
| 80 | 80 | elicitation?: ElicitationRequest; |
| 81 | 81 | inputType?: string; |
| 82 | 82 | outputType?: string; |
| 83 | − inputSchemaJson?: string; | |
| 84 | − outputSchemaJson?: string; | |
| 85 | − schemaBundleId?: string; | |
| 86 | − exampleInputJson?: string; | |
| 87 | − exampleOutputJson?: string; | |
| 83 | + inputSchema?: unknown; | |
| 84 | + outputSchema?: unknown; | |
| 85 | + exampleInput?: unknown; | |
| 86 | + exampleOutput?: unknown; | |
| 88 | 87 | sourceKey?: string; |
| 89 | 88 | providerKind?: string; |
| 90 | − providerDataJson?: string; | |
| 89 | + providerData?: unknown; | |
| 91 | 90 | }; |
| 92 | 91 | |
| 93 | 92 | export type ToolExecutionContext = { |
| @@ -134,13 +133,12 @@ export type ToolDescriptor = { · 40 unmodified lines | ||
| 134 | 133 | elicitation?: ElicitationRequest; |
| 135 | 134 | inputType?: string; |
| 136 | 135 | outputType?: string; |
| 137 | − inputSchemaJson?: string; | |
| 138 | − outputSchemaJson?: string; | |
| 139 | − schemaBundleId?: string; | |
| 140 | − exampleInputJson?: string; | |
| 141 | − exampleOutputJson?: string; | |
| 136 | + inputSchema?: unknown; | |
| 137 | + outputSchema?: unknown; | |
| 138 | + exampleInput?: unknown; | |
| 139 | + exampleOutput?: unknown; | |
| 142 | 140 | providerKind?: string; |
| 143 | − providerDataJson?: string; | |
| 141 | + providerData?: unknown; | |
| 144 | 142 | }; |
| 145 | 143 | |
| 146 | 144 | export const ToolDescriptorSchema = Schema.Struct({ |
| @@ -153,27 +151,12 @@ export const ToolDescriptorSchema = Schema.Struct({ · 6 unmodified lines | ||
| 153 | 151 | elicitation: Schema.optional(Schema.Unknown), |
| 154 | 152 | inputType: Schema.optional(Schema.String), |
| 155 | 153 | outputType: Schema.optional(Schema.String), |
| 156 | − inputSchemaJson: Schema.optional(Schema.String), | |
| 157 | − outputSchemaJson: Schema.optional(Schema.String), | |
| 158 | − schemaBundleId: Schema.optional(Schema.String), | |
| 159 | − exampleInputJson: Schema.optional(Schema.String), | |
| 160 | − exampleOutputJson: Schema.optional(Schema.String), | |
| 154 | + inputSchema: Schema.optional(Schema.Unknown), | |
| 155 | + outputSchema: Schema.optional(Schema.Unknown), | |
| 156 | + exampleInput: Schema.optional(Schema.Unknown), | |
| 157 | + exampleOutput: Schema.optional(Schema.Unknown), | |
| 161 | 158 | providerKind: Schema.optional(Schema.String), |
| 162 | − providerDataJson: Schema.optional(Schema.String), | |
| 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, | |
| 159 | + providerData: Schema.optional(Schema.Unknown), | |
| 177 | 160 | }); |
| 178 | 161 | |
| 179 | 162 | export type ToolNamespace = { |
| @@ -217,10 +200,6 @@ export interface ToolCatalog { · 37 unmodified lines | ||
| 217 | 200 | includeSchemas: boolean; |
| 218 | 201 | }): Effect.Effect<ToolDescriptor | null, unknown>; |
| 219 | 202 | |
| 220 | − getSchemaBundle(input: { | |
| 221 | − id: string; | |
| 222 | − }): Effect.Effect<ToolSchemaBundle | null, unknown>; | |
| 223 | − | |
| 224 | 203 | searchTools(input: { |
| 225 | 204 | query: string; |
| 226 | 205 | namespace?: string; |
| @@ -249,9 +228,6 @@ export type DescribePrimitive = { · 22 unmodified lines | ||
| 249 | 228 | path: ToolPath; |
| 250 | 229 | includeSchemas?: boolean; |
| 251 | 230 | }): Effect.Effect<ToolDescriptor | null, unknown>; |
| 252 | − schemaBundle(input: { | |
| 253 | − id: string; | |
| 254 | − }): Effect.Effect<ToolSchemaBundle | null, unknown>; | |
| 255 | 231 | }; |
| 256 | 232 | |
| 257 | 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 | 105 | const contentHash = (value: string): string => |
| 94 | 106 | sha256Hex(value); |
| 95 | 107 | |
| @@ -384,17 +396,29 @@ const manifestMethodFromDiscoveryMethod = (input: { · 288 unmodified lines | ||
| 384 | 396 | ...(methodRequestSchemaJson({ |
| 385 | 397 | method: input.method, |
| 386 | 398 | topLevelSchemas: input.topLevelSchemas, |
| 387 | − }) ? { inputSchemaJson: methodRequestSchemaJson({ | |
| 388 | − method: input.method, | |
| 389 | − topLevelSchemas: input.topLevelSchemas, | |
| 390 | − }) } : {}), | |
| 399 | + }) | |
| 400 | + ? { | |
| 401 | + inputSchema: parseJson( | |
| 402 | + methodRequestSchemaJson({ | |
| 403 | + method: input.method, | |
| 404 | + topLevelSchemas: input.topLevelSchemas, | |
| 405 | + }), | |
| 406 | + ), | |
| 407 | + } | |
| 408 | + : {}), | |
| 391 | 409 | ...(methodResponseSchemaJson({ |
| 392 | 410 | method: input.method, |
| 393 | 411 | topLevelSchemas: input.topLevelSchemas, |
| 394 | − }) ? { outputSchemaJson: methodResponseSchemaJson({ | |
| 395 | − method: input.method, | |
| 396 | − topLevelSchemas: input.topLevelSchemas, | |
| 397 | − }) } : {}), | |
| 412 | + }) | |
| 413 | + ? { | |
| 414 | + outputSchema: parseJson( | |
| 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 | 117 | expect(definitions.length).toBeGreaterThan(10); |
| 118 | 118 | expect(valuesGet).toBeDefined(); |
| 119 | 119 | expect(valuesGet?.method).toBe("get"); |
| 120 | − expect(valuesGet?.inputSchemaJson).toContain("\"spreadsheetId\""); | |
| 121 | − expect(valuesGet?.outputSchemaJson).toContain("\"values\""); | |
| 120 | + expect(JSON.stringify(valuesGet?.inputSchema)).toContain("\"spreadsheetId\""); | |
| 121 | + expect(JSON.stringify(valuesGet?.outputSchema)).toContain("\"values\""); | |
| 122 | 122 | expect(Object.keys(manifest.schemaRefTable ?? {})).toContain( |
| 123 | 123 | "#/$defs/google/ValueRange", |
| 124 | 124 | ); |
| @@ -27,10 +27,10 @@ import { · 26 unmodified lines | ||
| 27 | 27 | } from "./google-discovery-types"; |
| 28 | 28 | |
| 29 | 29 | const decodeGoogleDiscoveryToolProviderDataJson = Schema.decodeUnknownEither( |
| 30 | − Schema.parseJson(GoogleDiscoveryToolProviderDataSchema), | |
| 30 | + GoogleDiscoveryToolProviderDataSchema, | |
| 31 | 31 | ); |
| 32 | 32 | |
| 33 | −export const googleDiscoveryProviderDataJsonFromDefinition = ( | |
| 33 | +export const googleDiscoveryProviderDataFromDefinition = ( | |
| 34 | 34 | input: { |
| 35 | 35 | service: string; |
| 36 | 36 | version: string; |
| @@ -38,8 +38,7 @@ export const googleDiscoveryProviderDataJsonFromDefinition = ( · 1 unmodified line | ||
| 38 | 38 | servicePath: string; |
| 39 | 39 | definition: GoogleDiscoveryManifestMethod; |
| 40 | 40 | }, |
| 41 | −): string => | |
| 42 | − JSON.stringify({ | |
| 41 | +): typeof GoogleDiscoveryToolProviderDataSchema.Type => ({ | |
| 43 | 42 | kind: "google_discovery", |
| 44 | 43 | service: input.service, |
| 45 | 44 | version: input.version, |
| @@ -115,19 +114,14 @@ const setNestedSchemaProperty = ( · 69 unmodified lines | ||
| 115 | 114 | }; |
| 116 | 115 | |
| 117 | 116 | const materializeSchemaWithRefDefinitions = (input: { |
| 118 | − schemaJson: string | undefined; | |
| 117 | + schema: unknown; | |
| 119 | 118 | refTable?: Readonly<Record<string, unknown>>; |
| 120 | 119 | }): Record<string, unknown> => { |
| 121 | − if (!input.schemaJson) { | |
| 120 | + if (input.schema === undefined || input.schema === null) { | |
| 122 | 121 | return {}; |
| 123 | 122 | } |
| 124 | 123 | |
| 125 | − let rootSchema: Record<string, unknown>; | |
| 126 | − try { | |
| 127 | − rootSchema = asRecord(JSON.parse(input.schemaJson) as unknown); | |
| 128 | − } catch { | |
| 129 | − return {}; | |
| 130 | − } | |
| 124 | + const rootSchema = asRecord(input.schema); | |
| 131 | 125 | |
| 132 | 126 | if (!input.refTable || Object.keys(input.refTable).length === 0) { |
| 133 | 127 | return rootSchema; |
| @@ -250,14 +244,14 @@ const buildGoogleDiscoveryRequest = (input: { · 116 unmodified lines | ||
| 250 | 244 | defaultHeaders: Readonly<Record<string, string>>; |
| 251 | 245 | credentialPlacements: HttpRequestPlacements; |
| 252 | 246 | baseUrl?: string | undefined; |
| 253 | − providerDataJson: string; | |
| 247 | + providerData: typeof GoogleDiscoveryToolProviderDataSchema.Type; | |
| 254 | 248 | }): { |
| 255 | 249 | method: string; |
| 256 | 250 | url: URL; |
| 257 | 251 | headers: Record<string, string>; |
| 258 | 252 | body?: string; |
| 259 | 253 | } => { |
| 260 | − const providerData = decodeGoogleDiscoveryToolProviderDataJson(input.providerDataJson); | |
| 254 | + const providerData = decodeGoogleDiscoveryToolProviderDataJson(input.providerData); | |
| 261 | 255 | if (providerData._tag === "Left") { |
| 262 | 256 | throw new Error("Invalid Google Discovery provider data"); |
| 263 | 257 | } |
| @@ -365,28 +359,12 @@ export type CreateGoogleDiscoveryToolFromDefinitionInput = { · 101 unmodified lines | ||
| 365 | 359 | export const createGoogleDiscoveryToolFromDefinition = ( |
| 366 | 360 | input: CreateGoogleDiscoveryToolFromDefinitionInput, |
| 367 | 361 | ) => { |
| 368 | − const providerDataJson = JSON.stringify({ | |
| 369 | − kind: "google_discovery", | |
| 362 | + const providerData = googleDiscoveryProviderDataFromDefinition({ | |
| 370 | 363 | service: input.service, |
| 371 | 364 | version: input.version, |
| 372 | − toolId: input.definition.toolId, | |
| 373 | − rawToolId: input.definition.rawToolId, | |
| 374 | − methodId: input.definition.methodId, | |
| 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 | − }, | |
| 365 | + rootUrl: input.rootUrl, | |
| 366 | + servicePath: input.servicePath, | |
| 367 | + definition: input.definition, | |
| 390 | 368 | }); |
| 391 | 369 | const credentialPlacements = normalizeCredentialPlacements({ |
| 392 | 370 | credentialHeaders: input.credentialHeaders, |
| @@ -403,7 +381,7 @@ export const createGoogleDiscoveryToolFromDefinition = ( · 10 unmodified lines | ||
| 403 | 381 | }), |
| 404 | 382 | ); |
| 405 | 383 | const inputSchema = materializeSchemaWithRefDefinitions({ |
| 406 | − schemaJson: input.definition.inputSchemaJson, | |
| 384 | + schema: input.definition.inputSchema, | |
| 407 | 385 | refTable, |
| 408 | 386 | }); |
| 409 | 387 | |
| @@ -411,15 +389,15 @@ export const createGoogleDiscoveryToolFromDefinition = ( · 1 unmodified line | ||
| 411 | 389 | interaction: input.definition.method === "get" || input.definition.method === "head" |
| 412 | 390 | ? "auto" |
| 413 | 391 | : "required", |
| 414 | − ...(input.definition.inputSchemaJson | |
| 415 | − ? { inputSchemaJson: input.definition.inputSchemaJson } | |
| 392 | + ...(input.definition.inputSchema !== undefined | |
| 393 | + ? { inputSchema: input.definition.inputSchema } | |
| 416 | 394 | : {}), |
| 417 | − ...(input.definition.outputSchemaJson | |
| 418 | − ? { outputSchemaJson: input.definition.outputSchemaJson } | |
| 395 | + ...(input.definition.outputSchema !== undefined | |
| 396 | + ? { outputSchema: input.definition.outputSchema } | |
| 419 | 397 | : {}), |
| 420 | 398 | sourceKey: input.sourceKey, |
| 421 | 399 | providerKind: "google_discovery", |
| 422 | − providerDataJson, | |
| 400 | + providerData, | |
| 423 | 401 | }; |
| 424 | 402 | |
| 425 | 403 | return toTool({ |
| @@ -435,7 +413,7 @@ export const createGoogleDiscoveryToolFromDefinition = ( · 9 unmodified lines | ||
| 435 | 413 | defaultHeaders: input.defaultHeaders ?? {}, |
| 436 | 414 | credentialPlacements, |
| 437 | 415 | baseUrl: input.baseUrl, |
| 438 | − providerDataJson, | |
| 416 | + providerData, | |
| 439 | 417 | }); |
| 440 | 418 | let clientRequest = HttpClientRequest.make( |
| 441 | 419 | request.method as Parameters<typeof HttpClientRequest.make>[0], |
| @@ -74,8 +74,8 @@ export const GoogleDiscoveryManifestMethodSchema = Schema.Struct({ · 73 unmodified lines | ||
| 74 | 74 | scopes: Schema.Array(Schema.String), |
| 75 | 75 | supportsMediaUpload: Schema.Boolean, |
| 76 | 76 | supportsMediaDownload: Schema.Boolean, |
| 77 | − inputSchemaJson: Schema.optional(Schema.String), | |
| 78 | − outputSchemaJson: Schema.optional(Schema.String), | |
| 77 | + inputSchema: Schema.optional(Schema.Unknown), | |
| 78 | + outputSchema: Schema.optional(Schema.Unknown), | |
| 79 | 79 | }); |
| 80 | 80 | |
| 81 | 81 | export const GoogleDiscoverySchemaRefTableSchema = Schema.Record({ |
| @@ -5,7 +5,7 @@ export { · 4 unmodified lines | ||
| 5 | 5 | export { |
| 6 | 6 | createGoogleDiscoveryToolFromDefinition, |
| 7 | 7 | decodeGoogleDiscoverySchemaRefTableJson, |
| 8 | − googleDiscoveryProviderDataJsonFromDefinition, | |
| 8 | + googleDiscoveryProviderDataFromDefinition, | |
| 9 | 9 | type CreateGoogleDiscoveryToolFromDefinitionInput, |
| 10 | 10 | } from "./google-discovery-tools"; |
| 11 | 11 | export { |
| @@ -7,8 +7,8 @@ export type McpToolManifestEntry = { · 6 unmodified lines | ||
| 7 | 7 | toolId: string; |
| 8 | 8 | toolName: string; |
| 9 | 9 | description: string | null; |
| 10 | − inputSchemaJson?: string; | |
| 11 | − outputSchemaJson?: string; | |
| 10 | + inputSchema?: unknown; | |
| 11 | + outputSchema?: unknown; | |
| 12 | 12 | }; |
| 13 | 13 | |
| 14 | 14 | export type McpToolManifest = { |
| @@ -34,18 +34,6 @@ const uniqueToolId = (value: string, byBase: Map<string, number>): string => { · 19 unmodified lines | ||
| 34 | 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 | 37 | const ListedMcpToolSchema = Schema.Struct({ |
| 50 | 38 | name: Schema.String, |
| 51 | 39 | description: Schema.optional(Schema.NullOr(Schema.String)), |
| @@ -85,10 +73,8 @@ export const extractMcpToolManifestFromListToolsResult = ( · 33 unmodified lines | ||
| 85 | 73 | toolId: uniqueToolId(toolName, byBase), |
| 86 | 74 | toolName, |
| 87 | 75 | description: tool.description ?? null, |
| 88 | − inputSchemaJson: | |
| 89 | − stringifyJson(tool.inputSchema) | |
| 90 | − ?? stringifyJson(tool.parameters), | |
| 91 | − outputSchemaJson: stringifyJson(tool.outputSchema), | |
| 76 | + inputSchema: tool.inputSchema ?? tool.parameters, | |
| 77 | + outputSchema: tool.outputSchema, | |
| 92 | 78 | }; |
| 93 | 79 | }) |
| 94 | 80 | .filter((tool): tool is McpToolManifestEntry => tool !== null); |
| @@ -230,7 +230,7 @@ describe("codemode-mcp", () => { · 229 unmodified lines | ||
| 230 | 230 | |
| 231 | 231 | const toolDefinition = resolveToolDefinition(discovered.tools["source.mcp.echo"]!); |
| 232 | 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 | 235 | const invoke = resolveToolExecutor(discovered.tools, "source.mcp.echo"); |
| 236 | 236 | const invocationResult = yield* Effect.promise(() => invoke({ value: "hello" })); |
| @@ -93,13 +93,13 @@ const isExecutionSuspendedCause = (cause: unknown): boolean => · 92 unmodified lines | ||
| 93 | 93 | const toDetails = (cause: unknown): string => |
| 94 | 94 | cause instanceof Error ? cause.message : String(cause); |
| 95 | 95 | |
| 96 | −const inputSchemaFromManifest = (inputSchemaJson: string | undefined) => { | |
| 97 | − if (!inputSchemaJson) { | |
| 96 | +const inputSchemaFromManifest = (inputSchema: unknown) => { | |
| 97 | + if (inputSchema === undefined || inputSchema === null) { | |
| 98 | 98 | return unknownInputSchema; |
| 99 | 99 | } |
| 100 | 100 | |
| 101 | 101 | try { |
| 102 | − return standardSchemaFromJsonSchema(JSON.parse(inputSchemaJson), { | |
| 102 | + return standardSchemaFromJsonSchema(inputSchema, { | |
| 103 | 103 | vendor: "mcp", |
| 104 | 104 | fallback: unknownInputSchema, |
| 105 | 105 | }); |
| @@ -470,7 +470,7 @@ export const createMcpToolsFromManifest = (input: { · 364 unmodified lines | ||
| 470 | 470 | toTool({ |
| 471 | 471 | tool: { |
| 472 | 472 | description: entry.description ?? `MCP tool: ${entry.toolName}`, |
| 473 | − inputSchema: inputSchemaFromManifest(entry.inputSchemaJson), | |
| 473 | + inputSchema: inputSchemaFromManifest(entry.inputSchema), | |
| 474 | 474 | execute: async (args: unknown, executionContext?: ToolExecutionContext) => { |
| 475 | 475 | const exit = await Effect.runPromiseExit( |
| 476 | 476 | withConnectionEffect({ |
| @@ -504,8 +504,12 @@ export const createMcpToolsFromManifest = (input: { · 27 unmodified lines | ||
| 504 | 504 | }, |
| 505 | 505 | metadata: { |
| 506 | 506 | sourceKey, |
| 507 | − inputSchemaJson: entry.inputSchemaJson, | |
| 508 | − outputSchemaJson: entry.outputSchemaJson, | |
| 507 | + ...(entry.inputSchema !== undefined | |
| 508 | + ? { inputSchema: entry.inputSchema } | |
| 509 | + : {}), | |
| 510 | + ...(entry.outputSchema !== undefined | |
| 511 | + ? { outputSchema: entry.outputSchema } | |
| 512 | + : {}), | |
| 509 | 513 | }, |
| 510 | 514 | }), |
| 511 | 515 | ] as const; |
| @@ -8,13 +8,13 @@ export { · 7 unmodified lines | ||
| 8 | 8 | } from "./openapi-extraction"; |
| 9 | 9 | export { |
| 10 | 10 | compileOpenApiToolDefinitions, |
| 11 | − openApiProviderDataJsonFromDefinition, | |
| 11 | + openApiProviderDataFromDefinition, | |
| 12 | 12 | OpenApiToolDefinitionSchema, |
| 13 | 13 | type OpenApiToolDefinition, |
| 14 | 14 | } from "./openapi-definitions"; |
| 15 | 15 | export { |
| 16 | 16 | buildOpenApiToolPresentation, |
| 17 | − openApiOutputTypeSignatureFromSchemaJson, | |
| 17 | + openApiOutputTypeSignatureFromSchema, | |
| 18 | 18 | type OpenApiToolPresentation, |
| 19 | 19 | } from "./openapi-tool-presentation"; |
| 20 | 20 | export { |
| @@ -23,7 +23,7 @@ export { · 2 unmodified lines | ||
| 23 | 23 | createOpenApiToolsFromManifest, |
| 24 | 24 | createOpenApiToolsFromSpec, |
| 25 | 25 | } from "./openapi-tools"; |
| 26 | −export { resolveSchemaJsonWithRefHints, resolveTypingSchemasWithRefHints } from "./openapi-schema-refs"; | |
| 26 | +export { resolveSchemaWithRefHints, resolveTypingSchemasWithRefHints } from "./openapi-schema-refs"; | |
| 27 | 27 | export { |
| 28 | 28 | OPEN_API_HTTP_METHODS, |
| 29 | 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 | 240 | definition: OpenApiToolDefinition, |
| 241 | −): string => | |
| 242 | − JSON.stringify({ | |
| 241 | +): typeof OpenApiToolProviderDataSchema.Type => ({ | |
| 243 | 242 | kind: "openapi", |
| 244 | 243 | toolId: definition.toolId, |
| 245 | 244 | rawToolId: definition.rawToolId, |
| @@ -26,8 +26,8 @@ type ParsedInputSchema = { · 25 unmodified lines | ||
| 26 | 26 | required?: Array<string>; |
| 27 | 27 | }; |
| 28 | 28 | |
| 29 | −const parseInputSchema = (inputSchemaJson: string | undefined): ParsedInputSchema | null => | |
| 30 | − inputSchemaJson ? (JSON.parse(inputSchemaJson) as ParsedInputSchema) : null; | |
| 29 | +const parseInputSchema = (inputSchema: unknown): ParsedInputSchema | null => | |
| 30 | + inputSchema ? (inputSchema as ParsedInputSchema) : null; | |
| 31 | 31 | |
| 32 | 32 | const expectedTools = [ |
| 33 | 33 | { |
| @@ -89,7 +89,7 @@ describe("openapi-extraction real specs", () => { · 55 unmodified lines | ||
| 89 | 89 | expect(tool?.path).toBe(expectedTool.path); |
| 90 | 90 | expect(tool?.tags).toEqual(expectedTool.tags); |
| 91 | 91 | |
| 92 | − const inputSchema = parseInputSchema(tool?.typing?.inputSchemaJson); | |
| 92 | + const inputSchema = parseInputSchema(tool?.typing?.inputSchema); | |
| 93 | 93 | expect(inputSchema?.type).toBe("object"); |
| 94 | 94 | expect(Object.keys(inputSchema?.properties ?? {})).toEqual( |
| 95 | 95 | expect.arrayContaining([...expectedTool.requiredInputs]), |
| @@ -2,8 +2,8 @@ type JsonObject = Record<string, unknown>; · 1 unmodified line | ||
| 2 | 2 | type RefHintValue = string | JsonObject; |
| 3 | 3 | |
| 4 | 4 | type DiscoveryTypingLike = { |
| 5 | − inputSchemaJson?: string; | |
| 6 | − outputSchemaJson?: string; | |
| 5 | + inputSchema?: unknown; | |
| 6 | + outputSchema?: unknown; | |
| 7 | 7 | }; |
| 8 | 8 | |
| 9 | 9 | const isJsonObject = (value: unknown): value is JsonObject => |
| @@ -92,50 +92,40 @@ const resolveSchemaNode = ( · 82 unmodified lines | ||
| 92 | 92 | return next; |
| 93 | 93 | }; |
| 94 | 94 | |
| 95 | −export const resolveSchemaJsonWithRefHints = ( | |
| 96 | − schemaJson: string | undefined, | |
| 95 | +export const resolveSchemaWithRefHints = ( | |
| 96 | + schema: unknown, | |
| 97 | 97 | refHintTable: Readonly<Record<string, RefHintValue>> | undefined, |
| 98 | −): string | null => { | |
| 99 | − if (!schemaJson) { | |
| 98 | +): unknown | null => { | |
| 99 | + if (schema === undefined || schema === null) { | |
| 100 | 100 | return null; |
| 101 | 101 | } |
| 102 | 102 | |
| 103 | − const parsedSchema = parseJson(schemaJson); | |
| 104 | − if (parsedSchema === null) { | |
| 105 | − return schemaJson; | |
| 106 | − } | |
| 107 | − | |
| 108 | 103 | if (!refHintTable || Object.keys(refHintTable).length === 0) { |
| 109 | − return schemaJson; | |
| 104 | + return schema; | |
| 110 | 105 | } |
| 111 | 106 | |
| 112 | 107 | const resolved = resolveSchemaNode( |
| 113 | − parsedSchema, | |
| 108 | + schema, | |
| 114 | 109 | refHintTable, |
| 115 | 110 | new Map<string, unknown>(), |
| 116 | 111 | new Set<string>(), |
| 117 | 112 | ); |
| 118 | − | |
| 119 | − try { | |
| 120 | − return JSON.stringify(resolved); | |
| 121 | − } catch { | |
| 122 | − return schemaJson; | |
| 123 | − } | |
| 113 | + return resolved; | |
| 124 | 114 | }; |
| 125 | 115 | |
| 126 | 116 | export const resolveTypingSchemasWithRefHints = ( |
| 127 | 117 | typing: DiscoveryTypingLike | undefined, |
| 128 | 118 | refHintTable: Readonly<Record<string, RefHintValue>> | undefined, |
| 129 | 119 | ): { |
| 130 | − inputSchemaJson: string | null; | |
| 131 | − outputSchemaJson: string | null; | |
| 120 | + inputSchema: unknown | null; | |
| 121 | + outputSchema: unknown | null; | |
| 132 | 122 | } => ({ |
| 133 | − inputSchemaJson: resolveSchemaJsonWithRefHints( | |
| 134 | − typing?.inputSchemaJson, | |
| 123 | + inputSchema: resolveSchemaWithRefHints( | |
| 124 | + typing?.inputSchema, | |
| 135 | 125 | refHintTable, |
| 136 | 126 | ), |
| 137 | − outputSchemaJson: resolveSchemaJsonWithRefHints( | |
| 138 | − typing?.outputSchemaJson, | |
| 127 | + outputSchema: resolveSchemaWithRefHints( | |
| 128 | + typing?.outputSchema, | |
| 139 | 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 | 3 | import type { |
| 4 | 4 | OpenApiExample, |
| 5 | + OpenApiInvocationPayload, | |
| 5 | 6 | OpenApiToolDocumentation, |
| 7 | + OpenApiToolProviderData, | |
| 6 | 8 | } from "./openapi-types"; |
| 7 | 9 | import { |
| 8 | − openApiProviderDataJsonFromDefinition, | |
| 10 | + openApiProviderDataFromDefinition, | |
| 9 | 11 | type OpenApiToolDefinition, |
| 10 | 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 | 14 | const asRecord = (value: unknown): Record<string, unknown> => |
| 21 | 15 | typeof value === "object" && value !== null && !Array.isArray(value) |
| 22 | 16 | ? (value as Record<string, unknown>) |
| @@ -32,33 +26,62 @@ const isStrictEmptyObjectSchema = (value: unknown): boolean => { · 9 unmodified lines | ||
| 32 | 26 | return Object.keys(properties).length === 0 && schema.additionalProperties === false; |
| 33 | 27 | }; |
| 34 | 28 | |
| 35 | −export const openApiOutputTypeSignatureFromSchemaJson = ( | |
| 36 | − schemaJson: string | undefined, | |
| 29 | +export const openApiOutputTypeSignatureFromSchema = ( | |
| 30 | + schema: unknown, | |
| 37 | 31 | maxLength: number = 320, |
| 38 | 32 | ): string => { |
| 39 | − if (!schemaJson) { | |
| 33 | + if (schema === undefined || schema === null) { | |
| 40 | 34 | return "void"; |
| 41 | 35 | } |
| 42 | 36 | |
| 43 | − const parsed = parseJson(schemaJson); | |
| 44 | − if (parsed === undefined) { | |
| 45 | − return "unknown"; | |
| 46 | − } | |
| 47 | − | |
| 48 | − if (isStrictEmptyObjectSchema(parsed)) { | |
| 37 | + if (isStrictEmptyObjectSchema(schema)) { | |
| 49 | 38 | return "{}"; |
| 50 | 39 | } |
| 51 | 40 | |
| 52 | − return typeSignatureFromSchemaJson(schemaJson, "unknown", maxLength); | |
| 41 | + return typeSignatureFromSchema(schema, "unknown", maxLength); | |
| 53 | 42 | }; |
| 54 | 43 | |
| 55 | 44 | const firstExample = ( |
| 56 | 45 | examples: ReadonlyArray<OpenApiExample> | undefined, |
| 57 | 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 | 83 | documentation: OpenApiToolDocumentation | undefined, |
| 61 | −): string | undefined => { | |
| 84 | +): Record<string, unknown> | undefined => { | |
| 62 | 85 | if (!documentation) { |
| 63 | 86 | return undefined; |
| 64 | 87 | } |
| @@ -71,52 +94,51 @@ const buildExampleInputJson = ( · 6 unmodified lines | ||
| 71 | 94 | continue; |
| 72 | 95 | } |
| 73 | 96 | |
| 74 | − const parsedValue = parseJson(example.valueJson); | |
| 75 | − if (parsedValue !== undefined) { | |
| 76 | − input[parameter.name] = parsedValue; | |
| 77 | − } | |
| 97 | + input[parameter.name] = JSON.parse(example.valueJson) as unknown; | |
| 78 | 98 | } |
| 79 | 99 | |
| 80 | 100 | const requestBodyExample = firstExample(documentation.requestBody?.examples); |
| 81 | 101 | if (requestBodyExample) { |
| 82 | − const parsedValue = parseJson(requestBodyExample.valueJson); | |
| 83 | − if (parsedValue !== undefined) { | |
| 84 | − input.body = parsedValue; | |
| 85 | − } | |
| 102 | + input.body = JSON.parse(requestBodyExample.valueJson) as unknown; | |
| 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 | 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 | 115 | export type OpenApiToolPresentation = { |
| 96 | 116 | inputType: string; |
| 97 | 117 | outputType: string; |
| 98 | − inputSchemaJson?: string; | |
| 99 | − outputSchemaJson?: string; | |
| 100 | − exampleInputJson?: string; | |
| 101 | − exampleOutputJson?: string; | |
| 102 | − providerDataJson: string; | |
| 118 | + inputSchema?: unknown; | |
| 119 | + outputSchema?: unknown; | |
| 120 | + exampleInput?: unknown; | |
| 121 | + exampleOutput?: unknown; | |
| 122 | + providerData: OpenApiToolProviderData; | |
| 103 | 123 | }; |
| 104 | 124 | |
| 105 | 125 | export const buildOpenApiToolPresentation = (input: { |
| 106 | 126 | definition: OpenApiToolDefinition; |
| 107 | 127 | }): OpenApiToolPresentation => { |
| 108 | − const inputSchemaJson = input.definition.typing?.inputSchemaJson; | |
| 109 | − const outputSchemaJson = input.definition.typing?.outputSchemaJson; | |
| 110 | − const exampleInputJson = buildExampleInputJson(input.definition.documentation); | |
| 111 | − const exampleOutputJson = buildExampleOutputJson(input.definition.documentation); | |
| 128 | + const inputSchema = | |
| 129 | + input.definition.typing?.inputSchema | |
| 130 | + ?? fallbackInputSchemaFromInvocation(input.definition.invocation); | |
| 131 | + const outputSchema = input.definition.typing?.outputSchema; | |
| 132 | + const exampleInput = buildExampleInput(input.definition.documentation); | |
| 133 | + const exampleOutput = buildExampleOutput(input.definition.documentation); | |
| 112 | 134 | |
| 113 | 135 | return { |
| 114 | − inputType: typeSignatureFromSchemaJson(inputSchemaJson, "unknown", Infinity), | |
| 115 | − outputType: openApiOutputTypeSignatureFromSchemaJson(outputSchemaJson, Infinity), | |
| 116 | − ...(inputSchemaJson ? { inputSchemaJson } : {}), | |
| 117 | − ...(outputSchemaJson ? { outputSchemaJson } : {}), | |
| 118 | − ...(exampleInputJson ? { exampleInputJson } : {}), | |
| 119 | − ...(exampleOutputJson ? { exampleOutputJson } : {}), | |
| 120 | − providerDataJson: openApiProviderDataJsonFromDefinition(input.definition), | |
| 136 | + inputType: typeSignatureFromSchema(inputSchema, "unknown", Infinity), | |
| 137 | + outputType: openApiOutputTypeSignatureFromSchema(outputSchema, Infinity), | |
| 138 | + ...(inputSchema !== undefined ? { inputSchema } : {}), | |
| 139 | + ...(outputSchema !== undefined ? { outputSchema } : {}), | |
| 140 | + ...(exampleInput !== undefined ? { exampleInput } : {}), | |
| 141 | + ...(exampleOutput !== undefined ? { exampleOutput } : {}), | |
| 142 | + providerData: openApiProviderDataFromDefinition(input.definition), | |
| 121 | 143 | }; |
| 122 | 144 | }; |
| @@ -291,8 +291,8 @@ describe("openapi-tools", () => { · 290 unmodified lines | ||
| 291 | 291 | const resolvedGet = resolveToolDefinition(tools["source.demo.repos.getRepo"]!); |
| 292 | 292 | expect(resolvedGet.metadata?.sourceKey).toBe("api.demo"); |
| 293 | 293 | expect(resolvedGet.metadata?.providerKind).toBe("openapi"); |
| 294 | − expect(resolvedGet.metadata?.providerDataJson).toContain('"group":"repos"'); | |
| 295 | − expect(resolvedGet.metadata?.inputSchemaJson).toBeDefined(); | |
| 294 | + expect(resolvedGet.metadata?.providerData).toMatchObject({ group: "repos" }); | |
| 295 | + expect(resolvedGet.metadata?.inputSchema).toBeDefined(); | |
| 296 | 296 | }), |
| 297 | 297 | ); |
| 298 | 298 | |
| @@ -23,7 +23,7 @@ import { · 22 unmodified lines | ||
| 23 | 23 | type OpenApiToolDefinition, |
| 24 | 24 | } from "./openapi-definitions"; |
| 25 | 25 | import { buildOpenApiToolPresentation } from "./openapi-tool-presentation"; |
| 26 | −import { resolveSchemaJsonWithRefHints } from "./openapi-schema-refs"; | |
| 26 | +import { resolveSchemaWithRefHints } from "./openapi-schema-refs"; | |
| 27 | 27 | import { |
| 28 | 28 | extractOpenApiManifest, |
| 29 | 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: { | |
| 266 | − inputSchemaJson: string | undefined; | |
| 265 | +const inputSchemaFromTyping = (input: { | |
| 266 | + inputSchema: unknown; | |
| 267 | 267 | refHintTable?: Readonly<OpenApiRefHintTable>; |
| 268 | 268 | }) => { |
| 269 | − const resolvedSchemaJson = resolveSchemaJsonWithRefHints( | |
| 270 | − input.inputSchemaJson, | |
| 269 | + const resolvedSchema = resolveSchemaWithRefHints( | |
| 270 | + input.inputSchema, | |
| 271 | 271 | input.refHintTable, |
| 272 | − ) ?? input.inputSchemaJson; | |
| 272 | + ) ?? input.inputSchema; | |
| 273 | 273 | |
| 274 | − if (!resolvedSchemaJson) { | |
| 274 | + if (resolvedSchema === undefined || resolvedSchema === null) { | |
| 275 | 275 | return unknownInputSchema; |
| 276 | 276 | } |
| 277 | 277 | |
| 278 | 278 | try { |
| 279 | − return standardSchemaFromJsonSchema(JSON.parse(resolvedSchemaJson), { | |
| 279 | + return standardSchemaFromJsonSchema(resolvedSchema, { | |
| 280 | 280 | vendor: "openapi", |
| 281 | 281 | fallback: unknownInputSchema, |
| 282 | 282 | }); |
| @@ -469,8 +469,8 @@ export const createOpenApiToolFromDefinition = ( · 186 unmodified lines | ||
| 469 | 469 | return toTool({ |
| 470 | 470 | tool: { |
| 471 | 471 | description: input.definition.description, |
| 472 | − inputSchema: inputSchemaFromTypingJson({ | |
| 473 | − inputSchemaJson: presentation.inputSchemaJson ?? input.definition.typing?.inputSchemaJson, | |
| 472 | + inputSchema: inputSchemaFromTyping({ | |
| 473 | + inputSchema: presentation.inputSchema ?? input.definition.typing?.inputSchema, | |
| 474 | 474 | refHintTable: input.refHintTable, |
| 475 | 475 | }), |
| 476 | 476 | execute: async (args: unknown) => { |
| @@ -526,12 +526,20 @@ export const createOpenApiToolFromDefinition = ( · 49 unmodified lines | ||
| 526 | 526 | sourceKey: input.sourceKey, |
| 527 | 527 | inputType: presentation.inputType, |
| 528 | 528 | outputType: presentation.outputType, |
| 529 | − inputSchemaJson: presentation.inputSchemaJson, | |
| 530 | − outputSchemaJson: presentation.outputSchemaJson, | |
| 531 | − exampleInputJson: presentation.exampleInputJson, | |
| 532 | − exampleOutputJson: presentation.exampleOutputJson, | |
| 529 | + ...((presentation.inputSchema ?? input.definition.typing?.inputSchema) !== undefined | |
| 530 | + ? { inputSchema: presentation.inputSchema ?? input.definition.typing?.inputSchema } | |
| 531 | + : {}), | |
| 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 | 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 | 60 | export const DiscoveryTypingPayloadSchema = Schema.Struct({ |
| 61 | − inputSchemaJson: Schema.optional(Schema.String), | |
| 62 | − outputSchemaJson: Schema.optional(Schema.String), | |
| 61 | + inputSchema: Schema.optional(Schema.Unknown), | |
| 62 | + outputSchema: Schema.optional(Schema.Unknown), | |
| 63 | 63 | refHintKeys: Schema.optional(Schema.Array(Schema.String)), |
| 64 | 64 | }); |
| 65 | 65 | |
| @@ -5,6 +5,7 @@ · 4 unmodified lines | ||
| 5 | 5 | "exports": { |
| 6 | 6 | ".": "./src/index.ts", |
| 7 | 7 | "./client": "./src/client.ts", |
| 8 | + "./ir": "./src/ir/index.ts", | |
| 8 | 9 | "./schema": "./src/schema/index.ts" |
| 9 | 10 | }, |
| 10 | 11 | "imports": { |
| @@ -8,7 +8,6 @@ import { · 7 unmodified lines | ||
| 8 | 8 | SourceIdSchema, |
| 9 | 9 | SourceInspectionDiscoverPayloadSchema, |
| 10 | 10 | SourceInspectionDiscoverResultSchema, |
| 11 | − SourceInspectionSchemaBundleSchema, | |
| 12 | 11 | SourceInspectionSchema, |
| 13 | 12 | SourceInspectionToolDetailSchema, |
| 14 | 13 | SourceImportAuthPolicySchema, |
| @@ -80,7 +79,6 @@ export type UpdateSourcePayload = typeof UpdateSourcePayloadSchema.Type; · 65 unmodified lines | ||
| 80 | 79 | const workspaceIdParam = HttpApiSchema.param("workspaceId", WorkspaceIdSchema); |
| 81 | 80 | const sourceIdParam = HttpApiSchema.param("sourceId", SourceIdSchema); |
| 82 | 81 | const toolPathParam = HttpApiSchema.param("toolPath", Schema.String); |
| 83 | −const schemaBundleIdParam = HttpApiSchema.param("schemaBundleId", Schema.String); | |
| 84 | 82 | |
| 85 | 83 | const CredentialPageUrlParamsSchema = Schema.Struct({ |
| 86 | 84 | interactionId: ExecutionInteractionIdSchema, |
| @@ -242,15 +240,6 @@ export class SourcesApi extends HttpApiGroup.make("sources") · 155 unmodified lines | ||
| 242 | 240 | .addError(ControlPlaneNotFoundError) |
| 243 | 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 | 243 | .add( |
| 255 | 244 | HttpApiEndpoint.post("inspectionDiscover")`/workspaces/${workspaceIdParam}/sources/${sourceIdParam}/inspection/discover` |
| 256 | 245 | .setPayload(SourceInspectionDiscoverPayloadSchema) |
| @@ -16,7 +16,6 @@ import { · 15 unmodified lines | ||
| 16 | 16 | import { |
| 17 | 17 | discoverSourceInspectionTools, |
| 18 | 18 | getSourceInspection, |
| 19 | − getSourceInspectionSchemaBundle, | |
| 20 | 19 | getSourceInspectionToolDetail, |
| 21 | 20 | } from "../../runtime/source-inspection"; |
| 22 | 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 | 752 | .handle("inspectionDiscover", ({ path, payload }) => |
| 765 | 753 | resolveRequestedLocalWorkspace("sources.inspection_discover", path.workspaceId).pipe( |
| 766 | 754 | Effect.zipRight( |
| @@ -2,3 +2,4 @@ export * from "./schema/index"; · 1 unmodified line | ||
| 2 | 2 | export * from "./api/index"; |
| 3 | 3 | export * from "./runtime/index"; |
| 4 | 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 | 82 | 260, |
| 83 | 83 | ); |
| 84 | 84 | |
| 85 | −export const EXECUTOR_SOURCES_ADD_INPUT_SCHEMA_JSON = JSON.stringify( | |
| 86 | − deriveSchemaJson(ExecutorAddSourceInputSchema) ?? {}, | |
| 87 | −); | |
| 85 | +export const EXECUTOR_SOURCES_ADD_INPUT_SCHEMA = deriveSchemaJson( | |
| 86 | + ExecutorAddSourceInputSchema, | |
| 87 | +) ?? {}; | |
| 88 | 88 | |
| 89 | −export const EXECUTOR_SOURCES_ADD_OUTPUT_SCHEMA_JSON = JSON.stringify( | |
| 90 | − deriveSchemaJson(SourceSchema) ?? {}, | |
| 91 | −); | |
| 89 | +export const EXECUTOR_SOURCES_ADD_OUTPUT_SCHEMA = deriveSchemaJson( | |
| 90 | + SourceSchema, | |
| 91 | +) ?? {}; | |
| 92 | 92 | |
| 93 | 93 | export const EXECUTOR_SOURCES_ADD_HELP_LINES = [ |
| 94 | 94 | "Source add input shapes:", |
| @@ -437,8 +437,8 @@ export const createExecutorToolMap = (input: { · 342 unmodified lines | ||
| 437 | 437 | metadata: { |
| 438 | 438 | inputType: EXECUTOR_SOURCES_ADD_INPUT_HINT, |
| 439 | 439 | outputType: EXECUTOR_SOURCES_ADD_OUTPUT_SIGNATURE, |
| 440 | − inputSchemaJson: EXECUTOR_SOURCES_ADD_INPUT_SCHEMA_JSON, | |
| 441 | − outputSchemaJson: EXECUTOR_SOURCES_ADD_OUTPUT_SCHEMA_JSON, | |
| 440 | + inputSchema: EXECUTOR_SOURCES_ADD_INPUT_SCHEMA, | |
| 441 | + outputSchema: EXECUTOR_SOURCES_ADD_OUTPUT_SCHEMA, | |
| 442 | 442 | sourceKey: "executor", |
| 443 | 443 | interaction: "auto", |
| 444 | 444 | }, |
| @@ -1,6 +1,6 @@ | ||
| 1 | 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 | 5 | import { createGraphqlToolFromPersistedOperation } from "./graphql-tools"; |
| 6 | 6 | |
| @@ -91,7 +91,7 @@ describe("graphql-tools", () => { · 84 unmodified lines | ||
| 91 | 91 | path: "source.graphql.raw", |
| 92 | 92 | sourceKey: "src_graphql", |
| 93 | 93 | endpoint: server.baseUrl, |
| 94 | − providerDataJson: JSON.stringify({ | |
| 94 | + providerData: { | |
| 95 | 95 | kind: "graphql", |
| 96 | 96 | toolKind: "request", |
| 97 | 97 | toolId: "raw", |
| @@ -105,7 +105,7 @@ describe("graphql-tools", () => { · 7 unmodified lines | ||
| 105 | 105 | queryTypeName: "Query", |
| 106 | 106 | mutationTypeName: "Mutation", |
| 107 | 107 | subscriptionTypeName: null, |
| 108 | − }), | |
| 108 | + }, | |
| 109 | 109 | credentialPlacements: { |
| 110 | 110 | headers: { |
| 111 | 111 | authorization: "Token top-secret", |
| @@ -10,7 +10,7 @@ import { · 9 unmodified lines | ||
| 10 | 10 | type ToolMap, |
| 11 | 11 | type ToolMetadata, |
| 12 | 12 | type ToolPath, |
| 13 | − typeSignatureFromSchemaJson, | |
| 13 | + typeSignatureFromSchema, | |
| 14 | 14 | } from "@executor/codemode-core"; |
| 15 | 15 | import * as Effect from "effect/Effect"; |
| 16 | 16 | import * as Schema from "effect/Schema"; |
| @@ -51,9 +51,9 @@ type GraphqlManifestToolBase = { · 34 unmodified lines | ||
| 51 | 51 | rawToolId: string | null; |
| 52 | 52 | toolName: string; |
| 53 | 53 | description: string | null; |
| 54 | − inputSchemaJson?: string; | |
| 55 | − outputSchemaJson?: string; | |
| 56 | − exampleInputJson?: string; | |
| 54 | + inputSchema?: unknown; | |
| 55 | + outputSchema?: unknown; | |
| 56 | + exampleInput?: unknown; | |
| 57 | 57 | }; |
| 58 | 58 | |
| 59 | 59 | export type GraphqlRequestToolManifestEntry = GraphqlManifestToolBase & { |
| @@ -102,10 +102,10 @@ export type GraphqlToolDefinition = { · 42 unmodified lines | ||
| 102 | 102 | export type GraphqlToolPresentation = { |
| 103 | 103 | inputType: string; |
| 104 | 104 | outputType: string; |
| 105 | − inputSchemaJson?: string; | |
| 106 | − outputSchemaJson?: string; | |
| 107 | − exampleInputJson?: string; | |
| 108 | − providerDataJson: string; | |
| 105 | + inputSchema?: unknown; | |
| 106 | + outputSchema?: unknown; | |
| 107 | + exampleInput?: unknown; | |
| 108 | + providerData: GraphqlToolProviderData; | |
| 109 | 109 | }; |
| 110 | 110 | |
| 111 | 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 | 887 | args: readonly GraphQLArgument[], |
| 888 | −): string | undefined => { | |
| 888 | +): Record<string, unknown> | undefined => { | |
| 889 | 889 | if (args.length === 0) { |
| 890 | − return JSON.stringify({}); | |
| 890 | + return {}; | |
| 891 | 891 | } |
| 892 | 892 | |
| 893 | 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 | 908 | const buildGraphqlFieldOperationDocument = (input: { |
| @@ -958,11 +958,11 @@ const requestToolManifestEntry = ( · 49 unmodified lines | ||
| 958 | 958 | rawToolId: "request", |
| 959 | 959 | toolName: "GraphQL request", |
| 960 | 960 | description: `Execute a raw GraphQL request against ${sourceName}.`, |
| 961 | − inputSchemaJson: JSON.stringify(GRAPHQL_REQUEST_INPUT_SCHEMA), | |
| 962 | − outputSchemaJson: JSON.stringify(GRAPHQL_REQUEST_OUTPUT_SCHEMA), | |
| 963 | − exampleInputJson: JSON.stringify({ | |
| 961 | + inputSchema: GRAPHQL_REQUEST_INPUT_SCHEMA, | |
| 962 | + outputSchema: GRAPHQL_REQUEST_OUTPUT_SCHEMA, | |
| 963 | + exampleInput: { | |
| 964 | 964 | query: "query { __typename }", |
| 965 | − }), | |
| 965 | + }, | |
| 966 | 966 | }); |
| 967 | 967 | |
| 968 | 968 | type GraphqlFieldToolDraft = Omit<GraphqlFieldToolManifestEntry, "toolId"> & { |
| @@ -1030,11 +1030,13 @@ const fieldToolDraftsFromRootType = (input: { · 61 unmodified lines | ||
| 1030 | 1030 | .filter((field) => !field.name.startsWith("__")) |
| 1031 | 1031 | .map((field) => { |
| 1032 | 1032 | const leaf = toCamelCase(field.name); |
| 1033 | − const outputSchemaJson = JSON.stringify( | |
| 1034 | − outputEnvelopeSchemaForField(field.type, input.bundleBuilder), | |
| 1033 | + const outputSchema = outputEnvelopeSchemaForField( | |
| 1034 | + field.type, | |
| 1035 | + input.bundleBuilder, | |
| 1035 | 1036 | ); |
| 1036 | − const inputSchemaJson = JSON.stringify( | |
| 1037 | − inputSchemaForFieldArguments(field.args, input.bundleBuilder), | |
| 1037 | + const inputSchema = inputSchemaForFieldArguments( | |
| 1038 | + field.args, | |
| 1039 | + input.bundleBuilder, | |
| 1038 | 1040 | ); |
| 1039 | 1041 | const { operationName, operationDocument } = |
| 1040 | 1042 | buildGraphqlFieldOperationDocument({ |
| @@ -1061,9 +1063,9 @@ const fieldToolDraftsFromRootType = (input: { · 20 unmodified lines | ||
| 1061 | 1063 | operationType: input.operationType, |
| 1062 | 1064 | operationName, |
| 1063 | 1065 | operationDocument, |
| 1064 | − inputSchemaJson, | |
| 1065 | − outputSchemaJson, | |
| 1066 | − exampleInputJson: exampleInputJsonForField(field.args), | |
| 1066 | + inputSchema, | |
| 1067 | + outputSchema, | |
| 1068 | + exampleInput: exampleInputForField(field.args), | |
| 1067 | 1069 | searchTerms: [ |
| 1068 | 1070 | input.operationType, |
| 1069 | 1071 | field.name, |
| @@ -1143,26 +1145,26 @@ export const buildGraphqlToolPresentation = (input: { · 73 unmodified lines | ||
| 1143 | 1145 | const entry = input.manifest.tools.find( |
| 1144 | 1146 | (tool) => tool.toolId === input.definition.toolId, |
| 1145 | 1147 | ); |
| 1146 | − const inputSchemaJson = entry?.inputSchemaJson; | |
| 1147 | − const outputSchemaJson = entry?.outputSchemaJson; | |
| 1148 | + const inputSchema = entry?.inputSchema; | |
| 1149 | + const outputSchema = entry?.outputSchema; | |
| 1148 | 1150 | |
| 1149 | 1151 | return { |
| 1150 | − inputType: typeSignatureFromSchemaJson( | |
| 1151 | − inputSchemaJson, | |
| 1152 | + inputType: typeSignatureFromSchema( | |
| 1153 | + inputSchema, | |
| 1152 | 1154 | "unknown", |
| 1153 | 1155 | Infinity, |
| 1154 | 1156 | ), |
| 1155 | − outputType: typeSignatureFromSchemaJson( | |
| 1156 | − outputSchemaJson, | |
| 1157 | + outputType: typeSignatureFromSchema( | |
| 1158 | + outputSchema, | |
| 1157 | 1159 | "unknown", |
| 1158 | 1160 | Infinity, |
| 1159 | 1161 | ), |
| 1160 | − ...(inputSchemaJson ? { inputSchemaJson } : {}), | |
| 1161 | − ...(outputSchemaJson ? { outputSchemaJson } : {}), | |
| 1162 | − ...(entry?.exampleInputJson | |
| 1163 | − ? { exampleInputJson: entry.exampleInputJson } | |
| 1162 | + ...(inputSchema !== undefined ? { inputSchema } : {}), | |
| 1163 | + ...(outputSchema !== undefined ? { outputSchema } : {}), | |
| 1164 | + ...(entry?.exampleInput !== undefined | |
| 1165 | + ? { exampleInput: entry.exampleInput } | |
| 1164 | 1166 | : {}), |
| 1165 | − providerDataJson: JSON.stringify({ | |
| 1167 | + providerData: { | |
| 1166 | 1168 | kind: "graphql", |
| 1167 | 1169 | toolKind: entry?.kind ?? "request", |
| 1168 | 1170 | toolId: input.definition.toolId, |
| @@ -1176,12 +1178,12 @@ export const buildGraphqlToolPresentation = (input: { · 7 unmodified lines | ||
| 1176 | 1178 | queryTypeName: input.manifest.queryTypeName, |
| 1177 | 1179 | mutationTypeName: input.manifest.mutationTypeName, |
| 1178 | 1180 | subscriptionTypeName: input.manifest.subscriptionTypeName, |
| 1179 | − } satisfies GraphqlToolProviderData), | |
| 1181 | + } satisfies GraphqlToolProviderData, | |
| 1180 | 1182 | }; |
| 1181 | 1183 | }; |
| 1182 | 1184 | |
| 1183 | −const decodeGraphqlToolProviderDataJson = Schema.decodeUnknownEither( | |
| 1184 | − Schema.parseJson(GraphqlToolProviderDataSchema), | |
| 1185 | +const decodeGraphqlToolProviderData = Schema.decodeUnknownEither( | |
| 1186 | + GraphqlToolProviderDataSchema, | |
| 1185 | 1187 | ); |
| 1186 | 1188 | |
| 1187 | 1189 | export const decodeGraphqlSchemaRefTableJson = Schema.decodeUnknownEither( |
| @@ -1218,19 +1220,14 @@ const setNestedSchemaProperty = ( · 30 unmodified lines | ||
| 1218 | 1220 | }; |
| 1219 | 1221 | |
| 1220 | 1222 | const materializeSchemaWithRefDefinitions = (input: { |
| 1221 | − schemaJson: string | undefined; | |
| 1223 | + schema: unknown; | |
| 1222 | 1224 | refTable?: Readonly<Record<string, unknown>>; |
| 1223 | 1225 | }): Record<string, unknown> => { |
| 1224 | − if (!input.schemaJson) { | |
| 1226 | + if (input.schema === undefined || input.schema === null) { | |
| 1225 | 1227 | return {}; |
| 1226 | 1228 | } |
| 1227 | 1229 | |
| 1228 | − let rootSchema: Record<string, unknown>; | |
| 1229 | − try { | |
| 1230 | − rootSchema = asRecord(JSON.parse(input.schemaJson) as unknown); | |
| 1231 | − } catch { | |
| 1232 | − return {}; | |
| 1233 | − } | |
| 1230 | + const rootSchema = asRecord(input.schema); | |
| 1234 | 1231 | |
| 1235 | 1232 | if (!input.refTable || Object.keys(input.refTable).length === 0) { |
| 1236 | 1233 | return rootSchema; |
| @@ -1269,17 +1266,17 @@ export const createGraphqlToolFromPersistedOperation = (input: { · 32 unmodified lines | ||
| 1269 | 1266 | sourceKey: string; |
| 1270 | 1267 | endpoint: string; |
| 1271 | 1268 | description?: string; |
| 1272 | − inputSchemaJson?: string; | |
| 1273 | − outputSchemaJson?: string; | |
| 1274 | − exampleInputJson?: string; | |
| 1275 | − providerDataJson: string; | |
| 1269 | + inputSchema?: unknown; | |
| 1270 | + outputSchema?: unknown; | |
| 1271 | + exampleInput?: unknown; | |
| 1272 | + providerData: unknown; | |
| 1276 | 1273 | schemaRefTable?: Readonly<Record<string, unknown>>; |
| 1277 | 1274 | defaultHeaders?: Readonly<Record<string, string>>; |
| 1278 | 1275 | credentialHeaders?: Readonly<Record<string, string>>; |
| 1279 | 1276 | credentialPlacements?: HttpRequestPlacements; |
| 1280 | 1277 | }) => { |
| 1281 | − const decodedProviderData = decodeGraphqlToolProviderDataJson( | |
| 1282 | − input.providerDataJson, | |
| 1278 | + const decodedProviderData = decodeGraphqlToolProviderData( | |
| 1279 | + input.providerData, | |
| 1283 | 1280 | ); |
| 1284 | 1281 | if (decodedProviderData._tag === "Left") { |
| 1285 | 1282 | throw new Error("Invalid GraphQL provider data"); |
| @@ -1287,7 +1284,7 @@ export const createGraphqlToolFromPersistedOperation = (input: { · 1 unmodified line | ||
| 1287 | 1284 | |
| 1288 | 1285 | const providerData = decodedProviderData.right; |
| 1289 | 1286 | const inputSchema = materializeSchemaWithRefDefinitions({ |
| 1290 | − schemaJson: input.inputSchemaJson, | |
| 1287 | + schema: input.inputSchema, | |
| 1291 | 1288 | refTable: input.schemaRefTable, |
| 1292 | 1289 | }); |
| 1293 | 1290 | |
| @@ -1298,28 +1295,28 @@ export const createGraphqlToolFromPersistedOperation = (input: { · 4 unmodified lines | ||
| 1298 | 1295 | : providerData.operationType === "query" |
| 1299 | 1296 | ? "auto" |
| 1300 | 1297 | : "required", |
| 1301 | − inputType: typeSignatureFromSchemaJson( | |
| 1302 | − input.inputSchemaJson, | |
| 1298 | + inputType: typeSignatureFromSchema( | |
| 1299 | + input.inputSchema, | |
| 1303 | 1300 | "unknown", |
| 1304 | 1301 | Infinity, |
| 1305 | 1302 | ), |
| 1306 | − outputType: typeSignatureFromSchemaJson( | |
| 1307 | − input.outputSchemaJson, | |
| 1303 | + outputType: typeSignatureFromSchema( | |
| 1304 | + input.outputSchema, | |
| 1308 | 1305 | "unknown", |
| 1309 | 1306 | Infinity, |
| 1310 | 1307 | ), |
| 1311 | − ...(input.inputSchemaJson | |
| 1312 | − ? { inputSchemaJson: input.inputSchemaJson } | |
| 1308 | + ...(input.inputSchema !== undefined | |
| 1309 | + ? { inputSchema: input.inputSchema } | |
| 1313 | 1310 | : {}), |
| 1314 | − ...(input.outputSchemaJson | |
| 1315 | − ? { outputSchemaJson: input.outputSchemaJson } | |
| 1311 | + ...(input.outputSchema !== undefined | |
| 1312 | + ? { outputSchema: input.outputSchema } | |
| 1316 | 1313 | : {}), |
| 1317 | − ...(input.exampleInputJson | |
| 1318 | − ? { exampleInputJson: input.exampleInputJson } | |
| 1314 | + ...(input.exampleInput !== undefined | |
| 1315 | + ? { exampleInput: input.exampleInput } | |
| 1319 | 1316 | : {}), |
| 1320 | 1317 | sourceKey: input.sourceKey, |
| 1321 | 1318 | providerKind: "graphql", |
| 1322 | − providerDataJson: input.providerDataJson, | |
| 1319 | + providerData, | |
| 1323 | 1320 | }; |
| 1324 | 1321 | |
| 1325 | 1322 | return toTool({ |
| @@ -1387,17 +1384,17 @@ export const graphqlToolDescriptorFromDefinition = (input: { · 61 unmodified lines | ||
| 1387 | 1384 | input.definition.operationType === "query" ? "auto" : "required", |
| 1388 | 1385 | inputType: presentation.inputType, |
| 1389 | 1386 | outputType: presentation.outputType, |
| 1390 | − ...(input.includeSchemas && presentation.inputSchemaJson | |
| 1391 | − ? { inputSchemaJson: presentation.inputSchemaJson } | |
| 1387 | + ...(input.includeSchemas && presentation.inputSchema !== undefined | |
| 1388 | + ? { inputSchema: presentation.inputSchema } | |
| 1392 | 1389 | : {}), |
| 1393 | − ...(input.includeSchemas && presentation.outputSchemaJson | |
| 1394 | − ? { outputSchemaJson: presentation.outputSchemaJson } | |
| 1390 | + ...(input.includeSchemas && presentation.outputSchema !== undefined | |
| 1391 | + ? { outputSchema: presentation.outputSchema } | |
| 1395 | 1392 | : {}), |
| 1396 | − ...(presentation.exampleInputJson | |
| 1397 | − ? { exampleInputJson: presentation.exampleInputJson } | |
| 1393 | + ...(presentation.exampleInput !== undefined | |
| 1394 | + ? { exampleInput: presentation.exampleInput } | |
| 1398 | 1395 | : {}), |
| 1399 | 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 | 1569 | sourceKey: input.sourceKey, |
| 1573 | 1570 | endpoint, |
| 1574 | 1571 | description: definition.description, |
| 1575 | − inputSchemaJson: presentation.inputSchemaJson, | |
| 1576 | − outputSchemaJson: presentation.outputSchemaJson, | |
| 1577 | − exampleInputJson: presentation.exampleInputJson, | |
| 1578 | − providerDataJson: presentation.providerDataJson, | |
| 1572 | + inputSchema: presentation.inputSchema, | |
| 1573 | + outputSchema: presentation.outputSchema, | |
| 1574 | + exampleInput: presentation.exampleInput, | |
| 1575 | + providerData: presentation.providerData, | |
| 1579 | 1576 | schemaRefTable: input.manifest.schemaRefTable, |
| 1580 | 1577 | defaultHeaders: input.defaultHeaders, |
| 1581 | 1578 | credentialHeaders: input.credentialHeaders, |
| @@ -33,9 +33,9 @@ import { LocalToolRuntimeLoaderLive } from "./local-tools"; · 32 unmodified lines | ||
| 33 | 33 | import { synchronizeLocalWorkspaceState } from "./local-workspace-sync"; |
| 34 | 34 | import { ControlPlaneStore, type ControlPlaneStoreShape } from "./store"; |
| 35 | 35 | import { RuntimeSourceStoreLive } from "./source-store"; |
| 36 | −import { RuntimeSourceRecipeStoreLive } from "./source-recipes-runtime"; | |
| 36 | +import { RuntimeSourceCatalogStoreLive } from "./source-catalog-runtime"; | |
| 37 | 37 | import { RuntimeSourceAuthMaterialLive } from "./source-auth-material"; |
| 38 | −import { RuntimeSourceMaterializationLive } from "./source-materialization"; | |
| 38 | +import { RuntimeSourceCatalogSyncLive } from "./source-catalog-sync"; | |
| 39 | 39 | import { |
| 40 | 40 | RuntimeSourceAuthServiceLive, |
| 41 | 41 | } from "./source-auth-service"; |
| @@ -121,7 +121,7 @@ export const createRuntimeControlPlaneLayer = ( · 79 unmodified lines | ||
| 121 | 121 | Layer.provide(baseLayer), |
| 122 | 122 | ); |
| 123 | 123 | |
| 124 | − const sourceRecipeStoreLayer = RuntimeSourceRecipeStoreLive.pipe( | |
| 124 | + const sourceCatalogStoreLayer = RuntimeSourceCatalogStoreLive.pipe( | |
| 125 | 125 | Layer.provide(Layer.mergeAll(baseLayer, sourceStoreLayer)), |
| 126 | 126 | ); |
| 127 | 127 | |
| @@ -129,7 +129,7 @@ export const createRuntimeControlPlaneLayer = ( · 1 unmodified line | ||
| 129 | 129 | Layer.provide(Layer.mergeAll(baseLayer, secretMaterialLayer)), |
| 130 | 130 | ); |
| 131 | 131 | |
| 132 | − const sourceMaterializationLayer = RuntimeSourceMaterializationLive.pipe( | |
| 132 | + const sourceCatalogSyncLayer = RuntimeSourceCatalogSyncLive.pipe( | |
| 133 | 133 | Layer.provide( |
| 134 | 134 | Layer.mergeAll(baseLayer, secretMaterialLayer, sourceAuthMaterialLayer), |
| 135 | 135 | ), |
| @@ -143,7 +143,7 @@ export const createRuntimeControlPlaneLayer = ( · 7 unmodified lines | ||
| 143 | 143 | baseLayer, |
| 144 | 144 | secretMaterialLayer, |
| 145 | 145 | sourceStoreLayer, |
| 146 | − sourceMaterializationLayer, | |
| 146 | + sourceCatalogSyncLayer, | |
| 147 | 147 | ), |
| 148 | 148 | ), |
| 149 | 149 | ); |
| @@ -156,9 +156,9 @@ export const createRuntimeControlPlaneLayer = ( · 6 unmodified lines | ||
| 156 | 156 | baseLayer, |
| 157 | 157 | secretMaterialLayer, |
| 158 | 158 | sourceAuthMaterialLayer, |
| 159 | − sourceMaterializationLayer, | |
| 159 | + sourceCatalogSyncLayer, | |
| 160 | 160 | sourceAuthLayer, |
| 161 | − sourceRecipeStoreLayer, | |
| 161 | + sourceCatalogStoreLayer, | |
| 162 | 162 | localToolRuntimeLayer, |
| 163 | 163 | ), |
| 164 | 164 | ), |
| @@ -169,8 +169,8 @@ export const createRuntimeControlPlaneLayer = ( · 4 unmodified lines | ||
| 169 | 169 | secretMaterialLayer, |
| 170 | 170 | sourceStoreLayer, |
| 171 | 171 | sourceAuthMaterialLayer, |
| 172 | − sourceMaterializationLayer, | |
| 173 | − sourceRecipeStoreLayer, | |
| 172 | + sourceCatalogSyncLayer, | |
| 173 | + sourceCatalogStoreLayer, | |
| 174 | 174 | localToolRuntimeLayer, |
| 175 | 175 | sourceAuthLayer, |
| 176 | 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 | 3 | import { sanitizePersistedElicitationResponse } from "./live-execution"; |
| 4 | 4 | |
| @@ -3,18 +3,17 @@ import { FileSystem } from "@effect/platform"; · 2 unmodified lines | ||
| 3 | 3 | |
| 4 | 4 | import { |
| 5 | 5 | SourceIdSchema, |
| 6 | − SourceRecipeIdSchema, | |
| 7 | − StoredSourceRecipeDocumentRecordSchema, | |
| 8 | − StoredSourceRecipeOperationRecordSchema, | |
| 9 | − StoredSourceRecipeRevisionRecordSchema, | |
| 10 | − StoredSourceRecipeSchemaBundleRecordSchema, | |
| 6 | + SourceCatalogIdSchema, | |
| 7 | + StoredSourceCatalogRevisionRecordSchema, | |
| 11 | 8 | TimestampMsSchema, |
| 12 | 9 | type Source, |
| 13 | − type SourceRecipeId, | |
| 10 | + type SourceCatalogId, | |
| 14 | 11 | } from "#schema"; |
| 15 | 12 | import * as Effect from "effect/Effect"; |
| 16 | 13 | import * as Schema from "effect/Schema"; |
| 17 | 14 | |
| 15 | +import { CatalogSnapshotV1Schema } from "../ir/model"; | |
| 16 | +import type { SourceCatalogSyncResult } from "./source-catalog-support"; | |
| 18 | 17 | import type { ResolvedLocalWorkspaceContext } from "./local-config"; |
| 19 | 18 | import { |
| 20 | 19 | LocalFileSystemError, |
| @@ -22,25 +21,20 @@ import { · 1 unmodified line | ||
| 22 | 21 | unknownLocalErrorDetails, |
| 23 | 22 | } from "./local-errors"; |
| 24 | 23 | import { |
| 25 | − createSourceRecipeRevisionRecord, | |
| 26 | − stableSourceRecipeId, | |
| 24 | + createSourceCatalogRevisionRecord, | |
| 25 | + stableSourceCatalogId, | |
| 27 | 26 | } from "./source-definitions"; |
| 28 | −import { | |
| 29 | − contentHash, | |
| 30 | − type SourceRecipeMaterialization, | |
| 31 | −} from "./source-recipe-support"; | |
| 27 | +import { contentHash } from "./source-catalog-support"; | |
| 32 | 28 | |
| 33 | −const LOCAL_SOURCE_ARTIFACT_VERSION = 1 as const; | |
| 29 | +const LOCAL_SOURCE_ARTIFACT_VERSION = 2 as const; | |
| 34 | 30 | |
| 35 | 31 | export const LocalSourceArtifactSchema = Schema.Struct({ |
| 36 | 32 | version: Schema.Literal(LOCAL_SOURCE_ARTIFACT_VERSION), |
| 37 | 33 | sourceId: SourceIdSchema, |
| 38 | − recipeId: SourceRecipeIdSchema, | |
| 34 | + catalogId: SourceCatalogIdSchema, | |
| 39 | 35 | generatedAt: TimestampMsSchema, |
| 40 | − revision: StoredSourceRecipeRevisionRecordSchema, | |
| 41 | − documents: Schema.Array(StoredSourceRecipeDocumentRecordSchema), | |
| 42 | − schemaBundles: Schema.Array(StoredSourceRecipeSchemaBundleRecordSchema), | |
| 43 | − operations: Schema.Array(StoredSourceRecipeOperationRecordSchema), | |
| 36 | + revision: StoredSourceCatalogRevisionRecordSchema, | |
| 37 | + snapshot: CatalogSnapshotV1Schema, | |
| 44 | 38 | }); |
| 45 | 39 | |
| 46 | 40 | export type LocalSourceArtifact = typeof LocalSourceArtifactSchema.Type; |
| @@ -65,90 +59,32 @@ const localSourceArtifactPath = (input: { · 18 unmodified lines | ||
| 65 | 59 | `${input.sourceId}.json`, |
| 66 | 60 | ); |
| 67 | 61 | |
| 68 | −const canonicalMaterializationHash = (input: { | |
| 69 | − materialization: SourceRecipeMaterialization; | |
| 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 | − })); | |
| 62 | +const snapshotHash = (snapshot: SourceCatalogSyncResult["snapshot"]): string => | |
| 63 | + contentHash(JSON.stringify(snapshot)); | |
| 125 | 64 | |
| 126 | 65 | export const buildLocalSourceArtifact = (input: { |
| 127 | 66 | source: Source; |
| 128 | − materialization: SourceRecipeMaterialization; | |
| 67 | + syncResult: SourceCatalogSyncResult; | |
| 129 | 68 | }): LocalSourceArtifact => { |
| 130 | − const recipeId: SourceRecipeId = stableSourceRecipeId(input.source); | |
| 131 | − const now = Date.now(); | |
| 132 | − const revision = createSourceRecipeRevisionRecord({ | |
| 69 | + const catalogId: SourceCatalogId = stableSourceCatalogId(input.source); | |
| 70 | + const generatedAt = Date.now(); | |
| 71 | + const hash = snapshotHash(input.syncResult.snapshot); | |
| 72 | + const revision = createSourceCatalogRevisionRecord({ | |
| 133 | 73 | source: input.source, |
| 134 | − recipeId, | |
| 74 | + catalogId, | |
| 135 | 75 | revisionNumber: 1, |
| 136 | − manifestJson: input.materialization.manifestJson, | |
| 137 | − manifestHash: input.materialization.manifestHash, | |
| 138 | − materializationHash: canonicalMaterializationHash({ | |
| 139 | − materialization: input.materialization, | |
| 140 | − }), | |
| 76 | + importMetadataJson: JSON.stringify(input.syncResult.snapshot.import), | |
| 77 | + importMetadataHash: hash, | |
| 78 | + snapshotHash: hash, | |
| 141 | 79 | }); |
| 142 | 80 | |
| 143 | 81 | return { |
| 144 | 82 | version: LOCAL_SOURCE_ARTIFACT_VERSION, |
| 145 | 83 | sourceId: input.source.id, |
| 146 | − recipeId, | |
| 147 | − generatedAt: now, | |
| 84 | + catalogId, | |
| 85 | + generatedAt, | |
| 148 | 86 | revision, |
| 149 | − documents: bindRevisionId(input.materialization.documents, revision.id), | |
| 150 | − schemaBundles: bindRevisionId(input.materialization.schemaBundles, revision.id), | |
| 151 | − operations: bindRevisionId(input.materialization.operations, revision.id), | |
| 87 | + snapshot: input.syncResult.snapshot, | |
| 152 | 88 | }; |
| 153 | 89 | }; |
| 154 | 90 | |
| @@ -175,13 +111,12 @@ export const readLocalSourceArtifact = (input: { · 20 unmodified lines | ||
| 175 | 111 | ); |
| 176 | 112 | return yield* Effect.try({ |
| 177 | 113 | try: () => decodeLocalSourceArtifact(JSON.parse(content) as unknown), |
| 178 | − catch: (cause) => { | |
| 179 | − return new LocalSourceArtifactDecodeError({ | |
| 114 | + catch: (cause) => | |
| 115 | + new LocalSourceArtifactDecodeError({ | |
| 180 | 116 | message: `Invalid local source artifact at ${path}: ${unknownLocalErrorDetails(cause)}`, |
| 181 | 117 | path, |
| 182 | 118 | details: unknownLocalErrorDetails(cause), |
| 183 | − }); | |
| 184 | − }, | |
| 119 | + }), | |
| 185 | 120 | }); |
| 186 | 121 | }); |
| 187 | 122 | |
| @@ -32,7 +32,7 @@ import { · 31 unmodified lines | ||
| 32 | 32 | loadLocalWorkspaceState, |
| 33 | 33 | writeLocalWorkspaceState, |
| 34 | 34 | } from "./local-workspace-state"; |
| 35 | −import type { SourceRecipeMaterialization } from "./source-recipe-support"; | |
| 35 | +import type { SourceCatalogSyncResult } from "./source-catalog-support"; | |
| 36 | 36 | import type { Source } from "#schema"; |
| 37 | 37 | |
| 38 | 38 | export type InstallationStoreShape = { |
| @@ -80,7 +80,7 @@ export class WorkspaceStateStore extends Context.Tag( · 41 unmodified lines | ||
| 80 | 80 | export type SourceArtifactStoreShape = { |
| 81 | 81 | build: (input: { |
| 82 | 82 | source: Source; |
| 83 | − materialization: SourceRecipeMaterialization; | |
| 83 | + syncResult: SourceCatalogSyncResult; | |
| 84 | 84 | }) => LocalSourceArtifact; |
| 85 | 85 | read: (input: { |
| 86 | 86 | context: ResolvedLocalWorkspaceContext; |
| @@ -1,6 +1,6 @@ | ||
| 1 | 1 | import { createServer } from "node:http"; |
| 2 | 2 | |
| 3 | −import { describe, expect, it } from "vitest"; | |
| 3 | +import { describe, expect, it } from "@effect/vitest"; | |
| 4 | 4 | import * as Effect from "effect/Effect"; |
| 5 | 5 | |
| 6 | 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 | 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 | 60 | Effect.tryPromise({ |
| 61 | 61 | try: async () => { |
| 62 | 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( | |
| 90 | − googleDiscoverySourceAdapter.materializeSource({ | |
| 89 | + const syncResult = await Effect.runPromise( | |
| 90 | + googleDiscoverySourceAdapter.syncCatalog({ | |
| 91 | 91 | source, |
| 92 | 92 | resolveSecretMaterial: () => Effect.fail(new Error("unexpected secret lookup")), |
| 93 | 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"); | |
| 107 | − expect(materialization.schemaBundles.length).toBeGreaterThan(0); | |
| 108 | − expect(materialization.operations.length).toBeGreaterThan(50); | |
| 106 | + expect(Object.values(syncResult.snapshot.catalog.documents)[0]?.kind).toBe("google-discovery"); | |
| 107 | + expect(Object.keys(syncResult.snapshot.catalog.resources)).not.toHaveLength(0); | |
| 108 | + expect(Object.keys(syncResult.snapshot.catalog.capabilities).length).toBeGreaterThan(50); | |
| 109 | 109 | expect( |
| 110 | − materialization.operations.some( | |
| 111 | − (operation) => operation.toolId === "spreadsheets.values.get", | |
| 110 | + Object.values(syncResult.snapshot.catalog.capabilities).some((capability) => | |
| 111 | + capability.surface.toolPath.join(".") === "google.sheets.spreadsheets.values.get" | |
| 112 | 112 | ), |
| 113 | 113 | ).toBe(true); |
| 114 | 114 | }); |
| @@ -3,55 +3,32 @@ import { · 2 unmodified lines | ||
| 3 | 3 | HttpClient, |
| 4 | 4 | HttpClientRequest, |
| 5 | 5 | } from "@effect/platform"; |
| 6 | −import { | |
| 7 | − allowAllToolInteractions, | |
| 8 | − makeToolInvokerFromTools, | |
| 9 | − typeSignatureFromSchemaJson, | |
| 10 | −} from "@executor/codemode-core"; | |
| 11 | 6 | import { |
| 12 | 7 | compileGoogleDiscoveryToolDefinitions, |
| 13 | − createGoogleDiscoveryToolFromDefinition, | |
| 14 | − decodeGoogleDiscoverySchemaRefTableJson, | |
| 15 | 8 | extractGoogleDiscoveryManifest, |
| 16 | − googleDiscoveryProviderDataJsonFromDefinition, | |
| 17 | − GoogleDiscoveryToolProviderDataSchema, | |
| 18 | − type GoogleDiscoveryToolManifest, | |
| 9 | + type GoogleDiscoveryToolProviderData, | |
| 10 | + googleDiscoveryProviderDataFromDefinition, | |
| 19 | 11 | } from "@executor/codemode-google-discovery"; |
| 20 | −import type { | |
| 21 | − Source, | |
| 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"; | |
| 12 | +import type { Source } from "#schema"; | |
| 13 | +import { StringMapSchema } from "#schema"; | |
| 32 | 14 | import * as Effect from "effect/Effect"; |
| 33 | 15 | import * as Schema from "effect/Schema"; |
| 34 | 16 | |
| 35 | 17 | import { |
| 36 | − contentHash, | |
| 37 | − normalizeSearchText, | |
| 38 | −} from "../source-recipe-support"; | |
| 39 | −import type { | |
| 40 | − SourceAdapter, | |
| 41 | − SourceAdapterMaterialization, | |
| 42 | −} from "./types"; | |
| 18 | + createGoogleDiscoveryCatalogSnapshot, | |
| 19 | + type GoogleDiscoveryCatalogOperationInput, | |
| 20 | +} from "../source-catalog-snapshot"; | |
| 21 | +import type { SourceAdapter } from "./types"; | |
| 43 | 22 | import { |
| 44 | 23 | ConnectHttpAuthSchema, |
| 45 | 24 | ConnectHttpImportAuthSchema, |
| 46 | 25 | ConnectOauthClientSchema, |
| 47 | − createStandardToolDescriptor, | |
| 48 | 26 | decodeBindingConfig, |
| 49 | 27 | decodeSourceBindingPayload, |
| 50 | 28 | emptySourceBindingState, |
| 51 | 29 | encodeBindingConfig, |
| 52 | 30 | isSourceCredentialRequiredError, |
| 53 | 31 | OptionalNullableStringSchema, |
| 54 | − parseJsonValue, | |
| 55 | 32 | SourceCredentialRequiredError, |
| 56 | 33 | } from "./shared"; |
| 57 | 34 | |
| @@ -97,13 +74,6 @@ const GoogleDiscoverySourceBindingPayloadSchema = Schema.Struct({ · 39 unmodified lines | ||
| 97 | 74 | type GoogleDiscoveryBindingConfig = typeof GoogleDiscoveryBindingConfigSchema.Type; |
| 98 | 75 | |
| 99 | 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 | 77 | const bindingHasAnyField = ( |
| 108 | 78 | value: unknown, |
| 109 | 79 | fields: readonly string[], |
| @@ -205,129 +175,30 @@ const fetchGoogleDiscoveryDocumentWithHeaders = (input: { · 95 unmodified lines | ||
| 205 | 175 | ); |
| 206 | 176 | }).pipe(Effect.provide(FetchHttpClient.layer)); |
| 207 | 177 | |
| 208 | −const toGoogleDiscoverySchemaBundleRecord = (input: { | |
| 209 | − recipeRevisionId: SourceRecipeRevisionId; | |
| 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; | |
| 178 | +const googleDiscoveryCatalogOperationFromDefinition = (input: { | |
| 179 | + manifest: Parameters<typeof compileGoogleDiscoveryToolDefinitions>[0]; | |
| 245 | 180 | definition: ReturnType<typeof compileGoogleDiscoveryToolDefinitions>[number]; |
| 246 | − now: number; | |
| 247 | −}): StoredSourceRecipeOperationRecord => ({ | |
| 248 | − id: `src_recipe_op_${crypto.randomUUID()}`, | |
| 249 | − recipeRevisionId: input.recipeRevisionId, | |
| 250 | − operationKey: input.definition.toolId, | |
| 251 | − transportKind: "http", | |
| 181 | +}): GoogleDiscoveryCatalogOperationInput => ({ | |
| 252 | 182 | toolId: input.definition.toolId, |
| 253 | 183 | title: input.definition.name, |
| 254 | 184 | description: input.definition.description, |
| 255 | − operationKind: | |
| 185 | + effect: | |
| 256 | 186 | input.definition.method === "get" || input.definition.method === "head" |
| 257 | 187 | ? "read" |
| 258 | 188 | : input.definition.method === "delete" |
| 259 | 189 | ? "delete" |
| 260 | 190 | : "write", |
| 261 | − searchText: normalizeSearchText( | |
| 262 | − input.definition.toolId, | |
| 263 | − input.definition.rawToolId, | |
| 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({ | |
| 191 | + inputSchema: input.definition.inputSchema, | |
| 192 | + outputSchema: input.definition.outputSchema, | |
| 193 | + providerData: googleDiscoveryProviderDataFromDefinition({ | |
| 279 | 194 | service: input.manifest.service, |
| 280 | 195 | version: input.manifest.versionName, |
| 281 | 196 | rootUrl: input.manifest.rootUrl, |
| 282 | 197 | servicePath: input.manifest.servicePath, |
| 283 | 198 | definition: input.definition, |
| 284 | − }), | |
| 285 | − createdAt: input.now, | |
| 286 | − updatedAt: input.now, | |
| 199 | + }) as GoogleDiscoveryToolProviderData, | |
| 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 | 202 | const googleDiscoveryOauth2SetupConfig = (source: Source) => |
| 332 | 203 | Effect.gen(function* () { |
| 333 | 204 | const bindingConfig = yield* googleDiscoveryBindingConfigFromSource(source); |
| @@ -369,8 +240,6 @@ export const googleDiscoverySourceAdapter: SourceAdapter = { · 35 unmodified lines | ||
| 369 | 240 | bindingConfigVersion: GOOGLE_DISCOVERY_BINDING_CONFIG_VERSION, |
| 370 | 241 | providerKey: "google_workspace", |
| 371 | 242 | defaultImportAuthPolicy: "reuse_runtime", |
| 372 | − primaryDocumentKind: "google_discovery", | |
| 373 | − primarySchemaBundleKind: "json_schema_ref_map", | |
| 374 | 243 | connectPayloadSchema: GoogleDiscoveryConnectPayloadSchema, |
| 375 | 244 | executorAddInputSchema: GoogleDiscoveryExecutorAddInputSchema, |
| 376 | 245 | executorAddHelpText: [ |
| @@ -438,71 +307,7 @@ export const googleDiscoverySourceAdapter: SourceAdapter = { · 61 unmodified lines | ||
| 438 | 307 | }), |
| 439 | 308 | shouldAutoProbe: (source) => |
| 440 | 309 | source.enabled && (source.status === "draft" || source.status === "probing"), |
| 441 | − parseManifest: ({ source, manifestJson }) => | |
| 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 | − > => | |
| 310 | + syncCatalog: ({ source, resolveAuthMaterialForSlot }) => | |
| 506 | 311 | Effect.gen(function* () { |
| 507 | 312 | const bindingConfig = yield* googleDiscoveryBindingConfigFromSource(source); |
| 508 | 313 | const auth = yield* resolveAuthMaterialForSlot("import"); |
| @@ -529,37 +334,24 @@ export const googleDiscoverySourceAdapter: SourceAdapter = { · 20 unmodified lines | ||
| 529 | 334 | ); |
| 530 | 335 | const definitions = compileGoogleDiscoveryToolDefinitions(manifest); |
| 531 | 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 | 338 | return { |
| 539 | − manifestJson: JSON.stringify(manifest), | |
| 540 | − manifestHash: manifest.sourceHash, | |
| 541 | − sourceHash: manifest.sourceHash, | |
| 542 | − documents: [ | |
| 543 | − { | |
| 544 | − id: `src_recipe_doc_${crypto.randomUUID()}`, | |
| 545 | − recipeRevisionId: GOOGLE_DISCOVERY_MATERIALIZATION_REVISION_ID, | |
| 339 | + snapshot: createGoogleDiscoveryCatalogSnapshot({ | |
| 340 | + source, | |
| 341 | + documents: [{ | |
| 546 | 342 | documentKind: "google_discovery", |
| 547 | 343 | documentKey: bindingConfig.discoveryUrl, |
| 548 | 344 | contentText: discoveryDocument, |
| 549 | − contentHash: contentHash(discoveryDocument), | |
| 550 | 345 | fetchedAt: now, |
| 551 | − createdAt: now, | |
| 552 | − updatedAt: now, | |
| 553 | − }, | |
| 554 | − ], | |
| 555 | − schemaBundles: schemaBundle ? [schemaBundle] : [], | |
| 556 | − operations: definitions.map((definition) => | |
| 557 | − toGoogleDiscoveryRecipeOperationRecord({ | |
| 558 | − recipeRevisionId: GOOGLE_DISCOVERY_MATERIALIZATION_REVISION_ID, | |
| 559 | − manifest, | |
| 560 | − definition, | |
| 561 | − now, | |
| 562 | − })), | |
| 346 | + }], | |
| 347 | + operations: definitions.map((definition) => | |
| 348 | + googleDiscoveryCatalogOperationFromDefinition({ | |
| 349 | + manifest, | |
| 350 | + definition, | |
| 351 | + }) | |
| 352 | + ), | |
| 353 | + }), | |
| 354 | + sourceHash: manifest.sourceHash, | |
| 563 | 355 | }; |
| 564 | 356 | }), |
| 565 | 357 | getOauth2SetupConfig: ({ source }) => googleDiscoveryOauth2SetupConfig(source), |
| @@ -568,71 +360,4 @@ export const googleDiscoverySourceAdapter: SourceAdapter = { · 2 unmodified lines | ||
| 568 | 360 | ...input, |
| 569 | 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 | 1 | import { |
| 2 | − allowAllToolInteractions, | |
| 3 | 2 | applyCookiePlacementsToHeaders, |
| 4 | 3 | applyHttpQueryPlacementsToUrl, |
| 5 | 4 | applyJsonBodyPlacements, |
| 6 | − makeToolInvokerFromTools, | |
| 7 | − typeSignatureFromSchemaJson, | |
| 8 | 5 | } from "@executor/codemode-core"; |
| 9 | −import type { | |
| 10 | − GraphqlToolManifest, | |
| 11 | −} from "../graphql-tools"; | |
| 12 | 6 | import { |
| 13 | 7 | buildGraphqlToolPresentation, |
| 14 | 8 | compileGraphqlToolDefinitions, |
| 15 | − createGraphqlToolFromPersistedOperation, | |
| 16 | − decodeGraphqlSchemaRefTableJson, | |
| 17 | 9 | extractGraphqlManifest, |
| 18 | − GraphqlToolProviderDataSchema, | |
| 19 | 10 | GRAPHQL_INTROSPECTION_QUERY, |
| 11 | + type GraphqlToolProviderData, | |
| 20 | 12 | } from "../graphql-tools"; |
| 21 | −import type { | |
| 22 | − Source, | |
| 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"; | |
| 13 | +import type { Source } from "#schema"; | |
| 14 | +import { StringMapSchema } from "#schema"; | |
| 33 | 15 | import * as Effect from "effect/Effect"; |
| 34 | 16 | import * as Schema from "effect/Schema"; |
| 35 | 17 | |
| 36 | −import { namespaceFromSourceName } from "../source-names"; | |
| 37 | 18 | import { |
| 38 | − contentHash, | |
| 39 | − normalizeSearchText, | |
| 40 | −} from "../source-recipe-support"; | |
| 41 | −import type { | |
| 42 | − SourceAdapter, | |
| 43 | − SourceAdapterMaterialization, | |
| 44 | −} from "./types"; | |
| 19 | + createGraphqlCatalogSnapshot, | |
| 20 | + type GraphqlCatalogOperationInput, | |
| 21 | +} from "../source-catalog-snapshot"; | |
| 22 | +import type { SourceAdapter } from "./types"; | |
| 45 | 23 | import { |
| 46 | 24 | ConnectHttpAuthSchema, |
| 47 | 25 | ConnectHttpImportAuthSchema, |
| 48 | − createStandardToolDescriptor, | |
| 49 | 26 | decodeBindingConfig, |
| 50 | 27 | decodeSourceBindingPayload, |
| 51 | 28 | emptySourceBindingState, |
| 52 | 29 | encodeBindingConfig, |
| 53 | 30 | isSourceCredentialRequiredError, |
| 54 | 31 | OptionalNullableStringSchema, |
| 55 | − parseJsonValue, | |
| 56 | 32 | SourceCredentialRequiredError, |
| 57 | 33 | SourceConnectCommonFieldsSchema, |
| 58 | 34 | } from "./shared"; |
| @@ -91,12 +67,6 @@ const GraphqlSourceBindingPayloadSchema = Schema.Struct({ · 32 unmodified lines | ||
| 91 | 67 | |
| 92 | 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 | 70 | const bindingHasAnyField = ( |
| 101 | 71 | value: unknown, |
| 102 | 72 | fields: readonly string[], |
| @@ -198,92 +168,29 @@ const fetchGraphqlIntrospectionDocumentWithHeaders = (input: { · 95 unmodified lines | ||
| 198 | 168 | cause instanceof Error ? cause : new Error(String(cause)), |
| 199 | 169 | }); |
| 200 | 170 | |
| 201 | −const toGraphqlSchemaBundleRecord = (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; | |
| 171 | +const graphqlCatalogOperationFromDefinition = (input: { | |
| 237 | 172 | definition: ReturnType<typeof compileGraphqlToolDefinitions>[number]; |
| 238 | 173 | manifest: Parameters<typeof buildGraphqlToolPresentation>[0]["manifest"]; |
| 239 | − now: number; | |
| 240 | −}): StoredSourceRecipeOperationRecord => { | |
| 174 | +}): GraphqlCatalogOperationInput => { | |
| 241 | 175 | const presentation = buildGraphqlToolPresentation({ |
| 242 | 176 | manifest: input.manifest, |
| 243 | 177 | definition: input.definition, |
| 244 | 178 | }); |
| 245 | 179 | |
| 246 | 180 | return { |
| 247 | − id: `src_recipe_op_${crypto.randomUUID()}`, | |
| 248 | − recipeRevisionId: input.recipeRevisionId, | |
| 249 | − operationKey: input.definition.toolId, | |
| 250 | − transportKind: "graphql", | |
| 251 | 181 | toolId: input.definition.toolId, |
| 252 | 182 | title: input.definition.name, |
| 253 | 183 | description: input.definition.description, |
| 254 | − operationKind: | |
| 184 | + effect: | |
| 255 | 185 | input.definition.operationType === "query" |
| 256 | 186 | ? "read" |
| 257 | − : input.definition.operationType === "mutation" | |
| 258 | − ? "write" | |
| 259 | − : "unknown", | |
| 260 | − searchText: normalizeSearchText( | |
| 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, | |
| 187 | + : "write", | |
| 188 | + inputSchema: presentation.inputSchema, | |
| 189 | + outputSchema: presentation.outputSchema, | |
| 190 | + providerData: presentation.providerData as GraphqlToolProviderData, | |
| 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 | 194 | export const graphqlSourceAdapter: SourceAdapter = { |
| 288 | 195 | key: "graphql", |
| 289 | 196 | displayName: "GraphQL", |
| @@ -291,8 +198,6 @@ export const graphqlSourceAdapter: SourceAdapter = { · 1 unmodified line | ||
| 291 | 198 | bindingConfigVersion: GRAPHQL_BINDING_CONFIG_VERSION, |
| 292 | 199 | providerKey: "generic_graphql", |
| 293 | 200 | defaultImportAuthPolicy: "reuse_runtime", |
| 294 | − primaryDocumentKind: "graphql_introspection", | |
| 295 | − primarySchemaBundleKind: "json_schema_ref_map", | |
| 296 | 201 | connectPayloadSchema: GraphqlConnectPayloadSchema, |
| 297 | 202 | executorAddInputSchema: GraphqlExecutorAddInputSchema, |
| 298 | 203 | executorAddHelpText: [ |
| @@ -349,68 +254,7 @@ export const graphqlSourceAdapter: SourceAdapter = { · 50 unmodified lines | ||
| 349 | 254 | }), |
| 350 | 255 | shouldAutoProbe: (source) => |
| 351 | 256 | source.enabled && (source.status === "draft" || source.status === "probing"), |
| 352 | − parseManifest: ({ source, manifestJson }) => | |
| 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 }) => | |
| 257 | + syncCatalog: ({ source, resolveAuthMaterialForSlot }) => | |
| 414 | 258 | Effect.gen(function* () { |
| 415 | 259 | const bindingConfig = yield* graphqlBindingConfigFromSource(source); |
| 416 | 260 | const auth = yield* resolveAuthMaterialForSlot("import"); |
| @@ -447,94 +291,24 @@ export const graphqlSourceAdapter: SourceAdapter = { · 30 unmodified lines | ||
| 447 | 291 | |
| 448 | 292 | const definitions = compileGraphqlToolDefinitions(manifest); |
| 449 | 293 | const now = Date.now(); |
| 450 | − const schemaBundle = toGraphqlSchemaBundleRecord({ | |
| 451 | − recipeRevisionId: GRAPHQL_MATERIALIZATION_REVISION_ID, | |
| 452 | − manifest, | |
| 453 | − now, | |
| 454 | − }); | |
| 455 | 294 | |
| 456 | 295 | return { |
| 457 | − manifestJson: JSON.stringify(manifest), | |
| 458 | − manifestHash: manifest.sourceHash, | |
| 459 | − sourceHash: manifest.sourceHash, | |
| 460 | − documents: [ | |
| 461 | − { | |
| 462 | − id: `src_recipe_doc_${crypto.randomUUID()}`, | |
| 463 | − recipeRevisionId: GRAPHQL_MATERIALIZATION_REVISION_ID, | |
| 296 | + snapshot: createGraphqlCatalogSnapshot({ | |
| 297 | + source, | |
| 298 | + documents: [{ | |
| 464 | 299 | documentKind: "graphql_introspection", |
| 465 | 300 | documentKey: source.endpoint, |
| 466 | 301 | contentText: graphqlDocument, |
| 467 | − contentHash: contentHash(graphqlDocument), | |
| 468 | 302 | fetchedAt: now, |
| 469 | − createdAt: now, | |
| 470 | − updatedAt: now, | |
| 471 | − }, | |
| 472 | − ], | |
| 473 | − schemaBundles: schemaBundle ? [schemaBundle] : [], | |
| 474 | − operations: definitions.map((definition) => | |
| 475 | − toGraphqlRecipeOperationRecord({ | |
| 476 | − recipeRevisionId: GRAPHQL_MATERIALIZATION_REVISION_ID, | |
| 477 | − definition, | |
| 478 | − manifest, | |
| 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 | − ); | |
| 303 | + }], | |
| 304 | + operations: definitions.map((definition) => | |
| 305 | + graphqlCatalogOperationFromDefinition({ | |
| 306 | + definition, | |
| 307 | + manifest, | |
| 308 | + }) | |
| 309 | + ), | |
| 310 | + }), | |
| 311 | + sourceHash: manifest.sourceHash, | |
| 312 | + }; | |
| 539 | 313 | }), |
| 540 | 314 | }; |
| @@ -1,4 +1,4 @@ | ||
| 1 | −import type { Source, StoredSourceRecipeOperationRecord } from "#schema"; | |
| 1 | +import type { Source } from "#schema"; | |
| 2 | 2 | import * as Schema from "effect/Schema"; |
| 3 | 3 | |
| 4 | 4 | import { graphqlSourceAdapter } from "./graphql"; |
| @@ -67,10 +67,6 @@ export const getSourceAdapterForSource = (source: Pick<Source, "kind">): SourceA · 62 unmodified lines | ||
| 67 | 67 | export const sourceBindingStateFromSource = (source: Source) => |
| 68 | 68 | getSourceAdapterForSource(source).bindingStateFromSource(source); |
| 69 | 69 | |
| 70 | −export const getSourceAdapterForOperation = ( | |
| 71 | − operation: Pick<StoredSourceRecipeOperationRecord, "providerKind">, | |
| 72 | −): SourceAdapter => getSourceAdapter(operation.providerKind); | |
| 73 | − | |
| 74 | 70 | export const hasSourceAdapterFamily = ( |
| 75 | 71 | key: string, |
| 76 | 72 | family: SourceAdapter["family"], |
| @@ -4,7 +4,6 @@ import type { Source } from "#schema"; · 3 unmodified lines | ||
| 4 | 4 | |
| 5 | 5 | import type { SourceAdapter } from "./types"; |
| 6 | 6 | import { |
| 7 | − createStandardToolDescriptor, | |
| 8 | 7 | decodeBindingConfig, |
| 9 | 8 | decodeSourceBindingPayload, |
| 10 | 9 | emptySourceBindingState, |
| @@ -58,8 +57,6 @@ export const internalSourceAdapter: SourceAdapter = { · 47 unmodified lines | ||
| 58 | 57 | bindingConfigVersion: INTERNAL_BINDING_CONFIG_VERSION, |
| 59 | 58 | providerKey: "generic_internal", |
| 60 | 59 | defaultImportAuthPolicy: "none", |
| 61 | − primaryDocumentKind: null, | |
| 62 | − primarySchemaBundleKind: null, | |
| 63 | 60 | connectPayloadSchema: null, |
| 64 | 61 | executorAddInputSchema: null, |
| 65 | 62 | executorAddHelpText: null, |
| @@ -102,40 +99,28 @@ export const internalSourceAdapter: SourceAdapter = { · 36 unmodified lines | ||
| 102 | 99 | }; |
| 103 | 100 | }), |
| 104 | 101 | shouldAutoProbe: () => false, |
| 105 | − parseManifest: () => Effect.succeed(null), | |
| 106 | − describePersistedOperation: ({ operation, path }) => | |
| 107 | − Effect.succeed({ | |
| 108 | − method: null, | |
| 109 | − pathTemplate: null, | |
| 110 | − rawToolId: null, | |
| 111 | − operationId: null, | |
| 112 | − group: null, | |
| 113 | − leaf: null, | |
| 114 | − tags: [], | |
| 115 | − searchText: [path, operation.toolId, operation.title ?? "", operation.description ?? "", operation.searchText] | |
| 116 | − .filter((part) => part.length > 0) | |
| 117 | − .join(" ") | |
| 118 | − .toLowerCase(), | |
| 119 | − interaction: "auto", | |
| 120 | − approvalLabel: null, | |
| 121 | − } as const), | |
| 122 | − createToolDescriptor: ({ source, operation, path, includeSchemas, schemaBundleId }) => | |
| 123 | − createStandardToolDescriptor({ | |
| 124 | − source, | |
| 125 | − operation, | |
| 126 | − path, | |
| 127 | − includeSchemas, | |
| 128 | − interaction: "auto", | |
| 129 | − schemaBundleId, | |
| 130 | − }), | |
| 131 | − materializeSource: () => Effect.succeed({ | |
| 132 | − manifestJson: null, | |
| 133 | − manifestHash: null, | |
| 102 | + syncCatalog: () => Effect.succeed({ | |
| 103 | + snapshot: { | |
| 104 | + version: "ir.v1.snapshot", | |
| 105 | + import: { | |
| 106 | + sourceKind: "custom", | |
| 107 | + adapterKey: "internal", | |
| 108 | + importerVersion: "ir.v1.internal", | |
| 109 | + importedAt: new Date().toISOString(), | |
| 110 | + sourceConfigHash: "internal", | |
| 111 | + }, | |
| 112 | + catalog: { | |
| 113 | + version: "ir.v1", | |
| 114 | + documents: {}, | |
| 115 | + resources: {}, | |
| 116 | + scopes: {}, | |
| 117 | + symbols: {}, | |
| 118 | + capabilities: {}, | |
| 119 | + executables: {}, | |
| 120 | + responseSets: {}, | |
| 121 | + diagnostics: {}, | |
| 122 | + }, | |
| 123 | + }, | |
| 134 | 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 | 1 | import { |
| 2 | − allowAllToolInteractions, | |
| 3 | 2 | applyCookiePlacementsToHeaders, |
| 4 | − makeToolInvokerFromTools, | |
| 5 | 3 | } from "@executor/codemode-core"; |
| 6 | 4 | import { |
| 7 | − createMcpToolsFromManifest, | |
| 8 | 5 | createSdkMcpConnector, |
| 9 | 6 | discoverMcpToolsFromConnector, |
| 10 | 7 | type McpToolManifest, |
| @@ -12,8 +9,6 @@ import { · 1 unmodified line | ||
| 12 | 9 | } from "@executor/codemode-mcp"; |
| 13 | 10 | import type { |
| 14 | 11 | Source, |
| 15 | − SourceRecipeRevisionId, | |
| 16 | − StoredSourceRecipeOperationRecord, | |
| 17 | 12 | } from "#schema"; |
| 18 | 13 | import { |
| 19 | 14 | SourceTransportSchema, |
| @@ -22,22 +17,23 @@ import { · 2 unmodified lines | ||
| 22 | 17 | import * as Effect from "effect/Effect"; |
| 23 | 18 | import * as Schema from "effect/Schema"; |
| 24 | 19 | |
| 20 | +import { | |
| 21 | + createMcpCatalogSnapshot, | |
| 22 | + type McpCatalogOperationInput, | |
| 23 | +} from "../source-catalog-snapshot"; | |
| 25 | 24 | import { |
| 26 | 25 | contentHash, |
| 27 | − normalizeSearchText, | |
| 28 | − type SourceRecipeMaterialization, | |
| 29 | −} from "../source-recipe-support"; | |
| 26 | + type SourceCatalogSyncResult, | |
| 27 | +} from "../source-catalog-support"; | |
| 30 | 28 | import { namespaceFromSourceName } from "../source-names"; |
| 31 | −import type { SourceAdapter, SourceAdapterMaterialization } from "./types"; | |
| 29 | +import type { SourceAdapter } from "./types"; | |
| 32 | 30 | import { |
| 33 | − createStandardToolDescriptor, | |
| 34 | 31 | decodeBindingConfig, |
| 35 | 32 | decodeSourceBindingPayload, |
| 36 | 33 | emptySourceBindingState, |
| 37 | 34 | encodeBindingConfig, |
| 38 | 35 | McpConnectFieldsSchema, |
| 39 | 36 | OptionalNullableStringSchema, |
| 40 | − parseJsonValue, | |
| 41 | 37 | SourceConnectCommonFieldsSchema, |
| 42 | 38 | } from "./shared"; |
| 43 | 39 | |
| @@ -87,17 +83,6 @@ const McpSourceBindingPayloadSchema = Schema.Struct({ · 43 unmodified lines | ||
| 87 | 83 | |
| 88 | 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 | 86 | const bindingHasAnyField = ( |
| 102 | 87 | value: unknown, |
| 103 | 88 | fields: readonly string[], |
| @@ -137,43 +122,25 @@ const mcpBindingConfigFromSource = ( · 33 unmodified lines | ||
| 137 | 122 | } satisfies McpBindingConfig; |
| 138 | 123 | }); |
| 139 | 124 | |
| 140 | −const toMcpRecipeOperationRecord = (input: { | |
| 141 | − recipeRevisionId: SourceRecipeRevisionId; | |
| 142 | − entry: McpToolManifestEntry; | |
| 143 | − now: number; | |
| 144 | −}): StoredSourceRecipeOperationRecord => ({ | |
| 145 | − id: `src_recipe_op_${crypto.randomUUID()}`, | |
| 146 | − recipeRevisionId: input.recipeRevisionId, | |
| 147 | − operationKey: input.entry.toolId, | |
| 148 | − transportKind: "mcp", | |
| 149 | − toolId: input.entry.toolId, | |
| 150 | − title: input.entry.toolName, | |
| 151 | − description: input.entry.description ?? null, | |
| 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, | |
| 125 | +const mcpCatalogOperationFromManifestEntry = (entry: McpToolManifestEntry): McpCatalogOperationInput => ({ | |
| 126 | + toolId: entry.toolId, | |
| 127 | + title: entry.toolName, | |
| 128 | + description: entry.description ?? null, | |
| 129 | + effect: "action", | |
| 130 | + inputSchema: entry.inputSchema, | |
| 131 | + outputSchema: entry.outputSchema, | |
| 132 | + providerData: { | |
| 133 | + toolId: entry.toolId, | |
| 134 | + toolName: entry.toolName, | |
| 135 | + description: entry.description ?? null, | |
| 136 | + }, | |
| 170 | 137 | }); |
| 171 | 138 | |
| 172 | −export const materializationFromMcpManifestEntries = (input: { | |
| 173 | − recipeRevisionId: SourceRecipeRevisionId; | |
| 139 | +export const catalogSyncResultFromMcpManifestEntries = (input: { | |
| 140 | + source: Source; | |
| 174 | 141 | endpoint: string; |
| 175 | 142 | manifestEntries: readonly McpToolManifestEntry[]; |
| 176 | −}): SourceRecipeMaterialization => { | |
| 143 | +}): SourceCatalogSyncResult => { | |
| 177 | 144 | const now = Date.now(); |
| 178 | 145 | const manifest: McpToolManifest = { |
| 179 | 146 | version: 1, |
| @@ -183,30 +150,17 @@ export const materializationFromMcpManifestEntries = (input: { · 3 unmodified lines | ||
| 183 | 150 | const manifestHash = contentHash(manifestJson); |
| 184 | 151 | |
| 185 | 152 | return { |
| 186 | − manifestJson, | |
| 187 | − manifestHash, | |
| 188 | − sourceHash: manifestHash, | |
| 189 | − documents: [ | |
| 190 | − { | |
| 191 | − id: `src_recipe_doc_${crypto.randomUUID()}`, | |
| 192 | − recipeRevisionId: input.recipeRevisionId, | |
| 153 | + snapshot: createMcpCatalogSnapshot({ | |
| 154 | + source: input.source, | |
| 155 | + documents: [{ | |
| 193 | 156 | documentKind: "mcp_manifest", |
| 194 | 157 | documentKey: input.endpoint, |
| 195 | 158 | contentText: manifestJson, |
| 196 | − contentHash: manifestHash, | |
| 197 | 159 | fetchedAt: now, |
| 198 | − createdAt: now, | |
| 199 | − updatedAt: now, | |
| 200 | − }, | |
| 201 | − ], | |
| 202 | − schemaBundles: [], | |
| 203 | − operations: input.manifestEntries.map((entry) => | |
| 204 | − toMcpRecipeOperationRecord({ | |
| 205 | − recipeRevisionId: input.recipeRevisionId, | |
| 206 | − entry, | |
| 207 | − now, | |
| 208 | − }) | |
| 209 | − ), | |
| 160 | + }], | |
| 161 | + operations: input.manifestEntries.map(mcpCatalogOperationFromManifestEntry), | |
| 162 | + }), | |
| 163 | + sourceHash: manifestHash, | |
| 210 | 164 | }; |
| 211 | 165 | }; |
| 212 | 166 | |
| @@ -217,8 +171,6 @@ export const mcpSourceAdapter: SourceAdapter = { · 4 unmodified lines | ||
| 217 | 171 | bindingConfigVersion: MCP_BINDING_CONFIG_VERSION, |
| 218 | 172 | providerKey: "generic_mcp", |
| 219 | 173 | defaultImportAuthPolicy: "reuse_runtime", |
| 220 | − primaryDocumentKind: "mcp_manifest", | |
| 221 | − primarySchemaBundleKind: null, | |
| 222 | 174 | connectPayloadSchema: McpConnectPayloadSchema, |
| 223 | 175 | executorAddInputSchema: McpExecutorAddInputSchema, |
| 224 | 176 | executorAddHelpText: [ |
| @@ -280,53 +232,7 @@ export const mcpSourceAdapter: SourceAdapter = { · 55 unmodified lines | ||
| 280 | 232 | }; |
| 281 | 233 | }), |
| 282 | 234 | shouldAutoProbe: () => false, |
| 283 | − parseManifest: ({ source, manifestJson }) => | |
| 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 }) => | |
| 235 | + syncCatalog: ({ source, resolveAuthMaterialForSlot }) => | |
| 330 | 236 | Effect.gen(function* () { |
| 331 | 237 | const bindingConfig = yield* mcpBindingConfigFromSource(source); |
| 332 | 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({ | |
| 366 | − recipeRevisionId: "src_recipe_rev_materialization" as SourceRecipeRevisionId, | |
| 271 | + return catalogSyncResultFromMcpManifestEntries({ | |
| 272 | + source, | |
| 367 | 273 | endpoint: source.endpoint, |
| 368 | 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 | 4 | HttpClientRequest, |
| 5 | 5 | } from "@effect/platform"; |
| 6 | 6 | import { |
| 7 | − allowAllToolInteractions, | |
| 8 | 7 | applyCookiePlacementsToHeaders, |
| 9 | 8 | applyHttpQueryPlacementsToUrl, |
| 10 | − makeToolInvokerFromTools, | |
| 11 | 9 | } from "@executor/codemode-core"; |
| 12 | 10 | import { |
| 13 | 11 | buildOpenApiToolPresentation, |
| 14 | 12 | compileOpenApiToolDefinitions, |
| 15 | − createOpenApiToolFromDefinition, | |
| 13 | + type OpenApiToolProviderData, | |
| 16 | 14 | extractOpenApiManifest, |
| 17 | − openApiOutputTypeSignatureFromSchemaJson, | |
| 18 | − OpenApiRefHintTableSchema, | |
| 19 | − OpenApiToolProviderDataSchema, | |
| 20 | − type OpenApiToolManifest, | |
| 21 | 15 | } from "@executor/codemode-openapi"; |
| 22 | −import type { | |
| 23 | − Source, | |
| 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"; | |
| 16 | +import type { Source } from "#schema"; | |
| 17 | +import { StringMapSchema } from "#schema"; | |
| 34 | 18 | import * as Effect from "effect/Effect"; |
| 35 | 19 | import * as Schema from "effect/Schema"; |
| 36 | 20 | |
| 37 | 21 | import { |
| 38 | − contentHash, | |
| 39 | − normalizeSearchText, | |
| 40 | −} from "../source-recipe-support"; | |
| 41 | −import type { | |
| 42 | − SourceAdapter, | |
| 43 | − SourceAdapterMaterialization, | |
| 44 | −} from "./types"; | |
| 22 | + createOpenApiCatalogSnapshot, | |
| 23 | + type OpenApiCatalogOperationInput, | |
| 24 | +} from "../source-catalog-snapshot"; | |
| 25 | +import type { SourceAdapter } from "./types"; | |
| 45 | 26 | import { |
| 46 | 27 | ConnectHttpAuthSchema, |
| 47 | 28 | ConnectHttpImportAuthSchema, |
| 48 | − createStandardToolDescriptor, | |
| 49 | 29 | decodeBindingConfig, |
| 50 | 30 | decodeSourceBindingPayload, |
| 51 | 31 | emptySourceBindingState, |
| 52 | 32 | encodeBindingConfig, |
| 53 | 33 | isSourceCredentialRequiredError, |
| 54 | 34 | OptionalNullableStringSchema, |
| 55 | − parseJsonValue, | |
| 56 | 35 | SourceCredentialRequiredError, |
| 57 | 36 | SourceConnectCommonFieldsSchema, |
| 58 | 37 | } from "./shared"; |
| @@ -98,16 +77,6 @@ type OpenApiBindingConfig = { · 39 unmodified lines | ||
| 98 | 77 | |
| 99 | 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 | 80 | const bindingHasAnyField = ( |
| 112 | 81 | value: unknown, |
| 113 | 82 | fields: readonly string[], |
| @@ -198,137 +167,30 @@ const fetchOpenApiDocumentWithHeaders = (input: { · 84 unmodified lines | ||
| 198 | 167 | ); |
| 199 | 168 | }).pipe(Effect.provide(FetchHttpClient.layer)); |
| 200 | 169 | |
| 201 | −const toOpenApiRecipeOperationRecord = (input: { | |
| 202 | − recipeRevisionId: SourceRecipeRevisionId; | |
| 203 | − definition: ReturnType<typeof compileOpenApiToolDefinitions>[number]; | |
| 204 | − now: number; | |
| 205 | −}): StoredSourceRecipeOperationRecord => { | |
| 170 | +const openApiCatalogOperationFromDefinition = ( | |
| 171 | + definition: ReturnType<typeof compileOpenApiToolDefinitions>[number], | |
| 172 | +): OpenApiCatalogOperationInput => { | |
| 206 | 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 | 178 | return { |
| 212 | − id: `src_recipe_op_${crypto.randomUUID()}`, | |
| 213 | − recipeRevisionId: input.recipeRevisionId, | |
| 214 | − operationKey: input.definition.toolId, | |
| 215 | − transportKind: "http", | |
| 216 | − toolId: input.definition.toolId, | |
| 217 | − title: input.definition.name, | |
| 218 | − description: input.definition.description, | |
| 219 | − operationKind: | |
| 179 | + toolId: definition.toolId, | |
| 180 | + title: definition.name, | |
| 181 | + description: definition.description, | |
| 182 | + effect: | |
| 220 | 183 | method === "GET" || method === "HEAD" |
| 221 | 184 | ? "read" |
| 222 | 185 | : method === "DELETE" |
| 223 | 186 | ? "delete" |
| 224 | 187 | : "write", |
| 225 | − searchText: normalizeSearchText( | |
| 226 | − input.definition.toolId, | |
| 227 | − input.definition.name, | |
| 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 | − : {}), | |
| 188 | + inputSchema: presentation.inputSchema, | |
| 189 | + outputSchema: presentation.outputSchema, | |
| 190 | + providerData: presentation.providerData as OpenApiToolProviderData, | |
| 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 | 194 | export const openApiSourceAdapter: SourceAdapter = { |
| 333 | 195 | key: "openapi", |
| 334 | 196 | displayName: "OpenAPI", |
| @@ -336,8 +198,6 @@ export const openApiSourceAdapter: SourceAdapter = { · 1 unmodified line | ||
| 336 | 198 | bindingConfigVersion: OPENAPI_BINDING_CONFIG_VERSION, |
| 337 | 199 | providerKey: "generic_http", |
| 338 | 200 | defaultImportAuthPolicy: "reuse_runtime", |
| 339 | − primaryDocumentKind: "openapi", | |
| 340 | − primarySchemaBundleKind: "json_schema_ref_map", | |
| 341 | 201 | connectPayloadSchema: OpenApiConnectPayloadSchema, |
| 342 | 202 | executorAddInputSchema: OpenApiExecutorAddInputSchema, |
| 343 | 203 | executorAddHelpText: [ |
| @@ -398,65 +258,7 @@ export const openApiSourceAdapter: SourceAdapter = { · 54 unmodified lines | ||
| 398 | 258 | }), |
| 399 | 259 | shouldAutoProbe: (source) => |
| 400 | 260 | source.enabled && (source.status === "draft" || source.status === "probing"), |
| 401 | − parseManifest: ({ source, manifestJson }) => | |
| 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 }) => | |
| 261 | + syncCatalog: ({ source, resolveAuthMaterialForSlot }) => | |
| 460 | 262 | Effect.gen(function* () { |
| 461 | 263 | const bindingConfig = yield* openApiBindingConfigFromSource(source); |
| 462 | 264 | |
| @@ -491,89 +293,19 @@ export const openApiSourceAdapter: SourceAdapter = { · 28 unmodified lines | ||
| 491 | 293 | |
| 492 | 294 | const definitions = compileOpenApiToolDefinitions(manifest); |
| 493 | 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 | 297 | return { |
| 501 | − manifestJson: JSON.stringify(manifest), | |
| 502 | − manifestHash: manifest.sourceHash, | |
| 503 | − sourceHash: manifest.sourceHash, | |
| 504 | − documents: [ | |
| 505 | − { | |
| 506 | − id: `src_recipe_doc_${crypto.randomUUID()}`, | |
| 507 | − recipeRevisionId: OPENAPI_MATERIALIZATION_REVISION_ID, | |
| 298 | + snapshot: createOpenApiCatalogSnapshot({ | |
| 299 | + source, | |
| 300 | + documents: [{ | |
| 508 | 301 | documentKind: "openapi", |
| 509 | 302 | documentKey: bindingConfig.specUrl, |
| 510 | 303 | contentText: openApiDocument, |
| 511 | − contentHash: contentHash(openApiDocument), | |
| 512 | 304 | fetchedAt: now, |
| 513 | − createdAt: now, | |
| 514 | − updatedAt: now, | |
| 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, | |
| 305 | + }], | |
| 306 | + operations: definitions.map(openApiCatalogOperationFromDefinition), | |
| 554 | 307 | }), |
| 555 | − path, | |
| 556 | − sourceKey: source.id, | |
| 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 | − ); | |
| 308 | + sourceHash: manifest.sourceHash, | |
| 309 | + }; | |
| 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 | 1 | import { |
| 8 | 2 | SourceOauthClientInputSchema, |
| 9 | 3 | type CredentialSlot, |
| @@ -15,10 +9,8 @@ import { · 5 unmodified lines | ||
| 15 | 9 | } from "#schema"; |
| 16 | 10 | import type { |
| 17 | 11 | SecretRef, |
| 18 | − Source, | |
| 19 | 12 | SourceBinding, |
| 20 | 13 | SourceTransport, |
| 21 | − StoredSourceRecipeOperationRecord, | |
| 22 | 14 | StringMap, |
| 23 | 15 | } from "#schema"; |
| 24 | 16 | import * as Effect from "effect/Effect"; |
| @@ -28,8 +20,6 @@ import { · 3 unmodified lines | ||
| 28 | 20 | TrimmedNonEmptyStringSchema, |
| 29 | 21 | } from "../../api/string-schemas"; |
| 30 | 22 | |
| 31 | −const asToolPath = (value: string): ToolPath => value as ToolPath; | |
| 32 | − | |
| 33 | 23 | export const OptionalNullableStringSchema = Schema.optional( |
| 34 | 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 | 1 | import type { |
| 2 | − OnElicitation, | |
| 3 | − ToolDescriptor, | |
| 4 | − ToolSchemaBundle, | |
| 5 | −} from "@executor/codemode-core"; | |
| 6 | −import type { | |
| 7 | − AccountId, | |
| 8 | 2 | CredentialSlot, |
| 9 | 3 | OAuth2ClientAuthenticationMethod, |
| 10 | 4 | SourceOauthClientInput, |
| @@ -12,10 +6,6 @@ import type { · 1 unmodified line | ||
| 12 | 6 | SourceBinding, |
| 13 | 7 | SourceImportAuthPolicy, |
| 14 | 8 | SourceTransport, |
| 15 | − StoredSourceRecipeDocumentRecord, | |
| 16 | − StoredSourceRecipeOperationRecord, | |
| 17 | − StoredSourceRecipeSchemaBundleRecord, | |
| 18 | − StoredSourceRecipeRevisionRecord, | |
| 19 | 9 | StoredSourceRecord, |
| 20 | 10 | StringMap, |
| 21 | 11 | } from "#schema"; |
| @@ -24,6 +14,7 @@ import * as Schema from "effect/Schema"; · 2 unmodified lines | ||
| 24 | 14 | |
| 25 | 15 | import type { ResolvedSourceAuthMaterial } from "../source-auth-material"; |
| 26 | 16 | import type { ResolveSecretMaterial } from "../secret-material-providers"; |
| 17 | +import type { SourceCatalogSyncResult } from "../source-catalog-support"; | |
| 27 | 18 | |
| 28 | 19 | export type SourceAdapterFamily = "http_api" | "mcp" | "internal"; |
| 29 | 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 | 41 | export type SourceAdapterOauth2SetupConfig = { |
| 73 | 42 | providerKey: string; |
| 74 | 43 | authorizationEndpoint: string; |
| @@ -80,20 +49,6 @@ export type SourceAdapterOauth2SetupConfig = { · 5 unmodified lines | ||
| 80 | 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 | 52 | export type SourceAdapter = { |
| 98 | 53 | key: string; |
| 99 | 54 | displayName: string; |
| @@ -101,8 +56,6 @@ export type SourceAdapter = { · 1 unmodified line | ||
| 101 | 56 | bindingConfigVersion: number; |
| 102 | 57 | providerKey: string; |
| 103 | 58 | defaultImportAuthPolicy: SourceImportAuthPolicy; |
| 104 | − primaryDocumentKind: string | null; | |
| 105 | − primarySchemaBundleKind: string | null; | |
| 106 | 59 | connectPayloadSchema: SourceAdapterInputSchema | null; |
| 107 | 60 | executorAddInputSchema: SourceAdapterInputSchema | null; |
| 108 | 61 | executorAddHelpText: readonly string[] | null; |
| @@ -115,30 +68,9 @@ export type SourceAdapter = { · 6 unmodified lines | ||
| 115 | 68 | sourceConfigFromSource: (source: Source) => Record<string, unknown>; |
| 116 | 69 | validateSource: (source: Source) => Effect.Effect<Source, Error, never>; |
| 117 | 70 | shouldAutoProbe: (source: Source) => boolean; |
| 118 | − parseManifest: (input: { | |
| 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: ( | |
| 71 | + syncCatalog: ( | |
| 140 | 72 | input: SourceAdapterSyncInput, |
| 141 | − ) => Effect.Effect<SourceAdapterMaterialization, Error, never>; | |
| 73 | + ) => Effect.Effect<SourceCatalogSyncResult, Error, never>; | |
| 142 | 74 | getOauth2SetupConfig?: (input: { |
| 143 | 75 | source: Source; |
| 144 | 76 | slot: CredentialSlot; |
| @@ -146,7 +78,4 @@ export type SourceAdapter = { · 1 unmodified line | ||
| 146 | 78 | normalizeOauthClientInput?: ( |
| 147 | 79 | input: SourceOauthClientInput, |
| 148 | 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 | 2 | import * as Effect from "effect/Effect"; |
| 3 | 3 | |
| 4 | 4 | import { |
| @@ -68,9 +68,9 @@ import { · 67 unmodified lines | ||
| 68 | 68 | } from "./secret-material-providers"; |
| 69 | 69 | import { upsertOauth2AuthorizedUserLeaseFromTokenResponse } from "./auth-leases"; |
| 70 | 70 | import { |
| 71 | − RuntimeSourceMaterializationService, | |
| 72 | − type RuntimeSourceMaterializationShape, | |
| 73 | −} from "./source-materialization"; | |
| 71 | + RuntimeSourceCatalogSyncService, | |
| 72 | + type RuntimeSourceCatalogSyncShape, | |
| 73 | +} from "./source-catalog-sync"; | |
| 74 | 74 | import { |
| 75 | 75 | buildOAuth2AuthorizationUrl, |
| 76 | 76 | createPkceCodeVerifier, |
| @@ -978,7 +978,7 @@ const startOauth2PkceSourceCredentialSetup = (input: { · 901 unmodified lines | ||
| 978 | 978 | const connectMcpSourceInternal = (input: { |
| 979 | 979 | rows: ControlPlaneStoreShape; |
| 980 | 980 | sourceStore: RuntimeSourceStore; |
| 981 | − sourceMaterialization: RuntimeSourceMaterializationShape; | |
| 981 | + sourceCatalogSync: RuntimeSourceCatalogSyncShape; | |
| 982 | 982 | getLocalServerBaseUrl?: () => string | undefined; |
| 983 | 983 | baseUrl?: string | null; |
| 984 | 984 | workspaceId: WorkspaceId; |
| @@ -1087,7 +1087,7 @@ const connectMcpSourceInternal = (input: { · 102 unmodified lines | ||
| 1087 | 1087 | const persistedDraft = yield* input.sourceStore.persistSource(draftSource, { |
| 1088 | 1088 | actorAccountId: input.actorAccountId, |
| 1089 | 1089 | }); |
| 1090 | − yield* input.sourceMaterialization.sync({ | |
| 1090 | + yield* input.sourceCatalogSync.sync({ | |
| 1091 | 1091 | source: persistedDraft, |
| 1092 | 1092 | actorAccountId: input.actorAccountId, |
| 1093 | 1093 | }); |
| @@ -1110,7 +1110,7 @@ const connectMcpSourceInternal = (input: { · 16 unmodified lines | ||
| 1110 | 1110 | auth: { kind: "none" }, |
| 1111 | 1111 | }); |
| 1112 | 1112 | const indexed = yield* Effect.either( |
| 1113 | − input.sourceMaterialization.persistMcpRecipeMaterializationFromManifest({ | |
| 1113 | + input.sourceCatalogSync.persistMcpCatalogSnapshotFromManifest({ | |
| 1114 | 1114 | source: connected, |
| 1115 | 1115 | manifestEntries: result.manifest.tools, |
| 1116 | 1116 | }), |
| @@ -1206,7 +1206,7 @@ const connectMcpSourceInternal = (input: { · 89 unmodified lines | ||
| 1206 | 1206 | const addExecutorHttpSource = (input: { |
| 1207 | 1207 | rows: ControlPlaneStoreShape; |
| 1208 | 1208 | sourceStore: RuntimeSourceStore; |
| 1209 | − sourceMaterialization: RuntimeSourceMaterializationShape; | |
| 1209 | + sourceCatalogSync: RuntimeSourceCatalogSyncShape; | |
| 1210 | 1210 | sourceInput: Extract<ExecutorAddSourceInput, { kind: "openapi" | "graphql" }>; |
| 1211 | 1211 | storeSecretMaterial: StoreSecretMaterial; |
| 1212 | 1212 | resolveSecretMaterial: ResolveSecretMaterial; |
| @@ -1358,7 +1358,7 @@ const addExecutorHttpSource = (input: { · 145 unmodified lines | ||
| 1358 | 1358 | } |
| 1359 | 1359 | |
| 1360 | 1360 | const synced = yield* Effect.either( |
| 1361 | − input.sourceMaterialization.sync({ | |
| 1361 | + input.sourceCatalogSync.sync({ | |
| 1362 | 1362 | source: { |
| 1363 | 1363 | ...persistedDraft, |
| 1364 | 1364 | status: "connected", |
| @@ -1409,7 +1409,7 @@ const addExecutorHttpSource = (input: { · 44 unmodified lines | ||
| 1409 | 1409 | const addExecutorGoogleDiscoverySource = (input: { |
| 1410 | 1410 | rows: ControlPlaneStoreShape; |
| 1411 | 1411 | sourceStore: RuntimeSourceStore; |
| 1412 | − sourceMaterialization: RuntimeSourceMaterializationShape; | |
| 1412 | + sourceCatalogSync: RuntimeSourceCatalogSyncShape; | |
| 1413 | 1413 | sourceInput: Extract<ExecutorAddSourceInput, { kind: "google_discovery" }>; |
| 1414 | 1414 | storeSecretMaterial: StoreSecretMaterial; |
| 1415 | 1415 | resolveSecretMaterial: ResolveSecretMaterial; |
| @@ -1578,7 +1578,7 @@ const addExecutorGoogleDiscoverySource = (input: { · 162 unmodified lines | ||
| 1578 | 1578 | } |
| 1579 | 1579 | |
| 1580 | 1580 | const synced = yield* Effect.either( |
| 1581 | − input.sourceMaterialization.sync({ | |
| 1581 | + input.sourceCatalogSync.sync({ | |
| 1582 | 1582 | source: { |
| 1583 | 1583 | ...persistedDraft, |
| 1584 | 1584 | status: "connected", |
| @@ -1671,7 +1671,7 @@ type RuntimeSourceAuthDependencies = { · 86 unmodified lines | ||
| 1671 | 1671 | rows: ControlPlaneStoreShape; |
| 1672 | 1672 | liveExecutionManager: LiveExecutionManager; |
| 1673 | 1673 | sourceStore: RuntimeSourceStore; |
| 1674 | − sourceMaterialization: RuntimeSourceMaterializationShape; | |
| 1674 | + sourceCatalogSync: RuntimeSourceCatalogSyncShape; | |
| 1675 | 1675 | resolveSecretMaterial: ResolveSecretMaterial; |
| 1676 | 1676 | storeSecretMaterial: StoreSecretMaterial; |
| 1677 | 1677 | getLocalServerBaseUrl?: () => string | undefined; |
| @@ -1721,7 +1721,7 @@ const createRuntimeSourceConnectionService = ( · 43 unmodified lines | ||
| 1721 | 1721 | ? addExecutorGoogleDiscoverySource({ |
| 1722 | 1722 | rows: input.rows, |
| 1723 | 1723 | sourceStore: input.sourceStore, |
| 1724 | − sourceMaterialization: input.sourceMaterialization, | |
| 1724 | + sourceCatalogSync: input.sourceCatalogSync, | |
| 1725 | 1725 | sourceInput, |
| 1726 | 1726 | storeSecretMaterial: input.storeSecretMaterial, |
| 1727 | 1727 | resolveSecretMaterial: input.resolveSecretMaterial, |
| @@ -1732,7 +1732,7 @@ const createRuntimeSourceConnectionService = ( · 4 unmodified lines | ||
| 1732 | 1732 | ? addExecutorHttpSource({ |
| 1733 | 1733 | rows: input.rows, |
| 1734 | 1734 | sourceStore: input.sourceStore, |
| 1735 | − sourceMaterialization: input.sourceMaterialization, | |
| 1735 | + sourceCatalogSync: input.sourceCatalogSync, | |
| 1736 | 1736 | sourceInput: sourceInput as Extract< |
| 1737 | 1737 | ExecutorAddSourceInput, |
| 1738 | 1738 | { kind: "openapi" | "graphql" } |
| @@ -1745,7 +1745,7 @@ const createRuntimeSourceConnectionService = ( · 6 unmodified lines | ||
| 1745 | 1745 | : connectMcpSourceInternal({ |
| 1746 | 1746 | rows: input.rows, |
| 1747 | 1747 | sourceStore: input.sourceStore, |
| 1748 | − sourceMaterialization: input.sourceMaterialization, | |
| 1748 | + sourceCatalogSync: input.sourceCatalogSync, | |
| 1749 | 1749 | getLocalServerBaseUrl: input.getLocalServerBaseUrl, |
| 1750 | 1750 | workspaceId: sourceInput.workspaceId, |
| 1751 | 1751 | actorAccountId: sourceInput.actorAccountId, |
| @@ -1767,7 +1767,7 @@ const createRuntimeSourceConnectionService = ( · 15 unmodified lines | ||
| 1767 | 1767 | connectMcpSourceInternal({ |
| 1768 | 1768 | rows: input.rows, |
| 1769 | 1769 | sourceStore: input.sourceStore, |
| 1770 | − sourceMaterialization: input.sourceMaterialization, | |
| 1770 | + sourceCatalogSync: input.sourceCatalogSync, | |
| 1771 | 1771 | getLocalServerBaseUrl: input.getLocalServerBaseUrl, |
| 1772 | 1772 | workspaceId: sourceInput.workspaceId, |
| 1773 | 1773 | actorAccountId: sourceInput.actorAccountId, |
| @@ -2028,7 +2028,7 @@ const createRuntimeSourceOAuthSessionService = ( · 254 unmodified lines | ||
| 2028 | 2028 | status: "error", |
| 2029 | 2029 | lastError: reason, |
| 2030 | 2030 | }); |
| 2031 | − yield* input.sourceMaterialization.sync({ | |
| 2031 | + yield* input.sourceCatalogSync.sync({ | |
| 2032 | 2032 | source: failedSource, |
| 2033 | 2033 | actorAccountId: session.actorAccountId, |
| 2034 | 2034 | }); |
| @@ -2201,7 +2201,7 @@ const createRuntimeSourceOAuthSessionService = ( · 166 unmodified lines | ||
| 2201 | 2201 | ); |
| 2202 | 2202 | } |
| 2203 | 2203 | const indexed = yield* Effect.either( |
| 2204 | − input.sourceMaterialization.sync({ | |
| 2204 | + input.sourceCatalogSync.sync({ | |
| 2205 | 2205 | source: connectedSource, |
| 2206 | 2206 | actorAccountId: session.actorAccountId, |
| 2207 | 2207 | }), |
| @@ -2272,7 +2272,7 @@ export const RuntimeSourceAuthServiceLive = (input: { · 64 unmodified lines | ||
| 2272 | 2272 | const rows = yield* ControlPlaneStore; |
| 2273 | 2273 | const liveExecutionManager = yield* LiveExecutionManagerService; |
| 2274 | 2274 | const sourceStore = yield* RuntimeSourceStoreService; |
| 2275 | − const sourceMaterialization = yield* RuntimeSourceMaterializationService; | |
| 2275 | + const sourceCatalogSync = yield* RuntimeSourceCatalogSyncService; | |
| 2276 | 2276 | const resolveSecretMaterial = yield* SecretMaterialResolverService; |
| 2277 | 2277 | const storeSecretMaterial = yield* SecretMaterialStorerService; |
| 2278 | 2278 | const runtimeLocalWorkspace = yield* getRuntimeLocalWorkspaceOption(); |
| @@ -2281,7 +2281,7 @@ export const RuntimeSourceAuthServiceLive = (input: { · 2 unmodified lines | ||
| 2281 | 2281 | rows, |
| 2282 | 2282 | liveExecutionManager, |
| 2283 | 2283 | sourceStore, |
| 2284 | − sourceMaterialization, | |
| 2284 | + sourceCatalogSync, | |
| 2285 | 2285 | resolveSecretMaterial, |
| 2286 | 2286 | storeSecretMaterial, |
| 2287 | 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 | 27 | getSourceAdapterForSource, |
| 28 | 28 | } from "./source-adapters"; |
| 29 | 29 | import { |
| 30 | − materializationFromMcpManifestEntries, | |
| 30 | + catalogSyncResultFromMcpManifestEntries, | |
| 31 | 31 | } from "./source-adapters/mcp"; |
| 32 | 32 | import { SecretMaterialResolverService } from "./secret-material-providers"; |
| 33 | 33 | |
| @@ -36,7 +36,7 @@ const shouldIndexSource = (source: Source): boolean => · 2 unmodified lines | ||
| 36 | 36 | && source.status === "connected" |
| 37 | 37 | && getSourceAdapterForSource(source).family !== "internal"; |
| 38 | 38 | |
| 39 | −type RuntimeSourceMaterializationDeps = { | |
| 39 | +type RuntimeSourceCatalogSyncDeps = { | |
| 40 | 40 | runtimeLocalWorkspace: RuntimeLocalWorkspaceState; |
| 41 | 41 | workspaceStateStore: WorkspaceStateStoreShape; |
| 42 | 42 | sourceArtifactStore: SourceArtifactStoreShape; |
| @@ -44,30 +44,30 @@ type RuntimeSourceMaterializationDeps = { · 1 unmodified line | ||
| 44 | 44 | sourceAuthMaterialService: Effect.Effect.Success<typeof RuntimeSourceAuthMaterialService>; |
| 45 | 45 | }; |
| 46 | 46 | |
| 47 | −type SourceMaterializationServices = | |
| 47 | +type SourceCatalogSyncServices = | |
| 48 | 48 | | RuntimeLocalWorkspaceService |
| 49 | 49 | | WorkspaceStateStore |
| 50 | 50 | | SourceArtifactStore |
| 51 | 51 | | RuntimeSourceAuthMaterialService |
| 52 | 52 | | SecretMaterialResolverService; |
| 53 | 53 | |
| 54 | −export type RuntimeSourceMaterializationShape = { | |
| 54 | +export type RuntimeSourceCatalogSyncShape = { | |
| 55 | 55 | sync: (input: { |
| 56 | 56 | source: Source; |
| 57 | 57 | actorAccountId?: AccountId | null; |
| 58 | 58 | }) => Effect.Effect<void, Error, never>; |
| 59 | − persistMcpRecipeMaterializationFromManifest: (input: { | |
| 59 | + persistMcpCatalogSnapshotFromManifest: (input: { | |
| 60 | 60 | source: Source; |
| 61 | − manifestEntries: Parameters<typeof materializationFromMcpManifestEntries>[0]["manifestEntries"]; | |
| 61 | + manifestEntries: Parameters<typeof catalogSyncResultFromMcpManifestEntries>[0]["manifestEntries"]; | |
| 62 | 62 | }) => Effect.Effect<void, Error, never>; |
| 63 | 63 | }; |
| 64 | 64 | |
| 65 | −export class RuntimeSourceMaterializationService extends Context.Tag( | |
| 66 | − "#runtime/RuntimeSourceMaterializationService", | |
| 67 | −)<RuntimeSourceMaterializationService, RuntimeSourceMaterializationShape>() {} | |
| 65 | +export class RuntimeSourceCatalogSyncService extends Context.Tag( | |
| 66 | + "#runtime/RuntimeSourceCatalogSyncService", | |
| 67 | +)<RuntimeSourceCatalogSyncService, RuntimeSourceCatalogSyncShape>() {} | |
| 68 | 68 | |
| 69 | −const ensureRuntimeMaterializationWorkspace = ( | |
| 70 | − deps: RuntimeSourceMaterializationDeps, | |
| 69 | +const ensureRuntimeCatalogSyncWorkspace = ( | |
| 70 | + deps: RuntimeSourceCatalogSyncDeps, | |
| 71 | 71 | workspaceId: Source["workspaceId"], |
| 72 | 72 | ) => { |
| 73 | 73 | if (deps.runtimeLocalWorkspace.installation.workspaceId !== workspaceId) { |
| @@ -81,15 +81,15 @@ const ensureRuntimeMaterializationWorkspace = ( · 7 unmodified lines | ||
| 81 | 81 | return Effect.succeed(deps.runtimeLocalWorkspace.context); |
| 82 | 82 | }; |
| 83 | 83 | |
| 84 | −const syncSourceMaterializationWithDeps = ( | |
| 85 | − deps: RuntimeSourceMaterializationDeps, | |
| 84 | +const syncSourceCatalogWithDeps = ( | |
| 85 | + deps: RuntimeSourceCatalogSyncDeps, | |
| 86 | 86 | input: { |
| 87 | 87 | source: Source; |
| 88 | 88 | actorAccountId?: AccountId | null; |
| 89 | 89 | }, |
| 90 | 90 | ): Effect.Effect<void, Error, never> => |
| 91 | 91 | Effect.gen(function* () { |
| 92 | − const workspaceContext = yield* ensureRuntimeMaterializationWorkspace( | |
| 92 | + const workspaceContext = yield* ensureRuntimeCatalogSyncWorkspace( | |
| 93 | 93 | deps, |
| 94 | 94 | input.source.workspaceId, |
| 95 | 95 | ); |
| @@ -118,7 +118,7 @@ const syncSourceMaterializationWithDeps = ( · 22 unmodified lines | ||
| 118 | 118 | } |
| 119 | 119 | |
| 120 | 120 | const adapter = getSourceAdapterForSource(input.source); |
| 121 | − const materialization = yield* adapter.materializeSource({ | |
| 121 | + const syncResult = yield* adapter.syncCatalog({ | |
| 122 | 122 | source: input.source, |
| 123 | 123 | resolveSecretMaterial: deps.resolveSecretMaterial, |
| 124 | 124 | resolveAuthMaterialForSlot: (slot) => |
| @@ -133,7 +133,7 @@ const syncSourceMaterializationWithDeps = ( · 8 unmodified lines | ||
| 133 | 133 | sourceId: input.source.id, |
| 134 | 134 | artifact: deps.sourceArtifactStore.build({ |
| 135 | 135 | source: input.source, |
| 136 | − materialization, | |
| 136 | + syncResult, | |
| 137 | 137 | }), |
| 138 | 138 | }); |
| 139 | 139 | |
| @@ -146,7 +146,7 @@ const syncSourceMaterializationWithDeps = ( · 6 unmodified lines | ||
| 146 | 146 | [input.source.id]: { |
| 147 | 147 | status: "connected", |
| 148 | 148 | lastError: null, |
| 149 | − sourceHash: materialization.sourceHash, | |
| 149 | + sourceHash: syncResult.sourceHash, | |
| 150 | 150 | createdAt: existingSourceState?.createdAt ?? input.source.createdAt, |
| 151 | 151 | updatedAt: Date.now(), |
| 152 | 152 | }, |
| @@ -158,20 +158,20 @@ const syncSourceMaterializationWithDeps = ( · 5 unmodified lines | ||
| 158 | 158 | }); |
| 159 | 159 | }); |
| 160 | 160 | |
| 161 | −const persistMcpRecipeMaterializationFromManifestWithDeps = ( | |
| 162 | − deps: RuntimeSourceMaterializationDeps, | |
| 161 | +const persistMcpCatalogSnapshotFromManifestWithDeps = ( | |
| 162 | + deps: RuntimeSourceCatalogSyncDeps, | |
| 163 | 163 | input: { |
| 164 | 164 | source: Source; |
| 165 | − manifestEntries: Parameters<typeof materializationFromMcpManifestEntries>[0]["manifestEntries"]; | |
| 165 | + manifestEntries: Parameters<typeof catalogSyncResultFromMcpManifestEntries>[0]["manifestEntries"]; | |
| 166 | 166 | }, |
| 167 | 167 | ): Effect.Effect<void, Error, never> => |
| 168 | 168 | Effect.gen(function* () { |
| 169 | − const workspaceContext = yield* ensureRuntimeMaterializationWorkspace( | |
| 169 | + const workspaceContext = yield* ensureRuntimeCatalogSyncWorkspace( | |
| 170 | 170 | deps, |
| 171 | 171 | input.source.workspaceId, |
| 172 | 172 | ); |
| 173 | − const materialization = materializationFromMcpManifestEntries({ | |
| 174 | − recipeRevisionId: "src_recipe_rev_materialization" as never, | |
| 173 | + const syncResult = catalogSyncResultFromMcpManifestEntries({ | |
| 174 | + source: input.source, | |
| 175 | 175 | endpoint: input.source.endpoint, |
| 176 | 176 | manifestEntries: input.manifestEntries, |
| 177 | 177 | }); |
| @@ -181,15 +181,15 @@ const persistMcpRecipeMaterializationFromManifestWithDeps = ( · 3 unmodified lines | ||
| 181 | 181 | sourceId: input.source.id, |
| 182 | 182 | artifact: deps.sourceArtifactStore.build({ |
| 183 | 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 | 190 | source: Source; |
| 191 | 191 | actorAccountId?: AccountId | null; |
| 192 | −}): Effect.Effect<void, Error, SourceMaterializationServices> => | |
| 192 | +}): Effect.Effect<void, Error, SourceCatalogSyncServices> => | |
| 193 | 193 | Effect.gen(function* () { |
| 194 | 194 | const runtimeLocalWorkspace = yield* RuntimeLocalWorkspaceService; |
| 195 | 195 | const workspaceStateStore = yield* WorkspaceStateStore; |
| @@ -197,7 +197,7 @@ export const syncSourceMaterialization = (input: { · 1 unmodified line | ||
| 197 | 197 | const resolveSecretMaterial = yield* SecretMaterialResolverService; |
| 198 | 198 | const sourceAuthMaterialService = yield* RuntimeSourceAuthMaterialService; |
| 199 | 199 | |
| 200 | − return yield* syncSourceMaterializationWithDeps( | |
| 200 | + return yield* syncSourceCatalogWithDeps( | |
| 201 | 201 | { |
| 202 | 202 | runtimeLocalWorkspace, |
| 203 | 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 | 216 | source: Source; |
| 217 | − manifestEntries: Parameters<typeof materializationFromMcpManifestEntries>[0]["manifestEntries"]; | |
| 218 | −}): Effect.Effect<void, Error, SourceMaterializationServices> => | |
| 217 | + manifestEntries: Parameters<typeof catalogSyncResultFromMcpManifestEntries>[0]["manifestEntries"]; | |
| 218 | +}): Effect.Effect<void, Error, SourceCatalogSyncServices> => | |
| 219 | 219 | Effect.gen(function* () { |
| 220 | 220 | const runtimeLocalWorkspace = yield* RuntimeLocalWorkspaceService; |
| 221 | 221 | const workspaceStateStore = yield* WorkspaceStateStore; |
| @@ -223,7 +223,7 @@ export const persistMcpRecipeMaterializationFromManifest = (input: { · 1 unmodified line | ||
| 223 | 223 | const resolveSecretMaterial = yield* SecretMaterialResolverService; |
| 224 | 224 | const sourceAuthMaterialService = yield* RuntimeSourceAuthMaterialService; |
| 225 | 225 | |
| 226 | − return yield* persistMcpRecipeMaterializationFromManifestWithDeps( | |
| 226 | + return yield* persistMcpCatalogSnapshotFromManifestWithDeps( | |
| 227 | 227 | { |
| 228 | 228 | runtimeLocalWorkspace, |
| 229 | 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( | |
| 239 | − RuntimeSourceMaterializationService, | |
| 238 | +export const RuntimeSourceCatalogSyncLive = Layer.effect( | |
| 239 | + RuntimeSourceCatalogSyncService, | |
| 240 | 240 | Effect.gen(function* () { |
| 241 | 241 | const runtimeLocalWorkspace = yield* RuntimeLocalWorkspaceService; |
| 242 | 242 | const workspaceStateStore = yield* WorkspaceStateStore; |
| @@ -244,7 +244,7 @@ export const RuntimeSourceMaterializationLive = Layer.effect( · 1 unmodified line | ||
| 244 | 244 | const resolveSecretMaterial = yield* SecretMaterialResolverService; |
| 245 | 245 | const sourceAuthMaterialService = yield* RuntimeSourceAuthMaterialService; |
| 246 | 246 | |
| 247 | − const deps: RuntimeSourceMaterializationDeps = { | |
| 247 | + const deps: RuntimeSourceCatalogSyncDeps = { | |
| 248 | 248 | runtimeLocalWorkspace, |
| 249 | 249 | workspaceStateStore, |
| 250 | 250 | sourceArtifactStore, |
| @@ -252,10 +252,10 @@ export const RuntimeSourceMaterializationLive = Layer.effect( · 1 unmodified line | ||
| 252 | 252 | sourceAuthMaterialService, |
| 253 | 253 | }; |
| 254 | 254 | |
| 255 | − return RuntimeSourceMaterializationService.of({ | |
| 256 | − sync: (input) => syncSourceMaterializationWithDeps(deps, input), | |
| 257 | − persistMcpRecipeMaterializationFromManifest: (input) => | |
| 258 | − persistMcpRecipeMaterializationFromManifestWithDeps(deps, input), | |
| 255 | + return RuntimeSourceCatalogSyncService.of({ | |
| 256 | + sync: (input) => syncSourceCatalogWithDeps(deps, input), | |
| 257 | + persistMcpCatalogSnapshotFromManifest: (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 | 2 | import * as Effect from "effect/Effect"; |
| 3 | 3 | |
| 4 | 4 | import { |
| @@ -13,8 +13,8 @@ import { · 8 unmodified lines | ||
| 13 | 13 | createSourceFromPayload, |
| 14 | 14 | projectSourceFromStorage, |
| 15 | 15 | splitSourceForStorage, |
| 16 | − stableSourceRecipeId, | |
| 17 | − stableSourceRecipeRevisionId, | |
| 16 | + stableSourceCatalogId, | |
| 17 | + stableSourceCatalogRevisionId, | |
| 18 | 18 | updateSourceFromPayload, |
| 19 | 19 | } from "./source-definitions"; |
| 20 | 20 | import { namespaceFromSourceName } from "./source-names"; |
| @@ -63,7 +63,7 @@ const makeSource = (overrides: Partial<Source> = {}): Source => ({ · 42 unmodified lines | ||
| 63 | 63 | }); |
| 64 | 64 | |
| 65 | 65 | describe("source-definitions", () => { |
| 66 | − describe("stable recipe ids", () => { | |
| 66 | + describe("stable catalog ids", () => { | |
| 67 | 67 | it("is deterministic across calls and ignores source name/workspace", () => { |
| 68 | 68 | const source = makeSource(); |
| 69 | 69 | const renamed = makeSource({ |
| @@ -73,26 +73,26 @@ describe("source-definitions", () => { · 3 unmodified lines | ||
| 73 | 73 | workspaceId: WorkspaceIdSchema.make("ws_source_definitions_other"), |
| 74 | 74 | }); |
| 75 | 75 | |
| 76 | − expect(stableSourceRecipeId(source)).toBe(stableSourceRecipeId(source)); | |
| 77 | − expect(stableSourceRecipeRevisionId(source)).toBe(stableSourceRecipeRevisionId(source)); | |
| 78 | − expect(stableSourceRecipeId(renamed)).toBe(stableSourceRecipeId(source)); | |
| 79 | − expect(stableSourceRecipeRevisionId(renamed)).toBe(stableSourceRecipeRevisionId(source)); | |
| 80 | − expect(stableSourceRecipeId(differentWorkspace)).toBe(stableSourceRecipeId(source)); | |
| 81 | − expect(stableSourceRecipeRevisionId(differentWorkspace)).toBe( | |
| 82 | − stableSourceRecipeRevisionId(source), | |
| 76 | + expect(stableSourceCatalogId(source)).toBe(stableSourceCatalogId(source)); | |
| 77 | + expect(stableSourceCatalogRevisionId(source)).toBe(stableSourceCatalogRevisionId(source)); | |
| 78 | + expect(stableSourceCatalogId(renamed)).toBe(stableSourceCatalogId(source)); | |
| 79 | + expect(stableSourceCatalogRevisionId(renamed)).toBe(stableSourceCatalogRevisionId(source)); | |
| 80 | + expect(stableSourceCatalogId(differentWorkspace)).toBe(stableSourceCatalogId(source)); | |
| 81 | + expect(stableSourceCatalogRevisionId(differentWorkspace)).toBe( | |
| 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 | 87 | const source = makeSource(); |
| 88 | 88 | const changedEndpoint = makeSource({ |
| 89 | 89 | endpoint: "https://example.com", |
| 90 | 90 | binding: openApiBinding("https://example.com/openapi.json"), |
| 91 | 91 | }); |
| 92 | 92 | |
| 93 | − expect(stableSourceRecipeId(changedEndpoint)).not.toBe(stableSourceRecipeId(source)); | |
| 94 | − expect(stableSourceRecipeRevisionId(changedEndpoint)).not.toBe( | |
| 95 | − stableSourceRecipeRevisionId(source), | |
| 93 | + expect(stableSourceCatalogId(changedEndpoint)).not.toBe(stableSourceCatalogId(source)); | |
| 94 | + expect(stableSourceCatalogRevisionId(changedEndpoint)).not.toBe( | |
| 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); | |
| 362 | − const recipeRevisionId = stableSourceRecipeRevisionId(source); | |
| 361 | + const catalogId = stableSourceCatalogId(source); | |
| 362 | + const catalogRevisionId = stableSourceCatalogRevisionId(source); | |
| 363 | 363 | const existingCredentialId = CredentialIdSchema.make("cred_existing"); |
| 364 | 364 | |
| 365 | 365 | const { sourceRecord, runtimeAuthArtifact } = splitSourceForStorage({ |
| 366 | 366 | source, |
| 367 | − recipeId, | |
| 368 | − recipeRevisionId, | |
| 367 | + catalogId, | |
| 368 | + catalogRevisionId, | |
| 369 | 369 | existingRuntimeAuthArtifactId: existingCredentialId, |
| 370 | 370 | }); |
| 371 | 371 | |
| @@ -426,8 +426,8 @@ describe("source-definitions", () => { · 54 unmodified lines | ||
| 426 | 426 | for (const source of [withRefresh, withoutRefresh]) { |
| 427 | 427 | const { sourceRecord, runtimeAuthArtifact } = splitSourceForStorage({ |
| 428 | 428 | source, |
| 429 | − recipeId: stableSourceRecipeId(source), | |
| 430 | − recipeRevisionId: stableSourceRecipeRevisionId(source), | |
| 429 | + catalogId: stableSourceCatalogId(source), | |
| 430 | + catalogRevisionId: stableSourceCatalogRevisionId(source), | |
| 431 | 431 | }); |
| 432 | 432 | const projected = await Effect.runPromise(projectSourceFromStorage({ |
| 433 | 433 | sourceRecord, |
| @@ -445,8 +445,8 @@ describe("source-definitions", () => { · 11 unmodified lines | ||
| 445 | 445 | }); |
| 446 | 446 | const { sourceRecord, runtimeAuthArtifact } = splitSourceForStorage({ |
| 447 | 447 | source, |
| 448 | − recipeId: stableSourceRecipeId(source), | |
| 449 | − recipeRevisionId: stableSourceRecipeRevisionId(source), | |
| 448 | + catalogId: stableSourceCatalogId(source), | |
| 449 | + catalogRevisionId: stableSourceCatalogRevisionId(source), | |
| 450 | 450 | }); |
| 451 | 451 | |
| 452 | 452 | expect(runtimeAuthArtifact).toBeNull(); |
| @@ -11,19 +11,19 @@ import type { · 10 unmodified lines | ||
| 11 | 11 | Source, |
| 12 | 12 | SourceAuth, |
| 13 | 13 | SourceImportAuthPolicy, |
| 14 | − SourceRecipeAdapterKey, | |
| 15 | − SourceRecipeId, | |
| 16 | − SourceRecipeKind, | |
| 17 | − SourceRecipeRevisionId, | |
| 14 | + SourceCatalogAdapterKey, | |
| 15 | + SourceCatalogId, | |
| 16 | + SourceCatalogKind, | |
| 17 | + SourceCatalogRevisionId, | |
| 18 | 18 | StoredSourceRecord, |
| 19 | − StoredSourceRecipeRecord, | |
| 20 | − StoredSourceRecipeRevisionRecord, | |
| 19 | + StoredSourceCatalogRecord, | |
| 20 | + StoredSourceCatalogRevisionRecord, | |
| 21 | 21 | WorkspaceId, |
| 22 | 22 | } from "#schema"; |
| 23 | 23 | import { |
| 24 | 24 | AuthArtifactIdSchema, |
| 25 | − SourceRecipeIdSchema, | |
| 26 | − SourceRecipeRevisionIdSchema, | |
| 25 | + SourceCatalogIdSchema, | |
| 26 | + SourceCatalogRevisionIdSchema, | |
| 27 | 27 | } from "#schema"; |
| 28 | 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 | 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 | 48 | getSourceAdapterForSource(source).sourceConfigFromSource(source); |
| 49 | 49 | |
| 50 | −const sourceRecipeKindFromSource = (source: Source): SourceRecipeKind => { | |
| 50 | +const sourceCatalogKindFromSource = (source: Source): SourceCatalogKind => { | |
| 51 | 51 | const adapter = getSourceAdapterForSource(source); |
| 52 | 52 | return adapter.family; |
| 53 | 53 | }; |
| 54 | 54 | |
| 55 | −const sourceRecipeAdapterKeyFromSource = (source: Source): SourceRecipeAdapterKey => { | |
| 55 | +const sourceCatalogAdapterKeyFromSource = (source: Source): SourceCatalogAdapterKey => { | |
| 56 | 56 | return getSourceAdapterForSource(source).key; |
| 57 | 57 | }; |
| 58 | 58 | |
| 59 | −const sourceRecipeProviderKeyFromSource = (source: Source): string => { | |
| 59 | +const sourceCatalogProviderKeyFromSource = (source: Source): string => { | |
| 60 | 60 | return getSourceAdapterForSource(source).providerKey; |
| 61 | 61 | }; |
| 62 | 62 | |
| 63 | 63 | const stableHash = (value: string): string => |
| 64 | 64 | sha256Hex(value).slice(0, 24); |
| 65 | 65 | |
| 66 | −const sourceRecipeSignature = (source: Source): string => | |
| 66 | +const sourceCatalogSignature = (source: Source): string => | |
| 67 | 67 | JSON.stringify({ |
| 68 | − recipeKind: sourceRecipeKindFromSource(source), | |
| 69 | − adapterKey: sourceRecipeAdapterKeyFromSource(source), | |
| 70 | − providerKey: sourceRecipeProviderKeyFromSource(source), | |
| 68 | + catalogKind: sourceCatalogKindFromSource(source), | |
| 69 | + adapterKey: sourceCatalogAdapterKeyFromSource(source), | |
| 70 | + providerKey: sourceCatalogProviderKeyFromSource(source), | |
| 71 | 71 | sourceConfig: sourceConfigFromSource(source), |
| 72 | 72 | }); |
| 73 | 73 | |
| 74 | 74 | export const sourceConfigSignature = (source: Source): string => |
| 75 | 75 | JSON.stringify(sourceConfigFromSource(source)); |
| 76 | 76 | |
| 77 | −export const stableSourceRecipeId = (source: Source): SourceRecipeId => | |
| 78 | − SourceRecipeIdSchema.make(`src_recipe_${stableHash(sourceRecipeSignature(source))}`); | |
| 77 | +export const stableSourceCatalogId = (source: Source): SourceCatalogId => | |
| 78 | + SourceCatalogIdSchema.make(`src_catalog_${stableHash(sourceCatalogSignature(source))}`); | |
| 79 | 79 | |
| 80 | −export const stableSourceRecipeRevisionId = ( | |
| 80 | +export const stableSourceCatalogRevisionId = ( | |
| 81 | 81 | source: Source, |
| 82 | −): SourceRecipeRevisionId => | |
| 83 | − SourceRecipeRevisionIdSchema.make(`src_recipe_rev_${stableHash(sourceConfigSignature(source))}`); | |
| 82 | +): SourceCatalogRevisionId => | |
| 83 | + SourceCatalogRevisionIdSchema.make(`src_catalog_rev_${stableHash(sourceConfigSignature(source))}`); | |
| 84 | 84 | |
| 85 | 85 | const normalizeAuth = ( |
| 86 | 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 | 302 | source: Source; |
| 303 | − recipeId?: SourceRecipeId | null; | |
| 304 | − latestRevisionId: SourceRecipeRevisionId; | |
| 305 | −}): StoredSourceRecipeRecord => ({ | |
| 306 | − id: input.recipeId ?? stableSourceRecipeId(input.source), | |
| 307 | − kind: sourceRecipeKindFromSource(input.source), | |
| 308 | − adapterKey: sourceRecipeAdapterKeyFromSource(input.source), | |
| 309 | − providerKey: sourceRecipeProviderKeyFromSource(input.source), | |
| 303 | + catalogId?: SourceCatalogId | null; | |
| 304 | + latestRevisionId: SourceCatalogRevisionId; | |
| 305 | +}): StoredSourceCatalogRecord => ({ | |
| 306 | + id: input.catalogId ?? stableSourceCatalogId(input.source), | |
| 307 | + kind: sourceCatalogKindFromSource(input.source), | |
| 308 | + adapterKey: sourceCatalogAdapterKeyFromSource(input.source), | |
| 309 | + providerKey: sourceCatalogProviderKeyFromSource(input.source), | |
| 310 | 310 | name: input.source.name, |
| 311 | 311 | summary: null, |
| 312 | 312 | visibility: "workspace", |
| @@ -315,32 +315,32 @@ export const createSourceRecipeRecord = (input: { · 2 unmodified lines | ||
| 315 | 315 | updatedAt: input.source.updatedAt, |
| 316 | 316 | }); |
| 317 | 317 | |
| 318 | −export const createSourceRecipeRevisionRecord = (input: { | |
| 318 | +export const createSourceCatalogRevisionRecord = (input: { | |
| 319 | 319 | source: Source; |
| 320 | − recipeId: SourceRecipeId; | |
| 321 | − recipeRevisionId?: SourceRecipeRevisionId | null; | |
| 320 | + catalogId: SourceCatalogId; | |
| 321 | + catalogRevisionId?: SourceCatalogRevisionId | null; | |
| 322 | 322 | revisionNumber: number; |
| 323 | − manifestJson?: string | null; | |
| 324 | − manifestHash?: string | null; | |
| 325 | − materializationHash?: string | null; | |
| 326 | −}): StoredSourceRecipeRevisionRecord => ({ | |
| 323 | + importMetadataJson?: string | null; | |
| 324 | + importMetadataHash?: string | null; | |
| 325 | + snapshotHash?: string | null; | |
| 326 | +}): StoredSourceCatalogRevisionRecord => ({ | |
| 327 | 327 | id: |
| 328 | − input.recipeRevisionId | |
| 329 | − ?? stableSourceRecipeRevisionId(input.source), | |
| 330 | − recipeId: input.recipeId, | |
| 328 | + input.catalogRevisionId | |
| 329 | + ?? stableSourceCatalogRevisionId(input.source), | |
| 330 | + catalogId: input.catalogId, | |
| 331 | 331 | revisionNumber: input.revisionNumber, |
| 332 | 332 | sourceConfigJson: sourceConfigSignature(input.source), |
| 333 | − manifestJson: input.manifestJson ?? null, | |
| 334 | − manifestHash: input.manifestHash ?? null, | |
| 335 | − materializationHash: input.materializationHash ?? null, | |
| 333 | + importMetadataJson: input.importMetadataJson ?? null, | |
| 334 | + importMetadataHash: input.importMetadataHash ?? null, | |
| 335 | + snapshotHash: input.snapshotHash ?? null, | |
| 336 | 336 | createdAt: input.source.createdAt, |
| 337 | 337 | updatedAt: input.source.updatedAt, |
| 338 | 338 | }); |
| 339 | 339 | |
| 340 | 340 | export const splitSourceForStorage = (input: { |
| 341 | 341 | source: Source; |
| 342 | − recipeId: SourceRecipeId; | |
| 343 | − recipeRevisionId: SourceRecipeRevisionId; | |
| 342 | + catalogId: SourceCatalogId; | |
| 343 | + catalogRevisionId: SourceCatalogRevisionId; | |
| 344 | 344 | actorAccountId?: AccountId | null; |
| 345 | 345 | existingRuntimeAuthArtifactId?: AuthArtifact["id"] | null; |
| 346 | 346 | existingImportAuthArtifactId?: AuthArtifact["id"] | null; |
| @@ -352,8 +352,8 @@ export const splitSourceForStorage = (input: { · 5 unmodified lines | ||
| 352 | 352 | const sourceRecord: StoredSourceRecord = { |
| 353 | 353 | id: input.source.id, |
| 354 | 354 | workspaceId: input.source.workspaceId, |
| 355 | − recipeId: input.recipeId, | |
| 356 | − recipeRevisionId: input.recipeRevisionId, | |
| 355 | + catalogId: input.catalogId, | |
| 356 | + catalogRevisionId: input.catalogRevisionId, | |
| 357 | 357 | name: input.source.name, |
| 358 | 358 | kind: input.source.kind, |
| 359 | 359 | endpoint: input.source.endpoint, |
| @@ -4,7 +4,7 @@ import { · 3 unmodified lines | ||
| 4 | 4 | type ServerResponse, |
| 5 | 5 | } from "node:http"; |
| 6 | 6 | |
| 7 | −import { describe, expect, it } from "vitest"; | |
| 7 | +import { describe, expect, it } from "@effect/vitest"; | |
| 8 | 8 | import * as Effect from "effect/Effect"; |
| 9 | 9 | |
| 10 | 10 | import { discoverSource } from "./source-discovery"; |
| @@ -1,37 +1,27 @@ | ||
| 1 | 1 | import type { |
| 2 | − Source, | |
| 3 | 2 | SourceId, |
| 4 | 3 | SourceInspection, |
| 5 | 4 | SourceInspectionDiscoverPayload, |
| 6 | 5 | SourceInspectionDiscoverResult, |
| 7 | 6 | SourceInspectionDiscoverResultItem, |
| 8 | − SourceInspectionSchemaBundle, | |
| 9 | − SourceInspectionToolListItem, | |
| 10 | 7 | SourceInspectionToolDetail, |
| 8 | + SourceInspectionToolListItem, | |
| 11 | 9 | SourceInspectionToolSummary, |
| 12 | − StoredSourceRecipeOperationRecord, | |
| 13 | 10 | WorkspaceId, |
| 14 | 11 | } from "#schema"; |
| 15 | −import { SourceRecipeSchemaBundleIdSchema } from "#schema"; | |
| 16 | 12 | import { |
| 17 | 13 | ControlPlaneNotFoundError, |
| 18 | 14 | ControlPlaneStorageError, |
| 19 | 15 | } from "../api/errors"; |
| 20 | 16 | import * as Effect from "effect/Effect"; |
| 21 | −import * as Option from "effect/Option"; | |
| 22 | 17 | |
| 23 | 18 | import { operationErrors } from "./operation-errors"; |
| 24 | 19 | import { formatWithPrettier } from "./prettier-format"; |
| 25 | 20 | import { |
| 26 | − getSourceAdapterForOperation, | |
| 27 | −} from "./source-adapters"; | |
| 28 | −import { | |
| 29 | − RuntimeSourceRecipeStoreService, | |
| 30 | − recipeToolMetadata, | |
| 31 | − recipeToolPath, | |
| 32 | −} from "./source-recipes-runtime"; | |
| 33 | −import type { SourceAdapterPersistedOperationMetadata } from "./source-adapters/types"; | |
| 34 | −import { namespaceFromSourceName } from "./source-names"; | |
| 21 | + expandCatalogTools, | |
| 22 | + loadSourceWithCatalog, | |
| 23 | + type LoadedSourceCatalogTool, | |
| 24 | +} from "./source-catalog-runtime"; | |
| 35 | 25 | |
| 36 | 26 | const sourceInspectOps = { |
| 37 | 27 | bundle: operationErrors("sources.inspect.bundle"), |
| @@ -46,21 +36,6 @@ const tokenize = (value: string): Array<string> => · 8 unmodified lines | ||
| 46 | 36 | .split(/[^a-z0-9]+/) |
| 47 | 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 | 39 | const formatOptionalJson = (value: string | null) => |
| 65 | 40 | value === null |
| 66 | 41 | ? Effect.succeed<string | null>(null) |
| @@ -71,198 +46,163 @@ const formatOptionalTypeScript = (value: string | undefined) => · 4 unmodified lines | ||
| 71 | 46 | ? Effect.succeed<string | undefined>(undefined) |
| 72 | 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 | 49 | const formatInspectionToolDetail = (detail: SourceInspectionToolDetail) => |
| 87 | 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 | 61 | const detailFields = yield* Effect.all({ |
| 90 | 62 | definitionJson: formatOptionalJson(detail.definitionJson), |
| 91 | 63 | documentationJson: formatOptionalJson(detail.documentationJson), |
| 92 | − providerDataJson: formatOptionalJson(detail.providerDataJson), | |
| 93 | − inputSchemaJson: formatOptionalJson(detail.inputSchemaJson), | |
| 94 | − outputSchemaJson: formatOptionalJson(detail.outputSchemaJson), | |
| 95 | − exampleInputJson: formatOptionalJson(detail.exampleInputJson), | |
| 96 | − exampleOutputJson: formatOptionalJson(detail.exampleOutputJson), | |
| 64 | + nativeJson: formatOptionalJson(detail.nativeJson), | |
| 65 | + callSchemaJson: formatOptionalJson(detail.callSchemaJson), | |
| 66 | + resultSchemaJson: formatOptionalJson(detail.resultSchemaJson), | |
| 67 | + exampleCallJson: formatOptionalJson(detail.exampleCallJson), | |
| 68 | + exampleResultJson: formatOptionalJson(detail.exampleResultJson), | |
| 97 | 69 | }); |
| 98 | 70 | |
| 99 | 71 | return { |
| 100 | 72 | ...detail, |
| 101 | − ...detailFields, | |
| 102 | 73 | summary, |
| 74 | + ...detailFields, | |
| 103 | 75 | } satisfies SourceInspectionToolDetail; |
| 104 | 76 | }); |
| 105 | 77 | |
| 106 | −const loadSourceRecipeRecord = (input: { | |
| 107 | − workspaceId: WorkspaceId; | |
| 108 | − sourceId: SourceId; | |
| 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 | − | |
| 78 | +const executableDetails = (tool: LoadedSourceCatalogTool) => { | |
| 79 | + switch (tool.executable.protocol) { | |
| 80 | + case "http": | |
| 129 | 81 | return { |
| 130 | − source: recipe.source, | |
| 131 | − operations: recipe.operations, | |
| 132 | − schemaBundleId: recipe.schemaBundles.find((bundle) => bundle.bundleKind === "json_schema_ref_map")?.id | |
| 133 | − ?? recipe.schemaBundles[0]?.id | |
| 134 | − ?? null, | |
| 82 | + method: tool.executable.method, | |
| 83 | + pathTemplate: tool.executable.pathTemplate, | |
| 84 | + rawToolId: tool.path.split(".").at(-1) ?? null, | |
| 85 | + operationId: null, | |
| 86 | + group: null, | |
| 87 | + leaf: tool.path.split(".").at(-1) ?? null, | |
| 88 | + tags: tool.capability.surface.tags ?? [], | |
| 135 | 89 | }; |
| 136 | − }), | |
| 137 | − ); | |
| 90 | + case "graphql": | |
| 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: { | |
| 140 | − source: Source; | |
| 141 | − operation: StoredSourceRecipeOperationRecord; | |
| 142 | − metadata: SourceAdapterPersistedOperationMetadata; | |
| 143 | −}): SourceInspectionToolListItem => ({ | |
| 144 | − path: recipeToolPath({ | |
| 145 | − source: input.source, | |
| 146 | − operation: input.operation, | |
| 147 | − }), | |
| 148 | − method: input.metadata.method, | |
| 113 | +const inspectionToolListItemFromTool = (tool: LoadedSourceCatalogTool): SourceInspectionToolListItem => ({ | |
| 114 | + path: tool.path, | |
| 115 | + method: | |
| 116 | + tool.executable.protocol === "http" | |
| 117 | + ? tool.executable.method | |
| 118 | + : tool.executable.protocol === "graphql" | |
| 119 | + ? tool.executable.operationType | |
| 120 | + : null, | |
| 149 | 121 | }); |
| 150 | 122 | |
| 151 | −const persistedToolSummaryFromRecipeOperation = (input: { | |
| 152 | − source: Source; | |
| 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; | |
| 123 | +const persistedToolSummaryFromTool = (tool: LoadedSourceCatalogTool): SourceInspectionToolSummary => { | |
| 124 | + const details = executableDetails(tool); | |
| 166 | 125 | |
| 167 | 126 | return { |
| 168 | − path, | |
| 169 | − sourceKey: input.source.id, | |
| 170 | − ...(input.operation.title ? { title: input.operation.title } : {}), | |
| 171 | − ...(input.operation.description ? { description: input.operation.description } : {}), | |
| 172 | − providerKind: input.operation.providerKind, | |
| 173 | − toolId: input.operation.toolId, | |
| 174 | − rawToolId: input.metadata.rawToolId, | |
| 175 | − operationId: input.metadata.operationId, | |
| 176 | − group: input.metadata.group, | |
| 177 | − leaf: input.metadata.leaf, | |
| 178 | − tags: [...input.metadata.tags], | |
| 179 | − method: input.metadata.method, | |
| 180 | − pathTemplate: input.metadata.pathTemplate, | |
| 181 | − ...(descriptor?.inputType ? { inputType: descriptor.inputType } : {}), | |
| 182 | − ...(descriptor?.outputType ? { outputType: descriptor.outputType } : {}), | |
| 127 | + path: tool.path, | |
| 128 | + sourceKey: tool.source.id, | |
| 129 | + ...(tool.capability.surface.title ? { title: tool.capability.surface.title } : {}), | |
| 130 | + ...(tool.capability.surface.summary || tool.capability.surface.description | |
| 131 | + ? { description: tool.capability.surface.summary ?? tool.capability.surface.description! } | |
| 132 | + : {}), | |
| 133 | + protocol: tool.executable.protocol, | |
| 134 | + toolId: tool.path.split(".").at(-1) ?? tool.path, | |
| 135 | + rawToolId: details.rawToolId, | |
| 136 | + operationId: details.operationId, | |
| 137 | + group: details.group, | |
| 138 | + leaf: details.leaf, | |
| 139 | + tags: [...details.tags], | |
| 140 | + method: details.method, | |
| 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: { | |
| 187 | − source: Source; | |
| 188 | − operation: StoredSourceRecipeOperationRecord; | |
| 189 | − metadata: SourceAdapterPersistedOperationMetadata; | |
| 190 | − schemaBundleId: string | null; | |
| 191 | −}): SourceInspectionToolDetail => { | |
| 192 | − const summary = persistedToolSummaryFromRecipeOperation({ | |
| 193 | − source: input.source, | |
| 194 | − operation: input.operation, | |
| 195 | − metadata: input.metadata, | |
| 196 | − includeTypes: true, | |
| 197 | − }); | |
| 198 | − | |
| 199 | − return { | |
| 200 | − summary, | |
| 201 | − definitionJson: null, | |
| 202 | − documentationJson: null, | |
| 203 | − providerDataJson: input.operation.providerDataJson, | |
| 204 | − inputSchemaJson: input.operation.inputSchemaJson, | |
| 205 | − outputSchemaJson: input.operation.outputSchemaJson, | |
| 206 | − schemaBundleId: input.schemaBundleId, | |
| 207 | − exampleInputJson: null, | |
| 208 | − exampleOutputJson: null, | |
| 209 | − } satisfies SourceInspectionToolDetail; | |
| 210 | −}; | |
| 147 | +const inspectionToolDetailFromTool = (tool: LoadedSourceCatalogTool): SourceInspectionToolDetail => ({ | |
| 148 | + summary: persistedToolSummaryFromTool(tool), | |
| 149 | + definitionJson: JSON.stringify({ | |
| 150 | + capability: tool.capability, | |
| 151 | + executable: tool.executable, | |
| 152 | + }), | |
| 153 | + documentationJson: JSON.stringify({ | |
| 154 | + capabilityDocs: { | |
| 155 | + summary: tool.capability.surface.summary, | |
| 156 | + description: tool.capability.surface.description, | |
| 157 | + }, | |
| 158 | + }), | |
| 159 | + nativeJson: tool.executable.native?.[0]?.value | |
| 160 | + ? JSON.stringify(tool.executable.native[0]!.value) | |
| 161 | + : null, | |
| 162 | + callSchemaJson: tool.descriptor.inputSchema | |
| 163 | + ? JSON.stringify(tool.descriptor.inputSchema) | |
| 164 | + : null, | |
| 165 | + resultSchemaJson: tool.descriptor.outputSchema | |
| 166 | + ? JSON.stringify(tool.descriptor.outputSchema) | |
| 167 | + : null, | |
| 168 | + exampleCallJson: null, | |
| 169 | + exampleResultJson: null, | |
| 170 | +}); | |
| 211 | 171 | |
| 212 | 172 | const resolveSourceInspection = (input: { |
| 213 | 173 | workspaceId: WorkspaceId; |
| 214 | 174 | sourceId: SourceId; |
| 215 | 175 | }) => |
| 216 | 176 | Effect.gen(function* () { |
| 217 | − const { source, operations, schemaBundleId } = yield* loadSourceRecipeRecord(input); | |
| 218 | − const namespace = source.namespace ?? namespaceFromSourceName(source.name); | |
| 177 | + const catalogEntry = yield* loadSourceWithCatalog({ | |
| 178 | + workspaceId: input.workspaceId, | |
| 179 | + sourceId: input.sourceId, | |
| 180 | + }); | |
| 181 | + const tools = yield* expandCatalogTools({ | |
| 182 | + catalogs: [catalogEntry], | |
| 183 | + includeSchemas: true, | |
| 184 | + }); | |
| 219 | 185 | |
| 220 | 186 | return { |
| 221 | − source, | |
| 222 | − namespace, | |
| 223 | − pipelineKind: "persisted", | |
| 224 | − schemaBundleId, | |
| 225 | − tools: yield* Effect.forEach(operations, (operation) => | |
| 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; | |
| 187 | + source: catalogEntry.source, | |
| 188 | + namespace: catalogEntry.source.namespace ?? "", | |
| 189 | + pipelineKind: "ir" as const, | |
| 190 | + tools, | |
| 191 | + }; | |
| 251 | 192 | }); |
| 252 | 193 | |
| 253 | 194 | const scoreTool = (input: { |
| 254 | 195 | queryTokens: ReadonlyArray<string>; |
| 255 | − tool: InspectionToolRecord; | |
| 196 | + tool: LoadedSourceCatalogTool; | |
| 256 | 197 | }): SourceInspectionDiscoverResultItem | null => { |
| 257 | 198 | let score = 0; |
| 258 | 199 | const reasons: Array<string> = []; |
| 259 | − const pathTokens = tokenize(input.tool.listItem.path); | |
| 260 | − const titleTokens = tokenize(input.tool.operation.title ?? ""); | |
| 261 | − const descriptionTokens = tokenize(input.tool.operation.description ?? ""); | |
| 262 | − const tagTokens = input.tool.metadata.tags.flatMap(tokenize); | |
| 263 | − const methodPathTokens = tokenize( | |
| 264 | − `${input.tool.listItem.method ?? ""} ${input.tool.metadata.pathTemplate ?? ""}`, | |
| 265 | − ); | |
| 200 | + const summary = persistedToolSummaryFromTool(input.tool); | |
| 201 | + const pathTokens = tokenize(summary.path); | |
| 202 | + const titleTokens = tokenize(summary.title ?? ""); | |
| 203 | + const descriptionTokens = tokenize(summary.description ?? ""); | |
| 204 | + const tagTokens = summary.tags.flatMap(tokenize); | |
| 205 | + const methodPathTokens = tokenize(`${summary.method ?? ""} ${summary.pathTemplate ?? ""}`); | |
| 266 | 206 | |
| 267 | 207 | for (const token of input.queryTokens) { |
| 268 | 208 | if (pathTokens.includes(token)) { |
| @@ -296,13 +236,13 @@ const scoreTool = (input: { · 27 unmodified lines | ||
| 296 | 236 | } |
| 297 | 237 | |
| 298 | 238 | return { |
| 299 | − path: input.tool.listItem.path, | |
| 239 | + path: input.tool.path, | |
| 300 | 240 | score, |
| 301 | − ...(input.tool.operation.description | |
| 302 | − ? { description: input.tool.operation.description } | |
| 303 | − : {}), | |
| 241 | + ...(summary.description ? { description: summary.description } : {}), | |
| 242 | + ...(summary.inputType ? { inputType: summary.inputType } : {}), | |
| 243 | + ...(summary.outputType ? { outputType: summary.outputType } : {}), | |
| 304 | 244 | reasons, |
| 305 | − } satisfies SourceInspectionDiscoverResultItem; | |
| 245 | + }; | |
| 306 | 246 | }; |
| 307 | 247 | |
| 308 | 248 | const mapInspectionError = ( |
| @@ -316,6 +256,7 @@ const mapInspectionError = ( · 7 unmodified lines | ||
| 316 | 256 | if (cause instanceof ControlPlaneNotFoundError) { |
| 317 | 257 | return cause; |
| 318 | 258 | } |
| 259 | + | |
| 319 | 260 | if (cause instanceof ControlPlaneStorageError) { |
| 320 | 261 | return cause; |
| 321 | 262 | } |
| @@ -335,7 +276,7 @@ export const getSourceInspection = (input: { · 13 unmodified lines | ||
| 335 | 276 | namespace: inspection.namespace, |
| 336 | 277 | pipelineKind: inspection.pipelineKind, |
| 337 | 278 | toolCount: inspection.tools.length, |
| 338 | − tools: inspection.tools.map((tool) => tool.listItem), | |
| 279 | + tools: inspection.tools.map((tool) => inspectionToolListItemFromTool(tool)), | |
| 339 | 280 | } satisfies SourceInspection; |
| 340 | 281 | }).pipe( |
| 341 | 282 | Effect.mapError((cause) => |
| @@ -356,7 +297,7 @@ export const getSourceInspectionToolDetail = (input: { · 14 unmodified lines | ||
| 356 | 297 | workspaceId: input.workspaceId, |
| 357 | 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 | 302 | if (!tool) { |
| 362 | 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({ | |
| 371 | − source: inspection.source, | |
| 372 | − operation: tool.operation, | |
| 373 | − metadata: tool.metadata, | |
| 374 | − schemaBundleId: inspection.schemaBundleId, | |
| 375 | − })); | |
| 311 | + return yield* formatInspectionToolDetail(inspectionToolDetailFromTool(tool)); | |
| 376 | 312 | }).pipe( |
| 377 | 313 | Effect.mapError((cause) => |
| 378 | 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 | 321 | export const discoverSourceInspectionTools = (input: { |
| 437 | 322 | workspaceId: WorkspaceId; |
| 438 | 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 | 14 | import * as Layer from "effect/Layer"; |
| 15 | 15 | |
| 16 | 16 | import { |
| 17 | − stableSourceRecipeId, | |
| 18 | − stableSourceRecipeRevisionId, | |
| 17 | + stableSourceCatalogId, | |
| 18 | + stableSourceCatalogRevisionId, | |
| 19 | 19 | splitSourceForStorage, |
| 20 | 20 | } from "./source-definitions"; |
| 21 | 21 | import { |
| @@ -407,8 +407,8 @@ const buildLocalSourceRecord = (input: { · 385 unmodified lines | ||
| 407 | 407 | const sourceRecord = { |
| 408 | 408 | id: baseSource.id, |
| 409 | 409 | workspaceId: baseSource.workspaceId, |
| 410 | − recipeId: artifact?.recipeId ?? stableSourceRecipeId(baseSource), | |
| 411 | − recipeRevisionId: artifact?.revision.id ?? stableSourceRecipeRevisionId(baseSource), | |
| 410 | + catalogId: artifact?.catalogId ?? stableSourceCatalogId(baseSource), | |
| 411 | + catalogRevisionId: artifact?.revision.id ?? stableSourceCatalogRevisionId(baseSource), | |
| 412 | 412 | name: baseSource.name, |
| 413 | 413 | kind: baseSource.kind, |
| 414 | 414 | endpoint: baseSource.endpoint, |
| @@ -855,8 +855,8 @@ const persistSourceWithDeps = ( · 440 unmodified lines | ||
| 855 | 855 | |
| 856 | 856 | const { runtimeAuthArtifact, importAuthArtifact } = splitSourceForStorage({ |
| 857 | 857 | source: nextSource, |
| 858 | − recipeId: stableSourceRecipeId(nextSource), | |
| 859 | − recipeRevisionId: stableSourceRecipeRevisionId(nextSource), | |
| 858 | + catalogId: stableSourceCatalogId(nextSource), | |
| 859 | + catalogRevisionId: stableSourceCatalogRevisionId(nextSource), | |
| 860 | 860 | actorAccountId: options.actorAccountId, |
| 861 | 861 | existingRuntimeAuthArtifactId: existingRuntimeAuthArtifact?.id ?? null, |
| 862 | 862 | existingImportAuthArtifactId: existingImportAuthArtifact?.id ?? null, |
| @@ -25,7 +25,7 @@ import { · 24 unmodified lines | ||
| 25 | 25 | operationErrors, |
| 26 | 26 | } from "./operation-errors"; |
| 27 | 27 | import { ControlPlaneStore, type ControlPlaneStoreShape } from "./store"; |
| 28 | −import { RuntimeSourceMaterializationService } from "./source-materialization"; | |
| 28 | +import { RuntimeSourceCatalogSyncService } from "./source-catalog-sync"; | |
| 29 | 29 | import { |
| 30 | 30 | RuntimeSourceStoreService, |
| 31 | 31 | } from "./source-store"; |
| @@ -51,7 +51,7 @@ const syncArtifactsForSource = (input: { · 19 unmodified lines | ||
| 51 | 51 | | typeof sourceOps.update; |
| 52 | 52 | }) => |
| 53 | 53 | Effect.gen(function* () { |
| 54 | − const materializationService = yield* RuntimeSourceMaterializationService; | |
| 54 | + const catalogSyncService = yield* RuntimeSourceCatalogSyncService; | |
| 55 | 55 | |
| 56 | 56 | // For HTTP-backed source kinds that can validate themselves from a remote |
| 57 | 57 | // document, automatically attempt to probe and connect. This mirrors the |
| @@ -63,7 +63,7 @@ const syncArtifactsForSource = (input: { · 5 unmodified lines | ||
| 63 | 63 | : input.source; |
| 64 | 64 | |
| 65 | 65 | const synced = yield* Effect.either( |
| 66 | − materializationService.sync({ | |
| 66 | + catalogSyncService.sync({ | |
| 67 | 67 | source: sourceForSync, |
| 68 | 68 | actorAccountId: input.actorAccountId, |
| 69 | 69 | }), |
| @@ -8,11 +8,9 @@ import { · 7 unmodified lines | ||
| 8 | 8 | type ToolCatalog, |
| 9 | 9 | type ToolInvoker, |
| 10 | 10 | type ToolPath, |
| 11 | − type ToolSchemaBundle, | |
| 12 | 11 | } from "@executor/codemode-core"; |
| 13 | 12 | import { makeQuickJsExecutor } from "@executor/runtime-quickjs"; |
| 14 | −import type { AccountId, Source, SourceRecipeSchemaBundleId } from "#schema"; | |
| 15 | −import { SourceRecipeSchemaBundleIdSchema } from "#schema"; | |
| 13 | +import type { AccountId, Source } from "#schema"; | |
| 16 | 14 | import * as Context from "effect/Context"; |
| 17 | 15 | import * as Either from "effect/Either"; |
| 18 | 16 | import * as Effect from "effect/Effect"; |
| @@ -29,13 +27,15 @@ import { · 10 unmodified lines | ||
| 29 | 27 | type RuntimeSourceAuthService, |
| 30 | 28 | } from "./source-auth-service"; |
| 31 | 29 | import { |
| 32 | − RuntimeSourceRecipeStoreService, | |
| 33 | − type LoadedSourceRecipeToolIndexEntry, | |
| 34 | − recipeToolCatalogEntry, | |
| 35 | −} from "./source-recipes-runtime"; | |
| 30 | + RuntimeSourceCatalogStoreService, | |
| 31 | + type LoadedSourceCatalogToolIndexEntry, | |
| 32 | + catalogToolCatalogEntry, | |
| 33 | +} from "./source-catalog-runtime"; | |
| 34 | +import { | |
| 35 | + invocationDescriptorFromTool, | |
| 36 | + invokeIrTool, | |
| 37 | +} from "./ir-execution"; | |
| 36 | 38 | import { RuntimeSourceAuthMaterialService } from "./source-auth-material"; |
| 37 | −import { namespaceFromSourceName } from "./source-names"; | |
| 38 | −import { getSourceAdapterForOperation } from "./source-adapters"; | |
| 39 | 39 | import { |
| 40 | 40 | type SecretMaterialResolveContext, |
| 41 | 41 | } from "./secret-material-providers"; |
| @@ -145,14 +145,14 @@ const toSecretResolutionContext = ( · 103 unmodified lines | ||
| 145 | 145 | const queryTokenWeight = (token: string): number => |
| 146 | 146 | LOW_SIGNAL_QUERY_TOKENS.has(token) ? 0.25 : 1; |
| 147 | 147 | |
| 148 | −const loadWorkspaceRecipeTools = (input: { | |
| 148 | +const loadWorkspaceCatalogTools = (input: { | |
| 149 | 149 | workspaceId: Source["workspaceId"]; |
| 150 | 150 | accountId: AccountId; |
| 151 | − sourceRecipeStore: Effect.Effect.Success<typeof RuntimeSourceRecipeStoreService>; | |
| 151 | + sourceCatalogStore: Effect.Effect.Success<typeof RuntimeSourceCatalogStoreService>; | |
| 152 | 152 | includeSchemas: boolean; |
| 153 | −}): Effect.Effect<readonly LoadedSourceRecipeToolIndexEntry[], Error, WorkspaceStorageServices> => | |
| 153 | +}): Effect.Effect<readonly LoadedSourceCatalogToolIndexEntry[], Error, WorkspaceStorageServices> => | |
| 154 | 154 | Effect.map( |
| 155 | − input.sourceRecipeStore.loadWorkspaceSourceRecipeToolIndex({ | |
| 155 | + input.sourceCatalogStore.loadWorkspaceSourceCatalogToolIndex({ | |
| 156 | 156 | workspaceId: input.workspaceId, |
| 157 | 157 | actorAccountId: input.accountId, |
| 158 | 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 | 167 | workspaceId: Source["workspaceId"]; |
| 168 | 168 | accountId: AccountId; |
| 169 | − sourceRecipeStore: Effect.Effect.Success<typeof RuntimeSourceRecipeStoreService>; | |
| 169 | + sourceCatalogStore: Effect.Effect.Success<typeof RuntimeSourceCatalogStoreService>; | |
| 170 | 170 | path: string; |
| 171 | 171 | includeSchemas: boolean; |
| 172 | −}): Effect.Effect<LoadedSourceRecipeToolIndexEntry | null, Error, WorkspaceStorageServices> => | |
| 173 | − input.sourceRecipeStore.loadWorkspaceSourceRecipeToolByPath({ | |
| 172 | +}): Effect.Effect<LoadedSourceCatalogToolIndexEntry | null, Error, WorkspaceStorageServices> => | |
| 173 | + input.sourceCatalogStore.loadWorkspaceSourceCatalogToolByPath({ | |
| 174 | 174 | workspaceId: input.workspaceId, |
| 175 | 175 | path: input.path, |
| 176 | 176 | actorAccountId: input.accountId, |
| @@ -183,42 +183,29 @@ const loadWorkspaceRecipeToolByPath = (input: { · 6 unmodified lines | ||
| 183 | 183 | ), |
| 184 | 184 | ); |
| 185 | 185 | |
| 186 | −const loadWorkspaceSchemaBundle = (input: { | |
| 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 = ( | |
| 186 | +const scoreCatalogTool = ( | |
| 208 | 187 | queryTokens: readonly string[], |
| 209 | − tool: LoadedSourceRecipeToolIndexEntry, | |
| 188 | + tool: LoadedSourceCatalogToolIndexEntry, | |
| 210 | 189 | ): number => { |
| 211 | 190 | const pathText = tool.path.toLowerCase(); |
| 212 | 191 | const namespaceText = tool.searchNamespace.toLowerCase(); |
| 213 | − const toolIdText = tool.operation.toolId.toLowerCase(); | |
| 214 | − const titleText = tool.operation.title?.toLowerCase() ?? ""; | |
| 215 | − const descriptionText = tool.operation.description?.toLowerCase() ?? ""; | |
| 216 | − const templateText = tool.metadata.pathTemplate?.toLowerCase() ?? ""; | |
| 217 | − | |
| 218 | − const pathTokens = tokenize(`${tool.path} ${tool.operation.toolId}`); | |
| 192 | + const toolIdText = tool.path.split(".").at(-1)?.toLowerCase() ?? ""; | |
| 193 | + const titleText = tool.capability.surface.title?.toLowerCase() ?? ""; | |
| 194 | + const descriptionText = | |
| 195 | + tool.capability.surface.summary?.toLowerCase() | |
| 196 | + ?? tool.capability.surface.description?.toLowerCase() | |
| 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 | 206 | const namespaceTokens = tokenize(tool.searchNamespace); |
| 220 | − const titleTokens = tokenize(tool.operation.title ?? ""); | |
| 221 | − const templateTokens = tokenize(tool.metadata.pathTemplate ?? ""); | |
| 207 | + const titleTokens = tokenize(tool.capability.surface.title ?? ""); | |
| 208 | + const templateTokens = tokenize(templateText); | |
| 222 | 209 | |
| 223 | 210 | let score = 0; |
| 224 | 211 | let structuralHits = 0; |
| @@ -342,20 +329,9 @@ const approvalMessageForInvocation = ( · 117 unmodified lines | ||
| 342 | 329 | return `Allow tool call: ${descriptor.toolPath}?`; |
| 343 | 330 | }; |
| 344 | 331 | |
| 345 | −const toInvocationDescriptorFromRecipeTool = (input: { | |
| 346 | − tool: LoadedSourceRecipeToolIndexEntry; | |
| 347 | −}): 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 | −}); | |
| 332 | +const toInvocationDescriptorFromCatalogTool = (input: { | |
| 333 | + tool: LoadedSourceCatalogToolIndexEntry; | |
| 334 | +}): InvocationDescriptor => invocationDescriptorFromTool(input) satisfies InvocationDescriptor; | |
| 359 | 335 | |
| 360 | 336 | const authorizePersistedToolInvocation = (input: { |
| 361 | 337 | workspaceId: Source["workspaceId"]; |
| @@ -452,10 +428,10 @@ const provideRuntimeLocalWorkspace = <A, E, R>( · 90 unmodified lines | ||
| 452 | 428 | ): Effect.Effect<A, E, R> => |
| 453 | 429 | provideOptionalRuntimeLocalWorkspace(effect, runtimeLocalWorkspace); |
| 454 | 430 | |
| 455 | −const createWorkspaceRecipeCatalog = (input: { | |
| 431 | +const createWorkspaceSourceCatalog = (input: { | |
| 456 | 432 | workspaceId: Source["workspaceId"]; |
| 457 | 433 | accountId: AccountId; |
| 458 | − sourceRecipeStore: Effect.Effect.Success<typeof RuntimeSourceRecipeStoreService>; | |
| 434 | + sourceCatalogStore: Effect.Effect.Success<typeof RuntimeSourceCatalogStoreService>; | |
| 459 | 435 | workspaceConfigStore: WorkspaceConfigStoreShape; |
| 460 | 436 | workspaceStateStore: WorkspaceStateStoreShape; |
| 461 | 437 | sourceArtifactStore: SourceArtifactStoreShape; |
| @@ -471,26 +447,20 @@ const createWorkspaceRecipeCatalog = (input: { · 9 unmodified lines | ||
| 471 | 447 | |
| 472 | 448 | const createSharedCatalog = (includeSchemas: boolean): Effect.Effect<ToolCatalog, Error, never> => |
| 473 | 449 | provideWorkspaceStorage(Effect.gen(function* () { |
| 474 | − const recipeTools = yield* loadWorkspaceRecipeTools({ | |
| 450 | + const catalogTools = yield* loadWorkspaceCatalogTools({ | |
| 475 | 451 | workspaceId: input.workspaceId, |
| 476 | 452 | accountId: input.accountId, |
| 477 | − sourceRecipeStore: input.sourceRecipeStore, | |
| 453 | + sourceCatalogStore: input.sourceCatalogStore, | |
| 478 | 454 | includeSchemas, |
| 479 | 455 | }); |
| 480 | 456 | |
| 481 | 457 | return createToolCatalogFromEntries({ |
| 482 | − entries: recipeTools.map((tool) => | |
| 483 | − recipeToolCatalogEntry({ | |
| 458 | + entries: catalogTools.map((tool) => | |
| 459 | + catalogToolCatalogEntry({ | |
| 484 | 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 | 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 | 497 | searchTools: ({ query, namespace, limit }) => |
| 536 | 498 | provideRuntimeLocalWorkspace( |
| 537 | 499 | Effect.flatMap(createSharedCatalog(false), (catalog) => |
| @@ -549,7 +511,7 @@ const createWorkspaceRecipeCatalog = (input: { · 11 unmodified lines | ||
| 549 | 511 | const createWorkspaceToolInvoker = (input: { |
| 550 | 512 | workspaceId: Source["workspaceId"]; |
| 551 | 513 | accountId: AccountId; |
| 552 | − sourceRecipeStore: Effect.Effect.Success<typeof RuntimeSourceRecipeStoreService>; | |
| 514 | + sourceCatalogStore: Effect.Effect.Success<typeof RuntimeSourceCatalogStoreService>; | |
| 553 | 515 | workspaceConfigStore: WorkspaceConfigStoreShape; |
| 554 | 516 | workspaceStateStore: WorkspaceStateStoreShape; |
| 555 | 517 | sourceArtifactStore: SourceArtifactStoreShape; |
| @@ -578,10 +540,10 @@ const createWorkspaceToolInvoker = (input: { · 22 unmodified lines | ||
| 578 | 540 | sourceAuthService: input.sourceAuthService, |
| 579 | 541 | runtimeLocalWorkspace: input.runtimeLocalWorkspace, |
| 580 | 542 | }); |
| 581 | − const recipeCatalog = createWorkspaceRecipeCatalog({ | |
| 543 | + const sourceCatalog = createWorkspaceSourceCatalog({ | |
| 582 | 544 | workspaceId: input.workspaceId, |
| 583 | 545 | accountId: input.accountId, |
| 584 | − sourceRecipeStore: input.sourceRecipeStore, | |
| 546 | + sourceCatalogStore: input.sourceCatalogStore, | |
| 585 | 547 | workspaceConfigStore: input.workspaceConfigStore, |
| 586 | 548 | workspaceStateStore: input.workspaceStateStore, |
| 587 | 549 | sourceArtifactStore: input.sourceArtifactStore, |
| @@ -606,7 +568,7 @@ const createWorkspaceToolInvoker = (input: { · 18 unmodified lines | ||
| 606 | 568 | tools: authoredTools, |
| 607 | 569 | }); |
| 608 | 570 | catalog = mergeToolCatalogs({ |
| 609 | − catalogs: [authoredCatalog, recipeCatalog], | |
| 571 | + catalogs: [authoredCatalog, sourceCatalog], | |
| 610 | 572 | }); |
| 611 | 573 | const authoredToolPaths = new Set(Object.keys(authoredTools)); |
| 612 | 574 | const authoredInvoker = makeToolInvokerFromTools({ |
| @@ -621,14 +583,14 @@ const createWorkspaceToolInvoker = (input: { · 8 unmodified lines | ||
| 621 | 583 | }) => |
| 622 | 584 | provideRuntimeLocalWorkspace( |
| 623 | 585 | provideWorkspaceStorage(Effect.gen(function* () { |
| 624 | − const recipeTool = yield* loadWorkspaceRecipeToolByPath({ | |
| 586 | + const catalogTool = yield* loadWorkspaceCatalogToolByPath({ | |
| 625 | 587 | workspaceId: input.workspaceId, |
| 626 | 588 | accountId: input.accountId, |
| 627 | − sourceRecipeStore: input.sourceRecipeStore, | |
| 589 | + sourceCatalogStore: input.sourceCatalogStore, | |
| 628 | 590 | path: invocation.path, |
| 629 | 591 | includeSchemas: false, |
| 630 | 592 | }); |
| 631 | − if (!recipeTool) { | |
| 593 | + if (!catalogTool) { | |
| 632 | 594 | return yield* Effect.fail( |
| 633 | 595 | new Error(`Unknown tool path: ${invocation.path}`), |
| 634 | 596 | ); |
| @@ -637,49 +599,27 @@ const createWorkspaceToolInvoker = (input: { · 2 unmodified lines | ||
| 637 | 599 | yield* authorizePersistedToolInvocation({ |
| 638 | 600 | workspaceId: input.workspaceId, |
| 639 | 601 | accountId: input.accountId, |
| 640 | − descriptor: toInvocationDescriptorFromRecipeTool({ | |
| 641 | − tool: recipeTool, | |
| 602 | + descriptor: toInvocationDescriptorFromCatalogTool({ | |
| 603 | + tool: catalogTool, | |
| 642 | 604 | }), |
| 643 | 605 | args: invocation.args, |
| 644 | − source: recipeTool.source, | |
| 606 | + source: catalogTool.source, | |
| 645 | 607 | context: invocation.context, |
| 646 | 608 | onElicitation: input.onElicitation, |
| 647 | 609 | }); |
| 648 | 610 | |
| 649 | 611 | const auth = yield* input.sourceAuthMaterialService.resolve({ |
| 650 | − source: recipeTool.source, | |
| 612 | + source: catalogTool.source, | |
| 651 | 613 | actorAccountId: input.accountId, |
| 652 | 614 | context: toSecretResolutionContext(invocation.context), |
| 653 | 615 | }); |
| 654 | − const schemaBundle = recipeTool.schemaBundleId | |
| 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({ | |
| 616 | + return yield* invokeIrTool({ | |
| 672 | 617 | workspaceId: input.workspaceId, |
| 673 | 618 | accountId: input.accountId, |
| 674 | − source: recipeTool.source, | |
| 675 | − path: invocation.path, | |
| 676 | − operation: recipeTool.operation, | |
| 677 | − schemaBundle, | |
| 678 | − manifestJson: recipe.revision.manifestJson, | |
| 619 | + tool: catalogTool, | |
| 679 | 620 | auth, |
| 680 | 621 | args: invocation.args, |
| 681 | 622 | context: invocation.context, |
| 682 | − onElicitation: input.onElicitation, | |
| 683 | 623 | }); |
| 684 | 624 | })), |
| 685 | 625 | input.runtimeLocalWorkspace, |
| @@ -702,7 +642,7 @@ const createWorkspaceToolInvoker = (input: { · 16 unmodified lines | ||
| 702 | 642 | export const createWorkspaceExecutionEnvironmentResolver = (input: { |
| 703 | 643 | sourceAuthMaterialService: Effect.Effect.Success<typeof RuntimeSourceAuthMaterialService>; |
| 704 | 644 | sourceAuthService: RuntimeSourceAuthService; |
| 705 | − sourceRecipeStore: Effect.Effect.Success<typeof RuntimeSourceRecipeStoreService>; | |
| 645 | + sourceCatalogStore: Effect.Effect.Success<typeof RuntimeSourceCatalogStoreService>; | |
| 706 | 646 | localToolRuntimeLoader: LocalToolRuntimeLoaderShape; |
| 707 | 647 | workspaceConfigStore: WorkspaceConfigStoreShape; |
| 708 | 648 | workspaceStateStore: WorkspaceStateStoreShape; |
| @@ -723,7 +663,7 @@ export const createWorkspaceExecutionEnvironmentResolver = (input: { · 14 unmodified lines | ||
| 723 | 663 | const { catalog, toolInvoker } = createWorkspaceToolInvoker({ |
| 724 | 664 | workspaceId, |
| 725 | 665 | accountId, |
| 726 | − sourceRecipeStore: input.sourceRecipeStore, | |
| 666 | + sourceCatalogStore: input.sourceCatalogStore, | |
| 727 | 667 | workspaceConfigStore: input.workspaceConfigStore, |
| 728 | 668 | workspaceStateStore: input.workspaceStateStore, |
| 729 | 669 | sourceArtifactStore: input.sourceArtifactStore, |
| @@ -762,7 +702,7 @@ export const RuntimeExecutionResolverLive = ( · 32 unmodified lines | ||
| 762 | 702 | Effect.gen(function* () { |
| 763 | 703 | const sourceAuthMaterialService = yield* RuntimeSourceAuthMaterialService; |
| 764 | 704 | const sourceAuthService = yield* RuntimeSourceAuthServiceTag; |
| 765 | − const sourceRecipeStore = yield* RuntimeSourceRecipeStoreService; | |
| 705 | + const sourceCatalogStore = yield* RuntimeSourceCatalogStoreService; | |
| 766 | 706 | const localToolRuntimeLoader = yield* LocalToolRuntimeLoaderService; |
| 767 | 707 | const workspaceConfigStore = yield* WorkspaceConfigStore; |
| 768 | 708 | const workspaceStateStore = yield* WorkspaceStateStore; |
| @@ -771,7 +711,7 @@ export const RuntimeExecutionResolverLive = ( · 2 unmodified lines | ||
| 771 | 711 | return createWorkspaceExecutionEnvironmentResolver({ |
| 772 | 712 | sourceAuthService, |
| 773 | 713 | sourceAuthMaterialService, |
| 774 | − sourceRecipeStore, | |
| 714 | + sourceCatalogStore, | |
| 775 | 715 | localToolRuntimeLoader, |
| 776 | 716 | workspaceConfigStore, |
| 777 | 717 | workspaceStateStore, |
| @@ -16,21 +16,13 @@ export { · 15 unmodified lines | ||
| 16 | 16 | type SecretRef, |
| 17 | 17 | } from "./models/auth-artifact"; |
| 18 | 18 | export { |
| 19 | − SourceRecipeAdapterKeySchema, | |
| 20 | − SourceRecipeDocumentKindSchema, | |
| 21 | − SourceRecipeKindSchema, | |
| 22 | − SourceRecipeOperationKindSchema, | |
| 23 | − SourceRecipeOperationProviderKindSchema, | |
| 24 | − SourceRecipeTransportKindSchema, | |
| 25 | − SourceRecipeVisibilitySchema, | |
| 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"; | |
| 19 | + SourceCatalogAdapterKeySchema, | |
| 20 | + SourceCatalogKindSchema, | |
| 21 | + SourceCatalogVisibilitySchema, | |
| 22 | + type SourceCatalogAdapterKey, | |
| 23 | + type SourceCatalogKind, | |
| 24 | + type SourceCatalogVisibility, | |
| 25 | +} from "./models/source-catalog"; | |
| 34 | 26 | export { |
| 35 | 27 | SourceAuthInferenceSchema, |
| 36 | 28 | SourceDiscoveryAuthKindSchema, |
| @@ -3,12 +3,9 @@ import { Schema } from "effect"; · 2 unmodified lines | ||
| 3 | 3 | export const AccountIdSchema = Schema.String.pipe(Schema.brand("AccountId")); |
| 4 | 4 | export const WorkspaceIdSchema = Schema.String.pipe(Schema.brand("WorkspaceId")); |
| 5 | 5 | export const SourceIdSchema = Schema.String.pipe(Schema.brand("SourceId")); |
| 6 | −export const SourceRecipeIdSchema = Schema.String.pipe(Schema.brand("SourceRecipeId")); | |
| 7 | −export const SourceRecipeRevisionIdSchema = Schema.String.pipe( | |
| 8 | − Schema.brand("SourceRecipeRevisionId"), | |
| 9 | −); | |
| 10 | −export const SourceRecipeSchemaBundleIdSchema = Schema.String.pipe( | |
| 11 | − Schema.brand("SourceRecipeSchemaBundleId"), | |
| 6 | +export const SourceCatalogIdSchema = Schema.String.pipe(Schema.brand("SourceCatalogId")); | |
| 7 | +export const SourceCatalogRevisionIdSchema = Schema.String.pipe( | |
| 8 | + Schema.brand("SourceCatalogRevisionId"), | |
| 12 | 9 | ); |
| 13 | 10 | export const SourceAuthSessionIdSchema = Schema.String.pipe( |
| 14 | 11 | Schema.brand("SourceAuthSessionId"), |
| @@ -38,9 +35,8 @@ export const ExecutionStepIdSchema = Schema.String.pipe( · 23 unmodified lines | ||
| 38 | 35 | export type AccountId = typeof AccountIdSchema.Type; |
| 39 | 36 | export type WorkspaceId = typeof WorkspaceIdSchema.Type; |
| 40 | 37 | export type SourceId = typeof SourceIdSchema.Type; |
| 41 | −export type SourceRecipeId = typeof SourceRecipeIdSchema.Type; | |
| 42 | −export type SourceRecipeRevisionId = typeof SourceRecipeRevisionIdSchema.Type; | |
| 43 | −export type SourceRecipeSchemaBundleId = typeof SourceRecipeSchemaBundleIdSchema.Type; | |
| 38 | +export type SourceCatalogId = typeof SourceCatalogIdSchema.Type; | |
| 39 | +export type SourceCatalogRevisionId = typeof SourceCatalogRevisionIdSchema.Type; | |
| 44 | 40 | export type SourceAuthSessionId = typeof SourceAuthSessionIdSchema.Type; |
| 45 | 41 | export type AuthArtifactId = typeof AuthArtifactIdSchema.Type; |
| 46 | 42 | export type AuthLeaseId = typeof AuthLeaseIdSchema.Type; |
| @@ -4,7 +4,7 @@ export * from "./ids"; · 3 unmodified lines | ||
| 4 | 4 | |
| 5 | 5 | export * from "./models/auth"; |
| 6 | 6 | export * from "./models/source"; |
| 7 | −export * from "./models/source-recipe"; | |
| 7 | +export * from "./models/source-catalog"; | |
| 8 | 8 | export * from "./models/auth-artifact"; |
| 9 | 9 | export * from "./models/auth-lease"; |
| 10 | 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 | 3 | import { SourceSchema } from "./source"; |
| 4 | 4 | |
| 5 | −export const SourceInspectionPipelineKindSchema = Schema.Literal( | |
| 6 | − "openapi", | |
| 7 | − "graphql", | |
| 8 | − "persisted", | |
| 9 | −); | |
| 5 | +export const SourceInspectionPipelineKindSchema = Schema.Literal("ir"); | |
| 10 | 6 | |
| 11 | 7 | export const SourceInspectionToolSummarySchema = Schema.Struct({ |
| 12 | 8 | path: Schema.String, |
| 13 | 9 | sourceKey: Schema.String, |
| 14 | 10 | title: Schema.optional(Schema.String), |
| 15 | 11 | description: Schema.optional(Schema.String), |
| 16 | − providerKind: Schema.String, | |
| 12 | + protocol: Schema.String, | |
| 17 | 13 | toolId: Schema.String, |
| 18 | 14 | rawToolId: Schema.NullOr(Schema.String), |
| 19 | 15 | operationId: Schema.NullOr(Schema.String), |
| @@ -43,19 +39,11 @@ export const SourceInspectionToolDetailSchema = Schema.Struct({ · 23 unmodified lines | ||
| 43 | 39 | summary: SourceInspectionToolSummarySchema, |
| 44 | 40 | definitionJson: Schema.NullOr(Schema.String), |
| 45 | 41 | documentationJson: Schema.NullOr(Schema.String), |
| 46 | − providerDataJson: Schema.NullOr(Schema.String), | |
| 47 | − inputSchemaJson: Schema.NullOr(Schema.String), | |
| 48 | − outputSchemaJson: Schema.NullOr(Schema.String), | |
| 49 | − schemaBundleId: Schema.NullOr(Schema.String), | |
| 50 | − exampleInputJson: 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, | |
| 42 | + nativeJson: Schema.NullOr(Schema.String), | |
| 43 | + callSchemaJson: Schema.NullOr(Schema.String), | |
| 44 | + resultSchemaJson: Schema.NullOr(Schema.String), | |
| 45 | + exampleCallJson: Schema.NullOr(Schema.String), | |
| 46 | + exampleResultJson: Schema.NullOr(Schema.String), | |
| 59 | 47 | }); |
| 60 | 48 | |
| 61 | 49 | export const SourceInspectionDiscoverPayloadSchema = Schema.Struct({ |
| @@ -89,8 +77,6 @@ export type SourceInspectionToolListItem = · 27 unmodified lines | ||
| 89 | 77 | export type SourceInspection = typeof SourceInspectionSchema.Type; |
| 90 | 78 | export type SourceInspectionToolDetail = |
| 91 | 79 | typeof SourceInspectionToolDetailSchema.Type; |
| 92 | −export type SourceInspectionSchemaBundle = | |
| 93 | − typeof SourceInspectionSchemaBundleSchema.Type; | |
| 94 | 80 | export type SourceInspectionDiscoverPayload = |
| 95 | 81 | typeof SourceInspectionDiscoverPayloadSchema.Type; |
| 96 | 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 | 3 | import { TimestampMsSchema } from "../common"; |
| 4 | 4 | import { |
| 5 | 5 | SourceIdSchema, |
| 6 | − SourceRecipeIdSchema, | |
| 7 | − SourceRecipeRevisionIdSchema, | |
| 6 | + SourceCatalogIdSchema, | |
| 7 | + SourceCatalogRevisionIdSchema, | |
| 8 | 8 | WorkspaceIdSchema, |
| 9 | 9 | } from "../ids"; |
| 10 | 10 | import { SecretRefSchema } from "./auth-artifact"; |
| @@ -77,8 +77,8 @@ export const SourceBindingSchema = Schema.Struct({ · 66 unmodified lines | ||
| 77 | 77 | const SourceStorageRowSchema = Schema.Struct({ |
| 78 | 78 | workspaceId: WorkspaceIdSchema, |
| 79 | 79 | sourceId: SourceIdSchema, |
| 80 | − recipeId: SourceRecipeIdSchema, | |
| 81 | − recipeRevisionId: SourceRecipeRevisionIdSchema, | |
| 80 | + catalogId: SourceCatalogIdSchema, | |
| 81 | + catalogRevisionId: SourceCatalogRevisionIdSchema, | |
| 82 | 82 | name: Schema.String, |
| 83 | 83 | kind: SourceKindSchema, |
| 84 | 84 | endpoint: Schema.String, |
| @@ -98,8 +98,8 @@ export const StoredSourceRecordSchema = Schema.transform( · 13 unmodified lines | ||
| 98 | 98 | Schema.Struct({ |
| 99 | 99 | id: SourceIdSchema, |
| 100 | 100 | workspaceId: WorkspaceIdSchema, |
| 101 | − recipeId: SourceRecipeIdSchema, | |
| 102 | − recipeRevisionId: SourceRecipeRevisionIdSchema, | |
| 101 | + catalogId: SourceCatalogIdSchema, | |
| 102 | + catalogRevisionId: SourceCatalogRevisionIdSchema, | |
| 103 | 103 | name: Schema.String, |
| 104 | 104 | kind: SourceKindSchema, |
| 105 | 105 | endpoint: Schema.String, |
| @@ -118,8 +118,8 @@ export const StoredSourceRecordSchema = Schema.transform( · 12 unmodified lines | ||
| 118 | 118 | decode: (row) => ({ |
| 119 | 119 | id: row.sourceId, |
| 120 | 120 | workspaceId: row.workspaceId, |
| 121 | − recipeId: row.recipeId, | |
| 122 | − recipeRevisionId: row.recipeRevisionId, | |
| 121 | + catalogId: row.catalogId, | |
| 122 | + catalogRevisionId: row.catalogRevisionId, | |
| 123 | 123 | name: row.name, |
| 124 | 124 | kind: row.kind, |
| 125 | 125 | endpoint: row.endpoint, |
| @@ -136,8 +136,8 @@ export const StoredSourceRecordSchema = Schema.transform( · 10 unmodified lines | ||
| 136 | 136 | encode: (source) => ({ |
| 137 | 137 | workspaceId: source.workspaceId, |
| 138 | 138 | sourceId: source.id, |
| 139 | − recipeId: source.recipeId, | |
| 140 | − recipeRevisionId: source.recipeRevisionId, | |
| 139 | + catalogId: source.catalogId, | |
| 140 | + catalogRevisionId: source.catalogRevisionId, | |
| 141 | 141 | name: source.name, |
| 142 | 142 | kind: source.kind, |
| 143 | 143 | endpoint: source.endpoint, |
| @@ -3,55 +3,55 @@ import * as Either from "effect/Either"; · 2 unmodified lines | ||
| 3 | 3 | import * as Schema from "effect/Schema"; |
| 4 | 4 | |
| 5 | 5 | import { |
| 6 | − StoredSourceRecipeOperationRecordSchema, | |
| 6 | + SourceInspectionToolDetailSchema, | |
| 7 | + StoredSourceCatalogRevisionRecordSchema, | |
| 7 | 8 | } from "./index"; |
| 8 | 9 | |
| 9 | 10 | describe("control-plane-schema", () => { |
| 10 | − it("accepts canonical source recipe operation rows and rejects invalid transport kinds", () => { | |
| 11 | − const decode = Schema.decodeUnknownEither(StoredSourceRecipeOperationRecordSchema); | |
| 11 | + it("accepts snapshot-native inspection detail rows", () => { | |
| 12 | + const decode = Schema.decodeUnknownEither(SourceInspectionToolDetailSchema); | |
| 12 | 13 | |
| 13 | − const operationRecord = decode({ | |
| 14 | − id: "src_recipe_op_1", | |
| 15 | − recipeRevisionId: "src_recipe_rev_1", | |
| 16 | − operationKey: "getRepo", | |
| 17 | − transportKind: "http", | |
| 18 | − toolId: "getRepo", | |
| 19 | − title: "Get Repo", | |
| 20 | − description: "Read a repository", | |
| 21 | − operationKind: "read", | |
| 22 | − searchText: "get repo", | |
| 23 | − inputSchemaJson: null, | |
| 24 | − outputSchemaJson: null, | |
| 25 | − providerKind: "openapi", | |
| 26 | − providerDataJson: JSON.stringify({ | |
| 27 | − kind: "openapi", | |
| 28 | − }), | |
| 29 | − createdAt: 1, | |
| 30 | − updatedAt: 1, | |
| 14 | + const detail = decode({ | |
| 15 | + summary: { | |
| 16 | + path: "github.issues.list", | |
| 17 | + sourceKey: "src_1", | |
| 18 | + protocol: "http", | |
| 19 | + toolId: "list", | |
| 20 | + rawToolId: "issues.list", | |
| 21 | + operationId: "listIssues", | |
| 22 | + group: "issues", | |
| 23 | + leaf: "list", | |
| 24 | + tags: ["issues"], | |
| 25 | + method: "GET", | |
| 26 | + pathTemplate: "/issues", | |
| 27 | + }, | |
| 28 | + definitionJson: "{}", | |
| 29 | + documentationJson: "{}", | |
| 30 | + nativeJson: "{}", | |
| 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({ | |
| 36 | − id: "src_recipe_op_2", | |
| 37 | − recipeRevisionId: "src_recipe_rev_1", | |
| 38 | − operationKey: "viewer", | |
| 39 | − transportKind: "ftp", | |
| 40 | − toolId: "viewer", | |
| 41 | − title: "Viewer", | |
| 42 | − description: null, | |
| 43 | − operationKind: "read", | |
| 44 | − searchText: "viewer", | |
| 45 | − inputSchemaJson: null, | |
| 46 | − outputSchemaJson: null, | |
| 47 | − providerKind: "graphql", | |
| 48 | − providerDataJson: JSON.stringify({ | |
| 49 | − kind: "graphql", | |
| 50 | − }), | |
| 43 | + const invalidRevision = decode({ | |
| 44 | + id: "src_catalog_rev_1", | |
| 45 | + catalogId: "src_catalog_1", | |
| 46 | + revisionNumber: "1", | |
| 47 | + sourceConfigJson: "{}", | |
| 48 | + importMetadataJson: null, | |
| 49 | + importMetadataHash: null, | |
| 50 | + snapshotHash: null, | |
| 51 | 51 | createdAt: 1, |
| 52 | 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 | 67 | description: string | null; |
| 68 | 68 | searchNamespace: string; |
| 69 | 69 | searchText: string; |
| 70 | − inputSchemaJson: string | null; | |
| 71 | − outputSchemaJson: string | null; | |
| 70 | + inputSchema: unknown | null; | |
| 71 | + outputSchema: unknown | null; | |
| 72 | 72 | providerKind: "openapi"; |
| 73 | 73 | openApiMethod: "get" | "put" | "post" | "delete" | "patch" | "head" | "options" | "trace"; |
| 74 | 74 | openApiPathTemplate: string | null; |
| @@ -89,10 +89,10 @@ const toDescriptor = ( · 14 unmodified lines | ||
| 89 | 89 | sourceKey: artifact.sourceId, |
| 90 | 90 | description: artifact.description ?? artifact.title ?? undefined, |
| 91 | 91 | interaction: "auto" as const, |
| 92 | − inputType: includeSchemas && artifact.inputSchemaJson ? "object" : undefined, | |
| 93 | − outputType: includeSchemas && artifact.outputSchemaJson ? "output" : undefined, | |
| 94 | − inputSchemaJson: includeSchemas ? artifact.inputSchemaJson ?? undefined : undefined, | |
| 95 | − outputSchemaJson: includeSchemas ? artifact.outputSchemaJson ?? undefined : undefined, | |
| 92 | + inputType: includeSchemas && artifact.inputSchema ? "object" : undefined, | |
| 93 | + outputType: includeSchemas && artifact.outputSchema ? "output" : undefined, | |
| 94 | + inputSchema: includeSchemas ? artifact.inputSchema ?? undefined : undefined, | |
| 95 | + outputSchema: includeSchemas ? artifact.outputSchema ?? undefined : undefined, | |
| 96 | 96 | }); |
| 97 | 97 | |
| 98 | 98 | type SourceCallContext = { |
| @@ -359,7 +359,6 @@ const createWorkspaceToolCatalog = (input: { · 260 unmodified lines | ||
| 359 | 359 | path, |
| 360 | 360 | }).then((artifact) => (artifact ? toDescriptor(artifact, includeSchemas) : null)) |
| 361 | 361 | ), |
| 362 | − getSchemaBundle: () => Effect.succeed(null), | |
| 363 | 362 | searchTools: ({ query, namespace, limit }) => |
| 364 | 363 | Effect.promise(() => |
| 365 | 364 | input.toolStore.list({ |
| @@ -522,8 +521,8 @@ const openApiArtifact = (input: { · 156 unmodified lines | ||
| 522 | 521 | method: NonNullable<StoredToolArtifactRecord["openApiMethod"]>; |
| 523 | 522 | pathTemplate: string; |
| 524 | 523 | operationHash?: string; |
| 525 | − inputSchemaJson?: string | null; | |
| 526 | − outputSchemaJson?: string | null; | |
| 524 | + inputSchema?: unknown | null; | |
| 525 | + outputSchema?: unknown | null; | |
| 527 | 526 | }): StoredToolArtifactRecord => { |
| 528 | 527 | const path = input.path as string; |
| 529 | 528 | const description = |
| @@ -545,8 +544,8 @@ const openApiArtifact = (input: { · 15 unmodified lines | ||
| 545 | 544 | input.method.toUpperCase(), |
| 546 | 545 | input.pathTemplate, |
| 547 | 546 | ), |
| 548 | − inputSchemaJson: input.inputSchemaJson ?? null, | |
| 549 | − outputSchemaJson: input.outputSchemaJson ?? null, | |
| 547 | + inputSchema: input.inputSchema ?? null, | |
| 548 | + outputSchema: input.outputSchema ?? null, | |
| 550 | 549 | providerKind: "openapi", |
| 551 | 550 | openApiMethod: input.method, |
| 552 | 551 | openApiPathTemplate: input.pathTemplate, |
| @@ -783,7 +782,7 @@ describe("source runtime", () => { · 230 unmodified lines | ||
| 783 | 782 | "Workflow:", |
| 784 | 783 | '1) const matches = await tools.discover({ query: "<intent>", limit: 12 });', |
| 785 | 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 | 786 | "4) Call selected tools.<path>(input).", |
| 788 | 787 | "Do not use fetch; use tools.* only.", |
| 789 | 788 | ].join("\n"), |
| @@ -834,7 +833,7 @@ describe("source runtime", () => { · 44 unmodified lines | ||
| 834 | 833 | description: "GET /repos/{owner}/{repo}", |
| 835 | 834 | interaction: "auto", |
| 836 | 835 | inputType: "{ owner: string, repo: string }", |
| 837 | − outputType: "unknown", | |
| 836 | + outputType: "void", | |
| 838 | 837 | }, |
| 839 | 838 | ], |
| 840 | 839 | total: 1, |
| @@ -853,7 +852,7 @@ describe("source runtime", () => { · 12 unmodified lines | ||
| 853 | 852 | "Workflow:", |
| 854 | 853 | '1) const matches = await tools.discover({ query: "<intent>", limit: 12 });', |
| 855 | 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 | 856 | "4) Call selected tools.<path>(input).", |
| 858 | 857 | "Do not use fetch; use tools.* only.", |
| 859 | 858 | ].join("\n"), |
| @@ -19,7 +19,6 @@ import type { · 18 unmodified lines | ||
| 19 | 19 | SourceDiscoveryResult, |
| 20 | 20 | SourceInspection, |
| 21 | 21 | SourceInspectionDiscoverResult, |
| 22 | − SourceInspectionSchemaBundle, | |
| 23 | 22 | SourceInspectionToolDetail, |
| 24 | 23 | StartSourceOAuthPayload, |
| 25 | 24 | StartSourceOAuthResult, |
| @@ -59,13 +58,6 @@ type SourceToolDetailKeyParts = readonly [ · 33 unmodified lines | ||
| 59 | 58 | Source["id"], |
| 60 | 59 | string | null, |
| 61 | 60 | ]; |
| 62 | −type SourceSchemaBundleKeyParts = readonly [ | |
| 63 | − boolean, | |
| 64 | − Source["workspaceId"], | |
| 65 | − string, | |
| 66 | − Source["id"], | |
| 67 | − string | null, | |
| 68 | −]; | |
| 69 | 61 | type SourceDiscoveryKeyParts = readonly [ |
| 70 | 62 | boolean, |
| 71 | 63 | Source["workspaceId"], |
| @@ -86,7 +78,6 @@ type ActiveQueryCollections = { · 14 unmodified lines | ||
| 86 | 78 | sources: Set<string>; |
| 87 | 79 | inspections: Set<string>; |
| 88 | 80 | toolDetails: Set<string>; |
| 89 | − schemaBundles: Set<string>; | |
| 90 | 81 | discoveries: Set<string>; |
| 91 | 82 | }; |
| 92 | 83 | |
| @@ -162,17 +153,6 @@ const encodeToolDetailKey = ( · 69 unmodified lines | ||
| 162 | 153 | ): string => |
| 163 | 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 | 156 | const encodeDiscoveryKey = ( |
| 177 | 157 | enabled: boolean, |
| 178 | 158 | workspaceId: Source["workspaceId"], |
| @@ -367,25 +347,6 @@ const sourceInspectionToolAtom = Atom.family((key: string) => { · 188 unmodified lines | ||
| 367 | 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 | 350 | const sourceDiscoveryAtom = Atom.family((key: string) => { |
| 390 | 351 | const [enabled, workspaceId, accountId, sourceId, query, limit] = decodeAtomKey<SourceDiscoveryKeyParts>(key); |
| 391 | 352 | |
| @@ -547,7 +508,6 @@ const createActiveQueryCollections = (): ActiveQueryCollections => ({ · 155 unmodified lines | ||
| 547 | 508 | sources: new Set(), |
| 548 | 509 | inspections: new Set(), |
| 549 | 510 | toolDetails: new Set(), |
| 550 | − schemaBundles: new Set(), | |
| 551 | 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 | 565 | activeQueries.discoveries.forEach((key) => { |
| 613 | 566 | const [enabled, workspaceId, accountId, sourceId] = decodeAtomKey<SourceDiscoveryKeyParts>(key); |
| 614 | 567 | if (enabled && targetMatches(target, workspaceId, accountId, sourceId)) { |
| @@ -990,27 +943,6 @@ export const useSourceToolDetail = ( · 375 unmodified lines | ||
| 990 | 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 | 946 | export const useSourceDiscovery = (input: { |
| 1015 | 947 | sourceId: string; |
| 1016 | 948 | query: string; |