From c1676d48e9732d7538ec13acf4d2750699f7d072 Mon Sep 17 00:00:00 2001 From: nicholascole Date: Thu, 9 Jul 2026 21:01:19 -0700 Subject: [PATCH] feat(client): add Task.runtime_metadata 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. - runtime_metadata (dict(str, str)) in swagger_types + attribute_map ('runtimeMetadata'), constructor param, and property getter/setter. - Omitted from the wire when empty (sanitize_for_serialization skips None). - tests/serdeser/test_task_runtime_metadata.py: round-trips and omit-when-empty. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/conductor/client/http/models/task.py | 35 ++++++++++++++++++-- tests/serdeser/test_task_runtime_metadata.py | 34 +++++++++++++++++++ 2 files changed, 67 insertions(+), 2 deletions(-) create mode 100644 tests/serdeser/test_task_runtime_metadata.py diff --git a/src/conductor/client/http/models/task.py b/src/conductor/client/http/models/task.py index fc0dce3e..fe80fd6c 100644 --- a/src/conductor/client/http/models/task.py +++ b/src/conductor/client/http/models/task.py @@ -63,7 +63,8 @@ class Task: _subworkflow_changed: bool = field(default=None) _parent_task_id: str = field(default=None) _first_start_time: int = field(default=None) - + _runtime_metadata: Dict[str, str] = field(default=None) + # Fields that are in Python but not in Java _loop_over_task: bool = field(default=None) _task_definition: Any = field(default=None) @@ -110,6 +111,7 @@ class Task: 'subworkflow_changed': 'bool', 'parent_task_id': 'str', 'first_start_time': 'int', + 'runtime_metadata': 'dict(str, str)', 'loop_over_task': 'bool', 'task_definition': 'TaskDef', 'queue_wait_time': 'int' @@ -156,6 +158,7 @@ class Task: 'subworkflow_changed': 'subworkflowChanged', 'parent_task_id': 'parentTaskId', 'first_start_time': 'firstStartTime', + 'runtime_metadata': 'runtimeMetadata', 'loop_over_task': 'loopOverTask', 'task_definition': 'taskDefinition', 'queue_wait_time': 'queueWaitTime' @@ -171,7 +174,8 @@ def __init__(self, task_type=None, status=None, input_data=None, reference_task_ external_input_payload_storage_path=None, external_output_payload_storage_path=None, workflow_priority=None, execution_name_space=None, isolation_group_id=None, iteration=None, sub_workflow_id=None, subworkflow_changed=None, loop_over_task=None, task_definition=None, - queue_wait_time=None, parent_task_id=None, first_start_time=None): # noqa: E501 + queue_wait_time=None, parent_task_id=None, first_start_time=None, + runtime_metadata=None): # noqa: E501 """Task - a model defined in Swagger""" # noqa: E501 self._task_type = None self._status = None @@ -216,6 +220,7 @@ def __init__(self, task_type=None, status=None, input_data=None, reference_task_ self._loop_over_task = None self._task_definition = None self._queue_wait_time = None + self._runtime_metadata = None self.discriminator = None if task_type is not None: self.task_type = task_type @@ -303,6 +308,8 @@ def __init__(self, task_type=None, status=None, input_data=None, reference_task_ self.task_definition = task_definition if queue_wait_time is not None: self.queue_wait_time = queue_wait_time + if runtime_metadata is not None: + self.runtime_metadata = runtime_metadata def __post_init__(self): """Post initialization for dataclass""" @@ -1192,6 +1199,30 @@ def queue_wait_time(self, queue_wait_time): self._queue_wait_time = queue_wait_time + @property + def runtime_metadata(self): + """Gets the runtime_metadata of this Task. # noqa: E501 + + 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. + + :return: The runtime_metadata of this Task. # noqa: E501 + :rtype: dict(str, str) + """ + return self._runtime_metadata + + @runtime_metadata.setter + def runtime_metadata(self, runtime_metadata): + """Sets the runtime_metadata of this Task. + + + :param runtime_metadata: The runtime_metadata of this Task. # noqa: E501 + :type: dict(str, str) + """ + + self._runtime_metadata = runtime_metadata + def to_dict(self): """Returns the model properties as a dict""" result = {} diff --git a/tests/serdeser/test_task_runtime_metadata.py b/tests/serdeser/test_task_runtime_metadata.py new file mode 100644 index 00000000..f2e1b7f6 --- /dev/null +++ b/tests/serdeser/test_task_runtime_metadata.py @@ -0,0 +1,34 @@ +import unittest + +from conductor.client.http.api_client import ApiClient +from conductor.client.http.models import Task + + +class TestTaskRuntimeMetadata(unittest.TestCase): + """ + 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. + """ + + def setUp(self): + self.client = ApiClient() + + def test_runtime_metadata_round_trips(self): + task = Task(task_id="t1", runtime_metadata={"GITHUB_TOKEN": "ghp_secret", "GH_APP_ID": "42"}) + + wire = self.client.sanitize_for_serialization(task) + self.assertIn("runtimeMetadata", wire, "field must serialize under the wire key") + self.assertEqual({"GITHUB_TOKEN": "ghp_secret", "GH_APP_ID": "42"}, wire["runtimeMetadata"]) + + back = self.client.deserialize_class(wire, Task) + self.assertEqual("ghp_secret", back.runtime_metadata["GITHUB_TOKEN"]) + self.assertEqual("42", back.runtime_metadata["GH_APP_ID"]) + + def test_runtime_metadata_omitted_when_empty(self): + wire = self.client.sanitize_for_serialization(Task(task_id="t1")) + self.assertNotIn("runtimeMetadata", wire, "absent map must not appear on the wire") + + +if __name__ == "__main__": + unittest.main()