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
2727 type ResolveExecutionEnvironment,
2828 resolveLocalWorkspaceContext,
2929 SourceIdSchema,
30− SourceRecipeRevisionIdSchema,
30+ SourceCatalogRevisionIdSchema,
3131 writeLocalSourceArtifact,
3232 writeProjectLocalExecutorConfig,
3333 } from "@executor/control-plane";
@@ -144,7 +144,7 @@ const writeConfiguredLocalMcpSource = (input: { · 110 unmodified lines
144144 };
145145
146146 const materialization = materializationFromMcpManifestEntries({
147− recipeRevisionId: SourceRecipeRevisionIdSchema.make("src_recipe_rev_materialization"),
147+ catalogRevisionId: SourceCatalogRevisionIdSchema.make("src_catalog_rev_materialization"),
148148 endpoint: input.endpoint,
149149 manifestEntries: [{
150150 toolId: "gated_echo",
packages/ai-sdk-adapter/src/example-dynamic.ts+0−3
@@ -35,9 +35,6 @@ const catalog: ToolCatalog = { · 34 unmodified lines
3535 outputType: "object",
3636 });
3737 },
38− getSchemaBundle() {
39− return Effect.succeed(null);
40− },
4138 searchTools({ limit }) {
4239 return Effect.succeed(
4340 [
packages/codemode-core/src/discovery.ts+5−25
@@ -13,7 +13,6 @@ import type { · 12 unmodified lines
1313 ToolMap,
1414 ToolNamespace,
1515 ToolPath,
16− ToolSchemaBundle,
1716 } from "./types";
1817
1918 const tokenize = (value: string): string[] =>
@@ -46,8 +45,8 @@ const projectDescriptor = (input: { · 26 unmodified lines
4645
4746 return {
4847 ...descriptor,
49− inputSchemaJson: undefined,
50− outputSchemaJson: undefined,
48+ inputSchema: undefined,
49+ outputSchema: undefined,
5150 };
5251 };
5352
@@ -162,7 +161,6 @@ export function createToolCatalogFromTools(input: { · 108 unmodified lines
162161
163162 export function createToolCatalogFromEntries(input: {
164163 entries: ReadonlyArray<ToolCatalogEntry>;
165− getSchemaBundle?: (input: { id: string }) => Effect.Effect<ToolSchemaBundle | null, unknown>;
166164 }): ToolCatalog {
167165 const entries = [...input.entries];
168166 const byPath = new Map(
@@ -213,10 +211,6 @@ export function createToolCatalogFromEntries(input: { · 44 unmodified lines
213211 })
214212 : null,
215213 ),
216− getSchemaBundle: ({ id }) =>
217− input.getSchemaBundle
218− ? input.getSchemaBundle({ id })
219− : Effect.succeed(null),
220214 searchTools: ({ query, namespace, limit }) => {
221215 const queryTokens = tokenize(query);
222216
@@ -283,18 +277,6 @@ export function mergeToolCatalogs(input: { · 60 unmodified lines
283277 return null;
284278 }),
285279
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−
298280 searchTools: ({ query, namespace, limit }) =>
299281 Effect.gen(function* () {
300282 const groups = yield* Effect.forEach(
@@ -357,7 +339,6 @@ export function createDiscoveryPrimitivesFromToolCatalog(input: { · 56 unmodified lines
357339 const describe: DescribePrimitive = {
358340 tool: ({ path, includeSchemas = false }) =>
359341 catalog.getToolByPath({ path, includeSchemas }),
360− schemaBundle: ({ id }) => catalog.getSchemaBundle({ id }),
361342 };
362343
363344 const discover: DiscoverPrimitive = ({
@@ -406,9 +387,8 @@ export function createDiscoveryPrimitivesFromToolCatalog(input: { · 42 unmodified lines
406387 outputType: descriptor.outputType,
407388 ...(includeSchemas
408389 ? {
409− inputSchemaJson: descriptor.inputSchemaJson,
410− outputSchemaJson: descriptor.outputSchemaJson,
411− schemaBundleId: descriptor.schemaBundleId,
390+ inputSchema: descriptor.inputSchema,
391+ outputSchema: descriptor.outputSchema,
412392 }
413393 : {}),
414394 };
@@ -471,7 +451,7 @@ export function buildDynamicExecuteDescription(input: { · 56 unmodified lines
471451 "Workflow:",
472452 '1) const matches = await tools.discover({ query: "<intent>", limit: 12 });',
473453 "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.",
475455 "4) Call selected tools.<path>(input).",
476456 "Do not use fetch; use tools.* only.",
477457 ].join("\n");
packages/codemode-core/src/index.test.ts+5−20
@@ -326,13 +326,12 @@ describe("codemode-core", () => { · 325 unmodified lines
326326 .slice(0, limit)
327327 .map((descriptor) => ({
328328 ...descriptor,
329− inputSchemaJson: includeSchemas ? descriptor.inputSchemaJson : undefined,
330− outputSchemaJson: includeSchemas ? descriptor.outputSchemaJson : undefined,
329+ inputSchema: includeSchemas ? descriptor.inputSchema : undefined,
330+ outputSchema: includeSchemas ? descriptor.outputSchema : undefined,
331331 })),
332332 ),
333333 getToolByPath: ({ path }) =>
334334 Effect.succeed(descriptors[path] ?? null),
335− getSchemaBundle: () => Effect.succeed(null),
336335 searchTools: () =>
337336 Effect.succeed([
338337 { path: asToolPath("source.issues.create"), score: 0.93 },
@@ -369,7 +368,7 @@ describe("codemode-core", () => { · 30 unmodified lines
369368 description: "Get repository details",
370369 inputType: "{ owner: string; repo: string }",
371370 outputType: "Repository",
372− inputSchemaJson: '{"type":"object"}',
371+ inputSchema: { type: "object" },
373372 },
374373 },
375374 {
@@ -388,17 +387,6 @@ describe("codemode-core", () => { · 12 unmodified lines
388387 },
389388 },
390389 ],
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− ),
402390 });
403391
404392 const namespaces = yield* catalog.listNamespaces({ limit: 10 });
@@ -414,7 +402,7 @@ describe("codemode-core", () => { · 9 unmodified lines
414402 includeSchemas: false,
415403 });
416404 expect(listed[0]?.path).toBe("github.repos.getRepo");
417− expect(listed[0]?.inputSchemaJson).toBeUndefined();
405+ expect(listed[0]?.inputSchema).toBeUndefined();
418406
419407 const discovered = yield* catalog.searchTools({
420408 query: "repository issues",
@@ -426,10 +414,7 @@ describe("codemode-core", () => { · 5 unmodified lines
426414 path: asToolPath("github.repos.getRepo"),
427415 includeSchemas: true,
428416 });
429− expect(described?.inputSchemaJson).toContain('"type"');
430−
431− const bundle = yield* catalog.getSchemaBundle({ id: "bundle_1" });
432− expect(bundle?.id).toBe("bundle_1");
417+ expect(described?.inputSchema).toEqual({ type: "object" });
433418 }),
434419 );
435420
packages/codemode-core/src/schema-types.ts+16−0
@@ -123,3 +123,19 @@ export const typeSignatureFromSchemaJson = ( · 122 unmodified lines
123123 return fallback;
124124 }
125125 };
126+
127+export const typeSignatureFromSchema = (
128+ schema: unknown,
129+ fallback: string,
130+ maxLength: number = 220,
131+): string => {
132+ if (schema === undefined) {
133+ return fallback;
134+ }
135+
136+ try {
137+ return schemaToTypeSignature(schema, maxLength);
138+ } catch {
139+ return fallback;
140+ }
141+};
packages/codemode-core/src/system-tools.ts+0−29
@@ -6,7 +6,6 @@ import { toTool } from "./tool-map"; · 5 unmodified lines
66 import {
77 ToolDescriptorSchema,
88 ToolNamespaceSchema,
9− ToolSchemaBundleSchema,
109 type ToolCatalog,
1110 type ToolMap,
1211 type ToolPath,
@@ -52,16 +51,6 @@ const describeToolOutputSchema = Schema.standardSchemaV1( · 39 unmodified lines
5251 Schema.NullOr(ToolDescriptorSchema),
5352 );
5453
55−const describeSchemaBundleInputSchema = Schema.standardSchemaV1(
56− Schema.Struct({
57− id: Schema.String,
58− }),
59−);
60−
61−const describeSchemaBundleOutputSchema = Schema.standardSchemaV1(
62− Schema.NullOr(ToolSchemaBundleSchema),
63−);
64−
6554 const discoverInputSchema = Schema.standardSchemaV1(
6655 Schema.Struct({
6756 query: Schema.String,
@@ -181,24 +170,6 @@ export const createSystemToolMap = ( · 113 unmodified lines
181170 },
182171 });
183172
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−
202173 tools.discover = toTool({
203174 tool: {
204175 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
22 import * as Data from "effect/Data";
33 import * as Effect from "effect/Effect";
44 import * as JSONSchema from "effect/JSONSchema";
5−import { typeSignatureFromSchemaJson } from "./schema-types";
5+import { typeSignatureFromSchema } from "./schema-types";
66
77 import type {
88 ElicitationRequest,
@@ -336,7 +336,7 @@ export const toExecutorTool = wrapTool; · 327 unmodified lines
336336 const isToolDefinition = (value: ToolInput): value is ToolDefinition =>
337337 typeof value === "object" && value !== null && "tool" in value;
338338
339−const stringifySchema = (value: unknown): string | undefined => {
339+const deriveSchemaValue = (value: unknown): unknown | undefined => {
340340 if (value === undefined || value === null) {
341341 return undefined;
342342 }
@@ -347,20 +347,20 @@ const stringifySchema = (value: unknown): string | undefined => { · 4 unmodified lines
347347 && value !== null
348348 && "~standard" in value
349349 ) {
350− return JSON.stringify(JSONSchema.make(value as any));
350+ return JSONSchema.make(value as any);
351351 }
352352
353− return JSON.stringify(value);
353+ return value;
354354 } catch {
355355 return undefined;
356356 }
357357 };
358358
359−const inferTypeFromSchemaJson = (
360− schemaJson: string | undefined,
359+const inferTypeFromSchemaValue = (
360+ schema: unknown,
361361 fallback: string,
362362 maxLength: number = 240,
363−): string => typeSignatureFromSchemaJson(schemaJson, fallback, maxLength);
363+): string => typeSignatureFromSchema(schema, fallback, maxLength);
364364
365365 export function createToolsFromRecord(input: {
366366 tools: Record<string, ExecutableTool>;
@@ -418,13 +418,13 @@ export function toolDescriptorsFromTools(input: { · 51 unmodified lines
418418 return resolvedTools.map((entry) => {
419419 const metadata = entry.metadata;
420420 const definition = entry.tool;
421− const inputSchemaJson =
422− metadata?.inputSchemaJson
423− ?? stringifySchema(definition.inputSchema)
424− ?? stringifySchema(definition.parameters);
425− const outputSchemaJson =
426− metadata?.outputSchemaJson
427− ?? stringifySchema(definition.outputSchema);
421+ const inputSchema =
422+ metadata?.inputSchema
423+ ?? deriveSchemaValue(definition.inputSchema)
424+ ?? deriveSchemaValue(definition.parameters);
425+ const outputSchema =
426+ metadata?.outputSchema
427+ ?? deriveSchemaValue(definition.outputSchema);
428428
429429 return {
430430 path: entry.path,
@@ -433,21 +433,20 @@ export function toolDescriptorsFromTools(input: { · 2 unmodified lines
433433 interaction: metadata?.interaction,
434434 elicitation: metadata?.elicitation,
435435 inputType:
436− metadata?.inputType ?? inferTypeFromSchemaJson(inputSchemaJson, "unknown"),
436+ metadata?.inputType ?? inferTypeFromSchemaValue(inputSchema, "unknown"),
437437 outputType:
438− metadata?.outputType ?? inferTypeFromSchemaJson(outputSchemaJson, "unknown"),
439− inputSchemaJson,
440− outputSchemaJson,
441− ...(metadata?.schemaBundleId ? { schemaBundleId: metadata.schemaBundleId } : {}),
442− ...(metadata?.exampleInputJson
443− ? { exampleInputJson: metadata.exampleInputJson }
438+ metadata?.outputType ?? inferTypeFromSchemaValue(outputSchema, "unknown"),
439+ ...(inputSchema !== undefined ? { inputSchema } : {}),
440+ ...(outputSchema !== undefined ? { outputSchema } : {}),
441+ ...(metadata?.exampleInput !== undefined
442+ ? { exampleInput: metadata.exampleInput }
444443 : {}),
445− ...(metadata?.exampleOutputJson
446− ? { exampleOutputJson: metadata.exampleOutputJson }
444+ ...(metadata?.exampleOutput !== undefined
445+ ? { exampleOutput: metadata.exampleOutput }
447446 : {}),
448447 ...(metadata?.providerKind ? { providerKind: metadata.providerKind } : {}),
449− ...(metadata?.providerDataJson
450− ? { providerDataJson: metadata.providerDataJson }
448+ ...(metadata?.providerData !== undefined
449+ ? { providerData: metadata.providerData }
451450 : {}),
452451 } satisfies ToolDescriptor;
453452 });
packages/codemode-core/src/types.ts+15−39
@@ -80,14 +80,13 @@ export type ToolMetadata = { · 79 unmodified lines
8080 elicitation?: ElicitationRequest;
8181 inputType?: string;
8282 outputType?: string;
83− inputSchemaJson?: string;
84− outputSchemaJson?: string;
85− schemaBundleId?: string;
86− exampleInputJson?: string;
87− exampleOutputJson?: string;
83+ inputSchema?: unknown;
84+ outputSchema?: unknown;
85+ exampleInput?: unknown;
86+ exampleOutput?: unknown;
8887 sourceKey?: string;
8988 providerKind?: string;
90− providerDataJson?: string;
89+ providerData?: unknown;
9190 };
9291
9392 export type ToolExecutionContext = {
@@ -134,13 +133,12 @@ export type ToolDescriptor = { · 40 unmodified lines
134133 elicitation?: ElicitationRequest;
135134 inputType?: string;
136135 outputType?: string;
137− inputSchemaJson?: string;
138− outputSchemaJson?: string;
139− schemaBundleId?: string;
140− exampleInputJson?: string;
141− exampleOutputJson?: string;
136+ inputSchema?: unknown;
137+ outputSchema?: unknown;
138+ exampleInput?: unknown;
139+ exampleOutput?: unknown;
142140 providerKind?: string;
143− providerDataJson?: string;
141+ providerData?: unknown;
144142 };
145143
146144 export const ToolDescriptorSchema = Schema.Struct({
@@ -153,27 +151,12 @@ export const ToolDescriptorSchema = Schema.Struct({ · 6 unmodified lines
153151 elicitation: Schema.optional(Schema.Unknown),
154152 inputType: Schema.optional(Schema.String),
155153 outputType: Schema.optional(Schema.String),
156− inputSchemaJson: Schema.optional(Schema.String),
157− outputSchemaJson: Schema.optional(Schema.String),
158− schemaBundleId: Schema.optional(Schema.String),
159− exampleInputJson: Schema.optional(Schema.String),
160− exampleOutputJson: Schema.optional(Schema.String),
154+ inputSchema: Schema.optional(Schema.Unknown),
155+ outputSchema: Schema.optional(Schema.Unknown),
156+ exampleInput: Schema.optional(Schema.Unknown),
157+ exampleOutput: Schema.optional(Schema.Unknown),
161158 providerKind: Schema.optional(Schema.String),
162− providerDataJson: Schema.optional(Schema.String),
163−});
164−
165−export type ToolSchemaBundle = {
166− id: string;
167− kind: string;
168− hash: string;
169− refsJson: string;
170−};
171−
172−export const ToolSchemaBundleSchema = Schema.Struct({
173− id: Schema.String,
174− kind: Schema.String,
175− hash: Schema.String,
176− refsJson: Schema.String,
159+ providerData: Schema.optional(Schema.Unknown),
177160 });
178161
179162 export type ToolNamespace = {
@@ -217,10 +200,6 @@ export interface ToolCatalog { · 37 unmodified lines
217200 includeSchemas: boolean;
218201 }): Effect.Effect<ToolDescriptor | null, unknown>;
219202
220− getSchemaBundle(input: {
221− id: string;
222− }): Effect.Effect<ToolSchemaBundle | null, unknown>;
223−
224203 searchTools(input: {
225204 query: string;
226205 namespace?: string;
@@ -249,9 +228,6 @@ export type DescribePrimitive = { · 22 unmodified lines
249228 path: ToolPath;
250229 includeSchemas?: boolean;
251230 }): Effect.Effect<ToolDescriptor | null, unknown>;
252− schemaBundle(input: {
253− id: string;
254− }): Effect.Effect<ToolSchemaBundle | null, unknown>;
255231 };
256232
257233 export 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
9090 })
9191 : [];
9292
93+const parseJson = (value: string | undefined): unknown | undefined => {
94+ if (!value) {
95+ return undefined;
96+ }
97+
98+ try {
99+ return JSON.parse(value) as unknown;
100+ } catch {
101+ return undefined;
102+ }
103+};
104+
93105 const contentHash = (value: string): string =>
94106 sha256Hex(value);
95107
@@ -384,17 +396,29 @@ const manifestMethodFromDiscoveryMethod = (input: { · 288 unmodified lines
384396 ...(methodRequestSchemaJson({
385397 method: input.method,
386398 topLevelSchemas: input.topLevelSchemas,
387− }) ? { inputSchemaJson: methodRequestSchemaJson({
388− method: input.method,
389− topLevelSchemas: input.topLevelSchemas,
390− }) } : {}),
399+ })
400+ ? {
401+ inputSchema: parseJson(
402+ methodRequestSchemaJson({
403+ method: input.method,
404+ topLevelSchemas: input.topLevelSchemas,
405+ }),
406+ ),
407+ }
408+ : {}),
391409 ...(methodResponseSchemaJson({
392410 method: input.method,
393411 topLevelSchemas: input.topLevelSchemas,
394− }) ? { outputSchemaJson: methodResponseSchemaJson({
395− method: input.method,
396− topLevelSchemas: input.topLevelSchemas,
397− }) } : {}),
412+ })
413+ ? {
414+ outputSchema: parseJson(
415+ methodResponseSchemaJson({
416+ method: input.method,
417+ topLevelSchemas: input.topLevelSchemas,
418+ }),
419+ ),
420+ }
421+ : {}),
398422 };
399423 };
400424
packages/codemode-google-discovery/src/google-discovery-tools.test.ts+2−2
@@ -117,8 +117,8 @@ describe("google discovery tools", () => { · 116 unmodified lines
117117 expect(definitions.length).toBeGreaterThan(10);
118118 expect(valuesGet).toBeDefined();
119119 expect(valuesGet?.method).toBe("get");
120− expect(valuesGet?.inputSchemaJson).toContain("\"spreadsheetId\"");
121− expect(valuesGet?.outputSchemaJson).toContain("\"values\"");
120+ expect(JSON.stringify(valuesGet?.inputSchema)).toContain("\"spreadsheetId\"");
121+ expect(JSON.stringify(valuesGet?.outputSchema)).toContain("\"values\"");
122122 expect(Object.keys(manifest.schemaRefTable ?? {})).toContain(
123123 "#/$defs/google/ValueRange",
124124 );
packages/codemode-google-discovery/src/google-discovery-tools.ts+19−41
@@ -27,10 +27,10 @@ import { · 26 unmodified lines
2727 } from "./google-discovery-types";
2828
2929 const decodeGoogleDiscoveryToolProviderDataJson = Schema.decodeUnknownEither(
30− Schema.parseJson(GoogleDiscoveryToolProviderDataSchema),
30+ GoogleDiscoveryToolProviderDataSchema,
3131 );
3232
33−export const googleDiscoveryProviderDataJsonFromDefinition = (
33+export const googleDiscoveryProviderDataFromDefinition = (
3434 input: {
3535 service: string;
3636 version: string;
@@ -38,8 +38,7 @@ export const googleDiscoveryProviderDataJsonFromDefinition = ( · 1 unmodified line
3838 servicePath: string;
3939 definition: GoogleDiscoveryManifestMethod;
4040 },
41−): string =>
42− JSON.stringify({
41+): typeof GoogleDiscoveryToolProviderDataSchema.Type => ({
4342 kind: "google_discovery",
4443 service: input.service,
4544 version: input.version,
@@ -115,19 +114,14 @@ const setNestedSchemaProperty = ( · 69 unmodified lines
115114 };
116115
117116 const materializeSchemaWithRefDefinitions = (input: {
118− schemaJson: string | undefined;
117+ schema: unknown;
119118 refTable?: Readonly<Record<string, unknown>>;
120119 }): Record<string, unknown> => {
121− if (!input.schemaJson) {
120+ if (input.schema === undefined || input.schema === null) {
122121 return {};
123122 }
124123
125− let rootSchema: Record<string, unknown>;
126− try {
127− rootSchema = asRecord(JSON.parse(input.schemaJson) as unknown);
128− } catch {
129− return {};
130− }
124+ const rootSchema = asRecord(input.schema);
131125
132126 if (!input.refTable || Object.keys(input.refTable).length === 0) {
133127 return rootSchema;
@@ -250,14 +244,14 @@ const buildGoogleDiscoveryRequest = (input: { · 116 unmodified lines
250244 defaultHeaders: Readonly<Record<string, string>>;
251245 credentialPlacements: HttpRequestPlacements;
252246 baseUrl?: string | undefined;
253− providerDataJson: string;
247+ providerData: typeof GoogleDiscoveryToolProviderDataSchema.Type;
254248 }): {
255249 method: string;
256250 url: URL;
257251 headers: Record<string, string>;
258252 body?: string;
259253 } => {
260− const providerData = decodeGoogleDiscoveryToolProviderDataJson(input.providerDataJson);
254+ const providerData = decodeGoogleDiscoveryToolProviderDataJson(input.providerData);
261255 if (providerData._tag === "Left") {
262256 throw new Error("Invalid Google Discovery provider data");
263257 }
@@ -365,28 +359,12 @@ export type CreateGoogleDiscoveryToolFromDefinitionInput = { · 101 unmodified lines
365359 export const createGoogleDiscoveryToolFromDefinition = (
366360 input: CreateGoogleDiscoveryToolFromDefinitionInput,
367361 ) => {
368− const providerDataJson = JSON.stringify({
369− kind: "google_discovery",
362+ const providerData = googleDiscoveryProviderDataFromDefinition({
370363 service: input.service,
371364 version: input.version,
372− toolId: input.definition.toolId,
373− rawToolId: input.definition.rawToolId,
374− methodId: input.definition.methodId,
375− group: input.definition.group,
376− leaf: input.definition.leaf,
377− invocation: {
378− method: input.definition.method,
379− path: input.definition.path,
380− flatPath: input.definition.flatPath,
381− rootUrl: input.rootUrl,
382− servicePath: input.servicePath,
383− parameters: input.definition.parameters,
384− requestSchemaId: input.definition.requestSchemaId,
385− responseSchemaId: input.definition.responseSchemaId,
386− scopes: input.definition.scopes,
387− supportsMediaUpload: input.definition.supportsMediaUpload,
388− supportsMediaDownload: input.definition.supportsMediaDownload,
389− },
365+ rootUrl: input.rootUrl,
366+ servicePath: input.servicePath,
367+ definition: input.definition,
390368 });
391369 const credentialPlacements = normalizeCredentialPlacements({
392370 credentialHeaders: input.credentialHeaders,
@@ -403,7 +381,7 @@ export const createGoogleDiscoveryToolFromDefinition = ( · 10 unmodified lines
403381 }),
404382 );
405383 const inputSchema = materializeSchemaWithRefDefinitions({
406− schemaJson: input.definition.inputSchemaJson,
384+ schema: input.definition.inputSchema,
407385 refTable,
408386 });
409387
@@ -411,15 +389,15 @@ export const createGoogleDiscoveryToolFromDefinition = ( · 1 unmodified line
411389 interaction: input.definition.method === "get" || input.definition.method === "head"
412390 ? "auto"
413391 : "required",
414− ...(input.definition.inputSchemaJson
415− ? { inputSchemaJson: input.definition.inputSchemaJson }
392+ ...(input.definition.inputSchema !== undefined
393+ ? { inputSchema: input.definition.inputSchema }
416394 : {}),
417− ...(input.definition.outputSchemaJson
418− ? { outputSchemaJson: input.definition.outputSchemaJson }
395+ ...(input.definition.outputSchema !== undefined
396+ ? { outputSchema: input.definition.outputSchema }
419397 : {}),
420398 sourceKey: input.sourceKey,
421399 providerKind: "google_discovery",
422− providerDataJson,
400+ providerData,
423401 };
424402
425403 return toTool({
@@ -435,7 +413,7 @@ export const createGoogleDiscoveryToolFromDefinition = ( · 9 unmodified lines
435413 defaultHeaders: input.defaultHeaders ?? {},
436414 credentialPlacements,
437415 baseUrl: input.baseUrl,
438− providerDataJson,
416+ providerData,
439417 });
440418 let clientRequest = HttpClientRequest.make(
441419 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
7474 scopes: Schema.Array(Schema.String),
7575 supportsMediaUpload: Schema.Boolean,
7676 supportsMediaDownload: Schema.Boolean,
77− inputSchemaJson: Schema.optional(Schema.String),
78− outputSchemaJson: Schema.optional(Schema.String),
77+ inputSchema: Schema.optional(Schema.Unknown),
78+ outputSchema: Schema.optional(Schema.Unknown),
7979 });
8080
8181 export const GoogleDiscoverySchemaRefTableSchema = Schema.Record({
packages/codemode-google-discovery/src/index.ts+1−1
@@ -5,7 +5,7 @@ export { · 4 unmodified lines
55 export {
66 createGoogleDiscoveryToolFromDefinition,
77 decodeGoogleDiscoverySchemaRefTableJson,
8− googleDiscoveryProviderDataJsonFromDefinition,
8+ googleDiscoveryProviderDataFromDefinition,
99 type CreateGoogleDiscoveryToolFromDefinitionInput,
1010 } from "./google-discovery-tools";
1111 export {
packages/codemode-mcp/src/mcp-manifest.ts+4−18
@@ -7,8 +7,8 @@ export type McpToolManifestEntry = { · 6 unmodified lines
77 toolId: string;
88 toolName: string;
99 description: string | null;
10− inputSchemaJson?: string;
11− outputSchemaJson?: string;
10+ inputSchema?: unknown;
11+ outputSchema?: unknown;
1212 };
1313
1414 export type McpToolManifest = {
@@ -34,18 +34,6 @@ const uniqueToolId = (value: string, byBase: Map<string, number>): string => { · 19 unmodified lines
3434 return count === 1 ? base : `${base}_${count}`;
3535 };
3636
37−const stringifyJson = (value: unknown): string | undefined => {
38− if (value === undefined || value === null) {
39− return undefined;
40− }
41−
42− try {
43− return JSON.stringify(value);
44− } catch {
45− return undefined;
46− }
47−};
48−
4937 const ListedMcpToolSchema = Schema.Struct({
5038 name: Schema.String,
5139 description: Schema.optional(Schema.NullOr(Schema.String)),
@@ -85,10 +73,8 @@ export const extractMcpToolManifestFromListToolsResult = ( · 33 unmodified lines
8573 toolId: uniqueToolId(toolName, byBase),
8674 toolName,
8775 description: tool.description ?? null,
88− inputSchemaJson:
89− stringifyJson(tool.inputSchema)
90− ?? stringifyJson(tool.parameters),
91− outputSchemaJson: stringifyJson(tool.outputSchema),
76+ inputSchema: tool.inputSchema ?? tool.parameters,
77+ outputSchema: tool.outputSchema,
9278 };
9379 })
9480 .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
230230
231231 const toolDefinition = resolveToolDefinition(discovered.tools["source.mcp.echo"]!);
232232 expect(toolDefinition.metadata?.sourceKey).toBe("mcp.demo");
233− expect(toolDefinition.metadata?.inputSchemaJson).toContain("value");
233+ expect(JSON.stringify(toolDefinition.metadata?.inputSchema)).toContain("value");
234234
235235 const invoke = resolveToolExecutor(discovered.tools, "source.mcp.echo");
236236 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
9393 const toDetails = (cause: unknown): string =>
9494 cause instanceof Error ? cause.message : String(cause);
9595
96−const inputSchemaFromManifest = (inputSchemaJson: string | undefined) => {
97− if (!inputSchemaJson) {
96+const inputSchemaFromManifest = (inputSchema: unknown) => {
97+ if (inputSchema === undefined || inputSchema === null) {
9898 return unknownInputSchema;
9999 }
100100
101101 try {
102− return standardSchemaFromJsonSchema(JSON.parse(inputSchemaJson), {
102+ return standardSchemaFromJsonSchema(inputSchema, {
103103 vendor: "mcp",
104104 fallback: unknownInputSchema,
105105 });
@@ -470,7 +470,7 @@ export const createMcpToolsFromManifest = (input: { · 364 unmodified lines
470470 toTool({
471471 tool: {
472472 description: entry.description ?? `MCP tool: ${entry.toolName}`,
473− inputSchema: inputSchemaFromManifest(entry.inputSchemaJson),
473+ inputSchema: inputSchemaFromManifest(entry.inputSchema),
474474 execute: async (args: unknown, executionContext?: ToolExecutionContext) => {
475475 const exit = await Effect.runPromiseExit(
476476 withConnectionEffect({
@@ -504,8 +504,12 @@ export const createMcpToolsFromManifest = (input: { · 27 unmodified lines
504504 },
505505 metadata: {
506506 sourceKey,
507− inputSchemaJson: entry.inputSchemaJson,
508− outputSchemaJson: entry.outputSchemaJson,
507+ ...(entry.inputSchema !== undefined
508+ ? { inputSchema: entry.inputSchema }
509+ : {}),
510+ ...(entry.outputSchema !== undefined
511+ ? { outputSchema: entry.outputSchema }
512+ : {}),
509513 },
510514 }),
511515 ] as const;
packages/codemode-openapi/src/index.ts+3−3
@@ -8,13 +8,13 @@ export { · 7 unmodified lines
88 } from "./openapi-extraction";
99 export {
1010 compileOpenApiToolDefinitions,
11− openApiProviderDataJsonFromDefinition,
11+ openApiProviderDataFromDefinition,
1212 OpenApiToolDefinitionSchema,
1313 type OpenApiToolDefinition,
1414 } from "./openapi-definitions";
1515 export {
1616 buildOpenApiToolPresentation,
17− openApiOutputTypeSignatureFromSchemaJson,
17+ openApiOutputTypeSignatureFromSchema,
1818 type OpenApiToolPresentation,
1919 } from "./openapi-tool-presentation";
2020 export {
@@ -23,7 +23,7 @@ export { · 2 unmodified lines
2323 createOpenApiToolsFromManifest,
2424 createOpenApiToolsFromSpec,
2525 } from "./openapi-tools";
26−export { resolveSchemaJsonWithRefHints, resolveTypingSchemasWithRefHints } from "./openapi-schema-refs";
26+export { resolveSchemaWithRefHints, resolveTypingSchemasWithRefHints } from "./openapi-schema-refs";
2727 export {
2828 OPEN_API_HTTP_METHODS,
2929 OPEN_API_PARAMETER_LOCATIONS,
packages/codemode-openapi/src/openapi-definitions.ts+2−3
@@ -236,10 +236,9 @@ export const compileOpenApiToolDefinitions = ( · 235 unmodified lines
236236 );
237237 };
238238
239−export const openApiProviderDataJsonFromDefinition = (
239+export const openApiProviderDataFromDefinition = (
240240 definition: OpenApiToolDefinition,
241−): string =>
242− JSON.stringify({
241+): typeof OpenApiToolProviderDataSchema.Type => ({
243242 kind: "openapi",
244243 toolId: definition.toolId,
245244 rawToolId: definition.rawToolId,
packages/codemode-openapi/src/openapi-extraction.real-specs.test.ts+3−3
@@ -26,8 +26,8 @@ type ParsedInputSchema = { · 25 unmodified lines
2626 required?: Array<string>;
2727 };
2828
29−const parseInputSchema = (inputSchemaJson: string | undefined): ParsedInputSchema | null =>
30− inputSchemaJson ? (JSON.parse(inputSchemaJson) as ParsedInputSchema) : null;
29+const parseInputSchema = (inputSchema: unknown): ParsedInputSchema | null =>
30+ inputSchema ? (inputSchema as ParsedInputSchema) : null;
3131
3232 const expectedTools = [
3333 {
@@ -89,7 +89,7 @@ describe("openapi-extraction real specs", () => { · 55 unmodified lines
8989 expect(tool?.path).toBe(expectedTool.path);
9090 expect(tool?.tags).toEqual(expectedTool.tags);
9191
92− const inputSchema = parseInputSchema(tool?.typing?.inputSchemaJson);
92+ const inputSchema = parseInputSchema(tool?.typing?.inputSchema);
9393 expect(inputSchema?.type).toBe("object");
9494 expect(Object.keys(inputSchema?.properties ?? {})).toEqual(
9595 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
22 type RefHintValue = string | JsonObject;
33
44 type DiscoveryTypingLike = {
5− inputSchemaJson?: string;
6− outputSchemaJson?: string;
5+ inputSchema?: unknown;
6+ outputSchema?: unknown;
77 };
88
99 const isJsonObject = (value: unknown): value is JsonObject =>
@@ -92,50 +92,40 @@ const resolveSchemaNode = ( · 82 unmodified lines
9292 return next;
9393 };
9494
95−export const resolveSchemaJsonWithRefHints = (
96− schemaJson: string | undefined,
95+export const resolveSchemaWithRefHints = (
96+ schema: unknown,
9797 refHintTable: Readonly<Record<string, RefHintValue>> | undefined,
98−): string | null => {
99− if (!schemaJson) {
98+): unknown | null => {
99+ if (schema === undefined || schema === null) {
100100 return null;
101101 }
102102
103− const parsedSchema = parseJson(schemaJson);
104− if (parsedSchema === null) {
105− return schemaJson;
106− }
107−
108103 if (!refHintTable || Object.keys(refHintTable).length === 0) {
109− return schemaJson;
104+ return schema;
110105 }
111106
112107 const resolved = resolveSchemaNode(
113− parsedSchema,
108+ schema,
114109 refHintTable,
115110 new Map<string, unknown>(),
116111 new Set<string>(),
117112 );
118−
119− try {
120− return JSON.stringify(resolved);
121− } catch {
122− return schemaJson;
123− }
113+ return resolved;
124114 };
125115
126116 export const resolveTypingSchemasWithRefHints = (
127117 typing: DiscoveryTypingLike | undefined,
128118 refHintTable: Readonly<Record<string, RefHintValue>> | undefined,
129119 ): {
130− inputSchemaJson: string | null;
131− outputSchemaJson: string | null;
120+ inputSchema: unknown | null;
121+ outputSchema: unknown | null;
132122 } => ({
133− inputSchemaJson: resolveSchemaJsonWithRefHints(
134− typing?.inputSchemaJson,
123+ inputSchema: resolveSchemaWithRefHints(
124+ typing?.inputSchema,
135125 refHintTable,
136126 ),
137− outputSchemaJson: resolveSchemaJsonWithRefHints(
138− typing?.outputSchemaJson,
127+ outputSchema: resolveSchemaWithRefHints(
128+ typing?.outputSchema,
139129 refHintTable,
140130 ),
141131 });
packages/codemode-openapi/src/openapi-tool-presentation.ts+71−49
@@ -1,22 +1,16 @@
1−import { typeSignatureFromSchemaJson } from "@executor/codemode-core";
1+import { typeSignatureFromSchema } from "@executor/codemode-core";
22
33 import type {
44 OpenApiExample,
5+ OpenApiInvocationPayload,
56 OpenApiToolDocumentation,
7+ OpenApiToolProviderData,
68 } from "./openapi-types";
79 import {
8− openApiProviderDataJsonFromDefinition,
10+ openApiProviderDataFromDefinition,
911 type OpenApiToolDefinition,
1012 } from "./openapi-definitions";
1113
12−const parseJson = (value: string): unknown | undefined => {
13− try {
14− return JSON.parse(value) as unknown;
15− } catch {
16− return undefined;
17− }
18−};
19−
2014 const asRecord = (value: unknown): Record<string, unknown> =>
2115 typeof value === "object" && value !== null && !Array.isArray(value)
2216 ? (value as Record<string, unknown>)
@@ -32,33 +26,62 @@ const isStrictEmptyObjectSchema = (value: unknown): boolean => { · 9 unmodified lines
3226 return Object.keys(properties).length === 0 && schema.additionalProperties === false;
3327 };
3428
35−export const openApiOutputTypeSignatureFromSchemaJson = (
36− schemaJson: string | undefined,
29+export const openApiOutputTypeSignatureFromSchema = (
30+ schema: unknown,
3731 maxLength: number = 320,
3832 ): string => {
39− if (!schemaJson) {
33+ if (schema === undefined || schema === null) {
4034 return "void";
4135 }
4236
43− const parsed = parseJson(schemaJson);
44− if (parsed === undefined) {
45− return "unknown";
46− }
47−
48− if (isStrictEmptyObjectSchema(parsed)) {
37+ if (isStrictEmptyObjectSchema(schema)) {
4938 return "{}";
5039 }
5140
52− return typeSignatureFromSchemaJson(schemaJson, "unknown", maxLength);
41+ return typeSignatureFromSchema(schema, "unknown", maxLength);
5342 };
5443
5544 const firstExample = (
5645 examples: ReadonlyArray<OpenApiExample> | undefined,
5746 ): OpenApiExample | undefined => examples?.[0];
5847
59−const buildExampleInputJson = (
48+const fallbackInputSchemaFromInvocation = (
49+ invocation: OpenApiInvocationPayload,
50+): Record<string, unknown> | undefined => {
51+ const properties: Record<string, unknown> = {};
52+ const required: string[] = [];
53+
54+ for (const parameter of invocation.parameters) {
55+ properties[parameter.name] = {
56+ type: "string",
57+ };
58+ if (parameter.required) {
59+ required.push(parameter.name);
60+ }
61+ }
62+
63+ if (invocation.requestBody) {
64+ properties.body = {
65+ type: "object",
66+ };
67+ if (invocation.requestBody.required) {
68+ required.push("body");
69+ }
70+ }
71+
72+ return Object.keys(properties).length > 0
73+ ? {
74+ type: "object",
75+ properties,
76+ ...(required.length > 0 ? { required } : {}),
77+ additionalProperties: false,
78+ }
79+ : undefined;
80+};
81+
82+const buildExampleInput = (
6083 documentation: OpenApiToolDocumentation | undefined,
61−): string | undefined => {
84+): Record<string, unknown> | undefined => {
6285 if (!documentation) {
6386 return undefined;
6487 }
@@ -71,52 +94,51 @@ const buildExampleInputJson = ( · 6 unmodified lines
7194 continue;
7295 }
7396
74− const parsedValue = parseJson(example.valueJson);
75− if (parsedValue !== undefined) {
76− input[parameter.name] = parsedValue;
77− }
97+ input[parameter.name] = JSON.parse(example.valueJson) as unknown;
7898 }
7999
80100 const requestBodyExample = firstExample(documentation.requestBody?.examples);
81101 if (requestBodyExample) {
82− const parsedValue = parseJson(requestBodyExample.valueJson);
83− if (parsedValue !== undefined) {
84− input.body = parsedValue;
85− }
102+ input.body = JSON.parse(requestBodyExample.valueJson) as unknown;
86103 }
87104
88− return Object.keys(input).length > 0 ? JSON.stringify(input) : undefined;
105+ return Object.keys(input).length > 0 ? input : undefined;
89106 };
90107
91−const buildExampleOutputJson = (
108+const buildExampleOutput = (
92109 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+};
94114
95115 export type OpenApiToolPresentation = {
96116 inputType: string;
97117 outputType: string;
98− inputSchemaJson?: string;
99− outputSchemaJson?: string;
100− exampleInputJson?: string;
101− exampleOutputJson?: string;
102− providerDataJson: string;
118+ inputSchema?: unknown;
119+ outputSchema?: unknown;
120+ exampleInput?: unknown;
121+ exampleOutput?: unknown;
122+ providerData: OpenApiToolProviderData;
103123 };
104124
105125 export const buildOpenApiToolPresentation = (input: {
106126 definition: OpenApiToolDefinition;
107127 }): OpenApiToolPresentation => {
108− const inputSchemaJson = input.definition.typing?.inputSchemaJson;
109− const outputSchemaJson = input.definition.typing?.outputSchemaJson;
110− const exampleInputJson = buildExampleInputJson(input.definition.documentation);
111− const exampleOutputJson = buildExampleOutputJson(input.definition.documentation);
128+ const inputSchema =
129+ input.definition.typing?.inputSchema
130+ ?? fallbackInputSchemaFromInvocation(input.definition.invocation);
131+ const outputSchema = input.definition.typing?.outputSchema;
132+ const exampleInput = buildExampleInput(input.definition.documentation);
133+ const exampleOutput = buildExampleOutput(input.definition.documentation);
112134
113135 return {
114− inputType: typeSignatureFromSchemaJson(inputSchemaJson, "unknown", Infinity),
115− outputType: openApiOutputTypeSignatureFromSchemaJson(outputSchemaJson, Infinity),
116− ...(inputSchemaJson ? { inputSchemaJson } : {}),
117− ...(outputSchemaJson ? { outputSchemaJson } : {}),
118− ...(exampleInputJson ? { exampleInputJson } : {}),
119− ...(exampleOutputJson ? { exampleOutputJson } : {}),
120− providerDataJson: openApiProviderDataJsonFromDefinition(input.definition),
136+ inputType: typeSignatureFromSchema(inputSchema, "unknown", Infinity),
137+ outputType: openApiOutputTypeSignatureFromSchema(outputSchema, Infinity),
138+ ...(inputSchema !== undefined ? { inputSchema } : {}),
139+ ...(outputSchema !== undefined ? { outputSchema } : {}),
140+ ...(exampleInput !== undefined ? { exampleInput } : {}),
141+ ...(exampleOutput !== undefined ? { exampleOutput } : {}),
142+ providerData: openApiProviderDataFromDefinition(input.definition),
121143 };
122144 };
packages/codemode-openapi/src/openapi-tools.test.ts+2−2
@@ -291,8 +291,8 @@ describe("openapi-tools", () => { · 290 unmodified lines
291291 const resolvedGet = resolveToolDefinition(tools["source.demo.repos.getRepo"]!);
292292 expect(resolvedGet.metadata?.sourceKey).toBe("api.demo");
293293 expect(resolvedGet.metadata?.providerKind).toBe("openapi");
294− expect(resolvedGet.metadata?.providerDataJson).toContain('"group":"repos"');
295− expect(resolvedGet.metadata?.inputSchemaJson).toBeDefined();
294+ expect(resolvedGet.metadata?.providerData).toMatchObject({ group: "repos" });
295+ expect(resolvedGet.metadata?.inputSchema).toBeDefined();
296296 }),
297297 );
298298
packages/codemode-openapi/src/openapi-tools.ts+23−15
@@ -23,7 +23,7 @@ import { · 22 unmodified lines
2323 type OpenApiToolDefinition,
2424 } from "./openapi-definitions";
2525 import { buildOpenApiToolPresentation } from "./openapi-tool-presentation";
26−import { resolveSchemaJsonWithRefHints } from "./openapi-schema-refs";
26+import { resolveSchemaWithRefHints } from "./openapi-schema-refs";
2727 import {
2828 extractOpenApiManifest,
2929 type OpenApiExtractionError,
@@ -262,21 +262,21 @@ const summarizeHttpResponseBody = (body: unknown): string | null => { · 232 unmodified lines
262262 }
263263 };
264264
265−const inputSchemaFromTypingJson = (input: {
266− inputSchemaJson: string | undefined;
265+const inputSchemaFromTyping = (input: {
266+ inputSchema: unknown;
267267 refHintTable?: Readonly<OpenApiRefHintTable>;
268268 }) => {
269− const resolvedSchemaJson = resolveSchemaJsonWithRefHints(
270− input.inputSchemaJson,
269+ const resolvedSchema = resolveSchemaWithRefHints(
270+ input.inputSchema,
271271 input.refHintTable,
272− ) ?? input.inputSchemaJson;
272+ ) ?? input.inputSchema;
273273
274− if (!resolvedSchemaJson) {
274+ if (resolvedSchema === undefined || resolvedSchema === null) {
275275 return unknownInputSchema;
276276 }
277277
278278 try {
279− return standardSchemaFromJsonSchema(JSON.parse(resolvedSchemaJson), {
279+ return standardSchemaFromJsonSchema(resolvedSchema, {
280280 vendor: "openapi",
281281 fallback: unknownInputSchema,
282282 });
@@ -469,8 +469,8 @@ export const createOpenApiToolFromDefinition = ( · 186 unmodified lines
469469 return toTool({
470470 tool: {
471471 description: input.definition.description,
472− inputSchema: inputSchemaFromTypingJson({
473− inputSchemaJson: presentation.inputSchemaJson ?? input.definition.typing?.inputSchemaJson,
472+ inputSchema: inputSchemaFromTyping({
473+ inputSchema: presentation.inputSchema ?? input.definition.typing?.inputSchema,
474474 refHintTable: input.refHintTable,
475475 }),
476476 execute: async (args: unknown) => {
@@ -526,12 +526,20 @@ export const createOpenApiToolFromDefinition = ( · 49 unmodified lines
526526 sourceKey: input.sourceKey,
527527 inputType: presentation.inputType,
528528 outputType: presentation.outputType,
529− inputSchemaJson: presentation.inputSchemaJson,
530− outputSchemaJson: presentation.outputSchemaJson,
531− exampleInputJson: presentation.exampleInputJson,
532− exampleOutputJson: presentation.exampleOutputJson,
529+ ...((presentation.inputSchema ?? input.definition.typing?.inputSchema) !== undefined
530+ ? { inputSchema: presentation.inputSchema ?? input.definition.typing?.inputSchema }
531+ : {}),
532+ ...((presentation.outputSchema ?? input.definition.typing?.outputSchema) !== undefined
533+ ? { outputSchema: presentation.outputSchema ?? input.definition.typing?.outputSchema }
534+ : {}),
535+ ...(presentation.exampleInput !== undefined
536+ ? { exampleInput: presentation.exampleInput }
537+ : {}),
538+ ...(presentation.exampleOutput !== undefined
539+ ? { exampleOutput: presentation.exampleOutput }
540+ : {}),
533541 providerKind: "openapi",
534− providerDataJson: presentation.providerDataJson,
542+ providerData: presentation.providerData,
535543 },
536544 });
537545 };
packages/codemode-openapi/src/openapi-types.ts+2−2
@@ -58,8 +58,8 @@ export const OpenApiInvocationPayloadSchema = Schema.Struct({ · 57 unmodified lines
5858 });
5959
6060 export const DiscoveryTypingPayloadSchema = Schema.Struct({
61− inputSchemaJson: Schema.optional(Schema.String),
62− outputSchemaJson: Schema.optional(Schema.String),
61+ inputSchema: Schema.optional(Schema.Unknown),
62+ outputSchema: Schema.optional(Schema.Unknown),
6363 refHintKeys: Schema.optional(Schema.Array(Schema.String)),
6464 });
6565
packages/control-plane/package.json+1−0
@@ -5,6 +5,7 @@ · 4 unmodified lines
55 "exports": {
66 ".": "./src/index.ts",
77 "./client": "./src/client.ts",
8+ "./ir": "./src/ir/index.ts",
89 "./schema": "./src/schema/index.ts"
910 },
1011 "imports": {
packages/control-plane/src/api/sources/api.ts+0−11
@@ -8,7 +8,6 @@ import { · 7 unmodified lines
88 SourceIdSchema,
99 SourceInspectionDiscoverPayloadSchema,
1010 SourceInspectionDiscoverResultSchema,
11− SourceInspectionSchemaBundleSchema,
1211 SourceInspectionSchema,
1312 SourceInspectionToolDetailSchema,
1413 SourceImportAuthPolicySchema,
@@ -80,7 +79,6 @@ export type UpdateSourcePayload = typeof UpdateSourcePayloadSchema.Type; · 65 unmodified lines
8079 const workspaceIdParam = HttpApiSchema.param("workspaceId", WorkspaceIdSchema);
8180 const sourceIdParam = HttpApiSchema.param("sourceId", SourceIdSchema);
8281 const toolPathParam = HttpApiSchema.param("toolPath", Schema.String);
83−const schemaBundleIdParam = HttpApiSchema.param("schemaBundleId", Schema.String);
8482
8583 const CredentialPageUrlParamsSchema = Schema.Struct({
8684 interactionId: ExecutionInteractionIdSchema,
@@ -242,15 +240,6 @@ export class SourcesApi extends HttpApiGroup.make("sources") · 155 unmodified lines
242240 .addError(ControlPlaneNotFoundError)
243241 .addError(ControlPlaneStorageError),
244242 )
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− )
254243 .add(
255244 HttpApiEndpoint.post("inspectionDiscover")`/workspaces/${workspaceIdParam}/sources/${sourceIdParam}/inspection/discover`
256245 .setPayload(SourceInspectionDiscoverPayloadSchema)
packages/control-plane/src/api/sources/http.ts+0−12
@@ -16,7 +16,6 @@ import { · 15 unmodified lines
1616 import {
1717 discoverSourceInspectionTools,
1818 getSourceInspection,
19− getSourceInspectionSchemaBundle,
2019 getSourceInspectionToolDetail,
2120 } from "../../runtime/source-inspection";
2221 import {
@@ -750,17 +749,6 @@ export const ControlPlaneSourcesLive = HttpApiBuilder.group( · 727 unmodified lines
750749 ),
751750 ),
752751 )
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− )
764752 .handle("inspectionDiscover", ({ path, payload }) =>
765753 resolveRequestedLocalWorkspace("sources.inspection_discover", path.workspaceId).pipe(
766754 Effect.zipRight(
packages/control-plane/src/index.ts+1−0
@@ -2,3 +2,4 @@ export * from "./schema/index"; · 1 unmodified line
22 export * from "./api/index";
33 export * from "./runtime/index";
44 export * from "./client";
5+export * 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+
14+Importers should move toward:
15+
16+`source -> CatalogFragmentV1 -> merge -> CatalogV1 -> projectors -> InvocationPlan`
17+
18+The existing runtime-specific compiled payload blobs are no longer the target semantic center.
19+
20+## Package Surface
21+
22+Use either:
23+
24+- `@executor/control-plane/ir`
25+- `import { ir } from "@executor/control-plane"`
26+
27+## Guarantees
28+
29+- provenance is required on entities
30+- synthetic entities are marked explicitly
31+- lossy normalization is represented by diagnostics
32+- inheritance is preserved in scopes and resolved at projection/planning time
33+- invocation plans remain connection-bound runtime artifacts, not canonical IR
packages/control-plane/src/ir/catalog.test.ts+711−0
@@ -0,0 +1,711 @@
1+import { describe, expect, it } from "@effect/vitest";
2+
3+import {
4+ buildInvocationPlan,
5+ createCatalogSnapshotV1,
6+ createEmptyCatalogV1,
7+ decodeCatalogSnapshotV1,
8+ mergeCatalogFragments,
9+ projectCatalogForAgentSdk,
10+ projectSymbolExpandedView,
11+ projectSymbolShallowView,
12+ validateCatalogInvariants,
13+} from "./catalog";
14+import {
15+ CapabilityIdSchema,
16+ DiagnosticIdSchema,
17+ DocumentIdSchema,
18+ ExecutableIdSchema,
19+ ExampleSymbolIdSchema,
20+ HeaderSymbolIdSchema,
21+ ParameterSymbolIdSchema,
22+ RequestBodySymbolIdSchema,
23+ ResourceIdSchema,
24+ ResponseSetIdSchema,
25+ ResponseSymbolIdSchema,
26+ ScopeIdSchema,
27+ SecuritySchemeSymbolIdSchema,
28+ ShapeSymbolIdSchema,
29+} from "./ids";
30+import type { CatalogV1, CatalogFragmentV1, ProvenanceRef } from "./model";
31+
32+const put = <K extends string, V>(record: Record<K, V>, key: K, value: V) => {
33+ record[key] = value;
34+};
35+
36+const docId = DocumentIdSchema.make("doc_primary");
37+const baseProvenance = (pointer: string): ProvenanceRef[] => [{
38+ relation: "declared",
39+ documentId: docId,
40+ pointer,
41+}];
42+
43+const createHttpCatalog = (input: {
44+ collideIds?: boolean;
45+ method?: string;
46+} = {}): CatalogV1 => {
47+ const catalog = createEmptyCatalogV1();
48+
49+ const resourceId = ResourceIdSchema.make("res_primary");
50+ const scopeId = ScopeIdSchema.make("scope_service");
51+ const authSchemeId = SecuritySchemeSymbolIdSchema.make("sym_security_oauth");
52+ const bodyShapeId = ShapeSymbolIdSchema.make("shape_event_body");
53+ const resultShapeId = ShapeSymbolIdSchema.make("shape_event_result");
54+ const stringShapeId = ShapeSymbolIdSchema.make("shape_string");
55+ const idPathParamId = ParameterSymbolIdSchema.make("sym_param_path_event_id");
56+ const calendarParamId = ParameterSymbolIdSchema.make("sym_param_path_calendar_id");
57+ const queryParamId = ParameterSymbolIdSchema.make(
58+ input.collideIds ? "sym_param_query_id" : "sym_param_query_send_updates",
59+ );
60+ const requestBodyId = RequestBodySymbolIdSchema.make("sym_request_body_event");
61+ const responseId = ResponseSymbolIdSchema.make("sym_response_200");
62+ const responseSetId = ResponseSetIdSchema.make("response_set_events_update");
63+ const executableId = ExecutableIdSchema.make("exec_http_events_update");
64+ const capabilityId = CapabilityIdSchema.make("cap_events_update");
65+ const exampleId = ExampleSymbolIdSchema.make("sym_example_event_body");
66+
67+ put(catalog.documents as Record<typeof docId, CatalogV1["documents"][typeof docId]>, docId, {
68+ id: docId,
69+ kind: "openapi",
70+ title: "Calendar API",
71+ fetchedAt: "2026-03-14T00:00:00.000Z",
72+ rawRef: "memory://calendar/openapi.json",
73+ });
74+
75+ put(catalog.resources as Record<typeof resourceId, CatalogV1["resources"][typeof resourceId]>, resourceId, {
76+ id: resourceId,
77+ documentId: docId,
78+ canonicalUri: "https://example.test/openapi.json",
79+ baseUri: "https://example.test/openapi.json",
80+ anchors: {},
81+ dynamicAnchors: {},
82+ synthetic: false,
83+ provenance: baseProvenance("#"),
84+ });
85+
86+ put(catalog.scopes as Record<typeof scopeId, CatalogV1["scopes"][typeof scopeId]>, scopeId, {
87+ id: scopeId,
88+ kind: "service",
89+ name: "Calendar",
90+ namespace: "google.calendar",
91+ docs: {
92+ summary: "Calendar service",
93+ description: "Manage calendar events.",
94+ },
95+ synthetic: false,
96+ provenance: baseProvenance("#/servers/0"),
97+ });
98+
99+ put(catalog.symbols as Record<typeof authSchemeId, CatalogV1["symbols"][typeof authSchemeId]>, authSchemeId, {
100+ id: authSchemeId,
101+ kind: "securityScheme",
102+ schemeType: "oauth2",
103+ docs: {
104+ summary: "OAuth 2.0",
105+ },
106+ oauth: {
107+ scopes: {
108+ "calendar.events": "Manage calendar events",
109+ },
110+ },
111+ synthetic: false,
112+ provenance: baseProvenance("#/components/securitySchemes/oauth2"),
113+ });
114+
115+ put(catalog.symbols as Record<typeof stringShapeId, CatalogV1["symbols"][typeof stringShapeId]>, stringShapeId, {
116+ id: stringShapeId,
117+ kind: "shape",
118+ resourceId,
119+ title: "String",
120+ node: {
121+ type: "scalar",
122+ scalar: "string",
123+ },
124+ synthetic: false,
125+ provenance: baseProvenance("#/components/schemas/String"),
126+ });
127+
128+ put(catalog.symbols as Record<typeof bodyShapeId, CatalogV1["symbols"][typeof bodyShapeId]>, bodyShapeId, {
129+ id: bodyShapeId,
130+ kind: "shape",
131+ resourceId,
132+ title: "EventUpdateBody",
133+ node: {
134+ type: "object",
135+ fields: {
136+ summary: {
137+ shapeId: stringShapeId,
138+ },
139+ },
140+ additionalProperties: false,
141+ },
142+ synthetic: false,
143+ provenance: baseProvenance("#/components/schemas/EventUpdateBody"),
144+ });
145+
146+ put(catalog.symbols as Record<typeof resultShapeId, CatalogV1["symbols"][typeof resultShapeId]>, resultShapeId, {
147+ id: resultShapeId,
148+ kind: "shape",
149+ resourceId,
150+ title: "Event",
151+ node: {
152+ type: "object",
153+ fields: {
154+ id: {
155+ shapeId: stringShapeId,
156+ },
157+ summary: {
158+ shapeId: stringShapeId,
159+ },
160+ },
161+ required: ["id"],
162+ additionalProperties: false,
163+ },
164+ synthetic: false,
165+ provenance: baseProvenance("#/components/schemas/Event"),
166+ });
167+
168+ put(catalog.symbols as Record<typeof exampleId, CatalogV1["symbols"][typeof exampleId]>, exampleId, {
169+ id: exampleId,
170+ kind: "example",
171+ exampleKind: "value",
172+ name: "Event body example",
173+ value: {
174+ summary: "Team sync",
175+ },
176+ synthetic: false,
177+ provenance: baseProvenance("#/components/examples/EventBody"),
178+ });
179+
180+ put(catalog.symbols as Record<typeof calendarParamId, CatalogV1["symbols"][typeof calendarParamId]>, calendarParamId, {
181+ id: calendarParamId,
182+ kind: "parameter",
183+ name: input.collideIds ? "id" : "calendarId",
184+ location: "path",
185+ required: true,
186+ docs: {
187+ summary: "Calendar identifier",
188+ description: "The target calendar.",
189+ },
190+ schemaShapeId: stringShapeId,
191+ synthetic: false,
192+ provenance: baseProvenance("#/paths/~1events~1{calendarId}/parameters/0"),
193+ });
194+
195+ put(catalog.symbols as Record<typeof idPathParamId, CatalogV1["symbols"][typeof idPathParamId]>, idPathParamId, {
196+ id: idPathParamId,
197+ kind: "parameter",
198+ name: "eventId",
199+ location: "path",
200+ required: true,
201+ docs: {
202+ summary: "Event identifier",
203+ },
204+ schemaShapeId: stringShapeId,
205+ synthetic: false,
206+ provenance: baseProvenance("#/paths/~1events~1{eventId}/parameters/0"),
207+ });
208+
209+ put(catalog.symbols as Record<typeof queryParamId, CatalogV1["symbols"][typeof queryParamId]>, queryParamId, {
210+ id: queryParamId,
211+ kind: "parameter",
212+ name: input.collideIds ? "id" : "sendUpdates",
213+ location: "query",
214+ docs: {
215+ summary: "Notification mode",
216+ },
217+ schemaShapeId: stringShapeId,
218+ synthetic: false,
219+ provenance: baseProvenance("#/paths/~1events/parameters/0"),
220+ });
221+
222+ put(catalog.symbols as Record<typeof requestBodyId, CatalogV1["symbols"][typeof requestBodyId]>, requestBodyId, {
223+ id: requestBodyId,
224+ kind: "requestBody",
225+ required: true,
226+ docs: {
227+ summary: "Event payload",
228+ },
229+ contents: [
230+ {
231+ mediaType: "application/json",
232+ shapeId: bodyShapeId,
233+ exampleIds: [exampleId],
234+ },
235+ ],
236+ synthetic: false,
237+ provenance: baseProvenance("#/paths/~1events/requestBody"),
238+ });
239+
240+ put(catalog.symbols as Record<typeof responseId, CatalogV1["symbols"][typeof responseId]>, responseId, {
241+ id: responseId,
242+ kind: "response",
243+ docs: {
244+ summary: "Updated event",
245+ },
246+ contents: [
247+ {
248+ mediaType: "application/json",
249+ shapeId: resultShapeId,
250+ },
251+ ],
252+ synthetic: false,
253+ provenance: baseProvenance("#/paths/~1events/responses/200"),
254+ });
255+
256+ put(catalog.responseSets as Record<typeof responseSetId, CatalogV1["responseSets"][typeof responseSetId]>, responseSetId, {
257+ id: responseSetId,
258+ variants: [
259+ {
260+ match: {
261+ kind: "exact",
262+ status: 200,
263+ },
264+ responseId,
265+ traits: ["success"],
266+ },
267+ ],
268+ synthetic: false,
269+ provenance: baseProvenance("#/paths/~1events/responses"),
270+ });
271+
272+ put(catalog.executables as Record<typeof executableId, CatalogV1["executables"][typeof executableId]>, executableId, {
273+ id: executableId,
274+ protocol: "http",
275+ capabilityId,
276+ scopeId,
277+ method: input.method ?? "PATCH",
278+ pathTemplate: "/calendars/{calendarId}/events/{eventId}",
279+ pathParameterIds: [calendarParamId, idPathParamId],
280+ queryParameterIds: [queryParamId],
281+ requestBodyId,
282+ responseSetId,
283+ synthetic: false,
284+ provenance: baseProvenance("#/paths/~1events/patch"),
285+ });
286+
287+ put(catalog.capabilities as Record<typeof capabilityId, CatalogV1["capabilities"][typeof capabilityId]>, capabilityId, {
288+ id: capabilityId,
289+ serviceScopeId: scopeId,
290+ surface: {
291+ toolPath: ["google", "calendar", "events", "update"],
292+ title: "Update event",
293+ summary: "Update a calendar event.",
294+ description: "Updates an existing event by ID.",
295+ tags: ["calendar", "events"],
296+ },
297+ semantics: {
298+ effect: "write",
299+ safe: false,
300+ idempotent: false,
301+ destructive: false,
302+ },
303+ auth: {
304+ kind: "scheme",
305+ schemeId: authSchemeId,
306+ scopes: ["calendar.events"],
307+ },
308+ interaction: {
309+ approval: {
310+ mayRequire: true,
311+ reasons: ["write"],
312+ },
313+ elicitation: {
314+ mayRequest: false,
315+ },
316+ resume: {
317+ supported: true,
318+ },
319+ },
320+ executableIds: [executableId],
321+ preferredExecutableId: executableId,
322+ synthetic: false,
323+ provenance: baseProvenance("#/paths/~1events/patch"),
324+ });
325+
326+ return catalog;
327+};
328+
329+const createGraphqlCatalog = (): CatalogV1 => {
330+ const catalog = createEmptyCatalogV1();
331+
332+ const resourceId = ResourceIdSchema.make("res_graphql");
333+ const scopeId = ScopeIdSchema.make("scope_graphql_service");
334+ const argShapeId = ShapeSymbolIdSchema.make("shape_graphql_args");
335+ const inputShapeId = ShapeSymbolIdSchema.make("shape_graphql_input");
336+ const selectShapeId = ShapeSymbolIdSchema.make("shape_graphql_select");
337+ const resultShapeId = ShapeSymbolIdSchema.make("shape_graphql_result");
338+ const stringShapeId = ShapeSymbolIdSchema.make("shape_graphql_string");
339+ const responseId = ResponseSymbolIdSchema.make("sym_graphql_response");
340+ const responseSetId = ResponseSetIdSchema.make("response_set_graphql");
341+ const executableId = ExecutableIdSchema.make("exec_graphql_update");
342+ const capabilityId = CapabilityIdSchema.make("cap_graphql_update");
343+
344+ put(catalog.documents as Record<typeof docId, CatalogV1["documents"][typeof docId]>, docId, {
345+ id: docId,
346+ kind: "graphql-schema",
347+ title: "GraphQL schema",
348+ fetchedAt: "2026-03-14T00:00:00.000Z",
349+ rawRef: "memory://graphql/schema.json",
350+ });
351+
352+ put(catalog.resources as Record<typeof resourceId, CatalogV1["resources"][typeof resourceId]>, resourceId, {
353+ id: resourceId,
354+ documentId: docId,
355+ canonicalUri: "https://example.test/graphql",
356+ baseUri: "https://example.test/graphql",
357+ anchors: {},
358+ dynamicAnchors: {},
359+ synthetic: false,
360+ provenance: baseProvenance("#"),
361+ });
362+
363+ put(catalog.scopes as Record<typeof scopeId, CatalogV1["scopes"][typeof scopeId]>, scopeId, {
364+ id: scopeId,
365+ kind: "service",
366+ name: "GraphQL",
367+ synthetic: false,
368+ provenance: baseProvenance("#/schema"),
369+ });
370+
371+ put(catalog.symbols as Record<typeof stringShapeId, CatalogV1["symbols"][typeof stringShapeId]>, stringShapeId, {
372+ id: stringShapeId,
373+ kind: "shape",
374+ resourceId,
375+ title: "String",
376+ node: {
377+ type: "scalar",
378+ scalar: "string",
379+ },
380+ synthetic: false,
381+ provenance: baseProvenance("#/$defs/String"),
382+ });
383+
384+ put(catalog.symbols as Record<typeof inputShapeId, CatalogV1["symbols"][typeof inputShapeId]>, inputShapeId, {
385+ id: inputShapeId,
386+ kind: "shape",
387+ resourceId,
388+ title: "UserInput",
389+ node: {
390+ type: "object",
391+ fields: {
392+ name: {
393+ shapeId: stringShapeId,
394+ },
395+ },
396+ additionalProperties: false,
397+ },
398+ synthetic: false,
399+ provenance: baseProvenance("#/$defs/UserInput"),
400+ });
401+
402+ put(catalog.symbols as Record<typeof argShapeId, CatalogV1["symbols"][typeof argShapeId]>, argShapeId, {
403+ id: argShapeId,
404+ kind: "shape",
405+ resourceId,
406+ title: "UpdateUserArgs",
407+ node: {
408+ type: "object",
409+ fields: {
410+ id: {
411+ shapeId: stringShapeId,
412+ },
413+ input: {
414+ shapeId: inputShapeId,
415+ },
416+ },
417+ required: ["id", "input"],
418+ additionalProperties: false,
419+ },
420+ synthetic: false,
421+ provenance: baseProvenance("#/$defs/UpdateUserArgs"),
422+ });
423+
424+ put(catalog.symbols as Record<typeof selectShapeId, CatalogV1["symbols"][typeof selectShapeId]>, selectShapeId, {
425+ id: selectShapeId,
426+ kind: "shape",
427+ resourceId,
428+ title: "UserSelection",
429+ node: {
430+ type: "object",
431+ fields: {
432+ id: {
433+ shapeId: stringShapeId,
434+ },
435+ name: {
436+ shapeId: stringShapeId,
437+ },
438+ },
439+ additionalProperties: false,
440+ },
441+ synthetic: false,
442+ provenance: baseProvenance("#/$defs/UserSelection"),
443+ });
444+
445+ put(catalog.symbols as Record<typeof resultShapeId, CatalogV1["symbols"][typeof resultShapeId]>, resultShapeId, {
446+ id: resultShapeId,
447+ kind: "shape",
448+ resourceId,
449+ title: "User",
450+ node: {
451+ type: "object",
452+ fields: {
453+ id: {
454+ shapeId: stringShapeId,
455+ },
456+ name: {
457+ shapeId: stringShapeId,
458+ },
459+ },
460+ required: ["id"],
461+ additionalProperties: false,
462+ },
463+ synthetic: false,
464+ provenance: baseProvenance("#/$defs/User"),
465+ });
466+
467+ put(catalog.symbols as Record<typeof responseId, CatalogV1["symbols"][typeof responseId]>, responseId, {
468+ id: responseId,
469+ kind: "response",
470+ contents: [
471+ {
472+ mediaType: "application/json",
473+ shapeId: resultShapeId,
474+ },
475+ ],
476+ synthetic: false,
477+ provenance: baseProvenance("#/responses/default"),
478+ });
479+
480+ put(catalog.responseSets as Record<typeof responseSetId, CatalogV1["responseSets"][typeof responseSetId]>, responseSetId, {
481+ id: responseSetId,
482+ variants: [
483+ {
484+ match: {
485+ kind: "default",
486+ },
487+ responseId,
488+ traits: ["success"],
489+ },
490+ ],
491+ synthetic: false,
492+ provenance: baseProvenance("#/responses"),
493+ });
494+
495+ put(catalog.executables as Record<typeof executableId, CatalogV1["executables"][typeof executableId]>, executableId, {
496+ id: executableId,
497+ protocol: "graphql",
498+ capabilityId,
499+ scopeId,
500+ operationType: "mutation",
501+ rootField: "updateUser",
502+ argumentShapeId: argShapeId,
503+ resultShapeId,
504+ selectionShapeId: selectShapeId,
505+ selectionMode: "caller",
506+ responseSetId,
507+ synthetic: false,
508+ provenance: baseProvenance("#/mutations/updateUser"),
509+ });
510+
511+ put(catalog.capabilities as Record<typeof capabilityId, CatalogV1["capabilities"][typeof capabilityId]>, capabilityId, {
512+ id: capabilityId,
513+ serviceScopeId: scopeId,
514+ surface: {
515+ toolPath: ["graphql", "users", "update"],
516+ title: "Update user",
517+ summary: "Update a user.",
518+ },
519+ semantics: {
520+ effect: "write",
521+ },
522+ auth: {
523+ kind: "none",
524+ },
525+ interaction: {
526+ approval: {
527+ mayRequire: true,
528+ reasons: ["write"],
529+ },
530+ elicitation: {
531+ mayRequest: false,
532+ },
533+ resume: {
534+ supported: false,
535+ },
536+ },
537+ executableIds: [executableId],
538+ preferredExecutableId: executableId,
539+ synthetic: false,
540+ provenance: baseProvenance("#/mutations/updateUser"),
541+ });
542+
543+ return catalog;
544+};
545+
546+describe("IR catalog", () => {
547+ it("decodes a snapshot through the canonical schema", () => {
548+ const catalog = createHttpCatalog();
549+ const snapshot = createCatalogSnapshotV1({
550+ import: {
551+ sourceKind: "openapi",
552+ adapterKey: "openapi",
553+ importerVersion: "1.0.0",
554+ importedAt: "2026-03-14T00:00:00.000Z",
555+ sourceConfigHash: "hash_source",
556+ },
557+ catalog,
558+ });
559+
560+ const decoded = decodeCatalogSnapshotV1(snapshot);
561+ expect(decoded.version).toBe("ir.v1.snapshot");
562+ expect(decoded.catalog.capabilities[CapabilityIdSchema.make("cap_events_update")]?.surface.title).toBe(
563+ "Update event",
564+ );
565+ });
566+
567+ it("merges fragments and emits diagnostics on conflicting IDs", () => {
568+ const fragmentA: CatalogFragmentV1 = {
569+ version: "ir.v1.fragment",
570+ documents: {
571+ [docId]: {
572+ id: docId,
573+ kind: "openapi",
574+ title: "A",
575+ fetchedAt: "2026-03-14T00:00:00.000Z",
576+ rawRef: "memory://a",
577+ },
578+ },
579+ };
580+ const fragmentB: CatalogFragmentV1 = {
581+ version: "ir.v1.fragment",
582+ documents: {
583+ [docId]: {
584+ id: docId,
585+ kind: "openapi",
586+ title: "B",
587+ fetchedAt: "2026-03-14T00:00:00.000Z",
588+ rawRef: "memory://b",
589+ },
590+ },
591+ };
592+
593+ const merged = mergeCatalogFragments([fragmentA, fragmentB]);
594+ expect(merged.documents[docId]?.title).toBe("A");
595+ expect(Object.values(merged.diagnostics).some((diagnostic) => diagnostic.code === "merge_conflict_preserved_first")).toBe(true);
596+ });
597+
598+ it("reports invariant violations for unresolved shapes without diagnostics", () => {
599+ const catalog = createEmptyCatalogV1();
600+ const shapeId = ShapeSymbolIdSchema.make("shape_unresolved");
601+
602+ put(catalog.documents as Record<typeof docId, CatalogV1["documents"][typeof docId]>, docId, {
603+ id: docId,
604+ kind: "custom",
605+ fetchedAt: "2026-03-14T00:00:00.000Z",
606+ rawRef: "memory://custom",
607+ });
608+
609+ put(catalog.symbols as Record<typeof shapeId, CatalogV1["symbols"][typeof shapeId]>, shapeId, {
610+ id: shapeId,
611+ kind: "shape",
612+ title: "Unresolved",
613+ node: {
614+ type: "unknown",
615+ reason: "unresolved reference #/components/schemas/Missing",
616+ },
617+ synthetic: false,
618+ provenance: baseProvenance("#/components/schemas/Missing"),
619+ });
620+
621+ const violations = validateCatalogInvariants(catalog);
622+ expect(violations.map((violation) => violation.code)).toContain("missing_resource_context");
623+ expect(violations.map((violation) => violation.code)).toContain("missing_unresolved_ref_diagnostic");
624+ });
625+
626+ it("projects HTTP capabilities into ergonomic call and result shapes", () => {
627+ const projected = projectCatalogForAgentSdk({
628+ catalog: createHttpCatalog(),
629+ });
630+
631+ const descriptor = projected.toolDescriptors[CapabilityIdSchema.make("cap_events_update")];
632+ expect(descriptor.toolPath).toEqual(["google", "calendar", "events", "update"]);
633+ expect(descriptor.resultShapeId).toBe(ShapeSymbolIdSchema.make("shape_event_result"));
634+
635+ const callShape = projected.catalog.symbols[descriptor.callShapeId];
636+ expect(callShape?.kind).toBe("shape");
637+ if (callShape?.kind !== "shape" || callShape.node.type !== "object") {
638+ throw new Error("Expected projected call shape object");
639+ }
640+
641+ expect(Object.keys(callShape.node.fields)).toEqual([
642+ "calendarId",
643+ "eventId",
644+ "sendUpdates",
645+ "body",
646+ ]);
647+ expect(callShape.node.required).toEqual(["calendarId", "eventId", "body"]);
648+
649+ const searchDoc = projected.searchDocs[CapabilityIdSchema.make("cap_events_update")];
650+ expect(searchDoc.authHints).toContain("oauth2");
651+ expect(searchDoc.tags).toEqual(["calendar", "events"]);
652+ });
653+
654+ it("groups colliding HTTP parameter names into path/query containers", () => {
655+ const projected = projectCatalogForAgentSdk({
656+ catalog: createHttpCatalog({ collideIds: true }),
657+ });
658+
659+ const descriptor = projected.toolDescriptors[CapabilityIdSchema.make("cap_events_update")];
660+ const callShape = projected.catalog.symbols[descriptor.callShapeId];
661+ if (callShape?.kind !== "shape" || callShape.node.type !== "object") {
662+ throw new Error("Expected projected call shape object");
663+ }
664+
665+ expect(Object.keys(callShape.node.fields)).toContain("path");
666+ expect(Object.keys(callShape.node.fields)).toContain("query");
667+ expect(Object.keys(callShape.node.fields)).not.toContain("id");
668+ expect(Object.values(projected.catalog.diagnostics).some((diagnostic) => diagnostic.code === "projection_collision_grouped_fields")).toBe(true);
669+ });
670+
671+ it("keeps GraphQL selection explicit for caller-driven outputs", () => {
672+ const projected = projectCatalogForAgentSdk({
673+ catalog: createGraphqlCatalog(),
674+ });
675+
676+ const descriptor = projected.toolDescriptors[CapabilityIdSchema.make("cap_graphql_update")];
677+ const callShape = projected.catalog.symbols[descriptor.callShapeId];
678+ if (callShape?.kind !== "shape" || callShape.node.type !== "object") {
679+ throw new Error("Expected projected GraphQL call shape object");
680+ }
681+
682+ expect(Object.keys(callShape.node.fields)).toEqual(["id", "input", "select"]);
683+ expect(descriptor.resultShapeId).toBe(ShapeSymbolIdSchema.make("shape_graphql_result"));
684+ });
685+
686+ it("builds inspect views and invocation plans from the projected graph", () => {
687+ const projected = projectCatalogForAgentSdk({
688+ catalog: createHttpCatalog(),
689+ });
690+ const capabilityId = CapabilityIdSchema.make("cap_events_update");
691+ const descriptor = projected.toolDescriptors[capabilityId];
692+ const shallow = projectSymbolShallowView(projected.catalog, descriptor.callShapeId);
693+ const expanded = projectSymbolExpandedView(projected.catalog, descriptor.callShapeId);
694+ const plan = buildInvocationPlan({
695+ catalog: projected.catalog,
696+ capabilityId,
697+ connectionBindingId: "binding_primary",
698+ resolvedEndpoint: "https://example.test",
699+ requestPlan: {
700+ method: "PATCH",
701+ path: "/calendars/primary/events/abc",
702+ },
703+ });
704+
705+ expect(shallow.edges.some((edge) => edge.label === "field:body")).toBe(true);
706+ expect(expanded.symbol.id).toBe(descriptor.callShapeId);
707+ expect(plan.connectionBindingId).toBe("binding_primary");
708+ expect(plan.resolvedScopeChain).toEqual([ScopeIdSchema.make("scope_service")]);
709+ expect(plan.resolvedInteraction?.requiresApproval).toBe(true);
710+ });
711+});
packages/control-plane/src/ir/catalog.ts+1628−0
@@ -0,0 +1,1628 @@
1+import { sha256Hex } from "@executor/codemode-core";
2+import * as ParseResult from "effect/ParseResult";
3+import { Schema } from "effect";
4+
5+import {
6+ CapabilityIdSchema,
7+ type CapabilityId,
8+ DiagnosticIdSchema,
9+ type DiagnosticId,
10+ DocumentIdSchema,
11+ type DocumentId,
12+ ExecutableIdSchema,
13+ type ExecutableId,
14+ ExampleSymbolIdSchema,
15+ type ExampleSymbolId,
16+ HeaderSymbolIdSchema,
17+ type HeaderSymbolId,
18+ ParameterSymbolIdSchema,
19+ type ParameterSymbolId,
20+ type RequestBodySymbolId,
21+ ResourceIdSchema,
22+ type ResourceId,
23+ type ResponseSymbolId,
24+ type ResponseSetId,
25+ ScopeIdSchema,
26+ type ScopeId,
27+ type ShapeSymbolId,
28+ ShapeSymbolIdSchema,
29+ type SymbolId,
30+} from "./ids";
31+import {
32+ CatalogFragmentV1Schema,
33+ type CatalogFragmentV1,
34+ CatalogSnapshotV1Schema,
35+ CatalogV1Schema,
36+ type CatalogV1,
37+ type Capability,
38+ type ContentSpec,
39+ type DocumentationBlock,
40+ type Executable,
41+ type GraphQLExecutable,
42+ type HttpExecutable,
43+ type ImportDiagnostic,
44+ type ImportMetadata,
45+ type InteractionSpec,
46+ type McpExecutable,
47+ type ParameterSymbol,
48+ type HeaderSymbol,
49+ type ResponseSet,
50+ type ResponseSymbol,
51+ type Scope,
52+ type ScopeDefaults,
53+ type ShapeSymbol,
54+ type StatusMatch,
55+ type Symbol as IrSymbol,
56+} from "./model";
57+
58+export interface ToolDescriptor {
59+ toolPath: string[];
60+ capabilityId: CapabilityId;
61+ title?: string;
62+ summary?: string;
63+ effect: Capability["semantics"]["effect"];
64+ interaction: {
65+ mayRequireApproval: boolean;
66+ mayElicit: boolean;
67+ };
68+ callShapeId: ShapeSymbolId;
69+ resultShapeId?: ShapeSymbolId;
70+ responseSetId: ResponseSetId;
71+ diagnosticCounts: {
72+ warning: number;
73+ error: number;
74+ };
75+}
76+
77+export interface ToolSearchDoc {
78+ capabilityId: CapabilityId;
79+ toolPath: string[];
80+ title?: string;
81+ summary?: string;
82+ tags?: string[];
83+ protocolHints: string[];
84+ authHints: string[];
85+ effect: Capability["semantics"]["effect"];
86+}
87+
88+export interface CapabilitySummaryView {
89+ capabilityId: CapabilityId;
90+ toolPath: string[];
91+ summary?: string;
92+ executableIds: ExecutableId[];
93+ auth: Capability["auth"];
94+ interaction: InteractionSpec;
95+ callShapeId: ShapeSymbolId;
96+ resultShapeId?: ShapeSymbolId;
97+ responseSetId: ResponseSetId;
98+ diagnosticIds?: DiagnosticId[];
99+}
100+
101+export interface SymbolShallowView {
102+ symbolId: SymbolId;
103+ kind: IrSymbol["kind"];
104+ title?: string;
105+ summary?: string;
106+ edges: Array<{ label: string; targetId: SymbolId }>;
107+}
108+
109+export interface SymbolExpandedView {
110+ symbolId: SymbolId;
111+ symbol: IrSymbol;
112+ diagnostics?: ImportDiagnostic[];
113+}
114+
115+export interface InvocationPlan {
116+ capabilityId: CapabilityId;
117+ executableId: ExecutableId;
118+ connectionBindingId: string;
119+ resolvedScopeChain: ScopeId[];
120+ resolvedEndpoint?: string;
121+ resolvedAuth?: unknown;
122+ resolvedInteraction?: {
123+ requiresApproval: boolean;
124+ mayPauseForElicitation: boolean;
125+ resumable: boolean;
126+ };
127+ requestPlan: unknown;
128+ provenance: CatalogV1["capabilities"][CapabilityId]["provenance"];
129+}
130+
131+export interface ProjectedCatalog {
132+ catalog: CatalogV1;
133+ toolDescriptors: Record<CapabilityId, ToolDescriptor>;
134+ searchDocs: Record<CapabilityId, ToolSearchDoc>;
135+ capabilityViews: Record<CapabilityId, CapabilitySummaryView>;
136+}
137+
138+export interface CatalogInvariantViolation {
139+ code:
140+ | "missing_symbol_provenance"
141+ | "missing_entity_provenance"
142+ | "missing_resource_context"
143+ | "missing_reference_target"
144+ | "missing_unresolved_ref_diagnostic"
145+ | "missing_executable"
146+ | "missing_response_set"
147+ | "missing_scope"
148+ | "missing_service_scope"
149+ | "invalid_preferred_executable"
150+ | "missing_projection_synthetic_marker";
151+ message: string;
152+ entityId?: string;
153+}
154+
155+export type DecodeCatalogError = Error;
156+
157+type MutableScopeDefaults = {
158+ servers?: ScopeDefaults["servers"];
159+ auth?: ScopeDefaults["auth"];
160+ parameterIds?: ParameterSymbolId[];
161+ headerIds?: HeaderSymbolId[];
162+ variables?: Record<string, string>;
163+};
164+
165+type CatalogSnapshotV1 = {
166+ version: "ir.v1.snapshot";
167+ import: ImportMetadata;
168+ catalog: CatalogV1;
169+};
170+
171+const decodeCatalogSync = Schema.decodeUnknownSync(
172+ CatalogV1Schema as Schema.Schema<CatalogV1, unknown, never>,
173+);
174+const decodeCatalogSnapshotSync = Schema.decodeUnknownSync(
175+ CatalogSnapshotV1Schema as Schema.Schema<CatalogSnapshotV1, unknown, never>,
176+);
177+const decodeCatalogFragmentSync = Schema.decodeUnknownSync(
178+ CatalogFragmentV1Schema as Schema.Schema<CatalogFragmentV1, unknown, never>,
179+);
180+
181+const stableStringify = (value: unknown): string => {
182+ if (value === null || typeof value !== "object") {
183+ return JSON.stringify(value);
184+ }
185+
186+ if (Array.isArray(value)) {
187+ return `[${value.map((entry) => stableStringify(entry)).join(",")}]`;
188+ }
189+
190+ const entries = Object.entries(value as Record<string, unknown>)
191+ .sort(([left], [right]) => left.localeCompare(right))
192+ .map(([key, entry]) => `${JSON.stringify(key)}:${stableStringify(entry)}`);
193+ return `{${entries.join(",")}}`;
194+};
195+
196+const stableHash = (value: unknown): string => sha256Hex(stableStringify(value)).slice(0, 16);
197+
198+const unique = <T>(values: readonly T[]): T[] => [...new Set(values)];
199+
200+const cloneCatalog = (catalog: CatalogV1): CatalogV1 => ({
201+ version: catalog.version,
202+ documents: { ...catalog.documents },
203+ resources: { ...catalog.resources },
204+ scopes: { ...catalog.scopes },
205+ symbols: { ...catalog.symbols },
206+ capabilities: { ...catalog.capabilities },
207+ executables: { ...catalog.executables },
208+ responseSets: { ...catalog.responseSets },
209+ diagnostics: { ...catalog.diagnostics },
210+});
211+
212+const mutableRecord = <K extends string, V>(value: Readonly<Record<K, V>>): Record<K, V> =>
213+ value as unknown as Record<K, V>;
214+
215+const createStableEntityId = <A extends string>(
216+ make: (value: string) => A,
217+ prefix: string,
218+ value: unknown,
219+): A => make(`${prefix}_${stableHash(value)}`);
220+
221+const emptyCatalog = (): CatalogV1 => ({
222+ version: "ir.v1",
223+ documents: {},
224+ resources: {},
225+ scopes: {},
226+ symbols: {},
227+ capabilities: {},
228+ executables: {},
229+ responseSets: {},
230+ diagnostics: {},
231+});
232+
233+const authHintStrings = (catalog: CatalogV1, auth: Capability["auth"]): string[] => {
234+ switch (auth.kind) {
235+ case "none":
236+ return ["none"];
237+ case "scheme": {
238+ const scheme = catalog.symbols[auth.schemeId];
239+ if (!scheme || scheme.kind !== "securityScheme") {
240+ return ["unknown"];
241+ }
242+ return [
243+ scheme.schemeType,
244+ ...((auth.scopes ?? []).map((scope: string) => `scope:${scope}`)),
245+ ];
246+ }
247+ case "allOf":
248+ case "anyOf":
249+ return unique(auth.items.flatMap((item: Capability["auth"]) => authHintStrings(catalog, item)));
250+ }
251+};
252+
253+const docsSummary = (docs: DocumentationBlock | undefined): string | undefined =>
254+ docs?.summary ?? docs?.description;
255+
256+const getShape = (catalog: CatalogV1, shapeId: ShapeSymbolId): ShapeSymbol | undefined => {
257+ const symbol = catalog.symbols[shapeId];
258+ return symbol && symbol.kind === "shape" ? symbol : undefined;
259+};
260+
261+const getParameter = (
262+ catalog: CatalogV1,
263+ parameterId: ParameterSymbolId,
264+): ParameterSymbol | undefined => {
265+ const symbol = catalog.symbols[parameterId];
266+ return symbol && symbol.kind === "parameter" ? symbol : undefined;
267+};
268+
269+const getResponseSymbol = (
270+ catalog: CatalogV1,
271+ responseId: ResponseSymbolId,
272+): ResponseSymbol | undefined => {
273+ const symbol = catalog.symbols[responseId];
274+ return symbol && symbol.kind === "response" ? symbol : undefined;
275+};
276+
277+const getHeader = (
278+ catalog: CatalogV1,
279+ headerId: HeaderSymbolId,
280+): HeaderSymbol | undefined => {
281+ const symbol = catalog.symbols[headerId];
282+ return symbol && symbol.kind === "header" ? symbol : undefined;
283+};
284+
285+const scopeChain = (catalog: CatalogV1, scopeId: ScopeId): Scope[] => {
286+ const chain: Scope[] = [];
287+ let currentId: ScopeId | undefined = scopeId;
288+
289+ while (currentId) {
290+ const scope: Scope | undefined = catalog.scopes[currentId];
291+ if (!scope) {
292+ break;
293+ }
294+ chain.unshift(scope);
295+ currentId = scope.parentId;
296+ }
297+
298+ return chain;
299+};
300+
301+const mergeScopeDefaults = (chain: readonly Scope[]): ScopeDefaults => {
302+ const merged: MutableScopeDefaults = {};
303+
304+ for (const scope of chain) {
305+ const defaults = scope.defaults;
306+ if (!defaults) {
307+ continue;
308+ }
309+
310+ if (defaults.servers) {
311+ merged.servers = [...defaults.servers];
312+ }
313+
314+ if (defaults.auth) {
315+ merged.auth = defaults.auth;
316+ }
317+
318+ if (defaults.parameterIds) {
319+ merged.parameterIds = unique([
320+ ...(merged.parameterIds ?? []),
321+ ...defaults.parameterIds,
322+ ]);
323+ }
324+
325+ if (defaults.headerIds) {
326+ merged.headerIds = unique([
327+ ...(merged.headerIds ?? []),
328+ ...defaults.headerIds,
329+ ]);
330+ }
331+
332+ if (defaults.variables) {
333+ merged.variables = {
334+ ...(merged.variables ?? {}),
335+ ...defaults.variables,
336+ };
337+ }
338+ }
339+
340+ return merged as ScopeDefaults;
341+};
342+
343+const isJsonMediaType = (mediaType: string): boolean => {
344+ const normalized = mediaType.trim().toLowerCase();
345+ return normalized === "application/json"
346+ || normalized.endsWith("+json")
347+ || normalized === "text/json";
348+};
349+
350+const createDiagnostic = (
351+ catalog: CatalogV1,
352+ input: Omit<ImportDiagnostic, "id"> & { idSeed: unknown },
353+): DiagnosticId => {
354+ const diagnosticId = createStableEntityId(
355+ DiagnosticIdSchema.make,
356+ "diag",
357+ input.idSeed,
358+ );
359+
360+ mutableRecord(catalog.diagnostics)[diagnosticId] = {
361+ id: diagnosticId,
362+ level: input.level,
363+ code: input.code,
364+ message: input.message,
365+ ...(input.relatedSymbolIds ? { relatedSymbolIds: input.relatedSymbolIds } : {}),
366+ provenance: input.provenance,
367+ };
368+
369+ return diagnosticId;
370+};
371+
372+const ensureProjectionResource = (
373+ catalog: CatalogV1,
374+ capability: Capability,
375+): ResourceId => {
376+ const resourceId = createStableEntityId(
377+ ResourceIdSchema.make,
378+ "res",
379+ {
380+ kind: "projection",
381+ capabilityId: capability.id,
382+ },
383+ );
384+
385+ if (!catalog.resources[resourceId]) {
386+ const serviceScope = catalog.scopes[capability.serviceScopeId];
387+ const documentId = serviceScope?.provenance[0]?.documentId
388+ ?? DocumentIdSchema.make(`doc_projection_${stableHash(capability.id)}`);
389+
390+ if (!catalog.documents[documentId]) {
391+ mutableRecord(catalog.documents)[documentId] = {
392+ id: documentId,
393+ kind: "custom",
394+ title: `Projection resource for ${capability.id}`,
395+ fetchedAt: new Date(0).toISOString(),
396+ rawRef: `synthetic://projection/${capability.id}`,
397+ };
398+ }
399+
400+ mutableRecord(catalog.resources)[resourceId] = {
401+ id: resourceId,
402+ documentId,
403+ canonicalUri: `synthetic://projection/${capability.id}`,
404+ baseUri: `synthetic://projection/${capability.id}`,
405+ anchors: {},
406+ dynamicAnchors: {},
407+ synthetic: true,
408+ provenance: capability.provenance,
409+ };
410+
411+ createDiagnostic(catalog, {
412+ idSeed: {
413+ code: "synthetic_resource_context_created",
414+ capabilityId: capability.id,
415+ },
416+ level: "info",
417+ code: "synthetic_resource_context_created",
418+ message: `Created synthetic projection resource for ${capability.id}`,
419+ provenance: capability.provenance,
420+ });
421+ }
422+
423+ return resourceId;
424+};
425+
426+const createSyntheticShape = (
427+ catalog: CatalogV1,
428+ input: {
429+ capability: Capability;
430+ label: string;
431+ node: ShapeSymbol["node"];
432+ title?: string;
433+ docs?: DocumentationBlock;
434+ diagnostic?:
435+ | {
436+ level: ImportDiagnostic["level"];
437+ code: ImportDiagnostic["code"];
438+ message: string;
439+ relatedSymbolIds?: SymbolId[];
440+ }
441+ | undefined;
442+ },
443+): ShapeSymbolId => {
444+ const resourceId = ensureProjectionResource(catalog, input.capability);
445+ const shapeId = createStableEntityId(
446+ ShapeSymbolIdSchema.make,
447+ "shape",
448+ {
449+ capabilityId: input.capability.id,
450+ label: input.label,
451+ node: input.node,
452+ },
453+ );
454+
455+ if (!catalog.symbols[shapeId]) {
456+ const diagnostics = input.diagnostic
457+ ? [
458+ createDiagnostic(catalog, {
459+ idSeed: {
460+ code: input.diagnostic.code,
461+ shapeId,
462+ },
463+ level: input.diagnostic.level,
464+ code: input.diagnostic.code,
465+ message: input.diagnostic.message,
466+ ...(input.diagnostic.relatedSymbolIds
467+ ? { relatedSymbolIds: input.diagnostic.relatedSymbolIds }
468+ : {}),
469+ provenance: input.capability.provenance,
470+ }),
471+ ]
472+ : undefined;
473+
474+ mutableRecord(catalog.symbols)[shapeId] = {
475+ id: shapeId,
476+ kind: "shape",
477+ resourceId,
478+ ...(input.title ? { title: input.title } : {}),
479+ ...(input.docs ? { docs: input.docs } : {}),
480+ node: input.node,
481+ synthetic: true,
482+ provenance: input.capability.provenance,
483+ ...(diagnostics ? { diagnosticIds: diagnostics } : {}),
484+ };
485+ }
486+
487+ return shapeId;
488+};
489+
490+const contentShapeId = (
491+ catalog: CatalogV1,
492+ capability: Capability,
493+ contents: readonly ContentSpec[] | undefined,
494+ label: string,
495+): ShapeSymbolId | undefined => {
496+ const shapeIds = unique(
497+ (contents ?? [])
498+ .map((content) => content.shapeId)
499+ .filter((shapeId): shapeId is ShapeSymbolId => shapeId !== undefined),
500+ );
501+
502+ if (shapeIds.length === 0) {
503+ return undefined;
504+ }
505+
506+ if (shapeIds.length === 1) {
507+ return shapeIds[0];
508+ }
509+
510+ return createSyntheticShape(catalog, {
511+ capability,
512+ label,
513+ title: `${capability.surface.title ?? capability.id} content union`,
514+ node: {
515+ type: "anyOf",
516+ items: shapeIds,
517+ },
518+ diagnostic: {
519+ level: "warning",
520+ code: "projection_result_shape_synthesized",
521+ message: `Synthesized content union for ${capability.id}`,
522+ relatedSymbolIds: shapeIds,
523+ },
524+ });
525+};
526+
527+const parameterShapeId = (
528+ catalog: CatalogV1,
529+ capability: Capability,
530+ parameter: ParameterSymbol,
531+): ShapeSymbolId => {
532+ if (parameter.schemaShapeId) {
533+ return parameter.schemaShapeId;
534+ }
535+
536+ const contentShape = contentShapeId(
537+ catalog,
538+ capability,
539+ parameter.content,
540+ `parameter:${parameter.id}`,
541+ );
542+ if (contentShape) {
543+ return contentShape;
544+ }
545+
546+ return createSyntheticShape(catalog, {
547+ capability,
548+ label: `parameter:${parameter.id}:unknown`,
549+ title: parameter.name,
550+ docs: parameter.docs,
551+ node: {
552+ type: "unknown",
553+ reason: `Missing shape for parameter ${parameter.id}`,
554+ },
555+ diagnostic: {
556+ level: "warning",
557+ code: "projection_call_shape_synthesized",
558+ message: `Missing parameter shape for ${parameter.name}; using unknown`,
559+ relatedSymbolIds: [parameter.id],
560+ },
561+ });
562+};
563+
564+const requestBodyShapeId = (
565+ catalog: CatalogV1,
566+ capability: Capability,
567+ requestBodyId: RequestBodySymbolId | undefined,
568+): ShapeSymbolId | undefined => {
569+ if (!requestBodyId) {
570+ return undefined;
571+ }
572+
573+ const requestBody = catalog.symbols[requestBodyId];
574+ if (!requestBody || requestBody.kind !== "requestBody") {
575+ return undefined;
576+ }
577+
578+ return contentShapeId(
579+ catalog,
580+ capability,
581+ requestBody.contents,
582+ `requestBody:${requestBody.id}`,
583+ );
584+};
585+
586+const chooseExecutable = (catalog: CatalogV1, capability: Capability): Executable => {
587+ const preferredId = capability.preferredExecutableId;
588+ if (preferredId) {
589+ const preferred = catalog.executables[preferredId];
590+ if (preferred) {
591+ return preferred;
592+ }
593+ }
594+
595+ const executables = capability.executableIds
596+ .map((executableId) => catalog.executables[executableId])
597+ .filter((executable): executable is Executable => executable !== undefined);
598+
599+ if (executables.length === 0) {
600+ throw new Error(`Capability ${capability.id} has no executables`);
601+ }
602+
603+ return executables[0];
604+};
605+
606+const scoreStatusMatch = (match: StatusMatch): number => {
607+ switch (match.kind) {
608+ case "exact":
609+ return match.status === 200 ? 100 : match.status >= 200 && match.status < 300 ? 80 : 10;
610+ case "range":
611+ return match.value === "2XX" ? 60 : 5;
612+ case "default":
613+ return 40;
614+ }
615+};
616+
617+const responseContentCandidates = (
618+ response: ResponseSymbol,
619+): Array<{ mediaType: string; shapeId: ShapeSymbolId }> =>
620+ (response.contents ?? [])
621+ .flatMap((content) =>
622+ content.shapeId ? [{ mediaType: content.mediaType, shapeId: content.shapeId }] : [],
623+ );
624+
625+const projectResultShapeFromResponses = (
626+ catalog: CatalogV1,
627+ capability: Capability,
628+ responseSet: ResponseSet,
629+): ShapeSymbolId | undefined => {
630+ const ranked = [...responseSet.variants]
631+ .map((variant) => {
632+ const response = getResponseSymbol(catalog, variant.responseId);
633+ return response
634+ ? {
635+ variant,
636+ response,
637+ score: scoreStatusMatch(variant.match),
638+ }
639+ : null;
640+ })
641+ .filter((entry): entry is { variant: ResponseSet["variants"][number]; response: ResponseSymbol; score: number } => entry !== null)
642+ .sort((left, right) => right.score - left.score);
643+
644+ const jsonCandidates = ranked.flatMap(({ response }) =>
645+ responseContentCandidates(response).filter((candidate) => isJsonMediaType(candidate.mediaType)),
646+ );
647+
648+ if (jsonCandidates.length > 0) {
649+ return contentShapeId(
650+ catalog,
651+ capability,
652+ jsonCandidates.map((candidate) => ({
653+ mediaType: candidate.mediaType,
654+ shapeId: candidate.shapeId,
655+ })),
656+ `responseSet:${responseSet.id}:json`,
657+ );
658+ }
659+
660+ const fallbackShapeIds = unique(
661+ ranked.flatMap(({ response }) =>
662+ responseContentCandidates(response).map((candidate) => candidate.shapeId),
663+ ),
664+ );
665+
666+ if (fallbackShapeIds.length === 0) {
667+ return undefined;
668+ }
669+
670+ if (fallbackShapeIds.length === 1) {
671+ return fallbackShapeIds[0];
672+ }
673+
674+ return createSyntheticShape(catalog, {
675+ capability,
676+ label: `responseSet:${responseSet.id}:union`,
677+ title: `${capability.surface.title ?? capability.id} result`,
678+ node: {
679+ type: "anyOf",
680+ items: fallbackShapeIds,
681+ },
682+ diagnostic: {
683+ level: "warning",
684+ code: "multi_response_union_synthesized",
685+ message: `Synthesized response union for ${capability.id}`,
686+ relatedSymbolIds: fallbackShapeIds,
687+ },
688+ });
689+};
690+
691+const groupFieldShape = (
692+ catalog: CatalogV1,
693+ capability: Capability,
694+ location: "path" | "query" | "headers" | "cookies",
695+ fields: Record<string, { shapeId: ShapeSymbolId; docs?: DocumentationBlock }>,
696+ required: string[],
697+): ShapeSymbolId =>
698+ createSyntheticShape(catalog, {
699+ capability,
700+ label: `group:${capability.id}:${location}`,
701+ title: `${location} parameters`,
702+ node: {
703+ type: "object",
704+ fields: Object.fromEntries(
705+ Object.entries(fields).map(([name, field]) => [
706+ name,
707+ {
708+ shapeId: field.shapeId,
709+ ...(field.docs ? { docs: field.docs } : {}),
710+ },
711+ ]),
712+ ),
713+ ...(required.length > 0 ? { required } : {}),
714+ additionalProperties: false,
715+ },
716+ });
717+
718+const projectHttpCallShape = (
719+ catalog: CatalogV1,
720+ capability: Capability,
721+ executable: HttpExecutable,
722+): ShapeSymbolId => {
723+ const chain = scopeChain(catalog, executable.scopeId);
724+ const defaults = mergeScopeDefaults(chain);
725+
726+ const byLocation: Record<
727+ "path" | "query" | "header" | "cookie",
728+ ParameterSymbol[]
729+ > = {
730+ path: [],
731+ query: [],
732+ header: [],
733+ cookie: [],
734+ };
735+
736+ const pushParameter = (parameterId: ParameterSymbolId) => {
737+ const parameter = getParameter(catalog, parameterId);
738+ if (parameter) {
739+ byLocation[parameter.location].push(parameter);
740+ }
741+ };
742+
743+ for (const parameterId of defaults.parameterIds ?? []) {
744+ pushParameter(parameterId);
745+ }
746+ for (const parameterId of executable.pathParameterIds ?? []) {
747+ pushParameter(parameterId);
748+ }
749+ for (const parameterId of executable.queryParameterIds ?? []) {
750+ pushParameter(parameterId);
751+ }
752+ for (const parameterId of executable.headerParameterIds ?? []) {
753+ pushParameter(parameterId);
754+ }
755+ for (const parameterId of executable.cookieParameterIds ?? []) {
756+ pushParameter(parameterId);
757+ }
758+
759+ const explicitHeaders = unique([
760+ ...(defaults.headerIds ?? []),
761+ ])
762+ .map((headerId: HeaderSymbolId) => getHeader(catalog, headerId))
763+ .filter((symbol): symbol is HeaderSymbol => symbol !== undefined);
764+
765+ const reservedTopLevel = new Set(["body", "args", "select", "path", "query", "headers", "cookies"]);
766+ const allNames = [
767+ ...byLocation.path.map((parameter) => parameter.name),
768+ ...byLocation.query.map((parameter) => parameter.name),
769+ ...byLocation.header.map((parameter) => parameter.name),
770+ ...byLocation.cookie.map((parameter) => parameter.name),
771+ ];
772+
773+ const hasCollision = allNames.some((name, index) =>
774+ reservedTopLevel.has(name) || allNames.indexOf(name) !== index
775+ );
776+
777+ const topLevelFields: Record<string, { shapeId: ShapeSymbolId; docs?: DocumentationBlock }> = {};
778+ const topLevelRequired: string[] = [];
779+
780+ const groupedFieldNames: Array<keyof typeof byLocation> = hasCollision
781+ ? ["path", "query", "header", "cookie"]
782+ : [];
783+
784+ if (hasCollision) {
785+ createDiagnostic(catalog, {
786+ idSeed: {
787+ code: "projection_collision_grouped_fields",
788+ capabilityId: capability.id,
789+ },
790+ level: "info",
791+ code: "projection_collision_grouped_fields",
792+ message: `Grouped parameter fields for ${capability.id} to avoid collisions`,
793+ provenance: capability.provenance,
794+ });
795+ }
796+
797+ for (const location of Object.keys(byLocation) as Array<keyof typeof byLocation>) {
798+ const parameters = byLocation[location];
799+ if (parameters.length === 0) {
800+ continue;
801+ }
802+
803+ if (groupedFieldNames.includes(location)) {
804+ const groupFields: Record<string, { shapeId: ShapeSymbolId; docs?: DocumentationBlock }> = {};
805+ const groupRequired: string[] = [];
806+ for (const parameter of parameters) {
807+ groupFields[parameter.name] = {
808+ shapeId: parameterShapeId(catalog, capability, parameter),
809+ docs: parameter.docs,
810+ };
811+ if (parameter.required) {
812+ groupRequired.push(parameter.name);
813+ }
814+ }
815+
816+ const groupKey = location === "header" ? "headers" : location === "cookie" ? "cookies" : location;
817+ topLevelFields[groupKey] = {
818+ shapeId: groupFieldShape(catalog, capability, groupKey, groupFields, groupRequired),
819+ };
820+ if (groupRequired.length > 0) {
821+ topLevelRequired.push(groupKey);
822+ }
823+ continue;
824+ }
825+
826+ for (const parameter of parameters) {
827+ topLevelFields[parameter.name] = {
828+ shapeId: parameterShapeId(catalog, capability, parameter),
829+ docs: parameter.docs,
830+ };
831+ if (parameter.required) {
832+ topLevelRequired.push(parameter.name);
833+ }
834+ }
835+ }
836+
837+ for (const header of explicitHeaders) {
838+ if (hasCollision) {
839+ const existing = topLevelFields.headers
840+ ? getShape(catalog, topLevelFields.headers.shapeId)
841+ : undefined;
842+ const fields = existing && existing.node.type === "object"
843+ ? ({ ...existing.node.fields } as Record<string, { shapeId: ShapeSymbolId; docs?: DocumentationBlock }>)
844+ : {};
845+ fields[header.name] = {
846+ shapeId: header.schemaShapeId
847+ ?? createSyntheticShape(catalog, {
848+ capability,
849+ label: `header:${header.id}:unknown`,
850+ title: header.name,
851+ docs: header.docs,
852+ node: {
853+ type: "unknown",
854+ reason: `Missing shape for header ${header.id}`,
855+ },
856+ }),
857+ ...(header.docs ? { docs: header.docs } : {}),
858+ };
859+ topLevelFields.headers = {
860+ shapeId: groupFieldShape(
861+ catalog,
862+ capability,
863+ "headers",
864+ Object.fromEntries(
865+ Object.entries(fields).map(([name, field]) => [name, { shapeId: field.shapeId, docs: field.docs }]),
866+ ),
867+ [],
868+ ),
869+ };
870+ continue;
871+ }
872+
873+ topLevelFields[header.name] = {
874+ shapeId: header.schemaShapeId
875+ ?? createSyntheticShape(catalog, {
876+ capability,
877+ label: `header:${header.id}:unknown`,
878+ title: header.name,
879+ docs: header.docs,
880+ node: {
881+ type: "unknown",
882+ reason: `Missing shape for header ${header.id}`,
883+ },
884+ }),
885+ docs: header.docs,
886+ };
887+ }
888+
889+ const bodyShapeId = requestBodyShapeId(catalog, capability, executable.requestBodyId);
890+ if (bodyShapeId) {
891+ topLevelFields.body = { shapeId: bodyShapeId };
892+ const requestBody = executable.requestBodyId
893+ ? catalog.symbols[executable.requestBodyId]
894+ : undefined;
895+ if (requestBody && requestBody.kind === "requestBody" && requestBody.required) {
896+ topLevelRequired.push("body");
897+ }
898+ }
899+
900+ return createSyntheticShape(catalog, {
901+ capability,
902+ label: `call:${capability.id}`,
903+ title: `${capability.surface.title ?? capability.id} call`,
904+ node: {
905+ type: "object",
906+ fields: Object.fromEntries(
907+ Object.entries(topLevelFields).map(([name, field]) => [
908+ name,
909+ {
910+ shapeId: field.shapeId,
911+ ...(field.docs ? { docs: field.docs } : {}),
912+ },
913+ ]),
914+ ),
915+ ...(topLevelRequired.length > 0 ? { required: unique(topLevelRequired) } : {}),
916+ additionalProperties: false,
917+ },
918+ diagnostic: {
919+ level: "info",
920+ code: "projection_call_shape_synthesized",
921+ message: `Synthesized HTTP call shape for ${capability.id}`,
922+ },
923+ });
924+};
925+
926+const projectGraphqlCallShape = (
927+ catalog: CatalogV1,
928+ capability: Capability,
929+ executable: GraphQLExecutable,
930+): ShapeSymbolId => {
931+ const argumentShape = getShape(catalog, executable.argumentShapeId);
932+ const reserved = new Set(["body", "args", "select", "path", "query", "headers", "cookies"]);
933+
934+ const canFlatten =
935+ argumentShape?.node.type === "object"
936+ && Object.keys(argumentShape.node.fields).every((name) => !reserved.has(name));
937+
938+ const fields: Record<string, { shapeId: ShapeSymbolId; docs?: DocumentationBlock }> = {};
939+ const required: string[] = [];
940+
941+ if (canFlatten && argumentShape.node.type === "object") {
942+ for (const [name, field] of Object.entries(argumentShape.node.fields)) {
943+ fields[name] = {
944+ shapeId: field.shapeId,
945+ docs: field.docs,
946+ };
947+ }
948+ required.push(...(argumentShape.node.required ?? []));
949+ } else {
950+ fields.args = {
951+ shapeId: executable.argumentShapeId,
952+ docs: argumentShape?.docs,
953+ };
954+ required.push("args");
955+ }
956+
957+ if (executable.selectionMode === "caller") {
958+ fields.select = {
959+ shapeId:
960+ executable.selectionShapeId
961+ ?? createSyntheticShape(catalog, {
962+ capability,
963+ label: `select:${capability.id}:unknown`,
964+ title: `${capability.surface.title ?? capability.id} selection`,
965+ node: {
966+ type: "unknown",
967+ reason: `Missing selection shape for ${capability.id}`,
968+ },
969+ diagnostic: {
970+ level: "warning",
971+ code: "selection_shape_missing",
972+ message: `Missing selection shape for ${capability.id}`,
973+ },
974+ }),
975+ };
976+ required.push("select");
977+ }
978+
979+ return createSyntheticShape(catalog, {
980+ capability,
981+ label: `call:${capability.id}`,
982+ title: `${capability.surface.title ?? capability.id} call`,
983+ node: {
984+ type: "object",
985+ fields: Object.fromEntries(
986+ Object.entries(fields).map(([name, field]) => [
987+ name,
988+ {
989+ shapeId: field.shapeId,
990+ ...(field.docs ? { docs: field.docs } : {}),
991+ },
992+ ]),
993+ ),
994+ ...(required.length > 0 ? { required: unique(required) } : {}),
995+ additionalProperties: false,
996+ },
997+ diagnostic: {
998+ level: "info",
999+ code: "projection_call_shape_synthesized",
1000+ message: `Synthesized GraphQL call shape for ${capability.id}`,
1001+ },
1002+ });
1003+};
1004+
1005+const projectMcpCallShape = (
1006+ catalog: CatalogV1,
1007+ capability: Capability,
1008+ executable: McpExecutable,
1009+): ShapeSymbolId => {
1010+ if (!executable.inputShapeId) {
1011+ return createSyntheticShape(catalog, {
1012+ capability,
1013+ label: `call:${capability.id}:empty`,
1014+ title: `${capability.surface.title ?? capability.id} call`,
1015+ node: {
1016+ type: "object",
1017+ fields: {},
1018+ additionalProperties: false,
1019+ },
1020+ diagnostic: {
1021+ level: "info",
1022+ code: "projection_call_shape_synthesized",
1023+ message: `Synthesized empty MCP call shape for ${capability.id}`,
1024+ },
1025+ });
1026+ }
1027+
1028+ const inputShape = getShape(catalog, executable.inputShapeId);
1029+ if (inputShape?.node.type === "object") {
1030+ return executable.inputShapeId;
1031+ }
1032+
1033+ return createSyntheticShape(catalog, {
1034+ capability,
1035+ label: `call:${capability.id}`,
1036+ title: `${capability.surface.title ?? capability.id} call`,
1037+ node: {
1038+ type: "object",
1039+ fields: {
1040+ input: {
1041+ shapeId: executable.inputShapeId,
1042+ },
1043+ },
1044+ required: ["input"],
1045+ additionalProperties: false,
1046+ },
1047+ diagnostic: {
1048+ level: "info",
1049+ code: "projection_call_shape_synthesized",
1050+ message: `Wrapped non-object MCP input shape for ${capability.id}`,
1051+ },
1052+ });
1053+};
1054+
1055+const projectCapability = (
1056+ catalog: CatalogV1,
1057+ capability: Capability,
1058+): ToolDescriptor => {
1059+ const executable = chooseExecutable(catalog, capability);
1060+ const responseSet = catalog.responseSets[executable.responseSetId];
1061+
1062+ if (!responseSet) {
1063+ throw new Error(`Missing response set ${executable.responseSetId} for ${capability.id}`);
1064+ }
1065+
1066+ let callShapeId: ShapeSymbolId;
1067+ let resultShapeId: ShapeSymbolId | undefined;
1068+
1069+ switch (executable.protocol) {
1070+ case "http":
1071+ callShapeId = projectHttpCallShape(catalog, capability, executable);
1072+ resultShapeId = projectResultShapeFromResponses(catalog, capability, responseSet);
1073+ break;
1074+ case "graphql":
1075+ callShapeId = projectGraphqlCallShape(catalog, capability, executable);
1076+ resultShapeId = executable.resultShapeId;
1077+ break;
1078+ case "mcp":
1079+ callShapeId = projectMcpCallShape(catalog, capability, executable);
1080+ resultShapeId = executable.outputShapeId;
1081+ break;
1082+ }
1083+
1084+ const diagnosticIds = unique([
1085+ ...(capability.diagnosticIds ?? []),
1086+ ...(executable.diagnosticIds ?? []),
1087+ ...(responseSet.diagnosticIds ?? []),
1088+ ...(callShapeId ? catalog.symbols[callShapeId]?.diagnosticIds ?? [] : []),
1089+ ...(resultShapeId ? catalog.symbols[resultShapeId]?.diagnosticIds ?? [] : []),
1090+ ]);
1091+ const diagnostics = diagnosticIds
1092+ .map((diagnosticId) => catalog.diagnostics[diagnosticId])
1093+ .filter((diagnostic): diagnostic is ImportDiagnostic => diagnostic !== undefined);
1094+
1095+ return {
1096+ toolPath: [...capability.surface.toolPath],
1097+ capabilityId: capability.id,
1098+ ...(capability.surface.title ? { title: capability.surface.title } : {}),
1099+ ...(capability.surface.summary ? { summary: capability.surface.summary } : {}),
1100+ effect: capability.semantics.effect,
1101+ interaction: {
1102+ mayRequireApproval: capability.interaction.approval.mayRequire,
1103+ mayElicit: capability.interaction.elicitation.mayRequest,
1104+ },
1105+ callShapeId,
1106+ ...(resultShapeId ? { resultShapeId } : {}),
1107+ responseSetId: executable.responseSetId,
1108+ diagnosticCounts: {
1109+ warning: diagnostics.filter((diagnostic) => diagnostic.level === "warning").length,
1110+ error: diagnostics.filter((diagnostic) => diagnostic.level === "error").length,
1111+ },
1112+ };
1113+};
1114+
1115+const summaryView = (
1116+ catalog: CatalogV1,
1117+ capability: Capability,
1118+ descriptor: ToolDescriptor,
1119+): CapabilitySummaryView => ({
1120+ capabilityId: capability.id,
1121+ toolPath: [...capability.surface.toolPath],
1122+ ...(capability.surface.summary ? { summary: capability.surface.summary } : {}),
1123+ executableIds: [...capability.executableIds],
1124+ auth: capability.auth,
1125+ interaction: capability.interaction,
1126+ callShapeId: descriptor.callShapeId,
1127+ ...(descriptor.resultShapeId ? { resultShapeId: descriptor.resultShapeId } : {}),
1128+ responseSetId: descriptor.responseSetId,
1129+ ...(capability.diagnosticIds ? { diagnosticIds: [...capability.diagnosticIds] } : {}),
1130+});
1131+
1132+const searchDoc = (
1133+ catalog: CatalogV1,
1134+ capability: Capability,
1135+): ToolSearchDoc => ({
1136+ capabilityId: capability.id,
1137+ toolPath: [...capability.surface.toolPath],
1138+ ...(capability.surface.title ? { title: capability.surface.title } : {}),
1139+ ...(capability.surface.summary ? { summary: capability.surface.summary } : {}),
1140+ ...(capability.surface.tags ? { tags: [...capability.surface.tags] } : {}),
1141+ protocolHints: unique(
1142+ capability.executableIds
1143+ .map((executableId: ExecutableId) => catalog.executables[executableId]?.protocol)
1144+ .filter((protocol): protocol is Executable["protocol"] => protocol !== undefined),
1145+ ),
1146+ authHints: authHintStrings(catalog, capability.auth),
1147+ effect: capability.semantics.effect,
1148+});
1149+
1150+const symbolTitle = (symbol: IrSymbol): string | undefined => {
1151+ switch (symbol.kind) {
1152+ case "shape":
1153+ return symbol.title ?? symbol.docs?.summary;
1154+ case "parameter":
1155+ case "header":
1156+ return symbol.name;
1157+ case "example":
1158+ return symbol.name ?? symbol.docs?.summary;
1159+ case "requestBody":
1160+ case "response":
1161+ case "securityScheme":
1162+ return symbol.docs?.summary;
1163+ }
1164+};
1165+
1166+const symbolSummary = (symbol: IrSymbol): string | undefined =>
1167+ symbol.docs?.summary ?? docsSummary(symbol.docs);
1168+
1169+const symbolEdges = (symbol: IrSymbol): Array<{ label: string; targetId: SymbolId }> => {
1170+ switch (symbol.kind) {
1171+ case "shape": {
1172+ const edges: Array<{ label: string; targetId: SymbolId }> = [];
1173+ switch (symbol.node.type) {
1174+ case "object":
1175+ for (const [name, field] of Object.entries(symbol.node.fields)) {
1176+ edges.push({ label: `field:${name}`, targetId: field.shapeId });
1177+ }
1178+ if (typeof symbol.node.additionalProperties !== "boolean" && symbol.node.additionalProperties) {
1179+ edges.push({
1180+ label: "additionalProperties",
1181+ targetId: symbol.node.additionalProperties,
1182+ });
1183+ }
1184+ for (const [pattern, targetId] of Object.entries(symbol.node.patternProperties ?? {})) {
1185+ edges.push({ label: `pattern:${pattern}`, targetId });
1186+ }
1187+ break;
1188+ case "array":
1189+ edges.push({ label: "item", targetId: symbol.node.itemShapeId });
1190+ break;
1191+ case "tuple":
1192+ symbol.node.itemShapeIds.forEach((targetId, index) => {
1193+ edges.push({ label: `item:${String(index)}`, targetId });
1194+ });
1195+ if (typeof symbol.node.additionalItems !== "boolean" && symbol.node.additionalItems) {
1196+ edges.push({ label: "additionalItems", targetId: symbol.node.additionalItems });
1197+ }
1198+ break;
1199+ case "map":
1200+ edges.push({ label: "value", targetId: symbol.node.valueShapeId });
1201+ break;
1202+ case "allOf":
1203+ case "anyOf":
1204+ case "oneOf":
1205+ symbol.node.items.forEach((targetId, index) => {
1206+ edges.push({ label: `${symbol.node.type}:${String(index)}`, targetId });
1207+ });
1208+ if (symbol.node.type === "oneOf") {
1209+ for (const [key, targetId] of Object.entries(symbol.node.discriminator?.mapping ?? {})) {
1210+ edges.push({ label: `discriminator:${key}`, targetId });
1211+ }
1212+ }
1213+ break;
1214+ case "nullable":
1215+ edges.push({ label: "item", targetId: symbol.node.itemShapeId });
1216+ break;
1217+ case "ref":
1218+ edges.push({ label: "target", targetId: symbol.node.target });
1219+ break;
1220+ case "not":
1221+ edges.push({ label: "not", targetId: symbol.node.itemShapeId });
1222+ break;
1223+ case "conditional":
1224+ edges.push({ label: "if", targetId: symbol.node.ifShapeId });
1225+ if (symbol.node.thenShapeId) {
1226+ edges.push({ label: "then", targetId: symbol.node.thenShapeId });
1227+ }
1228+ if (symbol.node.elseShapeId) {
1229+ edges.push({ label: "else", targetId: symbol.node.elseShapeId });
1230+ }
1231+ break;
1232+ case "graphqlInterface":
1233+ for (const [name, field] of Object.entries(symbol.node.fields)) {
1234+ edges.push({ label: `field:${name}`, targetId: field.shapeId });
1235+ }
1236+ symbol.node.possibleTypeIds.forEach((targetId) => {
1237+ edges.push({ label: "possibleType", targetId });
1238+ });
1239+ break;
1240+ case "graphqlUnion":
1241+ symbol.node.memberTypeIds.forEach((targetId) => {
1242+ edges.push({ label: "member", targetId });
1243+ });
1244+ break;
1245+ default:
1246+ break;
1247+ }
1248+ return edges;
1249+ }
1250+ case "parameter":
1251+ case "header":
1252+ return unique([
1253+ ...(symbol.schemaShapeId ? [{ label: "schema", targetId: symbol.schemaShapeId as SymbolId }] : []),
1254+ ...((symbol.content ?? []).flatMap((content, index) => [
1255+ ...(content.shapeId ? [{ label: `content:${String(index)}`, targetId: content.shapeId as SymbolId }] : []),
1256+ ...((content.exampleIds ?? []).map((targetId) => ({
1257+ label: `example:${String(index)}`,
1258+ targetId: targetId as SymbolId,
1259+ }))),
1260+ ])),
1261+ ...((symbol.exampleIds ?? []).map((targetId) => ({
1262+ label: "example",
1263+ targetId: targetId as SymbolId,
1264+ }))),
1265+ ]);
1266+ case "requestBody":
1267+ return (symbol.contents ?? []).flatMap((content, index) => [
1268+ ...(content.shapeId ? [{ label: `content:${String(index)}`, targetId: content.shapeId as SymbolId }] : []),
1269+ ...((content.exampleIds ?? []).map((targetId) => ({
1270+ label: `example:${String(index)}`,
1271+ targetId: targetId as SymbolId,
1272+ }))),
1273+ ]);
1274+ case "response":
1275+ return [
1276+ ...((symbol.headerIds ?? []).map((targetId) => ({
1277+ label: "header",
1278+ targetId: targetId as SymbolId,
1279+ }))),
1280+ ...((symbol.contents ?? []).flatMap((content, index) => [
1281+ ...(content.shapeId ? [{ label: `content:${String(index)}`, targetId: content.shapeId as SymbolId }] : []),
1282+ ...((content.exampleIds ?? []).map((targetId) => ({
1283+ label: `example:${String(index)}`,
1284+ targetId: targetId as SymbolId,
1285+ }))),
1286+ ])),
1287+ ];
1288+ case "example":
1289+ case "securityScheme":
1290+ return [];
1291+ }
1292+};
1293+
1294+export const decodeCatalogV1 = (input: unknown): CatalogV1 => {
1295+ try {
1296+ return decodeCatalogSync(input);
1297+ } catch (cause) {
1298+ throw new Error(ParseResult.TreeFormatter.formatErrorSync(cause as never));
1299+ }
1300+};
1301+
1302+export const decodeCatalogFragmentV1 = (input: unknown): CatalogFragmentV1 => {
1303+ try {
1304+ return decodeCatalogFragmentSync(input);
1305+ } catch (cause) {
1306+ throw new Error(ParseResult.TreeFormatter.formatErrorSync(cause as never));
1307+ }
1308+};
1309+
1310+export const decodeCatalogSnapshotV1 = (input: unknown) => {
1311+ try {
1312+ return decodeCatalogSnapshotSync(input);
1313+ } catch (cause) {
1314+ throw new Error(ParseResult.TreeFormatter.formatErrorSync(cause as never));
1315+ }
1316+};
1317+
1318+export const createEmptyCatalogV1 = (): CatalogV1 => emptyCatalog();
1319+
1320+export const createCatalogSnapshotV1 = (input: {
1321+ import: ImportMetadata;
1322+ catalog: CatalogV1;
1323+}) => ({
1324+ version: "ir.v1.snapshot" as const,
1325+ import: input.import,
1326+ catalog: decodeCatalogV1(input.catalog),
1327+});
1328+
1329+export const mergeCatalogFragments = (fragments: readonly CatalogFragmentV1[]): CatalogV1 => {
1330+ const catalog = emptyCatalog();
1331+ const seenByCollection = new Map<string, string>();
1332+
1333+ const mergeRecord = <T extends { id: string }>(
1334+ collectionName: keyof CatalogV1,
1335+ entries: Record<string, T> | undefined,
1336+ ) => {
1337+ if (!entries) {
1338+ return;
1339+ }
1340+
1341+ const target = catalog[collectionName] as Record<string, T>;
1342+ for (const [id, entry] of Object.entries(entries)) {
1343+ const existing = target[id];
1344+ if (!existing) {
1345+ target[id] = entry;
1346+ seenByCollection.set(`${String(collectionName)}:${id}`, stableStringify(entry));
1347+ continue;
1348+ }
1349+
1350+ const existingHash = seenByCollection.get(`${String(collectionName)}:${id}`) ?? stableStringify(existing);
1351+ const nextHash = stableStringify(entry);
1352+ if (existingHash === nextHash) {
1353+ continue;
1354+ }
1355+
1356+ if (collectionName !== "diagnostics") {
1357+ createDiagnostic(catalog, {
1358+ idSeed: {
1359+ collectionName,
1360+ id,
1361+ existingHash,
1362+ nextHash,
1363+ },
1364+ level: "error",
1365+ code: "merge_conflict_preserved_first",
1366+ message: `Conflicting ${String(collectionName)} entry for ${id}; preserved first value`,
1367+ provenance: "provenance" in existing
1368+ ? (existing as { provenance: ImportDiagnostic["provenance"] }).provenance
1369+ : [],
1370+ });
1371+ }
1372+ }
1373+ };
1374+
1375+ for (const fragment of fragments) {
1376+ mergeRecord("documents", fragment.documents);
1377+ mergeRecord("resources", fragment.resources);
1378+ mergeRecord("scopes", fragment.scopes);
1379+ mergeRecord("symbols", fragment.symbols);
1380+ mergeRecord("capabilities", fragment.capabilities);
1381+ mergeRecord("executables", fragment.executables);
1382+ mergeRecord("responseSets", fragment.responseSets);
1383+ mergeRecord("diagnostics", fragment.diagnostics);
1384+ }
1385+
1386+ return catalog;
1387+};
1388+
1389+export const validateCatalogInvariants = (
1390+ catalog: CatalogV1,
1391+): CatalogInvariantViolation[] => {
1392+ const violations: CatalogInvariantViolation[] = [];
1393+
1394+ for (const symbol of Object.values(catalog.symbols)) {
1395+ if (symbol.provenance.length === 0) {
1396+ violations.push({
1397+ code: "missing_symbol_provenance",
1398+ entityId: symbol.id,
1399+ message: `Symbol ${symbol.id} is missing provenance`,
1400+ });
1401+ }
1402+
1403+ if (symbol.kind === "shape") {
1404+ if (!symbol.resourceId && !symbol.synthetic) {
1405+ violations.push({
1406+ code: "missing_resource_context",
1407+ entityId: symbol.id,
1408+ message: `Shape ${symbol.id} must belong to a resource or be synthetic`,
1409+ });
1410+ }
1411+
1412+ if (symbol.node.type === "ref" && !catalog.symbols[symbol.node.target]) {
1413+ violations.push({
1414+ code: "missing_reference_target",
1415+ entityId: symbol.id,
1416+ message: `Shape ${symbol.id} references missing shape ${symbol.node.target}`,
1417+ });
1418+ }
1419+
1420+ if (symbol.node.type === "unknown" && symbol.node.reason?.includes("unresolved")) {
1421+ const hasDiagnostic = (symbol.diagnosticIds ?? [])
1422+ .map((diagnosticId) => catalog.diagnostics[diagnosticId])
1423+ .some((diagnostic) => diagnostic?.code === "unresolved_ref");
1424+
1425+ if (!hasDiagnostic) {
1426+ violations.push({
1427+ code: "missing_unresolved_ref_diagnostic",
1428+ entityId: symbol.id,
1429+ message: `Shape ${symbol.id} is unresolved but has no unresolved_ref diagnostic`,
1430+ });
1431+ }
1432+ }
1433+
1434+ if (symbol.synthetic && symbol.resourceId && !catalog.resources[symbol.resourceId]) {
1435+ violations.push({
1436+ code: "missing_resource_context",
1437+ entityId: symbol.id,
1438+ message: `Synthetic shape ${symbol.id} references missing resource ${symbol.resourceId}`,
1439+ });
1440+ }
1441+ }
1442+ }
1443+
1444+ for (const collection of [
1445+ ...Object.values(catalog.resources),
1446+ ...Object.values(catalog.scopes),
1447+ ...Object.values(catalog.capabilities),
1448+ ...Object.values(catalog.executables),
1449+ ...Object.values(catalog.responseSets),
1450+ ]) {
1451+ if ("provenance" in collection && collection.provenance.length === 0) {
1452+ violations.push({
1453+ code: "missing_entity_provenance",
1454+ entityId: "id" in collection ? collection.id : undefined,
1455+ message: `Entity ${"id" in collection ? collection.id : "unknown"} is missing provenance`,
1456+ });
1457+ }
1458+ }
1459+
1460+ for (const capability of Object.values(catalog.capabilities)) {
1461+ if (!catalog.scopes[capability.serviceScopeId]) {
1462+ violations.push({
1463+ code: "missing_service_scope",
1464+ entityId: capability.id,
1465+ message: `Capability ${capability.id} references missing scope ${capability.serviceScopeId}`,
1466+ });
1467+ }
1468+
1469+ if (
1470+ capability.preferredExecutableId
1471+ && !capability.executableIds.includes(capability.preferredExecutableId)
1472+ ) {
1473+ violations.push({
1474+ code: "invalid_preferred_executable",
1475+ entityId: capability.id,
1476+ message: `Capability ${capability.id} preferred executable is not in executableIds`,
1477+ });
1478+ }
1479+
1480+ for (const executableId of capability.executableIds) {
1481+ if (!catalog.executables[executableId]) {
1482+ violations.push({
1483+ code: "missing_executable",
1484+ entityId: capability.id,
1485+ message: `Capability ${capability.id} references missing executable ${executableId}`,
1486+ });
1487+ }
1488+ }
1489+ }
1490+
1491+ for (const executable of Object.values(catalog.executables)) {
1492+ if (!catalog.capabilities[executable.capabilityId]) {
1493+ violations.push({
1494+ code: "missing_executable",
1495+ entityId: executable.id,
1496+ message: `Executable ${executable.id} references missing capability ${executable.capabilityId}`,
1497+ });
1498+ }
1499+
1500+ if (!catalog.scopes[executable.scopeId]) {
1501+ violations.push({
1502+ code: "missing_scope",
1503+ entityId: executable.id,
1504+ message: `Executable ${executable.id} references missing scope ${executable.scopeId}`,
1505+ });
1506+ }
1507+
1508+ if (!catalog.responseSets[executable.responseSetId]) {
1509+ violations.push({
1510+ code: "missing_response_set",
1511+ entityId: executable.id,
1512+ message: `Executable ${executable.id} references missing response set ${executable.responseSetId}`,
1513+ });
1514+ }
1515+ }
1516+
1517+ return violations;
1518+};
1519+
1520+export const projectCatalogForAgentSdk = (input: {
1521+ catalog: CatalogV1;
1522+}): ProjectedCatalog => {
1523+ const workingCatalog = cloneCatalog(input.catalog);
1524+ const toolDescriptors = {} as Record<CapabilityId, ToolDescriptor>;
1525+ const searchDocs = {} as Record<CapabilityId, ToolSearchDoc>;
1526+ const capabilityViews = {} as Record<CapabilityId, CapabilitySummaryView>;
1527+
1528+ const capabilities = Object.values(workingCatalog.capabilities)
1529+ .sort((left, right) => left.id.localeCompare(right.id));
1530+
1531+ for (const capability of capabilities) {
1532+ const descriptor = projectCapability(workingCatalog, capability);
1533+ toolDescriptors[capability.id] = descriptor;
1534+ searchDocs[capability.id] = searchDoc(workingCatalog, capability);
1535+ capabilityViews[capability.id] = summaryView(workingCatalog, capability, descriptor);
1536+ }
1537+
1538+ return {
1539+ catalog: workingCatalog,
1540+ toolDescriptors,
1541+ searchDocs,
1542+ capabilityViews,
1543+ };
1544+};
1545+
1546+export const projectSymbolShallowView = (
1547+ catalog: CatalogV1,
1548+ symbolId: SymbolId,
1549+): SymbolShallowView => {
1550+ const symbol = catalog.symbols[symbolId];
1551+ if (!symbol) {
1552+ throw new Error(`Unknown symbol ${symbolId}`);
1553+ }
1554+
1555+ return {
1556+ symbolId,
1557+ kind: symbol.kind,
1558+ ...(symbolTitle(symbol) ? { title: symbolTitle(symbol) } : {}),
1559+ ...(symbolSummary(symbol) ? { summary: symbolSummary(symbol) } : {}),
1560+ edges: symbolEdges(symbol),
1561+ };
1562+};
1563+
1564+export const projectSymbolExpandedView = (
1565+ catalog: CatalogV1,
1566+ symbolId: SymbolId,
1567+): SymbolExpandedView => {
1568+ const symbol = catalog.symbols[symbolId];
1569+ if (!symbol) {
1570+ throw new Error(`Unknown symbol ${symbolId}`);
1571+ }
1572+
1573+ return {
1574+ symbolId,
1575+ symbol,
1576+ ...(symbol.diagnosticIds
1577+ ? {
1578+ diagnostics: symbol.diagnosticIds
1579+ .map((diagnosticId) => catalog.diagnostics[diagnosticId])
1580+ .filter((diagnostic): diagnostic is ImportDiagnostic => diagnostic !== undefined),
1581+ }
1582+ : {}),
1583+ };
1584+};
1585+
1586+export const buildInvocationPlan = (input: {
1587+ catalog: CatalogV1;
1588+ capabilityId: CapabilityId;
1589+ executableId?: ExecutableId;
1590+ connectionBindingId: string;
1591+ resolvedEndpoint?: string;
1592+ resolvedAuth?: unknown;
1593+ resolvedInteraction?: Partial<InvocationPlan["resolvedInteraction"]>;
1594+ requestPlan: unknown;
1595+}): InvocationPlan => {
1596+ const capability = input.catalog.capabilities[input.capabilityId];
1597+ if (!capability) {
1598+ throw new Error(`Unknown capability ${input.capabilityId}`);
1599+ }
1600+
1601+ const executable = input.executableId
1602+ ? input.catalog.executables[input.executableId]
1603+ : chooseExecutable(input.catalog, capability);
1604+ if (!executable) {
1605+ throw new Error(`Unknown executable for capability ${input.capabilityId}`);
1606+ }
1607+
1608+ const scopes = scopeChain(input.catalog, executable.scopeId).map((scope) => scope.id);
1609+
1610+ return {
1611+ capabilityId: capability.id,
1612+ executableId: executable.id,
1613+ connectionBindingId: input.connectionBindingId,
1614+ resolvedScopeChain: scopes,
1615+ ...(input.resolvedEndpoint ? { resolvedEndpoint: input.resolvedEndpoint } : {}),
1616+ ...(input.resolvedAuth !== undefined ? { resolvedAuth: input.resolvedAuth } : {}),
1617+ resolvedInteraction: {
1618+ requiresApproval: input.resolvedInteraction?.requiresApproval
1619+ ?? capability.interaction.approval.mayRequire,
1620+ mayPauseForElicitation: input.resolvedInteraction?.mayPauseForElicitation
1621+ ?? capability.interaction.elicitation.mayRequest,
1622+ resumable: input.resolvedInteraction?.resumable
1623+ ?? capability.interaction.resume.supported,
1624+ },
1625+ requestPlan: input.requestPlan,
1626+ provenance: capability.provenance,
1627+ };
1628+};
packages/control-plane/src/ir/ids.ts+58−0
@@ -0,0 +1,58 @@
1+import { Schema } from "effect";
2+
3+export const DocumentIdSchema = Schema.String.pipe(Schema.brand("DocumentId"));
4+export const ResourceIdSchema = Schema.String.pipe(Schema.brand("ResourceId"));
5+export const ScopeIdSchema = Schema.String.pipe(Schema.brand("ScopeId"));
6+export const CapabilityIdSchema = Schema.String.pipe(Schema.brand("CapabilityId"));
7+export const ExecutableIdSchema = Schema.String.pipe(Schema.brand("ExecutableId"));
8+export const ResponseSetIdSchema = Schema.String.pipe(Schema.brand("ResponseSetId"));
9+export const DiagnosticIdSchema = Schema.String.pipe(Schema.brand("DiagnosticId"));
10+
11+export const ShapeSymbolIdSchema = Schema.String.pipe(Schema.brand("ShapeSymbolId"));
12+export const ParameterSymbolIdSchema = Schema.String.pipe(Schema.brand("ParameterSymbolId"));
13+export const RequestBodySymbolIdSchema = Schema.String.pipe(
14+ Schema.brand("RequestBodySymbolId"),
15+);
16+export const ResponseSymbolIdSchema = Schema.String.pipe(
17+ Schema.brand("ResponseSymbolId"),
18+);
19+export const HeaderSymbolIdSchema = Schema.String.pipe(Schema.brand("HeaderSymbolId"));
20+export const ExampleSymbolIdSchema = Schema.String.pipe(Schema.brand("ExampleSymbolId"));
21+export const SecuritySchemeSymbolIdSchema = Schema.String.pipe(
22+ Schema.brand("SecuritySchemeSymbolId"),
23+);
24+
25+export type DocumentId = typeof DocumentIdSchema.Type;
26+export type ResourceId = typeof ResourceIdSchema.Type;
27+export type ScopeId = typeof ScopeIdSchema.Type;
28+export type CapabilityId = typeof CapabilityIdSchema.Type;
29+export type ExecutableId = typeof ExecutableIdSchema.Type;
30+export type ResponseSetId = typeof ResponseSetIdSchema.Type;
31+export type DiagnosticId = typeof DiagnosticIdSchema.Type;
32+
33+export type ShapeSymbolId = typeof ShapeSymbolIdSchema.Type;
34+export type ParameterSymbolId = typeof ParameterSymbolIdSchema.Type;
35+export type RequestBodySymbolId = typeof RequestBodySymbolIdSchema.Type;
36+export type ResponseSymbolId = typeof ResponseSymbolIdSchema.Type;
37+export type HeaderSymbolId = typeof HeaderSymbolIdSchema.Type;
38+export type ExampleSymbolId = typeof ExampleSymbolIdSchema.Type;
39+export type SecuritySchemeSymbolId = typeof SecuritySchemeSymbolIdSchema.Type;
40+
41+export type SymbolId =
42+ | ShapeSymbolId
43+ | ParameterSymbolId
44+ | RequestBodySymbolId
45+ | ResponseSymbolId
46+ | HeaderSymbolId
47+ | ExampleSymbolId
48+ | SecuritySchemeSymbolId;
49+
50+export const SymbolIdSchema = Schema.Union(
51+ ShapeSymbolIdSchema,
52+ ParameterSymbolIdSchema,
53+ RequestBodySymbolIdSchema,
54+ ResponseSymbolIdSchema,
55+ HeaderSymbolIdSchema,
56+ ExampleSymbolIdSchema,
57+ SecuritySchemeSymbolIdSchema,
58+);
packages/control-plane/src/ir/index.ts+3−0
@@ -0,0 +1,3 @@
1+export * from "./ids";
2+export * from "./model";
3+export * from "./catalog";
packages/control-plane/src/ir/model.ts+962−0
@@ -0,0 +1,962 @@
1+import { Schema } from "effect";
2+
3+import {
4+ CapabilityIdSchema,
5+ type CapabilityId,
6+ DiagnosticIdSchema,
7+ type DiagnosticId,
8+ DocumentIdSchema,
9+ type DocumentId,
10+ ExecutableIdSchema,
11+ type ExecutableId,
12+ ExampleSymbolIdSchema,
13+ type ExampleSymbolId,
14+ HeaderSymbolIdSchema,
15+ type HeaderSymbolId,
16+ ParameterSymbolIdSchema,
17+ type ParameterSymbolId,
18+ RequestBodySymbolIdSchema,
19+ type RequestBodySymbolId,
20+ ResourceIdSchema,
21+ type ResourceId,
22+ ResponseSetIdSchema,
23+ type ResponseSetId,
24+ ResponseSymbolIdSchema,
25+ type ResponseSymbolId,
26+ ScopeIdSchema,
27+ type ScopeId,
28+ SecuritySchemeSymbolIdSchema,
29+ type SecuritySchemeSymbolId,
30+ ShapeSymbolIdSchema,
31+ type ShapeSymbolId,
32+ SymbolIdSchema,
33+ type SymbolId,
34+} from "./ids";
35+
36+export const SourceKindSchema = Schema.Literal(
37+ "openapi",
38+ "graphql-schema",
39+ "google-discovery",
40+ "mcp",
41+ "custom",
42+);
43+
44+export const ScopeKindSchema = Schema.Literal(
45+ "service",
46+ "document",
47+ "resource",
48+ "pathItem",
49+ "operation",
50+ "folder",
51+);
52+
53+export const EffectKindSchema = Schema.Literal(
54+ "read",
55+ "write",
56+ "delete",
57+ "action",
58+ "subscribe",
59+);
60+
61+export const ParameterLocationSchema = Schema.Literal(
62+ "path",
63+ "query",
64+ "header",
65+ "cookie",
66+);
67+
68+export const ResponseTraitSchema = Schema.Literal(
69+ "success",
70+ "redirect",
71+ "stream",
72+ "download",
73+ "upload",
74+ "longRunning",
75+);
76+
77+export const SecuritySchemeTypeSchema = Schema.Literal(
78+ "oauth2",
79+ "http",
80+ "apiKey",
81+ "basic",
82+ "bearer",
83+ "custom",
84+);
85+
86+export const SelectionModeSchema = Schema.Literal(
87+ "caller",
88+ "fixed",
89+ "persisted",
90+);
91+
92+export const StatusRangeSchema = Schema.Literal(
93+ "1XX",
94+ "2XX",
95+ "3XX",
96+ "4XX",
97+ "5XX",
98+);
99+
100+export const PaginationHintKindSchema = Schema.Literal(
101+ "cursor",
102+ "offset",
103+ "token",
104+ "unknown",
105+);
106+
107+export const DiagnosticLevelSchema = Schema.Literal(
108+ "info",
109+ "warning",
110+ "error",
111+);
112+
113+export const DiagnosticCodeSchema = Schema.Literal(
114+ "external_ref_bundled",
115+ "relative_ref_rebased",
116+ "schema_hoisted",
117+ "selection_shape_synthesized",
118+ "opaque_hook_imported",
119+ "discriminator_lost",
120+ "multi_response_union_synthesized",
121+ "unsupported_link_preserved_native",
122+ "unsupported_callback_preserved_native",
123+ "unresolved_ref",
124+ "merge_conflict_preserved_first",
125+ "projection_call_shape_synthesized",
126+ "projection_result_shape_synthesized",
127+ "projection_collision_grouped_fields",
128+ "synthetic_resource_context_created",
129+ "selection_shape_missing",
130+);
131+
132+export const NativeEncodingSchema = Schema.Literal(
133+ "json",
134+ "yaml",
135+ "graphql",
136+ "text",
137+ "unknown",
138+);
139+
140+export const ProvenanceRelationSchema = Schema.Literal(
141+ "declared",
142+ "hoisted",
143+ "derived",
144+ "merged",
145+ "projected",
146+);
147+
148+export const DocumentationBlockSchema = Schema.Struct({
149+ summary: Schema.optional(Schema.String),
150+ description: Schema.optional(Schema.String),
151+ externalDocsUrl: Schema.optional(Schema.String),
152+});
153+
154+export const ProvenanceRefSchema = Schema.Struct({
155+ relation: ProvenanceRelationSchema,
156+ documentId: DocumentIdSchema,
157+ resourceId: Schema.optional(ResourceIdSchema),
158+ pointer: Schema.optional(Schema.String),
159+ line: Schema.optional(Schema.Number),
160+ column: Schema.optional(Schema.Number),
161+});
162+
163+export const NativeBlobSchema = Schema.Struct({
164+ sourceKind: SourceKindSchema,
165+ kind: Schema.String,
166+ pointer: Schema.optional(Schema.String),
167+ encoding: Schema.optional(NativeEncodingSchema),
168+ summary: Schema.optional(Schema.String),
169+ value: Schema.optional(Schema.Unknown),
170+});
171+
172+export const EntityBaseSchema = Schema.Struct({
173+ synthetic: Schema.Boolean,
174+ provenance: Schema.Array(ProvenanceRefSchema),
175+ diagnosticIds: Schema.optional(Schema.Array(DiagnosticIdSchema)),
176+ native: Schema.optional(Schema.Array(NativeBlobSchema)),
177+});
178+
179+export const ImportMetadataSchema = Schema.Struct({
180+ sourceKind: SourceKindSchema,
181+ adapterKey: Schema.String,
182+ importerVersion: Schema.String,
183+ importedAt: Schema.String,
184+ sourceConfigHash: Schema.String,
185+});
186+
187+export const SourceDocumentSchema = Schema.Struct({
188+ id: DocumentIdSchema,
189+ kind: SourceKindSchema,
190+ title: Schema.optional(Schema.String),
191+ versionHint: Schema.optional(Schema.String),
192+ fetchedAt: Schema.String,
193+ rawRef: Schema.String,
194+ entryUri: Schema.optional(Schema.String),
195+ native: Schema.optional(Schema.Array(NativeBlobSchema)),
196+});
197+
198+export const FieldSpecSchema = Schema.Struct({
199+ shapeId: ShapeSymbolIdSchema,
200+ docs: Schema.optional(DocumentationBlockSchema),
201+ deprecated: Schema.optional(Schema.Boolean),
202+ exampleIds: Schema.optional(Schema.Array(ExampleSymbolIdSchema)),
203+});
204+
205+export const DiscriminatorSpecSchema = Schema.Struct({
206+ propertyName: Schema.String,
207+ mapping: Schema.optional(
208+ Schema.Record({
209+ key: Schema.String,
210+ value: ShapeSymbolIdSchema,
211+ }),
212+ ),
213+});
214+
215+export const ScalarShapeSchema = Schema.Struct({
216+ type: Schema.Literal("scalar"),
217+ scalar: Schema.Literal("string", "number", "integer", "boolean", "null", "bytes"),
218+ format: Schema.optional(Schema.String),
219+ constraints: Schema.optional(
220+ Schema.Record({
221+ key: Schema.String,
222+ value: Schema.Unknown,
223+ }),
224+ ),
225+});
226+
227+export const UnknownShapeSchema = Schema.Struct({
228+ type: Schema.Literal("unknown"),
229+ reason: Schema.optional(Schema.String),
230+});
231+
232+export const ConstShapeSchema = Schema.Struct({
233+ type: Schema.Literal("const"),
234+ value: Schema.Unknown,
235+});
236+
237+export const EnumShapeSchema = Schema.Struct({
238+ type: Schema.Literal("enum"),
239+ values: Schema.Array(Schema.Unknown),
240+});
241+
242+export const ObjectShapeSchema = Schema.Struct({
243+ type: Schema.Literal("object"),
244+ fields: Schema.Record({
245+ key: Schema.String,
246+ value: FieldSpecSchema,
247+ }),
248+ required: Schema.optional(Schema.Array(Schema.String)),
249+ additionalProperties: Schema.optional(
250+ Schema.Union(Schema.Boolean, ShapeSymbolIdSchema),
251+ ),
252+ patternProperties: Schema.optional(
253+ Schema.Record({
254+ key: Schema.String,
255+ value: ShapeSymbolIdSchema,
256+ }),
257+ ),
258+});
259+
260+export const ArrayShapeSchema = Schema.Struct({
261+ type: Schema.Literal("array"),
262+ itemShapeId: ShapeSymbolIdSchema,
263+ minItems: Schema.optional(Schema.Number),
264+ maxItems: Schema.optional(Schema.Number),
265+});
266+
267+export const TupleShapeSchema = Schema.Struct({
268+ type: Schema.Literal("tuple"),
269+ itemShapeIds: Schema.Array(ShapeSymbolIdSchema),
270+ additionalItems: Schema.optional(
271+ Schema.Union(Schema.Boolean, ShapeSymbolIdSchema),
272+ ),
273+});
274+
275+export const MapShapeSchema = Schema.Struct({
276+ type: Schema.Literal("map"),
277+ valueShapeId: ShapeSymbolIdSchema,
278+});
279+
280+export const AllOfShapeSchema = Schema.Struct({
281+ type: Schema.Literal("allOf"),
282+ items: Schema.Array(ShapeSymbolIdSchema),
283+});
284+
285+export const AnyOfShapeSchema = Schema.Struct({
286+ type: Schema.Literal("anyOf"),
287+ items: Schema.Array(ShapeSymbolIdSchema),
288+});
289+
290+export const OneOfShapeSchema = Schema.Struct({
291+ type: Schema.Literal("oneOf"),
292+ items: Schema.Array(ShapeSymbolIdSchema),
293+ discriminator: Schema.optional(DiscriminatorSpecSchema),
294+});
295+
296+export const NullableShapeSchema = Schema.Struct({
297+ type: Schema.Literal("nullable"),
298+ itemShapeId: ShapeSymbolIdSchema,
299+});
300+
301+export const RefShapeSchema = Schema.Struct({
302+ type: Schema.Literal("ref"),
303+ target: ShapeSymbolIdSchema,
304+});
305+
306+export const NotShapeSchema = Schema.Struct({
307+ type: Schema.Literal("not"),
308+ itemShapeId: ShapeSymbolIdSchema,
309+});
310+
311+export const ConditionalShapeSchema = Schema.Struct({
312+ type: Schema.Literal("conditional"),
313+ ifShapeId: ShapeSymbolIdSchema,
314+ thenShapeId: Schema.optional(ShapeSymbolIdSchema),
315+ elseShapeId: Schema.optional(ShapeSymbolIdSchema),
316+});
317+
318+export const GraphQLInterfaceShapeSchema = Schema.Struct({
319+ type: Schema.Literal("graphqlInterface"),
320+ fields: Schema.Record({
321+ key: Schema.String,
322+ value: FieldSpecSchema,
323+ }),
324+ possibleTypeIds: Schema.Array(ShapeSymbolIdSchema),
325+});
326+
327+export const GraphQLUnionShapeSchema = Schema.Struct({
328+ type: Schema.Literal("graphqlUnion"),
329+ memberTypeIds: Schema.Array(ShapeSymbolIdSchema),
330+});
331+
332+export const ShapeNodeSchema = Schema.Union(
333+ UnknownShapeSchema,
334+ ScalarShapeSchema,
335+ ConstShapeSchema,
336+ EnumShapeSchema,
337+ ObjectShapeSchema,
338+ ArrayShapeSchema,
339+ TupleShapeSchema,
340+ MapShapeSchema,
341+ AllOfShapeSchema,
342+ AnyOfShapeSchema,
343+ OneOfShapeSchema,
344+ NullableShapeSchema,
345+ RefShapeSchema,
346+ NotShapeSchema,
347+ ConditionalShapeSchema,
348+ GraphQLInterfaceShapeSchema,
349+ GraphQLUnionShapeSchema,
350+);
351+
352+export const AnchorTargetSchema = Schema.Struct({
353+ shapeId: ShapeSymbolIdSchema,
354+ pointer: Schema.optional(Schema.String),
355+});
356+
357+export const SchemaResourceSchema = Schema.extend(
358+ Schema.Struct({
359+ id: ResourceIdSchema,
360+ documentId: DocumentIdSchema,
361+ canonicalUri: Schema.String,
362+ baseUri: Schema.String,
363+ dialectUri: Schema.optional(Schema.String),
364+ rootShapeId: Schema.optional(ShapeSymbolIdSchema),
365+ anchors: Schema.Record({
366+ key: Schema.String,
367+ value: AnchorTargetSchema,
368+ }),
369+ dynamicAnchors: Schema.Record({
370+ key: Schema.String,
371+ value: AnchorTargetSchema,
372+ }),
373+ }),
374+ EntityBaseSchema,
375+);
376+
377+export const RequestPlacementHintSchema = Schema.Union(
378+ Schema.Struct({
379+ location: Schema.Literal("header", "query", "cookie"),
380+ name: Schema.String,
381+ }),
382+ Schema.Struct({
383+ location: Schema.Literal("body"),
384+ path: Schema.String,
385+ }),
386+);
387+
388+export const OAuthFlowSchema = Schema.Struct({
389+ authorizationUrl: Schema.optional(Schema.String),
390+ tokenUrl: Schema.optional(Schema.String),
391+ refreshUrl: Schema.optional(Schema.String),
392+ scopes: Schema.optional(
393+ Schema.Record({
394+ key: Schema.String,
395+ value: Schema.String,
396+ }),
397+ ),
398+});
399+
400+export const SecurityPlacementSchema = Schema.Struct({
401+ in: Schema.optional(Schema.Literal("header", "query", "cookie")),
402+ name: Schema.optional(Schema.String),
403+});
404+
405+export const SecuritySchemeSymbolSchema = Schema.extend(
406+ Schema.Struct({
407+ id: SecuritySchemeSymbolIdSchema,
408+ kind: Schema.Literal("securityScheme"),
409+ schemeType: SecuritySchemeTypeSchema,
410+ docs: Schema.optional(DocumentationBlockSchema),
411+ placement: Schema.optional(SecurityPlacementSchema),
412+ http: Schema.optional(
413+ Schema.Struct({
414+ scheme: Schema.String,
415+ bearerFormat: Schema.optional(Schema.String),
416+ }),
417+ ),
418+ apiKey: Schema.optional(
419+ Schema.Struct({
420+ in: Schema.Literal("header", "query", "cookie"),
421+ name: Schema.String,
422+ }),
423+ ),
424+ oauth: Schema.optional(
425+ Schema.Struct({
426+ flows: Schema.optional(
427+ Schema.Record({
428+ key: Schema.String,
429+ value: OAuthFlowSchema,
430+ }),
431+ ),
432+ scopes: Schema.optional(
433+ Schema.Record({
434+ key: Schema.String,
435+ value: Schema.String,
436+ }),
437+ ),
438+ }),
439+ ),
440+ custom: Schema.optional(
441+ Schema.Struct({
442+ placementHints: Schema.optional(Schema.Array(RequestPlacementHintSchema)),
443+ }),
444+ ),
445+ }),
446+ EntityBaseSchema,
447+);
448+
449+export const EncodingSpecSchema = Schema.Struct({
450+ contentType: Schema.optional(Schema.String),
451+ style: Schema.optional(Schema.String),
452+ explode: Schema.optional(Schema.Boolean),
453+ allowReserved: Schema.optional(Schema.Boolean),
454+ headers: Schema.optional(Schema.Array(HeaderSymbolIdSchema)),
455+});
456+
457+export const ContentSpecSchema = Schema.Struct({
458+ mediaType: Schema.String,
459+ shapeId: Schema.optional(ShapeSymbolIdSchema),
460+ exampleIds: Schema.optional(Schema.Array(ExampleSymbolIdSchema)),
461+ encoding: Schema.optional(
462+ Schema.Record({
463+ key: Schema.String,
464+ value: EncodingSpecSchema,
465+ }),
466+ ),
467+});
468+
469+export const ShapeSymbolSchema = Schema.extend(
470+ Schema.Struct({
471+ id: ShapeSymbolIdSchema,
472+ kind: Schema.Literal("shape"),
473+ resourceId: Schema.optional(ResourceIdSchema),
474+ title: Schema.optional(Schema.String),
475+ docs: Schema.optional(DocumentationBlockSchema),
476+ deprecated: Schema.optional(Schema.Boolean),
477+ node: ShapeNodeSchema,
478+ }),
479+ EntityBaseSchema,
480+);
481+
482+export const ParameterSymbolSchema = Schema.extend(
483+ Schema.Struct({
484+ id: ParameterSymbolIdSchema,
485+ kind: Schema.Literal("parameter"),
486+ name: Schema.String,
487+ location: ParameterLocationSchema,
488+ required: Schema.optional(Schema.Boolean),
489+ docs: Schema.optional(DocumentationBlockSchema),
490+ deprecated: Schema.optional(Schema.Boolean),
491+ exampleIds: Schema.optional(Schema.Array(ExampleSymbolIdSchema)),
492+ schemaShapeId: Schema.optional(ShapeSymbolIdSchema),
493+ content: Schema.optional(Schema.Array(ContentSpecSchema)),
494+ style: Schema.optional(Schema.String),
495+ explode: Schema.optional(Schema.Boolean),
496+ allowReserved: Schema.optional(Schema.Boolean),
497+ }),
498+ EntityBaseSchema,
499+);
500+
501+export const HeaderSymbolSchema = Schema.extend(
502+ Schema.Struct({
503+ id: HeaderSymbolIdSchema,
504+ kind: Schema.Literal("header"),
505+ name: Schema.String,
506+ docs: Schema.optional(DocumentationBlockSchema),
507+ deprecated: Schema.optional(Schema.Boolean),
508+ exampleIds: Schema.optional(Schema.Array(ExampleSymbolIdSchema)),
509+ schemaShapeId: Schema.optional(ShapeSymbolIdSchema),
510+ content: Schema.optional(Schema.Array(ContentSpecSchema)),
511+ style: Schema.optional(Schema.String),
512+ explode: Schema.optional(Schema.Boolean),
513+ }),
514+ EntityBaseSchema,
515+);
516+
517+export const RequestBodySymbolSchema = Schema.extend(
518+ Schema.Struct({
519+ id: RequestBodySymbolIdSchema,
520+ kind: Schema.Literal("requestBody"),
521+ docs: Schema.optional(DocumentationBlockSchema),
522+ required: Schema.optional(Schema.Boolean),
523+ contents: Schema.Array(ContentSpecSchema),
524+ }),
525+ EntityBaseSchema,
526+);
527+
528+export const ResponseSymbolSchema = Schema.extend(
529+ Schema.Struct({
530+ id: ResponseSymbolIdSchema,
531+ kind: Schema.Literal("response"),
532+ docs: Schema.optional(DocumentationBlockSchema),
533+ headerIds: Schema.optional(Schema.Array(HeaderSymbolIdSchema)),
534+ contents: Schema.optional(Schema.Array(ContentSpecSchema)),
535+ }),
536+ EntityBaseSchema,
537+);
538+
539+export const ExampleSymbolSchema = Schema.extend(
540+ Schema.Struct({
541+ id: ExampleSymbolIdSchema,
542+ kind: Schema.Literal("example"),
543+ name: Schema.optional(Schema.String),
544+ docs: Schema.optional(DocumentationBlockSchema),
545+ exampleKind: Schema.Literal("value", "call"),
546+ value: Schema.optional(Schema.Unknown),
547+ externalValue: Schema.optional(Schema.String),
548+ call: Schema.optional(
549+ Schema.Struct({
550+ args: Schema.Record({
551+ key: Schema.String,
552+ value: Schema.Unknown,
553+ }),
554+ result: Schema.optional(Schema.Unknown),
555+ }),
556+ ),
557+ }),
558+ EntityBaseSchema,
559+);
560+
561+export const SymbolSchema = Schema.Union(
562+ ShapeSymbolSchema,
563+ ParameterSymbolSchema,
564+ RequestBodySymbolSchema,
565+ ResponseSymbolSchema,
566+ HeaderSymbolSchema,
567+ ExampleSymbolSchema,
568+ SecuritySchemeSymbolSchema,
569+);
570+
571+export type AuthRequirement =
572+ | {
573+ readonly kind: "none";
574+ }
575+ | {
576+ readonly kind: "scheme";
577+ readonly schemeId: SecuritySchemeSymbolId;
578+ readonly scopes?: readonly string[];
579+ }
580+ | {
581+ readonly kind: "allOf";
582+ readonly items: readonly AuthRequirement[];
583+ }
584+ | {
585+ readonly kind: "anyOf";
586+ readonly items: readonly AuthRequirement[];
587+ };
588+
589+export const AuthRequirementSchema = Schema.suspend(() =>
590+ Schema.Union(
591+ Schema.Struct({
592+ kind: Schema.Literal("none"),
593+ }),
594+ Schema.Struct({
595+ kind: Schema.Literal("scheme"),
596+ schemeId: SecuritySchemeSymbolIdSchema,
597+ scopes: Schema.optional(Schema.Array(Schema.String)),
598+ }),
599+ Schema.Struct({
600+ kind: Schema.Literal("allOf"),
601+ items: Schema.Array(AuthRequirementSchema),
602+ }),
603+ Schema.Struct({
604+ kind: Schema.Literal("anyOf"),
605+ items: Schema.Array(AuthRequirementSchema),
606+ }),
607+ )
608+) as Schema.Schema<AuthRequirement, AuthRequirement, never>;
609+
610+export const ServerSpecSchema = Schema.Struct({
611+ url: Schema.String,
612+ description: Schema.optional(Schema.String),
613+ variables: Schema.optional(
614+ Schema.Record({
615+ key: Schema.String,
616+ value: Schema.String,
617+ }),
618+ ),
619+});
620+
621+export const ScopeDefaultsSchema = Schema.Struct({
622+ servers: Schema.optional(Schema.Array(ServerSpecSchema)),
623+ auth: Schema.optional(AuthRequirementSchema),
624+ parameterIds: Schema.optional(Schema.Array(ParameterSymbolIdSchema)),
625+ headerIds: Schema.optional(Schema.Array(HeaderSymbolIdSchema)),
626+ variables: Schema.optional(
627+ Schema.Record({
628+ key: Schema.String,
629+ value: Schema.String,
630+ }),
631+ ),
632+});
633+
634+export const ScopeSchema = Schema.extend(
635+ Schema.Struct({
636+ id: ScopeIdSchema,
637+ kind: ScopeKindSchema,
638+ parentId: Schema.optional(ScopeIdSchema),
639+ name: Schema.optional(Schema.String),
640+ namespace: Schema.optional(Schema.String),
641+ docs: Schema.optional(DocumentationBlockSchema),
642+ defaults: Schema.optional(ScopeDefaultsSchema),
643+ }),
644+ EntityBaseSchema,
645+);
646+
647+export const InteractionSpecSchema = Schema.Struct({
648+ approval: Schema.Struct({
649+ mayRequire: Schema.Boolean,
650+ reasons: Schema.optional(
651+ Schema.Array(
652+ Schema.Literal("write", "delete", "sensitive", "externalSideEffect"),
653+ ),
654+ ),
655+ }),
656+ elicitation: Schema.Struct({
657+ mayRequest: Schema.Boolean,
658+ shapeId: Schema.optional(ShapeSymbolIdSchema),
659+ }),
660+ resume: Schema.Struct({
661+ supported: Schema.Boolean,
662+ }),
663+});
664+
665+export const CapabilitySchema = Schema.extend(
666+ Schema.Struct({
667+ id: CapabilityIdSchema,
668+ serviceScopeId: ScopeIdSchema,
669+ surface: Schema.Struct({
670+ toolPath: Schema.Array(Schema.String),
671+ title: Schema.optional(Schema.String),
672+ summary: Schema.optional(Schema.String),
673+ description: Schema.optional(Schema.String),
674+ aliases: Schema.optional(Schema.Array(Schema.String)),
675+ tags: Schema.optional(Schema.Array(Schema.String)),
676+ }),
677+ semantics: Schema.Struct({
678+ effect: EffectKindSchema,
679+ safe: Schema.optional(Schema.Boolean),
680+ idempotent: Schema.optional(Schema.Boolean),
681+ destructive: Schema.optional(Schema.Boolean),
682+ }),
683+ docs: Schema.optional(DocumentationBlockSchema),
684+ auth: AuthRequirementSchema,
685+ interaction: InteractionSpecSchema,
686+ executableIds: Schema.Array(ExecutableIdSchema),
687+ preferredExecutableId: Schema.optional(ExecutableIdSchema),
688+ exampleIds: Schema.optional(Schema.Array(ExampleSymbolIdSchema)),
689+ }),
690+ EntityBaseSchema,
691+);
692+
693+export const HttpExecutableSchema = Schema.extend(
694+ Schema.Struct({
695+ id: ExecutableIdSchema,
696+ protocol: Schema.Literal("http"),
697+ capabilityId: CapabilityIdSchema,
698+ scopeId: ScopeIdSchema,
699+ method: Schema.String,
700+ pathTemplate: Schema.String,
701+ pathParameterIds: Schema.optional(Schema.Array(ParameterSymbolIdSchema)),
702+ queryParameterIds: Schema.optional(Schema.Array(ParameterSymbolIdSchema)),
703+ headerParameterIds: Schema.optional(Schema.Array(ParameterSymbolIdSchema)),
704+ cookieParameterIds: Schema.optional(Schema.Array(ParameterSymbolIdSchema)),
705+ requestBodyId: Schema.optional(RequestBodySymbolIdSchema),
706+ responseSetId: ResponseSetIdSchema,
707+ }),
708+ EntityBaseSchema,
709+);
710+
711+export const GraphQLExecutableSchema = Schema.extend(
712+ Schema.Struct({
713+ id: ExecutableIdSchema,
714+ protocol: Schema.Literal("graphql"),
715+ capabilityId: CapabilityIdSchema,
716+ scopeId: ScopeIdSchema,
717+ operationType: Schema.Literal("query", "mutation", "subscription"),
718+ rootField: Schema.String,
719+ argumentShapeId: ShapeSymbolIdSchema,
720+ resultShapeId: ShapeSymbolIdSchema,
721+ selectionShapeId: Schema.optional(ShapeSymbolIdSchema),
722+ selectionMode: SelectionModeSchema,
723+ responseSetId: ResponseSetIdSchema,
724+ }),
725+ EntityBaseSchema,
726+);
727+
728+export const McpExecutableSchema = Schema.extend(
729+ Schema.Struct({
730+ id: ExecutableIdSchema,
731+ protocol: Schema.Literal("mcp"),
732+ capabilityId: CapabilityIdSchema,
733+ scopeId: ScopeIdSchema,
734+ serverRef: Schema.String,
735+ toolName: Schema.String,
736+ inputShapeId: Schema.optional(ShapeSymbolIdSchema),
737+ outputShapeId: Schema.optional(ShapeSymbolIdSchema),
738+ responseSetId: ResponseSetIdSchema,
739+ }),
740+ EntityBaseSchema,
741+);
742+
743+export const ExecutableSchema = Schema.Union(
744+ HttpExecutableSchema,
745+ GraphQLExecutableSchema,
746+ McpExecutableSchema,
747+);
748+
749+export const PaginationHintSchema = Schema.Struct({
750+ kind: PaginationHintKindSchema,
751+ tokenParamName: Schema.optional(Schema.String),
752+ nextFieldPath: Schema.optional(Schema.String),
753+});
754+
755+export const StatusMatchSchema = Schema.Union(
756+ Schema.Struct({
757+ kind: Schema.Literal("exact"),
758+ status: Schema.Number,
759+ }),
760+ Schema.Struct({
761+ kind: Schema.Literal("range"),
762+ value: StatusRangeSchema,
763+ }),
764+ Schema.Struct({
765+ kind: Schema.Literal("default"),
766+ }),
767+);
768+
769+export const ResponseVariantSchema = Schema.Struct({
770+ match: StatusMatchSchema,
771+ responseId: ResponseSymbolIdSchema,
772+ traits: Schema.optional(Schema.Array(ResponseTraitSchema)),
773+ pagination: Schema.optional(PaginationHintSchema),
774+});
775+
776+export const ResponseSetSchema = Schema.extend(
777+ Schema.Struct({
778+ id: ResponseSetIdSchema,
779+ variants: Schema.Array(ResponseVariantSchema),
780+ }),
781+ EntityBaseSchema,
782+);
783+
784+export const ImportDiagnosticSchema = Schema.Struct({
785+ id: DiagnosticIdSchema,
786+ level: DiagnosticLevelSchema,
787+ code: DiagnosticCodeSchema,
788+ message: Schema.String,
789+ relatedSymbolIds: Schema.optional(Schema.Array(SymbolIdSchema)),
790+ provenance: Schema.Array(ProvenanceRefSchema),
791+});
792+
793+export const CatalogV1Schema = Schema.Struct({
794+ version: Schema.Literal("ir.v1"),
795+ documents: Schema.Record({
796+ key: DocumentIdSchema,
797+ value: SourceDocumentSchema,
798+ }),
799+ resources: Schema.Record({
800+ key: ResourceIdSchema,
801+ value: SchemaResourceSchema,
802+ }),
803+ scopes: Schema.Record({
804+ key: ScopeIdSchema,
805+ value: ScopeSchema,
806+ }),
807+ symbols: Schema.Record({
808+ key: Schema.String,
809+ value: SymbolSchema,
810+ }),
811+ capabilities: Schema.Record({
812+ key: CapabilityIdSchema,
813+ value: CapabilitySchema,
814+ }),
815+ executables: Schema.Record({
816+ key: ExecutableIdSchema,
817+ value: ExecutableSchema,
818+ }),
819+ responseSets: Schema.Record({
820+ key: ResponseSetIdSchema,
821+ value: ResponseSetSchema,
822+ }),
823+ diagnostics: Schema.Record({
824+ key: DiagnosticIdSchema,
825+ value: ImportDiagnosticSchema,
826+ }),
827+});
828+
829+export const CatalogFragmentV1Schema = Schema.Struct({
830+ version: Schema.Literal("ir.v1.fragment"),
831+ documents: Schema.optional(
832+ Schema.Record({
833+ key: DocumentIdSchema,
834+ value: SourceDocumentSchema,
835+ }),
836+ ),
837+ resources: Schema.optional(
838+ Schema.Record({
839+ key: ResourceIdSchema,
840+ value: SchemaResourceSchema,
841+ }),
842+ ),
843+ scopes: Schema.optional(
844+ Schema.Record({
845+ key: ScopeIdSchema,
846+ value: ScopeSchema,
847+ }),
848+ ),
849+ symbols: Schema.optional(
850+ Schema.Record({
851+ key: Schema.String,
852+ value: SymbolSchema,
853+ }),
854+ ),
855+ capabilities: Schema.optional(
856+ Schema.Record({
857+ key: CapabilityIdSchema,
858+ value: CapabilitySchema,
859+ }),
860+ ),
861+ executables: Schema.optional(
862+ Schema.Record({
863+ key: ExecutableIdSchema,
864+ value: ExecutableSchema,
865+ }),
866+ ),
867+ responseSets: Schema.optional(
868+ Schema.Record({
869+ key: ResponseSetIdSchema,
870+ value: ResponseSetSchema,
871+ }),
872+ ),
873+ diagnostics: Schema.optional(
874+ Schema.Record({
875+ key: DiagnosticIdSchema,
876+ value: ImportDiagnosticSchema,
877+ }),
878+ ),
879+});
880+
881+export const CatalogSnapshotV1Schema = Schema.Struct({
882+ version: Schema.Literal("ir.v1.snapshot"),
883+ import: ImportMetadataSchema,
884+ catalog: CatalogV1Schema,
885+});
886+
887+export type SourceKind = typeof SourceKindSchema.Type;
888+export type ScopeKind = typeof ScopeKindSchema.Type;
889+export type EffectKind = typeof EffectKindSchema.Type;
890+export type ParameterLocation = typeof ParameterLocationSchema.Type;
891+export type ResponseTrait = typeof ResponseTraitSchema.Type;
892+export type SecuritySchemeType = typeof SecuritySchemeTypeSchema.Type;
893+export type SelectionMode = typeof SelectionModeSchema.Type;
894+export type PaginationHintKind = typeof PaginationHintKindSchema.Type;
895+export type DiagnosticCode = typeof DiagnosticCodeSchema.Type;
896+export type DiagnosticLevel = typeof DiagnosticLevelSchema.Type;
897+
898+export type DocumentationBlock = typeof DocumentationBlockSchema.Type;
899+export type ProvenanceRef = typeof ProvenanceRefSchema.Type;
900+export type NativeBlob = typeof NativeBlobSchema.Type;
901+export type EntityBase = typeof EntityBaseSchema.Type;
902+export type ImportMetadata = typeof ImportMetadataSchema.Type;
903+export type SourceDocument = typeof SourceDocumentSchema.Type;
904+export type FieldSpec = typeof FieldSpecSchema.Type;
905+export type DiscriminatorSpec = typeof DiscriminatorSpecSchema.Type;
906+export type ScalarShape = typeof ScalarShapeSchema.Type;
907+export type UnknownShape = typeof UnknownShapeSchema.Type;
908+export type ConstShape = typeof ConstShapeSchema.Type;
909+export type EnumShape = typeof EnumShapeSchema.Type;
910+export type ObjectShape = typeof ObjectShapeSchema.Type;
911+export type ArrayShape = typeof ArrayShapeSchema.Type;
912+export type TupleShape = typeof TupleShapeSchema.Type;
913+export type MapShape = typeof MapShapeSchema.Type;
914+export type AllOfShape = typeof AllOfShapeSchema.Type;
915+export type AnyOfShape = typeof AnyOfShapeSchema.Type;
916+export type OneOfShape = typeof OneOfShapeSchema.Type;
917+export type NullableShape = typeof NullableShapeSchema.Type;
918+export type RefShape = typeof RefShapeSchema.Type;
919+export type NotShape = typeof NotShapeSchema.Type;
920+export type ConditionalShape = typeof ConditionalShapeSchema.Type;
921+export type GraphQLInterfaceShape = typeof GraphQLInterfaceShapeSchema.Type;
922+export type GraphQLUnionShape = typeof GraphQLUnionShapeSchema.Type;
923+export type ShapeNode = typeof ShapeNodeSchema.Type;
924+export type AnchorTarget = typeof AnchorTargetSchema.Type;
925+export type SchemaResource = typeof SchemaResourceSchema.Type;
926+export type RequestPlacementHint = typeof RequestPlacementHintSchema.Type;
927+export type OAuthFlow = typeof OAuthFlowSchema.Type;
928+export type SecurityPlacement = typeof SecurityPlacementSchema.Type;
929+export type SecuritySchemeSymbol = typeof SecuritySchemeSymbolSchema.Type;
930+export type EncodingSpec = typeof EncodingSpecSchema.Type;
931+export type ContentSpec = typeof ContentSpecSchema.Type;
932+export type ShapeSymbol = typeof ShapeSymbolSchema.Type;
933+export type ParameterSymbol = typeof ParameterSymbolSchema.Type;
934+export type HeaderSymbol = typeof HeaderSymbolSchema.Type;
935+export type RequestBodySymbol = typeof RequestBodySymbolSchema.Type;
936+export type ResponseSymbol = typeof ResponseSymbolSchema.Type;
937+export type ExampleSymbol = typeof ExampleSymbolSchema.Type;
938+export type Symbol =
939+ | ShapeSymbol
940+ | ParameterSymbol
941+ | RequestBodySymbol
942+ | ResponseSymbol
943+ | HeaderSymbol
944+ | ExampleSymbol
945+ | SecuritySchemeSymbol;
946+export type ServerSpec = typeof ServerSpecSchema.Type;
947+export type ScopeDefaults = typeof ScopeDefaultsSchema.Type;
948+export type Scope = typeof ScopeSchema.Type;
949+export type InteractionSpec = typeof InteractionSpecSchema.Type;
950+export type Capability = typeof CapabilitySchema.Type;
951+export type HttpExecutable = typeof HttpExecutableSchema.Type;
952+export type GraphQLExecutable = typeof GraphQLExecutableSchema.Type;
953+export type McpExecutable = typeof McpExecutableSchema.Type;
954+export type Executable = typeof ExecutableSchema.Type;
955+export type PaginationHint = typeof PaginationHintSchema.Type;
956+export type StatusMatch = typeof StatusMatchSchema.Type;
957+export type ResponseVariant = typeof ResponseVariantSchema.Type;
958+export type ResponseSet = typeof ResponseSetSchema.Type;
959+export type ImportDiagnostic = typeof ImportDiagnosticSchema.Type;
960+export type CatalogV1 = typeof CatalogV1Schema.Type;
961+export type CatalogFragmentV1 = typeof CatalogFragmentV1Schema.Type;
962+export type CatalogSnapshotV1 = typeof CatalogSnapshotV1Schema.Type;
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
8282 260,
8383 );
8484
85−export const EXECUTOR_SOURCES_ADD_INPUT_SCHEMA_JSON = JSON.stringify(
86− deriveSchemaJson(ExecutorAddSourceInputSchema) ?? {},
87−);
85+export const EXECUTOR_SOURCES_ADD_INPUT_SCHEMA = deriveSchemaJson(
86+ ExecutorAddSourceInputSchema,
87+) ?? {};
8888
89−export const EXECUTOR_SOURCES_ADD_OUTPUT_SCHEMA_JSON = JSON.stringify(
90− deriveSchemaJson(SourceSchema) ?? {},
91−);
89+export const EXECUTOR_SOURCES_ADD_OUTPUT_SCHEMA = deriveSchemaJson(
90+ SourceSchema,
91+) ?? {};
9292
9393 export const EXECUTOR_SOURCES_ADD_HELP_LINES = [
9494 "Source add input shapes:",
@@ -437,8 +437,8 @@ export const createExecutorToolMap = (input: { · 342 unmodified lines
437437 metadata: {
438438 inputType: EXECUTOR_SOURCES_ADD_INPUT_HINT,
439439 outputType: EXECUTOR_SOURCES_ADD_OUTPUT_SIGNATURE,
440− inputSchemaJson: EXECUTOR_SOURCES_ADD_INPUT_SCHEMA_JSON,
441− outputSchemaJson: EXECUTOR_SOURCES_ADD_OUTPUT_SCHEMA_JSON,
440+ inputSchema: EXECUTOR_SOURCES_ADD_INPUT_SCHEMA,
441+ outputSchema: EXECUTOR_SOURCES_ADD_OUTPUT_SCHEMA,
442442 sourceKey: "executor",
443443 interaction: "auto",
444444 },
packages/control-plane/src/runtime/graphql-tools.test.ts+3−3
@@ -1,6 +1,6 @@
11 import { createServer, type IncomingMessage } from "node:http";
22
3−import { afterEach, describe, expect, it } from "vitest";
3+import { afterEach, describe, expect, it } from "@effect/vitest";
44
55 import { createGraphqlToolFromPersistedOperation } from "./graphql-tools";
66
@@ -91,7 +91,7 @@ describe("graphql-tools", () => { · 84 unmodified lines
9191 path: "source.graphql.raw",
9292 sourceKey: "src_graphql",
9393 endpoint: server.baseUrl,
94− providerDataJson: JSON.stringify({
94+ providerData: {
9595 kind: "graphql",
9696 toolKind: "request",
9797 toolId: "raw",
@@ -105,7 +105,7 @@ describe("graphql-tools", () => { · 7 unmodified lines
105105 queryTypeName: "Query",
106106 mutationTypeName: "Mutation",
107107 subscriptionTypeName: null,
108− }),
108+ },
109109 credentialPlacements: {
110110 headers: {
111111 authorization: "Token top-secret",
packages/control-plane/src/runtime/graphql-tools.ts+71−74
@@ -10,7 +10,7 @@ import { · 9 unmodified lines
1010 type ToolMap,
1111 type ToolMetadata,
1212 type ToolPath,
13− typeSignatureFromSchemaJson,
13+ typeSignatureFromSchema,
1414 } from "@executor/codemode-core";
1515 import * as Effect from "effect/Effect";
1616 import * as Schema from "effect/Schema";
@@ -51,9 +51,9 @@ type GraphqlManifestToolBase = { · 34 unmodified lines
5151 rawToolId: string | null;
5252 toolName: string;
5353 description: string | null;
54− inputSchemaJson?: string;
55− outputSchemaJson?: string;
56− exampleInputJson?: string;
54+ inputSchema?: unknown;
55+ outputSchema?: unknown;
56+ exampleInput?: unknown;
5757 };
5858
5959 export type GraphqlRequestToolManifestEntry = GraphqlManifestToolBase & {
@@ -102,10 +102,10 @@ export type GraphqlToolDefinition = { · 42 unmodified lines
102102 export type GraphqlToolPresentation = {
103103 inputType: string;
104104 outputType: string;
105− inputSchemaJson?: string;
106− outputSchemaJson?: string;
107− exampleInputJson?: string;
108− providerDataJson: string;
105+ inputSchema?: unknown;
106+ outputSchema?: unknown;
107+ exampleInput?: unknown;
108+ providerData: GraphqlToolProviderData;
109109 };
110110
111111 const GraphqlToolKindSchema = Schema.Literal("request", "field");
@@ -883,11 +883,11 @@ const outputEnvelopeSchemaForField = ( · 771 unmodified lines
883883 };
884884 };
885885
886−const exampleInputJsonForField = (
886+const exampleInputForField = (
887887 args: readonly GraphQLArgument[],
888−): string | undefined => {
888+): Record<string, unknown> | undefined => {
889889 if (args.length === 0) {
890− return JSON.stringify({});
890+ return {};
891891 }
892892
893893 const requiredArgs = args.filter(
@@ -902,7 +902,7 @@ const exampleInputJsonForField = ( · 8 unmodified lines
902902 ]),
903903 );
904904
905− return JSON.stringify(example);
905+ return example;
906906 };
907907
908908 const buildGraphqlFieldOperationDocument = (input: {
@@ -958,11 +958,11 @@ const requestToolManifestEntry = ( · 49 unmodified lines
958958 rawToolId: "request",
959959 toolName: "GraphQL request",
960960 description: `Execute a raw GraphQL request against ${sourceName}.`,
961− inputSchemaJson: JSON.stringify(GRAPHQL_REQUEST_INPUT_SCHEMA),
962− outputSchemaJson: JSON.stringify(GRAPHQL_REQUEST_OUTPUT_SCHEMA),
963− exampleInputJson: JSON.stringify({
961+ inputSchema: GRAPHQL_REQUEST_INPUT_SCHEMA,
962+ outputSchema: GRAPHQL_REQUEST_OUTPUT_SCHEMA,
963+ exampleInput: {
964964 query: "query { __typename }",
965− }),
965+ },
966966 });
967967
968968 type GraphqlFieldToolDraft = Omit<GraphqlFieldToolManifestEntry, "toolId"> & {
@@ -1030,11 +1030,13 @@ const fieldToolDraftsFromRootType = (input: { · 61 unmodified lines
10301030 .filter((field) => !field.name.startsWith("__"))
10311031 .map((field) => {
10321032 const leaf = toCamelCase(field.name);
1033− const outputSchemaJson = JSON.stringify(
1034− outputEnvelopeSchemaForField(field.type, input.bundleBuilder),
1033+ const outputSchema = outputEnvelopeSchemaForField(
1034+ field.type,
1035+ input.bundleBuilder,
10351036 );
1036− const inputSchemaJson = JSON.stringify(
1037− inputSchemaForFieldArguments(field.args, input.bundleBuilder),
1037+ const inputSchema = inputSchemaForFieldArguments(
1038+ field.args,
1039+ input.bundleBuilder,
10381040 );
10391041 const { operationName, operationDocument } =
10401042 buildGraphqlFieldOperationDocument({
@@ -1061,9 +1063,9 @@ const fieldToolDraftsFromRootType = (input: { · 20 unmodified lines
10611063 operationType: input.operationType,
10621064 operationName,
10631065 operationDocument,
1064− inputSchemaJson,
1065− outputSchemaJson,
1066− exampleInputJson: exampleInputJsonForField(field.args),
1066+ inputSchema,
1067+ outputSchema,
1068+ exampleInput: exampleInputForField(field.args),
10671069 searchTerms: [
10681070 input.operationType,
10691071 field.name,
@@ -1143,26 +1145,26 @@ export const buildGraphqlToolPresentation = (input: { · 73 unmodified lines
11431145 const entry = input.manifest.tools.find(
11441146 (tool) => tool.toolId === input.definition.toolId,
11451147 );
1146− const inputSchemaJson = entry?.inputSchemaJson;
1147− const outputSchemaJson = entry?.outputSchemaJson;
1148+ const inputSchema = entry?.inputSchema;
1149+ const outputSchema = entry?.outputSchema;
11481150
11491151 return {
1150− inputType: typeSignatureFromSchemaJson(
1151− inputSchemaJson,
1152+ inputType: typeSignatureFromSchema(
1153+ inputSchema,
11521154 "unknown",
11531155 Infinity,
11541156 ),
1155− outputType: typeSignatureFromSchemaJson(
1156− outputSchemaJson,
1157+ outputType: typeSignatureFromSchema(
1158+ outputSchema,
11571159 "unknown",
11581160 Infinity,
11591161 ),
1160− ...(inputSchemaJson ? { inputSchemaJson } : {}),
1161− ...(outputSchemaJson ? { outputSchemaJson } : {}),
1162− ...(entry?.exampleInputJson
1163− ? { exampleInputJson: entry.exampleInputJson }
1162+ ...(inputSchema !== undefined ? { inputSchema } : {}),
1163+ ...(outputSchema !== undefined ? { outputSchema } : {}),
1164+ ...(entry?.exampleInput !== undefined
1165+ ? { exampleInput: entry.exampleInput }
11641166 : {}),
1165− providerDataJson: JSON.stringify({
1167+ providerData: {
11661168 kind: "graphql",
11671169 toolKind: entry?.kind ?? "request",
11681170 toolId: input.definition.toolId,
@@ -1176,12 +1178,12 @@ export const buildGraphqlToolPresentation = (input: { · 7 unmodified lines
11761178 queryTypeName: input.manifest.queryTypeName,
11771179 mutationTypeName: input.manifest.mutationTypeName,
11781180 subscriptionTypeName: input.manifest.subscriptionTypeName,
1179− } satisfies GraphqlToolProviderData),
1181+ } satisfies GraphqlToolProviderData,
11801182 };
11811183 };
11821184
1183−const decodeGraphqlToolProviderDataJson = Schema.decodeUnknownEither(
1184− Schema.parseJson(GraphqlToolProviderDataSchema),
1185+const decodeGraphqlToolProviderData = Schema.decodeUnknownEither(
1186+ GraphqlToolProviderDataSchema,
11851187 );
11861188
11871189 export const decodeGraphqlSchemaRefTableJson = Schema.decodeUnknownEither(
@@ -1218,19 +1220,14 @@ const setNestedSchemaProperty = ( · 30 unmodified lines
12181220 };
12191221
12201222 const materializeSchemaWithRefDefinitions = (input: {
1221− schemaJson: string | undefined;
1223+ schema: unknown;
12221224 refTable?: Readonly<Record<string, unknown>>;
12231225 }): Record<string, unknown> => {
1224− if (!input.schemaJson) {
1226+ if (input.schema === undefined || input.schema === null) {
12251227 return {};
12261228 }
12271229
1228− let rootSchema: Record<string, unknown>;
1229− try {
1230− rootSchema = asRecord(JSON.parse(input.schemaJson) as unknown);
1231− } catch {
1232− return {};
1233− }
1230+ const rootSchema = asRecord(input.schema);
12341231
12351232 if (!input.refTable || Object.keys(input.refTable).length === 0) {
12361233 return rootSchema;
@@ -1269,17 +1266,17 @@ export const createGraphqlToolFromPersistedOperation = (input: { · 32 unmodified lines
12691266 sourceKey: string;
12701267 endpoint: string;
12711268 description?: string;
1272− inputSchemaJson?: string;
1273− outputSchemaJson?: string;
1274− exampleInputJson?: string;
1275− providerDataJson: string;
1269+ inputSchema?: unknown;
1270+ outputSchema?: unknown;
1271+ exampleInput?: unknown;
1272+ providerData: unknown;
12761273 schemaRefTable?: Readonly<Record<string, unknown>>;
12771274 defaultHeaders?: Readonly<Record<string, string>>;
12781275 credentialHeaders?: Readonly<Record<string, string>>;
12791276 credentialPlacements?: HttpRequestPlacements;
12801277 }) => {
1281− const decodedProviderData = decodeGraphqlToolProviderDataJson(
1282− input.providerDataJson,
1278+ const decodedProviderData = decodeGraphqlToolProviderData(
1279+ input.providerData,
12831280 );
12841281 if (decodedProviderData._tag === "Left") {
12851282 throw new Error("Invalid GraphQL provider data");
@@ -1287,7 +1284,7 @@ export const createGraphqlToolFromPersistedOperation = (input: { · 1 unmodified line
12871284
12881285 const providerData = decodedProviderData.right;
12891286 const inputSchema = materializeSchemaWithRefDefinitions({
1290− schemaJson: input.inputSchemaJson,
1287+ schema: input.inputSchema,
12911288 refTable: input.schemaRefTable,
12921289 });
12931290
@@ -1298,28 +1295,28 @@ export const createGraphqlToolFromPersistedOperation = (input: { · 4 unmodified lines
12981295 : providerData.operationType === "query"
12991296 ? "auto"
13001297 : "required",
1301− inputType: typeSignatureFromSchemaJson(
1302− input.inputSchemaJson,
1298+ inputType: typeSignatureFromSchema(
1299+ input.inputSchema,
13031300 "unknown",
13041301 Infinity,
13051302 ),
1306− outputType: typeSignatureFromSchemaJson(
1307− input.outputSchemaJson,
1303+ outputType: typeSignatureFromSchema(
1304+ input.outputSchema,
13081305 "unknown",
13091306 Infinity,
13101307 ),
1311− ...(input.inputSchemaJson
1312− ? { inputSchemaJson: input.inputSchemaJson }
1308+ ...(input.inputSchema !== undefined
1309+ ? { inputSchema: input.inputSchema }
13131310 : {}),
1314− ...(input.outputSchemaJson
1315− ? { outputSchemaJson: input.outputSchemaJson }
1311+ ...(input.outputSchema !== undefined
1312+ ? { outputSchema: input.outputSchema }
13161313 : {}),
1317− ...(input.exampleInputJson
1318− ? { exampleInputJson: input.exampleInputJson }
1314+ ...(input.exampleInput !== undefined
1315+ ? { exampleInput: input.exampleInput }
13191316 : {}),
13201317 sourceKey: input.sourceKey,
13211318 providerKind: "graphql",
1322− providerDataJson: input.providerDataJson,
1319+ providerData,
13231320 };
13241321
13251322 return toTool({
@@ -1387,17 +1384,17 @@ export const graphqlToolDescriptorFromDefinition = (input: { · 61 unmodified lines
13871384 input.definition.operationType === "query" ? "auto" : "required",
13881385 inputType: presentation.inputType,
13891386 outputType: presentation.outputType,
1390− ...(input.includeSchemas && presentation.inputSchemaJson
1391− ? { inputSchemaJson: presentation.inputSchemaJson }
1387+ ...(input.includeSchemas && presentation.inputSchema !== undefined
1388+ ? { inputSchema: presentation.inputSchema }
13921389 : {}),
1393− ...(input.includeSchemas && presentation.outputSchemaJson
1394− ? { outputSchemaJson: presentation.outputSchemaJson }
1390+ ...(input.includeSchemas && presentation.outputSchema !== undefined
1391+ ? { outputSchema: presentation.outputSchema }
13951392 : {}),
1396− ...(presentation.exampleInputJson
1397− ? { exampleInputJson: presentation.exampleInputJson }
1393+ ...(presentation.exampleInput !== undefined
1394+ ? { exampleInput: presentation.exampleInput }
13981395 : {}),
13991396 providerKind: "graphql",
1400− providerDataJson: presentation.providerDataJson,
1397+ providerData: presentation.providerData,
14011398 };
14021399 };
14031400
@@ -1572,10 +1569,10 @@ export const createGraphqlToolsFromManifest = (input: { · 168 unmodified lines
15721569 sourceKey: input.sourceKey,
15731570 endpoint,
15741571 description: definition.description,
1575− inputSchemaJson: presentation.inputSchemaJson,
1576− outputSchemaJson: presentation.outputSchemaJson,
1577− exampleInputJson: presentation.exampleInputJson,
1578− providerDataJson: presentation.providerDataJson,
1572+ inputSchema: presentation.inputSchema,
1573+ outputSchema: presentation.outputSchema,
1574+ exampleInput: presentation.exampleInput,
1575+ providerData: presentation.providerData,
15791576 schemaRefTable: input.manifest.schemaRefTable,
15801577 defaultHeaders: input.defaultHeaders,
15811578 credentialHeaders: input.credentialHeaders,
packages/control-plane/src/runtime/index.ts+9−9
@@ -33,9 +33,9 @@ import { LocalToolRuntimeLoaderLive } from "./local-tools"; · 32 unmodified lines
3333 import { synchronizeLocalWorkspaceState } from "./local-workspace-sync";
3434 import { ControlPlaneStore, type ControlPlaneStoreShape } from "./store";
3535 import { RuntimeSourceStoreLive } from "./source-store";
36−import { RuntimeSourceRecipeStoreLive } from "./source-recipes-runtime";
36+import { RuntimeSourceCatalogStoreLive } from "./source-catalog-runtime";
3737 import { RuntimeSourceAuthMaterialLive } from "./source-auth-material";
38−import { RuntimeSourceMaterializationLive } from "./source-materialization";
38+import { RuntimeSourceCatalogSyncLive } from "./source-catalog-sync";
3939 import {
4040 RuntimeSourceAuthServiceLive,
4141 } from "./source-auth-service";
@@ -121,7 +121,7 @@ export const createRuntimeControlPlaneLayer = ( · 79 unmodified lines
121121 Layer.provide(baseLayer),
122122 );
123123
124− const sourceRecipeStoreLayer = RuntimeSourceRecipeStoreLive.pipe(
124+ const sourceCatalogStoreLayer = RuntimeSourceCatalogStoreLive.pipe(
125125 Layer.provide(Layer.mergeAll(baseLayer, sourceStoreLayer)),
126126 );
127127
@@ -129,7 +129,7 @@ export const createRuntimeControlPlaneLayer = ( · 1 unmodified line
129129 Layer.provide(Layer.mergeAll(baseLayer, secretMaterialLayer)),
130130 );
131131
132− const sourceMaterializationLayer = RuntimeSourceMaterializationLive.pipe(
132+ const sourceCatalogSyncLayer = RuntimeSourceCatalogSyncLive.pipe(
133133 Layer.provide(
134134 Layer.mergeAll(baseLayer, secretMaterialLayer, sourceAuthMaterialLayer),
135135 ),
@@ -143,7 +143,7 @@ export const createRuntimeControlPlaneLayer = ( · 7 unmodified lines
143143 baseLayer,
144144 secretMaterialLayer,
145145 sourceStoreLayer,
146− sourceMaterializationLayer,
146+ sourceCatalogSyncLayer,
147147 ),
148148 ),
149149 );
@@ -156,9 +156,9 @@ export const createRuntimeControlPlaneLayer = ( · 6 unmodified lines
156156 baseLayer,
157157 secretMaterialLayer,
158158 sourceAuthMaterialLayer,
159− sourceMaterializationLayer,
159+ sourceCatalogSyncLayer,
160160 sourceAuthLayer,
161− sourceRecipeStoreLayer,
161+ sourceCatalogStoreLayer,
162162 localToolRuntimeLayer,
163163 ),
164164 ),
@@ -169,8 +169,8 @@ export const createRuntimeControlPlaneLayer = ( · 4 unmodified lines
169169 secretMaterialLayer,
170170 sourceStoreLayer,
171171 sourceAuthMaterialLayer,
172− sourceMaterializationLayer,
173− sourceRecipeStoreLayer,
172+ sourceCatalogSyncLayer,
173+ sourceCatalogStoreLayer,
174174 localToolRuntimeLayer,
175175 sourceAuthLayer,
176176 executionResolverLayer,
packages/control-plane/src/runtime/ir-execution.ts+494−0
@@ -0,0 +1,494 @@
1+import {
2+ applyCookiePlacementsToHeaders,
3+ applyHttpQueryPlacementsToUrl,
4+ applyJsonBodyPlacements,
5+} from "@executor/codemode-core";
6+import { createSdkMcpConnector } from "@executor/codemode-mcp";
7+import type { AccountId, Source } from "#schema";
8+import * as Effect from "effect/Effect";
9+
10+import type { CatalogV1, Capability, Executable, GraphQLExecutable, HttpExecutable, McpExecutable, ParameterSymbol } from "../ir/model";
11+import type { LoadedSourceCatalogToolIndexEntry } from "./source-catalog-runtime";
12+import type { ResolvedSourceAuthMaterial } from "./source-auth-material";
13+
14+const asObject = (value: unknown): Record<string, unknown> =>
15+ value !== null && typeof value === "object" && !Array.isArray(value)
16+ ? (value as Record<string, unknown>)
17+ : {};
18+
19+const asString = (value: unknown): string | null =>
20+ typeof value === "string" && value.trim().length > 0 ? value : null;
21+
22+const asStringRecord = (value: unknown): Record<string, string> =>
23+ Object.fromEntries(
24+ Object.entries(asObject(value)).flatMap(([key, entry]) =>
25+ typeof entry === "string" ? [[key, entry]] : []),
26+ );
27+
28+const readSourceHeaders = (source: Source): Record<string, string> => {
29+ const binding = asObject(source.binding);
30+ const defaultHeaders = binding.defaultHeaders ?? binding.headers;
31+ return asStringRecord(defaultHeaders);
32+};
33+
34+const readSourceQueryParams = (source: Source): Record<string, string> =>
35+ asStringRecord(asObject(source.binding).queryParams);
36+
37+const readSourceTransport = (source: Source): "auto" | "streamable-http" | "sse" | undefined => {
38+ const transport = asString(asObject(source.binding).transport);
39+ if (transport === "streamable-http" || transport === "sse" || transport === "auto") {
40+ return transport;
41+ }
42+ return undefined;
43+};
44+
45+const summarizeHttpBody = (body: unknown): string => {
46+ if (typeof body === "string") {
47+ return body.slice(0, 400);
48+ }
49+ try {
50+ return JSON.stringify(body).slice(0, 400);
51+ } catch {
52+ return String(body);
53+ }
54+};
55+
56+const decodeFetchBody = async (response: Response): Promise<unknown> => {
57+ const contentType = response.headers.get("content-type")?.toLowerCase() ?? "";
58+ if (contentType.includes("application/json")) {
59+ return response.json();
60+ }
61+ if (response.status === 204) {
62+ return null;
63+ }
64+ return response.text();
65+};
66+
67+const primitiveString = (value: unknown): string => {
68+ if (value === null || value === undefined) {
69+ return "";
70+ }
71+ if (typeof value === "string") {
72+ return value;
73+ }
74+ if (typeof value === "number" || typeof value === "boolean") {
75+ return String(value);
76+ }
77+ return JSON.stringify(value);
78+};
79+
80+const parameterById = (catalog: CatalogV1, parameterId: string): ParameterSymbol | undefined => {
81+ const symbol = catalog.symbols[parameterId];
82+ return symbol?.kind === "parameter" ? symbol : undefined;
83+};
84+
85+const httpParameterLookupMode = (catalog: CatalogV1, executable: HttpExecutable): "flat" | "grouped" => {
86+ const reserved = new Set(["body", "args", "select", "path", "query", "headers", "cookies"]);
87+ const allParameters = [
88+ ...(executable.pathParameterIds ?? []),
89+ ...(executable.queryParameterIds ?? []),
90+ ...(executable.headerParameterIds ?? []),
91+ ...(executable.cookieParameterIds ?? []),
92+ ]
93+ .map((parameterId) => parameterById(catalog, parameterId))
94+ .filter((parameter): parameter is ParameterSymbol => parameter !== undefined);
95+
96+ const names = allParameters.map((parameter) => parameter.name);
97+ const hasCollision = names.some((name, index) =>
98+ reserved.has(name) || names.findIndex((candidate) => candidate === name) !== index
99+ );
100+
101+ return hasCollision ? "grouped" : "flat";
102+};
103+
104+const readHttpParameterValue = (input: {
105+ catalog: CatalogV1;
106+ executable: HttpExecutable;
107+ parameter: ParameterSymbol;
108+ args: Record<string, unknown>;
109+}): unknown => {
110+ const mode = httpParameterLookupMode(input.catalog, input.executable);
111+ if (mode === "flat") {
112+ return input.args[input.parameter.name];
113+ }
114+
115+ const groupKey =
116+ input.parameter.location === "header"
117+ ? "headers"
118+ : input.parameter.location === "cookie"
119+ ? "cookies"
120+ : input.parameter.location;
121+ return asObject(input.args[groupKey])[input.parameter.name];
122+};
123+
124+const executeHttp = (input: {
125+ source: Source;
126+ catalog: CatalogV1;
127+ executable: HttpExecutable;
128+ auth: ResolvedSourceAuthMaterial;
129+ args: unknown;
130+}) =>
131+ Effect.tryPromise({
132+ try: async () => {
133+ const argsRecord = asObject(input.args);
134+ let resolvedPath = input.executable.pathTemplate;
135+ const url = applyHttpQueryPlacementsToUrl({
136+ url: new URL(input.source.endpoint),
137+ queryParams: readSourceQueryParams(input.source),
138+ });
139+ const headers: Record<string, string> = {
140+ ...readSourceHeaders(input.source),
141+ };
142+ const cookies: Record<string, string> = {};
143+
144+ const allParameterIds = [
145+ ...(input.executable.pathParameterIds ?? []),
146+ ...(input.executable.queryParameterIds ?? []),
147+ ...(input.executable.headerParameterIds ?? []),
148+ ...(input.executable.cookieParameterIds ?? []),
149+ ];
150+
151+ for (const parameterId of allParameterIds) {
152+ const parameter = parameterById(input.catalog, parameterId);
153+ if (!parameter) {
154+ continue;
155+ }
156+
157+ const value = readHttpParameterValue({
158+ catalog: input.catalog,
159+ executable: input.executable,
160+ parameter,
161+ args: argsRecord,
162+ });
163+ if (value === undefined || value === null) {
164+ if (parameter.required) {
165+ throw new Error(`Missing required ${parameter.location} parameter ${parameter.name}`);
166+ }
167+ continue;
168+ }
169+
170+ if (parameter.location === "path") {
171+ resolvedPath = resolvedPath.replace(
172+ new RegExp(`{${parameter.name}}`, "g"),
173+ encodeURIComponent(primitiveString(value)),
174+ );
175+ continue;
176+ }
177+
178+ if (parameter.location === "query") {
179+ if (Array.isArray(value)) {
180+ for (const entry of value) {
181+ url.searchParams.append(parameter.name, primitiveString(entry));
182+ }
183+ } else {
184+ url.searchParams.set(parameter.name, primitiveString(value));
185+ }
186+ continue;
187+ }
188+
189+ if (parameter.location === "header") {
190+ headers[parameter.name] = primitiveString(value);
191+ continue;
192+ }
193+
194+ if (parameter.location === "cookie") {
195+ cookies[parameter.name] = primitiveString(value);
196+ }
197+ }
198+
199+ const bodySymbol = input.executable.requestBodyId
200+ ? input.catalog.symbols[input.executable.requestBodyId]
201+ : undefined;
202+ let body: string | undefined;
203+ if (bodySymbol?.kind === "requestBody") {
204+ const bodyValue = argsRecord.body ?? argsRecord.input;
205+ if (bodyValue !== undefined) {
206+ const contentType = bodySymbol.contents[0]?.mediaType ?? "application/json";
207+ headers["content-type"] = contentType;
208+ body = JSON.stringify(
209+ applyJsonBodyPlacements({
210+ body: bodyValue,
211+ bodyValues: input.auth.bodyValues,
212+ label: `${input.executable.method} ${input.executable.pathTemplate}`,
213+ }),
214+ );
215+ }
216+ }
217+
218+ const requestUrl = new URL(resolvedPath, url);
219+ const urlWithAuth = applyHttpQueryPlacementsToUrl({
220+ url: requestUrl,
221+ queryParams: input.auth.queryParams,
222+ });
223+ const headersWithCookies = applyCookiePlacementsToHeaders({
224+ headers: {
225+ ...headers,
226+ ...input.auth.headers,
227+ },
228+ cookies: {
229+ ...cookies,
230+ ...input.auth.cookies,
231+ },
232+ });
233+
234+ const response = await fetch(urlWithAuth, {
235+ method: input.executable.method,
236+ headers: headersWithCookies,
237+ ...(body !== undefined ? { body } : {}),
238+ });
239+ const responseBody = await decodeFetchBody(response);
240+ if (!response.ok) {
241+ throw new Error(`HTTP ${response.status}: ${summarizeHttpBody(responseBody)}`);
242+ }
243+
244+ return responseBody;
245+ },
246+ catch: (cause) => cause instanceof Error ? cause : new Error(String(cause)),
247+ });
248+
249+const graphqlSelectionFromArgs = (value: unknown): string => {
250+ const record = asObject(value);
251+ const fields = Object.entries(record).flatMap(([key, entry]) => {
252+ if (entry === true) {
253+ return [key];
254+ }
255+ if (entry && typeof entry === "object") {
256+ const child = graphqlSelectionFromArgs(entry);
257+ return child.length > 0 ? [`${key} { ${child} }`] : [];
258+ }
259+ return [];
260+ });
261+ return fields.join(" ");
262+};
263+
264+const graphqlArgsPayload = (input: {
265+ catalog: CatalogV1;
266+ executable: GraphQLExecutable;
267+ args: Record<string, unknown>;
268+}): Record<string, unknown> => {
269+ const argumentShape = input.catalog.symbols[input.executable.argumentShapeId];
270+ const reserved = new Set(["body", "args", "select", "path", "query", "headers", "cookies"]);
271+ if (
272+ argumentShape?.kind === "shape"
273+ && argumentShape.node.type === "object"
274+ && Object.keys(argumentShape.node.fields).every((name) => !reserved.has(name))
275+ ) {
276+ return Object.fromEntries(
277+ Object.entries(input.args).filter(([key]) => key !== "select" && key !== "headers"),
278+ );
279+ }
280+
281+ return asObject(input.args.args);
282+};
283+
284+const executeGraphql = (input: {
285+ source: Source;
286+ catalog: CatalogV1;
287+ executable: GraphQLExecutable;
288+ auth: ResolvedSourceAuthMaterial;
289+ args: unknown;
290+}) =>
291+ Effect.tryPromise({
292+ try: async () => {
293+ const argsRecord = asObject(input.args);
294+ const native = input.executable.native?.[0]?.value as Record<string, unknown> | undefined;
295+ const requestHeaders = {
296+ ...readSourceHeaders(input.source),
297+ ...input.auth.headers,
298+ ...asStringRecord(argsRecord.headers),
299+ "content-type": "application/json",
300+ };
301+ const queryParams = {
302+ ...readSourceQueryParams(input.source),
303+ ...input.auth.queryParams,
304+ };
305+ const endpoint = applyHttpQueryPlacementsToUrl({
306+ url: input.source.endpoint,
307+ queryParams,
308+ }).toString();
309+
310+ const toolKind = asString(native?.toolKind);
311+ if (toolKind === "request" || !native || !asString(native.operationDocument)) {
312+ const query = asString(argsRecord.query);
313+ if (query === null) {
314+ throw new Error(`GraphQL request tools require args.query`);
315+ }
316+
317+ const response = await fetch(endpoint, {
318+ method: "POST",
319+ headers: applyCookiePlacementsToHeaders({
320+ headers: requestHeaders,
321+ cookies: input.auth.cookies,
322+ }),
323+ body: JSON.stringify({
324+ query,
325+ ...(argsRecord.variables ? { variables: asObject(argsRecord.variables) } : {}),
326+ ...(asString(argsRecord.operationName) ? { operationName: asString(argsRecord.operationName)! } : {}),
327+ }),
328+ });
329+ const body = await decodeFetchBody(response);
330+ if (!response.ok) {
331+ throw new Error(`GraphQL HTTP ${response.status}: ${summarizeHttpBody(body)}`);
332+ }
333+ return body;
334+ }
335+
336+ const operationDocument = asString(native.operationDocument)!;
337+ const operationName = asString(native.operationName) ?? undefined;
338+ const variables = graphqlArgsPayload({
339+ catalog: input.catalog,
340+ executable: input.executable,
341+ args: argsRecord,
342+ });
343+
344+ const query =
345+ input.executable.selectionMode === "caller"
346+ ? (() => {
347+ const selection = graphqlSelectionFromArgs(argsRecord.select);
348+ if (selection.length === 0) {
349+ throw new Error(`GraphQL caller selection tools require args.select`);
350+ }
351+ return `${input.executable.operationType} ${input.executable.rootField} { ${input.executable.rootField} ${selection ? `{ ${selection} }` : ""} }`;
352+ })()
353+ : operationDocument;
354+
355+ const response = await fetch(endpoint, {
356+ method: "POST",
357+ headers: applyCookiePlacementsToHeaders({
358+ headers: requestHeaders,
359+ cookies: input.auth.cookies,
360+ }),
361+ body: JSON.stringify({
362+ query,
363+ variables,
364+ ...(operationName ? { operationName } : {}),
365+ }),
366+ });
367+ const body = asObject(await decodeFetchBody(response));
368+ if (!response.ok) {
369+ throw new Error(`GraphQL HTTP ${response.status}: ${summarizeHttpBody(body)}`);
370+ }
371+
372+ const errors = Array.isArray(body.errors) ? body.errors : [];
373+ const data = asObject(body.data);
374+ const fieldName = asString(native.fieldName) ?? input.executable.rootField;
375+ return {
376+ data: data[fieldName] ?? null,
377+ errors,
378+ isError: errors.length > 0,
379+ };
380+ },
381+ catch: (cause) => cause instanceof Error ? cause : new Error(String(cause)),
382+ });
383+
384+const executeMcp = (input: {
385+ source: Source;
386+ catalog: CatalogV1;
387+ executable: McpExecutable;
388+ auth: ResolvedSourceAuthMaterial;
389+ args: unknown;
390+}) =>
391+ Effect.tryPromise({
392+ try: async () => {
393+ const inputShape = input.executable.inputShapeId
394+ ? input.catalog.symbols[input.executable.inputShapeId]
395+ : undefined;
396+ const payload =
397+ inputShape?.kind === "shape" && inputShape.node.type !== "object"
398+ ? asObject(input.args).input
399+ : input.args;
400+ const connector = createSdkMcpConnector({
401+ endpoint: input.source.endpoint,
402+ transport: readSourceTransport(input.source),
403+ queryParams: {
404+ ...readSourceQueryParams(input.source),
405+ ...input.auth.queryParams,
406+ },
407+ headers: applyCookiePlacementsToHeaders({
408+ headers: {
409+ ...readSourceHeaders(input.source),
410+ ...input.auth.headers,
411+ },
412+ cookies: input.auth.cookies,
413+ }),
414+ });
415+ const connection = await connector();
416+ try {
417+ return await connection.client.callTool({
418+ name: input.executable.toolName,
419+ arguments: asObject(payload),
420+ });
421+ } finally {
422+ await connection.close?.();
423+ }
424+ },
425+ catch: (cause) => cause instanceof Error ? cause : new Error(String(cause)),
426+ });
427+
428+export const invocationDescriptorFromTool = (input: {
429+ tool: LoadedSourceCatalogToolIndexEntry;
430+}): {
431+ toolPath: string;
432+ sourceId: Source["id"];
433+ sourceName: Source["name"];
434+ sourceKind: Source["kind"];
435+ sourceNamespace: string | null;
436+ operationKind: "read" | "write" | "delete" | "execute" | "unknown";
437+ interaction: "auto" | "required";
438+ approvalLabel: string | null;
439+} => ({
440+ toolPath: input.tool.path,
441+ sourceId: input.tool.source.id,
442+ sourceName: input.tool.source.name,
443+ sourceKind: input.tool.source.kind,
444+ sourceNamespace:
445+ input.tool.source.namespace ?? null,
446+ operationKind:
447+ input.tool.capability.semantics.effect === "read"
448+ ? "read"
449+ : input.tool.capability.semantics.effect === "write"
450+ ? "write"
451+ : input.tool.capability.semantics.effect === "delete"
452+ ? "delete"
453+ : input.tool.capability.semantics.effect === "action"
454+ ? "execute"
455+ : "unknown",
456+ interaction: input.tool.descriptor.interaction ?? "auto",
457+ approvalLabel: input.tool.capability.surface.title ?? null,
458+});
459+
460+export const invokeIrTool = (input: {
461+ workspaceId: Source["workspaceId"];
462+ accountId: AccountId;
463+ tool: LoadedSourceCatalogToolIndexEntry;
464+ auth: ResolvedSourceAuthMaterial;
465+ args: unknown;
466+ context?: Record<string, unknown>;
467+}) => {
468+ switch (input.tool.executable.protocol) {
469+ case "http":
470+ return executeHttp({
471+ source: input.tool.source,
472+ catalog: input.tool.projectedCatalog,
473+ executable: input.tool.executable,
474+ auth: input.auth,
475+ args: input.args,
476+ });
477+ case "graphql":
478+ return executeGraphql({
479+ source: input.tool.source,
480+ catalog: input.tool.projectedCatalog,
481+ executable: input.tool.executable,
482+ auth: input.auth,
483+ args: input.args,
484+ });
485+ case "mcp":
486+ return executeMcp({
487+ source: input.tool.source,
488+ catalog: input.tool.projectedCatalog,
489+ executable: input.tool.executable,
490+ auth: input.auth,
491+ args: input.args,
492+ });
493+ }
494+};
packages/control-plane/src/runtime/live-execution.test.ts+1−1
@@ -1,4 +1,4 @@
1−import { describe, expect, it } from "vitest";
1+import { describe, expect, it } from "@effect/vitest";
22
33 import { sanitizePersistedElicitationResponse } from "./live-execution";
44
packages/control-plane/src/runtime/local-source-artifacts.ts+29−94
@@ -3,18 +3,17 @@ import { FileSystem } from "@effect/platform"; · 2 unmodified lines
33
44 import {
55 SourceIdSchema,
6− SourceRecipeIdSchema,
7− StoredSourceRecipeDocumentRecordSchema,
8− StoredSourceRecipeOperationRecordSchema,
9− StoredSourceRecipeRevisionRecordSchema,
10− StoredSourceRecipeSchemaBundleRecordSchema,
6+ SourceCatalogIdSchema,
7+ StoredSourceCatalogRevisionRecordSchema,
118 TimestampMsSchema,
129 type Source,
13− type SourceRecipeId,
10+ type SourceCatalogId,
1411 } from "#schema";
1512 import * as Effect from "effect/Effect";
1613 import * as Schema from "effect/Schema";
1714
15+import { CatalogSnapshotV1Schema } from "../ir/model";
16+import type { SourceCatalogSyncResult } from "./source-catalog-support";
1817 import type { ResolvedLocalWorkspaceContext } from "./local-config";
1918 import {
2019 LocalFileSystemError,
@@ -22,25 +21,20 @@ import { · 1 unmodified line
2221 unknownLocalErrorDetails,
2322 } from "./local-errors";
2423 import {
25− createSourceRecipeRevisionRecord,
26− stableSourceRecipeId,
24+ createSourceCatalogRevisionRecord,
25+ stableSourceCatalogId,
2726 } from "./source-definitions";
28−import {
29− contentHash,
30− type SourceRecipeMaterialization,
31−} from "./source-recipe-support";
27+import { contentHash } from "./source-catalog-support";
3228
33−const LOCAL_SOURCE_ARTIFACT_VERSION = 1 as const;
29+const LOCAL_SOURCE_ARTIFACT_VERSION = 2 as const;
3430
3531 export const LocalSourceArtifactSchema = Schema.Struct({
3632 version: Schema.Literal(LOCAL_SOURCE_ARTIFACT_VERSION),
3733 sourceId: SourceIdSchema,
38− recipeId: SourceRecipeIdSchema,
34+ catalogId: SourceCatalogIdSchema,
3935 generatedAt: TimestampMsSchema,
40− revision: StoredSourceRecipeRevisionRecordSchema,
41− documents: Schema.Array(StoredSourceRecipeDocumentRecordSchema),
42− schemaBundles: Schema.Array(StoredSourceRecipeSchemaBundleRecordSchema),
43− operations: Schema.Array(StoredSourceRecipeOperationRecordSchema),
36+ revision: StoredSourceCatalogRevisionRecordSchema,
37+ snapshot: CatalogSnapshotV1Schema,
4438 });
4539
4640 export type LocalSourceArtifact = typeof LocalSourceArtifactSchema.Type;
@@ -65,90 +59,32 @@ const localSourceArtifactPath = (input: { · 18 unmodified lines
6559 `${input.sourceId}.json`,
6660 );
6761
68−const canonicalMaterializationHash = (input: {
69− materialization: SourceRecipeMaterialization;
70−}): string => {
71− const documents = [...input.materialization.documents]
72− .map((document) => ({
73− documentKind: document.documentKind,
74− documentKey: document.documentKey,
75− contentHash: document.contentHash,
76− }))
77− .sort((left, right) =>
78− left.documentKind.localeCompare(right.documentKind)
79− || left.documentKey.localeCompare(right.documentKey)
80− || left.contentHash.localeCompare(right.contentHash)
81− );
82− const schemaBundles = [...input.materialization.schemaBundles]
83− .map((bundle) => ({
84− bundleKind: bundle.bundleKind,
85− contentHash: bundle.contentHash,
86− }))
87− .sort((left, right) =>
88− left.bundleKind.localeCompare(right.bundleKind)
89− || left.contentHash.localeCompare(right.contentHash)
90− );
91− const operations = [...input.materialization.operations]
92− .map((operation) => ({
93− operationKey: operation.operationKey,
94− transportKind: operation.transportKind,
95− toolId: operation.toolId,
96− title: operation.title,
97− description: operation.description,
98− operationKind: operation.operationKind,
99− searchText: operation.searchText,
100− inputSchemaJson: operation.inputSchemaJson,
101− outputSchemaJson: operation.outputSchemaJson,
102− providerKind: operation.providerKind,
103− providerDataJson: operation.providerDataJson,
104− }))
105− .sort((left, right) => left.operationKey.localeCompare(right.operationKey));
106−
107− return contentHash(JSON.stringify({
108− schemaVersion: 1,
109− manifestHash: input.materialization.manifestHash,
110− manifestJson: input.materialization.manifestJson,
111− documents,
112− schemaBundles,
113− operations,
114− }));
115−};
116−
117−const bindRevisionId = <T extends { recipeRevisionId: string }>(
118− items: readonly T[],
119− recipeRevisionId: string,
120−): T[] =>
121− items.map((item) => ({
122− ...item,
123− recipeRevisionId,
124− }));
62+const snapshotHash = (snapshot: SourceCatalogSyncResult["snapshot"]): string =>
63+ contentHash(JSON.stringify(snapshot));
12564
12665 export const buildLocalSourceArtifact = (input: {
12766 source: Source;
128− materialization: SourceRecipeMaterialization;
67+ syncResult: SourceCatalogSyncResult;
12968 }): LocalSourceArtifact => {
130− const recipeId: SourceRecipeId = stableSourceRecipeId(input.source);
131− const now = Date.now();
132− const revision = createSourceRecipeRevisionRecord({
69+ const catalogId: SourceCatalogId = stableSourceCatalogId(input.source);
70+ const generatedAt = Date.now();
71+ const hash = snapshotHash(input.syncResult.snapshot);
72+ const revision = createSourceCatalogRevisionRecord({
13373 source: input.source,
134− recipeId,
74+ catalogId,
13575 revisionNumber: 1,
136− manifestJson: input.materialization.manifestJson,
137− manifestHash: input.materialization.manifestHash,
138− materializationHash: canonicalMaterializationHash({
139− materialization: input.materialization,
140− }),
76+ importMetadataJson: JSON.stringify(input.syncResult.snapshot.import),
77+ importMetadataHash: hash,
78+ snapshotHash: hash,
14179 });
14280
14381 return {
14482 version: LOCAL_SOURCE_ARTIFACT_VERSION,
14583 sourceId: input.source.id,
146− recipeId,
147− generatedAt: now,
84+ catalogId,
85+ generatedAt,
14886 revision,
149− documents: bindRevisionId(input.materialization.documents, revision.id),
150− schemaBundles: bindRevisionId(input.materialization.schemaBundles, revision.id),
151− operations: bindRevisionId(input.materialization.operations, revision.id),
87+ snapshot: input.syncResult.snapshot,
15288 };
15389 };
15490
@@ -175,13 +111,12 @@ export const readLocalSourceArtifact = (input: { · 20 unmodified lines
175111 );
176112 return yield* Effect.try({
177113 try: () => decodeLocalSourceArtifact(JSON.parse(content) as unknown),
178− catch: (cause) => {
179− return new LocalSourceArtifactDecodeError({
114+ catch: (cause) =>
115+ new LocalSourceArtifactDecodeError({
180116 message: `Invalid local source artifact at ${path}: ${unknownLocalErrorDetails(cause)}`,
181117 path,
182118 details: unknownLocalErrorDetails(cause),
183− });
184− },
119+ }),
185120 });
186121 });
187122
packages/control-plane/src/runtime/local-storage.ts+2−2
@@ -32,7 +32,7 @@ import { · 31 unmodified lines
3232 loadLocalWorkspaceState,
3333 writeLocalWorkspaceState,
3434 } from "./local-workspace-state";
35−import type { SourceRecipeMaterialization } from "./source-recipe-support";
35+import type { SourceCatalogSyncResult } from "./source-catalog-support";
3636 import type { Source } from "#schema";
3737
3838 export type InstallationStoreShape = {
@@ -80,7 +80,7 @@ export class WorkspaceStateStore extends Context.Tag( · 41 unmodified lines
8080 export type SourceArtifactStoreShape = {
8181 build: (input: {
8282 source: Source;
83− materialization: SourceRecipeMaterialization;
83+ syncResult: SourceCatalogSyncResult;
8484 }) => LocalSourceArtifact;
8585 read: (input: {
8686 context: ResolvedLocalWorkspaceContext;
packages/control-plane/src/runtime/oauth-loopback.test.ts+1−1
@@ -1,6 +1,6 @@
11 import { createServer } from "node:http";
22
3−import { describe, expect, it } from "vitest";
3+import { describe, expect, it } from "@effect/vitest";
44 import * as Effect from "effect/Effect";
55
66 import { 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
5656 };
5757
5858 describe("google discovery source adapter", () => {
59− it("materializes a real Google Sheets discovery doc into recipe content", () =>
59+ it("syncs a real Google Sheets discovery doc into snapshot content", () =>
6060 Effect.tryPromise({
6161 try: async () => {
6262 const discoveryDocument = await fetchLiveDiscoveryDocument();
@@ -86,8 +86,8 @@ describe("google discovery source adapter", () => { · 23 unmodified lines
8686 }),
8787 );
8888
89− const materialization = await Effect.runPromise(
90− googleDiscoverySourceAdapter.materializeSource({
89+ const syncResult = await Effect.runPromise(
90+ googleDiscoverySourceAdapter.syncCatalog({
9191 source,
9292 resolveSecretMaterial: () => Effect.fail(new Error("unexpected secret lookup")),
9393 resolveAuthMaterialForSlot: () =>
@@ -103,12 +103,12 @@ describe("google discovery source adapter", () => { · 9 unmodified lines
103103 }),
104104 );
105105
106− expect(materialization.documents[0]?.documentKind).toBe("google_discovery");
107− expect(materialization.schemaBundles.length).toBeGreaterThan(0);
108− expect(materialization.operations.length).toBeGreaterThan(50);
106+ expect(Object.values(syncResult.snapshot.catalog.documents)[0]?.kind).toBe("google-discovery");
107+ expect(Object.keys(syncResult.snapshot.catalog.resources)).not.toHaveLength(0);
108+ expect(Object.keys(syncResult.snapshot.catalog.capabilities).length).toBeGreaterThan(50);
109109 expect(
110− materialization.operations.some(
111− (operation) => operation.toolId === "spreadsheets.values.get",
110+ Object.values(syncResult.snapshot.catalog.capabilities).some((capability) =>
111+ capability.surface.toolPath.join(".") === "google.sheets.spreadsheets.values.get"
112112 ),
113113 ).toBe(true);
114114 });
packages/control-plane/src/runtime/source-adapters/google-discovery.ts+29−304
@@ -3,55 +3,32 @@ import { · 2 unmodified lines
33 HttpClient,
44 HttpClientRequest,
55 } from "@effect/platform";
6−import {
7− allowAllToolInteractions,
8− makeToolInvokerFromTools,
9− typeSignatureFromSchemaJson,
10−} from "@executor/codemode-core";
116 import {
127 compileGoogleDiscoveryToolDefinitions,
13− createGoogleDiscoveryToolFromDefinition,
14− decodeGoogleDiscoverySchemaRefTableJson,
158 extractGoogleDiscoveryManifest,
16− googleDiscoveryProviderDataJsonFromDefinition,
17− GoogleDiscoveryToolProviderDataSchema,
18− type GoogleDiscoveryToolManifest,
9+ type GoogleDiscoveryToolProviderData,
10+ googleDiscoveryProviderDataFromDefinition,
1911 } from "@executor/codemode-google-discovery";
20−import type {
21− Source,
22− SourceRecipeRevisionId,
23− StoredSourceRecipeDocumentRecord,
24− StoredSourceRecipeOperationRecord,
25− StoredSourceRecipeSchemaBundleRecord,
26−} from "#schema";
27−import {
28− SourceRecipeSchemaBundleIdSchema,
29− StringMapSchema,
30−} from "#schema";
31−import * as Either from "effect/Either";
12+import type { Source } from "#schema";
13+import { StringMapSchema } from "#schema";
3214 import * as Effect from "effect/Effect";
3315 import * as Schema from "effect/Schema";
3416
3517 import {
36− contentHash,
37− normalizeSearchText,
38−} from "../source-recipe-support";
39−import type {
40− SourceAdapter,
41− SourceAdapterMaterialization,
42−} from "./types";
18+ createGoogleDiscoveryCatalogSnapshot,
19+ type GoogleDiscoveryCatalogOperationInput,
20+} from "../source-catalog-snapshot";
21+import type { SourceAdapter } from "./types";
4322 import {
4423 ConnectHttpAuthSchema,
4524 ConnectHttpImportAuthSchema,
4625 ConnectOauthClientSchema,
47− createStandardToolDescriptor,
4826 decodeBindingConfig,
4927 decodeSourceBindingPayload,
5028 emptySourceBindingState,
5129 encodeBindingConfig,
5230 isSourceCredentialRequiredError,
5331 OptionalNullableStringSchema,
54− parseJsonValue,
5532 SourceCredentialRequiredError,
5633 } from "./shared";
5734
@@ -97,13 +74,6 @@ const GoogleDiscoverySourceBindingPayloadSchema = Schema.Struct({ · 39 unmodified lines
9774 type GoogleDiscoveryBindingConfig = typeof GoogleDiscoveryBindingConfigSchema.Type;
9875
9976 const GOOGLE_DISCOVERY_BINDING_CONFIG_VERSION = 1;
100−const GOOGLE_DISCOVERY_MATERIALIZATION_REVISION_ID =
101− "src_recipe_rev_materialization" as SourceRecipeRevisionId;
102−
103−const decodeGoogleDiscoveryProviderDataJson = Schema.decodeUnknownEither(
104− Schema.parseJson(GoogleDiscoveryToolProviderDataSchema),
105−);
106−
10777 const bindingHasAnyField = (
10878 value: unknown,
10979 fields: readonly string[],
@@ -205,129 +175,30 @@ const fetchGoogleDiscoveryDocumentWithHeaders = (input: { · 95 unmodified lines
205175 );
206176 }).pipe(Effect.provide(FetchHttpClient.layer));
207177
208−const toGoogleDiscoverySchemaBundleRecord = (input: {
209− recipeRevisionId: SourceRecipeRevisionId;
210− refTable: Readonly<Record<string, string>> | undefined;
211− now: number;
212−}): StoredSourceRecipeSchemaBundleRecord | null => {
213− const refEntries = Object.entries(input.refTable ?? {});
214− if (refEntries.length === 0) {
215− return null;
216− }
217−
218− const refs = Object.fromEntries(
219− refEntries.map(([ref, rawValue]) => {
220− try {
221− return [ref, JSON.parse(rawValue) as unknown];
222− } catch {
223− return [ref, rawValue];
224− }
225− }),
226− );
227− const refsJson = JSON.stringify(refs);
228−
229− return {
230− id: SourceRecipeSchemaBundleIdSchema.make(
231− `src_recipe_bundle_${crypto.randomUUID()}`,
232− ),
233− recipeRevisionId: input.recipeRevisionId,
234− bundleKind: "json_schema_ref_map",
235− refsJson,
236− contentHash: contentHash(refsJson),
237− createdAt: input.now,
238− updatedAt: input.now,
239− };
240−};
241−
242−const toGoogleDiscoveryRecipeOperationRecord = (input: {
243− recipeRevisionId: SourceRecipeRevisionId;
244− manifest: GoogleDiscoveryToolManifest;
178+const googleDiscoveryCatalogOperationFromDefinition = (input: {
179+ manifest: Parameters<typeof compileGoogleDiscoveryToolDefinitions>[0];
245180 definition: ReturnType<typeof compileGoogleDiscoveryToolDefinitions>[number];
246− now: number;
247−}): StoredSourceRecipeOperationRecord => ({
248− id: `src_recipe_op_${crypto.randomUUID()}`,
249− recipeRevisionId: input.recipeRevisionId,
250− operationKey: input.definition.toolId,
251− transportKind: "http",
181+}): GoogleDiscoveryCatalogOperationInput => ({
252182 toolId: input.definition.toolId,
253183 title: input.definition.name,
254184 description: input.definition.description,
255− operationKind:
185+ effect:
256186 input.definition.method === "get" || input.definition.method === "head"
257187 ? "read"
258188 : input.definition.method === "delete"
259189 ? "delete"
260190 : "write",
261− searchText: normalizeSearchText(
262− input.definition.toolId,
263− input.definition.rawToolId,
264− input.definition.methodId,
265− input.definition.name,
266− input.definition.description ?? undefined,
267− input.definition.method,
268− input.definition.path,
269− input.definition.group ?? undefined,
270− input.definition.leaf,
271− input.manifest.service,
272− input.manifest.versionName,
273− input.definition.scopes.join(" "),
274− ),
275− inputSchemaJson: input.definition.inputSchemaJson ?? null,
276− outputSchemaJson: input.definition.outputSchemaJson ?? null,
277− providerKind: "google_discovery",
278− providerDataJson: googleDiscoveryProviderDataJsonFromDefinition({
191+ inputSchema: input.definition.inputSchema,
192+ outputSchema: input.definition.outputSchema,
193+ providerData: googleDiscoveryProviderDataFromDefinition({
279194 service: input.manifest.service,
280195 version: input.manifest.versionName,
281196 rootUrl: input.manifest.rootUrl,
282197 servicePath: input.manifest.servicePath,
283198 definition: input.definition,
284− }),
285− createdAt: input.now,
286− updatedAt: input.now,
199+ }) as GoogleDiscoveryToolProviderData,
287200 });
288201
289−const googleDiscoveryDefinitionFromPersistedOperation = (input: {
290− path: string;
291− operation: StoredSourceRecipeOperationRecord;
292−}) => {
293− const decoded = input.operation.providerDataJson
294− ? decodeGoogleDiscoveryProviderDataJson(input.operation.providerDataJson)
295− : Either.left(new Error("Missing providerDataJson"));
296−
297− if (Either.isLeft(decoded)) {
298− throw new Error(`Invalid Google Discovery provider data for ${input.path}`);
299− }
300−
301− const providerData = decoded.right;
302− return {
303− definition: {
304− toolId: input.operation.toolId,
305− rawToolId: providerData.rawToolId,
306− methodId: providerData.methodId,
307− name: input.operation.title ?? input.operation.toolId,
308− description: input.operation.description,
309− group: providerData.group,
310− leaf: providerData.leaf,
311− method: providerData.invocation.method,
312− path: providerData.invocation.path,
313− flatPath: providerData.invocation.flatPath,
314− parameters: providerData.invocation.parameters,
315− requestSchemaId: providerData.invocation.requestSchemaId,
316− responseSchemaId: providerData.invocation.responseSchemaId,
317− scopes: providerData.invocation.scopes,
318− supportsMediaUpload: providerData.invocation.supportsMediaUpload,
319− supportsMediaDownload: providerData.invocation.supportsMediaDownload,
320− ...(input.operation.inputSchemaJson
321− ? { inputSchemaJson: input.operation.inputSchemaJson }
322− : {}),
323− ...(input.operation.outputSchemaJson
324− ? { outputSchemaJson: input.operation.outputSchemaJson }
325− : {}),
326− },
327− providerData,
328− };
329−};
330−
331202 const googleDiscoveryOauth2SetupConfig = (source: Source) =>
332203 Effect.gen(function* () {
333204 const bindingConfig = yield* googleDiscoveryBindingConfigFromSource(source);
@@ -369,8 +240,6 @@ export const googleDiscoverySourceAdapter: SourceAdapter = { · 35 unmodified lines
369240 bindingConfigVersion: GOOGLE_DISCOVERY_BINDING_CONFIG_VERSION,
370241 providerKey: "google_workspace",
371242 defaultImportAuthPolicy: "reuse_runtime",
372− primaryDocumentKind: "google_discovery",
373− primarySchemaBundleKind: "json_schema_ref_map",
374243 connectPayloadSchema: GoogleDiscoveryConnectPayloadSchema,
375244 executorAddInputSchema: GoogleDiscoveryExecutorAddInputSchema,
376245 executorAddHelpText: [
@@ -438,71 +307,7 @@ export const googleDiscoverySourceAdapter: SourceAdapter = { · 61 unmodified lines
438307 }),
439308 shouldAutoProbe: (source) =>
440309 source.enabled && (source.status === "draft" || source.status === "probing"),
441− parseManifest: ({ source, manifestJson }) =>
442− parseJsonValue<GoogleDiscoveryToolManifest>({
443− label: `Google Discovery manifest for ${source.id}`,
444− value: manifestJson,
445− }),
446− describePersistedOperation: ({ path, operation }) =>
447− Effect.gen(function* () {
448− const decoded = operation.providerDataJson
449− ? decodeGoogleDiscoveryProviderDataJson(operation.providerDataJson)
450− : Either.left(new Error("Missing providerDataJson"));
451− if (Either.isLeft(decoded)) {
452− return yield* Effect.fail(
453− new Error(`Invalid Google Discovery provider data for ${path}`),
454− );
455− }
456−
457− const providerData = decoded.right;
458− const method = providerData.invocation.method.toUpperCase();
459− return {
460− method: providerData.invocation.method,
461− pathTemplate: providerData.invocation.path,
462− rawToolId: providerData.rawToolId,
463− operationId: providerData.methodId,
464− group: providerData.group,
465− leaf: providerData.leaf,
466− tags: [],
467− searchText: normalizeSearchText(
468− path,
469− operation.toolId,
470− operation.title ?? undefined,
471− operation.description ?? undefined,
472− providerData.rawToolId,
473− providerData.methodId,
474− method,
475− providerData.invocation.path,
476− providerData.group ?? undefined,
477− providerData.leaf,
478− providerData.service,
479− providerData.version,
480− providerData.invocation.scopes.join(" "),
481− operation.searchText,
482− ),
483− interaction: method === "GET" || method === "HEAD" ? "auto" : "required",
484− approvalLabel: `${method} ${providerData.invocation.path}`,
485− } as const;
486− }),
487− createToolDescriptor: ({ source, operation, path, includeSchemas, schemaBundleId }) =>
488− createStandardToolDescriptor({
489− source,
490− operation,
491− path,
492− includeSchemas,
493− interaction: operation.operationKind === "read" ? "auto" : "required",
494− outputType: typeSignatureFromSchemaJson(
495− operation.outputSchemaJson ?? undefined,
496− "unknown",
497− 320,
498− ),
499− schemaBundleId,
500− }),
501− materializeSource: ({ source, resolveAuthMaterialForSlot }): Effect.Effect<
502− SourceAdapterMaterialization,
503− Error,
504− never
505− > =>
310+ syncCatalog: ({ source, resolveAuthMaterialForSlot }) =>
506311 Effect.gen(function* () {
507312 const bindingConfig = yield* googleDiscoveryBindingConfigFromSource(source);
508313 const auth = yield* resolveAuthMaterialForSlot("import");
@@ -529,37 +334,24 @@ export const googleDiscoverySourceAdapter: SourceAdapter = { · 20 unmodified lines
529334 );
530335 const definitions = compileGoogleDiscoveryToolDefinitions(manifest);
531336 const now = Date.now();
532− const schemaBundle = toGoogleDiscoverySchemaBundleRecord({
533− recipeRevisionId: GOOGLE_DISCOVERY_MATERIALIZATION_REVISION_ID,
534− refTable: manifest.schemaRefTable,
535− now,
536− });
537337
538338 return {
539− manifestJson: JSON.stringify(manifest),
540− manifestHash: manifest.sourceHash,
541− sourceHash: manifest.sourceHash,
542− documents: [
543− {
544− id: `src_recipe_doc_${crypto.randomUUID()}`,
545− recipeRevisionId: GOOGLE_DISCOVERY_MATERIALIZATION_REVISION_ID,
339+ snapshot: createGoogleDiscoveryCatalogSnapshot({
340+ source,
341+ documents: [{
546342 documentKind: "google_discovery",
547343 documentKey: bindingConfig.discoveryUrl,
548344 contentText: discoveryDocument,
549− contentHash: contentHash(discoveryDocument),
550345 fetchedAt: now,
551− createdAt: now,
552− updatedAt: now,
553− },
554− ],
555− schemaBundles: schemaBundle ? [schemaBundle] : [],
556− operations: definitions.map((definition) =>
557− toGoogleDiscoveryRecipeOperationRecord({
558− recipeRevisionId: GOOGLE_DISCOVERY_MATERIALIZATION_REVISION_ID,
559− manifest,
560− definition,
561− now,
562− })),
346+ }],
347+ operations: definitions.map((definition) =>
348+ googleDiscoveryCatalogOperationFromDefinition({
349+ manifest,
350+ definition,
351+ })
352+ ),
353+ }),
354+ sourceHash: manifest.sourceHash,
563355 };
564356 }),
565357 getOauth2SetupConfig: ({ source }) => googleDiscoveryOauth2SetupConfig(source),
@@ -568,71 +360,4 @@ export const googleDiscoverySourceAdapter: SourceAdapter = { · 2 unmodified lines
568360 ...input,
569361 redirectMode: input.redirectMode ?? "loopback",
570362 }),
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− }),
638363 };
packages/control-plane/src/runtime/source-adapters/graphql.ts+28−254
@@ -1,58 +1,34 @@
11 import {
2− allowAllToolInteractions,
32 applyCookiePlacementsToHeaders,
43 applyHttpQueryPlacementsToUrl,
54 applyJsonBodyPlacements,
6− makeToolInvokerFromTools,
7− typeSignatureFromSchemaJson,
85 } from "@executor/codemode-core";
9−import type {
10− GraphqlToolManifest,
11−} from "../graphql-tools";
126 import {
137 buildGraphqlToolPresentation,
148 compileGraphqlToolDefinitions,
15− createGraphqlToolFromPersistedOperation,
16− decodeGraphqlSchemaRefTableJson,
179 extractGraphqlManifest,
18− GraphqlToolProviderDataSchema,
1910 GRAPHQL_INTROSPECTION_QUERY,
11+ type GraphqlToolProviderData,
2012 } from "../graphql-tools";
21−import type {
22− Source,
23− SourceRecipeRevisionId,
24− StoredSourceRecipeDocumentRecord,
25− StoredSourceRecipeOperationRecord,
26− StoredSourceRecipeSchemaBundleRecord,
27−} from "#schema";
28−import {
29− StringMapSchema,
30− SourceRecipeSchemaBundleIdSchema,
31−} from "#schema";
32−import * as Either from "effect/Either";
13+import type { Source } from "#schema";
14+import { StringMapSchema } from "#schema";
3315 import * as Effect from "effect/Effect";
3416 import * as Schema from "effect/Schema";
3517
36−import { namespaceFromSourceName } from "../source-names";
3718 import {
38− contentHash,
39− normalizeSearchText,
40−} from "../source-recipe-support";
41−import type {
42− SourceAdapter,
43− SourceAdapterMaterialization,
44−} from "./types";
19+ createGraphqlCatalogSnapshot,
20+ type GraphqlCatalogOperationInput,
21+} from "../source-catalog-snapshot";
22+import type { SourceAdapter } from "./types";
4523 import {
4624 ConnectHttpAuthSchema,
4725 ConnectHttpImportAuthSchema,
48− createStandardToolDescriptor,
4926 decodeBindingConfig,
5027 decodeSourceBindingPayload,
5128 emptySourceBindingState,
5229 encodeBindingConfig,
5330 isSourceCredentialRequiredError,
5431 OptionalNullableStringSchema,
55− parseJsonValue,
5632 SourceCredentialRequiredError,
5733 SourceConnectCommonFieldsSchema,
5834 } from "./shared";
@@ -91,12 +67,6 @@ const GraphqlSourceBindingPayloadSchema = Schema.Struct({ · 32 unmodified lines
9167
9268 const GRAPHQL_BINDING_CONFIG_VERSION = 1;
9369
94−const GRAPHQL_MATERIALIZATION_REVISION_ID = "src_recipe_rev_materialization" as SourceRecipeRevisionId;
95−
96−const decodeGraphqlToolProviderDataJson = Schema.decodeUnknownEither(
97− Schema.parseJson(GraphqlToolProviderDataSchema),
98−);
99−
10070 const bindingHasAnyField = (
10171 value: unknown,
10272 fields: readonly string[],
@@ -198,92 +168,29 @@ const fetchGraphqlIntrospectionDocumentWithHeaders = (input: { · 95 unmodified lines
198168 cause instanceof Error ? cause : new Error(String(cause)),
199169 });
200170
201−const toGraphqlSchemaBundleRecord = (input: {
202− recipeRevisionId: SourceRecipeRevisionId;
203− manifest: Parameters<typeof buildGraphqlToolPresentation>[0]["manifest"];
204− now: number;
205−}): StoredSourceRecipeSchemaBundleRecord | null => {
206− const refEntries = Object.entries(input.manifest.schemaRefTable ?? {});
207− if (refEntries.length === 0) {
208− return null;
209− }
210−
211− const refs = Object.fromEntries(
212− refEntries.map(([ref, rawValue]) => {
213− try {
214− return [ref, JSON.parse(rawValue) as unknown];
215− } catch {
216− return [ref, rawValue];
217− }
218− }),
219− );
220− const refsJson = JSON.stringify(refs);
221−
222− return {
223− id: SourceRecipeSchemaBundleIdSchema.make(
224− `src_recipe_bundle_${crypto.randomUUID()}`,
225− ),
226− recipeRevisionId: input.recipeRevisionId,
227− bundleKind: "json_schema_ref_map",
228− refsJson,
229− contentHash: contentHash(refsJson),
230− createdAt: input.now,
231− updatedAt: input.now,
232− };
233−};
234−
235−const toGraphqlRecipeOperationRecord = (input: {
236− recipeRevisionId: SourceRecipeRevisionId;
171+const graphqlCatalogOperationFromDefinition = (input: {
237172 definition: ReturnType<typeof compileGraphqlToolDefinitions>[number];
238173 manifest: Parameters<typeof buildGraphqlToolPresentation>[0]["manifest"];
239− now: number;
240−}): StoredSourceRecipeOperationRecord => {
174+}): GraphqlCatalogOperationInput => {
241175 const presentation = buildGraphqlToolPresentation({
242176 manifest: input.manifest,
243177 definition: input.definition,
244178 });
245179
246180 return {
247− id: `src_recipe_op_${crypto.randomUUID()}`,
248− recipeRevisionId: input.recipeRevisionId,
249− operationKey: input.definition.toolId,
250− transportKind: "graphql",
251181 toolId: input.definition.toolId,
252182 title: input.definition.name,
253183 description: input.definition.description,
254− operationKind:
184+ effect:
255185 input.definition.operationType === "query"
256186 ? "read"
257− : input.definition.operationType === "mutation"
258− ? "write"
259− : "unknown",
260− searchText: normalizeSearchText(
261− input.definition.toolId,
262− input.definition.name,
263− input.definition.description,
264− input.definition.rawToolId,
265− input.definition.group,
266− input.definition.leaf,
267− input.definition.fieldName,
268− input.definition.operationType,
269− input.definition.operationName,
270− input.definition.searchTerms.join(" "),
271− ),
272− inputSchemaJson: presentation.inputSchemaJson ?? null,
273− outputSchemaJson: presentation.outputSchemaJson ?? null,
274− providerKind: "graphql",
275− providerDataJson: presentation.providerDataJson,
276− createdAt: input.now,
277− updatedAt: input.now,
187+ : "write",
188+ inputSchema: presentation.inputSchema,
189+ outputSchema: presentation.outputSchema,
190+ providerData: presentation.providerData as GraphqlToolProviderData,
278191 };
279192 };
280193
281−const primaryDocument = (
282− documents: readonly StoredSourceRecipeDocumentRecord[],
283−): StoredSourceRecipeDocumentRecord | null =>
284− documents.find((document) => document.documentKind === "graphql_introspection")
285− ?? null;
286−
287194 export const graphqlSourceAdapter: SourceAdapter = {
288195 key: "graphql",
289196 displayName: "GraphQL",
@@ -291,8 +198,6 @@ export const graphqlSourceAdapter: SourceAdapter = { · 1 unmodified line
291198 bindingConfigVersion: GRAPHQL_BINDING_CONFIG_VERSION,
292199 providerKey: "generic_graphql",
293200 defaultImportAuthPolicy: "reuse_runtime",
294− primaryDocumentKind: "graphql_introspection",
295− primarySchemaBundleKind: "json_schema_ref_map",
296201 connectPayloadSchema: GraphqlConnectPayloadSchema,
297202 executorAddInputSchema: GraphqlExecutorAddInputSchema,
298203 executorAddHelpText: [
@@ -349,68 +254,7 @@ export const graphqlSourceAdapter: SourceAdapter = { · 50 unmodified lines
349254 }),
350255 shouldAutoProbe: (source) =>
351256 source.enabled && (source.status === "draft" || source.status === "probing"),
352− parseManifest: ({ source, manifestJson }) =>
353− parseJsonValue<GraphqlToolManifest>({
354− label: `GraphQL manifest for ${source.id}`,
355− value: manifestJson,
356− }),
357− describePersistedOperation: ({ operation, path }) =>
358− Effect.gen(function* () {
359− const decoded = operation.providerDataJson
360− ? decodeGraphqlToolProviderDataJson(operation.providerDataJson)
361− : Either.left(new Error("Missing providerDataJson"));
362− if (Either.isLeft(decoded)) {
363− return yield* Effect.fail(
364− new Error(`Invalid GraphQL provider data for ${path}`),
365− );
366− }
367−
368− const providerData = decoded.right;
369−
370− return {
371− method: null,
372− pathTemplate: null,
373− rawToolId: providerData.rawToolId,
374− operationId: providerData.operationName,
375− group: providerData.group,
376− leaf: providerData.leaf,
377− tags: [],
378− searchText: normalizeSearchText(
379− path,
380− operation.toolId,
381− operation.title ?? undefined,
382− operation.description ?? undefined,
383− providerData.rawToolId ?? undefined,
384− providerData.group ?? undefined,
385− providerData.leaf ?? undefined,
386− providerData.fieldName ?? undefined,
387− providerData.operationType ?? undefined,
388− providerData.operationName ?? undefined,
389− operation.searchText,
390− ),
391− interaction: providerData.operationType === "query" ? "auto" : "required",
392− approvalLabel: providerData.operationType
393− ? `GraphQL ${providerData.operationType} ${path}`
394− : `GraphQL ${path}`,
395− } as const;
396− }),
397− searchNamespace: ({ source }) =>
398− source.namespace ?? namespaceFromSourceName(source.name),
399− createToolDescriptor: ({ source, operation, path, includeSchemas, schemaBundleId }) =>
400− createStandardToolDescriptor({
401− source,
402− operation,
403− path,
404− includeSchemas,
405− interaction: operation.operationKind === "read" ? "auto" : "required",
406− outputType: typeSignatureFromSchemaJson(
407− operation.outputSchemaJson ?? undefined,
408− "unknown",
409− 320,
410− ),
411− schemaBundleId,
412− }),
413− materializeSource: ({ source, resolveAuthMaterialForSlot }) =>
257+ syncCatalog: ({ source, resolveAuthMaterialForSlot }) =>
414258 Effect.gen(function* () {
415259 const bindingConfig = yield* graphqlBindingConfigFromSource(source);
416260 const auth = yield* resolveAuthMaterialForSlot("import");
@@ -447,94 +291,24 @@ export const graphqlSourceAdapter: SourceAdapter = { · 30 unmodified lines
447291
448292 const definitions = compileGraphqlToolDefinitions(manifest);
449293 const now = Date.now();
450− const schemaBundle = toGraphqlSchemaBundleRecord({
451− recipeRevisionId: GRAPHQL_MATERIALIZATION_REVISION_ID,
452− manifest,
453− now,
454− });
455294
456295 return {
457− manifestJson: JSON.stringify(manifest),
458− manifestHash: manifest.sourceHash,
459− sourceHash: manifest.sourceHash,
460− documents: [
461− {
462− id: `src_recipe_doc_${crypto.randomUUID()}`,
463− recipeRevisionId: GRAPHQL_MATERIALIZATION_REVISION_ID,
296+ snapshot: createGraphqlCatalogSnapshot({
297+ source,
298+ documents: [{
464299 documentKind: "graphql_introspection",
465300 documentKey: source.endpoint,
466301 contentText: graphqlDocument,
467− contentHash: contentHash(graphqlDocument),
468302 fetchedAt: now,
469− createdAt: now,
470− updatedAt: now,
471− },
472− ],
473− schemaBundles: schemaBundle ? [schemaBundle] : [],
474− operations: definitions.map((definition) =>
475− toGraphqlRecipeOperationRecord({
476− recipeRevisionId: GRAPHQL_MATERIALIZATION_REVISION_ID,
477− definition,
478− manifest,
479− now,
480− }),
481− ),
482− } satisfies SourceAdapterMaterialization;
483− }),
484− invokePersistedTool: ({
485− source,
486− path,
487− operation,
488− schemaBundle,
489− auth,
490− args,
491− context,
492− onElicitation,
493− }) =>
494− Effect.gen(function* () {
495− const bindingConfig = yield* graphqlBindingConfigFromSource(source);
496− const schemaRefTable = schemaBundle
497− ? decodeGraphqlSchemaRefTableJson(schemaBundle.refsJson)
498− : Either.right({});
499−
500− if (Either.isLeft(schemaRefTable)) {
501− return yield* Effect.fail(
502− new Error(`Invalid GraphQL schema bundle for ${path}`),
503− );
504− }
505− if (!operation.providerDataJson) {
506− return yield* Effect.fail(
507− new Error(`Missing GraphQL provider data for ${path}`),
508− );
509− }
510−
511− const tool = createGraphqlToolFromPersistedOperation({
512− path,
513− sourceKey: source.id,
514− endpoint: source.endpoint,
515− description: operation.description ?? operation.title ?? undefined,
516− inputSchemaJson: operation.inputSchemaJson ?? undefined,
517− outputSchemaJson: operation.outputSchemaJson ?? undefined,
518− providerDataJson: operation.providerDataJson,
519− schemaRefTable: schemaRefTable.right,
520− defaultHeaders: bindingConfig.defaultHeaders ?? {},
521− credentialPlacements: auth,
522− });
523−
524− return yield* makeToolInvokerFromTools({
525− tools: {
526− [path]: tool,
527− },
528− onToolInteraction: allowAllToolInteractions,
529− onElicitation,
530− }).invoke({
531− path,
532− args,
533− context,
534− }).pipe(
535− Effect.mapError((cause) =>
536− cause instanceof Error ? cause : new Error(String(cause)),
537− ),
538− );
303+ }],
304+ operations: definitions.map((definition) =>
305+ graphqlCatalogOperationFromDefinition({
306+ definition,
307+ manifest,
308+ })
309+ ),
310+ }),
311+ sourceHash: manifest.sourceHash,
312+ };
539313 }),
540314 };
packages/control-plane/src/runtime/source-adapters/index.ts+1−5
@@ -1,4 +1,4 @@
1−import type { Source, StoredSourceRecipeOperationRecord } from "#schema";
1+import type { Source } from "#schema";
22 import * as Schema from "effect/Schema";
33
44 import { graphqlSourceAdapter } from "./graphql";
@@ -67,10 +67,6 @@ export const getSourceAdapterForSource = (source: Pick<Source, "kind">): SourceA · 62 unmodified lines
6767 export const sourceBindingStateFromSource = (source: Source) =>
6868 getSourceAdapterForSource(source).bindingStateFromSource(source);
6969
70−export const getSourceAdapterForOperation = (
71− operation: Pick<StoredSourceRecipeOperationRecord, "providerKind">,
72−): SourceAdapter => getSourceAdapter(operation.providerKind);
73−
7470 export const hasSourceAdapterFamily = (
7571 key: string,
7672 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
44
55 import type { SourceAdapter } from "./types";
66 import {
7− createStandardToolDescriptor,
87 decodeBindingConfig,
98 decodeSourceBindingPayload,
109 emptySourceBindingState,
@@ -58,8 +57,6 @@ export const internalSourceAdapter: SourceAdapter = { · 47 unmodified lines
5857 bindingConfigVersion: INTERNAL_BINDING_CONFIG_VERSION,
5958 providerKey: "generic_internal",
6059 defaultImportAuthPolicy: "none",
61− primaryDocumentKind: null,
62− primarySchemaBundleKind: null,
6360 connectPayloadSchema: null,
6461 executorAddInputSchema: null,
6562 executorAddHelpText: null,
@@ -102,40 +99,28 @@ export const internalSourceAdapter: SourceAdapter = { · 36 unmodified lines
10299 };
103100 }),
104101 shouldAutoProbe: () => false,
105− parseManifest: () => Effect.succeed(null),
106− describePersistedOperation: ({ operation, path }) =>
107− Effect.succeed({
108− method: null,
109− pathTemplate: null,
110− rawToolId: null,
111− operationId: null,
112− group: null,
113− leaf: null,
114− tags: [],
115− searchText: [path, operation.toolId, operation.title ?? "", operation.description ?? "", operation.searchText]
116− .filter((part) => part.length > 0)
117− .join(" ")
118− .toLowerCase(),
119− interaction: "auto",
120− approvalLabel: null,
121− } as const),
122− createToolDescriptor: ({ source, operation, path, includeSchemas, schemaBundleId }) =>
123− createStandardToolDescriptor({
124− source,
125− operation,
126− path,
127− includeSchemas,
128− interaction: "auto",
129− schemaBundleId,
130− }),
131− materializeSource: () => Effect.succeed({
132− manifestJson: null,
133− manifestHash: null,
102+ syncCatalog: () => Effect.succeed({
103+ snapshot: {
104+ version: "ir.v1.snapshot",
105+ import: {
106+ sourceKind: "custom",
107+ adapterKey: "internal",
108+ importerVersion: "ir.v1.internal",
109+ importedAt: new Date().toISOString(),
110+ sourceConfigHash: "internal",
111+ },
112+ catalog: {
113+ version: "ir.v1",
114+ documents: {},
115+ resources: {},
116+ scopes: {},
117+ symbols: {},
118+ capabilities: {},
119+ executables: {},
120+ responseSets: {},
121+ diagnostics: {},
122+ },
123+ },
134124 sourceHash: null,
135− documents: [],
136− schemaBundles: [],
137− operations: [],
138125 }),
139− invokePersistedTool: ({ path }) =>
140− Effect.fail(new Error(`Unsupported stored tool provider for ${path}`)),
141126 };
packages/control-plane/src/runtime/source-adapters/mcp.ts+32−180
@@ -1,10 +1,7 @@
11 import {
2− allowAllToolInteractions,
32 applyCookiePlacementsToHeaders,
4− makeToolInvokerFromTools,
53 } from "@executor/codemode-core";
64 import {
7− createMcpToolsFromManifest,
85 createSdkMcpConnector,
96 discoverMcpToolsFromConnector,
107 type McpToolManifest,
@@ -12,8 +9,6 @@ import { · 1 unmodified line
129 } from "@executor/codemode-mcp";
1310 import type {
1411 Source,
15− SourceRecipeRevisionId,
16− StoredSourceRecipeOperationRecord,
1712 } from "#schema";
1813 import {
1914 SourceTransportSchema,
@@ -22,22 +17,23 @@ import { · 2 unmodified lines
2217 import * as Effect from "effect/Effect";
2318 import * as Schema from "effect/Schema";
2419
20+import {
21+ createMcpCatalogSnapshot,
22+ type McpCatalogOperationInput,
23+} from "../source-catalog-snapshot";
2524 import {
2625 contentHash,
27− normalizeSearchText,
28− type SourceRecipeMaterialization,
29−} from "../source-recipe-support";
26+ type SourceCatalogSyncResult,
27+} from "../source-catalog-support";
3028 import { namespaceFromSourceName } from "../source-names";
31−import type { SourceAdapter, SourceAdapterMaterialization } from "./types";
29+import type { SourceAdapter } from "./types";
3230 import {
33− createStandardToolDescriptor,
3431 decodeBindingConfig,
3532 decodeSourceBindingPayload,
3633 emptySourceBindingState,
3734 encodeBindingConfig,
3835 McpConnectFieldsSchema,
3936 OptionalNullableStringSchema,
40− parseJsonValue,
4137 SourceConnectCommonFieldsSchema,
4238 } from "./shared";
4339
@@ -87,17 +83,6 @@ const McpSourceBindingPayloadSchema = Schema.Struct({ · 43 unmodified lines
8783
8884 const MCP_BINDING_CONFIG_VERSION = 1;
8985
90−const McpToolProviderDataSchema = Schema.Struct({
91− kind: Schema.Literal("mcp"),
92− toolId: Schema.String,
93− toolName: Schema.String,
94− description: Schema.NullOr(Schema.String),
95−});
96−
97−const decodeMcpToolProviderDataJson = Schema.decodeUnknownEither(
98− Schema.parseJson(McpToolProviderDataSchema),
99−);
100−
10186 const bindingHasAnyField = (
10287 value: unknown,
10388 fields: readonly string[],
@@ -137,43 +122,25 @@ const mcpBindingConfigFromSource = ( · 33 unmodified lines
137122 } satisfies McpBindingConfig;
138123 });
139124
140−const toMcpRecipeOperationRecord = (input: {
141− recipeRevisionId: SourceRecipeRevisionId;
142− entry: McpToolManifestEntry;
143− now: number;
144−}): StoredSourceRecipeOperationRecord => ({
145− id: `src_recipe_op_${crypto.randomUUID()}`,
146− recipeRevisionId: input.recipeRevisionId,
147− operationKey: input.entry.toolId,
148− transportKind: "mcp",
149− toolId: input.entry.toolId,
150− title: input.entry.toolName,
151− description: input.entry.description ?? null,
152− operationKind: "unknown",
153− searchText: normalizeSearchText(
154− input.entry.toolId,
155− input.entry.toolName,
156− input.entry.description ?? undefined,
157− "mcp",
158− ),
159− inputSchemaJson: input.entry.inputSchemaJson ?? null,
160− outputSchemaJson: input.entry.outputSchemaJson ?? null,
161− providerKind: "mcp",
162− providerDataJson: JSON.stringify({
163− kind: "mcp",
164− toolId: input.entry.toolId,
165− toolName: input.entry.toolName,
166− description: input.entry.description ?? null,
167− }),
168− createdAt: input.now,
169− updatedAt: input.now,
125+const mcpCatalogOperationFromManifestEntry = (entry: McpToolManifestEntry): McpCatalogOperationInput => ({
126+ toolId: entry.toolId,
127+ title: entry.toolName,
128+ description: entry.description ?? null,
129+ effect: "action",
130+ inputSchema: entry.inputSchema,
131+ outputSchema: entry.outputSchema,
132+ providerData: {
133+ toolId: entry.toolId,
134+ toolName: entry.toolName,
135+ description: entry.description ?? null,
136+ },
170137 });
171138
172−export const materializationFromMcpManifestEntries = (input: {
173− recipeRevisionId: SourceRecipeRevisionId;
139+export const catalogSyncResultFromMcpManifestEntries = (input: {
140+ source: Source;
174141 endpoint: string;
175142 manifestEntries: readonly McpToolManifestEntry[];
176−}): SourceRecipeMaterialization => {
143+}): SourceCatalogSyncResult => {
177144 const now = Date.now();
178145 const manifest: McpToolManifest = {
179146 version: 1,
@@ -183,30 +150,17 @@ export const materializationFromMcpManifestEntries = (input: { · 3 unmodified lines
183150 const manifestHash = contentHash(manifestJson);
184151
185152 return {
186− manifestJson,
187− manifestHash,
188− sourceHash: manifestHash,
189− documents: [
190− {
191− id: `src_recipe_doc_${crypto.randomUUID()}`,
192− recipeRevisionId: input.recipeRevisionId,
153+ snapshot: createMcpCatalogSnapshot({
154+ source: input.source,
155+ documents: [{
193156 documentKind: "mcp_manifest",
194157 documentKey: input.endpoint,
195158 contentText: manifestJson,
196− contentHash: manifestHash,
197159 fetchedAt: now,
198− createdAt: now,
199− updatedAt: now,
200− },
201− ],
202− schemaBundles: [],
203− operations: input.manifestEntries.map((entry) =>
204− toMcpRecipeOperationRecord({
205− recipeRevisionId: input.recipeRevisionId,
206− entry,
207− now,
208− })
209− ),
160+ }],
161+ operations: input.manifestEntries.map(mcpCatalogOperationFromManifestEntry),
162+ }),
163+ sourceHash: manifestHash,
210164 };
211165 };
212166
@@ -217,8 +171,6 @@ export const mcpSourceAdapter: SourceAdapter = { · 4 unmodified lines
217171 bindingConfigVersion: MCP_BINDING_CONFIG_VERSION,
218172 providerKey: "generic_mcp",
219173 defaultImportAuthPolicy: "reuse_runtime",
220− primaryDocumentKind: "mcp_manifest",
221− primarySchemaBundleKind: null,
222174 connectPayloadSchema: McpConnectPayloadSchema,
223175 executorAddInputSchema: McpExecutorAddInputSchema,
224176 executorAddHelpText: [
@@ -280,53 +232,7 @@ export const mcpSourceAdapter: SourceAdapter = { · 55 unmodified lines
280232 };
281233 }),
282234 shouldAutoProbe: () => false,
283− parseManifest: ({ source, manifestJson }) =>
284− parseJsonValue<McpToolManifest>({
285− label: `MCP manifest for ${source.id}`,
286− value: manifestJson,
287− }),
288− describePersistedOperation: ({ operation, path }) =>
289− Effect.gen(function* () {
290− const decoded = operation.providerDataJson
291− ? decodeMcpToolProviderDataJson(operation.providerDataJson)
292− : null;
293− if (decoded && decoded._tag === "Left") {
294− return yield* Effect.fail(
295− new Error(`Invalid MCP provider data for ${path}`),
296− );
297− }
298−
299− const providerData = decoded?._tag === "Right" ? decoded.right : null;
300−
301− return {
302− method: null,
303− pathTemplate: null,
304− rawToolId: providerData?.toolId ?? null,
305− operationId: null,
306− group: null,
307− leaf: null,
308− tags: [],
309− searchText: normalizeSearchText(
310− path,
311− operation.toolId,
312− providerData?.toolName ?? operation.title ?? undefined,
313− providerData?.description ?? operation.description ?? undefined,
314− operation.searchText,
315− ),
316− interaction: "auto",
317− approvalLabel: null,
318− } as const;
319− }),
320− createToolDescriptor: ({ source, operation, path, includeSchemas, schemaBundleId }) =>
321− createStandardToolDescriptor({
322− source,
323− operation,
324− path,
325− includeSchemas,
326− interaction: "auto",
327− schemaBundleId,
328− }),
329− materializeSource: ({ source, resolveAuthMaterialForSlot }) =>
235+ syncCatalog: ({ source, resolveAuthMaterialForSlot }) =>
330236 Effect.gen(function* () {
331237 const bindingConfig = yield* mcpBindingConfigFromSource(source);
332238 const auth = yield* resolveAuthMaterialForSlot("import");
@@ -362,64 +268,10 @@ export const mcpSourceAdapter: SourceAdapter = { · 29 unmodified lines
362268 ),
363269 );
364270
365− return materializationFromMcpManifestEntries({
366− recipeRevisionId: "src_recipe_rev_materialization" as SourceRecipeRevisionId,
271+ return catalogSyncResultFromMcpManifestEntries({
272+ source,
367273 endpoint: source.endpoint,
368274 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,
385275 });
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− );
424276 }),
425277 };
packages/control-plane/src/runtime/source-adapters/openapi.ts+27−295
@@ -4,55 +4,34 @@ import { · 3 unmodified lines
44 HttpClientRequest,
55 } from "@effect/platform";
66 import {
7− allowAllToolInteractions,
87 applyCookiePlacementsToHeaders,
98 applyHttpQueryPlacementsToUrl,
10− makeToolInvokerFromTools,
119 } from "@executor/codemode-core";
1210 import {
1311 buildOpenApiToolPresentation,
1412 compileOpenApiToolDefinitions,
15− createOpenApiToolFromDefinition,
13+ type OpenApiToolProviderData,
1614 extractOpenApiManifest,
17− openApiOutputTypeSignatureFromSchemaJson,
18− OpenApiRefHintTableSchema,
19− OpenApiToolProviderDataSchema,
20− type OpenApiToolManifest,
2115 } from "@executor/codemode-openapi";
22−import type {
23− Source,
24− SourceRecipeRevisionId,
25− StoredSourceRecipeDocumentRecord,
26− StoredSourceRecipeOperationRecord,
27− StoredSourceRecipeSchemaBundleRecord,
28−} from "#schema";
29−import {
30− StringMapSchema,
31− SourceRecipeSchemaBundleIdSchema,
32−} from "#schema";
33−import * as Either from "effect/Either";
16+import type { Source } from "#schema";
17+import { StringMapSchema } from "#schema";
3418 import * as Effect from "effect/Effect";
3519 import * as Schema from "effect/Schema";
3620
3721 import {
38− contentHash,
39− normalizeSearchText,
40−} from "../source-recipe-support";
41−import type {
42− SourceAdapter,
43− SourceAdapterMaterialization,
44−} from "./types";
22+ createOpenApiCatalogSnapshot,
23+ type OpenApiCatalogOperationInput,
24+} from "../source-catalog-snapshot";
25+import type { SourceAdapter } from "./types";
4526 import {
4627 ConnectHttpAuthSchema,
4728 ConnectHttpImportAuthSchema,
48− createStandardToolDescriptor,
4929 decodeBindingConfig,
5030 decodeSourceBindingPayload,
5131 emptySourceBindingState,
5232 encodeBindingConfig,
5333 isSourceCredentialRequiredError,
5434 OptionalNullableStringSchema,
55− parseJsonValue,
5635 SourceCredentialRequiredError,
5736 SourceConnectCommonFieldsSchema,
5837 } from "./shared";
@@ -98,16 +77,6 @@ type OpenApiBindingConfig = { · 39 unmodified lines
9877
9978 const OPENAPI_BINDING_CONFIG_VERSION = 1;
10079
101−const OPENAPI_MATERIALIZATION_REVISION_ID = "src_recipe_rev_materialization" as SourceRecipeRevisionId;
102−
103−const decodeOpenApiProviderDataJson = Schema.decodeUnknownEither(
104− Schema.parseJson(OpenApiToolProviderDataSchema),
105−);
106−
107−const decodeOpenApiRefHintTableJson = Schema.decodeUnknownEither(
108− Schema.parseJson(OpenApiRefHintTableSchema),
109−);
110−
11180 const bindingHasAnyField = (
11281 value: unknown,
11382 fields: readonly string[],
@@ -198,137 +167,30 @@ const fetchOpenApiDocumentWithHeaders = (input: { · 84 unmodified lines
198167 );
199168 }).pipe(Effect.provide(FetchHttpClient.layer));
200169
201−const toOpenApiRecipeOperationRecord = (input: {
202− recipeRevisionId: SourceRecipeRevisionId;
203− definition: ReturnType<typeof compileOpenApiToolDefinitions>[number];
204− now: number;
205−}): StoredSourceRecipeOperationRecord => {
170+const openApiCatalogOperationFromDefinition = (
171+ definition: ReturnType<typeof compileOpenApiToolDefinitions>[number],
172+): OpenApiCatalogOperationInput => {
206173 const presentation = buildOpenApiToolPresentation({
207− definition: input.definition,
174+ definition,
208175 });
209− const method = input.definition.method.toUpperCase();
176+ const method = definition.method.toUpperCase();
210177
211178 return {
212− id: `src_recipe_op_${crypto.randomUUID()}`,
213− recipeRevisionId: input.recipeRevisionId,
214− operationKey: input.definition.toolId,
215− transportKind: "http",
216− toolId: input.definition.toolId,
217− title: input.definition.name,
218− description: input.definition.description,
219− operationKind:
179+ toolId: definition.toolId,
180+ title: definition.name,
181+ description: definition.description,
182+ effect:
220183 method === "GET" || method === "HEAD"
221184 ? "read"
222185 : method === "DELETE"
223186 ? "delete"
224187 : "write",
225− searchText: normalizeSearchText(
226− input.definition.toolId,
227− input.definition.name,
228− input.definition.description,
229− input.definition.rawToolId,
230− input.definition.operationId ?? undefined,
231− input.definition.method,
232− input.definition.path,
233− input.definition.group,
234− input.definition.leaf,
235− input.definition.tags.join(" "),
236− ),
237− inputSchemaJson: presentation.inputSchemaJson ?? null,
238− outputSchemaJson: presentation.outputSchemaJson ?? null,
239− providerKind: "openapi",
240− providerDataJson: presentation.providerDataJson,
241− createdAt: input.now,
242− updatedAt: input.now,
243−};
244−};
245−
246−const toOpenApiSchemaBundleRecord = (input: {
247− recipeRevisionId: SourceRecipeRevisionId;
248− refTable: Readonly<Record<string, string>> | undefined;
249− now: number;
250−}): StoredSourceRecipeSchemaBundleRecord | null => {
251− const refEntries = Object.entries(input.refTable ?? {});
252− if (refEntries.length === 0) {
253− return null;
254− }
255−
256− const refs = Object.fromEntries(
257− refEntries.map(([ref, rawValue]) => {
258− try {
259− return [ref, JSON.parse(rawValue) as unknown];
260− } catch {
261− return [ref, rawValue];
262− }
263− }),
264− );
265− const refsJson = JSON.stringify(refs);
266−
267− return {
268− id: SourceRecipeSchemaBundleIdSchema.make(
269− `src_recipe_bundle_${crypto.randomUUID()}`,
270− ),
271− recipeRevisionId: input.recipeRevisionId,
272− bundleKind: "json_schema_ref_map",
273− refsJson,
274− contentHash: contentHash(refsJson),
275− createdAt: input.now,
276− updatedAt: input.now,
277− };
278−};
279−
280−const openApiDefinitionFromPersistedOperation = (input: {
281− source: Source;
282− path: string;
283− operation: StoredSourceRecipeOperationRecord;
284−}) => {
285− const decoded = input.operation.providerDataJson
286− ? decodeOpenApiProviderDataJson(input.operation.providerDataJson)
287− : Either.left(new Error("Missing providerDataJson"));
288−
289− if (Either.isLeft(decoded)) {
290− throw new Error(`Invalid OpenAPI provider data for ${input.path}`);
291− }
292−
293− return {
294− toolId: input.operation.toolId,
295− rawToolId: decoded.right.rawToolId,
296− ...(decoded.right.operationId
297− ? { operationId: decoded.right.operationId }
298− : {}),
299− name: input.operation.title ?? input.operation.toolId,
300− description:
301− input.operation.description ??
302− `${decoded.right.method.toUpperCase()} ${decoded.right.path}`,
303− group: decoded.right.group,
304− leaf: decoded.right.leaf,
305− tags: decoded.right.tags,
306− ...(decoded.right.versionSegment
307− ? { versionSegment: decoded.right.versionSegment }
308− : {}),
309− method: decoded.right.method,
310− path: decoded.right.path,
311− invocation: decoded.right.invocation,
312− operationHash: decoded.right.operationHash,
313− typing: {
314− ...(input.operation.inputSchemaJson
315− ? { inputSchemaJson: input.operation.inputSchemaJson }
316− : {}),
317− ...(input.operation.outputSchemaJson
318− ? { outputSchemaJson: input.operation.outputSchemaJson }
319− : {}),
320− },
321− ...(decoded.right.documentation
322− ? { documentation: decoded.right.documentation }
323− : {}),
188+ inputSchema: presentation.inputSchema,
189+ outputSchema: presentation.outputSchema,
190+ providerData: presentation.providerData as OpenApiToolProviderData,
324191 };
325192 };
326193
327−const primaryDocument = (
328− documents: readonly StoredSourceRecipeDocumentRecord[],
329−): StoredSourceRecipeDocumentRecord | null =>
330− documents.find((document) => document.documentKind === "openapi") ?? null;
331−
332194 export const openApiSourceAdapter: SourceAdapter = {
333195 key: "openapi",
334196 displayName: "OpenAPI",
@@ -336,8 +198,6 @@ export const openApiSourceAdapter: SourceAdapter = { · 1 unmodified line
336198 bindingConfigVersion: OPENAPI_BINDING_CONFIG_VERSION,
337199 providerKey: "generic_http",
338200 defaultImportAuthPolicy: "reuse_runtime",
339− primaryDocumentKind: "openapi",
340− primarySchemaBundleKind: "json_schema_ref_map",
341201 connectPayloadSchema: OpenApiConnectPayloadSchema,
342202 executorAddInputSchema: OpenApiExecutorAddInputSchema,
343203 executorAddHelpText: [
@@ -398,65 +258,7 @@ export const openApiSourceAdapter: SourceAdapter = { · 54 unmodified lines
398258 }),
399259 shouldAutoProbe: (source) =>
400260 source.enabled && (source.status === "draft" || source.status === "probing"),
401− parseManifest: ({ source, manifestJson }) =>
402− parseJsonValue<OpenApiToolManifest>({
403− label: `OpenAPI manifest for ${source.id}`,
404− value: manifestJson,
405− }),
406− describePersistedOperation: ({ operation, path }) =>
407− Effect.gen(function* () {
408− const decoded = operation.providerDataJson
409− ? decodeOpenApiProviderDataJson(operation.providerDataJson)
410− : Either.left(new Error("Missing providerDataJson"));
411− if (Either.isLeft(decoded)) {
412− return yield* Effect.fail(
413− new Error(`Invalid OpenAPI provider data for ${path}`),
414− );
415− }
416−
417− const providerData = decoded.right;
418− const method = providerData.method.toUpperCase();
419−
420− return {
421− method: providerData.method,
422− pathTemplate: providerData.path,
423− rawToolId: providerData.rawToolId,
424− operationId: providerData.operationId ?? null,
425− group: providerData.group,
426− leaf: providerData.leaf,
427− tags: providerData.tags,
428− searchText: normalizeSearchText(
429− path,
430− operation.toolId,
431− operation.title ?? undefined,
432− operation.description ?? undefined,
433− providerData.rawToolId,
434− providerData.operationId ?? undefined,
435− method,
436− providerData.path,
437− providerData.group,
438− providerData.leaf,
439− providerData.tags.join(" "),
440− operation.searchText,
441− ),
442− interaction: method === "GET" || method === "HEAD" ? "auto" : "required",
443− approvalLabel: `${method} ${providerData.path}`,
444− } as const;
445− }),
446− createToolDescriptor: ({ source, operation, path, includeSchemas, schemaBundleId }) =>
447− createStandardToolDescriptor({
448− source,
449− operation,
450− path,
451− includeSchemas,
452− interaction: operation.operationKind === "read" ? "auto" : "required",
453− outputType: openApiOutputTypeSignatureFromSchemaJson(
454− operation.outputSchemaJson ?? undefined,
455− 320,
456− ),
457− schemaBundleId,
458− }),
459− materializeSource: ({ source, resolveAuthMaterialForSlot }) =>
261+ syncCatalog: ({ source, resolveAuthMaterialForSlot }) =>
460262 Effect.gen(function* () {
461263 const bindingConfig = yield* openApiBindingConfigFromSource(source);
462264
@@ -491,89 +293,19 @@ export const openApiSourceAdapter: SourceAdapter = { · 28 unmodified lines
491293
492294 const definitions = compileOpenApiToolDefinitions(manifest);
493295 const now = Date.now();
494− const schemaBundle = toOpenApiSchemaBundleRecord({
495− recipeRevisionId: OPENAPI_MATERIALIZATION_REVISION_ID,
496− refTable: manifest.refHintTable,
497− now,
498− });
499296
500297 return {
501− manifestJson: JSON.stringify(manifest),
502− manifestHash: manifest.sourceHash,
503− sourceHash: manifest.sourceHash,
504− documents: [
505− {
506− id: `src_recipe_doc_${crypto.randomUUID()}`,
507− recipeRevisionId: OPENAPI_MATERIALIZATION_REVISION_ID,
298+ snapshot: createOpenApiCatalogSnapshot({
299+ source,
300+ documents: [{
508301 documentKind: "openapi",
509302 documentKey: bindingConfig.specUrl,
510303 contentText: openApiDocument,
511− contentHash: contentHash(openApiDocument),
512304 fetchedAt: now,
513− createdAt: now,
514− updatedAt: now,
515− },
516− ],
517− schemaBundles: schemaBundle ? [schemaBundle] : [],
518− operations: definitions.map((definition) =>
519− toOpenApiRecipeOperationRecord({
520− recipeRevisionId: OPENAPI_MATERIALIZATION_REVISION_ID,
521− definition,
522− now,
523− }),
524− ),
525− } satisfies SourceAdapterMaterialization;
526− }),
527− invokePersistedTool: ({
528− source,
529− path,
530− operation,
531− schemaBundle,
532− auth,
533− args,
534− context,
535− onElicitation,
536− }) =>
537− Effect.gen(function* () {
538− const bindingConfig = yield* openApiBindingConfigFromSource(source);
539− const refHintTable = schemaBundle
540− ? decodeOpenApiRefHintTableJson(schemaBundle.refsJson)
541− : Either.right({});
542−
543− if (Either.isLeft(refHintTable)) {
544− return yield* Effect.fail(
545− new Error(`Invalid OpenAPI schema bundle for ${path}`),
546− );
547− }
548−
549− const tool = createOpenApiToolFromDefinition({
550− definition: openApiDefinitionFromPersistedOperation({
551− source,
552− path,
553− operation,
305+ }],
306+ operations: definitions.map(openApiCatalogOperationFromDefinition),
554307 }),
555− path,
556− sourceKey: source.id,
557− baseUrl: source.endpoint,
558− defaultHeaders: bindingConfig.defaultHeaders ?? {},
559− credentialPlacements: auth,
560− refHintTable: refHintTable.right,
561− });
562−
563− return yield* makeToolInvokerFromTools({
564− tools: {
565− [path]: tool,
566− },
567− onToolInteraction: allowAllToolInteractions,
568− onElicitation,
569− }).invoke({
570− path,
571− args,
572− context,
573− }).pipe(
574− Effect.mapError((cause) =>
575− cause instanceof Error ? cause : new Error(String(cause)),
576− ),
577− );
308+ sourceHash: manifest.sourceHash,
309+ };
578310 }),
579311 };
packages/control-plane/src/runtime/source-adapters/shared.ts+0−50
@@ -1,9 +1,3 @@
1−import {
2− type ToolDescriptor,
3− type ToolPath,
4− type ToolSchemaBundle,
5− typeSignatureFromSchemaJson,
6−} from "@executor/codemode-core";
71 import {
82 SourceOauthClientInputSchema,
93 type CredentialSlot,
@@ -15,10 +9,8 @@ import { · 5 unmodified lines
159 } from "#schema";
1610 import type {
1711 SecretRef,
18− Source,
1912 SourceBinding,
2013 SourceTransport,
21− StoredSourceRecipeOperationRecord,
2214 StringMap,
2315 } from "#schema";
2416 import * as Effect from "effect/Effect";
@@ -28,8 +20,6 @@ import { · 3 unmodified lines
2820 TrimmedNonEmptyStringSchema,
2921 } from "../../api/string-schemas";
3022
31−const asToolPath = (value: string): ToolPath => value as ToolPath;
32−
3323 export const OptionalNullableStringSchema = Schema.optional(
3424 Schema.NullOr(Schema.String),
3525 );
@@ -236,43 +226,3 @@ export const decodeSourceBindingPayload = <A>(input: { · 200 unmodified lines
236226 );
237227 },
238228 });
239−
240−export const firstSchemaBundle = (input: {
241− schemaBundles: readonly ToolSchemaBundle[];
242− preferredKind: string | null;
243−}): ToolSchemaBundle | null =>
244− (input.preferredKind
245− ? input.schemaBundles.find((schemaBundle) => schemaBundle.kind === input.preferredKind)
246− : null)
247− ?? input.schemaBundles[0]
248− ?? null;
249−
250−export const createStandardToolDescriptor = (input: {
251− source: Source;
252− operation: StoredSourceRecipeOperationRecord;
253− path: string;
254− includeSchemas: boolean;
255− interaction: "auto" | "required";
256− outputType?: string | undefined;
257− schemaBundleId?: string | null;
258−}): ToolDescriptor => ({
259− path: asToolPath(input.path),
260− sourceKey: input.source.id,
261− description: input.operation.description ?? input.operation.title ?? undefined,
262− interaction: input.interaction,
263− inputType: typeSignatureFromSchemaJson(
264− input.operation.inputSchemaJson ?? undefined,
265− "unknown",
266− 320,
267− ),
268− ...(input.outputType ? { outputType: input.outputType } : {}),
269− inputSchemaJson: input.includeSchemas ? input.operation.inputSchemaJson ?? undefined : undefined,
270− outputSchemaJson: input.includeSchemas ? input.operation.outputSchemaJson ?? undefined : undefined,
271− ...(input.schemaBundleId ? { schemaBundleId: input.schemaBundleId } : {}),
272− ...(input.operation.providerKind
273− ? { providerKind: input.operation.providerKind }
274− : {}),
275− ...(input.operation.providerDataJson
276− ? { providerDataJson: input.operation.providerDataJson }
277− : {}),
278−});
packages/control-plane/src/runtime/source-adapters/types.ts+3−74
@@ -1,10 +1,4 @@
11 import type {
2− OnElicitation,
3− ToolDescriptor,
4− ToolSchemaBundle,
5−} from "@executor/codemode-core";
6−import type {
7− AccountId,
82 CredentialSlot,
93 OAuth2ClientAuthenticationMethod,
104 SourceOauthClientInput,
@@ -12,10 +6,6 @@ import type { · 1 unmodified line
126 SourceBinding,
137 SourceImportAuthPolicy,
148 SourceTransport,
15− StoredSourceRecipeDocumentRecord,
16− StoredSourceRecipeOperationRecord,
17− StoredSourceRecipeSchemaBundleRecord,
18− StoredSourceRecipeRevisionRecord,
199 StoredSourceRecord,
2010 StringMap,
2111 } from "#schema";
@@ -24,6 +14,7 @@ import * as Schema from "effect/Schema"; · 2 unmodified lines
2414
2515 import type { ResolvedSourceAuthMaterial } from "../source-auth-material";
2616 import type { ResolveSecretMaterial } from "../secret-material-providers";
17+import type { SourceCatalogSyncResult } from "../source-catalog-support";
2718
2819 export type SourceAdapterFamily = "http_api" | "mcp" | "internal";
2920 export type SourceAdapterInputSchema = Schema.Schema<any, any, never>;
@@ -47,28 +38,6 @@ export type SourceAdapterSyncInput = { · 17 unmodified lines
4738 >;
4839 };
4940
50−export type SourceAdapterMaterialization = {
51− manifestJson: string | null;
52− manifestHash: string | null;
53− sourceHash: string | null;
54− documents: readonly StoredSourceRecipeDocumentRecord[];
55− schemaBundles: readonly StoredSourceRecipeSchemaBundleRecord[];
56− operations: readonly StoredSourceRecipeOperationRecord[];
57−};
58−
59−export type SourceAdapterPersistedOperationMetadata = {
60− method: string | null;
61− pathTemplate: string | null;
62− rawToolId: string | null;
63− operationId: string | null;
64− group: string | null;
65− leaf: string | null;
66− tags: readonly string[];
67− searchText: string;
68− interaction: "auto" | "required";
69− approvalLabel: string | null;
70−};
71−
7241 export type SourceAdapterOauth2SetupConfig = {
7342 providerKey: string;
7443 authorizationEndpoint: string;
@@ -80,20 +49,6 @@ export type SourceAdapterOauth2SetupConfig = { · 5 unmodified lines
8049 authorizationParams?: Readonly<Record<string, string>>;
8150 };
8251
83−export type SourceAdapterInvokePersistedToolInput = {
84− workspaceId: Source["workspaceId"];
85− accountId: AccountId;
86− source: Source;
87− path: string;
88− operation: StoredSourceRecipeOperationRecord;
89− schemaBundle: ToolSchemaBundle | null;
90− manifestJson: string | null;
91− auth: ResolvedSourceAuthMaterial;
92− args: unknown;
93− context?: Record<string, unknown>;
94− onElicitation?: OnElicitation;
95−};
96−
9752 export type SourceAdapter = {
9853 key: string;
9954 displayName: string;
@@ -101,8 +56,6 @@ export type SourceAdapter = { · 1 unmodified line
10156 bindingConfigVersion: number;
10257 providerKey: string;
10358 defaultImportAuthPolicy: SourceImportAuthPolicy;
104− primaryDocumentKind: string | null;
105− primarySchemaBundleKind: string | null;
10659 connectPayloadSchema: SourceAdapterInputSchema | null;
10760 executorAddInputSchema: SourceAdapterInputSchema | null;
10861 executorAddHelpText: readonly string[] | null;
@@ -115,30 +68,9 @@ export type SourceAdapter = { · 6 unmodified lines
11568 sourceConfigFromSource: (source: Source) => Record<string, unknown>;
11669 validateSource: (source: Source) => Effect.Effect<Source, Error, never>;
11770 shouldAutoProbe: (source: Source) => boolean;
118− parseManifest: (input: {
119− source: Pick<Source, "id" | "kind">;
120− manifestJson: string | null;
121− }) => Effect.Effect<unknown | null, Error, never>;
122− describePersistedOperation: (input: {
123− source: Source;
124− path: string;
125− operation: StoredSourceRecipeOperationRecord;
126− }) => Effect.Effect<SourceAdapterPersistedOperationMetadata, Error, never>;
127− searchNamespace?: (input: {
128− source: Source;
129− path: string;
130− operation: StoredSourceRecipeOperationRecord;
131− }) => string;
132− createToolDescriptor: (input: {
133− source: Source;
134− operation: StoredSourceRecipeOperationRecord;
135− path: string;
136− schemaBundleId?: string | null;
137− includeSchemas: boolean;
138− }) => ToolDescriptor;
139− materializeSource: (
71+ syncCatalog: (
14072 input: SourceAdapterSyncInput,
141− ) => Effect.Effect<SourceAdapterMaterialization, Error, never>;
73+ ) => Effect.Effect<SourceCatalogSyncResult, Error, never>;
14274 getOauth2SetupConfig?: (input: {
14375 source: Source;
14476 slot: CredentialSlot;
@@ -146,7 +78,4 @@ export type SourceAdapter = { · 1 unmodified line
14678 normalizeOauthClientInput?: (
14779 input: SourceOauthClientInput,
14880 ) => Effect.Effect<SourceOauthClientInput, Error, never>;
149− invokePersistedTool: (
150− input: SourceAdapterInvokePersistedToolInput,
151− ) => Effect.Effect<unknown, Error, never>;
15281 };
packages/control-plane/src/runtime/source-auth-material.test.ts+1−1
@@ -1,4 +1,4 @@
1−import { describe, expect, it } from "vitest";
1+import { describe, expect, it } from "@effect/vitest";
22 import * as Effect from "effect/Effect";
33
44 import {
packages/control-plane/src/runtime/source-auth-service.ts+19−19
@@ -68,9 +68,9 @@ import { · 67 unmodified lines
6868 } from "./secret-material-providers";
6969 import { upsertOauth2AuthorizedUserLeaseFromTokenResponse } from "./auth-leases";
7070 import {
71− RuntimeSourceMaterializationService,
72− type RuntimeSourceMaterializationShape,
73−} from "./source-materialization";
71+ RuntimeSourceCatalogSyncService,
72+ type RuntimeSourceCatalogSyncShape,
73+} from "./source-catalog-sync";
7474 import {
7575 buildOAuth2AuthorizationUrl,
7676 createPkceCodeVerifier,
@@ -978,7 +978,7 @@ const startOauth2PkceSourceCredentialSetup = (input: { · 901 unmodified lines
978978 const connectMcpSourceInternal = (input: {
979979 rows: ControlPlaneStoreShape;
980980 sourceStore: RuntimeSourceStore;
981− sourceMaterialization: RuntimeSourceMaterializationShape;
981+ sourceCatalogSync: RuntimeSourceCatalogSyncShape;
982982 getLocalServerBaseUrl?: () => string | undefined;
983983 baseUrl?: string | null;
984984 workspaceId: WorkspaceId;
@@ -1087,7 +1087,7 @@ const connectMcpSourceInternal = (input: { · 102 unmodified lines
10871087 const persistedDraft = yield* input.sourceStore.persistSource(draftSource, {
10881088 actorAccountId: input.actorAccountId,
10891089 });
1090− yield* input.sourceMaterialization.sync({
1090+ yield* input.sourceCatalogSync.sync({
10911091 source: persistedDraft,
10921092 actorAccountId: input.actorAccountId,
10931093 });
@@ -1110,7 +1110,7 @@ const connectMcpSourceInternal = (input: { · 16 unmodified lines
11101110 auth: { kind: "none" },
11111111 });
11121112 const indexed = yield* Effect.either(
1113− input.sourceMaterialization.persistMcpRecipeMaterializationFromManifest({
1113+ input.sourceCatalogSync.persistMcpCatalogSnapshotFromManifest({
11141114 source: connected,
11151115 manifestEntries: result.manifest.tools,
11161116 }),
@@ -1206,7 +1206,7 @@ const connectMcpSourceInternal = (input: { · 89 unmodified lines
12061206 const addExecutorHttpSource = (input: {
12071207 rows: ControlPlaneStoreShape;
12081208 sourceStore: RuntimeSourceStore;
1209− sourceMaterialization: RuntimeSourceMaterializationShape;
1209+ sourceCatalogSync: RuntimeSourceCatalogSyncShape;
12101210 sourceInput: Extract<ExecutorAddSourceInput, { kind: "openapi" | "graphql" }>;
12111211 storeSecretMaterial: StoreSecretMaterial;
12121212 resolveSecretMaterial: ResolveSecretMaterial;
@@ -1358,7 +1358,7 @@ const addExecutorHttpSource = (input: { · 145 unmodified lines
13581358 }
13591359
13601360 const synced = yield* Effect.either(
1361− input.sourceMaterialization.sync({
1361+ input.sourceCatalogSync.sync({
13621362 source: {
13631363 ...persistedDraft,
13641364 status: "connected",
@@ -1409,7 +1409,7 @@ const addExecutorHttpSource = (input: { · 44 unmodified lines
14091409 const addExecutorGoogleDiscoverySource = (input: {
14101410 rows: ControlPlaneStoreShape;
14111411 sourceStore: RuntimeSourceStore;
1412− sourceMaterialization: RuntimeSourceMaterializationShape;
1412+ sourceCatalogSync: RuntimeSourceCatalogSyncShape;
14131413 sourceInput: Extract<ExecutorAddSourceInput, { kind: "google_discovery" }>;
14141414 storeSecretMaterial: StoreSecretMaterial;
14151415 resolveSecretMaterial: ResolveSecretMaterial;
@@ -1578,7 +1578,7 @@ const addExecutorGoogleDiscoverySource = (input: { · 162 unmodified lines
15781578 }
15791579
15801580 const synced = yield* Effect.either(
1581− input.sourceMaterialization.sync({
1581+ input.sourceCatalogSync.sync({
15821582 source: {
15831583 ...persistedDraft,
15841584 status: "connected",
@@ -1671,7 +1671,7 @@ type RuntimeSourceAuthDependencies = { · 86 unmodified lines
16711671 rows: ControlPlaneStoreShape;
16721672 liveExecutionManager: LiveExecutionManager;
16731673 sourceStore: RuntimeSourceStore;
1674− sourceMaterialization: RuntimeSourceMaterializationShape;
1674+ sourceCatalogSync: RuntimeSourceCatalogSyncShape;
16751675 resolveSecretMaterial: ResolveSecretMaterial;
16761676 storeSecretMaterial: StoreSecretMaterial;
16771677 getLocalServerBaseUrl?: () => string | undefined;
@@ -1721,7 +1721,7 @@ const createRuntimeSourceConnectionService = ( · 43 unmodified lines
17211721 ? addExecutorGoogleDiscoverySource({
17221722 rows: input.rows,
17231723 sourceStore: input.sourceStore,
1724− sourceMaterialization: input.sourceMaterialization,
1724+ sourceCatalogSync: input.sourceCatalogSync,
17251725 sourceInput,
17261726 storeSecretMaterial: input.storeSecretMaterial,
17271727 resolveSecretMaterial: input.resolveSecretMaterial,
@@ -1732,7 +1732,7 @@ const createRuntimeSourceConnectionService = ( · 4 unmodified lines
17321732 ? addExecutorHttpSource({
17331733 rows: input.rows,
17341734 sourceStore: input.sourceStore,
1735− sourceMaterialization: input.sourceMaterialization,
1735+ sourceCatalogSync: input.sourceCatalogSync,
17361736 sourceInput: sourceInput as Extract<
17371737 ExecutorAddSourceInput,
17381738 { kind: "openapi" | "graphql" }
@@ -1745,7 +1745,7 @@ const createRuntimeSourceConnectionService = ( · 6 unmodified lines
17451745 : connectMcpSourceInternal({
17461746 rows: input.rows,
17471747 sourceStore: input.sourceStore,
1748− sourceMaterialization: input.sourceMaterialization,
1748+ sourceCatalogSync: input.sourceCatalogSync,
17491749 getLocalServerBaseUrl: input.getLocalServerBaseUrl,
17501750 workspaceId: sourceInput.workspaceId,
17511751 actorAccountId: sourceInput.actorAccountId,
@@ -1767,7 +1767,7 @@ const createRuntimeSourceConnectionService = ( · 15 unmodified lines
17671767 connectMcpSourceInternal({
17681768 rows: input.rows,
17691769 sourceStore: input.sourceStore,
1770− sourceMaterialization: input.sourceMaterialization,
1770+ sourceCatalogSync: input.sourceCatalogSync,
17711771 getLocalServerBaseUrl: input.getLocalServerBaseUrl,
17721772 workspaceId: sourceInput.workspaceId,
17731773 actorAccountId: sourceInput.actorAccountId,
@@ -2028,7 +2028,7 @@ const createRuntimeSourceOAuthSessionService = ( · 254 unmodified lines
20282028 status: "error",
20292029 lastError: reason,
20302030 });
2031− yield* input.sourceMaterialization.sync({
2031+ yield* input.sourceCatalogSync.sync({
20322032 source: failedSource,
20332033 actorAccountId: session.actorAccountId,
20342034 });
@@ -2201,7 +2201,7 @@ const createRuntimeSourceOAuthSessionService = ( · 166 unmodified lines
22012201 );
22022202 }
22032203 const indexed = yield* Effect.either(
2204− input.sourceMaterialization.sync({
2204+ input.sourceCatalogSync.sync({
22052205 source: connectedSource,
22062206 actorAccountId: session.actorAccountId,
22072207 }),
@@ -2272,7 +2272,7 @@ export const RuntimeSourceAuthServiceLive = (input: { · 64 unmodified lines
22722272 const rows = yield* ControlPlaneStore;
22732273 const liveExecutionManager = yield* LiveExecutionManagerService;
22742274 const sourceStore = yield* RuntimeSourceStoreService;
2275− const sourceMaterialization = yield* RuntimeSourceMaterializationService;
2275+ const sourceCatalogSync = yield* RuntimeSourceCatalogSyncService;
22762276 const resolveSecretMaterial = yield* SecretMaterialResolverService;
22772277 const storeSecretMaterial = yield* SecretMaterialStorerService;
22782278 const runtimeLocalWorkspace = yield* getRuntimeLocalWorkspaceOption();
@@ -2281,7 +2281,7 @@ export const RuntimeSourceAuthServiceLive = (input: { · 2 unmodified lines
22812281 rows,
22822282 liveExecutionManager,
22832283 sourceStore,
2284− sourceMaterialization,
2284+ sourceCatalogSync,
22852285 resolveSecretMaterial,
22862286 storeSecretMaterial,
22872287 getLocalServerBaseUrl: input.getLocalServerBaseUrl,
packages/control-plane/src/runtime/source-catalog-runtime.ts+743−0
@@ -0,0 +1,743 @@
1+import {
2+ type ToolCatalogEntry,
3+ type ToolDescriptor as CatalogToolDescriptor,
4+ typeSignatureFromSchema,
5+} from "@executor/codemode-core";
6+import type {
7+ AccountId,
8+ Source,
9+ StoredSourceRecord,
10+ StoredSourceCatalogRevisionRecord,
11+ WorkspaceId,
12+} from "#schema";
13+import * as Context from "effect/Context";
14+import * as Effect from "effect/Effect";
15+import * as Layer from "effect/Layer";
16+
17+import {
18+ projectCatalogForAgentSdk,
19+ type ProjectedCatalog,
20+} from "../ir/catalog";
21+import type {
22+ Capability,
23+ CatalogSnapshotV1,
24+ CatalogV1,
25+ Executable,
26+ GraphQLExecutable,
27+ HttpExecutable,
28+ McpExecutable,
29+ ShapeSymbol,
30+ Symbol as IrSymbol,
31+} from "../ir/model";
32+import { LocalSourceArtifactMissingError } from "./local-errors";
33+import {
34+ RuntimeLocalWorkspaceService,
35+ type RuntimeLocalWorkspaceState,
36+} from "./local-runtime-context";
37+import type { LocalSourceArtifact } from "./local-source-artifacts";
38+import {
39+ SourceArtifactStore,
40+ type SourceArtifactStoreShape,
41+} from "./local-storage";
42+import { namespaceFromSourceName } from "./source-names";
43+import {
44+ RuntimeSourceStoreService,
45+ type RuntimeSourceStore,
46+} from "./source-store";
47+
48+type CatalogImportMetadata = CatalogSnapshotV1["import"];
49+
50+export type LoadedSourceCatalog = {
51+ source: Source;
52+ sourceRecord: StoredSourceRecord;
53+ revision: StoredSourceCatalogRevisionRecord;
54+ snapshot: CatalogSnapshotV1;
55+ catalog: CatalogV1;
56+ projected: ProjectedCatalog;
57+ importMetadata: CatalogImportMetadata;
58+};
59+
60+export type LoadedSourceCatalogTool = {
61+ path: string;
62+ searchNamespace: string;
63+ searchText: string;
64+ source: Source;
65+ sourceRecord: StoredSourceRecord;
66+ revision: StoredSourceCatalogRevisionRecord;
67+ capabilityId: keyof CatalogV1["capabilities"];
68+ executableId: keyof CatalogV1["executables"];
69+ capability: Capability;
70+ executable: Executable;
71+ descriptor: CatalogToolDescriptor;
72+ projectedCatalog: CatalogV1;
73+};
74+
75+export type LoadedSourceCatalogToolIndexEntry = Omit<LoadedSourceCatalogTool, "revision">;
76+
77+export const catalogToolCatalogEntry = (input: {
78+ tool: LoadedSourceCatalogToolIndexEntry;
79+ score: (queryTokens: readonly string[]) => number;
80+}): ToolCatalogEntry => ({
81+ descriptor: input.tool.descriptor,
82+ namespace: input.tool.searchNamespace,
83+ searchText: input.tool.searchText,
84+ score: input.score,
85+});
86+
87+const catalogNamespaceFromPath = (path: string): string => {
88+ const [first, second] = path.split(".");
89+ return second ? `${first}.${second}` : first;
90+};
91+
92+const chooseExecutable = (catalog: CatalogV1, capability: Capability): Executable => {
93+ const preferred =
94+ capability.preferredExecutableId !== undefined
95+ ? catalog.executables[capability.preferredExecutableId]
96+ : undefined;
97+ if (preferred) {
98+ return preferred;
99+ }
100+
101+ const first = capability.executableIds
102+ .map((id) => catalog.executables[id])
103+ .find((entry): entry is Executable => entry !== undefined);
104+ if (!first) {
105+ throw new Error(`Capability ${capability.id} has no executable`);
106+ }
107+ return first;
108+};
109+
110+const asShape = (catalog: CatalogV1, shapeId: string | undefined): ShapeSymbol | undefined => {
111+ if (!shapeId) {
112+ return undefined;
113+ }
114+
115+ const symbol = catalog.symbols[shapeId];
116+ return symbol?.kind === "shape" ? symbol : undefined;
117+};
118+
119+const symbolDocsSummary = (symbol: IrSymbol | undefined): string | undefined => {
120+ if (!symbol || !("docs" in symbol)) {
121+ return undefined;
122+ }
123+
124+ return symbol.docs?.summary ?? symbol.docs?.description;
125+};
126+
127+const shapeToJsonSchema = (catalog: CatalogV1, rootShapeId: string): unknown => {
128+ const defs: Record<string, unknown> = {};
129+ const visiting = new Set<string>();
130+ const built = new Set<string>();
131+
132+ const defNameFor = (shapeId: string) => shapeId.replace(/[^A-Za-z0-9_]/g, "_");
133+
134+ const visit = (shapeId: string): { $ref: string } => {
135+ const defName = defNameFor(shapeId);
136+ if (built.has(shapeId)) {
137+ return { $ref: `#/$defs/${defName}` };
138+ }
139+ if (visiting.has(shapeId)) {
140+ return { $ref: `#/$defs/${defName}` };
141+ }
142+
143+ const shape = asShape(catalog, shapeId);
144+ visiting.add(shapeId);
145+
146+ const schema = (() => {
147+ if (!shape) {
148+ return {} as Record<string, unknown>;
149+ }
150+
151+ const withDocs = (schemaValue: Record<string, unknown>): Record<string, unknown> => ({
152+ ...(shape.title ? { title: shape.title } : {}),
153+ ...(shape.docs?.description ? { description: shape.docs.description } : {}),
154+ ...schemaValue,
155+ });
156+
157+ switch (shape.node.type) {
158+ case "unknown":
159+ return withDocs({});
160+ case "const":
161+ return withDocs({ const: shape.node.value });
162+ case "enum":
163+ return withDocs({ enum: shape.node.values });
164+ case "scalar":
165+ return withDocs({
166+ type:
167+ shape.node.scalar === "bytes"
168+ ? "string"
169+ : shape.node.scalar,
170+ ...(shape.node.scalar === "bytes" ? { format: "binary" } : {}),
171+ ...(shape.node.format ? { format: shape.node.format } : {}),
172+ ...(shape.node.constraints ?? {}),
173+ });
174+ case "ref":
175+ return visit(shape.node.target);
176+ case "nullable":
177+ return withDocs({
178+ anyOf: [
179+ visit(shape.node.itemShapeId),
180+ { type: "null" },
181+ ],
182+ });
183+ case "allOf":
184+ return withDocs({
185+ allOf: shape.node.items.map((entry) => visit(entry)),
186+ });
187+ case "anyOf":
188+ return withDocs({
189+ anyOf: shape.node.items.map((entry) => visit(entry)),
190+ });
191+ case "oneOf":
192+ return withDocs({
193+ oneOf: shape.node.items.map((entry) => visit(entry)),
194+ ...(shape.node.discriminator
195+ ? {
196+ discriminator: {
197+ propertyName: shape.node.discriminator.propertyName,
198+ ...(shape.node.discriminator.mapping
199+ ? {
200+ mapping: Object.fromEntries(
201+ Object.entries(shape.node.discriminator.mapping).map(([key, value]) => [
202+ key,
203+ `#/$defs/${defNameFor(value)}`,
204+ ]),
205+ ),
206+ }
207+ : {}),
208+ },
209+ }
210+ : {}),
211+ });
212+ case "not":
213+ return withDocs({
214+ not: visit(shape.node.itemShapeId),
215+ });
216+ case "conditional":
217+ return withDocs({
218+ if: visit(shape.node.ifShapeId),
219+ ...(shape.node.thenShapeId ? { then: visit(shape.node.thenShapeId) } : {}),
220+ ...(shape.node.elseShapeId ? { else: visit(shape.node.elseShapeId) } : {}),
221+ });
222+ case "array":
223+ return withDocs({
224+ type: "array",
225+ items: visit(shape.node.itemShapeId),
226+ ...(shape.node.minItems !== undefined ? { minItems: shape.node.minItems } : {}),
227+ ...(shape.node.maxItems !== undefined ? { maxItems: shape.node.maxItems } : {}),
228+ });
229+ case "tuple":
230+ return withDocs({
231+ type: "array",
232+ prefixItems: shape.node.itemShapeIds.map((entry) => visit(entry)),
233+ ...(shape.node.additionalItems !== undefined
234+ ? {
235+ items:
236+ typeof shape.node.additionalItems === "boolean"
237+ ? shape.node.additionalItems
238+ : visit(shape.node.additionalItems),
239+ }
240+ : {}),
241+ });
242+ case "map":
243+ return withDocs({
244+ type: "object",
245+ additionalProperties: visit(shape.node.valueShapeId),
246+ });
247+ case "object":
248+ return withDocs({
249+ type: "object",
250+ properties: Object.fromEntries(
251+ Object.entries(shape.node.fields).map(([key, field]) => [
252+ key,
253+ {
254+ ...(visit(field.shapeId)),
255+ ...(field.docs?.description ? { description: field.docs.description } : {}),
256+ },
257+ ]),
258+ ),
259+ ...(shape.node.required && shape.node.required.length > 0
260+ ? { required: shape.node.required }
261+ : {}),
262+ ...(shape.node.additionalProperties !== undefined
263+ ? {
264+ additionalProperties:
265+ typeof shape.node.additionalProperties === "boolean"
266+ ? shape.node.additionalProperties
267+ : visit(shape.node.additionalProperties),
268+ }
269+ : {}),
270+ ...(shape.node.patternProperties
271+ ? {
272+ patternProperties: Object.fromEntries(
273+ Object.entries(shape.node.patternProperties).map(([key, value]) => [
274+ key,
275+ visit(value),
276+ ]),
277+ ),
278+ }
279+ : {}),
280+ });
281+ case "graphqlInterface":
282+ return withDocs({
283+ type: "object",
284+ properties: Object.fromEntries(
285+ Object.entries(shape.node.fields).map(([key, field]) => [
286+ key,
287+ visit(field.shapeId),
288+ ]),
289+ ),
290+ });
291+ case "graphqlUnion":
292+ return withDocs({
293+ oneOf: shape.node.memberTypeIds.map((entry) => visit(entry)),
294+ });
295+ }
296+ })();
297+
298+ visiting.delete(shapeId);
299+ built.add(shapeId);
300+ defs[defName] = schema;
301+ return { $ref: `#/$defs/${defName}` };
302+ };
303+
304+ const rootRef = visit(rootShapeId);
305+ return {
306+ ...rootRef,
307+ $defs: defs,
308+ };
309+};
310+
311+const codemodeDescriptorFromCapability = (input: {
312+ source: Source;
313+ projected: ProjectedCatalog;
314+ capability: Capability;
315+ executable: Executable;
316+ includeSchemas: boolean;
317+}): CatalogToolDescriptor => {
318+ const descriptor = input.projected.toolDescriptors[input.capability.id];
319+ const path = descriptor.toolPath.join(".");
320+ const interaction =
321+ descriptor.interaction.mayRequireApproval || descriptor.interaction.mayElicit
322+ ? "required"
323+ : "auto";
324+ const inputSchema = input.includeSchemas
325+ ? shapeToJsonSchema(input.projected.catalog, descriptor.callShapeId)
326+ : undefined;
327+ const outputSchema =
328+ input.includeSchemas && descriptor.resultShapeId
329+ ? shapeToJsonSchema(input.projected.catalog, descriptor.resultShapeId)
330+ : undefined;
331+
332+ return {
333+ path: path as CatalogToolDescriptor["path"],
334+ sourceKey: input.source.id,
335+ description: input.capability.surface.summary ?? input.capability.surface.description,
336+ interaction,
337+ ...(inputSchema !== undefined ? { inputSchema } : {}),
338+ ...(outputSchema !== undefined ? { outputSchema } : {}),
339+ ...(inputSchema !== undefined
340+ ? {
341+ inputType: typeSignatureFromSchema(inputSchema, "unknown"),
342+ }
343+ : {}),
344+ ...(outputSchema !== undefined
345+ ? {
346+ outputType: typeSignatureFromSchema(outputSchema, "unknown"),
347+ }
348+ : {}),
349+ providerKind: input.executable.protocol,
350+ providerData: {
351+ capabilityId: input.capability.id,
352+ executableId: input.executable.id,
353+ protocol: input.executable.protocol,
354+ },
355+ };
356+};
357+
358+const sourceRecordFromCatalogArtifact = (input: {
359+ source: Source;
360+ artifact: {
361+ catalogId: StoredSourceRecord["catalogId"];
362+ revision: StoredSourceCatalogRevisionRecord;
363+ };
364+}): StoredSourceRecord => ({
365+ id: input.source.id,
366+ workspaceId: input.source.workspaceId,
367+ catalogId: input.artifact.catalogId,
368+ catalogRevisionId: input.artifact.revision.id,
369+ name: input.source.name,
370+ kind: input.source.kind,
371+ endpoint: input.source.endpoint,
372+ status: input.source.status,
373+ enabled: input.source.enabled,
374+ namespace: input.source.namespace,
375+ importAuthPolicy: input.source.importAuthPolicy,
376+ bindingConfigJson: JSON.stringify(input.source.binding),
377+ sourceHash: input.source.sourceHash,
378+ lastError: input.source.lastError,
379+ createdAt: input.source.createdAt,
380+ updatedAt: input.source.updatedAt,
381+});
382+
383+type RuntimeSourceCatalogStoreShape = {
384+ loadWorkspaceSourceCatalogs: (input: {
385+ workspaceId: WorkspaceId;
386+ actorAccountId?: AccountId | null;
387+ }) => Effect.Effect<readonly LoadedSourceCatalog[], Error, never>;
388+ loadSourceWithCatalog: (input: {
389+ workspaceId: WorkspaceId;
390+ sourceId: Source["id"];
391+ actorAccountId?: AccountId | null;
392+ }) => Effect.Effect<LoadedSourceCatalog, Error | LocalSourceArtifactMissingError, never>;
393+ loadWorkspaceSourceCatalogToolIndex: (input: {
394+ workspaceId: WorkspaceId;
395+ actorAccountId?: AccountId | null;
396+ includeSchemas: boolean;
397+ }) => Effect.Effect<readonly LoadedSourceCatalogToolIndexEntry[], Error, never>;
398+ loadWorkspaceSourceCatalogToolByPath: (input: {
399+ workspaceId: WorkspaceId;
400+ path: string;
401+ actorAccountId?: AccountId | null;
402+ includeSchemas: boolean;
403+ }) => Effect.Effect<LoadedSourceCatalogToolIndexEntry | null, Error, never>;
404+};
405+
406+export type RuntimeSourceCatalogStore = RuntimeSourceCatalogStoreShape;
407+
408+export class RuntimeSourceCatalogStoreService extends Context.Tag(
409+ "#runtime/RuntimeSourceCatalogStoreService",
410+)<RuntimeSourceCatalogStoreService, RuntimeSourceCatalogStoreShape>() {}
411+
412+type RuntimeSourceCatalogStoreDeps = {
413+ runtimeLocalWorkspace: RuntimeLocalWorkspaceState;
414+ sourceStore: RuntimeSourceStore;
415+ sourceArtifactStore: SourceArtifactStoreShape;
416+};
417+
418+type SourceCatalogRuntimeServices =
419+ | RuntimeLocalWorkspaceService
420+ | RuntimeSourceStoreService
421+ | SourceArtifactStore;
422+
423+const ensureRuntimeCatalogWorkspace = (
424+ deps: RuntimeSourceCatalogStoreDeps,
425+ workspaceId: WorkspaceId,
426+) => {
427+ if (deps.runtimeLocalWorkspace.installation.workspaceId !== workspaceId) {
428+ return Effect.fail(
429+ new Error(
430+ `Runtime local workspace mismatch: expected ${workspaceId}, got ${deps.runtimeLocalWorkspace.installation.workspaceId}`,
431+ ),
432+ );
433+ }
434+
435+ return Effect.succeed(deps.runtimeLocalWorkspace.context);
436+};
437+
438+const buildSnapshotFromArtifact = (input: {
439+ source: Source;
440+ artifact: LocalSourceArtifact;
441+}): CatalogSnapshotV1 => {
442+ return input.artifact.snapshot;
443+};
444+
445+const loadWorkspaceSourceCatalogsWithDeps = (deps: RuntimeSourceCatalogStoreDeps, input: {
446+ workspaceId: WorkspaceId;
447+ actorAccountId?: AccountId | null;
448+}): Effect.Effect<readonly LoadedSourceCatalog[], Error, never> =>
449+ Effect.gen(function* () {
450+ const workspaceContext = yield* ensureRuntimeCatalogWorkspace(
451+ deps,
452+ input.workspaceId,
453+ );
454+ const sources = yield* deps.sourceStore.loadSourcesInWorkspace(
455+ input.workspaceId,
456+ {
457+ actorAccountId: input.actorAccountId,
458+ },
459+ );
460+
461+ const localCatalogs = yield* Effect.forEach(sources, (source) =>
462+ Effect.gen(function* () {
463+ const artifact = yield* deps.sourceArtifactStore.read({
464+ context: workspaceContext,
465+ sourceId: source.id,
466+ });
467+ if (artifact === null) {
468+ return null;
469+ }
470+
471+ const snapshot = buildSnapshotFromArtifact({
472+ source,
473+ artifact,
474+ });
475+ const projected = projectCatalogForAgentSdk({
476+ catalog: snapshot.catalog,
477+ });
478+
479+ return {
480+ source,
481+ sourceRecord: sourceRecordFromCatalogArtifact({
482+ source,
483+ artifact,
484+ }),
485+ revision: artifact.revision,
486+ snapshot,
487+ catalog: snapshot.catalog,
488+ projected,
489+ importMetadata: snapshot.import,
490+ } satisfies LoadedSourceCatalog;
491+ }),
492+ );
493+
494+ return localCatalogs.filter((catalogEntry): catalogEntry is LoadedSourceCatalog => catalogEntry !== null);
495+ });
496+
497+const loadSourceWithCatalogWithDeps = (deps: RuntimeSourceCatalogStoreDeps, input: {
498+ workspaceId: WorkspaceId;
499+ sourceId: Source["id"];
500+ actorAccountId?: AccountId | null;
501+}): Effect.Effect<LoadedSourceCatalog, Error | LocalSourceArtifactMissingError, never> =>
502+ Effect.gen(function* () {
503+ const workspaceContext = yield* ensureRuntimeCatalogWorkspace(
504+ deps,
505+ input.workspaceId,
506+ );
507+ const source = yield* deps.sourceStore.loadSourceById({
508+ workspaceId: input.workspaceId,
509+ sourceId: input.sourceId,
510+ actorAccountId: input.actorAccountId,
511+ });
512+ const artifact = yield* deps.sourceArtifactStore.read({
513+ context: workspaceContext,
514+ sourceId: source.id,
515+ });
516+ if (artifact === null) {
517+ return yield* Effect.fail(
518+ new LocalSourceArtifactMissingError({
519+ message: `Catalog artifact missing for source ${input.sourceId}`,
520+ sourceId: input.sourceId,
521+ }),
522+ );
523+ }
524+
525+ const snapshot = buildSnapshotFromArtifact({
526+ source,
527+ artifact,
528+ });
529+ const projected = projectCatalogForAgentSdk({
530+ catalog: snapshot.catalog,
531+ });
532+
533+ return {
534+ source,
535+ sourceRecord: sourceRecordFromCatalogArtifact({
536+ source,
537+ artifact,
538+ }),
539+ revision: artifact.revision,
540+ snapshot,
541+ catalog: snapshot.catalog,
542+ projected,
543+ importMetadata: snapshot.import,
544+ } satisfies LoadedSourceCatalog;
545+ });
546+
547+export const loadWorkspaceSourceCatalogs = (input: {
548+ workspaceId: WorkspaceId;
549+ actorAccountId?: AccountId | null;
550+}): Effect.Effect<readonly LoadedSourceCatalog[], Error, SourceCatalogRuntimeServices> =>
551+ Effect.gen(function* () {
552+ const runtimeLocalWorkspace = yield* RuntimeLocalWorkspaceService;
553+ const sourceStore = yield* RuntimeSourceStoreService;
554+ const sourceArtifactStore = yield* SourceArtifactStore;
555+
556+ return yield* loadWorkspaceSourceCatalogsWithDeps(
557+ {
558+ runtimeLocalWorkspace,
559+ sourceStore,
560+ sourceArtifactStore,
561+ },
562+ input,
563+ );
564+ });
565+
566+export const loadSourceWithCatalog = (input: {
567+ workspaceId: WorkspaceId;
568+ sourceId: Source["id"];
569+ actorAccountId?: AccountId | null;
570+}): Effect.Effect<
571+ LoadedSourceCatalog,
572+ Error | LocalSourceArtifactMissingError,
573+ SourceCatalogRuntimeServices
574+> =>
575+ Effect.gen(function* () {
576+ const runtimeLocalWorkspace = yield* RuntimeLocalWorkspaceService;
577+ const sourceStore = yield* RuntimeSourceStoreService;
578+ const sourceArtifactStore = yield* SourceArtifactStore;
579+
580+ return yield* loadSourceWithCatalogWithDeps(
581+ {
582+ runtimeLocalWorkspace,
583+ sourceStore,
584+ sourceArtifactStore,
585+ },
586+ input,
587+ );
588+ });
589+
590+export const expandCatalogTools = (input: {
591+ catalogs: readonly LoadedSourceCatalog[];
592+ includeSchemas: boolean;
593+}): Effect.Effect<readonly LoadedSourceCatalogTool[], Error, never> =>
594+ Effect.succeed(
595+ input.catalogs.flatMap((catalogEntry) =>
596+ Object.values(catalogEntry.catalog.capabilities).map((capability) => {
597+ const executable = chooseExecutable(catalogEntry.projected.catalog, capability);
598+ const descriptor = codemodeDescriptorFromCapability({
599+ source: catalogEntry.source,
600+ projected: catalogEntry.projected,
601+ capability,
602+ executable,
603+ includeSchemas: input.includeSchemas,
604+ });
605+ const path = descriptor.path;
606+ const searchDoc = catalogEntry.projected.searchDocs[capability.id];
607+ const searchNamespace = catalogNamespaceFromPath(path);
608+ const searchText = [
609+ path,
610+ searchNamespace,
611+ catalogEntry.source.name,
612+ capability.surface.title,
613+ capability.surface.summary,
614+ capability.surface.description,
615+ ...(searchDoc?.tags ?? []),
616+ ...(searchDoc?.protocolHints ?? []),
617+ ...(searchDoc?.authHints ?? []),
618+ ]
619+ .filter((part): part is string => typeof part === "string" && part.length > 0)
620+ .join(" ")
621+ .toLowerCase();
622+
623+ return {
624+ path,
625+ searchNamespace,
626+ searchText,
627+ source: catalogEntry.source,
628+ sourceRecord: catalogEntry.sourceRecord,
629+ revision: catalogEntry.revision,
630+ capabilityId: capability.id,
631+ executableId: executable.id,
632+ capability,
633+ executable,
634+ descriptor,
635+ projectedCatalog: catalogEntry.projected.catalog,
636+ } satisfies LoadedSourceCatalogTool;
637+ }),
638+ ),
639+ );
640+
641+export const loadWorkspaceSourceCatalogToolIndex = (input: {
642+ workspaceId: WorkspaceId;
643+ actorAccountId?: AccountId | null;
644+ includeSchemas: boolean;
645+}): Effect.Effect<
646+ readonly LoadedSourceCatalogToolIndexEntry[],
647+ Error,
648+ SourceCatalogRuntimeServices
649+> =>
650+ Effect.gen(function* () {
651+ const catalogs = yield* loadWorkspaceSourceCatalogs({
652+ workspaceId: input.workspaceId,
653+ actorAccountId: input.actorAccountId,
654+ });
655+ const tools = yield* expandCatalogTools({
656+ catalogs,
657+ includeSchemas: input.includeSchemas,
658+ });
659+ return tools.map((tool) => ({
660+ path: tool.path,
661+ searchNamespace: tool.searchNamespace,
662+ searchText: tool.searchText,
663+ source: tool.source,
664+ sourceRecord: tool.sourceRecord,
665+ capabilityId: tool.capabilityId,
666+ executableId: tool.executableId,
667+ capability: tool.capability,
668+ executable: tool.executable,
669+ descriptor: tool.descriptor,
670+ projectedCatalog: tool.projectedCatalog,
671+ }));
672+ });
673+
674+export const loadWorkspaceSourceCatalogToolByPath = (input: {
675+ workspaceId: WorkspaceId;
676+ path: string;
677+ actorAccountId?: AccountId | null;
678+ includeSchemas: boolean;
679+}): Effect.Effect<
680+ LoadedSourceCatalogToolIndexEntry | null,
681+ Error,
682+ SourceCatalogRuntimeServices
683+> =>
684+ Effect.gen(function* () {
685+ const catalogs = yield* loadWorkspaceSourceCatalogs({
686+ workspaceId: input.workspaceId,
687+ actorAccountId: input.actorAccountId,
688+ });
689+ const tools = yield* expandCatalogTools({
690+ catalogs,
691+ includeSchemas: input.includeSchemas,
692+ });
693+ const tool = tools.find((entry) => entry.path === input.path) ?? null;
694+ return tool
695+ ? {
696+ path: tool.path,
697+ searchNamespace: tool.searchNamespace,
698+ searchText: tool.searchText,
699+ source: tool.source,
700+ sourceRecord: tool.sourceRecord,
701+ capabilityId: tool.capabilityId,
702+ executableId: tool.executableId,
703+ capability: tool.capability,
704+ executable: tool.executable,
705+ descriptor: tool.descriptor,
706+ projectedCatalog: tool.projectedCatalog,
707+ }
708+ : null;
709+ });
710+
711+export const RuntimeSourceCatalogStoreLive = Layer.effect(
712+ RuntimeSourceCatalogStoreService,
713+ Effect.gen(function* () {
714+ const runtimeLocalWorkspace = yield* RuntimeLocalWorkspaceService;
715+ const sourceStore = yield* RuntimeSourceStoreService;
716+ const sourceArtifactStore = yield* SourceArtifactStore;
717+
718+ const deps: RuntimeSourceCatalogStoreDeps = {
719+ runtimeLocalWorkspace,
720+ sourceStore,
721+ sourceArtifactStore,
722+ };
723+
724+ return RuntimeSourceCatalogStoreService.of({
725+ loadWorkspaceSourceCatalogs: (input) =>
726+ loadWorkspaceSourceCatalogsWithDeps(deps, input),
727+ loadSourceWithCatalog: (input) =>
728+ loadSourceWithCatalogWithDeps(deps, input),
729+ loadWorkspaceSourceCatalogToolIndex: (input) =>
730+ loadWorkspaceSourceCatalogToolIndex(input).pipe(
731+ Effect.provideService(RuntimeLocalWorkspaceService, runtimeLocalWorkspace),
732+ Effect.provideService(RuntimeSourceStoreService, sourceStore),
733+ Effect.provideService(SourceArtifactStore, sourceArtifactStore),
734+ ),
735+ loadWorkspaceSourceCatalogToolByPath: (input) =>
736+ loadWorkspaceSourceCatalogToolByPath(input).pipe(
737+ Effect.provideService(RuntimeLocalWorkspaceService, runtimeLocalWorkspace),
738+ Effect.provideService(RuntimeSourceStoreService, sourceStore),
739+ Effect.provideService(SourceArtifactStore, sourceArtifactStore),
740+ ),
741+ });
742+ }),
743+);
packages/control-plane/src/runtime/source-catalog-snapshot.test.ts+221−0
@@ -0,0 +1,221 @@
1+import { describe, expect, it } from "@effect/vitest";
2+
3+import type { Source } from "#schema";
4+
5+import {
6+ createGoogleDiscoveryCatalogSnapshot,
7+ createGraphqlCatalogSnapshot,
8+ createOpenApiCatalogSnapshot,
9+} from "./source-catalog-snapshot";
10+
11+const baseSource: Source = {
12+ id: "src_calendar" as Source["id"],
13+ workspaceId: "ws_test" as Source["workspaceId"],
14+ name: "Calendar",
15+ kind: "openapi",
16+ endpoint: "https://api.example.test",
17+ status: "connected",
18+ enabled: true,
19+ namespace: "google.calendar",
20+ bindingVersion: 1,
21+ binding: {
22+ specUrl: "https://api.example.test/openapi.json",
23+ defaultHeaders: null,
24+ },
25+ importAuthPolicy: "none",
26+ importAuth: { kind: "none" },
27+ auth: { kind: "none" },
28+ sourceHash: "hash_source",
29+ lastError: null,
30+ createdAt: 1,
31+ updatedAt: 1,
32+};
33+
34+describe("source-catalog-snapshot", () => {
35+ it("builds an HTTP capability graph from OpenAPI operation inputs", () => {
36+ const snapshot = createOpenApiCatalogSnapshot({
37+ source: baseSource,
38+ documents: [{
39+ documentKind: "openapi",
40+ documentKey: "https://api.example.test/openapi.json",
41+ contentText: "{}",
42+ fetchedAt: 1,
43+ }],
44+ operations: [{
45+ toolId: "events.update",
46+ title: "Update event",
47+ description: "Update a calendar event",
48+ effect: "write",
49+ inputSchema: {
50+ type: "object",
51+ properties: {
52+ calendarId: { type: "string", description: "Calendar ID" },
53+ eventId: { type: "string" },
54+ sendUpdates: { type: "string", enum: ["all", "none"] },
55+ body: {
56+ type: "object",
57+ properties: {
58+ summary: { type: "string" },
59+ },
60+ },
61+ },
62+ required: ["calendarId", "eventId", "body"],
63+ },
64+ outputSchema: {
65+ type: "object",
66+ properties: {
67+ id: { type: "string" },
68+ summary: { type: "string" },
69+ },
70+ required: ["id"],
71+ },
72+ providerData: {
73+ kind: "openapi",
74+ toolId: "events.update",
75+ rawToolId: "events.update",
76+ group: "events",
77+ leaf: "update",
78+ tags: ["events"],
79+ method: "patch",
80+ path: "/calendars/{calendarId}/events/{eventId}",
81+ operationHash: "op_hash",
82+ invocation: {
83+ method: "patch",
84+ pathTemplate: "/calendars/{calendarId}/events/{eventId}",
85+ parameters: [
86+ { name: "calendarId", location: "path", required: true },
87+ { name: "eventId", location: "path", required: true },
88+ { name: "sendUpdates", location: "query", required: false },
89+ ],
90+ requestBody: {
91+ required: true,
92+ contentTypes: ["application/json"],
93+ },
94+ },
95+ documentation: {
96+ summary: "Update event",
97+ parameters: [
98+ {
99+ name: "calendarId",
100+ location: "path",
101+ required: true,
102+ description: "Calendar identifier",
103+ },
104+ ],
105+ requestBody: {
106+ description: "Event patch body",
107+ },
108+ response: {
109+ statusCode: "200",
110+ description: "Updated event",
111+ contentTypes: ["application/json"],
112+ },
113+ },
114+ },
115+ }],
116+ });
117+
118+ expect(snapshot.version).toBe("ir.v1.snapshot");
119+ expect(Object.keys(snapshot.catalog.capabilities)).toHaveLength(1);
120+
121+ const capability = Object.values(snapshot.catalog.capabilities)[0]!;
122+ const executable = Object.values(snapshot.catalog.executables)[0]!;
123+ const responseSet = Object.values(snapshot.catalog.responseSets)[0]!;
124+
125+ expect(capability.surface.toolPath).toEqual(["google", "calendar", "events", "update"]);
126+ expect(capability.semantics.effect).toBe("write");
127+ expect(executable.protocol).toBe("http");
128+ expect(executable.method).toBe("PATCH");
129+ expect(executable.pathTemplate).toBe("/calendars/{calendarId}/events/{eventId}");
130+ expect(responseSet.variants).toHaveLength(1);
131+ });
132+
133+ it("imports Google Discovery scopes as auth requirements", () => {
134+ const snapshot = createGoogleDiscoveryCatalogSnapshot({
135+ source: {
136+ ...baseSource,
137+ kind: "google_discovery",
138+ namespace: "google.drive",
139+ },
140+ documents: [],
141+ operations: [{
142+ toolId: "files.list",
143+ title: "List files",
144+ description: "List drive files",
145+ effect: "read",
146+ inputSchema: { type: "object", properties: { pageSize: { type: "integer" } } },
147+ outputSchema: { type: "object", properties: { files: { type: "array", items: { type: "string" } } } },
148+ providerData: {
149+ kind: "google_discovery",
150+ service: "drive",
151+ version: "v3",
152+ toolId: "files.list",
153+ rawToolId: "files.list",
154+ methodId: "drive.files.list",
155+ group: "files",
156+ leaf: "list",
157+ invocation: {
158+ method: "get",
159+ path: "/drive/v3/files",
160+ flatPath: null,
161+ rootUrl: "https://www.googleapis.com/",
162+ servicePath: "drive/v3/",
163+ parameters: [],
164+ requestSchemaId: null,
165+ responseSchemaId: "FileList",
166+ scopes: ["https://www.googleapis.com/auth/drive.readonly"],
167+ supportsMediaUpload: false,
168+ supportsMediaDownload: false,
169+ },
170+ },
171+ }],
172+ });
173+
174+ const capability = Object.values(snapshot.catalog.capabilities)[0]!;
175+ const securityScheme = Object.values(snapshot.catalog.symbols).find((symbol) => symbol.kind === "securityScheme");
176+
177+ expect(capability.auth.kind).toBe("scheme");
178+ expect(securityScheme?.kind).toBe("securityScheme");
179+ });
180+
181+ it("converts GraphQL field operations into GraphQL executables", () => {
182+ const snapshot = createGraphqlCatalogSnapshot({
183+ source: {
184+ ...baseSource,
185+ kind: "graphql",
186+ namespace: "github",
187+ },
188+ documents: [],
189+ operations: [{
190+ toolId: "viewer",
191+ title: "Viewer",
192+ description: "Load the current viewer",
193+ effect: "read",
194+ inputSchema: { type: "object", properties: {} },
195+ outputSchema: { type: "object", properties: { login: { type: "string" } } },
196+ providerData: {
197+ kind: "graphql",
198+ toolKind: "field",
199+ toolId: "viewer",
200+ rawToolId: "viewer",
201+ group: "query",
202+ leaf: "viewer",
203+ fieldName: "viewer",
204+ operationType: "query",
205+ operationName: "ViewerQuery",
206+ operationDocument: "query ViewerQuery { viewer { login } }",
207+ queryTypeName: "Query",
208+ mutationTypeName: null,
209+ subscriptionTypeName: null,
210+ },
211+ }],
212+ });
213+
214+ const executable = Object.values(snapshot.catalog.executables)[0]!;
215+
216+ expect(executable.protocol).toBe("graphql");
217+ expect(executable.operationType).toBe("query");
218+ expect(executable.rootField).toBe("viewer");
219+ expect(executable.selectionMode).toBe("fixed");
220+ });
221+});
packages/control-plane/src/runtime/source-catalog-snapshot.ts+1688−0
@@ -0,0 +1,1688 @@
1+import { sha256Hex } from "@executor/codemode-core";
2+import {
3+ type OpenApiToolProviderData,
4+} from "@executor/codemode-openapi";
5+import {
6+ type GoogleDiscoveryToolProviderData,
7+} from "@executor/codemode-google-discovery";
8+
9+import type { Source } from "#schema";
10+
11+import {
12+ createCatalogSnapshotV1,
13+ createEmptyCatalogV1,
14+} from "../ir/catalog";
15+import {
16+ CapabilityIdSchema,
17+ DiagnosticIdSchema,
18+ DocumentIdSchema,
19+ ExampleSymbolIdSchema,
20+ ExecutableIdSchema,
21+ HeaderSymbolIdSchema,
22+ ParameterSymbolIdSchema,
23+ RequestBodySymbolIdSchema,
24+ ResourceIdSchema,
25+ ResponseSetIdSchema,
26+ ResponseSymbolIdSchema,
27+ ScopeIdSchema,
28+ SecuritySchemeSymbolIdSchema,
29+ ShapeSymbolIdSchema,
30+} from "../ir/ids";
31+import type {
32+ AuthRequirement,
33+ Capability,
34+ CatalogSnapshotV1,
35+ CatalogV1,
36+ ContentSpec,
37+ DocumentationBlock,
38+ EffectKind,
39+ ExampleSymbol,
40+ GraphQLExecutable,
41+ HttpExecutable,
42+ ImportDiagnostic,
43+ McpExecutable,
44+ NativeBlob,
45+ ParameterSymbol,
46+ ProvenanceRef,
47+ ResponseSet,
48+ ResponseSymbol,
49+ Scope,
50+ SecuritySchemeSymbol,
51+ ShapeNode,
52+ ShapeSymbol,
53+ SourceKind,
54+ SourceDocument,
55+} from "../ir/model";
56+import type { GraphqlToolProviderData } from "./graphql-tools";
57+import { namespaceFromSourceName } from "./source-names";
58+
59+type JsonSchema = boolean | Record<string, unknown>;
60+
61+export type CatalogSourceDocumentInput = {
62+ documentKind: string;
63+ documentKey: string;
64+ contentText: string;
65+ fetchedAt?: number | null;
66+};
67+
68+type CatalogOperationInput = {
69+ toolId: string;
70+ title?: string | null;
71+ description?: string | null;
72+ effect: EffectKind;
73+ inputSchema?: unknown;
74+ outputSchema?: unknown;
75+};
76+
77+export type OpenApiCatalogOperationInput = CatalogOperationInput & {
78+ providerData: OpenApiToolProviderData;
79+};
80+
81+export type GoogleDiscoveryCatalogOperationInput = CatalogOperationInput & {
82+ providerData: GoogleDiscoveryToolProviderData;
83+};
84+
85+export type GraphqlCatalogOperationInput = CatalogOperationInput & {
86+ providerData: GraphqlToolProviderData;
87+};
88+
89+export type McpCatalogOperationInput = CatalogOperationInput & {
90+ providerData: {
91+ toolId: string;
92+ toolName: string;
93+ description: string | null;
94+ };
95+};
96+
97+type JsonSchemaImporter = {
98+ importSchema: (schema: unknown, key: string) => ReturnType<typeof ShapeSymbolIdSchema.make>;
99+};
100+
101+const stableStringify = (value: unknown): string => {
102+ if (value === null || typeof value !== "object") {
103+ return JSON.stringify(value);
104+ }
105+
106+ if (Array.isArray(value)) {
107+ return `[${value.map((entry) => stableStringify(entry)).join(",")}]`;
108+ }
109+
110+ const entries = Object.entries(value as Record<string, unknown>)
111+ .sort(([left], [right]) => left.localeCompare(right))
112+ .map(([key, entry]) => `${JSON.stringify(key)}:${stableStringify(entry)}`);
113+ return `{${entries.join(",")}}`;
114+};
115+
116+const stableHash = (value: unknown): string =>
117+ sha256Hex(stableStringify(value)).slice(0, 16);
118+
119+const mutableRecord = <K extends string, V>(value: Readonly<Record<K, V>>): Record<K, V> =>
120+ value as unknown as Record<K, V>;
121+
122+const asObject = (value: unknown): Record<string, unknown> =>
123+ value !== null && typeof value === "object" && !Array.isArray(value)
124+ ? (value as Record<string, unknown>)
125+ : {};
126+
127+const asString = (value: unknown): string | null =>
128+ typeof value === "string" && value.trim().length > 0 ? value : null;
129+
130+const asBoolean = (value: unknown): boolean | null =>
131+ typeof value === "boolean" ? value : null;
132+
133+const asNumber = (value: unknown): number | null =>
134+ typeof value === "number" && Number.isFinite(value) ? value : null;
135+
136+const asArray = (value: unknown): unknown[] =>
137+ Array.isArray(value) ? value : [];
138+
139+const asStringArray = (value: unknown): string[] =>
140+ asArray(value).flatMap((entry) => {
141+ const stringValue = asString(entry);
142+ return stringValue === null ? [] : [stringValue];
143+ });
144+
145+const sourceKindFromSource = (source: Source): SourceKind => {
146+ switch (source.kind) {
147+ case "openapi":
148+ return "openapi";
149+ case "graphql":
150+ return "graphql-schema";
151+ case "google_discovery":
152+ return "google-discovery";
153+ case "mcp":
154+ return "mcp";
155+ default:
156+ return "custom";
157+ }
158+};
159+
160+const toolPathSegments = (source: Source, toolId: string): string[] => {
161+ const namespace = source.namespace ?? namespaceFromSourceName(source.name);
162+ const fullPath = namespace ? `${namespace}.${toolId}` : toolId;
163+ return fullPath.split(".").filter((segment) => segment.length > 0);
164+};
165+
166+const serviceScopeIdForSource = (source: Source) =>
167+ ScopeIdSchema.make(`scope_service_${stableHash({ sourceId: source.id })}`);
168+
169+const documentIdFor = (source: Source, key: string) =>
170+ DocumentIdSchema.make(`doc_${stableHash({ sourceId: source.id, key })}`);
171+
172+const resourceIdForSource = (source: Source) =>
173+ ResourceIdSchema.make(`res_${stableHash({ sourceId: source.id })}`);
174+
175+const provenanceFor = (documentId: ReturnType<typeof DocumentIdSchema.make>, pointer: string): ProvenanceRef[] => [{
176+ relation: "declared",
177+ documentId,
178+ pointer,
179+}];
180+
181+const effectFromOperationKind = (operationKind: string): EffectKind => {
182+ switch (operationKind) {
183+ case "read":
184+ return "read";
185+ case "delete":
186+ return "delete";
187+ case "write":
188+ return "write";
189+ default:
190+ return "action";
191+ }
192+};
193+
194+const interactionForEffect = (effect: EffectKind): Capability["interaction"] => ({
195+ approval: {
196+ mayRequire: effect !== "read",
197+ reasons:
198+ effect === "delete"
199+ ? ["delete"]
200+ : effect === "write" || effect === "action"
201+ ? ["write"]
202+ : [],
203+ },
204+ elicitation: {
205+ mayRequest: false,
206+ },
207+ resume: {
208+ supported: false,
209+ },
210+});
211+
212+const importMetadataFor = (input: {
213+ source: Source;
214+ adapterKey: string;
215+}) => ({
216+ sourceKind: sourceKindFromSource(input.source),
217+ adapterKey: input.adapterKey,
218+ importerVersion: "ir.v1.snapshot_builder",
219+ importedAt: new Date().toISOString(),
220+ sourceConfigHash:
221+ input.source.sourceHash
222+ ?? stableHash({
223+ endpoint: input.source.endpoint,
224+ binding: input.source.binding,
225+ auth: input.source.auth.kind,
226+ }),
227+});
228+
229+const addDiagnostic = (
230+ catalog: CatalogV1,
231+ input: Omit<ImportDiagnostic, "id">,
232+) => {
233+ const id = DiagnosticIdSchema.make(`diag_${stableHash(input)}`);
234+ mutableRecord(catalog.diagnostics)[id] = {
235+ id,
236+ ...input,
237+ };
238+ return id;
239+};
240+
241+const nativeBlob = (input: {
242+ source: Source;
243+ kind: string;
244+ pointer: string;
245+ value: unknown;
246+ summary?: string;
247+}): NativeBlob => ({
248+ sourceKind: sourceKindFromSource(input.source),
249+ kind: input.kind,
250+ pointer: input.pointer,
251+ encoding: "json",
252+ ...(input.summary ? { summary: input.summary } : {}),
253+ value: input.value,
254+});
255+
256+const docsFrom = (input: {
257+ summary?: string | null;
258+ description?: string | null;
259+ externalDocsUrl?: string | null;
260+}): DocumentationBlock | undefined => {
261+ const summary = input.summary ?? undefined;
262+ const description = input.description ?? undefined;
263+ const externalDocsUrl = input.externalDocsUrl ?? undefined;
264+
265+ if (!summary && !description && !externalDocsUrl) {
266+ return undefined;
267+ }
268+
269+ return {
270+ ...(summary ? { summary } : {}),
271+ ...(description ? { description } : {}),
272+ ...(externalDocsUrl ? { externalDocsUrl } : {}),
273+ };
274+};
275+
276+const createJsonSchemaImporter = (input: {
277+ catalog: CatalogV1;
278+ source: Source;
279+ resourceId: ReturnType<typeof ResourceIdSchema.make>;
280+ documentId: ReturnType<typeof DocumentIdSchema.make>;
281+}) : JsonSchemaImporter => {
282+ const cache = new Map<string, ReturnType<typeof ShapeSymbolIdSchema.make>>();
283+
284+ const resolvePointer = (root: unknown, pointer: string): unknown => {
285+ if (pointer === "#" || pointer.length === 0) {
286+ return root;
287+ }
288+
289+ const segments = pointer
290+ .replace(/^#\//, "")
291+ .split("/")
292+ .map((segment) => segment.replace(/~1/g, "/").replace(/~0/g, "~"));
293+ let current: unknown = root;
294+ for (const segment of segments) {
295+ if (Array.isArray(current)) {
296+ const index = Number(segment);
297+ current = Number.isInteger(index) ? current[index] : undefined;
298+ continue;
299+ }
300+
301+ current = asObject(current)[segment];
302+ }
303+
304+ return current;
305+ };
306+
307+ const importSchema = (schema: unknown, key: string, rootSchema?: unknown): ReturnType<typeof ShapeSymbolIdSchema.make> => {
308+ const stableKey = `${input.resourceId}:${key}`;
309+ const cached = cache.get(stableKey);
310+ if (cached) {
311+ return cached;
312+ }
313+
314+ const shapeId = ShapeSymbolIdSchema.make(`shape_${stableHash({ resourceId: input.resourceId, key })}`);
315+ cache.set(stableKey, shapeId);
316+
317+ const objectSchema = asObject(schema);
318+ const title = asString(objectSchema.title) ?? undefined;
319+ const docs = docsFrom({
320+ description: asString(objectSchema.description),
321+ });
322+ const deprecated = asBoolean(objectSchema.deprecated) ?? undefined;
323+
324+ const register = (node: ShapeNode, extras: {
325+ native?: NativeBlob[];
326+ diagnosticIds?: ReturnType<typeof DiagnosticIdSchema.make>[];
327+ } = {}) => {
328+ mutableRecord(input.catalog.symbols)[shapeId] = {
329+ id: shapeId,
330+ kind: "shape",
331+ resourceId: input.resourceId,
332+ ...(title ? { title } : {}),
333+ ...(docs ? { docs } : {}),
334+ ...(deprecated !== undefined ? { deprecated } : {}),
335+ node,
336+ synthetic: false,
337+ provenance: provenanceFor(input.documentId, key),
338+ ...(extras.diagnosticIds && extras.diagnosticIds.length > 0
339+ ? { diagnosticIds: extras.diagnosticIds }
340+ : {}),
341+ ...(extras.native && extras.native.length > 0 ? { native: extras.native } : {}),
342+ } satisfies ShapeSymbol;
343+ };
344+
345+ if (typeof schema === "boolean") {
346+ register({
347+ type: "unknown",
348+ reason: schema ? "schema_true" : "schema_false",
349+ });
350+ return shapeId;
351+ }
352+
353+ const ref = asString(objectSchema.$ref);
354+ if (ref !== null) {
355+ const resolved =
356+ ref.startsWith("#")
357+ ? resolvePointer(rootSchema ?? schema, ref)
358+ : undefined;
359+
360+ if (resolved === undefined) {
361+ const diagnosticId = addDiagnostic(input.catalog, {
362+ level: "warning",
363+ code: "unresolved_ref",
364+ message: `Unresolved JSON schema ref ${ref}`,
365+ provenance: provenanceFor(input.documentId, key),
366+ relatedSymbolIds: [shapeId],
367+ });
368+ register(
369+ {
370+ type: "unknown",
371+ reason: `unresolved_ref:${ref}`,
372+ },
373+ {
374+ diagnosticIds: [diagnosticId],
375+ },
376+ );
377+ return shapeId;
378+ }
379+
380+ const target = importSchema(resolved, ref, rootSchema ?? schema);
381+ register({
382+ type: "ref",
383+ target,
384+ });
385+ return shapeId;
386+ }
387+
388+ const enumValues = asArray(objectSchema.enum);
389+ if (enumValues.length === 1) {
390+ register({
391+ type: "const",
392+ value: enumValues[0],
393+ });
394+ return shapeId;
395+ }
396+
397+ if (enumValues.length > 1) {
398+ register({
399+ type: "enum",
400+ values: enumValues,
401+ });
402+ return shapeId;
403+ }
404+
405+ if ("const" in objectSchema) {
406+ register({
407+ type: "const",
408+ value: objectSchema.const,
409+ });
410+ return shapeId;
411+ }
412+
413+ const anyOf = asArray(objectSchema.anyOf);
414+ if (anyOf.length > 0) {
415+ register({
416+ type: "anyOf",
417+ items: anyOf.map((entry, index) => importSchema(entry, `${key}/anyOf/${index}`, rootSchema ?? schema)),
418+ });
419+ return shapeId;
420+ }
421+
422+ const allOf = asArray(objectSchema.allOf);
423+ if (allOf.length > 0) {
424+ register({
425+ type: "allOf",
426+ items: allOf.map((entry, index) => importSchema(entry, `${key}/allOf/${index}`, rootSchema ?? schema)),
427+ });
428+ return shapeId;
429+ }
430+
431+ const oneOf = asArray(objectSchema.oneOf);
432+ if (oneOf.length > 0) {
433+ register({
434+ type: "oneOf",
435+ items: oneOf.map((entry, index) => importSchema(entry, `${key}/oneOf/${index}`, rootSchema ?? schema)),
436+ });
437+ return shapeId;
438+ }
439+
440+ if ("if" in objectSchema || "then" in objectSchema || "else" in objectSchema) {
441+ register({
442+ type: "conditional",
443+ ifShapeId: importSchema(objectSchema.if ?? {}, `${key}/if`, rootSchema ?? schema),
444+ thenShapeId: importSchema(objectSchema.then ?? {}, `${key}/then`, rootSchema ?? schema),
445+ ...(objectSchema.else !== undefined
446+ ? {
447+ elseShapeId: importSchema(objectSchema.else, `${key}/else`, rootSchema ?? schema),
448+ }
449+ : {}),
450+ });
451+ return shapeId;
452+ }
453+
454+ if ("not" in objectSchema) {
455+ register({
456+ type: "not",
457+ itemShapeId: importSchema(objectSchema.not, `${key}/not`, rootSchema ?? schema),
458+ });
459+ return shapeId;
460+ }
461+
462+ const declaredType = objectSchema.type;
463+ const typeArray = Array.isArray(declaredType)
464+ ? declaredType.flatMap((entry) => {
465+ const value = asString(entry);
466+ return value === null ? [] : [value];
467+ })
468+ : [];
469+ const nullable = asBoolean(objectSchema.nullable) === true || typeArray.includes("null");
470+ const effectiveType = Array.isArray(declaredType)
471+ ? typeArray.find((entry) => entry !== "null") ?? null
472+ : asString(declaredType);
473+
474+ const registerNullable = (itemShapeId: ReturnType<typeof ShapeSymbolIdSchema.make>) => {
475+ register({
476+ type: "nullable",
477+ itemShapeId,
478+ });
479+ return shapeId;
480+ };
481+
482+ const constraints: Record<string, unknown> = {};
483+ for (const constraintKey of [
484+ "format",
485+ "minLength",
486+ "maxLength",
487+ "pattern",
488+ "minimum",
489+ "maximum",
490+ "exclusiveMinimum",
491+ "exclusiveMaximum",
492+ "multipleOf",
493+ "default",
494+ "examples",
495+ ]) {
496+ if (objectSchema[constraintKey] !== undefined) {
497+ constraints[constraintKey] = objectSchema[constraintKey];
498+ }
499+ }
500+
501+ if (
502+ effectiveType === "object"
503+ || "properties" in objectSchema
504+ || "additionalProperties" in objectSchema
505+ || "patternProperties" in objectSchema
506+ ) {
507+ const fields = Object.fromEntries(
508+ Object.entries(asObject(objectSchema.properties)).map(([fieldName, fieldSchema]) => [
509+ fieldName,
510+ {
511+ shapeId: importSchema(
512+ fieldSchema,
513+ `${key}/properties/${fieldName}`,
514+ rootSchema ?? schema,
515+ ),
516+ ...(docsFrom({
517+ description: asString(asObject(fieldSchema).description),
518+ })
519+ ? {
520+ docs: docsFrom({
521+ description: asString(asObject(fieldSchema).description),
522+ })!,
523+ }
524+ : {}),
525+ },
526+ ]),
527+ );
528+
529+ const patternProperties = Object.fromEntries(
530+ Object.entries(asObject(objectSchema.patternProperties)).map(([pattern, value]) => [
531+ pattern,
532+ importSchema(value, `${key}/patternProperties/${pattern}`, rootSchema ?? schema),
533+ ]),
534+ );
535+
536+ const additionalPropertiesValue = objectSchema.additionalProperties;
537+ const additionalProperties =
538+ typeof additionalPropertiesValue === "boolean"
539+ ? additionalPropertiesValue
540+ : additionalPropertiesValue !== undefined
541+ ? importSchema(additionalPropertiesValue, `${key}/additionalProperties`, rootSchema ?? schema)
542+ : undefined;
543+
544+ const objectNode: ShapeNode = {
545+ type: "object",
546+ fields,
547+ ...(asStringArray(objectSchema.required).length > 0
548+ ? { required: asStringArray(objectSchema.required) }
549+ : {}),
550+ ...(additionalProperties !== undefined ? { additionalProperties } : {}),
551+ ...(Object.keys(patternProperties).length > 0 ? { patternProperties } : {}),
552+ };
553+
554+ if (nullable) {
555+ const innerId = importSchema(
556+ {
557+ ...objectSchema,
558+ nullable: false,
559+ type: "object",
560+ },
561+ `${key}:nonnull`,
562+ rootSchema ?? schema,
563+ );
564+ return registerNullable(innerId);
565+ }
566+
567+ register(objectNode);
568+ return shapeId;
569+ }
570+
571+ if (effectiveType === "array" || "items" in objectSchema || "prefixItems" in objectSchema) {
572+ if (Array.isArray(objectSchema.prefixItems) && objectSchema.prefixItems.length > 0) {
573+ const tupleNode: ShapeNode = {
574+ type: "tuple",
575+ itemShapeIds: objectSchema.prefixItems.map((entry, index) =>
576+ importSchema(entry, `${key}/prefixItems/${index}`, rootSchema ?? schema)
577+ ),
578+ ...(objectSchema.items !== undefined
579+ ? {
580+ additionalItems:
581+ typeof objectSchema.items === "boolean"
582+ ? objectSchema.items
583+ : importSchema(objectSchema.items, `${key}/items`, rootSchema ?? schema),
584+ }
585+ : {}),
586+ };
587+
588+ if (nullable) {
589+ const innerId = importSchema(
590+ {
591+ ...objectSchema,
592+ nullable: false,
593+ type: "array",
594+ },
595+ `${key}:nonnull`,
596+ rootSchema ?? schema,
597+ );
598+ return registerNullable(innerId);
599+ }
600+
601+ register(tupleNode);
602+ return shapeId;
603+ }
604+
605+ const items = objectSchema.items ?? {};
606+ const arrayNode: ShapeNode = {
607+ type: "array",
608+ itemShapeId: importSchema(items, `${key}/items`, rootSchema ?? schema),
609+ ...(asNumber(objectSchema.minItems) !== null ? { minItems: asNumber(objectSchema.minItems)! } : {}),
610+ ...(asNumber(objectSchema.maxItems) !== null ? { maxItems: asNumber(objectSchema.maxItems)! } : {}),
611+ };
612+
613+ if (nullable) {
614+ const innerId = importSchema(
615+ {
616+ ...objectSchema,
617+ nullable: false,
618+ type: "array",
619+ },
620+ `${key}:nonnull`,
621+ rootSchema ?? schema,
622+ );
623+ return registerNullable(innerId);
624+ }
625+
626+ register(arrayNode);
627+ return shapeId;
628+ }
629+
630+ if (
631+ effectiveType === "string"
632+ || effectiveType === "number"
633+ || effectiveType === "integer"
634+ || effectiveType === "boolean"
635+ || effectiveType === "null"
636+ ) {
637+ const scalar =
638+ effectiveType === "null"
639+ ? "null"
640+ : effectiveType === "integer"
641+ ? "integer"
642+ : effectiveType === "number"
643+ ? "number"
644+ : effectiveType === "boolean"
645+ ? "boolean"
646+ : asString(objectSchema.format) === "binary"
647+ ? "bytes"
648+ : "string";
649+
650+ const scalarNode: ShapeNode = {
651+ type: "scalar",
652+ scalar,
653+ ...(asString(objectSchema.format) ? { format: asString(objectSchema.format)! } : {}),
654+ ...(Object.keys(constraints).length > 0 ? { constraints } : {}),
655+ };
656+
657+ if (nullable && scalar !== "null") {
658+ const innerId = importSchema(
659+ {
660+ ...objectSchema,
661+ nullable: false,
662+ type: effectiveType,
663+ },
664+ `${key}:nonnull`,
665+ rootSchema ?? schema,
666+ );
667+ return registerNullable(innerId);
668+ }
669+
670+ register(scalarNode);
671+ return shapeId;
672+ }
673+
674+ register({
675+ type: "unknown",
676+ reason: `unsupported_schema:${key}`,
677+ }, {
678+ native: [nativeBlob({
679+ source: input.source,
680+ kind: "json_schema",
681+ pointer: key,
682+ value: schema,
683+ summary: "Unsupported JSON schema preserved natively",
684+ })],
685+ });
686+ return shapeId;
687+ };
688+
689+ return {
690+ importSchema: (schema, key) => importSchema(schema, key, schema),
691+ };
692+};
693+
694+const exampleSymbolFromValue = (input: {
695+ catalog: CatalogV1;
696+ source: Source;
697+ documentId: ReturnType<typeof DocumentIdSchema.make>;
698+ pointer: string;
699+ name?: string;
700+ value: unknown;
701+ summary?: string;
702+ description?: string;
703+}): ReturnType<typeof ExampleSymbolIdSchema.make> => {
704+ const exampleId = ExampleSymbolIdSchema.make(`example_${stableHash({
705+ pointer: input.pointer,
706+ value: input.value,
707+ })}`);
708+ mutableRecord(input.catalog.symbols)[exampleId] = {
709+ id: exampleId,
710+ kind: "example",
711+ exampleKind: "value",
712+ ...(input.name ? { name: input.name } : {}),
713+ ...(docsFrom({
714+ summary: input.summary ?? null,
715+ description: input.description ?? null,
716+ })
717+ ? {
718+ docs: docsFrom({
719+ summary: input.summary ?? null,
720+ description: input.description ?? null,
721+ })!,
722+ }
723+ : {}),
724+ value: input.value,
725+ synthetic: false,
726+ provenance: provenanceFor(input.documentId, input.pointer),
727+ } satisfies ExampleSymbol;
728+ return exampleId;
729+};
730+
731+const schemaFromField = (schema: unknown, fieldName: string): unknown => {
732+ const record = asObject(schema);
733+ const properties = asObject(record.properties);
734+ if (properties[fieldName] !== undefined) {
735+ return properties[fieldName];
736+ }
737+ return undefined;
738+};
739+
740+const groupedSchemaForParameter = (schema: unknown, location: string, name: string): unknown => {
741+ const direct = schemaFromField(schema, name);
742+ if (direct !== undefined) {
743+ return direct;
744+ }
745+
746+ const groupKey =
747+ location === "header"
748+ ? "headers"
749+ : location === "cookie"
750+ ? "cookies"
751+ : location;
752+ const groupSchema = schemaFromField(schema, groupKey);
753+ return groupSchema === undefined ? undefined : schemaFromField(groupSchema, name);
754+};
755+
756+const requestBodySchemaFromInput = (schema: unknown): unknown =>
757+ schemaFromField(schema, "body") ?? schemaFromField(schema, "input");
758+
759+const preferredResponseContentTypes = (
760+ mediaTypes: readonly string[] | undefined,
761+): string[] =>
762+ mediaTypes && mediaTypes.length > 0 ? [...mediaTypes] : ["application/json"];
763+
764+const createServiceScope = (input: {
765+ catalog: CatalogV1;
766+ source: Source;
767+ documentId: ReturnType<typeof DocumentIdSchema.make>;
768+}) => {
769+ const scopeId = serviceScopeIdForSource(input.source);
770+ mutableRecord(input.catalog.scopes)[scopeId] = {
771+ id: scopeId,
772+ kind: "service",
773+ name: input.source.name,
774+ namespace: input.source.namespace ?? namespaceFromSourceName(input.source.name),
775+ docs: docsFrom({
776+ summary: input.source.name,
777+ }),
778+ synthetic: false,
779+ provenance: provenanceFor(input.documentId, "#/service"),
780+ } satisfies Scope;
781+ return scopeId;
782+};
783+
784+const responseSetFromSingleResponse = (input: {
785+ catalog: CatalogV1;
786+ responseId: ReturnType<typeof ResponseSymbolIdSchema.make>;
787+ provenance: ProvenanceRef[];
788+ traits?: ResponseSet["variants"][number]["traits"];
789+}) => {
790+ const responseSetId = ResponseSetIdSchema.make(`response_set_${stableHash({
791+ responseId: input.responseId,
792+ traits: input.traits,
793+ })}`);
794+ mutableRecord(input.catalog.responseSets)[responseSetId] = {
795+ id: responseSetId,
796+ variants: [{
797+ match: {
798+ kind: "range",
799+ value: "2XX",
800+ },
801+ responseId: input.responseId,
802+ ...(input.traits && input.traits.length > 0 ? { traits: input.traits } : {}),
803+ }],
804+ synthetic: false,
805+ provenance: input.provenance,
806+ } satisfies ResponseSet;
807+ return responseSetId;
808+};
809+
810+const createHttpCapabilityFromOpenApi = (input: {
811+ catalog: CatalogV1;
812+ source: Source;
813+ documentId: ReturnType<typeof DocumentIdSchema.make>;
814+ serviceScopeId: ReturnType<typeof ScopeIdSchema.make>;
815+ operation: OpenApiCatalogOperationInput;
816+ importer: JsonSchemaImporter;
817+}) => {
818+ const toolPath = toolPathSegments(input.source, input.operation.providerData.toolId);
819+ const capabilityId = CapabilityIdSchema.make(`cap_${stableHash({
820+ sourceId: input.source.id,
821+ toolId: input.operation.providerData.toolId,
822+ })}`);
823+ const executableId = ExecutableIdSchema.make(`exec_${stableHash({
824+ sourceId: input.source.id,
825+ toolId: input.operation.providerData.toolId,
826+ protocol: "http",
827+ })}`);
828+ const inputSchema = input.operation.inputSchema ?? {};
829+ const outputSchema = input.operation.outputSchema ?? {};
830+ const exampleIds: Array<ReturnType<typeof ExampleSymbolIdSchema.make>> = [];
831+
832+ const parameterIds = input.operation.providerData.invocation.parameters.map((parameter) => {
833+ const parameterId = ParameterSymbolIdSchema.make(`param_${stableHash({
834+ capabilityId,
835+ location: parameter.location,
836+ name: parameter.name,
837+ })}`);
838+ const parameterSchema = groupedSchemaForParameter(inputSchema, parameter.location, parameter.name);
839+ const matchingDocs = input.operation.providerData.documentation?.parameters.find((candidate) =>
840+ candidate.name === parameter.name && candidate.location === parameter.location
841+ );
842+ const parameterExampleIds = (matchingDocs?.examples ?? []).map((example, index) => {
843+ const parsed = JSON.parse(example.valueJson) as unknown;
844+ return exampleSymbolFromValue({
845+ catalog: input.catalog,
846+ source: input.source,
847+ documentId: input.documentId,
848+ pointer: `#/openapi/${input.operation.providerData.toolId}/parameter/${parameter.location}/${parameter.name}/example/${index}`,
849+ name: example.label,
850+ summary: example.label,
851+ value: parsed,
852+ });
853+ });
854+ exampleIds.push(...parameterExampleIds);
855+ mutableRecord(input.catalog.symbols)[parameterId] = {
856+ id: parameterId,
857+ kind: "parameter",
858+ name: parameter.name,
859+ location: parameter.location,
860+ required: parameter.required,
861+ ...(docsFrom({
862+ description: matchingDocs?.description ?? null,
863+ })
864+ ? {
865+ docs: docsFrom({
866+ description: matchingDocs?.description ?? null,
867+ })!,
868+ }
869+ : {}),
870+ ...(parameterSchema !== undefined
871+ ? {
872+ schemaShapeId: input.importer.importSchema(
873+ parameterSchema,
874+ `#/openapi/${input.operation.providerData.toolId}/parameter/${parameter.location}/${parameter.name}`,
875+ ),
876+ }
877+ : {}),
878+ ...(parameterExampleIds.length > 0 ? { exampleIds: parameterExampleIds } : {}),
879+ synthetic: false,
880+ provenance: provenanceFor(
881+ input.documentId,
882+ `#/openapi/${input.operation.providerData.toolId}/parameter/${parameter.location}/${parameter.name}`,
883+ ),
884+ } satisfies ParameterSymbol;
885+ return parameterId;
886+ });
887+
888+ const requestBodyId = input.operation.providerData.invocation.requestBody
889+ ? RequestBodySymbolIdSchema.make(`request_body_${stableHash({ capabilityId })}`)
890+ : undefined;
891+
892+ if (requestBodyId) {
893+ const requestBodySchema = requestBodySchemaFromInput(inputSchema);
894+ const requestBodyExampleIds = (input.operation.providerData.documentation?.requestBody?.examples ?? []).map(
895+ (example, index) =>
896+ exampleSymbolFromValue({
897+ catalog: input.catalog,
898+ source: input.source,
899+ documentId: input.documentId,
900+ pointer: `#/openapi/${input.operation.providerData.toolId}/requestBody/example/${index}`,
901+ name: example.label,
902+ summary: example.label,
903+ value: JSON.parse(example.valueJson) as unknown,
904+ }),
905+ );
906+ exampleIds.push(...requestBodyExampleIds);
907+ const contents: ContentSpec[] = preferredResponseContentTypes(
908+ input.operation.providerData.invocation.requestBody?.contentTypes,
909+ ).map((mediaType) => ({
910+ mediaType,
911+ ...(requestBodySchema !== undefined
912+ ? {
913+ shapeId: input.importer.importSchema(
914+ requestBodySchema,
915+ `#/openapi/${input.operation.providerData.toolId}/requestBody`,
916+ ),
917+ }
918+ : {}),
919+ ...(requestBodyExampleIds.length > 0 ? { exampleIds: requestBodyExampleIds } : {}),
920+ }));
921+
922+ mutableRecord(input.catalog.symbols)[requestBodyId] = {
923+ id: requestBodyId,
924+ kind: "requestBody",
925+ ...(docsFrom({
926+ description: input.operation.providerData.documentation?.requestBody?.description ?? null,
927+ })
928+ ? {
929+ docs: docsFrom({
930+ description: input.operation.providerData.documentation?.requestBody?.description ?? null,
931+ })!,
932+ }
933+ : {}),
934+ required: input.operation.providerData.invocation.requestBody?.required ?? false,
935+ contents,
936+ synthetic: false,
937+ provenance: provenanceFor(input.documentId, `#/openapi/${input.operation.providerData.toolId}/requestBody`),
938+ };
939+ }
940+
941+ const responseId = ResponseSymbolIdSchema.make(`response_${stableHash({ capabilityId })}`);
942+ const responseExampleIds = (input.operation.providerData.documentation?.response?.examples ?? []).map((example, index) =>
943+ exampleSymbolFromValue({
944+ catalog: input.catalog,
945+ source: input.source,
946+ documentId: input.documentId,
947+ pointer: `#/openapi/${input.operation.providerData.toolId}/response/example/${index}`,
948+ name: example.label,
949+ summary: example.label,
950+ value: JSON.parse(example.valueJson) as unknown,
951+ })
952+ );
953+ exampleIds.push(...responseExampleIds);
954+ mutableRecord(input.catalog.symbols)[responseId] = {
955+ id: responseId,
956+ kind: "response",
957+ ...(docsFrom({
958+ description:
959+ input.operation.providerData.documentation?.response?.description
960+ ?? input.operation.description,
961+ })
962+ ? {
963+ docs: docsFrom({
964+ description:
965+ input.operation.providerData.documentation?.response?.description
966+ ?? input.operation.description,
967+ })!,
968+ }
969+ : {}),
970+ contents: [{
971+ mediaType: preferredResponseContentTypes(input.operation.providerData.documentation?.response?.contentTypes)[0] ?? "application/json",
972+ ...(input.operation.outputSchema !== undefined
973+ ? {
974+ shapeId: input.importer.importSchema(
975+ outputSchema,
976+ `#/openapi/${input.operation.providerData.toolId}/response`,
977+ ),
978+ }
979+ : {}),
980+ ...(responseExampleIds.length > 0 ? { exampleIds: responseExampleIds } : {}),
981+ }],
982+ synthetic: false,
983+ provenance: provenanceFor(input.documentId, `#/openapi/${input.operation.providerData.toolId}/response`),
984+ } satisfies ResponseSymbol;
985+
986+ const responseSetId = responseSetFromSingleResponse({
987+ catalog: input.catalog,
988+ responseId,
989+ provenance: provenanceFor(input.documentId, `#/openapi/${input.operation.providerData.toolId}/responseSet`),
990+ });
991+
992+ const executable: HttpExecutable = {
993+ id: executableId,
994+ protocol: "http",
995+ capabilityId,
996+ scopeId: input.serviceScopeId,
997+ method: input.operation.providerData.invocation.method.toUpperCase(),
998+ pathTemplate: input.operation.providerData.invocation.pathTemplate,
999+ pathParameterIds: parameterIds.filter((parameterId) => {
1000+ const parameter = input.catalog.symbols[parameterId] as ParameterSymbol | undefined;
1001+ return parameter?.location === "path";
1002+ }),
1003+ queryParameterIds: parameterIds.filter((parameterId) => {
1004+ const parameter = input.catalog.symbols[parameterId] as ParameterSymbol | undefined;
1005+ return parameter?.location === "query";
1006+ }),
1007+ headerParameterIds: parameterIds.filter((parameterId) => {
1008+ const parameter = input.catalog.symbols[parameterId] as ParameterSymbol | undefined;
1009+ return parameter?.location === "header";
1010+ }),
1011+ cookieParameterIds: parameterIds.filter((parameterId) => {
1012+ const parameter = input.catalog.symbols[parameterId] as ParameterSymbol | undefined;
1013+ return parameter?.location === "cookie";
1014+ }),
1015+ ...(requestBodyId ? { requestBodyId } : {}),
1016+ responseSetId,
1017+ synthetic: false,
1018+ provenance: provenanceFor(input.documentId, `#/openapi/${input.operation.providerData.toolId}/executable`),
1019+ native: [nativeBlob({
1020+ source: input.source,
1021+ kind: "openapi_provider_data",
1022+ pointer: `#/openapi/${input.operation.providerData.toolId}/providerData`,
1023+ value: input.operation.providerData,
1024+ })],
1025+ };
1026+ mutableRecord(input.catalog.executables)[executableId] = executable;
1027+
1028+ const effect = input.operation.effect;
1029+ mutableRecord(input.catalog.capabilities)[capabilityId] = {
1030+ id: capabilityId,
1031+ serviceScopeId: input.serviceScopeId,
1032+ surface: {
1033+ toolPath,
1034+ ...(input.operation.title ? { title: input.operation.title } : {}),
1035+ ...(input.operation.description ? { summary: input.operation.description } : {}),
1036+ ...(input.operation.providerData.tags.length > 0 ? { tags: input.operation.providerData.tags } : {}),
1037+ },
1038+ semantics: {
1039+ effect,
1040+ safe: effect === "read",
1041+ idempotent: effect === "read" || effect === "delete",
1042+ destructive: effect === "delete",
1043+ },
1044+ auth: { kind: "none" },
1045+ interaction: interactionForEffect(effect),
1046+ executableIds: [executableId],
1047+ ...(exampleIds.length > 0 ? { exampleIds } : {}),
1048+ synthetic: false,
1049+ provenance: provenanceFor(input.documentId, `#/openapi/${input.operation.providerData.toolId}/capability`),
1050+ native: [nativeBlob({
1051+ source: input.source,
1052+ kind: "openapi_documentation",
1053+ pointer: `#/openapi/${input.operation.providerData.toolId}/documentation`,
1054+ value: input.operation.providerData.documentation ?? null,
1055+ })],
1056+ } satisfies Capability;
1057+};
1058+
1059+const createHttpCapabilityFromGoogleDiscovery = (input: {
1060+ catalog: CatalogV1;
1061+ source: Source;
1062+ documentId: ReturnType<typeof DocumentIdSchema.make>;
1063+ serviceScopeId: ReturnType<typeof ScopeIdSchema.make>;
1064+ operation: GoogleDiscoveryCatalogOperationInput;
1065+ importer: JsonSchemaImporter;
1066+}) => {
1067+ const toolPath = toolPathSegments(input.source, input.operation.providerData.toolId);
1068+ const capabilityId = CapabilityIdSchema.make(`cap_${stableHash({
1069+ sourceId: input.source.id,
1070+ toolId: input.operation.providerData.toolId,
1071+ })}`);
1072+ const executableId = ExecutableIdSchema.make(`exec_${stableHash({
1073+ sourceId: input.source.id,
1074+ toolId: input.operation.providerData.toolId,
1075+ protocol: "http",
1076+ })}`);
1077+ const inputSchema = input.operation.inputSchema ?? {};
1078+ const outputSchema = input.operation.outputSchema ?? {};
1079+
1080+ const authSchemeId = input.operation.providerData.invocation.scopes.length > 0
1081+ ? SecuritySchemeSymbolIdSchema.make(`security_${stableHash({
1082+ sourceId: input.source.id,
1083+ scopes: input.operation.providerData.invocation.scopes,
1084+ })}`)
1085+ : undefined;
1086+
1087+ if (authSchemeId && !input.catalog.symbols[authSchemeId]) {
1088+ mutableRecord(input.catalog.symbols)[authSchemeId] = {
1089+ id: authSchemeId,
1090+ kind: "securityScheme",
1091+ schemeType: "oauth2",
1092+ docs: docsFrom({
1093+ summary: "OAuth 2.0",
1094+ description: "Imported from Google Discovery scopes.",
1095+ }),
1096+ oauth: {
1097+ flows: {},
1098+ scopes: Object.fromEntries(input.operation.providerData.invocation.scopes.map((scope) => [scope, scope])),
1099+ },
1100+ synthetic: false,
1101+ provenance: provenanceFor(input.documentId, "#/googleDiscovery/security"),
1102+ } satisfies SecuritySchemeSymbol;
1103+ }
1104+
1105+ const parameterIds = input.operation.providerData.invocation.parameters.map((parameter) => {
1106+ const parameterId = ParameterSymbolIdSchema.make(`param_${stableHash({
1107+ capabilityId,
1108+ location: parameter.location,
1109+ name: parameter.name,
1110+ })}`);
1111+ const parameterSchema = groupedSchemaForParameter(inputSchema, parameter.location, parameter.name)
1112+ ?? (parameter.repeated
1113+ ? {
1114+ type: "array",
1115+ items: {
1116+ type: parameter.type === "integer" ? "integer" : "string",
1117+ ...(parameter.enum ? { enum: parameter.enum } : {}),
1118+ },
1119+ }
1120+ : {
1121+ type: parameter.type === "integer" ? "integer" : "string",
1122+ ...(parameter.enum ? { enum: parameter.enum } : {}),
1123+ });
1124+
1125+ mutableRecord(input.catalog.symbols)[parameterId] = {
1126+ id: parameterId,
1127+ kind: "parameter",
1128+ name: parameter.name,
1129+ location: parameter.location,
1130+ required: parameter.required,
1131+ ...(docsFrom({
1132+ description: parameter.description,
1133+ })
1134+ ? {
1135+ docs: docsFrom({
1136+ description: parameter.description,
1137+ })!,
1138+ }
1139+ : {}),
1140+ schemaShapeId: input.importer.importSchema(
1141+ parameterSchema,
1142+ `#/googleDiscovery/${input.operation.providerData.toolId}/parameter/${parameter.location}/${parameter.name}`,
1143+ ),
1144+ synthetic: false,
1145+ provenance: provenanceFor(
1146+ input.documentId,
1147+ `#/googleDiscovery/${input.operation.providerData.toolId}/parameter/${parameter.location}/${parameter.name}`,
1148+ ),
1149+ } satisfies ParameterSymbol;
1150+ return parameterId;
1151+ });
1152+
1153+ const requestBodyId = input.operation.providerData.invocation.requestSchemaId || requestBodySchemaFromInput(inputSchema) !== undefined
1154+ ? RequestBodySymbolIdSchema.make(`request_body_${stableHash({ capabilityId })}`)
1155+ : undefined;
1156+
1157+ if (requestBodyId) {
1158+ const requestBodySchema = requestBodySchemaFromInput(inputSchema) ?? inputSchema;
1159+ mutableRecord(input.catalog.symbols)[requestBodyId] = {
1160+ id: requestBodyId,
1161+ kind: "requestBody",
1162+ contents: [{
1163+ mediaType: "application/json",
1164+ shapeId: input.importer.importSchema(
1165+ requestBodySchema,
1166+ `#/googleDiscovery/${input.operation.providerData.toolId}/requestBody`,
1167+ ),
1168+ }],
1169+ synthetic: false,
1170+ provenance: provenanceFor(input.documentId, `#/googleDiscovery/${input.operation.providerData.toolId}/requestBody`),
1171+ };
1172+ }
1173+
1174+ const responseId = ResponseSymbolIdSchema.make(`response_${stableHash({ capabilityId })}`);
1175+ mutableRecord(input.catalog.symbols)[responseId] = {
1176+ id: responseId,
1177+ kind: "response",
1178+ ...(docsFrom({
1179+ description: input.operation.description,
1180+ })
1181+ ? {
1182+ docs: docsFrom({
1183+ description: input.operation.description,
1184+ })!,
1185+ }
1186+ : {}),
1187+ ...(input.operation.outputSchema !== undefined
1188+ ? {
1189+ contents: [{
1190+ mediaType: "application/json",
1191+ shapeId: input.importer.importSchema(
1192+ outputSchema,
1193+ `#/googleDiscovery/${input.operation.providerData.toolId}/response`,
1194+ ),
1195+ }],
1196+ }
1197+ : {}),
1198+ synthetic: false,
1199+ provenance: provenanceFor(input.documentId, `#/googleDiscovery/${input.operation.providerData.toolId}/response`),
1200+ } satisfies ResponseSymbol;
1201+
1202+ const traits: Array<"upload" | "download"> = [];
1203+ if (input.operation.providerData.invocation.supportsMediaUpload) {
1204+ traits.push("upload");
1205+ }
1206+ if (input.operation.providerData.invocation.supportsMediaDownload) {
1207+ traits.push("download");
1208+ }
1209+
1210+ const responseSetId = responseSetFromSingleResponse({
1211+ catalog: input.catalog,
1212+ responseId,
1213+ provenance: provenanceFor(input.documentId, `#/googleDiscovery/${input.operation.providerData.toolId}/responseSet`),
1214+ traits,
1215+ });
1216+
1217+ mutableRecord(input.catalog.executables)[executableId] = {
1218+ id: executableId,
1219+ protocol: "http",
1220+ capabilityId,
1221+ scopeId: input.serviceScopeId,
1222+ method: input.operation.providerData.invocation.method.toUpperCase(),
1223+ pathTemplate: input.operation.providerData.invocation.path,
1224+ pathParameterIds: parameterIds.filter((parameterId) => (input.catalog.symbols[parameterId] as ParameterSymbol | undefined)?.location === "path"),
1225+ queryParameterIds: parameterIds.filter((parameterId) => (input.catalog.symbols[parameterId] as ParameterSymbol | undefined)?.location === "query"),
1226+ headerParameterIds: parameterIds.filter((parameterId) => (input.catalog.symbols[parameterId] as ParameterSymbol | undefined)?.location === "header"),
1227+ ...(requestBodyId ? { requestBodyId } : {}),
1228+ responseSetId,
1229+ synthetic: false,
1230+ provenance: provenanceFor(input.documentId, `#/googleDiscovery/${input.operation.providerData.toolId}/executable`),
1231+ native: [nativeBlob({
1232+ source: input.source,
1233+ kind: "google_discovery_provider_data",
1234+ pointer: `#/googleDiscovery/${input.operation.providerData.toolId}/providerData`,
1235+ value: input.operation.providerData,
1236+ })],
1237+ } satisfies HttpExecutable;
1238+
1239+ const effect = input.operation.effect;
1240+ const auth: AuthRequirement =
1241+ authSchemeId
1242+ ? {
1243+ kind: "scheme",
1244+ schemeId: authSchemeId,
1245+ scopes: input.operation.providerData.invocation.scopes,
1246+ }
1247+ : { kind: "none" };
1248+
1249+ mutableRecord(input.catalog.capabilities)[capabilityId] = {
1250+ id: capabilityId,
1251+ serviceScopeId: input.serviceScopeId,
1252+ surface: {
1253+ toolPath,
1254+ ...(input.operation.title ? { title: input.operation.title } : {}),
1255+ ...(input.operation.description ? { summary: input.operation.description } : {}),
1256+ tags: ["google", input.operation.providerData.service, input.operation.providerData.version],
1257+ },
1258+ semantics: {
1259+ effect,
1260+ safe: effect === "read",
1261+ idempotent: effect === "read" || effect === "delete",
1262+ destructive: effect === "delete",
1263+ },
1264+ auth,
1265+ interaction: interactionForEffect(effect),
1266+ executableIds: [executableId],
1267+ synthetic: false,
1268+ provenance: provenanceFor(input.documentId, `#/googleDiscovery/${input.operation.providerData.toolId}/capability`),
1269+ native: [nativeBlob({
1270+ source: input.source,
1271+ kind: "google_discovery_provider_data",
1272+ pointer: `#/googleDiscovery/${input.operation.providerData.toolId}/providerData`,
1273+ value: input.operation.providerData,
1274+ })],
1275+ } satisfies Capability;
1276+};
1277+
1278+const createGraphqlCapability = (input: {
1279+ catalog: CatalogV1;
1280+ source: Source;
1281+ documentId: ReturnType<typeof DocumentIdSchema.make>;
1282+ serviceScopeId: ReturnType<typeof ScopeIdSchema.make>;
1283+ operation: GraphqlCatalogOperationInput;
1284+ importer: JsonSchemaImporter;
1285+}) => {
1286+ const toolPath = toolPathSegments(input.source, input.operation.providerData.toolId);
1287+ const capabilityId = CapabilityIdSchema.make(`cap_${stableHash({
1288+ sourceId: input.source.id,
1289+ toolId: input.operation.providerData.toolId,
1290+ })}`);
1291+ const executableId = ExecutableIdSchema.make(`exec_${stableHash({
1292+ sourceId: input.source.id,
1293+ toolId: input.operation.providerData.toolId,
1294+ protocol: "graphql",
1295+ })}`);
1296+ const argumentShapeId = input.operation.inputSchema !== undefined
1297+ ? input.importer.importSchema(
1298+ input.operation.inputSchema,
1299+ `#/graphql/${input.operation.providerData.toolId}/input`,
1300+ )
1301+ : input.importer.importSchema(
1302+ {
1303+ type: "object",
1304+ additionalProperties: true,
1305+ },
1306+ `#/graphql/${input.operation.providerData.toolId}/input`,
1307+ );
1308+ const resultShapeId = input.operation.outputSchema !== undefined
1309+ ? input.importer.importSchema(
1310+ input.operation.outputSchema,
1311+ `#/graphql/${input.operation.providerData.toolId}/output`,
1312+ )
1313+ : input.importer.importSchema(
1314+ {
1315+ type: "object",
1316+ additionalProperties: true,
1317+ },
1318+ `#/graphql/${input.operation.providerData.toolId}/output`,
1319+ );
1320+
1321+ const responseId = ResponseSymbolIdSchema.make(`response_${stableHash({ capabilityId })}`);
1322+ mutableRecord(input.catalog.symbols)[responseId] = {
1323+ id: responseId,
1324+ kind: "response",
1325+ ...(docsFrom({
1326+ description: input.operation.description,
1327+ })
1328+ ? {
1329+ docs: docsFrom({
1330+ description: input.operation.description,
1331+ })!,
1332+ }
1333+ : {}),
1334+ contents: [{
1335+ mediaType: "application/json",
1336+ shapeId: resultShapeId,
1337+ }],
1338+ synthetic: false,
1339+ provenance: provenanceFor(input.documentId, `#/graphql/${input.operation.providerData.toolId}/response`),
1340+ } satisfies ResponseSymbol;
1341+
1342+ const responseSetId = responseSetFromSingleResponse({
1343+ catalog: input.catalog,
1344+ responseId,
1345+ provenance: provenanceFor(input.documentId, `#/graphql/${input.operation.providerData.toolId}/responseSet`),
1346+ });
1347+
1348+ mutableRecord(input.catalog.executables)[executableId] = {
1349+ id: executableId,
1350+ protocol: "graphql",
1351+ capabilityId,
1352+ scopeId: input.serviceScopeId,
1353+ operationType: input.operation.providerData.operationType ?? "query",
1354+ rootField:
1355+ input.operation.providerData.fieldName
1356+ ?? input.operation.providerData.leaf
1357+ ?? input.operation.providerData.toolId,
1358+ argumentShapeId,
1359+ resultShapeId,
1360+ selectionMode: "fixed",
1361+ responseSetId,
1362+ synthetic: false,
1363+ provenance: provenanceFor(input.documentId, `#/graphql/${input.operation.providerData.toolId}/executable`),
1364+ native: [nativeBlob({
1365+ source: input.source,
1366+ kind: "graphql_provider_data",
1367+ pointer: `#/graphql/${input.operation.providerData.toolId}/providerData`,
1368+ value: input.operation.providerData,
1369+ })],
1370+ } satisfies GraphQLExecutable;
1371+
1372+ const effect = input.operation.effect;
1373+
1374+ mutableRecord(input.catalog.capabilities)[capabilityId] = {
1375+ id: capabilityId,
1376+ serviceScopeId: input.serviceScopeId,
1377+ surface: {
1378+ toolPath,
1379+ ...(input.operation.title ? { title: input.operation.title } : {}),
1380+ ...(input.operation.description ? { summary: input.operation.description } : {}),
1381+ ...(input.operation.providerData.group ? { tags: [input.operation.providerData.group] } : {}),
1382+ },
1383+ semantics: {
1384+ effect,
1385+ safe: effect === "read",
1386+ idempotent: effect === "read",
1387+ destructive: false,
1388+ },
1389+ auth: { kind: "none" },
1390+ interaction: interactionForEffect(effect),
1391+ executableIds: [executableId],
1392+ synthetic: false,
1393+ provenance: provenanceFor(input.documentId, `#/graphql/${input.operation.providerData.toolId}/capability`),
1394+ native: [nativeBlob({
1395+ source: input.source,
1396+ kind: "graphql_provider_data",
1397+ pointer: `#/graphql/${input.operation.providerData.toolId}/providerData`,
1398+ value: input.operation.providerData,
1399+ })],
1400+ } satisfies Capability;
1401+};
1402+
1403+const createMcpCapability = (input: {
1404+ catalog: CatalogV1;
1405+ source: Source;
1406+ documentId: ReturnType<typeof DocumentIdSchema.make>;
1407+ serviceScopeId: ReturnType<typeof ScopeIdSchema.make>;
1408+ operation: McpCatalogOperationInput;
1409+ importer: JsonSchemaImporter;
1410+}) => {
1411+ const toolPath = toolPathSegments(input.source, input.operation.providerData.toolId);
1412+ const capabilityId = CapabilityIdSchema.make(`cap_${stableHash({
1413+ sourceId: input.source.id,
1414+ toolId: input.operation.providerData.toolId,
1415+ })}`);
1416+ const executableId = ExecutableIdSchema.make(`exec_${stableHash({
1417+ sourceId: input.source.id,
1418+ toolId: input.operation.providerData.toolId,
1419+ protocol: "mcp",
1420+ })}`);
1421+ const inputShapeId = input.operation.inputSchema !== undefined
1422+ ? input.importer.importSchema(
1423+ input.operation.inputSchema,
1424+ `#/mcp/${input.operation.providerData.toolId}/input`,
1425+ )
1426+ : undefined;
1427+ const outputShapeId = input.operation.outputSchema !== undefined
1428+ ? input.importer.importSchema(
1429+ input.operation.outputSchema,
1430+ `#/mcp/${input.operation.providerData.toolId}/output`,
1431+ )
1432+ : undefined;
1433+
1434+ const responseId = ResponseSymbolIdSchema.make(`response_${stableHash({ capabilityId })}`);
1435+ mutableRecord(input.catalog.symbols)[responseId] = {
1436+ id: responseId,
1437+ kind: "response",
1438+ ...(docsFrom({
1439+ description: input.operation.providerData.description ?? input.operation.description,
1440+ })
1441+ ? {
1442+ docs: docsFrom({
1443+ description: input.operation.providerData.description ?? input.operation.description,
1444+ })!,
1445+ }
1446+ : {}),
1447+ ...(outputShapeId
1448+ ? {
1449+ contents: [{
1450+ mediaType: "application/json",
1451+ shapeId: outputShapeId,
1452+ }],
1453+ }
1454+ : {}),
1455+ synthetic: false,
1456+ provenance: provenanceFor(input.documentId, `#/mcp/${input.operation.providerData.toolId}/response`),
1457+ } satisfies ResponseSymbol;
1458+ const responseSetId = responseSetFromSingleResponse({
1459+ catalog: input.catalog,
1460+ responseId,
1461+ provenance: provenanceFor(input.documentId, `#/mcp/${input.operation.providerData.toolId}/responseSet`),
1462+ });
1463+
1464+ mutableRecord(input.catalog.executables)[executableId] = {
1465+ id: executableId,
1466+ protocol: "mcp",
1467+ capabilityId,
1468+ scopeId: input.serviceScopeId,
1469+ serverRef: input.source.endpoint,
1470+ toolName: input.operation.providerData.toolName,
1471+ ...(inputShapeId ? { inputShapeId } : {}),
1472+ ...(outputShapeId ? { outputShapeId } : {}),
1473+ responseSetId,
1474+ synthetic: false,
1475+ provenance: provenanceFor(input.documentId, `#/mcp/${input.operation.providerData.toolId}/executable`),
1476+ native: [nativeBlob({
1477+ source: input.source,
1478+ kind: "mcp_provider_data",
1479+ pointer: `#/mcp/${input.operation.providerData.toolId}/providerData`,
1480+ value: input.operation.providerData,
1481+ })],
1482+ } satisfies McpExecutable;
1483+
1484+ mutableRecord(input.catalog.capabilities)[capabilityId] = {
1485+ id: capabilityId,
1486+ serviceScopeId: input.serviceScopeId,
1487+ surface: {
1488+ toolPath,
1489+ title: input.operation.providerData.toolName,
1490+ ...(input.operation.providerData.description ? { summary: input.operation.providerData.description } : {}),
1491+ },
1492+ semantics: {
1493+ effect: input.operation.effect,
1494+ safe: input.operation.effect === "read",
1495+ idempotent: input.operation.effect === "read",
1496+ destructive: false,
1497+ },
1498+ auth: { kind: "none" },
1499+ interaction: interactionForEffect(input.operation.effect),
1500+ executableIds: [executableId],
1501+ synthetic: false,
1502+ provenance: provenanceFor(input.documentId, `#/mcp/${input.operation.providerData.toolId}/capability`),
1503+ } satisfies Capability;
1504+};
1505+
1506+const buildCatalogSnapshot = (input: {
1507+ source: Source;
1508+ adapterKey: string;
1509+ documents: readonly CatalogSourceDocumentInput[];
1510+ registerOperations: (context: {
1511+ catalog: CatalogV1;
1512+ documentId: ReturnType<typeof DocumentIdSchema.make>;
1513+ serviceScopeId: ReturnType<typeof ScopeIdSchema.make>;
1514+ importer: JsonSchemaImporter;
1515+ }) => void;
1516+}): CatalogSnapshotV1 => {
1517+ const catalog = createEmptyCatalogV1();
1518+ const primaryDocumentKey =
1519+ input.documents[0]?.documentKey
1520+ ?? input.source.endpoint
1521+ ?? input.source.id;
1522+ const primaryDocumentId = documentIdFor(input.source, primaryDocumentKey);
1523+ const primaryResourceId = resourceIdForSource(input.source);
1524+
1525+ const documents = input.documents.length > 0
1526+ ? input.documents
1527+ : [{
1528+ documentKind: "synthetic",
1529+ documentKey: input.source.endpoint,
1530+ fetchedAt: Date.now(),
1531+ contentText: "{}",
1532+ }];
1533+
1534+ for (const document of documents) {
1535+ const documentId = documentIdFor(input.source, `${document.documentKind}:${document.documentKey}`);
1536+ mutableRecord(catalog.documents)[documentId] = {
1537+ id: documentId,
1538+ kind: sourceKindFromSource(input.source),
1539+ title: input.source.name,
1540+ fetchedAt: new Date(document.fetchedAt ?? Date.now()).toISOString(),
1541+ rawRef: document.documentKey,
1542+ entryUri: document.documentKey.startsWith("http") ? document.documentKey : undefined,
1543+ native: [nativeBlob({
1544+ source: input.source,
1545+ kind: "source_document",
1546+ pointer: `#/${document.documentKind}`,
1547+ value: document.contentText,
1548+ summary: document.documentKind,
1549+ })],
1550+ } satisfies SourceDocument;
1551+ }
1552+
1553+ mutableRecord(catalog.resources)[primaryResourceId] = {
1554+ id: primaryResourceId,
1555+ documentId: primaryDocumentId,
1556+ canonicalUri: primaryDocumentKey,
1557+ baseUri: primaryDocumentKey,
1558+ ...(input.source.kind === "openapi" || input.source.kind === "graphql"
1559+ ? {
1560+ dialectUri:
1561+ input.source.kind === "openapi"
1562+ ? "https://json-schema.org/draft/2020-12/schema"
1563+ : "https://spec.graphql.org/",
1564+ }
1565+ : {}),
1566+ anchors: {},
1567+ dynamicAnchors: {},
1568+ synthetic: false,
1569+ provenance: provenanceFor(primaryDocumentId, "#"),
1570+ };
1571+
1572+ const serviceScopeId = createServiceScope({
1573+ catalog,
1574+ source: input.source,
1575+ documentId: primaryDocumentId,
1576+ });
1577+ const importer = createJsonSchemaImporter({
1578+ catalog,
1579+ source: input.source,
1580+ resourceId: primaryResourceId,
1581+ documentId: primaryDocumentId,
1582+ });
1583+ input.registerOperations({
1584+ catalog,
1585+ documentId: primaryDocumentId,
1586+ serviceScopeId,
1587+ importer,
1588+ });
1589+
1590+ const snapshot = createCatalogSnapshotV1({
1591+ import: importMetadataFor(input),
1592+ catalog,
1593+ });
1594+
1595+ return snapshot;
1596+};
1597+
1598+export const createOpenApiCatalogSnapshot = (input: {
1599+ source: Source;
1600+ documents: readonly CatalogSourceDocumentInput[];
1601+ operations: readonly OpenApiCatalogOperationInput[];
1602+}): CatalogSnapshotV1 =>
1603+ buildCatalogSnapshot({
1604+ source: input.source,
1605+ adapterKey: "openapi",
1606+ documents: input.documents,
1607+ registerOperations: ({ catalog, documentId, serviceScopeId, importer }) => {
1608+ for (const operation of input.operations) {
1609+ createHttpCapabilityFromOpenApi({
1610+ catalog,
1611+ source: input.source,
1612+ documentId,
1613+ serviceScopeId,
1614+ operation,
1615+ importer,
1616+ });
1617+ }
1618+ },
1619+ });
1620+
1621+export const createGoogleDiscoveryCatalogSnapshot = (input: {
1622+ source: Source;
1623+ documents: readonly CatalogSourceDocumentInput[];
1624+ operations: readonly GoogleDiscoveryCatalogOperationInput[];
1625+}): CatalogSnapshotV1 =>
1626+ buildCatalogSnapshot({
1627+ source: input.source,
1628+ adapterKey: "google_discovery",
1629+ documents: input.documents,
1630+ registerOperations: ({ catalog, documentId, serviceScopeId, importer }) => {
1631+ for (const operation of input.operations) {
1632+ createHttpCapabilityFromGoogleDiscovery({
1633+ catalog,
1634+ source: input.source,
1635+ documentId,
1636+ serviceScopeId,
1637+ operation,
1638+ importer,
1639+ });
1640+ }
1641+ },
1642+ });
1643+
1644+export const createGraphqlCatalogSnapshot = (input: {
1645+ source: Source;
1646+ documents: readonly CatalogSourceDocumentInput[];
1647+ operations: readonly GraphqlCatalogOperationInput[];
1648+}): CatalogSnapshotV1 =>
1649+ buildCatalogSnapshot({
1650+ source: input.source,
1651+ adapterKey: "graphql",
1652+ documents: input.documents,
1653+ registerOperations: ({ catalog, documentId, serviceScopeId, importer }) => {
1654+ for (const operation of input.operations) {
1655+ createGraphqlCapability({
1656+ catalog,
1657+ source: input.source,
1658+ documentId,
1659+ serviceScopeId,
1660+ operation,
1661+ importer,
1662+ });
1663+ }
1664+ },
1665+ });
1666+
1667+export const createMcpCatalogSnapshot = (input: {
1668+ source: Source;
1669+ documents: readonly CatalogSourceDocumentInput[];
1670+ operations: readonly McpCatalogOperationInput[];
1671+}): CatalogSnapshotV1 =>
1672+ buildCatalogSnapshot({
1673+ source: input.source,
1674+ adapterKey: "mcp",
1675+ documents: input.documents,
1676+ registerOperations: ({ catalog, documentId, serviceScopeId, importer }) => {
1677+ for (const operation of input.operations) {
1678+ createMcpCapability({
1679+ catalog,
1680+ source: input.source,
1681+ documentId,
1682+ serviceScopeId,
1683+ operation,
1684+ importer,
1685+ });
1686+ }
1687+ },
1688+ });
packages/control-plane/src/runtime/source-catalog-support.ts+18−0
@@ -0,0 +1,18 @@
1+import { sha256Hex } from "@executor/codemode-core";
2+import type { CatalogSnapshotV1 } from "../ir/model";
3+
4+export const normalizeSearchText = (
5+ ...parts: ReadonlyArray<string | null | undefined>
6+): string =>
7+ parts
8+ .flatMap((part) => (part ? [part.trim()] : []))
9+ .filter(Boolean)
10+ .join(" ")
11+ .toLowerCase();
12+
13+export const contentHash = (value: string): string => sha256Hex(value);
14+
15+export type SourceCatalogSyncResult = {
16+ snapshot: CatalogSnapshotV1;
17+ sourceHash: string | null;
18+};
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
2727 getSourceAdapterForSource,
2828 } from "./source-adapters";
2929 import {
30− materializationFromMcpManifestEntries,
30+ catalogSyncResultFromMcpManifestEntries,
3131 } from "./source-adapters/mcp";
3232 import { SecretMaterialResolverService } from "./secret-material-providers";
3333
@@ -36,7 +36,7 @@ const shouldIndexSource = (source: Source): boolean => · 2 unmodified lines
3636 && source.status === "connected"
3737 && getSourceAdapterForSource(source).family !== "internal";
3838
39−type RuntimeSourceMaterializationDeps = {
39+type RuntimeSourceCatalogSyncDeps = {
4040 runtimeLocalWorkspace: RuntimeLocalWorkspaceState;
4141 workspaceStateStore: WorkspaceStateStoreShape;
4242 sourceArtifactStore: SourceArtifactStoreShape;
@@ -44,30 +44,30 @@ type RuntimeSourceMaterializationDeps = { · 1 unmodified line
4444 sourceAuthMaterialService: Effect.Effect.Success<typeof RuntimeSourceAuthMaterialService>;
4545 };
4646
47−type SourceMaterializationServices =
47+type SourceCatalogSyncServices =
4848 | RuntimeLocalWorkspaceService
4949 | WorkspaceStateStore
5050 | SourceArtifactStore
5151 | RuntimeSourceAuthMaterialService
5252 | SecretMaterialResolverService;
5353
54−export type RuntimeSourceMaterializationShape = {
54+export type RuntimeSourceCatalogSyncShape = {
5555 sync: (input: {
5656 source: Source;
5757 actorAccountId?: AccountId | null;
5858 }) => Effect.Effect<void, Error, never>;
59− persistMcpRecipeMaterializationFromManifest: (input: {
59+ persistMcpCatalogSnapshotFromManifest: (input: {
6060 source: Source;
61− manifestEntries: Parameters<typeof materializationFromMcpManifestEntries>[0]["manifestEntries"];
61+ manifestEntries: Parameters<typeof catalogSyncResultFromMcpManifestEntries>[0]["manifestEntries"];
6262 }) => Effect.Effect<void, Error, never>;
6363 };
6464
65−export class RuntimeSourceMaterializationService extends Context.Tag(
66− "#runtime/RuntimeSourceMaterializationService",
67−)<RuntimeSourceMaterializationService, RuntimeSourceMaterializationShape>() {}
65+export class RuntimeSourceCatalogSyncService extends Context.Tag(
66+ "#runtime/RuntimeSourceCatalogSyncService",
67+)<RuntimeSourceCatalogSyncService, RuntimeSourceCatalogSyncShape>() {}
6868
69−const ensureRuntimeMaterializationWorkspace = (
70− deps: RuntimeSourceMaterializationDeps,
69+const ensureRuntimeCatalogSyncWorkspace = (
70+ deps: RuntimeSourceCatalogSyncDeps,
7171 workspaceId: Source["workspaceId"],
7272 ) => {
7373 if (deps.runtimeLocalWorkspace.installation.workspaceId !== workspaceId) {
@@ -81,15 +81,15 @@ const ensureRuntimeMaterializationWorkspace = ( · 7 unmodified lines
8181 return Effect.succeed(deps.runtimeLocalWorkspace.context);
8282 };
8383
84−const syncSourceMaterializationWithDeps = (
85− deps: RuntimeSourceMaterializationDeps,
84+const syncSourceCatalogWithDeps = (
85+ deps: RuntimeSourceCatalogSyncDeps,
8686 input: {
8787 source: Source;
8888 actorAccountId?: AccountId | null;
8989 },
9090 ): Effect.Effect<void, Error, never> =>
9191 Effect.gen(function* () {
92− const workspaceContext = yield* ensureRuntimeMaterializationWorkspace(
92+ const workspaceContext = yield* ensureRuntimeCatalogSyncWorkspace(
9393 deps,
9494 input.source.workspaceId,
9595 );
@@ -118,7 +118,7 @@ const syncSourceMaterializationWithDeps = ( · 22 unmodified lines
118118 }
119119
120120 const adapter = getSourceAdapterForSource(input.source);
121− const materialization = yield* adapter.materializeSource({
121+ const syncResult = yield* adapter.syncCatalog({
122122 source: input.source,
123123 resolveSecretMaterial: deps.resolveSecretMaterial,
124124 resolveAuthMaterialForSlot: (slot) =>
@@ -133,7 +133,7 @@ const syncSourceMaterializationWithDeps = ( · 8 unmodified lines
133133 sourceId: input.source.id,
134134 artifact: deps.sourceArtifactStore.build({
135135 source: input.source,
136− materialization,
136+ syncResult,
137137 }),
138138 });
139139
@@ -146,7 +146,7 @@ const syncSourceMaterializationWithDeps = ( · 6 unmodified lines
146146 [input.source.id]: {
147147 status: "connected",
148148 lastError: null,
149− sourceHash: materialization.sourceHash,
149+ sourceHash: syncResult.sourceHash,
150150 createdAt: existingSourceState?.createdAt ?? input.source.createdAt,
151151 updatedAt: Date.now(),
152152 },
@@ -158,20 +158,20 @@ const syncSourceMaterializationWithDeps = ( · 5 unmodified lines
158158 });
159159 });
160160
161−const persistMcpRecipeMaterializationFromManifestWithDeps = (
162− deps: RuntimeSourceMaterializationDeps,
161+const persistMcpCatalogSnapshotFromManifestWithDeps = (
162+ deps: RuntimeSourceCatalogSyncDeps,
163163 input: {
164164 source: Source;
165− manifestEntries: Parameters<typeof materializationFromMcpManifestEntries>[0]["manifestEntries"];
165+ manifestEntries: Parameters<typeof catalogSyncResultFromMcpManifestEntries>[0]["manifestEntries"];
166166 },
167167 ): Effect.Effect<void, Error, never> =>
168168 Effect.gen(function* () {
169− const workspaceContext = yield* ensureRuntimeMaterializationWorkspace(
169+ const workspaceContext = yield* ensureRuntimeCatalogSyncWorkspace(
170170 deps,
171171 input.source.workspaceId,
172172 );
173− const materialization = materializationFromMcpManifestEntries({
174− recipeRevisionId: "src_recipe_rev_materialization" as never,
173+ const syncResult = catalogSyncResultFromMcpManifestEntries({
174+ source: input.source,
175175 endpoint: input.source.endpoint,
176176 manifestEntries: input.manifestEntries,
177177 });
@@ -181,15 +181,15 @@ const persistMcpRecipeMaterializationFromManifestWithDeps = ( · 3 unmodified lines
181181 sourceId: input.source.id,
182182 artifact: deps.sourceArtifactStore.build({
183183 source: input.source,
184− materialization,
184+ syncResult,
185185 }),
186186 });
187187 });
188188
189−export const syncSourceMaterialization = (input: {
189+export const syncSourceCatalog = (input: {
190190 source: Source;
191191 actorAccountId?: AccountId | null;
192−}): Effect.Effect<void, Error, SourceMaterializationServices> =>
192+}): Effect.Effect<void, Error, SourceCatalogSyncServices> =>
193193 Effect.gen(function* () {
194194 const runtimeLocalWorkspace = yield* RuntimeLocalWorkspaceService;
195195 const workspaceStateStore = yield* WorkspaceStateStore;
@@ -197,7 +197,7 @@ export const syncSourceMaterialization = (input: { · 1 unmodified line
197197 const resolveSecretMaterial = yield* SecretMaterialResolverService;
198198 const sourceAuthMaterialService = yield* RuntimeSourceAuthMaterialService;
199199
200− return yield* syncSourceMaterializationWithDeps(
200+ return yield* syncSourceCatalogWithDeps(
201201 {
202202 runtimeLocalWorkspace,
203203 workspaceStateStore,
@@ -212,10 +212,10 @@ export const syncSourceMaterialization = (input: { · 8 unmodified lines
212212 );
213213 });
214214
215−export const persistMcpRecipeMaterializationFromManifest = (input: {
215+export const persistMcpCatalogSnapshotFromManifest = (input: {
216216 source: Source;
217− manifestEntries: Parameters<typeof materializationFromMcpManifestEntries>[0]["manifestEntries"];
218−}): Effect.Effect<void, Error, SourceMaterializationServices> =>
217+ manifestEntries: Parameters<typeof catalogSyncResultFromMcpManifestEntries>[0]["manifestEntries"];
218+}): Effect.Effect<void, Error, SourceCatalogSyncServices> =>
219219 Effect.gen(function* () {
220220 const runtimeLocalWorkspace = yield* RuntimeLocalWorkspaceService;
221221 const workspaceStateStore = yield* WorkspaceStateStore;
@@ -223,7 +223,7 @@ export const persistMcpRecipeMaterializationFromManifest = (input: { · 1 unmodified line
223223 const resolveSecretMaterial = yield* SecretMaterialResolverService;
224224 const sourceAuthMaterialService = yield* RuntimeSourceAuthMaterialService;
225225
226− return yield* persistMcpRecipeMaterializationFromManifestWithDeps(
226+ return yield* persistMcpCatalogSnapshotFromManifestWithDeps(
227227 {
228228 runtimeLocalWorkspace,
229229 workspaceStateStore,
@@ -235,8 +235,8 @@ export const persistMcpRecipeMaterializationFromManifest = (input: { · 5 unmodified lines
235235 );
236236 });
237237
238−export const RuntimeSourceMaterializationLive = Layer.effect(
239− RuntimeSourceMaterializationService,
238+export const RuntimeSourceCatalogSyncLive = Layer.effect(
239+ RuntimeSourceCatalogSyncService,
240240 Effect.gen(function* () {
241241 const runtimeLocalWorkspace = yield* RuntimeLocalWorkspaceService;
242242 const workspaceStateStore = yield* WorkspaceStateStore;
@@ -244,7 +244,7 @@ export const RuntimeSourceMaterializationLive = Layer.effect( · 1 unmodified line
244244 const resolveSecretMaterial = yield* SecretMaterialResolverService;
245245 const sourceAuthMaterialService = yield* RuntimeSourceAuthMaterialService;
246246
247− const deps: RuntimeSourceMaterializationDeps = {
247+ const deps: RuntimeSourceCatalogSyncDeps = {
248248 runtimeLocalWorkspace,
249249 workspaceStateStore,
250250 sourceArtifactStore,
@@ -252,10 +252,10 @@ export const RuntimeSourceMaterializationLive = Layer.effect( · 1 unmodified line
252252 sourceAuthMaterialService,
253253 };
254254
255− return RuntimeSourceMaterializationService.of({
256− sync: (input) => syncSourceMaterializationWithDeps(deps, input),
257− persistMcpRecipeMaterializationFromManifest: (input) =>
258− persistMcpRecipeMaterializationFromManifestWithDeps(deps, input),
255+ return RuntimeSourceCatalogSyncService.of({
256+ sync: (input) => syncSourceCatalogWithDeps(deps, input),
257+ persistMcpCatalogSnapshotFromManifest: (input) =>
258+ persistMcpCatalogSnapshotFromManifestWithDeps(deps, input),
259259 });
260260 }),
261261 );
packages/control-plane/src/runtime/source-definitions.test.ts+23−23
@@ -1,4 +1,4 @@
1−import { describe, expect, it } from "vitest";
1+import { describe, expect, it } from "@effect/vitest";
22 import * as Effect from "effect/Effect";
33
44 import {
@@ -13,8 +13,8 @@ import { · 8 unmodified lines
1313 createSourceFromPayload,
1414 projectSourceFromStorage,
1515 splitSourceForStorage,
16− stableSourceRecipeId,
17− stableSourceRecipeRevisionId,
16+ stableSourceCatalogId,
17+ stableSourceCatalogRevisionId,
1818 updateSourceFromPayload,
1919 } from "./source-definitions";
2020 import { namespaceFromSourceName } from "./source-names";
@@ -63,7 +63,7 @@ const makeSource = (overrides: Partial<Source> = {}): Source => ({ · 42 unmodified lines
6363 });
6464
6565 describe("source-definitions", () => {
66− describe("stable recipe ids", () => {
66+ describe("stable catalog ids", () => {
6767 it("is deterministic across calls and ignores source name/workspace", () => {
6868 const source = makeSource();
6969 const renamed = makeSource({
@@ -73,26 +73,26 @@ describe("source-definitions", () => { · 3 unmodified lines
7373 workspaceId: WorkspaceIdSchema.make("ws_source_definitions_other"),
7474 });
7575
76− expect(stableSourceRecipeId(source)).toBe(stableSourceRecipeId(source));
77− expect(stableSourceRecipeRevisionId(source)).toBe(stableSourceRecipeRevisionId(source));
78− expect(stableSourceRecipeId(renamed)).toBe(stableSourceRecipeId(source));
79− expect(stableSourceRecipeRevisionId(renamed)).toBe(stableSourceRecipeRevisionId(source));
80− expect(stableSourceRecipeId(differentWorkspace)).toBe(stableSourceRecipeId(source));
81− expect(stableSourceRecipeRevisionId(differentWorkspace)).toBe(
82− stableSourceRecipeRevisionId(source),
76+ expect(stableSourceCatalogId(source)).toBe(stableSourceCatalogId(source));
77+ expect(stableSourceCatalogRevisionId(source)).toBe(stableSourceCatalogRevisionId(source));
78+ expect(stableSourceCatalogId(renamed)).toBe(stableSourceCatalogId(source));
79+ expect(stableSourceCatalogRevisionId(renamed)).toBe(stableSourceCatalogRevisionId(source));
80+ expect(stableSourceCatalogId(differentWorkspace)).toBe(stableSourceCatalogId(source));
81+ expect(stableSourceCatalogRevisionId(differentWorkspace)).toBe(
82+ stableSourceCatalogRevisionId(source),
8383 );
8484 });
8585
86− it("changes recipe and revision ids when the source config changes", () => {
86+ it("changes catalog and revision ids when the source config changes", () => {
8787 const source = makeSource();
8888 const changedEndpoint = makeSource({
8989 endpoint: "https://example.com",
9090 binding: openApiBinding("https://example.com/openapi.json"),
9191 });
9292
93− expect(stableSourceRecipeId(changedEndpoint)).not.toBe(stableSourceRecipeId(source));
94− expect(stableSourceRecipeRevisionId(changedEndpoint)).not.toBe(
95− stableSourceRecipeRevisionId(source),
93+ expect(stableSourceCatalogId(changedEndpoint)).not.toBe(stableSourceCatalogId(source));
94+ expect(stableSourceCatalogRevisionId(changedEndpoint)).not.toBe(
95+ stableSourceCatalogRevisionId(source),
9696 );
9797 });
9898 });
@@ -358,14 +358,14 @@ describe("source-definitions", () => { · 259 unmodified lines
358358 },
359359 },
360360 });
361− const recipeId = stableSourceRecipeId(source);
362− const recipeRevisionId = stableSourceRecipeRevisionId(source);
361+ const catalogId = stableSourceCatalogId(source);
362+ const catalogRevisionId = stableSourceCatalogRevisionId(source);
363363 const existingCredentialId = CredentialIdSchema.make("cred_existing");
364364
365365 const { sourceRecord, runtimeAuthArtifact } = splitSourceForStorage({
366366 source,
367− recipeId,
368− recipeRevisionId,
367+ catalogId,
368+ catalogRevisionId,
369369 existingRuntimeAuthArtifactId: existingCredentialId,
370370 });
371371
@@ -426,8 +426,8 @@ describe("source-definitions", () => { · 54 unmodified lines
426426 for (const source of [withRefresh, withoutRefresh]) {
427427 const { sourceRecord, runtimeAuthArtifact } = splitSourceForStorage({
428428 source,
429− recipeId: stableSourceRecipeId(source),
430− recipeRevisionId: stableSourceRecipeRevisionId(source),
429+ catalogId: stableSourceCatalogId(source),
430+ catalogRevisionId: stableSourceCatalogRevisionId(source),
431431 });
432432 const projected = await Effect.runPromise(projectSourceFromStorage({
433433 sourceRecord,
@@ -445,8 +445,8 @@ describe("source-definitions", () => { · 11 unmodified lines
445445 });
446446 const { sourceRecord, runtimeAuthArtifact } = splitSourceForStorage({
447447 source,
448− recipeId: stableSourceRecipeId(source),
449− recipeRevisionId: stableSourceRecipeRevisionId(source),
448+ catalogId: stableSourceCatalogId(source),
449+ catalogRevisionId: stableSourceCatalogRevisionId(source),
450450 });
451451
452452 expect(runtimeAuthArtifact).toBeNull();
packages/control-plane/src/runtime/source-definitions.ts+47−47
@@ -11,19 +11,19 @@ import type { · 10 unmodified lines
1111 Source,
1212 SourceAuth,
1313 SourceImportAuthPolicy,
14− SourceRecipeAdapterKey,
15− SourceRecipeId,
16− SourceRecipeKind,
17− SourceRecipeRevisionId,
14+ SourceCatalogAdapterKey,
15+ SourceCatalogId,
16+ SourceCatalogKind,
17+ SourceCatalogRevisionId,
1818 StoredSourceRecord,
19− StoredSourceRecipeRecord,
20− StoredSourceRecipeRevisionRecord,
19+ StoredSourceCatalogRecord,
20+ StoredSourceCatalogRevisionRecord,
2121 WorkspaceId,
2222 } from "#schema";
2323 import {
2424 AuthArtifactIdSchema,
25− SourceRecipeIdSchema,
26− SourceRecipeRevisionIdSchema,
25+ SourceCatalogIdSchema,
26+ SourceCatalogRevisionIdSchema,
2727 } from "#schema";
2828 import * as Effect from "effect/Effect";
2929
@@ -42,45 +42,45 @@ const trimOrNull = (value: string | null | undefined): string | null => { · 12 unmodified lines
4242 return trimmed.length > 0 ? trimmed : null;
4343 };
4444
45−type SourceRecipeSourceConfig = Record<string, unknown>;
45+type SourceCatalogSourceConfig = Record<string, unknown>;
4646
47−const sourceConfigFromSource = (source: Source): SourceRecipeSourceConfig =>
47+const sourceConfigFromSource = (source: Source): SourceCatalogSourceConfig =>
4848 getSourceAdapterForSource(source).sourceConfigFromSource(source);
4949
50−const sourceRecipeKindFromSource = (source: Source): SourceRecipeKind => {
50+const sourceCatalogKindFromSource = (source: Source): SourceCatalogKind => {
5151 const adapter = getSourceAdapterForSource(source);
5252 return adapter.family;
5353 };
5454
55−const sourceRecipeAdapterKeyFromSource = (source: Source): SourceRecipeAdapterKey => {
55+const sourceCatalogAdapterKeyFromSource = (source: Source): SourceCatalogAdapterKey => {
5656 return getSourceAdapterForSource(source).key;
5757 };
5858
59−const sourceRecipeProviderKeyFromSource = (source: Source): string => {
59+const sourceCatalogProviderKeyFromSource = (source: Source): string => {
6060 return getSourceAdapterForSource(source).providerKey;
6161 };
6262
6363 const stableHash = (value: string): string =>
6464 sha256Hex(value).slice(0, 24);
6565
66−const sourceRecipeSignature = (source: Source): string =>
66+const sourceCatalogSignature = (source: Source): string =>
6767 JSON.stringify({
68− recipeKind: sourceRecipeKindFromSource(source),
69− adapterKey: sourceRecipeAdapterKeyFromSource(source),
70− providerKey: sourceRecipeProviderKeyFromSource(source),
68+ catalogKind: sourceCatalogKindFromSource(source),
69+ adapterKey: sourceCatalogAdapterKeyFromSource(source),
70+ providerKey: sourceCatalogProviderKeyFromSource(source),
7171 sourceConfig: sourceConfigFromSource(source),
7272 });
7373
7474 export const sourceConfigSignature = (source: Source): string =>
7575 JSON.stringify(sourceConfigFromSource(source));
7676
77−export const stableSourceRecipeId = (source: Source): SourceRecipeId =>
78− SourceRecipeIdSchema.make(`src_recipe_${stableHash(sourceRecipeSignature(source))}`);
77+export const stableSourceCatalogId = (source: Source): SourceCatalogId =>
78+ SourceCatalogIdSchema.make(`src_catalog_${stableHash(sourceCatalogSignature(source))}`);
7979
80−export const stableSourceRecipeRevisionId = (
80+export const stableSourceCatalogRevisionId = (
8181 source: Source,
82−): SourceRecipeRevisionId =>
83− SourceRecipeRevisionIdSchema.make(`src_recipe_rev_${stableHash(sourceConfigSignature(source))}`);
82+): SourceCatalogRevisionId =>
83+ SourceCatalogRevisionIdSchema.make(`src_catalog_rev_${stableHash(sourceConfigSignature(source))}`);
8484
8585 const normalizeAuth = (
8686 auth: SourceAuth | undefined,
@@ -298,15 +298,15 @@ export const updateSourceFromPayload = (input: { · 211 unmodified lines
298298 });
299299 });
300300
301−export const createSourceRecipeRecord = (input: {
301+export const createSourceCatalogRecord = (input: {
302302 source: Source;
303− recipeId?: SourceRecipeId | null;
304− latestRevisionId: SourceRecipeRevisionId;
305−}): StoredSourceRecipeRecord => ({
306− id: input.recipeId ?? stableSourceRecipeId(input.source),
307− kind: sourceRecipeKindFromSource(input.source),
308− adapterKey: sourceRecipeAdapterKeyFromSource(input.source),
309− providerKey: sourceRecipeProviderKeyFromSource(input.source),
303+ catalogId?: SourceCatalogId | null;
304+ latestRevisionId: SourceCatalogRevisionId;
305+}): StoredSourceCatalogRecord => ({
306+ id: input.catalogId ?? stableSourceCatalogId(input.source),
307+ kind: sourceCatalogKindFromSource(input.source),
308+ adapterKey: sourceCatalogAdapterKeyFromSource(input.source),
309+ providerKey: sourceCatalogProviderKeyFromSource(input.source),
310310 name: input.source.name,
311311 summary: null,
312312 visibility: "workspace",
@@ -315,32 +315,32 @@ export const createSourceRecipeRecord = (input: { · 2 unmodified lines
315315 updatedAt: input.source.updatedAt,
316316 });
317317
318−export const createSourceRecipeRevisionRecord = (input: {
318+export const createSourceCatalogRevisionRecord = (input: {
319319 source: Source;
320− recipeId: SourceRecipeId;
321− recipeRevisionId?: SourceRecipeRevisionId | null;
320+ catalogId: SourceCatalogId;
321+ catalogRevisionId?: SourceCatalogRevisionId | null;
322322 revisionNumber: number;
323− manifestJson?: string | null;
324− manifestHash?: string | null;
325− materializationHash?: string | null;
326−}): StoredSourceRecipeRevisionRecord => ({
323+ importMetadataJson?: string | null;
324+ importMetadataHash?: string | null;
325+ snapshotHash?: string | null;
326+}): StoredSourceCatalogRevisionRecord => ({
327327 id:
328− input.recipeRevisionId
329− ?? stableSourceRecipeRevisionId(input.source),
330− recipeId: input.recipeId,
328+ input.catalogRevisionId
329+ ?? stableSourceCatalogRevisionId(input.source),
330+ catalogId: input.catalogId,
331331 revisionNumber: input.revisionNumber,
332332 sourceConfigJson: sourceConfigSignature(input.source),
333− manifestJson: input.manifestJson ?? null,
334− manifestHash: input.manifestHash ?? null,
335− materializationHash: input.materializationHash ?? null,
333+ importMetadataJson: input.importMetadataJson ?? null,
334+ importMetadataHash: input.importMetadataHash ?? null,
335+ snapshotHash: input.snapshotHash ?? null,
336336 createdAt: input.source.createdAt,
337337 updatedAt: input.source.updatedAt,
338338 });
339339
340340 export const splitSourceForStorage = (input: {
341341 source: Source;
342− recipeId: SourceRecipeId;
343− recipeRevisionId: SourceRecipeRevisionId;
342+ catalogId: SourceCatalogId;
343+ catalogRevisionId: SourceCatalogRevisionId;
344344 actorAccountId?: AccountId | null;
345345 existingRuntimeAuthArtifactId?: AuthArtifact["id"] | null;
346346 existingImportAuthArtifactId?: AuthArtifact["id"] | null;
@@ -352,8 +352,8 @@ export const splitSourceForStorage = (input: { · 5 unmodified lines
352352 const sourceRecord: StoredSourceRecord = {
353353 id: input.source.id,
354354 workspaceId: input.source.workspaceId,
355− recipeId: input.recipeId,
356− recipeRevisionId: input.recipeRevisionId,
355+ catalogId: input.catalogId,
356+ catalogRevisionId: input.catalogRevisionId,
357357 name: input.source.name,
358358 kind: input.source.kind,
359359 endpoint: input.source.endpoint,
packages/control-plane/src/runtime/source-discovery.test.ts+1−1
@@ -4,7 +4,7 @@ import { · 3 unmodified lines
44 type ServerResponse,
55 } from "node:http";
66
7−import { describe, expect, it } from "vitest";
7+import { describe, expect, it } from "@effect/vitest";
88 import * as Effect from "effect/Effect";
99
1010 import { discoverSource } from "./source-discovery";
packages/control-plane/src/runtime/source-inspection.ts+133−248
@@ -1,37 +1,27 @@
11 import type {
2− Source,
32 SourceId,
43 SourceInspection,
54 SourceInspectionDiscoverPayload,
65 SourceInspectionDiscoverResult,
76 SourceInspectionDiscoverResultItem,
8− SourceInspectionSchemaBundle,
9− SourceInspectionToolListItem,
107 SourceInspectionToolDetail,
8+ SourceInspectionToolListItem,
119 SourceInspectionToolSummary,
12− StoredSourceRecipeOperationRecord,
1310 WorkspaceId,
1411 } from "#schema";
15−import { SourceRecipeSchemaBundleIdSchema } from "#schema";
1612 import {
1713 ControlPlaneNotFoundError,
1814 ControlPlaneStorageError,
1915 } from "../api/errors";
2016 import * as Effect from "effect/Effect";
21−import * as Option from "effect/Option";
2217
2318 import { operationErrors } from "./operation-errors";
2419 import { formatWithPrettier } from "./prettier-format";
2520 import {
26− getSourceAdapterForOperation,
27−} from "./source-adapters";
28−import {
29− RuntimeSourceRecipeStoreService,
30− recipeToolMetadata,
31− recipeToolPath,
32−} from "./source-recipes-runtime";
33−import type { SourceAdapterPersistedOperationMetadata } from "./source-adapters/types";
34−import { namespaceFromSourceName } from "./source-names";
21+ expandCatalogTools,
22+ loadSourceWithCatalog,
23+ type LoadedSourceCatalogTool,
24+} from "./source-catalog-runtime";
3525
3626 const sourceInspectOps = {
3727 bundle: operationErrors("sources.inspect.bundle"),
@@ -46,21 +36,6 @@ const tokenize = (value: string): Array<string> => · 8 unmodified lines
4636 .split(/[^a-z0-9]+/)
4737 .filter(Boolean);
4838
49−type InspectionToolRecord = {
50− operation: StoredSourceRecipeOperationRecord;
51− metadata: SourceAdapterPersistedOperationMetadata;
52− listItem: SourceInspectionToolListItem;
53− searchText: string;
54−};
55−
56−type ResolvedSourceInspection = {
57− source: Source;
58− namespace: string;
59− pipelineKind: SourceInspection["pipelineKind"];
60− schemaBundleId: string | null;
61− tools: ReadonlyArray<InspectionToolRecord>;
62−};
63−
6439 const formatOptionalJson = (value: string | null) =>
6540 value === null
6641 ? Effect.succeed<string | null>(null)
@@ -71,198 +46,163 @@ const formatOptionalTypeScript = (value: string | undefined) => · 4 unmodified lines
7146 ? Effect.succeed<string | undefined>(undefined)
7247 : Effect.promise(() => formatWithPrettier(value, "typescript"));
7348
74−const formatToolSummary = (summary: SourceInspectionToolSummary) =>
75− Effect.all({
76− inputType: formatOptionalTypeScript(summary.inputType),
77− outputType: formatOptionalTypeScript(summary.outputType),
78− }).pipe(
79− Effect.map(({ inputType, outputType }) => ({
80− ...summary,
81− ...(inputType ? { inputType } : {}),
82− ...(outputType ? { outputType } : {}),
83− } satisfies SourceInspectionToolSummary)),
84− );
85−
8649 const formatInspectionToolDetail = (detail: SourceInspectionToolDetail) =>
8750 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+ );
8961 const detailFields = yield* Effect.all({
9062 definitionJson: formatOptionalJson(detail.definitionJson),
9163 documentationJson: formatOptionalJson(detail.documentationJson),
92− providerDataJson: formatOptionalJson(detail.providerDataJson),
93− inputSchemaJson: formatOptionalJson(detail.inputSchemaJson),
94− outputSchemaJson: formatOptionalJson(detail.outputSchemaJson),
95− exampleInputJson: formatOptionalJson(detail.exampleInputJson),
96− exampleOutputJson: formatOptionalJson(detail.exampleOutputJson),
64+ nativeJson: formatOptionalJson(detail.nativeJson),
65+ callSchemaJson: formatOptionalJson(detail.callSchemaJson),
66+ resultSchemaJson: formatOptionalJson(detail.resultSchemaJson),
67+ exampleCallJson: formatOptionalJson(detail.exampleCallJson),
68+ exampleResultJson: formatOptionalJson(detail.exampleResultJson),
9769 });
9870
9971 return {
10072 ...detail,
101− ...detailFields,
10273 summary,
74+ ...detailFields,
10375 } satisfies SourceInspectionToolDetail;
10476 });
10577
106−const loadSourceRecipeRecord = (input: {
107− workspaceId: WorkspaceId;
108− sourceId: SourceId;
109−}) =>
110− Effect.flatMap(RuntimeSourceRecipeStoreService, (sourceRecipeStore) =>
111− Effect.gen(function* () {
112− const recipe = yield* sourceRecipeStore.loadSourceWithRecipe({
113− workspaceId: input.workspaceId,
114− sourceId: input.sourceId,
115− }).pipe(
116− Effect.mapError((cause) =>
117− cause instanceof Error && cause.message.startsWith("Source not found:")
118− ? sourceInspectOps.bundle.notFound(
119− "Source not found",
120− `workspaceId=${input.workspaceId} sourceId=${input.sourceId}`,
121− )
122− : sourceInspectOps.bundle.unknownStorage(
123− cause,
124− "Failed loading source recipe",
125− ),
126− ),
127− );
128−
78+const executableDetails = (tool: LoadedSourceCatalogTool) => {
79+ switch (tool.executable.protocol) {
80+ case "http":
12981 return {
130− source: recipe.source,
131− operations: recipe.operations,
132− schemaBundleId: recipe.schemaBundles.find((bundle) => bundle.bundleKind === "json_schema_ref_map")?.id
133− ?? recipe.schemaBundles[0]?.id
134− ?? null,
82+ method: tool.executable.method,
83+ pathTemplate: tool.executable.pathTemplate,
84+ rawToolId: tool.path.split(".").at(-1) ?? null,
85+ operationId: null,
86+ group: null,
87+ leaf: tool.path.split(".").at(-1) ?? null,
88+ tags: tool.capability.surface.tags ?? [],
13589 };
136− }),
137− );
90+ case "graphql":
91+ return {
92+ method: tool.executable.operationType,
93+ pathTemplate: tool.executable.rootField,
94+ rawToolId: tool.path.split(".").at(-1) ?? null,
95+ operationId: tool.executable.rootField,
96+ group: null,
97+ leaf: tool.executable.rootField,
98+ tags: tool.capability.surface.tags ?? [],
99+ };
100+ case "mcp":
101+ return {
102+ method: null,
103+ pathTemplate: null,
104+ rawToolId: tool.path.split(".").at(-1) ?? null,
105+ operationId: tool.executable.toolName,
106+ group: null,
107+ leaf: tool.executable.toolName,
108+ tags: tool.capability.surface.tags ?? [],
109+ };
110+ }
111+};
138112
139−const inspectionToolListItemFromOperation = (input: {
140− source: Source;
141− operation: StoredSourceRecipeOperationRecord;
142− metadata: SourceAdapterPersistedOperationMetadata;
143−}): SourceInspectionToolListItem => ({
144− path: recipeToolPath({
145− source: input.source,
146− operation: input.operation,
147− }),
148− method: input.metadata.method,
113+const inspectionToolListItemFromTool = (tool: LoadedSourceCatalogTool): SourceInspectionToolListItem => ({
114+ path: tool.path,
115+ method:
116+ tool.executable.protocol === "http"
117+ ? tool.executable.method
118+ : tool.executable.protocol === "graphql"
119+ ? tool.executable.operationType
120+ : null,
149121 });
150122
151−const persistedToolSummaryFromRecipeOperation = (input: {
152− source: Source;
153− operation: StoredSourceRecipeOperationRecord;
154− metadata: SourceAdapterPersistedOperationMetadata;
155− includeTypes: boolean;
156−}): SourceInspectionToolSummary => {
157− const path = inspectionToolListItemFromOperation(input).path;
158− const descriptor = input.includeTypes
159− ? getSourceAdapterForOperation(input.operation).createToolDescriptor({
160− source: input.source,
161− operation: input.operation,
162− path,
163− includeSchemas: false,
164− })
165− : null;
123+const persistedToolSummaryFromTool = (tool: LoadedSourceCatalogTool): SourceInspectionToolSummary => {
124+ const details = executableDetails(tool);
166125
167126 return {
168− path,
169− sourceKey: input.source.id,
170− ...(input.operation.title ? { title: input.operation.title } : {}),
171− ...(input.operation.description ? { description: input.operation.description } : {}),
172− providerKind: input.operation.providerKind,
173− toolId: input.operation.toolId,
174− rawToolId: input.metadata.rawToolId,
175− operationId: input.metadata.operationId,
176− group: input.metadata.group,
177− leaf: input.metadata.leaf,
178− tags: [...input.metadata.tags],
179− method: input.metadata.method,
180− pathTemplate: input.metadata.pathTemplate,
181− ...(descriptor?.inputType ? { inputType: descriptor.inputType } : {}),
182− ...(descriptor?.outputType ? { outputType: descriptor.outputType } : {}),
127+ path: tool.path,
128+ sourceKey: tool.source.id,
129+ ...(tool.capability.surface.title ? { title: tool.capability.surface.title } : {}),
130+ ...(tool.capability.surface.summary || tool.capability.surface.description
131+ ? { description: tool.capability.surface.summary ?? tool.capability.surface.description! }
132+ : {}),
133+ protocol: tool.executable.protocol,
134+ toolId: tool.path.split(".").at(-1) ?? tool.path,
135+ rawToolId: details.rawToolId,
136+ operationId: details.operationId,
137+ group: details.group,
138+ leaf: details.leaf,
139+ tags: [...details.tags],
140+ method: details.method,
141+ pathTemplate: details.pathTemplate,
142+ ...(tool.descriptor.inputType ? { inputType: tool.descriptor.inputType } : {}),
143+ ...(tool.descriptor.outputType ? { outputType: tool.descriptor.outputType } : {}),
183144 };
184145 };
185146
186−const inspectionToolDetailFromOperation = (input: {
187− source: Source;
188− operation: StoredSourceRecipeOperationRecord;
189− metadata: SourceAdapterPersistedOperationMetadata;
190− schemaBundleId: string | null;
191−}): SourceInspectionToolDetail => {
192− const summary = persistedToolSummaryFromRecipeOperation({
193− source: input.source,
194− operation: input.operation,
195− metadata: input.metadata,
196− includeTypes: true,
197− });
198−
199− return {
200− summary,
201− definitionJson: null,
202− documentationJson: null,
203− providerDataJson: input.operation.providerDataJson,
204− inputSchemaJson: input.operation.inputSchemaJson,
205− outputSchemaJson: input.operation.outputSchemaJson,
206− schemaBundleId: input.schemaBundleId,
207− exampleInputJson: null,
208− exampleOutputJson: null,
209− } satisfies SourceInspectionToolDetail;
210−};
147+const inspectionToolDetailFromTool = (tool: LoadedSourceCatalogTool): SourceInspectionToolDetail => ({
148+ summary: persistedToolSummaryFromTool(tool),
149+ definitionJson: JSON.stringify({
150+ capability: tool.capability,
151+ executable: tool.executable,
152+ }),
153+ documentationJson: JSON.stringify({
154+ capabilityDocs: {
155+ summary: tool.capability.surface.summary,
156+ description: tool.capability.surface.description,
157+ },
158+ }),
159+ nativeJson: tool.executable.native?.[0]?.value
160+ ? JSON.stringify(tool.executable.native[0]!.value)
161+ : null,
162+ callSchemaJson: tool.descriptor.inputSchema
163+ ? JSON.stringify(tool.descriptor.inputSchema)
164+ : null,
165+ resultSchemaJson: tool.descriptor.outputSchema
166+ ? JSON.stringify(tool.descriptor.outputSchema)
167+ : null,
168+ exampleCallJson: null,
169+ exampleResultJson: null,
170+});
211171
212172 const resolveSourceInspection = (input: {
213173 workspaceId: WorkspaceId;
214174 sourceId: SourceId;
215175 }) =>
216176 Effect.gen(function* () {
217− const { source, operations, schemaBundleId } = yield* loadSourceRecipeRecord(input);
218− const namespace = source.namespace ?? namespaceFromSourceName(source.name);
177+ const catalogEntry = yield* loadSourceWithCatalog({
178+ workspaceId: input.workspaceId,
179+ sourceId: input.sourceId,
180+ });
181+ const tools = yield* expandCatalogTools({
182+ catalogs: [catalogEntry],
183+ includeSchemas: true,
184+ });
219185
220186 return {
221− source,
222− namespace,
223− pipelineKind: "persisted",
224− schemaBundleId,
225− tools: yield* Effect.forEach(operations, (operation) =>
226− Effect.gen(function* () {
227− const path = recipeToolPath({
228− source,
229− operation,
230− });
231− const metadata = yield* recipeToolMetadata({
232− source,
233− operation,
234− path,
235− });
236− const listItem = inspectionToolListItemFromOperation({
237− source,
238− operation,
239− metadata,
240− });
241−
242− return {
243− operation,
244− metadata,
245− listItem,
246− searchText: metadata.searchText,
247− } satisfies InspectionToolRecord;
248− })
249− ),
250− } satisfies ResolvedSourceInspection;
187+ source: catalogEntry.source,
188+ namespace: catalogEntry.source.namespace ?? "",
189+ pipelineKind: "ir" as const,
190+ tools,
191+ };
251192 });
252193
253194 const scoreTool = (input: {
254195 queryTokens: ReadonlyArray<string>;
255− tool: InspectionToolRecord;
196+ tool: LoadedSourceCatalogTool;
256197 }): SourceInspectionDiscoverResultItem | null => {
257198 let score = 0;
258199 const reasons: Array<string> = [];
259− const pathTokens = tokenize(input.tool.listItem.path);
260− const titleTokens = tokenize(input.tool.operation.title ?? "");
261− const descriptionTokens = tokenize(input.tool.operation.description ?? "");
262− const tagTokens = input.tool.metadata.tags.flatMap(tokenize);
263− const methodPathTokens = tokenize(
264− `${input.tool.listItem.method ?? ""} ${input.tool.metadata.pathTemplate ?? ""}`,
265− );
200+ const summary = persistedToolSummaryFromTool(input.tool);
201+ const pathTokens = tokenize(summary.path);
202+ const titleTokens = tokenize(summary.title ?? "");
203+ const descriptionTokens = tokenize(summary.description ?? "");
204+ const tagTokens = summary.tags.flatMap(tokenize);
205+ const methodPathTokens = tokenize(`${summary.method ?? ""} ${summary.pathTemplate ?? ""}`);
266206
267207 for (const token of input.queryTokens) {
268208 if (pathTokens.includes(token)) {
@@ -296,13 +236,13 @@ const scoreTool = (input: { · 27 unmodified lines
296236 }
297237
298238 return {
299− path: input.tool.listItem.path,
239+ path: input.tool.path,
300240 score,
301− ...(input.tool.operation.description
302− ? { description: input.tool.operation.description }
303− : {}),
241+ ...(summary.description ? { description: summary.description } : {}),
242+ ...(summary.inputType ? { inputType: summary.inputType } : {}),
243+ ...(summary.outputType ? { outputType: summary.outputType } : {}),
304244 reasons,
305− } satisfies SourceInspectionDiscoverResultItem;
245+ };
306246 };
307247
308248 const mapInspectionError = (
@@ -316,6 +256,7 @@ const mapInspectionError = ( · 7 unmodified lines
316256 if (cause instanceof ControlPlaneNotFoundError) {
317257 return cause;
318258 }
259+
319260 if (cause instanceof ControlPlaneStorageError) {
320261 return cause;
321262 }
@@ -335,7 +276,7 @@ export const getSourceInspection = (input: { · 13 unmodified lines
335276 namespace: inspection.namespace,
336277 pipelineKind: inspection.pipelineKind,
337278 toolCount: inspection.tools.length,
338− tools: inspection.tools.map((tool) => tool.listItem),
279+ tools: inspection.tools.map((tool) => inspectionToolListItemFromTool(tool)),
339280 } satisfies SourceInspection;
340281 }).pipe(
341282 Effect.mapError((cause) =>
@@ -356,7 +297,7 @@ export const getSourceInspectionToolDetail = (input: { · 14 unmodified lines
356297 workspaceId: input.workspaceId,
357298 sourceId: input.sourceId,
358299 });
359− const tool = inspection.tools.find((candidate) => candidate.listItem.path === input.toolPath);
300+ const tool = inspection.tools.find((candidate) => candidate.path === input.toolPath);
360301
361302 if (!tool) {
362303 return yield* Effect.fail(
@@ -367,12 +308,7 @@ export const getSourceInspectionToolDetail = (input: { · 4 unmodified lines
367308 );
368309 }
369310
370− return yield* formatInspectionToolDetail(inspectionToolDetailFromOperation({
371− source: inspection.source,
372− operation: tool.operation,
373− metadata: tool.metadata,
374− schemaBundleId: inspection.schemaBundleId,
375− }));
311+ return yield* formatInspectionToolDetail(inspectionToolDetailFromTool(tool));
376312 }).pipe(
377313 Effect.mapError((cause) =>
378314 mapInspectionError(
@@ -382,57 +318,6 @@ export const getSourceInspectionToolDetail = (input: { · 3 unmodified lines
382318 )),
383319 );
384320
385−export const getSourceInspectionSchemaBundle = (input: {
386− workspaceId: WorkspaceId;
387− sourceId: SourceId;
388− schemaBundleId: string;
389−}) =>
390− Effect.flatMap(RuntimeSourceRecipeStoreService, (sourceRecipeStore) =>
391− Effect.gen(function* () {
392− const recipe = yield* sourceRecipeStore.loadSourceWithRecipe({
393− workspaceId: input.workspaceId,
394− sourceId: input.sourceId,
395− }).pipe(
396− Effect.mapError((cause) =>
397− cause instanceof Error && cause.message.startsWith("Source not found:")
398− ? sourceInspectOps.tool.notFound(
399− "Source not found",
400− `workspaceId=${input.workspaceId} sourceId=${input.sourceId}`,
401− )
402− : sourceInspectOps.tool.unknownStorage(
403− cause,
404− "Failed loading source recipe",
405− ),
406− ),
407− );
408− const schemaBundle = recipe.schemaBundles.find(
409− (candidate) => candidate.id === SourceRecipeSchemaBundleIdSchema.make(input.schemaBundleId),
410− );
411− if (!schemaBundle) {
412− return yield* Effect.fail(
413− sourceInspectOps.tool.notFound(
414− "Schema bundle not found",
415− `workspaceId=${input.workspaceId} sourceId=${input.sourceId} schemaBundleId=${input.schemaBundleId}`,
416− ),
417− );
418− }
419−
420− return {
421− id: schemaBundle.id,
422− kind: schemaBundle.bundleKind,
423− hash: schemaBundle.contentHash,
424− refsJson: schemaBundle.refsJson,
425− } satisfies SourceInspectionSchemaBundle;
426− }),
427− ).pipe(
428− Effect.mapError((cause) =>
429− mapInspectionError(
430− sourceInspectOps.tool,
431− cause,
432− "Failed loading source inspection schema bundle",
433− )),
434− );
435−
436321 export const discoverSourceInspectionTools = (input: {
437322 workspaceId: WorkspaceId;
438323 sourceId: SourceId;
packages/control-plane/src/runtime/source-recipe-support.ts+0−27
@@ -1,27 +0,0 @@
1−import { sha256Hex } from "@executor/codemode-core";
2−
3−import type {
4− StoredSourceRecipeDocumentRecord,
5− StoredSourceRecipeOperationRecord,
6− StoredSourceRecipeSchemaBundleRecord,
7−} from "#schema";
8−
9−export const normalizeSearchText = (
10− ...parts: ReadonlyArray<string | null | undefined>
11−): string =>
12− parts
13− .flatMap((part) => (part ? [part.trim()] : []))
14− .filter(Boolean)
15− .join(" ")
16− .toLowerCase();
17−
18−export const contentHash = (value: string): string => sha256Hex(value);
19−
20−export type SourceRecipeMaterialization = {
21− manifestJson: string | null;
22− manifestHash: string | null;
23− sourceHash: string | null;
24− documents: readonly StoredSourceRecipeDocumentRecord[];
25− schemaBundles: readonly StoredSourceRecipeSchemaBundleRecord[];
26− operations: readonly StoredSourceRecipeOperationRecord[];
27−};
packages/control-plane/src/runtime/source-recipes-runtime.ts+0−598
@@ -1,598 +0,0 @@
1−import {
2− type ToolCatalogEntry,
3− type ToolDescriptor,
4−} from "@executor/codemode-core";
5−import type {
6− AccountId,
7− Source,
8− StoredSourceRecord,
9− StoredSourceRecipeDocumentRecord,
10− StoredSourceRecipeOperationRecord,
11− StoredSourceRecipeSchemaBundleRecord,
12− StoredSourceRecipeRevisionRecord,
13− WorkspaceId,
14−} from "#schema";
15−import * as Context from "effect/Context";
16−import * as Effect from "effect/Effect";
17−import * as Layer from "effect/Layer";
18−import { LocalSourceArtifactMissingError } from "./local-errors";
19−import {
20− RuntimeLocalWorkspaceService,
21− type RuntimeLocalWorkspaceState,
22−} from "./local-runtime-context";
23−import {
24− SourceArtifactStore,
25− type SourceArtifactStoreShape,
26−} from "./local-storage";
27−import { namespaceFromSourceName } from "./source-names";
28−import {
29− getSourceAdapterForOperation,
30− getSourceAdapterForSource,
31−} from "./source-adapters";
32−import type { SourceAdapterPersistedOperationMetadata } from "./source-adapters/types";
33−import { firstSchemaBundle } from "./source-adapters/shared";
34−import {
35− RuntimeSourceStoreService,
36− type RuntimeSourceStore,
37−} from "./source-store";
38−
39−type RecipeManifest = unknown | null;
40−
41−export type LoadedSourceRecipe = {
42− source: Source;
43− sourceRecord: StoredSourceRecord;
44− revision: StoredSourceRecipeRevisionRecord;
45− documents: readonly StoredSourceRecipeDocumentRecord[];
46− schemaBundles: readonly StoredSourceRecipeSchemaBundleRecord[];
47− operations: readonly StoredSourceRecipeOperationRecord[];
48− manifest: RecipeManifest;
49−};
50−
51−export type LoadedSourceRecipeTool = {
52− path: string;
53− searchNamespace: string;
54− searchText: string;
55− source: Source;
56− sourceRecord: StoredSourceRecord;
57− revision: StoredSourceRecipeRevisionRecord;
58− operation: StoredSourceRecipeOperationRecord;
59− metadata: SourceAdapterPersistedOperationMetadata;
60− schemaBundleId: string | null;
61− manifest: RecipeManifest;
62− descriptor: ToolDescriptor;
63−};
64−
65−export type LoadedSourceRecipeToolIndexEntry = {
66− path: string;
67− searchNamespace: string;
68− searchText: string;
69− source: Source;
70− sourceRecord: StoredSourceRecord;
71− operation: StoredSourceRecipeOperationRecord;
72− metadata: SourceAdapterPersistedOperationMetadata;
73− schemaBundleId: string | null;
74− descriptor: ToolDescriptor;
75−};
76−
77−export const recipeToolCatalogEntry = (input: {
78− tool: LoadedSourceRecipeToolIndexEntry;
79− score: (queryTokens: readonly string[]) => number;
80−}): ToolCatalogEntry => ({
81− descriptor: input.tool.descriptor,
82− namespace: input.tool.searchNamespace,
83− searchText: input.tool.searchText,
84− score: input.score,
85−});
86−
87−const parseManifestForRecipe = (input: {
88− source: Source;
89− revision: StoredSourceRecipeRevisionRecord;
90−}): Effect.Effect<RecipeManifest, Error, never> =>
91− getSourceAdapterForSource(input.source).parseManifest({
92− source: input.source,
93− manifestJson: input.revision.manifestJson,
94− });
95−
96−const catalogNamespaceFromPath = (path: string): string => {
97− const [first, second] = path.split(".");
98− return second ? `${first}.${second}` : first;
99−};
100−
101−export const recipeToolPath = (input: {
102− source: Source;
103− operation: StoredSourceRecipeOperationRecord;
104−}): string => {
105− const namespace = input.source.namespace ?? namespaceFromSourceName(input.source.name);
106− return namespace ? `${namespace}.${input.operation.toolId}` : input.operation.toolId;
107−};
108−
109−export const recipeToolSearchNamespace = (input: {
110− source: Source;
111− path: string;
112− operation: StoredSourceRecipeOperationRecord;
113−}): string =>
114− getSourceAdapterForOperation(input.operation).searchNamespace?.({
115− source: input.source,
116− path: input.path,
117− operation: input.operation,
118− })
119− ?? catalogNamespaceFromPath(input.path);
120−
121−export const recipeToolDescriptor = (input: {
122− source: Source;
123− operation: StoredSourceRecipeOperationRecord;
124− path: string;
125− schemaBundleId?: string | null;
126− includeSchemas: boolean;
127−}): ToolDescriptor =>
128− getSourceAdapterForOperation(input.operation).createToolDescriptor(input);
129−
130−export const recipeToolMetadata = (input: {
131− source: Source;
132− operation: StoredSourceRecipeOperationRecord;
133− path: string;
134−}): Effect.Effect<SourceAdapterPersistedOperationMetadata, Error, never> =>
135− getSourceAdapterForOperation(input.operation).describePersistedOperation(input);
136−
137−const sourceRecipeDocumentForSource = (input: {
138− source: Source;
139− documents: readonly StoredSourceRecipeDocumentRecord[];
140−}): StoredSourceRecipeDocumentRecord | null => {
141− const preferredKind = getSourceAdapterForSource(input.source).primaryDocumentKind;
142−
143− if (preferredKind === null) {
144− return null;
145− }
146−
147− return input.documents.find((document) => document.documentKind === preferredKind) ?? null;
148−};
149−
150−export const recipePrimaryDocumentText = (input: {
151− source: Source;
152− documents: readonly StoredSourceRecipeDocumentRecord[];
153−}): string | null =>
154− sourceRecipeDocumentForSource(input)?.contentText ?? null;
155−
156−const primarySchemaBundleForRevision = (input: {
157− source: Source;
158− schemaBundles: readonly StoredSourceRecipeSchemaBundleRecord[];
159−}): StoredSourceRecipeSchemaBundleRecord | null => {
160− const selected = firstSchemaBundle({
161− schemaBundles: input.schemaBundles.map((schemaBundle) => ({
162− id: schemaBundle.id,
163− kind: schemaBundle.bundleKind,
164− hash: schemaBundle.contentHash,
165− refsJson: schemaBundle.refsJson,
166− })),
167− preferredKind: getSourceAdapterForSource(input.source).primarySchemaBundleKind,
168− });
169−
170− return selected
171− ? input.schemaBundles.find((schemaBundle) => schemaBundle.id === selected.id) ?? null
172− : null;
173−};
174−
175−const sourceRecordFromRecipeArtifact = (input: {
176− source: Source;
177− artifact: {
178− recipeId: StoredSourceRecord["recipeId"];
179− revision: StoredSourceRecipeRevisionRecord;
180− };
181−}): StoredSourceRecord => ({
182− id: input.source.id,
183− workspaceId: input.source.workspaceId,
184− recipeId: input.artifact.recipeId,
185− recipeRevisionId: input.artifact.revision.id,
186− name: input.source.name,
187− kind: input.source.kind,
188− endpoint: input.source.endpoint,
189− status: input.source.status,
190− enabled: input.source.enabled,
191− namespace: input.source.namespace,
192− importAuthPolicy: input.source.importAuthPolicy,
193− bindingConfigJson: getSourceAdapterForSource(input.source).serializeBindingConfig(input.source),
194− sourceHash: input.source.sourceHash,
195− lastError: input.source.lastError,
196− createdAt: input.source.createdAt,
197− updatedAt: input.source.updatedAt,
198−});
199−
200−type RuntimeSourceRecipeStoreShape = {
201− loadWorkspaceSourceRecipes: (input: {
202− workspaceId: WorkspaceId;
203− actorAccountId?: AccountId | null;
204− }) => Effect.Effect<readonly LoadedSourceRecipe[], Error, never>;
205− loadSourceWithRecipe: (input: {
206− workspaceId: WorkspaceId;
207− sourceId: Source["id"];
208− actorAccountId?: AccountId | null;
209− }) => Effect.Effect<LoadedSourceRecipe, Error | LocalSourceArtifactMissingError, never>;
210− loadWorkspaceSourceRecipeToolIndex: (input: {
211− workspaceId: WorkspaceId;
212− actorAccountId?: AccountId | null;
213− includeSchemas: boolean;
214− }) => Effect.Effect<readonly LoadedSourceRecipeToolIndexEntry[], Error, never>;
215− loadWorkspaceSourceRecipeToolByPath: (input: {
216− workspaceId: WorkspaceId;
217− path: string;
218− actorAccountId?: AccountId | null;
219− includeSchemas: boolean;
220− }) => Effect.Effect<LoadedSourceRecipeToolIndexEntry | null, Error, never>;
221− loadWorkspaceSchemaBundle: (input: {
222− workspaceId: WorkspaceId;
223− id: string;
224− }) => Effect.Effect<
225− { id: string; kind: string; hash: string; refsJson: string } | null,
226− Error,
227− never
228− >;
229−};
230−
231−export type RuntimeSourceRecipeStore = RuntimeSourceRecipeStoreShape;
232−
233−export class RuntimeSourceRecipeStoreService extends Context.Tag(
234− "#runtime/RuntimeSourceRecipeStoreService",
235−)<RuntimeSourceRecipeStoreService, RuntimeSourceRecipeStoreShape>() {}
236−
237−type RuntimeSourceRecipeStoreDeps = {
238− runtimeLocalWorkspace: RuntimeLocalWorkspaceState;
239− sourceStore: RuntimeSourceStore;
240− sourceArtifactStore: SourceArtifactStoreShape;
241−};
242−
243−type SourceRecipeRuntimeServices =
244− | RuntimeLocalWorkspaceService
245− | RuntimeSourceStoreService
246− | SourceArtifactStore;
247−
248−const ensureRuntimeRecipeWorkspace = (
249− deps: RuntimeSourceRecipeStoreDeps,
250− workspaceId: WorkspaceId,
251−) => {
252− if (deps.runtimeLocalWorkspace.installation.workspaceId !== workspaceId) {
253− return Effect.fail(
254− new Error(
255− `Runtime local workspace mismatch: expected ${workspaceId}, got ${deps.runtimeLocalWorkspace.installation.workspaceId}`,
256− ),
257− );
258− }
259−
260− return Effect.succeed(deps.runtimeLocalWorkspace.context);
261−};
262−
263−const loadWorkspaceSourceRecipesWithDeps = (deps: RuntimeSourceRecipeStoreDeps, input: {
264− workspaceId: WorkspaceId;
265− actorAccountId?: AccountId | null;
266−}): Effect.Effect<readonly LoadedSourceRecipe[], Error, never> =>
267− Effect.gen(function* () {
268− const workspaceContext = yield* ensureRuntimeRecipeWorkspace(
269− deps,
270− input.workspaceId,
271− );
272− const sources = yield* deps.sourceStore.loadSourcesInWorkspace(
273− input.workspaceId,
274− {
275− actorAccountId: input.actorAccountId,
276− },
277− );
278− const localRecipes = yield* Effect.forEach(sources, (source) =>
279− Effect.gen(function* () {
280− const artifact = yield* deps.sourceArtifactStore.read({
281− context: workspaceContext,
282− sourceId: source.id,
283− });
284− if (artifact === null) {
285− return null;
286− }
287−
288− const manifest = yield* parseManifestForRecipe({
289− source,
290− revision: artifact.revision,
291− });
292−
293− return {
294− source,
295− sourceRecord: sourceRecordFromRecipeArtifact({ source, artifact }),
296− revision: artifact.revision,
297− documents: artifact.documents,
298− schemaBundles: artifact.schemaBundles,
299− operations: artifact.operations,
300− manifest,
301− } satisfies LoadedSourceRecipe;
302− }),
303− );
304− return localRecipes.filter((recipe): recipe is LoadedSourceRecipe => recipe !== null);
305− });
306−
307−const loadSourceWithRecipeWithDeps = (deps: RuntimeSourceRecipeStoreDeps, input: {
308− workspaceId: WorkspaceId;
309− sourceId: Source["id"];
310− actorAccountId?: AccountId | null;
311−}): Effect.Effect<LoadedSourceRecipe, Error | LocalSourceArtifactMissingError, never> =>
312− Effect.gen(function* () {
313− const workspaceContext = yield* ensureRuntimeRecipeWorkspace(
314− deps,
315− input.workspaceId,
316− );
317− const source = yield* deps.sourceStore.loadSourceById({
318− workspaceId: input.workspaceId,
319− sourceId: input.sourceId,
320− actorAccountId: input.actorAccountId,
321− });
322− const artifact = yield* deps.sourceArtifactStore.read({
323− context: workspaceContext,
324− sourceId: source.id,
325− });
326− if (artifact === null) {
327− return yield* Effect.fail(
328− new LocalSourceArtifactMissingError({
329− message: `Recipe artifact missing for source ${input.sourceId}`,
330− sourceId: input.sourceId,
331− }),
332− );
333− }
334−
335− const manifest = yield* parseManifestForRecipe({
336− source,
337− revision: artifact.revision,
338− });
339−
340− return {
341− source,
342− sourceRecord: sourceRecordFromRecipeArtifact({ source, artifact }),
343− revision: artifact.revision,
344− documents: artifact.documents,
345− schemaBundles: artifact.schemaBundles,
346− operations: artifact.operations,
347− manifest,
348− } satisfies LoadedSourceRecipe;
349− });
350−
351−export const loadWorkspaceSourceRecipes = (input: {
352− workspaceId: WorkspaceId;
353− actorAccountId?: AccountId | null;
354−}): Effect.Effect<readonly LoadedSourceRecipe[], Error, SourceRecipeRuntimeServices> =>
355− Effect.gen(function* () {
356− const runtimeLocalWorkspace = yield* RuntimeLocalWorkspaceService;
357− const sourceStore = yield* RuntimeSourceStoreService;
358− const sourceArtifactStore = yield* SourceArtifactStore;
359−
360− return yield* loadWorkspaceSourceRecipesWithDeps(
361− {
362− runtimeLocalWorkspace,
363− sourceStore,
364− sourceArtifactStore,
365− },
366− input,
367− );
368− });
369−
370−export const loadSourceWithRecipe = (input: {
371− workspaceId: WorkspaceId;
372− sourceId: Source["id"];
373− actorAccountId?: AccountId | null;
374−}): Effect.Effect<
375− LoadedSourceRecipe,
376− Error | LocalSourceArtifactMissingError,
377− SourceRecipeRuntimeServices
378−> =>
379− Effect.gen(function* () {
380− const runtimeLocalWorkspace = yield* RuntimeLocalWorkspaceService;
381− const sourceStore = yield* RuntimeSourceStoreService;
382− const sourceArtifactStore = yield* SourceArtifactStore;
383−
384− return yield* loadSourceWithRecipeWithDeps(
385− {
386− runtimeLocalWorkspace,
387− sourceStore,
388− sourceArtifactStore,
389− },
390− input,
391− );
392− });
393−
394−export const expandRecipeTools = (input: {
395− recipes: readonly LoadedSourceRecipe[];
396− includeSchemas: boolean;
397−}): Effect.Effect<readonly LoadedSourceRecipeTool[], Error, never> =>
398− Effect.map(
399− Effect.forEach(input.recipes, (recipe) =>
400− Effect.forEach(recipe.operations, (operation) =>
401− Effect.gen(function* () {
402− const path = recipeToolPath({
403− source: recipe.source,
404− operation,
405− });
406− const searchNamespace = recipeToolSearchNamespace({
407− source: recipe.source,
408− path,
409− operation,
410− });
411− const schemaBundleId = primarySchemaBundleForRevision({
412− source: recipe.source,
413− schemaBundles: recipe.schemaBundles,
414− })?.id ?? null;
415− const metadata = yield* recipeToolMetadata({
416− source: recipe.source,
417− operation,
418− path,
419− });
420−
421− return {
422− path,
423− searchNamespace,
424− searchText: [
425− path,
426− searchNamespace,
427− recipe.source.name,
428− metadata.searchText,
429− ]
430− .filter((part) => part.length > 0)
431− .join(" ")
432− .toLowerCase(),
433− source: recipe.source,
434− sourceRecord: recipe.sourceRecord,
435− revision: recipe.revision,
436− operation,
437− metadata,
438− schemaBundleId,
439− manifest: recipe.manifest,
440− descriptor: recipeToolDescriptor({
441− source: recipe.source,
442− operation,
443− path,
444− schemaBundleId,
445− includeSchemas: input.includeSchemas,
446− }),
447− } satisfies LoadedSourceRecipeTool;
448− })
449− ),
450− ),
451− (recipes) => recipes.flat(),
452− );
453−
454−export const loadWorkspaceSourceRecipeToolIndex = (input: {
455− workspaceId: WorkspaceId;
456− actorAccountId?: AccountId | null;
457− includeSchemas: boolean;
458−}): Effect.Effect<
459− readonly LoadedSourceRecipeToolIndexEntry[],
460− Error,
461− SourceRecipeRuntimeServices
462−> =>
463− Effect.gen(function* () {
464− const recipes = yield* loadWorkspaceSourceRecipes({
465− workspaceId: input.workspaceId,
466− actorAccountId: input.actorAccountId,
467− });
468− const tools = yield* expandRecipeTools({
469− recipes,
470− includeSchemas: input.includeSchemas,
471− });
472− return tools.map((tool) => ({
473− path: tool.path,
474− searchNamespace: tool.searchNamespace,
475− searchText: tool.searchText,
476− source: tool.source,
477− sourceRecord: tool.sourceRecord,
478− operation: tool.operation,
479− metadata: tool.metadata,
480− schemaBundleId: tool.schemaBundleId,
481− descriptor: tool.descriptor,
482− }));
483− });
484−
485−export const loadWorkspaceSourceRecipeToolByPath = (input: {
486− workspaceId: WorkspaceId;
487− path: string;
488− actorAccountId?: AccountId | null;
489− includeSchemas: boolean;
490−}): Effect.Effect<
491− LoadedSourceRecipeToolIndexEntry | null,
492− Error,
493− SourceRecipeRuntimeServices
494−> =>
495− Effect.gen(function* () {
496− const recipes = yield* loadWorkspaceSourceRecipes({
497− workspaceId: input.workspaceId,
498− actorAccountId: input.actorAccountId,
499− });
500− const tools = yield* expandRecipeTools({
501− recipes,
502− includeSchemas: input.includeSchemas,
503− });
504− const tool = tools.find((entry) => entry.path === input.path) ?? null;
505− return tool
506− ? {
507− path: tool.path,
508− searchNamespace: tool.searchNamespace,
509− searchText: tool.searchText,
510− source: tool.source,
511− sourceRecord: tool.sourceRecord,
512− operation: tool.operation,
513− metadata: tool.metadata,
514− schemaBundleId: tool.schemaBundleId,
515− descriptor: tool.descriptor,
516− }
517− : null;
518− });
519−
520−export const RuntimeSourceRecipeStoreLive = Layer.effect(
521− RuntimeSourceRecipeStoreService,
522− Effect.gen(function* () {
523− const runtimeLocalWorkspace = yield* RuntimeLocalWorkspaceService;
524− const sourceStore = yield* RuntimeSourceStoreService;
525− const sourceArtifactStore = yield* SourceArtifactStore;
526−
527− const deps: RuntimeSourceRecipeStoreDeps = {
528− runtimeLocalWorkspace,
529− sourceStore,
530− sourceArtifactStore,
531− };
532−
533− return RuntimeSourceRecipeStoreService.of({
534− loadWorkspaceSourceRecipes: (input) =>
535− loadWorkspaceSourceRecipesWithDeps(deps, input),
536− loadSourceWithRecipe: (input) =>
537− loadSourceWithRecipeWithDeps(deps, input),
538− loadWorkspaceSourceRecipeToolIndex: (input) =>
539− Effect.gen(function* () {
540− const recipes = yield* loadWorkspaceSourceRecipesWithDeps(deps, input);
541− const tools = yield* expandRecipeTools({
542− recipes,
543− includeSchemas: input.includeSchemas,
544− });
545− return tools.map((tool) => ({
546− path: tool.path,
547− searchNamespace: tool.searchNamespace,
548− searchText: tool.searchText,
549− source: tool.source,
550− sourceRecord: tool.sourceRecord,
551− operation: tool.operation,
552− metadata: tool.metadata,
553− schemaBundleId: tool.schemaBundleId,
554− descriptor: tool.descriptor,
555− }));
556− }),
557− loadWorkspaceSourceRecipeToolByPath: (input) =>
558− Effect.gen(function* () {
559− const recipes = yield* loadWorkspaceSourceRecipesWithDeps(deps, input);
560− const tools = yield* expandRecipeTools({
561− recipes,
562− includeSchemas: input.includeSchemas,
563− });
564− const tool = tools.find((entry) => entry.path === input.path) ?? null;
565− return tool
566− ? {
567− path: tool.path,
568− searchNamespace: tool.searchNamespace,
569− searchText: tool.searchText,
570− source: tool.source,
571− sourceRecord: tool.sourceRecord,
572− operation: tool.operation,
573− metadata: tool.metadata,
574− schemaBundleId: tool.schemaBundleId,
575− descriptor: tool.descriptor,
576− }
577− : null;
578− }),
579− loadWorkspaceSchemaBundle: (input) =>
580− Effect.gen(function* () {
581− const recipes = yield* loadWorkspaceSourceRecipesWithDeps(deps, {
582− workspaceId: input.workspaceId,
583− });
584− const localBundle = recipes
585− .flatMap((recipe) => recipe.schemaBundles)
586− .find((schemaBundle) => schemaBundle.id === input.id);
587− return localBundle
588− ? {
589− id: localBundle.id,
590− kind: localBundle.bundleKind,
591− hash: localBundle.contentHash,
592− refsJson: localBundle.refsJson,
593− }
594− : null;
595− }),
596− });
597− }),
598−);
packages/control-plane/src/runtime/source-store.ts+6−6
@@ -14,8 +14,8 @@ import * as Effect from "effect/Effect"; · 13 unmodified lines
1414 import * as Layer from "effect/Layer";
1515
1616 import {
17− stableSourceRecipeId,
18− stableSourceRecipeRevisionId,
17+ stableSourceCatalogId,
18+ stableSourceCatalogRevisionId,
1919 splitSourceForStorage,
2020 } from "./source-definitions";
2121 import {
@@ -407,8 +407,8 @@ const buildLocalSourceRecord = (input: { · 385 unmodified lines
407407 const sourceRecord = {
408408 id: baseSource.id,
409409 workspaceId: baseSource.workspaceId,
410− recipeId: artifact?.recipeId ?? stableSourceRecipeId(baseSource),
411− recipeRevisionId: artifact?.revision.id ?? stableSourceRecipeRevisionId(baseSource),
410+ catalogId: artifact?.catalogId ?? stableSourceCatalogId(baseSource),
411+ catalogRevisionId: artifact?.revision.id ?? stableSourceCatalogRevisionId(baseSource),
412412 name: baseSource.name,
413413 kind: baseSource.kind,
414414 endpoint: baseSource.endpoint,
@@ -855,8 +855,8 @@ const persistSourceWithDeps = ( · 440 unmodified lines
855855
856856 const { runtimeAuthArtifact, importAuthArtifact } = splitSourceForStorage({
857857 source: nextSource,
858− recipeId: stableSourceRecipeId(nextSource),
859− recipeRevisionId: stableSourceRecipeRevisionId(nextSource),
858+ catalogId: stableSourceCatalogId(nextSource),
859+ catalogRevisionId: stableSourceCatalogRevisionId(nextSource),
860860 actorAccountId: options.actorAccountId,
861861 existingRuntimeAuthArtifactId: existingRuntimeAuthArtifact?.id ?? null,
862862 existingImportAuthArtifactId: existingImportAuthArtifact?.id ?? null,
packages/control-plane/src/runtime/sources-operations.ts+3−3
@@ -25,7 +25,7 @@ import { · 24 unmodified lines
2525 operationErrors,
2626 } from "./operation-errors";
2727 import { ControlPlaneStore, type ControlPlaneStoreShape } from "./store";
28−import { RuntimeSourceMaterializationService } from "./source-materialization";
28+import { RuntimeSourceCatalogSyncService } from "./source-catalog-sync";
2929 import {
3030 RuntimeSourceStoreService,
3131 } from "./source-store";
@@ -51,7 +51,7 @@ const syncArtifactsForSource = (input: { · 19 unmodified lines
5151 | typeof sourceOps.update;
5252 }) =>
5353 Effect.gen(function* () {
54− const materializationService = yield* RuntimeSourceMaterializationService;
54+ const catalogSyncService = yield* RuntimeSourceCatalogSyncService;
5555
5656 // For HTTP-backed source kinds that can validate themselves from a remote
5757 // document, automatically attempt to probe and connect. This mirrors the
@@ -63,7 +63,7 @@ const syncArtifactsForSource = (input: { · 5 unmodified lines
6363 : input.source;
6464
6565 const synced = yield* Effect.either(
66− materializationService.sync({
66+ catalogSyncService.sync({
6767 source: sourceForSync,
6868 actorAccountId: input.actorAccountId,
6969 }),
packages/control-plane/src/runtime/workspace-execution-environment.ts+62−122
@@ -8,11 +8,9 @@ import { · 7 unmodified lines
88 type ToolCatalog,
99 type ToolInvoker,
1010 type ToolPath,
11− type ToolSchemaBundle,
1211 } from "@executor/codemode-core";
1312 import { makeQuickJsExecutor } from "@executor/runtime-quickjs";
14−import type { AccountId, Source, SourceRecipeSchemaBundleId } from "#schema";
15−import { SourceRecipeSchemaBundleIdSchema } from "#schema";
13+import type { AccountId, Source } from "#schema";
1614 import * as Context from "effect/Context";
1715 import * as Either from "effect/Either";
1816 import * as Effect from "effect/Effect";
@@ -29,13 +27,15 @@ import { · 10 unmodified lines
2927 type RuntimeSourceAuthService,
3028 } from "./source-auth-service";
3129 import {
32− RuntimeSourceRecipeStoreService,
33− type LoadedSourceRecipeToolIndexEntry,
34− recipeToolCatalogEntry,
35−} from "./source-recipes-runtime";
30+ RuntimeSourceCatalogStoreService,
31+ type LoadedSourceCatalogToolIndexEntry,
32+ catalogToolCatalogEntry,
33+} from "./source-catalog-runtime";
34+import {
35+ invocationDescriptorFromTool,
36+ invokeIrTool,
37+} from "./ir-execution";
3638 import { RuntimeSourceAuthMaterialService } from "./source-auth-material";
37−import { namespaceFromSourceName } from "./source-names";
38−import { getSourceAdapterForOperation } from "./source-adapters";
3939 import {
4040 type SecretMaterialResolveContext,
4141 } from "./secret-material-providers";
@@ -145,14 +145,14 @@ const toSecretResolutionContext = ( · 103 unmodified lines
145145 const queryTokenWeight = (token: string): number =>
146146 LOW_SIGNAL_QUERY_TOKENS.has(token) ? 0.25 : 1;
147147
148−const loadWorkspaceRecipeTools = (input: {
148+const loadWorkspaceCatalogTools = (input: {
149149 workspaceId: Source["workspaceId"];
150150 accountId: AccountId;
151− sourceRecipeStore: Effect.Effect.Success<typeof RuntimeSourceRecipeStoreService>;
151+ sourceCatalogStore: Effect.Effect.Success<typeof RuntimeSourceCatalogStoreService>;
152152 includeSchemas: boolean;
153−}): Effect.Effect<readonly LoadedSourceRecipeToolIndexEntry[], Error, WorkspaceStorageServices> =>
153+}): Effect.Effect<readonly LoadedSourceCatalogToolIndexEntry[], Error, WorkspaceStorageServices> =>
154154 Effect.map(
155− input.sourceRecipeStore.loadWorkspaceSourceRecipeToolIndex({
155+ input.sourceCatalogStore.loadWorkspaceSourceCatalogToolIndex({
156156 workspaceId: input.workspaceId,
157157 actorAccountId: input.accountId,
158158 includeSchemas: input.includeSchemas,
@@ -163,14 +163,14 @@ const loadWorkspaceRecipeTools = (input: { · 4 unmodified lines
163163 ),
164164 );
165165
166−const loadWorkspaceRecipeToolByPath = (input: {
166+const loadWorkspaceCatalogToolByPath = (input: {
167167 workspaceId: Source["workspaceId"];
168168 accountId: AccountId;
169− sourceRecipeStore: Effect.Effect.Success<typeof RuntimeSourceRecipeStoreService>;
169+ sourceCatalogStore: Effect.Effect.Success<typeof RuntimeSourceCatalogStoreService>;
170170 path: string;
171171 includeSchemas: boolean;
172−}): Effect.Effect<LoadedSourceRecipeToolIndexEntry | null, Error, WorkspaceStorageServices> =>
173− input.sourceRecipeStore.loadWorkspaceSourceRecipeToolByPath({
172+}): Effect.Effect<LoadedSourceCatalogToolIndexEntry | null, Error, WorkspaceStorageServices> =>
173+ input.sourceCatalogStore.loadWorkspaceSourceCatalogToolByPath({
174174 workspaceId: input.workspaceId,
175175 path: input.path,
176176 actorAccountId: input.accountId,
@@ -183,42 +183,29 @@ const loadWorkspaceRecipeToolByPath = (input: { · 6 unmodified lines
183183 ),
184184 );
185185
186−const loadWorkspaceSchemaBundle = (input: {
187− workspaceId: Source["workspaceId"];
188− sourceRecipeStore: Effect.Effect.Success<typeof RuntimeSourceRecipeStoreService>;
189− id: SourceRecipeSchemaBundleId;
190−}): Effect.Effect<ToolSchemaBundle | null, Error, WorkspaceStorageServices> =>
191− Effect.map(
192− input.sourceRecipeStore.loadWorkspaceSchemaBundle({
193− workspaceId: input.workspaceId,
194− id: input.id,
195− }),
196− (bundle) =>
197− bundle
198− ? {
199− id: bundle.id,
200− kind: bundle.kind as ToolSchemaBundle["kind"],
201− hash: bundle.hash,
202− refsJson: bundle.refsJson,
203− }
204− : null,
205− );
206−
207−const scoreRecipeTool = (
186+const scoreCatalogTool = (
208187 queryTokens: readonly string[],
209− tool: LoadedSourceRecipeToolIndexEntry,
188+ tool: LoadedSourceCatalogToolIndexEntry,
210189 ): number => {
211190 const pathText = tool.path.toLowerCase();
212191 const namespaceText = tool.searchNamespace.toLowerCase();
213− const toolIdText = tool.operation.toolId.toLowerCase();
214− const titleText = tool.operation.title?.toLowerCase() ?? "";
215− const descriptionText = tool.operation.description?.toLowerCase() ?? "";
216− const templateText = tool.metadata.pathTemplate?.toLowerCase() ?? "";
217−
218− const pathTokens = tokenize(`${tool.path} ${tool.operation.toolId}`);
192+ const toolIdText = tool.path.split(".").at(-1)?.toLowerCase() ?? "";
193+ const titleText = tool.capability.surface.title?.toLowerCase() ?? "";
194+ const descriptionText =
195+ tool.capability.surface.summary?.toLowerCase()
196+ ?? tool.capability.surface.description?.toLowerCase()
197+ ?? "";
198+ const templateText =
199+ tool.executable.protocol === "http"
200+ ? tool.executable.pathTemplate.toLowerCase()
201+ : tool.executable.protocol === "graphql"
202+ ? tool.executable.rootField.toLowerCase()
203+ : tool.executable.toolName.toLowerCase();
204+
205+ const pathTokens = tokenize(`${tool.path} ${toolIdText}`);
219206 const namespaceTokens = tokenize(tool.searchNamespace);
220− const titleTokens = tokenize(tool.operation.title ?? "");
221− const templateTokens = tokenize(tool.metadata.pathTemplate ?? "");
207+ const titleTokens = tokenize(tool.capability.surface.title ?? "");
208+ const templateTokens = tokenize(templateText);
222209
223210 let score = 0;
224211 let structuralHits = 0;
@@ -342,20 +329,9 @@ const approvalMessageForInvocation = ( · 117 unmodified lines
342329 return `Allow tool call: ${descriptor.toolPath}?`;
343330 };
344331
345−const toInvocationDescriptorFromRecipeTool = (input: {
346− tool: LoadedSourceRecipeToolIndexEntry;
347−}): InvocationDescriptor => ({
348− toolPath: input.tool.path,
349− sourceId: input.tool.source.id,
350− sourceName: input.tool.source.name,
351− sourceKind: input.tool.source.kind,
352− sourceNamespace:
353− input.tool.source.namespace ??
354− namespaceFromSourceName(input.tool.source.name),
355− operationKind: input.tool.operation.operationKind,
356− interaction: input.tool.metadata.interaction,
357− approvalLabel: input.tool.metadata.approvalLabel,
358−});
332+const toInvocationDescriptorFromCatalogTool = (input: {
333+ tool: LoadedSourceCatalogToolIndexEntry;
334+}): InvocationDescriptor => invocationDescriptorFromTool(input) satisfies InvocationDescriptor;
359335
360336 const authorizePersistedToolInvocation = (input: {
361337 workspaceId: Source["workspaceId"];
@@ -452,10 +428,10 @@ const provideRuntimeLocalWorkspace = <A, E, R>( · 90 unmodified lines
452428 ): Effect.Effect<A, E, R> =>
453429 provideOptionalRuntimeLocalWorkspace(effect, runtimeLocalWorkspace);
454430
455−const createWorkspaceRecipeCatalog = (input: {
431+const createWorkspaceSourceCatalog = (input: {
456432 workspaceId: Source["workspaceId"];
457433 accountId: AccountId;
458− sourceRecipeStore: Effect.Effect.Success<typeof RuntimeSourceRecipeStoreService>;
434+ sourceCatalogStore: Effect.Effect.Success<typeof RuntimeSourceCatalogStoreService>;
459435 workspaceConfigStore: WorkspaceConfigStoreShape;
460436 workspaceStateStore: WorkspaceStateStoreShape;
461437 sourceArtifactStore: SourceArtifactStoreShape;
@@ -471,26 +447,20 @@ const createWorkspaceRecipeCatalog = (input: { · 9 unmodified lines
471447
472448 const createSharedCatalog = (includeSchemas: boolean): Effect.Effect<ToolCatalog, Error, never> =>
473449 provideWorkspaceStorage(Effect.gen(function* () {
474− const recipeTools = yield* loadWorkspaceRecipeTools({
450+ const catalogTools = yield* loadWorkspaceCatalogTools({
475451 workspaceId: input.workspaceId,
476452 accountId: input.accountId,
477− sourceRecipeStore: input.sourceRecipeStore,
453+ sourceCatalogStore: input.sourceCatalogStore,
478454 includeSchemas,
479455 });
480456
481457 return createToolCatalogFromEntries({
482− entries: recipeTools.map((tool) =>
483− recipeToolCatalogEntry({
458+ entries: catalogTools.map((tool) =>
459+ catalogToolCatalogEntry({
484460 tool,
485− score: (queryTokens) => scoreRecipeTool(queryTokens, tool),
461+ score: (queryTokens) => scoreCatalogTool(queryTokens, tool),
486462 }),
487463 ),
488− getSchemaBundle: ({ id }) =>
489− provideWorkspaceStorage(loadWorkspaceSchemaBundle({
490− workspaceId: input.workspaceId,
491− sourceRecipeStore: input.sourceRecipeStore,
492− id: SourceRecipeSchemaBundleIdSchema.make(id),
493− })),
494464 });
495465 }));
496466
@@ -524,14 +494,6 @@ const createWorkspaceRecipeCatalog = (input: { · 27 unmodified lines
524494 input.runtimeLocalWorkspace,
525495 ),
526496
527− getSchemaBundle: ({ id }) =>
528− provideRuntimeLocalWorkspace(
529− Effect.flatMap(createSharedCatalog(false), (catalog) =>
530− catalog.getSchemaBundle({ id }),
531− ),
532− input.runtimeLocalWorkspace,
533− ),
534−
535497 searchTools: ({ query, namespace, limit }) =>
536498 provideRuntimeLocalWorkspace(
537499 Effect.flatMap(createSharedCatalog(false), (catalog) =>
@@ -549,7 +511,7 @@ const createWorkspaceRecipeCatalog = (input: { · 11 unmodified lines
549511 const createWorkspaceToolInvoker = (input: {
550512 workspaceId: Source["workspaceId"];
551513 accountId: AccountId;
552− sourceRecipeStore: Effect.Effect.Success<typeof RuntimeSourceRecipeStoreService>;
514+ sourceCatalogStore: Effect.Effect.Success<typeof RuntimeSourceCatalogStoreService>;
553515 workspaceConfigStore: WorkspaceConfigStoreShape;
554516 workspaceStateStore: WorkspaceStateStoreShape;
555517 sourceArtifactStore: SourceArtifactStoreShape;
@@ -578,10 +540,10 @@ const createWorkspaceToolInvoker = (input: { · 22 unmodified lines
578540 sourceAuthService: input.sourceAuthService,
579541 runtimeLocalWorkspace: input.runtimeLocalWorkspace,
580542 });
581− const recipeCatalog = createWorkspaceRecipeCatalog({
543+ const sourceCatalog = createWorkspaceSourceCatalog({
582544 workspaceId: input.workspaceId,
583545 accountId: input.accountId,
584− sourceRecipeStore: input.sourceRecipeStore,
546+ sourceCatalogStore: input.sourceCatalogStore,
585547 workspaceConfigStore: input.workspaceConfigStore,
586548 workspaceStateStore: input.workspaceStateStore,
587549 sourceArtifactStore: input.sourceArtifactStore,
@@ -606,7 +568,7 @@ const createWorkspaceToolInvoker = (input: { · 18 unmodified lines
606568 tools: authoredTools,
607569 });
608570 catalog = mergeToolCatalogs({
609− catalogs: [authoredCatalog, recipeCatalog],
571+ catalogs: [authoredCatalog, sourceCatalog],
610572 });
611573 const authoredToolPaths = new Set(Object.keys(authoredTools));
612574 const authoredInvoker = makeToolInvokerFromTools({
@@ -621,14 +583,14 @@ const createWorkspaceToolInvoker = (input: { · 8 unmodified lines
621583 }) =>
622584 provideRuntimeLocalWorkspace(
623585 provideWorkspaceStorage(Effect.gen(function* () {
624− const recipeTool = yield* loadWorkspaceRecipeToolByPath({
586+ const catalogTool = yield* loadWorkspaceCatalogToolByPath({
625587 workspaceId: input.workspaceId,
626588 accountId: input.accountId,
627− sourceRecipeStore: input.sourceRecipeStore,
589+ sourceCatalogStore: input.sourceCatalogStore,
628590 path: invocation.path,
629591 includeSchemas: false,
630592 });
631− if (!recipeTool) {
593+ if (!catalogTool) {
632594 return yield* Effect.fail(
633595 new Error(`Unknown tool path: ${invocation.path}`),
634596 );
@@ -637,49 +599,27 @@ const createWorkspaceToolInvoker = (input: { · 2 unmodified lines
637599 yield* authorizePersistedToolInvocation({
638600 workspaceId: input.workspaceId,
639601 accountId: input.accountId,
640− descriptor: toInvocationDescriptorFromRecipeTool({
641− tool: recipeTool,
602+ descriptor: toInvocationDescriptorFromCatalogTool({
603+ tool: catalogTool,
642604 }),
643605 args: invocation.args,
644− source: recipeTool.source,
606+ source: catalogTool.source,
645607 context: invocation.context,
646608 onElicitation: input.onElicitation,
647609 });
648610
649611 const auth = yield* input.sourceAuthMaterialService.resolve({
650− source: recipeTool.source,
612+ source: catalogTool.source,
651613 actorAccountId: input.accountId,
652614 context: toSecretResolutionContext(invocation.context),
653615 });
654− const schemaBundle = recipeTool.schemaBundleId
655− ? yield* loadWorkspaceSchemaBundle({
656− workspaceId: input.workspaceId,
657− sourceRecipeStore: input.sourceRecipeStore,
658− id: SourceRecipeSchemaBundleIdSchema.make(
659− recipeTool.schemaBundleId,
660− ),
661− })
662− : null;
663− const recipe = yield* input.sourceRecipeStore.loadSourceWithRecipe({
664− workspaceId: input.workspaceId,
665− sourceId: recipeTool.source.id,
666− actorAccountId: input.accountId,
667− });
668−
669− return yield* getSourceAdapterForOperation(
670− recipeTool.operation,
671− ).invokePersistedTool({
616+ return yield* invokeIrTool({
672617 workspaceId: input.workspaceId,
673618 accountId: input.accountId,
674− source: recipeTool.source,
675− path: invocation.path,
676− operation: recipeTool.operation,
677− schemaBundle,
678− manifestJson: recipe.revision.manifestJson,
619+ tool: catalogTool,
679620 auth,
680621 args: invocation.args,
681622 context: invocation.context,
682− onElicitation: input.onElicitation,
683623 });
684624 })),
685625 input.runtimeLocalWorkspace,
@@ -702,7 +642,7 @@ const createWorkspaceToolInvoker = (input: { · 16 unmodified lines
702642 export const createWorkspaceExecutionEnvironmentResolver = (input: {
703643 sourceAuthMaterialService: Effect.Effect.Success<typeof RuntimeSourceAuthMaterialService>;
704644 sourceAuthService: RuntimeSourceAuthService;
705− sourceRecipeStore: Effect.Effect.Success<typeof RuntimeSourceRecipeStoreService>;
645+ sourceCatalogStore: Effect.Effect.Success<typeof RuntimeSourceCatalogStoreService>;
706646 localToolRuntimeLoader: LocalToolRuntimeLoaderShape;
707647 workspaceConfigStore: WorkspaceConfigStoreShape;
708648 workspaceStateStore: WorkspaceStateStoreShape;
@@ -723,7 +663,7 @@ export const createWorkspaceExecutionEnvironmentResolver = (input: { · 14 unmodified lines
723663 const { catalog, toolInvoker } = createWorkspaceToolInvoker({
724664 workspaceId,
725665 accountId,
726− sourceRecipeStore: input.sourceRecipeStore,
666+ sourceCatalogStore: input.sourceCatalogStore,
727667 workspaceConfigStore: input.workspaceConfigStore,
728668 workspaceStateStore: input.workspaceStateStore,
729669 sourceArtifactStore: input.sourceArtifactStore,
@@ -762,7 +702,7 @@ export const RuntimeExecutionResolverLive = ( · 32 unmodified lines
762702 Effect.gen(function* () {
763703 const sourceAuthMaterialService = yield* RuntimeSourceAuthMaterialService;
764704 const sourceAuthService = yield* RuntimeSourceAuthServiceTag;
765− const sourceRecipeStore = yield* RuntimeSourceRecipeStoreService;
705+ const sourceCatalogStore = yield* RuntimeSourceCatalogStoreService;
766706 const localToolRuntimeLoader = yield* LocalToolRuntimeLoaderService;
767707 const workspaceConfigStore = yield* WorkspaceConfigStore;
768708 const workspaceStateStore = yield* WorkspaceStateStore;
@@ -771,7 +711,7 @@ export const RuntimeExecutionResolverLive = ( · 2 unmodified lines
771711 return createWorkspaceExecutionEnvironmentResolver({
772712 sourceAuthService,
773713 sourceAuthMaterialService,
774− sourceRecipeStore,
714+ sourceCatalogStore,
775715 localToolRuntimeLoader,
776716 workspaceConfigStore,
777717 workspaceStateStore,
packages/control-plane/src/schema/enums.ts+7−15
@@ -16,21 +16,13 @@ export { · 15 unmodified lines
1616 type SecretRef,
1717 } from "./models/auth-artifact";
1818 export {
19− SourceRecipeAdapterKeySchema,
20− SourceRecipeDocumentKindSchema,
21− SourceRecipeKindSchema,
22− SourceRecipeOperationKindSchema,
23− SourceRecipeOperationProviderKindSchema,
24− SourceRecipeTransportKindSchema,
25− SourceRecipeVisibilitySchema,
26− type SourceRecipeAdapterKey,
27− type SourceRecipeDocumentKind,
28− type SourceRecipeKind,
29− type SourceRecipeOperationKind,
30− type SourceRecipeOperationProviderKind,
31− type SourceRecipeTransportKind,
32− type SourceRecipeVisibility,
33−} from "./models/source-recipe";
19+ SourceCatalogAdapterKeySchema,
20+ SourceCatalogKindSchema,
21+ SourceCatalogVisibilitySchema,
22+ type SourceCatalogAdapterKey,
23+ type SourceCatalogKind,
24+ type SourceCatalogVisibility,
25+} from "./models/source-catalog";
3426 export {
3527 SourceAuthInferenceSchema,
3628 SourceDiscoveryAuthKindSchema,
packages/control-plane/src/schema/ids.ts+5−9
@@ -3,12 +3,9 @@ import { Schema } from "effect"; · 2 unmodified lines
33 export const AccountIdSchema = Schema.String.pipe(Schema.brand("AccountId"));
44 export const WorkspaceIdSchema = Schema.String.pipe(Schema.brand("WorkspaceId"));
55 export const SourceIdSchema = Schema.String.pipe(Schema.brand("SourceId"));
6−export const SourceRecipeIdSchema = Schema.String.pipe(Schema.brand("SourceRecipeId"));
7−export const SourceRecipeRevisionIdSchema = Schema.String.pipe(
8− Schema.brand("SourceRecipeRevisionId"),
9−);
10−export const SourceRecipeSchemaBundleIdSchema = Schema.String.pipe(
11− Schema.brand("SourceRecipeSchemaBundleId"),
6+export const SourceCatalogIdSchema = Schema.String.pipe(Schema.brand("SourceCatalogId"));
7+export const SourceCatalogRevisionIdSchema = Schema.String.pipe(
8+ Schema.brand("SourceCatalogRevisionId"),
129 );
1310 export const SourceAuthSessionIdSchema = Schema.String.pipe(
1411 Schema.brand("SourceAuthSessionId"),
@@ -38,9 +35,8 @@ export const ExecutionStepIdSchema = Schema.String.pipe( · 23 unmodified lines
3835 export type AccountId = typeof AccountIdSchema.Type;
3936 export type WorkspaceId = typeof WorkspaceIdSchema.Type;
4037 export type SourceId = typeof SourceIdSchema.Type;
41−export type SourceRecipeId = typeof SourceRecipeIdSchema.Type;
42−export type SourceRecipeRevisionId = typeof SourceRecipeRevisionIdSchema.Type;
43−export type SourceRecipeSchemaBundleId = typeof SourceRecipeSchemaBundleIdSchema.Type;
38+export type SourceCatalogId = typeof SourceCatalogIdSchema.Type;
39+export type SourceCatalogRevisionId = typeof SourceCatalogRevisionIdSchema.Type;
4440 export type SourceAuthSessionId = typeof SourceAuthSessionIdSchema.Type;
4541 export type AuthArtifactId = typeof AuthArtifactIdSchema.Type;
4642 export type AuthLeaseId = typeof AuthLeaseIdSchema.Type;
packages/control-plane/src/schema/index.ts+1−1
@@ -4,7 +4,7 @@ export * from "./ids"; · 3 unmodified lines
44
55 export * from "./models/auth";
66 export * from "./models/source";
7−export * from "./models/source-recipe";
7+export * from "./models/source-catalog";
88 export * from "./models/auth-artifact";
99 export * from "./models/auth-lease";
1010 export * from "./models/source-oauth-client";
packages/control-plane/src/schema/models/source-catalog.ts+53−0
@@ -0,0 +1,53 @@
1+import { Schema } from "effect";
2+
3+import { TimestampMsSchema } from "../common";
4+import {
5+ SourceCatalogIdSchema,
6+ SourceCatalogRevisionIdSchema,
7+} from "../ids";
8+
9+export const SourceCatalogKindSchema = Schema.Literal(
10+ "http_api",
11+ "mcp",
12+ "internal",
13+);
14+
15+export const SourceCatalogAdapterKeySchema = Schema.String;
16+
17+export const SourceCatalogVisibilitySchema = Schema.Literal(
18+ "private",
19+ "workspace",
20+ "organization",
21+ "public",
22+);
23+
24+export const StoredSourceCatalogRecordSchema = Schema.Struct({
25+ id: SourceCatalogIdSchema,
26+ kind: SourceCatalogKindSchema,
27+ adapterKey: SourceCatalogAdapterKeySchema,
28+ providerKey: Schema.String,
29+ name: Schema.String,
30+ summary: Schema.NullOr(Schema.String),
31+ visibility: SourceCatalogVisibilitySchema,
32+ latestRevisionId: SourceCatalogRevisionIdSchema,
33+ createdAt: TimestampMsSchema,
34+ updatedAt: TimestampMsSchema,
35+});
36+
37+export const StoredSourceCatalogRevisionRecordSchema = Schema.Struct({
38+ id: SourceCatalogRevisionIdSchema,
39+ catalogId: SourceCatalogIdSchema,
40+ revisionNumber: Schema.Number,
41+ sourceConfigJson: Schema.String,
42+ importMetadataJson: Schema.NullOr(Schema.String),
43+ importMetadataHash: Schema.NullOr(Schema.String),
44+ snapshotHash: Schema.NullOr(Schema.String),
45+ createdAt: TimestampMsSchema,
46+ updatedAt: TimestampMsSchema,
47+});
48+
49+export type SourceCatalogKind = typeof SourceCatalogKindSchema.Type;
50+export type SourceCatalogAdapterKey = typeof SourceCatalogAdapterKeySchema.Type;
51+export type SourceCatalogVisibility = typeof SourceCatalogVisibilitySchema.Type;
52+export type StoredSourceCatalogRecord = typeof StoredSourceCatalogRecordSchema.Type;
53+export type StoredSourceCatalogRevisionRecord = typeof StoredSourceCatalogRevisionRecordSchema.Type;
packages/control-plane/src/schema/models/source-inspection.ts+7−21
@@ -2,18 +2,14 @@ import { Schema } from "effect"; · 1 unmodified line
22
33 import { SourceSchema } from "./source";
44
5−export const SourceInspectionPipelineKindSchema = Schema.Literal(
6− "openapi",
7− "graphql",
8− "persisted",
9−);
5+export const SourceInspectionPipelineKindSchema = Schema.Literal("ir");
106
117 export const SourceInspectionToolSummarySchema = Schema.Struct({
128 path: Schema.String,
139 sourceKey: Schema.String,
1410 title: Schema.optional(Schema.String),
1511 description: Schema.optional(Schema.String),
16− providerKind: Schema.String,
12+ protocol: Schema.String,
1713 toolId: Schema.String,
1814 rawToolId: Schema.NullOr(Schema.String),
1915 operationId: Schema.NullOr(Schema.String),
@@ -43,19 +39,11 @@ export const SourceInspectionToolDetailSchema = Schema.Struct({ · 23 unmodified lines
4339 summary: SourceInspectionToolSummarySchema,
4440 definitionJson: Schema.NullOr(Schema.String),
4541 documentationJson: Schema.NullOr(Schema.String),
46− providerDataJson: Schema.NullOr(Schema.String),
47− inputSchemaJson: Schema.NullOr(Schema.String),
48− outputSchemaJson: Schema.NullOr(Schema.String),
49− schemaBundleId: Schema.NullOr(Schema.String),
50− exampleInputJson: Schema.NullOr(Schema.String),
51− exampleOutputJson: Schema.NullOr(Schema.String),
52−});
53−
54−export const SourceInspectionSchemaBundleSchema = Schema.Struct({
55− id: Schema.String,
56− kind: Schema.String,
57− hash: Schema.String,
58− refsJson: Schema.String,
42+ nativeJson: Schema.NullOr(Schema.String),
43+ callSchemaJson: Schema.NullOr(Schema.String),
44+ resultSchemaJson: Schema.NullOr(Schema.String),
45+ exampleCallJson: Schema.NullOr(Schema.String),
46+ exampleResultJson: Schema.NullOr(Schema.String),
5947 });
6048
6149 export const SourceInspectionDiscoverPayloadSchema = Schema.Struct({
@@ -89,8 +77,6 @@ export type SourceInspectionToolListItem = · 27 unmodified lines
8977 export type SourceInspection = typeof SourceInspectionSchema.Type;
9078 export type SourceInspectionToolDetail =
9179 typeof SourceInspectionToolDetailSchema.Type;
92−export type SourceInspectionSchemaBundle =
93− typeof SourceInspectionSchemaBundleSchema.Type;
9480 export type SourceInspectionDiscoverPayload =
9581 typeof SourceInspectionDiscoverPayloadSchema.Type;
9682 export type SourceInspectionDiscoverResultItem =
packages/control-plane/src/schema/models/source-recipe.ts+0−131
@@ -1,131 +0,0 @@
1−import { Schema } from "effect";
2−
3−import { TimestampMsSchema } from "../common";
4−import {
5− SourceRecipeIdSchema,
6− SourceRecipeRevisionIdSchema,
7− SourceRecipeSchemaBundleIdSchema,
8−} from "../ids";
9−
10−export const SourceRecipeKindSchema = Schema.Literal(
11− "http_api",
12− "mcp",
13− "internal",
14−);
15−
16−export const SourceRecipeAdapterKeySchema = Schema.String;
17−
18−export const SourceRecipeVisibilitySchema = Schema.Literal(
19− "private",
20− "workspace",
21− "organization",
22− "public",
23−);
24−
25−export const SourceRecipeDocumentKindSchema = Schema.String;
26−
27−export const SourceRecipeSchemaBundleKindSchema = Schema.String;
28−
29−export const SourceRecipeTransportKindSchema = Schema.Literal(
30− "http",
31− "graphql",
32− "mcp",
33− "internal",
34−);
35−
36−export const SourceRecipeOperationKindSchema = Schema.Literal(
37− "read",
38− "write",
39− "delete",
40− "unknown",
41−);
42−
43−export const SourceRecipeOperationProviderKindSchema = Schema.String;
44−
45−export const StoredSourceRecipeRecordSchema = Schema.Struct({
46− id: SourceRecipeIdSchema,
47− kind: SourceRecipeKindSchema,
48− adapterKey: SourceRecipeAdapterKeySchema,
49− providerKey: Schema.String,
50− name: Schema.String,
51− summary: Schema.NullOr(Schema.String),
52− visibility: SourceRecipeVisibilitySchema,
53− latestRevisionId: SourceRecipeRevisionIdSchema,
54− createdAt: TimestampMsSchema,
55− updatedAt: TimestampMsSchema,
56−});
57−
58−export const StoredSourceRecipeRevisionRecordSchema = Schema.Struct({
59− id: SourceRecipeRevisionIdSchema,
60− recipeId: SourceRecipeIdSchema,
61− revisionNumber: Schema.Number,
62− sourceConfigJson: Schema.String,
63− manifestJson: Schema.NullOr(Schema.String),
64− manifestHash: Schema.NullOr(Schema.String),
65− materializationHash: Schema.NullOr(Schema.String),
66− createdAt: TimestampMsSchema,
67− updatedAt: TimestampMsSchema,
68−});
69−
70−export const StoredSourceRecipeDocumentRecordSchema = Schema.Struct({
71− id: Schema.String,
72− recipeRevisionId: SourceRecipeRevisionIdSchema,
73− documentKind: SourceRecipeDocumentKindSchema,
74− documentKey: Schema.String,
75− contentText: Schema.String,
76− contentHash: Schema.String,
77− fetchedAt: Schema.NullOr(TimestampMsSchema),
78− createdAt: TimestampMsSchema,
79− updatedAt: TimestampMsSchema,
80−});
81−
82−export const StoredSourceRecipeSchemaBundleRecordSchema = Schema.Struct({
83− id: SourceRecipeSchemaBundleIdSchema,
84− recipeRevisionId: SourceRecipeRevisionIdSchema,
85− bundleKind: SourceRecipeSchemaBundleKindSchema,
86− refsJson: Schema.String,
87− contentHash: Schema.String,
88− createdAt: TimestampMsSchema,
89− updatedAt: TimestampMsSchema,
90−});
91−
92−export const StoredSourceRecipeOperationRowSchema = Schema.Struct({
93− id: Schema.String,
94− recipeRevisionId: SourceRecipeRevisionIdSchema,
95− operationKey: Schema.String,
96− transportKind: SourceRecipeTransportKindSchema,
97− toolId: Schema.String,
98− title: Schema.NullOr(Schema.String),
99− description: Schema.NullOr(Schema.String),
100− operationKind: SourceRecipeOperationKindSchema,
101− searchText: Schema.String,
102− inputSchemaJson: Schema.NullOr(Schema.String),
103− outputSchemaJson: Schema.NullOr(Schema.String),
104− providerKind: SourceRecipeOperationProviderKindSchema,
105− providerDataJson: Schema.NullOr(Schema.String),
106− createdAt: TimestampMsSchema,
107− updatedAt: TimestampMsSchema,
108−});
109−export const StoredSourceRecipeOperationRecordSchema =
110− StoredSourceRecipeOperationRowSchema.annotations({
111− identifier: "StoredSourceRecipeOperationRecord",
112− });
113−
114−export type SourceRecipeKind = typeof SourceRecipeKindSchema.Type;
115−export type SourceRecipeAdapterKey = typeof SourceRecipeAdapterKeySchema.Type;
116−export type SourceRecipeVisibility = typeof SourceRecipeVisibilitySchema.Type;
117−export type SourceRecipeDocumentKind = typeof SourceRecipeDocumentKindSchema.Type;
118−export type SourceRecipeSchemaBundleKind =
119− typeof SourceRecipeSchemaBundleKindSchema.Type;
120−export type SourceRecipeTransportKind = typeof SourceRecipeTransportKindSchema.Type;
121−export type SourceRecipeOperationKind = typeof SourceRecipeOperationKindSchema.Type;
122−export type SourceRecipeOperationProviderKind =
123− typeof SourceRecipeOperationProviderKindSchema.Type;
124−export type StoredSourceRecipeRecord = typeof StoredSourceRecipeRecordSchema.Type;
125−export type StoredSourceRecipeRevisionRecord = typeof StoredSourceRecipeRevisionRecordSchema.Type;
126−export type StoredSourceRecipeDocumentRecord = typeof StoredSourceRecipeDocumentRecordSchema.Type;
127−export type StoredSourceRecipeSchemaBundleRecord =
128− typeof StoredSourceRecipeSchemaBundleRecordSchema.Type;
129−export type StoredSourceRecipeOperationRow = typeof StoredSourceRecipeOperationRowSchema.Type;
130−export type StoredSourceRecipeOperationRecord =
131− typeof StoredSourceRecipeOperationRecordSchema.Type;
packages/control-plane/src/schema/models/source.ts+10−10
@@ -3,8 +3,8 @@ import { Schema } from "effect"; · 2 unmodified lines
33 import { TimestampMsSchema } from "../common";
44 import {
55 SourceIdSchema,
6− SourceRecipeIdSchema,
7− SourceRecipeRevisionIdSchema,
6+ SourceCatalogIdSchema,
7+ SourceCatalogRevisionIdSchema,
88 WorkspaceIdSchema,
99 } from "../ids";
1010 import { SecretRefSchema } from "./auth-artifact";
@@ -77,8 +77,8 @@ export const SourceBindingSchema = Schema.Struct({ · 66 unmodified lines
7777 const SourceStorageRowSchema = Schema.Struct({
7878 workspaceId: WorkspaceIdSchema,
7979 sourceId: SourceIdSchema,
80− recipeId: SourceRecipeIdSchema,
81− recipeRevisionId: SourceRecipeRevisionIdSchema,
80+ catalogId: SourceCatalogIdSchema,
81+ catalogRevisionId: SourceCatalogRevisionIdSchema,
8282 name: Schema.String,
8383 kind: SourceKindSchema,
8484 endpoint: Schema.String,
@@ -98,8 +98,8 @@ export const StoredSourceRecordSchema = Schema.transform( · 13 unmodified lines
9898 Schema.Struct({
9999 id: SourceIdSchema,
100100 workspaceId: WorkspaceIdSchema,
101− recipeId: SourceRecipeIdSchema,
102− recipeRevisionId: SourceRecipeRevisionIdSchema,
101+ catalogId: SourceCatalogIdSchema,
102+ catalogRevisionId: SourceCatalogRevisionIdSchema,
103103 name: Schema.String,
104104 kind: SourceKindSchema,
105105 endpoint: Schema.String,
@@ -118,8 +118,8 @@ export const StoredSourceRecordSchema = Schema.transform( · 12 unmodified lines
118118 decode: (row) => ({
119119 id: row.sourceId,
120120 workspaceId: row.workspaceId,
121− recipeId: row.recipeId,
122− recipeRevisionId: row.recipeRevisionId,
121+ catalogId: row.catalogId,
122+ catalogRevisionId: row.catalogRevisionId,
123123 name: row.name,
124124 kind: row.kind,
125125 endpoint: row.endpoint,
@@ -136,8 +136,8 @@ export const StoredSourceRecordSchema = Schema.transform( · 10 unmodified lines
136136 encode: (source) => ({
137137 workspaceId: source.workspaceId,
138138 sourceId: source.id,
139− recipeId: source.recipeId,
140− recipeRevisionId: source.recipeRevisionId,
139+ catalogId: source.catalogId,
140+ catalogRevisionId: source.catalogRevisionId,
141141 name: source.name,
142142 kind: source.kind,
143143 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
33 import * as Schema from "effect/Schema";
44
55 import {
6− StoredSourceRecipeOperationRecordSchema,
6+ SourceInspectionToolDetailSchema,
7+ StoredSourceCatalogRevisionRecordSchema,
78 } from "./index";
89
910 describe("control-plane-schema", () => {
10− it("accepts canonical source recipe operation rows and rejects invalid transport kinds", () => {
11− const decode = Schema.decodeUnknownEither(StoredSourceRecipeOperationRecordSchema);
11+ it("accepts snapshot-native inspection detail rows", () => {
12+ const decode = Schema.decodeUnknownEither(SourceInspectionToolDetailSchema);
1213
13− const operationRecord = decode({
14− id: "src_recipe_op_1",
15− recipeRevisionId: "src_recipe_rev_1",
16− operationKey: "getRepo",
17− transportKind: "http",
18− toolId: "getRepo",
19− title: "Get Repo",
20− description: "Read a repository",
21− operationKind: "read",
22− searchText: "get repo",
23− inputSchemaJson: null,
24− outputSchemaJson: null,
25− providerKind: "openapi",
26− providerDataJson: JSON.stringify({
27− kind: "openapi",
28− }),
29− createdAt: 1,
30− updatedAt: 1,
14+ const detail = decode({
15+ summary: {
16+ path: "github.issues.list",
17+ sourceKey: "src_1",
18+ protocol: "http",
19+ toolId: "list",
20+ rawToolId: "issues.list",
21+ operationId: "listIssues",
22+ group: "issues",
23+ leaf: "list",
24+ tags: ["issues"],
25+ method: "GET",
26+ pathTemplate: "/issues",
27+ },
28+ definitionJson: "{}",
29+ documentationJson: "{}",
30+ nativeJson: "{}",
31+ callSchemaJson: null,
32+ resultSchemaJson: null,
33+ exampleCallJson: null,
34+ exampleResultJson: null,
3135 });
3236
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);
3442
35− const invalidRecord = decode({
36− id: "src_recipe_op_2",
37− recipeRevisionId: "src_recipe_rev_1",
38− operationKey: "viewer",
39− transportKind: "ftp",
40− toolId: "viewer",
41− title: "Viewer",
42− description: null,
43− operationKind: "read",
44− searchText: "viewer",
45− inputSchemaJson: null,
46− outputSchemaJson: null,
47− providerKind: "graphql",
48− providerDataJson: JSON.stringify({
49− kind: "graphql",
50− }),
43+ const invalidRevision = decode({
44+ id: "src_catalog_rev_1",
45+ catalogId: "src_catalog_1",
46+ revisionNumber: "1",
47+ sourceConfigJson: "{}",
48+ importMetadataJson: null,
49+ importMetadataHash: null,
50+ snapshotHash: null,
5151 createdAt: 1,
5252 updatedAt: 1,
5353 });
5454
55− expect(Either.isLeft(invalidRecord)).toBe(true);
55+ expect(Either.isLeft(invalidRevision)).toBe(true);
5656 });
5757 });
packages/kitchen-sink/src/source-runtime.test.ts+13−14
@@ -67,8 +67,8 @@ type StoredToolArtifactRecord = { · 66 unmodified lines
6767 description: string | null;
6868 searchNamespace: string;
6969 searchText: string;
70− inputSchemaJson: string | null;
71− outputSchemaJson: string | null;
70+ inputSchema: unknown | null;
71+ outputSchema: unknown | null;
7272 providerKind: "openapi";
7373 openApiMethod: "get" | "put" | "post" | "delete" | "patch" | "head" | "options" | "trace";
7474 openApiPathTemplate: string | null;
@@ -89,10 +89,10 @@ const toDescriptor = ( · 14 unmodified lines
8989 sourceKey: artifact.sourceId,
9090 description: artifact.description ?? artifact.title ?? undefined,
9191 interaction: "auto" as const,
92− inputType: includeSchemas && artifact.inputSchemaJson ? "object" : undefined,
93− outputType: includeSchemas && artifact.outputSchemaJson ? "output" : undefined,
94− inputSchemaJson: includeSchemas ? artifact.inputSchemaJson ?? undefined : undefined,
95− outputSchemaJson: includeSchemas ? artifact.outputSchemaJson ?? undefined : undefined,
92+ inputType: includeSchemas && artifact.inputSchema ? "object" : undefined,
93+ outputType: includeSchemas && artifact.outputSchema ? "output" : undefined,
94+ inputSchema: includeSchemas ? artifact.inputSchema ?? undefined : undefined,
95+ outputSchema: includeSchemas ? artifact.outputSchema ?? undefined : undefined,
9696 });
9797
9898 type SourceCallContext = {
@@ -359,7 +359,6 @@ const createWorkspaceToolCatalog = (input: { · 260 unmodified lines
359359 path,
360360 }).then((artifact) => (artifact ? toDescriptor(artifact, includeSchemas) : null))
361361 ),
362− getSchemaBundle: () => Effect.succeed(null),
363362 searchTools: ({ query, namespace, limit }) =>
364363 Effect.promise(() =>
365364 input.toolStore.list({
@@ -522,8 +521,8 @@ const openApiArtifact = (input: { · 156 unmodified lines
522521 method: NonNullable<StoredToolArtifactRecord["openApiMethod"]>;
523522 pathTemplate: string;
524523 operationHash?: string;
525− inputSchemaJson?: string | null;
526− outputSchemaJson?: string | null;
524+ inputSchema?: unknown | null;
525+ outputSchema?: unknown | null;
527526 }): StoredToolArtifactRecord => {
528527 const path = input.path as string;
529528 const description =
@@ -545,8 +544,8 @@ const openApiArtifact = (input: { · 15 unmodified lines
545544 input.method.toUpperCase(),
546545 input.pathTemplate,
547546 ),
548− inputSchemaJson: input.inputSchemaJson ?? null,
549− outputSchemaJson: input.outputSchemaJson ?? null,
547+ inputSchema: input.inputSchema ?? null,
548+ outputSchema: input.outputSchema ?? null,
550549 providerKind: "openapi",
551550 openApiMethod: input.method,
552551 openApiPathTemplate: input.pathTemplate,
@@ -783,7 +782,7 @@ describe("source runtime", () => { · 230 unmodified lines
783782 "Workflow:",
784783 '1) const matches = await tools.discover({ query: "<intent>", limit: 12 });',
785784 "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.",
787786 "4) Call selected tools.<path>(input).",
788787 "Do not use fetch; use tools.* only.",
789788 ].join("\n"),
@@ -834,7 +833,7 @@ describe("source runtime", () => { · 44 unmodified lines
834833 description: "GET /repos/{owner}/{repo}",
835834 interaction: "auto",
836835 inputType: "{ owner: string, repo: string }",
837− outputType: "unknown",
836+ outputType: "void",
838837 },
839838 ],
840839 total: 1,
@@ -853,7 +852,7 @@ describe("source runtime", () => { · 12 unmodified lines
853852 "Workflow:",
854853 '1) const matches = await tools.discover({ query: "<intent>", limit: 12 });',
855854 "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.",
857856 "4) Call selected tools.<path>(input).",
858857 "Do not use fetch; use tools.* only.",
859858 ].join("\n"),
packages/react/src/index.ts+0−68
@@ -19,7 +19,6 @@ import type { · 18 unmodified lines
1919 SourceDiscoveryResult,
2020 SourceInspection,
2121 SourceInspectionDiscoverResult,
22− SourceInspectionSchemaBundle,
2322 SourceInspectionToolDetail,
2423 StartSourceOAuthPayload,
2524 StartSourceOAuthResult,
@@ -59,13 +58,6 @@ type SourceToolDetailKeyParts = readonly [ · 33 unmodified lines
5958 Source["id"],
6059 string | null,
6160 ];
62−type SourceSchemaBundleKeyParts = readonly [
63− boolean,
64− Source["workspaceId"],
65− string,
66− Source["id"],
67− string | null,
68−];
6961 type SourceDiscoveryKeyParts = readonly [
7062 boolean,
7163 Source["workspaceId"],
@@ -86,7 +78,6 @@ type ActiveQueryCollections = { · 14 unmodified lines
8678 sources: Set<string>;
8779 inspections: Set<string>;
8880 toolDetails: Set<string>;
89− schemaBundles: Set<string>;
9081 discoveries: Set<string>;
9182 };
9283
@@ -162,17 +153,6 @@ const encodeToolDetailKey = ( · 69 unmodified lines
162153 ): string =>
163154 encodeAtomKey([enabled, workspaceId, accountId, sourceId, toolPath] satisfies SourceToolDetailKeyParts);
164155
165−const encodeSchemaBundleKey = (
166− enabled: boolean,
167− workspaceId: Source["workspaceId"],
168− accountId: string,
169− sourceId: Source["id"],
170− schemaBundleId: string | null,
171−): string =>
172− encodeAtomKey(
173− [enabled, workspaceId, accountId, sourceId, schemaBundleId] satisfies SourceSchemaBundleKeyParts,
174− );
175−
176156 const encodeDiscoveryKey = (
177157 enabled: boolean,
178158 workspaceId: Source["workspaceId"],
@@ -367,25 +347,6 @@ const sourceInspectionToolAtom = Atom.family((key: string) => { · 188 unmodified lines
367347 ).pipe(Atom.keepAlive);
368348 });
369349
370−const sourceInspectionSchemaBundleAtom = Atom.family((key: string) => {
371− const [enabled, workspaceId, accountId, sourceId, schemaBundleId] = decodeAtomKey<SourceSchemaBundleKeyParts>(key);
372−
373− return Atom.make(
374− enabled && schemaBundleId
375− ? controlPlaneRequest({
376− accountId,
377− execute: (client) => client.sources.inspectionSchemaBundle({
378− path: {
379− workspaceId,
380− sourceId,
381− schemaBundleId,
382− },
383− }),
384− })
385− : Effect.succeed<SourceInspectionSchemaBundle | null>(null),
386− ).pipe(Atom.keepAlive);
387−});
388−
389350 const sourceDiscoveryAtom = Atom.family((key: string) => {
390351 const [enabled, workspaceId, accountId, sourceId, query, limit] = decodeAtomKey<SourceDiscoveryKeyParts>(key);
391352
@@ -547,7 +508,6 @@ const createActiveQueryCollections = (): ActiveQueryCollections => ({ · 155 unmodified lines
547508 sources: new Set(),
548509 inspections: new Set(),
549510 toolDetails: new Set(),
550− schemaBundles: new Set(),
551511 discoveries: new Set(),
552512 });
553513
@@ -602,13 +562,6 @@ const invalidateTrackedQueries = ( · 48 unmodified lines
602562 }
603563 });
604564
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−
612565 activeQueries.discoveries.forEach((key) => {
613566 const [enabled, workspaceId, accountId, sourceId] = decodeAtomKey<SourceDiscoveryKeyParts>(key);
614567 if (enabled && targetMatches(target, workspaceId, accountId, sourceId)) {
@@ -990,27 +943,6 @@ export const useSourceToolDetail = ( · 375 unmodified lines
990943 return workspace.enabled ? detail : pendingLoadable(workspace.workspace);
991944 };
992945
993−export const useSourceSchemaBundle = (
994− sourceId: string,
995− schemaBundleId: string | null,
996−): Loadable<SourceInspectionSchemaBundle | null> => {
997− const workspace = useWorkspaceRequestContext();
998− const requestedSourceId = workspace.enabled
999− ? (sourceId as Source["id"])
1000− : PLACEHOLDER_SOURCE_ID;
1001− const key = encodeSchemaBundleKey(
1002− workspace.enabled,
1003− workspace.workspaceId,
1004− workspace.accountId,
1005− requestedSourceId,
1006− schemaBundleId,
1007− );
1008− useTrackActiveKey("schemaBundles", key, workspace.enabled && schemaBundleId !== null);
1009− const schemaBundle = useLoadableAtom(sourceInspectionSchemaBundleAtom(key));
1010−
1011− return workspace.enabled ? schemaBundle : pendingLoadable(workspace.workspace);
1012−};
1013−
1014946 export const useSourceDiscovery = (input: {
1015947 sourceId: string;
1016948 query: string;