From 21d7f5559bf0a292aa1b290571848168bd8b391d Mon Sep 17 00:00:00 2001 From: Jason Bowman Date: Wed, 25 Aug 2021 19:36:12 -0700 Subject: [PATCH 1/9] Hash128 is not a cryptographic hash, replace with HMAC-SHA256. Address dotnet style issues Fix trailing space --- .../Runtime/Analytics/AnalyticsUtils.cs | 34 ++++++++++++++++--- .../Runtime/Analytics/InferenceAnalytics.cs | 2 +- .../Runtime/Analytics/TrainingAnalytics.cs | 2 +- 3 files changed, 31 insertions(+), 7 deletions(-) diff --git a/com.unity.ml-agents/Runtime/Analytics/AnalyticsUtils.cs b/com.unity.ml-agents/Runtime/Analytics/AnalyticsUtils.cs index fb480b7a11..6249144ac2 100644 --- a/com.unity.ml-agents/Runtime/Analytics/AnalyticsUtils.cs +++ b/com.unity.ml-agents/Runtime/Analytics/AnalyticsUtils.cs @@ -3,17 +3,41 @@ namespace Unity.MLAgents.Analytics { + internal static class AnalyticsUtils { + /// + /// Conversion function from byte array to hex string + /// + /// + /// A byte array to be hex encoded. + private static string ToHexString(byte[] array) + { + System.Text.StringBuilder hex = new System.Text.StringBuilder(array.Length * 2); + foreach (byte b in array) + { + hex.AppendFormat("{0:x2}", b); + } + return hex.ToString(); + } + /// /// Hash a string to remove PII or secret info before sending to analytics /// - /// - /// A string containing the Hash128 of the input string. - public static string Hash(string s) + /// + /// A string containing the key to be used for HMAC encoding. + /// + /// A string containing the value to be encoded. + public static string Hash(string key, string value) { - var behaviorNameHash = Hash128.Compute(s); - return behaviorNameHash.ToString(); + string hash; + System.Text.UTF8Encoding encoder = new System.Text.UTF8Encoding(); + using (System.Security.Cryptography.HMACSHA256 hmac = new System.Security.Cryptography.HMACSHA256(encoder.GetBytes(key))) + { + Byte[] hmBytes = hmac.ComputeHash(encoder.GetBytes(value)); + hash = ToHexString(hmBytes); + } + return hash; } internal static bool s_SendEditorAnalytics = true; diff --git a/com.unity.ml-agents/Runtime/Analytics/InferenceAnalytics.cs b/com.unity.ml-agents/Runtime/Analytics/InferenceAnalytics.cs index 4286aa844d..18d5fc62f8 100644 --- a/com.unity.ml-agents/Runtime/Analytics/InferenceAnalytics.cs +++ b/com.unity.ml-agents/Runtime/Analytics/InferenceAnalytics.cs @@ -156,7 +156,7 @@ IList actuators var inferenceEvent = new InferenceEvent(); // Hash the behavior name so that there's no concern about PII or "secret" data being leaked. - inferenceEvent.BehaviorName = AnalyticsUtils.Hash(behaviorName); + inferenceEvent.BehaviorName = AnalyticsUtils.Hash(k_VendorKey, behaviorName); inferenceEvent.BarracudaModelSource = barracudaModel.IrSource; inferenceEvent.BarracudaModelVersion = barracudaModel.IrVersion; diff --git a/com.unity.ml-agents/Runtime/Analytics/TrainingAnalytics.cs b/com.unity.ml-agents/Runtime/Analytics/TrainingAnalytics.cs index fb14e1bab7..691991141b 100644 --- a/com.unity.ml-agents/Runtime/Analytics/TrainingAnalytics.cs +++ b/com.unity.ml-agents/Runtime/Analytics/TrainingAnalytics.cs @@ -213,7 +213,7 @@ public static void TrainingBehaviorInitialized(TrainingBehaviorInitializedEvent // Hash the behavior name so that there's no concern about PII or "secret" data being leaked. tbiEvent.TrainingSessionGuid = s_TrainingSessionGuid.ToString(); - tbiEvent.BehaviorName = AnalyticsUtils.Hash(tbiEvent.BehaviorName); + tbiEvent.BehaviorName = AnalyticsUtils.Hash(k_VendorKey, tbiEvent.BehaviorName); // Note - to debug, use JsonUtility.ToJson on the event. // Debug.Log( From 18df53b6479747a63b856c1f73019cfe521b110c Mon Sep 17 00:00:00 2001 From: Jason Bowman Date: Wed, 25 Aug 2021 19:42:38 -0700 Subject: [PATCH 2/9] Extend TrainingAnalytics side channel to expose configuration details Update python files to adhere to linting rules Fix typing check Simplify variable usage --- .../Runtime/Analytics/Events.cs | 2 + .../Runtime/Analytics/TrainingAnalytics.cs | 10 +- .../Runtime/Communicator/GrpcExtensions.cs | 2 + .../CommunicatorObjects/TrainingAnalytics.cs | 87 ++++++++++++++--- .../training_analytics_pb2.py | 22 ++++- .../training_analytics_pb2.pyi | 12 ++- .../training_analytics_side_channel.py | 95 ++++++++++++++++++- .../training_analytics.proto | 2 + 8 files changed, 204 insertions(+), 28 deletions(-) diff --git a/com.unity.ml-agents/Runtime/Analytics/Events.cs b/com.unity.ml-agents/Runtime/Analytics/Events.cs index f269d91436..4a34273c04 100644 --- a/com.unity.ml-agents/Runtime/Analytics/Events.cs +++ b/com.unity.ml-agents/Runtime/Analytics/Events.cs @@ -156,6 +156,7 @@ internal struct TrainingEnvironmentInitializedEvent public string TorchDeviceType; public int NumEnvironments; public int NumEnvironmentParameters; + public string RunOptions; } [Flags] @@ -188,5 +189,6 @@ internal struct TrainingBehaviorInitializedEvent public string VisualEncoder; public int NumNetworkLayers; public int NumNetworkHiddenUnits; + public string Config; } } diff --git a/com.unity.ml-agents/Runtime/Analytics/TrainingAnalytics.cs b/com.unity.ml-agents/Runtime/Analytics/TrainingAnalytics.cs index 691991141b..0ea24f0ed7 100644 --- a/com.unity.ml-agents/Runtime/Analytics/TrainingAnalytics.cs +++ b/com.unity.ml-agents/Runtime/Analytics/TrainingAnalytics.cs @@ -211,10 +211,14 @@ public static void TrainingBehaviorInitialized(TrainingBehaviorInitializedEvent return; } - // Hash the behavior name so that there's no concern about PII or "secret" data being leaked. tbiEvent.TrainingSessionGuid = s_TrainingSessionGuid.ToString(); - tbiEvent.BehaviorName = AnalyticsUtils.Hash(k_VendorKey, tbiEvent.BehaviorName); + if(tbiEvent.Config.Length == 0 || tbiEvent.BehaviorName.Length != 64) { + // Hash the behavior name if the message version is from an older version of ml-agents that doesn't do trainer-side hashing. + // We'll also, for extra safety, verify that the BehaviorName is the size of the expected SHA256 hash. + // Context: The config field was added at the same time as trainer side hashing, so messages including it should already be hashed. + tbiEvent.BehaviorName = AnalyticsUtils.Hash(k_VendorKey, tbiEvent.BehaviorName); + } // Note - to debug, use JsonUtility.ToJson on the event. // Debug.Log( // $"Would send event {k_TrainingBehaviorInitializedEventName} with body {JsonUtility.ToJson(tbiEvent, true)}" @@ -236,7 +240,7 @@ IList actuators var remotePolicyEvent = new RemotePolicyInitializedEvent(); // Hash the behavior name so that there's no concern about PII or "secret" data being leaked. - remotePolicyEvent.BehaviorName = AnalyticsUtils.Hash(behaviorName); + remotePolicyEvent.BehaviorName = AnalyticsUtils.Hash(k_VendorKey, behaviorName); remotePolicyEvent.TrainingSessionGuid = s_TrainingSessionGuid.ToString(); remotePolicyEvent.ActionSpec = EventActionSpec.FromActionSpec(actionSpec); diff --git a/com.unity.ml-agents/Runtime/Communicator/GrpcExtensions.cs b/com.unity.ml-agents/Runtime/Communicator/GrpcExtensions.cs index 760d5e6b25..e49bd876e7 100644 --- a/com.unity.ml-agents/Runtime/Communicator/GrpcExtensions.cs +++ b/com.unity.ml-agents/Runtime/Communicator/GrpcExtensions.cs @@ -501,6 +501,7 @@ internal static TrainingEnvironmentInitializedEvent ToTrainingEnvironmentInitial TorchDeviceType = inputProto.TorchDeviceType, NumEnvironments = inputProto.NumEnvs, NumEnvironmentParameters = inputProto.NumEnvironmentParameters, + RunOptions = inputProto.RunOptions, }; } @@ -530,6 +531,7 @@ internal static TrainingBehaviorInitializedEvent ToTrainingBehaviorInitializedEv VisualEncoder = inputProto.VisualEncoder, NumNetworkLayers = inputProto.NumNetworkLayers, NumNetworkHiddenUnits = inputProto.NumNetworkHiddenUnits, + Config = inputProto.Config, }; } diff --git a/com.unity.ml-agents/Runtime/Grpc/CommunicatorObjects/TrainingAnalytics.cs b/com.unity.ml-agents/Runtime/Grpc/CommunicatorObjects/TrainingAnalytics.cs index 099563e949..042357f280 100644 --- a/com.unity.ml-agents/Runtime/Grpc/CommunicatorObjects/TrainingAnalytics.cs +++ b/com.unity.ml-agents/Runtime/Grpc/CommunicatorObjects/TrainingAnalytics.cs @@ -25,28 +25,29 @@ static TrainingAnalyticsReflection() { byte[] descriptorData = global::System.Convert.FromBase64String( string.Concat( "CjttbGFnZW50c19lbnZzL2NvbW11bmljYXRvcl9vYmplY3RzL3RyYWluaW5n", - "X2FuYWx5dGljcy5wcm90bxIUY29tbXVuaWNhdG9yX29iamVjdHMi2QEKHlRy", + "X2FuYWx5dGljcy5wcm90bxIUY29tbXVuaWNhdG9yX29iamVjdHMi7gEKHlRy", "YWluaW5nRW52aXJvbm1lbnRJbml0aWFsaXplZBIYChBtbGFnZW50c192ZXJz", "aW9uGAEgASgJEh0KFW1sYWdlbnRzX2VudnNfdmVyc2lvbhgCIAEoCRIWCg5w", "eXRob25fdmVyc2lvbhgDIAEoCRIVCg10b3JjaF92ZXJzaW9uGAQgASgJEhkK", "EXRvcmNoX2RldmljZV90eXBlGAUgASgJEhAKCG51bV9lbnZzGAYgASgFEiIK", - "Gm51bV9lbnZpcm9ubWVudF9wYXJhbWV0ZXJzGAcgASgFIq0DChtUcmFpbmlu", - "Z0JlaGF2aW9ySW5pdGlhbGl6ZWQSFQoNYmVoYXZpb3JfbmFtZRgBIAEoCRIU", - "Cgx0cmFpbmVyX3R5cGUYAiABKAkSIAoYZXh0cmluc2ljX3Jld2FyZF9lbmFi", - "bGVkGAMgASgIEhsKE2dhaWxfcmV3YXJkX2VuYWJsZWQYBCABKAgSIAoYY3Vy", - "aW9zaXR5X3Jld2FyZF9lbmFibGVkGAUgASgIEhoKEnJuZF9yZXdhcmRfZW5h", - "YmxlZBgGIAEoCBIiChpiZWhhdmlvcmFsX2Nsb25pbmdfZW5hYmxlZBgHIAEo", - "CBIZChFyZWN1cnJlbnRfZW5hYmxlZBgIIAEoCBIWCg52aXN1YWxfZW5jb2Rl", - "chgJIAEoCRIaChJudW1fbmV0d29ya19sYXllcnMYCiABKAUSIAoYbnVtX25l", - "dHdvcmtfaGlkZGVuX3VuaXRzGAsgASgFEhgKEHRyYWluZXJfdGhyZWFkZWQY", - "DCABKAgSGQoRc2VsZl9wbGF5X2VuYWJsZWQYDSABKAgSGgoSY3VycmljdWx1", - "bV9lbmFibGVkGA4gASgIQiWqAiJVbml0eS5NTEFnZW50cy5Db21tdW5pY2F0", - "b3JPYmplY3RzYgZwcm90bzM=")); + "Gm51bV9lbnZpcm9ubWVudF9wYXJhbWV0ZXJzGAcgASgFEhMKC3J1bl9vcHRp", + "b25zGAggASgJIr0DChtUcmFpbmluZ0JlaGF2aW9ySW5pdGlhbGl6ZWQSFQoN", + "YmVoYXZpb3JfbmFtZRgBIAEoCRIUCgx0cmFpbmVyX3R5cGUYAiABKAkSIAoY", + "ZXh0cmluc2ljX3Jld2FyZF9lbmFibGVkGAMgASgIEhsKE2dhaWxfcmV3YXJk", + "X2VuYWJsZWQYBCABKAgSIAoYY3VyaW9zaXR5X3Jld2FyZF9lbmFibGVkGAUg", + "ASgIEhoKEnJuZF9yZXdhcmRfZW5hYmxlZBgGIAEoCBIiChpiZWhhdmlvcmFs", + "X2Nsb25pbmdfZW5hYmxlZBgHIAEoCBIZChFyZWN1cnJlbnRfZW5hYmxlZBgI", + "IAEoCBIWCg52aXN1YWxfZW5jb2RlchgJIAEoCRIaChJudW1fbmV0d29ya19s", + "YXllcnMYCiABKAUSIAoYbnVtX25ldHdvcmtfaGlkZGVuX3VuaXRzGAsgASgF", + "EhgKEHRyYWluZXJfdGhyZWFkZWQYDCABKAgSGQoRc2VsZl9wbGF5X2VuYWJs", + "ZWQYDSABKAgSGgoSY3VycmljdWx1bV9lbmFibGVkGA4gASgIEg4KBmNvbmZp", + "ZxgPIAEoCUIlqgIiVW5pdHkuTUxBZ2VudHMuQ29tbXVuaWNhdG9yT2JqZWN0", + "c2IGcHJvdG8z")); descriptor = pbr::FileDescriptor.FromGeneratedCode(descriptorData, new pbr::FileDescriptor[] { }, new pbr::GeneratedClrTypeInfo(null, new pbr::GeneratedClrTypeInfo[] { - new pbr::GeneratedClrTypeInfo(typeof(global::Unity.MLAgents.CommunicatorObjects.TrainingEnvironmentInitialized), global::Unity.MLAgents.CommunicatorObjects.TrainingEnvironmentInitialized.Parser, new[]{ "MlagentsVersion", "MlagentsEnvsVersion", "PythonVersion", "TorchVersion", "TorchDeviceType", "NumEnvs", "NumEnvironmentParameters" }, null, null, null), - new pbr::GeneratedClrTypeInfo(typeof(global::Unity.MLAgents.CommunicatorObjects.TrainingBehaviorInitialized), global::Unity.MLAgents.CommunicatorObjects.TrainingBehaviorInitialized.Parser, new[]{ "BehaviorName", "TrainerType", "ExtrinsicRewardEnabled", "GailRewardEnabled", "CuriosityRewardEnabled", "RndRewardEnabled", "BehavioralCloningEnabled", "RecurrentEnabled", "VisualEncoder", "NumNetworkLayers", "NumNetworkHiddenUnits", "TrainerThreaded", "SelfPlayEnabled", "CurriculumEnabled" }, null, null, null) + new pbr::GeneratedClrTypeInfo(typeof(global::Unity.MLAgents.CommunicatorObjects.TrainingEnvironmentInitialized), global::Unity.MLAgents.CommunicatorObjects.TrainingEnvironmentInitialized.Parser, new[]{ "MlagentsVersion", "MlagentsEnvsVersion", "PythonVersion", "TorchVersion", "TorchDeviceType", "NumEnvs", "NumEnvironmentParameters", "RunOptions" }, null, null, null), + new pbr::GeneratedClrTypeInfo(typeof(global::Unity.MLAgents.CommunicatorObjects.TrainingBehaviorInitialized), global::Unity.MLAgents.CommunicatorObjects.TrainingBehaviorInitialized.Parser, new[]{ "BehaviorName", "TrainerType", "ExtrinsicRewardEnabled", "GailRewardEnabled", "CuriosityRewardEnabled", "RndRewardEnabled", "BehavioralCloningEnabled", "RecurrentEnabled", "VisualEncoder", "NumNetworkLayers", "NumNetworkHiddenUnits", "TrainerThreaded", "SelfPlayEnabled", "CurriculumEnabled", "Config" }, null, null, null) })); } #endregion @@ -85,6 +86,7 @@ public TrainingEnvironmentInitialized(TrainingEnvironmentInitialized other) : th torchDeviceType_ = other.torchDeviceType_; numEnvs_ = other.numEnvs_; numEnvironmentParameters_ = other.numEnvironmentParameters_; + runOptions_ = other.runOptions_; _unknownFields = pb::UnknownFieldSet.Clone(other._unknownFields); } @@ -170,6 +172,17 @@ public int NumEnvironmentParameters { } } + /// Field number for the "run_options" field. + public const int RunOptionsFieldNumber = 8; + private string runOptions_ = ""; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public string RunOptions { + get { return runOptions_; } + set { + runOptions_ = pb::ProtoPreconditions.CheckNotNull(value, "value"); + } + } + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] public override bool Equals(object other) { return Equals(other as TrainingEnvironmentInitialized); @@ -190,6 +203,7 @@ public bool Equals(TrainingEnvironmentInitialized other) { if (TorchDeviceType != other.TorchDeviceType) return false; if (NumEnvs != other.NumEnvs) return false; if (NumEnvironmentParameters != other.NumEnvironmentParameters) return false; + if (RunOptions != other.RunOptions) return false; return Equals(_unknownFields, other._unknownFields); } @@ -203,6 +217,7 @@ public override int GetHashCode() { if (TorchDeviceType.Length != 0) hash ^= TorchDeviceType.GetHashCode(); if (NumEnvs != 0) hash ^= NumEnvs.GetHashCode(); if (NumEnvironmentParameters != 0) hash ^= NumEnvironmentParameters.GetHashCode(); + if (RunOptions.Length != 0) hash ^= RunOptions.GetHashCode(); if (_unknownFields != null) { hash ^= _unknownFields.GetHashCode(); } @@ -244,6 +259,10 @@ public void WriteTo(pb::CodedOutputStream output) { output.WriteRawTag(56); output.WriteInt32(NumEnvironmentParameters); } + if (RunOptions.Length != 0) { + output.WriteRawTag(66); + output.WriteString(RunOptions); + } if (_unknownFields != null) { _unknownFields.WriteTo(output); } @@ -273,6 +292,9 @@ public int CalculateSize() { if (NumEnvironmentParameters != 0) { size += 1 + pb::CodedOutputStream.ComputeInt32Size(NumEnvironmentParameters); } + if (RunOptions.Length != 0) { + size += 1 + pb::CodedOutputStream.ComputeStringSize(RunOptions); + } if (_unknownFields != null) { size += _unknownFields.CalculateSize(); } @@ -305,6 +327,9 @@ public void MergeFrom(TrainingEnvironmentInitialized other) { if (other.NumEnvironmentParameters != 0) { NumEnvironmentParameters = other.NumEnvironmentParameters; } + if (other.RunOptions.Length != 0) { + RunOptions = other.RunOptions; + } _unknownFields = pb::UnknownFieldSet.MergeFrom(_unknownFields, other._unknownFields); } @@ -344,6 +369,10 @@ public void MergeFrom(pb::CodedInputStream input) { NumEnvironmentParameters = input.ReadInt32(); break; } + case 66: { + RunOptions = input.ReadString(); + break; + } } } } @@ -389,6 +418,7 @@ public TrainingBehaviorInitialized(TrainingBehaviorInitialized other) : this() { trainerThreaded_ = other.trainerThreaded_; selfPlayEnabled_ = other.selfPlayEnabled_; curriculumEnabled_ = other.curriculumEnabled_; + config_ = other.config_; _unknownFields = pb::UnknownFieldSet.Clone(other._unknownFields); } @@ -551,6 +581,17 @@ public bool CurriculumEnabled { } } + /// Field number for the "config" field. + public const int ConfigFieldNumber = 15; + private string config_ = ""; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public string Config { + get { return config_; } + set { + config_ = pb::ProtoPreconditions.CheckNotNull(value, "value"); + } + } + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] public override bool Equals(object other) { return Equals(other as TrainingBehaviorInitialized); @@ -578,6 +619,7 @@ public bool Equals(TrainingBehaviorInitialized other) { if (TrainerThreaded != other.TrainerThreaded) return false; if (SelfPlayEnabled != other.SelfPlayEnabled) return false; if (CurriculumEnabled != other.CurriculumEnabled) return false; + if (Config != other.Config) return false; return Equals(_unknownFields, other._unknownFields); } @@ -598,6 +640,7 @@ public override int GetHashCode() { if (TrainerThreaded != false) hash ^= TrainerThreaded.GetHashCode(); if (SelfPlayEnabled != false) hash ^= SelfPlayEnabled.GetHashCode(); if (CurriculumEnabled != false) hash ^= CurriculumEnabled.GetHashCode(); + if (Config.Length != 0) hash ^= Config.GetHashCode(); if (_unknownFields != null) { hash ^= _unknownFields.GetHashCode(); } @@ -667,6 +710,10 @@ public void WriteTo(pb::CodedOutputStream output) { output.WriteRawTag(112); output.WriteBool(CurriculumEnabled); } + if (Config.Length != 0) { + output.WriteRawTag(122); + output.WriteString(Config); + } if (_unknownFields != null) { _unknownFields.WriteTo(output); } @@ -717,6 +764,9 @@ public int CalculateSize() { if (CurriculumEnabled != false) { size += 1 + 1; } + if (Config.Length != 0) { + size += 1 + pb::CodedOutputStream.ComputeStringSize(Config); + } if (_unknownFields != null) { size += _unknownFields.CalculateSize(); } @@ -770,6 +820,9 @@ public void MergeFrom(TrainingBehaviorInitialized other) { if (other.CurriculumEnabled != false) { CurriculumEnabled = other.CurriculumEnabled; } + if (other.Config.Length != 0) { + Config = other.Config; + } _unknownFields = pb::UnknownFieldSet.MergeFrom(_unknownFields, other._unknownFields); } @@ -837,6 +890,10 @@ public void MergeFrom(pb::CodedInputStream input) { CurriculumEnabled = input.ReadBool(); break; } + case 122: { + Config = input.ReadString(); + break; + } } } } diff --git a/ml-agents-envs/mlagents_envs/communicator_objects/training_analytics_pb2.py b/ml-agents-envs/mlagents_envs/communicator_objects/training_analytics_pb2.py index 1e775c9710..2701dac858 100644 --- a/ml-agents-envs/mlagents_envs/communicator_objects/training_analytics_pb2.py +++ b/ml-agents-envs/mlagents_envs/communicator_objects/training_analytics_pb2.py @@ -19,7 +19,7 @@ name='mlagents_envs/communicator_objects/training_analytics.proto', package='communicator_objects', syntax='proto3', - serialized_pb=_b('\n;mlagents_envs/communicator_objects/training_analytics.proto\x12\x14\x63ommunicator_objects\"\xd9\x01\n\x1eTrainingEnvironmentInitialized\x12\x18\n\x10mlagents_version\x18\x01 \x01(\t\x12\x1d\n\x15mlagents_envs_version\x18\x02 \x01(\t\x12\x16\n\x0epython_version\x18\x03 \x01(\t\x12\x15\n\rtorch_version\x18\x04 \x01(\t\x12\x19\n\x11torch_device_type\x18\x05 \x01(\t\x12\x10\n\x08num_envs\x18\x06 \x01(\x05\x12\"\n\x1anum_environment_parameters\x18\x07 \x01(\x05\"\xad\x03\n\x1bTrainingBehaviorInitialized\x12\x15\n\rbehavior_name\x18\x01 \x01(\t\x12\x14\n\x0ctrainer_type\x18\x02 \x01(\t\x12 \n\x18\x65xtrinsic_reward_enabled\x18\x03 \x01(\x08\x12\x1b\n\x13gail_reward_enabled\x18\x04 \x01(\x08\x12 \n\x18\x63uriosity_reward_enabled\x18\x05 \x01(\x08\x12\x1a\n\x12rnd_reward_enabled\x18\x06 \x01(\x08\x12\"\n\x1a\x62\x65havioral_cloning_enabled\x18\x07 \x01(\x08\x12\x19\n\x11recurrent_enabled\x18\x08 \x01(\x08\x12\x16\n\x0evisual_encoder\x18\t \x01(\t\x12\x1a\n\x12num_network_layers\x18\n \x01(\x05\x12 \n\x18num_network_hidden_units\x18\x0b \x01(\x05\x12\x18\n\x10trainer_threaded\x18\x0c \x01(\x08\x12\x19\n\x11self_play_enabled\x18\r \x01(\x08\x12\x1a\n\x12\x63urriculum_enabled\x18\x0e \x01(\x08\x42%\xaa\x02\"Unity.MLAgents.CommunicatorObjectsb\x06proto3') + serialized_pb=_b('\n;mlagents_envs/communicator_objects/training_analytics.proto\x12\x14\x63ommunicator_objects\"\xee\x01\n\x1eTrainingEnvironmentInitialized\x12\x18\n\x10mlagents_version\x18\x01 \x01(\t\x12\x1d\n\x15mlagents_envs_version\x18\x02 \x01(\t\x12\x16\n\x0epython_version\x18\x03 \x01(\t\x12\x15\n\rtorch_version\x18\x04 \x01(\t\x12\x19\n\x11torch_device_type\x18\x05 \x01(\t\x12\x10\n\x08num_envs\x18\x06 \x01(\x05\x12\"\n\x1anum_environment_parameters\x18\x07 \x01(\x05\x12\x13\n\x0brun_options\x18\x08 \x01(\t\"\xbd\x03\n\x1bTrainingBehaviorInitialized\x12\x15\n\rbehavior_name\x18\x01 \x01(\t\x12\x14\n\x0ctrainer_type\x18\x02 \x01(\t\x12 \n\x18\x65xtrinsic_reward_enabled\x18\x03 \x01(\x08\x12\x1b\n\x13gail_reward_enabled\x18\x04 \x01(\x08\x12 \n\x18\x63uriosity_reward_enabled\x18\x05 \x01(\x08\x12\x1a\n\x12rnd_reward_enabled\x18\x06 \x01(\x08\x12\"\n\x1a\x62\x65havioral_cloning_enabled\x18\x07 \x01(\x08\x12\x19\n\x11recurrent_enabled\x18\x08 \x01(\x08\x12\x16\n\x0evisual_encoder\x18\t \x01(\t\x12\x1a\n\x12num_network_layers\x18\n \x01(\x05\x12 \n\x18num_network_hidden_units\x18\x0b \x01(\x05\x12\x18\n\x10trainer_threaded\x18\x0c \x01(\x08\x12\x19\n\x11self_play_enabled\x18\r \x01(\x08\x12\x1a\n\x12\x63urriculum_enabled\x18\x0e \x01(\x08\x12\x0e\n\x06\x63onfig\x18\x0f \x01(\tB%\xaa\x02\"Unity.MLAgents.CommunicatorObjectsb\x06proto3') ) @@ -81,6 +81,13 @@ message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='run_options', full_name='communicator_objects.TrainingEnvironmentInitialized.run_options', index=7, + number=8, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), ], extensions=[ ], @@ -94,7 +101,7 @@ oneofs=[ ], serialized_start=86, - serialized_end=303, + serialized_end=324, ) @@ -203,6 +210,13 @@ message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='config', full_name='communicator_objects.TrainingBehaviorInitialized.config', index=14, + number=15, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), ], extensions=[ ], @@ -215,8 +229,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=306, - serialized_end=735, + serialized_start=327, + serialized_end=772, ) DESCRIPTOR.message_types_by_name['TrainingEnvironmentInitialized'] = _TRAININGENVIRONMENTINITIALIZED diff --git a/ml-agents-envs/mlagents_envs/communicator_objects/training_analytics_pb2.pyi b/ml-agents-envs/mlagents_envs/communicator_objects/training_analytics_pb2.pyi index a347de6874..53709ca8e8 100644 --- a/ml-agents-envs/mlagents_envs/communicator_objects/training_analytics_pb2.pyi +++ b/ml-agents-envs/mlagents_envs/communicator_objects/training_analytics_pb2.pyi @@ -33,6 +33,7 @@ class TrainingEnvironmentInitialized(google___protobuf___message___Message): torch_device_type = ... # type: typing___Text num_envs = ... # type: builtin___int num_environment_parameters = ... # type: builtin___int + run_options = ... # type: typing___Text def __init__(self, *, @@ -43,15 +44,16 @@ class TrainingEnvironmentInitialized(google___protobuf___message___Message): torch_device_type : typing___Optional[typing___Text] = None, num_envs : typing___Optional[builtin___int] = None, num_environment_parameters : typing___Optional[builtin___int] = None, + run_options : typing___Optional[typing___Text] = None, ) -> None: ... @classmethod def FromString(cls, s: builtin___bytes) -> TrainingEnvironmentInitialized: ... def MergeFrom(self, other_msg: google___protobuf___message___Message) -> None: ... def CopyFrom(self, other_msg: google___protobuf___message___Message) -> None: ... if sys.version_info >= (3,): - def ClearField(self, field_name: typing_extensions___Literal[u"mlagents_envs_version",u"mlagents_version",u"num_environment_parameters",u"num_envs",u"python_version",u"torch_device_type",u"torch_version"]) -> None: ... + def ClearField(self, field_name: typing_extensions___Literal[u"mlagents_envs_version",u"mlagents_version",u"num_environment_parameters",u"num_envs",u"python_version",u"run_options",u"torch_device_type",u"torch_version"]) -> None: ... else: - def ClearField(self, field_name: typing_extensions___Literal[u"mlagents_envs_version",b"mlagents_envs_version",u"mlagents_version",b"mlagents_version",u"num_environment_parameters",b"num_environment_parameters",u"num_envs",b"num_envs",u"python_version",b"python_version",u"torch_device_type",b"torch_device_type",u"torch_version",b"torch_version"]) -> None: ... + def ClearField(self, field_name: typing_extensions___Literal[u"mlagents_envs_version",b"mlagents_envs_version",u"mlagents_version",b"mlagents_version",u"num_environment_parameters",b"num_environment_parameters",u"num_envs",b"num_envs",u"python_version",b"python_version",u"run_options",b"run_options",u"torch_device_type",b"torch_device_type",u"torch_version",b"torch_version"]) -> None: ... class TrainingBehaviorInitialized(google___protobuf___message___Message): DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... @@ -69,6 +71,7 @@ class TrainingBehaviorInitialized(google___protobuf___message___Message): trainer_threaded = ... # type: builtin___bool self_play_enabled = ... # type: builtin___bool curriculum_enabled = ... # type: builtin___bool + config = ... # type: typing___Text def __init__(self, *, @@ -86,12 +89,13 @@ class TrainingBehaviorInitialized(google___protobuf___message___Message): trainer_threaded : typing___Optional[builtin___bool] = None, self_play_enabled : typing___Optional[builtin___bool] = None, curriculum_enabled : typing___Optional[builtin___bool] = None, + config : typing___Optional[typing___Text] = None, ) -> None: ... @classmethod def FromString(cls, s: builtin___bytes) -> TrainingBehaviorInitialized: ... def MergeFrom(self, other_msg: google___protobuf___message___Message) -> None: ... def CopyFrom(self, other_msg: google___protobuf___message___Message) -> None: ... if sys.version_info >= (3,): - def ClearField(self, field_name: typing_extensions___Literal[u"behavior_name",u"behavioral_cloning_enabled",u"curiosity_reward_enabled",u"curriculum_enabled",u"extrinsic_reward_enabled",u"gail_reward_enabled",u"num_network_hidden_units",u"num_network_layers",u"recurrent_enabled",u"rnd_reward_enabled",u"self_play_enabled",u"trainer_threaded",u"trainer_type",u"visual_encoder"]) -> None: ... + def ClearField(self, field_name: typing_extensions___Literal[u"behavior_name",u"behavioral_cloning_enabled",u"config",u"curiosity_reward_enabled",u"curriculum_enabled",u"extrinsic_reward_enabled",u"gail_reward_enabled",u"num_network_hidden_units",u"num_network_layers",u"recurrent_enabled",u"rnd_reward_enabled",u"self_play_enabled",u"trainer_threaded",u"trainer_type",u"visual_encoder"]) -> None: ... else: - def ClearField(self, field_name: typing_extensions___Literal[u"behavior_name",b"behavior_name",u"behavioral_cloning_enabled",b"behavioral_cloning_enabled",u"curiosity_reward_enabled",b"curiosity_reward_enabled",u"curriculum_enabled",b"curriculum_enabled",u"extrinsic_reward_enabled",b"extrinsic_reward_enabled",u"gail_reward_enabled",b"gail_reward_enabled",u"num_network_hidden_units",b"num_network_hidden_units",u"num_network_layers",b"num_network_layers",u"recurrent_enabled",b"recurrent_enabled",u"rnd_reward_enabled",b"rnd_reward_enabled",u"self_play_enabled",b"self_play_enabled",u"trainer_threaded",b"trainer_threaded",u"trainer_type",b"trainer_type",u"visual_encoder",b"visual_encoder"]) -> None: ... + def ClearField(self, field_name: typing_extensions___Literal[u"behavior_name",b"behavior_name",u"behavioral_cloning_enabled",b"behavioral_cloning_enabled",u"config",b"config",u"curiosity_reward_enabled",b"curiosity_reward_enabled",u"curriculum_enabled",b"curriculum_enabled",u"extrinsic_reward_enabled",b"extrinsic_reward_enabled",u"gail_reward_enabled",b"gail_reward_enabled",u"num_network_hidden_units",b"num_network_hidden_units",u"num_network_layers",b"num_network_layers",u"recurrent_enabled",b"recurrent_enabled",u"rnd_reward_enabled",b"rnd_reward_enabled",u"self_play_enabled",b"self_play_enabled",u"trainer_threaded",b"trainer_threaded",u"trainer_type",b"trainer_type",u"visual_encoder",b"visual_encoder"]) -> None: ... diff --git a/ml-agents/mlagents/training_analytics_side_channel.py b/ml-agents/mlagents/training_analytics_side_channel.py index 4b1f1c2dd5..d6513b7d26 100644 --- a/ml-agents/mlagents/training_analytics_side_channel.py +++ b/ml-agents/mlagents/training_analytics_side_channel.py @@ -1,5 +1,10 @@ +import copy +import json +import hmac +import hashlib import sys -from typing import Optional +from typing import Optional, Dict +import uuid import mlagents_envs import mlagents.trainers from mlagents import torch_utils @@ -24,6 +29,8 @@ class TrainingAnalyticsSideChannel(DefaultTrainingAnalyticsSideChannel): Side channel that sends information about the training to the Unity environment so it can be logged. """ + __vendorKey: str = "unity.ml-agents" + def __init__(self) -> None: # >>> uuid.uuid5(uuid.NAMESPACE_URL, "com.unity.ml-agents/TrainingAnalyticsSideChannel") # UUID('b664a4a9-d86f-5a5f-95cb-e8353a7e8356') @@ -31,17 +38,84 @@ def __init__(self) -> None: super().__init__() self.run_options: Optional[RunOptions] = None + @staticmethod + def __hash(key: str, data: str) -> str: + res = hmac.new( + key.encode("utf-8"), data.encode("utf-8"), hashlib.sha256 + ).hexdigest() + print(res) + return res + def on_message_received(self, msg: IncomingMessage) -> None: raise UnityCommunicationException( "The TrainingAnalyticsSideChannel received a message from Unity, " "this should not have happened." ) + @staticmethod + def __sanitize_run_options(config: RunOptions) -> Dict[str, Any]: + res = copy.deepcopy(config.as_dict()) + + def hash(value: str) -> str: + return TrainingAnalyticsSideChannel.__hash( + TrainingAnalyticsSideChannel.__vendorKey, value + ) + + # Filter potentially PII behavior names + if "behaviors" in res and res["behaviors"]: + res["behaviors"] = {hash(k): v for (k, v) in res["behaviors"].items()} + for (k, v) in res["behaviors"].items(): + if "init_path" in v and v["init_path"] is not None: + hashed_path = hash(v["init_path"]) + res["behaviors"][k]["init_path"] = hashed_path + + # Filter potentially PII curriculum and behavior names from Checkpoint Settings + if "environment_parameters" in res and res["environment_parameters"]: + res["environment_parameters"] = { + hash(k): v for (k, v) in res["environment_parameters"].items() + } + for (curriculumName, curriculum) in res["environment_parameters"].items(): + updated_lessons = [] + for lesson in curriculum["curriculum"]: + new_lesson = copy.deepcopy(lesson) + if lesson.has_keys("name"): + new_lesson["name"] = hash(lesson["name"]) + if lesson.has_keys("completion_criteria"): + new_lesson["completion_criteria"]["behavior"] = hash( + new_lesson["completion_criteria"]["behavior"] + ) + updated_lessons.append(new_lesson) + res["environment_parameters"][curriculumName][ + "curriculum" + ] = updated_lessons + + # Filter potentially PII filenames from Checkpoint Settings + if "checkpoint_settings" in res and res["checkpoint_settings"] is not None: + if ( + "initialize_from" in res["checkpoint_settings"] + and res["checkpoint_settings"]["initialize_from"] is not None + ): + res["checkpoint_settings"]["initialize_from"] = hash( + res["checkpoint_settings"]["initialize_from"] + ) + if ( + "results_dir" in res["checkpoint_settings"] + and res["checkpoint_settings"]["results_dir"] is not None + ): + res["checkpoint_settings"]["results_dir"] = hash( + res["checkpoint_settings"]["results_dir"] + ) + + return res + def environment_initialized(self, run_options: RunOptions) -> None: self.run_options = run_options # Tuple of (major, minor, patch) vi = sys.version_info env_params = run_options.environment_parameters + sanitized_run_options = TrainingAnalyticsSideChannel.__sanitize_run_options( + run_options + ) msg = TrainingEnvironmentInitialized( python_version=f"{vi[0]}.{vi[1]}.{vi[2]}", @@ -51,8 +125,11 @@ def environment_initialized(self, run_options: RunOptions) -> None: torch_device_type=torch_utils.default_device().type, num_envs=run_options.env_settings.num_envs, num_environment_parameters=len(env_params) if env_params else 0, + run_options=json.dumps(sanitized_run_options), ) + print(msg) + any_message = Any() any_message.Pack(msg) @@ -60,9 +137,22 @@ def environment_initialized(self, run_options: RunOptions) -> None: env_init_msg.set_raw_bytes(any_message.SerializeToString()) super().queue_message_to_send(env_init_msg) + @staticmethod + def __sanitize_trainer_settings(config: TrainerSettings) -> Dict[str, Any]: + config_dict = copy.deepcopy(config.as_dict()) + if "init_path" in config_dict and config_dict["init_path"] is not None: + hashed_path = TrainingAnalyticsSideChannel.__hash( + TrainingAnalyticsSideChannel.__vendorKey, config_dict["init_path"] + ) + config_dict["init_path"] = hashed_path + return config_dict + def training_started(self, behavior_name: str, config: TrainerSettings) -> None: + raw_config = TrainingAnalyticsSideChannel.__sanitize_trainer_settings(config) msg = TrainingBehaviorInitialized( - behavior_name=behavior_name, + behavior_name=TrainingAnalyticsSideChannel.__hash( + self.__vendorKey, behavior_name + ), trainer_type=config.trainer_type.value, extrinsic_reward_enabled=( RewardSignalType.EXTRINSIC in config.reward_signals @@ -80,6 +170,7 @@ def training_started(self, behavior_name: str, config: TrainerSettings) -> None: trainer_threaded=config.threaded, self_play_enabled=config.self_play is not None, curriculum_enabled=self._behavior_uses_curriculum(behavior_name), + config=json.dumps(raw_config), ) any_message = Any() diff --git a/protobuf-definitions/proto/mlagents_envs/communicator_objects/training_analytics.proto b/protobuf-definitions/proto/mlagents_envs/communicator_objects/training_analytics.proto index 52f0ed6109..08905c516b 100644 --- a/protobuf-definitions/proto/mlagents_envs/communicator_objects/training_analytics.proto +++ b/protobuf-definitions/proto/mlagents_envs/communicator_objects/training_analytics.proto @@ -11,6 +11,7 @@ message TrainingEnvironmentInitialized { string torch_device_type = 5; int32 num_envs = 6; int32 num_environment_parameters = 7; + string run_options = 8; } message TrainingBehaviorInitialized { @@ -28,4 +29,5 @@ message TrainingBehaviorInitialized { bool trainer_threaded = 12; bool self_play_enabled = 13; bool curriculum_enabled = 14; + string config = 15; } From 86afcb2f7d599b29bed7656616413f671ef9d4f6 Mon Sep 17 00:00:00 2001 From: Jason Bowman Date: Mon, 30 Aug 2021 13:24:51 -0700 Subject: [PATCH 3/9] Simplify imports used by AnalyticsUtils.cs --- com.unity.ml-agents/Runtime/Analytics/AnalyticsUtils.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/com.unity.ml-agents/Runtime/Analytics/AnalyticsUtils.cs b/com.unity.ml-agents/Runtime/Analytics/AnalyticsUtils.cs index 6249144ac2..b206f6bd98 100644 --- a/com.unity.ml-agents/Runtime/Analytics/AnalyticsUtils.cs +++ b/com.unity.ml-agents/Runtime/Analytics/AnalyticsUtils.cs @@ -1,4 +1,6 @@ using System; +using System.Text; +using System.Security.Cryptography; using UnityEngine; namespace Unity.MLAgents.Analytics @@ -13,7 +15,7 @@ internal static class AnalyticsUtils /// A byte array to be hex encoded. private static string ToHexString(byte[] array) { - System.Text.StringBuilder hex = new System.Text.StringBuilder(array.Length * 2); + StringBuilder hex = new StringBuilder(array.Length * 2); foreach (byte b in array) { hex.AppendFormat("{0:x2}", b); @@ -31,8 +33,8 @@ private static string ToHexString(byte[] array) public static string Hash(string key, string value) { string hash; - System.Text.UTF8Encoding encoder = new System.Text.UTF8Encoding(); - using (System.Security.Cryptography.HMACSHA256 hmac = new System.Security.Cryptography.HMACSHA256(encoder.GetBytes(key))) + UTF8Encoding encoder = new UTF8Encoding(); + using (HMACSHA256 hmac = new HMACSHA256(encoder.GetBytes(key))) { Byte[] hmBytes = hmac.ComputeHash(encoder.GetBytes(value)); hash = ToHexString(hmBytes); From c9f1b4f84d5fe2b2904a52a164d40899514dc11e Mon Sep 17 00:00:00 2001 From: Jason Bowman Date: Mon, 30 Aug 2021 13:27:09 -0700 Subject: [PATCH 4/9] Address comments about moving hash to a class function and not nested. --- .../training_analytics_side_channel.py | 44 +++++++------------ 1 file changed, 16 insertions(+), 28 deletions(-) diff --git a/ml-agents/mlagents/training_analytics_side_channel.py b/ml-agents/mlagents/training_analytics_side_channel.py index d6513b7d26..4dda8a7ddd 100644 --- a/ml-agents/mlagents/training_analytics_side_channel.py +++ b/ml-agents/mlagents/training_analytics_side_channel.py @@ -38,12 +38,11 @@ def __init__(self) -> None: super().__init__() self.run_options: Optional[RunOptions] = None - @staticmethod - def __hash(key: str, data: str) -> str: + @classmethod + def __hash(cls, data: str) -> str: res = hmac.new( - key.encode("utf-8"), data.encode("utf-8"), hashlib.sha256 + cls.__vendorKey.encode("utf-8"), data.encode("utf-8"), hashlib.sha256 ).hexdigest() - print(res) return res def on_message_received(self, msg: IncomingMessage) -> None: @@ -52,36 +51,31 @@ def on_message_received(self, msg: IncomingMessage) -> None: "this should not have happened." ) - @staticmethod - def __sanitize_run_options(config: RunOptions) -> Dict[str, Any]: + @classmethod + def __sanitize_run_options(cls, config: RunOptions) -> Dict[str, Any]: res = copy.deepcopy(config.as_dict()) - def hash(value: str) -> str: - return TrainingAnalyticsSideChannel.__hash( - TrainingAnalyticsSideChannel.__vendorKey, value - ) - # Filter potentially PII behavior names if "behaviors" in res and res["behaviors"]: - res["behaviors"] = {hash(k): v for (k, v) in res["behaviors"].items()} + res["behaviors"] = {cls.__hash(k): v for (k, v) in res["behaviors"].items()} for (k, v) in res["behaviors"].items(): if "init_path" in v and v["init_path"] is not None: - hashed_path = hash(v["init_path"]) + hashed_path = cls.__hash(v["init_path"]) res["behaviors"][k]["init_path"] = hashed_path # Filter potentially PII curriculum and behavior names from Checkpoint Settings if "environment_parameters" in res and res["environment_parameters"]: res["environment_parameters"] = { - hash(k): v for (k, v) in res["environment_parameters"].items() + cls.__hash(k): v for (k, v) in res["environment_parameters"].items() } for (curriculumName, curriculum) in res["environment_parameters"].items(): updated_lessons = [] for lesson in curriculum["curriculum"]: new_lesson = copy.deepcopy(lesson) if lesson.has_keys("name"): - new_lesson["name"] = hash(lesson["name"]) + new_lesson["name"] = cls.__hash(lesson["name"]) if lesson.has_keys("completion_criteria"): - new_lesson["completion_criteria"]["behavior"] = hash( + new_lesson["completion_criteria"]["behavior"] = cls.__hash( new_lesson["completion_criteria"]["behavior"] ) updated_lessons.append(new_lesson) @@ -95,7 +89,7 @@ def hash(value: str) -> str: "initialize_from" in res["checkpoint_settings"] and res["checkpoint_settings"]["initialize_from"] is not None ): - res["checkpoint_settings"]["initialize_from"] = hash( + res["checkpoint_settings"]["initialize_from"] = cls.__hash( res["checkpoint_settings"]["initialize_from"] ) if ( @@ -128,8 +122,6 @@ def environment_initialized(self, run_options: RunOptions) -> None: run_options=json.dumps(sanitized_run_options), ) - print(msg) - any_message = Any() any_message.Pack(msg) @@ -137,22 +129,18 @@ def environment_initialized(self, run_options: RunOptions) -> None: env_init_msg.set_raw_bytes(any_message.SerializeToString()) super().queue_message_to_send(env_init_msg) - @staticmethod - def __sanitize_trainer_settings(config: TrainerSettings) -> Dict[str, Any]: + @classmethod + def __sanitize_trainer_settings(cls, config: TrainerSettings) -> Dict[str, Any]: config_dict = copy.deepcopy(config.as_dict()) if "init_path" in config_dict and config_dict["init_path"] is not None: - hashed_path = TrainingAnalyticsSideChannel.__hash( - TrainingAnalyticsSideChannel.__vendorKey, config_dict["init_path"] - ) + hashed_path = cls.__hash(config_dict["init_path"]) config_dict["init_path"] = hashed_path return config_dict def training_started(self, behavior_name: str, config: TrainerSettings) -> None: - raw_config = TrainingAnalyticsSideChannel.__sanitize_trainer_settings(config) + raw_config = self.__sanitize_trainer_settings(config) msg = TrainingBehaviorInitialized( - behavior_name=TrainingAnalyticsSideChannel.__hash( - self.__vendorKey, behavior_name - ), + behavior_name=self.__hash(behavior_name), trainer_type=config.trainer_type.value, extrinsic_reward_enabled=( RewardSignalType.EXTRINSIC in config.reward_signals From 4dd38845135cd8b3aa1282cce11bbd7de7a19a34 Mon Sep 17 00:00:00 2001 From: Jason Bowman Date: Wed, 1 Sep 2021 11:37:42 -0700 Subject: [PATCH 5/9] Change member function scopes and hash demo_paths --- .../training_analytics_side_channel.py | 34 +++++++++++-------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/ml-agents/mlagents/training_analytics_side_channel.py b/ml-agents/mlagents/training_analytics_side_channel.py index 4dda8a7ddd..47cc54dcb9 100644 --- a/ml-agents/mlagents/training_analytics_side_channel.py +++ b/ml-agents/mlagents/training_analytics_side_channel.py @@ -39,7 +39,7 @@ def __init__(self) -> None: self.run_options: Optional[RunOptions] = None @classmethod - def __hash(cls, data: str) -> str: + def _hash(cls, data: str) -> str: res = hmac.new( cls.__vendorKey.encode("utf-8"), data.encode("utf-8"), hashlib.sha256 ).hexdigest() @@ -52,30 +52,33 @@ def on_message_received(self, msg: IncomingMessage) -> None: ) @classmethod - def __sanitize_run_options(cls, config: RunOptions) -> Dict[str, Any]: + def _sanitize_run_options(cls, config: RunOptions) -> Dict[str, Any]: res = copy.deepcopy(config.as_dict()) # Filter potentially PII behavior names if "behaviors" in res and res["behaviors"]: - res["behaviors"] = {cls.__hash(k): v for (k, v) in res["behaviors"].items()} + res["behaviors"] = {cls._hash(k): v for (k, v) in res["behaviors"].items()} for (k, v) in res["behaviors"].items(): if "init_path" in v and v["init_path"] is not None: - hashed_path = cls.__hash(v["init_path"]) + hashed_path = cls._hash(v["init_path"]) res["behaviors"][k]["init_path"] = hashed_path + if "demo_path" in v and v["demo_path"] is not None: + hashed_path = cls._hash(v["demo_path"]) + res["behaviors"][k]["demo_path"] = hashed_path # Filter potentially PII curriculum and behavior names from Checkpoint Settings if "environment_parameters" in res and res["environment_parameters"]: res["environment_parameters"] = { - cls.__hash(k): v for (k, v) in res["environment_parameters"].items() + cls._hash(k): v for (k, v) in res["environment_parameters"].items() } for (curriculumName, curriculum) in res["environment_parameters"].items(): updated_lessons = [] for lesson in curriculum["curriculum"]: new_lesson = copy.deepcopy(lesson) if lesson.has_keys("name"): - new_lesson["name"] = cls.__hash(lesson["name"]) + new_lesson["name"] = cls._hash(lesson["name"]) if lesson.has_keys("completion_criteria"): - new_lesson["completion_criteria"]["behavior"] = cls.__hash( + new_lesson["completion_criteria"]["behavior"] = cls._hash( new_lesson["completion_criteria"]["behavior"] ) updated_lessons.append(new_lesson) @@ -89,7 +92,7 @@ def __sanitize_run_options(cls, config: RunOptions) -> Dict[str, Any]: "initialize_from" in res["checkpoint_settings"] and res["checkpoint_settings"]["initialize_from"] is not None ): - res["checkpoint_settings"]["initialize_from"] = cls.__hash( + res["checkpoint_settings"]["initialize_from"] = cls._hash( res["checkpoint_settings"]["initialize_from"] ) if ( @@ -107,9 +110,7 @@ def environment_initialized(self, run_options: RunOptions) -> None: # Tuple of (major, minor, patch) vi = sys.version_info env_params = run_options.environment_parameters - sanitized_run_options = TrainingAnalyticsSideChannel.__sanitize_run_options( - run_options - ) + sanitized_run_options = self._sanitize_run_options(run_options) msg = TrainingEnvironmentInitialized( python_version=f"{vi[0]}.{vi[1]}.{vi[2]}", @@ -130,17 +131,20 @@ def environment_initialized(self, run_options: RunOptions) -> None: super().queue_message_to_send(env_init_msg) @classmethod - def __sanitize_trainer_settings(cls, config: TrainerSettings) -> Dict[str, Any]: + def _sanitize_trainer_settings(cls, config: TrainerSettings) -> Dict[str, Any]: config_dict = copy.deepcopy(config.as_dict()) if "init_path" in config_dict and config_dict["init_path"] is not None: - hashed_path = cls.__hash(config_dict["init_path"]) + hashed_path = cls._hash(config_dict["init_path"]) config_dict["init_path"] = hashed_path + if "demo_path" in config_dict and config_dict["demo_path"] is not None: + hashed_path = cls._hash(config_dict["demo_path"]) + config_dict["demo_path"] = hashed_path return config_dict def training_started(self, behavior_name: str, config: TrainerSettings) -> None: - raw_config = self.__sanitize_trainer_settings(config) + raw_config = self._sanitize_trainer_settings(config) msg = TrainingBehaviorInitialized( - behavior_name=self.__hash(behavior_name), + behavior_name=self._hash(behavior_name), trainer_type=config.trainer_type.value, extrinsic_reward_enabled=( RewardSignalType.EXTRINSIC in config.reward_signals From 663ea5260c63b8ecf42f5f3bb5b9a25e529106c2 Mon Sep 17 00:00:00 2001 From: Jason Bowman Date: Wed, 25 Aug 2021 19:42:38 -0700 Subject: [PATCH 6/9] Extend TrainingAnalytics side channel to expose configuration details Update python files to adhere to linting rules Fix typing check Simplify variable usage --- ml-agents/mlagents/training_analytics_side_channel.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ml-agents/mlagents/training_analytics_side_channel.py b/ml-agents/mlagents/training_analytics_side_channel.py index 47cc54dcb9..dded686cc5 100644 --- a/ml-agents/mlagents/training_analytics_side_channel.py +++ b/ml-agents/mlagents/training_analytics_side_channel.py @@ -123,6 +123,8 @@ def environment_initialized(self, run_options: RunOptions) -> None: run_options=json.dumps(sanitized_run_options), ) + print(msg) + any_message = Any() any_message.Pack(msg) From 245119d9ca8ac8d8009c673b1284d5695f3e7237 Mon Sep 17 00:00:00 2001 From: Jason Bowman Date: Mon, 30 Aug 2021 13:27:09 -0700 Subject: [PATCH 7/9] Address comments about moving hash to a class function and not nested. --- ml-agents/mlagents/training_analytics_side_channel.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/ml-agents/mlagents/training_analytics_side_channel.py b/ml-agents/mlagents/training_analytics_side_channel.py index dded686cc5..47cc54dcb9 100644 --- a/ml-agents/mlagents/training_analytics_side_channel.py +++ b/ml-agents/mlagents/training_analytics_side_channel.py @@ -123,8 +123,6 @@ def environment_initialized(self, run_options: RunOptions) -> None: run_options=json.dumps(sanitized_run_options), ) - print(msg) - any_message = Any() any_message.Pack(msg) From 2d61dd7a2e33969a9d67e55a9b004b29b4c72cb6 Mon Sep 17 00:00:00 2001 From: Jason Bowman Date: Wed, 29 Sep 2021 15:55:13 -0700 Subject: [PATCH 8/9] Extract tbiEvent hashing method and add test coverage --- .../Runtime/Analytics/TrainingAnalytics.cs | 22 +++++++++++++------ .../Editor/Analytics/TrainingAnalyticsTest.cs | 13 +++++++++++ 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/com.unity.ml-agents/Runtime/Analytics/TrainingAnalytics.cs b/com.unity.ml-agents/Runtime/Analytics/TrainingAnalytics.cs index 0ea24f0ed7..1b12dcf5d4 100644 --- a/com.unity.ml-agents/Runtime/Analytics/TrainingAnalytics.cs +++ b/com.unity.ml-agents/Runtime/Analytics/TrainingAnalytics.cs @@ -192,8 +192,21 @@ internal static string ParseBehaviorName(string fullyQualifiedBehaviorName) return fullyQualifiedBehaviorName.Substring(0, lastQuestionIndex); } + internal static TrainingBehaviorInitializedEvent SanitizeTrainingBehaviorInitializedEvent(TrainingBehaviorInitializedEvent tbiEvent) + { + // Hash the behavior name if the message version is from an older version of ml-agents that doesn't do trainer-side hashing. + // We'll also, for extra safety, verify that the BehaviorName is the size of the expected SHA256 hash. + // Context: The config field was added at the same time as trainer side hashing, so messages including it should already be hashed. + if (tbiEvent.Config.Length == 0 || tbiEvent.BehaviorName.Length != 64) + { + tbiEvent.BehaviorName = AnalyticsUtils.Hash(k_VendorKey, tbiEvent.BehaviorName); + } + + return tbiEvent; + } + [Conditional("MLA_UNITY_ANALYTICS_MODULE")] - public static void TrainingBehaviorInitialized(TrainingBehaviorInitializedEvent tbiEvent) + public static void TrainingBehaviorInitialized(TrainingBehaviorInitializedEvent rawTbiEvent) { #if UNITY_EDITOR && MLA_UNITY_ANALYTICS_MODULE if (!IsAnalyticsEnabled()) @@ -202,6 +215,7 @@ public static void TrainingBehaviorInitialized(TrainingBehaviorInitializedEvent if (!EnableAnalytics()) return; + var tbiEvent = SanitizeTrainingBehaviorInitializedEvent(rawTbiEvent); var behaviorName = tbiEvent.BehaviorName; var added = s_SentTrainingBehaviorInitialized.Add(behaviorName); @@ -213,12 +227,6 @@ public static void TrainingBehaviorInitialized(TrainingBehaviorInitializedEvent tbiEvent.TrainingSessionGuid = s_TrainingSessionGuid.ToString(); - if(tbiEvent.Config.Length == 0 || tbiEvent.BehaviorName.Length != 64) { - // Hash the behavior name if the message version is from an older version of ml-agents that doesn't do trainer-side hashing. - // We'll also, for extra safety, verify that the BehaviorName is the size of the expected SHA256 hash. - // Context: The config field was added at the same time as trainer side hashing, so messages including it should already be hashed. - tbiEvent.BehaviorName = AnalyticsUtils.Hash(k_VendorKey, tbiEvent.BehaviorName); - } // Note - to debug, use JsonUtility.ToJson on the event. // Debug.Log( // $"Would send event {k_TrainingBehaviorInitializedEventName} with body {JsonUtility.ToJson(tbiEvent, true)}" diff --git a/com.unity.ml-agents/Tests/Editor/Analytics/TrainingAnalyticsTest.cs b/com.unity.ml-agents/Tests/Editor/Analytics/TrainingAnalyticsTest.cs index 1010b4549b..0487a7a524 100644 --- a/com.unity.ml-agents/Tests/Editor/Analytics/TrainingAnalyticsTest.cs +++ b/com.unity.ml-agents/Tests/Editor/Analytics/TrainingAnalyticsTest.cs @@ -70,6 +70,19 @@ public void TestRemotePolicy() Academy.Instance.Dispose(); } + [TestCase("a name we expect to hash", ExpectedResult = "d084a8b6da6a6a1c097cdc9ffea95e1546da4647352113ed77cbe7b4192e6d73")] + [TestCase("another_name", ExpectedResult = "0b74613c872e79aba11e06eda3538f2b646eb2b459e75087829ea500bd703d0b")] + [TestCase("0b74613c872e79aba11e06eda3538f2b646eb2b459e75087829ea500bd703d0b", ExpectedResult = "0b74613c872e79aba11e06eda3538f2b646eb2b459e75087829ea500bd703d0b")] + public string TestTrainingBehaviorInitialized(string stringToMaybeHash) + { + var tbiEvent = new TrainingBehaviorInitializedEvent(); + tbiEvent.BehaviorName = stringToMaybeHash; + tbiEvent.Config = "{}"; + + var sanitizedEvent = TrainingAnalytics.SanitizeTrainingBehaviorInitializedEvent(tbiEvent); + return sanitizedEvent.BehaviorName; + } + [Test] public void TestEnableAnalytics() { From 2b139aa8d1aed0aa93d999e70a0bbcee1597e172 Mon Sep 17 00:00:00 2001 From: Jason Bowman Date: Wed, 29 Sep 2021 16:23:47 -0700 Subject: [PATCH 9/9] Remove import accidentally pulled from rebase --- ml-agents/mlagents/training_analytics_side_channel.py | 1 - 1 file changed, 1 deletion(-) diff --git a/ml-agents/mlagents/training_analytics_side_channel.py b/ml-agents/mlagents/training_analytics_side_channel.py index 47cc54dcb9..1c12304a97 100644 --- a/ml-agents/mlagents/training_analytics_side_channel.py +++ b/ml-agents/mlagents/training_analytics_side_channel.py @@ -4,7 +4,6 @@ import hashlib import sys from typing import Optional, Dict -import uuid import mlagents_envs import mlagents.trainers from mlagents import torch_utils