History

wip

Rhys Sullivan committed 8 months ago

Commit fa98f631 parent 55e58d47 files · +534 −50Browse files

Unified · Split

7 files
  1. packages/control-plane/src/ir/catalog.test.ts +38 −2
  2. packages/control-plane/src/ir/catalog.ts +326 −17
  3. packages/control-plane/src/runtime/ir-execution.ts +47 −29
  4. packages/control-plane/src/runtime/local-source-artifacts.test.ts +4 −0
  5. packages/control-plane/src/runtime/local-source-artifacts.ts +110 −1
  6. packages/control-plane/src/runtime/source-catalog-snapshot.test.ts +3 −1
  7. packages/control-plane/src/runtime/source-catalog-snapshot.ts +6 −0
packages/control-plane/src/ir/catalog.test.ts+38−2
@@ -33,6 +33,19 @@ const put = <K extends string, V>(record: Record<K, V>, key: K, value: V) => { · 32 unmodified lines
33 record[key] = value;33 record[key] = value;
34};34};
35 35
36const expectNullableShapeContains = (
37 catalog: CatalogV1,
38 nullableShapeId: ReturnType<typeof ShapeSymbolIdSchema.make>,
39 expectedInnerShapeId: ReturnType<typeof ShapeSymbolIdSchema.make>,
40) => {
41 const shape = catalog.symbols[nullableShapeId];
42 if (shape?.kind !== "shape" || shape.node.type !== "anyOf") {
43 throw new Error("Expected nullable anyOf shape");
44 }
45
46 expect(shape.node.items).toContain(expectedInnerShapeId);
47};
48
36const docId = DocumentIdSchema.make("doc_primary");49const docId = DocumentIdSchema.make("doc_primary");
37const baseProvenance = (pointer: string): ProvenanceRef[] => [{50const baseProvenance = (pointer: string): ProvenanceRef[] => [{
38 relation: "declared",51 relation: "declared",
@@ -630,7 +643,6 @@ describe("IR catalog", () => { · 591 unmodified lines
630 643
631 const descriptor = projected.toolDescriptors[CapabilityIdSchema.make("cap_events_update")];644 const descriptor = projected.toolDescriptors[CapabilityIdSchema.make("cap_events_update")];
632 expect(descriptor.toolPath).toEqual(["google", "calendar", "events", "update"]);645 expect(descriptor.toolPath).toEqual(["google", "calendar", "events", "update"]);
633 expect(descriptor.resultShapeId).toBe(ShapeSymbolIdSchema.make("shape_event_result"));
634 646
635 const callShape = projected.catalog.symbols[descriptor.callShapeId];647 const callShape = projected.catalog.symbols[descriptor.callShapeId];
636 expect(callShape?.kind).toBe("shape");648 expect(callShape?.kind).toBe("shape");
@@ -646,6 +658,19 @@ describe("IR catalog", () => { · 9 unmodified lines
646 ]);658 ]);
647 expect(callShape.node.required).toEqual(["calendarId", "eventId", "body"]);659 expect(callShape.node.required).toEqual(["calendarId", "eventId", "body"]);
648 660
661 const resultShape = descriptor.resultShapeId
662 ? projected.catalog.symbols[descriptor.resultShapeId]
663 : undefined;
664 if (resultShape?.kind !== "shape" || resultShape.node.type !== "object") {
665 throw new Error("Expected projected HTTP result envelope");
666 }
667 expect(Object.keys(resultShape.node.fields)).toEqual(["data", "error", "headers", "status"]);
668 expectNullableShapeContains(
669 projected.catalog,
670 resultShape.node.fields.data.shapeId,
671 ShapeSymbolIdSchema.make("shape_event_result"),
672 );
673
649 const searchDoc = projected.searchDocs[CapabilityIdSchema.make("cap_events_update")];674 const searchDoc = projected.searchDocs[CapabilityIdSchema.make("cap_events_update")];
650 expect(searchDoc.authHints).toContain("oauth2");675 expect(searchDoc.authHints).toContain("oauth2");
651 expect(searchDoc.tags).toEqual(["calendar", "events"]);676 expect(searchDoc.tags).toEqual(["calendar", "events"]);
@@ -680,7 +705,18 @@ describe("IR catalog", () => { · 28 unmodified lines
680 }705 }
681 706
682 expect(Object.keys(callShape.node.fields)).toEqual(["id", "input", "select"]);707 expect(Object.keys(callShape.node.fields)).toEqual(["id", "input", "select"]);
683 expect(descriptor.resultShapeId).toBe(ShapeSymbolIdSchema.make("shape_graphql_result"));708 const resultShape = descriptor.resultShapeId
709 ? projected.catalog.symbols[descriptor.resultShapeId]
710 : undefined;
711 if (resultShape?.kind !== "shape" || resultShape.node.type !== "object") {
712 throw new Error("Expected projected GraphQL result envelope");
713 }
714 expect(Object.keys(resultShape.node.fields)).toEqual(["data", "error", "headers", "status"]);
715 expectNullableShapeContains(
716 projected.catalog,
717 resultShape.node.fields.data.shapeId,
718 ShapeSymbolIdSchema.make("shape_graphql_result"),
719 );
684 });720 });
685 721
686 it("builds inspect views and invocation plans from the projected graph", () => {722 it("builds inspect views and invocation plans from the projected graph", () => {
packages/control-plane/src/ir/catalog.ts+326−17
@@ -622,12 +622,17 @@ const responseContentCandidates = ( · 621 unmodified lines
622 content.shapeId ? [{ mediaType: content.mediaType, shapeId: content.shapeId }] : [],622 content.shapeId ? [{ mediaType: content.mediaType, shapeId: content.shapeId }] : [],
623 );623 );
624 624
625const projectResultShapeFromResponses = (625type RankedResponseVariantEntry = {
626 variant: ResponseSet["variants"][number];
627 response: ResponseSymbol;
628 score: number;
629};
630
631const responseVariantEntries = (
626 catalog: CatalogV1,632 catalog: CatalogV1,
627 capability: Capability,
628 responseSet: ResponseSet,633 responseSet: ResponseSet,
629): ShapeSymbolId | undefined => {634): RankedResponseVariantEntry[] =>
630 const ranked = [...responseSet.variants]635 [...responseSet.variants]
631 .map((variant) => {636 .map((variant) => {
632 const response = getResponseSymbol(catalog, variant.responseId);637 const response = getResponseSymbol(catalog, variant.responseId);
633 return response638 return response
@@ -638,14 +643,27 @@ const projectResultShapeFromResponses = ( · 4 unmodified lines
638 }643 }
639 : null;644 : null;
640 })645 })
641 .filter((entry): entry is { variant: ResponseSet["variants"][number]; response: ResponseSymbol; score: number } => entry !== null)646 .filter((entry): entry is RankedResponseVariantEntry => entry !== null);
642 .sort((left, right) => right.score - left.score);
643 647
644 const preferred = ranked.some(({ score }) => score >= 60)648const isSuccessStatusMatch = (match: StatusMatch): boolean => {
645 ? ranked.filter(({ score }) => score >= 60)649 switch (match.kind) {
646 : ranked.filter(({ score }) => score === (ranked[0]?.score ?? 0));650 case "exact":
651 return match.status >= 200 && match.status < 300;
652 case "range":
653 return match.value === "2XX";
654 case "default":
655 return false;
656 }
657};
647 658
648 const jsonCandidates = preferred.flatMap(({ response }) =>659const contentShapeIdFromResponseEntries = (
660 catalog: CatalogV1,
661 capability: Capability,
662 label: string,
663 title: string,
664 entries: readonly RankedResponseVariantEntry[],
665): ShapeSymbolId | undefined => {
666 const jsonCandidates = entries.flatMap(({ response }) =>
649 responseContentCandidates(response).filter((candidate) => isJsonMediaType(candidate.mediaType)),667 responseContentCandidates(response).filter((candidate) => isJsonMediaType(candidate.mediaType)),
650 );668 );
651 669
@@ -657,12 +675,12 @@ const projectResultShapeFromResponses = ( · 5 unmodified lines
657 mediaType: candidate.mediaType,675 mediaType: candidate.mediaType,
658 shapeId: candidate.shapeId,676 shapeId: candidate.shapeId,
659 })),677 })),
660 `responseSet:${responseSet.id}:json`,678 `${label}:json`,
661 );679 );
662 }680 }
663 681
664 const fallbackShapeIds = unique(682 const fallbackShapeIds = unique(
665 preferred.flatMap(({ response }) =>683 entries.flatMap(({ response }) =>
666 responseContentCandidates(response).map((candidate) => candidate.shapeId),684 responseContentCandidates(response).map((candidate) => candidate.shapeId),
667 ),685 ),
668 );686 );
@@ -677,8 +695,8 @@ const projectResultShapeFromResponses = ( · 8 unmodified lines
677 695
678 return createSyntheticShape(catalog, {696 return createSyntheticShape(catalog, {
679 capability,697 capability,
680 label: `responseSet:${responseSet.id}:union`,698 label: `${label}:union`,
681 title: `${capability.surface.title ?? capability.id} result`,699 title,
682 node: {700 node: {
683 type: "anyOf",701 type: "anyOf",
684 items: fallbackShapeIds,702 items: fallbackShapeIds,
@@ -692,6 +710,297 @@ const projectResultShapeFromResponses = ( · 7 unmodified lines
692 });710 });
693};711};
694 712
713const projectResultShapeFromResponses = (
714 catalog: CatalogV1,
715 capability: Capability,
716 responseSet: ResponseSet,
717): ShapeSymbolId | undefined =>
718 contentShapeIdFromResponseEntries(
719 catalog,
720 capability,
721 `responseSet:${responseSet.id}`,
722 `${capability.surface.title ?? capability.id} result`,
723 responseVariantEntries(catalog, responseSet).filter(({ variant }) => isSuccessStatusMatch(variant.match)),
724 );
725
726const projectErrorShapeFromResponses = (
727 catalog: CatalogV1,
728 capability: Capability,
729 responseSet: ResponseSet,
730): ShapeSymbolId | undefined =>
731 contentShapeIdFromResponseEntries(
732 catalog,
733 capability,
734 `responseSet:${responseSet.id}:error`,
735 `${capability.surface.title ?? capability.id} error`,
736 responseVariantEntries(catalog, responseSet).filter(({ variant }) => !isSuccessStatusMatch(variant.match)),
737 );
738
739const scalarShape = (
740 catalog: CatalogV1,
741 capability: Capability,
742 input: {
743 label: string;
744 title: string;
745 scalar: "string" | "number" | "integer" | "boolean" | "null" | "bytes";
746 },
747): ShapeSymbolId =>
748 createSyntheticShape(catalog, {
749 capability,
750 label: input.label,
751 title: input.title,
752 node: {
753 type: "scalar",
754 scalar: input.scalar,
755 },
756 });
757
758const constNullShape = (
759 catalog: CatalogV1,
760 capability: Capability,
761 label: string,
762): ShapeSymbolId =>
763 createSyntheticShape(catalog, {
764 capability,
765 label,
766 title: "null",
767 node: {
768 type: "const",
769 value: null,
770 },
771 });
772
773const unknownShape = (
774 catalog: CatalogV1,
775 capability: Capability,
776 input: {
777 label: string;
778 title: string;
779 reason: string;
780 },
781): ShapeSymbolId =>
782 createSyntheticShape(catalog, {
783 capability,
784 label: input.label,
785 title: input.title,
786 node: {
787 type: "unknown",
788 reason: input.reason,
789 },
790 });
791
792const nullableShape = (
793 catalog: CatalogV1,
794 capability: Capability,
795 input: {
796 label: string;
797 title: string;
798 baseShapeId: ShapeSymbolId;
799 },
800): ShapeSymbolId => {
801 const nullShapeId = constNullShape(catalog, capability, `${input.label}:null`);
802 if (input.baseShapeId === nullShapeId) {
803 return nullShapeId;
804 }
805
806 return createSyntheticShape(catalog, {
807 capability,
808 label: `${input.label}:nullable`,
809 title: input.title,
810 node: {
811 type: "anyOf",
812 items: unique([input.baseShapeId, nullShapeId]),
813 },
814 });
815};
816
817const headersShape = (
818 catalog: CatalogV1,
819 capability: Capability,
820 label: string,
821): ShapeSymbolId => {
822 const valueShapeId = scalarShape(catalog, capability, {
823 label: `${label}:value`,
824 title: "Header value",
825 scalar: "string",
826 });
827
828 return createSyntheticShape(catalog, {
829 capability,
830 label,
831 title: "Response headers",
832 node: {
833 type: "object",
834 fields: {},
835 additionalProperties: valueShapeId,
836 },
837 });
838};
839
840const shapeFieldShapeId = (
841 catalog: CatalogV1,
842 shapeId: ShapeSymbolId | undefined,
843 fieldName: string,
844 seen = new Set<ShapeSymbolId>(),
845): ShapeSymbolId | undefined => {
846 if (!shapeId || seen.has(shapeId)) {
847 return undefined;
848 }
849
850 const shape = getShape(catalog, shapeId);
851 if (!shape) {
852 return undefined;
853 }
854
855 const nextSeen = new Set(seen);
856 nextSeen.add(shapeId);
857
858 switch (shape.node.type) {
859 case "ref":
860 return shapeFieldShapeId(catalog, shape.node.target, fieldName, nextSeen);
861 case "object":
862 return shape.node.fields[fieldName]?.shapeId;
863 case "allOf":
864 for (const item of shape.node.items) {
865 const found = shapeFieldShapeId(catalog, item, fieldName, nextSeen);
866 if (found) {
867 return found;
868 }
869 }
870 return undefined;
871 default:
872 return undefined;
873 }
874};
875
876const projectExecutionResultShape = (
877 catalog: CatalogV1,
878 capability: Capability,
879 executable: Executable,
880 responseSet: ResponseSet,
881): ShapeSymbolId => {
882 const genericDataShape = unknownShape(catalog, capability, {
883 label: `executionResult:${capability.id}:data:unknown`,
884 title: "Response data",
885 reason: `Execution result data for ${capability.id} is not statically known`,
886 });
887 const genericErrorShape = unknownShape(catalog, capability, {
888 label: `executionResult:${capability.id}:error:unknown`,
889 title: "Response error",
890 reason: `Execution result error for ${capability.id} is not statically known`,
891 });
892 const genericHeadersShape = headersShape(
893 catalog,
894 capability,
895 `executionResult:${capability.id}:headers`,
896 );
897 const genericStatusShape = scalarShape(catalog, capability, {
898 label: `executionResult:${capability.id}:status`,
899 title: "Response status",
900 scalar: "integer",
901 });
902
903 let dataShapeId: ShapeSymbolId | undefined;
904 let errorShapeId: ShapeSymbolId | undefined;
905 let headersShapeId: ShapeSymbolId = genericHeadersShape;
906 let statusShapeId: ShapeSymbolId = genericStatusShape;
907
908 switch (executable.protocol) {
909 case "http":
910 dataShapeId = projectResultShapeFromResponses(catalog, capability, responseSet);
911 errorShapeId = projectErrorShapeFromResponses(catalog, capability, responseSet);
912 break;
913 case "graphql":
914 dataShapeId =
915 shapeFieldShapeId(catalog, executable.resultShapeId, "data")
916 ?? shapeFieldShapeId(catalog, executable.resultShapeId, "body")
917 ?? executable.resultShapeId;
918 errorShapeId =
919 shapeFieldShapeId(catalog, executable.resultShapeId, "error")
920 ?? shapeFieldShapeId(catalog, executable.resultShapeId, "errors");
921 headersShapeId =
922 shapeFieldShapeId(catalog, executable.resultShapeId, "headers")
923 ?? genericHeadersShape;
924 statusShapeId =
925 shapeFieldShapeId(catalog, executable.resultShapeId, "status")
926 ?? genericStatusShape;
927 break;
928 case "mcp":
929 dataShapeId = executable.outputShapeId;
930 errorShapeId = undefined;
931 statusShapeId = constNullShape(
932 catalog,
933 capability,
934 `executionResult:${capability.id}:status:null`,
935 );
936 break;
937 }
938
939 const dataFieldShapeId = nullableShape(catalog, capability, {
940 label: `executionResult:${capability.id}:data`,
941 title: "Result data",
942 baseShapeId: dataShapeId ?? genericDataShape,
943 });
944 const errorFieldShapeId = nullableShape(catalog, capability, {
945 label: `executionResult:${capability.id}:error`,
946 title: "Result error",
947 baseShapeId: errorShapeId ?? genericErrorShape,
948 });
949 const statusFieldShapeId = nullableShape(catalog, capability, {
950 label: `executionResult:${capability.id}:status`,
951 title: "Result status",
952 baseShapeId: statusShapeId,
953 });
954
955 return createSyntheticShape(catalog, {
956 capability,
957 label: `executionResult:${capability.id}`,
958 title: `${capability.surface.title ?? capability.id} result`,
959 node: {
960 type: "object",
961 fields: {
962 data: {
963 shapeId: dataFieldShapeId,
964 docs: {
965 description: "Successful result payload when available.",
966 },
967 },
968 error: {
969 shapeId: errorFieldShapeId,
970 docs: {
971 description: "Error payload when the remote execution completed but failed.",
972 },
973 },
974 headers: {
975 shapeId: headersShapeId,
976 docs: {
977 description: "Response headers when available.",
978 },
979 },
980 status: {
981 shapeId: statusFieldShapeId,
982 docs: {
983 description: "Transport status code when available.",
984 },
985 },
986 },
987 required: ["data", "error", "headers", "status"],
988 additionalProperties: false,
989 },
990 diagnostic: {
991 level: "info",
992 code: "projection_result_shape_synthesized",
993 message: `Synthesized execution result envelope for ${capability.id}`,
994 relatedSymbolIds: unique([
995 dataFieldShapeId,
996 errorFieldShapeId,
997 headersShapeId,
998 statusFieldShapeId,
999 ]),
1000 },
1001 });
1002};
1003
695const groupFieldShape = (1004const groupFieldShape = (
696 catalog: CatalogV1,1005 catalog: CatalogV1,
697 capability: Capability,1006 capability: Capability,
@@ -1073,15 +1382,15 @@ const projectCapability = ( · 375 unmodified lines
1073 switch (executable.protocol) {1382 switch (executable.protocol) {
1074 case "http":1383 case "http":
1075 callShapeId = projectHttpCallShape(catalog, capability, executable);1384 callShapeId = projectHttpCallShape(catalog, capability, executable);
1076 resultShapeId = projectResultShapeFromResponses(catalog, capability, responseSet);1385 resultShapeId = projectExecutionResultShape(catalog, capability, executable, responseSet);
1077 break;1386 break;
1078 case "graphql":1387 case "graphql":
1079 callShapeId = projectGraphqlCallShape(catalog, capability, executable);1388 callShapeId = projectGraphqlCallShape(catalog, capability, executable);
1080 resultShapeId = executable.resultShapeId;1389 resultShapeId = projectExecutionResultShape(catalog, capability, executable, responseSet);
1081 break;1390 break;
1082 case "mcp":1391 case "mcp":
1083 callShapeId = projectMcpCallShape(catalog, capability, executable);1392 callShapeId = projectMcpCallShape(catalog, capability, executable);
1084 resultShapeId = executable.outputShapeId;1393 resultShapeId = projectExecutionResultShape(catalog, capability, executable, responseSet);
1085 break;1394 break;
1086 }1395 }
1087 1396
packages/control-plane/src/runtime/ir-execution.ts+47−29
@@ -65,15 +65,11 @@ const readSourceTransport = (source: Source): "auto" | "streamable-http" | "sse" · 64 unmodified lines
65 return undefined;65 return undefined;
66};66};
67 67
68const summarizeHttpBody = (body: unknown): string => {68type ExecutionEnvelope = {
69 if (typeof body === "string") {69 data: unknown;
70 return body.slice(0, 400);70 error: unknown;
71 }71 headers: Record<string, string>;
72 try {72 status: number | null;
73 return JSON.stringify(body).slice(0, 400);
74 } catch {
75 return String(body);
76 }
77};73};
78 74
79const decodeFetchBody = async (response: Response): Promise<unknown> => {75const decodeFetchBody = async (response: Response): Promise<unknown> => {
@@ -87,6 +83,16 @@ const decodeFetchBody = async (response: Response): Promise<unknown> => { · 7 unmodified lines
87 return response.text();83 return response.text();
88};84};
89 85
86const responseHeadersRecord = (response: Response): Record<string, string> => {
87 const headers: Record<string, string> = {};
88 response.headers.forEach((value, key) => {
89 headers[key] = value;
90 });
91 return headers;
92};
93
94const executionEnvelope = (input: ExecutionEnvelope): ExecutionEnvelope => input;
95
90const parameterById = (catalog: CatalogV1, parameterId: string): ParameterSymbol | undefined => {96const parameterById = (catalog: CatalogV1, parameterId: string): ParameterSymbol | undefined => {
91 const symbol = catalog.symbols[parameterId];97 const symbol = catalog.symbols[parameterId];
92 return symbol?.kind === "parameter" ? symbol : undefined;98 return symbol?.kind === "parameter" ? symbol : undefined;
@@ -340,11 +346,12 @@ const executeHttp = (input: { · 247 unmodified lines
340 ...(body !== undefined ? { body } : {}),346 ...(body !== undefined ? { body } : {}),
341 });347 });
342 const responseBody = await decodeFetchBody(response);348 const responseBody = await decodeFetchBody(response);
343 if (!response.ok) {349 return executionEnvelope({
344 throw new Error(`HTTP ${response.status}: ${summarizeHttpBody(responseBody)}`);350 data: response.ok ? responseBody : null,
345 }351 error: response.ok ? null : responseBody,
346 352 headers: responseHeadersRecord(response),
347 return responseBody;353 status: response.status,
354 });
348 },355 },
349 catch: (cause) => cause instanceof Error ? cause : new Error(String(cause)),356 catch: (cause) => cause instanceof Error ? cause : new Error(String(cause)),
350 });357 });
@@ -394,7 +401,9 @@ const executeGraphql = (input: { · 43 unmodified lines
394 Effect.tryPromise({401 Effect.tryPromise({
395 try: async () => {402 try: async () => {
396 const argsRecord = asObject(input.args);403 const argsRecord = asObject(input.args);
397 const native = input.executable.native?.[0]?.value as Record<string, unknown> | undefined;404 const native = input.executable.native
405 ?.find((blob) => blob.kind === "graphql_provider_data")
406 ?.value as Record<string, unknown> | undefined;
398 const requestHeaders = {407 const requestHeaders = {
399 ...readSourceHeaders(input.source),408 ...readSourceHeaders(input.source),
400 ...input.auth.headers,409 ...input.auth.headers,
@@ -430,14 +439,19 @@ const executeGraphql = (input: { · 29 unmodified lines
430 }),439 }),
431 });440 });
432 const body = await decodeFetchBody(response);441 const body = await decodeFetchBody(response);
433 if (!response.ok) {442 const bodyRecord = asObject(body);
434 throw new Error(`GraphQL HTTP ${response.status}: ${summarizeHttpBody(body)}`);443 const errors = Array.isArray(bodyRecord.errors) ? bodyRecord.errors : [];
435 }444 return executionEnvelope({
436 return body;445 data: body,
446 error: errors.length > 0 ? errors : (response.status >= 400 ? body : null),
447 headers: responseHeadersRecord(response),
448 status: response.status,
449 });
437 }450 }
438 451
439 const operationDocument = asString(native.operationDocument)!;452 const operationDocument = asString(native.operationDocument)!;
440 const operationName = asString(native.operationName) ?? undefined;453 const operationName = asString(native.operationName) ?? undefined;
454 const fieldName = asString(native.fieldName) ?? input.executable.rootField;
441 const variables = graphqlArgsPayload({455 const variables = graphqlArgsPayload({
442 catalog: input.catalog,456 catalog: input.catalog,
443 executable: input.executable,457 executable: input.executable,
@@ -468,18 +482,14 @@ const executeGraphql = (input: { · 24 unmodified lines
468 }),482 }),
469 });483 });
470 const body = asObject(await decodeFetchBody(response));484 const body = asObject(await decodeFetchBody(response));
471 if (!response.ok) {
472 throw new Error(`GraphQL HTTP ${response.status}: ${summarizeHttpBody(body)}`);
473 }
474
475 const errors = Array.isArray(body.errors) ? body.errors : [];485 const errors = Array.isArray(body.errors) ? body.errors : [];
476 const data = asObject(body.data);486 const data = asObject(body.data);
477 const fieldName = asString(native.fieldName) ?? input.executable.rootField;487 return executionEnvelope({
478 return {
479 data: data[fieldName] ?? null,488 data: data[fieldName] ?? null,
480 errors,489 error: errors.length > 0 ? errors : (response.status >= 400 ? body : null),
481 isError: errors.length > 0,490 headers: responseHeadersRecord(response),
482 };491 status: response.status,
492 });
483 },493 },
484 catch: (cause) => cause instanceof Error ? cause : new Error(String(cause)),494 catch: (cause) => cause instanceof Error ? cause : new Error(String(cause)),
485 });495 });
@@ -572,7 +582,15 @@ const executeMcp = (input: { · 86 unmodified lines
572 }582 }
573 : undefined;583 : undefined;
574 584
575 return await definition.execute(asObject(payload), executionContext);585 const result = await definition.execute(asObject(payload), executionContext);
586 const resultRecord = asObject(result);
587 const isError = resultRecord.isError === true;
588 return executionEnvelope({
589 data: isError ? null : (result ?? null),
590 error: isError ? result : null,
591 headers: {},
592 status: null,
593 });
576 },594 },
577 catch: (cause) => cause instanceof Error ? cause : new Error(String(cause)),595 catch: (cause) => cause instanceof Error ? cause : new Error(String(cause)),
578 });596 });
packages/control-plane/src/runtime/local-source-artifacts.test.ts+4−0
@@ -109,8 +109,11 @@ describe("local-source-artifacts", () => { · 108 unmodified lines
109 const persistedArtifact = JSON.parse(readFileSync(path, "utf8"));109 const persistedArtifact = JSON.parse(readFileSync(path, "utf8"));
110 const persistedDocument = Object.values(persistedArtifact.snapshot.catalog.documents)[0] as {110 const persistedDocument = Object.values(persistedArtifact.snapshot.catalog.documents)[0] as {
111 native?: ReadonlyArray<{ kind?: string }>;111 native?: ReadonlyArray<{ kind?: string }>;
112 provenance?: unknown;
112 };113 };
113 expect(persistedDocument.native?.some((blob) => blob.kind === "source_document") ?? false).toBe(false);114 expect(persistedDocument.native?.some((blob) => blob.kind === "source_document") ?? false).toBe(false);
115 expect(persistedDocument.provenance).toBeUndefined();
116 expect(Object.keys(persistedArtifact.snapshot.catalog.diagnostics)).toHaveLength(0);
114 117
115 const decoded = yield* readLocalSourceArtifact({118 const decoded = yield* readLocalSourceArtifact({
116 context,119 context,
@@ -120,6 +123,7 @@ describe("local-source-artifacts", () => { · 3 unmodified lines
120 expect(decoded?.snapshot.import.adapterKey).toBe("openapi");123 expect(decoded?.snapshot.import.adapterKey).toBe("openapi");
121 expect(decoded?.sourceId).toBe("src_test");124 expect(decoded?.sourceId).toBe("src_test");
122 expect((Object.values(decoded?.snapshot.catalog.documents ?? {})[0] as { native?: Array<{ value?: unknown }> })?.native?.[0]?.value).toBe('{"openapi":"3.1.0"}');125 expect((Object.values(decoded?.snapshot.catalog.documents ?? {})[0] as { native?: Array<{ value?: unknown }> })?.native?.[0]?.value).toBe('{"openapi":"3.1.0"}');
126 expect((Object.values(decoded?.snapshot.catalog.documents ?? {})[0] as { provenance?: unknown }).provenance).toBeUndefined();
123 }).pipe(Effect.provide(NodeFileSystem.layer)),127 }).pipe(Effect.provide(NodeFileSystem.layer)),
124 );128 );
125 129
packages/control-plane/src/runtime/local-source-artifacts.ts+110−1
@@ -16,6 +16,7 @@ import * as Schema from "effect/Schema"; · 15 unmodified lines
16import {16import {
17 CatalogSnapshotV1Schema,17 CatalogSnapshotV1Schema,
18 type NativeBlob,18 type NativeBlob,
19 type ShapeNode,
19 type SourceDocument,20 type SourceDocument,
20} from "../ir/model";21} from "../ir/model";
21import type { SourceCatalogSyncResult } from "./source-catalog-support";22import type { SourceCatalogSyncResult } from "./source-catalog-support";
@@ -57,6 +58,10 @@ const mapFileSystemError = (path: string, action: string) => (cause: unknown) => · 35 unmodified lines
57const mutableRecord = <K extends string, V>(value: Readonly<Record<K, V>>): Record<K, V> =>58const mutableRecord = <K extends string, V>(value: Readonly<Record<K, V>>): Record<K, V> =>
58 value as Record<K, V>;59 value as Record<K, V>;
59 60
61const clearProvenance = (value: unknown): void => {
62 (value as { provenance: Array<unknown> }).provenance = [];
63};
64
60const localSourceArtifactPath = (input: {65const localSourceArtifactPath = (input: {
61 context: ResolvedLocalWorkspaceContext;66 context: ResolvedLocalWorkspaceContext;
62 sourceId: string;67 sourceId: string;
@@ -154,6 +159,109 @@ const hydrateArtifactSourceDocuments = (input: { · 91 unmodified lines
154 return nextArtifact;159 return nextArtifact;
155};160};
156 161
162const stripShapeFieldDocs = (node: ShapeNode): void => {
163 switch (node.type) {
164 case "object":
165 case "graphqlInterface":
166 for (const field of Object.values(node.fields)) {
167 delete (field as { docs?: unknown }).docs;
168 }
169 return;
170 default:
171 return;
172 }
173};
174
175const stripRuntimeMetadata = (artifact: LocalSourceArtifact): LocalSourceArtifact => {
176 const nextArtifact = structuredClone(artifact);
177
178 for (const document of Object.values(nextArtifact.snapshot.catalog.documents)) {
179 delete (document as { provenance?: unknown }).provenance;
180 delete (document as { docs?: unknown }).docs;
181 delete (document as { native?: unknown }).native;
182 }
183
184 for (const resource of Object.values(nextArtifact.snapshot.catalog.resources)) {
185 clearProvenance(resource as unknown);
186 delete (resource as { docs?: unknown }).docs;
187 delete (resource as { native?: unknown }).native;
188 }
189
190 for (const scope of Object.values(nextArtifact.snapshot.catalog.scopes)) {
191 clearProvenance(scope as unknown);
192 delete (scope as { docs?: unknown }).docs;
193 delete (scope as { native?: unknown }).native;
194 }
195
196 for (const capability of Object.values(nextArtifact.snapshot.catalog.capabilities)) {
197 clearProvenance(capability as unknown);
198 delete (capability as { native?: unknown }).native;
199 delete (capability as { exampleIds?: unknown }).exampleIds;
200 }
201
202 for (const executable of Object.values(nextArtifact.snapshot.catalog.executables)) {
203 clearProvenance(executable as unknown);
204 if (Array.isArray((executable as { native?: NativeBlob[] }).native)) {
205 const keptNative = ((executable as { native?: NativeBlob[] }).native ?? []).filter((blob) =>
206 blob.kind === "google_discovery_provider_data"
207 );
208 if (keptNative.length > 0) {
209 (executable as { native?: NativeBlob[] }).native = keptNative;
210 } else {
211 delete (executable as { native?: unknown }).native;
212 }
213 }
214 }
215
216 for (const responseSet of Object.values(nextArtifact.snapshot.catalog.responseSets)) {
217 clearProvenance(responseSet as unknown);
218 delete (responseSet as { native?: unknown }).native;
219 }
220
221 for (const diagnostic of Object.values(nextArtifact.snapshot.catalog.diagnostics)) {
222 delete (diagnostic as { provenance?: unknown }).provenance;
223 }
224
225 const mutableSymbols = mutableRecord(nextArtifact.snapshot.catalog.symbols);
226 for (const [symbolId, symbol] of Object.entries(mutableSymbols)) {
227 if (symbol.kind === "example") {
228 delete mutableSymbols[symbolId as keyof typeof mutableSymbols];
229 continue;
230 }
231
232 clearProvenance(symbol as unknown);
233 delete (symbol as { docs?: unknown }).docs;
234 delete (symbol as { diagnosticIds?: unknown }).diagnosticIds;
235 delete (symbol as { native?: unknown }).native;
236 delete (symbol as { deprecated?: unknown }).deprecated;
237
238 switch (symbol.kind) {
239 case "shape":
240 stripShapeFieldDocs(symbol.node);
241 break;
242 case "parameter":
243 case "header":
244 case "response":
245 delete (symbol as { exampleIds?: unknown }).exampleIds;
246 if ("content" in symbol && Array.isArray(symbol.content)) {
247 for (const content of symbol.content) {
248 delete (content as { exampleIds?: unknown }).exampleIds;
249 }
250 }
251 break;
252 case "requestBody":
253 for (const content of symbol.contents) {
254 delete (content as { exampleIds?: unknown }).exampleIds;
255 }
256 break;
257 }
258 }
259
260 mutableRecord(nextArtifact.snapshot.catalog).diagnostics = {};
261
262 return nextArtifact;
263};
264
157const snapshotHash = (snapshot: SourceCatalogSyncResult["snapshot"]): string =>265const snapshotHash = (snapshot: SourceCatalogSyncResult["snapshot"]): string =>
158 contentHash(JSON.stringify(snapshot));266 contentHash(JSON.stringify(snapshot));
159 267
@@ -281,6 +389,7 @@ export const writeLocalSourceArtifact = (input: { · 121 unmodified lines
281 const legacyPath = legacyLocalSourceArtifactPath(input);389 const legacyPath = legacyLocalSourceArtifactPath(input);
282 const sourceDocumentDirectory = localSourceDocumentDirectory(input);390 const sourceDocumentDirectory = localSourceDocumentDirectory(input);
283 const split = splitArtifactSourceDocuments(input.artifact);391 const split = splitArtifactSourceDocuments(input.artifact);
392 const runtimeArtifact = stripRuntimeMetadata(split.artifact);
284 yield* fs.makeDirectory(directory, { recursive: true }).pipe(393 yield* fs.makeDirectory(directory, { recursive: true }).pipe(
285 Effect.mapError(mapFileSystemError(directory, "create source artifact directory")),394 Effect.mapError(mapFileSystemError(directory, "create source artifact directory")),
286 );395 );
@@ -302,7 +411,7 @@ export const writeLocalSourceArtifact = (input: { · 15 unmodified lines
302 );411 );
303 }412 }
304 }413 }
305 yield* fs.writeFileString(path, `${JSON.stringify(split.artifact)}\n`).pipe(414 yield* fs.writeFileString(path, `${JSON.stringify(runtimeArtifact)}\n`).pipe(
306 Effect.mapError(mapFileSystemError(path, "write source artifact")),415 Effect.mapError(mapFileSystemError(path, "write source artifact")),
307 );416 );
308 const legacyExists = yield* fs.exists(legacyPath).pipe(417 const legacyExists = yield* fs.exists(legacyPath).pipe(
packages/control-plane/src/runtime/source-catalog-snapshot.test.ts+3−1
@@ -708,7 +708,9 @@ describe("source-catalog-snapshot", () => { · 707 unmodified lines
708 expect(executable.operationType).toBe("query");708 expect(executable.operationType).toBe("query");
709 expect(executable.rootField).toBe("viewer");709 expect(executable.rootField).toBe("viewer");
710 expect(executable.selectionMode).toBe("fixed");710 expect(executable.selectionMode).toBe("fixed");
711 expect(executable.native).toBeUndefined();711 expect(executable.native).toMatchObject([{
712 kind: "graphql_provider_data",
713 }]);
712 });714 });
713 715
714 it("projects MCP metadata into capability semantics", () => {716 it("projects MCP metadata into capability semantics", () => {
packages/control-plane/src/runtime/source-catalog-snapshot.ts+6−0
@@ -2009,6 +2009,12 @@ const createGraphqlCapability = (input: { · 2008 unmodified lines
2009 responseSetId,2009 responseSetId,
2010 synthetic: false,2010 synthetic: false,
2011 provenance: provenanceFor(input.documentId, `#/graphql/${input.operation.providerData.toolId}/executable`),2011 provenance: provenanceFor(input.documentId, `#/graphql/${input.operation.providerData.toolId}/executable`),
2012 native: [nativeBlob({
2013 source: input.source,
2014 kind: "graphql_provider_data",
2015 pointer: `#/graphql/${input.operation.providerData.toolId}/providerData`,
2016 value: input.operation.providerData,
2017 })],
2012 } satisfies GraphQLExecutable;2018 } satisfies GraphQLExecutable;
2013 2019
2014 const effect = input.operation.effect;2020 const effect = input.operation.effect;

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t