From b3a786e475bfb7028ac268d426567e1fc1561f4d Mon Sep 17 00:00:00 2001 From: Alberto Garcia Hierro Date: Wed, 12 Aug 2026 17:37:49 +0100 Subject: [PATCH 01/14] wip: expose prompt-to-query through MCP --- .../gen/proto/wg/cosmo/node/v1/node.pb.go | 19 +- connect/src/wg/cosmo/node/v1/node_pb.ts | 9 +- controlplane/src/core/composition/composer.ts | 7 +- .../src/core/services/PromptToQueryService.ts | 5 + controlplane/test/router-config.test.ts | 6 + .../router/mcp/oauth/configuration.mdx | 2 + docs-website/router/mcp/tools.mdx | 30 + mcp-yoko-run.sh | 766 ++++++++++++++++++ proto/wg/cosmo/node/v1/node.proto | 2 + router/core/graph_server.go | 2 +- router/core/router.go | 10 + router/core/router_config.go | 1 + router/core/supervisor_instance.go | 11 + router/gen/proto/wg/cosmo/node/v1/node.pb.go | 19 +- router/pkg/config/config.go | 3 + router/pkg/config/config.schema.json | 5 + .../pkg/config/testdata/config_defaults.json | 3 +- router/pkg/config/testdata/config_full.json | 3 +- .../pkg/controlplane/prompttoquery/client.go | 100 +++ .../controlplane/prompttoquery/client_test.go | 56 ++ router/pkg/mcpserver/auth_middleware.go | 2 + router/pkg/mcpserver/auth_middleware_test.go | 16 + router/pkg/mcpserver/discover_test.go | 2 +- router/pkg/mcpserver/server.go | 147 +++- router/pkg/mcpserver/server_test.go | 174 +++- 25 files changed, 1381 insertions(+), 19 deletions(-) create mode 100755 mcp-yoko-run.sh create mode 100644 router/pkg/controlplane/prompttoquery/client.go create mode 100644 router/pkg/controlplane/prompttoquery/client_test.go diff --git a/connect-go/gen/proto/wg/cosmo/node/v1/node.pb.go b/connect-go/gen/proto/wg/cosmo/node/v1/node.pb.go index b6a452657a..07b4bab53a 100644 --- a/connect-go/gen/proto/wg/cosmo/node/v1/node.pb.go +++ b/connect-go/gen/proto/wg/cosmo/node/v1/node.pb.go @@ -1198,8 +1198,10 @@ type EngineConfiguration struct { TypeConfigurations []*TypeConfiguration `protobuf:"bytes,5,rep,name=type_configurations,json=typeConfigurations,proto3" json:"type_configurations,omitempty"` StringStorage map[string]string `protobuf:"bytes,6,rep,name=string_storage,json=stringStorage,proto3" json:"string_storage,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` GraphqlClientSchema *string `protobuf:"bytes,7,opt,name=graphql_client_schema,json=graphqlClientSchema,proto3,oneof" json:"graphql_client_schema,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Content hash of graphqlSchema, formatted as "sha256:". + SchemaHash string `protobuf:"bytes,8,opt,name=schema_hash,json=schemaHash,proto3" json:"schema_hash,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *EngineConfiguration) Reset() { @@ -1281,6 +1283,13 @@ func (x *EngineConfiguration) GetGraphqlClientSchema() string { return "" } +func (x *EngineConfiguration) GetSchemaHash() string { + if x != nil { + return x.SchemaHash + } + return "" +} + type DataSourceConfiguration struct { state protoimpl.MessageState `protogen:"open.v1"` Kind DataSourceKind `protobuf:"varint,1,opt,name=kind,proto3,enum=wg.cosmo.node.v1.DataSourceKind" json:"kind,omitempty"` @@ -5197,7 +5206,7 @@ const file_wg_cosmo_node_v1_node_proto_rawDesc = "" + "\bdocument\x18\x02 \x01(\tR\bdocument\x12%\n" + "\x0eoperation_name\x18\x03 \x01(\tR\roperationName\x12O\n" + "\x0eoperation_type\x18\x04 \x01(\x0e2(.wg.cosmo.node.v1.SatisfiedOperationTypeR\roperationType\x12)\n" + - "\x10variables_schema\x18\x05 \x01(\tR\x0fvariablesSchema\"\xfc\x04\n" + + "\x10variables_schema\x18\x05 \x01(\tR\x0fvariablesSchema\"\x9d\x05\n" + "\x13EngineConfiguration\x122\n" + "\x14defaultFlushInterval\x18\x01 \x01(\x03R\x14defaultFlushInterval\x12f\n" + "\x19datasource_configurations\x18\x02 \x03(\v2).wg.cosmo.node.v1.DataSourceConfigurationR\x18datasourceConfigurations\x12W\n" + @@ -5205,7 +5214,9 @@ const file_wg_cosmo_node_v1_node_proto_rawDesc = "" + "\rgraphqlSchema\x18\x04 \x01(\tR\rgraphqlSchema\x12T\n" + "\x13type_configurations\x18\x05 \x03(\v2#.wg.cosmo.node.v1.TypeConfigurationR\x12typeConfigurations\x12_\n" + "\x0estring_storage\x18\x06 \x03(\v28.wg.cosmo.node.v1.EngineConfiguration.StringStorageEntryR\rstringStorage\x127\n" + - "\x15graphql_client_schema\x18\a \x01(\tH\x00R\x13graphqlClientSchema\x88\x01\x01\x1a@\n" + + "\x15graphql_client_schema\x18\a \x01(\tH\x00R\x13graphqlClientSchema\x88\x01\x01\x12\x1f\n" + + "\vschema_hash\x18\b \x01(\tR\n" + + "schemaHash\x1a@\n" + "\x12StringStorageEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01B\x18\n" + diff --git a/connect/src/wg/cosmo/node/v1/node_pb.ts b/connect/src/wg/cosmo/node/v1/node_pb.ts index 8bc656290f..5b7aae86f8 100644 --- a/connect/src/wg/cosmo/node/v1/node_pb.ts +++ b/connect/src/wg/cosmo/node/v1/node_pb.ts @@ -12,7 +12,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file wg/cosmo/node/v1/node.proto. */ export const file_wg_cosmo_node_v1_node: GenFile = /*@__PURE__*/ - fileDesc("Cht3Zy9jb3Ntby9ub2RlL3YxL25vZGUucHJvdG8SEHdnLmNvc21vLm5vZGUudjEiOQoIU3ViZ3JhcGgSCgoCaWQYASABKAkSDAoEbmFtZRgCIAEoCRITCgtyb3V0aW5nX3VybBgDIAEoCSKOAgohRmVhdHVyZUZsYWdSb3V0ZXJFeGVjdXRpb25Db25maWdzEnUKG2NvbmZpZ19ieV9mZWF0dXJlX2ZsYWdfbmFtZRgBIAMoCzJQLndnLmNvc21vLm5vZGUudjEuRmVhdHVyZUZsYWdSb3V0ZXJFeGVjdXRpb25Db25maWdzLkNvbmZpZ0J5RmVhdHVyZUZsYWdOYW1lRW50cnkacgocQ29uZmlnQnlGZWF0dXJlRmxhZ05hbWVFbnRyeRILCgNrZXkYASABKAkSQQoFdmFsdWUYAiABKAsyMi53Zy5jb3Ntby5ub2RlLnYxLkZlYXR1cmVGbGFnUm91dGVyRXhlY3V0aW9uQ29uZmlnOgI4ASKgAQogRmVhdHVyZUZsYWdSb3V0ZXJFeGVjdXRpb25Db25maWcSPAoNZW5naW5lX2NvbmZpZxgBIAEoCzIlLndnLmNvc21vLm5vZGUudjEuRW5naW5lQ29uZmlndXJhdGlvbhIPCgd2ZXJzaW9uGAIgASgJEi0KCXN1YmdyYXBocxgDIAMoCzIaLndnLmNvc21vLm5vZGUudjEuU3ViZ3JhcGginAIKDFJvdXRlckNvbmZpZxI8Cg1lbmdpbmVfY29uZmlnGAEgASgLMiUud2cuY29zbW8ubm9kZS52MS5FbmdpbmVDb25maWd1cmF0aW9uEg8KB3ZlcnNpb24YAiABKAkSLQoJc3ViZ3JhcGhzGAMgAygLMhoud2cuY29zbW8ubm9kZS52MS5TdWJncmFwaBJWChRmZWF0dXJlX2ZsYWdfY29uZmlncxgEIAEoCzIzLndnLmNvc21vLm5vZGUudjEuRmVhdHVyZUZsYWdSb3V0ZXJFeGVjdXRpb25Db25maWdzSACIAQESHQoVY29tcGF0aWJpbGl0eV92ZXJzaW9uGAUgASgJQhcKFV9mZWF0dXJlX2ZsYWdfY29uZmlncyJbCghSZXNwb25zZRItCgRjb2RlGAEgASgOMh8ud2cuY29zbW8uY29tbW9uLkVudW1TdGF0dXNDb2RlEhQKB2RldGFpbHMYAiABKAlIAIgBAUIKCghfZGV0YWlscyIvCg5SZXNwb25zZVN0YXR1cxIMCgRjb2RlGAEgASgFEg8KB21lc3NhZ2UYAiABKAkiZQoQUmVnaXN0cmF0aW9uSW5mbxI3Cg5hY2NvdW50X2xpbWl0cxgBIAEoCzIfLndnLmNvc21vLm5vZGUudjEuQWNjb3VudExpbWl0cxIYChBncmFwaF9wdWJsaWNfa2V5GAIgASgJIiwKDUFjY291bnRMaW1pdHMSGwoTdHJhY2Vfc2FtcGxpbmdfcmF0ZRgBIAEoAiIVChNTZWxmUmVnaXN0ZXJSZXF1ZXN0IpwBChRTZWxmUmVnaXN0ZXJSZXNwb25zZRIsCghyZXNwb25zZRgBIAEoCzIaLndnLmNvc21vLm5vZGUudjEuUmVzcG9uc2USQQoQcmVnaXN0cmF0aW9uSW5mbxgCIAEoCzIiLndnLmNvc21vLm5vZGUudjEuUmVnaXN0cmF0aW9uSW5mb0gAiAEBQhMKEV9yZWdpc3RyYXRpb25JbmZvIjsKFEdlbmVyYXRlUXVlcnlSZXF1ZXN0EhMKC3NjaGVtYV9oYXNoGAEgASgJEg4KBnByb21wdBgCIAEoCSKFAQoVR2VuZXJhdGVRdWVyeVJlc3BvbnNlEiwKCHJlc3BvbnNlGAEgASgLMhoud2cuY29zbW8ubm9kZS52MS5SZXNwb25zZRI0CgVxdWVyeRgCIAEoCzIgLndnLmNvc21vLm5vZGUudjEuU2F0aXNmaWVkUXVlcnlIAIgBAUIICgZfcXVlcnkiqwEKDlNhdGlzZmllZFF1ZXJ5EhMKC2Rlc2NyaXB0aW9uGAEgASgJEhAKCGRvY3VtZW50GAIgASgJEhYKDm9wZXJhdGlvbl9uYW1lGAMgASgJEkAKDm9wZXJhdGlvbl90eXBlGAQgASgOMigud2cuY29zbW8ubm9kZS52MS5TYXRpc2ZpZWRPcGVyYXRpb25UeXBlEhgKEHZhcmlhYmxlc19zY2hlbWEYBSABKAki5AMKE0VuZ2luZUNvbmZpZ3VyYXRpb24SHAoUZGVmYXVsdEZsdXNoSW50ZXJ2YWwYASABKAMSTAoZZGF0YXNvdXJjZV9jb25maWd1cmF0aW9ucxgCIAMoCzIpLndnLmNvc21vLm5vZGUudjEuRGF0YVNvdXJjZUNvbmZpZ3VyYXRpb24SQgoUZmllbGRfY29uZmlndXJhdGlvbnMYAyADKAsyJC53Zy5jb3Ntby5ub2RlLnYxLkZpZWxkQ29uZmlndXJhdGlvbhIVCg1ncmFwaHFsU2NoZW1hGAQgASgJEkAKE3R5cGVfY29uZmlndXJhdGlvbnMYBSADKAsyIy53Zy5jb3Ntby5ub2RlLnYxLlR5cGVDb25maWd1cmF0aW9uElAKDnN0cmluZ19zdG9yYWdlGAYgAygLMjgud2cuY29zbW8ubm9kZS52MS5FbmdpbmVDb25maWd1cmF0aW9uLlN0cmluZ1N0b3JhZ2VFbnRyeRIiChVncmFwaHFsX2NsaWVudF9zY2hlbWEYByABKAlIAIgBARo0ChJTdHJpbmdTdG9yYWdlRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4AUIYChZfZ3JhcGhxbF9jbGllbnRfc2NoZW1hIsYHChdEYXRhU291cmNlQ29uZmlndXJhdGlvbhIuCgRraW5kGAEgASgOMiAud2cuY29zbW8ubm9kZS52MS5EYXRhU291cmNlS2luZBIvCgpyb290X25vZGVzGAIgAygLMhsud2cuY29zbW8ubm9kZS52MS5UeXBlRmllbGQSMAoLY2hpbGRfbm9kZXMYAyADKAsyGy53Zy5jb3Ntby5ub2RlLnYxLlR5cGVGaWVsZBImCh5vdmVycmlkZV9maWVsZF9wYXRoX2Zyb21fYWxpYXMYBCABKAgSQgoOY3VzdG9tX2dyYXBocWwYBSABKAsyKi53Zy5jb3Ntby5ub2RlLnYxLkRhdGFTb3VyY2VDdXN0b21fR3JhcGhRTBJACg1jdXN0b21fc3RhdGljGAYgASgLMikud2cuY29zbW8ubm9kZS52MS5EYXRhU291cmNlQ3VzdG9tX1N0YXRpYxI8CgpkaXJlY3RpdmVzGAcgAygLMigud2cuY29zbW8ubm9kZS52MS5EaXJlY3RpdmVDb25maWd1cmF0aW9uEh8KF3JlcXVlc3RfdGltZW91dF9zZWNvbmRzGAggASgDEgoKAmlkGAkgASgJEi0KBGtleXMYCiADKAsyHy53Zy5jb3Ntby5ub2RlLnYxLlJlcXVpcmVkRmllbGQSMQoIcHJvdmlkZXMYCyADKAsyHy53Zy5jb3Ntby5ub2RlLnYxLlJlcXVpcmVkRmllbGQSMQoIcmVxdWlyZXMYDCADKAsyHy53Zy5jb3Ntby5ub2RlLnYxLlJlcXVpcmVkRmllbGQSPwoNY3VzdG9tX2V2ZW50cxgNIAEoCzIoLndnLmNvc21vLm5vZGUudjEuRGF0YVNvdXJjZUN1c3RvbUV2ZW50cxJJChFlbnRpdHlfaW50ZXJmYWNlcxgOIAMoCzIuLndnLmNvc21vLm5vZGUudjEuRW50aXR5SW50ZXJmYWNlQ29uZmlndXJhdGlvbhJJChFpbnRlcmZhY2Vfb2JqZWN0cxgPIAMoCzIuLndnLmNvc21vLm5vZGUudjEuRW50aXR5SW50ZXJmYWNlQ29uZmlndXJhdGlvbhI/ChJjb3N0X2NvbmZpZ3VyYXRpb24YECABKAsyIy53Zy5jb3Ntby5ub2RlLnYxLkNvc3RDb25maWd1cmF0aW9uElIKHGVudGl0eV9jYWNoaW5nX2NvbmZpZ3VyYXRpb24YESABKAsyLC53Zy5jb3Ntby5ub2RlLnYxLkVudGl0eUNhY2hpbmdDb25maWd1cmF0aW9uIpsCChpFbnRpdHlDYWNoaW5nQ29uZmlndXJhdGlvbhJPChtlbnRpdHlfY2FjaGVfY29uZmlndXJhdGlvbnMYASADKAsyKi53Zy5jb3Ntby5ub2RlLnYxLkVudGl0eUNhY2hlQ29uZmlndXJhdGlvbhJXCh9jYWNoZV9pbnZhbGlkYXRlX2NvbmZpZ3VyYXRpb25zGAIgAygLMi4ud2cuY29zbW8ubm9kZS52MS5DYWNoZUludmFsaWRhdGVDb25maWd1cmF0aW9uElMKHWNhY2hlX3BvcHVsYXRlX2NvbmZpZ3VyYXRpb25zGAMgAygLMiwud2cuY29zbW8ubm9kZS52MS5DYWNoZVBvcHVsYXRlQ29uZmlndXJhdGlvbiK1AQoYRW50aXR5Q2FjaGVDb25maWd1cmF0aW9uEhEKCXR5cGVfbmFtZRgBIAEoCRIXCg9tYXhfYWdlX3NlY29uZHMYAiABKAMSFwoPaW5jbHVkZV9oZWFkZXJzGAMgASgIEhoKEnBhcnRpYWxfY2FjaGVfbG9hZBgEIAEoCBITCgtzaGFkb3dfbW9kZRgFIAEoCBIjChtub3RfZm91bmRfY2FjaGVfdHRsX3NlY29uZHMYBiABKAMiZAocQ2FjaGVJbnZhbGlkYXRlQ29uZmlndXJhdGlvbhISCgpmaWVsZF9uYW1lGAEgASgJEhYKDm9wZXJhdGlvbl90eXBlGAIgASgJEhgKEGVudGl0eV90eXBlX25hbWUYAyABKAkiewoaQ2FjaGVQb3B1bGF0ZUNvbmZpZ3VyYXRpb24SEgoKZmllbGRfbmFtZRgBIAEoCRIWCg5vcGVyYXRpb25fdHlwZRgCIAEoCRIXCg9tYXhfYWdlX3NlY29uZHMYAyABKAMSGAoQZW50aXR5X3R5cGVfbmFtZRgEIAEoCSLAAwoRQ29zdENvbmZpZ3VyYXRpb24SQQoNZmllbGRfd2VpZ2h0cxgBIAMoCzIqLndnLmNvc21vLm5vZGUudjEuRmllbGRXZWlnaHRDb25maWd1cmF0aW9uEkAKCmxpc3Rfc2l6ZXMYAiADKAsyLC53Zy5jb3Ntby5ub2RlLnYxLkZpZWxkTGlzdFNpemVDb25maWd1cmF0aW9uEkoKDHR5cGVfd2VpZ2h0cxgDIAMoCzI0LndnLmNvc21vLm5vZGUudjEuQ29zdENvbmZpZ3VyYXRpb24uVHlwZVdlaWdodHNFbnRyeRJlChpkaXJlY3RpdmVfYXJndW1lbnRfd2VpZ2h0cxgEIAMoCzJBLndnLmNvc21vLm5vZGUudjEuQ29zdENvbmZpZ3VyYXRpb24uRGlyZWN0aXZlQXJndW1lbnRXZWlnaHRzRW50cnkaMgoQVHlwZVdlaWdodHNFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAU6AjgBGj8KHURpcmVjdGl2ZUFyZ3VtZW50V2VpZ2h0c0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoBToCOAEiowMKGEZpZWxkV2VpZ2h0Q29uZmlndXJhdGlvbhIRCgl0eXBlX25hbWUYASABKAkSEgoKZmllbGRfbmFtZRgCIAEoCRITCgZ3ZWlnaHQYAyABKAVIAIgBARJZChBhcmd1bWVudF93ZWlnaHRzGAQgAygLMj8ud2cuY29zbW8ubm9kZS52MS5GaWVsZFdlaWdodENvbmZpZ3VyYXRpb24uQXJndW1lbnRXZWlnaHRzRW50cnkSbAoaZGlyZWN0aXZlX2FyZ3VtZW50X3dlaWdodHMYBSADKAsySC53Zy5jb3Ntby5ub2RlLnYxLkZpZWxkV2VpZ2h0Q29uZmlndXJhdGlvbi5EaXJlY3RpdmVBcmd1bWVudFdlaWdodHNFbnRyeRo2ChRBcmd1bWVudFdlaWdodHNFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAU6AjgBGj8KHURpcmVjdGl2ZUFyZ3VtZW50V2VpZ2h0c0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoBToCOAFCCQoHX3dlaWdodCLsAQoaRmllbGRMaXN0U2l6ZUNvbmZpZ3VyYXRpb24SEQoJdHlwZV9uYW1lGAEgASgJEhIKCmZpZWxkX25hbWUYAiABKAkSGQoMYXNzdW1lZF9zaXplGAMgASgFSACIAQESGQoRc2xpY2luZ19hcmd1bWVudHMYBCADKAkSFAoMc2l6ZWRfZmllbGRzGAUgAygJEikKHHJlcXVpcmVfb25lX3NsaWNpbmdfYXJndW1lbnQYBiABKAhIAYgBAUIPCg1fYXNzdW1lZF9zaXplQh8KHV9yZXF1aXJlX29uZV9zbGljaW5nX2FyZ3VtZW50IlwKFUFyZ3VtZW50Q29uZmlndXJhdGlvbhIMCgRuYW1lGAEgASgJEjUKC3NvdXJjZV90eXBlGAIgASgOMiAud2cuY29zbW8ubm9kZS52MS5Bcmd1bWVudFNvdXJjZSIlCgZTY29wZXMSGwoTcmVxdWlyZWRfYW5kX3Njb3BlcxgBIAMoCSKvAQoaQXV0aG9yaXphdGlvbkNvbmZpZ3VyYXRpb24SHwoXcmVxdWlyZXNfYXV0aGVudGljYXRpb24YASABKAgSNAoScmVxdWlyZWRfb3Jfc2NvcGVzGAIgAygLMhgud2cuY29zbW8ubm9kZS52MS5TY29wZXMSOgoYcmVxdWlyZWRfb3Jfc2NvcGVzX2J5X29yGAMgAygLMhgud2cuY29zbW8ubm9kZS52MS5TY29wZXMi1QIKEkZpZWxkQ29uZmlndXJhdGlvbhIRCgl0eXBlX25hbWUYASABKAkSEgoKZmllbGRfbmFtZRgCIAEoCRJIChdhcmd1bWVudHNfY29uZmlndXJhdGlvbhgDIAMoCzInLndnLmNvc21vLm5vZGUudjEuQXJndW1lbnRDb25maWd1cmF0aW9uElEKG2F1dGhvcml6YXRpb25fY29uZmlndXJhdGlvbhgEIAEoCzIsLndnLmNvc21vLm5vZGUudjEuQXV0aG9yaXphdGlvbkNvbmZpZ3VyYXRpb24SWQodc3Vic2NyaXB0aW9uX2ZpbHRlcl9jb25kaXRpb24YBSABKAsyLS53Zy5jb3Ntby5ub2RlLnYxLlN1YnNjcmlwdGlvbkZpbHRlckNvbmRpdGlvbkgAiAEBQiAKHl9zdWJzY3JpcHRpb25fZmlsdGVyX2NvbmRpdGlvbiI5ChFUeXBlQ29uZmlndXJhdGlvbhIRCgl0eXBlX25hbWUYASABKAkSEQoJcmVuYW1lX3RvGAIgASgJInwKCVR5cGVGaWVsZBIRCgl0eXBlX25hbWUYASABKAkSEwoLZmllbGRfbmFtZXMYAiADKAkSHAoUZXh0ZXJuYWxfZmllbGRfbmFtZXMYAyADKAkSKQohcmVxdWlyZV9mZXRjaF9yZWFzb25zX2ZpZWxkX25hbWVzGAQgAygJIjkKEEZpZWxkQ29vcmRpbmF0ZXMSEgoKZmllbGRfbmFtZRgBIAEoCRIRCgl0eXBlX25hbWUYAiABKAkiawoRRmllbGRTZXRDb25kaXRpb24SQgoWZmllbGRfY29vcmRpbmF0ZXNfcGF0aBgBIAMoCzIiLndnLmNvc21vLm5vZGUudjEuRmllbGRDb29yZGluYXRlcxISCgpmaWVsZF9wYXRoGAIgAygJIqcBCg1SZXF1aXJlZEZpZWxkEhEKCXR5cGVfbmFtZRgBIAEoCRISCgpmaWVsZF9uYW1lGAIgASgJEhUKDXNlbGVjdGlvbl9zZXQYAyABKAkSHwoXZGlzYWJsZV9lbnRpdHlfcmVzb2x2ZXIYBCABKAgSNwoKY29uZGl0aW9ucxgFIAMoCzIjLndnLmNvc21vLm5vZGUudjEuRmllbGRTZXRDb25kaXRpb24iWAocRW50aXR5SW50ZXJmYWNlQ29uZmlndXJhdGlvbhIbChNpbnRlcmZhY2VfdHlwZV9uYW1lGAEgASgJEhsKE2NvbmNyZXRlX3R5cGVfbmFtZXMYAiADKAkijQUKEkZldGNoQ29uZmlndXJhdGlvbhI0CgN1cmwYASABKAsyJy53Zy5jb3Ntby5ub2RlLnYxLkNvbmZpZ3VyYXRpb25WYXJpYWJsZRIsCgZtZXRob2QYAiABKA4yHC53Zy5jb3Ntby5ub2RlLnYxLkhUVFBNZXRob2QSQAoGaGVhZGVyGAMgAygLMjAud2cuY29zbW8ubm9kZS52MS5GZXRjaENvbmZpZ3VyYXRpb24uSGVhZGVyRW50cnkSNQoEYm9keRgEIAEoCzInLndnLmNvc21vLm5vZGUudjEuQ29uZmlndXJhdGlvblZhcmlhYmxlEjYKBXF1ZXJ5GAUgAygLMicud2cuY29zbW8ubm9kZS52MS5VUkxRdWVyeUNvbmZpZ3VyYXRpb24SFwoPdXJsX2VuY29kZV9ib2R5GAcgASgIEjEKBG10bHMYCCABKAsyIy53Zy5jb3Ntby5ub2RlLnYxLk1UTFNDb25maWd1cmF0aW9uEjkKCGJhc2VfdXJsGAkgASgLMicud2cuY29zbW8ubm9kZS52MS5Db25maWd1cmF0aW9uVmFyaWFibGUSNQoEcGF0aBgKIAEoCzInLndnLmNvc21vLm5vZGUudjEuQ29uZmlndXJhdGlvblZhcmlhYmxlEkQKDmh0dHBfcHJveHlfdXJsGAsgASgLMicud2cuY29zbW8ubm9kZS52MS5Db25maWd1cmF0aW9uVmFyaWFibGVIAIgBARpLCgtIZWFkZXJFbnRyeRILCgNrZXkYASABKAkSKwoFdmFsdWUYAiABKAsyHC53Zy5jb3Ntby5ub2RlLnYxLkhUVFBIZWFkZXI6AjgBQhEKD19odHRwX3Byb3h5X3VybCJlChVTdGF0dXNDb2RlVHlwZU1hcHBpbmcSEwoLc3RhdHVzX2NvZGUYASABKAMSEQoJdHlwZV9uYW1lGAIgASgJEiQKHGluamVjdF9zdGF0dXNfY29kZV9pbnRvX2JvZHkYAyABKAgikwMKGERhdGFTb3VyY2VDdXN0b21fR3JhcGhRTBIzCgVmZXRjaBgBIAEoCzIkLndnLmNvc21vLm5vZGUudjEuRmV0Y2hDb25maWd1cmF0aW9uEkgKDHN1YnNjcmlwdGlvbhgCIAEoCzIyLndnLmNvc21vLm5vZGUudjEuR3JhcGhRTFN1YnNjcmlwdGlvbkNvbmZpZ3VyYXRpb24SRAoKZmVkZXJhdGlvbhgDIAEoCzIwLndnLmNvc21vLm5vZGUudjEuR3JhcGhRTEZlZGVyYXRpb25Db25maWd1cmF0aW9uEjkKD3Vwc3RyZWFtX3NjaGVtYRgEIAEoCzIgLndnLmNvc21vLm5vZGUudjEuSW50ZXJuZWRTdHJpbmcSRAoZY3VzdG9tX3NjYWxhcl90eXBlX2ZpZWxkcxgGIAMoCzIhLndnLmNvc21vLm5vZGUudjEuU2luZ2xlVHlwZUZpZWxkEjEKBGdycGMYByABKAsyIy53Zy5jb3Ntby5ub2RlLnYxLkdSUENDb25maWd1cmF0aW9uIpABChFHUlBDQ29uZmlndXJhdGlvbhIuCgdtYXBwaW5nGAEgASgLMh0ud2cuY29zbW8ubm9kZS52MS5HUlBDTWFwcGluZxIUCgxwcm90b19zY2hlbWEYAiABKAkSNQoGcGx1Z2luGAMgASgLMiUud2cuY29zbW8ubm9kZS52MS5QbHVnaW5Db25maWd1cmF0aW9uIjcKDkltYWdlUmVmZXJlbmNlEhIKCnJlcG9zaXRvcnkYASABKAkSEQoJcmVmZXJlbmNlGAIgASgJIogBChNQbHVnaW5Db25maWd1cmF0aW9uEgwKBG5hbWUYASABKAkSDwoHdmVyc2lvbhgCIAEoCRI+Cg9pbWFnZV9yZWZlcmVuY2UYAyABKAsyIC53Zy5jb3Ntby5ub2RlLnYxLkltYWdlUmVmZXJlbmNlSACIAQFCEgoQX2ltYWdlX3JlZmVyZW5jZSIjChBTU0xDb25maWd1cmF0aW9uEg8KB2VuYWJsZWQYASABKAgi2wIKC0dSUENNYXBwaW5nEg8KB3ZlcnNpb24YASABKAUSDwoHc2VydmljZRgCIAEoCRI+ChJvcGVyYXRpb25fbWFwcGluZ3MYAyADKAsyIi53Zy5jb3Ntby5ub2RlLnYxLk9wZXJhdGlvbk1hcHBpbmcSOAoPZW50aXR5X21hcHBpbmdzGAQgAygLMh8ud2cuY29zbW8ubm9kZS52MS5FbnRpdHlNYXBwaW5nEj8KE3R5cGVfZmllbGRfbWFwcGluZ3MYBSADKAsyIi53Zy5jb3Ntby5ub2RlLnYxLlR5cGVGaWVsZE1hcHBpbmcSNAoNZW51bV9tYXBwaW5ncxgGIAMoCzIdLndnLmNvc21vLm5vZGUudjEuRW51bU1hcHBpbmcSOQoQcmVzb2x2ZV9tYXBwaW5ncxgHIAMoCzIfLndnLmNvc21vLm5vZGUudjEuTG9va3VwTWFwcGluZyKpAQoNTG9va3VwTWFwcGluZxIqCgR0eXBlGAEgASgOMhwud2cuY29zbW8ubm9kZS52MS5Mb29rdXBUeXBlEjwKDmxvb2t1cF9tYXBwaW5nGAIgASgLMiQud2cuY29zbW8ubm9kZS52MS5Mb29rdXBGaWVsZE1hcHBpbmcSCwoDcnBjGAMgASgJEg8KB3JlcXVlc3QYBCABKAkSEAoIcmVzcG9uc2UYBSABKAkiWQoSTG9va3VwRmllbGRNYXBwaW5nEgwKBHR5cGUYASABKAkSNQoNZmllbGRfbWFwcGluZxgCIAEoCzIeLndnLmNvc21vLm5vZGUudjEuRmllbGRNYXBwaW5nIoYBChBPcGVyYXRpb25NYXBwaW5nEi0KBHR5cGUYASABKA4yHy53Zy5jb3Ntby5ub2RlLnYxLk9wZXJhdGlvblR5cGUSEAoIb3JpZ2luYWwYAiABKAkSDgoGbWFwcGVkGAMgASgJEg8KB3JlcXVlc3QYBCABKAkSEAoIcmVzcG9uc2UYBSABKAkitgEKDUVudGl0eU1hcHBpbmcSEQoJdHlwZV9uYW1lGAEgASgJEgwKBGtpbmQYAiABKAkSCwoDa2V5GAMgASgJEgsKA3JwYxgEIAEoCRIPCgdyZXF1ZXN0GAUgASgJEhAKCHJlc3BvbnNlGAYgASgJEkcKF3JlcXVpcmVkX2ZpZWxkX21hcHBpbmdzGAcgAygLMiYud2cuY29zbW8ubm9kZS52MS5SZXF1aXJlZEZpZWxkTWFwcGluZyJ9ChRSZXF1aXJlZEZpZWxkTWFwcGluZxI1Cg1maWVsZF9tYXBwaW5nGAEgASgLMh4ud2cuY29zbW8ubm9kZS52MS5GaWVsZE1hcHBpbmcSCwoDcnBjGAIgASgJEg8KB3JlcXVlc3QYAyABKAkSEAoIcmVzcG9uc2UYBCABKAkiWAoQVHlwZUZpZWxkTWFwcGluZxIMCgR0eXBlGAEgASgJEjYKDmZpZWxkX21hcHBpbmdzGAIgAygLMh4ud2cuY29zbW8ubm9kZS52MS5GaWVsZE1hcHBpbmcibgoMRmllbGRNYXBwaW5nEhAKCG9yaWdpbmFsGAEgASgJEg4KBm1hcHBlZBgCIAEoCRI8ChFhcmd1bWVudF9tYXBwaW5ncxgDIAMoCzIhLndnLmNvc21vLm5vZGUudjEuQXJndW1lbnRNYXBwaW5nIjMKD0FyZ3VtZW50TWFwcGluZxIQCghvcmlnaW5hbBgBIAEoCRIOCgZtYXBwZWQYAiABKAkiTwoLRW51bU1hcHBpbmcSDAoEdHlwZRgBIAEoCRIyCgZ2YWx1ZXMYAiADKAsyIi53Zy5jb3Ntby5ub2RlLnYxLkVudW1WYWx1ZU1hcHBpbmciNAoQRW51bVZhbHVlTWFwcGluZxIQCghvcmlnaW5hbBgBIAEoCRIOCgZtYXBwZWQYAiABKAkiagoXTmF0c1N0cmVhbUNvbmZpZ3VyYXRpb24SFQoNY29uc3VtZXJfbmFtZRgBIAEoCRITCgtzdHJlYW1fbmFtZRgCIAEoCRIjChtjb25zdW1lcl9pbmFjdGl2ZV90aHJlc2hvbGQYAyABKAUiwwEKFk5hdHNFdmVudENvbmZpZ3VyYXRpb24STgoaZW5naW5lX2V2ZW50X2NvbmZpZ3VyYXRpb24YASABKAsyKi53Zy5jb3Ntby5ub2RlLnYxLkVuZ2luZUV2ZW50Q29uZmlndXJhdGlvbhIQCghzdWJqZWN0cxgCIAMoCRJHChRzdHJlYW1fY29uZmlndXJhdGlvbhgDIAEoCzIpLndnLmNvc21vLm5vZGUudjEuTmF0c1N0cmVhbUNvbmZpZ3VyYXRpb24ieQoXS2Fma2FFdmVudENvbmZpZ3VyYXRpb24STgoaZW5naW5lX2V2ZW50X2NvbmZpZ3VyYXRpb24YASABKAsyKi53Zy5jb3Ntby5ub2RlLnYxLkVuZ2luZUV2ZW50Q29uZmlndXJhdGlvbhIOCgZ0b3BpY3MYAiADKAkiewoXUmVkaXNFdmVudENvbmZpZ3VyYXRpb24STgoaZW5naW5lX2V2ZW50X2NvbmZpZ3VyYXRpb24YASABKAsyKi53Zy5jb3Ntby5ub2RlLnYxLkVuZ2luZUV2ZW50Q29uZmlndXJhdGlvbhIQCghjaGFubmVscxgCIAMoCSKBAQoYRW5naW5lRXZlbnRDb25maWd1cmF0aW9uEhMKC3Byb3ZpZGVyX2lkGAEgASgJEikKBHR5cGUYAiABKA4yGy53Zy5jb3Ntby5ub2RlLnYxLkV2ZW50VHlwZRIRCgl0eXBlX25hbWUYAyABKAkSEgoKZmllbGRfbmFtZRgEIAEoCSLEAQoWRGF0YVNvdXJjZUN1c3RvbUV2ZW50cxI2CgRuYXRzGAEgAygLMigud2cuY29zbW8ubm9kZS52MS5OYXRzRXZlbnRDb25maWd1cmF0aW9uEjgKBWthZmthGAIgAygLMikud2cuY29zbW8ubm9kZS52MS5LYWZrYUV2ZW50Q29uZmlndXJhdGlvbhI4CgVyZWRpcxgDIAMoCzIpLndnLmNvc21vLm5vZGUudjEuUmVkaXNFdmVudENvbmZpZ3VyYXRpb24iUAoXRGF0YVNvdXJjZUN1c3RvbV9TdGF0aWMSNQoEZGF0YRgBIAEoCzInLndnLmNvc21vLm5vZGUudjEuQ29uZmlndXJhdGlvblZhcmlhYmxlIuUBChVDb25maWd1cmF0aW9uVmFyaWFibGUSOQoEa2luZBgBIAEoDjIrLndnLmNvc21vLm5vZGUudjEuQ29uZmlndXJhdGlvblZhcmlhYmxlS2luZBIfChdzdGF0aWNfdmFyaWFibGVfY29udGVudBgCIAEoCRIhChllbnZpcm9ubWVudF92YXJpYWJsZV9uYW1lGAMgASgJEioKImVudmlyb25tZW50X3ZhcmlhYmxlX2RlZmF1bHRfdmFsdWUYBCABKAkSIQoZcGxhY2Vob2xkZXJfdmFyaWFibGVfbmFtZRgFIAEoCSJDChZEaXJlY3RpdmVDb25maWd1cmF0aW9uEhYKDmRpcmVjdGl2ZV9uYW1lGAEgASgJEhEKCXJlbmFtZV90bxgCIAEoCSI0ChVVUkxRdWVyeUNvbmZpZ3VyYXRpb24SDAoEbmFtZRgBIAEoCRINCgV2YWx1ZRgCIAEoCSJFCgpIVFRQSGVhZGVyEjcKBnZhbHVlcxgBIAMoCzInLndnLmNvc21vLm5vZGUudjEuQ29uZmlndXJhdGlvblZhcmlhYmxlIpwBChFNVExTQ29uZmlndXJhdGlvbhI0CgNrZXkYASABKAsyJy53Zy5jb3Ntby5ub2RlLnYxLkNvbmZpZ3VyYXRpb25WYXJpYWJsZRI1CgRjZXJ0GAIgASgLMicud2cuY29zbW8ubm9kZS52MS5Db25maWd1cmF0aW9uVmFyaWFibGUSGgoSaW5zZWN1cmVTa2lwVmVyaWZ5GAMgASgIIsUCCiBHcmFwaFFMU3Vic2NyaXB0aW9uQ29uZmlndXJhdGlvbhIPCgdlbmFibGVkGAEgASgIEjQKA3VybBgCIAEoCzInLndnLmNvc21vLm5vZGUudjEuQ29uZmlndXJhdGlvblZhcmlhYmxlEhMKBnVzZVNTRRgDIAEoCEgAiAEBEkMKCHByb3RvY29sGAQgASgOMiwud2cuY29zbW8uY29tbW9uLkdyYXBoUUxTdWJzY3JpcHRpb25Qcm90b2NvbEgBiAEBEk8KFHdlYnNvY2tldFN1YnByb3RvY29sGAUgASgOMiwud2cuY29zbW8uY29tbW9uLkdyYXBoUUxXZWJzb2NrZXRTdWJwcm90b2NvbEgCiAEBQgkKB191c2VTU0VCCwoJX3Byb3RvY29sQhcKFV93ZWJzb2NrZXRTdWJwcm90b2NvbCJFCh5HcmFwaFFMRmVkZXJhdGlvbkNvbmZpZ3VyYXRpb24SDwoHZW5hYmxlZBgBIAEoCBISCgpzZXJ2aWNlU2RsGAIgASgJIh0KDkludGVybmVkU3RyaW5nEgsKA2tleRgBIAEoCSI4Cg9TaW5nbGVUeXBlRmllbGQSEQoJdHlwZV9uYW1lGAEgASgJEhIKCmZpZWxkX25hbWUYAiABKAkiPgoaU3Vic2NyaXB0aW9uRmllbGRDb25kaXRpb24SEgoKZmllbGRfcGF0aBgBIAMoCRIMCgRqc29uGAIgASgJIqMCChtTdWJzY3JpcHRpb25GaWx0ZXJDb25kaXRpb24SOgoDYW5kGAEgAygLMi0ud2cuY29zbW8ubm9kZS52MS5TdWJzY3JpcHRpb25GaWx0ZXJDb25kaXRpb24SPQoCaW4YAiABKAsyLC53Zy5jb3Ntby5ub2RlLnYxLlN1YnNjcmlwdGlvbkZpZWxkQ29uZGl0aW9uSACIAQESPwoDbm90GAMgASgLMi0ud2cuY29zbW8ubm9kZS52MS5TdWJzY3JpcHRpb25GaWx0ZXJDb25kaXRpb25IAYgBARI5CgJvchgEIAMoCzItLndnLmNvc21vLm5vZGUudjEuU3Vic2NyaXB0aW9uRmlsdGVyQ29uZGl0aW9uQgUKA19pbkIGCgRfbm90IkgKFUNhY2hlV2FybWVyT3BlcmF0aW9ucxIvCgpvcGVyYXRpb25zGAEgAygLMhsud2cuY29zbW8ubm9kZS52MS5PcGVyYXRpb24ibgoJT3BlcmF0aW9uEjMKB3JlcXVlc3QYASABKAsyIi53Zy5jb3Ntby5ub2RlLnYxLk9wZXJhdGlvblJlcXVlc3QSLAoGY2xpZW50GAIgASgLMhwud2cuY29zbW8ubm9kZS52MS5DbGllbnRJbmZvImoKEE9wZXJhdGlvblJlcXVlc3QSFgoOb3BlcmF0aW9uX25hbWUYASABKAkSDQoFcXVlcnkYAiABKAkSLwoKZXh0ZW5zaW9ucxgDIAEoCzIbLndnLmNvc21vLm5vZGUudjEuRXh0ZW5zaW9uIkYKCUV4dGVuc2lvbhI5Cg9wZXJzaXN0ZWRfcXVlcnkYASABKAsyIC53Zy5jb3Ntby5ub2RlLnYxLlBlcnNpc3RlZFF1ZXJ5IjYKDlBlcnNpc3RlZFF1ZXJ5EhMKC3NoYTI1Nl9oYXNoGAEgASgJEg8KB3ZlcnNpb24YAiABKAUiKwoKQ2xpZW50SW5mbxIMCgRuYW1lGAEgASgJEg8KB3ZlcnNpb24YAiABKAkqQwoWU2F0aXNmaWVkT3BlcmF0aW9uVHlwZRIJCgVRVUVSWRAAEgwKCE1VVEFUSU9OEAESEAoMU1VCU0NSSVBUSU9OEAIqggEKG0FyZ3VtZW50UmVuZGVyQ29uZmlndXJhdGlvbhIbChdSRU5ERVJfQVJHVU1FTlRfREVGQVVMVBAAEiQKIFJFTkRFUl9BUkdVTUVOVF9BU19HUkFQSFFMX1ZBTFVFEAESIAocUkVOREVSX0FSR1VNRU5UX0FTX0FSUkFZX0NTVhACKjYKDkFyZ3VtZW50U291cmNlEhAKDE9CSkVDVF9GSUVMRBAAEhIKDkZJRUxEX0FSR1VNRU5UEAEqNQoORGF0YVNvdXJjZUtpbmQSCgoGU1RBVElDEAASCwoHR1JBUEhRTBABEgoKBlBVQlNVQhACKlwKCkxvb2t1cFR5cGUSGwoXTE9PS1VQX1RZUEVfVU5TUEVDSUZJRUQQABIXChNMT09LVVBfVFlQRV9SRVNPTFZFEAESGAoUTE9PS1VQX1RZUEVfUkVRVUlSRVMQAiqHAQoNT3BlcmF0aW9uVHlwZRIeChpPUEVSQVRJT05fVFlQRV9VTlNQRUNJRklFRBAAEhgKFE9QRVJBVElPTl9UWVBFX1FVRVJZEAESGwoXT1BFUkFUSU9OX1RZUEVfTVVUQVRJT04QAhIfChtPUEVSQVRJT05fVFlQRV9TVUJTQ1JJUFRJT04QAyo0CglFdmVudFR5cGUSCwoHUFVCTElTSBAAEgsKB1JFUVVFU1QQARINCglTVUJTQ1JJQkUQAiqGAQoZQ29uZmlndXJhdGlvblZhcmlhYmxlS2luZBIhCh1TVEFUSUNfQ09ORklHVVJBVElPTl9WQVJJQUJMRRAAEh4KGkVOVl9DT05GSUdVUkFUSU9OX1ZBUklBQkxFEAESJgoiUExBQ0VIT0xERVJfQ09ORklHVVJBVElPTl9WQVJJQUJMRRACKkEKCkhUVFBNZXRob2QSBwoDR0VUEAASCAoEUE9TVBABEgcKA1BVVBACEgoKBkRFTEVURRADEgsKB09QVElPTlMQBDLSAQoLTm9kZVNlcnZpY2USXwoMU2VsZlJlZ2lzdGVyEiUud2cuY29zbW8ubm9kZS52MS5TZWxmUmVnaXN0ZXJSZXF1ZXN0GiYud2cuY29zbW8ubm9kZS52MS5TZWxmUmVnaXN0ZXJSZXNwb25zZSIAEmIKDUdlbmVyYXRlUXVlcnkSJi53Zy5jb3Ntby5ub2RlLnYxLkdlbmVyYXRlUXVlcnlSZXF1ZXN0Gicud2cuY29zbW8ubm9kZS52MS5HZW5lcmF0ZVF1ZXJ5UmVzcG9uc2UiAGIGcHJvdG8z", [file_wg_cosmo_common_common]); + fileDesc("Cht3Zy9jb3Ntby9ub2RlL3YxL25vZGUucHJvdG8SEHdnLmNvc21vLm5vZGUudjEiOQoIU3ViZ3JhcGgSCgoCaWQYASABKAkSDAoEbmFtZRgCIAEoCRITCgtyb3V0aW5nX3VybBgDIAEoCSKOAgohRmVhdHVyZUZsYWdSb3V0ZXJFeGVjdXRpb25Db25maWdzEnUKG2NvbmZpZ19ieV9mZWF0dXJlX2ZsYWdfbmFtZRgBIAMoCzJQLndnLmNvc21vLm5vZGUudjEuRmVhdHVyZUZsYWdSb3V0ZXJFeGVjdXRpb25Db25maWdzLkNvbmZpZ0J5RmVhdHVyZUZsYWdOYW1lRW50cnkacgocQ29uZmlnQnlGZWF0dXJlRmxhZ05hbWVFbnRyeRILCgNrZXkYASABKAkSQQoFdmFsdWUYAiABKAsyMi53Zy5jb3Ntby5ub2RlLnYxLkZlYXR1cmVGbGFnUm91dGVyRXhlY3V0aW9uQ29uZmlnOgI4ASKgAQogRmVhdHVyZUZsYWdSb3V0ZXJFeGVjdXRpb25Db25maWcSPAoNZW5naW5lX2NvbmZpZxgBIAEoCzIlLndnLmNvc21vLm5vZGUudjEuRW5naW5lQ29uZmlndXJhdGlvbhIPCgd2ZXJzaW9uGAIgASgJEi0KCXN1YmdyYXBocxgDIAMoCzIaLndnLmNvc21vLm5vZGUudjEuU3ViZ3JhcGginAIKDFJvdXRlckNvbmZpZxI8Cg1lbmdpbmVfY29uZmlnGAEgASgLMiUud2cuY29zbW8ubm9kZS52MS5FbmdpbmVDb25maWd1cmF0aW9uEg8KB3ZlcnNpb24YAiABKAkSLQoJc3ViZ3JhcGhzGAMgAygLMhoud2cuY29zbW8ubm9kZS52MS5TdWJncmFwaBJWChRmZWF0dXJlX2ZsYWdfY29uZmlncxgEIAEoCzIzLndnLmNvc21vLm5vZGUudjEuRmVhdHVyZUZsYWdSb3V0ZXJFeGVjdXRpb25Db25maWdzSACIAQESHQoVY29tcGF0aWJpbGl0eV92ZXJzaW9uGAUgASgJQhcKFV9mZWF0dXJlX2ZsYWdfY29uZmlncyJbCghSZXNwb25zZRItCgRjb2RlGAEgASgOMh8ud2cuY29zbW8uY29tbW9uLkVudW1TdGF0dXNDb2RlEhQKB2RldGFpbHMYAiABKAlIAIgBAUIKCghfZGV0YWlscyIvCg5SZXNwb25zZVN0YXR1cxIMCgRjb2RlGAEgASgFEg8KB21lc3NhZ2UYAiABKAkiZQoQUmVnaXN0cmF0aW9uSW5mbxI3Cg5hY2NvdW50X2xpbWl0cxgBIAEoCzIfLndnLmNvc21vLm5vZGUudjEuQWNjb3VudExpbWl0cxIYChBncmFwaF9wdWJsaWNfa2V5GAIgASgJIiwKDUFjY291bnRMaW1pdHMSGwoTdHJhY2Vfc2FtcGxpbmdfcmF0ZRgBIAEoAiIVChNTZWxmUmVnaXN0ZXJSZXF1ZXN0IpwBChRTZWxmUmVnaXN0ZXJSZXNwb25zZRIsCghyZXNwb25zZRgBIAEoCzIaLndnLmNvc21vLm5vZGUudjEuUmVzcG9uc2USQQoQcmVnaXN0cmF0aW9uSW5mbxgCIAEoCzIiLndnLmNvc21vLm5vZGUudjEuUmVnaXN0cmF0aW9uSW5mb0gAiAEBQhMKEV9yZWdpc3RyYXRpb25JbmZvIjsKFEdlbmVyYXRlUXVlcnlSZXF1ZXN0EhMKC3NjaGVtYV9oYXNoGAEgASgJEg4KBnByb21wdBgCIAEoCSKFAQoVR2VuZXJhdGVRdWVyeVJlc3BvbnNlEiwKCHJlc3BvbnNlGAEgASgLMhoud2cuY29zbW8ubm9kZS52MS5SZXNwb25zZRI0CgVxdWVyeRgCIAEoCzIgLndnLmNvc21vLm5vZGUudjEuU2F0aXNmaWVkUXVlcnlIAIgBAUIICgZfcXVlcnkiqwEKDlNhdGlzZmllZFF1ZXJ5EhMKC2Rlc2NyaXB0aW9uGAEgASgJEhAKCGRvY3VtZW50GAIgASgJEhYKDm9wZXJhdGlvbl9uYW1lGAMgASgJEkAKDm9wZXJhdGlvbl90eXBlGAQgASgOMigud2cuY29zbW8ubm9kZS52MS5TYXRpc2ZpZWRPcGVyYXRpb25UeXBlEhgKEHZhcmlhYmxlc19zY2hlbWEYBSABKAki+QMKE0VuZ2luZUNvbmZpZ3VyYXRpb24SHAoUZGVmYXVsdEZsdXNoSW50ZXJ2YWwYASABKAMSTAoZZGF0YXNvdXJjZV9jb25maWd1cmF0aW9ucxgCIAMoCzIpLndnLmNvc21vLm5vZGUudjEuRGF0YVNvdXJjZUNvbmZpZ3VyYXRpb24SQgoUZmllbGRfY29uZmlndXJhdGlvbnMYAyADKAsyJC53Zy5jb3Ntby5ub2RlLnYxLkZpZWxkQ29uZmlndXJhdGlvbhIVCg1ncmFwaHFsU2NoZW1hGAQgASgJEkAKE3R5cGVfY29uZmlndXJhdGlvbnMYBSADKAsyIy53Zy5jb3Ntby5ub2RlLnYxLlR5cGVDb25maWd1cmF0aW9uElAKDnN0cmluZ19zdG9yYWdlGAYgAygLMjgud2cuY29zbW8ubm9kZS52MS5FbmdpbmVDb25maWd1cmF0aW9uLlN0cmluZ1N0b3JhZ2VFbnRyeRIiChVncmFwaHFsX2NsaWVudF9zY2hlbWEYByABKAlIAIgBARITCgtzY2hlbWFfaGFzaBgIIAEoCRo0ChJTdHJpbmdTdG9yYWdlRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4AUIYChZfZ3JhcGhxbF9jbGllbnRfc2NoZW1hIsYHChdEYXRhU291cmNlQ29uZmlndXJhdGlvbhIuCgRraW5kGAEgASgOMiAud2cuY29zbW8ubm9kZS52MS5EYXRhU291cmNlS2luZBIvCgpyb290X25vZGVzGAIgAygLMhsud2cuY29zbW8ubm9kZS52MS5UeXBlRmllbGQSMAoLY2hpbGRfbm9kZXMYAyADKAsyGy53Zy5jb3Ntby5ub2RlLnYxLlR5cGVGaWVsZBImCh5vdmVycmlkZV9maWVsZF9wYXRoX2Zyb21fYWxpYXMYBCABKAgSQgoOY3VzdG9tX2dyYXBocWwYBSABKAsyKi53Zy5jb3Ntby5ub2RlLnYxLkRhdGFTb3VyY2VDdXN0b21fR3JhcGhRTBJACg1jdXN0b21fc3RhdGljGAYgASgLMikud2cuY29zbW8ubm9kZS52MS5EYXRhU291cmNlQ3VzdG9tX1N0YXRpYxI8CgpkaXJlY3RpdmVzGAcgAygLMigud2cuY29zbW8ubm9kZS52MS5EaXJlY3RpdmVDb25maWd1cmF0aW9uEh8KF3JlcXVlc3RfdGltZW91dF9zZWNvbmRzGAggASgDEgoKAmlkGAkgASgJEi0KBGtleXMYCiADKAsyHy53Zy5jb3Ntby5ub2RlLnYxLlJlcXVpcmVkRmllbGQSMQoIcHJvdmlkZXMYCyADKAsyHy53Zy5jb3Ntby5ub2RlLnYxLlJlcXVpcmVkRmllbGQSMQoIcmVxdWlyZXMYDCADKAsyHy53Zy5jb3Ntby5ub2RlLnYxLlJlcXVpcmVkRmllbGQSPwoNY3VzdG9tX2V2ZW50cxgNIAEoCzIoLndnLmNvc21vLm5vZGUudjEuRGF0YVNvdXJjZUN1c3RvbUV2ZW50cxJJChFlbnRpdHlfaW50ZXJmYWNlcxgOIAMoCzIuLndnLmNvc21vLm5vZGUudjEuRW50aXR5SW50ZXJmYWNlQ29uZmlndXJhdGlvbhJJChFpbnRlcmZhY2Vfb2JqZWN0cxgPIAMoCzIuLndnLmNvc21vLm5vZGUudjEuRW50aXR5SW50ZXJmYWNlQ29uZmlndXJhdGlvbhI/ChJjb3N0X2NvbmZpZ3VyYXRpb24YECABKAsyIy53Zy5jb3Ntby5ub2RlLnYxLkNvc3RDb25maWd1cmF0aW9uElIKHGVudGl0eV9jYWNoaW5nX2NvbmZpZ3VyYXRpb24YESABKAsyLC53Zy5jb3Ntby5ub2RlLnYxLkVudGl0eUNhY2hpbmdDb25maWd1cmF0aW9uIpsCChpFbnRpdHlDYWNoaW5nQ29uZmlndXJhdGlvbhJPChtlbnRpdHlfY2FjaGVfY29uZmlndXJhdGlvbnMYASADKAsyKi53Zy5jb3Ntby5ub2RlLnYxLkVudGl0eUNhY2hlQ29uZmlndXJhdGlvbhJXCh9jYWNoZV9pbnZhbGlkYXRlX2NvbmZpZ3VyYXRpb25zGAIgAygLMi4ud2cuY29zbW8ubm9kZS52MS5DYWNoZUludmFsaWRhdGVDb25maWd1cmF0aW9uElMKHWNhY2hlX3BvcHVsYXRlX2NvbmZpZ3VyYXRpb25zGAMgAygLMiwud2cuY29zbW8ubm9kZS52MS5DYWNoZVBvcHVsYXRlQ29uZmlndXJhdGlvbiK1AQoYRW50aXR5Q2FjaGVDb25maWd1cmF0aW9uEhEKCXR5cGVfbmFtZRgBIAEoCRIXCg9tYXhfYWdlX3NlY29uZHMYAiABKAMSFwoPaW5jbHVkZV9oZWFkZXJzGAMgASgIEhoKEnBhcnRpYWxfY2FjaGVfbG9hZBgEIAEoCBITCgtzaGFkb3dfbW9kZRgFIAEoCBIjChtub3RfZm91bmRfY2FjaGVfdHRsX3NlY29uZHMYBiABKAMiZAocQ2FjaGVJbnZhbGlkYXRlQ29uZmlndXJhdGlvbhISCgpmaWVsZF9uYW1lGAEgASgJEhYKDm9wZXJhdGlvbl90eXBlGAIgASgJEhgKEGVudGl0eV90eXBlX25hbWUYAyABKAkiewoaQ2FjaGVQb3B1bGF0ZUNvbmZpZ3VyYXRpb24SEgoKZmllbGRfbmFtZRgBIAEoCRIWCg5vcGVyYXRpb25fdHlwZRgCIAEoCRIXCg9tYXhfYWdlX3NlY29uZHMYAyABKAMSGAoQZW50aXR5X3R5cGVfbmFtZRgEIAEoCSLAAwoRQ29zdENvbmZpZ3VyYXRpb24SQQoNZmllbGRfd2VpZ2h0cxgBIAMoCzIqLndnLmNvc21vLm5vZGUudjEuRmllbGRXZWlnaHRDb25maWd1cmF0aW9uEkAKCmxpc3Rfc2l6ZXMYAiADKAsyLC53Zy5jb3Ntby5ub2RlLnYxLkZpZWxkTGlzdFNpemVDb25maWd1cmF0aW9uEkoKDHR5cGVfd2VpZ2h0cxgDIAMoCzI0LndnLmNvc21vLm5vZGUudjEuQ29zdENvbmZpZ3VyYXRpb24uVHlwZVdlaWdodHNFbnRyeRJlChpkaXJlY3RpdmVfYXJndW1lbnRfd2VpZ2h0cxgEIAMoCzJBLndnLmNvc21vLm5vZGUudjEuQ29zdENvbmZpZ3VyYXRpb24uRGlyZWN0aXZlQXJndW1lbnRXZWlnaHRzRW50cnkaMgoQVHlwZVdlaWdodHNFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAU6AjgBGj8KHURpcmVjdGl2ZUFyZ3VtZW50V2VpZ2h0c0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoBToCOAEiowMKGEZpZWxkV2VpZ2h0Q29uZmlndXJhdGlvbhIRCgl0eXBlX25hbWUYASABKAkSEgoKZmllbGRfbmFtZRgCIAEoCRITCgZ3ZWlnaHQYAyABKAVIAIgBARJZChBhcmd1bWVudF93ZWlnaHRzGAQgAygLMj8ud2cuY29zbW8ubm9kZS52MS5GaWVsZFdlaWdodENvbmZpZ3VyYXRpb24uQXJndW1lbnRXZWlnaHRzRW50cnkSbAoaZGlyZWN0aXZlX2FyZ3VtZW50X3dlaWdodHMYBSADKAsySC53Zy5jb3Ntby5ub2RlLnYxLkZpZWxkV2VpZ2h0Q29uZmlndXJhdGlvbi5EaXJlY3RpdmVBcmd1bWVudFdlaWdodHNFbnRyeRo2ChRBcmd1bWVudFdlaWdodHNFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAU6AjgBGj8KHURpcmVjdGl2ZUFyZ3VtZW50V2VpZ2h0c0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoBToCOAFCCQoHX3dlaWdodCLsAQoaRmllbGRMaXN0U2l6ZUNvbmZpZ3VyYXRpb24SEQoJdHlwZV9uYW1lGAEgASgJEhIKCmZpZWxkX25hbWUYAiABKAkSGQoMYXNzdW1lZF9zaXplGAMgASgFSACIAQESGQoRc2xpY2luZ19hcmd1bWVudHMYBCADKAkSFAoMc2l6ZWRfZmllbGRzGAUgAygJEikKHHJlcXVpcmVfb25lX3NsaWNpbmdfYXJndW1lbnQYBiABKAhIAYgBAUIPCg1fYXNzdW1lZF9zaXplQh8KHV9yZXF1aXJlX29uZV9zbGljaW5nX2FyZ3VtZW50IlwKFUFyZ3VtZW50Q29uZmlndXJhdGlvbhIMCgRuYW1lGAEgASgJEjUKC3NvdXJjZV90eXBlGAIgASgOMiAud2cuY29zbW8ubm9kZS52MS5Bcmd1bWVudFNvdXJjZSIlCgZTY29wZXMSGwoTcmVxdWlyZWRfYW5kX3Njb3BlcxgBIAMoCSKvAQoaQXV0aG9yaXphdGlvbkNvbmZpZ3VyYXRpb24SHwoXcmVxdWlyZXNfYXV0aGVudGljYXRpb24YASABKAgSNAoScmVxdWlyZWRfb3Jfc2NvcGVzGAIgAygLMhgud2cuY29zbW8ubm9kZS52MS5TY29wZXMSOgoYcmVxdWlyZWRfb3Jfc2NvcGVzX2J5X29yGAMgAygLMhgud2cuY29zbW8ubm9kZS52MS5TY29wZXMi1QIKEkZpZWxkQ29uZmlndXJhdGlvbhIRCgl0eXBlX25hbWUYASABKAkSEgoKZmllbGRfbmFtZRgCIAEoCRJIChdhcmd1bWVudHNfY29uZmlndXJhdGlvbhgDIAMoCzInLndnLmNvc21vLm5vZGUudjEuQXJndW1lbnRDb25maWd1cmF0aW9uElEKG2F1dGhvcml6YXRpb25fY29uZmlndXJhdGlvbhgEIAEoCzIsLndnLmNvc21vLm5vZGUudjEuQXV0aG9yaXphdGlvbkNvbmZpZ3VyYXRpb24SWQodc3Vic2NyaXB0aW9uX2ZpbHRlcl9jb25kaXRpb24YBSABKAsyLS53Zy5jb3Ntby5ub2RlLnYxLlN1YnNjcmlwdGlvbkZpbHRlckNvbmRpdGlvbkgAiAEBQiAKHl9zdWJzY3JpcHRpb25fZmlsdGVyX2NvbmRpdGlvbiI5ChFUeXBlQ29uZmlndXJhdGlvbhIRCgl0eXBlX25hbWUYASABKAkSEQoJcmVuYW1lX3RvGAIgASgJInwKCVR5cGVGaWVsZBIRCgl0eXBlX25hbWUYASABKAkSEwoLZmllbGRfbmFtZXMYAiADKAkSHAoUZXh0ZXJuYWxfZmllbGRfbmFtZXMYAyADKAkSKQohcmVxdWlyZV9mZXRjaF9yZWFzb25zX2ZpZWxkX25hbWVzGAQgAygJIjkKEEZpZWxkQ29vcmRpbmF0ZXMSEgoKZmllbGRfbmFtZRgBIAEoCRIRCgl0eXBlX25hbWUYAiABKAkiawoRRmllbGRTZXRDb25kaXRpb24SQgoWZmllbGRfY29vcmRpbmF0ZXNfcGF0aBgBIAMoCzIiLndnLmNvc21vLm5vZGUudjEuRmllbGRDb29yZGluYXRlcxISCgpmaWVsZF9wYXRoGAIgAygJIqcBCg1SZXF1aXJlZEZpZWxkEhEKCXR5cGVfbmFtZRgBIAEoCRISCgpmaWVsZF9uYW1lGAIgASgJEhUKDXNlbGVjdGlvbl9zZXQYAyABKAkSHwoXZGlzYWJsZV9lbnRpdHlfcmVzb2x2ZXIYBCABKAgSNwoKY29uZGl0aW9ucxgFIAMoCzIjLndnLmNvc21vLm5vZGUudjEuRmllbGRTZXRDb25kaXRpb24iWAocRW50aXR5SW50ZXJmYWNlQ29uZmlndXJhdGlvbhIbChNpbnRlcmZhY2VfdHlwZV9uYW1lGAEgASgJEhsKE2NvbmNyZXRlX3R5cGVfbmFtZXMYAiADKAkijQUKEkZldGNoQ29uZmlndXJhdGlvbhI0CgN1cmwYASABKAsyJy53Zy5jb3Ntby5ub2RlLnYxLkNvbmZpZ3VyYXRpb25WYXJpYWJsZRIsCgZtZXRob2QYAiABKA4yHC53Zy5jb3Ntby5ub2RlLnYxLkhUVFBNZXRob2QSQAoGaGVhZGVyGAMgAygLMjAud2cuY29zbW8ubm9kZS52MS5GZXRjaENvbmZpZ3VyYXRpb24uSGVhZGVyRW50cnkSNQoEYm9keRgEIAEoCzInLndnLmNvc21vLm5vZGUudjEuQ29uZmlndXJhdGlvblZhcmlhYmxlEjYKBXF1ZXJ5GAUgAygLMicud2cuY29zbW8ubm9kZS52MS5VUkxRdWVyeUNvbmZpZ3VyYXRpb24SFwoPdXJsX2VuY29kZV9ib2R5GAcgASgIEjEKBG10bHMYCCABKAsyIy53Zy5jb3Ntby5ub2RlLnYxLk1UTFNDb25maWd1cmF0aW9uEjkKCGJhc2VfdXJsGAkgASgLMicud2cuY29zbW8ubm9kZS52MS5Db25maWd1cmF0aW9uVmFyaWFibGUSNQoEcGF0aBgKIAEoCzInLndnLmNvc21vLm5vZGUudjEuQ29uZmlndXJhdGlvblZhcmlhYmxlEkQKDmh0dHBfcHJveHlfdXJsGAsgASgLMicud2cuY29zbW8ubm9kZS52MS5Db25maWd1cmF0aW9uVmFyaWFibGVIAIgBARpLCgtIZWFkZXJFbnRyeRILCgNrZXkYASABKAkSKwoFdmFsdWUYAiABKAsyHC53Zy5jb3Ntby5ub2RlLnYxLkhUVFBIZWFkZXI6AjgBQhEKD19odHRwX3Byb3h5X3VybCJlChVTdGF0dXNDb2RlVHlwZU1hcHBpbmcSEwoLc3RhdHVzX2NvZGUYASABKAMSEQoJdHlwZV9uYW1lGAIgASgJEiQKHGluamVjdF9zdGF0dXNfY29kZV9pbnRvX2JvZHkYAyABKAgikwMKGERhdGFTb3VyY2VDdXN0b21fR3JhcGhRTBIzCgVmZXRjaBgBIAEoCzIkLndnLmNvc21vLm5vZGUudjEuRmV0Y2hDb25maWd1cmF0aW9uEkgKDHN1YnNjcmlwdGlvbhgCIAEoCzIyLndnLmNvc21vLm5vZGUudjEuR3JhcGhRTFN1YnNjcmlwdGlvbkNvbmZpZ3VyYXRpb24SRAoKZmVkZXJhdGlvbhgDIAEoCzIwLndnLmNvc21vLm5vZGUudjEuR3JhcGhRTEZlZGVyYXRpb25Db25maWd1cmF0aW9uEjkKD3Vwc3RyZWFtX3NjaGVtYRgEIAEoCzIgLndnLmNvc21vLm5vZGUudjEuSW50ZXJuZWRTdHJpbmcSRAoZY3VzdG9tX3NjYWxhcl90eXBlX2ZpZWxkcxgGIAMoCzIhLndnLmNvc21vLm5vZGUudjEuU2luZ2xlVHlwZUZpZWxkEjEKBGdycGMYByABKAsyIy53Zy5jb3Ntby5ub2RlLnYxLkdSUENDb25maWd1cmF0aW9uIpABChFHUlBDQ29uZmlndXJhdGlvbhIuCgdtYXBwaW5nGAEgASgLMh0ud2cuY29zbW8ubm9kZS52MS5HUlBDTWFwcGluZxIUCgxwcm90b19zY2hlbWEYAiABKAkSNQoGcGx1Z2luGAMgASgLMiUud2cuY29zbW8ubm9kZS52MS5QbHVnaW5Db25maWd1cmF0aW9uIjcKDkltYWdlUmVmZXJlbmNlEhIKCnJlcG9zaXRvcnkYASABKAkSEQoJcmVmZXJlbmNlGAIgASgJIogBChNQbHVnaW5Db25maWd1cmF0aW9uEgwKBG5hbWUYASABKAkSDwoHdmVyc2lvbhgCIAEoCRI+Cg9pbWFnZV9yZWZlcmVuY2UYAyABKAsyIC53Zy5jb3Ntby5ub2RlLnYxLkltYWdlUmVmZXJlbmNlSACIAQFCEgoQX2ltYWdlX3JlZmVyZW5jZSIjChBTU0xDb25maWd1cmF0aW9uEg8KB2VuYWJsZWQYASABKAgi2wIKC0dSUENNYXBwaW5nEg8KB3ZlcnNpb24YASABKAUSDwoHc2VydmljZRgCIAEoCRI+ChJvcGVyYXRpb25fbWFwcGluZ3MYAyADKAsyIi53Zy5jb3Ntby5ub2RlLnYxLk9wZXJhdGlvbk1hcHBpbmcSOAoPZW50aXR5X21hcHBpbmdzGAQgAygLMh8ud2cuY29zbW8ubm9kZS52MS5FbnRpdHlNYXBwaW5nEj8KE3R5cGVfZmllbGRfbWFwcGluZ3MYBSADKAsyIi53Zy5jb3Ntby5ub2RlLnYxLlR5cGVGaWVsZE1hcHBpbmcSNAoNZW51bV9tYXBwaW5ncxgGIAMoCzIdLndnLmNvc21vLm5vZGUudjEuRW51bU1hcHBpbmcSOQoQcmVzb2x2ZV9tYXBwaW5ncxgHIAMoCzIfLndnLmNvc21vLm5vZGUudjEuTG9va3VwTWFwcGluZyKpAQoNTG9va3VwTWFwcGluZxIqCgR0eXBlGAEgASgOMhwud2cuY29zbW8ubm9kZS52MS5Mb29rdXBUeXBlEjwKDmxvb2t1cF9tYXBwaW5nGAIgASgLMiQud2cuY29zbW8ubm9kZS52MS5Mb29rdXBGaWVsZE1hcHBpbmcSCwoDcnBjGAMgASgJEg8KB3JlcXVlc3QYBCABKAkSEAoIcmVzcG9uc2UYBSABKAkiWQoSTG9va3VwRmllbGRNYXBwaW5nEgwKBHR5cGUYASABKAkSNQoNZmllbGRfbWFwcGluZxgCIAEoCzIeLndnLmNvc21vLm5vZGUudjEuRmllbGRNYXBwaW5nIoYBChBPcGVyYXRpb25NYXBwaW5nEi0KBHR5cGUYASABKA4yHy53Zy5jb3Ntby5ub2RlLnYxLk9wZXJhdGlvblR5cGUSEAoIb3JpZ2luYWwYAiABKAkSDgoGbWFwcGVkGAMgASgJEg8KB3JlcXVlc3QYBCABKAkSEAoIcmVzcG9uc2UYBSABKAkitgEKDUVudGl0eU1hcHBpbmcSEQoJdHlwZV9uYW1lGAEgASgJEgwKBGtpbmQYAiABKAkSCwoDa2V5GAMgASgJEgsKA3JwYxgEIAEoCRIPCgdyZXF1ZXN0GAUgASgJEhAKCHJlc3BvbnNlGAYgASgJEkcKF3JlcXVpcmVkX2ZpZWxkX21hcHBpbmdzGAcgAygLMiYud2cuY29zbW8ubm9kZS52MS5SZXF1aXJlZEZpZWxkTWFwcGluZyJ9ChRSZXF1aXJlZEZpZWxkTWFwcGluZxI1Cg1maWVsZF9tYXBwaW5nGAEgASgLMh4ud2cuY29zbW8ubm9kZS52MS5GaWVsZE1hcHBpbmcSCwoDcnBjGAIgASgJEg8KB3JlcXVlc3QYAyABKAkSEAoIcmVzcG9uc2UYBCABKAkiWAoQVHlwZUZpZWxkTWFwcGluZxIMCgR0eXBlGAEgASgJEjYKDmZpZWxkX21hcHBpbmdzGAIgAygLMh4ud2cuY29zbW8ubm9kZS52MS5GaWVsZE1hcHBpbmcibgoMRmllbGRNYXBwaW5nEhAKCG9yaWdpbmFsGAEgASgJEg4KBm1hcHBlZBgCIAEoCRI8ChFhcmd1bWVudF9tYXBwaW5ncxgDIAMoCzIhLndnLmNvc21vLm5vZGUudjEuQXJndW1lbnRNYXBwaW5nIjMKD0FyZ3VtZW50TWFwcGluZxIQCghvcmlnaW5hbBgBIAEoCRIOCgZtYXBwZWQYAiABKAkiTwoLRW51bU1hcHBpbmcSDAoEdHlwZRgBIAEoCRIyCgZ2YWx1ZXMYAiADKAsyIi53Zy5jb3Ntby5ub2RlLnYxLkVudW1WYWx1ZU1hcHBpbmciNAoQRW51bVZhbHVlTWFwcGluZxIQCghvcmlnaW5hbBgBIAEoCRIOCgZtYXBwZWQYAiABKAkiagoXTmF0c1N0cmVhbUNvbmZpZ3VyYXRpb24SFQoNY29uc3VtZXJfbmFtZRgBIAEoCRITCgtzdHJlYW1fbmFtZRgCIAEoCRIjChtjb25zdW1lcl9pbmFjdGl2ZV90aHJlc2hvbGQYAyABKAUiwwEKFk5hdHNFdmVudENvbmZpZ3VyYXRpb24STgoaZW5naW5lX2V2ZW50X2NvbmZpZ3VyYXRpb24YASABKAsyKi53Zy5jb3Ntby5ub2RlLnYxLkVuZ2luZUV2ZW50Q29uZmlndXJhdGlvbhIQCghzdWJqZWN0cxgCIAMoCRJHChRzdHJlYW1fY29uZmlndXJhdGlvbhgDIAEoCzIpLndnLmNvc21vLm5vZGUudjEuTmF0c1N0cmVhbUNvbmZpZ3VyYXRpb24ieQoXS2Fma2FFdmVudENvbmZpZ3VyYXRpb24STgoaZW5naW5lX2V2ZW50X2NvbmZpZ3VyYXRpb24YASABKAsyKi53Zy5jb3Ntby5ub2RlLnYxLkVuZ2luZUV2ZW50Q29uZmlndXJhdGlvbhIOCgZ0b3BpY3MYAiADKAkiewoXUmVkaXNFdmVudENvbmZpZ3VyYXRpb24STgoaZW5naW5lX2V2ZW50X2NvbmZpZ3VyYXRpb24YASABKAsyKi53Zy5jb3Ntby5ub2RlLnYxLkVuZ2luZUV2ZW50Q29uZmlndXJhdGlvbhIQCghjaGFubmVscxgCIAMoCSKBAQoYRW5naW5lRXZlbnRDb25maWd1cmF0aW9uEhMKC3Byb3ZpZGVyX2lkGAEgASgJEikKBHR5cGUYAiABKA4yGy53Zy5jb3Ntby5ub2RlLnYxLkV2ZW50VHlwZRIRCgl0eXBlX25hbWUYAyABKAkSEgoKZmllbGRfbmFtZRgEIAEoCSLEAQoWRGF0YVNvdXJjZUN1c3RvbUV2ZW50cxI2CgRuYXRzGAEgAygLMigud2cuY29zbW8ubm9kZS52MS5OYXRzRXZlbnRDb25maWd1cmF0aW9uEjgKBWthZmthGAIgAygLMikud2cuY29zbW8ubm9kZS52MS5LYWZrYUV2ZW50Q29uZmlndXJhdGlvbhI4CgVyZWRpcxgDIAMoCzIpLndnLmNvc21vLm5vZGUudjEuUmVkaXNFdmVudENvbmZpZ3VyYXRpb24iUAoXRGF0YVNvdXJjZUN1c3RvbV9TdGF0aWMSNQoEZGF0YRgBIAEoCzInLndnLmNvc21vLm5vZGUudjEuQ29uZmlndXJhdGlvblZhcmlhYmxlIuUBChVDb25maWd1cmF0aW9uVmFyaWFibGUSOQoEa2luZBgBIAEoDjIrLndnLmNvc21vLm5vZGUudjEuQ29uZmlndXJhdGlvblZhcmlhYmxlS2luZBIfChdzdGF0aWNfdmFyaWFibGVfY29udGVudBgCIAEoCRIhChllbnZpcm9ubWVudF92YXJpYWJsZV9uYW1lGAMgASgJEioKImVudmlyb25tZW50X3ZhcmlhYmxlX2RlZmF1bHRfdmFsdWUYBCABKAkSIQoZcGxhY2Vob2xkZXJfdmFyaWFibGVfbmFtZRgFIAEoCSJDChZEaXJlY3RpdmVDb25maWd1cmF0aW9uEhYKDmRpcmVjdGl2ZV9uYW1lGAEgASgJEhEKCXJlbmFtZV90bxgCIAEoCSI0ChVVUkxRdWVyeUNvbmZpZ3VyYXRpb24SDAoEbmFtZRgBIAEoCRINCgV2YWx1ZRgCIAEoCSJFCgpIVFRQSGVhZGVyEjcKBnZhbHVlcxgBIAMoCzInLndnLmNvc21vLm5vZGUudjEuQ29uZmlndXJhdGlvblZhcmlhYmxlIpwBChFNVExTQ29uZmlndXJhdGlvbhI0CgNrZXkYASABKAsyJy53Zy5jb3Ntby5ub2RlLnYxLkNvbmZpZ3VyYXRpb25WYXJpYWJsZRI1CgRjZXJ0GAIgASgLMicud2cuY29zbW8ubm9kZS52MS5Db25maWd1cmF0aW9uVmFyaWFibGUSGgoSaW5zZWN1cmVTa2lwVmVyaWZ5GAMgASgIIsUCCiBHcmFwaFFMU3Vic2NyaXB0aW9uQ29uZmlndXJhdGlvbhIPCgdlbmFibGVkGAEgASgIEjQKA3VybBgCIAEoCzInLndnLmNvc21vLm5vZGUudjEuQ29uZmlndXJhdGlvblZhcmlhYmxlEhMKBnVzZVNTRRgDIAEoCEgAiAEBEkMKCHByb3RvY29sGAQgASgOMiwud2cuY29zbW8uY29tbW9uLkdyYXBoUUxTdWJzY3JpcHRpb25Qcm90b2NvbEgBiAEBEk8KFHdlYnNvY2tldFN1YnByb3RvY29sGAUgASgOMiwud2cuY29zbW8uY29tbW9uLkdyYXBoUUxXZWJzb2NrZXRTdWJwcm90b2NvbEgCiAEBQgkKB191c2VTU0VCCwoJX3Byb3RvY29sQhcKFV93ZWJzb2NrZXRTdWJwcm90b2NvbCJFCh5HcmFwaFFMRmVkZXJhdGlvbkNvbmZpZ3VyYXRpb24SDwoHZW5hYmxlZBgBIAEoCBISCgpzZXJ2aWNlU2RsGAIgASgJIh0KDkludGVybmVkU3RyaW5nEgsKA2tleRgBIAEoCSI4Cg9TaW5nbGVUeXBlRmllbGQSEQoJdHlwZV9uYW1lGAEgASgJEhIKCmZpZWxkX25hbWUYAiABKAkiPgoaU3Vic2NyaXB0aW9uRmllbGRDb25kaXRpb24SEgoKZmllbGRfcGF0aBgBIAMoCRIMCgRqc29uGAIgASgJIqMCChtTdWJzY3JpcHRpb25GaWx0ZXJDb25kaXRpb24SOgoDYW5kGAEgAygLMi0ud2cuY29zbW8ubm9kZS52MS5TdWJzY3JpcHRpb25GaWx0ZXJDb25kaXRpb24SPQoCaW4YAiABKAsyLC53Zy5jb3Ntby5ub2RlLnYxLlN1YnNjcmlwdGlvbkZpZWxkQ29uZGl0aW9uSACIAQESPwoDbm90GAMgASgLMi0ud2cuY29zbW8ubm9kZS52MS5TdWJzY3JpcHRpb25GaWx0ZXJDb25kaXRpb25IAYgBARI5CgJvchgEIAMoCzItLndnLmNvc21vLm5vZGUudjEuU3Vic2NyaXB0aW9uRmlsdGVyQ29uZGl0aW9uQgUKA19pbkIGCgRfbm90IkgKFUNhY2hlV2FybWVyT3BlcmF0aW9ucxIvCgpvcGVyYXRpb25zGAEgAygLMhsud2cuY29zbW8ubm9kZS52MS5PcGVyYXRpb24ibgoJT3BlcmF0aW9uEjMKB3JlcXVlc3QYASABKAsyIi53Zy5jb3Ntby5ub2RlLnYxLk9wZXJhdGlvblJlcXVlc3QSLAoGY2xpZW50GAIgASgLMhwud2cuY29zbW8ubm9kZS52MS5DbGllbnRJbmZvImoKEE9wZXJhdGlvblJlcXVlc3QSFgoOb3BlcmF0aW9uX25hbWUYASABKAkSDQoFcXVlcnkYAiABKAkSLwoKZXh0ZW5zaW9ucxgDIAEoCzIbLndnLmNvc21vLm5vZGUudjEuRXh0ZW5zaW9uIkYKCUV4dGVuc2lvbhI5Cg9wZXJzaXN0ZWRfcXVlcnkYASABKAsyIC53Zy5jb3Ntby5ub2RlLnYxLlBlcnNpc3RlZFF1ZXJ5IjYKDlBlcnNpc3RlZFF1ZXJ5EhMKC3NoYTI1Nl9oYXNoGAEgASgJEg8KB3ZlcnNpb24YAiABKAUiKwoKQ2xpZW50SW5mbxIMCgRuYW1lGAEgASgJEg8KB3ZlcnNpb24YAiABKAkqQwoWU2F0aXNmaWVkT3BlcmF0aW9uVHlwZRIJCgVRVUVSWRAAEgwKCE1VVEFUSU9OEAESEAoMU1VCU0NSSVBUSU9OEAIqggEKG0FyZ3VtZW50UmVuZGVyQ29uZmlndXJhdGlvbhIbChdSRU5ERVJfQVJHVU1FTlRfREVGQVVMVBAAEiQKIFJFTkRFUl9BUkdVTUVOVF9BU19HUkFQSFFMX1ZBTFVFEAESIAocUkVOREVSX0FSR1VNRU5UX0FTX0FSUkFZX0NTVhACKjYKDkFyZ3VtZW50U291cmNlEhAKDE9CSkVDVF9GSUVMRBAAEhIKDkZJRUxEX0FSR1VNRU5UEAEqNQoORGF0YVNvdXJjZUtpbmQSCgoGU1RBVElDEAASCwoHR1JBUEhRTBABEgoKBlBVQlNVQhACKlwKCkxvb2t1cFR5cGUSGwoXTE9PS1VQX1RZUEVfVU5TUEVDSUZJRUQQABIXChNMT09LVVBfVFlQRV9SRVNPTFZFEAESGAoUTE9PS1VQX1RZUEVfUkVRVUlSRVMQAiqHAQoNT3BlcmF0aW9uVHlwZRIeChpPUEVSQVRJT05fVFlQRV9VTlNQRUNJRklFRBAAEhgKFE9QRVJBVElPTl9UWVBFX1FVRVJZEAESGwoXT1BFUkFUSU9OX1RZUEVfTVVUQVRJT04QAhIfChtPUEVSQVRJT05fVFlQRV9TVUJTQ1JJUFRJT04QAyo0CglFdmVudFR5cGUSCwoHUFVCTElTSBAAEgsKB1JFUVVFU1QQARINCglTVUJTQ1JJQkUQAiqGAQoZQ29uZmlndXJhdGlvblZhcmlhYmxlS2luZBIhCh1TVEFUSUNfQ09ORklHVVJBVElPTl9WQVJJQUJMRRAAEh4KGkVOVl9DT05GSUdVUkFUSU9OX1ZBUklBQkxFEAESJgoiUExBQ0VIT0xERVJfQ09ORklHVVJBVElPTl9WQVJJQUJMRRACKkEKCkhUVFBNZXRob2QSBwoDR0VUEAASCAoEUE9TVBABEgcKA1BVVBACEgoKBkRFTEVURRADEgsKB09QVElPTlMQBDLSAQoLTm9kZVNlcnZpY2USXwoMU2VsZlJlZ2lzdGVyEiUud2cuY29zbW8ubm9kZS52MS5TZWxmUmVnaXN0ZXJSZXF1ZXN0GiYud2cuY29zbW8ubm9kZS52MS5TZWxmUmVnaXN0ZXJSZXNwb25zZSIAEmIKDUdlbmVyYXRlUXVlcnkSJi53Zy5jb3Ntby5ub2RlLnYxLkdlbmVyYXRlUXVlcnlSZXF1ZXN0Gicud2cuY29zbW8ubm9kZS52MS5HZW5lcmF0ZVF1ZXJ5UmVzcG9uc2UiAGIGcHJvdG8z", [file_wg_cosmo_common_common]); /** * @generated from message wg.cosmo.node.v1.Subgraph @@ -366,6 +366,13 @@ export type EngineConfiguration = Message<"wg.cosmo.node.v1.EngineConfiguration" * @generated from field: optional string graphql_client_schema = 7; */ graphqlClientSchema?: string | undefined; + + /** + * Content hash of graphqlSchema, formatted as "sha256:". + * + * @generated from field: string schema_hash = 8; + */ + schemaHash: string; }; /** diff --git a/controlplane/src/core/composition/composer.ts b/controlplane/src/core/composition/composer.ts index 0cc04971a6..658feb49e7 100644 --- a/controlplane/src/core/composition/composer.ts +++ b/controlplane/src/core/composition/composer.ts @@ -40,7 +40,7 @@ import { NamespaceRepository } from '../repositories/NamespaceRepository.js'; import { InspectorSchemaChange } from '../services/SchemaUsageTrafficInspector.js'; import { SchemaCheckChangeAction } from '../../db/models.js'; import { traced } from '../tracing.js'; -import { PromptToQueryService } from '../services/PromptToQueryService.js'; +import { getSchemaHash, PromptToQueryService } from '../services/PromptToQueryService.js'; import { composeGraphsInWorker, DeserializedComposedGraph, @@ -496,6 +496,11 @@ export class Composer { }; } + if (!composedGraph.composedSchema || !routerExecutionConfig.engineConfig) { + throw new Error('A valid composition must include a composed schema and an engine configuration.'); + } + routerExecutionConfig.engineConfig.schemaHash = getSchemaHash(composedGraph.composedSchema); + let schemaChanges: GetDiffBetweenGraphsResult; // Prioritize diff against client schemas if no previous valid schema available or if both prev and current client schema is available. diff --git a/controlplane/src/core/services/PromptToQueryService.ts b/controlplane/src/core/services/PromptToQueryService.ts index 909967da3f..c29cd35ccc 100644 --- a/controlplane/src/core/services/PromptToQueryService.ts +++ b/controlplane/src/core/services/PromptToQueryService.ts @@ -1,3 +1,4 @@ +import { createHash } from 'node:crypto'; import { PostgresJsDatabase } from 'drizzle-orm/postgres-js'; import { FastifyBaseLogger } from 'fastify'; import { type AxiosInstance, create as createHttpClient } from 'axios'; @@ -38,6 +39,10 @@ const ptqResponseSchema = z.object({ type PtQResponse = z.infer; +export function getSchemaHash(schema: string): string { + return `sha256:${createHash('sha256').update(schema).digest('hex')}`; +} + @traced export class PromptToQueryService { readonly #httpClient: AxiosInstance; diff --git a/controlplane/test/router-config.test.ts b/controlplane/test/router-config.test.ts index 190a59fb3b..4332742586 100644 --- a/controlplane/test/router-config.test.ts +++ b/controlplane/test/router-config.test.ts @@ -1,3 +1,4 @@ +import { createHash } from 'node:crypto'; import { noBaseDefinitionForExtensionError, noQueryRootTypeError, OBJECT } from '@wundergraph/composition'; import { EnumStatusCode } from '@wundergraph/cosmo-connect/dist/common/common_pb'; import { joinLabel, routerConfigFromJsonString } from '@wundergraph/cosmo-shared'; @@ -217,6 +218,11 @@ describe('Router Config', (ctx) => { const config = routerConfigFromJsonString(configJsonString); expect(config).toBeDefined(); + expect(config?.engineConfig?.schemaHash).toBe( + `sha256:${createHash('sha256') + .update(config?.engineConfig?.graphqlSchema || '') + .digest('hex')}`, + ); }); test('Should not return routerConfig if an invalid schema version is available', async (testContext) => { diff --git a/docs-website/router/mcp/oauth/configuration.mdx b/docs-website/router/mcp/oauth/configuration.mdx index 054f288906..775ab5d040 100644 --- a/docs-website/router/mcp/oauth/configuration.mdx +++ b/docs-website/router/mcp/oauth/configuration.mdx @@ -18,6 +18,7 @@ icon: 'sliders-up' | `oauth.scopes.execute_graphql` | Scopes required to call the `execute_graphql` built-in tool. Additive to `tools_call`. Only relevant when `enable_arbitrary_operations` is `true`. | `[]` | | `oauth.scopes.get_operation_info` | Scopes required to call the `get_operation_info` built-in tool. Additive to `tools_call`. | `[]` | | `oauth.scopes.get_schema` | Scopes required to call the `get_schema` built-in tool. Additive to `tools_call`. Only relevant when `expose_schema` is `true`. | `[]` | +| `oauth.scopes.generate_query` | Scopes required to call the `generate_query` built-in tool. Additive to `tools_call`. Only relevant when a graph-token-backed control-plane client is configured. | `[]` | | `oauth.jwks` | List of JWKS providers for JWT verification. Supports remote JWKS URLs or symmetric secrets. | `[]` | ## JWKS Configuration @@ -67,6 +68,7 @@ oauth: | `MCP_OAUTH_AUTHORIZATION_SERVER_URL` | `mcp.oauth.authorization_server_url` | | `MCP_OAUTH_SCOPE_CHALLENGE_INCLUDE_TOKEN_SCOPES` | `mcp.oauth.scope_challenge_include_token_scopes` | | `MCP_OAUTH_MAX_SCOPE_COMBINATIONS` | `mcp.oauth.max_scope_combinations` | +| `MCP_OAUTH_SCOPES_GENERATE_QUERY` | `mcp.oauth.scopes.generate_query` | ## HTTP Error Responses diff --git a/docs-website/router/mcp/tools.mdx b/docs-website/router/mcp/tools.mdx index 6523d97cf2..644f88c23c 100644 --- a/docs-website/router/mcp/tools.mdx +++ b/docs-website/router/mcp/tools.mdx @@ -16,6 +16,7 @@ The MCP server gives AI models a set of tools they can discover and execute. It | `get_operation_info` | Returns instructions for executing the operation behind one of your tools directly via HTTP and integrating it into an application. | | `get_schema` | Returns the full GraphQL schema of the API, helping AI models understand the entire API structure. | | `execute_graphql` | Executes an arbitrary GraphQL query or mutation, letting AI models craft operations beyond the tools you have created. | +| `generate_query` | Uses Cosmo Cloud Prompt to Query to generate a GraphQL operation from a natural-language prompt. | `get_schema` and `execute_graphql` are disabled by default because they expose your full API surface to AI models. @@ -24,6 +25,35 @@ The MCP server gives AI models a set of tools they can discover and execute. It intended. Prefer creating focused tools. +### Generate a GraphQL Operation with Cosmo Cloud + +The `generate_query` tool is available when the router has a graph token and router registration is enabled. It sends the +prompt and a hash of the router's active schema to Cosmo Cloud's Prompt to Query service. The generated operation is +returned to the MCP client but is not executed. + +The tool accepts one required field: + +```json +{ + "prompt": "List the names and email addresses of the five most recently created users" +} +``` + +A successful response contains the GraphQL document and the metadata needed to use it: + +```json +{ + "description": "Lists the five most recently created users", + "document": "query RecentUsers { users(first: 5, orderBy: CREATED_AT_DESC) { name email } }", + "operationName": "RecentUsers", + "operationType": "query", + "variablesSchema": "{\"type\":\"object\"}" +} +``` + +Availability also depends on the Prompt to Query feature being enabled for the Cosmo Cloud organization associated with +the graph token. Control-plane or entitlement errors are returned as MCP tool errors. + ## Creating Tools Create a directory for your tools (as specified in your [storage provider configuration](/router/mcp/configuration#storage-providers)) and add `.graphql` or `.gql` files containing GraphQL operations. diff --git a/mcp-yoko-run.sh b/mcp-yoko-run.sh new file mode 100755 index 0000000000..13e7fce1cc --- /dev/null +++ b/mcp-yoko-run.sh @@ -0,0 +1,766 @@ +#!/usr/bin/env bash + +set -Eeuo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +YOKO_HOST="${MCP_YOKO_HOST:-127.0.0.1}" +YOKO_PORT="${MCP_YOKO_PORT:-5055}" +CONTROLPLANE_HOST="${MCP_YOKO_CONTROLPLANE_HOST:-127.0.0.1}" +CONTROLPLANE_PORT="${MCP_YOKO_CONTROLPLANE_PORT:-3001}" +ROUTER_HOST="${MCP_YOKO_ROUTER_HOST:-127.0.0.1}" +ROUTER_PORT="${MCP_YOKO_ROUTER_PORT:-3002}" +MCP_HOST="${MCP_YOKO_MCP_HOST:-127.0.0.1}" +MCP_PORT="${MCP_YOKO_MCP_PORT:-5025}" +GRAPH_NAME="${MCP_YOKO_GRAPH_NAME:-mcp-yoko}" +NAMESPACE="${MCP_YOKO_NAMESPACE:-default}" +ORGANIZATION_SLUG="${MCP_YOKO_ORGANIZATION_SLUG:-wundergraph}" + +FAKE_YOKO_ONLY=false +SKIP_INSTALL=false + +usage() { + cat <<'USAGE' +Usage: ./mcp-yoko-run.sh [options] + +Starts the local Cosmo infrastructure, a source-built control plane and router, +the demo subgraphs, and a fake Yoko service backed by `codex exec`. + +Options: + --fake-yoko-only Start only the fake Yoko HTTP service. + --skip-install Do not install/build the JavaScript workspaces. + -h, --help Show this help. + +Useful environment variables: + MCP_YOKO_CODEX_MODEL Optional model passed to `codex exec`. + MCP_YOKO_CODEX_REASONING Reasoning effort (default: low). + MCP_YOKO_CODEX_TIMEOUT Codex timeout in seconds (default: 180). + MCP_YOKO_PORT Fake Yoko port (default: 5055). + MCP_YOKO_MCP_PORT Router MCP port (default: 5025). + MCP_YOKO_GRAPH_NAME Federated graph name (default: mcp-yoko). + MCP_YOKO_FORCE_BUILD=1 Rebuild JavaScript workspaces even if present. + MCP_YOKO_RUN_DIR Directory in which to keep logs and run state. + +The Compose infrastructure is intentionally left running when this script +exits. Stop it later with `make infra-down`. +USAGE +} + +while (($# > 0)); do + case "$1" in + --fake-yoko-only) + FAKE_YOKO_ONLY=true + ;; + --skip-install) + SKIP_INSTALL=true + ;; + -h | --help) + usage + exit 0 + ;; + *) + echo "Unknown option: $1" >&2 + usage >&2 + exit 2 + ;; + esac + shift +done + +log() { + printf '[mcp-yoko] %s\n' "$*" +} + +die() { + printf '[mcp-yoko] ERROR: %s\n' "$*" >&2 + exit 1 +} + +require_command() { + command -v "$1" >/dev/null 2>&1 || die "Required command '$1' was not found." +} + +validate_name() { + [[ "$2" =~ ^[A-Za-z0-9._-]+$ ]] || die "$1 contains unsupported characters: $2" +} + +validate_port() { + [[ "$2" =~ ^[0-9]+$ ]] && ((10#$2 >= 1 && 10#$2 <= 65535)) || die "$1 must be a TCP port: $2" +} + +validate_name "graph name" "$GRAPH_NAME" +validate_name "namespace" "$NAMESPACE" +validate_name "organization slug" "$ORGANIZATION_SLUG" +validate_port "Yoko port" "$YOKO_PORT" +validate_port "control-plane port" "$CONTROLPLANE_PORT" +validate_port "router port" "$ROUTER_PORT" +validate_port "MCP port" "$MCP_PORT" + +require_command python3 +require_command curl +require_command codex +codex login status >/dev/null 2>&1 || die "Codex is not authenticated. Run 'codex login' and rerun the script." + +if [[ -n "${MCP_YOKO_RUN_DIR:-}" ]]; then + RUN_DIR="$MCP_YOKO_RUN_DIR" + mkdir -p "$RUN_DIR" +else + RUN_DIR="$(mktemp -d "${TMPDIR:-/tmp}/cosmo-mcp-yoko.XXXXXX")" +fi +STATE_DIR="$RUN_DIR/yoko-state" +mkdir -p "$STATE_DIR" + +PIDS=() +PROCESS_NAMES=() + +remember_process() { + PIDS+=("$1") + PROCESS_NAMES+=("$2") +} + +terminate_tree() { + local pid="$1" + local child + + if command -v pgrep >/dev/null 2>&1; then + while IFS= read -r child; do + [[ -n "$child" ]] && terminate_tree "$child" + done < <(pgrep -P "$pid" 2>/dev/null || true) + fi + + kill -TERM "$pid" 2>/dev/null || true +} + +cleanup() { + local status=$? + local index + + trap - EXIT INT TERM HUP + if ((${#PIDS[@]} > 0)); then + log "Stopping local processes..." + for ((index = ${#PIDS[@]} - 1; index >= 0; index--)); do + terminate_tree "${PIDS[$index]}" + done + for index in "${!PIDS[@]}"; do + wait "${PIDS[$index]}" 2>/dev/null || true + done + fi + + if ((status != 0)); then + log "A process failed. Recent logs:" + for index in "${!PROCESS_NAMES[@]}"; do + if [[ -f "$RUN_DIR/${PROCESS_NAMES[$index]}.log" ]]; then + printf '\n--- %s ---\n' "${PROCESS_NAMES[$index]}" + tail -n 25 "$RUN_DIR/${PROCESS_NAMES[$index]}.log" || true + fi + done + fi + + log "Logs and fake-Yoko state: $RUN_DIR" + exit "$status" +} + +trap cleanup EXIT +trap 'exit 0' INT TERM HUP + +require_free_port() { + local host="$1" + local port="$2" + local label="$3" + + python3 - "$host" "$port" <<'PY' || die "$label cannot bind to $host:$port; the port is already in use." +import socket +import sys + +host, port = sys.argv[1], int(sys.argv[2]) +family = socket.AF_INET6 if ":" in host else socket.AF_INET +with socket.socket(family, socket.SOCK_STREAM) as sock: + sock.bind((host, port)) +PY +} + +wait_for_http() { + local url="$1" + local label="$2" + local attempts="${3:-120}" + local attempt + + for ((attempt = 1; attempt <= attempts; attempt++)); do + if curl --silent --show-error --fail --max-time 2 "$url" >/dev/null 2>&1; then + return 0 + fi + sleep 1 + done + die "Timed out waiting for $label at $url" +} + +monitor_processes() { + local index + while true; do + for index in "${!PIDS[@]}"; do + if ! kill -0 "${PIDS[$index]}" 2>/dev/null; then + wait "${PIDS[$index]}" || true + die "${PROCESS_NAMES[$index]} exited unexpectedly." + fi + done + sleep 2 + done +} + +start_fake_yoko() { + require_free_port "$YOKO_HOST" "$YOKO_PORT" "fake Yoko" + log "Starting fake Yoko on http://$YOKO_HOST:$YOKO_PORT" + + python3 -u - "$YOKO_HOST" "$YOKO_PORT" "$STATE_DIR" <<'PY' >"$RUN_DIR/fake-yoko.log" 2>&1 & +import hashlib +import json +import os +from pathlib import Path +import re +import subprocess +import sys +import tempfile +import threading +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer + +host, port, state_arg = sys.argv[1], int(sys.argv[2]), sys.argv[3] +state_dir = Path(state_arg).resolve() +index_dir = state_dir / "indexes" +index_dir.mkdir(parents=True, exist_ok=True) + +output_schema = { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "additionalProperties": False, + "required": ["resolution"], + "properties": { + "resolution": { + "type": "object", + "additionalProperties": False, + "required": ["queries", "unsatisfied"], + "properties": { + "queries": { + "type": "array", + "maxItems": 1, + "items": { + "type": "object", + "additionalProperties": False, + "required": [ + "description", + "document", + "operationName", + "operationType", + "variablesSchema", + ], + "properties": { + "description": {"type": "string"}, + "document": {"type": "string", "minLength": 1}, + "operationName": {"type": "string", "minLength": 1}, + "operationType": { + "type": "string", + "enum": ["query", "mutation", "subscription"], + }, + "variablesSchema": {"type": "string"}, + }, + }, + }, + "unsatisfied": { + "type": "array", + "maxItems": 1, + "items": { + "type": "object", + "additionalProperties": False, + "required": ["reason"], + "properties": {"reason": {"type": "string", "minLength": 1}}, + }, + }, + }, + } + }, +} +schema_path = state_dir / "codex-output-schema.json" +schema_path.write_text(json.dumps(output_schema), encoding="utf-8") + +codex_bin = os.environ.get("MCP_YOKO_CODEX_BIN", "codex") +codex_model = os.environ.get("MCP_YOKO_CODEX_MODEL", "").strip() +codex_reasoning = os.environ.get("MCP_YOKO_CODEX_REASONING", "low").strip() +codex_timeout = int(os.environ.get("MCP_YOKO_CODEX_TIMEOUT", "180")) +codex_slots = threading.BoundedSemaphore(int(os.environ.get("MCP_YOKO_CODEX_CONCURRENCY", "2"))) + + +def resolution_error(reason): + return {"resolution": {"queries": [], "unsatisfied": [{"reason": reason}]}} + + +def validate_resolution(value): + if not isinstance(value, dict) or not isinstance(value.get("resolution"), dict): + raise ValueError("Codex did not return a resolution object") + + resolution = value["resolution"] + queries = resolution.get("queries") + unsatisfied = resolution.get("unsatisfied") + if not isinstance(queries, list) or not isinstance(unsatisfied, list): + raise ValueError("Codex returned invalid resolution lists") + if len(queries) + len(unsatisfied) != 1: + raise ValueError("Codex must return exactly one query or one unsatisfied reason") + + if unsatisfied: + reason = unsatisfied[0].get("reason") if isinstance(unsatisfied[0], dict) else None + if not isinstance(reason, str) or not reason.strip(): + raise ValueError("Codex returned an empty unsatisfied reason") + return resolution_error(reason.strip()) + + query = queries[0] + if not isinstance(query, dict): + raise ValueError("Codex returned an invalid query") + for key in ("description", "document", "operationName", "operationType", "variablesSchema"): + if not isinstance(query.get(key), str): + raise ValueError(f"Codex query field {key} is not a string") + if not query["document"].strip() or not re.fullmatch(r"[_A-Za-z][_0-9A-Za-z]*", query["operationName"]): + raise ValueError("Codex returned an invalid GraphQL document or operation name") + if query["operationType"] not in ("query", "mutation", "subscription"): + raise ValueError("Codex returned an invalid operation type") + try: + variables_schema = json.loads(query["variablesSchema"] or "{}") + except json.JSONDecodeError as exc: + raise ValueError("Codex returned an invalid variables JSON Schema") from exc + if not isinstance(variables_schema, dict): + raise ValueError("Codex variables JSON Schema is not an object") + query["variablesSchema"] = json.dumps(variables_schema, separators=(",", ":")) + query["description"] = query["description"].strip() + query["document"] = query["document"].strip() + return {"resolution": {"queries": [query], "unsatisfied": []}} + + +def generate_query(index_id, prompt): + digest = index_id.removeprefix("sha256:") + schema_file = index_dir / f"{digest}.graphql" + if not re.fullmatch(r"[0-9a-fA-F]{64}", digest) or not schema_file.is_file(): + return resolution_error(f"No schema is indexed for {index_id}") + + graphql_schema = schema_file.read_text(encoding="utf-8") + instructions = f"""You generate exactly one GraphQL operation from a natural-language request. + +Use only fields and argument types present in the supplied schema. Give the operation a descriptive +GraphQL operation name. Return a JSON object matching the provided output schema. variablesSchema +must itself be a JSON-encoded JSON Schema object describing the operation variables; use \"{{}}\" +when there are no variables. If the request cannot be satisfied by the schema, return no queries and +one concise unsatisfied reason. Do not execute commands, inspect files, or explain the answer outside +the structured response. + + +{prompt} + + + +{graphql_schema} + +""" + + request_dir = Path(tempfile.mkdtemp(prefix="request-", dir=state_dir)) + output_file = request_dir / "result.json" + command = [ + codex_bin, + "exec", + "--ephemeral", + "--sandbox", + "read-only", + "--skip-git-repo-check", + "--color", + "never", + "--output-schema", + str(schema_path), + "--output-last-message", + str(output_file), + "-C", + str(request_dir), + ] + if codex_model: + command.extend(["--model", codex_model]) + if codex_reasoning: + command.extend(["--config", f"model_reasoning_effort={codex_reasoning}"]) + command.append("-") + + try: + with codex_slots: + completed = subprocess.run( + command, + input=instructions, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + timeout=codex_timeout, + check=False, + ) + except subprocess.TimeoutExpired: + return resolution_error(f"Codex timed out after {codex_timeout} seconds") + except OSError as exc: + print(f"Could not start Codex: {exc}", flush=True) + return resolution_error("The local Codex process could not be started") + + if completed.returncode != 0 or not output_file.is_file(): + print(f"Codex failed with exit code {completed.returncode}:\n{completed.stdout}", flush=True) + return resolution_error("Codex failed to generate a query; inspect fake-yoko.log") + + try: + return validate_resolution(json.loads(output_file.read_text(encoding="utf-8"))) + except (OSError, json.JSONDecodeError, ValueError) as exc: + print(f"Invalid Codex response: {exc}\n{completed.stdout}", flush=True) + return resolution_error("Codex returned an invalid query response; inspect fake-yoko.log") + + +class Handler(BaseHTTPRequestHandler): + server_version = "FakeYokoCodex/1.0" + + def log_message(self, format_string, *args): + print(f"{self.address_string()} - {format_string % args}", flush=True) + + def send_json(self, status, value): + body = json.dumps(value, separators=(",", ":")).encode("utf-8") + self.send_response(status) + self.send_header("Content-Type", "application/json") + self.send_header("Content-Length", str(len(body))) + self.end_headers() + self.wfile.write(body) + + def read_json(self): + try: + length = int(self.headers.get("Content-Length", "0")) + except ValueError as exc: + raise ValueError("Invalid Content-Length") from exc + if length <= 0 or length > 32 * 1024 * 1024: + raise ValueError("Request body is empty or too large") + try: + return json.loads(self.rfile.read(length)) + except json.JSONDecodeError as exc: + raise ValueError("Request body is not valid JSON") from exc + + def do_GET(self): + if self.path == "/health": + self.send_json(200, {"status": "ok", "backend": "codex"}) + return + self.send_json(404, {"error": "not found"}) + + def do_POST(self): + try: + body = self.read_json() + if not isinstance(body, dict): + raise ValueError("Request body must be an object") + + if self.path == "/yoko.v1.YokoService/EnsureIndex": + sdl = body.get("sdl") + if not isinstance(sdl, str) or not sdl.strip(): + raise ValueError("sdl must be a non-empty string") + digest = hashlib.sha256(sdl.encode("utf-8")).hexdigest() + index_id = f"sha256:{digest}" + (index_dir / f"{digest}.graphql").write_text(sdl, encoding="utf-8") + print(f"Indexed {index_id} ({len(sdl)} bytes)", flush=True) + self.send_json(200, {"indexId": index_id}) + return + + if self.path == "/yoko.v1.YokoService/GenerateQuery": + index_id = body.get("indexId") + prompt = body.get("prompt") + if not isinstance(index_id, str) or not index_id: + raise ValueError("indexId must be a non-empty string") + if not isinstance(prompt, str) or not prompt.strip(): + raise ValueError("prompt must be a non-empty string") + print(f"Generating a query for {index_id}: {prompt.strip()}", flush=True) + self.send_json(200, generate_query(index_id, prompt.strip())) + return + + self.send_json(404, {"error": "not found"}) + except ValueError as exc: + self.send_json(400, {"error": str(exc)}) + except BrokenPipeError: + print("Client disconnected before the response was sent", flush=True) + + +server = ThreadingHTTPServer((host, port), Handler) +print(f"Fake Yoko listening on http://{host}:{port}", flush=True) +try: + server.serve_forever() +except KeyboardInterrupt: + pass +finally: + server.server_close() +PY + + remember_process "$!" "fake-yoko" + wait_for_http "http://$YOKO_HOST:$YOKO_PORT/health" "fake Yoko" 30 +} + +start_fake_yoko + +if [[ "$FAKE_YOKO_ONLY" == true ]]; then + log "Fake Yoko is ready. Press Ctrl-C to stop it." + log "Health: http://$YOKO_HOST:$YOKO_PORT/health" + monitor_processes +fi + +require_command make +require_command go +require_command docker + +if command -v pnpm >/dev/null 2>&1; then + PNPM=(pnpm) +elif command -v corepack >/dev/null 2>&1; then + mkdir -p "$RUN_DIR/bin" + python3 - "$RUN_DIR/bin/pnpm" <<'PY' +from pathlib import Path +import sys + +wrapper = Path(sys.argv[1]) +wrapper.write_text('#!/usr/bin/env bash\nexec corepack pnpm "$@"\n', encoding="utf-8") +wrapper.chmod(0o755) +PY + export PATH="$RUN_DIR/bin:$PATH" + PNPM=(pnpm) +else + die "pnpm was not found, and corepack is not available to provide it." +fi + +pnpm_run() { + "${PNPM[@]}" "$@" +} + +wgc() { + pnpm_run --silent --filter ./cli wgc "$@" +} + +docker info >/dev/null 2>&1 || die "Docker is installed but its daemon is not reachable. Start Docker and rerun the script." +docker compose version >/dev/null 2>&1 || die "The Docker Compose plugin is required." + +for port_spec in \ + "$CONTROLPLANE_HOST:$CONTROLPLANE_PORT:control-plane" \ + "$ROUTER_HOST:$ROUTER_PORT:router" \ + "$MCP_HOST:$MCP_PORT:MCP" \ + "127.0.0.1:4001:employees-subgraph" \ + "127.0.0.1:4002:family-subgraph" \ + "127.0.0.1:4003:hobbies-subgraph" \ + "127.0.0.1:4004:products-subgraph" \ + "127.0.0.1:4006:test-subgraph" \ + "127.0.0.1:4007:availability-subgraph" \ + "127.0.0.1:4008:mood-subgraph" \ + "127.0.0.1:4009:countries-subgraph" \ + "127.0.0.1:4010:products-feature-subgraph"; do + IFS=: read -r check_host check_port check_label <<<"$port_spec" + require_free_port "$check_host" "$check_port" "$check_label" +done + +if [[ ! -f "$ROOT_DIR/controlplane/.env" ]]; then + cp "$ROOT_DIR/controlplane/.env.example" "$ROOT_DIR/controlplane/.env" + log "Created controlplane/.env from its example." +fi +if [[ ! -f "$ROOT_DIR/cli/.env" ]]; then + cp "$ROOT_DIR/cli/.env.example" "$ROOT_DIR/cli/.env" + log "Created cli/.env from its example." +fi + +needs_workspace_build=false +if [[ ! -d "$ROOT_DIR/node_modules" || ! -d "$ROOT_DIR/connect/dist" ]]; then + needs_workspace_build=true +fi +if [[ "${MCP_YOKO_FORCE_BUILD:-0}" == "1" ]]; then + needs_workspace_build=true +fi + +if [[ "$SKIP_INSTALL" == false && "$needs_workspace_build" == true ]]; then + log "Installing and building JavaScript workspaces (first run can take several minutes)..." + ( + cd "$ROOT_DIR" + pnpm_run install + pnpm_run generate + pnpm_run -r run --filter '!studio' build + ) >"$RUN_DIR/workspace-build.log" 2>&1 || die "JavaScript workspace build failed; inspect $RUN_DIR/workspace-build.log" +elif [[ "$SKIP_INSTALL" == true ]]; then + [[ -d "$ROOT_DIR/node_modules" && -d "$ROOT_DIR/connect/dist" ]] || die "--skip-install was used, but workspace dependencies/build output are missing." + log "Skipping JavaScript install/build." +else + log "Reusing existing JavaScript workspace dependencies and build output." +fi + +log "Starting the repository's Compose development infrastructure..." +( + cd "$ROOT_DIR" + make infra-up +) >"$RUN_DIR/compose.log" 2>&1 || die "Compose startup failed; inspect $RUN_DIR/compose.log" + +wait_for_http "http://127.0.0.1:8123/ping" "ClickHouse" 180 +wait_for_http "http://127.0.0.1:8080/realms/cosmo" "Keycloak" 180 +wait_for_http "http://127.0.0.1:11000/health" "CDN" 180 + +log "Running control-plane migrations and the idempotent local seed..." +( + cd "$ROOT_DIR" + make migrate + make seed +) >"$RUN_DIR/bootstrap.log" 2>&1 || die "Migrations or seed failed; inspect $RUN_DIR/bootstrap.log" + +psql_controlplane() { + docker compose --file "$ROOT_DIR/docker-compose.yml" --profile dev exec -T postgres \ + psql -X -q -v ON_ERROR_STOP=1 -U "${POSTGRES_USER:-postgres}" -d "${POSTGRES_DB:-controlplane}" "$@" +} + +log "Enabling the prompt-to-query organization feature..." +psql_controlplane <"$RUN_DIR/feature-flag.log" +INSERT INTO organization_features (organization_id, feature, enabled) +SELECT id, 'prompt-to-query', TRUE +FROM organizations +WHERE slug = '$ORGANIZATION_SLUG' +ON CONFLICT (organization_id, feature) +DO UPDATE SET enabled = EXCLUDED.enabled; +SQL + +feature_enabled="$(psql_controlplane -Atc "SELECT COUNT(*) FROM organization_features f JOIN organizations o ON o.id = f.organization_id WHERE o.slug = '$ORGANIZATION_SLUG' AND f.feature = 'prompt-to-query' AND f.enabled = TRUE;")" +[[ "$feature_enabled" == "1" ]] || die "Could not enable prompt-to-query for organization '$ORGANIZATION_SLUG'." + +log "Starting the control plane with fake Yoko configured..." +( + cd "$ROOT_DIR/controlplane" + exec env \ + HOST="$CONTROLPLANE_HOST" \ + PORT="$CONTROLPLANE_PORT" \ + DEBUG_SQL=false \ + PROMPT_TO_QUERY_SERVICE_ENDPOINT="http://$YOKO_HOST:$YOKO_PORT" \ + "${PNPM[@]}" exec tsx src/index.ts +) >"$RUN_DIR/controlplane.log" 2>&1 & +remember_process "$!" "controlplane" +wait_for_http "http://$CONTROLPLANE_HOST:$CONTROLPLANE_PORT/health" "control plane" 180 + +export COSMO_API_KEY="cosmo_669b576aaadc10ee1ae81d9193425705" +export COSMO_API_URL="http://$CONTROLPLANE_HOST:$CONTROLPLANE_PORT" +export KC_API_URL="http://127.0.0.1:8080" +export CDN_URL="http://127.0.0.1:11000" +export DO_NOT_TRACK=1 + +target_exists() { + local target_name="$1" + local target_type="$2" + local result + + result="$(psql_controlplane -Atc "SELECT EXISTS (SELECT 1 FROM targets t JOIN namespaces n ON n.id = t.namespace_id JOIN organizations o ON o.id = t.organization_id WHERE t.name = '$target_name' AND t.type = '$target_type' AND n.name = '$NAMESPACE' AND o.slug = '$ORGANIZATION_SLUG');")" + [[ "$result" == "t" ]] +} + +log "Creating or refreshing the isolated '$GRAPH_NAME' demo graph..." +if ! target_exists "$GRAPH_NAME" "federated"; then + wgc federated-graph create "$GRAPH_NAME" \ + --namespace "$NAMESPACE" \ + --label-matcher team=A,team=B \ + --routing-url "http://$ROUTER_HOST:$ROUTER_PORT/graphql" \ + >>"$RUN_DIR/graph-setup.log" 2>&1 +else + wgc federated-graph update "$GRAPH_NAME" \ + --namespace "$NAMESPACE" \ + --label-matcher team=A,team=B \ + --routing-url "http://$ROUTER_HOST:$ROUTER_PORT/graphql" \ + >>"$RUN_DIR/graph-setup.log" 2>&1 +fi + +SUBGRAPH_NAMES=(employees family hobbies products availability mood) +SUBGRAPH_DIRS=(employees family hobbies products availability mood) +SUBGRAPH_PORTS=(4001 4002 4003 4004 4007 4008) +SUBGRAPH_TEAMS=(A A B B A B) + +for index in "${!SUBGRAPH_NAMES[@]}"; do + subgraph_name="${SUBGRAPH_NAMES[$index]}" + if ! target_exists "$subgraph_name" "subgraph"; then + wgc subgraph create "$subgraph_name" \ + --namespace "$NAMESPACE" \ + --label "team=${SUBGRAPH_TEAMS[$index]}" \ + --routing-url "http://127.0.0.1:${SUBGRAPH_PORTS[$index]}/graphql" \ + >>"$RUN_DIR/graph-setup.log" 2>&1 + else + wgc subgraph update "$subgraph_name" \ + --namespace "$NAMESPACE" \ + --label "team=${SUBGRAPH_TEAMS[$index]}" \ + --routing-url "http://127.0.0.1:${SUBGRAPH_PORTS[$index]}/graphql" \ + >>"$RUN_DIR/graph-setup.log" 2>&1 + fi + + wgc subgraph publish "$subgraph_name" \ + --namespace "$NAMESPACE" \ + --schema "$ROOT_DIR/demo/pkg/subgraphs/${SUBGRAPH_DIRS[$index]}/subgraph/schema.graphqls" \ + >>"$RUN_DIR/graph-setup.log" 2>&1 +done + +wgc federated-graph recompose "$GRAPH_NAME" \ + --namespace "$NAMESPACE" \ + --fail-on-composition-error \ + >>"$RUN_DIR/graph-setup.log" 2>&1 + +token_name="mcp-yoko-run-$(date +%s)-$$" +router_token="$(wgc router token create "$token_name" --graph-name "$GRAPH_NAME" --namespace "$NAMESPACE" --raw | tail -n 1 | tr -d '\r')" +[[ -n "$router_token" && "$router_token" != *[[:space:]]* ]] || die "The router token command did not return a usable token." + +log "Building the router and local demo subgraphs..." +( + cd "$ROOT_DIR/router" + CGO_ENABLED=0 go build -o "$RUN_DIR/router" ./cmd/router +) >"$RUN_DIR/router-build.log" 2>&1 || die "Router build failed; inspect $RUN_DIR/router-build.log" +( + cd "$ROOT_DIR/demo" + CGO_ENABLED=0 go build -o "$RUN_DIR/subgraphs" ./cmd/all +) >"$RUN_DIR/subgraphs-build.log" 2>&1 || die "Subgraph build failed; inspect $RUN_DIR/subgraphs-build.log" + +log "Starting local demo subgraphs..." +"$RUN_DIR/subgraphs" >"$RUN_DIR/subgraphs.log" 2>&1 & +remember_process "$!" "subgraphs" +wait_for_http "http://127.0.0.1:4001/" "employees subgraph" 60 + +log "Starting the source-built router and MCP server..." +( + cd "$ROOT_DIR/router" + exec env \ + CONFIG_PATH=demo.config.yaml \ + DEV_MODE=true \ + LOG_LEVEL=info \ + JSON_LOG=false \ + LISTEN_ADDR="$ROUTER_HOST:$ROUTER_PORT" \ + CONTROLPLANE_URL="http://$CONTROLPLANE_HOST:$CONTROLPLANE_PORT" \ + CDN_URL="http://127.0.0.1:11000" \ + DEFAULT_TELEMETRY_ENDPOINT="http://127.0.0.1:4318" \ + GRAPHQL_METRICS_COLLECTOR_ENDPOINT="http://127.0.0.1:4005" \ + GRAPH_API_TOKEN="$router_token" \ + ROUTER_REGISTRATION=true \ + MCP_ENABLED=true \ + MCP_GRAPH_NAME="$GRAPH_NAME" \ + MCP_SERVER_LISTEN_ADDR="$MCP_HOST:$MCP_PORT" \ + "$RUN_DIR/router" +) >"$RUN_DIR/router.log" 2>&1 & +remember_process "$!" "router" + +wait_for_http "http://$ROUTER_HOST:$ROUTER_PORT/health/ready" "router" 180 + +tools_response="$(curl --silent --show-error --fail \ + --header 'Content-Type: application/json' \ + --header 'Accept: application/json, text/event-stream' \ + --data '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' \ + "http://$MCP_HOST:$MCP_PORT/mcp")" || die "The router started, but its MCP tools/list request failed." +[[ "$tools_response" == *'generate_query'* ]] || die "MCP tools/list did not expose generate_query; inspect $RUN_DIR/router.log" + +cat < string_storage = 6; optional string graphql_client_schema = 7; + // Content hash of graphqlSchema, formatted as "sha256:". + string schema_hash = 8; } message DataSourceConfiguration { diff --git a/router/core/graph_server.go b/router/core/graph_server.go index 3cc45d5f9e..79e1701181 100644 --- a/router/core/graph_server.go +++ b/router/core/graph_server.go @@ -1611,7 +1611,7 @@ func (s *graphServer) buildGraphMux( // We support the MCP only on the base graph. Feature flags are not supported yet. if opts.IsBaseGraph() && s.mcpServer != nil { - if mErr := s.mcpServer.Reload(executor.ClientSchema, opts.EngineConfig.FieldConfigurations); mErr != nil { + if mErr := s.mcpServer.Reload(executor.ClientSchema, opts.EngineConfig.FieldConfigurations, opts.EngineConfig.GetSchemaHash()); mErr != nil { return nil, fmt.Errorf("failed to reload MCP server: %w", mErr) } } diff --git a/router/core/router.go b/router/core/router.go index 3491a7e516..b1028d6bbc 100644 --- a/router/core/router.go +++ b/router/core/router.go @@ -1218,6 +1218,9 @@ func (r *Router) startMCPServer(ctx context.Context) error { mcpserver.WithServerTitle(r.mcp.Server.Title), mcpserver.WithServerDescription(r.mcp.Server.Description), } + if r.promptToQueryClient != nil { + mcpOpts = append(mcpOpts, mcpserver.WithPromptToQueryClient(r.promptToQueryClient)) + } if r.corsOptions != nil { mcpOpts = append(mcpOpts, mcpserver.WithCORS(*r.corsOptions)) @@ -2089,6 +2092,13 @@ func WithSelfRegistration(sr selfregister.SelfRegister) Option { } } +// WithPromptToQueryClient sets the control-plane client used by the MCP generate_query tool. +func WithPromptToQueryClient(client mcpserver.PromptToQueryClient) Option { + return func(r *Router) { + r.promptToQueryClient = client + } +} + // WithGracePeriod sets the grace period for the router to shutdown. func WithGracePeriod(timeout time.Duration) Option { return func(r *Router) { diff --git a/router/core/router_config.go b/router/core/router_config.go index c61bd97167..94cc20bc6c 100644 --- a/router/core/router_config.go +++ b/router/core/router_config.go @@ -143,6 +143,7 @@ type Config struct { // Poller configPoller configpoller.ConfigPoller selfRegister selfregister.SelfRegister + promptToQueryClient mcpserver.PromptToQueryClient registrationInfo *nodev1.RegistrationInfo securityConfiguration config.SecurityConfiguration customModules []Module diff --git a/router/core/supervisor_instance.go b/router/core/supervisor_instance.go index 73570b166a..b4e5e7d474 100644 --- a/router/core/supervisor_instance.go +++ b/router/core/supervisor_instance.go @@ -11,6 +11,7 @@ import ( "github.com/dustin/go-humanize" "github.com/wundergraph/cosmo/router/pkg/authentication" "github.com/wundergraph/cosmo/router/pkg/config" + "github.com/wundergraph/cosmo/router/pkg/controlplane/prompttoquery" "github.com/wundergraph/cosmo/router/pkg/controlplane/selfregister" "github.com/wundergraph/cosmo/router/pkg/cors" "github.com/wundergraph/cosmo/router/pkg/logging" @@ -159,6 +160,16 @@ func newRouter(ctx context.Context, params RouterResources, additionalOptions .. options = append(options, WithSelfRegistration(selfRegister)) } + if cfg.MCP.Enabled && cfg.Graph.Token != "" { + promptToQueryClient, err := prompttoquery.New(cfg.ControlplaneURL, cfg.Graph.Token, + prompttoquery.WithLogger(logger), + ) + if err != nil { + return nil, fmt.Errorf("could not create prompt-to-query client: %w", err) + } + options = append(options, WithPromptToQueryClient(promptToQueryClient)) + } + if opt := optionFromExecutionConfig(&cfg.ExecutionConfig, cfg.RouterConfigPath); opt != nil { options = append(options, opt) } else { diff --git a/router/gen/proto/wg/cosmo/node/v1/node.pb.go b/router/gen/proto/wg/cosmo/node/v1/node.pb.go index 3dc0d344e0..896f00a981 100644 --- a/router/gen/proto/wg/cosmo/node/v1/node.pb.go +++ b/router/gen/proto/wg/cosmo/node/v1/node.pb.go @@ -1198,8 +1198,10 @@ type EngineConfiguration struct { TypeConfigurations []*TypeConfiguration `protobuf:"bytes,5,rep,name=type_configurations,json=typeConfigurations,proto3" json:"type_configurations,omitempty"` StringStorage map[string]string `protobuf:"bytes,6,rep,name=string_storage,json=stringStorage,proto3" json:"string_storage,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` GraphqlClientSchema *string `protobuf:"bytes,7,opt,name=graphql_client_schema,json=graphqlClientSchema,proto3,oneof" json:"graphql_client_schema,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Content hash of graphqlSchema, formatted as "sha256:". + SchemaHash string `protobuf:"bytes,8,opt,name=schema_hash,json=schemaHash,proto3" json:"schema_hash,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *EngineConfiguration) Reset() { @@ -1281,6 +1283,13 @@ func (x *EngineConfiguration) GetGraphqlClientSchema() string { return "" } +func (x *EngineConfiguration) GetSchemaHash() string { + if x != nil { + return x.SchemaHash + } + return "" +} + type DataSourceConfiguration struct { state protoimpl.MessageState `protogen:"open.v1"` Kind DataSourceKind `protobuf:"varint,1,opt,name=kind,proto3,enum=wg.cosmo.node.v1.DataSourceKind" json:"kind,omitempty"` @@ -5197,7 +5206,7 @@ const file_wg_cosmo_node_v1_node_proto_rawDesc = "" + "\bdocument\x18\x02 \x01(\tR\bdocument\x12%\n" + "\x0eoperation_name\x18\x03 \x01(\tR\roperationName\x12O\n" + "\x0eoperation_type\x18\x04 \x01(\x0e2(.wg.cosmo.node.v1.SatisfiedOperationTypeR\roperationType\x12)\n" + - "\x10variables_schema\x18\x05 \x01(\tR\x0fvariablesSchema\"\xfc\x04\n" + + "\x10variables_schema\x18\x05 \x01(\tR\x0fvariablesSchema\"\x9d\x05\n" + "\x13EngineConfiguration\x122\n" + "\x14defaultFlushInterval\x18\x01 \x01(\x03R\x14defaultFlushInterval\x12f\n" + "\x19datasource_configurations\x18\x02 \x03(\v2).wg.cosmo.node.v1.DataSourceConfigurationR\x18datasourceConfigurations\x12W\n" + @@ -5205,7 +5214,9 @@ const file_wg_cosmo_node_v1_node_proto_rawDesc = "" + "\rgraphqlSchema\x18\x04 \x01(\tR\rgraphqlSchema\x12T\n" + "\x13type_configurations\x18\x05 \x03(\v2#.wg.cosmo.node.v1.TypeConfigurationR\x12typeConfigurations\x12_\n" + "\x0estring_storage\x18\x06 \x03(\v28.wg.cosmo.node.v1.EngineConfiguration.StringStorageEntryR\rstringStorage\x127\n" + - "\x15graphql_client_schema\x18\a \x01(\tH\x00R\x13graphqlClientSchema\x88\x01\x01\x1a@\n" + + "\x15graphql_client_schema\x18\a \x01(\tH\x00R\x13graphqlClientSchema\x88\x01\x01\x12\x1f\n" + + "\vschema_hash\x18\b \x01(\tR\n" + + "schemaHash\x1a@\n" + "\x12StringStorageEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01B\x18\n" + diff --git a/router/pkg/config/config.go b/router/pkg/config/config.go index 781709a25d..613c6801fc 100644 --- a/router/pkg/config/config.go +++ b/router/pkg/config/config.go @@ -1397,6 +1397,9 @@ type MCPOAuthScopesConfiguration struct { // GetSchema specifies scopes required to call the get_schema built-in tool. // Additive to tools_call scopes. Only relevant when expose_schema is true. GetSchema []string `yaml:"get_schema,omitempty" env:"GET_SCHEMA"` + // GenerateQuery specifies scopes required to call the generate_query built-in tool. + // Additive to tools_call scopes. Only relevant when a control-plane client is configured. + GenerateQuery []string `yaml:"generate_query,omitempty" env:"GENERATE_QUERY"` } type MCPSessionConfig struct { diff --git a/router/pkg/config/config.schema.json b/router/pkg/config/config.schema.json index 8428a4a0e4..aed96dfac1 100644 --- a/router/pkg/config/config.schema.json +++ b/router/pkg/config/config.schema.json @@ -2810,6 +2810,11 @@ "type": "array", "description": "Scopes required to call the get_schema built-in tool. Additive to tools_call scopes. Only relevant when expose_schema is true.", "items": { "type": "string" } + }, + "generate_query": { + "type": "array", + "description": "Scopes required to call the generate_query built-in tool. Additive to tools_call scopes. Only relevant when a graph-token-backed control-plane client is configured.", + "items": { "type": "string" } } } }, diff --git a/router/pkg/config/testdata/config_defaults.json b/router/pkg/config/testdata/config_defaults.json index 080b9f7a30..f53703d266 100644 --- a/router/pkg/config/testdata/config_defaults.json +++ b/router/pkg/config/testdata/config_defaults.json @@ -221,7 +221,8 @@ "ToolsCall": null, "ExecuteGraphQL": null, "GetOperationInfo": null, - "GetSchema": null + "GetSchema": null, + "GenerateQuery": null }, "ScopeChallengeIncludeTokenScopes": false, "MaxScopeCombinations": 2048 diff --git a/router/pkg/config/testdata/config_full.json b/router/pkg/config/testdata/config_full.json index 446dcc3958..7baa6f1cb7 100644 --- a/router/pkg/config/testdata/config_full.json +++ b/router/pkg/config/testdata/config_full.json @@ -290,7 +290,8 @@ "ToolsCall": null, "ExecuteGraphQL": null, "GetOperationInfo": null, - "GetSchema": null + "GetSchema": null, + "GenerateQuery": null }, "ScopeChallengeIncludeTokenScopes": false, "MaxScopeCombinations": 2048 diff --git a/router/pkg/controlplane/prompttoquery/client.go b/router/pkg/controlplane/prompttoquery/client.go new file mode 100644 index 0000000000..23c7e6346c --- /dev/null +++ b/router/pkg/controlplane/prompttoquery/client.go @@ -0,0 +1,100 @@ +package prompttoquery + +import ( + "context" + "fmt" + "net/http" + "time" + + "connectrpc.com/connect" + "github.com/hashicorp/go-retryablehttp" + nodev1 "github.com/wundergraph/cosmo/router/gen/proto/wg/cosmo/node/v1" + "github.com/wundergraph/cosmo/router/gen/proto/wg/cosmo/node/v1/nodev1connect" + "go.uber.org/zap" + brotli "go.withmatt.com/connect-brotli" +) + +const defaultTimeout = 15 * time.Second + +type Option func(*Client) + +// Client generates GraphQL operations through the control plane. +type Client struct { + nodeServiceClient nodev1connect.NodeServiceClient + graphAPIToken string + controlplaneEndpoint string + logger *zap.Logger + clientTimeout time.Duration +} + +func New(endpoint, token string, opts ...Option) (*Client, error) { + if endpoint == "" { + return nil, fmt.Errorf("controlplane endpoint is required for prompt to query") + } + + if token == "" { + return nil, fmt.Errorf("graph api token is required for prompt to query") + } + + c := &Client{ + controlplaneEndpoint: endpoint, + graphAPIToken: token, + clientTimeout: defaultTimeout, + } + + for _, opt := range opts { + opt(c) + } + + if c.logger == nil { + c.logger = zap.NewNop() + } + + retryClient := retryablehttp.NewClient() + retryClient.RetryWaitMax = 15 * time.Second + retryClient.RetryMax = 3 + retryClient.Backoff = retryablehttp.DefaultBackoff + retryClient.Logger = nil + retryClient.RequestLogHook = func(_ retryablehttp.Logger, _ *http.Request, retry int) { + if retry > 0 { + c.logger.Info("Generate query through controlplane", zap.Int("retry", retry)) + } + } + + httpClient := retryClient.StandardClient() + httpClient.Timeout = c.clientTimeout + + c.nodeServiceClient = nodev1connect.NewNodeServiceClient(httpClient, c.controlplaneEndpoint, + brotli.WithCompression(), + connect.WithSendCompression(brotli.Name), + ) + + return c, nil +} + +func (c *Client) GenerateQuery(ctx context.Context, schemaHash, prompt string) (*nodev1.GenerateQueryResponse, error) { + req := connect.NewRequest(&nodev1.GenerateQueryRequest{ + SchemaHash: schemaHash, + Prompt: prompt, + }) + req.Header().Set("Authorization", fmt.Sprintf("Bearer %s", c.graphAPIToken)) + + resp, err := c.nodeServiceClient.GenerateQuery(ctx, req) + if err != nil { + return nil, err + } + + return resp.Msg, nil +} + +func WithLogger(logger *zap.Logger) Option { + return func(c *Client) { + c.logger = logger + } +} + +func WithClientTimeout(timeout time.Duration) Option { + return func(c *Client) { + c.clientTimeout = timeout + } +} diff --git a/router/pkg/controlplane/prompttoquery/client_test.go b/router/pkg/controlplane/prompttoquery/client_test.go new file mode 100644 index 0000000000..a4f43f10ee --- /dev/null +++ b/router/pkg/controlplane/prompttoquery/client_test.go @@ -0,0 +1,56 @@ +package prompttoquery + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" + + "connectrpc.com/connect" + "github.com/stretchr/testify/require" + nodev1 "github.com/wundergraph/cosmo/router/gen/proto/wg/cosmo/node/v1" + "github.com/wundergraph/cosmo/router/gen/proto/wg/cosmo/node/v1/nodev1connect" + brotli "go.withmatt.com/connect-brotli" +) + +type testNodeServiceHandler struct { + generateQueryRequest *nodev1.GenerateQueryRequest + authorizationHeader string +} + +func (h *testNodeServiceHandler) SelfRegister(context.Context, *connect.Request[nodev1.SelfRegisterRequest]) (*connect.Response[nodev1.SelfRegisterResponse], error) { + return connect.NewResponse(&nodev1.SelfRegisterResponse{}), nil +} + +func (h *testNodeServiceHandler) GenerateQuery(_ context.Context, req *connect.Request[nodev1.GenerateQueryRequest]) (*connect.Response[nodev1.GenerateQueryResponse], error) { + h.generateQueryRequest = req.Msg + h.authorizationHeader = req.Header().Get("Authorization") + + return connect.NewResponse(&nodev1.GenerateQueryResponse{ + Response: &nodev1.Response{}, + Query: &nodev1.SatisfiedQuery{ + Document: "query GetEmployees { employees { id } }", + OperationName: "GetEmployees", + }, + }), nil +} + +func TestClientGenerateQuery(t *testing.T) { + handler := &testNodeServiceHandler{} + path, nodeHandler := nodev1connect.NewNodeServiceHandler(handler, brotli.WithCompression()) + mux := http.NewServeMux() + mux.Handle(path, nodeHandler) + server := httptest.NewServer(mux) + t.Cleanup(server.Close) + + client, err := New(server.URL, "graph-token") + require.NoError(t, err) + + resp, err := client.GenerateQuery(t.Context(), "sha256:abcdef", "List all employees") + + require.NoError(t, err) + require.Equal(t, "Bearer graph-token", handler.authorizationHeader) + require.Equal(t, "sha256:abcdef", handler.generateQueryRequest.GetSchemaHash()) + require.Equal(t, "List all employees", handler.generateQueryRequest.GetPrompt()) + require.Equal(t, "GetEmployees", resp.GetQuery().GetOperationName()) +} diff --git a/router/pkg/mcpserver/auth_middleware.go b/router/pkg/mcpserver/auth_middleware.go index 6c78a9bfc2..60cf80e2d2 100644 --- a/router/pkg/mcpserver/auth_middleware.go +++ b/router/pkg/mcpserver/auth_middleware.go @@ -91,6 +91,8 @@ func (m *MCPAuthMiddleware) getBuiltinToolScopes(toolName string) []string { return m.scopes.GetOperationInfo case "get_schema": return m.scopes.GetSchema + case "generate_query": + return m.scopes.GenerateQuery default: return nil } diff --git a/router/pkg/mcpserver/auth_middleware_test.go b/router/pkg/mcpserver/auth_middleware_test.go index b6591418ab..1ae460a4d3 100644 --- a/router/pkg/mcpserver/auth_middleware_test.go +++ b/router/pkg/mcpserver/auth_middleware_test.go @@ -607,6 +607,8 @@ func TestMCPAuthMiddlewareBuiltinToolScopes(t *testing.T) { return authentication.Claims{"sub": "user3", "scope": "mcp:connect mcp:tools:call mcp:graphql:execute"}, nil case "has-ops-read": return authentication.Claims{"sub": "user4", "scope": "mcp:connect mcp:tools:call mcp:ops:read"}, nil + case "has-query-generate": + return authentication.Claims{"sub": "user5", "scope": "mcp:connect mcp:tools:call mcp:query:generate"}, nil default: return nil, errors.New("invalid token") } @@ -619,6 +621,7 @@ func TestMCPAuthMiddlewareBuiltinToolScopes(t *testing.T) { ExecuteGraphQL: []string{"mcp:graphql:execute"}, GetOperationInfo: []string{"mcp:ops:read"}, GetSchema: []string{"mcp:schema:read"}, + GenerateQuery: []string{"mcp:query:generate"}, } tests := []struct { @@ -667,6 +670,19 @@ func TestMCPAuthMiddlewareBuiltinToolScopes(t *testing.T) { body: `{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"get_operation_info"}}`, wantStatusCode: 200, }, + { + name: "returns 403 when generate_query lacks required builtin scope", + token: "base-only", + body: `{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"generate_query"}}`, + wantStatusCode: 403, + wantScope: `scope="mcp:query:generate"`, + }, + { + name: "allows generate_query when token has required builtin scope", + token: "has-query-generate", + body: `{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"generate_query"}}`, + wantStatusCode: 200, + }, { name: "allows non-builtin tool regardless of builtin scopes", token: "base-only", diff --git a/router/pkg/mcpserver/discover_test.go b/router/pkg/mcpserver/discover_test.go index b5b74650f8..68b6f30f4b 100644 --- a/router/pkg/mcpserver/discover_test.go +++ b/router/pkg/mcpserver/discover_test.go @@ -36,7 +36,7 @@ func newTestSession(t *testing.T, opts ...func(*Options)) *mcp.ClientSession { }, opts...)..., ) require.NoError(t, err) - require.NoError(t, srv.Reload(&schemaDoc, nil)) + require.NoError(t, srv.Reload(&schemaDoc, nil, "sha256:test")) serverTransport, clientTransport := mcp.NewInMemoryTransports() diff --git a/router/pkg/mcpserver/server.go b/router/pkg/mcpserver/server.go index 1a118a6858..adfddf317d 100644 --- a/router/pkg/mcpserver/server.go +++ b/router/pkg/mcpserver/server.go @@ -11,6 +11,7 @@ import ( "net/http" "slices" "strings" + "sync" "time" "github.com/hashicorp/go-retryablehttp" @@ -19,6 +20,7 @@ import ( "github.com/santhosh-tekuri/jsonschema/v6" "go.uber.org/zap" + "github.com/wundergraph/cosmo/router/gen/proto/wg/cosmo/common" nodev1 "github.com/wundergraph/cosmo/router/gen/proto/wg/cosmo/node/v1" "github.com/wundergraph/cosmo/router/internal/headers" "github.com/wundergraph/cosmo/router/pkg/authentication" @@ -35,6 +37,7 @@ import ( var reservedToolNames = []string{ "get_schema", "execute_graphql", + "generate_query", "get_operation_info", } @@ -110,6 +113,14 @@ type Options struct { // ServerDescription is a human-readable description reported in the MCP // serverInfo. ServerDescription string + // PromptToQueryClient generates GraphQL operations through the Cosmo control plane. + // When nil, the generate_query tool is not exposed. + PromptToQueryClient PromptToQueryClient +} + +// PromptToQueryClient generates a GraphQL operation for an indexed schema. +type PromptToQueryClient interface { + GenerateQuery(ctx context.Context, schemaHash, prompt string) (*nodev1.GenerateQueryResponse, error) } // GraphQLSchemaServer represents an MCP server that works with GraphQL schemas and operations @@ -137,6 +148,9 @@ type GraphQLSchemaServer struct { serverBaseURL string resourceDocumentation string authMiddleware *MCPAuthMiddleware + promptToQueryClient PromptToQueryClient + schemaHashMu sync.RWMutex + schemaHash string } type graphqlRequest struct { @@ -150,6 +164,20 @@ type ExecuteGraphQLInput struct { Variables json.RawMessage `json:"variables,omitempty"` } +// GenerateQueryInput defines the input structure for the generate_query tool. +type GenerateQueryInput struct { + Prompt string `json:"prompt"` +} + +// GeneratedQueryResponse is returned by the generate_query tool. +type GeneratedQueryResponse struct { + Description string `json:"description,omitempty"` + Document string `json:"document"` + OperationName string `json:"operationName"` + OperationType string `json:"operationType"` + VariablesSchema string `json:"variablesSchema,omitempty"` +} + // operationHandler holds an operation and its compiled JSON schema type operationHandler struct { operation schemaloader.Operation @@ -344,6 +372,7 @@ func NewGraphQLSchemaServer(ctx context.Context, routerGraphQLEndpoint string, o serverBaseURL: options.ServerBaseURL, resourceDocumentation: options.ResourceDocumentation, authMiddleware: authMiddleware, + promptToQueryClient: options.PromptToQueryClient, } return gs, nil @@ -383,6 +412,13 @@ func WithServerDescription(description string) func(*Options) { } } +// WithPromptToQueryClient enables the generate_query tool using the provided control-plane client. +func WithPromptToQueryClient(client PromptToQueryClient) func(*Options) { + return func(o *Options) { + o.PromptToQueryClient = client + } +} + // WithGraphName sets the graph name. An empty name keeps the default. func WithGraphName(graphName string) func(*Options) { return func(o *Options) { @@ -568,13 +604,14 @@ func (s *GraphQLSchemaServer) Start() error { // Reload reloads the operations and schema, and computes per-tool scope // requirements from @requiresScopes directives in the field configurations. -func (s *GraphQLSchemaServer) Reload(schema *ast.Document, fieldConfigs []*nodev1.FieldConfiguration) error { +func (s *GraphQLSchemaServer) Reload(schema *ast.Document, fieldConfigs []*nodev1.FieldConfiguration, schemaHash string) error { if s.server == nil { return fmt.Errorf("server is not started") } s.schemaCompiler = NewSchemaCompiler(s.logger) s.operationsManager = NewOperationsManager(schema, s.logger, s.excludeMutations) + s.setSchemaHash(schemaHash) if s.operationsDir != "" { if err := s.operationsManager.LoadOperationsFromDirectory(s.operationsDir); err != nil { @@ -690,6 +727,36 @@ func (s *GraphQLSchemaServer) registerTools() error { s.registeredTools = append(s.registeredTools, "execute_graphql") } + if s.promptToQueryClient != nil { + openWorldHint := true + generateQueryTool := &mcp.Tool{ + Name: "generate_query", + Description: "Generates a GraphQL operation for this API from a natural-language prompt.", + InputSchema: map[string]any{ + "type": "object", + "description": "A natural-language description of the GraphQL operation to generate.", + "properties": map[string]any{ + "prompt": map[string]any{ + "type": "string", + "minLength": 1, + "description": "A natural-language description of the GraphQL operation to generate.", + }, + }, + "additionalProperties": false, + "required": []string{"prompt"}, + }, + Annotations: &mcp.ToolAnnotations{ + Title: "Generate GraphQL Operation", + ReadOnlyHint: true, + IdempotentHint: true, + OpenWorldHint: &openWorldHint, + }, + } + + s.server.AddTool(generateQueryTool, s.handleGenerateQuery()) + s.registeredTools = append(s.registeredTools, "generate_query") + } + // Get operations filtered by the excludeMutations setting operations := s.operationsManager.GetFilteredOperations() @@ -819,6 +886,81 @@ func (s *GraphQLSchemaServer) registerTools() error { return nil } +func (s *GraphQLSchemaServer) setSchemaHash(schemaHash string) { + s.schemaHashMu.Lock() + defer s.schemaHashMu.Unlock() + s.schemaHash = schemaHash +} + +func (s *GraphQLSchemaServer) getSchemaHash() string { + s.schemaHashMu.RLock() + defer s.schemaHashMu.RUnlock() + return s.schemaHash +} + +func toolError(message string) *mcp.CallToolResult { + return &mcp.CallToolResult{ + Content: []mcp.Content{&mcp.TextContent{Text: message}}, + IsError: true, + } +} + +func (s *GraphQLSchemaServer) handleGenerateQuery() func(ctx context.Context, request *mcp.CallToolRequest) (*mcp.CallToolResult, error) { + return func(ctx context.Context, request *mcp.CallToolRequest) (*mcp.CallToolResult, error) { + var input GenerateQueryInput + if err := json.Unmarshal(request.Params.Arguments, &input); err != nil { + return toolError(fmt.Sprintf("Invalid input: %v", err)), nil + } + + input.Prompt = strings.TrimSpace(input.Prompt) + if input.Prompt == "" { + return toolError("Input validation failed: prompt is required"), nil + } + + schemaHash := s.getSchemaHash() + if schemaHash == "" { + return toolError("The active GraphQL schema is not available"), nil + } + + response, err := s.promptToQueryClient.GenerateQuery(ctx, schemaHash, input.Prompt) + if err != nil { + s.logger.Error("failed to generate query through the control plane", zap.Error(err)) + return toolError("Failed to generate a GraphQL operation through the control plane"), nil + } + if response == nil || response.GetResponse() == nil { + return toolError("The control plane returned an invalid generate-query response"), nil + } + if response.GetResponse().GetCode() != common.EnumStatusCode_OK { + details := response.GetResponse().GetDetails() + if details == "" { + details = fmt.Sprintf("control plane returned status %s", response.GetResponse().GetCode()) + } + return toolError(details), nil + } + + query := response.GetQuery() + if query == nil { + return toolError("The control plane did not return a generated GraphQL operation"), nil + } + + result := GeneratedQueryResponse{ + Description: query.GetDescription(), + Document: query.GetDocument(), + OperationName: query.GetOperationName(), + OperationType: strings.ToLower(query.GetOperationType().String()), + VariablesSchema: query.GetVariablesSchema(), + } + resultJSON, err := json.Marshal(result) + if err != nil { + return nil, fmt.Errorf("failed to marshal generated query response: %w", err) + } + + return &mcp.CallToolResult{ + Content: []mcp.Content{&mcp.TextContent{Text: string(resultJSON)}}, + }, nil + } +} + // handleOperation handles a specific operation func (s *GraphQLSchemaServer) handleOperation(handler *operationHandler) func(ctx context.Context, request *mcp.CallToolRequest) (*mcp.CallToolResult, error) { return func(ctx context.Context, request *mcp.CallToolRequest) (*mcp.CallToolResult, error) { @@ -1124,6 +1266,9 @@ func (s *GraphQLSchemaServer) handleProtectedResourceMetadata(w http.ResponseWri if s.exposeSchema { scopeLists = append(scopeLists, s.oauthConfig.Scopes.GetSchema) } + if s.promptToQueryClient != nil { + scopeLists = append(scopeLists, s.oauthConfig.Scopes.GenerateQuery) + } for _, scopeList := range scopeLists { for _, scope := range scopeList { diff --git a/router/pkg/mcpserver/server_test.go b/router/pkg/mcpserver/server_test.go index 3a3c044609..5191f4b404 100644 --- a/router/pkg/mcpserver/server_test.go +++ b/router/pkg/mcpserver/server_test.go @@ -1,12 +1,21 @@ package mcpserver import ( + "context" + "encoding/json" + "errors" + "net/http" + "net/http/httptest" "os" "path/filepath" "testing" + "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/wundergraph/cosmo/router/gen/proto/wg/cosmo/common" + nodev1 "github.com/wundergraph/cosmo/router/gen/proto/wg/cosmo/node/v1" + "github.com/wundergraph/cosmo/router/pkg/config" "github.com/wundergraph/graphql-go-tools/v2/pkg/astparser" "github.com/wundergraph/graphql-go-tools/v2/pkg/asttransform" "go.uber.org/zap" @@ -14,6 +23,23 @@ import ( "go.uber.org/zap/zaptest/observer" ) +type fakePromptToQueryClient struct { + response *nodev1.GenerateQueryResponse + err error + schemaHash string + prompt string +} + +func stringPointer(value string) *string { + return &value +} + +func (f *fakePromptToQueryClient) GenerateQuery(_ context.Context, schemaHash, prompt string) (*nodev1.GenerateQueryResponse, error) { + f.schemaHash = schemaHash + f.prompt = prompt + return f.response, f.err +} + const testSchema = ` schema { query: Query @@ -90,14 +116,14 @@ func TestReload_NoToolDuplication(t *testing.T) { require.NoError(t, err) // First load - err = srv.Reload(&schemaDoc, nil) + err = srv.Reload(&schemaDoc, nil, "sha256:test") require.NoError(t, err) firstLoadTools := make([]string, len(srv.registeredTools)) copy(firstLoadTools, srv.registeredTools) // Second load (simulates config reload) - err = srv.Reload(&schemaDoc, nil) + err = srv.Reload(&schemaDoc, nil, "sha256:test") require.NoError(t, err) // registeredTools should be identical after reload — no duplicates @@ -136,7 +162,7 @@ func TestReload_ReservedToolNameCollision(t *testing.T) { ) require.NoError(t, err) - err = srv.Reload(&schemaDoc, nil) + err = srv.Reload(&schemaDoc, nil, "sha256:test") require.NoError(t, err) // The operation "GetOperationInfo" (snake: "get_operation_info") should be skipped @@ -180,7 +206,7 @@ func TestReload_PrefixModeAvoidsReservedNameCollision(t *testing.T) { ) require.NoError(t, err) - err = srv.Reload(&schemaDoc, nil) + err = srv.Reload(&schemaDoc, nil, "sha256:test") require.NoError(t, err) // No collisions because the prefix disambiguates from the reserved name @@ -195,3 +221,143 @@ func TestReload_PrefixModeAvoidsReservedNameCollision(t *testing.T) { "get_operation_info", }, srv.registeredTools) } + +func TestGenerateQueryTool(t *testing.T) { + schemaDoc, report := astparser.ParseGraphqlDocumentString(testSchema) + require.False(t, report.HasErrors()) + require.NoError(t, asttransform.MergeDefinitionWithBaseSchema(&schemaDoc)) + + client := &fakePromptToQueryClient{ + response: &nodev1.GenerateQueryResponse{ + Response: &nodev1.Response{Code: common.EnumStatusCode_OK}, + Query: &nodev1.SatisfiedQuery{ + Description: "Lists employees", + Document: "query ListEmployees { employees { id name } }", + OperationName: "ListEmployees", + OperationType: nodev1.SatisfiedOperationType_QUERY, + VariablesSchema: `{"type":"object"}`, + }, + }, + } + srv, err := NewGraphQLSchemaServer( + t.Context(), + "http://localhost:4000/graphql", + WithPromptToQueryClient(client), + WithOperationsDir(""), + ) + require.NoError(t, err) + require.NoError(t, srv.Reload(&schemaDoc, nil, "sha256:first")) + require.Contains(t, srv.registeredTools, "generate_query") + + result, err := srv.handleGenerateQuery()(t.Context(), &mcp.CallToolRequest{ + Params: &mcp.CallToolParamsRaw{Arguments: json.RawMessage(`{"prompt":" List all employees "}`)}, + }) + + require.NoError(t, err) + require.False(t, result.IsError) + require.Equal(t, "sha256:first", client.schemaHash) + require.Equal(t, "List all employees", client.prompt) + require.Len(t, result.Content, 1) + textContent, ok := result.Content[0].(*mcp.TextContent) + require.True(t, ok) + require.JSONEq(t, `{ + "description":"Lists employees", + "document":"query ListEmployees { employees { id name } }", + "operationName":"ListEmployees", + "operationType":"query", + "variablesSchema":"{\"type\":\"object\"}" + }`, textContent.Text) + + require.NoError(t, srv.Reload(&schemaDoc, nil, "sha256:second")) + _, err = srv.handleGenerateQuery()(t.Context(), &mcp.CallToolRequest{ + Params: &mcp.CallToolParamsRaw{Arguments: json.RawMessage(`{"prompt":"List employees again"}`)}, + }) + require.NoError(t, err) + require.Equal(t, "sha256:second", client.schemaHash) +} + +func TestGenerateQueryToolErrors(t *testing.T) { + tests := []struct { + name string + client *fakePromptToQueryClient + schemaHash string + arguments string + wantText string + }{ + { + name: "blank prompt", + client: &fakePromptToQueryClient{}, + schemaHash: "sha256:test", + arguments: `{"prompt":" "}`, + wantText: "prompt is required", + }, + { + name: "schema unavailable", + client: &fakePromptToQueryClient{}, + arguments: `{"prompt":"List employees"}`, + wantText: "schema is not available", + }, + { + name: "control plane rejection", + client: &fakePromptToQueryClient{response: &nodev1.GenerateQueryResponse{ + Response: &nodev1.Response{Code: common.EnumStatusCode_ERR_UPGRADE_PLAN, Details: stringPointer("Prompt to Query not available with your current plan")}, + }}, + schemaHash: "sha256:test", + arguments: `{"prompt":"List employees"}`, + wantText: "Prompt to Query not available with your current plan", + }, + { + name: "transport failure", + client: &fakePromptToQueryClient{err: errors.New("control plane unavailable")}, + schemaHash: "sha256:test", + arguments: `{"prompt":"List employees"}`, + wantText: "Failed to generate a GraphQL operation", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + srv, err := NewGraphQLSchemaServer( + t.Context(), + "http://localhost:4000/graphql", + WithPromptToQueryClient(tt.client), + ) + require.NoError(t, err) + srv.setSchemaHash(tt.schemaHash) + + result, err := srv.handleGenerateQuery()(t.Context(), &mcp.CallToolRequest{ + Params: &mcp.CallToolParamsRaw{Arguments: json.RawMessage(tt.arguments)}, + }) + + require.NoError(t, err) + require.True(t, result.IsError) + textContent, ok := result.Content[0].(*mcp.TextContent) + require.True(t, ok) + require.Contains(t, textContent.Text, tt.wantText) + }) + } +} + +func TestGenerateQueryScopeIncludedInProtectedResourceMetadata(t *testing.T) { + srv := &GraphQLSchemaServer{ + oauthConfig: &config.MCPOAuthConfiguration{ + AuthorizationServerURL: "https://auth.example.com", + Scopes: config.MCPOAuthScopesConfiguration{ + ToolsCall: []string{"mcp:tools:call"}, + GenerateQuery: []string{"mcp:query:generate"}, + }, + }, + promptToQueryClient: &fakePromptToQueryClient{}, + serverBaseURL: "https://mcp.example.com", + logger: zap.NewNop(), + } + + recorder := httptest.NewRecorder() + request := httptest.NewRequest(http.MethodGet, "/.well-known/oauth-protected-resource/mcp", nil) + srv.handleProtectedResourceMetadata(recorder, request) + + require.Equal(t, http.StatusOK, recorder.Code) + var metadata ProtectedResourceMetadata + require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &metadata)) + require.Equal(t, []string{"mcp:query:generate", "mcp:tools:call"}, metadata.ScopesSupported) +} From 7fe0fa175326494cd21210da881d31cd56b1df78 Mon Sep 17 00:00:00 2001 From: Alberto Garcia Hierro Date: Wed, 12 Aug 2026 17:47:02 +0100 Subject: [PATCH 02/14] feat: resolve prompt-to-query indexes by schema version --- .../gen/proto/wg/cosmo/node/v1/node.pb.go | 40 +++---- connect/src/wg/cosmo/node/v1/node_pb.ts | 13 +-- .../src/core/bufservices/NodeService.ts | 36 ++++++- controlplane/src/core/composition/composer.ts | 7 +- .../src/core/services/PromptToQueryService.ts | 37 ++++--- .../test/prompt-to-query-service.test.ts | 72 +++++++++++++ controlplane/test/prompt-to-query.test.ts | 101 ++++++++++++++++++ controlplane/test/router-config.test.ts | 6 -- controlplane/test/test-util.ts | 3 + mcp-yoko-run.sh | 4 +- proto/wg/cosmo/node/v1/node.proto | 4 +- router/core/graph_server.go | 2 +- router/gen/proto/wg/cosmo/node/v1/node.pb.go | 40 +++---- .../pkg/controlplane/prompttoquery/client.go | 6 +- .../controlplane/prompttoquery/client_test.go | 4 +- router/pkg/mcpserver/discover_test.go | 2 +- router/pkg/mcpserver/server.go | 36 +++---- router/pkg/mcpserver/server_test.go | 68 ++++++------ 18 files changed, 330 insertions(+), 151 deletions(-) create mode 100644 controlplane/test/prompt-to-query-service.test.ts create mode 100644 controlplane/test/prompt-to-query.test.ts diff --git a/connect-go/gen/proto/wg/cosmo/node/v1/node.pb.go b/connect-go/gen/proto/wg/cosmo/node/v1/node.pb.go index 07b4bab53a..a9a9047088 100644 --- a/connect-go/gen/proto/wg/cosmo/node/v1/node.pb.go +++ b/connect-go/gen/proto/wg/cosmo/node/v1/node.pb.go @@ -1010,11 +1010,11 @@ func (x *SelfRegisterResponse) GetRegistrationInfo() *RegistrationInfo { } type GenerateQueryRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - SchemaHash string `protobuf:"bytes,1,opt,name=schema_hash,json=schemaHash,proto3" json:"schema_hash,omitempty"` - Prompt string `protobuf:"bytes,2,opt,name=prompt,proto3" json:"prompt,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + SchemaVersionId string `protobuf:"bytes,1,opt,name=schema_version_id,json=schemaVersionId,proto3" json:"schema_version_id,omitempty"` + Prompt string `protobuf:"bytes,2,opt,name=prompt,proto3" json:"prompt,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *GenerateQueryRequest) Reset() { @@ -1047,9 +1047,9 @@ func (*GenerateQueryRequest) Descriptor() ([]byte, []int) { return file_wg_cosmo_node_v1_node_proto_rawDescGZIP(), []int{10} } -func (x *GenerateQueryRequest) GetSchemaHash() string { +func (x *GenerateQueryRequest) GetSchemaVersionId() string { if x != nil { - return x.SchemaHash + return x.SchemaVersionId } return "" } @@ -1198,10 +1198,8 @@ type EngineConfiguration struct { TypeConfigurations []*TypeConfiguration `protobuf:"bytes,5,rep,name=type_configurations,json=typeConfigurations,proto3" json:"type_configurations,omitempty"` StringStorage map[string]string `protobuf:"bytes,6,rep,name=string_storage,json=stringStorage,proto3" json:"string_storage,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` GraphqlClientSchema *string `protobuf:"bytes,7,opt,name=graphql_client_schema,json=graphqlClientSchema,proto3,oneof" json:"graphql_client_schema,omitempty"` - // Content hash of graphqlSchema, formatted as "sha256:". - SchemaHash string `protobuf:"bytes,8,opt,name=schema_hash,json=schemaHash,proto3" json:"schema_hash,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *EngineConfiguration) Reset() { @@ -1283,13 +1281,6 @@ func (x *EngineConfiguration) GetGraphqlClientSchema() string { return "" } -func (x *EngineConfiguration) GetSchemaHash() string { - if x != nil { - return x.SchemaHash - } - return "" -} - type DataSourceConfiguration struct { state protoimpl.MessageState `protogen:"open.v1"` Kind DataSourceKind `protobuf:"varint,1,opt,name=kind,proto3,enum=wg.cosmo.node.v1.DataSourceKind" json:"kind,omitempty"` @@ -5192,10 +5183,9 @@ const file_wg_cosmo_node_v1_node_proto_rawDesc = "" + "\x14SelfRegisterResponse\x126\n" + "\bresponse\x18\x01 \x01(\v2\x1a.wg.cosmo.node.v1.ResponseR\bresponse\x12S\n" + "\x10registrationInfo\x18\x02 \x01(\v2\".wg.cosmo.node.v1.RegistrationInfoH\x00R\x10registrationInfo\x88\x01\x01B\x13\n" + - "\x11_registrationInfo\"O\n" + - "\x14GenerateQueryRequest\x12\x1f\n" + - "\vschema_hash\x18\x01 \x01(\tR\n" + - "schemaHash\x12\x16\n" + + "\x11_registrationInfo\"Z\n" + + "\x14GenerateQueryRequest\x12*\n" + + "\x11schema_version_id\x18\x01 \x01(\tR\x0fschemaVersionId\x12\x16\n" + "\x06prompt\x18\x02 \x01(\tR\x06prompt\"\x96\x01\n" + "\x15GenerateQueryResponse\x126\n" + "\bresponse\x18\x01 \x01(\v2\x1a.wg.cosmo.node.v1.ResponseR\bresponse\x12;\n" + @@ -5206,7 +5196,7 @@ const file_wg_cosmo_node_v1_node_proto_rawDesc = "" + "\bdocument\x18\x02 \x01(\tR\bdocument\x12%\n" + "\x0eoperation_name\x18\x03 \x01(\tR\roperationName\x12O\n" + "\x0eoperation_type\x18\x04 \x01(\x0e2(.wg.cosmo.node.v1.SatisfiedOperationTypeR\roperationType\x12)\n" + - "\x10variables_schema\x18\x05 \x01(\tR\x0fvariablesSchema\"\x9d\x05\n" + + "\x10variables_schema\x18\x05 \x01(\tR\x0fvariablesSchema\"\xfc\x04\n" + "\x13EngineConfiguration\x122\n" + "\x14defaultFlushInterval\x18\x01 \x01(\x03R\x14defaultFlushInterval\x12f\n" + "\x19datasource_configurations\x18\x02 \x03(\v2).wg.cosmo.node.v1.DataSourceConfigurationR\x18datasourceConfigurations\x12W\n" + @@ -5214,9 +5204,7 @@ const file_wg_cosmo_node_v1_node_proto_rawDesc = "" + "\rgraphqlSchema\x18\x04 \x01(\tR\rgraphqlSchema\x12T\n" + "\x13type_configurations\x18\x05 \x03(\v2#.wg.cosmo.node.v1.TypeConfigurationR\x12typeConfigurations\x12_\n" + "\x0estring_storage\x18\x06 \x03(\v28.wg.cosmo.node.v1.EngineConfiguration.StringStorageEntryR\rstringStorage\x127\n" + - "\x15graphql_client_schema\x18\a \x01(\tH\x00R\x13graphqlClientSchema\x88\x01\x01\x12\x1f\n" + - "\vschema_hash\x18\b \x01(\tR\n" + - "schemaHash\x1a@\n" + + "\x15graphql_client_schema\x18\a \x01(\tH\x00R\x13graphqlClientSchema\x88\x01\x01\x1a@\n" + "\x12StringStorageEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01B\x18\n" + diff --git a/connect/src/wg/cosmo/node/v1/node_pb.ts b/connect/src/wg/cosmo/node/v1/node_pb.ts index 5b7aae86f8..5bf5e261f1 100644 --- a/connect/src/wg/cosmo/node/v1/node_pb.ts +++ b/connect/src/wg/cosmo/node/v1/node_pb.ts @@ -12,7 +12,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file wg/cosmo/node/v1/node.proto. */ export const file_wg_cosmo_node_v1_node: GenFile = /*@__PURE__*/ - fileDesc("Cht3Zy9jb3Ntby9ub2RlL3YxL25vZGUucHJvdG8SEHdnLmNvc21vLm5vZGUudjEiOQoIU3ViZ3JhcGgSCgoCaWQYASABKAkSDAoEbmFtZRgCIAEoCRITCgtyb3V0aW5nX3VybBgDIAEoCSKOAgohRmVhdHVyZUZsYWdSb3V0ZXJFeGVjdXRpb25Db25maWdzEnUKG2NvbmZpZ19ieV9mZWF0dXJlX2ZsYWdfbmFtZRgBIAMoCzJQLndnLmNvc21vLm5vZGUudjEuRmVhdHVyZUZsYWdSb3V0ZXJFeGVjdXRpb25Db25maWdzLkNvbmZpZ0J5RmVhdHVyZUZsYWdOYW1lRW50cnkacgocQ29uZmlnQnlGZWF0dXJlRmxhZ05hbWVFbnRyeRILCgNrZXkYASABKAkSQQoFdmFsdWUYAiABKAsyMi53Zy5jb3Ntby5ub2RlLnYxLkZlYXR1cmVGbGFnUm91dGVyRXhlY3V0aW9uQ29uZmlnOgI4ASKgAQogRmVhdHVyZUZsYWdSb3V0ZXJFeGVjdXRpb25Db25maWcSPAoNZW5naW5lX2NvbmZpZxgBIAEoCzIlLndnLmNvc21vLm5vZGUudjEuRW5naW5lQ29uZmlndXJhdGlvbhIPCgd2ZXJzaW9uGAIgASgJEi0KCXN1YmdyYXBocxgDIAMoCzIaLndnLmNvc21vLm5vZGUudjEuU3ViZ3JhcGginAIKDFJvdXRlckNvbmZpZxI8Cg1lbmdpbmVfY29uZmlnGAEgASgLMiUud2cuY29zbW8ubm9kZS52MS5FbmdpbmVDb25maWd1cmF0aW9uEg8KB3ZlcnNpb24YAiABKAkSLQoJc3ViZ3JhcGhzGAMgAygLMhoud2cuY29zbW8ubm9kZS52MS5TdWJncmFwaBJWChRmZWF0dXJlX2ZsYWdfY29uZmlncxgEIAEoCzIzLndnLmNvc21vLm5vZGUudjEuRmVhdHVyZUZsYWdSb3V0ZXJFeGVjdXRpb25Db25maWdzSACIAQESHQoVY29tcGF0aWJpbGl0eV92ZXJzaW9uGAUgASgJQhcKFV9mZWF0dXJlX2ZsYWdfY29uZmlncyJbCghSZXNwb25zZRItCgRjb2RlGAEgASgOMh8ud2cuY29zbW8uY29tbW9uLkVudW1TdGF0dXNDb2RlEhQKB2RldGFpbHMYAiABKAlIAIgBAUIKCghfZGV0YWlscyIvCg5SZXNwb25zZVN0YXR1cxIMCgRjb2RlGAEgASgFEg8KB21lc3NhZ2UYAiABKAkiZQoQUmVnaXN0cmF0aW9uSW5mbxI3Cg5hY2NvdW50X2xpbWl0cxgBIAEoCzIfLndnLmNvc21vLm5vZGUudjEuQWNjb3VudExpbWl0cxIYChBncmFwaF9wdWJsaWNfa2V5GAIgASgJIiwKDUFjY291bnRMaW1pdHMSGwoTdHJhY2Vfc2FtcGxpbmdfcmF0ZRgBIAEoAiIVChNTZWxmUmVnaXN0ZXJSZXF1ZXN0IpwBChRTZWxmUmVnaXN0ZXJSZXNwb25zZRIsCghyZXNwb25zZRgBIAEoCzIaLndnLmNvc21vLm5vZGUudjEuUmVzcG9uc2USQQoQcmVnaXN0cmF0aW9uSW5mbxgCIAEoCzIiLndnLmNvc21vLm5vZGUudjEuUmVnaXN0cmF0aW9uSW5mb0gAiAEBQhMKEV9yZWdpc3RyYXRpb25JbmZvIjsKFEdlbmVyYXRlUXVlcnlSZXF1ZXN0EhMKC3NjaGVtYV9oYXNoGAEgASgJEg4KBnByb21wdBgCIAEoCSKFAQoVR2VuZXJhdGVRdWVyeVJlc3BvbnNlEiwKCHJlc3BvbnNlGAEgASgLMhoud2cuY29zbW8ubm9kZS52MS5SZXNwb25zZRI0CgVxdWVyeRgCIAEoCzIgLndnLmNvc21vLm5vZGUudjEuU2F0aXNmaWVkUXVlcnlIAIgBAUIICgZfcXVlcnkiqwEKDlNhdGlzZmllZFF1ZXJ5EhMKC2Rlc2NyaXB0aW9uGAEgASgJEhAKCGRvY3VtZW50GAIgASgJEhYKDm9wZXJhdGlvbl9uYW1lGAMgASgJEkAKDm9wZXJhdGlvbl90eXBlGAQgASgOMigud2cuY29zbW8ubm9kZS52MS5TYXRpc2ZpZWRPcGVyYXRpb25UeXBlEhgKEHZhcmlhYmxlc19zY2hlbWEYBSABKAki+QMKE0VuZ2luZUNvbmZpZ3VyYXRpb24SHAoUZGVmYXVsdEZsdXNoSW50ZXJ2YWwYASABKAMSTAoZZGF0YXNvdXJjZV9jb25maWd1cmF0aW9ucxgCIAMoCzIpLndnLmNvc21vLm5vZGUudjEuRGF0YVNvdXJjZUNvbmZpZ3VyYXRpb24SQgoUZmllbGRfY29uZmlndXJhdGlvbnMYAyADKAsyJC53Zy5jb3Ntby5ub2RlLnYxLkZpZWxkQ29uZmlndXJhdGlvbhIVCg1ncmFwaHFsU2NoZW1hGAQgASgJEkAKE3R5cGVfY29uZmlndXJhdGlvbnMYBSADKAsyIy53Zy5jb3Ntby5ub2RlLnYxLlR5cGVDb25maWd1cmF0aW9uElAKDnN0cmluZ19zdG9yYWdlGAYgAygLMjgud2cuY29zbW8ubm9kZS52MS5FbmdpbmVDb25maWd1cmF0aW9uLlN0cmluZ1N0b3JhZ2VFbnRyeRIiChVncmFwaHFsX2NsaWVudF9zY2hlbWEYByABKAlIAIgBARITCgtzY2hlbWFfaGFzaBgIIAEoCRo0ChJTdHJpbmdTdG9yYWdlRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4AUIYChZfZ3JhcGhxbF9jbGllbnRfc2NoZW1hIsYHChdEYXRhU291cmNlQ29uZmlndXJhdGlvbhIuCgRraW5kGAEgASgOMiAud2cuY29zbW8ubm9kZS52MS5EYXRhU291cmNlS2luZBIvCgpyb290X25vZGVzGAIgAygLMhsud2cuY29zbW8ubm9kZS52MS5UeXBlRmllbGQSMAoLY2hpbGRfbm9kZXMYAyADKAsyGy53Zy5jb3Ntby5ub2RlLnYxLlR5cGVGaWVsZBImCh5vdmVycmlkZV9maWVsZF9wYXRoX2Zyb21fYWxpYXMYBCABKAgSQgoOY3VzdG9tX2dyYXBocWwYBSABKAsyKi53Zy5jb3Ntby5ub2RlLnYxLkRhdGFTb3VyY2VDdXN0b21fR3JhcGhRTBJACg1jdXN0b21fc3RhdGljGAYgASgLMikud2cuY29zbW8ubm9kZS52MS5EYXRhU291cmNlQ3VzdG9tX1N0YXRpYxI8CgpkaXJlY3RpdmVzGAcgAygLMigud2cuY29zbW8ubm9kZS52MS5EaXJlY3RpdmVDb25maWd1cmF0aW9uEh8KF3JlcXVlc3RfdGltZW91dF9zZWNvbmRzGAggASgDEgoKAmlkGAkgASgJEi0KBGtleXMYCiADKAsyHy53Zy5jb3Ntby5ub2RlLnYxLlJlcXVpcmVkRmllbGQSMQoIcHJvdmlkZXMYCyADKAsyHy53Zy5jb3Ntby5ub2RlLnYxLlJlcXVpcmVkRmllbGQSMQoIcmVxdWlyZXMYDCADKAsyHy53Zy5jb3Ntby5ub2RlLnYxLlJlcXVpcmVkRmllbGQSPwoNY3VzdG9tX2V2ZW50cxgNIAEoCzIoLndnLmNvc21vLm5vZGUudjEuRGF0YVNvdXJjZUN1c3RvbUV2ZW50cxJJChFlbnRpdHlfaW50ZXJmYWNlcxgOIAMoCzIuLndnLmNvc21vLm5vZGUudjEuRW50aXR5SW50ZXJmYWNlQ29uZmlndXJhdGlvbhJJChFpbnRlcmZhY2Vfb2JqZWN0cxgPIAMoCzIuLndnLmNvc21vLm5vZGUudjEuRW50aXR5SW50ZXJmYWNlQ29uZmlndXJhdGlvbhI/ChJjb3N0X2NvbmZpZ3VyYXRpb24YECABKAsyIy53Zy5jb3Ntby5ub2RlLnYxLkNvc3RDb25maWd1cmF0aW9uElIKHGVudGl0eV9jYWNoaW5nX2NvbmZpZ3VyYXRpb24YESABKAsyLC53Zy5jb3Ntby5ub2RlLnYxLkVudGl0eUNhY2hpbmdDb25maWd1cmF0aW9uIpsCChpFbnRpdHlDYWNoaW5nQ29uZmlndXJhdGlvbhJPChtlbnRpdHlfY2FjaGVfY29uZmlndXJhdGlvbnMYASADKAsyKi53Zy5jb3Ntby5ub2RlLnYxLkVudGl0eUNhY2hlQ29uZmlndXJhdGlvbhJXCh9jYWNoZV9pbnZhbGlkYXRlX2NvbmZpZ3VyYXRpb25zGAIgAygLMi4ud2cuY29zbW8ubm9kZS52MS5DYWNoZUludmFsaWRhdGVDb25maWd1cmF0aW9uElMKHWNhY2hlX3BvcHVsYXRlX2NvbmZpZ3VyYXRpb25zGAMgAygLMiwud2cuY29zbW8ubm9kZS52MS5DYWNoZVBvcHVsYXRlQ29uZmlndXJhdGlvbiK1AQoYRW50aXR5Q2FjaGVDb25maWd1cmF0aW9uEhEKCXR5cGVfbmFtZRgBIAEoCRIXCg9tYXhfYWdlX3NlY29uZHMYAiABKAMSFwoPaW5jbHVkZV9oZWFkZXJzGAMgASgIEhoKEnBhcnRpYWxfY2FjaGVfbG9hZBgEIAEoCBITCgtzaGFkb3dfbW9kZRgFIAEoCBIjChtub3RfZm91bmRfY2FjaGVfdHRsX3NlY29uZHMYBiABKAMiZAocQ2FjaGVJbnZhbGlkYXRlQ29uZmlndXJhdGlvbhISCgpmaWVsZF9uYW1lGAEgASgJEhYKDm9wZXJhdGlvbl90eXBlGAIgASgJEhgKEGVudGl0eV90eXBlX25hbWUYAyABKAkiewoaQ2FjaGVQb3B1bGF0ZUNvbmZpZ3VyYXRpb24SEgoKZmllbGRfbmFtZRgBIAEoCRIWCg5vcGVyYXRpb25fdHlwZRgCIAEoCRIXCg9tYXhfYWdlX3NlY29uZHMYAyABKAMSGAoQZW50aXR5X3R5cGVfbmFtZRgEIAEoCSLAAwoRQ29zdENvbmZpZ3VyYXRpb24SQQoNZmllbGRfd2VpZ2h0cxgBIAMoCzIqLndnLmNvc21vLm5vZGUudjEuRmllbGRXZWlnaHRDb25maWd1cmF0aW9uEkAKCmxpc3Rfc2l6ZXMYAiADKAsyLC53Zy5jb3Ntby5ub2RlLnYxLkZpZWxkTGlzdFNpemVDb25maWd1cmF0aW9uEkoKDHR5cGVfd2VpZ2h0cxgDIAMoCzI0LndnLmNvc21vLm5vZGUudjEuQ29zdENvbmZpZ3VyYXRpb24uVHlwZVdlaWdodHNFbnRyeRJlChpkaXJlY3RpdmVfYXJndW1lbnRfd2VpZ2h0cxgEIAMoCzJBLndnLmNvc21vLm5vZGUudjEuQ29zdENvbmZpZ3VyYXRpb24uRGlyZWN0aXZlQXJndW1lbnRXZWlnaHRzRW50cnkaMgoQVHlwZVdlaWdodHNFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAU6AjgBGj8KHURpcmVjdGl2ZUFyZ3VtZW50V2VpZ2h0c0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoBToCOAEiowMKGEZpZWxkV2VpZ2h0Q29uZmlndXJhdGlvbhIRCgl0eXBlX25hbWUYASABKAkSEgoKZmllbGRfbmFtZRgCIAEoCRITCgZ3ZWlnaHQYAyABKAVIAIgBARJZChBhcmd1bWVudF93ZWlnaHRzGAQgAygLMj8ud2cuY29zbW8ubm9kZS52MS5GaWVsZFdlaWdodENvbmZpZ3VyYXRpb24uQXJndW1lbnRXZWlnaHRzRW50cnkSbAoaZGlyZWN0aXZlX2FyZ3VtZW50X3dlaWdodHMYBSADKAsySC53Zy5jb3Ntby5ub2RlLnYxLkZpZWxkV2VpZ2h0Q29uZmlndXJhdGlvbi5EaXJlY3RpdmVBcmd1bWVudFdlaWdodHNFbnRyeRo2ChRBcmd1bWVudFdlaWdodHNFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAU6AjgBGj8KHURpcmVjdGl2ZUFyZ3VtZW50V2VpZ2h0c0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoBToCOAFCCQoHX3dlaWdodCLsAQoaRmllbGRMaXN0U2l6ZUNvbmZpZ3VyYXRpb24SEQoJdHlwZV9uYW1lGAEgASgJEhIKCmZpZWxkX25hbWUYAiABKAkSGQoMYXNzdW1lZF9zaXplGAMgASgFSACIAQESGQoRc2xpY2luZ19hcmd1bWVudHMYBCADKAkSFAoMc2l6ZWRfZmllbGRzGAUgAygJEikKHHJlcXVpcmVfb25lX3NsaWNpbmdfYXJndW1lbnQYBiABKAhIAYgBAUIPCg1fYXNzdW1lZF9zaXplQh8KHV9yZXF1aXJlX29uZV9zbGljaW5nX2FyZ3VtZW50IlwKFUFyZ3VtZW50Q29uZmlndXJhdGlvbhIMCgRuYW1lGAEgASgJEjUKC3NvdXJjZV90eXBlGAIgASgOMiAud2cuY29zbW8ubm9kZS52MS5Bcmd1bWVudFNvdXJjZSIlCgZTY29wZXMSGwoTcmVxdWlyZWRfYW5kX3Njb3BlcxgBIAMoCSKvAQoaQXV0aG9yaXphdGlvbkNvbmZpZ3VyYXRpb24SHwoXcmVxdWlyZXNfYXV0aGVudGljYXRpb24YASABKAgSNAoScmVxdWlyZWRfb3Jfc2NvcGVzGAIgAygLMhgud2cuY29zbW8ubm9kZS52MS5TY29wZXMSOgoYcmVxdWlyZWRfb3Jfc2NvcGVzX2J5X29yGAMgAygLMhgud2cuY29zbW8ubm9kZS52MS5TY29wZXMi1QIKEkZpZWxkQ29uZmlndXJhdGlvbhIRCgl0eXBlX25hbWUYASABKAkSEgoKZmllbGRfbmFtZRgCIAEoCRJIChdhcmd1bWVudHNfY29uZmlndXJhdGlvbhgDIAMoCzInLndnLmNvc21vLm5vZGUudjEuQXJndW1lbnRDb25maWd1cmF0aW9uElEKG2F1dGhvcml6YXRpb25fY29uZmlndXJhdGlvbhgEIAEoCzIsLndnLmNvc21vLm5vZGUudjEuQXV0aG9yaXphdGlvbkNvbmZpZ3VyYXRpb24SWQodc3Vic2NyaXB0aW9uX2ZpbHRlcl9jb25kaXRpb24YBSABKAsyLS53Zy5jb3Ntby5ub2RlLnYxLlN1YnNjcmlwdGlvbkZpbHRlckNvbmRpdGlvbkgAiAEBQiAKHl9zdWJzY3JpcHRpb25fZmlsdGVyX2NvbmRpdGlvbiI5ChFUeXBlQ29uZmlndXJhdGlvbhIRCgl0eXBlX25hbWUYASABKAkSEQoJcmVuYW1lX3RvGAIgASgJInwKCVR5cGVGaWVsZBIRCgl0eXBlX25hbWUYASABKAkSEwoLZmllbGRfbmFtZXMYAiADKAkSHAoUZXh0ZXJuYWxfZmllbGRfbmFtZXMYAyADKAkSKQohcmVxdWlyZV9mZXRjaF9yZWFzb25zX2ZpZWxkX25hbWVzGAQgAygJIjkKEEZpZWxkQ29vcmRpbmF0ZXMSEgoKZmllbGRfbmFtZRgBIAEoCRIRCgl0eXBlX25hbWUYAiABKAkiawoRRmllbGRTZXRDb25kaXRpb24SQgoWZmllbGRfY29vcmRpbmF0ZXNfcGF0aBgBIAMoCzIiLndnLmNvc21vLm5vZGUudjEuRmllbGRDb29yZGluYXRlcxISCgpmaWVsZF9wYXRoGAIgAygJIqcBCg1SZXF1aXJlZEZpZWxkEhEKCXR5cGVfbmFtZRgBIAEoCRISCgpmaWVsZF9uYW1lGAIgASgJEhUKDXNlbGVjdGlvbl9zZXQYAyABKAkSHwoXZGlzYWJsZV9lbnRpdHlfcmVzb2x2ZXIYBCABKAgSNwoKY29uZGl0aW9ucxgFIAMoCzIjLndnLmNvc21vLm5vZGUudjEuRmllbGRTZXRDb25kaXRpb24iWAocRW50aXR5SW50ZXJmYWNlQ29uZmlndXJhdGlvbhIbChNpbnRlcmZhY2VfdHlwZV9uYW1lGAEgASgJEhsKE2NvbmNyZXRlX3R5cGVfbmFtZXMYAiADKAkijQUKEkZldGNoQ29uZmlndXJhdGlvbhI0CgN1cmwYASABKAsyJy53Zy5jb3Ntby5ub2RlLnYxLkNvbmZpZ3VyYXRpb25WYXJpYWJsZRIsCgZtZXRob2QYAiABKA4yHC53Zy5jb3Ntby5ub2RlLnYxLkhUVFBNZXRob2QSQAoGaGVhZGVyGAMgAygLMjAud2cuY29zbW8ubm9kZS52MS5GZXRjaENvbmZpZ3VyYXRpb24uSGVhZGVyRW50cnkSNQoEYm9keRgEIAEoCzInLndnLmNvc21vLm5vZGUudjEuQ29uZmlndXJhdGlvblZhcmlhYmxlEjYKBXF1ZXJ5GAUgAygLMicud2cuY29zbW8ubm9kZS52MS5VUkxRdWVyeUNvbmZpZ3VyYXRpb24SFwoPdXJsX2VuY29kZV9ib2R5GAcgASgIEjEKBG10bHMYCCABKAsyIy53Zy5jb3Ntby5ub2RlLnYxLk1UTFNDb25maWd1cmF0aW9uEjkKCGJhc2VfdXJsGAkgASgLMicud2cuY29zbW8ubm9kZS52MS5Db25maWd1cmF0aW9uVmFyaWFibGUSNQoEcGF0aBgKIAEoCzInLndnLmNvc21vLm5vZGUudjEuQ29uZmlndXJhdGlvblZhcmlhYmxlEkQKDmh0dHBfcHJveHlfdXJsGAsgASgLMicud2cuY29zbW8ubm9kZS52MS5Db25maWd1cmF0aW9uVmFyaWFibGVIAIgBARpLCgtIZWFkZXJFbnRyeRILCgNrZXkYASABKAkSKwoFdmFsdWUYAiABKAsyHC53Zy5jb3Ntby5ub2RlLnYxLkhUVFBIZWFkZXI6AjgBQhEKD19odHRwX3Byb3h5X3VybCJlChVTdGF0dXNDb2RlVHlwZU1hcHBpbmcSEwoLc3RhdHVzX2NvZGUYASABKAMSEQoJdHlwZV9uYW1lGAIgASgJEiQKHGluamVjdF9zdGF0dXNfY29kZV9pbnRvX2JvZHkYAyABKAgikwMKGERhdGFTb3VyY2VDdXN0b21fR3JhcGhRTBIzCgVmZXRjaBgBIAEoCzIkLndnLmNvc21vLm5vZGUudjEuRmV0Y2hDb25maWd1cmF0aW9uEkgKDHN1YnNjcmlwdGlvbhgCIAEoCzIyLndnLmNvc21vLm5vZGUudjEuR3JhcGhRTFN1YnNjcmlwdGlvbkNvbmZpZ3VyYXRpb24SRAoKZmVkZXJhdGlvbhgDIAEoCzIwLndnLmNvc21vLm5vZGUudjEuR3JhcGhRTEZlZGVyYXRpb25Db25maWd1cmF0aW9uEjkKD3Vwc3RyZWFtX3NjaGVtYRgEIAEoCzIgLndnLmNvc21vLm5vZGUudjEuSW50ZXJuZWRTdHJpbmcSRAoZY3VzdG9tX3NjYWxhcl90eXBlX2ZpZWxkcxgGIAMoCzIhLndnLmNvc21vLm5vZGUudjEuU2luZ2xlVHlwZUZpZWxkEjEKBGdycGMYByABKAsyIy53Zy5jb3Ntby5ub2RlLnYxLkdSUENDb25maWd1cmF0aW9uIpABChFHUlBDQ29uZmlndXJhdGlvbhIuCgdtYXBwaW5nGAEgASgLMh0ud2cuY29zbW8ubm9kZS52MS5HUlBDTWFwcGluZxIUCgxwcm90b19zY2hlbWEYAiABKAkSNQoGcGx1Z2luGAMgASgLMiUud2cuY29zbW8ubm9kZS52MS5QbHVnaW5Db25maWd1cmF0aW9uIjcKDkltYWdlUmVmZXJlbmNlEhIKCnJlcG9zaXRvcnkYASABKAkSEQoJcmVmZXJlbmNlGAIgASgJIogBChNQbHVnaW5Db25maWd1cmF0aW9uEgwKBG5hbWUYASABKAkSDwoHdmVyc2lvbhgCIAEoCRI+Cg9pbWFnZV9yZWZlcmVuY2UYAyABKAsyIC53Zy5jb3Ntby5ub2RlLnYxLkltYWdlUmVmZXJlbmNlSACIAQFCEgoQX2ltYWdlX3JlZmVyZW5jZSIjChBTU0xDb25maWd1cmF0aW9uEg8KB2VuYWJsZWQYASABKAgi2wIKC0dSUENNYXBwaW5nEg8KB3ZlcnNpb24YASABKAUSDwoHc2VydmljZRgCIAEoCRI+ChJvcGVyYXRpb25fbWFwcGluZ3MYAyADKAsyIi53Zy5jb3Ntby5ub2RlLnYxLk9wZXJhdGlvbk1hcHBpbmcSOAoPZW50aXR5X21hcHBpbmdzGAQgAygLMh8ud2cuY29zbW8ubm9kZS52MS5FbnRpdHlNYXBwaW5nEj8KE3R5cGVfZmllbGRfbWFwcGluZ3MYBSADKAsyIi53Zy5jb3Ntby5ub2RlLnYxLlR5cGVGaWVsZE1hcHBpbmcSNAoNZW51bV9tYXBwaW5ncxgGIAMoCzIdLndnLmNvc21vLm5vZGUudjEuRW51bU1hcHBpbmcSOQoQcmVzb2x2ZV9tYXBwaW5ncxgHIAMoCzIfLndnLmNvc21vLm5vZGUudjEuTG9va3VwTWFwcGluZyKpAQoNTG9va3VwTWFwcGluZxIqCgR0eXBlGAEgASgOMhwud2cuY29zbW8ubm9kZS52MS5Mb29rdXBUeXBlEjwKDmxvb2t1cF9tYXBwaW5nGAIgASgLMiQud2cuY29zbW8ubm9kZS52MS5Mb29rdXBGaWVsZE1hcHBpbmcSCwoDcnBjGAMgASgJEg8KB3JlcXVlc3QYBCABKAkSEAoIcmVzcG9uc2UYBSABKAkiWQoSTG9va3VwRmllbGRNYXBwaW5nEgwKBHR5cGUYASABKAkSNQoNZmllbGRfbWFwcGluZxgCIAEoCzIeLndnLmNvc21vLm5vZGUudjEuRmllbGRNYXBwaW5nIoYBChBPcGVyYXRpb25NYXBwaW5nEi0KBHR5cGUYASABKA4yHy53Zy5jb3Ntby5ub2RlLnYxLk9wZXJhdGlvblR5cGUSEAoIb3JpZ2luYWwYAiABKAkSDgoGbWFwcGVkGAMgASgJEg8KB3JlcXVlc3QYBCABKAkSEAoIcmVzcG9uc2UYBSABKAkitgEKDUVudGl0eU1hcHBpbmcSEQoJdHlwZV9uYW1lGAEgASgJEgwKBGtpbmQYAiABKAkSCwoDa2V5GAMgASgJEgsKA3JwYxgEIAEoCRIPCgdyZXF1ZXN0GAUgASgJEhAKCHJlc3BvbnNlGAYgASgJEkcKF3JlcXVpcmVkX2ZpZWxkX21hcHBpbmdzGAcgAygLMiYud2cuY29zbW8ubm9kZS52MS5SZXF1aXJlZEZpZWxkTWFwcGluZyJ9ChRSZXF1aXJlZEZpZWxkTWFwcGluZxI1Cg1maWVsZF9tYXBwaW5nGAEgASgLMh4ud2cuY29zbW8ubm9kZS52MS5GaWVsZE1hcHBpbmcSCwoDcnBjGAIgASgJEg8KB3JlcXVlc3QYAyABKAkSEAoIcmVzcG9uc2UYBCABKAkiWAoQVHlwZUZpZWxkTWFwcGluZxIMCgR0eXBlGAEgASgJEjYKDmZpZWxkX21hcHBpbmdzGAIgAygLMh4ud2cuY29zbW8ubm9kZS52MS5GaWVsZE1hcHBpbmcibgoMRmllbGRNYXBwaW5nEhAKCG9yaWdpbmFsGAEgASgJEg4KBm1hcHBlZBgCIAEoCRI8ChFhcmd1bWVudF9tYXBwaW5ncxgDIAMoCzIhLndnLmNvc21vLm5vZGUudjEuQXJndW1lbnRNYXBwaW5nIjMKD0FyZ3VtZW50TWFwcGluZxIQCghvcmlnaW5hbBgBIAEoCRIOCgZtYXBwZWQYAiABKAkiTwoLRW51bU1hcHBpbmcSDAoEdHlwZRgBIAEoCRIyCgZ2YWx1ZXMYAiADKAsyIi53Zy5jb3Ntby5ub2RlLnYxLkVudW1WYWx1ZU1hcHBpbmciNAoQRW51bVZhbHVlTWFwcGluZxIQCghvcmlnaW5hbBgBIAEoCRIOCgZtYXBwZWQYAiABKAkiagoXTmF0c1N0cmVhbUNvbmZpZ3VyYXRpb24SFQoNY29uc3VtZXJfbmFtZRgBIAEoCRITCgtzdHJlYW1fbmFtZRgCIAEoCRIjChtjb25zdW1lcl9pbmFjdGl2ZV90aHJlc2hvbGQYAyABKAUiwwEKFk5hdHNFdmVudENvbmZpZ3VyYXRpb24STgoaZW5naW5lX2V2ZW50X2NvbmZpZ3VyYXRpb24YASABKAsyKi53Zy5jb3Ntby5ub2RlLnYxLkVuZ2luZUV2ZW50Q29uZmlndXJhdGlvbhIQCghzdWJqZWN0cxgCIAMoCRJHChRzdHJlYW1fY29uZmlndXJhdGlvbhgDIAEoCzIpLndnLmNvc21vLm5vZGUudjEuTmF0c1N0cmVhbUNvbmZpZ3VyYXRpb24ieQoXS2Fma2FFdmVudENvbmZpZ3VyYXRpb24STgoaZW5naW5lX2V2ZW50X2NvbmZpZ3VyYXRpb24YASABKAsyKi53Zy5jb3Ntby5ub2RlLnYxLkVuZ2luZUV2ZW50Q29uZmlndXJhdGlvbhIOCgZ0b3BpY3MYAiADKAkiewoXUmVkaXNFdmVudENvbmZpZ3VyYXRpb24STgoaZW5naW5lX2V2ZW50X2NvbmZpZ3VyYXRpb24YASABKAsyKi53Zy5jb3Ntby5ub2RlLnYxLkVuZ2luZUV2ZW50Q29uZmlndXJhdGlvbhIQCghjaGFubmVscxgCIAMoCSKBAQoYRW5naW5lRXZlbnRDb25maWd1cmF0aW9uEhMKC3Byb3ZpZGVyX2lkGAEgASgJEikKBHR5cGUYAiABKA4yGy53Zy5jb3Ntby5ub2RlLnYxLkV2ZW50VHlwZRIRCgl0eXBlX25hbWUYAyABKAkSEgoKZmllbGRfbmFtZRgEIAEoCSLEAQoWRGF0YVNvdXJjZUN1c3RvbUV2ZW50cxI2CgRuYXRzGAEgAygLMigud2cuY29zbW8ubm9kZS52MS5OYXRzRXZlbnRDb25maWd1cmF0aW9uEjgKBWthZmthGAIgAygLMikud2cuY29zbW8ubm9kZS52MS5LYWZrYUV2ZW50Q29uZmlndXJhdGlvbhI4CgVyZWRpcxgDIAMoCzIpLndnLmNvc21vLm5vZGUudjEuUmVkaXNFdmVudENvbmZpZ3VyYXRpb24iUAoXRGF0YVNvdXJjZUN1c3RvbV9TdGF0aWMSNQoEZGF0YRgBIAEoCzInLndnLmNvc21vLm5vZGUudjEuQ29uZmlndXJhdGlvblZhcmlhYmxlIuUBChVDb25maWd1cmF0aW9uVmFyaWFibGUSOQoEa2luZBgBIAEoDjIrLndnLmNvc21vLm5vZGUudjEuQ29uZmlndXJhdGlvblZhcmlhYmxlS2luZBIfChdzdGF0aWNfdmFyaWFibGVfY29udGVudBgCIAEoCRIhChllbnZpcm9ubWVudF92YXJpYWJsZV9uYW1lGAMgASgJEioKImVudmlyb25tZW50X3ZhcmlhYmxlX2RlZmF1bHRfdmFsdWUYBCABKAkSIQoZcGxhY2Vob2xkZXJfdmFyaWFibGVfbmFtZRgFIAEoCSJDChZEaXJlY3RpdmVDb25maWd1cmF0aW9uEhYKDmRpcmVjdGl2ZV9uYW1lGAEgASgJEhEKCXJlbmFtZV90bxgCIAEoCSI0ChVVUkxRdWVyeUNvbmZpZ3VyYXRpb24SDAoEbmFtZRgBIAEoCRINCgV2YWx1ZRgCIAEoCSJFCgpIVFRQSGVhZGVyEjcKBnZhbHVlcxgBIAMoCzInLndnLmNvc21vLm5vZGUudjEuQ29uZmlndXJhdGlvblZhcmlhYmxlIpwBChFNVExTQ29uZmlndXJhdGlvbhI0CgNrZXkYASABKAsyJy53Zy5jb3Ntby5ub2RlLnYxLkNvbmZpZ3VyYXRpb25WYXJpYWJsZRI1CgRjZXJ0GAIgASgLMicud2cuY29zbW8ubm9kZS52MS5Db25maWd1cmF0aW9uVmFyaWFibGUSGgoSaW5zZWN1cmVTa2lwVmVyaWZ5GAMgASgIIsUCCiBHcmFwaFFMU3Vic2NyaXB0aW9uQ29uZmlndXJhdGlvbhIPCgdlbmFibGVkGAEgASgIEjQKA3VybBgCIAEoCzInLndnLmNvc21vLm5vZGUudjEuQ29uZmlndXJhdGlvblZhcmlhYmxlEhMKBnVzZVNTRRgDIAEoCEgAiAEBEkMKCHByb3RvY29sGAQgASgOMiwud2cuY29zbW8uY29tbW9uLkdyYXBoUUxTdWJzY3JpcHRpb25Qcm90b2NvbEgBiAEBEk8KFHdlYnNvY2tldFN1YnByb3RvY29sGAUgASgOMiwud2cuY29zbW8uY29tbW9uLkdyYXBoUUxXZWJzb2NrZXRTdWJwcm90b2NvbEgCiAEBQgkKB191c2VTU0VCCwoJX3Byb3RvY29sQhcKFV93ZWJzb2NrZXRTdWJwcm90b2NvbCJFCh5HcmFwaFFMRmVkZXJhdGlvbkNvbmZpZ3VyYXRpb24SDwoHZW5hYmxlZBgBIAEoCBISCgpzZXJ2aWNlU2RsGAIgASgJIh0KDkludGVybmVkU3RyaW5nEgsKA2tleRgBIAEoCSI4Cg9TaW5nbGVUeXBlRmllbGQSEQoJdHlwZV9uYW1lGAEgASgJEhIKCmZpZWxkX25hbWUYAiABKAkiPgoaU3Vic2NyaXB0aW9uRmllbGRDb25kaXRpb24SEgoKZmllbGRfcGF0aBgBIAMoCRIMCgRqc29uGAIgASgJIqMCChtTdWJzY3JpcHRpb25GaWx0ZXJDb25kaXRpb24SOgoDYW5kGAEgAygLMi0ud2cuY29zbW8ubm9kZS52MS5TdWJzY3JpcHRpb25GaWx0ZXJDb25kaXRpb24SPQoCaW4YAiABKAsyLC53Zy5jb3Ntby5ub2RlLnYxLlN1YnNjcmlwdGlvbkZpZWxkQ29uZGl0aW9uSACIAQESPwoDbm90GAMgASgLMi0ud2cuY29zbW8ubm9kZS52MS5TdWJzY3JpcHRpb25GaWx0ZXJDb25kaXRpb25IAYgBARI5CgJvchgEIAMoCzItLndnLmNvc21vLm5vZGUudjEuU3Vic2NyaXB0aW9uRmlsdGVyQ29uZGl0aW9uQgUKA19pbkIGCgRfbm90IkgKFUNhY2hlV2FybWVyT3BlcmF0aW9ucxIvCgpvcGVyYXRpb25zGAEgAygLMhsud2cuY29zbW8ubm9kZS52MS5PcGVyYXRpb24ibgoJT3BlcmF0aW9uEjMKB3JlcXVlc3QYASABKAsyIi53Zy5jb3Ntby5ub2RlLnYxLk9wZXJhdGlvblJlcXVlc3QSLAoGY2xpZW50GAIgASgLMhwud2cuY29zbW8ubm9kZS52MS5DbGllbnRJbmZvImoKEE9wZXJhdGlvblJlcXVlc3QSFgoOb3BlcmF0aW9uX25hbWUYASABKAkSDQoFcXVlcnkYAiABKAkSLwoKZXh0ZW5zaW9ucxgDIAEoCzIbLndnLmNvc21vLm5vZGUudjEuRXh0ZW5zaW9uIkYKCUV4dGVuc2lvbhI5Cg9wZXJzaXN0ZWRfcXVlcnkYASABKAsyIC53Zy5jb3Ntby5ub2RlLnYxLlBlcnNpc3RlZFF1ZXJ5IjYKDlBlcnNpc3RlZFF1ZXJ5EhMKC3NoYTI1Nl9oYXNoGAEgASgJEg8KB3ZlcnNpb24YAiABKAUiKwoKQ2xpZW50SW5mbxIMCgRuYW1lGAEgASgJEg8KB3ZlcnNpb24YAiABKAkqQwoWU2F0aXNmaWVkT3BlcmF0aW9uVHlwZRIJCgVRVUVSWRAAEgwKCE1VVEFUSU9OEAESEAoMU1VCU0NSSVBUSU9OEAIqggEKG0FyZ3VtZW50UmVuZGVyQ29uZmlndXJhdGlvbhIbChdSRU5ERVJfQVJHVU1FTlRfREVGQVVMVBAAEiQKIFJFTkRFUl9BUkdVTUVOVF9BU19HUkFQSFFMX1ZBTFVFEAESIAocUkVOREVSX0FSR1VNRU5UX0FTX0FSUkFZX0NTVhACKjYKDkFyZ3VtZW50U291cmNlEhAKDE9CSkVDVF9GSUVMRBAAEhIKDkZJRUxEX0FSR1VNRU5UEAEqNQoORGF0YVNvdXJjZUtpbmQSCgoGU1RBVElDEAASCwoHR1JBUEhRTBABEgoKBlBVQlNVQhACKlwKCkxvb2t1cFR5cGUSGwoXTE9PS1VQX1RZUEVfVU5TUEVDSUZJRUQQABIXChNMT09LVVBfVFlQRV9SRVNPTFZFEAESGAoUTE9PS1VQX1RZUEVfUkVRVUlSRVMQAiqHAQoNT3BlcmF0aW9uVHlwZRIeChpPUEVSQVRJT05fVFlQRV9VTlNQRUNJRklFRBAAEhgKFE9QRVJBVElPTl9UWVBFX1FVRVJZEAESGwoXT1BFUkFUSU9OX1RZUEVfTVVUQVRJT04QAhIfChtPUEVSQVRJT05fVFlQRV9TVUJTQ1JJUFRJT04QAyo0CglFdmVudFR5cGUSCwoHUFVCTElTSBAAEgsKB1JFUVVFU1QQARINCglTVUJTQ1JJQkUQAiqGAQoZQ29uZmlndXJhdGlvblZhcmlhYmxlS2luZBIhCh1TVEFUSUNfQ09ORklHVVJBVElPTl9WQVJJQUJMRRAAEh4KGkVOVl9DT05GSUdVUkFUSU9OX1ZBUklBQkxFEAESJgoiUExBQ0VIT0xERVJfQ09ORklHVVJBVElPTl9WQVJJQUJMRRACKkEKCkhUVFBNZXRob2QSBwoDR0VUEAASCAoEUE9TVBABEgcKA1BVVBACEgoKBkRFTEVURRADEgsKB09QVElPTlMQBDLSAQoLTm9kZVNlcnZpY2USXwoMU2VsZlJlZ2lzdGVyEiUud2cuY29zbW8ubm9kZS52MS5TZWxmUmVnaXN0ZXJSZXF1ZXN0GiYud2cuY29zbW8ubm9kZS52MS5TZWxmUmVnaXN0ZXJSZXNwb25zZSIAEmIKDUdlbmVyYXRlUXVlcnkSJi53Zy5jb3Ntby5ub2RlLnYxLkdlbmVyYXRlUXVlcnlSZXF1ZXN0Gicud2cuY29zbW8ubm9kZS52MS5HZW5lcmF0ZVF1ZXJ5UmVzcG9uc2UiAGIGcHJvdG8z", [file_wg_cosmo_common_common]); + fileDesc("Cht3Zy9jb3Ntby9ub2RlL3YxL25vZGUucHJvdG8SEHdnLmNvc21vLm5vZGUudjEiOQoIU3ViZ3JhcGgSCgoCaWQYASABKAkSDAoEbmFtZRgCIAEoCRITCgtyb3V0aW5nX3VybBgDIAEoCSKOAgohRmVhdHVyZUZsYWdSb3V0ZXJFeGVjdXRpb25Db25maWdzEnUKG2NvbmZpZ19ieV9mZWF0dXJlX2ZsYWdfbmFtZRgBIAMoCzJQLndnLmNvc21vLm5vZGUudjEuRmVhdHVyZUZsYWdSb3V0ZXJFeGVjdXRpb25Db25maWdzLkNvbmZpZ0J5RmVhdHVyZUZsYWdOYW1lRW50cnkacgocQ29uZmlnQnlGZWF0dXJlRmxhZ05hbWVFbnRyeRILCgNrZXkYASABKAkSQQoFdmFsdWUYAiABKAsyMi53Zy5jb3Ntby5ub2RlLnYxLkZlYXR1cmVGbGFnUm91dGVyRXhlY3V0aW9uQ29uZmlnOgI4ASKgAQogRmVhdHVyZUZsYWdSb3V0ZXJFeGVjdXRpb25Db25maWcSPAoNZW5naW5lX2NvbmZpZxgBIAEoCzIlLndnLmNvc21vLm5vZGUudjEuRW5naW5lQ29uZmlndXJhdGlvbhIPCgd2ZXJzaW9uGAIgASgJEi0KCXN1YmdyYXBocxgDIAMoCzIaLndnLmNvc21vLm5vZGUudjEuU3ViZ3JhcGginAIKDFJvdXRlckNvbmZpZxI8Cg1lbmdpbmVfY29uZmlnGAEgASgLMiUud2cuY29zbW8ubm9kZS52MS5FbmdpbmVDb25maWd1cmF0aW9uEg8KB3ZlcnNpb24YAiABKAkSLQoJc3ViZ3JhcGhzGAMgAygLMhoud2cuY29zbW8ubm9kZS52MS5TdWJncmFwaBJWChRmZWF0dXJlX2ZsYWdfY29uZmlncxgEIAEoCzIzLndnLmNvc21vLm5vZGUudjEuRmVhdHVyZUZsYWdSb3V0ZXJFeGVjdXRpb25Db25maWdzSACIAQESHQoVY29tcGF0aWJpbGl0eV92ZXJzaW9uGAUgASgJQhcKFV9mZWF0dXJlX2ZsYWdfY29uZmlncyJbCghSZXNwb25zZRItCgRjb2RlGAEgASgOMh8ud2cuY29zbW8uY29tbW9uLkVudW1TdGF0dXNDb2RlEhQKB2RldGFpbHMYAiABKAlIAIgBAUIKCghfZGV0YWlscyIvCg5SZXNwb25zZVN0YXR1cxIMCgRjb2RlGAEgASgFEg8KB21lc3NhZ2UYAiABKAkiZQoQUmVnaXN0cmF0aW9uSW5mbxI3Cg5hY2NvdW50X2xpbWl0cxgBIAEoCzIfLndnLmNvc21vLm5vZGUudjEuQWNjb3VudExpbWl0cxIYChBncmFwaF9wdWJsaWNfa2V5GAIgASgJIiwKDUFjY291bnRMaW1pdHMSGwoTdHJhY2Vfc2FtcGxpbmdfcmF0ZRgBIAEoAiIVChNTZWxmUmVnaXN0ZXJSZXF1ZXN0IpwBChRTZWxmUmVnaXN0ZXJSZXNwb25zZRIsCghyZXNwb25zZRgBIAEoCzIaLndnLmNvc21vLm5vZGUudjEuUmVzcG9uc2USQQoQcmVnaXN0cmF0aW9uSW5mbxgCIAEoCzIiLndnLmNvc21vLm5vZGUudjEuUmVnaXN0cmF0aW9uSW5mb0gAiAEBQhMKEV9yZWdpc3RyYXRpb25JbmZvIkEKFEdlbmVyYXRlUXVlcnlSZXF1ZXN0EhkKEXNjaGVtYV92ZXJzaW9uX2lkGAEgASgJEg4KBnByb21wdBgCIAEoCSKFAQoVR2VuZXJhdGVRdWVyeVJlc3BvbnNlEiwKCHJlc3BvbnNlGAEgASgLMhoud2cuY29zbW8ubm9kZS52MS5SZXNwb25zZRI0CgVxdWVyeRgCIAEoCzIgLndnLmNvc21vLm5vZGUudjEuU2F0aXNmaWVkUXVlcnlIAIgBAUIICgZfcXVlcnkiqwEKDlNhdGlzZmllZFF1ZXJ5EhMKC2Rlc2NyaXB0aW9uGAEgASgJEhAKCGRvY3VtZW50GAIgASgJEhYKDm9wZXJhdGlvbl9uYW1lGAMgASgJEkAKDm9wZXJhdGlvbl90eXBlGAQgASgOMigud2cuY29zbW8ubm9kZS52MS5TYXRpc2ZpZWRPcGVyYXRpb25UeXBlEhgKEHZhcmlhYmxlc19zY2hlbWEYBSABKAki5AMKE0VuZ2luZUNvbmZpZ3VyYXRpb24SHAoUZGVmYXVsdEZsdXNoSW50ZXJ2YWwYASABKAMSTAoZZGF0YXNvdXJjZV9jb25maWd1cmF0aW9ucxgCIAMoCzIpLndnLmNvc21vLm5vZGUudjEuRGF0YVNvdXJjZUNvbmZpZ3VyYXRpb24SQgoUZmllbGRfY29uZmlndXJhdGlvbnMYAyADKAsyJC53Zy5jb3Ntby5ub2RlLnYxLkZpZWxkQ29uZmlndXJhdGlvbhIVCg1ncmFwaHFsU2NoZW1hGAQgASgJEkAKE3R5cGVfY29uZmlndXJhdGlvbnMYBSADKAsyIy53Zy5jb3Ntby5ub2RlLnYxLlR5cGVDb25maWd1cmF0aW9uElAKDnN0cmluZ19zdG9yYWdlGAYgAygLMjgud2cuY29zbW8ubm9kZS52MS5FbmdpbmVDb25maWd1cmF0aW9uLlN0cmluZ1N0b3JhZ2VFbnRyeRIiChVncmFwaHFsX2NsaWVudF9zY2hlbWEYByABKAlIAIgBARo0ChJTdHJpbmdTdG9yYWdlRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4AUIYChZfZ3JhcGhxbF9jbGllbnRfc2NoZW1hIsYHChdEYXRhU291cmNlQ29uZmlndXJhdGlvbhIuCgRraW5kGAEgASgOMiAud2cuY29zbW8ubm9kZS52MS5EYXRhU291cmNlS2luZBIvCgpyb290X25vZGVzGAIgAygLMhsud2cuY29zbW8ubm9kZS52MS5UeXBlRmllbGQSMAoLY2hpbGRfbm9kZXMYAyADKAsyGy53Zy5jb3Ntby5ub2RlLnYxLlR5cGVGaWVsZBImCh5vdmVycmlkZV9maWVsZF9wYXRoX2Zyb21fYWxpYXMYBCABKAgSQgoOY3VzdG9tX2dyYXBocWwYBSABKAsyKi53Zy5jb3Ntby5ub2RlLnYxLkRhdGFTb3VyY2VDdXN0b21fR3JhcGhRTBJACg1jdXN0b21fc3RhdGljGAYgASgLMikud2cuY29zbW8ubm9kZS52MS5EYXRhU291cmNlQ3VzdG9tX1N0YXRpYxI8CgpkaXJlY3RpdmVzGAcgAygLMigud2cuY29zbW8ubm9kZS52MS5EaXJlY3RpdmVDb25maWd1cmF0aW9uEh8KF3JlcXVlc3RfdGltZW91dF9zZWNvbmRzGAggASgDEgoKAmlkGAkgASgJEi0KBGtleXMYCiADKAsyHy53Zy5jb3Ntby5ub2RlLnYxLlJlcXVpcmVkRmllbGQSMQoIcHJvdmlkZXMYCyADKAsyHy53Zy5jb3Ntby5ub2RlLnYxLlJlcXVpcmVkRmllbGQSMQoIcmVxdWlyZXMYDCADKAsyHy53Zy5jb3Ntby5ub2RlLnYxLlJlcXVpcmVkRmllbGQSPwoNY3VzdG9tX2V2ZW50cxgNIAEoCzIoLndnLmNvc21vLm5vZGUudjEuRGF0YVNvdXJjZUN1c3RvbUV2ZW50cxJJChFlbnRpdHlfaW50ZXJmYWNlcxgOIAMoCzIuLndnLmNvc21vLm5vZGUudjEuRW50aXR5SW50ZXJmYWNlQ29uZmlndXJhdGlvbhJJChFpbnRlcmZhY2Vfb2JqZWN0cxgPIAMoCzIuLndnLmNvc21vLm5vZGUudjEuRW50aXR5SW50ZXJmYWNlQ29uZmlndXJhdGlvbhI/ChJjb3N0X2NvbmZpZ3VyYXRpb24YECABKAsyIy53Zy5jb3Ntby5ub2RlLnYxLkNvc3RDb25maWd1cmF0aW9uElIKHGVudGl0eV9jYWNoaW5nX2NvbmZpZ3VyYXRpb24YESABKAsyLC53Zy5jb3Ntby5ub2RlLnYxLkVudGl0eUNhY2hpbmdDb25maWd1cmF0aW9uIpsCChpFbnRpdHlDYWNoaW5nQ29uZmlndXJhdGlvbhJPChtlbnRpdHlfY2FjaGVfY29uZmlndXJhdGlvbnMYASADKAsyKi53Zy5jb3Ntby5ub2RlLnYxLkVudGl0eUNhY2hlQ29uZmlndXJhdGlvbhJXCh9jYWNoZV9pbnZhbGlkYXRlX2NvbmZpZ3VyYXRpb25zGAIgAygLMi4ud2cuY29zbW8ubm9kZS52MS5DYWNoZUludmFsaWRhdGVDb25maWd1cmF0aW9uElMKHWNhY2hlX3BvcHVsYXRlX2NvbmZpZ3VyYXRpb25zGAMgAygLMiwud2cuY29zbW8ubm9kZS52MS5DYWNoZVBvcHVsYXRlQ29uZmlndXJhdGlvbiK1AQoYRW50aXR5Q2FjaGVDb25maWd1cmF0aW9uEhEKCXR5cGVfbmFtZRgBIAEoCRIXCg9tYXhfYWdlX3NlY29uZHMYAiABKAMSFwoPaW5jbHVkZV9oZWFkZXJzGAMgASgIEhoKEnBhcnRpYWxfY2FjaGVfbG9hZBgEIAEoCBITCgtzaGFkb3dfbW9kZRgFIAEoCBIjChtub3RfZm91bmRfY2FjaGVfdHRsX3NlY29uZHMYBiABKAMiZAocQ2FjaGVJbnZhbGlkYXRlQ29uZmlndXJhdGlvbhISCgpmaWVsZF9uYW1lGAEgASgJEhYKDm9wZXJhdGlvbl90eXBlGAIgASgJEhgKEGVudGl0eV90eXBlX25hbWUYAyABKAkiewoaQ2FjaGVQb3B1bGF0ZUNvbmZpZ3VyYXRpb24SEgoKZmllbGRfbmFtZRgBIAEoCRIWCg5vcGVyYXRpb25fdHlwZRgCIAEoCRIXCg9tYXhfYWdlX3NlY29uZHMYAyABKAMSGAoQZW50aXR5X3R5cGVfbmFtZRgEIAEoCSLAAwoRQ29zdENvbmZpZ3VyYXRpb24SQQoNZmllbGRfd2VpZ2h0cxgBIAMoCzIqLndnLmNvc21vLm5vZGUudjEuRmllbGRXZWlnaHRDb25maWd1cmF0aW9uEkAKCmxpc3Rfc2l6ZXMYAiADKAsyLC53Zy5jb3Ntby5ub2RlLnYxLkZpZWxkTGlzdFNpemVDb25maWd1cmF0aW9uEkoKDHR5cGVfd2VpZ2h0cxgDIAMoCzI0LndnLmNvc21vLm5vZGUudjEuQ29zdENvbmZpZ3VyYXRpb24uVHlwZVdlaWdodHNFbnRyeRJlChpkaXJlY3RpdmVfYXJndW1lbnRfd2VpZ2h0cxgEIAMoCzJBLndnLmNvc21vLm5vZGUudjEuQ29zdENvbmZpZ3VyYXRpb24uRGlyZWN0aXZlQXJndW1lbnRXZWlnaHRzRW50cnkaMgoQVHlwZVdlaWdodHNFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAU6AjgBGj8KHURpcmVjdGl2ZUFyZ3VtZW50V2VpZ2h0c0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoBToCOAEiowMKGEZpZWxkV2VpZ2h0Q29uZmlndXJhdGlvbhIRCgl0eXBlX25hbWUYASABKAkSEgoKZmllbGRfbmFtZRgCIAEoCRITCgZ3ZWlnaHQYAyABKAVIAIgBARJZChBhcmd1bWVudF93ZWlnaHRzGAQgAygLMj8ud2cuY29zbW8ubm9kZS52MS5GaWVsZFdlaWdodENvbmZpZ3VyYXRpb24uQXJndW1lbnRXZWlnaHRzRW50cnkSbAoaZGlyZWN0aXZlX2FyZ3VtZW50X3dlaWdodHMYBSADKAsySC53Zy5jb3Ntby5ub2RlLnYxLkZpZWxkV2VpZ2h0Q29uZmlndXJhdGlvbi5EaXJlY3RpdmVBcmd1bWVudFdlaWdodHNFbnRyeRo2ChRBcmd1bWVudFdlaWdodHNFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAU6AjgBGj8KHURpcmVjdGl2ZUFyZ3VtZW50V2VpZ2h0c0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoBToCOAFCCQoHX3dlaWdodCLsAQoaRmllbGRMaXN0U2l6ZUNvbmZpZ3VyYXRpb24SEQoJdHlwZV9uYW1lGAEgASgJEhIKCmZpZWxkX25hbWUYAiABKAkSGQoMYXNzdW1lZF9zaXplGAMgASgFSACIAQESGQoRc2xpY2luZ19hcmd1bWVudHMYBCADKAkSFAoMc2l6ZWRfZmllbGRzGAUgAygJEikKHHJlcXVpcmVfb25lX3NsaWNpbmdfYXJndW1lbnQYBiABKAhIAYgBAUIPCg1fYXNzdW1lZF9zaXplQh8KHV9yZXF1aXJlX29uZV9zbGljaW5nX2FyZ3VtZW50IlwKFUFyZ3VtZW50Q29uZmlndXJhdGlvbhIMCgRuYW1lGAEgASgJEjUKC3NvdXJjZV90eXBlGAIgASgOMiAud2cuY29zbW8ubm9kZS52MS5Bcmd1bWVudFNvdXJjZSIlCgZTY29wZXMSGwoTcmVxdWlyZWRfYW5kX3Njb3BlcxgBIAMoCSKvAQoaQXV0aG9yaXphdGlvbkNvbmZpZ3VyYXRpb24SHwoXcmVxdWlyZXNfYXV0aGVudGljYXRpb24YASABKAgSNAoScmVxdWlyZWRfb3Jfc2NvcGVzGAIgAygLMhgud2cuY29zbW8ubm9kZS52MS5TY29wZXMSOgoYcmVxdWlyZWRfb3Jfc2NvcGVzX2J5X29yGAMgAygLMhgud2cuY29zbW8ubm9kZS52MS5TY29wZXMi1QIKEkZpZWxkQ29uZmlndXJhdGlvbhIRCgl0eXBlX25hbWUYASABKAkSEgoKZmllbGRfbmFtZRgCIAEoCRJIChdhcmd1bWVudHNfY29uZmlndXJhdGlvbhgDIAMoCzInLndnLmNvc21vLm5vZGUudjEuQXJndW1lbnRDb25maWd1cmF0aW9uElEKG2F1dGhvcml6YXRpb25fY29uZmlndXJhdGlvbhgEIAEoCzIsLndnLmNvc21vLm5vZGUudjEuQXV0aG9yaXphdGlvbkNvbmZpZ3VyYXRpb24SWQodc3Vic2NyaXB0aW9uX2ZpbHRlcl9jb25kaXRpb24YBSABKAsyLS53Zy5jb3Ntby5ub2RlLnYxLlN1YnNjcmlwdGlvbkZpbHRlckNvbmRpdGlvbkgAiAEBQiAKHl9zdWJzY3JpcHRpb25fZmlsdGVyX2NvbmRpdGlvbiI5ChFUeXBlQ29uZmlndXJhdGlvbhIRCgl0eXBlX25hbWUYASABKAkSEQoJcmVuYW1lX3RvGAIgASgJInwKCVR5cGVGaWVsZBIRCgl0eXBlX25hbWUYASABKAkSEwoLZmllbGRfbmFtZXMYAiADKAkSHAoUZXh0ZXJuYWxfZmllbGRfbmFtZXMYAyADKAkSKQohcmVxdWlyZV9mZXRjaF9yZWFzb25zX2ZpZWxkX25hbWVzGAQgAygJIjkKEEZpZWxkQ29vcmRpbmF0ZXMSEgoKZmllbGRfbmFtZRgBIAEoCRIRCgl0eXBlX25hbWUYAiABKAkiawoRRmllbGRTZXRDb25kaXRpb24SQgoWZmllbGRfY29vcmRpbmF0ZXNfcGF0aBgBIAMoCzIiLndnLmNvc21vLm5vZGUudjEuRmllbGRDb29yZGluYXRlcxISCgpmaWVsZF9wYXRoGAIgAygJIqcBCg1SZXF1aXJlZEZpZWxkEhEKCXR5cGVfbmFtZRgBIAEoCRISCgpmaWVsZF9uYW1lGAIgASgJEhUKDXNlbGVjdGlvbl9zZXQYAyABKAkSHwoXZGlzYWJsZV9lbnRpdHlfcmVzb2x2ZXIYBCABKAgSNwoKY29uZGl0aW9ucxgFIAMoCzIjLndnLmNvc21vLm5vZGUudjEuRmllbGRTZXRDb25kaXRpb24iWAocRW50aXR5SW50ZXJmYWNlQ29uZmlndXJhdGlvbhIbChNpbnRlcmZhY2VfdHlwZV9uYW1lGAEgASgJEhsKE2NvbmNyZXRlX3R5cGVfbmFtZXMYAiADKAkijQUKEkZldGNoQ29uZmlndXJhdGlvbhI0CgN1cmwYASABKAsyJy53Zy5jb3Ntby5ub2RlLnYxLkNvbmZpZ3VyYXRpb25WYXJpYWJsZRIsCgZtZXRob2QYAiABKA4yHC53Zy5jb3Ntby5ub2RlLnYxLkhUVFBNZXRob2QSQAoGaGVhZGVyGAMgAygLMjAud2cuY29zbW8ubm9kZS52MS5GZXRjaENvbmZpZ3VyYXRpb24uSGVhZGVyRW50cnkSNQoEYm9keRgEIAEoCzInLndnLmNvc21vLm5vZGUudjEuQ29uZmlndXJhdGlvblZhcmlhYmxlEjYKBXF1ZXJ5GAUgAygLMicud2cuY29zbW8ubm9kZS52MS5VUkxRdWVyeUNvbmZpZ3VyYXRpb24SFwoPdXJsX2VuY29kZV9ib2R5GAcgASgIEjEKBG10bHMYCCABKAsyIy53Zy5jb3Ntby5ub2RlLnYxLk1UTFNDb25maWd1cmF0aW9uEjkKCGJhc2VfdXJsGAkgASgLMicud2cuY29zbW8ubm9kZS52MS5Db25maWd1cmF0aW9uVmFyaWFibGUSNQoEcGF0aBgKIAEoCzInLndnLmNvc21vLm5vZGUudjEuQ29uZmlndXJhdGlvblZhcmlhYmxlEkQKDmh0dHBfcHJveHlfdXJsGAsgASgLMicud2cuY29zbW8ubm9kZS52MS5Db25maWd1cmF0aW9uVmFyaWFibGVIAIgBARpLCgtIZWFkZXJFbnRyeRILCgNrZXkYASABKAkSKwoFdmFsdWUYAiABKAsyHC53Zy5jb3Ntby5ub2RlLnYxLkhUVFBIZWFkZXI6AjgBQhEKD19odHRwX3Byb3h5X3VybCJlChVTdGF0dXNDb2RlVHlwZU1hcHBpbmcSEwoLc3RhdHVzX2NvZGUYASABKAMSEQoJdHlwZV9uYW1lGAIgASgJEiQKHGluamVjdF9zdGF0dXNfY29kZV9pbnRvX2JvZHkYAyABKAgikwMKGERhdGFTb3VyY2VDdXN0b21fR3JhcGhRTBIzCgVmZXRjaBgBIAEoCzIkLndnLmNvc21vLm5vZGUudjEuRmV0Y2hDb25maWd1cmF0aW9uEkgKDHN1YnNjcmlwdGlvbhgCIAEoCzIyLndnLmNvc21vLm5vZGUudjEuR3JhcGhRTFN1YnNjcmlwdGlvbkNvbmZpZ3VyYXRpb24SRAoKZmVkZXJhdGlvbhgDIAEoCzIwLndnLmNvc21vLm5vZGUudjEuR3JhcGhRTEZlZGVyYXRpb25Db25maWd1cmF0aW9uEjkKD3Vwc3RyZWFtX3NjaGVtYRgEIAEoCzIgLndnLmNvc21vLm5vZGUudjEuSW50ZXJuZWRTdHJpbmcSRAoZY3VzdG9tX3NjYWxhcl90eXBlX2ZpZWxkcxgGIAMoCzIhLndnLmNvc21vLm5vZGUudjEuU2luZ2xlVHlwZUZpZWxkEjEKBGdycGMYByABKAsyIy53Zy5jb3Ntby5ub2RlLnYxLkdSUENDb25maWd1cmF0aW9uIpABChFHUlBDQ29uZmlndXJhdGlvbhIuCgdtYXBwaW5nGAEgASgLMh0ud2cuY29zbW8ubm9kZS52MS5HUlBDTWFwcGluZxIUCgxwcm90b19zY2hlbWEYAiABKAkSNQoGcGx1Z2luGAMgASgLMiUud2cuY29zbW8ubm9kZS52MS5QbHVnaW5Db25maWd1cmF0aW9uIjcKDkltYWdlUmVmZXJlbmNlEhIKCnJlcG9zaXRvcnkYASABKAkSEQoJcmVmZXJlbmNlGAIgASgJIogBChNQbHVnaW5Db25maWd1cmF0aW9uEgwKBG5hbWUYASABKAkSDwoHdmVyc2lvbhgCIAEoCRI+Cg9pbWFnZV9yZWZlcmVuY2UYAyABKAsyIC53Zy5jb3Ntby5ub2RlLnYxLkltYWdlUmVmZXJlbmNlSACIAQFCEgoQX2ltYWdlX3JlZmVyZW5jZSIjChBTU0xDb25maWd1cmF0aW9uEg8KB2VuYWJsZWQYASABKAgi2wIKC0dSUENNYXBwaW5nEg8KB3ZlcnNpb24YASABKAUSDwoHc2VydmljZRgCIAEoCRI+ChJvcGVyYXRpb25fbWFwcGluZ3MYAyADKAsyIi53Zy5jb3Ntby5ub2RlLnYxLk9wZXJhdGlvbk1hcHBpbmcSOAoPZW50aXR5X21hcHBpbmdzGAQgAygLMh8ud2cuY29zbW8ubm9kZS52MS5FbnRpdHlNYXBwaW5nEj8KE3R5cGVfZmllbGRfbWFwcGluZ3MYBSADKAsyIi53Zy5jb3Ntby5ub2RlLnYxLlR5cGVGaWVsZE1hcHBpbmcSNAoNZW51bV9tYXBwaW5ncxgGIAMoCzIdLndnLmNvc21vLm5vZGUudjEuRW51bU1hcHBpbmcSOQoQcmVzb2x2ZV9tYXBwaW5ncxgHIAMoCzIfLndnLmNvc21vLm5vZGUudjEuTG9va3VwTWFwcGluZyKpAQoNTG9va3VwTWFwcGluZxIqCgR0eXBlGAEgASgOMhwud2cuY29zbW8ubm9kZS52MS5Mb29rdXBUeXBlEjwKDmxvb2t1cF9tYXBwaW5nGAIgASgLMiQud2cuY29zbW8ubm9kZS52MS5Mb29rdXBGaWVsZE1hcHBpbmcSCwoDcnBjGAMgASgJEg8KB3JlcXVlc3QYBCABKAkSEAoIcmVzcG9uc2UYBSABKAkiWQoSTG9va3VwRmllbGRNYXBwaW5nEgwKBHR5cGUYASABKAkSNQoNZmllbGRfbWFwcGluZxgCIAEoCzIeLndnLmNvc21vLm5vZGUudjEuRmllbGRNYXBwaW5nIoYBChBPcGVyYXRpb25NYXBwaW5nEi0KBHR5cGUYASABKA4yHy53Zy5jb3Ntby5ub2RlLnYxLk9wZXJhdGlvblR5cGUSEAoIb3JpZ2luYWwYAiABKAkSDgoGbWFwcGVkGAMgASgJEg8KB3JlcXVlc3QYBCABKAkSEAoIcmVzcG9uc2UYBSABKAkitgEKDUVudGl0eU1hcHBpbmcSEQoJdHlwZV9uYW1lGAEgASgJEgwKBGtpbmQYAiABKAkSCwoDa2V5GAMgASgJEgsKA3JwYxgEIAEoCRIPCgdyZXF1ZXN0GAUgASgJEhAKCHJlc3BvbnNlGAYgASgJEkcKF3JlcXVpcmVkX2ZpZWxkX21hcHBpbmdzGAcgAygLMiYud2cuY29zbW8ubm9kZS52MS5SZXF1aXJlZEZpZWxkTWFwcGluZyJ9ChRSZXF1aXJlZEZpZWxkTWFwcGluZxI1Cg1maWVsZF9tYXBwaW5nGAEgASgLMh4ud2cuY29zbW8ubm9kZS52MS5GaWVsZE1hcHBpbmcSCwoDcnBjGAIgASgJEg8KB3JlcXVlc3QYAyABKAkSEAoIcmVzcG9uc2UYBCABKAkiWAoQVHlwZUZpZWxkTWFwcGluZxIMCgR0eXBlGAEgASgJEjYKDmZpZWxkX21hcHBpbmdzGAIgAygLMh4ud2cuY29zbW8ubm9kZS52MS5GaWVsZE1hcHBpbmcibgoMRmllbGRNYXBwaW5nEhAKCG9yaWdpbmFsGAEgASgJEg4KBm1hcHBlZBgCIAEoCRI8ChFhcmd1bWVudF9tYXBwaW5ncxgDIAMoCzIhLndnLmNvc21vLm5vZGUudjEuQXJndW1lbnRNYXBwaW5nIjMKD0FyZ3VtZW50TWFwcGluZxIQCghvcmlnaW5hbBgBIAEoCRIOCgZtYXBwZWQYAiABKAkiTwoLRW51bU1hcHBpbmcSDAoEdHlwZRgBIAEoCRIyCgZ2YWx1ZXMYAiADKAsyIi53Zy5jb3Ntby5ub2RlLnYxLkVudW1WYWx1ZU1hcHBpbmciNAoQRW51bVZhbHVlTWFwcGluZxIQCghvcmlnaW5hbBgBIAEoCRIOCgZtYXBwZWQYAiABKAkiagoXTmF0c1N0cmVhbUNvbmZpZ3VyYXRpb24SFQoNY29uc3VtZXJfbmFtZRgBIAEoCRITCgtzdHJlYW1fbmFtZRgCIAEoCRIjChtjb25zdW1lcl9pbmFjdGl2ZV90aHJlc2hvbGQYAyABKAUiwwEKFk5hdHNFdmVudENvbmZpZ3VyYXRpb24STgoaZW5naW5lX2V2ZW50X2NvbmZpZ3VyYXRpb24YASABKAsyKi53Zy5jb3Ntby5ub2RlLnYxLkVuZ2luZUV2ZW50Q29uZmlndXJhdGlvbhIQCghzdWJqZWN0cxgCIAMoCRJHChRzdHJlYW1fY29uZmlndXJhdGlvbhgDIAEoCzIpLndnLmNvc21vLm5vZGUudjEuTmF0c1N0cmVhbUNvbmZpZ3VyYXRpb24ieQoXS2Fma2FFdmVudENvbmZpZ3VyYXRpb24STgoaZW5naW5lX2V2ZW50X2NvbmZpZ3VyYXRpb24YASABKAsyKi53Zy5jb3Ntby5ub2RlLnYxLkVuZ2luZUV2ZW50Q29uZmlndXJhdGlvbhIOCgZ0b3BpY3MYAiADKAkiewoXUmVkaXNFdmVudENvbmZpZ3VyYXRpb24STgoaZW5naW5lX2V2ZW50X2NvbmZpZ3VyYXRpb24YASABKAsyKi53Zy5jb3Ntby5ub2RlLnYxLkVuZ2luZUV2ZW50Q29uZmlndXJhdGlvbhIQCghjaGFubmVscxgCIAMoCSKBAQoYRW5naW5lRXZlbnRDb25maWd1cmF0aW9uEhMKC3Byb3ZpZGVyX2lkGAEgASgJEikKBHR5cGUYAiABKA4yGy53Zy5jb3Ntby5ub2RlLnYxLkV2ZW50VHlwZRIRCgl0eXBlX25hbWUYAyABKAkSEgoKZmllbGRfbmFtZRgEIAEoCSLEAQoWRGF0YVNvdXJjZUN1c3RvbUV2ZW50cxI2CgRuYXRzGAEgAygLMigud2cuY29zbW8ubm9kZS52MS5OYXRzRXZlbnRDb25maWd1cmF0aW9uEjgKBWthZmthGAIgAygLMikud2cuY29zbW8ubm9kZS52MS5LYWZrYUV2ZW50Q29uZmlndXJhdGlvbhI4CgVyZWRpcxgDIAMoCzIpLndnLmNvc21vLm5vZGUudjEuUmVkaXNFdmVudENvbmZpZ3VyYXRpb24iUAoXRGF0YVNvdXJjZUN1c3RvbV9TdGF0aWMSNQoEZGF0YRgBIAEoCzInLndnLmNvc21vLm5vZGUudjEuQ29uZmlndXJhdGlvblZhcmlhYmxlIuUBChVDb25maWd1cmF0aW9uVmFyaWFibGUSOQoEa2luZBgBIAEoDjIrLndnLmNvc21vLm5vZGUudjEuQ29uZmlndXJhdGlvblZhcmlhYmxlS2luZBIfChdzdGF0aWNfdmFyaWFibGVfY29udGVudBgCIAEoCRIhChllbnZpcm9ubWVudF92YXJpYWJsZV9uYW1lGAMgASgJEioKImVudmlyb25tZW50X3ZhcmlhYmxlX2RlZmF1bHRfdmFsdWUYBCABKAkSIQoZcGxhY2Vob2xkZXJfdmFyaWFibGVfbmFtZRgFIAEoCSJDChZEaXJlY3RpdmVDb25maWd1cmF0aW9uEhYKDmRpcmVjdGl2ZV9uYW1lGAEgASgJEhEKCXJlbmFtZV90bxgCIAEoCSI0ChVVUkxRdWVyeUNvbmZpZ3VyYXRpb24SDAoEbmFtZRgBIAEoCRINCgV2YWx1ZRgCIAEoCSJFCgpIVFRQSGVhZGVyEjcKBnZhbHVlcxgBIAMoCzInLndnLmNvc21vLm5vZGUudjEuQ29uZmlndXJhdGlvblZhcmlhYmxlIpwBChFNVExTQ29uZmlndXJhdGlvbhI0CgNrZXkYASABKAsyJy53Zy5jb3Ntby5ub2RlLnYxLkNvbmZpZ3VyYXRpb25WYXJpYWJsZRI1CgRjZXJ0GAIgASgLMicud2cuY29zbW8ubm9kZS52MS5Db25maWd1cmF0aW9uVmFyaWFibGUSGgoSaW5zZWN1cmVTa2lwVmVyaWZ5GAMgASgIIsUCCiBHcmFwaFFMU3Vic2NyaXB0aW9uQ29uZmlndXJhdGlvbhIPCgdlbmFibGVkGAEgASgIEjQKA3VybBgCIAEoCzInLndnLmNvc21vLm5vZGUudjEuQ29uZmlndXJhdGlvblZhcmlhYmxlEhMKBnVzZVNTRRgDIAEoCEgAiAEBEkMKCHByb3RvY29sGAQgASgOMiwud2cuY29zbW8uY29tbW9uLkdyYXBoUUxTdWJzY3JpcHRpb25Qcm90b2NvbEgBiAEBEk8KFHdlYnNvY2tldFN1YnByb3RvY29sGAUgASgOMiwud2cuY29zbW8uY29tbW9uLkdyYXBoUUxXZWJzb2NrZXRTdWJwcm90b2NvbEgCiAEBQgkKB191c2VTU0VCCwoJX3Byb3RvY29sQhcKFV93ZWJzb2NrZXRTdWJwcm90b2NvbCJFCh5HcmFwaFFMRmVkZXJhdGlvbkNvbmZpZ3VyYXRpb24SDwoHZW5hYmxlZBgBIAEoCBISCgpzZXJ2aWNlU2RsGAIgASgJIh0KDkludGVybmVkU3RyaW5nEgsKA2tleRgBIAEoCSI4Cg9TaW5nbGVUeXBlRmllbGQSEQoJdHlwZV9uYW1lGAEgASgJEhIKCmZpZWxkX25hbWUYAiABKAkiPgoaU3Vic2NyaXB0aW9uRmllbGRDb25kaXRpb24SEgoKZmllbGRfcGF0aBgBIAMoCRIMCgRqc29uGAIgASgJIqMCChtTdWJzY3JpcHRpb25GaWx0ZXJDb25kaXRpb24SOgoDYW5kGAEgAygLMi0ud2cuY29zbW8ubm9kZS52MS5TdWJzY3JpcHRpb25GaWx0ZXJDb25kaXRpb24SPQoCaW4YAiABKAsyLC53Zy5jb3Ntby5ub2RlLnYxLlN1YnNjcmlwdGlvbkZpZWxkQ29uZGl0aW9uSACIAQESPwoDbm90GAMgASgLMi0ud2cuY29zbW8ubm9kZS52MS5TdWJzY3JpcHRpb25GaWx0ZXJDb25kaXRpb25IAYgBARI5CgJvchgEIAMoCzItLndnLmNvc21vLm5vZGUudjEuU3Vic2NyaXB0aW9uRmlsdGVyQ29uZGl0aW9uQgUKA19pbkIGCgRfbm90IkgKFUNhY2hlV2FybWVyT3BlcmF0aW9ucxIvCgpvcGVyYXRpb25zGAEgAygLMhsud2cuY29zbW8ubm9kZS52MS5PcGVyYXRpb24ibgoJT3BlcmF0aW9uEjMKB3JlcXVlc3QYASABKAsyIi53Zy5jb3Ntby5ub2RlLnYxLk9wZXJhdGlvblJlcXVlc3QSLAoGY2xpZW50GAIgASgLMhwud2cuY29zbW8ubm9kZS52MS5DbGllbnRJbmZvImoKEE9wZXJhdGlvblJlcXVlc3QSFgoOb3BlcmF0aW9uX25hbWUYASABKAkSDQoFcXVlcnkYAiABKAkSLwoKZXh0ZW5zaW9ucxgDIAEoCzIbLndnLmNvc21vLm5vZGUudjEuRXh0ZW5zaW9uIkYKCUV4dGVuc2lvbhI5Cg9wZXJzaXN0ZWRfcXVlcnkYASABKAsyIC53Zy5jb3Ntby5ub2RlLnYxLlBlcnNpc3RlZFF1ZXJ5IjYKDlBlcnNpc3RlZFF1ZXJ5EhMKC3NoYTI1Nl9oYXNoGAEgASgJEg8KB3ZlcnNpb24YAiABKAUiKwoKQ2xpZW50SW5mbxIMCgRuYW1lGAEgASgJEg8KB3ZlcnNpb24YAiABKAkqQwoWU2F0aXNmaWVkT3BlcmF0aW9uVHlwZRIJCgVRVUVSWRAAEgwKCE1VVEFUSU9OEAESEAoMU1VCU0NSSVBUSU9OEAIqggEKG0FyZ3VtZW50UmVuZGVyQ29uZmlndXJhdGlvbhIbChdSRU5ERVJfQVJHVU1FTlRfREVGQVVMVBAAEiQKIFJFTkRFUl9BUkdVTUVOVF9BU19HUkFQSFFMX1ZBTFVFEAESIAocUkVOREVSX0FSR1VNRU5UX0FTX0FSUkFZX0NTVhACKjYKDkFyZ3VtZW50U291cmNlEhAKDE9CSkVDVF9GSUVMRBAAEhIKDkZJRUxEX0FSR1VNRU5UEAEqNQoORGF0YVNvdXJjZUtpbmQSCgoGU1RBVElDEAASCwoHR1JBUEhRTBABEgoKBlBVQlNVQhACKlwKCkxvb2t1cFR5cGUSGwoXTE9PS1VQX1RZUEVfVU5TUEVDSUZJRUQQABIXChNMT09LVVBfVFlQRV9SRVNPTFZFEAESGAoUTE9PS1VQX1RZUEVfUkVRVUlSRVMQAiqHAQoNT3BlcmF0aW9uVHlwZRIeChpPUEVSQVRJT05fVFlQRV9VTlNQRUNJRklFRBAAEhgKFE9QRVJBVElPTl9UWVBFX1FVRVJZEAESGwoXT1BFUkFUSU9OX1RZUEVfTVVUQVRJT04QAhIfChtPUEVSQVRJT05fVFlQRV9TVUJTQ1JJUFRJT04QAyo0CglFdmVudFR5cGUSCwoHUFVCTElTSBAAEgsKB1JFUVVFU1QQARINCglTVUJTQ1JJQkUQAiqGAQoZQ29uZmlndXJhdGlvblZhcmlhYmxlS2luZBIhCh1TVEFUSUNfQ09ORklHVVJBVElPTl9WQVJJQUJMRRAAEh4KGkVOVl9DT05GSUdVUkFUSU9OX1ZBUklBQkxFEAESJgoiUExBQ0VIT0xERVJfQ09ORklHVVJBVElPTl9WQVJJQUJMRRACKkEKCkhUVFBNZXRob2QSBwoDR0VUEAASCAoEUE9TVBABEgcKA1BVVBACEgoKBkRFTEVURRADEgsKB09QVElPTlMQBDLSAQoLTm9kZVNlcnZpY2USXwoMU2VsZlJlZ2lzdGVyEiUud2cuY29zbW8ubm9kZS52MS5TZWxmUmVnaXN0ZXJSZXF1ZXN0GiYud2cuY29zbW8ubm9kZS52MS5TZWxmUmVnaXN0ZXJSZXNwb25zZSIAEmIKDUdlbmVyYXRlUXVlcnkSJi53Zy5jb3Ntby5ub2RlLnYxLkdlbmVyYXRlUXVlcnlSZXF1ZXN0Gicud2cuY29zbW8ubm9kZS52MS5HZW5lcmF0ZVF1ZXJ5UmVzcG9uc2UiAGIGcHJvdG8z", [file_wg_cosmo_common_common]); /** * @generated from message wg.cosmo.node.v1.Subgraph @@ -252,9 +252,9 @@ export const SelfRegisterResponseSchema: GenMessage = /*@_ */ export type GenerateQueryRequest = Message<"wg.cosmo.node.v1.GenerateQueryRequest"> & { /** - * @generated from field: string schema_hash = 1; + * @generated from field: string schema_version_id = 1; */ - schemaHash: string; + schemaVersionId: string; /** * @generated from field: string prompt = 2; @@ -366,13 +366,6 @@ export type EngineConfiguration = Message<"wg.cosmo.node.v1.EngineConfiguration" * @generated from field: optional string graphql_client_schema = 7; */ graphqlClientSchema?: string | undefined; - - /** - * Content hash of graphqlSchema, formatted as "sha256:". - * - * @generated from field: string schema_hash = 8; - */ - schemaHash: string; }; /** diff --git a/controlplane/src/core/bufservices/NodeService.ts b/controlplane/src/core/bufservices/NodeService.ts index 8de53ee000..8d570875e0 100644 --- a/controlplane/src/core/bufservices/NodeService.ts +++ b/controlplane/src/core/bufservices/NodeService.ts @@ -2,6 +2,7 @@ import { ServiceImpl } from '@connectrpc/connect'; import { EnumStatusCode } from '@wundergraph/cosmo-connect/dist/common/common_pb'; import { NodeService, RegistrationInfo } from '@wundergraph/cosmo-connect/dist/node/v1/node_pb'; import { lru } from 'tiny-lru'; +import { validate as validateUUID } from 'uuid'; import { FederatedGraphRepository } from '../repositories/FederatedGraphRepository.js'; import { OrganizationRepository } from '../repositories/OrganizationRepository.js'; import type { PlainMessage } from '../../types/index.js'; @@ -74,6 +75,39 @@ export default function (opts: RouterOptions): Partial value.trim().length > 0), prompt: z.string().trim().min(1), }); +const ensureIndexResponseSchema = z.object({ + indexId: z.string().trim().min(1), +}); + const ptqQuerySchema = z.object({ description: z.string().optional(), document: z.string().min(1), @@ -39,10 +45,6 @@ const ptqResponseSchema = z.object({ type PtQResponse = z.infer; -export function getSchemaHash(schema: string): string { - return `sha256:${createHash('sha256').update(schema).digest('hex')}`; -} - @traced export class PromptToQueryService { readonly #httpClient: AxiosInstance; @@ -70,7 +72,7 @@ export class PromptToQueryService { }); } - async generateQuery(schemaSha: string, prompt: string): Promise { + async generateQuery(schemaSDL: string, prompt: string): Promise { if (!this.serviceAddress) { // The feature doesn't seem to be configured correctly return create(GenerateQueryResponseSchema, { @@ -82,7 +84,7 @@ export class PromptToQueryService { } // Ensure that the provided parameters are valid - const parsed = validationSchema.safeParse({ schemaSha, prompt }); + const parsed = validationSchema.safeParse({ schemaSDL, prompt }); if (!parsed.success) { return create(GenerateQueryResponseSchema, { response: { @@ -105,11 +107,12 @@ export class PromptToQueryService { // Invoke the `prompt to query` service try { + const indexId = await this.ensureIndex(parsed.data.schemaSDL); const response = await this.#httpClient('/yoko.v1.YokoService/GenerateQuery', { method: 'POST', headers: { 'Content-Type': 'application/json' }, data: JSON.stringify({ - indexId: parsed.data.schemaSha, + indexId, prompt: parsed.data.prompt, }), }); @@ -150,11 +153,21 @@ export class PromptToQueryService { } // Fire and forget the schema indexation - this.#httpClient('/yoko.v1.YokoService/EnsureIndex', { + this.ensureIndex(schema).catch((e) => this.logger.error(e, 'Failed to index schema due an unexpected error')); + } + + private async ensureIndex(schemaSDL: string): Promise { + const response = await this.#httpClient('/yoko.v1.YokoService/EnsureIndex', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - data: JSON.stringify({ sdl: schema }), - }).catch((e) => this.logger.error(e, 'Failed to index schema due an unexpected error')); + data: JSON.stringify({ sdl: schemaSDL }), + }); + const parsed = ensureIndexResponseSchema.safeParse(response.data); + if (!parsed.success) { + throw new Error('It was not possible to parse the response returned by the Prompt to Query index service'); + } + + return parsed.data.indexId; } private static getOperationType(type: z.infer['operationType']): SatisfiedOperationType { diff --git a/controlplane/test/prompt-to-query-service.test.ts b/controlplane/test/prompt-to-query-service.test.ts new file mode 100644 index 0000000000..225282dbf8 --- /dev/null +++ b/controlplane/test/prompt-to-query-service.test.ts @@ -0,0 +1,72 @@ +import { PostgresJsDatabase } from 'drizzle-orm/postgres-js'; +import { pino } from 'pino'; +import { http, HttpResponse } from 'msw'; +import { setupServer } from 'msw/node'; +import { afterAll, afterEach, beforeAll, describe, expect, test, vi } from 'vitest'; +import { EnumStatusCode } from '@wundergraph/cosmo-connect/dist/common/common_pb'; +import { OrganizationRepository } from '../src/core/repositories/OrganizationRepository.js'; +import { PromptToQueryService } from '../src/core/services/PromptToQueryService.js'; +import * as schema from '../src/db/schema.js'; + +describe('PromptToQueryService', () => { + const yokoURL = 'http://yoko.test'; + const mockServer = setupServer(); + + beforeAll(() => mockServer.listen({ onUnhandledRequest: 'error' })); + afterEach(() => { + mockServer.resetHandlers(); + vi.restoreAllMocks(); + }); + afterAll(() => mockServer.close()); + + test('uses the index ID returned by EnsureIndex to generate the query', async () => { + const schemaSDL = 'type Query { employees: [String!]! }'; + let ensureIndexRequest: unknown; + let generateQueryRequest: unknown; + + mockServer.use( + http.post(`${yokoURL}/yoko.v1.YokoService/EnsureIndex`, async ({ request }) => { + ensureIndexRequest = await request.json(); + return HttpResponse.json({ indexId: 'opaque-yoko-index-id' }); + }), + http.post(`${yokoURL}/yoko.v1.YokoService/GenerateQuery`, async ({ request }) => { + generateQueryRequest = await request.json(); + return HttpResponse.json({ + resolution: { + queries: [ + { + description: 'Lists employees', + document: 'query ListEmployees { employees }', + operationName: 'ListEmployees', + operationType: 'query', + variablesSchema: '{}', + }, + ], + unsatisfied: [], + }, + }); + }), + ); + vi.spyOn(OrganizationRepository.prototype, 'getFeature').mockResolvedValue({ + id: 'prompt-to-query', + enabled: true, + }); + + const service = new PromptToQueryService( + {} as PostgresJsDatabase, + pino(), + yokoURL, + 'organization-id', + undefined, + ); + const response = await service.generateQuery(schemaSDL, 'List all employees'); + + expect(ensureIndexRequest).toEqual({ sdl: schemaSDL }); + expect(generateQueryRequest).toEqual({ + indexId: 'opaque-yoko-index-id', + prompt: 'List all employees', + }); + expect(response.response?.code).toBe(EnumStatusCode.OK); + expect(response.query?.operationName).toBe('ListEmployees'); + }); +}); diff --git a/controlplane/test/prompt-to-query.test.ts b/controlplane/test/prompt-to-query.test.ts new file mode 100644 index 0000000000..630a94ea9b --- /dev/null +++ b/controlplane/test/prompt-to-query.test.ts @@ -0,0 +1,101 @@ +import { http, HttpResponse } from 'msw'; +import { setupServer } from 'msw/node'; +import { afterAll, afterEach, beforeAll, describe, expect, test } from 'vitest'; +import { EnumStatusCode } from '@wundergraph/cosmo-connect/dist/common/common_pb'; +import { joinLabel, routerConfigFromJsonString } from '@wundergraph/cosmo-shared'; +import { afterAllSetup, beforeAllSetup, genID, genUniqueLabel } from '../src/core/test-util.js'; +import { + createFederatedGraph, + createThenPublishSubgraph, + DEFAULT_NAMESPACE, + DEFAULT_ROUTER_URL, + DEFAULT_SUBGRAPH_URL_ONE, + SetupTest, +} from './test-util.js'; + +let dbname = ''; + +describe('Prompt to Query RPC', () => { + const yokoURL = 'http://yoko.test'; + const mockServer = setupServer(); + + beforeAll(async () => { + mockServer.listen({ onUnhandledRequest: 'bypass' }); + dbname = await beforeAllSetup(); + }); + afterEach(() => mockServer.resetHandlers()); + afterAll(async () => { + mockServer.close(); + await afterAllSetup(dbname); + }); + + test("resolves the router schema version and generates a query with Yoko's index ID", async (testContext) => { + const ensureIndexRequests: unknown[] = []; + let generateQueryRequest: unknown; + mockServer.use( + http.post(`${yokoURL}/yoko.v1.YokoService/EnsureIndex`, async ({ request }) => { + ensureIndexRequests.push(await request.json()); + return HttpResponse.json({ indexId: 'opaque-yoko-index-id' }); + }), + http.post(`${yokoURL}/yoko.v1.YokoService/GenerateQuery`, async ({ request }) => { + generateQueryRequest = await request.json(); + return HttpResponse.json({ + resolution: { + queries: [ + { + description: 'Returns hello', + document: 'query GetHello { hello }', + operationName: 'GetHello', + operationType: 'query', + variablesSchema: '{}', + }, + ], + unsatisfied: [], + }, + }); + }), + ); + + const { client, nodeClient, server, users, blobStorage } = await SetupTest({ + dbname, + enabledFeatures: ['prompt-to-query'], + promptToQueryServiceAddress: yokoURL, + }); + testContext.onTestFinished(() => server.close()); + + const subgraphName = genID('subgraph'); + const graphName = genID('graph'); + const label = genUniqueLabel(); + await createThenPublishSubgraph( + client, + subgraphName, + DEFAULT_NAMESPACE, + 'type Query { hello: String! }', + [label], + DEFAULT_SUBGRAPH_URL_ONE, + ); + await createFederatedGraph(client, graphName, DEFAULT_NAMESPACE, [joinLabel(label)], DEFAULT_ROUTER_URL); + + const graphResponse = await client.getFederatedGraphByName({ name: graphName, namespace: DEFAULT_NAMESPACE }); + expect(graphResponse.response?.code).toBe(EnumStatusCode.OK); + const tokenResponse = await client.generateRouterToken({ fedGraphName: graphName, namespace: DEFAULT_NAMESPACE }); + expect(tokenResponse.response?.code).toBe(EnumStatusCode.OK); + + const configBlob = await blobStorage.getObject({ + key: `${users.adminAliceCompanyA.organizationId}/${graphResponse.graph?.id}/routerconfigs/latest.json`, + }); + const config = routerConfigFromJsonString(await new Response(configBlob.stream).text()); + const response = await nodeClient.generateQuery( + { + schemaVersionId: config?.version, + prompt: 'Return hello', + }, + { headers: { Authorization: `Bearer ${tokenResponse.token}` } }, + ); + + expect(response.response?.code).toBe(EnumStatusCode.OK); + expect(response.query?.operationName).toBe('GetHello'); + expect(ensureIndexRequests.at(-1)).toEqual({ sdl: config?.engineConfig?.graphqlSchema }); + expect(generateQueryRequest).toEqual({ indexId: 'opaque-yoko-index-id', prompt: 'Return hello' }); + }); +}); diff --git a/controlplane/test/router-config.test.ts b/controlplane/test/router-config.test.ts index 4332742586..190a59fb3b 100644 --- a/controlplane/test/router-config.test.ts +++ b/controlplane/test/router-config.test.ts @@ -1,4 +1,3 @@ -import { createHash } from 'node:crypto'; import { noBaseDefinitionForExtensionError, noQueryRootTypeError, OBJECT } from '@wundergraph/composition'; import { EnumStatusCode } from '@wundergraph/cosmo-connect/dist/common/common_pb'; import { joinLabel, routerConfigFromJsonString } from '@wundergraph/cosmo-shared'; @@ -218,11 +217,6 @@ describe('Router Config', (ctx) => { const config = routerConfigFromJsonString(configJsonString); expect(config).toBeDefined(); - expect(config?.engineConfig?.schemaHash).toBe( - `sha256:${createHash('sha256') - .update(config?.engineConfig?.graphqlSchema || '') - .digest('hex')}`, - ); }); test('Should not return routerConfig if an invalid schema version is available', async (testContext) => { diff --git a/controlplane/test/test-util.ts b/controlplane/test/test-util.ts index 41ce9e0d00..fddcecc8ac 100644 --- a/controlplane/test/test-util.ts +++ b/controlplane/test/test-util.ts @@ -86,6 +86,7 @@ export const SetupTest = async function ({ createScimKey, setupBilling, organizationId, + promptToQueryServiceAddress, }: { dbname: string; chClient?: ClickHouseClient; @@ -96,6 +97,7 @@ export const SetupTest = async function ({ plan: 'developer@1' | 'launch@1' | 'scale@1' | 'enterprise'; }; organizationId?: UUID; + promptToQueryServiceAddress?: string; }) { const log = pino(); const databaseConnectionUrl = `postgresql://postgres:changeme@localhost:5432/${dbname}`; @@ -173,6 +175,7 @@ export const SetupTest = async function ({ clientSecret: 'test', }, cdnBaseUrl: 'http://localhost:11000', + promptToQueryServiceAddress, admissionWebhookJWTSecret: 'secret', keycloakApiUrl: apiUrl, blobStorage, diff --git a/mcp-yoko-run.sh b/mcp-yoko-run.sh index 13e7fce1cc..b22414807a 100755 --- a/mcp-yoko-run.sh +++ b/mcp-yoko-run.sh @@ -333,7 +333,7 @@ def validate_resolution(value): def generate_query(index_id, prompt): - digest = index_id.removeprefix("sha256:") + digest = index_id.removeprefix("yoko:") schema_file = index_dir / f"{digest}.graphql" if not re.fullmatch(r"[0-9a-fA-F]{64}", digest) or not schema_file.is_file(): return resolution_error(f"No schema is indexed for {index_id}") @@ -452,7 +452,7 @@ class Handler(BaseHTTPRequestHandler): if not isinstance(sdl, str) or not sdl.strip(): raise ValueError("sdl must be a non-empty string") digest = hashlib.sha256(sdl.encode("utf-8")).hexdigest() - index_id = f"sha256:{digest}" + index_id = f"yoko:{digest}" (index_dir / f"{digest}.graphql").write_text(sdl, encoding="utf-8") print(f"Indexed {index_id} ({len(sdl)} bytes)", flush=True) self.send_json(200, {"indexId": index_id}) diff --git a/proto/wg/cosmo/node/v1/node.proto b/proto/wg/cosmo/node/v1/node.proto index 0a74927c5f..a3132951f3 100644 --- a/proto/wg/cosmo/node/v1/node.proto +++ b/proto/wg/cosmo/node/v1/node.proto @@ -61,7 +61,7 @@ message SelfRegisterResponse { } message GenerateQueryRequest { - string schema_hash = 1; + string schema_version_id = 1; string prompt = 2; } @@ -97,8 +97,6 @@ message EngineConfiguration { repeated TypeConfiguration type_configurations = 5; map string_storage = 6; optional string graphql_client_schema = 7; - // Content hash of graphqlSchema, formatted as "sha256:". - string schema_hash = 8; } message DataSourceConfiguration { diff --git a/router/core/graph_server.go b/router/core/graph_server.go index 79e1701181..9b779be06e 100644 --- a/router/core/graph_server.go +++ b/router/core/graph_server.go @@ -1611,7 +1611,7 @@ func (s *graphServer) buildGraphMux( // We support the MCP only on the base graph. Feature flags are not supported yet. if opts.IsBaseGraph() && s.mcpServer != nil { - if mErr := s.mcpServer.Reload(executor.ClientSchema, opts.EngineConfig.FieldConfigurations, opts.EngineConfig.GetSchemaHash()); mErr != nil { + if mErr := s.mcpServer.Reload(executor.ClientSchema, opts.EngineConfig.FieldConfigurations, opts.RouterConfigVersion); mErr != nil { return nil, fmt.Errorf("failed to reload MCP server: %w", mErr) } } diff --git a/router/gen/proto/wg/cosmo/node/v1/node.pb.go b/router/gen/proto/wg/cosmo/node/v1/node.pb.go index 896f00a981..3f961c179e 100644 --- a/router/gen/proto/wg/cosmo/node/v1/node.pb.go +++ b/router/gen/proto/wg/cosmo/node/v1/node.pb.go @@ -1010,11 +1010,11 @@ func (x *SelfRegisterResponse) GetRegistrationInfo() *RegistrationInfo { } type GenerateQueryRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - SchemaHash string `protobuf:"bytes,1,opt,name=schema_hash,json=schemaHash,proto3" json:"schema_hash,omitempty"` - Prompt string `protobuf:"bytes,2,opt,name=prompt,proto3" json:"prompt,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + SchemaVersionId string `protobuf:"bytes,1,opt,name=schema_version_id,json=schemaVersionId,proto3" json:"schema_version_id,omitempty"` + Prompt string `protobuf:"bytes,2,opt,name=prompt,proto3" json:"prompt,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *GenerateQueryRequest) Reset() { @@ -1047,9 +1047,9 @@ func (*GenerateQueryRequest) Descriptor() ([]byte, []int) { return file_wg_cosmo_node_v1_node_proto_rawDescGZIP(), []int{10} } -func (x *GenerateQueryRequest) GetSchemaHash() string { +func (x *GenerateQueryRequest) GetSchemaVersionId() string { if x != nil { - return x.SchemaHash + return x.SchemaVersionId } return "" } @@ -1198,10 +1198,8 @@ type EngineConfiguration struct { TypeConfigurations []*TypeConfiguration `protobuf:"bytes,5,rep,name=type_configurations,json=typeConfigurations,proto3" json:"type_configurations,omitempty"` StringStorage map[string]string `protobuf:"bytes,6,rep,name=string_storage,json=stringStorage,proto3" json:"string_storage,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` GraphqlClientSchema *string `protobuf:"bytes,7,opt,name=graphql_client_schema,json=graphqlClientSchema,proto3,oneof" json:"graphql_client_schema,omitempty"` - // Content hash of graphqlSchema, formatted as "sha256:". - SchemaHash string `protobuf:"bytes,8,opt,name=schema_hash,json=schemaHash,proto3" json:"schema_hash,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *EngineConfiguration) Reset() { @@ -1283,13 +1281,6 @@ func (x *EngineConfiguration) GetGraphqlClientSchema() string { return "" } -func (x *EngineConfiguration) GetSchemaHash() string { - if x != nil { - return x.SchemaHash - } - return "" -} - type DataSourceConfiguration struct { state protoimpl.MessageState `protogen:"open.v1"` Kind DataSourceKind `protobuf:"varint,1,opt,name=kind,proto3,enum=wg.cosmo.node.v1.DataSourceKind" json:"kind,omitempty"` @@ -5192,10 +5183,9 @@ const file_wg_cosmo_node_v1_node_proto_rawDesc = "" + "\x14SelfRegisterResponse\x126\n" + "\bresponse\x18\x01 \x01(\v2\x1a.wg.cosmo.node.v1.ResponseR\bresponse\x12S\n" + "\x10registrationInfo\x18\x02 \x01(\v2\".wg.cosmo.node.v1.RegistrationInfoH\x00R\x10registrationInfo\x88\x01\x01B\x13\n" + - "\x11_registrationInfo\"O\n" + - "\x14GenerateQueryRequest\x12\x1f\n" + - "\vschema_hash\x18\x01 \x01(\tR\n" + - "schemaHash\x12\x16\n" + + "\x11_registrationInfo\"Z\n" + + "\x14GenerateQueryRequest\x12*\n" + + "\x11schema_version_id\x18\x01 \x01(\tR\x0fschemaVersionId\x12\x16\n" + "\x06prompt\x18\x02 \x01(\tR\x06prompt\"\x96\x01\n" + "\x15GenerateQueryResponse\x126\n" + "\bresponse\x18\x01 \x01(\v2\x1a.wg.cosmo.node.v1.ResponseR\bresponse\x12;\n" + @@ -5206,7 +5196,7 @@ const file_wg_cosmo_node_v1_node_proto_rawDesc = "" + "\bdocument\x18\x02 \x01(\tR\bdocument\x12%\n" + "\x0eoperation_name\x18\x03 \x01(\tR\roperationName\x12O\n" + "\x0eoperation_type\x18\x04 \x01(\x0e2(.wg.cosmo.node.v1.SatisfiedOperationTypeR\roperationType\x12)\n" + - "\x10variables_schema\x18\x05 \x01(\tR\x0fvariablesSchema\"\x9d\x05\n" + + "\x10variables_schema\x18\x05 \x01(\tR\x0fvariablesSchema\"\xfc\x04\n" + "\x13EngineConfiguration\x122\n" + "\x14defaultFlushInterval\x18\x01 \x01(\x03R\x14defaultFlushInterval\x12f\n" + "\x19datasource_configurations\x18\x02 \x03(\v2).wg.cosmo.node.v1.DataSourceConfigurationR\x18datasourceConfigurations\x12W\n" + @@ -5214,9 +5204,7 @@ const file_wg_cosmo_node_v1_node_proto_rawDesc = "" + "\rgraphqlSchema\x18\x04 \x01(\tR\rgraphqlSchema\x12T\n" + "\x13type_configurations\x18\x05 \x03(\v2#.wg.cosmo.node.v1.TypeConfigurationR\x12typeConfigurations\x12_\n" + "\x0estring_storage\x18\x06 \x03(\v28.wg.cosmo.node.v1.EngineConfiguration.StringStorageEntryR\rstringStorage\x127\n" + - "\x15graphql_client_schema\x18\a \x01(\tH\x00R\x13graphqlClientSchema\x88\x01\x01\x12\x1f\n" + - "\vschema_hash\x18\b \x01(\tR\n" + - "schemaHash\x1a@\n" + + "\x15graphql_client_schema\x18\a \x01(\tH\x00R\x13graphqlClientSchema\x88\x01\x01\x1a@\n" + "\x12StringStorageEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01B\x18\n" + diff --git a/router/pkg/controlplane/prompttoquery/client.go b/router/pkg/controlplane/prompttoquery/client.go index 23c7e6346c..c5de3675bd 100644 --- a/router/pkg/controlplane/prompttoquery/client.go +++ b/router/pkg/controlplane/prompttoquery/client.go @@ -72,10 +72,10 @@ func New(endpoint, token string, opts ...Option) (*Client, error) { return c, nil } -func (c *Client) GenerateQuery(ctx context.Context, schemaHash, prompt string) (*nodev1.GenerateQueryResponse, error) { +func (c *Client) GenerateQuery(ctx context.Context, schemaVersionID, prompt string) (*nodev1.GenerateQueryResponse, error) { req := connect.NewRequest(&nodev1.GenerateQueryRequest{ - SchemaHash: schemaHash, - Prompt: prompt, + SchemaVersionId: schemaVersionID, + Prompt: prompt, }) req.Header().Set("Authorization", fmt.Sprintf("Bearer %s", c.graphAPIToken)) diff --git a/router/pkg/controlplane/prompttoquery/client_test.go b/router/pkg/controlplane/prompttoquery/client_test.go index a4f43f10ee..dc17238a12 100644 --- a/router/pkg/controlplane/prompttoquery/client_test.go +++ b/router/pkg/controlplane/prompttoquery/client_test.go @@ -46,11 +46,11 @@ func TestClientGenerateQuery(t *testing.T) { client, err := New(server.URL, "graph-token") require.NoError(t, err) - resp, err := client.GenerateQuery(t.Context(), "sha256:abcdef", "List all employees") + resp, err := client.GenerateQuery(t.Context(), "14a1d197-7e3a-48df-88d7-a663de90527e", "List all employees") require.NoError(t, err) require.Equal(t, "Bearer graph-token", handler.authorizationHeader) - require.Equal(t, "sha256:abcdef", handler.generateQueryRequest.GetSchemaHash()) + require.Equal(t, "14a1d197-7e3a-48df-88d7-a663de90527e", handler.generateQueryRequest.GetSchemaVersionId()) require.Equal(t, "List all employees", handler.generateQueryRequest.GetPrompt()) require.Equal(t, "GetEmployees", resp.GetQuery().GetOperationName()) } diff --git a/router/pkg/mcpserver/discover_test.go b/router/pkg/mcpserver/discover_test.go index 68b6f30f4b..7330741e1d 100644 --- a/router/pkg/mcpserver/discover_test.go +++ b/router/pkg/mcpserver/discover_test.go @@ -36,7 +36,7 @@ func newTestSession(t *testing.T, opts ...func(*Options)) *mcp.ClientSession { }, opts...)..., ) require.NoError(t, err) - require.NoError(t, srv.Reload(&schemaDoc, nil, "sha256:test")) + require.NoError(t, srv.Reload(&schemaDoc, nil, "schema-version-test")) serverTransport, clientTransport := mcp.NewInMemoryTransports() diff --git a/router/pkg/mcpserver/server.go b/router/pkg/mcpserver/server.go index adfddf317d..9dc365dbbb 100644 --- a/router/pkg/mcpserver/server.go +++ b/router/pkg/mcpserver/server.go @@ -118,9 +118,9 @@ type Options struct { PromptToQueryClient PromptToQueryClient } -// PromptToQueryClient generates a GraphQL operation for an indexed schema. +// PromptToQueryClient generates a GraphQL operation for a schema version. type PromptToQueryClient interface { - GenerateQuery(ctx context.Context, schemaHash, prompt string) (*nodev1.GenerateQueryResponse, error) + GenerateQuery(ctx context.Context, schemaVersionID, prompt string) (*nodev1.GenerateQueryResponse, error) } // GraphQLSchemaServer represents an MCP server that works with GraphQL schemas and operations @@ -149,8 +149,8 @@ type GraphQLSchemaServer struct { resourceDocumentation string authMiddleware *MCPAuthMiddleware promptToQueryClient PromptToQueryClient - schemaHashMu sync.RWMutex - schemaHash string + schemaVersionIDMu sync.RWMutex + schemaVersionID string } type graphqlRequest struct { @@ -604,14 +604,14 @@ func (s *GraphQLSchemaServer) Start() error { // Reload reloads the operations and schema, and computes per-tool scope // requirements from @requiresScopes directives in the field configurations. -func (s *GraphQLSchemaServer) Reload(schema *ast.Document, fieldConfigs []*nodev1.FieldConfiguration, schemaHash string) error { +func (s *GraphQLSchemaServer) Reload(schema *ast.Document, fieldConfigs []*nodev1.FieldConfiguration, schemaVersionID string) error { if s.server == nil { return fmt.Errorf("server is not started") } s.schemaCompiler = NewSchemaCompiler(s.logger) s.operationsManager = NewOperationsManager(schema, s.logger, s.excludeMutations) - s.setSchemaHash(schemaHash) + s.setSchemaVersionID(schemaVersionID) if s.operationsDir != "" { if err := s.operationsManager.LoadOperationsFromDirectory(s.operationsDir); err != nil { @@ -886,16 +886,16 @@ func (s *GraphQLSchemaServer) registerTools() error { return nil } -func (s *GraphQLSchemaServer) setSchemaHash(schemaHash string) { - s.schemaHashMu.Lock() - defer s.schemaHashMu.Unlock() - s.schemaHash = schemaHash +func (s *GraphQLSchemaServer) setSchemaVersionID(schemaVersionID string) { + s.schemaVersionIDMu.Lock() + defer s.schemaVersionIDMu.Unlock() + s.schemaVersionID = schemaVersionID } -func (s *GraphQLSchemaServer) getSchemaHash() string { - s.schemaHashMu.RLock() - defer s.schemaHashMu.RUnlock() - return s.schemaHash +func (s *GraphQLSchemaServer) getSchemaVersionID() string { + s.schemaVersionIDMu.RLock() + defer s.schemaVersionIDMu.RUnlock() + return s.schemaVersionID } func toolError(message string) *mcp.CallToolResult { @@ -917,12 +917,12 @@ func (s *GraphQLSchemaServer) handleGenerateQuery() func(ctx context.Context, re return toolError("Input validation failed: prompt is required"), nil } - schemaHash := s.getSchemaHash() - if schemaHash == "" { - return toolError("The active GraphQL schema is not available"), nil + schemaVersionID := s.getSchemaVersionID() + if schemaVersionID == "" { + return toolError("The active GraphQL schema version is not available"), nil } - response, err := s.promptToQueryClient.GenerateQuery(ctx, schemaHash, input.Prompt) + response, err := s.promptToQueryClient.GenerateQuery(ctx, schemaVersionID, input.Prompt) if err != nil { s.logger.Error("failed to generate query through the control plane", zap.Error(err)) return toolError("Failed to generate a GraphQL operation through the control plane"), nil diff --git a/router/pkg/mcpserver/server_test.go b/router/pkg/mcpserver/server_test.go index 5191f4b404..29c4e81cc8 100644 --- a/router/pkg/mcpserver/server_test.go +++ b/router/pkg/mcpserver/server_test.go @@ -24,18 +24,18 @@ import ( ) type fakePromptToQueryClient struct { - response *nodev1.GenerateQueryResponse - err error - schemaHash string - prompt string + response *nodev1.GenerateQueryResponse + err error + schemaVersionID string + prompt string } func stringPointer(value string) *string { return &value } -func (f *fakePromptToQueryClient) GenerateQuery(_ context.Context, schemaHash, prompt string) (*nodev1.GenerateQueryResponse, error) { - f.schemaHash = schemaHash +func (f *fakePromptToQueryClient) GenerateQuery(_ context.Context, schemaVersionID, prompt string) (*nodev1.GenerateQueryResponse, error) { + f.schemaVersionID = schemaVersionID f.prompt = prompt return f.response, f.err } @@ -116,14 +116,14 @@ func TestReload_NoToolDuplication(t *testing.T) { require.NoError(t, err) // First load - err = srv.Reload(&schemaDoc, nil, "sha256:test") + err = srv.Reload(&schemaDoc, nil, "schema-version-test") require.NoError(t, err) firstLoadTools := make([]string, len(srv.registeredTools)) copy(firstLoadTools, srv.registeredTools) // Second load (simulates config reload) - err = srv.Reload(&schemaDoc, nil, "sha256:test") + err = srv.Reload(&schemaDoc, nil, "schema-version-test") require.NoError(t, err) // registeredTools should be identical after reload — no duplicates @@ -162,7 +162,7 @@ func TestReload_ReservedToolNameCollision(t *testing.T) { ) require.NoError(t, err) - err = srv.Reload(&schemaDoc, nil, "sha256:test") + err = srv.Reload(&schemaDoc, nil, "schema-version-test") require.NoError(t, err) // The operation "GetOperationInfo" (snake: "get_operation_info") should be skipped @@ -206,7 +206,7 @@ func TestReload_PrefixModeAvoidsReservedNameCollision(t *testing.T) { ) require.NoError(t, err) - err = srv.Reload(&schemaDoc, nil, "sha256:test") + err = srv.Reload(&schemaDoc, nil, "schema-version-test") require.NoError(t, err) // No collisions because the prefix disambiguates from the reserved name @@ -246,7 +246,7 @@ func TestGenerateQueryTool(t *testing.T) { WithOperationsDir(""), ) require.NoError(t, err) - require.NoError(t, srv.Reload(&schemaDoc, nil, "sha256:first")) + require.NoError(t, srv.Reload(&schemaDoc, nil, "schema-version-first")) require.Contains(t, srv.registeredTools, "generate_query") result, err := srv.handleGenerateQuery()(t.Context(), &mcp.CallToolRequest{ @@ -255,7 +255,7 @@ func TestGenerateQueryTool(t *testing.T) { require.NoError(t, err) require.False(t, result.IsError) - require.Equal(t, "sha256:first", client.schemaHash) + require.Equal(t, "schema-version-first", client.schemaVersionID) require.Equal(t, "List all employees", client.prompt) require.Len(t, result.Content, 1) textContent, ok := result.Content[0].(*mcp.TextContent) @@ -268,50 +268,50 @@ func TestGenerateQueryTool(t *testing.T) { "variablesSchema":"{\"type\":\"object\"}" }`, textContent.Text) - require.NoError(t, srv.Reload(&schemaDoc, nil, "sha256:second")) + require.NoError(t, srv.Reload(&schemaDoc, nil, "schema-version-second")) _, err = srv.handleGenerateQuery()(t.Context(), &mcp.CallToolRequest{ Params: &mcp.CallToolParamsRaw{Arguments: json.RawMessage(`{"prompt":"List employees again"}`)}, }) require.NoError(t, err) - require.Equal(t, "sha256:second", client.schemaHash) + require.Equal(t, "schema-version-second", client.schemaVersionID) } func TestGenerateQueryToolErrors(t *testing.T) { tests := []struct { - name string - client *fakePromptToQueryClient - schemaHash string - arguments string - wantText string + name string + client *fakePromptToQueryClient + schemaVersionID string + arguments string + wantText string }{ { - name: "blank prompt", - client: &fakePromptToQueryClient{}, - schemaHash: "sha256:test", - arguments: `{"prompt":" "}`, - wantText: "prompt is required", + name: "blank prompt", + client: &fakePromptToQueryClient{}, + schemaVersionID: "schema-version-test", + arguments: `{"prompt":" "}`, + wantText: "prompt is required", }, { name: "schema unavailable", client: &fakePromptToQueryClient{}, arguments: `{"prompt":"List employees"}`, - wantText: "schema is not available", + wantText: "schema version is not available", }, { name: "control plane rejection", client: &fakePromptToQueryClient{response: &nodev1.GenerateQueryResponse{ Response: &nodev1.Response{Code: common.EnumStatusCode_ERR_UPGRADE_PLAN, Details: stringPointer("Prompt to Query not available with your current plan")}, }}, - schemaHash: "sha256:test", - arguments: `{"prompt":"List employees"}`, - wantText: "Prompt to Query not available with your current plan", + schemaVersionID: "schema-version-test", + arguments: `{"prompt":"List employees"}`, + wantText: "Prompt to Query not available with your current plan", }, { - name: "transport failure", - client: &fakePromptToQueryClient{err: errors.New("control plane unavailable")}, - schemaHash: "sha256:test", - arguments: `{"prompt":"List employees"}`, - wantText: "Failed to generate a GraphQL operation", + name: "transport failure", + client: &fakePromptToQueryClient{err: errors.New("control plane unavailable")}, + schemaVersionID: "schema-version-test", + arguments: `{"prompt":"List employees"}`, + wantText: "Failed to generate a GraphQL operation", }, } @@ -323,7 +323,7 @@ func TestGenerateQueryToolErrors(t *testing.T) { WithPromptToQueryClient(tt.client), ) require.NoError(t, err) - srv.setSchemaHash(tt.schemaHash) + srv.setSchemaVersionID(tt.schemaVersionID) result, err := srv.handleGenerateQuery()(t.Context(), &mcp.CallToolRequest{ Params: &mcp.CallToolParamsRaw{Arguments: json.RawMessage(tt.arguments)}, From fd817ffc9633f6f0ef05f722d9bce53c038d7ce3 Mon Sep 17 00:00:00 2001 From: Alberto Garcia Hierro Date: Wed, 12 Aug 2026 17:50:53 +0100 Subject: [PATCH 03/14] refactor: align generate query version field --- .../gen/proto/wg/cosmo/node/v1/node.pb.go | 21 ++++++++++--------- connect/src/wg/cosmo/node/v1/node_pb.ts | 8 ++++--- .../src/core/bufservices/NodeService.ts | 4 ++-- controlplane/test/prompt-to-query.test.ts | 2 +- proto/wg/cosmo/node/v1/node.proto | 3 ++- router/gen/proto/wg/cosmo/node/v1/node.pb.go | 21 ++++++++++--------- .../pkg/controlplane/prompttoquery/client.go | 4 ++-- .../controlplane/prompttoquery/client_test.go | 2 +- 8 files changed, 35 insertions(+), 30 deletions(-) diff --git a/connect-go/gen/proto/wg/cosmo/node/v1/node.pb.go b/connect-go/gen/proto/wg/cosmo/node/v1/node.pb.go index a9a9047088..f9b2f4b88d 100644 --- a/connect-go/gen/proto/wg/cosmo/node/v1/node.pb.go +++ b/connect-go/gen/proto/wg/cosmo/node/v1/node.pb.go @@ -1010,11 +1010,12 @@ func (x *SelfRegisterResponse) GetRegistrationInfo() *RegistrationInfo { } type GenerateQueryRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - SchemaVersionId string `protobuf:"bytes,1,opt,name=schema_version_id,json=schemaVersionId,proto3" json:"schema_version_id,omitempty"` - Prompt string `protobuf:"bytes,2,opt,name=prompt,proto3" json:"prompt,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + // Copied from RouterConfig.version in the active router configuration. + Version string `protobuf:"bytes,1,opt,name=version,proto3" json:"version,omitempty"` + Prompt string `protobuf:"bytes,2,opt,name=prompt,proto3" json:"prompt,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *GenerateQueryRequest) Reset() { @@ -1047,9 +1048,9 @@ func (*GenerateQueryRequest) Descriptor() ([]byte, []int) { return file_wg_cosmo_node_v1_node_proto_rawDescGZIP(), []int{10} } -func (x *GenerateQueryRequest) GetSchemaVersionId() string { +func (x *GenerateQueryRequest) GetVersion() string { if x != nil { - return x.SchemaVersionId + return x.Version } return "" } @@ -5183,9 +5184,9 @@ const file_wg_cosmo_node_v1_node_proto_rawDesc = "" + "\x14SelfRegisterResponse\x126\n" + "\bresponse\x18\x01 \x01(\v2\x1a.wg.cosmo.node.v1.ResponseR\bresponse\x12S\n" + "\x10registrationInfo\x18\x02 \x01(\v2\".wg.cosmo.node.v1.RegistrationInfoH\x00R\x10registrationInfo\x88\x01\x01B\x13\n" + - "\x11_registrationInfo\"Z\n" + - "\x14GenerateQueryRequest\x12*\n" + - "\x11schema_version_id\x18\x01 \x01(\tR\x0fschemaVersionId\x12\x16\n" + + "\x11_registrationInfo\"H\n" + + "\x14GenerateQueryRequest\x12\x18\n" + + "\aversion\x18\x01 \x01(\tR\aversion\x12\x16\n" + "\x06prompt\x18\x02 \x01(\tR\x06prompt\"\x96\x01\n" + "\x15GenerateQueryResponse\x126\n" + "\bresponse\x18\x01 \x01(\v2\x1a.wg.cosmo.node.v1.ResponseR\bresponse\x12;\n" + diff --git a/connect/src/wg/cosmo/node/v1/node_pb.ts b/connect/src/wg/cosmo/node/v1/node_pb.ts index 5bf5e261f1..9256ba2bf7 100644 --- a/connect/src/wg/cosmo/node/v1/node_pb.ts +++ b/connect/src/wg/cosmo/node/v1/node_pb.ts @@ -12,7 +12,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file wg/cosmo/node/v1/node.proto. */ export const file_wg_cosmo_node_v1_node: GenFile = /*@__PURE__*/ - fileDesc("Cht3Zy9jb3Ntby9ub2RlL3YxL25vZGUucHJvdG8SEHdnLmNvc21vLm5vZGUudjEiOQoIU3ViZ3JhcGgSCgoCaWQYASABKAkSDAoEbmFtZRgCIAEoCRITCgtyb3V0aW5nX3VybBgDIAEoCSKOAgohRmVhdHVyZUZsYWdSb3V0ZXJFeGVjdXRpb25Db25maWdzEnUKG2NvbmZpZ19ieV9mZWF0dXJlX2ZsYWdfbmFtZRgBIAMoCzJQLndnLmNvc21vLm5vZGUudjEuRmVhdHVyZUZsYWdSb3V0ZXJFeGVjdXRpb25Db25maWdzLkNvbmZpZ0J5RmVhdHVyZUZsYWdOYW1lRW50cnkacgocQ29uZmlnQnlGZWF0dXJlRmxhZ05hbWVFbnRyeRILCgNrZXkYASABKAkSQQoFdmFsdWUYAiABKAsyMi53Zy5jb3Ntby5ub2RlLnYxLkZlYXR1cmVGbGFnUm91dGVyRXhlY3V0aW9uQ29uZmlnOgI4ASKgAQogRmVhdHVyZUZsYWdSb3V0ZXJFeGVjdXRpb25Db25maWcSPAoNZW5naW5lX2NvbmZpZxgBIAEoCzIlLndnLmNvc21vLm5vZGUudjEuRW5naW5lQ29uZmlndXJhdGlvbhIPCgd2ZXJzaW9uGAIgASgJEi0KCXN1YmdyYXBocxgDIAMoCzIaLndnLmNvc21vLm5vZGUudjEuU3ViZ3JhcGginAIKDFJvdXRlckNvbmZpZxI8Cg1lbmdpbmVfY29uZmlnGAEgASgLMiUud2cuY29zbW8ubm9kZS52MS5FbmdpbmVDb25maWd1cmF0aW9uEg8KB3ZlcnNpb24YAiABKAkSLQoJc3ViZ3JhcGhzGAMgAygLMhoud2cuY29zbW8ubm9kZS52MS5TdWJncmFwaBJWChRmZWF0dXJlX2ZsYWdfY29uZmlncxgEIAEoCzIzLndnLmNvc21vLm5vZGUudjEuRmVhdHVyZUZsYWdSb3V0ZXJFeGVjdXRpb25Db25maWdzSACIAQESHQoVY29tcGF0aWJpbGl0eV92ZXJzaW9uGAUgASgJQhcKFV9mZWF0dXJlX2ZsYWdfY29uZmlncyJbCghSZXNwb25zZRItCgRjb2RlGAEgASgOMh8ud2cuY29zbW8uY29tbW9uLkVudW1TdGF0dXNDb2RlEhQKB2RldGFpbHMYAiABKAlIAIgBAUIKCghfZGV0YWlscyIvCg5SZXNwb25zZVN0YXR1cxIMCgRjb2RlGAEgASgFEg8KB21lc3NhZ2UYAiABKAkiZQoQUmVnaXN0cmF0aW9uSW5mbxI3Cg5hY2NvdW50X2xpbWl0cxgBIAEoCzIfLndnLmNvc21vLm5vZGUudjEuQWNjb3VudExpbWl0cxIYChBncmFwaF9wdWJsaWNfa2V5GAIgASgJIiwKDUFjY291bnRMaW1pdHMSGwoTdHJhY2Vfc2FtcGxpbmdfcmF0ZRgBIAEoAiIVChNTZWxmUmVnaXN0ZXJSZXF1ZXN0IpwBChRTZWxmUmVnaXN0ZXJSZXNwb25zZRIsCghyZXNwb25zZRgBIAEoCzIaLndnLmNvc21vLm5vZGUudjEuUmVzcG9uc2USQQoQcmVnaXN0cmF0aW9uSW5mbxgCIAEoCzIiLndnLmNvc21vLm5vZGUudjEuUmVnaXN0cmF0aW9uSW5mb0gAiAEBQhMKEV9yZWdpc3RyYXRpb25JbmZvIkEKFEdlbmVyYXRlUXVlcnlSZXF1ZXN0EhkKEXNjaGVtYV92ZXJzaW9uX2lkGAEgASgJEg4KBnByb21wdBgCIAEoCSKFAQoVR2VuZXJhdGVRdWVyeVJlc3BvbnNlEiwKCHJlc3BvbnNlGAEgASgLMhoud2cuY29zbW8ubm9kZS52MS5SZXNwb25zZRI0CgVxdWVyeRgCIAEoCzIgLndnLmNvc21vLm5vZGUudjEuU2F0aXNmaWVkUXVlcnlIAIgBAUIICgZfcXVlcnkiqwEKDlNhdGlzZmllZFF1ZXJ5EhMKC2Rlc2NyaXB0aW9uGAEgASgJEhAKCGRvY3VtZW50GAIgASgJEhYKDm9wZXJhdGlvbl9uYW1lGAMgASgJEkAKDm9wZXJhdGlvbl90eXBlGAQgASgOMigud2cuY29zbW8ubm9kZS52MS5TYXRpc2ZpZWRPcGVyYXRpb25UeXBlEhgKEHZhcmlhYmxlc19zY2hlbWEYBSABKAki5AMKE0VuZ2luZUNvbmZpZ3VyYXRpb24SHAoUZGVmYXVsdEZsdXNoSW50ZXJ2YWwYASABKAMSTAoZZGF0YXNvdXJjZV9jb25maWd1cmF0aW9ucxgCIAMoCzIpLndnLmNvc21vLm5vZGUudjEuRGF0YVNvdXJjZUNvbmZpZ3VyYXRpb24SQgoUZmllbGRfY29uZmlndXJhdGlvbnMYAyADKAsyJC53Zy5jb3Ntby5ub2RlLnYxLkZpZWxkQ29uZmlndXJhdGlvbhIVCg1ncmFwaHFsU2NoZW1hGAQgASgJEkAKE3R5cGVfY29uZmlndXJhdGlvbnMYBSADKAsyIy53Zy5jb3Ntby5ub2RlLnYxLlR5cGVDb25maWd1cmF0aW9uElAKDnN0cmluZ19zdG9yYWdlGAYgAygLMjgud2cuY29zbW8ubm9kZS52MS5FbmdpbmVDb25maWd1cmF0aW9uLlN0cmluZ1N0b3JhZ2VFbnRyeRIiChVncmFwaHFsX2NsaWVudF9zY2hlbWEYByABKAlIAIgBARo0ChJTdHJpbmdTdG9yYWdlRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4AUIYChZfZ3JhcGhxbF9jbGllbnRfc2NoZW1hIsYHChdEYXRhU291cmNlQ29uZmlndXJhdGlvbhIuCgRraW5kGAEgASgOMiAud2cuY29zbW8ubm9kZS52MS5EYXRhU291cmNlS2luZBIvCgpyb290X25vZGVzGAIgAygLMhsud2cuY29zbW8ubm9kZS52MS5UeXBlRmllbGQSMAoLY2hpbGRfbm9kZXMYAyADKAsyGy53Zy5jb3Ntby5ub2RlLnYxLlR5cGVGaWVsZBImCh5vdmVycmlkZV9maWVsZF9wYXRoX2Zyb21fYWxpYXMYBCABKAgSQgoOY3VzdG9tX2dyYXBocWwYBSABKAsyKi53Zy5jb3Ntby5ub2RlLnYxLkRhdGFTb3VyY2VDdXN0b21fR3JhcGhRTBJACg1jdXN0b21fc3RhdGljGAYgASgLMikud2cuY29zbW8ubm9kZS52MS5EYXRhU291cmNlQ3VzdG9tX1N0YXRpYxI8CgpkaXJlY3RpdmVzGAcgAygLMigud2cuY29zbW8ubm9kZS52MS5EaXJlY3RpdmVDb25maWd1cmF0aW9uEh8KF3JlcXVlc3RfdGltZW91dF9zZWNvbmRzGAggASgDEgoKAmlkGAkgASgJEi0KBGtleXMYCiADKAsyHy53Zy5jb3Ntby5ub2RlLnYxLlJlcXVpcmVkRmllbGQSMQoIcHJvdmlkZXMYCyADKAsyHy53Zy5jb3Ntby5ub2RlLnYxLlJlcXVpcmVkRmllbGQSMQoIcmVxdWlyZXMYDCADKAsyHy53Zy5jb3Ntby5ub2RlLnYxLlJlcXVpcmVkRmllbGQSPwoNY3VzdG9tX2V2ZW50cxgNIAEoCzIoLndnLmNvc21vLm5vZGUudjEuRGF0YVNvdXJjZUN1c3RvbUV2ZW50cxJJChFlbnRpdHlfaW50ZXJmYWNlcxgOIAMoCzIuLndnLmNvc21vLm5vZGUudjEuRW50aXR5SW50ZXJmYWNlQ29uZmlndXJhdGlvbhJJChFpbnRlcmZhY2Vfb2JqZWN0cxgPIAMoCzIuLndnLmNvc21vLm5vZGUudjEuRW50aXR5SW50ZXJmYWNlQ29uZmlndXJhdGlvbhI/ChJjb3N0X2NvbmZpZ3VyYXRpb24YECABKAsyIy53Zy5jb3Ntby5ub2RlLnYxLkNvc3RDb25maWd1cmF0aW9uElIKHGVudGl0eV9jYWNoaW5nX2NvbmZpZ3VyYXRpb24YESABKAsyLC53Zy5jb3Ntby5ub2RlLnYxLkVudGl0eUNhY2hpbmdDb25maWd1cmF0aW9uIpsCChpFbnRpdHlDYWNoaW5nQ29uZmlndXJhdGlvbhJPChtlbnRpdHlfY2FjaGVfY29uZmlndXJhdGlvbnMYASADKAsyKi53Zy5jb3Ntby5ub2RlLnYxLkVudGl0eUNhY2hlQ29uZmlndXJhdGlvbhJXCh9jYWNoZV9pbnZhbGlkYXRlX2NvbmZpZ3VyYXRpb25zGAIgAygLMi4ud2cuY29zbW8ubm9kZS52MS5DYWNoZUludmFsaWRhdGVDb25maWd1cmF0aW9uElMKHWNhY2hlX3BvcHVsYXRlX2NvbmZpZ3VyYXRpb25zGAMgAygLMiwud2cuY29zbW8ubm9kZS52MS5DYWNoZVBvcHVsYXRlQ29uZmlndXJhdGlvbiK1AQoYRW50aXR5Q2FjaGVDb25maWd1cmF0aW9uEhEKCXR5cGVfbmFtZRgBIAEoCRIXCg9tYXhfYWdlX3NlY29uZHMYAiABKAMSFwoPaW5jbHVkZV9oZWFkZXJzGAMgASgIEhoKEnBhcnRpYWxfY2FjaGVfbG9hZBgEIAEoCBITCgtzaGFkb3dfbW9kZRgFIAEoCBIjChtub3RfZm91bmRfY2FjaGVfdHRsX3NlY29uZHMYBiABKAMiZAocQ2FjaGVJbnZhbGlkYXRlQ29uZmlndXJhdGlvbhISCgpmaWVsZF9uYW1lGAEgASgJEhYKDm9wZXJhdGlvbl90eXBlGAIgASgJEhgKEGVudGl0eV90eXBlX25hbWUYAyABKAkiewoaQ2FjaGVQb3B1bGF0ZUNvbmZpZ3VyYXRpb24SEgoKZmllbGRfbmFtZRgBIAEoCRIWCg5vcGVyYXRpb25fdHlwZRgCIAEoCRIXCg9tYXhfYWdlX3NlY29uZHMYAyABKAMSGAoQZW50aXR5X3R5cGVfbmFtZRgEIAEoCSLAAwoRQ29zdENvbmZpZ3VyYXRpb24SQQoNZmllbGRfd2VpZ2h0cxgBIAMoCzIqLndnLmNvc21vLm5vZGUudjEuRmllbGRXZWlnaHRDb25maWd1cmF0aW9uEkAKCmxpc3Rfc2l6ZXMYAiADKAsyLC53Zy5jb3Ntby5ub2RlLnYxLkZpZWxkTGlzdFNpemVDb25maWd1cmF0aW9uEkoKDHR5cGVfd2VpZ2h0cxgDIAMoCzI0LndnLmNvc21vLm5vZGUudjEuQ29zdENvbmZpZ3VyYXRpb24uVHlwZVdlaWdodHNFbnRyeRJlChpkaXJlY3RpdmVfYXJndW1lbnRfd2VpZ2h0cxgEIAMoCzJBLndnLmNvc21vLm5vZGUudjEuQ29zdENvbmZpZ3VyYXRpb24uRGlyZWN0aXZlQXJndW1lbnRXZWlnaHRzRW50cnkaMgoQVHlwZVdlaWdodHNFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAU6AjgBGj8KHURpcmVjdGl2ZUFyZ3VtZW50V2VpZ2h0c0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoBToCOAEiowMKGEZpZWxkV2VpZ2h0Q29uZmlndXJhdGlvbhIRCgl0eXBlX25hbWUYASABKAkSEgoKZmllbGRfbmFtZRgCIAEoCRITCgZ3ZWlnaHQYAyABKAVIAIgBARJZChBhcmd1bWVudF93ZWlnaHRzGAQgAygLMj8ud2cuY29zbW8ubm9kZS52MS5GaWVsZFdlaWdodENvbmZpZ3VyYXRpb24uQXJndW1lbnRXZWlnaHRzRW50cnkSbAoaZGlyZWN0aXZlX2FyZ3VtZW50X3dlaWdodHMYBSADKAsySC53Zy5jb3Ntby5ub2RlLnYxLkZpZWxkV2VpZ2h0Q29uZmlndXJhdGlvbi5EaXJlY3RpdmVBcmd1bWVudFdlaWdodHNFbnRyeRo2ChRBcmd1bWVudFdlaWdodHNFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAU6AjgBGj8KHURpcmVjdGl2ZUFyZ3VtZW50V2VpZ2h0c0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoBToCOAFCCQoHX3dlaWdodCLsAQoaRmllbGRMaXN0U2l6ZUNvbmZpZ3VyYXRpb24SEQoJdHlwZV9uYW1lGAEgASgJEhIKCmZpZWxkX25hbWUYAiABKAkSGQoMYXNzdW1lZF9zaXplGAMgASgFSACIAQESGQoRc2xpY2luZ19hcmd1bWVudHMYBCADKAkSFAoMc2l6ZWRfZmllbGRzGAUgAygJEikKHHJlcXVpcmVfb25lX3NsaWNpbmdfYXJndW1lbnQYBiABKAhIAYgBAUIPCg1fYXNzdW1lZF9zaXplQh8KHV9yZXF1aXJlX29uZV9zbGljaW5nX2FyZ3VtZW50IlwKFUFyZ3VtZW50Q29uZmlndXJhdGlvbhIMCgRuYW1lGAEgASgJEjUKC3NvdXJjZV90eXBlGAIgASgOMiAud2cuY29zbW8ubm9kZS52MS5Bcmd1bWVudFNvdXJjZSIlCgZTY29wZXMSGwoTcmVxdWlyZWRfYW5kX3Njb3BlcxgBIAMoCSKvAQoaQXV0aG9yaXphdGlvbkNvbmZpZ3VyYXRpb24SHwoXcmVxdWlyZXNfYXV0aGVudGljYXRpb24YASABKAgSNAoScmVxdWlyZWRfb3Jfc2NvcGVzGAIgAygLMhgud2cuY29zbW8ubm9kZS52MS5TY29wZXMSOgoYcmVxdWlyZWRfb3Jfc2NvcGVzX2J5X29yGAMgAygLMhgud2cuY29zbW8ubm9kZS52MS5TY29wZXMi1QIKEkZpZWxkQ29uZmlndXJhdGlvbhIRCgl0eXBlX25hbWUYASABKAkSEgoKZmllbGRfbmFtZRgCIAEoCRJIChdhcmd1bWVudHNfY29uZmlndXJhdGlvbhgDIAMoCzInLndnLmNvc21vLm5vZGUudjEuQXJndW1lbnRDb25maWd1cmF0aW9uElEKG2F1dGhvcml6YXRpb25fY29uZmlndXJhdGlvbhgEIAEoCzIsLndnLmNvc21vLm5vZGUudjEuQXV0aG9yaXphdGlvbkNvbmZpZ3VyYXRpb24SWQodc3Vic2NyaXB0aW9uX2ZpbHRlcl9jb25kaXRpb24YBSABKAsyLS53Zy5jb3Ntby5ub2RlLnYxLlN1YnNjcmlwdGlvbkZpbHRlckNvbmRpdGlvbkgAiAEBQiAKHl9zdWJzY3JpcHRpb25fZmlsdGVyX2NvbmRpdGlvbiI5ChFUeXBlQ29uZmlndXJhdGlvbhIRCgl0eXBlX25hbWUYASABKAkSEQoJcmVuYW1lX3RvGAIgASgJInwKCVR5cGVGaWVsZBIRCgl0eXBlX25hbWUYASABKAkSEwoLZmllbGRfbmFtZXMYAiADKAkSHAoUZXh0ZXJuYWxfZmllbGRfbmFtZXMYAyADKAkSKQohcmVxdWlyZV9mZXRjaF9yZWFzb25zX2ZpZWxkX25hbWVzGAQgAygJIjkKEEZpZWxkQ29vcmRpbmF0ZXMSEgoKZmllbGRfbmFtZRgBIAEoCRIRCgl0eXBlX25hbWUYAiABKAkiawoRRmllbGRTZXRDb25kaXRpb24SQgoWZmllbGRfY29vcmRpbmF0ZXNfcGF0aBgBIAMoCzIiLndnLmNvc21vLm5vZGUudjEuRmllbGRDb29yZGluYXRlcxISCgpmaWVsZF9wYXRoGAIgAygJIqcBCg1SZXF1aXJlZEZpZWxkEhEKCXR5cGVfbmFtZRgBIAEoCRISCgpmaWVsZF9uYW1lGAIgASgJEhUKDXNlbGVjdGlvbl9zZXQYAyABKAkSHwoXZGlzYWJsZV9lbnRpdHlfcmVzb2x2ZXIYBCABKAgSNwoKY29uZGl0aW9ucxgFIAMoCzIjLndnLmNvc21vLm5vZGUudjEuRmllbGRTZXRDb25kaXRpb24iWAocRW50aXR5SW50ZXJmYWNlQ29uZmlndXJhdGlvbhIbChNpbnRlcmZhY2VfdHlwZV9uYW1lGAEgASgJEhsKE2NvbmNyZXRlX3R5cGVfbmFtZXMYAiADKAkijQUKEkZldGNoQ29uZmlndXJhdGlvbhI0CgN1cmwYASABKAsyJy53Zy5jb3Ntby5ub2RlLnYxLkNvbmZpZ3VyYXRpb25WYXJpYWJsZRIsCgZtZXRob2QYAiABKA4yHC53Zy5jb3Ntby5ub2RlLnYxLkhUVFBNZXRob2QSQAoGaGVhZGVyGAMgAygLMjAud2cuY29zbW8ubm9kZS52MS5GZXRjaENvbmZpZ3VyYXRpb24uSGVhZGVyRW50cnkSNQoEYm9keRgEIAEoCzInLndnLmNvc21vLm5vZGUudjEuQ29uZmlndXJhdGlvblZhcmlhYmxlEjYKBXF1ZXJ5GAUgAygLMicud2cuY29zbW8ubm9kZS52MS5VUkxRdWVyeUNvbmZpZ3VyYXRpb24SFwoPdXJsX2VuY29kZV9ib2R5GAcgASgIEjEKBG10bHMYCCABKAsyIy53Zy5jb3Ntby5ub2RlLnYxLk1UTFNDb25maWd1cmF0aW9uEjkKCGJhc2VfdXJsGAkgASgLMicud2cuY29zbW8ubm9kZS52MS5Db25maWd1cmF0aW9uVmFyaWFibGUSNQoEcGF0aBgKIAEoCzInLndnLmNvc21vLm5vZGUudjEuQ29uZmlndXJhdGlvblZhcmlhYmxlEkQKDmh0dHBfcHJveHlfdXJsGAsgASgLMicud2cuY29zbW8ubm9kZS52MS5Db25maWd1cmF0aW9uVmFyaWFibGVIAIgBARpLCgtIZWFkZXJFbnRyeRILCgNrZXkYASABKAkSKwoFdmFsdWUYAiABKAsyHC53Zy5jb3Ntby5ub2RlLnYxLkhUVFBIZWFkZXI6AjgBQhEKD19odHRwX3Byb3h5X3VybCJlChVTdGF0dXNDb2RlVHlwZU1hcHBpbmcSEwoLc3RhdHVzX2NvZGUYASABKAMSEQoJdHlwZV9uYW1lGAIgASgJEiQKHGluamVjdF9zdGF0dXNfY29kZV9pbnRvX2JvZHkYAyABKAgikwMKGERhdGFTb3VyY2VDdXN0b21fR3JhcGhRTBIzCgVmZXRjaBgBIAEoCzIkLndnLmNvc21vLm5vZGUudjEuRmV0Y2hDb25maWd1cmF0aW9uEkgKDHN1YnNjcmlwdGlvbhgCIAEoCzIyLndnLmNvc21vLm5vZGUudjEuR3JhcGhRTFN1YnNjcmlwdGlvbkNvbmZpZ3VyYXRpb24SRAoKZmVkZXJhdGlvbhgDIAEoCzIwLndnLmNvc21vLm5vZGUudjEuR3JhcGhRTEZlZGVyYXRpb25Db25maWd1cmF0aW9uEjkKD3Vwc3RyZWFtX3NjaGVtYRgEIAEoCzIgLndnLmNvc21vLm5vZGUudjEuSW50ZXJuZWRTdHJpbmcSRAoZY3VzdG9tX3NjYWxhcl90eXBlX2ZpZWxkcxgGIAMoCzIhLndnLmNvc21vLm5vZGUudjEuU2luZ2xlVHlwZUZpZWxkEjEKBGdycGMYByABKAsyIy53Zy5jb3Ntby5ub2RlLnYxLkdSUENDb25maWd1cmF0aW9uIpABChFHUlBDQ29uZmlndXJhdGlvbhIuCgdtYXBwaW5nGAEgASgLMh0ud2cuY29zbW8ubm9kZS52MS5HUlBDTWFwcGluZxIUCgxwcm90b19zY2hlbWEYAiABKAkSNQoGcGx1Z2luGAMgASgLMiUud2cuY29zbW8ubm9kZS52MS5QbHVnaW5Db25maWd1cmF0aW9uIjcKDkltYWdlUmVmZXJlbmNlEhIKCnJlcG9zaXRvcnkYASABKAkSEQoJcmVmZXJlbmNlGAIgASgJIogBChNQbHVnaW5Db25maWd1cmF0aW9uEgwKBG5hbWUYASABKAkSDwoHdmVyc2lvbhgCIAEoCRI+Cg9pbWFnZV9yZWZlcmVuY2UYAyABKAsyIC53Zy5jb3Ntby5ub2RlLnYxLkltYWdlUmVmZXJlbmNlSACIAQFCEgoQX2ltYWdlX3JlZmVyZW5jZSIjChBTU0xDb25maWd1cmF0aW9uEg8KB2VuYWJsZWQYASABKAgi2wIKC0dSUENNYXBwaW5nEg8KB3ZlcnNpb24YASABKAUSDwoHc2VydmljZRgCIAEoCRI+ChJvcGVyYXRpb25fbWFwcGluZ3MYAyADKAsyIi53Zy5jb3Ntby5ub2RlLnYxLk9wZXJhdGlvbk1hcHBpbmcSOAoPZW50aXR5X21hcHBpbmdzGAQgAygLMh8ud2cuY29zbW8ubm9kZS52MS5FbnRpdHlNYXBwaW5nEj8KE3R5cGVfZmllbGRfbWFwcGluZ3MYBSADKAsyIi53Zy5jb3Ntby5ub2RlLnYxLlR5cGVGaWVsZE1hcHBpbmcSNAoNZW51bV9tYXBwaW5ncxgGIAMoCzIdLndnLmNvc21vLm5vZGUudjEuRW51bU1hcHBpbmcSOQoQcmVzb2x2ZV9tYXBwaW5ncxgHIAMoCzIfLndnLmNvc21vLm5vZGUudjEuTG9va3VwTWFwcGluZyKpAQoNTG9va3VwTWFwcGluZxIqCgR0eXBlGAEgASgOMhwud2cuY29zbW8ubm9kZS52MS5Mb29rdXBUeXBlEjwKDmxvb2t1cF9tYXBwaW5nGAIgASgLMiQud2cuY29zbW8ubm9kZS52MS5Mb29rdXBGaWVsZE1hcHBpbmcSCwoDcnBjGAMgASgJEg8KB3JlcXVlc3QYBCABKAkSEAoIcmVzcG9uc2UYBSABKAkiWQoSTG9va3VwRmllbGRNYXBwaW5nEgwKBHR5cGUYASABKAkSNQoNZmllbGRfbWFwcGluZxgCIAEoCzIeLndnLmNvc21vLm5vZGUudjEuRmllbGRNYXBwaW5nIoYBChBPcGVyYXRpb25NYXBwaW5nEi0KBHR5cGUYASABKA4yHy53Zy5jb3Ntby5ub2RlLnYxLk9wZXJhdGlvblR5cGUSEAoIb3JpZ2luYWwYAiABKAkSDgoGbWFwcGVkGAMgASgJEg8KB3JlcXVlc3QYBCABKAkSEAoIcmVzcG9uc2UYBSABKAkitgEKDUVudGl0eU1hcHBpbmcSEQoJdHlwZV9uYW1lGAEgASgJEgwKBGtpbmQYAiABKAkSCwoDa2V5GAMgASgJEgsKA3JwYxgEIAEoCRIPCgdyZXF1ZXN0GAUgASgJEhAKCHJlc3BvbnNlGAYgASgJEkcKF3JlcXVpcmVkX2ZpZWxkX21hcHBpbmdzGAcgAygLMiYud2cuY29zbW8ubm9kZS52MS5SZXF1aXJlZEZpZWxkTWFwcGluZyJ9ChRSZXF1aXJlZEZpZWxkTWFwcGluZxI1Cg1maWVsZF9tYXBwaW5nGAEgASgLMh4ud2cuY29zbW8ubm9kZS52MS5GaWVsZE1hcHBpbmcSCwoDcnBjGAIgASgJEg8KB3JlcXVlc3QYAyABKAkSEAoIcmVzcG9uc2UYBCABKAkiWAoQVHlwZUZpZWxkTWFwcGluZxIMCgR0eXBlGAEgASgJEjYKDmZpZWxkX21hcHBpbmdzGAIgAygLMh4ud2cuY29zbW8ubm9kZS52MS5GaWVsZE1hcHBpbmcibgoMRmllbGRNYXBwaW5nEhAKCG9yaWdpbmFsGAEgASgJEg4KBm1hcHBlZBgCIAEoCRI8ChFhcmd1bWVudF9tYXBwaW5ncxgDIAMoCzIhLndnLmNvc21vLm5vZGUudjEuQXJndW1lbnRNYXBwaW5nIjMKD0FyZ3VtZW50TWFwcGluZxIQCghvcmlnaW5hbBgBIAEoCRIOCgZtYXBwZWQYAiABKAkiTwoLRW51bU1hcHBpbmcSDAoEdHlwZRgBIAEoCRIyCgZ2YWx1ZXMYAiADKAsyIi53Zy5jb3Ntby5ub2RlLnYxLkVudW1WYWx1ZU1hcHBpbmciNAoQRW51bVZhbHVlTWFwcGluZxIQCghvcmlnaW5hbBgBIAEoCRIOCgZtYXBwZWQYAiABKAkiagoXTmF0c1N0cmVhbUNvbmZpZ3VyYXRpb24SFQoNY29uc3VtZXJfbmFtZRgBIAEoCRITCgtzdHJlYW1fbmFtZRgCIAEoCRIjChtjb25zdW1lcl9pbmFjdGl2ZV90aHJlc2hvbGQYAyABKAUiwwEKFk5hdHNFdmVudENvbmZpZ3VyYXRpb24STgoaZW5naW5lX2V2ZW50X2NvbmZpZ3VyYXRpb24YASABKAsyKi53Zy5jb3Ntby5ub2RlLnYxLkVuZ2luZUV2ZW50Q29uZmlndXJhdGlvbhIQCghzdWJqZWN0cxgCIAMoCRJHChRzdHJlYW1fY29uZmlndXJhdGlvbhgDIAEoCzIpLndnLmNvc21vLm5vZGUudjEuTmF0c1N0cmVhbUNvbmZpZ3VyYXRpb24ieQoXS2Fma2FFdmVudENvbmZpZ3VyYXRpb24STgoaZW5naW5lX2V2ZW50X2NvbmZpZ3VyYXRpb24YASABKAsyKi53Zy5jb3Ntby5ub2RlLnYxLkVuZ2luZUV2ZW50Q29uZmlndXJhdGlvbhIOCgZ0b3BpY3MYAiADKAkiewoXUmVkaXNFdmVudENvbmZpZ3VyYXRpb24STgoaZW5naW5lX2V2ZW50X2NvbmZpZ3VyYXRpb24YASABKAsyKi53Zy5jb3Ntby5ub2RlLnYxLkVuZ2luZUV2ZW50Q29uZmlndXJhdGlvbhIQCghjaGFubmVscxgCIAMoCSKBAQoYRW5naW5lRXZlbnRDb25maWd1cmF0aW9uEhMKC3Byb3ZpZGVyX2lkGAEgASgJEikKBHR5cGUYAiABKA4yGy53Zy5jb3Ntby5ub2RlLnYxLkV2ZW50VHlwZRIRCgl0eXBlX25hbWUYAyABKAkSEgoKZmllbGRfbmFtZRgEIAEoCSLEAQoWRGF0YVNvdXJjZUN1c3RvbUV2ZW50cxI2CgRuYXRzGAEgAygLMigud2cuY29zbW8ubm9kZS52MS5OYXRzRXZlbnRDb25maWd1cmF0aW9uEjgKBWthZmthGAIgAygLMikud2cuY29zbW8ubm9kZS52MS5LYWZrYUV2ZW50Q29uZmlndXJhdGlvbhI4CgVyZWRpcxgDIAMoCzIpLndnLmNvc21vLm5vZGUudjEuUmVkaXNFdmVudENvbmZpZ3VyYXRpb24iUAoXRGF0YVNvdXJjZUN1c3RvbV9TdGF0aWMSNQoEZGF0YRgBIAEoCzInLndnLmNvc21vLm5vZGUudjEuQ29uZmlndXJhdGlvblZhcmlhYmxlIuUBChVDb25maWd1cmF0aW9uVmFyaWFibGUSOQoEa2luZBgBIAEoDjIrLndnLmNvc21vLm5vZGUudjEuQ29uZmlndXJhdGlvblZhcmlhYmxlS2luZBIfChdzdGF0aWNfdmFyaWFibGVfY29udGVudBgCIAEoCRIhChllbnZpcm9ubWVudF92YXJpYWJsZV9uYW1lGAMgASgJEioKImVudmlyb25tZW50X3ZhcmlhYmxlX2RlZmF1bHRfdmFsdWUYBCABKAkSIQoZcGxhY2Vob2xkZXJfdmFyaWFibGVfbmFtZRgFIAEoCSJDChZEaXJlY3RpdmVDb25maWd1cmF0aW9uEhYKDmRpcmVjdGl2ZV9uYW1lGAEgASgJEhEKCXJlbmFtZV90bxgCIAEoCSI0ChVVUkxRdWVyeUNvbmZpZ3VyYXRpb24SDAoEbmFtZRgBIAEoCRINCgV2YWx1ZRgCIAEoCSJFCgpIVFRQSGVhZGVyEjcKBnZhbHVlcxgBIAMoCzInLndnLmNvc21vLm5vZGUudjEuQ29uZmlndXJhdGlvblZhcmlhYmxlIpwBChFNVExTQ29uZmlndXJhdGlvbhI0CgNrZXkYASABKAsyJy53Zy5jb3Ntby5ub2RlLnYxLkNvbmZpZ3VyYXRpb25WYXJpYWJsZRI1CgRjZXJ0GAIgASgLMicud2cuY29zbW8ubm9kZS52MS5Db25maWd1cmF0aW9uVmFyaWFibGUSGgoSaW5zZWN1cmVTa2lwVmVyaWZ5GAMgASgIIsUCCiBHcmFwaFFMU3Vic2NyaXB0aW9uQ29uZmlndXJhdGlvbhIPCgdlbmFibGVkGAEgASgIEjQKA3VybBgCIAEoCzInLndnLmNvc21vLm5vZGUudjEuQ29uZmlndXJhdGlvblZhcmlhYmxlEhMKBnVzZVNTRRgDIAEoCEgAiAEBEkMKCHByb3RvY29sGAQgASgOMiwud2cuY29zbW8uY29tbW9uLkdyYXBoUUxTdWJzY3JpcHRpb25Qcm90b2NvbEgBiAEBEk8KFHdlYnNvY2tldFN1YnByb3RvY29sGAUgASgOMiwud2cuY29zbW8uY29tbW9uLkdyYXBoUUxXZWJzb2NrZXRTdWJwcm90b2NvbEgCiAEBQgkKB191c2VTU0VCCwoJX3Byb3RvY29sQhcKFV93ZWJzb2NrZXRTdWJwcm90b2NvbCJFCh5HcmFwaFFMRmVkZXJhdGlvbkNvbmZpZ3VyYXRpb24SDwoHZW5hYmxlZBgBIAEoCBISCgpzZXJ2aWNlU2RsGAIgASgJIh0KDkludGVybmVkU3RyaW5nEgsKA2tleRgBIAEoCSI4Cg9TaW5nbGVUeXBlRmllbGQSEQoJdHlwZV9uYW1lGAEgASgJEhIKCmZpZWxkX25hbWUYAiABKAkiPgoaU3Vic2NyaXB0aW9uRmllbGRDb25kaXRpb24SEgoKZmllbGRfcGF0aBgBIAMoCRIMCgRqc29uGAIgASgJIqMCChtTdWJzY3JpcHRpb25GaWx0ZXJDb25kaXRpb24SOgoDYW5kGAEgAygLMi0ud2cuY29zbW8ubm9kZS52MS5TdWJzY3JpcHRpb25GaWx0ZXJDb25kaXRpb24SPQoCaW4YAiABKAsyLC53Zy5jb3Ntby5ub2RlLnYxLlN1YnNjcmlwdGlvbkZpZWxkQ29uZGl0aW9uSACIAQESPwoDbm90GAMgASgLMi0ud2cuY29zbW8ubm9kZS52MS5TdWJzY3JpcHRpb25GaWx0ZXJDb25kaXRpb25IAYgBARI5CgJvchgEIAMoCzItLndnLmNvc21vLm5vZGUudjEuU3Vic2NyaXB0aW9uRmlsdGVyQ29uZGl0aW9uQgUKA19pbkIGCgRfbm90IkgKFUNhY2hlV2FybWVyT3BlcmF0aW9ucxIvCgpvcGVyYXRpb25zGAEgAygLMhsud2cuY29zbW8ubm9kZS52MS5PcGVyYXRpb24ibgoJT3BlcmF0aW9uEjMKB3JlcXVlc3QYASABKAsyIi53Zy5jb3Ntby5ub2RlLnYxLk9wZXJhdGlvblJlcXVlc3QSLAoGY2xpZW50GAIgASgLMhwud2cuY29zbW8ubm9kZS52MS5DbGllbnRJbmZvImoKEE9wZXJhdGlvblJlcXVlc3QSFgoOb3BlcmF0aW9uX25hbWUYASABKAkSDQoFcXVlcnkYAiABKAkSLwoKZXh0ZW5zaW9ucxgDIAEoCzIbLndnLmNvc21vLm5vZGUudjEuRXh0ZW5zaW9uIkYKCUV4dGVuc2lvbhI5Cg9wZXJzaXN0ZWRfcXVlcnkYASABKAsyIC53Zy5jb3Ntby5ub2RlLnYxLlBlcnNpc3RlZFF1ZXJ5IjYKDlBlcnNpc3RlZFF1ZXJ5EhMKC3NoYTI1Nl9oYXNoGAEgASgJEg8KB3ZlcnNpb24YAiABKAUiKwoKQ2xpZW50SW5mbxIMCgRuYW1lGAEgASgJEg8KB3ZlcnNpb24YAiABKAkqQwoWU2F0aXNmaWVkT3BlcmF0aW9uVHlwZRIJCgVRVUVSWRAAEgwKCE1VVEFUSU9OEAESEAoMU1VCU0NSSVBUSU9OEAIqggEKG0FyZ3VtZW50UmVuZGVyQ29uZmlndXJhdGlvbhIbChdSRU5ERVJfQVJHVU1FTlRfREVGQVVMVBAAEiQKIFJFTkRFUl9BUkdVTUVOVF9BU19HUkFQSFFMX1ZBTFVFEAESIAocUkVOREVSX0FSR1VNRU5UX0FTX0FSUkFZX0NTVhACKjYKDkFyZ3VtZW50U291cmNlEhAKDE9CSkVDVF9GSUVMRBAAEhIKDkZJRUxEX0FSR1VNRU5UEAEqNQoORGF0YVNvdXJjZUtpbmQSCgoGU1RBVElDEAASCwoHR1JBUEhRTBABEgoKBlBVQlNVQhACKlwKCkxvb2t1cFR5cGUSGwoXTE9PS1VQX1RZUEVfVU5TUEVDSUZJRUQQABIXChNMT09LVVBfVFlQRV9SRVNPTFZFEAESGAoUTE9PS1VQX1RZUEVfUkVRVUlSRVMQAiqHAQoNT3BlcmF0aW9uVHlwZRIeChpPUEVSQVRJT05fVFlQRV9VTlNQRUNJRklFRBAAEhgKFE9QRVJBVElPTl9UWVBFX1FVRVJZEAESGwoXT1BFUkFUSU9OX1RZUEVfTVVUQVRJT04QAhIfChtPUEVSQVRJT05fVFlQRV9TVUJTQ1JJUFRJT04QAyo0CglFdmVudFR5cGUSCwoHUFVCTElTSBAAEgsKB1JFUVVFU1QQARINCglTVUJTQ1JJQkUQAiqGAQoZQ29uZmlndXJhdGlvblZhcmlhYmxlS2luZBIhCh1TVEFUSUNfQ09ORklHVVJBVElPTl9WQVJJQUJMRRAAEh4KGkVOVl9DT05GSUdVUkFUSU9OX1ZBUklBQkxFEAESJgoiUExBQ0VIT0xERVJfQ09ORklHVVJBVElPTl9WQVJJQUJMRRACKkEKCkhUVFBNZXRob2QSBwoDR0VUEAASCAoEUE9TVBABEgcKA1BVVBACEgoKBkRFTEVURRADEgsKB09QVElPTlMQBDLSAQoLTm9kZVNlcnZpY2USXwoMU2VsZlJlZ2lzdGVyEiUud2cuY29zbW8ubm9kZS52MS5TZWxmUmVnaXN0ZXJSZXF1ZXN0GiYud2cuY29zbW8ubm9kZS52MS5TZWxmUmVnaXN0ZXJSZXNwb25zZSIAEmIKDUdlbmVyYXRlUXVlcnkSJi53Zy5jb3Ntby5ub2RlLnYxLkdlbmVyYXRlUXVlcnlSZXF1ZXN0Gicud2cuY29zbW8ubm9kZS52MS5HZW5lcmF0ZVF1ZXJ5UmVzcG9uc2UiAGIGcHJvdG8z", [file_wg_cosmo_common_common]); + fileDesc("Cht3Zy9jb3Ntby9ub2RlL3YxL25vZGUucHJvdG8SEHdnLmNvc21vLm5vZGUudjEiOQoIU3ViZ3JhcGgSCgoCaWQYASABKAkSDAoEbmFtZRgCIAEoCRITCgtyb3V0aW5nX3VybBgDIAEoCSKOAgohRmVhdHVyZUZsYWdSb3V0ZXJFeGVjdXRpb25Db25maWdzEnUKG2NvbmZpZ19ieV9mZWF0dXJlX2ZsYWdfbmFtZRgBIAMoCzJQLndnLmNvc21vLm5vZGUudjEuRmVhdHVyZUZsYWdSb3V0ZXJFeGVjdXRpb25Db25maWdzLkNvbmZpZ0J5RmVhdHVyZUZsYWdOYW1lRW50cnkacgocQ29uZmlnQnlGZWF0dXJlRmxhZ05hbWVFbnRyeRILCgNrZXkYASABKAkSQQoFdmFsdWUYAiABKAsyMi53Zy5jb3Ntby5ub2RlLnYxLkZlYXR1cmVGbGFnUm91dGVyRXhlY3V0aW9uQ29uZmlnOgI4ASKgAQogRmVhdHVyZUZsYWdSb3V0ZXJFeGVjdXRpb25Db25maWcSPAoNZW5naW5lX2NvbmZpZxgBIAEoCzIlLndnLmNvc21vLm5vZGUudjEuRW5naW5lQ29uZmlndXJhdGlvbhIPCgd2ZXJzaW9uGAIgASgJEi0KCXN1YmdyYXBocxgDIAMoCzIaLndnLmNvc21vLm5vZGUudjEuU3ViZ3JhcGginAIKDFJvdXRlckNvbmZpZxI8Cg1lbmdpbmVfY29uZmlnGAEgASgLMiUud2cuY29zbW8ubm9kZS52MS5FbmdpbmVDb25maWd1cmF0aW9uEg8KB3ZlcnNpb24YAiABKAkSLQoJc3ViZ3JhcGhzGAMgAygLMhoud2cuY29zbW8ubm9kZS52MS5TdWJncmFwaBJWChRmZWF0dXJlX2ZsYWdfY29uZmlncxgEIAEoCzIzLndnLmNvc21vLm5vZGUudjEuRmVhdHVyZUZsYWdSb3V0ZXJFeGVjdXRpb25Db25maWdzSACIAQESHQoVY29tcGF0aWJpbGl0eV92ZXJzaW9uGAUgASgJQhcKFV9mZWF0dXJlX2ZsYWdfY29uZmlncyJbCghSZXNwb25zZRItCgRjb2RlGAEgASgOMh8ud2cuY29zbW8uY29tbW9uLkVudW1TdGF0dXNDb2RlEhQKB2RldGFpbHMYAiABKAlIAIgBAUIKCghfZGV0YWlscyIvCg5SZXNwb25zZVN0YXR1cxIMCgRjb2RlGAEgASgFEg8KB21lc3NhZ2UYAiABKAkiZQoQUmVnaXN0cmF0aW9uSW5mbxI3Cg5hY2NvdW50X2xpbWl0cxgBIAEoCzIfLndnLmNvc21vLm5vZGUudjEuQWNjb3VudExpbWl0cxIYChBncmFwaF9wdWJsaWNfa2V5GAIgASgJIiwKDUFjY291bnRMaW1pdHMSGwoTdHJhY2Vfc2FtcGxpbmdfcmF0ZRgBIAEoAiIVChNTZWxmUmVnaXN0ZXJSZXF1ZXN0IpwBChRTZWxmUmVnaXN0ZXJSZXNwb25zZRIsCghyZXNwb25zZRgBIAEoCzIaLndnLmNvc21vLm5vZGUudjEuUmVzcG9uc2USQQoQcmVnaXN0cmF0aW9uSW5mbxgCIAEoCzIiLndnLmNvc21vLm5vZGUudjEuUmVnaXN0cmF0aW9uSW5mb0gAiAEBQhMKEV9yZWdpc3RyYXRpb25JbmZvIjcKFEdlbmVyYXRlUXVlcnlSZXF1ZXN0Eg8KB3ZlcnNpb24YASABKAkSDgoGcHJvbXB0GAIgASgJIoUBChVHZW5lcmF0ZVF1ZXJ5UmVzcG9uc2USLAoIcmVzcG9uc2UYASABKAsyGi53Zy5jb3Ntby5ub2RlLnYxLlJlc3BvbnNlEjQKBXF1ZXJ5GAIgASgLMiAud2cuY29zbW8ubm9kZS52MS5TYXRpc2ZpZWRRdWVyeUgAiAEBQggKBl9xdWVyeSKrAQoOU2F0aXNmaWVkUXVlcnkSEwoLZGVzY3JpcHRpb24YASABKAkSEAoIZG9jdW1lbnQYAiABKAkSFgoOb3BlcmF0aW9uX25hbWUYAyABKAkSQAoOb3BlcmF0aW9uX3R5cGUYBCABKA4yKC53Zy5jb3Ntby5ub2RlLnYxLlNhdGlzZmllZE9wZXJhdGlvblR5cGUSGAoQdmFyaWFibGVzX3NjaGVtYRgFIAEoCSLkAwoTRW5naW5lQ29uZmlndXJhdGlvbhIcChRkZWZhdWx0Rmx1c2hJbnRlcnZhbBgBIAEoAxJMChlkYXRhc291cmNlX2NvbmZpZ3VyYXRpb25zGAIgAygLMikud2cuY29zbW8ubm9kZS52MS5EYXRhU291cmNlQ29uZmlndXJhdGlvbhJCChRmaWVsZF9jb25maWd1cmF0aW9ucxgDIAMoCzIkLndnLmNvc21vLm5vZGUudjEuRmllbGRDb25maWd1cmF0aW9uEhUKDWdyYXBocWxTY2hlbWEYBCABKAkSQAoTdHlwZV9jb25maWd1cmF0aW9ucxgFIAMoCzIjLndnLmNvc21vLm5vZGUudjEuVHlwZUNvbmZpZ3VyYXRpb24SUAoOc3RyaW5nX3N0b3JhZ2UYBiADKAsyOC53Zy5jb3Ntby5ub2RlLnYxLkVuZ2luZUNvbmZpZ3VyYXRpb24uU3RyaW5nU3RvcmFnZUVudHJ5EiIKFWdyYXBocWxfY2xpZW50X3NjaGVtYRgHIAEoCUgAiAEBGjQKElN0cmluZ1N0b3JhZ2VFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBQhgKFl9ncmFwaHFsX2NsaWVudF9zY2hlbWEixgcKF0RhdGFTb3VyY2VDb25maWd1cmF0aW9uEi4KBGtpbmQYASABKA4yIC53Zy5jb3Ntby5ub2RlLnYxLkRhdGFTb3VyY2VLaW5kEi8KCnJvb3Rfbm9kZXMYAiADKAsyGy53Zy5jb3Ntby5ub2RlLnYxLlR5cGVGaWVsZBIwCgtjaGlsZF9ub2RlcxgDIAMoCzIbLndnLmNvc21vLm5vZGUudjEuVHlwZUZpZWxkEiYKHm92ZXJyaWRlX2ZpZWxkX3BhdGhfZnJvbV9hbGlhcxgEIAEoCBJCCg5jdXN0b21fZ3JhcGhxbBgFIAEoCzIqLndnLmNvc21vLm5vZGUudjEuRGF0YVNvdXJjZUN1c3RvbV9HcmFwaFFMEkAKDWN1c3RvbV9zdGF0aWMYBiABKAsyKS53Zy5jb3Ntby5ub2RlLnYxLkRhdGFTb3VyY2VDdXN0b21fU3RhdGljEjwKCmRpcmVjdGl2ZXMYByADKAsyKC53Zy5jb3Ntby5ub2RlLnYxLkRpcmVjdGl2ZUNvbmZpZ3VyYXRpb24SHwoXcmVxdWVzdF90aW1lb3V0X3NlY29uZHMYCCABKAMSCgoCaWQYCSABKAkSLQoEa2V5cxgKIAMoCzIfLndnLmNvc21vLm5vZGUudjEuUmVxdWlyZWRGaWVsZBIxCghwcm92aWRlcxgLIAMoCzIfLndnLmNvc21vLm5vZGUudjEuUmVxdWlyZWRGaWVsZBIxCghyZXF1aXJlcxgMIAMoCzIfLndnLmNvc21vLm5vZGUudjEuUmVxdWlyZWRGaWVsZBI/Cg1jdXN0b21fZXZlbnRzGA0gASgLMigud2cuY29zbW8ubm9kZS52MS5EYXRhU291cmNlQ3VzdG9tRXZlbnRzEkkKEWVudGl0eV9pbnRlcmZhY2VzGA4gAygLMi4ud2cuY29zbW8ubm9kZS52MS5FbnRpdHlJbnRlcmZhY2VDb25maWd1cmF0aW9uEkkKEWludGVyZmFjZV9vYmplY3RzGA8gAygLMi4ud2cuY29zbW8ubm9kZS52MS5FbnRpdHlJbnRlcmZhY2VDb25maWd1cmF0aW9uEj8KEmNvc3RfY29uZmlndXJhdGlvbhgQIAEoCzIjLndnLmNvc21vLm5vZGUudjEuQ29zdENvbmZpZ3VyYXRpb24SUgocZW50aXR5X2NhY2hpbmdfY29uZmlndXJhdGlvbhgRIAEoCzIsLndnLmNvc21vLm5vZGUudjEuRW50aXR5Q2FjaGluZ0NvbmZpZ3VyYXRpb24imwIKGkVudGl0eUNhY2hpbmdDb25maWd1cmF0aW9uEk8KG2VudGl0eV9jYWNoZV9jb25maWd1cmF0aW9ucxgBIAMoCzIqLndnLmNvc21vLm5vZGUudjEuRW50aXR5Q2FjaGVDb25maWd1cmF0aW9uElcKH2NhY2hlX2ludmFsaWRhdGVfY29uZmlndXJhdGlvbnMYAiADKAsyLi53Zy5jb3Ntby5ub2RlLnYxLkNhY2hlSW52YWxpZGF0ZUNvbmZpZ3VyYXRpb24SUwodY2FjaGVfcG9wdWxhdGVfY29uZmlndXJhdGlvbnMYAyADKAsyLC53Zy5jb3Ntby5ub2RlLnYxLkNhY2hlUG9wdWxhdGVDb25maWd1cmF0aW9uIrUBChhFbnRpdHlDYWNoZUNvbmZpZ3VyYXRpb24SEQoJdHlwZV9uYW1lGAEgASgJEhcKD21heF9hZ2Vfc2Vjb25kcxgCIAEoAxIXCg9pbmNsdWRlX2hlYWRlcnMYAyABKAgSGgoScGFydGlhbF9jYWNoZV9sb2FkGAQgASgIEhMKC3NoYWRvd19tb2RlGAUgASgIEiMKG25vdF9mb3VuZF9jYWNoZV90dGxfc2Vjb25kcxgGIAEoAyJkChxDYWNoZUludmFsaWRhdGVDb25maWd1cmF0aW9uEhIKCmZpZWxkX25hbWUYASABKAkSFgoOb3BlcmF0aW9uX3R5cGUYAiABKAkSGAoQZW50aXR5X3R5cGVfbmFtZRgDIAEoCSJ7ChpDYWNoZVBvcHVsYXRlQ29uZmlndXJhdGlvbhISCgpmaWVsZF9uYW1lGAEgASgJEhYKDm9wZXJhdGlvbl90eXBlGAIgASgJEhcKD21heF9hZ2Vfc2Vjb25kcxgDIAEoAxIYChBlbnRpdHlfdHlwZV9uYW1lGAQgASgJIsADChFDb3N0Q29uZmlndXJhdGlvbhJBCg1maWVsZF93ZWlnaHRzGAEgAygLMioud2cuY29zbW8ubm9kZS52MS5GaWVsZFdlaWdodENvbmZpZ3VyYXRpb24SQAoKbGlzdF9zaXplcxgCIAMoCzIsLndnLmNvc21vLm5vZGUudjEuRmllbGRMaXN0U2l6ZUNvbmZpZ3VyYXRpb24SSgoMdHlwZV93ZWlnaHRzGAMgAygLMjQud2cuY29zbW8ubm9kZS52MS5Db3N0Q29uZmlndXJhdGlvbi5UeXBlV2VpZ2h0c0VudHJ5EmUKGmRpcmVjdGl2ZV9hcmd1bWVudF93ZWlnaHRzGAQgAygLMkEud2cuY29zbW8ubm9kZS52MS5Db3N0Q29uZmlndXJhdGlvbi5EaXJlY3RpdmVBcmd1bWVudFdlaWdodHNFbnRyeRoyChBUeXBlV2VpZ2h0c0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoBToCOAEaPwodRGlyZWN0aXZlQXJndW1lbnRXZWlnaHRzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgFOgI4ASKjAwoYRmllbGRXZWlnaHRDb25maWd1cmF0aW9uEhEKCXR5cGVfbmFtZRgBIAEoCRISCgpmaWVsZF9uYW1lGAIgASgJEhMKBndlaWdodBgDIAEoBUgAiAEBElkKEGFyZ3VtZW50X3dlaWdodHMYBCADKAsyPy53Zy5jb3Ntby5ub2RlLnYxLkZpZWxkV2VpZ2h0Q29uZmlndXJhdGlvbi5Bcmd1bWVudFdlaWdodHNFbnRyeRJsChpkaXJlY3RpdmVfYXJndW1lbnRfd2VpZ2h0cxgFIAMoCzJILndnLmNvc21vLm5vZGUudjEuRmllbGRXZWlnaHRDb25maWd1cmF0aW9uLkRpcmVjdGl2ZUFyZ3VtZW50V2VpZ2h0c0VudHJ5GjYKFEFyZ3VtZW50V2VpZ2h0c0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoBToCOAEaPwodRGlyZWN0aXZlQXJndW1lbnRXZWlnaHRzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgFOgI4AUIJCgdfd2VpZ2h0IuwBChpGaWVsZExpc3RTaXplQ29uZmlndXJhdGlvbhIRCgl0eXBlX25hbWUYASABKAkSEgoKZmllbGRfbmFtZRgCIAEoCRIZCgxhc3N1bWVkX3NpemUYAyABKAVIAIgBARIZChFzbGljaW5nX2FyZ3VtZW50cxgEIAMoCRIUCgxzaXplZF9maWVsZHMYBSADKAkSKQoccmVxdWlyZV9vbmVfc2xpY2luZ19hcmd1bWVudBgGIAEoCEgBiAEBQg8KDV9hc3N1bWVkX3NpemVCHwodX3JlcXVpcmVfb25lX3NsaWNpbmdfYXJndW1lbnQiXAoVQXJndW1lbnRDb25maWd1cmF0aW9uEgwKBG5hbWUYASABKAkSNQoLc291cmNlX3R5cGUYAiABKA4yIC53Zy5jb3Ntby5ub2RlLnYxLkFyZ3VtZW50U291cmNlIiUKBlNjb3BlcxIbChNyZXF1aXJlZF9hbmRfc2NvcGVzGAEgAygJIq8BChpBdXRob3JpemF0aW9uQ29uZmlndXJhdGlvbhIfChdyZXF1aXJlc19hdXRoZW50aWNhdGlvbhgBIAEoCBI0ChJyZXF1aXJlZF9vcl9zY29wZXMYAiADKAsyGC53Zy5jb3Ntby5ub2RlLnYxLlNjb3BlcxI6ChhyZXF1aXJlZF9vcl9zY29wZXNfYnlfb3IYAyADKAsyGC53Zy5jb3Ntby5ub2RlLnYxLlNjb3BlcyLVAgoSRmllbGRDb25maWd1cmF0aW9uEhEKCXR5cGVfbmFtZRgBIAEoCRISCgpmaWVsZF9uYW1lGAIgASgJEkgKF2FyZ3VtZW50c19jb25maWd1cmF0aW9uGAMgAygLMicud2cuY29zbW8ubm9kZS52MS5Bcmd1bWVudENvbmZpZ3VyYXRpb24SUQobYXV0aG9yaXphdGlvbl9jb25maWd1cmF0aW9uGAQgASgLMiwud2cuY29zbW8ubm9kZS52MS5BdXRob3JpemF0aW9uQ29uZmlndXJhdGlvbhJZCh1zdWJzY3JpcHRpb25fZmlsdGVyX2NvbmRpdGlvbhgFIAEoCzItLndnLmNvc21vLm5vZGUudjEuU3Vic2NyaXB0aW9uRmlsdGVyQ29uZGl0aW9uSACIAQFCIAoeX3N1YnNjcmlwdGlvbl9maWx0ZXJfY29uZGl0aW9uIjkKEVR5cGVDb25maWd1cmF0aW9uEhEKCXR5cGVfbmFtZRgBIAEoCRIRCglyZW5hbWVfdG8YAiABKAkifAoJVHlwZUZpZWxkEhEKCXR5cGVfbmFtZRgBIAEoCRITCgtmaWVsZF9uYW1lcxgCIAMoCRIcChRleHRlcm5hbF9maWVsZF9uYW1lcxgDIAMoCRIpCiFyZXF1aXJlX2ZldGNoX3JlYXNvbnNfZmllbGRfbmFtZXMYBCADKAkiOQoQRmllbGRDb29yZGluYXRlcxISCgpmaWVsZF9uYW1lGAEgASgJEhEKCXR5cGVfbmFtZRgCIAEoCSJrChFGaWVsZFNldENvbmRpdGlvbhJCChZmaWVsZF9jb29yZGluYXRlc19wYXRoGAEgAygLMiIud2cuY29zbW8ubm9kZS52MS5GaWVsZENvb3JkaW5hdGVzEhIKCmZpZWxkX3BhdGgYAiADKAkipwEKDVJlcXVpcmVkRmllbGQSEQoJdHlwZV9uYW1lGAEgASgJEhIKCmZpZWxkX25hbWUYAiABKAkSFQoNc2VsZWN0aW9uX3NldBgDIAEoCRIfChdkaXNhYmxlX2VudGl0eV9yZXNvbHZlchgEIAEoCBI3Cgpjb25kaXRpb25zGAUgAygLMiMud2cuY29zbW8ubm9kZS52MS5GaWVsZFNldENvbmRpdGlvbiJYChxFbnRpdHlJbnRlcmZhY2VDb25maWd1cmF0aW9uEhsKE2ludGVyZmFjZV90eXBlX25hbWUYASABKAkSGwoTY29uY3JldGVfdHlwZV9uYW1lcxgCIAMoCSKNBQoSRmV0Y2hDb25maWd1cmF0aW9uEjQKA3VybBgBIAEoCzInLndnLmNvc21vLm5vZGUudjEuQ29uZmlndXJhdGlvblZhcmlhYmxlEiwKBm1ldGhvZBgCIAEoDjIcLndnLmNvc21vLm5vZGUudjEuSFRUUE1ldGhvZBJACgZoZWFkZXIYAyADKAsyMC53Zy5jb3Ntby5ub2RlLnYxLkZldGNoQ29uZmlndXJhdGlvbi5IZWFkZXJFbnRyeRI1CgRib2R5GAQgASgLMicud2cuY29zbW8ubm9kZS52MS5Db25maWd1cmF0aW9uVmFyaWFibGUSNgoFcXVlcnkYBSADKAsyJy53Zy5jb3Ntby5ub2RlLnYxLlVSTFF1ZXJ5Q29uZmlndXJhdGlvbhIXCg91cmxfZW5jb2RlX2JvZHkYByABKAgSMQoEbXRscxgIIAEoCzIjLndnLmNvc21vLm5vZGUudjEuTVRMU0NvbmZpZ3VyYXRpb24SOQoIYmFzZV91cmwYCSABKAsyJy53Zy5jb3Ntby5ub2RlLnYxLkNvbmZpZ3VyYXRpb25WYXJpYWJsZRI1CgRwYXRoGAogASgLMicud2cuY29zbW8ubm9kZS52MS5Db25maWd1cmF0aW9uVmFyaWFibGUSRAoOaHR0cF9wcm94eV91cmwYCyABKAsyJy53Zy5jb3Ntby5ub2RlLnYxLkNvbmZpZ3VyYXRpb25WYXJpYWJsZUgAiAEBGksKC0hlYWRlckVudHJ5EgsKA2tleRgBIAEoCRIrCgV2YWx1ZRgCIAEoCzIcLndnLmNvc21vLm5vZGUudjEuSFRUUEhlYWRlcjoCOAFCEQoPX2h0dHBfcHJveHlfdXJsImUKFVN0YXR1c0NvZGVUeXBlTWFwcGluZxITCgtzdGF0dXNfY29kZRgBIAEoAxIRCgl0eXBlX25hbWUYAiABKAkSJAocaW5qZWN0X3N0YXR1c19jb2RlX2ludG9fYm9keRgDIAEoCCKTAwoYRGF0YVNvdXJjZUN1c3RvbV9HcmFwaFFMEjMKBWZldGNoGAEgASgLMiQud2cuY29zbW8ubm9kZS52MS5GZXRjaENvbmZpZ3VyYXRpb24SSAoMc3Vic2NyaXB0aW9uGAIgASgLMjIud2cuY29zbW8ubm9kZS52MS5HcmFwaFFMU3Vic2NyaXB0aW9uQ29uZmlndXJhdGlvbhJECgpmZWRlcmF0aW9uGAMgASgLMjAud2cuY29zbW8ubm9kZS52MS5HcmFwaFFMRmVkZXJhdGlvbkNvbmZpZ3VyYXRpb24SOQoPdXBzdHJlYW1fc2NoZW1hGAQgASgLMiAud2cuY29zbW8ubm9kZS52MS5JbnRlcm5lZFN0cmluZxJEChljdXN0b21fc2NhbGFyX3R5cGVfZmllbGRzGAYgAygLMiEud2cuY29zbW8ubm9kZS52MS5TaW5nbGVUeXBlRmllbGQSMQoEZ3JwYxgHIAEoCzIjLndnLmNvc21vLm5vZGUudjEuR1JQQ0NvbmZpZ3VyYXRpb24ikAEKEUdSUENDb25maWd1cmF0aW9uEi4KB21hcHBpbmcYASABKAsyHS53Zy5jb3Ntby5ub2RlLnYxLkdSUENNYXBwaW5nEhQKDHByb3RvX3NjaGVtYRgCIAEoCRI1CgZwbHVnaW4YAyABKAsyJS53Zy5jb3Ntby5ub2RlLnYxLlBsdWdpbkNvbmZpZ3VyYXRpb24iNwoOSW1hZ2VSZWZlcmVuY2USEgoKcmVwb3NpdG9yeRgBIAEoCRIRCglyZWZlcmVuY2UYAiABKAkiiAEKE1BsdWdpbkNvbmZpZ3VyYXRpb24SDAoEbmFtZRgBIAEoCRIPCgd2ZXJzaW9uGAIgASgJEj4KD2ltYWdlX3JlZmVyZW5jZRgDIAEoCzIgLndnLmNvc21vLm5vZGUudjEuSW1hZ2VSZWZlcmVuY2VIAIgBAUISChBfaW1hZ2VfcmVmZXJlbmNlIiMKEFNTTENvbmZpZ3VyYXRpb24SDwoHZW5hYmxlZBgBIAEoCCLbAgoLR1JQQ01hcHBpbmcSDwoHdmVyc2lvbhgBIAEoBRIPCgdzZXJ2aWNlGAIgASgJEj4KEm9wZXJhdGlvbl9tYXBwaW5ncxgDIAMoCzIiLndnLmNvc21vLm5vZGUudjEuT3BlcmF0aW9uTWFwcGluZxI4Cg9lbnRpdHlfbWFwcGluZ3MYBCADKAsyHy53Zy5jb3Ntby5ub2RlLnYxLkVudGl0eU1hcHBpbmcSPwoTdHlwZV9maWVsZF9tYXBwaW5ncxgFIAMoCzIiLndnLmNvc21vLm5vZGUudjEuVHlwZUZpZWxkTWFwcGluZxI0Cg1lbnVtX21hcHBpbmdzGAYgAygLMh0ud2cuY29zbW8ubm9kZS52MS5FbnVtTWFwcGluZxI5ChByZXNvbHZlX21hcHBpbmdzGAcgAygLMh8ud2cuY29zbW8ubm9kZS52MS5Mb29rdXBNYXBwaW5nIqkBCg1Mb29rdXBNYXBwaW5nEioKBHR5cGUYASABKA4yHC53Zy5jb3Ntby5ub2RlLnYxLkxvb2t1cFR5cGUSPAoObG9va3VwX21hcHBpbmcYAiABKAsyJC53Zy5jb3Ntby5ub2RlLnYxLkxvb2t1cEZpZWxkTWFwcGluZxILCgNycGMYAyABKAkSDwoHcmVxdWVzdBgEIAEoCRIQCghyZXNwb25zZRgFIAEoCSJZChJMb29rdXBGaWVsZE1hcHBpbmcSDAoEdHlwZRgBIAEoCRI1Cg1maWVsZF9tYXBwaW5nGAIgASgLMh4ud2cuY29zbW8ubm9kZS52MS5GaWVsZE1hcHBpbmcihgEKEE9wZXJhdGlvbk1hcHBpbmcSLQoEdHlwZRgBIAEoDjIfLndnLmNvc21vLm5vZGUudjEuT3BlcmF0aW9uVHlwZRIQCghvcmlnaW5hbBgCIAEoCRIOCgZtYXBwZWQYAyABKAkSDwoHcmVxdWVzdBgEIAEoCRIQCghyZXNwb25zZRgFIAEoCSK2AQoNRW50aXR5TWFwcGluZxIRCgl0eXBlX25hbWUYASABKAkSDAoEa2luZBgCIAEoCRILCgNrZXkYAyABKAkSCwoDcnBjGAQgASgJEg8KB3JlcXVlc3QYBSABKAkSEAoIcmVzcG9uc2UYBiABKAkSRwoXcmVxdWlyZWRfZmllbGRfbWFwcGluZ3MYByADKAsyJi53Zy5jb3Ntby5ub2RlLnYxLlJlcXVpcmVkRmllbGRNYXBwaW5nIn0KFFJlcXVpcmVkRmllbGRNYXBwaW5nEjUKDWZpZWxkX21hcHBpbmcYASABKAsyHi53Zy5jb3Ntby5ub2RlLnYxLkZpZWxkTWFwcGluZxILCgNycGMYAiABKAkSDwoHcmVxdWVzdBgDIAEoCRIQCghyZXNwb25zZRgEIAEoCSJYChBUeXBlRmllbGRNYXBwaW5nEgwKBHR5cGUYASABKAkSNgoOZmllbGRfbWFwcGluZ3MYAiADKAsyHi53Zy5jb3Ntby5ub2RlLnYxLkZpZWxkTWFwcGluZyJuCgxGaWVsZE1hcHBpbmcSEAoIb3JpZ2luYWwYASABKAkSDgoGbWFwcGVkGAIgASgJEjwKEWFyZ3VtZW50X21hcHBpbmdzGAMgAygLMiEud2cuY29zbW8ubm9kZS52MS5Bcmd1bWVudE1hcHBpbmciMwoPQXJndW1lbnRNYXBwaW5nEhAKCG9yaWdpbmFsGAEgASgJEg4KBm1hcHBlZBgCIAEoCSJPCgtFbnVtTWFwcGluZxIMCgR0eXBlGAEgASgJEjIKBnZhbHVlcxgCIAMoCzIiLndnLmNvc21vLm5vZGUudjEuRW51bVZhbHVlTWFwcGluZyI0ChBFbnVtVmFsdWVNYXBwaW5nEhAKCG9yaWdpbmFsGAEgASgJEg4KBm1hcHBlZBgCIAEoCSJqChdOYXRzU3RyZWFtQ29uZmlndXJhdGlvbhIVCg1jb25zdW1lcl9uYW1lGAEgASgJEhMKC3N0cmVhbV9uYW1lGAIgASgJEiMKG2NvbnN1bWVyX2luYWN0aXZlX3RocmVzaG9sZBgDIAEoBSLDAQoWTmF0c0V2ZW50Q29uZmlndXJhdGlvbhJOChplbmdpbmVfZXZlbnRfY29uZmlndXJhdGlvbhgBIAEoCzIqLndnLmNvc21vLm5vZGUudjEuRW5naW5lRXZlbnRDb25maWd1cmF0aW9uEhAKCHN1YmplY3RzGAIgAygJEkcKFHN0cmVhbV9jb25maWd1cmF0aW9uGAMgASgLMikud2cuY29zbW8ubm9kZS52MS5OYXRzU3RyZWFtQ29uZmlndXJhdGlvbiJ5ChdLYWZrYUV2ZW50Q29uZmlndXJhdGlvbhJOChplbmdpbmVfZXZlbnRfY29uZmlndXJhdGlvbhgBIAEoCzIqLndnLmNvc21vLm5vZGUudjEuRW5naW5lRXZlbnRDb25maWd1cmF0aW9uEg4KBnRvcGljcxgCIAMoCSJ7ChdSZWRpc0V2ZW50Q29uZmlndXJhdGlvbhJOChplbmdpbmVfZXZlbnRfY29uZmlndXJhdGlvbhgBIAEoCzIqLndnLmNvc21vLm5vZGUudjEuRW5naW5lRXZlbnRDb25maWd1cmF0aW9uEhAKCGNoYW5uZWxzGAIgAygJIoEBChhFbmdpbmVFdmVudENvbmZpZ3VyYXRpb24SEwoLcHJvdmlkZXJfaWQYASABKAkSKQoEdHlwZRgCIAEoDjIbLndnLmNvc21vLm5vZGUudjEuRXZlbnRUeXBlEhEKCXR5cGVfbmFtZRgDIAEoCRISCgpmaWVsZF9uYW1lGAQgASgJIsQBChZEYXRhU291cmNlQ3VzdG9tRXZlbnRzEjYKBG5hdHMYASADKAsyKC53Zy5jb3Ntby5ub2RlLnYxLk5hdHNFdmVudENvbmZpZ3VyYXRpb24SOAoFa2Fma2EYAiADKAsyKS53Zy5jb3Ntby5ub2RlLnYxLkthZmthRXZlbnRDb25maWd1cmF0aW9uEjgKBXJlZGlzGAMgAygLMikud2cuY29zbW8ubm9kZS52MS5SZWRpc0V2ZW50Q29uZmlndXJhdGlvbiJQChdEYXRhU291cmNlQ3VzdG9tX1N0YXRpYxI1CgRkYXRhGAEgASgLMicud2cuY29zbW8ubm9kZS52MS5Db25maWd1cmF0aW9uVmFyaWFibGUi5QEKFUNvbmZpZ3VyYXRpb25WYXJpYWJsZRI5CgRraW5kGAEgASgOMisud2cuY29zbW8ubm9kZS52MS5Db25maWd1cmF0aW9uVmFyaWFibGVLaW5kEh8KF3N0YXRpY192YXJpYWJsZV9jb250ZW50GAIgASgJEiEKGWVudmlyb25tZW50X3ZhcmlhYmxlX25hbWUYAyABKAkSKgoiZW52aXJvbm1lbnRfdmFyaWFibGVfZGVmYXVsdF92YWx1ZRgEIAEoCRIhChlwbGFjZWhvbGRlcl92YXJpYWJsZV9uYW1lGAUgASgJIkMKFkRpcmVjdGl2ZUNvbmZpZ3VyYXRpb24SFgoOZGlyZWN0aXZlX25hbWUYASABKAkSEQoJcmVuYW1lX3RvGAIgASgJIjQKFVVSTFF1ZXJ5Q29uZmlndXJhdGlvbhIMCgRuYW1lGAEgASgJEg0KBXZhbHVlGAIgASgJIkUKCkhUVFBIZWFkZXISNwoGdmFsdWVzGAEgAygLMicud2cuY29zbW8ubm9kZS52MS5Db25maWd1cmF0aW9uVmFyaWFibGUinAEKEU1UTFNDb25maWd1cmF0aW9uEjQKA2tleRgBIAEoCzInLndnLmNvc21vLm5vZGUudjEuQ29uZmlndXJhdGlvblZhcmlhYmxlEjUKBGNlcnQYAiABKAsyJy53Zy5jb3Ntby5ub2RlLnYxLkNvbmZpZ3VyYXRpb25WYXJpYWJsZRIaChJpbnNlY3VyZVNraXBWZXJpZnkYAyABKAgixQIKIEdyYXBoUUxTdWJzY3JpcHRpb25Db25maWd1cmF0aW9uEg8KB2VuYWJsZWQYASABKAgSNAoDdXJsGAIgASgLMicud2cuY29zbW8ubm9kZS52MS5Db25maWd1cmF0aW9uVmFyaWFibGUSEwoGdXNlU1NFGAMgASgISACIAQESQwoIcHJvdG9jb2wYBCABKA4yLC53Zy5jb3Ntby5jb21tb24uR3JhcGhRTFN1YnNjcmlwdGlvblByb3RvY29sSAGIAQESTwoUd2Vic29ja2V0U3VicHJvdG9jb2wYBSABKA4yLC53Zy5jb3Ntby5jb21tb24uR3JhcGhRTFdlYnNvY2tldFN1YnByb3RvY29sSAKIAQFCCQoHX3VzZVNTRUILCglfcHJvdG9jb2xCFwoVX3dlYnNvY2tldFN1YnByb3RvY29sIkUKHkdyYXBoUUxGZWRlcmF0aW9uQ29uZmlndXJhdGlvbhIPCgdlbmFibGVkGAEgASgIEhIKCnNlcnZpY2VTZGwYAiABKAkiHQoOSW50ZXJuZWRTdHJpbmcSCwoDa2V5GAEgASgJIjgKD1NpbmdsZVR5cGVGaWVsZBIRCgl0eXBlX25hbWUYASABKAkSEgoKZmllbGRfbmFtZRgCIAEoCSI+ChpTdWJzY3JpcHRpb25GaWVsZENvbmRpdGlvbhISCgpmaWVsZF9wYXRoGAEgAygJEgwKBGpzb24YAiABKAkiowIKG1N1YnNjcmlwdGlvbkZpbHRlckNvbmRpdGlvbhI6CgNhbmQYASADKAsyLS53Zy5jb3Ntby5ub2RlLnYxLlN1YnNjcmlwdGlvbkZpbHRlckNvbmRpdGlvbhI9CgJpbhgCIAEoCzIsLndnLmNvc21vLm5vZGUudjEuU3Vic2NyaXB0aW9uRmllbGRDb25kaXRpb25IAIgBARI/CgNub3QYAyABKAsyLS53Zy5jb3Ntby5ub2RlLnYxLlN1YnNjcmlwdGlvbkZpbHRlckNvbmRpdGlvbkgBiAEBEjkKAm9yGAQgAygLMi0ud2cuY29zbW8ubm9kZS52MS5TdWJzY3JpcHRpb25GaWx0ZXJDb25kaXRpb25CBQoDX2luQgYKBF9ub3QiSAoVQ2FjaGVXYXJtZXJPcGVyYXRpb25zEi8KCm9wZXJhdGlvbnMYASADKAsyGy53Zy5jb3Ntby5ub2RlLnYxLk9wZXJhdGlvbiJuCglPcGVyYXRpb24SMwoHcmVxdWVzdBgBIAEoCzIiLndnLmNvc21vLm5vZGUudjEuT3BlcmF0aW9uUmVxdWVzdBIsCgZjbGllbnQYAiABKAsyHC53Zy5jb3Ntby5ub2RlLnYxLkNsaWVudEluZm8iagoQT3BlcmF0aW9uUmVxdWVzdBIWCg5vcGVyYXRpb25fbmFtZRgBIAEoCRINCgVxdWVyeRgCIAEoCRIvCgpleHRlbnNpb25zGAMgASgLMhsud2cuY29zbW8ubm9kZS52MS5FeHRlbnNpb24iRgoJRXh0ZW5zaW9uEjkKD3BlcnNpc3RlZF9xdWVyeRgBIAEoCzIgLndnLmNvc21vLm5vZGUudjEuUGVyc2lzdGVkUXVlcnkiNgoOUGVyc2lzdGVkUXVlcnkSEwoLc2hhMjU2X2hhc2gYASABKAkSDwoHdmVyc2lvbhgCIAEoBSIrCgpDbGllbnRJbmZvEgwKBG5hbWUYASABKAkSDwoHdmVyc2lvbhgCIAEoCSpDChZTYXRpc2ZpZWRPcGVyYXRpb25UeXBlEgkKBVFVRVJZEAASDAoITVVUQVRJT04QARIQCgxTVUJTQ1JJUFRJT04QAiqCAQobQXJndW1lbnRSZW5kZXJDb25maWd1cmF0aW9uEhsKF1JFTkRFUl9BUkdVTUVOVF9ERUZBVUxUEAASJAogUkVOREVSX0FSR1VNRU5UX0FTX0dSQVBIUUxfVkFMVUUQARIgChxSRU5ERVJfQVJHVU1FTlRfQVNfQVJSQVlfQ1NWEAIqNgoOQXJndW1lbnRTb3VyY2USEAoMT0JKRUNUX0ZJRUxEEAASEgoORklFTERfQVJHVU1FTlQQASo1Cg5EYXRhU291cmNlS2luZBIKCgZTVEFUSUMQABILCgdHUkFQSFFMEAESCgoGUFVCU1VCEAIqXAoKTG9va3VwVHlwZRIbChdMT09LVVBfVFlQRV9VTlNQRUNJRklFRBAAEhcKE0xPT0tVUF9UWVBFX1JFU09MVkUQARIYChRMT09LVVBfVFlQRV9SRVFVSVJFUxACKocBCg1PcGVyYXRpb25UeXBlEh4KGk9QRVJBVElPTl9UWVBFX1VOU1BFQ0lGSUVEEAASGAoUT1BFUkFUSU9OX1RZUEVfUVVFUlkQARIbChdPUEVSQVRJT05fVFlQRV9NVVRBVElPThACEh8KG09QRVJBVElPTl9UWVBFX1NVQlNDUklQVElPThADKjQKCUV2ZW50VHlwZRILCgdQVUJMSVNIEAASCwoHUkVRVUVTVBABEg0KCVNVQlNDUklCRRACKoYBChlDb25maWd1cmF0aW9uVmFyaWFibGVLaW5kEiEKHVNUQVRJQ19DT05GSUdVUkFUSU9OX1ZBUklBQkxFEAASHgoaRU5WX0NPTkZJR1VSQVRJT05fVkFSSUFCTEUQARImCiJQTEFDRUhPTERFUl9DT05GSUdVUkFUSU9OX1ZBUklBQkxFEAIqQQoKSFRUUE1ldGhvZBIHCgNHRVQQABIICgRQT1NUEAESBwoDUFVUEAISCgoGREVMRVRFEAMSCwoHT1BUSU9OUxAEMtIBCgtOb2RlU2VydmljZRJfCgxTZWxmUmVnaXN0ZXISJS53Zy5jb3Ntby5ub2RlLnYxLlNlbGZSZWdpc3RlclJlcXVlc3QaJi53Zy5jb3Ntby5ub2RlLnYxLlNlbGZSZWdpc3RlclJlc3BvbnNlIgASYgoNR2VuZXJhdGVRdWVyeRImLndnLmNvc21vLm5vZGUudjEuR2VuZXJhdGVRdWVyeVJlcXVlc3QaJy53Zy5jb3Ntby5ub2RlLnYxLkdlbmVyYXRlUXVlcnlSZXNwb25zZSIAYgZwcm90bzM", [file_wg_cosmo_common_common]); /** * @generated from message wg.cosmo.node.v1.Subgraph @@ -252,9 +252,11 @@ export const SelfRegisterResponseSchema: GenMessage = /*@_ */ export type GenerateQueryRequest = Message<"wg.cosmo.node.v1.GenerateQueryRequest"> & { /** - * @generated from field: string schema_version_id = 1; + * Copied from RouterConfig.version in the active router configuration. + * + * @generated from field: string version = 1; */ - schemaVersionId: string; + version: string; /** * @generated from field: string prompt = 2; diff --git a/controlplane/src/core/bufservices/NodeService.ts b/controlplane/src/core/bufservices/NodeService.ts index 8d570875e0..2382056bc8 100644 --- a/controlplane/src/core/bufservices/NodeService.ts +++ b/controlplane/src/core/bufservices/NodeService.ts @@ -75,7 +75,7 @@ export default function (opts: RouterOptions): Partial { const config = routerConfigFromJsonString(await new Response(configBlob.stream).text()); const response = await nodeClient.generateQuery( { - schemaVersionId: config?.version, + version: config?.version, prompt: 'Return hello', }, { headers: { Authorization: `Bearer ${tokenResponse.token}` } }, diff --git a/proto/wg/cosmo/node/v1/node.proto b/proto/wg/cosmo/node/v1/node.proto index a3132951f3..476e9867f2 100644 --- a/proto/wg/cosmo/node/v1/node.proto +++ b/proto/wg/cosmo/node/v1/node.proto @@ -61,7 +61,8 @@ message SelfRegisterResponse { } message GenerateQueryRequest { - string schema_version_id = 1; + // Copied from RouterConfig.version in the active router configuration. + string version = 1; string prompt = 2; } diff --git a/router/gen/proto/wg/cosmo/node/v1/node.pb.go b/router/gen/proto/wg/cosmo/node/v1/node.pb.go index 3f961c179e..7dc9dc335a 100644 --- a/router/gen/proto/wg/cosmo/node/v1/node.pb.go +++ b/router/gen/proto/wg/cosmo/node/v1/node.pb.go @@ -1010,11 +1010,12 @@ func (x *SelfRegisterResponse) GetRegistrationInfo() *RegistrationInfo { } type GenerateQueryRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - SchemaVersionId string `protobuf:"bytes,1,opt,name=schema_version_id,json=schemaVersionId,proto3" json:"schema_version_id,omitempty"` - Prompt string `protobuf:"bytes,2,opt,name=prompt,proto3" json:"prompt,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + // Copied from RouterConfig.version in the active router configuration. + Version string `protobuf:"bytes,1,opt,name=version,proto3" json:"version,omitempty"` + Prompt string `protobuf:"bytes,2,opt,name=prompt,proto3" json:"prompt,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *GenerateQueryRequest) Reset() { @@ -1047,9 +1048,9 @@ func (*GenerateQueryRequest) Descriptor() ([]byte, []int) { return file_wg_cosmo_node_v1_node_proto_rawDescGZIP(), []int{10} } -func (x *GenerateQueryRequest) GetSchemaVersionId() string { +func (x *GenerateQueryRequest) GetVersion() string { if x != nil { - return x.SchemaVersionId + return x.Version } return "" } @@ -5183,9 +5184,9 @@ const file_wg_cosmo_node_v1_node_proto_rawDesc = "" + "\x14SelfRegisterResponse\x126\n" + "\bresponse\x18\x01 \x01(\v2\x1a.wg.cosmo.node.v1.ResponseR\bresponse\x12S\n" + "\x10registrationInfo\x18\x02 \x01(\v2\".wg.cosmo.node.v1.RegistrationInfoH\x00R\x10registrationInfo\x88\x01\x01B\x13\n" + - "\x11_registrationInfo\"Z\n" + - "\x14GenerateQueryRequest\x12*\n" + - "\x11schema_version_id\x18\x01 \x01(\tR\x0fschemaVersionId\x12\x16\n" + + "\x11_registrationInfo\"H\n" + + "\x14GenerateQueryRequest\x12\x18\n" + + "\aversion\x18\x01 \x01(\tR\aversion\x12\x16\n" + "\x06prompt\x18\x02 \x01(\tR\x06prompt\"\x96\x01\n" + "\x15GenerateQueryResponse\x126\n" + "\bresponse\x18\x01 \x01(\v2\x1a.wg.cosmo.node.v1.ResponseR\bresponse\x12;\n" + diff --git a/router/pkg/controlplane/prompttoquery/client.go b/router/pkg/controlplane/prompttoquery/client.go index c5de3675bd..d29b6aa52d 100644 --- a/router/pkg/controlplane/prompttoquery/client.go +++ b/router/pkg/controlplane/prompttoquery/client.go @@ -74,8 +74,8 @@ func New(endpoint, token string, opts ...Option) (*Client, error) { func (c *Client) GenerateQuery(ctx context.Context, schemaVersionID, prompt string) (*nodev1.GenerateQueryResponse, error) { req := connect.NewRequest(&nodev1.GenerateQueryRequest{ - SchemaVersionId: schemaVersionID, - Prompt: prompt, + Version: schemaVersionID, + Prompt: prompt, }) req.Header().Set("Authorization", fmt.Sprintf("Bearer %s", c.graphAPIToken)) diff --git a/router/pkg/controlplane/prompttoquery/client_test.go b/router/pkg/controlplane/prompttoquery/client_test.go index dc17238a12..2b3354f64c 100644 --- a/router/pkg/controlplane/prompttoquery/client_test.go +++ b/router/pkg/controlplane/prompttoquery/client_test.go @@ -50,7 +50,7 @@ func TestClientGenerateQuery(t *testing.T) { require.NoError(t, err) require.Equal(t, "Bearer graph-token", handler.authorizationHeader) - require.Equal(t, "14a1d197-7e3a-48df-88d7-a663de90527e", handler.generateQueryRequest.GetSchemaVersionId()) + require.Equal(t, "14a1d197-7e3a-48df-88d7-a663de90527e", handler.generateQueryRequest.GetVersion()) require.Equal(t, "List all employees", handler.generateQueryRequest.GetPrompt()) require.Equal(t, "GetEmployees", resp.GetQuery().GetOperationName()) } From 189412bdb919f37750022ce452b8df3a9a1d3a74 Mon Sep 17 00:00:00 2001 From: Alberto Garcia Hierro Date: Wed, 12 Aug 2026 18:14:21 +0100 Subject: [PATCH 04/14] refactor: resolve prompt schema inside service --- .../src/core/bufservices/NodeService.ts | 36 +----------------- .../src/core/services/PromptToQueryService.ts | 37 +++++++++++++++---- .../test/prompt-to-query-service.test.ts | 20 +++++++++- 3 files changed, 50 insertions(+), 43 deletions(-) diff --git a/controlplane/src/core/bufservices/NodeService.ts b/controlplane/src/core/bufservices/NodeService.ts index 2382056bc8..7b16727ece 100644 --- a/controlplane/src/core/bufservices/NodeService.ts +++ b/controlplane/src/core/bufservices/NodeService.ts @@ -2,7 +2,6 @@ import { ServiceImpl } from '@connectrpc/connect'; import { EnumStatusCode } from '@wundergraph/cosmo-connect/dist/common/common_pb'; import { NodeService, RegistrationInfo } from '@wundergraph/cosmo-connect/dist/node/v1/node_pb'; import { lru } from 'tiny-lru'; -import { validate as validateUUID } from 'uuid'; import { FederatedGraphRepository } from '../repositories/FederatedGraphRepository.js'; import { OrganizationRepository } from '../repositories/OrganizationRepository.js'; import type { PlainMessage } from '../../types/index.js'; @@ -75,39 +74,6 @@ export default function (opts: RouterOptions): Partial value.trim().length > 0), + version: z.string().refine(validateUUID), prompt: z.string().trim().min(1), }); @@ -72,7 +71,7 @@ export class PromptToQueryService { }); } - async generateQuery(schemaSDL: string, prompt: string): Promise { + async generateQuery(federatedGraphId: string, version: string, prompt: string): Promise { if (!this.serviceAddress) { // The feature doesn't seem to be configured correctly return create(GenerateQueryResponseSchema, { @@ -84,7 +83,7 @@ export class PromptToQueryService { } // Ensure that the provided parameters are valid - const parsed = validationSchema.safeParse({ schemaSDL, prompt }); + const parsed = validationSchema.safeParse({ version, prompt }); if (!parsed.success) { return create(GenerateQueryResponseSchema, { response: { @@ -105,9 +104,33 @@ export class PromptToQueryService { }); } + const fedRepo = new FederatedGraphRepository(this.logger, this.db, this.organizationId); + const federatedGraph = await fedRepo.byId(federatedGraphId); + if (!federatedGraph) { + return create(GenerateQueryResponseSchema, { + response: { + code: EnumStatusCode.ERR_NOT_FOUND, + details: 'Federated graph not found', + }, + }); + } + + const schemaVersion = await fedRepo.getSdlBasedOnSchemaVersion({ + targetId: federatedGraph.targetId, + schemaVersionId: parsed.data.version, + }); + if (!schemaVersion?.sdl) { + return create(GenerateQueryResponseSchema, { + response: { + code: EnumStatusCode.ERR_NOT_FOUND, + details: 'Schema version not found for this federated graph', + }, + }); + } + // Invoke the `prompt to query` service try { - const indexId = await this.ensureIndex(parsed.data.schemaSDL); + const indexId = await this.ensureIndex(schemaVersion.sdl); const response = await this.#httpClient('/yoko.v1.YokoService/GenerateQuery', { method: 'POST', headers: { 'Content-Type': 'application/json' }, diff --git a/controlplane/test/prompt-to-query-service.test.ts b/controlplane/test/prompt-to-query-service.test.ts index 225282dbf8..270f8bf399 100644 --- a/controlplane/test/prompt-to-query-service.test.ts +++ b/controlplane/test/prompt-to-query-service.test.ts @@ -4,9 +4,11 @@ import { http, HttpResponse } from 'msw'; import { setupServer } from 'msw/node'; import { afterAll, afterEach, beforeAll, describe, expect, test, vi } from 'vitest'; import { EnumStatusCode } from '@wundergraph/cosmo-connect/dist/common/common_pb'; +import { FederatedGraphRepository } from '../src/core/repositories/FederatedGraphRepository.js'; import { OrganizationRepository } from '../src/core/repositories/OrganizationRepository.js'; import { PromptToQueryService } from '../src/core/services/PromptToQueryService.js'; import * as schema from '../src/db/schema.js'; +import type { FederatedGraphDTO } from '../src/types/index.js'; describe('PromptToQueryService', () => { const yokoURL = 'http://yoko.test'; @@ -51,6 +53,13 @@ describe('PromptToQueryService', () => { id: 'prompt-to-query', enabled: true, }); + const byId = vi + .spyOn(FederatedGraphRepository.prototype, 'byId') + .mockResolvedValue({ targetId: 'target-id' } as FederatedGraphDTO); + const getSdl = vi.spyOn(FederatedGraphRepository.prototype, 'getSdlBasedOnSchemaVersion').mockResolvedValue({ + sdl: schemaSDL, + clientSchema: schemaSDL, + }); const service = new PromptToQueryService( {} as PostgresJsDatabase, @@ -59,9 +68,18 @@ describe('PromptToQueryService', () => { 'organization-id', undefined, ); - const response = await service.generateQuery(schemaSDL, 'List all employees'); + const response = await service.generateQuery( + 'graph-id', + '14a1d197-7e3a-48df-88d7-a663de90527e', + 'List all employees', + ); expect(ensureIndexRequest).toEqual({ sdl: schemaSDL }); + expect(byId).toHaveBeenCalledWith('graph-id'); + expect(getSdl).toHaveBeenCalledWith({ + targetId: 'target-id', + schemaVersionId: '14a1d197-7e3a-48df-88d7-a663de90527e', + }); expect(generateQueryRequest).toEqual({ indexId: 'opaque-yoko-index-id', prompt: 'List all employees', From 25cbbeede207e1a9fa6670938e088ac78f594bad Mon Sep 17 00:00:00 2001 From: Alberto Garcia Hierro Date: Wed, 12 Aug 2026 19:44:43 +0100 Subject: [PATCH 05/14] style(router): clean up prompt-to-query client --- router/pkg/controlplane/prompttoquery/client.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/router/pkg/controlplane/prompttoquery/client.go b/router/pkg/controlplane/prompttoquery/client.go index d29b6aa52d..b39c6fdba7 100644 --- a/router/pkg/controlplane/prompttoquery/client.go +++ b/router/pkg/controlplane/prompttoquery/client.go @@ -27,7 +27,7 @@ type Client struct { clientTimeout time.Duration } -func New(endpoint, token string, opts ...Option) (*Client, error) { +func New(endpoint string, token string, opts ...Option) (*Client, error) { if endpoint == "" { return nil, fmt.Errorf("controlplane endpoint is required for prompt to query") } @@ -77,7 +77,7 @@ func (c *Client) GenerateQuery(ctx context.Context, schemaVersionID, prompt stri Version: schemaVersionID, Prompt: prompt, }) - req.Header().Set("Authorization", fmt.Sprintf("Bearer %s", c.graphAPIToken)) + req.Header().Set("Authorization", "Bearer "+c.graphAPIToken) resp, err := c.nodeServiceClient.GenerateQuery(ctx, req) if err != nil { From 4fb96f9ca09ff8b97974026eef0b594045ed17f6 Mon Sep 17 00:00:00 2001 From: Alberto Garcia Hierro Date: Wed, 12 Aug 2026 19:45:01 +0100 Subject: [PATCH 06/14] test(router): remove prompt-to-query client test --- .../controlplane/prompttoquery/client_test.go | 56 ------------------- 1 file changed, 56 deletions(-) delete mode 100644 router/pkg/controlplane/prompttoquery/client_test.go diff --git a/router/pkg/controlplane/prompttoquery/client_test.go b/router/pkg/controlplane/prompttoquery/client_test.go deleted file mode 100644 index 2b3354f64c..0000000000 --- a/router/pkg/controlplane/prompttoquery/client_test.go +++ /dev/null @@ -1,56 +0,0 @@ -package prompttoquery - -import ( - "context" - "net/http" - "net/http/httptest" - "testing" - - "connectrpc.com/connect" - "github.com/stretchr/testify/require" - nodev1 "github.com/wundergraph/cosmo/router/gen/proto/wg/cosmo/node/v1" - "github.com/wundergraph/cosmo/router/gen/proto/wg/cosmo/node/v1/nodev1connect" - brotli "go.withmatt.com/connect-brotli" -) - -type testNodeServiceHandler struct { - generateQueryRequest *nodev1.GenerateQueryRequest - authorizationHeader string -} - -func (h *testNodeServiceHandler) SelfRegister(context.Context, *connect.Request[nodev1.SelfRegisterRequest]) (*connect.Response[nodev1.SelfRegisterResponse], error) { - return connect.NewResponse(&nodev1.SelfRegisterResponse{}), nil -} - -func (h *testNodeServiceHandler) GenerateQuery(_ context.Context, req *connect.Request[nodev1.GenerateQueryRequest]) (*connect.Response[nodev1.GenerateQueryResponse], error) { - h.generateQueryRequest = req.Msg - h.authorizationHeader = req.Header().Get("Authorization") - - return connect.NewResponse(&nodev1.GenerateQueryResponse{ - Response: &nodev1.Response{}, - Query: &nodev1.SatisfiedQuery{ - Document: "query GetEmployees { employees { id } }", - OperationName: "GetEmployees", - }, - }), nil -} - -func TestClientGenerateQuery(t *testing.T) { - handler := &testNodeServiceHandler{} - path, nodeHandler := nodev1connect.NewNodeServiceHandler(handler, brotli.WithCompression()) - mux := http.NewServeMux() - mux.Handle(path, nodeHandler) - server := httptest.NewServer(mux) - t.Cleanup(server.Close) - - client, err := New(server.URL, "graph-token") - require.NoError(t, err) - - resp, err := client.GenerateQuery(t.Context(), "14a1d197-7e3a-48df-88d7-a663de90527e", "List all employees") - - require.NoError(t, err) - require.Equal(t, "Bearer graph-token", handler.authorizationHeader) - require.Equal(t, "14a1d197-7e3a-48df-88d7-a663de90527e", handler.generateQueryRequest.GetVersion()) - require.Equal(t, "List all employees", handler.generateQueryRequest.GetPrompt()) - require.Equal(t, "GetEmployees", resp.GetQuery().GetOperationName()) -} From 116737f6d43ed40d52ee4d510b8e0c36d06452d3 Mon Sep 17 00:00:00 2001 From: Alberto Garcia Hierro Date: Wed, 12 Aug 2026 19:45:56 +0100 Subject: [PATCH 07/14] test(controlplane): remove stacked prompt-to-query tests --- .../test/prompt-to-query-service.test.ts | 90 ---------------- controlplane/test/prompt-to-query.test.ts | 101 ------------------ controlplane/test/test-util.ts | 3 - 3 files changed, 194 deletions(-) delete mode 100644 controlplane/test/prompt-to-query-service.test.ts delete mode 100644 controlplane/test/prompt-to-query.test.ts diff --git a/controlplane/test/prompt-to-query-service.test.ts b/controlplane/test/prompt-to-query-service.test.ts deleted file mode 100644 index 270f8bf399..0000000000 --- a/controlplane/test/prompt-to-query-service.test.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { PostgresJsDatabase } from 'drizzle-orm/postgres-js'; -import { pino } from 'pino'; -import { http, HttpResponse } from 'msw'; -import { setupServer } from 'msw/node'; -import { afterAll, afterEach, beforeAll, describe, expect, test, vi } from 'vitest'; -import { EnumStatusCode } from '@wundergraph/cosmo-connect/dist/common/common_pb'; -import { FederatedGraphRepository } from '../src/core/repositories/FederatedGraphRepository.js'; -import { OrganizationRepository } from '../src/core/repositories/OrganizationRepository.js'; -import { PromptToQueryService } from '../src/core/services/PromptToQueryService.js'; -import * as schema from '../src/db/schema.js'; -import type { FederatedGraphDTO } from '../src/types/index.js'; - -describe('PromptToQueryService', () => { - const yokoURL = 'http://yoko.test'; - const mockServer = setupServer(); - - beforeAll(() => mockServer.listen({ onUnhandledRequest: 'error' })); - afterEach(() => { - mockServer.resetHandlers(); - vi.restoreAllMocks(); - }); - afterAll(() => mockServer.close()); - - test('uses the index ID returned by EnsureIndex to generate the query', async () => { - const schemaSDL = 'type Query { employees: [String!]! }'; - let ensureIndexRequest: unknown; - let generateQueryRequest: unknown; - - mockServer.use( - http.post(`${yokoURL}/yoko.v1.YokoService/EnsureIndex`, async ({ request }) => { - ensureIndexRequest = await request.json(); - return HttpResponse.json({ indexId: 'opaque-yoko-index-id' }); - }), - http.post(`${yokoURL}/yoko.v1.YokoService/GenerateQuery`, async ({ request }) => { - generateQueryRequest = await request.json(); - return HttpResponse.json({ - resolution: { - queries: [ - { - description: 'Lists employees', - document: 'query ListEmployees { employees }', - operationName: 'ListEmployees', - operationType: 'query', - variablesSchema: '{}', - }, - ], - unsatisfied: [], - }, - }); - }), - ); - vi.spyOn(OrganizationRepository.prototype, 'getFeature').mockResolvedValue({ - id: 'prompt-to-query', - enabled: true, - }); - const byId = vi - .spyOn(FederatedGraphRepository.prototype, 'byId') - .mockResolvedValue({ targetId: 'target-id' } as FederatedGraphDTO); - const getSdl = vi.spyOn(FederatedGraphRepository.prototype, 'getSdlBasedOnSchemaVersion').mockResolvedValue({ - sdl: schemaSDL, - clientSchema: schemaSDL, - }); - - const service = new PromptToQueryService( - {} as PostgresJsDatabase, - pino(), - yokoURL, - 'organization-id', - undefined, - ); - const response = await service.generateQuery( - 'graph-id', - '14a1d197-7e3a-48df-88d7-a663de90527e', - 'List all employees', - ); - - expect(ensureIndexRequest).toEqual({ sdl: schemaSDL }); - expect(byId).toHaveBeenCalledWith('graph-id'); - expect(getSdl).toHaveBeenCalledWith({ - targetId: 'target-id', - schemaVersionId: '14a1d197-7e3a-48df-88d7-a663de90527e', - }); - expect(generateQueryRequest).toEqual({ - indexId: 'opaque-yoko-index-id', - prompt: 'List all employees', - }); - expect(response.response?.code).toBe(EnumStatusCode.OK); - expect(response.query?.operationName).toBe('ListEmployees'); - }); -}); diff --git a/controlplane/test/prompt-to-query.test.ts b/controlplane/test/prompt-to-query.test.ts deleted file mode 100644 index 87266ac574..0000000000 --- a/controlplane/test/prompt-to-query.test.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { http, HttpResponse } from 'msw'; -import { setupServer } from 'msw/node'; -import { afterAll, afterEach, beforeAll, describe, expect, test } from 'vitest'; -import { EnumStatusCode } from '@wundergraph/cosmo-connect/dist/common/common_pb'; -import { joinLabel, routerConfigFromJsonString } from '@wundergraph/cosmo-shared'; -import { afterAllSetup, beforeAllSetup, genID, genUniqueLabel } from '../src/core/test-util.js'; -import { - createFederatedGraph, - createThenPublishSubgraph, - DEFAULT_NAMESPACE, - DEFAULT_ROUTER_URL, - DEFAULT_SUBGRAPH_URL_ONE, - SetupTest, -} from './test-util.js'; - -let dbname = ''; - -describe('Prompt to Query RPC', () => { - const yokoURL = 'http://yoko.test'; - const mockServer = setupServer(); - - beforeAll(async () => { - mockServer.listen({ onUnhandledRequest: 'bypass' }); - dbname = await beforeAllSetup(); - }); - afterEach(() => mockServer.resetHandlers()); - afterAll(async () => { - mockServer.close(); - await afterAllSetup(dbname); - }); - - test("resolves the router schema version and generates a query with Yoko's index ID", async (testContext) => { - const ensureIndexRequests: unknown[] = []; - let generateQueryRequest: unknown; - mockServer.use( - http.post(`${yokoURL}/yoko.v1.YokoService/EnsureIndex`, async ({ request }) => { - ensureIndexRequests.push(await request.json()); - return HttpResponse.json({ indexId: 'opaque-yoko-index-id' }); - }), - http.post(`${yokoURL}/yoko.v1.YokoService/GenerateQuery`, async ({ request }) => { - generateQueryRequest = await request.json(); - return HttpResponse.json({ - resolution: { - queries: [ - { - description: 'Returns hello', - document: 'query GetHello { hello }', - operationName: 'GetHello', - operationType: 'query', - variablesSchema: '{}', - }, - ], - unsatisfied: [], - }, - }); - }), - ); - - const { client, nodeClient, server, users, blobStorage } = await SetupTest({ - dbname, - enabledFeatures: ['prompt-to-query'], - promptToQueryServiceAddress: yokoURL, - }); - testContext.onTestFinished(() => server.close()); - - const subgraphName = genID('subgraph'); - const graphName = genID('graph'); - const label = genUniqueLabel(); - await createThenPublishSubgraph( - client, - subgraphName, - DEFAULT_NAMESPACE, - 'type Query { hello: String! }', - [label], - DEFAULT_SUBGRAPH_URL_ONE, - ); - await createFederatedGraph(client, graphName, DEFAULT_NAMESPACE, [joinLabel(label)], DEFAULT_ROUTER_URL); - - const graphResponse = await client.getFederatedGraphByName({ name: graphName, namespace: DEFAULT_NAMESPACE }); - expect(graphResponse.response?.code).toBe(EnumStatusCode.OK); - const tokenResponse = await client.generateRouterToken({ fedGraphName: graphName, namespace: DEFAULT_NAMESPACE }); - expect(tokenResponse.response?.code).toBe(EnumStatusCode.OK); - - const configBlob = await blobStorage.getObject({ - key: `${users.adminAliceCompanyA.organizationId}/${graphResponse.graph?.id}/routerconfigs/latest.json`, - }); - const config = routerConfigFromJsonString(await new Response(configBlob.stream).text()); - const response = await nodeClient.generateQuery( - { - version: config?.version, - prompt: 'Return hello', - }, - { headers: { Authorization: `Bearer ${tokenResponse.token}` } }, - ); - - expect(response.response?.code).toBe(EnumStatusCode.OK); - expect(response.query?.operationName).toBe('GetHello'); - expect(ensureIndexRequests.at(-1)).toEqual({ sdl: config?.engineConfig?.graphqlSchema }); - expect(generateQueryRequest).toEqual({ indexId: 'opaque-yoko-index-id', prompt: 'Return hello' }); - }); -}); diff --git a/controlplane/test/test-util.ts b/controlplane/test/test-util.ts index fddcecc8ac..41ce9e0d00 100644 --- a/controlplane/test/test-util.ts +++ b/controlplane/test/test-util.ts @@ -86,7 +86,6 @@ export const SetupTest = async function ({ createScimKey, setupBilling, organizationId, - promptToQueryServiceAddress, }: { dbname: string; chClient?: ClickHouseClient; @@ -97,7 +96,6 @@ export const SetupTest = async function ({ plan: 'developer@1' | 'launch@1' | 'scale@1' | 'enterprise'; }; organizationId?: UUID; - promptToQueryServiceAddress?: string; }) { const log = pino(); const databaseConnectionUrl = `postgresql://postgres:changeme@localhost:5432/${dbname}`; @@ -175,7 +173,6 @@ export const SetupTest = async function ({ clientSecret: 'test', }, cdnBaseUrl: 'http://localhost:11000', - promptToQueryServiceAddress, admissionWebhookJWTSecret: 'secret', keycloakApiUrl: apiUrl, blobStorage, From c3103890b53f9298dc4631db2e107fdc4df923dc Mon Sep 17 00:00:00 2001 From: Alberto Garcia Hierro Date: Wed, 12 Aug 2026 19:47:23 +0100 Subject: [PATCH 08/14] test(router): remove generate-query error cases --- router/pkg/mcpserver/server_test.go | 67 ----------------------------- 1 file changed, 67 deletions(-) diff --git a/router/pkg/mcpserver/server_test.go b/router/pkg/mcpserver/server_test.go index 29c4e81cc8..590ad1fc23 100644 --- a/router/pkg/mcpserver/server_test.go +++ b/router/pkg/mcpserver/server_test.go @@ -3,7 +3,6 @@ package mcpserver import ( "context" "encoding/json" - "errors" "net/http" "net/http/httptest" "os" @@ -30,10 +29,6 @@ type fakePromptToQueryClient struct { prompt string } -func stringPointer(value string) *string { - return &value -} - func (f *fakePromptToQueryClient) GenerateQuery(_ context.Context, schemaVersionID, prompt string) (*nodev1.GenerateQueryResponse, error) { f.schemaVersionID = schemaVersionID f.prompt = prompt @@ -276,68 +271,6 @@ func TestGenerateQueryTool(t *testing.T) { require.Equal(t, "schema-version-second", client.schemaVersionID) } -func TestGenerateQueryToolErrors(t *testing.T) { - tests := []struct { - name string - client *fakePromptToQueryClient - schemaVersionID string - arguments string - wantText string - }{ - { - name: "blank prompt", - client: &fakePromptToQueryClient{}, - schemaVersionID: "schema-version-test", - arguments: `{"prompt":" "}`, - wantText: "prompt is required", - }, - { - name: "schema unavailable", - client: &fakePromptToQueryClient{}, - arguments: `{"prompt":"List employees"}`, - wantText: "schema version is not available", - }, - { - name: "control plane rejection", - client: &fakePromptToQueryClient{response: &nodev1.GenerateQueryResponse{ - Response: &nodev1.Response{Code: common.EnumStatusCode_ERR_UPGRADE_PLAN, Details: stringPointer("Prompt to Query not available with your current plan")}, - }}, - schemaVersionID: "schema-version-test", - arguments: `{"prompt":"List employees"}`, - wantText: "Prompt to Query not available with your current plan", - }, - { - name: "transport failure", - client: &fakePromptToQueryClient{err: errors.New("control plane unavailable")}, - schemaVersionID: "schema-version-test", - arguments: `{"prompt":"List employees"}`, - wantText: "Failed to generate a GraphQL operation", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - srv, err := NewGraphQLSchemaServer( - t.Context(), - "http://localhost:4000/graphql", - WithPromptToQueryClient(tt.client), - ) - require.NoError(t, err) - srv.setSchemaVersionID(tt.schemaVersionID) - - result, err := srv.handleGenerateQuery()(t.Context(), &mcp.CallToolRequest{ - Params: &mcp.CallToolParamsRaw{Arguments: json.RawMessage(tt.arguments)}, - }) - - require.NoError(t, err) - require.True(t, result.IsError) - textContent, ok := result.Content[0].(*mcp.TextContent) - require.True(t, ok) - require.Contains(t, textContent.Text, tt.wantText) - }) - } -} - func TestGenerateQueryScopeIncludedInProtectedResourceMetadata(t *testing.T) { srv := &GraphQLSchemaServer{ oauthConfig: &config.MCPOAuthConfiguration{ From 155e2d979dddbe90490b944d2497a960576e9415 Mon Sep 17 00:00:00 2001 From: Alberto Garcia Hierro Date: Wed, 12 Aug 2026 19:51:52 +0100 Subject: [PATCH 09/14] chore(router): remove local prompt-to-query script --- mcp-yoko-run.sh | 766 ------------------------------------------------ 1 file changed, 766 deletions(-) delete mode 100755 mcp-yoko-run.sh diff --git a/mcp-yoko-run.sh b/mcp-yoko-run.sh deleted file mode 100755 index b22414807a..0000000000 --- a/mcp-yoko-run.sh +++ /dev/null @@ -1,766 +0,0 @@ -#!/usr/bin/env bash - -set -Eeuo pipefail - -ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" - -YOKO_HOST="${MCP_YOKO_HOST:-127.0.0.1}" -YOKO_PORT="${MCP_YOKO_PORT:-5055}" -CONTROLPLANE_HOST="${MCP_YOKO_CONTROLPLANE_HOST:-127.0.0.1}" -CONTROLPLANE_PORT="${MCP_YOKO_CONTROLPLANE_PORT:-3001}" -ROUTER_HOST="${MCP_YOKO_ROUTER_HOST:-127.0.0.1}" -ROUTER_PORT="${MCP_YOKO_ROUTER_PORT:-3002}" -MCP_HOST="${MCP_YOKO_MCP_HOST:-127.0.0.1}" -MCP_PORT="${MCP_YOKO_MCP_PORT:-5025}" -GRAPH_NAME="${MCP_YOKO_GRAPH_NAME:-mcp-yoko}" -NAMESPACE="${MCP_YOKO_NAMESPACE:-default}" -ORGANIZATION_SLUG="${MCP_YOKO_ORGANIZATION_SLUG:-wundergraph}" - -FAKE_YOKO_ONLY=false -SKIP_INSTALL=false - -usage() { - cat <<'USAGE' -Usage: ./mcp-yoko-run.sh [options] - -Starts the local Cosmo infrastructure, a source-built control plane and router, -the demo subgraphs, and a fake Yoko service backed by `codex exec`. - -Options: - --fake-yoko-only Start only the fake Yoko HTTP service. - --skip-install Do not install/build the JavaScript workspaces. - -h, --help Show this help. - -Useful environment variables: - MCP_YOKO_CODEX_MODEL Optional model passed to `codex exec`. - MCP_YOKO_CODEX_REASONING Reasoning effort (default: low). - MCP_YOKO_CODEX_TIMEOUT Codex timeout in seconds (default: 180). - MCP_YOKO_PORT Fake Yoko port (default: 5055). - MCP_YOKO_MCP_PORT Router MCP port (default: 5025). - MCP_YOKO_GRAPH_NAME Federated graph name (default: mcp-yoko). - MCP_YOKO_FORCE_BUILD=1 Rebuild JavaScript workspaces even if present. - MCP_YOKO_RUN_DIR Directory in which to keep logs and run state. - -The Compose infrastructure is intentionally left running when this script -exits. Stop it later with `make infra-down`. -USAGE -} - -while (($# > 0)); do - case "$1" in - --fake-yoko-only) - FAKE_YOKO_ONLY=true - ;; - --skip-install) - SKIP_INSTALL=true - ;; - -h | --help) - usage - exit 0 - ;; - *) - echo "Unknown option: $1" >&2 - usage >&2 - exit 2 - ;; - esac - shift -done - -log() { - printf '[mcp-yoko] %s\n' "$*" -} - -die() { - printf '[mcp-yoko] ERROR: %s\n' "$*" >&2 - exit 1 -} - -require_command() { - command -v "$1" >/dev/null 2>&1 || die "Required command '$1' was not found." -} - -validate_name() { - [[ "$2" =~ ^[A-Za-z0-9._-]+$ ]] || die "$1 contains unsupported characters: $2" -} - -validate_port() { - [[ "$2" =~ ^[0-9]+$ ]] && ((10#$2 >= 1 && 10#$2 <= 65535)) || die "$1 must be a TCP port: $2" -} - -validate_name "graph name" "$GRAPH_NAME" -validate_name "namespace" "$NAMESPACE" -validate_name "organization slug" "$ORGANIZATION_SLUG" -validate_port "Yoko port" "$YOKO_PORT" -validate_port "control-plane port" "$CONTROLPLANE_PORT" -validate_port "router port" "$ROUTER_PORT" -validate_port "MCP port" "$MCP_PORT" - -require_command python3 -require_command curl -require_command codex -codex login status >/dev/null 2>&1 || die "Codex is not authenticated. Run 'codex login' and rerun the script." - -if [[ -n "${MCP_YOKO_RUN_DIR:-}" ]]; then - RUN_DIR="$MCP_YOKO_RUN_DIR" - mkdir -p "$RUN_DIR" -else - RUN_DIR="$(mktemp -d "${TMPDIR:-/tmp}/cosmo-mcp-yoko.XXXXXX")" -fi -STATE_DIR="$RUN_DIR/yoko-state" -mkdir -p "$STATE_DIR" - -PIDS=() -PROCESS_NAMES=() - -remember_process() { - PIDS+=("$1") - PROCESS_NAMES+=("$2") -} - -terminate_tree() { - local pid="$1" - local child - - if command -v pgrep >/dev/null 2>&1; then - while IFS= read -r child; do - [[ -n "$child" ]] && terminate_tree "$child" - done < <(pgrep -P "$pid" 2>/dev/null || true) - fi - - kill -TERM "$pid" 2>/dev/null || true -} - -cleanup() { - local status=$? - local index - - trap - EXIT INT TERM HUP - if ((${#PIDS[@]} > 0)); then - log "Stopping local processes..." - for ((index = ${#PIDS[@]} - 1; index >= 0; index--)); do - terminate_tree "${PIDS[$index]}" - done - for index in "${!PIDS[@]}"; do - wait "${PIDS[$index]}" 2>/dev/null || true - done - fi - - if ((status != 0)); then - log "A process failed. Recent logs:" - for index in "${!PROCESS_NAMES[@]}"; do - if [[ -f "$RUN_DIR/${PROCESS_NAMES[$index]}.log" ]]; then - printf '\n--- %s ---\n' "${PROCESS_NAMES[$index]}" - tail -n 25 "$RUN_DIR/${PROCESS_NAMES[$index]}.log" || true - fi - done - fi - - log "Logs and fake-Yoko state: $RUN_DIR" - exit "$status" -} - -trap cleanup EXIT -trap 'exit 0' INT TERM HUP - -require_free_port() { - local host="$1" - local port="$2" - local label="$3" - - python3 - "$host" "$port" <<'PY' || die "$label cannot bind to $host:$port; the port is already in use." -import socket -import sys - -host, port = sys.argv[1], int(sys.argv[2]) -family = socket.AF_INET6 if ":" in host else socket.AF_INET -with socket.socket(family, socket.SOCK_STREAM) as sock: - sock.bind((host, port)) -PY -} - -wait_for_http() { - local url="$1" - local label="$2" - local attempts="${3:-120}" - local attempt - - for ((attempt = 1; attempt <= attempts; attempt++)); do - if curl --silent --show-error --fail --max-time 2 "$url" >/dev/null 2>&1; then - return 0 - fi - sleep 1 - done - die "Timed out waiting for $label at $url" -} - -monitor_processes() { - local index - while true; do - for index in "${!PIDS[@]}"; do - if ! kill -0 "${PIDS[$index]}" 2>/dev/null; then - wait "${PIDS[$index]}" || true - die "${PROCESS_NAMES[$index]} exited unexpectedly." - fi - done - sleep 2 - done -} - -start_fake_yoko() { - require_free_port "$YOKO_HOST" "$YOKO_PORT" "fake Yoko" - log "Starting fake Yoko on http://$YOKO_HOST:$YOKO_PORT" - - python3 -u - "$YOKO_HOST" "$YOKO_PORT" "$STATE_DIR" <<'PY' >"$RUN_DIR/fake-yoko.log" 2>&1 & -import hashlib -import json -import os -from pathlib import Path -import re -import subprocess -import sys -import tempfile -import threading -from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer - -host, port, state_arg = sys.argv[1], int(sys.argv[2]), sys.argv[3] -state_dir = Path(state_arg).resolve() -index_dir = state_dir / "indexes" -index_dir.mkdir(parents=True, exist_ok=True) - -output_schema = { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "object", - "additionalProperties": False, - "required": ["resolution"], - "properties": { - "resolution": { - "type": "object", - "additionalProperties": False, - "required": ["queries", "unsatisfied"], - "properties": { - "queries": { - "type": "array", - "maxItems": 1, - "items": { - "type": "object", - "additionalProperties": False, - "required": [ - "description", - "document", - "operationName", - "operationType", - "variablesSchema", - ], - "properties": { - "description": {"type": "string"}, - "document": {"type": "string", "minLength": 1}, - "operationName": {"type": "string", "minLength": 1}, - "operationType": { - "type": "string", - "enum": ["query", "mutation", "subscription"], - }, - "variablesSchema": {"type": "string"}, - }, - }, - }, - "unsatisfied": { - "type": "array", - "maxItems": 1, - "items": { - "type": "object", - "additionalProperties": False, - "required": ["reason"], - "properties": {"reason": {"type": "string", "minLength": 1}}, - }, - }, - }, - } - }, -} -schema_path = state_dir / "codex-output-schema.json" -schema_path.write_text(json.dumps(output_schema), encoding="utf-8") - -codex_bin = os.environ.get("MCP_YOKO_CODEX_BIN", "codex") -codex_model = os.environ.get("MCP_YOKO_CODEX_MODEL", "").strip() -codex_reasoning = os.environ.get("MCP_YOKO_CODEX_REASONING", "low").strip() -codex_timeout = int(os.environ.get("MCP_YOKO_CODEX_TIMEOUT", "180")) -codex_slots = threading.BoundedSemaphore(int(os.environ.get("MCP_YOKO_CODEX_CONCURRENCY", "2"))) - - -def resolution_error(reason): - return {"resolution": {"queries": [], "unsatisfied": [{"reason": reason}]}} - - -def validate_resolution(value): - if not isinstance(value, dict) or not isinstance(value.get("resolution"), dict): - raise ValueError("Codex did not return a resolution object") - - resolution = value["resolution"] - queries = resolution.get("queries") - unsatisfied = resolution.get("unsatisfied") - if not isinstance(queries, list) or not isinstance(unsatisfied, list): - raise ValueError("Codex returned invalid resolution lists") - if len(queries) + len(unsatisfied) != 1: - raise ValueError("Codex must return exactly one query or one unsatisfied reason") - - if unsatisfied: - reason = unsatisfied[0].get("reason") if isinstance(unsatisfied[0], dict) else None - if not isinstance(reason, str) or not reason.strip(): - raise ValueError("Codex returned an empty unsatisfied reason") - return resolution_error(reason.strip()) - - query = queries[0] - if not isinstance(query, dict): - raise ValueError("Codex returned an invalid query") - for key in ("description", "document", "operationName", "operationType", "variablesSchema"): - if not isinstance(query.get(key), str): - raise ValueError(f"Codex query field {key} is not a string") - if not query["document"].strip() or not re.fullmatch(r"[_A-Za-z][_0-9A-Za-z]*", query["operationName"]): - raise ValueError("Codex returned an invalid GraphQL document or operation name") - if query["operationType"] not in ("query", "mutation", "subscription"): - raise ValueError("Codex returned an invalid operation type") - try: - variables_schema = json.loads(query["variablesSchema"] or "{}") - except json.JSONDecodeError as exc: - raise ValueError("Codex returned an invalid variables JSON Schema") from exc - if not isinstance(variables_schema, dict): - raise ValueError("Codex variables JSON Schema is not an object") - query["variablesSchema"] = json.dumps(variables_schema, separators=(",", ":")) - query["description"] = query["description"].strip() - query["document"] = query["document"].strip() - return {"resolution": {"queries": [query], "unsatisfied": []}} - - -def generate_query(index_id, prompt): - digest = index_id.removeprefix("yoko:") - schema_file = index_dir / f"{digest}.graphql" - if not re.fullmatch(r"[0-9a-fA-F]{64}", digest) or not schema_file.is_file(): - return resolution_error(f"No schema is indexed for {index_id}") - - graphql_schema = schema_file.read_text(encoding="utf-8") - instructions = f"""You generate exactly one GraphQL operation from a natural-language request. - -Use only fields and argument types present in the supplied schema. Give the operation a descriptive -GraphQL operation name. Return a JSON object matching the provided output schema. variablesSchema -must itself be a JSON-encoded JSON Schema object describing the operation variables; use \"{{}}\" -when there are no variables. If the request cannot be satisfied by the schema, return no queries and -one concise unsatisfied reason. Do not execute commands, inspect files, or explain the answer outside -the structured response. - - -{prompt} - - - -{graphql_schema} - -""" - - request_dir = Path(tempfile.mkdtemp(prefix="request-", dir=state_dir)) - output_file = request_dir / "result.json" - command = [ - codex_bin, - "exec", - "--ephemeral", - "--sandbox", - "read-only", - "--skip-git-repo-check", - "--color", - "never", - "--output-schema", - str(schema_path), - "--output-last-message", - str(output_file), - "-C", - str(request_dir), - ] - if codex_model: - command.extend(["--model", codex_model]) - if codex_reasoning: - command.extend(["--config", f"model_reasoning_effort={codex_reasoning}"]) - command.append("-") - - try: - with codex_slots: - completed = subprocess.run( - command, - input=instructions, - text=True, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - timeout=codex_timeout, - check=False, - ) - except subprocess.TimeoutExpired: - return resolution_error(f"Codex timed out after {codex_timeout} seconds") - except OSError as exc: - print(f"Could not start Codex: {exc}", flush=True) - return resolution_error("The local Codex process could not be started") - - if completed.returncode != 0 or not output_file.is_file(): - print(f"Codex failed with exit code {completed.returncode}:\n{completed.stdout}", flush=True) - return resolution_error("Codex failed to generate a query; inspect fake-yoko.log") - - try: - return validate_resolution(json.loads(output_file.read_text(encoding="utf-8"))) - except (OSError, json.JSONDecodeError, ValueError) as exc: - print(f"Invalid Codex response: {exc}\n{completed.stdout}", flush=True) - return resolution_error("Codex returned an invalid query response; inspect fake-yoko.log") - - -class Handler(BaseHTTPRequestHandler): - server_version = "FakeYokoCodex/1.0" - - def log_message(self, format_string, *args): - print(f"{self.address_string()} - {format_string % args}", flush=True) - - def send_json(self, status, value): - body = json.dumps(value, separators=(",", ":")).encode("utf-8") - self.send_response(status) - self.send_header("Content-Type", "application/json") - self.send_header("Content-Length", str(len(body))) - self.end_headers() - self.wfile.write(body) - - def read_json(self): - try: - length = int(self.headers.get("Content-Length", "0")) - except ValueError as exc: - raise ValueError("Invalid Content-Length") from exc - if length <= 0 or length > 32 * 1024 * 1024: - raise ValueError("Request body is empty or too large") - try: - return json.loads(self.rfile.read(length)) - except json.JSONDecodeError as exc: - raise ValueError("Request body is not valid JSON") from exc - - def do_GET(self): - if self.path == "/health": - self.send_json(200, {"status": "ok", "backend": "codex"}) - return - self.send_json(404, {"error": "not found"}) - - def do_POST(self): - try: - body = self.read_json() - if not isinstance(body, dict): - raise ValueError("Request body must be an object") - - if self.path == "/yoko.v1.YokoService/EnsureIndex": - sdl = body.get("sdl") - if not isinstance(sdl, str) or not sdl.strip(): - raise ValueError("sdl must be a non-empty string") - digest = hashlib.sha256(sdl.encode("utf-8")).hexdigest() - index_id = f"yoko:{digest}" - (index_dir / f"{digest}.graphql").write_text(sdl, encoding="utf-8") - print(f"Indexed {index_id} ({len(sdl)} bytes)", flush=True) - self.send_json(200, {"indexId": index_id}) - return - - if self.path == "/yoko.v1.YokoService/GenerateQuery": - index_id = body.get("indexId") - prompt = body.get("prompt") - if not isinstance(index_id, str) or not index_id: - raise ValueError("indexId must be a non-empty string") - if not isinstance(prompt, str) or not prompt.strip(): - raise ValueError("prompt must be a non-empty string") - print(f"Generating a query for {index_id}: {prompt.strip()}", flush=True) - self.send_json(200, generate_query(index_id, prompt.strip())) - return - - self.send_json(404, {"error": "not found"}) - except ValueError as exc: - self.send_json(400, {"error": str(exc)}) - except BrokenPipeError: - print("Client disconnected before the response was sent", flush=True) - - -server = ThreadingHTTPServer((host, port), Handler) -print(f"Fake Yoko listening on http://{host}:{port}", flush=True) -try: - server.serve_forever() -except KeyboardInterrupt: - pass -finally: - server.server_close() -PY - - remember_process "$!" "fake-yoko" - wait_for_http "http://$YOKO_HOST:$YOKO_PORT/health" "fake Yoko" 30 -} - -start_fake_yoko - -if [[ "$FAKE_YOKO_ONLY" == true ]]; then - log "Fake Yoko is ready. Press Ctrl-C to stop it." - log "Health: http://$YOKO_HOST:$YOKO_PORT/health" - monitor_processes -fi - -require_command make -require_command go -require_command docker - -if command -v pnpm >/dev/null 2>&1; then - PNPM=(pnpm) -elif command -v corepack >/dev/null 2>&1; then - mkdir -p "$RUN_DIR/bin" - python3 - "$RUN_DIR/bin/pnpm" <<'PY' -from pathlib import Path -import sys - -wrapper = Path(sys.argv[1]) -wrapper.write_text('#!/usr/bin/env bash\nexec corepack pnpm "$@"\n', encoding="utf-8") -wrapper.chmod(0o755) -PY - export PATH="$RUN_DIR/bin:$PATH" - PNPM=(pnpm) -else - die "pnpm was not found, and corepack is not available to provide it." -fi - -pnpm_run() { - "${PNPM[@]}" "$@" -} - -wgc() { - pnpm_run --silent --filter ./cli wgc "$@" -} - -docker info >/dev/null 2>&1 || die "Docker is installed but its daemon is not reachable. Start Docker and rerun the script." -docker compose version >/dev/null 2>&1 || die "The Docker Compose plugin is required." - -for port_spec in \ - "$CONTROLPLANE_HOST:$CONTROLPLANE_PORT:control-plane" \ - "$ROUTER_HOST:$ROUTER_PORT:router" \ - "$MCP_HOST:$MCP_PORT:MCP" \ - "127.0.0.1:4001:employees-subgraph" \ - "127.0.0.1:4002:family-subgraph" \ - "127.0.0.1:4003:hobbies-subgraph" \ - "127.0.0.1:4004:products-subgraph" \ - "127.0.0.1:4006:test-subgraph" \ - "127.0.0.1:4007:availability-subgraph" \ - "127.0.0.1:4008:mood-subgraph" \ - "127.0.0.1:4009:countries-subgraph" \ - "127.0.0.1:4010:products-feature-subgraph"; do - IFS=: read -r check_host check_port check_label <<<"$port_spec" - require_free_port "$check_host" "$check_port" "$check_label" -done - -if [[ ! -f "$ROOT_DIR/controlplane/.env" ]]; then - cp "$ROOT_DIR/controlplane/.env.example" "$ROOT_DIR/controlplane/.env" - log "Created controlplane/.env from its example." -fi -if [[ ! -f "$ROOT_DIR/cli/.env" ]]; then - cp "$ROOT_DIR/cli/.env.example" "$ROOT_DIR/cli/.env" - log "Created cli/.env from its example." -fi - -needs_workspace_build=false -if [[ ! -d "$ROOT_DIR/node_modules" || ! -d "$ROOT_DIR/connect/dist" ]]; then - needs_workspace_build=true -fi -if [[ "${MCP_YOKO_FORCE_BUILD:-0}" == "1" ]]; then - needs_workspace_build=true -fi - -if [[ "$SKIP_INSTALL" == false && "$needs_workspace_build" == true ]]; then - log "Installing and building JavaScript workspaces (first run can take several minutes)..." - ( - cd "$ROOT_DIR" - pnpm_run install - pnpm_run generate - pnpm_run -r run --filter '!studio' build - ) >"$RUN_DIR/workspace-build.log" 2>&1 || die "JavaScript workspace build failed; inspect $RUN_DIR/workspace-build.log" -elif [[ "$SKIP_INSTALL" == true ]]; then - [[ -d "$ROOT_DIR/node_modules" && -d "$ROOT_DIR/connect/dist" ]] || die "--skip-install was used, but workspace dependencies/build output are missing." - log "Skipping JavaScript install/build." -else - log "Reusing existing JavaScript workspace dependencies and build output." -fi - -log "Starting the repository's Compose development infrastructure..." -( - cd "$ROOT_DIR" - make infra-up -) >"$RUN_DIR/compose.log" 2>&1 || die "Compose startup failed; inspect $RUN_DIR/compose.log" - -wait_for_http "http://127.0.0.1:8123/ping" "ClickHouse" 180 -wait_for_http "http://127.0.0.1:8080/realms/cosmo" "Keycloak" 180 -wait_for_http "http://127.0.0.1:11000/health" "CDN" 180 - -log "Running control-plane migrations and the idempotent local seed..." -( - cd "$ROOT_DIR" - make migrate - make seed -) >"$RUN_DIR/bootstrap.log" 2>&1 || die "Migrations or seed failed; inspect $RUN_DIR/bootstrap.log" - -psql_controlplane() { - docker compose --file "$ROOT_DIR/docker-compose.yml" --profile dev exec -T postgres \ - psql -X -q -v ON_ERROR_STOP=1 -U "${POSTGRES_USER:-postgres}" -d "${POSTGRES_DB:-controlplane}" "$@" -} - -log "Enabling the prompt-to-query organization feature..." -psql_controlplane <"$RUN_DIR/feature-flag.log" -INSERT INTO organization_features (organization_id, feature, enabled) -SELECT id, 'prompt-to-query', TRUE -FROM organizations -WHERE slug = '$ORGANIZATION_SLUG' -ON CONFLICT (organization_id, feature) -DO UPDATE SET enabled = EXCLUDED.enabled; -SQL - -feature_enabled="$(psql_controlplane -Atc "SELECT COUNT(*) FROM organization_features f JOIN organizations o ON o.id = f.organization_id WHERE o.slug = '$ORGANIZATION_SLUG' AND f.feature = 'prompt-to-query' AND f.enabled = TRUE;")" -[[ "$feature_enabled" == "1" ]] || die "Could not enable prompt-to-query for organization '$ORGANIZATION_SLUG'." - -log "Starting the control plane with fake Yoko configured..." -( - cd "$ROOT_DIR/controlplane" - exec env \ - HOST="$CONTROLPLANE_HOST" \ - PORT="$CONTROLPLANE_PORT" \ - DEBUG_SQL=false \ - PROMPT_TO_QUERY_SERVICE_ENDPOINT="http://$YOKO_HOST:$YOKO_PORT" \ - "${PNPM[@]}" exec tsx src/index.ts -) >"$RUN_DIR/controlplane.log" 2>&1 & -remember_process "$!" "controlplane" -wait_for_http "http://$CONTROLPLANE_HOST:$CONTROLPLANE_PORT/health" "control plane" 180 - -export COSMO_API_KEY="cosmo_669b576aaadc10ee1ae81d9193425705" -export COSMO_API_URL="http://$CONTROLPLANE_HOST:$CONTROLPLANE_PORT" -export KC_API_URL="http://127.0.0.1:8080" -export CDN_URL="http://127.0.0.1:11000" -export DO_NOT_TRACK=1 - -target_exists() { - local target_name="$1" - local target_type="$2" - local result - - result="$(psql_controlplane -Atc "SELECT EXISTS (SELECT 1 FROM targets t JOIN namespaces n ON n.id = t.namespace_id JOIN organizations o ON o.id = t.organization_id WHERE t.name = '$target_name' AND t.type = '$target_type' AND n.name = '$NAMESPACE' AND o.slug = '$ORGANIZATION_SLUG');")" - [[ "$result" == "t" ]] -} - -log "Creating or refreshing the isolated '$GRAPH_NAME' demo graph..." -if ! target_exists "$GRAPH_NAME" "federated"; then - wgc federated-graph create "$GRAPH_NAME" \ - --namespace "$NAMESPACE" \ - --label-matcher team=A,team=B \ - --routing-url "http://$ROUTER_HOST:$ROUTER_PORT/graphql" \ - >>"$RUN_DIR/graph-setup.log" 2>&1 -else - wgc federated-graph update "$GRAPH_NAME" \ - --namespace "$NAMESPACE" \ - --label-matcher team=A,team=B \ - --routing-url "http://$ROUTER_HOST:$ROUTER_PORT/graphql" \ - >>"$RUN_DIR/graph-setup.log" 2>&1 -fi - -SUBGRAPH_NAMES=(employees family hobbies products availability mood) -SUBGRAPH_DIRS=(employees family hobbies products availability mood) -SUBGRAPH_PORTS=(4001 4002 4003 4004 4007 4008) -SUBGRAPH_TEAMS=(A A B B A B) - -for index in "${!SUBGRAPH_NAMES[@]}"; do - subgraph_name="${SUBGRAPH_NAMES[$index]}" - if ! target_exists "$subgraph_name" "subgraph"; then - wgc subgraph create "$subgraph_name" \ - --namespace "$NAMESPACE" \ - --label "team=${SUBGRAPH_TEAMS[$index]}" \ - --routing-url "http://127.0.0.1:${SUBGRAPH_PORTS[$index]}/graphql" \ - >>"$RUN_DIR/graph-setup.log" 2>&1 - else - wgc subgraph update "$subgraph_name" \ - --namespace "$NAMESPACE" \ - --label "team=${SUBGRAPH_TEAMS[$index]}" \ - --routing-url "http://127.0.0.1:${SUBGRAPH_PORTS[$index]}/graphql" \ - >>"$RUN_DIR/graph-setup.log" 2>&1 - fi - - wgc subgraph publish "$subgraph_name" \ - --namespace "$NAMESPACE" \ - --schema "$ROOT_DIR/demo/pkg/subgraphs/${SUBGRAPH_DIRS[$index]}/subgraph/schema.graphqls" \ - >>"$RUN_DIR/graph-setup.log" 2>&1 -done - -wgc federated-graph recompose "$GRAPH_NAME" \ - --namespace "$NAMESPACE" \ - --fail-on-composition-error \ - >>"$RUN_DIR/graph-setup.log" 2>&1 - -token_name="mcp-yoko-run-$(date +%s)-$$" -router_token="$(wgc router token create "$token_name" --graph-name "$GRAPH_NAME" --namespace "$NAMESPACE" --raw | tail -n 1 | tr -d '\r')" -[[ -n "$router_token" && "$router_token" != *[[:space:]]* ]] || die "The router token command did not return a usable token." - -log "Building the router and local demo subgraphs..." -( - cd "$ROOT_DIR/router" - CGO_ENABLED=0 go build -o "$RUN_DIR/router" ./cmd/router -) >"$RUN_DIR/router-build.log" 2>&1 || die "Router build failed; inspect $RUN_DIR/router-build.log" -( - cd "$ROOT_DIR/demo" - CGO_ENABLED=0 go build -o "$RUN_DIR/subgraphs" ./cmd/all -) >"$RUN_DIR/subgraphs-build.log" 2>&1 || die "Subgraph build failed; inspect $RUN_DIR/subgraphs-build.log" - -log "Starting local demo subgraphs..." -"$RUN_DIR/subgraphs" >"$RUN_DIR/subgraphs.log" 2>&1 & -remember_process "$!" "subgraphs" -wait_for_http "http://127.0.0.1:4001/" "employees subgraph" 60 - -log "Starting the source-built router and MCP server..." -( - cd "$ROOT_DIR/router" - exec env \ - CONFIG_PATH=demo.config.yaml \ - DEV_MODE=true \ - LOG_LEVEL=info \ - JSON_LOG=false \ - LISTEN_ADDR="$ROUTER_HOST:$ROUTER_PORT" \ - CONTROLPLANE_URL="http://$CONTROLPLANE_HOST:$CONTROLPLANE_PORT" \ - CDN_URL="http://127.0.0.1:11000" \ - DEFAULT_TELEMETRY_ENDPOINT="http://127.0.0.1:4318" \ - GRAPHQL_METRICS_COLLECTOR_ENDPOINT="http://127.0.0.1:4005" \ - GRAPH_API_TOKEN="$router_token" \ - ROUTER_REGISTRATION=true \ - MCP_ENABLED=true \ - MCP_GRAPH_NAME="$GRAPH_NAME" \ - MCP_SERVER_LISTEN_ADDR="$MCP_HOST:$MCP_PORT" \ - "$RUN_DIR/router" -) >"$RUN_DIR/router.log" 2>&1 & -remember_process "$!" "router" - -wait_for_http "http://$ROUTER_HOST:$ROUTER_PORT/health/ready" "router" 180 - -tools_response="$(curl --silent --show-error --fail \ - --header 'Content-Type: application/json' \ - --header 'Accept: application/json, text/event-stream' \ - --data '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' \ - "http://$MCP_HOST:$MCP_PORT/mcp")" || die "The router started, but its MCP tools/list request failed." -[[ "$tools_response" == *'generate_query'* ]] || die "MCP tools/list did not expose generate_query; inspect $RUN_DIR/router.log" - -cat < Date: Wed, 12 Aug 2026 20:13:45 +0100 Subject: [PATCH 10/14] refactor(controlplane): use zod UUID validation --- controlplane/src/core/services/PromptToQueryService.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/controlplane/src/core/services/PromptToQueryService.ts b/controlplane/src/core/services/PromptToQueryService.ts index 72ed223eb5..a8ffcea008 100644 --- a/controlplane/src/core/services/PromptToQueryService.ts +++ b/controlplane/src/core/services/PromptToQueryService.ts @@ -10,14 +10,13 @@ import { import { create } from '@bufbuild/protobuf'; import { EnumStatusCode } from '@wundergraph/cosmo-connect/dist/common/common_pb'; import * as z from 'zod'; -import { validate as validateUUID } from 'uuid'; import { traced } from '../tracing.js'; import * as schema from '../../db/schema.js'; import { FederatedGraphRepository } from '../repositories/FederatedGraphRepository.js'; import { OrganizationRepository } from '../repositories/OrganizationRepository.js'; const validationSchema = z.object({ - version: z.string().refine(validateUUID), + version: z.string().uuid(), prompt: z.string().trim().min(1), }); From 064d7d2b20807e78e803ec1d389575096adf3e00 Mon Sep 17 00:00:00 2001 From: Alberto Garcia Hierro Date: Wed, 12 Aug 2026 20:29:20 +0100 Subject: [PATCH 11/14] fix(controlplane): avoid indexing schemas during query generation --- .../src/core/services/PromptToQueryService.ts | 29 +++++++------------ 1 file changed, 10 insertions(+), 19 deletions(-) diff --git a/controlplane/src/core/services/PromptToQueryService.ts b/controlplane/src/core/services/PromptToQueryService.ts index a8ffcea008..46e743e24e 100644 --- a/controlplane/src/core/services/PromptToQueryService.ts +++ b/controlplane/src/core/services/PromptToQueryService.ts @@ -1,3 +1,4 @@ +import { createHash } from 'node:crypto'; import { PostgresJsDatabase } from 'drizzle-orm/postgres-js'; import { FastifyBaseLogger } from 'fastify'; import { type AxiosInstance, create as createHttpClient } from 'axios'; @@ -20,10 +21,6 @@ const validationSchema = z.object({ prompt: z.string().trim().min(1), }); -const ensureIndexResponseSchema = z.object({ - indexId: z.string().trim().min(1), -}); - const ptqQuerySchema = z.object({ description: z.string().optional(), document: z.string().min(1), @@ -43,6 +40,11 @@ const ptqResponseSchema = z.object({ type PtQResponse = z.infer; +// Yoko generates index IDs from the SHA-256 hash of the schema SDL using this format. +function getYokoIndexId(schemaSDL: string): string { + return `sha256:${createHash('sha256').update(schemaSDL).digest('hex')}`; +} + @traced export class PromptToQueryService { readonly #httpClient: AxiosInstance; @@ -129,12 +131,11 @@ export class PromptToQueryService { // Invoke the `prompt to query` service try { - const indexId = await this.ensureIndex(schemaVersion.sdl); const response = await this.#httpClient('/yoko.v1.YokoService/GenerateQuery', { method: 'POST', headers: { 'Content-Type': 'application/json' }, data: JSON.stringify({ - indexId, + indexId: getYokoIndexId(schemaVersion.sdl), prompt: parsed.data.prompt, }), }); @@ -175,21 +176,11 @@ export class PromptToQueryService { } // Fire and forget the schema indexation - this.ensureIndex(schema).catch((e) => this.logger.error(e, 'Failed to index schema due an unexpected error')); - } - - private async ensureIndex(schemaSDL: string): Promise { - const response = await this.#httpClient('/yoko.v1.YokoService/EnsureIndex', { + this.#httpClient('/yoko.v1.YokoService/EnsureIndex', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - data: JSON.stringify({ sdl: schemaSDL }), - }); - const parsed = ensureIndexResponseSchema.safeParse(response.data); - if (!parsed.success) { - throw new Error('It was not possible to parse the response returned by the Prompt to Query index service'); - } - - return parsed.data.indexId; + data: JSON.stringify({ sdl: schema }), + }).catch((e) => this.logger.error(e, 'Failed to index schema due an unexpected error')); } private static getOperationType(type: z.infer['operationType']): SatisfiedOperationType { From b7f1b0fccb5c0bbc85f2c1b4a9e46a4a2eb84540 Mon Sep 17 00:00:00 2001 From: Alberto Garcia Hierro Date: Mon, 17 Aug 2026 22:41:22 +0100 Subject: [PATCH 12/14] refactor(router): simplify prompt-to-query client Replace the functional options with an explicit logger constructor parameter and keep the fixed client timeout internal. Move the package under router/internal since it is only consumed by router core. --- router/core/supervisor_instance.go | 6 ++--- .../prompttoquery/client.go | 27 +++---------------- 2 files changed, 6 insertions(+), 27 deletions(-) rename router/{pkg/controlplane => internal}/prompttoquery/client.go (79%) diff --git a/router/core/supervisor_instance.go b/router/core/supervisor_instance.go index b4e5e7d474..c6f59d11b2 100644 --- a/router/core/supervisor_instance.go +++ b/router/core/supervisor_instance.go @@ -9,9 +9,9 @@ import ( "github.com/KimMachineGun/automemlimit/memlimit" "github.com/dustin/go-humanize" + "github.com/wundergraph/cosmo/router/internal/prompttoquery" "github.com/wundergraph/cosmo/router/pkg/authentication" "github.com/wundergraph/cosmo/router/pkg/config" - "github.com/wundergraph/cosmo/router/pkg/controlplane/prompttoquery" "github.com/wundergraph/cosmo/router/pkg/controlplane/selfregister" "github.com/wundergraph/cosmo/router/pkg/cors" "github.com/wundergraph/cosmo/router/pkg/logging" @@ -161,9 +161,7 @@ func newRouter(ctx context.Context, params RouterResources, additionalOptions .. } if cfg.MCP.Enabled && cfg.Graph.Token != "" { - promptToQueryClient, err := prompttoquery.New(cfg.ControlplaneURL, cfg.Graph.Token, - prompttoquery.WithLogger(logger), - ) + promptToQueryClient, err := prompttoquery.New(cfg.ControlplaneURL, cfg.Graph.Token, logger) if err != nil { return nil, fmt.Errorf("could not create prompt-to-query client: %w", err) } diff --git a/router/pkg/controlplane/prompttoquery/client.go b/router/internal/prompttoquery/client.go similarity index 79% rename from router/pkg/controlplane/prompttoquery/client.go rename to router/internal/prompttoquery/client.go index b39c6fdba7..704ac63282 100644 --- a/router/pkg/controlplane/prompttoquery/client.go +++ b/router/internal/prompttoquery/client.go @@ -14,9 +14,7 @@ import ( brotli "go.withmatt.com/connect-brotli" ) -const defaultTimeout = 15 * time.Second - -type Option func(*Client) +const clientTimeout = 15 * time.Second // Client generates GraphQL operations through the control plane. type Client struct { @@ -24,10 +22,9 @@ type Client struct { graphAPIToken string controlplaneEndpoint string logger *zap.Logger - clientTimeout time.Duration } -func New(endpoint string, token string, opts ...Option) (*Client, error) { +func New(endpoint string, token string, logger *zap.Logger) (*Client, error) { if endpoint == "" { return nil, fmt.Errorf("controlplane endpoint is required for prompt to query") } @@ -39,11 +36,7 @@ func New(endpoint string, token string, opts ...Option) (*Client, error) { c := &Client{ controlplaneEndpoint: endpoint, graphAPIToken: token, - clientTimeout: defaultTimeout, - } - - for _, opt := range opts { - opt(c) + logger: logger, } if c.logger == nil { @@ -62,7 +55,7 @@ func New(endpoint string, token string, opts ...Option) (*Client, error) { } httpClient := retryClient.StandardClient() - httpClient.Timeout = c.clientTimeout + httpClient.Timeout = clientTimeout c.nodeServiceClient = nodev1connect.NewNodeServiceClient(httpClient, c.controlplaneEndpoint, brotli.WithCompression(), @@ -86,15 +79,3 @@ func (c *Client) GenerateQuery(ctx context.Context, schemaVersionID, prompt stri return resp.Msg, nil } - -func WithLogger(logger *zap.Logger) Option { - return func(c *Client) { - c.logger = logger - } -} - -func WithClientTimeout(timeout time.Duration) Option { - return func(c *Client) { - c.clientTimeout = timeout - } -} From 561f7e2c39c65270f7e45cc893948808642fac60 Mon Sep 17 00:00:00 2001 From: Alberto Garcia Hierro Date: Mon, 17 Aug 2026 23:17:04 +0100 Subject: [PATCH 13/14] refactor(router): centralize control plane transport Move bearer authentication and retry configuration into a shared internal transport. Inject it into prompt-to-query from router setup. --- router/core/supervisor_instance.go | 8 +++- router/internal/controlplane/transport.go | 58 +++++++++++++++++++++++ router/internal/prompttoquery/client.go | 43 ++++------------- 3 files changed, 74 insertions(+), 35 deletions(-) create mode 100644 router/internal/controlplane/transport.go diff --git a/router/core/supervisor_instance.go b/router/core/supervisor_instance.go index c6f59d11b2..77aed82a71 100644 --- a/router/core/supervisor_instance.go +++ b/router/core/supervisor_instance.go @@ -9,6 +9,7 @@ import ( "github.com/KimMachineGun/automemlimit/memlimit" "github.com/dustin/go-humanize" + "github.com/wundergraph/cosmo/router/internal/controlplane" "github.com/wundergraph/cosmo/router/internal/prompttoquery" "github.com/wundergraph/cosmo/router/pkg/authentication" "github.com/wundergraph/cosmo/router/pkg/config" @@ -161,7 +162,12 @@ func newRouter(ctx context.Context, params RouterResources, additionalOptions .. } if cfg.MCP.Enabled && cfg.Graph.Token != "" { - promptToQueryClient, err := prompttoquery.New(cfg.ControlplaneURL, cfg.Graph.Token, logger) + controlplaneTransport, err := controlplane.NewTransport(cfg.Graph.Token, logger) + if err != nil { + return nil, fmt.Errorf("could not create controlplane transport: %w", err) + } + + promptToQueryClient, err := prompttoquery.New(cfg.ControlplaneURL, controlplaneTransport) if err != nil { return nil, fmt.Errorf("could not create prompt-to-query client: %w", err) } diff --git a/router/internal/controlplane/transport.go b/router/internal/controlplane/transport.go new file mode 100644 index 0000000000..6a863f19a0 --- /dev/null +++ b/router/internal/controlplane/transport.go @@ -0,0 +1,58 @@ +package controlplane + +import ( + "fmt" + "net/http" + "time" + + "github.com/hashicorp/go-retryablehttp" + "go.uber.org/zap" +) + +const ( + retryWaitMax = 15 * time.Second + retryMax = 3 +) + +type bearerAuthTransport struct { + token string + next http.RoundTripper +} + +// NewTransport creates an authenticated, retrying transport for control plane requests. +func NewTransport(token string, logger *zap.Logger) (http.RoundTripper, error) { + if token == "" { + return nil, fmt.Errorf("graph api token is required for controlplane requests") + } + + if logger == nil { + logger = zap.NewNop() + } + + retryClient := retryablehttp.NewClient() + retryClient.RetryWaitMax = retryWaitMax + retryClient.RetryMax = retryMax + retryClient.Backoff = retryablehttp.DefaultBackoff + retryClient.Logger = nil + retryClient.RequestLogHook = func(_ retryablehttp.Logger, _ *http.Request, retry int) { + if retry > 0 { + logger.Info("Retry controlplane request", zap.Int("retry", retry)) + } + } + + return newBearerAuthTransport(token, retryClient.StandardClient().Transport), nil +} + +func newBearerAuthTransport(token string, next http.RoundTripper) http.RoundTripper { + return &bearerAuthTransport{ + token: token, + next: next, + } +} + +func (t *bearerAuthTransport) RoundTrip(req *http.Request) (*http.Response, error) { + req = req.Clone(req.Context()) + req.Header.Set("Authorization", "Bearer "+t.token) + + return t.next.RoundTrip(req) +} diff --git a/router/internal/prompttoquery/client.go b/router/internal/prompttoquery/client.go index 704ac63282..31b268b773 100644 --- a/router/internal/prompttoquery/client.go +++ b/router/internal/prompttoquery/client.go @@ -7,10 +7,8 @@ import ( "time" "connectrpc.com/connect" - "github.com/hashicorp/go-retryablehttp" nodev1 "github.com/wundergraph/cosmo/router/gen/proto/wg/cosmo/node/v1" "github.com/wundergraph/cosmo/router/gen/proto/wg/cosmo/node/v1/nodev1connect" - "go.uber.org/zap" brotli "go.withmatt.com/connect-brotli" ) @@ -18,51 +16,29 @@ const clientTimeout = 15 * time.Second // Client generates GraphQL operations through the control plane. type Client struct { - nodeServiceClient nodev1connect.NodeServiceClient - graphAPIToken string - controlplaneEndpoint string - logger *zap.Logger + nodeServiceClient nodev1connect.NodeServiceClient } -func New(endpoint string, token string, logger *zap.Logger) (*Client, error) { +func New(endpoint string, transport http.RoundTripper) (*Client, error) { if endpoint == "" { return nil, fmt.Errorf("controlplane endpoint is required for prompt to query") } - if token == "" { - return nil, fmt.Errorf("graph api token is required for prompt to query") + if transport == nil { + return nil, fmt.Errorf("controlplane transport is required for prompt to query") } - c := &Client{ - controlplaneEndpoint: endpoint, - graphAPIToken: token, - logger: logger, + httpClient := &http.Client{ + Transport: transport, + Timeout: clientTimeout, } - if c.logger == nil { - c.logger = zap.NewNop() - } - - retryClient := retryablehttp.NewClient() - retryClient.RetryWaitMax = 15 * time.Second - retryClient.RetryMax = 3 - retryClient.Backoff = retryablehttp.DefaultBackoff - retryClient.Logger = nil - retryClient.RequestLogHook = func(_ retryablehttp.Logger, _ *http.Request, retry int) { - if retry > 0 { - c.logger.Info("Generate query through controlplane", zap.Int("retry", retry)) - } - } - - httpClient := retryClient.StandardClient() - httpClient.Timeout = clientTimeout - - c.nodeServiceClient = nodev1connect.NewNodeServiceClient(httpClient, c.controlplaneEndpoint, + nodeServiceClient := nodev1connect.NewNodeServiceClient(httpClient, endpoint, brotli.WithCompression(), connect.WithSendCompression(brotli.Name), ) - return c, nil + return &Client{nodeServiceClient: nodeServiceClient}, nil } func (c *Client) GenerateQuery(ctx context.Context, schemaVersionID, prompt string) (*nodev1.GenerateQueryResponse, error) { @@ -70,7 +46,6 @@ func (c *Client) GenerateQuery(ctx context.Context, schemaVersionID, prompt stri Version: schemaVersionID, Prompt: prompt, }) - req.Header().Set("Authorization", "Bearer "+c.graphAPIToken) resp, err := c.nodeServiceClient.GenerateQuery(ctx, req) if err != nil { From 8c0591b127276cf9b8d9355454659e82af7f4857 Mon Sep 17 00:00:00 2001 From: Alberto Garcia Hierro Date: Tue, 18 Aug 2026 00:22:45 +0100 Subject: [PATCH 14/14] fix(controlplane): wait for prompt-to-query index Ensure the schema index exists before generating a query and poll Yoko until the index is ready. Propagate request cancellation through indexing and generation so polling stops when the caller disconnects or reaches its deadline. --- .../src/core/bufservices/NodeService.ts | 2 +- .../src/core/services/PromptToQueryService.ts | 65 ++++++++++++++++--- 2 files changed, 58 insertions(+), 9 deletions(-) diff --git a/controlplane/src/core/bufservices/NodeService.ts b/controlplane/src/core/bufservices/NodeService.ts index 7b16727ece..9e86899b3f 100644 --- a/controlplane/src/core/bufservices/NodeService.ts +++ b/controlplane/src/core/bufservices/NodeService.ts @@ -82,7 +82,7 @@ export default function (opts: RouterOptions): Partial; -// Yoko generates index IDs from the SHA-256 hash of the schema SDL using this format. -function getYokoIndexId(schemaSDL: string): string { - return `sha256:${createHash('sha256').update(schemaSDL).digest('hex')}`; -} - @traced export class PromptToQueryService { readonly #httpClient: AxiosInstance; @@ -72,7 +77,12 @@ export class PromptToQueryService { }); } - async generateQuery(federatedGraphId: string, version: string, prompt: string): Promise { + async generateQuery( + federatedGraphId: string, + version: string, + prompt: string, + signal?: AbortSignal, + ): Promise { if (!this.serviceAddress) { // The feature doesn't seem to be configured correctly return create(GenerateQueryResponseSchema, { @@ -131,13 +141,15 @@ export class PromptToQueryService { // Invoke the `prompt to query` service try { + const indexId = await this.ensureIndex(schemaVersion.sdl, signal); const response = await this.#httpClient('/yoko.v1.YokoService/GenerateQuery', { method: 'POST', headers: { 'Content-Type': 'application/json' }, data: JSON.stringify({ - indexId: getYokoIndexId(schemaVersion.sdl), + indexId, prompt: parsed.data.prompt, }), + signal, }); const parsedResponse = ptqResponseSchema.safeParse(response.data); @@ -183,6 +195,43 @@ export class PromptToQueryService { }).catch((e) => this.logger.error(e, 'Failed to index schema due an unexpected error')); } + private async ensureIndex(schemaSDL: string, signal?: AbortSignal): Promise { + const response = await this.#httpClient('/yoko.v1.YokoService/EnsureIndex', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + data: JSON.stringify({ sdl: schemaSDL }), + signal, + }); + + let index = PromptToQueryService.parseIndexResponse(response.data); + while (index.status === 'INDEX_STATUS_INDEXING') { + await setTimeout(indexPollInterval, undefined, { signal }); + + const response = await this.#httpClient('/yoko.v1.YokoService/GetIndex', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + data: JSON.stringify({ indexId: index.indexId }), + signal, + }); + index = PromptToQueryService.parseIndexResponse(response.data); + } + + if (index.status === 'INDEX_STATUS_FAILED') { + throw new Error(`Prompt to Query index generation failed${index.error ? `: ${index.error}` : ''}`); + } + + return index.indexId; + } + + private static parseIndexResponse(response: unknown): z.infer['index'] { + const parsed = indexResponseSchema.safeParse(response); + if (!parsed.success) { + throw new Error('It was not possible to parse the response returned by the Prompt to Query index service'); + } + + return parsed.data.index; + } + private static getOperationType(type: z.infer['operationType']): SatisfiedOperationType { switch (type) { case 'query': {