From 1d63ec7ed8bbb98051bc059ba1067e7c247e8515 Mon Sep 17 00:00:00 2001 From: nicholascole Date: Thu, 9 Jul 2026 21:03:38 -0700 Subject: [PATCH] feat(client): add Task.RuntimeMetadata for host-resolved worker secrets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The server can resolve a worker's declared TaskDef.runtimeMetadata secret names at poll time and deliver the values on the wire-only Task.runtimeMetadata (conductor-oss PR #1255) — never persisted to task input. Add the field to the client Task model so it is not dropped on deserialization. - Dictionary RuntimeMetadata with [DataMember(Name="runtimeMetadata", EmitDefaultValue=false)] (empty omitted; [DataContract] makes Newtonsoft honor it). - Wired into the constructor, ToString, Equals, and GetHashCode like OutputData. - Tests/Client/TaskRuntimeMetadataTests.cs: round-trips and omit-when-empty. Note: not built/tested locally (no dotnet in the authoring environment); relies on CI for the build + test run. Co-Authored-By: Claude Opus 4.8 (1M context) --- Conductor/Client/Models/Task.cs | 20 ++++++++- Tests/Client/TaskRuntimeMetadataTests.cs | 53 ++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 Tests/Client/TaskRuntimeMetadataTests.cs diff --git a/Conductor/Client/Models/Task.cs b/Conductor/Client/Models/Task.cs index e2537fc7..89ea9e24 100644 --- a/Conductor/Client/Models/Task.cs +++ b/Conductor/Client/Models/Task.cs @@ -128,7 +128,7 @@ public enum StatusEnum /// workflowPriority. /// workflowTask. /// workflowType. - public Task(long? callbackAfterSeconds = default(long?), bool? callbackFromWorker = default(bool?), string correlationId = default(string), string domain = default(string), long? endTime = default(long?), bool? executed = default(bool?), string executionNameSpace = default(string), string externalInputPayloadStoragePath = default(string), string externalOutputPayloadStoragePath = default(string), Dictionary inputData = default(Dictionary), string isolationGroupId = default(string), int? iteration = default(int?), bool? loopOverTask = default(bool?), Dictionary outputData = default(Dictionary), int? pollCount = default(int?), long? queueWaitTime = default(long?), int? rateLimitFrequencyInSeconds = default(int?), int? rateLimitPerFrequency = default(int?), string reasonForIncompletion = default(string), string referenceTaskName = default(string), long? responseTimeoutSeconds = default(long?), bool? retried = default(bool?), string retriedTaskId = default(string), int? retryCount = default(int?), long? scheduledTime = default(long?), int? seq = default(int?), int? startDelayInSeconds = default(int?), long? startTime = default(long?), StatusEnum? status = default(StatusEnum?), string subWorkflowId = default(string), bool? subworkflowChanged = default(bool?), string taskDefName = default(string), TaskDef taskDefinition = default(TaskDef), string taskId = default(string), string taskType = default(string), long? updateTime = default(long?), string workerId = default(string), string workflowInstanceId = default(string), int? workflowPriority = default(int?), WorkflowTask workflowTask = default(WorkflowTask), string workflowType = default(string)) + public Task(long? callbackAfterSeconds = default(long?), bool? callbackFromWorker = default(bool?), string correlationId = default(string), string domain = default(string), long? endTime = default(long?), bool? executed = default(bool?), string executionNameSpace = default(string), string externalInputPayloadStoragePath = default(string), string externalOutputPayloadStoragePath = default(string), Dictionary inputData = default(Dictionary), string isolationGroupId = default(string), int? iteration = default(int?), bool? loopOverTask = default(bool?), Dictionary outputData = default(Dictionary), int? pollCount = default(int?), long? queueWaitTime = default(long?), int? rateLimitFrequencyInSeconds = default(int?), int? rateLimitPerFrequency = default(int?), string reasonForIncompletion = default(string), string referenceTaskName = default(string), long? responseTimeoutSeconds = default(long?), bool? retried = default(bool?), string retriedTaskId = default(string), int? retryCount = default(int?), long? scheduledTime = default(long?), int? seq = default(int?), int? startDelayInSeconds = default(int?), long? startTime = default(long?), StatusEnum? status = default(StatusEnum?), string subWorkflowId = default(string), bool? subworkflowChanged = default(bool?), string taskDefName = default(string), TaskDef taskDefinition = default(TaskDef), string taskId = default(string), string taskType = default(string), long? updateTime = default(long?), string workerId = default(string), string workflowInstanceId = default(string), int? workflowPriority = default(int?), WorkflowTask workflowTask = default(WorkflowTask), string workflowType = default(string), Dictionary runtimeMetadata = default(Dictionary)) { this.CallbackAfterSeconds = callbackAfterSeconds; this.CallbackFromWorker = callbackFromWorker; @@ -171,6 +171,7 @@ public enum StatusEnum this.WorkflowPriority = workflowPriority; this.WorkflowTask = workflowTask; this.WorkflowType = workflowType; + this.RuntimeMetadata = runtimeMetadata; } /// @@ -257,6 +258,14 @@ public enum StatusEnum [DataMember(Name = "outputData", EmitDefaultValue = false)] public Dictionary OutputData { get; set; } + /// + /// Secret values the server resolved for this task at poll time and delivered on the wire + /// only (never persisted). Populated when the task's TaskDef.runtimeMetadata declares secret + /// names the host resolves from its secret store (conductor-oss PR #1255). Empty/absent otherwise. + /// + [DataMember(Name = "runtimeMetadata", EmitDefaultValue = false)] + public Dictionary RuntimeMetadata { get; set; } + /// /// Gets or Sets PollCount /// @@ -436,6 +445,7 @@ public override string ToString() sb.Append(" Iteration: ").Append(Iteration).Append("\n"); sb.Append(" LoopOverTask: ").Append(LoopOverTask).Append("\n"); sb.Append(" OutputData: ").Append(OutputData).Append("\n"); + sb.Append(" RuntimeMetadata: ").Append(RuntimeMetadata).Append("\n"); sb.Append(" PollCount: ").Append(PollCount).Append("\n"); sb.Append(" QueueWaitTime: ").Append(QueueWaitTime).Append("\n"); sb.Append(" RateLimitFrequencyInSeconds: ").Append(RateLimitFrequencyInSeconds).Append("\n"); @@ -569,6 +579,12 @@ public bool Equals(Task input) input.OutputData != null && this.OutputData.SequenceEqual(input.OutputData) ) && + ( + this.RuntimeMetadata == input.RuntimeMetadata || + this.RuntimeMetadata != null && + input.RuntimeMetadata != null && + this.RuntimeMetadata.SequenceEqual(input.RuntimeMetadata) + ) && ( this.PollCount == input.PollCount || (this.PollCount != null && @@ -743,6 +759,8 @@ public override int GetHashCode() hashCode = hashCode * 59 + this.LoopOverTask.GetHashCode(); if (this.OutputData != null) hashCode = hashCode * 59 + this.OutputData.GetHashCode(); + if (this.RuntimeMetadata != null) + hashCode = hashCode * 59 + this.RuntimeMetadata.GetHashCode(); if (this.PollCount != null) hashCode = hashCode * 59 + this.PollCount.GetHashCode(); if (this.QueueWaitTime != null) diff --git a/Tests/Client/TaskRuntimeMetadataTests.cs b/Tests/Client/TaskRuntimeMetadataTests.cs new file mode 100644 index 00000000..8959d421 --- /dev/null +++ b/Tests/Client/TaskRuntimeMetadataTests.cs @@ -0,0 +1,53 @@ +/* + * Copyright 2025 Conductor Authors. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +using System.Collections.Generic; +using Newtonsoft.Json; +using Xunit; +using ModelTask = Conductor.Client.Models.Task; + +namespace Tests.Client +{ + ///

+ /// The server delivers host-resolved secret values on Task.runtimeMetadata (wire-only, never + /// persisted) when a worker's TaskDef.runtimeMetadata declares secret names (conductor-oss PR + /// #1255). Verify the client model round-trips the field and omits it when empty. + /// + public class TaskRuntimeMetadataTests + { + [Fact] + public void RuntimeMetadata_RoundTrips() + { + var task = new ModelTask( + taskId: "t1", + runtimeMetadata: new Dictionary + { + { "GITHUB_TOKEN", "ghp_secret" }, + { "GH_APP_ID", "42" } + }); + + var json = JsonConvert.SerializeObject(task); + Assert.Contains("\"runtimeMetadata\"", json); + + var back = JsonConvert.DeserializeObject(json); + Assert.Equal("ghp_secret", back.RuntimeMetadata["GITHUB_TOKEN"]); + Assert.Equal("42", back.RuntimeMetadata["GH_APP_ID"]); + } + + [Fact] + public void RuntimeMetadata_OmittedWhenEmpty() + { + var json = JsonConvert.SerializeObject(new ModelTask(taskId: "t1")); + Assert.DoesNotContain("runtimeMetadata", json); + } + } +}