History

Migrate sources to IR catalog snapshots

Rhys Sullivan committed 8 months ago

Commit 5e6d0591 parent 31ad09078 files · +7562 −3187Browse files

Unified · Split

78 files
  1. apps/executor/src/server/server.test.ts +2 −2
  2. packages/ai-sdk-adapter/src/example-dynamic.ts +0 −3
  3. packages/codemode-core/src/discovery.ts +5 −25
  4. packages/codemode-core/src/index.test.ts +5 −20
  5. packages/codemode-core/src/schema-types.ts +16 −0
  6. packages/codemode-core/src/system-tools.ts +0 −29
  7. packages/codemode-core/src/tool-map.ts +24 −25
  8. packages/codemode-core/src/types.ts +15 −39
  9. packages/codemode-google-discovery/src/google-discovery-document.ts +32 −8
  10. packages/codemode-google-discovery/src/google-discovery-tools.test.ts +2 −2
  11. packages/codemode-google-discovery/src/google-discovery-tools.ts +19 −41
  12. packages/codemode-google-discovery/src/google-discovery-types.ts +2 −2
  13. packages/codemode-google-discovery/src/index.ts +1 −1
  14. packages/codemode-mcp/src/mcp-manifest.ts +4 −18
  15. packages/codemode-mcp/src/mcp-tools.test.ts +1 −1
  16. packages/codemode-mcp/src/mcp-tools.ts +10 −6
  17. packages/codemode-openapi/src/index.ts +3 −3
  18. packages/codemode-openapi/src/openapi-definitions.ts +2 −3
  19. packages/codemode-openapi/src/openapi-extraction.real-specs.test.ts +3 −3
  20. packages/codemode-openapi/src/openapi-schema-refs.ts +15 −25
  21. packages/codemode-openapi/src/openapi-tool-presentation.ts +71 −49
  22. packages/codemode-openapi/src/openapi-tools.test.ts +2 −2
  23. packages/codemode-openapi/src/openapi-tools.ts +23 −15
  24. packages/codemode-openapi/src/openapi-types.ts +2 −2
  25. packages/control-plane/package.json +1 −0
  26. packages/control-plane/src/api/sources/api.ts +0 −11
  27. packages/control-plane/src/api/sources/http.ts +0 −12
  28. packages/control-plane/src/index.ts +1 −0
  29. packages/control-plane/src/ir/README.md +33 −0
  30. packages/control-plane/src/ir/catalog.test.ts +711 −0
  31. packages/control-plane/src/ir/catalog.ts +1628 −0
  32. packages/control-plane/src/ir/ids.ts +58 −0
  33. packages/control-plane/src/ir/index.ts +3 −0
  34. packages/control-plane/src/ir/model.ts +962 −0
  35. packages/control-plane/src/runtime/executor-tools.ts +8 −8
  36. packages/control-plane/src/runtime/graphql-tools.test.ts +3 −3
  37. packages/control-plane/src/runtime/graphql-tools.ts +71 −74
  38. packages/control-plane/src/runtime/index.ts +9 −9
  39. packages/control-plane/src/runtime/ir-execution.ts +494 −0
  40. packages/control-plane/src/runtime/live-execution.test.ts +1 −1
  41. packages/control-plane/src/runtime/local-source-artifacts.ts +29 −94
  42. packages/control-plane/src/runtime/local-storage.ts +2 −2
  43. packages/control-plane/src/runtime/oauth-loopback.test.ts +1 −1
  44. packages/control-plane/src/runtime/source-adapters/google-discovery.test.ts +8 −8
  45. packages/control-plane/src/runtime/source-adapters/google-discovery.ts +29 −304
  46. packages/control-plane/src/runtime/source-adapters/graphql.ts +28 −254
  47. packages/control-plane/src/runtime/source-adapters/index.ts +1 −5
  48. packages/control-plane/src/runtime/source-adapters/internal.ts +22 −37
  49. packages/control-plane/src/runtime/source-adapters/mcp.ts +32 −180
  50. packages/control-plane/src/runtime/source-adapters/openapi.ts +27 −295
  51. packages/control-plane/src/runtime/source-adapters/shared.ts +0 −50
  52. packages/control-plane/src/runtime/source-adapters/types.ts +3 −74
  53. packages/control-plane/src/runtime/source-auth-material.test.ts +1 −1
  54. packages/control-plane/src/runtime/source-auth-service.ts +19 −19
  55. packages/control-plane/src/runtime/source-catalog-runtime.ts +743 −0
  56. packages/control-plane/src/runtime/source-catalog-snapshot.test.ts +221 −0
  57. packages/control-plane/src/runtime/source-catalog-snapshot.ts +1688 −0
  58. packages/control-plane/src/runtime/source-catalog-support.ts +18 −0
  59. packages/control-plane/src/runtime/source-catalog-sync.ts +38 −38
  60. packages/control-plane/src/runtime/source-definitions.test.ts +23 −23
  61. packages/control-plane/src/runtime/source-definitions.ts +47 −47
  62. packages/control-plane/src/runtime/source-discovery.test.ts +1 −1
  63. packages/control-plane/src/runtime/source-inspection.ts +133 −248
  64. packages/control-plane/src/runtime/source-recipe-support.ts +0 −27
  65. packages/control-plane/src/runtime/source-recipes-runtime.ts +0 −598
  66. packages/control-plane/src/runtime/source-store.ts +6 −6
  67. packages/control-plane/src/runtime/sources-operations.ts +3 −3
  68. packages/control-plane/src/runtime/workspace-execution-environment.ts +62 −122
  69. packages/control-plane/src/schema/enums.ts +7 −15
  70. packages/control-plane/src/schema/ids.ts +5 −9
  71. packages/control-plane/src/schema/index.ts +1 −1
  72. packages/control-plane/src/schema/models/source-catalog.ts +53 −0
  73. packages/control-plane/src/schema/models/source-inspection.ts +7 −21
  74. packages/control-plane/src/schema/models/source-recipe.ts +0 −131
  75. packages/control-plane/src/schema/models/source.ts +10 −10
  76. packages/control-plane/src/schema/schema.test.ts +39 −39
  77. packages/kitchen-sink/src/source-runtime.test.ts +13 −14
  78. packages/react/src/index.ts +0 −68
apps/executor/src/server/server.test.ts+2−2
@@ -27,7 +27,7 @@ import { · 26 unmodified lines
27 type ResolveExecutionEnvironment,27 type ResolveExecutionEnvironment,
28 resolveLocalWorkspaceContext,28 resolveLocalWorkspaceContext,
29 SourceIdSchema,29 SourceIdSchema,
30 SourceRecipeRevisionIdSchema,30 SourceCatalogRevisionIdSchema,
31 writeLocalSourceArtifact,31 writeLocalSourceArtifact,
32 writeProjectLocalExecutorConfig,32 writeProjectLocalExecutorConfig,
33} from "@executor/control-plane";33} from "@executor/control-plane";
@@ -144,7 +144,7 @@ const writeConfiguredLocalMcpSource = (input: { · 110 unmodified lines
144 };144 };
145 145
146 const materialization = materializationFromMcpManifestEntries({146 const materialization = materializationFromMcpManifestEntries({
147 recipeRevisionId: SourceRecipeRevisionIdSchema.make("src_recipe_rev_materialization"),147 catalogRevisionId: SourceCatalogRevisionIdSchema.make("src_catalog_rev_materialization"),
148 endpoint: input.endpoint,148 endpoint: input.endpoint,
149 manifestEntries: [{149 manifestEntries: [{
150 toolId: "gated_echo",150 toolId: "gated_echo",
packages/ai-sdk-adapter/src/example-dynamic.ts+0−3
@@ -35,9 +35,6 @@ const catalog: ToolCatalog = { · 34 unmodified lines
35 outputType: "object",35 outputType: "object",
36 });36 });
37 },37 },
38 getSchemaBundle() {
39 return Effect.succeed(null);
40 },
41 searchTools({ limit }) {38 searchTools({ limit }) {
42 return Effect.succeed(39 return Effect.succeed(
43 [40 [
packages/codemode-core/src/discovery.ts+5−25
@@ -13,7 +13,6 @@ import type { · 12 unmodified lines
13 ToolMap,13 ToolMap,
14 ToolNamespace,14 ToolNamespace,
15 ToolPath,15 ToolPath,
16 ToolSchemaBundle,
17} from "./types";16} from "./types";
18 17
19const tokenize = (value: string): string[] =>18const tokenize = (value: string): string[] =>
@@ -46,8 +45,8 @@ const projectDescriptor = (input: { · 26 unmodified lines
46 45
47 return {46 return {
48 ...descriptor,47 ...descriptor,
49 inputSchemaJson: undefined,48 inputSchema: undefined,
50 outputSchemaJson: undefined,49 outputSchema: undefined,
51 };50 };
52};51};
53 52
@@ -162,7 +161,6 @@ export function createToolCatalogFromTools(input: { · 108 unmodified lines
162 161
163export function createToolCatalogFromEntries(input: {162export function createToolCatalogFromEntries(input: {
164 entries: ReadonlyArray<ToolCatalogEntry>;163 entries: ReadonlyArray<ToolCatalogEntry>;
165 getSchemaBundle?: (input: { id: string }) => Effect.Effect<ToolSchemaBundle | null, unknown>;
166}): ToolCatalog {164}): ToolCatalog {
167 const entries = [...input.entries];165 const entries = [...input.entries];
168 const byPath = new Map(166 const byPath = new Map(
@@ -213,10 +211,6 @@ export function createToolCatalogFromEntries(input: { · 44 unmodified lines
213 })211 })
214 : null,212 : null,
215 ),213 ),
216 getSchemaBundle: ({ id }) =>
217 input.getSchemaBundle
218 ? input.getSchemaBundle({ id })
219 : Effect.succeed(null),
220 searchTools: ({ query, namespace, limit }) => {214 searchTools: ({ query, namespace, limit }) => {
221 const queryTokens = tokenize(query);215 const queryTokens = tokenize(query);
222 216
@@ -283,18 +277,6 @@ export function mergeToolCatalogs(input: { · 60 unmodified lines
283 return null;277 return null;
284 }),278 }),
285 279
286 getSchemaBundle: ({ id }) =>
287 Effect.gen(function* () {
288 for (const catalog of catalogs) {
289 const bundle = yield* catalog.getSchemaBundle({ id });
290 if (bundle) {
291 return bundle;
292 }
293 }
294
295 return null;
296 }),
297
298 searchTools: ({ query, namespace, limit }) =>280 searchTools: ({ query, namespace, limit }) =>
299 Effect.gen(function* () {281 Effect.gen(function* () {
300 const groups = yield* Effect.forEach(282 const groups = yield* Effect.forEach(
@@ -357,7 +339,6 @@ export function createDiscoveryPrimitivesFromToolCatalog(input: { · 56 unmodified lines
357 const describe: DescribePrimitive = {339 const describe: DescribePrimitive = {
358 tool: ({ path, includeSchemas = false }) =>340 tool: ({ path, includeSchemas = false }) =>
359 catalog.getToolByPath({ path, includeSchemas }),341 catalog.getToolByPath({ path, includeSchemas }),
360 schemaBundle: ({ id }) => catalog.getSchemaBundle({ id }),
361 };342 };
362 343
363 const discover: DiscoverPrimitive = ({344 const discover: DiscoverPrimitive = ({
@@ -406,9 +387,8 @@ export function createDiscoveryPrimitivesFromToolCatalog(input: { · 42 unmodified lines
406 outputType: descriptor.outputType,387 outputType: descriptor.outputType,
407 ...(includeSchemas388 ...(includeSchemas
408 ? {389 ? {
409 inputSchemaJson: descriptor.inputSchemaJson,390 inputSchema: descriptor.inputSchema,
410 outputSchemaJson: descriptor.outputSchemaJson,391 outputSchema: descriptor.outputSchema,
411 schemaBundleId: descriptor.schemaBundleId,
412 }392 }
413 : {}),393 : {}),
414 };394 };
@@ -471,7 +451,7 @@ export function buildDynamicExecuteDescription(input: { · 56 unmodified lines
471 "Workflow:",451 "Workflow:",
472 '1) const matches = await tools.discover({ query: "<intent>", limit: 12 });',452 '1) const matches = await tools.discover({ query: "<intent>", limit: 12 });',
473 "2) const details = await tools.describe.tool({ path, includeSchemas: true });",453 "2) const details = await tools.describe.tool({ path, includeSchemas: true });",
474 "3) If details.schemaBundleId is present, fetch it once with tools.describe.schemaBundle({ id }).",454 "3) Read details.inputSchema/details.outputSchema when you need the projected shape.",
475 "4) Call selected tools.<path>(input).",455 "4) Call selected tools.<path>(input).",
476 "Do not use fetch; use tools.* only.",456 "Do not use fetch; use tools.* only.",
477 ].join("\n");457 ].join("\n");
packages/codemode-core/src/index.test.ts+5−20
@@ -326,13 +326,12 @@ describe("codemode-core", () => { · 325 unmodified lines
326 .slice(0, limit)326 .slice(0, limit)
327 .map((descriptor) => ({327 .map((descriptor) => ({
328 ...descriptor,328 ...descriptor,
329 inputSchemaJson: includeSchemas ? descriptor.inputSchemaJson : undefined,329 inputSchema: includeSchemas ? descriptor.inputSchema : undefined,
330 outputSchemaJson: includeSchemas ? descriptor.outputSchemaJson : undefined,330 outputSchema: includeSchemas ? descriptor.outputSchema : undefined,
331 })),331 })),
332 ),332 ),
333 getToolByPath: ({ path }) =>333 getToolByPath: ({ path }) =>
334 Effect.succeed(descriptors[path] ?? null),334 Effect.succeed(descriptors[path] ?? null),
335 getSchemaBundle: () => Effect.succeed(null),
336 searchTools: () =>335 searchTools: () =>
337 Effect.succeed([336 Effect.succeed([
338 { path: asToolPath("source.issues.create"), score: 0.93 },337 { path: asToolPath("source.issues.create"), score: 0.93 },
@@ -369,7 +368,7 @@ describe("codemode-core", () => { · 30 unmodified lines
369 description: "Get repository details",368 description: "Get repository details",
370 inputType: "{ owner: string; repo: string }",369 inputType: "{ owner: string; repo: string }",
371 outputType: "Repository",370 outputType: "Repository",
372 inputSchemaJson: '{"type":"object"}',371 inputSchema: { type: "object" },
373 },372 },
374 },373 },
375 {374 {
@@ -388,17 +387,6 @@ describe("codemode-core", () => { · 12 unmodified lines
388 },387 },
389 },388 },
390 ],389 ],
391 getSchemaBundle: ({ id }) =>
392 Effect.succeed(
393 id === "bundle_1"
394 ? {
395 id,
396 kind: "json_schema_ref_map",
397 hash: "hash_1",
398 refsJson: "{}",
399 }
400 : null,
401 ),
402 });390 });
403 391
404 const namespaces = yield* catalog.listNamespaces({ limit: 10 });392 const namespaces = yield* catalog.listNamespaces({ limit: 10 });
@@ -414,7 +402,7 @@ describe("codemode-core", () => { · 9 unmodified lines
414 includeSchemas: false,402 includeSchemas: false,
415 });403 });
416 expect(listed[0]?.path).toBe("github.repos.getRepo");404 expect(listed[0]?.path).toBe("github.repos.getRepo");
417 expect(listed[0]?.inputSchemaJson).toBeUndefined();405 expect(listed[0]?.inputSchema).toBeUndefined();
418 406
419 const discovered = yield* catalog.searchTools({407 const discovered = yield* catalog.searchTools({
420 query: "repository issues",408 query: "repository issues",
@@ -426,10 +414,7 @@ describe("codemode-core", () => { · 5 unmodified lines
426 path: asToolPath("github.repos.getRepo"),414 path: asToolPath("github.repos.getRepo"),
427 includeSchemas: true,415 includeSchemas: true,
428 });416 });
429 expect(described?.inputSchemaJson).toContain('"type"');417 expect(described?.inputSchema).toEqual({ type: "object" });
430
431 const bundle = yield* catalog.getSchemaBundle({ id: "bundle_1" });
432 expect(bundle?.id).toBe("bundle_1");
433 }),418 }),
434 );419 );
435 420
packages/codemode-core/src/schema-types.ts+16−0
@@ -123,3 +123,19 @@ export const typeSignatureFromSchemaJson = ( · 122 unmodified lines
123 return fallback;123 return fallback;
124 }124 }
125};125};
126
127export 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};
packages/codemode-core/src/system-tools.ts+0−29
@@ -6,7 +6,6 @@ import { toTool } from "./tool-map"; · 5 unmodified lines
6import {6import {
7 ToolDescriptorSchema,7 ToolDescriptorSchema,
8 ToolNamespaceSchema,8 ToolNamespaceSchema,
9 ToolSchemaBundleSchema,
10 type ToolCatalog,9 type ToolCatalog,
11 type ToolMap,10 type ToolMap,
12 type ToolPath,11 type ToolPath,
@@ -52,16 +51,6 @@ const describeToolOutputSchema = Schema.standardSchemaV1( · 39 unmodified lines
52 Schema.NullOr(ToolDescriptorSchema),51 Schema.NullOr(ToolDescriptorSchema),
53);52);
54 53
55const describeSchemaBundleInputSchema = Schema.standardSchemaV1(
56 Schema.Struct({
57 id: Schema.String,
58 }),
59);
60
61const describeSchemaBundleOutputSchema = Schema.standardSchemaV1(
62 Schema.NullOr(ToolSchemaBundleSchema),
63);
64
65const discoverInputSchema = Schema.standardSchemaV1(54const discoverInputSchema = Schema.standardSchemaV1(
66 Schema.Struct({55 Schema.Struct({
67 query: Schema.String,56 query: Schema.String,
@@ -181,24 +170,6 @@ export const createSystemToolMap = ( · 113 unmodified lines
181 },170 },
182 });171 });
183 172
184 tools["describe.schemaBundle"] = toTool({
185 tool: {
186 description: "Get a shared schema bundle by id",
187 inputSchema: describeSchemaBundleInputSchema,
188 outputSchema: describeSchemaBundleOutputSchema,
189 execute: ({ id }: { id: string }) =>
190 Effect.runPromise(
191 getPrimitives().describe.schemaBundle({
192 id,
193 }),
194 ),
195 },
196 metadata: {
197 sourceKey,
198 interaction: "auto",
199 },
200 });
201
202 tools.discover = toTool({173 tools.discover = toTool({
203 tool: {174 tool: {
204 description: "Search tools by intent and return ranked matches",175 description: "Search tools by intent and return ranked matches",
packages/codemode-core/src/tool-map.ts+24−25
@@ -2,7 +2,7 @@ import type { StandardSchemaV1 } from "@standard-schema/spec"; · 1 unmodified line
2import * as Data from "effect/Data";2import * as Data from "effect/Data";
3import * as Effect from "effect/Effect";3import * as Effect from "effect/Effect";
4import * as JSONSchema from "effect/JSONSchema";4import * as JSONSchema from "effect/JSONSchema";
5import { typeSignatureFromSchemaJson } from "./schema-types";5import { typeSignatureFromSchema } from "./schema-types";
6 6
7import type {7import type {
8 ElicitationRequest,8 ElicitationRequest,
@@ -336,7 +336,7 @@ export const toExecutorTool = wrapTool; · 327 unmodified lines
336const isToolDefinition = (value: ToolInput): value is ToolDefinition =>336const isToolDefinition = (value: ToolInput): value is ToolDefinition =>
337 typeof value === "object" && value !== null && "tool" in value;337 typeof value === "object" && value !== null && "tool" in value;
338 338
339const stringifySchema = (value: unknown): string | undefined => {339const deriveSchemaValue = (value: unknown): unknown | undefined => {
340 if (value === undefined || value === null) {340 if (value === undefined || value === null) {
341 return undefined;341 return undefined;
342 }342 }
@@ -347,20 +347,20 @@ const stringifySchema = (value: unknown): string | undefined => { · 4 unmodified lines
347 && value !== null347 && value !== null
348 && "~standard" in value348 && "~standard" in value
349 ) {349 ) {
350 return JSON.stringify(JSONSchema.make(value as any));350 return JSONSchema.make(value as any);
351 }351 }
352 352
353 return JSON.stringify(value);353 return value;
354 } catch {354 } catch {
355 return undefined;355 return undefined;
356 }356 }
357};357};
358 358
359const inferTypeFromSchemaJson = (359const inferTypeFromSchemaValue = (
360 schemaJson: string | undefined,360 schema: unknown,
361 fallback: string,361 fallback: string,
362 maxLength: number = 240,362 maxLength: number = 240,
363): string => typeSignatureFromSchemaJson(schemaJson, fallback, maxLength);363): string => typeSignatureFromSchema(schema, fallback, maxLength);
364 364
365export function createToolsFromRecord(input: {365export function createToolsFromRecord(input: {
366 tools: Record<string, ExecutableTool>;366 tools: Record<string, ExecutableTool>;
@@ -418,13 +418,13 @@ export function toolDescriptorsFromTools(input: { · 51 unmodified lines
418 return resolvedTools.map((entry) => {418 return resolvedTools.map((entry) => {
419 const metadata = entry.metadata;419 const metadata = entry.metadata;
420 const definition = entry.tool;420 const definition = entry.tool;
421 const inputSchemaJson =421 const inputSchema =
422 metadata?.inputSchemaJson422 metadata?.inputSchema
423 ?? stringifySchema(definition.inputSchema)423 ?? deriveSchemaValue(definition.inputSchema)
424 ?? stringifySchema(definition.parameters);424 ?? deriveSchemaValue(definition.parameters);
425 const outputSchemaJson =425 const outputSchema =
426 metadata?.outputSchemaJson426 metadata?.outputSchema
427 ?? stringifySchema(definition.outputSchema);427 ?? deriveSchemaValue(definition.outputSchema);
428 428
429 return {429 return {
430 path: entry.path,430 path: entry.path,
@@ -433,21 +433,20 @@ export function toolDescriptorsFromTools(input: { · 2 unmodified lines
433 interaction: metadata?.interaction,433 interaction: metadata?.interaction,
434 elicitation: metadata?.elicitation,434 elicitation: metadata?.elicitation,
435 inputType:435 inputType:
436 metadata?.inputType ?? inferTypeFromSchemaJson(inputSchemaJson, "unknown"),436 metadata?.inputType ?? inferTypeFromSchemaValue(inputSchema, "unknown"),
437 outputType:437 outputType:
438 metadata?.outputType ?? inferTypeFromSchemaJson(outputSchemaJson, "unknown"),438 metadata?.outputType ?? inferTypeFromSchemaValue(outputSchema, "unknown"),
439 inputSchemaJson,439 ...(inputSchema !== undefined ? { inputSchema } : {}),
440 outputSchemaJson,440 ...(outputSchema !== undefined ? { outputSchema } : {}),
441 ...(metadata?.schemaBundleId ? { schemaBundleId: metadata.schemaBundleId } : {}),441 ...(metadata?.exampleInput !== undefined
442 ...(metadata?.exampleInputJson442 ? { exampleInput: metadata.exampleInput }
443 ? { exampleInputJson: metadata.exampleInputJson }
444 : {}),443 : {}),
445 ...(metadata?.exampleOutputJson444 ...(metadata?.exampleOutput !== undefined
446 ? { exampleOutputJson: metadata.exampleOutputJson }445 ? { exampleOutput: metadata.exampleOutput }
447 : {}),446 : {}),
448 ...(metadata?.providerKind ? { providerKind: metadata.providerKind } : {}),447 ...(metadata?.providerKind ? { providerKind: metadata.providerKind } : {}),
449 ...(metadata?.providerDataJson448 ...(metadata?.providerData !== undefined
450 ? { providerDataJson: metadata.providerDataJson }449 ? { providerData: metadata.providerData }
451 : {}),450 : {}),
452 } satisfies ToolDescriptor;451 } satisfies ToolDescriptor;
453 });452 });
packages/codemode-core/src/types.ts+15−39
@@ -80,14 +80,13 @@ export type ToolMetadata = { · 79 unmodified lines
80 elicitation?: ElicitationRequest;80 elicitation?: ElicitationRequest;
81 inputType?: string;81 inputType?: string;
82 outputType?: string;82 outputType?: string;
83 inputSchemaJson?: string;83 inputSchema?: unknown;
84 outputSchemaJson?: string;84 outputSchema?: unknown;
85 schemaBundleId?: string;85 exampleInput?: unknown;
86 exampleInputJson?: string;86 exampleOutput?: unknown;
87 exampleOutputJson?: string;
88 sourceKey?: string;87 sourceKey?: string;
89 providerKind?: string;88 providerKind?: string;
90 providerDataJson?: string;89 providerData?: unknown;
91};90};
92 91
93export type ToolExecutionContext = {92export type ToolExecutionContext = {
@@ -134,13 +133,12 @@ export type ToolDescriptor = { · 40 unmodified lines
134 elicitation?: ElicitationRequest;133 elicitation?: ElicitationRequest;
135 inputType?: string;134 inputType?: string;
136 outputType?: string;135 outputType?: string;
137 inputSchemaJson?: string;136 inputSchema?: unknown;
138 outputSchemaJson?: string;137 outputSchema?: unknown;
139 schemaBundleId?: string;138 exampleInput?: unknown;
140 exampleInputJson?: string;139 exampleOutput?: unknown;
141 exampleOutputJson?: string;
142 providerKind?: string;140 providerKind?: string;
143 providerDataJson?: string;141 providerData?: unknown;
144};142};
145 143
146export const ToolDescriptorSchema = Schema.Struct({144export const ToolDescriptorSchema = Schema.Struct({
@@ -153,27 +151,12 @@ export const ToolDescriptorSchema = Schema.Struct({ · 6 unmodified lines
153 elicitation: Schema.optional(Schema.Unknown),151 elicitation: Schema.optional(Schema.Unknown),
154 inputType: Schema.optional(Schema.String),152 inputType: Schema.optional(Schema.String),
155 outputType: Schema.optional(Schema.String),153 outputType: Schema.optional(Schema.String),
156 inputSchemaJson: Schema.optional(Schema.String),154 inputSchema: Schema.optional(Schema.Unknown),
157 outputSchemaJson: Schema.optional(Schema.String),155 outputSchema: Schema.optional(Schema.Unknown),
158 schemaBundleId: Schema.optional(Schema.String),156 exampleInput: Schema.optional(Schema.Unknown),
159 exampleInputJson: Schema.optional(Schema.String),157 exampleOutput: Schema.optional(Schema.Unknown),
160 exampleOutputJson: Schema.optional(Schema.String),
161 providerKind: Schema.optional(Schema.String),158 providerKind: Schema.optional(Schema.String),
162 providerDataJson: Schema.optional(Schema.String),159 providerData: Schema.optional(Schema.Unknown),
163});
164
165export type ToolSchemaBundle = {
166 id: string;
167 kind: string;
168 hash: string;
169 refsJson: string;
170};
171
172export const ToolSchemaBundleSchema = Schema.Struct({
173 id: Schema.String,
174 kind: Schema.String,
175 hash: Schema.String,
176 refsJson: Schema.String,
177});160});
178 161
179export type ToolNamespace = {162export type ToolNamespace = {
@@ -217,10 +200,6 @@ export interface ToolCatalog { · 37 unmodified lines
217 includeSchemas: boolean;200 includeSchemas: boolean;
218 }): Effect.Effect<ToolDescriptor | null, unknown>;201 }): Effect.Effect<ToolDescriptor | null, unknown>;
219 202
220 getSchemaBundle(input: {
221 id: string;
222 }): Effect.Effect<ToolSchemaBundle | null, unknown>;
223
224 searchTools(input: {203 searchTools(input: {
225 query: string;204 query: string;
226 namespace?: string;205 namespace?: string;
@@ -249,9 +228,6 @@ export type DescribePrimitive = { · 22 unmodified lines
249 path: ToolPath;228 path: ToolPath;
250 includeSchemas?: boolean;229 includeSchemas?: boolean;
251 }): Effect.Effect<ToolDescriptor | null, unknown>;230 }): Effect.Effect<ToolDescriptor | null, unknown>;
252 schemaBundle(input: {
253 id: string;
254 }): Effect.Effect<ToolSchemaBundle | null, unknown>;
255};231};
256 232
257export type DiscoverPrimitive = (input: {233export type DiscoverPrimitive = (input: {
packages/codemode-google-discovery/src/google-discovery-document.ts+32−8
@@ -90,6 +90,18 @@ const asStringArray = (value: unknown): ReadonlyArray<string> => · 89 unmodified lines
90 })90 })
91 : [];91 : [];
92 92
93const 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
93const contentHash = (value: string): string =>105const contentHash = (value: string): string =>
94 sha256Hex(value);106 sha256Hex(value);
95 107
@@ -384,17 +396,29 @@ const manifestMethodFromDiscoveryMethod = (input: { · 288 unmodified lines
384 ...(methodRequestSchemaJson({396 ...(methodRequestSchemaJson({
385 method: input.method,397 method: input.method,
386 topLevelSchemas: input.topLevelSchemas,398 topLevelSchemas: input.topLevelSchemas,
387 }) ? { inputSchemaJson: methodRequestSchemaJson({399 })
388 method: input.method,400 ? {
389 topLevelSchemas: input.topLevelSchemas,401 inputSchema: parseJson(
390 }) } : {}),402 methodRequestSchemaJson({
403 method: input.method,
404 topLevelSchemas: input.topLevelSchemas,
405 }),
406 ),
407 }
408 : {}),
391 ...(methodResponseSchemaJson({409 ...(methodResponseSchemaJson({
392 method: input.method,410 method: input.method,
393 topLevelSchemas: input.topLevelSchemas,411 topLevelSchemas: input.topLevelSchemas,
394 }) ? { outputSchemaJson: methodResponseSchemaJson({412 })
395 method: input.method,413 ? {
396 topLevelSchemas: input.topLevelSchemas,414 outputSchema: parseJson(
397 }) } : {}),415 methodResponseSchemaJson({
416 method: input.method,
417 topLevelSchemas: input.topLevelSchemas,
418 }),
419 ),
420 }
421 : {}),
398 };422 };
399};423};
400 424
packages/codemode-google-discovery/src/google-discovery-tools.test.ts+2−2
@@ -117,8 +117,8 @@ describe("google discovery tools", () => { · 116 unmodified lines
117 expect(definitions.length).toBeGreaterThan(10);117 expect(definitions.length).toBeGreaterThan(10);
118 expect(valuesGet).toBeDefined();118 expect(valuesGet).toBeDefined();
119 expect(valuesGet?.method).toBe("get");119 expect(valuesGet?.method).toBe("get");
120 expect(valuesGet?.inputSchemaJson).toContain("\"spreadsheetId\"");120 expect(JSON.stringify(valuesGet?.inputSchema)).toContain("\"spreadsheetId\"");
121 expect(valuesGet?.outputSchemaJson).toContain("\"values\"");121 expect(JSON.stringify(valuesGet?.outputSchema)).toContain("\"values\"");
122 expect(Object.keys(manifest.schemaRefTable ?? {})).toContain(122 expect(Object.keys(manifest.schemaRefTable ?? {})).toContain(
123 "#/$defs/google/ValueRange",123 "#/$defs/google/ValueRange",
124 );124 );
packages/codemode-google-discovery/src/google-discovery-tools.ts+19−41
@@ -27,10 +27,10 @@ import { · 26 unmodified lines
27} from "./google-discovery-types";27} from "./google-discovery-types";
28 28
29const decodeGoogleDiscoveryToolProviderDataJson = Schema.decodeUnknownEither(29const decodeGoogleDiscoveryToolProviderDataJson = Schema.decodeUnknownEither(
30 Schema.parseJson(GoogleDiscoveryToolProviderDataSchema),30 GoogleDiscoveryToolProviderDataSchema,
31);31);
32 32
33export const googleDiscoveryProviderDataJsonFromDefinition = (33export const googleDiscoveryProviderDataFromDefinition = (
34 input: {34 input: {
35 service: string;35 service: string;
36 version: string;36 version: string;
@@ -38,8 +38,7 @@ export const googleDiscoveryProviderDataJsonFromDefinition = ( · 1 unmodified line
38 servicePath: string;38 servicePath: string;
39 definition: GoogleDiscoveryManifestMethod;39 definition: GoogleDiscoveryManifestMethod;
40 },40 },
41): string =>41): typeof GoogleDiscoveryToolProviderDataSchema.Type => ({
42 JSON.stringify({
43 kind: "google_discovery",42 kind: "google_discovery",
44 service: input.service,43 service: input.service,
45 version: input.version,44 version: input.version,
@@ -115,19 +114,14 @@ const setNestedSchemaProperty = ( · 69 unmodified lines
115};114};
116 115
117const materializeSchemaWithRefDefinitions = (input: {116const materializeSchemaWithRefDefinitions = (input: {
118 schemaJson: string | undefined;117 schema: unknown;
119 refTable?: Readonly<Record<string, unknown>>;118 refTable?: Readonly<Record<string, unknown>>;
120}): Record<string, unknown> => {119}): Record<string, unknown> => {
121 if (!input.schemaJson) {120 if (input.schema === undefined || input.schema === null) {
122 return {};121 return {};
123 }122 }
124 123
125 let rootSchema: Record<string, unknown>;124 const rootSchema = asRecord(input.schema);
126 try {
127 rootSchema = asRecord(JSON.parse(input.schemaJson) as unknown);
128 } catch {
129 return {};
130 }
131 125
132 if (!input.refTable || Object.keys(input.refTable).length === 0) {126 if (!input.refTable || Object.keys(input.refTable).length === 0) {
133 return rootSchema;127 return rootSchema;
@@ -250,14 +244,14 @@ const buildGoogleDiscoveryRequest = (input: { · 116 unmodified lines
250 defaultHeaders: Readonly<Record<string, string>>;244 defaultHeaders: Readonly<Record<string, string>>;
251 credentialPlacements: HttpRequestPlacements;245 credentialPlacements: HttpRequestPlacements;
252 baseUrl?: string | undefined;246 baseUrl?: string | undefined;
253 providerDataJson: string;247 providerData: typeof GoogleDiscoveryToolProviderDataSchema.Type;
254}): {248}): {
255 method: string;249 method: string;
256 url: URL;250 url: URL;
257 headers: Record<string, string>;251 headers: Record<string, string>;
258 body?: string;252 body?: string;
259} => {253} => {
260 const providerData = decodeGoogleDiscoveryToolProviderDataJson(input.providerDataJson);254 const providerData = decodeGoogleDiscoveryToolProviderDataJson(input.providerData);
261 if (providerData._tag === "Left") {255 if (providerData._tag === "Left") {
262 throw new Error("Invalid Google Discovery provider data");256 throw new Error("Invalid Google Discovery provider data");
263 }257 }
@@ -365,28 +359,12 @@ export type CreateGoogleDiscoveryToolFromDefinitionInput = { · 101 unmodified lines
365export const createGoogleDiscoveryToolFromDefinition = (359export const createGoogleDiscoveryToolFromDefinition = (
366 input: CreateGoogleDiscoveryToolFromDefinitionInput,360 input: CreateGoogleDiscoveryToolFromDefinitionInput,
367) => {361) => {
368 const providerDataJson = JSON.stringify({362 const providerData = googleDiscoveryProviderDataFromDefinition({
369 kind: "google_discovery",
370 service: input.service,363 service: input.service,
371 version: input.version,364 version: input.version,
372 toolId: input.definition.toolId,365 rootUrl: input.rootUrl,
373 rawToolId: input.definition.rawToolId,366 servicePath: input.servicePath,
374 methodId: input.definition.methodId,367 definition: input.definition,
375 group: input.definition.group,
376 leaf: input.definition.leaf,
377 invocation: {
378 method: input.definition.method,
379 path: input.definition.path,
380 flatPath: input.definition.flatPath,
381 rootUrl: input.rootUrl,
382 servicePath: input.servicePath,
383 parameters: input.definition.parameters,
384 requestSchemaId: input.definition.requestSchemaId,
385 responseSchemaId: input.definition.responseSchemaId,
386 scopes: input.definition.scopes,
387 supportsMediaUpload: input.definition.supportsMediaUpload,
388 supportsMediaDownload: input.definition.supportsMediaDownload,
389 },
390 });368 });
391 const credentialPlacements = normalizeCredentialPlacements({369 const credentialPlacements = normalizeCredentialPlacements({
392 credentialHeaders: input.credentialHeaders,370 credentialHeaders: input.credentialHeaders,
@@ -403,7 +381,7 @@ export const createGoogleDiscoveryToolFromDefinition = ( · 10 unmodified lines
403 }),381 }),
404 );382 );
405 const inputSchema = materializeSchemaWithRefDefinitions({383 const inputSchema = materializeSchemaWithRefDefinitions({
406 schemaJson: input.definition.inputSchemaJson,384 schema: input.definition.inputSchema,
407 refTable,385 refTable,
408 });386 });
409 387
@@ -411,15 +389,15 @@ export const createGoogleDiscoveryToolFromDefinition = ( · 1 unmodified line
411 interaction: input.definition.method === "get" || input.definition.method === "head"389 interaction: input.definition.method === "get" || input.definition.method === "head"
412 ? "auto"390 ? "auto"
413 : "required",391 : "required",
414 ...(input.definition.inputSchemaJson392 ...(input.definition.inputSchema !== undefined
415 ? { inputSchemaJson: input.definition.inputSchemaJson }393 ? { inputSchema: input.definition.inputSchema }
416 : {}),394 : {}),
417 ...(input.definition.outputSchemaJson395 ...(input.definition.outputSchema !== undefined
418 ? { outputSchemaJson: input.definition.outputSchemaJson }396 ? { outputSchema: input.definition.outputSchema }
419 : {}),397 : {}),
420 sourceKey: input.sourceKey,398 sourceKey: input.sourceKey,
421 providerKind: "google_discovery",399 providerKind: "google_discovery",
422 providerDataJson,400 providerData,
423 };401 };
424 402
425 return toTool({403 return toTool({
@@ -435,7 +413,7 @@ export const createGoogleDiscoveryToolFromDefinition = ( · 9 unmodified lines
435 defaultHeaders: input.defaultHeaders ?? {},413 defaultHeaders: input.defaultHeaders ?? {},
436 credentialPlacements,414 credentialPlacements,
437 baseUrl: input.baseUrl,415 baseUrl: input.baseUrl,
438 providerDataJson,416 providerData,
439 });417 });
440 let clientRequest = HttpClientRequest.make(418 let clientRequest = HttpClientRequest.make(
441 request.method as Parameters<typeof HttpClientRequest.make>[0],419 request.method as Parameters<typeof HttpClientRequest.make>[0],
packages/codemode-google-discovery/src/google-discovery-types.ts+2−2
@@ -74,8 +74,8 @@ export const GoogleDiscoveryManifestMethodSchema = Schema.Struct({ · 73 unmodified lines
74 scopes: Schema.Array(Schema.String),74 scopes: Schema.Array(Schema.String),
75 supportsMediaUpload: Schema.Boolean,75 supportsMediaUpload: Schema.Boolean,
76 supportsMediaDownload: Schema.Boolean,76 supportsMediaDownload: Schema.Boolean,
77 inputSchemaJson: Schema.optional(Schema.String),77 inputSchema: Schema.optional(Schema.Unknown),
78 outputSchemaJson: Schema.optional(Schema.String),78 outputSchema: Schema.optional(Schema.Unknown),
79});79});
80 80
81export const GoogleDiscoverySchemaRefTableSchema = Schema.Record({81export const GoogleDiscoverySchemaRefTableSchema = Schema.Record({
packages/codemode-google-discovery/src/index.ts+1−1
@@ -5,7 +5,7 @@ export { · 4 unmodified lines
5export {5export {
6 createGoogleDiscoveryToolFromDefinition,6 createGoogleDiscoveryToolFromDefinition,
7 decodeGoogleDiscoverySchemaRefTableJson,7 decodeGoogleDiscoverySchemaRefTableJson,
8 googleDiscoveryProviderDataJsonFromDefinition,8 googleDiscoveryProviderDataFromDefinition,
9 type CreateGoogleDiscoveryToolFromDefinitionInput,9 type CreateGoogleDiscoveryToolFromDefinitionInput,
10} from "./google-discovery-tools";10} from "./google-discovery-tools";
11export {11export {
packages/codemode-mcp/src/mcp-manifest.ts+4−18
@@ -7,8 +7,8 @@ export type McpToolManifestEntry = { · 6 unmodified lines
7 toolId: string;7 toolId: string;
8 toolName: string;8 toolName: string;
9 description: string | null;9 description: string | null;
10 inputSchemaJson?: string;10 inputSchema?: unknown;
11 outputSchemaJson?: string;11 outputSchema?: unknown;
12};12};
13 13
14export type McpToolManifest = {14export type McpToolManifest = {
@@ -34,18 +34,6 @@ const uniqueToolId = (value: string, byBase: Map<string, number>): string => { · 19 unmodified lines
34 return count === 1 ? base : `${base}_${count}`;34 return count === 1 ? base : `${base}_${count}`;
35};35};
36 36
37const 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
49const ListedMcpToolSchema = Schema.Struct({37const ListedMcpToolSchema = Schema.Struct({
50 name: Schema.String,38 name: Schema.String,
51 description: Schema.optional(Schema.NullOr(Schema.String)),39 description: Schema.optional(Schema.NullOr(Schema.String)),
@@ -85,10 +73,8 @@ export const extractMcpToolManifestFromListToolsResult = ( · 33 unmodified lines
85 toolId: uniqueToolId(toolName, byBase),73 toolId: uniqueToolId(toolName, byBase),
86 toolName,74 toolName,
87 description: tool.description ?? null,75 description: tool.description ?? null,
88 inputSchemaJson:76 inputSchema: tool.inputSchema ?? tool.parameters,
89 stringifyJson(tool.inputSchema)77 outputSchema: tool.outputSchema,
90 ?? stringifyJson(tool.parameters),
91 outputSchemaJson: stringifyJson(tool.outputSchema),
92 };78 };
93 })79 })
94 .filter((tool): tool is McpToolManifestEntry => tool !== null);80 .filter((tool): tool is McpToolManifestEntry => tool !== null);
packages/codemode-mcp/src/mcp-tools.test.ts+1−1
@@ -230,7 +230,7 @@ describe("codemode-mcp", () => { · 229 unmodified lines
230 230
231 const toolDefinition = resolveToolDefinition(discovered.tools["source.mcp.echo"]!);231 const toolDefinition = resolveToolDefinition(discovered.tools["source.mcp.echo"]!);
232 expect(toolDefinition.metadata?.sourceKey).toBe("mcp.demo");232 expect(toolDefinition.metadata?.sourceKey).toBe("mcp.demo");
233 expect(toolDefinition.metadata?.inputSchemaJson).toContain("value");233 expect(JSON.stringify(toolDefinition.metadata?.inputSchema)).toContain("value");
234 234
235 const invoke = resolveToolExecutor(discovered.tools, "source.mcp.echo");235 const invoke = resolveToolExecutor(discovered.tools, "source.mcp.echo");
236 const invocationResult = yield* Effect.promise(() => invoke({ value: "hello" }));236 const invocationResult = yield* Effect.promise(() => invoke({ value: "hello" }));
packages/codemode-mcp/src/mcp-tools.ts+10−6
@@ -93,13 +93,13 @@ const isExecutionSuspendedCause = (cause: unknown): boolean => · 92 unmodified lines
93const toDetails = (cause: unknown): string =>93const toDetails = (cause: unknown): string =>
94 cause instanceof Error ? cause.message : String(cause);94 cause instanceof Error ? cause.message : String(cause);
95 95
96const inputSchemaFromManifest = (inputSchemaJson: string | undefined) => {96const inputSchemaFromManifest = (inputSchema: unknown) => {
97 if (!inputSchemaJson) {97 if (inputSchema === undefined || inputSchema === null) {
98 return unknownInputSchema;98 return unknownInputSchema;
99 }99 }
100 100
101 try {101 try {
102 return standardSchemaFromJsonSchema(JSON.parse(inputSchemaJson), {102 return standardSchemaFromJsonSchema(inputSchema, {
103 vendor: "mcp",103 vendor: "mcp",
104 fallback: unknownInputSchema,104 fallback: unknownInputSchema,
105 });105 });
@@ -470,7 +470,7 @@ export const createMcpToolsFromManifest = (input: { · 364 unmodified lines
470 toTool({470 toTool({
471 tool: {471 tool: {
472 description: entry.description ?? `MCP tool: ${entry.toolName}`,472 description: entry.description ?? `MCP tool: ${entry.toolName}`,
473 inputSchema: inputSchemaFromManifest(entry.inputSchemaJson),473 inputSchema: inputSchemaFromManifest(entry.inputSchema),
474 execute: async (args: unknown, executionContext?: ToolExecutionContext) => {474 execute: async (args: unknown, executionContext?: ToolExecutionContext) => {
475 const exit = await Effect.runPromiseExit(475 const exit = await Effect.runPromiseExit(
476 withConnectionEffect({476 withConnectionEffect({
@@ -504,8 +504,12 @@ export const createMcpToolsFromManifest = (input: { · 27 unmodified lines
504 },504 },
505 metadata: {505 metadata: {
506 sourceKey,506 sourceKey,
507 inputSchemaJson: entry.inputSchemaJson,507 ...(entry.inputSchema !== undefined
508 outputSchemaJson: entry.outputSchemaJson,508 ? { inputSchema: entry.inputSchema }
509 : {}),
510 ...(entry.outputSchema !== undefined
511 ? { outputSchema: entry.outputSchema }
512 : {}),
509 },513 },
510 }),514 }),
511 ] as const;515 ] as const;
packages/codemode-openapi/src/index.ts+3−3
@@ -8,13 +8,13 @@ export { · 7 unmodified lines
8} from "./openapi-extraction";8} from "./openapi-extraction";
9export {9export {
10 compileOpenApiToolDefinitions,10 compileOpenApiToolDefinitions,
11 openApiProviderDataJsonFromDefinition,11 openApiProviderDataFromDefinition,
12 OpenApiToolDefinitionSchema,12 OpenApiToolDefinitionSchema,
13 type OpenApiToolDefinition,13 type OpenApiToolDefinition,
14} from "./openapi-definitions";14} from "./openapi-definitions";
15export {15export {
16 buildOpenApiToolPresentation,16 buildOpenApiToolPresentation,
17 openApiOutputTypeSignatureFromSchemaJson,17 openApiOutputTypeSignatureFromSchema,
18 type OpenApiToolPresentation,18 type OpenApiToolPresentation,
19} from "./openapi-tool-presentation";19} from "./openapi-tool-presentation";
20export {20export {
@@ -23,7 +23,7 @@ export { · 2 unmodified lines
23 createOpenApiToolsFromManifest,23 createOpenApiToolsFromManifest,
24 createOpenApiToolsFromSpec,24 createOpenApiToolsFromSpec,
25} from "./openapi-tools";25} from "./openapi-tools";
26export { resolveSchemaJsonWithRefHints, resolveTypingSchemasWithRefHints } from "./openapi-schema-refs";26export { resolveSchemaWithRefHints, resolveTypingSchemasWithRefHints } from "./openapi-schema-refs";
27export {27export {
28 OPEN_API_HTTP_METHODS,28 OPEN_API_HTTP_METHODS,
29 OPEN_API_PARAMETER_LOCATIONS,29 OPEN_API_PARAMETER_LOCATIONS,
packages/codemode-openapi/src/openapi-definitions.ts+2−3
@@ -236,10 +236,9 @@ export const compileOpenApiToolDefinitions = ( · 235 unmodified lines
236 );236 );
237};237};
238 238
239export const openApiProviderDataJsonFromDefinition = (239export const openApiProviderDataFromDefinition = (
240 definition: OpenApiToolDefinition,240 definition: OpenApiToolDefinition,
241): string =>241): typeof OpenApiToolProviderDataSchema.Type => ({
242 JSON.stringify({
243 kind: "openapi",242 kind: "openapi",
244 toolId: definition.toolId,243 toolId: definition.toolId,
245 rawToolId: definition.rawToolId,244 rawToolId: definition.rawToolId,
packages/codemode-openapi/src/openapi-extraction.real-specs.test.ts+3−3
@@ -26,8 +26,8 @@ type ParsedInputSchema = { · 25 unmodified lines
26 required?: Array<string>;26 required?: Array<string>;
27};27};
28 28
29const parseInputSchema = (inputSchemaJson: string | undefined): ParsedInputSchema | null =>29const parseInputSchema = (inputSchema: unknown): ParsedInputSchema | null =>
30 inputSchemaJson ? (JSON.parse(inputSchemaJson) as ParsedInputSchema) : null;30 inputSchema ? (inputSchema as ParsedInputSchema) : null;
31 31
32const expectedTools = [32const expectedTools = [
33 {33 {
@@ -89,7 +89,7 @@ describe("openapi-extraction real specs", () => { · 55 unmodified lines
89 expect(tool?.path).toBe(expectedTool.path);89 expect(tool?.path).toBe(expectedTool.path);
90 expect(tool?.tags).toEqual(expectedTool.tags);90 expect(tool?.tags).toEqual(expectedTool.tags);
91 91
92 const inputSchema = parseInputSchema(tool?.typing?.inputSchemaJson);92 const inputSchema = parseInputSchema(tool?.typing?.inputSchema);
93 expect(inputSchema?.type).toBe("object");93 expect(inputSchema?.type).toBe("object");
94 expect(Object.keys(inputSchema?.properties ?? {})).toEqual(94 expect(Object.keys(inputSchema?.properties ?? {})).toEqual(
95 expect.arrayContaining([...expectedTool.requiredInputs]),95 expect.arrayContaining([...expectedTool.requiredInputs]),
packages/codemode-openapi/src/openapi-schema-refs.ts+15−25
@@ -2,8 +2,8 @@ type JsonObject = Record<string, unknown>; · 1 unmodified line
2type RefHintValue = string | JsonObject;2type RefHintValue = string | JsonObject;
3 3
4type DiscoveryTypingLike = {4type DiscoveryTypingLike = {
5 inputSchemaJson?: string;5 inputSchema?: unknown;
6 outputSchemaJson?: string;6 outputSchema?: unknown;
7};7};
8 8
9const isJsonObject = (value: unknown): value is JsonObject =>9const isJsonObject = (value: unknown): value is JsonObject =>
@@ -92,50 +92,40 @@ const resolveSchemaNode = ( · 82 unmodified lines
92 return next;92 return next;
93};93};
94 94
95export const resolveSchemaJsonWithRefHints = (95export const resolveSchemaWithRefHints = (
96 schemaJson: string | undefined,96 schema: unknown,
97 refHintTable: Readonly<Record<string, RefHintValue>> | undefined,97 refHintTable: Readonly<Record<string, RefHintValue>> | undefined,
98): string | null => {98): unknown | null => {
99 if (!schemaJson) {99 if (schema === undefined || schema === null) {
100 return null;100 return null;
101 }101 }
102 102
103 const parsedSchema = parseJson(schemaJson);
104 if (parsedSchema === null) {
105 return schemaJson;
106 }
107
108 if (!refHintTable || Object.keys(refHintTable).length === 0) {103 if (!refHintTable || Object.keys(refHintTable).length === 0) {
109 return schemaJson;104 return schema;
110 }105 }
111 106
112 const resolved = resolveSchemaNode(107 const resolved = resolveSchemaNode(
113 parsedSchema,108 schema,
114 refHintTable,109 refHintTable,
115 new Map<string, unknown>(),110 new Map<string, unknown>(),
116 new Set<string>(),111 new Set<string>(),
117 );112 );
118 113 return resolved;
119 try {
120 return JSON.stringify(resolved);
121 } catch {
122 return schemaJson;
123 }
124};114};
125 115
126export const resolveTypingSchemasWithRefHints = (116export const resolveTypingSchemasWithRefHints = (
127 typing: DiscoveryTypingLike | undefined,117 typing: DiscoveryTypingLike | undefined,
128 refHintTable: Readonly<Record<string, RefHintValue>> | undefined,118 refHintTable: Readonly<Record<string, RefHintValue>> | undefined,
129): {119): {
130 inputSchemaJson: string | null;120 inputSchema: unknown | null;
131 outputSchemaJson: string | null;121 outputSchema: unknown | null;
132} => ({122} => ({
133 inputSchemaJson: resolveSchemaJsonWithRefHints(123 inputSchema: resolveSchemaWithRefHints(
134 typing?.inputSchemaJson,124 typing?.inputSchema,
135 refHintTable,125 refHintTable,
136 ),126 ),
137 outputSchemaJson: resolveSchemaJsonWithRefHints(127 outputSchema: resolveSchemaWithRefHints(
138 typing?.outputSchemaJson,128 typing?.outputSchema,
139 refHintTable,129 refHintTable,
140 ),130 ),
141});131});
packages/codemode-openapi/src/openapi-tool-presentation.ts+71−49
@@ -1,22 +1,16 @@
1import { typeSignatureFromSchemaJson } from "@executor/codemode-core";1import { typeSignatureFromSchema } from "@executor/codemode-core";
2 2
3import type {3import type {
4 OpenApiExample,4 OpenApiExample,
5 OpenApiInvocationPayload,
5 OpenApiToolDocumentation,6 OpenApiToolDocumentation,
7 OpenApiToolProviderData,
6} from "./openapi-types";8} from "./openapi-types";
7import {9import {
8 openApiProviderDataJsonFromDefinition,10 openApiProviderDataFromDefinition,
9 type OpenApiToolDefinition,11 type OpenApiToolDefinition,
10} from "./openapi-definitions";12} from "./openapi-definitions";
11 13
12const parseJson = (value: string): unknown | undefined => {
13 try {
14 return JSON.parse(value) as unknown;
15 } catch {
16 return undefined;
17 }
18};
19
20const asRecord = (value: unknown): Record<string, unknown> =>14const asRecord = (value: unknown): Record<string, unknown> =>
21 typeof value === "object" && value !== null && !Array.isArray(value)15 typeof value === "object" && value !== null && !Array.isArray(value)
22 ? (value as Record<string, unknown>)16 ? (value as Record<string, unknown>)
@@ -32,33 +26,62 @@ const isStrictEmptyObjectSchema = (value: unknown): boolean => { · 9 unmodified lines
32 return Object.keys(properties).length === 0 && schema.additionalProperties === false;26 return Object.keys(properties).length === 0 && schema.additionalProperties === false;
33};27};
34 28
35export const openApiOutputTypeSignatureFromSchemaJson = (29export const openApiOutputTypeSignatureFromSchema = (
36 schemaJson: string | undefined,30 schema: unknown,
37 maxLength: number = 320,31 maxLength: number = 320,
38 ): string => {32 ): string => {
39 if (!schemaJson) {33 if (schema === undefined || schema === null) {
40 return "void";34 return "void";
41 }35 }
42 36
43 const parsed = parseJson(schemaJson);37 if (isStrictEmptyObjectSchema(schema)) {
44 if (parsed === undefined) {
45 return "unknown";
46 }
47
48 if (isStrictEmptyObjectSchema(parsed)) {
49 return "{}";38 return "{}";
50 }39 }
51 40
52 return typeSignatureFromSchemaJson(schemaJson, "unknown", maxLength);41 return typeSignatureFromSchema(schema, "unknown", maxLength);
53};42};
54 43
55const firstExample = (44const firstExample = (
56 examples: ReadonlyArray<OpenApiExample> | undefined,45 examples: ReadonlyArray<OpenApiExample> | undefined,
57): OpenApiExample | undefined => examples?.[0];46): OpenApiExample | undefined => examples?.[0];
58 47
59const buildExampleInputJson = (48const 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
82const buildExampleInput = (
60 documentation: OpenApiToolDocumentation | undefined,83 documentation: OpenApiToolDocumentation | undefined,
61): string | undefined => {84): Record<string, unknown> | undefined => {
62 if (!documentation) {85 if (!documentation) {
63 return undefined;86 return undefined;
64 }87 }
@@ -71,52 +94,51 @@ const buildExampleInputJson = ( · 6 unmodified lines
71 continue;94 continue;
72 }95 }
73 96
74 const parsedValue = parseJson(example.valueJson);97 input[parameter.name] = JSON.parse(example.valueJson) as unknown;
75 if (parsedValue !== undefined) {
76 input[parameter.name] = parsedValue;
77 }
78 }98 }
79 99
80 const requestBodyExample = firstExample(documentation.requestBody?.examples);100 const requestBodyExample = firstExample(documentation.requestBody?.examples);
81 if (requestBodyExample) {101 if (requestBodyExample) {
82 const parsedValue = parseJson(requestBodyExample.valueJson);102 input.body = JSON.parse(requestBodyExample.valueJson) as unknown;
83 if (parsedValue !== undefined) {
84 input.body = parsedValue;
85 }
86 }103 }
87 104
88 return Object.keys(input).length > 0 ? JSON.stringify(input) : undefined;105 return Object.keys(input).length > 0 ? input : undefined;
89};106};
90 107
91const buildExampleOutputJson = (108const buildExampleOutput = (
92 documentation: OpenApiToolDocumentation | undefined,109 documentation: OpenApiToolDocumentation | undefined,
93): string | undefined => firstExample(documentation?.response?.examples)?.valueJson;110): unknown | undefined => {
111 const example = firstExample(documentation?.response?.examples)?.valueJson;
112 return example ? JSON.parse(example) as unknown : undefined;
113};
94 114
95export type OpenApiToolPresentation = {115export type OpenApiToolPresentation = {
96 inputType: string;116 inputType: string;
97 outputType: string;117 outputType: string;
98 inputSchemaJson?: string;118 inputSchema?: unknown;
99 outputSchemaJson?: string;119 outputSchema?: unknown;
100 exampleInputJson?: string;120 exampleInput?: unknown;
101 exampleOutputJson?: string;121 exampleOutput?: unknown;
102 providerDataJson: string;122 providerData: OpenApiToolProviderData;
103};123};
104 124
105export const buildOpenApiToolPresentation = (input: {125export const buildOpenApiToolPresentation = (input: {
106 definition: OpenApiToolDefinition;126 definition: OpenApiToolDefinition;
107}): OpenApiToolPresentation => {127}): OpenApiToolPresentation => {
108 const inputSchemaJson = input.definition.typing?.inputSchemaJson;128 const inputSchema =
109 const outputSchemaJson = input.definition.typing?.outputSchemaJson;129 input.definition.typing?.inputSchema
110 const exampleInputJson = buildExampleInputJson(input.definition.documentation);130 ?? fallbackInputSchemaFromInvocation(input.definition.invocation);
111 const exampleOutputJson = buildExampleOutputJson(input.definition.documentation);131 const outputSchema = input.definition.typing?.outputSchema;
132 const exampleInput = buildExampleInput(input.definition.documentation);
133 const exampleOutput = buildExampleOutput(input.definition.documentation);
112 134
113 return {135 return {
114 inputType: typeSignatureFromSchemaJson(inputSchemaJson, "unknown", Infinity),136 inputType: typeSignatureFromSchema(inputSchema, "unknown", Infinity),
115 outputType: openApiOutputTypeSignatureFromSchemaJson(outputSchemaJson, Infinity),137 outputType: openApiOutputTypeSignatureFromSchema(outputSchema, Infinity),
116 ...(inputSchemaJson ? { inputSchemaJson } : {}),138 ...(inputSchema !== undefined ? { inputSchema } : {}),
117 ...(outputSchemaJson ? { outputSchemaJson } : {}),139 ...(outputSchema !== undefined ? { outputSchema } : {}),
118 ...(exampleInputJson ? { exampleInputJson } : {}),140 ...(exampleInput !== undefined ? { exampleInput } : {}),
119 ...(exampleOutputJson ? { exampleOutputJson } : {}),141 ...(exampleOutput !== undefined ? { exampleOutput } : {}),
120 providerDataJson: openApiProviderDataJsonFromDefinition(input.definition),142 providerData: openApiProviderDataFromDefinition(input.definition),
121 };143 };
122};144};
packages/codemode-openapi/src/openapi-tools.test.ts+2−2
@@ -291,8 +291,8 @@ describe("openapi-tools", () => { · 290 unmodified lines
291 const resolvedGet = resolveToolDefinition(tools["source.demo.repos.getRepo"]!);291 const resolvedGet = resolveToolDefinition(tools["source.demo.repos.getRepo"]!);
292 expect(resolvedGet.metadata?.sourceKey).toBe("api.demo");292 expect(resolvedGet.metadata?.sourceKey).toBe("api.demo");
293 expect(resolvedGet.metadata?.providerKind).toBe("openapi");293 expect(resolvedGet.metadata?.providerKind).toBe("openapi");
294 expect(resolvedGet.metadata?.providerDataJson).toContain('"group":"repos"');294 expect(resolvedGet.metadata?.providerData).toMatchObject({ group: "repos" });
295 expect(resolvedGet.metadata?.inputSchemaJson).toBeDefined();295 expect(resolvedGet.metadata?.inputSchema).toBeDefined();
296 }),296 }),
297 );297 );
298 298
packages/codemode-openapi/src/openapi-tools.ts+23−15
@@ -23,7 +23,7 @@ import { · 22 unmodified lines
23 type OpenApiToolDefinition,23 type OpenApiToolDefinition,
24} from "./openapi-definitions";24} from "./openapi-definitions";
25import { buildOpenApiToolPresentation } from "./openapi-tool-presentation";25import { buildOpenApiToolPresentation } from "./openapi-tool-presentation";
26import { resolveSchemaJsonWithRefHints } from "./openapi-schema-refs";26import { resolveSchemaWithRefHints } from "./openapi-schema-refs";
27import {27import {
28 extractOpenApiManifest,28 extractOpenApiManifest,
29 type OpenApiExtractionError,29 type OpenApiExtractionError,
@@ -262,21 +262,21 @@ const summarizeHttpResponseBody = (body: unknown): string | null => { · 232 unmodified lines
262 }262 }
263};263};
264 264
265const inputSchemaFromTypingJson = (input: {265const inputSchemaFromTyping = (input: {
266 inputSchemaJson: string | undefined;266 inputSchema: unknown;
267 refHintTable?: Readonly<OpenApiRefHintTable>;267 refHintTable?: Readonly<OpenApiRefHintTable>;
268}) => {268}) => {
269 const resolvedSchemaJson = resolveSchemaJsonWithRefHints(269 const resolvedSchema = resolveSchemaWithRefHints(
270 input.inputSchemaJson,270 input.inputSchema,
271 input.refHintTable,271 input.refHintTable,
272 ) ?? input.inputSchemaJson;272 ) ?? input.inputSchema;
273 273
274 if (!resolvedSchemaJson) {274 if (resolvedSchema === undefined || resolvedSchema === null) {
275 return unknownInputSchema;275 return unknownInputSchema;
276 }276 }
277 277
278 try {278 try {
279 return standardSchemaFromJsonSchema(JSON.parse(resolvedSchemaJson), {279 return standardSchemaFromJsonSchema(resolvedSchema, {
280 vendor: "openapi",280 vendor: "openapi",
281 fallback: unknownInputSchema,281 fallback: unknownInputSchema,
282 });282 });
@@ -469,8 +469,8 @@ export const createOpenApiToolFromDefinition = ( · 186 unmodified lines
469 return toTool({469 return toTool({
470 tool: {470 tool: {
471 description: input.definition.description,471 description: input.definition.description,
472 inputSchema: inputSchemaFromTypingJson({472 inputSchema: inputSchemaFromTyping({
473 inputSchemaJson: presentation.inputSchemaJson ?? input.definition.typing?.inputSchemaJson,473 inputSchema: presentation.inputSchema ?? input.definition.typing?.inputSchema,
474 refHintTable: input.refHintTable,474 refHintTable: input.refHintTable,
475 }),475 }),
476 execute: async (args: unknown) => {476 execute: async (args: unknown) => {
@@ -526,12 +526,20 @@ export const createOpenApiToolFromDefinition = ( · 49 unmodified lines
526 sourceKey: input.sourceKey,526 sourceKey: input.sourceKey,
527 inputType: presentation.inputType,527 inputType: presentation.inputType,
528 outputType: presentation.outputType,528 outputType: presentation.outputType,
529 inputSchemaJson: presentation.inputSchemaJson,529 ...((presentation.inputSchema ?? input.definition.typing?.inputSchema) !== undefined
530 outputSchemaJson: presentation.outputSchemaJson,530 ? { inputSchema: presentation.inputSchema ?? input.definition.typing?.inputSchema }
531 exampleInputJson: presentation.exampleInputJson,531 : {}),
532 exampleOutputJson: presentation.exampleOutputJson,532 ...((presentation.outputSchema ?? input.definition.typing?.outputSchema) !== undefined
533 ? { outputSchema: presentation.outputSchema ?? input.definition.typing?.outputSchema }
534 : {}),
535 ...(presentation.exampleInput !== undefined
536 ? { exampleInput: presentation.exampleInput }
537 : {}),
538 ...(presentation.exampleOutput !== undefined
539 ? { exampleOutput: presentation.exampleOutput }
540 : {}),
533 providerKind: "openapi",541 providerKind: "openapi",
534 providerDataJson: presentation.providerDataJson,542 providerData: presentation.providerData,
535 },543 },
536 });544 });
537};545};
packages/codemode-openapi/src/openapi-types.ts+2−2
@@ -58,8 +58,8 @@ export const OpenApiInvocationPayloadSchema = Schema.Struct({ · 57 unmodified lines
58});58});
59 59
60export const DiscoveryTypingPayloadSchema = Schema.Struct({60export const DiscoveryTypingPayloadSchema = Schema.Struct({
61 inputSchemaJson: Schema.optional(Schema.String),61 inputSchema: Schema.optional(Schema.Unknown),
62 outputSchemaJson: Schema.optional(Schema.String),62 outputSchema: Schema.optional(Schema.Unknown),
63 refHintKeys: Schema.optional(Schema.Array(Schema.String)),63 refHintKeys: Schema.optional(Schema.Array(Schema.String)),
64});64});
65 65
packages/control-plane/package.json+1−0
@@ -5,6 +5,7 @@ · 4 unmodified lines
5 "exports": {5 "exports": {
6 ".": "./src/index.ts",6 ".": "./src/index.ts",
7 "./client": "./src/client.ts",7 "./client": "./src/client.ts",
8 "./ir": "./src/ir/index.ts",
8 "./schema": "./src/schema/index.ts"9 "./schema": "./src/schema/index.ts"
9 },10 },
10 "imports": {11 "imports": {
packages/control-plane/src/api/sources/api.ts+0−11
@@ -8,7 +8,6 @@ import { · 7 unmodified lines
8 SourceIdSchema,8 SourceIdSchema,
9 SourceInspectionDiscoverPayloadSchema,9 SourceInspectionDiscoverPayloadSchema,
10 SourceInspectionDiscoverResultSchema,10 SourceInspectionDiscoverResultSchema,
11 SourceInspectionSchemaBundleSchema,
12 SourceInspectionSchema,11 SourceInspectionSchema,
13 SourceInspectionToolDetailSchema,12 SourceInspectionToolDetailSchema,
14 SourceImportAuthPolicySchema,13 SourceImportAuthPolicySchema,
@@ -80,7 +79,6 @@ export type UpdateSourcePayload = typeof UpdateSourcePayloadSchema.Type; · 65 unmodified lines
80const workspaceIdParam = HttpApiSchema.param("workspaceId", WorkspaceIdSchema);79const workspaceIdParam = HttpApiSchema.param("workspaceId", WorkspaceIdSchema);
81const sourceIdParam = HttpApiSchema.param("sourceId", SourceIdSchema);80const sourceIdParam = HttpApiSchema.param("sourceId", SourceIdSchema);
82const toolPathParam = HttpApiSchema.param("toolPath", Schema.String);81const toolPathParam = HttpApiSchema.param("toolPath", Schema.String);
83const schemaBundleIdParam = HttpApiSchema.param("schemaBundleId", Schema.String);
84 82
85const CredentialPageUrlParamsSchema = Schema.Struct({83const CredentialPageUrlParamsSchema = Schema.Struct({
86 interactionId: ExecutionInteractionIdSchema,84 interactionId: ExecutionInteractionIdSchema,
@@ -242,15 +240,6 @@ export class SourcesApi extends HttpApiGroup.make("sources") · 155 unmodified lines
242 .addError(ControlPlaneNotFoundError)240 .addError(ControlPlaneNotFoundError)
243 .addError(ControlPlaneStorageError),241 .addError(ControlPlaneStorageError),
244 )242 )
245 .add(
246 HttpApiEndpoint.get("inspectionSchemaBundle")`/workspaces/${workspaceIdParam}/sources/${sourceIdParam}/inspection/schema-bundles/${schemaBundleIdParam}`
247 .addSuccess(SourceInspectionSchemaBundleSchema)
248 .addError(ControlPlaneBadRequestError)
249 .addError(ControlPlaneUnauthorizedError)
250 .addError(ControlPlaneForbiddenError)
251 .addError(ControlPlaneNotFoundError)
252 .addError(ControlPlaneStorageError),
253 )
254 .add(243 .add(
255 HttpApiEndpoint.post("inspectionDiscover")`/workspaces/${workspaceIdParam}/sources/${sourceIdParam}/inspection/discover`244 HttpApiEndpoint.post("inspectionDiscover")`/workspaces/${workspaceIdParam}/sources/${sourceIdParam}/inspection/discover`
256 .setPayload(SourceInspectionDiscoverPayloadSchema)245 .setPayload(SourceInspectionDiscoverPayloadSchema)
packages/control-plane/src/api/sources/http.ts+0−12
@@ -16,7 +16,6 @@ import { · 15 unmodified lines
16import {16import {
17 discoverSourceInspectionTools,17 discoverSourceInspectionTools,
18 getSourceInspection,18 getSourceInspection,
19 getSourceInspectionSchemaBundle,
20 getSourceInspectionToolDetail,19 getSourceInspectionToolDetail,
21} from "../../runtime/source-inspection";20} from "../../runtime/source-inspection";
22import {21import {
@@ -750,17 +749,6 @@ export const ControlPlaneSourcesLive = HttpApiBuilder.group( · 727 unmodified lines
750 ),749 ),
751 ),750 ),
752 )751 )
753 .handle("inspectionSchemaBundle", ({ path }) =>
754 resolveRequestedLocalWorkspace("sources.inspection_schema_bundle", path.workspaceId).pipe(
755 Effect.zipRight(
756 getSourceInspectionSchemaBundle({
757 workspaceId: path.workspaceId,
758 sourceId: path.sourceId,
759 schemaBundleId: path.schemaBundleId,
760 }),
761 ),
762 ),
763 )
764 .handle("inspectionDiscover", ({ path, payload }) =>752 .handle("inspectionDiscover", ({ path, payload }) =>
765 resolveRequestedLocalWorkspace("sources.inspection_discover", path.workspaceId).pipe(753 resolveRequestedLocalWorkspace("sources.inspection_discover", path.workspaceId).pipe(
766 Effect.zipRight(754 Effect.zipRight(
packages/control-plane/src/index.ts+1−0
@@ -2,3 +2,4 @@ export * from "./schema/index"; · 1 unmodified line
2export * from "./api/index";2export * from "./api/index";
3export * from "./runtime/index";3export * from "./runtime/index";
4export * from "./client";4export * from "./client";
5export * as ir from "./ir/index";
packages/control-plane/src/ir/README.md+33−0
@@ -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
14Importers should move toward:
15
16`source -> CatalogFragmentV1 -> merge -> CatalogV1 -> projectors -> InvocationPlan`
17
18The existing runtime-specific compiled payload blobs are no longer the target semantic center.
19
20## Package Surface
21
22Use 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
packages/control-plane/src/ir/catalog.test.ts+711−0
@@ -0,0 +1,711 @@
1import { describe, expect, it } from "@effect/vitest";
2
3import {
4 buildInvocationPlan,
5 createCatalogSnapshotV1,
6 createEmptyCatalogV1,
7 decodeCatalogSnapshotV1,
8 mergeCatalogFragments,
9 projectCatalogForAgentSdk,
10 projectSymbolExpandedView,
11 projectSymbolShallowView,
12 validateCatalogInvariants,
13} from "./catalog";
14import {
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";
30import type { CatalogV1, CatalogFragmentV1, ProvenanceRef } from "./model";
31
32const put = <K extends string, V>(record: Record<K, V>, key: K, value: V) => {
33 record[key] = value;
34};
35
36const docId = DocumentIdSchema.make("doc_primary");
37const baseProvenance = (pointer: string): ProvenanceRef[] => [{
38 relation: "declared",
39 documentId: docId,
40 pointer,
41}];
42
43const 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
329const 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
546describe("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});
packages/control-plane/src/ir/catalog.ts+1628−0
@@ -0,0 +1,1628 @@
1import { sha256Hex } from "@executor/codemode-core";
2import * as ParseResult from "effect/ParseResult";
3import { Schema } from "effect";
4
5import {
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";
31import {
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
58export 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
77export 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
88export 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
101export interface SymbolShallowView {
102 symbolId: SymbolId;
103 kind: IrSymbol["kind"];
104 title?: string;
105 summary?: string;
106 edges: Array<{ label: string; targetId: SymbolId }>;
107}
108
109export interface SymbolExpandedView {
110 symbolId: SymbolId;
111 symbol: IrSymbol;
112 diagnostics?: ImportDiagnostic[];
113}
114
115export 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
131export interface ProjectedCatalog {
132 catalog: CatalogV1;
133 toolDescriptors: Record<CapabilityId, ToolDescriptor>;
134 searchDocs: Record<CapabilityId, ToolSearchDoc>;
135 capabilityViews: Record<CapabilityId, CapabilitySummaryView>;
136}
137
138export 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
155export type DecodeCatalogError = Error;
156
157type MutableScopeDefaults = {
158 servers?: ScopeDefaults["servers"];
159 auth?: ScopeDefaults["auth"];
160 parameterIds?: ParameterSymbolId[];
161 headerIds?: HeaderSymbolId[];
162 variables?: Record<string, string>;
163};
164
165type CatalogSnapshotV1 = {
166 version: "ir.v1.snapshot";
167 import: ImportMetadata;
168 catalog: CatalogV1;
169};
170
171const decodeCatalogSync = Schema.decodeUnknownSync(
172 CatalogV1Schema as Schema.Schema<CatalogV1, unknown, never>,
173);
174const decodeCatalogSnapshotSync = Schema.decodeUnknownSync(
175 CatalogSnapshotV1Schema as Schema.Schema<CatalogSnapshotV1, unknown, never>,
176);
177const decodeCatalogFragmentSync = Schema.decodeUnknownSync(
178 CatalogFragmentV1Schema as Schema.Schema<CatalogFragmentV1, unknown, never>,
179);
180
181const 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
196const stableHash = (value: unknown): string => sha256Hex(stableStringify(value)).slice(0, 16);
197
198const unique = <T>(values: readonly T[]): T[] => [...new Set(values)];
199
200const 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
212const mutableRecord = <K extends string, V>(value: Readonly<Record<K, V>>): Record<K, V> =>
213 value as unknown as Record<K, V>;
214
215const createStableEntityId = <A extends string>(
216 make: (value: string) => A,
217 prefix: string,
218 value: unknown,
219): A => make(`${prefix}_${stableHash(value)}`);
220
221const emptyCatalog = (): CatalogV1 => ({
222 version: "ir.v1",
223 documents: {},
224 resources: {},
225 scopes: {},
226 symbols: {},
227 capabilities: {},
228 executables: {},
229 responseSets: {},
230 diagnostics: {},
231});
232
233const 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
253const docsSummary = (docs: DocumentationBlock | undefined): string | undefined =>
254 docs?.summary ?? docs?.description;
255
256const getShape = (catalog: CatalogV1, shapeId: ShapeSymbolId): ShapeSymbol | undefined => {
257 const symbol = catalog.symbols[shapeId];
258 return symbol && symbol.kind === "shape" ? symbol : undefined;
259};
260
261const 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
269const 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
277const 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
285const 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
301const 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
343const 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
350const 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
372const 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
426const 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
490const 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
527const 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
564const 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
586const 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
606const 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
617const 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
625const 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
691const 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
718const 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
926const 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
1005const 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
1055const 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
1115const 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
1132const 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
1150const 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
1166const symbolSummary = (symbol: IrSymbol): string | undefined =>
1167 symbol.docs?.summary ?? docsSummary(symbol.docs);
1168
1169const 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
1294export 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
1302export 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
1310export 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
1318export const createEmptyCatalogV1 = (): CatalogV1 => emptyCatalog();
1319
1320export 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
1329export 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
1389export 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
1520export 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
1546export 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
1564export 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
1586export 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};
packages/control-plane/src/ir/ids.ts+58−0
@@ -0,0 +1,58 @@
1import { Schema } from "effect";
2
3export const DocumentIdSchema = Schema.String.pipe(Schema.brand("DocumentId"));
4export const ResourceIdSchema = Schema.String.pipe(Schema.brand("ResourceId"));
5export const ScopeIdSchema = Schema.String.pipe(Schema.brand("ScopeId"));
6export const CapabilityIdSchema = Schema.String.pipe(Schema.brand("CapabilityId"));
7export const ExecutableIdSchema = Schema.String.pipe(Schema.brand("ExecutableId"));
8export const ResponseSetIdSchema = Schema.String.pipe(Schema.brand("ResponseSetId"));
9export const DiagnosticIdSchema = Schema.String.pipe(Schema.brand("DiagnosticId"));
10
11export const ShapeSymbolIdSchema = Schema.String.pipe(Schema.brand("ShapeSymbolId"));
12export const ParameterSymbolIdSchema = Schema.String.pipe(Schema.brand("ParameterSymbolId"));
13export const RequestBodySymbolIdSchema = Schema.String.pipe(
14 Schema.brand("RequestBodySymbolId"),
15);
16export const ResponseSymbolIdSchema = Schema.String.pipe(
17 Schema.brand("ResponseSymbolId"),
18);
19export const HeaderSymbolIdSchema = Schema.String.pipe(Schema.brand("HeaderSymbolId"));
20export const ExampleSymbolIdSchema = Schema.String.pipe(Schema.brand("ExampleSymbolId"));
21export const SecuritySchemeSymbolIdSchema = Schema.String.pipe(
22 Schema.brand("SecuritySchemeSymbolId"),
23);
24
25export type DocumentId = typeof DocumentIdSchema.Type;
26export type ResourceId = typeof ResourceIdSchema.Type;
27export type ScopeId = typeof ScopeIdSchema.Type;
28export type CapabilityId = typeof CapabilityIdSchema.Type;
29export type ExecutableId = typeof ExecutableIdSchema.Type;
30export type ResponseSetId = typeof ResponseSetIdSchema.Type;
31export type DiagnosticId = typeof DiagnosticIdSchema.Type;
32
33export type ShapeSymbolId = typeof ShapeSymbolIdSchema.Type;
34export type ParameterSymbolId = typeof ParameterSymbolIdSchema.Type;
35export type RequestBodySymbolId = typeof RequestBodySymbolIdSchema.Type;
36export type ResponseSymbolId = typeof ResponseSymbolIdSchema.Type;
37export type HeaderSymbolId = typeof HeaderSymbolIdSchema.Type;
38export type ExampleSymbolId = typeof ExampleSymbolIdSchema.Type;
39export type SecuritySchemeSymbolId = typeof SecuritySchemeSymbolIdSchema.Type;
40
41export type SymbolId =
42 | ShapeSymbolId
43 | ParameterSymbolId
44 | RequestBodySymbolId
45 | ResponseSymbolId
46 | HeaderSymbolId
47 | ExampleSymbolId
48 | SecuritySchemeSymbolId;
49
50export const SymbolIdSchema = Schema.Union(
51 ShapeSymbolIdSchema,
52 ParameterSymbolIdSchema,
53 RequestBodySymbolIdSchema,
54 ResponseSymbolIdSchema,
55 HeaderSymbolIdSchema,
56 ExampleSymbolIdSchema,
57 SecuritySchemeSymbolIdSchema,
58);
packages/control-plane/src/ir/index.ts+3−0
@@ -0,0 +1,3 @@
1export * from "./ids";
2export * from "./model";
3export * from "./catalog";
packages/control-plane/src/ir/model.ts+962−0
@@ -0,0 +1,962 @@
1import { Schema } from "effect";
2
3import {
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
36export const SourceKindSchema = Schema.Literal(
37 "openapi",
38 "graphql-schema",
39 "google-discovery",
40 "mcp",
41 "custom",
42);
43
44export const ScopeKindSchema = Schema.Literal(
45 "service",
46 "document",
47 "resource",
48 "pathItem",
49 "operation",
50 "folder",
51);
52
53export const EffectKindSchema = Schema.Literal(
54 "read",
55 "write",
56 "delete",
57 "action",
58 "subscribe",
59);
60
61export const ParameterLocationSchema = Schema.Literal(
62 "path",
63 "query",
64 "header",
65 "cookie",
66);
67
68export const ResponseTraitSchema = Schema.Literal(
69 "success",
70 "redirect",
71 "stream",
72 "download",
73 "upload",
74 "longRunning",
75);
76
77export const SecuritySchemeTypeSchema = Schema.Literal(
78 "oauth2",
79 "http",
80 "apiKey",
81 "basic",
82 "bearer",
83 "custom",
84);
85
86export const SelectionModeSchema = Schema.Literal(
87 "caller",
88 "fixed",
89 "persisted",
90);
91
92export const StatusRangeSchema = Schema.Literal(
93 "1XX",
94 "2XX",
95 "3XX",
96 "4XX",
97 "5XX",
98);
99
100export const PaginationHintKindSchema = Schema.Literal(
101 "cursor",
102 "offset",
103 "token",
104 "unknown",
105);
106
107export const DiagnosticLevelSchema = Schema.Literal(
108 "info",
109 "warning",
110 "error",
111);
112
113export 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
132export const NativeEncodingSchema = Schema.Literal(
133 "json",
134 "yaml",
135 "graphql",
136 "text",
137 "unknown",
138);
139
140export const ProvenanceRelationSchema = Schema.Literal(
141 "declared",
142 "hoisted",
143 "derived",
144 "merged",
145 "projected",
146);
147
148export const DocumentationBlockSchema = Schema.Struct({
149 summary: Schema.optional(Schema.String),
150 description: Schema.optional(Schema.String),
151 externalDocsUrl: Schema.optional(Schema.String),
152});
153
154export 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
163export 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
172export 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
179export 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
187export 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
198export 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
205export 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
215export 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
227export const UnknownShapeSchema = Schema.Struct({
228 type: Schema.Literal("unknown"),
229 reason: Schema.optional(Schema.String),
230});
231
232export const ConstShapeSchema = Schema.Struct({
233 type: Schema.Literal("const"),
234 value: Schema.Unknown,
235});
236
237export const EnumShapeSchema = Schema.Struct({
238 type: Schema.Literal("enum"),
239 values: Schema.Array(Schema.Unknown),
240});
241
242export 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
260export 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
267export 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
275export const MapShapeSchema = Schema.Struct({
276 type: Schema.Literal("map"),
277 valueShapeId: ShapeSymbolIdSchema,
278});
279
280export const AllOfShapeSchema = Schema.Struct({
281 type: Schema.Literal("allOf"),
282 items: Schema.Array(ShapeSymbolIdSchema),
283});
284
285export const AnyOfShapeSchema = Schema.Struct({
286 type: Schema.Literal("anyOf"),
287 items: Schema.Array(ShapeSymbolIdSchema),
288});
289
290export const OneOfShapeSchema = Schema.Struct({
291 type: Schema.Literal("oneOf"),
292 items: Schema.Array(ShapeSymbolIdSchema),
293 discriminator: Schema.optional(DiscriminatorSpecSchema),
294});
295
296export const NullableShapeSchema = Schema.Struct({
297 type: Schema.Literal("nullable"),
298 itemShapeId: ShapeSymbolIdSchema,
299});
300
301export const RefShapeSchema = Schema.Struct({
302 type: Schema.Literal("ref"),
303 target: ShapeSymbolIdSchema,
304});
305
306export const NotShapeSchema = Schema.Struct({
307 type: Schema.Literal("not"),
308 itemShapeId: ShapeSymbolIdSchema,
309});
310
311export const ConditionalShapeSchema = Schema.Struct({
312 type: Schema.Literal("conditional"),
313 ifShapeId: ShapeSymbolIdSchema,
314 thenShapeId: Schema.optional(ShapeSymbolIdSchema),
315 elseShapeId: Schema.optional(ShapeSymbolIdSchema),
316});
317
318export 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
327export const GraphQLUnionShapeSchema = Schema.Struct({
328 type: Schema.Literal("graphqlUnion"),
329 memberTypeIds: Schema.Array(ShapeSymbolIdSchema),
330});
331
332export 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
352export const AnchorTargetSchema = Schema.Struct({
353 shapeId: ShapeSymbolIdSchema,
354 pointer: Schema.optional(Schema.String),
355});
356
357export 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
377export 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
388export 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
400export const SecurityPlacementSchema = Schema.Struct({
401 in: Schema.optional(Schema.Literal("header", "query", "cookie")),
402 name: Schema.optional(Schema.String),
403});
404
405export 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
449export 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
457export 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
469export 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
482export 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
501export 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
517export 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
528export 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
539export 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
561export const SymbolSchema = Schema.Union(
562 ShapeSymbolSchema,
563 ParameterSymbolSchema,
564 RequestBodySymbolSchema,
565 ResponseSymbolSchema,
566 HeaderSymbolSchema,
567 ExampleSymbolSchema,
568 SecuritySchemeSymbolSchema,
569);
570
571export 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
589export 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
610export 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
621export 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
634export 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
647export 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
665export 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
693export 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
711export 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
728export 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
743export const ExecutableSchema = Schema.Union(
744 HttpExecutableSchema,
745 GraphQLExecutableSchema,
746 McpExecutableSchema,
747);
748
749export const PaginationHintSchema = Schema.Struct({
750 kind: PaginationHintKindSchema,
751 tokenParamName: Schema.optional(Schema.String),
752 nextFieldPath: Schema.optional(Schema.String),
753});
754
755export 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
769export const ResponseVariantSchema = Schema.Struct({
770 match: StatusMatchSchema,
771 responseId: ResponseSymbolIdSchema,
772 traits: Schema.optional(Schema.Array(ResponseTraitSchema)),
773 pagination: Schema.optional(PaginationHintSchema),
774});
775
776export const ResponseSetSchema = Schema.extend(
777 Schema.Struct({
778 id: ResponseSetIdSchema,
779 variants: Schema.Array(ResponseVariantSchema),
780 }),
781 EntityBaseSchema,
782);
783
784export 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
793export 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
829export 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
881export const CatalogSnapshotV1Schema = Schema.Struct({
882 version: Schema.Literal("ir.v1.snapshot"),
883 import: ImportMetadataSchema,
884 catalog: CatalogV1Schema,
885});
886
887export type SourceKind = typeof SourceKindSchema.Type;
888export type ScopeKind = typeof ScopeKindSchema.Type;
889export type EffectKind = typeof EffectKindSchema.Type;
890export type ParameterLocation = typeof ParameterLocationSchema.Type;
891export type ResponseTrait = typeof ResponseTraitSchema.Type;
892export type SecuritySchemeType = typeof SecuritySchemeTypeSchema.Type;
893export type SelectionMode = typeof SelectionModeSchema.Type;
894export type PaginationHintKind = typeof PaginationHintKindSchema.Type;
895export type DiagnosticCode = typeof DiagnosticCodeSchema.Type;
896export type DiagnosticLevel = typeof DiagnosticLevelSchema.Type;
897
898export type DocumentationBlock = typeof DocumentationBlockSchema.Type;
899export type ProvenanceRef = typeof ProvenanceRefSchema.Type;
900export type NativeBlob = typeof NativeBlobSchema.Type;
901export type EntityBase = typeof EntityBaseSchema.Type;
902export type ImportMetadata = typeof ImportMetadataSchema.Type;
903export type SourceDocument = typeof SourceDocumentSchema.Type;
904export type FieldSpec = typeof FieldSpecSchema.Type;
905export type DiscriminatorSpec = typeof DiscriminatorSpecSchema.Type;
906export type ScalarShape = typeof ScalarShapeSchema.Type;
907export type UnknownShape = typeof UnknownShapeSchema.Type;
908export type ConstShape = typeof ConstShapeSchema.Type;
909export type EnumShape = typeof EnumShapeSchema.Type;
910export type ObjectShape = typeof ObjectShapeSchema.Type;
911export type ArrayShape = typeof ArrayShapeSchema.Type;
912export type TupleShape = typeof TupleShapeSchema.Type;
913export type MapShape = typeof MapShapeSchema.Type;
914export type AllOfShape = typeof AllOfShapeSchema.Type;
915export type AnyOfShape = typeof AnyOfShapeSchema.Type;
916export type OneOfShape = typeof OneOfShapeSchema.Type;
917export type NullableShape = typeof NullableShapeSchema.Type;
918export type RefShape = typeof RefShapeSchema.Type;
919export type NotShape = typeof NotShapeSchema.Type;
920export type ConditionalShape = typeof ConditionalShapeSchema.Type;
921export type GraphQLInterfaceShape = typeof GraphQLInterfaceShapeSchema.Type;
922export type GraphQLUnionShape = typeof GraphQLUnionShapeSchema.Type;
923export type ShapeNode = typeof ShapeNodeSchema.Type;
924export type AnchorTarget = typeof AnchorTargetSchema.Type;
925export type SchemaResource = typeof SchemaResourceSchema.Type;
926export type RequestPlacementHint = typeof RequestPlacementHintSchema.Type;
927export type OAuthFlow = typeof OAuthFlowSchema.Type;
928export type SecurityPlacement = typeof SecurityPlacementSchema.Type;
929export type SecuritySchemeSymbol = typeof SecuritySchemeSymbolSchema.Type;
930export type EncodingSpec = typeof EncodingSpecSchema.Type;
931export type ContentSpec = typeof ContentSpecSchema.Type;
932export type ShapeSymbol = typeof ShapeSymbolSchema.Type;
933export type ParameterSymbol = typeof ParameterSymbolSchema.Type;
934export type HeaderSymbol = typeof HeaderSymbolSchema.Type;
935export type RequestBodySymbol = typeof RequestBodySymbolSchema.Type;
936export type ResponseSymbol = typeof ResponseSymbolSchema.Type;
937export type ExampleSymbol = typeof ExampleSymbolSchema.Type;
938export type Symbol =
939 | ShapeSymbol
940 | ParameterSymbol
941 | RequestBodySymbol
942 | ResponseSymbol
943 | HeaderSymbol
944 | ExampleSymbol
945 | SecuritySchemeSymbol;
946export type ServerSpec = typeof ServerSpecSchema.Type;
947export type ScopeDefaults = typeof ScopeDefaultsSchema.Type;
948export type Scope = typeof ScopeSchema.Type;
949export type InteractionSpec = typeof InteractionSpecSchema.Type;
950export type Capability = typeof CapabilitySchema.Type;
951export type HttpExecutable = typeof HttpExecutableSchema.Type;
952export type GraphQLExecutable = typeof GraphQLExecutableSchema.Type;
953export type McpExecutable = typeof McpExecutableSchema.Type;
954export type Executable = typeof ExecutableSchema.Type;
955export type PaginationHint = typeof PaginationHintSchema.Type;
956export type StatusMatch = typeof StatusMatchSchema.Type;
957export type ResponseVariant = typeof ResponseVariantSchema.Type;
958export type ResponseSet = typeof ResponseSetSchema.Type;
959export type ImportDiagnostic = typeof ImportDiagnosticSchema.Type;
960export type CatalogV1 = typeof CatalogV1Schema.Type;
961export type CatalogFragmentV1 = typeof CatalogFragmentV1Schema.Type;
962export type CatalogSnapshotV1 = typeof CatalogSnapshotV1Schema.Type;
packages/control-plane/src/runtime/executor-tools.ts+8−8
@@ -82,13 +82,13 @@ export const EXECUTOR_SOURCES_ADD_OUTPUT_SIGNATURE = deriveSchemaTypeSignature( · 81 unmodified lines
82 260,82 260,
83);83);
84 84
85export const EXECUTOR_SOURCES_ADD_INPUT_SCHEMA_JSON = JSON.stringify(85export const EXECUTOR_SOURCES_ADD_INPUT_SCHEMA = deriveSchemaJson(
86 deriveSchemaJson(ExecutorAddSourceInputSchema) ?? {},86 ExecutorAddSourceInputSchema,
87);87) ?? {};
88 88
89export const EXECUTOR_SOURCES_ADD_OUTPUT_SCHEMA_JSON = JSON.stringify(89export const EXECUTOR_SOURCES_ADD_OUTPUT_SCHEMA = deriveSchemaJson(
90 deriveSchemaJson(SourceSchema) ?? {},90 SourceSchema,
91);91) ?? {};
92 92
93export const EXECUTOR_SOURCES_ADD_HELP_LINES = [93export const EXECUTOR_SOURCES_ADD_HELP_LINES = [
94 "Source add input shapes:",94 "Source add input shapes:",
@@ -437,8 +437,8 @@ export const createExecutorToolMap = (input: { · 342 unmodified lines
437 metadata: {437 metadata: {
438 inputType: EXECUTOR_SOURCES_ADD_INPUT_HINT,438 inputType: EXECUTOR_SOURCES_ADD_INPUT_HINT,
439 outputType: EXECUTOR_SOURCES_ADD_OUTPUT_SIGNATURE,439 outputType: EXECUTOR_SOURCES_ADD_OUTPUT_SIGNATURE,
440 inputSchemaJson: EXECUTOR_SOURCES_ADD_INPUT_SCHEMA_JSON,440 inputSchema: EXECUTOR_SOURCES_ADD_INPUT_SCHEMA,
441 outputSchemaJson: EXECUTOR_SOURCES_ADD_OUTPUT_SCHEMA_JSON,441 outputSchema: EXECUTOR_SOURCES_ADD_OUTPUT_SCHEMA,
442 sourceKey: "executor",442 sourceKey: "executor",
443 interaction: "auto",443 interaction: "auto",
444 },444 },
packages/control-plane/src/runtime/graphql-tools.test.ts+3−3
@@ -1,6 +1,6 @@
1import { createServer, type IncomingMessage } from "node:http";1import { createServer, type IncomingMessage } from "node:http";
2 2
3import { afterEach, describe, expect, it } from "vitest";3import { afterEach, describe, expect, it } from "@effect/vitest";
4 4
5import { createGraphqlToolFromPersistedOperation } from "./graphql-tools";5import { createGraphqlToolFromPersistedOperation } from "./graphql-tools";
6 6
@@ -91,7 +91,7 @@ describe("graphql-tools", () => { · 84 unmodified lines
91 path: "source.graphql.raw",91 path: "source.graphql.raw",
92 sourceKey: "src_graphql",92 sourceKey: "src_graphql",
93 endpoint: server.baseUrl,93 endpoint: server.baseUrl,
94 providerDataJson: JSON.stringify({94 providerData: {
95 kind: "graphql",95 kind: "graphql",
96 toolKind: "request",96 toolKind: "request",
97 toolId: "raw",97 toolId: "raw",
@@ -105,7 +105,7 @@ describe("graphql-tools", () => { · 7 unmodified lines
105 queryTypeName: "Query",105 queryTypeName: "Query",
106 mutationTypeName: "Mutation",106 mutationTypeName: "Mutation",
107 subscriptionTypeName: null,107 subscriptionTypeName: null,
108 }),108 },
109 credentialPlacements: {109 credentialPlacements: {
110 headers: {110 headers: {
111 authorization: "Token top-secret",111 authorization: "Token top-secret",
packages/control-plane/src/runtime/graphql-tools.ts+71−74
@@ -10,7 +10,7 @@ import { · 9 unmodified lines
10 type ToolMap,10 type ToolMap,
11 type ToolMetadata,11 type ToolMetadata,
12 type ToolPath,12 type ToolPath,
13 typeSignatureFromSchemaJson,13 typeSignatureFromSchema,
14} from "@executor/codemode-core";14} from "@executor/codemode-core";
15import * as Effect from "effect/Effect";15import * as Effect from "effect/Effect";
16import * as Schema from "effect/Schema";16import * as Schema from "effect/Schema";
@@ -51,9 +51,9 @@ type GraphqlManifestToolBase = { · 34 unmodified lines
51 rawToolId: string | null;51 rawToolId: string | null;
52 toolName: string;52 toolName: string;
53 description: string | null;53 description: string | null;
54 inputSchemaJson?: string;54 inputSchema?: unknown;
55 outputSchemaJson?: string;55 outputSchema?: unknown;
56 exampleInputJson?: string;56 exampleInput?: unknown;
57};57};
58 58
59export type GraphqlRequestToolManifestEntry = GraphqlManifestToolBase & {59export type GraphqlRequestToolManifestEntry = GraphqlManifestToolBase & {
@@ -102,10 +102,10 @@ export type GraphqlToolDefinition = { · 42 unmodified lines
102export type GraphqlToolPresentation = {102export type GraphqlToolPresentation = {
103 inputType: string;103 inputType: string;
104 outputType: string;104 outputType: string;
105 inputSchemaJson?: string;105 inputSchema?: unknown;
106 outputSchemaJson?: string;106 outputSchema?: unknown;
107 exampleInputJson?: string;107 exampleInput?: unknown;
108 providerDataJson: string;108 providerData: GraphqlToolProviderData;
109};109};
110 110
111const GraphqlToolKindSchema = Schema.Literal("request", "field");111const GraphqlToolKindSchema = Schema.Literal("request", "field");
@@ -883,11 +883,11 @@ const outputEnvelopeSchemaForField = ( · 771 unmodified lines
883 };883 };
884};884};
885 885
886const exampleInputJsonForField = (886const exampleInputForField = (
887 args: readonly GraphQLArgument[],887 args: readonly GraphQLArgument[],
888): string | undefined => {888): Record<string, unknown> | undefined => {
889 if (args.length === 0) {889 if (args.length === 0) {
890 return JSON.stringify({});890 return {};
891 }891 }
892 892
893 const requiredArgs = args.filter(893 const requiredArgs = args.filter(
@@ -902,7 +902,7 @@ const exampleInputJsonForField = ( · 8 unmodified lines
902 ]),902 ]),
903 );903 );
904 904
905 return JSON.stringify(example);905 return example;
906};906};
907 907
908const buildGraphqlFieldOperationDocument = (input: {908const buildGraphqlFieldOperationDocument = (input: {
@@ -958,11 +958,11 @@ const requestToolManifestEntry = ( · 49 unmodified lines
958 rawToolId: "request",958 rawToolId: "request",
959 toolName: "GraphQL request",959 toolName: "GraphQL request",
960 description: `Execute a raw GraphQL request against ${sourceName}.`,960 description: `Execute a raw GraphQL request against ${sourceName}.`,
961 inputSchemaJson: JSON.stringify(GRAPHQL_REQUEST_INPUT_SCHEMA),961 inputSchema: GRAPHQL_REQUEST_INPUT_SCHEMA,
962 outputSchemaJson: JSON.stringify(GRAPHQL_REQUEST_OUTPUT_SCHEMA),962 outputSchema: GRAPHQL_REQUEST_OUTPUT_SCHEMA,
963 exampleInputJson: JSON.stringify({963 exampleInput: {
964 query: "query { __typename }",964 query: "query { __typename }",
965 }),965 },
966});966});
967 967
968type GraphqlFieldToolDraft = Omit<GraphqlFieldToolManifestEntry, "toolId"> & {968type GraphqlFieldToolDraft = Omit<GraphqlFieldToolManifestEntry, "toolId"> & {
@@ -1030,11 +1030,13 @@ const fieldToolDraftsFromRootType = (input: { · 61 unmodified lines
1030 .filter((field) => !field.name.startsWith("__"))1030 .filter((field) => !field.name.startsWith("__"))
1031 .map((field) => {1031 .map((field) => {
1032 const leaf = toCamelCase(field.name);1032 const leaf = toCamelCase(field.name);
1033 const outputSchemaJson = JSON.stringify(1033 const outputSchema = outputEnvelopeSchemaForField(
1034 outputEnvelopeSchemaForField(field.type, input.bundleBuilder),1034 field.type,
1035 input.bundleBuilder,
1035 );1036 );
1036 const inputSchemaJson = JSON.stringify(1037 const inputSchema = inputSchemaForFieldArguments(
1037 inputSchemaForFieldArguments(field.args, input.bundleBuilder),1038 field.args,
1039 input.bundleBuilder,
1038 );1040 );
1039 const { operationName, operationDocument } =1041 const { operationName, operationDocument } =
1040 buildGraphqlFieldOperationDocument({1042 buildGraphqlFieldOperationDocument({
@@ -1061,9 +1063,9 @@ const fieldToolDraftsFromRootType = (input: { · 20 unmodified lines
1061 operationType: input.operationType,1063 operationType: input.operationType,
1062 operationName,1064 operationName,
1063 operationDocument,1065 operationDocument,
1064 inputSchemaJson,1066 inputSchema,
1065 outputSchemaJson,1067 outputSchema,
1066 exampleInputJson: exampleInputJsonForField(field.args),1068 exampleInput: exampleInputForField(field.args),
1067 searchTerms: [1069 searchTerms: [
1068 input.operationType,1070 input.operationType,
1069 field.name,1071 field.name,
@@ -1143,26 +1145,26 @@ export const buildGraphqlToolPresentation = (input: { · 73 unmodified lines
1143 const entry = input.manifest.tools.find(1145 const entry = input.manifest.tools.find(
1144 (tool) => tool.toolId === input.definition.toolId,1146 (tool) => tool.toolId === input.definition.toolId,
1145 );1147 );
1146 const inputSchemaJson = entry?.inputSchemaJson;1148 const inputSchema = entry?.inputSchema;
1147 const outputSchemaJson = entry?.outputSchemaJson;1149 const outputSchema = entry?.outputSchema;
1148 1150
1149 return {1151 return {
1150 inputType: typeSignatureFromSchemaJson(1152 inputType: typeSignatureFromSchema(
1151 inputSchemaJson,1153 inputSchema,
1152 "unknown",1154 "unknown",
1153 Infinity,1155 Infinity,
1154 ),1156 ),
1155 outputType: typeSignatureFromSchemaJson(1157 outputType: typeSignatureFromSchema(
1156 outputSchemaJson,1158 outputSchema,
1157 "unknown",1159 "unknown",
1158 Infinity,1160 Infinity,
1159 ),1161 ),
1160 ...(inputSchemaJson ? { inputSchemaJson } : {}),1162 ...(inputSchema !== undefined ? { inputSchema } : {}),
1161 ...(outputSchemaJson ? { outputSchemaJson } : {}),1163 ...(outputSchema !== undefined ? { outputSchema } : {}),
1162 ...(entry?.exampleInputJson1164 ...(entry?.exampleInput !== undefined
1163 ? { exampleInputJson: entry.exampleInputJson }1165 ? { exampleInput: entry.exampleInput }
1164 : {}),1166 : {}),
1165 providerDataJson: JSON.stringify({1167 providerData: {
1166 kind: "graphql",1168 kind: "graphql",
1167 toolKind: entry?.kind ?? "request",1169 toolKind: entry?.kind ?? "request",
1168 toolId: input.definition.toolId,1170 toolId: input.definition.toolId,
@@ -1176,12 +1178,12 @@ export const buildGraphqlToolPresentation = (input: { · 7 unmodified lines
1176 queryTypeName: input.manifest.queryTypeName,1178 queryTypeName: input.manifest.queryTypeName,
1177 mutationTypeName: input.manifest.mutationTypeName,1179 mutationTypeName: input.manifest.mutationTypeName,
1178 subscriptionTypeName: input.manifest.subscriptionTypeName,1180 subscriptionTypeName: input.manifest.subscriptionTypeName,
1179 } satisfies GraphqlToolProviderData),1181 } satisfies GraphqlToolProviderData,
1180 };1182 };
1181};1183};
1182 1184
1183const decodeGraphqlToolProviderDataJson = Schema.decodeUnknownEither(1185const decodeGraphqlToolProviderData = Schema.decodeUnknownEither(
1184 Schema.parseJson(GraphqlToolProviderDataSchema),1186 GraphqlToolProviderDataSchema,
1185);1187);
1186 1188
1187export const decodeGraphqlSchemaRefTableJson = Schema.decodeUnknownEither(1189export const decodeGraphqlSchemaRefTableJson = Schema.decodeUnknownEither(
@@ -1218,19 +1220,14 @@ const setNestedSchemaProperty = ( · 30 unmodified lines
1218};1220};
1219 1221
1220const materializeSchemaWithRefDefinitions = (input: {1222const materializeSchemaWithRefDefinitions = (input: {
1221 schemaJson: string | undefined;1223 schema: unknown;
1222 refTable?: Readonly<Record<string, unknown>>;1224 refTable?: Readonly<Record<string, unknown>>;
1223}): Record<string, unknown> => {1225}): Record<string, unknown> => {
1224 if (!input.schemaJson) {1226 if (input.schema === undefined || input.schema === null) {
1225 return {};1227 return {};
1226 }1228 }
1227 1229
1228 let rootSchema: Record<string, unknown>;1230 const rootSchema = asRecord(input.schema);
1229 try {
1230 rootSchema = asRecord(JSON.parse(input.schemaJson) as unknown);
1231 } catch {
1232 return {};
1233 }
1234 1231
1235 if (!input.refTable || Object.keys(input.refTable).length === 0) {1232 if (!input.refTable || Object.keys(input.refTable).length === 0) {
1236 return rootSchema;1233 return rootSchema;
@@ -1269,17 +1266,17 @@ export const createGraphqlToolFromPersistedOperation = (input: { · 32 unmodified lines
1269 sourceKey: string;1266 sourceKey: string;
1270 endpoint: string;1267 endpoint: string;
1271 description?: string;1268 description?: string;
1272 inputSchemaJson?: string;1269 inputSchema?: unknown;
1273 outputSchemaJson?: string;1270 outputSchema?: unknown;
1274 exampleInputJson?: string;1271 exampleInput?: unknown;
1275 providerDataJson: string;1272 providerData: unknown;
1276 schemaRefTable?: Readonly<Record<string, unknown>>;1273 schemaRefTable?: Readonly<Record<string, unknown>>;
1277 defaultHeaders?: Readonly<Record<string, string>>;1274 defaultHeaders?: Readonly<Record<string, string>>;
1278 credentialHeaders?: Readonly<Record<string, string>>;1275 credentialHeaders?: Readonly<Record<string, string>>;
1279 credentialPlacements?: HttpRequestPlacements;1276 credentialPlacements?: HttpRequestPlacements;
1280}) => {1277}) => {
1281 const decodedProviderData = decodeGraphqlToolProviderDataJson(1278 const decodedProviderData = decodeGraphqlToolProviderData(
1282 input.providerDataJson,1279 input.providerData,
1283 );1280 );
1284 if (decodedProviderData._tag === "Left") {1281 if (decodedProviderData._tag === "Left") {
1285 throw new Error("Invalid GraphQL provider data");1282 throw new Error("Invalid GraphQL provider data");
@@ -1287,7 +1284,7 @@ export const createGraphqlToolFromPersistedOperation = (input: { · 1 unmodified line
1287 1284
1288 const providerData = decodedProviderData.right;1285 const providerData = decodedProviderData.right;
1289 const inputSchema = materializeSchemaWithRefDefinitions({1286 const inputSchema = materializeSchemaWithRefDefinitions({
1290 schemaJson: input.inputSchemaJson,1287 schema: input.inputSchema,
1291 refTable: input.schemaRefTable,1288 refTable: input.schemaRefTable,
1292 });1289 });
1293 1290
@@ -1298,28 +1295,28 @@ export const createGraphqlToolFromPersistedOperation = (input: { · 4 unmodified lines
1298 : providerData.operationType === "query"1295 : providerData.operationType === "query"
1299 ? "auto"1296 ? "auto"
1300 : "required",1297 : "required",
1301 inputType: typeSignatureFromSchemaJson(1298 inputType: typeSignatureFromSchema(
1302 input.inputSchemaJson,1299 input.inputSchema,
1303 "unknown",1300 "unknown",
1304 Infinity,1301 Infinity,
1305 ),1302 ),
1306 outputType: typeSignatureFromSchemaJson(1303 outputType: typeSignatureFromSchema(
1307 input.outputSchemaJson,1304 input.outputSchema,
1308 "unknown",1305 "unknown",
1309 Infinity,1306 Infinity,
1310 ),1307 ),
1311 ...(input.inputSchemaJson1308 ...(input.inputSchema !== undefined
1312 ? { inputSchemaJson: input.inputSchemaJson }1309 ? { inputSchema: input.inputSchema }
1313 : {}),1310 : {}),
1314 ...(input.outputSchemaJson1311 ...(input.outputSchema !== undefined
1315 ? { outputSchemaJson: input.outputSchemaJson }1312 ? { outputSchema: input.outputSchema }
1316 : {}),1313 : {}),
1317 ...(input.exampleInputJson1314 ...(input.exampleInput !== undefined
1318 ? { exampleInputJson: input.exampleInputJson }1315 ? { exampleInput: input.exampleInput }
1319 : {}),1316 : {}),
1320 sourceKey: input.sourceKey,1317 sourceKey: input.sourceKey,
1321 providerKind: "graphql",1318 providerKind: "graphql",
1322 providerDataJson: input.providerDataJson,1319 providerData,
1323 };1320 };
1324 1321
1325 return toTool({1322 return toTool({
@@ -1387,17 +1384,17 @@ export const graphqlToolDescriptorFromDefinition = (input: { · 61 unmodified lines
1387 input.definition.operationType === "query" ? "auto" : "required",1384 input.definition.operationType === "query" ? "auto" : "required",
1388 inputType: presentation.inputType,1385 inputType: presentation.inputType,
1389 outputType: presentation.outputType,1386 outputType: presentation.outputType,
1390 ...(input.includeSchemas && presentation.inputSchemaJson1387 ...(input.includeSchemas && presentation.inputSchema !== undefined
1391 ? { inputSchemaJson: presentation.inputSchemaJson }1388 ? { inputSchema: presentation.inputSchema }
1392 : {}),1389 : {}),
1393 ...(input.includeSchemas && presentation.outputSchemaJson1390 ...(input.includeSchemas && presentation.outputSchema !== undefined
1394 ? { outputSchemaJson: presentation.outputSchemaJson }1391 ? { outputSchema: presentation.outputSchema }
1395 : {}),1392 : {}),
1396 ...(presentation.exampleInputJson1393 ...(presentation.exampleInput !== undefined
1397 ? { exampleInputJson: presentation.exampleInputJson }1394 ? { exampleInput: presentation.exampleInput }
1398 : {}),1395 : {}),
1399 providerKind: "graphql",1396 providerKind: "graphql",
1400 providerDataJson: presentation.providerDataJson,1397 providerData: presentation.providerData,
1401 };1398 };
1402};1399};
1403 1400
@@ -1572,10 +1569,10 @@ export const createGraphqlToolsFromManifest = (input: { · 168 unmodified lines
1572 sourceKey: input.sourceKey,1569 sourceKey: input.sourceKey,
1573 endpoint,1570 endpoint,
1574 description: definition.description,1571 description: definition.description,
1575 inputSchemaJson: presentation.inputSchemaJson,1572 inputSchema: presentation.inputSchema,
1576 outputSchemaJson: presentation.outputSchemaJson,1573 outputSchema: presentation.outputSchema,
1577 exampleInputJson: presentation.exampleInputJson,1574 exampleInput: presentation.exampleInput,
1578 providerDataJson: presentation.providerDataJson,1575 providerData: presentation.providerData,
1579 schemaRefTable: input.manifest.schemaRefTable,1576 schemaRefTable: input.manifest.schemaRefTable,
1580 defaultHeaders: input.defaultHeaders,1577 defaultHeaders: input.defaultHeaders,
1581 credentialHeaders: input.credentialHeaders,1578 credentialHeaders: input.credentialHeaders,
packages/control-plane/src/runtime/index.ts+9−9
@@ -33,9 +33,9 @@ import { LocalToolRuntimeLoaderLive } from "./local-tools"; · 32 unmodified lines
33import { synchronizeLocalWorkspaceState } from "./local-workspace-sync";33import { synchronizeLocalWorkspaceState } from "./local-workspace-sync";
34import { ControlPlaneStore, type ControlPlaneStoreShape } from "./store";34import { ControlPlaneStore, type ControlPlaneStoreShape } from "./store";
35import { RuntimeSourceStoreLive } from "./source-store";35import { RuntimeSourceStoreLive } from "./source-store";
36import { RuntimeSourceRecipeStoreLive } from "./source-recipes-runtime";36import { RuntimeSourceCatalogStoreLive } from "./source-catalog-runtime";
37import { RuntimeSourceAuthMaterialLive } from "./source-auth-material";37import { RuntimeSourceAuthMaterialLive } from "./source-auth-material";
38import { RuntimeSourceMaterializationLive } from "./source-materialization";38import { RuntimeSourceCatalogSyncLive } from "./source-catalog-sync";
39import {39import {
40 RuntimeSourceAuthServiceLive,40 RuntimeSourceAuthServiceLive,
41} from "./source-auth-service";41} from "./source-auth-service";
@@ -121,7 +121,7 @@ export const createRuntimeControlPlaneLayer = ( · 79 unmodified lines
121 Layer.provide(baseLayer),121 Layer.provide(baseLayer),
122 );122 );
123 123
124 const sourceRecipeStoreLayer = RuntimeSourceRecipeStoreLive.pipe(124 const sourceCatalogStoreLayer = RuntimeSourceCatalogStoreLive.pipe(
125 Layer.provide(Layer.mergeAll(baseLayer, sourceStoreLayer)),125 Layer.provide(Layer.mergeAll(baseLayer, sourceStoreLayer)),
126 );126 );
127 127
@@ -129,7 +129,7 @@ export const createRuntimeControlPlaneLayer = ( · 1 unmodified line
129 Layer.provide(Layer.mergeAll(baseLayer, secretMaterialLayer)),129 Layer.provide(Layer.mergeAll(baseLayer, secretMaterialLayer)),
130 );130 );
131 131
132 const sourceMaterializationLayer = RuntimeSourceMaterializationLive.pipe(132 const sourceCatalogSyncLayer = RuntimeSourceCatalogSyncLive.pipe(
133 Layer.provide(133 Layer.provide(
134 Layer.mergeAll(baseLayer, secretMaterialLayer, sourceAuthMaterialLayer),134 Layer.mergeAll(baseLayer, secretMaterialLayer, sourceAuthMaterialLayer),
135 ),135 ),
@@ -143,7 +143,7 @@ export const createRuntimeControlPlaneLayer = ( · 7 unmodified lines
143 baseLayer,143 baseLayer,
144 secretMaterialLayer,144 secretMaterialLayer,
145 sourceStoreLayer,145 sourceStoreLayer,
146 sourceMaterializationLayer,146 sourceCatalogSyncLayer,
147 ),147 ),
148 ),148 ),
149 );149 );
@@ -156,9 +156,9 @@ export const createRuntimeControlPlaneLayer = ( · 6 unmodified lines
156 baseLayer,156 baseLayer,
157 secretMaterialLayer,157 secretMaterialLayer,
158 sourceAuthMaterialLayer,158 sourceAuthMaterialLayer,
159 sourceMaterializationLayer,159 sourceCatalogSyncLayer,
160 sourceAuthLayer,160 sourceAuthLayer,
161 sourceRecipeStoreLayer,161 sourceCatalogStoreLayer,
162 localToolRuntimeLayer,162 localToolRuntimeLayer,
163 ),163 ),
164 ),164 ),
@@ -169,8 +169,8 @@ export const createRuntimeControlPlaneLayer = ( · 4 unmodified lines
169 secretMaterialLayer,169 secretMaterialLayer,
170 sourceStoreLayer,170 sourceStoreLayer,
171 sourceAuthMaterialLayer,171 sourceAuthMaterialLayer,
172 sourceMaterializationLayer,172 sourceCatalogSyncLayer,
173 sourceRecipeStoreLayer,173 sourceCatalogStoreLayer,
174 localToolRuntimeLayer,174 localToolRuntimeLayer,
175 sourceAuthLayer,175 sourceAuthLayer,
176 executionResolverLayer,176 executionResolverLayer,
packages/control-plane/src/runtime/ir-execution.ts+494−0
@@ -0,0 +1,494 @@
1import {
2 applyCookiePlacementsToHeaders,
3 applyHttpQueryPlacementsToUrl,
4 applyJsonBodyPlacements,
5} from "@executor/codemode-core";
6import { createSdkMcpConnector } from "@executor/codemode-mcp";
7import type { AccountId, Source } from "#schema";
8import * as Effect from "effect/Effect";
9
10import type { CatalogV1, Capability, Executable, GraphQLExecutable, HttpExecutable, McpExecutable, ParameterSymbol } from "../ir/model";
11import type { LoadedSourceCatalogToolIndexEntry } from "./source-catalog-runtime";
12import type { ResolvedSourceAuthMaterial } from "./source-auth-material";
13
14const asObject = (value: unknown): Record<string, unknown> =>
15 value !== null && typeof value === "object" && !Array.isArray(value)
16 ? (value as Record<string, unknown>)
17 : {};
18
19const asString = (value: unknown): string | null =>
20 typeof value === "string" && value.trim().length > 0 ? value : null;
21
22const 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
28const 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
34const readSourceQueryParams = (source: Source): Record<string, string> =>
35 asStringRecord(asObject(source.binding).queryParams);
36
37const 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
45const 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
56const 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
67const 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
80const parameterById = (catalog: CatalogV1, parameterId: string): ParameterSymbol | undefined => {
81 const symbol = catalog.symbols[parameterId];
82 return symbol?.kind === "parameter" ? symbol : undefined;
83};
84
85const 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
104const 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
124const 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
249const 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
264const 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
284const 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
384const 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
428export 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
460export 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};
packages/control-plane/src/runtime/live-execution.test.ts+1−1
@@ -1,4 +1,4 @@
1import { describe, expect, it } from "vitest";1import { describe, expect, it } from "@effect/vitest";
2 2
3import { sanitizePersistedElicitationResponse } from "./live-execution";3import { sanitizePersistedElicitationResponse } from "./live-execution";
4 4
packages/control-plane/src/runtime/local-source-artifacts.ts+29−94
@@ -3,18 +3,17 @@ import { FileSystem } from "@effect/platform"; · 2 unmodified lines
3 3
4import {4import {
5 SourceIdSchema,5 SourceIdSchema,
6 SourceRecipeIdSchema,6 SourceCatalogIdSchema,
7 StoredSourceRecipeDocumentRecordSchema,7 StoredSourceCatalogRevisionRecordSchema,
8 StoredSourceRecipeOperationRecordSchema,
9 StoredSourceRecipeRevisionRecordSchema,
10 StoredSourceRecipeSchemaBundleRecordSchema,
11 TimestampMsSchema,8 TimestampMsSchema,
12 type Source,9 type Source,
13 type SourceRecipeId,10 type SourceCatalogId,
14} from "#schema";11} from "#schema";
15import * as Effect from "effect/Effect";12import * as Effect from "effect/Effect";
16import * as Schema from "effect/Schema";13import * as Schema from "effect/Schema";
17 14
15import { CatalogSnapshotV1Schema } from "../ir/model";
16import type { SourceCatalogSyncResult } from "./source-catalog-support";
18import type { ResolvedLocalWorkspaceContext } from "./local-config";17import type { ResolvedLocalWorkspaceContext } from "./local-config";
19import {18import {
20 LocalFileSystemError,19 LocalFileSystemError,
@@ -22,25 +21,20 @@ import { · 1 unmodified line
22 unknownLocalErrorDetails,21 unknownLocalErrorDetails,
23} from "./local-errors";22} from "./local-errors";
24import {23import {
25 createSourceRecipeRevisionRecord,24 createSourceCatalogRevisionRecord,
26 stableSourceRecipeId,25 stableSourceCatalogId,
27} from "./source-definitions";26} from "./source-definitions";
28import {27import { contentHash } from "./source-catalog-support";
29 contentHash,
30 type SourceRecipeMaterialization,
31} from "./source-recipe-support";
32 28
33const LOCAL_SOURCE_ARTIFACT_VERSION = 1 as const;29const LOCAL_SOURCE_ARTIFACT_VERSION = 2 as const;
34 30
35export const LocalSourceArtifactSchema = Schema.Struct({31export const LocalSourceArtifactSchema = Schema.Struct({
36 version: Schema.Literal(LOCAL_SOURCE_ARTIFACT_VERSION),32 version: Schema.Literal(LOCAL_SOURCE_ARTIFACT_VERSION),
37 sourceId: SourceIdSchema,33 sourceId: SourceIdSchema,
38 recipeId: SourceRecipeIdSchema,34 catalogId: SourceCatalogIdSchema,
39 generatedAt: TimestampMsSchema,35 generatedAt: TimestampMsSchema,
40 revision: StoredSourceRecipeRevisionRecordSchema,36 revision: StoredSourceCatalogRevisionRecordSchema,
41 documents: Schema.Array(StoredSourceRecipeDocumentRecordSchema),37 snapshot: CatalogSnapshotV1Schema,
42 schemaBundles: Schema.Array(StoredSourceRecipeSchemaBundleRecordSchema),
43 operations: Schema.Array(StoredSourceRecipeOperationRecordSchema),
44});38});
45 39
46export type LocalSourceArtifact = typeof LocalSourceArtifactSchema.Type;40export type LocalSourceArtifact = typeof LocalSourceArtifactSchema.Type;
@@ -65,90 +59,32 @@ const localSourceArtifactPath = (input: { · 18 unmodified lines
65 `${input.sourceId}.json`,59 `${input.sourceId}.json`,
66 );60 );
67 61
68const canonicalMaterializationHash = (input: {62const snapshotHash = (snapshot: SourceCatalogSyncResult["snapshot"]): string =>
69 materialization: SourceRecipeMaterialization;63 contentHash(JSON.stringify(snapshot));
70}): string => {
71 const documents = [...input.materialization.documents]
72 .map((document) => ({
73 documentKind: document.documentKind,
74 documentKey: document.documentKey,
75 contentHash: document.contentHash,
76 }))
77 .sort((left, right) =>
78 left.documentKind.localeCompare(right.documentKind)
79 || left.documentKey.localeCompare(right.documentKey)
80 || left.contentHash.localeCompare(right.contentHash)
81 );
82 const schemaBundles = [...input.materialization.schemaBundles]
83 .map((bundle) => ({
84 bundleKind: bundle.bundleKind,
85 contentHash: bundle.contentHash,
86 }))
87 .sort((left, right) =>
88 left.bundleKind.localeCompare(right.bundleKind)
89 || left.contentHash.localeCompare(right.contentHash)
90 );
91 const operations = [...input.materialization.operations]
92 .map((operation) => ({
93 operationKey: operation.operationKey,
94 transportKind: operation.transportKind,
95 toolId: operation.toolId,
96 title: operation.title,
97 description: operation.description,
98 operationKind: operation.operationKind,
99 searchText: operation.searchText,
100 inputSchemaJson: operation.inputSchemaJson,
101 outputSchemaJson: operation.outputSchemaJson,
102 providerKind: operation.providerKind,
103 providerDataJson: operation.providerDataJson,
104 }))
105 .sort((left, right) => left.operationKey.localeCompare(right.operationKey));
106
107 return contentHash(JSON.stringify({
108 schemaVersion: 1,
109 manifestHash: input.materialization.manifestHash,
110 manifestJson: input.materialization.manifestJson,
111 documents,
112 schemaBundles,
113 operations,
114 }));
115};
116
117const bindRevisionId = <T extends { recipeRevisionId: string }>(
118 items: readonly T[],
119 recipeRevisionId: string,
120): T[] =>
121 items.map((item) => ({
122 ...item,
123 recipeRevisionId,
124 }));
125 64
126export const buildLocalSourceArtifact = (input: {65export const buildLocalSourceArtifact = (input: {
127 source: Source;66 source: Source;
128 materialization: SourceRecipeMaterialization;67 syncResult: SourceCatalogSyncResult;
129}): LocalSourceArtifact => {68}): LocalSourceArtifact => {
130 const recipeId: SourceRecipeId = stableSourceRecipeId(input.source);69 const catalogId: SourceCatalogId = stableSourceCatalogId(input.source);
131 const now = Date.now();70 const generatedAt = Date.now();
132 const revision = createSourceRecipeRevisionRecord({71 const hash = snapshotHash(input.syncResult.snapshot);
72 const revision = createSourceCatalogRevisionRecord({
133 source: input.source,73 source: input.source,
134 recipeId,74 catalogId,
135 revisionNumber: 1,75 revisionNumber: 1,
136 manifestJson: input.materialization.manifestJson,76 importMetadataJson: JSON.stringify(input.syncResult.snapshot.import),
137 manifestHash: input.materialization.manifestHash,77 importMetadataHash: hash,
138 materializationHash: canonicalMaterializationHash({78 snapshotHash: hash,
139 materialization: input.materialization,
140 }),
141 });79 });
142 80
143 return {81 return {
144 version: LOCAL_SOURCE_ARTIFACT_VERSION,82 version: LOCAL_SOURCE_ARTIFACT_VERSION,
145 sourceId: input.source.id,83 sourceId: input.source.id,
146 recipeId,84 catalogId,
147 generatedAt: now,85 generatedAt,
148 revision,86 revision,
149 documents: bindRevisionId(input.materialization.documents, revision.id),87 snapshot: input.syncResult.snapshot,
150 schemaBundles: bindRevisionId(input.materialization.schemaBundles, revision.id),
151 operations: bindRevisionId(input.materialization.operations, revision.id),
152 };88 };
153};89};
154 90
@@ -175,13 +111,12 @@ export const readLocalSourceArtifact = (input: { · 20 unmodified lines
175 );111 );
176 return yield* Effect.try({112 return yield* Effect.try({
177 try: () => decodeLocalSourceArtifact(JSON.parse(content) as unknown),113 try: () => decodeLocalSourceArtifact(JSON.parse(content) as unknown),
178 catch: (cause) => {114 catch: (cause) =>
179 return new LocalSourceArtifactDecodeError({115 new LocalSourceArtifactDecodeError({
180 message: `Invalid local source artifact at ${path}: ${unknownLocalErrorDetails(cause)}`,116 message: `Invalid local source artifact at ${path}: ${unknownLocalErrorDetails(cause)}`,
181 path,117 path,
182 details: unknownLocalErrorDetails(cause),118 details: unknownLocalErrorDetails(cause),
183 });119 }),
184 },
185 });120 });
186 });121 });
187 122
packages/control-plane/src/runtime/local-storage.ts+2−2
@@ -32,7 +32,7 @@ import { · 31 unmodified lines
32 loadLocalWorkspaceState,32 loadLocalWorkspaceState,
33 writeLocalWorkspaceState,33 writeLocalWorkspaceState,
34} from "./local-workspace-state";34} from "./local-workspace-state";
35import type { SourceRecipeMaterialization } from "./source-recipe-support";35import type { SourceCatalogSyncResult } from "./source-catalog-support";
36import type { Source } from "#schema";36import type { Source } from "#schema";
37 37
38export type InstallationStoreShape = {38export type InstallationStoreShape = {
@@ -80,7 +80,7 @@ export class WorkspaceStateStore extends Context.Tag( · 41 unmodified lines
80export type SourceArtifactStoreShape = {80export type SourceArtifactStoreShape = {
81 build: (input: {81 build: (input: {
82 source: Source;82 source: Source;
83 materialization: SourceRecipeMaterialization;83 syncResult: SourceCatalogSyncResult;
84 }) => LocalSourceArtifact;84 }) => LocalSourceArtifact;
85 read: (input: {85 read: (input: {
86 context: ResolvedLocalWorkspaceContext;86 context: ResolvedLocalWorkspaceContext;
packages/control-plane/src/runtime/oauth-loopback.test.ts+1−1
@@ -1,6 +1,6 @@
1import { createServer } from "node:http";1import { createServer } from "node:http";
2 2
3import { describe, expect, it } from "vitest";3import { describe, expect, it } from "@effect/vitest";
4import * as Effect from "effect/Effect";4import * as Effect from "effect/Effect";
5 5
6import { startOauthLoopbackRedirectServer } from "./oauth-loopback";6import { startOauthLoopbackRedirectServer } from "./oauth-loopback";
packages/control-plane/src/runtime/source-adapters/google-discovery.test.ts+8−8
@@ -56,7 +56,7 @@ const withStaticServer = async <T>(content: string, handler: (baseUrl: string) = · 55 unmodified lines
56};56};
57 57
58describe("google discovery source adapter", () => {58describe("google discovery source adapter", () => {
59 it("materializes a real Google Sheets discovery doc into recipe content", () =>59 it("syncs a real Google Sheets discovery doc into snapshot content", () =>
60 Effect.tryPromise({60 Effect.tryPromise({
61 try: async () => {61 try: async () => {
62 const discoveryDocument = await fetchLiveDiscoveryDocument();62 const discoveryDocument = await fetchLiveDiscoveryDocument();
@@ -86,8 +86,8 @@ describe("google discovery source adapter", () => { · 23 unmodified lines
86 }),86 }),
87 );87 );
88 88
89 const materialization = await Effect.runPromise(89 const syncResult = await Effect.runPromise(
90 googleDiscoverySourceAdapter.materializeSource({90 googleDiscoverySourceAdapter.syncCatalog({
91 source,91 source,
92 resolveSecretMaterial: () => Effect.fail(new Error("unexpected secret lookup")),92 resolveSecretMaterial: () => Effect.fail(new Error("unexpected secret lookup")),
93 resolveAuthMaterialForSlot: () =>93 resolveAuthMaterialForSlot: () =>
@@ -103,12 +103,12 @@ describe("google discovery source adapter", () => { · 9 unmodified lines
103 }),103 }),
104 );104 );
105 105
106 expect(materialization.documents[0]?.documentKind).toBe("google_discovery");106 expect(Object.values(syncResult.snapshot.catalog.documents)[0]?.kind).toBe("google-discovery");
107 expect(materialization.schemaBundles.length).toBeGreaterThan(0);107 expect(Object.keys(syncResult.snapshot.catalog.resources)).not.toHaveLength(0);
108 expect(materialization.operations.length).toBeGreaterThan(50);108 expect(Object.keys(syncResult.snapshot.catalog.capabilities).length).toBeGreaterThan(50);
109 expect(109 expect(
110 materialization.operations.some(110 Object.values(syncResult.snapshot.catalog.capabilities).some((capability) =>
111 (operation) => operation.toolId === "spreadsheets.values.get",111 capability.surface.toolPath.join(".") === "google.sheets.spreadsheets.values.get"
112 ),112 ),
113 ).toBe(true);113 ).toBe(true);
114 });114 });
packages/control-plane/src/runtime/source-adapters/google-discovery.ts+29−304
@@ -3,55 +3,32 @@ import { · 2 unmodified lines
3 HttpClient,3 HttpClient,
4 HttpClientRequest,4 HttpClientRequest,
5} from "@effect/platform";5} from "@effect/platform";
6import {
7 allowAllToolInteractions,
8 makeToolInvokerFromTools,
9 typeSignatureFromSchemaJson,
10} from "@executor/codemode-core";
11import {6import {
12 compileGoogleDiscoveryToolDefinitions,7 compileGoogleDiscoveryToolDefinitions,
13 createGoogleDiscoveryToolFromDefinition,
14 decodeGoogleDiscoverySchemaRefTableJson,
15 extractGoogleDiscoveryManifest,8 extractGoogleDiscoveryManifest,
16 googleDiscoveryProviderDataJsonFromDefinition,9 type GoogleDiscoveryToolProviderData,
17 GoogleDiscoveryToolProviderDataSchema,10 googleDiscoveryProviderDataFromDefinition,
18 type GoogleDiscoveryToolManifest,
19} from "@executor/codemode-google-discovery";11} from "@executor/codemode-google-discovery";
20import type {12import type { Source } from "#schema";
21 Source,13import { StringMapSchema } from "#schema";
22 SourceRecipeRevisionId,
23 StoredSourceRecipeDocumentRecord,
24 StoredSourceRecipeOperationRecord,
25 StoredSourceRecipeSchemaBundleRecord,
26} from "#schema";
27import {
28 SourceRecipeSchemaBundleIdSchema,
29 StringMapSchema,
30} from "#schema";
31import * as Either from "effect/Either";
32import * as Effect from "effect/Effect";14import * as Effect from "effect/Effect";
33import * as Schema from "effect/Schema";15import * as Schema from "effect/Schema";
34 16
35import {17import {
36 contentHash,18 createGoogleDiscoveryCatalogSnapshot,
37 normalizeSearchText,19 type GoogleDiscoveryCatalogOperationInput,
38} from "../source-recipe-support";20} from "../source-catalog-snapshot";
39import type {21import type { SourceAdapter } from "./types";
40 SourceAdapter,
41 SourceAdapterMaterialization,
42} from "./types";
43import {22import {
44 ConnectHttpAuthSchema,23 ConnectHttpAuthSchema,
45 ConnectHttpImportAuthSchema,24 ConnectHttpImportAuthSchema,
46 ConnectOauthClientSchema,25 ConnectOauthClientSchema,
47 createStandardToolDescriptor,
48 decodeBindingConfig,26 decodeBindingConfig,
49 decodeSourceBindingPayload,27 decodeSourceBindingPayload,
50 emptySourceBindingState,28 emptySourceBindingState,
51 encodeBindingConfig,29 encodeBindingConfig,
52 isSourceCredentialRequiredError,30 isSourceCredentialRequiredError,
53 OptionalNullableStringSchema,31 OptionalNullableStringSchema,
54 parseJsonValue,
55 SourceCredentialRequiredError,32 SourceCredentialRequiredError,
56} from "./shared";33} from "./shared";
57 34
@@ -97,13 +74,6 @@ const GoogleDiscoverySourceBindingPayloadSchema = Schema.Struct({ · 39 unmodified lines
97type GoogleDiscoveryBindingConfig = typeof GoogleDiscoveryBindingConfigSchema.Type;74type GoogleDiscoveryBindingConfig = typeof GoogleDiscoveryBindingConfigSchema.Type;
98 75
99const GOOGLE_DISCOVERY_BINDING_CONFIG_VERSION = 1;76const GOOGLE_DISCOVERY_BINDING_CONFIG_VERSION = 1;
100const GOOGLE_DISCOVERY_MATERIALIZATION_REVISION_ID =
101 "src_recipe_rev_materialization" as SourceRecipeRevisionId;
102
103const decodeGoogleDiscoveryProviderDataJson = Schema.decodeUnknownEither(
104 Schema.parseJson(GoogleDiscoveryToolProviderDataSchema),
105);
106
107const bindingHasAnyField = (77const bindingHasAnyField = (
108 value: unknown,78 value: unknown,
109 fields: readonly string[],79 fields: readonly string[],
@@ -205,129 +175,30 @@ const fetchGoogleDiscoveryDocumentWithHeaders = (input: { · 95 unmodified lines
205 );175 );
206 }).pipe(Effect.provide(FetchHttpClient.layer));176 }).pipe(Effect.provide(FetchHttpClient.layer));
207 177
208const toGoogleDiscoverySchemaBundleRecord = (input: {178const googleDiscoveryCatalogOperationFromDefinition = (input: {
209 recipeRevisionId: SourceRecipeRevisionId;179 manifest: Parameters<typeof compileGoogleDiscoveryToolDefinitions>[0];
210 refTable: Readonly<Record<string, string>> | undefined;
211 now: number;
212}): StoredSourceRecipeSchemaBundleRecord | null => {
213 const refEntries = Object.entries(input.refTable ?? {});
214 if (refEntries.length === 0) {
215 return null;
216 }
217
218 const refs = Object.fromEntries(
219 refEntries.map(([ref, rawValue]) => {
220 try {
221 return [ref, JSON.parse(rawValue) as unknown];
222 } catch {
223 return [ref, rawValue];
224 }
225 }),
226 );
227 const refsJson = JSON.stringify(refs);
228
229 return {
230 id: SourceRecipeSchemaBundleIdSchema.make(
231 `src_recipe_bundle_${crypto.randomUUID()}`,
232 ),
233 recipeRevisionId: input.recipeRevisionId,
234 bundleKind: "json_schema_ref_map",
235 refsJson,
236 contentHash: contentHash(refsJson),
237 createdAt: input.now,
238 updatedAt: input.now,
239 };
240};
241
242const toGoogleDiscoveryRecipeOperationRecord = (input: {
243 recipeRevisionId: SourceRecipeRevisionId;
244 manifest: GoogleDiscoveryToolManifest;
245 definition: ReturnType<typeof compileGoogleDiscoveryToolDefinitions>[number];180 definition: ReturnType<typeof compileGoogleDiscoveryToolDefinitions>[number];
246 now: number;181}): GoogleDiscoveryCatalogOperationInput => ({
247}): StoredSourceRecipeOperationRecord => ({
248 id: `src_recipe_op_${crypto.randomUUID()}`,
249 recipeRevisionId: input.recipeRevisionId,
250 operationKey: input.definition.toolId,
251 transportKind: "http",
252 toolId: input.definition.toolId,182 toolId: input.definition.toolId,
253 title: input.definition.name,183 title: input.definition.name,
254 description: input.definition.description,184 description: input.definition.description,
255 operationKind:185 effect:
256 input.definition.method === "get" || input.definition.method === "head"186 input.definition.method === "get" || input.definition.method === "head"
257 ? "read"187 ? "read"
258 : input.definition.method === "delete"188 : input.definition.method === "delete"
259 ? "delete"189 ? "delete"
260 : "write",190 : "write",
261 searchText: normalizeSearchText(191 inputSchema: input.definition.inputSchema,
262 input.definition.toolId,192 outputSchema: input.definition.outputSchema,
263 input.definition.rawToolId,193 providerData: googleDiscoveryProviderDataFromDefinition({
264 input.definition.methodId,
265 input.definition.name,
266 input.definition.description ?? undefined,
267 input.definition.method,
268 input.definition.path,
269 input.definition.group ?? undefined,
270 input.definition.leaf,
271 input.manifest.service,
272 input.manifest.versionName,
273 input.definition.scopes.join(" "),
274 ),
275 inputSchemaJson: input.definition.inputSchemaJson ?? null,
276 outputSchemaJson: input.definition.outputSchemaJson ?? null,
277 providerKind: "google_discovery",
278 providerDataJson: googleDiscoveryProviderDataJsonFromDefinition({
279 service: input.manifest.service,194 service: input.manifest.service,
280 version: input.manifest.versionName,195 version: input.manifest.versionName,
281 rootUrl: input.manifest.rootUrl,196 rootUrl: input.manifest.rootUrl,
282 servicePath: input.manifest.servicePath,197 servicePath: input.manifest.servicePath,
283 definition: input.definition,198 definition: input.definition,
284 }),199 }) as GoogleDiscoveryToolProviderData,
285 createdAt: input.now,
286 updatedAt: input.now,
287});200});
288 201
289const 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
331const googleDiscoveryOauth2SetupConfig = (source: Source) =>202const googleDiscoveryOauth2SetupConfig = (source: Source) =>
332 Effect.gen(function* () {203 Effect.gen(function* () {
333 const bindingConfig = yield* googleDiscoveryBindingConfigFromSource(source);204 const bindingConfig = yield* googleDiscoveryBindingConfigFromSource(source);
@@ -369,8 +240,6 @@ export const googleDiscoverySourceAdapter: SourceAdapter = { · 35 unmodified lines
369 bindingConfigVersion: GOOGLE_DISCOVERY_BINDING_CONFIG_VERSION,240 bindingConfigVersion: GOOGLE_DISCOVERY_BINDING_CONFIG_VERSION,
370 providerKey: "google_workspace",241 providerKey: "google_workspace",
371 defaultImportAuthPolicy: "reuse_runtime",242 defaultImportAuthPolicy: "reuse_runtime",
372 primaryDocumentKind: "google_discovery",
373 primarySchemaBundleKind: "json_schema_ref_map",
374 connectPayloadSchema: GoogleDiscoveryConnectPayloadSchema,243 connectPayloadSchema: GoogleDiscoveryConnectPayloadSchema,
375 executorAddInputSchema: GoogleDiscoveryExecutorAddInputSchema,244 executorAddInputSchema: GoogleDiscoveryExecutorAddInputSchema,
376 executorAddHelpText: [245 executorAddHelpText: [
@@ -438,71 +307,7 @@ export const googleDiscoverySourceAdapter: SourceAdapter = { · 61 unmodified lines
438 }),307 }),
439 shouldAutoProbe: (source) =>308 shouldAutoProbe: (source) =>
440 source.enabled && (source.status === "draft" || source.status === "probing"),309 source.enabled && (source.status === "draft" || source.status === "probing"),
441 parseManifest: ({ source, manifestJson }) =>310 syncCatalog: ({ source, resolveAuthMaterialForSlot }) =>
442 parseJsonValue<GoogleDiscoveryToolManifest>({
443 label: `Google Discovery manifest for ${source.id}`,
444 value: manifestJson,
445 }),
446 describePersistedOperation: ({ path, operation }) =>
447 Effect.gen(function* () {
448 const decoded = operation.providerDataJson
449 ? decodeGoogleDiscoveryProviderDataJson(operation.providerDataJson)
450 : Either.left(new Error("Missing providerDataJson"));
451 if (Either.isLeft(decoded)) {
452 return yield* Effect.fail(
453 new Error(`Invalid Google Discovery provider data for ${path}`),
454 );
455 }
456
457 const providerData = decoded.right;
458 const method = providerData.invocation.method.toUpperCase();
459 return {
460 method: providerData.invocation.method,
461 pathTemplate: providerData.invocation.path,
462 rawToolId: providerData.rawToolId,
463 operationId: providerData.methodId,
464 group: providerData.group,
465 leaf: providerData.leaf,
466 tags: [],
467 searchText: normalizeSearchText(
468 path,
469 operation.toolId,
470 operation.title ?? undefined,
471 operation.description ?? undefined,
472 providerData.rawToolId,
473 providerData.methodId,
474 method,
475 providerData.invocation.path,
476 providerData.group ?? undefined,
477 providerData.leaf,
478 providerData.service,
479 providerData.version,
480 providerData.invocation.scopes.join(" "),
481 operation.searchText,
482 ),
483 interaction: method === "GET" || method === "HEAD" ? "auto" : "required",
484 approvalLabel: `${method} ${providerData.invocation.path}`,
485 } as const;
486 }),
487 createToolDescriptor: ({ source, operation, path, includeSchemas, schemaBundleId }) =>
488 createStandardToolDescriptor({
489 source,
490 operation,
491 path,
492 includeSchemas,
493 interaction: operation.operationKind === "read" ? "auto" : "required",
494 outputType: typeSignatureFromSchemaJson(
495 operation.outputSchemaJson ?? undefined,
496 "unknown",
497 320,
498 ),
499 schemaBundleId,
500 }),
501 materializeSource: ({ source, resolveAuthMaterialForSlot }): Effect.Effect<
502 SourceAdapterMaterialization,
503 Error,
504 never
505 > =>
506 Effect.gen(function* () {311 Effect.gen(function* () {
507 const bindingConfig = yield* googleDiscoveryBindingConfigFromSource(source);312 const bindingConfig = yield* googleDiscoveryBindingConfigFromSource(source);
508 const auth = yield* resolveAuthMaterialForSlot("import");313 const auth = yield* resolveAuthMaterialForSlot("import");
@@ -529,37 +334,24 @@ export const googleDiscoverySourceAdapter: SourceAdapter = { · 20 unmodified lines
529 );334 );
530 const definitions = compileGoogleDiscoveryToolDefinitions(manifest);335 const definitions = compileGoogleDiscoveryToolDefinitions(manifest);
531 const now = Date.now();336 const now = Date.now();
532 const schemaBundle = toGoogleDiscoverySchemaBundleRecord({
533 recipeRevisionId: GOOGLE_DISCOVERY_MATERIALIZATION_REVISION_ID,
534 refTable: manifest.schemaRefTable,
535 now,
536 });
537 337
538 return {338 return {
539 manifestJson: JSON.stringify(manifest),339 snapshot: createGoogleDiscoveryCatalogSnapshot({
540 manifestHash: manifest.sourceHash,340 source,
541 sourceHash: manifest.sourceHash,341 documents: [{
542 documents: [
543 {
544 id: `src_recipe_doc_${crypto.randomUUID()}`,
545 recipeRevisionId: GOOGLE_DISCOVERY_MATERIALIZATION_REVISION_ID,
546 documentKind: "google_discovery",342 documentKind: "google_discovery",
547 documentKey: bindingConfig.discoveryUrl,343 documentKey: bindingConfig.discoveryUrl,
548 contentText: discoveryDocument,344 contentText: discoveryDocument,
549 contentHash: contentHash(discoveryDocument),
550 fetchedAt: now,345 fetchedAt: now,
551 createdAt: now,346 }],
552 updatedAt: now,347 operations: definitions.map((definition) =>
553 },348 googleDiscoveryCatalogOperationFromDefinition({
554 ],349 manifest,
555 schemaBundles: schemaBundle ? [schemaBundle] : [],350 definition,
556 operations: definitions.map((definition) =>351 })
557 toGoogleDiscoveryRecipeOperationRecord({352 ),
558 recipeRevisionId: GOOGLE_DISCOVERY_MATERIALIZATION_REVISION_ID,353 }),
559 manifest,354 sourceHash: manifest.sourceHash,
560 definition,
561 now,
562 })),
563 };355 };
564 }),356 }),
565 getOauth2SetupConfig: ({ source }) => googleDiscoveryOauth2SetupConfig(source),357 getOauth2SetupConfig: ({ source }) => googleDiscoveryOauth2SetupConfig(source),
@@ -568,71 +360,4 @@ export const googleDiscoverySourceAdapter: SourceAdapter = { · 2 unmodified lines
568 ...input,360 ...input,
569 redirectMode: input.redirectMode ?? "loopback",361 redirectMode: input.redirectMode ?? "loopback",
570 }),362 }),
571 invokePersistedTool: ({
572 source,
573 path,
574 operation,
575 schemaBundle,
576 auth,
577 args,
578 context,
579 onElicitation,
580 }) =>
581 Effect.gen(function* () {
582 const bindingConfig = yield* googleDiscoveryBindingConfigFromSource(source);
583 const schemaRefTable = schemaBundle
584 ? decodeGoogleDiscoverySchemaRefTableJson(schemaBundle.refsJson)
585 : Either.right({});
586 if (Either.isLeft(schemaRefTable)) {
587 return yield* Effect.fail(
588 new Error(`Invalid Google Discovery schema bundle for ${path}`),
589 );
590 }
591
592 const { definition, providerData } = googleDiscoveryDefinitionFromPersistedOperation({
593 path,
594 operation,
595 });
596 const stringRefTable = Object.fromEntries(
597 Object.entries(schemaRefTable.right).map(([ref, value]) => [
598 ref,
599 typeof value === "string" ? value : JSON.stringify(value),
600 ]),
601 );
602
603 const tool = createGoogleDiscoveryToolFromDefinition({
604 definition,
605 service: providerData.service,
606 version: providerData.version,
607 rootUrl: providerData.invocation.rootUrl,
608 servicePath: providerData.invocation.servicePath,
609 path,
610 sourceKey: source.id,
611 defaultHeaders: bindingConfig.defaultHeaders ?? {},
612 credentialPlacements: auth,
613 schemaRefTable: stringRefTable,
614 });
615
616 return yield* makeToolInvokerFromTools({
617 tools: {
618 [path]: tool,
619 },
620 // Authorization was already handled by authorizePersistedToolInvocation
621 // at the workspace level, so skip the redundant tool-level interaction
622 // gate. Without this, the default "required" interaction on write tools
623 // would re-elicit approval and then merge the approval form content
624 // (e.g. { approve: true }) into the tool args, breaking schemas that
625 // use additionalProperties: false.
626 onToolInteraction: allowAllToolInteractions,
627 onElicitation,
628 }).invoke({
629 path,
630 args,
631 context,
632 }).pipe(
633 Effect.mapError((cause) =>
634 cause instanceof Error ? cause : new Error(String(cause)),
635 ),
636 );
637 }),
638};363};
packages/control-plane/src/runtime/source-adapters/graphql.ts+28−254
@@ -1,58 +1,34 @@
1import {1import {
2 allowAllToolInteractions,
3 applyCookiePlacementsToHeaders,2 applyCookiePlacementsToHeaders,
4 applyHttpQueryPlacementsToUrl,3 applyHttpQueryPlacementsToUrl,
5 applyJsonBodyPlacements,4 applyJsonBodyPlacements,
6 makeToolInvokerFromTools,
7 typeSignatureFromSchemaJson,
8} from "@executor/codemode-core";5} from "@executor/codemode-core";
9import type {
10 GraphqlToolManifest,
11} from "../graphql-tools";
12import {6import {
13 buildGraphqlToolPresentation,7 buildGraphqlToolPresentation,
14 compileGraphqlToolDefinitions,8 compileGraphqlToolDefinitions,
15 createGraphqlToolFromPersistedOperation,
16 decodeGraphqlSchemaRefTableJson,
17 extractGraphqlManifest,9 extractGraphqlManifest,
18 GraphqlToolProviderDataSchema,
19 GRAPHQL_INTROSPECTION_QUERY,10 GRAPHQL_INTROSPECTION_QUERY,
11 type GraphqlToolProviderData,
20} from "../graphql-tools";12} from "../graphql-tools";
21import type {13import type { Source } from "#schema";
22 Source,14import { StringMapSchema } from "#schema";
23 SourceRecipeRevisionId,
24 StoredSourceRecipeDocumentRecord,
25 StoredSourceRecipeOperationRecord,
26 StoredSourceRecipeSchemaBundleRecord,
27} from "#schema";
28import {
29 StringMapSchema,
30 SourceRecipeSchemaBundleIdSchema,
31} from "#schema";
32import * as Either from "effect/Either";
33import * as Effect from "effect/Effect";15import * as Effect from "effect/Effect";
34import * as Schema from "effect/Schema";16import * as Schema from "effect/Schema";
35 17
36import { namespaceFromSourceName } from "../source-names";
37import {18import {
38 contentHash,19 createGraphqlCatalogSnapshot,
39 normalizeSearchText,20 type GraphqlCatalogOperationInput,
40} from "../source-recipe-support";21} from "../source-catalog-snapshot";
41import type {22import type { SourceAdapter } from "./types";
42 SourceAdapter,
43 SourceAdapterMaterialization,
44} from "./types";
45import {23import {
46 ConnectHttpAuthSchema,24 ConnectHttpAuthSchema,
47 ConnectHttpImportAuthSchema,25 ConnectHttpImportAuthSchema,
48 createStandardToolDescriptor,
49 decodeBindingConfig,26 decodeBindingConfig,
50 decodeSourceBindingPayload,27 decodeSourceBindingPayload,
51 emptySourceBindingState,28 emptySourceBindingState,
52 encodeBindingConfig,29 encodeBindingConfig,
53 isSourceCredentialRequiredError,30 isSourceCredentialRequiredError,
54 OptionalNullableStringSchema,31 OptionalNullableStringSchema,
55 parseJsonValue,
56 SourceCredentialRequiredError,32 SourceCredentialRequiredError,
57 SourceConnectCommonFieldsSchema,33 SourceConnectCommonFieldsSchema,
58} from "./shared";34} from "./shared";
@@ -91,12 +67,6 @@ const GraphqlSourceBindingPayloadSchema = Schema.Struct({ · 32 unmodified lines
91 67
92const GRAPHQL_BINDING_CONFIG_VERSION = 1;68const GRAPHQL_BINDING_CONFIG_VERSION = 1;
93 69
94const GRAPHQL_MATERIALIZATION_REVISION_ID = "src_recipe_rev_materialization" as SourceRecipeRevisionId;
95
96const decodeGraphqlToolProviderDataJson = Schema.decodeUnknownEither(
97 Schema.parseJson(GraphqlToolProviderDataSchema),
98);
99
100const bindingHasAnyField = (70const bindingHasAnyField = (
101 value: unknown,71 value: unknown,
102 fields: readonly string[],72 fields: readonly string[],
@@ -198,92 +168,29 @@ const fetchGraphqlIntrospectionDocumentWithHeaders = (input: { · 95 unmodified lines
198 cause instanceof Error ? cause : new Error(String(cause)),168 cause instanceof Error ? cause : new Error(String(cause)),
199 });169 });
200 170
201const toGraphqlSchemaBundleRecord = (input: {171const graphqlCatalogOperationFromDefinition = (input: {
202 recipeRevisionId: SourceRecipeRevisionId;
203 manifest: Parameters<typeof buildGraphqlToolPresentation>[0]["manifest"];
204 now: number;
205}): StoredSourceRecipeSchemaBundleRecord | null => {
206 const refEntries = Object.entries(input.manifest.schemaRefTable ?? {});
207 if (refEntries.length === 0) {
208 return null;
209 }
210
211 const refs = Object.fromEntries(
212 refEntries.map(([ref, rawValue]) => {
213 try {
214 return [ref, JSON.parse(rawValue) as unknown];
215 } catch {
216 return [ref, rawValue];
217 }
218 }),
219 );
220 const refsJson = JSON.stringify(refs);
221
222 return {
223 id: SourceRecipeSchemaBundleIdSchema.make(
224 `src_recipe_bundle_${crypto.randomUUID()}`,
225 ),
226 recipeRevisionId: input.recipeRevisionId,
227 bundleKind: "json_schema_ref_map",
228 refsJson,
229 contentHash: contentHash(refsJson),
230 createdAt: input.now,
231 updatedAt: input.now,
232 };
233};
234
235const toGraphqlRecipeOperationRecord = (input: {
236 recipeRevisionId: SourceRecipeRevisionId;
237 definition: ReturnType<typeof compileGraphqlToolDefinitions>[number];172 definition: ReturnType<typeof compileGraphqlToolDefinitions>[number];
238 manifest: Parameters<typeof buildGraphqlToolPresentation>[0]["manifest"];173 manifest: Parameters<typeof buildGraphqlToolPresentation>[0]["manifest"];
239 now: number;174}): GraphqlCatalogOperationInput => {
240}): StoredSourceRecipeOperationRecord => {
241 const presentation = buildGraphqlToolPresentation({175 const presentation = buildGraphqlToolPresentation({
242 manifest: input.manifest,176 manifest: input.manifest,
243 definition: input.definition,177 definition: input.definition,
244 });178 });
245 179
246 return {180 return {
247 id: `src_recipe_op_${crypto.randomUUID()}`,
248 recipeRevisionId: input.recipeRevisionId,
249 operationKey: input.definition.toolId,
250 transportKind: "graphql",
251 toolId: input.definition.toolId,181 toolId: input.definition.toolId,
252 title: input.definition.name,182 title: input.definition.name,
253 description: input.definition.description,183 description: input.definition.description,
254 operationKind:184 effect:
255 input.definition.operationType === "query"185 input.definition.operationType === "query"
256 ? "read"186 ? "read"
257 : input.definition.operationType === "mutation"187 : "write",
258 ? "write"188 inputSchema: presentation.inputSchema,
259 : "unknown",189 outputSchema: presentation.outputSchema,
260 searchText: normalizeSearchText(190 providerData: presentation.providerData as GraphqlToolProviderData,
261 input.definition.toolId,
262 input.definition.name,
263 input.definition.description,
264 input.definition.rawToolId,
265 input.definition.group,
266 input.definition.leaf,
267 input.definition.fieldName,
268 input.definition.operationType,
269 input.definition.operationName,
270 input.definition.searchTerms.join(" "),
271 ),
272 inputSchemaJson: presentation.inputSchemaJson ?? null,
273 outputSchemaJson: presentation.outputSchemaJson ?? null,
274 providerKind: "graphql",
275 providerDataJson: presentation.providerDataJson,
276 createdAt: input.now,
277 updatedAt: input.now,
278 };191 };
279};192};
280 193
281const primaryDocument = (
282 documents: readonly StoredSourceRecipeDocumentRecord[],
283): StoredSourceRecipeDocumentRecord | null =>
284 documents.find((document) => document.documentKind === "graphql_introspection")
285 ?? null;
286
287export const graphqlSourceAdapter: SourceAdapter = {194export const graphqlSourceAdapter: SourceAdapter = {
288 key: "graphql",195 key: "graphql",
289 displayName: "GraphQL",196 displayName: "GraphQL",
@@ -291,8 +198,6 @@ export const graphqlSourceAdapter: SourceAdapter = { · 1 unmodified line
291 bindingConfigVersion: GRAPHQL_BINDING_CONFIG_VERSION,198 bindingConfigVersion: GRAPHQL_BINDING_CONFIG_VERSION,
292 providerKey: "generic_graphql",199 providerKey: "generic_graphql",
293 defaultImportAuthPolicy: "reuse_runtime",200 defaultImportAuthPolicy: "reuse_runtime",
294 primaryDocumentKind: "graphql_introspection",
295 primarySchemaBundleKind: "json_schema_ref_map",
296 connectPayloadSchema: GraphqlConnectPayloadSchema,201 connectPayloadSchema: GraphqlConnectPayloadSchema,
297 executorAddInputSchema: GraphqlExecutorAddInputSchema,202 executorAddInputSchema: GraphqlExecutorAddInputSchema,
298 executorAddHelpText: [203 executorAddHelpText: [
@@ -349,68 +254,7 @@ export const graphqlSourceAdapter: SourceAdapter = { · 50 unmodified lines
349 }),254 }),
350 shouldAutoProbe: (source) =>255 shouldAutoProbe: (source) =>
351 source.enabled && (source.status === "draft" || source.status === "probing"),256 source.enabled && (source.status === "draft" || source.status === "probing"),
352 parseManifest: ({ source, manifestJson }) =>257 syncCatalog: ({ source, resolveAuthMaterialForSlot }) =>
353 parseJsonValue<GraphqlToolManifest>({
354 label: `GraphQL manifest for ${source.id}`,
355 value: manifestJson,
356 }),
357 describePersistedOperation: ({ operation, path }) =>
358 Effect.gen(function* () {
359 const decoded = operation.providerDataJson
360 ? decodeGraphqlToolProviderDataJson(operation.providerDataJson)
361 : Either.left(new Error("Missing providerDataJson"));
362 if (Either.isLeft(decoded)) {
363 return yield* Effect.fail(
364 new Error(`Invalid GraphQL provider data for ${path}`),
365 );
366 }
367
368 const providerData = decoded.right;
369
370 return {
371 method: null,
372 pathTemplate: null,
373 rawToolId: providerData.rawToolId,
374 operationId: providerData.operationName,
375 group: providerData.group,
376 leaf: providerData.leaf,
377 tags: [],
378 searchText: normalizeSearchText(
379 path,
380 operation.toolId,
381 operation.title ?? undefined,
382 operation.description ?? undefined,
383 providerData.rawToolId ?? undefined,
384 providerData.group ?? undefined,
385 providerData.leaf ?? undefined,
386 providerData.fieldName ?? undefined,
387 providerData.operationType ?? undefined,
388 providerData.operationName ?? undefined,
389 operation.searchText,
390 ),
391 interaction: providerData.operationType === "query" ? "auto" : "required",
392 approvalLabel: providerData.operationType
393 ? `GraphQL ${providerData.operationType} ${path}`
394 : `GraphQL ${path}`,
395 } as const;
396 }),
397 searchNamespace: ({ source }) =>
398 source.namespace ?? namespaceFromSourceName(source.name),
399 createToolDescriptor: ({ source, operation, path, includeSchemas, schemaBundleId }) =>
400 createStandardToolDescriptor({
401 source,
402 operation,
403 path,
404 includeSchemas,
405 interaction: operation.operationKind === "read" ? "auto" : "required",
406 outputType: typeSignatureFromSchemaJson(
407 operation.outputSchemaJson ?? undefined,
408 "unknown",
409 320,
410 ),
411 schemaBundleId,
412 }),
413 materializeSource: ({ source, resolveAuthMaterialForSlot }) =>
414 Effect.gen(function* () {258 Effect.gen(function* () {
415 const bindingConfig = yield* graphqlBindingConfigFromSource(source);259 const bindingConfig = yield* graphqlBindingConfigFromSource(source);
416 const auth = yield* resolveAuthMaterialForSlot("import");260 const auth = yield* resolveAuthMaterialForSlot("import");
@@ -447,94 +291,24 @@ export const graphqlSourceAdapter: SourceAdapter = { · 30 unmodified lines
447 291
448 const definitions = compileGraphqlToolDefinitions(manifest);292 const definitions = compileGraphqlToolDefinitions(manifest);
449 const now = Date.now();293 const now = Date.now();
450 const schemaBundle = toGraphqlSchemaBundleRecord({
451 recipeRevisionId: GRAPHQL_MATERIALIZATION_REVISION_ID,
452 manifest,
453 now,
454 });
455 294
456 return {295 return {
457 manifestJson: JSON.stringify(manifest),296 snapshot: createGraphqlCatalogSnapshot({
458 manifestHash: manifest.sourceHash,297 source,
459 sourceHash: manifest.sourceHash,298 documents: [{
460 documents: [
461 {
462 id: `src_recipe_doc_${crypto.randomUUID()}`,
463 recipeRevisionId: GRAPHQL_MATERIALIZATION_REVISION_ID,
464 documentKind: "graphql_introspection",299 documentKind: "graphql_introspection",
465 documentKey: source.endpoint,300 documentKey: source.endpoint,
466 contentText: graphqlDocument,301 contentText: graphqlDocument,
467 contentHash: contentHash(graphqlDocument),
468 fetchedAt: now,302 fetchedAt: now,
469 createdAt: now,303 }],
470 updatedAt: now,304 operations: definitions.map((definition) =>
471 },305 graphqlCatalogOperationFromDefinition({
472 ],306 definition,
473 schemaBundles: schemaBundle ? [schemaBundle] : [],307 manifest,
474 operations: definitions.map((definition) =>308 })
475 toGraphqlRecipeOperationRecord({309 ),
476 recipeRevisionId: GRAPHQL_MATERIALIZATION_REVISION_ID,310 }),
477 definition,311 sourceHash: manifest.sourceHash,
478 manifest,312 };
479 now,
480 }),
481 ),
482 } satisfies SourceAdapterMaterialization;
483 }),
484 invokePersistedTool: ({
485 source,
486 path,
487 operation,
488 schemaBundle,
489 auth,
490 args,
491 context,
492 onElicitation,
493 }) =>
494 Effect.gen(function* () {
495 const bindingConfig = yield* graphqlBindingConfigFromSource(source);
496 const schemaRefTable = schemaBundle
497 ? decodeGraphqlSchemaRefTableJson(schemaBundle.refsJson)
498 : Either.right({});
499
500 if (Either.isLeft(schemaRefTable)) {
501 return yield* Effect.fail(
502 new Error(`Invalid GraphQL schema bundle for ${path}`),
503 );
504 }
505 if (!operation.providerDataJson) {
506 return yield* Effect.fail(
507 new Error(`Missing GraphQL provider data for ${path}`),
508 );
509 }
510
511 const tool = createGraphqlToolFromPersistedOperation({
512 path,
513 sourceKey: source.id,
514 endpoint: source.endpoint,
515 description: operation.description ?? operation.title ?? undefined,
516 inputSchemaJson: operation.inputSchemaJson ?? undefined,
517 outputSchemaJson: operation.outputSchemaJson ?? undefined,
518 providerDataJson: operation.providerDataJson,
519 schemaRefTable: schemaRefTable.right,
520 defaultHeaders: bindingConfig.defaultHeaders ?? {},
521 credentialPlacements: auth,
522 });
523
524 return yield* makeToolInvokerFromTools({
525 tools: {
526 [path]: tool,
527 },
528 onToolInteraction: allowAllToolInteractions,
529 onElicitation,
530 }).invoke({
531 path,
532 args,
533 context,
534 }).pipe(
535 Effect.mapError((cause) =>
536 cause instanceof Error ? cause : new Error(String(cause)),
537 ),
538 );
539 }),313 }),
540};314};
packages/control-plane/src/runtime/source-adapters/index.ts+1−5
@@ -1,4 +1,4 @@
1import type { Source, StoredSourceRecipeOperationRecord } from "#schema";1import type { Source } from "#schema";
2import * as Schema from "effect/Schema";2import * as Schema from "effect/Schema";
3 3
4import { graphqlSourceAdapter } from "./graphql";4import { graphqlSourceAdapter } from "./graphql";
@@ -67,10 +67,6 @@ export const getSourceAdapterForSource = (source: Pick<Source, "kind">): SourceA · 62 unmodified lines
67export const sourceBindingStateFromSource = (source: Source) =>67export const sourceBindingStateFromSource = (source: Source) =>
68 getSourceAdapterForSource(source).bindingStateFromSource(source);68 getSourceAdapterForSource(source).bindingStateFromSource(source);
69 69
70export const getSourceAdapterForOperation = (
71 operation: Pick<StoredSourceRecipeOperationRecord, "providerKind">,
72): SourceAdapter => getSourceAdapter(operation.providerKind);
73
74export const hasSourceAdapterFamily = (70export const hasSourceAdapterFamily = (
75 key: string,71 key: string,
76 family: SourceAdapter["family"],72 family: SourceAdapter["family"],
packages/control-plane/src/runtime/source-adapters/internal.ts+22−37
@@ -4,7 +4,6 @@ import type { Source } from "#schema"; · 3 unmodified lines
4 4
5import type { SourceAdapter } from "./types";5import type { SourceAdapter } from "./types";
6import {6import {
7 createStandardToolDescriptor,
8 decodeBindingConfig,7 decodeBindingConfig,
9 decodeSourceBindingPayload,8 decodeSourceBindingPayload,
10 emptySourceBindingState,9 emptySourceBindingState,
@@ -58,8 +57,6 @@ export const internalSourceAdapter: SourceAdapter = { · 47 unmodified lines
58 bindingConfigVersion: INTERNAL_BINDING_CONFIG_VERSION,57 bindingConfigVersion: INTERNAL_BINDING_CONFIG_VERSION,
59 providerKey: "generic_internal",58 providerKey: "generic_internal",
60 defaultImportAuthPolicy: "none",59 defaultImportAuthPolicy: "none",
61 primaryDocumentKind: null,
62 primarySchemaBundleKind: null,
63 connectPayloadSchema: null,60 connectPayloadSchema: null,
64 executorAddInputSchema: null,61 executorAddInputSchema: null,
65 executorAddHelpText: null,62 executorAddHelpText: null,
@@ -102,40 +99,28 @@ export const internalSourceAdapter: SourceAdapter = { · 36 unmodified lines
102 };99 };
103 }),100 }),
104 shouldAutoProbe: () => false,101 shouldAutoProbe: () => false,
105 parseManifest: () => Effect.succeed(null),102 syncCatalog: () => Effect.succeed({
106 describePersistedOperation: ({ operation, path }) =>103 snapshot: {
107 Effect.succeed({104 version: "ir.v1.snapshot",
108 method: null,105 import: {
109 pathTemplate: null,106 sourceKind: "custom",
110 rawToolId: null,107 adapterKey: "internal",
111 operationId: null,108 importerVersion: "ir.v1.internal",
112 group: null,109 importedAt: new Date().toISOString(),
113 leaf: null,110 sourceConfigHash: "internal",
114 tags: [],111 },
115 searchText: [path, operation.toolId, operation.title ?? "", operation.description ?? "", operation.searchText]112 catalog: {
116 .filter((part) => part.length > 0)113 version: "ir.v1",
117 .join(" ")114 documents: {},
118 .toLowerCase(),115 resources: {},
119 interaction: "auto",116 scopes: {},
120 approvalLabel: null,117 symbols: {},
121 } as const),118 capabilities: {},
122 createToolDescriptor: ({ source, operation, path, includeSchemas, schemaBundleId }) =>119 executables: {},
123 createStandardToolDescriptor({120 responseSets: {},
124 source,121 diagnostics: {},
125 operation,122 },
126 path,123 },
127 includeSchemas,
128 interaction: "auto",
129 schemaBundleId,
130 }),
131 materializeSource: () => Effect.succeed({
132 manifestJson: null,
133 manifestHash: null,
134 sourceHash: null,124 sourceHash: null,
135 documents: [],
136 schemaBundles: [],
137 operations: [],
138 }),125 }),
139 invokePersistedTool: ({ path }) =>
140 Effect.fail(new Error(`Unsupported stored tool provider for ${path}`)),
141};126};
packages/control-plane/src/runtime/source-adapters/mcp.ts+32−180
@@ -1,10 +1,7 @@
1import {1import {
2 allowAllToolInteractions,
3 applyCookiePlacementsToHeaders,2 applyCookiePlacementsToHeaders,
4 makeToolInvokerFromTools,
5} from "@executor/codemode-core";3} from "@executor/codemode-core";
6import {4import {
7 createMcpToolsFromManifest,
8 createSdkMcpConnector,5 createSdkMcpConnector,
9 discoverMcpToolsFromConnector,6 discoverMcpToolsFromConnector,
10 type McpToolManifest,7 type McpToolManifest,
@@ -12,8 +9,6 @@ import { · 1 unmodified line
12} from "@executor/codemode-mcp";9} from "@executor/codemode-mcp";
13import type {10import type {
14 Source,11 Source,
15 SourceRecipeRevisionId,
16 StoredSourceRecipeOperationRecord,
17} from "#schema";12} from "#schema";
18import {13import {
19 SourceTransportSchema,14 SourceTransportSchema,
@@ -22,22 +17,23 @@ import { · 2 unmodified lines
22import * as Effect from "effect/Effect";17import * as Effect from "effect/Effect";
23import * as Schema from "effect/Schema";18import * as Schema from "effect/Schema";
24 19
20import {
21 createMcpCatalogSnapshot,
22 type McpCatalogOperationInput,
23} from "../source-catalog-snapshot";
25import {24import {
26 contentHash,25 contentHash,
27 normalizeSearchText,26 type SourceCatalogSyncResult,
28 type SourceRecipeMaterialization,27} from "../source-catalog-support";
29} from "../source-recipe-support";
30import { namespaceFromSourceName } from "../source-names";28import { namespaceFromSourceName } from "../source-names";
31import type { SourceAdapter, SourceAdapterMaterialization } from "./types";29import type { SourceAdapter } from "./types";
32import {30import {
33 createStandardToolDescriptor,
34 decodeBindingConfig,31 decodeBindingConfig,
35 decodeSourceBindingPayload,32 decodeSourceBindingPayload,
36 emptySourceBindingState,33 emptySourceBindingState,
37 encodeBindingConfig,34 encodeBindingConfig,
38 McpConnectFieldsSchema,35 McpConnectFieldsSchema,
39 OptionalNullableStringSchema,36 OptionalNullableStringSchema,
40 parseJsonValue,
41 SourceConnectCommonFieldsSchema,37 SourceConnectCommonFieldsSchema,
42} from "./shared";38} from "./shared";
43 39
@@ -87,17 +83,6 @@ const McpSourceBindingPayloadSchema = Schema.Struct({ · 43 unmodified lines
87 83
88const MCP_BINDING_CONFIG_VERSION = 1;84const MCP_BINDING_CONFIG_VERSION = 1;
89 85
90const McpToolProviderDataSchema = Schema.Struct({
91 kind: Schema.Literal("mcp"),
92 toolId: Schema.String,
93 toolName: Schema.String,
94 description: Schema.NullOr(Schema.String),
95});
96
97const decodeMcpToolProviderDataJson = Schema.decodeUnknownEither(
98 Schema.parseJson(McpToolProviderDataSchema),
99);
100
101const bindingHasAnyField = (86const bindingHasAnyField = (
102 value: unknown,87 value: unknown,
103 fields: readonly string[],88 fields: readonly string[],
@@ -137,43 +122,25 @@ const mcpBindingConfigFromSource = ( · 33 unmodified lines
137 } satisfies McpBindingConfig;122 } satisfies McpBindingConfig;
138 });123 });
139 124
140const toMcpRecipeOperationRecord = (input: {125const mcpCatalogOperationFromManifestEntry = (entry: McpToolManifestEntry): McpCatalogOperationInput => ({
141 recipeRevisionId: SourceRecipeRevisionId;126 toolId: entry.toolId,
142 entry: McpToolManifestEntry;127 title: entry.toolName,
143 now: number;128 description: entry.description ?? null,
144}): StoredSourceRecipeOperationRecord => ({129 effect: "action",
145 id: `src_recipe_op_${crypto.randomUUID()}`,130 inputSchema: entry.inputSchema,
146 recipeRevisionId: input.recipeRevisionId,131 outputSchema: entry.outputSchema,
147 operationKey: input.entry.toolId,132 providerData: {
148 transportKind: "mcp",133 toolId: entry.toolId,
149 toolId: input.entry.toolId,134 toolName: entry.toolName,
150 title: input.entry.toolName,135 description: entry.description ?? null,
151 description: input.entry.description ?? null,136 },
152 operationKind: "unknown",
153 searchText: normalizeSearchText(
154 input.entry.toolId,
155 input.entry.toolName,
156 input.entry.description ?? undefined,
157 "mcp",
158 ),
159 inputSchemaJson: input.entry.inputSchemaJson ?? null,
160 outputSchemaJson: input.entry.outputSchemaJson ?? null,
161 providerKind: "mcp",
162 providerDataJson: JSON.stringify({
163 kind: "mcp",
164 toolId: input.entry.toolId,
165 toolName: input.entry.toolName,
166 description: input.entry.description ?? null,
167 }),
168 createdAt: input.now,
169 updatedAt: input.now,
170});137});
171 138
172export const materializationFromMcpManifestEntries = (input: {139export const catalogSyncResultFromMcpManifestEntries = (input: {
173 recipeRevisionId: SourceRecipeRevisionId;140 source: Source;
174 endpoint: string;141 endpoint: string;
175 manifestEntries: readonly McpToolManifestEntry[];142 manifestEntries: readonly McpToolManifestEntry[];
176}): SourceRecipeMaterialization => {143}): SourceCatalogSyncResult => {
177 const now = Date.now();144 const now = Date.now();
178 const manifest: McpToolManifest = {145 const manifest: McpToolManifest = {
179 version: 1,146 version: 1,
@@ -183,30 +150,17 @@ export const materializationFromMcpManifestEntries = (input: { · 3 unmodified lines
183 const manifestHash = contentHash(manifestJson);150 const manifestHash = contentHash(manifestJson);
184 151
185 return {152 return {
186 manifestJson,153 snapshot: createMcpCatalogSnapshot({
187 manifestHash,154 source: input.source,
188 sourceHash: manifestHash,155 documents: [{
189 documents: [
190 {
191 id: `src_recipe_doc_${crypto.randomUUID()}`,
192 recipeRevisionId: input.recipeRevisionId,
193 documentKind: "mcp_manifest",156 documentKind: "mcp_manifest",
194 documentKey: input.endpoint,157 documentKey: input.endpoint,
195 contentText: manifestJson,158 contentText: manifestJson,
196 contentHash: manifestHash,
197 fetchedAt: now,159 fetchedAt: now,
198 createdAt: now,160 }],
199 updatedAt: now,161 operations: input.manifestEntries.map(mcpCatalogOperationFromManifestEntry),
200 },162 }),
201 ],163 sourceHash: manifestHash,
202 schemaBundles: [],
203 operations: input.manifestEntries.map((entry) =>
204 toMcpRecipeOperationRecord({
205 recipeRevisionId: input.recipeRevisionId,
206 entry,
207 now,
208 })
209 ),
210 };164 };
211};165};
212 166
@@ -217,8 +171,6 @@ export const mcpSourceAdapter: SourceAdapter = { · 4 unmodified lines
217 bindingConfigVersion: MCP_BINDING_CONFIG_VERSION,171 bindingConfigVersion: MCP_BINDING_CONFIG_VERSION,
218 providerKey: "generic_mcp",172 providerKey: "generic_mcp",
219 defaultImportAuthPolicy: "reuse_runtime",173 defaultImportAuthPolicy: "reuse_runtime",
220 primaryDocumentKind: "mcp_manifest",
221 primarySchemaBundleKind: null,
222 connectPayloadSchema: McpConnectPayloadSchema,174 connectPayloadSchema: McpConnectPayloadSchema,
223 executorAddInputSchema: McpExecutorAddInputSchema,175 executorAddInputSchema: McpExecutorAddInputSchema,
224 executorAddHelpText: [176 executorAddHelpText: [
@@ -280,53 +232,7 @@ export const mcpSourceAdapter: SourceAdapter = { · 55 unmodified lines
280 };232 };
281 }),233 }),
282 shouldAutoProbe: () => false,234 shouldAutoProbe: () => false,
283 parseManifest: ({ source, manifestJson }) =>235 syncCatalog: ({ source, resolveAuthMaterialForSlot }) =>
284 parseJsonValue<McpToolManifest>({
285 label: `MCP manifest for ${source.id}`,
286 value: manifestJson,
287 }),
288 describePersistedOperation: ({ operation, path }) =>
289 Effect.gen(function* () {
290 const decoded = operation.providerDataJson
291 ? decodeMcpToolProviderDataJson(operation.providerDataJson)
292 : null;
293 if (decoded && decoded._tag === "Left") {
294 return yield* Effect.fail(
295 new Error(`Invalid MCP provider data for ${path}`),
296 );
297 }
298
299 const providerData = decoded?._tag === "Right" ? decoded.right : null;
300
301 return {
302 method: null,
303 pathTemplate: null,
304 rawToolId: providerData?.toolId ?? null,
305 operationId: null,
306 group: null,
307 leaf: null,
308 tags: [],
309 searchText: normalizeSearchText(
310 path,
311 operation.toolId,
312 providerData?.toolName ?? operation.title ?? undefined,
313 providerData?.description ?? operation.description ?? undefined,
314 operation.searchText,
315 ),
316 interaction: "auto",
317 approvalLabel: null,
318 } as const;
319 }),
320 createToolDescriptor: ({ source, operation, path, includeSchemas, schemaBundleId }) =>
321 createStandardToolDescriptor({
322 source,
323 operation,
324 path,
325 includeSchemas,
326 interaction: "auto",
327 schemaBundleId,
328 }),
329 materializeSource: ({ source, resolveAuthMaterialForSlot }) =>
330 Effect.gen(function* () {236 Effect.gen(function* () {
331 const bindingConfig = yield* mcpBindingConfigFromSource(source);237 const bindingConfig = yield* mcpBindingConfigFromSource(source);
332 const auth = yield* resolveAuthMaterialForSlot("import");238 const auth = yield* resolveAuthMaterialForSlot("import");
@@ -362,64 +268,10 @@ export const mcpSourceAdapter: SourceAdapter = { · 29 unmodified lines
362 ),268 ),
363 );269 );
364 270
365 return materializationFromMcpManifestEntries({271 return catalogSyncResultFromMcpManifestEntries({
366 recipeRevisionId: "src_recipe_rev_materialization" as SourceRecipeRevisionId,272 source,
367 endpoint: source.endpoint,273 endpoint: source.endpoint,
368 manifestEntries: discovered.manifest.tools,274 manifestEntries: discovered.manifest.tools,
369 }) satisfies SourceAdapterMaterialization;
370 }),
371 invokePersistedTool: ({
372 source,
373 path,
374 manifestJson,
375 auth,
376 args,
377 context,
378 onElicitation,
379 }) =>
380 Effect.gen(function* () {
381 const bindingConfig = yield* mcpBindingConfigFromSource(source);
382 const manifest = yield* parseJsonValue<McpToolManifest>({
383 label: `MCP manifest for ${source.id}`,
384 value: manifestJson,
385 });275 });
386 if (manifest === null) {
387 return yield* Effect.fail(
388 new Error(`Missing MCP manifest for ${source.id}`),
389 );
390 }
391
392 const tools = createMcpToolsFromManifest({
393 manifest,
394 connect: createSdkMcpConnector({
395 endpoint: source.endpoint,
396 transport: bindingConfig.transport ?? undefined,
397 queryParams: {
398 ...(bindingConfig.queryParams ?? {}),
399 ...auth.queryParams,
400 },
401 headers: headersWithAuthCookies({
402 headers: bindingConfig.headers ?? {},
403 authHeaders: auth.headers,
404 authCookies: auth.cookies,
405 }),
406 }),
407 namespace: source.namespace ?? namespaceFromSourceName(source.name),
408 sourceKey: source.id,
409 });
410
411 return yield* makeToolInvokerFromTools({
412 tools,
413 onToolInteraction: allowAllToolInteractions,
414 onElicitation,
415 }).invoke({
416 path,
417 args,
418 context,
419 }).pipe(
420 Effect.mapError((cause) =>
421 cause instanceof Error ? cause : new Error(String(cause)),
422 ),
423 );
424 }),276 }),
425};277};
packages/control-plane/src/runtime/source-adapters/openapi.ts+27−295
@@ -4,55 +4,34 @@ import { · 3 unmodified lines
4 HttpClientRequest,4 HttpClientRequest,
5} from "@effect/platform";5} from "@effect/platform";
6import {6import {
7 allowAllToolInteractions,
8 applyCookiePlacementsToHeaders,7 applyCookiePlacementsToHeaders,
9 applyHttpQueryPlacementsToUrl,8 applyHttpQueryPlacementsToUrl,
10 makeToolInvokerFromTools,
11} from "@executor/codemode-core";9} from "@executor/codemode-core";
12import {10import {
13 buildOpenApiToolPresentation,11 buildOpenApiToolPresentation,
14 compileOpenApiToolDefinitions,12 compileOpenApiToolDefinitions,
15 createOpenApiToolFromDefinition,13 type OpenApiToolProviderData,
16 extractOpenApiManifest,14 extractOpenApiManifest,
17 openApiOutputTypeSignatureFromSchemaJson,
18 OpenApiRefHintTableSchema,
19 OpenApiToolProviderDataSchema,
20 type OpenApiToolManifest,
21} from "@executor/codemode-openapi";15} from "@executor/codemode-openapi";
22import type {16import type { Source } from "#schema";
23 Source,17import { StringMapSchema } from "#schema";
24 SourceRecipeRevisionId,
25 StoredSourceRecipeDocumentRecord,
26 StoredSourceRecipeOperationRecord,
27 StoredSourceRecipeSchemaBundleRecord,
28} from "#schema";
29import {
30 StringMapSchema,
31 SourceRecipeSchemaBundleIdSchema,
32} from "#schema";
33import * as Either from "effect/Either";
34import * as Effect from "effect/Effect";18import * as Effect from "effect/Effect";
35import * as Schema from "effect/Schema";19import * as Schema from "effect/Schema";
36 20
37import {21import {
38 contentHash,22 createOpenApiCatalogSnapshot,
39 normalizeSearchText,23 type OpenApiCatalogOperationInput,
40} from "../source-recipe-support";24} from "../source-catalog-snapshot";
41import type {25import type { SourceAdapter } from "./types";
42 SourceAdapter,
43 SourceAdapterMaterialization,
44} from "./types";
45import {26import {
46 ConnectHttpAuthSchema,27 ConnectHttpAuthSchema,
47 ConnectHttpImportAuthSchema,28 ConnectHttpImportAuthSchema,
48 createStandardToolDescriptor,
49 decodeBindingConfig,29 decodeBindingConfig,
50 decodeSourceBindingPayload,30 decodeSourceBindingPayload,
51 emptySourceBindingState,31 emptySourceBindingState,
52 encodeBindingConfig,32 encodeBindingConfig,
53 isSourceCredentialRequiredError,33 isSourceCredentialRequiredError,
54 OptionalNullableStringSchema,34 OptionalNullableStringSchema,
55 parseJsonValue,
56 SourceCredentialRequiredError,35 SourceCredentialRequiredError,
57 SourceConnectCommonFieldsSchema,36 SourceConnectCommonFieldsSchema,
58} from "./shared";37} from "./shared";
@@ -98,16 +77,6 @@ type OpenApiBindingConfig = { · 39 unmodified lines
98 77
99const OPENAPI_BINDING_CONFIG_VERSION = 1;78const OPENAPI_BINDING_CONFIG_VERSION = 1;
100 79
101const OPENAPI_MATERIALIZATION_REVISION_ID = "src_recipe_rev_materialization" as SourceRecipeRevisionId;
102
103const decodeOpenApiProviderDataJson = Schema.decodeUnknownEither(
104 Schema.parseJson(OpenApiToolProviderDataSchema),
105);
106
107const decodeOpenApiRefHintTableJson = Schema.decodeUnknownEither(
108 Schema.parseJson(OpenApiRefHintTableSchema),
109);
110
111const bindingHasAnyField = (80const bindingHasAnyField = (
112 value: unknown,81 value: unknown,
113 fields: readonly string[],82 fields: readonly string[],
@@ -198,137 +167,30 @@ const fetchOpenApiDocumentWithHeaders = (input: { · 84 unmodified lines
198 );167 );
199 }).pipe(Effect.provide(FetchHttpClient.layer));168 }).pipe(Effect.provide(FetchHttpClient.layer));
200 169
201const toOpenApiRecipeOperationRecord = (input: {170const openApiCatalogOperationFromDefinition = (
202 recipeRevisionId: SourceRecipeRevisionId;171 definition: ReturnType<typeof compileOpenApiToolDefinitions>[number],
203 definition: ReturnType<typeof compileOpenApiToolDefinitions>[number];172): OpenApiCatalogOperationInput => {
204 now: number;
205}): StoredSourceRecipeOperationRecord => {
206 const presentation = buildOpenApiToolPresentation({173 const presentation = buildOpenApiToolPresentation({
207 definition: input.definition,174 definition,
208 });175 });
209 const method = input.definition.method.toUpperCase();176 const method = definition.method.toUpperCase();
210 177
211 return {178 return {
212 id: `src_recipe_op_${crypto.randomUUID()}`,179 toolId: definition.toolId,
213 recipeRevisionId: input.recipeRevisionId,180 title: definition.name,
214 operationKey: input.definition.toolId,181 description: definition.description,
215 transportKind: "http",182 effect:
216 toolId: input.definition.toolId,
217 title: input.definition.name,
218 description: input.definition.description,
219 operationKind:
220 method === "GET" || method === "HEAD"183 method === "GET" || method === "HEAD"
221 ? "read"184 ? "read"
222 : method === "DELETE"185 : method === "DELETE"
223 ? "delete"186 ? "delete"
224 : "write",187 : "write",
225 searchText: normalizeSearchText(188 inputSchema: presentation.inputSchema,
226 input.definition.toolId,189 outputSchema: presentation.outputSchema,
227 input.definition.name,190 providerData: presentation.providerData as OpenApiToolProviderData,
228 input.definition.description,
229 input.definition.rawToolId,
230 input.definition.operationId ?? undefined,
231 input.definition.method,
232 input.definition.path,
233 input.definition.group,
234 input.definition.leaf,
235 input.definition.tags.join(" "),
236 ),
237 inputSchemaJson: presentation.inputSchemaJson ?? null,
238 outputSchemaJson: presentation.outputSchemaJson ?? null,
239 providerKind: "openapi",
240 providerDataJson: presentation.providerDataJson,
241 createdAt: input.now,
242 updatedAt: input.now,
243};
244};
245
246const 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
280const openApiDefinitionFromPersistedOperation = (input: {
281 source: Source;
282 path: string;
283 operation: StoredSourceRecipeOperationRecord;
284}) => {
285 const decoded = input.operation.providerDataJson
286 ? decodeOpenApiProviderDataJson(input.operation.providerDataJson)
287 : Either.left(new Error("Missing providerDataJson"));
288
289 if (Either.isLeft(decoded)) {
290 throw new Error(`Invalid OpenAPI provider data for ${input.path}`);
291 }
292
293 return {
294 toolId: input.operation.toolId,
295 rawToolId: decoded.right.rawToolId,
296 ...(decoded.right.operationId
297 ? { operationId: decoded.right.operationId }
298 : {}),
299 name: input.operation.title ?? input.operation.toolId,
300 description:
301 input.operation.description ??
302 `${decoded.right.method.toUpperCase()} ${decoded.right.path}`,
303 group: decoded.right.group,
304 leaf: decoded.right.leaf,
305 tags: decoded.right.tags,
306 ...(decoded.right.versionSegment
307 ? { versionSegment: decoded.right.versionSegment }
308 : {}),
309 method: decoded.right.method,
310 path: decoded.right.path,
311 invocation: decoded.right.invocation,
312 operationHash: decoded.right.operationHash,
313 typing: {
314 ...(input.operation.inputSchemaJson
315 ? { inputSchemaJson: input.operation.inputSchemaJson }
316 : {}),
317 ...(input.operation.outputSchemaJson
318 ? { outputSchemaJson: input.operation.outputSchemaJson }
319 : {}),
320 },
321 ...(decoded.right.documentation
322 ? { documentation: decoded.right.documentation }
323 : {}),
324 };191 };
325};192};
326 193
327const primaryDocument = (
328 documents: readonly StoredSourceRecipeDocumentRecord[],
329): StoredSourceRecipeDocumentRecord | null =>
330 documents.find((document) => document.documentKind === "openapi") ?? null;
331
332export const openApiSourceAdapter: SourceAdapter = {194export const openApiSourceAdapter: SourceAdapter = {
333 key: "openapi",195 key: "openapi",
334 displayName: "OpenAPI",196 displayName: "OpenAPI",
@@ -336,8 +198,6 @@ export const openApiSourceAdapter: SourceAdapter = { · 1 unmodified line
336 bindingConfigVersion: OPENAPI_BINDING_CONFIG_VERSION,198 bindingConfigVersion: OPENAPI_BINDING_CONFIG_VERSION,
337 providerKey: "generic_http",199 providerKey: "generic_http",
338 defaultImportAuthPolicy: "reuse_runtime",200 defaultImportAuthPolicy: "reuse_runtime",
339 primaryDocumentKind: "openapi",
340 primarySchemaBundleKind: "json_schema_ref_map",
341 connectPayloadSchema: OpenApiConnectPayloadSchema,201 connectPayloadSchema: OpenApiConnectPayloadSchema,
342 executorAddInputSchema: OpenApiExecutorAddInputSchema,202 executorAddInputSchema: OpenApiExecutorAddInputSchema,
343 executorAddHelpText: [203 executorAddHelpText: [
@@ -398,65 +258,7 @@ export const openApiSourceAdapter: SourceAdapter = { · 54 unmodified lines
398 }),258 }),
399 shouldAutoProbe: (source) =>259 shouldAutoProbe: (source) =>
400 source.enabled && (source.status === "draft" || source.status === "probing"),260 source.enabled && (source.status === "draft" || source.status === "probing"),
401 parseManifest: ({ source, manifestJson }) =>261 syncCatalog: ({ source, resolveAuthMaterialForSlot }) =>
402 parseJsonValue<OpenApiToolManifest>({
403 label: `OpenAPI manifest for ${source.id}`,
404 value: manifestJson,
405 }),
406 describePersistedOperation: ({ operation, path }) =>
407 Effect.gen(function* () {
408 const decoded = operation.providerDataJson
409 ? decodeOpenApiProviderDataJson(operation.providerDataJson)
410 : Either.left(new Error("Missing providerDataJson"));
411 if (Either.isLeft(decoded)) {
412 return yield* Effect.fail(
413 new Error(`Invalid OpenAPI provider data for ${path}`),
414 );
415 }
416
417 const providerData = decoded.right;
418 const method = providerData.method.toUpperCase();
419
420 return {
421 method: providerData.method,
422 pathTemplate: providerData.path,
423 rawToolId: providerData.rawToolId,
424 operationId: providerData.operationId ?? null,
425 group: providerData.group,
426 leaf: providerData.leaf,
427 tags: providerData.tags,
428 searchText: normalizeSearchText(
429 path,
430 operation.toolId,
431 operation.title ?? undefined,
432 operation.description ?? undefined,
433 providerData.rawToolId,
434 providerData.operationId ?? undefined,
435 method,
436 providerData.path,
437 providerData.group,
438 providerData.leaf,
439 providerData.tags.join(" "),
440 operation.searchText,
441 ),
442 interaction: method === "GET" || method === "HEAD" ? "auto" : "required",
443 approvalLabel: `${method} ${providerData.path}`,
444 } as const;
445 }),
446 createToolDescriptor: ({ source, operation, path, includeSchemas, schemaBundleId }) =>
447 createStandardToolDescriptor({
448 source,
449 operation,
450 path,
451 includeSchemas,
452 interaction: operation.operationKind === "read" ? "auto" : "required",
453 outputType: openApiOutputTypeSignatureFromSchemaJson(
454 operation.outputSchemaJson ?? undefined,
455 320,
456 ),
457 schemaBundleId,
458 }),
459 materializeSource: ({ source, resolveAuthMaterialForSlot }) =>
460 Effect.gen(function* () {262 Effect.gen(function* () {
461 const bindingConfig = yield* openApiBindingConfigFromSource(source);263 const bindingConfig = yield* openApiBindingConfigFromSource(source);
462 264
@@ -491,89 +293,19 @@ export const openApiSourceAdapter: SourceAdapter = { · 28 unmodified lines
491 293
492 const definitions = compileOpenApiToolDefinitions(manifest);294 const definitions = compileOpenApiToolDefinitions(manifest);
493 const now = Date.now();295 const now = Date.now();
494 const schemaBundle = toOpenApiSchemaBundleRecord({
495 recipeRevisionId: OPENAPI_MATERIALIZATION_REVISION_ID,
496 refTable: manifest.refHintTable,
497 now,
498 });
499 296
500 return {297 return {
501 manifestJson: JSON.stringify(manifest),298 snapshot: createOpenApiCatalogSnapshot({
502 manifestHash: manifest.sourceHash,299 source,
503 sourceHash: manifest.sourceHash,300 documents: [{
504 documents: [
505 {
506 id: `src_recipe_doc_${crypto.randomUUID()}`,
507 recipeRevisionId: OPENAPI_MATERIALIZATION_REVISION_ID,
508 documentKind: "openapi",301 documentKind: "openapi",
509 documentKey: bindingConfig.specUrl,302 documentKey: bindingConfig.specUrl,
510 contentText: openApiDocument,303 contentText: openApiDocument,
511 contentHash: contentHash(openApiDocument),
512 fetchedAt: now,304 fetchedAt: now,
513 createdAt: now,305 }],
514 updatedAt: now,306 operations: definitions.map(openApiCatalogOperationFromDefinition),
515 },
516 ],
517 schemaBundles: schemaBundle ? [schemaBundle] : [],
518 operations: definitions.map((definition) =>
519 toOpenApiRecipeOperationRecord({
520 recipeRevisionId: OPENAPI_MATERIALIZATION_REVISION_ID,
521 definition,
522 now,
523 }),
524 ),
525 } satisfies SourceAdapterMaterialization;
526 }),
527 invokePersistedTool: ({
528 source,
529 path,
530 operation,
531 schemaBundle,
532 auth,
533 args,
534 context,
535 onElicitation,
536 }) =>
537 Effect.gen(function* () {
538 const bindingConfig = yield* openApiBindingConfigFromSource(source);
539 const refHintTable = schemaBundle
540 ? decodeOpenApiRefHintTableJson(schemaBundle.refsJson)
541 : Either.right({});
542
543 if (Either.isLeft(refHintTable)) {
544 return yield* Effect.fail(
545 new Error(`Invalid OpenAPI schema bundle for ${path}`),
546 );
547 }
548
549 const tool = createOpenApiToolFromDefinition({
550 definition: openApiDefinitionFromPersistedOperation({
551 source,
552 path,
553 operation,
554 }),307 }),
555 path,308 sourceHash: manifest.sourceHash,
556 sourceKey: source.id,309 };
557 baseUrl: source.endpoint,
558 defaultHeaders: bindingConfig.defaultHeaders ?? {},
559 credentialPlacements: auth,
560 refHintTable: refHintTable.right,
561 });
562
563 return yield* makeToolInvokerFromTools({
564 tools: {
565 [path]: tool,
566 },
567 onToolInteraction: allowAllToolInteractions,
568 onElicitation,
569 }).invoke({
570 path,
571 args,
572 context,
573 }).pipe(
574 Effect.mapError((cause) =>
575 cause instanceof Error ? cause : new Error(String(cause)),
576 ),
577 );
578 }),310 }),
579};311};
packages/control-plane/src/runtime/source-adapters/shared.ts+0−50
@@ -1,9 +1,3 @@
1import {
2 type ToolDescriptor,
3 type ToolPath,
4 type ToolSchemaBundle,
5 typeSignatureFromSchemaJson,
6} from "@executor/codemode-core";
7import {1import {
8 SourceOauthClientInputSchema,2 SourceOauthClientInputSchema,
9 type CredentialSlot,3 type CredentialSlot,
@@ -15,10 +9,8 @@ import { · 5 unmodified lines
15} from "#schema";9} from "#schema";
16import type {10import type {
17 SecretRef,11 SecretRef,
18 Source,
19 SourceBinding,12 SourceBinding,
20 SourceTransport,13 SourceTransport,
21 StoredSourceRecipeOperationRecord,
22 StringMap,14 StringMap,
23} from "#schema";15} from "#schema";
24import * as Effect from "effect/Effect";16import * as Effect from "effect/Effect";
@@ -28,8 +20,6 @@ import { · 3 unmodified lines
28 TrimmedNonEmptyStringSchema,20 TrimmedNonEmptyStringSchema,
29} from "../../api/string-schemas";21} from "../../api/string-schemas";
30 22
31const asToolPath = (value: string): ToolPath => value as ToolPath;
32
33export const OptionalNullableStringSchema = Schema.optional(23export const OptionalNullableStringSchema = Schema.optional(
34 Schema.NullOr(Schema.String),24 Schema.NullOr(Schema.String),
35);25);
@@ -236,43 +226,3 @@ export const decodeSourceBindingPayload = <A>(input: { · 200 unmodified lines
236 );226 );
237 },227 },
238 });228 });
239
240export 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
250export 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});
packages/control-plane/src/runtime/source-adapters/types.ts+3−74
@@ -1,10 +1,4 @@
1import type {1import type {
2 OnElicitation,
3 ToolDescriptor,
4 ToolSchemaBundle,
5} from "@executor/codemode-core";
6import type {
7 AccountId,
8 CredentialSlot,2 CredentialSlot,
9 OAuth2ClientAuthenticationMethod,3 OAuth2ClientAuthenticationMethod,
10 SourceOauthClientInput,4 SourceOauthClientInput,
@@ -12,10 +6,6 @@ import type { · 1 unmodified line
12 SourceBinding,6 SourceBinding,
13 SourceImportAuthPolicy,7 SourceImportAuthPolicy,
14 SourceTransport,8 SourceTransport,
15 StoredSourceRecipeDocumentRecord,
16 StoredSourceRecipeOperationRecord,
17 StoredSourceRecipeSchemaBundleRecord,
18 StoredSourceRecipeRevisionRecord,
19 StoredSourceRecord,9 StoredSourceRecord,
20 StringMap,10 StringMap,
21} from "#schema";11} from "#schema";
@@ -24,6 +14,7 @@ import * as Schema from "effect/Schema"; · 2 unmodified lines
24 14
25import type { ResolvedSourceAuthMaterial } from "../source-auth-material";15import type { ResolvedSourceAuthMaterial } from "../source-auth-material";
26import type { ResolveSecretMaterial } from "../secret-material-providers";16import type { ResolveSecretMaterial } from "../secret-material-providers";
17import type { SourceCatalogSyncResult } from "../source-catalog-support";
27 18
28export type SourceAdapterFamily = "http_api" | "mcp" | "internal";19export type SourceAdapterFamily = "http_api" | "mcp" | "internal";
29export type SourceAdapterInputSchema = Schema.Schema<any, any, never>;20export type SourceAdapterInputSchema = Schema.Schema<any, any, never>;
@@ -47,28 +38,6 @@ export type SourceAdapterSyncInput = { · 17 unmodified lines
47 >;38 >;
48};39};
49 40
50export 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
59export 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
72export type SourceAdapterOauth2SetupConfig = {41export type SourceAdapterOauth2SetupConfig = {
73 providerKey: string;42 providerKey: string;
74 authorizationEndpoint: string;43 authorizationEndpoint: string;
@@ -80,20 +49,6 @@ export type SourceAdapterOauth2SetupConfig = { · 5 unmodified lines
80 authorizationParams?: Readonly<Record<string, string>>;49 authorizationParams?: Readonly<Record<string, string>>;
81};50};
82 51
83export 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
97export type SourceAdapter = {52export type SourceAdapter = {
98 key: string;53 key: string;
99 displayName: string;54 displayName: string;
@@ -101,8 +56,6 @@ export type SourceAdapter = { · 1 unmodified line
101 bindingConfigVersion: number;56 bindingConfigVersion: number;
102 providerKey: string;57 providerKey: string;
103 defaultImportAuthPolicy: SourceImportAuthPolicy;58 defaultImportAuthPolicy: SourceImportAuthPolicy;
104 primaryDocumentKind: string | null;
105 primarySchemaBundleKind: string | null;
106 connectPayloadSchema: SourceAdapterInputSchema | null;59 connectPayloadSchema: SourceAdapterInputSchema | null;
107 executorAddInputSchema: SourceAdapterInputSchema | null;60 executorAddInputSchema: SourceAdapterInputSchema | null;
108 executorAddHelpText: readonly string[] | null;61 executorAddHelpText: readonly string[] | null;
@@ -115,30 +68,9 @@ export type SourceAdapter = { · 6 unmodified lines
115 sourceConfigFromSource: (source: Source) => Record<string, unknown>;68 sourceConfigFromSource: (source: Source) => Record<string, unknown>;
116 validateSource: (source: Source) => Effect.Effect<Source, Error, never>;69 validateSource: (source: Source) => Effect.Effect<Source, Error, never>;
117 shouldAutoProbe: (source: Source) => boolean;70 shouldAutoProbe: (source: Source) => boolean;
118 parseManifest: (input: {71 syncCatalog: (
119 source: Pick<Source, "id" | "kind">;
120 manifestJson: string | null;
121 }) => Effect.Effect<unknown | null, Error, never>;
122 describePersistedOperation: (input: {
123 source: Source;
124 path: string;
125 operation: StoredSourceRecipeOperationRecord;
126 }) => Effect.Effect<SourceAdapterPersistedOperationMetadata, Error, never>;
127 searchNamespace?: (input: {
128 source: Source;
129 path: string;
130 operation: StoredSourceRecipeOperationRecord;
131 }) => string;
132 createToolDescriptor: (input: {
133 source: Source;
134 operation: StoredSourceRecipeOperationRecord;
135 path: string;
136 schemaBundleId?: string | null;
137 includeSchemas: boolean;
138 }) => ToolDescriptor;
139 materializeSource: (
140 input: SourceAdapterSyncInput,72 input: SourceAdapterSyncInput,
141 ) => Effect.Effect<SourceAdapterMaterialization, Error, never>;73 ) => Effect.Effect<SourceCatalogSyncResult, Error, never>;
142 getOauth2SetupConfig?: (input: {74 getOauth2SetupConfig?: (input: {
143 source: Source;75 source: Source;
144 slot: CredentialSlot;76 slot: CredentialSlot;
@@ -146,7 +78,4 @@ export type SourceAdapter = { · 1 unmodified line
146 normalizeOauthClientInput?: (78 normalizeOauthClientInput?: (
147 input: SourceOauthClientInput,79 input: SourceOauthClientInput,
148 ) => Effect.Effect<SourceOauthClientInput, Error, never>;80 ) => Effect.Effect<SourceOauthClientInput, Error, never>;
149 invokePersistedTool: (
150 input: SourceAdapterInvokePersistedToolInput,
151 ) => Effect.Effect<unknown, Error, never>;
152};81};
packages/control-plane/src/runtime/source-auth-material.test.ts+1−1
@@ -1,4 +1,4 @@
1import { describe, expect, it } from "vitest";1import { describe, expect, it } from "@effect/vitest";
2import * as Effect from "effect/Effect";2import * as Effect from "effect/Effect";
3 3
4import {4import {
packages/control-plane/src/runtime/source-auth-service.ts+19−19
@@ -68,9 +68,9 @@ import { · 67 unmodified lines
68} from "./secret-material-providers";68} from "./secret-material-providers";
69import { upsertOauth2AuthorizedUserLeaseFromTokenResponse } from "./auth-leases";69import { upsertOauth2AuthorizedUserLeaseFromTokenResponse } from "./auth-leases";
70import {70import {
71 RuntimeSourceMaterializationService,71 RuntimeSourceCatalogSyncService,
72 type RuntimeSourceMaterializationShape,72 type RuntimeSourceCatalogSyncShape,
73} from "./source-materialization";73} from "./source-catalog-sync";
74import {74import {
75 buildOAuth2AuthorizationUrl,75 buildOAuth2AuthorizationUrl,
76 createPkceCodeVerifier,76 createPkceCodeVerifier,
@@ -978,7 +978,7 @@ const startOauth2PkceSourceCredentialSetup = (input: { · 901 unmodified lines
978const connectMcpSourceInternal = (input: {978const connectMcpSourceInternal = (input: {
979 rows: ControlPlaneStoreShape;979 rows: ControlPlaneStoreShape;
980 sourceStore: RuntimeSourceStore;980 sourceStore: RuntimeSourceStore;
981 sourceMaterialization: RuntimeSourceMaterializationShape;981 sourceCatalogSync: RuntimeSourceCatalogSyncShape;
982 getLocalServerBaseUrl?: () => string | undefined;982 getLocalServerBaseUrl?: () => string | undefined;
983 baseUrl?: string | null;983 baseUrl?: string | null;
984 workspaceId: WorkspaceId;984 workspaceId: WorkspaceId;
@@ -1087,7 +1087,7 @@ const connectMcpSourceInternal = (input: { · 102 unmodified lines
1087 const persistedDraft = yield* input.sourceStore.persistSource(draftSource, {1087 const persistedDraft = yield* input.sourceStore.persistSource(draftSource, {
1088 actorAccountId: input.actorAccountId,1088 actorAccountId: input.actorAccountId,
1089 });1089 });
1090 yield* input.sourceMaterialization.sync({1090 yield* input.sourceCatalogSync.sync({
1091 source: persistedDraft,1091 source: persistedDraft,
1092 actorAccountId: input.actorAccountId,1092 actorAccountId: input.actorAccountId,
1093 });1093 });
@@ -1110,7 +1110,7 @@ const connectMcpSourceInternal = (input: { · 16 unmodified lines
1110 auth: { kind: "none" },1110 auth: { kind: "none" },
1111 });1111 });
1112 const indexed = yield* Effect.either(1112 const indexed = yield* Effect.either(
1113 input.sourceMaterialization.persistMcpRecipeMaterializationFromManifest({1113 input.sourceCatalogSync.persistMcpCatalogSnapshotFromManifest({
1114 source: connected,1114 source: connected,
1115 manifestEntries: result.manifest.tools,1115 manifestEntries: result.manifest.tools,
1116 }),1116 }),
@@ -1206,7 +1206,7 @@ const connectMcpSourceInternal = (input: { · 89 unmodified lines
1206const addExecutorHttpSource = (input: {1206const addExecutorHttpSource = (input: {
1207 rows: ControlPlaneStoreShape;1207 rows: ControlPlaneStoreShape;
1208 sourceStore: RuntimeSourceStore;1208 sourceStore: RuntimeSourceStore;
1209 sourceMaterialization: RuntimeSourceMaterializationShape;1209 sourceCatalogSync: RuntimeSourceCatalogSyncShape;
1210 sourceInput: Extract<ExecutorAddSourceInput, { kind: "openapi" | "graphql" }>;1210 sourceInput: Extract<ExecutorAddSourceInput, { kind: "openapi" | "graphql" }>;
1211 storeSecretMaterial: StoreSecretMaterial;1211 storeSecretMaterial: StoreSecretMaterial;
1212 resolveSecretMaterial: ResolveSecretMaterial;1212 resolveSecretMaterial: ResolveSecretMaterial;
@@ -1358,7 +1358,7 @@ const addExecutorHttpSource = (input: { · 145 unmodified lines
1358 }1358 }
1359 1359
1360 const synced = yield* Effect.either(1360 const synced = yield* Effect.either(
1361 input.sourceMaterialization.sync({1361 input.sourceCatalogSync.sync({
1362 source: {1362 source: {
1363 ...persistedDraft,1363 ...persistedDraft,
1364 status: "connected",1364 status: "connected",
@@ -1409,7 +1409,7 @@ const addExecutorHttpSource = (input: { · 44 unmodified lines
1409const addExecutorGoogleDiscoverySource = (input: {1409const addExecutorGoogleDiscoverySource = (input: {
1410 rows: ControlPlaneStoreShape;1410 rows: ControlPlaneStoreShape;
1411 sourceStore: RuntimeSourceStore;1411 sourceStore: RuntimeSourceStore;
1412 sourceMaterialization: RuntimeSourceMaterializationShape;1412 sourceCatalogSync: RuntimeSourceCatalogSyncShape;
1413 sourceInput: Extract<ExecutorAddSourceInput, { kind: "google_discovery" }>;1413 sourceInput: Extract<ExecutorAddSourceInput, { kind: "google_discovery" }>;
1414 storeSecretMaterial: StoreSecretMaterial;1414 storeSecretMaterial: StoreSecretMaterial;
1415 resolveSecretMaterial: ResolveSecretMaterial;1415 resolveSecretMaterial: ResolveSecretMaterial;
@@ -1578,7 +1578,7 @@ const addExecutorGoogleDiscoverySource = (input: { · 162 unmodified lines
1578 }1578 }
1579 1579
1580 const synced = yield* Effect.either(1580 const synced = yield* Effect.either(
1581 input.sourceMaterialization.sync({1581 input.sourceCatalogSync.sync({
1582 source: {1582 source: {
1583 ...persistedDraft,1583 ...persistedDraft,
1584 status: "connected",1584 status: "connected",
@@ -1671,7 +1671,7 @@ type RuntimeSourceAuthDependencies = { · 86 unmodified lines
1671 rows: ControlPlaneStoreShape;1671 rows: ControlPlaneStoreShape;
1672 liveExecutionManager: LiveExecutionManager;1672 liveExecutionManager: LiveExecutionManager;
1673 sourceStore: RuntimeSourceStore;1673 sourceStore: RuntimeSourceStore;
1674 sourceMaterialization: RuntimeSourceMaterializationShape;1674 sourceCatalogSync: RuntimeSourceCatalogSyncShape;
1675 resolveSecretMaterial: ResolveSecretMaterial;1675 resolveSecretMaterial: ResolveSecretMaterial;
1676 storeSecretMaterial: StoreSecretMaterial;1676 storeSecretMaterial: StoreSecretMaterial;
1677 getLocalServerBaseUrl?: () => string | undefined;1677 getLocalServerBaseUrl?: () => string | undefined;
@@ -1721,7 +1721,7 @@ const createRuntimeSourceConnectionService = ( · 43 unmodified lines
1721 ? addExecutorGoogleDiscoverySource({1721 ? addExecutorGoogleDiscoverySource({
1722 rows: input.rows,1722 rows: input.rows,
1723 sourceStore: input.sourceStore,1723 sourceStore: input.sourceStore,
1724 sourceMaterialization: input.sourceMaterialization,1724 sourceCatalogSync: input.sourceCatalogSync,
1725 sourceInput,1725 sourceInput,
1726 storeSecretMaterial: input.storeSecretMaterial,1726 storeSecretMaterial: input.storeSecretMaterial,
1727 resolveSecretMaterial: input.resolveSecretMaterial,1727 resolveSecretMaterial: input.resolveSecretMaterial,
@@ -1732,7 +1732,7 @@ const createRuntimeSourceConnectionService = ( · 4 unmodified lines
1732 ? addExecutorHttpSource({1732 ? addExecutorHttpSource({
1733 rows: input.rows,1733 rows: input.rows,
1734 sourceStore: input.sourceStore,1734 sourceStore: input.sourceStore,
1735 sourceMaterialization: input.sourceMaterialization,1735 sourceCatalogSync: input.sourceCatalogSync,
1736 sourceInput: sourceInput as Extract<1736 sourceInput: sourceInput as Extract<
1737 ExecutorAddSourceInput,1737 ExecutorAddSourceInput,
1738 { kind: "openapi" | "graphql" }1738 { kind: "openapi" | "graphql" }
@@ -1745,7 +1745,7 @@ const createRuntimeSourceConnectionService = ( · 6 unmodified lines
1745 : connectMcpSourceInternal({1745 : connectMcpSourceInternal({
1746 rows: input.rows,1746 rows: input.rows,
1747 sourceStore: input.sourceStore,1747 sourceStore: input.sourceStore,
1748 sourceMaterialization: input.sourceMaterialization,1748 sourceCatalogSync: input.sourceCatalogSync,
1749 getLocalServerBaseUrl: input.getLocalServerBaseUrl,1749 getLocalServerBaseUrl: input.getLocalServerBaseUrl,
1750 workspaceId: sourceInput.workspaceId,1750 workspaceId: sourceInput.workspaceId,
1751 actorAccountId: sourceInput.actorAccountId,1751 actorAccountId: sourceInput.actorAccountId,
@@ -1767,7 +1767,7 @@ const createRuntimeSourceConnectionService = ( · 15 unmodified lines
1767 connectMcpSourceInternal({1767 connectMcpSourceInternal({
1768 rows: input.rows,1768 rows: input.rows,
1769 sourceStore: input.sourceStore,1769 sourceStore: input.sourceStore,
1770 sourceMaterialization: input.sourceMaterialization,1770 sourceCatalogSync: input.sourceCatalogSync,
1771 getLocalServerBaseUrl: input.getLocalServerBaseUrl,1771 getLocalServerBaseUrl: input.getLocalServerBaseUrl,
1772 workspaceId: sourceInput.workspaceId,1772 workspaceId: sourceInput.workspaceId,
1773 actorAccountId: sourceInput.actorAccountId,1773 actorAccountId: sourceInput.actorAccountId,
@@ -2028,7 +2028,7 @@ const createRuntimeSourceOAuthSessionService = ( · 254 unmodified lines
2028 status: "error",2028 status: "error",
2029 lastError: reason,2029 lastError: reason,
2030 });2030 });
2031 yield* input.sourceMaterialization.sync({2031 yield* input.sourceCatalogSync.sync({
2032 source: failedSource,2032 source: failedSource,
2033 actorAccountId: session.actorAccountId,2033 actorAccountId: session.actorAccountId,
2034 });2034 });
@@ -2201,7 +2201,7 @@ const createRuntimeSourceOAuthSessionService = ( · 166 unmodified lines
2201 );2201 );
2202 }2202 }
2203 const indexed = yield* Effect.either(2203 const indexed = yield* Effect.either(
2204 input.sourceMaterialization.sync({2204 input.sourceCatalogSync.sync({
2205 source: connectedSource,2205 source: connectedSource,
2206 actorAccountId: session.actorAccountId,2206 actorAccountId: session.actorAccountId,
2207 }),2207 }),
@@ -2272,7 +2272,7 @@ export const RuntimeSourceAuthServiceLive = (input: { · 64 unmodified lines
2272 const rows = yield* ControlPlaneStore;2272 const rows = yield* ControlPlaneStore;
2273 const liveExecutionManager = yield* LiveExecutionManagerService;2273 const liveExecutionManager = yield* LiveExecutionManagerService;
2274 const sourceStore = yield* RuntimeSourceStoreService;2274 const sourceStore = yield* RuntimeSourceStoreService;
2275 const sourceMaterialization = yield* RuntimeSourceMaterializationService;2275 const sourceCatalogSync = yield* RuntimeSourceCatalogSyncService;
2276 const resolveSecretMaterial = yield* SecretMaterialResolverService;2276 const resolveSecretMaterial = yield* SecretMaterialResolverService;
2277 const storeSecretMaterial = yield* SecretMaterialStorerService;2277 const storeSecretMaterial = yield* SecretMaterialStorerService;
2278 const runtimeLocalWorkspace = yield* getRuntimeLocalWorkspaceOption();2278 const runtimeLocalWorkspace = yield* getRuntimeLocalWorkspaceOption();
@@ -2281,7 +2281,7 @@ export const RuntimeSourceAuthServiceLive = (input: { · 2 unmodified lines
2281 rows,2281 rows,
2282 liveExecutionManager,2282 liveExecutionManager,
2283 sourceStore,2283 sourceStore,
2284 sourceMaterialization,2284 sourceCatalogSync,
2285 resolveSecretMaterial,2285 resolveSecretMaterial,
2286 storeSecretMaterial,2286 storeSecretMaterial,
2287 getLocalServerBaseUrl: input.getLocalServerBaseUrl,2287 getLocalServerBaseUrl: input.getLocalServerBaseUrl,
packages/control-plane/src/runtime/source-catalog-runtime.ts+743−0
@@ -0,0 +1,743 @@
1import {
2 type ToolCatalogEntry,
3 type ToolDescriptor as CatalogToolDescriptor,
4 typeSignatureFromSchema,
5} from "@executor/codemode-core";
6import type {
7 AccountId,
8 Source,
9 StoredSourceRecord,
10 StoredSourceCatalogRevisionRecord,
11 WorkspaceId,
12} from "#schema";
13import * as Context from "effect/Context";
14import * as Effect from "effect/Effect";
15import * as Layer from "effect/Layer";
16
17import {
18 projectCatalogForAgentSdk,
19 type ProjectedCatalog,
20} from "../ir/catalog";
21import 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";
32import { LocalSourceArtifactMissingError } from "./local-errors";
33import {
34 RuntimeLocalWorkspaceService,
35 type RuntimeLocalWorkspaceState,
36} from "./local-runtime-context";
37import type { LocalSourceArtifact } from "./local-source-artifacts";
38import {
39 SourceArtifactStore,
40 type SourceArtifactStoreShape,
41} from "./local-storage";
42import { namespaceFromSourceName } from "./source-names";
43import {
44 RuntimeSourceStoreService,
45 type RuntimeSourceStore,
46} from "./source-store";
47
48type CatalogImportMetadata = CatalogSnapshotV1["import"];
49
50export 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
60export 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
75export type LoadedSourceCatalogToolIndexEntry = Omit<LoadedSourceCatalogTool, "revision">;
76
77export 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
87const catalogNamespaceFromPath = (path: string): string => {
88 const [first, second] = path.split(".");
89 return second ? `${first}.${second}` : first;
90};
91
92const 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
110const 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
119const 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
127const 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
311const 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
358const 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
383type 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
406export type RuntimeSourceCatalogStore = RuntimeSourceCatalogStoreShape;
407
408export class RuntimeSourceCatalogStoreService extends Context.Tag(
409 "#runtime/RuntimeSourceCatalogStoreService",
410)<RuntimeSourceCatalogStoreService, RuntimeSourceCatalogStoreShape>() {}
411
412type RuntimeSourceCatalogStoreDeps = {
413 runtimeLocalWorkspace: RuntimeLocalWorkspaceState;
414 sourceStore: RuntimeSourceStore;
415 sourceArtifactStore: SourceArtifactStoreShape;
416};
417
418type SourceCatalogRuntimeServices =
419 | RuntimeLocalWorkspaceService
420 | RuntimeSourceStoreService
421 | SourceArtifactStore;
422
423const 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
438const buildSnapshotFromArtifact = (input: {
439 source: Source;
440 artifact: LocalSourceArtifact;
441}): CatalogSnapshotV1 => {
442 return input.artifact.snapshot;
443};
444
445const 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
497const 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
547export 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
566export 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
590export 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
641export 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
674export 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
711export 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);
packages/control-plane/src/runtime/source-catalog-snapshot.test.ts+221−0
@@ -0,0 +1,221 @@
1import { describe, expect, it } from "@effect/vitest";
2
3import type { Source } from "#schema";
4
5import {
6 createGoogleDiscoveryCatalogSnapshot,
7 createGraphqlCatalogSnapshot,
8 createOpenApiCatalogSnapshot,
9} from "./source-catalog-snapshot";
10
11const 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
34describe("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});
packages/control-plane/src/runtime/source-catalog-snapshot.ts+1688−0
@@ -0,0 +1,1688 @@
1import { sha256Hex } from "@executor/codemode-core";
2import {
3 type OpenApiToolProviderData,
4} from "@executor/codemode-openapi";
5import {
6 type GoogleDiscoveryToolProviderData,
7} from "@executor/codemode-google-discovery";
8
9import type { Source } from "#schema";
10
11import {
12 createCatalogSnapshotV1,
13 createEmptyCatalogV1,
14} from "../ir/catalog";
15import {
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";
31import 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";
56import type { GraphqlToolProviderData } from "./graphql-tools";
57import { namespaceFromSourceName } from "./source-names";
58
59type JsonSchema = boolean | Record<string, unknown>;
60
61export type CatalogSourceDocumentInput = {
62 documentKind: string;
63 documentKey: string;
64 contentText: string;
65 fetchedAt?: number | null;
66};
67
68type CatalogOperationInput = {
69 toolId: string;
70 title?: string | null;
71 description?: string | null;
72 effect: EffectKind;
73 inputSchema?: unknown;
74 outputSchema?: unknown;
75};
76
77export type OpenApiCatalogOperationInput = CatalogOperationInput & {
78 providerData: OpenApiToolProviderData;
79};
80
81export type GoogleDiscoveryCatalogOperationInput = CatalogOperationInput & {
82 providerData: GoogleDiscoveryToolProviderData;
83};
84
85export type GraphqlCatalogOperationInput = CatalogOperationInput & {
86 providerData: GraphqlToolProviderData;
87};
88
89export type McpCatalogOperationInput = CatalogOperationInput & {
90 providerData: {
91 toolId: string;
92 toolName: string;
93 description: string | null;
94 };
95};
96
97type JsonSchemaImporter = {
98 importSchema: (schema: unknown, key: string) => ReturnType<typeof ShapeSymbolIdSchema.make>;
99};
100
101const 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
116const stableHash = (value: unknown): string =>
117 sha256Hex(stableStringify(value)).slice(0, 16);
118
119const mutableRecord = <K extends string, V>(value: Readonly<Record<K, V>>): Record<K, V> =>
120 value as unknown as Record<K, V>;
121
122const asObject = (value: unknown): Record<string, unknown> =>
123 value !== null && typeof value === "object" && !Array.isArray(value)
124 ? (value as Record<string, unknown>)
125 : {};
126
127const asString = (value: unknown): string | null =>
128 typeof value === "string" && value.trim().length > 0 ? value : null;
129
130const asBoolean = (value: unknown): boolean | null =>
131 typeof value === "boolean" ? value : null;
132
133const asNumber = (value: unknown): number | null =>
134 typeof value === "number" && Number.isFinite(value) ? value : null;
135
136const asArray = (value: unknown): unknown[] =>
137 Array.isArray(value) ? value : [];
138
139const asStringArray = (value: unknown): string[] =>
140 asArray(value).flatMap((entry) => {
141 const stringValue = asString(entry);
142 return stringValue === null ? [] : [stringValue];
143 });
144
145const 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
160const 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
166const serviceScopeIdForSource = (source: Source) =>
167 ScopeIdSchema.make(`scope_service_${stableHash({ sourceId: source.id })}`);
168
169const documentIdFor = (source: Source, key: string) =>
170 DocumentIdSchema.make(`doc_${stableHash({ sourceId: source.id, key })}`);
171
172const resourceIdForSource = (source: Source) =>
173 ResourceIdSchema.make(`res_${stableHash({ sourceId: source.id })}`);
174
175const provenanceFor = (documentId: ReturnType<typeof DocumentIdSchema.make>, pointer: string): ProvenanceRef[] => [{
176 relation: "declared",
177 documentId,
178 pointer,
179}];
180
181const 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
194const 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
212const 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
229const 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
241const 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
256const 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
276const 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
694const 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
731const 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
740const 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
756const requestBodySchemaFromInput = (schema: unknown): unknown =>
757 schemaFromField(schema, "body") ?? schemaFromField(schema, "input");
758
759const preferredResponseContentTypes = (
760 mediaTypes: readonly string[] | undefined,
761): string[] =>
762 mediaTypes && mediaTypes.length > 0 ? [...mediaTypes] : ["application/json"];
763
764const 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
784const 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
810const 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
1059const 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
1278const 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
1403const 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
1506const 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
1598export 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
1621export 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
1644export 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
1667export 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 });
packages/control-plane/src/runtime/source-catalog-support.ts+18−0
@@ -0,0 +1,18 @@
1import { sha256Hex } from "@executor/codemode-core";
2import type { CatalogSnapshotV1 } from "../ir/model";
3
4export 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
13export const contentHash = (value: string): string => sha256Hex(value);
14
15export type SourceCatalogSyncResult = {
16 snapshot: CatalogSnapshotV1;
17 sourceHash: string | null;
18};
packages/control-plane/src/runtime/source-catalog-sync.tsrenamed from packages/control-plane/src/runtime/source-materialization.ts+38−38
@@ -27,7 +27,7 @@ import { · 26 unmodified lines
27 getSourceAdapterForSource,27 getSourceAdapterForSource,
28} from "./source-adapters";28} from "./source-adapters";
29import {29import {
30 materializationFromMcpManifestEntries,30 catalogSyncResultFromMcpManifestEntries,
31} from "./source-adapters/mcp";31} from "./source-adapters/mcp";
32import { SecretMaterialResolverService } from "./secret-material-providers";32import { SecretMaterialResolverService } from "./secret-material-providers";
33 33
@@ -36,7 +36,7 @@ const shouldIndexSource = (source: Source): boolean => · 2 unmodified lines
36 && source.status === "connected"36 && source.status === "connected"
37 && getSourceAdapterForSource(source).family !== "internal";37 && getSourceAdapterForSource(source).family !== "internal";
38 38
39type RuntimeSourceMaterializationDeps = {39type RuntimeSourceCatalogSyncDeps = {
40 runtimeLocalWorkspace: RuntimeLocalWorkspaceState;40 runtimeLocalWorkspace: RuntimeLocalWorkspaceState;
41 workspaceStateStore: WorkspaceStateStoreShape;41 workspaceStateStore: WorkspaceStateStoreShape;
42 sourceArtifactStore: SourceArtifactStoreShape;42 sourceArtifactStore: SourceArtifactStoreShape;
@@ -44,30 +44,30 @@ type RuntimeSourceMaterializationDeps = { · 1 unmodified line
44 sourceAuthMaterialService: Effect.Effect.Success<typeof RuntimeSourceAuthMaterialService>;44 sourceAuthMaterialService: Effect.Effect.Success<typeof RuntimeSourceAuthMaterialService>;
45};45};
46 46
47type SourceMaterializationServices =47type SourceCatalogSyncServices =
48 | RuntimeLocalWorkspaceService48 | RuntimeLocalWorkspaceService
49 | WorkspaceStateStore49 | WorkspaceStateStore
50 | SourceArtifactStore50 | SourceArtifactStore
51 | RuntimeSourceAuthMaterialService51 | RuntimeSourceAuthMaterialService
52 | SecretMaterialResolverService;52 | SecretMaterialResolverService;
53 53
54export type RuntimeSourceMaterializationShape = {54export type RuntimeSourceCatalogSyncShape = {
55 sync: (input: {55 sync: (input: {
56 source: Source;56 source: Source;
57 actorAccountId?: AccountId | null;57 actorAccountId?: AccountId | null;
58 }) => Effect.Effect<void, Error, never>;58 }) => Effect.Effect<void, Error, never>;
59 persistMcpRecipeMaterializationFromManifest: (input: {59 persistMcpCatalogSnapshotFromManifest: (input: {
60 source: Source;60 source: Source;
61 manifestEntries: Parameters<typeof materializationFromMcpManifestEntries>[0]["manifestEntries"];61 manifestEntries: Parameters<typeof catalogSyncResultFromMcpManifestEntries>[0]["manifestEntries"];
62 }) => Effect.Effect<void, Error, never>;62 }) => Effect.Effect<void, Error, never>;
63};63};
64 64
65export class RuntimeSourceMaterializationService extends Context.Tag(65export class RuntimeSourceCatalogSyncService extends Context.Tag(
66 "#runtime/RuntimeSourceMaterializationService",66 "#runtime/RuntimeSourceCatalogSyncService",
67)<RuntimeSourceMaterializationService, RuntimeSourceMaterializationShape>() {}67)<RuntimeSourceCatalogSyncService, RuntimeSourceCatalogSyncShape>() {}
68 68
69const ensureRuntimeMaterializationWorkspace = (69const ensureRuntimeCatalogSyncWorkspace = (
70 deps: RuntimeSourceMaterializationDeps,70 deps: RuntimeSourceCatalogSyncDeps,
71 workspaceId: Source["workspaceId"],71 workspaceId: Source["workspaceId"],
72) => {72) => {
73 if (deps.runtimeLocalWorkspace.installation.workspaceId !== workspaceId) {73 if (deps.runtimeLocalWorkspace.installation.workspaceId !== workspaceId) {
@@ -81,15 +81,15 @@ const ensureRuntimeMaterializationWorkspace = ( · 7 unmodified lines
81 return Effect.succeed(deps.runtimeLocalWorkspace.context);81 return Effect.succeed(deps.runtimeLocalWorkspace.context);
82};82};
83 83
84const syncSourceMaterializationWithDeps = (84const syncSourceCatalogWithDeps = (
85 deps: RuntimeSourceMaterializationDeps,85 deps: RuntimeSourceCatalogSyncDeps,
86 input: {86 input: {
87 source: Source;87 source: Source;
88 actorAccountId?: AccountId | null;88 actorAccountId?: AccountId | null;
89 },89 },
90): Effect.Effect<void, Error, never> =>90): Effect.Effect<void, Error, never> =>
91 Effect.gen(function* () {91 Effect.gen(function* () {
92 const workspaceContext = yield* ensureRuntimeMaterializationWorkspace(92 const workspaceContext = yield* ensureRuntimeCatalogSyncWorkspace(
93 deps,93 deps,
94 input.source.workspaceId,94 input.source.workspaceId,
95 );95 );
@@ -118,7 +118,7 @@ const syncSourceMaterializationWithDeps = ( · 22 unmodified lines
118 }118 }
119 119
120 const adapter = getSourceAdapterForSource(input.source);120 const adapter = getSourceAdapterForSource(input.source);
121 const materialization = yield* adapter.materializeSource({121 const syncResult = yield* adapter.syncCatalog({
122 source: input.source,122 source: input.source,
123 resolveSecretMaterial: deps.resolveSecretMaterial,123 resolveSecretMaterial: deps.resolveSecretMaterial,
124 resolveAuthMaterialForSlot: (slot) =>124 resolveAuthMaterialForSlot: (slot) =>
@@ -133,7 +133,7 @@ const syncSourceMaterializationWithDeps = ( · 8 unmodified lines
133 sourceId: input.source.id,133 sourceId: input.source.id,
134 artifact: deps.sourceArtifactStore.build({134 artifact: deps.sourceArtifactStore.build({
135 source: input.source,135 source: input.source,
136 materialization,136 syncResult,
137 }),137 }),
138 });138 });
139 139
@@ -146,7 +146,7 @@ const syncSourceMaterializationWithDeps = ( · 6 unmodified lines
146 [input.source.id]: {146 [input.source.id]: {
147 status: "connected",147 status: "connected",
148 lastError: null,148 lastError: null,
149 sourceHash: materialization.sourceHash,149 sourceHash: syncResult.sourceHash,
150 createdAt: existingSourceState?.createdAt ?? input.source.createdAt,150 createdAt: existingSourceState?.createdAt ?? input.source.createdAt,
151 updatedAt: Date.now(),151 updatedAt: Date.now(),
152 },152 },
@@ -158,20 +158,20 @@ const syncSourceMaterializationWithDeps = ( · 5 unmodified lines
158 });158 });
159 });159 });
160 160
161const persistMcpRecipeMaterializationFromManifestWithDeps = (161const persistMcpCatalogSnapshotFromManifestWithDeps = (
162 deps: RuntimeSourceMaterializationDeps,162 deps: RuntimeSourceCatalogSyncDeps,
163 input: {163 input: {
164 source: Source;164 source: Source;
165 manifestEntries: Parameters<typeof materializationFromMcpManifestEntries>[0]["manifestEntries"];165 manifestEntries: Parameters<typeof catalogSyncResultFromMcpManifestEntries>[0]["manifestEntries"];
166 },166 },
167): Effect.Effect<void, Error, never> =>167): Effect.Effect<void, Error, never> =>
168 Effect.gen(function* () {168 Effect.gen(function* () {
169 const workspaceContext = yield* ensureRuntimeMaterializationWorkspace(169 const workspaceContext = yield* ensureRuntimeCatalogSyncWorkspace(
170 deps,170 deps,
171 input.source.workspaceId,171 input.source.workspaceId,
172 );172 );
173 const materialization = materializationFromMcpManifestEntries({173 const syncResult = catalogSyncResultFromMcpManifestEntries({
174 recipeRevisionId: "src_recipe_rev_materialization" as never,174 source: input.source,
175 endpoint: input.source.endpoint,175 endpoint: input.source.endpoint,
176 manifestEntries: input.manifestEntries,176 manifestEntries: input.manifestEntries,
177 });177 });
@@ -181,15 +181,15 @@ const persistMcpRecipeMaterializationFromManifestWithDeps = ( · 3 unmodified lines
181 sourceId: input.source.id,181 sourceId: input.source.id,
182 artifact: deps.sourceArtifactStore.build({182 artifact: deps.sourceArtifactStore.build({
183 source: input.source,183 source: input.source,
184 materialization,184 syncResult,
185 }),185 }),
186 });186 });
187 });187 });
188 188
189export const syncSourceMaterialization = (input: {189export const syncSourceCatalog = (input: {
190 source: Source;190 source: Source;
191 actorAccountId?: AccountId | null;191 actorAccountId?: AccountId | null;
192}): Effect.Effect<void, Error, SourceMaterializationServices> =>192}): Effect.Effect<void, Error, SourceCatalogSyncServices> =>
193 Effect.gen(function* () {193 Effect.gen(function* () {
194 const runtimeLocalWorkspace = yield* RuntimeLocalWorkspaceService;194 const runtimeLocalWorkspace = yield* RuntimeLocalWorkspaceService;
195 const workspaceStateStore = yield* WorkspaceStateStore;195 const workspaceStateStore = yield* WorkspaceStateStore;
@@ -197,7 +197,7 @@ export const syncSourceMaterialization = (input: { · 1 unmodified line
197 const resolveSecretMaterial = yield* SecretMaterialResolverService;197 const resolveSecretMaterial = yield* SecretMaterialResolverService;
198 const sourceAuthMaterialService = yield* RuntimeSourceAuthMaterialService;198 const sourceAuthMaterialService = yield* RuntimeSourceAuthMaterialService;
199 199
200 return yield* syncSourceMaterializationWithDeps(200 return yield* syncSourceCatalogWithDeps(
201 {201 {
202 runtimeLocalWorkspace,202 runtimeLocalWorkspace,
203 workspaceStateStore,203 workspaceStateStore,
@@ -212,10 +212,10 @@ export const syncSourceMaterialization = (input: { · 8 unmodified lines
212 );212 );
213 });213 });
214 214
215export const persistMcpRecipeMaterializationFromManifest = (input: {215export const persistMcpCatalogSnapshotFromManifest = (input: {
216 source: Source;216 source: Source;
217 manifestEntries: Parameters<typeof materializationFromMcpManifestEntries>[0]["manifestEntries"];217 manifestEntries: Parameters<typeof catalogSyncResultFromMcpManifestEntries>[0]["manifestEntries"];
218}): Effect.Effect<void, Error, SourceMaterializationServices> =>218}): Effect.Effect<void, Error, SourceCatalogSyncServices> =>
219 Effect.gen(function* () {219 Effect.gen(function* () {
220 const runtimeLocalWorkspace = yield* RuntimeLocalWorkspaceService;220 const runtimeLocalWorkspace = yield* RuntimeLocalWorkspaceService;
221 const workspaceStateStore = yield* WorkspaceStateStore;221 const workspaceStateStore = yield* WorkspaceStateStore;
@@ -223,7 +223,7 @@ export const persistMcpRecipeMaterializationFromManifest = (input: { · 1 unmodified line
223 const resolveSecretMaterial = yield* SecretMaterialResolverService;223 const resolveSecretMaterial = yield* SecretMaterialResolverService;
224 const sourceAuthMaterialService = yield* RuntimeSourceAuthMaterialService;224 const sourceAuthMaterialService = yield* RuntimeSourceAuthMaterialService;
225 225
226 return yield* persistMcpRecipeMaterializationFromManifestWithDeps(226 return yield* persistMcpCatalogSnapshotFromManifestWithDeps(
227 {227 {
228 runtimeLocalWorkspace,228 runtimeLocalWorkspace,
229 workspaceStateStore,229 workspaceStateStore,
@@ -235,8 +235,8 @@ export const persistMcpRecipeMaterializationFromManifest = (input: { · 5 unmodified lines
235 );235 );
236 });236 });
237 237
238export const RuntimeSourceMaterializationLive = Layer.effect(238export const RuntimeSourceCatalogSyncLive = Layer.effect(
239 RuntimeSourceMaterializationService,239 RuntimeSourceCatalogSyncService,
240 Effect.gen(function* () {240 Effect.gen(function* () {
241 const runtimeLocalWorkspace = yield* RuntimeLocalWorkspaceService;241 const runtimeLocalWorkspace = yield* RuntimeLocalWorkspaceService;
242 const workspaceStateStore = yield* WorkspaceStateStore;242 const workspaceStateStore = yield* WorkspaceStateStore;
@@ -244,7 +244,7 @@ export const RuntimeSourceMaterializationLive = Layer.effect( · 1 unmodified line
244 const resolveSecretMaterial = yield* SecretMaterialResolverService;244 const resolveSecretMaterial = yield* SecretMaterialResolverService;
245 const sourceAuthMaterialService = yield* RuntimeSourceAuthMaterialService;245 const sourceAuthMaterialService = yield* RuntimeSourceAuthMaterialService;
246 246
247 const deps: RuntimeSourceMaterializationDeps = {247 const deps: RuntimeSourceCatalogSyncDeps = {
248 runtimeLocalWorkspace,248 runtimeLocalWorkspace,
249 workspaceStateStore,249 workspaceStateStore,
250 sourceArtifactStore,250 sourceArtifactStore,
@@ -252,10 +252,10 @@ export const RuntimeSourceMaterializationLive = Layer.effect( · 1 unmodified line
252 sourceAuthMaterialService,252 sourceAuthMaterialService,
253 };253 };
254 254
255 return RuntimeSourceMaterializationService.of({255 return RuntimeSourceCatalogSyncService.of({
256 sync: (input) => syncSourceMaterializationWithDeps(deps, input),256 sync: (input) => syncSourceCatalogWithDeps(deps, input),
257 persistMcpRecipeMaterializationFromManifest: (input) =>257 persistMcpCatalogSnapshotFromManifest: (input) =>
258 persistMcpRecipeMaterializationFromManifestWithDeps(deps, input),258 persistMcpCatalogSnapshotFromManifestWithDeps(deps, input),
259 });259 });
260 }),260 }),
261);261);
packages/control-plane/src/runtime/source-definitions.test.ts+23−23
@@ -1,4 +1,4 @@
1import { describe, expect, it } from "vitest";1import { describe, expect, it } from "@effect/vitest";
2import * as Effect from "effect/Effect";2import * as Effect from "effect/Effect";
3 3
4import {4import {
@@ -13,8 +13,8 @@ import { · 8 unmodified lines
13 createSourceFromPayload,13 createSourceFromPayload,
14 projectSourceFromStorage,14 projectSourceFromStorage,
15 splitSourceForStorage,15 splitSourceForStorage,
16 stableSourceRecipeId,16 stableSourceCatalogId,
17 stableSourceRecipeRevisionId,17 stableSourceCatalogRevisionId,
18 updateSourceFromPayload,18 updateSourceFromPayload,
19} from "./source-definitions";19} from "./source-definitions";
20import { namespaceFromSourceName } from "./source-names";20import { namespaceFromSourceName } from "./source-names";
@@ -63,7 +63,7 @@ const makeSource = (overrides: Partial<Source> = {}): Source => ({ · 42 unmodified lines
63});63});
64 64
65describe("source-definitions", () => {65describe("source-definitions", () => {
66 describe("stable recipe ids", () => {66 describe("stable catalog ids", () => {
67 it("is deterministic across calls and ignores source name/workspace", () => {67 it("is deterministic across calls and ignores source name/workspace", () => {
68 const source = makeSource();68 const source = makeSource();
69 const renamed = makeSource({69 const renamed = makeSource({
@@ -73,26 +73,26 @@ describe("source-definitions", () => { · 3 unmodified lines
73 workspaceId: WorkspaceIdSchema.make("ws_source_definitions_other"),73 workspaceId: WorkspaceIdSchema.make("ws_source_definitions_other"),
74 });74 });
75 75
76 expect(stableSourceRecipeId(source)).toBe(stableSourceRecipeId(source));76 expect(stableSourceCatalogId(source)).toBe(stableSourceCatalogId(source));
77 expect(stableSourceRecipeRevisionId(source)).toBe(stableSourceRecipeRevisionId(source));77 expect(stableSourceCatalogRevisionId(source)).toBe(stableSourceCatalogRevisionId(source));
78 expect(stableSourceRecipeId(renamed)).toBe(stableSourceRecipeId(source));78 expect(stableSourceCatalogId(renamed)).toBe(stableSourceCatalogId(source));
79 expect(stableSourceRecipeRevisionId(renamed)).toBe(stableSourceRecipeRevisionId(source));79 expect(stableSourceCatalogRevisionId(renamed)).toBe(stableSourceCatalogRevisionId(source));
80 expect(stableSourceRecipeId(differentWorkspace)).toBe(stableSourceRecipeId(source));80 expect(stableSourceCatalogId(differentWorkspace)).toBe(stableSourceCatalogId(source));
81 expect(stableSourceRecipeRevisionId(differentWorkspace)).toBe(81 expect(stableSourceCatalogRevisionId(differentWorkspace)).toBe(
82 stableSourceRecipeRevisionId(source),82 stableSourceCatalogRevisionId(source),
83 );83 );
84 });84 });
85 85
86 it("changes recipe and revision ids when the source config changes", () => {86 it("changes catalog and revision ids when the source config changes", () => {
87 const source = makeSource();87 const source = makeSource();
88 const changedEndpoint = makeSource({88 const changedEndpoint = makeSource({
89 endpoint: "https://example.com",89 endpoint: "https://example.com",
90 binding: openApiBinding("https://example.com/openapi.json"),90 binding: openApiBinding("https://example.com/openapi.json"),
91 });91 });
92 92
93 expect(stableSourceRecipeId(changedEndpoint)).not.toBe(stableSourceRecipeId(source));93 expect(stableSourceCatalogId(changedEndpoint)).not.toBe(stableSourceCatalogId(source));
94 expect(stableSourceRecipeRevisionId(changedEndpoint)).not.toBe(94 expect(stableSourceCatalogRevisionId(changedEndpoint)).not.toBe(
95 stableSourceRecipeRevisionId(source),95 stableSourceCatalogRevisionId(source),
96 );96 );
97 });97 });
98 });98 });
@@ -358,14 +358,14 @@ describe("source-definitions", () => { · 259 unmodified lines
358 },358 },
359 },359 },
360 });360 });
361 const recipeId = stableSourceRecipeId(source);361 const catalogId = stableSourceCatalogId(source);
362 const recipeRevisionId = stableSourceRecipeRevisionId(source);362 const catalogRevisionId = stableSourceCatalogRevisionId(source);
363 const existingCredentialId = CredentialIdSchema.make("cred_existing");363 const existingCredentialId = CredentialIdSchema.make("cred_existing");
364 364
365 const { sourceRecord, runtimeAuthArtifact } = splitSourceForStorage({365 const { sourceRecord, runtimeAuthArtifact } = splitSourceForStorage({
366 source,366 source,
367 recipeId,367 catalogId,
368 recipeRevisionId,368 catalogRevisionId,
369 existingRuntimeAuthArtifactId: existingCredentialId,369 existingRuntimeAuthArtifactId: existingCredentialId,
370 });370 });
371 371
@@ -426,8 +426,8 @@ describe("source-definitions", () => { · 54 unmodified lines
426 for (const source of [withRefresh, withoutRefresh]) {426 for (const source of [withRefresh, withoutRefresh]) {
427 const { sourceRecord, runtimeAuthArtifact } = splitSourceForStorage({427 const { sourceRecord, runtimeAuthArtifact } = splitSourceForStorage({
428 source,428 source,
429 recipeId: stableSourceRecipeId(source),429 catalogId: stableSourceCatalogId(source),
430 recipeRevisionId: stableSourceRecipeRevisionId(source),430 catalogRevisionId: stableSourceCatalogRevisionId(source),
431 });431 });
432 const projected = await Effect.runPromise(projectSourceFromStorage({432 const projected = await Effect.runPromise(projectSourceFromStorage({
433 sourceRecord,433 sourceRecord,
@@ -445,8 +445,8 @@ describe("source-definitions", () => { · 11 unmodified lines
445 });445 });
446 const { sourceRecord, runtimeAuthArtifact } = splitSourceForStorage({446 const { sourceRecord, runtimeAuthArtifact } = splitSourceForStorage({
447 source,447 source,
448 recipeId: stableSourceRecipeId(source),448 catalogId: stableSourceCatalogId(source),
449 recipeRevisionId: stableSourceRecipeRevisionId(source),449 catalogRevisionId: stableSourceCatalogRevisionId(source),
450 });450 });
451 451
452 expect(runtimeAuthArtifact).toBeNull();452 expect(runtimeAuthArtifact).toBeNull();
packages/control-plane/src/runtime/source-definitions.ts+47−47
@@ -11,19 +11,19 @@ import type { · 10 unmodified lines
11 Source,11 Source,
12 SourceAuth,12 SourceAuth,
13 SourceImportAuthPolicy,13 SourceImportAuthPolicy,
14 SourceRecipeAdapterKey,14 SourceCatalogAdapterKey,
15 SourceRecipeId,15 SourceCatalogId,
16 SourceRecipeKind,16 SourceCatalogKind,
17 SourceRecipeRevisionId,17 SourceCatalogRevisionId,
18 StoredSourceRecord,18 StoredSourceRecord,
19 StoredSourceRecipeRecord,19 StoredSourceCatalogRecord,
20 StoredSourceRecipeRevisionRecord,20 StoredSourceCatalogRevisionRecord,
21 WorkspaceId,21 WorkspaceId,
22} from "#schema";22} from "#schema";
23import {23import {
24 AuthArtifactIdSchema,24 AuthArtifactIdSchema,
25 SourceRecipeIdSchema,25 SourceCatalogIdSchema,
26 SourceRecipeRevisionIdSchema,26 SourceCatalogRevisionIdSchema,
27} from "#schema";27} from "#schema";
28import * as Effect from "effect/Effect";28import * as Effect from "effect/Effect";
29 29
@@ -42,45 +42,45 @@ const trimOrNull = (value: string | null | undefined): string | null => { · 12 unmodified lines
42 return trimmed.length > 0 ? trimmed : null;42 return trimmed.length > 0 ? trimmed : null;
43};43};
44 44
45type SourceRecipeSourceConfig = Record<string, unknown>;45type SourceCatalogSourceConfig = Record<string, unknown>;
46 46
47const sourceConfigFromSource = (source: Source): SourceRecipeSourceConfig =>47const sourceConfigFromSource = (source: Source): SourceCatalogSourceConfig =>
48 getSourceAdapterForSource(source).sourceConfigFromSource(source);48 getSourceAdapterForSource(source).sourceConfigFromSource(source);
49 49
50const sourceRecipeKindFromSource = (source: Source): SourceRecipeKind => {50const sourceCatalogKindFromSource = (source: Source): SourceCatalogKind => {
51 const adapter = getSourceAdapterForSource(source);51 const adapter = getSourceAdapterForSource(source);
52 return adapter.family;52 return adapter.family;
53};53};
54 54
55const sourceRecipeAdapterKeyFromSource = (source: Source): SourceRecipeAdapterKey => {55const sourceCatalogAdapterKeyFromSource = (source: Source): SourceCatalogAdapterKey => {
56 return getSourceAdapterForSource(source).key;56 return getSourceAdapterForSource(source).key;
57};57};
58 58
59const sourceRecipeProviderKeyFromSource = (source: Source): string => {59const sourceCatalogProviderKeyFromSource = (source: Source): string => {
60 return getSourceAdapterForSource(source).providerKey;60 return getSourceAdapterForSource(source).providerKey;
61};61};
62 62
63const stableHash = (value: string): string =>63const stableHash = (value: string): string =>
64 sha256Hex(value).slice(0, 24);64 sha256Hex(value).slice(0, 24);
65 65
66const sourceRecipeSignature = (source: Source): string =>66const sourceCatalogSignature = (source: Source): string =>
67 JSON.stringify({67 JSON.stringify({
68 recipeKind: sourceRecipeKindFromSource(source),68 catalogKind: sourceCatalogKindFromSource(source),
69 adapterKey: sourceRecipeAdapterKeyFromSource(source),69 adapterKey: sourceCatalogAdapterKeyFromSource(source),
70 providerKey: sourceRecipeProviderKeyFromSource(source),70 providerKey: sourceCatalogProviderKeyFromSource(source),
71 sourceConfig: sourceConfigFromSource(source),71 sourceConfig: sourceConfigFromSource(source),
72 });72 });
73 73
74export const sourceConfigSignature = (source: Source): string =>74export const sourceConfigSignature = (source: Source): string =>
75 JSON.stringify(sourceConfigFromSource(source));75 JSON.stringify(sourceConfigFromSource(source));
76 76
77export const stableSourceRecipeId = (source: Source): SourceRecipeId =>77export const stableSourceCatalogId = (source: Source): SourceCatalogId =>
78 SourceRecipeIdSchema.make(`src_recipe_${stableHash(sourceRecipeSignature(source))}`);78 SourceCatalogIdSchema.make(`src_catalog_${stableHash(sourceCatalogSignature(source))}`);
79 79
80export const stableSourceRecipeRevisionId = (80export const stableSourceCatalogRevisionId = (
81 source: Source,81 source: Source,
82): SourceRecipeRevisionId =>82): SourceCatalogRevisionId =>
83 SourceRecipeRevisionIdSchema.make(`src_recipe_rev_${stableHash(sourceConfigSignature(source))}`);83 SourceCatalogRevisionIdSchema.make(`src_catalog_rev_${stableHash(sourceConfigSignature(source))}`);
84 84
85const normalizeAuth = (85const normalizeAuth = (
86 auth: SourceAuth | undefined,86 auth: SourceAuth | undefined,
@@ -298,15 +298,15 @@ export const updateSourceFromPayload = (input: { · 211 unmodified lines
298 });298 });
299 });299 });
300 300
301export const createSourceRecipeRecord = (input: {301export const createSourceCatalogRecord = (input: {
302 source: Source;302 source: Source;
303 recipeId?: SourceRecipeId | null;303 catalogId?: SourceCatalogId | null;
304 latestRevisionId: SourceRecipeRevisionId;304 latestRevisionId: SourceCatalogRevisionId;
305}): StoredSourceRecipeRecord => ({305}): StoredSourceCatalogRecord => ({
306 id: input.recipeId ?? stableSourceRecipeId(input.source),306 id: input.catalogId ?? stableSourceCatalogId(input.source),
307 kind: sourceRecipeKindFromSource(input.source),307 kind: sourceCatalogKindFromSource(input.source),
308 adapterKey: sourceRecipeAdapterKeyFromSource(input.source),308 adapterKey: sourceCatalogAdapterKeyFromSource(input.source),
309 providerKey: sourceRecipeProviderKeyFromSource(input.source),309 providerKey: sourceCatalogProviderKeyFromSource(input.source),
310 name: input.source.name,310 name: input.source.name,
311 summary: null,311 summary: null,
312 visibility: "workspace",312 visibility: "workspace",
@@ -315,32 +315,32 @@ export const createSourceRecipeRecord = (input: { · 2 unmodified lines
315 updatedAt: input.source.updatedAt,315 updatedAt: input.source.updatedAt,
316});316});
317 317
318export const createSourceRecipeRevisionRecord = (input: {318export const createSourceCatalogRevisionRecord = (input: {
319 source: Source;319 source: Source;
320 recipeId: SourceRecipeId;320 catalogId: SourceCatalogId;
321 recipeRevisionId?: SourceRecipeRevisionId | null;321 catalogRevisionId?: SourceCatalogRevisionId | null;
322 revisionNumber: number;322 revisionNumber: number;
323 manifestJson?: string | null;323 importMetadataJson?: string | null;
324 manifestHash?: string | null;324 importMetadataHash?: string | null;
325 materializationHash?: string | null;325 snapshotHash?: string | null;
326}): StoredSourceRecipeRevisionRecord => ({326}): StoredSourceCatalogRevisionRecord => ({
327 id:327 id:
328 input.recipeRevisionId328 input.catalogRevisionId
329 ?? stableSourceRecipeRevisionId(input.source),329 ?? stableSourceCatalogRevisionId(input.source),
330 recipeId: input.recipeId,330 catalogId: input.catalogId,
331 revisionNumber: input.revisionNumber,331 revisionNumber: input.revisionNumber,
332 sourceConfigJson: sourceConfigSignature(input.source),332 sourceConfigJson: sourceConfigSignature(input.source),
333 manifestJson: input.manifestJson ?? null,333 importMetadataJson: input.importMetadataJson ?? null,
334 manifestHash: input.manifestHash ?? null,334 importMetadataHash: input.importMetadataHash ?? null,
335 materializationHash: input.materializationHash ?? null,335 snapshotHash: input.snapshotHash ?? null,
336 createdAt: input.source.createdAt,336 createdAt: input.source.createdAt,
337 updatedAt: input.source.updatedAt,337 updatedAt: input.source.updatedAt,
338});338});
339 339
340export const splitSourceForStorage = (input: {340export const splitSourceForStorage = (input: {
341 source: Source;341 source: Source;
342 recipeId: SourceRecipeId;342 catalogId: SourceCatalogId;
343 recipeRevisionId: SourceRecipeRevisionId;343 catalogRevisionId: SourceCatalogRevisionId;
344 actorAccountId?: AccountId | null;344 actorAccountId?: AccountId | null;
345 existingRuntimeAuthArtifactId?: AuthArtifact["id"] | null;345 existingRuntimeAuthArtifactId?: AuthArtifact["id"] | null;
346 existingImportAuthArtifactId?: AuthArtifact["id"] | null;346 existingImportAuthArtifactId?: AuthArtifact["id"] | null;
@@ -352,8 +352,8 @@ export const splitSourceForStorage = (input: { · 5 unmodified lines
352 const sourceRecord: StoredSourceRecord = {352 const sourceRecord: StoredSourceRecord = {
353 id: input.source.id,353 id: input.source.id,
354 workspaceId: input.source.workspaceId,354 workspaceId: input.source.workspaceId,
355 recipeId: input.recipeId,355 catalogId: input.catalogId,
356 recipeRevisionId: input.recipeRevisionId,356 catalogRevisionId: input.catalogRevisionId,
357 name: input.source.name,357 name: input.source.name,
358 kind: input.source.kind,358 kind: input.source.kind,
359 endpoint: input.source.endpoint,359 endpoint: input.source.endpoint,
packages/control-plane/src/runtime/source-discovery.test.ts+1−1
@@ -4,7 +4,7 @@ import { · 3 unmodified lines
4 type ServerResponse,4 type ServerResponse,
5} from "node:http";5} from "node:http";
6 6
7import { describe, expect, it } from "vitest";7import { describe, expect, it } from "@effect/vitest";
8import * as Effect from "effect/Effect";8import * as Effect from "effect/Effect";
9 9
10import { discoverSource } from "./source-discovery";10import { discoverSource } from "./source-discovery";
packages/control-plane/src/runtime/source-inspection.ts+133−248
@@ -1,37 +1,27 @@
1import type {1import type {
2 Source,
3 SourceId,2 SourceId,
4 SourceInspection,3 SourceInspection,
5 SourceInspectionDiscoverPayload,4 SourceInspectionDiscoverPayload,
6 SourceInspectionDiscoverResult,5 SourceInspectionDiscoverResult,
7 SourceInspectionDiscoverResultItem,6 SourceInspectionDiscoverResultItem,
8 SourceInspectionSchemaBundle,
9 SourceInspectionToolListItem,
10 SourceInspectionToolDetail,7 SourceInspectionToolDetail,
8 SourceInspectionToolListItem,
11 SourceInspectionToolSummary,9 SourceInspectionToolSummary,
12 StoredSourceRecipeOperationRecord,
13 WorkspaceId,10 WorkspaceId,
14} from "#schema";11} from "#schema";
15import { SourceRecipeSchemaBundleIdSchema } from "#schema";
16import {12import {
17 ControlPlaneNotFoundError,13 ControlPlaneNotFoundError,
18 ControlPlaneStorageError,14 ControlPlaneStorageError,
19} from "../api/errors";15} from "../api/errors";
20import * as Effect from "effect/Effect";16import * as Effect from "effect/Effect";
21import * as Option from "effect/Option";
22 17
23import { operationErrors } from "./operation-errors";18import { operationErrors } from "./operation-errors";
24import { formatWithPrettier } from "./prettier-format";19import { formatWithPrettier } from "./prettier-format";
25import {20import {
26 getSourceAdapterForOperation,21 expandCatalogTools,
27} from "./source-adapters";22 loadSourceWithCatalog,
28import {23 type LoadedSourceCatalogTool,
29 RuntimeSourceRecipeStoreService,24} from "./source-catalog-runtime";
30 recipeToolMetadata,
31 recipeToolPath,
32} from "./source-recipes-runtime";
33import type { SourceAdapterPersistedOperationMetadata } from "./source-adapters/types";
34import { namespaceFromSourceName } from "./source-names";
35 25
36const sourceInspectOps = {26const sourceInspectOps = {
37 bundle: operationErrors("sources.inspect.bundle"),27 bundle: operationErrors("sources.inspect.bundle"),
@@ -46,21 +36,6 @@ const tokenize = (value: string): Array<string> => · 8 unmodified lines
46 .split(/[^a-z0-9]+/)36 .split(/[^a-z0-9]+/)
47 .filter(Boolean);37 .filter(Boolean);
48 38
49type InspectionToolRecord = {
50 operation: StoredSourceRecipeOperationRecord;
51 metadata: SourceAdapterPersistedOperationMetadata;
52 listItem: SourceInspectionToolListItem;
53 searchText: string;
54};
55
56type ResolvedSourceInspection = {
57 source: Source;
58 namespace: string;
59 pipelineKind: SourceInspection["pipelineKind"];
60 schemaBundleId: string | null;
61 tools: ReadonlyArray<InspectionToolRecord>;
62};
63
64const formatOptionalJson = (value: string | null) =>39const formatOptionalJson = (value: string | null) =>
65 value === null40 value === null
66 ? Effect.succeed<string | null>(null)41 ? Effect.succeed<string | null>(null)
@@ -71,198 +46,163 @@ const formatOptionalTypeScript = (value: string | undefined) => · 4 unmodified lines
71 ? Effect.succeed<string | undefined>(undefined)46 ? Effect.succeed<string | undefined>(undefined)
72 : Effect.promise(() => formatWithPrettier(value, "typescript"));47 : Effect.promise(() => formatWithPrettier(value, "typescript"));
73 48
74const 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
86const formatInspectionToolDetail = (detail: SourceInspectionToolDetail) =>49const formatInspectionToolDetail = (detail: SourceInspectionToolDetail) =>
87 Effect.gen(function* () {50 Effect.gen(function* () {
88 const summary = yield* formatToolSummary(detail.summary);51 const summary = yield* Effect.all({
52 inputType: formatOptionalTypeScript(detail.summary.inputType),
53 outputType: formatOptionalTypeScript(detail.summary.outputType),
54 }).pipe(
55 Effect.map(({ inputType, outputType }) => ({
56 ...detail.summary,
57 ...(inputType ? { inputType } : {}),
58 ...(outputType ? { outputType } : {}),
59 })),
60 );
89 const detailFields = yield* Effect.all({61 const detailFields = yield* Effect.all({
90 definitionJson: formatOptionalJson(detail.definitionJson),62 definitionJson: formatOptionalJson(detail.definitionJson),
91 documentationJson: formatOptionalJson(detail.documentationJson),63 documentationJson: formatOptionalJson(detail.documentationJson),
92 providerDataJson: formatOptionalJson(detail.providerDataJson),64 nativeJson: formatOptionalJson(detail.nativeJson),
93 inputSchemaJson: formatOptionalJson(detail.inputSchemaJson),65 callSchemaJson: formatOptionalJson(detail.callSchemaJson),
94 outputSchemaJson: formatOptionalJson(detail.outputSchemaJson),66 resultSchemaJson: formatOptionalJson(detail.resultSchemaJson),
95 exampleInputJson: formatOptionalJson(detail.exampleInputJson),67 exampleCallJson: formatOptionalJson(detail.exampleCallJson),
96 exampleOutputJson: formatOptionalJson(detail.exampleOutputJson),68 exampleResultJson: formatOptionalJson(detail.exampleResultJson),
97 });69 });
98 70
99 return {71 return {
100 ...detail,72 ...detail,
101 ...detailFields,
102 summary,73 summary,
74 ...detailFields,
103 } satisfies SourceInspectionToolDetail;75 } satisfies SourceInspectionToolDetail;
104 });76 });
105 77
106const loadSourceRecipeRecord = (input: {78const executableDetails = (tool: LoadedSourceCatalogTool) => {
107 workspaceId: WorkspaceId;79 switch (tool.executable.protocol) {
108 sourceId: SourceId;80 case "http":
109}) =>
110 Effect.flatMap(RuntimeSourceRecipeStoreService, (sourceRecipeStore) =>
111 Effect.gen(function* () {
112 const recipe = yield* sourceRecipeStore.loadSourceWithRecipe({
113 workspaceId: input.workspaceId,
114 sourceId: input.sourceId,
115 }).pipe(
116 Effect.mapError((cause) =>
117 cause instanceof Error && cause.message.startsWith("Source not found:")
118 ? sourceInspectOps.bundle.notFound(
119 "Source not found",
120 `workspaceId=${input.workspaceId} sourceId=${input.sourceId}`,
121 )
122 : sourceInspectOps.bundle.unknownStorage(
123 cause,
124 "Failed loading source recipe",
125 ),
126 ),
127 );
128
129 return {81 return {
130 source: recipe.source,82 method: tool.executable.method,
131 operations: recipe.operations,83 pathTemplate: tool.executable.pathTemplate,
132 schemaBundleId: recipe.schemaBundles.find((bundle) => bundle.bundleKind === "json_schema_ref_map")?.id84 rawToolId: tool.path.split(".").at(-1) ?? null,
133 ?? recipe.schemaBundles[0]?.id85 operationId: null,
134 ?? null,86 group: null,
87 leaf: tool.path.split(".").at(-1) ?? null,
88 tags: tool.capability.surface.tags ?? [],
135 };89 };
136 }),90 case "graphql":
137 );91 return {
92 method: tool.executable.operationType,
93 pathTemplate: tool.executable.rootField,
94 rawToolId: tool.path.split(".").at(-1) ?? null,
95 operationId: tool.executable.rootField,
96 group: null,
97 leaf: tool.executable.rootField,
98 tags: tool.capability.surface.tags ?? [],
99 };
100 case "mcp":
101 return {
102 method: null,
103 pathTemplate: null,
104 rawToolId: tool.path.split(".").at(-1) ?? null,
105 operationId: tool.executable.toolName,
106 group: null,
107 leaf: tool.executable.toolName,
108 tags: tool.capability.surface.tags ?? [],
109 };
110 }
111};
138 112
139const inspectionToolListItemFromOperation = (input: {113const inspectionToolListItemFromTool = (tool: LoadedSourceCatalogTool): SourceInspectionToolListItem => ({
140 source: Source;114 path: tool.path,
141 operation: StoredSourceRecipeOperationRecord;115 method:
142 metadata: SourceAdapterPersistedOperationMetadata;116 tool.executable.protocol === "http"
143}): SourceInspectionToolListItem => ({117 ? tool.executable.method
144 path: recipeToolPath({118 : tool.executable.protocol === "graphql"
145 source: input.source,119 ? tool.executable.operationType
146 operation: input.operation,120 : null,
147 }),
148 method: input.metadata.method,
149});121});
150 122
151const persistedToolSummaryFromRecipeOperation = (input: {123const persistedToolSummaryFromTool = (tool: LoadedSourceCatalogTool): SourceInspectionToolSummary => {
152 source: Source;124 const details = executableDetails(tool);
153 operation: StoredSourceRecipeOperationRecord;
154 metadata: SourceAdapterPersistedOperationMetadata;
155 includeTypes: boolean;
156}): SourceInspectionToolSummary => {
157 const path = inspectionToolListItemFromOperation(input).path;
158 const descriptor = input.includeTypes
159 ? getSourceAdapterForOperation(input.operation).createToolDescriptor({
160 source: input.source,
161 operation: input.operation,
162 path,
163 includeSchemas: false,
164 })
165 : null;
166 125
167 return {126 return {
168 path,127 path: tool.path,
169 sourceKey: input.source.id,128 sourceKey: tool.source.id,
170 ...(input.operation.title ? { title: input.operation.title } : {}),129 ...(tool.capability.surface.title ? { title: tool.capability.surface.title } : {}),
171 ...(input.operation.description ? { description: input.operation.description } : {}),130 ...(tool.capability.surface.summary || tool.capability.surface.description
172 providerKind: input.operation.providerKind,131 ? { description: tool.capability.surface.summary ?? tool.capability.surface.description! }
173 toolId: input.operation.toolId,132 : {}),
174 rawToolId: input.metadata.rawToolId,133 protocol: tool.executable.protocol,
175 operationId: input.metadata.operationId,134 toolId: tool.path.split(".").at(-1) ?? tool.path,
176 group: input.metadata.group,135 rawToolId: details.rawToolId,
177 leaf: input.metadata.leaf,136 operationId: details.operationId,
178 tags: [...input.metadata.tags],137 group: details.group,
179 method: input.metadata.method,138 leaf: details.leaf,
180 pathTemplate: input.metadata.pathTemplate,139 tags: [...details.tags],
181 ...(descriptor?.inputType ? { inputType: descriptor.inputType } : {}),140 method: details.method,
182 ...(descriptor?.outputType ? { outputType: descriptor.outputType } : {}),141 pathTemplate: details.pathTemplate,
142 ...(tool.descriptor.inputType ? { inputType: tool.descriptor.inputType } : {}),
143 ...(tool.descriptor.outputType ? { outputType: tool.descriptor.outputType } : {}),
183 };144 };
184};145};
185 146
186const inspectionToolDetailFromOperation = (input: {147const inspectionToolDetailFromTool = (tool: LoadedSourceCatalogTool): SourceInspectionToolDetail => ({
187 source: Source;148 summary: persistedToolSummaryFromTool(tool),
188 operation: StoredSourceRecipeOperationRecord;149 definitionJson: JSON.stringify({
189 metadata: SourceAdapterPersistedOperationMetadata;150 capability: tool.capability,
190 schemaBundleId: string | null;151 executable: tool.executable,
191}): SourceInspectionToolDetail => {152 }),
192 const summary = persistedToolSummaryFromRecipeOperation({153 documentationJson: JSON.stringify({
193 source: input.source,154 capabilityDocs: {
194 operation: input.operation,155 summary: tool.capability.surface.summary,
195 metadata: input.metadata,156 description: tool.capability.surface.description,
196 includeTypes: true,157 },
197 });158 }),
198 159 nativeJson: tool.executable.native?.[0]?.value
199 return {160 ? JSON.stringify(tool.executable.native[0]!.value)
200 summary,161 : null,
201 definitionJson: null,162 callSchemaJson: tool.descriptor.inputSchema
202 documentationJson: null,163 ? JSON.stringify(tool.descriptor.inputSchema)
203 providerDataJson: input.operation.providerDataJson,164 : null,
204 inputSchemaJson: input.operation.inputSchemaJson,165 resultSchemaJson: tool.descriptor.outputSchema
205 outputSchemaJson: input.operation.outputSchemaJson,166 ? JSON.stringify(tool.descriptor.outputSchema)
206 schemaBundleId: input.schemaBundleId,167 : null,
207 exampleInputJson: null,168 exampleCallJson: null,
208 exampleOutputJson: null,169 exampleResultJson: null,
209 } satisfies SourceInspectionToolDetail;170});
210};
211 171
212const resolveSourceInspection = (input: {172const resolveSourceInspection = (input: {
213 workspaceId: WorkspaceId;173 workspaceId: WorkspaceId;
214 sourceId: SourceId;174 sourceId: SourceId;
215}) =>175}) =>
216 Effect.gen(function* () {176 Effect.gen(function* () {
217 const { source, operations, schemaBundleId } = yield* loadSourceRecipeRecord(input);177 const catalogEntry = yield* loadSourceWithCatalog({
218 const namespace = source.namespace ?? namespaceFromSourceName(source.name);178 workspaceId: input.workspaceId,
179 sourceId: input.sourceId,
180 });
181 const tools = yield* expandCatalogTools({
182 catalogs: [catalogEntry],
183 includeSchemas: true,
184 });
219 185
220 return {186 return {
221 source,187 source: catalogEntry.source,
222 namespace,188 namespace: catalogEntry.source.namespace ?? "",
223 pipelineKind: "persisted",189 pipelineKind: "ir" as const,
224 schemaBundleId,190 tools,
225 tools: yield* Effect.forEach(operations, (operation) =>191 };
226 Effect.gen(function* () {
227 const path = recipeToolPath({
228 source,
229 operation,
230 });
231 const metadata = yield* recipeToolMetadata({
232 source,
233 operation,
234 path,
235 });
236 const listItem = inspectionToolListItemFromOperation({
237 source,
238 operation,
239 metadata,
240 });
241
242 return {
243 operation,
244 metadata,
245 listItem,
246 searchText: metadata.searchText,
247 } satisfies InspectionToolRecord;
248 })
249 ),
250 } satisfies ResolvedSourceInspection;
251 });192 });
252 193
253const scoreTool = (input: {194const scoreTool = (input: {
254 queryTokens: ReadonlyArray<string>;195 queryTokens: ReadonlyArray<string>;
255 tool: InspectionToolRecord;196 tool: LoadedSourceCatalogTool;
256}): SourceInspectionDiscoverResultItem | null => {197}): SourceInspectionDiscoverResultItem | null => {
257 let score = 0;198 let score = 0;
258 const reasons: Array<string> = [];199 const reasons: Array<string> = [];
259 const pathTokens = tokenize(input.tool.listItem.path);200 const summary = persistedToolSummaryFromTool(input.tool);
260 const titleTokens = tokenize(input.tool.operation.title ?? "");201 const pathTokens = tokenize(summary.path);
261 const descriptionTokens = tokenize(input.tool.operation.description ?? "");202 const titleTokens = tokenize(summary.title ?? "");
262 const tagTokens = input.tool.metadata.tags.flatMap(tokenize);203 const descriptionTokens = tokenize(summary.description ?? "");
263 const methodPathTokens = tokenize(204 const tagTokens = summary.tags.flatMap(tokenize);
264 `${input.tool.listItem.method ?? ""} ${input.tool.metadata.pathTemplate ?? ""}`,205 const methodPathTokens = tokenize(`${summary.method ?? ""} ${summary.pathTemplate ?? ""}`);
265 );
266 206
267 for (const token of input.queryTokens) {207 for (const token of input.queryTokens) {
268 if (pathTokens.includes(token)) {208 if (pathTokens.includes(token)) {
@@ -296,13 +236,13 @@ const scoreTool = (input: { · 27 unmodified lines
296 }236 }
297 237
298 return {238 return {
299 path: input.tool.listItem.path,239 path: input.tool.path,
300 score,240 score,
301 ...(input.tool.operation.description241 ...(summary.description ? { description: summary.description } : {}),
302 ? { description: input.tool.operation.description }242 ...(summary.inputType ? { inputType: summary.inputType } : {}),
303 : {}),243 ...(summary.outputType ? { outputType: summary.outputType } : {}),
304 reasons,244 reasons,
305 } satisfies SourceInspectionDiscoverResultItem;245 };
306};246};
307 247
308const mapInspectionError = (248const mapInspectionError = (
@@ -316,6 +256,7 @@ const mapInspectionError = ( · 7 unmodified lines
316 if (cause instanceof ControlPlaneNotFoundError) {256 if (cause instanceof ControlPlaneNotFoundError) {
317 return cause;257 return cause;
318 }258 }
259
319 if (cause instanceof ControlPlaneStorageError) {260 if (cause instanceof ControlPlaneStorageError) {
320 return cause;261 return cause;
321 }262 }
@@ -335,7 +276,7 @@ export const getSourceInspection = (input: { · 13 unmodified lines
335 namespace: inspection.namespace,276 namespace: inspection.namespace,
336 pipelineKind: inspection.pipelineKind,277 pipelineKind: inspection.pipelineKind,
337 toolCount: inspection.tools.length,278 toolCount: inspection.tools.length,
338 tools: inspection.tools.map((tool) => tool.listItem),279 tools: inspection.tools.map((tool) => inspectionToolListItemFromTool(tool)),
339 } satisfies SourceInspection;280 } satisfies SourceInspection;
340 }).pipe(281 }).pipe(
341 Effect.mapError((cause) =>282 Effect.mapError((cause) =>
@@ -356,7 +297,7 @@ export const getSourceInspectionToolDetail = (input: { · 14 unmodified lines
356 workspaceId: input.workspaceId,297 workspaceId: input.workspaceId,
357 sourceId: input.sourceId,298 sourceId: input.sourceId,
358 });299 });
359 const tool = inspection.tools.find((candidate) => candidate.listItem.path === input.toolPath);300 const tool = inspection.tools.find((candidate) => candidate.path === input.toolPath);
360 301
361 if (!tool) {302 if (!tool) {
362 return yield* Effect.fail(303 return yield* Effect.fail(
@@ -367,12 +308,7 @@ export const getSourceInspectionToolDetail = (input: { · 4 unmodified lines
367 );308 );
368 }309 }
369 310
370 return yield* formatInspectionToolDetail(inspectionToolDetailFromOperation({311 return yield* formatInspectionToolDetail(inspectionToolDetailFromTool(tool));
371 source: inspection.source,
372 operation: tool.operation,
373 metadata: tool.metadata,
374 schemaBundleId: inspection.schemaBundleId,
375 }));
376 }).pipe(312 }).pipe(
377 Effect.mapError((cause) =>313 Effect.mapError((cause) =>
378 mapInspectionError(314 mapInspectionError(
@@ -382,57 +318,6 @@ export const getSourceInspectionToolDetail = (input: { · 3 unmodified lines
382 )),318 )),
383 );319 );
384 320
385export 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
436export const discoverSourceInspectionTools = (input: {321export const discoverSourceInspectionTools = (input: {
437 workspaceId: WorkspaceId;322 workspaceId: WorkspaceId;
438 sourceId: SourceId;323 sourceId: SourceId;
packages/control-plane/src/runtime/source-recipe-support.ts+0−27
@@ -1,27 +0,0 @@
1import { sha256Hex } from "@executor/codemode-core";
2
3import type {
4 StoredSourceRecipeDocumentRecord,
5 StoredSourceRecipeOperationRecord,
6 StoredSourceRecipeSchemaBundleRecord,
7} from "#schema";
8
9export 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
18export const contentHash = (value: string): string => sha256Hex(value);
19
20export 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};
packages/control-plane/src/runtime/source-recipes-runtime.ts+0−598
@@ -1,598 +0,0 @@
1import {
2 type ToolCatalogEntry,
3 type ToolDescriptor,
4} from "@executor/codemode-core";
5import type {
6 AccountId,
7 Source,
8 StoredSourceRecord,
9 StoredSourceRecipeDocumentRecord,
10 StoredSourceRecipeOperationRecord,
11 StoredSourceRecipeSchemaBundleRecord,
12 StoredSourceRecipeRevisionRecord,
13 WorkspaceId,
14} from "#schema";
15import * as Context from "effect/Context";
16import * as Effect from "effect/Effect";
17import * as Layer from "effect/Layer";
18import { LocalSourceArtifactMissingError } from "./local-errors";
19import {
20 RuntimeLocalWorkspaceService,
21 type RuntimeLocalWorkspaceState,
22} from "./local-runtime-context";
23import {
24 SourceArtifactStore,
25 type SourceArtifactStoreShape,
26} from "./local-storage";
27import { namespaceFromSourceName } from "./source-names";
28import {
29 getSourceAdapterForOperation,
30 getSourceAdapterForSource,
31} from "./source-adapters";
32import type { SourceAdapterPersistedOperationMetadata } from "./source-adapters/types";
33import { firstSchemaBundle } from "./source-adapters/shared";
34import {
35 RuntimeSourceStoreService,
36 type RuntimeSourceStore,
37} from "./source-store";
38
39type RecipeManifest = unknown | null;
40
41export 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
51export 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
65export 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
77export 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
87const 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
96const catalogNamespaceFromPath = (path: string): string => {
97 const [first, second] = path.split(".");
98 return second ? `${first}.${second}` : first;
99};
100
101export 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
109export 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
121export 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
130export 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
137const 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
150export const recipePrimaryDocumentText = (input: {
151 source: Source;
152 documents: readonly StoredSourceRecipeDocumentRecord[];
153}): string | null =>
154 sourceRecipeDocumentForSource(input)?.contentText ?? null;
155
156const 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
175const 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
200type 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
231export type RuntimeSourceRecipeStore = RuntimeSourceRecipeStoreShape;
232
233export class RuntimeSourceRecipeStoreService extends Context.Tag(
234 "#runtime/RuntimeSourceRecipeStoreService",
235)<RuntimeSourceRecipeStoreService, RuntimeSourceRecipeStoreShape>() {}
236
237type RuntimeSourceRecipeStoreDeps = {
238 runtimeLocalWorkspace: RuntimeLocalWorkspaceState;
239 sourceStore: RuntimeSourceStore;
240 sourceArtifactStore: SourceArtifactStoreShape;
241};
242
243type SourceRecipeRuntimeServices =
244 | RuntimeLocalWorkspaceService
245 | RuntimeSourceStoreService
246 | SourceArtifactStore;
247
248const 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
263const 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
307const 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
351export 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
370export 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
394export 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
454export 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
485export 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
520export 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);
packages/control-plane/src/runtime/source-store.ts+6−6
@@ -14,8 +14,8 @@ import * as Effect from "effect/Effect"; · 13 unmodified lines
14import * as Layer from "effect/Layer";14import * as Layer from "effect/Layer";
15 15
16import {16import {
17 stableSourceRecipeId,17 stableSourceCatalogId,
18 stableSourceRecipeRevisionId,18 stableSourceCatalogRevisionId,
19 splitSourceForStorage,19 splitSourceForStorage,
20} from "./source-definitions";20} from "./source-definitions";
21import {21import {
@@ -407,8 +407,8 @@ const buildLocalSourceRecord = (input: { · 385 unmodified lines
407 const sourceRecord = {407 const sourceRecord = {
408 id: baseSource.id,408 id: baseSource.id,
409 workspaceId: baseSource.workspaceId,409 workspaceId: baseSource.workspaceId,
410 recipeId: artifact?.recipeId ?? stableSourceRecipeId(baseSource),410 catalogId: artifact?.catalogId ?? stableSourceCatalogId(baseSource),
411 recipeRevisionId: artifact?.revision.id ?? stableSourceRecipeRevisionId(baseSource),411 catalogRevisionId: artifact?.revision.id ?? stableSourceCatalogRevisionId(baseSource),
412 name: baseSource.name,412 name: baseSource.name,
413 kind: baseSource.kind,413 kind: baseSource.kind,
414 endpoint: baseSource.endpoint,414 endpoint: baseSource.endpoint,
@@ -855,8 +855,8 @@ const persistSourceWithDeps = ( · 440 unmodified lines
855 855
856 const { runtimeAuthArtifact, importAuthArtifact } = splitSourceForStorage({856 const { runtimeAuthArtifact, importAuthArtifact } = splitSourceForStorage({
857 source: nextSource,857 source: nextSource,
858 recipeId: stableSourceRecipeId(nextSource),858 catalogId: stableSourceCatalogId(nextSource),
859 recipeRevisionId: stableSourceRecipeRevisionId(nextSource),859 catalogRevisionId: stableSourceCatalogRevisionId(nextSource),
860 actorAccountId: options.actorAccountId,860 actorAccountId: options.actorAccountId,
861 existingRuntimeAuthArtifactId: existingRuntimeAuthArtifact?.id ?? null,861 existingRuntimeAuthArtifactId: existingRuntimeAuthArtifact?.id ?? null,
862 existingImportAuthArtifactId: existingImportAuthArtifact?.id ?? null,862 existingImportAuthArtifactId: existingImportAuthArtifact?.id ?? null,
packages/control-plane/src/runtime/sources-operations.ts+3−3
@@ -25,7 +25,7 @@ import { · 24 unmodified lines
25 operationErrors,25 operationErrors,
26} from "./operation-errors";26} from "./operation-errors";
27import { ControlPlaneStore, type ControlPlaneStoreShape } from "./store";27import { ControlPlaneStore, type ControlPlaneStoreShape } from "./store";
28import { RuntimeSourceMaterializationService } from "./source-materialization";28import { RuntimeSourceCatalogSyncService } from "./source-catalog-sync";
29import {29import {
30 RuntimeSourceStoreService,30 RuntimeSourceStoreService,
31} from "./source-store";31} from "./source-store";
@@ -51,7 +51,7 @@ const syncArtifactsForSource = (input: { · 19 unmodified lines
51 | typeof sourceOps.update;51 | typeof sourceOps.update;
52}) =>52}) =>
53 Effect.gen(function* () {53 Effect.gen(function* () {
54 const materializationService = yield* RuntimeSourceMaterializationService;54 const catalogSyncService = yield* RuntimeSourceCatalogSyncService;
55 55
56 // For HTTP-backed source kinds that can validate themselves from a remote56 // For HTTP-backed source kinds that can validate themselves from a remote
57 // document, automatically attempt to probe and connect. This mirrors the57 // document, automatically attempt to probe and connect. This mirrors the
@@ -63,7 +63,7 @@ const syncArtifactsForSource = (input: { · 5 unmodified lines
63 : input.source;63 : input.source;
64 64
65 const synced = yield* Effect.either(65 const synced = yield* Effect.either(
66 materializationService.sync({66 catalogSyncService.sync({
67 source: sourceForSync,67 source: sourceForSync,
68 actorAccountId: input.actorAccountId,68 actorAccountId: input.actorAccountId,
69 }),69 }),
packages/control-plane/src/runtime/workspace-execution-environment.ts+62−122
@@ -8,11 +8,9 @@ import { · 7 unmodified lines
8 type ToolCatalog,8 type ToolCatalog,
9 type ToolInvoker,9 type ToolInvoker,
10 type ToolPath,10 type ToolPath,
11 type ToolSchemaBundle,
12} from "@executor/codemode-core";11} from "@executor/codemode-core";
13import { makeQuickJsExecutor } from "@executor/runtime-quickjs";12import { makeQuickJsExecutor } from "@executor/runtime-quickjs";
14import type { AccountId, Source, SourceRecipeSchemaBundleId } from "#schema";13import type { AccountId, Source } from "#schema";
15import { SourceRecipeSchemaBundleIdSchema } from "#schema";
16import * as Context from "effect/Context";14import * as Context from "effect/Context";
17import * as Either from "effect/Either";15import * as Either from "effect/Either";
18import * as Effect from "effect/Effect";16import * as Effect from "effect/Effect";
@@ -29,13 +27,15 @@ import { · 10 unmodified lines
29 type RuntimeSourceAuthService,27 type RuntimeSourceAuthService,
30} from "./source-auth-service";28} from "./source-auth-service";
31import {29import {
32 RuntimeSourceRecipeStoreService,30 RuntimeSourceCatalogStoreService,
33 type LoadedSourceRecipeToolIndexEntry,31 type LoadedSourceCatalogToolIndexEntry,
34 recipeToolCatalogEntry,32 catalogToolCatalogEntry,
35} from "./source-recipes-runtime";33} from "./source-catalog-runtime";
34import {
35 invocationDescriptorFromTool,
36 invokeIrTool,
37} from "./ir-execution";
36import { RuntimeSourceAuthMaterialService } from "./source-auth-material";38import { RuntimeSourceAuthMaterialService } from "./source-auth-material";
37import { namespaceFromSourceName } from "./source-names";
38import { getSourceAdapterForOperation } from "./source-adapters";
39import {39import {
40 type SecretMaterialResolveContext,40 type SecretMaterialResolveContext,
41} from "./secret-material-providers";41} from "./secret-material-providers";
@@ -145,14 +145,14 @@ const toSecretResolutionContext = ( · 103 unmodified lines
145const queryTokenWeight = (token: string): number =>145const queryTokenWeight = (token: string): number =>
146 LOW_SIGNAL_QUERY_TOKENS.has(token) ? 0.25 : 1;146 LOW_SIGNAL_QUERY_TOKENS.has(token) ? 0.25 : 1;
147 147
148const loadWorkspaceRecipeTools = (input: {148const loadWorkspaceCatalogTools = (input: {
149 workspaceId: Source["workspaceId"];149 workspaceId: Source["workspaceId"];
150 accountId: AccountId;150 accountId: AccountId;
151 sourceRecipeStore: Effect.Effect.Success<typeof RuntimeSourceRecipeStoreService>;151 sourceCatalogStore: Effect.Effect.Success<typeof RuntimeSourceCatalogStoreService>;
152 includeSchemas: boolean;152 includeSchemas: boolean;
153}): Effect.Effect<readonly LoadedSourceRecipeToolIndexEntry[], Error, WorkspaceStorageServices> =>153}): Effect.Effect<readonly LoadedSourceCatalogToolIndexEntry[], Error, WorkspaceStorageServices> =>
154 Effect.map(154 Effect.map(
155 input.sourceRecipeStore.loadWorkspaceSourceRecipeToolIndex({155 input.sourceCatalogStore.loadWorkspaceSourceCatalogToolIndex({
156 workspaceId: input.workspaceId,156 workspaceId: input.workspaceId,
157 actorAccountId: input.accountId,157 actorAccountId: input.accountId,
158 includeSchemas: input.includeSchemas,158 includeSchemas: input.includeSchemas,
@@ -163,14 +163,14 @@ const loadWorkspaceRecipeTools = (input: { · 4 unmodified lines
163 ),163 ),
164 );164 );
165 165
166const loadWorkspaceRecipeToolByPath = (input: {166const loadWorkspaceCatalogToolByPath = (input: {
167 workspaceId: Source["workspaceId"];167 workspaceId: Source["workspaceId"];
168 accountId: AccountId;168 accountId: AccountId;
169 sourceRecipeStore: Effect.Effect.Success<typeof RuntimeSourceRecipeStoreService>;169 sourceCatalogStore: Effect.Effect.Success<typeof RuntimeSourceCatalogStoreService>;
170 path: string;170 path: string;
171 includeSchemas: boolean;171 includeSchemas: boolean;
172}): Effect.Effect<LoadedSourceRecipeToolIndexEntry | null, Error, WorkspaceStorageServices> =>172}): Effect.Effect<LoadedSourceCatalogToolIndexEntry | null, Error, WorkspaceStorageServices> =>
173 input.sourceRecipeStore.loadWorkspaceSourceRecipeToolByPath({173 input.sourceCatalogStore.loadWorkspaceSourceCatalogToolByPath({
174 workspaceId: input.workspaceId,174 workspaceId: input.workspaceId,
175 path: input.path,175 path: input.path,
176 actorAccountId: input.accountId,176 actorAccountId: input.accountId,
@@ -183,42 +183,29 @@ const loadWorkspaceRecipeToolByPath = (input: { · 6 unmodified lines
183 ),183 ),
184 );184 );
185 185
186const loadWorkspaceSchemaBundle = (input: {186const scoreCatalogTool = (
187 workspaceId: Source["workspaceId"];
188 sourceRecipeStore: Effect.Effect.Success<typeof RuntimeSourceRecipeStoreService>;
189 id: SourceRecipeSchemaBundleId;
190}): Effect.Effect<ToolSchemaBundle | null, Error, WorkspaceStorageServices> =>
191 Effect.map(
192 input.sourceRecipeStore.loadWorkspaceSchemaBundle({
193 workspaceId: input.workspaceId,
194 id: input.id,
195 }),
196 (bundle) =>
197 bundle
198 ? {
199 id: bundle.id,
200 kind: bundle.kind as ToolSchemaBundle["kind"],
201 hash: bundle.hash,
202 refsJson: bundle.refsJson,
203 }
204 : null,
205 );
206
207const scoreRecipeTool = (
208 queryTokens: readonly string[],187 queryTokens: readonly string[],
209 tool: LoadedSourceRecipeToolIndexEntry,188 tool: LoadedSourceCatalogToolIndexEntry,
210): number => {189): number => {
211 const pathText = tool.path.toLowerCase();190 const pathText = tool.path.toLowerCase();
212 const namespaceText = tool.searchNamespace.toLowerCase();191 const namespaceText = tool.searchNamespace.toLowerCase();
213 const toolIdText = tool.operation.toolId.toLowerCase();192 const toolIdText = tool.path.split(".").at(-1)?.toLowerCase() ?? "";
214 const titleText = tool.operation.title?.toLowerCase() ?? "";193 const titleText = tool.capability.surface.title?.toLowerCase() ?? "";
215 const descriptionText = tool.operation.description?.toLowerCase() ?? "";194 const descriptionText =
216 const templateText = tool.metadata.pathTemplate?.toLowerCase() ?? "";195 tool.capability.surface.summary?.toLowerCase()
217 196 ?? tool.capability.surface.description?.toLowerCase()
218 const pathTokens = tokenize(`${tool.path} ${tool.operation.toolId}`);197 ?? "";
198 const templateText =
199 tool.executable.protocol === "http"
200 ? tool.executable.pathTemplate.toLowerCase()
201 : tool.executable.protocol === "graphql"
202 ? tool.executable.rootField.toLowerCase()
203 : tool.executable.toolName.toLowerCase();
204
205 const pathTokens = tokenize(`${tool.path} ${toolIdText}`);
219 const namespaceTokens = tokenize(tool.searchNamespace);206 const namespaceTokens = tokenize(tool.searchNamespace);
220 const titleTokens = tokenize(tool.operation.title ?? "");207 const titleTokens = tokenize(tool.capability.surface.title ?? "");
221 const templateTokens = tokenize(tool.metadata.pathTemplate ?? "");208 const templateTokens = tokenize(templateText);
222 209
223 let score = 0;210 let score = 0;
224 let structuralHits = 0;211 let structuralHits = 0;
@@ -342,20 +329,9 @@ const approvalMessageForInvocation = ( · 117 unmodified lines
342 return `Allow tool call: ${descriptor.toolPath}?`;329 return `Allow tool call: ${descriptor.toolPath}?`;
343};330};
344 331
345const toInvocationDescriptorFromRecipeTool = (input: {332const toInvocationDescriptorFromCatalogTool = (input: {
346 tool: LoadedSourceRecipeToolIndexEntry;333 tool: LoadedSourceCatalogToolIndexEntry;
347}): InvocationDescriptor => ({334}): InvocationDescriptor => invocationDescriptorFromTool(input) satisfies InvocationDescriptor;
348 toolPath: input.tool.path,
349 sourceId: input.tool.source.id,
350 sourceName: input.tool.source.name,
351 sourceKind: input.tool.source.kind,
352 sourceNamespace:
353 input.tool.source.namespace ??
354 namespaceFromSourceName(input.tool.source.name),
355 operationKind: input.tool.operation.operationKind,
356 interaction: input.tool.metadata.interaction,
357 approvalLabel: input.tool.metadata.approvalLabel,
358});
359 335
360const authorizePersistedToolInvocation = (input: {336const authorizePersistedToolInvocation = (input: {
361 workspaceId: Source["workspaceId"];337 workspaceId: Source["workspaceId"];
@@ -452,10 +428,10 @@ const provideRuntimeLocalWorkspace = <A, E, R>( · 90 unmodified lines
452): Effect.Effect<A, E, R> =>428): Effect.Effect<A, E, R> =>
453 provideOptionalRuntimeLocalWorkspace(effect, runtimeLocalWorkspace);429 provideOptionalRuntimeLocalWorkspace(effect, runtimeLocalWorkspace);
454 430
455const createWorkspaceRecipeCatalog = (input: {431const createWorkspaceSourceCatalog = (input: {
456 workspaceId: Source["workspaceId"];432 workspaceId: Source["workspaceId"];
457 accountId: AccountId;433 accountId: AccountId;
458 sourceRecipeStore: Effect.Effect.Success<typeof RuntimeSourceRecipeStoreService>;434 sourceCatalogStore: Effect.Effect.Success<typeof RuntimeSourceCatalogStoreService>;
459 workspaceConfigStore: WorkspaceConfigStoreShape;435 workspaceConfigStore: WorkspaceConfigStoreShape;
460 workspaceStateStore: WorkspaceStateStoreShape;436 workspaceStateStore: WorkspaceStateStoreShape;
461 sourceArtifactStore: SourceArtifactStoreShape;437 sourceArtifactStore: SourceArtifactStoreShape;
@@ -471,26 +447,20 @@ const createWorkspaceRecipeCatalog = (input: { · 9 unmodified lines
471 447
472 const createSharedCatalog = (includeSchemas: boolean): Effect.Effect<ToolCatalog, Error, never> =>448 const createSharedCatalog = (includeSchemas: boolean): Effect.Effect<ToolCatalog, Error, never> =>
473 provideWorkspaceStorage(Effect.gen(function* () {449 provideWorkspaceStorage(Effect.gen(function* () {
474 const recipeTools = yield* loadWorkspaceRecipeTools({450 const catalogTools = yield* loadWorkspaceCatalogTools({
475 workspaceId: input.workspaceId,451 workspaceId: input.workspaceId,
476 accountId: input.accountId,452 accountId: input.accountId,
477 sourceRecipeStore: input.sourceRecipeStore,453 sourceCatalogStore: input.sourceCatalogStore,
478 includeSchemas,454 includeSchemas,
479 });455 });
480 456
481 return createToolCatalogFromEntries({457 return createToolCatalogFromEntries({
482 entries: recipeTools.map((tool) =>458 entries: catalogTools.map((tool) =>
483 recipeToolCatalogEntry({459 catalogToolCatalogEntry({
484 tool,460 tool,
485 score: (queryTokens) => scoreRecipeTool(queryTokens, tool),461 score: (queryTokens) => scoreCatalogTool(queryTokens, tool),
486 }),462 }),
487 ),463 ),
488 getSchemaBundle: ({ id }) =>
489 provideWorkspaceStorage(loadWorkspaceSchemaBundle({
490 workspaceId: input.workspaceId,
491 sourceRecipeStore: input.sourceRecipeStore,
492 id: SourceRecipeSchemaBundleIdSchema.make(id),
493 })),
494 });464 });
495 }));465 }));
496 466
@@ -524,14 +494,6 @@ const createWorkspaceRecipeCatalog = (input: { · 27 unmodified lines
524 input.runtimeLocalWorkspace,494 input.runtimeLocalWorkspace,
525 ),495 ),
526 496
527 getSchemaBundle: ({ id }) =>
528 provideRuntimeLocalWorkspace(
529 Effect.flatMap(createSharedCatalog(false), (catalog) =>
530 catalog.getSchemaBundle({ id }),
531 ),
532 input.runtimeLocalWorkspace,
533 ),
534
535 searchTools: ({ query, namespace, limit }) =>497 searchTools: ({ query, namespace, limit }) =>
536 provideRuntimeLocalWorkspace(498 provideRuntimeLocalWorkspace(
537 Effect.flatMap(createSharedCatalog(false), (catalog) =>499 Effect.flatMap(createSharedCatalog(false), (catalog) =>
@@ -549,7 +511,7 @@ const createWorkspaceRecipeCatalog = (input: { · 11 unmodified lines
549const createWorkspaceToolInvoker = (input: {511const createWorkspaceToolInvoker = (input: {
550 workspaceId: Source["workspaceId"];512 workspaceId: Source["workspaceId"];
551 accountId: AccountId;513 accountId: AccountId;
552 sourceRecipeStore: Effect.Effect.Success<typeof RuntimeSourceRecipeStoreService>;514 sourceCatalogStore: Effect.Effect.Success<typeof RuntimeSourceCatalogStoreService>;
553 workspaceConfigStore: WorkspaceConfigStoreShape;515 workspaceConfigStore: WorkspaceConfigStoreShape;
554 workspaceStateStore: WorkspaceStateStoreShape;516 workspaceStateStore: WorkspaceStateStoreShape;
555 sourceArtifactStore: SourceArtifactStoreShape;517 sourceArtifactStore: SourceArtifactStoreShape;
@@ -578,10 +540,10 @@ const createWorkspaceToolInvoker = (input: { · 22 unmodified lines
578 sourceAuthService: input.sourceAuthService,540 sourceAuthService: input.sourceAuthService,
579 runtimeLocalWorkspace: input.runtimeLocalWorkspace,541 runtimeLocalWorkspace: input.runtimeLocalWorkspace,
580 });542 });
581 const recipeCatalog = createWorkspaceRecipeCatalog({543 const sourceCatalog = createWorkspaceSourceCatalog({
582 workspaceId: input.workspaceId,544 workspaceId: input.workspaceId,
583 accountId: input.accountId,545 accountId: input.accountId,
584 sourceRecipeStore: input.sourceRecipeStore,546 sourceCatalogStore: input.sourceCatalogStore,
585 workspaceConfigStore: input.workspaceConfigStore,547 workspaceConfigStore: input.workspaceConfigStore,
586 workspaceStateStore: input.workspaceStateStore,548 workspaceStateStore: input.workspaceStateStore,
587 sourceArtifactStore: input.sourceArtifactStore,549 sourceArtifactStore: input.sourceArtifactStore,
@@ -606,7 +568,7 @@ const createWorkspaceToolInvoker = (input: { · 18 unmodified lines
606 tools: authoredTools,568 tools: authoredTools,
607 });569 });
608 catalog = mergeToolCatalogs({570 catalog = mergeToolCatalogs({
609 catalogs: [authoredCatalog, recipeCatalog],571 catalogs: [authoredCatalog, sourceCatalog],
610 });572 });
611 const authoredToolPaths = new Set(Object.keys(authoredTools));573 const authoredToolPaths = new Set(Object.keys(authoredTools));
612 const authoredInvoker = makeToolInvokerFromTools({574 const authoredInvoker = makeToolInvokerFromTools({
@@ -621,14 +583,14 @@ const createWorkspaceToolInvoker = (input: { · 8 unmodified lines
621 }) =>583 }) =>
622 provideRuntimeLocalWorkspace(584 provideRuntimeLocalWorkspace(
623 provideWorkspaceStorage(Effect.gen(function* () {585 provideWorkspaceStorage(Effect.gen(function* () {
624 const recipeTool = yield* loadWorkspaceRecipeToolByPath({586 const catalogTool = yield* loadWorkspaceCatalogToolByPath({
625 workspaceId: input.workspaceId,587 workspaceId: input.workspaceId,
626 accountId: input.accountId,588 accountId: input.accountId,
627 sourceRecipeStore: input.sourceRecipeStore,589 sourceCatalogStore: input.sourceCatalogStore,
628 path: invocation.path,590 path: invocation.path,
629 includeSchemas: false,591 includeSchemas: false,
630 });592 });
631 if (!recipeTool) {593 if (!catalogTool) {
632 return yield* Effect.fail(594 return yield* Effect.fail(
633 new Error(`Unknown tool path: ${invocation.path}`),595 new Error(`Unknown tool path: ${invocation.path}`),
634 );596 );
@@ -637,49 +599,27 @@ const createWorkspaceToolInvoker = (input: { · 2 unmodified lines
637 yield* authorizePersistedToolInvocation({599 yield* authorizePersistedToolInvocation({
638 workspaceId: input.workspaceId,600 workspaceId: input.workspaceId,
639 accountId: input.accountId,601 accountId: input.accountId,
640 descriptor: toInvocationDescriptorFromRecipeTool({602 descriptor: toInvocationDescriptorFromCatalogTool({
641 tool: recipeTool,603 tool: catalogTool,
642 }),604 }),
643 args: invocation.args,605 args: invocation.args,
644 source: recipeTool.source,606 source: catalogTool.source,
645 context: invocation.context,607 context: invocation.context,
646 onElicitation: input.onElicitation,608 onElicitation: input.onElicitation,
647 });609 });
648 610
649 const auth = yield* input.sourceAuthMaterialService.resolve({611 const auth = yield* input.sourceAuthMaterialService.resolve({
650 source: recipeTool.source,612 source: catalogTool.source,
651 actorAccountId: input.accountId,613 actorAccountId: input.accountId,
652 context: toSecretResolutionContext(invocation.context),614 context: toSecretResolutionContext(invocation.context),
653 });615 });
654 const schemaBundle = recipeTool.schemaBundleId616 return yield* invokeIrTool({
655 ? yield* loadWorkspaceSchemaBundle({
656 workspaceId: input.workspaceId,
657 sourceRecipeStore: input.sourceRecipeStore,
658 id: SourceRecipeSchemaBundleIdSchema.make(
659 recipeTool.schemaBundleId,
660 ),
661 })
662 : null;
663 const recipe = yield* input.sourceRecipeStore.loadSourceWithRecipe({
664 workspaceId: input.workspaceId,
665 sourceId: recipeTool.source.id,
666 actorAccountId: input.accountId,
667 });
668
669 return yield* getSourceAdapterForOperation(
670 recipeTool.operation,
671 ).invokePersistedTool({
672 workspaceId: input.workspaceId,617 workspaceId: input.workspaceId,
673 accountId: input.accountId,618 accountId: input.accountId,
674 source: recipeTool.source,619 tool: catalogTool,
675 path: invocation.path,
676 operation: recipeTool.operation,
677 schemaBundle,
678 manifestJson: recipe.revision.manifestJson,
679 auth,620 auth,
680 args: invocation.args,621 args: invocation.args,
681 context: invocation.context,622 context: invocation.context,
682 onElicitation: input.onElicitation,
683 });623 });
684 })),624 })),
685 input.runtimeLocalWorkspace,625 input.runtimeLocalWorkspace,
@@ -702,7 +642,7 @@ const createWorkspaceToolInvoker = (input: { · 16 unmodified lines
702export const createWorkspaceExecutionEnvironmentResolver = (input: {642export const createWorkspaceExecutionEnvironmentResolver = (input: {
703 sourceAuthMaterialService: Effect.Effect.Success<typeof RuntimeSourceAuthMaterialService>;643 sourceAuthMaterialService: Effect.Effect.Success<typeof RuntimeSourceAuthMaterialService>;
704 sourceAuthService: RuntimeSourceAuthService;644 sourceAuthService: RuntimeSourceAuthService;
705 sourceRecipeStore: Effect.Effect.Success<typeof RuntimeSourceRecipeStoreService>;645 sourceCatalogStore: Effect.Effect.Success<typeof RuntimeSourceCatalogStoreService>;
706 localToolRuntimeLoader: LocalToolRuntimeLoaderShape;646 localToolRuntimeLoader: LocalToolRuntimeLoaderShape;
707 workspaceConfigStore: WorkspaceConfigStoreShape;647 workspaceConfigStore: WorkspaceConfigStoreShape;
708 workspaceStateStore: WorkspaceStateStoreShape;648 workspaceStateStore: WorkspaceStateStoreShape;
@@ -723,7 +663,7 @@ export const createWorkspaceExecutionEnvironmentResolver = (input: { · 14 unmodified lines
723 const { catalog, toolInvoker } = createWorkspaceToolInvoker({663 const { catalog, toolInvoker } = createWorkspaceToolInvoker({
724 workspaceId,664 workspaceId,
725 accountId,665 accountId,
726 sourceRecipeStore: input.sourceRecipeStore,666 sourceCatalogStore: input.sourceCatalogStore,
727 workspaceConfigStore: input.workspaceConfigStore,667 workspaceConfigStore: input.workspaceConfigStore,
728 workspaceStateStore: input.workspaceStateStore,668 workspaceStateStore: input.workspaceStateStore,
729 sourceArtifactStore: input.sourceArtifactStore,669 sourceArtifactStore: input.sourceArtifactStore,
@@ -762,7 +702,7 @@ export const RuntimeExecutionResolverLive = ( · 32 unmodified lines
762 Effect.gen(function* () {702 Effect.gen(function* () {
763 const sourceAuthMaterialService = yield* RuntimeSourceAuthMaterialService;703 const sourceAuthMaterialService = yield* RuntimeSourceAuthMaterialService;
764 const sourceAuthService = yield* RuntimeSourceAuthServiceTag;704 const sourceAuthService = yield* RuntimeSourceAuthServiceTag;
765 const sourceRecipeStore = yield* RuntimeSourceRecipeStoreService;705 const sourceCatalogStore = yield* RuntimeSourceCatalogStoreService;
766 const localToolRuntimeLoader = yield* LocalToolRuntimeLoaderService;706 const localToolRuntimeLoader = yield* LocalToolRuntimeLoaderService;
767 const workspaceConfigStore = yield* WorkspaceConfigStore;707 const workspaceConfigStore = yield* WorkspaceConfigStore;
768 const workspaceStateStore = yield* WorkspaceStateStore;708 const workspaceStateStore = yield* WorkspaceStateStore;
@@ -771,7 +711,7 @@ export const RuntimeExecutionResolverLive = ( · 2 unmodified lines
771 return createWorkspaceExecutionEnvironmentResolver({711 return createWorkspaceExecutionEnvironmentResolver({
772 sourceAuthService,712 sourceAuthService,
773 sourceAuthMaterialService,713 sourceAuthMaterialService,
774 sourceRecipeStore,714 sourceCatalogStore,
775 localToolRuntimeLoader,715 localToolRuntimeLoader,
776 workspaceConfigStore,716 workspaceConfigStore,
777 workspaceStateStore,717 workspaceStateStore,
packages/control-plane/src/schema/enums.ts+7−15
@@ -16,21 +16,13 @@ export { · 15 unmodified lines
16 type SecretRef,16 type SecretRef,
17} from "./models/auth-artifact";17} from "./models/auth-artifact";
18export {18export {
19 SourceRecipeAdapterKeySchema,19 SourceCatalogAdapterKeySchema,
20 SourceRecipeDocumentKindSchema,20 SourceCatalogKindSchema,
21 SourceRecipeKindSchema,21 SourceCatalogVisibilitySchema,
22 SourceRecipeOperationKindSchema,22 type SourceCatalogAdapterKey,
23 SourceRecipeOperationProviderKindSchema,23 type SourceCatalogKind,
24 SourceRecipeTransportKindSchema,24 type SourceCatalogVisibility,
25 SourceRecipeVisibilitySchema,25} from "./models/source-catalog";
26 type SourceRecipeAdapterKey,
27 type SourceRecipeDocumentKind,
28 type SourceRecipeKind,
29 type SourceRecipeOperationKind,
30 type SourceRecipeOperationProviderKind,
31 type SourceRecipeTransportKind,
32 type SourceRecipeVisibility,
33} from "./models/source-recipe";
34export {26export {
35 SourceAuthInferenceSchema,27 SourceAuthInferenceSchema,
36 SourceDiscoveryAuthKindSchema,28 SourceDiscoveryAuthKindSchema,
packages/control-plane/src/schema/ids.ts+5−9
@@ -3,12 +3,9 @@ import { Schema } from "effect"; · 2 unmodified lines
3export const AccountIdSchema = Schema.String.pipe(Schema.brand("AccountId"));3export const AccountIdSchema = Schema.String.pipe(Schema.brand("AccountId"));
4export const WorkspaceIdSchema = Schema.String.pipe(Schema.brand("WorkspaceId"));4export const WorkspaceIdSchema = Schema.String.pipe(Schema.brand("WorkspaceId"));
5export const SourceIdSchema = Schema.String.pipe(Schema.brand("SourceId"));5export const SourceIdSchema = Schema.String.pipe(Schema.brand("SourceId"));
6export const SourceRecipeIdSchema = Schema.String.pipe(Schema.brand("SourceRecipeId"));6export const SourceCatalogIdSchema = Schema.String.pipe(Schema.brand("SourceCatalogId"));
7export const SourceRecipeRevisionIdSchema = Schema.String.pipe(7export const SourceCatalogRevisionIdSchema = Schema.String.pipe(
8 Schema.brand("SourceRecipeRevisionId"),8 Schema.brand("SourceCatalogRevisionId"),
9);
10export const SourceRecipeSchemaBundleIdSchema = Schema.String.pipe(
11 Schema.brand("SourceRecipeSchemaBundleId"),
12);9);
13export const SourceAuthSessionIdSchema = Schema.String.pipe(10export const SourceAuthSessionIdSchema = Schema.String.pipe(
14 Schema.brand("SourceAuthSessionId"),11 Schema.brand("SourceAuthSessionId"),
@@ -38,9 +35,8 @@ export const ExecutionStepIdSchema = Schema.String.pipe( · 23 unmodified lines
38export type AccountId = typeof AccountIdSchema.Type;35export type AccountId = typeof AccountIdSchema.Type;
39export type WorkspaceId = typeof WorkspaceIdSchema.Type;36export type WorkspaceId = typeof WorkspaceIdSchema.Type;
40export type SourceId = typeof SourceIdSchema.Type;37export type SourceId = typeof SourceIdSchema.Type;
41export type SourceRecipeId = typeof SourceRecipeIdSchema.Type;38export type SourceCatalogId = typeof SourceCatalogIdSchema.Type;
42export type SourceRecipeRevisionId = typeof SourceRecipeRevisionIdSchema.Type;39export type SourceCatalogRevisionId = typeof SourceCatalogRevisionIdSchema.Type;
43export type SourceRecipeSchemaBundleId = typeof SourceRecipeSchemaBundleIdSchema.Type;
44export type SourceAuthSessionId = typeof SourceAuthSessionIdSchema.Type;40export type SourceAuthSessionId = typeof SourceAuthSessionIdSchema.Type;
45export type AuthArtifactId = typeof AuthArtifactIdSchema.Type;41export type AuthArtifactId = typeof AuthArtifactIdSchema.Type;
46export type AuthLeaseId = typeof AuthLeaseIdSchema.Type;42export type AuthLeaseId = typeof AuthLeaseIdSchema.Type;
packages/control-plane/src/schema/index.ts+1−1
@@ -4,7 +4,7 @@ export * from "./ids"; · 3 unmodified lines
4 4
5export * from "./models/auth";5export * from "./models/auth";
6export * from "./models/source";6export * from "./models/source";
7export * from "./models/source-recipe";7export * from "./models/source-catalog";
8export * from "./models/auth-artifact";8export * from "./models/auth-artifact";
9export * from "./models/auth-lease";9export * from "./models/auth-lease";
10export * from "./models/source-oauth-client";10export * from "./models/source-oauth-client";
packages/control-plane/src/schema/models/source-catalog.ts+53−0
@@ -0,0 +1,53 @@
1import { Schema } from "effect";
2
3import { TimestampMsSchema } from "../common";
4import {
5 SourceCatalogIdSchema,
6 SourceCatalogRevisionIdSchema,
7} from "../ids";
8
9export const SourceCatalogKindSchema = Schema.Literal(
10 "http_api",
11 "mcp",
12 "internal",
13);
14
15export const SourceCatalogAdapterKeySchema = Schema.String;
16
17export const SourceCatalogVisibilitySchema = Schema.Literal(
18 "private",
19 "workspace",
20 "organization",
21 "public",
22);
23
24export 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
37export 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
49export type SourceCatalogKind = typeof SourceCatalogKindSchema.Type;
50export type SourceCatalogAdapterKey = typeof SourceCatalogAdapterKeySchema.Type;
51export type SourceCatalogVisibility = typeof SourceCatalogVisibilitySchema.Type;
52export type StoredSourceCatalogRecord = typeof StoredSourceCatalogRecordSchema.Type;
53export type StoredSourceCatalogRevisionRecord = typeof StoredSourceCatalogRevisionRecordSchema.Type;
packages/control-plane/src/schema/models/source-inspection.ts+7−21
@@ -2,18 +2,14 @@ import { Schema } from "effect"; · 1 unmodified line
2 2
3import { SourceSchema } from "./source";3import { SourceSchema } from "./source";
4 4
5export const SourceInspectionPipelineKindSchema = Schema.Literal(5export const SourceInspectionPipelineKindSchema = Schema.Literal("ir");
6 "openapi",
7 "graphql",
8 "persisted",
9);
10 6
11export const SourceInspectionToolSummarySchema = Schema.Struct({7export const SourceInspectionToolSummarySchema = Schema.Struct({
12 path: Schema.String,8 path: Schema.String,
13 sourceKey: Schema.String,9 sourceKey: Schema.String,
14 title: Schema.optional(Schema.String),10 title: Schema.optional(Schema.String),
15 description: Schema.optional(Schema.String),11 description: Schema.optional(Schema.String),
16 providerKind: Schema.String,12 protocol: Schema.String,
17 toolId: Schema.String,13 toolId: Schema.String,
18 rawToolId: Schema.NullOr(Schema.String),14 rawToolId: Schema.NullOr(Schema.String),
19 operationId: Schema.NullOr(Schema.String),15 operationId: Schema.NullOr(Schema.String),
@@ -43,19 +39,11 @@ export const SourceInspectionToolDetailSchema = Schema.Struct({ · 23 unmodified lines
43 summary: SourceInspectionToolSummarySchema,39 summary: SourceInspectionToolSummarySchema,
44 definitionJson: Schema.NullOr(Schema.String),40 definitionJson: Schema.NullOr(Schema.String),
45 documentationJson: Schema.NullOr(Schema.String),41 documentationJson: Schema.NullOr(Schema.String),
46 providerDataJson: Schema.NullOr(Schema.String),42 nativeJson: Schema.NullOr(Schema.String),
47 inputSchemaJson: Schema.NullOr(Schema.String),43 callSchemaJson: Schema.NullOr(Schema.String),
48 outputSchemaJson: Schema.NullOr(Schema.String),44 resultSchemaJson: Schema.NullOr(Schema.String),
49 schemaBundleId: Schema.NullOr(Schema.String),45 exampleCallJson: Schema.NullOr(Schema.String),
50 exampleInputJson: Schema.NullOr(Schema.String),46 exampleResultJson: Schema.NullOr(Schema.String),
51 exampleOutputJson: Schema.NullOr(Schema.String),
52});
53
54export const SourceInspectionSchemaBundleSchema = Schema.Struct({
55 id: Schema.String,
56 kind: Schema.String,
57 hash: Schema.String,
58 refsJson: Schema.String,
59});47});
60 48
61export const SourceInspectionDiscoverPayloadSchema = Schema.Struct({49export const SourceInspectionDiscoverPayloadSchema = Schema.Struct({
@@ -89,8 +77,6 @@ export type SourceInspectionToolListItem = · 27 unmodified lines
89export type SourceInspection = typeof SourceInspectionSchema.Type;77export type SourceInspection = typeof SourceInspectionSchema.Type;
90export type SourceInspectionToolDetail =78export type SourceInspectionToolDetail =
91 typeof SourceInspectionToolDetailSchema.Type;79 typeof SourceInspectionToolDetailSchema.Type;
92export type SourceInspectionSchemaBundle =
93 typeof SourceInspectionSchemaBundleSchema.Type;
94export type SourceInspectionDiscoverPayload =80export type SourceInspectionDiscoverPayload =
95 typeof SourceInspectionDiscoverPayloadSchema.Type;81 typeof SourceInspectionDiscoverPayloadSchema.Type;
96export type SourceInspectionDiscoverResultItem =82export type SourceInspectionDiscoverResultItem =
packages/control-plane/src/schema/models/source-recipe.ts+0−131
@@ -1,131 +0,0 @@
1import { Schema } from "effect";
2
3import { TimestampMsSchema } from "../common";
4import {
5 SourceRecipeIdSchema,
6 SourceRecipeRevisionIdSchema,
7 SourceRecipeSchemaBundleIdSchema,
8} from "../ids";
9
10export const SourceRecipeKindSchema = Schema.Literal(
11 "http_api",
12 "mcp",
13 "internal",
14);
15
16export const SourceRecipeAdapterKeySchema = Schema.String;
17
18export const SourceRecipeVisibilitySchema = Schema.Literal(
19 "private",
20 "workspace",
21 "organization",
22 "public",
23);
24
25export const SourceRecipeDocumentKindSchema = Schema.String;
26
27export const SourceRecipeSchemaBundleKindSchema = Schema.String;
28
29export const SourceRecipeTransportKindSchema = Schema.Literal(
30 "http",
31 "graphql",
32 "mcp",
33 "internal",
34);
35
36export const SourceRecipeOperationKindSchema = Schema.Literal(
37 "read",
38 "write",
39 "delete",
40 "unknown",
41);
42
43export const SourceRecipeOperationProviderKindSchema = Schema.String;
44
45export 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
58export 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
70export 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
82export 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
92export 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});
109export const StoredSourceRecipeOperationRecordSchema =
110 StoredSourceRecipeOperationRowSchema.annotations({
111 identifier: "StoredSourceRecipeOperationRecord",
112 });
113
114export type SourceRecipeKind = typeof SourceRecipeKindSchema.Type;
115export type SourceRecipeAdapterKey = typeof SourceRecipeAdapterKeySchema.Type;
116export type SourceRecipeVisibility = typeof SourceRecipeVisibilitySchema.Type;
117export type SourceRecipeDocumentKind = typeof SourceRecipeDocumentKindSchema.Type;
118export type SourceRecipeSchemaBundleKind =
119 typeof SourceRecipeSchemaBundleKindSchema.Type;
120export type SourceRecipeTransportKind = typeof SourceRecipeTransportKindSchema.Type;
121export type SourceRecipeOperationKind = typeof SourceRecipeOperationKindSchema.Type;
122export type SourceRecipeOperationProviderKind =
123 typeof SourceRecipeOperationProviderKindSchema.Type;
124export type StoredSourceRecipeRecord = typeof StoredSourceRecipeRecordSchema.Type;
125export type StoredSourceRecipeRevisionRecord = typeof StoredSourceRecipeRevisionRecordSchema.Type;
126export type StoredSourceRecipeDocumentRecord = typeof StoredSourceRecipeDocumentRecordSchema.Type;
127export type StoredSourceRecipeSchemaBundleRecord =
128 typeof StoredSourceRecipeSchemaBundleRecordSchema.Type;
129export type StoredSourceRecipeOperationRow = typeof StoredSourceRecipeOperationRowSchema.Type;
130export type StoredSourceRecipeOperationRecord =
131 typeof StoredSourceRecipeOperationRecordSchema.Type;
packages/control-plane/src/schema/models/source.ts+10−10
@@ -3,8 +3,8 @@ import { Schema } from "effect"; · 2 unmodified lines
3import { TimestampMsSchema } from "../common";3import { TimestampMsSchema } from "../common";
4import {4import {
5 SourceIdSchema,5 SourceIdSchema,
6 SourceRecipeIdSchema,6 SourceCatalogIdSchema,
7 SourceRecipeRevisionIdSchema,7 SourceCatalogRevisionIdSchema,
8 WorkspaceIdSchema,8 WorkspaceIdSchema,
9} from "../ids";9} from "../ids";
10import { SecretRefSchema } from "./auth-artifact";10import { SecretRefSchema } from "./auth-artifact";
@@ -77,8 +77,8 @@ export const SourceBindingSchema = Schema.Struct({ · 66 unmodified lines
77const SourceStorageRowSchema = Schema.Struct({77const SourceStorageRowSchema = Schema.Struct({
78 workspaceId: WorkspaceIdSchema,78 workspaceId: WorkspaceIdSchema,
79 sourceId: SourceIdSchema,79 sourceId: SourceIdSchema,
80 recipeId: SourceRecipeIdSchema,80 catalogId: SourceCatalogIdSchema,
81 recipeRevisionId: SourceRecipeRevisionIdSchema,81 catalogRevisionId: SourceCatalogRevisionIdSchema,
82 name: Schema.String,82 name: Schema.String,
83 kind: SourceKindSchema,83 kind: SourceKindSchema,
84 endpoint: Schema.String,84 endpoint: Schema.String,
@@ -98,8 +98,8 @@ export const StoredSourceRecordSchema = Schema.transform( · 13 unmodified lines
98 Schema.Struct({98 Schema.Struct({
99 id: SourceIdSchema,99 id: SourceIdSchema,
100 workspaceId: WorkspaceIdSchema,100 workspaceId: WorkspaceIdSchema,
101 recipeId: SourceRecipeIdSchema,101 catalogId: SourceCatalogIdSchema,
102 recipeRevisionId: SourceRecipeRevisionIdSchema,102 catalogRevisionId: SourceCatalogRevisionIdSchema,
103 name: Schema.String,103 name: Schema.String,
104 kind: SourceKindSchema,104 kind: SourceKindSchema,
105 endpoint: Schema.String,105 endpoint: Schema.String,
@@ -118,8 +118,8 @@ export const StoredSourceRecordSchema = Schema.transform( · 12 unmodified lines
118 decode: (row) => ({118 decode: (row) => ({
119 id: row.sourceId,119 id: row.sourceId,
120 workspaceId: row.workspaceId,120 workspaceId: row.workspaceId,
121 recipeId: row.recipeId,121 catalogId: row.catalogId,
122 recipeRevisionId: row.recipeRevisionId,122 catalogRevisionId: row.catalogRevisionId,
123 name: row.name,123 name: row.name,
124 kind: row.kind,124 kind: row.kind,
125 endpoint: row.endpoint,125 endpoint: row.endpoint,
@@ -136,8 +136,8 @@ export const StoredSourceRecordSchema = Schema.transform( · 10 unmodified lines
136 encode: (source) => ({136 encode: (source) => ({
137 workspaceId: source.workspaceId,137 workspaceId: source.workspaceId,
138 sourceId: source.id,138 sourceId: source.id,
139 recipeId: source.recipeId,139 catalogId: source.catalogId,
140 recipeRevisionId: source.recipeRevisionId,140 catalogRevisionId: source.catalogRevisionId,
141 name: source.name,141 name: source.name,
142 kind: source.kind,142 kind: source.kind,
143 endpoint: source.endpoint,143 endpoint: source.endpoint,
packages/control-plane/src/schema/schema.test.ts+39−39
@@ -3,55 +3,55 @@ import * as Either from "effect/Either"; · 2 unmodified lines
3import * as Schema from "effect/Schema";3import * as Schema from "effect/Schema";
4 4
5import {5import {
6 StoredSourceRecipeOperationRecordSchema,6 SourceInspectionToolDetailSchema,
7 StoredSourceCatalogRevisionRecordSchema,
7} from "./index";8} from "./index";
8 9
9describe("control-plane-schema", () => {10describe("control-plane-schema", () => {
10 it("accepts canonical source recipe operation rows and rejects invalid transport kinds", () => {11 it("accepts snapshot-native inspection detail rows", () => {
11 const decode = Schema.decodeUnknownEither(StoredSourceRecipeOperationRecordSchema);12 const decode = Schema.decodeUnknownEither(SourceInspectionToolDetailSchema);
12 13
13 const operationRecord = decode({14 const detail = decode({
14 id: "src_recipe_op_1",15 summary: {
15 recipeRevisionId: "src_recipe_rev_1",16 path: "github.issues.list",
16 operationKey: "getRepo",17 sourceKey: "src_1",
17 transportKind: "http",18 protocol: "http",
18 toolId: "getRepo",19 toolId: "list",
19 title: "Get Repo",20 rawToolId: "issues.list",
20 description: "Read a repository",21 operationId: "listIssues",
21 operationKind: "read",22 group: "issues",
22 searchText: "get repo",23 leaf: "list",
23 inputSchemaJson: null,24 tags: ["issues"],
24 outputSchemaJson: null,25 method: "GET",
25 providerKind: "openapi",26 pathTemplate: "/issues",
26 providerDataJson: JSON.stringify({27 },
27 kind: "openapi",28 definitionJson: "{}",
28 }),29 documentationJson: "{}",
29 createdAt: 1,30 nativeJson: "{}",
30 updatedAt: 1,31 callSchemaJson: null,
32 resultSchemaJson: null,
33 exampleCallJson: null,
34 exampleResultJson: null,
31 });35 });
32 36
33 expect(Either.isRight(operationRecord)).toBe(true);37 expect(Either.isRight(detail)).toBe(true);
38 });
39
40 it("rejects malformed catalog revisions", () => {
41 const decode = Schema.decodeUnknownEither(StoredSourceCatalogRevisionRecordSchema);
34 42
35 const invalidRecord = decode({43 const invalidRevision = decode({
36 id: "src_recipe_op_2",44 id: "src_catalog_rev_1",
37 recipeRevisionId: "src_recipe_rev_1",45 catalogId: "src_catalog_1",
38 operationKey: "viewer",46 revisionNumber: "1",
39 transportKind: "ftp",47 sourceConfigJson: "{}",
40 toolId: "viewer",48 importMetadataJson: null,
41 title: "Viewer",49 importMetadataHash: null,
42 description: null,50 snapshotHash: null,
43 operationKind: "read",
44 searchText: "viewer",
45 inputSchemaJson: null,
46 outputSchemaJson: null,
47 providerKind: "graphql",
48 providerDataJson: JSON.stringify({
49 kind: "graphql",
50 }),
51 createdAt: 1,51 createdAt: 1,
52 updatedAt: 1,52 updatedAt: 1,
53 });53 });
54 54
55 expect(Either.isLeft(invalidRecord)).toBe(true);55 expect(Either.isLeft(invalidRevision)).toBe(true);
56 });56 });
57});57});
packages/kitchen-sink/src/source-runtime.test.ts+13−14
@@ -67,8 +67,8 @@ type StoredToolArtifactRecord = { · 66 unmodified lines
67 description: string | null;67 description: string | null;
68 searchNamespace: string;68 searchNamespace: string;
69 searchText: string;69 searchText: string;
70 inputSchemaJson: string | null;70 inputSchema: unknown | null;
71 outputSchemaJson: string | null;71 outputSchema: unknown | null;
72 providerKind: "openapi";72 providerKind: "openapi";
73 openApiMethod: "get" | "put" | "post" | "delete" | "patch" | "head" | "options" | "trace";73 openApiMethod: "get" | "put" | "post" | "delete" | "patch" | "head" | "options" | "trace";
74 openApiPathTemplate: string | null;74 openApiPathTemplate: string | null;
@@ -89,10 +89,10 @@ const toDescriptor = ( · 14 unmodified lines
89 sourceKey: artifact.sourceId,89 sourceKey: artifact.sourceId,
90 description: artifact.description ?? artifact.title ?? undefined,90 description: artifact.description ?? artifact.title ?? undefined,
91 interaction: "auto" as const,91 interaction: "auto" as const,
92 inputType: includeSchemas && artifact.inputSchemaJson ? "object" : undefined,92 inputType: includeSchemas && artifact.inputSchema ? "object" : undefined,
93 outputType: includeSchemas && artifact.outputSchemaJson ? "output" : undefined,93 outputType: includeSchemas && artifact.outputSchema ? "output" : undefined,
94 inputSchemaJson: includeSchemas ? artifact.inputSchemaJson ?? undefined : undefined,94 inputSchema: includeSchemas ? artifact.inputSchema ?? undefined : undefined,
95 outputSchemaJson: includeSchemas ? artifact.outputSchemaJson ?? undefined : undefined,95 outputSchema: includeSchemas ? artifact.outputSchema ?? undefined : undefined,
96});96});
97 97
98type SourceCallContext = {98type SourceCallContext = {
@@ -359,7 +359,6 @@ const createWorkspaceToolCatalog = (input: { · 260 unmodified lines
359 path,359 path,
360 }).then((artifact) => (artifact ? toDescriptor(artifact, includeSchemas) : null))360 }).then((artifact) => (artifact ? toDescriptor(artifact, includeSchemas) : null))
361 ),361 ),
362 getSchemaBundle: () => Effect.succeed(null),
363 searchTools: ({ query, namespace, limit }) =>362 searchTools: ({ query, namespace, limit }) =>
364 Effect.promise(() =>363 Effect.promise(() =>
365 input.toolStore.list({364 input.toolStore.list({
@@ -522,8 +521,8 @@ const openApiArtifact = (input: { · 156 unmodified lines
522 method: NonNullable<StoredToolArtifactRecord["openApiMethod"]>;521 method: NonNullable<StoredToolArtifactRecord["openApiMethod"]>;
523 pathTemplate: string;522 pathTemplate: string;
524 operationHash?: string;523 operationHash?: string;
525 inputSchemaJson?: string | null;524 inputSchema?: unknown | null;
526 outputSchemaJson?: string | null;525 outputSchema?: unknown | null;
527}): StoredToolArtifactRecord => {526}): StoredToolArtifactRecord => {
528 const path = input.path as string;527 const path = input.path as string;
529 const description =528 const description =
@@ -545,8 +544,8 @@ const openApiArtifact = (input: { · 15 unmodified lines
545 input.method.toUpperCase(),544 input.method.toUpperCase(),
546 input.pathTemplate,545 input.pathTemplate,
547 ),546 ),
548 inputSchemaJson: input.inputSchemaJson ?? null,547 inputSchema: input.inputSchema ?? null,
549 outputSchemaJson: input.outputSchemaJson ?? null,548 outputSchema: input.outputSchema ?? null,
550 providerKind: "openapi",549 providerKind: "openapi",
551 openApiMethod: input.method,550 openApiMethod: input.method,
552 openApiPathTemplate: input.pathTemplate,551 openApiPathTemplate: input.pathTemplate,
@@ -783,7 +782,7 @@ describe("source runtime", () => { · 230 unmodified lines
783 "Workflow:",782 "Workflow:",
784 '1) const matches = await tools.discover({ query: "<intent>", limit: 12 });',783 '1) const matches = await tools.discover({ query: "<intent>", limit: 12 });',
785 "2) const details = await tools.describe.tool({ path, includeSchemas: true });",784 "2) const details = await tools.describe.tool({ path, includeSchemas: true });",
786 "3) If details.schemaBundleId is present, fetch it once with tools.describe.schemaBundle({ id }).",785 "3) Read details.inputSchema/details.outputSchema when you need the projected shape.",
787 "4) Call selected tools.<path>(input).",786 "4) Call selected tools.<path>(input).",
788 "Do not use fetch; use tools.* only.",787 "Do not use fetch; use tools.* only.",
789 ].join("\n"),788 ].join("\n"),
@@ -834,7 +833,7 @@ describe("source runtime", () => { · 44 unmodified lines
834 description: "GET /repos/{owner}/{repo}",833 description: "GET /repos/{owner}/{repo}",
835 interaction: "auto",834 interaction: "auto",
836 inputType: "{ owner: string, repo: string }",835 inputType: "{ owner: string, repo: string }",
837 outputType: "unknown",836 outputType: "void",
838 },837 },
839 ],838 ],
840 total: 1,839 total: 1,
@@ -853,7 +852,7 @@ describe("source runtime", () => { · 12 unmodified lines
853 "Workflow:",852 "Workflow:",
854 '1) const matches = await tools.discover({ query: "<intent>", limit: 12 });',853 '1) const matches = await tools.discover({ query: "<intent>", limit: 12 });',
855 "2) const details = await tools.describe.tool({ path, includeSchemas: true });",854 "2) const details = await tools.describe.tool({ path, includeSchemas: true });",
856 "3) If details.schemaBundleId is present, fetch it once with tools.describe.schemaBundle({ id }).",855 "3) Read details.inputSchema/details.outputSchema when you need the projected shape.",
857 "4) Call selected tools.<path>(input).",856 "4) Call selected tools.<path>(input).",
858 "Do not use fetch; use tools.* only.",857 "Do not use fetch; use tools.* only.",
859 ].join("\n"),858 ].join("\n"),
packages/react/src/index.ts+0−68
@@ -19,7 +19,6 @@ import type { · 18 unmodified lines
19 SourceDiscoveryResult,19 SourceDiscoveryResult,
20 SourceInspection,20 SourceInspection,
21 SourceInspectionDiscoverResult,21 SourceInspectionDiscoverResult,
22 SourceInspectionSchemaBundle,
23 SourceInspectionToolDetail,22 SourceInspectionToolDetail,
24 StartSourceOAuthPayload,23 StartSourceOAuthPayload,
25 StartSourceOAuthResult,24 StartSourceOAuthResult,
@@ -59,13 +58,6 @@ type SourceToolDetailKeyParts = readonly [ · 33 unmodified lines
59 Source["id"],58 Source["id"],
60 string | null,59 string | null,
61];60];
62type SourceSchemaBundleKeyParts = readonly [
63 boolean,
64 Source["workspaceId"],
65 string,
66 Source["id"],
67 string | null,
68];
69type SourceDiscoveryKeyParts = readonly [61type SourceDiscoveryKeyParts = readonly [
70 boolean,62 boolean,
71 Source["workspaceId"],63 Source["workspaceId"],
@@ -86,7 +78,6 @@ type ActiveQueryCollections = { · 14 unmodified lines
86 sources: Set<string>;78 sources: Set<string>;
87 inspections: Set<string>;79 inspections: Set<string>;
88 toolDetails: Set<string>;80 toolDetails: Set<string>;
89 schemaBundles: Set<string>;
90 discoveries: Set<string>;81 discoveries: Set<string>;
91};82};
92 83
@@ -162,17 +153,6 @@ const encodeToolDetailKey = ( · 69 unmodified lines
162): string =>153): string =>
163 encodeAtomKey([enabled, workspaceId, accountId, sourceId, toolPath] satisfies SourceToolDetailKeyParts);154 encodeAtomKey([enabled, workspaceId, accountId, sourceId, toolPath] satisfies SourceToolDetailKeyParts);
164 155
165const 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
176const encodeDiscoveryKey = (156const encodeDiscoveryKey = (
177 enabled: boolean,157 enabled: boolean,
178 workspaceId: Source["workspaceId"],158 workspaceId: Source["workspaceId"],
@@ -367,25 +347,6 @@ const sourceInspectionToolAtom = Atom.family((key: string) => { · 188 unmodified lines
367 ).pipe(Atom.keepAlive);347 ).pipe(Atom.keepAlive);
368});348});
369 349
370const 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
389const sourceDiscoveryAtom = Atom.family((key: string) => {350const sourceDiscoveryAtom = Atom.family((key: string) => {
390 const [enabled, workspaceId, accountId, sourceId, query, limit] = decodeAtomKey<SourceDiscoveryKeyParts>(key);351 const [enabled, workspaceId, accountId, sourceId, query, limit] = decodeAtomKey<SourceDiscoveryKeyParts>(key);
391 352
@@ -547,7 +508,6 @@ const createActiveQueryCollections = (): ActiveQueryCollections => ({ · 155 unmodified lines
547 sources: new Set(),508 sources: new Set(),
548 inspections: new Set(),509 inspections: new Set(),
549 toolDetails: new Set(),510 toolDetails: new Set(),
550 schemaBundles: new Set(),
551 discoveries: new Set(),511 discoveries: new Set(),
552});512});
553 513
@@ -602,13 +562,6 @@ const invalidateTrackedQueries = ( · 48 unmodified lines
602 }562 }
603 });563 });
604 564
605 activeQueries.schemaBundles.forEach((key) => {
606 const [enabled, workspaceId, accountId, sourceId] = decodeAtomKey<SourceSchemaBundleKeyParts>(key);
607 if (enabled && targetMatches(target, workspaceId, accountId, sourceId)) {
608 registry.refresh(sourceInspectionSchemaBundleAtom(key));
609 }
610 });
611
612 activeQueries.discoveries.forEach((key) => {565 activeQueries.discoveries.forEach((key) => {
613 const [enabled, workspaceId, accountId, sourceId] = decodeAtomKey<SourceDiscoveryKeyParts>(key);566 const [enabled, workspaceId, accountId, sourceId] = decodeAtomKey<SourceDiscoveryKeyParts>(key);
614 if (enabled && targetMatches(target, workspaceId, accountId, sourceId)) {567 if (enabled && targetMatches(target, workspaceId, accountId, sourceId)) {
@@ -990,27 +943,6 @@ export const useSourceToolDetail = ( · 375 unmodified lines
990 return workspace.enabled ? detail : pendingLoadable(workspace.workspace);943 return workspace.enabled ? detail : pendingLoadable(workspace.workspace);
991};944};
992 945
993export 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
1014export const useSourceDiscovery = (input: {946export const useSourceDiscovery = (input: {
1015 sourceId: string;947 sourceId: string;
1016 query: string;948 query: string;